本文的代码来自于《TensorFlow自然语言处理》(Natural Language Processing with TensorFlow),作者是Thushan Ganegedara。
对了宝贝儿们,卑微小李的公众号【野指针小李】已开通,期待与你一起探讨学术哟~摸摸大!
目录
- 0 前言
- 1 数据准备
- 2 定义超参数与常量
- 3 定义输入的占位符
- 4 定义权重与偏置的参数
- 5 定义不同作用域中不同参数的作用
- 6 定义损失函数与优化器
- 7 定义预测
- 8 运行神经网络
- 9 可视化损失与准确率
- 参考
0 前言
本文的代码来自于《TensorFlow自然语言处理》(Natural Language Processing with TensorFlow),作者是Thushan Ganegedara。在作者代码的基础上,我添加了部分自己的注释(作者的注释是英文,我的注释是用的中文)。由于有些笔记较多,并没有在代码中书写,而是写在本博客中。代码已上传至github,这里是链接。
同时这是我用TensorFlow第一次写代码,所以如果有任何错误或者没有讲解清楚的部分,请评论在下方,看到后我会更改。
作为神经网络的入门,该代码要实现的是通过神经网络,对mnist数据集进行预测。mnist是手写数字的数据集。
TensorFlow版本是1.8.0。
1 数据准备
def maybe_download(url, filename, expected_bytes, force=False):
"""Download a file if not present, and make sure it's the right size."""
if force or not os.path.exists(filename):
print('Attempting to download:', filename)
# 将远程数据下载到本地
filename, _ = urlretrieve(url + filename, filename)
print('\nDownload Complete!')
statinfo = os.stat(filename)
if statinfo.st_size == expected_bytes:
print('Found and verified', filename)
else:
raise Exception(
'Failed to verify ' + filename + '. Can you get to it with a browser?')
return filename
def read_mnist(fname_img, fname_lbl):
print('\nReading files %s and %s'%(fname_img, fname_lbl))
with gzip.open(fname_img) as fimg:
magic, num, rows, cols = struct.unpack(">IIII", fimg.read(16))
print(num,rows,cols)
img = (np.frombuffer(fimg.read(num*rows*cols), dtype=np.uint8).reshape(num, rows * cols)).astype(np.float32)
print('(Images) Returned a tensor of shape ',img.shape)
img = (img - np.mean(img))/np.std(img)
with gzip.open(fname_lbl) as flbl:
# flbl.read(8) reads upto 8 bytes
magic, num = struct.unpack(">II", flbl.read(8))
# 将data以流的形式读取,实现动态数组
lbl = np.frombuffer(flbl.read(num), dtype=np.int8)
print('(Labels) Returned a tensor of shape: %s'%lbl.shape)
print('Sample labels: ',lbl[:10])
return img, lbl
# Download data if needed
url = 'http://yann.lecun.com/exdb/mnist/'
# training data
maybe_download(url,'train-images-idx3-ubyte.gz',9912422) # 训练图片
maybe_download(url,'train-labels-idx1-ubyte.gz',28881) # 训练标签
# testing data
maybe_download(url,'t10k-images-idx3-ubyte.gz',1648877) # 预测图片
maybe_download(url,'t10k-labels-idx1-ubyte.gz',4542) # 预测标签
# Read the training and testing data
train_inputs, train_labels = read_mnist('train-images-idx3-ubyte.gz', 'train-labels-idx1-ubyte.gz')
test_inputs, test_labels = read_mnist('t10k-images-idx3-ubyte.gz', 't10k-labels-idx1-ubyte.gz')
数据准备这一部分没有什么好说的,就是从网站上下载数据。如果使用代码下载数据过慢,可以直接到网站中下载,网站为:http://yann.lecun.com/exdb/mnist/,要下载的4个数据如图所示:
2 定义超参数与常量
WEIGHTS_STRING = 'weights'
BIAS_STRING = 'bias'
batch_size = 100 # 一次训练100个样本
img_width, img_height = 28,28
input_size = img_height * img_width
num_labels = 10
# resets the default graph Otherwise raises an error about already initialized variables
tf.reset_default_graph()
这里定义了两个常量名字"WEIGHTS_STRING"和"BIAS_STRING",主要用于后面代码作用域中变量的命名。
batch_size定义了一次训练所选取的样本数。
由于该数据集中图片的大小为 28 × 28 28\times 28 28×28,所以定义图片的宽高分别为 28 28 28,每张图片的像素为 784 784 784 ( 28 × 28 28\times 28 28×28)。
因为是手写数字识别,数字一共有0-9十个,所以定义标签的数量为10。
3 定义输入的占位符
tf_inputs = tf.placeholder(shape=[batch_size, input_size], dtype=tf.float32,
name='inputs')
tf_labels = tf.placeholder(shape=[batch_size, num_labels], dtype=tf.float32,
name='labels')
这里定义了两个占位符,输入的占位符大小为 100 × 784 100 \times 784 100×784,标签的占位符大小为 100 × 10 100 \times 10 100×10。
4 定义权重与偏置的参数
def define_net_parameters():
with tf.variable_scope('layer1'):
tf.get_variable(WEIGHTS_STRING, shape=[input_size, 500],
initializer=tf.random_normal_initializer(0, 0.02))
tf.get_variable(BIAS_STRING, shape=[500],
initializer=tf.random_uniform_initializer(0, 0.01))
with tf.variable_scope('layer2'):
tf.get_variable(WEIGHTS_STRING, shape=[500, 250],
initializer=tf.random_normal_initializer(0, 0.02))
tf.get_variable(BIAS_STRING, shape=[250],
initializer=tf.random_uniform_initializer(0, 0.01))
with tf.variable_scope('output'):
tf.get_variable(WEIGHTS_STRING, shape=[250, 10],
initializer=tf.random_normal_initializer(0, 0.02))
tf.get_variable(BIAS_STRING, shape=[10],
initializer=tf.random_uniform_initializer(0, 0.01))
这一步实际上是初始化网络参数,该网络有两层隐藏层。
第一层隐藏层的scope定义为"layer1",共有500个节点,即输入层到layer1的权重 W W W的大小为 784 × 500 784 \times 500 784×500,偏置 b b b的大小为 500 × 1 500 \times 1 500×1。 W W W初始化满足正态分布 N ∼ ( 0 , 0.02 ) N \sim (0, 0.02) N∼(0,0.02), b b b初始化满足均匀分布 U ∼ ( 0 , 0.01 ) U \sim (0, 0.01) U∼(0,0.01)。这里有的同学可能会有疑问,就是正态分布的取值不是 ( − ∞ , ∞ ) (-\infty, \infty) (−∞,∞)么?这里确实是 ( − ∞ , ∞ ) (-\infty, \infty) (−∞,∞),负的权重代表着上一个节点对下一个节点的影响为负的。
第二层隐藏层的scope定义为"layer2",共有250个节点,即layer1到layer2的权重 W ′ W' W′的大小为 500 × 250 500 \times 250 500×250,偏置 b ′ b' b′的大小为 250 × 1 250 \times 1 250×1。 W W W初始化满足正态分布 N ∼ ( 0 , 0.02 ) N \sim (0, 0.02) N∼(0,0.02), b b b初始化满足均匀分布 U ∼ ( 0 , 0.01 ) U \sim (0, 0.01) U∼(0,0.01)。
输出层的scope定义为"output",共有10个节点(代表输出标签为0-9)。layer2到output的权重 W ′ ′ W'' W′′的大小为 250 × 10 250 \times 10 250×10,偏置 b ′ ′ b'' b′′的大小为 10 × 1 10\times 1 10×1。 W W W初始化满足正态分布 N ∼ ( 0 , 0.02 ) N \sim (0, 0.02) N∼(0,0.02), b b b初始化满足均匀分布 U ∼ ( 0 , 0.01 ) U \sim (0, 0.01) U∼(0,0.01)。
根据定义我们可以绘制出其网络结构:
看到这个网络图,可能聪明的宝贝一下就有问题了,代码里面有偏置项,为什么网络结构里面没有呢?。可能大家平常见的带偏置的网络结构是这样的:
我不清楚各位会不会和我有一样的被误导,就是明明偏置只有一个神经元节点,怎么是个多维向量?虽然我们从矩阵计算来看,假设输入
X
⃗
\vec{X}
X
是
N
−
d
i
m
N-{\rm dim}
N−dim,隐层是
K
−
dim
K-{\dim}
K−dim(偏置
b
⃗
\vec{b}
b
也是
K
−
dim
K-{\dim}
K−dim),
W
W
W是
N
×
K
−
d
i
m
N\times K-{\rm dim}
N×K−dim,于是矩阵计算变为:
Y
⃗
=
W
T
X
⃗
+
b
⃗
=
(
K
×
N
)
∗
(
N
×
1
)
+
(
K
×
1
)
=
K
×
1
\vec{Y}=W^T\vec{X} + \vec{b}=(K\times N) * (N\times 1) + (K\times 1)=K\times 1
Y
=WTX
+b
=(K×N)∗(N×1)+(K×1)=K×1
好像也说得通,但是问题本质没解决,就是为什么偏置也是
K
−
d
i
m
K-{\rm dim}
K−dim?
这就要说到偏置的本质了,偏置简单来说就是一个修正值,用于修正第 l l l节点到第 l + 1 l+1 l+1层节点 i i i的值,以保证这个值达到了某个阈值节点 i i i才能激活。
这句话看上去有点拗口,我们用两张图来讲解。假设我们没有偏置,那么在经过非线性函数前,函数是下图这样:
我们发现,无论我们如何改变
W
W
W的值,函数始终是要经过原点的,也就是说在
W
>
0
W>0
W>0的情况下,只有3类情况:
{
y
>
0
,
x
>
0
y
=
0
,
x
=
0
y
<
0
,
x
<
0
\begin{aligned} \left\{ \begin{aligned} &y>0, &&x>0 \\ &y=0, &&x=0 \\ &y<0, &&x<0 \end{aligned} \right. \end{aligned}
⎩⎪⎨⎪⎧y>0,y=0,y<0,x>0x=0x<0
W
=
0
W=0
W=0时就是x轴,
W
<
0
W<0
W<0时与上式刚好相反。那么如果我们不想要以0作为临界值,假设想要以5作为临界值呢?这个时候偏置就有用了,加上偏置后的函数图像就如下:
这样我们就会发现,
y
=
0
y=0
y=0时(神经元是否激活)对应的
x
x
x发生了改变。
这是原理上说明了为何偏置的维度与隐层节点的维度一致(每个隐层节点的激活阈值不同)。从这个原理来讲,我也就不喜欢将偏置单独列为一个神经元来看待,而是这样看待隐层的神经元:
虽然这样表达有点不严谨(比如没有求和过程),但是重点是我把偏置看作是每个神经元内部的参数,这样易于理解这个偏置的维度。
5 定义不同作用域中不同参数的作用
def inference(x):
with tf.variable_scope('layer1', reuse=True):
w, b = tf.get_variable(WEIGHTS_STRING), tf.get_variable(BIAS_STRING)
tf_h1 = tf.nn.relu(tf.matmul(x, w) + b, name='hidden1')
with tf.variable_scope('layer2', reuse=True):
w, b = tf.get_variable(WEIGHTS_STRING), tf.get_variable(BIAS_STRING)
tf_h2 = tf.nn.relu(tf.matmul(tf_h1, w) + b, name='hidden2')
with tf.variable_scope('output', reuse=True):
w, b = tf.get_variable(WEIGHTS_STRING), tf.get_variable(BIAS_STRING)
tf_logits = tf.nn.bias_add(tf.matmul(tf_h2, w), b, name='logits') # 将bias加到矩阵上
return tf_logits
这里layer1与layer2用的激活函数是ReLU;input → \rightarrow → layer1, layer1 → \rightarrow → layer2, layer2 → \rightarrow → output都是全连接层。
对于input
→
\rightarrow
→ layer1, layer1
→
\rightarrow
→ layer2,公式都是:
y
⃗
=
R
e
L
U
(
W
x
⃗
+
b
⃗
)
\vec{y}={\rm ReLU}(W\vec{x}+\vec{b})
y
=ReLU(Wx
+b
)
对于layer2
→
\rightarrow
→ output,公式是:
y
⃗
=
W
x
⃗
+
b
⃗
\vec{y}=W\vec{x}+\vec{b}
y
=Wx
+b
这里并没有做归一化,归一化在后面。
6 定义损失函数与优化器
由于是分类问题,所以采用交叉熵作为损失函数,采用MomentumOptimizer作为优化器。
define_net_parameters()
# defining the loss
tf_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=inference(tf_inputs),
labels=tf_labels))
# defining the optimize function
tf_loss_minimize = tf.train.MomentumOptimizer(momentum=0.9,learning_rate=0.01).minimize(tf_loss)
这里第一行代码初始化参数;第二行代码调用刚刚定义的inference函数,进行前向传递,与标签一起计算损失;第三行代码采用MomentumOptimizer优化器优化损失。
MomentumOptimizer(动量优化法),具有加速度的梯度下降方法[4]。
使用动量(Momentum)的随机梯度下降法(SGD), 其主要思想是引入一个积攒历史梯度信息动量来加速SGD.
其参数更新公式是:
v
t
=
α
v
t
−
1
+
η
Δ
J
v_t = \alpha v_{t-1} + \eta \Delta J
vt=αvt−1+ηΔJ
W
t
+
1
=
W
t
−
v
t
W_{t+1}=W_t - v_t
Wt+1=Wt−vt
其中
α
\alpha
α是超参数, 表示动量的大小, 一般取值0.9;
η
\eta
η是学习率;
Δ
J
\Delta J
ΔJ为损失函数的梯度;
v
t
v_t
vt表示
t
t
t时刻积攒的加速度;
W
t
W_t
Wt表示
t
t
t时刻的模型参数.
理解策略为:由于当前权值的改变会受到上一次权值改变的影响,类似于小球向下滚动的时候带上了惯性。这样可以加快小球向下滚动的速度。
7 定义预测
tf_predictions = tf.nn.softmax(inference(tf_inputs))
这一步是定义测试数据的预测值,由于tf_inputs只是占位符,所以这里只是先定义好预测值。
8 运行神经网络
session = tf.InteractiveSession()
tf.global_variables_initializer().run()
这一步是创建会话,并初始化所有参数。
NUM_EPOCHS = 50
def accuracy(predictions, labels):
''' Measure the classification accuracy of some predictions (softmax outputs)
and labels (integer class labels)'''
return np.sum(np.argmax(predictions,axis=1).flatten()==labels.flatten())/batch_size
test_accuracy_over_time = []
train_loss_over_time = []
for epoch in range(NUM_EPOCHS):
train_loss = []
# Training Phase
for step in range(train_inputs.shape[0]//batch_size):
# Creating one-hot encoded labels with labels
# One-hot encoding dight 3 for 10-class MNIST data set will result in
# [0,0,0,1,0,0,0,0,0,0]
labels_one_hot = np.zeros((batch_size, num_labels),dtype=np.float32)
labels_one_hot[np.arange(batch_size),train_labels[step*batch_size:(step+1)*batch_size]] = 1.0
# Printing the one-hot labels
if epoch ==0 and step==0:
print('Sample labels (one-hot)')
print(labels_one_hot[:10])
print()
# Running the optimization process
loss, _ = session.run([tf_loss,tf_loss_minimize],feed_dict={
tf_inputs: train_inputs[step*batch_size: (step+1)*batch_size,:],
tf_labels: labels_one_hot})
train_loss.append(loss) # Used to average the loss for a single epoch
test_accuracy = []
# Testing Phase
for step in range(test_inputs.shape[0]//batch_size):
test_predictions = session.run(tf_predictions,feed_dict={tf_inputs: test_inputs[step*batch_size: (step+1)*batch_size,:]})
batch_test_accuracy = accuracy(test_predictions,test_labels[step*batch_size: (step+1)*batch_size])
test_accuracy.append(batch_test_accuracy)
print('Average train loss for the %d epoch: %.3f\n'%(epoch+1,np.mean(train_loss)))
train_loss_over_time.append(np.mean(train_loss))
print('\tAverage test accuracy for the %d epoch: %.2f\n'%(epoch+1,np.mean(test_accuracy)*100.0))
test_accuracy_over_time.append(np.mean(test_accuracy)*100)
session.close()
由于这里是核心代码,我们将其拆分开讲解。关于变量与常量的定义这里不再赘述,看其名字就能理解含义。
8.1 准确率计算
def accuracy(predictions, labels):
''' Measure the classification accuracy of some predictions (softmax outputs)
and labels (integer class labels)'''
return np.sum(np.argmax(predictions,axis=1).flatten()==labels.flatten())/batch_size
接着定义了求准确率的函数,其代码含义就是将预测的向量中最大概率的下标提取出来并展开向量,与展开后的标签向量进行对比,相同则为1,反之为0,求和后再除以这一batch中的样本数,即可获得这一批的准确率。
8.2 循环epoch
for epoch in range(NUM_EPOCHS):
for step in range(train_inputs.shape[0]//batch_size):
...
for step in range(test_inputs.shape[0]//batch_size):
...
接着开始循环每一个epoch。每个epoch中,都有两个step。这个step指的是(训练/预测)样本总数 / 每一个批次中的样本数(//表示整数除法),得到的结果就是(训练/预测)一共进行多少批次。也就是说,循环的是这一epoch中的第几批次。
8.2.1 训练样本的代码
# Creating one-hot encoded labels with labels
# One-hot encoding dight 3 for 10-class MNIST data set will result in
# [0,0,0,1,0,0,0,0,0,0]
labels_one_hot = np.zeros((batch_size, num_labels),dtype=np.float32)
labels_one_hot[np.arange(batch_size),train_labels[step*batch_size:(step+1)*batch_size]] = 1.0
# Printing the one-hot labels
if epoch ==0 and step==0:
print('Sample labels (one-hot)')
print(labels_one_hot[:10])
print()
# Running the optimization process
loss, _ = session.run([tf_loss,tf_loss_minimize],feed_dict={
tf_inputs: train_inputs[step*batch_size: (step+1)*batch_size,:],
tf_labels: labels_one_hot})
train_loss.append(loss) # Used to average the loss for a single epoch
这里首先将标签用one-hot进行编码。这里为何要对其进行one-hot编码,是因为原始的标签是个标量(我们以一个标签为例),比如"5",但是我们的输出层却有10个节点(0-9),为了方便使用向量间运算,这里将标签向量化,向量化的方法就是one-hot编码。
这里向量化的方法也很巧妙,首先先创建一个大小为 100 × 10 100\times 10 100×10的零矩阵(因为每一个batch有100个样本,而标签是10维)。接着通过这行代码赋值:
labels_one_hot[np.arange(batch_size),train_labels[step*batch_size:(step+1)*batch_size]] = 1.0
这里就是取矩阵元素,并赋值为1.0。而取值的方式是,对于行,取每一行(对应np.arange(batch_size),即[0-99]);对于列,直接从标签样本中取出这100个标签(比如step为1,那么代码可以表达为train_labels[100: 200],就是train_labels中第100-199的标签),而这100个标签都是标量(比如"5")。神奇的事情就发生了,我们这行代码就可以这样表达:
l
a
b
e
l
s
_
o
n
e
_
h
o
t
[
0
,
5
1
,
0
⋮
99
,
4
]
=
1.0
{\rm labels\_one\_hot} \begin{bmatrix} 0 , 5 \\ 1 , 0 \\ \vdots \\ 99 , 4 \end{bmatrix} =1.0
labels_one_hot⎣⎢⎢⎢⎡0,51,0⋮99,4⎦⎥⎥⎥⎤=1.0
由于labels_one_hot是个
100
×
10
100\times 10
100×10的矩阵,这样就可以直接提取每一列的索引,并标记为1。
接着就是调用损失的计算以及优化。唯一要注意的是,由于损失优化是有定义一个变量的(tf_loss_minimize),所以是有返回值的(虽然是None),于是这里要用一个占位符"_"来接收这个None。
因为神经网络中的参数是定义了作用域的,这里就可以重复使用这些参数。
8.2.2 测试样本代码
for step in range(test_inputs.shape[0]//batch_size):
test_predictions = session.run(tf_predictions,feed_dict={tf_inputs: test_inputs[step*batch_size: (step+1)*batch_size,:]})
batch_test_accuracy = accuracy(test_predictions,test_labels[step*batch_size: (step+1)*batch_size])
test_accuracy.append(batch_test_accuracy)
由于训练样本在这一epoch中已经训练好了,所以对于测试样本,就通过前向传播计算这一epoch中预测的情况,并计算准确率。
9 可视化损失与准确率
可视化这里没什么说的,就是用matplotlib绘图。
import matplotlib.pyplot as plt
x_axis = np.arange(len(train_loss_over_time))
fig, ax = plt.subplots(nrows=1, ncols=2)
fig.set_size_inches(w=25,h=5)
ax[0].plot(x_axis, train_loss_over_time)
ax[0].set_xlabel('Epochs',fontsize=18)
ax[0].set_ylabel('Average train loss',fontsize=18)
ax[0].set_title('Training Loss over Time',fontsize=20)
ax[1].plot(x_axis, test_accuracy_over_time)
ax[1].set_xlabel('Epochs',fontsize=18)
ax[1].set_ylabel('Test accuracy',fontsize=18)
ax[1].set_title('Test Accuracy over Time',fontsize=20)
fig.savefig('mnist_stats.jpg')
最终平均损失以及准确率的图如下:
参考
[1] Thushan Ganegedara. Natural Language Processing with TensorFlow (TensorFlow自然语言处理)[M]. 北京: 机械工业出版社, 2019: 42-46.
[2] 子楠. 第四周笔记:神经网络是什么[EB/OL]. (2016-09-20)[2021-06-11]. https://zhuanlan.zhihu.com/p/21423252
[3] 3Blue1Brown. 【官方双语】深度学习之神经网络的结构 Part 1 ver 2.0[EB/OL]. (2017-10-19)[2021-06-11]. https://www.bilibili.com/video/BV1bx411M7Zx?t=695
[4] SanFanCSgo. 机器学习:各种优化器Optimizer的总结与比较[EB/OL]. (2018-04-26)[2021-06-11]. https://blog.csdn.net/weixin_40170902/article/details/80092628