2017/11/25

秒做 CNN 手寫數字辨識



降低AI開發的技術門檻能收到什麼好處呢?巨人們佈的局是建立新的生態體系(Ecosystem)與盤算將來的規模經濟綜效,而站在巨人肩膀上的你我究竟是看能得更遠?亦或只是最終實現巨人們野心過程被供養的代理人呢?於現在所謂「場景即商機」的時代,巨人們將更容易透過免費送我們玩的工具與我們樂此不疲的活動中洞悉未來更宏觀的趨勢呢!

這個問題還蠻樂觀的!未來跨技術領域的門檻將會越來越低(就像藝術工作者可以輕易的拿Arduino創作出比工科宅更棒的點子),相信跨領域的腦思維震盪定會迸出許多連巨人們都想像不到的火花(所以他們也非常積極地投資各種將來可能威脅到自己的新創)。倘若你滿腦子創新的火苗沒找到出口,跨領域進去半導體顛覆整個產業也蠻不錯的!

搞個BetaGo來做FloorplanP&RDeepCritic來評估製程/原件庫體質與正確的時序約束,DeepCoach來教新人一步一步做實體設計並優化功耗與效能,DeepRecipe來優化產品競爭力並提升產能等等。一點都不誇張,當你深入其中將發現:即使是自稱tier-one Fab所提供的設計方法與原件庫都有太多空間(甚至是bug)可以優化與改善。



深層卷積神經網路(Convolutional Neural Network
我們於前一篇文章「神經網路拼圖」中秒做了一下以MNIST資料集為基礎的手寫字元辨識系統,實作過程雖然如堆積木般簡單,然而實驗結果卻一點也不含糊(辨識率可達到98.3%)。這一次,我們試著再增加兩層卷積層(Convolutional Layer),再來秒做一下卷積神經網路(Convolutional Neural Network, CNN)看看效果如何?

若以Scratch來思考,概念如下圖:



相較前一版專案,所引用的函式庫與秀圖的程式碼沒變,只是增加了引用卷積層所需的函式庫(以藍色字體標示):

from matplotlib import pyplot as plt
from keras.datasets import mnist
from keras.utils import np_utils as kutil
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Conv2D, Flatten, MaxPooling2D

def show_images(img,img_label,imglist):
    gif=plt.gcf()
    gif.set_size_inches(8,10)
    ai=1
    for i in imglist:
        ax=plt.subplot(5,5,ai)
        ai+=1
        ax.set_title('label:'+str(img_label[i]),fontsize=10)
        ax.set_xticks([])
        ax.set_yticks([])
        ax.imshow(img[i],cmap='binary')
    plt.show()




輸入資料正規畫
相較前一版專案,由於前面使用的卷積層其輸入是對二維灰階影像(維度為28x28x1)做處理,因此所載入的資料集維度須做轉換,其它程式碼則不變。

# import MNIST data set
(x_train2D, y_train_label), (x_test2D, y_test_label) = mnist.load_data()
show_images(x_train2D,y_train_label,tuple(range(1,11)))

x_train = x_train2D.reshape(x_train2D.shape[0],28,28,1).astype('float32')
x_test = x_test2D.reshape(x_test2D.shape[0],28,28,1).astype('float32')

# normalization: mean=0, std=1
for i in range(len(x_train)):
    x=x_train[i]
    m=x.mean()
    s=x.std()
    x_train[i]=(x-m)/s
for i in range(len(x_test)):
    x=x_test[i]
    m=x.mean()
    s=x.std()
    x_test[i]=(x-m)/s

# convert label to on-hot encoding
y_train = kutil.to_categorical(y_train_label)
y_test = kutil.to_categorical(y_test_label)




秒做手寫字元辨識系統
所增加的卷積層(Conv2D)包括池化層(Max Pooling)只有短短五行程式碼,其它如損失函數、網路參數的優化與小批次樣本訓練方式都不變。CNN第一階的16個濾波器系數大小為5X5,而第二階的16個濾波器系數大小為3X3,它們初始值(神經元的鍵值)都是隨機給定(訓練過程會逐漸萃取出適應所有訓練樣本的濾波器樣貌)。相較DenseConv2D神經元並不是全連結(fully connected,它是以執行影像卷積運算(convolution)的方式做部份連結並共享部份鍵值。

# CNN handwritten character recognition
model = Sequential()
model.add(Conv2D(
        input_shape=(28,28,1),
        filters=16,kernel_size= (5,5),
        padding='same',
        activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(
        filters=16,kernel_size= (3,3),
        padding='same',
        activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(units=128,activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(units=10,activation='softmax'))

model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])

# model fitting with training set
train = model.fit(x=x_train,y=y_train,validation_split=0.2,epochs=10,batch_size=200,verbose=2)



訓練時同樣將樣本分割出另外20%做為交叉驗證(cross validation)以評估將來實際受測的可能準確率,每次取200筆資料做小批次採樣,並重複整個過程10次(epoch)。下面程式碼把每次遞回的辨識準確度依訓練樣本與驗證樣本各別做圖。

# CNN handwritten character recognition
plt.subplot(1,1,1)
plt.title('Train History')
plt.plot(train.history['acc'],'-o',label='train')
plt.plot(train.history['val_acc'],'-x',label='valid')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.grid()
plt.xticks(list(range(0,10)))
plt.legend()
plt.show()

# model evaluation with test data set
score = model.evaluate(x_test,y_test)





相較前一版專案,引入CNN之後的辨識率可以輕鬆地更進一步提高到99.03%以上了!



卷積層濾波器係數
我們曾經於「機器人(AI)創作」文中解釋過「卷積運算(Convolution)」的物理意義,例如經過高通濾波器的卷積運算會偵測出圖形的輪廓,而低通濾波器會使得輸出圖形變得平滑/模糊。只不過空間濾波器(例如短脈衝響應濾波器)係數是人決定,而CNN的濾波器係數值是透過訓練自動產生(神經網路依據訓練樣本自動調適出可萃取不同特徵的各式濾波器)。

麼?特徵自動萃取?這麼神奇的事值得我們細部研究!我們可以透過下面程式碼偷窺一下CNN所產生的濾波器(其實就隱身在CNN的神經元鍵值)長相:

# Given 16 filters after a specified layer, we can call plot_filters(layer,4,4) to get a 4x4 plot of all filters
def plot_filters(layer,x,y):
    print('filter of {} layer'.format(layer.name))
    filters = layer.get_weights()[0]
    (w,h,_,n)=filters.shape
    for j in range(n):
        ax=plt.subplot(y,x,j+1)
        ax.imshow(filters[:,:,0,j],cmap='binary')
        plt.xticks([])
        plt.yticks([])
    plt.show()
    return plt

plot_filters(model.layers[0],4,4)  # 1st convolution layer
plot_filters(model.layers[2],4,4)  # 2nd convolution layer



CNN萃取出的濾波器長相不難發現,它跟我們人類視覺處理過程有點類似:傾向萃取圖形輪廓的能力。下面是CNN第一階的165X5濾波器,可看出CNN想產生出能判斷這些手寫字元圖形中邊緣輪廓的能力。



我們可以將池化層(max pooling)的物理看成是降低空間解析度的過程(低通又允許保有一些銳利的細節)。而CNN第二階的16個濾波器,由於它的輸入是針對經過池化層(max pooling)的輸出(降維到14X14)做淬取,因此我們可以將其設計為3X3濾波器以節省資源配置。

下面是CNN第二階的濾波器係數(CNN的神經元鍵值),可看出CNN想產生出能判斷一些圖形中斜邊與直線等構造(紋理)的能力。





卷積層濾波器輸出
我們可以試著將濾波器輸出結果顯示出來,以證明前面所說:CNN想產生出能判斷文字圖形輪廓、斜邊或直線等構造的能力。由於CNN置於整個深層神經網路的前兩層,因此我們需要用到一些技巧來把先前訓練完成的網路截取出我們要的部份。此時,整個網路好比是一個model好的一個方程式。



以輸入測試樣本「7」為例,下面程式碼可以簡單的把第一階CNN的方程式截取出來。其中,module.layers[0]表示整個神經網路的第一階層(即CNN layer-1)。由於第一階CNN16個濾波器,其輸出也會有16張經過濾波器的輸出影像。

# CNN model extraction
from keras.models import Model

# output of CNN layer1
fun = Model(inputs=model.layers[0].input,outputs=model.layers[0].output) # extract CNN1 function
out = fun.predict(x_test[0:1])  # input the 1st mage: 7
(_,w,h,n) = out.shape  # (1, 28, 28, 16)
plt.imshow(out[0,:,:,0],cmap='binary') # show the 1st convolution output


根據所輸入的測試樣本,下面程式碼可以更進一步將16張經過濾波器(convolution)的輸出影像以陣列的形式批次顯示。注意,CNN第二階層的截取是從最頂層的輸入開始。

# Given 16 filters after a specified layer, we can get a 4x4 plot of all filters’ output
from keras.models import Model

def plot_cnn_output(dist_layer_id,test,x,y):
    li=model.layers[0]
    lo=model.layers[dist_layer_id]
    print('CNN output of layer {}'.format(lo.name))
    fun = Model(inputs=li.input,outputs=lo.output)  # extract layer function
    out = fun.predict(test)  # generate layet output
    (_,w,h,n) = out.shape
    for j in range(n):
        ax=plt.subplot(y,x,j+1)
        ax.imshow(out[0,:,:,j],cmap='binary')
        plt.xticks([])
        plt.yticks([])
    plt.show()
    return plt

plot_cnn_output(0,x_test[0:1],4,4) # CNN layer-1, given the 1st test image (input=7)
plot_cnn_output(2,x_test[0:1],4,4) # CNN layer-2, given the 1st test image (input=7)



CNN第一階的濾波器輸出(convolution)如預期,有如針對手寫字元圖形中的輪廓進行檢測。
  


CNN第二階的濾波器輸出結果,有如進一步將手寫字元圖形拆解成斜邊與直線等構造。





混淆矩陣分析(Confusion Matrix Analysis
同樣地,我們透過混淆矩陣進一步分析哪些類別最容易被混淆誤判,例如輸出標籤應為「5的類別卻被判定為「3的結果從上一回(DNN7張影像減少為4張(CNN)。

# confusion matrix analysis
import pandas as pd
predict = model.predict_classes(x_test)

cm=pd.crosstab(y_test_label,predict,rownames=['label'],colnames=['predict'])
print('Confusion Matrix:\n{}'.format(cm))







 實驗結果顯示,CNN對數字「5」的辨識能力整體改善了許多。細部看那些剩下來標示為「5」卻被錯誤辨識為「3」的樣本確實不易判讀(有些模稜兩可)。





請注意!雖然CNN整體辨識率提高到99.03%,但對數字「7」的辨識能力反而出現了較高的混淆狀況(被辨識為「2」)。然而細部來看,大部分人類視覺還是區分的出來的!

我們若比喻上一回(DNN是針對輸入圖形中像素的絕對位置硬train,那麼CNN比較像是先將圖形分拆成數個紋理的構造再進行訓練。所以當數字「7」的寫法出現了類似「2」底部的橫向紋理時,CNN反而誤判了!真的是蠻有趣的現象。





這不是AI啊?
目前登陸火星仍然是很遙遠的夢想,但是先放風箏(光帆離子引擎)到火星甚至是太陽系外拍拍照或許是不錯的可行方法。