CSDN博客-第2天-多样本训练与分类边界

【深度学习入门 Day 2】从单样本到多样本:向量化训练与分类边界

本文记录深度学习学习第 2 天的内容:把昨天的单样本 sigmoid 神经元扩展到多样本训练,理解 X @ w + b、平均损失、梯度聚合 X.T @ dL_dz,以及训练出来的 wb 为什么对应一条分类边界。

文章目录

  • 一、从单样本训练到多样本训练
  • 二、多样本数据如何表示
  • 三、向量化前向传播:X @ w + b
  • 四、多个样本的损失怎么合并
  • 五、多样本反向传播:为什么是 X.T @ dL_dz
  • 六、一次多样本参数更新
  • 七、wb 的几何意义:分类边界
  • 八、今天的上机练习代码
  • 九、今日总结
  • 十、课后自测

一、从单样本训练到多样本训练

昨天我们只训练了一个样本:

python 复制代码
x = np.array([2.0, 3.0])
y = 1.0

模型形式是:

text 复制代码
z = w1*x1 + w2*x2 + b
a = sigmoid(z)

这只能说明一件事:

对于 [2, 3] 这个点,让模型输出尽量接近 1

但机器学习真正要做的不是只记住一个点,而是从多个样本中学习规律。今天我们把单个输入 x 扩展为样本矩阵 X


二、多样本数据如何表示

今天使用 4 个二维样本:

python 复制代码
X = np.array([
    [2.0, 3.0],
    [1.0, 1.0],
    [3.0, 4.0],
    [0.0, 2.0],
])

y = np.array([1.0, 0.0, 1.0, 0.0])

其中:

text 复制代码
X.shape = (4, 2)
y.shape = (4,)

含义是:

text 复制代码
4 个样本
每个样本 2 个特征
每个样本对应 1 个标签

参数仍然是:

python 复制代码
w = np.array([0.5, -1.0])
b = 1.0

因为每个样本有 2 个特征,所以权重 w 有 2 个分量。


三、向量化前向传播:X @ w + b

单样本时:

python 复制代码
z = np.dot(w, x) + b

多样本时:

python 复制代码
z = X @ w + b

形状关系是:

text 复制代码
X.shape = (4, 2)
w.shape = (2,)
z.shape = (4,)

语义上,z 是 4 个样本各自的线性输出:

text 复制代码
z = [z1, z2, z3, z4]

代入初始参数:

text 复制代码
样本1: z1 = 2*0.5 + 3*(-1) + 1 = -1
样本2: z2 = 1*0.5 + 1*(-1) + 1 = 0.5
样本3: z3 = 3*0.5 + 4*(-1) + 1 = -1.5
样本4: z4 = 0*0.5 + 2*(-1) + 1 = -1

所以:

text 复制代码
z = [-1, 0.5, -1.5, -1]

经过 sigmoid:

text 复制代码
a = sigmoid(z) ≈ [0.269, 0.622, 0.182, 0.269]

对应标签:

text 复制代码
y = [1.000, 0.000, 1.000, 0.000]

当前预测情况:

text 复制代码
样本1:预测 0.269,标签 1,偏低,希望输出变大
样本2:预测 0.622,标签 0,偏高,希望输出变小
样本3:预测 0.182,标签 1,偏低,希望输出变大
样本4:预测 0.269,标签 0,偏高,希望输出变小

这就是多样本训练和单样本训练的本质差别:

同一组参数要同时照顾多个样本,最终更新方向是多个样本意见的综合。


四、多个样本的损失怎么合并

单样本平方损失:

text 复制代码
L = (a - y)^2

多样本时通常取平均:

python 复制代码
loss = np.mean((a - y) ** 2)

数学上可以写成:

text 复制代码
loss = 1/N * Σ(ai - yi)^2

其中 N 是样本数量。

为什么要取平均?

因为我们希望优化的是整体效果,而不是只服务某一个样本。如果只看单个样本,参数可能朝着对其他样本更差的方向更新。


五、多样本反向传播:为什么是 X.T @ dL_dz

平方损失下,反向传播可以写成:

python 复制代码
N = len(y)

dL_da = 2 * (a - y) / N
da_dz = a * (1 - a)
dL_dz = dL_da * da_dz

这里的 dL_dz 是每个样本对 z 的梯度信号:

text 复制代码
dL_dz.shape = (4,)

近似计算得到:

text 复制代码
dL_dz ≈ [-0.072, 0.073, -0.061, 0.027]

符号含义:

text 复制代码
负数:希望对应样本的 z 变大
正数:希望对应样本的 z 变小

权重梯度是:

python 复制代码
dL_dw = X.T @ dL_dz

形状关系:

text 复制代码
X.shape       = (4, 2)
X.T.shape     = (2, 4)
dL_dz.shape   = (4,)

dL_dw.shape   = (2,)

也就是得到:

text 复制代码
[dL/dw1, dL/dw2]

具体展开:

text 复制代码
X 第一列 = [2, 1, 3, 0]
X 第二列 = [3, 1, 4, 2]

所以:

text 复制代码
dL/dw1 = 2*dL_dz1 + 1*dL_dz2 + 3*dL_dz3 + 0*dL_dz4

dL/dw2 = 3*dL_dz1 + 1*dL_dz2 + 4*dL_dz3 + 2*dL_dz4

这就是 X.T @ dL_dz 的核心含义:

把每个样本的误差信号,按照每个特征列加权汇总,得到每个权重的梯度。

偏置梯度更简单:

python 复制代码
dL_db = np.sum(dL_dz)

因为每个样本的 z 都加了同一个 b,所以偏置梯度就是所有样本误差信号的总和。


六、一次多样本参数更新

近似有:

text 复制代码
dL_dz ≈ [-0.072, 0.073, -0.061, 0.027]

计算 w1 梯度:

text 复制代码
dL/dw1 = 2*(-0.072) + 1*(0.073) + 3*(-0.061) + 0*(0.027)
        = -0.144 + 0.073 - 0.183 + 0
        ≈ -0.254

计算 w2 梯度:

text 复制代码
dL/dw2 = 3*(-0.072) + 1*(0.073) + 4*(-0.061) + 2*(0.027)
        = -0.216 + 0.073 - 0.244 + 0.054
        ≈ -0.333

计算 b 梯度:

text 复制代码
dL/db = -0.072 + 0.073 - 0.061 + 0.027
      ≈ -0.033

设学习率:

text 复制代码
lr = 0.1

初始参数:

text 复制代码
w1 = 0.5
w2 = -1
b  = 1

更新:

text 复制代码
w1_new = 0.5 - 0.1*(-0.254)  = 0.5254
w2_new = -1  - 0.1*(-0.333)  = -0.9667
b_new  = 1   - 0.1*(-0.033)  = 1.0033

注意:w2-1 变成 -0.9667 也是变大,因为它没有那么负了。


七、wb 的几何意义:分类边界

对于二维输入:

text 复制代码
z = w1*x1 + w2*x2 + b

预测规则是:

text 复制代码
z > 0 -> sigmoid(z) > 0.5 -> 预测 1
z = 0 -> sigmoid(z) = 0.5 -> 在边界上
z < 0 -> sigmoid(z) < 0.5 -> 预测 0

因此分类边界是:

text 复制代码
w1*x1 + w2*x2 + b = 0

这是一条直线。

把它改写成 x2 = k*x1 + c 的形式:

text 复制代码
w2*x2 = -w1*x1 - b
x2 = -(w1/w2)*x1 - b/w2

所以:

text 复制代码
斜率 = -w1 / w2
截距 = -b / w2

这里要注意:

b 不是斜率。w1w2 决定直线方向,b 控制直线整体平移。

举例,初始参数:

text 复制代码
w1 = 0.5
w2 = -1
b = 1

分类边界:

text 复制代码
0.5*x1 - x2 + 1 = 0
x2 = 0.5*x1 + 1

对于点 [3, 4]

text 复制代码
边界上的 x2 = 0.5*3 + 1 = 2.5
实际 x2 = 4

它在直线上方。但是否预测为 1,不能只看"上方"两个字,还要看 z 的符号:

text 复制代码
z = 0.5*3 - 4 + 1 = -1.5 < 0

所以初始模型预测为 0

一个可以分开这组数据的边界是:

text 复制代码
x1 + x2 - 4 = 0

对应:

text 复制代码
w = [1, 1]
b = -4

检查:

text 复制代码
[2, 3]: 2+3-4 = 1  -> 预测 1
[3, 4]: 3+4-4 = 3  -> 预测 1
[1, 1]: 1+1-4 = -2 -> 预测 0
[0, 2]: 0+2-4 = -2 -> 预测 0

所以训练的目标可以理解为:

找到一组 wb,让正类和负类尽量落在分类边界的不同侧。


八、今天的上机练习代码

python 复制代码
import numpy as np

# 4 个样本,每个样本 2 个特征
X = np.array([
    [2.0, 3.0],
    [1.0, 1.0],
    [3.0, 4.0],
    [0.0, 2.0],
])

# 标签:1 表示正类,0 表示负类
y = np.array([1.0, 0.0, 1.0, 0.0])

# 初始化参数
w = np.array([0.5, -1.0])
b = 1.0
lr = 0.1


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


for step in range(101):
    # forward
    z = X @ w + b
    a = sigmoid(z)
    loss = np.mean((a - y) ** 2)

    # backward
    N = len(y)

    dL_da = 2 * (a - y) / N
    da_dz = a * (1 - a)
    dL_dz = dL_da * da_dz

    dL_dw = X.T @ dL_dz
    dL_db = np.sum(dL_dz)

    # update
    w = w - lr * dL_dw
    b = b - lr * dL_db

    if step % 10 == 0:
        pred = (a >= 0.5).astype(int)
        print(
            f"step={step:03d}, "
            f"loss={loss:.6f}, "
            f"a={np.round(a, 3)}, "
            f"pred={pred}, "
            f"w={np.round(w, 4)}, "
            f"b={b:.4f}"
        )

重点观察:

text 复制代码
loss 是否下降
a 是否逐渐靠近 y
pred 是否从错误变正确
w 和 b 如何变化

尤其要盯住这行:

python 复制代码
dL_dw = X.T @ dL_dz

它是多样本训练从"逐样本计算"走向"矩阵化计算"的关键。


九、今日总结

今天的核心内容可以压缩成 4 点:

  1. 单样本输入 x 扩展为多样本矩阵 X
  2. 前向传播从 np.dot(w, x) + b 变成 X @ w + b
  3. 多样本梯度通过 X.T @ dL_dz 聚合。
  4. 二维 sigmoid 神经元学到的 wb 对应一条分类直线。

最终要记住这句话:

多样本训练不是让某一个点输出正确,而是找到一组参数,让整体平均损失下降,并让不同类别尽量落在分类边界的不同侧。


十、课后自测

  1. 为什么 X @ w + b 的结果是 4 个 z,而不是 1 个?
  2. X.T @ dL_dz 为什么能得到 [dL/dw1, dL/dw2]
  3. 为什么 w1*x1 + w2*x2 + b = 0 是分类边界?
  4. b 是斜率吗?如果不是,斜率和截距分别是什么?
  5. 如果 z = -2,sigmoid 输出大于 0.5 还是小于 0.5?预测为哪一类?