用 JavaScript 编写神经网络(2020)- 神经网络简介

2025-06-07

用 JavaScript 编写神经网络(2020)- 神经网络简介

什么是神经元和神经网络?

在生物学中,神经元是一种通过称为突触的特殊连接与其他细胞进行通信的细胞。

正如我们在下图中看到的,一个神经元有一组不同大小和形状的连接。

在软件中,神经元(人工神经元)是一种数学函数,被认为是生物神经元的模型。
人工神经元具有一组连接(x1、x2、x3),并具有不同的权重(w1、w2、w3)。

当神经元利用输入连接(x1,x2,x3 … w1,w2,w3)的值执行函数(o)时,它本身会产生输出(y)。
简单来说,神经元就是一个根据输入值给出值的函数。

软件人工神经网络是相互连接的神经元的集合,它们代表一个数学函数,可以模拟我们想要完成的事情。

现实生活可以分解成数学。想象一下,你想编写代码来识别图片中的猫。这将需要你投入大量时间和复杂的数学运算。想象一下:将图像分解成像素组,猜测哪些特征代表猫,并分析每组像素是否对应其中一种特征。听起来很复杂。
这时,神经网络就派上用场了。神经网络可以通过训练来学习如何解决特定问题。

神经网络一开始是有点“随机”的。它们由随机值生成,并针对一组数据(数据集)进行训练。它们会不断自我调整,学习如何给出预期的结果。一旦网络被训练完成,它就能利用从未见过的新数据进行预测。

因此,如果你用数千张猫咪图片训练一个网络,它就能识别出你展示的是一只猫。但当你展示的是一栋房子时,它却无法识别。你已经训练好了网络,现在网络只包含模拟猫咪特征的函数(或代码),其他什么都没有。
学习神经网络的最佳资源之一是 3blue1Brown 的下一个视频

https://www.youtube.com/watch?v=aircAruvnKk

编写我们的第一个神经网络

我们要建模的是这样的:

我们要创建以下实体:

  • 神经元:具有输入连接、输出连接和偏差
  • 连接:具有“起点”神经元、“终点”神经元和权重。
  • 层:具有神经元和激活函数
  • 网络:有层

有了上面这个简单的 JavaScript 神经网络,我们就能神奇地自动编写简单的逻辑门(AND、OR、XOR 等)。这些逻辑门可以用普通函数轻松编写,但我们将展示神经网络如何自动解决这类问题。
掌握这些知识后,你将能够理解机器学习的基础知识,并将其应用于其他领域。
对于更专业的应用,我们建议你使用 TensorFlow 或 Pytorch 等可靠的框架。
让我们从零开始,用 JavaScript 编写我们的第一个神经网络代码。
在本例中,我们将使用面向对象编程,结合 ES6 类和单元测试。
你可以在以下代码库中找到本教程的所有代码:https://github.com/rafinskipg/neural-network-js

Neuron.js

如您所见,神经元的大部分代码都是可以省略的样板(设置器、打印函数等),唯一重要的是:

  • 偏见
  • 三角洲
  • 输出
  • 错误
  • 连接
import uid from './uid'
class Neuron {
  constructor() {
    this.inputConnections = []
    this.outputConnections = []
    this.bias = 0
    // delta is used to store a percentage of change in the weight
    this.delta = 0
    this.output = 0
    this.error = 0
    this.id = uid()
  }

  toJSON() {
    return {
      id: this.id,
      delta: this.delta,
      output: this.output,
      error: this.error,
      bias: this.bias,
      inputConnections: this.inputConnections.map(i => i.toJSON()),
      outputConnections: this.outputConnections.map(i => i.toJSON())
    }
  }

  getRandomBias() {
    const min = -3;
    const max = 3
    return Math.floor(Math.random() * (+max - +min)) +min; 
  }

  addInputConnection(connection) {
    this.inputConnections.push(connection)
  }

  addOutputConnection(connection) {
    this.outputConnections.push(connection)
  }

  setBias(val) {
    this.bias = val
  }

  setOutput(val) {
    this.output = val
  }

  setDelta(val) {
    this.delta = val
  }

  setError(val) {
    this.error = val
  }
}

export default Neuron
Enter fullscreen mode Exit fullscreen mode

联系

连接从一个神经元连接到另一个神经元,并具有权重。
我们还将存储变化属性,以便在反向传播阶段了解权重在迭代之间应该变化多少。

class Connection {
  constructor(from, to) {
    this.from = from
    this.to = to
    this.weight = Math.random()
    this.change = 0
  }

  toJSON() {
    return {
      change: this.change,
      weight: this.weight,
      from: this.from.id,
      to: this.to.id
    }
  }

  setWeight(w) {
    this.weight = w
  }

  setChange(val) {
    this.change = val
  }
}

export default Connection
Enter fullscreen mode Exit fullscreen mode

层只是神经元的集合。
我们new Layer(5);创建了一个包含 5 个神经元的层。


import Neuron from './neuron'

class Layer {
  constructor(numberOfNeurons) {
    const neurons = []
    for (var j = 0; j < numberOfNeurons; j++) {
      const neuron = new Neuron()
      neurons.push(neuron)
    }

    this.neurons = neurons
  }

  toJSON() {
    return this.neurons.map(n => {
      return n.toJSON()
    })
  }
}

export default Layer
Enter fullscreen mode Exit fullscreen mode

目前来说很简单,对吧?

让我们快速回顾一下:我们现在只有 3 个不同的概念或类,我们可以像这样简单地使用它们:

var myLayer = new Layer(5); // create a layer of 5 neurons
// Create a connection
var connection = new Connection(myLayer.neurons[0], myLayer.neurons[1])
// Store references to the connection in the neurons
myLayer.neurons[0].addOutputConnection(connection)
myLayer.neurons[1].addInputConnection(connection)
Enter fullscreen mode Exit fullscreen mode

基本上,要创建一个网络,我们只需要不同的层,每个层有不同的神经元,以及具有权重的不同连接。

为了对此进行建模,您可以使用另一种抽象,而不必遵循我的做法。例如,您可以只创建一个对象矩阵,并存储所有数据,而无需使用类。我使用面向对象编程 (OOP) 是因为它更容易让我学习易于建模的新概念。

网络

在创建网络(层组)之前,我们应该了解一些事情。1
- 我们需要创建各种层 2 - 输入层神经元没有输入连接,只有输出 3 - 输出层神经元没有输出连接,只有输入 4 - 所有神经元都使用随机偏差值创建。输入层中的神经元除外,它们将具有输入值。输入值是我们将用来给出预测或结果的数据。例如,在 28x28 的图像中,它将是 784 像素的数据。在逻辑门中,它将是 2 个值(0 或 1)。5 - 在每个训练步骤中,我们将向输入层(训练数据)提供一些值,然后计算输出并应用反向传播重新计算连接的权重。6 - 反向传播是一种根据期望输出与实际输出之间的误差差异来调整连接权重的方法。经过多次执行后,网络会给出更接近预期的结果。这就是训练网络。 在我们看到所有网络代码之前,我们需要了解神经元如何在每次迭代中计算它自己的值。









const bias = this.layers[layer].neurons[neuron].bias
// For each neuron in this layer we compute its output value, 
// the output value is obtained from all the connections comming to this neuron
const connectionsValue = this.layers[layer].neurons[neuron].inputConnections.reduce((prev, conn)  => {
  const val = conn.weight * conn.from.output
  return prev + val
}, 0)
this.layers[layer].neurons[neuron].setOutput(sigmoid(bias + connectionsValue))
Enter fullscreen mode Exit fullscreen mode

我们通过将之前连接的所有权重与输出的乘积相加来计算神经元的输出。这意味着,获取连接到该神经元的所有连接,对于每个连接,我们将权重与输出相乘,然后将其加到总和中。一旦我们得到所有乘积的总和,我们将应用 Sigmoid 函数来对输出进行归一化。

什么是 S 型函数?

S 型函数是一种具有特征性“S”形曲线或 S 型曲线的数学函数。
在神经网络中,S 型函数用于将神经元的值归一化到 0 到 1 之间。
神经网络使用不同类型的函数,这些函数被称为激活函数。一些最常用的激活函数包括 S 型函数、Tanh 函数或 ReLU 函数。

您可以在此处阅读有关激活函数的更深入的解释

现在我们只使用用 JavaScript 编写的 sigmoid 函数:

function sigmoid(z) {  
  return 1 / (1 + Math.exp(-z));
} 

export default sigmoid
Enter fullscreen mode Exit fullscreen mode

现在让我们看一下完整的网络代码。

网络中发生了很多事情:

  • 网络将所有神经元从一层连接到下一层
  • 当网络训练时,它会运行该runInputSigmoid方法,该方法使用 S 型函数作为激活函数。
  • 反向传播是通过计算权重所需的变化量(delta)并应用它来完成的。计算权重和delta的代码很复杂。
  • run方法仅调用runInputSigmoid以给出结果
import sigmoid from './sigmoid'
import Connection from './connection'
import Layer from './layer'

class Network {
  constructor(numberOfLayers) {
    // Create a network with a number of layers. For layers different than the input layer we add a random Bias to each neuron
    this.layers = numberOfLayers.map((length, index) => {
      const layer = new Layer(length) 
      if (index !== 0 ) {
        layer.neurons.forEach(neuron => {
          neuron.setBias(neuron.getRandomBias())
        })
      }
      return layer
    })

    this.learningRate = 0.3  // multiply's against the input and the delta then adds to momentum
    this.momentum =  0.1  // multiply's against the specified "change" then adds to learning rate for change

    this.iterations = 0 // number of iterations in the training process
    this.connectLayers()
  }

  toJSON() {
    return {
      learningRate: this.learningRate,
      iterations: this.iterations,
      layers: this.layers.map(l => l.toJSON())
    }
  }

  setLearningRate(value) {
    this.learningRate = value
  }

  setIterations(val) {
    this.iterations = val
  }

  connectLayers() {
    // Connects current layer with the previous one. This is for a fully connected network
    // (each neuron connects with all the neurons from the previous layer)
    for (var layer = 1; layer < this.layers.length; layer++) {
      const thisLayer = this.layers[layer]
      const prevLayer = this.layers[layer - 1]
      for (var neuron = 0; neuron < prevLayer.neurons.length; neuron++) {
        for(var neuronInThisLayer = 0; neuronInThisLayer < thisLayer.neurons.length; neuronInThisLayer++) {
          const connection = new Connection(prevLayer.neurons[neuron], thisLayer.neurons[neuronInThisLayer])
          prevLayer.neurons[neuron].addOutputConnection(connection)
          thisLayer.neurons[neuronInThisLayer].addInputConnection(connection)
        }
      }
    }
  }

  // When training we will run this set of functions each time
  train(input, output) {
    // Set the input data on the first layer
    this.activate(input)

    // Forward propagate
    this.runInputSigmoid()

    // backpropagate
    this.calculateDeltasSigmoid(output)
    this.adjustWeights()

    // You can use as a debugger
    // console.log(this.layers.map(l => l.toJSON()))

    this.setIterations(this.iterations + 1)
  }

  activate(values) {
    this.layers[0].neurons.forEach((n, i) => {
      n.setOutput(values[i])
    })
  }

  run() {
    // For now we only use sigmoid function
    return this.runInputSigmoid()
  }

  runInputSigmoid() {
    for (var layer = 1; layer < this.layers.length; layer++) {
      for (var neuron = 0; neuron < this.layers[layer].neurons.length; neuron++) {
        const bias = this.layers[layer].neurons[neuron].bias
        // For each neuron in this layer we compute its output value, 
        // the output value is obtained from all the connections comming to this neuron

        const connectionsValue = this.layers[layer].neurons[neuron].inputConnections.reduce((prev, conn)  => {
          const val = conn.weight * conn.from.output
          return prev + val
        }, 0) 

        this.layers[layer].neurons[neuron].setOutput(sigmoid(bias + connectionsValue))
      }
    }

    return this.layers[this.layers.length - 1].neurons.map(n => n.output)
  }

  calculateDeltasSigmoid(target) {
    // calculates the needed change of weights for backpropagation, based on the error rate
    // It starts in the output layer and goes back to the first layer
    for (let layer = this.layers.length - 1; layer >= 0; layer--) {
      const currentLayer = this.layers[layer]

      for (let neuron = 0; neuron < currentLayer.neurons.length; neuron++) {
        const currentNeuron = currentLayer.neurons[neuron]
        let output = currentNeuron.output;

        let error = 0;
        if (layer === this.layers.length -1 ) {
          // Is output layer, 
          // the error is the difference between the expected result and the current output of this neuron
          error = target[neuron] - output;
          // console.log('calculate delta, error, last layer', error)
        }
        else {
          // Other than output layer
          // the error is the sum of all the products of the output connection neurons * the connections weight
          for (let k = 0; k < currentNeuron.outputConnections.length; k++) {
            const currentConnection = currentNeuron.outputConnections[k]
            error += currentConnection.to.delta * currentConnection.weight
            // console.log('calculate delta, error, inner layer', error)
          }

        }
        currentNeuron.setError(error)
        currentNeuron.setDelta(error * output * (1 - output))
      }
    }
  }

  adjustWeights() {
    // we start adjusting weights from the output layer back to the input layer
    for (let layer = 1; layer <= this.layers.length -1; layer++) {
      const prevLayer = this.layers[layer - 1]
      const currentLayer = this.layers[layer]

      for (let neuron = 0; neuron < currentLayer.neurons.length; neuron++) {
         const currentNeuron = currentLayer.neurons[neuron]
         let delta = currentNeuron.delta

        for (let i = 0; i < currentNeuron.inputConnections.length; i++) {
          const currentConnection = currentNeuron.inputConnections[i]
          let change = currentConnection.change

          // The change on the weight of this connection is:
          // the learningRate * the delta of the neuron * the output of the input neuron + (the connection change * momentum)
          change = (this.learningRate * delta * currentConnection.from.output)
              + (this.momentum * change);

          currentConnection.setChange(change)
          currentConnection.setWeight(currentConnection.weight + change)
        }

        currentNeuron.setBias(currentNeuron.bias + (this.learningRate * delta))

      }
    }
  }

}

export default Network
Enter fullscreen mode Exit fullscreen mode

我不会解释为什么用这个公式计算增量和权重。反向传播是一个复杂的主题,需要你自己进行深入研究。我提供一些资源供你参考:

有了网络代码,你就能运行反向传播来训练它。但重要的是,你需要花时间详细阐述你的想法。

编写测试来训练我们的网络:

示例存储库中,您将找到允许以不同方式训练网络的不同测试:

以下是我们对异或门的测试,它将作为一个完整的示例,展示如何将此网络用于不同的用途。
您可以尝试针对不同的情况训练网络,看看会发生什么。

import Network from '../network'

// Training data for a xor gate
const trainingData = [{
  input : [0,0],
  output: [0]
}, {
  input : [0,1],
  output: [1]
}, {
  input : [1,0],
  output: [1]
}, {
  input : [1,1],
  output: [0]
}]


describe('XOR Gate', () => {
  let network

  beforeAll(done => {
    // Create the network
    network = new Network([2, 10, 10, 1])

    // Set a learning rate
    const learningRate = 0.3
    network.setLearningRate(learningRate)

    // Train the network
    for(var i = 0; i < 20000  ; i ++) {
      const trainingItem = trainingData[Math.floor((Math.random()*trainingData.length))]
      // Randomly train
      network.train(trainingItem.input, trainingItem.output);
    }

    done()

  })

  it('should return 0 for a [0,0] input', () => {
    network.activate([0, 0])
    const result = network.runInputSigmoid()
    expect(Math.round(result[0])).toEqual(0)
  })

  it('should return 1 for a [0,1] input', () => {
    network.activate([0, 1])
    const result = network.runInputSigmoid()
    expect(Math.round(result[0])).toEqual(1)
  })

  it('should return 1 for a [1,0] input', () => {
    network.activate([1, 0])
    const result = network.runInputSigmoid()
    expect(Math.round(result[0])).toEqual(1)
  })

  it('should return 0 for a [1,1] input', () => {
    network.activate([1, 1])
    const result = network.runInputSigmoid()
    expect(Math.round(result[0])).toEqual(0)
  })
})
Enter fullscreen mode Exit fullscreen mode

如果您想要做一些需要使用 GPU 进行训练(更强的计算能力)或更复杂的层的事情,您可能需要使用更高级的库,例如:

但请记住,您刚刚编写了一个神经网络,现在您知道如何深入研究它们了!

文章来源:https://dev.to/venture/writing-a-neural-network-in-javascript-2020-intro-to-neural-networks-2c4n
PREV
在 Windows 上设置 Linux Javascript 开发环境
NEXT
支持科技界女性的三种方式