小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类

简介:近些天重新学习梳理了一遍卷积神经网络部分的知识,因此写博客总结一下,方便自己以后翻阅,也希望能够帮助到一些同学。第一次写博客,如果内容有不当之处,希望大家能够指正、交流。

科普:近几年来,人工智能,机器学习,深度学习,神经网络这几个词常常出现在我们耳边,它们也不断发展,被应用在生活中的方方面面。举个例子,比如我们手机的人脸识别,刷脸解锁,这就属于人工智能方面的应用。除此之外,还有文字识别,短视频与广告推荐等等。

想知道人工智能、机器学习、深度学习、神经网络的区别及各自的含义可以自行百度,或者点一下这个链接。

本文主要介绍经典的图片分类网络:GoogleNet网络,及基于Tensorflow2.0的实现。

神经网络是模仿人脑中的神经元而搭建出来的一种网络,准确地说应该是一种复杂的函数,通常我们给这个网络输入一组数据,经过网络得到输出数据。简单地说,就是下面的这个数学公式:Y=F(X)。这里的X是输入数据,与数学公式相比,X不仅仅是一个数字了,也可以是一个图片、一句话等等。F呢,就是我们的网络函数,不过更加复杂,大型的网络有亿万个参数。Y就是网络的输出。

神经网络的示意图如下,其中的小圆圈就是神经元,神经元之间的每一条连线都相当于一个函数,函数中的参数是可以训练的。
小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类_第1张图片
我们通过给搭建出来的网络输入大量的数据X(图片),数据X中的每个样本都带有一个标签y(真实类别),通过网络输出Y(预测类别),这里的Y是网络对于X的预测值,通过一种评判标准与真实的y相比,来判断预测输出Y的好坏,再通过优化算法去修改网络中的参数,这个过程就是网络的训练过程,包括前向传播(预测)与反向传播(更新参数)。训练好网络之后,就可以利用它实现一些功能。

一、原理知识

卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络,是深度学习的代表算法之一。卷积神经网络的研究始于二十世纪80至90年代,卷积神经网络仿造生物的视知觉机制构建,其优点在网络的输入是图像时表现的更为明显,使得图像可以直接作为网络的输入,避免了传统识别算法中复杂的特征提取和数据重建的过程,如网络能够自行抽取图像的特征包括颜色、纹理、形状,深度神经网络的基本思想是通过构建多层网络,对目标进行多层表示,通过多层的高层次特征来表示数据的抽象语义信息,获得更好的特征鲁棒性。一句话,卷积神经网络就是提取图像的特征!

详细了解可以看一下这个链接:卷积神经网络原理

卷积运算

说到卷积网络就不得不提卷积运算,这里的卷积运算与数学中的卷积不同。对图片进行表示时,一副灰度图片可以看作一个一层的矩阵,其中每个元素取值范围为0到255,0为黑色,255为白色(彩色RBG图像是三通道的图片,为3层)。借用一张图对卷积运算进行解释:
小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类_第2张图片
以上图为例:

原始图像为一层5x5的矩阵,中间的一层3x3的矩阵就是我们的卷积核,最后得到的就是经过卷积之后提取到的特征图。(这个得到的特征图可以作为新的输入,再次进行卷积操作)

卷积核的每个元素都对应一个权重系数w和一个偏差量b。卷积核在工作时,在原始图像(特征)的左上角开始,向右滑动扫过原始图像,扫至最右侧,再向下移动一个步长,从左向右滑动…在感受野(卷积核所能覆盖的范围)内对输入特征做矩阵元素乘法求和并叠加偏差量,就得到特征图上对应的元素。卷积运算需要对卷积核进行设置。

首先是卷积核的边长,一般设置为正方形,但也可以设置为长方形,边长越大提取的是原图像中面积越大的特征,比如对于人脸来说,提取嘴巴特征的卷积核要比瞳孔特征的大。接着是卷积运算的步长(stride),也就是卷积核在图像上每一步移动的距离,stride为1,则每步移动一个元素的距离。此外,还有一个**填充属性(padding)**的设置,如果不对图像进行填充就进行卷积操作,卷积之后的特征图的大小会变小,如果想要经过卷积操作后还能保持和原来一样的大小,则需要设置padding为"same",不进行填充操作为"valid"。

激活函数

在神经网络里,激活函数的作用是增强网络的非线性表达能力,从而能够进行更复杂的特征表示,因为激活函数都是非线性函数,现在经常用的有Relu函数等。激活函数详解

损失函数

开头说利用神经网络对图片进行分类,需要通过一个判别标准来衡量分类结果的好坏,这里的判别标标准就是损失函数,分类问题中常用的损失函数是交叉熵函数。关于交叉熵函数,下面这个链接比较详细。交叉熵函数详解此外,还可以在损失函数中加入正则项:正则项与损失函数。

优化算法

通过前向传播,我们可以获得对一个输入图片的预测结果,接下来就可以通过优化算法对网络模型的参数进行更新,也就是反向传播。详细链接:优化算法详解

二、GoogleNet网络

GoogleNet是谷歌提出来的一个网络模型,其中提出来了一个叫做inception的网络模块。那为什么提出这样一个模块,它的作用是什么呢?

一个网络网络越宽、越深,其表示复杂特征的能力越强,但是参数会不可避免地增大,inception模块就是在保证网络规模更大的同时,使参数规模减小,提高计算效率

卷积的作用是为了提取特征,为了能提取出不同的特征,可以利用不同的卷积核去与原图像进行卷积运算,也就是增加网络的宽度。inception模块如下所示:
小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类_第3张图片
可以看到,inception模块中采用了多个卷积核提取特征,最后将特征图连接在一起,包括一个1x1卷积、一个3x3卷积、一个5x5卷积,还有一个3x3的最大池化层(池化层与卷积层类似,但不是卷积操作,而是取感受野下的最大值)。

网络变宽了,但是怎么减少参数量呢?在此基础上,inception模块又加入了1x1的卷积运算。1x1卷积的作用
小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类_第4张图片
在第二、三、四分支分别加入了1x1卷积进行降维,比如输入为深度为100(通道数为100)的特征,希望得到输出特征图的深度也为100。如果直接用3x3卷积核去卷积,需要3x3x100x100个参数量,用50个1x1卷积核去卷积,得到的特征图深度为50,然后再用3x3的卷积核去卷积,需要的参数量为1x1x100x50+3x3x50x100个,可以看到参数量明显减小。这就是inception模块的原理和作用。

GoogleNet的整体框图及参数设置可以从论文原文中获得:论文地址,这里贴出来,整体框图如下。

小白教程:卷积神经网络之GoogLeNet原理+Tensorflow2.0实现图片分类_第5张图片
首先要确定输入图片的大小,输入图片大小为224x224,如果不是这么大,请对输入图片进行预处理,reshape为224x224。

从输入到输出,前几层网络并没有用inception模块,只是卷积与池化层相连,LocalRespNorm是一种归一化方法,但现在用的并不多,可以用批量归一化(BN方法详解链接)来代替,后面一共连接了9个inception模块。最终进行平均池化,并将其拉平,传入最终的全连接层(最终的全连接层个数为图片的类别个数),为了防止过拟合,中间还要加入dropout层(dropout详解链接)。最终经过softmax函数,输出该图像的类别预测结果。

可以看到网络的中间部分引出来了两个softmax分类器作为辅助分类器,它们的作用是为了让网络加速收敛(缓解梯度消失,在反向传播的过程中也能训练到前面几层的参数)。在训练过程,辅助分类器的loss乘以0.3加到主干网络的loss上,作为总的loss。训练完之后,利用模型去推理,辅助分类器就不会产生作用。(但听有的博主说好像效果不是很大,也可以不加辅助分类器)

网络结构的介绍到此结束,下面上手操作。

三、GoogleNet实现图片分类

环境不再多说,为tensorflow2.0,在pycharm上编程实现。

本文代码参考的github链接,侵权必删

首先新建文件夹,在文件夹中放入训练数据集和测试数据集。我是在桌面上的AI文件夹里新建了文件夹GoogleNet,然后将训练集train与测试集val放入其中。

按照GoogleNet的结构和参数搭建网络模型,在GoogleNet下新建文件model.py:

from tensorflow.keras import layers, models, Model, Sequential

def GoogLeNet(im_height=224, im_width=224, class_num=2, 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

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)
        outputs = layers.concatenate([branch1, branch2, branch3, branch4])  # 将4个分支输出按通道连接
        return outputs

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

然后在GoogleNet下新建文件train.py:

from tensorflow.keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
from GoogleNet.model import GoogLeNet
import tensorflow as tf
import json
import os

data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # 根路径
# 此处要根据自己文件存放路径来改
image_path = data_root + "/AI/GoogleNet/"  # 数据集路径
train_dir = image_path + "train"  # 训练集路径
validation_dir = image_path + "val"  # 验证集路径

# 创建文件save_weights用来存放训练好的模型
if not os.path.exists("save_weights"):
    os.makedirs("save_weights")

im_height = 224
im_width = 224
batch_size = 32
epochs = 30


def pre_function(img):
    # img = im.open('test.jpg')
    # img = np.array(img).astype(np.float32)
    img = img / 255.  # 归一化
    img = (img - 0.5) * 2.0  # 标准化
    return img


# 定义训练集图像生成器,并对图像进行预处理
train_image_generator = ImageDataGenerator(preprocessing_function=pre_function,
                                           horizontal_flip=True)  # 水平翻转
# 定义验证集图像生成器,并对图像进行预处理
validation_image_generator = ImageDataGenerator(preprocessing_function=pre_function)
# 使用图像生成器从文件夹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)
# 使用图像生成器从验证集validation_dir中读取样本
val_data_gen = train_image_generator.flow_from_directory(directory=validation_dir,
                                                         batch_size=batch_size,
                                                         shuffle=True,
                                                         target_size=(im_height, im_width),
                                                         class_mode='categorical')
total_val = val_data_gen.n  # 验证集样本数
model = GoogLeNet(im_height=im_height, im_width=im_width, class_num=2, aux_logits=True)  # 实例化模型
# model.build((batch_size, 224, 224, 3))  # when using subclass model
model.summary()  # 每层参数信息

# 使用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')


@tf.function
def train_step(images, labels):
    with tf.GradientTape() as tape:
        aux1, aux2, output = model(images, training=True)
        loss1 = loss_object(labels, aux1)  # 辅助分类器损失函数
        loss2 = loss_object(labels, aux2)
        loss3 = loss_object(labels, output)  # 主分类器损失函数
        loss = loss1 * 0.3 + loss2 * 0.3 + loss3  # 总损失函数
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    train_loss(loss)
    train_accuracy(labels, output)


@tf.function
def test_step(images, labels):
    _, _, output = model(images, training=False)
    t_loss = loss_object(labels, output)
    test_loss(t_loss)
    test_accuracy(labels, output)


best_test_loss = float('inf')
for epoch in range(1, epochs + 1):
    train_loss.reset_states()  # 训练损失值清零
    train_accuracy.reset_states()  # clear history info
    test_loss.reset_states()  # clear history info
    test_accuracy.reset_states()  # clear history info

    for step in range(total_train // batch_size):
        images, labels = next(train_data_gen)
        train_step(images, labels)

    for step in range(total_val // batch_size):
        test_images, test_labels = next(val_data_gen)
        test_step(test_images, test_labels)

    template = 'Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'
    print(template.format(epoch,
                          train_loss.result(),
                          train_accuracy.result() * 100,
                          test_loss.result(),
                          test_accuracy.result() * 100))
    if test_loss.result() < best_test_loss:
        best_test_loss = test_loss.result()
        model.save_weights("./save_weights/myGoogLeNet.h5")  # 保存模型为.h5格式

进行训练,可以看到我们之前定义的模型的信息,之后是迭代训练的信息。

本文先总结到这里,后面再进行更新。

侵权必删

你可能感兴趣的:(学习深度学习,笔记,神经网络,卷积神经网络,tensorflow,人工智能)