深度学习复习:NumPy实战、反向传播与两层神经网络全
🔥 深度学习复习: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=1∑N(yi−ti)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=1∑Ntilog(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(x−h)
特点:代码简单、结果可靠、运行速度慢,仅用于梯度校验。
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=dfdL⋅dgdf⋅dxdg
神经网络利用该规则,从损失端由后向前逐层计算所有参数梯度,效率远高于数值微分。
四、基础网络层、两层神经网络实现
采用模块化思想封装网络层,依次实现全连接层、两层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 数值稳定性要点
- Softmax 计算前减去每行最大值,避免指数溢出;
- 交叉熵中
log(y + 1e-8),防止输入为0时报错; - 数值微分
h推荐取值1e-5,过小会引入浮点误差。
七、附录:完整梯度校验代码
使用数值梯度校验反向传播解析梯度,验证代码正确性,梯度差异小于 10 − 5 10^{-5} 10−5 即为合格。
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(" ❌ 梯度校验失败,请检查代码")

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



所有评论(0)