chap4 多层感知机(1)

本章视频地址 本章讲义地址

在本章中,我们将介绍你的第一个真正的深度网络。最简单的深度网络称为多层感知机,它们由多层神经元组成,每一层都与下面一层(从中接收输入)和上面一层(反过来影响当前层的神经元)完全相连。当我们训练大容量模型时,我们面临着过拟合的风险。因此,我们需要为你提供第一次严格的概念介绍,包括过拟合、欠拟合和模型选择。为了帮助你解决这些问题,我们将介绍权重衰减dropout等正则化技术。我们还将讨论数值稳定性参数初始化相关的问题,这些问题是成功训练深度网络的关键。在整个过程中,我们的目标不仅是让你掌握概念,还希望让你掌握深度网络的实践方法。在本章的最后,我们将把到目前为止所介绍的内容应用到一个真实的案例:房价预测。我们将有关于模型计算性能、可伸缩性和效率相关的问题放在后面的章节中讨论。

感知机

模型定义

给定输入 x,权重 w,偏移 b,感知机输出:

o=σ(<w,x>+b), σ(x)={1, if x>01, otherwise\begin{aligned} \mathbf{o} & = \sigma(<\mathbf{w} ,\mathbf{x}> + b), ~ \sigma(x)=\left\{ \begin{aligned} 1,& ~if~x>0 \\ -1,& ~otherwise \end{aligned} \right. \end{aligned}

输出 1 或 -1,是一个二分类模型。

模型训练

1
2
3
4
5
6
7
8
9
# 初始化模型参数
initialize w=0 and b=0
# 如果分类错误,更新参数
repeat
if yi[<wi,xi>+b]<= 0 then
w <- w+yixi and b <- b+yi
end if
# 直到所有样本分类正确为止
until all classified correctly

等价于使用批量大小为 1 的梯度下降,并使用如下损失函数: l(y,x,w)=max(0,y<w,x>)l(y,\mathbf{x,w})=max(0,-y<\mathbf{w,x}>)

收敛定理:

  • 数据在半径 r 内
  • 余量(margin) ρ\rho ,对于 w2+b21||w||^2+b^2\le1 这个分界面,所有数据样本都能分类正确: y(xTw+b)ρy(\mathbf{x^T w}+b)\ge \rho
  • 感知机保证在 $(r2+1)/\rho2 $ 步后收敛

模型缺陷

感知机不能拟合 XOR 函数,它只能产生线性分割面。这导致了人工智能领域的第一个寒冬。

多层感知机

上文我们提到过,感知机(单层)不能拟合 XOR 函数。这个缺陷可以由多层感知机来解决,它的基本想法是,先通过一些不同的简单函数来从多个角度拟合数据,再用函数来综合这些简单函数的拟合结果,即,在感知机中加入隐藏层。

隐藏层

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系类型。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。

我们可以把前L1L-1层看作表示,把最后一层看作线性预测器。这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。下面,我们以图的方式描述了多层感知机。

mlp

这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。

输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算;因此,这个多层感知机中的层数为2。

注意,这两个层都是全连接的。每个输入都会影响隐藏层中个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。

然而,正如 之前所说,具有全连接层的多层感知机的参数开销可能会高得令人望而却步,即使在不改变输入或输出大小的情况下,也可能促使在参数节约和模型有效性之间进行权衡。

模型定义: 从线性到非线性

对于具有hh个隐藏单元的单隐藏层多层感知机,用 HRn×h\mathbf{H} \in \mathbb{R}^{n \times h} 表示隐藏层的输出,称为隐藏表示(hidden representations)。在数学或代码中,H\mathbf{H} 也被称为隐藏层变量(hidden-layer variable)或隐藏变量(hidden variable)。

  • 输入:XRn×d\mathbf{X} \in \mathbb{R}^{n \times d} 来表示 nn 个样本的小批量,其中每个样本具有 dd 个输入(特征)。
  • 隐藏层:隐藏层权重W(1)Rd×h\mathbf{W}^{(1)} \in \mathbb{R}^{d \times h},隐藏层偏置b(1)R1×h\mathbf{b}^{(1)} \in \mathbb{R}^{1 \times h}
  • 输出层:输出层权重W(2)Rh×q\mathbf{W}^{(2)} \in \mathbb{R}^{h \times q},输出层偏置b(2)R1×q\mathbf{b}^{(2)} \in \mathbb{R}^{1 \times q}
  • 形式上,我们按如下方式计算单隐藏层多层感知机的输出ORn×q\mathbf{O} \in \mathbb{R}^{n \times q}

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}

若是多类分类问题,则对输出再加一层 softmax 函数。

可以发现,我们在仿射变换之后对每个隐藏单元使用了非线性激活函数(activation function)σ\sigma。激活函数的输出(例如,σ()\sigma(\cdot))被称为激活值(activations)。若没有激活函数(或者激活函数也是线性的),则我们的多层感知机本质上和线性模型没有区别,因为对于任意权重值,我们只需合并隐藏层,便可产生具有参数W=W(1)W(2)\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}b=b(1)W(2)+b(2)\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}的等价单层模型。

一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型。

为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,例如,H(1)=σ1(XW(1)+b(1))\mathbf{H}^{(1)} = \sigma_1(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})H(2)=σ2(H(1)W(2)+b(2))\mathbf{H}^{(2)} = \sigma_2(\mathbf{H}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}),一层叠一层,从而产生更有表达能力的模型。

  • 超参数:1. 隐藏层数;2. 每层隐藏层的大小;

激活函数

sigmoid 函数

[对于一个定义域在R\mathbb{R}中的输入,sigmoid函数将输入变换为区间(0, 1)上的输出]。因此,sigmoid通常称为挤压函数(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:

sigmoid(x)=11+exp(x),σ(x)={1, if x>00, otherwise\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)},\sigma(x)=\left\{ \begin{aligned} 1,& ~if~x>0 \\ 0,& ~otherwise \end{aligned} \right.

1
y = torch.sigmoid(x)

sigmoid函数的导数为下面的公式:

ddxsigmoid(x)=exp(x)(1+exp(x))2=sigmoid(x)(1sigmoid(x)).\frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).

注意,当输入为0时,sigmoid函数的导数达到最大值0.25。而输入在任一方向上越远离0点,导数越接近0。

1
2
3
x.grad.data.zero_()
y.backward(torch.ones_like(x))
x.grad

Tanh 函数

和 sigmoid 函数很像,不过是把输入投影到 (-1,1):

tanh(x)=1exp(2x)1+exp(2x)\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}

1
y = torch.tanh(x)

tanh函数的导数是:

ddxtanh(x)=1tanh2(x).\frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x).

当输入接近0时,tanh函数的导数接近最大值1。与我们在sigmoid函数图像中看到的类似,输入在任一方向上越远离0点,导数越接近0。

1
2
3
x.grad.data.zero_()
y.backward(torch.ones_like(x))
x.grad

ReLu 函数

最受欢迎的选择是线性整流单元(Rectified linear unit,ReLU),因为它实现简单(不用做指数运算),同时在各种预测任务中表现良好。
[ReLU提供了一种非常简单的非线性变换]。
给定元素xx,ReLU函数被定义为该元素与00的最大值:

$$\operatorname{ReLU}(x) = \max(x, 0).$$

通俗地说,ReLU函数通过将相应的激活值设为0来仅保留正元素并丢弃所有负元素。很容易理解到,激活函数是分段线性的。

1
y = torch.relu(x)

当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。

注意,当输入值精确等于0时,ReLU函数不可导。

在此时,我们默认使用左侧的导数,即当输入为0时导数为0。我们可以忽略这种情况,因为输入可能永远都不会是0。

总结

  • 多层感知机使用隐藏层和激活函数得到非线性模型
  • 常用的激活函数是 Sigmoid,Tanh,ReLU
  • 使用 softmax 来处理多类分类问题
  • 超参数是隐藏层数,和各个隐藏层的大小。

Q&A

  1. 神经网络为什么是增加深度而不是增加神经元的个数?

    每层的神经元过多会有过拟合的风险,而且增加深度的模型训练更简单,效果更好。

  2. 激活函数的本质是什么?

    激活函数的本质是引入非线性性。所以选择什么样的激活函数本质上没有区别,一切从简就好(ReLU 大法好)。

  3. 如何选择多层感知机的层数和每层神经元的个数?

    基本方法:由简到繁,挨个试(开始炼丹)

简洁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

net.apply(init_weights);

# 训练
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss()
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)

train_ch3

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
class Accumulator:  #@save
"""在`n`个变量上累加。"""
def __init__(self, n):
self.data = [0.0] * n

def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):
self.data = [0.0] * len(self.data)

def __getitem__(self, idx):
return self.data[idx]

def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]


def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)。"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.backward()
updater.step()
metric.add(float(l) * len(y), accuracy(y_hat, y),
y.size().numel())
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练准确率
return metric[0] / metric[2], metric[1] / metric[2]


class Animator: #@save
"""在动画中绘制数据。"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts

def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)。"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc