相信大家这两年都被深度学习和机器学习等高大上的人工智能概念勾起了好奇心,那么到底什么是人工智能呢?它到底有什么用?本文将带大家了解深度学习中的判别模型CNN(Convolutional Neural Networks)卷积神经网络,以及如何用一个CNN模型识别图片中的汉字。
深度学习框架 Tensorflow
正所谓,工欲善其事必先利其器。想要玩转深度学习,一个称手的兵器是少不了的。目前业界比较流行的框架有:
Google开源的Tensorflow
- Facebook AI 研究团队开发的PyTorch
- 基于Tensorflow、Theano以及CNTK后端的高层神经网络APIKeras
- Caffe、MXNet、Theano等等....
1.1 Tensorflow Core
在Tensorflow中有几个十分重要的核心概念,分别是张量、数据流、计算图、会话。
如上图所示,我们在编写Tensorflow代码的时候,实际上是构建了一张有向图,数据流图(Data Flow Graphs)。在这张有向计算图中,每个节点代表一种数据操作,节点与节点之间的连线表示数据的流向。数据实体是用称为张量(Tensor)的一种数据形式表现的。当我们在某个节点定义了一个操作(比如做了向量内积),返回的其实是一个定义好形状的张量,它只是对TensorFlow中运算结果的引用。
引文:在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程。
编写好代码后,我们算是定义好了一张计算图。在Tensorflow中,可以有多个计算图,不同计算图上的张量和运算均不会共享。我们可以通过计算图和Tensorflow提供的集合(collection)对象来管理不同的资源(如张量,变量,或者运行TensorFlow程序所需的队列资源等等)。
有了计算图,我们直接运行程序是没有反应的。在Tensorflow中,数据的计算是依赖于某个会话Session,使用Session来管理上下文。所以我们需要建立Session对象,指定计算图在哪个Session下运行。
1.2 Tensorflow特点
- 灵活性。TensorFlow不只局限于神经网络,其数据流式图支持非常自由的算法表达。理论上只要可以将计算表示成计算图的形式,就可以使用TensorFlow。用户可以写内层循环代码控制计算图分支的计算,TensorFlow会自动将相关的分支转为子图并执行迭代运算。
- 移植性。TensorFlow的另外一个重要特点是它灵活的移植性,可以将同一份代码几乎不经过修改就轻松地部署到有任意数量CPU或GPU的PC、服务器或者移动设备上。
- 分布式。对于大规模深度学习来说,巨大的数据量使得单机很难在有限的时间完成训练。这时需要分布式计算使GPU集群乃至TPU集群并行计算,共同训练出一个模型,所以框架的分布式性能是至关重要的。TensorFlow在2016年4月开源了分布式版本,使用16块GPU可达单GPU的15倍提速,在50块GPU时可达到40倍提速,分布式的效率很高。不过目前TensorFlow的设计对不同设备间的通信优化得不是很好,其单机的reduction只能用CPU处理,分布式的通信使用基于socket的RPC,而不是速度更快的RDMA,所以其分布式性能可能还没有达到最优。
- 可视化。TensorFlow还有功能强大的可视化组件TensorBoard,能可视化网络结构和训练过程,对于观察复杂的网络结构和监控长时间、大规模的训练很有帮助。
卷积神经网络 Convolutional Neural Networks
谈到深度学习,就不得不先介绍神经网络。谈到神经网络,就不得不介绍神经元。其实深度学习就是让机器模仿人脑神经元交互的行为。最早神经网络其实是用来解决线性不可分问题,即神经网络的每层代表一条曲线,样本经过多层神经网络,输出为某一类别(在平面上用多条曲线将一些点圈起来)。
2.1 感知器
为了理解神经网络,我们应该先理解神经网络的组成单元——神经元。神经元也叫做感知器。感知器算法在上个世纪50-70年代很流行,也成功解决了很多问题。并且,感知器算法也是非常简单的。感知器定义如下图:
一个感知器由多个输入的线性组合加一个激活函数组成。
感知器的激活函数可以有很多种选择,最常见的是下面这个阶跃函数。
感知器的输出由下面这个公式计算:
2.2 线性单元
上面的感知器,输出的结果是0,1。这是一个二分类感知器。假设我们需要训练一个可以输出连续值的感知器呢?那么我们需要把激活函数拿掉,直接将线性函数的结果返回,那么此时的感知器是一个解决回归问题线性单元。
假设小明(怎么又是小明!!)工作三年,在互联网行业某大型企业上班,职级评定为初级,那么我需要一个感知器帮我预测一下小明可能的收入是多少,我们可以令:
x1对应工作年限,x2对应行业,x3对应公司,x4对应职级
这样小明的收入预测公式为:
其中,b称为偏置项,可以写做:
写成向量形式:
有了模型以后,我们需要对其进行训练,不断调整参数W,使其可以拟合大部分的样本。假设我们有以下的已知数据:
对于第一次训练,我们可以将所有的w初始化为0.1,输入样本小明的数据,得到一个预测值y,但是小明收入的真实值Y并不是模型预测的值,两个值存在一个差值,我们称为误差Error
对于n个输入样本,我们可以得到:
结合上面的公式 h(x) 可以得到:
这个就是我们模型的损失函数,损失函数可以选择的种类很多,具体要根据不同的任务而定。为了使模型可以尽可能的预测正确,我们需要使损失函数降到最低。将损失函数展开,可以发现这是一个关于w的多项式函数:
使这个多项式变小的问题在数学上称作优化问题,E(w)称作目标函数。
2.3 梯度下降算法
为了方便直观理解,我们这里把向量x设定为1维向量,并且丢弃偏置项b。那么目标函数可以写成:
大家不要被以前的数学思维带偏,这个式子中只有w0是未知量,x和y都是我们已有的观测数据。那么显而易见,这个式子是一个一元二次函数,我们需要得到使这个一元二次函数最小值的w0,如下图:
这就是梯度下降算法的直观理解。梯度下降法的基本思想可以类比为一个下山的过程。对目标函数E(w)求导,可以得到一个导函数,导函数某一点的值,就是目标函数在该点沿X轴正方向的变化率。
即
是不是觉得很眼熟?
这就是目标函数的导函数,当w0取值不同的时候,表示目标函数在不同w0点的变化率。这也要求损失函数是连续可导的,存在可收敛的极值。
那么这跟梯度有什么关系呢?让我们来看一下有两个w时候的目标函数图像
在二元函数中,梯度就是方向导数最大的那个方向上的一个向量,它指向函数值上升最快的方向。显然,梯度的反方向当然就是函数值下降最快的方向了。我们每次沿着梯度相反方向去修改x的值,当然就能走到函数的最小值附近。
梯度下降算法可以写成:
其中,∇f(x)就是指f(x)的梯度。η是步长,也称作学习速率。
2.4 神经网络
讲了那么多数学公式,终于可以开始介绍神经网络了。神经网络其实就是按照一定规则连接起来的多个神经元。数学表达上是一个嵌套多层线性函数的非线性函数。让我们看一个全连接神经网络。
我们可以发现全连接神经网络有以下特点:
- 神经元按照层来布局。最左边的层叫做输入层,负责接收输入数据;最右边的层叫输出层,我们可以从这层获取神经网络输出数据。输入层和输出层之间的层叫做隐藏层。隐藏层超过两层的称为深层神经网络DNN。
- 同一层的神经元之间没有连接。
- 第N层的每个神经元和第N-1层的所有神经元相连(这就是full connected的含义),第N-1层神经元的输出就是第N层神经元的输入。
- 每个连接都有一个权值w。
2.5 卷积网络
卷积神经网络在最近几年大放异彩,几乎所有图像、语音识别领域的重要突破都是卷积神经网络取得的,比如谷歌的GoogleNet、微软的ResNet等,打败李世石的AlphaGo也用到了这种网络。在卷积神经网络中,图片中的每一个像素都是一个神经元,并且随着层数的加深,能够从图像中提取的特征也越多,我们称提取出来的特征为特征图。我们先来看一下最简单的LeNet-5卷积网络结构。
卷积神经网络包含几个重要的隐藏层,包括卷积层,池化(降采样)层,全连接层。相比于传统的全连接网络,卷积神经网络有以下特点:
- 局部连接 :每个神经元不再和上一层的所有神经元连接,只和一小部分神经元连接,这样就减小了很多参数量。
- 权值共享:一个卷积核的权值,可以被一张图片中的每个像素点(神经元)共享,至于什么是卷积核,我们等下讲。
- 下采样:也叫池化。将区域内像素点减半(甚至更多),进一步减少参数数量,同时提升模型的鲁棒性。
卷积操作可以说是整个CNN的灵魂,不得不感叹前辈大牛的脑洞。卷积的灵感来自于滤波,所以卷积核也可以叫做滤波器。我们来看一下,卷积的直观理解:
图中最左边的是一张输入的图片,它是一张彩色图片,有三个通道,也称RGB色彩空间图片。中间两个粉红色的,是我们的主角,卷积核。可以看到每个卷积核是3x3的正方形,并且也有三个通道,分别对应输入图像的三个通道。常见的卷积核有1x1、3x3、5x5和7x7。图像中被卷积核覆盖的范围内的像素值,分别乘以卷积核对应位置的权值,然后求和,把三个通道的值相加,再加上一个偏置项b,得到输出特征图中的某个像素点的值。有多少个卷积核,输出的特征图就有多少个。
其中还涉及到滑动步长、padding等概念,有兴趣的读者可以自行深入了解。
2.7 池化
池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。池化包括最大池化、平均池化等。其中最大池化是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出层,如下图所示
2.8.激活函数
最近几年卷积神经网络中,激活函数往往不选择sigmoid或tanh函数,而是选择relu函数。Relu函数的定义是:
Relu函数图像如下图所示:
使用Relu作为激活函数相比于传统的激活函数,不仅可以大大加快反向传播中的求导速度,而且可以防止反向传播中的梯度消散问题。对于不重要的神经元,也可以起到抑制的作用。
2.9 cnn的训练
因篇幅原因,关于cnn网络如何训练,感兴趣的读者可以自行阅读研究:
卷积神经网络(CNN)反向传播算法推导(https://zhuanlan.zhihu.com/p/61898234)
CNN汉字识别
我们现在就来用卷积神经网络训练一个可以识别汉字的模型。用的数据集是中科院自动化研究所公开的手写汉字数据集: CASIA-HWDB,里面的图片大概长这样:
这个数据集一共有3755个汉字,由于时间资源有限,我只选取一部分汉字进行训练,主要是为了验证模型设计合理性。
- Tensorflow版本:1.13
- python版本:3.7
- GPU:NVIDIA GTX1050Ti
- 显存:4G
- 内存:8G
- cpu:i5-6200U 4core
我们要训练的汉字如下:
将汉字保存在一个文本中chars.txt
tensorflow提供了多种API供我们读取自己的数据集,这里我们选用pipline shuffle的方式。
def input_pipeline(self, batch_size, num_epochs=None, aug=False):
images_tensor = tf.convert_to_tensor(self.image_paths, dtype=tf.string)
labels_tensor = tf.convert_to_tensor(self.labels, dtype=tf.int64)
input_queue = tf.train.slice_input_producer([images_tensor, labels_tensor],num_epochs=num_epochs)
labels = input_queue[1]
# labels = tf.one_hot(labels,FLAGS.charset_size,1,0)
images_content = tf.read_file(input_queue[0])
images = tf.image.convert_image_dtype(tf.image.decode_jpeg(images_content, channels=1), tf.float32)
if aug:
images = self.data_enhance(images)
new_size = tf.constant([FLAGS.image_size, FLAGS.image_size], dtype=tf.int32)
images = tf.image.resize_images(images, new_size)
image_batch, label_batch = tf.train.shuffle_batch([images, labels], batch_size=batch_size, capacity=50000,
min_after_dequeue=10000)
return image_batch, label_batch
用pipline的好处是,和tfrecord一样,都不会将图片全部加载进内存,而是只会保存图片的路径在内存中,每次只取出batch_size数量的图片解压。
3.2 网络设计
TF-Slim是一个封装了常用神经网络操作的高级API,能够将tensorflow中重复性的代码提取出来。我们不需要再去写很繁琐的网络代码,基本上几行就能定义一个网络。
这里我定义了4层特征提取层,顺序是卷积->池化->卷积->池化->卷积->池化->卷积->卷积->池化->全连接*2
激活函数全都是relu,并且加入dropout防过拟合,加入BN加快收敛速度。
# 核心代码
with tf.device('/gpu:0'):
with slim.arg_scope([slim.conv2d, slim.fully_connected],
normalizer_fn=slim.batch_norm,
normalizer_params={'is_training': is_training}):
conv3_1 = slim.conv2d(images, 64, [3, 3], 1, padding='SAME', scope='conv3_1')
max_pool_1 = slim.max_pool2d(conv3_1, [2, 2], [2, 2], padding='SAME', scope='pool1')
conv3_2 = slim.conv2d(max_pool_1, 128, [3, 3], padding='SAME', scope='conv3_2')
max_pool_2 = slim.max_pool2d(conv3_2, [2, 2], [2, 2], padding='SAME', scope='pool2')
conv3_3 = slim.conv2d(max_pool_2, 256, [3, 3], padding='SAME', scope='conv3_3')
max_pool_3 = slim.max_pool2d(conv3_3, [2, 2], [2, 2], padding='SAME', scope='pool3')
conv3_4 = slim.conv2d(max_pool_3, 512, [3, 3], padding='SAME', scope='conv3_4')
conv3_5 = slim.conv2d(conv3_4, 512, [3, 3], padding='SAME', scope='conv3_5')
max_pool_4 = slim.max_pool2d(conv3_5, [2, 2], [2, 2], padding='SAME', scope='pool4')
flatten = slim.flatten(max_pool_4)
fc1 = slim.fully_connected(slim.dropout(flatten, keep_prob), 1024,
activation_fn=tf.nn.relu, scope='fc1')
fc2 = slim.fully_connected(slim.dropout(fc1, keep_prob), 2048,
activation_fn=tf.nn.relu, scope='fc2')
logits = slim.fully_connected(slim.dropout(fc2, keep_prob), FLAGS.charset_size, activation_fn=None,
scope='fc3')
这是我们的网络结构图:
3.3 网络训练和验证
在跑了8个小时以后......我在训练集的准确率已经有98.9%以上,测试集的平均准确率也有90%以上。
accuracy曲线:
loss函数曲线:
OK,我在代码中设定了每500次训练保存一次模型,并且只保留最新的3个。于是我们有如下checkpoint文件:
接下来就是验证一下我们的模型对没见过的图片是否能正确预测。我从网上一张图片中切了两个字下来,分别对应上面我们要训练的文字下标9和35:
模型的泛化能力还是可以的。Top1的概率都比Top2和Top3高出不少。
小结
目前的人工智能都还是以记忆为主的机器学习,离像我们人类一样,只接触少量样本就能够提取关键特征,做到高泛化的识别,还早的很。但不可否认的是,越来越多的人投身到人工智能行业,使得广义机器学习在很短的时间内取得了许多突破性的研究成果。希望在未来,真正的人工智能可以早日到来。