翻譯Deep Learning and the Game of Go(9)第六章:給圍棋數據設計神經網絡(下)

6.4.使用卷積網絡對空間進行分析

在圍棋中,你經常會看到一些特定的局部棋形。人類玩家已經學會識別幾十種棋形,並經常給它們起一些令人回味的名字(比如老虎口,雙關,或我個人最喜歡的,花六)。要像人類一樣做出決定,我們的圍棋A I還必須認識到許多的局部棋形。現在有一種特殊類型的神經網絡,稱爲卷積網絡,它是專門爲檢測像這樣的空間形狀而設計的。卷積神經網絡(CNNs)有很多遊戲以外的應用:你會發現它們被應用到圖像、音頻,甚至文字。本節演示如何構建CNN並將其應用於圍棋遊戲數據。首先,我們要介紹卷積的概念。接下來,我們將展示如何在Keras中構建CNNs,最後,我們展示處理卷積層輸出的有用方法。 

6.4.1.什麼卷積是直觀的?

卷積層和我們構建的網絡是從計算機視覺的傳統操作中得到他們的名字:卷積。卷積是一種直觀的變換圖像或應用過濾器的方法。對於兩個相同大小的矩陣,通過以下方法計算簡單卷積:

  1. 將這兩個矩陣裏的元素對應相乘
  2. 計算先前矩陣所有值的和

這樣一個簡單卷積的輸出是一個標量值。圖6.5顯示了這樣一個操作的示例,將兩個3×3矩陣卷積起來計算標量。 

這些簡單的卷積本身並不能馬上對你有幫助,但它們可以用來計算更復雜的卷積,而更復雜的卷積對你的用例是有用的。現在我們一開始不從兩個相同大小的矩陣開始,讓我們固定第二個矩陣的大小,並任意增加第一個矩陣的大小。在這個場景中,您將第一個矩陣稱爲輸入圖像,第二個矩陣稱爲卷積內核,或簡單的內核(有時您也會看到使用的過濾器)。由於內核比輸入圖像小,您可以在輸入圖像的許多塊上計算簡單的卷積。在圖6.6中,你看到這樣一個卷積操作,一個10×10的輸入圖像與一個3×3內核在相互作用。

圖6.6中的示例可能會給您第一個提示,來說明爲什麼卷積對我們來說是有趣的。輸入圖像是由中心4×8塊的1被0包圍的10×10矩陣。被選擇的內核,矩陣的第一列(-1,-2,-1)爲第三列(1,2,1)的相反數,中間列均爲0。因此,以下幾點是正確的:

  • 無論何時將此內核應用於輸入圖像的3×3塊,其中所有像素值相同,其卷積的輸出將是0。
  • 當您將此卷積內核應用於圖像塊時,左列的值比右列高,卷積將是負的。
  • 當您將此卷積內核應用於一個圖像塊時,右列的值比左邊高,卷積將是正的。

卷積內核被選擇去檢測輸入圖像中的垂直邊緣。一個物體左邊邊緣將有正值,而右邊邊緣是負值。這正是您可以在圖6.6中的卷積結果中看到的。

圖6.6中的內核是許多應用程序中使用的經典內核,稱爲Sobel內核。如果你把這個內核翻轉90度,你最終會得到一個水平邊緣檢測器。同樣,您可以定義使圖像模糊或銳化、檢測角和任何其他事情的卷積內核,其中許多內核可以在標準圖像處理庫中找到。

有趣的是看到卷積可以用來從圖像數據中提取有價值的信息,這正是您打算從圍棋數據中預測下一步落子所要做的事情。雖然在前面的例子中,我們選擇了一個特定的卷積核,但是卷積方式是在神經網絡中使用這些核通過反向傳播從數據中學習得到的。

到目前爲止,我們已經討論瞭如何將一個卷積核應用於一個輸入圖像。一般來說,將許多內核應用於許多圖像以產生許多輸出圖像是有用的。爲什麼可以這樣做?假設您有四個輸入圖像並定義了四個內核,然後你可以計算每個輸入和輸出圖像的卷積之和。在下面的內容中,您將調用這樣的卷積特徵映射的輸出圖像。現在,如果您想要五個而不是一個結果的特徵映射,你需要五個內核。利用n×m個卷積內核將n個輸入圖像映射到m個特徵映射,稱爲卷積層。圖6.7就說明了這種情況。

這樣看來,卷積層是一種將多個輸入圖像轉換爲輸出圖像的方法,從而提取輸入的相關空間信息。特別地,你可能有預想到,卷積層可以被鏈化,從而形成有卷積層的神經網絡。通常,僅由卷積層和Dense層組成的網絡被稱爲卷積神經網絡,或者簡單地說是卷積網絡

深度學習中的張量

我們需要指出的是,卷積層的輸出是一堆圖像。雖然這樣子是有幫助的,但還有更多的事情要做。正如向量(1D)由個別條目組成,它們不僅僅是一堆數字。同樣,矩陣(2D)由列向量組成,但其具有固定的二維結構,可用於矩陣乘法和其他操作(如卷積)。一個卷積層的輸出具有三維結構。卷積層中的濾波器具有更多的一維,並且具有4D結構(每個輸入和輸出圖像組合的二維濾波器),而且它並沒有停止——先進的深度學習技術可以處理更高維度的數據結構。

在線性代數中,向量和矩陣的高維等價物是張量,在附錄A有更多的細節。想得到張量的更多具體知識,但我們不能在這裏討論張量的定義,這本書的剩下部分中,你不需要知道任何正式的張量定義。但是,張量給了我們方便的術語,我們在後面的章節中將會使用。例如,從卷積層輸出的圖像集合可被稱爲3-Tensor,卷積層中的4D濾波器形成4-Tensor,因此你可以說卷積是一種運算一個4-Tensor(卷積濾波器)在一個3-Tensor(輸入圖像)上工作,並將其轉換爲另一個3-Tensor。

更一般地,你可以說順序神經網絡是逐步變換不同維數張量的一種機制。這種利用張量在網絡中“流動”來輸入數據的思想正是由此產生了TensorFlow這個名字,它是谷歌最受歡迎的機器學習庫,你將可以用來運行你的Keras模型。

請注意,在所有這些討論中,我們只討論瞭如何通過卷積層提供數據,而沒有討論反向傳播將如何做。我們故意把這一部分排除在外,因爲它在數學上超出了這本書的範圍,更重要的是,Keras已經爲我們實現反向傳遞做了一些工作。

一般來說,卷積層與dense層相比有更少的參數。如果在28×28的輸入圖像上定義一個內核大小(3,3)的卷積層,將會導致26×26的輸出,則卷積層具有3×3=9個參數。在卷積層中,您通常也會有一個偏置項,它被添加到每個卷積的輸出中,因此總共產生了10個參數。作爲比較,dense層連接28×28的輸入向量和26×26的輸出向量,則這樣層將具有28×28×26×26=529984個參數,並且不包括偏移量。同時,卷積運算在計算上比dense層中使用的矩陣乘法要耗費更多的時候。

6.4.2.使用Keras構建卷積神經網絡

要使用Keras構建和運行卷積神經網絡,您需要使用一種名爲Conv2D的新圖層類型在二維數據上執行卷積,如圍棋棋盤數據。你還可以瞭解另一個稱爲Flatten的層,它將卷積層的輸出展平爲向量,然後將其輸入到dense層。

首先,您的輸入數據的預處理步驟看起來與以前有點不同,並不是扁平化圍棋棋盤,而是保持其二維結構完整。

import numpy as np
import keras
from keras.models import Sequential
from keras.layers import Dense


# 通過設置隨機種子,您可以確保此腳本完全可複製
np.random.seed(123)

# 1.獲取輸入數據
# 加載特徵(就是局面)
X = np.load("../generate_game/features-200.npy")
# 加載標籤(當前局面的落子)
Y = np.load("../generate_game/labels-200.npy")

# 樣本數量
num_samples = X.shape[0]
board_size = 9
# 輸入數據形狀是三維的;您使用一個平面表示9×9的棋盤。
input_shape = (board_size,board_size,1)

# 將樣本形狀改掉
X = X.reshape(num_samples,board_size,board_size,1)

# 拿出90%用來訓練,10%用於測試
train_num_samples = int(0.9*num_samples)
X_train,X_test = X[:train_num_samples],X[train_num_samples:]
Y_train,Y_test = Y[:train_num_samples],Y[train_num_samples:]


現在您可以使用Keras的Conv2D對象來構建網絡。需要使用兩個卷積層,然後將第二層的輸出展平,然後使用兩個dense層,和以前一樣。

#模型定義
model = Sequential()
# 網絡中的第一層是具有32個輸出濾波器,內核尺寸3*3的Conv2D層
# 通常,卷積的輸出尺寸小於輸入。通過添加padding="same",您可以要求Keras在邊緣周圍用0來填充矩陣,使得輸出具有與輸入相同的尺寸
model.add(Conv2D(filters=32,kernel_size=(3,3),activation="sigmoid",padding='same',input_shape=input_shape))
# 與上一層幾乎一樣,只是因爲不是第一層,不需要傳入輸入數據形狀
model.add(Conv2D(filters=64,kernel_size=(3,3),activation="sigmoid",padding='same'))
# 然後,將上一卷積層的3D輸出壓平
model.add(Flatten())
# 增加兩個Dense層
model.add(Dense(300,activation="sigmoid"))
model.add(Dense(board_size*board_size,activation="sigmoid"))
model.summary()

該模型的編譯、運行和評估可以與前面的示例完全相同。

# 模型編譯
model.compile(optimizer="sgd",loss="mean_squared_error",metrics=["accuracy"])

#模型訓練和評估
model.fit(X_train,Y_train,batch_size=64,epochs=15,verbose=1,validation_data=(X_test,Y_test))

score = model.evaluate(X_test,Y_test,verbose=0)
print("loss:",score[0])
print("accuracy:",score[1])

您唯一更改的是輸入數據形狀和模型本身的規範。

如果您運行起來,您將看到測試精度幾乎沒有改變:它應該再次降落在1.3%左右的某個地方。這完全沒問題。在本章的其餘部分,我們將介紹更先進的深度學習技術,以提高您的落子預測精度。

6.4.3.用池化層縮小空間

在大多數具有卷積層的深度學習應用中,你會發現一種常見的技術是池化技術。你用池化去縮小圖像,減少之前層神經元的數量。

池化的概念很容易解釋:你可以通過將圖像分組或池化圖的塊成一個簡單值來降低圖像樣本。圖6.8中的示例演示瞭如何切割圖像,即保持圖中2*2的塊最大值

這種技術稱爲最大池,用於池的不相交塊的大小稱爲池大小。您還可以定義其他類型的池;例如,計算一塊中的平均值,這個版本稱爲平均池。您可以定義一個神經網絡層,通常在卷積層之前或之後,如下所示。

# 增加2*2的最大池
model.add(MaxPooling2D(pool_size=(2, 2)))

加了一層池化層後結果如下,精確性提高了一些 

您還可以將Max Pooling2D替換爲Average Pooling2D,結果如下,差不多

對於圖像識別這樣的情況,池在實踐中往往是必不可少的,可以減少卷積層輸出的大小。雖然該操作會使圖像丟失了一些信息,但它通常會在減少計算量的情況下,保留足夠的信息來進行準確的預測。

在您看到在池化層之前,讓我們討論一些其他工具,這些工具將使您的圍棋落子預測更加準確。 

6.5 預測圍棋落子準確性

自從我們在第五章中第一次引入神經網絡以來,您只使用了一個激活函數:Sigmoid函數。另外,您一直使用均方誤差作爲損失函數。這兩種選擇在開始使用時挺好,可以在你的深度學習工具箱中佔有一席之地,但並不特別適合我們的用例。

最後,當預測圍棋落子時,一個真正的問題:對於棋盤上的每一個可能的落子點,這個落子點是下一步要下的落子點可能性有多大?在每個時間點上,許多圍棋落子都是可以信賴的。深度學習可以充分了解遊戲的結構,以預測落子的可能性。你想預測所有可能落子的概率分佈,而sigmoid函數並不能保證實現這個功能。爲了取代這個函數,您引入了Softmax激活函數,該函數用於預測最後一層的概率。 

6.5.1.使用最後一層的Softmax激活函數

Softmax激活函數是Sigmoid的直接推廣。要計算向量x=(x1,...,XL)的Softmax函數,首先應用指數函數到每個組件;然後計算。接着用所有值之和對每個值進行規範化: 

根據定義,Softmax函數的分量是非負的,加起來是1,這意味着Softmax得出了概率。讓我們用一個例子來看看它是如何工作的。 

import numpy as np


def softmax(x): 
    e_x = np.exp(x)
    e_x_sum = np.sum(e_x) 
    return e_x / e_x_sum 

x = np.array([100, 100]) 
print(softmax(x))

 在Python中定義Softmax後,您將其計算長度爲2的向量,即x=(100,100)。如果計算sigmoid(x),結果將接近(1,1),但是計算softmax(x)則是(0.5,0.5)。這就是你應該期待的:因爲softmax函數的值會合併爲1,而且兩個條目是相同的,所以softmax分配給兩個組件的概率相同的。

大多數情況下,你會看到softmax激活函數作爲神經網絡最後一層的激活函數,這樣你就可以保證得到預測輸出概率

model.add(Dense(9*9, activation='softmax'))

6.5.2.分類問題的交叉熵損失

在前一章中,你以均方誤差作爲損失函數,我們注意到它不是你用例的最佳選擇。爲了跟進這件事,讓我們仔細看看什麼地方可能出錯,並提出一個可行的替代方案。

回想一下,您將您的落子預測用例描述爲一個分類問題,其中您有9×9個可能的類,而只有其中一個類是對的。正確的類被標記爲1,其他的都被標記爲0。您對每個類的預測將始終是0到1之間的值。從你的預測數據看起來這是一個強有力的假設,而你正在使用的損失函數應該反映這一點。如果你使用均方誤差,用預測和標籤之間的差異的平方,事實上沒有被限制在0到1的範圍內。事實上,均方誤差對於迴歸問題最有效,其中輸出是一個連續的範圍。想想預測一個人的身高。在這樣的情景下,均方誤差將限制巨大的差異,預測和實際結果之間的絕對最大差異趨於1。

均方誤差的另一個問題是它用同樣地方式限制了所有81個預測值。最後,你只關心一個正確的類,標記爲1。假設你有一個預測正確的落子,其值爲0.6,而其他被標記爲0的點分配概率加起來是0.4。在這種情況下,平均平方誤差爲(1-0.6)²+(0-0.4)²=2×0.4²,約爲0.32。你的預測是正確的,但對於兩個非零預測值你給出了相同的損失值:大約0.16。如果情況是正確落子點0.6,另外兩個落子點是0.2,那麼其均方誤差是(1-0.6)²+2*0.2²,大約0.24,這比前面的場景要低得多。但如果0.4的數值更精確一點,這個點也可能是下一落子的候選點,而你真的應該用你的損失函數去排斥這一點嗎?

爲了解決這些問題,我們介紹了分類交叉熵損失函數,簡稱交叉熵損失。對於模型的標籤和預測值y,此損失函數定義如下:

請注意,儘管這看起來涉及到很多計算,但對於我們的用例,這個公式可以歸結爲一個簡單的東西:其中一個是1。對於索引i,當是1的時候,交叉熵誤差是簡單的-log()。很簡單,你能從中得到什麼?

  • 由於交叉熵損失只限制標籤爲1的點,而其他所有值的分佈並不會直接受它影響。特別地,在您以0.6的概率預測正確的下一步落子的場景中,另一個0.4落子點或另兩個0.2落子點之間沒有區別。在這兩種情況下,交叉熵損失都是-log(0.6)=0.51。
  • 交叉熵損失是根據[0,1]的範圍量身定做的。如果你的模型預測實際落子點的概率爲0,這肯定是錯的。您知道log(1)=0,x在0到1之間的-log(x)在x接近0時接近無窮大,這意味着-log(X)可以變得任意大(而不像MSE那樣二次變化)。
  • 此外,當x接近1時,MSE下降得更快,這意味着對於不那麼好的預測,損失要小得多。圖6.9給出均方誤差和交叉熵損失的直觀對比。 

區分交叉熵損失和均方誤差的另一個關鍵點是它在隨機梯度下降(SGD)學習過程中的行爲。事實上,均方誤差在獲得更好預測值的時候,梯度會越來越小,導致其學習通常會減慢。與此相比,使用交叉熵損失時,SGD並沒有放緩,參數更新與預測值和真實值之間的差異成正比。我們不能在這裏討論細節,但這對我們的落子預測用例來說是一個巨大的幫助。

使用分類交叉熵損失函數來編譯keras模型而不用均方誤差,其實現也是十分簡單的

model.compile(loss="categorical_crossentropy"...)

隨着交叉熵損失和Softmax激活函數應用到你的用例中,您現在可以更好地處理分類標籤和使用神經網絡預測概率。爲了結束這一章節,讓我們添加兩種技術,允許您構建更深層次的網絡

替換編譯時用的損失函數,結果顯示精確概率1.8%左右,比之前提高了一些

6.6.用ReLu和DropOut去構建更深層的網絡

到目前爲止,你還沒有建立一個超過2到4層的神經網絡,你可能希望通過增加相同的內容去提高準確率,如果真能這樣就太好了,但在實踐中,你有幾個方面需要考慮。儘管不斷地建立更深層次的神經網絡可以增加模型的參數數目,從而提高模型適應你輸入數據的能力,但這樣做你也可能會遇到麻煩。導致失敗的主要原因之一是過度擬合:你的模型在預測訓練數據方面變得越來越好,但是在測試數據上表現不行。舉一個極端情況,對於一個幾乎完全可以預測,甚至能夠記住它以前看到過的東西的模型來說,當其遇到稍微有些不同的數據時就不知道該怎麼辦了,因此你需要會概括。對於像圍棋這樣複雜的遊戲,要去預測下一步落子,這是特別重要的。不管你花多少時間去收集訓練數據,在遊戲中總是會出現你的模型以前沒有遇到過的情況。

6.6.1.將神經元丟棄使之規範化

 防止過度擬合是機器學習中普遍面臨的挑戰。您可以找到許多關於規範化技術的文獻,這些技術旨在解決過度擬合問題。對於深度神經網絡,你可以應用一種令人驚訝的簡單卻有效的技術,稱爲dropout。當dropout應用於網絡中的一層時,每個訓練步驟都會隨機選擇一些神經元設置爲0;然後在訓練過程中完全刪除這些神經元。在每個訓練步驟中,您隨機選擇要丟棄的新神經元。這通常是通過一個特定的丟棄概率來完成的。圖6.10顯示了一個dropout層的示例,在該層中,每個小訓練集(前向和後向)的神經元都有一半的機率被丟棄。

這一過程背後的原理是,通過隨機丟棄神經元,您可以避免單個層,從而防止整個網絡對給定數據的過度擬合化。層必須要足夠靈活,不能過分依賴單個神經元。通過這樣做,你可以防止你的神經網絡過度擬合。在Keras中,您可以定義具有降低活性率的dropout層,如下所示。 

from keras.layers import Dropout 


model.add(Dropout(rate=0.25))

您可以在其他的每層之前或之後的順序網絡中添加類似的dropout層。特別是在更深層次的體系結構中,添加dropout層往往是必不可少的。

6.6.2 ReLU函數

作爲本節的最後一個構建塊,您將瞭解校正線性單元(ReLU)激活函數,它通常比Sigmoid和其他激活函數對深度網絡產生更好的激活功能。圖6.11顯示了ReLU的形狀

通過設置爲0來忽略負輸入,返回正輸入不變。正信號越強,ReLU激活越強。考慮到這種解釋,線性單元激活函數非常接近大腦中神經元的一個簡單模型,其中較弱的信號被忽略,而較強的信號會導致神經元的放電。在這個基本的類比之外,我們不關心ReLU的任何理論好處,但請注意,使用它們往往會得到令人滿意的結果。若要在Keras中使用Relu,請將任何層的激活參數Sigmoid替換爲Relu,測試如下,精確概率提高了不少

6.7.把上面的改進全部放在一起加強網絡預測準確率

前面的章節不僅引入了具有最大池化層的卷積網絡,而且還引入了交叉熵損失、最後一層的Softmax激活函數、丟棄規範,和ReLU激活函數以提高您的網絡的性能。爲了結束這一章,讓我們一起把你學到的所有新的成分都輸入一個神經網絡,來看看你的神經網絡在預測圍棋落子方面的正確率怎麼樣。

首先,讓我們回顧一下如何加載圍棋數據,用簡單的one-plane編碼,併爲了卷積網絡重塑它的形狀。


# 通過設置隨機種子,您可以確保此腳本完全可複製
np.random.seed(123)

# 1.獲取輸入數據
# 加載特徵(就是局面)
X = np.load("../generate_game/features-200.npy")
# 加載標籤(當前局面的落子)
Y = np.load("../generate_game/labels-200.npy")

# 樣本數量
num_samples = X.shape[0]
board_size = 9
# 輸入數據形狀是三維的;您使用一個平面表示9×9的棋盤。
input_shape = (board_size,board_size,1)
接下來,讓我們增強您以前的卷積網絡,如下所示:
  • 保持基本架構完整,從兩個卷積層開始,然後一個最大池層和兩個密集層。
  • 爲了規範化我們添加三個dropout層:在每個卷積層和第一個密集層之後。使用50%的丟棄率。
  • 將輸出層的激活函數更改爲Softmax,內部層更改爲ReLU。
  • 改變損失函數爲交叉熵損失,而不是均方誤差。
#模型定義
model = Sequential()
# 網絡中的第一層是具有32個輸出濾波器,內核尺寸3*3的Conv2D層
# 通常,卷積的輸出尺寸小於輸入。通過添加padding="same",您可以要求Keras在邊緣周圍用0來填充矩陣,使得輸出具有與輸入相同的尺寸
model.add(Conv2D(filters=32,kernel_size=(3,3),activation="relu",padding='same',input_shape=input_shape))
#添加dropout層
model.add(Dropout(rate=0.6))
# 與上一層幾乎一樣,只是因爲不是第一層,不需要傳入輸入數據形狀
model.add(Conv2D(filters=64,kernel_size=(3,3),activation="relu",padding='same'))
# 添加一個最大池化層
model.add(MaxPool2D(pool_size=(2,2)))
# model.add(AveragePooling2D(pool_size=(2,2)))
#添加dropout層
model.add(Dropout(rate=0.6))
# 然後,將上一卷積層的3D輸出壓平
model.add(Flatten())
# 增加兩個Dense層
model.add(Dense(300,activation="relu"))
#添加dropout層
model.add(Dropout(rate=0.6))
model.add(Dense(board_size*board_size,activation="softmax"))
model.summary()

# 模型編譯
model.compile(optimizer="sgd",loss="categorical_crossentropy",metrics=["accuracy"])

最後,評估這個模型

#模型訓練和評估
model.fit(X_train,Y_train,batch_size=64,epochs=5,verbose=1,validation_data=(X_test,Y_test))

score = model.evaluate(X_test,Y_test,verbose=0)
print("loss:",score[0])
print("accuracy:",score[1])


# 嘗試預測
# 表示棋盤的矩陣
test_board = np.array([[
    0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, -1, 1, -1, 0, 0, 0, 0,
    0, 1, -1, 1, -1, 0, 0, 0, 0,
    0, 0, 1, -1, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0,
]])

test_board = test_board.reshape(1,board_size,board_size,1)

# 輸出一個棋盤局面下的預測值
move_prob = model.predict(test_board)[0]
i = 0
for row in range(9):
    row_formatted = []
    for cow in range(9):
        row_formatted.append('{:.3f}'.format(move_prob[i]));
        i += 1
    print(' '.join(row_formatted))

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章