从零理解神经网络:手把手带你看懂神经网络是怎么学习的

向量数据库大模型机器学习

本文通过一个最简单的手写数字识别神经网络的训练过程,带你一步步看清前向传播、反向传播、梯度下降、学习率 这些常被提及但让人犯糊涂的术语是什么,具体是怎么做的,搞懂神经网络到底是怎么学会识别数字的!

我们要做什么?

我们用一个最简单的神经网络(MLP,全称 Multi-Layer Perceptron)来识别手写数字。

比如下面这些图片:

picture.image

每张图片都是分辨率为 28×28 的灰度图,图中是一个手写体的数字。我们希望神经网络看一眼这些图,就能说出其中的数字。或者说判断出图片中数字最可能是几。

在后面的处理中,我们会把每个图片,用一个28×28=784维的向量表示。向量的每个值,表示这个图片对应像素的灰度值。比如一个图片对应的向量可能是:(0,36,255,...,0)

我们的模型结构

我们使用 PyTorch 定义一个非常简单的模型,只包含一个隐藏层:


 
 
 
 
   
class MLP(nn.Module):  
    def \_\_init\_\_(self):  
        super(MLP, self).\_\_init\_\_()  
        self.fc1 = nn.Linear(28*28, 512)  # 输入层 → 隐藏层,输入784,输出512  
        self.fc2 = nn.Linear(512, 10)     # 隐藏层 → 输出层,输入512,输出10,对应10个类别  
  
    def forward(self, x):  
        x = x.view(-1, 28*28)             # 展平图像  
        x = torch.relu(self.fc1(x))       # ReLU激活  
        x = self.fc2(x)                   # 输出10类logits  
        return x

其中 class MLP(nn.Module) 表示 MLP 继承自 nn.Module 类,而 nn.Module 类封装了神经网络模型的所有公共操作。一个 nn.Module 对象,就表示一个神经网络模型实例。

初始化函数 __init__中,放置模型每一层的定义。类似 self.fc1 = nn.Linear(28*28, 512) 这样就定义了一个线性层(也称为全连接层或稠密层)。nn.Linear 封装了线性层的所有计算。fc1 是给这层取个名字,方便在其他地方调用。

nn.Linear 是 PyTorch 中表示线性层的类。这个类对应的计算过程,就是一个简单的线性变换:

其中

是输入数据向量,

是权重矩阵,

是偏置向量。

nn.Linear(28*28, 512) 表示输入特征的数量是 28×28=784 维,也就是展平后的图像像素数量。第二个参数 512 表示输出特征的数量,也可以理解为神经元的数量,每个神经元,学习一个特征。所以这里定义了模型的2个层,都是线性层:

  • • self.fc1 = nn.Linear(28*28, 512) 表示 fc1这个线性层,接受784维的输入向量,输出一个512维的向量
  • • self.fc2 = nn.Linear(512, 10) 表示 fc2 这个线性层,接受512维的输入向量,输出一个10维的向量

forward 函数定义了模型的具体计算过程。也就是通常所说的前向计算。其中 x 表示输入数据。

  • • x = x.view(-1, 28*28) 将输入的二维数据拉平为一维
  • • x = torch.relu(self.fc1(x)) 将拉平的一维数据传入第一层fc1进行计算,结果应用ReLU激活函数
  • • x = self.fc2(x) 将经过ReLU编号值,输入模型第二次fc2进行计算,计算结果作为模型输出

下图表示了这段代码定义的模型结构:

picture.image

本质上是什么呢,就是我有个一数据集,数据集每个样本有784个数据。现在我要定义一个神经网络,神经网络的核心是一个包含512个神经元的隐藏层。把数据集每个样本的每个数据都看做一个信号。每个神经元都接收每个样本的784个信号进行计算。计算的方式为

即每个信号乘以一个权重后相加,再加上一个偏置值b,进行激活函数变换后,作为该神经元的计算结果。不同的神经元,用不同的权重来计算:

...

我们把这层神经元的所有参数和偏置合并起来,作为参数矩阵

和偏置向量

; 我们期望通过调整

,让这1层的512个神经元,能学习到手写体的不同的512个特征。然后将这512个特征,再转交给下一层(输出层)10个神经元处理。期望这10个神经元,能通过这512个特征,判断出其是某一个数字的概率:

...

我们把这层神经元的所有参数和偏置合并起来,作为参数矩阵

和偏置向量

;。我们期望通过调整

,及偏置

,

让这个神经网络能识别每个样本对应的数字。

所以整个神经网络的计算过程为:

其中

是ReLU激活函数。这个函数的作用是,就是把所有负值变成0,正值不变。不要小看这个处理,有了这个处理,就为模型引入了非线性因素。如果没有经过这样的非线性激活函数处理,那么计算过程就成了纯线性的叠加:

也就是不管你加多少层,最后都会等效坍缩为一层,模型也就失去了抽象复杂特征的能力。这就像遗传中缺了变异,物种就不会进化出丰富的形态和能力了

那么参数

,及偏置

,

的初始值是什么? 又是如何更新的呢?我们先来看下初始值

初始权重

当定义好模型,执行以下代码实例化模型时:


 
 
 
 
   
model = MLP()

PyTorch 会自动为你的每一层(例如 nn.Linear)生成一组初始权重 初始偏置。

这些初始值 不是全为 0,也不是全为 1 ,而是从某种概率分布中 随机采样出来的数值

PyTorch 也支持你用 torch.nn.init 模块手动设定权重初始值,这里就不展开了。

我们可以通过以下代码,打印下初始权重看看:


 
 
 
 
   
model = MLP()  
  
print(f"fc1.weight:")  
print(model.fc1.weight.data)  
  
print("\nfc1.bias:")  
print(model.fc1.bias.data)  
  
print("\nfc3.weight:")  
print(model.fc3.weight.data)  
  
print("\nfc3.bias:")  
print(model.fc3.bias.data)

在 Jupyter 中可以看到类似如下输出:


 
 
 
 
   
----- 初始权重和偏置参数:  
  
fc1.weight.shape:torch.Size([512, 784])  
fc1.weight:  
tensor([[-0.0114, -0.0108, -0.0142,  ..., -0.0291,  0.0284, -0.0165],  
        [ 0.0025,  0.0220, -0.0124,  ..., -0.0053,  0.0252,  0.0342],  
        [ 0.0184,  0.0034,  0.0044,  ...,  0.0037, -0.0164, -0.0269],  
        ...,  
        [ 0.0055,  0.0348, -0.0229,  ...,  0.0182,  0.0155, -0.0074],  
        [-0.0117, -0.0185, -0.0128,  ..., -0.0299, -0.0345,  0.0234],  
        [-0.0321,  0.0172,  0.0070,  ..., -0.0080, -0.0198, -0.0178]])  
  
fc1.bias:  
tensor([-2.4902e-03, -1.4291e-02,  2.9421e-02,  3.4671e-03, -1.6535e-02,  
        。。。  
        -1.6776e-02, -1.7924e-02])  
  
fc3.weight:  
tensor([[ 0.0079,  0.0337, -0.0289,  ..., -0.0052,  0.0380,  0.0055],  
        [ 0.0127, -0.0006,  0.0209,  ...,  0.0379,  0.0191,  0.0254],  
        [ 0.0247,  0.0291, -0.0160,  ..., -0.0087,  0.0204, -0.0298],  
        ...,  
        [ 0.0068, -0.0374, -0.0327,  ...,  0.0371,  0.0080,  0.0135],  
        [ 0.0042, -0.0291, -0.0007,  ..., -0.0246, -0.0369, -0.0115],  
        [ 0.0229, -0.0348,  0.0250,  ...,  0.0060,  0.0274, -0.0048]])  
  
fc3.bias:  
tensor([-0.0227,  0.0110, -0.0008, -0.0030,  0.0097,  0.0047,  0.0030,  0.0433,  
         0.0112, -0.0292])

这就是初始权重。这个模型的参数数量:

  • • 第一层:权重参数:784 × 512 = 401,408,偏置参数:512,共 401,408 + 512 = 401920
  • • 第二层:权重参数:512 × 10 = 5120,偏置参数:10,共 5120 + 10 = 5130

总共有 401920+5130 = 407050 个可训练参数。模型从这些初始权重开始,在后面的训练过程中,通过一个个样本来学习其内在规律,并逐步更新这些权重。

模型具体是怎么学习的呢? 对于每个样本,学习过程(训练迭代)包含前向传播、损失计算、反向传播、梯度下降、权重更新这几个环节

前向传播

前向传播的计算过程,就是前面 PyTorch 模型定义中,forword 函数执行的过程。第一次前向传播计算,从初始权重开始。每次 “前向传播”也可以认为是基于模型当前的权重参数,对输入样本做一次预测。我们用一个简化的数据,一步步手算下,假设:

  • • 输入图像

(为了简单,我们只用2个输入维度来讲解)

  • • 第一层权重矩阵

,偏置

  • • 第二层(输出层)权重

,我们暂时只取前两个神经元的输出做说明。

第一步:输入 → 隐藏层

经过 ReLU 激活:

第二步:隐藏层 → 输出层

设输出层的参数为:

那么:

这个输出 [-0.1, 1.18] 就是 logits ,可以用 softmax 函数转为概率。

这个预测的结果,如果是推理场景,就作为结果返回了。如果是训练场景,就会计算和标注值的偏差,根据这个偏差程度,来计算权重如何更新。这个偏差,通常叫做损失(loss)

损失计算

假设真实标签是数字 "1"(标注为1),我们希望模型对“1”打分高。

我们常用 CrossEntropyLoss 作为损失函数,在 PyTorch 中有对应的实现 nn.CrossEntropyLoss() ,它的计算等价于:

其中

是模型输出的 logits

是真实标签。对我们前面的 logits 样例使用交叉熵损失函数:

那么损失0.26 这个值越小,表示模型越接近正确答案。

反向传播

反向传播的本质就是:计算每个参数对损失的“责任”(梯度),然后反省调整!

如何根据损失确定每个参数的责任呢?这里要用到链式求导法则(Chain Rule)。

在我们的例子中:

这是交叉熵损失 + softmax。那我们会先计算:

再乘上:

得到:

继续传回去:

最终:

反向传播也就是梯度在链式法则下层层往回传递。回到我们前面的简化数据的例子,通过 链式法则 计算损失对每个参数的导数。

比如:

  • • 输出层梯度:

  • • 再计算:

这表示第一个输出神经元的权重应略增,第二个应略减。

梯度下降

神经网络的目标是最小化一个“误差”或者“损失”,比如预测错数字的程度。梯度 :表示“你当前的参数往哪个方向调整,误差会上升最快”。那么我们就:往反方向走 ,让误差最小。这就是梯度下降。

权重更新

梯度指明了权重更新的方向。在这个方向上具体怎么更新,涉及到一个更新的步子大小的问题。这个步子大小由学习率控制。

权重更新的数学式:对每个参数

,我们通过下式来更新它:

其中:

是学习率(learning rate);

是损失函数对参数 w 的导数;

  • 表示我们每次“走一点点”,让误差越来越小。

举个例子:

假设模型对数字3预测错了,计算出的损失对某个权重的梯度是 +0.2,学习率是 0.1:

这个参数就被“拉回来”一点,模型变得更“准确”一点

权重更新后,一个完整的训练迭代就完成了。下一个(下一批)样本会在新的权重下,继续同样的迭代,直到所有的训练数据执行完,则训练完成。如果过程中 loss 值已经达到最小,也可以提前结束训练,避免过拟合。最后产生的权重,就是训练出来的结果模型。

总结

MLP就是这样一步步学会如何分类的:

前向传播 :像人一样“推理”出一个结论

损失函数 :衡量结论离正确答案有多远

反向传播 :计算每个参数对错误的“责任”

梯度下降 :根据责任方向(梯度方向)小步修正这些参数

循环这一过程 ,直到模型变得“聪明”

在神经网络中,我们通过前向传播 计算模型预测结果,通过损失函数 评估预测的好坏。接着,利用链式法则 ,一步步反推每个参数对误差的“责任”(即导数或梯度),再通过梯度下降 方法,把每个参数朝着减小误差的方向微调一小步。这个“预测 → 评估 → 反省 → 改正”的循环,就是神经网络训练的核心。

是不是破除了“神秘”感后,比你想象得更“简单”了?

具体代码可以参见上一篇《 深度学习模型训练的一般过程》。上一篇用了两个隐藏层。发现删掉一个后,最终loss略有下降,但差别不大:

  
测试集: 平均损失: 0.0001, 准确率: 9743/10000 (97.43%)
0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动 XR 技术的探索与实践
火山引擎开发者社区技术大讲堂第二期邀请到了火山引擎 XR 技术负责人和火山引擎创作 CV 技术负责人,为大家分享字节跳动积累的前沿视觉技术及内外部的应用实践,揭秘现代炫酷的视觉效果背后的技术实现。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论