上个月底上交了人工智能报告,现在过去了半个月,打算写个回顾记录一下,这也算分类算法的入门。我选的课题是使用卷积神经网络、SVM或者KNN来对Mnist数据集进行分类处理。
MNIST数据集
MNIST 数据集来自美国国家标准与技术研究所 (NIST)。训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员。测试集(test set) 也是同样比例的手写数字数据。在 MNIST 官网可以看到它包括四个数据集:
里面的数据就是这样的手写数字图案:
数据集被分成两部分:60000 行的训练数据集(mnist.train)和 10000 行的测试数据集(mnist.test),在机器学习模型设计时必须有一个单独的测试数据集不用于训练而是用来评估这个模型的性能,每一张图片包含 28X28 个像素点,可以用一个数字数组来表示这张图片,这个数组可以展开成一个向量,长度是 28x28 = 784。如何展开这个数组不重要,只要保持各个图片采用相同的方式展开,这样的话,MNIST 数据集的图片就是在784 维向量空间里面的点。可以直接读取 MNIST 的原数据得到灰度矩阵来表示它们:
此矩阵打印以后效果如图(此为数据集第 20001 张图):
里面的数据又分为图片和标签,标签就是标明某张图片属于哪个数字的标准答案。这里有一张对 mnist 里数据的介绍:
还可以使用 scipy.misc 的 toimage 方法保存一些图片:
这就是 train 数据集中的部分图片。
卷积神经网络
我主要使用的卷积神经网络算法,依赖Tensorflow库,一个基于数据流编程的符号数学系统,被广泛应用于各类机器学习算法的编程实现,其前身是谷歌的神经网络算法库 DistBelief。Tensorflow 提供了一个类来处理 MNIST 数据。而卷积神经网络是一类包含卷积计算且具有深度结构的前馈神经网络,是深度学习的代表算法之一。卷积神经网络具有表征学习能力,能够按其阶层结构对输入信息进行平移不变分类。它仿造生物的视知觉机制构建,其结构包括卷积层(C层)和池化层(S 层,也可以叫下采样层),最后一定有全连接层。
这里给出卷积神经网络训练代码:
import tensorflow as tf
#Tensorflow提供了一个类来处理MNIST数据
from tensorflow.examples.tutorials.mnist import input_data
import time
#载入数据集
mnist=input_data.read_data_sets('MNIST_data',one_hot=True)
#设置批次的大小
batch_size=1000
#计算一共有多少个批次
n_batch=mnist.train.num_examples//batch_size
#定义初始化权值函数
def weight_variable(shape):
initial=tf.truncated_normal(shape,stddev=0.1)
return tf.Variable(initial)
#定义初始化偏置函数
def bias_variable(shape):
initial=tf.constant(0.1,shape=shape)
return tf.Variable(initial)
#卷积层
def conv2d(input,filter):
return tf.nn.conv2d(input,filter,strides=[1,1,1,1],padding='SAME')
#池化层
def max_pool_2x2(value):
return tf.nn.max_pool(value,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
#输入层
#定义两个placeholder
x=tf.placeholder(tf.float32,[None,784]) #28*28
y=tf.placeholder(tf.float32,[None,10])
#改变x的格式转为4维的向量[batch,in_hight,in_width,in_channels]
x_image=tf.reshape(x,[-1,28,28,1])
#卷积、激励、池化操作
#初始化第一个卷积层的权值和偏置
W_conv1=weight_variable([5,5,1,32]) #5*5的采样窗口,32个卷积核从1个平面抽取特征
b_conv1=bias_variable([32]) #每一个卷积核一个偏置值
#把x_image和权值向量进行卷积,再加上偏置值,然后应用于relu激活函数
h_conv1=tf.nn.relu(conv2d(x_image,W_conv1)+b_conv1)
h_pool1=max_pool_2x2(h_conv1) #进行max_pooling 池化层
#初始化第二个卷积层的权值和偏置
W_conv2=weight_variable([5,5,32,64]) #5*5的采样窗口,64个卷积核从32个平面抽取特征
b_conv2=bias_variable([64])
#把第一个池化层结果和权值向量进行卷积,再加上偏置值,然后应用于relu激活函数
h_conv2=tf.nn.relu(conv2d(h_pool1,W_conv2)+b_conv2)
h_pool2=max_pool_2x2(h_conv2) #池化层
#28*28的图片第一次卷积后还是28*28,第一次池化后变为14*14
#第二次卷积后为14*14,第二次池化后变为了7*7
#经过上面操作后得到64张7*7的平面
#全连接层
#初始化第一个全连接层的权值
W_fc1=weight_variable([7*7*64,1024])#经过池化层后有7*7*64个神经元,全连接层有1024个神经元
b_fc1 = bias_variable([1024])#1024个节点
#把池化层2的输出扁平化为1维
h_pool2_flat = tf.reshape(h_pool2,[-1,7*7*64])
#求第一个全连接层的输出
h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,W_fc1)+b_fc1)
#keep_prob用来表示神经元的输出概率
keep_prob=tf.placeholder(tf.float32)
h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
#初始化第二个全连接层
W_fc2=weight_variable([1024,10])
b_fc2=bias_variable([10])
#输出层
#计算输出
prediction=tf.nn.softmax(tf.matmul(h_fc1_drop,W_fc2)+b_fc2)
#交叉熵代价函数
cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=prediction))
#使用AdamOptimizer进行优化
train_step=tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
#结果存放在一个布尔列表中(argmax函数返回一维张量中最大的值所在的位置)
correct_prediction=tf.equal(tf.argmax(prediction,1),tf.argmax(y,1))
#求准确率(tf.cast将布尔值转换为float型)
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
#创建会话
with tf.Session() as sess:
start_time=time.clock()
sess.run(tf.global_variables_initializer()) #初始化变量
for epoch in range(30): #迭代30次(训练30次)
for batch in range(n_batch):
batch_xs,batch_ys=mnist.train.next_batch(batch_size)
sess.run(train_step,feed_dict={x:batch_xs,y:batch_ys,keep_prob:0.65}) #进行迭代训练
#测试数据计算出准确率
acc=sess.run(accuracy,feed_dict={x:mnist.test.images,y:mnist.test.labels,keep_prob:1.0})
print('迭代'+str(epoch)+',测试准确率为'+str(acc))
end_time=time.clock()
print('运行耗时:%s Second'%(end_time-start_time)) #输出运行时间
卷积神经网络模型:
输入层:MNIST 数据集(28x28)
第一层卷积:感受视野 5x5,步长为 1,卷积核:32 个
第一层池化:池化视野 2x2,步长为 2
第二层卷积:感受视野 5x5,步长为 1,卷积核:64 个
第二层池化:池化视野 2x2,步长为 2
全连接层:设置 1024 个神经元
输出层:0~9 十个数字类别
输入
从 tensorflow 里引入自带的 MNIST 数据集,并使用独热编码(one_hot)进行分类,MNIST 里面的数据为手写数字,那么分类结果就是图片中的数字属于 0~9的哪个类别,因此分类目标就是一个 1x10 的向量,所属类别对应向量中的位置为 1,其余都为 0。再设置 1000 组数据为一个批次,并用 tf.placeholder()函数构造了两个占位符,后面可以利用 feed_dict()函数对其赋值。
卷积层
使用 tf.nn.conv2d(input,filter,strides,padding)卷积函数,其中一般填四个参数:第一个参数 input 是输入,要求一-定一个形状为(a, b,c,d)的 tensor,也就是通常说的 4 维张量.它具有两种形式: NHWC,NCHW ,表示输入的通道在第二维或者是第四维,在后面的参数 data_ format 中可以进行选择。第二个参数 filter 就是参与卷积的卷积核,要求是一个 4 维张量, 形状是[height, width, in_ depth,out_ depth] .其中 height, width 表示卷积核本身的大小,in_ depth 必须和 input 的通道数保持一致,out_ depth 表示卷积核的个数。8第三个参数 strides 是卷积核滑动的步长,要求是一个 4 维张量, 第二维和第三维表示卷积核的大小,对于 NHWC 的输入来说形状是(1,stride_ h,stride W,1) ,对于 NCHW 的输入来说是(1,1,stride_ h,stride W)。第四个参数 padding 是补洞策略,可以选择"SAME"或者是"VALID", 在使用”SAME"时,tensorflow 会对输入自动补 0,这样在移动步长是 1 的时候保证输出大小和输入大小相同,也就是说得到的平面跟原平面大小一样;在使用 VALID"时,tensorflow 就不会进行自动补 0,那么卷积就是从平面中间某个点开始的,得到的平面比原来小。演示如图,未标的另一种就是 VALID:
第一个卷积层采用 5x5 的采样窗口,32 个卷积核从 1 个平面抽取特征,每一个卷积核一个偏置值。第二个卷积层采用 5x5 的采样窗口,64 个卷积核从 32 个平面抽取特征。
池化层
池化层夹在连续的卷积层中间,用于压缩数据和参数的量,可以使图片特征降维,压缩图像。池化层运用了最大池化操作 tf.nn.max_pool(value, ksize,strides, padding),一般也是四个参数,用法类似卷积操作:第一个参数 value:需要池化的输入,一般池化层接在卷积层后面,所以输入通常是 feature map,依然是[batch, height, width, channels]这样的形式。第二个参数 ksize:池化窗口的大小,取一个四维向量,一般是[1, height,width, 1],一般不在 batch 和 channels 上做池化,所以这两个维度设为了 1。第三个参数 strides:和卷积类似,窗口在每一个维度上滑动的步长,一般也是[1, stride,stride, 1]第四个参数 padding:和卷积类似,可以取'VALID' 或者'SAME',返回一个9Tensor,类型不变,shape 仍然是[batch, height, width, channels]这种形式。例如对图片进行池化,设置参数 tf.reshape(a,[1,4,4,2]),tf.nn.max_pool(a,[1,2,2,1],[1,1,1,1],padding='VALID'),那么可以得到池化结果为
。与卷积层对应,同样有两个池化层。
全连接层
MNIST 数据集中本来 28x28 的图片第一次卷积后仍然是 28x28,第一次池化后变为 14x14。第二次卷积后为 14x14,第二次池化后变为了 7x7。那么经过上面操作后得到了 64 张 7x7 的平面,也就是说经过第二个池化层后有 7x7x64 个神经元,第一个全连接层有 1024 个神经元。再使用 tf.reshape()变维函数,把池化层 2 的输出扁平化为 1 维,使用 tf.matmul()函数将得到的矩阵与神经元矩阵相乘后输出结果。第二个全连接层将刚刚得到的 1024 个神经元进一步处理,使用了 tf.nn.dropout()函数减轻过拟合,这是一种对具有深度结构的人工神经网络进行优化的方法,在学习过程中通过将隐含层的部分权重或输出随机归零,降低节点间的相互依赖性从而实现神经网络的正则化,降低其结构风险。最后使用了 tf.nn.softmax()函数来输出属于各个类的概率。大概流程如图:
tf.nn.softmax 的输入是 Tx1 的向量,输出也是 Tx1 的向量(也就是图中的prob[Tx1],这个向量的每个值表示这个样本属于每个类的概率),只不过输出10的向量的每个值的大小范围为 0 到 1。就是此公式
输出层
先使用函数 tf.nn.softmax_cross_entropy_with_logits(),让 softmax 的输出向量[Y1,Y2,Y3...]和样本的实际标签做一个交叉熵,交叉熵主要用于度量两个概率分布间的差异性信息,而交叉熵损失函数可以衡量两组数据的相似性。就是这个式子,外层使用 tf.reduce_mean()函数对向量求均值得到loss。之后又调用了 tf.train.AdamOptimizer 进行优化,Adam指自适应矩估计,是一个寻找全局最优点的优化算法,引入了二次梯度校正。此 函 数 的 函 数 原 型 为tf.train.AdamOptimizer.init(learning_rate=0.001,beta1=0.9,beta2=0.999, epsilon=1e-08, use_locking=False, name=’Adam’),在此算法中只重新指定了 learning_rate 为 1e-4,其他的参数都没有赋值,直接按默认值处理了 cross_entropy 交叉熵。之后使用 tf.equal()函数和 tf.argmax()函数把预测结果存入一个布尔列表中,再使用 tf.cast()函数转换数据类型,一般在要在训练前把图像的数据格式转换为 float32。就可以得到 accuracy。
训练过程
使用 tensorflow 的 Session 来创建会话,Session 提供了 Operation 执行和 Tensor 求值的环境,但是会话接受以后需要手动关闭,否则变量没有办法释放,因此使用了 with tf.Session()创建 Context 来执行,当上下文退出时自动释放避免报错。使用 feed_dict()对之前构造的placeholder 进行赋值,并迭代训练 30 次,训练过程中随机失活率设置为 0.65,之后再使用测试数据进行测试,并把之前求得的 accuracy 与测试数据进行对比,得到该算法的准确率。
实验结果
结果如下图所示,可以看到除了第一组准确率低一些以外,其他的迭代组准确率都高于 90%,最后稳定在 98%以上,准确率还是很高的,第一组的偏差是因为参数训练需要足够的数据,刚开始训练时数据量少,第一次一般都会偏低一些。另外附上内存占用量,方便和其他的算法进行对比。
平均内存占用大概 1G 左右,峰值大概 4G,CPU 使用平均 80%。
算法对比
与其他两种算法的对比。
KNN
如果一个样本在特征空间中的 K 个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。中文可以叫做邻近算法。
这种方法计算量较大,因为对每一个待分类的文本都要计算它到全体已知样12本的距离,才能求得它的 K 个最邻近点。对资源的占用确实太多了,这里附上其准确度和运行时的内存占用图,测试的机器总共 64G 内存基本被占满:
其测试准确率如上图,97.5%左右。
附测试代码:
import tensorflow as tf
import numpy as np
import random
from tensorflow.examples.tutorials.mnist import input_data
mnist=input_data.read_data_sets('MNIST_data',one_hot=True)
trainNum=60000 # 训练图片总数
testNum=10000 # 测试图片总数
trainSize=50000 # 训练时候用到的图片数量
testSize=200 # 测试时候用到的图片数量
k=80 # 距离最小的K个图片
trainIndex=np.random.choice(55000,trainSize,replace=False)
testIndex=np.random.choice(testNum,testSize,replace=False)
print(trainIndex.shape,testIndex.shape)
# 生成训练数据
trainData=mnist.train.images[trainIndex]
trainLabel=mnist.train.labels[trainIndex]
# 生成测试数据
testData=mnist.test.images[testIndex]
testLabel=mnist.test.labels[testIndex]
print('trainData.shape=',trainData.shape)
print('trainLabel.shape=',trainLabel.shape)
print('testData.shape=',testData.shape)
print('testLabel.shape=',testLabel.shape)
print('testLabel=',testLabel)
trainDataInput=tf.placeholder(shape=[None,784],dtype=tf.float32)
trainLabelInput=tf.placeholder(shape=[None,10],dtype=tf.float32)
testDataInput=tf.placeholder(shape=[None,784],dtype=tf.float32)
testLabelInput=tf.placeholder(shape=[None,10],dtype=tf.float32)
f1=tf.expand_dims(testDataInput,1) # 用expand_dim()来增加维度,将原来的testDataInput扩展成三维的,f1:(?,1,784)
f2=tf.subtract(trainDataInput,f1) # subtract()执行相减操作,即 trainDataInput-testDataInput ,最终得到一个三维数据
f3=tf.reduce_sum(tf.abs(f2),reduction_indices=2) # tf.abs()求数据绝对值,tf.reduce_sum()完成数据累加,把数据放到f3中
with tf.Session() as sess:
p1=sess.run(f1,feed_dict={testDataInput:testData[0:testSize]}) # 取testData中的前testSize个样本来代替输入的测试数据
print(p1)
print('p1=',p1.shape)
p2=sess.run(f2,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize]})
print('p2=',p2.shape)
p3=sess.run(f3,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize]})
print('p3=',p3.shape)
print('p3[0,0]=',p3[0,0]) # 输出第一张测试图片和第一张训练图片的距离
f4=tf.negative(f3) # 计算f3数组中元素的负数
f5,f6=tf.nn.top_k(f4,k=4) # f5:选取f4最大的四个值,即f3最小的四个值,f6:这四个值对应的索引
with tf.Session() as sess:
p4=sess.run(f4,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize]})
print('p4=',p4.shape)
print('p4[0,0]=',p4[0,0])
# p5=(5,4),每一张测试图片(共5张),分别对应4张最近训练图片,共20张
p5,p6=sess.run((f5,f6),feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize]})
print('p5=',p5.shape)
print('p6=',p6.shape)
print('p5:',p5,'\n','p6:',p6)
f7=tf.gather(trainLabelInput,f6) # 根据索引找到对应的标签值
f8=tf.reduce_sum(f7,reduction_indices=1) # 累加维度1的数值
f9=tf.argmax(f8,dimension=1) # 返回的是f8中的最大值的索引号
# 执行
with tf.Session() as sess:
p7=sess.run(f7,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize],trainLabelInput:trainLabel})
print('p7=',p7.shape)
print('p7:',p7)
p8=sess.run(f8,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize],trainLabelInput:trainLabel})
print('p8=',p8.shape)
print('p8:',p8)
p9=sess.run(f9,feed_dict={trainDataInput:trainData,testDataInput:testData[0:testSize],trainLabelInput:trainLabel})
print('p9=',p9.shape)
print('p9:',p9)
with tf.Session() as sess:
p10=np.argmax(testLabel[0:testSize],axis=1) # 如果p9=p10,代表正确
print(('p10:',p10))
j=0
for i in range(0,testSize):
if p10[i]==p9[i]:
j=j+1
# 输出准确率
print('准确率为',j*100/testSize,'%')
【注:本 KNN 算法设置的测试时候用图片数量为 200,最近的图片数量为 80,比较偏大了,如果参数调小的话资源占用也可以比较小,但是可能准确度偏差就比较大,比如每次调用 5 张选最近 4 张准确率一直都是 100%】
SVM
SVM 中文叫做支持向量机,是一类按监督学习方式对数据进行二元分类的广义线性分类器,其决策边界是对学习样本求解的最大边距超平面。可以分为普通SVM 和带核 SVM,普通的 SVM 是线性分类器,而带核 SVM 可以变成非线性的分类器。所谓带核就是含有一个核函数映射,比如线性核函数、多项式核函数、高斯核函数等。它的性能也是比较好的。
下面给出 SVM 的运算准确度(使用默认核函数 rbf):
准确率还是比较高,而且资源占用量也不算很大。
附测试代码:
from sklearn import svm
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=False)
train_num = 20000
test_num = 2000
x_train = mnist.train.images
y_train = mnist.train.labels
x_test = mnist.test.images
y_test = mnist.test.labels
# 获取一个支持向量机模型
predictor = svm.SVC(gamma='scale', C=1.0, decision_function_shape='ovr', kernel='rbf')
# 把数据丢进去
predictor.fit(x_train[:train_num], y_train[:train_num])
# 预测结果
result = predictor.predict(x_test[:test_num])
# 准确率估计
accurancy = np.sum(np.equal(result, y_test[:test_num])) / test_num
print(accurancy)