ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

神经网络基础:从神经元到反向传播与PyTorch实现

神经网络基础:从神经元到反向传播与PyTorch实现 连续写了两天的深度学习笔记今天来到神经网络基础这一节。前两篇已经聊过感知机、梯度下降这些前置概念按我的习惯今天应该把神经网络的骨架彻底讲透神经元、权重、偏置、激活函数、前向传播、反向传播最后再手写并用PyTorch跑通一个最小示例。写这篇笔记前我翻了很久资料发现很多教程要么上来就是矩阵公式轰炸要么用神经元像大脑细胞这种完全帮不上忙的比喻把读者带偏。所以这篇我换一种讲法神经网络就是一堆可以微调的旋钮组成的函数机器我们要搞清楚的只有三件事——机器怎么搭、机器怎么算、机器怎么调。这篇文章适合两类人一类是刚看完感知机和损失函数、准备进入神经网络的新手另一类是已经会用框架但总觉得内部原理模糊、想补功课的读者。我会把核心概念用生活化类比拆开给出完整的推导逻辑和可直接抄走的PyTorch代码把我实际调试时踩过的坑也一并交代清楚。1. 神经网络到底在做什么从函数拟合这个角度看问题1.1 为什么说神经网络是高维非线性映射先抛一个我反复强调的观点深度学习的本质是拟合一个输入到输出的映射函数。这个函数有无数个参数网络的训练过程就是在调这些参数让目标函数在训练集上输出尽量接近真实标签。你给一张图片网络输出猫给一段语音网络输出你好给一堆用户行为数据网络输出点击率。这些都是从输入空间到输出空间的映射只是输入输出的维度不同、复杂度不同。神经网络能火起来核心原因是它能拟合极其复杂的非线性关系。传统机器学习里你大多时候得自己设计和选择特征比如做房价预测你得手动考虑面积、位置、楼层、朝向还想办法把它们组合成更高级的特征。而神经网络的做法是把特征工程也一起学出来——隐藏层的每个神经元会自动学会提取不同级别的特征第一层可能学到边缘第二层学到纹理第三层学到物体部件。这种逐层抽象的能力就是深度两个字的意义。1.2 从感知机到神经网络的跳跃感知机其实就是一个线性分类器输入加权求和超过阈值就输出1否则输出0。问题是它太硬了输出不是0就是1参数稍一变就可能从全错变成全对导致梯度根本无法传递。神经网络的突破在于两个改动第一输出不再是阶跃函数改成了连续可导的激活函数比如sigmoid、ReLU输出从0到1之间的连续值或更宽范围的数值。这样参数的微小调整会导致输出的微小变化梯度就能顺着这个变化一路传回去。第二不再是单层结构而是多层堆叠。单层网络能力极其有限——它只能解决线性可分的问题异或XOR问题直接卡死。但多层之后哪怕每层只做简单的线性变换加非线性激活堆叠起来就能表达非常复杂的函数。这里有个著名的结论——通用近似定理只要隐藏层神经元数量足够一个单隐藏层的神经网络就能以任意精度逼近任何连续函数。这个定理告诉我们神经网络的上限很高但实践中单层往往需要极多的神经元且难以训练所以大家才选择深而窄的多层结构。我需要强调一点层数堆上去之后模型的表达能力增强但优化难度也随之上升。梯度消失、过拟合、训练不稳定都会找上门来。所以神经网络不是越深越好调整网络结构本身就是一个经验和实验的过程。2. 基本单元拆解神经元、权重、偏置与矩阵化2.1 神经元的输入输出加权求和加偏置单个神经元的计算逻辑非常简单就两步把输入的各维度分别乘以对应权重再加起来最后加上偏置把上一步的结果送进激活函数得到输出。如果写成数学式就是典型的z w1*x1 w2*x2 ... wn*xn b a activation(z)这里w是权重向量b是偏置activation是激活函数a是当前神经元的输出它会作为下一层的输入继续参与计算。整个网络就是这样一个接一个地传递前一层输出等于后一层输入。2.2 权重、偏置的物理直觉很多人学到这里就记住了公式但没建立直觉。我给几个我觉得最有用的类比权重可以理解成重要程度。每个输入都会说话但嗓门大小由权重控制。如果w1的绝对值远大于w2等于说网络认为x1对最终决定的影响远高于x2。权重还可以是负的负权重表示这个输入越大会抑制我的激活相当于投反对票。偏置可以理解成阈值偏移。如果偏置很大哪怕输入全是0神经元也有可能被激活如果偏置很负神经元就比较迟钝需要足够强的输入总和才能被激活。我之前做过一个比喻权重像评委打分的力度偏置像评委的立场——有人严格有人宽松。千万别小看这两个概念调参时理解越深越容易定位问题。比如某个神经元死活激活不起来多半是偏置初始化不当或者输入分布太集中这时候不要盲目调学习率先检查数据和初始化。2.3 矩阵化把一堆神经元变成一行代码单个神经元容易理解但真实网络动辄几十万甚至上亿参数逐个写循环必然爆炸。所以深度学习框架都用矩阵运算来批量处理。假设输入是X形状是(批次大小, 特征数)某个全连接层的权重是W形状是(输入特征数, 神经元数)偏置是b形状是(神经元数,)那么这一层的输出就是Z X W b A activation(Z)是矩阵乘法。这一步看似微不足道却是神经网络能被高效训练的关键之一。GPU天生就是做矩阵乘法的CPU也能通过底层优化库快速计算。理解形状关系后你会发现自己调试时手到擒来——绝大多数新手报错都是维度对不上而维度问题只要画一下矩阵形状立刻清楚。我自己的习惯是调试时打印每一层的input_shape和output_shape一边写代码一遍对照。别觉得low出bug时这个习惯能省一小时。3. 前向传播与激活函数为什么非线性才是关键3.1 前向传播的计算过程前向传播就是数据从输入层一路计算到输出层的全过程。我们平时写代码调model(x)时PyTorch已经在内部替你跑完了这个过程。对于全连接神经网络假设计算过程如下h1 activation(X W1 b1) h2 activation(h1 W2 b2) output h2 W3 b3 // 输出层可能没有激活函数这里每一层的输出都作为下一层的输入。activation是激活函数W1、b1是第一层参数W2、b2是第二层参数以此类推。注意最后一层往往不加激活函数尤其是回归任务直接输出连续值即可如果是分类任务最后会套一个softmax把logits转换成概率分布。3.2 为什么必须要有激活函数这个问题我记得特别清楚。如果所有层都用线性变换没有激活函数那么无论叠多少层它的整体仍然是一个线性函数。因为线性函数的组合还是线性函数你把三层Wxb展开合并之后还是一个Wxb。这样的话深层网络和单层网络没有区别直接退化成线性回归或线性分类器表达能力和深度就彻底失去意义了。激活函数的核心作用就是给网络注入非线性。它往往是一个逐元素操作比如ReLU(x) max(0, x)。有了它层与层之间才真正叠加出复杂函数。这也是为什么激活函数被称为神经网络必不可少的开关——它决定某个神经元要不要被激活。3.3 常用激活函数选型对照我用过比较多的激活函数有这么几种放到一起对比一下激活函数公式输出范围优点缺点常用场景Sigmoid1/(1e^-x)(0, 1)平滑、可解释性强适合输出概率容易梯度消失输出非零中心不利于训练二分类输出层、早期网络Tanh(e^x - e^-x)/(e^x e^-x)(-1, 1)零中心比sigmoid训练更稳仍可能梯度消失隐藏层早期常用、RNN变体、归一化场景ReLUmax(0, x)[0, ∞)计算快、缓解梯度消失实际中最常用神经元死亡输出无上界卷积网络、全连接网络默认首选LeakyReLUx0时xx0时0.01x(-∞, ∞)解决ReLU死亡问题需要多调一个斜率参数对ReLU死亡敏感的深度网络Softmax见公式(0,1)和为1把logits转成概率分布只适合输出层多分类输出层ReLU用起来确实省心但注意它有一个叫神经元死亡的问题如果某个神经元的输入长期为负梯度为0权重再也更新不了这个神经元就废了。我跑模型时如果看到loss不降会检查一下隐藏层有多少死亡神经元——特征分布不合适时ReLU死亡比例可能很高这时候用LeakyReLU会明显改善。3.4 手算一次前向传播为了彻底搞懂我们手算一个极简的例子。假设输入是一个二维向量x [1, 2]第一层权重W1 [[0.5, -0.2], [0.3, 0.8]]形状2x2偏置b1 [0.1, -0.1]激活函数用ReLU。第一步算线性部分z1 x · W1的列方向计算 z1_1 1*0.5 2*0.3 0.1 1.2 z1_2 1*(-0.2) 2*0.8 (-0.1) 1.3第二步过ReLUh1 [max(0,1.2), max(0,1.3)] [1.2, 1.3]这些数字就是第一层的输出继续往下传直到最后一层前向传播就结束了。你可能会觉得这步骤未免太简单了但深度学习靠的就是把这么简单的步骤重复成千上万次、并调整其中参数。4. 反向传播和梯度下降神经网络的学习是怎么发生的4.1 损失函数目标驱动的标尺网络算出来的输出未必正确那怎么量化错多少靠损失函数。损失函数的输入是网络输出和真实标签输出是一个标量数值数值越小代表预测越准。回归任务最常用的是均方误差MSE公式为L (1/N) * Σ(y_pred - y_true)^2。分类任务最常用的是交叉熵CrossEntropyLoss它衡量预测概率分布与真实分布之间的差距。注意PyTorch里的CrossEntropyLoss是直接接收logits未过softmax的输出内部已经帮你做了softmax和取对数所以不要再手动加softmax否则等于多套一层效果反而变差。这个坑我见过不止一次。损失函数的选择直接决定网络优化的方向。比如多分类用交叉熵二分类用二元交叉熵回归用MSE或MAE。还有一些任务用自定义损失比如目标检测会组合分类损失和回归损失。先记住最常用的几种后面遇到具体任务再按需扩展。4.2 链式法则与反向传播的直观理解训练的核心是让损失函数值变小办法是梯度下降算出损失对每个参数的偏导然后朝着梯度负方向更新参数。这个偏导的计算过程就是反向传播。一说反向传播很多人本能的头疼但其实可以拆解成两步正向传播时记录每层的中间输出——这些值后面要用从输出层开始用链式法则一层一层向前求导数。链式法则的数学直觉你肯定见过如果y f(u)u g(x)那么dy/dx (dy/du) * (du/dx)。反向传播就是沿着这个规则从最终的loss往前逐步乘以各层的局部梯度。这个想法非常朴素却支撑起了整个深度学习框架。框架替你干了所有求导的脏活你只需要定义网络结构和损失函数调用loss.backward()PyTorch就会为所有requires_gradTrue的变量填充好.grad值。但你心里要清楚它做了什么它是在高效地、分阶段地计算复杂函数的链式偏导。真到了训练不收敛的时候你会感激自己理解过这一步。4.3 优化器与学习率调参经验有了梯度接下来就是用优化器更新参数。最朴素的是随机梯度下降SGD更新公式是w w - lr * grad。但纯SGD收敛慢而且容易卡在局部最优附近震荡所以有了各种改进版动量Momentum加上历史梯度的指数加权SGD的震荡能大幅缓解Adam则结合了动量和自适应学习率对绝大多数任务来说开箱即用。我自己的实践规律是新手期无脑用Adamlr3e-4稳妥不容易爆炸。跑出一版baseline之后如果想追求极致效果再换SGDMomentum配合学习率衰减策略。当年我从Adam换成SGD时模型精度确实提升了一点但代价是调参更细、时间更长。先别追求最优先求稳。关于学习率我有几个真实心得学习率设置过大loss会剧烈震荡甚至直接NaN我在PyTorch里见过最经典的报错就是loss突然变NaN十有八九是学习率太高或数据没归一化。学习率设置过小loss下降极慢半天看不到进展人也容易崩溃。比较高效的做法是先跑几次短训练输出loss曲线观察它在不同学习率下的行为再选一个合理的起点。比如从1e-2开始依次减半眼看loss下降速度骤减就停下来换更小的。等理解了这些基础再去看学习率预热、余弦退火、周期策略这些高级玩法才算水到渠成。5. 用PyTorch搭一个最简网络代码直接抄5.1 环境准备与踩坑点深度学习第一步永远卡在环境配置。我的建议是别折腾直接按这条路径来装好Python 3.8及以上版本用conda建一个干净的环境conda create -n dl python3.9安装PyTorch时去PyTorch官网选好自己的操作系统、包管理器和CUDA版本复制粘贴对应命令不要自己猜。如果没有NVIDIA显卡就别装CUDA版了直接装CPU版本照样能跑代码只是慢一点。很多人一上来就想配双GPU训练环境、用分布式结果搞了一整天环境都没跑通非常打击信心。先跑通再谈效率。另外我强烈建议在环境里装好jupyter或直接用IDE比如VS Code边写代码边查看中间变量比纯脚本调试舒服得多。还有一点固定随机种子。很多新手困惑为什么我跑出来的结果跟教程不一样大概率就是没固定种子导致数据加载顺序、权重初始化都不一样。代码开头写上torch.manual_seed(42) np.random.seed(42)结果就会可复现很多。5.2 最小复现两层网络训练MNIST下面是一段可以直接跑的最小编程示例用全连接网络在MNIST手写数字数据集上做分类。MNIST很小CPU也能在几分钟内训练完非常适合用于理解神经网络全流程import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader # 固定种子 torch.manual_seed(42) # 数据预处理转成Tensor并归一化到[0,1] transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # 加载MNIST train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse) # 定义两层全连接网络 class SimpleNN(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28*28, 128) # 输入784维 - 128个神经元 self.fc2 nn.Linear(128, 64) # 128 - 64 self.fc3 nn.Linear(64, 10) # 64 - 10个类别 self.relu nn.ReLU() def forward(self, x): x x.view(x.size(0), -1) # 拉平成784维 x self.relu(self.fc1(x)) x self.relu(self.fc2(x)) x self.fc3(x) # 输出层不加激活配合CrossEntropyLoss return x model SimpleNN() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) # 训练 def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 200 0: print(fEpoch {epoch}, Batch {batch_idx}, Loss: {loss.item():.4f}) # 测试 def test(): model.eval() correct 0 total 0 with torch.no_grad(): for data, target in test_loader: output model(data) pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() total target.size(0) print(fTest Accuracy: {100.0 * correct / total:.2f}%) for epoch in range(1, 6): train(epoch) test()这个代码很短但把神经网络的核心流程全走了一遍数据加载、模型定义、前向计算、损失计算、反向传播、参数更新、评估。你运行后会发现准确率能到97%以上而这一切只用了五轮迭代和一个刚能跑通的全连接网络。这个实验是入门神经网络的绝佳路径。5.3 训练过程中我亲眼见过的几种真实现象跑这个示例时你会观察到几个非常典型的现象loss下降曲线有波动很正常。因为mini-batch是随机抽的每个batch的分布略有差异loss忽高忽低是正常现象别一看到反弹就想加学习率或换模型。Epoch 1之后准确率会突然拔高。这既让人兴奋也容易产生误判。刚训练一个epoch模型还没有完全收敛准确率高并不意味着网络已经学得很好继续训练你会发现后面提升幅度要小得多。深度学习靠的是调参堆量前期涨得快后期涨得慢得有心理预期。测试准确率如果比训练准确率低很多就是过拟合信号。比如训练集上到了99%测试集只有92%说明模型开始死记硬背训练样本。这时可以加dropout、加正则、减小模型容量、增加数据增强。我见过不少人第一次训练就追求训练集100%准确率这其实是错误方向——真正该关注的是测试集泛化能力。5.4 代码层面的几个关键细节几个代码细节值得单独拎出来说说model.train()和model.eval()这两个模式对dropout和BatchNorm层的行为影响很大。训练时用train()让dropout生效测试时用eval()关掉dropout否则每次预测结果都会随机的准确率忽高忽低。optimizer.zero_grad()千万别漏。PyTorch的梯度默认是累加的如果不每次清零梯度会不断累积损失函数直接飘走。torch.no_grad()测试时根本不需要计算梯度包上它能让内存占用和计算量少很多跑大批次数据时尤其明显。x.view(x.size(0), -1)-1的意思是自动推导这一维的大小全连接层输入必须拉平成向量形式这个操作非常常用。6. 从day03往后看下一步该学什么神经网络基础这一篇终于写完了但我知道很多同学可能还在迷茫。今天的内容是后续所有模型——卷积网络、循环网络、Transformer、扩散模型——的公共地基。你如果能把这篇里的概念吃透之后学CNN和RNN时会有一种哦原来只是换了一些层结构的通透感。接下来我计划这样安排day04PyTorch数据加载与预处理细节把Dataset、DataLoader、transform的组合用法彻底搞明白day05训练一个真正能用的CNN在CIFAR-10上走一遍卷积、池化、全连接的完整流程day06反向传播手推与自动求导原理把梯度计算的细节彻底过一遍。按部就班走就好。深度学习入门的真正门槛不是数学也不是代码而是你能不能沉下心把最简单的东西做到透彻理解。神经网络基础这部分我写出来的都是自己反复验证过的东西你拿去用、拿去改跑通一遍比浏览十篇教程都管用。
返回列表