6.1 感知机
- z称为感知机的净活性值.感知机是线性模型,并不能处理线性不可分问题.通过在线性模型后添加激活函数后得到活性值a.
- 添加激活函数后,感知机可以用来完成二分类任务。阶跃函数和符号函数在𝑧 = 0处是不连续的,其他位置导数为 0,无法利用梯度下降算法进行参数优化
6.2 全连接层
- 感知机模型的不可导特性严重约束了它的潜力,使得它只能解决极其简单的任务.它在感知机的基础上,将不连续的阶跃激活函数换成了其它平滑连续可导的激活函数,并通过堆叠多个网络层来增强网络的表达能力。
- 每个输出节点与全部的输入节点相连接,这种网络层称为全连接层(Fully-connected Layer),或者稠密连接层.
6.2.1 张量方式实现
- 创建输入𝑿矩阵为𝑏 = 2个样本,每个样本的输入特征长度为𝑑in = 784,输出节点数为𝑑out = 256,故定义权值矩阵𝑾的 shape 为[784,256],并采用正态分布初始化𝑾;偏置向量𝒃的 shape 定义为[256],在计算完𝑿@𝑾后相加即可,最终全连接层的输出𝑶的 shape 为[2,256],即 2 个样本的特征,每个特征长度为 256.
x = torch.randn([2,784])
w1 = torch.randn([784, 256])
b1 = torch.zeros([256])
o1 = torch.matmul(x, w1) + b1 #线性变换
o1 = F.relu(o1) # 激活函数
print('output:', o1.shape) # output: torch.Size([2, 256])
6.2.2 层方式实现
全连接层本质上是矩阵的相乘和相加运算,实现并不复杂。但是作为最常用的网络层之一,PyTorch 中有更高层、使用更方便的层实现方式:nn.Linear(in_features, out_features)
。通过 nn.Linear 类,只需要指定输入节点数 in_features、输出节点数out_features 可。注意,正如 nn.Linear 名字所表达的意思,Linear 层值提供了线性变换部分,非线性的激活函数需要组合其他的激活函数来实现.
x = torch.randn([4, 28 * 28])
# 创建全连接层,指定输入节点数和输出节点数,指定输入节点数为 784
fc = nn.Linear(28 * 28, 512)
# 通过 fc 类实例完成一次全连接层的计算,返回输出张量
h1 = fc(x)
print('h1:', h1.shape) # h1: torch.Size([4, 512])
print(fc.weight.shape)
# 获取 Dense 类的权值矩阵 torch.Size([512, 784])
print(fc.bias.shape)
# 获取 Dense 类的偏置向量 torch.Size([512])
但是 PyTorch中全连接层类的权值张量𝑾的 shape 定义为[out_features, in_features]
- 在优化参数时,需要获得网络的所有待优化的张量参数列表,可以通过类的parameters 函数来返回待优化参数列表
for p in fc.parameters():
print(p.shape)
- 实际上,网络层除了保存了待优化张量列表 parameters,还有部分层包含了不参与梯度优化的张量,如后续要介绍的 Batch Normalization 层,可以通过 named_buffers 函数返回所有不需要优化的参数列表。
- 除了通过 parameters 函数获得匿名的待优化张量列表外,还可以通过成员函数named_parameters 获得待优化张量名和对象列表。
for name,p in fc.named_parameters():
print(name, p.shape)
'''
weight torch.Size([512, 784])
bias torch.Size([512])
'''
对于全连接层,待优化张量只有 weight 和 bias 两个。
- 利用网络层类对象进行前向计算时,只需要调用类的__call__方法即可,即写成 fc(x)方式便可,它会自动调用类的__call__方法,在__call__方法中会自动调用 forward 方法,这一设定由 PyTorch 框架自动完成,因此用户只需要将网络层的前向计算逻辑实现在 forward方法中。
6.3 神经网络
6.3.1 张量方式实现
# 隐藏层 1 张量
w1 = torch.randn([784, 256])
b1 = torch.zeros([256])
# 隐藏层 2 张量
w2 = torch.randn([256, 128])
b2 = torch.zeros([128])
# 隐藏层 3 张量
w3 = torch.randn([128, 64])
b3 = torch.zeros([64])
# 输出层张量
w4 = torch.randn([64, 10])
b4 = torch.zeros([10])
# x: [b, 28*28]
# 隐藏层 1 前向计算,[b, 28*28] => [b, 256]
h1 = x@w1 + torch.broadcast_to(b1, [x.shape[0], 256])
h1 = F.relu(h1)
'''
torch.broadcast_to(input, shape) → Tensor
input(Tensor) -输入张量。
shape(列表、元组或torch.Size) -新的形状。
将 input 广播到形状 shape 。
'''
# 隐藏层 2 前向计算,[b, 256] => [b, 128]
h2 = h1@w2 + b2
h2 = F.relu(h2)
# 隐藏层 3 前向计算,[b, 128] => [b, 64]
h3 = h2@w3 + b3
h3 = F.relu(h3)
# 输出层前向计算,[b, 64] => [b, 10]
h4 = h3@w4 + b4
不同于 Numpy 实现的数值计算模式,PyTorch 张量在计算的过程中同时也在内部构建了计算图,方便后续的梯度方向传播。如果只需要实现纯数值计算,可以将代码包裹在torch.no_grad 环境中
6.3.2 层方式实现
方式一:
# 创建隐藏层 1 的线性层和激活函数
fc1 = nn.Linear(784, 256)
act1 = nn.ReLU()
# 创建隐藏层 2 的线性层和激活函数
fc2 = nn.Linear(256, 128)
act2 = nn.ReLU()
# 创建隐藏层 3 的线性层和激活函数
fc3 = nn.Linear(128, 64)
act3 = nn.ReLU()
# 创建输出层,无激活函数
fc4 = nn.Linear(64, 10)
x = torch.randn([4,28*28])
h1 = fc1(x) # 通过隐藏层 1 得到输出
h2 = fc2(h1) # 通过隐藏层 2 得到输出
h3 = fc3(h2) # 通过隐藏层 3 得到输出
h4 = fc4(h3) # 通过输出层得到网络输出
方式二:
# 通过 Sequential 容器封装为一个网络类
model = nn.Sequential(
nn.Linear(784, 256), # 创建隐藏层 1
nn.ReLU(),
nn.Linear(256, 128), # 创建隐藏层 2
nn.ReLU(),
nn.Linear(128, 64), # 创建隐藏层 3
nn.ReLU(),
nn.Linear(64, 10), # 创建输出层
)
out = model(x) # 前向计算得到输出
6.3.3 优化目标
6.4 激活函数
6.4.1 Sigmoid
- Sigmoid 函数也叫 Logistic 函数
- 在 PyTorch 中,可以通过 F.sigmoid 实现 Sigmoid 函数,也可以利用 nn.Sigmoid 类
6.4.2 ReLU
- Sigmoid 函数在输入值较大或较小时容易出现梯度值接近于 0 的现象,称为梯度弥散现象。出现梯度弥散现象时,网络参数长时间得不到更新,导致训练不收敛或停滞不动的现象发生,较深层次的网络模型中更容易出现梯度弥散现象
- 经过 ReLU 激活函数后,负数全部抑制为 0,正数得以保留。
6.4.3 LeakyReLU
- ReLU 函数在𝑥 < 0时导数值恒为 0,也可能会造成梯度弥散现象
- F.leaky_relu(x, negative_slope=0.01).其中 negative_slope 参数代表p
6.4.4 Tanh
- torch.tanh(x) # 通过 Tanh 激活函数
- 这里调用的 tanh 位于 torch 命名空间下,而不是 F.tanh。PyTorch 大部分的函数式接口在 torch.nn.functional 下,当然也有少数例外,例如此处。
6.5 输出层设计
- 它除了和所有的隐藏层一样,完成维度变换、特征提取的功能,还作为输出层使用,需要根据具体的任务场景来决定是否使用激活函数,以及使用什么类型的激活函数等
- 常见的几种输出类型包括
- 𝑜𝑖 ∈ 𝑅𝑑 输出属于
整个实数空间
,或者某段普通的实数空间,比如函数值趋势的预测,年龄的预测问题等。 - 𝑜𝑖 ∈ [0,1] 输出值特别地落在
[0, 1]
的区间,如图片生成,图片像素值一般用[0, 1]区间的值表示;或者二分类问题的概率,如硬币正反面的概率预测问题。 - 𝑜𝑖 ∈ [0, 1], 𝑖 𝑜𝑖 = 1 输出值落在
[0, 1]的区间,并且所有输出值之和为 1
,常见的如多分类问题,如 MNIST 手写数字图片识别,图片属于 10 个类别的概率之和应为 1。 - 𝑜𝑖 ∈ [−1, 1] 输出值在
[-1, 1]
之间.
- 𝑜𝑖 ∈ 𝑅𝑑 输出属于
6.5.1 普通实数空间
6.5.2 [0,1]区间
- 一般会将图片的像素值归一化到[0,1]区间,如果直接使用输出层的值,像素的值范围会分布在整个实数空间。为了让像素的值范围映射到[0,1]的有效实数空间,需要在输出层后添加某个合适的激活函数𝜎,其中 Sigmoid 函数刚好具有此功能
- 对于二分类问题,如硬币的正反面的预测,输出层可以只设置一个节点
6.5.3 [0,1]区间,和为1
- 多分类问题
- 可以通过在输出层添加 Softmax 函数实现
- 在 PyTorch 中,可以通过 F.softmax 实现 Softmax 函数
z = torch.tensor([2.,1.,0.1])
out = F.softmax(z,dim=0)
print(out)
- 与 Linear 层类似,Softmax 函数也可以作为网络层类使用,通过类 nn.Softmax(dim=-1)可以方便添加 Softmax 层,其中 dim 参数指定需要进行计算的维度.
- 除了 Softmax 激活函数外,还有一个 Log Softmax 函数 F.log_softmax,他就是在完成Softmax 计算后,再进行 Log 运算。
- 在 Softmax 函数的数值计算过程中,容易因
输入值偏大发生数值溢出现象
;在计算交叉熵时
,也会出现数值溢出的问题。为了数值计算的稳定性,PyTorch 中提供了一个统一的接口nn.CrossEntropyLoss 类
,将Softmax 函数和交叉熵损失函数
同时实现,同时也处理了可能发生数值不稳定的异常,一般推荐使用这个接口,避免自行调用 Softmax、Log、NLLLoss 这一系列运算实现交叉熵损失函数。
z = torch.randn([2, 10])
a = F.log_softmax(z,dim=1)
y = torch.tensor([1,3]).long()
loss = nn.NLLLoss()(a,y)
print('loss: ',loss) # loss: tensor(3.1213)
# 基于 nn.CrossEntropyLoss 实现 Softmax、Log 与 NLLLoss 的统一计算
# 创建 Softmax 与交叉熵计算类,输出层的输出 z 未使用 softmax
loss = nn.CrossEntropyLoss()(z, y)
print('loss:', loss) # loss: tensor(3.1213)
6.5.4 [-1,1]区间
- 如果希望输出值的范围分布在(−1, 1)区间,可以简单地使用 tanh 激活函数
6.6 误差计算
- 常见的误差函数有均方差、交叉熵、KL 散度、Hinge Loss 函数等,其中均方差函数和交叉熵函数在深度学习中比较常见,均方差函数主要用于回归问题,交叉熵函数主要用于分类问题。
6.6.1 均方误差函数(MSE)
- 均方差误差函数广泛应用在回归问题中
o = torch.randn([2,10]) # 构造网络输出
y_onehot = torch.tensor([1,3]) # 构造真实类别值
# 将类别值编码为 One-hot 形式,总类别数为 10 类
y_onehot = F.one_hot(y_onehot, num_classes=10)
loss = F.mse_loss(o, y_onehot) # 计算均方差
- F.mse_loss(input, target, reduction=’mean’)函数默认的参数 reduction 设置为”mean”,即求平样本的平均误差值,可以将 reduction 设置为‘none’来返回每个样本每个特征点上的误差,
- 也可以通过层方式实现,
criterion = nn.MSELoss(reduction='mean')
loss = criterion(o, y_onehot)
print('loss:', loss)
6.6.2 交叉熵误差函数
-
某个分布𝑃(𝑖)的熵定义为
-
KL 散度
-
KL散度衡量 2 个分布之间 距离的指标。
-
交叉熵
-
由于𝑃(𝑖) ∈ [0,1], log2 𝑃(𝑖) ≤ 0,因此
熵𝐻(𝑃)总是大于等于 0
交叉熵可以分解为𝑝的熵𝐻(𝑝)和𝑝与𝑞的 KL 散度
-
交叉熵可以很好地衡量 2 个分布之间的“距离”。特别地,当分类问题中 y 的编码分布𝑝采用 One-hot 编码𝒚时:𝐻(𝑝) = 0,此时
-
𝐻(𝑝||𝑞) = 𝐻(𝑝) + 𝐷𝐾𝐿(𝑝||𝑞) = 𝐷𝐾𝐿(𝑝||𝑞)
-
退化到真实标签分布𝒚与输出概率分布𝒐之间的 KL 散度上。
-
只与真实类别𝑖上的概率𝑜𝑖有关,对应概率𝑜𝑖越大,𝐻(𝑝||𝑞)越小。当对应类别上的概率为 1 时,交叉熵𝐻(𝑝||𝑞)取得最小值 0,此时网络输出𝒐与真实标签𝒚完全一致,神经网络取得最优状态。
-
因此最小化交叉熵损失函数的过程也是最大化正确类别节点上的预测概率、最小化其他节点概率值的过程
6.7 神经网络类型
6.7.1 卷积神经网络
- 局部相关性和权值共享
- 用于图片分类的 AlexNet、VGG、GoogLeNet、ResNet、DenseNet、EfficientNet等,用于目标检测的 R-CNN、Fast RCNN、Faster RCNN、Mask RCNN、YOLO、RetinaNet、CenterNet 等
6.7.2 循环神经网络
- RNN
- LSTM 较好地克服了 RNN 缺乏长期记忆、不擅长处理长序列的问题
- Google 提出了用于机器翻译的 Seq2Seq 模型
6.7.3 注意力机制网络
- 2017 年,Google 提出了第一个利用纯注意力机制实现的网络模型Transformer
6.7.4 图卷积神经网络(GCN)
- 图片、文本等数据具有规则的空间、时间结构,称为 Euclidean Data(欧几里德数据)。卷积神经网络和循环神经网络被证明非常擅长处理这种类型的数据。而像类似于社交网络、通信网络、蛋白质分子结构等一系列的不规则空间拓扑结构的数据,它们往往显得力不从心