目录
批量归一化和残差网络
批量归一化(BatchNormalization)
对输入的标准化(浅层模型)
处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。
标准化处理输入数据使各个特征的分布相近
批量归一化(深度模型)
利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定
1.对全连接层做批量归一化
位置:全连接层中的仿射变换和激活函数之间。
全连接:
x
=
W
u
+
b
o
u
t
p
u
t
=
ϕ
(
x
)
\boldsymbol{x} = \boldsymbol{W\boldsymbol{u} + \boldsymbol{b}} \\ output =\phi(\boldsymbol{x})
x=Wu+boutput=ϕ(x)
批量归一化: 对中间值
x
x
x批量归一化为在batch_size上均值为0方差为1.
o
u
t
p
u
t
=
ϕ
(
BN
(
x
)
)
output=\phi(\text{BN}(\boldsymbol{x}))
output=ϕ(BN(x))
y ( i ) = BN ( x ( i ) ) \boldsymbol{y}^{(i)} = \text{BN}(\boldsymbol{x}^{(i)}) y(i)=BN(x(i))
μ
B
←
1
m
∑
i
=
1
m
x
(
i
)
,
\boldsymbol{\mu}_\mathcal{B} \leftarrow \frac{1}{m}\sum_{i = 1}^{m} \boldsymbol{x}^{(i)},
μB←m1i=1∑mx(i),
σ
B
2
←
1
m
∑
i
=
1
m
(
x
(
i
)
−
μ
B
)
2
,
\boldsymbol{\sigma}_\mathcal{B}^2 \leftarrow \frac{1}{m} \sum_{i=1}^{m}(\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B})^2,
σB2←m1i=1∑m(x(i)−μB)2,
x ^ ( i ) ← x ( i ) − μ B σ B 2 + ϵ , \hat{\boldsymbol{x}}^{(i)} \leftarrow \frac{\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B}}{\sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon}}, x^(i)←σB2+ϵx(i)−μB,
这⾥ϵ > 0是个很小的常数,保证分母大于0
y ( i ) ← γ ⊙ x ^ ( i ) + β . {\boldsymbol{y}}^{(i)} \leftarrow \boldsymbol{\gamma} \odot \hat{\boldsymbol{x}}^{(i)} + \boldsymbol{\beta}. y(i)←γ⊙x^(i)+β.
引入可学习参数:拉伸参数γ和偏移参数β。若 γ = σ B 2 + ϵ \boldsymbol{\gamma} = \sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon} γ=σB2+ϵ和 β = μ B \boldsymbol{\beta} = \boldsymbol{\mu}_\mathcal{B} β=μB,批量归一化无效。
2.对卷积层做批量归⼀化
位置:卷积计算之后、应⽤激活函数之前。
如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数。
计算:对单通道,batchsize=m,卷积计算输出=pxq
对该通道中m×p×q个元素同时做批量归一化,使用相同的均值和方差。
3.预测时的批量归⼀化
训练:以batch为单位,对每个batch计算均值和方差。
预测:用移动平均估算整个训练数据集的样本均值和方差。
def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 判断当前模式是训练模式还是预测模式
if not is_training:
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持
# X的形状以便后面可以做广播运算
mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
# 训练模式下用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 拉伸和偏移
return Y, moving_mean, moving_var
以上定义了预测时的批量归一化,其中参数X是需要进行归一化的目标,参数momentum是超参数,用于更新移动平均的均值和方差。
class BatchNorm(nn.Module):
def __init__(self, num_features, num_dims):
super(BatchNorm, self).__init__()
if num_dims == 2:
shape = (1, num_features) #全连接层输出神经元
else:
shape = (1, num_features, 1, 1) #通道数
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 不参与求梯度和迭代的变量,全在内存上初始化成0
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.zeros(shape)
def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var, Module实例的traning属性默认为true, 调用.eval()后设成false
Y, self.moving_mean, self.moving_var = batch_norm(self.training,
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y
其中num_dims=2时代表全连接层的BN,此时num_features代表输出神经元的个数;num_dims=4时代表卷积层的BN,此时num_features代表输出的通道数。
实际应用时不需要自己动手写以上代码,在pytorch中有已经包装内置好的函数用来BN,直接调用即可。
nn.BatchNorm2d()
用于卷积层,括号内的参数为输入通道数;
nn.BatchNorm1d(120)
用于全连接层,括号内的参数为神经元的个数。
应用于LeNet中:
net = nn.Sequential(
nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
nn.BatchNorm2d(6),
nn.Sigmoid(),
nn.MaxPool2d(2, 2), # kernel_size, stride
nn.Conv2d(6, 16, 5),
nn.BatchNorm2d(16),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
d2l.FlattenLayer(),
nn.Linear(16*4*4, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
残差网络(ResNet)
深度学习的问题:深度CNN网络达到一定深度后再一味地增加层数并不能带来进一步地分类性能提高,反而会招致网络收敛变得更慢,准确率也变得更差。
残差块(Residual Block)
恒等映射:
左边(普通神经网络):f(x)=x
右边(引入残差块):f(x)-x=0 (易于捕捉恒等映射的细微波动)
在残差块中,输⼊可通过跨层的数据线路更快地向前传播。
ResNet模型
卷积(64,7x7,3)
批量一体化
最大池化(3x3,2)
残差块x4 (通过步幅为2的残差块在每个模块之间减小高和宽)
全局平均池化
全连接
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
if first_block:
assert in_channels == out_channels # 第一个模块的通道数同输入通道数一致
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
else:
blk.append(Residual(out_channels, out_channels))
return nn.Sequential(*blk)
net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))
稠密连接网络(DenseNet)
主要构建模块:
稠密块(dense block): 定义了输入和输出是如何连结的。
过渡层(transition layer):用来控制通道数,使之不过大。
稠密块
def conv_block(in_channels, out_channels):
blk = nn.Sequential(nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
return blk
class DenseBlock(nn.Module):
def __init__(self, num_convs, in_channels, out_channels):
super(DenseBlock, self).__init__()
net = []
for i in range(num_convs):
in_c = in_channels + i * out_channels
net.append(conv_block(in_c, out_channels))
self.net = nn.ModuleList(net)
self.out_channels = in_channels + num_convs * out_channels # 计算输出通道数
def forward(self, X):
for blk in self.net:
Y = blk(X)
X = torch.cat((X, Y), dim=1) # 在通道维上将输入和输出连结
return X
稠密块的使用会使得通道数越来越多,这里使用过渡层来减小模型的复杂度。
过渡层
1
×
1
1\times1
1×1卷积层:来减小通道数
步幅为2的平均池化层:减半高和宽
def transition_block(in_channels, out_channels):
blk = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2))
return blk
blk = transition_block(23, 10)
blk(Y).shape # torch.Size([4, 10, 4, 4])
DenseNet模型
首先
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
之后接入(稠密块—过渡层)3
这里稠密块为4层卷积,过渡层为减半。最开始稠密块输入通道数为64,每一个卷积层输出新增通道数32。因此通道数变化为:64 — 64+432=192 — 192/2=96 — 96+4*32=224 — 224/2…以此类推下去。
num_channels, growth_rate = 64, 32 # num_channels为当前的通道数
num_convs_in_dense_blocks = [4, 4, 4, 4]
for i, num_convs in enumerate(num_convs_in_dense_blocks):
DB = DenseBlock(num_convs, num_channels, growth_rate)
net.add_module("DenseBlosk_%d" % i, DB)
# 上一个稠密块的输出通道数
num_channels = DB.out_channels
# 在稠密块之间加入通道数减半的过渡层
if i != len(num_convs_in_dense_blocks) - 1:
net.add_module("transition_block_%d" % i, transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2
最后接上剩下的结构:
net.add_module("BN", nn.BatchNorm2d(num_channels))
net.add_module("relu", nn.ReLU())
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, num_channels, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(num_channels, 10)))
X = torch.rand((1, 1, 96, 96))
for name, layer in net.named_children():
X = layer(X)
print(name, ' output shape:\t', X.shape)
另外1*1卷积层使用地方很多,它可以保持高宽不变有效的降低数据维度如下图:
凸优化
尽管优化方法可以最小化深度学习中的损失函数值,但本质上优化方法达到的目标与深度学习的目标并不相同。
- 优化方法目标:训练集损失函数值
- 深度学习目标:测试集损失函数值(泛化性)
鞍点是对所有自变量一阶偏导数都为0,且Hessian矩阵特征值有正有负的点。
梯度下降
梯度下降、随机梯度下降和小批量随机梯度下降三者的区别在于每次更新时用的样本量。
对于梯度下降,每次更新参数时使用的是整个数据集,选择所有样本的平均梯度来对参数进行更新,这种更新方法对于数据会有一定的相似性,对于共性比较大的数据集这种方法对于数据的利用率比较低。
对于随机梯度下降,每次更新参数时使用的是单个样本点的梯度,这种方法算力的利用率比较低,并没有充分利用上目前CPU或GPU的矢量运算能力。
小批量随机梯度下降可以中和以上两种方法,小批量随机梯度下降每次在进行参数更新的时候选择的是一部分的样本,对这些样本的梯度取均值对参数进行更新。
随机梯度下降参数更新
对于有 n n n 个样本对训练数据集,设 f i ( x ) f_i(x) fi(x) 是第 i i i 个样本的损失函数, 则目标函数为:
f ( x ) = 1 n ∑ i = 1 n f i ( x ) f(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} f_{i}(\mathbf{x}) f(x)=n1i=1∑nfi(x)
其梯度为:
∇ f ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) \nabla f(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} \nabla f_{i}(\mathbf{x}) ∇f(x)=n1i=1∑n∇fi(x)
使用该梯度的一次更新的时间复杂度为 O ( n ) \mathcal{O}(n) O(n),即梯度下降的时间复杂度是 O ( n ) \mathcal{O}(n) O(n)。
随机梯度下降更新公式 O ( 1 ) \mathcal{O}(1) O(1):
x
←
x
−
η
∇
f
i
(
x
)
\mathbf{x} \leftarrow \mathbf{x}-\eta \nabla f_{i}(\mathbf{x})
x←x−η∇fi(x)
随机梯度下降的时间复杂度是
O
(
1
)
\mathcal{O}(1)
O(1)。
且有:
E i ∇ f i ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) = ∇ f ( x ) \mathbb{E}_{i} \nabla f_{i}(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} \nabla f_{i}(\mathbf{x})=\nabla f(\mathbf{x}) Ei∇fi(x)=n1i=1∑n∇fi(x)=∇f(x)
对于随机梯度下降,每次更新参数时使用的是单个样本点的梯度,这种方法算力的利用率比较低,并没有充分利用上目前CPU或GPU的矢量运算能力。
动态学习率
η
(
t
)
=
η
i
if
t
i
≤
t
≤
t
i
+
1
piecewise constant
η
(
t
)
=
η
0
⋅
e
−
λ
t
exponential
η
(
t
)
=
η
0
⋅
(
β
t
+
1
)
−
α
polynomial
\begin{array}{ll}{\eta(t)=\eta_{i} \text { if } t_{i} \leq t \leq t_{i+1}} & {\text { piecewise constant }} \\ {\eta(t)=\eta_{0} \cdot e^{-\lambda t}} & {\text { exponential }} \\ {\eta(t)=\eta_{0} \cdot(\beta t+1)^{-\alpha}} & {\text { polynomial }}\end{array}
η(t)=ηi if ti≤t≤ti+1η(t)=η0⋅e−λtη(t)=η0⋅(βt+1)−α piecewise constant exponential polynomial
动态学习率在最开始学习率设计比较大,加速收敛;随着迭代次数增加减小学习率,学习率可以设计为指数衰减或多项式衰减;在优化进行一段时间后可以适当减小学习率来避免振荡。
小批量随机梯度下降
小批量随机梯度下降可以中和以上两种方法。
小批量随机梯度下降每次在进行参数更新的时候选择的是一部分的样本,对这些样本的梯度取均值对参数进行更新。
读取数据
def get_data_ch7(): # 本函数已保存在d2lzh_pytorch包中方便以后使用
data = np.genfromtxt('/home/kesci/input/airfoil4755/airfoil_self_noise.dat', delimiter='\t')
data = (data - data.mean(axis=0)) / data.std(axis=0) # 标准化
return torch.tensor(data[:1500, :-1], dtype=torch.float32), \
torch.tensor(data[:1500, -1], dtype=torch.float32) # 前1500个样本(每个样本5个特征)
features, labels = get_data_ch7()
features.shape
其中np.genfromtxt()
函数是用来创建数组表格数据,详情见:genfromtxt函数解析
import pandas as pd
df = pd.read_csv('/home/kesci/input/airfoil4755/airfoil_self_noise.dat', delimiter='\t', header=None)
df.head(10)
0-4列为特征,5列为标签。
优化算法进阶
在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题。对于noisy gradient,我们需要谨慎的选取学习率和batch size, 来控制梯度方差和收敛的结果。
g t = ∂ w 1 ∣ B t ∣ ∑ i ∈ B t f ( x i , w t − 1 ) = 1 ∣ B t ∣ ∑ i ∈ B t g i , t − 1 . \mathbf{g}_t = \partial_{\mathbf{w}} \frac{1}{|\mathcal{B}_t|} \sum_{i \in \mathcal{B}_t} f(\mathbf{x}_{i}, \mathbf{w}_{t-1}) = \frac{1}{|\mathcal{B}_t|} \sum_{i \in \mathcal{B}_t} \mathbf{g}_{i, t-1}. gt=∂w∣Bt∣1i∈Bt∑f(xi,wt−1)=∣Bt∣1i∈Bt∑gi,t−1.
An ill-conditioned Problem
Condition Number of Hessian Matrix:
c
o
n
d
H
=
λ
m
a
x
λ
m
i
n
cond_{H} = \frac{\lambda_{max}}{\lambda_{min}}
condH=λminλmax
where
λ
m
a
x
,
λ
m
i
n
\lambda_{max}, \lambda_{min}
λmax,λmin is the maximum amd minimum eignvalue of Hessian matrix.(Hessian矩阵中的最大最小特征值)
让我们考虑一个输入和输出分别为二维向量
x
=
[
x
1
,
x
2
]
⊤
\boldsymbol{x} = [x_1, x_2]^\top
x=[x1,x2]⊤和标量的目标函数:
f
(
x
)
=
0.1
x
1
2
+
2
x
2
2
f(\boldsymbol{x})=0.1x_1^2+2x_2^2
f(x)=0.1x12+2x22
c
o
n
d
H
=
4
0.2
=
20
→
ill-conditioned
cond_{H} = \frac{4}{0.2} = 20 \quad \rightarrow \quad \text{ill-conditioned}
condH=0.24=20→ill-conditioned
一般来说condition number比较大时即认定该问题为ill-conditioner problem.可参考神经网络优化中的病态问题
Maximum Learning Rate
- For f ( x ) f(x) f(x), according to convex optimizaiton conclusions, we need step size η ≤ 1 L \eta \leq \frac{1}{L} η≤L1 to have the fastest convergence,where L = max X ∇ 2 f ( x ) L=\max _{X} \nabla^{2} f(x) L=maxX∇2f(x) called the smooth.(即L是 H e s s i a n 阵 ∇ 2 f ( x ) 中 对 应 某 一 维 度 的 值 Hessian阵 \nabla^{2} f(x)中对应某一维度的值 Hessian阵∇2f(x)中对应某一维度的值 )
- To guarantee the convergence, we need to have η ≤ 2 L \eta \leq \frac{2}{L} η≤L2 .
以上即说明当某一维坐标上学习率满足在该坐标上 η ≤ 1 L \eta \leq \frac{1}{L} η≤L1时,函数将沿着单调区间进行递减迭代,当某一维坐标上学习率满足在该坐标上 η ≤ 2 L \eta \leq \frac{2}{L} η≤L2 且不满足 η ≤ 1 L \eta \leq \frac{1}{L} η≤L1时,函数将在区间谷壁对碰折线递减迭代,但依旧能够保证最终收敛到最小点,而当某一维坐标上学习率不满足在该坐标上 η ≤ 2 L \eta \leq \frac{2}{L} η≤L2时,函数将不能递减迭代,也不能收敛到该维度上的最小点处。
Supp: Preconditioning
在二阶优化中,我们使用Hessian matrix的逆矩阵(或者pseudo inverse)来左乘梯度向量 i . e . Δ x = H − 1 g i.e. \Delta_{x} = H^{-1}\mathbf{g} i.e.Δx=H−1g,这样的做法称为precondition,相当于将 H H H 映射为一个单位矩阵,拥有分布均匀的Spectrum,也即我们去优化的等价标函数的Hessian matrix为良好的identity matrix,同一个学习率在各个坐标维度上递减迭代减少量接近,就不会出现在某一维度上学习率过高而另一维度上则学习率过低的情况。
Solution to ill-condition
- Preconditioning gradient vector: applied in Adam, RMSProp, AdaGrad, Adelta, KFC, Natural gradient and other secord-order optimization algorithms.
- Averaging history gradient: like momentum, which allows larger learning rates to accelerate convergence; applied in Adam, RMSProp, SGD momentum.
Momentum Algorithm
动量法的提出是为了解决梯度下降的上述问题。设时间步
t
t
t 的自变量为
x
t
\boldsymbol{x}_t
xt,学习率为
η
t
\eta_t
ηt。
在时间步
t
=
0
t=0
t=0,动量法创建速度变量
m
0
\boldsymbol{m}_0
m0,并将其元素初始化成 0。在时间步
t
>
0
t>0
t>0,动量法对每次迭代的步骤做如下修改:
m t ← β m t − 1 + η t g t , x t ← x t − 1 − m t , \begin{aligned} \boldsymbol{m}_t &\leftarrow \beta \boldsymbol{m}_{t-1} + \eta_t \boldsymbol{g}_t, \\ \boldsymbol{x}_t &\leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{m}_t, \end{aligned} mtxt←βmt−1+ηtgt,←xt−1−mt,
Another version:
m t ← β m t − 1 + ( 1 − β ) g t , x t ← x t − 1 − α t m t , \begin{aligned} \boldsymbol{m}_t &\leftarrow \beta \boldsymbol{m}_{t-1} + (1-\beta) \boldsymbol{g}_t, \\ \boldsymbol{x}_t &\leftarrow \boldsymbol{x}_{t-1} - \alpha_t \boldsymbol{m}_t, \end{aligned} mtxt←βmt−1+(1−β)gt,←xt−1−αtmt,
α t = η t 1 − β \alpha_t = \frac{\eta_t}{1-\beta} αt=1−βηt
其中,动量超参数 β \beta β满足 0 ≤ β < 1 0 \leq \beta < 1 0≤β<1。当 β = 0 \beta=0 β=0 时,动量法等价于小批量随机梯度下降。
在解释动量法的数学原理前,让我们先从实验中观察梯度下降在使用动量法后的迭代轨迹。
由实验观察可得,在学习率满足以上收敛条件时,优化仍旧收敛,并且没有之前在某一维度上陡增陡减的情况,反而是迭代得更为平缓;在学习率超出以上收敛条件规定范围时候,优化依旧收敛,即momentum对历史的梯度会有一个加权的平均,平均值就是 m t \boldsymbol{m}_{t} mt,经过加权之后更新参数时使用的并不是某点处的直接梯度方向,而是以某种更平缓更接近最优点的方向进行参数更新,所以momentum可以允许使用更大的学习率从而更快迭代寻找到最优点。
Exponential Moving Average
为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponential moving average)。给定超参数 0 ≤ β < 1 0 \leq \beta < 1 0≤β<1,当前时间步 t t t 的变量 y t y_t yt 是上一时间步 t − 1 t-1 t−1 的变量 y t − 1 y_{t-1} yt−1 和当前时间步另一变量 x t x_t xt 的线性组合:
y t = β y t − 1 + ( 1 − β ) x t . y_t = \beta y_{t-1} + (1-\beta) x_t. yt=βyt−1+(1−β)xt.
我们可以对 y t y_t yt 展开:
y t = ( 1 − β ) x t + β y t − 1 = ( 1 − β ) x t + ( 1 − β ) ⋅ β x t − 1 + β 2 y t − 2 = ( 1 − β ) x t + ( 1 − β ) ⋅ β x t − 1 + ( 1 − β ) ⋅ β 2 x t − 2 + β 3 y t − 3 = ( 1 − β ) ∑ i = 0 t β i x t − i \begin{aligned} y_t &= (1-\beta) x_t + \beta y_{t-1}\\ &= (1-\beta)x_t + (1-\beta) \cdot \beta x_{t-1} + \beta^2y_{t-2}\\ &= (1-\beta)x_t + (1-\beta) \cdot \beta x_{t-1} + (1-\beta) \cdot \beta^2x_{t-2} + \beta^3y_{t-3}\\ &= (1-\beta) \sum_{i=0}^{t} \beta^{i}x_{t-i} \end{aligned} yt=(1−β)xt+βyt−1=(1−β)xt+(1−β)⋅βxt−1+β2yt−2=(1−β)xt+(1−β)⋅βxt−1+(1−β)⋅β2xt−2+β3yt−3=(1−β)i=0∑tβixt−i
(
1
−
β
)
∑
i
=
0
t
β
i
=
1
−
β
t
1
−
β
(
1
−
β
)
=
(
1
−
β
t
)
(1-\beta)\sum_{i=0}^{t} \beta^{i} = \frac{1-\beta^{t}}{1-\beta} (1-\beta) = (1-\beta^{t})
(1−β)i=0∑tβi=1−β1−βt(1−β)=(1−βt)
由于
0
≤
β
<
1
0 \leq \beta < 1
0≤β<1,所以我们可以知道离当前时间步越近
x
t
−
i
\mathcal{x}_{t-i}
xt−i的权重越大。
Supp
Approximate Average of 1 1 − β \frac{1}{1-\beta} 1−β1 Steps
令 n = 1 / ( 1 − β ) n = 1/(1-\beta) n=1/(1−β),那么 ( 1 − 1 / n ) n = β 1 / ( 1 − β ) \left(1-1/n\right)^n = \beta^{1/(1-\beta)} (1−1/n)n=β1/(1−β)。因为
lim n → ∞ ( 1 − 1 n ) n = exp ( − 1 ) ≈ 0.3679 , \lim_{n \rightarrow \infty} \left(1-\frac{1}{n}\right)^n = \exp(-1) \approx 0.3679, n→∞lim(1−n1)n=exp(−1)≈0.3679,
所以当 β → 1 \beta \rightarrow 1 β→1时, β 1 / ( 1 − β ) = exp ( − 1 ) \beta^{1/(1-\beta)}=\exp(-1) β1/(1−β)=exp(−1),如 0.9 5 20 ≈ exp ( − 1 ) 0.95^{20} \approx \exp(-1) 0.9520≈exp(−1)。如果把 exp ( − 1 ) \exp(-1) exp(−1) 当作一个比较小的数,我们可以在近似中忽略所有含 β 1 / ( 1 − β ) \beta^{1/(1-\beta)} β1/(1−β) 和比 β 1 / ( 1 − β ) \beta^{1/(1-\beta)} β1/(1−β) 更高阶的系数的项。例如,当 β = 0.95 \beta=0.95 β=0.95 时,
y t ≈ 0.05 ∑ i = 0 19 0.9 5 i x t − i . y_t \approx 0.05 \sum_{i=0}^{19} 0.95^i x_{t-i}. yt≈0.05i=0∑190.95ixt−i.
因此,在实际中,我们常常将 y t y_t yt 看作是对最近 1 / ( 1 − β ) 1/(1-\beta) 1/(1−β) 个时间步的 x t x_t xt 值的加权平均。例如,当 γ = 0.95 \gamma = 0.95 γ=0.95 时, y t y_t yt 可以被看作对最近20个时间步的 x t x_t xt 值的加权平均;当 β = 0.9 \beta = 0.9 β=0.9 时, y t y_t yt 可以看作是对最近10个时间步的 x t x_t xt 值的加权平均。而且,离当前时间步 t t t 越近的 x t x_t xt 值获得的权重越大(越接近1)。
由指数加权移动平均理解动量法
现在,我们对动量法的速度变量做变形:
m t ← β m t − 1 + ( 1 − β ) ( η t 1 − β g t ) . \boldsymbol{m}_t \leftarrow \beta \boldsymbol{m}_{t-1} + (1 - \beta) \left(\frac{\eta_t}{1 - \beta} \boldsymbol{g}_t\right). mt←βmt−1+(1−β)(1−βηtgt).
Another version:
m t ← β m t − 1 + ( 1 − β ) g t . \boldsymbol{m}_t \leftarrow \beta \boldsymbol{m}_{t-1} + (1 - \beta) \boldsymbol{g}_t. mt←βmt−1+(1−β)gt.
x t ← x t − 1 − α t m t , \begin{aligned} \boldsymbol{x}_t &\leftarrow \boldsymbol{x}_{t-1} - \alpha_t \boldsymbol{m}_t, \end{aligned} xt←xt−1−αtmt,
α t = η t 1 − β \alpha_t = \frac{\eta_t}{1-\beta} αt=1−βηt
由指数加权移动平均的形式可得,速度变量 v t \boldsymbol{v}_t vt 实际上对序列 { η t − i g t − i / ( 1 − β ) : i = 0 , … , 1 / ( 1 − β ) − 1 } \{\eta_{t-i}\boldsymbol{g}_{t-i} /(1-\beta):i=0,\ldots,1/(1-\beta)-1\} {ηt−igt−i/(1−β):i=0,…,1/(1−β)−1} 做了指数加权移动平均。换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将前者对应的最近 1 / ( 1 − β ) 1/(1-\beta) 1/(1−β) 个时间步的更新量做了指数加权移动平均后再除以 1 − β 1-\beta 1−β。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。
Pytorch Class
在Pytorch中,torch.optim.SGD
已实现了Momentum。
d2l.train_pytorch_ch7(torch.optim.SGD, {'lr': 0.004, 'momentum': 0.9},
features, labels)
当不传入momentum参数时该参数默认为零,即使用SGD。
AdaGrad
AdaGrad算法根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题 。
Algorithm
AdaGrad算法会使用一个小批量随机梯度 g t \boldsymbol{g}_t gt按元素平方的累加变量 s t \boldsymbol{s}_t st。在时间步0,AdaGrad将 s 0 \boldsymbol{s}_0 s0中每个元素初始化为0。在时间步 t t t,首先将小批量随机梯度 g t \boldsymbol{g}_t gt按元素平方后累加到变量 s t \boldsymbol{s}_t st:
s t ← s t − 1 + g t ⊙ g t , \boldsymbol{s}_t \leftarrow \boldsymbol{s}_{t-1} + \boldsymbol{g}_t \odot \boldsymbol{g}_t, st←st−1+gt⊙gt,
其中 ⊙ \odot ⊙是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:
x t ← x t − 1 − η s t + ϵ ⊙ g t , \boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \frac{\eta}{\sqrt{\boldsymbol{s}_t + \epsilon}} \odot \boldsymbol{g}_t, xt←xt−1−st+ϵη⊙gt,
其中 η \eta η是学习率, ϵ \epsilon ϵ是为了维持数值稳定性而添加的常数,如 1 0 − 6 10^{-6} 10−6。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
例如当梯度较大(小)时, g t \boldsymbol{g}_{t} gt就很大(小),从而 s t \boldsymbol{s}_{t} st很大(小),所以学习率由 η \eta η调整为 η s t + ϵ \frac{\eta}{\sqrt{s_{t}+\epsilon}} st+ϵη,对应学习率就很小(大)。
Feature
需要强调的是,小批量随机梯度按元素平方的累加变量 s t \boldsymbol{s}_t st出现在学习率的分母项中。因此,如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快;反之,如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。然而,由于 s t \boldsymbol{s}_t st一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。
Pytorch Class
通过名称为“adagrad”的Trainer
实例,我们便可使用Pytorch提供的AdaGrad算法来训练模型。
d2l.train_pytorch_ch7(torch.optim.Adagrad, {'lr': 0.1}, features, labels)
RMSProp
当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp算法对AdaGrad算法做了修改。该算法源自Coursera上的一门课程,即“机器学习的神经网络”。
Algorithm
在“动量法”(momentum)一节里介绍过指数加权移动平均。不同于AdaGrad算法里状态变量 s t \boldsymbol{s}_t st是截至时间步 t t t所有小批量随机梯度 g t \boldsymbol{g}_t gt按元素平方和,RMSProp算法将这些梯度按元素平方做指数加权移动平均。具体来说,给定超参数 0 ≤ γ 0 0 \leq \gamma 0 0≤γ0计算
v t ← β v t − 1 + ( 1 − β ) g t ⊙ g t . \boldsymbol{v}_t \leftarrow \beta \boldsymbol{v}_{t-1} + (1 - \beta) \boldsymbol{g}_t \odot \boldsymbol{g}_t. vt←βvt−1+(1−β)gt⊙gt.
和AdaGrad算法一样,RMSProp算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量
x t ← x t − 1 − α v t + ϵ ⊙ g t , \boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \frac{\alpha}{\sqrt{\boldsymbol{v}_t + \epsilon}} \odot \boldsymbol{g}_t, xt←xt−1−vt+ϵα⊙gt,
以上 α \alpha α和 β \beta β均为超参数。
其中 η \eta η是学习率, ϵ \epsilon ϵ是为了维持数值稳定性而添加的常数,如 1 0 − 6 10^{-6} 10−6。因为RMSProp算法的状态变量 s t \boldsymbol{s}_t st是对平方项 g t ⊙ g t \boldsymbol{g}_t \odot \boldsymbol{g}_t gt⊙gt的指数加权移动平均,所以可以看作是最近 1 / ( 1 − β ) 1/(1-\beta) 1/(1−β)个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)。
Pytorch Class
通过名称为“rmsprop”的Trainer
实例,我们便可使用Gluon提供的RMSProp算法来训练模型。注意,超参数
γ
\gamma
γ通过gamma1
指定。
d2l.train_pytorch_ch7(torch.optim.RMSprop, {'lr': 0.01, 'alpha': 0.9},
features, labels)
AdaDelta
除了RMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的问题做了改进 。有意思的是,AdaDelta算法没有学习率这一超参数。
Algorithm
AdaDelta算法也像RMSProp算法一样,使用了小批量随机梯度 g t \boldsymbol{g}_t gt按元素平方的指数加权移动平均变量 s t \boldsymbol{s}_t st。在时间步0,它的所有元素被初始化为0。给定超参数 0 ≤ ρ 0 0 \leq \rho 0 0≤ρ0,同RMSProp算法一样计算
s t ← ρ s t − 1 + ( 1 − ρ ) g t ⊙ g t . \boldsymbol{s}_t \leftarrow \rho \boldsymbol{s}_{t-1} + (1 - \rho) \boldsymbol{g}_t \odot \boldsymbol{g}_t. st←ρst−1+(1−ρ)gt⊙gt.
与RMSProp算法不同的是,AdaDelta算法还维护一个额外的状态变量 Δ x t \Delta\boldsymbol{x}_t Δxt,其元素同样在时间步0时被初始化为0。我们使用 Δ x t − 1 \Delta\boldsymbol{x}_{t-1} Δxt−1来计算自变量的变化量:
g t ′ ← Δ x t − 1 + ϵ s t + ϵ ⊙ g t , \boldsymbol{g}_t' \leftarrow \sqrt{\frac{\Delta\boldsymbol{x}_{t-1} + \epsilon}{\boldsymbol{s}_t + \epsilon}} \odot \boldsymbol{g}_t, gt′←st+ϵΔxt−1+ϵ⊙gt,
其中 ϵ \epsilon ϵ是为了维持数值稳定性而添加的常数,如 1 0 − 5 10^{-5} 10−5。接着更新自变量:
x t ← x t − 1 − g t ′ . \boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{g}'_t. xt←xt−1−gt′.
最后,我们使用 Δ x t \Delta\boldsymbol{x}_t Δxt来记录自变量变化量 g t ′ \boldsymbol{g}'_t gt′按元素平方的指数加权移动平均:
Δ x t ← ρ Δ x t − 1 + ( 1 − ρ ) g t ′ ⊙ g t ′ . \Delta\boldsymbol{x}_t \leftarrow \rho \Delta\boldsymbol{x}_{t-1} + (1 - \rho) \boldsymbol{g}'_t \odot \boldsymbol{g}'_t. Δxt←ρΔxt−1+(1−ρ)gt′⊙gt′.
可以看到,如不考虑 ϵ \epsilon ϵ的影响,AdaDelta算法与RMSProp算法的不同之处在于使用 Δ x t − 1 \sqrt{\Delta\boldsymbol{x}_{t-1}} Δxt−1来替代超参数 η \eta η。
Implement
AdaDelta算法需要对每个自变量维护两个状态变量,即 s t \boldsymbol{s}_t st和 Δ x t \Delta\boldsymbol{x}_t Δxt。我们按AdaDelta算法中的公式实现该算法。
Pytorch Class
通过名称为“adadelta”的Trainer
实例,我们便可使用pytorch提供的AdaDelta算法。它的超参数可以通过rho
来指定。
d2l.train_pytorch_ch7(torch.optim.Adadelta, {'rho': 0.9}, features, labels)
Adam
Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均 。
Algorithm
Adam算法使用了动量变量 m t \boldsymbol{m}_t mt和RMSProp算法中小批量随机梯度按元素平方的指数加权移动平均变量 v t \boldsymbol{v}_t vt,并在时间步0将它们中每个元素初始化为0。给定超参数 0 ≤ β 1 < 1 0 \leq \beta_1 < 1 0≤β1<1(算法作者建议设为0.9),时间步 t t t的动量变量 m t \boldsymbol{m}_t mt即小批量随机梯度 g t \boldsymbol{g}_t gt的指数加权移动平均:
m t ← β 1 m t − 1 + ( 1 − β 1 ) g t . \boldsymbol{m}_t \leftarrow \beta_1 \boldsymbol{m}_{t-1} + (1 - \beta_1) \boldsymbol{g}_t. mt←β1mt−1+(1−β1)gt.
和RMSProp算法中一样,给定超参数
0
≤
β
2
<
1
0 \leq \beta_2 < 1
0≤β2<1(算法作者建议设为0.999),
将小批量随机梯度按元素平方后的项
g
t
⊙
g
t
\boldsymbol{g}_t \odot \boldsymbol{g}_t
gt⊙gt做指数加权移动平均得到
v
t
\boldsymbol{v}_t
vt:
v t ← β 2 v t − 1 + ( 1 − β 2 ) g t ⊙ g t . \boldsymbol{v}_t \leftarrow \beta_2 \boldsymbol{v}_{t-1} + (1 - \beta_2) \boldsymbol{g}_t \odot \boldsymbol{g}_t. vt←β2vt−1+(1−β2)gt⊙gt.
由于我们将
m
0
\boldsymbol{m}_0
m0和
s
0
\boldsymbol{s}_0
s0中的元素都初始化为0,
在时间步
t
t
t我们得到
m
t
=
(
1
−
β
1
)
∑
i
=
1
t
β
1
t
−
i
g
i
\boldsymbol{m}_t = (1-\beta_1) \sum_{i=1}^t \beta_1^{t-i} \boldsymbol{g}_i
mt=(1−β1)∑i=1tβ1t−igi。将过去各时间步小批量随机梯度的权值相加,得到
(
1
−
β
1
)
∑
i
=
1
t
β
1
t
−
i
=
1
−
β
1
t
(1-\beta_1) \sum_{i=1}^t \beta_1^{t-i} = 1 - \beta_1^t
(1−β1)∑i=1tβ1t−i=1−β1t。需要注意的是,当
t
t
t较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当
β
1
=
0.9
\beta_1 = 0.9
β1=0.9时,
m
1
=
0.1
g
1
\boldsymbol{m}_1 = 0.1\boldsymbol{g}_1
m1=0.1g1。为了消除这样的影响,对于任意时间步
t
t
t,我们可以将
m
t
\boldsymbol{m}_t
mt再除以
1
−
β
1
t
1 - \beta_1^t
1−β1t,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在Adam算法中,我们对变量
m
t
\boldsymbol{m}_t
mt和
v
t
\boldsymbol{v}_t
vt均作偏差修正:
m ^ t ← m t 1 − β 1 t , \hat{\boldsymbol{m}}_t \leftarrow \frac{\boldsymbol{m}_t}{1 - \beta_1^t}, m^t←1−β1tmt,
v ^ t ← v t 1 − β 2 t . \hat{\boldsymbol{v}}_t \leftarrow \frac{\boldsymbol{v}_t}{1 - \beta_2^t}. v^t←1−β2tvt.
接下来,Adam算法使用以上偏差修正后的变量 m ^ t \hat{\boldsymbol{m}}_t m^t和 m ^ t \hat{\boldsymbol{m}}_t m^t,将模型参数中每个元素的学习率通过按元素运算重新调整:
g t ′ ← η m ^ t v ^ t + ϵ , \boldsymbol{g}_t' \leftarrow \frac{\eta \hat{\boldsymbol{m}}_t}{\sqrt{\hat{\boldsymbol{v}}_t} + \epsilon}, gt′←v^t+ϵηm^t,
其中 η \eta η是学习率, ϵ \epsilon ϵ是为了维持数值稳定性而添加的常数,如 1 0 − 8 10^{-8} 10−8。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用 g t ′ \boldsymbol{g}_t' gt′迭代自变量:
x t ← x t − 1 − g t ′ . \boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{g}_t'. xt←xt−1−gt′.
Implement
我们按照Adam算法中的公式实现该算法。其中时间步
t
t
t通过hyperparams
参数传入adam
函数。
Pytorch Class
d2l.train_pytorch_ch7(torch.optim.Adam, {'lr': 0.01}, features, labels)
word2vec
二次采样
文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样。 具体来说,数据集中每个被索引词 w i w_i wi 将有一定概率被丢弃,该丢弃概率为
P ( w i ) = max ( 1 − t f ( w i ) , 0 ) P(w_i)=\max(1-\sqrt{\frac{t}{f(w_i)}},0) P(wi)=max(1−f(wi)t,0)
其中 f ( w i ) f(w_i) f(wi) 是数据集中词 w i w_i wi 的个数与总词数之比,常数 t t t 是一个超参数(实验中设为 1 0 − 4 10^{−4} 10−4)。可见,只有当 f ( w i ) > t f(w_i)>t f(wi)>t 时,我们才有可能在二次采样中丢弃词 w i w_i wi,并且越高频的词被丢弃的概率越大。
Skip-Gram 模型的前向计算
def skip_gram(center, contexts_and_negatives, embed_v, embed_u):
'''
@params:
center: 中心词下标,形状为 (n, 1) 的整数张量
contexts_and_negatives: 背景词和噪音词下标,形状为 (n, m) 的整数张量
embed_v: 中心词的 embedding 层
embed_u: 背景词的 embedding 层
@return:
pred: 中心词与背景词(或噪音词)的内积,之后可用于计算概率 p(w_o|w_c)
'''
v = embed_v(center) # shape of (n, 1, d)
u = embed_u(contexts_and_negatives) # shape of (n, m, d)
pred = torch.bmm(v, u.permute(0, 2, 1)) # bmm((n, 1, d), (n, d, m)) => shape of (n, 1, m)
return pred
负采样近似
由于 softmax 运算考虑了背景词可能是词典 V \mathcal{V} V 中的任一词,对于含几十万或上百万词的较大词典,就可能导致计算的开销过大。我们将以 skip-gram 模型为例,介绍负采样 (negative sampling) 的实现来尝试解决这个问题。
负采样方法用以下公式来近似条件概率 P ( w o ∣ w c ) = exp ( u o ⊤ v c ) ∑ i ∈ V exp ( u i ⊤ v c ) P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)} P(wo∣wc)=∑i∈Vexp(ui⊤vc)exp(uo⊤vc):
$$
P(w_o\mid w_c)=P(D=1\mid w_c,w_o)\prod_{k=1,w_k\sim P(w)}^K P(D=0\mid w_c,w_k)
$$
其中 P ( D = 1 ∣ w c , w o ) = σ ( u o ⊤ v c ) P(D=1\mid w_c,w_o)=\sigma(\boldsymbol{u}_o^\top\boldsymbol{v}_c) P(D=1∣wc,wo)=σ(uo⊤vc), σ ( ⋅ ) \sigma(\cdot) σ(⋅) 为 sigmoid 函数。对于一对中心词和背景词,我们从词典中随机采样 K K K 个噪声词(实验中设 K = 5 K=5 K=5)。根据 Word2Vec 论文的建议,噪声词采样概率 P ( w ) P(w) P(w) 设为 w w w 词频与总词频之比的 0.75 0.75 0.75 次方。
损失函数
应用负采样方法后,我们可利用最大似然估计的对数等价形式将损失函数定义为如下
∑ t = 1 T ∑ − m ≤ j ≤ m , j ≠ 0 [ − log P ( D = 1 ∣ w ( t ) , w ( t + j ) ) − ∑ k = 1 , w k ∼ P ( w ) K log P ( D = 0 ∣ w ( t ) , w k ) ] \sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} [-\log P(D=1\mid w^{(t)},w^{(t+j)})-\sum_{k=1,w_k\sim P(w)^K}\log P(D=0\mid w^{(t)},w_k)] t=1∑T−m≤j≤m,j=0∑[−logP(D=1∣w(t),w(t+j))−k=1,wk∼P(w)K∑logP(D=0∣w(t),wk)]
根据这个损失函数的定义,我们可以直接使用二元交叉熵损失函数进行计算。
词嵌入进阶
在“Word2Vec的实现”一节中,我们在小规模数据集上训练了一个 Word2Vec 词嵌入模型,并通过词向量的余弦相似度搜索近义词。虽然 Word2Vec 已经能够成功地将离散的单词转换为连续的词向量,并能一定程度上地保存词与词之间的近似关系,但 Word2Vec 模型仍不是完美的,它还可以被进一步地改进:
- 子词嵌入(subword embedding):FastText 以固定大小的 n-gram 形式将单词更细致地表示为了子词的集合,而 BPE (byte pair encoding) 算法则能根据语料库的统计信息,自动且动态地生成高频子词的集合;
- GloVe 全局向量的词嵌入: 通过等价转换 Word2Vec 模型的条件概率公式,我们可以得到一个全局的损失函数表达,并在此基础上进一步优化模型。
实际中,我们常常在大规模的语料上训练这些词嵌入模型,并将预训练得到的词向量应用到下游的自然语言处理任务中。本节就将以 GloVe 模型为例,演示如何用预训练好的词向量来求近义词和类比词。
GloVe 全局向量的词嵌入
GloVe 模型
先简单回顾以下 Word2Vec 的损失函数(以 Skip-Gram 模型为例,不考虑负采样近似):
−
∑
t
=
1
T
∑
−
m
≤
j
≤
m
,
j
≠
0
log
P
(
w
(
t
+
j
)
∣
w
(
t
)
)
-\sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} \log P(w^{(t+j)}\mid w^{(t)})
−t=1∑T−m≤j≤m,j=0∑logP(w(t+j)∣w(t))
其中
P
(
w
j
∣
w
i
)
=
exp
(
u
j
⊤
v
i
)
∑
k
∈
V
exp
(
u
k
⊤
v
i
)
P(w_j\mid w_i) = \frac{\exp(\boldsymbol{u}_j^\top\boldsymbol{v}_i)}{\sum_{k\in\mathcal{V}}\exp(\boldsymbol{u}_k^\top\boldsymbol{v}_i)}
P(wj∣wi)=∑k∈Vexp(uk⊤vi)exp(uj⊤vi)
是
w
i
w_i
wi 为中心词,
w
j
w_j
wj 为背景词时 Skip-Gram 模型所假设的条件概率计算公式,我们将其简写为
q
i
j
q_{ij}
qij。
注意到此时我们的损失函数中包含两个求和符号,它们分别枚举了语料库中的每个中心词和其对应的每个背景词。实际上我们还可以采用另一种计数方式,那就是直接枚举每个词分别作为中心词和背景词的情况:
−
∑
i
∈
V
∑
j
∈
V
x
i
j
log
q
i
j
-\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} x_{ij}\log q_{ij}
−i∈V∑j∈V∑xijlogqij
其中
x
i
j
x_{ij}
xij 表示整个数据集中
w
j
w_j
wj 作为
w
i
w_i
wi 的背景词的次数总和。
我们还可以将该式进一步地改写为交叉熵 (cross-entropy) 的形式如下:
−
∑
i
∈
V
x
i
∑
j
∈
V
p
i
j
log
q
i
j
-\sum_{i\in\mathcal{V}}x_i\sum_{j\in\mathcal{V}}p_{ij} \log q_{ij}
−i∈V∑xij∈V∑pijlogqij
其中
x
i
x_i
xi 是
w
i
w_i
wi 的背景词窗大小总和,
p
i
j
=
x
i
j
/
x
i
p_{ij}=x_{ij}/x_i
pij=xij/xi 是
w
j
w_j
wj 在
w
i
w_i
wi 的背景词窗中所占的比例。
从这里可以看出,我们的词嵌入方法实际上就是想让模型学出 w j w_j wj 有多大概率是 w i w_i wi 的背景词,而真实的标签则是语料库上的统计数据。同时,语料库中的每个词根据 x i x_i xi 的不同,在损失函数中所占的比重也不同。
注意到目前为止,我们只是改写了 Skip-Gram 模型损失函数的表面形式,还没有对模型做任何实质上的改动。而在 Word2Vec 之后提出的 GloVe 模型,则是在之前的基础上做出了以下几点改动:
- 使用非概率分布的变量 p i j ′ = x i j p'_{ij}=x_{ij} pij′=xij 和 q ′ i j = exp ( u j ⊤ v i ) q′_{ij}=\exp(\boldsymbol{u}^\top_j\boldsymbol{v}_i) q′ij=exp(uj⊤vi),并对它们取对数;
- 为每个词 w i w_i wi 增加两个标量模型参数:中心词偏差项 b i b_i bi 和背景词偏差项 c i c_i ci,松弛了概率定义中的规范性;
- 将每个损失项的权重 x i x_i xi 替换成函数 h ( x i j ) h(x_{ij}) h(xij),权重函数 h ( x ) h(x) h(x) 是值域在 [ 0 , 1 ] [0,1] [0,1] 上的单调递增函数,松弛了中心词重要性与 x i x_i xi 线性相关的隐含假设;
- 用平方损失函数替代了交叉熵损失函数。
综上,我们获得了 GloVe 模型的损失函数表达式:
∑
i
∈
V
∑
j
∈
V
h
(
x
i
j
)
(
u
j
⊤
v
i
+
b
i
+
c
j
−
log
x
i
j
)
2
\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} h(x_{ij}) (\boldsymbol{u}^\top_j\boldsymbol{v}_i+b_i+c_j-\log x_{ij})^2
i∈V∑j∈V∑h(xij)(uj⊤vi+bi+cj−logxij)2
由于这些非零 x i j x_{ij} xij 是预先基于整个数据集计算得到的,包含了数据集的全局统计信息,因此 GloVe 模型的命名取“全局向量”(Global Vectors)之意。
载入预训练的 GloVe 向量
GloVe 官方 提供了多种规格的预训练词向量,语料库分别采用了维基百科、CommonCrawl和推特等,语料库中词语总数也涵盖了从60亿到8,400亿的不同规模,同时还提供了多种词向量维度供下游模型使用。
torchtext.vocab
中已经支持了 GloVe, FastText, CharNGram 等常用的预训练词向量,我们可以通过声明 torchtext.vocab.GloVe
类的实例来加载预训练好的 GloVe 词向量。
求类比词
除了求近义词以外,我们还可以使用预训练词向量求词与词之间的类比关系,例如“man”之于“woman”相当于“son”之于“daughter”。求类比词问题可以定义为:对于类比关系中的4个词“
a
a
a 之于
b
b
b 相当于
c
c
c 之于
d
d
d”,给定前3个词
a
,
b
,
c
a,b,c
a,b,c 求
d
d
d。求类比词的思路是,搜索与
vec
(
c
)
+
vec
(
b
)
−
vec
(
a
)
\text{vec}(c)+\text{vec}(b)−\text{vec}(a)
vec(c)+vec(b)−vec(a) 的结果向量最相似的词向量,其中
vec
(
w
)
\text{vec}(w)
vec(w) 为
w
w
w 的词向量。
文本分类
图像增广
大规模数据集是成功应用深度神经网络的前提。图像增广(image augmentation)技术通过对训练图像做一系列随机改变,来产生相似但又不同的训练样本,从而扩大训练数据集的规模。图像增广的另一种解释是,随机改变训练样本可以降低模型对某些属性的依赖,从而提高模型的泛化能力。例如,我们可以对图像进行不同方式的裁剪,使感兴趣的物体出现在不同位置,从而减轻模型对物体出现位置的依赖性。我们也可以调整亮度、色彩等因素来降低模型对色彩的敏感度。可以说,在当年AlexNet的成功中,图像增广技术功不可没。
定义绘图函数show_images
# 本函数已保存在d2lzh_pytorch包中方便以后使用
def show_images(imgs, num_rows, num_cols, scale=2):
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
for i in range(num_rows):
for j in range(num_cols):
axes[i][j].imshow(imgs[i * num_cols + j])
axes[i][j].axes.get_xaxis().set_visible(False)
axes[i][j].axes.get_yaxis().set_visible(False)
return axes
此函数可以绘制多张图片,图片数量为num_rows*num_cols
,这些图片会被保存在第一个参数imgs
数组中。而axes[i][j].axes.get_xaxis().set_visible(False),axes[i[j].axes.get_yaxis().set_visible(False)
分别将x轴和y轴删除。
微调(迁移学习)
假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户。一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然后在收集到的图像数据集上训练一个分类模型。这个椅子数据集虽然可能比Fashion-MNIST数据集要庞大,但样本数仍然不及ImageNet数据集中样本数的十分之一。这可能会导致适用于ImageNet数据集的复杂模型在这个椅子数据集上过拟合。同时,因为数据量有限,最终训练得到的模型的精度也可能达不到实用的要求。
为了应对上述问题,一个显而易见的解决办法是收集更多的数据。然而,收集和标注数据会花费大量的时间和资金。例如,为了收集ImageNet数据集,研究人员花费了数百万美元的研究经费。虽然目前的数据采集成本已降低了不少,但其成本仍然不可忽略。
另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别椅子也可能同样有效。
本节我们介绍迁移学习中的一种常用技术:微调(fine tuning)。如图所示,微调由以下4步构成。
- 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
- 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
- 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
- 在目标数据集(如椅子数据集)上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。
pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)
此时,pretrained_net
的fc
层就被随机初始化了,但是其他层依然保存着预训练得到的参数。由于是在很大的ImageNet数据集上预训练的,所以参数已经足够好,因此一般只需使用较小的学习率来微调这些参数,而fc
中的随机初始化参数一般需要更大的学习率从头训练。PyTorch可以方便的对模型的不同部分设置不同的学习参数,我们在下面代码中将fc
的学习率设为已经预训练过的部分的10倍。
output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id(p) not in output_params, pretrained_net.parameters())
lr = 0.01
optimizer = optim.SGD([{'params': feature_params},
{'params': pretrained_net.fc.parameters(), 'lr': lr * 10}],
lr=lr, weight_decay=0.001)
其中:
setup optimizer
params = filter(lambda p: p.requires_grad, model.parameters())
optimizer = torch.optim.Adam(params, lr=1e-4)
将满足条件的参数的 requires_grad 属性设置为False, 同时 filter 函数将模型中属性 requires_grad = True 的参数帅选出来,传到优化器(以Adam为例)中,只有这些参数会被求导数和更新。