【精简教程版】100行代码入手天池CV赛事

本文针对阿里天池《零基础入门CV赛事-街景字符编码识别》,给出了百行代码Baseline,帮助cv学习者更好地结合赛事实践。同时,从赛题数据分析和解题思路分析两方面进行了详细的解读,以便于你们进阶学习。算法

数据及背景

https://tianchi.aliyun.com/competition/entrance/531795/information(阿里天池-零基础入门CV赛事json

百行Baseline

Baseline以定长字符识别为解题思路,进行了必要的注释和代码实现,分数在0.6左右,运用时长:CPU大约须要2小时,GPU大约10分钟。网络

import glob, json
from PIL import Image
from tqdm import tqdm
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.models as models
import torchvision.transforms as transforms 
from torch.utils.data.dataset import Dataset
class SVHNDataset(Dataset):
    def __init__(self, img_path, img_label, transform=None):
        self.img_path, self.img_label, self.transform = img_path, img_label, transform

    def __getitem__(self, index):
        img = Image.open(self.img_path[index]).convert('RGB') # 读取数据
        img = self.transform(img) # 作相应变换
        if self.img_label:      
            lbl = np.array(self.img_label[index], dtype=np.int) # 制做标签
            lbl = list(lbl)  + (5 - len(lbl)) * [10] # 标签长度少于五的用10来填充
            return img, torch.from_numpy(np.array(lbl[:5]))
        else:
            return img

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

# 定义模型
class SVHN_Model1(nn.Module):
    def __init__(self):
        super(SVHN_Model1, self).__init__()
        self.cnn = models.resnet50(pretrained=True) # 加载resnet50
        self.cnn.avgpool = nn.AdaptiveAvgPool2d(1) # 将平均池化改成自适应平均池化
        self.cnn = nn.Sequential(*list(self.cnn.children())[:-1]) # 去除最后的线性层
        self.fc1,self.fc2,self.fc3 = nn.Linear(2048, 11), nn.Linear(2048, 11), nn.Linear(2048, 11)
        self.fc4,self.fc5 = nn.Linear(2048, 11), nn.Linear(2048, 11)

    def forward(self, img):        
        feat = self.cnn(img)
        feat = feat.view(feat.shape[0], -1)
        c1,c2,c3 = self.fc1(feat), self.fc2(feat), self.fc3(feat)
        c4,c5 = self.fc4(feat), self.fc5(feat)
        return c1, c2, c3, c4, c5

def train(train_loader, model, criterion, optimizer):
    model.train() # 切换模型为训练模式
    train_loss = []
    for input, target in tqdm(train_loader): # 取出数据与对应标签
        if use_cuda: # 若是是gpu版本
            input, target = input.cuda(), target.cuda()
        target = target.long()
        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
        optimizer.zero_grad() # 梯度清零
        loss.backward() # 反向传播
        optimizer.step() # 参数更新
        train_loss.append(loss.item())
    return np.mean(train_loss)

def predict(test_loader, model):
    model.eval() # 切换模型为预测模型
    test_pred = []  
    with torch.no_grad(): # 不记录模型梯度信息
        for input in tqdm(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(), 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) # 将每一个batch的结果垂直堆起来
    return test_pred

train_path, test_path = glob.glob('../input/train/*.png'), glob.glob('../input/test_a/*.png') # 读取训练数据和测试数据
train_path.sort(); test_path.sort()

train_json = json.load(open('../input/train.json'))  #读取训练集标注文件
train_label = [train_json[x]['label'] for x in train_json] # 拿出训练集的标签

trans_fun = transforms.Compose([
                transforms.Resize((64, 128)), # 将图片裁剪为64*128
                transforms.ToTensor(),  #转为Tensor
                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 标准化
])

train_loader = torch.utils.data.DataLoader(
    SVHNDataset(train_path, train_label, trans_fun), 
    batch_size=40, shuffle=True) # 批量大小40,打乱顺序

test_loader = torch.utils.data.DataLoader(
    SVHNDataset(test_path, [], trans_fun), 
    batch_size=40, shuffle=False)

model = SVHN_Model1()
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数 
optimizer = torch.optim.Adam(model.parameters(), 0.001) # Adam优化器

use_cuda = torch.cuda.is_available()
if use_cuda: model = model.cuda()

for epoch in range(10):
    train_loss = train(train_loader, model, criterion, optimizer) # 训练
    print(epoch, train_loss)    
test_predict_label = predict(test_loader, model)
test_predict_label = np.vstack([
    test_predict_label[:, :11].argmax(1), test_predict_label[:, 11:22].argmax(1),
    test_predict_label[:, 22:33].argmax(1), test_predict_label[:, 33:44].argmax(1),
    test_predict_label[:, 44:55].argmax(1),
]).T

test_label_pred = []
for x in test_predict_label:
    test_label_pred.append(''.join(map(str, x[x!=10]))) # 取出预字符不为10的字符且顺序排列

df_submit = pd.read_csv('../input/sample_submit_A.csv')
df_submit['file_code'] = test_label_pred
df_submit.to_csv('submit.csv', index=None) # 保存结果文件

数据解读

以街道字符为赛题数据,该数据来自收集的SVHN街道字符,并进行了匿名采样处理。 app

【精简教程版】100行代码入手天池CV赛事
训练集数据包括3W张照片,验证集数据包括1W张照片,每张照片包括颜色图像和对应的编码类别和具体位置。
须要注意的是本赛题须要选手识别图片中全部的字符,为了下降比赛难度,比赛提供了训练集、验证集和测试集中全部字符的位置框。框架

数据标签

对于训练数据每张图片将给出对于的编码标签,和具体的字符框的位置(训练集、测试集和验证集都给出字符位置),可用于模型训练:
【精简教程版】100行代码入手天池CV赛事
字符的坐标具体以下所示:
【精简教程版】100行代码入手天池CV赛事
在比赛数据(训练集、测试集和验证集)中,同一张图片中可能包括一个或者多个字符,所以在比赛数据的JSON标注中,会有两个字符的边框信息:
【精简教程版】100行代码入手天池CV赛事ide

数据读取

为了方便你们进行数据读取,在此给出JSON中标签的读取方式:函数

import json
train_json = json.load(open('../input/train.json'))

# 数据标注处理
def parse_json(d):
    arr = np.array([
        d['top'], d['height'], d['left'],  d['width'], d['label']
    ])
    arr = arr.astype(int)
    return arr

img = cv2.imread('../input/train/000000.png')
arr = parse_json(train_json['000000.png'])

plt.figure(figsize=(10, 10))
plt.subplot(1, arr.shape[1]+1, 1)
plt.imshow(img)
plt.xticks([]); plt.yticks([])

for idx in range(arr.shape[1]):
    plt.subplot(1, arr.shape[1]+1, idx+2)
    plt.imshow(img[arr[0, idx]:arr[0, idx]+arr[1, idx],arr[2, idx]:arr[2, idx]+arr[3, idx]])
    plt.title(arr[4, idx])
    plt.xticks([]); plt.yticks([])

结果示例:
【精简教程版】100行代码入手天池CV赛事学习

进阶学习(3种思路总结)

赛题本质是分类问题,须要对图片的字符进行识别。但赛题给定的数据,不一样图片中包含的字符数量不相同。以下图所示,有的图片的字符个数为2,有的图片字符个数为3,有的图片字符个数为4。
【精简教程版】100行代码入手天池CV赛事
所以本次赛题的难点是须要对不定长的字符进行识别,与传统的图像分类任务有所不一样。下文将给出三种难度从低到高的解决思路,具体以下:测试

解题思路1:定长字符识别

能够将赛题抽象为一个定长字符识别问题,在赛题数据集中大部分图像中字符个数为2-4个,最多的字符个数为6个。所以能够对于全部的图像都抽象为6个字符的识别问题,字符23填充为23XXXX,字符241填充为241XXX。优化

【精简教程版】100行代码入手天池CV赛事
通过填充以后,原始的赛题能够简化了6个字符的分类问题。在每一个字符的分类中会进行11个类别的分类:0-9及空字符,假如分类为填充字符,则代表该字符为空。
下文将给出本思路的具体Baseline实现,先来看一下Baseline中的检测框架ResNet18。
ResNet系列的每一个网络都包括三个主要部分:输入部分、输出部分和中间卷积部分(中间卷积部分包括如图所示的Stage1到Stage4共计四个stage)。下图表示的是ResNet系列的结构:
【精简教程版】100行代码入手天池CV赛事

  1. 网络输入部分
    全部的ResNet网络输入部分是一个size=7 7, stride=2的大卷积核,以及一个size=3 3, stride=2的最大池化组成。经过这一步,一个224 224的输入图像就会变56 56大小的特征图,极大减小了存储所需大小。
  2. 网络中间卷积部分
    中间卷积部分经过3 * 3卷积的堆叠来实现信息的提取。上图中每一个矩阵后乘的数字,就是后面的列[2, 2, 2, 2]和[3, 4, 6, 3]等则表明了bolck的重复堆叠次数。
  3. 残差块实现
    输入数据分红两条路,一条路通过两个3 * 3卷积,另外一条路直接短接,两者相加通过relu输出。
  4. 网络输出部分
    网络输出部分很简单,经过全局自适应平滑池化,把全部的特征图拉成1 1,对于res18来讲,就是1 512 7 7 的输入数据拉成 1 512 1 * 1,而后接全链接层输出,输出节点个数与预测类别个数一致。

    解题思路2:不定长字符识别

    在字符识别研究中,有特定的方法来解决此种不定长的字符识别问题,比较典型的有CRNN字符识别模型。在本次赛题中给定的图像数据都比较规整,能够视为一个单词或者一个句子。

【精简教程版】100行代码入手天池CV赛事
CRNN网络结构能够分为三个部分:特征提取、序列建模、转录

  1. 特征提取(Convolutional Layers)
    这里的卷积层就是一个普通的CNN网络,用于提取输入图像的Convolutional feature maps,即将大小为(32,100,3)的图像转换为 (1,25,100)大小的卷积特征矩阵。
  2. 序列建模(Recurrent Layers)
    这里的循环网络层是一个深层双向LSTM网络,在卷积特征的基础上继续提取文字序列特征。RNN循环神经网络有如下两个优点:
    • 让网络能够结合上下文进行识别
    • 反传时将残差传给输入层。
  3. 转录(Transcription Layer)
    将RNN输出作softmax后,为字符输出。
    【精简教程版】100行代码入手天池CV赛事
    CRNN有几个优势:
    • 它能够对整个文本进行识别,不定长
    • 这个网络不在意文字大小
    • 能够本身添加词库
    • 生成的模型小,准确率提升
      这边要注意的是,CRNN没有全链接层,由于它就是要将特征提取后进行对比,若是有全链接层,就会把一些特征归一化(去掉了),会影响后面找出可能字符的过程。

解题思路3:检测再识别

在赛题数据中已经给出了训练集、验证集中全部图片中字符的位置,所以能够首先将字符的位置进行识别,利用物体检测的思路完成。
【精简教程版】100行代码入手天池CV赛事
此种思路须要参赛选手构建字符检测模型,对测试集中的字符进行识别。选手能够参考物体检测模型SSD或者YOLO来完成。为方便你们学习,对两种算法进行简单的介绍。

1. SSD算法

是一种直接预测目标类别和bounding box的多目标检测算法。与faster rcnn相比,该算法没有生成 proposal 的过程,这就极大提升了检测速度。
针对不一样大小的目标检测,传统的作法是先将图像转换成不一样大小(图像金字塔),而后分别检测,最后将结果综合起来(NMS)。而SSD算法则利用不一样卷积层的 feature map 进行综合也能达到一样的效果。
算法的主网络结构是VGG16,将最后两个全链接层改为卷积层,并随后增长了4个卷积层来构造网络结构。
【精简教程版】100行代码入手天池CV赛事

2. YOLO(实时快速目标检测)

You Only Look Once: Unified, Real-Time Object Detection,是Joseph Redmon和Ali Farhadi等人于2015年提出的基于单个神经网络的目标检测系统。
YOLO是一个能够一次性预测多个Box位置和类别的卷积神经网络,可以实现端到端的目标检测和识别,其最大的优点就是速度快。事实上,目标检测的本质就是回归,所以一个实现回归功能的CNN并不须要复杂的设计过程。
YOLO没有选择滑动窗口(silding window)或提取proposal的方式训练网络,而是直接选用整图训练模型。这样作的好处在于能够更好的区分目标和背景区域,相比之下,采用proposal训练方式的Fast-R-CNN经常把背景区域误检为特定目标。

【精简教程版】100行代码入手天池CV赛事
延伸阅读:
书籍:《深度实践OCR:基于深度学习的文字识别》
做者:刘树春 阿里巴巴本地生活研究院算法专家,前复旦七牛云联合实验室OCR算法负责人

【精简教程版】100行代码入手天池CV赛事

相关文章
相关标签/搜索