第3例 基于卷积神经网络AlexNet的猫狗分类

最近在积攒粉丝500,大家帮帮忙,动动小手指关注、点赞、收藏…🙏🙏🙏🙏🙏🙏


第一个典型的CNN是LeNet5网络结构,但是第一个引起大家注意的网络却是AlexNet,也就是文章《ImageNet Classification with Deep Convolutional Neural Networks》介绍的网络结构,它是2012年ImageNet挑战赛的冠军。

一、AlexNet网络结构

网络总共的层数为8层,5层卷积,3层全连接层,输出1000个类别;使用2个GPU训练,结构如下:
在这里插入图片描述

1.1 卷积网络提取特征

卷积神经网络的第一部分就是使用卷积层去提取特征,网络结构由卷积层、池化层堆叠而成。

1.1.1 卷积层C1

在这里插入图片描述
该层的处理流程是:卷积–>ReLU–>归一化。
(1)Conv:卷积层输入3x224x224的图像,卷积核3x96x11x11,步长stride=4,填充pad=0; 输出特征图96x55x55;
(2)Relu:经过激活函数ReLu;
(3)BN:局部归一化LRN (Local Response Normalized),LRN后来被证实用处不大;这里复现时使用BN层代替

1.1.2 卷积层C2在这里插入图片描述

该层的处理流程是:卷积–>ReLU–>归一化–>池化。
(1)Conv:卷积层输入96x55x55,卷积核96x256x5x5,步长stride=1,填充pad=2; 输出256x55x55;
(2)Relu:经过激活函数ReLu;
(3)BN:批量归一化BN(Batch Normalization);
(4)Pooling:池化pool_size=(3, 3), stride=2, pad=0,输出256x27x27;

1.1.3 卷积层C3在这里插入图片描述

该层的处理流程是:卷积–>ReLU–>归一化–>池化。
(1)Conv:卷积层输入256x27x27,卷积核256x384x3x3,步长stride=1,填充pad=2; 输出192x27x27;
(2)Relu:经过激活函数ReLu;
(3)BN:批量归一化BN(Batch Normalization);
(4)Pooling:池化pool_size=(3, 3), stride=2, pad=0,输出256x13x13;

1.1.4 卷积层C4,C5在这里插入图片描述

该层的处理流程是:卷积–>ReLU–>卷积–>ReLU。
(1)Conv:卷积层输入384x13x13,卷积核384x384x3x3,步长stride=1,填充pad=1; 输出384x13x13;
(2)Relu:经过激活函数ReLu;
(3)Conv:卷积层输入384x13x13,卷积核384x256x3x3,步长stride=1,填充pad=1; 输出256x13x13;
(4)Relu:经过激活函数ReLu;

1.2 全连接层对特征分类

以上是C1-C5是输入图像,通过卷积神经网络提取特征,得到大小为13x13的256特征图;

根据卷积神经网络图像分类思想(卷积神经网络 = 卷积提取特征 + 全连接分类),接下来把256个特征图送入全连接分类器,得到最终的分类结果。

1.2.1 全连接层FC6在这里插入图片描述

该层的处理流程是:池化–>卷积–>ReLU–>归一化。
PS:这里比较特殊,使用卷积层替换了全连接层,节省了计算量。
(1)Pooling:池化pool_size=(3, 3), stride=2, pad=0,输出256x6x6;
(2)Conv:卷积层输入256x6x6,卷积核256x2048x3x3,步长stride=1,填充pad=1; 输出2048x1x1;
(3)Relu:经过激活函数ReLu;
(4)BN:批量归一化BN(Batch Normalization);
(5)展平:把(2048, 1, 1)展平成(2048,)

1.2.3 全连接层FC7,FC8在这里插入图片描述

该层的处理流程是:Dropout-> FC7->Dropout->FC8->softmax。
(1)Dropout:抑制过拟合,随机的断开某些神经元的连接或者是不激活某些神经元;
(2)全连接:输入2048,激活函数sigmoid,输出2048.
(3)全连接:输入2048,激活函数sigmoid,输出1000(类别数,ImageNet挑战赛的输出书1000类);
(4)softmax:最后经过softmax把输出转成概率。

1.3 网络结构代码实现

使用的是tensorflow2.x的类继承编码格式,AlexNet网络结构的代码如下:

import tensorflow as tf
from tensorflow.keras import layers

class ConvBA(layers.Layer):
    """ Conv->relu->bn """
    def __init__(self, filters, kernel_size, strides=1, padding='valid'):
        super(ConvBA, self).__init__()
        self.conv1 = layers.Conv2D(filters, kernel_size, strides=strides, padding=padding, activation="relu")
        self.bn1 = layers.BatchNormalization()

    def call(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        return x

class AlexNet(tf.keras.Model):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        # 1. 卷积提取特征
        # self.conv1 = ConvBA(48, (11, 11), strides=4, padding="same") # 原本是步长4
        self.conv1 = ConvBA(48, (11, 11), strides=2, padding="same")    # 我为了适应输入112,改为2
        self.conv2 = ConvBA(128, (5, 5), strides=1, padding="same")
        self.pool1 = layers.MaxPool2D(pool_size=(3, 3), strides=2, padding='valid')
        self.conv3 = ConvBA(192, (3, 3), strides=1, padding='same')
        self.pool2 = layers.MaxPool2D(pool_size=(3, 3), strides=2, padding='valid')
        self.conv4 = ConvBA(192, (3, 3), strides=1, padding='same')
        self.conv5 = ConvBA(128, (3, 3), strides=1, padding='same')
        self.pool3 = layers.MaxPool2D(pool_size=(3, 3), strides=2, padding='valid')
        # 2. 分类(dense->Dropout->dense->Dropout->dense->softmax)
        self.flatten = layers.Flatten()
        self.fc0 = layers.Dense(2048, activation="sigmoid", use_bias=False)
        self.drop1 = layers.Dropout(0.5)
        self.fc1 = layers.Dense(1024, activation="sigmoid", use_bias=False)
        self.drop2 = layers.Dropout(0.5)
        self.fc2 = layers.Dense(num_classes, activation="sigmoid", use_bias=False)
        self.softmax = layers.Softmax()

    def call(self, x, training=False):
        """"  输入X -> 原来(batch,224,224,3)
            实验我输入图像112*112
        """
        # 提取特征,这里只是模拟AlexNet原来的一半
        x = self.conv1(x)       # 96*55*55
        x = self.conv2(x)
        x = self.pool1(x)       # 256*27*27
        x = self.conv3(x)
        x = self.pool2(x)       # 384*13*13
        x = self.conv4(x)       # 384*13*13
        x = self.conv5(x)       # 256*13*13
        x = self.pool3(x)       # 256*6*6
        # 分类
        x = self.flatten(x)     # 展平
        x = self.fc0(x)         # 2048
        if training:
            x = self.drop1(x)
        x = self.fc1(x)         # 1024(这一层原文是2048,我改为了1024)
        if training:
            x = self.drop1(x)
        x = self.fc2(x)         # 输出类别数个数据
        x = self.softmax(x)     # 转为概率
        return x

二、AlexNet的创新点

这个网络也是基于LeNet基础上进行改进优化的,首次运用在大规模图像分类上,取得惊人的效果。其在LeNet基础上改进有如下几点。

2.1 激活函数ReLu

AlexNet在卷积层上,改进的激活函数,由sigmoid/tanh改为ReLU
2011年Yoshua Bengio等人提出ReLU激活函数,ReLU函数是目前比较火的一个激活函数。
函数公式:f(x) = max(0, x), 函数图像如下:
image-20220405155815907
ReLU有效性体现:
(1)Relu函数缓解解了梯度消失问题;
(2)相比sigmoid、tanh函数计算量更少;
(3)训练时网络收敛更快;

2.2 数据归一化

在神经网络中,我们用激活函数将神经元的输出做一个非线性映射,但是tanh和sigmoid这些传统的激活函数的值域都是有范围的,但是ReLU激活函数得到的值域没有一个区间,所以要对ReLU得到的结果进行归一化。

再AlexNet中首次使用了归一化层(Local Response Normalization,局部归一化),但是局部归一化后来被证实用处不大,取而代之的时BN层(Batch normalization批量归一化)。2015年提出的Batch normalization,是一个用于优化训练神经网络的技巧。
在这里插入图片描述
BN层的优点很多,如下:

  1. 训练的更快;
  2. 容忍更高的学习率(learning rate);
  3. 让权重更容易初始化;
  4. 更好的支持创建深层的神经网络;
  5. 防止梯度消失或梯度爆炸等问题;

2.3 全连接层前使用Dropout

AlexNet网络在全连接层之前使用了Dropout(一种有效解决过拟合的方法),来解决训练是出现过拟合Overfitting问题。
Overfitting 也被称为过度学习,过度拟合,它是机器学习中常见的问题。
如下:数据有两个类别(蓝类、红类)。左图是训练,训练得到两个分割曲线;右图是预测;
在这里插入图片描述

现在预测3堆数据,分别是黄色(蓝类)、紫色(蓝类)、橙色(红类);图上看得出,预测黑色分割曲线准确率更高更好;得到绿色的分割曲线是因为训练过拟合了,过拟合表现在训练集上精度很高,但是在测试集(应用预测)上精度差。

Dropout的原理:过拟合意味着网络记住了训练样本,而打破网络固定的工作方式,就有可能打破这种不好的记忆;所以我们可以再网络训练时,在全连接层随机的断开某些神经元的连接或者不激活某些神经元,来达到抑制过拟合作用。在这里插入图片描述

三、Alexnet代码实现猫狗图片分类

3.1 数据集介绍

数据集目录如下:
在这里插入图片描述

训练集每个类别有3000张、验证集每类有600张;图片大致如下:
在这里插入图片描述
在这里插入图片描述

3.2 数据集读取

​ 定义基本函数:

# myreaddata.py
import random
import pathlib

""" 
将所有数据存放在同一目录下,
# 然后将不同类别的图片分别地存放在各自的类别子目录下
"""
def get_all_image_paths(image_dir):
    '''
    获取所有图片路径,例如 ['flower_photos\\sunflowers\\4895721242_89014e723c_n.jpg', ...] '''
    data_path = pathlib.Path(image_dir)
    paths = list(data_path.glob('*/*')) # 图片全路径
    paths = [str(p) for p in paths]
    # random.shuffle(paths)
    return paths

def get_label_and_index(image_dir):
    '''获取类别名称及其数字表示,例如
        ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']
        {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
    '''
    data_path = pathlib.Path(image_dir)
    label_names = sorted(item.name for item in data_path.glob('*/') if item.is_dir())
    label_index = dict((name,index) for index,name in enumerate(label_names))
    return label_names, label_index

tensorflow2.x的格式读取数据集:

# data_manager.py
import tensorflow as tf
from data_process.myreaddata import *

def process_image(fpath, label):
    """ 图片预处理 """
    image = tf.io.read_file(fpath)                  # 读取图像
    image = tf.image.decode_jpeg(image,channels=3)  # jpg图像解码
    image = tf.image.resize(image, [112, 112])      # 原始图片大重设为(x, x), AlexNet的输入是224X224
    label = tf.one_hot(label, depth=2)              # 标签转成onehot格式,这里实验是标签2个类别数据
    return image, label

def get_dataset(image_dir, is_shuffle=False, batch_size=64):
    # 获取所有图片路径
    image_paths = get_all_image_paths(image_dir)
    _, label_index = get_label_and_index(image_dir)
    # 每个图片路径名->数字标签
    image_labels = [label_index[pathlib.Path(path).parent.name] for path in image_paths]
    # tensorflow接口创建数据集读取
    ds = tf.data.Dataset.from_tensor_slices((image_paths, image_labels))
    ds = ds.map(process_image)
    if is_shuffle:
        ds = ds.shuffle(buffer_size=len(image_paths))
    ds = ds.batch(batch_size)
    return ds

3.2 训练模型代码

import os
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras import layers,losses, metrics
from data_process.data_manager import get_dataset
from network.alexnet import AlexNet

class TrainModel():
    def __init__(self, lr=0.01):
        self.model = AlexNet(num_classes=2)                # 定义网络,2分类
        self.model.build(input_shape=(None, 112, 112, 3))  # BHWC
        self.model.summary()
        self.loss_fun = losses.CategoricalCrossentropy()    # 定义损失函数, 这里交叉熵
        self.opt = tf.optimizers.SGD(learning_rate=lr)      # 随机梯度下降优化器
        self.train_acc_metric = metrics.CategoricalAccuracy()   # 设定统计参数
        self.val_acc_metric = metrics.CategoricalAccuracy()

    def train(self, fpath="./data/mycatdog2", epochs=300, m=5):
        """ 训练网络 """
        batch_size = 64
        test_acc_list = []
        # 读取数据集
        train_dataset = get_dataset(os.path.join(fpath, "train"), is_shuffle=True, batch_size=batch_size)
        val_dataset = get_dataset(os.path.join(fpath, "valid"), is_shuffle=False, batch_size=batch_size)
        # 训练
        loss_val = 0
        for epoch in range(epochs):
            print(" ** Start of epoch {} **".format(epoch))
            # 每次获取一个batch的数据来训练
            for nbatch, (inputs, labels) in enumerate(train_dataset):
                with tf.GradientTape() as tape:                 # 开启自动求导
                    y_pred = self.model(inputs, training=True)  # 前向计算  
                    loss_val = self.loss_fun(labels, y_pred)    # 误差计算
                    grads = tape.gradient(loss_val, self.model.trainable_variables)         # 梯度计算
                    self.opt.apply_gradients(zip(grads, self.model.trainable_variables))    # 权重更新
                    self.train_acc_metric(labels, y_pred)   # 更新统计传输
                    if nbatch % m == 0: # 打印
                        correct = tf.equal(tf.argmax(labels, 1), tf.argmax(y_pred, 1))
                        acc = tf.reduce_mean(tf.cast(correct, tf.float32))
                        print('{}-{} train_loss:{:.5f}, train_acc:{:.5f}'.format(epoch, nbatch, float(loss_val), acc))
            # 输出统计参数的值
            train_acc = self.train_acc_metric.result()
            self.train_acc_metric.reset_states()
            print('Training acc over epoch: {}, acc:{:.5f}'.format(epoch, float(train_acc)))
            # 每次迭代在验证集上测试一次
            for nbatch, (inputs, labels) in enumerate(val_dataset):
                y_pred = self.model(inputs)
                self.val_acc_metric(labels, y_pred)
            val_acc = self.val_acc_metric.result()
            self.val_acc_metric.reset_states()
            print('Valid acc over epoch: {}, acc:{:.5f}'.format(epoch, float(val_acc)))
            test_acc_list.append(val_acc)
        # 训练完成保存模型
        tf.saved_model.save(self.model, "./output/mnist_model")
        # 画泛化能力曲线(横坐标是epoch, 测试集上的精度),并保存
        x = np.arange(1, len(test_acc_list)+1, 1)
        y = np.array(test_acc_list)
        plt.plot(x, y)
        plt.xlabel("epoch")
        plt.ylabel("val_acc")
        plt.title('model acc in valid dataset')
        plt.savefig("./output/val_acc.png", format='png')

if __name__ == "__main__":
    path = "./output"
    if not os.path.exists(path):
        os.makedirs(path)
    model = TrainModel()
    model.train(fpath="F:\数据集\mycatdog2")

3.3 训练结果

训练了300个epoch, 在训练接上acc达到1.00。在验证集上acc=85%。
在这里插入图片描述

若是对大家有帮助,不要忘了关注、点赞、收藏哦…🙏🙏🙏🙏🙏🙏🙏

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI学长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值