恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
损失函数与优化算法:从线性分类器到模型训练的核心机制
首页
资讯中心
/
损失函数与优化算法:从线性分类器到模型训练的核心机制
损失函数与优化算法:从线性分类器到模型训练的核心机制
发布时间:2026/10/15 13:46:12
1. 从“打分”到“纠偏”损失函数与优化到底在解决什么问题很多人学完线性分类器之后会有一种错觉模型已经能对每张图片输出一组分数了任务好像已经完成了大半。但真正跑过代码的人都知道一组未经训练的权重矩阵输出的分数基本等同于随机猜测。比如把一张猫的图片送进一个初始化后的线性分类器它可能给“猫”这个类别打 2.1 分给“狗”打 2.3 分给“汽车”打 1.9 分——分数是有了但完全不对。问题出在哪里出在我们还没有告诉模型“什么样的分数算好什么样的分数算差”更没有告诉它“如果算错了该往哪个方向调整”。这就是损失函数和优化要解决的两件事。损失函数负责量化“当前模型有多差”优化负责“如何让模型变得不那么差”。前者是评价标准后者是改进手段两者缺一不可。你可以把损失函数想象成一把尺子它不直接改变模型但它告诉模型现在的预测和真实答案之间差了多少优化器则像是一个导航系统根据尺子量出来的差距决定下一步往哪个方向走、走多远。这个内容适合谁如果你正在学习计算机视觉的基础课程或者已经写过一些简单的分类代码但总觉得“训练过程像个黑箱”那这篇内容就是为你准备的。我会把损失函数的设计逻辑、优化算法的迭代过程、以及实际编码中容易踩的坑按照从业者的视角完整拆开讲一遍。不会只停留在公式层面而是会结合具体的数值例子和代码片段让你真正理解每一步在做什么、为什么这么做。2. 损失函数的设计逻辑为什么不能只看准确率2.1 准确率的致命缺陷不可导与不敏感初学者最容易想到的评价指标是准确率预测对了多少张除以总张数得到一个百分比。这个指标很直观但它不能直接用来训练模型。原因有两个而且都是致命的。第一个原因是不可导。准确率是一个离散的统计量你稍微调整一下权重矩阵里的某个数值准确率可能从 0.72 跳到 0.73也可能纹丝不动。它不提供梯度信息优化器就不知道该怎么调参数。这就好比你蒙着眼睛调收音机的旋钮只能听到“有台”或“没台”但听不到“离台还有多远”调起来全靠运气。第二个原因是不敏感。假设一个模型对某张图片的正确类别打了 4.9 分错误类别打了 5.0 分另一个模型对同一张图片的正确类别打了 0.1 分错误类别打了 9.9 分。从准确率角度看这两个模型都预测错了损失是一样的。但显然第二个模型错得更离谱应该受到更大的“惩罚”。准确率无法区分这种细微差别而损失函数可以。2.2 多类支持向量机损失用“边界”来定义好坏多类支持向量机损失的核心思想非常朴素正确类别的分数应该比错误类别的分数高出至少一个边界值。如果高出的幅度不够就要产生损失如果已经超过了这个边界就不再惩罚。用数学语言描述就是对于第 i 个样本设正确类别的分数为 s_yi某个错误类别的分数为 s_j那么这一对产生的损失是 max(0, s_j - s_yi Δ)其中 Δ 是边界值通常设为 1.0。把所有错误类别的损失加起来就是这张图片的总损失。我举一个具体的数值例子这样你能更直观地理解。假设有三张图片三个类别分别是猫、狗、汽车。某张猫的图片模型输出的分数是猫 3.2、狗 5.1、汽车 -1.7。边界值 Δ 取 1.0。对于狗这个错误类别max(0, 5.1 - 3.2 1.0) max(0, 2.9) 2.9。对于汽车这个错误类别max(0, -1.7 - 3.2 1.0) max(0, -3.9) 0。所以这张图片的总损失是 2.9 0 2.9。这个结果说明什么说明模型把狗和猫搞混了而且狗的分数比猫高出很多所以损失很大。而汽车那一项没有产生损失因为汽车的分数远低于猫的分数模型在这对类别上没有犯错。注意边界值 Δ 的选择会影响损失的尺度但不会改变最优解的方向。因为当你把所有分数同时放大或缩小时边界值的影响会被权重正则化抵消。实际使用中Δ 取 1.0 是最常见的做法。2.3 交叉熵损失从概率角度看问题支持向量机损失把分数当作“打分”只关心正确类别的分数是否比错误类别高出足够多。而交叉熵损失则把分数转化为概率从信息论的角度来衡量预测分布和真实分布之间的差异。具体做法是先用 Softmax 函数把原始分数转化为概率对于每个类别计算 e 的分数次方然后除以所有类别 e 的分数次方之和。这样得到的概率值都在 0 到 1 之间且所有类别的概率之和为 1。然后取正确类别概率的负对数就是交叉熵损失。还是用刚才的例子。猫 3.2、狗 5.1、汽车 -1.7。先算 e 的次方e^3.2 ≈ 24.5e^5.1 ≈ 164.0e^-1.7 ≈ 0.18。总和是 24.5 164.0 0.18 188.68。那么猫的概率是 24.5 / 188.68 ≈ 0.13狗的概率是 164.0 / 188.68 ≈ 0.87汽车的概率是 0.18 / 188.68 ≈ 0.001。正确类别是猫概率只有 0.13交叉熵损失是 -ln(0.13) ≈ 2.04。这个损失值意味着什么如果模型给正确类别的概率是 1.0损失就是 0如果概率是 0.5损失约等于 0.69如果概率是 0.1损失约等于 2.30。概率越低损失增长越快这种“惩罚”是非线性的模型会更有动力把正确类别的概率推高。2.4 两种损失的对比与选择对比维度多类支持向量机损失交叉熵损失关注点正确类别与错误类别的分数差距正确类别的概率大小对“已经正确”样本的态度超过边界后不再惩罚永远希望正确概率更高梯度特性稀疏只有未满足边界的类别产生梯度密集所有类别都参与梯度更新适用场景对分类边界敏感的任务需要概率输出的任务实际使用频率较少单独使用分类任务的主流选择从实际经验来看交叉熵损失在大多数分类任务中表现更稳定因为它提供了更平滑的梯度信号。支持向量机损失在某些对边界敏感的场景下仍有价值但需要更仔细地调参。我个人的建议是如果你不确定用哪个先用交叉熵损失它几乎不会让你失望。3. 正则化为什么“完美分类”反而可能是灾难3.1 过拟合的本质模型把训练集“背下来”了假设你有一个足够复杂的模型它可以把训练集中每一张图片都正确分类损失降到零。这听起来很美好但实际测试时表现可能一塌糊涂。为什么因为模型可能记住的是训练集中每张图片的“个性特征”而不是“共性规律”。比如它可能记住了“这张猫的图片左上角有一个白色像素”而不是“猫有尖耳朵和胡须”。这种“死记硬背”就是过拟合。过拟合的根源在于模型参数太多而训练数据相对太少。模型有足够的“容量”去拟合训练集中的噪声和偶然特征。正则化就是用来限制模型容量、防止过拟合的手段。3.2 L2 正则化的作用机制L2 正则化是最常用的正则化方式它的做法是在损失函数后面加上一项λ 乘以所有权重平方和的一半。λ 是一个超参数控制正则化强度。这一项为什么能防止过拟合因为它对大的权重值施加惩罚。当模型试图通过放大某些权重来“记住”训练样本时正则化项会拉高总损失迫使模型选择更小的权重。权重越小模型对输入的微小变化就越不敏感泛化能力就越强。从另一个角度看L2 正则化相当于在权重空间中施加了一个高斯先验倾向于让权重分布在零附近。这听起来有点抽象但你可以这样理解它告诉模型“不要对任何一个特征过于自信保持谦逊”。3.3 正则化强度的选择λ 不是越大越好λ 的选择是一个权衡。λ 太小正则化效果微弱模型仍然可能过拟合λ 太大模型会被过度约束连训练集都拟合不好出现欠拟合。我通常的做法是从一个较小的值开始比如 1e-4 或 1e-3然后观察训练损失和验证损失的变化。如果训练损失远低于验证损失说明过拟合了可以适当增大 λ如果两者都很高且接近说明欠拟合应该减小 λ。实操心得在训练初期可以先不加正则化观察模型是否过拟合。如果确实过拟合再逐步加入正则化并调整 λ。这样比一开始就盲目调参更有效率。4. 优化算法从“盲人摸象”到“精准导航”4.1 梯度下降的直观理解优化的目标是找到一组权重使损失函数的值最小。梯度下降是最基础的优化方法它的思路是计算损失函数对每个权重的梯度然后沿着梯度的反方向更新权重。梯度告诉我们“哪个方向损失上升最快”反方向就是“损失下降最快的方向”。用生活化的类比来说你站在山坡上蒙着眼睛想走到山谷最低点。你能感知到脚下哪个方向最陡然后朝着最陡的下坡方向迈一步。这一步的大小就是学习率。步子太小走得太慢步子太大可能直接跨过山谷跑到对面山坡上去了。4.2 学习率最重要的超参数学习率决定了每次更新的步长。它是所有超参数中最需要仔细调整的一个。学习率太大损失可能震荡甚至发散学习率太小收敛速度慢得让人抓狂。我试过的一个典型情况是学习率设为 0.1 时损失在前几个迭代就炸成了 NaN降到 0.01 后损失开始稳定下降但速度很慢最终用 0.001 配合学习率衰减策略才得到了比较理想的结果。实际使用中很少有人用固定的学习率。常见的策略包括步长衰减、指数衰减、余弦退火等。步长衰减是每隔几个 epoch 把学习率乘以一个小于 1 的因子比如 0.1。这样做的逻辑是训练初期需要大步快走快速接近最优区域训练后期需要小步微调避免在最优解附近震荡。4.3 随机梯度下降与小批量标准梯度下降每次更新都要计算整个训练集的梯度计算量太大。随机梯度下降每次只用一个样本计算梯度速度快但噪声大。实际使用中最常见的是小批量梯度下降每次用一小批样本比如 32、64、128 个计算梯度。小批量的大小也是一个需要权衡的参数。批量太小梯度估计噪声大训练不稳定批量太大内存占用高且可能陷入尖锐的局部最小值。我通常从 64 或 128 开始尝试根据显存大小和训练稳定性调整。4.4 动量法给优化过程加“惯性”想象一个球从山坡上滚下来它不会每时每刻都完全按照当前最陡的方向走而是会保留一部分之前的运动方向。动量法就是这个思路在更新权重时不仅考虑当前的梯度还考虑之前累积的更新方向。这样做的好处是在梯度方向变化不大的维度上动量会加速收敛在梯度方向频繁变化的维度上动量会抑制震荡。实际效果是收敛更快、更稳定。动量系数通常设为 0.9意味着保留 90% 的历史方向。这个值不需要频繁调整0.9 在大多数情况下都工作得很好。4.5 自适应学习率方法Adam 与它的朋友们Adam 是目前最流行的自适应学习率优化器。它结合了动量和自适应学习率的思想为每个参数单独维护一个学习率。具体来说它计算梯度的一阶矩估计和二阶矩估计然后用这些估计来调整每个参数的更新步长。Adam 的优点是对学习率的初始值不那么敏感通常 0.001 就能工作得很好收敛速度快适合大多数任务。缺点是在某些任务上可能不如精心调参的带动量 SGD尤其是在需要精细收敛的场景下。我个人的使用习惯是如果任务比较常规直接用 Adam学习率设 0.001如果追求极致性能会尝试带动量的 SGD学习率从 0.01 开始配合衰减策略。5. 完整实操流程从零实现一个线性分类器5.1 数据准备与预处理假设我们有一个图像分类数据集每张图片被展平成一个一维向量。在送入线性分类器之前需要做两步预处理。第一步是中心化计算所有训练图片在每个像素位置上的均值然后每张图片减去这个均值。这样做的目的是让数据分布以零为中心避免某些特征因为数值过大而主导梯度。第二步是归一化把每个像素值除以标准差使数据分布具有单位方差。这一步不是必须的但在某些数据集上能加速收敛。注意均值和标准差必须只从训练集计算然后应用到验证集和测试集。如果从整个数据集计算会造成信息泄露导致验证结果过于乐观。5.2 损失函数与梯度的代码实现下面是一个多类支持向量机损失的 Python 实现包含梯度计算import numpy as np def svm_loss_naive(W, X, y, reg): 输入: W: 权重矩阵形状 (D, C)D 是特征维度C 是类别数 X: 数据矩阵形状 (N, D)N 是样本数 y: 标签数组形状 (N,)每个元素是 0 到 C-1 的整数 reg: 正则化强度 输出: loss: 标量损失值 dW: 权重梯度形状与 W 相同 dW np.zeros(W.shape) num_classes W.shape[1] num_train X.shape[0] loss 0.0 for i in range(num_train): scores X[i].dot(W) correct_class_score scores[y[i]] for j in range(num_classes): if j y[i]: continue margin scores[j] - correct_class_score 1.0 if margin 0: loss margin dW[:, j] X[i] dW[:, y[i]] - X[i] loss / num_train dW / num_train loss reg * np.sum(W * W) dW 2 * reg * W return loss, dW这段代码的逻辑很直接对每个样本计算每个错误类别的边界损失如果损失大于零就累加同时更新梯度。梯度更新的逻辑是错误类别的权重应该减小因为它的分数太高了正确类别的权重应该增大因为它的分数太低了。5.3 训练循环的搭建训练循环的核心步骤是前向传播计算损失反向传播计算梯度然后用优化器更新权重。下面是一个带动量的 SGD 训练循环示例def train(X_train, y_train, X_val, y_val, learning_rate1e-3, reg1e-5, num_iters1000, batch_size200, momentum0.9): num_train, dim X_train.shape num_classes np.max(y_train) 1 W 0.001 * np.random.randn(dim, num_classes) velocity np.zeros_like(W) loss_history [] for it in range(num_iters): batch_indices np.random.choice(num_train, batch_size) X_batch X_train[batch_indices] y_batch y_train[batch_indices] loss, grad svm_loss_naive(W, X_batch, y_batch, reg) loss_history.append(loss) velocity momentum * velocity - learning_rate * grad W velocity if it % 100 0: val_acc np.mean(np.argmax(X_val.dot(W), axis1) y_val) print(f迭代 {it}: 损失 {loss:.4f}, 验证准确率 {val_acc:.4f}) return W, loss_history这个训练循环里有两个细节值得注意。第一每次迭代随机抽取一个小批量而不是按顺序遍历这样可以打乱样本顺序避免模型学到与顺序相关的虚假模式。第二动量项累积了历史梯度方向使更新更加平滑。5.4 超参数调优的实操策略超参数调优是训练过程中最耗时的环节。我的策略是分两步走先粗调再细调。粗调阶段用较小的迭代次数比如 200 次和较大的学习率范围比如 1e-1、1e-2、1e-3、1e-4进行网格搜索观察哪些学习率能让损失稳定下降。细调阶段在粗调找到的最佳学习率附近用更小的步长比如 3e-3、1e-3、3e-4和更多的迭代次数进行搜索。正则化强度 λ 的调优类似通常从 1e-5 到 1e-1 之间取几个值进行尝试。需要注意的是学习率和正则化强度之间存在一定的耦合关系最好交替调优而不是一次性同时搜索。实操心得在调参时一定要记录每次实验的配置和结果。我习惯用一个简单的表格记录学习率、正则化强度、训练损失、验证准确率这样能快速看出趋势避免重复试错。6. 常见问题与排查技巧实录6.1 损失不下降或下降太慢这是最常见的问题。可能的原因和排查思路如下现象可能原因排查方法解决方案损失几乎不变学习率太小尝试增大学习率 10 倍逐步增大直到损失开始下降损失震荡剧烈学习率太大观察损失曲线是否上下跳动减小学习率或使用学习率衰减损失下降但很慢动量系数太小检查动量设置增大动量到 0.9 或 0.99损失卡在某个值初始化不当检查权重初始化范围使用更小的初始化标准差我踩过的一个坑是权重初始化时用了标准差为 1 的正态分布结果初始损失非常大梯度也很大训练直接发散。后来改成 0.001 的标准差问题就解决了。权重初始化宁小勿大这是经验之谈。6.2 验证准确率远低于训练准确率这是典型的过拟合信号。排查思路是先看训练损失是否远低于验证损失如果是说明模型在训练集上过度拟合了。解决过拟合的手段有几个增大正则化强度 λ、增加训练数据、使用数据增强、简化模型结构。其中增大 λ 是最直接的方法但要注意不要矫枉过正导致欠拟合。6.3 损失出现 NaN损失变成 NaN 通常意味着数值溢出。可能的原因包括学习率太大导致权重爆炸、Softmax 计算中出现了指数溢出、除零错误等。排查方法是先检查学习率是否过大尝试减小 10 倍然后检查数据中是否有异常值或缺失值最后检查 Softmax 实现中是否做了数值稳定处理比如减去最大值。注意在计算 Softmax 时一定要先减去每行的最大值否则 e 的分数次方很容易溢出。这是一个非常常见的实现细节但初学者经常忽略。6.4 梯度检查验证反向传播是否正确当你自己实现损失函数和梯度时一定要做梯度检查。方法是用数值近似的方法计算梯度然后和你解析计算的梯度对比。如果相对误差在 1e-6 到 1e-4 之间说明梯度实现是正确的。数值梯度的计算方式是(f(x h) - f(x - h)) / (2h)其中 h 是一个很小的值比如 1e-5。这个方法的原理是中心差分比单侧差分更精确。梯度检查时有一个细节要先关闭正则化因为正则化项的梯度很容易计算正确但会掩盖数据损失项的梯度错误。等数据损失项的梯度检查通过后再打开正则化检查。6.5 学习率衰减的时机选择学习率衰减的时机很关键。衰减太早模型还没充分训练就被限制了衰减太晚模型在最优解附近震荡浪费计算资源。我的经验是先观察损失曲线当损失下降明显变缓时就是衰减的好时机。通常在第 10 到 20 个 epoch 之间会有一次明显的放缓。也可以设置固定的衰减节点比如在第 10、20、30 个 epoch 时各衰减一次。7. 从线性分类器到更复杂模型的过渡思考线性分类器的损失函数和优化方法是理解更复杂模型的基础。卷积神经网络、Transformer 等模型本质上也是在做同样的事情定义损失函数、计算梯度、更新权重。区别只在于模型结构更复杂、参数更多、损失函数可能更精细。我个人的体会是把线性分类器这一块吃透后面学深度学习框架时就不会觉得是在“调包”。你知道每一行代码背后在做什么知道为什么学习率要设成那个值知道为什么需要正则化知道梯度消失和梯度爆炸是怎么回事。这些底层认知比会调用几个 API 重要得多。后续如果想继续深入可以沿着两个方向扩展一是尝试不同的损失函数比如合页损失的变体、Focal Loss 等理解它们各自解决什么问题二是尝试不同的优化器比如 RMSProp、AdaGrad、AdamW 等对比它们在不同任务上的表现。这些内容在线性分类器的框架下都能快速实验是很好的练手项目。最后分享一个小技巧在实现损失函数时先用一个极小的数据集比如 10 张图片、3 个类别手动计算一遍损失和梯度然后和代码输出对比。这个“手算验证”的过程虽然笨但能帮你发现很多隐藏的 bug比盲目调参有效得多。