深度学习:神经网络与激活函数
目录
前言
这一章主要了解一下神经网络,然后知道所有的激活函数
一、深度学习概述
1.什么是深度学习
人工智能、机器学习和深度学习之间的关系:

机器学习是实现人工智能的一种途径,深度学习是机器学习的子集,区别如下:

传统机器学习算法依赖人工设计特征、提取特征,而深度学习依赖算法自动提取特征。深度学习模仿人类大脑的运行方式,从大量数据中学习特征,这也是深度学习被看做黑盒子、可解释性差的原因。
随着算力的提升,深度学习可以处理图像,文本,音频,视频等各种内容,主要应用领域有:
-
图像处理:分类、目标检测、图像分割(语义分割)
-
自然语言处理:LLM、NLP、Transformer
-
语音识别:对话机器人、智能客服(语音+NLP)
-
自动驾驶:语义分割(行人、车辆、实线等)
-
LLM:大Large语言Language模型Model
-
机器人:非常火的行业
有了大模型的加持,AI+各行各业。
2.深度学习发展历史
深度学习其实并不是新的事物,深度学习所需要的神经网络技术起源于20世纪50年代,叫做感知机。当时使用单层感知机,因为只能学习线性可分函数,连简单的异或(XOR)等线性不可分问题都无能为力,1969年Marvin Minsky写了一本叫做《Perceptrons》的书,他提出了著名的两个观点:1.单层感知机没用,我们需要多层感知机来解决复杂问题 2.没有有效的训练算法。
20世纪80年代末期,用于人工神经网络的反向传播算法(也叫Back Propagation算法或者BP算法)的发明,给机器学习带来了希望,掀起了基于统计模型的机器学习热潮。这个热潮一直持续到今天。人们发现,利用BP算法可以让一个人工神经网络模型从大量训练样本中学习统计规律,从而对未知事件做预测。这种基于统计的机器学习方法比起过去基于人工规则的系统,在很多方面显出优越性。这个时候的人工神经网络,虽也被称作多层感知机(Multi-layer Perceptron),但实际是种只含有一层隐层节点的浅层模型。
2006年,杰弗里·辛顿以及他的学生鲁斯兰·萨拉赫丁诺夫正式提出了深度学习的概念。
2012年,在著名的ImageNet图像识别大赛中,杰弗里·辛顿领导的小组采用深度学习模型AlexNet一举夺冠。AlexNet采用ReLU激活函数,从根本上解决了梯度消失问题,并采用GPU极大的提高了模型的运算速度。
同年,吴恩达教授和Jeff Dean主导的深度神经网络DNN技术在ImageNet评测中把错误率从26%降低到15%,再一次吸引了学术界和工业界对于深度学习领域的关注。
2016年,随着谷歌公司基于深度学习开发的AlphaGo以4:1的比分战胜了国际顶尖围棋高手李世石,深度学习的热度一时无两。后来,AlphaGo又接连和众多世界级围棋高手过招,均取得了完胜。这也证明了在围棋界,基于深度学习技术的机器人已经超越了人类。
2017年,基于强化学习算法的AlphaGo升级版AlphaGo Zero横空出世。其采用“从零开始”、“无师自通”的学习模式,以100:0的比分轻而易举打败了之前的AlphaGo。除了围棋,它还精通国际象棋等其它棋类游戏,可以说是真正的棋类“天才”。此外在这一年,深度学习的相关算法在医疗、金融、艺术、无人驾驶等多个领域均取得了显著的成果。所以,也有专家把2017年看作是深度学习甚至是人工智能发展最为突飞猛进的一年。
2019年,基于Transformer 的自然语言模型的持续增长和扩散,这是一种语言建模神经网络模型,可以在几乎所有任务上提高NLP的质量。Google甚至将其用作相关性的主要信号之一,这是多年来最重要的更新。
2020年,深度学习扩展到更多的应用场景,比如积水识别,路面塌陷等,而且疫情期间,在智能外呼系统,人群测温系统,口罩人脸识别等都有深度学习的应用。
3.深度学习的优势

二、神经网络
我们要学习的深度学习(Deep Learning)是神经网络的一个子领域,主要关注更深层次的神经网络结构,也就是深层神经网络(Deep Neural Networks,DNNs)。所以,我们需要先搞清楚什么是神经网络.
1.感知神经网络
神经网络(Neural Networks)是一种模拟人脑神经元网络结构的计算模型,用于处理复杂的模式识别、分类和预测等任务。生物神经元如下图:

生物学:
人脑可以看做是一个生物神经网络,由众多的神经元连接而成
-
树突:从其他神经元接收信息的分支
-
细胞核:处理从树突接收到的信息
-
轴突:被神经元用来传递信息的生物电缆
-
突触:轴突和其他神经元树突之间的连接
人脑神经元处理信息的过程:
-
多个信号到达树突,然后整合到细胞体的细胞核中
-
当积累的信号超过某个阈值,细胞就会被激活
-
产生一个输出信号,由轴突传递。
神经网络由多个互相连接的节点(即人工神经元)组成。
2.人工神经元
人工神经元(Artificial Neuron)是神经网络的基本构建单元,模仿了生物神经元的工作原理。其核心功能是接收输入信号,经过加权求和和非线性激活函数处理后,输出结果。
2.1 构建人工神经元
人工神经元接受多个输入信息,对它们进行加权求和,再经过激活函数处理,最后将这个结果输出。 
2.2 组成部分
-
输入(Inputs): 代表输入数据,通常用向量表示,每个输入值对应一个权重。
-
权重(Weights): 每个输入数据都有一个权重,表示该输入对最终结果的重要性。
-
偏置(Bias): 一个额外的可调参数,作用类似于线性方程中的截距,帮助调整模型的输出。
-
加权求和: 神经元将输入乘以对应的权重后求和,再加上偏置。
-
激活函数(Activation Function): 用于将加权求和后的结果转换为输出结果,引入非线性特性,使神经网络能够处理复杂的任务。常见的激活函数有Sigmoid、ReLU(Rectified Linear Unit)、Tanh等。
2.3 数学表示
如果有 n 个输入,权重分别为
,偏置为 b,则神经元的输出 y 表示为:
其中, 是激活函数
线性回归:
线性回归不需要激活函数
逻辑回归:
2.4 对比生物神经元
人工神经元和生物神经元对比如下表:
| 生物神经元 | 人工神经元 |
|---|---|
| 细胞核 | 节点 (加权求和 + 激活函数) |
| 树突 | 输入 |
| 轴突 | 带权重的连接 |
| 突触 | 输出 |
3.深入神经网络
神经网络是由大量人工神经元按层次结构连接而成的计算模型。每一层神经元的输出作为下一层的输入,最终得到网络的输出。
3.1 基本结构
神经网络有下面三个基础层(Layer)构建而成:
-
输入层(Input): 神经网络的第一层,负责接收外部数据,不进行计算。
-
隐藏层(Hidden): 位于输入层和输出层之间,进行特征提取和转换。隐藏层一般有多层,每一层有多个神经元。
-
输出层(Output): 网络的最后一层,产生最终的预测结果或分类结果
3.2 网络构建
我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个权重,经典如下:
注意:同一层的各个神经元之间是没有连接的。

3.3 全连接神经网络
前馈神经网络(Feedforward Neural Network,FNN)是一种最基本的神经网络结构,其特点是信息从输入层经过隐藏层单向传递到输出层,没有反馈或循环连接。
全连接神经网络(Fully Connected Neural Network,FCNN)是前馈神经网络的一种,每一层的神经元与上一层的所有神经元全连接,常用于图像分类、文本分类等任务。


如上图,网络中每个神经元:
说明:三个等式中的w1和w2在这里只是为了方便表示对应x1和x2的权重,实际三个等式中的w值是不同的。
向量x为:
向量w:,其形状为(3,2),3是神经元节点个数,2是向量x的个数
向量z:
向量b:
所以用向量表示为:
-
x是输入数据,形状为 (batch_size, in_features)。
-
W是权重矩阵,形状为 (out_features, in_features)。
-
b是偏置项,形状为 (out_features,)。
-
z是输出数据,形状为 (batch_size, out_features)。
3.3.1 特点
-
全连接层: 层与层之间的每个神经元都与前一层的所有神经元相连。
-
权重数量: 由于全连接的特点,权重数量较大,容易导致计算量大、模型复杂度高。
-
学习能力: 能够学习输入数据的全局特征,但对于高维数据却不擅长捕捉局部特征(如图像就需要CNN)。
3.3.2 计算步骤
-
数据传递: 输入数据经过每一层的计算,逐层传递到输出层。
-
激活函数: 每一层的输出通过激活函数处理。
-
损失计算: 在输出层计算预测值与真实值之间的差距,即损失函数值。
-
反向传播(Back Propagation): 通过反向传播算法计算损失函数对每个权重的梯度,并更新权重以最小化损失。
3.3.3 基本组件认知
官方文档:torch.nn — PyTorch 2.8 documentation
线性层组件
nn.Linear是 PyTorch 中的一个非常重要的模块,用于实现全连接层(也称为线性层)。它是神经网络中常用的一种层类型,主要用于将输入数据通过线性变换映射到输出空间。
torch.nn.Linear(in_features, out_features, bias=True)
参数说明:
in_features:
-
输入特征的数量(即输入数据的维度)。
-
例如,如果输入是一个长度为 100 的向量,则 in_features=100。
out_features:
-
输出特征的数量(即输出数据的维度)。
-
例如,如果希望输出是一个长度为 50 的向量,则 out_features=50。
bias:
-
是否使用偏置项(默认值为 True)。
-
如果设置为 False,则不会学习偏置项。
示例:构建3层全连接神经网络:在__init__方法中定义网络结构,在forward定义前向传播
import torch
from torch import nn
# 定义全连接神经网络模型
class MyFcnn(nn.Module):
def __init__(self, input_size,out_size):
# 父类初始化
super(MyFcnn, self).__init__()
# 定义线性层1
self.fc1 = nn.Linear(input_size, 64)
# 定义线性层2,输入要和第一层的输出一致
self.fc2 = nn.Linear(64, 32)
# 定义线性层3,输入要和第二层的输出一致
self.fc3 = nn.Linear(32, out_size)
def forward(self, x):
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
input_size = 32
out_size = 1
model = MyFcnn(input_size,out_size)
print(model)
如果模型中线性层按顺序叠加,也可以使用nn.Sequential构建模型。nn.Sequential 是一个顺序容器,内置了自动的前向传播逻辑,它会自动将输入数据依次传递给其中的每一层,并执行前向传播,不需要显式定义 forward() 方法。
import torch
from torch import nn
input_size = 32
# 定义全连接神经网络模型
model = nn.Sequential(
nn.Linear(input_size, 64),
nn.Linear(64, 32),
nn.Linear(32, 1)
)
print(model)
优化器
官方文档:torch.optim — PyTorch 2.8 documentation
在PyTorch中,优化器(Optimizer)是用于更新模型参数以最小化损失函数的核心工具。
PyTorch 在 torch.optim 模块中提供了多种优化器,常用的包括:
-
SGD(随机梯度下降)
-
Adagrad(自适应梯度)
-
RMSprop(均方根传播)
-
Adam(自适应矩估计)
核心方法有:
zero_grad():清空模型参数的梯度(将梯度置零)。必须在loss.backward()之前调用zero_grad(),避免梯度累积。
step():参数更新;是优化器的核心方法,用于根据计算得到的梯度更新模型参数。优化器会根据梯度和学习率等参数,调整模型的权重和偏置。
以SGD(随机梯度下降)优化器为例:
import torch
import torch.nn as nn
import torch.optim as optim
# 优化方法SGD的学习
def test3():
model = nn.Linear(20, 60)
criterion = nn.MSELoss()
# 优化器:更新模型参数
optimizer = optim.SGD(model.parameters(), lr=0.01)
input = torch.randn(128, 20)
output = model(input)
# 计算损失及反向传播
loss = criterion(output, torch.randn(128, 60))
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新模型参数
optimizer.step()
print(loss.item())
if __name__ == "__main__":
test3()
-
optim.SGD():优化器方法;是 PyTorch 提供的随机梯度下降(Stochastic Gradient Descent, SGD)优化器。
-
model.parameters():模型参数获取;是一个生成器,用于获取模型中所有可训练的参数(权重和偏置)。
注意:这里只是组件认识和用法演示,没有具体的模型训练功能实现
三、数据准备
1.数据加载器
分数据集和加载器2个步骤~
1.1 构建数据类
1.1.1 Dataset类
Dataset是一个抽象类,是所有自定义数据集应该继承的基类。它定义了数据集必须实现的方法。
必须实现的方法
-
__len__: 返回数据集的大小 -
__getitem__: 支持整数索引,返回对应的样本
在 PyTorch 中,构建自定义数据加载类通常需要继承 torch.utils.data.Dataset 并实现以下几个方法:
__init__ 方法 用于初始化数据集对象:通常在这里加载数据,或者定义如何从存储中获取数据的路径和方法。
def __init__(self, data, labels):
self.data = data
self.labels = labels
__len__ 方法 返回样本数量:需要实现,以便 Dataloader加载器能够知道数据集的大小。
def __len__(self):
return len(self.data)
__getitem__ 方法 根据索引返回样本:将从数据集中提取一个样本,并可能对样本进行预处理或变换。
def __getitem__(self, index):
sample = self.data[index]
label = self.labels[index]
return sample, label
1.1.2 TensorDataset类
TensorDataset是Dataset的一个简单实现,它封装了张量数据,适用于数据已经是张量形式的情况。
特点
-
简单快捷:当数据已经是张量形式时,无需自定义Dataset类
-
多张量支持:可以接受多个张量作为输入,按顺序返回
-
索引一致:所有张量的第一个维度必须相同,表示样本数量
源码:
class TensorDataset(Dataset):
def __init__(self, *tensors):
# size(0)在python中同shape[0],获取的是样本数量
# 用第一个张量中的样本数量和其他张量对比,如果全部相同则通过断言,否则抛异常
assert all(tensors[0].size(0) == tensor.size(0) for tensor in tensors)
self.tensors = tensors
def __getitem__(self, index):
return tuple(tensor[index] for tensor in self.tensors)
def __len__(self):
return self.tensors[0].size(0)
示例:
def test1():
torch.manual_seed(0)
# 创建特征张量和标签张量
features = torch.randn(100, 5) # 100个样本,每个样本5个特征
labels = torch.randint(0, 2, (100,)) # 100个二进制标签
# 创建TensorDataset
dataset = TensorDataset(features, labels)
# 使用方式与自定义Dataset相同
print(len(dataset)) # 输出: 100
print(dataset[0]) # 输出: (tensor([...]), tensor(0))
1.2 数据加载器
在训练或者验证的时候,需要用到数据加载器批量的加载样本。
DataLoader 是一个迭代器,用于从 Dataset 中批量加载数据。它的主要功能包括:
-
批量加载:将多个样本组合成一个批次。
-
打乱数据:在每个 epoch 中随机打乱数据顺序。
-
多线程加载:使用多线程加速数据加载。
创建DataLoader:
# 创建 DataLoader
dataloader = DataLoader(
dataset, # 数据集
batch_size=10, # 批量大小
shuffle=True, # 是否打乱数据
num_workers=2 # 使用 2 个子进程加载数据
)
遍历:
# 遍历 DataLoader
# enumerate返回一个枚举对象(iterator),生成由索引和值组成的元组
for batch_idx, (samples, labels) in enumerate(dataloader):
print(f"Batch {batch_idx}:")
print("Samples:", samples)
print("Labels:", labels)
案例:
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
# 定义数据加载类
class CustomDataset(Dataset):
#略......
def test1():
# 简单的数据集准备
data_x = torch.randn(666, 20, requires_grad=True, dtype=torch.float32)
data_y = torch.randn(data_x.size(0), 1, dtype=torch.float32)
dataset = CustomDataset(data_x, data_y)
# 构建数据加载器
data_loader = DataLoader(dataset, batch_size=8, shuffle=True)
for i, (batch_x, batch_y) in enumerate(data_loader):
print(batch_x, batch_y)
break
if __name__ == "__main__":
test1()
2.数据集加载案例
通过一些数据集的加载案例,真正了解数据类及数据加载器。
2.1 加载csv数据集
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
class MyCsvDataset(Dataset):
def __init__(self, filename):
df = pd.read_csv(filename)
# 删除文字列
df = df.drop(["学号", "姓名"], axis=1)
# 转换为tensor
data = torch.tensor(df.values)
# 最后一列以前的为data,最后一列为label
self.data = data[:, :-1]
self.label = data[:, -1]
self.len = len(self.data)
def __len__(self):
return self.len
def __getitem__(self, index):
idx = min(max(index, 0), self.len - 1)
return self.data[idx], self.label[idx]
def test001():
excel_path = r"./大数据答辩成绩表.csv"
dataset = MyCsvDataset(excel_path)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
for i, (data, label) in enumerate(dataloader):
print(i, data, label)
if __name__ == "__main__":
test001()
上述示例数据构建器改成TensorDataset
def build_dataset(filepath):
df = pd.read_csv(filepath)
df.drop(columns=['学号', '姓名'], inplace=True)
data = df.iloc[..., :-1]
labels = df.iloc[..., -1]
x = torch.tensor(data.values, dtype=torch.float)
y = torch.tensor(labels.values)
dataset = TensorDataset(x, y)
return dataset
def test001():
filepath = r"./大数据答辩成绩表.csv"
dataset = build_dataset(filepath)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
for i, (data, label) in enumerate(dataloader):
print(i, data, label)
2.2 加载图片数据集
使用ImageFolder加载图片集
ImageFolder 会根据文件夹的结构来加载图像数据。它假设每个子文件夹对应一个类别,文件夹名称即为类别名称。例如,一个典型的文件夹结构如下:
root/
class1/
img1.jpg
img2.jpg
...
class2/
img1.jpg
img2.jpg
...
在这个结构中:
-
root是根目录。 -
class1、class2等是类别名称。 -
每个类别文件夹中的图像文件会被加载为一个样本。
ImageFolder构造函数如下:
torchvision.datasets.ImageFolder(root, transform=None, target_transform=None, is_valid_file=None)
参数解释
-
root:字符串,指定图像数据集的根目录。 -
transform:可选参数,用于对图像进行预处理。通常是一个torchvision.transforms的组合。 -
target_transform:可选参数,用于对目标(标签)进行转换。 -
is_valid_file:可选参数,用于过滤无效文件。如果提供,只有返回True的文件才会被加载。
import torch
from torchvision import datasets, transforms
import os
from torch.utils.data import DataLoader
from matplotlib import pyplot as plt
torch.manual_seed(42)
def load():
path = os.path.join(os.path.dirname(__file__), 'dataset')
print(path)
transform = transforms.Compose([
transforms.Resize((112, 112)),
transforms.ToTensor()
])
dataset = datasets.ImageFolder(path, transform=transform)
dataloader = DataLoader(dataset, batch_size=1, shuffle=True)
for x,y in dataloader:
x = x.squeeze(0).permute(1, 2, 0).numpy()
plt.imshow(x)
plt.show()
print(y[0])
break
if __name__ == '__main__':
load()
2.3 加载官方数据集
在 PyTorch 中官方提供了一些经典的数据集,如 CIFAR-10、MNIST、ImageNet 等,可以直接使用这些数据集进行训练和测试。
数据集:Datasets — Torchvision 0.23 documentation
常见数据集:
-
MNIST: 手写数字数据集,包含 60,000 张训练图像和 10,000 张测试图像。
-
CIFAR10: 包含 10 个类别的 60,000 张 32x32 彩色图像,每个类别 6,000 张图像。
-
CIFAR100: 包含 100 个类别的 60,000 张 32x32 彩色图像,每个类别 600 张图像。
-
COCO: 通用对象识别数据集,包含超过 330,000 张图像,涵盖 80 个对象类别。
torchvision.transforms 和 torchvision.datasets 是 PyTorch 中处理计算机视觉任务的两个核心模块,它们为图像数据的预处理和标准数据集的加载提供了强大支持。
transforms 模块提供了一系列用于图像预处理的工具,可以将多个变换组合成处理流水线。
datasets 模块提供了多种常用计算机视觉数据集的接口,可以方便地下载和加载。
参考如下:
import torch
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision import transforms, datasets
def test():
transform = transforms.Compose(
[
transforms.ToTensor(),
]
)
# 训练数据集
data_train = datasets.MNIST(
root="./data",
train=True,
download=True,
transform=transform,
)
trainloader = DataLoader(data_train, batch_size=8, shuffle=True)
for x, y in trainloader:
print(x.shape)
print(y)
break
# 测试数据集
data_test = datasets.MNIST(
root="./data",
train=False,
download=True,
transform=transform,
)
testloader = DataLoader(data_test, batch_size=8, shuffle=True)
for x, y in testloader:
print(x.shape)
print(y)
break
def test006():
transform = transforms.Compose(
[
transforms.ToTensor(),
]
)
# 训练数据集
data_train = datasets.CIFAR10(
root="./data",
train=True,
download=True,
transform=transform,
)
trainloader = DataLoader(data_train, batch_size=4, shuffle=True, num_workers=2)
for x, y in trainloader:
print(x.shape)
print(y)
break
# 测试数据集
data_test = datasets.CIFAR10(
root="./data",
train=False,
download=True,
transform=transform,
)
testloader = DataLoader(data_test, batch_size=4, shuffle=False, num_workers=2)
for x, y in testloader:
print(x.shape)
print(y)
break
if __name__ == "__main__":
test()
test006()
四、激活函数
激活函数的作用是在隐藏层引入非线性,使得神经网络能够学习和表示复杂的函数关系,使网络具备非线性能力,增强其表达能力。
1.基础概念
通过认识线性和非线性的基础概念,深刻理解激活函数存在的价值。
1.1 线性理解
如果在隐藏层不使用激活函数,那么整个神经网络会表现为一个线性模型。我们可以通过数学推导来展示这一点。
假设:
-
神经网络有L 层,每层的输出为
。
-
每层的权重矩阵为
,偏置向量为
。
-
输入数据为
,输出为
。
一层网络的情况
对于单层网络(输入层到输出层),如果没有激活函数,输出可以表示为:
两层网络的情况
假设我们有两层网络,且每层都没有激活函数,则:
-
第一层的输出:
-
第二层的输出:
将代入到
中,可以得到:
我们可以看到,输出是输入
的线性变换,因为:
其中
,
。
多层网络的情况
如果有L层,每层都没有激活函数,则第l层的输出为:
通过递归代入,可以得到:
表达式可简化为:
其中,'' 是所有权重矩阵的乘积,
''是所有偏置项的线性组合。
如此可以看得出来,无论网络多少层,意味着:
整个网络就是线性模型,无法捕捉数据中的非线性关系。
激活函数是引入非线性特性、使神经网络能够处理复杂问题的关键。
1.2 非线性可视化
我们可以通过可视化的方式去理解非线性的拟合能力:A Neural Network Playground
2.常见激活函数
激活函数通过引入非线性来增强神经网络的表达能力,对于解决线性模型的局限性至关重要。由于反向传播算法(BP)用于更新网络参数,因此激活函数必须是可微的,也就是说能够求导的。
2.1 sigmoid
Sigmoid激活函数是一种常见的非线性激活函数,特别是在早期神经网络中应用广泛。它将输入映射到0到1之间的值,因此非常适合处理概率问题。
2.1.1 公式
Sigmoid函数的数学表达式为:
其中,e 是自然常数(约等于2.718),x 是输入。
2.1.2 特征
-
将任意实数输入映射到 (0, 1)之间,因此非常适合处理概率场景。
-
sigmoid函数一般只用于二分类的输出层。
-
微分性质: 导数计算比较方便,可以用自身表达式来表示:
2.1.3 缺点
-
梯度消失:
-
在输入非常大或非常小时,Sigmoid函数的梯度会变得非常小,接近于0。这导致在反向传播过程中,梯度逐渐衰减。
-
最终使得早期层的权重更新非常缓慢,进而导致训练速度变慢甚至停滞。
-
-
信息丢失:输入100和输入10000经过sigmoid的激活值几乎都是等于 1 的,但是输入的数据却相差 100 倍。
-
计算成本高: 由于涉及指数运算,Sigmoid的计算比ReLU等函数更复杂,尽管差异并不显著。
2.1.4 函数绘制
通过代码实现函数和导函数绘制:
import torch
import matplotlib.pyplot as plt
# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test001():
# 一行两列绘制图像
_, ax = plt.subplots(1, 2)
# 绘制函数图像
x = torch.linspace(-10, 10, 100)
y = torch.sigmoid(x)
# 网格
ax[0].grid(True)
ax[0].set_title("sigmoid 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 在第一行第一列绘制sigmoid函数曲线图
ax[0].plot(x, y)
# 绘制sigmoid导数曲线图
x = torch.linspace(-10, 10, 100, requires_grad=True)
# y = torch.sigmoid(x) * (1 - torch.sigmoid(x))
# 自动求导
torch.sigmoid(x).sum().backward()
ax[1].grid(True)
ax[1].set_title("sigmoid 函数导数曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("y")
# ax[1].plot(x.detach().numpy(), y.detach())
# 用自动求导的结果绘制曲线图
ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
# 设置曲线颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test001()

2.2 tanh
tanh(双曲正切)是一种常见的非线性激活函数,常用于神经网络的隐藏层。tanh 函数也是一种S形曲线,输出范围为(−1,1)。
2.2.1 公式
tanh数学表达式为:
2.2.2 特征
-
输出范围: 将输入映射到(-1, 1)之间,因此输出是零中心的。相比于Sigmoid函数,这种零中心化的输出有助于加速收敛。
-
对称性: Tanh函数是关于原点对称的奇函数,因此在输入为0时,输出也为0。这种对称性有助于在训练神经网络时使数据更平衡。
-
平滑性: Tanh函数在整个输入范围内都是连续且可微的,这使其非常适合于使用梯度下降法进行优化。
2.2.3 缺点
-
梯度消失: 虽然一定程度上改善了梯度消失问题,但在输入值非常大或非常小时导数还是非常小,这在深层网络中仍然是个问题。这是因为每一层的梯度都会乘以一个小于1的值,经过多层乘积后,梯度会变得非常小,导致训练过程变得非常缓慢,甚至无法收敛。
-
计算成本: 由于涉及指数运算,Tanh的计算成本还是略高,尽管差异不大。
2.2.4 函数绘制
import torch
import matplotlib.pyplot as plt
# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test001():
# 一行两列绘制图像
_, ax = plt.subplots(1, 2)
# 绘制函数图像
x = torch.linspace(-10, 10, 100)
y = torch.tanh(x)
# 网格
ax[0].grid(True)
ax[0].set_title("tanh 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 在第一行第一列绘制tanh函数曲线图
ax[0].plot(x, y)
# 绘制tanh导数曲线图
x = torch.linspace(-10, 10, 100, requires_grad=True)
# y = torch.tanh(x) * (1 - torch.tanh(x))
# 自动求导:需要标量才能反向传播
torch.tanh(x).sum().backward()
ax[1].grid(True)
ax[1].set_title("tanh 函数导数曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
# ax[1].plot(x.detach().numpy(), y.detach())
# 用自动求导的结果绘制曲线图
ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
# 设置曲线颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test001()

2.3 ReLU
ReLU(Rectified Linear Unit)是深度学习中最常用的激活函数之一,它的全称是修正线性单元。ReLU 激活函数的定义非常简单,但在实践中效果非常好。
2.3.1 公式
ReLU 函数定义如下:
即ReLU对输入x进行非线性变换:

2.3.2 特征
-
计算简单:ReLU 的计算非常简单,只需要对输入进行一次比较运算,这在实际应用中大大加速了神经网络的训练。
-
ReLU 函数的导数是分段函数:
-
缓解梯度消失问题:相比于 Sigmoid 和 Tanh 激活函数,ReLU 在正半区的导数恒为 1,这使得深度神经网络在训练过程中可以更好地传播梯度,不存在饱和问题。
-
稀疏激活:ReLU在输入小于等于 0 时输出为 0,这使得 ReLU 可以在神经网络中引入稀疏性(即一些神经元不被激活),这种稀疏性可以减少网络中的冗余信息,提高网络的效率和泛化能力。
2.3.3 缺点
神经元死亡:由于ReLU在x≤0时输出为0,如果某个神经元输入值是负,那么该神经元将永远不再激活,成为“死亡”神经元。随着训练的进行,网络中可能会出现大量死亡神经元,从而会降低模型的表达能力。
2.3.4 函数绘图
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# 中文问题
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test006():
# 输入数据x
x = torch.linspace(-20, 20, 1000)
y = F.relu(x)
# 绘制一行2列
_, ax = plt.subplots(1, 2)
ax[0].plot(x.numpy(), y.numpy())
# 显示坐标格子
ax[0].grid()
ax[0].set_title("relu 激活函数")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 绘制导数函数
x = torch.linspace(-20, 20, 1000, requires_grad=True)
F.relu(x).sum().backward()
ax[1].plot(x.detach().numpy(), x.grad.numpy())
ax[1].grid()
ax[1].set_title("relu 激活函数导数", color="red")
# 设置绘制线色颜色
ax[1].lines[0].set_color("red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
plt.show()
if __name__ == "__main__":
test006()

2.4 LeakyReLU
Leaky ReLU是一种对 ReLU 函数的改进,旨在解决 ReLU 的一些缺点,特别是Dying ReLU 问题。Leaky ReLU 通过在输入为负时引入一个小的负斜率来改善这一问题。
2.4.1 公式
Leaky ReLU 函数的定义如下:
其中,\alpha 是一个非常小的常数(如 0.01),它控制负半轴的斜率。这个常数 \alpha是一个超参数,可以在训练过程中可自行进行调整。
2.4.2 特征
-
避免神经元死亡:通过在x\leq 0 区域引入一个小的负斜率,这样即使输入值小于等于零,Leaky ReLU仍然会有梯度,允许神经元继续更新权重,避免神经元在训练过程中完全“死亡”的问题。
-
计算简单:Leaky ReLU 的计算与 ReLU 相似,只需简单的比较和线性运算,计算开销低。
2.4.3 缺点
-
参数选择:\alpha 是一个需要调整的超参数,选择合适的\alpha 值可能需要实验和调优。
-
出现负激活:如果\alpha 设定得不当,仍然可能导致激活值过低。
2.4.4 函数绘制
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# 中文设置
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test006():
x = torch.linspace(-5, 5, 200)
# 设置leaky_relu的负斜率超参数
slope = 0.03
y = F.leaky_relu(x, slope)
# 一行两列
_, ax = plt.subplots(1, 2)
# 开始绘制函数曲线图
ax[0].plot(x, y)
ax[0].set_title("Leaky ReLU 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
ax[0].grid(True)
# 绘制leaky_relu的梯度曲线图
x = torch.linspace(-5, 5, 200, requires_grad=True)
F.leaky_relu(x, slope).sum().backward()
ax[1].plot(x.detach().numpy(), x.grad)
ax[1].set_title("Leaky ReLU 梯度曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
ax[1].grid(True)
# 设置线的颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test006()

2.5 softmax
Softmax激活函数通常用于分类问题的输出层,它能够将网络的输出转换为概率分布,使得输出的各个类别的概率之和为 1。Softmax 特别适合用于多分类问题。
2.5.1 公式
假设神经网络的输出层有n个节点,每个节点的输入为z_i,则 Softmax 函数的定义如下:
给定输入向量 z=[z_1,z_2,…,z_n]
1.指数变换:对每个 z_i进行指数变换,得到,使z的取值区间从
变为
2.将所有指数变换后的值求和,得到
3.将t中每个 除以归一化因子s,得到概率分布:
从上述公式可以看出:
-
每个输出值在 (0,1)之间
-
Softmax()对向量的值做了改变,但其位置不变
-
所有输出值之和为1,即
2.5.2 特征
将输出转化为概率:通过Softmax,可以将网络的原始输出转化为各个类别的概率,从而可以根据这些概率进行分类决策。

概率分布:Softmax的输出是一个概率分布,即每个输出值\text{Softmax}(z_i)都是一个介于0和1之间的数,并且所有输出值的和为 1:
突出差异:Softmax会放大差异,使得概率最大的类别的输出值更接近1,而其他类别更接近0。
在实际应用中,Softmax常与交叉熵损失函数Cross-Entropy Loss结合使用,用于多分类问题。在反向传播中,Softmax的导数计算是必需的。

2.5.3 缺点
数值不稳定性:在计算过程中,如果的数值过大,
可能会导致数值溢出。因此在实际应用中,经常会对
进行调整,如减去最大值以确保数值稳定。
是一个非正数,由于
的形式,当
接近 max(z) 时,e^{z_i−max(z)} 的值会接近 1,而当 z_i 远小于 max(z) 时,e^{z_i−max(z)} 的值会接近 0。这使得 Softmax 函数的输出中,最大值对应的概率会相对较大,而其他值对应的概率会相对较小,从而提高数值稳定性。
这种调整不会改变Softmax的概率分布结果,因为从数学的角度讲相当于分子、分母都除以了e^{\max(z)}。
在 PyTorch 中,torch.nn.functional.softmax 函数就自动处理了数值稳定性问题。
难以处理大量类别:Softmax在处理类别数非常多的情况下(如大模型中的词汇表)计算开销会较大。
2.5.4 代码实现
import torch
import torch.nn as nn
# 表示4分类,每个样本全连接后得到4个得分,下面示例模拟的是两个样本的得分
input_tensor = torch.tensor([[-1.0, 2.0, -3.0, 4.0], [-2, 3, -3, 9]])
softmax = nn.Softmax()
output_tensor = softmax(input_tensor)
# 关闭科学计数法
torch.set_printoptions(sci_mode=False)
print("输入张量:", input_tensor)
print("输出张量:", output_tensor)
"""
输入张量: tensor([[-1., 2., -3., 4.],
[-2., 3., -3., 9.]])
输出张量: tensor([[ 0.0059, 0.1184, 0.0008, 0.8749],
[ 0.0000, 0.0025, 0.0000, 0.9975]])
"""
3.如何选择
更多激活函数可以查看官方文档:https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity
那这么多激活函数应该如何选择呢?
3.1 隐藏层
-
优先选ReLU;
-
如果ReLU效果不咋地,那么尝试其他激活,如Leaky ReLU等;
-
使用ReLU时注意神经元死亡问题, 避免出现过多神经元死亡;
-
避免使用sigmoid,尝试使用tanh;
3.2 输出层
-
二分类问题选择sigmoid激活函数;
-
多分类问题选择softmax激活函数;
五、参数初始化
神经网络的参数初始化是训练深度学习模型的关键步骤之一。初始化参数(通常是权重和偏置)会对模型的训练速度、收敛性以及最终的性能产生重要影响。下面是关于神经网络参数初始化的一些常见方法及其相关知识点。
官方文档参考:torch.nn.init — PyTorch 2.8 documentation
1.固定值初始化
固定值初始化是指在神经网络训练开始时,将所有权重或偏置初始化为一个特定的常数值。这种初始化方法虽然简单,但在实际深度学习应用中通常并不推荐。
1.1 全零初始化
将神经网络中的所有权重参数初始化为0。
方法:将所有权重初始化为零。
缺点:导致对称性破坏,每个神经元在每一层中都会执行相同的计算,模型无法学习。
应用场景:通常不用来初始化权重,但可以用来初始化偏置。
对称性问题
-
现象:同一层的所有神经元具有完全相同的初始权重和偏置。
-
后果:
-
在反向传播时,所有神经元会收到相同的梯度,导致权重更新完全一致。
-
无论训练多久,同一层的神经元本质上会保持相同的功能(相当于“一个神经元”的多个副本),极大降低模型的表达能力。
-
import torch
import torch.nn as nn
def test004():
# 3. 全0参数初始化
linear = nn.Linear(in_features=6, out_features=4)
# 初始化权重参数
nn.init.zeros_(linear.weight)
# 打印权重参数
print(linear.weight)
if __name__ == "__main__":
test004()
"""
Parameter containing:
tensor([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]], requires_grad=True)
"""
1.2 全1初始化
全1初始化会导致网络中每个神经元接收到相同的输入信号,进而输出相同的值,这就无法进行学习和收敛。所以全1初始化只是一个理论上的初始化方法,但在实际神经网络的训练中并不适用。
import torch
import torch.nn as nn
def test003():
# 3. 全1参数初始化
linear = nn.Linear(in_features=6, out_features=4)
# 初始化权重参数
nn.init.ones_(linear.weight)
# 打印权重参数
print(linear.weight)
if __name__ == "__main__":
test003()
"""
Parameter containing:
tensor([[1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1.]], requires_grad=True)
"""
1.3 任意常数初始化
将所有参数初始化为某个非零的常数(如 0.1,-1 等)。虽然不同于全0和全1,但这种方法依然不能避免对称性破坏的问题。
import torch
import torch.nn as nn
def test002():
# 2. 固定值参数初始化
linear = nn.Linear(in_features=6, out_features=4)
# 初始化权重参数
nn.init.constant_(linear.weight, 0.63)
# 打印权重参数
print(linear.weight)
pass
if __name__ == "__main__":
test002()
"""
Parameter containing:
tensor([[0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
[0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
[0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300],
[0.6300, 0.6300, 0.6300, 0.6300, 0.6300, 0.6300]], requires_grad=True)
"""
import torch
import torch.nn as nn
def test002():
net = nn.Linear(2, 2, bias=True)
# 假设一个数值
x = torch.tensor([[0.1, 0.95]])
# 初始化权重参数
net.weight.data = torch.tensor([
[0.1, 0.2],
[0.3, 0.4]
])
# 输出什么:权重参数会转置
output = net(x)
print(output, net.bias)
pass
if __name__ == "__main__":
test002()
-
所有输入特征被同等对待
-
无法学习特征间的不同重要性
2.随机初始化
方法:将权重初始化为随机的小值,通常从正态分布或均匀分布中采样。
应用场景:这是最基本的初始化方法,通过随机初始化避免对称性破坏。
代码:随机分布之均匀初始化
import torch
import torch.nn as nn
def test001():
# 1. 均匀分布随机初始化
linear = nn.Linear(in_features=6, out_features=4)
# 初始化权重参数
nn.init.uniform_(linear.weight)
# 打印权重参数
print(linear.weight)
if __name__ == "__main__":
test001()
"""
Parameter containing:
tensor([[0.4080, 0.7444, 0.7616, 0.0565, 0.2589, 0.0562],
[0.1485, 0.9544, 0.3323, 0.9802, 0.1847, 0.6254],
[0.6256, 0.2047, 0.5049, 0.3547, 0.9279, 0.8045],
[0.1994, 0.7670, 0.8306, 0.1364, 0.4395, 0.0412]], requires_grad=True)
"""
代码:正态分布初始化
import torch
import torch.nn as nn
def test005():
# 5. 正太分布初始化
linear = nn.Linear(in_features=6, out_features=4)
# 初始化权重参数
nn.init.normal_(linear.weight, mean=0, std=1)
# 打印权重参数
print(linear.weight)
if __name__ == "__main__":
test005()
"""
Parameter containing:
tensor([[ 1.5321, 0.2394, 0.0622, 0.4482, 0.0757, -0.6056],
[ 1.0632, 1.8069, 1.1189, 0.2448, 0.8095, -0.3486],
[-0.8975, 1.8253, -0.9931, 0.7488, 0.2736, -1.3892],
[-0.3752, 0.0500, -0.1723, -0.4370, -1.5334, -0.5393]],
requires_grad=True)
"""
3.Xavier 初始化
前置知识:
均匀分布:
均匀分布的概率密度函数(PDF):

计算期望值(均值):

计算方差(二阶矩减去均值的平方):

-
先计算
:

代入方差公式:

Xavier 初始化(由 Xavier Glorot 在 2010 年提出)是一种自适应权重初始化方法,专门为解决神经网络训练初期的梯度消失或爆炸问题而设计。Xavier 初始化也叫做Glorot初始化。Xavier 初始化的核心思想是根据输入和输出的维度来初始化权重,使得每一层的输出的方差保持一致。具体来说,权重的初始化范围取决于前一层的神经元数量(输入维度)和当前层的神经元数量(输出维度)。
方法:根据输入和输出神经元的数量来选择权重的初始值。
数学原理:
(1) 前向传播的方差一致性
假设输入 x 的均值为 0,方差为,权重 W的均值为 0,方差为
,则输出 z=Wx的方差为:

为了使 Var(z)=Var(x),需要:

其中 n_{in}是输入维度(fan_in)。这里乘以 nin 的原因是,输出 z 是由 nin 个输入 x 的线性组合得到的,每个输入 x 都与一个权重 W 相乘。因此,输出 z 的方差是 nin 个独立的 Wx 项的方差之和。
(2) 反向传播的梯度方差一致性
在反向传播过程中,梯度 =w 是通过链式法则计算得到的,其中 L 是损失函数,x 是输入,z 是输出。梯度
可以表示为:

假设 z=Wx,其中 W 是权重矩阵,那么 =W。因此,梯度
可以写为:
=
W
反向传播时梯度 的方差应与
相同,因此

其中 n_{out}是输出维度(fan_out)。为了保持梯度的方差一致性,我们需要确保每个输入维度 nin 的梯度方差与输出维度 nout 的梯度方差相同。因此,我们需要将 W 的方差乘以 nout,以确保梯度的方差在反向传播过程中保持一致。
(3) 综合考虑
为了同时平衡前向传播和反向传播,Xavier 采用:

权重从以下分布中采样:
均匀分布:

正态分布:

其中 n_{\text{in}} 是当前层的输入神经元数量,n_{\text{out}}是输出神经元数量。
在前向传播中,输出的方差受 n_{in} 影响。在反向传播中,梯度的方差受 n_{out} 影响。
优点:平衡了输入和输出的方差,适合Sigmoid 和 Tanh 激活函数。
应用场景:常用于浅层网络或使用Sigmoid 、Tanh 激活函数的网络。
import torch
import torch.nn as nn
def test007():
# Xavier初始化:正态分布
linear = nn.Linear(in_features=6, out_features=4)
nn.init.xavier_normal_(linear.weight)
print(linear.weight)
# Xavier初始化:均匀分布
linear = nn.Linear(in_features=6, out_features=4)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight)
if __name__ == "__main__":
test007()
"""
Parameter containing:
tensor([[-0.4838, 0.4121, -0.3171, -0.2214, -0.8666, -0.4340],
[ 0.1059, 0.6740, -0.1025, -0.1006, 0.5757, -0.1117],
[ 0.7467, -0.0554, -0.5593, -0.1513, -0.5867, -0.1564],
[-0.1058, 0.5266, 0.0243, -0.5646, -0.4982, -0.1844]],
requires_grad=True)
Parameter containing:
tensor([[-0.5263, 0.3455, 0.6449, 0.2807, -0.3698, -0.6890],
[ 0.1578, -0.3161, -0.1910, -0.4318, -0.5760, 0.3746],
[ 0.2017, -0.6320, -0.4060, 0.3903, 0.3103, -0.5881],
[ 0.6212, 0.3077, 0.0783, -0.6187, 0.3109, -0.6060]],
requires_grad=True)
"""
4.He初始化
也叫kaiming 初始化。He 初始化的核心思想是调整权重的初始化范围,使得每一层的输出的方差保持一致。与 Xavier 初始化不同,He 初始化专门针对 ReLU 激活函数的特性进行了优化。
数学推导
(1) 前向传播的方差一致性
对于 ReLU 激活函数,输出的方差为:
(2) 反向传播的梯度一致性
类似地,反向传播时梯度方差需满足:

(3) 两种模式
-
fan_in模式(默认):优先保证前向传播稳定,方差 \frac{2}{n_{in}}。 -
fan_out模式:优先保证反向传播稳定,方差\frac{2}{n_{out}}。
方法:专门为 ReLU 激活函数设计。权重从以下分布中采样:
均匀分布:

正态分布:

其中 n_{\text{in}} 是当前层的输入神经元数量。
优点:适用于ReLU 和 Leaky ReLU 激活函数。
应用场景:深度网络,尤其是使用 ReLU 激活函数时。
import torch
import torch.nn as nn
def test006():
# He初始化:正态分布
linear = nn.Linear(in_features=6, out_features=4)
nn.init.kaiming_normal_(linear.weight, nonlinearity="relu", mode='fan_in')
print(linear.weight)
# He初始化:均匀分布
linear = nn.Linear(in_features=6, out_features=4)
nn.init.kaiming_uniform_(linear.weight, nonlinearity="relu", mode='fan_out')
print(linear.weight)
if __name__ == "__main__":
test006()
"""
Parameter containing:
tensor([[ 1.4020, 0.2030, 0.3585, -0.7419, 0.6077, 0.0178],
[-0.2860, -1.2135, 0.0773, -0.3750, -0.5725, 0.9756],
[ 0.2938, -0.6159, -1.1721, 0.2093, 0.4212, 0.9079],
[ 0.2050, 0.3866, -0.3129, -0.3009, -0.6659, -0.2261]],
requires_grad=True)
Parameter containing:
tensor([[-0.1924, -0.6155, -0.7438, -0.2796, -0.1671, -0.2979],
[ 0.7609, 0.9836, -0.0961, 0.7139, -0.8044, -0.3827],
[ 0.1416, 0.6636, 0.9539, 0.4735, -0.2384, -0.1330],
[ 0.7254, -0.4056, -0.7621, -0.6139, -0.6093, -0.2577]],
requires_grad=True)
"""
5.总结
在使用Torch构建网络模型时,每个网络层的参数都有默认的初始化方法,同时还可以通过以上方法来对网络参数进行初始化。
总结
激活函数是重点,辨别Sigmoid、Tanh、ReLU、LeakyReLU和Softmax等常见激活函数的适用场景,要知道激活函数在引入非线性中的关键作用。
更多推荐




所有评论(0)