# 05 · 神经网络与深度学习入门 > AI生成|2026年10月26日|codebuddy **本篇目标**:从单个神经元讲到今天的大模型骨架。学完你应该能看懂一份 PyTorch 训练代码在干什么,也明白 **CNN / RNN / Transformer 各自解决什么问题、为什么 Transformer 赢了**。 **导航**:[栏目首页](./README.md) | 上一篇:[04 · 无监督学习:聚类与降维](./04-无监督学习-聚类与降维.md) | 下一篇:[06 · 模型评估、调参与端到端实战](./06-模型评估-调参与端到端实战.md) --- ## 一、核心概念 **神经元**(一个做两件事的计算单元): ```text 1. 线性加权: z = w₁x₁ + w₂x₂ + ... + b = wᵀx + b 2. 非线性激活: a = f(z) ← f 是激活函数,去掉它整个网络就退化成线性回归 ``` **为什么必须非线性**:多层线性变换叠加 `W₂(W₁x)` 依然只是一个线性变换 `(W₂W₁)x`。有了非线性,网络才能拟合任意形状的决策边界(万能逼近定理)。 **「深度」的含义**:后面的层在前面层的表示之上继续抽象。图像领域最直观:像素 → 边缘 → 纹理 → 部件 → 物体;NLP 里则是:字 → 词 → 短语 → 语义 → 篇章。 --- ## 二、模型原理 ### 2.1 前馈计算(Forward) 一个 L 层网络就是反复做同一件事: ```text a⁽⁰⁾ = x z⁽ˡ⁾ = W⁽ˡ⁾ a⁽ˡ⁻¹⁾ + b⁽ˡ⁾ a⁽ˡ⁾ = f⁽ˡ⁾(z⁽ˡ⁾) l = 1..L ŷ = a⁽ᴸ⁾ ``` ### 2.2 激活函数怎么选 | 激活 | 公式 | 优点 | 缺点/注意 | | --- | --- | --- | --- | | Sigmoid | `1/(1+e⁻ᶻ)` | 输出天然在 (0,1),可做概率 | **梯度消失**(两端梯度≈0)、输出非零中心 | | Tanh | `(eᶻ−e⁻ᶻ)/(eᶻ+e⁻ᶻ)` | 零中心,收敛更快 | 仍有梯度消失 | | ReLU | `max(0, z)` | 计算极简、**缓解梯度消失**、稀疏激活 | **死亡 ReLU**(负半轴永远为 0) | | LeakyReLU / ELU | `max(αz, z)` | 解决神经元死亡 | 多一个超参 | | GELU | 高斯误差线性单元的平滑近似 | Transformer 的默认选择 | 计算略贵 | | Softmax | `eᶻᵏ / Σeᶻʲ` | 多分类输出层归一化成概率 | 只用在**输出层** | **默认建议**:隐藏层用 ReLU(或 GELU 做 Transformer),二分类输出 1 个单元 + Sigmoid,多分类输出 C 个单元 + Softmax,回归输出 1 个单元**不加激活**。 ### 2.3 损失函数 | 任务 | 损失 | 说明 | | --- | --- | --- | | 回归 | MSE / MAE / Huber | Huber 对异常值更稳 | | 二分类 | BCE / BCEWithLogits | 后者内部做数值稳定的 sigmoid,PyTorch 首选 | | 多分类 | CrossEntropyLoss | 输入是 **logits**(不要先 softmax) | | 分布匹配 | KL 散度 | 蒸馏、VAE、对齐任务 | ### 2.4 反向传播(Backward) 本质就是**链式法则沿计算图往回传梯度**: ```text δ⁽ᴸ⁾ = ∂L/∂a⁽ᴸ⁾ ⊙ f′(z⁽ᴸ⁾) δ⁽ˡ⁾ = (W⁽ˡ⁺¹⁾ᵀ δ⁽ˡ⁺¹⁾) ⊙ f′(z⁽ˡ⁾) ∂L/∂W⁽ˡ⁾ = δ⁽ˡ⁾ a⁽ˡ⁻¹⁾ᵀ ``` 框架(PyTorch/TF)会自动构建计算图帮你算,但你要知道三件事: 1. **梯度是从后往前一层层乘出来的**:激活导数小于 1 时,乘到浅层就趋近 0 → **梯度消失**(这也是 LSTM、残差连接被发明出来的原因)。 2. **每一步都要清空旧梯度**:PyTorch 里必须写 `optimizer.zero_grad()`,否则梯度会累加。 3. **反向传播只在 float 上定义**:标签要是 LongTensor(分类),输入要是 float32。 ### 2.5 优化器与学习率 | 优化器 | 特点 | | --- | --- | | SGD | 朴素但正则化效果好;视觉任务调好后常比 Adam 更泛化 | | SGD + Momentum | 加"惯性",山谷震荡更小 | | RMSProp | 按梯度平方的滑动平均自适应步长 | | **Adam / AdamW** | 动量 + 自适应步长,**默认首选**;AdamW 把权重衰减与梯度更新解耦,常作为 Transformer 默认 | **学习率是最重要的超参数**,没有之一。经验做法: ```text 起点:Adam 用 3e-4,SGD 用 0.1(配 momentum 0.9) 策略:warmup(前几步线性升上去)+ cosine decay(余弦衰减到 0) batch size 放大 k 倍时,学习率通常也放大 k 倍(线性缩放规则,上限约为 sqrt-k 经验修正) ``` ### 2.6 防止过拟合的六板斧 | 手段 | 做法 | 直觉 | | --- | --- | --- | | 权重衰减 (L2) | `optimizer=AdamW(..., weight_decay=1e-4)` | 惩罚大权重,逼网络用更多小权重协作 | | Dropout | `nn.Dropout(p=0.2)` | 随机屏蔽神经元,等价于训练多个子网络并平均 | | BatchNorm / LayerNorm | 归一化中间层激活 | 稳定分布、加速收敛,附带的噪声还能正则化(LN 是 Transformer 标配) | | 数据增强 | 图像翻转裁剪、文本回译 | **最有效**,等于免费增加数据 | | Early Stopping | 验证集指标不再涨就停 | 简单粗暴且永远有效 | | 权重初始化 | Xavier(Tanh) / He(ReLU) | 保证各层激活方差稳定,不然一开局就饱和 | ### 2.7 三大架构概览 #### CNN( convolutional neural network,图像与局部模式) 两个核心先验:**局部性**(像素只与邻居相关)+ **权值共享**(同一个模式在图像各处通用)。 ```text 输出尺寸: out = ⌊(in + 2·padding − kernel) / stride⌋ + 1 典型块: Conv2d → BatchNorm → ReLU → MaxPool (重复堆叠) ``` 经典演进:LeNet → AlexNet → VGG → GoogLeNet → **ResNet**(残差连接 `y = F(x) + x` 解决了深层网络的退化问题,让上千层可训练)→ EfficientNet / ConvNeXt。 #### RNN / LSTM(序列建模) 每一步携带一个隐状态:`hₜ = f(Wₓ xₜ + Wₕ hₜ₋₁)`。LSTM 用**遗忘门 / 输入门 / 输出门**控制信息流,缓解长程遗忘。缺点明确:**串行计算慢、超长序列仍会忘**,如今基本被 Transformer 取代。 #### Transformer(当下的绝对主角) 核心是**自注意力**,让序列里每个位置直接看到所有位置(并行、且路径长度 O(1)): ```text Attention(Q, K, V) = softmax( QKᵀ / √d_k ) · V ``` - **Q/K/V**:由同一输入经三个不同线性变换得到(Query 去问,Key 被匹配,Value 是被取出的内容)。 - **除以 √d_k**:防止点积过大把 softmax 推进饱和区。 - **多头注意力**:多个头并行捕捉不同子空间的关系(语法、指代、位置…)。 - **位置编码**:注意力本身对顺序无感,必须人为注入位置信息(正弦编码 / RoPE)。 - **模块结构**:`MultiHeadAttn → Add&Norm → FFN → Add&Norm`,FFN 通常是两层全连接带 GELU。 **为什么它赢了**:训练可并行(相比 RNN 快一个数量级)、任意距离依赖一步可达、规模上去后能力持续增长 → 催生了 BERT/GPT 的**预训练 + 微调**范式。 ### 2.8 迁移学习与微调 不要从零训练。标准做法: ```text 1. 拿在大数据上预训练好的模型(ResNet / BERT / CLIP) 2. 换掉最后的分类头(head) 3. 冻结主干(backbone),只训练头几个 epoch 4. 解冻部分层,用很小的学习率(1e-5 量级)整体微调 ``` 大模型时代的低成本方案:**LoRA / QLoRA**——冻结原权重,只训练插入的低秩矩阵,显存与存储成本降一到两个数量级。 --- ## 三、实例 ### 3.1 sklearn 的 MLP:先看 neural network 的 ABC 无需装 PyTorch 就能跑,"轻量版神经网络"适合做基线。 ```python import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.neural_network import MLPClassifier from sklearn.metrics import classification_report X, y = load_digits(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) for tag, hidden in [("shallow (32,)", (32,)), ("deep (128,64)", (128, 64)), ("deep+early_stop", (128, 64))]: early = tag.endswith("early_stop") pipe = Pipeline([ ("scaler", StandardScaler()), # 神经网络不做缩放基本学不动 ("mlp", MLPClassifier(hidden_layer_sizes=hidden, max_iter=500, early_stopping=early, n_iter_no_change=10, random_state=42)), ]) pipe.fit(X_train, y_train) print("{:16s} train={:.4f} test={:.4f} iters={}".format( tag, pipe.score(X_train, y_train), pipe.score(X_test, y_test), pipe.named_steps["mlp"].n_iter_)) # 损失曲线就是深度学习版的"学习曲线" mlp = MLPClassifier(hidden_layer_sizes=(128, 64), max_iter=300, random_state=42) mlp.fit(StandardScaler().fit_transform(X_train), y_train) plt.plot(mlp.loss_curve_) plt.xlabel("iterations"); plt.ylabel("loss"); plt.title("training loss"); plt.show() best = Pipeline([("scaler", StandardScaler()), ("mlp", MLPClassifier(hidden_layer_sizes=(128, 64), max_iter=500, early_stopping=True, random_state=42))]) best.fit(X_train, y_train) print(classification_report(y_test, best.predict(X_test))) ``` ### 3.2 PyTorch 训练循环模板(二分类,月牙数据) 先安装:`python3 -m pip install -U torch`(这是本篇唯一需要额外安装的依赖)。 ```python import numpy as np import torch import torch.nn as nn from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler torch.manual_seed(42) # 可复现:CPU / CUDA 都要设 np.random.seed(42) # 1) 数据 X, y = make_moons(n_samples=2000, noise=0.2, random_state=42) X = StandardScaler().fit_transform(X).astype("float32") X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.25, stratify=y, random_state=42) t_tr = torch.from_numpy(X_tr), torch.from_numpy(y_tr.astype("float32").reshape(-1, 1)) t_va = torch.from_numpy(X_va), torch.from_numpy(y_va.astype("float32").reshape(-1, 1)) # 2) 模型:三层全连接,隐藏层用 ReLU model = nn.Sequential( nn.Linear(2, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 1), # 输出 logits,不加 Sigmoid ) # 3) 损失与优化器 criterion = nn.BCEWithLogitsLoss() # 数值稳定版 BCE optimizer = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-4) # 4) 训练循环(深度学习的标准骨架,背下来) BATCH, EPOCHS = 128, 200 hist = {"train": [], "val": []} for epoch in range(1, EPOCHS + 1): model.train() perm = torch.randperm(len(t_tr[0])) for idx in torch.split(perm, BATCH): xb, yb = t_tr[0][idx], t_tr[1][idx] optimizer.zero_grad() # 不清零梯度就会累加 loss = criterion(model(xb), yb) loss.backward() # 反向传播 optimizer.step() # 参数更新 model.eval() with torch.no_grad(): # 评估不算梯度,省显存 tr_loss = criterion(model(t_tr[0]), t_tr[1]).item() va_loss = criterion(model(t_va[0]), t_va[1]).item() va_acc = ((model(t_va[0]) > 0).float() == t_va[1]).float().mean().item() hist["train"].append(tr_loss); hist["val"].append(va_loss) if epoch % 20 == 0 or epoch == 1: print("epoch {:3d} train={:.4f} val={:.4f} val_acc={:.3f}".format( epoch, tr_loss, va_loss, va_acc)) ``` **这段的各个部分对应什么**:`model.train()/eval()` 切换 Dropout 与 BN 的行为;`no_grad()` 关掉计算图;`zero_grad → backward → step` 是每次更新的三步,改动任何深度学习代码基本都在这三步上做文章。 ### 3.3 PyTorch 小 CNN:手写数字(离线可用) 不去下载 MNIST,直接把 scikit-learn 自带的 digits 重排��成 8×8 单通道图,一样能演示卷积。 ```python import numpy as np import torch import torch.nn as nn from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split torch.manual_seed(42) data = load_digits() X = data.data.astype("float32") / 16.0 - 1.0 # 灰度值 0~16 → −1~1 X = X.reshape(-1, 1, 8, 8) y = data.target.astype("int64") X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42) tx, ty = torch.from_numpy(X_tr), torch.from_numpy(y_tr) vx, vy = torch.from_numpy(X_va), torch.from_numpy(y_va) class SmallCNN(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 16, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 8x8 → 4x4 nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 4x4 → 2x2 ) self.head = nn.Sequential( nn.Flatten(), nn.Linear(32 * 2 * 2, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 10), # 10 类,输出 logits ) def forward(self, x): return self.head(self.features(x)) model = SmallCNN() opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-4) crit = nn.CrossEntropyLoss() # 多分类标配,输入是 logits for epoch in range(1, 61): model.train() for idx in torch.split(torch.randperm(len(tx)), 128): opt.zero_grad() loss = crit(model(tx[idx]), ty[idx]) # 注意:不要再对输出做 softmax loss.backward(); opt.step() if epoch % 10 == 0: model.eval() with torch.no_grad(): acc = (model(vx).argmax(1) == vy).float().mean().item() print("epoch {:3d} loss={:.4f} val_acc={:.4f}".format(epoch, loss.item(), acc)) ``` ### 3.4 迁移学习的最小写法(需要 torchvision 且会下载权重) ```python import torch, torchvision from torch import nn model = torchvision.models.resnet18(weights="IMAGENET1K_V1") for p in model.parameters(): # 1) 冻结预训练主干 p.requires_grad = False model.fc = nn.Linear(model.fc.in_features, 10) # 2) 换成自己的分类头 opt = torch.optim.AdamW(model.fc.parameters(), lr=1e-3) # 3) 只训练新加的层 # 训练若干轮后,可解冻 model.layer4,用 lr=1e-5 整体微调 ``` --- ## 四、常见坑与调参要点 | 坑 | 现象 | 解决 | | --- | --- | --- | | 输入没缩放 | loss 不降、梯度爆炸 | 标准化 / 归一化到 [0,1] 或 [−1,1] | | 学习率不合适 | loss 震荡(太大)或几乎不动(太小) | 从小开始试 `3e-4`(Adam);用 lr finder 或 warmup+cosine | | 多分类又做了一次 softmax | 训练不收敛 | `CrossEntropyLoss` 已经内置,直接喂 logits | | 回归输出加了激活 | 预测值被压到固定区间 | 输出层保持线性 | | 标签类型不对 | `RuntimeError` | 分类标签用 `int64`,输入用 `float32` | | 忘了 `zero_grad()` | 梯度累加、训练发散 | 每次 step 前必写 | | 过拟合 | train loss 降、val loss 反升 | 早停 + Dropout + weight_decay + 数据增强 | | 结果不可复现 | 每次跑分不一样 | `torch.manual_seed`,必要时 `cudnn.deterministic=True` | | 类别不平衡 | 模型只预测多数类 | 损失里加权重 `nn.CrossEntropyLoss(weight=...)` | **调参顺序建议**: ```text 1. 先让模型在少量样本上过拟合(几步训练到 train acc=1.0)——验证代码没写错 2. 加正则找回泛化(Dropout / weight_decay / 增强 / 早停) 3. 调容量(宽度/深度)与学习率,通常只需要这两步就能拿到八成收益 4. 最后才是换架构、用预训练权重 ``` --- ## 五、小结 - 神经网络 = 多层"线性加非线性",靠反向传播的链式法则更新参数;**学习率是最关键的旋钮**。 - **CNN** 用局部性+权值共享搞定网格数据(图像、语音、棋盘);**RNN** 处理序列但受限于串行;**Transformer** 用自注意力实现了并行与任意距离依赖,成为大模型的统一骨架。 - 现在的默认起手式不是"从头训练",而是**拿预训练权重做迁移/微调**,数据少时还可以冻结主干或上 LoRA。 - 真正决定效果的往往是**数据质量与正则化策略**,而不是网络结构的微小改动。 下一篇把前三篇的技能串成一套完整工作流:[06 · 模型评估、调参与端到端实战](./06-模型评估-调参与端到端实战.md)。