TF2.0深度学习实战(四):搭建AlexNet卷积神经网络


导读:
  欢迎大家来到《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分类器

数据集的下载地址:宝可梦数据集,提取码:dsxl

一、详解AlexNet

1.1 AlexNet 简介

  在2012年,多伦多大学的Alex Krizhevsky、Hinton等人提出了8层的深度神经网络模型AlexNet,并且因此而获得了ILSVRC12 挑战赛 ImageNet 数据集分类任务的冠军。为了纪念Alex Krizhevsky所做的突出贡献,所以将该网络结构命名为AlexNet。
  Hinton当时是多伦多大学的教授,现在是公认的人工智能领域三巨头之一。Deep Learning的概念就是由他提出来的。而Alex Krizhevsky是他当时的学生。
  AlexNet 模型的优越性能启发了业界朝着更深层的网络模型方向研究。自 AlexNet 模型提出后,各种各样的算法模型相继被发表,其中有 VGG 系列,GooLeNet,ResNet 系列,DenseNet 系列等等

1.2 AlexNet网络结构

  整体结构上,AlexNet包含8层;前5层是卷积层,剩下的3层是全连接层。全连接层的输出是1000维的,最后通过softmax的得到各个类别的概率,实现了1000分类。整体结构如下:
在这里插入图片描述
  由于当时计算机硬件性能有限,使得AlexNet采用了两块GTX580 3GB GPU进行分布式训练。但是现在的计算机水平完全可以考虑在单台电脑上跑。后面的实战过程中,我就将它简化为了单cpu/gpu版。
  具体每层网络结构如下:
  如果对卷积网络基本结构、输出图像大小的推导过程等不太了解,建议先戳戳:神经网络搭建:卷积层+激活函数+池化层+全连接层
在这里插入图片描述

1.3 AlexNet的创新之处

(1)层数达到了较深的8层,这在当时已经是个突破了
(2)采用了 ReLU 激活函数。成功地解决了以往使用Sigmoid函数而产生的梯度弥散问题;并且使得网络训练的速度得到了一定的提升。
想深入了解ReLU激活函数,可以参考这篇:深度学习笔记:激活函数全家桶
(3)引入了Dropout,提高了模型的泛化能力,防止了过拟合现象的发生。在AlexNet中主要是最后几个全连接层使用了Dropout。
(4)多GPU训练。受限于当时的计算机水平,使用多GPU,可以满足大规模数据集和模型的训练。
深入了解Dropout,可戳戳:防止过拟合(二):Dropout

1.4 AlexNet的性能

在ImageNet LSVRC-2012竞赛的数据集上进行1000分类。在测试数据上如下:
在这里插入图片描述
在 AlexNet 出现之前的网络模型都是浅层的神经网络,Top-5 错误率均在 25%以上,AlexNet 8 层的深层神经网络将 Top-5 错误率降低至 15.3.%,比第二名的26.2%低很多,性能提升巨大。
注:top-5错误率是指测试图像的正确标签不在模型预测的五个最可能的结果之中。

二、搭建AlexNet进行图片分类

2.1 自定义数据集加载

数据集介绍
  本次实验采用的是宝可梦数据集,该数据集共收集了皮卡丘(Pikachu)、超梦(Mewtwo)、杰尼龟(Squirtle)、小火龙(Charmander)和妙蛙种子(Bulbasaur)共 5 种精灵生物(看过神奇宝贝的盆友肯定知道~)
每种精灵的信息如下表:
在这里插入图片描述
自定义数据集加载过程一共分为三步:
(1)创建图片路径和标签,并写入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))  # images: string path, labels: number
db = db.shuffle(1000).map(preprocess).batch(32).repeat(20)

2.2 网络结构搭建

为了使得网络能在单cpu/gpu上训练,在保持原来的整体结构上做了如下几点微调:
(1)保持原有结构不变,使用但cpu/gpu进行训练
(2)由于原网络结构是进行1000分类,而我这里是进行5分类。所以将全连接层的节点个数相应的减少了。把原来的节点数2048、2048、100分别改为了: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()

2.3 迭代训练

  依据论文中的方法,采用的是随机梯度下降算法进行迭代训练。一次送入训练的batch数是32,一共对数据集训练20个epos,每20轮打印出一次测试精确度。

# 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()

2.4 可视化

每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()
发布了51 篇原创文章 · 获赞 135 · 访问量 5039

猜你喜欢

转载自blog.csdn.net/wjinjie/article/details/105163126