全部文章

13.图像分类(算法进阶、图像增强、模型微调)

图像分类简介

学习目标

  • 知道图像分类的目的
  • 知道imageNet数据集

1 图像分类

图像分类实质上就是从给定的类别集合中为图像分配对应标签的任务。也就是说我们的任务是分析一个输入图像并返回一个该图像类别的标签。

假定类别集为categories = {dog, cat, panda},之后我们提供一张图片给分类模型,如下图所示:

分类模型给图像分配多个标签,每个标签的概率值不同,如dog:95%,cat:4%,panda:1%,根据概率值的大小将该图片分类为dog,那就完成了图像分类的任务。

2 常用数据集

2.1 mnist数据集

该数据集是手写数字0-9的集合,共有60k训练图像、10k测试图像、10个类别、图像大小28×28×1.我们可以通过tf.keras直接加载该数据集:

from tensorflow.keras.datasets import mnist
# 加载mnist数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

随机选择图像展示结果如下所示:

for i in range(9):
    plt.subplot(3,3,i+1)
    plt.imshow(x_train[i])
    plt.title(y_train[i])
    plt.axis('off')
plt.show()

2.2 CIFAR-10和CIFAR-100

  • CIFAR-10数据集5万张训练图像、1万张测试图像、10个类别、每个类别有6k个图像,图像大小32×32×3。下图列举了10个类,每一类随机展示了10张图片:

  • CIFAR-100数据集也是有5万张训练图像、1万张测试图像、包含100个类别、图像大小32×32×3。

在tf.keras中加载数据集时:

import tensorflow as tf
from tensorflow.keras.datasets import cifar10,cifar100
# 加载Cifar10数据集
(train_images, train_labels), (test_images, test_labels) = cifar10.load_data()
# 加载Cifar100数据集
(train_images, train_labels), (test_images, test_labels)= cifar100.load_data()

随机查看部分图片:

import numpy as np
imglist=np.random.randint(0,train_images.shape[0],30)
plt.figure(figsize=(20,8))
for i in range(30):
    plt.subplot(3,10,i+1)
    plt.imshow(train_images[i])
    plt.title(train_labels[i])
    plt.axis('off')
plt.tight_layout()
plt.show()

 

2.3 ImageNet

ImageNet数据集是ILSVRC竞赛使用的是数据集,由斯坦福大学李飞飞教授主导,包含了超过1400万张全尺寸的有标记图片,大约有22000个类别的数据。ILSVRC全称ImageNet Large-Scale Visual Recognition Challenge,是视觉领域最受追捧也是最具权威的学术竞赛之一,代表了图像领域的最高水平。从2010年开始举办到2017年最后一届,使用ImageNet数据集的一个子集,总共有1000类。

官网地址:https://image-net.org/index.php

kaggle数据下载地址:https://www.kaggle.com/c/imagenet-object-localization-challenge/overview/description

3.图像分类技术演进(2010 - 2025 年)

该比赛的获胜者从2012年开始都是使用的深度学习的方法:

  • 2012年冠军是AlexNet,由于准确率远超传统方法的第二名(top5错误率为15.3%,第二名为26.2%),引起了很大的轰动。自此之后,CNN成为在图像识别分类的核心算法模型,带来了深度学习的大爆发。
  • 2013年冠军是ZFNet,结构和AlexNet区别不大,分类效果也差不多。
  • 2014年亚军是VGG网络,网络结构十分简单,因此至今VGG-16仍在广泛使用。
  • 2014年的冠军网络是GooLeNet ,核心模块是Inception Module。Inception历经了V1、V2、V3、V4等多个版本的发展,不断趋于完善。GoogLeNet取名中L大写是为了向LeNet致敬,而Inception的名字来源于盗梦空间中的"we need to go deeper"梗。
  • 2015年冠军网络是ResNet。核心是带短连接的残差模块,其中主路径有两层卷积核(Res34),短连接把模块的输入信息直接和经过两次卷积之后的信息融合,相当于加了一个恒等变换。短连接是深度学习又一重要思想,除计算机视觉外,短连接思想也被用到了机器翻译、语音识别/合成领域。
  • 2017年冠军SENet是一个模块,可以和其他的网络架构结合,比如GoogLeNet、ResNet等。

 

年份 代表模型 / 技术 错误率 技术突破与价值(含学习意义)
2010 NEC - UIU 28.20% 基于传统手工特征(如 SIFT、HOG )的方法,是深度学习爆发前的技术基线,可帮助理解 CV 从 “人工设计” 到 “自动学习” 的变革起点,适合对比学习
2011 XRCE 25.80% 延续传统特征工程思路,微调特征组合,凸显手工特征瓶颈,为理解深度学习必要性提供案例
2012 AlexNet 16.40% CNN 里程碑,8 层深度结构 + ReLU + Dropout,首次证明深度学习在图像分类的碾压优势。学习它能掌握 CNN 基础架构(卷积、池化、全连接层协同),是 CV 入门核心模型
2013 ZFNet 11.70% 优化 AlexNet 卷积核,通过可视化分析网络,验证 CNN 可解释性方向。适合学习 “模型迭代优化” 思路,理解小改进对性能的影响
2014 VGG - 16 7.30% 极简 CNN 典范,16 层统一 3×3 小卷积核堆叠,证明 “深度提升性能”。至今广泛用于特征提取(如迁移学习预训练)、教学演示,学习它能掌握 CNN 深度与特征提取的关系,是模型轻量化设计的经典参考
2014 GoogLeNet(Inception v1) 6.67% 创新 Inception 模块,多尺度特征融合 + 稀疏连接,平衡性能与效率。Inception 系列(v1 - v4 )持续优化,学习它可掌握 “多尺度特征利用” 和 “模型效率优化” 思路,对复杂场景分类(如细粒度识别)极具价值
2015 ResNet 3.57% 残差连接革命,通过短连接解决深层网络梯度消失,152 层深度刷新纪录(错误率反超人类)。短连接思想已跨领域应用(机器翻译、语音),学习 ResNet 是掌握深度网络设计、梯度优化的核心,是 CV 进阶必学
2017 SENet 2.25% 通道注意力先驱,动态加权特征通道,可与 ResNet、GoogLeNet 等结合。学习它能掌握注意力机制在 CV 的应用,对提升模型特征选择能力(如医学图像、工业检测)至关重要
2018 EfficientNet ~1.8% 模型缩放标杆,通过复合系数协同调节深度、宽度、分辨率,小参数量实现高性能。适合学习 “工程化模型优化”,理解如何在实际场景平衡精度与计算成本
2019 Vision Transformer(ViT) ~1.6% 打破 CNN 垄断,首次将 Transformer 引入 CV,证明序列建模对图像的适配性。学习 ViT 可掌握跨领域架构融合思路,是理解当前多模态(图文结合)、大模型的基础
2020 Swin Transformer ~1.3% 优化 Transformer 落地,滑动窗口注意力解决长序列计算难题,推动 CV 与 NLP 深度融合。适合学习 “Transformer 工程化改进”,理解如何让大模型适配 CV 任务
2021 BEiT(自监督预训练) ~1.1% 自监督变革,通过掩码图像建模(MIM)减少标注依赖,预训练 + 微调模式普及。学习它能掌握 “无监督学习赋能 CV” 思路,应对数据稀缺场景极具价值
2022 ConvNeXt ~0.9% CNN - Transformer 融合,借鉴 Transformer 设计(逐层下采样、大卷积核),重拾 CNN 优势。适合学习 “架构融合创新”,理解不同范式互补的潜力
2023 SAM + 分类微调 ~0.7% 多模态联动,利用 Segment Anything Model 通用分割能力辅助分类,跨任务特征迁移提升精度。学习它可掌握 “多模态协同” 思路,应对复杂场景分类(如语义关联任务)
2024 AutoML + NAS ~0.5% 全自动模型设计,神经架构搜索(NAS)自动优化拓扑,强化学习调参,突破人工设计瓶颈。适合学习 “AI 自动化” 趋势,理解未来模型开发模式
2025 具身智能辅助 CV ~0.3%(推演) 类人认知升级,结合机器人具身经验(物理环境交互数据),修正图像 “虚假关联”,逼近理论极限。学习它可探索 “CV + 物理反馈” 的前沿方向,理解深度学习与现实世界交互的潜力
 

4.关键学习建议

  1. 基础必学:AlexNet(CNN 入门)、VGG - 16(深度与结构设计)、ResNet(残差思想)
  2. 进阶拓展:GoogLeNet(多尺度融合)、SENet(注意力机制)、ViT(Transformer 跨界)
  3. 前沿关注:EfficientNet(工程化优化)、AutoML(自动化趋势)、具身智能(跨领域融合)
这些模型覆盖了 CV 从 “手工到自动”“单一架构到多模态融合” 的完整演进,学习时可结合代码复现(如用 PyTorch/Keras 搭建网络) + 论文精读(理解创新动机) ,深入掌握技术逻辑~

上述图像分类模型都比较经典,特别是VGG16、GoogLeNet和ResNet,现在仍然在广泛使用,在接下来的课程中我们对这些网络进行逐一介绍。


总结

1.图像分类是什么?

从给定的类别集合中为图像分配对应的类别标签

2.常用的数据集

Mnist,cifar数据集,ImageNet数据集

AlexNet

学习目标

  • 知道AlexNet网络结构
  • 能够利用AlexNet完成图像分类

2012年,AlexNet横空出世,该模型的名字源于论文第一作者的姓名Alex Krizhevsky 。AlexNet使用了8层卷积神经网络,以很大的优势赢得了ImageNet 2012图像识别挑战赛。它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的方向。

1.AlexNet的网络架构

AlexNet与LeNet的设计理念非常相似,但也有显著的区别,其网络架构如下图所示:

该网络的特点是:

  • AlexNet包含8层变换,有5层卷积和2层全连接隐藏层,以及1个全连接输出层。

  • AlexNet第一层中的卷积核形状是。第二层中的卷积核形状减小到,之后全采用。所有的池化层窗口大小为、步幅为2的最大池化。

  • AlexNet将sigmoid激活函数改成了ReLU激活函数,使计算更简单,网络更容易训练。

  • AlexNet通过dropOut来控制全连接层的模型复杂度。

  • AlexNet引入了大量的图像增强,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。

在tf.keras中实现AlexNet模型:

# 构建AlexNet模型
net = tf.keras.models.Sequential([
    # 卷积层:96个卷积核,卷积核为11*11,步幅为4,激活函数relu
    tf.keras.layers.Conv2D(filters=96,kernel_size=11,strides=4,activation='relu'),
    # 池化:窗口大小为3*3、步幅为2
    tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
    # 卷积层:256个卷积核,卷积核为5*5,步幅为1,padding为same,激活函数relu
    tf.keras.layers.Conv2D(filters=256,kernel_size=5,padding='same',activation='relu'),
    # 池化:窗口大小为3*3、步幅为2
    tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
    # 卷积层:384个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
    tf.keras.layers.Conv2D(filters=384,kernel_size=3,padding='same',activation='relu'),
    # 卷积层:384个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
    tf.keras.layers.Conv2D(filters=384,kernel_size=3,padding='same',activation='relu'),
    # 卷积层:256个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
    tf.keras.layers.Conv2D(filters=256,kernel_size=3,padding='same',activation='relu'),
    # 池化:窗口大小为3*3、步幅为2
    tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
    # 伸展为1维向量
    tf.keras.layers.Flatten(),
    # 全连接层:4096个神经元,激活函数relu
    tf.keras.layers.Dense(4096,activation='relu'),
    # 随机失活
    tf.keras.layers.Dropout(0.5),
    # 全链接层:4096个神经元,激活函数relu
    tf.keras.layers.Dense(4096,activation='relu'),
    # 随机失活
    tf.keras.layers.Dropout(0.5),
    # 输出层:10个神经元,激活函数softmax
    tf.keras.layers.Dense(10,activation='softmax')
])

我们构造一个高和宽均为227的单通道数据样本来看一下模型的架构:

# 构造输入X,并将其送入到net网络中
X = tf.random.uniform((1,227,227,1))
y = net(X)
# 通过net.summay()查看网络的形状
net.summary()

网络架构如下:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (1, 55, 55, 96)           11712     
_________________________________________________________________
max_pooling2d (MaxPooling2D) (1, 27, 27, 96)           0         
_________________________________________________________________
conv2d_1 (Conv2D)            (1, 27, 27, 256)          614656    
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (1, 13, 13, 256)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (1, 13, 13, 384)          885120    
_________________________________________________________________
conv2d_3 (Conv2D)            (1, 13, 13, 384)          1327488   
_________________________________________________________________
conv2d_4 (Conv2D)            (1, 13, 13, 256)          884992    
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (1, 6, 6, 256)            0         
_________________________________________________________________
flatten (Flatten)            (1, 9216)                 0         
_________________________________________________________________
dense (Dense)                (1, 4096)                 37752832  
_________________________________________________________________
dropout (Dropout)            (1, 4096)                 0         
_________________________________________________________________
dense_1 (Dense)              (1, 4096)                 16781312  
_________________________________________________________________
dropout_1 (Dropout)          (1, 4096)                 0         
_________________________________________________________________
dense_2 (Dense)              (1, 10)                   40970     
=================================================================
Total params: 58,299,082
Trainable params: 58,299,082
Non-trainable params: 0
_________________________________________________________________

2.手写数字势识别

AlexNet使用ImageNet数据集进行训练,但因为ImageNet数据集较大训练时间较长,我们仍用前面的MNIST数据集来演示AlexNet。读取数据的时将图像高和宽扩大到AlexNet使用的图像高和宽227。这个通过tf.image.resize_with_pad来实现。

2.1 数据读取

首先获取数据,并进行维度调整:

import numpy as np
from tensorflow.keras.datasets import mnist
# 获取手写数字数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()#(60000, 28, 28)
# 训练集数据维度的调整:N H W C
train_images = np.reshape(train_images,(train_images.shape[0],train_images.shape[1],train_images.shape[2],1))#(60000, 28, 28, 1)
# 测试集数据维度的调整:N H W C
test_images = np.reshape(test_images,(test_images.shape[0],test_images.shape[1],test_images.shape[2],1))

由于使用全部数据训练时间较长,我们定义两个方法获取部分数据,并将图像调整为227*227大小,进行模型训练:

# 定义两个方法随机抽取部分样本演示
# 获取训练集数据
def get_train(size):
    # 随机生成要抽样的样本的索引
    index = np.random.randint(0, np.shape(train_images)[0], size)
    # 将这些数据resize成227*227大小
    resized_images = tf.image.resize_with_pad(train_images[index],227,227,)
    # 返回抽取的
    return resized_images.numpy(), train_labels[index]
# 获取测试集数据 
def get_test(size):
    # 随机生成要抽样的样本的索引
    index = np.random.randint(0, np.shape(test_images)[0], size)
    # 将这些数据resize成227*227大小
    resized_images = tf.image.resize_with_pad(test_images[index],227,227,)
    # 返回抽样的测试样本
    return resized_images.numpy(), test_labels[index]

调用上述两个方法,获取参与模型训练和测试的数据集:

# 获取训练样本和测试样本
train_images,train_labels = get_train(256)
test_images,test_labels = get_test(128)

为了让大家更好的理解,我们将数据展示出来:

# 数据展示:将数据集的前九个数据集进行展示
for i in range(9):
    plt.subplot(3,3,i+1)
    # 以灰度图显示,不进行插值
    plt.imshow(train_images[i].astype(np.uint8).squeeze(), cmap='gray', interpolation='none')
    # 设置图片的标题:对应的类别
    plt.title("数字{}".format(train_labels[i]))

结果为:

我们就使用上述创建的模型进行训练和评估。

关于“train_images[i].astype(np.uint8).squeeze()”的作用详解见文末:数据类型转换维度压缩

  • .astype(np.uint8):将数据转为 [0, 255] 的无符号整数,适配 imshow 的默认显示逻辑。
  • .squeeze():移除冗余的单维度,确保图像形状符合 imshow 的输入要求(2D 或 3D 数组)。
  • cmap='gray':指定使用灰度色彩映射,适用于单通道图像。
  • interpolation='none':禁用插值算法,直接显示像素值(而非平滑过渡),常用于显示低分辨率图像或确保像素边界清晰。

 

2.2 模型编译

# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0, nesterov=False)

net.compile(optimizer=optimizer,
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

2.3 模型训练

# 模型训练:指定训练数据,batchsize,epoch,验证集
net.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)

训练输出为:

Epoch 1/3
2/2 [==============================] - 3s 2s/step - loss: 2.3003 - accuracy: 0.0913 - val_loss: 2.3026 - val_accuracy: 0.0000e+00
Epoch 2/3
2/2 [==============================] - 3s 2s/step - loss: 2.3069 - accuracy: 0.0957 - val_loss: 2.3026 - val_accuracy: 0.0000e+00
Epoch 3/3
2/2 [==============================] - 4s 2s/step - loss: 2.3117 - accuracy: 0.0826 - val_loss: 2.3026 - val_accuracy: 0.0000e+00

2.4 模型评估

# 指定测试数据
net.evaluate(test_images,test_labels,verbose=1)

输出为:

4/4 [==============================] - 1s 168ms/step - loss: 2.3026 - accuracy: 0.0781
[2.3025851249694824, 0.078125]

如果我们使用整个数据集训练网络,并进行评估的结果:

[0.4866700246334076, 0.8395]

总结

  • 知道AlexNet的网络架构
  • 动手实现手写数字的识别

VGG

学习目标

  • 知道VGG网络结构的特点
  • 能够利用VGG完成图像分类

2014年,牛津大学计算机视觉组(Visual Geometry Group)和Google DeepMind公司的研究员一起研发出了新的深度卷积神经网络:VGGNet,并取得了ILSVRC2014比赛分类项目的第二名,主要贡献是使用很小的卷积核(3×3)构建卷积神经网络结构,能够取得较好的识别精度,常用来提取图像特征的VGG-16和VGG-19。

1.VGG的网络架构

VGG可以看成是加深版的AlexNet,整个网络由卷积层和全连接层叠加而成,和AlexNet不同的是,VGG中使用的都是小尺寸的卷积核(3×3),其网络架构如下图所示:

VGGNet使用的全部都是3x3的小卷积核和2x2的池化核,通过不断加深网络来提升性能。VGG可以通过重复使用简单的基础块来构建深度模型。

在tf.keras中实现VGG模型,首先来实现VGG块,它的组成规律是:连续使用多个相同的填充为1、卷积核大小为的卷积层后接上一个步幅为2、窗口形状为的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。我们使用vgg_block函数来实现这个基础的VGG块,它可以指定卷积层的数量num_convs和每层的卷积核个数num_filters:

# 定义VGG网络中的卷积块:卷积层的个数,卷积层中卷积核的个数
def vgg_block(num_convs, num_filters):
    # 构建序列模型
    blk = tf.keras.models.Sequential()
    # 遍历所有的卷积层
    for _ in range(num_convs):
        # 每个卷积层:num_filter个卷积核,卷积核大小为3*3,padding是same,激活函数是relu
        blk.add(tf.keras.layers.Conv2D(num_filters,kernel_size=3,
                                    padding='same',activation='relu'))
    # 卷积块最后是一个最大池化,窗口大小为2*2,步长为2
    blk.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
    return blk

VGG16网络有5个卷积块,前2块使用两个卷积层,而后3块使用三个卷积层。第一块的输出通道是64,之后每次对输出通道数翻倍,直到变为512。

# 定义5个卷积块,指明每个卷积块中的卷积层个数及相应的卷积核个数
conv_arch = ((2, 64), (2, 128), (3, 256), (3, 512), (3, 512))

因为这个网络使用了13个卷积层和3个全连接层,所以经常被称为VGG-16,通过制定conv_arch得到模型架构后构建VGG16:

# 定义VGG网络
def vgg(conv_arch):
    # 构建序列模型
    net = tf.keras.models.Sequential()
    # 根据conv_arch生成卷积部分
    for (num_convs, num_filters) in conv_arch:
        net.add(vgg_block(num_convs, num_filters))
    # 卷积块序列后添加全连接层
    net.add(tf.keras.models.Sequential([
        # 将特征图展成一维向量
        tf.keras.layers.Flatten(),
        # 全连接层:4096个神经元,激活函数是relu
        tf.keras.layers.Dense(4096, activation='relu'),
        # 随机失活
        tf.keras.layers.Dropout(0.5),
        # 全连接层:4096个神经元,激活函数是relu
        tf.keras.layers.Dense(4096, activation='relu'),
        # 随机失活
        tf.keras.layers.Dropout(0.5),
        # 全连接层:10个神经元,激活函数是softmax
        tf.keras.layers.Dense(10, activation='softmax')]))
    return net
# 网络实例化
net = vgg(conv_arch)

我们构造一个高和宽均为224的单通道数据样本来看一下模型的架构:

# 构造输入X,并将其送入到net网络中
X = tf.random.uniform((1,224,224,1))
y = net(X)
# 通过net.summay()查看网络的形状
net.summary()

网络架构如下:

Model: "sequential_15"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
sequential_16 (Sequential)   (1, 112, 112, 64)         37568     
_________________________________________________________________
sequential_17 (Sequential)   (1, 56, 56, 128)          221440    
_________________________________________________________________
sequential_18 (Sequential)   (1, 28, 28, 256)          1475328   
_________________________________________________________________
sequential_19 (Sequential)   (1, 14, 14, 512)          5899776   
_________________________________________________________________
sequential_20 (Sequential)   (1, 7, 7, 512)            7079424   
_________________________________________________________________
sequential_21 (Sequential)   (1, 10)                   119586826 
=================================================================
Total params: 134,300,362
Trainable params: 134,300,362
Non-trainable params: 0
__________________________________________________________________

2.手写数字势识别

因为ImageNet数据集较大训练时间较长,我们仍用前面的MNIST数据集来演示VGGNet。读取数据的时将图像高和宽扩大到VggNet使用的图像高和宽224。这个通过tf.image.resize_with_pad来实现。

2.1 数据读取

参照上文AlexNet:数据读取

注意:数据读取中需要“将图像调整为224*224大小”:

# 将这些数据resize成224*224大小
    resized_images = tf.image.resize_with_pad(train_images[index],224,224,)

其他一律一样。

我们就使用上述创建的模型进行训练和评估。

2.2 模型编译

# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0)

net.compile(optimizer=optimizer,
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

2.3 模型训练

# 模型训练:指定训练数据,batchsize,epoch,验证集
net.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)

训练输出为:

Epoch 1/3
2/2 [==============================] - 34s 17s/step - loss: 2.6026 - accuracy: 0.0957 - val_loss: 2.2982 - val_accuracy: 0.0385
Epoch 2/3
2/2 [==============================] - 27s 14s/step - loss: 2.2604 - accuracy: 0.1087 - val_loss: 2.4905 - val_accuracy: 0.1923
Epoch 3/3
2/2 [==============================] - 29s 14s/step - loss: 2.3650 - accuracy: 0.1000 - val_loss: 2.2994 - val_accuracy: 0.1538

2.4 模型评估

# 指定测试数据
net.evaluate(test_images,test_labels,verbose=1)

输出为:

4/4 [==============================] - 5s 1s/step - loss: 2.2955 - accuracy: 0.1016
[2.2955007553100586, 0.1015625]

如果我们使用整个数据集训练网络,并进行评估的结果:

[0.31822608125209806, 0.8855]

总结

  • 知道VGG的网络架构
  • 动手实现手写数字的识别

GoogLeNet

学习目标

  • 知道GoogLeNet网络结构的特点
  • 能够利用GoogLeNet完成图像分类

GoogLeNet的名字不是GoogleNet,而是GoogLeNet,这是为了致敬LeNet。GoogLeNet和AlexNet/VGGNet这类依靠加深网络结构的深度的思想不完全一样。GoogLeNet在加深度的同时做了结构上的创新,引入了一个叫做Inception的结构来代替之前的卷积加激活的经典组件。GoogLeNet在ImageNet分类比赛上的Top-5错误率降低到了6.7%。。

1.Inception 块

GoogLeNet中的基础卷积块叫作Inception块,得名于同名电影《盗梦空间》(Inception)。Inception块在结构比较复杂,如下图所示:

Inception块里有4条并行的线路。前3条线路使用窗口大小分别是的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做卷积来减少输入通道数,以降低模型复杂度。第4条线路则使用最大池化层,后接卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并向后进行传输。

关于Inception块,进一步理解参照文末:Inception块

卷积

它的计算方法和其他卷积核一样,唯一不同的是它的大小是,没有考虑在特征图局部信息之间的关系。

它的作用主要是:

  • 实现跨通道的交互和信息整合

  • 卷积核通道数的降维和升维,减少网络参数

在tf.keras中实现Inception模块,各个卷积层卷积核的个数通过输入参数来控制,如下所示:

# 定义Inception模块
class Inception(tf.keras.layers.Layer):
    # 输入参数为各个卷积的卷积核个数
    def __init__(self, c1, c2, c3, c4):
        super().__init__()
        # 线路1:1 x 1卷积层,激活函数是RELU,padding是same
        self.p1_1 = tf.keras.layers.Conv2D(
            c1, kernel_size=1, activation='relu', padding='same')
        # 线路2,1 x 1卷积层后接3 x 3卷积层,激活函数是RELU,padding是same
        self.p2_1 = tf.keras.layers.Conv2D(
            c2[0], kernel_size=1, padding='same', activation='relu')
        self.p2_2 = tf.keras.layers.Conv2D(c2[1], kernel_size=3, padding='same',
                                           activation='relu')
        # 线路3,1 x 1卷积层后接5 x 5卷积层,激活函数是RELU,padding是same
        self.p3_1 = tf.keras.layers.Conv2D(
            c3[0], kernel_size=1, padding='same', activation='relu')
        self.p3_2 = tf.keras.layers.Conv2D(c3[1], kernel_size=5, padding='same',
                                           activation='relu')
        # 线路4,3 x 3最大池化层后接1 x 1卷积层,激活函数是RELU,padding是same
        self.p4_1 = tf.keras.layers.MaxPool2D(
            pool_size=3, padding='same', strides=1)
        self.p4_2 = tf.keras.layers.Conv2D(
            c4, kernel_size=1, padding='same', activation='relu')
    # 完成前向传播过程
    def call(self, x):
        # 线路1
        p1 = self.p1_1(x)
        # 线路2
        p2 = self.p2_2(self.p2_1(x))
        # 线路3
        p3 = self.p3_2(self.p3_1(x))
        # 线路4
        p4 = self.p4_2(self.p4_1(x))
        # 在通道维上concat输出
        outputs = tf.concat([p1, p2, p3, p4], axis=-1)
        return outputs  

指定通道数,对Inception模块进行实例化:

Inception(64, (96, 128), (16, 32), 32)

2.GoogLeNet模型

GoogLeNet主要由Inception模块构成,如下图所示:

整个网络架构我们分为五个模块,每个模块之间使用步幅为2的最大池化层来减小输出高宽。

2.1 B1模块

第一模块使用一个64通道的卷积层。

# 定义模型的输入
inputs = tf.keras.Input(shape=(224,224,3),name = "input")
# b1 模块
# 卷积层7*7的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same', activation='relu')(inputs)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

2.2 B2模块

第二模块使用2个卷积层:首先是64通道的卷积层,然后是将通道增大3倍的卷积层。

# b2 模块
# 卷积层1*1的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(64, kernel_size=1, padding='same', activation='relu')(x)
# 卷积层3*3的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(192, kernel_size=3, padding='same', activation='relu')(x)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

2.3 B3模块

第三模块串联2个完整的Inception块。第一个Inception块的输出通道数为。第二个Inception块输出通道数增至

# b3 模块
# Inception
x = Inception(64, (96, 128), (16, 32), 32)(x)
# Inception
x = Inception(128, (128, 192), (32, 96), 64)(x)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

2.4 B4模块

第四模块更加复杂。它串联了5个Inception块,其输出通道数分别是。并且增加了辅助分类器,根据实验发现网络的中间层具有很强的识别能力,为了利用中间层抽象的特征,在某些中间层中添加含有多层的分类器,如下图所示:

实现如下所示:

def aux_classifier(x, filter_size):
    #x:输入数据,filter_size:卷积层卷积核个数,全连接层神经元个数
    # 池化层
    x = tf.keras.layers.AveragePooling2D(
        pool_size=5, strides=3, padding='same')(x)
    # 1x1 卷积层
    x = tf.keras.layers.Conv2D(filters=filter_size[0], kernel_size=1, strides=1,
                               padding='valid', activation='relu')(x)
    # 展平
    x = tf.keras.layers.Flatten()(x)
    # 全连接层1
    x = tf.keras.layers.Dense(units=filter_size[1], activation='relu')(x)
    # softmax输出层
    x = tf.keras.layers.Dense(units=10, activation='softmax')(x)
    return x

b4模块的实现:

# b4 模块
# Inception
x = Inception(192, (96, 208), (16, 48), 64)(x)
# 辅助输出1
aux_output_1 = aux_classifier(x, [128, 1024])
# Inception
x = Inception(160, (112, 224), (24, 64), 64)(x)
# Inception
x = Inception(128, (128, 256), (24, 64), 64)(x)
# Inception
x = Inception(112, (144, 288), (32, 64), 64)(x)
# 辅助输出2
aux_output_2 = aux_classifier(x, [128, 1024])
# Inception
x = Inception(256, (160, 320), (32, 128), 128)(x)
# 最大池化
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

2.5 B5模块

第五模块有输出通道数为的两个Inception块。后面紧跟输出层,该模块使用全局平均池化层(GAP)来将每个通道的高和宽变成1。最后输出变成二维数组后接输出个数为标签类别数的全连接层。

全局平均池化层(GAP)

用来替代全连接层,将特征图每一通道中所有像素值相加后求平均,得到就是GAP的结果,在将其送入后续网络中进行计算

实现过程是:

# b5 模块
# Inception
x = Inception(256, (160, 320), (32, 128), 128)(x)
# Inception
x = Inception(384, (192, 384), (48, 128), 128)(x)
# GAP
x = tf.keras.layers.GlobalAvgPool2D()(x)
# 输出层
main_outputs = tf.keras.layers.Dense(10,activation='softmax')(x)
# 使用Model来创建模型,指明输入和输出

构建GoogLeNet模型并通过summary来看下模型的结构:

# 使用Model来创建模型,指明输入和输出
model = tf.keras.Model(inputs=inputs, outputs=[main_outputs,aux_output_1,aux_output_2]) 
model.summary()
Model: "functional_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           [(None, 224, 224, 3)]     0         
_________________________________________________________________
conv2d_122 (Conv2D)          (None, 112, 112, 64)      9472      
_________________________________________________________________
max_pooling2d_27 (MaxPooling (None, 56, 56, 64)        0         
_________________________________________________________________
conv2d_123 (Conv2D)          (None, 56, 56, 64)        4160      
_________________________________________________________________
conv2d_124 (Conv2D)          (None, 56, 56, 192)       110784    
_________________________________________________________________
max_pooling2d_28 (MaxPooling (None, 28, 28, 192)       0         
_________________________________________________________________
inception_19 (Inception)     (None, 28, 28, 256)       163696    
_________________________________________________________________
inception_20 (Inception)     (None, 28, 28, 480)       388736    
_________________________________________________________________
max_pooling2d_31 (MaxPooling (None, 14, 14, 480)       0         
_________________________________________________________________
inception_21 (Inception)     (None, 14, 14, 512)       376176    
_________________________________________________________________
inception_22 (Inception)     (None, 14, 14, 512)       449160    
_________________________________________________________________
inception_23 (Inception)     (None, 14, 14, 512)       510104    
_________________________________________________________________
inception_24 (Inception)     (None, 14, 14, 528)       605376    
_________________________________________________________________
inception_25 (Inception)     (None, 14, 14, 832)       868352    
_________________________________________________________________
max_pooling2d_37 (MaxPooling (None, 7, 7, 832)         0         
_________________________________________________________________
inception_26 (Inception)     (None, 7, 7, 832)         1043456   
_________________________________________________________________
inception_27 (Inception)     (None, 7, 7, 1024)        1444080   
_________________________________________________________________
global_average_pooling2d_2 ( (None, 1024)              0         
_________________________________________________________________
dense_10 (Dense)             (None, 10)                10250     
=================================================================
Total params: 5,983,802
Trainable params: 5,983,802
Non-trainable params: 0
___________________________________________________________

3.手写数字识别

因为ImageNet数据集较大训练时间较长,我们仍用前面的MNIST数据集来演示GoogLeNet。读取数据的时将图像高和宽扩大到图像高和宽224。这个通过tf.image.resize_with_pad来实现。

2.1 数据读取

参照上文AlexNet:数据读取

 

3.2 模型编译

# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0)
# 模型有3个输出,所以指定损失函数对应的权重系数
net.compile(optimizer=optimizer,
             loss=['sparse_categorical_crossentropy', 
                   'sparse_categorical_crossentropy', 
                   'sparse_categorical_crossentropy'],
              metrics=['accuracy'],loss_weights=[1,0.3,0.3])

3.3 模型训练

# 模型训练:指定训练数据,batchsize,epoch,验证集
net.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)

训练过程:

Epoch 1/3
2/2 [==============================] - 8s 4s/step - loss: 2.9527 - accuracy: 0.1174 - val_loss: 3.3254 - val_accuracy: 0.1154
Epoch 2/3
2/2 [==============================] - 7s 4s/step - loss: 2.8111 - accuracy: 0.0957 - val_loss: 2.2718 - val_accuracy: 0.2308
Epoch 3/3
2/2 [==============================] - 7s 4s/step - loss: 2.3055 - accuracy: 0.0957 - val_loss: 2.2669 - val_accuracy: 0.2308

2.4 模型评估

# 指定测试数据
net.evaluate(test_images,test_labels,verbose=1)

输出为:

4/4 [==============================] - 1s 338ms/step - loss: 2.3110 - accuracy: 0.0781
[2.310971260070801, 0.078125]

4.延伸版本

GoogLeNet是以InceptionV1为基础进行构建的,所以GoogLeNet也叫做InceptionNet,在随后的⼏年⾥,研究⼈员对GoogLeNet进⾏了数次改进, 就又产生了InceptionV2,V3,V4等版本。

4.1 InceptionV2

在InceptionV2中将大卷积核拆分为小卷积核,将V1中的的卷积用两个的卷积替代,从而增加网络的深度,减少了参数。

4.2 InceptionV3

将n×n卷积分割为1×n和n×1两个卷积,例如,一个的卷积首先执行一个的卷积,然后执行一个的卷积,这种方法的参数量和计算量都比原来降低。

from tensorflow.keras.applications.inception_v3 import InceptionV3

# 加载预训练模型或自定义Inception模块
base_model = InceptionV3(weights='imagenet', include_top=False)

总结

  • 知道GoogLeNet的网络架构:有基础模块Inception构成
  • 能够利用GoogleNet完成图像分类

ResNet

学习目标

  • 知道ResNet网络结构的特点
  • 能够利用ResNet完成图像分类

网络越深,获取的信息就越多,特征也越丰富。但是在实践中,随着网络的加深,优化效果反而越差,测试数据和训练数据的准确率反而降低了。

针对这一问题,何恺明等人提出了残差网络(ResNet)在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。

1 残差块

假设 F(x) 代表某个只包含有两层的映射函数, x 是输入, F(x)是输出。假设他们具有相同的维度。在训练的过程中我们希望能够通过修改网络中的 w和b去拟合一个理想的 H(x)(从输入到输出的一个理想的映射函数)。也就是我们的目标是修改F(x) 中的 w和b逼近 H(x) 。如果我们改变思路,用F(x) 来逼近 H(x)-x ,那么我们最终得到的输出就变为 F(x)+x(这里的加指的是对应位置上的元素相加,也就是element-wise addition),这里将直接从输入连接到输出的结构也称为shortcut,那整个结构就是残差块,ResNet的基础模块。

残差的含义:输出与输入之间的差异部分。

ResNet沿用了VGG全卷积层的设计。残差块里首先有2个有相同输出通道数的卷积层。每个卷积层后接BN层和ReLU激活函数,然后将输入直接加在最后的ReLU激活函数前,这种结构用于层数较少的神经网络中,比如ResNet34。若输入通道数比较多,就需要引入卷积层来调整输入的通道数,这种结构也叫作瓶颈模块,通常用于网络层数较多的结构中。如下图所示:

上图左图中的残差块的实现如下,可以设定输出通道数,是否使用1*1的卷积及卷积层的步幅。

# 导入相关的工具包
import tensorflow as tf
from tensorflow.keras import layers, activations


# 定义ResNet的残差块
class Residual(tf.keras.Model):
    # 指明残差块的通道数,是否使用1*1卷积,步长
    def __init__(self, num_channels, use_1x1conv=False, strides=1):
        super(Residual, self).__init__()
        # 卷积层:指明卷积核个数,padding,卷积核大小,步长
        self.conv1 = layers.Conv2D(num_channels,padding='same',kernel_size=3,strides=strides)
        # 卷积层:指明卷积核个数,padding,卷积核大小,步长
        self.conv2 = layers.Conv2D(num_channels, padding='same', kernel_size=3)
        if use_1x1conv:
            self.conv3 = layers.Conv2D(num_channels,kernel_size=1,strides=strides)
        else:
            self.conv3 = None
        # 指明BN层
        self.bn1 = layers.BatchNormalization()
        self.bn2 = layers.BatchNormalization()

    # 定义前向传播过程
    def call(self, X):
        # 卷积,BN,激活
        Y = activations.relu(self.bn1(self.conv1(X)))
        # 卷积,BN
        Y = self.bn2(self.conv2(Y))
        # 对输入数据进行1*1卷积保证通道数相同
        if self.conv3:
            X = self.conv3(X)
        # 返回与输入相加后激活的结果
        return activations.relu(Y + X)

1*1卷积用来调整通道数。

2 ResNet模型

ResNet模型的构成如下图所示:

ResNet网络中按照残差块的通道数分为不同的模块。第一个模块前使用了步幅为2的最大池化层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。

下面我们来实现这些模块。注意,这里对第一个模块做了特别处理。

# ResNet网络中模块的构成
class ResnetBlock(tf.keras.layers.Layer):
    # 网络层的定义:输出通道数(卷积核个数),模块中包含的残差块个数,是否为第一个模块
    def __init__(self,num_channels, num_residuals, first_block=False):
        super(ResnetBlock, self).__init__()
        # 模块中的网络层
        self.listLayers=[]
        # 遍历模块中所有的层
        for i in range(num_residuals):
            # 若为第一个残差块并且不是第一个模块,则使用1*1卷积,步长为2(目的是减小特征图,并增大通道数)
            if i == 0 and not first_block:
                self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
            # 否则不使用1*1卷积,步长为1 
            else:
                self.listLayers.append(Residual(num_channels))      
    # 定义前向传播过程
    def call(self, X):
        # 所有层依次向前传播即可
        for layer in self.listLayers:
            X = layer(X)
        return X

ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的卷积层后接步幅为2的的最大池化层。不同之处在于ResNet每个卷积层后增加了BN层,接着是所有残差模块,最后,与GoogLeNet一样,加入全局平均池化层(GAP)后接上全连接层输出。

# 构建ResNet网络
class ResNet(tf.keras.Model):
    # 初始化:指定每个模块中的残差快的个数
    def __init__(self,num_blocks):
        super(ResNet, self).__init__()
        # 输入层:7*7卷积,步长为2
        self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
        # BN层
        self.bn=layers.BatchNormalization()
        # 激活层
        self.relu=layers.Activation('relu')
        # 最大池化层
        self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
        # 第一个block,通道数为64
        self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
        # 第二个block,通道数为128
        self.resnet_block2=ResnetBlock(128,num_blocks[1])
        # 第三个block,通道数为256
        self.resnet_block3=ResnetBlock(256,num_blocks[2])
        # 第四个block,通道数为512
        self.resnet_block4=ResnetBlock(512,num_blocks[3])
        # 全局平均池化
        self.gap=layers.GlobalAvgPool2D()
        # 全连接层:分类
        self.fc=layers.Dense(units=10,activation=tf.keras.activations.softmax)
    # 前向传播过程
    def call(self, x):
        # 卷积
        x=self.conv(x)
        # BN
        x=self.bn(x)
        # 激活
        x=self.relu(x)
        # 最大池化
        x=self.mp(x)
        # 残差模块
        x=self.resnet_block1(x)
        x=self.resnet_block2(x)
        x=self.resnet_block3(x)
        x=self.resnet_block4(x)
        # 全局平均池化
        x=self.gap(x)
        # 全链接层
        x=self.fc(x)
        return x
# 模型实例化:指定每个block中的残差块个数 
mynet=ResNet([2,2,2,2])

这里每个模块里有4个卷积层(不计算 1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型被称为ResNet-18。通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。 在训练ResNet之前,我们来观察一下输入形状在ResNe的架构:

X = tf.random.uniform(shape=(1,  224, 224 , 1))
y = mynet(X)
mynet.summary()
Model: "res_net"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_2 (Conv2D)            multiple                  3200      
_________________________________________________________________
batch_normalization_2 (Batch multiple                  256       
_________________________________________________________________
activation (Activation)      multiple                  0         
_________________________________________________________________
max_pooling2d (MaxPooling2D) multiple                  0         
_________________________________________________________________
resnet_block (ResnetBlock)   multiple                  148736    
_________________________________________________________________
resnet_block_1 (ResnetBlock) multiple                  526976    
_________________________________________________________________
resnet_block_2 (ResnetBlock) multiple                  2102528   
_________________________________________________________________
resnet_block_3 (ResnetBlock) multiple                  8399360   
_________________________________________________________________
global_average_pooling2d (Gl multiple                  0         
_________________________________________________________________
dense (Dense)                multiple                  5130      
=================================================================
Total params: 11,186,186
Trainable params: 11,178,378
Non-trainable params: 7,808
_________________________________________________________________

2.手写数字势识别

因为ImageNet数据集较大训练时间较长,我们仍用前面的MNIST数据集来演示resNet。读取数据的时将图像高和宽扩大到ResNet使用的图像高和宽224。这个通过tf.image.resize_with_pad来实现。

2.1 数据读取

参照上文AlexNet:数据读取

注意:数据读取中需要“将图像调整为224*224大小”。

2.2 模型编译

# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0)

mynet.compile(optimizer=optimizer,
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

2.3 模型训练

# 模型训练:指定训练数据,batchsize,epoch,验证集
mynet.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)

训练输出为:

Epoch 1/3
2/2 [==============================] - 10s 5s/step - loss: 2.7811 - accuracy: 0.1391 - val_loss: 4.7931 - val_accuracy: 0.1923
Epoch 2/3
2/2 [==============================] - 8s 4s/step - loss: 2.2579 - accuracy: 0.2478 - val_loss: 2.9262 - val_accuracy: 0.2692
Epoch 3/3
2/2 [==============================] - 15s 7s/step - loss: 2.0874 - accuracy: 0.2609 - val_loss: 2.5882 - val_accuracy: 0.2692

2.4 模型评估

# 指定测试数据
mynet.evaluate(test_images,test_labels,verbose=1)

输出为:

4/4 [==============================] - 1s 370ms/step - loss: 3.4343 - accuracy: 0.1016
[3.4342570304870605, 0.1015625]

总结

  • 知道ResNet的网络结构,残差块的构成
  • 能够搭建ResNet网络结构

图像增强

学习目标

  • 知道图像增强的常用方法
  • 能够利用tf.keras来完成图像增强

大规模数据集是成功应用深度神经网络的前提。例如,我们可以对图像进行不同方式的裁剪,使感兴趣的物体出现在不同位置,从而减轻模型对物体出现位置的依赖性。我们也可以调整亮度、色彩等因素来降低模型对色彩的敏感度。可以说,在当年AlexNet的成功中,图像增强技术功不可没

1.常用的图像增强方法

图像增强(image augmentation)指通过剪切、旋转/反射/翻转变换、缩放变换、平移变换、尺度变换、对比度变换、噪声扰动、颜色变换等一种或多种组合数据增强变换的方式来增加数据集的大小。图像增强的意义是通过对训练图像做一系列随机改变,来产生相似但又不同的训练样本,从而扩大训练数据集的规模,而且随机改变训练样本可以降低模型对某些属性的依赖,从而提高模型的泛化能力。

常见的图像增强方式可以分为两类:几何变换类和颜色变换类

  • 几何变换类,主要是对图像进行几何变换操作,包括**翻转,旋转,裁剪,变形,缩放**等。

  • 颜色变换类,指通过模糊、颜色变换、擦除、填充等方式对图像进行处理

实现图像增强可以通过tf.image来完成,也可以通过tf.keras.imageGenerator来完成。

2.tf.image进行图像增强

导入所需的工具包并读取要处理的图像:

# 导入工具包
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
# 读取图像并显示
cat = plt.imread('./cat.jpg')
plt.imshow(cat)

2.1 翻转和裁剪

左右翻转图像是最早也是最广泛使用的一种图像增广方法。可以通过tf.image.random_flip_left_right来实现图像左右翻转。

# 左右翻转并显示
cat1 = tf.image.random_flip_left_right(cat)
plt.imshow(cat1)

创建tf.image.random_flip_up_down实例来实现图像的上下翻转,上下翻转使用的较少。

# 上下翻转
cat2 = tf.image.random_flip_up_down(cat)
plt.imshow(cat2)

随机裁剪出一块面积为原面积的区域,且该区域的宽和高之比随机取自,然后再将该区域的宽和高分别缩放到200像素。

# 随机裁剪
cat3 = tf.image.random_crop(cat,(200,200,3))
plt.imshow(cat3)

2.2 颜色变换

另一类增广方法是颜色变换。我们可以从4个方面改变图像的颜色:亮度、对比度、饱和度和色调。接下来将图像的亮度随机变化为原图亮度的(即(即)。

cat4=tf.image.random_brightness(cat,0.5)
plt.imshow(cat4)

类似地,我们也可以随机变化图像的色调

cat5 = tf.image.random_hue(cat,0.5)
plt.imshow(cat5)

上面的案例如果报错,可能是图片通道数不对,可以参照如下代码:

import tensorflow as tf
import matplotlib.pyplot as plt

# 假设cat是一个4通道的图像张量
# 移除Alpha通道,只保留RGB三个通道
cat_rgb = cat[..., :3]  # 选取前三个通道

# 应用随机色调调整
cat5 = tf.image.random_hue(cat_rgb, 0.5)

# 显示处理后的图像
plt.imshow(cat5)
plt.axis('off')  # 关闭坐标轴
plt.show()

 

3 使用ImageDataGenerator()进行图像增强

ImageDataGenerator()是keras.preprocessing.image模块中的图片生成器,可以在batch中对数据进行增强,扩充数据集大小,增强模型的泛化能力。比如旋转,变形等,如下所示:

keras.preprocessing.image.ImageDataGenerator(
               rotation_range=0, #整数。随机旋转的度数范围。
               width_shift_range=0.0, #浮点数、宽度平移
               height_shift_range=0.0, #浮点数、高度平移
               brightness_range=None, # 亮度调整
               shear_range=0.0, # 裁剪
               zoom_range=0.0, #浮点数 或 [lower, upper]。随机缩放范围
               horizontal_flip=False, # 左右翻转
               vertical_flip=False, # 垂直翻转
               rescale=None # 尺度调整
            )

来看下水平翻转的结果:

# 获取数据集
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()

# 将数据转换为4维的形式
x_train = X_train.reshape(X_train.shape[0],28,28,1)
x_test = X_test.reshape(X_test.shape[0],28,28,1)
# 设置图像增强方式:水平翻转
datagen = tf.keras.preprocessing.image.ImageDataGenerator(horizontal_flip=True)
# 查看增强后的结果
for X_batch,y_batch in datagen.flow(x_train,y_train,batch_size=9):
    plt.figure(figsize=(8,8)) # 设定每个图像显示的大小
    # 产生一个3*3网格的图像
    for i in range(0,9):
        plt.subplot(330+1+i) 
        plt.title(y_batch[i])
        plt.axis('off')
        plt.imshow(X_batch[i].reshape(28,28),cmap='gray')
    plt.show()
    break


总结

  • 常用的图像增强方法:几何和颜色
  • 在tf,keras中可以通过:tf.image和ImageDataGenerator()完成图像增强

微调

学习目标

  • 知道微调的原理
  • 能够利用微调模型来完成图像的分类任务

1.微调

如何在只有6万张图像的MNIST训练数据集上训练模型。学术界当下使用最广泛的大规模图像数据集ImageNet,它有超过1,000万的图像和1,000类的物体。然而,我们平常接触到数据集的规模通常在这两者之间。假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户。一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然后在收集到的图像数据集上训练一个分类模型。

另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别椅子也可能同样有效。

微调由以下4步构成。

  1. 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
  2. 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
  3. 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
  4. 在目标数据集(如椅子数据集)上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

当目标数据集远小于源数据集时,微调有助于提升模型的泛化能力。

2.热狗识别

接下来我们来实践一个具体的例子:热狗识别。将基于一个小数据集对在ImageNet数据集上训练好的ResNet模型进行微调。该小数据集含有数千张热狗或者其他事物的图像。我们将使用微调得到的模型来识别一张图像中是否包含热狗。

首先,导入实验所需的工具包。

import tensorflow as tf
import numpy as np

2.1 获取数据集

我们首先将数据集放在路径D:\learn\000人工智能数据大全\黑马数据\深度学习\热狗数据集\hotdog\train之下:

每个类别文件夹里面是图像文件。

上一节中我们介绍了ImageDataGenerator进行图像增强,我们可以通过以下方法读取图像文件,该方法以文件夹路径为参数,生成经过图像增强后的结果,并产生batch数据:

flow_from_directory(self, directory,
                            target_size=(256, 256), color_mode='rgb',
                            classes=None, class_mode='categorical',
                            batch_size=32, shuffle=True, seed=None,
                            save_to_dir=None)

主要参数:

  • directory: 目标文件夹路径,对于每一个类对应一个子文件夹,该子文件夹中任何JPG、PNG、BNP、PPM的图片都可以读取。
  • target_size: 默认为(256, 256),图像将被resize成该尺寸。
  • batch_size: batch数据的大小,默认32。
  • shuffle: 是否打乱数据,默认为True。

详参下文:数据生成器(Generator)

我们创建两个tf.keras.preprocessing.image.ImageDataGenerator实例来分别读取训练数据集和测试数据集中的所有图像文件。将训练集图片全部处理为高和宽均为224像素的输入。此外,我们对RGB(红、绿、蓝)三个颜色通道的数值做标准化。

# 获取数据集
import pathlib
train_dir = r'D:\learn\000人工智能数据大全\黑马数据\深度学习\热狗数据集\hotdog\train'
test_dir = r'D:\learn\000人工智能数据大全\黑马数据\深度学习\热狗数据集\hotdog\test'
# 获取训练集数据
# 将字符串路径转换为pathlib.Path对象,便于后续的路径操作。
train_dir = pathlib.Path(train_dir)#WindowsPath('D:/learn/000人工智能数据大全/黑马数据/深度学习/热狗数据集/hotdog/train')
train_count = len(list(train_dir.glob('*/*.png')))
# 获取测试集数据
test_dir = pathlib.Path(test_dir)
test_count = len(list(test_dir.glob('*/*.png')))
# 创建imageDataGenerator进行图像处理
image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
# 设置参数
BATCH_SIZE = 32
IMG_HEIGHT = 224
IMG_WIDTH = 224
# 获取训练数据
train_data_gen = image_generator.flow_from_directory(directory=str(train_dir),
                                                    batch_size=BATCH_SIZE,
                                                    target_size=(IMG_HEIGHT, IMG_WIDTH),
                                                    shuffle=True)
# 获取测试数据
test_data_gen = image_generator.flow_from_directory(directory=str(test_dir),
                                                    batch_size=BATCH_SIZE,
                                                    target_size=(IMG_HEIGHT, IMG_WIDTH),
                                                    shuffle=True)
  • train_dir.glob('*/*.jpg'):递归查找train_dir下所有子目录中的.jpg文件。
  • list(...):将生成器转换为列表。
  • len(...):计算列表长度,即训练集图片总数。

下面我们随机取1个batch的图片然后绘制出来。

import matplotlib.pyplot as plt
# 显示图像
def show_batch(image_batch, label_batch):
    plt.figure(figsize=(10,10))
    for n in range(15):
        ax = plt.subplot(5,5,n+1)
        plt.imshow(image_batch[n])
        plt.axis('off')
        plt.title(label_batch[n])#将分类标签作为title显示
# 随机选择一个batch的图像        
image_batch, label_batch = next(train_data_gen)
# 图像显示
show_batch(image_batch, label_batch)
next() 是 Python 的内置函数,用于从迭代器中获取下一个元素。对于数据生成器 train_data_gen
  • 每次调用 next(train_data_gen),会返回一个批次(本案例32个)的图像和对应的标签。
  • 图像和标签通常以 (image_batch, label_batch) 的元组形式返回。
  • 当所有数据被遍历完后,生成器会重新从头开始(除非设置了 shuffle=False)。

2.2 模型构建与训练

我们使用在ImageNet数据集上预训练的ResNet-50作为源模型。这里指定weights='imagenet'来自动下载并加载预训练的模型参数。在第一次使用时需要联网下载模型参数。

Keras应用程序(keras.applications)是具有预先训练权值的固定架构,该类封装了很多重量级的网络架构,如下图所示:

实现时实例化模型架构:

tf.keras.applications.ResNet50(
    include_top=True, 
    weights='imagenet', 
    input_tensor=None, 
    input_shape=None,
    pooling=None, 
    classes=1000, 
    **kwargs
)

主要参数:

  • include_top: 是否包括顶层的全连接层。
  • weights: None 代表随机初始化, 'imagenet' 代表加载在 ImageNet 上预训练的权值。
  • input_shape: 可选,输入尺寸元组,仅当 include_top=False 时有效,否则输入形状必须是 (224, 224, 3)(channels_last 格式)或 (3, 224, 224)(channels_first 格式)。它必须为 3 个输入通道,且宽高必须不小于 32,比如 (200, 200, 3) 是一个合法的输入尺寸。

在该案例中我们使用resNet50预训练模型构建模型:

# 加载预训练模型
ResNet50 = tf.keras.applications.ResNet50(weights='imagenet', input_shape=(224,224,3))
# 设置所有层不可训练
for layer in ResNet50.layers:
    layer.trainable = False
# 设置模型
net = tf.keras.models.Sequential()
# 预训练模型
net.add(ResNet50)
# 展开
net.add(tf.keras.layers.Flatten())
# 二分类的全连接层
net.add(tf.keras.layers.Dense(2, activation='softmax'))

接下来我们使用之前定义好的ImageGenerator将训练集图片送入ResNet50进行训练。

# 模型编译:指定优化器,损失函数和评价指标
net.compile(optimizer='adam',
            loss='categorical_crossentropy',
            metrics=['accuracy'])
# 模型训练:指定数据,每一个epoch中只运行10个迭代,指定验证数据集
history = net.fit(
                    train_data_gen,
                    steps_per_epoch=10,
                    epochs=3,
                    validation_data=test_data_gen,
                    validation_steps=10
                    )
Epoch 1/3
10/10 [==============================] - 28s 3s/step - loss: 0.6931 - accuracy: 0.5031 - val_loss: 0.6930 - val_accuracy: 0.5094
Epoch 2/3
10/10 [==============================] - 29s 3s/step - loss: 0.6932 - accuracy: 0.5094 - val_loss: 0.6935 - val_accuracy: 0.4812
Epoch 3/3
10/10 [==============================] - 31s 3s/step - loss: 0.6935 - accuracy: 0.4844 - val_loss: 0.6933 - val_accuracy: 0.4875

总结

  • 微调是目标模型复制了源模型上除了输出层外的所有模型设计及其参数,并基于目标数据集微调这些参数。而目标模型的输出层需要从头训练。
  • 利用tf.keras中的application实现迁移学习

 

补充

数据类型转换维度压缩

matplotlib 的 imshow 通常期望输入为 uint8[0, 255])或浮点数([0.0, 1.0])。

.astype(np.uint8):数据类型转换

  • 作用:将图像数据从当前类型(如 float32uint16 等)强制转换为 8 位无符号整数(uint8,范围是 [0, 255]
  • 常见场景
    • 若图像数据是 浮点数(如 [0.0, 1.0] 范围),转换时需先乘以 255(如 (x * 255).astype(np.uint8))。
    • 若图像数据是 其他整数类型(如 int16,转换会截断超出 [0, 255] 的值(如 -10 变为 0300 变为 255)。
# 更安全的做法
image_uint8 = np.clip(image*255, 0, 255).astype(np.uint8).squeeze()
  • 可视化意义
    matplotlib 的 imshow 对 uint8 类型有最优支持,直接将数值映射为像素亮度(0 = 黑色,255 = 白色)。

.squeeze():维度压缩

  • 作用:移除数组中长度为 1 的维度
  • 示例
    • 若图像形状为 (1, 224, 224, 1)squeeze() 后变为 (224, 224)(移除第 0 维和第 3 维)。
    • 若图像形状为 (224, 224, 1)squeeze() 后变为 (224, 224)(移除第 2 维)。
  • 必要性
    imshow 要求输入为 2D 数组(灰度图) 或 3D 数组(RGB 图,最后一维为 3 或 4)。若图像包含冗余的单维度(如 (H, W, 1)),squeeze() 可确保正确显示。
​作用​ ​解释​ ​必要性​
​维度压缩​ 去除单维度轴 ★★★★★
​兼容显示​ 确保输入是2D(灰度)或3D(RGB) ★★★★★

实践建议​​:在显示来自Keras/TensorFlow/PyTorch等框架的图像数据时,.astype(np.uint8).squeeze() 是通用解决方案,适用于95%的显示场景。

ResNet深入理解

一、先看 “普通网络” 的问题(左图:Plain Layers)

在普通的卷积网络中(左图),我们希望通过堆叠卷积层(Conv)和激活函数(ReLU),让网络学习一个 “理想的映射 ”,也就是输入 x 经过多层变换后,输出能拟合我们期望的结果(比如分类任务中的标签)。
但随着网络加深(比如几十、上百层),会出现两个问题:
  1. 梯度消失 / 爆炸:反向传播时,梯度经过多层传递会衰减或放大,导致浅层参数难以更新。
  2. 网络退化: deeper 网络的训练误差反而比 shallow 网络更高,因为多层简单堆叠难以学习到复杂的恒等映射(比如 这种 “输入直接等于输出” 的简单关系)。

二、残差块的思路(右图:Residual Block)

为解决上述问题,残差块的核心是改变学习目标: 不再让多层网络直接学习,而是学习(即 “残差”)。

1. 数学表达式拆解

  • 原目标(普通网络):让F(x)是多层网络的实际输出)。
  • 残差块目标:让(学习残差)。
这样,最终的输出就变成:
(这里的 + 是逐元素相加(element-wise addition),要求F(x)和 x 维度相同,所以需要通过卷积或池化保证维度匹配,也就是图中的 “shortcut” 连接)

2. 为什么要学 “残差”?

  • 简化学习难度:学习比直接学习更容易。比如,当理想映射(恒等映射)时,只需让,网络通过将权重逼近 0 就能轻松实现,避免了 “多层网络难以拟合简单恒等映射” 的问题。
  • 缓解梯度消失:shortcut 连接(直接从输入连到输出)让梯度在反向传播时更顺畅,浅层网络的参数更容易更新。

三、结合图示理解结构

  • 左图(Plain Layers):输入 x 经过两层卷积(Conv)和 ReLU,直接输出 \(\mathcal{H}(x)\),学习目标是 \(\mathcal{H}(x)\)
  • 右图(Residual Block):输入 x 分为两条路:
    • 主路:经过两层卷积和 ReLU,输出(学习残差 )。
    • shortcut(捷径):输入 x 直接跳过主路,与F(x)相加,得到,最终输出

四、残差块的价值

  • 允许更深的网络:通过残差学习和 shortcut 连接,ResNet 可以轻松堆叠上百层,而不会出现 “网络退化” 问题。
  • 梯度传递更高效:shortcut 让梯度在反向传播时直接通过,缓解了梯度消失,使浅层参数也能有效更新。
  • 成为 ResNet 基础:残差块是 ResNet 的核心模块,基于它构建的深层网络在 ImageNet、目标检测等任务中取得了 SOTA 成绩,推动了深度学习的发展。

总结

残差块的本质是改变学习目标(学残差)+ shortcut 连接(简化梯度传递),让深层网络更容易训练,同时避免了 “网络退化” 问题。理解这一点,就能明白 ResNet 为何能在深度学习中占据重要地位啦~

数据生成器(Generator)

在深度学习中,数据生成器是一种迭代器,用于批量加载数据,避免一次性将所有数据加载到内存中。特别是处理大型数据集(如图像)时,这种方式非常高效。
例如,使用 ImageDataGenerator 创建的 train_data_gen 会按批次生成图像和标签:
from tensorflow.keras.preprocessing.image import ImageDataGenerator

# 创建数据生成器并设置参数(如归一化、数据增强等)
train_datagen = ImageDataGenerator(rescale=1./255, shear_range=0.2, zoom_range=0.2)

# 从目录加载数据并生成批次
train_data_gen = train_datagen.flow_from_directory(
    train_dir,
    target_size=(224, 224),
    batch_size=32,  # 每批次32张图像
    class_mode='binary'
)
 
 
 
 
 
 

Inception块

GoogLeNet中的Inception块是其核心创新,它的作用可以概括为:​​在一个卷积层中并行地、高效地提取不同尺度的特征,从而让网络能够同时捕获到细节信息(小感受野)和抽象信息(大感受野),并显著减少了计算参数总量。​

下面我们来详细拆解它的作用和设计动机:

1. 解决了什么问题?(设计动机)

在传统的卷积神经网络(如AlexNet、VGG)中,每一层通常只使用一种尺寸的卷积核(比如3x3或5x5)。这带来一个困境:

​使用大卷积核(如5x5)​​:可以获得较大的​​感受野(Receptive Field)​​,从而捕获更宏观、更抽象的特征。但缺点是​​计算成本非常高​​(参数多,计算量大)。

​使用小卷积核(如3x3或1x1)​​:计算效率高,​​感受野小​​,擅长捕获细节、局部的特征(如边缘、颜色),但难以感知更大范围的模式。

那么问题来了:​​在网络某一层,我们到底该用3x3还是5x5的卷积?哪种尺度的特征对当前任务最有用?​

Inception结构的答案是:​​小孩子才做选择,我全都要!​

2. Inception块是如何工作的?(核心思想)

Inception块的基本结构如下图所示,它在一个块内并行地使用了​​四条不同路径​​:

  1. ​1x1卷积路径​​:主要用于​​降维​​和捕捉极局部的特征(可以看作是对每个像素点的通道进行线性组合)。
  2. ​3x3卷积路径​​:捕捉中等范围的特征模式。
  3. ​5x5卷积路径​​:捕捉更大范围、更抽象的特征模式。
  4. 3x3最大池化路径​​:进行下采样,捕捉最显著的特征,保留原始特征的强度。

最后,将这四条路径输出的​​特征图(Feature Maps)在深度(Channel)维度上进行拼接(Concatenate)​​,形成一个包含了多种尺度信息的复合特征输出。

3. 关键创新:1x1卷积的“瓶颈”作用

最初的朴素想法是直接并行使用这些卷积,但计算量会巨大无比,尤其是5x5卷积。GoogLeNet的一个神来之笔是引入了 ​​1x1卷积​​ 来充当“​​瓶颈(Bottleneck)​​”层。

​作用一:降维,减少计算量​

在3x3和5x5卷积​​之前​​,先使用1x1卷积来减少输入特征图的通道数。例如,如果输入有256个通道,先用1x1卷积将其压缩到64个通道,然后再进行5x5卷积。这样,5x5卷积需要处理的通道数大大减少,计算成本急剧下降。

​计算量对比​​:(5x5卷积直接处理256通道输入)vs (1x1卷积先降到64通道 + 5x5卷积处理64通道输入),后者计算量远小于前者。

​作用二:增加非线性​

1x1卷积后通常会接一个ReLU激活函数,为网络引入了额外的非线性能力,使得模型的表达能力更强。

4. Inception块的主要作用总结

多尺度特征提取​​:​​核心作用​​。允许网络在同一层级同时、并行地处理不同感受野的特征,同时捕获细节和全局信息,极大地丰富了特征的多样性。

降低计算量和参数量​​:通过1x1卷积的“瓶颈”结构,显著减少了3x3和5x5卷积路径的计算复杂度,使得构建更深的网络成为可能,而不会导致计算爆炸。

缓解梯度消失​​:通过拓宽网络(增加每一层的宽度,即通道数),而非一味地加深,为梯度回传提供了更多路径,有助于训练非常深的网络(GoogLeNet有22层,远深于之前的VGG-16/19)。

一个生动的比喻

想象一下,你要识别一张图片中的猫。

传统CNN​​:像是一个侦探,先拿放大镜(3x3卷积)看毛发的纹理,然后再换望远镜(5x5卷积)看整体的轮廓。一次只能用一个工具,效率低且可能遗漏信息。

带Inception的CNN​​:像是一个侦探团队,​​同时工作​​。一个侦探用放大镜检查爪子(1x1路径),一个用普通镜头看脸(3x3路径),一个用广角镜头看整个身体姿态(5x5路径),还有一个负责记录最明显的特征比如胡须(池化路径)。最后,他们把所有人的发现汇总起来,立刻就能得出“这是一只猫”的准确结论。这样既全面又高效。

正是由于Inception块这种高效且强大的设计,GoogLeNet才能在2014年的ImageNet竞赛中以极高的效率击败了当时以“深度”和“大量参数”著称的VGG网络,赢得了冠军。

 

Inception 块是 GoogLeNet(也称为 Inception v1)的核心组件,其设计目标是解决卷积神经网络中如何高效提取不同尺度特征的问题,主要作用包括以下几个方面:

  1. 多尺度特征提取:Inception 块通过并行使用不同大小的卷积核(如 1×1、3×3、5×5)和池化操作(如 3×3 最大池化),能够同时捕获输入数据中不同尺度的特征。这是因为不同大小的卷积核适合检测不同大小的目标(例如,小卷积核适合细节特征,大卷积核适合全局特征)。
  2. 提升计算效率:在较大的卷积核(3×3、5×5)前加入 1×1 卷积,实现了 “降维” 操作。1×1 卷积可以减少输入通道数,从而降低后续大卷积核的计算量,在保证特征提取能力的同时提高了网络效率。
  3. 增加网络深度和宽度:通过并行分支的设计,Inception 块在不显著增加计算复杂度的前提下,能够有效扩展网络的宽度(特征通道数)和深度(网络层数),从而增强网络的表达能力。
  4. 缓解过拟合:多分支结构带来的特征多样性,以及 1×1 卷积引入的非线性激活(通常配合 ReLU),有助于网络学习更鲁棒的特征表示,一定程度上缓解了过拟合问题。

简言之,Inception 块通过巧妙的多分支设计,在高效利用计算资源的同时,实现了对不同尺度特征的有效捕获,为 GoogLeNet 在 ImageNet 等任务上的优异性能奠定了基础。
 
 
 
 
 
 
posted @ 2025-07-01 21:34  指尖下的世界  阅读(1037)  评论(0)    收藏  举报