从LeNet-5到GoogLeNet,聊聊深度学习的前世今生!

写在前面:大家好!我是【AI 菌】。我热爱AI、热爱分享、热爱开源! 这博客是我对学习的一点总结与思考。如果您也对 深度学习、机器视觉、算法、Python、C++ 感兴趣,可以关注我的动态,我们一起学习,一起进步~
我的博客地址为:【AI 菌】的博客
我的Github项目地址是:【AI 菌】的Github

第一章 深度学习之图像识别

在这一章,我们将把深度学习理论与TensorFlow2.0算法实战相结合,用来搭建我们的图像分类器,实现计算机视觉领域的一个基础任务——图像识别。在本章每一小节中,我将分为理论和实战两部分进行讲解。理论部分,我会对相应的深度学习算法进行必要的阐述;实战部分,我会使用TensorFlow2.0框架手把手带你搭建卷积神经网络,实现图像识别任务。总之,通过这一章的案例,您将学习到:

  1. 搭建第一个图像分类器,实现手写数字识别。
  2. 搭建第一个卷积神经网络LeNet-5,实现Fashion MNIST数据集分类器。
  3. 搭建经典的AlexNet卷积神经网络,实现对宝可梦数据集的分类。
  4. 搭建经典的GooLeNet卷积神经网络,实现对花数据集的分类与图像识别。

1.1 手写数字识别

1.1.1 手写数字识别简介

手写数字识别是一个非常经典的图像分类任务,经常被作为深度学习入门的第一个指导案例。相当于我们学编程语言时,编写的第一个程序“Hello World !”。不一样的是,入门深度学习需要有一定量的理论基础,这部分可以本书参考前面的章节。
手写数字识别是基于MNIST数据集的一个图像分类任务,目的是通过搭建深度神经网络,实现对MNIST数据集中手写数字图像的识别。

1.1.2 MNIST数据集介绍

为了方便业界统一测试和评估算法, Lecun, Bottou, Bengio和Haffner在1998年发布了手写数字图片数据集,命名为 MNIST,它包含了 0~9 共 10 种数字的手写图片,每种数字一共有 7000 张图片,采集自不同书写风格的真实手写图片,一共 70000 张图片。其中60000张图片作为训练集,用来训练模型。10000张图片作为测试集,用来训练或者预测。训练集和测试集共同组成了整个 MNIST 数据集。
MNIST数据集中的每张图片,大小为28 × \times × 28,同时只保留灰度信息(即单通道)。下图是MNIST数据集中的部分图片:
在这里插入图片描述

1.1.3 手写数字识别实战

这一小节,我们将学习使用TensorFlow2.0深度学习框架搭建一个简单的3层全连接网络,用于实现对MNIST数据集中的手写数字进行分类或识别。
1. 数据集准备
(1)在tensorflow2.0官方API中提供了自动加载MNIST数据集的函数,我们直接使用即可。

# 加载MNIST数据集,返回的是两个元组,分别表示训练集和测试集
(x, y), (x_val, y_val) = datasets.mnist.load_data()  

(2)将数据集格式转换为张量,方便进行张量运算,并且将灰度值缩放到0-1,方便训练。

x = tf.convert_to_tensor(x, dtype=tf.float32)/255.  # 转换为张量,并缩放到0~1
y = tf.convert_to_tensor(y, dtype=tf.int32)  # 转换为张量(标签)

(3)构建数据集对象,设置batch和epos。

train_dataset = tf.data.Dataset.from_tensor_slices((x, y))  # 构建数据集对象
train_dataset = train_dataset.batch(32).repeat(10)  # 设置批量训练的batch为32,要将训练集重复训练10遍

2. 网络结构的搭建
由于MNIST数据集里的图像特征较为简单,所以本次以搭建一个3层的全连接网络为例,来实现MNIST数据集10分类任务。其中,每个全连接层的节点数分别为:256,128和10。

# 搭建3层全连接网络
network = Sequential([
    layers.Dense(256, activation='relu'),  # 第一层
    layers.Dense(128, activation='relu'),  # 第二层
    layers.Dense(10)  # 输出层
])
network.build(input_shape=(None, 28*28))  # 输入
network.summary()  # 打印出网络的参数列表

3. 模型的装配与训练
(1)模型的装配。搭建好网络结构后,即可通过TensorFlow2.0内置函数compile()指定网络使用的优化器对象,损失函数,评价指标等:

# 模型装配
network.compile(optimizer=optimizers.Adam(lr=0.01),  # 指定Adam优化器,学习率为0.01
              loss=tf.losses.CategoricalCrossentropy(from_logits=True),  # 指定采用交叉熵损失函数,包含Softmax
              metrics=['accuracy'])  # 指定评价指标为准备率

(2)模型的训练。模型装配完成后,就可以通过TensorFlow2.0内置函数fit(),将数据送入神经网络进行训练,同时打印出图片分类精度的测试结果。

# 模型训练
# 输入训练集train_dataset,训练20个epochs; 每训练完2个epochs,对验证集val_dataset验证一次
network.fit(train_dataset, epochs=20, validation_data=val_dataset, validation_freq=2)
network.evaluate(val_dataset)  # 打印输出验证后的结果(loss和accuracy)

4. 图像分类效果
在图像分类或识别任务中,经常会将预测精确度accuracy作为评价一个分类器好坏的指标。当accuracy越接近1(100%)时,说明该分类器的预测效果越好。
下图展示了训练过程中的一个测试结果。可以看到,此时训练集上的准确度达到0.9780,验证集上的准确度达到了0.9716。已经很接近1了,继续训练应该可以得到更高的精确度。
在这里插入图片描述
注:本小节只对核心代码进行了必要的讲解和呈现,如需完整代码文件,请到我的Github地址下载:【AI 菌】的Github

1.2 第一个卷积神经网络LeNet-5

1.2.1 LeNet-5简介

在 1990 年代,Yann LeCun 等人提出了用于手写数字和机器打印字符识别的神经网络,被命名为 LeNet-5 (Lecun, Bottou, Bengio, & Haffner, 1998)。LeNet-5 的提出,使得卷积神经网络在当时能够成功被商用,广泛应用在邮政编码、支票号码识别等任务中。

1.2.2 Fashion MNIST数据集简介

本次实验采用的是Fashion MNIST数据集。Fashion MNIST是一个定位在比MNIST识别问题更复杂的数据集,它的设定与MNIST几乎完全一样,包含了 10 类不同类型的衣服、鞋子、包等灰度图片,图片大小为28x28,共 70000 张图片,其中 60000 张用于训练集,10000 张用于测试集。如图下图所示,每行对应一种类别。
在这里插入图片描述

1.2.3 LeNet-5网络结构介绍

LeNet-5是第一个被成功商用的卷积神经网络。它的网络结构如下图所示,一共由5层组成,其中前两层为卷积层,后三层是全连接层。
在这里插入图片描述LeNet-5网络逐层结构:
图片输入:32 × \times × 32 × \times × 1
第一层:卷积核( 6个,5 × \times × 5 × \times × 1 ,步长:1),输出:28 × \times × 28 × \times × 6
最大池化层:卷积核(2 × \times × 2,步长:2),输出:14 × \times × 14 × \times × 6
第二层:卷积核(16个,5 × \times × 5 × \times × 6,步长:1),输出:10 × \times × 10 × \times × 16
最大池化层:卷积核(2 × \times × 2,步长:2),输出:5 × \times × 5 × \times × 16
拉直后输出:5 × \times × 5 × \times × 16 = 400
全连接层1:120个节点
全连接层2:84个节点
输出层:10个节点
注:在进行卷积运算时,没有对输入进行填充。因此第一层的输出size=32-5+1=28,第二层的输出size=14-5+1=10

1.2.4 搭建LeNet-5实现Fashion MNIST分类器实战

在本小结,我们将学习用TensorFlow2.0框架搭建第一个卷积神经网络LeNet-5,用来实现对Fahion MNIST 数据集中的10类图片进行分类。

1. 数据集准备
在TensorFlow2.0官方API中提供了自动加载Fashion MNIST数据集的函数,我们直接使用即可。

# 加载MNIST数据集,返回的是两个元组,分别表示训练集和测试集
(x, y), (x_val, y_val) = datasets.fashion_mnist.load_data()  
# 转换为张量,并缩放到0~1
x = tf.convert_to_tensor(x, dtype=tf.float32)/255. 
# 转换为张量(标签)
y = tf.convert_to_tensor(y, dtype=tf.int32) 
# 构建数据集对象 
train_dataset = tf.data.Dataset.from_tensor_slices((x, y)) 
# 设置批量训练的batch为32,要将训练集重复训练10遍
train_dataset = train_dataset.batch(32).repeat(10)

2. 搭建网络结构
由于MNIST数据集的图片大小为28 × \times × 28,与原输入32 × \times × 32不一致,因此这里需要对网络结构进行了简单的调整,主要表现在两个方面:(1)将输入改为为 28 × \times × 28 的大小(2)卷积核的尺寸变为3 × \times × 3,个数保持不变。

network = Sequential([  # 搭建网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层,6个3*3*1卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 池化层,卷积核2*2,步长2
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=3, strides=1),  # 第二个卷积层,16个3*3*6卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 拉直,方便全连接层处理

    layers.Dense(120, activation='relu'),  # 全连接层,120个节点
    layers.Dense(84, activation='relu'),  # 全连接层,84个节点
    layers.Dense(10)  # 输出层,10个节点
])
network.build(input_shape=(None, 28, 28, 1))  # 定义输入,batch_size=32,输入图片大小是28*28,通道数为1。
network.summary()  # 显示出每层的参数列表

3. 模型的装配与训练
搭建好网络结构后,首先要对网路模型装配,指定网络使用的优化器对象,损失函数,评价指标等。然后对网络模型进行训练,在这过程中,要将数据送入神经网络进行训练,同时建立好梯度记录环境,最终打印出图片分类精度的测试结果。

optimizer = optimizers.SGD(lr=0.01)   # 声明采用批量随机梯度下降方法,学习率=0.01
acc_meter = metrics.Accuracy()  # 新建accuracy测量器
for step, (x, y) in enumerate(train_dataset):  # 一次输入batch组数据进行训练
    with tf.GradientTape() as tape:  # 构建梯度记录环境
    x = tf.reshape(x, (32, 28, 28, 1))  # 将输入拉直,[b,28,28]->[b,784]
        out = network(x)  # 输出[b, 10]
        y_onehot = tf.one_hot(y, depth=10)  # 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 % 200 == 0:  # 每200个step,打印一次结果
        print('Step', step, ': Loss is: ', float(loss), ' Accuracy: ', acc_meter.result().numpy())  # 读取数据
        acc_meter.reset_states()  # 清零测量器

4. 图像分类效果
下图展示了训练过程中的一个测试结果。可以看到,此时训练集上的准确度达到了0.9780,已经很接近1了,继续训练应该可以得到更高的精确度。
在这里插入图片描述
注:本小节只对核心代码进行了必要的讲解和呈现,如需完整代码文件,请到我的Github地址下载:【AI 菌】的Github

1.3 卷积神经网络AlexNet

在这一小节,我们将学习非常经典的AlexNet卷积神经网络。首先在理论部分,本小节会依据对AlexNet进行一个必要的讲解。然后在实战部分,我会对自定义数据集进行加载、搭建AlexNet网络、迭代训练,最终完成图片分类和识别任务。

1.3.1 AlexNet详解

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

2. AlexNet网络结构
从整体结构上来看,AlexNet包含8层;前5层是卷积层,剩下的3层是全连接层。全连接层的输出是1000维的,最后通过softmax的得到各个类别的概率,实现了1000分类。整体结构如下图:
在这里插入图片描述在2012年,当时计算机硬件性能还比较有限,所以AlexNet采用了两块GTX580 3GB GPU进行分布式训练。但是现在的计算机水平完全可以考虑在单台电脑上跑。后面的实战过程中,我就将它简化为了单cpu/gpu版。具体每层网络结构如下:
在这里插入图片描述注:如果对卷积网络基本结构、输出图像大小的推导过程等不太了解,请参考前面的章节。

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

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

1.3.2 搭建AlexNet进行图片分类实战

1. 宝可梦数据集介绍
本次实验采用的是自定义的宝可梦数据集,该数据集共收集了皮卡丘(Pikachu)、超梦(Mewtwo)、杰尼龟(Squirtle)、小火龙(Charmander)和妙蛙种子(Bulbasaur)共 5 种精灵生物(看过神奇宝贝的盆友肯定知道~),一共是1122张图片。每种精灵的信息如下表:
在这里插入图片描述
2. 数据集加载
自定义数据集加载过程一共分为三步:
(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)创建数字编码表,并划分数据集。

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文件中已经写好的图片路径,和对应的标签

(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遍

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

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

4. 模型装配与训练
依据原论文中的方法,采用的是随机梯度下降算法进行迭代训练。由于我们使用的是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()

5. 结果可视化
每20轮采集一次分类精确度数据,使用matplotlib的绘画工具将精确度结果显示出来。

plt.plot(x_step, y_accuracy, label="training")
plt.xlabel("step")
plt.ylabel("accuracy")
plt.title("accuracy of training")
plt.legend()
plt.show()

6. 图像分类效果
下图是训练20个epos的过程中,测试精确度的变化。整个训练过程大概不到10分钟,测试精确度就达到了97%。当然,想得到更高的分类精度,可以多训练几个epos。
在这里插入图片描述
注:本小节只对核心代码进行了必要的讲解和呈现,如需完整代码文件,请到我的Github地址下载:【AI 菌】的Github

1.4 卷积神经网络GoogLeNet

在这一小节,我们将学习非常经典的GooLeNet卷积神经网络。首先在理论部分,本小节会对GooLeNet进行一个简要的讲解。然后在实战部分,我会对自定义数据集进行加载、搭建GooLeNet网络、迭代训练,最终完成图片分类和识别任务。

1.4.1 GoogLeNet 详解

1. GoogLeNet 简介
  GoogLeNet卷积神经网络出自于《Going deeper with convolutions》这篇论文,是由谷歌公司Christian Szegedy、Yangqing Jia等人联合发表。其研究成果在2014年 ILSVRC 挑战赛 ImageNet 分类任务上获得冠军,而当时的亚军就是上一篇文中讲到的VGG系列。
  很有意思的是,GoogLeNet名字是由Google的前缀 Goog 与 LeNet 的组合而来,这其实是对Yann LeCuns开拓性的LeNet-5网络的致敬。
  GoogLeNet卷积神经网络的最大贡献在于,提出了非常经典的Inception模块。该网络结构的最大特点是网络内部计算资源的利用率很高。因此该设计允许在保持计算资源预算不变的情况下增加网络的深度和宽度,使得GoogLeNet网络层数达到了更深的22层,但是网络参数仅为AlexNet的1/12。

2. GoogLeNet 的创新点
(1) 引入了非常经典的Inception模块。
(2)采用了模块化设计的思想。通过大量堆叠Inception模块,形成了更深更复杂的网络结构。
(3)采用了大量的1×1卷积核。主要是用作降维以消除计算瓶颈。
(4)在网络中间层设计了两个辅助分类器。
(5)采用平均池化层替代了原来的全连接层,使得模型参数大大减少。

3. GoogLeNet 网络结构
GoogLeNet 网络结构较为复杂,我们将学习下面这四个主要的部分:

  • Inception模块
  • 1x1卷积核降维
  • 辅助分类器
  • GoogLeNet整体结构

(1)Inception模块
在 GoogLeNet 的原始论文中,重点研究了名为Inception的高效的用于计算机视觉的深度神经网络结构,该结构的名称源自Lin等提出的论文《Network in Network》。在这种方法中,“深层”一词有两种不同的含义:首先,在某种意义上,我们以“Inception模块”的形式引入了新的组织层次,在更直接的意义上是网络深度的增加。Inception模块结构如下:
在这里插入图片描述图(a)是原始的Inception模块结构。图(b)是改进后的Inception模块结构。

图(b)在(a)的基础上,在3×3和5×5的卷积核之前,添加1×1的卷积核来缩减计算量,实质上是一个降维的过程
实验中最后采用的是图(b)所示的加入降维模块的Inception模块,因此这里以(b)为例进行讲解:
Inception模块的输入,通过4 个分支网络得到 4 个网络输出,在通道轴上面进行拼接,形成 Inception 模块的输出。这四个分支网络分别是:

  • 1x1 卷积层,步长为1,padding=‘same’
  • 1x1 卷积层,再通过一个 3x3 卷积层,步长为1,padding=‘same’
  • 1x1 卷积层,再通过一个 5x5 卷积层,步长为1,padding=‘same’
  • 3x3 最大池化城,再通过 1x1 卷积层,步长为1,padding=‘same’

Inception 模块的优点

  • 在每个3×3和5×5的卷积核之前,添加了1×1的卷积核进行降维,大大减少了参数量。
  • 融合不同尺度的特征信息。
  • 并行结构,结构稀疏,局部最优。

(2)1x1卷积核降维
前面Inception模块中提到,使用1x1卷积核来进行降维。所谓降维,就是通过降低卷积核的数量,从而大大减少模型参数。那下面我们就来演示一下,1x1卷积核具体是如何进行降维的。
如下图所示,对于同样的通道数为512的输入特征图,分别使用1x1卷积核和不使用1x1卷积核进行了一组对比实验
在这里插入图片描述

  • 不使用1x1卷积核降维。由卷积规则(卷积核通道数=输入特征图通道数)可知,5x5卷积核的通道数(维度)为512。因此64个5x5卷积核的参数一共是:5x5x512x64=819200
  • 使用1x1卷积核进行降维。若采用24个1x1卷积核进行降维,由卷积规则可知,中间层特征图的维度为24。因此,当再用5x5卷积核对中间层特征图进行卷积时,维度就降为了24,也就是说这时候64个5x5卷积核的通道数降为了24。因此总的参数量是:1x1x512x64+5x5x24x64=50688

对比可知:没有进行降维的模型参数有819200个,经过1x1卷积核降维之后的模型参数仅有50688个,参数量减少了16倍多

(3)辅助分类器
考虑到网络的深度较大,以有效方式将梯度传播回所有层的能力有限,可能会产生梯度弥散现象。因此在网络中间层设计了两个辅助分类器,希望以此激励网络在较低层进行分类,从而增加了被传播回的梯度信号,避免出现梯度弥散。
在训练过程中,它们的损失将以折扣权重添加到网络的总损失中(辅助分类器的损失加权为0.3)。 在测试过程中,这些辅助网络将被丢弃。其网络结构如下:
在这里插入图片描述
设计了完全相同的两个辅助分类器,这些分类器采用较小的卷积网络的形式,位于Inception(4a)和(4d)模块的输出之上。
如上图所示,具体结构与参数如下:

  • 第一层是一个平均池化下采样层,池化核大小为5x5,步长为3
  • 第二层是卷积层,卷积核大小为1x1,步长为1,卷积核个数是128
  • 第三层是全连接层,节点个数是1024
  • 第四层是全连接层,节点个数是1000,进行1000分类。
  • 最后经过Softmax激活函数,将模型输出转化为预测的类别概率输出

(4)GoogLeNet整体结构
下表是原论文中给出的参数列表,描述了GoogLeNet每个卷积层的卷积核个数、卷积核大小等信息。在这里插入图片描述

  • 在该网络中,输入大小为224×224的RGB颜色通道图片。
  • 所有卷积层,包括Inception模块内部的那些卷积,均使用ReLU激活函数。
  • 对于我们搭建的Inception模块,需要关注的是#1x1, #3x3reduce, #3x3, #5x5reduce, #5x5, poolproj这六列,分别对应着Inception模块内置卷积层所使用的卷积核个数。具体对应关系可参见下图:
    在这里插入图片描述
    4. GoogLeNet 的性能
    在2014年 ILSVRC 挑战赛 ImageNet 分类任务上获得冠军,测试结果如下表:
    在这里插入图片描述
    GoogLeNet最终在验证和测试数据集上均获得6.67%Top-5错误率,排名第一。 与2012年的SuperVision方法相比,相对减少了56.5%,与上一年的最佳方法(Clarifai)相比,减少了约40%
    Top-5错误率,是将真实类别与排名前5个的预测类进行比较:如果真实类别位于前五名预测类之中,则无论其排名如何,图像被视为正确分类 。 该挑战赛使用Top-5错误率进行排名。
    注:GoogLeNet的检测效果也很不错,但本小节只对分类效果进行评价。

1.4.2 TensorFlow2.0搭建GoogLeNet实战

本小节,将学习使用TensorFlow2.0搭建GoogLeNet卷积神经网络,对花分类数据集进行训练,从而实现图片的分类和识别任务。

1. 数据集介绍
这次我们采用的是花分类数据集,该数据集一共有5个类别,分别是:daisy、dandelion、roses、sunflowers、tulips,一共有3670张图片。按9:1划分数据集,其中训练集train中有3306张、验证集val中有364张图片。数据集的类别如下图所示:
在这里插入图片描述
数据集下载地址:花分类数据集, 提取码:9ao5
下载完成后,将文件解压后直接放在工程根目录下,就像下图这样:
在这里插入图片描述
2. 加载数据集
这里我们采用的方式是,使用keras底层模块图像生成器对数据集进行加载和预处理。需要说明一点的是:原来的类别标签是daisy、dandelion、roses、sunflowers、tulips,不能直接喂入神经网络,要将其转化为数字标签。因此在这里,将创建好的数字标签字典写入了class_indices.json文件。核心代码如下:

# 定义训练集图像生成器,并对图像进行预处理
train_image_generator = ImageDataGenerator(preprocessing_function=pre_function,
                                           horizontal_flip=True)  # 水平翻转
# 使用图像生成器从文件夹train_dir中读取样本,默认对标签进行了one-hot编码
train_data_gen = train_image_generator.flow_from_directory(directory=train_dir,
                                                           batch_size=batch_size,
                                                           shuffle=True,
                                                           target_size=(im_height, im_width),
                                                           class_mode='categorical')  # 分类方式
total_train = train_data_gen.n  # 训练集样本数
class_indices = train_data_gen.class_indices  # 数字编码标签字典:{类别名称:索引}
inverse_dict = dict((val, key) for key, val in class_indices.items())  # 转换字典中键与值的位置
json_str = json.dumps(inverse_dict, indent=4)  # 将转换后的字典写入文件class_indices.json
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)

3. 搭建网络结构
由于GooLeNet网络结构较为复杂,这里我们按照前文中讲到的各个主要结构:Inception模块、辅助分类器、完整结构,将它们用函数或类的形式进行封装。主要代码如下:
(1) Inception模块

class Inception(layers.Layer):
   # ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj分别对应Inception中各个卷积核的个数,**kwargs可变长度字典变量,存层名称
   def __init__(self, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj, **kwargs):  
       super(Inception, self).__init__(**kwargs)
       # 分支1
       self.branch1 = layers.Conv2D(ch1x1, kernel_size=1, activation="relu")
       # 分支2
       self.branch2 = Sequential([  
           layers.Conv2D(ch3x3red, kernel_size=1, activation="relu"),
           layers.Conv2D(ch3x3, kernel_size=3, padding="SAME", activation="relu")])
       # 分支3
       self.branch3 = Sequential([
           layers.Conv2D(ch5x5red, kernel_size=1, activation="relu"),
           layers.Conv2D(ch5x5, kernel_size=5, padding="SAME", activation="relu")])      
       # 分支4
       self.branch4 = Sequential([
           layers.MaxPool2D(pool_size=3, strides=1, padding="SAME"),  
           layers.Conv2D(pool_proj, kernel_size=1, activation="relu")])                 
       
   def call(self, inputs, **kwargs):
       branch1 = self.branch1(inputs)
       branch2 = self.branch2(inputs)
       branch3 = self.branch3(inputs)
       branch4 = self.branch4(inputs)
       # 将4个分支输出按通道连接
       outputs = layers.concatenate([branch1, branch2, branch3, branch4])  
       return outputs

(2)辅助分类器InceptionAux

class InceptionAux(layers.Layer):
    # num_classes表示输出分类节点数,**kwargs存放每层名称
    def __init__(self, num_classes, **kwargs):
        super(InceptionAux, self).__init__(**kwargs)
        self.averagePool = layers.AvgPool2D(pool_size=5, strides=3)  # 平均池化
        self.conv = layers.Conv2D(128, kernel_size=1, activation="relu")

        self.fc1 = layers.Dense(1024, activation="relu")  # 全连接层1
        self.fc2 = layers.Dense(num_classes)  # 全连接层2
        self.softmax = layers.Softmax()  # softmax激活函数

    def call(self, inputs, **kwargs):
        x = self.averagePool(inputs)
        x = self.conv(x)
        x = layers.Flatten()(x)  # 拉直
        x = layers.Dropout(rate=0.5)(x)
        x = self.fc1(x)
        x = layers.Dropout(rate=0.5)(x)
        x = self.fc2(x)
        x = self.softmax(x)
        return x

(3) GooLeNet整体结构

def GoogLeNet(im_height=224, im_width=224, class_num=1000, aux_logits=False):
    # 输入224*224的3通道彩色图片
    input_image = layers.Input(shape=(im_height, im_width, 3), dtype="float32")
    x = layers.Conv2D(64, kernel_size=7, strides=2, padding="SAME", activation="relu", name="conv2d_1")(input_image)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_1")(x)
    x = layers.Conv2D(64, kernel_size=1, activation="relu", name="conv2d_2")(x)
    x = layers.Conv2D(192, kernel_size=3, padding="SAME", activation="relu", name="conv2d_3")(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_2")(x)
    # Inception模块
    x = Inception(64, 96, 128, 16, 32, 32, name="inception_3a")(x)
    x = Inception(128, 128, 192, 32, 96, 64, name="inception_3b")(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_3")(x)
    # Inception模块
    x = Inception(192, 96, 208, 16, 48, 64, name="inception_4a")(x)
    # 判断是否使用辅助分类器1。训练时使用,测试时去掉。
    if aux_logits:
        aux1 = InceptionAux(class_num, name="aux_1")(x)
    # Inception模块
    x = Inception(160, 112, 224, 24, 64, 64, name="inception_4b")(x)
    x = Inception(128, 128, 256, 24, 64, 64, name="inception_4c")(x)
    x = Inception(112, 144, 288, 32, 64, 64, name="inception_4d")(x)
    # 判断是否使用辅助分类器2。训练时使用,测试时去掉。
    if aux_logits:
        aux2 = InceptionAux(class_num, name="aux_2")(x)
    # Inception模块
    x = Inception(256, 160, 320, 32, 128, 128, name="inception_4e")(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_4")(x)
    # Inception模块
    x = Inception(256, 160, 320, 32, 128, 128, name="inception_5a")(x)
    x = Inception(384, 192, 384, 48, 128, 128, name="inception_5b")(x)
    # 平均池化层
    x = layers.AvgPool2D(pool_size=7, strides=1, name="avgpool_1")(x)
    # 拉直
    x = layers.Flatten(name="output_flatten")(x)
    x = layers.Dropout(rate=0.4, name="output_dropout")(x)
    x = layers.Dense(class_num, name="output_dense")(x)
    aux3 = layers.Softmax(name="aux_3")(x)
    # 判断是否使用辅助分类器
    if aux_logits: 
        model = models.Model(inputs=input_image, outputs=[aux1, aux2, aux3])
    else:
        model = models.Model(inputs=input_image, outputs=aux3)
    return model

4. 模型的装配与训练
搭建好网络结构后,首先要对网路模型装配,指定网络使用的优化器对象,损失函数,评价指标等。然后对网络模型进行训练,在这过程中,要将数据送入神经网络进行训练,同时建立好梯度记录环境,最终打印出图片分类精度的测试结果。部分代码如下:

# 使用keras底层api进行网络训练。 
loss_object = tf.keras.losses.CategoricalCrossentropy(from_logits=False)  # 定义损失函数(这种方式需要one-hot编码)
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0003)  # 优化器

train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.CategoricalAccuracy(name='train_accuracy')  # 定义平均准确率

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.CategoricalAccuracy(name='test_accuracy')

5. 训练集/验证集上测试结果
如下图所示,打印出了每训练完一个epoch后的loss值、训练集分类准确度、测试集分类准确度。可见,网络的损失值loss在不断减小,数据集上的准确度都在稳步上升,因此模型此时是收敛的,继续训练可以得到更好的分类准确度。
在这里插入图片描述
6. 加载模型,识别单张图片
在工程根目录下,放入一张类别为daisy的图片,将其命名为daisy_test.jpg。我们读入这张图片,加载刚才已经训练好的模型,对图片进行预测。预测部分的代码如下:

# 读入图片
img = Image.open("E:/DeepLearning/GoogLeNet/daisy_test.jpg")
# resize成224x224的格式
img = img.resize((im_width, im_height))
plt.imshow(img)
# 对原图标准化处理
img = ((np.array(img) / 255.) - 0.5) / 0.5
# Add the image to a batch where it's the only member.
img = (np.expand_dims(img, 0))
# 读class_indict文件
try:
    json_file = open('./class_indices.json', 'r')
    class_indict = json.load(json_file)
except Exception as e:
    print(e)
    exit(-1)
model = GoogLeNet(class_num=5, aux_logits=False)  # 重新构建网络
model.summary()
model.load_weights("./save_weights/myGoogLenet.h5", by_name=True)  # 加载模型参数
result = model.predict(img)
predict_class = np.argmax(result)
print('预测出的类别是:', class_indict[str(predict_class)])  # 打印显示出预测类别
plt.show()

如下图所示,我们输入一张图片daisy_test.jpg,它属于daisy类。

在这里插入图片描述
神经网络识别的结果如下:

在这里插入图片描述
可见,识别结果与原图daisy_test.jpg的标签一致,识别成功!

注:本小节只对核心代码进行了必要的讲解和呈现,如需完整代码文件,请到我的Github地址下载,欢迎star收藏:【AI 菌】的Github

更多更详细的内容可前往我的专栏查看。

本文内容较长,我将持续更新,欢迎大家点赞收藏,您的支持是我创作的最大动力!

最后更新日期:2020/10/24 总字数:25298

AI 菌 CSDN认证博客专家 博客专家 CSDN合作伙伴 算法实习僧
研究僧一枚,CSDN博客专家,公众号【AI 修炼之路】作者。专注于无人驾驶(环境感知方向),热衷于分享AI、CV、DL、ML、OpenCV、Python、C++等相关技术文章。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:白松林 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值