Tensorflow Experience

Table of Contents

综述

当下机器学习火热,作为4年前也算是学过一些的研究生(半路出家),也很好奇现在的主流工业界对ML已经发展到什么程度了。在看了《精通数据科学:从线性回归到深度学习》1,以及简单的翻阅了几页的《TensorFlow for Deep Learning》2,我觉得现代深度学习的应用还算简单,比我们读书的时候新思路是很多,但是鉴于工业界的发展,实践也容易多了。

安装

[2019-04-21 Sun]

安装tensorflow

就数据研究而言,数据可视化是重要的工具。在python界,这个工具链做的最好的应该是 iPython / jupyter-notebook 系列。于是我们安装 tensorflow + iPython 的组合。

所有网上的教程都是使用 anacoda 作为包管理器,可能是因为 R 语言的原因?

我个人因为做python开发习惯用 pipenv ,直接就不做改动的也作为安装工具好了。

# 新建项目目录
mkdir tensorflow-tutorial
# 进入项目目录
cd tensorflow-tutorial
# 建设环境(使用pytho3)
pipenv shell --three
# 安装numpy
pipenv install numpy --pypi-mirror https://pypi.douban.com/simple
# 安装tensorflow
pipenv install tensorflow --pypi-mirror https://pypi.douban.com/simple

现在检验安装成果

# 启动python
python
# 创建一个简单的 TensorFlow 程序在 python 命令行
>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
>>> print(sess.run(hello))

使用jupyter可视化编程

jupyter 是使用可视化方式进行 python 编程的解释器,可以方便的进行操作回溯,文档生成,数据可视化和任务追踪。另外还可以对接远程云服务器,如 AWS 的计算资源,很方便的就可以进行重资源的计算任务。

# 安装jupyter
pipenv install jupyter
# 启动jupyter
jupyter notebook

然后就可以看到 jupyter 的交互式编程界面了。

TaOBkRcRYIGm.png?imageslim

Figure 1: jupyter-running

使用 Tensorflow 做线性回归

[2019-04-26 Fri]

线性回归

线性回归是非常基础的统计学知识,也是所有机器学习研究的源头,因为现在的数学都还没有能很好的解决非线性的问题,所以基本上所有的机器学习的思路都是用非线性核函数,将数据转换到近似线性的空间中,然后再用线性方法如线性回归进行解决。一般的,线性回归是要解决如下的问题,对线性公式: \[ y = wx + b \]

我们采集了大量 \(x\) 和 \(y\) 的值对,代表客观观察,现在想要还原他的参数 \(w\) 和 \(b\) ,从而还原出整个公式。但是由于实际的数据会有噪音,不妨假设这个噪音符合正态分布,则我们可以用如下公式表示这个真实公式: \[ y = wx + b + N(0,\epsilon) \] 其中 \(\epsilon\) 代表噪音的标准差。我们使用 NumPy 生成人造数据:

import numpy as np

N=100 # 100 个数据
w_true=5 # 假设 w = 5
b_true=5 # 假设 b = 5
noise_scale=.1
# 构建 Nx1 维的 x 数据
x_np=np.random.rand(N,1)
# 生成正态分布的 Nx1 维的噪音
noise = np.random.normal(scale=noise_scale,size=(N,1))
# 生成 y 数据,这里的 y 是 1xN 维的,方便计算
y_np=np.reshape(w_true*x_np+b_true+noise,(-1))

使用 matplotlib 画个图看一下生成的数据的分布,在 jupyter 中使用

%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt

line,=plt.plot(x_np,y_np,'bo')

结果如图所示

201904260013_105.png?imageslim

Figure 2: 生成的数据分布

设置训练网络

机器学习的步骤最主要的就是3步:

  1. 定义损失函数,也就是如何评价当前参数的指标,一般来说损失函数的值越小,当前得到的参数性能越好
  2. 定义优化算法。常见的优化算法有梯度下降算法,退火算法和遗传算法。用于在当前参数下,快速找到下一批待选择参数组合
  3. 反复迭代,寻找最优解。利用上面两个步骤反复多次,直到函数收敛,得到最终的参数,也就是模型训练完成。

本次案例中,我们用最简单的方式来定义我们的训练网络。损失函数就直接使用预测值与真实值之间的空间距离: \[ loss = ( y_{pred} - y )^2 \]

而优化算法直接使用梯度下降算法: \[ W_{next} = W - \alpha\Delta W \] 其中的 \(\alpha\) 就是迭代步长,是一个超参数,需要手动选择或者使用网格搜索才能调优的参数。

具体的代码如下:

import tensorflow as tf

# 定义数据占位符
with tf.name_scope("placeholders"): 
    x = tf.placeholder(tf.float32, (N, 1))
    y = tf.placeholder(tf.float32, (N,))
# 定义变量占位符
with tf.name_scope("weights"):
    # 使用正态分布的初始化数据
    W = tf.Variable(tf.random_normal((1, 1)))
    b = tf.Variable(tf.random_normal((1,)))
# 定义计算函数
with tf.name_scope("prediction"):
    y_pred = tf.matmul(x, W) + b
# 定义损失函数
with tf.name_scope("loss"):
    l = tf.reduce_sum((y - tf.squeeze(y_pred))**2)
# 定义下降算法
with tf.name_scope("optim"):
    # 使用默认的梯度下降算法, alpha = 0.001
    train_op = tf.train.AdamOptimizer(.001).minimize(l)
# 定义统计项
with tf.name_scope("summaries"):
    # 统计损失函数
    tf.summary.scalar("loss", l)
    # 将所有指标写入磁盘
    merged = tf.summary.merge_all()

执行训练

我们将我们生成的模拟数据,填入到训练网络的占位符,多次迭代进行网络训练。每次迭代,都将中间结果写入磁盘,方便我们观察我们的网络的有效性。代码如下:

# 定义结果写入文件
train_writer = tf.summary.FileWriter('logs/train', tf.get_default_graph())

n_steps = 8000 # 迭代 8000 次
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    # 训练模型
    for i in range(n_steps):
        feed_dict = {x: x_np, y: y_np}
        _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
        print("step %d, loss: %f" % (i, loss))
        train_writer.add_summary(summary, i)

    # 计算参数
    w_final, b_final = sess.run([W, b])

    # 计算预测值
    y_pred_np = sess.run(y_pred, feed_dict={x: x_np})

8000步后,损失函数稳定值在3.59左右

201904260942_663.png?imageslim

Figure 3: 迭代损失函数最终得到的值

我们使用命令查看一下刚才的训练过程

tensorboard --logdir logs/train

201904260944_825.png?imageslim

Figure 4: 损失函数的下降图

上图为损失函数的下降曲线。可以看到快接近最终值时,曲线明显收敛。

另一张图是网络的执行图:

201904260946_334.png?imageslim

Figure 5: 网络训练可视化

上图可以看到训练过程中每一步的流程和执行次数,线条越粗执行的次数越多。这个图可以方便的帮助我们优化自己的训练网络。

验证训练结果

现在我们已经得到了一个训练好的网络,这个预测网络的两个参数 \(W_{final}\) 和 \(b_{final}\),也就得到了最终的线性方程的公式: \[ y_{pred}=W_{final}x+b_{final} \]

现在我们用两种不同的指标来验证我们训练的这个模型: \(R^2\) 和 RMSE (root-mean-squared error,均方根差). \(R^2\) 是用于表示两个变量之间的相关性,用0~1之间的值表示,0表示不相关,1表示非常相关。对于数据集合 X 和 Y ,我们定义其 \(R^2\) 如下 \[ R^2 = \frac{cov(X,Y)^2}{\sigma^2_X\sigma^2_Y} \]

其中 \(cov(X, Y)\) 是 X和Y的协方差,表示这两个数据集之间的相似情况,而 \(\sigma_X\) 和 \(\sigma_Y\) 是标准差,记录数据集各自内部数据的独立性。所以 \(R^2\) 表示在两个数据集内,有多少个独立变量可以解释数据集的相关性。我们用如下代码计算 \(y\) 和 \(y_{pred}\) 的 \(R^2\) :

from scipy.stats import pearsonr

y_pred_np = np.resharpe(y_pred_np, -1)
r2 = pearsonr(y_np, y_pred_np)[0]**2 
print("Pearson R^2:", r2)
>> Pearson R^2: 0.994
# 绘制图像
plt.clf()
plt.xlabel["Y-true"]
plt.ylabel["Y-pred"]
plt.title("Predicted versus True valuees")
plt.scatter(y_np, y_pred_np)

201904260949_219.png?imageslim

Figure 6: 预测值和真实值之间的关系

从值可以看出,预测值和真值之间 \(R^2\) 近似 1 ,高度相关,说明我们的模型基本上学会了数据的真实规则。但是我们仔细观察绘制的图像发现,两个数轴之间的值并不完全对应。说明 \(R^2\) 无法在缩放尺度上描述模型的准确性。我们使用 RMSE 计算:

from sklearn.metrics import mean_squared_error

rms = np.sqrt(mean_squared_error(y_np, y_pred_np))
print("RMSE: ", rms)
>> RMSE: 1.027
# 绘制 RMSE 图像 
plt.clf()
plt.xlabel("x")
plt.ylabel("y")
plt.title("True Model versus Learned Model ")
plt.xlim(0, 1)
plt.scatter(x_np, y_np)
x_left = 0
y_left = w_final[0]*x_left + b_final
x_right = 1
y_right = w_final[0]*x_right + b_final
plt.plot([x_left, x_right], [y_left, y_right], color='k')

201904261359_944.png?imageslim

Figure 7: 线性公式图

RMSE代表数据预测值和真值之间的平均差异,这个值大约 1.027,说明在一定程度上,数据和真实值之间有一定差距,说明我们的算法在梯度下降过程中,达到了局部最优险井,这也是梯度下降算法常见的问题,我们可以通过多初始点为,多步长值等超参数的调优,说着增加迭代次数的方式,降低这种风险。

构建的时候遇到的问题

运行时报错:

FailedPreconditionError (see above for traceback): Attempting to use uninitialized value weights2/Variable

这个问题是因为我在开始忘了执行 sess.run(tf.global_variables_initializer()) ,导致tf的 session 无法找到对应的全局值,从而报错。在初始化 session 时加上这句话就行。

运行时报错:

You must feed a value for placeholder tensor 'placeholders6/Placeholder' with dtype float and shape [100,1]

这是因为我在交互式命令行下执行的 python 语句,在多次执行某一语句块时,干扰了图的设定。所以在每次执行设定网络参数前,先执行一句 tf.reset_default_graph() 重置网络配置就行。

小结

本篇只是用一个简单的例子说明了线性回归的基本流程。但是基本上覆盖了机器学习的全部流程:定义模型,定义损失函数,定义调优函数,然后迭代运行,在执行方法上也涵盖了tf的大部分功能点。同时因为线性回归是基本上所有机器学习的基础,所以需要仔细体会。

使用 Tensorflow 做逻辑回归

[2019-05-03 Fri]

逻辑回归

逻辑回归是机器学习中最重要的概念之一。所谓逻辑回归就是在几组数据集中,利用广义线性回归的方法,对数据进行分类的方法。虽然他叫回归,但本质上是利用回归方法做了分类。其使用前提是:

  1. 数据符合伯努利二项分布3,也就是说各项分类之间相互独立,每次成功的概率为 p,即\(x,y\sim B(\pm1,p)\)
  2. 样本数据线性可分
  3. 特征空间不是特别大

考虑一个数据集的类型分布为 0 或 1 两种,我们需要对分布在其上的数据进行类别划分,也就是说在一个数集上, \(f(x)\to{0,1}\) ,那么我们使用 sigmoid 核函数对类型进行激活,其公式如下 \[ g(z) = \frac{1}{1+e^{-z}} \]

通过绘制他的图形,可以看到其是个在坐标轴上的 S 型曲线,如图所示

201905031053_435.png?imageslim

Figure 8: sigmoid 函数图形

可以看到,当图形在 \((0,1)\) 之间分布区分,在 \(z\to-\infty\) 时,类别趋近 0,在 \(z\to\infty\) 时,类别趋近 1,且两者分别在 0 轴附近进行显著区分,以 0.5 作为分界线。所以我们只需要让我们的两个类别的数据在 0 轴左右尽可能的相互远离,类别就会在 sigmoid 函数上显现。那么我们设 \(h_\theta(x)\) 为变量 \(x\) 在线性权重为 \(\theta\) 时的类别为 1 概率,即,设 \(x\) 的线性函数: \[ z = \theta_0 + \theta_1x_1 + \theta_2x_2 ... + \theta_nx_n = \theta^Tx \]

那么他在 sigmoid 核函数的值就是其数据类别为 1 的概率,也就是: \[ h_\theta(x) = g(\theta^Tx) = \frac{1}{1+e^{-\theta^Tx}} \]

我们就把分类问题,转换成了求线性参数 \(\theta\),让 \(h_\theta(x)\) 的值更接近我们样本数据的类别分布,把一个非线性的问题,转换成了线性问题。所以可以说线性回归是一切机器学习算法的基础。

损失函数

我们让 \(h_\theta(x)\) 为数据类别为 1 时的概率,因为类型是二元的,我们可以得到各类别的概率: \[ P(y=1|x;\theta) = h_\theta(x) \] \[ P(y=0|x;\theta) = 1 - h_\theta(x) \]

又因为类别不是 0 就是 1,不妨我们可以做一个统一的公式: \[ P(y|x;\theta) = (h_\theta(x))^y(1-h_\theta(x))^{1-y} \]

又因为所有数据都是相互独立的,所以我们可以写出如下的似然函数:

\begin{align} L(\theta) &= P(y|x;\theta) \\\ &= \prod P(y_i|x_i;\theta) \\\ &= \prod (h_\theta(x_i))^y_i(1-h_\theta(x_i))^{1-y_i} \end{align}

我们求其对数,将乘法简化为加法:

\begin{array}{lcl} l(\theta) & = & logL(\theta) \\\ & = & \sum y_ilogh(x_i)+(1-y_i)log(1-h(x_i)) \end{array}

现在我们想求得最接近数据类型分布的 \(\theta\),也就是取值 \(\theta\) 让 \(l\) 的值最大,而这个 \(l\) 正是我们的损失函数(的反数,实际使用的时候取负就行了)。求值过程也就是最大似然估计法,实际操作中,我们用梯度上升法,也就是对每次迭代的 \(\theta\) 迭代一个在其位置的导数,令其快速接近最大似然值: \[ \theta_{j+1} = \theta_j + \alpha (y_j - h_\theta(x_j))x_j \]

生成模拟数据

有了以上的理论知识,我们可以进行实验。首先我们创建一系列模拟数据,创建的两组相互区分的二维数据,假设两组数据的分布接近 \((-1,-1)\) 和 \((1,1,)\),那么算法应该能把两组数据区分到 \(y_0=(-1,-1)\) 和 \(y_1=(1,1)\),假设分布满足高斯分布,则创建数据:

import numpy as np

N = 100
# 创建0.1为协方差的,均值在(-1,-1)附近的,满足高斯分布的2维数据集
x_zeros = np.random.multivariate_normal(
    mean=np.array((-1,-1)),
    cov=.1*np.eye(2),
    size=(N//2,)
)
y_zeros = np.zeros((N//2,))

# 创建0.1为协方差的,均值在(1,1)附近的,满足高斯分布的2维数据集
x_ones = np.random.multivariate_normal(
    mean=np.array((1,1)),
    cov=.1*np.eye(2),
    size=(N//2,)
)
y_ones = np.ones((N//2,))

我们绘制一下这个图像:

%matplotlib inline
import matplotlib
import numpy as np
import matplotlib.pyplot as plt

plt.scatter([x[0] for x in x_zeros], [x[1] for x in x_zeros], c="blue")
plt.scatter([x[0] for x in x_ones], [x[1] for x in x_ones], c="red")

201905031252_551.png?imageslim

Figure 9: 数据分布图

我们生成了两组数据,他们在二维空间中明显区分。

创建训练网络

通过理论分析,我们创建训练网络:

import tensorflow as tf

with tf.name_scope("placeholders"):
    x = tf.placeholder(tf.float32, (N, 2))
    y = tf.placeholder(tf.float32, (N,))
with tf.name_scope("weights"):
    W = tf.Variable(tf.random_normal((2,1)))
    b = tf.Variable(tf.random_normal((1,)))
with tf.name_scope("prediction"):
    # 创建 z 函数的定义
    y_logit = tf.squeeze(tf.matmul(x,W) + b)
    # 利用 sigmoid 函数计算类别
    y_one_prob = tf.sigmoid(y_logit)
    # 四舍五入求整获得类型
    y_pred = tf.round(y_one_prob)
with tf.name_scope("loss"):
    # 定义损失函数,这里使用的是交叉熵损失函数,本质上和负对数似然损失函数是一样的
    entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=y_logit, labels=y)
    l = tf.reduce_sum(entropy)
with tf.name_scope('optim'):
    # 以 0.01 为步长,定义梯度下降法
    train_op = tf.train.AdamOptimizer(0.01).minimize(l)
with tf.name_scope("summaries"):
    tf.summary.scalar("loss", l)
    merged = tf.summary.merge_all()

迭代训练

我们对训练网络进行迭代训练:

n_steps = 1000 # 训练迭代 1000 次
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(n_steps):
        feed_dict = {x: x_np, y: y_np}
        _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
        print("loss: %f" % loss)
        train_writer.add_summary(summary, i)

    # Get weights
    w_final, b_final = sess.run([W, b])

    # Make Predictions
    y_pred_np = sess.run(y_pred, feed_dict={x: x_np})

得到训练结果

201905031307_610.png?imageslim

Figure 10: 训练结果

可以看到,数据的损失函数在 1000 次迭代后,降到了 0.22

评估训练结果

我们使用 sklearn 的 accuracy_score 来计算分类准确性,他是计算所有分类正确的比例,即分类正确的数量比上总数量的比例:

from sklearn.metrics import accuracy_score

score = accuracy_score(y_np, y_pred_np)
print("分类指标: ", score)
>>> 分类指标 1.0

我们可以看到,所有的数据都被正确的分类了。 我们再绘制分类函数的图像,以获得更直观的体现:

from scipy.special import logit

plt.clf()
plt.xlabel("x")
plt.ylabel("y")
plt.title("True Model versus Learned Model ")

x_left = -2
# 计算概率为 0.5 时,x2 所在代位置
y_left = (1./w_final[1]) * (-b_final + logit(.5) - w_final[0]*x_left)

x_right = 2
# 计算概率为 0.5 时,x2 所在代位置
y_right = (1./w_final[1]) * (-b_final + logit(.5) - w_final[0]*x_right)

plt.scatter([x[0] for x in x_zeros], [x[1] for x in x_zeros], c="blue")
plt.scatter([x[0] for x in x_ones], [x[1] for x in x_ones], c="red")

plt.plot([x_left, x_right], [y_left, y_right], color='k')

得到图像:

201905031308_244.png?imageslim

Figure 11: 分类结果显示

可以看到数据被合理区分了。 再使用命令查看线性函数计算的下降过程:

tensorboard --logdir logs/train

得到图像:

201905031309_18.png?imageslim

Figure 12: 损失函数下降曲线

我们可以看到损失函数的下降基本达到了其极限。

问题回顾

我在运行中遇到了如下问题


TypeError Traceback (most recent call last) <ipython-input-3-8562f2a61f5e> in <module> 6 mean=np.array((-1,-1)), 7 cov=.1*np.eye(2), -—> 8 size=(N/2,) 9 ) 10 yzeros = np.zeros((N/2,))

mtrand.pyx in mtrand.RandomState.multivariatenormal() mtrand.pyx in mtrand.RandomState.standardnormal() mtrand.pyx in mtrand.cont0array() TypeError: 'float' object cannot be interpreted as an integer

这是因为在 python3 中,除号 \(/\) 默认是返回浮点数,需要像 python2 一样返回整数的话,需要用双除,即 \(//\)

小结

本文学习了逻辑回归的基本概念,并用 tensorflow 实现了逻辑回归的基本操作。其基本操作也如线性回归一样,就是 定义模型,设定损失函数,迭代,评估。逻辑回归的公式虽然多,但是基本思路还是线性回归的思路,同时他也是机器学习的重要方法,特别是 sigmoid 函数,是最常见的激活函数之一,非常重要,我们在后续的学习中还会反复遇到他。

全连接网络

[2019-10-07 Mon]

全连接网络(fully connected network)是神经网络中非常重要的概念,他是由多层网络输入输出所组成的节点映射,每一层之间的节点都相互连接,对于一个 \(R^m->R^n\) 的全连接网络,输入(m 个节点) 和输出(n 个节点)之间可能会有多个隐藏层,如图所示:

201910071139_769.jpg?imageslim

Figure 13: 一个含有 3 个隐藏层的全连接网络

全连接网络也是一个经典的神经网络的结构,其最主要的好处就是“结构无关”,也就是说我们可以无需猜测输入数据是什么结构的,所以常用在图像或者视频分析的领域。

使用 BP 算法训练全连接网络

全神经网络使用 BP 算法为下降函数,计算网络的权重。其基本原理是将正向计算的整体网络的全体损失,反向平摊到网络的各节点权重上,作为一次权重调整,多次迭代后达到整体损失最低化的过程,大致可以理解为适应于神经网络的梯度下降法。更详细的解释,可以参考这篇文章:https://www.cnblogs.com/charlotte77/p/5629865.html

值得注意的是,bp 算法不是所有的函数都能用做下降算法,最主要的限制就是 bp 算法无法保证求得的解就是全连接网络的最优解。另外 bp 算法也不是神经网络的唯一下降算法,还有一些模拟自然过程的如退火算法,或者遗传算法,都可用与下降优化。

为什么要用深度网络

实践中,大家发现越深的网络越能容易理解复杂的模型,能让更复杂的模型被理解的“更迅速”。在有同样节点数的各种神经网络中,深度越深的网络能够学习更复杂的网络结构。虽然没有明确的证明,但是大量的例证展示出,越深的神经网络能够达到越广连接网络的学习复杂度,甚至能超过我们数学家的理解范围。

训练全连接神经网络

数据变换

为什么全连接神经网络有这样的能力?一种简单的理解方式就是,认为全连接层其实就是数据空间的特征变换问题。比如通过复变函数,我们能将复杂的时域问题,转换成频域问题,把原来需要用积分的复杂问题,转换到了加减乘除就能解决的小学生问题,大大简化了问题处理难度,增加了数学处理问题的范围,经典的如傅利叶变换。在一些经典的图像处理算法中,也会有一些频域变换或者特征提取的办法,提高图像分类准确性。全连接神经网络其实起到的就是这个作用,他通过较深层次的节点变换,将难以找到规律的图像、视频等问题,转换成相对好收敛的数域中,加快了训练速度。只是,这种变换的本身是难以解释的,不像傅利叶变换这样的人为推倒出来的公式,他是通过算法学习的结果。从某种程度上来说,可能这个过程就是智能的过程。

所以有人认为,深度学习是第一种特征变换的算法,以后可能有基于更精妙的数学原理构建的特征变换算法,拓宽我们人工智能的研究领域。

激活函数

在传统的神经网络或者回归算法中,激活函数会采用 sigmoid 这样的非线性算法来完成。但是在现在的深度网络中,往往会使用更简单的线性激活函数 ReLU: \(\theta(x) = max(x,0)\) ,在实践效果中比 sigmoid 函数更有效。这是因为随着网络的深度增加,sigmoid 容易在较深的网络中将输入全转换成 0,不利于较深网络的构建。关于两者比较的进一步讨论,可以参考这篇文章:https://blog.csdn.net/Leo_Xu06/article/details/53708647

201910071326_168.jpg?imageslim

Figure 14: 比较深度 sigmoid 函数和 ReLU

在后续的分类网络中,还有 softmax 等激活函数,他们与经典的 sigmoid 有各自的特点。

全连接网络的记忆性

全连接网络的一个大问题就是只要训练的时间够长,他就能记忆整个过程中的训练数据,也就是有一定的过拟合性,所以无法用收敛来指示一个网络的训练程度。一个大型的网络很可能会将训练的损失训练成 0,这就是全连接网络的全局模糊性的一个特点的体现。但是这并不意味着训练的成功。如何去除这种过拟合是决定模型训练成功的关键。

常用的处理办法是将数据正则化(Regulaziation)。但是深度学习中的正则化和统计学中的正则化又有一定的区别,统计学中非常符合直觉的规律,在深度学习中并不适用,比如 LASSO,在深度学习的建模过程中,并没有太大的意义。

深度学习中比较常用的正则化方法有 Dropout, Early Stopping 和权值正则化等。其中最有意思的就是 Dropout,他在训练的过程中,随机的去除一些节点,如果训练得到的是“真规律”,则这些去除的节点也并不会对结果产生较大影响,否则就是伪规律。Dropout 可以有效防止网络的记忆性,并在一定程度上算是模拟了网络对新数据的预测能力。不过注意,在网络训练完成,预测结果时,需要将 dropout 关闭。

实践训练全连接网络

全连接网络在训练大型数据库时,往往会采用使用多批次的数据集,分批计算下降梯度。本次实践我们使用 deepchem 的化合物数据集 Tox21,他是一个有多种化合物特征的数据库,我们的任务则是使用机器学习来判断某种化合物是否有毒。Deepchem 的安装方法在后续章节我们再做介绍。

我们首先载入数据集

import deepchem as dc

_, (train, valid, test), _ = dc.molnet.load_tox21()

train_X, train_y, train_w = train.X, train.y, train.w
valid_X, valid_y, valid_w = valid.X, valid.y, valid.w
test_X, test_y, test_w = test.X, test.y, test.w

# 删除一些多余的标签,以做简化
train_y = train_y[:, 0]
valid_y = valid_y[:, 0]
test_y = test_y[:, 0]
train_w = train_w[:, 0]
valid_w = valid_w[:, 0]

其中,x 是化合物的特征向量,y 是标签,使用 0/1 的二元数据表示化合物是否和受体反映。由于 Tox21 是一个不平衡的数据集,也就是说正样本远小于负样本,所以数据库再单独给了一个值 w,用于表示样本的权重,以增加某个特征对正样本的重要性。这是在非平衡样本中,常用的处理方式。

创建批次数据

由于样本中共有 947 个样本,50 个一个批次的话,最后一个批次只有 47 个样本。Tensorflow 中可以方便的处理这种情况,创建可变的占位符(Placeholder),就是设置 tesnor 的第一个维度为 None:

# 使用分批数据,处理所有的不同数据内容
d = 1024  # Tox21 特征向量维度数
with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))

构建全连接网络的隐藏层

隐藏层的构建和逻辑回归中构建的方式比较类似,我们使用 n_hidden 代表隐藏层层数,这是一个超变量。

# 定义一个隐藏层
with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据
    # 使用ReLU的非线性整流结果
    # max(0, w^Tx + b)
    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)

剩余部分和前几个章节是一致的,不再赘述。

# 使用分批数据,处理所有的不同数据内容
d = 1024  # 特征向量维度数
with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))

# 定义一个隐藏层
with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据
    # 使用ReLU的非线性整流结果
    # max(0, w^Tx + b)
    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)

# 定义输出层
with tf.name_scope("output"):
    W = tf.Variable(tf.random_normal((n_hidden, 1)))
    b = tf.Variable(tf.random_normal((1, )))
    y_logit = tf.matmul(x_hidden, W) + b
    y_one_prob = tf.sigmoid(y_logit)
    y_pred = tf.round(y_one_prob)

# 定义损失函数
with tf.name_scope("loss"):
    y_expand = tf.expand_dims(y, 1)
    # 交叉熵差异损失函数
    entropy = tf.nn.sigmoid_cross_entropy_with_logits(
        logits=y_logit, labels=y_expand)
    l = tf.reduce_sum(entropy)

# 定义下降函数
with tf.name_scope("optim"):
    train_op = tf.train.AdamOptimizer(learning_rate).minimize(l)

with tf.name_scope("summaries"):
    tf.summary.scalar("loss", l)
    merged = tf.summary.merge_all()

train_writer = tf.summary.FileWriter('logs/train', tf.get_default_graph())

在隐藏层中使用 Dropout

在隐藏层中加入 Dropout 比较简单,有内置函数即可完成。但 Dropout 的概率也是个超变量,并且在训练和预测的过程中使用不同的值,训练时我们采用 0.5,预测时我们使用 1.0。所以我们也把他加入到 placeholder 中去。

with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))
    keep_prob = tf.placeholder(tf.float32)

# 定义一个隐藏层
with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据
    # 使用ReLU的非线性整流结果
    # max(0, w^Tx + b)
    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)
    # add dropout
    x_hidden = tf.nn.dropout(x_hidden, keep_prob)

喂入批量数据

过程和之前的一样,只是多一个步分 batch 的过程。实验时我们就进行了 10 段数据的分隔

# 喂入批量数据
step = 0

N = train_X.shape[0]

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for epoch in range(n_epochs):
        pos = 0
        while pos < N:
            batch_x = train_X[pos:pos + batch_size]
            batch_y = train_y[pos:pos + batch_size]
            feed_dict = {x: batch_x, y: batch_y, keep_prob: dropout_prob}
            _, summary, loss = sess.run([train_op, merged, l],
                                        feed_dict=feed_dict)
            print("epoch %d, step %d, loss: %f" % (epoch, step, loss))

            train_writer.add_summary(summary, step)

            step += 1
            pos += batch_size

    # 得到预测结果
    train_y_pred = sess.run(y_pred, feed_dict={x: train_X, keep_prob: 1.0})
    valid_y_pred = sess.run(y_pred, feed_dict={x: valid_X, keep_prob: 1.0})
    test_y_pred = sess.run(y_pred, feed_dict={x: test_X, keep_prob: 1.0})

判断训练的准确性

由于 Tox21 的数据不平衡性,使用 accuracy_score 函数进行计算时,需要加入权重,以保证样本的平衡。实践中,正样本的权重为负样本的 19 倍左右,此时用 MolecureNet 分类类别可得到 50%的准确度,相对来说就比较合理了。

# 定义评价函数,由于0标签数量比1标签数量多的多,我们需要给1标签数量加权重
train_weighted_score = accuracy_score(
    train_y, train_y_pred, sample_weight=train_w)
print("Train Weighted Classfiication Accuracy: %f" % train_weighted_score)
valid_weighted_score = accuracy_score(
    valid_y, valid_y_pred, sample_weight=valid_w)
print("Valid Weighted Classfication Accuracy: %f" % valid_weighted_score)
test_weighted_score = accuracy_score(test_y, test_y_pred, sample_weight=test_w)
print("Test Weighted Classfication Accuracy: %f" % test_weighted_score)

执行脚本,得到训练结果:

201910071428_8.png?imageslim

Figure 15: 训练结果

可以看到分 10 批,共 630 次训练后,在测试集上有了 53.8%的准确度,可以说并不算太高。

研究训练过程

使用命令

tensorboard  --logdir logs/train

打开 tensorboard,查看训练过程。首先查看可视化的计算网络,可以看到训练过程中加入了一个隐藏网络 hidden layer。

201910071433_685.png?imageslim

Figure 16: 可视化训练网络

再点开隐藏层,可以看到其中的具体过程

201910071435_699.png?imageslim

Figure 17: 隐藏层的具体计算过程

而损失函数的下降可视化如图

201910071437_519.png?imageslim

Figure 18: 损失函数可视化

可见这个过程是极其不稳定的,这也是多批次数据训练的代价。其加快了训练过程,但也降低了稳定性。后续学习中,我们就要想办法来增加训练的稳定性,以及提升准确度。

遇到的问题和解决

实践过程中,由于在这之前我都是使用的 pipenv 代替 anaconda 的方式安装的依赖,本次过程中 deepchem 无法正常使用,pip 直接安装依赖无法满足所有依赖,特别是 rdkit,提示

> pip install rdkit
Collecting rdkit
ERROR: Could not find a version that satisfies the requirement rdkit (from versions: none)
ERROR: No matching distribution found for rdkit

看 deepchem 的官网,其官方支持的只有 linux 和 osx。索幸 windows 还有 WSL,于是我在 WSL 上安装了 anaconda,具体过程如资料:https://gist.github.com/kauffmanes/5e74916617f9993bc3479f401dfec7da

安装的时候直接安装也可能会有依赖无法全部满足的问题,提示

# gsj987 @ LOU-THINKPAD in /mnt/c/Users/gsj98/Projects/tensorflow_tutorial [16:02:44] C:1
$ ~/anaconda2/bin/conda install -c deepchem deepchem=2.1.0 python=3.5

Solving environment: failed
PackagesNotFoundError: The following packages are not available from current channels:

 - deepchem=2.1.0
 - pdbfixer==1.4
 - deepchem=2.1.0
 - xgboost==0.6a2
 - deepchem=2.1.0
 - rdkit==2017.09.1

Current channels:

 - [https://conda.anaconda.org/deepchem/linux-64](https://conda.anaconda.org/deepchem/linux-64)
 - [https://conda.anaconda.org/deepchem/noarch](https://conda.anaconda.org/deepchem/noarch)
 - [https://repo.anaconda.com/pkgs/main/linux-64](https://repo.anaconda.com/pkgs/main/linux-64)
 - [https://repo.anaconda.com/pkgs/main/noarch](https://repo.anaconda.com/pkgs/main/noarch)
 - [https://repo.anaconda.com/pkgs/free/linux-64](https://repo.anaconda.com/pkgs/free/linux-64)
 - [https://repo.anaconda.com/pkgs/free/noarch](https://repo.anaconda.com/pkgs/free/noarch)
 - [https://repo.anaconda.com/pkgs/r/linux-64](https://repo.anaconda.com/pkgs/r/linux-64)
 - [https://repo.anaconda.com/pkgs/r/noarch](https://repo.anaconda.com/pkgs/r/noarch)
 - [https://repo.anaconda.com/pkgs/pro/linux-64](https://repo.anaconda.com/pkgs/pro/linux-64)
 - [https://repo.anaconda.com/pkgs/pro/noarch](https://repo.anaconda.com/pkgs/pro/noarch)

To search for alternate channels that may provide the conda package you're
looking for, navigate to
 [https://anaconda.org](https://anaconda.org)

and use the search bar at the top of the page.

解决办法是指定更详细的依赖列表:

conda install -c deepchem -c rdkit -c conda-forge -c omnia deepchem=2.1.0

另外有个坑就是 conda 启用 virtualenv 后,直接使用的 python 代替的默认 python 版本,而不是系统的 python3,后者使用的仍旧是系统版本,需要注意。

小结

全连接网络是神经网络和深度学习中的重要概念。可以说特别是在图像的分类问题上,最后都会加上一个全连接网络作为最后的分类器。现代深度学习最大的突破也是将全连接网络的可计算深度大大增加,从而带来了更多的抽象理解能力。后续的工作就是如何提升网络的准确性,加快收敛速度等角度,但基本原理都来自全连接网络,需要重点理解。

卷积网络和图像分类

[2020-03-09 Mon] 从理论上说,全连接神经网络能够表达一切的多项式,只要深度(隐藏层)够深,全连接神经网络能适应所有的模型,但缺点是需要极大的计算量,而且在深度增加,激活函数的传递量会大大减弱。卷积神经网络是为了解决这个问题,他利用类似生物的局部兴奋逐层整合,形成高级的抽象的概念的过程,减少训练参数,但增加深度,增加对抽象概念的准确性。CNN 在图像领域使用较多。

本文使用 CNN 来对 MNIST (http://yann.lecun.com/exdb/mnist/ ) 的手写数字图片集进行分类,预测出图片所对应的手写数字。

202003092051_992.png?imageslim

Figure 19: MNIST

基本概念

Local Receptive Fields 本地感受野

感受野代表的是在一个图像特征的观察窗口,就像是人的每个神经元只能在一个有限的范围内感受外界,超出这个范围的外界就需要其他的感受野来接收。如下图中的 Layer 1 上的绿色区域, 代表这个感受野为 3x3 像素的区域。这样区域的输入图像,组成的一连串框像素(特征),构成神经元,组成全连接层,最后形成网络,这就是 CNN 的基础。这个过程和人眼的视觉细胞的观察过程是类似的。

202003092059_227.png?imageslim

Figure 20: Receptive Fields

Convolutional Kernels 卷积核

每个感受野会在向下一层传递时,会应用一个非线性变换——卷积。机器视觉里的卷积和信号处理中的卷积有些类似,但更简单,只是单纯的将区域内的值相加。如上图中,Layer 1 到 Layer 2 的传递,就是将 Layer 1 中的特征相加,得到了 Layer 2 中的一个特征值。在一个特征感受野上应用的非现线性卷积变换,就是卷积核。卷积核上带有权重,会对感受野内的值进行加权计算,得到下一层的卷积特征。

202003092119_557.png?imageslim

Figure 21: Convolutional Kernels

图像中会常用多感受野的像素尺寸来标记,如 5x5, 7x7 都是很常见的卷积核,代表这个卷积核会应用在每一个 5x5 或 7x7 的感受野上。在整个图像应用卷积核时,会对画面按感受野大小进行滑窗,这个滑窗的行为会有步长,叫 stride size,比如 stride size 为 2 时,卷积核的计算之间会有一个像素的间隔。

卷积核又被称为 filter,而 filter 的数量决定我们能得到的结果数,使用时对同一个图像往往会应用多个不同的卷积核,这个数量就是卷积核的 channel 数。如我看一个风量画面,先用近视眼镜看看近景,再用远视眼镜看看远景,把两者的画面都传到下层,就是 2 个 channels。

一组卷积核就是组成了一个卷积层,比如一个 5x5x64 的卷积层,就是由 64 个以 5x5 为感受野窗口的卷积核组成的卷积层。而在整个网络中,我们要训练得到的就是这 64 个 5x5 的卷积核的权重,共 64x5x5=1600 个参数。

池化层

池化层是一个对参数进行降维的过程,比如一个 5x5 的最大池化层(Maxpool Layer),就是对每个特征的每 5x5 的窗口取最大值,传递给下一层。这样如果是一个 20x20x64 的输入,在传到下一层时,就只有 4x4x64 的大小了,大大减小了特征数量。

上文中我们讨论的卷积核、池化层都简化了第三个维度,也就是 depth。实际使用时,卷积核和池化层都会有 depth,只是在最后输入为 channel 后,得到的维度就是 channel 了。如输入的数据是 28x28x3 的,在 4x4x64 的卷积核计算后,不补齐的情况下,得到的就是 24x24x64 的特征。

Padding 补齐

由于卷积核的运算,输入的特征的尺寸一定是会变小的。实际使用中,我们会用 Padding 来补齐特征,让输出的特征的大小被控制,往往是补齐到和输入一致,这样就可以在 CNN 中多次重复某个结构多多次。如图,使用时是填 0,这样可以防止引入噪音。

202003102118_46.png?imageslim

Figure 22: padding

卷积神经网络

202003102124_76.png?imageslim

一个典型的卷积神经网络是由多个卷积层-池化层组成的结构,多次重复后,接一个全连接神经网络。实际构建时有许多的细节,但基本上是这样一个结构。卷积层的实际作用其实是一种特征变换的过程,全连接网络的作用则是真正的分类。有的多步网络也会把这两个过程分开,变换和学习做两个步骤。

识别 MNIST 手写数字图片

MNIST 数据集

MNIST [LeCun et al., 1998a] 是一个比较老的数据集,有 60,000 个手写数字样例和 10,000 个测试样例。这是 NIST 的一个子集,而后者有更大量的数据样例。每一张图片都已经二值化且合规到一样的大小(28*28)。MNIST 是很合适的图像算法上手数据集。

Google Colaboratory

Google Colaboratory (https://colab.research.google.com/) 是 Google 出品的在线版的 Jupyter Notebook,免费的分配计算资源,甚至还可以免费调用 GPU 和 TPU 来加速计算,还能在线协同,是很好的实验工具。本次识别训练,我就是使用的这个平台。

LeNet-5

202003112131_304.png?imageslim LeNet-5 是一个简单的 CNN 网络结构,由两个卷积层,中夹有两个池化层组成。最后有两个全连接层组成的全连接网络,输出结果。这是一个典型的 CNN 网络的组成。

Google 的教程中,用了一个三层卷积层的网络,后跟了一个局部连接层 Locally-connected layer 做为和全连接层的连接,这个方法常见在图像的特征在画面的局部区域,与其他的区域的关联性比较小的时候,如人脸检测。Google 的方法除了这一点,基本就是 LeNet-5 的类似实现。本次实验我只用了一个 2 层卷积层和 1 层全接连层的网络。

训练过程

导入 MNIST 数据集

由于我使用的是 Google 的 Colaboratory 服务,所以 MNIST 数据直接就使用 Google 提供的在线数据集,不再从 MNIST 的官方地址下载了。

%tensorflow_version 1.x
import tensorflow as tf
import numpy as np
import pandas as pd

tf.logging.set_verbosity(tf.logging.ERROR)
pd.options.display.max_rows = 10
pd.options.display.float_format = '{:.1f}'.format

# 载入 mnist 训练数据, 用 pandas 的 dataframe 读数据
mnist_dataframe = pd.read_csv(
  "https://download.mlcc.google.cn/mledu-datasets/mnist_train_small.csv",
  sep=",",
  header=None)

# 只使用前10000个数据做实验
mnist_dataframe = mnist_dataframe.head(10000)
mnist_dataframe = mnist_dataframe.reindex(np.random.permutation(mnist_dataframe.index))
# 展示一下数据
mnist_dataframe.head()

最后一个语句会打印一部分数据看看,如图

202003112153_190.png?imageslim

Figure 23: 展示的数据

写一个方法从数据中提取标签和特征

def parse_labels_and_features(dataset):
  """从数据中提取标签和特征

  Args:
    dataset: Dataframe, 第一列是标签,余下的列为单色的像素数据

  Returns:
    A `tuple` `(labels, features)`:
      labels: A Pandas `Series`.
      features: A Pandas `DataFrame`.
  """
  labels = dataset[0]
  # DataFrame.loc index ranges are inclusive at both ends.
  features = dataset.loc[:,1:784]
  # 原值是 0~255,归一化到 0~1.
  features = features / 255

  return labels, features

然后用这个方法分别提取出训练集,验证集和测试集。其中因为 MNIST 没有提供验证集,所以直接从训练集中取一部分做验证集。用验证集的作用是调整模型参数,找到最合适的模型,防止进入局部最优。

# 将数据的前7500个做为训练集,将标签和数据拆分
training_targets, training_examples = parse_labels_and_features(mnist_dataframe[:7500])

# 拆出验证集的数据,用数据的后2500个
validation_targets, validation_examples = parse_labels_and_features(mnist_dataframe[7500:10000])

随机的取一个数据样本,看看他长什么样。MNIST 的图都是 28x28x1 的。

# 显示一个随机的数据
from matplotlib import pyplot as plt
from IPython import display

rand_example = np.random.choice(training_examples.index)
_, ax = plt.subplots()
ax.matshow(training_examples.loc[rand_example].values.reshape(28, 28))
ax.set_title("Label: %i" % training_targets.loc[rand_example])
ax.grid(False)

得到结果

202003112204_160.png?imageslim

Figure 24: 一个展示数据的样子

用同样的方法下载 MNIST 的测试数据

# 载入 mnist 测试数据
test_dataframe = pd.read_csv(
  "https://download.mlcc.google.cn/mledu-datasets/mnist_test.csv",
  sep=",",
  header=None)

# 只使用前10000个数据做测试
test_dataframe = test_dataframe.head(5000)
test_dataframe = test_dataframe.reindex(np.random.permutation(test_dataframe.index))

# 提取测试用的特征和标签
testing_targets, testing_examples = parse_labels_and_features(test_dataframe)

设计卷积层

先设定一些基本的参数

# 参数设定
IMAGE_SIZE = 28
NUM_CHANNELS = 1
PIXEL_DEPTH = 255
NUM_LABELS = 10
SEED = 66478  # Set to None for random seed.
BATCH_SIZE = 64 # 训练集的每批次数据量大小
NUM_EPOCHS = 10
EVAL_BATCH_SIZE = 64 # 验证集和测试集的每批次数据量大小
EVAL_FREQUENCY = 100  #用验证集作评估的频率
TRAIN_SIZE = training_targets.shape[0]

然后按照 LeNet-5 的结构,设计卷积层

# 定义 train 用的 placeholder 来存放 input 数据
train_data_node = tf.placeholder(
    tf.float32,
    shape=(BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))
train_labels_node = tf.placeholder(tf.int64, shape=(BATCH_SIZE,))

# 评估用的 placeholder 来存放数据
eval_data = tf.placeholder(
    tf.float32,
    shape=(EVAL_BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))

# 定义网络的权重,2层卷积层,中有2池化层
# 第一个卷积层的权重, 5x5 的大小, depth 为 32.
conv1_weights = tf.Variable(
    tf.truncated_normal([5, 5, NUM_CHANNELS, 32],  
                        stddev=0.1,
                        seed=SEED, dtype=tf.float32))
# 加一个 bais,下同
conv1_biases = tf.Variable(tf.zeros([32], dtype=tf.float32))
# 第二个卷积层,5x5x64
conv2_weights = tf.Variable(tf.truncated_normal(
    [5, 5, 32, 64], stddev=0.1,
    seed=SEED, dtype=tf.float32))
conv2_biases = tf.Variable(tf.constant(0.1, shape=[64],
                           dtype=tf.float32))

# 定义全连接网络层,由于经过了两个 2x2 的池化层,
# 输入的特征为 (IMAGE_SIZE // 4 )^2 * 64
# 全连接网络为 7*7*64 => 512 => 10
# 第一层, depth 512.
fc1_weights = tf.Variable(
    tf.truncated_normal([IMAGE_SIZE // 4 * IMAGE_SIZE // 4 * 64, 512],
                        stddev=0.1,
                        seed=SEED,
                        dtype=tf.float32))
fc1_biases = tf.Variable(tf.constant(0.1, shape=[512],
                         dtype=tf.float32))
# 第二层,为输出层,depth 为 10
fc2_weights = tf.Variable(tf.truncated_normal([512, NUM_LABELS],
                                              stddev=0.1,
                                              seed=SEED,
                                              dtype=tf.float32))
fc2_biases = tf.Variable(tf.constant(
    0.1, shape=[NUM_LABELS], dtype=tf.float32))

组织卷积网络

开始定义 LeNet-5 网络,组织各层的结构,conv1 → maxPool1 → conv2 -> maxPool2 -> fc1 -> fc2

# 定义网络
def model(data, train=False):
  """The Model definition."""
  # 4维的strides用以匹配数据的格式 [image index, y, x, depth]
  # 每一层的 padding 为 SAME,即输入和输出的大小补全为一样
  ###
  # 第一层
  conv = tf.nn.conv2d(data,
                      conv1_weights,
                      strides=[1, 1, 1, 1],
                      padding='SAME')
  # 使用 relu 做激活函数
  relu = tf.nn.relu(tf.nn.bias_add(conv, conv1_biases))
  # 最大池化层,窗口为2,步长为2
  pool = tf.nn.max_pool(relu,
                        ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1],
                        padding='SAME')
  ###
  # 第二层
  conv = tf.nn.conv2d(pool,
                      conv2_weights,
                      strides=[1, 1, 1, 1],
                      padding='SAME')
  relu = tf.nn.relu(tf.nn.bias_add(conv, conv2_biases))
  pool = tf.nn.max_pool(relu,
                        ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1],
                        padding='SAME')

  # 将结果 reshape 后输入到全连接网络, [image index, x*y*64]
  pool_shape = pool.get_shape().as_list()
  reshape = tf.reshape(
      pool,
      [pool_shape[0], pool_shape[1] * pool_shape[2] * pool_shape[3]])

  # 一个全连接层
  hidden = tf.nn.relu(tf.matmul(reshape, fc1_weights) + fc1_biases)

  # 在训练时加入一个50%的dropout,防止过拟合
  if train:
    hidden = tf.nn.dropout(hidden, 0.5, seed=SEED)
  return tf.matmul(hidden, fc2_weights) + fc2_biases

构建预测和损失评估

# 训练计算,使用 logits + cross_entropy 做损失计算
logits = model(train_data_node, True)
loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels=train_labels_node, logits=logits))

# 使用 L2 regularization 正则化全连接网络参数
regularizers = (tf.nn.l2_loss(fc1_weights)
                + tf.nn.l2_loss(fc1_biases)
                + tf.nn.l2_loss(fc2_weights)
                + tf.nn.l2_loss(fc2_biases))

# Add the regularization term to the loss.
loss += 5e-4 * regularizers

# 优化器:设一个变量,每批数据会增加,以控制训练速度
batch = tf.Variable(0, dtype=tf.float32)

# 每个 epoch 后降低训练速度
learning_rate = tf.train.exponential_decay(
    0.01,                # Base learning rate.
    batch * BATCH_SIZE,  # Current index into the dataset.
    TRAIN_SIZE,          # Decay step.
    0.95,                # Decay rate.
    staircase=True)

# 使用 MomentumOptimizer 做为优化器
optimizer = tf.train.MomentumOptimizer(learning_rate,
                                       0.9).minimize(loss,
                                                     global_step=batch)

# 计算预测结果
train_prediction = tf.nn.softmax(logits)

# 计算余下的结果用于 validation 和 test
eval_prediction = tf.nn.softmax(model(eval_data))

其中,使用 L2 regularization 正则化的目的是为了防止过拟合,控制模型的复杂度。更详细的介绍看这里 4

另外定义一下预测结果的 loss 的评估方式。利用 numpy 的便捷语法,实现很简洁

def error_rate(predictions, labels):
  """返回预测结果的误差率"""
  return 100.0 - (
      100.0 *
      np.sum(np.argmax(predictions, 1) == labels) /
      predictions.shape[0])

分批的计算

在计算的过程中,我们采用多个 batch 的方案来让网络分批训练。使用多个 batch 的目的是减少内存和 GPU 的使用,加速训练过程,适合数据量比较大的训练过程。多个 batch 组成一个 epoch,构成一次训练。

这里定义一个方法能分批的在验证集和测试集上计算

def eval_in_batches(data, sess):
  """分批计算网络,减少内存的使用"""
  size = data.shape[0]
  predictions = np.ndarray(shape=(size, NUM_LABELS),
                              dtype=np.float32)
  for begin in xrange(0, size, EVAL_BATCH_SIZE):
    end = begin + EVAL_BATCH_SIZE
    if end <= size:
      # 取一个 EVAL_BATCH_SIZE 的数据量,放入网络评估
      predictions[begin:end, :] = sess.run(
          eval_prediction,
          feed_dict={eval_data: data[begin:end, ...]})
    else:
      # 最后一批数据,放入余下的所有数据
      batch_predictions = sess.run(
          eval_prediction,
          feed_dict={eval_data: data[-EVAL_BATCH_SIZE:, ...]})
      predictions[begin:, :] = batch_predictions[begin - size:, :]
  return predictions

整合训练过程

from six.moves import xrange

# 把 dataframe 转成 numpy array
train_data = training_examples.to_numpy(np.float32).reshape((-1, 28, 28, 1))
train_labels = training_targets.to_numpy(np.float32)
validation_data = validation_examples.to_numpy(np.float32).reshape((-1, 28, 28, 1))
validation_labels = validation_targets.to_numpy(np.float32)
test_data = testing_examples.to_numpy(np.float32).reshape((-1, 28, 28, 1))
test_labels = testing_targets.to_numpy(np.float32)

# 开启 TF session,运行训练
with tf.Session() as sess:
  # 初始化 session
  tf.global_variables_initializer().run()

  # 执行多步循环
  for step in xrange(int(NUM_EPOCHS * TRAIN_SIZE) // BATCH_SIZE):
    # 计算当前 batch 的位置,进行运算
    offset = (step * BATCH_SIZE) % (TRAIN_SIZE - BATCH_SIZE)
    batch_data = train_data[offset:(offset + BATCH_SIZE), ...]
    batch_labels = train_labels[offset:(offset + BATCH_SIZE)]

    # 将这一批的数据进行训练
    feed_dict = {train_data_node: batch_data,
                 train_labels_node: batch_labels}
    sess.run(optimizer, feed_dict=feed_dict)

    # 每次训练到一定的程度,用验证集做一次评估,并打印一些信息
    if step % EVAL_FREQUENCY == 0:
      # 计算一下当前的具体 loss 值,指示现在的训练进度
      l, lr, predictions = sess.run([loss, learning_rate,
                                     train_prediction],
                                    feed_dict=feed_dict)

      print('Step %d (epoch %.2f)' %
            (step, float(step) * BATCH_SIZE / TRAIN_SIZE))
      print('Minibatch loss: %.3f, learning rate: %.6f' % (l, lr))
      print('Minibatch error: %.1f%%'
            % error_rate(predictions, batch_labels))
      # 计算一下验证集的错误率
      print('Validation error: %.1f%%' % error_rate(
          eval_in_batches(validation_data, sess), validation_labels))

  # 训练完成,打印模型在测试集上的结果
  test_error = error_rate(eval_in_batches(test_data, sess),
                          test_labels)
  print('Test error: %.1f%%' % test_error)

训练结果

由于这里只做了 10 个 epoch,最后的测试结果在错误率 3.1%,但比线性模型已经有非常大的提升,且只在 4 个 epoch 后就达到了 3.1% 的验证错误率。

202003122159_611.png?imageslim

用得到的模型来识别手写数字

现在随便在测试集中找几个数据,用刚训练得到的模型,来识别类型,并比对结果。 我这里就是粗暴的,取了前 4 个结果来看。正式的方法应该是为单个 predict 动作再做一个 placeholde来做运算。

# 测试集数据看看
test_images = test_data[:EVAL_BATCH_SIZE, ...]

# 使用模型识别
test_predictions = sess.run(
eval_prediction,
feed_dict={eval_data: test_images})
predictions = np.argmax(test_predictions, 1)

# 显示4个结果
for i in range(4):
plt.imshow(np.reshape(test_images[i], [28, 28]), cmap='gray')
plt.show()
print("Model prediction:", predictions[i])

结果如下图,可以说是相对准确的识别出了手写数字

202003122235_699.png?imageslim

Figure 25: 预测结果

小结

本文学习了 CNN 的基本结构,卷积层的组成,卷积核的基本算法等。然后仿照 LeNet-5 的结构,用 Google Colaboratory 对 MNIST 手写数据进行训练和验证,得到了不错的结果,并用这个模型识别了测试数据,验证模型的可用性。

Footnotes:

Author: gsj987

Publish Date: 2019-04-21 Sun 00:00

License: CC BY-NC 4.0