即使是这么乱写也很准确,准确率达到995这种程度的手写数字识别项目,请看如下

好家伙这破选题给我折腾了一整个小时,手写数字识别增强,MNIST加个平移旋转噪点

完整源码链接:https://pan.quark.cn/s/1e54aa2ae950

先说数据这一块,torchvision的datasets.MNIST倒是方便,一行就下好了,但他*的下到一半断了,那个破进度条卡在67%一动不动,我等了特么半小时才发现是yann.lecun那个老域名404了,好在torchvision自己会切到aws的镜像,不然我真想砸电脑

数据增强这一块我用的transforms.Compose一条龙,RandomAffine设了degrees=15, translate=(0.15,0.15),然后ToTensor之后再加一个Lambda层塞高斯噪声,torch.randn_like出来的噪声乘0.15再clamp到0到1之间,不然归一化之后有些像素直接变负数了,踩过的坑

def get_transform(augment=False):
    if not augment:
        return transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])
    return transforms.Compose([
        transforms.RandomAffine(degrees=15, translate=(0.15, 0.15)),
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.clamp(
            x + torch.randn_like(x) * 0.15, 0.0, 1.0
        )),
        transforms.Normalize((0.1307,), (0.3081,))
    ])

normalize那俩参数(0.1307,)和(0.3081,)是MNIST数据集的均值和标准差,算出来的,不是瞎编的,这玩意儿要是搞错了模型收敛巨慢,我之前直接拿(0.5,0.5)归一化结果loss跟心电图一样上下蹦

然后说Dataloader那块的num_workers,windows下这玩意儿要是设大于0直接给你报错BrokenPipeError,气得我直接设0拉倒,linux下没问题但咱这破windows不配用多进程是吧

CNN模型结构我就搞了两层卷积,第一层32个3x3核,第二层64个3x3核,每层后面跟BatchNorm和MaxPool,最后接一个128的全连接加Dropout(0.5),输出10类

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout1 = nn.Dropout2d(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.bn1(self.conv1(x))))
        x = self.dropout1(x)
        x = self.pool(F.relu(self.bn2(self.conv2(x))))
        x = self.dropout1(x)
        x = x.view(-1, 64 * 7 * 7)
        x = F.relu(self.fc1(x))
        x = self.dropout2(x)
        x = self.fc2(x)
        return x

我一开始没加BatchNorm,结果训练到第三个epoch准确率就卡在93%上不去了,加了bn之后直接冲到98%+,细节啊兄弟们,bn的gamma和beta虽然多了点参数量但值,Dropout的位置也讲究,我放在pool后面而不是conv后面效果更好

训练的时候有个坑,就是增强过的数据训练集准确率看着没那么高,第一轮才61.5%,吓我一跳以为模型崩了,后来一想不对啊,增强之后样本更难了,原始测试集反而一路飙到96%+,这就对了,说明增强起到了正则化的作用

def train_epoch(model, loader, optimizer, device):
    model.train()
    total_loss = 0.0
    correct = 0
    total = 0
    for data, target in loader:
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.cross_entropy(output, target)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * data.size(0)
        pred = output.argmax(dim=1)
        correct += pred.eq(target).sum().item()
        total += target.size(0)
    return total_loss / total, correct / total

学习率我设的0.001用Adam,每5个epoch衰减一半,最后一轮降到0.00025,其实也可以直接用CosineAnnealing但我懒

可视化那块才叫折腾,matplotlib中文显示的问题我说了多少遍了每次都得配SimHei,还特么得设axes.unicode_minus=False不然负号显示成方块,我直接把这两行扔文件开头

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

跑出来一看增强对比图,原始数字整整齐齐,增强之后歪七扭八的还带雪花噪点,看着就真实,随机平移了±15%,旋转±15度,再加高斯噪声σ=0.15

训练曲线那张图最能说明问题,损失从1.1一路降到0.29,训练集准确率从61.5%爬到91.1%,测试集准确率从96.24%一路升到98.8%,关键是训练和测试的差距在缩小,说明增强确实有用没过拟合

混淆矩阵那边我用的sklearn的confusion_matrix配合seaborn的热力图画的,大部分数字都分得挺好,就是4和9偶尔搞混,还有3和8也有几回,这特么也正常人类写潦草了也分不清

我单独看了一下错误分类的样本图,有些数字写得确实离谱,7写得跟1一样,5写得跟6似的,这要是人能认错那也不能怪模型对吧

训练结束后保存的模型才1.6MB,放到手机上跑都没问题,pth格式直接load就行

torch.save(model.state_dict(), 'mnist_cnn_final.pth')

最后跑一下main.py就全自动了,数据增强→训练→保存→画图一条龙,10个epoch在我这破i5笔记本cpu上跑了大概七八分钟,要是能调cuda估计两三分钟搞定

整体的目录结构也不复杂,项目文件夹里五个py文件加一个data目录存MNIST数据再加一个pth模型和五张png图

生成的05_错误分类.png里面能看到模型具体在哪些样本上翻车了,这个对调试特别有用,我看了下大部分错误样本本身就模糊到人眼都认不出,那没辙

总的来说这破项目就是MNIST数据增强加CNN的一套标准流程,你要是毕设选这个,切记三点:一是数据增强的参数别设太猛不然模型学不动,二是BatchNorm和Dropout一定要加,三是matplotlib的SimHei赶紧配好不要等到画图才抓瞎

最终测试集准确率98.8%,马马虎虎吧要是调调超参数上99%也不难,但我困了就这样吧

非常不错了。

更多推荐