零基础入门CV赛事- 街景字符编码识别

官方链接: https://tianchi.aliyun.com/competition/entrance/531795/introduction

Task1:赛题理解

赛题数据:
赛题来源自Google街景图像中的门牌号数据集(The Street View House Numbers Dataset, SVHN),并根据一定方式采样得到比赛数据集。
该数据来自真实场景的门牌号。训练集数据包括3W张照片,验证集数据包括1W张照片,每张照片包括颜色图像和对应的编码类别和具体位置;为了保证比赛的公平性,测试集A包括4W张照片,测试集B包括4W张照片。
需要注意的是本赛题需要选手识别图片中所有的字符,为了降低比赛难度,我们提供了训练集、验证集和测试集中字符的位置框。


数据集样本展示

官方提供的数据


官方提供的数据
Field Description
top 坐标左上角X
height 字符高度
left 左上角最表Y
width 字符宽度
label 字符编码

json数据

"000000.png": {
  "height": [219, 219],     #字符高度
  "label": [1, 9],          #字符编码
  "left": [246, 323],       #左上角最表Y
   "top": [77, 81],         #坐标左上角X
   "width": [81, 96]        #字符宽度
}, 
.....

评价指标:

 选手提交结果与实际图片的编码进行对比,以编码整体识别准确率为评价指标。
 任何一个字符错误都为错误,最终评测指标结果越大越好,具体计算公式如下:
 Score=编码识别正确的数量/测试集图片数量

file_name, file_code
0010000.jpg,451
0010001.jpg,232
0010002.jpg,45
0010003.jpg,67
0010004.jpg,191
0010005.jpg,892

思路:参考他人https://blog.csdn.net/qq_40317204/article/details/106218501?fps=1&locationNum=2

思路一:定长字符识别(入门思路)

可将赛题抽象为定长字符识别问题,将识别长度定为数据集中最长的字符长度,对于达不到识别长度的字符通过进行填充:


思路二:不定长字符识别(专业字符识别思路)

在字符识别问题当中,有特定的研究方法,典型的有CRNN字符识别模型。

思路三:先检测再识别(专业分类问题思路)

由于本次数据集中已经给出字符框的位置和大小,无需检测,只需识别即可。
但是对于专业的分类问题,总要先构建(字符)检测模型,再构建识别模型。典型物体检测模型有SSD或YOLO。


Task2:数据读取与数据扩增

2.1 工具

  1. Python
  2. Pytorch
  3. Pillow (图像读取操作)
  4. OpenCV (图像读取操作)

2.2 图像处理

Pillow

Pillow是Python图像处理函数库PIL的一个分支,Pillow提供了常见的图像读取和处理的操作,而且可以与ipython notebook无缝集成,是应用比较广泛的库

from PIL import Image
from matplotlib import pyplot as plt

im = Image.open('/Users/pqq/Desktop/CV/data/mchar_train/000012.png')
plt.imshow(im)
plt.show()
000012.png

应用模糊滤镜
首先可以利用系统自带的画图工具转为jpg格式
实现应用模糊滤镜

from PIL import Image
from matplotlib import pyplot as plt

im2 = Image.open('/Users/pqq/Desktop/CV/data/mchar_train/000012.png')
im2 = im.filter(ImageFilter.BLUR)
im2.save('2.jpg','jpeg')
plt.imshow(im2)
图片格式转换

图片放缩

im.thumbnail((w//2,h//2))
OpenCV

OpenCV是一个跨平台的计算机视觉库,最早由Intel开源得来,拥有众多的计算机视觉、数字图像处理和机器视觉等功能。OpenCV在功能上比Pillow更强大

import cv2
# 导入Opencv库
img = cv2.imread(‘cat.jpg’)
# Opencv默认颜色通道顺序是BRG,转换一下
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
import cv2

img = cv2.imread(‘cat.jpg’)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 转换为灰度图
import cv2

img = cv2.imread(‘cat.jpg’)
img =cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 转换为灰度图
# Canny边缘检测
edges = cv2.Canny(img, 30, 70)
cv2.imwrite(‘canny.jpg’, edges)

OpenCV包含了众多的图像处理的功能,OpenCV包含了你能想得到的只要与图像相关的操作。此外OpenCV还内置了很多的图像特征处理算法,如关键点检测、边缘检测和直线检测等。
OpenCV官网:https://opencv.org/
OpenCV Github:https://github.com/opencv/opencv
OpenCV 扩展算法库:https://github.com/opencv/opencv_contrib

2.3 数据拓展

在深度学习中数据扩增方法非常重要,数据扩增可以增加训练集的样本,同时也可以有效缓解模型过拟合的情况,也可以给模型带来的更强的泛化能力。

  • 数据扩增为什么有用?
    在深度学习模型的训练过程中,数据扩增是必不可少的环节。现有深度学习的参数非常多,一般的模型可训练的参数量基本上都是万到百万级别,而训练集样本的数量很难有这么多。
    其次数据扩增可以扩展样本空间,假设现在的分类模型需要对汽车进行分类,左边的是汽车A,右边为汽车B。如果不使用任何数据扩增方法,深度学习模型会从汽车车头的角度来进行判别,而不是汽车具体的区别。


  • 有哪些数据扩增方法?

数据扩增方法有很多:从颜色空间、尺度空间到样本空间,同时根据不同任务数据扩增都有相应的区别。
对于图像分类,数据扩增一般不会改变标签;
对于物体检测,数据扩增会改变物体坐标位置;
对于图像分割,数据扩增会改变像素标签。

2.3.2 常见数据拓展方法

在常见的数据扩增方法中,一般会从图像颜色、尺寸、形态、空间和像素等角度进行变换。当然不同的数据扩增方法可以自由进行组合,得到更加丰富的数据扩增方法。

以torchvision为例,常见的数据扩增方法包括:

  1. transforms.CenterCrop 对图片中心进行裁剪
  2. transforms.ColorJitter 对图像颜色的对比度、饱和度和零度进行变换
  3. transforms.FiveCrop 对图像四个角和中心进行裁剪得到五分图像
  4. transforms.Grayscale 对图像进行灰度变换
  5. transforms.Pad 使用固定值进行像素填充
  6. transforms.RandomAffine 随机仿射变换
  7. transforms.RandomCrop 随机区域裁剪
  8. transforms.RandomHorizontalFlip 随机水平翻转
  9. transforms.RandomRotation 随机旋转
  10. transforms.RandomVerticalFlip 随机垂直翻转
2.3.3 常见数据拓展库
  • torchvision

    https://github.com/pytorch/vision
    pytorch官方提供的数据扩增库,提供了基本的数据数据扩增方法,可以无缝与torch进行集成;但数据扩增方法种类较少,且速度中等;

  • imgaug

    https://github.com/aleju/imgaug
    imgaug是常用的第三方数据扩增库,提供了多样的数据扩增方法,且组合起来非常方便,速度较快;

  • albumentations

    https://albumentations.readthedocs.io
    是常用的第三方数据扩增库,提供了多样的数据扩增方法,对图像分类、语义分割、物体检测和关键点检测都支持,速度较快。

2.4 Pytorch读取数据

import os, sys, glob, shutil, json
import cv2

from PIL import Image
import numpy as np

import torch
from torch.utils.data.dataset import Dataset
import torchvision.transforms as transforms

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')

        if self.transform is not None:
            img = self.transform(img)
        
        # 原始SVHN中类别10为数字0
        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)

train_path = glob.glob('../input/train/*.png')
train_path.sort()
train_json = json.load(open('../input/train.json'))
train_label = [train_json[x]['label'] for x in train_json]

data = SVHNDataset(train_path, train_label,
          transforms.Compose([
              # 缩放到固定尺寸
              transforms.Resize((64, 128)),

              # 随机颜色变换
              transforms.ColorJitter(0.2, 0.2, 0.2),

              # 加入随机旋转
              transforms.RandomRotation(5),

              # 将图片转换为pytorch 的tesntor
              # transforms.ToTensor(),

              # 对图像像素进行归一化
              # transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
            ]))

Dataset:对数据集的封装,提供索引方式的对数据样本进行读取
DataLoder:对Dataset进行封装,提供批量读取的迭代读取

加入DataLoder后,数据读取代码改为如下:

import os, sys, glob, shutil, json
import cv2

from PIL import Image
import numpy as np

import torch
from torch.utils.data.dataset import Dataset
import torchvision.transforms as transforms

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')

        if self.transform is not None:
            img = self.transform(img)
        
        # 原始SVHN中类别10为数字0
        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)

train_path = glob.glob('../input/train/*.png')
train_path.sort()
train_json = json.load(open('../input/train.json'))
train_label = [train_json[x]['label'] for x in train_json]

train_loader = torch.utils.data.DataLoader(
        SVHNDataset(train_path, train_label,
                   transforms.Compose([
                       transforms.Resize((64, 128)),
                       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])
            ])), 
    batch_size=10, # 每批样本个数
    shuffle=False, # 是否打乱顺序
    num_workers=10, # 读取的线程个数
)

for data in train_loader:
    break

Task3: 字符识别模型

CNN:卷积 图像 (本章学习内容)
RNN:递归 时间序列

CNN介绍

卷积神经网络(简称CNN)是一类特殊的人工神经网络,是深度学习中重要的一个分支。CNN在很多领域都表现优异,精度和速度比传统计算学习算法高很多。特别是在计算机视觉领域,CNN是解决图像分类、图像检索、物体检测和语义分割的主流模型。

CNN每一层由众多的卷积核组成,每个卷积核对输入的像素进行卷积操作,得到下一次的输入。随着网络层的增加卷积核会逐渐扩大感受野,并缩减图像的尺寸。

CNN是一种层次模型,输入的是原始的像素数据。CNN通过卷积(convolution)、池化(pooling)、非线性激活函数(non-linear activation function)和全连接层(fully connected layer)构成。

如下图所示为LeNet网络结构,是非常经典的字符识别模型。两个卷积层,两个池化层,两个全连接层组成。卷积核都是5×5,stride=1,池化层使用最大池化。

通过多次卷积和池化,CNN的最后一层将输入的图像像素映射为具体的输出。如在分类任务中会转换为不同类别的概率输出,然后计算真实标签与CNN模型的预测结果的差异,并通过反向传播更新每层的参数,并在更新完成后再次前向传播,如此反复直到训练完成 。

与传统机器学习模型相比,CNN具有一种端到端(End to End)的思路。在CNN训练的过程中是直接从图像像素到最终的输出,并不涉及到具体的特征提取和构建模型的过程,也不需要人工的参与。

CNN发展

随着网络结构的发展,研究人员最初发现网络模型结构越深、网络参数越多模型的精度更优。比较典型的是AlexNet、VGG、InceptionV3和ResNet的发展脉络。



LeNet-5(1998)



AlexNet(2012)

VGG-16(2014)


Inception-v1 (2014)


ResNet-50 (2015)


Pytorch构建CNN模型

在Pytorch中构建CNN模型非常简单,只需要定义好模型的参数和正向传播即可,Pytorch会根据正向传播自动计算反向传播。

在本章我们会构建一个非常简单的CNN,然后进行训练。这个CNN模型包括两个卷积层,最后并联6个全连接层进行分类。

import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
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

# 定义模型
class SVHN_Model1(nn.Module):
    def __init__(self):
        super(SVHN_Model1, self).__init__()
        # CNN提取特征模块
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2)),
            nn.ReLU(),  
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2)),
            nn.ReLU(), 
            nn.MaxPool2d(2),
        )
        # 
        self.fc1 = nn.Linear(32*3*7, 11)
        self.fc2 = nn.Linear(32*3*7, 11)
        self.fc3 = nn.Linear(32*3*7, 11)
        self.fc4 = nn.Linear(32*3*7, 11)
        self.fc5 = nn.Linear(32*3*7, 11)
        self.fc6 = nn.Linear(32*3*7, 11)
    
    def forward(self, img):        
        feat = self.cnn(img)
        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)
        c6 = self.fc6(feat)
        return c1, c2, c3, c4, c5, c6
    
model = SVHN_Model1()

训练代码

# 损失函数
criterion = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(model.parameters(), 0.005)

loss_plot, c0_plot = [], []
# 迭代10个Epoch
for epoch in range(10):
    for data in train_loader:
        c0, c1, c2, c3, c4, c5 = model(data[0])
        loss = criterion(c0, data[1][:, 0]) + \
                criterion(c1, data[1][:, 1]) + \
                criterion(c2, data[1][:, 2]) + \
                criterion(c3, data[1][:, 3]) + \
                criterion(c4, data[1][:, 4]) + \
                criterion(c5, data[1][:, 5])
        loss /= 6
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        loss_plot.append(loss.item())
        c0_plot.append((c0.argmax(1) == data[1][:, 0]).sum().item()*1.0 / c0.shape[0])
        
    print(epoch)

训练完成后我们可以将训练过程中的损失和准确率进行绘制,如下图所示。从图中可以看出模型的损失在迭代过程中逐渐减小,字符预测的准确率逐渐升高。


Task4:模型训练与验证

结合上节知识(构建了一个简单地CNN),本章深度学习加深以下几个方面:

  1. 在训练集上进行训练,并在验证集上进行验证;
  2. 模型可以保存最优的权重,并读取权重;
  3. 记录下训练集和验证集的精度,便于调参。

4.1 构造验证集

知识:
过拟合:过拟合是指为了得到一致假设而使假设变得过度严格。


在机器学习模型(特别是深度学习模型)的训练过程中,模型是非常容易过拟合的。深度学习模型在不断的训练过程中训练误差会逐渐降低,但测试误差的走势则不一定。

在模型的训练过程中,模型只能利用训练数据来进行训练,模型并不能接触到测试集上的样本。因此模型如果将训练集学的过好,模型就会记住训练样本的细节,导致模型在测试集的泛化效果较差,这种现象称为过拟合(Overfitting)。与过拟合相对应的是欠拟合(Underfitting),即模型在训练集上的拟合效果较差。



如图所示:随着模型复杂度和模型训练轮数的增加,CNN模型在训练集上的误差会降低,但在测试集上的误差会逐渐降低,然后逐渐升高,而我们为了追求的是模型在测试集上的精度越高越好。

在一般情况下,参赛选手也可以自己在本地划分出一个验证集出来,进行本地验证。训练集、验证集和测试集分别有不同的作用:

  • 训练集(Train Set):模型用于训练和调整模型参数;

  • 验证集(Validation Set):用来验证模型精度和调整模型超参数;

  • 测试集(Test Set):验证模型的泛化能力。

验证集的划分:


  • 留出法(Hold-Out)

    直接将训练集划分成两部分,新的训练集和验证集。这种划分方式的优点是最为直接简单;缺点是只得到了一份验证集,有可能导致模型在验证集上过拟合。留出法应用场景是数据量比较大的情况。

  • 交叉验证法

    将训练集划分成K份,将其中的K-1份作为训练集,剩余的1份作为验证集,循环K训练。这种划分方式是所有的训练集都是验证集,最终模型验证精度是K份平均得到。这种方式的优点是验证集精度比较可靠,训练K次可以得到K个有多样性差异的模型;CV验证的缺点是需要训练K次,不适合数据量很大的情况。

  • 自助采样法(BootStrap)

    通过有放回的采样方式得到新的训练集和验证集,每次的训练集和验证集都是有区别的。这种划分方式一般适用于数据量较小的情况。

4.2 模型的训练与验证

train_loader = torch.utils.data.DataLoader(
    train_dataset,
    batch_size=10,
    shuffle=True,
    num_workers=10,
)
val_loader = torch.utils.data.DataLoader(
    val_dataset,
    batch_size=10,
    shuffle=False,
    num_workers=10,
)
model = SVHN_Model1()
criterion = nn.CrossEntropyLoss (size_average=False)
optimizer = torch.optim.Adam(model.parameters(), 0.001)
best_loss = 1000.0
for epoch in range(20):
    print('Epoch: ', epoch)
train(train_loader, model, criterion, optimizer, epoch)
val_loss = validate(val_loader, model, criterion)
# 记录下验证集精度
if val_loss < best_loss:
    best_loss = val_loss
    torch.save(model.state_dict(), './model.pt')
 

每个epoch的训练代码:

def train(train_loader, model, criterion, optimizer, epoch): # 切换模型为训练模式
model.train()
            for i, (input, target) in enumerate(train_loader):
                c0, c1, c2, c3, c4, c5 = model(data[0])
                loss = criterion(c0, data[1][:, 0]) + \
                        criterion(c1, data[1][:, 1]) + \
                        criterion(c2, data[1][:, 2]) + \
                        criterion(c3, data[1][:, 3]) + \
                        criterion(c4, data[1][:, 4]) + \
                        criterion(c5, data[1][:, 5])
                loss /= 6
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

每个epoch的训练代码:

def validate(val_loader, model, criterion): # 切换模型为预测模型
model.eval()
val_loss = []
# 不不记录模型梯度信息 with torch.no_grad():
                for i, (input, target) in enumerate(val_loader):
                    c0, c1, c2, c3, c4, c5 = model(data[0])
                    loss = criterion(c0, data[1][:, 0]) + \
                            criterion(c1, data[1][:, 1]) + \
                            criterion(c2, data[1][:, 2]) + \
                            criterion(c3, data[1][:, 3]) + \
                            criterion(c4, data[1][:, 4]) + \
                            criterion(c5, data[1][:, 5])
                    loss /= 6
        val_loss.append(loss.item())
return np.mean(val_loss)

补充知识点:
Epoch, Batch, Iteration

  1. epoch:训练时,所有训练数据集都训练过一次。
  2. batch_size:在训练集中选择一组样本用来更新权值。1个batch包含的样本的数目,通常设为2的n次幂,常用的包括64,128,256。 网络较小时选用256,较大时选用64。
  3. iteration​:训练时,1个batch训练图像通过网络训练一次​(一次前向传播+一次后向传播),每迭代一次权重更新一次;测试时,1个batch测试图像通过网络一次​(一次前向传播)。所谓iterations就是完成一次epoch所需的batch个数



    计算示例

4.3 模型保存和加载

torch.save(model_object.state_dict(), 'model.pt') 
model.load_state_dict(torch.load(' model.pt'))

4.4 调参

深度学习原理少但实践性非常强,基本上很多的模型的验证只能通过训练来完成。同时深度学习有众多的网络结构和超参数,因此需要反复尝试。训练深度学习模型需要GPU的硬件支持,也需要较多的训练时间,如何有效的训练深度学习模型逐渐成为了一门学问。

深度学习有众多的训练技巧,比较推荐的阅读链接有:

  • http://lamda.nju.edu.cn/weixs/project/CNNTricks/CNNTricks.html
  • http://karpathy.github.io/2019/04/25/recipe/

本节挑选了常见的一些技巧来讲解,并针对本次赛题进行具体分析。与传统的机器学习模型不同,深度学习模型的精度与模型的复杂度、数据量、正则化、数据扩增等因素直接相关。所以当深度学习模型处于不同的阶段(欠拟合、过拟合和完美拟合)的情况下,大家可以知道可以什么角度来继续优化模型。

在参加本次比赛的过程中,我建议大家以如下逻辑完成:

  • 1.初步构建简单的CNN模型,不用特别复杂,跑通训练、验证和预测的流程;

  • 2.简单CNN模型的损失会比较大,尝试增加模型复杂度,并观察验证集精度;

  • 3.在增加模型复杂度的同时增加数据扩增方法,直至验证集精度不变。

Task5:模型集成

5.1 集成学习方法

在机器学习中的集成学习可以在一定程度上提高预测精度,常见的集成学习方法有Stacking、Bagging和Boosting,同时这些集成学习方法与具体验证集划分联系紧密。
由于深度学习模型一般需要较长的训练周期,如果硬件设备不允许建议选取留出法,如果需要追求精度可以使用交叉验证的方法。
下面假设构建了10折交叉验证,训练得到10个CNN模型。


那么在10个CNN模型可以使用如下方式进行集成:
对预测的结果的概率值进行平均,然后解码为具体字符;
对预测的字符进行投票,得到最终字符。

5.2 深度学习中的集成学习

5.3.1 Dropout

Dropout可以作为训练深度神经网络的一种技巧。在每个训练批次中,通过随机让一部分的节点停止工作。同时在预测的过程中让所有的节点都其作用。


Dropout经常出现在在先有的CNN网络中,可以有效的缓解模型过拟合的情况,也可以在预测时增加模型的精度。

加入Dropout后的网络结构如下

# 定义模型
class SVHN_Model1(nn.Module):
    def __init__(self):
        super(SVHN_Model1, self).__init__()
        # CNN提取特征模块
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2)),
            nn.ReLU(),
            nn.Dropout(0.25),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2)),
            nn.ReLU(), 
            nn.Dropout(0.25),
            nn.MaxPool2d(2),
        )
        # 
        self.fc1 = nn.Linear(32*3*7, 11)
        self.fc2 = nn.Linear(32*3*7, 11)
        self.fc3 = nn.Linear(32*3*7, 11)
        self.fc4 = nn.Linear(32*3*7, 11)
        self.fc5 = nn.Linear(32*3*7, 11)
        self.fc6 = nn.Linear(32*3*7, 11)

    def forward(self, img):        
        feat = self.cnn(img)
        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)
        c6 = self.fc6(feat)
        return c1, c2, c3, c4, c5, c6

5.3.2 TTA

测试集数据扩增(Test Time Augmentation,简称TTA)也是常用的集成学习技巧,数据扩增不仅可以在训练时候用,而且可以同样在预测时候进行数据扩增,对同一个样本预测三次,然后对三次结果进行平均。

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):
                c0, c1, c2, c3, c4, c5 = model(data[0])
                output = np.concatenate([c0.data.numpy(), c1.data.numpy(),
                   c2.data.numpy(), c3.data.numpy(),
                   c4.data.numpy(), c5.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

5.3.3 Snapshot

假设我们训练了10个CNN则可以将多个模型的预测结果进行平均。但是加入只训练了一个CNN模型,如何做模型集成呢?

在论文Snapshot Ensembles中,作者提出使用cyclical learning rate进行训练模型,并保存精度比较好的一些checkopint,最后将多个checkpoint进行模型集成。


由于在cyclical learning rate中学习率的变化有周期性变大和减少的行为,因此CNN模型很有可能在跳出局部最优进入另一个局部最优。在Snapshot论文中作者通过使用表明,此种方法可以在一定程度上提高模型精度,但需要更长的训练时间。


5.4 结果后处理

在不同的任务中可能会有不同的解决方案,不同思路的模型不仅可以互相借鉴,同时也可以修正最终的预测结果。

在本次赛题中,可以从以下几个思路对预测结果进行后处理:

统计图片中每个位置字符出现的频率,使用规则修正结果;
单独训练一个字符长度预测模型,用来预测图片中字符个数,并修正结果。

5.5 本章小节

在本章中我们讲解了深度学习模型做集成学习的各种方法,并以此次赛题为例讲解了部分代码。以下几点需要同学们注意:

集成学习只能在一定程度上提高精度,并需要耗费较大的训练时间,因此建议先使用提高单个模型的精度,再考虑集成学习过程;
具体的集成学习方法需要与验证集划分方法结合,Dropout和TTA在所有场景有可以起作用。

你可能感兴趣的:(零基础入门CV赛事- 街景字符编码识别)