我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
Posted 盼小辉丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的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 = \\beginbmatrix w_1\\\\ w_2 \\\\ \\vdots \\\\ w_n \\endbmatrix \\beginbmatrix x_1 & x_2 & \\cdots &x_n \\endbmatrix + b
o=⎣⎢⎢⎢⎡w1w2⋮wn⎦⎥⎥⎥⎤[x1x2⋯xn]+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 激活函数
仅仅通过上述线性计算,无法拟合现实生活中广泛存在的非线性模型,因此,神经网络中引入了激活函数来赋予网络非线性,有很多激活函数:sigmoid
、ReLU
和 tanh
等等。这以简单的 sigmoid
函数为例:
s
i
g
m
o
i
d
(
x
)
=
1
1
+
e
−
x
sigmoid(x)=\\frac 1 1+e^-x
sigmoid(x)=1+e−x1
在 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
。
- 首先需要下载数据;
- 然后读取数据;
//读取数据
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++
- 最后将数据整形,使得其加油可用于网络输入的形状。
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 [][]float64wHiddenRaw, 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 反向传播
神经网络的反向传播,较为复杂,需要使用利用链式法则,计算每层的梯度信息,这里以 以上是关于我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别的主要内容,如果未能解决你的问题,请参考以下文章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
−
σ
)
\\beginaligned \\frac d dx σ(x) & = \\frac d dx (\\frac 1 1+e^-x) \\\\ &=\\frac e^-x (1+e^-x)^2 \\\\ &= \\frac (1+e^-x)-1 (1+e^-x)^2\\\\ &= \\frac 1+e^-x (1+e^-x)^2-(\\frac 1 1+e^-x)^2\\\\ &=σ(x) - σ(x)^2=σ(1-σ) \\endaligned
dxdσ(x)=dxd(1+e−x1)=(1+e−x)2e−x=(1+e−x)2(1+e−x)−1=(1+e−x