39從傳統(tǒng)算法到深度學(xué)習(xí):目標(biāo)檢測(cè)入門實(shí)戰(zhàn) --卷積神經(jīng)網(wǎng)絡(luò)基礎(chǔ)

卷積神經(jīng)網(wǎng)絡(luò)

通過(guò)上一節(jié)實(shí)驗(yàn)我們學(xué)習(xí)了神經(jīng)網(wǎng)絡(luò)的基本概念和結(jié)構(gòu),我們了解到神經(jīng)網(wǎng)絡(luò)的輸入層中的每個(gè)節(jié)點(diǎn)都與下一層的每個(gè)節(jié)點(diǎn)相連接,我們稱這種連接方式為全連接(Fully-connected),但是這種全連接方式存在一些明顯的缺陷。首先如果使用全連接網(wǎng)絡(luò)處理圖片的話,需要將圖像矩陣轉(zhuǎn)換為一列向量,這樣就破壞了圖像的空間信息。其次假設(shè)我們將一張尺寸為240×240×3 的三通道圖片作為全連接網(wǎng)絡(luò)的輸入,則在輸入層總共需要 172800個(gè)權(quán)重,如此多的參數(shù)需要很大的計(jì)算量和時(shí)間處理,并且大量的參數(shù)還會(huì)導(dǎo)致過(guò)擬合(模型在訓(xùn)練集上表現(xiàn)好,在測(cè)試集上表現(xiàn)差,泛化能力差)。在計(jì)算機(jī)視覺(jué)中廣泛應(yīng)用的卷積神經(jīng)網(wǎng)絡(luò)可以用來(lái)克服上述問(wèn)題,在卷積神經(jīng)網(wǎng)絡(luò)中我們采取局部連接節(jié)點(diǎn)的方式代替全連接的方式,通常一個(gè)卷積神經(jīng)網(wǎng)絡(luò)由輸入層、卷積層,激活層、池化層和全連接層構(gòu)成。
卷積和卷積核
在開始學(xué)習(xí)卷積神經(jīng)網(wǎng)絡(luò)前,我們需要了解卷積和卷積核(Kernel)的相關(guān)內(nèi)容。通常情況下,深度學(xué)習(xí)中所謂的卷積實(shí)際上是互相關(guān)操作(在后面的內(nèi)容中我將用卷積來(lái)稱呼互相關(guān)操作),如下圖,兩個(gè)矩陣的卷積即是將兩個(gè)矩陣中對(duì)應(yīng)位置的元素相乘再求和,則這兩個(gè)矩陣的卷積結(jié)果是10×1+56×2+34×3+12×4+94×5+16×6+0×7+100×8+11×9=1737。

image.png

現(xiàn)在讓我們來(lái)了解下圖片的卷積是如何操作的,在圖片上進(jìn)行卷積需要用到卷積核(kernel),卷積核實(shí)際上就是一個(gè)矩陣,我們讓這個(gè)矩陣在圖像上從左向右、從上向下滑動(dòng),在滑動(dòng)的過(guò)程中矩陣所覆蓋的區(qū)域內(nèi)的像素值與矩陣內(nèi)元素按位相乘再求和,這些求和結(jié)果組成一個(gè)新的矩陣我們稱之為特征圖(Feature map)。
這個(gè)過(guò)程類似我們學(xué)習(xí)過(guò)的滑動(dòng)窗口,假設(shè)我們使用一個(gè)3×3 的卷積核(下圖中間的矩陣,矩陣中每個(gè)值都為 1/9)對(duì)一張7×7 尺寸的圖片進(jìn)行卷積,那么首先將卷積核的左上角頂點(diǎn)與圖片的左上角頂點(diǎn)重疊,下圖左邊矩陣上的紅色區(qū)域?yàn)橹丿B區(qū)域,然后按位計(jì)算紅色矩形區(qū)域的元素與卷積核中的元素的乘積再將所有乘積結(jié)果求和,就得到了特征圖上的第一個(gè)值為 67,然后按照從左向右、從上向下的順序依次移動(dòng) 1 個(gè)像素的距離(每次移動(dòng)的像素個(gè)數(shù)稱為步長(zhǎng)(Stride),也可以移動(dòng)多個(gè)像素)然后再計(jì)算重疊部分的卷積直到卷積核到達(dá)圖片的左下角(下圖左邊矩陣的左下角虛線框),這樣我們就獲得了一個(gè)5×5 的特征圖。需要注意的是卷積核的尺寸必須是奇數(shù),例如1×1、3×3、5×5 等。
image.png

使用上述卷積方法得到的特征圖尺寸會(huì)縮小,同時(shí)會(huì)丟失圖片的邊緣信息,因?yàn)榫矸e核移動(dòng)到圖片的邊緣就結(jié)束了。為了解決這個(gè)問(wèn)題,我們可以使用填充(Padding)方法,填充就是在圖片外圍填充像素值為 0 的像素點(diǎn)(見(jiàn)下圖最左邊矩陣),然后通過(guò)卷積計(jì)算得到的特征圖尺寸就和輸入圖片的尺寸一樣了。填充不僅可以在卷積過(guò)程中保留圖像邊緣信息,還可以對(duì)不同尺寸的圖片進(jìn)行填充,統(tǒng)一圖片尺寸。
image.png

卷積層
卷積層是卷積神經(jīng)網(wǎng)絡(luò)的最重要組成部分,卷積層就是由不同數(shù)量和尺寸的卷積核構(gòu)成的,其作用是用于圖像的局部特征提取。在卷積神經(jīng)網(wǎng)絡(luò)中我們經(jīng)常會(huì)遇到一個(gè)概念稱為深度(Depth),深度與圖像的通道類似,我們使用一個(gè)卷積核對(duì)圖像進(jìn)行卷積操作后會(huì)得到一個(gè)二維特征圖,這個(gè)特征圖和輸入圖像一樣具有高和寬,使用多個(gè)尺寸相同的卷積核對(duì)輸入圖像進(jìn)行卷積時(shí)我們將得到多個(gè)特征圖,將這些特征圖堆疊起來(lái)將得到一個(gè)三維特征圖,這三個(gè)維度分別對(duì)應(yīng)寬、高和深度,深度值就等于卷積核的個(gè)數(shù)。
如下圖,我們使用 5 個(gè)尺寸相同的卷積核對(duì)圖像進(jìn)行卷積,我們將得到 5 個(gè)特征圖,將這 5 個(gè)特征圖堆疊起來(lái)就是一個(gè)具有寬、高和深度的三維矩陣,這個(gè)矩陣的深度就是 5。提到深度我們還需要了解一個(gè)概念稱為濾波器(Filter),濾波器是由多個(gè)卷積核堆疊而成,其深度是其內(nèi)卷積核的數(shù)量,當(dāng)卷積核的個(gè)數(shù)為 1 時(shí)可以認(rèn)為濾波器等同于卷積核。當(dāng)給網(wǎng)絡(luò)輸入一張 RGB 圖片時(shí),由于圖片有三個(gè)通道,需要用三個(gè)卷積核對(duì)圖片進(jìn)行卷積,這三個(gè)卷積核就構(gòu)成一個(gè)濾波器。
image.png

在卷積神經(jīng)網(wǎng)絡(luò)中我們采取局部連接節(jié)點(diǎn)的方式代替全連接的方式,如下圖,右邊兩個(gè)圓表示神經(jīng)元節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)只與圖片上的部分區(qū)域的像素值(下圖中間的矩陣表示局部像素點(diǎn))連接,這些區(qū)域之外的其他像素值都不會(huì)影響與這個(gè)區(qū)域相連的節(jié)點(diǎn),這些區(qū)域稱為對(duì)應(yīng)節(jié)點(diǎn)的感受野(Receptive field)。如果輸入網(wǎng)絡(luò)的圖像尺寸是16×16×3(圖像的寬、高是 16,通道數(shù)是 3),假設(shè)感受野的尺寸是3×3 那么每個(gè)與這個(gè)區(qū)域連接的節(jié)點(diǎn)將接受3××3×3=27 個(gè)權(quán)重(圖像有三個(gè)通道)。假設(shè)我們輸入的尺寸是 5×5×100 以及感受野是 3×3,則與之相連的節(jié)點(diǎn)所接受的權(quán)重個(gè)數(shù)是3×3×100=900 。
image.png

激活層
激活層在每一個(gè)卷積層后,其作用是引入了非線性因素為節(jié)點(diǎn)建立一個(gè)輸出邊界,判斷各區(qū)域特征強(qiáng)弱來(lái)篩選有用特征。例如通過(guò)卷積后的一塊區(qū)域沒(méi)能達(dá)到激活閾值,則激活函數(shù)將輸出 0,表示這塊區(qū)域提取的特征無(wú)關(guān)緊要。在卷積神經(jīng)網(wǎng)絡(luò)中比較常用的是 ReLU 函數(shù),在本節(jié)實(shí)驗(yàn)我們并不需要了解 ReLU 函數(shù)的公式,因?yàn)楝F(xiàn)有的開源框架中已經(jīng)內(nèi)置了一些激活函數(shù),我們只需要調(diào)用就行了。
池化層
池化(Pooling)最直觀的作用就是壓縮輸入的尺寸(當(dāng)卷積核的步長(zhǎng)大于 1 時(shí)也可以壓縮輸入尺寸),池化層通常放在激活層之后。池化方法有兩種,最大池化(Max pooling)和平均池化(Average pooling)。最大池化就是選定域內(nèi)最大值來(lái)表示該區(qū)域,下圖中我們?cè)?×4 的矩陣中選定2×2 區(qū)域進(jìn)行池化,選出這個(gè)區(qū)域內(nèi)最大值 46 來(lái)表示該區(qū)域,然后向左移動(dòng) 2 個(gè)步長(zhǎng),在新的區(qū)域中選擇最大值 105 來(lái)表示該區(qū)域,依次類推我們將原來(lái)的4×4 矩陣壓縮到2×2 尺寸。同理平均池化就是選定區(qū)域內(nèi)的平均值表示該區(qū)域。
image.png

全連接層
全連接層就是前一層的激活值與這一層所有的節(jié)點(diǎn)相連,在上一節(jié)實(shí)驗(yàn)中我們已經(jīng)詳細(xì)講解了全連接神經(jīng)網(wǎng)絡(luò)。在卷積神經(jīng)網(wǎng)絡(luò)中,全連接層總是放在網(wǎng)絡(luò)的末尾。
Dropout
在全連接層后我們通常會(huì)進(jìn)行 Dropout 操作,Dropout 是一種預(yù)防過(guò)擬合提高模型準(zhǔn)確率的方法。其原理是在訓(xùn)練過(guò)程中以一定概率隨機(jī)丟棄部分節(jié)點(diǎn)從而提高模型的泛化能力(見(jiàn)下圖)。
image.png

使用卷積神經(jīng)網(wǎng)絡(luò)分類數(shù)字

至此我們已經(jīng)了解了卷積神經(jīng)網(wǎng)絡(luò)的基本原理和結(jié)構(gòu),下面我們將通過(guò)代碼來(lái)構(gòu)建一個(gè)卷積神經(jīng)網(wǎng)絡(luò)并訓(xùn)練它,最后我們將使用訓(xùn)練好的模型實(shí)現(xiàn)數(shù)字的檢測(cè)和識(shí)別。接下來(lái)使用 Tensorflow 中的 Keras 來(lái)構(gòu)建一個(gè)卷積神經(jīng)網(wǎng)絡(luò),Keras 是 Tensorflow 中的高級(jí)神經(jīng)網(wǎng)絡(luò) API,它能夠幫我們快速構(gòu)建神經(jīng)網(wǎng)絡(luò)模型,我們從 tensorflow.keras 中導(dǎo)入將要用到的模塊,這些模塊我們將在后面的代碼說(shuō)明其作用。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras import backend as K

首先我們創(chuàng)建一個(gè)名為 CNN 的類,這個(gè)類將包含前面我們提到的所有層。這個(gè)網(wǎng)絡(luò)的結(jié)構(gòu)是模仿 1998 年 Y. Lecun 等人改進(jìn)的 LeNet 網(wǎng)絡(luò),感興趣的同學(xué)可以閱讀原文。然后我們用 @staticmethod 聲明一個(gè)靜態(tài)的 frame 方法,在該方法內(nèi)我們將一層一層構(gòu)建網(wǎng)絡(luò),這個(gè)方法需要提供 4 個(gè)參數(shù) widthheight、channelclasses 分別對(duì)應(yīng)輸入圖像的寬、高、通道數(shù)以及要分類的類別數(shù)。
第 4 行我們使用從 tensorflow.keras.models 中導(dǎo)入的 Sequential 模塊創(chuàng)建一個(gè)序列模型 model,這個(gè)模型可以讓我們一層一層的堆疊我們的網(wǎng)絡(luò)。第 5 行我們按照 (height, width, channel) 的順序初始化輸入矩陣維度 img_shape。第 7 行我們判斷 k.image_data_formate 返回默認(rèn)圖像數(shù)據(jù)格式如果是 "channels_first" 則將 img_shape 修改為 (channel, height, width),這里 k 是從 tensorflow.keras 中導(dǎo)入的 beckend 模塊,Keras 并不處理張量和卷積等低級(jí)計(jì)算和操作,相反其依賴后端引擎來(lái)完成這個(gè)操作,通過(guò)該模塊我們可以獲得后端引擎的一些信息。

第 10 行通過(guò) model 的 add 方法為我們的網(wǎng)絡(luò)添加相應(yīng)的層。我們使用從 tensorflow.keras.layers 中導(dǎo)入的 Conv2D 添加卷積層,Conv2D 會(huì)接收 4 個(gè)參數(shù)用于構(gòu)建卷積層。其參數(shù)的意義如下所示:
第一個(gè)參數(shù) 20 表示使用 20 個(gè)濾波器對(duì)圖片進(jìn)行卷積。
第二個(gè)參數(shù) (5, 5) 表示每個(gè)濾波器的尺寸為5×5。
第三個(gè)參數(shù) padding="same" 表示為輸入圖像添加填充使得卷積后的特征圖尺寸與輸入的尺寸相同。
第四個(gè)參數(shù) input_shape=img_shape 表示輸入圖像的尺寸。

第 11 行使用從 tensorflow.keras.layers 中導(dǎo)入的 Activation 將激活函數(shù)作用于特征圖,這里我們提供 "relu" 參數(shù)表示使用 ReLU 函數(shù)。第 12 行使用從 tensorflow.keras.layers 中導(dǎo)入的 MaxPooling2D 添加池化層,這里使用最大池化的方法,這里需要提供 2 個(gè)參數(shù),pooling_size=(2,2) 表示池化的尺寸是2×2,strides=(2,2) 表示池化的步長(zhǎng)為 2。
第 14 行再添加一層卷積層,這一次使用 50 個(gè)濾波器,每個(gè)濾波器的尺寸為5×5,同樣使用填充方法使輸入和輸出具有相同尺寸。然后在第 15 行添加一個(gè) ReLU 激活層,最后在第 16 行添加一個(gè)最大池化層,池化的尺寸仍然是2×2,步長(zhǎng)為 2。
接下來(lái)在第 18 行使用從 tensorflow.keras.layers 中導(dǎo)入的 Flatten 將輸入的張量展開為一個(gè)向量。第 19 行使用 tensorflow.keras.layers 中導(dǎo)入的 Dense 添加全連接層,這里我們提供一個(gè) 500 參數(shù)表示該全連接層共有 500 個(gè)節(jié)點(diǎn)。第 20 行添加一個(gè) ReLU 激活層。第 21 行使用從 tensorflow.keras.layers 中導(dǎo)入的 Dropout 方法添加 Dropout 層并將隨機(jī)丟棄部分節(jié)點(diǎn)的概率設(shè)為 0.5。
第 23 行我們添加一個(gè)全連接層,其節(jié)點(diǎn)數(shù)等于分類任務(wù)的類別數(shù)。然后在第 24 行添加一個(gè) softmax 函數(shù)進(jìn)行分類。最后返回我們構(gòu)建好的模型 model。

class CNN:
    @staticmethod
    def frame(width, height, channel, classes):
        model = Sequential()
        img_shape = (height, width, channel)

        if K.image_data_format() == "channels_first":
            img_shape = (channel, height, width)
            
        model.add(Conv2D(20, (5, 5), padding="same", input_shape=img_shape))
        model.add(Activation("relu"))
        model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

        model.add(Conv2D(50, (5, 5), padding="same"))
        model.add(Activation("relu"))
        model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

        model.add(Flatten())
        model.add(Dense(500))
        model.add(Activation("relu"))
        model.add(Dropout(0.5))

        model.add(Dense(classes))
        model.add(Activation("softmax"))
        
        return model

至此我們的神經(jīng)網(wǎng)絡(luò)已經(jīng)構(gòu)建完成,接下來(lái)我們將使用 Mnist 數(shù)據(jù)集訓(xùn)練我們構(gòu)建的網(wǎng)絡(luò)。首先通過(guò)下面命令下載數(shù)據(jù)集,整個(gè)數(shù)據(jù)集共有 7 萬(wàn)張手寫數(shù)字圖片和其對(duì)應(yīng)的標(biāo)簽組成,數(shù)據(jù)集分為訓(xùn)練集(6 萬(wàn)張圖片和每張圖片對(duì)應(yīng)的標(biāo)簽)和測(cè)試集(1 萬(wàn)張圖片和每張圖片對(duì)應(yīng)的標(biāo)簽),每張圖片的尺寸為 28×28×1。

!wget https://labfile.oss.aliyuncs.com/courses/3096/train-images.idx3-ubyte
!wget https://labfile.oss.aliyuncs.com/courses/3096/train-labels.idx1-ubyte
!wget https://labfile.oss.aliyuncs.com/courses/3096/test-images.idx3-ubyte
!wget https://labfile.oss.aliyuncs.com/courses/3096/test-labels.idx1-ubyte

我們首先導(dǎo)入需要用到的模塊,具體每個(gè)模塊的作用我們將在后面代碼中介紹。

from tensorflow.keras.optimizers import SGD
from sklearn.preprocessing import LabelBinarizer
from sklearn.metrics import classification_report
import numpy as np
import matplotlib.pyplot as plt
from IPython import display
%matplotlib inline

接著我們創(chuàng)建一個(gè) load_mnist 函數(shù)用于讀取和處理數(shù)據(jù)集。該函數(shù)有三個(gè)輸入?yún)?shù),其參數(shù)意義如下所示:
images_path 是保存圖片文件的地址。
labels_path 是保存標(biāo)簽文件的地址。
amount 是輸入函數(shù)的圖片總數(shù),例如訓(xùn)練集總共有 60000 張圖片要被讀取,則 amount 就等于 60000。

第 2,3 行表示根據(jù)提供的 images_path 地址打開文件并使用 np.fromfile 將二進(jìn)制文件讀取為無(wú)符號(hào)整數(shù)。第 4 行剔除 images 中的前 16 個(gè)元素(這 16 個(gè)元素存儲(chǔ)了一些屬性信息,不屬于圖片像素值),然后使用 reshape 方法重塑數(shù)組為 (amount, 28, 28, 1),最后將這些無(wú)符號(hào)整數(shù)轉(zhuǎn)換為浮點(diǎn)型。同理第 6,7,8 行獲取圖片對(duì)應(yīng)標(biāo)簽。最后函數(shù)返回處理后的圖片和標(biāo)簽。

def load_mnist(images_path, labels_path, amount):  
    with open(images_path, 'rb') as imgpath:
        images = np.fromfile(imgpath, dtype=np.uint8)
        images = images[16:].reshape((amount, 28, 28, 1)).astype("float32")
        
    with open(labels_path, 'rb') as lbpath:
        labels = np.fromfile(lbpath, dtype=np.uint8)
        labels = labels[8:].reshape((amount)).astype("float32")
        
    return images, labels

接下來(lái)使用 load_mnist 函數(shù)分別載入訓(xùn)練集和測(cè)試集。

print("Loading MNIST...")
trainData, trainLabels = load_mnist("train-images.idx3-ubyte", "train-labels.idx1-ubyte", 60000)
testData, testLabels = load_mnist("test-images.idx3-ubyte", "test-labels.idx1-ubyte", 10000)

然后我們判斷 k.image_data_formate 返回默認(rèn)圖像數(shù)據(jù)格式如果是 "channels_first" 則將 trainData 和 testData 重塑為 (圖片數(shù)量, 1, 28, 28)。

if K.image_data_format() == "channels_first":
    trainData = trainData.reshape((trainData.shape[0], 1, 28, 28))
    testData = testData.reshape((testData.shape[0], 1, 28, 28))

下面我們對(duì)圖像進(jìn)行歸一化處理以加快訓(xùn)練過(guò)程,我們將所有像素值除以最大像素值 255,這樣所有像素值都會(huì)在 [0, 1] 之間。

trainData = trainData / 255.0
testData = testData / 255.0

然后我們使用從 sklearn 的 preprocessing 模塊中導(dǎo)入的 LabelBinarizer 對(duì)訓(xùn)練集和測(cè)試集的標(biāo)簽進(jìn)行編碼,使用 LabelBinarizer 中的 fit_trainsform 方法分別將訓(xùn)練集和測(cè)試集的標(biāo)簽二值化。

la = LabelBinarizer()
trainLabels = la.fit_transform(trainLabels)
testLabels = la.transform(testLabels)

我們使用從 tensorflow.keras.optimizers 中導(dǎo)入的 SGD 優(yōu)化我們的誤差函數(shù),下面第 1 行我們初始化隨機(jī)梯度下降法并將學(xué)習(xí)率設(shè)為 0.01。第 2 行實(shí)例化我們前面構(gòu)建好的卷積神經(jīng)網(wǎng)絡(luò),frame 的輸入?yún)?shù) width 表示圖片的寬,height 表示圖片的高,channel 表示圖片的通道數(shù),classes 表示數(shù)據(jù)集的圖片會(huì)被分為 10 個(gè)類別(數(shù)字是 0 到 9)。第 3 行使用 compile 配置編譯我們的訓(xùn)練模型,這里我們將輸入 3 個(gè)參數(shù),loss 表示我們選擇的目標(biāo)函數(shù),這里將其設(shè)置為 "categorical_crossentropy",optimizer 是設(shè)置優(yōu)化方法,這里我們將其設(shè)置為前面初始化的隨機(jī)梯度下降法。metrics 用于設(shè)置模型評(píng)估標(biāo)準(zhǔn),這里我們將其設(shè)置為 ["accuracy"]。

opt = SGD(lr=0.01)
model = CNN.frame(width=28, height=28, channel=1, classes=10)
model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])

下面第 2 行我們?yōu)?fit 方法提供幾個(gè)參數(shù)訓(xùn)練我們的網(wǎng)絡(luò),trainData 和 trainLabels 是我們的訓(xùn)練集和對(duì)應(yīng)的標(biāo)簽,我們將元組 (testData, testLabels) 作為參數(shù)傳遞給 validation_data,表示我們將用測(cè)試集和其對(duì)應(yīng)標(biāo)簽評(píng)估模型,需要注意的是模型不會(huì)在這個(gè)測(cè)試集上進(jìn)行訓(xùn)練。batch_size = 128 表示每次我們訓(xùn)練 128 張圖片。
這里我們簡(jiǎn)單介紹下 Mini Batch 的概念,Mini Batch 是將數(shù)據(jù)集分成若干份,然后逐個(gè)訓(xùn)練每個(gè)小部分,這個(gè)方法可以解決設(shè)備因內(nèi)存小、無(wú)法一次訓(xùn)練整個(gè)數(shù)據(jù)集的問(wèn)題。epochs = 5 表示訓(xùn)練過(guò)程在整個(gè)訓(xùn)練集被訓(xùn)練 5 次時(shí)停止。verbose = 1 表示將用進(jìn)度條顯示訓(xùn)練進(jìn)度。第 3 行我們使用 save 方法保存訓(xùn)練好的模型并將其命名為 cnn_weights.hdf5。

print("Training CNN...")
H = model.fit(trainData, trainLabels, validation_data=(testData, testLabels), batch_size=128,epochs=5, verbose=1)
model.save("cnn_weights.hdf5")

訓(xùn)練完成后我們就可以使用模型進(jìn)行分類了,第 2 行我們使用 predict 方法在測(cè)試集上進(jìn)行測(cè)試,batch_size=128 表示將 Mini Batch 設(shè)置為 128。結(jié)果 predictions 是一個(gè)維數(shù)是(60000,10)的 NumPy 數(shù)組,10000 表示測(cè)試集有 10000 個(gè)圖片,第二維的 10 表示每張圖片所對(duì)應(yīng)從 0 到 9 這 10 個(gè)數(shù)所對(duì)應(yīng)的概率。然后第 3 行我們使用從 sklearn 的 metrics 模塊中導(dǎo)入的 classification_report 方法顯示評(píng)估的結(jié)果。該方法的第一個(gè)參數(shù) testLabels.argmax(axis=1) 就是測(cè)試集中每張圖片所對(duì)應(yīng)的真實(shí)標(biāo)簽。predictions.argmax(axis=1) 是模型預(yù)測(cè)每張圖片對(duì)應(yīng)的標(biāo)簽概率。target_names=[str(x) for x in la.classes_] 表示在屏幕輸出中顯示標(biāo)簽每個(gè)數(shù)字的標(biāo)簽。

print("Evaluating ...")
predictions = model.predict(testData, batch_size=128)
print(classification_report(testLabels.argmax(axis=1), predictions.argmax(axis=1), target_names=[str(x) for x in la.classes_]))

如果腳本執(zhí)行沒(méi)有問(wèn)題的話,應(yīng)該能看到類似下圖的輸出結(jié)果。通過(guò)下圖我們可以看到模型在測(cè)試集上的準(zhǔn)確率達(dá)到了 99%。


image.png

下面我們將使用這個(gè)模型來(lái)檢測(cè)和識(shí)別出圖片中的數(shù)字。我們先通過(guò)下面兩條命令下載需要用到的圖片。

!wget https://labfile.oss.aliyuncs.com/courses/3096/digit.zip

通過(guò)下面的命令解壓文件將得到 4 張圖片,我們將用這 4 張圖片進(jìn)行手寫數(shù)字檢測(cè)和識(shí)別,下圖為下載的圖片。

!unzip digit.zip
image.png

導(dǎo)入需要用到的模塊,在后面的代碼中我們將講解每個(gè)模塊的作用。

import cv2
from tensorflow.keras.models import load_model

下面我們創(chuàng)建一個(gè) paths 變量用于保存 4 張圖片路徑,然后我們使用 cv2.imread 讀取列表中第一張圖片。

paths = ["1.jpg", "2.jpeg", "3.jpeg", "4.jpg"]
image = cv2.imread(paths[0])

接著首先使用 cv2.cvtColor 方法將彩色圖片轉(zhuǎn)換為單通道灰度圖(我們模型只接受單通道圖片)。然后使用 cv2.GaussianBlur 對(duì)圖片進(jìn)行平滑預(yù)處理。最后使用 cv2.Canny 方法對(duì)圖片進(jìn)行邊緣檢測(cè),找出圖片中的邊緣信息。

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(blurred, 30, 150)

我們使用 cv2.findContours 在邊緣信息圖像 edged 中找出其中輪廓信息并且只返回外輪廓,cnts 里面的每個(gè)元素都是一個(gè)輪廓。第 2 行我們先用 cv2.boundingRect 方法找到每個(gè)輪廓的最小外接矩形,然后將每個(gè)輪廓和其最小外接矩形的左上角頂點(diǎn)的橫坐標(biāo)以輪廓,左上角頂點(diǎn)橫坐標(biāo)的形式放在一個(gè)元組中,最后使用 sorted 方法將元組輪廓,左上角頂點(diǎn)橫坐標(biāo)按照左上角頂點(diǎn)橫坐標(biāo)的大小排列順序。

cnts, _ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted([(c, cv2.boundingRect(c)[0]) for c in cnts], key = lambda x: x[1])

下面我們使用從 tensorflow.keras.models 中導(dǎo)入的 load_model 加載前面已經(jīng)保存的模型 cnn_weights.hdf5。

model = load_model("cnn_weights.hdf5")

下面我們創(chuàng)建兩個(gè)列表,image_data 用于存儲(chǔ)從原圖中截取的每個(gè)數(shù)字圖像,roi_c 存儲(chǔ)前面我們提到的每個(gè)輪廓的最小外接矩形的坐標(biāo)和寬高。接下來(lái)我們使用 for 循環(huán)遍歷 cnts 中的每個(gè)輪廓。在循環(huán)內(nèi)我們首先用 cv2.boundingRect 找到每個(gè)輪廓的最小外接矩形的左上角頂點(diǎn)坐標(biāo) x 和 y,以及矩形的寬 w 和高 h。
下面第 7 行使用 if 語(yǔ)句來(lái)剔除寬小于 10、高小于 20 的外接矩形。下面第 8 行我們用第 5 行獲得的坐標(biāo) x、y 以及寬和高 w、h 從灰度圖 gray 中截取出每個(gè)數(shù)字(這里將坐標(biāo)值和寬高都加減 10 是為了避免最小外接矩形不能完整的將數(shù)字包含在其內(nèi))。第 10 行我們用 cv2.resize 將截取的圖片的尺寸縮放為28×28,因?yàn)槲覀兊木W(wǎng)絡(luò)輸入尺寸是28×28。第 11 行使用 cv2.threshold 將 roi 中所有小于 100 的像素值設(shè)置為 255, 其他的像素值設(shè)置為 0。第 12 行將所有像素值進(jìn)行歸一化處理。最后將處理后的圖片和外接矩形的 4 個(gè)量存儲(chǔ)到 image_data 和 roi_c 中。

image_data = []
roi_c = []

for (c, _) in cnts:
    (x, y, w, h) = cv2.boundingRect(c)

    if w >= 10 and h >= 20:
        roi = gray[y-10:y + h+10, x-10:x + w+10]
        
        roi = cv2.resize(roi, (28, 28), interpolation = cv2.INTER_AREA)
        T, thresh = cv2.threshold(roi, 100, 255, cv2.THRESH_BINARY_INV)
        thresh = thresh.astype("float32")/255.0
        
        image_data.append(thresh)
        roi_c.append((x,y,w,h))

我們的模型對(duì)輸入的維度要求是n×28×28×1,而 image_data 的維度是n×28×28,所以我們需要用 np.expand_dim 添加第 4 個(gè)維度使得 image_data 的維度與模型輸入維度相同。然后第 2 行我們使用 model.predict 方法對(duì)輸入圖片進(jìn)行分類并使用 argmax 輸出其所屬類的標(biāo)簽。

image_data = np.expand_dims(image_data, axis=3)
result = model.predict(image_data).argmax(axis=1)

最后我們使用 cv2.rectangle 標(biāo)記出圖片中的每個(gè)數(shù)字并用 cv2.putText 將其對(duì)應(yīng)的標(biāo)簽添加到數(shù)字上方。

for i, r in enumerate(roi_c):
    (x, y, w, h) = r
    cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 1)
    cv2.putText(image, str(result[i]), (x - 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2)

接下來(lái)我們通過(guò)下面的代碼來(lái)顯示檢測(cè)結(jié)果。

plt.figure(figsize = (10,10))
image = image[:,:,::-1]
plt.imshow(image)

執(zhí)行整個(gè)腳本,如果沒(méi)有意外的話我們能看到類似下圖的結(jié)果。讀取 paths 列表中的其他圖片只需將 cv2.imread(path[0]) 中的 0 修改為 1、2 或 3。雖然有些數(shù)字被錯(cuò)誤的分類,但是大部分的數(shù)字還是被正確的識(shí)別了。如果想要提高模型的性能可以嘗試增加訓(xùn)練次數(shù)(我們的模型只訓(xùn)練了 5 次),增加卷積層(我們的模型只有兩層卷積層),在每個(gè)激活層之后應(yīng)用批標(biāo)準(zhǔn)化(Batch Normalization)和 Dropout 都能提高模型的性能(我們的模型沒(méi)有使用批標(biāo)準(zhǔn)化且只使用了一次 Dropout,添加批標(biāo)準(zhǔn)化和 Dropout 可以明顯提高我們的模型表現(xiàn),大家可以自行嘗試上述方法)。


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容