街景字符识别Baseline解析

上一篇文章已经将pytorch环境配置完毕,本篇对baseline代码进行解析。下一篇文章将对baseline进行改进。

  1. 相关库导入
import os, sys, glob, shutil, json
os.environ["CUDA_VISIBLE_DEVICES"] = '0'  ##用于指定用哪块GPU
import cv2

from PIL import Image
import numpy as np

from tqdm import tqdm, tqdm_notebook  ##时间进度条模块

%pylab inline
import torch
torch.manual_seed(0)  ##限定一个随机种子点,保证每次运行结果相同,方便验证

##使用GPU需用用到cuda模块,配合cudnn加速模块一起使用
torch.backends.cudnn.deterministic = True
#用以保证实验的可重复性
torch.backends.cudnn.benchmark = True 

import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset

相关问题:
(1)一般而言,设置torch.backends.cudnn.benchmark = True 可以大大提升运行效率。那么pytorch库为什么不直接将它设置为True呢?
总的来说,大部分情况下,设置这个 flag 可以让内置的 cuDNN 的 auto-tuner 自动寻找最适合当前配置的高效算法,来达到优化运行效率的问题。

一般来讲,应该遵循以下准则:

如果网络的输入数据维度或类型上变化不大,设置 torch.backends.cudnn.benchmark = true 可以增加运行效率;
如果网络的输入数据在每次 iteration 都变化的话,会导致 cnDNN 每次都会去寻找一遍最优配置,这样反而会降低运行效率。

  1. 数据载入与预处理
##数据预处理模块
class SVHNDataset(Dataset):
    def __init__(self, img_path, img_label, transform=None):
        self.img_path = img_path
        self.img_label = img_label 
        if transform is not None:
            self.transform = transform
        else:
            self.transform = None

    def __getitem__(self, index):
        img = Image.open(self.img_path[index]).convert('RGB')  ##转成RGB格式图片

        if self.transform is not None:
            img = self.transform(img)##把图片裁剪缩放到指定形式
        
        lbl = np.array(self.img_label[index], dtype=np.int)
        lbl = list(lbl)  + (5 - len(lbl)) * [10]  ##标签扩展到统一长度
        return img, torch.from_numpy(np.array(lbl[:5]))

    def __len__(self):
        return len(self.img_path)

相关问题
赛题中的label有的图片的字符个数为2,有的图片字符个数为3,有的图片字符个数为4,长度不固定。
可以将赛题抽象为一个定长字符识别问题,在赛题数据集中大部分图像中字符个数为2-4个,最多的字符 个数为5个。
因此可以对于所有的图像都抽象为5个字符的识别问题,字符23填充为23XXX,字符231填充为231XX。

train_path = glob.glob('../input/train/*.png')
#glob函数用于模糊匹配一定格式的文件名
train_path.sort()
train_json = json.load(open('../input/train.json'))
train_label = [train_json[x]['label'] for x in train_json]
print(len(train_path), len(train_label))

train_loader = torch.utils.data.DataLoader(
    SVHNDataset(train_path, train_label,
                transforms.Compose([
                    transforms.Resize((64, 128)),##统一缩放到64*128
                    transforms.RandomCrop((60, 120)),##随机剪裁
                    transforms.ColorJitter(0.3, 0.3, 0.2),##调整亮度透明度
                    transforms.RandomRotation(10),##随机旋转
                    transforms.ToTensor(),##转成一个tensor
                    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])##标准化
    ])), 
    batch_size=40, ##一个batch的图片数量
    shuffle=True, 
    num_workers=0,  ##num_workers为线程数目,之前为10,报错,由于线程数与内核数不匹配,把num_workers改为0成功
)

val_path = glob.glob('../input/val/*.png')
val_path.sort()
val_json = json.load(open('../input/val.json'))
val_label = [val_json[x]['label'] for x in val_json]
print(len(val_path), len(val_label))

val_loader = torch.utils.data.DataLoader(
    SVHNDataset(val_path, val_label,
                transforms.Compose([  #图像预处理常写在compose内,成为一组操作
                    transforms.Resize((60, 120)),
                    # transforms.ColorJitter(0.3, 0.3, 0.2),
                    # transforms.RandomRotation(5),
                    transforms.ToTensor(),
                    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])##使用resnet18必须标准化到特定
    ])), 
    batch_size=40, 
    shuffle=False, 
    num_workers=10,
)

相关问题:
(1)训练阶段数据增广
为了增加样本,本文将样本进行旋转、剪裁、对比度亮度调节等操作。
transforms.RandomCrop((60, 120)),##随机剪裁
transforms.ColorJitter(0.3, 0.3, 0.2),##调整亮度透明度
transforms.RandomRotation(10),##随机旋转

在常见的数据扩增方法中,一般会从图像颜色、尺寸、形态、空间和像素等角度进行变换。在test阶段不需要此操作。

(2)数据归一化
由于需要使用resnet预训练模型,模型输入的图片最好是经过相同方式归一化的3通道数据,即(3HW)H与W至少是224,
图像必须被加载到(0,1)的范围内,然后再归一化到mean = [0.485, 0.456, 0.406]且std=[0.229,0.224,0.225]。
可以使用pytorch自带的归一化函数

  1. 定义分类模型
#定义分类模型
class SVHN_Model1(nn.Module):
    def __init__(self):
        super(SVHN_Model1, self).__init__()               
        model_conv = models.resnet18(pretrained=True)
         # #加载模型,并设为预训练模式
        model_conv.avgpool = nn.AdaptiveAvgPool2d(1)
        #自适应平均池化,strides,paddings等参数都自适应好了
        model_conv = nn.Sequential(*list(model_conv.children())[:-1])
        #取模型结果
        self.cnn = model_conv
        
        self.fc1 = nn.Linear(512, 11)
        self.fc2 = nn.Linear(512, 11)
        self.fc3 = nn.Linear(512, 11)
        self.fc4 = nn.Linear(512, 11)
        self.fc5 = nn.Linear(512, 11)
        #五个fc层并联,由于图片对应的是一个5位数的值,分别对每一个进行识别,所以5个fc
    
    def forward(self, img):        
        feat = self.cnn(img)
        # print(feat.shape)
        feat = feat.view(feat.shape[0], -1)
        c1 = self.fc1(feat)
        c2 = self.fc2(feat)
        c3 = self.fc3(feat)
        c4 = self.fc4(feat)
        c5 = self.fc5(feat)
        return c1, c2, c3, c4, c5
  1. 定义训练与预测
def train(train_loader, model, criterion, optimizer, epoch):
    # 切换模型为训练模式
    model.train()
    train_loss = []
    
    for i, (input, target) in enumerate(train_loader):  ##每次循环为一个batch
        if use_cuda:
            input = input.cuda()
            target = target.cuda()
            
        c0, c1, c2, c3, c4 = model(input)
        loss = criterion(c0, target[:, 0]) + \
                criterion(c1, target[:, 1]) + \
                criterion(c2, target[:, 2]) + \
                criterion(c3, target[:, 3]) + \
                criterion(c4, target[:, 4])
        ##计算损失函数的时候是5个FC层的损失相加
     
        optimizer.zero_grad()
        loss.backward()##损失回传
        optimizer.step()
        train_loss.append(loss.item())
    return np.mean(train_loss)

def validate(val_loader, model, criterion):
    # 切换模型为预测模型
    model.eval()
    val_loss = []

    # 不记录模型梯度信息
    with torch.no_grad():
        for i, (input, target) in enumerate(val_loader):
            if use_cuda:
                input = input.cuda()
                target = target.cuda()
            
            c0, c1, c2, c3, c4 = model(input)
            loss = criterion(c0, target[:, 0]) + \
                    criterion(c1, target[:, 1]) + \
                    criterion(c2, target[:, 2]) + \
                    criterion(c3, target[:, 3]) + \
                    criterion(c4, target[:, 4])
            # loss /= 6
            val_loss.append(loss.item())
    return np.mean(val_loss)

def predict(test_loader, model, tta=10):
    model.eval()
    test_pred_tta = None
    
    # TTA 次数
    for _ in range(tta):
        test_pred = []
    
        with torch.no_grad():
            for i, (input, target) in enumerate(test_loader):
                if use_cuda:
                    input = input.cuda()
                
                c0, c1, c2, c3, c4 = model(input)
                if use_cuda:
                    output = np.concatenate([
                        c0.data.cpu().numpy(),  ##预测阶段只用cpu就可以
                        c1.data.cpu().numpy(),
                        c2.data.cpu().numpy(), 
                        c3.data.cpu().numpy(),
                        c4.data.cpu().numpy()], axis=1)
                else:
                    output = np.concatenate([
                        c0.data.numpy(), 
                        c1.data.numpy(),
                        c2.data.numpy(), 
                        c3.data.numpy(),
                        c4.data.numpy()], axis=1)
                
                test_pred.append(output)
        
        test_pred = np.vstack(test_pred)
        if test_pred_tta is None:
            test_pred_tta = test_pred
        else:
            test_pred_tta += test_pred
    
    return test_pred_tta

相关问题:

(1)损失回传 loss.backward()在前,然后跟一个optimizer.step()。

那么为什么optimizer.step()需要放在每一个batch训练中,而不是epoch训练中,
这是因为现在的mini-batch训练模式是假定每一个训练集就只有mini-batch这样大,
因此实际上可以将每一次mini-batch看做是一次训练,一次训练更新一次参数空间,因而optimizer.step()放在这里。

(2)cuda 与cudnn
CUDA是显卡厂商NVIDIA推出的运算平台。cuDNN是用于深度神经网络的GPU加速库。cuDNN可以集成到更高级别的机器学习框架中,如谷歌的Tensorflow、加州大学伯克利分校的流行caffe软件。简单的插入式设计可以让开发人员专注于设计和实现神经网络模型,而不是简单调整性能,同时还可以在GPU上实现高性能现代并行计算。

  1. 训练与验证
##验证
model = SVHN_Model1()
criterion = nn.CrossEntropyLoss()##交叉熵损失函数
optimizer = torch.optim.Adam(model.parameters(), 0.001)#0.001为学习率
best_loss = 1000.0  

# 是否使用GPU
use_cuda = True
if use_cuda:
    model = model.cuda()

for epoch in range(10):
    train_loss = train(train_loader, model, criterion, optimizer, epoch)
    val_loss = validate(val_loader, model, criterion)
    
    val_label = [''.join(map(str, x)) for x in val_loader.dataset.img_label]
    val_predict_label = predict(val_loader, model, 1)
    val_predict_label = np.vstack([
        val_predict_label[:, :11].argmax(1),
        val_predict_label[:, 11:22].argmax(1),
        val_predict_label[:, 22:33].argmax(1),
        val_predict_label[:, 33:44].argmax(1),
        val_predict_label[:, 44:55].argmax(1),
    ]).T
    val_label_pred = []
    for x in val_predict_label:
        val_label_pred.append(''.join(map(str, x[x!=10])))
    
    val_char_acc = np.mean(np.array(val_label_pred) == np.array(val_label))
    
    print('Epoch: {0}, Train loss: {1} \t Val loss: {2}'.format(epoch, train_loss, val_loss))
    print('Val Acc', val_char_acc)
    # 记录下验证集精度
    if val_loss < best_loss:
        best_loss = val_loss
        # print('Find better model in Epoch {0}, saving model.'.format(epoch))
        torch.save(model.state_dict(), './model.pt')

相关问题:
(1)训练拆分为10个epoch分别训练。
(2)预测结果如何转化?
预测结果为5组11个概率值。分别对应5位数,每个数有11种可能:0-9和填充X
然后取每组概率最大的label,即为那一位数的预测结果。
val_predict_label[:, :11].argmax(1),
val_predict_label[:, 11:22].argmax(1),
val_predict_label[:, 22:33].argmax(1),
val_predict_label[:, 33:44].argmax(1),
val_predict_label[:, 44:55].argmax(1)

运行结果

Epoch: 0, Train loss: 3.549230361620585 	 Val loss: 3.4495382709503173
0.3484
Epoch: 1, Train loss: 2.262838746547699 	 Val loss: 3.070304814338684
0.4262
Epoch: 2, Train loss: 1.9043410422801972 	 Val loss: 2.8098975682258605
0.4742
Epoch: 3, Train loss: 1.6968964731693268 	 Val loss: 2.6710116691589354
0.4948
Epoch: 4, Train loss: 1.5359156634807587 	 Val loss: 2.652583309173584
0.5126
Epoch: 5, Train loss: 1.4319444489479065 	 Val loss: 2.630811668395996
0.5247
Epoch: 6, Train loss: 1.3359439101219177 	 Val loss: 2.477479434013367
0.5383
Epoch: 7, Train loss: 1.244316361983617 	 Val loss: 2.458457417488098
0.5498
Epoch: 8, Train loss: 1.182295233209928 	 Val loss: 2.5932382040023803
0.5316
Epoch: 9, Train loss: 1.1050671869913737 	 Val loss: 2.4330560541152955
0.554

从loss和acc总体来看,baseline准确度很低。下一步将做优化。

那么如何训练一个较为理想的medel?
  首先,要有一个期望的准确率,通过不同模型的实验,找到最能接近的;
  然后,选定模型后进行参数调优;

你可能感兴趣的:(深度学习)