1.项目任务
在常见深度学习模型的基础上(本文为LeNet-5),调整模型结构和参数,使用Tensorflow进行部署。利用公开的德国交通标志数据集进行训练,得到模型,并利用该模型对新的图片进行预测。全文运行环境Linux, 使用语言Python3。需要说明的是,本项目来源于Udacity,在Udacity无人驾驶工程师课程作业的基础上演化而来。
无人驾驶工程师课程_无人车培训_无人驾驶_自动驾驶基础技术-优达学城(Udacity)官网cn.udacity.com
2. 整体思路整体流程
总体而言,本文注重梳理卷积神经网络实现的基本逻辑,希望通过这个小项目打开深度学习的大门,拨开深度学习这颗“大洋葱”最外面的一层表皮,至于里面的果实及本文中未理解清楚的一部分,希望能有机会在后面的学习中解决。
3. 项目实施
3.1 环境搭建
本项目在Linux环境中运行,使用python3搭建Tensorflow模型进行运算。因此第一步先在Linux下搭建Tensorflow运行环境。可以采用anaconda作为python的环境管理仓库,在anaconda中直接通过命令行安装tensorflow。
3.1.1 Anaconda 的安装
Anaconda 为我们提供了很好的python环境管理机制,使同一台电脑上能够允许多个版本的python共存,且相互不干扰;它还能创建独立的环境,相当于在一台主机中分割出一个个独立的空间,存放不同的工作环境。
# 首先到官网上下载anaconda
wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh
# 安装anaconda
bash Anaconda3-5.0.1-Linux-x86_64.sh
# 将anaconda添加到环境变量
echo 'export PATH="~/anaconda3/bin:$PATH"' >> ~/.bashrc
# 更新环境变量
source .bashrc
# 更新anaconda
conda upgrade --al
具体可以参考:linux命令 安装anacondablog.csdn.net
3.1.2 Tensorflow的安装
Tensorflow目前最常使用的是版本1.1.1, 它除了正常迭代的版本号之外,还有CPU和GPU之分。本文中的项目模型复杂度不高,需要的算力CPU就能解决,因此在这里选择安装CPU版本的tensorflow。
# 创建一个单独运行的环境,取名py37,采用python3
conda create --name = py37 python=3
# 激活刚刚创建的环境
source acitvate py37
# 安装CPU版本的Tensorflow(此过程需联网)
conda install -c conda-forge tensorflow
安装完成后,我们需要检查tensorflow是否安装好,进入pyhon环境:
import tensorflow as tf
如果没有报错,证明该环境中已经有了tensorflow,安装正确。
激活环境后,进入jupyter notebook工作环境:
#直接键入 jupyter notebook
PS-1:在调试过程中需要掌握一些anaconda常用命令,列举如下,已经会的可直接跳过:
1)查看版本:conda --version
2)创建环境:conda create -n py37 python=3
3)删除环境:conda remove -n py37 --all
4)查看本机所有环境:conda env list
5)激活环境:activate py37
6)安装第三方软件包:conda install 包名 或 pip install 包名
7)卸载第三方软件包: conda remove 包名 或 pip uninstall 包名
8)切换到base环境:activate
PS-2: GPU版本Tensorflow的安装
1)pip install tensorflow-gpu
需要注意的是,不要在同一个环境中共存CPU和GPU版本的Tensorflow, 容易出现环境混乱。
3.2 数据集处理
3.2.1 数据集导入
本文使用的数据集是公开的德国交通标志数据集, 下载地址:German Traffic Sign Benchmarksbenchmark.ini.rub.de
已经被分为了训练集,验证集和测试集三个部分,分别是三个.p 文件,将三个载入。
# 导入pickle模块,用于加载数据,如果该句报错,需要使用anaconda 安装pickle包。在导入其他包
# 的过程中,如果出现没有该模块的情况,也需要使用anaconda安装。
# pickle 用来将python的对象序列化和解序列化
import pickle
# 找到三个数据文件的路径
training_file = '/home/project_our/project2/traffic-signs-data/train.p'
validation_file='/home/project_our/project2/traffic-signs-data/valid.p'
testing_file = '/home/project_our/project2/traffic-signs-data/test.p'
# 加载数据
with open(training_file, mode='rb') as f:
train = pickle.load(f)
with open(validation_file, mode='rb') as f:
valid = pickle.load(f)
with open(testing_file, mode='rb') as f:
test = pickle.load(f)
# 将数据分为图片和标签两个部分,X部分为图片,y为对应图片的标签值
X_train, y_train = train['features'], train['labels']
X_valid, y_valid = valid['features'], valid['labels']
X_test, y_test = test['features'], test['labels']
3.2.2 数据集情况总结
### 查看图片的尺寸,图片的数量验证数据集中图片数量和标签数量是否匹配
import numpy as np
# 训练数据数量
n_train = len(X_train)
# 验证数据数量
n_validation = len(X_valid)
# 测试数据数量
n_test = len(X_test)
# 图片尺寸
image_shape = X_train[1].shape
# 图片种类数
n_classes = len(np.unique(y_train))
print("Number of training examples =", n_train)
print("Number of testing examples =", n_test)
print("Image data shape =", image_shape)
print("Number of classes =", n_classes)
print("Number of classes =", n_validation)
输出数据集中共有训练数据34799张,验证数据4410张,测试数据12630张。训练数据标签数34799, 验证数据标签数4410。标签数量与图片数量一致。共有43种不同类型的交通标志牌。每张图片的大小为32x32x3。数据集样本量统计
### 数据可视化
# 导入随机函数
import random
# 导入python 画图功能包
import matplotlib.pyplot as plt
# 该语句称为”魔术公式” 能够让jupyter notebook 显示图片
%matplotlib inline
# 随机选取一个索引
index = random.randint(0, len(X_train))
# 从训练集中选取对应索引的图片
image = X_train[index]#不懂;
# 打印显示图片
plt.figure(figsize = (1,1))
plt.imshow(image, cmap="gray")
print(y_train[index])数据可视化
随机抽取的图片为直行的标志,对应的标签为35。
# 导入matplotlib下的cm模块
import matplotlib.cm as cm
# 以图表方式显示样本中各个标签样本的数量
bin = [x for x in range(0,43)]
plt.ylabel('Count of Examples')
plt.xlabel('Traffic Sign Number')
plt.hist(y_train, bin, color = 'r', rwidth = 0.8, label = 'train')
plt.hist(y_test, bin, color = 'c', rwidth = 0.8, label = 'test')
plt.hist(y_valid, bin, color = 'b', rwidth = 0.8, label = 'valid')
plt.legend()
plt.show()数据集样本分布
3.2.3 数据前处理
有很多方法对数据集中的图片进行处理,比如正则化,灰度化, 锐度处理,旋转等。对数据的前处理能够增强网络的鲁棒性,避免因为数据同质化或局部同质化造成模型局部收敛。
本文这里只进行正则化处理,对于图片,采用128进行正则化,性能较优。
# 定义正则化函数
def normalized(image):
return (image-128.)/128.
# 处理训练数据
X_train_normalized = []
for i in range(0,n_train):
X_train_normalized.append(normalized(X_train[i]))
# 处理验证数据
X_valid_normalized = []
for j in range(0, n_validation):
X_valid_normalized.append(normalized(X_valid[j]))
# 处理测试数据
X_test_normalized = []
for s in range(0, n_test):
X_test_normalized.append(normalized(X_test[s]))原始图像与正则化后的图像
3.3 模型搭建
3.3.1 卷积神经网络的基础知识
1) 一个卷积神经网络的基本组成输入层
卷积层
非线性变化(Relu)
池化(子采样)
分类(完全链接层)
2)各层的作用
(1)卷积层的作用:相当于滤波器,可以用来提取图片中的特征(点,线,图形等),不同的卷积核就相当于不同的滤波器,能够提取到不同的特征。
# 常用卷积函数,以Tensorflow 为例
tf.nn.convolution()
tf.nn.conv2d()
tf.nn.conv3d_transpose
# 以上只列举了3种卷积函数,没有列全,具体三个函数之间有什么区别还没有仔细研究
(2) 非线性变化:在卷积神经网络中引入非线性因数,因为实际生活中我们用神经网络解决的问题都是非线性的。目前最常用的非线性变化方法为Relu(修正线性单元),
output = Max(zeros, Input)ReLu 修正线性单元
(3)池化:也称为子采样或下采样,可降低每个特征映射的维度,并保留最重要的信息。翻译成人话:池化就是提取特征映射中最重要的信息,降低特征的维度,减少计算量(在保留数据特征的前提下,最大程度的降低运算量)。选取特征映射中最重要信息的方法有很多种,例如最大池化,提取每个每个特征向量中的最大值,其他删掉。一般跟在卷积层的后面。
# 常用池化函数
tf.nn.avg_pool() 平均池化
tf.nn.max() 最大池化
(4)全链接层:利用卷积层和池化层在数据集的基础上得到的特征,将输入图像分为不同的类。也就是利用上述的特征判断输入图像属于哪个标签的概率。
# 常用的分类函数
sigmoid_cross_entropy_with_logits
softmax
log_softmax
softmax_cross_entropy_with_logits
总结以上:卷积层+池化层======= 从输入图像提取特征;
完全链接层========== 用来做分类器
3)图片在卷积神经网络中的“生命”流程
a)输入一张图像:猪;
b)输入标签:比如识别猫,狗,鸟,猪 四种动物,那我们输入的标签应该是[0,0,0,1]
c) 迭代:
(1)卷积核(滤波器)参数和权重初始化:一般采用符合高斯分布的随机值;
(2)对输入图像使用“前向传播”:包括一系列卷积,Relu,池化和全链接,得到每个类的输出概率
比如说是[0.2, 0.4, 0.1, 0.3](这是个随机概率,因为一次的权重是随机的)
(3)计算输出层的总误差(4个类总误差之和)
(4)使用反向传播,计算网络中所有权重的误差梯度,并使用梯度下降更新所有 滤波器的权重
和误差,更新的目标是最小化输出误差。
(5)对训练集中的所有图像重复(2)-(4)
d)利用模型进行预测
(1)对于一个新图像,模型利用之前训练的参数进行前向传播得到分类概率。
3.3.2 LeNet-5 框架
LeNet-5框架
LeNet-5原本用于手写英文字母的识别,如上图所示,主要又5层组成,分别是:输入层-卷积层1-池化层1-卷积层2-池化层2-卷积层3-全链接层1-全链接层2(输出层)。
对于原始LeNet-5的详细解析可以参考一下文章:网络解析(一):LeNet-5详解cuijiahua.com
3.3.3 搭建模型
# 导入tensorflow
import tensorflow as tf
# 设置迭代代数
EPOCHS = 20
# 设置没批处理图片的张数:数据集图片太多,受内存大小的限制,需要规定每次循环迭代处理的图片数量,
# 理论上批量越大,模型运转越快,但批量大小受内存大小的限制;
BATCH_SIZE = 128
定义模型
from tensorflow.contrib.layers import flatten
def LeNet(x, keep_prob):
# 每一层初始化weight和bias时的均值和方差
mu = 0
sigma = 0.1
# 第一层卷积,输入32x32x1, 输出 28x28x6。卷积滤波器大小5x5,输入深度为3,输出深度为6
conv1_w = tf.Variable(tf.truncated_normal(shape=(5, 5, 3, 6),mean = mu, stddev=sigma))
# 参数的维度,均值,方差
conv1_b = tf.Variable(tf.zeros(6))
# 使用conv2d函数对滤波器和图像进行卷积运算,然后再加上偏差
conv1 = tf.nn.conv2d(x,conv1_w, strides=[1,1,1,1],padding='VALID') + conv1_b
# 非线性变换(激活函数)
conv1 = tf.nn.relu(conv1)
# 池化或下采样 输入28x28x6,输出14x14x6: 使用2x2的卷积核,并以2x2的步长对输出进行池化
conv1 = tf.nn.max_pool(conv1,ksize=[1, 2, 2, 1],strides=[1, 2, 2, 1], padding='VALID')
# 第二层卷积,输入14x14x6, 输出10x10x16
conv2_w = tf.Variable(tf.truncated_normal(shape=(5, 5, 6, 16), mean = mu, stddev=sigma))
conv2_b = tf.Variable(tf.zeros(16))
conv2 = tf.nn.conv2d(conv1, conv2_w, strides = [1,1,1,1], padding='VALID') + conv2_b
# 非线性变换(激活函数)
conv2 = tf.nn.relu(conv2)
# 池化(下采样),输入10x10x16, 输出是5x5x16
conv2 = tf.nn.max_pool(conv2,ksize=[1, 2, 2, 1],strides=[1, 2, 2,1], padding='VALID')
# 压平,把输出排成一列,得到一个向量,输入5x5x16,输出 400
fc0 = flatten(conv2)
# 第三层:全链接层,输入400,输出120
fc1_w = tf.Variable(tf.truncated_normal(shape=(400, 120),mean = mu, stddev = sigma))
fc1_b = tf.Variable(tf.zeros(120))
fc1 = tf.matmul(fc0, fc1_w)+ fc1_b
# 非线性变换(激活函数)
fc1 = tf.nn.relu(fc1)
# dropout
fc1 = tf.nn.dropout(fc1, keep_prob)
# 第四层:全链接层,输入120, 输出84
fc2_w = tf.Variable(tf.truncated_normal(shape=(120, 84),mean = mu, stddev = sigma))
fc2_b = tf.Variable(tf.zeros(84))
fc2 = tf.matmul(fc1, fc2_w)+ fc2_b
# 非线性变换(激活函数)
fc2 = tf.nn.relu(fc2)
# dropout2
fc2 = tf.nn.dropout(fc2, keep_prob)
# 第五层:全链接层,输入84, 输出10。层的宽度等于标签集中所包含的类别数
fc3_w = tf.Variable(tf.truncated_normal(shape=(84,43), mean = mu, stddev = sigma))
fc3_b = tf.Variable(tf.zeros(43))
#这些输出被称为对数几率(logits)
logits = tf.matmul(fc2, fc3_w)+ fc3_b
return logits
以上使用Tensorflow重新部署了LeNet模型,但原本的LeNet模型用于手势识别,需要针对我们当前的任务进行调整。具体的调整体现在:
(1)输入数据维度,原来的LeNet-5输入为32x32x1,但我们数据集中的尺寸为32x32x3
(2)各个卷积层对应的尺寸都进行了调整
(3)增加了两个池化层,防止过拟合
3.3.4 训练-验证-测试模型
Tensorflow 使用图计算,在开始训练模型之前,需要搭建图的结构。等把整个运算流程图搭建好之后,在运算的过程中通过feed_dict填充具体的数据。
参数占位:
# 占位符,为输入数据占位子,None 表示batch_size 批量的大小,可以接收任意批量大小的输入数据
x = tf.placeholder(tf.float32,(None, 32, 32, 3))
# 为标签占位子,标签维度为1,批量为任意值
y = tf.placeholder(tf.int32,(None))
# 为下采样概率占位子,以keep_prob的概率丢弃一部分的特征
keep_prob = tf.placeholder(tf.float32)
# 独热编码
one_hot_y = tf.one_hot(y, 43)
搭建训练框图,设定参数:优化器类型,优化目标
## 搭建训练框图(只是一个流程,还没有输入数据)
# 学习速率,决定模型中更新权重的速率,默认0.001是一个比较好的初始速度;
rate = 0.001
# 将图片输入的LeNet模型中计算对数几率
logits = LeNet(x,keep_prob)
# 分类函数,将对数几率结果和真实标签进行比较,并计算交叉熵
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels = one_hot_y,logits = logits)
# 求出所有训练图像的交叉熵的平均值,交叉熵越小,模型值越好
loss_operation = tf.reduce_mean(cross_entropy)
# 采用Adam优化方法优化参数(可以动态调整每个参数的学习速率)
optimizer = tf.train.AdamOptimizer(learning_rate = rate)
# 设定优化目标,在优化器上运行最小化函数
training_operation = optimizer.minimize(loss_operation)
搭建评估模型性能的框图:
# tf.argmax返回最大值的索引
# tf.equal返回True or false
correct_prediction = tf.equal(tf.argmax(logits,1), tf.argmax(one_hot_y,1))
# 计算模型的整体准确度:所有预测的准确度的平均值
accuracy_operation = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 建立保存模型的对象
saver = tf.train.Saver()
# 构建一个评估函数,输入是一整个数据集
def evaluate(x_data, y_data):
num_examples = len(x_data)
total_accuracy = 0
#获取当前的默认会话
sess = tf.get_default_session()
# 对数据进行分批评估
for offset in range(0, num_examples, BATCH_SIZE):
batch_x, batch_y = x_data[offset:offset+BATCH_SIZE], y_data[offset:offset+BATCH_SIZE]
accuracy = sess.run(accuracy_operation, feed_dict={x:batch_x, y:batch_y, keep_prob:1.0})
# 计算总精度
total_accuracy+=(accuracy* len(batch_x))
return total_accuracy / num_examples
开始训练,并保存模型:
## 创建一个函数,训练并验证模型
# 创建一个TensorFlow会话,并初始化变量
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
print("Training...")
print()
for i in range(EPOCHS):
# 在没轮训练开始前打乱数据的顺序,确保数据顺序不会影响模型
X_normal_image, y_train = shuffle(X_normal_image, y_train)
# 将数据集分成几批,分批训练模型
for offset in range(0, n_train, BATCH_SIZE):
end = offset + BATCH_SIZE
batch_x, batch_y = X_normal_image[offset:end], y_train[offset:end]
sess.run(training_operation, feed_dict={x:batch_x, y:batch_y, keep_prob:0.8})
# 每轮结束的时候评估模型的精度
train_accuracy = evaluate(X_normal_image, y_train)
# 每轮结束的时候利用验证数据集评估模型精度
validation_accuracy = evaluate(X_valid_normalized, y_valid)
print("EPOCH {}...".format(i+1))
print("Train accuracy = {:.3f}" .format(train_accuracy))
print("Validation accuracy = {: .3f}" .format(validation_accuracy))
print()
# 一但模型训练结束,保存模型
save_file = './model2'
saver = tf.train.Saver()
saver.save(sess, save_file)
print("Model saved")训练过程
模型训练完成后会输出4个文件:其中:checkpoint为文本文件,里面记录了保存的最新的checkpoint文件以及其它checkpoint文件列表; train_model.ckpt.meat 是图结构,meta文件是pb(protocol buffer)格式文件,包含变量、op、集合等; ckpt保存每个变量的取值,此处文件名的写入方式会因不同参数的设置而不同。是二进制文件,保存了所有的weights、biases、gradients等变量。在tensorflow 0.11之 前,保存在.ckpt文件中。0.11后,通过两个文件保存,如:.data-00000-of-00001和.index文件。
参考:Tensorflow模型保存与加载 - 追求沉默者 - 博客园www.cnblogs.com
测试模型:
# 使用测试数据,评价模型
# 只有彻底完成训练,并且选定模型之后我们才可以用测试数据集评估模型
with tf.Session() as sess:
# 导入刚刚运行的模型,输入test数据和对应的标签
new_saver = tf.train.import_meta_graph('model2.meta')
new_saver.restore(sess, tf.train.latest_checkpoint('.'))
test_accuracy = evaluate(X_test_normalized, y_test)
print("Test Accuracy = {:.3f}".format(test_accuracy))测试数据精度
测试集的输出精度为0.939
3.3.5 利用模型对新图片进行预测
## 导入新图片,并进行处理
import numpy as np
import os
from scipy import ndimage, misc
import matplotlib.image as mpimg
# 解决ros的opencv和anaconda 中opencv的冲突问题
import sys
sys.path.remove('/opt/ros/kinetic/lib/python2.7/dist-packages')
# 导入opencv
import cv2
import matplotlib.pyplot as plt
# 导入图片
test_images = os.listdir("web_images/")
print("test_images: ok")
image1=[]
image2=[]
for images in test_images:
image1 = cv2.imread('web_images/' + images)
print(image1.shape)
image1 = cv2.resize(image1,(32,32))
plt.imshow(image1)
#确保每张图片都保存起来
image2.append(image1)
# 同时打印5张图片
for sk in range(0,5):
plt.imshow(image2[sk])
plt.show()待预测图片
# 正则化
import numpy as np
image3=[]
for i in range(5):
image3.append((image2[i] - 128.)/128.)
# 真实标签
image_labels = [4, 23, 25, 28, 31]
predicted_labels = tf.argmax(logits, axis=1)
with tf.Session() as sess:
# 初始化参数
init=tf.initialize_all_variables()
sess.run(init)
# 导入模型
new_saver2 = tf.train.import_meta_graph('model2.meta')
new_saver2.restore(sess, tf.train.latest_checkpoint('.'))
# 开始预测
predicted_logits = sess.run(predicted_labels,feed_dict={x:image3, keep_prob:1.0})
print("predicted labels: ", predicted_logits)
print("real labels:", image_labels)
输出结果 [4, 23, 28, 30, 31],准确率80%
4. 后续还可以调整参数提高预测准确率:步长, batch_size, 图像预处理等。由于篇幅限制,本文没有进行尝试。行文仓促,如有不正确的地方还望见谅,可以指出,一起讨论。
其他参考文献:
2) TensorFlow 技术解析与实战