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

“我的Go+语言初体验” | 征文活动进行中…
我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别

    • 0. 前言
    • 1. 神经网络相关概念
    • 2. 构建神经网络实战手写数字识别
      • 2.1 构建神经网络
        • 2.1.1 节点计算
        • 2.1.2 激活函数
        • 2.1.3 网络架构
      • 2.2 读取手写数字MNIST数据集
      • 2.3 训练神经网络
        • 2.3.1 前向计算
        • 2.3.2 反向传播
      • 2.4 评估神经网络
    • 3. 程序运行
    • 后记
0. 前言 之前发blink说自己想学一门新语言,很多热心的小伙伴推荐了 Go,这时又恰逢看到官方创作活动“我的Go+语言初体验”征文大赛,看了官方文档,发现 Go+ 完全兼容 Go 语言,并且代码更加易读。这不就是说,这波实际学习了一门语言却掌握了两门语言,表示赚到了。
于是迫不及待的开始准备体验下,既然官方介绍说 Go+ 「for engineering, STEM education, and data science」,融合了数据科学领域的 Python,那作为人工智能领域的相关从业人员,探索 Go+ 在人工智能领域的应用,我辈当然又是义不容辞了。
本文,首先简要概述下神经网络的相关概念,然后使用 Go+ 语言构建神经网络实战手写数字识别。
1. 神经网络相关概念 人工神经网络的发展受到了人脑神经元的启发,并且在多个领域中都已经取得了广泛的应用,包括图像识别、语音识别以及推荐系统等等,本文并非人工智能的详尽教程,但会简要介绍相关基础,为使用 Go+ 语言构建神经网络奠定基础。
在人工神经网络中,使用神经元接受输入数据,对数据执行操作后传递到下一神经元,每个神经元的输出称为激活,获取激活的函数称为激活函数,神经元中的参数称为权重或偏置。每个网络层中包含若干个神经元,其中接收初始输入的网络层称为输入层,产生最终结果的网络层称为输出层,位于输出层与隐藏层之间的网络层称为隐藏层。数据从输入到输出的整个传输过程称为正向传播;而反向传播是一种训练神经网络的方法,通过计算真实值与网络输出值间的误差,反向修改网络的权重。
在如下图所示的全连接网络中,每个节点表示一个神经元,整个网络包括一层输入层、一层输出层已经两层隐藏层。
面试|我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
文章图片

虽然已经有一些现有的神经网络框架可以使用,但作为体验作,本文将从头开始构建简单的全连接网络,以更好了解神经网络的基本组成以及运行原理。
本文使用 MNIST 数据集和 gonum 构建简单的全连接网络,虽然全连接网络是十分基础简单的神经网络,但是相关的模型训练流程和原理是相通的。
2. 构建神经网络实战手写数字识别 2.1 构建神经网络
我们已经知道神经网络中的节点接受输入矩阵,通过与权重矩阵进行计算后,通过激活函数后,产生输出,接下来将讲解具体计算流程。
2.1.1 节点计算 每个神经元的计算形式如下图所示:
面试|我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
文章图片

公式化后,如下所示:
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)

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

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)) }

2.1.3 网络架构 接下来,构建包含一个输入层,一个隐藏层,一个输出层的神经网络。其中,输入层包含 784 个神经元,这是由于 MNIST 数据集中每张照片包含 784 个像素点,每个像素点就是一个输入;隐藏层包含 512 个神经元,也可以使用更多或更少的神经元数量进行测试;输出层包含 10 个神经元,每个节点对应一个数字类别,这在神经网络中也称为独热编码。
网络架构定义如下:
config := neuralNetConfig{ // 输入层神经元 inputNeurons:784, // 输出层神经元 outputNeurons: 10, // 隐藏层神经元 hiddenNeurons: 128, // 训练 Epoch 数 numEpochs:5000, // 学习率 learningRate:0.01, }

学习率用于控制每个 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++ }

    }
  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)

然后,在每个 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)

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)) }

其他层的详细的计算步骤在此就不予展示了,这里直接给出 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)

其中函数 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 }

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)

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

程序输出结果如下所示:
Accuracy = 0.89

考虑到仅使用了一层隐藏层,可以获得接近 90% 的准确率,已经超出了基线水平了。
后记 改进神经网络的方法有很多,包括使用不同的神经网络模型、加深神经网络、使用不同的损失函数、修改激活函数等等,通过之后的 Go+ 学习,再继续完善改进此网络。
【面试|我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别】先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

    推荐阅读