• 我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别


    “我的Go+语言初体验” | 征文活动进行中…

    我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别

    0. 前言

    之前发blink说自己想学一门新语言,很多热心的小伙伴推荐了 Go,这时又恰逢看到官方创作活动“我的Go+语言初体验”征文大赛,看了官方文档,发现 Go+ 完全兼容 Go 语言,并且代码更加易读。这不就是说,这波实际学习了一门语言却掌握了两门语言,表示赚到了。
    于是迫不及待的开始准备体验下,既然官方介绍说 Go+ 「for engineering, STEM education, and data science」,融合了数据科学领域的 Python,那作为人工智能领域的相关从业人员,探索 Go+ 在人工智能领域的应用,我辈当然又是义不容辞了。
    本文,首先简要概述下神经网络的相关概念,然后使用 Go+ 语言构建神经网络实战手写数字识别。

    1. 神经网络相关概念

    人工神经网络的发展受到了人脑神经元的启发,并且在多个领域中都已经取得了广泛的应用,包括图像识别、语音识别以及推荐系统等等,本文并非人工智能的详尽教程,但会简要介绍相关基础,为使用 Go+ 语言构建神经网络奠定基础。
    在人工神经网络中,使用神经元接受输入数据,对数据执行操作后传递到下一神经元,每个神经元的输出称为激活,获取激活的函数称为激活函数,神经元中的参数称为权重偏置。每个网络层中包含若干个神经元,其中接收初始输入的网络层称为输入层,产生最终结果的网络层称为输出层,位于输出层与隐藏层之间的网络层称为隐藏层。数据从输入到输出的整个传输过程称为正向传播;而反向传播是一种训练神经网络的方法,通过计算真实值与网络输出值间的误差,反向修改网络的权重。
    在如下图所示的全连接网络中,每个节点表示一个神经元,整个网络包括一层输入层、一层输出层已经两层隐藏层。
    全连接网络

    虽然已经有一些现有的神经网络框架可以使用,但作为体验作,本文将从头开始构建简单的全连接网络,以更好了解神经网络的基本组成以及运行原理。
    本文使用 MNIST 数据集和 gonum 构建简单的全连接网络,虽然全连接网络是十分基础简单的神经网络,但是相关的模型训练流程和原理是相通的。

    2. 构建神经网络实战手写数字识别

    2.1 构建神经网络

    我们已经知道神经网络中的节点接受输入矩阵,通过与权重矩阵进行计算后,通过激活函数后,产生输出,接下来将讲解具体计算流程。

    2.1.1 节点计算

    每个神经元的计算形式如下图所示:

    神经元

    公式化后,如下所示:
    o = [ w 1 w 2 w n ] [ x 1 x 2 x n ] + b o = egin{bmatrix} w_1\ w_2 \ dots \ w_n end{bmatrix} egin{bmatrix} x_1 & x_2 & cdots &x_n end{bmatrix} + b o=w1w2wn[x1x2xn]+b

    其中, w i w_i wi 表示权重, b b b 表示偏置。
    Go+ 中利用 gonum 实现上述计算过程如下:

    	hiddenLayerInput.Mul(x, nn.wHidden)
    	addBHidden := func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }
    	hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)
    
    • 1
    • 2
    • 3

    gonum 用于高效编写数字和科学算法的算法库,可以通过执行以下命令获取:

    go get gonum.org/v1/gonum
    
    • 1

    2.1.2 激活函数

    仅仅通过上述线性计算,无法拟合现实生活中广泛存在的非线性模型,因此,神经网络中引入了激活函数来赋予网络非线性,有很多激活函数:sigmoidReLUtanh 等等。这以简单的 sigmoid 函数为例:
    s i g m o i d ( x ) = 1 1 + e x sigmoid(x)= rac 1 {1+e^{-x}} sigmoid(x)=1+ex1

    Go+ 中实现 sigmoid 函数如下:

    // activation functions
    func sigmoid(x float64) float64 {
    	return 1.0 / (1.0 + math.Exp(-x))
    }
    
    • 1
    • 2
    • 3
    • 4

    2.1.3 网络架构

    接下来,构建包含一个输入层,一个隐藏层,一个输出层的神经网络。其中,输入层包含 784 个神经元,这是由于 MNIST 数据集中每张照片包含 784 个像素点,每个像素点就是一个输入;隐藏层包含 512 个神经元,也可以使用更多或更少的神经元数量进行测试;输出层包含 10 个神经元,每个节点对应一个数字类别,这在神经网络中也称为独热编码。
    网络架构定义如下:

    config := neuralNetConfig{
    	// 输入层神经元
    	inputNeurons:  784,
    	// 输出层神经元
    	outputNeurons: 10,
    	// 隐藏层神经元
    	hiddenNeurons: 128,
    	// 训练 Epoch 数
    	numEpochs:     5000,
    	// 学习率
    	learningRate:  0.01,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    学习率用于控制每个 Epoch 中的参数的调整幅度。

    2.2 读取手写数字MNIST数据集

    训练数据是由 MNIST 手写数字组成的,MNIST 数据集来自美国国家标准与技术研究所,由来自 250 个不同人手写的数字构成,其中训练集包含 60000 张图片,测试集包含 10000 张图片,每个图片都有其标签,图片大小为 28*28

    1. 首先需要下载数据

    2. 然后读取数据;

      //读取数据
      f, err := os.Open(“new_mnist_train.csv”)
      if err != nil {
      log.Fatal(err)
      }
      defer f.Close()

      reader := csv.NewReader(f)
      reader.FieldsPerRecord = 794

      // 读取所有CSV记录
      mnistData, err := reader.ReadAll()
      if err != nil {
      log.Fatal(err)
      }

      // trainInputsData和trainLabelsData用于保存所有浮点值
      trainInputsData := make([]float64, 784len(mnistData))
      println(len(inputsData))
      trainLabelsData := make([]float64, 10
      len(mnistData))

      // 记录输入矩阵值的当前索引
      var trainInputsIndex int
      var trainLabelsIndex int

      for idx, record := range mnistData {
      // 跳过文件头
      if idx == 0 {
      continue
      }

      // 循环读取每行的每个数据
      for i, val := range record {
      	// 将数据转换为浮点形
      	parsedVal, err := strconv.ParseFloat(val, 64)
      	if err != nil {
      		log.Fatal(err)
      	}
      	
      	// 构造标签数据
      	if i == 0 || i == 1 || i == 2 || i == 3 || i == 4 || i == 5 || i == 6 || i == 7 || i == 8 || i == 9{
      		trainLabelsData[trainLabelsIndex] = parsedVal
      		trainLabelsIndex++
      		continue
      	}
      
      	// 构建输入数据
      	trainInputsData[trainInputsIndex] = parsedVal
      	trainInputsIndex++
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

      }

    3. 最后将数据整形,使得其加油可用于网络输入的形状。

      inputs := mat.NewDense(len(mnistData), 784, trainInputsData)
      labels := mat.NewDense(len(mnistData), 10, trainLabelsData)

    测试数据的读取方法与训练数据完全相同,不再赘述。

    2.3 训练神经网络

    网络的训练可以分为两部分,包括前向计算与反向传播。

    2.3.1 前向计算

    网络的前向计算十分简单,即通过数据流过网络层获得最终结果,首先需要初始化网络权重和偏置值:

    	// 初始化网络权重和偏置值
    	wHiddenRaw := make([]float64, nn.config.hiddenNeurons*nn.config.inputNeurons)
    	bHiddenRaw := make([]float64, nn.config.hiddenNeurons)
    	wOutRaw := make([]float64, nn.config.outputNeurons*nn.config.hiddenNeurons)
    	bOutRaw := make([]float64, nn.config.outputNeurons)
    
    	for _, param := range [][]float64{wHiddenRaw, bHiddenRaw, wOutRaw, bOutRaw} {
    		for i := range param {
    			param[i] = randGen.Float64()
    		}
    	}
    
    	wHidden := mat.NewDense(nn.config.inputNeurons, nn.config.hiddenNeurons, wHiddenRaw)
    	bHidden := mat.NewDense(1, nn.config.hiddenNeurons, bHiddenRaw)
    	wOut := mat.NewDense(nn.config.hiddenNeurons, nn.config.outputNeurons, wOutRaw)
    	bOut := mat.NewDense(1, nn.config.outputNeurons, bOutRaw)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后,在每个 Epoch 中首先完成前向计算:

    	// 前向计算过程
    	hiddenLayerInput := &mat.Dense{}
    	hiddenLayerInput.Mul(x, wHidden)
    	addBHidden := func(_, col int, v float64) float64 { return v + bHidden.At(0, col) }
    	hiddenLayerInput.Apply(addBHidden, hiddenLayerInput)
    
    	hiddenLayerActivations := &mat.Dense{}
    	applySigmoid := func(_, _ int, v float64) float64 { return sigmoid(v) }
    	hiddenLayerActivations.Apply(applySigmoid, hiddenLayerInput)
    
    	outputLayerInput := &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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.3.2 反向传播

    神经网络的反向传播,较为复杂,需要使用利用链式法则,计算每层的梯度信息,这里以 sigmoid 函数为例:
    d d x σ ( x ) = d d x ( 1 1 + e x ) = e x ( 1 + e x ) 2 = ( 1 + e x ) 1 ( 1 + e x ) 2 = 1 + e x ( 1 + e x ) 2 ( 1 1 + e x ) 2 = σ ( x ) σ ( x ) 2 = σ ( 1 σ ) egin{aligned} rac d {dx} σ(x) & = rac d {dx} ( rac 1 {1+e^{-x}}) \ &= rac {e^{-x}} {(1+e{-x})2} \ &= rac {(1+e^{-x})-1} {(1+e{-x})2}\ &= rac {1+e^{-x}} {(1+e{-x})2}-( rac 1 {1+e{-x}})2\ &=σ(x) - σ(x)^2=σ(1-σ) end{aligned} dxdσ(x)=dxd(1+ex1)=(1+ex)2ex=(1+ex)2(1+ex)1=(1+ex)21+ex(1+ex1)2=σ(x)σ(x)2=σ(1σ)
    使用 Go+ 语言实现代码如下:

    func sigmoidDerivation(x float64) float64 {
    	return sigmoid(x) * (1.0 - sigmoid(x))
    }
    
    • 1
    • 2
    • 3

    其他层的详细的计算步骤在此就不予展示了,这里直接给出 Go+ 语言代码:

    	//梯度的反向传播
    	networkError := &mat.Dense{}
    	networkError.Sub(y, output)	// 损失函数
    
    	slopeOutputLayer := &mat.Dense{}
    	applySigmoidDerivation := func(_, _ int, v float64) float64 { return sigmoidDerivation(v) }
    	slopeOutputLayer.Apply(applySigmoidDerivation, output)
    	slopeHiddenLayer := &mat.Dense{}
    	slopeHiddenLayer.Apply(applySigmoidDerivation, hiddenLayerActivations)
    
    	dOutput := &mat.Dense{}
    	dOutput.MulElem(networkError, slopeOutputLayer)
    	errorAtHiddenLayer := &mat.Dense{}
    	errorAtHiddenLayer.Mul(dOutput, wOut.T())
    
    	dHiddenLayer := &mat.Dense{}
    	dHiddenLayer.MulElem(errorAtHiddenLayer, slopeHiddenLayer)
    
    	// 参数修改
    	wOutAdj := &mat.Dense{}
    	wOutAdj.Mul(hiddenLayerActivations.T(), dOutput)
    	wOutAdj.Scale(nn.config.learningRate, wOutAdj)
    	wOut.Add(wOut, wOutAdj)
    
    	bOutAdj, err := sumAxis(0, dOutput)
    	if err != nil {
    		return err
    	}
    	bOutAdj.Scale(nn.config.learningRate, bOutAdj)
    	bOut.Add(bOut, bOutAdj)
    
    	wHiddenAdj := &mat.Dense{}
    	wHiddenAdj.Mul(x.T(), dHiddenLayer)
    	wHiddenAdj.Scale(nn.config.learningRate, wHiddenAdj)
    	wHidden.Add(wHidden, wHiddenAdj)
    
    	bHiddenAdj, err := sumAxis(0, dHiddenLayer)
    	if err != nil {
    		return err
    	}
    	bHiddenAdj.Scale(nn.config.learningRate, bHiddenAdj)
    	bHidden.Add(bHidden, bHiddenAdj)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    其中函数 sumAxis 用于根据维度对矩阵求和:

    func sumAxis(axis int, m *mat.Dense) (*mat.Dense, error) {
    
    	numRows, numCols := m.Dims()
    	// println(numRows, numCols)
    	
    	var output *mat.Dense
    
    	switch axis {
    	case 0:
    		result := make([]float64, numCols)
    		for i := 0; i < numCols; i++ {
    			col := mat.Col(nil, i, m)
    			result[i] = floats.Sum(col)
    		}
    		output = mat.NewDense(1, numCols, result)
    	case 1:
    		result := make([]float64, numRows)
    		for i := 0; i < numRows; i++ {
    			row := mat.Row(nil, i, m)
    			result[i] = floats.Sum(row)
    		}
    		output = mat.NewDense(numRows, 1, result)
    	default:
    		return nil, errors.New("invalid axis, must be 0 or 1")
    	}
    
    	return output, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2.4 评估神经网络

    网络的评估,首先需要利用测试数据集使用前向计算,获得网络的输出,然后和测试数据集中的标签进行对比。前向计算过程与训练过程类似,这里仅介绍如何进行评估:

    	// 使用经过训练的模型进行预测
    	predictions, err := network.predict(testInputs)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// 计算模型预测准确率
    	var truePred int
    	numPreds, _ := predictions.Dims()
    	for i := 0; i < numPreds; i++ {
    
    		// 获取标签
    		labelRow := mat.Row(nil, i, testLabels)
    		var species int
    		for idx, label := range labelRow {
    			// println(idx, label)
    			if label == 1.0 {
    				// println(idx)
    				category = idx
    				break
    			}
    		}
    
    		// 计算预测正确的个数
    		if predictions.At(i, category) == floats.Max(mat.Row(nil, i, predictions)) {
    			// for j:= 0; j < 10; j++ {
    			// 		println(j, mat.Row(nil, i, predictions)[j])
    			// }
    			truePred++
    		}
    	}
    
    	// 计算准确率
    	accuracy := float64(truePred) / float64(numPreds)
    
    	// 输出准确率
    	fmt.Printf("
    Accuracy = %0.2f
    
    ", accuracy)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    3. 程序运行

    最后就是运行程序,检测模型运行效果的时候了,在命令行中使用以下命令运行程序:

    gop run mnist_recognition.go
    
    • 1

    程序输出结果如下所示:

    Accuracy = 0.89
    
    • 1

    考虑到仅使用了一层隐藏层,可以获得接近 90% 的准确率,已经超出了基线水平了。

    后记

    改进神经网络的方法有很多,包括使用不同的神经网络模型、加深神经网络、使用不同的损失函数、修改激活函数等等,通过之后的 Go+ 学习,再继续完善改进此网络。

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    手写简单vue3响应式原理(reactive ref toRef toRefs)
    docker&kubernets篇(二十二)
    NewStarCTF 2023 公开赛道 WEEK2|MISC1-序章
    Ubuntu18.04 redis 哨兵模式搭建的步骤
    WEB逆向—X-Bogus逆向分析(纯算+补环境)
    java基础---缓冲流、数据流、打印流
    【鸿蒙软件开发】ArkUI容器组件之Grid(网格布局)
    算法笔记-第十章-图的定义和相关术语
    ubuntu18.04.1LTS 编译安装ffmpeg详解
    谈谈分布式事务原理
  • 原文地址:https://blog.csdn.net/m0_67392126/article/details/126066568