Go Machine Learning

Go Machine Learning

前言

最近因为一直在弄部署整天c++写的非常头疼,趁着昨天把分割部署写好后打算换换口味,想着试试Go语言来实现一些机器学习,深度学习会是什么样子.之前推荐过Go+(goplus),不过这次打算用更基础的go语法来尝试.

1.准备工作

对于某个从未涉及的领域一开始肯定是一脸茫然,所以需要先找点资料入门.网上相关资料也没有特别多,搜的话基本就只有那几本书.不过这不重要随便找一本书了解入个门,后面的就都可以举一反三了

这里我看的是这本机器学习Go语言实现,提取码:o69s.这本书的出版时间是2018年,也就代表着书中的代码不一定全部能用,毕竟一些三方库肯定会有更新改动.

对应的仓库地址:https://github.com/PacktPublishing/Machine-Learning-With-Go

1.1 基本知识

这里的基本知识不止包括Go语言语法,编程知识和一些机器学习深度学习的知识,更主要的是了解第三方的库如何使用.拿到书以后迅速看一看前几章(两章左右吧,不用太细致看文字毕竟还有第二次细读),主要关注里面使用的库,这里总结一下我使用到的(有的书里的库我替换成了自己在github上找的)

  • gonum: ”gonum.org/v1/gonum“

  • gota: “github.com/go-gota/gota”

  • sklearn “github.com/pa-m/sklearn”

  • plot “gonum.org/v1/plot”

大致总结一下:gonum提供一些矩阵计算,类似于numpy;gota下有dataframe和series,类似于pandas;sklearn就是go实现部分sklearn的方法;plot顾名思义就是画图

对于Go来说,由于静态类型的限制在机器学习的代码编写上肯定是不如python便捷的,这一点心里要做好准备.如果习惯了python的灵活性,或者是只会python的那部分人最好不要尝试,当然估计这部分人也不会有这种兴趣爱好花时间在其他语言的探索上,有些话最后再说.

扯回这篇Blog,上面的三方库都可以去github上搜到源码和文档说明,配合着书里的代码很容易理解使用.

1.2 项目创建

这个应该是最简单的,go mod init 随便创建一个新的项目,然后根据自己的想法创建相关的文件结构

2. 正式开始

2.1 数据读取部分

既然是机器学习,那么数据当然是必不可少的.所以书上一开始就讲述了数据的收集和组织.这里我不多作赘述,只要学会怎么读取数据就好,给个基本的读取csv文件模板(下面的demo都是基于csv文件)

file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}

defer file.Close()

irisDF := dataframe.ReadCSV(file)
fmt.Println(irisDF)

image-20221020200904415

当然如果不想借助dataframe,自己写个读取函数也很简单

func PrintCSV(filename string) {
	f, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}

	r := csv.NewReader(f)

	records, err := r.ReadAll()
	if err != nil {
		log.Fatal(err)
	}

	for _, record := range records {
		for _, i := range record {
			print(i)
			print(" ")
		}
		println()
	}
}

image-20221020200732574

还有一些其他数据格式,例如jsondatabase等都可以通过各自的方法实现读取,剩下的就是在dataframe里面对数据进行操作

2.2 矩阵计算部分

这部分主要就是利用gonum对数据进行计算,类似的还有numcpp,主要是实现一些矩阵计算.这部分看看书里demo就好,用的时候还是得自己查官网相关的文档

2.3 基本demo

这部分基本就是一些实战,从数据读取到探索性分析,再到建模评估.这里书上的库我没用,而是选择go语言版的sklearn,虽然功能还不太完善,但是基本的几个模型,预处理方法以及评价指标都还比较全.同样的查看文档后去实现书上的几个demo

2.3.1 回归

书上的例子是Advertising.csv,根据自变量回归得到target Sales.根据上述流程,读取数据就直接使用dataframe.然后进行探索性分析,主要是查看自变量和目标是否符合线性关系或者说存在相关性,我们肯定不会拿一个一看就毫不相关的变量去拟合回归.

func PlotImg(filename string) {
	file, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	DF := dataframe.ReadCSV(file)

	target := DF.Col("Sales").Float()

	for _, colName := range DF.Names() {
		pts := make(plotter.XYs, DF.Nrow())

		for i, floatVal := range DF.Col(colName).Float() {
			pts[i].X = floatVal
			pts[i].Y = target[i]
		}

		p := plot.New()
		p.X.Label.Text = colName
		p.Y.Label.Text = "y"

		p.Add(plotter.NewGrid())

		s, err := plotter.NewScatter(pts)

		if err != nil {
			log.Fatal(err)
		}

		s.GlyphStyle.Radius = vg.Points(3)
		p.Add(s)

		if err := p.Save(4*vg.Inch, 4*vg.Inch, colName+"_scatter.jpg"); err != nil {
			log.Fatal(err)
		}
	}
}

想比matplotlib等python的画图库来说这个是不是复杂很多,除了要自己新建figure,还得一对对的添加(x,y).当然也可以把遍历部分封装成一个函数,只需要输入想要画的列名,不过如果不是需要多次使用也没必要,毕竟灵活性在这放着.如果改成绘制其他图表,不仅plotter的对象得用if或者switch判断,其他的格式也有可能要改.这里列的选择可以用DF.Select([]string{"xx","xxx"})

image-20221020203009051

image-20221020203035137

应该可以得到书上的效果,从图上也可以看出哪些变量和Sales具有线性关系.这里demo我选择一元线性回归,目标是找到Sales=w*TV+b

线性回归的原理很多书,博客都有介绍,如何利用梯度下降一步步拟合减小损失函数,所以这里直接用sklearn来实现.

sklearn的模型输入是gonum中的mat.Matrix,但是我们是dataframe读取的数据,并不是同一种类型,这里就是静态类型的麻烦之处.不过还好mat.Matrix只是一个interface,所以为了能够转换,我们实现接口相应的方法就好.

image-20221020203850392

type matrix struct {
   dataframe.DataFrame
}

func (m matrix) At(i, j int) float64 {
   return m.Elem(i, j).Float()
}

func (m matrix) T() mat.Matrix {
   return mat.Transpose{m}
}

image-20221020203947278

我们只需要实现At()和T(),也就是取值和转置,Dims()方法在dataframe中本身就有实现;其实仔细看的话,方法基本都有只是名字不一样.当然这只是针对float,如果是int或者string,At()中也要改为.Int()或者.String().

转为Matrix之后,剩下的就和python类似了,创建模型->fit()->predict()->评估,完整代码

package p3

import (
	"fmt"
	"github.com/go-gota/gota/dataframe"
	"github.com/pa-m/sklearn/linear_model"
	"github.com/pa-m/sklearn/metrics"
	modelselection "github.com/pa-m/sklearn/model_selection"
	"gonum.org/v1/gonum/mat"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/plotter"
	"gonum.org/v1/plot/vg"
	"image/color"
	"log"
	"os"
)

// for convert dataframe to matrix
// create nessesary implementations

type matrix struct {
	dataframe.DataFrame
}

func (m matrix) At(i, j int) float64 {
	return m.Elem(i, j).Float()
}

func (m matrix) T() mat.Matrix {
	return mat.Transpose{m}
}

// sales=w*TV+b
func LinearDemo(Isplot bool) {
	//load file
	filename := "Advertising.csv"
	file, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	DF := dataframe.ReadCSV(file)

	X := mat.DenseCopyOf(&matrix{DF.Select([]string{"TV"})})
	Y := mat.DenseCopyOf(&matrix{DF.Select([]string{"Sales"})})
	XTrain, XTest, YTrain, YTest := modelselection.TrainTestSplit(X, Y, 0.2, 0)

	lr := linearmodel.NewLinearRegression()
	lr.Fit(XTrain, YTrain)

	NPred, _ := XTest.Dims()
	Pred := mat.NewDense(NPred, 1, nil)

	lr.Predict(XTest, Pred)

	fmt.Printf("Coefficients: %.3fn", mat.Formatted(lr.Coef))
	fmt.Printf("Coefficients: %.3fn", mat.Formatted(lr.Intercept))
	fmt.Printf("the pred result is: Sales=%.3f *TV + %.3fn", mat.Formatted(lr.Intercept), mat.Formatted(lr.Coef))
	fmt.Printf("Mean squared error: %.2fn", metrics.MeanSquaredError(YTest, Pred, nil, "").At(0, 0))
	fmt.Printf("Mean absolute error: %.2fn", metrics.MeanAbsoluteError(YTest, Pred, nil, "").At(0, 0))
	fmt.Printf("Variance score: %.2fn", metrics.R2Score(YTest, Pred, nil, "").At(0, 0))

	if Isplot {
		//predict all
		NPred, _ = X.Dims()
		Pred := mat.NewDense(NPred, 1, nil)
		lr.Predict(X, Pred)

		p := plot.New()
		xys := func(X, Y mat.Matrix) plotter.XYs {
			var data plotter.XYs
			for sample := 0; sample < NPred; sample++ {
				data = append(data, struct{ X, Y float64 }{X.At(sample, 0), Y.At(sample, 0)})
			}
			return data
		}

		s, _ := plotter.NewScatter(xys(X, Y))
		l, _ := plotter.NewLine(xys(X, Pred))
		l.Color = color.RGBA{0, 0, 255, 255}
		p.Add(s, l)

		// Save the plot to a PNG file.
		pngfile := "linearregression.png"
		os.Remove(pngfile)
		if err := p.Save(4*vg.Inch, 3*vg.Inch, pngfile); err != nil {
			panic(err)
		}
	}
}

image-20221020204505805

image-20221020204527712

2.3.2 分类

这里以iris为例,首先尝试一下传统的机器学习模型

2.3.2.1 KNN&&SVM

还是一样先查看数据,探索性分析这部分我直接略过,主要是后续对数据的处理.

iris.csv的内容如下

image-20221020205028322

对应的label是string,所以需要先变为数值型.这部分可以使用sklearn中的LabelEncoder 或者OneHotEncoder,也可以利用dataframe的Capply()自定义方法实现,这是我的转换函数

func replaceLabel(col series.Series) series.Series {
	rows := col.Len()
	NewSeries := series.New([]float64{}, series.Float, "")
	changeMap := map[string]float64{
		"Iris-setosa":     0.0,
		"Iris-versicolor": 1.0,
		"Iris-virginica":  2.0,
	}

	for i := 0; i < rows; i++ {
		NewSeries.Append(changeMap[col.Elem(i).String()])
	}

	return NewSeries

}

查看文档可以知道Capply()需要入参和输出都是Series,所以得根据方法要求写转换函数,再次体会一下类型限制.其余的就是使用sklearn没啥好说的,完整代码如下

package p3

import (
	"fmt"
	"github.com/go-gota/gota/dataframe"
	"github.com/go-gota/gota/series"
	"github.com/pa-m/sklearn/metrics"
	modelselection "github.com/pa-m/sklearn/model_selection"
	"github.com/pa-m/sklearn/neighbors"
	"github.com/pa-m/sklearn/svm"
	"gonum.org/v1/gonum/mat"
	"log"
	"os"
)

func replaceLabel(col series.Series) series.Series {
	rows := col.Len()
	NewSeries := series.New([]float64{}, series.Float, "")
	changeMap := map[string]float64{
		"Iris-setosa":     0.0,
		"Iris-versicolor": 1.0,
		"Iris-virginica":  2.0,
	}

	for i := 0; i < rows; i++ {
		NewSeries.Append(changeMap[col.Elem(i).String()])
	}

	return NewSeries

}

func ClassifyDemo() {
	filename := "iris.csv"
	file, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	DF := dataframe.ReadCSV(file)

	//replace the label from string to float
	X := mat.DenseCopyOf(&matrix{DF.Select([]int{0, 1, 3})})
	Y := mat.DenseCopyOf(&matrix{DF.Select(4).Capply(replaceLabel)})
	XTrain, XTest, YTrain, YTest := modelselection.TrainTestSplit(X, Y, 0.2, 0)

	clf := neighbors.NewKNeighborsClassifier(3, "uniform")
	clf.Fit(XTrain, YTrain)

	NPred, _ := XTest.Dims()
	Pred := mat.NewDense(NPred, 1, nil)
	clf.Predict(XTest, Pred)

	fmt.Printf("The accuracy of KNN: %.02f%%n", metrics.AccuracyScore(YTest, Pred, true, nil)*100)

	clf2 := svm.NewSVC()
	//clf2.Degree = 3
	clf2.Kernel = "linear"
	clf2.Fit(XTrain, YTrain)
	clf2.Predict(XTest, Pred)
	//fmt.Printf("Pred:n%gn", mat.Formatted(Pred))
	fmt.Printf("The accuracy of SVM: %.02f%%n", metrics.AccuracyScore(YTest, Pred, true, nil)*100)
}

image-20221020211125078

KNN本来就是用于多分类问题,所以效果还可以;svm只是二分类,没有用上一对多等策略,所以预测的大多都是某一类导致准确率很差;

2.3.2.2 MLP

尝试了传统的方法,在简单数据集上选择了合适的模型,效果已经非常好了,那再用深度学习方法来试试呢

这里可以选择用sklearn的MLPClassifier,不过书上也给出了纯go实现,这部分还是值得好好品一品的.

回顾一下神经网络训练的基本内容

  1. 构造简单网络,定义输入,隐藏层和输出,中间加入激活函数
  2. 正向传播,计算网络输出与目标的损失
  3. 根据损失函数反向传播计算梯度,对网络中的权重和偏置项进行更新

对于计算推导,可以看看这篇博客,有图有计算步骤https://www.cnblogs.com/charlotte77/p/5629865.html

下面主要来分析一下这本书中的写法,首先要构思如何去写这个功能,根据上面几个库的使用,应该体会到如果想复用或者模块化,那肯定得构建一个对象,然后一步步实现里面的类方法.go语言虽然没有class,但是struct也能实现我们想要的.

type neuralNet struct {
	config  neuralNetConfig
	wHidden *mat.Dense
	bHidden *mat.Dense
	wOut    *mat.Dense
	bOut    *mat.Dense
}

type neuralNetConfig struct {
	inputNum     int
	outputNum    int
	hiddenNum    int
	Epochs       int
	learningRate float64
}

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

这就是最基本的类以及初始化方法,我们构造神经网络的时候必须指定输入输出的大小,隐藏层的层数以及训练时的Epochs和学习率

再来看看两个最基本的类方法,train()predict()

对于train(),根据上面说的流程,我们先初始化一些权重和bias,然后一次次迭代,正向传播计算输出,再反向传播更新网络参数.这里需要注意的是现在的mat不再允许创建行列均为0的Matrix,所以得自己一步步计算原代码中的维度大小进行修改填充

原代码中激活函数用的Sigmoid(),损失就是target-output,好处是对于损失的梯度不用计算,默认就是error

(

1

2

(

y

y

^

)

2

)

=

y

y

^

(frac{1}{2}(y-hat{y})^2)^{'}=y-hat{y}

(21(yy^)2)=yy^.sigmoid的导数也可以预先计算设置好.修改后的相关代码如下

func (nn *neuralNet) train(x, y *mat.Dense) error {
	N, _ := x.Dims()
	randSource := rand.NewSource(time.Now().UnixNano())
	randGen := rand.New(randSource)

	wHiddenRaw := make([]float64, nn.config.hiddenNum*nn.config.inputNum)
	bHiddenRaw := make([]float64, nn.config.hiddenNum)
	wOutRaw := make([]float64, nn.config.outputNum*nn.config.hiddenNum)
	bOutRaw := make([]float64, nn.config.outputNum)

	for _, param := range [][]float64{wHiddenRaw, bHiddenRaw, wOutRaw, bOutRaw} {
		for i := range param {
			param[i] = randGen.Float64()
		}
	}

	wHidden := mat.NewDense(nn.config.inputNum, nn.config.hiddenNum, wHiddenRaw)
	bHidden := mat.NewDense(1, nn.config.hiddenNum, bHiddenRaw)
	wOut := mat.NewDense(nn.config.hiddenNum, nn.config.outputNum, wOutRaw)
	bOut := mat.NewDense(1, nn.config.outputNum, bOutRaw)

	output := mat.NewDense(N, nn.config.outputNum, nil)
	// train model.
	for i := 0; i < nn.config.Epochs; i++ {
		// forward
		hiddenLayerInput := mat.NewDense(N, nn.config.hiddenNum, nil)
		hiddenLayerInput.Mul(x, wHidden)
		addBHidden := func(_, col int, v float64) float64 { return v + bHidden.At(0, col) }
		hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)

		hiddenLayerActivations := mat.NewDense(N, nn.config.hiddenNum, nil)
		applySigmoid := func(_, _ int, v float64) float64 { return Sigmoid(v) }
		hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)

		outputLayerInput := mat.NewDense(N, nn.config.outputNum, nil)
		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)

		// backpropagation.
		networkError := mat.NewDense(N, nn.config.outputNum, nil)
		networkError.Sub(y, output)

		slopeOutputLayer := mat.NewDense(N, nn.config.outputNum, nil)
		applySigmoidPrime := func(_, _ int, v float64) float64 { return SigmoidPrime(v) }
		slopeOutputLayer.Apply(applySigmoidPrime, output)
		slopeHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		slopeHiddenLayer.Apply(applySigmoidPrime, hiddenLayerActivations)

		dOutput := mat.NewDense(N, nn.config.outputNum, nil)
		dOutput.MulElem(networkError, slopeOutputLayer)
		errorAtHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		errorAtHiddenLayer.Mul(dOutput, wOut.T())

		dHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		dHiddenLayer.MulElem(errorAtHiddenLayer, slopeHiddenLayer)

		// Adjust the parameters.
		wOutAdj := mat.NewDense(nn.config.hiddenNum, nn.config.outputNum, nil)
		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 := mat.NewDense(nn.config.inputNum, nn.config.hiddenNum, nil)
		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)
	}

	nn.wHidden = wHidden
	nn.bHidden = bHidden
	nn.wOut = wOut
	nn.bOut = bOut

	return nil
}

func (nn *neuralNet) predict(x *mat.Dense) (*mat.Dense, error) {
	N, _ := x.Dims()
	// Check to make sure that our neuralNet value
	// represents a trained model.
	if nn.wHidden == nil || nn.wOut == nil || nn.bHidden == nil || nn.bOut == nil {
		return nil, errors.New("the supplied neurnal net weights and biases are empty")
	}

	// Define the output of the neural network.
	output := mat.NewDense(N, nn.config.outputNum, nil)

	// Complete the feed forward process.
	hiddenLayerInput := mat.NewDense(N, nn.config.hiddenNum, nil)
	hiddenLayerInput.Mul(x, nn.wHidden)
	addBHidden := func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }
	hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)

	hiddenLayerActivations := mat.NewDense(N, nn.config.hiddenNum, nil)
	applySigmoid := func(_, _ int, v float64) float64 { return Sigmoid(v) }
	hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)

	outputLayerInput := mat.NewDense(N, nn.config.outputNum, nil)
	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
}

func Sigmoid(x float64) float64 {
	return 1.0 / (1.0 + math.Exp(-x))
}

func SigmoidPrime(x float64) float64 {
	return x * (1.0 - x)
}

然后这次将iris的标签进行one-hot,实现如下

func replaceLabel2(col dataframe.DataFrame) dataframe.DataFrame {
	rows, _ := col.Dims()
	var NewDF = make([][]string, 0)
	changeMap := map[string][]string{
		"Iris-setosa":     []string{"1.", "0.", "0."},
		"Iris-versicolor": []string{"0.", "1.", "0."},
		"Iris-virginica":  []string{"0.", "0.", "1."},
	}

	for i := 0; i < rows; i++ {
		NewDF = append(NewDF, changeMap[col.Elem(i, 0).String()])
	}

	return dataframe.LoadRecords(NewDF, dataframe.HasHeader(false))
}

为什么这次得这样,因为Capply()返回的是Series,而one-hot之后肯定是二维的dataframe,因此干脆直接转为dataframe.然后就是一些维度求和以及求每一行最大值下标的函数(为了将predict变为one-hot),最后这部分完整代码如下

package p3

import (
	"errors"
	"fmt"
	"github.com/go-gota/gota/dataframe"
	"github.com/pa-m/sklearn/metrics"
	modelselection "github.com/pa-m/sklearn/model_selection"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/gonum/mat"
	"log"
	"math"
	"math/rand"
	"os"
	"time"
)

type neuralNet struct {
	config  neuralNetConfig
	wHidden *mat.Dense
	bHidden *mat.Dense
	wOut    *mat.Dense
	bOut    *mat.Dense
}

type neuralNetConfig struct {
	inputNum     int
	outputNum    int
	hiddenNum    int
	Epochs       int
	learningRate float64
}

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

func (nn *neuralNet) train(x, y *mat.Dense) error {
	N, _ := x.Dims()
	randSource := rand.NewSource(time.Now().UnixNano())
	randGen := rand.New(randSource)

	wHiddenRaw := make([]float64, nn.config.hiddenNum*nn.config.inputNum)
	bHiddenRaw := make([]float64, nn.config.hiddenNum)
	wOutRaw := make([]float64, nn.config.outputNum*nn.config.hiddenNum)
	bOutRaw := make([]float64, nn.config.outputNum)

	for _, param := range [][]float64{wHiddenRaw, bHiddenRaw, wOutRaw, bOutRaw} {
		for i := range param {
			param[i] = randGen.Float64()
		}
	}

	wHidden := mat.NewDense(nn.config.inputNum, nn.config.hiddenNum, wHiddenRaw)
	bHidden := mat.NewDense(1, nn.config.hiddenNum, bHiddenRaw)
	wOut := mat.NewDense(nn.config.hiddenNum, nn.config.outputNum, wOutRaw)
	bOut := mat.NewDense(1, nn.config.outputNum, bOutRaw)

	output := mat.NewDense(N, nn.config.outputNum, nil)
	// train model.
	for i := 0; i < nn.config.Epochs; i++ {
		// forward
		hiddenLayerInput := mat.NewDense(N, nn.config.hiddenNum, nil)
		hiddenLayerInput.Mul(x, wHidden)
		addBHidden := func(_, col int, v float64) float64 { return v + bHidden.At(0, col) }
		hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)

		hiddenLayerActivations := mat.NewDense(N, nn.config.hiddenNum, nil)
		applySigmoid := func(_, _ int, v float64) float64 { return Sigmoid(v) }
		hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)

		outputLayerInput := mat.NewDense(N, nn.config.outputNum, nil)
		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)

		// backpropagation.
		networkError := mat.NewDense(N, nn.config.outputNum, nil)
		networkError.Sub(y, output)

		slopeOutputLayer := mat.NewDense(N, nn.config.outputNum, nil)
		applySigmoidPrime := func(_, _ int, v float64) float64 { return SigmoidPrime(v) }
		slopeOutputLayer.Apply(applySigmoidPrime, output)
		slopeHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		slopeHiddenLayer.Apply(applySigmoidPrime, hiddenLayerActivations)

		dOutput := mat.NewDense(N, nn.config.outputNum, nil)
		dOutput.MulElem(networkError, slopeOutputLayer)
		errorAtHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		errorAtHiddenLayer.Mul(dOutput, wOut.T())

		dHiddenLayer := mat.NewDense(N, nn.config.hiddenNum, nil)
		dHiddenLayer.MulElem(errorAtHiddenLayer, slopeHiddenLayer)

		// Adjust the parameters.
		wOutAdj := mat.NewDense(nn.config.hiddenNum, nn.config.outputNum, nil)
		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 := mat.NewDense(nn.config.inputNum, nn.config.hiddenNum, nil)
		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)
	}

	nn.wHidden = wHidden
	nn.bHidden = bHidden
	nn.wOut = wOut
	nn.bOut = bOut

	return nil
}

func (nn *neuralNet) predict(x *mat.Dense) (*mat.Dense, error) {
	N, _ := x.Dims()
	// Check to make sure that our neuralNet value
	// represents a trained model.
	if nn.wHidden == nil || nn.wOut == nil || nn.bHidden == nil || nn.bOut == nil {
		return nil, errors.New("the supplied neurnal net weights and biases are empty")
	}

	// Define the output of the neural network.
	output := mat.NewDense(N, nn.config.outputNum, nil)

	// Complete the feed forward process.
	hiddenLayerInput := mat.NewDense(N, nn.config.hiddenNum, nil)
	hiddenLayerInput.Mul(x, nn.wHidden)
	addBHidden := func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }
	hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)

	hiddenLayerActivations := mat.NewDense(N, nn.config.hiddenNum, nil)
	applySigmoid := func(_, _ int, v float64) float64 { return Sigmoid(v) }
	hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)

	outputLayerInput := mat.NewDense(N, nn.config.outputNum, nil)
	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
}

func Sigmoid(x float64) float64 {
	return 1.0 / (1.0 + math.Exp(-x))
}

func SigmoidPrime(x float64) float64 {
	return x * (1.0 - x)
}

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 MaxAlongAxis(m *mat.Dense) (*mat.Dense, error) {
	numRows, numCols := m.Dims()
	var output *mat.Dense
	res := []float64{}
	for i := 0; i < numRows; i++ {
		row := mat.Row(nil, i, m)
		idx := floats.MaxIdx(row)
		for j := 0; j < numCols; j++ {
			if j == idx {
				res = append(res, 1)
			} else {
				res = append(res, 0)
			}
		}
	}
	output = mat.NewDense(numRows, numCols, res)
	return output, nil
}

func replaceLabel2(col dataframe.DataFrame) dataframe.DataFrame {
	rows, _ := col.Dims()
	var NewDF = make([][]string, 0)
	changeMap := map[string][]string{
		"Iris-setosa":     []string{"1.", "0.", "0."},
		"Iris-versicolor": []string{"0.", "1.", "0."},
		"Iris-virginica":  []string{"0.", "0.", "1."},
	}

	for i := 0; i < rows; i++ {
		NewDF = append(NewDF, changeMap[col.Elem(i, 0).String()])
	}

	return dataframe.LoadRecords(NewDF, dataframe.HasHeader(false))
}

func NNClassify() {
	config := neuralNetConfig{
		inputNum:     4,
		outputNum:    3,
		hiddenNum:    10,
		Epochs:       1000,
		learningRate: 0.009,
	}

	filename := "iris.csv"
	file, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	DF := dataframe.ReadCSV(file, dataframe.HasHeader(true))

	//replace the label from string to float
	X := mat.DenseCopyOf(&matrix{DF.Select([]int{0, 1, 2, 3})})
	Y := mat.DenseCopyOf(&matrix{replaceLabel2(DF.Select(4))})

	XTrain, XTest, YTrain, YTest := modelselection.TrainTestSplit(X, Y, 0.33, 0)
	//fmt.Printf("X_train:n%gn", mat.Formatted(XTrain))
	//fmt.Printf("Y_train:n%gn", mat.Formatted(YTrain))
	network := NewNetwork(config)
	start := time.Now()
	if err := network.train(XTrain, YTrain); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Train cost time:", time.Since(start))

	f := mat.Formatted(network.wHidden, mat.Prefix(" "))
	fmt.Printf("nwHidden = % vnn", f)

	f = mat.Formatted(network.bHidden, mat.Prefix(" "))
	fmt.Printf("nbHidden = % vnn", f)

	f = mat.Formatted(network.wOut, mat.Prefix(" "))
	fmt.Printf("nwOut = % vnn", f)

	f = mat.Formatted(network.bOut, mat.Prefix(" "))
	fmt.Printf("nbOut = % vnn", f)

	//predict
	predictions, err := network.predict(XTest)
	fmt.Printf("npredictions = % vnn", mat.Formatted(predictions))
	if err != nil {
		log.Fatal(err)
	}
	//fmt.Println(mat.Formatted(predictions))
	pred, err := MaxAlongAxis(predictions)
	if err != nil {
		log.Fatal(err)
	}
	//fmt.Println(mat.Formatted(pred))
	fmt.Printf("The accuracy of MLP: %.02f%%n", metrics.AccuracyScore(YTest, pred, true, nil)*100)

}

image-20221020215844126

image-20221020215857628

迭代1000次只花了66ms,准确率98%也超过了传统模型.

2.4 More

当然远远不止书上这些,社区里还有很多很好的库能帮助go实现机器学习/深度学习.比如tensorflow(libtorch) binding for Go,可惜已经很久没更新.特别是libtorch的,我去了解的时候一看安装,里面还是cuda10.x,就没有再去探索了.相反的一些点赞数不多的库https://github.com/kuroko1t/gotorch看着真的还很不错,也许目前功能还不完善但拿来作为项目学习下还是可以的.之前很早就提到的Go+也是数据科学方面非常好的go语言拓展.未来如果有时间,会好好探索如何将go转为易用于深度学习的语言之一

最后

花了一天抛开聚类和时间序列两章没具体细看,从完全不会这些库的使用到根据文档基本能实现自己想要的功能;同时还把自己放空了一天.不用再去想为什么onnx转换后trt的精度会有损失,部署的时候各种后处理怎么实现等等,我觉得这种自我调节休息还是很有必要的.长期只使用python,c++,都快忘了其他语言怎么写.

作为喜欢写写代码,研究技术的人,在闲暇时抛开工作自我学习本身就是一种休闲.python在好几年前刚学习的时候我对它爱不释手,因为学习成本很低也不用编译,只要配好解释器随处可以运行,不像c++有的时候为了验证一个demo还得写CMakeLists或者命令行链接一堆头文件.随着不断学习,有的时候运行效率,适用性根本达不到要求.不可能每台电脑都得配python环境再给你pip装一堆库,pyinstaller打包只要引入一些三方库(numpy,pandas等)出来的文件就不会很小,不用三方库那python的优势又在哪呢?

像我们这种科研狗,如果毕业打算继续从事研究,那用python快速验证模型效果等等无可厚非.如果打算从事互联网开发或者算法工程师,仅仅会写点python只能说坑了同事也坑了自己.不是clone一个项目稍微改几句,或者添加点东西点个run能跑就是会写代码.不考虑项目结构,不考虑代码效率,不考虑格式规范真的很头疼.(有感而发,别怼怼就是你对)

现在很多“火箭”三方库已经实现了,我们只需要会用就行.小的“轮子”在这基础上自己造一造,举一反三.

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>