从手写数字识别深入理解交叉熵损失函数原理与实战
1. 从“猜数字”说起损失函数到底在做什么很多人第一次接触手写数字识别注意力全在“网络结构怎么搭”“准确率能到多少”上损失函数往往被当成一行代码随手带过。我刚开始学的时候也这样loss CrossEntropyLoss()一写训练跑起来就完事。直到有一次模型怎么训都不收敛排查了大半天才发现问题出在我根本没搞懂损失函数在背后干了什么。先把场景说清楚。手写数字识别要解决的问题是给一张28×28的灰度图判断它写的是0到9中的哪个数字。这是一个十分类问题。模型前向传播之后会输出10个数值我们叫它logits可以理解成模型对每个数字的“信心打分”。但打分归打分怎么把这个打分和真实标签之间的差距量化成一个可以反向传播的数值这就是损失函数的活儿。打个生活化的比方。你让一个新手去猜盒子里有几个球他猜“大概3个”实际是5个。损失函数就是那个裁判负责告诉他“你差了2个”。但裁判的判法有很多种可以直接算差值可以算差值的平方也可以算比例。不同的判法给新手的反馈方向完全不同有的温和有的严厉有的对大误差特别敏感。损失函数的选择直接决定了模型学习的“性格”。这一块内容我打算从三个层面拆开讲第一为什么分类任务不能直接用均方误差第二交叉熵损失函数的数学原理和它为什么适合分类第三在具体代码里怎么用、有哪些坑。中间会穿插我自己踩过的坑和实测数据尽量让零基础的朋友也能跟上。提示本文所有代码基于常见的深度学习框架编写思路通用换框架只需调整API名称。2. 为什么分类任务不能照搬回归的损失函数2.1 均方误差在分类任务上的尴尬回归任务里最常用的损失函数是均方误差公式很简单预测值和真实值相减平方再求平均。比如预测房价真实300万预测280万损失就是(300-280)²400。这个逻辑直观梯度也好算。但把它搬到分类任务上问题就来了。假设一张图真实标签是“3”经过独热编码之后真实向量是[0,0,0,1,0,0,0,0,0,0]。模型输出经过某种归一化后是[0.1,0.1,0.1,0.4,0.1,0.1,0.05,0.02,0.02,0.01]。用均方误差算损失是各项差值的平方和。看起来也能算对吧问题出在梯度上。均方误差对输出的梯度是线性的当输出值经过Sigmoid或Softmax这类函数压缩到0到1之间时梯度会变得非常小。我用一个具体数字说明假设某个输出节点的值接近0或1Sigmoid函数的导数在两端趋近于0。这时候均方误差传回去的梯度再乘上这个接近0的导数结果就是梯度消失参数几乎不更新。实测下来用均方误差训练分类模型收敛速度比交叉熵慢好几倍而且容易卡在局部最优。还有一个更本质的问题均方误差隐含假设了输出是高斯分布而分类任务的输出本质上是概率分布应该用描述概率分布差异的度量方式。2.2 从信息论借来的交叉熵交叉熵这个概念来自信息论原本是用来衡量两个概率分布之间差异的。公式长这样H(p, q) -Σ p(x) log q(x)其中p是真实分布q是模型预测分布。在分类任务里真实分布p就是独热编码只有正确类别那一项是1其他都是0。所以公式可以简化成H(p, q) -log q(正确类别)这个简化太关键了。它意味着交叉熵损失只关心模型对正确类别的预测概率。预测得越准q(正确类别)越接近1log之后接近0损失就小预测得越离谱q(正确类别)接近0log之后趋向负无穷取负号就变成正无穷大损失巨大。这种“只盯着正确答案”的特性让交叉熵在分类任务上天然合适。而且它的梯度形式非常漂亮对logits的梯度就是预测概率减去真实标签。这个梯度不会因为Softmax的饱和而消失因为Softmax的导数被巧妙地抵消掉了。这也是为什么交叉熵配Softmax成为分类任务的标准组合。2.3 一个直观的对比实验我之前做过一个对比实验同样的网络结构同样的数据只换损失函数。用均方误差的时候训练10轮之后准确率还在60%左右徘徊损失下降得很慢。换成交叉熵之后第3轮准确率就冲到了90%以上。这个差距不是偶然背后就是梯度性质的差异。对比维度均方误差交叉熵适用任务回归分类梯度特性容易饱和消失梯度稳定收敛速度慢快概率解释无有明确概率意义与Softmax配合不推荐标准组合注意不是说均方误差完全不能用在某些特殊场景下配合特定的输出层设计也能工作但对于标准的多分类任务交叉熵是更稳妥的选择。3. 交叉熵损失函数的代码实现与参数细节3.1 从logits到概率Softmax的角色在代码里交叉熵损失通常不会单独出现它往往和Softmax绑定在一起。Softmax的作用是把模型输出的原始logits转换成概率分布保证所有输出在0到1之间且和为1。公式是softmax(z_i) exp(z_i) / Σ exp(z_j)这个操作有个特点它会放大大的logits压制小的logits。比如两个logits是[2, 1]经过Softmax之后大约是[0.73, 0.27]。如果差距更大比如[5, 1]结果就是[0.98, 0.02]。这种“赢家通吃”的特性让模型对正确类别的预测越来越自信。但这里有个坑如果直接先算Softmax再算交叉熵数值上容易不稳定。因为exp操作可能溢出尤其是logits很大的时候。所以实际框架里通常把Softmax和交叉熵合并成一个操作用log-sum-exp技巧来保证数值稳定性。你在代码里看到的CrossEntropyLoss内部其实已经包含了Softmax所以不要在前向传播里再加一层Softmax否则就重复了。3.2 代码实操手写数字识别的损失计算下面这段代码展示了一个完整的损失计算流程我加了详细注释import torch import torch.nn as nn # 假设batch_size64类别数10 batch_size 64 num_classes 10 # 模拟模型输出的logits形状是[batch_size, num_classes] logits torch.randn(batch_size, num_classes) # 模拟真实标签形状是[batch_size]值是0到9的整数 labels torch.randint(0, num_classes, (batch_size,)) # 定义交叉熵损失函数 criterion nn.CrossEntropyLoss() # 计算损失 loss criterion(logits, labels) print(f损失值: {loss.item():.4f})这段代码里有个关键点labels是整数索引不是独热编码。CrossEntropyLoss内部会自动把整数标签转成独热形式然后计算交叉熵。如果你手动传独热编码进去反而会报错或者算错。这是新手很容易搞混的地方。另外logits的形状是[batch_size, num_classes]labels的形状是[batch_size]。维度要对齐否则会出各种奇怪的错误。我见过有人把labels的形状搞成[batch_size, 1]结果损失算出来完全不对。3.3 手动实现交叉熵理解每一步为了真正搞懂交叉熵我建议你手动实现一遍。下面是不依赖框架的版本import numpy as np def softmax(x): # 减去最大值防止溢出 x_shifted x - np.max(x, axis1, keepdimsTrue) exp_x np.exp(x_shifted) return exp_x / np.sum(exp_x, axis1, keepdimsTrue) def cross_entropy_loss(logits, labels): # 先算softmax得到概率 probs softmax(logits) # 取正确类别的概率 batch_size logits.shape[0] correct_probs probs[np.arange(batch_size), labels] # 算负对数 loss -np.mean(np.log(correct_probs 1e-10)) return loss这里加1e-10是为了防止log(0)出现负无穷。虽然实际框架内部有更精细的处理但手动实现时这个保护是必要的。跑一遍这个代码再对比框架的输出你会发现数值几乎一致这时候你对交叉熵的理解就到位了。提示手动实现主要用于理解原理实际训练还是用框架自带的损失函数因为框架版本在数值稳定性和计算效率上都做了大量优化。4. 训练过程中损失函数的常见问题与排查4.1 损失不下降的几种可能训练时最让人抓狂的就是损失一动不动。我遇到过好几次总结下来大概有这么几类原因第一类是学习率设置不当。学习率太大损失会震荡甚至发散学习率太小损失下降慢得像蜗牛。我一般会先用一个中等学习率试跑几十个batch观察损失变化。如果损失几乎不变就调大学习率如果损失上下跳动就调小。第二类是数据标签有问题。有一次我拿到的数据集标签编码方式和损失函数期望的不一致导致模型一直在学错误的目标。排查方法是打印几个batch的标签和对应的预测值肉眼看看是否合理。第三类是网络输出层设计有误。比如输出维度不是类别数或者忘记去掉最后的Softmax层导致重复计算。这些细节在代码审查时容易被忽略。4.2 损失突然变成NaNNaN是另一个常见问题。原因通常是数值溢出或除零。交叉熵里涉及log和exp如果logits数值过大exp之后可能溢出成inf再取log就变成NaN。解决办法是在Softmax之前减去最大值这个操作框架内部已经做了但如果你手动实现就要注意。还有一种情况是学习率过大导致参数更新过猛logits直接飞到很大的值。这时候可以加梯度裁剪限制梯度的最大范数。我在训练手写数字识别时一般会把梯度裁剪的阈值设在5左右效果比较稳。4.3 损失下降但准确率不涨这种情况说明模型在优化损失但学到的特征没有泛化能力。可能的原因包括过拟合、数据分布不均衡、评估指标计算有误。我一般会同时监控训练集和验证集的损失与准确率。如果训练集损失降但验证集损失升那就是过拟合需要加正则化或早停。如果两者都不涨那可能是模型容量不够或者数据本身有问题。问题现象可能原因排查方法解决思路损失不下降学习率不当观察损失曲线调整学习率损失变NaN数值溢出检查logits范围加梯度裁剪损失降但准确率不涨过拟合对比训练验证曲线加正则化损失震荡学习率过大观察损失波动减小学习率4.4 一个容易被忽略的细节reduction参数CrossEntropyLoss有个reduction参数默认是mean表示对所有样本的损失求平均。还有sum和none两个选项。sum是求和none是不做聚合返回每个样本的损失。这个参数在有些场景下很重要。比如你想对不同样本加权就需要先用none拿到每个样本的损失再手动加权求和。如果直接用默认的mean加权逻辑就不好实现。我刚开始用的时候没注意这个参数后来做类别不均衡任务时才发现它的用处。比如手写数字里数字1的样本特别多数字9特别少就可以给数字9的样本更高的权重让模型更关注少数类。5. 损失函数的选择与调优经验5.1 多分类任务的损失函数选型虽然交叉熵是多分类的标配但也不是唯一选择。有些场景下会用到其他变体带权重的交叉熵用于类别不均衡给少数类更高权重。Focal Loss用于难易样本不均衡降低易分类样本的权重让模型聚焦难样本。标签平滑交叉熵把硬标签变成软标签缓解过拟合提升泛化能力。我在手写数字识别任务上试过标签平滑把真实标签的1改成0.9其他0改成0.1/9。效果上训练集准确率略有下降但验证集准确率有提升说明泛化能力变好了。这个技巧在数据量不大或者模型容易过拟合时特别有用。5.2 损失函数与优化器的配合损失函数和优化器是搭档配合不好也会出问题。交叉熵的梯度形式是预测概率 - 真实标签这个梯度范围在-1到1之间比较温和。配合SGD或者Adam都能工作。但如果用均方误差梯度可能非常小就需要更大的学习率或者更激进的优化器。我个人的习惯是交叉熵配Adam学习率设0.001大部分情况下都能稳定收敛。如果追求更好的最终精度可以换成SGD加动量学习率设0.01配合学习率衰减策略但调参成本更高。5.3 监控损失曲线的正确姿势损失曲线是训练过程中最重要的诊断工具。我一般会关注几个点初始损失值是否合理、下降趋势是否平滑、有没有异常尖峰、训练和验证曲线的差距。对于十分类任务随机猜测的交叉熵损失是log(10)≈2.3。如果初始损失远大于这个值说明初始化有问题如果远小于可能哪里算错了。训练过程中损失应该逐渐下降并趋于平缓。如果一直下降但验证损失上升就是过拟合的信号。我习惯每隔一定步数记录一次损失然后用平滑后的曲线来看趋势。原始曲线往往抖动很大平滑之后更容易看出方向。平滑窗口一般取10到50个点太小了没效果太大了会掩盖细节。注意不要只看最终损失值过程同样重要。一个最终损失很低但中途剧烈震荡的训练往往不如一个损失稍高但平稳下降的训练可靠。6. 从损失函数延伸出去几个值得深挖的方向6.1 损失函数与模型校准模型校准是这两年比较热的话题。简单说就是模型输出的概率是否真的代表置信度。比如模型说“这张图有80%概率是3”那在所有它说80%的样本里是否真的有80%是3交叉熵训练出来的模型往往过度自信输出的概率偏高。这时候可以用温度缩放或者标签平滑来校准。我在实际项目里发现校准后的模型在决策阈值选择上更可靠尤其是需要拒绝低置信度样本的场景。6.2 自定义损失函数的注意事项有时候标准损失函数不够用需要自定义。自定义时要注意几点第一确保损失函数可导否则反向传播会断第二注意数值稳定性该加的保护要加第三先在小批量数据上验证梯度是否正确。我一般会用数值梯度检验的方法把自定义损失函数的梯度和数值近似梯度对比误差在1e-5以内才算通过。6.3 损失函数在多任务学习中的角色多任务学习里多个损失函数需要加权组合。权重怎么设是个学问。简单做法是手动调但费时费力。进阶做法是用不确定性加权或者梯度归一化。我在一个多任务项目里试过不确定性加权让模型自己学习每个任务的权重效果比手动调参好不少。不过这个方法对数据量有要求数据太少时学出来的权重不稳定。损失函数看起来只是训练流程里的一个环节但它的影响贯穿整个模型行为。选对了训练顺风顺水选错了怎么调都别扭。我自己的经验是先把交叉熵吃透理解它的来龙去脉和代码细节再去尝试其他变体这样每一步都走得踏实。手写数字识别虽然简单但作为理解损失函数的载体它足够直观也足够有代表性。把这个小任务里的损失函数搞明白了换到更复杂的任务上思路是一样的。

相关新闻

Qt桌面程序嵌入网页:miniblink轻量WebView封装实战

Qt桌面程序嵌入网页:miniblink轻量WebView封装实战

简介:面向需要在 Windows 平台集成轻量级浏览器的 Qt 与 C 开发者,这份资源提供了基于 miniblink 的网页视图封装示例,同时兼容 mingw 与 VC 两种编译方式,可适用于开发内嵌浏览器、混合界面、桌面客户端中的网页展示模块&#xf…

2026/10/17 15:03:36 阅读更多 →
相机与C-arm联合标定:手术导航的亚毫米级空间对齐

相机与C-arm联合标定:手术导航的亚毫米级空间对齐

1. 这不是调个摄像头那么简单:为什么“相机校参”和“C-arm校参”必须放在一起讲你手头拿到的这个标题——“相机校参及C-arm校参 Camera calibration & X-ray calibration 02”,乍看像两份独立实验报告的拼接,但实际它指向一个临床影像系…

2026/10/16 13:19:22 阅读更多 →
GIS坡度分析实战:从DEM到坡度分级的避坑指南

GIS坡度分析实战:从DEM到坡度分级的避坑指南

国产GIS工具里坡度分析这个功能,我前前后后用了差不多七八年。说实话,最早接触ArcGIS那会儿,我以为坡度分析就是把DEM丢进去点一下“Slope”完事,后来才发现,同样的DEM、同样的工具,不同人做出来的结果能差…

2026/10/16 13:19:22 阅读更多 →

最新新闻

深入解析DNS存根解析器与权威域服务器:从原理到排错实战

深入解析DNS存根解析器与权威域服务器:从原理到排错实战

1. 从一个被忽略的解析环节说起很多人第一次接触DNS,都是从“域名怎么变成IP”这个问题开始的。查资料、看教程,翻来覆去就是递归查询、迭代查询、根域名服务器、顶级域名服务器、权威域名服务器这一套流程。画个图,背下来,考试能…

2026/10/17 15:03:41 阅读更多 →
C++ libcurl实战:静态链接、断点续传与multipart上传全链路解析

C++ libcurl实战:静态链接、断点续传与multipart上传全链路解析

简介:本资源是一份面向C中高级开发者的技术实践包,聚焦于使用libcurl库实现HTTP协议下的文件上传与下载功能,并配套Node.js服务端用于端到端验证,解决跨平台网络通信开发中的核心集成问题。压缩包共872个文件,涵盖227个…

2026/10/17 15:03:41 阅读更多 →
VS Code Git可视化操作:新手从提交到分支协作完整指南

VS Code Git可视化操作:新手从提交到分支协作完整指南

坦白讲,每次有同事或者新手朋友跑过来问我:Git 到底怎么学?我都建议他们别一上来就啃命令行,先在 VS Code 里把 Git 玩明白。VS Code 内置的 Git 能力已经相当完整,而且把所有关键操作都做成了可视化的按钮、面板和提示…

2026/10/17 15:03:41 阅读更多 →
TLS 1.3配置审计、证书锁定绕过与中间人攻击模拟实战

TLS 1.3配置审计、证书锁定绕过与中间人攻击模拟实战

做安全这块的同行应该都有同感:TLS 1.3 铺开之后,很多以前靠"看版本号、查证书有效期"就能过关的检查项突然就不够用了。我最近在给一个内部业务系统做数据传输安全评估时,就遇到过这样的场景——服务器明明只开了 TLS 1.2 和 1.3&…

2026/10/17 15:03:41 阅读更多 →
Docker镜像批量拉取与自动重打标签:私有仓库同步脚本实战

Docker镜像批量拉取与自动重打标签:私有仓库同步脚本实战

拉取镜像自动重打标签,这个需求在部署和交付场景里出现频率比想象中高得多。我自己整理部署环境基础镜像的时候,面对几十个待同步的镜像,最头疼的不是 docker pull,而是手动敲 docker tag 那一串又长又容易出错的标签命令。版本号…

2026/10/17 15:03:41 阅读更多 →
Muse-Glimmer-30B-mxfp8 chat_template 详解:正确书写 System Prompt 与多轮图像对话的完整方法

Muse-Glimmer-30B-mxfp8 chat_template 详解:正确书写 System Prompt 与多轮图像对话的完整方法

【免费下载链接】Muse-Glimmer-30B-mxfp8 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/Muse-Glimmer-30B-mxfp8 点击查看 免费下载 Muse-Glimmer-30B-mxfp8 是一个 300 亿参数的多模态视觉语言模型(image-text-to-text)&…

2026/10/17 15:02:41 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →