从梯度下降到神经网络学习 —— 《深度学习入门》前四章
第一部分:理论理解与问题回答
一、学习与模型(第 1 章)
Q1. 神经网络训练过程中,哪些量是已知的,哪些量是未知的?学习的目标到底是什么?
-
已知量:训练数据集的输入特征(如 MNIST 的像素矩阵)和标签(如数字 0-9 的真实类别)、神经网络的网络结构(层数、每层神经元数)、激活函数 / 损失函数 / 优化器的选择。
-
未知量:权重参数W和偏置参数b
-
学习目标:通过在训练数据上的迭代优化,找到最优的参数(W,b),使得神经网络未知的测试数据也能做出准确的预测,即实现泛化,让模型的预测输出尽可能接近真实标签,最小化预测误差。
二、线性模型与非线性(第 2 章)
Q2. 为什么单层感知机只能解决线性可分问题?
-
单层感知机的决策边界是线性的,只能处理线性可分的二分类问题,对于非线性问题(如异或门问题),不存在任何超平面能实现完全分隔,因此单层感知机无法解决
Q3. 为什么必须引入非线性激活函数?
-
神经网络的核心目标是拟合现实世界中的非线性复杂规律(如图像、语音、文本的特征映射),而如果仅由线性变换堆叠,无论多少层,最终的整体变换仍为线性变换,无法拟合非线性关系,此时深层网络与单层线性模型没有区别
如果把神经网络中所有激活函数都去掉,会发生什么?
-
任意深度的神经网络退化为单层线性模型,只能解决线性可分问题,无法拟合复杂的非线性数据,失去实际应用价值。
三、神经网络的前向计算(第 3 章)
Q4. 神经网络的前向传播,本质上在做什么数学运算?
-
整个过程是从输入到输出的正向复合映射,核心数学运算为矩阵乘法(实现批量特征的线性变换)和逐元素的非线性运算(激活函数)
Q5. 为什么分类问题中,输出层通常使用 Softmax?
-
分类问题需要模型输出每个类别的概率,且所有类别的概率和为1,方便后续通过概率大小判断预测类别,而Softmax函数恰好满足这一需求
Softmax 在概率意义上做了什么?
-
将无界的logit转换为合法的概率分布,即[0,1],同时 Softmax 输出的不是绝对概率,而是相对概率
四、损失函数与梯度(第 4 章 · 核心)
Q6. 为什么准确率不能作为训练时的优化目标?
-
准确率是离散的、非可微的函数,而神经网络的训练依赖梯度下降,要求优化目标是连续可微(或分段可微)的函数,只有可微的函数才能计算梯度,进而通过梯度更新参数
为什么必须引入损失函数?
-
损失函数是连续可微的,能计算梯度,为梯度下降提供参数更新的方向;损失函数能精细量化模型的预测误差
五、梯度下降的本质(第 4 章 · 灵魂)
Q7. 梯度在几何意义上代表什么?
-
梯度是损失函数在当前参数点处的方向导数最大值方向,即函数值增长最快的方向,其模长表示该方向上升速率,模长越大,上升越快
为什么沿着负梯度方向更新参数?
-
神经网络的训练目标是最小化损失函数,需要找到损失函数的最小值点。由于梯度方向是上升最快的方向,那么负梯度方向就是损失函数下降最快的方向
Q8. 学习率在梯度下降中起什么作用?
-
学习率η是梯度下降的步长因子,决定了每次沿着负梯度方向更新参数时的步长大小,核心作用是平衡模型的收敛速度和收敛稳定性
学习率过大会怎样?
-
参数更新步长过大,会在损失函数的最小值点附近来回震荡,甚至远离最小值点,导致损失值始终无法下降,模型不收敛
过小又会怎样?
-
参数更新步长过小,需要极多的训练轮次才能接近最小值点,训练效率极低
第二部分:手写神经网络与手写数字识别
要求
任务 1:激活函数
使用 numpy 实现:Sigmoid,ReLU,Softmax
任务 2:损失函数
实现:交叉熵损失(支持 batch 输入)
任务 3:数值梯度
任务 4:实现一个神经网络
任务 5:模型训练
数据集:MNIST
优化方法:SGD
任务 6:实验观察与分析
训练过程中请记录并绘图。
完整代码
# 安装依赖
# pip install numpy matplotlib scikit-learn pandas
# 导入库
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
import time
# 全局设置
plt.rcParams['font.sans-serif'] = ['SimHei'] # 显示中文
plt.rcParams['axes.unicode_minus'] = False # 显示负号
np.random.seed(42) # 固定随机种子,保证实验可复现
# 加载MNIST数据集
print("加载MNIST数据集...")
mnist = fetch_openml('mnist_784', version=1, cache=True, as_frame=False)
X = mnist.data
y = mnist.target.astype(np.int32)
# 数据预处理
X = X / 255.0 # 像素归一化到[0,1],加速训练
X = X.astype(np.float32)
# 标签独热编码(用于交叉熵损失计算)
encoder = OneHotEncoder(sparse_output=False)
y_one_hot = encoder.fit_transform(y.reshape(-1, 1))
# 划分训练集和测试集(60000训练,10000测试)
X_train, X_test, y_train, y_test, y_train_one_hot, y_test_one_hot = train_test_split(
X, y, y_one_hot, test_size=1 / 7, random_state=42
)
# 查看数据形状
print(f"训练集输入:{X_train.shape}, 训练集标签(独热):{y_train_one_hot.shape}")
print(f"测试集输入:{X_test.shape}, 测试集标签(独热):{y_test_one_hot.shape}")
# ===================== 激活函数 =====================
class Activation:
@staticmethod
def sigmoid(x):
"""Sigmoid激活函数,带数值稳定性优化"""
return np.where(x >= 0,
1 / (1 + np.exp(-x)),
np.exp(x) / (1 + np.exp(x)))
@staticmethod
def sigmoid_grad(x):
"""Sigmoid梯度"""
return Activation.sigmoid(x) * (1 - Activation.sigmoid(x))
@staticmethod
def relu(x):
"""ReLU激活函数"""
return np.maximum(0, x)
@staticmethod
def relu_grad(x):
"""ReLU梯度"""
return np.where(x > 0, 1, 0)
@staticmethod
def softmax(x):
"""Softmax激活函数,带数值稳定性优化(防止指数爆炸)"""
if x.ndim == 2:
x = x - np.max(x, axis=1, keepdims=True) # 按行减最大值
exp_x = np.exp(x)
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
else:
x = x - np.max(x)
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
# ===================== 损失函数 =====================
class Loss:
@staticmethod
def cross_entropy_error(y_pred, y_true):
"""
交叉熵损失,支持batch输入
:param y_pred: 模型预测的概率分布 (batch_size, n_class)
:param y_true: 真实标签(独热编码)(batch_size, n_class)
:return: 批次的平均损失
"""
if y_pred.ndim == 1:
y_pred = y_pred.reshape(1, y_pred.size)
y_true = y_true.reshape(1, y_true.size)
# 防止log(0)出现,加极小值epsilon
epsilon = 1e-7
batch_size = y_pred.shape[0]
# 仅取真实标签对应的预测概率计算损失
loss = -np.sum(y_true * np.log(y_pred + epsilon)) / batch_size
return loss
# 实例化
act = Activation()
loss_fn = Loss()
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_decay=0.01):
"""
两层全连接神经网络(输入层→隐藏层→输出层)
:param input_size: 输入维度(MNIST为784)
:param hidden_size: 隐藏层神经元数
:param output_size: 输出维度(MNIST为10)
:param weight_decay: 权重衰减系数(L2正则化)
"""
# 初始化参数:He初始化(适用于ReLU)
self.params = {}
self.params['W1'] = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
self.params['b2'] = np.zeros(output_size)
self.weight_decay = weight_decay
def predict(self, x):
"""前向传播,预测概率"""
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
# 输入层→隐藏层:线性变换+ReLU
self.z1 = np.dot(x, W1) + b1
self.a1 = act.relu(self.z1)
# 隐藏层→输出层:线性变换+Softmax
self.z2 = np.dot(self.a1, W2) + b2
y = act.softmax(self.z2)
return y
def loss(self, x, t):
"""计算损失:前向传播+交叉熵+L2正则化"""
y = self.predict(x)
# 交叉熵损失
data_loss = loss_fn.cross_entropy_error(y, t)
# L2正则化项
reg_loss = 0.5 * self.weight_decay * (
np.sum(self.params['W1'] ** 2) + np.sum(self.params['W2'] ** 2)
)
return data_loss + reg_loss
def accuracy(self, x, t):
"""计算准确率(用于评价模型,非优化目标)"""
y = self.predict(x)
y_pred = np.argmax(y, axis=1)
t_true = np.argmax(t, axis=1)
acc = np.sum(y_pred == t_true) / float(x.shape[0])
return acc
def gradient(self, x, t):
"""
通过反向传播计算梯度
:param x: 输入数据
:param t: 真实标签
:return: 各参数的梯度字典
"""
batch_size = x.shape[0]
# 前向传播(确保缓存中间变量)
y = self.predict(x)
# 反向传播
grads = {}
# 输出层梯度
dy = (y - t) / batch_size # Softmax + Cross Entropy的梯度简化形式
grads['W2'] = np.dot(self.a1.T, dy) + self.weight_decay * self.params['W2']
grads['b2'] = np.sum(dy, axis=0)
# 隐藏层梯度
da1 = np.dot(dy, self.params['W2'].T)
dz1 = da1 * act.relu_grad(self.z1)
grads['W1'] = np.dot(x.T, dz1) + self.weight_decay * self.params['W1']
grads['b1'] = np.sum(dz1, axis=0)
return grads
# 保留数值梯度方法用于调试(可选)
def numerical_gradient(self, x, t):
"""
计算所有参数的数值梯度(仅用于梯度检查)
"""
def loss_W(params_dict):
# 临时保存原始参数
original_params = self.params.copy()
# 更新参数
self.params = params_dict
# 计算损失
loss_val = self.loss(x, t)
# 恢复原始参数
self.params = original_params
return loss_val
grads = {}
for key in ['W1', 'b1', 'W2', 'b2']:
grads[key] = self._numerical_gradient_array(loss_W, self.params[key], key)
return grads
def _numerical_gradient_array(self, f, x, param_name):
"""辅助函数:计算单个参数的数值梯度"""
h = 1e-4
grad = np.zeros_like(x)
# 对于大矩阵,只采样部分点进行梯度检查
if x.size > 1000 and param_name.startswith('W'):
# 随机选择100个位置进行梯度检查
idx_i = np.random.randint(0, x.shape[0], 100)
idx_j = np.random.randint(0, x.shape[1], 100)
for i, j in zip(idx_i, idx_j):
tmp_val = x[i, j]
# 计算f(x+h)
x[i, j] = tmp_val + h
fxh1 = f(self.params)
# 计算f(x-h)
x[i, j] = tmp_val - h
fxh2 = f(self.params)
grad[i, j] = (fxh1 - fxh2) / (2 * h)
# 恢复原始值
x[i, j] = tmp_val
else:
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(self.params)
x[idx] = tmp_val - h
fxh2 = f(self.params)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val
it.iternext()
return grad
# 实例化神经网络(MNIST专用)
net = TwoLayerNet(input_size=784, hidden_size=128, output_size=10, weight_decay=0.0001)
# ===================== 训练超参数 =====================
train_epochs = 20 # 训练轮次
batch_size = 128 # 批次大小
lr = 0.1 # 学习率
lr_decay = 0.95 # 学习率衰减
train_size = X_train.shape[0]
# ===================== 训练过程记录 =====================
train_loss_list = []
train_acc_list = []
test_acc_list = []
best_test_acc = 0.0
# 每个轮次的迭代数
iter_per_epoch = max(train_size // batch_size, 1)
# ===================== 开始训练 =====================
print("开始训练神经网络...")
print(f"训练样本数: {train_size}, 批次大小: {batch_size}, 每轮迭代: {iter_per_epoch}")
start_time = time.time()
for epoch in range(train_epochs):
# 学习率衰减
current_lr = lr * (lr_decay ** epoch)
# 随机打乱训练集
idx = np.random.permutation(train_size)
X_train_shuffle = X_train[idx]
y_train_shuffle = y_train_one_hot[idx]
epoch_loss = 0.0
for i in range(iter_per_epoch):
# 采样mini-batch
start = i * batch_size
end = min(start + batch_size, train_size)
x_batch = X_train_shuffle[start:end]
t_batch = y_train_shuffle[start:end]
# 计算梯度(使用反向传播)
grads = net.gradient(x_batch, t_batch)
# SGD参数更新
for key in ('W1', 'b1', 'W2', 'b2'):
net.params[key] -= current_lr * grads[key]
# 记录训练损失
loss_val = net.loss(x_batch, t_batch)
train_loss_list.append(loss_val)
epoch_loss += loss_val
# 每个轮次计算一次训练集和测试集的准确率
train_acc = net.accuracy(X_train[:5000], y_train_one_hot[:5000]) # 用子集加速验证
test_acc = net.accuracy(X_test[:2000], y_test_one_hot[:2000]) # 用子集加速验证
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
# 保存最佳模型
if test_acc > best_test_acc:
best_test_acc = test_acc
best_params = net.params.copy()
# 打印训练日志
avg_epoch_loss = epoch_loss / iter_per_epoch
elapsed_time = time.time() - start_time
print(f"Epoch [{epoch + 1}/{train_epochs}] | "
f"损失: {avg_epoch_loss:.4f} | "
f"训练准确率: {train_acc:.4f} | "
f"测试准确率: {test_acc:.4f} | "
f"学习率: {current_lr:.4f} | "
f"用时: {elapsed_time:.1f}s")
# 加载最佳模型
net.params = best_params
# 最终评估(使用全部测试集)
final_train_acc = net.accuracy(X_train[:10000], y_train_one_hot[:10000])
final_test_acc = net.accuracy(X_test, y_test_one_hot)
print(f"\n训练完成!总用时: {time.time() - start_time:.1f}秒")
print(f"最终训练准确率(子集): {final_train_acc:.4f}")
print(f"最终测试准确率: {final_test_acc:.4f}")
# ===================== 可视化训练结果 =====================
plt.figure(figsize=(15, 5))
# 训练损失曲线
plt.subplot(1, 3, 1)
plt.plot(train_loss_list, label='训练损失', alpha=0.7)
plt.xlabel('迭代次数')
plt.ylabel('交叉熵损失')
plt.title('MNIST训练损失曲线')
plt.legend()
plt.grid(True, alpha=0.3)
# 训练/测试准确率曲线
plt.subplot(1, 3, 2)
x = np.arange(train_epochs)
plt.plot(x, train_acc_list, label='训练准确率', marker='o')
plt.plot(x, test_acc_list, label='测试准确率', marker='s')
plt.xlabel('训练轮次')
plt.ylabel('准确率')
plt.title('MNIST训练/测试准确率曲线')
plt.legend()
plt.grid(True, alpha=0.3)
# 显示一些预测示例
plt.subplot(1, 3, 3)
sample_idx = np.random.choice(len(X_test), 10, replace=False)
sample_images = X_test[sample_idx]
sample_labels = y_test[sample_idx]
sample_pred = np.argmax(net.predict(sample_images), axis=1)
for i in range(10):
plt.subplot(2, 5, i + 1)
plt.imshow(sample_images[i].reshape(28, 28), cmap='gray')
color = 'green' if sample_pred[i] == sample_labels[i] else 'red'
plt.title(f'真:{sample_labels[i]}\n预:{sample_pred[i]}', color=color, fontsize=8)
plt.axis('off')
plt.suptitle('MNIST测试集预测示例(绿色=正确,红色=错误)')
plt.tight_layout()
plt.savefig('mnist_train_result.png', dpi=300)
plt.show()
# 梯度检查(可选,验证反向传播正确性)
def gradient_check():
"""梯度检查:比较数值梯度和解析梯度"""
print("\n执行梯度检查...")
# 使用小批量数据
x_check = X_train[:10]
t_check = y_train_one_hot[:10]
# 创建临时网络进行梯度检查
check_net = TwoLayerNet(input_size=784, hidden_size=20, output_size=10, weight_decay=0)
# 计算解析梯度
grads_backprop = check_net.gradient(x_check, t_check)
# 计算数值梯度(只检查部分参数以节省时间)
grads_numerical = check_net.numerical_gradient(x_check, t_check)
# 比较差异
print("梯度检查结果(相对误差):")
for key in grads_backprop.keys():
diff = np.abs(grads_backprop[key] - grads_numerical[key])
norm = np.abs(grads_backprop[key]) + np.abs(grads_numerical[key]) + 1e-7
rel_error = np.mean(diff / norm)
print(f" {key}: {rel_error:.6f}")
if rel_error > 1e-4:
print(f" 警告:{key} 的梯度可能有问题!")
运行结果展示
加载MNIST数据集...
训练集输入:(60000, 784), 训练集标签(独热):(60000, 10)
测试集输入:(10000, 784), 测试集标签(独热):(10000, 10)
开始训练神经网络...
训练样本数: 60000, 批次大小: 128, 每轮迭代: 468
Epoch [1/20] | 损失: 0.4345 | 训练准确率: 0.9264 | 测试准确率: 0.9180 | 学习率: 0.1000 | 用时: 3.8s
Epoch [2/20] | 损失: 0.2432 | 训练准确率: 0.9414 | 测试准确率: 0.9325 | 学习率: 0.0950 | 用时: 8.4s
Epoch [3/20] | 损失: 0.2024 | 训练准确率: 0.9514 | 测试准确率: 0.9405 | 学习率: 0.0902 | 用时: 14.2s
Epoch [4/20] | 损失: 0.1753 | 训练准确率: 0.9624 | 测试准确率: 0.9510 | 学习率: 0.0857 | 用时: 18.1s
Epoch [5/20] | 损失: 0.1567 | 训练准确率: 0.9664 | 测试准确率: 0.9505 | 学习率: 0.0815 | 用时: 21.8s
Epoch [6/20] | 损失: 0.1428 | 训练准确率: 0.9682 | 测试准确率: 0.9555 | 学习率: 0.0774 | 用时: 25.4s
Epoch [7/20] | 损失: 0.1318 | 训练准确率: 0.9700 | 测试准确率: 0.9565 | 学习率: 0.0735 | 用时: 29.1s
Epoch [8/20] | 损失: 0.1237 | 训练准确率: 0.9722 | 测试准确率: 0.9590 | 学习率: 0.0698 | 用时: 32.4s
Epoch [9/20] | 损失: 0.1169 | 训练准确率: 0.9742 | 测试准确率: 0.9580 | 学习率: 0.0663 | 用时: 36.0s
Epoch [10/20] | 损失: 0.1112 | 训练准确率: 0.9760 | 测试准确率: 0.9600 | 学习率: 0.0630 | 用时: 39.1s
Epoch [11/20] | 损失: 0.1069 | 训练准确率: 0.9772 | 测试准确率: 0.9610 | 学习率: 0.0599 | 用时: 42.3s
Epoch [12/20] | 损失: 0.1029 | 训练准确率: 0.9794 | 测试准确率: 0.9635 | 学习率: 0.0569 | 用时: 45.9s
Epoch [13/20] | 损失: 0.0997 | 训练准确率: 0.9806 | 测试准确率: 0.9635 | 学习率: 0.0540 | 用时: 49.1s
Epoch [14/20] | 损失: 0.0970 | 训练准确率: 0.9794 | 测试准确率: 0.9655 | 学习率: 0.0513 | 用时: 52.4s
Epoch [15/20] | 损失: 0.0944 | 训练准确率: 0.9814 | 测试准确率: 0.9650 | 学习率: 0.0488 | 用时: 55.6s
Epoch [16/20] | 损失: 0.0921 | 训练准确率: 0.9828 | 测试准确率: 0.9655 | 学习率: 0.0463 | 用时: 58.8s
Epoch [17/20] | 损失: 0.0904 | 训练准确率: 0.9816 | 测试准确率: 0.9665 | 学习率: 0.0440 | 用时: 61.9s
Epoch [18/20] | 损失: 0.0887 | 训练准确率: 0.9840 | 测试准确率: 0.9685 | 学习率: 0.0418 | 用时: 65.2s
Epoch [19/20] | 损失: 0.0870 | 训练准确率: 0.9844 | 测试准确率: 0.9660 | 学习率: 0.0397 | 用时: 68.4s
Epoch [20/20] | 损失: 0.0858 | 训练准确率: 0.9826 | 测试准确率: 0.9690 | 学习率: 0.0377 | 用时: 71.6s
训练完成!总用时: 71.8秒
最终训练准确率(子集): 0.9817
最终测试准确率: 0.9687
实验记录

更多推荐
所有评论(0)