EliorFoy Lv3

(一)多层感知机

1.多层感知机介绍

如图是一张单隐藏层的感知机,当有许多层的时候,最后一层看作线性预测器,这种架构就叫做多层感知机(multilayer perceptron 即MLP).
单隐藏层的多层感知机
再应用上激活函数σ\sigma就能让多层感知机不再是简单的线性模型从而能够表示任何仿射函数.如下所示:

H=σ(XW(1)+b(1)),O=HW(2)+b(2).\begin{aligned} \mathbf{H}&=\sigma(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)}), \\\mathbf{O}&=\mathbf{H}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}. \end{aligned}

2.常见激活函数

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。

2.1 ReLU(Rectified linear unit)函数

ReLU(x)=max(x,0)\mathrm{ReLU}(x) = max(x,0)
图像:
ReLU(Rectified linear unit)函数

2.2 sigmoid函数

前面已经提到过该函数:sigmoid(x)=11+exp(x)\mathrm{sigmoid}(x)=\frac{1}{1+\exp(-x)}
图像:
sigmoid函数

2.3 tanh函数

双曲正切函数:tanh(x)=1exp(2x)1+exp(2x)\tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)}
图像:
tanh函数

(二) 多层感知机的实现

1.从零开始实现

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import torch
from torch import nn
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = nn.Parameter(torch.randn(
num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
loss = nn.CrossEntropyLoss(reduction='none')
# 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr) # 随机梯度下降
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

可以看出基本和之前线性神经网络从零开始实现类似,不过多了一层以及激活函数使用了自定义的relu函数.

2.简洁实现

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# nn.init.normal_函数将线性层的权重向量中的每个元素都初始化为一个正态随机值

net.apply(init_weights);
# 训练
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

(三) 模型选择

原文废话比较多,简而言之就是训练的模型会有训练误差和泛化误差.训练误差是在训练集上训练得到的误差,泛化误差是无穷集中误差的期望值.由此我们可以知道模型会有欠拟合和过拟合,通俗的讲欠拟合就是训练误差大,表达能力不足,不能准确预测;而过拟合是指模型在训练数据上表现很好,但在新的、未见过的数据上表现不佳.
过拟合有以下一些影响因素:

  • 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
  • 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
  • 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
    由此出现了验证集:将我们的数据分成三份, 除了训练和测试数据集之外,还增加一个验证数据集(validation dataset), 也叫验证集(validation set).但现实是验证数据和测试数据之间的边界模糊得令人担忧…训练数据稀缺时,使用K折交叉验证:原始训练数据被分成个不重叠的子集。 然后执行次模型训练和验证,每次在个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对次实验的结果取平均来估计训练和验证误差.

(四) 正则化模型技术

正则化(Regularization)是机器学习中用来防止模型过拟合的一种技术。

1.权重衰减(L2L_2正则化)

之前有提到过范数,范数能够度量向量大小.因为在很多模型中,尤其是线性模型,参数的大小可以反映模型的复杂度。较大的参数值意味着模型对输入数据的变化更敏感,这可能导致模型在训练数据上过度拟合,而在未见过的数据上表现不佳.所以最小化L2L_2范数,可以防止任何单个权重变得过大,这有助于保持模型的稳定性.
L2L_2正则化也就是权重衰减就是在损失函数上加上一个范数作为惩罚项,将原来的训练目标最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和.现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数.
实际上还有L1L_1正则化,那为什么选择L2L_2范数呢.一个原因是它对权重向量的大分量施加了巨大的惩罚.这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型.相比之下,L1L_1惩罚会导致模型将权重集中在一小部分特征上,而将其他权重清除为零.这称为特征选择(feature selection).
于是平衡后的新的额外惩罚的损失变为:

L(w,b)+λ2w2L(\mathbf{w},b)+\frac{\lambda}{2}\|\mathbf{w}\|^2

λ\lambda称为正则化系数,较小的λ\lambda值对应较少约束的w\mathbf{w},而较大的λ\lambda值对w\mathbf{w}的约束更大.λ\lambda除以2仍然是因为求导可以抵消平方项的12\frac{1}{2},选择平方范数而不是标准范数也是为了方便计算.
于是小批量随机梯度下降变为:

w(1ηλ)wηBiBx(i)(wx(i)+by(i))\begin{aligned} \mathbf{w}\leftarrow(1-\eta\lambda)\mathbf{w}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\mathbf{x}^{(i)}\left(\mathbf{w}^{\top}\mathbf{x}^{(i)}+b-y^{(i)}\right) \end{aligned}

所以在更新w\mathbf{w}的过程中同时也在试图将w\mathbf{w}的大小缩到零.

举例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

# 初始化模型参数
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
# 定义范数惩罚
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
def train(lambd):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数是:', torch.norm(w).item())

λ\lambda为0的时候也就是没有引入惩罚的时候得到:

当设置λ\lambda为3后得到:

通过比较就可以发现,引入λ\lambda后,虽然最后训练数据集的误差到后面降不下去了,但是测试数据集中的误差得到了下降.
简洁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# weight decay作为参数输入
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}] # 只为权重设置了weight_decay,所以偏置参数b(bias)不会衰减
, lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())

2.暂退法(Dropout)

摘自原文:

深度网络的泛化性质令人费解,而这种泛化性质的数学基础仍然是悬而未决的研究问题。 我们鼓
励喜好研究理论的读者更深入地研究这个主题。 本节,我们将着重对实际工具的探究,这些工具
倾向于改进深层网络的泛化性。

说明还没有完美解决过拟合的问题…
我们期待“好”的预测模型能在未知的数据上有很好的表现: 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。权重衰减中参数的范数也代表了一种有用的简单性度量。简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。如果平滑那添加一些随机噪声应该是基本无影响的.所以Srivastava et al., 2014等人就想出了在网络中注入噪声.暂退法就是在训练过程中随机“丢弃”(即暂时移除)网络中的一些神经元(及其连接),来减少神经元之间复杂的共适应关系,增强模型的泛化能力。
有趣的是暂退法的原始论文提到的是有性生殖的类比,这说明深度学习与生物学有极其重要的联系,更别提今年的物理和化学诺奖都落入深度学习之手…

举例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import torch
from torch import nn
from d2l import torch as d2l

def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
mask = (torch.rand(X.shape) > dropout).float()
# 使用torch.rand(X.shape)生成一个与X形状相同的随机数张量,其中的每个元素都是从0到1的均匀分布中随机抽取的,通过比较这个随机数张量和dropout值,生成一个布尔值的掩码(mask)
return mask * X / (1.0 - dropout) # 使期望值不变

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
# 定义一个具有两个隐藏层的多层感知机,每个隐藏层包含256个单元

dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training = True):
super(Net, self).__init__() # 父类构造函数
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()

def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用dropout
if self.training == True:
# 在第一个全连接层之后添加一个dropout层
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个dropout层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out

net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)

# 训练
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

简洁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

# 训练
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

(五) 前向传播、反向传播和计算图

1.前向传播

前向传播(forward propagation或forward pass) 指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。
举个栗子(对于有两层隐藏层的多层感知机):

z=W(1)xh=ϕ(z)o=W(2)hL=l(o,y)正则化项:s=λ2(W(1)F2+W(2)F2)正则化损失(目标函数):J=L+s\begin{aligned} \mathbf{z}&=\mathbf{W}^{(1)}\mathbf{x}\\ \mathbf h&=\phi(\mathbf z) \\ \mathbf o&=\mathbf W^{(2)}\mathbf h \\ L&=l(\mathbf{o},y)\\ 正则化项: s&=\frac{\lambda}{2}\Big(\|\mathbf{W}^{(1)}\|_F^2+\|\mathbf{W}^{(2)}\|_F^2\Big)\\ 正则化损失(目标函数): J&=L+s \end{aligned}

前向传播计算图:
前向传播图

2. 反向传播

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。
举几个栗子:

Z=XW(1)+b(1),H=σ(Z),O=HW(2)+b(2).\begin{aligned} \mathbf{Z}&=\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)}, \\ \mathbf{H}&=\sigma(\mathbf{Z}),\\ \mathbf{O}&=\mathbf{H}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}. \end{aligned}

正则化损失JJ关于W(2)\mathbf{W}^{(2)}的梯度:

JW(2)=prod(Jo,oW(2))+prod(Js,sW(2))=Joh+λW(2)\frac{\partial J}{\partial\mathbf{W}^{(2)}}=\mathrm{prod}\left(\frac{\partial J}{\partial\mathbf{o}},\frac{\partial\mathbf{o}}{\partial\mathbf{W}^{(2)}}\right)+\mathrm{prod}\left(\frac{\partial J}{\partial s},\frac{\partial s}{\partial\mathbf{W}^{(2)}}\right)=\frac{\partial J}{\partial\mathbf{o}}\mathbf{h}^{\top}+\lambda\mathbf{W}^{(2)}

注:我们使用运算符prod在执行必要的操作(如换位和交换输入位置)后将其参数相乘。 对于向量,这很简单,它只是矩阵-矩阵乘法。 对于高维张量,我们使用适当的对应项。 运算符指代了所有的这些符号。
关于W(1)\mathbf{W}^{(1)}的梯度:

JW(1)=prod(Jo,oh,hZ,ZW(1))+prod(Js,sW(1))=prod(Jo,oh,hZ)x+λW(1)=Jhϕ(z)x+λW(1)=JoW(2)ϕ(z)x+λW(1)\begin{aligned} \frac{\partial J}{\partial \mathbf{W}^{(1)}} &= \mathrm{prod}\left(\frac{\partial J}{\partial\mathbf{o}},\frac{\partial\mathbf{o}}{\partial\mathbf{h}} ,\frac{\partial\mathbf{h}}{\partial\mathbf{Z}} ,\frac{\partial\mathbf{Z}}{\partial\mathbf{\mathbf{W}^{(1)}}} \right) +\mathrm{prod}\left(\frac{\partial J}{\partial s},\frac{\partial s}{\partial \mathbf{W}^{(1)}}\right) \\ &= \mathrm{prod}\left(\frac{\partial J}{\partial\mathbf{o}},\frac{\partial\mathbf{o}}{\partial\mathbf{h}} ,\frac{\partial\mathbf{h}}{\partial\mathbf{Z}}\right)\mathbf{x}^{\top} +\lambda\mathbf{W}^{(1)}\\ &= {\frac{\partial J}{\partial\mathbf{h}}}\odot\phi^{\prime}\left(\mathbf{z}\right)\mathbf{x}^{\top} +\lambda\mathbf{W}^{(1)}\\ &= \frac{\partial J}{\partial\mathbf{o}}\mathbf{W}^{(2)^{\top}}\odot\phi^{\prime}\left(\mathbf{z}\right)\mathbf{x}^{\top} +\lambda\mathbf{W}^{(1)}\\ \end{aligned}

3.训练神经网络

在训练神经网络时,前向传播和反向传播相互依赖。对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。 然后将这些用于反向传播,其中计算顺序与计算图的相反。

(六) 数值稳定和模型初始化

始化方案的选择在神经网络学习中起着举足轻重的作用,它对保持数值稳定性至关重要.不稳定梯度可能会导致一些问题:

  • 梯度爆炸(gradient exploding)问题: 参数更新过大,破坏了模型的稳定收敛
  • 梯度消失(gradient vanishing)问题: 参数更新过小,在每次更新时几乎不会移动,导致模型无法学习
    此外神经网络设计中的另一个问题是其参数化所固有的对称性。至于什么是参数对称性,我感觉书中讲的不是很明白.只需要了解解决该问题的一种方法是参数初始化,优化期间的注意和适当的正则化也可以进一步提高稳定性.
    Xavier初始化 可供了解.

(七) 环境和分布偏移

考虑环境其实就是考虑反馈,书中举了一个例子:假设我们训练了一个贷款申请人违约风险模型,用来预测谁将偿还贷款或违约。 这个模型发现申请人的鞋子与违约风险相关(穿牛津鞋申请人会偿还,穿运动鞋申请人会违约)。一旦模型开始根据鞋类做出决定,顾客就会理解并改变他们的行为。 不久,所有的申请者都会穿牛津鞋,而信用度却没有相应的提高。通过将基于模型的决策引入环境,我们可能会破坏模型.强化学习实际上就是引入了反馈,但是实现比较困难.

1.分布偏移

书中只是粗略地聊了一下概念,该部分应当属于拓展内容.

  • 协变量偏移:在模型训练和应用时,输入数据(特征)的分布发生了变化,但输出标签的分布保持不变
  • 标签偏移:与协变量偏移相反.在标签偏移的情况下,假设特征(输入数据)的分布保持不变,但是标签的边缘概率P(y)P(y)发生了变化,即不同类别的标签在训练集和测试集中的分布不同,而给定标签时特征的分布P(xy)P(x∣y)保持不变 。
  • 概念偏移:标签定义发生变化,比如随着人类文明发展,我们把猫也叫做狗(这只是个例子,听上去很扯淡,其实就是人们的概念发生了变化).

2.分布偏移纠正

由于本小节不涉及详细例子和代码,理论也很复杂但是解释很简洁…故暂时略去本章…

  • 标题:
  • 作者: EliorFoy
  • 创建于 : 2024-10-25 17:11:20
  • 更新于 : 2024-10-25 17:07:47
  • 链接: https://eliorfoy.github.io/2024/10/25/大三上/动手学深度学习(四):多层感知机/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。