動手造輪子自己實現人工智慧神經網路(ANN),解決鳶尾花分類問題Golang1.18實現

劉悅的技術部落格發表於2023-03-28

人工智慧神經網路( Artificial Neural Network,又稱為ANN)是一種由人工神經元組成的網路結構,神經網路結構是所有機器學習的基本結構,換句話說,無論是深度學習還是強化學習都是基於神經網路結構進行構建。關於人工神經元,請參見:人工智慧機器學習底層原理剖析,人造神經元,您一定能看懂,通俗解釋把AI“黑話”轉化為“白話文”

機器學習可以解決什麼問題

機器學習可以幫助我們解決兩大類問題:迴歸問題和分類問題,它們的主要區別在於輸出變數的型別和預測目標的不同。

在迴歸問題中,輸出變數是連續值,預測目標是預測一個數值。例如,預測房價、預測銷售額等都是迴歸問題。通常使用迴歸模型,如線性迴歸、決策樹迴歸、神經網路迴歸等來解決這類問題。迴歸問題的評估指標通常是均方誤差(Mean Squared Error,MSE)、平均絕對誤差(Mean Absolute Error,MAE)等。

在分類問題中,輸出變數是離散值,預測目標是將樣本劃分到不同的類別中。例如,預測郵件是否是垃圾郵件、預測影像中的物體類別等都是分類問題。通常使用分類模型,如邏輯迴歸、決策樹分類、支援向量機、神經網路分類等來解決這類問題。分類問題的評估指標通常是準確率、精度(Precision)、召回率(Recall)等。

事實上,機器學習只能解決“可以”被解決的問題,也就是說,機器學習能幫我們做的是提高解決問題的效率,而不是解決我們本來解決不了的問題,說白了,機器學習只能解決人目前能解決的問題,比如說人現在不能做什麼?人不能永生,不能白日飛昇,也不能治癒絕症,所以你指望機器學習解決此類問題,就是痴心妄想。

同時,機器學習輸入的特徵引數和輸出的預期結果必須有邏輯相關性,什麼意思?比如說我們想預測房價,結果特徵引數輸入了很多沒有任何邏輯相關性的資料,比如歷年水稻的出產率,這就是沒有邏輯相關性的資料,這樣的問題再怎麼調參也是無法透過機器學習來解決的。

此外,迴歸問題中有一個領域非常引人關注,那就是預測股票價格,國內經常有人說自己訓練的模型可以預測某支A股的價格走勢,甚至可以精準到具體價格單位。說實話,挺滑稽的,關鍵是還真有人相信靠機器學習能在A股市場大殺特殺。

因為,稍微有點投資經驗的人都知道,股票的歷史資料和未來某個時間點或者某個時間段的實際價格,並不存在因果關係,尤其像A股市場這種可被操控的黑盒環境,連具體特徵都是隱藏的,或者說特徵是什麼都是未知的,你以為的特徵只是你以為的,並不是市場或者政策以為的,所以你輸入之前十年或者二十年的歷史股票資料,你讓它預測,就是在搞笑,機器學習沒法幫你解決此類問題。

為什麼現在GPT模型現在這麼火?是因為它在NLP(自然語言分析)領域有了質的突破,可以透過大資料模型聯絡上下文關係生成可信度高的回答,而這個上下文關係,就是我們所謂的引數和預期結果的因果關係。

鳶尾花分類問題

鳶尾花分類問題是一個經典的機器學習問題,也是神經網路入門的常用案例之一。它的目標是透過鳶尾花的花萼長度、花萼寬度、花瓣長度和花瓣寬度這四個特徵來預測鳶尾花的品種,分為三種:山鳶尾(Iris Setosa)、變色鳶尾(Iris Versicolour)和維吉尼亞鳶尾(Iris Virginica)。

通俗來講,就是我們要訓練一個神經網路模型,它能夠根據鳶尾花的四個特徵,自動地對鳶尾花的品種進行分類。

在這個案例中,我們使用了一個包含一個隱藏層的神經網路,它的輸入層有4個神經元,代表鳶尾花的4個特徵;隱藏層有3個神經元;輸出層有3個神經元,分別代表3種鳶尾花的品種:

由此可見,神經網路通常由三層組成:輸入層、隱藏層和輸出層。

輸入層:輸入層接收外部輸入訊號,是神經網路的起點。它的神經元數量與輸入特徵的數量相同,每個神經元代表一個輸入特徵。輸入層的主要作用是將外部輸入轉換為神經網路內部的訊號。

隱藏層:隱藏層位於輸入層和輸出層之間,是神經網路的核心部分。它的神經元數量可以根據問題的複雜度自由設定,每個神經元接收上一層神經元輸出的訊號,並進行加權處理和啟用函式處理,再將結果傳遞給下一層神經元。隱藏層的主要作用是對輸入訊號進行復雜的非線性轉換,提取出輸入訊號中的特徵,從而使得神經網路能夠對複雜的問題進行處理。

輸出層:輸出層是神經網路的終點,它的神經元數量通常與問題的輸出數量相同。每個神經元代表一個輸出結果,輸出層的主要作用是將隱藏層處理後的訊號進行進一步處理,並將最終的結果輸出。

在神經網路中,輸入訊號從輸入層開始,透過隱藏層的處理,最終到達輸出層。每一層的神經元都與下一層的神經元相連,它們之間的連線可以看成是一種權重關係,權重值代表了兩個神經元之間的相關性強度。當神經網路接收到輸入訊號後,每個神經元都會對這些訊號進行加權處理,並透過啟用函式將結果輸出給下一層神經元,最終形成輸出結果。透過不斷調整權重和啟用函式,神經網路可以學習到輸入和輸出之間的複雜非線性關係,從而對未知資料進行預測和分類等任務。

定義神經網路結構體

在開始訓練之前,我們先定義一些需要的結構體和函式:

// neuralNet contains all of the information  
// that defines a trained neural network.  
type neuralNet struct {  
	config  neuralNetConfig  
	wHidden *mat.Dense  
	bHidden *mat.Dense  
	wOut    *mat.Dense  
	bOut    *mat.Dense  
}  
  
// neuralNetConfig defines our neural network  
// architecture and learning parameters.  
type neuralNetConfig struct {  
	inputNeurons  int  
	outputNeurons int  
	hiddenNeurons int  
	numEpochs     int  
	learningRate  float64  
}

這裡neuralNet是神經網路結構體,同時定義輸入、隱藏和輸出層神經元的配置。

隨後宣告函式初始化神經網路:

func newNetwork(config neuralNetConfig) *neuralNet {  
        return &neuralNet{config: config}  
}

這裡返回神經網路的指標。

除此之外,我們還需要定義啟用函式及其導數,這是在反向傳播過程中需要使用的。啟用函式有很多選擇,但在這裡我們將使用sigmoid函式。這個函式有很多優點,包括機率解釋和方便的導數表示式:

// sigmoid implements the sigmoid function  
// for use in activation functions.  
func sigmoid(x float64) float64 {  
        return 1.0 / (1.0 + math.Exp(-x))  
}  
  
// sigmoidPrime implements the derivative  
// of the sigmoid function for backpropagation.  
func sigmoidPrime(x float64) float64 {  
    return sigmoid(x) * (1.0 - sigmoid(x))  
}

實現反向傳播

反向傳播是指在前向傳播之後,計算神經網路誤差並將誤差反向傳播到各層神經元中進行引數(包括權重和偏置)的更新。在反向傳播過程中,首先需要計算網路的誤差,然後透過鏈式法則將誤差反向傳播到各層神經元,以更新每個神經元的權重和偏置。這個過程也被稱為“反向梯度下降”,因為它是透過梯度下降演算法來更新神經網路引數的。

說白了,反向傳播就是逆運算,用結果反推過程,這裡我們可以編寫一個實現反向傳播方法的方法,用於訓練或最佳化我們網路的權重和偏置。反向傳播方法包括以下步驟:

1 初始化權重和偏置(例如,隨機初始化)。

2 將訓練資料輸入神經網路中進行前饋,以生成輸出。

3 將輸出與正確輸出進行比較,以獲取誤差。

4 基於誤差計算權重和偏置的變化。

5 將變化透過神經網路進行反向傳播。

對於給定的迭代次數或滿足停止條件時,重複步驟2-5。

在步驟3-5中,我們將利用隨機梯度下降(SGD)來確定權重和偏置的更新:

// train trains a neural network using backpropagation.  
func (nn *neuralNet) train(x, y *mat.Dense) error {  
  
    // Initialize biases/weights.  
    randSource := rand.NewSource(time.Now().UnixNano())  
    randGen := rand.New(randSource)  
  
    wHidden := mat.NewDense(nn.config.inputNeurons, nn.config.hiddenNeurons, nil)  
    bHidden := mat.NewDense(1, nn.config.hiddenNeurons, nil)  
    wOut := mat.NewDense(nn.config.hiddenNeurons, nn.config.outputNeurons, nil)  
    bOut := mat.NewDense(1, nn.config.outputNeurons, nil)  
  
    wHiddenRaw := wHidden.RawMatrix().Data  
    bHiddenRaw := bHidden.RawMatrix().Data  
    wOutRaw := wOut.RawMatrix().Data  
    bOutRaw := bOut.RawMatrix().Data  
  
    for _, param := range [][]float64{  
        wHiddenRaw,  
        bHiddenRaw,  
        wOutRaw,  
        bOutRaw,  
    } {  
        for i := range param {  
            param[i] = randGen.Float64()  
        }  
    }  
  
    // Define the output of the neural network.  
    output := new(mat.Dense)  
  
    // Use backpropagation to adjust the weights and biases.  
    if err := nn.backpropagate(x, y, wHidden, bHidden, wOut, bOut, output); err != nil {  
        return err  
    }  
  
    // Define our trained neural network.  
    nn.wHidden = wHidden  
    nn.bHidden = bHidden  
    nn.wOut = wOut  
    nn.bOut = bOut  
  
    return nil  
}

接著實現具體的反向傳播邏輯:

// backpropagate completes the backpropagation method.  
func (nn *neuralNet) backpropagate(x, y, wHidden, bHidden, wOut, bOut, output *mat.Dense) error {  
  
    // Loop over the number of epochs utilizing  
    // backpropagation to train our model.  
    for i := 0; i < nn.config.numEpochs; i++ {  
  
        // Complete the feed forward process.  
        hiddenLayerInput := new(mat.Dense)  
        hiddenLayerInput.Mul(x, wHidden)  
        addBHidden := func(_, col int, v float64) float64 { return v + bHidden.At(0, col) }  
        hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)  
  
        hiddenLayerActivations := new(mat.Dense)  
        applySigmoid := func(_, _ int, v float64) float64 { return sigmoid(v) }  
        hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)  
  
        outputLayerInput := new(mat.Dense)  
        outputLayerInput.Mul(hiddenLayerActivations, wOut)  
        addBOut := func(_, col int, v float64) float64 { return v + bOut.At(0, col) }  
        outputLayerInput.Apply(addBOut, outputLayerInput)  
        output.Apply(applySigmoid, outputLayerInput)  
  
        // Complete the backpropagation.  
        networkError := new(mat.Dense)  
        networkError.Sub(y, output)  
  
        slopeOutputLayer := new(mat.Dense)  
        applySigmoidPrime := func(_, _ int, v float64) float64 { return sigmoidPrime(v) }  
        slopeOutputLayer.Apply(applySigmoidPrime, output)  
        slopeHiddenLayer := new(mat.Dense)  
        slopeHiddenLayer.Apply(applySigmoidPrime, hiddenLayerActivations)  
  
        dOutput := new(mat.Dense)  
        dOutput.MulElem(networkError, slopeOutputLayer)  
        errorAtHiddenLayer := new(mat.Dense)  
        errorAtHiddenLayer.Mul(dOutput, wOut.T())  
  
        dHiddenLayer := new(mat.Dense)  
        dHiddenLayer.MulElem(errorAtHiddenLayer, slopeHiddenLayer)  
  
        // Adjust the parameters.  
        wOutAdj := new(mat.Dense)  
        wOutAdj.Mul(hiddenLayerActivations.T(), dOutput)  
        wOutAdj.Scale(nn.config.learningRate, wOutAdj)  
        wOut.Add(wOut, wOutAdj)  
  
        bOutAdj, err := sumAlongAxis(0, dOutput)  
        if err != nil {  
            return err  
        }  
        bOutAdj.Scale(nn.config.learningRate, bOutAdj)  
        bOut.Add(bOut, bOutAdj)  
  
        wHiddenAdj := new(mat.Dense)  
        wHiddenAdj.Mul(x.T(), dHiddenLayer)  
        wHiddenAdj.Scale(nn.config.learningRate, wHiddenAdj)  
        wHidden.Add(wHidden, wHiddenAdj)  
  
        bHiddenAdj, err := sumAlongAxis(0, dHiddenLayer)  
        if err != nil {  
            return err  
        }  
        bHiddenAdj.Scale(nn.config.learningRate, bHiddenAdj)  
        bHidden.Add(bHidden, bHiddenAdj)  
    }  
  
    return nil  
}

接著宣告一個工具函式,它幫助我們沿一個矩陣維度求和,同時保持另一個維度不變:

// sumAlongAxis sums a matrix along a particular dimension,   
// preserving the other dimension.  
func sumAlongAxis(axis int, m *mat.Dense) (*mat.Dense, error) {  
  
        numRows, numCols := m.Dims()  
  
        var output *mat.Dense  
  
        switch axis {  
        case 0:  
                data := make([]float64, numCols)  
                for i := 0; i < numCols; i++ {  
                        col := mat.Col(nil, i, m)  
                        data[i] = floats.Sum(col)  
                }  
                output = mat.NewDense(1, numCols, data)  
        case 1:  
                data := make([]float64, numRows)  
                for i := 0; i < numRows; i++ {  
                        row := mat.Row(nil, i, m)  
                        data[i] = floats.Sum(row)  
                }  
                output = mat.NewDense(numRows, 1, data)  
        default:  
                return nil, errors.New("invalid axis, must be 0 or 1")  
        }  
  
        return output, nil  
}

實現前向傳播進行預測

在訓練完我們的神經網路之後,我們希望使用它進行預測。為此,我們只需要將一些給定的鳶尾花特徵值輸入到網路中進行前向傳播,用來生成輸出。

有點像反向傳播邏輯,不同之處在於,這裡我們將返回生成的輸出:

// predict makes a prediction based on a trained  
// neural network.  
func (nn *neuralNet) predict(x *mat.Dense) (*mat.Dense, error) {  
  
    // Check to make sure that our neuralNet value  
    // represents a trained model.  
    if nn.wHidden == nil || nn.wOut == nil {  
        return nil, errors.New("the supplied weights are empty")  
    }  
    if nn.bHidden == nil || nn.bOut == nil {  
        return nil, errors.New("the supplied biases are empty")  
    }  
  
    // Define the output of the neural network.  
    output := new(mat.Dense)  
  
    // Complete the feed forward process.  
    hiddenLayerInput := new(mat.Dense)  
    hiddenLayerInput.Mul(x, nn.wHidden)  
    addBHidden := func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }  
    hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)  
  
    hiddenLayerActivations := new(mat.Dense)  
    applySigmoid := func(_, _ int, v float64) float64 { return sigmoid(v) }  
    hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)  
  
    outputLayerInput := new(mat.Dense)  
    outputLayerInput.Mul(hiddenLayerActivations, nn.wOut)  
    addBOut := func(_, col int, v float64) float64 { return v + nn.bOut.At(0, col) }  
    outputLayerInput.Apply(addBOut, outputLayerInput)  
    output.Apply(applySigmoid, outputLayerInput)  
  
    return output, nil  
}

準備特徵和期望資料

下面我們需要準備鳶尾花的特徵和期望資料,可以在加州大學官網下載:https://archive.ics.uci.edu/ml/datasets/iris

這裡包含花瓣和花蕊的具體資料,以及這些樣本所對應的花的種類,分別對應上文提到的山鳶尾(Iris Setosa)、維吉尼亞鳶尾(Iris Virginica)和 變色鳶尾(Iris Versicolour),注意鳶尾花種類順序分先後,分別對應上表中的資料。

開始訓練

訓練之前,需要安裝基於Golang的浮點庫:

go get gonum.org/v1/gonum/floats

安裝後之後,編寫指令碼:

package main  
  
import (  
	"encoding/csv"  
	"errors"  
	"fmt"  
	"log"  
	"math"  
	"math/rand"  
	"os"  
	"strconv"  
	"time"  
  
	"gonum.org/v1/gonum/floats"  
	"gonum.org/v1/gonum/mat"  
)  
  
// neuralNet contains all of the information  
// that defines a trained neural network.  
type neuralNet struct {  
	config  neuralNetConfig  
	wHidden *mat.Dense  
	bHidden *mat.Dense  
	wOut    *mat.Dense  
	bOut    *mat.Dense  
}  
  
// neuralNetConfig defines our neural network  
// architecture and learning parameters.  
type neuralNetConfig struct {  
	inputNeurons  int  
	outputNeurons int  
	hiddenNeurons int  
	numEpochs     int  
	learningRate  float64  
}  
  
func main() {  
  
	// Form the training matrices.  
	inputs, labels := makeInputsAndLabels("data/train.csv")  
  
	// Define our network architecture and learning parameters.  
	config := neuralNetConfig{  
		inputNeurons:  4,  
		outputNeurons: 3,  
		hiddenNeurons: 3,  
		numEpochs:     5000,  
		learningRate:  0.3,  
	}  
  
	// Train the neural network.  
	network := newNetwork(config)  
	if err := network.train(inputs, labels); err != nil {  
		log.Fatal(err)  
	}  
  
	// Form the testing matrices.  
	testInputs, testLabels := makeInputsAndLabels("data/test.csv")  
  
	// Make the predictions using the trained model.  
	predictions, err := network.predict(testInputs)  
	if err != nil {  
		log.Fatal(err)  
	}  
  
	// Calculate the accuracy of our model.  
	var truePosNeg int  
	numPreds, _ := predictions.Dims()  
	for i := 0; i < numPreds; i++ {  
  
		// Get the label.  
		labelRow := mat.Row(nil, i, testLabels)  
		var prediction int  
		for idx, label := range labelRow {  
			if label == 1.0 {  
				prediction = idx  
				break  
			}  
		}  
  
		// Accumulate the true positive/negative count.  
		if predictions.At(i, prediction) == floats.Max(mat.Row(nil, i, predictions)) {  
			truePosNeg++  
		}  
	}  
  
	// Calculate the accuracy (subset accuracy).  
	accuracy := float64(truePosNeg) / float64(numPreds)  
  
	// Output the Accuracy value to standard out.  
	fmt.Printf("\nAccuracy = %0.2f\n\n", accuracy)  
}  
  
// NewNetwork initializes a new neural network.  
func newNetwork(config neuralNetConfig) *neuralNet {  
	return &neuralNet{config: config}  
}  
  
// train trains a neural network using backpropagation.  
func (nn *neuralNet) train(x, y *mat.Dense) error {  
  
	// Initialize biases/weights.  
	randSource := rand.NewSource(time.Now().UnixNano())  
	randGen := rand.New(randSource)  
  
	wHidden := mat.NewDense(nn.config.inputNeurons, nn.config.hiddenNeurons, nil)  
	bHidden := mat.NewDense(1, nn.config.hiddenNeurons, nil)  
	wOut := mat.NewDense(nn.config.hiddenNeurons, nn.config.outputNeurons, nil)  
	bOut := mat.NewDense(1, nn.config.outputNeurons, nil)  
  
	wHiddenRaw := wHidden.RawMatrix().Data  
	bHiddenRaw := bHidden.RawMatrix().Data  
	wOutRaw := wOut.RawMatrix().Data  
	bOutRaw := bOut.RawMatrix().Data  
  
	for _, param := range [][]float64{  
		wHiddenRaw,  
		bHiddenRaw,  
		wOutRaw,  
		bOutRaw,  
	} {  
		for i := range param {  
			param[i] = randGen.Float64()  
		}  
	}  
  
	// Define the output of the neural network.  
	output := new(mat.Dense)  
  
	// Use backpropagation to adjust the weights and biases.  
	if err := nn.backpropagate(x, y, wHidden, bHidden, wOut, bOut, output); err != nil {  
		return err  
	}  
  
	// Define our trained neural network.  
	nn.wHidden = wHidden  
	nn.bHidden = bHidden  
	nn.wOut = wOut  
	nn.bOut = bOut  
  
	return nil  
}  
  
// backpropagate completes the backpropagation method.  
func (nn *neuralNet) backpropagate(x, y, wHidden, bHidden, wOut, bOut, output *mat.Dense) error {  
  
	// Loop over the number of epochs utilizing  
	// backpropagation to train our model.  
	for i := 0; i < nn.config.numEpochs; i++ {  
  
		// Complete the feed forward process.  
		hiddenLayerInput := new(mat.Dense)  
		hiddenLayerInput.Mul(x, wHidden)  
		addBHidden := func(_, col int, v float64) float64 { return v + bHidden.At(0, col) }  
		hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)  
  
		hiddenLayerActivations := new(mat.Dense)  
		applySigmoid := func(_, _ int, v float64) float64 { return sigmoid(v) }  
		hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)  
  
		outputLayerInput := new(mat.Dense)  
		outputLayerInput.Mul(hiddenLayerActivations, wOut)  
		addBOut := func(_, col int, v float64) float64 { return v + bOut.At(0, col) }  
		outputLayerInput.Apply(addBOut, outputLayerInput)  
		output.Apply(applySigmoid, outputLayerInput)  
  
		// Complete the backpropagation.  
		networkError := new(mat.Dense)  
		networkError.Sub(y, output)  
  
		slopeOutputLayer := new(mat.Dense)  
		applySigmoidPrime := func(_, _ int, v float64) float64 { return sigmoidPrime(v) }  
		slopeOutputLayer.Apply(applySigmoidPrime, output)  
		slopeHiddenLayer := new(mat.Dense)  
		slopeHiddenLayer.Apply(applySigmoidPrime, hiddenLayerActivations)  
  
		dOutput := new(mat.Dense)  
		dOutput.MulElem(networkError, slopeOutputLayer)  
		errorAtHiddenLayer := new(mat.Dense)  
		errorAtHiddenLayer.Mul(dOutput, wOut.T())  
  
		dHiddenLayer := new(mat.Dense)  
		dHiddenLayer.MulElem(errorAtHiddenLayer, slopeHiddenLayer)  
  
		// Adjust the parameters.  
		wOutAdj := new(mat.Dense)  
		wOutAdj.Mul(hiddenLayerActivations.T(), dOutput)  
		wOutAdj.Scale(nn.config.learningRate, wOutAdj)  
		wOut.Add(wOut, wOutAdj)  
  
		bOutAdj, err := sumAlongAxis(0, dOutput)  
		if err != nil {  
			return err  
		}  
		bOutAdj.Scale(nn.config.learningRate, bOutAdj)  
		bOut.Add(bOut, bOutAdj)  
  
		wHiddenAdj := new(mat.Dense)  
		wHiddenAdj.Mul(x.T(), dHiddenLayer)  
		wHiddenAdj.Scale(nn.config.learningRate, wHiddenAdj)  
		wHidden.Add(wHidden, wHiddenAdj)  
  
		bHiddenAdj, err := sumAlongAxis(0, dHiddenLayer)  
		if err != nil {  
			return err  
		}  
		bHiddenAdj.Scale(nn.config.learningRate, bHiddenAdj)  
		bHidden.Add(bHidden, bHiddenAdj)  
	}  
  
	return nil  
}  
  
// predict makes a prediction based on a trained  
// neural network.  
func (nn *neuralNet) predict(x *mat.Dense) (*mat.Dense, error) {  
  
	// Check to make sure that our neuralNet value  
	// represents a trained model.  
	if nn.wHidden == nil || nn.wOut == nil {  
		return nil, errors.New("the supplied weights are empty")  
	}  
	if nn.bHidden == nil || nn.bOut == nil {  
		return nil, errors.New("the supplied biases are empty")  
	}  
  
	// Define the output of the neural network.  
	output := new(mat.Dense)  
  
	// Complete the feed forward process.  
	hiddenLayerInput := new(mat.Dense)  
	hiddenLayerInput.Mul(x, nn.wHidden)  
	addBHidden := func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }  
	hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)  
  
	hiddenLayerActivations := new(mat.Dense)  
	applySigmoid := func(_, _ int, v float64) float64 { return sigmoid(v) }  
	hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)  
  
	outputLayerInput := new(mat.Dense)  
	outputLayerInput.Mul(hiddenLayerActivations, nn.wOut)  
	addBOut := func(_, col int, v float64) float64 { return v + nn.bOut.At(0, col) }  
	outputLayerInput.Apply(addBOut, outputLayerInput)  
	output.Apply(applySigmoid, outputLayerInput)  
  
	return output, nil  
}  
  
// sigmoid implements the sigmoid function  
// for use in activation functions.  
func sigmoid(x float64) float64 {  
	return 1.0 / (1.0 + math.Exp(-x))  
}  
  
// sigmoidPrime implements the derivative  
// of the sigmoid function for backpropagation.  
func sigmoidPrime(x float64) float64 {  
	return sigmoid(x) * (1.0 - sigmoid(x))  
}  
  
// sumAlongAxis sums a matrix along a  
// particular dimension, preserving the  
// other dimension.  
func sumAlongAxis(axis int, m *mat.Dense) (*mat.Dense, error) {  
  
	numRows, numCols := m.Dims()  
  
	var output *mat.Dense  
  
	switch axis {  
	case 0:  
		data := make([]float64, numCols)  
		for i := 0; i < numCols; i++ {  
			col := mat.Col(nil, i, m)  
			data[i] = floats.Sum(col)  
		}  
		output = mat.NewDense(1, numCols, data)  
	case 1:  
		data := make([]float64, numRows)  
		for i := 0; i < numRows; i++ {  
			row := mat.Row(nil, i, m)  
			data[i] = floats.Sum(row)  
		}  
		output = mat.NewDense(numRows, 1, data)  
	default:  
		return nil, errors.New("invalid axis, must be 0 or 1")  
	}  
  
	return output, nil  
}  
  
func makeInputsAndLabels(fileName string) (*mat.Dense, *mat.Dense) {  
	// Open the dataset file.  
	f, err := os.Open(fileName)  
	if err != nil {  
		log.Fatal(err)  
	}  
	defer f.Close()  
  
	// Create a new CSV reader reading from the opened file.  
	reader := csv.NewReader(f)  
	reader.FieldsPerRecord = 7  
  
	// Read in all of the CSV records  
	rawCSVData, err := reader.ReadAll()  
	if err != nil {  
		log.Fatal(err)  
	}  
  
	// inputsData and labelsData will hold all the  
	// float values that will eventually be  
	// used to form matrices.  
	inputsData := make([]float64, 4*len(rawCSVData))  
	labelsData := make([]float64, 3*len(rawCSVData))  
  
	// Will track the current index of matrix values.  
	var inputsIndex int  
	var labelsIndex int  
  
	// Sequentially move the rows into a slice of floats.  
	for idx, record := range rawCSVData {  
  
		// Skip the header row.  
		if idx == 0 {  
			continue  
		}  
  
		// Loop over the float columns.  
		for i, val := range record {  
  
			// Convert the value to a float.  
			parsedVal, err := strconv.ParseFloat(val, 64)  
			if err != nil {  
				log.Fatal(err)  
			}  
  
			// Add to the labelsData if relevant.  
			if i == 4 || i == 5 || i == 6 {  
				labelsData[labelsIndex] = parsedVal  
				labelsIndex++  
				continue  
			}  
  
			// Add the float value to the slice of floats.  
			inputsData[inputsIndex] = parsedVal  
			inputsIndex++  
		}  
	}  
	inputs := mat.NewDense(len(rawCSVData), 4, inputsData)  
	labels := mat.NewDense(len(rawCSVData), 3, labelsData)  
	return inputs, labels  
}

程式碼最後將測試集資料匯入,並且開始進行預測:

// Form the testing matrices.  
	testInputs, testLabels := makeInputsAndLabels("data/test.csv")  
  
	fmt.Println(testLabels)  
  
	// Make the predictions using the trained model.  
	predictions, err := network.predict(testInputs)  
	if err != nil {  
		log.Fatal(err)  
	}  
  
	// Calculate the accuracy of our model.  
	var truePosNeg int  
	numPreds, _ := predictions.Dims()  
	for i := 0; i < numPreds; i++ {  
  
		// Get the label.  
		labelRow := mat.Row(nil, i, testLabels)  
		var prediction int  
		for idx, label := range labelRow {  
			if label == 1.0 {  
				prediction = idx  
				break  
			}  
		}  
  
		// Accumulate the true positive/negative count.  
		if predictions.At(i, prediction) == floats.Max(mat.Row(nil, i, predictions)) {  
			truePosNeg++  
		}  
	}  
  
	// Calculate the accuracy (subset accuracy).  
	accuracy := float64(truePosNeg) / float64(numPreds)  
  
	// Output the Accuracy value to standard out.  
	fmt.Printf("\nAccuracy = %0.2f\n\n", accuracy)

程式輸出:

&{{31 3 [0 1 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 1 0 0 1 0 0 1 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1 0 0 1 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 1 0 1 0 0 0 0 1 0 0 1 1 0 0 1 0 0 0 1 0 0 0 1 0 0 1 0 0 0] 3} 31 3}  
  
Accuracy = 0.97

可以看到,一共31個測試樣本,只錯了3次,成功率達到了97%。

當然,就算是自己實現的小型神經網路,預測結果正確率也不可能達到100%,因為機器學習也是基於機率學範疇的學科。

為什麼使用Golang?

事實上,大部分人都存在這樣一個刻板影響:機器學習必須要用Python來實現。就像前文所提到的,機器學習和Python語言並不存在因果關係,我們使用Golang同樣可以實現神經網路,同樣可以完成機器學習的流程,程式語言,僅僅是實現的工具而已。

但不能否認的是,Python當前在人工智慧領域的很多細分方向都有比較廣泛的應用,比如自然語言處理、計算機視覺和機器學習等領域,但是並不意味著人工智慧研發一定離不開Python語言,實際上很多其他程式語言也完全可以替代Python,比如Java、C++、Golang等等。

機器學習相關業務之所以大量使用Python,是因為Python有著極其豐富的三方庫進行支援,能夠讓研發人員把更多的精力放在演算法設計和演算法訓練等方面,說白了,就是不用重複造輪子,提高研發團隊整體產出的效率,比如面對基於Python的Pytorch和Tensorflow這兩個顛撲不破的深度學習巨石重鎮,Golang就得敗下陣來,沒有任何優勢可言。

所以,單以人工智慧生態圈的繁榮程度而論,Golang還及不上Python。

結語

至此,我們就使用Golang完成了一個小型神經網路的實現,並且解決了一個真實存在的分類問題。那麼,走完了整個流程,我們應該對基於神經網路架構的機器學習過程有了一個大概的瞭解,那就是機器學習只能解決可以被解決的問題,有經驗或者相關知識儲備的人類透過肉眼也能識別鳶尾花的種類,機器學習只是幫我們提高了識別效率而已,所以,如果還有人在你面前吹噓他能夠用機器學習來預測A股價格賺大錢,那麼,他可能對機器學習存在誤解,或者可能對A股市場存在誤解,或者就是個純騙子,三者必居其一。

相關文章