【機械学習】Goでパーセプトロン実装

  • 5
    Like
  • 0
    Comment

Goで機械学習フルスクラッチシリーズ

この記事は、自身のブログ、Data Science Struggleでも掲載予定。許可なき掲載とかではない。

概略

Goでパーセプトロンを書いてみる。最近Goの勉強を始めたので機械学習界のHello world、パーセプトロンを書いていく。
GoにもPythonのnumpyのような行列計算ライブラリはあるが、今回はそれは使わずに標準提供されているTypeのみを用いて作成する。
機械学習というと、ここ最近はRかPythonで行うのが一般的で、sklearnなどのライブラリを使用していないコードも大抵はPythonでの例なのでお試しがてら作ってみる。

パーセプトロンとは?

機械学習アルゴリズムの一種。基本的な説明はライブラリに頼らないパーセプトロン実装のほうで書いたのでそちらを参照。
仕組みがシンプルであり、機械学習コーディングの練習の第一歩にはちょうど良い。

コード

コードは以下の通り。使用しているデータはirisだが、今回はHello worldが目的なので、irisデータのラベルを勝手に二つにして使用する。(本来は三つのラベルがある。)


package main

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

func main() {
    //データ読み込み
    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)
    }

    //説明変数と被説明変数にデータを分割
    X := [][]float64{}
    Y := []float64{}
    for _, data := range irisMatrix {

        //strスライスデータをfloatスライスデータに変換
        temp := []float64{}
        for _, i := range data[:4] {
            parsedValue, err := strconv.ParseFloat(i, 64)
            if err != nil {
                panic(err)
            }
            temp = append(temp, parsedValue)
        }
        //説明変数へ
        X = append(X, temp)

        //被説明変数
        if data[4] == "Iris-setosa" {
            Y = append(Y, -1.0)
        } else {
            Y = append(Y, 1.0)
        }

    }

    //学習
    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) {
    //重みの初期化
    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())
        }
    }
    //データによる重み更新
    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)))
    }
}

パーツごとに見ていく

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

構造体で設定しているのはそれぞれ、

  • eta : 学習係数
  • weights : 重み
  • iterNum : 学習のためのデータの読み込ませ回数

となる。

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

入力データと重みとの線形結合値を入力として受付け、1か-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)
}

予測のためのメソッド。データと重みとの線形結合値を計算しactivate()に投げてデータに対する予測値を返す。ここで、p.weights[0]はバイアス項。


func (p *Perceptron) fit(X [][]float64, Y []float64) {
    //重みの初期化
    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())
        }
    }
    //データによる重み更新
    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)))
    }
}

学習部分。データを読み込んで重みを更新していく。

func main() {
    //データ読み込み
    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)
    }

    //説明変数と被説明変数にデータを分割
    X := [][]float64{}
    Y := []float64{}
    for _, data := range irisMatrix {

        //strスライスデータをfloatスライスデータに変換
        temp := []float64{}
        for _, i := range data[:4] {
            parsedValue, err := strconv.ParseFloat(i, 64)
            if err != nil {
                panic(err)
            }
            temp = append(temp, parsedValue)
        }
        //説明変数へ
        X = append(X, temp)

        //被説明変数
        if data[4] == "Iris-setosa" {
            Y = append(Y, -1.0)
        } else {
            Y = append(Y, 1.0)
        }

    }

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

}

実行部。データの読み込みと予測を行う前の処理を行なっている。本来Irisデータは三つの分類先を持つが、今回は『Hello world』なので、簡便のために分類先を二つに変更している。

感想

Goでの機械学習コーディングの練習だったが、書いていて、『これだとかなり無駄が多いんだろなー』といった感じ。Goでのデータ操作のノウハウを覚えていきたい。