/ 程式機器學習TensorFlow

Keras + TensorFlow:用 CNN 辨識中文手寫數字

TensorFlow 正夯,大家都在寫機器學習;這學期修了一門人工智慧的課,期末是要我們用 Keras + TensorFlow 弄出辨識中文數字的模組。

至於為什麼不是用 tf.js 而是用 Python,沒辦法,還是得拿學分。

收集資料

原本我們是用 MNIST 的資料集做了一次 MLP 跟 CNN,但期末作業是要用中文手寫字。我們不知道哪裡有中文手寫字,所以由強者我學長印出了 150 張的 A4 紙,上面有格子讓大家寫滿數字的一二三四五六七八九十。我們花了一節課寫這些東西,就像回到國小時光。

datasets

然後再由強者我朋友,用了一些什麼 Python + OpenCV 套件,還是手動切出來我也不太清楚,最後弄出了將近兩萬張的 28x28 像素的圖片。
datasets_2

環境建置

這學期初我們花了好幾節課在裝環境,先裝什麼 Anaconda 的,然後再裝了一些套件。每次開啟還要 source activate tensorflow 之類的,對於一個 Python 苦手來說真是蠻困難的。

後來發現東西丟到 Google Colab 上超級方便、超級開心、超級好做,之後推薦大家都在 Google Colaboratory 上做,還附贈免費 GPU 加速。剛開始學這種東西光搞懂那些概念就夠難的了,真的不需要花那麼多時間在自己的筆電上等待運算!

資料預處理

首先要下載資料:我們把檔案都包成 zip 檔,之後再透過 urlib2 下載下來、zipfile 解壓縮。

img_url = 'https://....../processed_img.zip' # your file.

import urllib2
import zipfile

print 'Downloading file...'
downloadurl = urllib2.urlopen(img_url);

zipcontent= downloadurl.read()
with open("processed_img.zip", 'wb') as f:
    f.write(zipcontent)
print 'Extracting file...'
with zipfile.ZipFile(open('processed_img.zip')) as f:
    f.extractall('.')
print 'Done.'
!ls # Google Colab 可以直接用 ! 執行 bash 語法。我不怎麼寫 Python,我不太確定什麼環境可以這樣搞。

由於資料是課堂上大家蒐集的,也不太確定能不能公開,這篇就先不把資料集放出來了。

解壓縮後會看到資料夾下有一個 processed_img.zip 和 processed_img 的資料夾,進到資料夾後會看到大約兩萬張圖片。

接著我們要把檔案都讀進來,然後把圖片轉成陣列,讓電腦運算:

import os
import numpy as np
from sklearn.model_selection import train_test_split
from keras.preprocessing import image
from keras.utils import np_utils, plot_model

def load_data():
    path = './processed_img/'
    files = os.listdir(path)
    images = []
    labels = []
    for f in files:
        img_path = path + f
        img = image.load_img(img_path, grayscale=True, target_size=(28, 28))
        img_array = image.img_to_array(img)
        images.append(img_array)
        
        lb = f.split('-');
        lb = lb[2].split('.jpg')
        lb = lb[0]
        labels.append(lb);

    data = np.array(images)
    labels = np.array(labels)

    return data, labels

print 'Loading data...'
images, lables = load_data()
images /= 255
(x_train, x_test, y_train, y_test) = train_test_split(images, lables, test_size=0.2)
y_train_onehot = np_utils.to_categorical(y_train)
y_test_onehot = np_utils.to_categorical(y_test)

稍微解釋下,我們先把檔案透過 image.load_img 讀進來,由於圖片是灰階的,所以加上 grayscale=True,然後 target_size 指定圖片大小。

lblabels 的部分是在處理圖片標籤。剛剛提到強者我朋友已經把圖片都整理完,而且用 張數-列-標籤.jpg 的方式命名。所以我們只要取出 標籤,就能當作標籤來用。

之後用 sklearn 套件裡的 train_test_split 方法把資料集切成訓練資料集和測試資料集。

最後 np_utils.to)categorical 則是要把標籤處理成 One-hot 格式。

One-hot 是什麼?有興趣可以去看 Yeh James 的 資料分析&機器學習 第2.4講:資料前處理(Missing data, One-hot encoding, Feature Scaling)。這裡借用他示意圖帶過:
onehot

模型建置

由於是圖片分類,這邊選用卷積層模型(CNN)來做。

from keras.models import Sequential, model_from_yaml, load_model
from keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D
from keras.optimizers import Adam, SGD

model = Sequential()
model.add(Conv2D(filters=16, kernel_size=(5, 5), padding='same', input_shape=(28, 28, 1), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Conv2D(filters=36, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
model.summary()

中間加入 Dropout 層是防止 Overfitting,其他部分這邊就不多講為什麼要這樣建立模型。如果對 CNN 模型建立有興趣,可以看看不久前翻譯的 圖像訓練:使用卷積神經網路辨識手寫數字,如果覺得哪裡翻的怪怪的歡迎發 PR XD。

接著用 model.summary 印出模型的摘要:

model_summary

訓練模型

接著要定義優化器和損失函數,這裡使用傳說中很好用的 Adambinary_crossentropy,並定義評估指標 accuracy

sgd = Adam(lr=0.0003)
model.compile(loss='binary_crossentropy',optimizer=sgd, metrics=['accuracy'])

想知道優化器和損失函數是什麼的話,也可以看看這篇 訓練的第一步:從合成數據中訓練曲線

最後把我們剛剛預處理完的資料都丟進去:

train_history = model.fit(x_train, y_train_onehot, batch_size=200, epochs=50, verbose=2, validation_data=(x_test, y_test_onehot))

查看訓練結果

剛剛我們有將訓練過程存到 train_history 裡面,現在我們可以用 matplotlib 將它繪製成圖表:

import matplotlib.pyplot as plt
def show_train_history(train_history, title, train, validation):
    plt.plot(train_history.history[train])
    plt.plot(train_history.history[validation])
    plt.title(title)
    plt.ylabel(train)
    plt.xlabel('Epoch')
    plt.legend(['train', 'validation'], loc = 'upper left')
    plt.show()

show_train_history(train_history, 'Accuracy', 'acc', 'val_acc')
show_train_history(train_history, 'Loss', 'loss', 'val_loss')

可以看到訓練狀況:

train_history

也可以用 pandascrosstable 畫成混淆矩陣:

import pandas as pd
prediction = model.predict_classes(x_test)
pd.crosstab(y_test, prediction, rownames=['label'], colnames=['predict'])

訓練狀況應該還不錯?看起來原本 label 是 9 的樣本預測錯的最多。
cross_table