在机器学习和深度学习的广袤领域中,图像分类一直是一个备受关注且极具挑战性的任务。今天,我们将聚焦于使用卷积神经网络(CNN)来处理美国手语数据集,深入探讨如何通过一系列步骤构建、训练一个高效的 CNN 模型,并对其性能进行评估与分析。
一、目标明确
以下是我们需要掌握的点
- 熟练掌握专门为 CNN 定制的数据准备流程,这包括数据的加载、清洗、转换以及结构化处理,确保数据能够完美契合 CNN 模型的输入要求。
- 构建一个更为复杂且功能强大的 CNN 模型架构,深入理解模型中多种类型层(如卷积层、池化层、全连接层等)的独特作用、工作原理以及它们之间的协同机制。
- 成功训练我们精心构建的 CNN 模型,并精准观察其在训练集和验证集上的性能表现,通过对训练过程和结果的细致分析,挖掘模型的优势与潜在不足,为后续的优化改进提供有力依据。
二、数据加载与预处理
-
数据读取与初步探索
- 首先,我们采用
pandas库以常规方式加载美国手语数据集的训练集和验证集的DataFrame:
- 首先,我们采用
import pandas as pd
train_df = pd.read_csv("data/asl_data/sign_mnist_train.csv")
valid_df = pd.read_csv("data/asl_data/sign_mnist_valid.csv")
- 原始的 ASL 数据呈现出展开的状态,为了更直观地感受数据的形态,我们提取训练集的前 5 行数据进行示例分析:
sample_df = train_df.head().copy() # Grab the top 5 rows
sample_df.pop('label')
sample_x = sample_df.values
此时,样本数据 sample_x 呈现出形状为 (5, 784) 的二维数组结构,这显然不符合我们对图像数据进行卷积操作的要求。
2. 维度重塑:恢复图像结构
- 为了能够顺利应用卷积操作,我们借助
reshape方法对数据集进行维度变换。考虑到我们的图像是灰度图像,仅有 1 个颜色通道,并且图像的原始尺寸为 28x28 像素,我们将数据的形状从(5, 784)转换为(5, 1, 28, 28):
import torch.nn as nn
IMG_HEIGHT = 28
IMG_WIDTH = 28
IMG_CHS = 1
sample_x = sample_x.reshape(-1, IMG_CHS, IMG_HEIGHT, IMG_WIDTH)
在这个过程中,我们巧妙地利用了 NumPy 数组的特性,对于那些我们希望保持不变的维度传递 -1,使得 reshape 操作能够自动根据其他已知维度信息计算出正确的形状。
3. 数据集类的构建与完善
- 接下来,我们着手构建一个自定义的数据集类
MyDataset,这个类将继承自torch.utils.data.Dataset,以便能够与 PyTorch 的数据加载和处理工具无缝对接:
import torch
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, base_df):
x_df = base_df.copy() # Some operations below are in-place
y_df = x_df.pop('label')
x_df = x_df.values / 255 # Normalize values from 0 to 1
x_df = x_df.reshape(-1, IMG_CHS, IMG_WIDTH, IMG_HEIGHT)
self.xs = torch.tensor(x_df).float().to(device)
self.ys = torch.tensor(y_df).to(device)
def __getitem__(self, idx):
x = self.xs[idx]
y = self.ys[idx]
return x, y
def __len__(self):
return len(self.xs)
在 MyDataset 类的 __init__ 方法中,我们首先对传入的基础 DataFrame 进行深拷贝,以避免在后续操作中对原始数据造成意外修改。然后,通过 pop 操作准确地提取出标签列,并将其存储为 y_df,而剩余的数据部分则进行归一化处理,将像素值从 0 - 255 的范围转换为 0 - 1 的区间,这有助于模型在训练过程中更快地收敛。随后,我们再次使用 reshape 方法将数据转换为适合 CNN 输入的形状 (-1, IMG_CHS, IMG_WIDTH, IMG_HEIGHT),这里的 IMG_CHS、IMG_WIDTH 和 IMG_HEIGHT 分别表示图像的通道数、宽度和高度,均已根据我们的图像数据特点进行了正确的定义。最后,我们将处理后的数据转换为 torch.tensor 类型,并将其移动到指定的计算设备(GPU 或 CPU)上,以便在后续的模型训练和推理过程中能够高效地进行计算。
4. 数据加载器的创建与配置
- 在构建好
MyDataset类之后,我们进一步创建DataLoader,它将负责批量加载数据并在训练过程中提供数据迭代功能:
from torch.utils.data import DataLoader
BATCH_SIZE = 32
train_data = MyDataset(train_df)
# 注意这里添加了 shuffle=True 参数
train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
train_N = len(train_loader.dataset)
valid_data = MyDataset(valid_df)
valid_loader = DataLoader(valid_data, batch_size=BATCH_SIZE)
valid_N = len(valid_loader.dataset)
我们设置了合适的批量大小 BATCH_SIZE = 32,并创建了训练集和验证集的 DataLoader。需要特别注意的是,在创建训练集的 DataLoader 时,我们必须添加 shuffle=True 参数,这样可以确保在每个训练 epoch 中,数据都能够以随机的顺序被加载,从而增加模型训练的随机性和泛化能力。
- 为了验证数据加载器的正确性,我们从训练集的
DataLoader中取出一个批次的数据,并仔细查看其数据格式和形状:
batch = next(iter(train_loader))
batch[0].shape
batch[1].shape
三、卷积模型架构设计:构建核心引擎
-
模型整体架构概述
- 如今,在深度学习项目的开发过程中,借鉴类似项目的成功模型配置是一种常见且高效的策略。在处理美国手语图像分类问题时,我们也采用了一个经过精心设计和验证的 CNN 模型架构。
-
各层详细解析
- Conv2D 卷积层:这是 CNN 模型的核心层之一,其主要作用是通过小型卷积核对输入图像进行滑动扫描,从而检测出对图像分类具有重要意义的特征。在模型的早期阶段,卷积层能够捕捉到图像中的简单特征,如线条、边缘等;而随着模型深度的增加,后续的卷积层则能够逐渐检测到更为复杂和抽象的特征,如形状、纹理等。以我们模型中的第一个
Conv2D层为例:
- Conv2D 卷积层:这是 CNN 模型的核心层之一,其主要作用是通过小型卷积核对输入图像进行滑动扫描,从而检测出对图像分类具有重要意义的特征。在模型的早期阶段,卷积层能够捕捉到图像中的简单特征,如线条、边缘等;而随着模型深度的增加,后续的卷积层则能够逐渐检测到更为复杂和抽象的特征,如形状、纹理等。以我们模型中的第一个
n_classes = 24
kernel_size = 3
flattened_img_size = 75 * 3 * 3
model = nn.Sequential(
# First convolution
nn.Conv2d(IMG_CHS, 25, kernel_size, stride=1, padding=1), # 25 x 28 x 28
nn.BatchNorm2d(25),
nn.ReLU(),
nn.MaxPool2d(2, stride=2), # 25 x 14 x 14
)
其中 25 表示将要学习的滤波器数量,这些滤波器将在图像上以步长 stride = 1 进行滑动,并且通过 padding = 1 的设置,确保了输出图像的大小与输入图像能够保持一致,从而避免了边缘信息的丢失。
- BatchNormalization 批量归一化层:批量归一化层在深度学习模型中扮演着重要的角色,它类似于对输入数据进行归一化处理,通过对隐藏层中的数据进行缩放,有效地改善了模型的训练过程。它能够加速模型的收敛速度,减少梯度消失或梯度爆炸的风险,并且在一定程度上提高模型的泛化能力。关于批量归一化层在模型架构中的最佳放置位置,在深度学习社区中一直存在着一些争议,有兴趣的读者可以参考相关的研究论文和技术论坛(如 Stack Overflow 上的相关讨论),以深入了解不同观点和实践经验。
- MaxPool2D 最大池化层:最大池化层的主要功能是对图像进行下采样操作,将图像的分辨率降低。这不仅有助于减少模型的计算量和参数数量,提高模型的训练和推理效率,同时还能够使模型对图像中的平移变换具有更强的鲁棒性。例如,在我们的模型中:
nn.MaxPool2d(2, stride=2), # 25 x 14 x 14
表示将图像在水平和垂直方向上均缩小为原来的一半,通过这种方式,模型能够在不损失太多关键信息的前提下,更快地聚焦于图像中的重要特征区域。
- Dropout 随机失活层:Dropout 是一种非常有效的防止过拟合的技术手段。在模型的训练过程中,Dropout 层会随机地选择一部分神经元,并将它们暂时关闭,使其在当前的前向传播或后向传播过程中不参与计算。这样做的目的是为了避免模型过度依赖某些特定的神经元或特征,从而增强模型的鲁棒性和冗余性,使得模型能够更好地泛化到未见过的数据上。例如,在我们的模型中:
nn.Dropout(.2),
表示在每次训练时,有 20% 的神经元会被随机失活。
- Flatten 展平层:Flatten 层的作用相对较为简单直接,它主要是将多维的层输出数据展平为一维数组。这个一维数组通常被称为特征向量,它将作为后续全连接层的输入,起到了连接卷积层和全连接层的桥梁作用。在我们的模型中:
nn.Flatten(),
- Linear 全连接层:全连接层在 CNN 模型中通常用于对特征向量进行分类预测。在我们的模型中,第一个全连接层(具有 512 个单元)以 Flatten 层输出的特征向量为输入,通过学习数据中的特征模式和关系,尝试找出哪些特征对特定的分类任务具有重要的贡献。而第二个全连接层(具有 24 个单元)则作为最终的分类层,其输出直接对应着我们对美国手语图像的分类预测结果。
nn.Linear(flattened_img_size, 512),
nn.Dropout(.3),
nn.ReLU(),
nn.Linear(512, n_classes)
四、模型总结与配置
- 模型编译与设备适配
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = torch.compile(model.to(device))
在完成模型架构的构建之后,我们首先将模型移动到指定的计算设备(GPU 或 CPU)上,并使用 torch.compile 对模型进行编译优化。这一步骤能够使模型在相应的设备上以更高效的方式运行,充分发挥硬件的计算能力,从而加快模型的训练和推理速度。
- 经过编译后的模型将以一种更优化的形式存在,准备好接受数据输入并进行训练和预测操作。
- 损失函数与优化器选择
import torch.optim as Adam
loss_function = nn.CrossEntropyLoss()
optimizer = Adam(model.parameters())
由于我们所处理的问题仍然是美国手语图像的分类任务,因此我们继续沿用之前在类似任务中表现良好的损失函数 nn.CrossEntropyLoss(),它能够有效地衡量模型预测结果与真实标签之间的差异,为模型的训练提供准确的梯度信息。对于模型的优化器,我们选择了 Adam 优化器,它结合了梯度的一阶矩估计和二阶矩估计,能够自适应地调整学习率,在大多数情况下都能够取得较好的训练效果。通过将模型的参数传递给 Adam 优化器,我们为模型的训练过程做好了充分的准备。
3. 准确率指标计算函数定义
def get_batch_accuracy(output, y, N):
pred = output.argmax(dim=1, keepdim=True)
correct = pred.eq(y.view_as(pred)).sum().item()
return correct / N
为了能够准确地评估模型在训练和验证过程中的性能表现,我们定义了一个专门的函数 get_batch_accuracy 来计算批次数据的准确率。这个函数首先通过 argmax 操作获取模型预测结果中概率最大的类别索引,然后将其与真实标签进行比较,计算出正确预测的数量,并最终得出准确率指标。通过在训练和验证过程中定期调用这个函数,我们能够实时监控模型的准确率变化情况,及时发现模型可能存在的问题并进行调整。
五、模型训练与性能评估
-
训练与验证函数定义与实现
- 尽管我们此次构建的 CNN 模型架构与之前的简单模型有很大的不同,但模型训练的基本过程在本质上仍然是相似的。我们定义了
train和validate两个关键函数来分别完成模型的训练和验证操作:
- 尽管我们此次构建的 CNN 模型架构与之前的简单模型有很大的不同,但模型训练的基本过程在本质上仍然是相似的。我们定义了
def validate():
loss = 0
accuracy = 0
model.eval()
with torch.no_grad():
for x, y in valid_loader:
output = model(x)
loss += loss_function(output, y).item()
accuracy += get_batch_accuracy(output, y, valid_N)
print('Valid - Loss: {:.4f} Accuracy: {:.4f}'.format(loss, accuracy))
def train():
loss = 0
accuracy = 0
model.train()
for x, y in train_loader:
output = model(x)
optimizer.zero_grad()
batch_loss = loss_function(output, y)
batch_loss.backward()
optimizer.step()
loss += batch_loss.item()
accuracy += get_batch_accuracy(output, y, train_N)
print('Train - Loss: {:.4f} Accuracy: {:.4f}'.format(loss, accuracy))
在 train 函数中,首先将模型设置为训练模式(model.train()),然后在训练数据加载器的迭代过程中,对于每一批次的数据,依次进行前向传播、损失计算、梯度清零、反向传播和参数更新等操作。在这个过程中,我们不断累积批次损失和准确率信息,以便在每个训练 epoch 结束后能够准确地输出模型的训练损失和准确率指标。而在 validate 函数中,我们将模型设置为评估模式(model.eval()),并在验证数据加载器的迭代过程中,同样进行前向传播和损失计算操作,但由于在验证过程中不需要进行梯度更新,因此我们使用 torch.no_grad() 上下文管理器来禁用梯度计算,以减少不必要的计算开销。最后,我们输出模型在验证集上的损失和准确率指标,以便评估模型的泛化能力。
2. 训练循环与结果分析
epochs = 20
for epoch in range(epochs):
print('Epoch: {}'.format(epoch))
train()
validate()
我们设置了训练的轮数 epochs = 20,然后在一个循环中依次调用 train 和 validate 函数,对模型进行多轮的训练和验证。在训练过程中,我们可以观察到训练准确率随着训练轮数的增加而逐渐提高,这表明模型在不断地学习数据中的特征和模式,并且能够很好地拟合训练数据。同时,我们也注意到验证准确率也有了明显的提升,这说明我们构建的 CNN 模型相较于之前的简单模型,在泛化能力上有了很大的进步。然而,我们也发现验证准确率存在一定程度的波动,这暗示着我们的模型仍然有进一步优化的空间,例如可以通过调整模型架构、超参数设置或者采用数据增强等技术手段来提高模型的稳定性和泛化性能。
六、总结与展望:回顾与前行
在本次关于使用卷积神经网络处理美国手语数据集的实践之旅中,我们通过一系列系统而严谨的步骤,成功地构建、训练并评估了一个 CNN 模型。我们深入了解了数据加载与预处理的重要性和方法,精心设计了复杂而高效的 CNN 模型架构,熟练掌握了模型训练与验证的流程和技巧,并且对模型的性能表现进行了详细的分析和讨论。 希望通过本次的分享,能够为广大深度学习爱好者和从业者在处理图像分类任务时提供一些有益的思路和参考,让我们一起在深度学习的海洋中不断探索前行