Python学习(三)


  • 目标:理解并实现深度学习中两个最基础的组件——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.02

2.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个元素的平方误差均值

关键理解点:

  1. 损失函数应当设计成接受 (batch_size, ...) 形状的输入,而不是写两个版本(单样本版和批量版)。
  2. np.mean 不加 axis 参数时,默认对全部元素求平均,天然支持批量。
  3. 但有些损失函数(如分类交叉熵)需要先对每个样本内的维度求和,再跨样本平均,这时就要用 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 对所有元素平均。

扩展思考(如果你有余力)

  1. 实现 交叉熵损失(Cross-Entropy Loss),它常与 Softmax 搭配使用。
  2. 研究一下:为什么分类任务不用 MSE,而用交叉熵?(提示:梯度特性)

声明:Xuhao's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Python学习(三)


Carpe Diem and Do what I like