第11章:错误是进步的阶梯(反向传播) 兔狲教授的亲切开场 上一章,我们探索了最简单的感知单元——神经元,看到了它如何整合信息、做出决策。但一个孤独的神经元是有限的,真正的智慧需要连接与合作。今天,我们要回答一个关键问题:当神经元犯错时,它如何调整自己? 错误不是终点,而是进步的阶梯。让我们慢慢来,探索反向传播的智慧。 核心议题:学习从错误开始 “教授,”小小猪盯着屏幕上错误的分类结果,“我训练了一个神经元识别圆形和方形,但它总是把一些菱形也识别成圆形。它明明‘看到’了自己的错误,却不知道如何改正。” 中山大学康乐园的深秋午后,阳光斜射进黑石屋的书房,在红砖地上投下长长的光影。窗外,珠江的水面波光粼粼,偶尔有船只缓缓驶过。
:::info
兔狲教授的亲切开场
上一章,我们探索了最简单的感知单元——神经元,看到了它如何整合信息、做出决策。但一个孤独的神经元是有限的,真正的智慧需要连接与合作。今天,我们要回答一个关键问题:当神经元犯错时,它如何调整自己? 错误不是终点,而是进步的阶梯。让我们慢慢来,探索反向传播的智慧。
:::
“教授,”小小猪盯着屏幕上错误的分类结果,“我训练了一个神经元识别圆形和方形,但它总是把一些菱形也识别成圆形。它明明‘看到’了自己的错误,却不知道如何改正。”
中山大学康乐园的深秋午后,阳光斜射进黑石屋的书房,在红砖地上投下长长的光影。窗外,珠江的水面波光粼粼,偶尔有船只缓缓驶过。书房里,功夫茶具上飘着淡淡的热气,墙上的挂钟滴答作响,记录着学习的每一刻。
小海豹:“这是个经典问题。历史上,早期的神经网络研究者也遇到了同样的困境:如何让网络从错误中学习?”
兔狲教授轻轻放下茶壶,微笑道:“你们遇到了学习问题的核心。知道错误只是第一步,知道如何改正错误才是真正的学习。今天,我们要探索这个‘如何’——反向传播算法。”
小小猪走到白板前,画了一个神经元的示意图。
“教授,神经元有权重 w 和偏置 b。当它犯错时,我们应该调整这些参数。但……往哪个方向调整?调多少?”
小海豹补充道:“这让我想起数学中的优化问题。要找到函数的最小值,我们需要知道当前位置的‘坡度’——也就是梯度。”
兔狲教授点头:“是的,这就是梯度下降的核心思想。想象你站在一座山上,四周是浓雾,看不见山脚。你只能感觉到脚下的坡度。要下山,你就往坡度最陡的方向走一小步。”
他在白板上画了一个山谷的示意图:
山顶 /\ / \ / \ / \ / \ 山谷(最小值)
“我们的目标是找到损失函数的最小值,”兔狲教授解释,“损失函数 L 衡量神经网络的预测与真实值之间的差距。权重和偏置是我们可以调整的‘位置’。”
小小猪思考着:“所以,学习就是……在山谷中摸索下山的路?每一步都根据脚下的坡度调整方向?”
“精辟的比喻,”兔狲教授微笑,“但这里有一个关键问题:神经网络有很多层,每层有很多神经元。我们如何计算每个参数的‘坡度’?”
窗外天色渐暗,黑石屋里亮起了温暖的灯光。
“教授,”小小猪问,“如果错误发生在输出层,我们怎么知道隐藏层的参数应该怎么调整?”
兔狲教授走到白板前,画了一个简单的两层网络:
输入层 → 隐藏层 → 输出层 x → h → y
“这就是反向传播的关键洞察,”他说,“误差从输出层向输入层反向传播,像涟漪一样扩散。数学工具是链式法则。”
小海豹若有所思:“链式法则……微积分中的那个?\frac{dz}{dx} = \frac{dz}{dy} \cdot \frac{dy}{dx}?”
“正是,”兔狲教授点头,“在神经网络中,输出误差对某个权重的导数,可以分解为:输出对中间变量的导数 × 中间变量对权重的导数。”
他在白板上写下公式:
其中:
小小猪仔细观察公式:“这个公式像……误差从后向前传递?\frac{\partial L}{\partial y} 是输出层的误差,然后乘以激活函数的导数 \frac{\partial y}{\partial z},再乘以输入的贡献 \frac{\partial z}{\partial w}?”
“很好的理解,”兔狲教授赞许道,“\frac{\partial L}{\partial y} 告诉我们‘预测离目标有多远’,\frac{\partial y}{\partial z} 告诉我们‘激活函数在这里有多敏感’,\frac{\partial z}{\partial w} 告诉我们‘这个权重对结果有多大影响’。”
兔狲教授在白板上总结反向传播的三个步骤:
“这三步形成一个循环,”兔狲教授说,“前向传播是‘尝试’,反向传播是‘反思’。通过不断循环,网络逐渐调整参数,减少错误。”
兔狲教授打开投影仪,一幅规整的计算图出现在屏幕上。
“这是反向传播的正交计算图,”兔狲教授指着图说,“蓝色箭头表示前向传播的数据流,红色箭头表示反向传播的误差流。”
小小猪仔细观察着图中的双向箭头:“这个图是……双向的?数据从左向右流,误差从右向左流?”
“正是,”兔狲教授解释,“正交计算图清晰地展示了反向传播的对称性:前向传播计算输出,反向传播计算梯度。”
小海豹若有所思:“图中的每个计算节点都需要保存前向传播的中间结果,以便反向传播时计算梯度?”
“是的,”兔狲教授说,“这是反向传播的内存代价。为了计算梯度,我们需要记住前向传播的‘足迹’。”
小小猪认真看着计算图:“教授,这个图中有些节点标着‘∂’符号,那是偏导数的意思?”
“是的,”兔狲教授指着图中的关键节点,“反向传播的核心是计算损失函数对每个参数的偏导数。这些偏导数告诉我们:如果稍微增加这个参数,损失会如何变化。”
兔狲教授在白板上画了一个陡峭的山坡。
“假设山坡很陡,”他说,“如果你跨出一大步,可能会冲过头,甚至跑到山的另一边。如果步长太小,下山会很慢。”
小小猪理解了这个比喻:“学习率就是‘步长’?太大可能震荡,太小可能太慢?”
“正是,”兔狲教授点头,“学习率 \eta 是梯度下降的关键超参数。更新公式是:”
小海豹补充道:“历史上,学习率的选择一直是经验性的。现代优化器(如Adam)会动态调整学习率。”
“是的,”兔狲教授说,“但今天我们先理解基本原理:沿着负梯度方向,以适当步长更新参数。”
小海豹从书架取下一本教育心理学著作,“教授,这让我想起教育中的‘形成性评价’概念。”
“很好的联系,”兔狲教授说,“反向传播实现了机器学习的‘形成性评价’。”
他在白板上写下思想模型:
“这四种机制,”兔狲教授解释,“对应着人类学习的核心过程:尝试、反思、调整、进步。”
小小猪思考着:“所以,神经网络的学习和人类学习有相似的结构?我们都从错误中学习,都需要反思错误的原因,都需要调整自己的‘内部模型’?”
“是的,”兔狲教授回答,“这是反向传播的深刻启示:学习本质上是误差驱动的模型修正。无论对于机器还是人类,错误都不是失败,而是进步的机会。”
兔狲教授在白板上画了多个山谷:
___ ___ / \___/ \___ / \__
“现实中的损失函数地形很复杂,”他说,“有多个‘山谷’(局部最小值)。梯度下降可能被困在某个小山谷,错过更深的山谷。”
小海豹若有所思:“这就是‘局部最小’问题?网络找到了一个‘还不错’的解,但不是‘最好’的解?”
“正是,”兔狲教授说,“这是梯度下降的固有局限。实践中,我们使用随机初始化、动量、随机梯度下降等策略来缓解这个问题。”
小小猪问:“但……我们怎么知道找到了‘最好’的解?”
兔狲教授微笑:“这是个深刻问题。在实践中,我们不知道是否找到了全局最优。我们只能确保找到了一个‘足够好’的解。这引出了机器学习的重要哲学:追求实用而非完美。”
:::info
兔狲教授的总结:反向传播的智慧
"让我们用Python代码来实践反向传播,"兔狲教授说,"代码不仅能帮助我们理解抽象的数学公式,还能让我们'运行'这个误差驱动的学习过程。"
import numpy as np import matplotlib.pyplot as plt class SingleLayerNetwork: """单层神经网络(逻辑回归),演示反向传播""" def __init__(self, num_features): """初始化网络 参数: num_features: 输入特征的数量 """ # 随机初始化权重和偏置 self.weights = np.random.randn(num_features) * 0.01 self.bias = 0.0 self.learning_rate = 0.1 def sigmoid(self, z): """sigmoid激活函数""" return 1 / (1 + np.exp(-z)) def sigmoid_derivative(self, z): """sigmoid函数的导数""" s = self.sigmoid(z) return s * (1 - s) def forward(self, X): """前向传播 参数: X: 输入数据 (n_samples, n_features) 返回: A: 激活值 Z: 加权和(缓存用于反向传播) """ # 加权求和: Z = XW + b Z = np.dot(X, self.weights) + self.bias # 激活函数 A = self.sigmoid(Z) return A, Z def compute_loss(self, y_true, y_pred): """计算二元交叉熵损失 参数: y_true: 真实标签 (0或1) y_pred: 预测概率 (0到1之间) 返回: 损失值 """ # 避免log(0)的情况 epsilon = 1e-15 y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # 二元交叉熵损失 loss = -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) return loss def backward(self, X, y_true, y_pred, Z): """反向传播计算梯度 参数: X: 输入数据 y_true: 真实标签 y_pred: 预测概率 Z: 前向传播的加权和(缓存) 返回: dW: 权重的梯度 db: 偏置的梯度 """ m = X.shape[0] # 样本数量 # 输出层的误差: dL/dA dA = -(y_true / y_pred - (1 - y_true) / (1 - y_pred)) / m # 通过激活函数的梯度: dA/dZ = σ'(Z) dZ = dA * self.sigmoid_derivative(Z) # 计算权重和偏置的梯度 dW = np.dot(X.T, dZ) db = np.sum(dZ) return dW, db def update_parameters(self, dW, db): """更新参数(梯度下降) 参数: dW: 权重的梯度 db: 偏置的梯度 """ self.weights -= self.learning_rate * dW self.bias -= self.learning_rate * db def train(self, X, y, epochs=1000, verbose=True): """训练网络 参数: X: 训练数据 y: 训练标签 epochs: 训练轮数 verbose: 是否打印训练过程 返回: losses: 每轮的损失值列表 """ losses = [] for epoch in range(epochs): # 前向传播 y_pred, Z = self.forward(X) # 计算损失 loss = self.compute_loss(y, y_pred) losses.append(loss) # 反向传播 dW, db = self.backward(X, y, y_pred, Z) # 更新参数 self.update_parameters(dW, db) # 每100轮打印一次损失 if verbose and epoch % 100 == 0: accuracy = np.mean((y_pred > 0.5) == y) print(f"轮数 {epoch}: 损失 = {loss:.4f}, 准确率 = {accuracy:.2%}") return losses # 创建并训练单层网络 print("单层神经网络反向传播演示:") print("=" * 60) # 生成简单的二分类数据 np.random.seed(42) n_samples = 100 n_features = 2 # 类别0: 均值[0, 0],类别1: 均值[1, 1] X0 = np.random.randn(n_samples//2, n_features) * 0.5 X1 = np.random.randn(n_samples//2, n_features) * 0.5 + 1.0 X = np.vstack([X0, X1]) y = np.hstack([np.zeros(n_samples//2), np.ones(n_samples//2)]) print(f"数据形状: X={X.shape}, y={y.shape}") print(f"类别分布: 类别0={np.sum(y==0)}个, 类别1={np.sum(y==1)}个") # 创建并训练网络 network = SingleLayerNetwork(num_features=n_features) losses = network.train(X, y, epochs=500, verbose=True) print(f"\n训练完成!") print(f"最终权重: {network.weights}") print(f"最终偏置: {network.bias}") # 测试网络 test_inputs = np.array([[0, 0], [1, 1], [0.5, 0.5]]) predictions, _ = network.forward(test_inputs) print(f"\n测试预测:") for i, (x, p) in enumerate(zip(test_inputs, predictions)): print(f" 输入 {x} → 预测概率 {p:.3f} → 分类 {1 if p>0.5 else 0}")
def visualize_backpropagation_process(): """可视化反向传播的关键步骤""" # 创建一个简单的网络用于可视化 viz_network = SingleLayerNetwork(num_features=2) viz_network.weights = np.array([0.5, -0.3]) viz_network.bias = 0.2 # 创建一个测试样本 test_X = np.array([[0.7, 0.3]]) test_y = np.array([1]) # 真实标签为1 print("\n反向传播步骤可视化:") print("=" * 60) # 步骤1: 前向传播 y_pred, Z = viz_network.forward(test_X) print(f"1. 前向传播:") print(f" 输入: {test_X[0]}") print(f" 权重: {viz_network.weights}") print(f" 偏置: {viz_network.bias}") print(f" 加权和 Z = {Z[0]:.3f}") print(f" 预测概率 σ(Z) = {y_pred[0]:.3f}") # 步骤2: 计算损失 loss = viz_network.compute_loss(test_y, y_pred) print(f"\n2. 计算损失:") print(f" 真实标签: {test_y[0]}") print(f" 预测概率: {y_pred[0]:.3f}") print(f" 二元交叉熵损失: {loss:.4f}") # 步骤3: 反向传播 dW, db = viz_network.backward(test_X, test_y, y_pred, Z) print(f"\n3. 反向传播(计算梯度):") print(f" 输出误差 dL/dA = {-(test_y[0]/y_pred[0] - (1-test_y[0])/(1-y_pred[0])):.3f}") print(f" sigmoid导数 σ'(Z) = {viz_network.sigmoid_derivative(Z[0]):.3f}") print(f" 梯度 dZ = dL/dA * σ'(Z) = {dW[0]/test_X[0,0]:.3f}") print(f" 权重梯度 dW = [{dW[0]:.3f}, {dW[1]:.3f}]") print(f" 偏置梯度 db = {db:.3f}") # 步骤4: 参数更新 old_weights = viz_network.weights.copy() old_bias = viz_network.bias viz_network.update_parameters(dW, db) print(f"\n4. 参数更新(学习率={viz_network.learning_rate}):") print(f" 旧权重: {old_weights}") print(f" 新权重: {viz_network.weights}") print(f" 权重变化: {viz_network.weights - old_weights}") print(f" 旧偏置: {old_bias:.3f}") print(f" 新偏置: {viz_network.bias:.3f}") print(f" 偏置变化: {viz_network.bias - old_bias:.3f}") # 验证更新后损失减少 new_pred, _ = viz_network.forward(test_X) new_loss = viz_network.compute_loss(test_y, new_pred) print(f"\n5. 验证更新效果:") print(f" 更新前损失: {loss:.4f}") print(f" 更新后损失: {new_loss:.4f}") print(f" 损失减少: {loss - new_loss:.4f}") print(f" 更新前预测: {y_pred[0]:.3f}") print(f" 更新后预测: {new_pred[0]:.3f}") print(f" 更接近真实标签{test_y[0]}了吗?{abs(new_pred[0]-test_y[0]) < abs(y_pred[0]-test_y[0])}") # 运行可视化 visualize_backpropagation_process()
class TwoLayerNetwork: """两层神经网络,演示深度网络的反向传播""" def __init__(self, input_size, hidden_size, output_size): """初始化两层网络 参数: input_size: 输入层大小 hidden_size: 隐藏层大小 output_size: 输出层大小 """ # 初始化参数 self.W1 = np.random.randn(input_size, hidden_size) * 0.01 self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * 0.01 self.b2 = np.zeros((1, output_size)) self.learning_rate = 0.1 def relu(self, Z): """ReLU激活函数""" return np.maximum(0, Z) def relu_derivative(self, Z): """ReLU函数的导数""" return (Z > 0).astype(float) def softmax(self, Z): """softmax函数(用于多分类)""" exp_Z = np.exp(Z - np.max(Z, axis=1, keepdims=True)) return exp_Z / np.sum(exp_Z, axis=1, keepdims=True) def forward(self, X): """前向传播 返回: cache: 缓存中间结果用于反向传播 """ # 第一层 Z1 = np.dot(X, self.W1) + self.b1 A1 = self.relu(Z1) # 第二层 Z2 = np.dot(A1, self.W2) + self.b2 A2 = self.softmax(Z2) # 缓存中间结果 cache = { 'X': X, 'Z1': Z1, 'A1': A1, 'Z2': Z2, 'A2': A2 } return A2, cache def compute_loss(self, Y, A2): """计算交叉熵损失""" m = Y.shape[0] log_probs = np.log(A2 + 1e-15) # 避免log(0) loss = -np.sum(Y * log_probs) / m return loss def backward(self, Y, cache): """反向传播 参数: Y: 真实标签(one-hot编码) cache: 前向传播的缓存 返回: grads: 各参数的梯度 """ m = Y.shape[0] # 从缓存中取出中间结果 X, Z1, A1, Z2, A2 = cache['X'], cache['Z1'], cache['A1'], cache['Z2'], cache['A2'] # 输出层梯度 dZ2 = A2 - Y # softmax交叉熵的优雅导数 dW2 = np.dot(A1.T, dZ2) / m db2 = np.sum(dZ2, axis=0, keepdims=True) / m # 隐藏层梯度 dA1 = np.dot(dZ2, self.W2.T) dZ1 = dA1 * self.relu_derivative(Z1) dW1 = np.dot(X.T, dZ1) / m db1 = np.sum(dZ1, axis=0, keepdims=True) / m grads = { 'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2 } return grads def update_parameters(self, grads): """更新参数""" self.W1 -= self.learning_rate * grads['dW1'] self.b1 -= self.learning_rate * grads['db1'] self.W2 -= self.learning_rate * grads['dW2'] self.b2 -= self.learning_rate * grads['db2'] def train(self, X, Y, epochs=1000): """训练网络""" losses = [] for epoch in range(epochs): # 前向传播 A2, cache = self.forward(X) # 计算损失 loss = self.compute_loss(Y, A2) losses.append(loss) # 反向传播 grads = self.backward(Y, cache) # 更新参数 self.update_parameters(grads) if epoch % 200 == 0: predictions = np.argmax(A2, axis=1) labels = np.argmax(Y, axis=1) accuracy = np.mean(predictions == labels) print(f"轮数 {epoch}: 损失 = {loss:.4f}, 准确率 = {accuracy:.2%}") return losses # 多层网络演示 print("\n两层神经网络反向传播演示:") print("=" * 60) # 生成简单的螺旋数据集 np.random.seed(42) n_samples = 200 n_classes = 3 # 创建螺旋数据 theta = np.linspace(0, 4*np.pi, n_samples//n_classes) radius = np.linspace(0.5, 2.5, n_samples//n_classes) X_spiral = [] Y_spiral = [] for k in range(n_classes): angle = theta + k * 2*np.pi/n_classes r = radius + np.random.randn(len(radius))*0.1 x1 = r * np.sin(angle) x2 = r * np.cos(angle) X_spiral.append(np.column_stack([x1, x2])) Y_spiral.append(np.full(len(x1), k)) X_spiral = np.vstack(X_spiral) Y_spiral = np.hstack(Y_spiral) # 打乱数据 indices = np.random.permutation(n_samples) X_spiral = X_spiral[indices] Y_spiral = Y_spiral[indices] # one-hot编码标签 Y_onehot = np.eye(n_classes)[Y_spiral] print(f"螺旋数据集: 样本数={n_samples}, 类别数={n_classes}") print(f"输入形状: {X_spiral.shape}, 输出形状: {Y_onehot.shape}") # 创建并训练两层网络 two_layer_net = TwoLayerNetwork(input_size=2, hidden_size=10, output_size=3) losses_2layer = two_layer_net.train(X_spiral, Y_onehot, epochs=1000) print(f"\n训练完成!") # 可视化决策边界 def plot_decision_boundary(model, X, y): """绘制神经网络的决策边界""" h = 0.02 # 网格步长 x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # 预测网格上每个点的类别 Z, _ = model.forward(np.c_[xx.ravel(), yy.ravel()]) Z = np.argmax(Z, axis=1) Z = Z.reshape(xx.shape) # 绘制决策边界 plt.figure(figsize=(10, 8)) plt.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu') # 绘制数据点 scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='black', s=50, alpha=0.8) plt.xlabel('特征 x₁') plt.ylabel('特征 x₂') plt.title('两层神经网络的决策边界(螺旋数据)') plt.colorbar(scatter, label='类别') plt.grid(True, alpha=0.3) plt.savefig('/tmp/two_layer_decision_boundary.png', dpi=150, bbox_inches='tight') plt.close() print("决策边界可视化已保存到 /tmp/two_layer_decision_boundary.png") plot_decision_boundary(two_layer_net, X_spiral, Y_spiral)
def compare_optimization_algorithms(): """比较不同的优化算法""" # 生成简单的二次损失函数: L(w) = (w - 2)^2 def loss_function(w): return (w - 2) ** 2 def gradient(w): return 2 * (w - 2) # 不同的优化算法 def gradient_descent(w_init, lr=0.1, epochs=50): """普通梯度下降""" w = w_init history = [w] for _ in range(epochs): w = w - lr * gradient(w) history.append(w) return np.array(history) def momentum_gd(w_init, lr=0.1, beta=0.9, epochs=50): """带动量的梯度下降""" w = w_init v = 0 # 动量项 history = [w] for _ in range(epochs): g = gradient(w) v = beta * v + (1 - beta) * g w = w - lr * v history.append(w) return np.array(history) def rmsprop(w_init, lr=0.1, beta=0.9, epsilon=1e-8, epochs=50): """RMSProp优化器""" w = w_init s = 0 # 平方梯度的移动平均 history = [w] for _ in range(epochs): g = gradient(w) s = beta * s + (1 - beta) * g**2 w = w - lr * g / (np.sqrt(s) + epsilon) history.append(w) return np.array(history) # 比较不同优化器 print("\n优化算法比较(目标: 最小化 L(w) = (w - 2)²):") print("=" * 60) w_init = -3.0 # 初始点 target = 2.0 # 最优值 # 运行不同优化器 gd_path = gradient_descent(w_init, lr=0.3, epochs=30) momentum_path = momentum_gd(w_init, lr=0.3, epochs=30) rmsprop_path = rmsprop(w_init, lr=0.3, epochs=30) # 计算最终误差 gd_error = abs(gd_path[-1] - target) momentum_error = abs(momentum_path[-1] - target) rmsprop_error = abs(rmsprop_path[-1] - target) print(f"初始值: w = {w_init:.2f}") print(f"目标值: w* = {target:.2f}") print(f"\n最终结果:") print(f" 普通梯度下降: w = {gd_path[-1]:.4f}, 误差 = {gd_error:.4f}") print(f" 动量梯度下降: w = {momentum_path[-1]:.4f}, 误差 = {momentum_error:.4f}") print(f" RMSProp: w = {rmsprop_path[-1]:.4f}, 误差 = {rmsprop_error:.4f}") # 可视化优化路径 plt.figure(figsize=(12, 5)) # 损失函数曲线 w_range = np.linspace(-3, 4, 100) loss_range = loss_function(w_range) plt.subplot(1, 2, 1) plt.plot(w_range, loss_range, 'b-', linewidth=2, label='L(w) = (w-2)²') plt.plot(gd_path, loss_function(gd_path), 'ro-', label='普通GD', alpha=0.6) plt.plot(momentum_path, loss_function(momentum_path), 'gs-', label='动量GD', alpha=0.6) plt.plot(rmsprop_path, loss_function(rmsprop_path), 'm^-', label='RMSProp', alpha=0.6) plt.xlabel('参数 w') plt.ylabel('损失 L(w)') plt.title('优化算法在损失函数上的路径') plt.legend() plt.grid(True, alpha=0.3) # 参数收敛曲线 plt.subplot(1, 2, 2) plt.plot(range(len(gd_path)), gd_path, 'ro-', label='普通GD', alpha=0.6) plt.plot(range(len(momentum_path)), momentum_path, 'gs-', label='动量GD', alpha=0.6) plt.plot(range(len(rmsprop_path)), rmsprop_path, 'm^-', label='RMSProp', alpha=0.6) plt.axhline(y=target, color='black', linestyle='--', label='目标值') plt.xlabel('迭代次数') plt.ylabel('参数 w') plt.title('参数随迭代次数的收敛情况') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('/tmp/optimization_algorithms_comparison.png', dpi=150, bbox_inches='tight') plt.close() print("\n优化算法比较图已保存到 /tmp/optimization_algorithms_comparison.png") # 分析不同算法的特点 print("\n优化算法特点分析:") print(" 普通梯度下降:") print(" - 简单直接,每次沿负梯度方向更新") print(" - 学习率选择关键,太大可能震荡,太小收敛慢") print(" - 在山谷中可能 zigzag 前进") print("\n 动量梯度下降:") print(" - 引入'动量'概念,积累之前的梯度方向") print(" - 减少震荡,加速在山谷中的收敛") print(" - 超参数β控制历史梯度的权重") print("\n RMSProp:") print(" - 自适应调整每个参数的学习率") print(" - 对频繁更新的参数使用较小的学习率") print(" - 对稀疏参数使用较大的学习率") print("\n 实际建议:") print(" - 简单问题: 普通GD或动量GD") print(" - 复杂问题: Adam(结合动量和RMSProp)") print(" - 学习率: 通常从0.001或0.0001开始尝试") # 运行优化算法比较 compare_optimization_algorithms()
"记住,"兔狲教授总结道,"反向传播是深度学习的关键算法,它将误差转化为学习的指导。通过梯度下降,网络沿着损失函数的斜坡缓慢下山;通过链式法则,误差从输出层反向传播到每一层。这个过程不仅高效,而且优雅——它展示了数学如何将‘知道错误’转化为‘知道如何改正’。最重要的是,反向传播教会我们关于学习本身的智慧:进步不是直线前进,而是在试错中曲折上升。"
茶香在黑石屋中弥漫,夜色渐深。
"今天我们探索了反向传播的智慧,"兔狲教授说,"单个神经元学会了从错误中调整自己。但真正的记忆需要时间——如何让网络记住过去的信息?"
小小猪好奇地问:"记忆?就像我们记住刚才说的话?"
"是的,"兔狲教授解释,"下一章,我们要探索记忆的链条。当信息随时间流动时,网络如何保持状态?这就是循环神经网络(RNN)和长短期记忆(LSTM)的故事。"
小海豹翻动着笔记本,"这引出了序列建模的历史。历史上,人们是如何处理时间序列数据的?"
兔狲教授微笑:"我们慢慢来,下一章见。"
小小猪的笔记:我实现了一个两层网络,训练它在螺旋数据上分类。一开始准确率只有33%(随机猜测),1000轮后达到92%!但我也发现,如果学习率太大(0.5),损失会爆炸;如果太小(0.001),几乎不学习。找到合适的步长就像找到下山的节奏——不能太急,也不能太慢。
小海豹的笔记:研究了反向传播的历史,惊讶于它经历了"发明-遗忘-再发现"的循环。1960年代就有类似想法,但直到1986年鲁梅尔哈特等人的论文才引起广泛关注。最有趣的是,反向传播的数学基础(链式法则)在微积分中早已存在,但将其应用于神经网络需要跨学科的洞察力。
兔狲教授的结语:反向传播教给我们关于学习的深刻一课:进步需要反馈,优化需要方向,复杂需要分解。在这个算法中,我们看到了数学的优雅与实践的智慧的结合。最重要的是,它提醒我们:错误不是终点,而是重新开始的起点。在这条路上,耐心比速度更重要,方向比力量更有价值。我们慢慢来,理解了最重要。