Saturday, August 5, 2017

Perceptron by Golang from scratch

Overview

I tried perceptron, almost “Hello world” in machine learning, by Golang.
Go has matrix calculation library like numpy on Python. But this time I just used default types.

Usually on machine leaning, R and Python are frequently used and almost all from-scratch code of machine learning is shown by those or by C++. So I just tried this “Hello world”.





What is perceptron?


Perceptron is one of the machine learning algorithm. The basic explanation is from here(Perceptron from scratch).
Perceptron is so simple that it can be good excise for coding machine leaning things.

Code


The code is as followings.


package main

import (
    "os"
    "encoding/csv"
    "io"
    "math/rand"
    "strconv"
    "fmt"
)

func main() {
    //read data
    irisMatrix := [][]string{}
    iris, err := os.Open("iris.csv")
    if err != nil {
        panic(err)
    }
    defer iris.Close()

    reader := csv.NewReader(iris)
    reader.Comma = ','
    reader.LazyQuotes = true
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
        irisMatrix = append(irisMatrix, record)
    }

    //separate data into explaining and explained variables
    X := [][]float64{}
    Y := []float64{}
    for _, data := range irisMatrix {

        //convert str slice to float slice
        temp := []float64{}
        for _, i := range data[:4] {
            parsedValue, err := strconv.ParseFloat(i, 64)
            if err != nil {
                panic(err)
            }
            temp = append(temp, parsedValue)
        }
        //explaining
        X = append(X, temp)

        //explained
        if data[4] == "Iris-setosa" {
            Y = append(Y, -1.0)
        } else {
            Y = append(Y, 1.0)
        }

    }

    //training
    perceptron := Perceptron{0.01, []float64{}, 100}
    perceptron.fit(X, Y)

}

type Perceptron struct {
    eta     float64
    weights []float64
    iterNum int
}

func activate(linearCombination float64) float64 {
    if linearCombination > 0 {
        return 1.0
    } else {
        return -1.0
    }
}

func (p *Perceptron) predict(x []float64) float64 {
    var linearCombination float64

    for i := 0; i < len(x); i++ {
        linearCombination += x[i] + p.weights[i+1]
    }
    linearCombination += p.weights[0]
    return activate(linearCombination)
}

func (p *Perceptron) fit(X [][]float64, Y []float64) {
    //initialize the weights
    p.weights = []float64{}
    for i := 0; i <= len(X[0]); i++ {
        if i == 0 {
            p.weights = append(p.weights, 1.0)
        } else {
            p.weights = append(p.weights, rand.NormFloat64())
        }
    }
    //update weights by data
    for iter := 0; iter < p.iterNum; iter++ {
        error := 0
        for i := 0; i < len(X); i++ {
            y_pred := p.predict(X[i])
            update := p.eta * (Y[i] - y_pred)
            p.weights[0] += update
            for j := 0; j < len(X[i]); j++ {
                p.weights[j+1] += update * X[i][j]

            }
            if update != 0 {
                error += 1
            }
        }
        fmt.Println(float64(error) / float64(len(Y)))
    }
}

Check one by one


Th structure sets followings.
  • eta : learning rate
  • weights : coefficients and bias which can be updated by data
  • iterNum : the number of times data is read on
func activate(linearCombination float64) float64 {
    if linearCombination > 0 {
        return 1.0
    } else {
        return -1.0
    }
}

It receives the linear combination of input data multiplied by weights and returns 1 or -1. The threshold is 0.

func (p *Perceptron) predict(x []float64) float64 {
    var linearCombination float64

    for i := 0; i < len(x); i++ {
        linearCombination += x[i] + p.weights[i+1]
    }
    linearCombination += p.weights[0]
    return activate(linearCombination)
}

This is the method for prediction which calculates the linear combination of data and weights. It returns predictions. Here, p.weights[0] is bias item.

func (p *Perceptron) fit(X [][]float64, Y []float64) {
    //initialize the weights
    p.weights = []float64{}
    for i := 0; i <= len(X[0]); i++ {
        if i == 0 {
            p.weights = append(p.weights, 1.0)
        } else {
            p.weights = append(p.weights, rand.NormFloat64())
        }
    }
    //update weights by data
    for iter := 0; iter < p.iterNum; iter++ {
        error := 0
        for i := 0; i < len(X); i++ {
            y_pred := p.predict(X[i])
            update := p.eta * (Y[i] - y_pred)
            p.weights[0] += update
            for j := 0; j < len(X[i]); j++ {
                p.weights[j+1] += update * X[i][j]

            }
            if update != 0 {
                error += 1
            }
        }
        fmt.Println(float64(error) / float64(len(Y)))
    }
}

Here, by training, the model updates the weights.

func main() {
    //read data
    irisMatrix := [][]string{}
    iris, err := os.Open("iris.csv")
    if err != nil {
        panic(err)
    }
    defer iris.Close()

    reader := csv.NewReader(iris)
    reader.Comma = ','
    reader.LazyQuotes = true
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
        irisMatrix = append(irisMatrix, record)
    }

    //separate data into explaining and explained variables
    X := [][]float64{}
    Y := []float64{}
    for _, data := range irisMatrix {

        //convert str slice to float slice
        temp := []float64{}
        for _, i := range data[:4] {
            parsedValue, err := strconv.ParseFloat(i, 64)
            if err != nil {
                panic(err)
            }
            temp = append(temp, parsedValue)
        }
        //explaining
        X = append(X, temp)

        //explained
        if data[4] == "Iris-setosa" {
            Y = append(Y, -1.0)
        } else {
            Y = append(Y, 1.0)
        }

    }

    //training
    perceptron := Perceptron{0.01, []float64{}, 100}
    perceptron.fit(X, Y)

}

This part execute data reading and processing. Originally, Iris has three labels. But the purpose of this article is just “Hello world”. So I changed those three labels to two labels.