2026年4月

  • 目标:理解并实现深度学习中两个最基础的组件——Softmax(用于多分类概率输出)和 MSE(均方误差,用于回归任务)。
  • 学习方式:在 Jupyter Notebook 中逐行敲代码,并完成测试。

今天将学会

  • Softmax 的数学公式与 NumPy 实现(含数值稳定性技巧)

  • MSE 的数学公式与 NumPy 实现

  • 理解它们分别在什么场景使用

  • 用小测试验证你的实现是否正确

第一部分:Softmax(预计 70 分钟)

1.1 Softmax 是什么?(10 分钟)

\int_0^1 x dx = \frac12

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
测试一维向量:
```python
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**:确保函数能正确处理二维输入(批量数据)
```python
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,而用交叉熵?(提示:梯度特性)

Day 2 numpy创建数组、索引、广播

建议用 Jupyter Notebook 边敲边记。启动方式:bash输入

conda activate ai_env
jupyter notebook

然后在浏览器里新建一个 Python 3 笔记本,把今天的所有代码按章节写在一个 .ipynb 文件里,方便复习。

1️⃣ 热身:导入 NumPy 并检查版本(5 分钟)

在第一个 cell 里输入:

import numpy as np
print(np.__version__)

运行(Shift+Enter),看到版本号(如 2.4.3)即可。

2️⃣ 创建数组(40 分钟)

掌握以下 8 种创建方式,每种手打一遍并观察输出。

# 1. 从列表创建
a = np.array([1, 2, 3])
print("1D array:", a)

# 2. 二维数组
b = np.array([[1,2,3], [4,5,6]])
print("2D array:\n", b)

# 3. 全零矩阵
zeros = np.zeros((3, 4))
print("zeros:\n", zeros)

# 4. 全一矩阵
ones = np.ones((2, 3))
print("ones:\n", ones)

# 5. 单位矩阵
eye = np.eye(4)   # 4x4 对角线上为1
print("eye:\n", eye)

# 6. 等间隔数组(类似 range)
ar = np.arange(0, 10, 2)   # 起始0,终止10(不含),步长2
print("arange:", ar)

# 7. 线性间隔(等分)
lin = np.linspace(0, 1, 5)  # 0到1之间均匀取5个点
print("linspace:", lin)

# 8. 随机数
rand = np.random.rand(3, 3)   # 0~1均匀分布
randn = np.random.randn(3, 3) # 标准正态分布
print("random uniform:\n", rand)
print("random normal:\n", randn)

3️⃣ 数组索引与切片(50 分钟)

基本索引(类似 Python 列表)

arr = np.arange(10)
print(arr[0])    # 第一个元素
print(arr[-1])   # 最后一个
print(arr[2:5])  # 索引 2,3,4

二维索引

mat = np.array([[1,2,3], [4,5,6], [7,8,9]])
print(mat[0, 1])    # 第0行第1列 → 2
print(mat[1])       # 第1行 → [4,5,6]
print(mat[:, 0])    # 所有行的第0列 → [1,4,7]
print(mat[0:2, 1:3]) # 0~1行,1~2列 → [[2,3],[5,6]]

花式索引(整数数组索引)

arr = np.array([10, 20, 30, 40, 50])
idx = [0, 2, 4]
print(arr[idx])   # [10 30 50]

# 二维花式索引
mat = np.arange(12).reshape(3, 4)
rows = [0, 2]
cols = [1, 3]
print(mat[rows, cols])   # 取 (0,1) 和 (2,3) 两个元素

布尔索引(非常重要)

arr = np.array([1, 5, 2, 8, 3])
mask = arr > 3
print(mask)               # [False  True False  True False]
print(arr[mask])          # [5 8]  选出所有大于3的元素

# 实际应用:替换
arr[arr > 3] = 99
print(arr)                # [1 99 2 99 3]

练习

randomMat = np.random.rand(5, 5) # 创建一个随机的 5 * 5矩阵
print("原始矩阵:\n", randomMat)
randomMat[randomMat > 0.5] = 0 # # 将所有大于 0.5 的元素替换为 0 
print(randomMat)
print("是否还有大于0.5的元素?", np.any(randomMat > 0.5))
# 原始矩阵:
 [[0.24105789 0.86278547 0.61101981 0.63600007 0.51729033]
 [0.51432014 0.53458585 0.13090915 0.89073187 0.4918729 ]
 [0.88033846 0.14628945 0.92441329 0.21437359 0.14920108]
 [0.80158892 0.41307432 0.09961127 0.22901789 0.98483656]
 [0.6531539  0.62407493 0.3365452  0.80032319 0.93666646]]
[[0.24105789 0.         0.         0.         0.        ]
 [0.         0.         0.13090915 0.         0.4918729 ]
 [0.         0.14628945 0.         0.21437359 0.14920108]
 [0.         0.41307432 0.09961127 0.22901789 0.        ]
 [0.         0.         0.3365452  0.         0.        ]]
是否还有大于0.5的元素? False

Day 1 安装Miniconda, Jupyter, PyTorch

第一步:准备工作

确认电脑类型与芯片类型(这里是mac m5pro)

第二步:安装 Miniconda

Miniconda 是一个轻量级的 Python 环境管理工具,可以避免不同项目之间的依赖冲突 初始化 bash执行 conda init zsh conda --version 验证是否安装成功 执行成功后,请关闭当前终端窗口,并打开一个新的终端窗口,以使配置生效

第三步:安装 PyTorch

创建并激活虚拟环境:在终端中,执行以下命令来创建一个名为 ai_env 的新环境,并指定 Python 版本为 3.11 bash中执行 conda create -n ai_env python=3.11 执行命令激活 conda activate ai_env 激活成功后,你会在终端提示符前面看到 (ai_env) 字样

安装 PyTorch 请访问 PyTorch 官方安装页面。在页面中,选择 Stable (稳定版),操作系统选 Mac,Package (包管理器) 选 Conda,语言选 Python。它会自动生成一个安装命令,通常是 bash 命令

conda install pytorch torchvision torchaudio -c pytorch -c apple

将命令完整地复制并粘贴到终端中执行。系统会列出需要下载和安装的包,输入 y 并回车确认

验证 PyTorch 安装:在终端中,依次输入以下命令 python 这会进入 Python 交互环境。然后,输入

import torch
print(torch.__version__)

如果能看到打印出的 PyTorch 版本号(例如 2.3.0),就说明安装成功了

第四步:安装 Jupyter Notebook

  1. 确保你的终端已经激活了 ai_env 环境(提示符前面有 (ai_env))。如果不是,请先执行 conda activate ai_env
  2. 在终端输入以下命令来安装 Jupyter。 pip install jupyter
  3. 验证并运行 Jupyter:安装完成后,在终端输入以下命令来启动它 jupyter notebook 这条命令会自动在你的默认浏览器中打开 Jupyter Notebook 的主界面。如果一切正常,你就可以开始创建第一个 notebook 文件了。