写在前面:大家好!我是【AI 菌】,一枚爱弹吉他的程序员。我
热爱AI、热爱分享、热爱开源
! 这博客是我对学习的一点总结与记录。如果您也对深度学习、机器视觉、算法、Python、C++
感兴趣,可以关注我的动态,我们一起学习,一起进步~
我的博客地址为:【AI 菌】的博客
我的Github项目地址是:【AI 菌】的Github
本教程会持续更新,如果对您有帮助的话,欢迎star收藏~
前言:
欢迎大家来到《TF2.0深度学习实战》第四讲,这一次我将复现经典的卷积神经网络AlexNet,然后对自定义数据集进行迭代训练,完成图片分类任务。之后我将使用TensorFlow2.0框架逐一复现经典的卷积神经网络:VGG系列、GooLeNet、ResNet 系列、DenseNet 系列,以及现在比较流行的:RCNN系列、YOLO系列等。
本博客将持续更新,欢迎关注。本着学习的心。希望和大家相互交流,一起进步~
学习记录:
深度学习环境搭建:Anaconda3+tensorflow2.0+PyCharm
TF2.0深度学习实战(一):分类问题之手写数字识别
TF2.0深度学习实战(二):用compile()和fit()快速搭建MINIST分类器
TF2.0深度学习实战(三):LeNet-5搭建MINIST分类器
TF2.0深度学习实战(五):搭建VGG系列卷积神经网络
TF2.0深度学习实战(六):搭建GoogLeNet卷积神经网络
数据集的下载地址:宝可梦数据集,提取码:9n21
在2012年,多伦多大学的Alex Krizhevsky、Hinton等人提出了8层的深度神经网络模型AlexNet,并且因此而获得了ILSVRC12 挑战赛 ImageNet 数据集分类任务的冠军。为了纪念Alex Krizhevsky所做的突出贡献,所以将该网络结构命名为AlexNet。
Hinton当时是多伦多大学的教授,现在是公认的人工智能领域三巨头之一。Deep Learning的概念就是由他提出来的。而Alex Krizhevsky是他当时的学生。
AlexNet 模型的优越性能启发了业界朝着更深层的网络模型方向研究。自 AlexNet 模型提出后,各种各样的算法模型相继被发表,其中有 VGG 系列,GooLeNet,ResNet 系列,DenseNet 系列等等
整体结构上,AlexNet包含8层;前5层是卷积层,剩下的3层是全连接层。全连接层的输出是1000维的,最后通过softmax的得到各个类别的概率,实现了1000分类。整体结构如下:
由于当时计算机硬件性能有限,使得AlexNet采用了两块GTX580 3GB GPU进行分布式训练。但是现在的计算机水平完全可以考虑在单台电脑上跑。后面的实战过程中,我就将它简化为了单cpu/gpu版。
如果对卷积网络基本结构、输出图像大小的推导过程等不太了解,建议先戳戳:神经网络搭建:卷积层+激活函数+池化层+全连接层
具体每层网络结构如下:
(1)层数达到了较深的8层,这在当时已经是个突破了
(2)采用了 ReLU 激活函数。成功地解决了以往使用Sigmoid函数而产生的梯度弥散问题;并且使得网络训练的速度得到了一定的提升。
(3)引入了Dropout,提高了模型的泛化能力,防止了过拟合现象的发生。在AlexNet中主要是最后几个全连接层使用了Dropout。
(4)多GPU训练。受限于当时的计算机水平,使用多GPU,可以满足大规模数据集和模型的训练。
在LSVRC-2012挑战赛的ImageNet数据集上进行1000分类。测试结果如下:
在 AlexNet 出现之前的网络模型都是浅层的神经网络,Top-5 错误率均在 25%以上,AlexNet 8层的深层神经网络将 Top-5 错误率降低至 15.3%,比第二名的26.2%低很多,性能提升巨大。
注:top-5错误率是指测试图像的正确标签不在模型预测的五个最可能的结果之中。
LSVRC:全称ImageNet Large Scale Visual Recognition Challenge
数据集介绍
本次实验采用的是宝可梦数据集,该数据集共收集了皮卡丘(Pikachu)、超梦(Mewtwo)、杰尼龟(Squirtle)、小火龙(Charmander)和妙蛙种子(Bulbasaur)共 5 种精灵生物(看过神奇宝贝的盆友肯定知道~),一共是1122张图片。
每种精灵的信息如下表:
自定义数据集加载过程一共分为三步:
(1)创建图片路径和标签,写入csv文件,然后再从csv文件读取存入字符串数组。
def load_csv(root, filename, name2label):
# root:数据集根目录
# filename:csv文件名
# name2label:类别名编码表
if not os.path.exists(os.path.join(root, filename)): # 如果不存在csv,则创建一个
images = [] # 初始化存放图片路径的字符串数组
for name in name2label.keys(): # 遍历所有子目录,获得所有图片的路径
# glob文件名匹配模式,不用遍历整个目录判断而获得文件夹下所有同类文件
# 只考虑后缀为png,jpg,jpeg的图片,比如:pokemon\\mewtwo\\00001.png
images += glob.glob(os.path.join(root, name, '*.png'))
images += glob.glob(os.path.join(root, name, '*.jpg'))
images += glob.glob(os.path.join(root, name, '*.jpeg'))
print(len(images), images) # 打印出images的长度和所有图片路径名
random.shuffle(images) # 随机打乱存放顺序
# 创建csv文件,并且写入图片路径和标签信息
with open(os.path.join(root, filename), mode='w', newline='') as f:
writer = csv.writer(f)
for img in images: # 遍历images中存放的每一个图片的路径,如pokemon\\mewtwo\\00001.png
name = img.split(os.sep)[-2] # 用\\分隔,取倒数第二项作为类名
label = name2label[name] # 找到类名键对应的值,作为标签
writer.writerow([img, label]) # 写入csv文件,以逗号隔开,如:pokemon\\mewtwo\\00001.png, 2
print('written into csv file:', filename)
# 读csv文件
images, labels = [], [] # 创建两个空数组,用来存放图片路径和标签
with open(os.path.join(root, filename)) as f:
reader = csv.reader(f)
for row in reader: # 逐行遍历csv文件
img, label = row # 每行信息包括图片路径和标签
label = int(label) # 强制类型转换为整型
images.append(img) # 插入到images数组的后面
labels.append(label)
assert len(images) == len(labels) # 断言,判断images和labels的长度是否相同
return images, labels
(2)创建数字编码表,并划分数据集
创建数字编码表是为了:将类别标签转化为数字标签。实现方法就是:将5个类别和数字1到5存入字典中,类别作为键key,数字编码作为值value。一个类对应一个值{键:值},是一一对应的关系。
def load_pokemon(root, mode='train'):
# 创建数字编码表
name2label = {} # 创建一个空字典{key:value},用来存放类别名和对应的标签
for name in sorted(os.listdir(os.path.join(root))): # 遍历根目录下的子目录,并排序
if not os.path.isdir(os.path.join(root, name)): # 如果不是文件夹,则跳过
continue
name2label[name] = len(name2label.keys()) # 给每个类别编码一个数字
images, labels = load_csv(root, 'images.csv', name2label) # 读取csv文件中已经写好的图片路径,和对应的标签
# 将数据集按6:2:2的比例分成训练集、验证集、测试集
if mode == 'train': # 60%
images = images[:int(0.6 * len(images))]
labels = labels[:int(0.6 * len(labels))]
elif mode == 'val': # 20% = 60%->80%
images = images[int(0.6 * len(images)):int(0.8 * len(images))]
labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
else: # 20% = 80%->100%
images = images[int(0.8 * len(images)):]
labels = labels[int(0.8 * len(labels)):]
return images, labels, name2label
(3)创建数据集对象。读入图片,对原图进行预处理,然后转化为张量。
def preprocess(image_path, label):
# x: 图片的路径,y:图片的数字编码
x = tf.io.read_file(image_path) # 读入图片
x = tf.image.decode_jpeg(x, channels=3) # 将原图解码为通道数为3的三维矩阵
x = tf.image.resize(x, [244, 244])
# 数据增强
# x = tf.image.random_flip_up_down(x) # 上下翻转
# x = tf.image.random_flip_left_right(x) # 左右镜像
x = tf.image.random_crop(x, [224, 224, 3]) # 裁剪
x = tf.cast(x, dtype=tf.float32) / 255. # 归一化
x = normalize(x)
y = tf.convert_to_tensor(label) # 转换为张量
return x, y
# 1.加载自定义数据集
images, labels, table = load_pokemon('pokemon', 'train')
print('images', len(images), images)
print('labels', len(labels), labels)
print(table)
db = tf.data.Dataset.from_tensor_slices((images, labels)) # 创建数据集对象
db = db.shuffle(1000).map(preprocess).batch(32).repeat(20) # 设置批量训练的batch为32,要将训练集重复训练20遍
为了使得网络能在单cpu/gpu上训练,在保持原来的整体结构上做了如下几点微调:
(1)保持原有结构不变,使用但cpu/gpu进行训练
(2)由于原网络结构是进行1000分类,而我这里是进行5分类。所以将全连接层的节点个数相应的减少了。把原来的节点数2048、2048、1000分别改为了:1024、128、5。
# 2.网络搭建
network = Sequential([
# 第一层
layers.Conv2D(48, kernel_size=11, strides=4, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 55*55*48
layers.MaxPooling2D(pool_size=3, strides=2), # 27*27*48
# 第二层
layers.Conv2D(128, kernel_size=5, strides=1, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 27*27*128
layers.MaxPooling2D(pool_size=3, strides=2), # 13*13*128
# 第三层
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第四层
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第五层
layers.Conv2D(128, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*128
layers.MaxPooling2D(pool_size=3, strides=2), # 6*6*128
layers.Flatten(), # 6*6*128=4608
# 第六层
layers.Dense(1024, activation='relu'),
layers.Dropout(rate=0.5),
# 第七层
layers.Dense(128, activation='relu'),
layers.Dropout(rate=0.5),
# 第八层(输出层)
layers.Dense(5)
])
network.build(input_shape=(32, 224, 224, 3)) # 设置输入格式
network.summary()
依据论文中的方法,我采用的是随机梯度下降算法进行迭代训练。由于我使用的是cpu进行训练,因此一次只送入32组数据进行迭代,一共对整个数据集训练20个epochs,每20轮打印出一次测试精确度。(如果使用GPU进行训练的话,可以相应的将batch数调大)
# 3.模型训练(计算梯度,迭代更新网络参数)
optimizer = optimizers.SGD(lr=0.01) # 声明采用批量随机梯度下降方法,学习率=0.01
acc_meter = metrics.Accuracy()
x_step = []
y_accuracy = []
for step, (x, y) in enumerate(db): # 一次输入batch组数据进行训练
with tf.GradientTape() as tape: # 构建梯度记录环境
x = tf.reshape(x, (-1, 224, 224, 3)) # 输入[b, 224, 224, 3]
out = network(x) # 输出[b, 5]
y_onehot = tf.one_hot(y, depth=5) # one-hot编码
loss = tf.square(out - y_onehot)
loss = tf.reduce_sum(loss)/32 # 定义均方差损失函数,注意此处的32对应为batch的大小
grads = tape.gradient(loss, network.trainable_variables) # 计算网络中各个参数的梯度
optimizer.apply_gradients(zip(grads, network.trainable_variables)) # 更新网络参数
acc_meter.update_state(tf.argmax(out, axis=1), y) # 比较预测值与标签,并计算精确度
if step % 10 == 0: # 每200个step,打印一次结果
print('Step', step, ': Loss is: ', float(loss), ' Accuracy: ', acc_meter.result().numpy())
x_step.append(step)
y_accuracy.append(acc_meter.result().numpy())
acc_meter.reset_states()
每20轮采集一次精确度数据,最后通过可视化显示出来。
# 4.可视化
plt.plot(x_step, y_accuracy, label="training")
plt.xlabel("step")
plt.ylabel("accuracy")
plt.title("accuracy of training")
plt.legend()
plt.show()
此数据集较小,因此使用单cpu即可完成训练,下图是我训练20个epos的过程中,测试精确度的变化。整个训练过程大概不到10分钟,测试精确度就达到了97%。当然,想得到更高的分类精度,可以多训练几个epos。
测试结果:
完整代码如下:
import tensorflow as tf # 导入TF库
from tensorflow.keras import layers, optimizers, datasets, Sequential, metrics # 导入TF子库
import os, glob
import random, csv
import matplotlib.pyplot as plt
# 创建图片路径和标签,并写入csv文件
def load_csv(root, filename, name2label):
# root:数据集根目录
# filename:csv文件名
# name2label:类别名编码表
if not os.path.exists(os.path.join(root, filename)): # 如果不存在csv,则创建一个
images = [] # 初始化存放图片路径的字符串数组
for name in name2label.keys(): # 遍历所有子目录,获得所有图片的路径
# glob文件名匹配模式,不用遍历整个目录判断而获得文件夹下所有同类文件
# 只考虑后缀为png,jpg,jpeg的图片,比如:pokemon\\mewtwo\\00001.png
images += glob.glob(os.path.join(root, name, '*.png'))
images += glob.glob(os.path.join(root, name, '*.jpg'))
images += glob.glob(os.path.join(root, name, '*.jpeg'))
print(len(images), images) # 打印出images的长度和所有图片路径名
random.shuffle(images) # 随机打乱存放顺序
# 创建csv文件,并且写入图片路径和标签信息
with open(os.path.join(root, filename), mode='w', newline='') as f:
writer = csv.writer(f)
for img in images: # 遍历images中存放的每一个图片的路径,如pokemon\\mewtwo\\00001.png
name = img.split(os.sep)[-2] # 用\\分隔,取倒数第二项作为类名
label = name2label[name] # 找到类名键对应的值,作为标签
writer.writerow([img, label]) # 写入csv文件,以逗号隔开,如:pokemon\\mewtwo\\00001.png, 2
print('written into csv file:', filename)
# 读csv文件
images, labels = [], [] # 创建两个空数组,用来存放图片路径和标签
with open(os.path.join(root, filename)) as f:
reader = csv.reader(f)
for row in reader: # 逐行遍历csv文件
img, label = row # 每行信息包括图片路径和标签
label = int(label) # 强制类型转换为整型
images.append(img) # 插入到images数组的后面
labels.append(label)
assert len(images) == len(labels) # 断言,判断images和labels的长度是否相同
return images, labels
# 首先遍历pokemon根目录下的所有子目录。对每个子目录,用类别名作为编码表的key,编码表的长度作为类别的标签,存进name2label字典对象
def load_pokemon(root, mode='train'):
# 创建数字编码表
name2label = {} # 创建一个空字典{key:value},用来存放类别名和对应的标签
for name in sorted(os.listdir(os.path.join(root))): # 遍历根目录下的子目录,并排序
if not os.path.isdir(os.path.join(root, name)): # 如果不是文件夹,则跳过
continue
name2label[name] = len(name2label.keys()) # 给每个类别编码一个数字
images, labels = load_csv(root, 'images.csv', name2label) # 读取csv文件中已经写好的图片路径,和对应的标签
# 将数据集按6:2:2的比例分成训练集、验证集、测试集
if mode == 'train': # 60%
images = images[:int(0.6 * len(images))]
labels = labels[:int(0.6 * len(labels))]
elif mode == 'val': # 20% = 60%->80%
images = images[int(0.6 * len(images)):int(0.8 * len(images))]
labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
else: # 20% = 80%->100%
images = images[int(0.8 * len(images)):]
labels = labels[int(0.8 * len(labels)):]
return images, labels, name2label
img_mean = tf.constant([0.485, 0.456, 0.406])
img_std = tf.constant([0.229, 0.224, 0.225])
def normalize(x, mean=img_mean, std=img_std):
x = (x - mean)/std
return x
# def denormalize(x, mean=img_mean, std=img_std):
# x = x * std + mean
# return x
def preprocess(image_path, label):
# x: 图片的路径,y:图片的数字编码
x = tf.io.read_file(image_path) # 读入图片
x = tf.image.decode_jpeg(x, channels=3) # 将原图解码为通道数为3的三维矩阵
x = tf.image.resize(x, [244, 244])
# 数据增强
# x = tf.image.random_flip_up_down(x) # 上下翻转
# x = tf.image.random_flip_left_right(x) # 左右镜像
x = tf.image.random_crop(x, [224, 224, 3]) # 裁剪
x = tf.cast(x, dtype=tf.float32) / 255. # 归一化
x = normalize(x)
y = tf.convert_to_tensor(label) # 转换为张量
return x, y
# 1.加载自定义数据集
images, labels, table = load_pokemon('pokemon', 'train')
print('images', len(images), images)
print('labels', len(labels), labels)
print(table)
db = tf.data.Dataset.from_tensor_slices((images, labels)) # images: string path, labels: number
db = db.shuffle(1000).map(preprocess).batch(32).repeat(20)
# 2.网络搭建
network = Sequential([
# 第一层
layers.Conv2D(48, kernel_size=11, strides=4, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 55*55*48
layers.MaxPooling2D(pool_size=3, strides=2), # 27*27*48
# 第二层
layers.Conv2D(128, kernel_size=5, strides=1, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 27*27*128
layers.MaxPooling2D(pool_size=3, strides=2), # 13*13*128
# 第三层
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第四层
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第五层
layers.Conv2D(128, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*128
layers.MaxPooling2D(pool_size=3, strides=2), # 6*6*128
layers.Flatten(), # 6*6*128=4608
# 第六层
layers.Dense(1024, activation='relu'),
layers.Dropout(rate=0.5),
# 第七层
layers.Dense(128, activation='relu'),
layers.Dropout(rate=0.5),
# 第八层(输出层)
layers.Dense(5)
])
network.build(input_shape=(32, 224, 224, 3)) # 设置输入格式
network.summary()
# 3.模型训练(计算梯度,迭代更新网络参数)
optimizer = optimizers.SGD(lr=0.01) # 声明采用批量随机梯度下降方法,学习率=0.01
acc_meter = metrics.Accuracy()
x_step = []
y_accuracy = []
for step, (x, y) in enumerate(db): # 一次输入batch组数据进行训练
with tf.GradientTape() as tape: # 构建梯度记录环境
x = tf.reshape(x, (-1, 224, 224, 3)) # 将输入拉直,[b,28,28]->[b,784]
out = network(x) # 输出[b, 10]
y_onehot = tf.one_hot(y, depth=5) # one-hot编码
loss = tf.square(out - y_onehot)
loss = tf.reduce_sum(loss)/32 # 定义均方差损失函数,注意此处的32对应为batch的大小
grads = tape.gradient(loss, network.trainable_variables) # 计算网络中各个参数的梯度
optimizer.apply_gradients(zip(grads, network.trainable_variables)) # 更新网络参数
acc_meter.update_state(tf.argmax(out, axis=1), y) # 比较预测值与标签,并计算精确度
if step % 10 == 0: # 每200个step,打印一次结果
print('Step', step, ': Loss is: ', float(loss), ' Accuracy: ', acc_meter.result().numpy())
x_step.append(step)
y_accuracy.append(acc_meter.result().numpy())
acc_meter.reset_states()
# 4.可视化
plt.plot(x_step, y_accuracy, label="training")
plt.xlabel("step")
plt.ylabel("accuracy")
plt.title("accuracy of training")
plt.legend()
plt.show()