在生物学中,神经元使用一种称为突触的特殊连接与其他细胞进行通信,从而实现大脑的思考。而人工神经网络则使用代码模拟了这种结构。
神经网络由多个层构成,分别是 1 个输入层、n 个隐藏层(n >= 0)、1 个输出层构成。每个层包含 n 个神经元(n >= 1)。右侧展示了一个神经网络:
每个神经元有一个偏置 bias
。例如右图中的 b1
、b2
等等。每个神经元之间的连接都有一个权重 weight
。例如右图中的 w1
、w2
等等。
计算神经元的输出过程为:所有输入神经元的加权和 + 当前神经元的偏置。例如 x1
神经元的输出为:
x1 = input1 * w1 + input2 * w2 + b1
由于所有的输入和输出都是 0-1 的数值,因此我们还需要一个函数把输出值映射到 0-1 的范围,这个函数称为激活函数。我们选择一个通用的 sigmoid
函数:
f(n) = 1 / (1 + e^(-n))
我们了解了神经网络的原理后,尝试手动构建一个最简单的神经网络:取反。
输入层有 1 个神经元,输出层有 1 个神经元,没有隐藏层。当输入为 1 的时候,输出 0;当输入 0 的时候,输出 1。
现在这个网络是这样的,如右图所示。
这个网络足够简单,输出的计算方式是:
f(x) = x * w + b
把 0 和 1 带入到方程:
0 = 1 * w + b
1 = 0 * w + b
解方程:w = -1,b = 1。
于是我们得到了最终的神经网络,如右图所示。利用我们手动计算出的偏置和权重,我们可以得到输入 0 和 1 的输出。
现实中的神经网络通常包括几万甚至上亿个参数,如果手动计算这些参数肯定是不可能的。因此我们需要让网络自主学习。
现在我们构建一个网络来执行这个任务:判断是否相等。这个网络的结构:输入层 2 个神经元,输出层 1 个神经元,隐藏层 3 个神经元。
我们执行 const network = new Network([2, 3, 1])
初始化这个网络。
点击这个按钮 网络的神经元会设置为随机值。
接下来我们准备一组训练数据,对其训练 1000 次。
const trainingData = [
{ input: [0, 0], output: [1] },
{ input: [1, 1], output: [1] },
{ input: [0, 1], output: [0] },
{ input: [1, 0], output: [0] },
];
for (let i = 0; i < 1000; i++) {
const index = Math.floor(Math.random() * trainingData.length);
const { input, output } = trainingData[index];
network.train(input, output);
}
每点一次 ,我们都会对网络训练 1000 次,并输出测试结果。
当点击 10 次后,0,0
和 1,1
的输出接近 1,而 0,1
和 1,0
的输出接近 0。说明我们的网络经过一万次训练后,已经可以成功预测两个比特位是否相等。
网络的训练可能需要几小时、几天、甚至几个月的时间。如果我们能够提前训练好一个网络,那么我们就可以直接使用这个网络,而不需要再训练。
而我们训练的过程,就是调整网络中的参数,使得网络的输出与我们期望的输出尽可能接近。参数包含了网络的结构,因此我们可以将网络的参数保存下来,以便下次使用。 在我们的网络中,参数就是每个神经元的偏置和连接的权重。
当网络训练好之后,我们可以使用 network.exportModel()
导出模型。为了方便我们使用 JSON 格式存储模型,现实中我们应该使用二进制格式来存储,由于网络参数都是数值,因此二进制相比文本文件可以达到相当高的压缩率。
随后我们就可以使用 const network = Network.fromModel(model)
从模型中直接创建神经网络。
一个加载与门的预训练模型的例子:
import { Network } from "@/dnn/network.ts";
const model = await Deno.readTextFile("./models/and_gate.json");
const network = Network.fromModel(JSON.parse(model));
console.log("0 AND 0 = ", network.predict([0, 0]));
console.log("0 AND 1 = ", network.predict([0, 1]));
console.log("1 AND 0 = ", network.predict([1, 0]));
console.log("1 AND 1 = ", network.predict([1, 1]));
点击 模型然后 此模型。
在前面的例子中一直存在一个问题。
我们训练的网络只有简单的几个输入和输出,而且我们使用了所有输入和输出对网络进行训练。
下面我们来看一个稍微复杂一点的例子,我们使用一个网络来预测一个 0
- 1023
的数字是不是偶数。但是这次,我们只使用 10%
的数字来训练网络。
首先,我们需要生成 0
- 1023
的数字,将其转换为二进制数,最后将其转换为数组。然后我们将这些数据随机排序,然后取前 10%
的数据作为训练数据。训练完成之后,我们使用剩下的数据来测试网络的准确率。
// 生成 0 到 1023 的二进制数
const allData = Array(2 ** 10)
.fill(0)
.map((_, i) => ({
i: i,
input: i.toString(2).padStart(10, "0").split("").map(Number),
output: [i % 2],
}))
.sort(() => Math.random() - 0.5);
// 只用 10% 的数据进行训练
const trainingData = allData
.slice(0, Math.floor(allData.length * 0.1));
点击 模型并输出测试结果。