自己动手构建一个人脸识别模型----看她是否认识您

第18章 CNN实例--人脸识别

广义的人脸识别实际包括构建人脸识别系统的一系列相关技术,包括人脸图像采集、人脸定位或检测、人脸识别预处理、身份确认以及身份查找等;而狭义的人脸识别特指通过人脸进行身份确认或者身份查找的技术或系统。 人脸识别是一项热门的计算机技术研究领域,它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。本章主要内容如下:
1)先获取自己的头像,可以通过手机、电脑等拍摄;
2)下载别人的头像,具体网址详见下节;
3)利用dlib、opencv对人脸进行检测;
4)根据检测后的图片,利用卷积神经网络训练模型;
5)把新头像用模型进行识别,看模型是否能认出是你。

18.1 人脸识别简介

广义的人脸识别实际包括构建人脸识别系统的一系列相关技术,包括人脸图像采集、人脸定位、人脸识别预处理、身份确认以及身份查找等;而狭义的人脸识别特指通过人脸进行身份确认或者身份查找的技术或系统。
人脸识别是一项热门的计算机技术研究领域,它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。生物特征识别技术所研究的生物特征包括脸、指纹、手掌纹、虹膜、视网膜、声音(语音)、体形、个人习惯(例如敲击键盘的力度和频率、签字)等,相应的识别技术就有人脸识别、指纹识别、掌纹识别、虹膜识别、视网膜识别、语音识别(用语音识别可以进行身份识别,也可以进行语音内容的识别,只有前者属于生物特征识别技术)、体形识别、键盘敲击识别、签字识别等。
人脸识别的优势在于其自然性和不被被测个体察觉的特点,容易被大家接受。
人脸识别的一般处理流程,如下图:


其中:
1)图像获取:可以通过摄像镜把人脸图像采集下来头或图片上传等方式
2)人脸检测:就是给定任意一张图片,找到其中是否存在一个或多个人脸,并返回图片中 每个人脸的位置、范围及特征等。如下图:

3)人脸定位:通过人脸来确定位置信息。
4)预处理:基于人脸检测结果,对图像进行处理,为后续的特征提取服务。系统获取到的人脸图像可能受到各种条件的限制或影响,需要对进行大小缩放、旋转、拉伸、灰度变换规范化及过滤等图像预处理。由于图像中存在很多干扰因素,如外部因素:清晰度、天气、角度、距离等;目标本身因素:胖瘦,假发、围巾、银镜、表情等。所以神经网络一般需要比较多的训练数据,才能从原始的特征中提炼出有意义的特征。如下图所示,如果数据少了,神经网络性能可能还不及传统机器学习。

5)特征提取:就是将人脸图像信息数字化,把人脸图像转换为一串数字。特征提取是一项重要内容,传统机器学习这部分往往要占据大部分时间和精力,有时虽然花去了时间,效果却不一定理想,好在深度学习很多都是自动获取特征,下图为传统机器学习与深度学习的一些异同,尤其是在提取特征方面。


6)人脸特征:找到人脸的一些关键特征或位置,如眼镜、嘴唇、鼻子、下巴等的位置,利用特征点间的欧氏距离、曲率和角度等提取特征分量,最终把相关的特征连接成一个长的特征向量。如下图显示人脸的一些特征点。

7)比对识别:通过模型回答两张人脸属于相同的人或指出一张新脸是人脸库中的谁的脸。
8)输出结果:对人脸库中的新图像进行身份认证,并给出是或否的结果。

人脸识别的应用非常广泛,主要有:
1)门禁系统:受安全保护的地区可以通过人脸识别辨识试图进入者的身份,比如监狱、看守所、小区、学校等。
2)摄像监视系统:在例如银行、机场、体育场、商场、超级市场等公共场所对人群进行监视,以达到身份识别的目的。例如在机场安装监视系统以防止恐怖分子登机。
3)网络应用:利用人脸识别辅助信用卡网络支付,以防止非信用卡的拥有者使用信用卡,社保支付防止冒领等。
4)学生考勤系统:香港及澳门的中、小学已开始将智能卡配合人脸识别来为学生进行每天的出席点名记录。
5)相机:新型的数码相机已内建人脸识别功能以辅助拍摄人物时对焦。
6)智能手机:解锁手机、识别使用者等。

18.2 导入数据

获取其他人脸图片集
需要收集一个其他人脸的图片集,只要不是自己的人脸都可以,可以在网上找到,这里我给出一个我用到的图片集:
网站地址:http://vis-www.cs.umass.edu/lfw/
图片集下载:http://vis-www.cs.umass.edu/lfw/lfw.tgz
先将下载的图片集,解压到项目目录下的lfw目录下,也可以自己指定目录(修改代码中的input_dir变量)
程序中使用的是dlib来识别人脸部分,也可以使用opencv来识别人脸,在实际使用过程中,dlib的识别效果比opencv的好,但opencv识别的速度会快很多,获取10000张人脸照片的情况下,dlib大约花费了1小时,而opencv的花费时间大概只有20分钟。opencv可能会识别一些奇怪的部分,所以综合考虑之后我使用了dlib来识别人脸。
1)导入需要的包,这里使用dlib库进行人脸识别。

import sys
import os
import cv2
import dlib

2)定义输入、输出目录,文件解压到当前目录./data/my_faces目录下。
#我的头像(可以用手机或电脑等拍摄,尽量清晰、尽量多,越多越好)上传到以下input_dir目录下,output_dir为检测以后的头像

input_dir = './data/face_recog/my_faces'
output_dir = './data/my_faces'
size = 64

3)判断输出目录是否存在,不存在,则创建。

if not os.path.exists(output_dir):
os.makedirs(output_dir)

18.3 预处理数据

接下来使用dlib来批量识别图片中的人脸部分,并对原图像进行预处理,并保存到指定目录下。
1)预处理我的头像

%matplotlib inline
index = 1
for (path, dirnames, filenames) in os.walk(input_dir):
for filename in filenames:
if filename.endswith('.jpg'):
print('Being processed picture %s' % index)
img_path = path+'/'+filename
# 从文件读取图片
img = cv2.imread(img_path)
# 转为灰度图片
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用detector进行人脸检测 dets为返回的结果
dets = detector(gray_img, 1)

#使用enumerate 函数遍历序列中的元素以及它们的下标
#下标i即为人脸序号
#left:人脸左边距离图片左边界的距离 ;right:人脸右边距离图片左边界的距离
#top:人脸上边距离图片上边界的距离 ;bottom:人脸下边距离图片上边界的距离
for i, d in enumerate(dets):
x1 = d.top() if d.top() > 0 else 0
y1 = d.bottom() if d.bottom() > 0 else 0
x2 = d.left() if d.left() > 0 else 0
y2 = d.right() if d.right() > 0 else 0
# img[y:y+h,x:x+w]
face = img[x1:y1,x2:y2]
# 调整图片的尺寸
face = cv2.resize(face, (size,size))
cv2.imshow('image',face)
# 保存图片
cv2.imwrite(output_dir+'/'+str(index)+'.jpg', face)
index += 1

key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)

Being processed picture 109
Being processed picture 110
Being processed picture 111
Being processed picture 112
Being processed picture 113
Being processed picture 114
Being processed picture 115
这是处理后我的一张头像

2)用同样方法预处理别人的头像(我只选用别人部分头像)
#别人图片输入输出目录

input_dir = './data/face_recog/other_faces'
output_dir = './data/other_faces'
size = 64

3)判断输出目录是否存在,不存在,则创建。

if not os.path.exists(output_dir):
os.makedirs(output_dir)

4)预处理别人头像,同样调用本节的1)程序。

运行结果如下:
Being processed picture 264
Being processed picture 265
Being processed picture 266
Being processed picture 267
Being processed picture 268
Being processed picture 269
这是处理后别人的一张头像

以下是经预处理后的文件格式,各文件已标上序列号。

18.4 训练模型

有了训练数据之后,通过cnn来训练数据,就可以让她记住我的人脸特征,学习怎么认识我了。
1)导入需要的库

import tensorflow as tf
import cv2
import numpy as np
import os
import random
import sys
from sklearn.model_selection import train_test_split

2)定义预处理后图片(我的和别人的)所在目录

my_faces_path = './data/my_faces'
other_faces_path = './data/other_faces'
size = 64

3) 利用卷积循环网络开始训练,标注时我的表为[0,1],别人的标注为[1,0]

imgs = []
labs = []

#重新创建图形变量
tf.reset_default_graph()

def getPaddingSize(img):
h, w, _ = img.shape
top, bottom, left, right = (0,0,0,0)
longest = max(h, w)

if w < longest:
tmp = longest - w
# //表示整除符号
left = tmp // 2
right = tmp - left
elif h < longest: tmp = longest - h top = tmp // 2 bottom = tmp - top else: pass return top, bottom, left, right def readData(path , h=size, w=size): for filename in os.listdir(path): if filename.endswith('.jpg'): filename = path + '/' + filename img = cv2.imread(filename) top,bottom,left,right = getPaddingSize(img) # 将图片放大, 扩充图片边缘部分 img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=[0,0,0]) img = cv2.resize(img, (h, w)) imgs.append(img) labs.append(path) readData(my_faces_path) readData(other_faces_path) # 将图片数据与标签转换成数组 imgs = np.array(imgs) labs = np.array([[0,1] if lab == my_faces_path else [1,0] for lab in labs]) # 随机划分测试集与训练集 train_x,test_x,train_y,test_y = train_test_split(imgs, labs, test_size=0.05, random_state=random.randint(0,100)) # 参数:图片数据的总数,图片的高、宽、通道 train_x = train_x.reshape(train_x.shape[0], size, size, 3) test_x = test_x.reshape(test_x.shape[0], size, size, 3) # 将数据转换成小于1的数 train_x = train_x.astype('float32')/255.0 test_x = test_x.astype('float32')/255.0 print('train size:%s, test size:%s' % (len(train_x), len(test_x))) # 图片块,每次取100张图片 batch_size = 20 num_batch = len(train_x) // batch_size x = tf.placeholder(tf.float32, [None, size, size, 3]) y_ = tf.placeholder(tf.float32, [None, 2]) keep_prob_5 = tf.placeholder(tf.float32) keep_prob_75 = tf.placeholder(tf.float32) def weightVariable(shape): init = tf.random_normal(shape, stddev=0.01) return tf.Variable(init) def biasVariable(shape): init = tf.random_normal(shape) return tf.Variable(init) def conv2d(x, W): return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME') def maxPool(x): return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME') def dropout(x, keep): return tf.nn.dropout(x, keep) def cnnLayer(): # 第一层 W1 = weightVariable([3,3,3,32]) # 卷积核大小(3,3), 输入通道(3), 输出通道(32) b1 = biasVariable([32]) # 卷积 conv1 = tf.nn.relu(conv2d(x, W1) + b1) # 池化 pool1 = maxPool(conv1) # 减少过拟合,随机让某些权重不更新 drop1 = dropout(pool1, keep_prob_5) # 第二层 W2 = weightVariable([3,3,32,64]) b2 = biasVariable([64]) conv2 = tf.nn.relu(conv2d(drop1, W2) + b2) pool2 = maxPool(conv2) drop2 = dropout(pool2, keep_prob_5) # 第三层 W3 = weightVariable([3,3,64,64]) b3 = biasVariable([64]) conv3 = tf.nn.relu(conv2d(drop2, W3) + b3) pool3 = maxPool(conv3) drop3 = dropout(pool3, keep_prob_5) # 全连接层 Wf = weightVariable([8*16*32, 512]) bf = biasVariable([512]) drop3_flat = tf.reshape(drop3, [-1, 8*16*32]) dense = tf.nn.relu(tf.matmul(drop3_flat, Wf) + bf) dropf = dropout(dense, keep_prob_75) # 输出层 Wout = weightVariable([512,2]) bout = weightVariable([2]) #out = tf.matmul(dropf, Wout) + bout out = tf.add(tf.matmul(dropf, Wout), bout) return out def cnnTrain(): out = cnnLayer() cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=out, labels=y_)) train_step = tf.train.AdamOptimizer(0.01).minimize(cross_entropy) # 比较标签是否相等,再求的所有数的平均值,tf.cast(强制转换类型) accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(out, 1), tf.argmax(y_, 1)), tf.float32)) # 将loss与accuracy保存以供tensorboard使用 tf.summary.scalar('loss', cross_entropy) tf.summary.scalar('accuracy', accuracy) merged_summary_op = tf.summary.merge_all() # 数据保存器的初始化 saver = tf.train.Saver() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) summary_writer = tf.summary.FileWriter('./tmp', graph=tf.get_default_graph()) for n in range(10): # 每次取128(batch_size)张图片 for i in range(num_batch): batch_x = train_x[i*batch_size : (i+1)*batch_size] batch_y = train_y[i*batch_size : (i+1)*batch_size] # 开始训练数据,同时训练三个变量,返回三个数据 _,loss,summary = sess.run([train_step, cross_entropy, merged_summary_op], feed_dict={x:batch_x,y_:batch_y, keep_prob_5:0.5,keep_prob_75:0.75}) summary_writer.add_summary(summary, n*num_batch+i) # 打印损失 print(n*num_batch+i, loss) if (n*num_batch+i) % 40 == 0: # 获取测试数据的准确率 acc = accuracy.eval({x:test_x, y_:test_y, keep_prob_5:1.0, keep_prob_75:1.0}) print(n*num_batch+i, acc) # 由于数据不多,这里设为准确率大于0.80时保存并退出 if acc > 0.8 and n > 2:
#saver.save(sess, './train_face_model/train_faces.model',global_step=n*num_batch+i)
saver.save(sess, './train_face_model/train_faces.model')
#sys.exit(0)
#print('accuracy less 0.80, exited!')

cnnTrain()

运行结果:
278 0.69154
279 0.068455
280 0.092965
280 1.0
281 0.189453
282 0.0440276
283 0.078829
284 0.32079
285 0.476557
286 0.193189
287 0.147238
288 0.2862
289 0.514215
290 0.0191329
291 0.0881194
292 0.337078
293 0.191775
294 0.054846
295 0.268961
296 0.1875
297 0.11575
298 0.175487
299 0.168204

18.5 测试模型

用训练得到的模型,测试我新拍摄的头像,看她是否认识我。
首先,把我的4张测试照片放在./data/face_recog/test_faces目录,然后,让模型来识别这些照片是否是我。

%matplotlib inline

input_dir='./data/face_recog/test_faces'
index=1

output = cnnLayer()
predict = tf.argmax(output, 1)

#先加载 meta graph并恢复权重变量
saver = tf.train.import_meta_graph('./train_face_model/train_faces.model.meta')
sess = tf.Session()

saver.restore(sess, tf.train.latest_checkpoint('./train_face_model/'))
#saver.restore(sess,tf.train.latest_checkpoint('./my_test_model/'))

def is_my_face(image):
sess.run(tf.global_variables_initializer())
res = sess.run(predict, feed_dict={x: [image/255.0], keep_prob_5:1.0, keep_prob_75: 1.0})
if res[0] == 1:
return True
else:
return False

#使用dlib自带的frontal_face_detector作为我们的特征提取器
detector = dlib.get_frontal_face_detector()

#cam = cv2.VideoCapture(0)

#while True:
#_, img = cam.read()
for (path, dirnames, filenames) in os.walk(input_dir):
for filename in filenames:
if filename.endswith('.jpg'):
print('Being processed picture %s' % index)
index+=1
img_path = path+'/'+filename
# 从文件读取图片
img = cv2.imread(img_path)
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
dets = detector(gray_image, 1)
if not len(dets):
print('Can`t get face.')
cv2.imshow('img', img)
key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)
for i, d in enumerate(dets):
x1 = d.top() if d.top() > 0 else 0
y1 = d.bottom() if d.bottom() > 0 else 0
x2 = d.left() if d.left() > 0 else 0
y2 = d.right() if d.right() > 0 else 0
face = img[x1:y1,x2:y2]
# 调整图片的尺寸
face = cv2.resize(face, (size,size))
print('Is this my face? %s' % is_my_face(face))
cv2.rectangle(img, (x2,x1),(y2,y1), (255,0,0),3)
cv2.imshow('image',img)
key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)

sess.close()

测试结果:
INFO:tensorflow:Restoring parameters from ./train_face_model/train_faces.model
Being processed picture 1
Is this my face? False
Being processed picture 2
Is this my face? True
Being processed picture 3
Is this my face? True
Being processed picture 4
Is this my face? True
通过识别我的脸来判断是否是我:

结果不错,4张照片,认出了3张。
因这次拍摄照片不多(不到200张),清晰度也不很好,有这个结果,感觉还不错,如果要想达到98%以上的精度,拍摄多一点照片是有效方法。
此外,本身算法还有很多优化空间。

循环神经网络(RNN)活起来了!

第13 章 循环神经网络

13.1 循环神经网络简介

前面介绍卷积神经网络模型主要用于处理网格化数据,而且预先假设输入数据之间是互相独立的。但在很多实际应用中,数据之间是互相依赖的。比如,当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,我们需要处理这些词连接起来的整个序列;当我们思考问题时,我们都是根据以往的经验和知识,再结合当前的实际情况来综合考虑的。像处理这些有顺序有关的问题,如果用前馈神经网络(如卷积神经网络),会有很大的局限性。为解决这类问题,就诞生了时序神经网络(循环神经网络和递归神经网络)。
这章我们主要介绍循环神经网络,循环神经网络(Recurrent Neural Networks,简称为RNN)是目前非常流行的神经网络模型,在自然语言处理的很多任务中已经展示出卓越的效果。
循环神经网络的发展并不顺利,自从20世纪80年代以来,人们不断优化,出现了很多RNN的变种,由于受当时计算能力的限制,都没有得到广泛的应用。不过随着计算能力的不断提升,近年来情况大有改观。随着一些重要架构的出现,尤其在2006年提出的LSTM,RNN已经有非常强大的应用,能够很好地完成许多领域的序列任务,在语音识别、机器人翻译、人机对话、语音合成、视频处理等方面大显身手。
RNN有很多变种,我们先介绍一种简单的RNN网络,即Elman循环网络,然后介绍循环神经网络的几种有代表性的升级版,如LSTM、GRC、BiLSTM等。

13.2 Elman神经网络

Elman网络是 J. L. Elman于 1990年首先针对语音处理问题而提出来的, 它是一种典型的局部回归网络( global feed for ward l ocal recurrent)。Elman网络可以看作是一个具有局部记忆单元和局部反馈连接的前向神经网络。Elman网络具有与多层前向网络相似的多层结构。它的主要结构是前馈连接, 包括输入层、 隐含层、 输出层, 其连接权可以进行学习修正;反馈连接由一组“结构 ” 单元构成,用来记忆前一时刻的输出值, 其连接权值是固定的。

13.2.1 Elman结构

我们先回顾一下全连接的神经网络,如下图:

这种神经网络中,隐含层的值只取决于输入的x,而且隐含层的神经元之间是没有关联的。
我们看一简单RNN图形,如下图,它由输入层、一个隐藏层和一个输出层组成。

初次看到这个图,很多人会有点头晕,我们通常神经网络,一般是输入-->隐含-->输出,这里隐含层中突然冒出一个带方向的圈,而且上面还标有一个W。这个表示啥意思呢?
其实这个带箭头及W的圈就是循环网络的灵魂,它表示循环神经网络的隐藏层的值s不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值s。权重矩阵 W就是隐藏层上一次的值作为这一次的输入的权重。如果我们把上面的图展开(unfold),循环神经网络也可以画成下面这个样子:

 


输入与之前的状态合并为一个向量:


不像传统的深度神经网络,在不同的层使用不同的参数,循环神经网络在所有步骤中共享参数(U、V、W)。这个反映一个事实,我们在每一步上执行相同的任务,仅仅是输入不同。这个机制极大减少了我们需要学习的参数的数量;
上图在每一步都有输出,但是根据任务的不同,这个并不是必须的。例如,当预测一个句子的情感时,我们可能仅仅关注最后的输出,而不是每个词的情感。相似地,我们在每一步中可能也不需要输入。循环神经网络最大的特点就是隐层状态,它可以捕获一个序列的一些信息。

13.2.2 随时间反向传播(BPTT)

前面我们简单介绍RNN的结构,下面我们探讨一下如何找到最好的权值矩阵,或如何对权值矩阵进行优化。对前馈网络,最流行的优化方法是梯度下降法,优化权值矩阵通过反向传播法(BP),这些方法能在应用在RNN网络吗?当然能,只不过需要对RNN做一点小变化,只要把RNN沿时间轴展开,展开之后便可使用前馈网络一般的方式对RNN进行优化。这样,计算误差相对于各权值的梯度,便可对展开的RNN使用标准的BP方法,因这里涉及一个时间因素,故对RNN来说,这种算法称为随时间反向传播(Back-Propagation Through Time,BPTT)。

我们的优化目标是参数,计算出误差分别对参数U、W、V的梯度,然后用随机梯度下降学习好的参数。像我们汇总这些误差一样,我们也需要汇总一个训练样例在每个时间步(time step)的梯度:

 

 

我们可以看到每个时间步长对梯度的贡献。因为W在我们关心的输出的每个步骤中都使用了,所以我们需要将梯度从 网络中一直反向传播到 ,具体过程如下图:

请注意,这与我们在深度前馈神经网络中使用的标准反向传播算法完全相同。关键的区别在于我们汇总了 每个时间步的梯度。在传统的神经网络中,我们不共享层间参数,所以我们不需要汇总任何东西。不过,BPTT只是展开RNN标准反向传播的一个奇特名称。就像Backpropagation一样,我们也可以定义一个向后传递的δ向量,例如:

在代码中,一个朴素的BPTT实现如下所示:

def bptt(self, x, y):
T = len(y)
# 实现前向传导
o, s = self.forward_propagation(x)
# 初始化这些变量的梯度
dLdU = np.zeros(self.U.shape)
dLdV = np.zeros(self.V.shape)
dLdW = np.zeros(self.W.shape)
delta_o = o
delta_o[np.arange(len(y)), y] -= 1.
# For each output backwards...
for t in np.arange(T)[::-1]:
dLdV += np.outer(delta_o[t], s[t].T)
# Initial delta calculation: dL/dz
delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))
# Backpropagation through time (for at most self.bptt_truncate steps)
for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)
# Add to gradients at each previous step
dLdW += np.outer(delta_t, s[bptt_step-1])
dLdU[:,x[bptt_step]] += delta_t
# Update delta for next step dL/dz at t-1
delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)
return [dLdU, dLdV, dLdW]

从以上代码中易发现,为什么标准RNN比较难训练:序列(句子)可能很长,可能是20个字或更多,因此你需要通过很多层向后传播。在实践中,许多人通过截断方式来限制传播的步数。

13.2.3 梯度消失或爆炸

前面我们提到RNN在学习远程依赖方面存在的问题,这些远程依赖是相隔几个步骤的单词之间的相互作用。这是有问题的,因为英语句子的意思通常是由不太接近的词语来决定的:“头上戴假发的人进去了”。这句话实际上是关于一个人进去,而不是关于假发。但是简单的RNN不可能捕捉到这样的信息。为了理解为什么,让我们仔细看看我们上面计算的梯度:

事实上,上面的雅可比矩阵的2范数的上界为1(其证明大家可以参考相关文档)。而为 (或sigmoid)激活函数将所有值映射到-1和1之间的范围内,并且导数也小于1(而对sigmoid其导数小于1/4),可参考下图:

图13.1 tanh函数及其导数图形

图13.2 sigmoid函数及导数图形

从上图不难看出,tanh(或sigmoid)函数的两端导数逐渐趋于0。最后接近一条水平线。当这种情况出现时,我们就认为相应的神经元饱和了。
它们的梯度为0使得前面层的梯度也为0。矩阵中存在比较小的值,多个矩阵相乘会使梯度值以指数级速度下降,最终在几步后完全消失。比较远的时刻的梯度值为0,这些时刻的状态对学习过程没有帮助,导致你无法学习到长距离依赖。消失梯度问题不仅出现在RNN中,同样也出现在深度前向神经网中。只是RNN通常比较深(例子中深度和句子长度一致),使得这个问题更加普遍。
不难想到,依赖于我们的激活函数和网络参数,如果Jacobian矩阵中的值太大,会产生梯度爆炸而不是梯度消失问题。梯度消失比梯度爆炸受到了更多的关注有两方面的原因。其一,梯度爆炸容易发现,梯度值会变成NaN,导致程序崩溃。
其二,用预定义的阈值裁剪梯度可以简单有效的解决梯度爆炸问题。
梯度消失出现的时候不那么明显而且不好处理。
遇到梯度消失或爆炸是件比较麻烦的事,幸运的是,已经有一些方法解决了梯度消失或爆炸的问题。合适的初始化矩阵W可以减小梯度消失效应,正则化也能起作用。更好的方法是选择ReLU而不是sigmoid和tanh作为激活函数。ReLU的导数是常数值0或1,所以不可能会引起梯度消失。更通用的方案时采用长短项记忆(LSTM)或门限递归单元(GRU)结构。LSTM在1997年第一次提出,可能是目前在NLP上最普遍采用的模型。GRU,2014年第一次提出,是LSTM的简化版本。这两种RNN结构都是为了处理梯度消失问题而设计的,可以有效地学习到长距离依赖,我们会在本章后面部分进行介绍。

13.2.4 循环神经网络扩展

多年来,研究人员开发了更复杂的RNN来处理简单RNN模型的一些缺点。我们将在本章后面的更详细地介绍它们,但是我希望本节能够作为一个简要概述,以便您熟悉模型的分类。



深度(双向)RNN类似于双向RNN,只是在每个时间步有多个层。实际上这给了我们更高的学习能力(但是我们也需要大量的训练数据)。其结构如下:

13.2.5 RNN应用举例

接下来,我们介绍一下基于RNN语言模型。我们首先把词依次输入到循环神经网络中,每输入一个词,循环神经网络就输出截止到目前为止,下一个最可能的词。例如,当我们依次输入:
我 昨天 上学 迟到 了
神经网络的输出如下图所示:

其中,s和e是两个特殊的词,分别表示一个序列的开始和结束。
整个计算过程下图所示,第一个单词被转换成机器可读的向量。然后,RNN 逐个处理向量序列

在处理过程中,它将之前的隐状态传递给序列的下一个步骤。隐状态作为神经网络的记忆,保存着网络先前观察到的数据信息。

13.2.5 .1向量化

神经网络的输入和输出都是向量,为了让语言模型能够被神经网络处理,我们必须把词表达为向量的形式,这样神经网络才能处理它。
神经网络的输入是词,我们可以用下面的步骤对输入进行向量化:
1)建立一个包含所有词的词典,每个词在词典里面有一个唯一的编号。
2)任意一个词都可以用一个N维的one-hot向量来表示(也可以采用word2vec方式)。其中,N是词典中包含的词的个数。假设一个词在词典中的编号是i,v是表示这个词的向量,是向量的第j个元素,则:

上面这个公式的含义,可以用下面的图来直观的表示:

使用这种向量化方法,我们就得到了一个高维、稀疏的向量(稀疏是指绝大部分元素的值都是0)。处理这样的向量会导致我们的神经网络有很多的参数,带来庞大的计算量。因此,往往会需要使用一些降维方法,将高维的稀疏向量转变为低维的稠密向量。不过这个话题我们就不再这篇文章中讨论了。
语言模型要求的输出是下一个最可能的词,我们可以让循环神经网络计算计算词典中每个词是下一个词的概率,这样,概率最大的词就是下一个最可能的词。因此,神经网络的输出向量也是一个N维向量,向量中的每个元素对应着词典中相应的词是下一个词的概率。如下图所示:

13.2.5 .2 Softmax层

前面提到,语言模型是对下一个词出现的概率进行建模。那么,怎样让神经网络输出概率呢?方法就是用softmax层作为神经网络的输出层。
我们先来看一下softmax函数的定义:

这个公式看起来有点抽象,我们举一个例子来说明。Softmax层如下图所示:

从上图我们可以看到,softmax layer的输入是一个向量,输出也是一个向量,两个向量的维度是一样的(在这个例子里面是4)。输入向量x=[1 2 3 4]经过softmax层之后,经过上面的softmax函数计算,转变为输出向量y=[0.03 0.09 0.24 0.64]。计算过程为:

我们来看看输出向量y的特征:
1)每一项为取值为0-1之间的正数;
2)所有项的总和是1。
我们不难发现,这些特征和概率的特征是一样的,因此我们可以把它们看做是概率。对于语言模型来说,我们可以认为模型预测下一个词是词典中第一个词的概率是0.03,是词典中第二个词的概率是0.09,以此类推。

13.2.5 .3语言模型的训练

可以使用监督学习的方法对语言模型进行训练,首先,需要准备训练数据集。接下来,我们介绍怎样把语料。

我 昨天 上学 迟到 了

转换成语言模型的训练数据集。
首先,我们获取输入-标签对:

然后,使用前面介绍过的向量化方法,对输入x和标签y进行向量化。这里面有意思的是,对标签y进行向量化,其结果也是一个one-hot向量。例如,我们对标签『我』进行向量化,得到的向量中,只有第2019个元素的值是1,其他位置的元素的值都是0。它的含义就是下一个词是『我』的概率是1,是其它词的概率都是0。
最后,我们使用交叉熵误差函数作为优化目标,对模型进行优化。
在实际工程中,我们可以使用大量的语料来对模型进行训练,获取训练数据和训练的方法都是相同的。

13.2.5 .4交叉熵误差

一般来说,当神经网络的输出层是softmax层时,对应的误差函数E通常选择交叉熵误差函数,其定义如下:

当然我们可以选择其他函数作为我们的误差函数,比如最小平方误差函数(MSE)。不过对概率进行建模时,选择交叉熵误差函数更合理一些。

13.3 LSTM网络

前面我们介绍了循环神经网络的简单模型,这种模型有很多不足,尤其是层数稍多时出现梯度消失或爆炸的情况。为解决这些问题,研究人员苦苦探究,经过近8年的不懈努力,终于结出硕果,于1997年由Sepp Hochreiter和Jürgen Schmidhuber首次提出LSTM(Long Short Term Memory Network, LSTM),它成功的解决了原始循环神经网络的缺陷,成为当前最流行的RNN,在语音识别、图片描述、自然语言处理等许多领域中成功应用。2014年首次使用的GRU是LSTM的一个简单变体,它们拥有许多相同的属性。我们先看看LSTM,然后看看GRU是如何不同的。

13.3.1 LSTM网络

LSTM的思路比较简单。原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么,假如我们再增加一个状态,即c,让它来保存长期的状态,那么问题不就解决了么?如下图所示:

新增加的状态c,称为单元状态(cell state)。我们把上图按照时间维度展开:


LSTM的关键,就是怎样控制长期状态c。在这里,LSTM的思路是使用三个控制开关。第一个开关,负责控制继续保存长期状态c;第二个开关,负责控制把即时状态输入到长期状态c;第三个开关,负责控制是否把长期状态c作为当前的LSTM的输出。三个开关的作用如下图所示:

13.3.2 LSTM前向计算

前面描述的开关是怎样在算法中实现的呢?这就用到了门(gate)的概念。门实际上就是一层全连接层,它的输入是一个向量,输出是一个0到1之间的实数向量。假设W是门的权重向量,b是偏置项,那么门可以表示为:

门的使用,就是用门的输出向量按元素乘以我们需要控制的那个向量。因为门的输出是0到1之间的实数向量,那么,当门输出为0时,任何向量与之相乘都会得到0向量,这就相当于啥都不能通过;输出为1时,任何向量与之相乘都不会有任何改变,这就相当于啥都可以通过。因为σ(也就是sigmoid函数)的值域是(0,1),所以门的状态都是半开半闭的。
下图显示了遗忘门的计算过程:

 
下图为遗忘门的计算公式:

下图描述了输入门的计算过程:


下图演示了状态的计算过程:

下图表示输出门的过程及计算公式:

LSTM最终的输出,是由输出门和单元状态共同确定的:


下图表示LSTM最终输出的计算:

除了LSTM的四个门控制其状态外,LSTM还有一个固定权值为1的自连接,以及一个线性激活函数,因此,其局部偏导数始终为1。 在反向传播阶段,这个所谓的常量误差传输子(constant error carousel,CEC)能够在许多时间步中携带误差面不会发生梯度消失或梯度爆炸。

13.3.3 LSTM的训练

13.3.3.1 LSTM训练算法主要步骤


2)反向计算每个神经元的误差项值。与循环神经网络一样,LSTM误差项的反向传播也是包括两个方向:
一个是沿时间的反向传播,即从当前t时刻开始,计算每个时刻的误差项;
一个是将误差项向上一层传播。
3)根据相应的误差项,计算每个权重的梯度。
整个推导过程比较长,有兴趣的读者可参考这篇博文,这里只列出一些结果,过程就省略了。

13.3.3.2关于公式和符号的说明

为便于大家理解,预定义一些公式及运算符号等,这些公式或运算在推导一些结论时需要用到。

13.3.3.2误差项沿时间的反向传递

将误差项向前传递到任意k时刻的公式:

13.3.3.3将误差项传递到上一层

13.3.3.4权重梯度的计算

13.4 GRU网络

前面我们介绍了RNN的改进版LSTM,它有效克服了传统RNN的一些不足,事物总是向前发展的,LSTM也存在很多变体,在众多的LSTM变体中,GRU (Gated Recurrent Unit)也许是最成功的一种。它对LSTM做了很多简化,同时却保持着和LSTM相同的效果。因此,GRU最近变得越来越流行。
GRU对LSTM做了两个大改动:

GRU的前向计算公式为:

GRU的示意图:

13.5 Bi-LSTM网络

双向循环神经网络(Bidirectional Recurrent Neural Networks,Bi-RNN),Schuster、Paliwal,1997年首次提出,和LSTM同年。Bi-RNN,增加RNN可利用信息。普通MLP,数据长度有限制。RNN,可以处理不固定长度时序数据,无法利用历史输入未来信息。Bi-RNN,同时使用时序数据输入历史及未来数据,时序相反两个循环神经网络连接同一输出,输出层可以同时获取历史未来信息。
Language Modeling,不适合Bi-RNN,目标是通过前文预测下一单词,不能将下文信息传给模型。不过对分类问题,手写文字识别、机器翻译、蛋白结构预测等,采用Bi-RNN能提升模型效果。百度语音识别,通过Bi-RNN综合上下文语境,提升模型准确率。
双向循环神经网络(BRNN)的基本思想是提出每一个训练序列向前和向后分别是两个循环神经网络(RNN),而且这两个都连接着一个输出层。这个结构提供给输出层输入序列中每一个点的完整的过去和未来的上下文信息。下图展示的是一个沿着时间展开的双向循环神经网络。六个独特的权值在每一个时步被重复的利用,六个权值分别对应:输入到向前和向后隐含层(w1, w3),隐含层到隐含层自己(w2, w5),向前和向后隐含层到输出层(w4, w6)。值得注意的是:向前和向后隐含层之间没有信息流,这保证了展开图是非循环的。

13.6 一些优化方法

选择rmsprop优化算法
rmsprop优化算法背后的基本思想是根据先前梯度的和来调整学习率per-parameter。直观的 ,它意味着频繁出现的特征得到更小的学习率(因为它的梯度的和将会更大),稀有的特征得到更大的学习率; rmsprop的实现相当的简单;对于每个参数,有一个缓存变量。 在梯度下降时,我们更新参数和此变量;
cacheW = decay * cacheW + (1 - decay) * dW ** 2
W = W - learning_rate * dW / np.sqrt(cacheW + 1e-6)
衰退典型的被设置为0.9或0.95,1e-6 是为了避免0的出现;
加入嵌入层
使用例如word2vect和GloVe单词嵌入是一个流行的方法提高我们精度。代替使用one-hot vector来表达单词,使用word2vec或GloVe学习得到的携带语义的低维向量(形似的单词具有相似的向量),使用这些向量是训练前预处理的一种方式,直观的,能够告诉神经网络那些单词是相似的,以至于需要更少的学习语言;在你没有大量数据的时候,使用预训练向量很有效,因为它允许神经网络推广到没有见过的单词。我没有使用过预处理单词向量,但是加入一个嵌入层使它们更容易的插入进来;嵌入矩阵(E)就是一个查找表。第i列向量对应于我们的单词表中的第i个单词;
通过更新E进行单词的向量表示的学习更新;但是,这与我们的特殊任务相关,并不是可以下载使用大量文档寻训练的模型进行通用;
添加第二个GRU层
在神经网络中,加入第二层能够使我们的模型捕捉到更高水平相互作用;你能够加入额外的层;你将会发现在2-3层之后,结果会衰退,除非你拥有大量的数据,更多的层次不太可能造成大的差异,可能导致过拟合。

13.7 应用场景

上图中每一个矩形是一个向量,箭头则表示函数(比如矩阵相乘)。输入向量用红色标出,输出向量用蓝色标出,绿色的矩形是RNN的状态。从做到右:
(1)没有使用RNN的Vanilla模型,从固定大小的输入得到固定大小输出(比如图像分类)。

(2)序列输出(比如图片字幕,输入一张图片输出一段文字序列)。
(3)序列输入(比如情感分析,输入一段文字然后将它分类成积极或者消极情感)。
(4)序列输入和序列输出(比如机器翻译:一个RNN读取一条英文语句然后将它以法语形式输出)。
(5)同步序列输入输出(比如视频分类,对视频中每一帧打标签)。
我们注意到在每一个案例中,都没有对序列长度进行预先特定约束,因为递归变换(绿色部分)是固定的,而且我们可以多次使用。
正如你预想的那样,与使用固定计算步骤的固定网络相比,使用序列进行操作要更加强大,因此,这激起了我们建立对智能系统更大的兴趣。而且,我们可以从一小方面看出,RNNs将输入向量与状态向量用一个固定(但可以学习)函数绑定起来,从而用来产生一个新的状态向量。在编程层面,在运行一个程序时,可以用特定的输入和一些内部变量对其进行解释。从这个角度来看,RNNs本质上可以描述程序。事实上, RNNs是图灵完备的 ,即它们可以模拟任意程序(使用恰当的权值向量)。
13.8 LSTM实例之一(Python3实现)
前面我们介绍了LSTM的主要框架及原理,以及多种LSTM的改进模型,接下来我们通过一个实例来说明LSTM的具体实现。使用python3,后续我们将使用Tensorflow、Keras等实现。在下面的实现中,LSTMLayer的参数包括输入维度、输出维度、隐藏层维度,单元状态维度等于隐藏层维度。gate的激活函数为sigmoid函数,输出的激活函数为tanh。

13.8.1 LSTM实激活函数的实现

我们当然可以选择其他函数作为我们的误差函数,比如最小平方误差函数(MSE)。不过对概率进行建模时,选择交叉熵误差函数更make sense。

class SigmoidActivator(object):
def forward(self, weighted_input):
return 1.0 / (1.0 + np.exp(-weighted_input))
def backward(self, output):
return output * (1 - output)
class TanhActivator(object):
def forward(self, weighted_input):
return 2.0 / (1.0 + np.exp(-2 * weighted_input)) - 1.0
def backward(self, output):
return 1 - output * output

13.8.2 LSTM初始化

我们把LSTM的实现放在LstmLayer类中,根据LSTM前向计算和反向传播算法,我们需要初始化一系列矩阵和向量。这些矩阵和向量有两类用途,
一类是用于保存模型参数,例如:

另一类是保存各种中间计算结果,以便于反向传播算法使用,它们包括:

以及各个权重对应的梯度。
在构造函数的初始化中,只初始化了与forward计算相关的变量,与backward相关的变量没有初始化。

class LstmLayer(object):
def __init__(self, input_width, state_width,
learning_rate):
self.input_width = input_width
self.state_width = state_width
self.learning_rate = learning_rate
# 门的激活函数
self.gate_activator = SigmoidActivator()
# 输出的激活函数
self.output_activator = TanhActivator()
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()
# 遗忘门权重矩阵Wfh, Wfx, 偏置项bf
self.Wfh, self.Wfx, self.bf = (
self.init_weight_mat())
# 输入门权重矩阵Wfh, Wfx, 偏置项bf
self.Wih, self.Wix, self.bi = (
self.init_weight_mat())
# 输出门权重矩阵Wfh, Wfx, 偏置项bf
self.Woh, self.Wox, self.bo = (
self.init_weight_mat())
# 单元状态权重矩阵Wfh, Wfx, 偏置项bf
self.Wch, self.Wcx, self.bc = (
self.init_weight_mat())
def init_state_vec(self):
'''
初始化保存状态的向量
'''

state_vec_list = []
state_vec_list.append(np.zeros(
(self.state_width, 1)))
return state_vec_list
def init_weight_mat(self):
'''
初始化权重矩阵
'''

Wh = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.state_width))
Wx = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.input_width))
b = np.zeros((self.state_width, 1))
return Wh, Wx, b

13.8.3前向计算的实现

forward方法实现了LSTM的前向计算:

def forward(self, x):
'''
根据式1-式6进行前向计算
'''

self.times += 1
# 遗忘门
fg = self.calc_gate(x, self.Wfx, self.Wfh,
self.bf, self.gate_activator)
self.f_list.append(fg)
# 输入门
ig = self.calc_gate(x, self.Wix, self.Wih,
self.bi, self.gate_activator)
self.i_list.append(ig)
# 输出门
og = self.calc_gate(x, self.Wox, self.Woh,
self.bo, self.gate_activator)
self.o_list.append(og)
# 即时状态
ct = self.calc_gate(x, self.Wcx, self.Wch,
self.bc, self.output_activator)
self.ct_list.append(ct)
# 单元状态
c = fg * self.c_list[self.times - 1] + ig * ct
self.c_list.append(c)
# 输出
h = og * self.output_activator.forward(c)
self.h_list.append(h)
def calc_gate(self, x, Wx, Wh, b, activator):
'''
计算门
'''

h = self.h_list[self.times - 1] # 上次的LSTM输出
net = np.dot(Wh, h) + np.dot(Wx, x) + b
gate = activator.forward(net)
return gate

从上面的代码我们可以看到,门的计算都是相同的算法,而门和c ̃_t的计算仅仅是激活函数不同。因此我们提出了calc_gate方法,这样减少了很多重复代码

13.8.4反向传播算法的实现

backward方法实现了LSTM的反向传播算法。需要注意的是,与backword相关的内部状态变量是在调用backward方法之后才初始化的。这种延迟初始化的一个好处是,如果LSTM只是用来推理,那么就不需要初始化这些变量,节省了很多内存。

def backward(self, x, delta_h, activator):
'''
实现LSTM训练算法
'''

self.calc_delta(delta_h, activator)
self.calc_gradient(x)

算法主要分成两个部分,一部分使计算误差项:

def calc_delta(self, delta_h, activator):
# 初始化各个时刻的误差项
self.delta_h_list = self.init_delta() # 输出误差项
self.delta_o_list = self.init_delta() # 输出门误差项
self.delta_i_list = self.init_delta() # 输入门误差项
self.delta_f_list = self.init_delta() # 遗忘门误差项
self.delta_ct_list = self.init_delta() # 即时输出误差项
# 保存从上一层传递下来的当前时刻的误差项
self.delta_h_list[-1] = delta_h
# 迭代计算每个时刻的误差项
for k in range(self.times, 0, -1):
self.calc_delta_k(k)
def init_delta(self):
'''
初始化误差项
'''

delta_list = []
for i in range(self.times + 1):
delta_list.append(np.zeros(
(self.state_width, 1)))
return delta_list
def calc_delta_k(self, k):
'''
根据k时刻的delta_h,计算k时刻的delta_f、
delta_i、delta_o、delta_ct,以及k-1时刻的delta_h
'''

# 获得k时刻前向计算的值
ig = self.i_list[k]
og = self.o_list[k]
fg = self.f_list[k]
ct = self.ct_list[k]
c = self.c_list[k]
c_prev = self.c_list[k-1]
tanh_c = self.output_activator.forward(c)
delta_k = self.delta_h_list[k]
# 根据式9计算delta_o
delta_o = (delta_k * tanh_c *
self.gate_activator.backward(og))
delta_f = (delta_k * og *
(1 - tanh_c * tanh_c) * c_prev *
self.gate_activator.backward(fg))
delta_i = (delta_k * og *
(1 - tanh_c * tanh_c) * ct *
self.gate_activator.backward(ig))
delta_ct = (delta_k * og *
(1 - tanh_c * tanh_c) * ig *
self.output_activator.backward(ct))
delta_h_prev = (
np.dot(delta_o.transpose(), self.Woh) +
np.dot(delta_i.transpose(), self.Wih) +
np.dot(delta_f.transpose(), self.Wfh) +
np.dot(delta_ct.transpose(), self.Wch)
).transpose()
# 保存全部delta值
self.delta_h_list[k-1] = delta_h_prev
self.delta_f_list[k] = delta_f
self.delta_i_list[k] = delta_i
self.delta_o_list[k] = delta_o
self.delta_ct_list[k] = delta_ct

另一部分是计算梯度:

def calc_gradient(self, x):
# 初始化遗忘门权重梯度矩阵和偏置项
self.Wfh_grad, self.Wfx_grad, self.bf_grad = (
self.init_weight_gradient_mat())
# 初始化输入门权重梯度矩阵和偏置项
self.Wih_grad, self.Wix_grad, self.bi_grad = (
self.init_weight_gradient_mat())
# 初始化输出门权重梯度矩阵和偏置项
self.Woh_grad, self.Wox_grad, self.bo_grad = (
self.init_weight_gradient_mat())
# 初始化单元状态权重梯度矩阵和偏置项
self.Wch_grad, self.Wcx_grad, self.bc_grad = (
self.init_weight_gradient_mat())
# 计算对上一次输出h的权重梯度
for t in range(self.times, 0, -1):
# 计算各个时刻的梯度
(Wfh_grad, bf_grad,
Wih_grad, bi_grad,
Woh_grad, bo_grad,
Wch_grad, bc_grad) = (
self.calc_gradient_t(t))
# 实际梯度是各时刻梯度之和
self.Wfh_grad += Wfh_grad
self.bf_grad += bf_grad
self.Wih_grad += Wih_grad
self.bi_grad += bi_grad
self.Woh_grad += Woh_grad
self.bo_grad += bo_grad
self.Wch_grad += Wch_grad
self.bc_grad += bc_grad
print( '-----%d-----' % t)
print(Wfh_grad)
print(self.Wfh_grad)
# 计算对本次输入x的权重梯度
xt = x.transpose()
self.Wfx_grad = np.dot(self.delta_f_list[-1], xt)
self.Wix_grad = np.dot(self.delta_i_list[-1], xt)
self.Wox_grad = np.dot(self.delta_o_list[-1], xt)
self.Wcx_grad = np.dot(self.delta_ct_list[-1], xt)
def init_weight_gradient_mat(self):
'''
初始化权重矩阵
'''

Wh_grad = np.zeros((self.state_width,
self.state_width))
Wx_grad = np.zeros((self.state_width,
self.input_width))
b_grad = np.zeros((self.state_width, 1))
return Wh_grad, Wx_grad, b_grad
def calc_gradient_t(self, t):
'''
计算每个时刻t权重的梯度
'''

h_prev = self.h_list[t-1].transpose()
Wfh_grad = np.dot(self.delta_f_list[t], h_prev)
bf_grad = self.delta_f_list[t]
Wih_grad = np.dot(self.delta_i_list[t], h_prev)
bi_grad = self.delta_f_list[t]
Woh_grad = np.dot(self.delta_o_list[t], h_prev)
bo_grad = self.delta_f_list[t]
Wch_grad = np.dot(self.delta_ct_list[t], h_prev)
bc_grad = self.delta_ct_list[t]
return Wfh_grad, bf_grad, Wih_grad, bi_grad, \
Woh_grad, bo_grad, Wch_grad, bc_grad

13.8.5梯度下降算法的实现

下面是用梯度下降算法来更新权重

def update(self):
'''
按照梯度下降,更新权重
'''

self.Wfh -= self.learning_rate * self.Whf_grad
self.Wfx -= self.learning_rate * self.Whx_grad
self.bf -= self.learning_rate * self.bf_grad
self.Wih -= self.learning_rate * self.Whi_grad
self.Wix -= self.learning_rate * self.Whi_grad
self.bi -= self.learning_rate * self.bi_grad
self.Woh -= self.learning_rate * self.Wof_grad
self.Wox -= self.learning_rate * self.Wox_grad
self.bo -= self.learning_rate * self.bo_grad
self.Wch -= self.learning_rate * self.Wcf_grad
self.Wcx -= self.learning_rate * self.Wcx_grad
self.bc -= self.learning_rate * self.bc_grad

13.8.6梯度检查的实现

和RecurrentLayer一样,为了支持梯度检查,我们需要支持重置内部状态。

def reset_state(self):
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()

梯度检查的代码:

def data_set():
x = [np.array([[1], [2], [3]]),
np.array([[2], [3], [4]])]
d = np.array([[1], [2]])
return x, d
def gradient_check():
'''
梯度检查
'''

# 设计一个误差函数,取所有节点输出项之和
error_function = lambda o: o.sum()
lstm = LstmLayer(3, 2, 1e-3)
# 计算forward值
x, d = data_set()
lstm.forward(x[0])
lstm.forward(x[1])
# 求取sensitivity map
sensitivity_array = np.ones(lstm.h_list[-1].shape,
dtype=np.float64)
# 计算梯度
lstm.backward(x[1], sensitivity_array, IdentityActivator())
# 检查梯度
epsilon = 10e-4
for i in range(lstm.Wfh.shape[0]):
for j in range(lstm.Wfh.shape[1]):
lstm.Wfh[i,j] += epsilon
lstm.reset_state()
lstm.forward(x[0])
lstm.forward(x[1])
err1 = error_function(lstm.h_list[-1])
lstm.Wfh[i,j] -= 2*epsilon
lstm.reset_state()
lstm.forward(x[0])
lstm.forward(x[1])
err2 = error_function(lstm.h_list[-1])
expect_grad = (err1 - err2) / (2 * epsilon)
lstm.Wfh[i,j] += epsilon
print( 'weights(%d,%d): expected - actural %.4e - %.4e' % (
i, j, expect_grad, lstm.Wfh_grad[i,j]))
return lstm

13.8.7 运行

为便于运行,需要把一些函数,如forward、calc_gate、backward、calc_gradient等整合到LstmLayer类中
具体如下:

class LstmLayer(object):
def __init__(self, input_width, state_width,
learning_rate):
self.input_width = input_width
self.state_width = state_width
self.learning_rate = learning_rate
# 门的激活函数
self.gate_activator = SigmoidActivator()
# 输出的激活函数
self.output_activator = TanhActivator()
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()
# 遗忘门权重矩阵Wfh, Wfx, 偏置项bf
self.Wfh, self.Wfx, self.bf = (
self.init_weight_mat())
# 输入门权重矩阵Wfh, Wfx, 偏置项bf
self.Wih, self.Wix, self.bi = (
self.init_weight_mat())
# 输出门权重矩阵Wfh, Wfx, 偏置项bf
self.Woh, self.Wox, self.bo = (
self.init_weight_mat())
# 单元状态权重矩阵Wfh, Wfx, 偏置项bf
self.Wch, self.Wcx, self.bc = (
self.init_weight_mat())
def init_state_vec(self):
'''
初始化保存状态的向量
'''

state_vec_list = []
state_vec_list.append(np.zeros(
(self.state_width, 1)))
return state_vec_list
def init_weight_mat(self):
'''
初始化权重矩阵
'''

Wh = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.state_width))
Wx = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.input_width))
b = np.zeros((self.state_width, 1))
return Wh, Wx, b
def forward(self, x):
'''
根据式1-式6进行前向计算
'''

self.times += 1
# 遗忘门
fg = self.calc_gate(x, self.Wfx, self.Wfh,
self.bf, self.gate_activator)
self.f_list.append(fg)
# 输入门
ig = self.calc_gate(x, self.Wix, self.Wih,
self.bi, self.gate_activator)
self.i_list.append(ig)
# 输出门
og = self.calc_gate(x, self.Wox, self.Woh,
self.bo, self.gate_activator)
self.o_list.append(og)
# 即时状态
ct = self.calc_gate(x, self.Wcx, self.Wch,
self.bc, self.output_activator)
self.ct_list.append(ct)
# 单元状态
c = fg * self.c_list[self.times - 1] + ig * ct
self.c_list.append(c)
# 输出
h = og * self.output_activator.forward(c)
self.h_list.append(h)
def calc_gate(self, x, Wx, Wh, b, activator):
'''
计算门
'''

h = self.h_list[self.times - 1] # 上次的LSTM输出
net = np.dot(Wh, h) + np.dot(Wx, x) + b
gate = activator.forward(net)
return gate
def backward(self, x, delta_h, activator):
'''
实现LSTM训练算法
'''

self.calc_delta(delta_h, activator)
self.calc_gradient(x)
def calc_delta(self, delta_h, activator):
# 初始化各个时刻的误差项
self.delta_h_list = self.init_delta() # 输出误差项
self.delta_o_list = self.init_delta() # 输出门误差项
self.delta_i_list = self.init_delta() # 输入门误差项
self.delta_f_list = self.init_delta() # 遗忘门误差项
self.delta_ct_list = self.init_delta() # 即时输出误差项
# 保存从上一层传递下来的当前时刻的误差项
self.delta_h_list[-1] = delta_h
# 迭代计算每个时刻的误差项
for k in range(self.times, 0, -1):
self.calc_delta_k(k)
def init_delta(self):
'''
初始化误差项
'''

delta_list = []
for i in range(self.times + 1):
delta_list.append(np.zeros(
(self.state_width, 1)))
return delta_list
def calc_delta_k(self, k):
'''
根据k时刻的delta_h,计算k时刻的delta_f、
delta_i、delta_o、delta_ct,以及k-1时刻的delta_h
'''

# 获得k时刻前向计算的值
ig = self.i_list[k]
og = self.o_list[k]
fg = self.f_list[k]
ct = self.ct_list[k]
c = self.c_list[k]
c_prev = self.c_list[k-1]
tanh_c = self.output_activator.forward(c)
delta_k = self.delta_h_list[k]
# 根据式9计算delta_o
delta_o = (delta_k * tanh_c *
self.gate_activator.backward(og))
delta_f = (delta_k * og *
(1 - tanh_c * tanh_c) * c_prev *
self.gate_activator.backward(fg))
delta_i = (delta_k * og *
(1 - tanh_c * tanh_c) * ct *
self.gate_activator.backward(ig))
delta_ct = (delta_k * og *
(1 - tanh_c * tanh_c) * ig *
self.output_activator.backward(ct))
delta_h_prev = (
np.dot(delta_o.transpose(), self.Woh) +
np.dot(delta_i.transpose(), self.Wih) +
np.dot(delta_f.transpose(), self.Wfh) +
np.dot(delta_ct.transpose(), self.Wch)
).transpose()
# 保存全部delta值
self.delta_h_list[k-1] = delta_h_prev
self.delta_f_list[k] = delta_f
self.delta_i_list[k] = delta_i
self.delta_o_list[k] = delta_o
self.delta_ct_list[k] = delta_ct
def calc_gradient(self, x):
# 初始化遗忘门权重梯度矩阵和偏置项
self.Wfh_grad, self.Wfx_grad, self.bf_grad = (
self.init_weight_gradient_mat())
# 初始化输入门权重梯度矩阵和偏置项
self.Wih_grad, self.Wix_grad, self.bi_grad = (
self.init_weight_gradient_mat())
# 初始化输出门权重梯度矩阵和偏置项
self.Woh_grad, self.Wox_grad, self.bo_grad = (
self.init_weight_gradient_mat())
# 初始化单元状态权重梯度矩阵和偏置项
self.Wch_grad, self.Wcx_grad, self.bc_grad = (
self.init_weight_gradient_mat())
# 计算对上一次输出h的权重梯度
for t in range(self.times, 0, -1):
# 计算各个时刻的梯度
(Wfh_grad, bf_grad,
Wih_grad, bi_grad,
Woh_grad, bo_grad,
Wch_grad, bc_grad) = (
self.calc_gradient_t(t))
# 实际梯度是各时刻梯度之和
self.Wfh_grad += Wfh_grad
self.bf_grad += bf_grad
self.Wih_grad += Wih_grad
self.bi_grad += bi_grad
self.Woh_grad += Woh_grad
self.bo_grad += bo_grad
self.Wch_grad += Wch_grad
self.bc_grad += bc_grad
print( '-----%d-----' % t)
print(Wfh_grad)
print(self.Wfh_grad)
# 计算对本次输入x的权重梯度
xt = x.transpose()
self.Wfx_grad = np.dot(self.delta_f_list[-1], xt)
self.Wix_grad = np.dot(self.delta_i_list[-1], xt)
self.Wox_grad = np.dot(self.delta_o_list[-1], xt)
self.Wcx_grad = np.dot(self.delta_ct_list[-1], xt)
def init_weight_gradient_mat(self):
'''
初始化权重矩阵
'''

Wh_grad = np.zeros((self.state_width,
self.state_width))
Wx_grad = np.zeros((self.state_width,
self.input_width))
b_grad = np.zeros((self.state_width, 1))
return Wh_grad, Wx_grad, b_grad
def calc_gradient_t(self, t):
'''
计算每个时刻t权重的梯度
'''

h_prev = self.h_list[t-1].transpose()
Wfh_grad = np.dot(self.delta_f_list[t], h_prev)
bf_grad = self.delta_f_list[t]
Wih_grad = np.dot(self.delta_i_list[t], h_prev)
bi_grad = self.delta_f_list[t]
Woh_grad = np.dot(self.delta_o_list[t], h_prev)
bo_grad = self.delta_f_list[t]
Wch_grad = np.dot(self.delta_ct_list[t], h_prev)
bc_grad = self.delta_ct_list[t]
return Wfh_grad, bf_grad, Wih_grad, bi_grad, \
Woh_grad, bo_grad, Wch_grad, bc_grad
def reset_state(self):
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()

运行检查梯度函数
gradient_check()
运行结果如下:

-----2-----
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
-----1-----
[[ 0. 0.]
[ 0. 0.]]
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
weights(0,0): expected - actural 3.5432e-10 - 3.5429e-10
weights(0,1): expected - actural 9.6844e-11 - 9.6806e-11
weights(1,0): expected - actural 9.6801e-11 - 9.6801e-11
weights(1,1): expected - actural 2.6420e-11 - 2.6450e-11

参考:https://zybuluo.com/hanbingtao/note/541458

电信客户流失分析

第5 章 用Sklearn预测客户流失

“流失率”是描述客户离开或停止支付产品或服务费率的业务术语。这在许多企业中是一个关键的数字,因为通常情况下,获取新客户的成本比保留现有成本(在某些情况下,贵5到20倍)。
因此,了解保持客户参与度是非常宝贵的,因为它是开发保留策略和推出旨在阻止客户流失的重要基础。因此,公司对开发更好的流失检测技术愈来愈有兴趣,导致许多人寻求数据挖掘和机器学习以获得新的和创造性的方法。

5.1 导入数据集

这里我们以一个长期的电信客户数据作为数据集,您可以点击这里下载数据集。
数据很简单。 每行代表一个预订的电话用户。 每列包含客户属性,例如电话号码,在一天中不同时间使用的通话分钟,服务产生的费用,生命周期帐户持续时间等。
使用pandas方便直接读取.csv里数据

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import json

from sklearn.cross_validation import KFold
from sklearn.preprocessing import StandardScaler
from sklearn.cross_validation import train_test_split
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
%matplotlib inline

churn_df = pd.read_csv('./data/customer_loss/churn.csv')
col_names = churn_df.columns.tolist()

print(col_names)

to_show = col_names[:3] + col_names[-6:]
churn_df[to_show].head(4)

显示结果如下:

5.2 预处理数据集

删除不相关的列,并将字符串转换为布尔值(因为模型不处理“yes”和“no”非常好)。 其余的数字列保持不变。
将预测结果分离转化为0,1形式,把False转换为,把True转换为1。

churn_result = churn_df['Churn?']
y = np.where(churn_result == 'True.',1,0)
y[:20]
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0])

删除一些不必要的特征或字段,这里是从业务角度进行取舍。

to_drop = ['State','Area Code','Phone','Churn?','Unnamed: 21']
churn_feat_space = churn_df.drop(to_drop,axis=1)

将属性值yes,no等转化为boolean values,即False或True。numpy将把这些值转换为1或0.

yes_no_cols = ["Int'l Plan","VMail Plan"]
churn_feat_space[yes_no_cols] = churn_feat_space[yes_no_cols] == 'yes'

获取新的属性和属性值。

features = churn_feat_space.columns
X = churn_feat_space.as_matrix().astype(np.float)

对规模级别差距较大的数据,需要进行规范化处理。 例如:篮球队每场比赛得分的分数自然比他们的胜率要大几个数量级。 但这并不意味着后者的重要性低100倍,故需要进行标准化处理。
公式为:(X-mean)/std 计算时对每个属性/每列分别进行。
将数据按期属性(按列进行)减去其均值,并处以其方差。得到的结果是,对于每个属性/每列来说所有数据都聚集在-1到1附近,方差为1。这里直接使用sklearn库中数据预处理模块:StandardScaler

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)

打印出数据数量和属性个数及分类label。

print("Feature space holds %d observations and %d features" % X.shape)
print("Unique target labels:", np.unique(y))
Feature space holds 3332 observations and 17 features
Unique target labels: [0 1]

5.3 使用交叉验证方法

模型的性能如何?我们可通过哪些方法来验证或检验一个模型的优劣?交叉验证是一种有效方法。此外,利用交叉验证尝试避免过拟合(对同一数据点进行训练和预测),同时仍然为每个观测数据集产生预测。
交叉验证(Cross Validation)的基本思想是把在某种意义下将原始数据(dataset)进行分组,一部分做为训练集(train set),另一部分做为验证集(validation set or test set),首先用训练集对分类器进行训练,再利用验证集来测试训练得到的模型(model),以此来做为评价分类器的性能指标。
另外这里顺便介绍一下使用scikit-learn库的常用算法,如下图,接下来我们将使用多种方法来构建模型,然后比较各种算法的性能。

from sklearn.cross_validation import KFold
def run_cv(X,y,clf_class,**kwargs):
# Construct a kfolds object
kf = KFold(len(y),n_folds=5,shuffle=True)
y_pred = y.copy()
# Iterate through folds
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
# Initialize a classifier with key word arguments
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
y_pred[test_index] = clf.predict(X_test)
return y_pred

5.4 训练模型

比较三个相当独特的算法:支持向量机,集成方法,随机森林和k最近邻。 然后,显示各分类器预测正确率。

from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.linear_model import LogisticRegression as LR
from sklearn.ensemble import GradientBoostingClassifier as GBC
from sklearn.metrics import average_precision_score

def accuracy(y_true,y_pred):
# NumPy interpretes True and False as 1. and 0.
return np.mean(y_true == y_pred)

print( "Logistic Regression:")
print( "%.3f" % accuracy(y, run_cv(X,y,LR)))
print( "Gradient Boosting Classifier")
print( "%.3f" % accuracy(y, run_cv(X,y,GBC)))
print( "Support vector machines:")
print( "%.3f" % accuracy(y, run_cv(X,y,SVC)))
print( "Random forest:")
print( "%.3f" % accuracy(y, run_cv(X,y,RF)))
print( "K-nearest-neighbors:")
print( "%.3f" % accuracy(y, run_cv(X,y,KNN)))

Logistic Regression:
0.862
Gradient Boosting Classifier
0.951
Support vector machines:
0.922
Random forest:
0.942
K-nearest-neighbors:
0.893

可以看出Gradient Boosting、Random forest性能较好。

5.5 精确率和召回率

评估模型有很多方法,对分类问题,尤其是二分类问题通常采用准确率和召回率来衡量。这里我们将使用scikit-learn一个内置的函数来构造混淆矩阵。该矩阵反应模型的这些指标。混淆矩阵是一种可视化由分类器进行的预测的方式,并且仅仅是一个表格,其示出了对于特定类的预测的分布。 x轴表示每个观察的真实类别(如果客户流失或不流失),而y轴对应于模型预测的类别(如果我的分类器表示客户会流失或不流失)。这里补充一下有关混淆矩阵的有关定义,供大家参考。
二元分类的混淆矩阵形式如下:
True Positive(真正,TP):将正类预测为正类数;
True Negative(真负,TN):将负类预测为负类数;
False Positive(假正,FP):将负类预测为正类数误报 (Type I error);
False Negative(假负,FN):将正类预测为负类数→漏报 (Type II error)


混淆矩阵的缺点:
一些positive事件发生概率极小的不平衡数据集(imbalanced data),混淆矩阵可能效果不好。比如对信用卡交易是否异常做分类的情形,很可能1万笔交易中只有1笔交易是异常的。一个将所有交易都判定为正常的分类器,准确率是99.99%。这个数字虽然很高,但是没有任何现实意义。

from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score

def draw_confusion_matrices(confusion_matricies,class_names):
class_names = class_names.tolist()
for cm in confusion_matrices:
classifier, cm = cm[0], cm[1]
print(cm)

fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(cm)
plt.title('Confusion matrix for %s' % classifier)
fig.colorbar(cax)
ax.set_xticklabels([''] + class_names)
ax.set_yticklabels([''] + class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()

y = np.array(y)
class_names = np.unique(y)

confusion_matrices = [
( "Support Vector Machines", confusion_matrix(y,run_cv(X,y,SVC)) ),
( "Random Forest", confusion_matrix(y,run_cv(X,y,RF)) ),
( "K-Nearest-Neighbors", confusion_matrix(y,run_cv(X,y,KNN)) ),
( "Gradient Boosting Classifier", confusion_matrix(y,run_cv(X,y,GBC)) ),
( "Logisitic Regression", confusion_matrix(y,run_cv(X,y,LR)) )
]

# Pyplot code not included to reduce clutter
# from churn_display import draw_confusion_matrices
%matplotlib inline

draw_confusion_matrices(confusion_matrices,class_names)

5.6 ROC & AUC

这里我们简单介绍一下这两个概念。
ROC曲线是根据一系列不同的二分类方式(分界值或决定阈),以真阳性率(灵敏度)为纵坐标,假阳性率(1-特异度)为横坐标绘制的曲线。传统的诊断试验评价方法有一个共同的特点,必须将试验结果分为两类,再进行统计分析。ROC曲线的评价方法与传统的评价方法不同,无须此限制,而是根据实际情况,允许有中间状态,可以把试验结果划分为多个有序分类,如正常、大致正常、可疑、大致异常和异常五个等级再进行统计分析。因此,ROC曲线评价方法适用的范围更为广泛
AUC值为ROC曲线所覆盖的区域面积,显然,AUC越大,分类器分类效果越好。

from sklearn.metrics import roc_curve, auc
from scipy import interp

def plot_roc(X, y, clf_class, **kwargs):
kf = KFold(len(y), n_folds=5, shuffle=True)
y_prob = np.zeros((len(y),2))
mean_tpr = 0.0
mean_fpr = np.linspace(0, 1, 100)
all_tpr = []
for i, (train_index, test_index) in enumerate(kf):
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test)
fpr, tpr, thresholds = roc_curve(y[test_index], y_prob[test_index, 1])
mean_tpr += interp(mean_fpr, fpr, tpr)
mean_tpr[0] = 0.0
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, lw=1, label='ROC fold %d (area = %0.2f)' % (i, roc_auc))
mean_tpr /= len(kf)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)
plt.plot(mean_fpr, mean_tpr, 'k--',label='Mean ROC (area = %0.2f)' % mean_auc, lw=2)

plt.plot([0, 1], [0, 1], '--', color=(0.6, 0.6, 0.6), label='Random')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.show()

print("Support vector machines:")
print(plot_roc(X,y,SVC,probability=True))

print("Random forests:")
print(plot_roc(X,y,RF,n_estimators=18))

print("K-nearest-neighbors:")
print(plot_roc(X,y,KNN))

print("Gradient Boosting Classifier:")
print(plot_roc(X,y,GBC))

5.7 特征分析

我们想进一步分析,流失客户有哪些比较明显的特征。或哪些特征对客户流失影响比较大。

train_index,test_index = train_test_split(churn_df.index)

forest = RF()
forest_fit = forest.fit(X[train_index], y[train_index])
forest_predictions = forest_fit.predict(X[test_index])

importances = forest_fit.feature_importances_[:10]
std = np.std([tree.feature_importances_ for tree in forest.estimators_],
axis=0)
indices = np.argsort(importances)[::-1]

# Print the feature ranking
print("Feature ranking:")

for f in range(10):
#print("%d. %s (%f)" % (f + 1, features[f], importances[indices[f]]))
print("%d. %s (%f)" % (f + 1, features[f], importances[f]))
# Plot the feature importances of the forest
#import pylab as pl
plt.figure()
plt.title("Feature importances")
plt.bar(range(10), importances[indices], yerr=std[indices], color="r", align="center")
plt.xticks(range(10), indices)
plt.xlim([-1, 10])
plt.show()

Feature ranking:
1. Account Length (0.026546)
2. Int'l Plan (0.054757)
3. VMail Plan (0.020334)
4. VMail Message (0.041116)
5. Day Mins (0.164627)
6. Day Calls (0.026826)
7. Day Charge (0.120162)
8. Eve Mins (0.086248)
9. Eve Calls (0.022896)
10. Eve Charge (0.046945)

<img src="http://www.feiguyunai.com/wp-content/uploads/2017/11/3620ceddf163b3f60d0ba6544c5098ca.png" alt="" />
def run_prob_cv(X, y, clf_class, roc=False, **kwargs):
kf = KFold(len(y), n_folds=5, shuffle=True)
y_prob = np.zeros((len(y),2))
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test)
return y_prob
import warnings
warnings.filterwarnings('ignore')

# Use 10 estimators so predictions are all multiples of 0.1
pred_prob = run_prob_cv(X, y, RF, n_estimators=10)
pred_churn = pred_prob[:,1]
is_churn = y == 1

# Number of times a predicted probability is assigned to an observation
counts = pd.value_counts(pred_churn)
counts[:]

0.0 1782
0.1 666
0.2 276
0.3 123
0.9 89
0.4 85
0.7 73
0.8 65
1.0 63
0.6 61
0.5 49
dtype: int64

from collections import defaultdict
true_prob = defaultdict(float)

# calculate true probabilities
for prob in counts.index:
true_prob[prob] = np.mean(is_churn[pred_churn == prob])
true_prob = pd.Series(true_prob)

# pandas-fu
counts = pd.concat([counts,true_prob], axis=1).reset_index()
counts.columns = ['pred_prob', 'count', 'true_prob']
counts


我们可以看到,随机森林预测89个人将有0.9的可能性的流失,而在现实中,该群体具有大约0.98的流失率。

参考:
http://www.sohu.com/a/149724745_99906660
http://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/analyses/churn.ipynb

轻松玩转--卷积神经网络(CNN)

第12章 TensorFlow卷积神经网络

12.1 卷积神经网络简介

卷积神经网路(Convolutional Neural Network, CNN)是一种前馈神经网络,对于CNN最早可以追溯到1986年BP算法的提出,1989年LeCun将其用到多层神经网络中,直到1998年LeCun提出LeNet-5模型,神经网络的雏形基本形成。在接下来近十年的时间里,卷积神经网络的相关研究处于低谷,原因有两个:一是研究人员意识到多层神经网络在进行BP训练时的计算量极其之大,当时的硬件计算能力完全不可能实现;二是包括SVM在内的浅层机器学习算法也渐渐开始暂露头脚。
2006年,Hinton终于一鸣惊人,在《科学》上发表文章,CNN再度觉醒,并取得长足发展。2012年,ImageNet大赛上CNN夺冠,2014年,谷歌研发出20层的VGG模型。同年,DeepFace、DeepID模型横空出世,直接将LFW数据库上的人脸识别、人脸认证的正确率刷到99.75%,几乎超越人类。
卷积神经网路由一个或多个卷积层和顶端的全连通层(对应经典的神经网路)组成,同时也包括关联权重和池化层(pooling layer)。这一结构使得卷积神经网路能够利用输入数据的二维结构。与其他深度学习结构相比,卷积神经网路在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网路,卷积神经网路需要考量的参数更少,使之成为一种颇具吸引力的深度学习结构。

12.2卷积的定义

卷积现在可能是深度学习中最重要的概念。正是靠着卷积和卷积神经网络,深度学习才超越了几乎其他所有的机器学习方法。要理解卷积神经网络,首先需要了解卷积的含义,卷积的定义有点抽象,我们先给出一个数学上的定义,然后通过一个简单例子及其物理意义来帮助大家理解。
卷积(Convolution)是一种数学运算,它是通过两个函数f和g生成第三个函数的一种运算或数学操作。卷积的数据定义为:
其离散定义为:

通常把函数g称为输入函数,函数f称为滤波器(Filter),或卷积核(kernel),得到的结果h为特征图或特征映射(feature map)。
数学上的定义有点抽象,下面我通过几个示例来帮助大家理解。
1)小李定期存款示例
小李存入100元钱,年利率是4%,按本息计算(即将每一年所获利息加入本金,以计算下一年的利息),那么在五年之后他能拿到的钱数是:

以此类推,如果小李每年都往银行中存入新的100元钱,那么这个收益表格将是这样的:

在上式中,f(t)为小李的存钱函数,而g(t)为存入银行的每一笔钱的本息计算函数。在这里,小李最终得到的钱就是他的存钱函数和本息计算函数的卷积。
如果我们把这个公式推广到连续的情况,也就是说,小李在从0到x的这一段时间内,每时每刻都往银行里存钱,他的存钱函数为
f(t) (0≤t≤x)
而银行也对他存入的每一笔钱按本息公式计算收益:

通过上面这个例子,大家应该能够很清晰地记住卷积公式了。
如果我们将小李的存款函数视为一个信号发生(也就是激励)的过程,而将本息函数
g(x-t)
视为一个系统对信号的响应函数(也就是响应),那么二者的卷积
(f*g)(x)
就可以看做是在x时刻对系统进行观察,得到的观察结果(也就是输出)将是过去产生的所有信号经过系统的处理后得到的结果的叠加或加权平均,这也就是卷积的物理意义了。
参考了:https://www.zhihu.com/question/21686447
2)两个方形脉冲波的卷积示例
下图为两个方形脉冲波的卷积。其中函数"g"首先对τ=0反射,接着平移"t",成为 g(t-τ)。那么重叠部分的面积就相当于"t"处的卷积,其中横坐标代表待变量τ 以及新函数f*g的自变量"t"。
Convolution_of_box_signal_with_itself2
图1 两个方形脉冲的卷积

下图示方形脉冲波和指数衰退的脉冲波的卷积(后者可能出现于RC电路中),同样地重叠部分面积就相当于"t"处的卷积。注意到因为"g"是对称的,所以在这两张图中,反射并不会改变它的形状。

Convolution_of_spiky_function
图2 一个方形与指数衰退的脉冲波的卷积

该图取自:
https://zh.wikipedia.org/wiki/%E5%8D%B7%E7%A7%AF

12.3卷积运算

上节我们通过一些简单示例介绍了卷积的定义,卷积本质上是一个线性运算,因此卷积操作也被称为线性滤波。卷积如何运算?为提供一个直观印象,我们先看一个简单的二维空间卷积运算示例:

这个二维空间的卷积运算对两个输入张量(即输入和卷积核)进行卷积,并把结果输出。
接下来再看一个二维卷积详细运算过程,以便于大家对卷积运算有个更全面的了解。
saddle_point_evaluation_optimizers

图3 卷积运算
该图取自:https://www.zybuluo.com/hanbingtao/note/485480
在以上计算中,输入为一个5x5的矩阵,移动窗口为3x3矩阵,又称为卷积矩阵或卷积核,这个矩阵值为共享参数,移动步长或步幅(stride)为1,当然这个值可以大于1,如2或3等。小窗口移动过程中,其中的值是保持不变的:

图4 卷积核
这就是参数共享的说法,这些参数就决定一个卷积核,每个卷积核能够提取某一部分的特征。一般情况下,只有一个卷积核是不够的,在实际处理中,往往通过多个卷积核来提取多重特征,每一个卷积核与原始输入数据执行卷积操作后得到一个特征映射,如下图:

上图中,最右边,特征映射矩阵中的4,具体计算公式如下:

上图中,最右边特征映射矩阵中的3,具体计算公式如下:

其他以此类推,最后输出结果为:

图5 输出结果
以上介绍了一个输入,通过一个卷积核后,得到一个特征映射。如果输入有多个,卷积核也有多个情况又如何呢?另外,步幅也可以是大于1的整数,如为2,如果原来的输入为5*5的矩阵,卷积核还是3*3的矩阵,那么卷积核小窗口按步幅2从左到右,从上到下移动过程中,可能导致卷积核窗口部分在输入矩阵之外,如下图:

当卷积窗口在3这个位置时,它只覆盖输入矩阵的一列,这个时候该如何处理呢?为防止信息丢失,我们采用边界外面补0(Zero padding)策略。下面的动画显示了包含两个卷积核的卷积层的计算。我们可以看到7*7*3输入,经过两个3*3*3的卷积(步幅为2),得到了3*3*2的输出。另外我们也会看到下图的Zero padding是1,也就是在输入元素的周围补了一圈0。Zero padding对于图像边缘部分的特征提取是很有帮助的,可以防止信息丢失。

图6 多个输入及卷积核的卷积计算

以上就是卷积层的计算方法。这里面体现了局部连接和权值共享:每层神经元只和上一层部分神经元相连(卷积计算规则),且卷积核的权值对于上一层所有神经元都是一样的。对于包含两个3*3*3的卷积核的卷积层来说,其参数数量仅有(3*3*3+1)*2=56个,且参数数量与上一层神经元个数无关。与全连接神经网络相比,其参数数量大大减少了。

这是通常的卷积运算,此外,还有多种卷积运算,如空洞卷积、分离卷积等等,这里我们介绍一下空洞卷积(atrous convolutions),空洞卷积又称为扩张卷积(dilated convolutions),向卷积层引入了一个称为 “扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时各值的间距。

卷积核为3、扩张率为2和无边界扩充的二维空洞卷积
一个扩张率为2的3×3卷积核,感受野与5×5的卷积核相同,而且仅需要9个参数。你可以把它想象成一个5×5的卷积核,每隔一行或一列删除一行或一列。
在相同的计算条件下,空洞卷积提供了更大的感受野。空洞卷积经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑空洞卷积。

卷积核,从这个名字可以看出它的重要性,它是整个卷积过程的核心。比较简单的卷积核或过滤器有Horizontalfilter、Verticalfilter、Sobel filter等。这些过滤器能够检测图像的水平边缘、垂直边缘、增强图片中心区域权重等。

12.4 卷积网络结构

卷积神经网络到底啥样?为获取一个感性认识,我们先看一下卷积神经网络的示意图。

图7 卷积神经网络
由图7可知,一个卷积神经网络由若干卷积层(Convolution)、池化层(Pooling)、全连接层(fully connected)组成。
从图7我们可以发现卷积神经网络的层结构和全连接神经网络的层结构有很大不同。全连接神经网络每层的神经元是按照一维排列的,也就是排成一条线的样子;而卷积神经网络每层的神经元是按照三维排列的,也就是排成一个长方体的样子,有宽度、高度和深度。
对于图7展示的神经网络,我们看到输入层的宽度和高度对应于输入图像的宽度和高度,而它的深度为1。接着,第一个卷积层对这幅图像进行了卷积操作,得到了三个特征映射。这里的"3"可能是让很多初学者迷惑的地方,实际上,就是这个卷积层包含三个卷积核,也就是三套参数(或共享参数),每个卷积核都可以把原始输入图像卷积得到一个特征映射,三个卷积核就可以得到三个特征映射。至于一个卷积层可以有多少个卷积核,那是可以自由设定的。也就是说,卷积层的卷积核个数也是一个超参数。我们可以把特征映射可以看做是通过卷积变换提取到的图像特征,三个卷积核就对原始图像提取出三组不同的特征,也就是得到了三个特征映射,也称做三个通道(channel)。
继续观察图7,在第一个卷积层之后,Pooling层对三个特征映射做了下采样,得到了三个更小的特征映射。接着,是第二个卷积层,它有5个卷积核。每个卷积核都把前面下采样之后的3个**特征映射卷积在一起,得到一个新的特征映射。这样,5个卷积核就得到了5个特征映射。接着,是第二个Pooling,继续对5个特征映射进行下采样,得到了5个更小的特征映射。
图7所示网络的最后两层是全连接层。第一个全连接层的每个神经元,和上一层5个特征映射中的每个神经元相连,第二个全连接层(也就是输出层)的每个神经元,则和第一个全连接层的每个神经元相连,这样得到了整个网络的输出。
至此,我们对卷积神经网络有了最基本的感性认识。接下来,我们将介绍卷积神经网络中各种层的计算和训练。

12.4.1卷积层

我们知道一般神经网络没有卷积层,深度学习中的卷积层能解决哪些问题?
神经网络在处理图像时数据量太大,例如一个输入1000*1000像素的图片(一百万像素,现在已经不能算大图了),输入层有1000*1000=100万节点。假设第一个隐藏层有100个节点(这个数量并不多),那么仅这一层就有(1000*1000+1)*100=1亿参数,这实在是太多了!神经网络训练本来就慢,这么多数据是不合理的。我们看卷积如何解决这个问题。
主要从三个方面:
局部连接
这个是最容易想到的,每个神经元不再和上一层的所有神经元相连,而只和一小部分神经元相连。这样就减少了很多参数。
权值共享
一组连接可以共享同一个权重,而不是每个连接有一个不同的权重,这样又减少了很多参数。
池化或下采样
可以使用Pooling来减少每层的样本数,进一步减少参数数量,同时还可以提升模型的鲁棒性。
对于图像识别任务来说,卷积神经网络通过尽可能保留重要的参数,去掉大量不重要的参数,来达到更好的学习效果
卷积核对图片进行卷积操作就得到的卷积层的输出,卷积核的大小是人为设定的,比如可以设置成3×3或其他尺寸,卷积核的个数也是人为设定,太多太少都不合适,卷积核的内容是训练的时候学习得来的,学习方法一般也是梯度下降法。
由上节内容可知,卷积操作,其实就是卷积核在输入的二维数据面上每次移动一个步长,进行乘法运算再除以卷积核的元素个数。
我们通常会使用多层卷积层来得到更深层次的特征图。如下:

12.4.2激活函数

为增强各层的表现力,各层的输出往往使用激活函数来生成特征映射或特征图,并以此来加入非线性因素的,因为线性模型的表达力不够。
在具体处理图像的时候,如何处理呢?我们知道在神经网络中,对于图像,我们主要采用了卷积的方式来处理,也就是对每个像素点赋予一个权值,这个操作显然就是线性的。但是样本不一定是线性可分的,为了解决这个问题,我们可以进行线性变化,或者我们引入非线性因素,解决线性模型所不能解决的问题。
在选择激活函数时应该满足注意哪些方面呢?
首先,我们知道神经网络的数学基础是处处可微的,所以选取的激活函数要能保证数据输入与输出也是可微的,运算特征是不断进行循环计算,所以在每代循环过程中,每个神经元的值也是在不断变化的。 这就导致了tanh特征相差明显时的效果会很好,在循环过程中会不断扩大特征效果显示出来,但有时,在特征相差比较复杂或是相差不是特别大时,需要更细微的分类判断的时候,sigmoid效果就好了。
其次,sigmoid 和 tanh作为激活函数的话,一定要注意一定要对 input 进行归一话,否则激活后的值都会进入平坦区,使隐层的输出全部趋同,但是 ReLU 并不需要输入归一化来防止它们达到饱和。
此外,对稀疏矩阵,也就是大多数为0的稀疏矩阵来表示。这个特性矩阵比较适合采用于Relu激活函数,Relu就是取的max(0,x),因为神经网络是不断反复计算,实际上变成了它在尝试不断试探如何用一个大多数为0的矩阵来尝试表达数据特征,结果因为稀疏特性的存在,反而这种方法变得运算得又快效果又好了。所以我们可以看到目前大部分的卷积神经网络中,基本上都是采用了ReLU 函数。
常用的激活函数
激活函数应该具有的性质:
(1)非线性。线性激活层对于深层神经网络没有作用,因为其作用以后仍然是输入的各种线性变换。。
(2)连续可微。梯度下降法的要求。
(3)范围最好不饱和,当有饱和的区间段时,若系统优化进入到该段,梯度近似为0,网络的学习就会停止。
(4)单调性,当激活函数是单调时,单层神经网络的误差函数是凸的,好优化。
(5)在原点处近似线性,这样当权值初始化为接近0的随机值时,网络可以学习的较快,不用可以调节网络的初始值。
目前常用的激活函数都只拥有上述性质的部分,没有一个拥有全部。
下面介绍几种常用的激活函数。

目前已被淘汰 ,其主要缺点:
饱和时梯度值非常小。由于BP算法反向传播的时候后层的梯度是以乘性方式传递到前层,因此当层数比较多的时候,传到前层的梯度就会非常小,网络权值得不到有效的更新,即梯度消失。如果该层的权值初始化使得f(x) 处于饱和状态时,网络基本上权值无法更新。
Tanh函数
Tanh和Sigmoid是有异曲同工之妙的,它的图形如上图右所示,不同的是它把实值得输入压缩到-1~1的范围,因此它基本是0均值的,也就解决了上述Sigmoid缺点中的第二个,所以实际中tanh会比sigmoid更常用。但是它还是存在梯度饱和的问题。Tanh是sigmoid的变形:

 

Alex在2012年提出的一种新的激活函数。该函数的提出很大程度的解决了BP算法在优化深层神经网络时的梯度耗散问题 。
优点:
(1) x>0 时,梯度恒为1,无梯度耗散问题,收敛快;
(2)增大了网络的稀疏性。当x (3)运算量很小;
缺点:
如果后层的某一个梯度特别大,导致W更新以后变得特别大,导致该层的输入<0,输出为0,这时该层就会‘die’,没有更新。当学习率比较大时可能会有40%的神经元都会在训练开始就‘die’,因此需要对学习率进行一个好的设置。
由优缺点可知max(0,x) 函数为一个双刃剑,既可以形成网络的稀疏性,也可能造成有很多永远处于‘die’的神经元,需要tradeoff。

改善了ReLU的死亡特性,但是也同时损失了一部分稀疏性,且增加了一个超参数,目前来说其好处不太明确


根据一定的概率(可配置)将输出设置为0,当引入少量随机性将有助于训练时,这个层有很好的表现,它是防止过拟合的一种策略,Dropout在训练的时候有,但在测试的时候是不能有Dropout的,毕竟只有在训练学习的时候才才担心过拟合。
简单讲,Dropout就是在按梯度下降法求网络参数的时候,每一次迭代都随机删掉一些隐含神经元,注意删掉之后会再次放回,下次迭代继续在所有隐含神经元存在的情况下随机删除。
解决过拟合的方法还有在代价函数添加正则项的方法,L1是添加绝对值,L2是添加二次方差。这两个正则项在数学上是可以证明的,能防止参数w过大。
那Dropout为什么有助于防止过拟合呢?可以简单地这样解释,运用了dropout的训练过程,相当于训练了很多个只有半数隐层单元的神经网络(后面简称为“半数网络”),每一个这样的半数网络,都可以给出一个分类结果,这些结果有的是正确的,有的是错误的。随着训练的进行,大部分半数网络都可以给出正确的分类结果,那么少数的错误分类结果就不会对最终结果造成大的影响。实际上dropout不一定非要在全连接层隐含层,卷积似乎也可以dropout。
如何选择激活函数?
通常来说,很少会把各种激活函数串起来在一个网络中使用的。
如果使用 ReLU,那么一定要小心设置 learning rate,而且要注意不要让你的网络出现很多 “dead” 神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU、PReLU 或者 Maxout.
最好不要用 sigmoid,可以试试 tanh,不过可以预期它的效果会比不上 ReLU 和 Maxout。
实际使用的时候最常用的还是ReLU函数,注意学习率的设置以及死亡节点所占的比例即可。

12.4.3池化层(Pooling)

池化(Pooling)又称为下采样,通过卷积层获得了图像的特征之后,理论上我们可以直接使用这些特征训练分类器(如softmax),但是这样做将面临巨大的计算量的挑战,而且容易产生过拟合的现象。为了进一步降低网络训练参数及模型的过拟合程度,对卷积层进行池化/采样(Pooling)处理。池化/采样的方式通常有以下两种:
最大池化(Max Pooling: 选择Pooling窗口中的最大值作为采样值;
均值池化(Mean Pooling): 将Pooling窗口中的所有值相加取平均,以平均值作为采样值
高斯池化:借鉴高斯模糊的方法。不常用。
图像经过池化后,得到的是一系列的特征图,而多层感知器接受的输入是一个向量。因此需要将这些特征图中的像素依次取出,排列成一个向量。各种池化方法可用如下图来表示:

12.4.4归一化层

对于归一化我们应该不陌生,在线性回归和逻辑回归中经常使用,而且很有效。因为输入层的输入值的大小变化不剧烈,那么输入也不会。但是,对于一个可能有很多层的深度学习模型来说,情况可能会比较复杂。
举个例子,随着第一层和第二层的参数在训练时不断变化,第三层所使用的激活函数的输入值可能由于乘法效应而变得极大或极小,例如和第一层所使用的激活函数的输入值不在一个数量级上。这种在训练时可能出现的情况会造成模型训练的不稳定性。例如,给定一个学习率,某次参数迭代后,目标函数值会剧烈变化或甚至升高。数学的解释是,如果把目标函数 f根据参数 w迭代进行泰勒展开,有关学习率 η 的高阶项的系数可能由于数量级的原因(通常由于层数多)而不容忽略。然而常用的低阶优化算法(如梯度下降)对于不断降低目标函数的有效性通常基于一个基本假设:在以上泰勒展开中把有关学习率的高阶项通通忽略不计。
为了应对上述这种情况,Sergey Ioffe和Christian Szegedy在2015年提出了批量归一化(Batch Normalization, BN)的方法。简而言之,在训练时给定一个批量输入,批量归一化试图对深度学习模型的某一层所使用的激活函数的输入进行归一化:使批量呈标准正态分布(均值为0,标准差为1)。
批量归一化通常应用于输入层或任意中间层

12.4.5全连接层

全连接层(fully connected layers,FC)在整个卷积神经网络中起到“分类器”的作用。如果说卷积层、池化层和激活函数层等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用。在实际使用中,全连接层可由卷积操作实现:对前层是全连接的全连接层可以转化为卷积核为1x1的卷积;而前层是卷积层的全连接层可以转化为卷积核为hxw的全局卷积,h和w分别为前层卷积结果的高和宽。
目前由于全连接层参数冗余(仅全连接层参数就可占整个网络参数80%左右),近期一些性能优异的网络模型如ResNet和GoogLeNet等均用全局平均池化(global average pooling,GAP)取代FC来融合学到的深度特征,最后仍用softmax等损失函数作为网络目标函数来指导学习过程。需要指出的是,用GAP替代FC的网络通常有较好的预测性能。
近期的研究(In Defense of Fully Connected Layers in Visual Representation Transfer)发现,FC可在模型表示能力迁移过程中充当“防火墙”的作用。具体来讲,假设在ImageNet上预训练得到的模型为 ,则ImageNet可视为源域(迁移学习中的source domain)。微调(fine tuning)是深度学习领域最常用的迁移学习技术。针对微调,若目标域(target domain)中的图像与源域中图像差异巨大(如相比ImageNet,目标域图像不是物体为中心的图像,而是风景照,见下图),不含FC的网络微调后的结果要差于含FC的网络。因此FC可视作模型表示能力的“防火墙”,特别是在源域与目标域差异较大的情况下,FC可保持较大的模型capacity从而保证模型表示能力的迁移。

12.4.6几种经典的CNN

这里介绍几种经典的CNN,如LeNet5、AlexNet、VGGNet、Google Inception Net、ResNet等。

12.4.6.1 LeNet5

LeNet5 诞生于 1994 年,是最早的卷积神经网络之一,并且推动了深度学习领域的发展。自从 1988 年开始,在许多次成功的迭代后,这项由 Yann LeCun 完成的开拓性成果被命名为 LeNet5(参见:Gradient-Based Learning Applied to Document Recognition)。

图1

LeNet5 的架构基于这样的观点:(尤其是)图像的特征分布在整张图像上,以及带有可学习参数的卷积是一种用少量参数在多个位置上提取相似特征的有效方式。在那时候,没有 GPU 帮助训练,甚至 CPU 的速度也很慢。因此,能够保存参数以及计算过程是一个关键进展。这和将每个像素用作一个大型多层神经网络的单独输入相反。LeNet5 阐述了那些像素不应该被使用在第一层,因为图像具有很强的空间相关性,而使用图像中独立的像素作为不同的输入特征则利用不到这些相关性。
LeNet5特征能够总结为如下几点:
1)卷积神经网络使用三个层作为一个系列: 卷积,池化,非线性
2) 使用卷积提取空间特征
3)使用映射到空间均值下采样(subsample)
4)双曲线(tanh)或S型(sigmoid)形式的非线性
5)多层神经网络(MLP)作为最后的分类器
6)层与层之间的稀疏连接矩阵避免大的计算成本
总体看来,这个网络是最近大量神经网络架构的起点,并且也给这个领域带来了许多灵感。

12.4.6.2 AlexNet

2012年,Hinton的学生Alex Krizhevsky提出了深度卷积神经网络模型AlexNet,它可以算是LeNet的一种更深更宽的版本。AlexNet中包含了几个比较新的技术点,也首次在CNN中成功应用了ReLU、Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速,作者开源了他们在GPU上训练卷积神经网络的CUDA代码。AlexNet包含了6亿3000万个连接,6000万个参数和65万个神经元,拥有5个卷积层,其中3个卷积层后面连接了最大池化层,最后还有3个全连接层。AlexNet以显著的优势赢得了竞争激烈的ILSVRC 2012比赛,top-5的错误率降低至了16.4%,相比第二名的成绩26.2%错误率有了巨大的提升。AlexNet可以说是神经网络在低谷期后的第一次发声,确立了深度学习(深度卷积网络)在计算机视觉的统治地位,同时也推动了深度学习在语音识别、自然语言处理、强化学习等领域的拓展。
AlexNet将LeNet的思想发扬光大,把CNN的基本原理应用到了很深很宽的网络中。AlexNet主要使用到的新技术点如下。
(1)成功使用ReLU作为CNN的激活函数,并验证其效果在较深的网络超过了Sigmoid,成功解决了Sigmoid在网络较深时的梯度弥散问题。虽然ReLU激活函数在很久之前就被提出了,但是直到AlexNet的出现才将其发扬光大。
(2)训练时使用Dropout随机忽略一部分神经元,以避免模型过拟合。Dropout虽有单独的论文论述,但是AlexNet将其实用化,通过实践证实了它的效果。在AlexNet中主要是最后几个全连接层使用了Dropout。
(3)在CNN中使用重叠的最大池化。此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
(4)提出了LRN层,对局部神经元的活动创建竞争