关于神经网络的说明
这是一个用 JavaScript 实现神经网络的库。
背景
这是一个用 JavaScript 实现神经网络的库。虽然它不是一个支持深度学习的全面库,但它支持一个基本的人工神经网络的大部分功能。
- 它支持任意数量的输入。
- 它支持任意数量的输出。
- 它支持任意数量的隐藏层,并且每层的节点数是可配置的。
- 它支持输入层和隐藏层的偏置节点。
为了简化,做出了以下两个假设。
基本的神经网络结构和算法多年来已经被充分研究。我在这里不再赘述。如果您不熟悉它们,可以参考此链接。
ANN 神经网络库
为了实验和娱乐,我实现了这个用于实验目的的神经网络库。由于它是为实验目的而编写的,所以我用ES6编写,并使用Jest进行了测试。介绍完库之后,我将向您展示一个如何训练神经网络来执行异或(XOR)运算的例子。
网络结构
要创建一个神经网络,我们需要创建一个配置文件实例,它定义了网络的结构。
export class config {
constructor(numOfInputs,
numberOfOutputs,
hiddenlayers,
activatorName,
learningRate) {
this.numOfInputs = numOfInputs;
this.numberOfOutputs = numberOfOutputs;
this.hiddenlayers = hiddenlayers;
this.activatorName = activatorName;
this.learningRate = learningRate? learningRate: 1;
}
}
网络中有两种节点,即普通节点和偏置节点。偏置节点只有一个out
入口,其值始终为1
。
export class node {
constructor() {
this.sum = 0;
this.out = 0;
this.gradient = 0;
}
}
export class biasnode {
constructor() { this.out = 1; }
}
基于配置文件,我们可以获得构建网络结构所需的信息。
export class network {
constructor(config) {
this.config = config;
this.layers = [];
this.weights = [];
}
getOutput() {
let layers = this.layers;
let output = [];
if (!layers || layers.length < 1) {
return output;
}
let oLayer = layers[layers.length - 1];
let length = oLayer.length;
for (let i = 0; i < length; i++) {
output[i] = oLayer[i].out;
}
return output;
}
}
每个神经网络都有一个层数组和一个权重矩阵数组。当我们讨论网络上的操作时,我们会讨论如何初始化层和权重。
激活函数
除了输入节点之外,其他所有节点的输出都是前一层节点输出的加权和,经过一个激活函数。
export const activatorFactory = {
availableActivators: {
linear: {
value: function(x) {
return x;
},
prime: function(x) {
return 1;
}
},
signoid: {
value: function(x) {
return 1 / (1 + Math.exp(-1 * x));
},
prime: function(x) {
let v = this.value(x);
return v * (1 - v);
}
},
ReLU: {
value: function(x) {
return Math.max(0, x);
},
prime: function(x) {
return (x >= 0)? 1: 0;
}
}
},
getActivator: function(name) {
return this.availableActivators[name];
}
}
为了简单起见,我只实现了线性、Sigmoid 和 ReLU 函数。如果您想尝试其他激活函数,可以在此处添加。
网络操作
要初始化层和权重矩阵,我们可以使用networkInitiator
对象,通过传入一个包含配置信息的网络对象。
import {node, biasnode} from '../network/node';
export const networkInitiator = function() {
let initLayer = function(n, skipBias) {
let nodes = [];
for (let i = 0; i < n; i++) {
nodes[i] = new node();
}
if (! skipBias) { nodes[n] = new biasnode();}
return nodes;
};
let initWeight = function(ni, no) {
let w = [[]];
for (let i = 0; i < no; i++) {
let r = [];
for (let j = 0; j < ni; j++) { r[j] = Math.random(); }
r[ni] = Math.random();
w[i] = r;
}
return w;
};
let initLayers = function(conf) {
let ni = conf.numOfInputs;
let no = conf.numberOfOutputs;
let hl = conf.hiddenlayers;
let layers = [[]];
layers[0] = initLayer(ni);
let l = hl.length;
for (let i = 0; i < l; i++) {
layers[i + 1] = initLayer(hl[i]);
}
layers[l + 1] = initLayer(no, true);
return layers;
};
let initWeights = function(conf) {
let ni = conf.numOfInputs;
let no = conf.numberOfOutputs;
let hl = conf.hiddenlayers;
let w = [[]];
w[0] = initWeight(ni, hl[0]);
let l = hl.length;
for (let i = 1; i < l; i++) {
w[i] = initWeight(hl[i - 1], hl[i]);
}
w[l] = initWeight(hl[l - 1], no);
return w;
};
return {
initNetwork: function(nn) {
nn.layers = initLayers(nn.config);
nn.weights = initWeights(nn.config);
},
};
}();
- 由于每层都添加了一个偏置节点,因此每层的节点数为
n + 1
,其中n
是配置文件中定义的普通节点数。 - 由于每层都添加了一个偏置节点,因此每个权重矩阵的维度为
m x (n + 1)
,其中n
是前一层的普通节点数,m
是下一层普通节点数,如配置文件中所定义。权重矩阵中的初始权重是随机初始化的。
要执行神经网络上的操作,需要创建networkOperator
对象。
import { internalOperations } from './internalOperations';
import { activatorFactory } from '../activations/activatorFactory';
export const networkOperator = {
forward: function(nn, input) {
let config = nn.config;
let layers = nn.layers;
let weights = nn.weights;
let activator = activatorFactory.getActivator(config.activatorName);
internalOperations.applyInput(layers[0], input);
for (let i = 0; i < weights.length; i++) {
internalOperations.applyWeight
(layers[i], weights[i], layers[i + 1], activator);
}
return nn.getOutput();
},
backward: function(nn, diff) {
let config = nn.config;
let layers = nn.layers;
let weights = nn.weights;
let activator = activatorFactory.getActivator(config.activatorName);
internalOperations.applyDiff(layers[layers.length - 1], diff, activator);
for (let i = layers.length - 1; i > 0; i--) {
internalOperations.applyGradient(layers[i - 1], weights[i - 1], layers[i],
activator, config.learningRate);
}
},
train: function(nn, data) {
let input = data[0];
let desired = data[1];
let diff = internalOperations.getDiff(this.forward(nn, input), desired)
this.backward(nn, diff);
return diff;
}
}
- 给定神经网络的输入,我们可以使用
forward
函数来计算输出。 - 要训练神经网络,我们可以使用
train
函数。data
参数应同时包含训练的输入和预期的输出。
出于文档目的,如果您有兴趣,支持networkOperator
的详细操作实现在internalOperations
对象中。
export const internalOperations = {
applyInput: function(l, input) {
let length = input.length;
for (let i = 0; i < length; i++) {
let value = input[i];
l[i].sum = value;
l[i].out = value;
}
},
applyWeight: function(ll, w, lr, activator) {
let nr = w.length;
let nc = w[0].length;
for (let i = 0; i < nr; i++) {
let node = lr[i];
let sum = 0;
for (let j = 0; j < nc; j++) {
sum += w[i][j] * ll[j].out;
}
node.sum = sum;
node.out = activator.value(sum);
}
},
getDiff(actual, desired) {
let diff = [];
let length = actual.length;
for (let i = 0; i < length; i++) {
diff[i] = actual[i] - desired[i];
}
return diff;
},
applyDiff: function(l, diff, activator) {
let length = l.length;
for (let i = 0; i < length; i++) {
let node = l[i];
node.gradient = diff[i] * activator.prime(node.sum)
}
},
applyGradient(ll, w, lr, activator, lrate) {
let nr = w[0].length - 1;
let nc = w.length;
for (let i = 0; i < nr; i++) {
let node = ll[i];
let gradient = 0;
for (let j = 0; j < nc; j++) {
let wt = w[j][i];
let rgradient = lr[j].gradient;
gradient += wt * rgradient;
w[j][i] = wt - node.out * rgradient * lrate;
}
node.gradient = activator.value(node.sum) * gradient;
}
let bias = ll[nr];
for (let i = 0; i < nc; i++) {
let wt = w[i][nr];
let rgradient = lr[i].gradient;
w[i][nr] = wt - bias.out * rgradient * lrate;
}
}
};
"ann.js"
虽然您可以直接使用network
对象以及网络操作来执行神经网络上的所有任务,但建议通过ann.js类来使用,该类充当一个外观模式,使库更容易使用。
import { network } from './network/network';
import { networkInitiator } from './network-operators/networkInitiator';
import { networkOperator } from './network-operators/networkOperator';
export class ann {
constructor() { this.nn = null; }
initiate(config) {
this.nn = new network(config);
networkInitiator.initNetwork(this.nn);
return this;
}
forward(data) {
return networkOperator.forward(this.nn, data);
}
train(data) {
return networkOperator.train(this.nn, data);
}
}
XOR 示例
这个库经过了良好的测试。如果您有兴趣查看所有单元测试,请随时查看Github 仓库。但在本文中,我将只向您展示如何训练神经网络来执行 XOR 运算。
import { config } from '../ann/network/config';
import { ann } from '../ann/ann';
it('Nural network training Test', () => {
let conf = new config(2, 1, [4], 'ReLU', 0.01);
let nn = new ann().initiate(conf);
let data = [];
data.push([[0, 0], [0]]);
data.push([[1, 1], [0]]);
data.push([[0, 1], [1]]);
data.push([[1, 0], [1]]);
// Train the network
for (let i = 0; i < 10000; i++) {
for (let j = 0; j < data.length; j++) {
nn.train(data[j]);
}
}
// Validate the result
let result = [];
for (let i = 0; i < data.length; i++) {
let r = nn.forward(data[i][0])[0];
result.push([...data[i][0], ...[r]])
}
console.log(result);
});
- 该神经网络配置为具有 2 个输入、1 个输出、1 个隐藏层和 4 个隐藏节点。它使用ReLU激活函数,学习率为
0.01
。 - 我对标准输入进行了 10000 次训练,以满足所需的输出。
由于程序是用 ES6 编写的,因此要运行测试,您需要首先发出以下命令来安装node_modules
。如果您的计算机上没有npm,则需要安装它。
npm install
安装node_modules
后,您可以使用以下命令运行测试。
npm run test
讨论
过拟合问题
通过 XOR 示例,我们可以看到,训练后的神经网络对相应的输入产生了完全期望的输出。但是,如果您尝试一个稍微不同的输入,例如 [0.1, 0.9],您会发现输出与期望输出 1 存在显著差异。这就是所谓的过拟合。在神经网络中,过拟合是不希望的,因为它会导致网络无法识别未用于训练的数据中的模式。为了避免过拟合,需要为训练添加额外的带噪声的训练数据。在 XOR 示例中,我们可以使用以下训练数据。
data.push([[0, 0], [0]]);
data.push([[1, 1], [0]]);
data.push([[0, 1], [1]]);
data.push([[1, 0], [1]]);
data.push([[0.1, 0.1], [0]]);
data.push([[0.9, 0.9], [0]]);
data.push([[0.1, 0.9], [1]]);
data.push([[0.9, 0.1], [1]]);
使用额外数据进行训练后,网络应该能够识别 XOR 模式,即使输入中存在轻微的噪声。
隐藏层数量
在 XOR 示例中,我使用了 1 个隐藏层,但您可以尝试使用更多的隐藏层。
let conf = new config(2, 1, [4, 4], 'ReLU', 0.01);
使用此配置,您将拥有一个包含 2 个隐藏层的网络。每个隐藏层有 4 个节点。根据我的实验,我注意到增加隐藏层确实会使训练更困难。它需要更小的学习率和更多的迭代次数。由于权重矩阵是随机生成的,因此如果学习率对于某些初始权重值不够小,则可能无法收敛。在实践中,如果较少数量的隐藏层可以满足要求,最好使用较少数量的隐藏层。
初始权重矩阵
在 XOR 示例中,我使用了随机生成的初始权重矩阵。当您尝试使用多个隐藏层时,您可能会注意到由于初始随机值,训练可能无法收敛。在实践中,通常最好从一个简单的网络和一套较小的训练数据开始。当我们获得训练数据上的期望结果时,我们可以逐步增加数据集和网络的复杂性。
关注点
- 这是一个用 JavaScript 实现神经网络的库。
- 希望您喜欢我的帖子,并希望这篇说明能对您有所帮助。
历史
- 2018 年 10 月 10 日:首次修订