🔥 深度学习复习:NumPy实战、反向传播与两层神经网络全解析

本文记录第三周深度学习学习内容,包含 NumPy 多维运算、Matplotlib 可视化、激活函数、损失函数、数据集划分、数值微分、计算图、反向传播、模块化两层神经网络、梯度校验全套知识点。所有代码附带详细注释,可直接运行,适合入门复习与实验复盘。

一、NumPy 多维数组运算与 Matplotlib 可视化

本周持续巩固 Python 科学计算工具,熟练使用 NumPy 完成数组、矩阵、批量数据运算,结合 Matplotlib 实现函数绘图,同时排查数组维度、广播等常见报错。

1.1 NumPy 核心基础操作

# 导入库
import numpy as np

# 1. 创建不同类型数组
a = np.array([[1, 2, 3], [4, 5, 6]])   # 手动创建 2行3列二维数组
b = np.arange(6).reshape(2, 3)          # 生成0-5并重塑为 2×3 数组
zeros_arr = np.zeros((3, 4))            # 全0数组 3行4列
ones_arr = np.ones((2, 2))              # 全1数组 2行2列

# 2. 数组索引与切片
print("数组a第0行第1列元素:", a[0, 1])
print("数组a所有行,第1~2列:\n", a[:, 1:3])

# 3. 广播机制(不同形状数组运算,自动扩展维度)
c = np.array([10, 20, 30])
print("广播运算结果:\n", a + c)

# 4. 统计运算:按行/按列求和、求均值
print("按列求和:", np.sum(a, axis=0))
print("按行求均值:", np.mean(a, axis=1))

# 5. 矩阵乘法(深度学习核心运算)
x = np.random.randn(3, 4)   # 3行4列
y = np.random.randn(4, 5)   # 4行5列
z = x @ y                   # 矩阵乘法,结果形状:(3, 5)

# 6. 数组形状变换
x_flat = x.reshape(-1)      # 展平为一维数组
print("展平后形状:", x_flat.shape)

请添加图片描述

1.2 Matplotlib 可视化绘图

用于绘制激活函数曲线、损失变化曲线,直观理解函数形态。

import numpy as np
import matplotlib.pyplot as plt

# 生成坐标数据
x = np.linspace(-np.pi, np.pi, 100)
y1 = np.sin(x)
y2 = np.cos(x)

# 绘图
plt.figure(figsize=(8,4))
plt.plot(x, y1, label='sin', linewidth=2)
plt.plot(x, y2, 'r--', label='cos')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Sine and Cosine')
plt.legend()
plt.grid(True)
plt.show()

请添加图描述

2.2 损失函数

损失函数用于衡量模型预测值与真实标签的差距,网络训练目标就是最小化损失。

1)均方误差(MSE):回归任务

公式:
L = 1 N ∑ i = 1 N ( y i − t i ) 2 L = \frac{1}{N}\sum_{i=1}^N (y_i - t_i)^2 L=N1i=1N(yiti)2

2)交叉熵损失:分类任务(配合Softmax使用)

多分类公式:
L = − 1 N ∑ i = 1 N t i log ⁡ ( y i ) L = -\frac{1}{N}\sum_{i=1}^N t_i \log(y_i) L=N1i=1Ntilog(yi)

def mse_loss(y, t):
    """均方误差 MSE:回归任务专用"""
    return np.mean((y - t) ** 2)

def cross_entropy_loss(y, t):
    """
    交叉熵损失:多分类任务专用
    :param y: Softmax 输出概率
    :param t: one-hot 真实标签
    :return: 平均损失值
    """
    # +1e-8 防止 log(0) 出现负无穷报错
    return -np.mean(np.sum(t * np.log(y + 1e-8), axis=1))

2.3 数据集划分

将整体数据拆分为训练集、验证集、测试集

  • 训练集:更新网络参数,学习数据规律
  • 验证集:训练过程中调参
  • 测试集:最终评估模型泛化能力,判断是否过拟合
方式1:调用工具库(简洁常用)
from sklearn.model_selection import train_test_split
import numpy as np

# 模拟数据集:1000个样本,每个样本20维特征,二分类标签
X = np.random.randn(1000, 20)
y = np.random.randint(0, 2, size=1000)

# 第一次划分:分出测试集
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.3, random_state=42
)
# 第二次划分:从剩余数据分出验证集
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, random_state=42
)

# 输出各数据集形状
print(f"训练集形状: {X_train.shape}")
print(f"验证集形状: {X_val.shape}")
print(f"测试集形状: {X_test.shape}")
方式2:手动划分(理解底层逻辑)
def my_train_test_split(x, y, test_ratio=0.2, seed=42):
    """手动实现数据集划分"""
    np.random.seed(seed)
    sample_num = x.shape[0]
    # 打乱样本索引
    shuffle_idx = np.random.permutation(sample_num)
    test_num = int(sample_num * test_ratio)
    # 切分索引
    test_idx = shuffle_idx[:test_num]
    train_idx = shuffle_idx[test_num:]
    return x[train_idx], y[train_idx], x[test_idx], y[test_idx]

# 测试手动划分
X = np.random.randn(200, 10)
y = np.random.randint(0, 3, size=200)
X_train, y_train, X_test, y_test = my_train_test_split(X, y)
print(f"手动划分-训练集:{X_train.shape},测试集:{X_test.shape}")

三、数值微分、计算图与反向传播

3.1 数值微分(数值梯度)

原理:使用中心差分近似求导,公式:
f ′ ( x ) ≈ f ( x + h ) − f ( x − h ) 2 h f'(x) \approx \frac{f(x+h) - f(x-h)}{2h} f(x)2hf(x+h)f(xh)
特点:代码简单、结果可靠、运行速度慢,仅用于梯度校验

def numerical_diff(f, x, h=1e-5):
    """
    数值微分(中心差分)
    :param f: 待求导函数
    :param x: 自变量
    :param h: 微小偏移量
    :return: 近似导数值
    """
    return (f(x + h) - f(x - h)) / (2 * h)

# 测试:函数 f(x) = x^2,在 x=3 处导数理论值为6
if __name__ == "__main__":
    f = lambda x: x ** 2
    res = numerical_diff(f, 3)
    print(f"数值微分结果:{res:.4f},理论结果:6")

3.2 简易计算图演示

计算图将复杂运算拆分为加法、乘法等基础节点,是反向传播的基础。

class MultiplyNode:
    """乘法运算节点"""
    def forward(self, x, y):
        """前向传播:计算 x * y"""
        self.x = x
        self.y = y
        return x * y

    def backward(self, grad):
        """反向传播:链式求导,返回两个输入的梯度"""
        dx = grad * self.y
        dy = grad * self.x
        return dx, grad

# 测试计算图:L = (x + y) * u
if __name__ == "__main__":
    x, y, u = 2.0, 3.0, 4.0
    add_res = x + y          # 加法节点
    mul_node = MultiplyNode()
    L = mul_node.forward(add_res, u)

    # 反向传播(最末端梯度初始为1)
    d_add, d_u = mul_node.backward(1)
    d_x = d_add
    d_y = d_add

    print(f"函数结果 L = {L}")
    print(f"梯度 dL/dx = {d_x}, dL/dy = {d_y}, dL/du = {d_u}")

3.3 反向传播核心思想

基于链式法则:对于复合函数 L = f ( g ( x ) ) L = f(g(x)) L=f(g(x))
d L d x = d L d f ⋅ d f d g ⋅ d g d x \frac{dL}{dx} = \frac{dL}{df} \cdot \frac{df}{dg} \cdot \frac{dg}{dx} dxdL=dfdLdgdfdxdg
神经网络利用该规则,从损失端由后向前逐层计算所有参数梯度,效率远高于数值微分。


四、基础网络层、两层神经网络实现

采用模块化思想封装网络层,依次实现全连接层、两层MLP神经网络。

4.1 全连接层(Affine层)

import numpy as np

class AffineLayer:
    """
    全连接层 / 仿射层
    运算公式:out = x · W + b
    包含前向传播、反向传播、参数更新
    """
    def __init__(self, in_dim, out_dim):
        # 初始化权重与偏置
        self.W = np.random.randn(in_dim, out_dim) * 0.01
        self.b = np.zeros(out_dim)

        # 缓存变量(反向传播使用)
        self.x = None
        self.dW = None   # 权重梯度
        self.db = None   # 偏置梯度

    def forward(self, x):
        """前向传播"""
        self.x = x
        out = np.dot(x, self.W) + self.b
        return out

    def backward(self, dout):
        """
        反向传播
        :param dout: 上一层传回的梯度
        :return: 传给上一层的梯度
        """
        # 计算当前层参数梯度
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        # 向上传递梯度
        dx = np.dot(dout, self.W.T)
        return dx

    def update(self, lr):
        """根据学习率更新权重、偏置"""
        self.W -= lr * self.dW
        self.b -= lr * self.db

4.2 两层神经网络(MLP)

class TwoLayerNet:
    """两层神经网络:输入层 -> 隐藏层(ReLU) -> 输出层"""
    def __init__(self, input_size, hidden_size, output_size):
        # 搭建网络层
        self.layer1 = AffineLayer(input_size, hidden_size)
        self.layer2 = AffineLayer(hidden_size, output_size)

        # 激活函数
        self.activation = relu
        self.activation_deriv = relu_deriv

        # 缓存隐藏层线性结果(反向传播使用)
        self.hidden_linear = None

    def forward(self, x):
        """整体前向传播"""
        # 第一层全连接
        h1 = self.layer1.forward(x)
        self.hidden_linear = h1
        # 激活函数
        h1_act = self.activation(h1)
        # 第二层全连接
        out = self.layer2.forward(h1_act)
        return out

    def backward(self, dout):
        """整体反向传播"""
        # 第二层反向
        dout = self.layer2.backward(dout)
        # 激活函数反向(逐元素相乘)
        dout = dout * self.activation_deriv(self.hidden_linear)
        # 第一层反向
        dout = self.layer1.backward(dout)
        return dout

    def update(self, lr):
        """更新所有层参数"""
        self.layer1.update(lr)
        self.layer2.update(lr)

五、完整训练流程(前向+反向+损失可视化)

整合所有模块,实现数据生成、前向传播、损失计算、反向传播、参数更新、损失绘图完整训练流程。

import numpy as np
import matplotlib.pyplot as plt

# 复用前面函数:relu、relu_deriv、cross_entropy_loss、AffineLayer、TwoLayerNet

# 1. 生成模拟非线性二分类数据集
np.random.seed(42)
N = 100                # 样本总数
X = np.random.randn(N, 2)
# 圆形边界分类规则(非线性可分)
y = (X[:, 0] ** 2 + X[:, 1] ** 2 < 1).astype(int)
# 转为 one-hot 编码标签
t = np.zeros((N, 2))
t[np.arange(N), y] = 1

# 2. 初始化网络与超参数
net = TwoLayerNet(input_size=2, hidden_size=10, output_size=2)
lr = 0.5               # 学习率
epochs = 200           # 训练轮数
loss_history = []      # 记录每轮损失

# 3. 训练循环
for epoch in range(epochs):
    # 前向传播
    logits = net.forward(X)

    # Softmax 归一化(防止指数溢出)
    exp_out = np.exp(logits - np.max(logits, axis=1, keepdims=True))
    probs = exp_out / np.sum(exp_out, axis=1, keepdims=True)

    # 计算交叉熵损失
    loss = cross_entropy_loss(probs, t)
    loss_history.append(loss)

    # 反向传播入口梯度:probs - t(Softmax+交叉熵联合导数)
    dout = probs - t
    net.backward(dout)

    # 参数更新
    net.update(lr)

    # 每隔40轮打印损失
    if epoch % 40 == 0:
        print(f"第{epoch}轮训练,损失值:{loss:.4f}")

# 4. 绘制训练损失曲线
plt.figure(figsize=(8, 4))
plt.plot(loss_history)
plt.xlabel("训练轮数 Epoch")
plt.ylabel("损失值 Loss")
plt.title("训练损失变化曲线")
plt.grid(True)
plt.show()

# 5. 预测函数 & 绘制决策边界
def predict(model, data):
    """模型预测,返回类别索引"""
    logits = model.forward(data)
    exp_out = np.exp(logits - np.max(logits, axis=1, keepdims=True))
    probs = exp_out / np.sum(exp_out, axis=1, keepdims=True)
    return np.argmax(probs, axis=1)

# 生成网格数据绘制分类边界
xx, yy = np.meshgrid(np.linspace(-3, 3, 200), np.linspace(-3, 3, 200))
grid_data = np.c_[xx.ravel(), yy.ravel()]
pred_label = predict(net, grid_data).reshape(xx.shape)

plt.figure(figsize=(6, 6))
plt.contourf(xx, yy, pred_label, alpha=0.6, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.Spectral)
plt.title("模型分类决策边界")
plt.show()

请添加图片描述
请添加图片描述


六、代码调试与常见问题总结

6.1 常见报错与解决方案

报错类型 报错原因 解决办法
矩阵维度不匹配 矩阵行列数不一致,无法相乘 使用 .reshape() / .T 调整数组形状
索引越界 下标超过数组最大长度 打印 shape 核对维度,修正索引
广播失败 数组维度不满足广播规则 使用 np.expand_dims 补充维度

6.2 通用调试技巧

def safe_matmul(A, B):
    """安全矩阵乘法:增加维度断言,提前报错定位问题"""
    assert A.shape[1] == B.shape[0], f"维度不匹配:{A.shape} @ {B.shape}"
    return A @ B

# 调试建议
# 1. 每一步运算后 print(变量.shape) 核对维度
# 2. 使用 assert 断言约束形状
# 3. 复杂逻辑分步拆解运行

6.3 数值稳定性要点

  1. Softmax 计算前减去每行最大值,避免指数溢出;
  2. 交叉熵中 log(y + 1e-8),防止输入为0时报错;
  3. 数值微分 h 推荐取值 1e-5,过小会引入浮点误差。

七、附录:完整梯度校验代码

使用数值梯度校验反向传播解析梯度,验证代码正确性,梯度差异小于 10 − 5 10^{-5} 105 即为合格。

import numpy as np

def numerical_gradient(f, x, h=1e-5):
    """通用数值梯度计算(支持矩阵/数组)"""
    grad = np.zeros_like(x)
    # 遍历数组每一个元素
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        old_val = x[idx]

        # f(x+h)
        x[idx] = old_val + h
        fxh1 = f(x)
        # f(x-h)
        x[idx] = old_val - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = old_val
        it.iternext()
    return grad

# ------------------- 梯度校验主程序 -------------------
if __name__ == "__main__":
    # 构造测试数据
    np.random.seed(42)
    net = TwoLayerNet(3, 4, 2)
    X = np.random.randn(5, 3)                     # 5个样本,3维特征
    t = np.eye(2)[np.random.randint(0, 2, 5)]     # one-hot标签

    # 定义损失函数(传入网络参数计算损失)
    def calc_loss(W0, b0, W1, b1):
        net.layer1.W = W0
        net.layer1.b = b0
        net.layer2.W = W1
        net.layer2.b = b1
        logits = net.forward(X)
        exp_out = np.exp(logits - np.max(logits, axis=1, keepdims=True))
        probs = exp_out / np.sum(exp_out, axis=1, keepdims=True)
        return cross_entropy_loss(probs, t)

    # 保存原始参数
    params = [net.layer1.W, net.layer1.b, net.layer2.W, net.layer2.b]

    # 1. 计算反向传播解析梯度
    _ = net.forward(X)
    exp_out = np.exp(_ - np.max(_, axis=1, keepdims=True))
    probs = exp_out / np.sum(exp_out, axis=1, keepdims=True)
    dout = probs - t
    net.backward(dout)
    analytic_grads = [net.layer1.dW, net.layer1.db, net.layer2.dW, net.layer2.db]

    # 2. 逐参数对比数值梯度 & 解析梯度
    param_names = ["W1", "b1", "W2", "b2"]
    for i, (p, grad_a, name) in enumerate(zip(params, analytic_grads, param_names)):
        # 定义单参数损失函数
        def func(param_flat):
            p_reshaped = param_flat.reshape(p.shape)
            if i == 0:
                return calc_loss(p_reshaped, params[1], params[2], params[3])
            elif i == 1:
                return calc_loss(params[0], p_reshaped, params[2], params[3])
            elif i == 2:
                return calc_loss(params[0], params[1], p_reshaped, params[3])
            else:
                return calc_loss(params[0], params[1], params[2], p_reshaped)

        # 计算数值梯度
        grad_n = numerical_gradient(func, p.flatten()).reshape(p.shape)
        # 计算最大误差
        max_diff = np.max(np.abs(grad_n - grad_a))
        print(f"参数 {name} 最大误差:{max_diff:.2e}")
        if max_diff < 1e-5:
            print("  ✅ 梯度校验通过")
        else:
            print("  ❌ 梯度校验失败,请检查代码")

请添加图片描述


八、本周学习总结与复习建议

  1. 全部代码手动逐行敲写,加深语法与逻辑记忆;
  2. 手动推导两层网络反向传播公式,吃透链式法则;
  3. 替换激活函数、学习率、网络节点数,观察训练效果变化;
  4. 故意制造维度错误,强化问题排查能力;
  5. 结合梯度校验实验,巩固“数值微分+反向传播”两套梯度求解思路。

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐