月度归档:2017年11月

第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%以上的精度,拍摄多一点照片是有效方法。
此外,本身算法还有很多优化空间。

第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

第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层,对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
(5)使用CUDA加速深度卷积网络的训练,利用GPU强大的并行计算能力,处理神经网络训练时大量的矩阵运算。AlexNet使用了两块GTX 580 GPU进行训练,单个GTX 580只有3GB显存,这限制了可训练的网络的最大规模。因此作者将AlexNet分布在两个GPU上,在每个GPU的显存中储存一半的神经元的参数。因为GPU之间通信方便,可以互相访问显存,而不需要通过主机内存,所以同时使用多块GPU也是非常高效的。同时,AlexNet的设计让GPU之间的通信只在网络的某些层进行,控制了通信的性能损耗。
(6)数据增强,随机地从256 x 256的原始图像中截取224 x 224大小的区域(以及水平翻转的镜像),相当于增加了倍的数据量。如果没有数据增强,仅靠原始的数据量,参数众多的CNN会陷入过拟合中,使用了数据增强后可以大大减轻过拟合,提升泛化能力。进行预测时,则是取图片的四个角加中间共5个位置,并进行左右翻转,一共获得10张图片,对他们进行预测并对10次结果求均值。同时,AlexNet论文中提到了会对图像的RGB数据进行PCA处理,并对主成分做一个标准差为0.1的高斯扰动,增加一些噪声,这个Trick可以让错误率再下降1%。
整个AlexNet有8个需要训练参数的层(不包括池化层和LRN层),前5层为卷积层,后3层为全连接层,如图4所示。AlexNet最后一层是有1000类输出的Softmax层用作分类。 LRN层出现在第1个及第2个卷积层后,而最大池化层出现在两个LRN层及最后一个卷积层后。ReLU激活函数则应用在这8层每一层的后面。因为AlexNet训练时使用了两块GPU,因此这个结构图中不少组件都被拆为了两部分。现在我们GPU的显存可以放下全部模型参数,因此只考虑一块GPU的情况即可。

图4
AlexNet每层的超参数如图5所示。其中输入的图片尺寸为224x224,第一个卷积层使用了较大的卷积核尺寸11x11,步长为4,有96个卷积核;紧接着一个LRN层;然后是一个3 x 3的最大池化层,步长为2。这之后的卷积核尺寸都比较小,都是5 x 5或者3 x 3的大小,并且步长都为1,即会扫描全图所有像素;而最大池化层依然保持为3 x 3,并且步长为2。我们可以发现一个比较有意思的现象,在前几个卷积层,虽然计算量很大,但参数量很小,都在1M左右甚至更小,只占AlexNet总参数量的很小一部分。这就是卷积层有用的地方,可以通过较小的参数量提取有效的特征。而如果前几层直接使用全连接层,那么参数量和计算量将成为天文数字。虽然每一个卷积层占整个网络的参数量的1%都不到,但是如果去掉任何一个卷积层,都会使网络的分类性能大幅地下降。

图5

12.4.6.3 VGGNet

VGGNet是牛津大学计算机视觉组(Visual Geometry Group)和Google DeepMind公司的研究员一起研发的的深度卷积神经网络。VGGNet探索了卷积神经网络的深度与其性能之间的关系,通过反复堆叠3x3的小型卷积核和2x2的最大池化层,VGGNet成功地构筑了16~19层深的卷积神经网络。VGGNet相比之前state-of-the-art的网络结构,错误率大幅下降,并取得了ILSVRC 2014比赛分类项目的第2名和定位项目的第1名。同时VGGNet的拓展性很强,迁移到其他图片数据上的泛化性非常好。VGGNet的结构非常简洁,整个网络都使用了同样大小的卷积核尺寸(3x3)和最大池化尺寸(2x2)。到目前为止,VGGNet依然经常被用来提取图像特征。VGGNet训练后的模型参数在其官方网站上开源了,可用来在domain specific的图像分类任务上进行再训练(相当于提供了非常好的初始化权重),因此被用在了很多地方。
VGGNet论文中全部使用了3x3的卷积核和2x2的池化核,通过不断加深网络结构来提升性能。图6所示为VGGNet各级别的网络结构图,图7所示为每一级别的参数量,从11层的网络一直到19层的网络都有详尽的性能测试。虽然从A到E每一级网络逐渐变深,但是网络的参数量并没有增长很多,这是因为参数量主要都消耗在最后3个全连接层。前面的卷积部分虽然很深,但是消耗的参数量不大,不过训练比较耗时的部分依然是卷积,因其计算量比较大。这其中的D、E也就是我们常说的VGGNet-16和VGGNet-19。C很有意思,相比B多了几个1x1的卷积层,1x1卷积的意义主要在于线性变换,而输入通道数和输出通道数不变,没有发生降维。


图6

图7
VGGNet拥有5段卷积,每一段内有2~3个卷积层,同时每段尾部会连接一个最大池化层用来缩小图片尺寸。每段内的卷积核数量一样,越靠后的段的卷积核数量越多:64 – 128 – 256 – 512 – 512。其中经常出现多个完全一样的3x3的卷积层堆叠在一起的情况,这其实是非常有用的设计。如图8所示,两个3x3的卷积层串联相当于1个5x5的卷积层,即一个像素会跟周围5x5的像素产生关联,可以说感受野大小为5x5。而3个3x3的卷积层串联的效果则相当于1个7x7的卷积层。除此之外,3个串联的3x3的卷积层,拥有比1个7x7的卷积层更少的参数量,只有后者的。最重要的是,3个3x3的卷积层拥有比1个7x7的卷积层更多的非线性变换(前者可以使用三次ReLU激活函数,而后者只有一次),使得CNN对特征的学习能力更强。

图8

VGGNet在训练时有一个小技巧,先训练级别A的简单网络,再复用A网络的权重来初始化后面的几个复杂模型,这样训练收敛的速度更快。在预测时,VGG采用Multi-Scale的方法,将图像scale到一个尺寸Q,并将图片输入卷积网络计算。然后在最后一个卷积层使用滑窗的方式进行分类预测,将不同窗口的分类结果平均,再将不同尺寸Q的结果平均得到最后结果,这样可提高图片数据的利用率并提升预测准确率。同时在训练中,VGGNet还使用了Multi-Scale的方法做数据增强,将原始图像缩放到不同尺寸S,然后再随机裁切224x224的图片,这样能增加很多数据量,对于防止模型过拟合有很不错的效果。实践中,作者令S在[256,512]这个区间内取值,使用Multi-Scale获得多个版本的数据,并将多个版本的数据合在一起进行训练。图9所示为VGGNet使用Multi-Scale训练时得到的结果,可以看到D和E都可以达到7.5%的错误率。最终提交到ILSVRC 2014的版本是仅使用Single-Scale的6个不同等级的网络与Multi-Scale的D网络的融合,达到了7.3%的错误率。不过比赛结束后作者发现只融合Multi-Scale的D和E可以达到更好的效果,错误率达到7.0%,再使用其他优化策略最终错误率可达到6.8%左右,非常接近同年的冠军Google Inceptin Net。同时,作者在对比各级网络时总结出了以下几个观点。
(1)LRN层作用不大。
(2)越深的网络效果越好。
(3)1x1的卷积也是很有效的,但是没有3x3的卷积好,大一些的卷积核可以学习更大的空间特征。

图9

12.4.6.4Google Inception Net

Google Inception Net首次出现在ILSVRC 2014的比赛中(和VGGNet同年),就以较大优势取得了第一名。那届比赛中的Inception Net通常被称为Inception V1,它最大的特点是控制了计算量和参数量的同时,获得了非常好的分类性能——top-5错误率6.67%,只有AlexNet的一半不到。Inception V1有22层深,比AlexNet的8层或者VGGNet的19层还要更深。但其计算量只有15亿次浮点运算,同时只有500万的参数量,仅为AlexNet参数量(6000万)的1/12,却可以达到远胜于AlexNet的准确率,可以说是非常优秀并且非常实用的模型。Inception V1降低参数量的目的有两点,第一,参数越多模型越庞大,需要供模型学习的数据量就越大,而目前高质量的数据非常昂贵;第二,参数越多,耗费的计算资源也会更大。Inception V1参数少但效果好的原因除了模型层数更深、表达能力更强外,还有两点:一是去除了最后的全连接层,用全局平均池化层(即将图片尺寸变为1x1)来取代它。全连接层几乎占据了AlexNet或VGGNet中90%的参数量,而且会引起过拟合,去除全连接层后模型训练更快并且减轻了过拟合。用全局平均池化层取代全连接层的做法借鉴了Network In Network(以下简称NIN)论文。二是Inception V1中精心设计的Inception Module提高了参数的利用效率,其结构如图10所示。这一部分也借鉴了NIN的思想,形象的解释就是Inception Module本身如同大网络中的一个小网络,其结构可以反复堆叠在一起形成大网络。不过Inception V1比NIN更进一步的是增加了分支网络,NIN则主要是级联的卷积层和MLPConv层。一般来说卷积层要提升表达能力,主要依靠增加输出通道数,但副作用是计算量增大和过拟合。每一个输出通道对应一个滤波器,同一个滤波器共享参数,只能提取一类特征,因此一个输出通道只能做一种特征处理。而NIN中的MLPConv则拥有更强大的能力,允许在输出通道之间组合信息,因此效果明显。可以说,MLPConv基本等效于普通卷积层后再连接1x1的卷积和ReLU激活函数。

我们再来看Inception Module的基本结构,其中有4个分支:第一个分支对输入进行1x1的卷积,这其实也是NIN中提出的一个重要结构。1x1的卷积是一个非常优秀的结构,它可以跨通道组织信息,提高网络的表达能力,同时可以对输出通道升维和降维。可以看到Inception Module的4个分支都用到了1x1卷积,来进行低成本(计算量比3x3小很多)的跨通道的特征变换。第二个分支先使用了1x1卷积,然后连接3x3卷积,相当于进行了两次特征变换。第三个分支类似,先是1x1的卷积,然后连接5x5卷积。最后一个分支则是3x3最大池化后直接使用1x1卷积。我们可以发现,有的分支只使用1x1卷积,有的分支使用了其他尺寸的卷积时也会再使用1x1卷积,这是因为1x1卷积的性价比很高,用很小的计算量就能增加一层特征变换和非线性化。Inception Module的4个分支在最后通过一个聚合操作合并(在输出通道数这个维度上聚合)。Inception Module中包含了3种不同尺寸的卷积和1个最大池化,增加了网络对不同尺度的适应性,这一部分和Multi-Scale的思想类似。早期计算机视觉的研究中,受灵长类神经视觉系统的启发,Serre使用不同尺寸的Gabor滤波器处理不同尺寸的图片,Inception V1借鉴了这种思想。Inception V1的论文中指出,Inception Module可以让网络的深度和宽度高效率地扩充,提升准确率且不致于过拟合。

图10
人脑神经元的连接是稀疏的,因此研究者认为大型神经网络的合理的连接方式应该也是稀疏的。稀疏结构是非常适合神经网络的一种结构,尤其是对非常大型、非常深的神经网络,可以减轻过拟合并降低计算量,例如卷积神经网络就是稀疏的连接。Inception Net的主要目标就是找到最优的稀疏结构单元(即Inception Module),论文中提到其稀疏结构基于Hebbian原理,这里简单解释一下Hebbian原理:神经反射活动的持续与重复会导致神经元连接稳定性的持久提升,当两个神经元细胞A和B距离很近,并且A参与了对B重复、持续的兴奋,那么某些代谢变化会导致A将作为能使B兴奋的细胞。总结一下即“一起发射的神经元会连在一起”(Cells that fire together, wire together),学习过程中的刺激会使神经元间的突触强度增加。受Hebbian原理启发,另一篇文章Provable Bounds for Learning Some Deep Representations提出,如果数据集的概率分布可以被一个很大很稀疏的神经网络所表达,那么构筑这个网络的最佳方法是逐层构筑网络:将上一层高度相关(correlated)的节点聚类,并将聚类出来的每一个小簇(cluster)连接到一起,如图11所示。这个相关性高的节点应该被连接在一起的结论,即是从神经网络的角度对Hebbian原理有效性的证明。

图11
因此一个“好”的稀疏结构,应该是符合Hebbian原理的,我们应该把相关性高的一簇神经元节点连接在一起。在普通的数据集中,这可能需要对神经元节点聚类,但是在图片数据中,天然的就是临近区域的数据相关性高,因此相邻的像素点被卷积操作连接在一起。而我们可能有多个卷积核,在同一空间位置但在不同通道的卷积核的输出结果相关性极高。因此,一个1x1的卷积就可以很自然地把这些相关性很高的、在同一个空间位置但是不同通道的特征连接在一起,这就是为什么1x1卷积这么频繁地被应用到Inception Net中的原因。1x1卷积所连接的节点的相关性是最高的,而稍微大一点尺寸的卷积,比如3x3、5x5的卷积所连接的节点相关性也很高,因此也可以适当地使用一些大尺寸的卷积,增加多样性(diversity)。最后Inception Module通过4个分支中不同尺寸的1x1、3x3、5x5等小型卷积将相关性很高的节点连接在一起,就完成了其设计初衷,构建出了很高效的符合Hebbian原理的稀疏结构。

在Inception Module中,通常1x1卷积的比例(输出通道数占比)最高,3x3卷积和5x5卷积稍低。而在整个网络中,会有多个堆叠的Inception Module,我们希望靠后的Inception Module可以捕捉更高阶的抽象特征,因此靠后的Inception Module的卷积的空间集中度应该逐渐降低,这样可以捕获更大面积的特征。因此,越靠后的Inception Module中,3x3和5x5这两个大面积的卷积核的占比(输出通道数)应该更多。
Inception Net有22层深,除了最后一层的输出,其中间节点的分类效果也很好。因此在Inception Net中,还使用到了辅助分类节点(auxiliary classifiers),即将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中。这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个Inception Net的训练很有裨益。
当年的Inception V1还是跑在TensorFlow的前辈DistBelief上的,并且只运行在CPU上。当时使用了异步的SGD训练,学习速率每迭代8个epoch降低4%。同时,Inception V1也使用了Multi-Scale、Multi-Crop等数据增强方法,并在不同的采样数据上训练了7个模型进行融合,得到了最后的ILSVRC 2014的比赛成绩——top-5错误率6.67%。
同时,Google Inception Net还是一个大家族,包括:
— 2014年9月的论文Going Deeper with Convolutions提出的Inception V1(top-5错误率6.67%)。
— 2015年2月的论文Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate提出的Inception V2(top-5错误率4.8%)。
— 2015年12月的论文Rethinking the Inception Architecture for Computer Vision提出的Inception V3(top-5错误率3.5%)。
— 2016年2月的论文Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning提出的Inception V4(top-5错误率3.08%)。

12.4.6.5 ResNet

2015 年 12 月又出现了新的变革,这和 Inception V3 出现的时间一样。ResNet 有着简单的思路:供给两个连续卷积层的输出,并分流(bypassing)输入进入下一层(参见论文:Deep Residual Learning for Image Recognition)。

图12
这和之前的一些旧思路类似。但 ResNet 中,它们分流两个层并被应用于更大的规模。在 2 层后分流是一个关键直觉,因为分流一个层并未给出更多的改进。通过 2 层可能认为是一个小型分类器,或者一个 Network-In-Network。
这是第一次网络层数超过一百,甚至还能训练出 1000 层的网络。
有大量网络层的 ResNet 开始使用类似于 Inception 瓶颈层的网络层:

图13
这种层通过首先是由带有更小输出(通常是输入的 1/4)的 1×1 卷积较少特征的数量,然后使用一个 3×3 的层,再使用 1×1 的层处理更大量的特征。类似于 Inception 模块,这样做能保证计算量低,同时提供丰富的特征结合。
ResNet 在输入上使用相对简单的初始层:一个带有两个池的 7×7 卷基层。可以把这个与更复杂、更少直觉性的 Inception V3、V4 做下对比。
ResNet 也使用一个池化层加上 softmax 作为最后的分类器。
关于 ResNet 的其他洞见每天都有发生:
ResNet 可被认为既是平行模块又是连续模块,把输入输出(inout)视为在许多模块中并行,同时每个模块的输出又是连续连接的。
ResNet 也可被视为并行模块或连续模块的多种组合(参见论文:Residual Networks are Exponential Ensembles of Relatively Shallow Networks)。
已经发现 ResNet 通常在 20-30 层的网络块上以并行的方式运行。而不是连续流过整个网络长度。
当 ResNet 像 RNN 一样把输出反馈给输入时,该网络可被视为更好的生物上可信的皮质模型(参见论文:Bridging the Gaps Between Residual Learning, Recurrent Neural Networks and Visual Cortex)。

12.4.6.6 CNN发展趋势

以上,我们简单回顾了卷积神经网络的历史,下图所示大致勾勒出最近几十年卷积神经网络的发展方向。
Perceptron(感知机)于1957年由Frank Resenblatt提出,而Perceptron不仅是卷积网络,也是神经网络的始祖。Neocognitron(神经认知机)是一种多层级的神经网络,由日本科学家Kunihiko Fukushima于20世纪80年代提出,具有一定程度的视觉认知的功能,并直接启发了后来的卷积神经网络。LeNet-5由CNN之父Yann LeCun于1997年提出,首次提出了多层级联的卷积结构,可对手写数字进行有效识别。

图14

可以看到前面这三次关于卷积神经网络的技术突破,间隔时间非常长,需要十余年甚至更久才出现一次理论创新。而后于2012年,Hinton的学生Alex依靠8层深的卷积神经网络一举获得了ILSVRC 2012比赛的冠军,瞬间点燃了卷积神经网络研究的热潮。AlexNet成功应用了ReLU激活函数、Dropout、最大覆盖池化、LRN层、GPU加速等新技术,并启发了后续更多的技术创新,卷积神经网络的研究从此进入快车道。
在AlexNet之后,我们可以将卷积神经网络的发展分为两类,一类是网络结构上的改进调整(图14左侧分支),另一类是网络深度的增加(图14右侧分支)。
2013年,颜水成教授的Network in Network工作首次发表,优化了卷积神经网络的结构,并推广了1x1的卷积结构。在改进卷积网络结构的工作中,后继者还有2014年的Google Inception Net V1,提出了Inception Module这个可以反复堆叠的高效的卷积网络结构,并获得了当年ILSVRC比赛的冠军。2015年初的Inception V2提出了Batch Normalization,大大加速了训练过程,并提升了网络性能。2015年年末的Inception V3则继续优化了网络结构,提出了Factorization in Small Convolutions的思想,分解大尺寸卷积为多个小卷积乃至一维卷积。
而另一条分支上,许多研究工作则致力于加深网络层数,2014年,ILSVRC比赛的亚军VGGNet全程使用3x3的卷积,成功训练了深达19层的网络,当年的季军MSRA-Net也使用了非常深的网络。2015年,微软的ResNet成功训练了152层深的网络,一举拿下了当年ILSVRC比赛的冠军,top-5错误率降低至3.46%。其后又更新了ResNet V2,增加了Batch Normalization,并去除了激活层而使用Identity Mapping或Preactivation,进一步提升了网络性能。此后,Inception ResNet V2融合了Inception Net优良的网络结构,和ResNet训练极深网络的残差学习模块,集两个方向之长,取得了更好的分类效果。
我们可以看到,自AlexNet于2012年提出后,深度学习领域的研究发展极其迅速,基本上每年甚至每几个月都会出现新一代的技术。新的技术往往伴随着新的网络结构,更深的网络的训练方法等,并在图像识别等领域不断创造新的准确率记录。至今,ILSVRC比赛和卷积神经网络的研究依然处于高速发展期,CNN的技术日新月异。当然其中不可忽视的推动力是,我们拥有了更快的GPU计算资源用以实验,以及非常方便的开源工具(比如TensorFlow)可以让研究人员快速地进行探索和尝试。在以前,研究人员如果没有像Alex那样高超的编程实力能自己实现cuda-convnet,可能都没办法设计CNN或者快速地进行实验。现在有了TensorFlow,研究人员和开发人员都可以简单而快速地设计神经网络结构并进行研究、测试、部署乃至实用。

参考:http://blog.csdn.net/app_12062011/article/details/62886113

12.5 卷积网络TensorFlow实例

在梯度下降和最优化部分我们用传统的神经网络在MNIST数据集上得到了90%左右的测试准确率。这个结果其实并不太理想。
在本章中,我们将使用卷积神经网络来得到一个准确率更高的模型,接近99%。卷积神经网络使用共享的卷积核对图像进行卷积操作,以提取图像深层特征。这些深层特征然后组合成特征向量输入全连接的神经网络中,再使用类似传统神经网络的方法进行分类。

12.5.1 网络架构图

输入为原始的28x28的图像,它首先进入第一个拥有16个5x5卷积核的卷积层,得到16张28x28的卷积后的图像,再进入降采样层(图中省略)最终得到16张14x14的图像(可称为16个通道)。为了保证卷积前后图像的像素不变,在卷积过后,对图像边框采取补零的操作(在TensorFlow中的conv2d的padding参数为’SAME’,如果不采取补零而是缩小像素值,padding参数值设置为’VALID’)。降采样层使用max pooling操作,将2x2的像素块取最大值合并为一个像素点,这个操作会将图像缩小1倍。
对于得到的16通道的14x14图像,进入第二个拥有36个卷积核的卷积层,得到36张14x14的卷积后图像,再进入降采样层得到36张7x7的图像。在这里包括了一些隐含的操作,对于16张原始图像,每一张图像使用36个卷积核卷积,应该得到16x36张新的图像,但是为了减少模型的参数量,降低复杂度,卷积层对每个卷积核得到的16张图像相加,最后得到36张卷积后图像。
经过两层卷积后,将36张7x7的图像展平,得到一个7x7x36的向量,输入到一个128维的全连接层,最后输入到10维的softmx层进行分类。
这里层数较多,但关键是卷积层,下节我们重点介绍一下卷积层的运算逻辑。

12.5.2卷积层

卷积层使用多个卷积核作用于同一幅图像,以得到多个卷积后的图像。如下图所示:

对于原始的图像7,使用一个5x5的卷积核,从左到右从上到下滑动。滑动的过程称为stride,卷积核分别从上到下,从左到右,步长一般设定为1或2。对卷积核覆盖的区域于卷积核进行点乘操作得到一个值作为该区域的中心点的像素。在上图中,红色代表这部分的像素对原始图像存在一个正的影响,而蓝色表示负的影响,在这个样例中卷积核似乎在识别图像中的横线部分,因为从结果看来7的那一横具有更强烈的反应。
此外,对于每一个卷积层的输出,一般会经过一个relu层或激活层,以保证全部的像素值都为正(因为所有为负的像素值都被设定为0),同时增强模型的泛化能力。

12.5.3导入需要的包

12.5.4 定义网络参数

12.5.5 导入数据

TensorFlow在样例教程中已经做了下载并导入MNIST数字手写体识别数据集的实现,可以直接使用。但运行时经常发生断网或无法下载的情况,所以这里我们采用先从http://yann.lecun.com/exdb/mnist/下载,然后把下载的4个文件放在本地当前运行目录的data/mnist目录下,具体实现请看如下代码。其中load_mnist函数请参考梯度下降及优化部分。

12.5.6 把标签转换为one-hot格式

从以上结果我们可以看出,目前标签值是0-9之间的数字,在机器学习或深度学习中,为提高分类的性能,一般会把类别转换one-hot的格式,这种格式把每个数字或类别转换为一个长度为类别总数的向量,一行只有一个1其余都是0。这里类别总数为10(共有10个不同的数字),7这个数字one-hot后就变成:[0. 0. 0. 0. 0. 0. 0. 1. 0. 0. ],具体实现如下:

在one-hot编码中,只有对应类别的那个位置为1,其余都为0,我们可以使用以下代码将其转换为真实类别:

12.5.7 数据维度

在MNIST数据集中,原始的28*28像素的黑白图片被展平为784维的向量。

为使得网络结构更加清晰,在这里对这些固定维度做如下定义:

12.5.8打印部分样例图片

12.5.9 CNN的Tensorflow实现

TensorFlow使用计算图模型来构建神经网络。其主要流程是先建立好整个网络的计算图模型,然后再导入数据进行计算。
一个TensorFlow计算图包含以下几个部分:
Placeholder: 占位符,用来读取用户输入与输出;
Variable: 模型的变量,也称为参数,在计算过程中逐步优化;
Model: 使用的神经网络模型,也可以使用一些简单的计算;
Cost Function: 代价函数,也称损失函数,如何计算模型的误差;
Optimizer: 优化器,使用哪种优化策略来降低损失。

12.5.9.1 创建变量

卷积神经网络中有两类变量,权重和偏置项。以下为初始化这两种变量的函数,其中对权重参数采用随机生成其符合正态分布的随机值,对偏置项初始化为常量0.05。

12.5.9.2 创建卷积

这个函数创建了一个卷积层。输入为4维的tensor,维度如下:
图像数量
图像高度
图像宽度
通道数
输出同样是一个4维的tensor,维度如下:
图像数量,与输入相同
图像高度,如果使用2x2 pooling,高宽都除以2
图像宽度,同上
由卷积层生成的通道数

12.5.9.3 展平操作

一个卷积层的输出为4维度的tensor。我们需要在卷积层后添加一个全连接层,首先得将4为的tensor展平为2维的tensor,这样才能直接输入到全连接层。

12.5.9.4 创建全连接层

12.5.9.5 定义占位符

占位符(Placeholder)为输入与输出占据位置,这些输入输出一般在不同的轮次都会有所变化。由于TensorFlow先构图再计算,所以需要使用占位符为输入和输出预留位置。

12.5.9.6 定义卷积层1

输入为(?, 28, 28, 1)的图像,其中?为图像数量。可以看到,第一个卷积层的输入为(?, 14, 14, 16)的tensor,即14x14像素的16通道图像。

12.5.9.7 定义卷积层2

12.5.9.8 定义展平层

展平层将第二个卷积层展平为二维tensor。

12.5.9.9 定义全连接层1

输出为(?, 10)的二维tensor,意在判定输入图像属于哪一类, 注意该层未使用relu,因为将要输入到后续的softmax中。

12.5.9.10 预测类别

第二个全连接层估计输入的图像属于某一类别的程度,这个估计有些粗糙,需要添加一个softmax层归一化为概率表示。

12.5.9.12 选择优化方法

使用自适应的梯度下降优化法,Adam。

12.5.9.13 模型性能度量

12.5.10.2执行优化的帮助函数

定义批处理函数,该函数在优化算法中被调用。

定义优化算法,调用批处理函数、Adam优化器等,并增加部分状态输出的代码。

12.5.10.3执行优化的帮助函数

12.5.10.4显示性能的帮助函数

用来输出测试准确率的的函数。计算所有图像的分类需要一定的时间,因此我们在上面定义的一些函数中重用了分类结果。这个函数会占据大量的内存,所以将测试集分成了多个小的批次。如果你的机器内存太小,你可以尝试减小batch_size。

可以看到,测试的准确率极低,但是函数的功能正常。

12.5.10.5执行一轮优化后的性能

用时: 0:00:14
测试集准确率: 80.1% (8010 / 10000)

可以看到,执行100轮迭代后,性能存在大幅度提升。

12.5.10.7 1000轮优化后的性能

迭代轮次: 101, 训练准确率: 76.6%
迭代轮次: 201, 训练准确率: 76.6%
迭代轮次: 301, 训练准确率: 93.8%
迭代轮次: 401, 训练准确率: 87.5%
迭代轮次: 501, 训练准确率: 90.6%
迭代轮次: 601, 训练准确率: 96.9%
迭代轮次: 701, 训练准确率: 93.8%
迭代轮次: 801, 训练准确率: 98.4%
迭代轮次: 901, 训练准确率: 96.9%
用时: 0:02:07
测试集准确率: 95.6% (9563 / 10000)
Example errors:

可以发现,测试集的准确率为95.6%,已经比传统的90.9%要高。输出的部分错误样例显示,部分形状相似的数字仍然难以区分。

12.5.10.8 10000轮优化后的性能

迭代轮次: 8701, 训练准确率: 100.0%
迭代轮次: 8801, 训练准确率: 100.0%
迭代轮次: 8901, 训练准确率: 100.0%
迭代轮次: 9001, 训练准确率: 100.0%
迭代轮次: 9101, 训练准确率: 98.4%
迭代轮次: 9201, 训练准确率: 100.0%
迭代轮次: 9301, 训练准确率: 95.3%
迭代轮次: 9401, 训练准确率: 100.0%
迭代轮次: 9501, 训练准确率: 100.0%
迭代轮次: 9601, 训练准确率: 100.0%
迭代轮次: 9701, 训练准确率: 100.0%
迭代轮次: 9801, 训练准确率: 100.0%
迭代轮次: 9901, 训练准确率: 100.0%
用时: 0:21:04
测试集准确率: 98.3% (9833 / 10000)
Example errors:

Confusion Matrix:
[[ 977 0 2 0 0 0 0 1 0 0]
[ 0 1129 5 0 0 0 0 1 0 0]
[ 0 1 1028 0 1 0 0 2 0 0]
[ 0 0 4 996 0 9 0 0 0 1]
[ 0 1 2 0 973 0 0 0 0 6]
[ 1 0 0 4 0 886 1 0 0 0]
[ 6 2 2 1 3 8 935 0 1 0]
[ 1 2 14 2 0 0 0 1002 2 5]
[ 12 0 17 5 5 9 1 2 914 9]
[ 2 0 2 2 3 6 0 1 0 993]]

经过10000轮迭代后,测试集的准确率达到了98.3%的准确率。在分错的样本中,部分用肉眼也难以分辨。而混淆矩阵表明绝大部分的样本都分类正确。这是一个非常好的模型。

12.5.11权重和层的可视化

为了更好的理解卷积神经网络为何能识别手写体数字,我来来可视化部分权重和层输出。

12.5.11.1卷积权重可视化

12.5.11.2卷积层输出可视化

12.5.11.3打印输入图像

12.5.11.4显示卷积层1的权重


以上就是16个卷积核在第一个通道的权重情况。其中红色为正的权重,蓝色为负的权重。在这里我们很难判别这些权重是如何起作用的。
将image1输入卷积层1,得到使用不同卷积后得到的图像,这些图像的棱角更加分明,而且在不同的边的突出情况也不同:


将image2输入卷积层1,得到如下图像,在不同部位的突出情况不同

12.5.11.5显示卷积层2的权重

现在输出第二个卷积层的权重。

由于卷积层1有16个输出通道,这意味着卷积层2有16个输入通道,每个通道的输入又对应36个输出通道,因此总共有16x36个通道的卷积核。我们先输出第一个通道的卷积核


这些权重相对与卷积层1的权重更加抽象,无法用语言来解释。接下来输出第二个通道的卷积核。


可以说明,不同输入通道对应的卷积核是不同的。将image1在卷积层1的输出再次输入卷积层2,得到如下输出:
[cceN_python theme="blackboard"]
plot_conv_layer(layer=layer_conv1, image=image2)


所输出的图像达到了一个更高的层次,卷积核试图提取一些边缘化的特征,这些特征对于同类图像的变化并不敏感。

在运行完整个计算图后,需要将它关闭,否则将一直占用资源:

参考:https://gaussic.github.io/2017/08/14/tensorflow-cnn/

5.1 梯度下降与最优化

梯度下降及最优化是机器学习、深度学习中关键技术之一,也是核心内容之一。梯度下降法是基本、经典方法,现在很多深度学习仍然使用,当然不是简单使用,而是做了很多优化。
本节就是从基础梯度下降方法定义开始,由浅入深、由简单到复杂,从训练样本数、学习率、梯度等多方面对算法进行优化。为便于大家更好理解,为此我们提供了很多实例代码、主要公式推导、图形等。

5.1.1梯度下降法简介

梯度下降法是一种致力于找到函数极值点的算法,在机器学习中,我们一般通过这种方法获取模型参数,从而求得目标函数或损失函数的极值。
问题描述:


随着迭代步数的增加,逐渐逼近极值点,可参考下图:

5.1.2训练集数据大小对优化的影响

根据每一次迭代所使用的数据集范围的不同,可以把梯度下降算法区分为批量梯度下降(Batch Gradient Descent, BGD)、随机梯度下降(Stochastic Gradient Descent, SGD)和小批量梯度下降(Mini-Batch Gradient Descent, MBGD)。
1)、BGD 是梯度下降算法最原始的形式, 其特点是每次更新参数 时, 都使用整个训练集的数据。当数据集比较大时,速度将非常慢,同时,它不能以在线的方式更新模型,当有新元素加入时,需要对全量数据进行更新,效率较低,因此,当前的梯度下降法一般不采用这种方法。
2)、SGD是对BGD的改进,SGD每次更新,只考虑一个样本数据,因此,它的速度比较快,一般要远远BGD,尤其是它能在线更新参数。不过由于单个样本会出现相似或重复的情况,因此,数据更新会出现冗余;另外,因单个样本之间的数据差异会比较大,因此,训练时可能造成每次迭代的损失函数会出现较大的波动。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_function,example,params)
params = params - learning_rate * params_grad

随机梯度下降最大的缺点在于每次更新可能并不会按照正确的方向进行,因此可以带来优化波动(扰动),如下图

3)、MBGD每次参数更新时,由m个样本数据构成,这种优化方法结合了BGD与SGD两者的优点,同时克服了两者的不足。m的取值一般较小,一般在[10,500]之间。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function,batch,params)
params = params - learning_rate * params_grad

5.1.3步长对优化的影响

在利用梯度下降法求目标函数极值时,学习速率(即探索步长)这个参数非常重要,太小可能导致迭代慢,太大了有可能跳过极值点。如何调整搜索的步长、如何加快收敛速度以及如何防止搜索时发生震荡却是一门值得深究的学问。接下来本文将分析第一个问题:学习速率的大小对搜索过程的影响。以下通过一个实例来具体说明,为简便起见,这里目标函数为一个一元二次函数:

1)、定义函数:

import numpy as np
import matplotlib.flot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

2)、编写梯度下降法函数

def GD(x_start, df, epochs, lr):
"""
梯度下降法。给定起始点与目标函数的一阶导函数,求在epochs次迭代中x的更新值
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr
x += v
xs[i+1] = x
return xs

3)、根据以上梯度的定义,假设起始搜索点(即:x_0)为-5,迭代周期(或迭代次数)为5,学习速率为0.3,测试代码如下:

def demo_GD():
x_start = -5
epochs = 5
lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)
print x

运行demo0_GD()的结果如下:
[-5. -2. -0.8 -0.32 -0.128 -0.0512]

4)、继续修改一下demo_GD()函数,可视化梯度下降法的搜索的整个过程。

ef demo0_GD():
"""演示如何使用梯度下降法GD()"""
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)

x_start = -5
epochs = 5

lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)

color = 'r'
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color, label='lr={}'.format(lr))
plt.scatter(x, func(x), c=color, )
plt.legend()
plt.show()

运行函数demo0_GD(),得到下图:

5)、看了lr=0.3时,效果不错,假设选用其它学习速率,情况如何呢?以下我们分别设置学习速率为0.1、0.3与0.9,比较一下各个学习速率的迭代过程,并把整个迭代过程进行可视化。

def demo1_GD_lr():
# 函数图像
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate')

x_start = -5
epochs = 5

lr = [0.1, 0.3, 0.9]

color = ['r', 'g', 'y']
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(len(lr)):
x = GD(x_start, dfunc, epochs, lr=lr[i])
plt.subplot(1, 3, i+1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}'.format(lr[i]))
plt.scatter(x, func(x), c=color[i])
plt.legend()
plt.show()

运行函数demo1_GD_lr()的结果如下:

由此可以看出,学习速率大小对梯度下降法的搜索过程起着非常大的影响,太小,搜索速度比较慢;太大又可能跳过极值点。要选中一个恰当的学习速率往往要花费不少时间。是否有方法能避免或解决这个问题呢?使我们不必花费太多时间在选择学习速率这个参数上,又能获取同样、甚至更好的效果呢?
有的,我们可以通过衰减因子、引入动量、自动调整学习速率的方法(又称为自适应梯度策略)等方法就可。
(3)衰减因子
当学习速率较大时,容易在搜索过程中发生震荡,而发生震荡的根本原因无非就是搜索的步长迈的太大了。
如果能够让 lr 随着迭代周期不断衰减变小,那么搜索时迈的步长就能不断减少以减缓震荡。学习速率衰减因子由此诞生。如何使lr 随着迭代周期不断衰减变小?我们只要把循环次数加入参数中即可:
lr_i = lr_start * 1.0 / (1.0 + decay * i)
上面的公式即为学习速率衰减公式,其中 lr_i 为第 i 次迭代时的学习速率, lr_start 为原始学习速率, decay 为 一个介于[0.0, 1.0]的小数。
decay 越小,学习速率衰减地越慢,当 decay = 0 时,学习速率保持不变。
decay 越大,学习速率衰减地越快,当 decay = 1 时,学习速率衰减最快。
以下我们通过一个实例来说明,如果通过衰减因子避免因学习速率过大,导致搜索震荡问题。
还是以函数为例,这次引入衰减因子decay 来改变学习速率。
(1)、先定义函数及导数,确定衰减因子与学习速率lr的关系

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_decay(x_start, df, epochs, lr, decay):
xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# 学习速率衰减
lr_i = lr * 1.0 / (1.0 + decay * i)
# v表示x要改变的幅度
v = - dx * lr_i
x += v
xs[i+1] = x
return xs

(2)、利用新的学习速率来进行优化

def demo3_GD_decay():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Decay')

x_start = -5
epochs = 10

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

plt.figure(figsize=(14,10))
row = len(lr)
col = len(decay)
size = np.ones(epochs + 1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_decay(x_start, dfunc, epochs, lr=lr[i], decay=decay[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, de={}'.format(lr[i], decay[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

demo3_GD_decay()

运行结果如下:

在所有行中均可以看出,decay越大,学习速率衰减地越快。在第三行与第四行可看到,decay确实能够对震荡起到减缓的作用。
(3)、比较多种衰减因子,对学习速率的影响。
假设起始学习速率为1.0,decay为[0.0, 0.001, 0.1, 0.5, 0.9, 0.99],迭代周期为300

ef demo4_how_to_chose_decay():
lr = 1.0
iterations = np.arange(300)

decay = [0.0, 0.001, 0.1, 0.5, 0.9, 0.99]
for i in range(len(decay)):
decay_lr = lr * (1.0 / (1.0 + decay[i] * iterations))
plt.plot(iterations, decay_lr, label='decay={}'.format(decay[i]))

plt.ylim([0, 1.1])
plt.legend(loc='best')
plt.show()

demo4_how_to_chose_decay()

运行结果如下:

可以看到,当decay为0.1时,50次迭代后学习速率已从1.0急剧降低到了0.2。如果decay设置得太大,则可能会收敛到一个不是极值的位置。
由上可知,小批量梯度下降法选择合适的learning rate比较困难,也很难保证良好地收敛,此外,对神经网络最优化非凸的罚函数时,另一个通常面临的挑战,是如何避免目标函数被困在局部最小值中。Dauphin 及其他人认为,这个困难并不来自于局部最小值,而是来自于「鞍点」,也就是在一个方向上斜率是正的、在一个方向上斜率是负的点。这些鞍点通常由一些函数值相同的面环绕,它们在各个方向的梯度值都为 0,所以 SGD 很难从这些鞍点中脱开。是否有更好的方法能解决这些问题或挑战?下面我们介绍引入动量(momentum)的优化方法。

5.1.4 动量对梯度下降法的影响

通过前面学习速率的实例,我们知道学习速率较小时,收敛到极值的速度较慢。
学习速率较大时,容易在搜索过程中发生震荡,如下图。

在这种情况下,SGD 在陡谷的周围震荡,向局部极值处缓慢地前进。优化方法除与学习率有关外,还与梯度方法有关,这里我们可以通过引入一个动量方法,来避免这个比较费时问题。动量(momentum)是模拟物理里动量的概念,积累之前的动量来替代真正的梯度。动量在物理学上定义为质量乘以速度,这里我们不妨假设单位质量,因此速度向量就可以看成为动量。一个物体在运动时具有惯性,把这个思想运用到梯度下降计算中,增加算法的收敛速度和稳定性,如下图:

从这个图形可以看出:
当本次梯度下降- lr*dx 的方向与上次更新量的方向相同时,上次的更新量能够对本次的搜索起到一个正向加速的作用。
当本次梯度下降- lr*dx的方向与上次更新量的方向相反时,上次的更新量能够对本次的搜索起到一个减速的作用。
使用动量的梯度下降法的Python代码如下:

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_momentum(x_start, df, epochs, lr, mu):
"""
带有冲量的梯度下降法。
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:param mu: 动量
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr + mu * v
x += v
xs[i+1] = x
return xs

为了查看mu大小对不同学习速率的影响,此处设置学习速率为lr = [0.01, 0.1, 0.6, 0.9],冲量依次为mu = [0.0, 0.1, 0.5, 0.9],起始位置为x_start = -5,迭代周期为6。测试以及绘图代码如下:

def demo2_GD_momentum():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate, Momentum')

x_start = -5
epochs = 6

lr = [0.01, 0.1, 0.6, 0.9]
mu = [0.0, 0.1, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

row = len(lr)
col = len(mu)
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_momentum(x_start, dfunc, epochs, lr=lr[i], mu=mu[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, mu={}'.format(lr[i], mu[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

运行结果如下:

从上图不难以下几点:
(1)从第一行可看出:在学习率较小的时候,适当的momentum能够起到一个加速收敛速度的作用。
(2)从第四行可看出:在学习率较大的时候,适当的momentum能够起到一个减小收敛时震荡幅度的作用。
从上述两点来看,momentum确实能够解决收敛慢或震荡的两个问题。

然而在第二行与第三行的最后一列图片中也发现了一个问题,当momentum较大时,原本能够正确收敛的时候却因为刹不住车跑过头了。那么怎么继续解决这个新出现的问题呢?下面我们介绍一种改进动量方法。

5.1.5 改进的动量更新策略

 

NAG算法相对于Momentum多了一个本次梯度相对上次梯度的变化量,这个变化量本质上是对目标函数二阶导的近似。由于利用了二阶导的信息,NAG算法才会比Momentum具有更快的收敛速度.
NAG的python代码具体实现:

import numpy as np
import matplotlib.pyplot as plt
def f(x):
return x[0] * x[0] + 50 * x[1] * x[1]
def g(x):
return np.array([2 * x[0], 100 * x[1]])
xi = np.linspace(-200,200,1000)
yi = np.linspace(-100,100,1000)
X,Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y

%matplotlib inline
def contour(X,Y,Z, arr = None):
plt.figure(figsize=(15,7))
xx = X.flatten()
yy = Y.flatten()
zz = Z.flatten()
plt.contour(X, Y, Z, colors='black')
plt.plot(0,0,marker='*')
if arr is not None:
arr = np.array(arr)
for i in range(len(arr) - 1):
plt.plot(arr[i:i+2,0],arr[i:i+2,1])

contour(X,Y,Z)

def nesterov(x_start, step, g, discount = 0.7):
x = np.array(x_start, dtype='float64')
passing_dot = [x.copy()]
pre_grad = np.zeros_like(x)
for i in range(50):
x_future = x - step * discount * pre_grad
grad = g(x_future)
pre_grad = pre_grad * discount + grad
x -= pre_grad * step
passing_dot.append(x.copy())
#print '[ Epoch {0} ] grad = {1}, x = {2}'.format(i, grad, x)
if abs(sum(grad)) < 1e-6:
break;
return x, passing_dot

start_point = [150,75]
step = 0.012
discount = 0.9
res2, x_arr2 = nesterov(start_point, step, g, discount)
contour(X,Y,Z, x_arr2)


参考了:
http://www.jianshu.com/p/58b3fe300ecb
https://github.com/WarBean/zhihuzhuanlan/blob/master/Momentum_Nesterov.ipynb

5.1.6 自适应梯度策略

通过以上实例的分析我们了解到,虽然梯度下降算法效果很好,并且广泛使用,但同时其也存在一些挑战与问题需要解决:
1)选择一个合理的学习速率很难。
如果学习速率过小,则会导致收敛速度很慢。如果学习速率过大,那么其会阻碍收敛,即在极值点附近会振荡。
2)学习速率调整也不易
学习速率调整(又称学习速率调度),试图在每次更新过程中,改变学习速率,一般使用某种事先设定的策略或者在每次迭代中衰减一个较小的阈值。无论哪种调整方法,都需要事先进行固定设置,这边便无法自适应每次学习的数据集特点。
3)固定学习速率易导致算法卡在鞍点。
前面的策略都是针对迭代方向进行优化,学习速率为固定值,即所有参数共享相同的学习速率,学习速率在每一个方向上的大小固定,很容易造成算法被卡在鞍点的位置。

图 5.3 鞍点
这是实现上图的python代码

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
plot_args = {'rstride': 1, 'cstride': 1, 'cmap':"Blues_r",
'linewidth': 0.4, 'antialiased': True,
'vmin': -1, 'vmax': 1}

x, y = np.mgrid[-1:1:31j, -1:1:31j]
z = x**2 - y**2

ax.plot_surface(x, y, z, **plot_args)
ax.plot([0], [0], [0], 'ro')
ax.view_init(azim=-60, elev=30)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
plt.xticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
plt.yticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.set_zticks([-1, -0.5, 0, 0.5, 1])
ax.set_zticklabels([r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
plt.savefig("Saddle_point.svg", bbox_inches="tight", transparent=True)
plt.show()

4)对于非凸目标函数,容易陷入那些次优的局部极值点中。
如何解决这些问题?这里我们介绍几种自适应方法来解决此类问题。

5.1.6.1 AdaGrad自适应梯度策略

如图5.3 所示,假设鞍点为a,由于学习速率不变,容易在鞍点处来回震荡,无法找到最优点。下面我们介绍学习速率随迭代数的变化而变化的自适应梯度策略,简称为AdaGrad。AdaGrad是一种自适应的调整学习速率的优化方法。
假设每个模型参数使用相同的学习速率η,而Adagrad在每一个更新步骤中对于每一个模型参数使用不同的学习速率lr,设第t次更新步骤中,目标函数的参数梯度为
AdaGrad算法主要步骤如下:
lr为全局学习速率参数
初始参数为θ
小参数δ
初始化梯度累积变量 γ=0

由上面算法的伪代码可知,
1)、随着迭代时间越长,累积梯度r越大,从而学习速率随着时间就减小,在接近目标值时,不会因为学习速率过大而越过极值点;
2)、不同参数之间学习速率是不同,因此,与前面固定学习速率相比,不容易在鞍点卡住;
3)、如果梯度累积参数r比较小,则学习速率会比较大,所以参数迭代的步长就会比较大;相反,如果梯度累积参数比较大,则学习速率会比较小,所以迭代的步长会比较小。

5.1.6.2RMSprop自适应梯度策略

经验上,RMSProp已被证明是一种有效而且实用的深度学习网络优化算法,这也是深度学习中经常采用的方法之一。

5.1.6.3 Adam自适应梯度策略

Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
Adam是另一种学习速率自适应的深度神经网络方法,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习速率。Adam的优点主要在于经过偏置校正后,每一次迭代学习速率都有个确定范围,使得参数比较平稳。公式如下:


特点:
结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
对内存需求较小
为不同的参数计算不同的自适应学习速率
也适用于大多非凸优化 - 适用于大数据集和高维空间

5.1.7 各种算法可视化

如下的两个动画(图像版权:Alec Radford)给了我们关于各种优化算法在优化过程中行为的直观说明。

contours_evaluation_optimizers
图5.4
在图 5.4 中,我们可以看到,在罚函数的等高线图中,优化器的位置随时间的变化情况。注意到,Adagrad、 Adadelta 及 RMSprop 法几乎立刻就找到了正确前进方向并以相似的速度很快收敛。而动量法和 NAG 法,则找错了方向,如图所示,让小球沿着梯度下降的方向前进。但 NAG 法能够很快改正它的方向向最小指出前进,因为他能够往前看并对前面的情况做出响应。
saddle_point_evaluation_optimizers

图5.5

图 5.5 展现了各算法在鞍点附近的表现。如上面所说,这对对于 SGD 法、动量法及 NAG 法制造了一个难题。他们很难打破」对称性「带来的壁垒,尽管最后两者设法逃脱了鞍点。而 Adagrad 法、RMSprop 法及 Adadelta 法都能快速的沿着负斜率的方向前进。

5.1.8 如何选择优化方法

在一般机器学习中很多会用 SGD,SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点。
如果需要更快的收敛,或者是训练更深更复杂的神经网络,需要用一种自适应的算法。
最后两张动图从直观上展现了算法的优化过程。第一张图为不同算法在损失平面等高线上随时间的变化情况,第二张图为不同算法在鞍点处的行为比较。
我们该如何选择优化器呢?如果数据为稀疏的,而且采用深度学习,一般需要考虑使用自适用方法,如 Adagrad, RMSprop, Adam等。当然RMSprop, Adam 在很多情况下的效果是相似的。Adam在 RMSprop 的基础上加了bias-correction 和 momentum,随着梯度变的稀疏,Adam 比 RMSprop 效果会好。整体来讲,Adam 是最好的选择。
总的来说,RMSprop 法是一种基于 Adagrad 法的拓展,他从根本上解决学习率骤缩的问题。而 Adam 法,则基于 RMSprop 法添加了偏差修正项和动量项。在我们地讨论范围中,RMSprop、Adam 法都是非常相似地算法,在相似地情况下都能做的很好。Kingma 及其他人展示了他们的偏差修正项帮助 Adam 法,在最优化过程快要结束、梯度变得越发稀疏的时候,表现略微优于 RMSprop 法。总的来说,Adam 也许是总体来说最好的选择。
有趣的是,很多最新的论文,都直接使用了(不带动量项的)Vanilla SGD 法,配合一个简单的学习率(退火)列表。如论文所示,这些 SGD 最终都能帮助他们找到一个最小值,但会花费远多于上述方法的时间。并且这些方法非常依赖于鲁棒的初始化值及退火列表。因此,如果你非常在你的模型能快速收敛,或是你需要训练一个深度或复杂模型,你可能需要选择上述的适应性模型。
就一般经验而言:
对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值
SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下,结果更可靠
如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多。
在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果。

为便于大家了解Python生态及如何学习Python提供一些参考,这里附上几张Python知识图谱,供参考。

1、Python基础学习

2、Python生态图谱

3、零基础学人工智能

这里引用莫烦的一个学习路线图,供大家参考。据他说,他学人工智能基本靠自学,莫烦(实名:周沫凡,湖南人,本科为桂林理工,土木工程系),下图从start开始。

4、Python全栈技能

5、人工智能工程师---学习路线图

6、吴恩达深度学习课程精要

第1章 MySQL简介

1.1 数据库泛述

为什么要学习数据库?
这个问题我觉得还是从反面来回答比较好,数据库出故障了,会发生啥呢?
学数据库与我找工作或找到更好工作有关系吗?有,关系还很大哦。当然,如果你去应聘不用电脑的职业除外。否则,很可能产生“一丑遮百俊”。
学了数据库有哪些好处?
其他好处不好说,但如果你学了熟悉数据库,对学习其他技术有非常大得帮助,尤其对学习大数据相关技术如Hive、HBase、SparkSQL、SparkRDD等等更是如此,数据库很多都是相通或相似的,学好一个学其他的就轻松多了。
有哪些数据库?
数据库种类很多,从大得方面来说,可分为关系型数据库和非关系型数据库,如MySQL、SQL Server、Oracle、DB2、Sybase等属于关系型数据库,近些年比较火的HBase、MongoDB、Redis等属于非关系型数据库,从存储方式方面来说,可分为行存储数据库、列存储数据库、键值数据库、NoSQL数据库等,当然各类关系型数据库或非关系型数据库自身都各有一些特点。这里就不展开说了。
如何学习数据库?
这个问题有点仁者见仁智者见智,一百人可能有一百个答案,不过我个人认为,数据库作为一个基础性非常强、使用非常广泛的系统,多花些时间进行一些有系统的学习是非常必要,好的基础将大大提升你的竞争力、拓展你的职业发展空间。

1.2 MySQL特点

为何选择MySQL? 它有哪些特点?
MySQL是由原MySQL AB公司自主研发的,现在已经被Sun公司收购,是目前IT行业最流行的开放源代码的数据库管理系统,同时它也是一个支持多线程高并发多用户的关系型数据库管理系统。支持Linux、Windows、MAC等多种操作系统,虽然功能上没有其他的大型数据库Oracle、DB2等那么齐全,但好用、易用、开源、可靠性等特点受到成千上万企业和用户的青睐重要原因。
向大家推荐几个学习MySQL的网站:
MySQL社区: http://www.mysqlpub.com/
MySQL菜鸟教程: http://www.runoob.com/mysql/mysql-tutorial.html
MySQL官网: http://www.mysql.com/
《深入浅出MySQL》唐汉名等著

1.3 数据库基础

数据库是一个长期存储在计算机内的、有组织、共享、统一的数据集合。作为关系型数据其关系可理解为数据库表,表是一系列二维数组的集合。
 表定义:表(Table),在关系型数据库中,表是一系列二维数组的集合,由纵向的列和横向的行组成,列又称为字段,行又称为记录。
表样例:stud_info(学生基本信息表)

 表要素有:
关系或表、列,字段行,记录
 数据类型:
字符串数据类型、整数型、日期/时间类型、浮点数据型等
 关键字,主键:
主键(Primary key),唯一标志表的每一条记录,可以定义表中一列或多列为主键, 主键列上不能有重复的值,也不能为空,如stud_info表中代码字段为该表的主键。
关系模式或表结构,格式为:表名称(属性1,属性2,…属性n)

1.4 数据库语言

我们一般通过数据库语言与数据库打交道,其中SQL是我们常用的,SQL的含义是结构化查询语言(Structured Query Language)。
SQL语言分类
 数据定义语言(DDL)
如:DROP,CREATE,ALTER等
 数据操作语言(DML)
如:INSERT,UPDATE,DELETE等
 数据查询语言(DQL)
SELECT等
 数据控制语言(DCL)
GRANT,REVOKE,COMMIT,ROLLBAK等。
为进一步理解SQL语句含义,下面以创建一张表的SQL语句为例,表名为t_student,具体SQL语句如下:

CREATE TABLE t_student
(
stud_id INT NOT NULL,
stud_name VARCHAR(40) NOT NULL,
stud_sex CHAR(1),
stud_birthday DATE,
PRIMARY KEY (stud_id)
);

这是一个典型的数据定义语言(DDL),该表共有4个字段,分别为stud_id,stud_name,stud_sex,stud_birthday,其中stud_id为主键。
目前这个表是空表,只有结构没有数据,我们可以用数据操作语句(DML)往表插入记录或数据。

INSERT INTO t_student(stud_id,stud_name,stud_sex,stud_birthday)
VALUES(1001001,'刘芳','F','1995-06-19');

以上记录是否成功插入到表中呢?我们可以通过数据查询语言(DQL)来验证一下:

select * from t_student;
+---------+-----------+----------+---------------+
| stud_id | stud_name | stud_sex | stud_birthday |
+---------+-----------+----------+---------------+
| 1001001 | 刘芳 | F | 1995-06-19

1.5 数据库系统架构

(图1-1 数据库系统架构)

第2章 安装与配置

2.1 Windows平台下安装配置

MySQL支持多平台,如常见的windows,linux等,这里我们以Windows下安装为主,然后简单说明在Linux平台上的安装。
这里介绍一个针对初学者的Windows版的MySQL安装程序(mysqlSetup.exe,文件下载链接:http://pan.baidu.com/s/1kVHK3eZ),文件大小为35M左右,版本为V5.0,依赖较少,安装方便,但基础功能都有。
以下为详细的安装步骤:
第1步:点击“mysqlSetup.exe”文件,弹出如下界面

第2步:选择安装类型,选择typical(典型安装)。

第3步:是否注册,选择skip sign-up(不注册)

第4步:开始配置服务器

第5步:在配置类型界面中,选择Detailed configuration(详细配置)

第6步:在服务器类型中,作为初学者,可以选择Developer Machine(作为开发机),占用系统资源较少,但基本功能都有。

第7步:字符集选择界面,第一个为西文编码,第二个是多字节的通用utf8编码,都不是我们通用的编码,这里选择第三个,然后在Character Set那里选择或填入“gbk”,当然也可以用“gb2312”,区别就是gbk的字库容量大,包括了gb2312的所有汉字。

第8步:在数据库用途界面,选择Mutifunctional Database(多功能数据库)。

第9步:进入服务器最多并发连接数界面,作为初学者,不涉及很复杂的业务逻辑,而且并发数也需要很多,可选择第一项OLAP或OLTP,并发连接数(concurrent connection)缺省为15,当然你也可以根据需要进行修改。

第10步:配置网络,使用缺省配置即可。

第11步:在Windows设置界面,记得在勾上“Inclode bin Directory in windows PATH”,这样自动把mysql命令所在目录放在Windows的环境变量Path中,接下来你便可在任何目录下启动mysql。


第12步,确认root用户密码

第13步,显示将执行的内容(无需选择),点击“Execute”

第14步 安装结束

第15步 登录MyQL系统。
 通过Windows命令行登录
第1步,从开始菜单选择“运行”,打开运行对话框,输入‘cmd’

第2步,按确定,打开DOS窗口。

第3步,在DOS窗口中,可以通过输入命令登录MySQL系统,命令格式为:
mysql -h hostname -u username -p
其中mysql为登录命令,
-h 后的参数为服务器名称或IP地址,如果为本地服务器,可为localhost 或127.0.0.1, -u后的参数为用户名称,还没有创建其他用户时,只有root用户。
-p 后的参数为用户密码。

第4步,退出mysql系统,只要在mysql>后,输入quit 然后回车即可。
 通过MySQL命令行登录
选择“开始”菜单,点击含“MySQL Command Line Client”字样的图标,进入登录界面,然后输入root用户密码即可登录。

这是在登录windows下MySQL系统的几种方式,下章我们将介绍如何登录远程服务器上的MySQL系统。

第3章 操作数据库

3.1 登录Linux服务器的MySQL

在第2章我们介绍了如何在Windows下安装和配置MySQL、通过“运行”或MySQL的命令行等界面登录Windows下的MySQL系统。
如果要连接Linux下的MySQL系统,该如何配置或操作呢?这个比较简单,首先,先通过Xshell或Putty等客户端连接Linux服务器,然后在Linux命令行输入连接MySQL的信息即可。如何连接Linux系统请参考第1章的1.6节。现在我们以用户feigu(密码也是feigu)连接MySQL。

3.2 创建数据库

登录MySQL系统后,我们可查看已创建的数据库,当然,我们也可自己创建数据。MySQL中命令一般以分号(;)或\g结束。

mysql> show databases; ###查看已有数据库
+--------------------+
| Database |
+--------------------+
| information_schema | ###系统自建数据库
| mysql | ###系统自建数据库
| performance_schema | ###系统自建数据库
| testdb |
+--------------------+
4 rows in set (0.00 sec)

其中information_schema、mysql、performance_schema为系统自建数据库,用户不宜修改这些库。
创建数据库,其语法格式为:
CREATE DATABASE database_name;
创建一个数据库test_db,然后,验证创建是否成功。

mysql> CREATE DATABASE test_db; ###创建数据库test_db
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| test_db | ###创建数据库成功
| testdb |
+--------------------+

3.3 删除数据库

创建了数据库,如果不用时,我们可以删除,当然测试关系不大,平时删除数据库要非常谨慎,如果有实际数据建议先备份,然后再删除,有备无患哦。
删除数据库的命令格式为:

DROP DATABASE dabase_name;
mysql> drop database test_db; ###删除数据库test_db
Query OK, 0 rows affected (0.09 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| testdb |
+--------------------+

3.4 获取帮助信息

MySQL有很多命令,各种命令的格式一般不同,忘记一些命令的使用方法是大概率事件,万一忘记了咋办?百度、google当然不失为方法之一,实际上MySQL自身就带有很强大的帮助功能,如查看MySQL一般可用help,如果要查询具体某个命令的使用方法,用help 命令名称,非常方便而且详细。

mysql> help; ###查看帮助信息

For information about MySQL products and services, visit:
http://www.mysql.com/
For developer information, including the MySQL Reference Manual, visit:
http://dev.mysql.com/
To buy MySQL Enterprise support, training, or other products, visit:
https://shop.mysql.com/

List of all MySQL commands:
Note that all text commands must be first on line and end with ';'
? (\?) Synonym for <code>help'.
clear (\c) Clear the current input statement.
connect (\r) Reconnect to the server. Optional arguments are db and host.
delimiter (\d) Set statement delimiter.
edit (\e) Edit command with $EDITOR.
ego (\G) Send command to mysql server, display result vertically.
exit (\q) Exit mysql. Same as quit.
go (\g) Send command to mysql server.
help (\h) Display this help.
nopager (\n) Disable pager, print to stdout.
notee (\t) Don'
t write into outfile.
pager (\P) Set PAGER [to_pager]. Print the query results via PAGER.
print (\p) Print current command.
prompt (\R) Change your mysql prompt.
quit (\q) Quit mysql.
rehash (\#) Rebuild completion hash.
source (\.) Execute an SQL script file. Takes a file name as an argument.
status (\s) Get status information from the server.
system (\!) Execute a system shell command.
tee (\T) Set outfile [to_outfile]. Append everything into given outfile.
use (\u) Use another database. Takes database name as argument.
charset (\C) Switch to another charset. Might be needed for processing binlog with multi-byte charsets.
warnings (\W) Show warnings after every statement.
nowarning (\w) Don't show warnings after every statement.

For server side help, type '
help contents'

如何查看具体命令的使用方法?可用help 命令关键字,或? 命令关键字。如查看create database的具体使用方法。

mysql> help create dabase;

Nothing found
Please try to run 'help contents' for a list of all accessible topics

mysql> help create database;
Name: 'CREATE DATABASE'
Description:
Syntax:
CREATE {DATABASE | SCHEMA} [IF NOT EXISTS] db_name
[create_specification] ...

create_specification:
[DEFAULT] CHARACTER SET [=] charset_name
| [DEFAULT] COLLATE [=] collation_name

CREATE DATABASE creates a database with the given name. To use this
statement, you need the CREATE privilege for the database. CREATE
SCHEMA is a synonym for CREATE DATABASE.

URL: http://dev.mysql.com/doc/refman/5.5/en/create-database.html

3.5 数据库存储引擎

MySQL有个独特、强大而灵活的功能,可以根据表的用途选择存储引擎。
它提供了很多种类型的存储引擎,我们可以根据对数据处理的需求,选择不同的存储引擎,从而最大限度的利用MySQL强大的功能。MySQL支持的存储引擎有:InnoDB、MyISAM、Memory、Merge、Archive等,可以使用SHOW ENGINES语句查看目前系统支持的存储引擎的类型:

mysql> SHOW ENGINES \G
*************************** 1. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 2. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 4. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 5. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 7. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 8. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
XA: YES
Savepoints: YES
*************************** 9. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
9 rows in set (0.00 sec)

以下我们列举几种常用存储引擎。
 InnoDB
InnoDB是一个健壮的事务型存储引擎,这种存储引擎已经被很多互联网公司使用,为用户操作非常大的数据存储提供了一个强大的解决方案。作为默认的存储引擎,InnoDB还引入了行级锁定和外键约束,在以下场合下,使用InnoDB是最理想的选择:
1.更新密集的表。InnoDB存储引擎特别适合处理多重并发的更新请求。
2.事务。InnoDB存储引擎是支持事务的标准MySQL存储引擎。
3.自动灾难恢复。与其它存储引擎不同,InnoDB表能够自动从灾难中恢复。
4.外键约束。MySQL支持外键的存储引擎只有InnoDB。
5.支持自动增加列AUTO_INCREMENT属性。
一般来说,如果需要事务支持,并且有较高的并发读取频率,InnoDB是不错的选择。
 MyISAM
MyISAM表是独立于操作系统的,这说明可以轻松地将其从Windows服务器移植到Linux服务器;每当我们建立一个MyISAM引擎的表时,就会在本地磁盘上建立三个文件,文件名就是表明。例如,我建立了一个MyISAM引擎的tb_Demo表,那么就会生成以下三个文件:
1.tb_demo.frm,存储表定义;
2.tb_demo.MYD,存储数据;
3.tb_demo.MYI,存储索引。
MyISAM表无法处理事务,这就意味着有事务处理需求的表,不能使用MyISAM存储引擎。MyISAM存储引擎特别适合在以下几种情况下使用:
1.查询密集型的表。MyISAM存储引擎在筛选大量数据时非常迅速,这是它最突出的优点。
2.插入密集型的表。MyISAM的并发插入特性允许同时选择和插入数据。例如:MyISAM存储引擎很适合管理邮件或Web服务器日志数据等。
 MEMORY
使用MySQL Memory存储引擎的出发点是速度。为得到最快的响应时间,采用的逻辑存储介质是系统内存。虽然在内存中存储表数据确实会提供很高的性能,但当mysqld守护进程崩溃时,所有的Memory数据都会丢失。获得速度的同时也带来了一些缺陷。它要求存储在Memory数据表里的数据使用的是长度不变的格式,这意味着不能使用BLOB和TEXT这样的长度可变的数据类型,VARCHAR是一种长度可变的类型,但因为它在MySQL内部当做长度固定不变的CHAR类型,所以可以使用。
一般在以下几种情况下使用Memory存储引擎:
1.目标数据较小,而且被非常频繁地访问。在内存中存放数据,所以会造成内存的使用,可以通过参数max_heap_table_size控制Memory表的大小,设置此参数,就可以限制Memory表的最大大小。
2.如果数据是临时的,而且要求必须立即可用,那么就可以存放在内存表中。
3.存储在Memory表中的数据如果突然丢失,不会对应用服务产生实质的负面影响。
Memory同时支持散列索引和B树索引。B树索引的优于散列索引的是,可以使用部分查询和通配查询,也可以使用<、>和>=等操作符方便数据挖掘。散列索引进行“相等比较”非常快,但是对“范围比较”的速度就慢多了,因此散列索引值适合使用在=和<>的操作符中,不适合在<或>操作符中,也同样不适合用在order by子句中。
表存储引擎可以在创建表时利用USING子句指定。

第4章 操作表

在关系型数据库中,表是是数据库中最重要、最基本的操作对象,是数据存储的基本单位,数据是按行存储的(非关系型数据库有些是按列存储的),同时通过表中的主键、外键、索引、非空等约束来保证数据的一致性和完整性。这一章主要介绍数据表的基本操作:如何创建表、查看表的结构、修改数据表、删除数据表等。
创建表的方式,可以命令行的方式,也可以通过客户端(navicat for mysql)或通过该客户端的建模方式创建,然后,把模型同步到数据库即可。

4.1 创建表

创建表的过程就是确定数据列的属性、制定数据完整性、一致性等约束的过程。而这些约束主要通过主键、外键、是否可空、索引等约束来实现的。
创建表的语法形式:
CREATE TABLE 表名称
(
字段名称1 数据类型 [列级的约束] [默认值],
字段名称2 数据类型 [列级的约束] [默认值],
--------
[表级的约束]
);
注:
(1)、表的名称不区分大小写,不能以SQL中的一些关键字为表名,如CREATE、ALTER、INSERT等;
(2)、列名间用逗号隔开。
下面以创建一个学生的基本信息表为例,说明如何创建一张表。
表的定义或结构如下:

学生信息(t_stud_info)表结构

注:
(1)、stud_code这个字段为主键,作为主键(PRIMARY KEY)的字段不能为空,为空的字段不能为主键,主键可以建的一个字段上,也可建的两个或两个以上的字段上,称为组合主键;
(2)、非空字段,是指这些字段的值,不能为空(即为null)。
创建表的SQL语句:

CREATE TABLE t_stud_info
(
stud_code varchar(20) NOT NULL,
stud_name varchar(100) NOT NULL,
stud_gend varchar(10) , #没有说明NULL,说明是NULL
college_name varchar(300) NULL,
PRIMARY KEY (stud_code) #指明构成主键的字段
); #最后是分号

创建表的SQL以写好,如何执行,如何查看表结构?通过实例来说明:
 直接把创建表的SQL语句放在mysql命令行执行:

mysql> use testdb; ###把表创建在该数据库上
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> CREATE TABLE t_stud_info
-> (
-> stud_code varchar(20) NOT NULL,
-> stud_name varchar(100) NOT NULL,
-> stud_gend varchar(10) ,
-> college_name varchar(300) NULL,
-> PRIMARY KEY (stud_code)
-> );
Query OK, 0 rows affected (0.31 sec)
mysql> show tables; ###查看已创建的表
+------------------+
| Tables_in_testdb |
+------------------+
| stud_info |
| stud_info_copy |
| stud_score |
| stud_score_view |
| t_stud_info | ###表创建成功
+------------------+
5 rows in set (0.00 sec)

mysql> desc t_stud_info; ###查看表结构
+--------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| stud_code | varchar(20) | NO | PRI | NULL | |
| stud_name | varchar(100) | NO | | NULL | |
| stud_gend | varchar(10) | YES | | NULL | |
| college_name | varchar(300) | YES | | NULL | |
+--------------+--------------+------+-----+---------+-------+
4 rows in set (0.17 sec)
mysql> show create table t_stud_info \G ###查看表的详细信息
*************************** 1. row ***************************
Table: t_stud_info
Create Table: CREATE TABLE </code>t_stud_info<code> (
</code>stud_code<code> varchar(20) NOT NULL,
</code>stud_name<code> varchar(100) NOT NULL,
</code>stud_gend<code> varchar(10) DEFAULT NULL,
</code>college_name<code> varchar(300) DEFAULT NULL,
PRIMARY KEY (</code>stud_code<code>)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ###表的存储引擎、字符集。

 通过执行脚本创建表
如果一次要创建几十张表,把创建表的语句直接放在mysql命令行运行就不方便了,此时我们可以把创建这些表的SQL语句保存本地当前目录下,名为t_stud_info.sql的文件中,,具体内容请参看以下system cat t_stud_info.sql部分,然后用source命令在mysql命令下执行这个文件即可。这个source命令有点像shell命令。

mysql> system cat t_stud_info.sql; ###查看sql文件内容
DROP TABLE IF EXISTS t_stud_info;
CREATE TABLE t_stud_info
(
stud_code varchar(20) NOT NULL,
stud_name varchar(100) NOT NULL,
stud_gend varchar(10) ,
college_name varchar(300) NULL,
PRIMARY KEY (stud_code)
);
mysql> source t_stud_info.sql;
Query OK, 0 rows affected (0.03 sec)
mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| stud_info |
| stud_score |
| t_stud_info | ###表创建成功
+------------------+

除了以上两种方法外,还有其他方法,如通过客户端创建、通过模型来创建等等,这里就不展开来说了。

4.2 修改表结构

表创建好以后,有时间我们可能根据新的需求,需要修改字段类型、字段名称、添加字段、新建其它约束如索引、是否可空等。当表中无数据时做这些修改比较方便,如果表已有数据可能就需要慎重,否则可能导致修改失败,此时建议备份原表数据,然后清空数据,再做修改,修改后根据新的规则把数据导入新表中。但添加字段、放大自动长度等与是否有数据无关。

4.2.1 修改字段类型

如果发现创建的表的某个字段长度太小,需要放大其长度,该如何修改呢?我们可以使用ALTER TABLE 语句来修改。
【注意】如果是正式环境的数据,记得先备份,后修改,有备无患。
修改表字段类型的语法格式:
ALTER TABLE <表名> MODIFY <字段名> <字符类型>;
我们创建一张表test01 (a1 varchar(20),a2 int,a3 date),然后,修改a1字段的数据类型,由varchar(20)改为varchar(60)。具体操作如下:

mysql> CREATE TABLE test01 ###创建表test01
-> (
-> a1 varchar(20),
-> a2 int,
-> a3 date
-> );
Query OK, 0 rows affected (0.07 sec)

mysql> DESC test01; ###查看表结构
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| a1 | varchar(20) | YES | | NULL | |
| a2 | int(11) | YES | | NULL | |
| a3 | date | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

mysql> ALTER TABLE test01 MODIFY a1 varchar(60); ###修改字段a1类型
Query OK, 0 rows affected (0.24 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> DESC test01;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| a1 | varchar(60) | YES | | NULL | | ###修改成功
| a2 | int(11) | YES | | NULL | |
| a3 | date | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+

大家考虑一下,是否可以把INT修改为字符型,或把字符型修改整数型?如果要修改需要满足哪些条件?
除了可以修改字段类型,我们还可以修改表名称、字段名称、字段属性等。

4.2.2 修改字段名称

修改字段名称的语句与修改字段类型的不一样,其语法格式为:
ALTER TABLE <表名> CHANGE <旧字段名> <新字段名> <数据类型>;
现在我们把test01表中a1字段名称改为name,数据类型不变。

mysql> desc test01; ###查看表结构
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| a1 | varchar(60) | YES | | NULL | |
| a2 | int(11) | YES | | NULL | |
| a3 | date | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
mysql> ALTER TABLE test01 CHANGE a1 name varchar(60); ###把字段a1改为name
Query OK, 0 rows affected (0.38 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> desc test01;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| name | varchar(60) | YES | | NULL | | ###修改成功
| a2 | int(11) | YES | | NULL | |
| a3 | date | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+

4.2.3 新增字段

表创建后,根据需要我们可以修改表名、修改表中字段名称、字段类型,当然也可添加字段,而且可以更加指定位置的添加,如果没有指定,缺省是添加到最后。添加字段的语法格式为:
ALTER TABLE <表名> ADD <新增字段名> <字段类型> [字段约束条件][FIRST|AFTER 已有字段名];
我们还是以test01表为例,在name字段后,添加一个名为code的字段,数据类型为varchar(20),并且不能为空或not null。

mysql> ALTER TABLE test01 ADD code varchar(20) not null AFTER name;
Query OK, 0 rows affected (0.17 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> desc test01;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| name | varchar(60) | YES | | NULL | |
| code | varchar(20) | NO | | NULL | |
| a2 | int(11) | YES | | NULL | |
| a3 | date | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+

4.2.4 修改表的字符集

表的字符集或缺省字符集也能修改?能,这应该也是MySQL特色之一吧,一般数据库系统字符集与数据库绑定在一起,但MySQL把字符集粒度精确到了表甚至字段。虽然这个功能很强大,但也存在很大风险,特别是表中有数据时,可能导致字符类型不兼容问题,为降低风险,还是这句老话,先备份,后修改。
这里的修改一般指修改表的缺省字符集,常用的字符集有:UTF8、GBK、GB2312、latin1等,其中UTF-8用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24为(三个字节)来编码。UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。GBK是国家标准GB2312基础上扩容后兼容GB2312的标准。GBK的文字编码是用双字节来表示的,即不论中、英文字符均使用双字节来表示,为了区分中文,将其最高位都设定成1。GBK包含全部中文字符,是国家编码,通用性比UTF8差,不过UTF8占用的数据库比GBK大。
GBK、GB2312等与UTF8不兼容,需要通过Unicode编码才能相互转换:
GBK、GB2312--Unicode--UTF8
UTF8--Unicode--GBK、GB2312 。
字符集涉及面比较广,如服务器或数据库或表字符集、应用字符集(如连接字符集、文件字符集等)、客户端字符,一般这些环节的字符集需要一致或兼容,尤其对中文而言,否则可能导致乱码。如何解决乱码问题,后面我们也有介绍。
以下是修改表的字符集的一个简单实例:

mysql> show create table test01 ; ###查看表存储引擎、字符集等信息
+--------+---------------------------------------------------------------------| Table | Create Table
+--------+---------------------------------------------------------------------| test01 | CREATE TABLE </code>test01<code> (
</code>name<code> varchar(60) DEFAULT NULL,
</code>code<code> varchar(20) NOT NULL,
</code>a2<code> int(11) DEFAULT NULL,
</code>a3<code> date DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 | ###缺省字符集为utf8
----------------------------------+
mysql> ALTER TABLE test01 CONVERT TO CHARACTER SET gbk; ###修改缺省字符集为gbk
Query OK, 0 rows affected (0.12 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show create table test01 ; ###检查修改是否成功
+--------+---------------------------------------------------------------------| Table | Create Table
+--------+---------------------------------------------------------------------| test01 | CREATE TABLE </code>test01<code> (
</code>name<code> varchar(60) DEFAULT NULL,
</code>code<code> varchar(20) NOT NULL,
</code>a2<code> int(11) DEFAULT NULL,
</code>a3<code> date DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk | ###修改成功

修改方式或命令还有很多,大家可以借助其强大的help进一步获取其他命令的使用方法,如可以通过help ALTER TABLE 查询其他使用方法,这里就不在一一例举了。

4.3 插入数据

往表中插入数据,有多种方法,如通过SQL语句、客户端、数据备份工具等。
1)、通过SQL命令:insert into table_name (列1,列2,..) Values(‘’,’’,’’,….);
用这种方法需要注意列与值的对应关系;如果不指定列名,则指所有列:
如insert into table_name values(‘’,’’,’’,…),values(),
2)、通过客户端导入;
3)、利用工具(load data file或mysqlimport等)借助数据文件来导入数据。
这里我们介绍第1种方法,其它2种后面讲数据备份时将介绍。
往表test01插入一条记录。

mysql> select * from test01; ###查看表记录信息
Empty set (0.01 sec)

mysql> INSERT INTO test01(name,code,a2,a3)
-> VALUES('mysql','001',10,'2016-10-30'); ###插入1条记录
Query OK, 1 row affected (0.07 sec)

mysql> select * from test01;
+-------+------+------+------------+
| name | code | a2 | a3 |
+-------+------+------+------------+
| mysql | 001 | 10 | 2016-10-30 | ###插入成功
+-------+------+------+------------+

这是往插入1条记录,如果想一次往表插入多条记录如何实现呢?我们只要在后面添加个values即可,如同时往表中插入2条记录:

insert into table_name values(‘’,’’,’’,…) ,(‘’,’’,’’,…)

注意记录间用逗号。
具体操作请看下例

mysql> INSERT INTO test01(name,code,a2,a3) ###同时插入2条记录
-> VALUES('linux','002',20,'2016-10-30'),
-> ('spark','003',30,'2016-10-30');
Query OK, 2 rows affected (0.06 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select * from test01; ###检查结果,插入成功
+-------+------+------+------------+
| name | code | a2 | a3 |
+-------+------+------+------------+
| mysql | 001 | 10 | 2016-10-30 |
| linux | 002 | 20 | 2016-10-30 |
| spark | 003 | 30 | 2016-10-30 |
+-------+------+------+------------+

第5章 数据类型和运算符

前面介绍创建表结构、往表中插入记录时,涉及数据类型,数据类型对数据质量、性能提升、甚至业务的拓展都有一定关系,在日常使用中经常看到中途修改字段类型的问题,这将带来很大风险,因此,设置合理的数据类型非常重要,我们需要理解各种数据类型及其取值范围,同时也要注意数据类型间的异同。

5.1 数据类型介绍

MySQL支持多种类型,大致可以分为三类:数值、日期/时间和字符串类型。
 数值类型
包括整数类型(TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT),以及近似数值数据类型,浮点数据类型(FLOAT、DOUBLE)和定位数据类型(DECIMAL或DEC)。
 日期时间类型
日期和时间类型有DATETIME、DATE、TIMESTAMP、TIME和YEAR。
 字符串类型
字符串类型有CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT等。

5.1.1 数值类型

说明:
1、浮点数或定位数,如果实际值超出定义的精度范围,则采用四舍五入进行处理。处理时浮点数不提示,定位数会提示。
2、定位数据以字符串形式存储,如果对精度要求较高的科学计算、货币等,建议使用DECIMAL类型,浮点数进行比较时易出现问题。
3、FLOAT(M,D),其中M参数称为精度,是数据的总长度,即有效数字;D参数成为标度,是指小数点后的长度为D。如FLOAT(5,2)(如213.36)表示数据总长度为2,小数部分长度为2。

5.1.2 日期类型


说明:
1、DATETIME和TIMESTAMP虽然都是19位,但前者范围比后者大,具体请看上表。

5.1.3 字符类型


说明:
1、CHAR 把用户定义大小视为值的大小,不长度不足的情况下就用空格补足。而 VARCHAR 类型把它视为最大值并且只使用存储字符串实际需要的长度。
2、因为 VARCHAR 类型可以根据实际内容动态改变存储值的长度,所以在不能确定字段需要多少字符时使用 VARCHAR 类型可以大大地节约磁盘空间、提高存储效率。

5.2 运算符介绍

运算符连接表达式中各个操作数,其作用是用来指明对操作数所进行的运算。常见的运算有数学计算、比较运算、位运算以及逻辑运算。运用运算符可以更加灵活地使用表中的数据,常用的运算符类型有:算术运算符,比较运算符,逻辑运算符等。
 算术运算符
包括加(+)、减(-)、乘(*)、除(/)、求于(或称模运算,%)。
 比较运算符
包括大于(>)、小于(<)、等于(=)、大于等于(>=)、小于等于(<=)、不等于(!=)、以及IN、 BETWEEN AND、IS NULL、GREATEST、LEAST、LIKE、REGEXP等。  LIKE运算符在进行匹配时,可以使用下面两种通配符: 1.‘%’,匹配任何数目的字符,甚至包括0字符。 2.. ‘_’,只能匹配一个字符。 REGEXP运算符在进行匹配时,常用的有下面几种通配符: 1.‘^’匹配以该字符后面的字符开头的字符串。 2.‘$’匹配以该字符后面的字符结尾的字符串。 3.‘.’匹配任何一个单字符。 4.‘[…]’匹配在方括号内的任何字符。例如,”[abc]”匹配”a”、”b”或”c”。 为了命名字符串的范围,使用一个’-‘。”[a-z]”匹配任何字母,而”[0-9]” 匹配任何数字。 5.‘*’匹配0个或多个在它前面的字符。  逻辑运算符 逻辑非(NOT或者!)、逻辑与(AND或者&&)、逻辑或(OR或者||)、逻辑异或(XOR)。 以下通过一些实例来加深理解:

 mysql> SELECT ISNULL(NULL),ISNULL(12),12 IS NOT NULL;
+--------------+------------+----------------+
| ISNULL(NULL) | ISNULL(12) | 12 IS NOT NULL |
+--------------+------------+----------------+
| 1 | 0 | 1 |
+--------------+------------+----------------+
1 row in set (0.06 sec)

mysql> SELECT 5 BETWEEN 4 AND 5;
+-------------------+
| 5 BETWEEN 4 AND 5 |
+-------------------+
| 1 |
+-------------------+
1 row in set (0.04 sec)
mysql> SELECT LEAST(2,3,5),LEAST('a','b','c'),LEAST(2,NULL),GREATEST(2,NULL);
+--------------+--------------------+---------------+------------------+
| LEAST(2,3,5) | LEAST('a','b','c') | LEAST(2,NULL) | GREATEST(2,NULL) |
+--------------+--------------------+---------------+------------------+
| 2 | a | NULL | NULL |
+--------------+--------------------+---------------+------------------+
mysql> SELECT 'ipython'like '%thon','sql'like '__l','ok'like NULL;
+-----------------------+-----------------+---------------+
| 'ipython'like '%thon' | 'sql'like '__l' | 'ok'like NULL |
+-----------------------+-----------------+---------------+
| 1 | 1 | NULL |
+-----------------------+-----------------+---------------+
1 row in set (0.00 sec)
mysql> SELECT 'spark'REGEXP '^s','10$'REGEXP ''</span><span class=,'spark'REGEXP '[abc]';
+--------------------+------------------+-----------------------+
| 'spark'REGEXP '^s' | '10$'REGEXP '" />'
| 'spark'REGEXP '[abc]' |
+--------------------+------------------+-----------------------+
| 1 | 1 | 1 |
+--------------------+------------------+-----------------------+

第6章 查询数据

数据库主要功能是存储数据,但存储数据不是最终目的,存储数据最终目的是为了展示和分析,如何分析展示数据库中数据,数据查询就是重要手段。MySQL提供了功能强大、又非常灵活、非常方便的语句实现这些操作。这一章将介绍使用SELECT语句实现简单查询、子查询、连接查询、分组查询及利用正则表达式查询等。

6.1 一般查询语句

最简单的是SELECT [列名]FROM [表名] WHERE [条件] 。然后你可以在后面加上像[LIMIT][ORDER BY][GROUP BY][HAVING]等。
[列名]: 可以多个字段(列间用逗号分隔),也可所以字段(一般用*表示所有字段)
[表名]: 可以是一个表名或视图名,也可以是多表或多视图(表间用逗号分隔)。
[条件]: 为可选项,如果选择该项,将限制行必须满足的查询条件。
[LIMIT]: 后跟[位置偏移量,] 行数 (第1行的位置偏移量为0,第2行为1,以此类推。)
[ORDER BY]: 后跟字段,可一个或多个,根据这些字段进行分组。
[GROUP BY]: 后跟可一个或多个字段,根据这些字段进行排序,升序(ASC)或降序(DESC)。
其后也可跟WITH ROLLUP,增加一条合计记录。
[HAVING]: 一般与GROUP BY一起使用,用来显示满足条件的分组记录。

6.2 单表查询

单表查询就是从1张表中查询数据,后续将介绍多表查询。为查询表数据我们需要先做些准备工作。

6.2.1 准备工作

准备工作包括:1)、定义表结构,创建表;2)、查看分析数据文件;3).把数据导入到表中。
1).首先我们创建一个存储学生各科成绩的表(stud_score),表的定义如下:
(表6-1 学生成绩表 stud_score)

转换为建表的SQL语句为:

DROP TABLE IF EXISTS stud_score;
CREATE TABLE stud_score (
stud_code varchar(20) NOT NULL,
sub_code varchar(20) NOT NULL,
sub_name varchar(100) default NULL,
sub_tech varchar(20) default NULL COMMENT '教师代码',
sub_score smallint(10) default NULL,
stat_date date default NULL,
PRIMARY KEY (stud_code,sub_code)
);

2)、创建这个表以后,我们需要把一个包含该表数据的文件(在slave02节点的/tmp目录下,名称为stud_score.csv)导入该表,另该文件第1行为字段名称,需过滤掉。我们先操作,具体语法等我们后续会介绍。
查看该数据文件信息

hadoop@master:/tmp$ pwd
/tmp
hadoop@master:/tmp$ ls -l |grep 'stud*'
-rw-rw-r-- 1 feigu feigu 1508 Jul 6 15:47 stud_info.csv
-rw-rw-rw- 1 mysql mysql 7157 Jul 2 11:32 stud_score_0702.txt
-rw-rw-rw- 1 mysql mysql 7157 Jul 2 21:43 stud_score_0703.txt
-rw-rw-r-- 1 feigu feigu 4652 Jul 2 10:26 stud_score.csv
hadoop@master:/tmp$ head -2 stud_score.csv
stud_code,sub_code,sub_nmae,sub_tech,sub_score,stat_date
2015101000,10101,数学分析,,90,
hadoop@master:/tmp$ cat stud_score.csv |wc -l ###查看文件记录总数
122

3)、把数据文件导入表中

mysql> DROP TABLE IF EXISTS stud_score; ###判断是否存在,存在则删除
Query OK, 0 rows affected (0.36 sec)

mysql> CREATE TABLE stud_score ( ###创建表
-> stud_code varchar(20) NOT NULL,
-> sub_code varchar(20) NOT NULL,
-> sub_name varchar(100) default NULL,
-> sub_tech varchar(20) default NULL COMMENT '教师代码',
-> sub_score smallint(10) default NULL,
-> stat_date date default NULL,
-> PRIMARY KEY (stud_code,sub_code)
-> );
Query OK, 0 rows affected (0.07 sec)
mysql> select * from stud_score; ###查看是否有数据
Empty set (0.00 sec)
mysql> load data infile '/tmp/stud_score.csv' into table stud_score fields terminated by "," ignore 1 lines; ###把数据导入到表中
Query OK, 121 rows affected, 121 warnings (0.25 sec)
Records: 121 Deleted: 0 Skipped: 0 Warnings: 121
mysql> select count(*) from stud_score; ###查看验证记录总数
+----------+
| count(*) |
+----------+
| 121 |
+----------+
1 row in set (0.07 sec)

6.2.2 查看指定行数据

mysql> SELECT * FROM stud_score where sub_name='高等代数'; ###查看指定学科
+------------+----------+--------------+----------+-----------+------------+
| stud_code | sub_code | sub_name | sub_tech | sub_score | stat_date |
+------------+----------+--------------+----------+-----------+------------+
| 2015101000 | 10102 | 高等代数 | | 88 | 0000-00-00 |
| 2015101001 | 10102 | 高等代数 | | 78 | 0000-00-00 |
| 2015101002 | 10102 | 高等代数 | | 97 | 0000-00-00 |
| 2015101003 | 10102 | 高等代数 | | 87 | 0000-00-00 |
| 2015101004 | 10102 | 高等代数 | | 77 | 0000-00-00 |
| 2015101005 | 10102 | 高等代数 | | 65 | 0000-00-00 |
| 2015101006 | 10102 | 高等代数 | | 68 | 0000-00-00 |
| 2015101007 | 10102 | 高等代数 | | 80 | 0000-00-00 |
| 2015101008 | 10102 | 高等代数 | | 96 | 0000-00-00 |
| 2015101009 | 10102 | 高等代数 | | 79 | 0000-00-00 |
| 2015101010 | 10102 | 高等代数 | | 52 | 0000-00-00 |
+------------+----------+--------------+----------+-----------+------------+
mysql> SELECT * FROM stud_score LIMIT 3; ###查看前3行
+------------+----------+--------------+----------+-----------+------------+
| stud_code | sub_code | sub_name | sub_tech | sub_score | stat_date |
+------------+----------+--------------+----------+-----------+------------+
| 2015101000 | 10101 | 数学分析 | | 90 | 0000-00-00 |
| 2015101000 | 10102 | 高等代数 | | 88 | 0000-00-00 |
| 2015101000 | 10103 | 大学物理 | | 67 | 0000-00-00 |
+------------+----------+--------------+----------+-----------+------------+
3 rows in set (0.00 sec)

mysql> SELECT * FROM stud_score LIMIT 2, 2; ###查看第3行开始后2行
+------------+----------+-----------------+----------+-----------+------------+
| stud_code | sub_code | sub_name | sub_tech | sub_score | stat_date |
+------------+----------+-----------------+----------+-----------+------------+
| 2015101000 | 10103 | 大学物理 | | 67 | 0000-00-00 |
| 2015101000 | 10104 | 计算机原理 | | 78 | 0000-00-00 |
+------------+----------+-----------------+----------+-----------+------------+
2 rows in set (0.00 sec)

6.2.3 模糊查询

利用LIKE 关键字可以进行模糊查询。下例查询学科代码以101开头的记录总数。

mysql> SELECT count(*) FROM stud_score WHERE sub_code LIKE '101%';
+----------+
| count(*) |
+----------+
| 55 |
+----------+
mysql>select * from stud_info where birthday REGEXP '^1998' ;
mysql> select * from stud_info where birthday REGEXP '10$' ;

6.2.4 分组查询

如果我们希望按学科来统计学生成绩、班级成绩该如何实现呢?这就涉及到分组统计问题,如果需要按学科统计,可以GROUP BY sub_code;然后取前3名学科,具体实现请看下例:

mysql> SELECT sub_code,SUM(sub_score) from stud_score GROUP BY sub_code;
+----------+----------------+
| sub_code | SUM(sub_score) |
+----------+----------------+
| 10101 | 863 |
| 10102 | 867 |
| 10103 | 830 |
| 10104 | 932 |
| 10105 | 857 |
| 20101 | 870 |
| 20102 | 892 |
| 20103 | 866 |
| 20104 | 864 |
| 20105 | 822 |
| 20106 | 843 |
+----------+----------------+
11 rows in set (0.00 sec)
mysql> SELECT sub_code,SUM(sub_score) AS sum_score from stud_score GROUP BY sub_code
-> ORDER BY sum_score DESC LIMIT 3; ###按学科总分排序,取前3。
+----------+-----------+
| sub_code | sum_score |
+----------+-----------+
| 10104 | 932 |
| 20102 | 892 |
| 20101 | 870 |
+----------+-----------+

6.3 多表查询

多表查询,需要连接2张或2张以上的表一起查询,连接有多种方式,如内连接(通常缺省连接)、外链接(又分为左连接,右连接)等。多表连接时,建议不宜一下连接很多表,尤其是数据比较大时,可以采用两两连接等方式。
要实现多表连接,还需创建一个存储学生基本信息的表(stud_info)并导入数据.

6.3.1 准备工作

创建一个学生基本信息的表(表定义如下),并把数据(一个数据文件)导入到表中。
准备步骤,1).定义并创建表,2),查看分析数据文件,3). 导入数据,并验证导入结果。
1).定义并创建表

mysql> CREATE TABLE stud_info (
-> stud_code varchar(20) NOT NULL,
-> stud_name varchar(100) NOT NULL,
-> stud_sex char(1) NOT NULL default 'M' COMMENT '性别',
-> birthday date default NULL,
-> log_date date default NULL,
-> orig_addr varchar(100) default NULL,
-> lev_date date default NULL,
-> college_code varchar(10) default NULL COMMENT '学院编码',
-> college_name varchar(100) default NULL,
-> state varchar(10) default NULL,
-> PRIMARY KEY (stud_code)
-> ) ;
Query OK, 0 rows affected (0.05 sec)

2).查看分析数据文件

hadoop@master:/tmp$ ls -l|grep 'stud_info*'
-rw-rw-r-- 1 feigu feigu 1508 Jul 6 15:47 stud_info.csv
hadoop@master:/tmp$ cat stud_info.csv|wc -l ###查看文件记录总数
23
hadoop@master:/tmp$ head -3 stud_info.csv ###查看前3行
stud_code,stud_name,stud_gend,birthday,log_date,orig_addr,lev_date,college_code,college_name,state
2015101000,王进,M,1997/8/1,2014/9/1,苏州,,10,理学院,1
2015101001,刘海,M,1997/9/29,2014/9/1,上海,,10,理学院,1

3).导入数据并验证结果

mysql> select count(*) from stud_info; ###查看表数据
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)
mysql> load data infile '/tmp/stud_info.csv' into table stud_info fields terminated by "," ignore 1 lines;
Query OK, 22 rows affected, 22 warnings (0.05 sec)
Records: 22 Deleted: 0 Skipped: 0 Warnings: 22
mysql> select count(*) from stud_info; ###成功导入22条记录
+----------+
| count(*) |
+----------+
| 22 |
+----------+
1 row in set (0.00 sec)

6.3.2 多表连接查询

两表内连接、左连接,右连接的含义及使用关键字请参看下图:
(图6-1 多表连接示意图)

以下通过几个实例进一步说明多表连接的使用方法:

mysql> SELECT a.stud_name,b.sub_name,b.sub_score FROM stud_info a,stud_score b WHERE a.stud_code=b.stud_code LIMIT 3; ###内连接,a定义为stud_info表的别名。
+-----------+--------------+-----------+
| stud_name | sub_name | sub_score |
+-----------+--------------+-----------+
| 王进 | 数学分析 | 90 |
| 王进 | 高等代数 | 88 |
| 王进 | 大学物理 | 67 |
+-----------+--------------+-----------+
mysql> SELECT a.stud_name,b.sub_name,b.sub_score FROM stud_info a LEFT OUTER JOIN stud_score b ON a.stud_code=b.stud_code LIMIT 3; ###左连接
+-----------+--------------+-----------+
| stud_name | sub_name | sub_score |
+-----------+--------------+-----------+
| 王进 | 数学分析 | 90 |
| 王进 | 高等代数 | 88 |
| 王进 | 大学物理 | 67 |
+-----------+--------------+-----------+
3 rows in set (0.00 sec)
mysql> SELECT a.stud_name,b.sub_name,b.sub_score FROM stud_info a RIGHT OUTER JOIN stud_score b ON a.stud_code=b.stud_code LIMIT 3; ###右连接
+-----------+--------------+-----------+
| stud_name | sub_name | sub_score |
+-----------+--------------+-----------+
| 王进 | 数学分析 | 90 |
| 王进 | 高等代数 | 88 |
| 王进 | 大学物理 | 67 |
+-----------+--------------+-----------+
3 rows in set (0.00 sec)

6.3.3 子连接查询

这里介绍以用IN关键字实现子链接,基本格式为:
SELECT * FROM 表名WHERE 字段 IN (SELECT 字段 FROM 表名 WHERE 条件);
使用子查询,比较灵活,且有利于把大表关联转换为小表关联。下例为表stud_info中
stud_code从表stud_score中成绩为大于或等于90分的子查询中获取。

mysql> SELECT stud_name FROM stud_info WHERE stud_code IN(SELECT stud_code FROM stud_score WHERE sub_score>=90 ) LIMIT 3;
+-----------+
| stud_name |
+-----------+
| 王进 |
| 刘海 |
| 张飞 |
+-----------+
3 rows in set (0.02 sec)

6.3.4 视图查询

上面的查询语句,有的比较简单,有的比较复杂,像对那些复杂的查询语句,包含了很多信息量,而且有可能还要经常使用,但命令行是无法保证这些语句的,如果下次还要使用是否又重新写一遍呢?大可不必,我们可以把这个查询语句以视图的形式保存到数据库中,然后直接查询这个视图即可。
如上面内连接的SQL语句:SELECT a.stud_name,b.sub_name,b.sub_score FROM stud_info a,stud_score b WHERE a.stud_code=b.stud_code LIMIT 3;
我们可以把它定义为一个视图V_STUD,然后查询视图即可,非常方便!而且视图会保存到数据库中,但视图本身不保存数据。具体实现请看下例:

mysql> CREATE VIEW V_STUD AS SELECT a.stud_name,b.sub_name,b.sub_score FROM stud_info a,stud_score b WHERE a.stud_code=b.stud_code LIMIT 3; ###创建视图
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM V_STUD; ###查询视图
+-----------+--------------+-----------+
| stud_name | sub_name | sub_score |
+-----------+--------------+-----------+
| 王进 | 数学分析 | 90 |
| 王进 | 高等代数 | 88 |
| 王进 | 大学物理 | 67 |
+-----------+--------------+-----------+
3 rows in set (0.00 sec)

第7章 增删改数据

存储数据时用来查询分析用的,所以查询分析数据是平时重要任务,但数据库中数据它不会自动生成,需要我们去维护,当然大部分是系统自动维护,不需要手工去操作,不过维护程序还是需要写的,这章我们介绍如何维护数据库数据。这里我们主要介绍如何新增数据、如何修改数据、如何删除数据等。

7.1 插入数据

插入数据语句的语法:
INSERT INTO 表名[(列名1,……列名n)] values(值1,…..值n);
这个SQL语句一次往表中插入1条记录,如果一次要插入多条记录是否可以呢?可以,而且很方便,插入多条语句为:
INSERT INTO 表名[(列名1,……列名n)] VALUES(值1,…..值n), (值1,…..值n),..;
下面我们还是通过一些实例来进步说明如何操作。

mysql> CREATE TABLE test03 (id INT NOT NULL,name VARCHAR(20),birthday DATE);
Query OK, 0 rows affected (0.17 sec)

mysql> DESC TEST03; ###查看表结构
ERROR 1146 (42S02): Table 'testdb.TEST03' doesn't exist
mysql> DESC test03;
+----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| id | int(11) | NO | | NULL | |
| name | varchar(20) | YES | | NULL | |
| birthday | date | YES | | NULL | |
+----------+-------------+------+-----+---------+-------+
3 rows in set (0.04 sec)

mysql> INSERT INTO test03 VALUES(100,'
张华', '2000-01-01'); ##对所有字段插值
Query OK, 1 row affected (0.16 sec)

mysql> INSERT INTO test03(id,name) VALUES(200,'
刘婷'); ###选择字段插值
Query OK, 1 row affected (0.13 sec)

mysql> SELECT * FROM test03;
+-----+--------+------------+
| id | name | birthday |
+-----+--------+------------+
| 100 | 张华 | 2000-01-01 |
| 200 | 刘婷 | NULL |
+-----+--------+------------+
2 rows in set (0.00 sec)
mysql> INSERT INTO test03(id,name) VALUES(300,'
貂蝉'),(400,'吕布'); ##插入多条
Query OK, 2 rows affected (0.03 sec)
Records: 2 Duplicates: 0 Warnings: 0

7.2 修改数据

修改数据也是很常见的,不过修改数据前,记得备份数据。如何备份数据后面将介绍。
修改数据的一般语法:
UPDATE 表名 SET 列名1=值1,….列名n=值n
[WHERE 条件];
以下以修改id为200对应的name为例,假设发现id为200对应的name输错了,不是刘婷,而是刘涛。

mysql> UPDATE test03 SET name='刘涛' WHERE id=200; ###修改id对应name值
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM test03 WHERE id=200; ###查询验证结果
+-----+--------+----------+
| id | name | birthday |
+-----+--------+----------+
| 200 | 刘涛 | NULL | ###修改成功
+-----+--------+----------+
1 row in set (0.00 sec)

7.3 删除数据

删除数据时,我们可以选择删除几条,或满足某些条件的记录,当然也可以删除所有记录。在日常工作中,删除数据需要非常谨慎,务必养成一个良好习惯,先备份,后删除。对生产环境数据、或正式环境的数据,不建议物理删除,最好采用逻辑删除的方式(即修改对应记录的状态或有效时间等)。数据都有价值。
用DELETE删除数据的一般语法:
DELETE FROM 表名 [WHERE 条件];
DELETE 加上条件,就可以有条件删除记录;如果没有条件,将删除所有数据。删除所有数据也可用TRUNCATE命令(这种方式删除数据比较快),其命令格式为:
TRUNCATE [table] 表名;
使用DELETE FROM 表名或TRUNCATE [table] 表名命令删除全部记录时,有一种情况需要注意,如果一个表中有自增字段,这个自增字段将起始值恢复成1.如果你不想这样做的话,可以在DELETE语句中加上永真的WHERE,如WHERE 1或WHERE true。

mysql> SELECT * FROM test03;
+-----+--------+------------+
| id | name | birthday |
+-----+--------+------------+
| 100 | 张华 | 2000-01-01 |
| 200 | 刘涛 | NULL |
| 300 | 貂蝉 | NULL |
| 400 | 吕布 | NULL |
+-----+--------+------------+
4 rows in set (0.00 sec)

mysql> DELETE FROM test03 WHERE (id=300 or id=400);
Query OK, 2 rows affected (0.02 sec)

mysql> SELECT * FROM test03;
+-----+--------+------------+
| id | name | birthday |
+-----+--------+------------+
| 100 | 张华 | 2000-01-01 |
| 200 | 刘涛 | NULL |
+-----+--------+------------+
2 rows in set (0.00 sec)

mysql> DELETE FROM test03;
Query OK, 2 rows affected (0.01 sec)

mysql> SELECT * FROM test03;
Empty set (0.00 sec)

7.4 删除含自增字段的表

CREATE TABLE person
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name CHAR(40) NOT NULL DEFAULT '',
age INT NOT NULL DEFAULT 0,
info CHAR(50) NULL,
PRIMARY KEY (id)
);

【1】在person表中,插入一条新记录,列的顺序与表一致,SQL语句如下:
INSERT INTO person (id ,name, age , info) VALUES (1,'Green', 21, 'Lawyer');

【2】在person表中,插入一条新记录,列顺序与表不一致,SQL语句如下:
INSERT INTO person (age ,name, id , info)
VALUES (22, 'Suse', 2, 'dancer');

【3】在person表中,插入一条新记录,不写列名,SQL语句如下:
INSERT INTO person VALUES (3,'Mary', 24, 'Musician');

【4】在person表中,插入一条新记录,不写自增字段,SQL语句如下:
INSERT INTO person (name, age,info) VALUES('Willam', 20, 'sports man');

【5】在person表中,插入一条新记录,不写id、age,SQL语句如下:
INSERT INTO person (name, info ) VALUES ('Laura', 'teacher');

【6】在person表中,在name、age和info字段指定插入值,同时插入3条新记录,SQL语句如下:

INSERT INTO person(name, age, info)
VALUES ('Evans',27, 'secretary'),
('Dale',22, 'cook'),
('Edison',28, 'singer');

【7】在person表中,不指定插入列表,同时插入2条新记录,SQL语句如下:
INSERT INTO person
VALUES (9,'Harry',21, 'magician'),
(NULL,'Harriet',19, 'pianist');
【8】在person表中,delete 全表然后看自增字段的值,SQL语句如下
show table status like 'person';
delete from person;
truncate table person;
show table status like 'person';
修改自动增长字段起始值
alter table person auto_increment = 100 ;

INSERT INTO person (name, age,info) VALUES('Willam', 20, 'sports man');
select * from person;

7.5 事务控制

在mysql中不同存储引擎对锁的粒度不同,InnoDB可到行级,MyISAM和MEMORY可到表级,BDB可到页级,下面以InnoDB引擎为例,说明MySQL的锁及事务控制。
 给表加锁
session1 session2
给表加锁

 事务控制实例:

 事务控制及回滚

第8章 MySQL函数

MySQL数据库自身带有很多函数,合理使用这些函数能有效提高我们的编程效率和编程质量,可以直接使用数据库自带函数(或称为内置函数),当然我们也可以根据自己需要自定义函数。自定义函数(UDF)后面将介绍。

8.1 函数简介

MySQL提供了大量函数,具体可为:数学函数、字符串函数、日期及时间函数、聚合函数、条件判断函数、系统信息函数等等。这些函数极大丰富和增强了对数据的处理和分析能力。下面我们就一些常用的函数进行说明,大家也可通过help或网络查看MySQL函数的一些使用方法。

8.2 数学函数

ABS(x)
返回x的绝对值
MOD(x,y)
返回x/y的模(余数)
RAND()
返回0到1内的随机值,可以通过提供一个参数(种子),如RAND(N)使RAND() 随机数生成器生成一个指定的值。
SQRT(x)
返回一个数的平方根。
ROUND(X)
返回参数X的四舍五入的一个整数。

mysql> SELECT ABS(10),ABS(-20);
+---------+----------+
| ABS(10) | ABS(-20) |
+---------+----------+
| 10 | 20 |
+---------+----------+
1 row in set (0.00 sec)

mysql> SELECT MOD(10,3),RAND(),RAND(),RAND(2),RAND(2);
+-----------+--------------------+---------------------+--------------------+
| MOD(10,3) | RAND() | RAND() | RAND(2) | RAND(2) |
+-----------+--------------------+---------------------+--------------------+
| 1 | 0.8742189052274627 | 0.21504458060026596 | 0.6555866465490187 | 0.6555866465490187 |
+-----------+--------------------+---------------------+--------------------+
1 row in set (0.00 sec)
mysql> SELECT SQRT(4),SQRT(5),ROUND(SQRT(5));
+---------+------------------+----------------+
| SQRT(4) | SQRT(5) | ROUND(SQRT(5)) |
+---------+------------------+----------------+
| 2 | 2.23606797749979 | 2 |
+---------+------------------+----------------+

8.3 字符串函数

CONCAT(str1,str2,...)
返回来自于参数连结的字符串。如果任何参数是NULL,返回NULL。可以 有超过2个的参数。一个数字参数被变换为等价的字符串形式。
LOWER(str)
返回将字符串str中所有字符改变为小写后的结果
LEFT(str,x)
返回字符串str中最左边的x个字符
LENGTH(s)
返回字符串str中的字符数
LTRIM(str)
从字符串str中切掉开头的空格
LOCATE(substr,str)
返回子串substr在字符串str中第一次出现的位置
REVERSE(str)
返回颠倒字符串str的结果
RIGHT(str,x)
返回字符串str中最右边的x个字符
RTRIM(str)
返回字符串str尾部的空格
STRCMP(s1,s2)
比较字符串s1和s2
TRIM(str)
去除字符串首部和尾部的所有空格
SBUSTR(str,pos)
就是从pos开始的位置,一直截取到最后。
SUBSTR(str,pos,len)
就是从pos开始的位置,截取len个字符(空白也算字符)
UPPER(str)
返回将字符串str中所有字符转变为大写后的结果

mysql> SELECT CONCAT('My','SQL');
+--------------------+
| CONCAT('My','SQL') |
+--------------------+
| MySQL |
+--------------------+
mysql> SELECT LOCATE('SQL','Spark SQL');
+---------------------------+
| LOCATE('SQL','Spark SQL') |
+---------------------------+
| 7 |
+---------------------------+
mysql> SELECT TRIM(' Python ');
+--------------------+
| TRIM(' Python ') |
+--------------------+
| Python |
+--------------------+
mysql> SELECT SUBSTR('Hadoop V2.8',8),SUBSTR('Hadoop V2.8',6,4),SUBSTR('Hadoop V2.8',-4,4);
+-------------------------+---------------------------+-----------------------
| SUBSTR('Hadoop V2.8',8) | SUBSTR('Hadoop V2.8',6,4) | SUBSTR('Hadoop V2.8',-4,4)|
+-------------------------+---------------------------+-----------------------
| V2.8 | p V2 | V2.8
+-------------------------+---------------------------+------------------------
mysql> select stud_name,substr(stud_name,2,1),length(stud_name),char_length(stud_name) from stud_info;
+-----------+-----------------------+-------------------+------------------------+
| stud_name | substr(stud_name,2,1) | length(stud_name) | char_length(stud_name) |
+-----------+-----------------------+-------------------+------------------------+
| 王进 | 进 | 6 | 2 |
| 刘海 | 海 | 6 | 2 |
| 张飞 | 飞 | 6 | 2 |
| 刘婷 | 婷 | 6 | 2 |
| 卢家 | 家 | 6 | 2 |
| 韩林 | 林 | 6 | 2 |
| 张景和 | 景 | 9 | 3 |
| 刘芳菲 | 芳 | 9 | 3 |

8.4 日期和时间函数

CURRENT_DATE()
返回当前的日期
CURRENT_TIME()
返回当前的时间
NOW()
返回当前的日期和时间
DATE_ADD(date,INTERVAL int keyword)
返回日期date加上间隔时间int的结果(int必须按 照关键字进行格式化), 如:SELECTDATE_ADD(CURRENT_DATE,INTERVAL 6 MONTH);
DATE_FORMAT(date,fmt)
依照指定的fmt格式格式化日期date值

mysql> SELECT CURRENT_DATE(),CURRENT_TIME(),NOW();
+----------------+----------------+---------------------+
| CURRENT_DATE() | CURRENT_TIME() | NOW() |
+----------------+----------------+---------------------+
| 2016-11-01 | 15:06:57 | 2016-11-01 15:06:57 |
+----------------+----------------+---------------------+
mysql> SELECT CURRENT_DATE,DATE_ADD(CURRENT_DATE,INTERVAL 5 DAY);
+--------------+---------------------------------------+
| CURRENT_DATE | DATE_ADD(CURRENT_DATE,INTERVAL 5 DAY) |
+--------------+---------------------------------------+
| 2016-11-01 | 2016-11-06 |
+--------------+---------------------------------------+
mysql> SELECT DATE_FORMAT(CURRENT_DATE,'%Y %m %d');
+--------------------------------------+
| DATE_FORMAT(CURRENT_DATE,'%Y %m %d') |
+--------------------------------------+
| 2016 11 01 |
+--------------------------------------+

8.5 聚合函数

什么叫聚合计算?举个简单例子大家就明白了,假如要统计一个班男女同学各多少,这个问题就是聚合计算,聚合统计实际上就是分组计算或分组统计。以下通过图形来说明根据课程来分组聚合计算课程成绩的详细过程:


(图 8-1 按课程分组与聚合成绩的计算过程)

数据库如何进行聚合计算?非常方便,MySQL提供了现成的语句,即GROUP BY,具体格式为:
SELECT 聚合函数(如COUNT、SUM等) FROM 表名称 GROUP BY 分组字段(可以1个或多个)。
常用于GROUP BY从句的SELECT查询中。
AVG(col) 返回指定列的平均值
COUNT(col) 返回指定列中非NULL值的个数
MIN(col) 返回指定列的最小值
MAX(col) 返回指定列的最大值
SUM(col) 返回指定列的所有值之和

mysql> SELECT sub_name,AVG(sub_score),SUM(sub_score),COUNT(sub_score) FROM stud_score GROUP BY sub_code;
+--------------------------+----------------+----------------+------------------+
| sub_name | AVG(sub_score) | SUM(sub_score) | COUNT(sub_score) |
+--------------------------+----------------+----------------+------------------+
| 数学分析 | 78.4545 | 863 | 11 |
| 高等代数 | 78.8182 | 867 | 11 |
| 大学物理 | 75.4545 | 830 | 11 |
| 计算机原理 | 84.7273 | 932 | 11 |
+--------------------------+----------------+----------------+------------------+

8.6 条件判断函数

IFNULL(expr1,expr2)
如果expr1不是NULL,IFNULL()返回expr1,否则它返回expr2。
IF(expr1,expr2,expr3)
如果expr1是TRUE(expr1<>0且expr1<>NULL),那么返回expr2,否则它 返回expr3。
CASE value WHEN [compare-value] THEN result [WHEN [compare-value] THEN result ...] [ELSE result] END
CASE WHEN [condition] THEN result [WHEN [condition] THEN result ...] [ELSE result] END
如果第一个条件为真,返回result。如果没有匹配的result值,那么结 果在ELSE后的result被返回。如果没有ELSE部分,那么NULL被返回。

mysql> SELECT IFNULL(1,2),IFNULL(NULL,2);
+-------------+----------------+
| IFNULL(1,2) | IFNULL(NULL,2) |
+-------------+----------------+
| 1 | 2 |
+-------------+----------------+
mysql> SELECT IF(2>1,2,1),IF(2<1,2,1),IF(TRUE,10,100); +-------------+-------------+-----------------+ | IF(2>1,2,1) | IF(2<1,2,1) | IF(TRUE,10,100) | +-------------+-------------+-----------------+ | 2 | 1 | 10 | +-------------+-------------+-----------------+ mysql> SELECT sub_name,
-> CASE
-> WHEN AVG(sub_score)>=90 THEN '优'
-> WHEN AVG(sub_score)>=80 AND AVG(sub_score) ELSE '合格'END as level
-> FROM stud_score GROUP BY sub_code
-> ORDER BY AVG(sub_score) DESC
-> LIMIT 5;
+--------------------------+--------+
| sub_name | level |
+--------------------------+--------+
| 计算机原理 | 良 |
| 计算机系统结构 | 良 |
| 计算机软件与理论 | 合格 |
| 高等代数 | 合格 |
| 操作系统 | 合格 |
+--------------------------+--------+
5 rows in set (0.09 sec)

8.7 系统信息函数

DATABASE() 返回当前数据库名
CONNECTION_ID() 返回当前客户的连接ID
FOUND_ROWS() 返回最后一个SELECT查询进行检索的总行数
USER() 返回当前登陆用户名
VERSION() 返回MySQL服务器的版本

mysql> SELECT FOUND_ROWS(),USER(),VERSION() ;
+--------------+-----------------------+-------------------------+
| FOUND_ROWS() | USER() | VERSION() |
+--------------+-----------------------+-------------------------+
| 5 | feigu@master | 5.5.40-0ubuntu0.14.04.1 |
+--------------+-----------------------+-------------------------+

第9章 存储过程与函数

存储过程或函数,有啥作用?如果我们平时只是使用一些简单SQL,可能不需要用到它们,如果那天你需要编写一个多功能、比较复杂的SQL,它们的作用就不可小看了。特别是一些功能需要提交到生产环境,那就更离不开它们了。这个有点像shell中命令与脚本一样。脚本可以把很多命令写到一个文件中,而且这个脚本可以放到其他服务器,可以定时执行等等,非常方便。
实际上MySQL的存储过程或函数还有些特殊有点。我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。而且执行都在数据库中,可减少数据在数据库与应用服务间的传输,既可节省网络资源,又可提高运行效率。
存储过程与函数的区别可从以下几个方面来比较:
返回值:存储过程没有,但函数必须有。
参数: 存储过程有IN,OUT,INOUT类型,函数只有IN类型。
调用: CALL 存储过程名(参数),SELECT 函数名(参数)

9.1 创建存储过程或函数

创建或修改存储过程或函数的语法如下:
CREATE PROCEDURE 过程名 ([过程参数[,...]])
[特性 ...] 过程体
CREATE FUNCTION 函数名称([参数列表[,..]])
RETURNS 返回值类型
以下存储过程输入学生代码,统计该学生所选课程总数,并输出。

mysql> DELIMITER // ##定义存储过程结束符为://
mysql> DROP PROCEDURE if EXISTS p_stat_score ;
-> CREATE PROCEDURE p_stat_score(IN i_stud_code VARCHAR(20),OUT o_count INT)
-> BEGIN
-> SELECT count(*) INTO o_count FROM stud_score WHERE stud_code =i_stud_code;
-> END//
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> DELIMITER ; ###恢复结束符
mysql> CALL p_stat_score(2015101000,@count); ###执行存储过程
Query OK, 1 row affected (0.00 sec)

mysql> SELECT @count;
+--------+
| @count |
+--------+
| 5 |
+--------+

上面这个存储过程的功能,如何用函数来实现呢?

mysql> DROP FUNCTION if EXISTS f_stat_score ;
-> CREATE FUNCTION f_stat_score(i_stud_code VARCHAR(20))
-> RETURNS INT
-> BEGIN
-> DECLARE o_count INT; ###定义保存返回值变量
-> set @s1=1; ###定义用户变量,用来调试
-> SELECT count(*) INTO o_count FROM stud_score WHERE stud_code =i_stud_code;
-> set @s2=2;
-> RETURN o_count;
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql> SELECT f_stat_score(2015101000);
+--------------------------+
| f_stat_score(2015101000) |
+--------------------------+
| 5 |
+--------------------------+
1 row in set (0.00 sec)

mysql> SELECT @s1,@s2;
+------+------+
| @s1 | @s2 |
+------+------+
| 1 | 2 | ##说明以上步骤运行正常
+------+------+

9.2 查看存储过程或函数

查看存储过程和函数的状态的语法格式:
SHOW [PROCEDURE][FUNCTION]STATUS [LIKE'pattern' ];
查看存储过程和函数的定义的语法格式:
SHOW CREATE [PROCEDURE][FUNCTION] sp_name;

mysql> SHOW PROCEDURE STATUS LIKE 'p%'\G
*************************** 1. row ***************************
Db: testdb
Name: p_stat_score
Type: PROCEDURE
Definer: feigu@%
Modified: 2016-11-01 22:02:05
Created: 2016-11-01 22:02:05
Security_type: DEFINER
Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: utf8_general_ci
mysql> SHOW CREATE PROCEDURE p_stat_score\G
*************************** 1. row ***************************
Procedure: p_stat_score
sql_mode:
Create Procedure: CREATE DEFINER=</code>feigu<code>@</code>%<code> PROCEDURE </code>p_stat_score`(IN i_stud_code VARCHAR(20),OUT o_count INT)
BEGIN
SELECT count(*) INTO o_count FROM stud_score WHERE stud_code =i_stud_code;
END
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: utf8_general_ci

第10章 数据备份和恢复

10.1 备份数据

备份数据是一项非常重要的事情,是保证数据安全、降低系统损坏、断电、失误操作等等所带来的危害的有力措施。任何的侥幸心理,都可能带来严重后果。
MySQL数据库备份的方法很多,这里我们介绍几种常用的方法:
1)、mysqldump工具
使用mysqldump备份单个数据库中的所有表,其语法格式为:
mysqldump -u user -h host -p dbname>filename.sql
使用mysqldump备份数据库中的部分表,其语法格式为:
mysqldump -u user -h host -p dbname [tablename[,tablename,,,]]>filename.sql
导出的结果包括表结构、数据等信息
2)、使用SELECT ... INTO OUTFILE导出到文件,其命令格式为:
SELECT ... INTO OUTFILE 'path/filename' [参数]
这样导出方式,比较简单,导入也简单,但只导出数据,不包含表结构等信息。
下面通过一个实例来进步说明如何操作,实例的目的是把表stud_info的数据导出到/tmp目录下。

mysql> SELECT COUNT(*) FROM stud_info;
+----------+
| COUNT(*) |
+----------+
| 22 |
+----------+
mysql> SELECT * FROM stud_info into outfile '/tmp/stud_info_20161102.txt'
-> fields terminated by "," enclosed by '"';
Query OK, 22 rows affected (0.00 sec)

验证导出结果:

hadoop@master:/tmp$ ls -l|grep 'stud_info*'
-rw-rw-r-- 1 feigu feigu 1508 Jul 6 15:47 stud_info.csv
hadoop@master:/tmp$ ls -l|grep 'stud_info*' ###文件导出成功
-rw-rw-rw- 1 mysql mysql 2127 Nov 2 09:21 stud_info_20161102.txt
-rw-rw-r-- 1 feigu feigu 1508 Jul 6 15:47 stud_info.csv
hadoop@master:/tmp$ head -3 stud_info_20161102.txt ###查看前3条
"2015101000","王进","M","1997-08-01","2014-09-01","苏州","0000-00-00","10","理学院","1
"
2015101001","刘海","M","1997-09-29","2014-09-01","上海","0000-00-00","10","理学院","1
"2015101002","张飞","M","1996-10-21","2014-09-02","济南","0000-00-00","10","理学院","1
hadoop@master:/tmp$ wc -l 22

10.2 恢复数据

恢复数据或还原数据也有多种方法,如通过MySQL命令、工具等。如果备份的文件是以SQL语句的格式保存的可采用执行该文件即可,如source filename.sql,利用工具mysqldump备份出的文件就是sql文件,恢复时就可采用执行文件的方式,其语法格式为:
mysql -u user -h host -p dbname DELETE FROM stud_info; ##全删表数据
Query OK, 22 rows affected (0.08 sec)

mysql> SELECT COUNT(*) FROM stud_info;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
mysql> LOAD DATA INFILE '/tmp/stud_info_20161102.txt' INTO TABLE stud_info
-> fields terminated by "," enclosed by '"' ;
Query OK, 22 rows affected (0.02 sec)
Records: 22 Deleted: 0 Skipped: 0 Warnings: 0
mysql> SELECT COUNT(*) FROM stud_info;
+----------+
| COUNT(*) |
+----------+
| 22 |
+----------+
mysql> SELECT * FROM stud_info LIMIT 2\G
*************************** 1. row ***************************
stud_code: 2015101000
stud_name: 王进
stud_sex: M
birthday: 1997-08-01
log_date: 2014-09-01
orig_addr: 苏州
lev_date: 0000-00-00
college_code: 10
college_name: 理学院
state: 1
*************************** 2. row ***************************
stud_code: 2015101001
stud_name: 刘海
stud_sex: M
birthday: 1997-09-29
log_date: 2014-09-01
orig_addr: 上海
lev_date: 0000-00-00
college_code: 10
college_name: 理学院
state: 1
2 rows in set (0.00 sec)
[/cceN_python]

第11章 性能优化

11.1优化概述

优化数据库是数据库管理人员和开发人员需要具备的一项重要技能,在实际项目中,往往遇到运行一个查询或一个存储过程或显示1张报表耗时十几分钟或几个小时,甚至更多,但经优化后,原来需要几个小时的只要不到一分钟,由此,优化对开发和管理的意义就不言自明了。
数据库的优化,方法很多,但总的原则就是减少系统资源(包括I/O,内存、CPU等)的占有、增强系统的健壮性。
优化的一般步骤:先找到性能瓶颈,然后,根据资源使用情况,找到引起瓶颈的语句或应用,调整结构或设计或参数、优化的过程往往迭代式。
优化的方法主要有设计优化、查询优化、参数的优化等。

11.2优化查询

查询是我们用得最多的操作,提高查询速度是一个我们经常遇到的问题。
如果能及时查询使用资源的情况,将有助于我们发现问题所在,幸好,MySQL提供了一个分析语句:EXPLAIN或DES CRIBE,通过这个分析语句我们可以很方便地看到查询语句查询的行数、是否利用了索引等重要信息。
EXPLAIN语句的基本语法如下:
EXPLAIN 查询语句;

下面对查询结果的各项含义进行说明:
(1)id
Select语句的序列号
(2)select_type
表示查询类型,它主要有如下几种情况:
SIMPLE----表示简单查询,其中不包括连接查询、子查询等;
PRIMARY---表示主查询;
UNION-----表示涉及连接查询
(3)table
表示查询的表
(4)type
表示表的连接类型,各种连接类型从优到差大致有如下几种。
system:该表为仅有一行的系统表
const:数据表最多只有一个匹配行,它查询时开始读取,在后续查询中将优化为常量。const表查询很快,因只需读一次。它一般使用常量值比较主键或唯一索引的所有部分。
如下查询属于这类型:
SELECT * FROM TABLE_NAME WHERE PK_KEY1=a;
eq_ref:从关联表中读取一行,常用于主键或唯一索引字段的等式匹配。
ref: 从关联表中匹配所有行,常用于非主键或唯一索引字段的等式或不等式的匹配。
ref_or_null:。类型如同ref,但多了含null的查询条件;
index_merge:说明连接类型使用了索引合并优化方法;
uique_subquery:使用了索引的子查询
index_subquery:使用了非唯一性索引的子查询
range:搜索指定范围的行
index:只扫描索引
ALL: 进行总表扫描
(5)possible_keys
说明能用哪个索引,如果为null,没有使用索引,可考虑使用或创建相关索引来提高查询效率。
(6)key
茶香中使用到的索引,如果为null,说明没有使用索引。
(7)key_len
指选择索引字段的字节长度,如果为null,则长度为null,通过该值可知道实际使用了组合索引的几个字段。
(8)ref
说明使用了哪列或常数与索引一起查询;
(9)rows
说明查询时查询的行数;
(10)Extra
查询附加信息,如是否用了where等。
通过分析我们知道,该查询是单表查询,是全表扫描,共扫描了118行。

以下介绍通过不同字段查询相同记录的性能分析,看索引对查询的影响。
有一张表存储学生信息的表stud_info,分别通过学院代码字段(建立了一个索引)、学院名称字段(没有索引)查询。

11.3索引对性能的影响

索引对提高查询速度或数据库性能关系非常密切,其效果往往是立竿见影的,如果索引设计合理,能大幅提升的查询效率。下面我们以一个简单实例来说明,比较一个字段在没有索引和有索引的查询效能如何改变。
还是以表stud_info为例,对其字段college_code先没索引到创建索引,然后比较查询性能分析结果。

mysql> SHOW INDEX FROM stud_info \G ###只有一个主键索引
*************************** 1. row ***************************
Table: stud_info
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: stud_code
Collation: A
Cardinality: 22
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT * FROM stud_info WHERE college_code='10'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: stud_info
type: ALL ###全表扫描
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 22
Extra: Using where
1 row in set (0.00 sec)
mysql> ALTER TABLE stud_info ADD INDEX collindex(college_code); ##创建索引
Query OK, 0 rows affected (0.25 sec)
mysql> SHOW INDEX FROM stud_info \G
*************************** 1. row ***************************
Table: stud_info
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: stud_code
Collation: A
Cardinality: 22
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: stud_info
Non_unique: 1
Key_name: collindex ###刚创建的索引
Seq_in_index: 1
Column_name: college_code
Collation: A
Cardinality: 22
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.00 sec)
mysql> EXPLAIN SELECT * FROM stud_info WHERE college_code='10'\G ##性能分析
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: stud_info
type: ref ##非全表扫描
possible_keys: collindex
key: collindex
key_len: 33
ref: const
rows: 11
Extra: Using where
1 row in set (0.02 sec)

如果有个组合索引,是否创建了索引,然后使用其中一个为查询条件,是否一定走索引呢?我们通过一个实例来看一下,假设我们在stud_score表的stud_code,sub_code两个字段上创建一个联合索引或联合主键。

mysql> DESC stud_score;
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| stud_code | varchar(20) | NO | PRI | NULL | |
| sub_code | varchar(20) | NO | PRI | NULL | | ##有联合主键
| sub_name | varchar(100) | YES | | NULL | |
| sub_tech | varchar(20) | YES | | NULL | |
| sub_score | smallint(10) | YES | | NULL | |
| stat_date | date | YES | | NULL | |
+-----------+--------------+------+-----+---------+-------+
6 rows in set (0.01 sec)

mysql> SELECT COUNT(*) FROM stud_score;
+----------+
| COUNT(*) |
+----------+
| 121 |
+----------+
1 row in set (0.06 sec)

mysql> EXPLAIN SELECT * FROM stud_score WHERE sub_code='10104'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: stud_score
type: ALL ##说明索引没起作用,还是全表扫描
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 121
Extra: Using where
1 row in set (0.00 sec)

以上例子中虽然我们查询时用到了一个索引字段,但查询时却没有走索引,这是为什么呢?这里违背索引的一条重要原则,即最左原则。该原则是:如果使用的字段不含组合索引中的第一个字段(即最左边那个字段),查询时不走索引。以下我们还是通过实例来验证这个原则。

mysql> EXPLAIN SELECT * FROM stud_score WHERE stud_code='2015101001' and sub_code='10104'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: stud_score
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 124
ref: const,const
rows: 1
Extra:
1 row in set (0.00 sec)

11.4设计优化

优化工作,除了在查询时需要考虑,实际上在数据库设计或建模时,更应考虑,而且意义更大、影响更广,这是把优化工作前移。一个差的设计方案,不但影响后续优化,甚至影响系统的性能、可靠性、可维护性。
在设计层面考虑优化时,一般主要以下原则:
1、表中字段不宜过多,否则,应考虑大表拆分成几个小表;
2、为减少多表关联,可以考虑适当添加一些冗余字段,或中间表;
3、需要经常出现在where条件的字段,需要考虑建立索引。
4、表分区设计
表分区的定义:
通俗地讲表分区是将一大表,根据条件分割成若干个小表。mysql5.1开始支持数据表分区了。 如:某商品的库存表的记录超过了400万条,那么就可以根据入库日期将表分区,也可以根据所在地将表分区。当然也可根据其他的条件分区。
查看目前MySQL版本是否支持表分区。

mysql> SHOW VARIABLES LIKE '%partition%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| have_partitioning | YES |
+-------------------+-------+
1 row in set (0.00 sec)

表分区的意义:
 与单个磁盘或文件系统分区相比,可以存储更多的数据。
 一些查询可以得到极大的优化,这主要是借助于满足一个给定WHERE语句的数据可以只保存在一个或多个分区内,无需查询所有分区。涉及到例如SUM()和COUNT()这样聚合函数的查询,可以很容易地进行并行处理。
 通过跨多个磁盘来分散数据查询,来获得更大的查询吞吐量。

表分区类型:

表分区示例:

mysql> SHOW VARIABLES LIKE '%partition%';
mysql>
DROP TABLE IF EXISTS p_range;
CREATE TABLE p_range (
id int(10) NOT NULL AUTO_INCREMENT,
name char(20) NOT NULL,
PRIMARY KEY (id)
)
PARTITION BY RANGE (id)
(PARTITION p1 VALUES LESS THAN (10),
PARTITION p2 VALUES LESS THAN (20),
PARTITION p3 VALUES LESS THAN MAXVALUE
);
mysql>
DROP TABLE IF EXISTS p_partition;
CREATE TABLE p_partition (
id int(10) DEFAULT NULL,
title char(255) NOT NULL,
createtime date NOT NULL
)
PARTITION BY RANGE (YEAR(createtime))
(PARTITION p0 VALUES LESS THAN (2012),
PARTITION p1 VALUES LESS THAN (2013),
PARTITION p2 VALUES LESS THAN MAXVALUE
);

mysql>
DROP TABLE IF EXISTS p_list;
CREATE TABLE p_list (
id int(10) NOT NULL AUTO_INCREMENT,
typeid mediumint(10) NOT NULL DEFAULT '0',
typename char(20) DEFAULT NULL,
PRIMARY KEY (id,typeid)
)
PARTITION BY LIST (typeid)
(PARTITION p0 VALUES IN (1,2,3,4),
PARTITION p1 VALUES IN (5,6,7,8));
mysql>
DROP TABLE IF EXISTS p_hash;
CREATE TABLE p_hash (
id int(10) NOT NULL AUTO_INCREMENT,
storeid mediumint(10) NOT NULL DEFAULT '0',
storename char(255) DEFAULT NULL,
PRIMARY KEY (id,storeid)
) PARTITION BY HASH (storeid)
PARTITIONS 4 ;
mysql>
explain partitions select * from p_hash

11.5系统优化

数据库系统方面的优化,主要涉及系统资源的优化、架构的优化及一些系统参数的优化。
系统资源方面的优化主要有以下方法:
1、配上较大的内存、或使用集群方式,共享内存,可大大增加内存总量;
2、配置高速磁盘系统,或采用集群方式,便于把数据分布到不同节点上。
3、采用多线程的处理器,或采用集群方式,提升并发处理能力。
在系统参数方面,需要关注以下参数:
1、max_connections:表示数据库的最大连接数。
2、query_cache_size:表示查询缓存大小。
3、sort_buffer_size:用来排序的缓冲大小。
4、key_buffer_size:表示索引缓存大小。
(注:参数涉及配置文件my.cnf)

附录1:常见问题

一、连接数据库丢失SOCK
在MySQL服务器上连接数据库时,有时报can't connect to /tmp/mysql.sock类似错误。
这是因为连接中指定的localhost作为一个主机名,而mysqladmin默认使用Unix套接字文件(一命名为mysql.SOCK),而不是TCP/IP。为此,解决此类问题,
1、显式说明指明连接协议,如mysql --protocol=TCP -u user -p -h localhost
2、可以把localhost改为127.0.0.1
3、在连接信息中添加文件mysql.sock所在位置,这个参数(如unix_socket="/var/run/mysqld/mysqld.sock"),
4、修改相关配置参数my.cnf中socket。修改配置文件需要停启服务。

二、中文乱码问题
中文乱码一般是数据库(或表)字符集与客户端字符集、应用字符集不兼容导致的,三者间如果其中两个不兼容就有可能导致乱码。
查看表的字符集:mysql>SHOW CREATE TALE tablename;
查看文件的字符集:vim或vi 文件名,然后输入:set fileencoding 可以查看文件字符集
查看服务、数据库、客户端等字符集:
mysql>show variables like '%char%';
修改客户端字符集:mysql>SET character_set_results=字符集(utf8,gbk等);
三、mysql导入csv报错ERROR 29 (HY000): File not found (Errcode: 13)
解决方法;
1、在mysqld中添加存放数据路径的读写权限
假设存放数据路径为/tmp
#vi /etc/apparmor.d/usr.sbin.mysqld
在最后添加以下两行
/tmp/ r,
/tmp/* rw,
2、然后重新加载这个文件
#/etc/init.d/apparmor reload

附录2:课后练习

一、在windows下安装配置MySQL,并尝试在cmd启动并连接mysql。
二、在testdb库上创建表,表结构如下:

写一个sql文件,功能包括:
1、创建以上表结构,表名为:test_学号;
2、往表test_序号插入至少3条数据;
3、在表最后位置添加一个字段,字段要求如下:

三、创建表并把数据导入表中,然后查询相关数据,把这些操作放到一个SQL文件中,具体要求如下:
1、创建两表,表名分别为,stud_score_学号:stud_info_学号,表结构请参考第6.2.1节和6.3.1节
2、导入数据,把/tmp目录下stud_info.csv,stud_score.csv分别导入到表stud_info_学号,stud_score_学号中
3、查询数据,查询表stud_info_学号中,把姓刘的同学过滤出来。
四、多表查询,在一个SQL语句实现以下功能
1、内连接,实现stud_score_学号:stud_info_学号的内连接,连接字段为stud_code
2、左连接, 实现stud_score_学号:stud_info_学号的左连接,连接字段为stud_code
五、聚合查询
1、把选了大于5门课的同学的名称及各科总成绩打印出来。
六、写一个存储过程,实现功能,输入学院代码,输出,学生名称、学院名称、各科平均成绩,如下图:

七、备份数据
1、导出表stud_socre_学号的数据,并验证导出数据的总条数、是否有乱码等。
八、性能优化
1、谈谈你对数据库优化方法,可包括数据库设计、SQL编写等方面内容。

此外大家还可参考网站其他数据库经典博客或文章 或My SQL 基础入门 或MySQL入门200句

第14章 由浅入深--轻松学会--用Python实现神经网络

我们知道现在神经网络很火,功能很强大、运用范围也很广泛,所以很多人都想在神经网络这方面有所突破和进展。但有不少读者,苦于基础不够,进展一致不尽人意。我们这一章或许能提高您的学习效率,使您能更快了解神经网络的架构和原理,并逐渐自己写神经网络算法。
本章我们将由浅入深介绍用Python实现神经网络,首先介绍感知器的实现方法,然后,介绍一种激活函数为恒等式的自适应神经网络算法,最后,介绍含隐含层的多层神经网络算法。学好这些算法将为学习深度学习打下扎实基础。

14.1 使用Python实现感知器学习算法

这里我们使用弗兰克•罗森布拉特(Frank Rossenblatt)提出的一个感知器,这类感知器有一个自学习算法,该算法可以自动通过优化得到权重系数,此权重系统与输入值的乘积决定神经元是否被激活。其权重更新无需损失函数,当然更无需求导,所以非常简单。

14.1.1感知器简单结构

这里净输入函数为输入样本x与权重值w的相乘后的累加,假设:

分段函数ϕ(z)用图形可表示为:

14.1.2 感知器算法步骤


14.1.3 用Python实现感知器算法

这里使用的数据为鸢尾花数据集,定义感知器类(perceptron),在这个类中定义一个从数据中进行学习的fit方法,使用predict方法进行预测。对非初始化定义的变量,为区别起见我们将假设一个下划线,如self.w_。
1)定义感知器类
定义一个感知器类,在这个类中,先定义两个参数:学习速率eta,迭代步数n_iter;然后定义了两个属性:每次训练样本时记录权重的1维向量w_,用于记录每次迭代时发生错误的样本数errors_;在这个基础上定义了用来训练数据集的函数fit,计算净输入函数net_input,用于预测的函数predict。

import numpy as np

class Perceptron(object):
"""参数
eta : float
学习速率 (between 0.0 and 1.0)
n_iter : int
迭代步数.

属性:
w_ : 1d-array,训练后记录权重.
errors_ : list,记录每次迭代的错误数.

"""

def __init__(self, eta=0.01, n_iter=10):
self.eta = eta
self.n_iter = n_iter

def fit(self, X, y):
"""Fit training data.
Parameters
----------
X : {array-like}, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
y : array-like, shape = [n_samples]
Target values.
Returns
-------
self : object
"""

self.w_ = np.zeros(1 + X.shape[1])
self.errors_ = []

for _ in range(self.n_iter):
errors = 0
for xi, target in zip(X, y):
update = self.eta * (target - self.predict(xi))
self.w_[1:] += update * xi
self.w_[0] += update
errors += int(update != 0.0)
self.errors_.append(errors)
return self

def net_input(self, X):
"""计算输入"""
return np.dot(X, self.w_[1:]) + self.w_[0]

def predict(self, X):
"""Return class label after unit step"""
return np.where(self.net_input(X) >= 0.0, 1, -1)

2)导入鸢尾花数据集
该数据集前4列为特征,最后1列为类别。

import pandas as pd

df = pd.read_csv('https://archive.ics.uci.edu/ml/'
'machine-learning-databases/iris/iris.data', header=None)
df.tail()


3)可视化数据集
为便于可视化及使用二分类(实际也可以使用多分类),这里我们只取前100个类标,其中前50个为山鸢尾类标,后50个为变色鸢尾类标,类标赋给向量y。提取这100个样本中第1、3个特征,并赋值给矩阵X。

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
#显示中文
import matplotlib.font_manager as fm
myfont = fm.FontProperties(fname='/home/hadoop/anaconda3/lib/python3.6/site-packages/matplotlib/mpl-data/fonts/ttf/simhei.ttf')

# select setosa and versicolor
y = df.iloc[0:100, 4].values
y = np.where(y == 'Iris-setosa', -1, 1)

# extract sepal length and petal length
X = df.iloc[0:100, [0, 2]].values

# plot data
plt.scatter(X[:50, 0], X[:50, 1],
color='red', marker='o', label='setosa')
plt.scatter(X[50:100, 0], X[50:100, 1],
color='blue', marker='x', label='versicolor')

plt.xlabel('花瓣长度[cm]',fontproperties=myfont)
plt.ylabel('萼片长度[cm]',fontproperties=myfont)
plt.legend(loc='upper left')

plt.tight_layout()
plt.show()


4)利用鸢尾花数据集训练感知器,并同时绘制每次迭代的错误分类数量。

ppn = Perceptron(eta=0.1, n_iter=10)

ppn.fit(X, y)

plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, marker='o')
plt.xlabel('迭代次数',fontproperties=myfont)
plt.ylabel('错误分类样本数',fontproperties=myfont)

plt.tight_layout()
plt.show()


如上图所示,我们的分类器在第6次迭代后,错误数就为0,说明已收敛了。

14.2 使用Python实现自适应线性神经网络

第1节我们介绍使用Python实现一个单层的感知器神经网络,其激活函数为一个分段函数,这里我们介绍的自适应(Adaline)线性神经网络,其激活函数为一个恒等式。这个可以认为是对前一章神经网络的改进。其权重更新采用了一个连续的线性激活函数(一个恒等函数)来完成,而不是基于分段函数。Adaline算法中作用于净输入的激活函数ϕ(z)是个简单的线性恒等式,

14.2.1 自适应线性神经网络结构

线性激活函数在更新权重的同时,我们使用量化器对类标进行预测,量化器与上节的单位分段函数类似,自适应线性神经网络的结构如下图所示:

14.2.2 通过梯度下降法最小化损失函数

在机器学习中一个最核心的内容就是如何定义一个损失函数及优化损失函数,在自适应算法中,我们可以把模型输出值与实际类标之间的误差平方和(SSE),具体公式如下:

与单位阶跃函数相比,这种连续型激活函数有几个有点:
1)损失函数可导
2)损失函数是一个凸函数,可以通过梯度下降法更新权重,并获取全局最小值。
当然也有一些缺点,线性激活函数无法对非线性数据集进行划分,这点后续我们会介绍。
这里采用梯度下降法(如下图)来最小化损失函数J(w), 首先来看看梯度下降的一个直观的解释。比如我们在一座大山上的某处位置,由于我们不知道怎么下山,于是决定走一步算一步,先确定一个初始位置,然后求解当前位置的梯度,沿着梯度的负方向,也就是当前最陡峭的位置向下走一步,然后继续求解当前位置梯度,向这一步所在位置沿着最陡峭最易下山的位置走一步。这样一步步的走下去,一直走到觉得我们已经到了山脚。当然这样走下去,有可能我们不能走到山脚,而是到了某一个局部的山峰低处。
从上面的解释可以看出,梯度下降不一定能够找到全局的最优解,有可能是一个局部最优解。当然,如果损失函数是凸函数,梯度下降法得到的解就一定是全局最优解。所以我们这里的选择凸函数作为我们的损失函数,这是重要考量之一。


通过梯度下降,我们可以基于损失函数J(w)沿着梯度∆J(w)方向做一次权重更新:

为求损失函数的梯度,我们需要计算损失函数对于每个权重w_j的偏导:

14.2.3 使用Python实现自适应线性神经网络

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
#显示中文
import matplotlib.font_manager as fm
myfont = fm.FontProperties(fname='/home/hadoop/anaconda3/lib/python3.6/site-packages/matplotlib/mpl-data/fonts/ttf/simhei.ttf')

df = pd.read_csv('https://archive.ics.uci.edu/ml/'
'machine-learning-databases/iris/iris.data', header=None)

# select setosa and versicolor
y = df.iloc[0:100, 4].values
y = np.where(y == 'Iris-setosa', -1, 1)

# extract sepal length and petal length
X = df.iloc[0:100, [0, 2]].values

#对特征进行标准化处理
X_std = np.copy(X)
X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()

#fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 4))

ada1 = AdalineGD(n_iter=20, eta=0.01).fit(X_std, y)
plt.plot(range(1, len(ada1.cost_) + 1), np.log10(ada1.cost_), marker='o')
plt.xlabel('迭代次数',fontproperties=myfont)
plt.ylabel('log(均方根误差)',fontproperties=myfont)
plt.title('Adaline - 学习速率 0.01',fontproperties=myfont)
plt.tight_layout()
plt.show()

从以上图形可以看出,Adaline算法收敛效果不错,迭代到15次后,损失函数逐渐收敛。
【延伸思考】
1)、大家可以调整学习速率这个参数,分几种情况,如大于0.01或小于0.01的情况,然后看一下收敛情况。
2)、这里学习速率是固定不变,我们是否可以在迭代过程中动态调整这个参数?如何调整?
3)这里我们是一次训练整个数据集,如果数据集不大,还可以。如果数据量很大,这种方法就可能带来性能问题。对于大数据集对此,是否可以采用分批训练?如何实现,不妨动手尝试一下。
14.3 用Python实现多层神经网络
前面两节,我们分别介绍了感知器神经网络(Rossenblatt提出的)、自适应线性神经网络(Adaline),这两种虽然有激活函数,但只有输入层和输出层,没有隐含层,所以一般称为单层神经网络,这节我们将介绍一种含隐含层的神经网络,这样就有三层,分别为输入层、隐含层、输出层,隐含层的所有单元连接到输入层上,输出层的所有单元也全连接到隐含层中,如下图所示:如下图:

图 三层神经网络

14.3 三层神经网络介绍

下图为典型的三层神经网络的基本构成,由输入层、隐含层、输出层构成,激活函数取sigmoid函数:

的图形为S型曲线,其输出介于[0,1]之间,如下图:

sigmoid函数的导数比较有特色,下面我们简单演示一下其求导过程:

14.3.1 正向传播构造神经网络

为便于后续标识统一起见,我们使用Numpy中的向量化的代码,不使用一般的循环方法,所以基本从矩阵或向量的角度考虑各个节点的数据,具体设置如下:

这些矩阵或向量的具体位置,可参考下图:

有了上面这些设置后,我们就可方便设计出这个神经网络的前向传导算法的步骤:

14.3.2 反向传播构造神经网络

反向传播算法(BP算法)与前向传导算法的方向正好相反,前向传导是从左到右,反向传播算法是从右到左。前向传导把激活值往后传导,反向传播则往前传播梯度,并更新权重参数。
传播的梯度将使损失函数最小化,这里的损失函数我们采用交叉熵函数:

14.3.3 用Python实现前向传导与反向传播算法

这里以MNIST为数据集,MNIST是一个手写数字0-9的数据集,它有60000个训练样本集和10000个测试样本集它是NIST数据库的一个子集。
MNIST数据库官方网址为:http://yann.lecun.com/exdb/mnist/ ,也可以在windows下直接下载,train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz等。下载四个文件,可用
gzip *ubyte.gz -d 解压缩。解压缩后发现这些文件并不是标准的图像格式。这些图像数据都保存在二进制文件中。每个样本图像的宽高为28*28。

14.3.4 导入数据

import os
import struct
import numpy as np

def load_mnist(path, kind='train'):
"""Load MNIST data from <code>path</code>"""
labels_path = os.path.join(path,
'%s-labels-idx1-ubyte'
% kind)
images_path = os.path.join(path,
'%s-images-idx3-ubyte'
% kind)

with open(labels_path, 'rb') as lbpath:
magic, n = struct.unpack('>II',
lbpath.read(8))
labels = np.fromfile(lbpath,
dtype=np.uint8)

with open(images_path, 'rb') as imgpath:
magic, num, rows, cols = struct.unpack(">IIII",
imgpath.read(16))
images = np.fromfile(imgpath,
dtype=np.uint8).reshape(len(labels), 784)

return images, labels

14.3.5 获取训练及测试数据

X_train, y_train = load_mnist('./data/mnist', kind='train')
print('Rows: %d, columns: %d' % (X_train.shape[0], X_train.shape[1]))

X_test, y_test = load_mnist('./data/mnist', kind='t10k')
print('Rows: %d, columns: %d' % (X_test.shape[0], X_test.shape[1]))

import matplotlib.pyplot as plt
%matplotlib inline

fig, ax = plt.subplots(nrows=2, ncols=5, sharex=True, sharey=True,)
ax = ax.flatten()
for i in range(10):
img = X_train[y_train == i][0].reshape(28, 28)
ax[i].imshow(img, cmap='Greys', interpolation='nearest')

ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()

plt.show()

<img src="http://www.feiguyunai.com/wp-content/uploads/2017/11/8ab93c7d758f8861c54f132165ba8e93.png" alt="" />
<h3>14.3.6 定义多层神经网络</h3>
import numpy as np
from scipy.special import expit
import sys

class NeuralNetMLP(object):
""" Feedforward neural network / Multi-layer perceptron classifier.

Parameters
------------
n_output : int
Number of output units, should be equal to the
number of unique class labels.

n_features : int
Number of features (dimensions) in the target dataset.
Should be equal to the number of columns in the X array.

n_hidden : int (default: 30)
Number of hidden units.

l1 : float (default: 0.0)
Lambda value for L1-regularization.
No regularization if l1=0.0 (default)

l2 : float (default: 0.0)
Lambda value for L2-regularization.
No regularization if l2=0.0 (default)

epochs : int (default: 500)
Number of passes over the training set.

eta : float (default: 0.001)
Learning rate.

alpha : float (default: 0.0)
Momentum constant. Factor multiplied with the
gradient of the previous epoch t-1 to improve
learning speed
w(t) := w(t) - (grad(t) + alpha*grad(t-1))

decrease_const : float (default: 0.0)
Decrease constant. Shrinks the learning rate
after each epoch via eta / (1 + epoch*decrease_const)

shuffle : bool (default: True)
Shuffles training data every epoch if True to prevent circles.

minibatches : int (default: 1)
Divides training data into k minibatches for efficiency.
Normal gradient descent learning if k=1 (default).

random_state : int (default: None)
Set random state for shuffling and initializing the weights.

Attributes
-----------
cost_ : list
Sum of squared errors after each epoch.

"""

def __init__(self, n_output, n_features, n_hidden=30,
l1=0.0, l2=0.0, epochs=500, eta=0.001,
alpha=0.0, decrease_const=0.0, shuffle=True,
minibatches=1, random_state=None):

np.random.seed(random_state)
self.n_output = n_output
self.n_features = n_features
self.n_hidden = n_hidden
self.w2, self.w3 = self._initialize_weights()
self.l1 = l1
self.l2 = l2
self.epochs = epochs
self.eta = eta
self.alpha = alpha
self.decrease_const = decrease_const
self.shuffle = shuffle
self.minibatches = minibatches

def _encode_labels(self, y, k):
"""Encode labels into one-hot representation

Parameters
------------
y : array, shape = [n_samples]
Target values.

Returns
-----------
onehot : array, shape = (n_labels, n_samples)

"""

onehot = np.zeros((k, y.shape[0]))
for idx, val in enumerate(y):
onehot[val, idx] = 1.0
return onehot

def _initialize_weights(self):
"""Initialize weights with small random numbers."""
w2 = np.random.uniform(-1.0, 1.0, size=self.n_hidden*(self.n_features + 1))
w2 = w2.reshape(self.n_hidden, self.n_features + 1)
w3 = np.random.uniform(-1.0, 1.0, size=self.n_output*(self.n_hidden + 1))
w3 = w3.reshape(self.n_output, self.n_hidden + 1)
return w2, w3

def _sigmoid(self, z):
"""Compute logistic function (sigmoid)

Uses scipy.special.expit to avoid overflow
error for very small input values z.

"""

# return 1.0 / (1.0 + np.exp(-z))
return expit(z)

def _sigmoid_gradient(self, z):
"""Compute gradient of the logistic function"""
sg = self._sigmoid(z)
return sg * (1 - sg)

def _add_bias_unit(self, X, how='column'):
"""Add bias unit (column or row of 1s) to array at index 0"""
if how == 'column':
X_new = np.ones((X.shape[0], X.shape[1]+1))
X_new[:, 1:] = X
elif how == 'row':
X_new = np.ones((X.shape[0]+1, X.shape[1]))
X_new[1:, :] = X
else:
raise AttributeError('<code>how</code> must be <code>column</code> or <code>row</code>')
return X_new

def _feedforward(self, X, w2, w3):
"""Compute feedforward step

Parameters
-----------
X : array, shape = [n_samples, n_features]
Input layer with original features.

w2 : array, shape = [n_hidden_units, n_features]
Weight matrix for input layer -> hidden layer.

w3 : array, shape = [n_output_units, n_hidden_units]
Weight matrix for hidden layer -> output layer.

Returns
----------
a1 : array, shape = [n_samples, n_features+1]
Input values with bias unit.

z2 : array, shape = [n_hidden, n_samples]
Net input of hidden layer.

a2 : array, shape = [n_hidden+1, n_samples]
Activation of hidden layer.

z3 : array, shape = [n_output_units, n_samples]
Net input of output layer.

a3 : array, shape = [n_output_units, n_samples]
Activation of output layer.

"""

a1 = self._add_bias_unit(X, how='column')
z2 = w2.dot(a1.T)
a2 = self._sigmoid(z2)
a2 = self._add_bias_unit(a2, how='row')
z3 = w3.dot(a2)
a3 = self._sigmoid(z3)
return a1, z2, a2, z3, a3

def _L2_reg(self, lambda_, w2, w3):
"""Compute L2-regularization cost"""
return (lambda_/2.0) * (np.sum(w2[:, 1:] ** 2) + np.sum(w3[:, 1:] ** 2))

def _L1_reg(self, lambda_, w2, w3):
"""Compute L1-regularization cost"""
return (lambda_/2.0) * (np.abs(w2[:, 1:]).sum() + np.abs(w3[:, 1:]).sum())

def _get_cost(self, y_enc, output, w2, w3):
"""Compute cost function.

y_enc : array, shape = (n_labels, n_samples)
one-hot encoded class labels.

output : array, shape = [n_output_units, n_samples]
Activation of the output layer (feedforward)

w2 : array, shape = [n_hidden_units, n_features]
Weight matrix for input layer -> hidden layer.

w3 : array, shape = [n_output_units, n_hidden_units]
Weight matrix for hidden layer -> output layer.

Returns
---------
cost : float
Regularized cost.

"""

term1 = -y_enc * (np.log(output))
term2 = (1 - y_enc) * np.log(1 - output)
cost = np.sum(term1 - term2)
L1_term = self._L1_reg(self.l1, w2, w3)
L2_term = self._L2_reg(self.l2, w2, w3)
cost = cost + L1_term + L2_term
return cost

def _get_gradient(self, a1, a2, a3, z2, y_enc, w2, w3):
""" Compute gradient step using backpropagation.

Parameters
------------
a1 : array, shape = [n_samples, n_features+1]
Input values with bias unit.

a2 : array, shape = [n_hidden+1, n_samples]
Activation of hidden layer.

a3 : array, shape = [n_output_units, n_samples]
Activation of output layer.

z2 : array, shape = [n_hidden, n_samples]
Net input of hidden layer.

y_enc : array, shape = (n_labels, n_samples)
one-hot encoded class labels.

w2 : array, shape = [n_hidden_units, n_features]
Weight matrix for input layer -> hidden layer.

w3 : array, shape = [n_output_units, n_hidden_units]
Weight matrix for hidden layer -> output layer.

Returns
---------

grad1 : array, shape = [n_hidden_units, n_features]
Gradient of the weight matrix w2.

grad2 : array, shape = [n_output_units, n_hidden_units]
Gradient of the weight matrix w3.

"""

# backpropagation
sigma3 = a3 - y_enc
z2 = self._add_bias_unit(z2, how='row')
sigma2 = w3.T.dot(sigma3) * self._sigmoid_gradient(z2)
sigma2 = sigma2[1:, :]
grad1 = sigma2.dot(a1)
grad2 = sigma3.dot(a2.T)

# regularize
grad1[:, 1:] += (w2[:, 1:] * (self.l1 + self.l2))
grad2[:, 1:] += (w3[:, 1:] * (self.l1 + self.l2))

return grad1, grad2

def predict(self, X):
"""Predict class labels

Parameters
-----------
X : array, shape = [n_samples, n_features]
Input layer with original features.

Returns:
----------
y_pred : array, shape = [n_samples]
Predicted class labels.

"""

if len(X.shape) != 2:
raise AttributeError('X must be a [n_samples, n_features] array.\n'
'Use X[:,None] for 1-feature classification,'
'\nor X[[i]] for 1-sample classification')

a1, z2, a2, z3, a3 = self._feedforward(X, self.w2, self.w3)
y_pred = np.argmax(z3, axis=0)
return y_pred

def fit(self, X, y, print_progress=False):
""" Learn weights from training data.

Parameters
-----------
X : array, shape = [n_samples, n_features]
Input layer with original features.

y : array, shape = [n_samples]
Target class labels.

print_progress : bool (default: False)
Prints progress as the number of epochs
to stderr.

Returns:
----------
self

"""

self.cost_ = []
X_data, y_data = X.copy(), y.copy()
y_enc = self._encode_labels(y, self.n_output)

delta_w2_prev = np.zeros(self.w2.shape)
delta_w3_prev = np.zeros(self.w3.shape)

for i in range(self.epochs):

# adaptive learning rate
self.eta /= (1 + self.decrease_const*i)

if print_progress:
sys.stderr.write('\rEpoch: %d/%d' % (i+1, self.epochs))
sys.stderr.flush()

if self.shuffle:
idx = np.random.permutation(y_data.shape[0])
X_data, y_enc = X_data[idx], y_enc[:, idx]

mini = np.array_split(range(y_data.shape[0]), self.minibatches)
for idx in mini:

# feedforward
a1, z2, a2, z3, a3 = self._feedforward(X_data[idx], self.w2, self.w3)
cost = self._get_cost(y_enc=y_enc[:, idx],
output=a3,
w2=self.w2,
w3=self.w3)
self.cost_.append(cost)

# compute gradient via backpropagation
grad1, grad2 = self._get_gradient(a1=a1, a2=a2,
a3=a3, z2=z2,
y_enc=y_enc[:, idx],
w2=self.w2,
w3=self.w3)

delta_w2, delta_w3 = self.eta * grad1, self.eta * grad2
self.w2 -= (delta_w2 + (self.alpha * delta_w2_prev))
self.w3 -= (delta_w3 + (self.alpha * delta_w3_prev))
delta_w2_prev, delta_w3_prev = delta_w2, delta_w3

return self

【备注】程序具体实现时,做了一些优化,如正则化,自适应学习速率等。

14.3.7 训练模型

训练模型

nn = NeuralNetMLP(n_output=10,
n_features=X_train.shape[1],
n_hidden=50,
l2=0.1,
l1=0.0,
epochs=1000,
eta=0.001,
alpha=0.001,
decrease_const=0.00001,
minibatches=50,
shuffle=True,
random_state=1)

nn.fit(X_train, y_train, print_progress=True)
<h3>14.3.8 可视化训练结果</h3>
%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(range(len(nn.cost_)), nn.cost_)
plt.ylim([0, 2000])
plt.ylabel('Cost')
plt.xlabel('Epochs * 50')
plt.tight_layout()
plt.show()

【延伸思考】
本章先从最简单的单层感知器入手,然后介绍了Adaline神经元的实现,这两个都是属于单层神经网络,之后,我们介绍了含隐含层的多层神经网络,并重点说明了前向传导、反向传播的原理。这些原理及实现方法应该是神经网络的关键技术之一,是进一步学习深度学习的重要基础。随着层数的不断增多,深度学习遇到各种挑战,如过拟合问题、梯度消失、梯度爆炸问题、性能问题等等,这些问题都是深度学习时必须解决的问题。如何有效解决这些问题,后续介绍深度学习时将会详细说明。