【精簡教程版】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賽事

相關文章
相關標籤/搜索