一、引言
在深度学习领域,图像分类是一个重要的研究方向。本文将聚焦于美国手语数据集的图像分类任务,详细阐述从数据准备到模型创建、训练以及结果分析的全过程。通过这一实例,读者将深入理解如何运用相关技术和工具处理实际的图像分类问题,并为进一步探索更复杂的模型奠定坚实基础。
二、手语数据集探究
(一)数据集来源与特点
本次实验所采用的美国手语数据集主要聚焦于美国手语字母表图像,其中原本包含 26 个字母。然而,由于 “j” 和 “z” 这两个字母涉及特定动作,其数据采集和处理相对复杂,因此在本次训练数据集中予以排除。该数据集来源于 Kaggle 网站,这是一个在深度学习领域极具价值的资源宝库。Kaggle 不仅提供了丰富多样的数据集,还涵盖了大量的深度学习资源,例如许多数据贡献者会发布类似于本文所涉及的 “内核”,这些 “内核” 详细展示了如何对特定数据集进行模型训练以及数据探索的具体步骤和方法。此外,Kaggle 还会在其组织的各类竞赛中附加相关数据集,这为深度学习爱好者和从业者提供了一个绝佳的平台,大家可以在竞赛中相互切磋,在训练高精度模型方面一较高下,同时也能够从其他参赛者的经验和方法中汲取灵感,不断提升自己的技术水平。
(二)数据加载与初步处理
1. 数据读取
手语数据集采用了常见的 CSV(Comma Separated Values)格式,这种格式在数据处理领域应用广泛,与我们熟知的 Microsoft Excel 和 Google Sheet 所采用的格式相同。它以行和列组成的网格形式呈现数据,并在顶部附带标签,以便清晰地标识每一列数据的含义。在 Python 中,我们借助强大的 Pandas 库来加载和处理这些数据。Pandas 库提供了read_csv方法,通过该方法,我们能够轻松地将 CSV 文件读取并转换为名为DataFrame的格式,这是 Pandas 库用于存储数据网格的特定格式,它为后续的数据处理和分析提供了极大的便利。以下是数据读取的示例代码:
train_df = pd.read_csv("data/asl_data/sign_mnist_train.csv")
valid_df = pd.read_csv("data/asl_data/sign_mnist_valid.csv")
2. 数据探索
在成功读取数据后,我们需要对数据进行深入的探索,以了解其内在结构和特征。Pandas 库的head函数为我们提供了便捷的方式来查看数据集的前几行数据。通过观察可以发现,数据集中的每一行都代表着一个图像,其中包含一个label列用于标识图像对应的类别,另外还有 784 个值,这些值分别代表了该图像中每个像素的取值,这与我们之前接触过的 MNIST 数据集在数据结构上具有相似性。需要注意的是,此时的标签是以数值形式呈现的,并非我们直观理解的字母形式。通过执行train_df.head()语句,我们可以清晰地看到数据的具体样式和内容。
3. 标签与图像提取
与处理 MNIST 数据集类似,我们需要将训练和测试标签分别存储在y_train和y_valid变量中,同时将训练和测试图像存储在x_train和x_valid变量中。在 Python 中,我们可以利用DataFrame的pop方法来实现标签列的提取,并将其赋值给相应的变量,例如:
y_train = train_df.pop('label')
y_valid = valid_df.pop('label')
而对于图像数据的提取,我们直接将DataFrame的值赋给对应的变量,代码如下:
x_train = train_df.values
x_valid = valid_df.values
4. 数据总结
经过上述一系列操作,我们得到了用于训练和验证的图像数据及其对应的标签。具体而言,我们拥有 27,455 张 784 像素的图像用于训练,通过x_train.shape可以查看其形状信息;与之对应的训练标签数量也为 27,455 个,通过y_train.shape可获取。在验证集方面,我们有 7,172 张图像用于验证,其形状信息可通过x_valid.shape查看,相应的验证标签数量同样为 7,172 个,通过y_valid.shape可知。这些数据的总结信息对于我们后续评估模型的训练效果和泛化能力具有重要的参考价值。
三、数据可视化与预处理
(一)数据可视化
为了更直观地感受数据集中图像的特征和样式,我们采用matplotlib库进行数据可视化操作。由于原始数据的形状为 784 像素的 1D 形状,这种形状不利于直接呈现图像,因此我们需要将其重构为 28x28 像素的 2D 形状。以下是数据可视化的示例代码:
import matplotlib.pyplot as plt
plt.figure(figsize=(40, 40))
num_images = 20
for i in range(num_images):
row = x_train[i]
label = y_train[i]
image = row.reshape(28, 28)
plt.subplot(1, num_images, i + 1)
plt.title(label, fontdict={'fontsize': 30})
plt.axis('off')
plt.imshow(image, cmap='gray')
通过上述代码,我们可以绘制出数据集中部分图像的可视化结果,从而对数据有更直观的认识和理解。
(二)数据归一化
在图像处理中,数据归一化是一个重要的预处理步骤。与处理 MNIST 数据集时相同,我们需要将图像数据的像素值从原本的 0 到 255 范围转换为 0 到 1 之间的浮点值。这样做的目的在于使数据在不同的特征维度上具有相似的尺度,有助于提高模型的训练效果和收敛速度。以下是数据归一化的示例代码:
# x_train = x_train / 255 is also correct
x_train = train_df.values / 255
x_valid = valid_df.values / 255
(三)定制数据集
为了能够更好地利用 PyTorch 框架进行模型训练,我们需要创建自定义的数据集类。在 PyTorch 中,我们可以通过继承Dataset类来实现自定义数据集。在自定义数据集类中,__init__方法会在类初始化时运行一次,主要用于初始化数据集的相关属性,例如将数据转换为 PyTorch 张量并将其移动到指定的设备(如 GPU)上,以加快数据处理速度。__getitem__方法则用于返回数据集中的单个图像和对应的标签,而__len__方法用于返回数据集的总长度。以下是自定义数据集类的示例代码:
class MyDataset(Dataset):
def __init__(self, x_df, y_df):
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)
创建自定义数据集后,我们还需要使用DataLoader类来加载数据,以便在模型训练过程中能够以批次的形式获取数据。以下是DataLoader的使用示例:
BATCH_SIZE = 32
train_data = MyDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
train_N = len(train_loader.dataset)
valid_data = MyDataset(x_valid, y_valid)
valid_loader = DataLoader(valid_data, batch_size=BATCH_SIZE)
valid_N = len(valid_loader.dataset)
我们可以通过以下代码验证DataLoader是否按预期工作:
train_loader
batch = next(iter(train_loader))
batch
batch[0].shape
batch[1].shape
通过多次运行上述代码,我们可以观察到每次获取的批次数据的形状和内容都是不同的,这表明DataLoader能够正确地对数据进行批次划分和随机打乱操作。
四、模型构建
(一)模型架构设计
在完成数据处理和加载后,我们进入模型构建阶段。本次实验构建一个Sequential模型,该模型包含多个层,具体如下:
- 首先是一个展平层,用于将输入的图像数据从二维形状(28x28)展平为一维向量,以便后续全连接层能够处理。
- 接着是一个输入层,该层为全连接层,包含 512 个神经元,并使用 ReLU 激活函数。ReLU 激活函数能够为模型引入非线性因素,增强模型的表达能力。
- 然后是第二个全连接层,同样包含 512 个神经元,并使用 ReLU 激活函数。
- 最后是输出层,其神经元数量等于类别数,在本案例中为 26,用于输出图像属于每个类别的预测概率。 在构建模型之前,我们先定义一些必要的变量:
input_size = 28 * 28
n_classes = 26
以下是构建模型的示例代码:
model = nn.Sequential(
nn.Flatten(),
nn.Linear(input_size, 512), # Input
nn.ReLU(), # Activation for input
nn.Linear(512, 512), # Hidden
nn.ReLU(), # Activation for hidden
nn.Linear(512, n_classes) # Output
)
(二)模型编译与优化
在构建模型后,我们需要对模型进行编译并设置优化器。本次实验中,我们将模型移动到 GPU 上进行加速处理,并使用torch.compile函数对模型进行编译,以提高模型的执行效率。同时,由于本次任务与对 MNIST 手写数字进行分类相似,我们采用相同的损失函数(Categorical CrossEntropy)和优化器(Adam)。以下是模型编译和优化的示例代码:
model = torch.compile(model.to(device))
loss_function = nn.CrossEntropyLoss()
optimizer = Adam(model.parameters())
五、模型训练
(一)训练函数
模型训练是整个实验的核心环节之一。在训练函数中,我们首先将模型设置为model.train模式,这是为了确保模型的参数在训练过程中能够被正确更新。在循环遍历DataLoader获取的每个批次数据时,我们按照以下步骤进行操作:
- 从模型中获取输出预测,即模型对当前批次数据的预测结果。
- 使用优化器的
zero_grad函数将梯度置零,这是为了避免上一批次的梯度对当前批次的影响。 - 使用我们定义的损失函数计算预测结果与真实标签之间的损失值。
- 使用
backward函数计算梯度,这一步骤根据损失值计算模型参数的梯度,为后续的参数更新做准备。 - 使用优化器的
step函数更新模型参数,根据计算得到的梯度对模型参数进行调整,以优化模型的性能。 - 在每次批次训练后,我们还需要更新总的损失和准确率,以便在训练结束后能够对模型的性能进行评估。
以下是训练函数的示例代码:
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))
(二)验证函数
在模型验证过程中,我们需要确保模型不会对参数进行更新,因此将模型设置为评估模式,即model.eval。在验证函数中,我们通过torch.no_grad上下文管理器来禁止梯度计算,因为在验证过程中不需要计算梯度。然后,我们按照与训练函数类似的步骤,对验证集数据进行预测并计算损失和准确率。以下是验证函数的示例代码:
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))
(三)计算准确率函数
在训练和验证函数中,我们都使用了get_batch_accuracy函数来计算批次准确率。该函数的主要功能是根据模型的输出预测和真实标签计算准确率。以下是get_batch_accuracy函数的示例代码:
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
(四)训练循环
最后,我们将训练函数、验证函数和计算准确率函数整合到训练循环中,进行多个 epochs 的训练。在每个 epoch 中,我们先打印当前 epoch 的编号,然后依次调用训练函数和验证函数,以便在训练过程中观察模型在训练集和验证集上的性能变化。以下是训练循环的示例代码:
epochs = 20
for epoch in range(epochs):
print('Epoch: {}'.format(epoch))
train()
validate()
六、结果讨论与分析
在完成模型训练后,我们观察到训练集上的准确率已经达到了相当高的水平,然而验证集上的准确率却相对较低。这种现象表明模型在训练过程中出现了过拟合问题。过拟合是指模型在训练数据上表现良好,但在未见过的新数据(验证集或测试集)上表现不佳的情况。其本质原因是模型过度学习了训练数据中的细节和噪声,而没有真正掌握数据的内在规律和特征,从而导致在面对新数据时无法泛化。在接下来的学习中,我们将深入探讨过拟合问题,并介绍一些有效的解决方法,例如数据增强、正则化等技术,以提高模型的泛化能力和性能。