- 目标:理解并实现深度学习中两个最基础的组件——Softmax(用于多分类概率输出)和 MSE(均方误差,用于回归任务)。
- 学习方式:在 Jupyter Notebook 中逐行敲代码,并完成测试。
今天将学会
Softmax 的数学公式与 NumPy 实现(含数值稳定性技巧)
MSE 的数学公式与 NumPy 实现
理解它们分别在什么场景使用
- 用小测试验证你的实现是否正确
第一部分:Softmax(预计 70 分钟)
1.1 Softmax 是什么?(10 分钟)
Softmax 将一个实数向量转换为概率分布。
对于输入向量 $z=[z_1,z_2,...,z_n]$,Softmax 输出 $s=[s_1,s_2,...,s_n]$:
s_i = \frac{e^{z_i}}{\sum_{j=1}^n e^{z_j}}特点:
- 每个 $s_i$ 在 (0,1) 之间
- 所有 $s_i$ 之和为 1
用途:多分类神经网络的输出层。
1.2 朴素实现(先写一个能工作的版本)
在 Jupyter 中新建 cell,输入:
import numpy as np
def softmax_naive(z):
"""z: 任意形状的 numpy 数组(一维或二维)"""
exp_z = np.exp(z) # np.exp()函数是求e^{x}的值的函数
# exp_z:输入数组(在 softmax 中是经过指数运算的数组,`exp_z = np.exp(...)`)。计算的是 e 的 z 次方,即对数组 `z` 中的每一个元素分别求指数
# axis=-1:指定求和的轴。-1表示【最后一个轴】,这是一种灵活的写法,无论数组是2维,3纬还是更高维,都能准确选中最后一个维度。
# keepdims=True:保持求和后的维度不变(不压缩难度)。求和后,被求和的轴长度会变成1,其他轴形状保持不变。
sum_exp = np.sum(exp_z, axis=-1, keepdims=True) # 沿最后一维求和
return exp_z / sum_exp测试一维向量:
z = np.array([1.0, 2.0, 3.0])
probs = softmax_naive(z)
print("概率:", probs)
print("概率和:", np.sum(probs))输出应接近 [0.09, 0.24, 0.67],和为 1。
测试二维矩阵(每行是一个样本):
Z = np.array([[1, 2, 3],
[1, 1, 1]])
probs = softmax_naive(Z)
print("每行概率:\n", probs)
print("每行和:", np.sum(probs, axis=1))1.3 数值稳定性问题(重要!15 分钟)
当 z中数值很大(如 1000)时, ${e^1000}$ 会溢出(Python 返回 inf)。
解决方法:对每个向量减去其最大值。
推导:
$$ \frac{e^{z_i}}{\sum{e^{z_j}}} = \frac{e^{z_j}-M}{\sum{e^{z_j}-M}},M = max(z) $$
改进后的稳定版本:
def softmax(z):
"""数值稳定的 Softmax"""
# 对最后一维取最大值,保持维度以便广播
z_max = np.max(z, axis=-1, keepdims=True)
exp_z = np.exp(z - z_max)
sum_exp = np.sum(exp_z, axis=-1, keepdims=True)
return exp_z / sum_exp验证稳定性:
z_large = np.array([1000, 1001, 1002])
print(softmax(z_large)) # 不会溢出,输出合理概率1.4 动手实现与测试(30 分钟)
任务 A:自己从头写一遍 softmax 函数(不要复制,手敲)。
任务 B:测试边缘情况
输入全为 0 → 输出应均匀分布
[1/3, 1/3, 1/3]- 输入一个极大值,其余很小 → 极大值位置概率接近 1
z_zero = np.zeros(3)
print(softmax(z_zero)) # [0.33333333, 0.33333333, 0.3333333]
z_extreme = np.array([1000, 0, 0])
print(softmax(z_extreme)) # 第一个接近1,后两个接近0任务 C:确保函数能正确处理二维输入(批量数据)
batch = np.random.randn(4, 10) # 4个样本,10个类别
output = softmax(batch)
print("输入形状:", output.shape) # 应为 (4, 10)
print("每行和:", np.sum(output, axis=1)) # 全为 1第二部分:MSE 损失(预计 40 分钟)
2.1 MSE 是什么?
均方误差(Mean Squared Error)用于回归任务。 数值越小 → 模型越准
给定预测值 $\hat{y}$ 和真实值 $y$(两者形状相同),
$$ \text{MSE} = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2, 其中 n 是元素总数(或者样本数,依习惯)$$
2.2 实现 MSE
def mse(y_pred, y_true):
"""均方误差"""
diff = y_pred - y_true
squared_diff = diff ** 2
return np.mean(squared_diff) # 默认对所有元素求平均测试:
y_true = np.array([1.0, 2.0, 3.0])
y_pred = np.array([1.1, 1.9, 3.2])
loss = mse(y_pred, y_true)
print("MSE:", loss) # 手动计算(0.01 + 0.01 + 0.04)/ 3 = 0.022.3 扩展到批量(可选,但很重要)
在机器学习中,我们通常计算批量样本的平均损失:
def mse_batch(y_pred, y_true):
"""y_pred, y_true 形状都是(batch_size, ...)"""
diff = y_pred - y_true
squared_diff = diff ** 2
# 先对每个样本内的所有维度求和,再对所有样本求平均
return np.mean(squared_diff) # 还是用np.mean 最简单因为 np.mean 默认对所有元素平均,所以直接一样用,==无论数组有多少维度==。
因此这里的mse_batch和上面定义的mse 作用一致。不需要单独写一个 mse_batch,原来的 mse 已经通用。
测试:
batch_true = np.array([[1, 2], [3, 4]])
batch_pred = np.array([[1.1, 1.9], [3.2, 4.1]])
print(mse(batch_pred, batch_true)) # 计算所有4个元素的平方误差均值关键理解点:
- 损失函数应当设计成接受
(batch_size, ...)形状的输入,而不是写两个版本(单样本版和批量版)。 np.mean不加axis参数时,默认对全部元素求平均,天然支持批量。- 但有些损失函数(如分类交叉熵)需要先对每个样本内的维度求和,再跨样本平均,这时就要用
axis参数。而 MSE 恰好所有元素地位平等,所以直接np.mean即可。
2.4 自己手写 MSE 并测试边界情况(10 分钟)
完美预测 → MSE = 0
y_true = np.array([1, 2, 3]) y_pred = np.array([1, 2, 3]) assert mse(y_pred, y_true) == 0.0 # Python 中的断言(assert),用于自动测试你的 `mse` 函数是否正确预测全为 0,真实全为 1 → MSE = 1
y_true = np.ones(5) y_pred = np.zeros(5) # 每个误差平方 = (0-1)^2 = 1, 平均 = 1 asset mse(y_pred, y_true) == 1.0随机数据的手工验证
# 用一个小例子手动计算,验证代码逻辑 y_true = np.array([1.0, 2.0, 3.0]) y_pred = np.array([1.1, 1.9, 3.2]) # 差: [0.1, -0.1, 0.2] → 平方: [0.01, 0.01, 0.04] → 均值 = 0.02 assert np.isclose(mse(y_pred, y_true), 0.02) # np.isclose 判断两个数字 / 数组 是否 “几乎相等” 专门解决:浮点数精度误差的问题形状不一致 → 预期报错或警告?
NumPy 的广播规则:如果两个数组的形状不匹配,但满足可广播条件(从尾部维度对齐,每个维度要么相等,要么其中一个为 1),则不会报错,而是自动扩展。
这对 MSE 通常是危险的,因为你可能无意中计算了错误维度的平均。y_true = np.array([[1, 2], [3, 4]]) # shape (2,2) y_pred = np.array([1, 2]) # shape (2,) # 广播后 y_pred 变成 (2,2),与 y_true 相减得到 (2,2) # 这不会报错,但可能不是你想要的行为边界测试建议:
- 显式检查形状一致性,若不一致则抛出
ValueError。 - 或依赖 NumPy 的行为,但要在文档中明确说明“要求形状完全一致”。
- 显式检查形状一致性,若不一致则抛出
空数组
y_true = np.array([]) y_pred = np.array([]) # np.mean([]) 返回np.nan, 这通常不是期望的处理:在函数开头检查数组大小是否为 0,若为 0 可返回 0.0 或抛出异常。
大数值导致溢出
y_true = np.array([1e200, 1e200]) y_pred = np.array([0, 0]) diff = y_pred - y_true # 得到 [-1e200, -1e200] squared = diff ** 2 # 得到 [inf, inf] → 溢出解决方案:考虑对输入进行缩放,或使用
np.square(diff)并注意数据类型。对于正常机器学习任务(输入通常经过归一化),此问题少见。整数类型输入
y_true = np.array([1, 2, 3], dtype=np.int32) y_pred = np.array([2, 3, 4], dtype=np.int32) # diff 为整数,平方为整数,np.mean 在整数上会执行整数除法(Python 3 中返回浮点数?) # 实际上 np.mean 对整数数组返回 float64,安全。潜在陷阱:手动实现时若使用
sum / len,整数除法会导致截断。使用np.mean则可避免
边界测试代码示例汇总
import numpy as np
def safe_mse(y_pred, y_true):
"""带边界检查的MSE"""
if y_pred.shape != y_true.shape:
raise ValueError(f"Shape mismatch: {y_pred.shape} vs {y_true.shape}")
if y_pred.size == 0:
return 0.0
diff = y_pred - y_true
squared = diff ** 2
return np.mean(squared)
# 测试边界
def test_mse_boundaries():
# 正常情况
assert safe_mse(np.array([1,2,3]), np.array([1,2,3])) == 0.0
assert safe_mse(np.zeros(5), np.ones(5)) == 1.0
# 形状不一致应报错
try:
safe_mse(np.array([1,2]), np.array([[1,2],[3,4]]))
except ValueError:
print("形状检查通过")
# 空数组
assert safe_mse(np.array([]), np.array([])) == 0.0
# 高维
y_true = np.random.rand(2,3,4)
y_pred = y_true.copy()
assert safe_mse(y_pred, y_true) == 0.0
print("所有边界测试通过")第三部分:综合小项目(30 分钟)
任务:手动实现一个线性回归的损失计算
假设你有一个简单的线性模型:y_pred = w * x + b
给定 x 和真实 y,以及随机的 w, b,计算 MSE 损失。
# 生成伪数据
np.random.seed(42)
x = np.random.randn(100, 1) # 100个样本,1个特征
true_w = 2.5
true_b = 1.0
y_true = true_w * x + true_b + 0.1 * np.random.randn(100, 1) # 加噪声
# 随机初始化参数
w = np.random.randn(1)
b = np.random.randn(1)
# 前向传播
y_pred = w * x + b
# 计算损失
loss = mse(y_pred, y_true)
print(f"初始损失:{loss:.4f}")
# 可以手动调整w和b 看看损失如何变化
w = 2.5
b = 1.0
y_pred = w * x + b
loss = mse(y_pred, y_true)
print(f"最优参数下的损失: {loss:.4f}")这个练习可以看到:损失函数是用来衡量模型好坏的,后续会用梯度下降自动优化 w, b。
第四部分:检查点与扩展思考(15 分钟)
今日完成清单
- 能独立写出
softmax(稳定版本) - 能解释为什么要减去最大值
- 能独立写出
mse函数 - 完成了线性回归的损失计算小项目
常见错误与解答
Q1: Softmax 输出不是概率(有负数或和不等于 1)
A: 检查是否忘记除以 sum_exp,或者分母求和时 axis 是否正确。
Q2: 我的 softmax 处理二维数组时报错
A: 确保在 np.max 和 np.sum 中设置了 axis=-1, keepdims=True,否则形状不对。
Q3: MSE 的结果是标量还是数组?
A: 应该是标量(单一数值),因为你用了 np.mean 对所有元素平均。
扩展思考(如果你有余力)
- 实现 交叉熵损失(Cross-Entropy Loss),它常与 Softmax 搭配使用。
- 研究一下:为什么分类任务不用 MSE,而用交叉熵?(提示:梯度特性)

Comments | NOTHING