- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
导读
本文以 Coding Train(The Coding Train)神经网络系列直播第 115 期《Backpropagation》为核心,结合仓库中 Nature of Code 第 10 章(神经网络)系列文档与 Toy-Neural-Network-JS 库的完整实现,系统讲解反向传播算法的原理、梯度下降更新规则,以及它在 JavaScript / p5.js 与 Processing 中的落地方式。读完本文,你将能够理解误差如何从输出层逐层回传、权重与偏置如何按梯度更新,并能用仓库中的 XOR 示例亲手验证一个 2-4-1 结构网络的训练过程。
直播背景:神经网络系列的关键一环
_Streams/115-backpropagation.md记录了 2018 年 1 月 20 日(视频号 115,视频 IDXHj52k75PYo)的一场直播。这场直播是 "Neural Networks" 系列(_Streams/113-return-neural-networks.md、_Streams/114.1-neural-networks-continued.md)的延续,主题是讨论反向传播(Backpropagation)。直播页面的描述非常坦诚:作者试图讨论反向传播,但过程"大部分时间走向混乱"("I attempt to discuss backpropagation, but it mostly goes haywire"),这正是许多学习者初接触反向传播时的真实状态。
直播按时间戳划分为四个话题:
| 时间点 | 话题 | 对应仓库文档 |
|---|---|---|
| 30:20 | Reading Random Numbers(随机数读取) | — |
| 36:22 | Backpropagation Part 1 | _more/archive/nature-of-code/10-Neural-Networks/10.14-Neural-Networks-Backpropagation-Part-1.md |
| 1:06:56 | Backpropagation Part 2 | _more/archive/nature-of-code/10-Neural-Networks/10.15-Neural-Networks-Backpropagation-Part-2.md |
| 1:40:25 | Learning with Gradient Descent(梯度下降学习) | _more/archive/nature-of-code/10-Neural-Networks/10.16-Neural-Networks-Backpropagation-Part-3.md |
围绕这一期直播,仓库_more/archive/nature-of-code/10-Neural-Networks/目录下保存了完整的Backpropagation Part 1~5系列文档(10.14~10.18),分别对应:反向传播概念引入、在简易 JavaScript 神经网络库中开始实现、用梯度下降调整权重、在train()函数中实现梯度下降公式并更新权重、以及更新偏置并用 XOR 数据集测试。这一系列文档与直播互为表里,构成了本文的技术骨架。
前置知识:矩阵类与前向传播(Feedforward)
反向传播是在"前向传播(feedforward)"基础上的误差回传过程,因此必须先理解网络前向计算所依赖的矩阵工具。
Matrix 类的核心运算
Toy Neural Network 库的矩阵实现位于 CodingChallenges/CC_092_xor/P5/libraries/matrix.js,它为反向传播提供了四类关键运算:
- 矩阵乘法(
Matrix.multiply(a, b),静态方法,matrix.js):要求a.cols === b.rows,按行-列点积计算,用于前向传播中weights × inputs以及反向传播中gradients × hidden^T; - 逐元素乘法 / 标量乘法(实例方法
multiply(n),matrix.js):当参数是矩阵时做 Hadamard 乘积(对应代码注释// hadamard product),当参数是标量时做缩放,用于"梯度 × 误差 × 学习率"的组合运算; - 转置(
Matrix.transpose(matrix),matrix.js):交换行列索引,用于误差从输出层向隐藏层回传(weights_ho^T × output_errors); - 减法(
Matrix.subtract(a, b),matrix.js):用于计算targets - outputs的误差。
此外,fromArray/toArray负责普通数组与矩阵之间的转换,randomize()用Math.random() * 2 - 1将权重和偏置初始化为[-1, 1]区间内的随机值。
前向传播:predict() 的两层计算
网络对象在 CodingChallenges/CC_092_xor/P5/libraries/nn.js 中定义。构造时按(input_nodes, hidden_nodes, output_nodes)创建两组权重矩阵weights_ih(隐藏层×输入层)与weights_ho(输出层×隐藏层),以及两组偏置bias_h、bias_o(nn.js)。
前向传播逻辑见predict(input_array)(nn.js):
predict(input_array) { // Generating the Hidden Outputs let inputs = Matrix.fromArray(input_array); let hidden = Matrix.multiply(this.weights_ih, inputs); hidden.add(this.bias_h); hidden.map(this.activation_function.func); // 隐藏层激活 // Generating the output's output! let output = Matrix.multiply(this.weights_ho, hidden); output.add(this.bias_o); output.map(this.activation_function.func); // 输出层激活 return output.toArray(); }其计算链为:hidden = f(weights_ih · inputs + bias_h),output = f(weights_ho · hidden + bias_o)。train()的第一段与predict()完全相同,因为反向传播必须以一次前向计算的结果作为误差计算的起点。
反向传播的核心思想:从输出误差到梯度下降
Part 1 文档(10.14-Neural-Networks-Backpropagation-Part-1.md)指出:这一视频讨论的是监督学习(supervised learning)语境下的反向传播算法。所谓监督学习,即训练集中每个样本都带有期望输出(target),网络根据"实际输出与期望输出的差距"来调整自身参数。
反向传播的基本逻辑分三步:
- 计算输出误差:
output_errors = targets - outputs; - 把误差"分摊"回上一层:隐藏层误差由输出层误差经权重矩阵转置后回传得到;
- 沿误差下降最快的方向更新参数:这正是 Part 3 文档(10.16-Neural-Networks-Backpropagation-Part-3.md)所讨论的gradient descent(梯度下降)——每个权重按其对误差的偏导数(梯度)的负方向微调,学习率(learning rate)控制每次调整的步长。
仓库中还保留了 Part 4 与 Part 5 的文档:10.17 在train()中实现梯度下降公式并更新权重,10.18 继续实现偏置更新并使用 XOR 数据集测试。这两篇文档描述的内容,在 nn.js 的train()方法(第 65~120 行)中有完整落地。
输出层:误差、梯度与权重/偏置更新
train(input_array, target_array)在完成前向传播后,首先处理输出层(nn.js):
// Convert array to matrix object let targets = Matrix.fromArray(target_array); // Calculate the error // ERROR = TARGETS - OUTPUTS let output_errors = Matrix.subtract(targets, outputs); // Calculate gradient let gradients = Matrix.map(outputs, this.activation_function.dfunc); gradients.multiply(output_errors); gradients.multiply(this.learning_rate); // Calculate deltas let hidden_T = Matrix.transpose(hidden); let weight_ho_deltas = Matrix.multiply(gradients, hidden_T); // Adjust the weights by deltas this.weights_ho.add(weight_ho_deltas); // Adjust the bias by its deltas (which is just the gradients) this.bias_o.add(gradients);对应公式可以概括为:
- 输出误差:
output_errors = targets − outputs; - 输出层梯度:
gradients = f'(outputs) ⊙ output_errors × learning_rate,其中⊙为逐元素乘(Hadamard 积); - 权重增量:
Δweights_ho = gradients × hiddenᵀ; - 偏置增量:
Δbias_o = gradients(偏置对误差的梯度就是该节点的梯度本身)。
注意代码中刻意保留了注释掉的写法// let gradient = outputs * (1 - outputs);,它对应 Sigmoid 导数的直接形式,后面会被activation_function.dfunc的统一机制替代。
隐藏层:误差回传与梯度计算
输出层处理完毕后,误差沿网络反向"传播"到隐藏层(nn.js):
// Calculate the hidden layer errors let who_t = Matrix.transpose(this.weights_ho); let hidden_errors = Matrix.multiply(who_t, output_errors); // Calculate hidden gradient let hidden_gradient = Matrix.map(hidden, this.activation_function.dfunc); hidden_gradient.multiply(hidden_errors); hidden_gradient.multiply(this.learning_rate); // Calcuate input->hidden deltas let inputs_T = Matrix.transpose(inputs); let weight_ih_deltas = Matrix.multiply(hidden_gradient, inputs_T); this.weights_ih.add(weight_ih_deltas); // Adjust the bias by its deltas (which is just the gradients) this.bias_h.add(hidden_gradient);这一段的本质是"把误差从输出层按权重比例分摊给隐藏层":
- 隐藏层误差:
hidden_errors = weights_hoᵀ × output_errors——这是反向传播中"传播"一词的直接体现:误差不是凭空出现,而是经由输出层权重矩阵的转置回传; - 隐藏层梯度:
hidden_gradient = f'(hidden) ⊙ hidden_errors × learning_rate; - 权重增量:
Δweights_ih = hidden_gradient × inputsᵀ; - 偏置增量:
Δbias_h = hidden_gradient。
至此,网络的两组权重weights_ho、weights_ih与两组偏置bias_o、bias_h都完成了一次基于梯度下降的更新,一次train()调用即结束。若网络有更多隐藏层,只需把同样的"转置回传误差 → 计算梯度 → 更新权重偏置"过程逐层继续,这正是反向传播可扩展到多层网络的原理。
激活函数与其导数:反向传播的"开关"
反向传播能够计算梯度的前提是激活函数可导。库中用ActivationFunction类将函数与其导数绑定(nn.js):
class ActivationFunction { constructor(func, dfunc) { this.func = func; this.dfunc = dfunc; } } let sigmoid = new ActivationFunction( x => 1 / (1 + Math.exp(-x)), // Sigmoid y => y * (1 - y) // 其导数 ); let tanh = new ActivationFunction( x => Math.tanh(x), y => 1 - y * y );setActivationFunction(func = sigmoid)(nn.js)默认使用 Sigmoid。Sigmoid 的导数有一个著名性质:σ'(y) = y(1 − y),因此代码中dfunc直接写成y => y * (1 - y),y就是该神经元当前的激活值,无需重新计算。tanh的导数同理为1 − y²。在train()中,Matrix.map(outputs, this.activation_function.dfunc)与Matrix.map(hidden, this.activation_function.dfunc)分别对输出层与隐藏层节点应用导数,实现"梯度 = 导数 × 误差 × 学习率"的逐元素运算。
学习率的调节与序列化
setLearningRate(learning_rate = 0.1)(nn.js)设置默认学习率 0.1。学习率直接控制每次梯度更新的步长:过大容易震荡甚至发散,过小收敛缓慢。库还提供了serialize()与静态deserialize()(nn.js),可把训练好的网络(结构、权重、偏置、学习率)导出为 JSON 再还原,方便保存训练结果。
实战验证:用 XOR 数据集训练 2-4-1 网络
Part 5 文档明确说明实现完成后使用 XOR 数据集进行测试。仓库中对应的完整示例位于 CodingChallenges/CC_092_xor/P5/sketch.js:
let nn; let lr_slider; let training_data = [ { inputs: [0, 0], outputs: [0] }, { inputs: [0, 1], outputs: [1] }, { inputs: [1, 0], outputs: [1] }, { inputs: [1, 1], outputs: [0] } ]; function setup() { createCanvas(400, 400); nn = new NeuralNetwork(2, 4, 1); lr_slider = createSlider(0.01, 0.5, 0.1, 0.01); } function draw() { background(0); for (let i = 0; i < 100; i++) { let data = random(training_data); nn.train(data.inputs, data.outputs); } nn.setLearningRate(lr_slider.value()); // ...以 10px 网格遍历画布,用 nn.predict 输出灰度填充每个格子 }要点解读:
- 网络结构:
new NeuralNetwork(2, 4, 1),即 2 个输入节点、4 个隐藏节点、1 个输出节点。XOR(异或)问题无法用单层感知机解决,必须有隐藏层,是验证反向传播的经典基准; - 训练方式:每帧随机抽取 4 条训练样本中的一条,执行 100 次
train(),即随机梯度下降式的在线学习(每次只用一条样本更新); - 学习率交互:滑杆范围
0.01 ~ 0.5,默认 0.1,步长 0.01,可在运行时直观观察学习率对收敛效果的影响; - 可视化:遍历 400×400 画布上 10px 分辨率的网格,对每个坐标点
(x1, x2)调用nn.predict([x1, x2]),用输出值乘以 255 作为灰度填充。训练完成后,画布会呈现两个对角亮区、两个对角暗区的 XOR 决策面形态; - 页面组织:index.html 依次加载
p5.min.js、libraries/matrix.js、libraries/nn.js与sketch.js,直接用浏览器打开即可运行。
仓库同样提供了 Processing 版本:Neural_Network.pde 中的train()(第 135~181 行)与 JS 版算法完全一致,并显式实现了sigmoid、dsigmoid、tanh、dtanh函数(第 10~26 行);主程序 CC_092_xor.pde 使用同样的 4 条 XOR 数据训练new NeuralNetwork(2, 4, 1),并以 10px 网格渲染决策面。JS 与 Processing 双版本互为印证,便于对照学习。
反向传播之外:同一网络的两种训练范式
仓库中同一套 Toy Neural Network 结构还被复用于其他场景,有助于理解反向传播在网络训练中的定位:
- CodingChallenges/CC_100.1_NeuroEvolution_FlappyBird/P5/neuralnetwork/nn.js 在
train()之外额外提供copy()与mutate()(第 159~177 行),走的是**神经进化(Neuroevolution)**路线——用遗传算法(复制+变异)而非反向传播来搜索权重; - CodingChallenges/CC_099_ColorPredictor/P5/lib/nn.js 则保留了完整的
train()反向传播实现,用于从 RGB 预测颜色名称等监督学习任务。
对比可知:反向传播依赖"样本的期望输出"计算梯度(监督学习),而神经进化只需一个适应度(fitness)信号即可演化网络,两者是互补的两种训练范式。
系列脉络与延伸学习
本期直播处于神经网络系列的中段,前置内容(矩阵运算、前向传播)记录于_Streams/113-return-neural-networks.md与_Streams/114.1-neural-networks-continued.md;完整的反向传播专题则以 5 篇文档保存在 _more/archive/nature-of-code/10-Neural-Networks/(10.14~10.18),该目录同时收录了感知机、多层感知机、矩阵运算等系列文档,配套 Nature of Code 一书第 10 章(见 index.md)。直播中提到的外部参考(如 Michael Nielsen 的神经网络在线教材、3Blue1Brown 的神经网络系列与梯度下降视频等)适合作为进阶理解材料,但本文的技术结论均以仓库源码与文档为准。
小结
- 反向传播 = 前向传播 + 误差回传 + 梯度下降:误差从输出层经权重矩阵转置逐层回传,每层按"激活函数导数 × 误差 × 学习率"计算梯度并更新权重与偏置;
- Toy-Neural-Network-JS 的
train()是完整参考实现:输出层与隐藏层两段对称的"计算梯度 → 转置相乘得增量 → 累加更新"逻辑清晰可读,配合 matrix.js 的矩阵工具即可独立运行; - XOR 是验证反向传播正确性的最小基准:2-4-1 网络配合随机样本在线训练与决策面可视化,可直观确认网络确实学会了异或映射;
- 从源码结构看,该库把"导数与函数绑定(
ActivationFunction)""学习率可调(setLearningRate)""序列化保存(serialize/deserialize)"等设计固化下来,为后续在神经进化等场景复用同一网络结构奠定了基础。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
Coding Train 直播 118 期:用反向传播、梯度下降与 Toy Neural Network 库求解 XOR 问题
Coding Train 直播 118 期:用反向传播、梯度下降与 Toy Neural Network 库求解 XOR 问题 导读 本文基于 website
教程文档示例工程《神经网络与深度学习》第4章:前馈神经网络反向传播算法详解
《神经网络与深度学习》第4章:前馈神经网络反向传播算法详解 你是否还在为多层神经网络的参数更新而困扰?反向传播算法(Backpropagation)作为训练前馈
website-archive 神经网络系列直播 114.1:Matrix 类改进、前馈(Feedforward)算法与 Floyd–Steinberg 抖动实现解析
website archive 神经网络系列直播 114.1:Matrix 类改进、前馈(Feedforward)算法与 Floyd–Steinberg 抖动实
教程文档示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考