上一期我们手算了 softmax:把一堆分数变成概率,加起来等于 1。概率有了,新的问题来了:训练的时候,怎么判断模型"猜得好不好"?光看答对没答对太粗糙。90% 把握答对,和 51% 把握蒙对,都是"答对",能一样吗?今天讲交叉熵损失,它就是干这个的:专门衡量模型给出的概率"有多靠谱"。先给直觉。交叉熵只盯一件事:正确答案那个位置,模型给了多少概率。公式长这样:loss = -Σ y log p。分类任务里 y 是 one-hot,正确答案的位置是 1,其余是 0,公式就塌缩成 loss = -log(p_正确)。模型在正确答案上给的概率越高,loss 越小;给的概率越低,loss 越大。就这么简单粗暴。环境还是 numpy 一个包。今天 3 个实验,全部手算,跑一遍就记住。实验一:同样的正确答案,模型"自信答对"和"自信答错",loss 差多少。正确答案是第 0 类。第一种情况模型押对了,分数 [3.0, 1.0, 0.2],softmax 之后概率大约是 [0.836, 0.113, 0.051]。第二种情况模型押错了,分数 [0.2, 1.0, 3.0],概率大约是 [0.051, 0.113, 0.836]。注意看,两种情况只是把最大概率从第 0 类挪到了第 2 类import numpy as np def softmax(z): z = np.asarray(z, dtype=float) z = z - z.max() # 数值稳定:先减最大值 e = np.exp(z) return e / e.sum() def cross_entropy(p, y): eps = 1e-12 p = np.clip(p, eps, 1.0) # 避开 log(0),后面讲为什么 return -np.sum(y * np.log(p)) y = np.array([1, 0, 0]) p_right = softmax([3.0, 1.0, 0.2]) p_wrong = softmax([0.2, 1.0, 3.0]) print("答对:", np.round(p_right, 3), "loss =", round(float(cross_entropy(p_right, y)), 4)) print("答错:", np.round(p_wrong, 3), "loss =", round(float(cross_entropy(p_wrong, y)), 4))输出:答对 loss = 0.1791,答错 loss = 2.9791。差了 16 倍还多。交叉熵最狠的地方就在这:它不只看你答没答对,还看你错的时候有多自信。51% 把握答错,loss 也就 0.67;83% 把握答错,loss 直接干到 2.98。训练就是在逼模型:没把握就别瞎自信。实验二:训练时梯度长什么样。交叉熵配 softmax 有个漂亮的结论:loss 对原始分数(logits)的梯度,就是 p - y,一行搞定。直觉很好懂:正确答案的位置 y=1,梯度是 p-1,永远是负数,梯度下降会把这个分数往上推;答错的位置 y=0,梯度是 p,正数,往下压。推拉之间,概率分布就往正确答案靠。不信可以数值验证:z = np.array([2.0, 1.0, 0.1]) y2 = np.array([0, 1, 0]) p = softmax(z) analytic = p - y2 # 解析梯度:一行 h = 1e-6 numeric = np.zeros_like(z) for i in range(len(z)): # 数值梯度:硬算验证 zp, zm = z.copy(), z.copy() zp[i] += h; zm[i] -= h numeric[i] = (cross_entropy(softmax(zp), y2) - cross_entropy(softmax(zm), y2)) / (2 * h) print("解析梯度:", np.round(analytic, 6)) print("数值梯度:", np.round(numeric, 6)) print("最大误差:", float(np.abs(analytic - numeric).max()))输出两行梯度完全一致,最大误差 2e-10,基本就是浮点噪声。这个 p - y 的结论,面试被问烂了,但自己手算验证过一次,比背一百遍管用。实验三:一个 batch 怎么算。实际训练一次喂几十上百个样本,每个样本一个 loss,最后要合成一个数。有两种合法规:mean(取平均)和 sum(求和)。我跑了个 3 样本的 batch,逐样本 loss 是 [0.1791, 0.22, 0.1323],mean 得到 0.1772,sum 得到 0.5315。选哪个?推荐 mean。mean 不随 batch size 变化,学习率调好一次就行;sum 的话,batch 从 32 换成 128,loss 直接翻 4 倍,学习率得重调。新手最常见的坑就是换了 batch size 忘了这茬,训练直接崩还有个经典坑:log(0)。softmax 的概率可能被压到非常接近 0,log(0) 是负无穷,loss 直接变 inf,接着就是满屏 nan。上面代码里 np.clip(p, 1e-12, 1.0) 就是干这个的,把概率钳在 [1e-12, 1] 之间。PyTorch 的 CrossEntropyLoss 内部也是这么处理的,你手写的时候别漏了。几个新手必踩的坑再点一遍。第一,忘记先 softmax,直接拿原始分数算交叉熵,公式里 log 的对象都不是概率,算出来全是错的。第二,mean 和 sum 混用还不调学习率,batch size 一换训练就崩。第三,标签不是 one-hot 的情况,比如 label smoothing 把标签抹成 [0.9, 0.05, 0.05],公式照样能用,y 直接换成这个概率分布就行,交叉熵本来衡量的就是两个分布的距离。收藏这篇,交叉熵这套东西,面试、调模型、看论文天天见。公式背下来没用,3 个实验跑一遍,下次看到 loss 曲线抖一下,你大概能猜到是哪里的问题。下期预告:ML数学基础第5期,讲"反向传播的链式法则"。p - y 只是最后一层的梯度,前面几十层的梯度怎么一层层传回去?NumPy 手写一个两层网络,亲眼看梯度流动。关注我,下期不错过。留个问题:你有没有遇到过 loss 突然变成 nan 的情况?最后查出来是什么原因?评论区说说,看看大家踩的是不是同一个坑。Cyrus
上一期我们手算了 softmax:把一堆分数变成概率,加起来等于 1。概率有了,新的问题来了:训练的时候,怎么判断模型"猜得好不好"?光看答对没答对太粗糙。90% 把握答对,和 51% 把握蒙对,都是"答对",能一样吗?今天讲交叉熵损失,它就是干这个的:专门衡量模型给出的概率"有多靠谱"。先给直觉。交叉熵只盯一件事:正确答案那个位置,模型给了多少概率。公式长这样:loss = -Σ y log p。分类任务里 y 是 one-hot,正确答案的位置是 1,其余是 0,公式就塌缩成 loss = -log(p_正确)。模型在正确答案上给的概率越高,loss 越小;给的概率越低,loss 越大。就这么简单粗暴。环境还是 numpy 一个包。今天 3 个实验,全部手算,跑一遍就记住。实验一:同样的正确答案,模型"自信答对"和"自信答错",loss 差多少。正确答案是第 0 类。第一种情况模型押对了,分数 [3.0, 1.0, 0.2],softmax 之后概率大约是 [0.836, 0.113, 0.051]。第二种情况模型押错了,分数 [0.2, 1.0, 3.0],概率大约是 [0.051, 0.113, 0.836]。注意看,两种情况只是把最大概率从第 0 类挪到了第 2 类import numpy as np def softmax(z): z = np.asarray(z, dtype=float) z = z - z.max() # 数值稳定:先减最大值 e = np.exp(z) return e / e.sum() def cross_entropy(p, y): eps = 1e-12 p = np.clip(p, eps, 1.0) # 避开 log(0),后面讲为什么 return -np.sum(y * np.log(p)) y = np.array([1, 0, 0]) p_right = softmax([3.0, 1.0, 0.2]) p_wrong = softmax([0.2, 1.0, 3.0]) print("答对:", np.round(p_right, 3), "loss =", round(float(cross_entropy(p_right, y)), 4)) print("答错:", np.round(p_wrong, 3), "loss =", round(float(cross_entropy(p_wrong, y)), 4))输出:答对 loss = 0.1791,答错 loss = 2.9791。差了 16 倍还多。交叉熵最狠的地方就在这:它不只看你答没答对,还看你错的时候有多自信。51% 把握答错,loss 也就 0.67;83% 把握答错,loss 直接干到 2.98。训练就是在逼模型:没把握就别瞎自信。实验二:训练时梯度长什么样。交叉熵配 softmax 有个漂亮的结论:loss 对原始分数(logits)的梯度,就是 p - y,一行搞定。直觉很好懂:正确答案的位置 y=1,梯度是 p-1,永远是负数,梯度下降会把这个分数往上推;答错的位置 y=0,梯度是 p,正数,往下压。推拉之间,概率分布就往正确答案靠。不信可以数值验证:z = np.array([2.0, 1.0, 0.1]) y2 = np.array([0, 1, 0]) p = softmax(z) analytic = p - y2 # 解析梯度:一行 h = 1e-6 numeric = np.zeros_like(z) for i in range(len(z)): # 数值梯度:硬算验证 zp, zm = z.copy(), z.copy() zp[i] += h; zm[i] -= h numeric[i] = (cross_entropy(softmax(zp), y2) - cross_entropy(softmax(zm), y2)) / (2 * h) print("解析梯度:", np.round(analytic, 6)) print("数值梯度:", np.round(numeric, 6)) print("最大误差:", float(np.abs(analytic - numeric).max()))输出两行梯度完全一致,最大误差 2e-10,基本就是浮点噪声。这个 p - y 的结论,面试被问烂了,但自己手算验证过一次,比背一百遍管用。实验三:一个 batch 怎么算。实际训练一次喂几十上百个样本,每个样本一个 loss,最后要合成一个数。有两种合法规:mean(取平均)和 sum(求和)。我跑了个 3 样本的 batch,逐样本 loss 是 [0.1791, 0.22, 0.1323],mean 得到 0.1772,sum 得到 0.5315。选哪个?推荐 mean。mean 不随 batch size 变化,学习率调好一次就行;sum 的话,batch 从 32 换成 128,loss 直接翻 4 倍,学习率得重调。新手最常见的坑就是换了 batch size 忘了这茬,训练直接崩还有个经典坑:log(0)。softmax 的概率可能被压到非常接近 0,log(0) 是负无穷,loss 直接变 inf,接着就是满屏 nan。上面代码里 np.clip(p, 1e-12, 1.0) 就是干这个的,把概率钳在 [1e-12, 1] 之间。PyTorch 的 CrossEntropyLoss 内部也是这么处理的,你手写的时候别漏了。几个新手必踩的坑再点一遍。第一,忘记先 softmax,直接拿原始分数算交叉熵,公式里 log 的对象都不是概率,算出来全是错的。第二,mean 和 sum 混用还不调学习率,batch size 一换训练就崩。第三,标签不是 one-hot 的情况,比如 label smoothing 把标签抹成 [0.9, 0.05, 0.05],公式照样能用,y 直接换成这个概率分布就行,交叉熵本来衡量的就是两个分布的距离。收藏这篇,交叉熵这套东西,面试、调模型、看论文天天见。公式背下来没用,3 个实验跑一遍,下次看到 loss 曲线抖一下,你大概能猜到是哪里的问题。下期预告:ML数学基础第5期,讲"反向传播的链式法则"。p - y 只是最后一层的梯度,前面几十层的梯度怎么一层层传回去?NumPy 手写一个两层网络,亲眼看梯度流动。关注我,下期不错过。留个问题:你有没有遇到过 loss 突然变成 nan 的情况?最后查出来是什么原因?评论区说说,看看大家踩的是不是同一个坑。Cyrus