1. 项目概述:为什么我们需要“硬啃”机器学习基础?

最近在整理硬盘,翻到了几年前刚开始学机器学习时做的笔记,标题就叫“硬啃:《Deep Learning》机器学习基础篇(三)”。看到这个标题,我自己都笑了,那个“硬啃”二字,真是道尽了当时的状态——面对Ian Goodfellow那本近800页的“花书”,尤其是前面几章关于概率论、信息论和数值计算的数学基础,感觉就像在啃一块没煮烂的牛肉,费劲,但啃下来又特别有滋味。我相信很多朋友,无论是刚入门的学生,还是想转行的工程师,看到“机器学习”、“深度学习”这些词,第一反应可能是去学框架、调模型,结果一上来就被反向传播、梯度下降里的各种数学符号劝退,最后要么放弃,要么就成了只会调包的“调参侠”。

所以,我想借着复盘这篇旧笔记的机会,和大家聊聊“硬啃”的必要性。 机器学习,尤其是深度学习,其核心魅力不在于你会用PyTorch或TensorFlow写几行代码,而在于你能否理解模型背后的“为什么” 。比如,为什么用交叉熵而不用均方误差做分类损失?为什么ReLU能缓解梯度消失?为什么Batch Normalization要那样计算?这些问题的答案,都藏在《Deep Learning》这类经典教材的前几章里。我当时的“硬啃”,就是强迫自己把那些看似枯燥的数学公式,和实际训练中遇到的“模型不收敛”、“过拟合”、“梯度爆炸”等问题一一对应起来。这个过程痛苦,但它是把“知识”变成“直觉”的关键一步。这篇内容,就是把我当年“硬啃”第三章(通常涉及概率与信息论基础)的心得、踩过的坑,以及如何把这些理论应用到实际问题的技巧,重新梳理分享出来。无论你是正在入门,还是感觉基础不牢想回炉,希望这些“硬啃”出来的经验能帮你把路走得更扎实。

2. 核心理论拆解:概率与信息论——模型世界的语言

《Deep Learning》第三章,标题通常是“Probability and Information Theory”。很多读者可能会跳过,觉得这是数学课内容,和编程无关。但恰恰相反,这一章是理解后续所有模型的“语法”。机器学习本质上是 从数据中学习概率分布 的过程。我们所有的模型、损失函数、评估指标,都构建在这个基础之上。

2.1 概率论:不确定性下的决策框架

为什么机器学习需要概率?因为现实世界的数据充满了噪声和不确定性。一张图片被识别为“猫”,我们通常不是说“这绝对是猫”,而是说“根据模型,它是猫的概率为92%”。

2.1.1 核心概念关联实战

  • 随机变量与分布 :这不仅仅是数学定义。在代码里,你加载的每一批(batch)训练数据,都可以看作是从某个复杂分布(比如ImageNet的图片分布)中采样出的一组随机变量。理解这一点,就能明白为什么数据增强(如随机裁剪、翻转)是有效的——它是在合理范围内对数据分布进行扰动,增加了采样样本,让模型学到的分布更鲁棒。
  • 条件概率 P(y|x) :这是监督学习的核心建模对象。无论是逻辑回归还是深度神经网络,我们最终想得到的就是给定输入x,输出为某个类别y的概率。 反向传播算法可以看作是在利用链式法则,高效地计算和优化这个条件概率模型的参数
  • 贝叶斯规则 P(model|data) ∝ P(data|model) * P(model) 。这个公式是理解正则化(如L2正则化)的钥匙。 P(data|model) 是似然(Likelihood),对应我们的损失函数(如负对数似然)。 P(model) 是先验(Prior),它表达了我们在看到数据之前对模型的偏好。L2正则化等价于假设模型参数服从高斯先验,它惩罚大的参数值,促使模型更简单,从而直接对抗过拟合。

注意 :很多初学者调参时只知道“权重衰减”(Weight Decay)能让模型泛化更好,但说不清原理。当你把它理解为贝叶斯框架下的高斯先验时,你就知道它不是在“魔法般地”让模型变好,而是在引入一种合理的模型偏好假设。

2.2 信息论:度量与传递“惊讶度”

信息论最初用于通信,但在机器学习中,它提供了衡量概率分布差异的绝佳工具。

2.2.1 信息量、熵与KL散度

  • 信息量 I(x) = -log P(x) 。一个事件发生的概率越低,其包含的信息量就越大。这非常直观:听到“明天太阳从东边升起”你觉得毫无信息量(概率接近1),但听到“明天本地有日食”你会觉得信息量很大(概率低)。
  • 熵(Entropy) H(P) = -Σ P(x) log P(x) 。用于衡量概率分布P的 不确定性 混乱程度 。均匀分布熵最大(最不确定),确定性分布熵为0(完全确定)。
    • 实战关联 :在决策树算法中,“信息增益”就是父节点的熵减去子节点加权平均熵,用来选择最佳分裂特征。
  • KL散度(相对熵) D_KL(P||Q) = Σ P(x) log (P(x)/Q(x)) 。衡量两个概率分布P和Q之间的差异。注意,它 不对称 D_KL(P||Q) ≠ D_KL(Q||P)
    • 核心理解 :KL散度是从P的角度看,用Q来近似P所损失的 信息量 。在机器学习中,P通常是真实的数据分布(或标签的one-hot分布),Q是我们的模型预测分布。我们的目标就是最小化这个散度。

2.2.2 交叉熵:KL散度的“亲民版”

交叉熵: H(P, Q) = -Σ P(x) log Q(x) = H(P) + D_KL(P||Q)

对于固定的真实分布P,其熵H(P)是一个常数。因此, 最小化交叉熵H(P, Q) 等价于最小化KL散度 D_KL(P||Q) 。这就是为什么分类任务几乎总是使用交叉熵作为损失函数的原因。

实操心得 :在PyTorch中, nn.CrossEntropyLoss 实际上已经内置了Softmax操作。这意味着你传给这个损失函数的模型输出(logits)不需要自己过Softmax。常见的错误是自己先做 F.softmax ,再用 nn.CrossEntropyLoss ,这会导致数值计算和理论上的错误。正确的做法是,模型最后一层线性层输出后,直接送入 nn.CrossEntropyLoss

2.2.3 JS散度与GAN的关联

KL散度不对称性可能导致训练不稳定。JS散度(Jensen-Shannon Divergence)是对称化的版本。虽然在原始GAN的论文中,理论推导涉及最小化真实分布与生成分布之间的JS散度,但实际训练中,由于种种原因(如分布重叠测度为0导致JS散度恒为常数),直接优化JS散度很困难,所以才有了后来的Wasserstein距离(WGAN)等一系列改进。理解这个演进过程,能让你明白GAN训练为什么那么“玄学”以及改进的方向在哪。

3. 数值计算基础:稳定训练的幕后功臣

这一部分通常讨论数值上溢、下溢、病态条件、梯度优化等。这些内容直接决定了你的模型能否训练、训练得是否稳定。

3.1 Softmax与数值稳定性

Softmax函数: softmax(z)_i = exp(z_i) / Σ_j exp(z_j) 。看似简单,但直接计算极易发生数值上溢( exp(z_i) 太大,超出浮点数表示范围)。

标准稳定实现技巧

def stable_softmax(z):
    z_max = np.max(z, axis=-1, keepdims=True)
    exp_z = np.exp(z - z_max)  # 减去最大值,防止上溢
    return exp_z / np.sum(exp_z, axis=-1, keepdims=True)

原理: softmax(z) = softmax(z - c) ,其中c是任意常数。减去最大值后,最大的 exp 项变为 exp(0)=1 ,完美避免了上溢。同时,因为至少有一个 z_i - z_max 为0,分母至少为1,也避免了下溢(所有 exp 都很小接近0)导致除零错误。

踩坑记录 :早期自己实现Softmax时没做这个优化,在某个输出值较大的任务中直接得到了 nan 损失,排查了半天才发现是数值溢出。框架(如PyTorch、TensorFlow)的 softmax 函数内部都做了这个处理,但如果你需要自己实现(比如在某些嵌入式环境),这个技巧必须掌握。

3.2 梯度下降中的病态条件与优化器选择

海森矩阵(Hessian)的条件数(最大特征值与最小特征值之比)衡量了损失函数表面的“崎岖”程度。条件数大(病态),意味着不同方向的曲率差异极大。

  • 现象 :沿着曲率大的方向(陡峭),需要小的学习率以防振荡;沿着曲率小的方向(平缓),需要大的学习率以快速前进。固定学习率的SGD在这里会非常低效,表现为损失下降缓慢、震荡。
  • 解决方案——自适应优化器
    • Momentum :引入“动量”概念,不仅考虑当前梯度,还累积历史梯度的指数移动平均。这有助于在曲率小的方向加速,并抑制曲率大方向的振荡。可以想象成小球滚下山时有了惯性。
    • AdaGrad/RMSprop/Adam :这些方法为每个参数维护一个自适应的学习率。对于频繁更新、梯度大的参数,给予较小的学习率(分母累积了大的梯度平方);对于不常更新、梯度小的参数,给予较大的学习率。这相当于 自动为不同方向(参数)分配了不同的学习率 ,有效缓解了病态问题。

个人经验 :对于标准的卷积网络(如ResNet)或Transformer训练, Adam 通常是默认的、稳健的起点。对于需要极致泛化性能的任务(如某些自然语言处理任务), SGD with Momentum 配合精心的学习率调度(如余弦退火)可能最终效果更优,但调参成本更高。新手建议从Adam开始,它让你更少操心学习率设置。

4. 从理论到实践:构建一个基于交叉熵的图像分类器

光说不练假把式。我们用一个简单的图像分类任务,把概率、信息论和数值计算的知识串起来。假设我们使用经典的MNIST手写数字数据集。

4.1 问题定义与模型选择

  • 任务 :多分类(10个数字,0-9)。
  • 真实分布P :对于一张标为“7”的图片,其标签的 真实分布 是一个one-hot向量: [0,0,0,0,0,0,0,1,0,0] 。这是一个确定的分布,熵为0。
  • 模型预测分布Q :我们的模型(比如一个简单的全连接网络)会输出一个10维的向量(logits)。经过Softmax函数后,我们得到一个概率分布,例如 [0.01, 0.01, 0.02, 0.05, 0.01, 0.01, 0.02, 0.85, 0.01, 0.01] 。这个分布就是我们的 预测分布Q
  • 目标 :最小化真实分布P与预测分布Q之间的差异。根据信息论,我们使用交叉熵作为损失函数。

4.2 损失计算过程详解

交叉熵损失 L = -Σ_i P(i) * log Q(i)

对于one-hot标签(只有第k类为1,其余为0),公式简化为: L = -log Q(k) 。其中 Q(k) 是模型预测该样本属于真实类别k的概率。

  • 计算示例 :假设真实类别是7(索引为7),模型预测的 Q(7) = 0.85
    • 损失 L = -log(0.85) ≈ 0.1625
    • 如果模型预测得很准, Q(7)=0.99 ,则 L = -log(0.99) ≈ 0.0101
    • 如果模型预测得很差, Q(7)=0.10 ,则 L = -log(0.10) ≈ 2.3026
    • 关键观察 :损失对预测概率的变化非常敏感,尤其是当概率很小时, -log 函数会给出巨大的惩罚。这迫使模型在训练中“极力避免”做出非常错误的预测。

4.3 PyTorch实现核心代码与解读

import torch
import torch.nn as nn
import torch.optim as optim

# 1. 定义一个简单的模型
class SimpleClassifier(nn.Module):
    def __init__(self, input_size=784, hidden_size=128, num_classes=10):
        super().__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)
        # 注意:这里没有显式的Softmax层

    def forward(self, x):
        x = x.view(-1, 28*28)  # 展平MNIST图像
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)  # 输出的是logits,未归一化
        return x

# 2. 初始化模型、损失函数、优化器
model = SimpleClassifier()
criterion = nn.CrossEntropyLoss()  # 内部整合了Softmax和交叉熵计算
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 3. 模拟一个训练步骤
# 假设我们有一个batch的数据
images = torch.randn(32, 1, 28, 28)  # [batch, channel, height, width]
labels = torch.randint(0, 10, (32,))   # 真实标签,形状为 [batch]

# 前向传播
logits = model(images)  # 输出形状 [32, 10]
# 计算损失:criterion内部会先对logits做Softmax得到Q,再与labels(作为P的索引)计算交叉熵
loss = criterion(logits, labels)

# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

print(f"Loss: {loss.item():.4f}")

代码解读与注意事项

  1. 模型输出 SimpleClassifier 的最后一层 fc2 直接输出10个值(logits),没有使用 nn.Softmax 。这是标准做法。
  2. 损失函数 nn.CrossEntropyLoss 做了三件事:
    • 对输入的logits应用Softmax,将其转换为概率分布Q。
    • 将真实标签 labels (整数形式)转换为one-hot形式(内部隐式完成,不显式生成矩阵)。
    • 计算Q与one-hot P之间的交叉熵。
  3. 数值稳定 nn.CrossEntropyLoss 的实现已经包含了数值稳定的Softmax计算(即我们之前讨论的减去最大值的技巧),无需自己操心。
  4. 梯度流动 :损失 loss 是一个标量。 loss.backward() 会计算模型所有参数关于这个损失的梯度。 optimizer.step() 则根据梯度(如Adam的规则)更新参数,完成一次学习迭代。

5. 训练中的典型问题与信息论视角的排查

理解了理论基础,很多训练中的问题就不再是黑盒。我们可以从概率和信息论的角度来诊断。

5.1 问题:损失不下降,准确率卡在“随机猜测”水平

例如,10分类问题,准确率长期在10%左右徘徊。

  • 信息论视角分析 :模型没有从数据中学到任何信息,其预测分布Q可能接近均匀分布。对于一个10分类问题,均匀分布的预测概率是每个类0.1。
  • 计算此时的交叉熵损失 L = -log(0.1) ≈ 2.3026 。你可以观察训练初期的损失值,如果它非常接近这个数(对于自然对数底),且长时间不下降,就印证了这一点。
  • 可能原因与排查
    1. 学习率过大或过小 :过大导致梯度更新震荡甚至发散;过小导致更新微乎其微。 解决方案 :尝试一个经典的学习率,如1e-3(Adam)或0.1(SGD with Momentum),并观察损失曲线最初几步是否有明显下降。
    2. 模型初始化问题 :所有权重初始化不当(如全为0),导致所有神经元输出相同,反向传播无法产生有差异的梯度。 解决方案 :使用标准的初始化方法,如 nn.Linear 默认的Kaiming初始化(针对ReLU)。
    3. 数据流或标签错误 :输入数据没有正确归一化(如像素值仍在0-255),或者标签与数据不对应。 解决方案 :检查前几个batch的数据和标签,可视化输入图像,确保预处理正确。
    4. 损失函数或模型最后一层用错 :错误地自己先做了Softmax又用CrossEntropyLoss,或者在多分类任务中误用了二分类的BCELoss。 解决方案 :仔细核对代码,确保使用 nn.CrossEntropyLoss (用于多分类)或 nn.BCEWithLogitsLoss (用于多标签二分类)。

5.2 问题:训练损失持续下降,但验证损失早早上涨(过拟合)

  • 信息论视角分析 :模型对训练数据的分布P_train拟合得“太好”,以至于学到了训练数据中的噪声和特定样本的细节,导致其学到的条件分布 Q(x|θ) 与真实的潜在数据分布 P_data(x) 差异(KL散度)在训练集上很小,但在验证集上很大。换句话说,模型对训练集的不确定性(熵)降得太低,丧失了泛化能力。
  • 可能原因与解决方案
    1. 模型复杂度过高 :参数太多,表达能力过强。 解决方案 :降低模型层数或宽度,或引入更强的正则化。
    2. 正则化不足 P(model) 先验太弱。 解决方案
      • L2正则化(权重衰减) :在优化器中设置 weight_decay 参数(如 optim.Adam(..., weight_decay=1e-4) ),这等价于给参数施加高斯先验。
      • Dropout :在训练时随机“关闭”一部分神经元,可以理解为在训练多个子模型的集成,是一种近似贝叶斯推理的方法,增加了模型预测的随机性(熵),防止对特定神经路径的过度依赖。
      • 数据增强 :如前所述,通过对训练数据进行随机变换,来人工扩展 P_train ,使其更接近 P_data
    3. 训练数据量不足 :这是过拟合的根本原因之一。 P_train 无法很好地代表 P_data 解决方案 :收集更多数据,或使用更激进的数据增强、迁移学习。

5.3 问题:梯度消失/爆炸

  • 数值计算视角分析 :在深层网络中,梯度通过链式法则反向传播。如果每一层的线性变换的权重矩阵 W 的特征值(或奇异值)的模长期大于1或小于1,经过多层连乘后,梯度会指数级增长(爆炸)或衰减(消失)。
  • 解决方案
    1. 权重初始化 :使用Xavier或Kaiming初始化,使每一层输出的方差在正向传播时保持稳定,这也有助于梯度的稳定。
    2. 激活函数 :使用ReLU及其变种(Leaky ReLU, PReLU)替代Sigmoid/Tanh,因为它们的导数为常数(在激活区),缓解了梯度消失。
    3. 归一化层 :使用Batch Normalization(BN)或Layer Normalization(LN)。BN通过对每一层的输入进行标准化(减均值、除标准差),将其强行拉回到均值为0、方差为1的分布附近,这极大地改善了网络的 病态条件 ,允许使用更大的学习率,并显著缓解了梯度消失/爆炸问题。从信息流动的角度看,BN让每一层的输入分布更稳定,减少了内部协变量偏移(Internal Covariate Shift)。
    4. 梯度裁剪 :在训练RNN或非常深的网络时,即使采取了上述措施,梯度爆炸仍可能发生。一个简单有效的后置方法是梯度裁剪( torch.nn.utils.clip_grad_norm_ ),将梯度的总范数限制在一个阈值内。

6. 超越基础:信息论在模型设计与评估中的延伸应用

掌握了基础,我们可以看看这些理论如何指导更高级的模型设计和评估。

6.1 变分自编码器(VAE)中的KL散度

VAE的目标函数由两部分构成:重构损失和KL散度正则项。 Loss = Reconstruction_Loss + β * D_KL(q(z|x) || p(z))

  • q(z|x) 是编码器输出的潜在变量z的分布(通常是高斯分布)。
  • p(z) 是先验分布(标准正态分布)。
  • D_KL 项的作用是 迫使编码器产生的潜在分布q(z|x)接近标准正态分布p(z) 。这带来了两个好处:
    1. 正则化 :防止编码器为不同的输入x编码出差异巨大、互不重叠的z分布,从而让潜在空间更连续、更规则。这样,在潜在空间中插值生成的新z,解码后也能得到有意义的输出。
    2. 解纠缠 :理想情况下,潜在空间z的每个维度对应数据的一个独立变化因子。KL散度项与解纠缠学习密切相关。

β是一个超参数,控制正则化的强度。β-VAE通过调整β的大小,可以在重构精度和潜在空间规整度(解纠缠能力)之间进行权衡。

6.2 模型校准与预期校准误差(ECE)

一个分类模型不仅要有高准确率,其预测的 置信度 也应该与 正确率 相匹配。例如,在100个模型以0.9置信度预测的样本中,我们期望大约有90个被正确分类。如果只有70个正确,说明模型 过度自信 了。

信息论关联 :一个校准良好的模型,其预测概率分布Q应该真实反映其判断的不确定性。这涉及到预测概率的“真实性”。

评估方法——预期校准误差(Expected Calibration Error, ECE)

  1. 将模型对所有测试样本的预测置信度(即预测最大类的概率)区间[0,1]分成M个桶(bin)。
  2. 对每个桶,计算桶内所有样本的平均预测置信度( conf(B_m) )和平均准确率( acc(B_m) )。
  3. ECE = Σ (|B_m|/N) * |acc(B_m) - conf(B_m)| ,其中N是总样本数。

一个校准良好的模型,其ECE应该接近0。现代深度神经网络往往存在过度自信的问题。 改善校准的技术包括:使用标签平滑(Label Smoothing)、在训练后使用温度缩放(Temperature Scaling)等 。温度缩放非常简单:在Softmax函数中引入一个温度参数T: softmax(z/T) 。在测试时,寻找一个最优的T(>1)来“软化”概率分布,使其更平缓,从而改善校准性。

6.3 互信息与对比学习

互信息(Mutual Information, MI)衡量两个随机变量之间相互依赖的程度。 I(X; Y) = H(X) - H(X|Y) = H(Y) - H(Y|X) 。它表示知道了Y后,X的不确定性减少了多少。

对比学习 (如SimCLR, MoCo)中,核心思想是拉近同一图像不同增强视图(正样本对)的表示,拉远不同图像(负样本对)的表示。其目标函数(InfoNCE Loss)可以被理解为 最大化正样本对之间的互信息的下界

通过这种方式,模型学习到的表示能够捕捉到图像中那些不受随机增强影响的核心语义信息(即“内容”),过滤掉无关的细节(即“风格”或“噪声”)。这为我们理解自监督学习为何有效提供了一个强大的信息论视角。

“硬啃”数学基础的意义就在于此。它不仅仅是为了通过考试,更是为了在你面对千变万化的模型、层出不穷的论文时,拥有一套强大的分析工具和直觉。当你在代码中写下 nn.CrossEntropyLoss() 时,你能想到它背后是KL散度的最小化;当你调整 weight_decay 时,你能意识到这是在引入高斯先验;当你使用BatchNorm时,你能理解它在改善优化问题的条件数。这种从“知道”到“懂得”的转变,才是“硬啃”带给你的最大财富。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐