李沐《动手学深度学习》 | 多层感知机

发布于:2025-05-10 ⋅ 阅读:(11) ⋅ 点赞:(0)

感知机模型

感知机是二分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别。

感知机旨在求出将输入空间中的实例划分为两类的分离超平面。如果训练数据集是线性可分的,则感知机一定能求得分离超平面。如果是非线性可分的数据,则无法获得超平面。为了找出这个超平面,也就是确定感知机模型的参数和w,b。

算法描述

给定输入向量x,权重向量w,和偏移标量b感知机输出 o = σ ( < w , x > + b )          σ ( x ) = { 1 i f    x > 0 − 1 o t h e r w i s e o=\sigma (<w,x>+b) \;\;\;\; \sigma(x) = \begin{cases} 1 & if\;x>0\\ -1 &otherwise \end{cases} o=σ(<w,x>+b)σ(x)={11ifx>0otherwise

< w , x > <w,x> <w,x>表示w和x做内积,感知机是个二分类问题, x > 0 x>0 x>0取1,其余情况取-1.

对于特征空间中有一个超平面 w x + b = 0 wx+b=0 wx+b=0,其中 w w w是超平面的法向量, b b b是超平面的截距。这个超平面将特征空间分为两部分,位于两部分的特征向量被分为正负两类,因此称超平面S为分离超平面。

  • 正类区域 w ⋅ x + b > 0 w⋅x+b>0 wx+b>0
  • 负类区域 w ⋅ x + b < 0 w⋅x+b<0 wx+b<0

法向量 w w w 始终垂直于超平面,指向超平面的“正方向”。

调整 b b b 可以平移超平面,使其靠近或远离原点。

《深度学习入门》的解释

将输入想成不同的信号,每个神经元会计算传送过来的信号总和,只有当这个总和超过某个界限值 θ \theta θ时,才会被激活。

比如 w 1 x 1 + w 2 x 2 ≤ θ w_1x_1+w_2x_2\leq \theta w1x1+w2x2θ θ \theta θ移动后使用 b = − θ b=-\theta b=θ表示原来的式子 b + w 1 x 1 + w 2 x 2 ≤ 0 b+w_1x_1+w_2x_2\leq 0 b+w1x1+w2x20

o = σ ( < w , x > + b )          σ ( x ) = { 1 i f    x > 0 − 1 o t h e r w i s e o=\sigma (<w,x>+b) \;\;\;\; \sigma(x) = \begin{cases} 1 & if\;x>0\\ -1 &otherwise \end{cases} o=σ(<w,x>+b)σ(x)={11ifx>0otherwise

激活函数 σ \sigma σ函数将输入信号的总和转换为输出信号

激活函数的作用:决定如何来激活信号的总和

:
每个神经元对应一个偏置:每个神经元的计算都需独立调节其激活阈值(偏置)。

权重控制输入信号的重要性,偏置调整神经元被激活的容易程度。

训练感知机

假设当前是第i个样本, y i y_i yi是该样本的真是标号,假设+1和-1, y ^ = < w , x i > + b \hat y = <w,x_i>+b y^=<w,xi>+b表示线性模型预测的结果 y ^ \hat y y^

分类判断:如果真实值 y i y_i yi y ^ \hat y y^异号,说明感知机模型预测的结果错误。此时需要更新参数w与b,使用该错误样本来更新权重 w = w + y i x i w=w+y_ix_i w=w+yixi,标量偏差 b = b + y i b=b+y_i b=b+yi

终止条件:所有的类都分类正确。

这个算法参数更新部分实际上是使用的梯度下降算法,在这里批量大小为1也就是每一次拿一个样本去算梯度。

损失函数的选择

核心:最小化误分类样本的损失,修改参数向正确分类方向更新

李沐视频里介绍的损失函数

损失函数(单样本)为 l ( y , x , w ) = m a x ( 0 , − y < w , x > ) l(y,x,w)=max(0,-y<w,x>) l(y,x,w)=max(0,y<w,x>),如果分类正确了预测值和真实值一致 − y < w , x > -y<w,x> y<w,x>的结果为负数,那么max取0;如果分类错误,预测值和真实值不一致 − y < w , x > -y<w,x> y<w,x>的结果是正数,那么max取 − y < w , x > -y<w,x> y<w,x>

李航书中介绍的损失函数

损失函数的一个自然选择是误分类点的总数,但是这样的损失函数不是参数w,b的连续可导函数,不能求导优化。

损失函数的另一个选择是误分类点到超平面S的总距离

  1. 一点到超平面的距离公式: d = ∣ w ⋅ x 0 + b ∣ ∣ ∣ w ∣ ∣ d=\frac{|w·x_0+b|}{||w||} d=∣∣w∣∣wx0+b
  • 函数距离(Functional Distance)是点 x 0 x_0 x0 到超平面 S : w ⋅ x + b = 0 S:w⋅x+b=0 S:wx+b=0未规范化距离: 函数距离 = w ⋅ x 0 + b 函数距离=w⋅x_0+b 函数距离=wx0+b,距离为正号,位于超平面的正上方;距离为符号位于超平面的负方向侧。绝对值越大,表示离超平面越远,分类置信度越高。
  • 几何距离是物理意义上的实际距离(垂直距离): d = ∣ w ⋅ x 0 + b ∣ ∣ ∣ w ∣ ∣ d=\frac{|w·x_0+b|}{||w||} d=∣∣w∣∣wx0+b
    1. 假设点 x 0 x_0 x0 沿法向量方向投影到超平面 S S S 上的点为 x ′ x′ x,放缩的方向是 w ∣ ∣ w ∣ ∣ \frac{w}{||w||} ∣∣w∣∣w,移动的距离就是待求的几何距离d。可以得到公式 x ′ = x 0 − d w ∣ ∣ w ∣ ∣ x^′=x_0−d\frac{w}{||w||} x=x0d∣∣w∣∣w,这里是加距离还是减距离是看 x 0 x_0 x0位于超平面的哪一侧。
    2. x ′ x′ x代入超平面方程就可以解出 d = ∣ w ⋅ x 0 + b ∣ ∣ ∣ w ∣ ∣ d=\frac{|w·x_0+b|}{||w||} d=∣∣w∣∣wx0+b
  1. 对于误分类的数据 ( x i , y i ) (x_i,y_i) (xi,yi),有 − y i ( w ⋅ x i + b ) > 0 -y_i(w·x_i+b)>0 yi(wxi+b)>0,当预测值大于0时 y i = − 1 y_i=-1 yi=1,当预测值小于0时 y i = 1 y_i=1 yi=1

误差分类点 x i x_i xi到超平面S的距离是 d = − y i ( w ⋅ x 0 + b ) ∣ ∣ w ∣ ∣ d=\frac{-y_i(w·x_0+b)}{||w||} d=∣∣w∣∣yi(wx0+b)

  1. 假设超平面S的误分类点集合为M,那么所有误分类点到超平面S的总距离为 − 1 ∣ ∣ w ∣ ∣ ∑ x i ∈ M ( − y i ( w ⋅ x 0 + b ) ) -\frac{1}{||w||} \sum_{x_i \in M}(-y_i(w·x_0+b)) ∣∣w∣∣1xiM(yi(wx0+b))
  2. 不考虑 1 ∣ ∣ w ∣ ∣ \frac{1}{||w||} ∣∣w∣∣1,得到感知机的损失函数 − ∑ x i ∈ M ( − y i ( w ⋅ x 0 + b ) ) -\sum_{x_i \in M}(-y_i(w·x_0+b)) xiM(yi(wx0+b))

感知机的收敛定理:什么时候能够停下来,是不是真的可以停下来

收敛定理:若训练数据集线性可分,则感知机学习算法在有限次迭代后,必定收敛到一个能够将训练数据完全正确分类的超平面。

假设数据在一个半径r的区域内,且存在完美超平面 y ( X T w + b ) ≥ ρ > 0 y(X^Tw+b)\geq \rho \gt 0 y(XTw+b)ρ>0 ρ \rho ρ表示余量,该公式的意思是存在完美超平面使得所有样本分类正确且存在余量。

该分界面的参数满足条件 ∣ ∣ w ∣ ∣ 2 + b 2 ≤ 1 ||w||^2+b^2 \leq 1 ∣∣w2+b21,感知机保证在 r 2 + 1 ρ 2 {\frac{r^2+1}{\rho^2}} ρ2r2+1步后收敛。数据越“容易分开”( ρ ρ ρ 越大)或越“紧凑”( r r r 越小),收敛速度越快。

感知机的不足

缺点:感知机不能拟合XOR函数,只能产生线性分割面(一条线)

XOR函数是指当输入的x和y都是1(-1)时,输出-1类;如果x和y不相同,则输出+1类。在下图中,紫色坐标轴下统一颜色的小球是一类。

感知机对于二维输入的分割面是一条线,无论如何切割都没有办法分割不同类的样本。

多层感知模型

多层感知机本质:使用隐藏层和激活函数来得到非线性模型

案例引入

前面有讲过感知机模型的缺点是不能拟合XOR函数,只能产生线性分割面,那么如何学习XOR呢?

案例:学习XOR

要学习非线性可分问题,需考虑使用多层功能神经元。输出层与输入层之间的一层神经元,被成为隐含层,隐含层和输出层神经元都是拥有激活函数的功能神经元。

**做法:**在网络中加入一个或多个隐藏层来突破线性模型的限制,使其可以处理更普遍的函数关系


隐藏层

多层感知机是一种前馈人工神经网络**,**由多个神经元层(输入层、隐藏层、输出层)组成,能够学习复杂的非线性关系。

以下是一个单隐藏机的多层感知机,这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。这个多层感知机中的层数为2, 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。

我们可以把前 L − 1 L−1 L1层看作表示,把最后一层看作线性预测器。

  • 前 L-1 层:负责对输入数据进行特征提取和表示学习(Representation Learning),通过非线性变换将原始数据映射到高维特征空间。
  • 第 L 层(最后一层):基于前 L-1 层提取的特征,进行线性预测(如分类或回归),直接输出最终结果。

多层前馈神经网络:

  • 多层:由多个层级组成,包括 输入层隐藏层(至少一层)和 输出层
  • 前馈(Feedforward):数据从输入层单向传递到输出层,没有循环或反馈连接,与递归神经网络(RNN)不同。
  • 神经元(节点):每个层由多个神经元构成,神经元之间通过权重 连接。
从线性到非线性

单层感知机的局限是无法分离分线性空间,而感知机的叠加(多层感知机)可以分离非线性空间。。

  • 线性空间:使用一条直线分割的空间
  • 非线性空间:实用曲线分割的空间
单隐藏层-单分类案例

输入 x ∈ R n x\in R^n xRn

  • 隐藏层:假设隐藏层有m个隐藏单元,所以输入层->隐藏层有m个标量偏置。隐藏层的权重矩阵 W 1 ∈ R m × n , b 1 ∈ R m W_1\in R^{m \times n},b_1 \in R^m W1Rm×nb1Rm

h = σ ( W 1 x + b 1 ) h=\sigma(W_1x+b_1) h=σ(W1x+b1),其中 σ \sigma σ是按元素的激活函数

  • 输出层:单分类的输出层只有一个神经元所以隐藏层->输出层只有一个标量偏置,输出层权重 w 2 ∈ R m , b 2 ∈ R w_2\in R^m,b_2 \in R w2Rmb2R

o = w 2 T h + b 2 o=w^T_2h+b_2 o=w2Th+b2,输出是一个标量

多隐藏层

超参数

  • 隐藏层数
  • 每层隐藏单元数(一般随深度减少)

深度学习本质就是做压损,所以每一层数据会越来越少,也就是逐层压缩。

一般不会先很狠的压缩数据,再扩充数据。因为这样可能会损失特征。

一般图中会省略偏置权重b的表示,下图在每一层的神经元中都增加了表示偏置的神经元1

以下几个说明需要注意

  1. 每一层(除去输出层)表示偏置的神经元1只会有一个。
  2. 偏置权重b的个数取决于后一层神经元的数量(后一层的神经元不包含偏置神经元)
    3. w 12 ( 1 ) w^{(1)}_{12} w12(1)表示是第1层(这里是从连接线开始数)的权重,且指向后一层的第1个神经元,来源是前一层的第2个神经元。
    4. a 1 ( 1 ) a^{(1)}_1 a1(1)表示第1层的第1个神经元,输入层的神经元一般没有计算功能所以一般是第0层,计算神经网络的总层数也不会计入。
  3. 第1层的神经元用大圈表示意思是 从输入总和后得到输出 a a a然后利用激活函数 h h h处理输出 a a a得到 z z z用于下一层的输入。

激活函数

神经网络的激活函数必须使用非线性函数

问题: 为什么需要非线性的激活函数?

假设 h = W 1 x + b 1 , o = w 2 T h + b 2 h=W_1x+b_1,o=w^T_2h+b_2 h=W1x+b1,o=w2Th+b2 o = w 2 T W 1 x + b ′ o=w^T_2W_1x+b' o=w2TW1x+b仍然输出线性,模型的处理能力并没有变强

激活函数 公式 描述 使用场景 特点
阶跃函数 一旦超过阈值,就切换输出(有点像分段函数) 感知机中神经元之间的流动的是0或1的二元信号
Sigmoid激活函数 h ( x ) = 1 1 + e x p ( − x ) h(x)=\frac{1}{1+exp(-x)} h(x)=1+exp(x)1 sigmoid函数:

sigmoid导数:
将输入投影到(0,1)

神经网络中流动的是连续的实数值信号
ReLU函数 x > 0 x>0 x>0输出该值, x ≤ 0 x\leq 0 x0输出0 h ( x ) = { x i f    x > 0 0 o t h e r w i s e h(x) = \begin{cases} x & if\;x>0\\ 0 &otherwise \end{cases} h(x)={x0ifx>0otherwise ReLu函数:

ReLuh导数:
最常用的激活函数
softmax函数 y i = e x p ( o i ) ∑ k e x p ( o k ) y_i=\frac{exp(o_i)}{\sum_k exp(o_k)} yi=kexp(ok)exp(oi) 多分类问题
分类问题的输出层适合用softmax函数
1. 函数输出(0.0,1.0)之间的实数,输出值总数为1。
2. 使用sigmoid函数之后(单调递增的函数),原本输出元素之间的关系也不会改变(大的置信度转换为大的概率)。所以在推理预测阶段一般会省略输出层的softmax操作
tanh激活函数 t a n h ( x ) = 1 − e x p ( − 2 x ) 1 + e x p ( − 2 x ) tanh(x)=\frac{1-exp(-2x)}{1+exp(-2x)} tanh(x)=1+exp(2x)1exp(2x) tanh函数: tanh导数: 将输入投影到(-1,1)
softmax函数溢出的问题

在softmax回归这一章节,其实有介绍过softmax的数值稳定法。这里再介绍一下如何防止溢出。

y i = e x p ( o i ) ∑ k e x p ( o k ) = C e x p ( o i ) C ∑ k e x p ( o k ) = e x p ( o i + l o g C ) ∑ k e x p ( o k + l o g C ) = e x p ( o i + C ′ ) ∑ k e x p ( o k + C ′ ) y_i=\frac{exp(o_i)}{\sum_k exp(o_k)} = \frac{Cexp(o_i)}{C\sum_k exp(o_k)} = \frac{exp(o_i+logC)}{\sum_k exp(o_k+logC)} = \frac{exp(o_i+C')}{\sum_k exp(o_k+C')} yi=kexp(ok)exp(oi)=Ckexp(ok)Cexp(oi)=kexp(ok+logC)exp(oi+logC)=kexp(ok+C)exp(oi+C)

在进行softmax的指数函数运算时,加上(减去)某个常数并不会改变运算的结果,这里的 C ′ C' C可以是任何值,但是为了防止溢出一般会使用输入信号中的最大值 C ′ = m a x ( o k ) C'=max(o_k) C=max(ok)

稳定版的softmax函数 y i = e x p ( o i + m a x ( o k ) ) ∑ k e x p ( o k + m a x ( o k ) ) y_i=\frac{exp(o_i+max(o_k))}{\sum_k exp(o_k+max(o_k))} yi=kexp(ok+max(ok))exp(oi+max(ok))

多层感知机从零开始实现

数据获取采用之前的方式

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)

初始化模型参数

Fashion-MNIST中的每个图像由 28 × 28 = 784 28×28=784 28×28=784个灰度像素值组成, 所有图像共分为 10 10 10个类别。忽略像素之间的空间结构, 我们可以将每个图像视为具有784个输入特征 和10个类的简单分类数据集。

这里我们实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元。这两个参数都是超参数,可以自己设定。

因为内存在硬件中的分配和寻址方式,通常选择2的若干次幂作为层的宽度,会在计算上更高效。

这里一共是两层神经网络,一层隐藏层,一层输出层,每一层的权重矩阵和偏置向量都需要记录。

定义输入层->隐藏层信号传递的参数, w 1 w_1 w1的形状是 ( n u m _ i n p u t s , n u m _ h i d d e n s ) = ( 784 , 256 ) (num\_inputs,num\_hiddens)=(784,256) (num_inputs,num_hiddens)=(784,256),隐藏层有256个神经元,所以偏置向量 b 1 b_1 b1的形状为 ( n u m _ h i d d e n s , ) = ( 256 , ) (num\_hiddens,)=(256,) (num_hiddens,)=(256,)

定义隐藏层->输出层信号传递的参数,w_2的形状是(256,10),输出层有10个神经元,所以偏置向量 b 2 b_2 b2的形状为(10,)

# num_hiddens 表示隐藏层的神经元个数256个,也就是隐藏层->输出层的输入由256个
num_inputs, num_outputs, num_hiddens = 784, 10, 256

# torch.randn 初始化服从标准正态分布(均值为0,标准差为1)的随机数。
W1 = nn.Parameter(torch.randn(
    num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
    num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

问题1:之前采用的是torch.normal函数,为什么这里采用torch.randn函数

这里本质是一样的,只是用不同的函数生成。

  • torch.randn(size):生成元素服从 标准正态分布(均值为 0,标准差为 1) 的张量
  • torch.normal:生成元素服从 自定义均值和标准差 的正态分布

问题2:为什么得到的矩阵要乘0.01?

在神经网络中,将权重初始值缩小(例如乘以 0.01) 是常见的初始化策略,其核心目的是 避免激活值在初始阶段进入非线性激活函数的饱和区,从而缓解梯度消失问题,加速模型收敛。

若权重初始值较大,线性变换后的输入z(z = Wx + b) 可能较大,导致激活函数输出进入饱和区,梯度消失,阻碍参数更新。

问题3:为什么李沐视频里说加不加nn.Parameter都可以?

在 PyTorch 中,nn.Parameter 的作用是将一个张量标记为模型的可训练参数,使其能够被优化器自动追踪和更新。

如果代码中显式将张量加入参数列表(如 params = [W1, b1, W2, b2]),即使未使用 nn.Parameter,只要张量的 requires_grad=True,优化器依然可以更新它们的梯度。

W1 = torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01  # 未使用 nn.Parameter
b1 = torch.zeros(num_hiddens, requires_grad=True)
params = [W1, b1, W2, b2]  # 手动传递参数给优化器
# 优化器仍会更新 W1 和 b1 的梯度,因为它们的 requires_grad=True。
optimizer = torch.optim.SGD(params, lr=0.1) 

激活函数:ReLU函数

这里使用ReLU函数作为激活函数

def relu(X):
    # 创建与X形状相同的全零张量
    a = torch.zeros_like(X) 
    # 对X和a逐元素取最大值
    return torch.max(X, a)  

模型

在 PyTorch 中,@ 符号是 矩阵乘法运算符,用于执行两个张量之间的矩阵乘法。

X @ W 等同于 torch.matmul(X, W)

def net(X):
    # 先输入拉成一个二维矩阵
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  
    return (H@W2 + b2)

损失函数

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

训练

因为李沐老师模型训练用到了train_ch3函数,这个函数里画图适用于Jupyter,所以开发工具开始使用Jupyter

num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
报错module 'd2l.torch' has no attribute 'train_ch3'

原因:我的版本是1.0.3太新了,在新版本中移除了

**解决办法:**把之前手动定义的函数搬过来

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)


# 初始化参数
# num_hiddens 表示隐藏层的神经元个数256个,也就是隐藏层->输出层的输入由256个
num_inputs, num_outputs, num_hiddens = 784, 10, 256

# torch.randn 初始化服从标准正态分布(均值为0,标准差为1)的随机数。
W1 = nn.Parameter(torch.randn(
    num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
    num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

def relu(X):
    # 创建与X形状相同的全零张量
    a = torch.zeros_like(X)
    # 对X和a逐元素取最大值
    return torch.max(X, a)

def net(X):
    # 先输入拉成一个二维矩阵
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)
    return (H@W2 + b2)


# 返回y_hat与y元素比较的结果数组
def accuracy(y_hat, y):
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

# 常用的变量累计类
class Accumulator:  #@save
    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]
# 每个epoch的训练过程
def train_epoch(net, train_iter, loss, updater):  # @save
    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):
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        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]
# 计算在指定数据集上的精准度,返回准确率
def evaluate_accuracy(net, data_iter):  # @save
    if isinstance(net, torch.nn.Module):
        net.eval()
    metric = Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]
# 模型的训练
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
    animator = d2l.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(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))

num_epochs, lr = 10, 0.1
loss = nn.CrossEntropyLoss(reduction='none')
updater = torch.optim.SGD(params, lr=lr)
train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

多层感知机模型的观察

损失逐渐减小,但精度并没有比之前好很低(之后内容会探讨这个问题)。

多层感知机的简洁实现

  1. 神经网络的定义
  2. 参数的初始化
  3. 损失函数 ✔

已经使用了pytorch里的nn(Neural Network)的交叉熵损失函数nn.CrossEntropyLoss(reduction='none')

  1. 优化器 ✔

已经使用了pytorch里的随机梯度下降优化器torch.optim.SGD(params, lr=lr)


  1. 模型的定义可以采用nn(Neural Network)模块
    1. 展平层:将输入的数据拉平为(batch_size, 784)

Flatten()默认第一个数保持不变,后面的维度拉平。

2. 隐藏层(全连接层),将 784 维输入映射到 256 维特征空间。然后传递给激活函数处理。

这里256是超参数假设有256个隐藏单元。

3. 最后一层输出层(全连接层),将 256 维特征映射到 10 维输出
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))

Flatten函数展平的细节

标准输入形状:(batch_size, channels, height, width),简称 NCHW。

这是 PyTorch 中大多数层(如卷积层、全连接层)的默认预期格式。
nn.Flatten() 默认从 第 1 维开始展平(即保留第 0 维 batch_size)。

输入形状 (256, 1, 28, 28) → 展平后为 (256, 1 * 28 * 28) = (256, 784)

  1. 参数初始化

如果模块是 nn.Linear 层,则用均值为 0、标准差为 0.01 的正态分布初始化其权重。

偏置(bias)未显式初始化,默认使用 PyTorch 的默认初始化(通常为零初始化)。

PyTorch 的 nn.Linear 层在初始化时会 根据输入和输出维度自动创建权重矩阵,所以不需要现实化定义维度。

def init_weights(m):
    if type(m) == nn.Linear:
        #初始化层:Linear(in_features=784, out_features=256, bias=True), 权重形状:torch.Size([256, 784])
        #初始化层:Linear(in_features=256, out_features=10, bias=True), 权重形状:torch.Size([10, 256])
        print(f"初始化层:{m}, 权重形状:{m.weight.shape}")
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)

手动实现与PyTorch全连接层实现的细节区别

  • 手动实现:W的形状为[输入特征维度,输出类别数],模型计算是 X W + b XW+b XW+b
  • 全连接层的前向传播公式为: 输出 = X ⋅ W T + b 输出=X⋅W^T+b 输出=XWT+b

其实这里都无所谓,手动实现也可以将计算改为 W T W^T WT,只要前后维度是一样的就行。

  1. 损失函数和优化器

直接使用之前的代码

batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

代码

MLP(Multilayer Perceptron)多层感知机 如果效果不好,很容易的转卷积、RN、Transformer,所以MLP经常被使用。

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:
        print(f"初始化层:{m}, 权重形状:{m.weight.shape}")
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)

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)

# 返回y_hat与y元素比较的结果数组
def accuracy(y_hat, y):
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())
# 常用的变量累计类
class Accumulator:  #@save
    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]
# 每个epoch的训练过程
def train_epoch(net, train_iter, loss, updater):  # @save
    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):
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        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]
# 计算在指定数据集上的精准度,返回准确率
def evaluate_accuracy(net, data_iter):  # @save
    if isinstance(net, torch.nn.Module):
        net.eval()
    metric = Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]
# 模型的训练
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
    animator = d2l.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(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))

train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

Q&A解疑

问题1:神经网络的一层是指什么?

每一个箭头表示可以学习的权重,通常一层需要包含权重+激活函数

问题2:为什么神经网络要增加隐藏层的层数,而不是增加隐藏层的神经元个数

胖神经网络:增加神经元个数 - 容易过拟合

深神经网络:增加层数

这个问题刘宏毅老师课程的《鱼和熊掌可以兼得的机器学习》有讲解过

当参数量一样(未知参数量一样)的时候,Fat+Short的网络和Thin+Tall的网络谁的效果更好?=> 实验表明:Thin+Tall的网络效果更好

deep learing的核心:产生同一个function,使用deep的架构需要的参数量更少,可能需要较少的训练资料就可以了(不容易overfitting)。

问题3:不同任务下的激活函数是不是都不一样?都是通过实验来确定的吗?

激活函数的选择都差不多,远远没有选择隐藏层大小等超参数重要,推荐用ReLu


网站公告

今日签到

点亮在社区的每一天
去签到