上一篇:《設計卷積神經網路CNN為什麼不是程式設計?》
序言:我們已經踏入了設計人工智慧(AI)模型的大門,有一個重要概念請大家務必記住:人工智慧模型 = 架構 + 特徵。任何一個AI模型都是先設計出架構,再透過資料訓練獲得特徵。整合了特徵的架構才算是一個完整的人工智慧模型,如果沒有特徵的支撐,架構本身還不能稱為人工智慧模型。
在本節中,我們將探索一個比 Fashion MNIST 分類器更復雜的場景。我們將擴充套件我們對卷積和卷積神經網路的理解,嘗試對圖片內容進行分類,其中特徵的位置並不總是在同一個地方。所以我們為此建立了馬或人資料集。(我們全部的編碼都是經過驗證過的,大家可以用來自己驗證。)
馬或人資料集
本節的資料集包含了一千多張 300 × 300 畫素的圖片,大約各有一半是馬和人,以不同的姿勢呈現。你可以在圖 3-7 中看到一些示例。
圖 3-7:馬和人
正如你所看到的,主體具有不同的方向和姿勢,影像的構圖也各不相同。比如說這兩匹馬——它們的頭部方向不同,一張是遠景,展示了整個動物,而另一張是近景,僅展示了頭部和部分身體。同樣地,人類的光照不同,膚色各異,姿勢也不同。男性雙手叉腰,而女性則伸展雙手。這些影像中還包含了背景,比如樹木和海灘,因此分類器需要識別出影像中哪些部分是決定馬和人特徵的重要特徵,而不被背景干擾。
儘管之前預測 Y = 2X – 1 或分類小型單色衣物影像的例子可能可以透過傳統編碼實現,但很明顯,這個問題要複雜得多,進入了機器學習成為解決問題關鍵的領域。
一個有趣的旁註是,這些影像都是計算機生成的。理論上,CGI 馬的影像中識別出的特徵應適用於真實影像。在本章稍後,你將看到這種方法的效果如何。
Keras 的 ImageDataGenerator
你一直使用的 Fashion MNIST 資料集自帶標籤。每個影像檔案都有一個包含標籤詳情的檔案。然而,許多基於影像的資料集並沒有這一點,“馬或人”資料集也不例外。沒有標籤的情況下,影像被分成各自型別的子目錄。在 TensorFlow 的 Keras 中,有一個名為 ImageDataGenerator 的工具可以利用這種結構來自動為影像分配標籤。
要使用 ImageDataGenerator,只需確保你的目錄結構中包含一組命名的子目錄,每個子目錄就是一個標籤。例如,“馬或人”資料集提供為一組 ZIP 檔案,一個包含訓練資料(1000 多張影像),另一個包含驗證資料(256 張影像)。當你下載並將它們解壓到本地用於訓練和驗證的目錄中時,確保它們按圖 3-8 所示的檔案結構存放。
以下是獲取訓練資料並將其提取到相應命名子目錄的程式碼,如圖所示:
import urllib.request
import zipfile
url = "https://storage.googleapis.com/laurencemoroney-blog.appspot.com/
horse-or-human.zip"
file_name = "horse-or-human.zip"
training_dir = 'horse-or-human/training/'
urllib.request.urlretrieve(url, file_name)
zip_ref = zipfile.ZipFile(file_name, 'r')
zip_ref.extractall(training_dir)
zip_ref.close()
圖 3-8:確保影像位於命名的子目錄中
以下是獲取訓練資料並將其提取到如圖所示的相應命名子目錄中的程式碼:
import urllib.request
import zipfile
url = "https://storage.googleapis.com/laurencemoroney-blog.appspot.com/
horse-or-human.zip"
file_name = "horse-or-human.zip"
training_dir = 'horse-or-human/training/'
urllib.request.urlretrieve(url, file_name)
zip_ref = zipfile.ZipFile(file_name, 'r')
zip_ref.extractall(training_dir)
zip_ref.close()
這只是簡單地下載訓練資料的 ZIP 檔案,並將其解壓到 horse-or-human/training 目錄中(我們稍後會處理驗證資料的下載)。這是包含影像型別子目錄的父目錄。
要使用 ImageDataGenerator,我們現在只需使用以下程式碼:
from tensorflow.keras.preprocessing.image import ImageDataGenerator
All images will be rescaled by 1./255
train_datagen = ImageDataGenerator(rescale=1/255)
train_generator = train_datagen.flow_from_directory(
training_dir,
target_size=(300, 300),
class_mode='binary'
)
我們首先建立一個名為 train_datagen 的 ImageDataGenerator 例項。然後,我們指定它將從一個目錄中流出影像用於訓練過程。目錄是之前指定的 training_dir。此外,我們還設定了一些資料的超引數,比如目標大小——在這個例子中,影像大小為 300 × 300,並且類別模式為二進位制。如果只有兩類影像(就像這個例子),模式通常是二進位制;如果有兩類以上,模式則為分類。
馬或人的卷積神經網路架構
在設計用於分類這些影像的架構時,你需要考慮幾個與 Fashion MNIST 資料集的主要差異。首先,這些影像要大得多——300 × 300 畫素——因此可能需要更多的層次。其次,這些影像是全綵色的,而不是灰度的,因此每張影像將擁有三個通道而非一個。第三,只有兩種影像型別,因此我們有一個二分類器,只需一個輸出神經元即可實現,其中對於一個類別接近 0,另一個類別接近 1。在探索這個架構時,請牢記這些考慮事項:
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(16, (3,3), activation='relu' ,
input_shape=(300, 300, 3)),
tf.keras.layers.MaxPooling2D(2, 2),
tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
tf.keras.layers.MaxPooling2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(512, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
這裡有一些需要注意的點。首先,這是第一個卷積層。我們定義了 16 個 3 × 3 的過濾器,但影像的輸入形狀為 (300, 300, 3)。記住,這是因為我們的輸入影像是 300 × 300 的彩色影像,所以有三個通道,而不是之前用於單色 Fashion MNIST 資料集的一個通道。
在另一端,你會注意到輸出層中只有一個神經元。這是因為我們使用的是二分類器,如果用 sigmoid 函式啟用,這個單一神經元就能實現二分類。sigmoid 函式的作用是將一組值驅動到 0,另一組驅動到 1,這對於二分類來說非常合適。
接下來,注意我們如何堆疊了好幾個卷積層。這樣做是因為我們的影像源相當大,我們希望隨著時間推移,得到許多較小的影像,每張影像都突出了特徵。如果我們檢視 model.summary 的結果,就能看到這種效果:
=================================================================
conv2d (Conv2D) (None, 298, 298, 16) 448
max_pooling2d (MaxPooling2D) (None, 149, 149, 16) 0
conv2d_1 (Conv2D) (None, 147, 147, 32) 4640
max_pooling2d_1 (MaxPooling2 (None, 73, 73, 32) 0
conv2d_2 (Conv2D) (None, 71, 71, 64) 18496
max_pooling2d_2 (MaxPooling2 (None, 35, 35, 64) 0
conv2d_3 (Conv2D) (None, 33, 33, 64) 36928
max_pooling2d_3 (MaxPooling2 (None, 16, 16, 64) 0
conv2d_4 (Conv2D) (None, 14, 14, 64) 36928
max_pooling2d_4 (MaxPooling2 (None, 7, 7, 64) 0
flatten (Flatten) (None, 3136) 0
dense (Dense) (None, 512) 1606144
dense_1 (Dense) (None, 1) 513
=================================================================
Total params: 1,704,097
Trainable params: 1,704,097
Non-trainable params: 0
注意,當資料透過所有卷積層和池化層後,最終變成了 7 × 7 的元素。理論上,這些將是相對簡單的啟用特徵圖,只包含 49 個畫素。這些特徵圖可以傳遞給全連線的神經網路,以將它們匹配到相應的標籤。
當然,這也使得我們比之前的網路擁有更多的引數,因此訓練速度會更慢。使用這種架構,我們將學習 170 萬個引數。
為了訓練網路,我們需要用一個損失函式和一個最佳化器來編譯它。在這種情況下,因為只有兩類,我們可以使用二元交叉熵損失函式 binary cross entropy,顧名思義,這是專為這種場景設計的損失函式。我們還可以嘗試一個新的最佳化器,均方根傳播(RMSprop),它帶有一個學習率(lr)引數,可以用來調整學習速率。程式碼如下:
model.compile(loss='binary_crossentropy',
optimizer=RMSprop(lr=0.001),
metrics=['accuracy'])
我們透過使用 fit_generator 並傳入我們之前建立的 train_generator 來進行訓練:
history = model.fit_generator(
train_generator,
epochs=15
)
這個示例可以在 Colab 中執行,但如果你想在自己的機器上執行,請確保使用 pip install pillow 安裝 Pillow 庫。
請注意,在 TensorFlow 的 Keras 中,你可以使用 model.fit 將訓練資料和訓練標籤進行擬合。在使用生成器時,舊版本要求使用 model.fit_generator。在 TensorFlow 的較新版本中,你可以使用任一方法。
僅僅經過 15 輪訓練,這種架構在訓練集上就達到了非常令人印象深刻的 95% 以上的準確率。當然,這僅適用於訓練資料,並不能代表網路在未曾見過的資料上的表現。
接下來我們將看看如何使用生成器新增驗證集並測量其效能,以更好地指示該模型在實際中的表現。
向馬或人資料集中新增驗證
要新增驗證,你需要一個獨立於訓練集的驗證資料集。有時你會得到一個主資料集,需要自己進行拆分,但在馬或人資料集中,有一個獨立的驗證集可以下載。
你可能會想,為什麼我們這裡談的是驗證資料集而不是測試資料集,它們是一樣的嗎?
對於之前章節中的簡單模型,通常將資料集分成兩個部分就夠了,一個用於訓練,另一個用於測試。但對於我們正在構建的這種更復雜的模型,你會希望建立單獨的驗證集和測試集。它們之間有什麼區別呢?訓練資料用於教網路如何將資料和標籤對應起來。驗證資料在訓練過程中用於檢視網路在之前未見過的資料上的表現——即,它並不是用來擬合資料和標籤的,而是用來檢查擬合的效果如何。測試資料則是在訓練完成後用於檢視網路在完全未見過的資料上的表現。有些資料集提供三向分割,在其他情況下,你可能需要將測試集拆分為驗證和測試兩個部分。這裡,你將下載一些額外的影像來測試模型。
你可以使用與訓練影像類似的程式碼來下載驗證集並將其解壓到另一個目錄中:
validation_url = "https://storage.googleapis.com/laurencemoroney-blog.appspot.com/validation-horse-or-human.zip"
validation_file_name = "validation-horse-or-human.zip"
validation_dir = 'horse-or-human/validation/'
urllib.request.urlretrieve(validation_url, validation_file_name)
zip_ref = zipfile.ZipFile(validation_file_name, 'r')
zip_ref.extractall(validation_dir)
zip_ref.close()
一旦有了驗證資料,就可以設定另一個 ImageDataGenerator 來管理這些影像:
validation_datagen = ImageDataGenerator(rescale=1/255)
validation_generator = train_datagen.flow_from_directory(
validation_dir,
target_size=(300, 300),
class_mode='binary'
)
要讓 TensorFlow 為你執行驗證,只需更新 model.fit_generator 方法,指定希望使用驗證資料在每個訓練週期(epoch)測試模型。你可以使用 validation_data 引數,並傳入剛構建的驗證生成器:
history = model.fit_generator(
train_generator,
epochs=15,
validation_data=validation_generator
)
經過 15 個訓練週期後,你會發現模型在訓練集上的準確率超過 99%,但在驗證集上只有大約 88%。這表明模型出現了過擬合,就像我們在上一章中看到的那樣。
儘管如此,考慮到模型訓練所用的影像數量少且影像多樣化,效能並不算差。你開始遇到由於資料不足而帶來的瓶頸,但有一些技術可以用來提升模型的效能。我們會在本章稍後探討這些技術,不過在此之前,讓我們來看看如何使用這個模型。
測試馬或人影像
能夠構建一個模型當然很好,但你肯定也想試一試它的實際效果。剛開始我的 AI 之旅時,一個讓我感到非常沮喪的問題是,我能找到大量展示如何構建模型的程式碼和模型表現的圖表,但卻很少能找到幫助我自己測試模型的程式碼。我會盡量避免在本書中出現這樣的情況!
測試模型可能在 Colab 中最為方便。我已經在 GitHub 上提供了一個馬或人筆記本,可以直接在 Colab 中開啟。
一旦你訓練好模型,你會看到一個名為“執行模型”的部分。在執行它之前,先線上上找幾張馬或人的圖片並下載到你的電腦上。Pixabay.com 是一個不錯的版權免費影像網站。建議先將測試影像準備好,因為在你搜尋時節點可能會超時。
圖 3-9 顯示了我從 Pixabay 下載的幾張用於測試模型的馬和人影像。
圖 3-9:測試影像
上傳影像後,如圖 3-10 所示,模型正確地將第一張影像分類為人,將第三張影像分類為馬,但儘管中間那張影像顯然是人,卻被錯誤地分類為馬!
圖 3-10:執行模型
你也可以同時上傳多張圖片,讓模型對它們進行預測。你可能會注意到模型傾向於過擬合馬的分類。如果人類沒有完全呈現姿勢——即,看不到他們的全身——模型可能會偏向於將其歸為馬。這就是這種情況下發生的事。第一個人類模型完全呈現了姿勢,影像的姿勢類似於資料集中許多姿勢,因此模型能正確分類她。而第二個人面對著相機,但影像中只有她的上半身。訓練資料中沒有類似的影像,所以模型無法正確識別她。
現在我們來看一下程式碼,瞭解它的作用。也許最重要的部分是這一段:
img = image.load_img(path, target_size=(300, 300))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
這裡,我們從 Colab 儲存的路徑中載入影像。請注意,我們指定了目標大小為 300 × 300。上傳的影像可以是任意形狀,但如果要輸入到模型中,它們必須是 300 × 300,因為模型是按這個尺寸訓練的。所以,第一行程式碼載入影像並將其調整為 300 × 300。
接下來的程式碼將影像轉換為二維陣列。然而,模型期望的是三維陣列,這在模型架構中的 input_shape 指定了。幸運的是,Numpy 提供了一個 expand_dims 方法,可以輕鬆地為陣列新增一個新維度。
現在我們將影像放入三維陣列中,只需確保它是垂直堆疊的,這樣它的形狀與訓練資料一致:
image_tensor = np.vstack([x])
影像格式正確後,就可以進行分類了:
classes = model.predict(image_tensor)
模型返回一個包含分類結果的陣列。因為在這個例子中只有一個分類,它實際上是一個包含陣列的陣列。你可以在圖 3-10 中看到,對於第一個(人類)模型,它看起來像 [[1.]]。
接下來只需檢查陣列第一個元素的值。如果大於 0.5,我們認為它是人類:
if classes[0] > 0.5:
print(fn + " is a human")
else:
print(fn + " is a horse")
這裡有幾個關鍵點需要考慮。首先,儘管網路是用合成的計算機生成影像訓練的,它在識別真實照片中的馬或人方面表現得相當好。這是一個潛在的好處,意味著你不一定需要數千張照片來訓練模型,可以透過 CGI 以相對低成本完成訓練。
但這個資料集也展示了你將面臨的一個基本問題。你的訓練集無法代表模型在真實環境中可能遇到的每種情況,因此模型總會對訓練集有一定程度的過度專門化。這裡就有一個清晰的例子:圖 3-9 中間的人類被錯誤分類了。訓練集中沒有包含這個姿勢的人類,因此模型沒有“學到”人類可以是這個樣子的。因此,它很可能將該形象視為馬,在這種情況下,它確實如此。
解決方案是什麼?最明顯的辦法是增加更多訓練資料,包含該特定姿勢的人類以及其他未代表的姿勢。然而,這並不總是可行的。幸運的是,TensorFlow 提供了一個巧妙的技巧來虛擬擴充套件資料集——這叫做影像增強(image augmentation),我們接下來將探索它。