這一節的標題是
10.4 Machine Learning withml5.jsKeras
因為方格子標題字數限制,所以沒完整顯現
在上一節,我們並沒有去探究,在類神經網路的訓練過程中,倒傳遞演算法到底是怎麼去調整權重的。這並不是說它不重要,而是我們可以藉助機器學習程式庫來做這件事,不需要事必躬親自己寫程式來做。
原書所使用的機器學習程式庫是ml5.js,不過因為沒有對應的python版本,所以在後續的內容中,就改用同樣是建構在TensorFlow上,使用上相對比較簡單的Keras程式庫。
因為Keras需搭配TensorFlow、JAX、PyTorch等任一個後端程式庫才能運作,所以在安裝時,也要一併安裝。在實際使用Keras來建造、訓練模型之前,要先來看一下,在使用監督式學習法來訓練多層的類神經網路模型時,需要執行的步驟有哪些。
The Machine Learning Life Cycle
機器學習的生命週期指的是,開發機器學習模型時,會經歷哪些過程;這過程通常會被區分為七個步驟:
- 蒐集資料:資料是機器學習的根本,沒有資料,就什麼都做不成。資料的蒐集方式百百種,從做實驗、人工輸入、擷取公眾資料(public data)到製作合成資料,都是可行的方式。
- 準備資料:蒐集到的資料通常不適合直接拿來做機器學習,因為其中可能會有重複、缺漏,以及含有離群值(outlier)等問題。所以,原始資料需要清洗一番,把那些不合用的部分去除掉。另外,把資料正規化,以及將其切分成訓練資料(training data)、驗證資料(validation data)、測試資料(testing data)等三個部分,這些也都是在準備資料時必須要做的工作。這三種資料的用途及其差異,在後面會有更詳細的解釋。
- 選擇模型:設計類神經網路的架構。某些模型會更適用於某種類型的資料和輸出。
- 訓練模型:把訓練資料餵給模型,讓模型根據誤差來調整權重。模型調整權重來讓誤差減到最少,這個過程叫做「最佳化」(optimization)。
- 評估模型:利用測試資料來評估模型的優劣。既然測試資料沒被用來訓練模型,對模型而言,那是新的、從沒見過的東西。所以,測試資料很適合拿來評估模型的優劣。
- 調整參數:模型的訓練過程會受到一組事先給定,用來控制學習過程參數的影響;這組參數通常稱為「超參數」(hyperparameter)。先前提到過的學習速率,就是超參數。透過微調超參數,並重新執行步驟4、3,甚至於步驟2,通常就能夠提升模型的效能。
- 部署模型:模型訓練完成並具備讓人滿意的效能之後,就可以正式服役了。
上述步驟是利用監督式學習法進行機器學習的基石。除了這些步驟外,還有一個更關鍵,可以稱之為步驟0的步驟存在,那就是:
0. 識別問題:在這個初始步驟中,要明確地定義要解決的問題是什麼?想要用機器學習模
型達成什麼樣的目標?
步驟0具有像北極星一樣的指引作用,其他的步驟都需依據步驟0所得到的結果來展開;如果連要解決的問題是什麼都不知道,那又如何去蒐集資料及設計模型呢?
機器學習的應用,大部分可以歸結為分類(classification)及迴歸(regression)這兩種類型。分類,就是把東西分成不同種類;迴歸,則是預測數字。接下來,就來看看機器學習是怎麼做分類和迴歸的。
Classification and Regression
分類是機器學習的一種問題類型,它所涉及的,是要去預測一段資料的標籤(label)。標籤也稱為種類(category)或類別(class)。先前在Example 10.1中,訓練感知器將資料點歸類為位於直線上方或下方,就是在進行資料點的分類。
分類的另一個例子是影像分類器。我們可以訓練一個分類器,讓它能夠判別圖片中的動物是狗或是貓,並給這張圖片一個對應的標籤。
要訓練模型來判別圖片中的動物是狗或是貓時,必須要餵給它一堆貓和狗的圖片。這些用來訓練模型的圖片,必須帶有正確的標籤,標註裡頭的動物是貓還是狗,這樣模型才能正確地調整權重,學習到如何辨識貓和狗。很明顯的,這是監督式學習的訓練方式。
在學習程式語言時,動手寫的第一支程式,通常會是在螢幕上顯示出「Hello, world!」字串;這幾乎可以說是學習程式時的標準「新生體驗式」了。在機器學習的監督式學習領域中,也有這麼一個「Hello, world!」體驗式,那就是MNIST資料集的分類問題。
MNIST是Modified National Institute of Standards and Technology的縮寫。這個資料集是由Yann LeCun (Courant Institute, NYU)、Corinna Cortes (Google Labs)、Christopher J.C. Burges (Microsoft Research)等人所蒐集製作的,裡頭含有70,000張手寫的0~9阿拉伯數字圖片;每張圖片都是灰階的,大小都是28×28像素,並且都標註有對應的數字。
MNIST是影像分類訓練資料集的典型範例,這也就是說,模型在決定圖片是屬於哪個種類時,它可以選擇的種類,其數量是固定的;確切來說,不多不少,就是10個。使用MNIST中的70,000張已標註正確答案的圖片來訓練模型,最終的目標,是希望模型完成訓練之後,當碰到新的、沒看過的圖片時,也能夠正確地將其分類,並賦予正確的標籤,也就是0~9這10個阿拉伯數字之中的一個。
迴歸是機器學習的另一種問題類型,它所涉及的,是預測一個或多個連續型的數字,也就是浮點數。例如,當一個機器學習模型根據居住人數、房屋大小、戶外溫度等輸入來預測房屋的每日用電量時,它的輸出不會是幾個不同選項中的一個,而會是像20.3度、75.9度等,這樣一個連續範圍內的某個數字。
Network Design
知道要解決的是怎樣的問題,對於類神經網路的設計,特別是輸入和輸出的設計,有著顯著的影響。接下來,就用實際的例子來看看,這個影響,到底是個怎樣的影響法。
第一個例子是關於分類的例子。在這個例子中,我們要來設計鳶尾花的分類器,所使用的資料集,是「Iris」這個在機器學習及資料科學界也是屬於「Hello, world!」體驗式等級的資料集。
Iris資料集可以在加州大學Irvine分校的UCI Machine Learning Repository找到,其資料來源是美國植物學家Edgar Anderson在美國和加拿大的許多地區蒐集鳶尾花資料多年的研究成果。更詳細的,有關這個資料集資料的來源,可以參考 Antony Unwin及Kim Kleinman所寫的文章:〈The Iris Data Set: In Search of the Source of Virginica〉。
經過仔細地分析之後,Anderson製作了一個表格,將鳶尾花分成三個不同的種類:Iris setosa、Iris versicolor,以及Iris virginica。在Anderson的表格中,每朵花都有萼片長度、萼片寬度、花瓣長度、花瓣寬度等四個以公分為單位的數值屬性,以及其所屬的鳶尾花種類。Anderson的表格長這樣:

在設計類神經網路模型時,把資料集前四個欄位作為輸入,而最後一個欄位作為輸出;模型可以設計成這樣:

在這個模型中,輸入層有四個神經元,每個神經元對應資料集表格前四個欄位中的一個;輸出層有三個神經元,每個神經元的輸出,就代表鳶尾花種類標籤中的一個;介於輸入層和輸出層之間的,則是含有五個神經元的隱藏層。要注意的是,隱藏層的神經元,每個都和前一層及後一層的所有神經元相連;這種結構,通常稱之為「全連接層」(fully connected layer)或「緻密層」(dense layer)。另一個要注意的是,在模型圖中,並沒有把偏置畫出來;但這並不代表沒有。真相是:既然偏置很重要,而且一定會有,那就不必畫出來佔空間;反正大家心照不宣,都知道有它的存在。
在輸出層有三個神經元,這三個神經元都有輸出,那怎麼從這三個輸出值來判斷正確的鳶尾花種類標籤呢?要回答這個問題,就必須知道,在訓練模型時,並不是直接就把資料集內的資料原封不動拿來餵給模型,而必須先把非數字的資料轉成數字,這樣神經元才有辦法處理;如果不這麼做,當輸入是文字時,你要讓神經元怎麼去計算輸入乘上權重的值呢?因此,我們得先把Iris資料集表格的最後一個欄位,也就是「分類」這個欄位內的資料,從文字給轉換成數字,然後才能拿來訓練模型。這也就是說,轉換後所得到的數字和鳶尾花種類標籤之間,會有一個對應關係;利用這個對應關係,我們就可以從輸出層三個神經元的輸出值,找到對應的鳶尾花種類標籤。
那從文字資料轉換成數字資料,實際上要怎麼做呢?這可以利用「獨熱編碼法」(one-hot encoding)來完成。例如,鳶尾花種類有setosa、versicolor、virginica等三種,我們可以設定(1, 0, 0)代表setosa、(0, 1, 0)代表versicolor、(0, 0, 1)代表virginica。依照這樣子的設定,如果我們把輸出層由上而下三個神經元的輸出值,就作為小括號內由左至右的三個數字,那就可以從輸出層三個神經元的輸出值,找到對應的鳶尾花種類了;這也是原書的模型圖,之所以會在輸出層三個神經元之後,還分別標註出setosa、versicolor、virginica等字樣的原因。
在設計分類用的模型時,有個原書因為使用ml5.js所以沒有特別提及的地方要注意,那就是輸出層神經元的啟動函數要使用softmax。為什麼呢?因為這樣,輸出層所有神經元的輸出值都會介於0~1之間,而且總和會是1。當有一個神經元的輸出值很接近1時,其他神經元的輸出值就會被壓得很接近0,這樣我們會比較能夠確定,模型的輸出值所指的是哪個類別標籤。
來總結一下設計分類用模型時的訣竅:
- 輸入層神經元的數量,要和資料集表格欄位,扣除分類欄位之後的數量一樣多。
- 輸出層神經元的數量,要和種類的數量一樣多。
- 輸出層神經元的啟動函數,要使用softmax。
那隱藏層呢?要用幾層?每層要有多少個神經元?有沒有什麼可以依循的原則呢?很不幸的,關於隱藏層的設計,並沒有什麼可以依循的原則。隱藏層要怎麼設計比較好,通常就只能用試誤法,或是其他有一定根據的方法,也就是啟發式法(heuristics),來加以確認。
接下來,來看關於迴歸的例子。這個例子是關於先前提到過的,房屋每日用電量的例子。
假設我們有個關於房屋每日用電量的資料集如下:

這個資料集是原書作者自己編造出來的,不是真實的數據。
在設計類神經網路來預測在不同條件下的房屋每日用電量時,模型的輸入層需要三個神經元,分別接收居住人數、房屋大小、戶外溫度等前三個欄位的數值;輸出層只需要一個神經元,用來輸出對應於第四個欄位,也就是用電量的預測值。至於隱藏層,原書就隨意使用一層,神經元的數量則是四個。這個設計的模型長這樣:

這裡要注意的是,因為就只預測一個數值,所以模型就只有一個輸出,但這並不代表迴歸就只能有一個輸出。事實上,機器學習的模型,也可以用迴歸來預測多個數值;在這種情況之下,模型的輸出就不會只有一個,而是有很多個。另外也要注意的是,因為要預測的是連續型的數值,所以輸出層神經元的啟動函數,要使用像sigmoid這類能輸出連續型數值的函數。
ml5.js Keras Syntax
知道設計分類用及迴歸用模型的訣竅之後,現在就來看看怎麼用Keras實做出模型。
Keras有Sequential model、Functional API、Model subclassing等三種方式可以建造模型;這三種建造方式的差異,詳見Kerase官網〈Models API〉這分文件的說明。因為我們的模型結構很簡單,就一層一層地堆疊起來而已,所以用Sequential model的方式來建造就可以了。
用Sequential model的方式來建造模型有兩種寫法,一種是把模型各層一次全部列出,全寫在Sequential()裡頭,另一種是一層一層加進去。一層一層加進去,程式碼看起來比較清爽,也比較容易除錯,所以後續建造模型時,就用一層一層加進去的方式來寫。
接下來,就實際用Sequential model的方式來建個模型。假設我們要建造的是前面提到的,用來分類鳶尾花的那個模型;在那個模型中,輸入層、隱藏層、輸出層,各有4、5、3個神經元,
建造模型時,要先準備一個空的模型;程式這樣寫:
model = keras.models.Sequential()
再來就是用add(),一層一層依序的把輸入層、隱藏層、輸出層等各層加進model中。
首先來加入輸入層。
輸入層的作用,就只是用來接收輸入的資料而已,所以在Keras中,它並不被認為是貨真價實、實實在在的一層,因此建造的方式也就異於其他各層的建造方式。輸入層的建造方式,是將一個Input物件加進model中;像這樣:
model.add(keras.Input(shape=(4,)))
Input物件裡頭的參數shape,資料型態是tuple。因為輸入層有四個輸入,所以就寫成shape=(4,);這裡的(4,)是python的語法,就代表這是個一維的tuple。關於輸入層的種種,可以參考〈Input Keras Layer Explanation With Code Samples〉這篇文章,裡頭有非常詳盡的說明。
加入輸入層之後,再來是加入隱藏層和輸出層。這兩層的結構都是緻密層的結構,所以建造方式是一樣的,都是用Dense();程式如下:
# 加入隱藏層
model.add(keras.layers.Dense(units=5, activation='relu'))
# 加入輸出層
model.add(keras.layers.Dense(3, activation='softmax'))
參數units,指的是神經元的數量;而activation則是指啟動函數。因為現在是要設計分類器,所以輸出層的啟動函數要使用softmax。
模型的建造就這樣,只要觀念搞清楚了,其實程式寫起來挺簡單的。建造分類鳶尾花那個模型的完整程式如下:
import keras
model = keras.models.Sequential()
# 加入輸入層
model.add(keras.Input(shape=(4,)))
# 加入隱藏層
model.add(keras.layers.Dense(units=5, activation='relu'))
# 加入輸出層
model.add(keras.layers.Dense(3, activation='softmax'))
模型建造完成後,可以用model.summary()來看看建造的結果:

從圖中可以看出,就只有列出隱藏層和輸出層,輸入層並未列出。最右邊那一欄,也就是「Param #」那一欄,指的是那一層參數的數量;在這裡就等於是權重的數量。以隱藏層來說,每個神經元的輸入,從輸入層來的有四個,加上偏置,所以總共會有五個權重。因為隱藏層共有五個神經元,所以權重的數量,總共會有5×5=25個。至於輸出層的權重數量,從隱藏層來的輸入有五個,加上偏置,所以每個神經元會有六個權重。輸出層共有三個神經元,所以權重的總數是6×3=18個。













