65.9K
CodeProject 正在变化。 阅读更多。
Home

关于神经网络的说明

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (1投票)

2018 年 10 月 10 日

CPOL

6分钟阅读

viewsIcon

12288

downloadIcon

148

这是一个用 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 日:首次修订
© . All rights reserved.