目录
过拟合、欠拟合及其解决方案
训练误差和泛化误差
训练误差(training error)和泛化误差(generalization error),通俗来讲,前者指模型在训练数据集上表现出的误差,后者指模型在任意一个测试数据样本上表现出的误差的期望,并常常通过测试数据集上的误差来近似。
过拟合与欠拟合
一类是模型无法得到较低的训练误差,我们将这一现象称作欠拟合(underfitting);
另一类是模型的训练误差远小于它在测试数据集上的误差,我们称该现象为过拟合(overfitting)。 在实践中,我们要尽可能同时应对欠拟合和过拟合。虽然有很多因素可能导致这两种拟合问题,在这里我们重点讨论两个因素:模型复杂度和训练数据集大小。
-
欠拟合现象:模型无法达到一个较低的误差
-
过拟合现象:训练误差较低但是泛化误差依然较高,二者相差较大
应对过拟合有权重衰减法(L2范数正则化)和丢弃法。
权重衰减法(L2范数正则化)
L 2 L_2 L2范数正则化在模型原损失函数基础上添加 L 2 L_2 L2范数惩罚项,从而得到训练所需要最小化的函数。 L 2 L_2 L2范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积。以线性回归中的线性回归损失函数为例
ℓ
(
w
1
,
w
2
,
b
)
=
1
n
∑
i
=
1
n
1
2
(
x
1
(
i
)
w
1
+
x
2
(
i
)
w
2
+
b
−
y
(
i
)
)
2
\ell(w_1, w_2, b) = \frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right)^2
ℓ(w1,w2,b)=n1i=1∑n21(x1(i)w1+x2(i)w2+b−y(i))2
其中
w
1
,
w
2
w_1, w_2
w1,w2是权重参数,
b
b
b是偏差参数,样本
i
i
i的输入为
x
1
(
i
)
,
x
2
(
i
)
x_1^{(i)}, x_2^{(i)}
x1(i),x2(i),标签为
y
(
i
)
y^{(i)}
y(i),样本数为
n
n
n。将权重参数用向量
w
=
[
w
1
,
w
2
]
\boldsymbol{w} = [w_1, w_2]
w=[w1,w2]表示,带有
L
2
L_2
L2范数惩罚项的新损失函数为
ℓ ( w 1 , w 2 , b ) + λ 2 n ∣ w ∣ 2 , \ell(w_1, w_2, b) + \frac{\lambda}{2n} |\boldsymbol{w}|^2, ℓ(w1,w2,b)+2nλ∣w∣2,
其中超参数
λ
>
0
\lambda > 0
λ>0。当权重参数均为0时,惩罚项最小。当
λ
\lambda
λ较大时,惩罚项在损失函数中的比重较大,这通常会使学到的权重参数的元素较接近0。当
λ
\lambda
λ设为0时,惩罚项完全不起作用。上式中
L
2
L_2
L2范数平方
∣
w
∣
2
|\boldsymbol{w}|^2
∣w∣2展开后得到
w
1
2
+
w
2
2
w_1^2 + w_2^2
w12+w22。
有了
L
2
L_2
L2范数惩罚项后,在小批量随机梯度下降中,我们将线性回归一节中权重
w
1
w_1
w1和
w
2
w_2
w2的迭代方式更改为
w 1 ← ( 1 − η λ ∣ B ∣ ) w 1 − η ∣ B ∣ ∑ i ∈ B x 1 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) , w 2 ← ( 1 − η λ ∣ B ∣ ) w 2 − η ∣ B ∣ ∑ i ∈ B x 2 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) . \begin{aligned} w_1 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_1^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right),\\ w_2 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_2^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right). \end{aligned} w1w2←(1−∣B∣ηλ)w1−∣B∣ηi∈B∑x1(i)(x1(i)w1+x2(i)w2+b−y(i)),←(1−∣B∣ηλ)w2−∣B∣ηi∈B∑x2(i)(x1(i)w1+x2(i)w2+b−y(i)).
可见, L 2 L_2 L2范数正则化令权重 w 1 w_1 w1和 w 2 w_2 w2先自乘小于1的数,再减去不含惩罚项的梯度。因此, L 2 L_2 L2范数正则化又叫权重衰减。权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。
丢弃法
多层感知机中神经网络图描述了一个单隐藏层的多层感知机。其中输入个数为4,隐藏单元个数为5,且隐藏单元 h i h_i hi( i = 1 , … , 5 i=1, \ldots, 5 i=1,…,5)的计算表达式为
h i = ϕ ( x 1 w 1 i + x 2 w 2 i + x 3 w 3 i + x 4 w 4 i + b i ) h_i = \phi\left(x_1 w_{1i} + x_2 w_{2i} + x_3 w_{3i} + x_4 w_{4i} + b_i\right) hi=ϕ(x1w1i+x2w2i+x3w3i+x4w4i+bi)
这里 ϕ \phi ϕ是激活函数, x 1 , … , x 4 x_1, \ldots, x_4 x1,…,x4是输入,隐藏单元 i i i的权重参数为 w 1 i , … , w 4 i w_{1i}, \ldots, w_{4i} w1i,…,w4i,偏差参数为 b i b_i bi。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为 p p p,那么有 p p p的概率 h i h_i hi会被清零,有 1 − p 1-p 1−p的概率 h i h_i hi会除以 1 − p 1-p 1−p做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量 ξ i \xi_i ξi为0和1的概率分别为 p p p和 1 − p 1-p 1−p。使用丢弃法时我们计算新的隐藏单元 h i ′ h_i' hi′
h i ′ = ξ i 1 − p h i h_i' = \frac{\xi_i}{1-p} h_i hi′=1−pξihi
由于 E ( ξ i ) = 1 − p E(\xi_i) = 1-p E(ξi)=1−p,因此
E ( h i ′ ) = E ( ξ i ) 1 − p h i = h i E(h_i') = \frac{E(\xi_i)}{1-p}h_i = h_i E(hi′)=1−pE(ξi)hi=hi
即丢弃法不改变其输入的期望值。让我们对之前多层感知机的神经网络中的隐藏层使用丢弃法,一种可能的结果如图所示,其中 h 2 h_2 h2和 h 5 h_5 h5被清零。这时输出值的计算不再依赖 h 2 h_2 h2和 h 5 h_5 h5,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即 h 1 , … , h 5 h_1, \ldots, h_5 h1,…,h5都有可能被清零,输出层的计算无法过度依赖 h 1 , … , h 5 h_1, \ldots, h_5 h1,…,h5中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法
绘图函数
def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
legend=None, figsize=(3.5, 2.5)):
# d2l.set_figsize(figsize)
d2l.plt.xlabel(x_label)
d2l.plt.ylabel(y_label)
d2l.plt.semilogy(x_vals, y_vals)
if x2_vals and y2_vals:
d2l.plt.semilogy(x2_vals, y2_vals, linestyle=':')
d2l.plt.legend(legend)
plt.xlabel(x_label)
和plt.ylabel(y_label)
分别用来设置x轴和y轴,plt.legend(legend)
用来设置图例。
semilogy(x_vals, y_vals)
表示使用 y 轴的以 10 为基数的对数刻度和 x 轴的线性刻度创建一个绘图。
梯度消失、梯度爆炸
在激活函数的选择的地方讲过,在深层网络中尽量避免选择sigmoid和tanh激活函数,原因是这两个激活函数会把元素转换到[0, 1]和[-1, 1]之间,会加剧梯度消失的现象。
当神经网络的层数较多时,模型的数值稳定性容易变差。
假设一个层数为 L L L的多层感知机的第 l l l层 H ( l ) \boldsymbol{H}^{(l)} H(l)的权重参数为 W ( l ) \boldsymbol{W}^{(l)} W(l),输出层 H ( L ) \boldsymbol{H}^{(L)} H(L)的权重参数为 W ( L ) \boldsymbol{W}^{(L)} W(L)。为了便于讨论,不考虑偏差参数,且设所有隐藏层的激活函数为恒等映射(identity mapping) ϕ ( x ) = x \phi(x) = x ϕ(x)=x。给定输入 X \boldsymbol{X} X,多层感知机的第 l l l层的输出 H ( l ) = X W ( 1 ) W ( 2 ) … W ( l ) \boldsymbol{H}^{(l)} = \boldsymbol{X} \boldsymbol{W}^{(1)} \boldsymbol{W}^{(2)} \ldots \boldsymbol{W}^{(l)} H(l)=XW(1)W(2)…W(l)。此时,如果层数 l l l较大, H ( l ) \boldsymbol{H}^{(l)} H(l)的计算可能会出现衰减或爆炸。举个例子,假设输入和所有层的权重参数都是标量,如权重参数为0.2和5,多层感知机的第30层输出为输入 X \boldsymbol{X} X分别与 0. 2 30 ≈ 1 × 1 0 − 21 0.2^{30} \approx 1 \times 10^{-21} 0.230≈1×10−21(消失)和 5 30 ≈ 9 × 1 0 20 5^{30} \approx 9 \times 10^{20} 530≈9×1020(爆炸)的乘积。当层数较多时,梯度的计算也容易出现消失或爆炸。
循环神经网络进阶
GRU(Gate Recurrent Unit)
R
t
=
σ
(
X
t
W
x
r
+
H
t
−
1
W
h
r
+
b
r
)
Z
t
=
σ
(
X
t
W
x
z
+
H
t
−
1
W
h
z
+
b
z
)
H
~
t
=
t
a
n
h
(
X
t
W
x
h
+
(
R
t
⊙
H
t
−
1
)
W
h
h
+
b
h
)
H
t
=
Z
t
⊙
H
t
−
1
+
(
1
−
Z
t
)
⊙
H
~
t
R_{t} = σ(X_tW_{xr} + H_{t−1}W_{hr} + b_r)\\ Z_{t} = σ(X_tW_{xz} + H_{t−1}W_{hz} + b_z)\\ \widetilde{H}_t = tanh(X_tW_{xh} + (R_t ⊙H_{t−1})W_{hh} + b_h)\\ H_t = Z_t⊙H_{t−1} + (1−Z_t)⊙\widetilde{H}_t
Rt=σ(XtWxr+Ht−1Whr+br)Zt=σ(XtWxz+Ht−1Whz+bz)H
t=tanh(XtWxh+(Rt⊙Ht−1)Whh+bh)Ht=Zt⊙Ht−1+(1−Zt)⊙H
t
其中
⊙
⊙
⊙为按元素乘法。
• 重置⻔有助于捕捉时间序列⾥短期的依赖关系,控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态,可以用来丢弃与预测无关的历史信息;
• 更新⻔有助于捕捉时间序列⾥⻓期的依赖关系,可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系;
• 候选隐藏状态用来辅助稍后的隐藏状态计算,如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为[−1,1]。
模型设计
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
H_tilda = torch.tanh(torch.matmul(X, W_xh) + R * torch.matmul(H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
GRU的简洁实现
num_hiddens=256
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
lr = 1e-2 # 注意调整学习率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes)
在d2l.train_and_predict_rnn_pytorch()
括号中的第一个元素为使用的工具,此处载入的model为引用了gru_layer的model,就可以直接使用GRU进行训练了。
LSTM(长短期记忆long short-term memory)
长短期记忆(Long short-term memory, LSTM)是一种特殊的RNN,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。简单来说,就是相比普通的RNN,LSTM能够在更长的序列中有更好的表现。
详见LSTM理解
遗忘门:控制上一时间步的记忆细胞,主要是对上一个节点传进来的输入进行选择性忘记,简单来说就是会 “忘记不重要的,记住重要的”;
输入门:控制当前时间步的输入 ,将新的信息选择性的记录到输入中
输出门:控制从记忆细胞到隐藏状态
记忆细胞:⼀种特殊的隐藏状态的信息的流动
I
t
=
σ
(
X
t
W
x
i
+
H
t
−
1
W
h
i
+
b
i
)
F
t
=
σ
(
X
t
W
x
f
+
H
t
−
1
W
h
f
+
b
f
)
O
t
=
σ
(
X
t
W
x
o
+
H
t
−
1
W
h
o
+
b
o
)
C
~
t
=
t
a
n
h
(
X
t
W
x
c
+
H
t
−
1
W
h
c
+
b
c
)
C
t
=
F
t
⊙
C
t
−
1
+
I
t
⊙
C
~
t
H
t
=
O
t
⊙
t
a
n
h
(
C
t
)
I_t = σ(X_tW_{xi} + H_{t−1}W_{hi} + b_i) \\ F_t = σ(X_tW_{xf} + H_{t−1}W_{hf} + b_f)\\ O_t = σ(X_tW_{xo} + H_{t−1}W_{ho} + b_o)\\ \widetilde{C}_t = tanh(X_tW_{xc} + H_{t−1}W_{hc} + b_c)\\ C_t = F_t ⊙C_{t−1} + I_t ⊙\widetilde{C}_t\\ H_t = O_t⊙tanh(C_t)
It=σ(XtWxi+Ht−1Whi+bi)Ft=σ(XtWxf+Ht−1Whf+bf)Ot=σ(XtWxo+Ht−1Who+bo)C
t=tanh(XtWxc+Ht−1Whc+bc)Ct=Ft⊙Ct−1+It⊙C
tHt=Ot⊙tanh(Ct)
模型设计
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
LSTM的简洁实现
num_hiddens=256
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
lr = 1e-2 # 注意调整学习率
lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(lstm_layer, vocab_size)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes)
调用nn.LSTM
即可简洁实现LSTM。
每个循环单元中的记忆细胞和循环单元的值为LSTM模型中的隐状态,而非参数,因此不需要初始化。
深度循环神经网络
H
t
(
1
)
=
ϕ
(
X
t
W
x
h
(
1
)
+
H
t
−
1
(
1
)
W
h
h
(
1
)
+
b
h
(
1
)
)
\boldsymbol{H}_{t}^{(1)}=\phi\left(\boldsymbol{X}_{t} \boldsymbol{W}_{x h}^{(1)}+\boldsymbol{H}_{t-1}^{(1)} \boldsymbol{W}_{h h}^{(1)}+\boldsymbol{b}_{h}^{(1)}\right)
Ht(1)=ϕ(XtWxh(1)+Ht−1(1)Whh(1)+bh(1))
H
t
(
ℓ
)
=
ϕ
(
H
t
(
ℓ
−
1
)
W
x
h
(
ℓ
)
+
H
t
−
1
(
ℓ
)
W
h
h
(
ℓ
)
+
b
h
(
ℓ
)
)
\boldsymbol{H}_{t}^{(\ell)}=\phi\left(\boldsymbol{H}_{t}^{(\ell-1)} \boldsymbol{W}_{x h}^{(\ell)}+\boldsymbol{H}_{t-1}^{(\ell)} \boldsymbol{W}_{h h}^{(\ell)}+\boldsymbol{b}_{h}^{(\ell)}\right)
Ht(ℓ)=ϕ(Ht(ℓ−1)Wxh(ℓ)+Ht−1(ℓ)Whh(ℓ)+bh(ℓ))
O
t
=
H
t
(
L
)
W
h
q
+
b
q
\boldsymbol{O}_{t}=\boldsymbol{H}_{t}^{(L)} \boldsymbol{W}_{h q}+\boldsymbol{b}_{q}
Ot=Ht(L)Whq+bq
num_hiddens=256
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
lr = 1e-2 # 注意调整学习率
gru_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens,num_layers=2)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes)
在gru_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens,num_layers=2)
最后设计了有num_layers=2
代表有两层隐藏,若不设置的话该参数默认为1。
需要注意的是,深度循环神经网络并不是深度越深(num_layers参数值越大)效果越好,深度越深代表模型越复杂,对数据集的要求越高,内容也更加抽象,层数的加深也会导致模型的收敛变得困难。
双向循环神经网络
H
→
t
=
ϕ
(
X
t
W
x
h
(
f
)
+
H
→
t
−
1
W
h
h
(
f
)
+
b
h
(
f
)
)
H
←
t
=
ϕ
(
X
t
W
x
h
(
b
)
+
H
←
t
+
1
W
h
h
(
b
)
+
b
h
(
b
)
)
\begin{array}{l} {\overrightarrow{\boldsymbol{H}}_{t}=\phi\left(\boldsymbol{X}_{t} \boldsymbol{W}_{x h}^{(f)}+\overrightarrow{\boldsymbol{H}}_{t-1} \boldsymbol{W}_{h h}^{(f)}+\boldsymbol{b}_{h}^{(f)}\right)} \\ {\overleftarrow{\boldsymbol{H}}_{t}=\phi\left(\boldsymbol{X}_{t} \boldsymbol{W}_{x h}^{(b)}+\overleftarrow{\boldsymbol{H}}_{t+1} \boldsymbol{W}_{h h}^{(b)}+\boldsymbol{b}_{h}^{(b)}\right)} \end{array}
Ht=ϕ(XtWxh(f)+Ht−1Whh(f)+bh(f))Ht=ϕ(XtWxh(b)+Ht+1Whh(b)+bh(b))
→
H
t
=
(
H
t
,
H
t
)
\begin{aligned} &\rightarrow\\ &\boldsymbol{H}_{t}=\left(\boldsymbol{H}_{t}, \boldsymbol{H}_{t}\right) \end{aligned}
→Ht=(Ht,Ht)
O
t
=
H
t
W
h
q
+
b
q
\boldsymbol{O}_{t}=\boldsymbol{H}_{t} \boldsymbol{W}_{h q}+\boldsymbol{b}_{q}
Ot=HtWhq+bq
num_hiddens=128
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e-2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
lr = 1e-2 # 注意调整学习率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens,bidirectional=True)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes)
在gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens,bidirectional=True)
最后设计了有bidirectional=True
代表为双向循环神经网络,若为False则则为单向循环神经网络,默认值为False,即默认不采用双向循环神经网络。
机器翻译及相关技术
将一段文本从一种语言自动翻译为另一种语言,用神经网络解决这个问题通常称为神经机器翻译(NMT)。机器翻译是通过循环神经网络来实现的,但是使用的并不是传统的循环神经网络。
主要特征:输出是单词序列而不是单个单词。 输出序列的长度可能与源序列的长度不同。
数据预处理
对数据集进行预处理,清洗数据,即去除或者替换一些容易使程序报错的乱码、在字母与标点之间添加空格、将所有字母转换成小写形式等操作。
替换操作用replace()
函数。
str.replace(old, new[, max])
old – 将被替换的子字符串;new – 新字符串,用于替换old子字符串;max – 可选字符串, 替换不超过 max 次。
分词
经过数据预处理后的文本仍然是字符串,字符串的形式是机器无法理解的,因此需要进行分词操作,将字符串分词后形成由单词组成的列表。
此处用到了str.split()
函数。
str.split(str="", num=string.count(str)).
str – 分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等;
num – 分割次数,默认为 -1, 即分隔所有。
建立词典
在分词操作中得到了由单词组成的列表,建立词典这一步操作是将文本中出现的所有词构成一个词典,即得到一个由单词id组成的列表。
def build_vocab(tokens):
tokens = [token for line in tokens for token in line]
return d2l.data.base.Vocab(tokens, min_freq=3, use_special_tokens=True)
建立词典用到了一个很重要的类。
简要介绍一下这个类中一些参数的含义以及用到的一些函数:
(1)use_special_tokens
使用了一些特殊字符:
pad的作用是在采用批量样本训练时,对于长度不同的样本(句子),对于短的样本采用pad进行填充,使得每个样本的长度是一致的;
bos( begin of sentence)和eos(end of sentence)是用来表示一句话的开始和结尾;
unk(unknow)的作用是,处理遇到从未出现在预料库的词时都统一认为是unknow ,在代码中还可以将一些频率特别低的词也归为这一类。
建立词典时每个单词对应都有一个id,上面四个特殊字符已经被赋予了id为0、1、2、3,因此文本中的字符id应该从4开始。
(2)idx_to_token
建立了一个从id到单词的映射,id可以作为列表的id,直接从字典中取出单词,用列表取元素的方法即可。
(3)token_to_idx
建立了一个从单词到id的映射,因此需要使用字典结构dict()
,输入一个单词输出单词对应的id。
(4)__getitem__
和token_to_idx
类似,不过对于__getitem__
,输入的是一个单词列表,输出的是单词对应的id组成的一个列表。
载入数据集
一个句子在输入的时候要做到每一个batch里所有输入的句子长度是一样的,因为一个batch使用的是同一个时序大小的RNN,所以需要对句子进行pad,即长度不够的需要用pad进行填充。
补齐之后将每一个句子转换成一个id序列,即使用前面提到的__getitem__
函数,这个函数是个魔法函数。
魔法函数是Python中定义的,以__
开头,__
结尾,形如__fun__()
的函数,一般使用已经定义好了的即可。使用这样一些函数,可以让我们自定义的类有更加强大的特性。魔法函数一般是隐式调用的,不需要我们显示调用。
魔法函数不属于定义它的那个类,只是增强了类的一些功能。实现了特定的魔法函数之后,某些操作会变得特别简单。我们可以采用实现魔法函数来灵活地设计我们需要的类。
def build_array(lines, vocab, max_len, is_source):
lines = [vocab[line] for line in lines]
if not is_source:
lines = [[vocab.bos] + line + [vocab.eos] for line in lines]
array = torch.tensor([pad(line, max_len, vocab.pad) for line in lines])
valid_len = (array != vocab.pad).sum(1) #第一个维度
return array, valid_len
在这个代码段中vocab[line]
就实现了__getitem__
的功能。(我还是没有明白怎么调用的这个魔法函数…)。
is_source
用来判断是翻译样本还是翻译目标语言,这里表示如果是target句子的话,就需要在每行句子前后加上开始和结束特殊字符。
Encoder-Decoder
encoder:输入到隐藏状态
decoder:隐藏状态到输出
在之前已经介绍过机器翻译存在输入与输出长度不等的问题,这个问题就需要使用Encoder-Decoder来解决。
Encoder将输入翻译成一个语义编码,语义编码就相当于是一个隐藏状态(hidden state),再由Decoder翻译输出,当输出到eos(即结束符)时输出就停止。
class Encoder(nn.Module):
def __init__(self, **kwargs):
super(Encoder, self).__init__(**kwargs)
def forward(self, X, *args):
raise NotImplementedError
class Decoder(nn.Module):
def __init__(self, **kwargs):
super(Decoder, self).__init__(**kwargs)
def init_state(self, enc_outputs, *args):
raise NotImplementedError
def forward(self, X, state):
raise NotImplementedError
Encoder-Decoder可以应用在对话系统、生成式任务中。
Sequence to Sequence模型
在Encoder-Decoderencoder和decoder都是循环神经网络结构,其中encoder输出得到语义编码,decoder也可以叫做一个生成式的语言模型。循环神经网络的隐藏层状态一般都要初始化为零,而在decoder中隐藏层状态则被初始化为encoder输出得到的语义编码,训练时把bos开始符作为decoder的第一个输入。
训练和预测时decoder的每一个时序单元输出都会伴有一个隐藏状态的更新,这个更新的隐藏状态需要参与到下一个时序单元的输出中。
具体模型为:
sources为输入样本单词对应的id列表,这个列表不能直接输入到网络中去,而是在Embedding层先将列表对应转化为一个个形状大小相同的词向量,
decoder和encoder类似,多了一个dense层,dense层用来输出训练或预测的单词。
Encoder
class Seq2SeqEncoder(d2l.Encoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
self.num_hiddens=num_hiddens
self.num_layers=num_layers
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.LSTM(embed_size,num_hiddens, num_layers, dropout=dropout)
def begin_state(self, batch_size, device):
return [torch.zeros(size=(self.num_layers, batch_size, self.num_hiddens), device=device),
torch.zeros(size=(self.num_layers, batch_size, self.num_hiddens), device=device)]
def forward(self, X, *args):
X = self.embedding(X) # X shape: (batch_size, seq_len, embed_size)
X = X.transpose(0, 1) # RNN needs first axes to be time
# state = self.begin_state(X.shape[1], device=X.device)
out, state = self.rnn(X)
# The shape of out is (seq_len, batch_size, num_hiddens).
# state contains the hidden state and the memory cell
# of the last time step, the shape is (num_layers, batch_size, num_hiddens)
return out, state
Decoder
class Seq2SeqDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.LSTM(embed_size,num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Linear(num_hiddens,vocab_size)
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
def forward(self, X, state):
X = self.embedding(X).transpose(0, 1)
out, state = self.rnn(X, state)
# Make the batch to be the first dimension to simplify loss computation.
out = self.dense(out).transpose(0, 1)
return out, state
损失函数
在载入数据集产生batch时使用了padding将句子长度补齐,而在计算损失函数时需要避免补齐部分产生的loss,所以需要先将原来的句子超出有效长度的部分用0或者1进行替换。
def SequenceMask(X, X_len,value=0):
maxlen = X.size(1)
mask = torch.arange(maxlen)[None, :].to(X_len.device) < X_len[:, None]
X[~mask]=value
return X
X是一个batch的输入,X_len是有效长度。
定义损失函数:
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
# pred shape: (batch_size, seq_len, vocab_size)
# label shape: (batch_size, seq_len)
# valid_length shape: (batch_size, )
def forward(self, pred, label, valid_length):
# the sample weights shape should be (batch_size, seq_len)
weights = torch.ones_like(label)
weights = SequenceMask(weights, valid_length).float()
self.reduction='none'
output=super(MaskedSoftmaxCELoss, self).forward(pred.transpose(1,2), label)
return (output*weights).mean(dim=1)
其中# pred shape: (batch_size, seq_len, vocab_size)
为预测的结果,最后一个维度vocab_size的每个单元记录的是预测出结果对应建立的词典中每个词典的得分(可以理解为概率)。
训练
def train_ch7(model, data_iter, lr, num_epochs, device): # Saved in d2l
model.to(device)
optimizer = optim.Adam(model.parameters(), lr=lr)
loss = MaskedSoftmaxCELoss()
tic = time.time()
for epoch in range(1, num_epochs+1):
l_sum, num_tokens_sum = 0.0, 0.0
for batch in data_iter:
optimizer.zero_grad()
X, X_vlen, Y, Y_vlen = [x.to(device) for x in batch]
Y_input, Y_label, Y_vlen = Y[:,:-1], Y[:,1:], Y_vlen-1
Y_hat, _ = model(X, Y_input, X_vlen, Y_vlen)
l = loss(Y_hat, Y_label, Y_vlen).sum()
l.backward()
with torch.no_grad():
d2l.grad_clipping_nn(model, 5, device)
num_tokens = Y_vlen.sum().item()
optimizer.step()
l_sum += l.sum().item()
num_tokens_sum += num_tokens
if epoch % 50 == 0:
print("epoch {0:4d},loss {1:.3f}, time {2:.1f} sec".format(
epoch, (l_sum/num_tokens_sum), time.time()-tic))
tic = time.time()
要注意device这个参数,所有参与反向传播计算的参数都要放在同一个device上。
Y:bos word eos
Y_input:decoder的输入 bos word
Y_label:decoder输出的groundtruth真值 word eos
Y_vlen:原本认为的有效长度是 bos word eos的长度,实际label的有效长度是word eos的长度,所以需要减一
Y_hat:通过模型生成的预测的Y值
所以最终是利用Y_label、Y_hat、Y_vlen来计算loss。
with torch.no_grad():
d2l.grad_clipping_nn(model, 5, device)
这一段的作用是梯度裁剪。
Beam Search
在上一小节提到过,# pred shape: (batch_size, seq_len, vocab_size)
为预测的结果,最后一个维度vocab_size的每个单元记录的是预测出结果对应建立的词典中每个词典的得分(可以理解为概率),选取得分最高的一个词作为输出。这样会产生一个问题是输出只考虑当前时序下的最优解,并没有将整个语句的语义连贯性考虑进去。
因此产生了维比特算法,这个方法是在对每一个时刻每一个得分对应的输出都统计一遍最后选取一个总体得分最高的为语句连贯性合理性最佳的输出,但是这样做会导致搜索空间太大。基于以上两种方案,提出了Beam Search(集束搜索)。
比如当beam=2时,每一次输出选取得分最高的两个进行下一步预测,在下一步预测也选取得分最高的两个进行下下一步预测最后得到结果。
注意力机制与Seq2seq模型
注意力机制框架
Attention 是一种通用的带权池化方法,输入由两部分构成:询问(query)和键值对(key-value pairs)。 k i ∈ R d k , v i ∈ R d r . \mathbf{k}_{i} \in \mathbb{R}^{d_{k}}, \mathbf{v}_{i} \in \mathbb{R}^{d_{r}} . ki∈Rdk,vi∈Rdr. Query q ∈ R d 9 \mathbf{q} \in \mathbb{R}^{d_{9}} q∈Rd9 , attention layer得到输出与value的维度一致 o ∈ R d v \mathbf{o} \in \mathbb{R}^{d_{v}} o∈Rdv. 对于一个query来说,attention layer 会与每一个key计算注意力分数并进行权重的归一化,输出的向量 o o o则是value的加权求和,而每个key计算的权重与value一一对应。
为了计算输出,我们首先假设有一个函数 α \alpha α 用于计算query和key的相似性,然后可以计算所有的 attention scores a 1 , … , a n a_1, \ldots, a_n a1,…,an by
a i = α ( q , k i ) . a_i = \alpha(\mathbf q, \mathbf k_i). ai=α(q,ki).
我们使用 softmax函数 获得注意力权重:
b 1 , … , b n = softmax ( a 1 , … , a n ) . b_1, \ldots, b_n = \textrm{softmax}(a_1, \ldots, a_n). b1,…,bn=softmax(a1,…,an).
最终的输出就是value的加权求和:
o
=
∑
i
=
1
n
b
i
v
i
.
\mathbf o = \sum_{i=1}^n b_i \mathbf v_i.
o=i=1∑nbivi.
不同的attetion layer的区别在于score函数的选择,在本节的其余部分,我们将讨论两个常用的注意层 Dot-product Attention 和 Multilayer Perceptron Attention;随后我们将实现一个引入attention的seq2seq模型并在英法翻译语料上进行训练与测试。
Softmax屏蔽
def SequenceMask(X, X_len,value=-1e6):
maxlen = X.size(1)
#print(X.size(),torch.arange((maxlen),dtype=torch.float)[None, :],'\n',X_len[:, None] )
mask = torch.arange((maxlen),dtype=torch.float)[None, :] >= X_len[:, None]
#print(mask)
X[mask]=value
return X
这里先介绍torch.squeeze()和torch.unsqueeze()
两个函数,详见squeeze函数。
其中mask = torch.arange((maxlen),dtype=torch.float)[None, :] >= X_len[:, None]
Tensor的特殊索引方式详见Tensor中None索引。
点积注意力
The dot product 假设query和keys有相同的维度, 即 ∀ i , q , k i ∈ R d \forall i, \mathbf{q}, \mathbf{k}_{i} \in \mathbb{R}_{d} ∀i,q,ki∈Rd。通过计算query和key转置的乘积来计算attention score,通常还会除去 d \sqrt{d} d 减少计算出来的score对维度𝑑的依赖性,如下
α
(
q
,
k
)
=
⟨
q
,
k
⟩
/
d
\alpha(\mathbf{q}, \mathbf{k})=\langle\mathbf{q}, \mathbf{k}\rangle / \sqrt{d}
α(q,k)=⟨q,k⟩/d
假设
Q
∈
R
∧
{
m
×
d
}
\mathbf{Q} \in \mathbb{R}^{\wedge}\{m \times d\}
Q∈R∧{m×d}有
m
m
m 个query,
K
∈
R
n
×
d
\mathbf{K} \in \mathbb{R}^{n \times d}
K∈Rn×d有
n
n
n 个keys. 我们可以通过矩阵运算的方式计算所有
m
n
mn
mn 个score:
α
(
Q
,
K
)
=
Q
K
T
/
d
\alpha(\mathbf{Q}, \mathbf{K})=\mathbf{Q} \mathbf{K}^{T} / \sqrt{d}
α(Q,K)=QKT/d
现在让我们实现这个层,它支持一批查询和键值对。此外,它支持作为正则化随机删除一些注意力权重。
多层感知机注意力
在多层感知器中,我们首先将 query and keys 投影到
R
h
ℝ^ℎ
Rh .为了更具体,我们将可以学习的参数做如下映射
W
k
∈
R
h
×
d
k
,
W
q
∈
R
h
×
d
q
,
\mathbf{W}_{k} \in \mathbb{R}^{h \times d_{k}}, \mathbf{W}_{q} \in \mathbb{R}^{h \times d_{q}},
Wk∈Rh×dk,Wq∈Rh×dq, and
v
∈
R
h
\mathbf{v} \in \mathbb{R}^{h}
v∈Rh。将score函数定义
α
(
k
,
q
)
=
v
T
tanh
(
W
k
k
+
W
q
q
)
\alpha(\mathbf{k}, \mathbf{q})=\mathbf{v}^{T} \tanh \left(\mathbf{W}_{k} \mathbf{k}+\mathbf{W}_{q} \mathbf{q}\right)
α(k,q)=vTtanh(Wkk+Wqq)
然后将key 和 value 在特征的维度上合并(concatenate),然后送至 a single hidden layer perceptron 这层中 hidden layer 为 ℎ and 输出的size为 1 .隐层激活函数为tanh,无偏置.
引入注意力机制的Seq2seq模型
本节中将注意机制添加到sequence to sequence 模型中,以显式地使用权重聚合states。下图展示encoding 和decoding的模型结构,在时间步为t的时候。此刻attention layer保存着encodering看到的所有信息——即encoding的每一步输出。在decoding阶段,解码器的
t
t
t时刻的隐藏状态被当作query,encoder的每个时间步的hidden states作为key和value进行attention聚合. Attetion model的输出当作成上下文信息context vector,并与解码器输入
D
t
D_t
Dt拼接起来一起送到解码器:
下图展示了seq2seq机制的所以层的关系,下面展示了encoder和decoder的layer结构:
解码器
由于带有注意机制的seq2seq的编码器与之前章节中的Seq2SeqEncoder相同,所以在此处我们只关注解码器。我们添加了一个MLP注意层(MLPAttention),它的隐藏大小与解码器中的LSTM层相同。然后我们通过从编码器传递三个参数来初始化解码器的状态:
-
the encoder outputs of all timesteps:encoder输出的各个状态,被用于attetion layer的memory部分,有相同的key和values
-
the hidden state of the encoder’s final timestep:编码器最后一个时间步的隐藏状态,被用于初始化decoder 的hidden state
-
the encoder valid length: 编码器的有效长度,借此,注意层不会考虑编码器输出中的填充标记(Paddings)
在解码的每个时间步,我们使用解码器的最后一个RNN层的输出作为注意层的query。然后,将注意力模型的输出与输入嵌入向量连接起来,输入到RNN层。虽然RNN层隐藏状态也包含来自解码器的历史信息,但是attention model的输出显式地选择了enc_valid_len以内的编码器输出,这样attention机制就会尽可能排除其他不相关的信息。
习题知识点
(1)在Dot-product Attention中,key与query维度需要一致,在MLP Attention中则不需要。
(2)点积注意力层不引入新的模型参数,注意力掩码可以用来解决一组变长序列的编码问题。
(3)加入Attention机制的seq2seq模型的预测需人为设定终止条件,设定最长序列长度或者输出[EOS]结束符号,若不加以限制则可能生成无穷长度序列。每个时间步,解码器输入的语境向量(context vector)不相同,每个位置都会计算各自的attention输出。解码器RNN仍由编码器最后一个时间步的隐藏状态初始化。注意力机制本身有高效的并行性,但引入注意力并不能改变seq2seq内部RNN的迭代机制,因此无法加速模型训练。
(4)
卷积神经网络基础
for i in range(step):
Y_hat = conv2d(X)
l = ((Y_hat - Y) ** 2).sum()
l.backward()
# 梯度下降
conv2d.weight.data -= lr * conv2d.weight.grad
conv2d.bias.data -= lr * conv2d.bias.grad
# 梯度清零
conv2d.weight.grad.zero_()
conv2d.bias.grad.zero_()
if (i + 1) % 5 == 0:
print('Step %d, loss %.3f' % (i + 1, l.item()))
.data
仍保留,但建议使用 .detach()
, 区别在于 .data
返回和 x 的相同数据 tensor, 但不会加入到x的计算历史里,且require s_grad = False, 这样有些时候是不安全的, 因为 x.data
不能被 autograd 追踪求微分 。 .detach()
返回相同数据的 tensor ,且 requires_grad=False ,但能通过 in-place 操作报告给 autograd 在进行反向传播的时候.
在以上这段代码中,参数更新部分并不需要追踪梯度,所以需要使用.data
来进行参数更新。
特征图和感受野
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响特征图中元素 x x x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做 x x x的感受野(receptive field)。
填充和步幅
卷积层的两个超参数,即填充和步幅,它们可以对给定形状的输入和卷积核改变输出形状。
**填充(padding)**是指在输入高和宽的两侧填充元素(通常是0元素)。
如果原输入的高和宽是
n
h
n_h
nh和
n
w
n_w
nw,卷积核的高和宽是
k
h
k_h
kh和
k
w
k_w
kw,在高的两侧一共填充
p
h
p_h
ph行,在宽的两侧一共填充
p
w
p_w
pw列,则输出形状为:
(
n
h
+
p
h
−
k
h
+
1
)
×
(
n
w
+
p
w
−
k
w
+
1
)
\left(n_{h}+p_{h}-k_{h}+1\right) \times\left(n_{w}+p_{w}-k_{w}+1\right)
(nh+ph−kh+1)×(nw+pw−kw+1)
我们在卷积神经网络中使用奇数高宽的核,比如
3
×
3
3 \times 3
3×3,
5
×
5
5 \times 5
5×5的卷积核,对于高度(或宽度)为大小为
2
k
+
1
2 k + 1
2k+1的核,令步幅为1,在高(或宽)两侧选择大小为
k
k
k的填充,便可保持输入与输出尺寸相同。
在互相关运算中,卷积核在输入数组上滑动,每次滑动的行数与列数即是步幅(stride)。
如果原输入的高和宽是
n
h
n_h
nh和
n
w
n_w
nw,卷积核的高和宽是
k
h
k_h
kh和
k
w
k_w
kw,在高的两侧一共填充
p
h
p_h
ph行,在宽的两侧一共填充
p
w
p_w
pw列,则输出形状为:
⌊
(
n
h
+
p
h
−
k
h
+
s
h
)
/
s
h
⌋
×
⌊
(
n
w
+
p
w
−
k
w
+
s
w
)
/
s
w
⌋
\left\lfloor\left(n_{h}+p_{h}-k_{h}+s_{h}\right) / s_{h}\right\rfloor \times\left\lfloor\left(n_{w}+p_{w}-k_{w}+s_{w}\right) / s_{w}\right\rfloor
⌊(nh+ph−kh+sh)/sh⌋×⌊(nw+pw−kw+sw)/sw⌋
我们在卷积神经网络中使用奇数高宽的核,比如
3
×
3
3 \times 3
3×3,
5
×
5
5 \times 5
5×5的卷积核,对于高度(或宽度)为大小为
2
k
+
1
2 k + 1
2k+1的核,令步幅为1,在高(或宽)两侧选择大小为
k
k
k的填充,便可保持输入与输出尺寸相同。
卷积层与全连接层的对比
二维卷积层经常用于处理图像,与此前的全连接层相比,它主要有两个优势:
一是全连接层把图像展平成一个向量,在输入图像上相邻的元素可能因为展平操作不再相邻,网络难以捕捉局部信息。而卷积层的设计,天然地具有提取局部信息的能力。
卷积层的简洁实现
我们使用Pytorch中的nn.Conv2d
类来实现二维卷积层,主要关注以下几个构造函数参数:
in_channels
(python:int) – Number of channels in the input imagout_channels
(python:int) – Number of channels produced by the convolutionkernel_size
(python:int or tuple) – Size of the convolving kernelstride
(python:int or tuple, optional) – Stride of the convolution. Default: 1padding
(python:int or tuple, optional) – Zero-padding added to both sides of the input. padding指的是上下两侧同时扩充的层数,总的扩展层数为padding*2.- Default: 0
bias
(bool, optional) – If True, adds a learnable bias to the output. Default: True
以上参数凡是涉及到类型为int整数型或者tuple元组型都对应着两种情况:一是卷积核的长宽、宽度和高度上的步幅、行方向和列方向填充数相等,则为int整数型;二是以上参数不相等,则为tuple元组型。
forward
函数的参数为一个四维张量,形状为
(
N
,
C
i
n
,
H
i
n
,
W
i
n
)
(N, C_{in}, H_{in}, W_{in})
(N,Cin,Hin,Win),返回值也是一个四维张量,形状为
(
N
,
C
o
u
t
,
H
o
u
t
,
W
o
u
t
)
(N, C_{out}, H_{out}, W_{out})
(N,Cout,Hout,Wout),其中
N
N
N是批量大小,
C
,
H
,
W
C, H, W
C,H,W分别表示通道数、高度、宽度。
池化层
池化层主要用于缓解卷积层对位置的过度敏感性。同卷积层一样,池化层每次对输入数据的一个固定形状窗口(又称池化窗口)中的元素计算输出,池化层直接计算池化窗口内元素的最大值或者平均值,该运算也分别叫做最大池化或平均池化。
池化层也可以在输入的高和宽两侧填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的工作机制一样。
在处理多通道输入数据时,池化层对每个输入通道分别池化,但不会像卷积层那样将各通道的结果按通道相加。这意味着池化层的输出通道数与输入通道数相等。
池化层的简洁实现
我们使用Pytorch中的nn.MaxPool2d
实现最大池化层,关注以下构造函数参数:
kernel_size
– the size of the window to take a max overstride
– the stride of the window. Default value is kernel_sizepadding
– implicit zero padding to be added on both sides
forward
函数的参数为一个四维张量,形状为
(
N
,
C
,
H
i
n
,
W
i
n
)
(N, C, H_{in}, W_{in})
(N,C,Hin,Win),返回值也是一个四维张量,形状为
(
N
,
C
,
H
o
u
t
,
W
o
u
t
)
(N, C, H_{out}, W_{out})
(N,C,Hout,Wout),其中
N
N
N是批量大小,
C
,
H
,
W
C, H, W
C,H,W分别表示通道数、高度、宽度。
LeNet
LeNet分为卷积层块和全连接层块。
卷积层块里的基本单位是卷积层后接平均池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的平均池化层则用来降低卷积层对位置的敏感性。
Sequential类实现LeNet模型
#net
class Flatten(torch.nn.Module): #展平操作
def forward(self, x):
return x.view(x.shape[0], -1)
class Reshape(torch.nn.Module): #将图像大小重定型
def forward(self, x):
return x.view(-1,1,28,28) #(B x C x H x W)
net = torch.nn.Sequential( #Lelet
Reshape(),
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28 =>b*6*28*28
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*6*28*28 =>b*6*14*14
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), #b*6*14*14 =>b*16*10*10
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*16*10*10 => b*16*5*5
Flatten(), #b*16*5*5 => b*400
nn.Linear(in_features=16*5*5, out_features=120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
因为卷积神经网络计算比多层感知机要复杂,建议使用GPU来加速计算。我们查看看是否可以用GPU,如果成功则使用cuda:0
,否则仍然使用cpu
。
# This function has been saved in the d2l package for future use
#use GPU
def try_gpu():
"""If GPU is available, return torch.device as cuda:0; else return torch.device as cpu."""
if torch.cuda.is_available():
device = torch.device('cuda:0')
else:
device = torch.device('cpu')
return device
device = try_gpu()
device
注:
- LeNet模型中,90%以上的参数集中在全连接层块。
- 在通过卷积层或池化层后,输出的高和宽可能减小,为了尽可能保留输入的特征,我们可以在减小高宽的同时增加通道数。
- LeNet在大的真实数据集上的表现并不尽如⼈意。
1.神经网络计算复杂。
2.还没有⼤量深⼊研究参数初始化和⾮凸优化算法等诸多领域。
卷积神经网络进阶
机器学习的特征提取:手工定义的特征提取函数
神经网络的特征提取:通过学习得到数据的多级表征,并逐级表⽰越来越抽象的概念或模式。
深度卷积神经网络(AlexNet)
AlexNet首次证明了学习到的特征可以超越⼿⼯设计的特征,从而⼀举打破计算机视觉研究的前状。
特征:
- 8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。
- 将sigmoid激活函数改成了更加简单的ReLU激活函数。
- 用Dropout来控制全连接层的模型复杂度。
- 引入数据增强,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),
#由于使用CPU镜像,精简网络,若为GPU镜像可添加该层
#nn.Linear(4096, 4096),
#nn.ReLU(),
#nn.Dropout(0.5),
# 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10),
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
使用重复元素的网络(VGG)
VGG:通过重复使⽤简单的基础块来构建深度模型。
Block:数个相同的填充为1、窗口形状为
3
×
3
3\times 3
3×3的卷积层,接上一个步幅为2、窗口形状为
2
×
2
2\times 2
2×2的最大池化层。
卷积层保持输入的高和宽不变,而池化层则对其减半。
VGG的简单实现
def vgg_block(num_convs, in_channels, out_channels): #卷积层个数,输入通道数,输出通道数
blk = []
for i in range(num_convs):
if i == 0:
blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
else:
blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
blk.append(nn.ReLU())
blk.append(nn.MaxPool2d(kernel_size=2, stride=2)) # 这里会使宽高减半
return nn.Sequential(*blk)
⽹络中的⽹络(NiN)
LeNet、AlexNet和VGG:先以由卷积层构成的模块充分抽取 空间特征,再以由全连接层构成的模块来输出分类结果。
NiN:串联多个由卷积层和“全连接”层构成的小⽹络来构建⼀个深层⽹络。
⽤了输出通道数等于标签类别数的NiN块,然后使⽤全局平均池化层对每个通道中所有元素求平均并直接⽤于分类。
1×1卷积核作用
1.放缩通道数:通过控制卷积核的数量达到通道数的放缩。
2.增加非线性。1×1卷积核的卷积过程相当于全连接层的计算过程,并且还加入了非线性激活函数,从而可以增加网络的非线性。
3.计算参数少
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU())
return blk
NiN重复使⽤由卷积层和代替全连接层的1×1卷积层构成的NiN块来构建深层⽹络。
NiN去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数 的NiN块和全局平均池化层。
NiN的以上设计思想影响了后⾯⼀系列卷积神经⽹络的设计。
GoogLeNet
- 由Inception基础块组成。
- Inception块相当于⼀个有4条线路的⼦⽹络。它通过不同窗口形状的卷积层和最⼤池化层来并⾏抽取信息,并使⽤1×1卷积层减少通道数从而降低模型复杂度。
- 可以⾃定义的超参数是每个层的输出通道数,我们以此来控制模型复杂度。
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
return torch.cat((p1, p2, p3, p4), dim=1) # 在通道维上连结输出
基于inception块构建GoogLeNet如图:
注:
- VGG相对AlexNet网络表达能力更强。
Transformer
- CNNs 易于并行化,却不适合捕捉变长序列内的依赖关系。
- RNNs 适合捕捉长距离变长序列的依赖,但是却难以实现并行化处理序列。
与 seq2seq模型相似,Transformer同样基于编码器-解码器架构,其区别主要在于以下三点:
(1) Transformer blocks:将seq2seq模型重的循环网络替换为了Transformer Blocks,该模块包含一个多头注意力层(Multi-head Attention Layers)以及两个position-wise feed-forward (2)networks(FFN)。对于解码器来说,另一个多头注意力层被用于接受编码器的隐藏状态。
(3)Add and norm:多头注意力层和前馈网络的输出被送到两个“add and norm”层进行处理,该层包含残差结构以及层归一化。
Position encoding:由于自注意力层并没有区分元素的顺序,所以一个位置编码层被用于向序列元素里添加位置信息。
多头注意力层
在我们讨论多头注意力层之前,先来迅速理解以下自注意力(self-attention)的结构。自注意力模型是一个正规的注意力模型,序列的每一个元素对应的key,value,query是完全一致的。如图10.3.2 自注意力输出了一个与输入长度相同的表征序列,与循环神经网络相比,自注意力对每个元素输出的计算是并行的,所以我们可以高效的实现这个模块。
F i g . 10.3.2 自 注 意 力 结 构 Fig.10.3.2\ 自注意力结构 Fig.10.3.2 自注意力结构
多头注意力层包含 h h h个并行的自注意力层,每一个这种层被成为一个head。对每个头来说,在进行注意力计算之前,我们会将query、key和value用三个现行层进行映射,这 h h h个注意力头的输出将会被拼接之后输入最后一个线性层进行整合。
F i g . 10.3.3 多 头 注 意 力 Fig.10.3.3\ 多头注意力 Fig.10.3.3 多头注意力
假设query,key和value的维度分别是 d q d_q dq、 d k d_k dk和 d v d_v dv。那么对于每一个头 i = 1 , … , h i=1,\ldots,h i=1,…,h,我们可以训练相应的模型权重 W q ( i ) ∈ R p q × d q W_q^{(i)} \in \mathbb{R}^{p_q\times d_q} Wq(i)∈Rpq×dq、 W k ( i ) ∈ R p k × d k W_k^{(i)} \in \mathbb{R}^{p_k\times d_k} Wk(i)∈Rpk×dk和 W v ( i ) ∈ R p v × d v W_v^{(i)} \in \mathbb{R}^{p_v\times d_v} Wv(i)∈Rpv×dv,以得到每个头的输出:
o ( i ) = a t t e n t i o n ( W q ( i ) q , W k ( i ) k , W v ( i ) v ) o^{(i)} = attention(W_q^{(i)}q, W_k^{(i)}k, W_v^{(i)}v) o(i)=attention(Wq(i)q,Wk(i)k,Wv(i)v)
这里的attention可以是任意的attention function,比如前一节介绍的dot-product attention以及MLP attention。之后我们将所有head对应的输出拼接起来,送入最后一个线性层进行整合,这个层的权重可以表示为 W o ∈ R d 0 × h p v W_o\in \mathbb{R}^{d_0 \times hp_v} Wo∈Rd0×hpv
o = W o [ o ( 1 ) , … , o ( h ) ] o = W_o[o^{(1)}, \ldots, o^{(h)}] o=Wo[o(1),…,o(h)]
接下来我们就可以来实现多头注意力了,假设我们有h个头,隐藏层权重 h i d d e n _ s i z e = p q = p k = p v hidden\_size = p_q = p_k = p_v hidden_size=pq=pk=pv 与query,key,value的维度一致。除此之外,因为多头注意力层保持输入与输出张量的维度不变,所以输出feature的维度也设置为 d 0 = h i d d e n _ s i z e d_0 = hidden\_size d0=hidden_size。
Position-wise Feed-Foward Networks基于位置的前馈网络
Transformer 模块另一个非常重要的部分就是基于位置的前馈网络(FFN),它接受一个形状为(batch_size,seq_length, feature_size)的三维张量。Position-wise FFN由两个全连接层组成,他们作用在最后一维上。因为序列的每个位置的状态都会被单独地更新,所以我们称他为position-wise,这等效于一个1x1的卷积。
Add and Norm
除了上面两个模块之外,Transformer还有一个重要的相加归一化层,它可以平滑地整合输入和其他层的输出,因此我们在每个多头注意力层和FFN层后面都添加一个含残差连接的Layer Norm层。这里 Layer Norm 与7.5小节的Batch Norm很相似,唯一的区别在于Batch Norm是对于batch size这个维度进行计算均值和方差的,而Layer Norm则是对最后一维进行计算。层归一化可以防止层内的数值变化过大,从而有利于加快训练速度并且提高泛化性能。 (ref)
这一层并不会改变输入张量的维度。
位置编码
与循环神经网络不同,无论是多头注意力网络还是前馈神经网络都是独立地对每个位置的元素进行更新,这种特性帮助我们实现了高效的并行,却丢失了重要的序列顺序的信息。为了更好的捕捉序列信息,Transformer模型引入了位置编码去保持输入序列元素的位置。
假设输入序列的嵌入表示 X ∈ R l × d X\in \mathbb{R}^{l\times d} X∈Rl×d, 序列长度为 l l l嵌入向量维度为 d d d,则其位置编码为 P ∈ R l × d P \in \mathbb{R}^{l\times d} P∈Rl×d ,输出的向量就是二者相加 X + P X + P X+P。
位置编码是一个二维的矩阵,i对应着序列中的顺序,j对应其embedding vector内部的维度索引。我们可以通过以下等式计算位置编码:
P i , 2 j = s i n ( i / 1000 0 2 j / d ) P_{i,2j} = sin(i/10000^{2j/d}) Pi,2j=sin(i/100002j/d)
P i , 2 j + 1 = c o s ( i / 1000 0 2 j / d ) P_{i,2j+1} = cos(i/10000^{2j/d}) Pi,2j+1=cos(i/100002j/d)
f
o
r
i
=
0
,
…
,
l
−
1
a
n
d
j
=
0
,
…
,
⌊
(
d
−
1
)
/
2
⌋
for\ i=0,\ldots, l-1\ and\ j=0,\ldots,\lfloor (d-1)/2 \rfloor
for i=0,…,l−1 and j=0,…,⌊(d−1)/2⌋
F i g . 10.3.4 位 置 编 码 Fig. 10.3.4\ 位置编码 Fig.10.3.4 位置编码
编码器
编码器包含一个多头注意力层,一个position-wise FFN,和两个 Add and Norm层。对于attention模型以及FFN模型,我们的输出维度都是与embedding维度一致的,这也是由于残差连接天生的特性导致的,因为我们要将前一层的输出与原始输入相加并归一化。
class EncoderBlock(nn.Module):
def __init__(self, embedding_size, ffn_hidden_size, num_heads,
dropout, **kwargs):
super(EncoderBlock, self).__init__(**kwargs)
self.attention = MultiHeadAttention(embedding_size, embedding_size, num_heads, dropout)
self.addnorm_1 = AddNorm(embedding_size, dropout)
self.ffn = PositionWiseFFN(embedding_size, ffn_hidden_size, embedding_size)
self.addnorm_2 = AddNorm(embedding_size, dropout)
def forward(self, X, valid_length):
Y = self.addnorm_1(X, self.attention(X, X, X, valid_length))
return self.addnorm_2(Y, self.ffn(Y))
整个编码器由n个刚刚定义的Encoder Block堆叠而成,因为残差连接的缘故,中间状态的维度始终与嵌入向量的维度d一致;同时注意到我们把嵌入向量乘以 d \sqrt{d} d 以防止其值过小。
class TransformerEncoder(d2l.Encoder):
def __init__(self, vocab_size, embedding_size, ffn_hidden_size,
num_heads, num_layers, dropout, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.embedding_size = embedding_size
self.embed = nn.Embedding(vocab_size, embedding_size)
self.pos_encoding = PositionalEncoding(embedding_size, dropout)
self.blks = nn.ModuleList()
for i in range(num_layers):
self.blks.append(
EncoderBlock(embedding_size, ffn_hidden_size,
num_heads, dropout))
def forward(self, X, valid_length, *args):
X = self.pos_encoding(self.embed(X) * math.sqrt(self.embedding_size))
for blk in self.blks:
X = blk(X, valid_length)
return X
解码器
Transformer 模型的解码器与编码器结构类似,然而,除了之前介绍的几个模块之外,编码器部分有另一个子模块。该模块也是多头注意力层,接受编码器的输出作为key和value,decoder的状态作为query。与编码器部分相类似,解码器同样是使用了add and norm机制,用残差和层归一化将各个子层的输出相连。
仔细来讲,在第t个时间步,当前输入
x
t
x_t
xt是query,那么self attention接受了第t步以及前t-1步的所有输入
x
1
,
…
,
x
t
−
1
x_1,\ldots, x_{t-1}
x1,…,xt−1。在训练时,由于第t位置的输入可以观测到全部的序列,这与预测阶段的情形项矛盾,所以我们要通过将第t个时间步所对应的可观测长度设置为t,以消除不需要看到的未来的信息。
对于Transformer解码器来说,构造方式与编码器一样,除了最后一层添加一个dense layer以获得输出的置信度分数。下面让我们来实现一下Transformer Decoder,除了常规的超参数例如vocab_size embedding_size 之外,解码器还需要编码器的输出 enc_outputs 和句子有效长度 enc_valid_length。
summary
- 关于Transformer描述正确的是: 在训练和预测过程中,解码器部分在训练过程1次,预测过程要进行句子长度次前向传播。
- Transformer 内部的注意力模块不全为自注意力模块,Decoder部分的第二个注意力层不是自注意力,key-value来自编码器而query来自解码器 。
- 解码器部分在预测过程中不需要使用 Attention Mask。
- 自注意力模块理论上可以捕捉任意距离的依赖关系,因为自注意力会计算句子内任意两个位置的注意力权重。
- 在Transformer模型中,注意力头数为h,嵌入向量和隐藏状态维度均为d,那么一个多头注意力层所含的参数量是 4 h d 2 4 h d^{2} 4hd2。 h h h个注意力头中,每个的参数量为 3 d 2 3 d^{2} 3d2 ,最后的输出层形状为 h d × d h d \times d hd×d,所以参数量共为 4 h d 2 4 h d^{2} 4hd2 。
- 层归一化有利于加快收敛,减少训练时间成本;层归一化对一个中间层的所有神经元进行归一化;层归一化的效果不会受到batch大小的影响;层归一化不是对每个神经元的输入数据以mini-batch为单位进行汇总,批归一化(Batch Normalization)才是对每个神经元的输入数据以mini-batch为单位进行汇总。