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

盼小辉丶 社区初级贡献者 2021-12-09 10:56:23

“我的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 节点计算

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

  使用公式进行描述,如下所示:

其中, 表示权重, 表示偏置。
在 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 激活函数

仅仅通过上述线性计算,无法拟合现实生活中广泛存在的非线性模型,因此,神经网络中引入了激活函数来赋予网络非线性,有很多激活函数:sigmoid、ReLU 和 tanh 等等。这以简单的 sigmoid 函数为例:

 在 Go+ 中实现 sigmoid 函数如下:

// 激活函数
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, 784*len(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 函数求导为例:

 使用  Go+ 语言实现代码如下:

func sigmoidDerivation(x float64) float64 {
    return sigmoid(x) * (1.0 - sigmoid(x))
}

其他层的详细的计算步骤在此就不予展示了,这里直接给出 Go+ 语言代码,反向传播在前向计算后,同样需要在每个 Epoch 中完成一次:

    //梯度的反向传播
    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("\nAccuracy = %0.2f\n\n", accuracy)

3. 程序运行

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

gop run mnist_recognition.go

程序输出结果如下所示:

Accuracy = 0.89

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

后记

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

...全文
692 16 打赏 收藏 举报
写回复
16 条回复
切换为时间正序
请发表友善的回复…
发表回复
筑梦之行 2021-12-10
大佬这是推广小能手啊,每次官方活动都有你.😝
  • 打赏
  • 举报
回复
盼小辉丶 社区初级贡献者 2021-12-10
@筑梦之行 我辈义不容辞.
  • 举报
回复
把一门语言学精,效果不是更好吗?
  • 打赏
  • 举报
回复
盼小辉丶 社区初级贡献者 2021-12-10
@清风明月晓我心 融会贯通,可以根据项目需要选择合适语言,嘿嘿,技多不压身.
  • 举报
回复
能详细介绍下链式法则么?
  • 打赏
  • 举报
回复
盼小辉丶 社区初级贡献者 2021-12-10
@大数据分析师 这个博文主要是体验GO+,关于这个数学原理可以之后详细介绍,欢迎持续关注.
  • 举报
回复
头铁达人 2021-12-09
怎么才能快速掌握一门新语言呢?🧐
  • 打赏
  • 举报
回复 1
盼小辉丶 社区初级贡献者 2021-12-10
@头铁达人 多多通过编程联系吧,多用就能快速掌握.
  • 举报
回复
TechMaker 2021-12-09
我也想学下了,博主可以分享下源码么?
  • 打赏
  • 举报
回复
Go+ 2021-12-10
@TechMaker https://bbs.csdn.net/forums/goplus?typeId=19393 可以在这里了解、学习Go+语言哈
  • 举报
回复
盼小辉丶 社区初级贡献者 2021-12-10
@TechMaker 嗯,可以私信发我你邮箱,我给你发一份.
  • 举报
回复
展开其他2条回复
大佬这执行力太强了,慕了.
  • 打赏
  • 举报
回复 1
盼小辉丶 社区初级贡献者 2021-12-10
@你的眼睛里有小星星 嘿嘿,共同学习.
  • 举报
回复
盼小辉丶 社区初级贡献者 2021-12-09

我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
https://blog.csdn.net/LOVEmy134611/article/details/121757994

  • 打赏
  • 举报
回复 2
相关推荐
发帖
Go+ 开发者社区
加入

768

社区成员

Go+ 官方开发者社区。我们希望向广大的开发者和数据科学家介绍 Go+ 的定位和意义,并邀请更多开发者一起贡献代码、共建 Go+ 生态。 Go+ 官网:https://goplus.org/
帖子事件
编辑了帖子
2021-12-09 11:03
创建了帖子
2021-12-09 10:56
社区公告

本社区为 Go+ 官方开发者社区。我们希望向广大的开发者和数据科学家介绍 Go+ 的定位和意义,并邀请更多开发者一起贡献代码、共建 Go+ 生态。

Go+ 官网:https://goplus.org/
GitHub地址:https://github.com/goplus/gop