神经网络基础

上一章已经把一批输入写成 X∈RB×dX\in\mathbb R^{B\times d}。现在要让每条记录产生预测:先用一组权重组合输入,再用非线性改变响应方式,多次重复后得到输出。这个结构就是多层感知机,简称 MLP。

仿射变换

设输入有 dd 个特征,单个神经元先算

a=∑i=1dwixi+b.a=\sum_{i=1}^{d}w_ix_i+b.

xix_i 是输入的第 ii 个值,wiw_i 是对应权重,bb 是偏置,aa 是加权结果。权重控制各特征的贡献,偏置让响应阈值能够平移。若 x=(2,−1)x=(2,-1)、w=(3,4)w=(3,4)、b=0.5b=0.5,则 a=6−4+0.5=2.5a=6-4+0.5=2.5。

同时计算 mm 个神经元,就得到一个全连接层:每个输出都连接全部 dd 个输入。本文统一采用“每条样本占一行”的记法:

A=XW+b,X∈RB×d,W∈Rd×m,b∈Rm,A∈RB×m.\begin{gathered} A=XW+b,\\ X\in\mathbb R^{B\times d},\quad W\in\mathbb R^{d\times m},\\ b\in\mathbb R^m,\quad A\in\mathbb R^{B\times m}. \end{gathered}

矩阵乘法要求中间维度 dd 相同;每个输出 An,j=∑iXn,iWi,j+bjA_{n,j}=\sum_iX_{n,i}W_{i,j}+b_j,其中 nn 是 batch 内样本下标,jj 是输出坐标。偏置沿 batch 轴广播。参数量为 dm+mdm+m,增加 batch 大小只增加同时计算的样本数。

严格说,带偏置的运算是仿射变换,b=0b=0 时才是线性变换。深度学习库通常仍将这一模块命名为 Linear。PyTorch 保存的 weight 形状是 (m,d)(m,d),实际算 X @ weight.T + bias;和本文的 WW 互为转置。Linear 文档同时列出了权重与输入输出形状。

激活函数

若接连使用两个仿射层而不插入激活,结果为

(XW1+b1)W2+b2=X(W1W2)+(b1W2+b2).\begin{aligned} &(XW_1+b_1)W_2+b_2\\ &\quad=X(W_1W_2)+(b_1W_2+b_2). \end{aligned}

两个层可以合并成一个,增加深度没有改变这类函数的形式。非线性激活逐元素作用于 AA,使网络能够根据输入所在区域采用不同的响应。

常用 ReLU 为 ReLU⁡(a)=max⁡(0,a)\operatorname{ReLU}(a)=\max(0,a)。例如 (−2,0.5,3)(-2,0.5,3) 经 ReLU 后变成 (0,0.5,3)(0,0.5,3)。在固定的正负激活模式内,带 ReLU 的 MLP 是仿射函数;跨过某个神经元的零点时,组合方式改变,因此整个网络可以形成分段线性的边界。ReLU 在正半轴导数为 1、负半轴为 0;实现会在零点选一个约定的次梯度,PyTorch 取 0。

其他常见激活有

σ(a)=11+e−a,tanh⁡(a)=ea−e−aea+e−a.\sigma(a)=\frac{1}{1+e^{-a}},\qquad \tanh(a)=\frac{e^a-e^{-a}}{e^a+e^{-a}}.

sigmoid 的输出在 (0,1)(0,1),可表示二元概率或门控系数;tanh 在 (−1,1)(-1,1),可用于有正负号的状态。它们在大绝对值输入处趋于饱和,导数变小。隐藏层激活和输出层的选择由计算目的决定:回归头常直接输出实数,分类头输出 logits 后交给损失函数。可对照 PyTorch 的 ReLU、Sigmoid 与 Tanh定义。

一个具体的非线性能力例子是 XOR。输入 (x1,x2)(x_1,x_2) 限于 {0,1}2\{0,1\}^2,定义

h1=ReLU⁡(x1+x2),h2=ReLU⁡(x1+x2−1),o=h1−2h2.\begin{aligned} h_1&=\operatorname{ReLU}(x_1+x_2),\\ h_2&=\operatorname{ReLU}(x_1+x_2-1),\\ o&=h_1-2h_2. \end{aligned}

四种输入 (0,0),(0,1),(1,0),(1,1)(0,0),(0,1),(1,0),(1,1) 分别得到 o=0,1,1,0o=0,1,1,0。单个仿射函数若在前三点输出 0,1,10,1,1,就被迫在最后一点输出 2;这个两隐藏单元的网络则通过 ReLU 的折点完成了所需映射。

多层感知机(MLP)

设第 ll 层输入宽度为 dl−1d_{l-1}、输出宽度为 dld_l:

H(0)=X,H(l)=ϕl(H(l−1)Wl+bl).H^{(0)}=X,\qquad H^{(l)}=\phi_l\bigl(H^{(l-1)}W_l+b_l\bigr).

H(l)∈RB×dlH^{(l)}\in\mathbb R^{B\times d_l} 是第 ll 层激活,Wl∈Rdl−1×dlW_l\in\mathbb R^{d_{l-1}\times d_l}、bl∈Rdlb_l\in\mathbb R^{d_l} 是该层参数,ϕl\phi_l 是逐元素激活或输出所需的恒等映射。隐藏层夹在输入和任务输出之间,通常没有逐单元的人为标签;它的值由最终损失通过反传约束。

Dropout 论文图 1a:输入层经两个全连接隐藏层连接到输出节点

图 1:摘自 Srivastava 等,Dropout,2014,图 1(a),只裁取左侧标准网络。圆点表示单元,连线表示层间权重;从底向上是输入、两个隐藏层与输出。原图右侧的随机删单元机制在评价与泛化中讨论。

权重和偏置跨样本复用,中间激活随输入变化。一个普通前馈 MLP 不在两次独立样本调用之间保存循环状态。训练需要保留部分中间值以便求导;优化器还可能为每个参数保存动量等状态。这些数据的数量和网络参数量应分别计算。

前向传播计算示例

取一条输入 X=[1,2]X=[1,2],网络宽度为 2→3→22\to3\to2。先算隐藏层:

W1=[1−10011],b1=[0,0,−3].W_1=\begin{bmatrix}1&-1&0\\0&1&1\end{bmatrix},\quad b_1=[0,0,-3].

于是

A1=XW1+b1=[1,1,−1],H=ReLU⁡(A1)=[1,1,0].\begin{aligned} A_1&=XW_1+b_1=[1,1,-1],\\ H&=\operatorname{ReLU}(A_1)=[1,1,0]. \end{aligned}

输出层取

W2=[10−112−1],b2=[0.5,−0.5].W_2=\begin{bmatrix}1&0\\-1&1\\2&-1\end{bmatrix},\quad b_2=[0.5,-0.5].

得到两类 logits:

Z=HW2+b2=[0.5,0.5],p=softmax⁡(Z)=[0.5,0.5].\begin{aligned} Z&=HW_2+b_2=[0.5,0.5],\\ p&=\operatorname{softmax}(Z)=[0.5,0.5]. \end{aligned}

若标签是类别 0,交叉熵为 −log⁡0.5≈0.693147-\log0.5\approx0.693147。第一层有 2×3+3=92\times3+3=9 个参数,第二层有 3×2+2=83\times2+2=8 个,总共 17 个。第三个隐藏单元在这条输入上为 0,但其输入权重仍属于模型参数,并可能在别的样本上参与计算。

参数更新

对单样本 softmax 交叉熵,输出分数的梯度是 p−yonehotp-y_{\mathrm{onehot}}。本例标签的 one-hot 为 [1,0][1,0],因此 ∂ℓ/∂Z=[−0.5,0.5]\partial\ell/\partial Z=[-0.5,0.5]。根据矩阵乘法的求导规则,

∂ℓ∂W2=H⊤[−0.5,0.5]=[−0.50.5−0.50.500].\frac{\partial\ell}{\partial W_2}=H^\top[-0.5,0.5] =\begin{bmatrix}-0.5&0.5\\-0.5&0.5\\0&0\end{bmatrix}.

沿第一层继续传播,得到 ∂ℓ/∂A1=[−0.5,1,0]\partial\ell/\partial A_1=[-0.5,1,0],其中第三项被 ReLU 的零导数截断。使用学习率 η=0.1\eta=0.1 的 SGD,对全部权重和偏置作 θ←θ−η∇θℓ\theta\leftarrow\theta-\eta\nabla_\theta\ell 后,再算同一输入,logits 约为 [1.535,−0.235][1.535,-0.235],类别 0 概率约为 0.854460.85446,损失约为 0.157290.15729。

下面代码固定了参数,可以复现上述结果。

import torch
from torch import nn

net = nn.Sequential(nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 2))
with torch.no_grad():
    net[0].weight.copy_(torch.tensor([[1., 0.], [-1., 1.], [0., 1.]]))
    net[0].bias.copy_(torch.tensor([0., 0., -3.]))
    net[2].weight.copy_(torch.tensor([[1., -1., 2.], [0., 1., -1.]]))
    net[2].bias.copy_(torch.tensor([.5, -.5]))

x = torch.tensor([[1., 2.]])
y = torch.tensor([0], dtype=torch.long)
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
optimizer.zero_grad()
logits = net(x)
loss = nn.functional.cross_entropy(logits, y)
loss.backward()
optimizer.step()
print(logits.detach(), loss.item())
print(net(x).detach())                # tensor([[ 1.5350, -0.2350]])
assert sum(p.numel() for p in net.parameters()) == 17

cross_entropy 直接接收 logits,内部组合了对数归一化与负对数似然,因此训练代码没有先调 softmax;输入格式见官方文档。预测时可以取 logits.argmax(-1),它与 softmax 后选最大概率得到相同类别。

实际训练通常随机初始化不同单元的权重,以打破它们完全相同的响应。批量训练将损失在样本轴上归约,再对同一组参数累积梯度。增加层宽会提高参数量与计算量;对于图像,直接把每个像素连到所有隐藏单元开销很大。例如 3072→1283072\to128 的第一层已有 393344393344 个参数。卷积网络通过局部连接与共享权重处理这种空间输入。