ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

神经网络入门:从拟合曲线到反向传播的认知地图

神经网络入门:从拟合曲线到反向传播的认知地图 神经网络这个概念很多人第一次接触时都会被各种术语砸晕——反向传播、激活函数、梯度下降、卷积核每个词单拎出来都能写一本书。但如果你把那些数学包装全部剥掉会发现它的核心思想朴素得惊人用一堆简单的计算单元搭成网络让数据自己告诉网络该怎么调整参数。我从最早用MATLAB工具箱做拟合到后来手写反向传播、再到用框架搭CNN和RNN踩过的坑比看过的教程多得多。这篇文章不打算复述教科书而是想把我这些年理解神经网络时真正帮我打通任督二脉的那些点按认知顺序重新组织一遍。不管你是刚入门机器学习的学生还是想从传统算法转向深度学习的开发者只要跟着这个思路走应该能少走不少弯路。1. 从“拟合一条曲线”理解神经网络到底在干什么1.1 先忘掉“神经”只看“拟合”很多人被“神经网络”这个名字吓住以为它模拟了人脑的某种神秘机制。其实从工程角度看它就是一个万能函数逼近器。你给它一堆输入输出对它想办法找到一个函数让输入经过这个函数后尽可能接近输出。举个最直观的例子假设你有一组数据x是温度y是某个设备的能耗。你想知道温度和能耗之间的关系。传统做法是假设一个模型比如线性回归 y ax b然后用最小二乘法求出a和b。但如果关系不是线性的呢比如是一条S形曲线线性回归就无能为力了。这时候神经网络的做法是不预设函数形式而是用多个简单的非线性函数叠加起来去逼近那条S形曲线。每个“简单函数”就是一个神经元多个神经元组合起来就能表达非常复杂的形状。我最早用MATLAB的fitnet工具箱做这个事输入一层、隐藏层十个神经元、输出一层几行代码就能拟合出一条相当复杂的曲线。当时最大的感受是它不需要你告诉它曲线长什么样它自己从数据里学。1.2 一个神经元内部到底算了什么单个神经元做的事情用一句话概括加权求和再过一下非线性。假设有三个输入 x1、x2、x3每个输入对应一个权重 w1、w2、w3再加一个偏置 b。神经元先算z w1x1 w2x2 w3*x3 b然后把这个 z 送进一个激活函数比如 Sigmoida 1 / (1 e^(-z))输出的 a 就是这个神经元的输出。为什么要有激活函数如果没有它多个神经元叠加起来仍然是线性的和直接做线性回归没区别。激活函数引入了非线性才让网络有了拟合复杂函数的能力。我见过很多初学者在这里卡住他们能理解加权求和但不理解为什么非要加那个Sigmoid。你可以这样想——加权求和是在画一条直线激活函数是在把这条直线“掰弯”。弯的直线叠加起来就能拼出任意形状。1.3 从单层到多层为什么要“深”单层神经网络也叫感知机只能解决线性可分的问题。什么叫线性可分就是在二维平面上能用一条直线把两类点分开。如果两类点混在一起像棋盘格那样交替分布一条直线怎么也分不开。多层网络的做法是第一层先把空间做一次变换把原本混在一起的点映射到一个新的空间里在新空间里它们就变得线性可分了。第二层再在这个新空间里画一条直线完成分类。这就像你在一张揉皱的纸上画线分不开两种颜色但如果你把纸重新铺平可能一条直线就分开了。每一层网络都在做一次“重新铺平”的操作。深度的意义就在这里层数越多能做的空间变换就越复杂能表达的函数就越复杂。但也不是越深越好后面会讲到梯度消失的问题。2. 反向传播神经网络真正“学习”的那一步2.1 损失函数怎么衡量“现在有多差”网络初始化时权重是随机的输出肯定一塌糊涂。我们需要一个指标来衡量当前输出和真实值差多少这个指标就是损失函数。回归问题常用均方误差L (1/n) * Σ(y_pred - y_true)^2分类问题常用交叉熵L -Σ y_true * log(y_pred)损失函数的值越大说明网络当前的表现越差。训练的目标就是找到一组权重让损失函数的值最小。2.2 梯度下降往哪个方向调权重知道了“有多差”下一步是知道“往哪个方向调能变好”。这个方向就是梯度的反方向。想象你站在一座山上蒙着眼睛想走到山谷最低点。你能做的就是用脚感受周围哪个方向最陡然后往那个方向迈一步。梯度就是“最陡的方向”梯度下降就是“沿着最陡方向往下走”。权重的更新公式w_new w_old - learning_rate * (∂L/∂w)learning_rate 是学习率控制每一步迈多大。迈太小走得太慢迈太大可能直接跨过山谷跑到对面山上去了。2.3 链式法则反向传播的数学核心问题来了损失函数 L 和最后一层的权重直接相关但和前面几层的权重关系是间接的。怎么求前面那些权重的梯度答案是链式法则。从输出层开始把梯度一层一层往前传每传一层就乘上当前层的局部梯度。这就是“反向传播”名字的由来。具体来说假设网络有三层输出层的梯度可以直接算然后传到第二层再传到第一层。每一层只需要知道“从后面传过来的梯度”和“自己这一层的局部导数”就能算出自己权重的梯度。我手写过一次反向传播的代码最大的体会是公式看着吓人但代码写出来其实很短。核心就是一个循环从最后一层往前遍历每次做两件事——算当前层权重的梯度然后把梯度传给前一层。2.4 学习率、批量大小、迭代次数三个最常调的参数这三个参数几乎决定了训练能不能收敛、收敛得快不快。学习率最敏感的参数。太大容易震荡不收敛太小收敛太慢。实践中常用0.01、0.001、0.0001这几个量级。如果训练时损失忽上忽下大概率是学习率太大了。批量大小每次用多少样本算梯度。全批量所有样本一起算稳定但慢随机梯度每次一个样本快但震荡大。实践中常用mini-batch比如32、64、128。批量大小还影响内存占用太大可能爆显存。迭代次数整个数据集过多少遍。太少欠拟合太多过拟合。通常配合早停法——验证集损失不再下降就停。我踩过的一个坑一开始用全批量训练损失下降很平滑但速度慢得让人想砸电脑。后来改成mini-batch速度快了十倍损失曲线虽然有点毛刺但整体趋势一样。不要追求损失曲线的完美平滑能收敛就行。3. 不同网络结构解决不同问题CNN、RNN和它们的适用场景3.1 全连接网络最通用但最“笨”全连接网络就是每一层的每个神经元都和上一层的所有神经元相连。它通用性强理论上能拟合任何函数但有两个致命问题一是参数太多。假设输入是1000x1000的图片展平后是100万个输入第一层隐藏层如果有1000个神经元光这一层就有10亿个参数。训练不动也存不下。二是丢失空间信息。图片展平后相邻像素的关系就没了。而图片的很多特征恰恰是局部的——边缘、纹理都是相邻像素的组合。所以处理图像时全连接网络不是好选择。3.2 卷积神经网络为图像而生CNN的核心思想是局部连接和权值共享。局部连接每个神经元只看输入的一个小区域比如3x3或5x5的窗口。这符合图像的局部相关性——相邻像素才有关系。权值共享同一个卷积核在整个图像上滑动所有位置共用同一组权重。这大大减少了参数数量也让网络具有平移不变性——猫在图片左上角还是右下角都能被识别出来。一个典型的CNN结构是卷积层 → 激活层 → 池化层 → 卷积层 → 激活层 → 池化层 → 全连接层 → 输出。卷积层负责提取特征浅层提取边缘、颜色深层提取纹理、形状。池化层负责降维减少计算量同时增强鲁棒性。全连接层负责最后的分类。我最早用CNN做手写数字识别MNIST数据集两层卷积加两层全连接准确率轻松到99%。但换成真实场景的图片准确率就掉得厉害。MNIST太干净了真实图片有光照、角度、遮挡各种问题需要数据增强和更深的网络。3.3 循环神经网络处理序列数据CNN处理的是空间数据RNN处理的是时间序列。文本、语音、股票价格这些数据的特点是有先后顺序后面的内容依赖前面的内容。RNN的做法是每个时间步的隐藏层状态不仅取决于当前输入还取决于上一个时间步的隐藏层状态。这样网络就有了“记忆”。但普通RNN有个问题序列太长时早期的信息传不到后面梯度消失或爆炸。所以实践中常用LSTM或GRU它们通过门控机制选择性地记住或遗忘信息。我做过一个文本情感分类的项目用LSTM比普通RNN准确率高了好几个百分点。如果你的序列长度超过20直接上LSTM或GRU别在普通RNN上浪费时间。3.4 选型对照表数据类型推荐结构原因表格数据全连接网络特征之间没有明显的空间或时间关系图像CNN局部相关性和平移不变性文本/语音RNN/LSTM/GRU序列依赖关系图结构GNN节点之间的连接关系不规则生成任务GAN/VAE需要生成新样本4. 训练一个神经网络时最容易踩的五个坑4.1 数据不归一化训练直接崩这是新手最常犯的错误。输入特征的量纲差异太大比如一个特征是0.001量级另一个是10000量级梯度下降会非常不稳定。解决办法很简单标准化。对每个特征减去均值再除以标准差让所有特征都在相近的范围内。我见过有人用原始像素值0-255直接训练损失死活不下降。改成除以255归一化到0-1之后立刻就开始收敛了。这一步花不了五分钟但不做可能浪费你五天。4.2 过拟合训练集表现完美测试集一塌糊涂过拟合的本质是网络把训练数据的噪声也学进去了导致在新数据上表现差。判断方法很简单训练集损失持续下降但验证集损失先降后升那个拐点就是过拟合开始的点。应对手段有几种增加数据最有效但成本最高数据增强旋转、翻转、裁剪、加噪声正则化L1/L2正则化在损失函数里加权重的惩罚项Dropout训练时随机丢弃一部分神经元强迫网络不依赖特定路径早停验证集损失不再下降就停止训练我通常先上Dropout和早停这两个几乎没成本。如果还过拟合再考虑加数据或加正则化。4.3 梯度消失和梯度爆炸深层网络训练时梯度在反向传播过程中可能越来越小消失或越来越大爆炸。梯度消失的表现是前面几层的权重几乎不更新网络学不到东西。梯度爆炸的表现是损失变成NaN训练直接崩。解决办法梯度消失用ReLU替代Sigmoid用Batch Normalization用残差连接梯度爆炸梯度裁剪把梯度限制在一个范围内我用Sigmoid激活函数搭过一个十层的网络前面几层死活不更新。换成ReLU之后立刻正常了。深层网络默认用ReLU这是血泪教训。4.4 学习率设错一切白搭学习率是训练中最难调的参数。太大不收敛太小收敛慢。一个实用的技巧是学习率预热开始时用很小的学习率慢慢增大然后再慢慢减小。这样既不会一开始就震荡也不会后期收敛太慢。另一个技巧是学习率衰减每过几个epoch就把学习率乘以一个小于1的系数比如0.1。这样后期微调更精细。我现在的习惯是先用一个较大的学习率跑几个epoch看损失曲线。如果震荡就减半如果下降太慢就加倍。不要迷信任何教程给的默认值你的数据和任务不一样。4.5 盲目堆层数以为越深越好深度学习之所以叫“深度”确实是因为深层网络能表达更复杂的函数。但层数不是越多越好。层数增加带来的问题参数增多、计算量增大、梯度消失风险增大、过拟合风险增大。实践中先从简单的结构开始。比如图像分类先试一个三层卷积加两层全连接的网络。如果欠拟合再加深。不要一上来就搭ResNet-152你的数据量可能根本撑不起那么深的网络。5. 从MATLAB到PyTorch我的工具链演进和选型建议5.1 MATLAB入门拟合的好帮手我最早接触神经网络是用MATLAB的神经网络工具箱。它的优点是开箱即用几行代码就能搭一个BP网络做拟合或分类。net feedforwardnet(10); net train(net, x, y); y_pred net(x);三行代码一个前馈神经网络就训练好了。对于教学和小规模实验MATLAB非常友好。但MATLAB的局限也很明显深度学习支持不如Python生态丰富GPU加速配置麻烦部署到生产环境不方便。适合学习和快速验证不适合做工程项目。5.2 PyTorch目前最主流的选择PyTorch现在几乎是深度学习研究和应用的首选框架。它的动态计算图让调试非常方便API设计也很直观。一个最小的全连接网络import torch import torch.nn as nn model nn.Sequential( nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1) ) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) for epoch in range(1000): y_pred model(X) loss criterion(y_pred, y) optimizer.zero_grad() loss.backward() optimizer.step()这段代码包含了训练的所有核心步骤前向传播、算损失、清梯度、反向传播、更新权重。我推荐新手直接从PyTorch入手因为它的文档和社区资源最丰富遇到问题容易找到答案。5.3 工具选型对照工具适合场景不适合场景MATLAB教学、小规模拟合、快速验证大规模深度学习、生产部署PyTorch研究、工程、几乎所有深度学习任务极边缘设备部署TensorFlow/Keras生产部署、移动端需要灵活调试的研究scikit-learn传统机器学习、小数据集深度学习5.4 关于“要不要自己手写反向传播”我的建议是至少手写一次。手写一次反向传播你会真正理解梯度是怎么传的、为什么需要激活函数的导数、为什么会有梯度消失。这些理解在你后面调参和排错时非常有用。但实际项目中用框架就好。框架帮你处理了数值稳定性、GPU加速、自动微分这些繁琐的事情没必要重复造轮子。6. 几个让我豁然开朗的认知转折点6.1 神经网络不是黑箱只是维度太高很多人说神经网络是黑箱不知道它学到了什么。其实不是完全不可解释。对于图像CNN你可以可视化每一层的卷积核浅层看到的是边缘检测器深层看到的是纹理和部件检测器。对于全连接网络你可以看权重的绝对值大小判断哪些输入特征更重要。当然完全解释一个深层网络确实很难但“部分可解释”已经能帮你排查很多问题了。不要因为“黑箱”这个标签就放弃理解它。6.2 泛化能力才是目标不是训练集上的完美表现初学者容易陷入一个误区追求训练集上的损失降到极低。但训练集表现好不代表模型好在新数据上表现好才是真的好。这就是为什么一定要留验证集和测试集。验证集用来调超参数测试集用来最终评估。测试集在训练过程中绝对不能碰碰了就是作弊。我见过有人反复在测试集上调参最后报告了一个漂亮的结果但实际部署时效果一塌糊涂。测试集只用一次这是铁律。6.3 数据质量比模型结构更重要我做过一个对比实验同样的网络结构一份数据做了仔细的清洗和标注另一份数据有噪声和错误标签。前者的准确率比后者高了将近二十个百分点。垃圾进垃圾出。再好的网络也救不了烂数据。花在数据清洗和标注上的时间往往比调模型结构更值得。6.4 从简单模型开始逐步复杂化不要一上来就搭最复杂的网络。先用最简单的模型跑通整个流程确认数据没问题、流程没问题再逐步增加复杂度。我的习惯是先跑一个逻辑回归或单层网络看损失能不能下降。如果能再加深加宽。如果不能说明数据或流程有问题加层也没用。7. 写给想动手的人一个最小可运行的完整示例7.1 任务定义拟合一个非线性函数我们用神经网络拟合 y sin(x) 在 [-π, π] 上的曲线。这是一个回归任务输入是一维的x输出是一维的y。7.2 数据准备import numpy as np import torch import torch.nn as nn x np.linspace(-np.pi, np.pi, 1000).reshape(-1, 1) y np.sin(x) X torch.FloatTensor(x) Y torch.FloatTensor(y)7.3 模型定义model nn.Sequential( nn.Linear(1, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 1) )三层全连接两个隐藏层各64个神经元激活函数用ReLU。7.4 训练循环criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.01) for epoch in range(2000): y_pred model(X) loss criterion(y_pred, Y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 200 0: print(fEpoch {epoch}, Loss: {loss.item():.6f})7.5 结果验证训练完成后把预测值和真实值画在一起应该能看到两条曲线几乎重合。这个例子虽然简单但包含了神经网络训练的完整流程数据准备、模型定义、损失函数、优化器、训练循环、结果验证。把这个流程跑通你就已经入门了。7.6 几个可以立刻尝试的改进把隐藏层神经元数量从64改成16或256看效果变化把学习率从0.01改成0.001或0.1看收敛速度把ReLU换成Sigmoid或Tanh看拟合效果增加或减少层数看欠拟合和过拟合的表现这些实验花不了多少时间但能帮你建立对超参数的直觉。调参的直觉只能通过动手获得看再多教程也没用。8. 关于学习路径的一点个人建议如果你是完全零基础我的建议是先花一周时间把Python和NumPy的基本操作过一遍不需要精通能看懂数组运算就行。然后直接上手PyTorch找一个简单的例子跑通。不要先去啃《深度学习》那本花书那本书适合当参考手册不适合入门。跑通第一个例子之后回头补数学。重点补三块线性代数矩阵乘法、特征值、微积分偏导数、链式法则、概率论条件概率、贝叶斯。不需要学到数学系的深度够理解公式就行。然后就是做项目。找一个你感兴趣的数据集分类也好回归也好从头到尾做一遍。遇到问题就查查不到就问。做三个完整项目比看十本书都管用。最后说一个我自己的体会神经网络这个领域理论更新很快但核心思想其实很稳定。把反向传播、梯度下降、过拟合这几个核心概念真正搞懂后面不管出什么新结构你都能快速理解它在解决什么问题。基础打牢了新东西都是旧东西的组合。
返回列表