/ machine learning

Red Neuronal simple en JavaScript desde 0 - Parte 2

Seguimos con las mejoras al perceptrón que creamos en la parte uno, ahora con mejoras en cuanto al código y la función de activación

función de activación

La idea de utilizar la función de sigmoid, es por que oscila entre valores de 0 y 1, esto es ideal para nuestro caso, ya que la probabilidad de que llueva o no, está entre 0 y 1

Así que básicamente, aplicaremos la función tal como existe indicada, posteriormente, verificaremos contra un umbral para "redondear" la probabilidad, por ejemplo, 0.5:

const activationFunction = (value) => {
  const activation = 1 / (1 + Math.pow(Math.E, -value))
  
  return Number(activation > 0.5)
}

Ahora bien, probemos todos los casos para verificar:

const testingData = [[0, 0], [0, 1], [1, 0], [1, 1]] 

testingData.forEach(ev => {
  const prediction = predict(ev)

  console.log(`For ${ev}, the prediction is ${prediction}`)
})

Los resultados:

For 0,0, the prediction is 0
For 0,1, the prediction is 0
For 1,0, the prediction is 1
For 1,1, the prediction is 1

Como podemos observar, el perceptrón funciona de forma correcta

refactorizando

Si bien el código hasta ahora funciona, hay unas cosas que podemos mejorar en calidad:

  • Dado que la función de activación puede cambiar, sería mejor recibirla como parámetro
  • Los valores de los pesos son globales, esto quiere decir que si queremos utilizarlo para otro escenario, podríamos alterar los valores del entrenamiento anterior

encapsulamiento

Primero, hay que generar una clase para el percetron, esta clase tomara como constructor a un builder, para que podamos configurar nuestro perceptron y construirlo de una forma mas sencilla:

const random = _ => {
  const max = 1
  const min = -1
  return Math.random() * (max - min) + min
}


class Perceptron {

  constructor(builder) {
    this.weights = builder.weights
    this.activationFn = builder.activationFn
    this.learningRate = builder.learningRate
    this.usedForProbability = builder.usedForProbability
  }


  train(trainingData, results) {
    trainingData.forEach((input, index) => {
      const guess = this.predict(input)
      const error = results[index] - guess

      for (let index = 0; index < this.weights.length; index++) {
        this.weights[index] += error * input[index] * this.learningRate
      }
    })
  }

  predict(input) {
    let sum = 0 

    input.forEach((field, index) => {
      sum += field * this.weights[index]
    })

    const activated = this.activationFn(sum)

    // if we are using probabilities, just round it up
    if (this.usedForProbability) {
      return Number(activated > 0.5)
    }

    return activated
  }


  print() {
    console.log(`Using weights ${this.weights}`)
  }
}

Ahora, construyamos el builder:

class PerceptronBuilder {

  constructor() {
  }

  withShape(shape) {
    this.shape = shape
    return this
  }

  withLearningRate(lr) {
    this.learningRate = lr
    return this
  }

  usingActivationFunction(activationFn) {
    this.activationFn = activationFn
    return this
  }

  initialWeights(weights) {
    this.weigths = weights
    return this
  }

  asProbability() {
    this.usedForProbability = true
    return this
  }

  build() {
    if (this.weights == null && this.shape == null) {
      throw new Error('We need the shape if weights are not initially set')
    }

    if (this.weights == null) {
      this.weights = [] 

      for (let i = 0; i < this.shape; i++) {
        this.weights.push(random())
      }
    }

    return new Perceptron(this)
  }
}

module.exports = PerceptronBuilder

activaciones

Dado que pueden existir distintas funciones de activacion, lo que haremos en este caso será crear otro módulo, ahora, para centralizar todas las funciones que podamos llegar a tener:

const sigmoid = (value) => {
  return 1 / (1 + Math.pow(Math.E, -value))
}


module.exports = { sigmoid }

construyendo todo

// importamos el builder para construir nuestro perceptron
const PerceptronBuilder = require('./refactored.js')
const { sigmoid } = require('./activationFunctions.js')

const training = [ 
  [1, 1, 1], 
  [1, 0, 1], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0], 
  [1, 1, 1], 
  [1, 0, 1], 
  [1, 1, 1], 
  [1, 0, 1], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0], 
  [0, 1, 0]
]

// separar la informacion entre "observaciones" (llamado X)
// y los resultados de dichas observaciones (llamado Y)

// en este caso, obtenemos posiciones 0 y 1
const x = training.map(t => t.slice(0, 2))
// para el resultado, podemos accesar directamente a la posicion 2
const y = training.map(t => t[2])

// creamos el perceptron
const perceptron = new PerceptronBuilder()
    .withLearningRate(0.4)
    .usingActivationFunction(sigmoid)
    .withShape(2)
    .asProbability()
    .build()


// entrenar el modelo
perceptron.train(x, y)

const testingData = [[0, 0], [0, 1], [1, 0], [1, 1]]

testingData.forEach(ev => {
  const prediction = perceptron.predict(ev)

  console.log(`For ${ev}, the prediction is ${prediction}`)
})

console.log(perceptron.print())

Como podemos ver en este snippet, no solo el codigo es mas claro y entendible, pero ya llegamos a ver de manera más directa los pasos comunes en una tarea de machine learning:

  • obtención de la información
  • limpieza (o en nuestro caso, formato) de la información de entrada
  • entrenamiento
  • prueba del "modelo" (el perceptrón)

El código puede ser encontrado en este repositorio en github