CNN识别MNIST手写字

CNN识别MNIST手写字

开源地址:https://github.com/MYJOKERML/CNN

学了这么久,终于可以自己搭建一个CNN了,还记得大一时看了好多次这样的代码,然后全都无功而返。现在会看去年的自己,哈,这小家伙居然连个python代码都看不懂,连个class都不会。也许再等一年后再次回头时我可以笑着说,哈,一年前的自己居然看一个小时论文要翻半个小时字典,自己的进步真的肉眼可见啊,也得益于日益进步的科技,在copilot的协助下,现在普通的代码已经主要以理清逻辑为主了,人类的生产力又再一次有了质的飞跃,真的了不起。

话不多说,直接上代码记录自己的学习过程。

  1. 导入相关包

    import torch
    from torch import nn, optim
    from torchvision import datasets, transforms, models
    from torch.utils.data import DataLoader, random_split
    
    from matplotlib import pyplot as plt
    from PIL import Image
    
  2. 划分数据集并实现数据增强

    这里数据增强我才用了常见的数据增强办法,随机旋转图片和平移图片,以及颜色变换。我注释掉了随机水平翻转,因为注意到很多数字水平翻转后并没有任何意义。

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # 定义数据预处理转换,包括数据增强操作
    transform = transforms.Compose([
        transforms.RandomRotation(degrees=15),  # 随机旋转
        transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),  # 随机平移
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),  # 随机颜色变换
        # transforms.RandomHorizontalFlip(),  # 随机水平翻转
        transforms.ToTensor(),
    ])
    
    dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
    
    # 定义训练集和测试集的比例
    train_ration = 0.8
    test_ration = 1 - train_ration
    
    # 计算训练集和测试集的数量
    train_size = int(train_ration * len(dataset))
    test_size = len(dataset) - train_size
    
    # 按照计算的数量随机划分训练集和测试集
    train_dataset, test_dataset = random_split(dataset, [train_size, test_size])
    
  3. 加载数据集并查看第一张图片

    # 加载数据并查看数据
    # from einops import rearrange
    
    BATCH_SIZE = 64
    train_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    test_loader = DataLoader(dataset=test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    
    for i, (images, labels) in enumerate(train_loader):
        print(i, images.shape, labels.shape)
        plt.imshow(images[0][0], cmap='gray')
        plt.title(labels[0].item())
        break
    
  4. 搭建卷积神经网络

    卷积核一般是3*3的大小,于是令padding=1就可以保持原图像大小不变。

    最开始只搭了两层卷积层,然后2层全连接层,事实证明这样非常容易过拟合,通常第一轮epoch结束后就有90%几的正确率,5轮之后会有97%的正确率,显然过拟合了,我自己手写了一张 8 的图片,训练了好多次一直都是判断为 0 。。。

    后来我想明白了,结果不理想的原因是网络深度太浅,根本提取不到足够大小范围的图片,于是后面搭了4层卷积层,3层全连接层,效果确实变好了。

    最加深网络时特征提取多了(即 out_channels 设大了),于是模型很难训练,正确率一直在70%几上不去,想了想应该是因为本来图片尺寸就小28*28,又是灰度图,没这么多特征,所以最后输出通道数就比较小,成功改善了结果。最后训练出来正确率在94%左右。

    本来一般是有加池化层的,但现在算力足够了,也就没加池化层,完全可以直接算。

    class CNN(nn.Module):
        def __init__(self):
            super(CNN, self).__init__()
            # 卷积层提取图片特征
            self.conv1 = nn.Sequential(
                nn.Conv2d(1, 16, 3, 1, 1),
                nn.ReLU()
            )
            self.conv2 = nn.Sequential(
                nn.Conv2d(16, 16, 3, 1, 1),
                nn.ReLU()
            )
            
            # 添加更多的卷积层
            self.conv3 = nn.Sequential(
                nn.Conv2d(16, 32, 3, 1, 1),
                nn.ReLU()
            )
            self.conv4 = nn.Sequential(
                nn.Conv2d(32, 32, 3, 1, 1),
                nn.ReLU()
            )
    
            # 添加池化层
            # self.pool = nn.MaxPool2d(2, 2)
            
            self.fc_input_size = 32 * 28 * 28
    
            self.fc1 = nn.Sequential(nn.Linear(self.fc_input_size, 256), nn.Dropout(0.2), nn.ReLU())
            self.fc2 = nn.Sequential(nn.Linear(256, 128), nn.Dropout(0.2), nn.ReLU())
            self.fc3 = nn.Sequential(nn.Linear(128, 10), nn.Softmax(dim=1))
    
        def forward(self, x):
            x = self.conv1(x)
            # x = self.pool(x)
            x = self.conv2(x)
            # x = self.pool(x)
            x = self.conv3(x)
            # x = self.pool(x)
            x = self.conv4(x)
            # x = self.pool(x)
    
            x = x.view(x.shape[0], -1)
    
            x = self.fc1(x)
            x = self.fc2(x)
            x = self.fc3(x)
    
            return x
    
  5. 定义损失函数和优化器

    # 定义损失函数和优化器
    model = CNN()
    model = model.to(device)
    criterion = nn.CrossEntropyLoss()
    lr=0.001 # 学习率
    optimizer = optim.Adam(model.parameters(), lr)
    
  6. 训练模型和测试模型

    from tqdm import tqdm
    
    best_loss = float('inf')  # 初始化为正无穷大,确保第一个损失值一定会小于它
    
    # 训练模型
    def train():
        model.train()
        total_loss = 0 # 用于计算平均损失
        num_batches = len(train_loader) # 用于记录训练的batch数目
    
        with tqdm(total=num_batches, desc='Training', unit='batch') as pbar:
            for i, data in enumerate(train_loader):
                inputs, labels = data
                inputs, labels = inputs.to(device), labels.to(device)
                optimizer.zero_grad() # 梯度清零
                outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward() # 反向传播
                optimizer.step() # 更新参数
    
                total_loss += loss.item()
                current_loss = loss.item()
    
                if i % 100 == 0:
                    # print('Train Step: {}\tLoss: {:.3f}'.format(i, loss.item()))
                    avg_loss = total_loss / (i + 1)
                    pbar.set_postfix({'cur_loss': '{:.3f}'.format(loss), 'avg_loss':'{:.3f}'.format(avg_loss)})
                    pbar.update(100) # 每处理100个batch更新一次tqdm
                    # 判断当前损失是否比最佳损失小,如果是则保存模型状态
                if current_loss < best_loss:
                    best_loss = current_loss
                    torch.save(model.state_dict(), 'best_model.pth')  # 保存模型
        
        print('Best loss:', best_loss)            
    
    
    # 测试模型
    def test():
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad(): # 测试过程中不需要计算梯度
            with tqdm(total=len(test_loader), desc="Testing", unit="batch") as pbar:
                for data in test_loader:
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)
                    outputs = model(inputs)
                    _, predicted = torch.max(outputs.data, dim=1)
                    total += labels.size(0)
                    # print(predicted.shape, labels.shape)
                    correct += (predicted == labels).sum().item()
                    pbar.update(1)# 每处理1个batch更新一次tqdm
    
        print('Accuracy on test set: {:.3f}'.format(correct / total))
    
    print('Before training:', end=' ')
    test()
    
  7. 保存和加载模型

    上面已经保存了loss最低的模型,但可能过拟合,因此再保存一个最后一次运行的模型,加载loss最低的模型进行测试。

    # 保存最后一次模型
    
    torch.save(model.state_dict(), os.path.join(save_path, 'model.pth'))
    
    # 加载模型
    model = CNN()
    model.load_state_dict(torch.load(os.path.join(save_path, 'best_model.pth')))
    model = model.to(device)
    
    # 预测
    def predict(img):
        model.eval()
        img = img.to(device)
        with torch.no_grad():
            output = model(img)
            # print(output)
            _, predicted = torch.max(output.data, dim=1)
            return predicted.item()
        
    import random
    # 随机选取一张图片进行预测
    index = random.randint(0, len(test_dataset))
    img = test_dataset[index][0].unsqueeze(0)
    label = test_dataset[index][1]
    plt.imshow(img[0][0], cmap='gray')
    plt.title('label: {}'.format(label))
    plt.show()
    print('predict: {}'.format(predict(img)))
    

    自己手写了一张数字 8 的图片进行测试。

    8

    先对图片进行缩放,再转换为灰度图,再转换成张量之后输入模型进行预测。之前一直预测为0,但更改模型深度后就可以较好地预测出该图片为8。

    from PIL import Image
    
    img = Image.open('./8.jpg')
    
    transform = transforms.Compose([
        transforms.Resize((28, 28)),
        transforms.Grayscale(),
        transforms.ToTensor(),
    ])
    
    print("Original size: ", img.size)
    img = transform(img)
    print(img.shape)
    plt.title('label: 8')
    plt.imshow(img[0], cmap='gray')
    plt.show()
    
    print('predict: {}'.format(predict(img.unsqueeze(0))))
    
    8
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值