Playground

与门

或门

非门

十以内加法

奇偶判断

源码

一步步开发自己的深度神经网络

在生物学中,神经元使用一种称为突触的特殊连接与其他细胞进行通信,从而实现大脑的思考。而人工神经网络则使用代码模拟了这种结构。

神经网络由多个构成,分别是 1 个输入层、n 个隐藏层(n >= 0)、1 个输出层构成。每个层包含 n 个神经元(n >= 1)。右侧展示了一个神经网络:

  • 1 个输入层: 包含 2 个神经元。
  • 1 个隐藏层: 包含 2 个神经元。
  • 1 个输出层: 包含 1 个神经元。

每个神经元有一个偏置 bias。例如右图中的 b1b2 等等。每个神经元之间的连接都有一个权重 weight。例如右图中的 w1w2等等。

计算神经元的输出过程为:所有输入神经元的加权和 + 当前神经元的偏置。例如 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,01,1 的输出接近 1,而 0,11,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));

点击 模型并输出测试结果。