先聊点实际的。我当年入门神经网络的时候,最大的误区就是以为"基础"等于"把数学公式推导清楚"。书买了一堆,《深度学习》那本花书在书架上接了半年的灰,每次拿起都从"泛函"和"概率论"开始补,然后在一章还没看完的情况下放弃。后来真正让我理解神经网络的,反而是先写完一个能跑、能收敛的代码,再回头去啃原理。所以这篇"神经网络与深度学习基础",我不想写成教材的压缩版,而是想以"过来人给自己做复盘"的方式,把真正该建立的知识框架、该避开的坑、以及第一次实作出一个模型时最需要的细节,一次讲清楚。
这篇文章适合两类人:一是已经开始上课或看书,但总觉得"每个公式都认识,连起来就不知道在干嘛"的初学者;二是想系统梳理一遍基础,准备做实际项目的开发者。你可以把这里的内容当成一张地图——先知道整片森林长什么样,再精确到某一棵树的枝干,最后亲手种一棵属于自己的树。
1. 入门路线:先把"神经网络能干什么"的体感建立起来
1.1 先跑起来,再搞懂原理
我见过太多人卡死在入门第一步,原因是把顺序搞反了。神经网络和传统程序最大的不同在于:写普通代码时,你知道每一步在做什么;而神经网络更像一个"黑箱系统",你定义它的结构、给它数据,它自己调整内部参数来逼近目标。如果你一开始就试图理解黑箱里所有细节,很容易被反向传播、梯度下降这些概念劝退。
我的建议是先跑通一个现成的小项目。比如用 PyTorch 或 TensorFlow 加载官方示例里的手写数字识别模型,训练几个 epoch,看到测试准确率从 0.1 涨到 0.9 以上。这个过程通常只需十几分钟,但它在你的大脑里建立了一个关键认知:神经网络是一个通过数据驱动、自动调整参数的函数近似器。有了这个体感,再回来看前向传播、反向传播,你会发现每个概念都有了具体的落点。
这样做的另一个好处是能提前暴露环境问题。很多人学到后面才发现自己的电脑训练一个稍大的模型要跑到天荒地老,中途被迫换设备、换框架,浪费大量时间。先跑通一个小项目,等于提前把"能不能训练"这件事验证了,后面学原理的心态会完全不同。
1.2 环境准备:一台能训练模型的电脑需要什么
关于环境,先说结论:学基础阶段,CPU 完全够用。一个几层的小型神经网络在 CPU 上训练 MNIST,单 epoch 也就十几秒;哪怕是 LeNet 这种经典卷积网络,CPU 跑完整训练流程也就是一顿饭的功夫。所以完全不用为了"学基础"就花大价钱买显卡。
不过一旦进入 CNN、LSTM、Transformer 这些模型的实战,GPU 的价值就凸显出来了。一个在小数据集上需要跑 10 分钟的模型,换 GPU 可能只需要十几秒。我的建议是分步走:先用 CPU 把代码逻辑跑通,确认无误后再切到 GPU 环境跑完整训练。这样既控制了成本,也避免了"调试一次等半小时"的痛苦。
如果你没有独立显卡,也不用愁。现在很多云平台提供了免费或低成本的 GPU 资源,比如 Colab 的免费 GPU、各类科研云的共享实例。我自己实践下来,这类平台用来跑基础课程作业和中小型项目完全够用。另外,系统选择上,Windows、macOS、Linux 都可以,但如果你后续要长期做深度学习,Ubuntu 系会省去很多兼容性麻烦——很多底层库和训练工具的首选支持平台都是 Linux。Windows 上如果遇到驱动问题,可以优先检查 GPU 驱动和 CUDA 版本是否匹配,这个坑踩的人最多。
1.3 资料选择:哪些教程和书籍值得反复读
市面上深度学习资料多到让人选择困难。以我自己的经验,最值得当"主粮"的有三类。
第一类是视频课程,典型案例是吴恩达的《深度学习专项课程》。这套课程的优点是"大局观"很好,把为什么用神经网络、各模型适用场景讲得很透。适合在地铁上、通勤时看,建立整体框架。第二类是实践导向的书籍,比如"鱼书"《深度学习入门:基于Python的理论与实现》。这本书用极少的数学和极清晰的代码带你从零实现一个神经网络,我建议把书里的代码自己敲一遍,敲完之后你对前向传播、反向传播的认知会非常扎实。第三类是深度学习 Atlas 级别的经典教材《深度学习》(花书),这本书更适合当工具书查阅,初读不必逐页啃。
这里我想强调一个原则:资料不在于多,而在于形成"输入-实践-输出"闭环。很多人收藏了几十G 的资料,却始终在一门课的第三讲徘徊。有效的做法是:选定一个资料,跟着它完成一个小项目,写完一篇笔记,然后才进入下一个资料。
2. 三个核心机制拆解:前向传播、损失函数、反向传播
2.1 前向传播:数据在网络里的流动路径
如果把神经网络想象成一个"信息加工流水线",前向传播就是原料从入口流到出口的过程。每一层做的事其实就两件:先做线性变换,再做非线性变换。
线性变换就是我们熟悉的 y = wx + b。其中 w 是权重矩阵,决定了上一层每个神经元对下一层某个神经元的"影响力";b 是偏置,相当于给结果加一个可学习的偏移。非线性变换则来自激活函数,比如 ReLU(max(0, x))、Sigmoid、Tanh 等。为什么必须有这一步?因为如果没有非线性,无论网络堆多深,整体依然是线性变换,那和单层网络没有本质区别,根本拟合不了复杂模式。
用一个小例子来说明。假设输入是一个 2 维向量 x = [x1, x2],第一个隐藏层有 3 个神经元,那么这一层的权重 w 就是一个 3×2 的矩阵,偏置 b 是一个 3 维向量。前向传播就是计算 z = wx + b,再套上激活函数 a = ReLU(z)。然后 a 作为下一层的输入继续传下去。最后一层通常不加激活函数或加 softmax,用来输出预测值或类别概率分布。
理解了这一步,你就明白为什么神经网络能处理各种各样的输入了——图像可以拉平成向量,文本可以映射成词向量,表格数据可以直接作为特征向量。前向传播本身不关心数据的语义,只关心矩阵维度是否匹配。这个"维度匹配"虽然是基础中的基础,但实际操作中闹出的错误非常常见,后面实战部分我会专门提到。
2.2 损失函数:量化"错了多少"的尺子
有了预测输出,怎么知道模型好不好?这就需要损失函数。可以把它理解成一把尺子,量出"预测值离真实值差多远",然后用这个数字指导模型调整。
回归问题最常用的是均方误差(MSE),公式是预测值和真实值差的平方再取平均。平方的作用是放大较大误差,同时让损失函数可导,便于后续梯度计算。分类问题则更常用交叉熵损失。这里我想多解释一点:为什么分类不用 MSE?因为分类任务的输出通常是经过 softmax 的概率分布,MSE 在概率输出上的梯度特性不好,收敛慢;而交叉熵对"错误分类"的惩罚力度更大,梯度更陡,模型学得更快。这个差异在你做大项目时感受会特别明显——用错损失函数,模型可能一直停滞在一个很不理想的准确率上。
交叉熵还有一个常见搭档是 softmax。Softmax 把网络最后一层的原始分值(logits)转换成一个概率分布,所有类别概率和为 1。实际编码时,PyTorch 这类框架会直接提供CrossEntropyLoss,它内部已经融合了 softmax 和交叉熵,不需要你手动分开做。这个设计是为了数值稳定性——分开计算时如果 logits 里有一个特别大的数,指数运算可能溢出,而融合版本会做平移处理,避免这个问题。
2.3 反向传播:神经网络"学习"的根基
反向传播是整个神经网络训练机制里最精华的部分。它的核心思想其实就一句话:利用链式法则,从输出层开始反向计算损失函数对每一层参数的梯度,然后沿梯度负方向更新参数。
打个比方。你站在山上,想走到山脚,但雾很大看不清路。你能做的就是把脚往各个方向伸一下,感受哪个方向是下坡,然后向下坡方向迈一步。反向传播就是高效地计算"哪个方向是下坡"的方法。
具体来说,训练一个样本时,先做一次前向传播得到预测值和损失,然后计算损失对输出层输入的梯度;再利用链式法则,把梯度逐层传回前面每一层,得到损失对所有权重 w 和偏置 b 的梯度。最后用梯度下降更新:w = w - 学习率 × 梯度。这个"前向传播算误差,反向传播算梯度,梯度下降更新参数"的三步循环,就是深度学习训练的全部核心。
反向传播真正容易出问题的地方在于梯度消失和梯度爆炸。网络层数很深时,梯度在逐层回传中会不断相乘。如果每层梯度的模都小于 1,浅层梯度就会指数级缩小,导致浅层参数几乎不更新,这就是梯度消失;反过来说,如果梯度模大于 1,浅层梯度会指数级膨胀,训练直接发散,这就是梯度爆炸。深度网络早期难以训练,主要就是这个问题。解决方法包括:使用 ReLU 这类不易饱和的激活函数、合理的权重初始化(比如 Xavier 和 He 初始化)、Batch Normalization、以及后面要讲到的残差连接。
3. 网络模型家族快速识别:从MLP到CNN、RNN与Transformer
3.1 全连接神经网络:最朴素但最重要
全连接神经网络,也叫多层感知机(MLP)或前馈神经网络,就是最基础的那类网络。每一层的每个神经元都与下一层所有神经元相连,层与层之间没有循环。这就是为什么叫"前馈"——信号只向前流动,不回头。
它的价值有两个:一是作为理解一切复杂模型的基础,像 CNN、RNN、Transformer 在组件层面都会用到全连接层;二是它本身在中小型表格数据上依然有不错的实用性。我见过不少刚入门的人急着学 CNN、LSTM,结果连一个简单的 MLP 都调不好,这是本末倒置。先把全连接网络在一个小数据集上训练到收敛,亲手调几轮学习率和隐藏层大小,你对神经网络的理解会远超直接套一个大模型。
3.2 卷积神经网络:图像任务的默认选择
卷积神经网络(CNN)的诞生是为了解决图像数据带来的挑战。一张 224×224 的彩色图片,拉成向量就是 15 万个像素值,如果直接用全连接层处理,第一层的参数量就大到可怕,而且会丢失图片的二维空间结构。
CNN 的核心武器是卷积核。一个卷积核是一个小窗口(比如 3×3),它在图片上滑动,每个位置做一个局部加权求和,相当于提取一种局部特征。不同卷积核提取不同特征——边缘、角点、纹理等。卷积层的另一个关键是权值共享:同一个卷积核在整张图上共用同一组权重,这使参数量大幅下降。池化层则负责压缩空间尺寸,保留主要特征同时减少后续计算量。最后再接全连接层或全局池化输出分类结果。
这些年 CNN 也有了很多变体,比如经典结构的堆叠、一维卷积用于序列处理、深度可分离卷积用于移动端轻量模型。热搜词里提到的"一维卷积神经网络"就是把卷积核从二维窗口变成一维窗口,在文本、音频、心电图这类序列数据上也能用。如果你做图像相关的项目,CNN 几乎是绕不开的基础设施。
3.3 循环神经网络与LSTM:处理序列数据的尝试
处理文本、语音这类"有先后顺序"的数据时,全连接和卷积都不太合适。原因很简单:序列中后面的内容依赖前面的内容,而全连接网络没有"记忆"。
循环神经网络(RNN)的解决思路是:让网络在读取序列每个元素时,额外接收一个来自上一个时间步的"隐状态"。这个隐状态就像一种短期记忆,携带了之前看到过的信息。结构上,"每个时间步共享同一组权重"是 RNN 的重要特征,这让模型可以处理任意长度的序列,也大大减少了参数量。
但普通 RNN 有个老大难问题:当序列较长时,反向传播经过的梯度路径很长,容易出现梯度消失,导致网络记不住太久之前的信息。LSTM(长短期记忆网络)通过引入"门控"机制来解决。简单说,LSTM 内部有一个细胞状态,可以让信息在不被干扰的情况下长时间流动;输入门决定新信息写入多少,遗忘门决定旧信息丢掉多少,输出门决定输出多少。这些门控让模型能够选择性记忆上下文。后来大量任务转向了注意力机制和 Transformer,但你要读文献、看旧模型或处理时序预测任务,LSTM 依然是常客。
3.4 注意力机制与Transformer:现代大模型的底层支柱
提到深度学习基础,现在必须提到注意力机制,因为当前最火的语言模型、多模态模型几乎都在用 Transformer 体系。注意力机制可以理解成给每个输入词分配"关注度":预测某个位置时,模型会自动判断输入序列中哪些位置和自己最相关,并加权聚合它们的信息。
Transformer 的核心是自注意力机制,它把序列中的每个元素与其他所有元素两两计算相关性,一次性捕捉全局依赖。和 RNN 相比,Transformer 有两个明显优势:一是并行度高,因为所有位置的计算可以同时进行,不需要按时间顺序逐个处理;二是长距离依赖捕捉能力强,不会因为序列太长而丢失早期信息。
图像领域用 Transformer 的做法也相当成熟了,比如把图像切分成小块(patch)再当作序列处理,就是 ViT 的基本思路。所以今天学基础,绕开注意力是不行的。不过也请你放心,理解自注意力不需要特别高深的数学,抓住"Query、Key、Value 三者交互"这个机制,后面读论文会顺畅很多。
4. 训练中真正决定成败的细节:优化器、正则化与诊断
4.1 优化器选择:SGD、Adam 与 AdamW
没有优化器,损失函数算出来的梯度就无从落地。优化器的职责是决定"怎么沿着梯度更新参数"。
最朴素的随机梯度下降(SGD)每次迭代用当前梯度直接更新参数,实现简单,但收敛往往较慢,且容易在小批量梯度的噪声里震荡。动量(Momentum)的引入解决了这个问题:它像给小球加了惯性,让更新方向不仅看当前梯度,还考虑历史累计方向,从而加速收敛、减小震荡。
Adam 则是在此基础上进一步发展,它自适应地为每个参数计算不同的学习率。近些年很多实际项目都默认选择 Adam 或它的改进版 AdamW。AdamW 在 Adam 的基础上把权重衰减(L2 正则化)的实现方式做了修正,使正则化与梯度更新解耦,效果通常更稳。我实践中有一个建议:大部分小规模任务直接用 AdamW 能快速收敛;如果你追求极致的泛化性能且样本量不算太小,不妨试试 SGD + Momentum,最后结果有时好于 Adam。两者的选择不是"谁先进"的问题,而是任务和数据特性的适配问题。
4.2 正则化:防止模型"死记硬背"
模型参数量一大,很容易过拟合——在训练集上表现很好,换到新数据就拉胯。正则化的本质是给模型加点"约束",让它不要过度依赖某些特征或参数。
最常见的几种手段:
- L2 正则化(权重衰减):在损失函数中加入所有权重平方和的惩罚项,逼迫权重保持较小值,从而提升泛化能力。在 PyTorch 里,直接在优化器中设置
weight_decay参数即可,例如torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)。L1 正则化则让权重变得稀疏,适合特征选择场景,但训练上不如 L2 稳定。 - Dropout:训练时随机让一部分神经元"失活"(输出置零),迫使网络不能依赖单个神经元,相当于训练了多个子网络的集成效果。注意 Dropout 只在训练时启用,测试时要关闭。
- 早停:监控验证集指标,一旦连续多个 epoch 不再提升就停止训练。这是最简单有效但常被忽略的手段,尤其适合训练时间昂贵的场景。
- 数据增强:对图像做随机裁剪、翻转、颜色扰动等,等于凭空扩展了训练数据,让模型见到更多变化,在视觉任务里几乎是标配。
4.3 诊断模型训练状态:从损失曲线到调参决策
模型不收敛的时候,很多人第一反应是"再改改网络结构",但更有效的做法是看训练曲线,用曲线做诊断。
一次正常的训练通常表现为:训练损失稳步下降,验证损失也随之下降,最终都趋于平稳。如果你看到训练损失下降得很好、验证损失却在某个节点开始反弹,说明模型开始过拟合,应加强正则化或增加数据量。如果训练损失和验证损失都居高不下,可能是模型表达能力不足、学习率不合适、或数据本身有问题。如果训练损失震荡剧烈且迟迟不下降,第一优先检查的往往是学习率——梯度过大或步子迈得太大都会造成震荡。一种常用做法是将学习率按数量级搜索,比如从 1e-2、1e-3、1e-4 里试,看初始损失能否稳定下降。
可视化工具方面,TensorBoard 是最常用的选择,可以实时画损失曲线、学习率曲线和梯度分布。我自己用下来的体会是:梯度分布图特别有用——如果某层梯度始终接近零,说明该层可能没在学;如果梯度出现 NaN 或剧烈抖动,基本可以怀疑学习率过大或数据有异常值。
5. 一个完整实例:用PyTorch从零搭出可收敛的识别模型
5.1 数据准备与模型定义
理论讲再多,不如写一个能跑的模型。我用 PyTorch 在 MNIST 手写数字数据集上搭一个最简 MLP,并用它演示整个训练流程。MNIST 是 28×28 的灰度图,总共 10 个类别,数据量小、训练快,非常适合复现。
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader # 数据预处理:将像素值归一化到 [0,1] transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False) class SimpleMLP(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Flatten(), # (B, 1, 28, 28) -> (B, 784) nn.Linear(28*28, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10) # 输出10个类别的logits ) def forward(self, x): return self.net(x) model = SimpleMLP()这里有两个细节值得留意。
第一,输入为什么要 Flatten?因为全连接层只能接收二维输入(batch, feature_num),而图像数据是四维(batch, channels, height, width),不展平的话线性层的维度会把nn.Linear直接搞错。这个报错信息在初学者里出现频率极高。
第二,为什么最后一层不加激活函数?因为后面用CrossEntropyLoss,它内部会做 softmax 操作,如再加激活反而多余,甚至会导致数值不稳定。这也是典型新手坑。
5.2 训练循环、损失与评估
接下来定义损失函数和优化器,并进行训练。为了演示方便,我把训练循环写在一个简单的函数里,方便你观察每个 epoch 的损失变化。
criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-5) def train_one_epoch(): model.train() total_loss = 0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) return total_loss / len(train_loader.dataset) def evaluate(): model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total for epoch in range(5): train_loss = train_one_epoch() acc = evaluate() print(f"Epoch {epoch+1}, Loss: {train_loss:.4f}, Test Acc: {acc:.4f}")跑 5 个 epoch,最终测试准确率通常能到 97% 以上。请注意训练循环里三个关键行:optimizer.zero_grad()清空上一次迭代保存的梯度——不清空会导致梯度累积,这是新手最容易踩的坑;loss.backward()完成反向传播,自动计算所有可学习参数的梯度;optimizer.step()用优化器更新参数。这三个操作顺序固定,缺一不可。model.train()和model.eval()则负责切换训练/测试模式,对包含 Dropout、BatchNorm 的模型尤其重要——如果忘记切回eval,预测结果可能会带着随机的干扰,准确率波动很大。
5.3 常见问题与从MNIST到真实项目的扩展
用 MNIST 搭一遍流程后,可以顺手验证几个常见的"故障模式"。比如把学习率从 1e-3 调到 1e-1,训练损失大概率会在前几个 step 变成 NaN;把归一化去掉,准确率会明显下降;如果把中间层宽度从 128 缩到 8,模型的表达能力不足,准确率会跌到 90% 左右。自己亲手触发并观察这些问题,比死记任何调参口诀都管用。
做完 MNIST,往真实项目扩展时,有几个点需要立刻注意。真实数据的维度更复杂,做分类时要考虑类别不平衡问题;图像不再居中、数据量更大,数据增强和预训练模型迁移学习会成为标配;文本或时间序列数据则需要引入嵌入层、LSTM 或 Transformer。但所有复杂的模型,底层依然是"前向传播算损失、反向传播算梯度、优化器更新参数"这一条主线。
最后再分享一个小技巧:开始一个新任务时,先不要急着搭大模型,而是用一个小模型加少量数据,确保整个训练闭环能跑通、损失能下降,然后再逐步增加模型复杂度和数据量。这种"从最小可行闭环起步"的做法,能让 90% 的流程性问题在浪费大量算力之前暴露出来。我这些年处理过的训练故障里,有很大一部分最终都指向一个非常基础的原因——输入数据格式不对、标签没对齐、梯度没清零。把基础环节扎扎实实检查一遍,深度学习的实战之路才能走得更远。