卷积神经网络(CNN)解读,Python菜鸟也能看懂!

卷积神经网络(CNN)已彻底改变了计算机视觉领域,成为图像和视频分析应用的基石。

本文将深入探讨使CNN功能强大的关键组件和操作,包括卷积、最大池化、步长、填充、上采样、下采样等概念。

此外,我们还将使用Python带大家从头构建一个简单CNN模型在数据集上的应用。

卷积神经网络(CNN)的构成:

CNN由各种类型的层组成,这些层协同工作,从输入数据中学习层次化表示。

每一层在整体架构中都扮演着独特的角色,让我们探索典型CNN中的关键层类型:

输入层:输入层是网络的初始数据入口点,在基于图像的任务中,输入层表示图像的像素值。以下示例中,我们假设处理的是大小为28x28像素的灰度图像。

# 导入TensorFlow中的Input层from tensorflow.keras.layers import Input
# 定义输入层,shape参数指定输入数据的形状# shape=(28, 28, 1) 表示输入是一个28x28的灰度图像,1表示单通道(灰度图)input_layer = Input(shape=(28, 28, 1))

卷积层:卷积层是CNN的核心构建块,这些层使用可学习的滤波器对输入数据执行卷积操作。这些滤波器扫描输入,提取边缘、纹理和模式等特征。

# 导入TensorFlow中的Conv2D层from tensorflow.keras.layers import Conv2D
# 定义卷积层# filters=32:表示使用32个卷积核(滤波器),每个卷积核会提取输入数据中的不同特征# kernel_size=(3, 3):表示每个卷积核的大小为3x3# activation='relu':指定激活函数为ReLU(Rectified Linear Unit),用于引入非线性# input_layer:表示该卷积层的输入是上一层的输出(即输入层)conv_layer = Conv2D(filters=32, kernel_size=(3, 3), activation='relu')(input_layer)

在CNN中,“核”和“滤波器”这两个术语经常互换使用,指的是同一概念。让我们分解这些术语的含义:

核:核是卷积操作中使用的小矩阵。它是一组可学习的权重,应用于输入数据以产生输出特征图。核是使CNN能够自动学习输入数据中特征空间层次的关键元素。在图像处理中,核可能是一个像3x3或5x5这样的小矩阵。

滤波器:滤波器则是多个核的集合。在大多数情况下,卷积层使用多个滤波器来捕获输入数据中的不同特征。每个滤波器与输入进行卷积以产生特征图,网络通过训练期间调整这些滤波器的权重(参数)来学习提取各种模式。

在本例中,我们定义了一个具有32个滤波器的卷积层,每个滤波器具有3x3的核大小。

在训练期间,神经网络会调整这32个滤波器的权重(参数),以从输入数据中学习不同的特征。让我们通过图像示例来看一看:

图片

综上所述,核是在输入数据上滑动或卷积的小矩阵,而滤波器是用于从输入中提取各种特征的这些核的集合,从而使神经网络能够学习层次化表示。

激活层(ReLU):卷积操作后,通常会逐元素应用激活函数,如修正线性单元(ReLU),以引入模型的非线性。

ReLU有助于网络学习复杂关系,并使模型更具表达力。

具体使用哪种激活函数取决于您的用例,大多数情况下研究人员使用ReLU,但也可以使用其他激活函数,例如Leaky ReLU、ELU。

图片

在Python中实现ReLU函数非常简单。ReLU是神经网络中常用的激活函数,用于引入非线性。以下是一个简单的Python实现:

# 定义ReLU(Rectified Linear Unit)激活函数def relu(x):    """    ReLU激活函数的实现。    参数:        x (float): 输入值,可以是单个数值或数组中的元素。    返回值:        float: 经过ReLU激活函数处理后的值。    """    return max(0, x)  # 如果x大于0,返回x;否则返回0

池化层:池化层(如最大池化或平均池化)减少由卷积层生成的特征图的空间维度。例如,最大池化从一组值中选择最大值,关注最显著的特征。

图片

池化层减少空间维度,最大池化通常用于此目的。

# 导入TensorFlow中的MaxPooling2D层from tensorflow.keras.layers import MaxPooling2D
# 定义最大池化层# pool_size=(2, 2):表示池化窗口的大小为2x2# conv_layer:表示该池化层的输入是上一层的输出(即卷积层的输出)pooling_layer = MaxPooling2D(pool_size=(2, 2))(conv_layer)

全连接(密集)层:全连接层将一层的每个神经元连接到下一层的每个神经元。

这些层通常位于网络的末尾,将学习到的特征转换为预测或类别概率。

全连接层通常用于网络的末尾。对于分类任务:

# 导入TensorFlow中的Dense(全连接层)和Flatten(展平层)from tensorflow.keras.layers import Dense, Flatten
# 定义展平层# Flatten():将多维输入数据展平为一维向量# pooling_layer:表示该展平层的输入是上一层的输出(即池化层的输出)flatten_layer = Flatten()(pooling_layer)
# 定义全连接层(Dense层)# units=128:表示该层有128个神经元# activation='relu':指定激活函数为ReLU(Rectified Linear Unit)# flatten_layer:表示该全连接层的输入是展平层的输出dense_layer = Dense(units=128, activation='relu')(flatten_layer)

Dropout层:Dropout层用于正则化以防止过拟合。

在训练期间,随机丢弃一些神经元(即忽略它们),迫使网络学习更鲁棒和泛化的特征。

它通过训练期间随机忽略一部分输入单元来帮助防止过拟合。

图片

# 导入TensorFlow中的Dropout层from tensorflow.keras.layers import Dropout
# 定义Dropout层# rate=0.5:表示在每次训练时随机丢弃50%的神经元# dense_layer:表示该Dropout层的输入是上一层的输出(即全连接层的输出)dropout_layer = Dropout(rate=0.5)(dense_layer)

批量归一化层:批量归一化(BN)是神经网络中用于稳定和加速训练过程的技术。

它通过训练期间调整和缩放层的输入来归一化它们,批量归一化的数学细节涉及归一化、缩放和移位操作,让我们深入了解批量归一化的数学原理。

假设我们有一个大小为m、具有n个特征的小批量。批量归一化的输入可以概括如下:

均值计算:计算每个特征的小批量的均值μ。

图片

在这里,xi表示小批量数据中第i个特征的值。

方差计算:计算每个特征在小批量数据上的方差σ²。

图片

归一化:通过减去均值并除以标准差(σ)对输入进行归一化处理。

图片

这里,为了避免除以零的情况,加入了一个很小的常数ϵ。

缩放和平移:引入可学习的参数(γ和β)对归一化后的值进行缩放和平移。

图片

其中,γ是缩放参数,β是平移参数。

批量归一化操作通常被插入到神经网络层的激活函数之前,研究表明,它具有正则化效果,并能缓解内部协变量偏移等问题,使训练过程更加稳定和快速。

以下是批量归一化在卷积神经网络或任何深度神经网络中的简单代码示例。

# 导入TensorFlow中的BatchNormalization层from tensorflow.keras.layers import BatchNormalization
# 定义批量归一化层(Batch Normalization Layer)# BatchNormalization():对输入数据进行归一化处理,使其均值为0,方差为1# dropout_layer:表示该批量归一化层的输入是上一层的输出(即Dropout层的输出)batch_norm_layer = BatchNormalization()(dropout_layer)

总结来说,批量归一化首先对输入进行归一化处理,然后对归一化后的值进行缩放和平移,并引入可学习的参数,使网络在训练过程中能够进行自适应调整,批量归一化的使用已成为深度学习架构中的标准做法。

展平层:展平层将多维特征图转换为一维向量,为输入到全连接层做准备。

# 定义展平层(Flatten Layer)# Flatten():将多维输入数据展平为一维向量# batch_norm_layer:表示该展平层的输入是上一层的输出(即批量归一化层的输出)flatten_layer = Flatten()(batch_norm_layer)

上采样层:上采样是深度学习中用于增加特征图空间分辨率的技术。

它常用于图像分割和生成等任务。以下是常见上采样方法的简要描述:

最近邻上采样:最近邻上采样(也称为复制或复制上采样)是一种简单直观的方法。

该方法通过复制输入中的每个像素来生成更大的输出,虽然方法简单,但最近邻上采样可能会导致块状伪影和细节丢失,因为它不会在相邻像素之间进行插值。

图片

转置卷积上采样:转置卷积(通常称为反卷积)是一种可学习的上采样方法。

它通过使用具有可学习参数的卷积操作来增加输入的空间维度。转置卷积层的权重在优化过程中进行训练,使网络能够学习特定于任务的上采样模式。

图片

# 导入TensorFlow库import tensorflow as tf# 导入TensorFlow中的Conv2DTranspose层(转置卷积层)from tensorflow.keras.layers import Conv2DTranspose
# 定义转置卷积层(Transposed Convolution Layer)# filters=32:表示使用32个卷积核(滤波器),每个卷积核会生成输出特征图的一个通道# kernel_size=(3, 3):表示每个卷积核的大小为3x3# strides=(2, 2):表示卷积核在输入数据上滑动的步长为2x2,用于上采样(扩大特征图尺寸)# padding='same':表示在输入数据的边缘填充0,使得输出的特征图尺寸与输入尺寸成比例transposed_conv_upsampling = Conv2DTranspose(filters=32, kernel_size=(3, 3), strides=(2, 2), padding='same')

每种上采样方法都有其优缺点,选择取决于任务的具体要求和数据的特性。

填充和步幅

在卷积神经网络(CNN)中,填充和步幅是影响卷积操作后输出特征图大小的关键因素。下面将讨论三种类型的填充,并解释步幅的概念。

有效填充(无填充):在有效填充(也称为无填充)中,在应用卷积操作之前,不对输入添加额外的填充。

因此,卷积操作仅在滤波器和输入完全重叠的地方进行。这通常会导致输出特征图的空间维度减小。

图片

# 导入TensorFlow中的Conv2D层(二维卷积层)from tensorflow.keras.layers import Conv2D
# 定义使用有效填充(Valid Padding)的卷积层# filters=32:表示使用32个卷积核(滤波器),每个卷积核会生成输出特征图的一个通道# kernel_size=(3, 3):表示每个卷积核的大小为3x3# strides=(1, 1):表示卷积核在输入数据上滑动的步长为1x1# padding='valid':表示不使用填充(即有效填充),输出的特征图尺寸会减小valid_padding_conv = Conv2D(filters=32, kernel_size=(3, 3), strides=(1, 1), padding='valid')

相同填充:相同填充确保输出特征图具有与输入相同的空间维度。它通过在输入周围添加零填充来实现,以便滤波器可以在输入上滑动而不会超出其边界。填充量是根据保持维度相同而计算的。

图片

# 导入TensorFlow中的Conv2D层(二维卷积层)from tensorflow.keras.layers import Conv2D
# 定义使用相同填充(Same Padding)的卷积层# filters=32:表示使用32个卷积核(滤波器),每个卷积核会生成输出特征图的一个通道# kernel_size=(3, 3):表示每个卷积核的大小为3x3# strides=(1, 1):表示卷积核在输入数据上滑动的步长为1x1# padding='same':表示使用相同填充,输出的特征图尺寸与输入尺寸相同same_padding_conv = Conv2D(filters=32, kernel_size=(3, 3), strides=(1, 1), padding='same')

步幅:步幅定义了卷积过程中滤波器在输入上移动的步长,较大的步幅会导致输出特征图的空间维度减小,步幅可以调整以控制网络中的下采样程度。

# 导入TensorFlow中的Conv2D层(二维卷积层)from tensorflow.keras.layers import Conv2D
# 定义带步长(Stride)的卷积层# filters=32:表示使用32个卷积核(滤波器),每个卷积核会生成输出特征图的一个通道# kernel_size=(3, 3):表示每个卷积核的大小为3x3# strides=(2, 2):表示卷积核在输入数据上滑动的步长为2x2# padding='same':表示使用相同填充,输出的特征图尺寸与输入尺寸成比例conv_with_stride = Conv2D(filters=32, kernel_size=(3, 3), strides=(2, 2), padding='same')

在本例中,步幅设置为(2, 2),表示滤波器在水平和垂直方向上每次移动两个像素,步幅是控制特征图空间分辨率和影响网络感受野的关键参数。

在本文中,我们将从头开始构建一个简单的卷积神经网络——最流行的计算机视觉任务猫狗分类。

这个任务包括以下几个步骤:

导入库

import tensorflow_datasets as tfdsimport tensorflow as tffrom tensorflow.keras import layers
import kerasfrom keras.models import Sequential,Modelfrom keras.layers import Dense,Conv2D,Flatten,MaxPooling2D,GlobalAveragePooling2Dfrom keras.utils import plot_model
import numpy as npimport matplotlib.pyplot as pltimport scipy as spimport cv2

加载数据:猫狗数据集

!curl -O https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip!unzip -q kagglecatsanddogs_5340.zip!ls

下面的单元格将对图像进行预处理并创建批次,然后将其输入到我们的模型中。

# 定义图像增强函数def augment_images(image, label):    """    对输入图像进行预处理和增强。    参数:        image: 输入的图像张量。        label: 对应的标签。    返回值:        image: 预处理后的图像张量。        label: 对应的标签。    """    # 将图像转换为float类型    image = tf.cast(image, tf.float32)    # 将像素值归一化到[0, 1]范围    image = (image / 255)    # 将图像大小调整为300x300    image = tf.image.resize(image, (300, 300))
    return image, label

# 使用上述工具函数对训练数据进行预处理# train_data: 原始训练数据集# map函数将augment_images应用于数据集中的每个元素augmented_training_data = train_data.map(augment_images)
# 在训练前对数据进行打乱并分批次# shuffle(1024): 打乱数据,缓冲区大小为1024# batch(32): 将数据分为每批次32个样本train_batches = augmented_training_data.shuffle(1024).batch(32)

过滤损坏的图像

在处理大量真实世界图像数据时,损坏的图像是常见现象,让我们过滤掉头部不包含“JFIF”字符串的编码不良图像。

# 导入操作系统模块import os
# 初始化计数器,用于记录被删除的损坏图像数量num_skipped = 0
# 遍历"Cat"和"Dog"两个文件夹for folder_name in ("Cat", "Dog"):    # 构建文件夹路径    folder_path = os.path.join("PetImages", folder_name)        # 遍历文件夹中的每个文件    for fname in os.listdir(folder_path):        # 构建文件的完整路径        fpath = os.path.join(folder_path, fname)                try:            # 以二进制模式打开文件            fobj = open(fpath, "rb")            # 检查文件头部是否包含"JFIF"标识(JPEG文件的标识)            is_jfif = tf.compat.as_bytes("JFIF") in fobj.peek(10)        finally:            # 确保文件对象被关闭            fobj.close()
        # 如果文件不是JPEG格式(即损坏的图像)        if not is_jfif:            # 增加计数器            num_skipped += 1            # 删除损坏的图像文件            os.remove(fpath)
# 打印被删除的损坏图像数量print("Deleted %d images" % num_skipped)

生成数据集

# 定义图像的目标尺寸image_size = (300, 300)  # 图像将被调整为300x300像素# 定义每个批次的样本数量batch_size = 128  # 每批次包含128张图像
# 使用Keras的image_dataset_from_directory函数从目录加载图像数据集# 参数说明:# - "PetImages": 数据集所在的目录路径# - validation_split=0.2: 将数据集的20%作为验证集,80%作为训练集# - subset="both": 同时返回训练集和验证集# - seed=1337: 随机种子,确保每次运行时数据集的划分一致# - image_size=image_size: 将图像调整为指定的尺寸(300x300)# - batch_size=batch_size: 每批次包含128张图像train_ds, val_ds = tf.keras.utils.image_dataset_from_directory(    "PetImages",    validation_split=0.2,    subset="both",    seed=1337,    image_size=image_size,    batch_size=batch_size,)

可视化数据

以下是训练数据集中的前9张图像。如你所见,标签1表示“狗”,标签0表示“猫”。

# 导入matplotlib库,用于可视化图像import matplotlib.pyplot as plt
# 创建一个6x6英寸的画布plt.figure(figsize=(6, 6))
# 从训练数据集中取一个批次(包含128张图像)for images, labels in train_ds.take(1):    # 遍历前9张图像    for i in range(9):        # 创建3x3的子图,并定位到第i+1个子图        ax = plt.subplot(3, 3, i + 1)        # 显示图像        # images[i].numpy():将TensorFlow张量转换为NumPy数组        # .astype("uint8"):将像素值转换为8位无符号整数(0-255)        plt.imshow(images[i].numpy().astype("uint8"))        # 设置子图标题为图像的标签(0表示"Cat",1表示"Dog"        plt.title(int(labels[i]))        # 关闭坐标轴显示        plt.axis("off")
# 显示画布plt.show()

图片

使用图像数据增强

当你没有大型图像数据集时,通过对训练图像应用随机且真实的变换(如随机水平翻转或小幅度随机旋转)来人为引入样本多样性是一种很好的做法,这有助于模型暴露于训练数据的不同方面,同时减缓过拟合。

# 导入Keras库from tensorflow import keras# 导入Keras中的层模块from tensorflow.keras import layers
# 定义数据增强模块# 使用Keras的Sequential模型将多个数据增强操作组合在一起data_augmentation = keras.Sequential(    [        # 随机水平翻转图像        layers.RandomFlip("horizontal"),        # 随机旋转图像,旋转角度范围为-10%到10%(即-36度到36度)        layers.RandomRotation(0.1),    ])

让我们通过反复对数据集中的第一张图像应用数据增强来可视化增强后的样本。

# 创建一个6x6英寸的画布plt.figure(figsize=(6, 6))
# 从训练数据集中取一个批次(包含128张图像)for images, _ in train_ds.take(1):    # 遍历前9张图像    for i in range(9):        # 对图像进行数据增强        augmented_images = data_augmentation(images)        # 创建3x3的子图,并定位到第i+1个子图        ax = plt.subplot(3, 3, i + 1)        # 显示增强后的图像        # augmented_images[0].numpy():将TensorFlow张量转换为NumPy数组        # .astype("uint8"):将像素值转换为8位无符号整数(0-255)        plt.imshow(augmented_images[0].numpy().astype("uint8"))        # 关闭坐标轴显示        plt.axis("off")
# 显示画布plt.show()

图片

为性能配置数据集

让我们对训练数据集应用数据增强,并确保使用缓冲预取,以便我们可以从磁盘获取数据而不会使I/O成为阻塞因素。

# 对训练数据集应用数据增强# 使用map函数将data_augmentation应用于每个图像train_ds = train_ds.map(    # lambda函数:对每个图像和标签对进行处理    # img: 输入图像    # label: 对应的标签    lambda img, label: (data_augmentation(img), label),    # num_parallel_calls=tf.data.AUTOTUNE: 自动并行化数据增强操作,以优化性能    num_parallel_calls=tf.data.AUTOTUNE,)
# 预取数据到GPU内存中,以最大化GPU利用率# prefetch(tf.data.AUTOTUNE): 自动调整预取缓冲区大小,优化数据加载性能train_ds = train_ds.prefetch(tf.data.AUTOTUNE)
# 对验证数据集也进行预取操作val_ds = val_ds.prefetch(tf.data.AUTOTUNE)

构建分类器

这对你来说可能很熟悉,因为它与我们之前构建的模型几乎相同。

关键区别在于输出只有一个单元,并且使用了sigmoid激活函数,这是因为我们只处理两个类别。

# 导入Keras中的Sequential模型和所需的层from tensorflow.keras.models import Sequentialfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D, Dense
# 定义自定义模型类,继承自Sequential模型class CustomModel(Sequential):    def __init__(self):        # 调用父类的初始化方法        super(CustomModel, self).__init__()
        # 添加第一层卷积层        # Conv2D: 二维卷积层        # 16: 卷积核的数量        # input_shape=(300, 300, 3): 输入图像的形状为300x300,3个通道(RGB)        # kernel_size=(3, 3): 卷积核的大小为3x3        # activation='relu': 使用ReLU激活函数        # padding='same': 使用相同填充,保持输出特征图的尺寸与输入相同        self.add(Conv2D(16, input_shape=(300, 300, 3), kernel_size=(3, 3), activation='relu', padding='same'))        # 添加最大池化层        # MaxPooling2D: 二维最大池化层        # pool_size=(2, 2): 池化窗口的大小为2x2        self.add(MaxPooling2D(pool_size=(2, 2)))
        # 添加第二层卷积层        # 32: 卷积核的数量        self.add(Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same'))        # 添加最大池化层        self.add(MaxPooling2D(pool_size=(2, 2)))
        # 添加第三层卷积层        # 64: 卷积核的数量        self.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))        # 添加最大池化层        self.add(MaxPooling2D(pool_size=(2, 2)))
        # 添加第四层卷积层        # 128: 卷积核的数量        self.add(Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same'))        # 添加全局平均池化层        # GlobalAveragePooling2D: 对每个特征图取平均值,输出一个一维向量        self.add(GlobalAveragePooling2D())        # 添加全连接层        # Dense: 全连接层        # 1: 输出维度为1(适用于二分类任务)        # activation='sigmoid': 使用Sigmoid激活函数,输出概率值        self.add(Dense(1, activation='sigmoid'))

# 实例化自定义模型model = CustomModel()
# 显示模型的结构摘要model.summary()

损失函数可以调整为仅处理两个类别,为此,我们选择二元交叉熵。

# 编译模型# loss='binary_crossentropy': 使用二元交叉熵作为损失函数,适用于二分类任务# metrics=['accuracy']: 在训练过程中监控准确率# optimizer=tf.keras.optimizers.RMSprop(lr=0.001): 使用RMSprop优化器,学习率为0.001model.compile(loss='binary_crossentropy',              metrics=['accuracy'],              optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.001))
# 训练模型# train_ds: 训练数据集# epochs=25: 训练25个周期# validation_data=val_ds: 使用验证数据集评估模型性能model.fit(train_ds,          epochs=25,          validation_data=val_ds)

测试模型

让我们下载一些图像,看看类激活图的样子。

!wget -O cat1.jpg https://storage.googleapis.com/laurencemoroney-blog.appspot.com/MLColabImages/cat1.jpg!wget -O cat2.jpg https://storage.googleapis.com/laurencemoroney-blog.appspot.com/MLColabImages/cat2.jpg!wget -O catanddog.jpg https://storage.googleapis.com/laurencemoroney-blog.appspot.com/MLColabImages/catanddog.jpg!wget -O dog1.jpg https://storage.googleapis.com/laurencemoroney-blog.appspot.com/MLColabImages/dog1.jpg!wget -O dog2.jpg https://storage.googleapis.com/laurencemoroney-blog.appspot.com/MLColabImages/dog2.jpg
# 导入OpenCV库import cv2# 导入NumPy库import numpy as np
# 定义工具函数,用于预处理图像并显示类激活图(CAM)def convert_and_classify(image):    """    加载图像、预处理图像、进行分类并生成类激活图(CAM)。    参数:        image: 图像文件的路径。    """    # 加载图像    img = cv2.imread(image)
    # 预处理图像    # 将图像大小调整为300x300像素,并将像素值归一化到[0, 1]范围    img = cv2.resize(img, (300, 300)) / 255.0
    # 添加批次维度,因为模型期望输入具有批次维度    tensor_image = np.expand_dims(img, axis=0)
    # 使用CAM模型获取特征和预测结果    features, results = cam_model.predict(tensor_image)
    # 生成并显示类激活图(CAM)    show_cam(tensor_image, features, results)

# 对多张图像进行分类并显示CAMconvert_and_classify('cat1.jpg')  # 对cat1.jpg进行分类convert_and_classify('cat2.jpg')  # 对cat2.jpg进行分类convert_and_classify('catanddog.jpg')  # 对catanddog.jpg进行分类convert_and_classify('dog1.jpg')  # 对dog1.jpg进行分类convert_and_classify('dog2.jpg')  # 对dog2.jpg进行分类

输出

图片

相信到这里大家都应该清楚什么是卷积神经网络(cnn)了。

来源:人工智能学习指南

THE END