动手学习深度学习————多层感知机

8987 字
23 分钟

在过去几年里,深度学习给世界带来惊喜,推动了计算机视觉、自然语言处理、自动语音识别、强化学习和统计建模等领域的快速发展。

感知机

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

o=σ(w,x+b)o = \sigma \left( \langle \mathbf{w}, \mathbf{x} \rangle + b \right) σ(x)={1if x>01otherwise\sigma(x) = \begin{cases} 1 & \text{if } x > 0 \\ -1 & \text{otherwise} \end{cases}

1770001937529

这是一个二分类的问题:输出1/-1

  • 回归问题:输出一个实数;而二分类问题输出一个类别
  • Softmax回归:多分类问题,输出概率

感知机的训练算法

1770002936991

初始化:将权重向量ww和偏置bb都初始化为0。

循环:算法会不断循环遍历训练数据,直到所有的样本都被正确的分类。

判断错误分类

yi[w,xi+b]0y_i [\langle w, x_i \rangle + b] \leq 0
  • 这里的yiy_i是真实的标签(为+1或-1)
  • w,xi+b\langle w, x_i \rangle + b 是模型对样本xix_i的预测输出值
  • 含义:如果yiy_i和预测值的符号相反(乘积为负),或者预测值为0,说明分类错误。

更新规则

ww+yixiw \leftarrow w + y_i x_i bb+yib \leftarrow b + y_i
  • 含义:当发现一个错误样本时,利用该样本的信息来调整权重。
  • 如果是正样本(y=1y=1)被误判为负,就加上xx(让wwxx的方向靠近)
  • 如果是负样本(y=1y=-1)被误判为正,就减去xx(让ww远离xx)

更新规则的数学逻辑:

感知机的预测完全取决于内积 w,x\langle w, x \rangle 的正负(为了简化,我们先忽略偏置 bb)。

  • y=1y=1(正样本)被误判:此时目前的 w,x0\langle w, x \rangle \le 0。我们希望这个值 变大 (最好变成正数)。如果我们更新 wnew=w+xw_{new} = w + x,那么新的内积为:

    w+x,x=w,x+x,x=w,x+x2\langle w+x, x \rangle = \langle w, x \rangle + \langle x, x \rangle = \langle w, x \rangle + \|x\|^2

    因为 x2\|x\|^2 永远是正数,所以更新后的内积 一定会增加 。这让该样本在下次预测时更趋向于被判定为正。

  • y=1y=-1(负样本)被误判:此时目前的 w,x0\langle w, x \rangle \ge 0。我们希望这个值变小(最好变成负数)。如果我们更新 wnew=wxw_{new} = w - x,新的内积为:

    wx,x=w,xx2\langle w-x, x \rangle = \langle w, x \rangle - \|x\|^2

    内积一定会减小,从而让该样本更趋向于被判定为负。

优化视角解释

以上的感知机训练算法,等价于使用批量大小为1的梯度下降,并使用如下的损失函数:

(y,x,w)=max(0,yw,x)\ell(y, \mathbf{x}, \mathbf{w}) = \max(0, -y\langle \mathbf{w}, \mathbf{x} \rangle)

理解这个公式:

  • 如果分类正确:yw,x>0y\langle \mathbf{w}, \mathbf{x} \rangle > 0,那么 yw,x<0-y\langle \mathbf{w}, \mathbf{x} \rangle < 0,经过 max(0,)\max(0, \dots) 后损失为 0 。此时梯度为 0,参数 不更新
  • 如果分类错误: yw,x<0y\langle \mathbf{w}, \mathbf{x} \rangle < 0,那么 yw,x>0-y\langle \mathbf{w}, \mathbf{x} \rangle > 0,损失为正。

更新规则:当分类错误时,损失函数对 ww 求导(梯度)是 yx-yx

代入 SGD 更新公式(假设学习率为 1):

wnew=woldlearning_rate×gradientw_{new} = w_{old} - \text{learning\_rate} \times \text{gradient} wnew=wold1×(yx)=wold+yxw_{new} = w_{old} - 1 \times (-yx) = w_{old} + yx

感知机收敛定理

  • 数据半径 rr :假设所有的输入数据 xx 都分布在一个半径为 rr 的圆(或高维球体)内。即数据的大小(范数)是有界的。
  • 余量 ρ\rho (Margin) :这代表了正负两类样本之间“最宽”的那条缝隙。

感知机的步数上限:

最大步数r2+1ρ2\text{最大步数} \le \frac{r^2 + 1}{\rho^2}

感知机的问题

感知机不能拟合XOR函数,它只能产生线性分割面。导致了第一次AI的寒冬。

XOR问题:无法使用一条直线,将两种颜色的球完全分开。

1770038030674

多层感知机

上面我们提到单层感知机模型无法解决非线性问题,而引入隐藏层可以解决这一问题。

1770110738845

在这个问题中,隐藏层相当于两条辅助线,这就好比我们在做特征工程,我们将原始复杂的分类任务拆解成了两个简单的子任务:

  • 蓝色特征
    • 网络学会的第一条规则是区分左右(即x轴的符号)
    • 左边为 + ,右边为 - ,这对应图中的蓝色竖线
  • 黄色特征
    • 网络学会的第二条规则是区分上下(即y轴的符号)
    • 上边为 + , 下边为 - ,这对应了图中的黄色横线。

现在,我们有了两个新的特征(蓝色和黄色)。神经网络的输出层(Output Layer)所做的工作,就是将这两个特征进行 非线性组合 (在这里可以理解为乘法或异或逻辑):

公式逻辑:Product=Blue×Yellow公式逻辑:Product = Blue × Yellow

对应到神经网络架构,这就是一个最简单的多层感知机(MLP):

  1. 输入层(x, y):也就是原始数据的坐标。
  2. 隐藏层:这里有两个神经元:
    • 一个负责学习蓝色规则
    • 一个负责学习黄色规则
  3. 输出层:接收隐藏层信号,完整最终的逻辑判断

单隐藏层

1770111590411

  • 隐藏层的大小是一个超参数。

输入输出的大小都是由数据和类别决定的。

单分类

核心组件

单分类指输出为一个标量的情况。

输入层:

  • xRn\mathbf{x} \in \mathbb{R}^n:代表输入是一个n维向量。

隐藏层:

  • W1Rm×n\mathbf{W}_1 \in \mathbb{R}^{m \times n}:这是第一层的权重矩阵。它将n维输入映射到m维空间。
  • b1Rm\mathbf{b}_1 \in \mathbb{R}^m:偏置项,对应隐藏层的m个神经元。

输出层:

  • w2Rm,b2R\mathbf{w}_2 \in \mathbb{R}^m, b_2 \in \mathbb{R}:单输出的情况,w2\mathbf{w}_2是一个一维向量。

数学表达式

  1. 隐藏层计算:
h=σ(W1x+b1)\mathbf{h} = \sigma(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1)
  • 这里先进行线性变换W1x+b1\mathbf{W}_1\mathbf{x} + \mathbf{b}_1
  • σ\sigma(激活函数):按元素做运算的函数。后续细讲。
  1. 输出层计算:
o=w2Th+b2o = \mathbf{w}_2^T \mathbf{h} + b_2
  • 这是将隐藏层提取到的特征h进行加权求和,得到最终的预测值。

激活函数 Activation function

激活函数通过计算加权和并加上偏置来确定神经元是否应该被激活,它们将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。 由于激活函数是深度学习的基础。

为什么需要激活函数

简单来说,如果没有激活函数,再深的网络也只是一层。

假设我们有一个两层的神经网络,但没有激活函数。

  • 第一层输出:h=W1x+b1\mathbf{h} = \mathbf{W}_1 \mathbf{x} + \mathbf{b}_1
  • 第二层输出:o=W2h+b2\mathbf{o} = \mathbf{W}_2 \mathbf{h} + \mathbf{b}_2

我们将第一层代入第二层:

o=W2(W1x+b1)+b2\mathbf{o} = \mathbf{W}_2 (\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 o=(W2W1)x+(W2b1+b2)\mathbf{o} = (\mathbf{W}_2 \mathbf{W}_1) \mathbf{x} + (\mathbf{W}_2 \mathbf{b}_1 + \mathbf{b}_2)

如果我们令 Wnew=W2W1\mathbf{W}_{new} = \mathbf{W}_2 \mathbf{W}_1bnew=W2b1+b2\mathbf{b}_{new} = \mathbf{W}_2 \mathbf{b}_1 + \mathbf{b}_2,那么公式就变成了:

o=Wnewx+bnew\mathbf{o} = \mathbf{W}_{new} \mathbf{x} + \mathbf{b}_{new}

结论: 无论你堆叠多少层,只要是线性的,它们最终都可以合并成一个单一的线性层。这意味着你的“深度”网络在表达能力上和最简单的感知机没有任何区别,无法处理复杂的任务。

激活函数的作用:它像一个”开关”或”过滤器”,决定了神经元的哪些信息应该传递到下一层。它引入了非线性,让网络能够拟合出复杂的边界。

  • 万能近似定理:只要神经网络拥有至少一个包含足够多神经元的隐藏层,并配合非线性激活函数,它就可以以任意精度拟合闭区间内的连续函数。

常见的激活函数

ReLU函数

修正线性单元(Rectified linear unit, ReLU),这是最受欢迎的激活函数。

ReLU(x)=max(x,0)\text{ReLU}(x) = \max(x, 0)

1770125127665

ReLU导数的图像:

1770125303997

注意:当输入值精确等于0时,ReLU函数不可导。在此时,我们默认使用左侧的导数。我们可以忽略这种情况,因为输入可能永远都不会是0。 这里引用一句古老的谚语,“如果微妙的边界条件很重要,我们很可能是在研究数学而非工程”, 这个观点正好适用于这里。

优点:

  • 实现简单。
  • 无需计算指数,计算速度快。
  • 求导表现好

sigmoid函数

sigmoid函数将输入变换为区间(0, 1)上的输出。

sigmoid(x)=11+exp(x).\text{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.

1770125558151

sigmoid函数的导数:

1770125582589

针对梯度下降的学习时,sigmoid是一个自然的选择,因为他是一个平滑的、可微的阈值单元近似。然而,sigmoid在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。

tanh函数

与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。 tanh函数的公式如下:

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

1770125646984

tanh函数的导数:

1770125663048

tanh函数和sigmoid函数计算都比较复杂,在深度学习中算力是宝贵的资源,所以一般情况下都选择使用ReLU函数。

多类分类

y1,y2,,yk=softmax(o1,o2,,ok)y_1, y_2, \dots, y_k = \text{softmax}(o_1, o_2, \dots, o_k)

1770125993810

输入层:xRn\mathbf{x} \in \mathbb{R}^n

隐藏层:W1Rm×n,b1Rm \mathbf{W}_1 \in \mathbb{R}^{m \times n}, \mathbf{b}_1 \in \mathbb{R}^m

输出层:W2Rm×k,b2Rk \mathbf{W}_2 \in \mathbb{R}^{m \times k}, \mathbf{b}_2 \in \mathbb{R}^k

这里W2\mathbf{W}_2不再是一个向量,而是一个矩阵Rm×k\mathbb{R}^{m \times k}。它把m维的隐藏特征映射到k个类别的评分上。

计算公式:

  • 隐藏层激活:
h=σ(W1x+b1)\mathbf{h} = \sigma(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1)
  • 输出层线性变换:
o=W2Th+b2\mathbf{o} = \mathbf{W}_2^T \mathbf{h} + \mathbf{b}_2
  • 最终输出:
y=softmax(o)\mathbf{y} = \text{softmax}(\mathbf{o})

多类分类与softmax回归没有本质区别,只是增加了隐藏层。

多隐藏层

1770126503542

计算公式:

h1=σ(W1x+b1)h2=σ(W2h1+b2)h3=σ(W3h2+b3)o=W4h3+b4\begin{aligned} \mathbf{h}_1 &= \sigma(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) \\ \mathbf{h}_2 &= \sigma(\mathbf{W}_2 \mathbf{h}_1 + \mathbf{b}_2) \\ \mathbf{h}_3 &= \sigma(\mathbf{W}_3 \mathbf{h}_2 + \mathbf{b}_3) \\ \mathbf{o} &= \mathbf{W}_4 \mathbf{h}_3 + \mathbf{b}_4 \end{aligned}

针对多层感知机,我们需要设计其中的超参数:

  • 隐藏层数
  • 每层隐藏层的大小

多层感知机的从零开始实现

我们使用softmax中相同的数据集进行训练——Fashion-MNIST。

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) # 直接使用d2l保中现成的导入函数
# 让我们现在可以先把注意力放在模型训练上

初始化模型参数

我们将要实现一个单隐藏层的多层感知机,它包含256个隐藏单元。

隐藏层的数量宽度都是超参数。通常我们选择2的幂次作为层的宽度,因为内存硬件中分配和寻址方式,这么做可以在计算上更高效。

我们用几个张量来表示我们的参数。

每一层都要记录一个权重矩阵和偏置向量。

# 定义网络架构的维度:输入特征784、输出类别10、隐藏层单元 256
num_inputs, num_outputs, num_hiddens = 784, 10, 256

# 初始化第一层的权重矩阵 W1
# 形状为(784, 256)
# 使用均值为0、标准差为1的随机正态分布,并乘以0.01。等价于(0, 0.01)的正太分布
W1 = nn.Parameter(torch.randn(
    num_inputs, num_hiddens, requires_grad=True) * 0.01)

# 初始化第一层的偏置项b1
# 形状为隐藏层维度,初始值全部设为0
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))

# 初始化第二层(输出层)的权重矩阵W2
W2 = nn.Parameter(torch.randn(
    num_hiddens, num_outputs, requires_grad=True) * 0.01)

# 初始化第三层的偏置项b2
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

# 将所有需要反向传播更新的参数封装进一个列表
params = [W1, b1, W2, b2]

激活函数

def relu(X):
    # 创建一个与输入张量X形状(shape)和类型完全相同、但元素全部为0的新张量
    # 这一步是为了后续进行逐元素的对比
    a = torch.zeros_like(X)

    # 比较X和a中的每一个对应位置的数值,返回其中较大的那个
    # 数学表达式:ReLU(x) = max(0, x)
    return torch.max(X, a)

模型

因为我们忽略了空间结构,所以我们使用 reshape将每个二维图像转化为一个长度为 num_inputs的向量。只需要几行代码就可以实现模型:

def net(X):
    # 将输入X展平为二维向量
    X = X.reshape((-1, num_inputs))

    # 计算隐藏层输出:
    # 1. X @ W1: 输入与第一层权重进行矩阵乘法
    # 2. + b1: 加上第一层的偏置项
    # 3. relu(...): 对计算结果应用ReLU激活函数,引入非线性
    H = relu(X @ W1 + b1)  # 这里“@”代表矩阵乘法

    # 计算输出层的预测值:
    return (H @ W2 + b2)

损失函数

这里的损失函数与softmax中的完全一样,我们直接使用高级API来简化:

loss = nn.CrossEntropyLoss(reduction='none')

训练

多层感知机的训练过程与softmax回归的训练过程完全相同:

def updater(batch_size):
    return d2l.sgd(params, lr, batch_size)

num_epochs, lr = 10, 0.1
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

1770195259896

多层感知机的简洁实现

首先导入包:

import torch
from torch import nn
from d2l import torch as d2l

模型

与softmax回归的简洁实现相比,唯一的区别是我们添加了两个全连接层(之前我们只添加了一个全连接层)。

# 使用Sequential 容器按顺序堆叠各个层
net = nn.Sequential(
		    # 将输入的四维图像张量,展平为二维张量
		    nn.Flatten(),
		    # 定义第一个全连接层: 输入784维,输出256维
                    nn.Linear(784, 256),
		    # 添加ReLU激活函数,为模型引入非线性,必须放在两个全连接层之间
                    nn.ReLU(),
		    # 定义第二个全连接层(输出层)
                    nn.Linear(256, 10))

# 定义一个初始化权重的函数
def init_weights(m):
    # 检查当前层m是否为全连接层(nn.Linear)
    if type(m) == nn.Linear:
	# 使用正太分布(0, 0.01)初始化该层的权重矩阵
        nn.init.normal_(m.weight, std=0.01)

# 将init_weights 函数递归地应用到net的每一个子模型(即Sequential里的每一层)上
# 对于不是nn.Linear 的层,if判断不成立,直接跳过
net.apply(init_weights);

训练过程的实现与我们实现softmax回归时完全相同, 这种模块化设计使我们能够将与模型架构有关的内容独立出来

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)

1770197678160

模型选择

训练误差和泛化误差

  • 训练误差:模型在训练数据上的误差
  • 泛化误差:模型在新数据上的误差
  • 例子:根据模考成绩来预测未来考试分数
    • 在过去的考试中表现很好(训练误差)不代表未来考试一定会好
    • 学生A 通过背书在模考中拿到很好的成绩
    • 学生B 知道答案后面的原因

验证数据集和测试数据集

  • 验证数据集:一个用来评估模型好坏的数据集

    • 本质作用:帮助模型做选择 。它虽然不直接参与梯度下降(即不直接更新权重),但它间接参与了训练过程。
    • 如何影响:我们根据验证集上的表现来调整超参数(比如学习率、层数、Dropout比例)或者决定何时停止训练(Early Stopping)。
    • 潜台词:模型其实是“看”过验证集的,我们的训练策略针对它进行了优化。
  • 测试数据集:只用一次的数据集

    • 本质作用无偏估计真实能力 。它是用来模拟模型上线后在真实世界中会遇到的未知数据。
    • 如何影响完全不影响 。它绝不能参与任何参数更新或模型选择的决策。
    • 潜台词 :模型在看到测试集之前,必须已经完全定型(冻结参数)。

如果把训练模型比作学生备考:

  • 训练集 :是 课后作业 。学生(模型)通过做题来学习知识点,做错了就改(更新参数)。
  • 验证集 :是 模拟考试
    • 考完后,老师会根据成绩告诉你:“你这次复习策略不对,要调整一下重点”(调整超参数)。
    • 你可以考很多次模拟考,不断调整学习方法,直到模拟考成绩满意为止。
  • 测试集 :是 真正的高考(或未来的考试)
    • PPT里提到的“只用一次”就是这个意思。
    • 你不能考了一半觉得分低,就要求老师把试卷拿回去让你重新复习再考一遍。它的结果就是你的最终能力体现。

K-则交叉验证

在没有足够多的数据时使用。

算法:

  1. 将训练数据分割成K块
  2. For i = 1, …, K
    • 使用第i块作为验证数据集,其余的作为训练数据集
  3. 报告K个验证集误差的平均

常用:K = 5或10