0


Kaggle竞赛——手写数字识别(Digit Recognizer)

目录

本次手写数字识别使用了resnet18(比resnet50精度更好)、CNN和FCNN三种模型,精度上

  1. resnet18 > CNN > FCNN

,最终提交到官网的测试集精度为0.983,排名为758(提交时间:2024年9月1日)。数据集、代码、python虚拟环境和训练后的最佳模型已打包上传到Gitee,[点击直达]。
在这里插入图片描述

1. 数据集介绍

竞赛使用的是 MNIST (Modified National Institute of Standards and Technology, 美国国家标准与技术研究院修改版) 手写图像数据集,其中训练集

  1. 42000

条,测试集

  1. 28000

条,每条数据有784 个像素点,即原始图像的像素为 28 * 28。训练集中的

  1. Label

列表示手写数字的类别(共10个类别,0-10)。

2. 数据分析

  1. import pandas as pd
  2. import matplotlib.pyplot as plt
  3. import seaborn as sns
  4. from sklearn.model_selection import train_test_split
  5. train = pd.read_csv("D:/Desktop/kaggle数据集/digit-recognizer/train.csv")
  6. test = pd.read_csv("D:/Desktop/kaggle数据集/digit-recognizer/test.csv")
  1. train.info()
  1. <class'pandas.core.frame.DataFrame'>
  2. RangeIndex:42000 entries,0 to 41999
  3. Columns:785 entries, label to pixel783
  4. dtypes: int64(785)
  5. memory usage:251.5 MB
  1. test.info()
  1. <class'pandas.core.frame.DataFrame'>
  2. RangeIndex:28000 entries,0 to 27999
  3. Columns:784 entries, pixel0 to pixel783
  4. dtypes: int64(784)
  5. memory usage:167.5 MB

查看空缺值

  1. #--------------------------------------------------------------------------------------------------------------------------------## train_data.isnull(): 返回一个与 train_data 相同维度的布尔值数据框,其中 True 表示该位置存在缺失值,False 表示没有缺失值# any(): 对每一列进行操作,如果某列中存在至少一个 True 那么这一列的结果就是 True;否则就是 False,结果是一个布尔类型的 Series# describe(): 统计摘要,包括总列数、唯一值个数、最频繁出现的值(top)及其出现频率(freq)#--------------------------------------------------------------------------------------------------------------------------------#
  2. train.isnull().any().describe()
  1. count 785
  2. unique 1
  3. top False
  4. freq 785
  5. dtype:object

由结果可知,仅有一个唯一值

  1. False

,且出现785次,故训练集中无缺失值。

查看类别统计

  1. sns.countplot(x=train['label']);

在这里插入图片描述

3. 数据处理与封装

3.1 数据集划分

将训练集划分为训练集和验证集。

  1. # 分割特征和标签
  2. train_labels = train["label"]
  3. train= train.drop(labels=["label"], axis=1)# 划分训练集和验证集
  4. X_train, X_val, y_train, y_val = train_test_split(train, train_labels, test_size =0.2, random_state=41)print("训练集大小:{},验证集大小:{}".format(len(X_train),len(X_val)))
  1. 训练集大小:33600,验证集大小:8400

3.2 将数据转为tensor张量

  1. dataFrame

  1. Series

类型需要先转为

  1. numpy

类型,

  1. import torch
  2. from torch.utils.data import DataLoader, TensorDataset
  3. X_train_tensor = torch.tensor(X_train.values, dtype = torch.float32)
  4. y_train_tensor = torch.tensor(y_train.values)
  5. X_val_tensor = torch.tensor(X_val.values, dtype = torch.float32)
  6. y_val_tensor = torch.tensor(y_val.values)
  7. test_tensor = torch.tensor(test.values, dtype = torch.float32)

3.3 数据封装

使用

  1. TensorDataset

创建创建包含数据特征和数据类别的tensor数据集,再用

  1. DataLoader

划分封装数据集。封装数据集时,训练集中的shuffle参数设置为

  1. True

(随机打乱数据),可以防止模型学习到数据的顺序,从而提高模型的泛化能力;验证集和测试集shuffle参数设置为

  1. False

,能够保证测试集预测结果的一致性和可比性。

  1. train_tensor = TensorDataset(X_train_tensor, y_train_tensor)
  2. train_loader = DataLoader(train_tensor, batch_size=100, shuffle=True)
  3. val_tensor = TensorDataset(X_val_tensor, y_val_tensor)
  4. val_loader = DataLoader(val_tensor, batch_size=100, shuffle=False)
  5. test_loader = DataLoader(test_tensor, batch_size =100, shuffle=False)

可视化训练集中的一张图像

  1. plt.imshow(train.values[10].reshape(28,28), cmap='gray')
  2. plt.axis("off")
  3. plt.title(str(train_labels.values[10]));

在这里插入图片描述

4. 模型训练

4.1 定义功能函数

定义模型的训练和验证函数:

  1. """
  2. 模型训练函数
  3. Params:
  4. epoch: 训练轮次
  5. model: 预定义模型
  6. dataloader: 批处理数据
  7. criterion: 损失函数(交叉熵)
  8. optimizer: 优化器
  9. Returns
  10. running_loss/len(train_loader):本轮次(遍历一遍训练集)的平均损失
  11. sum_correct/train_num:本轮次(遍历一遍训练集)准确率
  12. """defmodel_train(epoch, model, model_name, dataloader, criterion, optimizer):# print("-------------------------Training-------------------------")# 设置模型为训练模式
  13. model.train()
  14. running_loss =0.0# 训练集大小
  15. train_num =len(X_train)#记录遍历一轮数据集后分类正确的样本数
  16. sum_correct =0for step, data inenumerate(dataloader):
  17. images, labels = data
  18. if model_name =='resnet18':#-------------------------------------------------------------------------------------------------## ResNet18 期望输入的形状为 [batch_size, channels, height, width],其中 channels 3RGB 图像)# expand(): 沿指定维度扩展张量(但不复制数据,只改变视图)#-------------------------------------------------------------------------------------------------#
  19. images = images.view(-1,1,28,28).expand(-1,3,-1,-1)if model_name =='cnn':# 自定义CNN的输入维度为 1
  20. images = images.view(-1,1,28,28)# 模型为FCNN时无需转换
  21. images = images.to(device)
  22. labels = labels.to(device)# 清除上一次迭代的梯度信息,防止梯度累积
  23. optimizer.zero_grad()#-------------------------------------------------------------------------------------------------## outputs的尺寸[每次输入的样本数(batch_size), 类别数]# 表示的含义:对应样本被分为某一类别的概率#-------------------------------------------------------------------------------------------------#
  24. outputs = model(images)# 计算损失值
  25. loss = criterion(outputs, labels)#-------------------------------------------------------------------------------------------------## 计算损失函数相对于模型参数的梯度,并将这些梯度存储在每个参数的 .grad 属性中。# 随后,优化器会使用这些梯度来更新模型参数,从而逐步最小化损失函数,实现模型的训练#-------------------------------------------------------------------------------------------------#
  26. loss.backward()# 使用优化器 optimizer 更新模型参数
  27. optimizer.step()
  28. running_loss += loss.item()#-------------------------------------------------------------------------------------------------## torch.max()函数返回两个值:每行的最大值和最大值的索引# _:表示忽略了第一个返回值(每行的最大值)# 1:寻找每行的最大值和索引#-------------------------------------------------------------------------------------------------#
  29. _, predicted = torch.max(outputs,1)#-------------------------------------------------------------------------------------------------## sum(): 将布尔张量转换为整数张量并对其进行求和,得到正确预测的总数。# 布尔值 True 计算为 1False 计算为 0。# item(): 将单元素张量转换为 Python 标量值,便于计算#-------------------------------------------------------------------------------------------------#
  30. correct =(predicted == labels).sum().item()
  31. sum_correct += correct
  32. train_acc = correct /len(labels)# print("[Epoch {}, step: {}] Train Loss: {:.4f}, Train Acc: {:.2f}%".format(epoch + 1, step+1, loss, train_acc*100))# print("-------------------------Training-------------------------")return running_loss/len(train_loader), sum_correct/train_num
  33. """
  34. 模型评估函数
  35. Params:
  36. epoch: 训练轮次
  37. model: 预定义模型
  38. dataloader: 批处理数据
  39. criterion: 损失函数(交叉熵)
  40. Returns
  41. running_loss/len(train_loader):本轮次(遍历一遍验证集)的平均损失
  42. sum_correct/train_num:本轮次(遍历一遍验证集)准确率
  43. """defmodel_validate(epoch, model, model_name, dataloader, criterion):# print("------------------------Validating------------------------")# 设置模型为测试模式
  44. model.eval()
  45. val_loss =0.0# 训练集大小
  46. val_num =len(X_val)
  47. sum_correct =0# 禁止梯度反传with torch.no_grad():for step, data inenumerate(dataloader):
  48. images, labels = data
  49. if model_name =='resnet18':#-------------------------------------------------------------------------------------------------## ResNet18 期望输入的形状为 [batch_size, channels, height, width],其中 channels 3RGB 图像)# expand(): 沿指定维度扩展张量(但不复制数据,只改变视图)#-------------------------------------------------------------------------------------------------#
  50. images = images.view(-1,1,28,28).expand(-1,3,-1,-1)if model_name =='cnn':# 自定义CNN的输入维度为 1
  51. images = images.view(-1,1,28,28)# 模型为FCNN时无需转换
  52. images = images.to(device)
  53. labels = labels.to(device)
  54. outputs = model(images)# 计算损失值
  55. loss = criterion(outputs, labels)
  56. val_loss += loss.item()#-------------------------------------------------------------------------------------------------## torch.max()函数返回两个值:每行的最大值和最大值的索引# _:表示忽略了第一个返回值(每行的最大值)# 1:寻找每行的最大值和索引#-------------------------------------------------------------------------------------------------#
  57. _, predicted = torch.max(outputs,1)
  58. correct =(predicted == labels).sum().item()
  59. sum_correct += correct
  60. total =len(labels)
  61. val_acc = correct / total
  62. # print("[Epoch {}, step: {}] Val Loss: {:.4f}, Val Acc: {:.2f}%".format(epoch + 1, step+1, loss, val_acc*100))# print("------------------------Validating------------------------")return val_loss/len(train_loader), sum_correct/val_num

定义模型训练与验证的综合函数:

  1. import torch.nn as nn
  2. import torch.optim as optim
  3. """
  4. 模型整体训练与验证函数
  5. Params:
  6. model: 预定义模型
  7. """deftrain_val(model, model_name):# 定义损失函数(交叉熵)和优化器
  8. criterion = nn.CrossEntropyLoss()
  9. optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)# 验证集上的最佳准确率和最佳轮次
  10. best_val_acc =0.0
  11. best_epoch =0for epoch inrange(10):# 模型训练
  12. train_loss, train_acc = model_train(epoch, model, model_name, train_loader, criterion, optimizer)
  13. train_losses.append(train_loss)
  14. train_accuracies.append(train_acc)# 模型验证
  15. val_loss, val_acc = model_validate(epoch, model, model_name, val_loader, criterion)
  16. val_losses.append(val_loss)
  17. val_accuracies.append(val_acc)if val_acc > best_val_acc:
  18. best_val_acc = val_acc
  19. best_epoch = epoch +1
  20. torch.save(model.state_dict(), model_name+'_best_model.pth')print("[第{}轮训练完成,训练集中 Loss:{},Accuracy:{}]".format(epoch+1, train_loss, train_acc))print("训练完成!最佳训练轮次:{},该轮次验证集上的准确率:{}".format(best_epoch, best_val_acc))

定义损失值和准确率的可视化函数:

  1. """
  2. 可视化损失值和准确率
  3. """defloss_acc_plot(train_losses, val_losses, train_accuracies, val_accuracies):
  4. plt.figure(figsize=(12,4))
  5. plt.subplot(1,2,1)# 默认情况下,plt.plot 会将 train_losses 的索引作为 X 轴的值
  6. plt.plot(train_losses, label='Train Loss')
  7. plt.plot(val_losses, label='Validation Loss')
  8. plt.xlabel('Epoch')
  9. plt.ylabel('Loss')
  10. plt.legend()
  11. plt.subplot(1,2,2)
  12. plt.plot(train_accuracies, label='Train Accuracy')
  13. plt.plot(val_accuracies, label='Validation Accuracy')
  14. plt.xlabel('Epoch')
  15. plt.ylabel('Accuracy')
  16. plt.legend()
  17. plt.tight_layout()

4.1 resnet18模型

  1. from torchvision import models
  2. # 使用GPU训练模型(如果GPU可用的话)
  3. device = torch.device("cuda"if torch.cuda.is_available()else"cpu")# 调用resnet18
  4. resnet_model = models.resnet18()
  5. resnet_model = resnet_model.to(device)# 记录训练集和验证集的损失值和准确率
  6. train_losses =[]
  7. val_losses =[]
  8. train_accuracies =[]
  9. val_accuracies =[]
  10. train_val(resnet_model,"resnet18")
  1. [第1轮训练完成,训练集中 Loss0.48536758923104834Accuracy0.9060714285714285][第2轮训练完成,训练集中 Loss0.05270050720095502Accuracy0.985][第3轮训练完成,训练集中 Loss0.02555189496238849Accuracy0.9938392857142857][第4轮训练完成,训练集中 Loss0.015233770400560129Accuracy0.9965773809523809][第5轮训练完成,训练集中 Loss0.007979269749263213Accuracy0.9988690476190476][第6轮训练完成,训练集中 Loss0.005160370017706771Accuracy0.9996428571428572][第7轮训练完成,训练集中 Loss0.0035936778385803336Accuracy0.9998511904761904][第8轮训练完成,训练集中 Loss0.0028507261213235324Accuracy0.9999107142857143][第9轮训练完成,训练集中 Loss0.002293311161511589Accuracy0.9998809523809524][第10轮训练完成,训练集中 Loss0.0019566422187857653Accuracy0.9998511904761904]
  2. 训练完成!最佳训练轮次:6,该轮次验证集上的准确率:0.9858333333333333

可视化损失值和准确率:

  1. loss_acc_plot(train_losses, val_losses, train_accuracies, val_accuracies)

在这里插入图片描述

4.3 CNN模型

定义CNN模型结构:

  1. classCNNModel(nn.Module):def__init__(self):super(CNNModel, self).__init__()# 卷积层 1
  2. self.cnn1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=0)
  3. self.relu1 = nn.ReLU()# 最大池化层 1
  4. self.maxpool1 = nn.MaxPool2d(kernel_size=2)# 卷积层 2
  5. self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=0)
  6. self.relu2 = nn.ReLU()# 最大池化层 2
  7. self.maxpool2 = nn.MaxPool2d(kernel_size=2)# 全连接层
  8. self.fc1 = nn.Linear(32*4*4,10)defforward(self, x):# 卷积层 1
  9. out = self.cnn1(x)
  10. out = self.relu1(out)#最大池化层 1
  11. out = self.maxpool1(out)# 卷积层 2
  12. out = self.cnn2(out)
  13. out = self.relu2(out)# 最大池化层 2
  14. out = self.maxpool2(out)# flatten
  15. out = out.view(out.size(0),-1)# 全连接层
  16. out = self.fc1(out)return out

网络结构可视化:
在这里插入图片描述

上述结构图中,双层黄色块的第一层表示卷积操作,第二层表示

  1. ReLU()

激活操作,红色块表示最大池化操作。

  1. 16

表示卷积操作后输出的的通道数,784表示卷积操作后输出的图像大小(宽度*高度)。


训练模型:

  1. cnn_model = CNNModel()
  2. cnn_model = cnn_model.to(device)# 记录训练集和验证集的损失值和准确率
  3. train_losses =[]
  4. val_losses =[]
  5. train_accuracies =[]
  6. val_accuracies =[]
  7. train_val(cnn_model,"cnn")
  1. [第1轮训练完成,训练集中 Loss1.004740373009727Accuracy0.8109226190476191][第2轮训练完成,训练集中 Loss0.17581947764293068Accuracy0.945625][第3轮训练完成,训练集中 Loss0.13953285299551985Accuracy0.9569345238095238][第4轮训练完成,训练集中 Loss0.12599757561526662Accuracy0.9607738095238095][第5轮训练完成,训练集中 Loss0.11612535938842311Accuracy0.9638095238095238][第6轮训练完成,训练集中 Loss0.10294126443720113Accuracy0.9671726190476191][第7轮训练完成,训练集中 Loss0.09651396153051228Accuracy0.9698214285714286][第8轮训练完成,训练集中 Loss0.09004475945229864Accuracy0.9717857142857143][第9轮训练完成,训练集中 Loss0.08583687311537298Accuracy0.9727083333333333][第10轮训练完成,训练集中 Loss0.08039018868779142Accuracy0.9748214285714286]
  2. 训练完成!最佳训练轮次:9,该轮次验证集上的准确率:0.9721428571428572

可视化损失值和准确率:

  1. loss_acc_plot(train_losses, val_losses, train_accuracies, val_accuracies)

在这里插入图片描述

4.4 FCNN模型

定义FCNN模型结构:

  1. classFCNNModel(nn.Module):def__init__(self, input_dim, hidden_dim, output_dim):super(FCNNModel, self).__init__()# 784 --> 150
  2. self.fc1 = nn.Linear(input_dim, hidden_dim)# 激活函数
  3. self.relu1 = nn.ReLU()# 150 --> 150
  4. self.fc2 = nn.Linear(hidden_dim, hidden_dim)# 激活函数
  5. self.tanh2 = nn.Tanh()# 150 --> 150
  6. self.fc3 = nn.Linear(hidden_dim, hidden_dim)# 激活函数
  7. self.elu3 = nn.ELU()# 150 --> 10
  8. self.fc4 = nn.Linear(hidden_dim, output_dim)defforward(self, x):# 784 --> 150
  9. out = self.fc1(x)
  10. out = self.relu1(out)# 150 --> 150
  11. out = self.fc2(out)
  12. out = self.tanh2(out)# 150 --> 150
  13. out = self.fc3(out)
  14. out = self.elu3(out)# 150 --> 10
  15. out = self.fc4(out)return out

模型训练:

  1. # 记录训练集和验证集的损失值和准确率
  2. train_losses =[]
  3. val_losses =[]
  4. train_accuracies =[]
  5. val_accuracies =[]
  6. input_dim =28*28# 可微调
  7. hidden_dim =150
  8. output_dim =10
  9. fcnn_model = FCNNModel(input_dim, hidden_dim, output_dim)
  10. fcnn_model = fcnn_model.to(device)
  11. train_val(fcnn_model,"fcnn")
  1. [第1轮训练完成,训练集中 Loss0.8977700323753414Accuracy0.7811904761904762][第2轮训练完成,训练集中 Loss0.3077347204089165Accuracy0.9172916666666666][第3轮训练完成,训练集中 Loss0.2244828560034789Accuracy0.9372619047619047][第4轮训练完成,训练集中 Loss0.18338089338725522Accuracy0.9476488095238095][第5轮训练完成,训练集中 Loss0.15651956990006424Accuracy0.9541071428571428][第6轮训练完成,训练集中 Loss0.1355396158483234Accuracy0.9603869047619048][第7轮训练完成,训练集中 Loss0.11753073033122789Accuracy0.965625][第8轮训练完成,训练集中 Loss0.10319345946135443Accuracy0.9705059523809524][第9轮训练完成,训练集中 Loss0.09024346410296857Accuracy0.974047619047619][第10轮训练完成,训练集中 Loss0.07875394061695606Accuracy0.9776785714285714]
  2. 训练完成!最佳训练轮次:10,该轮次验证集上的准确率:0.963452380952381

可视化损失值和准确率:

  1. loss_acc_plot(train_losses, val_losses, train_accuracies, val_accuracies)

在这里插入图片描述

5. 结果分析

5.1 混淆矩阵

计算混淆矩阵,

  1. from sklearn.metrics import confusion_matrix
  2. cm = confusion_matrix(all_labels, all_predictions)
  3. plt.figure(figsize=(5,5))
  4. sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", cbar=False,
  5. xticklabels=range(10), yticklabels=range(10))
  6. plt.xlabel("Predicted Label")
  7. plt.ylabel("True Label")
  8. plt.title("Confusion Matrix");

在这里插入图片描述
横轴为预测类别,纵轴为实际类别。对标线上的值表示模型正确预测的样本数量,非对角线上的值表示模型错误预测的样本数量。对角线(1, 1)中的值900表示实际类别为1的样本中有900条被正确预测为1;(1, 4)中的值为1表示实际类别为1的样本中有1个样本被错误预测为4。

5.2 查看错误分类的样本

  1. incorrect_images =[]
  2. incorrect_labels =[]
  3. predicted_labels =[]with torch.no_grad():for step, data inenumerate(val_loader):
  4. images, labels = data
  5. images = images.view(-1,1,28,28)
  6. outputs = best_resnet_model(images.expand(-1,3,-1,-1))
  7. _, predicted = torch.max(outputs,1)for i inrange(len(predicted)):if predicted[i]!= labels[i]:
  8. incorrect_images.append(images[i])
  9. incorrect_labels.append(labels[i])
  10. predicted_labels.append(predicted[i])# 展示部分预测错误的样本
  11. num_samples =6
  12. fig, axes = plt.subplots(nrows=2, ncols=num_samples //2, figsize=(10,6))
  13. axes = axes.flatten()for i inrange(num_samples):
  14. ax = axes[i]
  15. img = incorrect_images[i].reshape(28,28)
  16. ax.imshow(img, cmap='gray')
  17. ax.set_title(f"True: {incorrect_labels[i]}, Pred: {predicted_labels[i]}")
  18. ax.axis('off')

在这里插入图片描述

6. 加载最佳模型

保存的最佳模型中,

  1. resnet18

  1. CNN

  1. FCNN

在验证集中的准确率分别为

  1. 98.58%

  1. 97.21%

  1. 96.35%

,因此选择resnet18模型来预测测试集。

  1. best_model = models.resnet18()
  2. best_model.load_state_dict(torch.load("./resnet18_best_model.pth"))
  3. predictions =[]with torch.no_grad():for data in test_loader:
  4. images = data.view(-1,1,28,28).expand(-1,3,-1,-1)
  5. outputs = best_model(images)
  6. _, predicted = torch.max(outputs,1)
  7. predictions.extend(predicted.numpy())# 保存预测结果
  8. submission = pd.DataFrame({'ImageId':range(1,len(test)+1),'Label': predictions})# submission.to_csv('/kaggle/working/submission.csv', index=False)print('Submission file created!')

7. 参考文献

[1] kaggle:Digit Recognizer《手写数字识别》你的第一个图像识别竞赛项目
[2] Pytorch Tutorial for Deep Learning Lovers


本文转载自: https://blog.csdn.net/m0_53062159/article/details/141819470
版权归原作者 哈密瓜Q 所有, 如有侵权,请联系我们删除。

“Kaggle竞赛——手写数字识别(Digit Recognizer)”的评论:

还没有评论