由於原文過長,機器之心在編譯過程中進行了少量刪節。想了解更多細節,請檢視原文連結:https://colab.research.google.com/drive/1VdwQq8JJsonfT4SV0pfXKZ1vsoNvvxcH#scrollTo=C810qURdm3hZ。
想學 AI 又擔心沒有數學背景或軟體背景?沒關係,這篇部落格非常適合你。
我曾經花費一年半的時間自學各種線上課程和部落格,過程中有太多專家、太多資訊,而且他們的很多觀點還有衝突。我在學習過程中經常充滿自我懷疑。
我不想要很多專家幫我學習,我只想要一個老師。我希望有人能夠拉著我的手說:「Dave,這就是你需要學的東西,請按照這個順序學。現在我將用圖畫、有趣的故事、現實示例和淺顯易懂的語言教你學習 AI。」
而現在,我就是那個老師。
為什麼是我?和網上的專家不同,我沒有數學或程式設計背景,不過我曾就讀於耶魯大學和普林斯頓大學,環遊過 100 多個國家,是一名旅行作家,我曾在《週六夜現場》工作,我的作品得過獎。也就是說,我知道如何透過寫作來傳達複雜的概念,知道如何講故事。我熱愛教學,也善於發現好的老師。在學習深度學習過程中我遇到了四位優秀的老師,他們是 Andrew Trask、Grant Sanderson、Kalid Azad 和我的導師 Adam Koenig。
最重要的是,我理解你正在經歷的痛苦。你聽說過「專家盲區」(expert blindness)嗎?專家向新手講授某個學科時,由於他成為專家的時間太久,會忘了初學者對教材的感受。因此專家會快速介紹一些複雜的概念,而這些概念需要分解成小塊才能方便初學者掌握。或者他們不使用類比、圖畫或示例來幫助初學者掌握概念,導致初學者非常受挫。
每一個初學者都想要專家來教他們 AI。而事實上,你需要的不是專家,而是一名老師。
最好的教師就是那個剛剛學過這些知識的人,因為他仍然記得自己掙扎過的地方以及克服方法,並且他可以向你傳授捷徑。而我就是這個人。我不是專家,但我是個好老師,而且富有激情。
本文使用指南
本文的閱讀過程和小說不同,只讀一次是無法理解和掌握所有內容的。我學數學的朋友告訴我,他們常常需要讀至少 7 遍數學文字才能開始理解。這不是開玩笑……
為方便講授,我使用了類比、圖畫、示例和幾何表示。但是請放心,本文在數學層面上是準確而嚴謹的。請做好閱讀本文五遍的準備,無法立刻領會也不要著急。
我在學習複雜材料的時候,通常會設定計時器,每五分鐘響一次,不斷地提醒自己不要在絕望中沉淪,要微笑、耐心並堅持下去。這真的有效,相信我。
以下是一些宏觀要點:
神經網路是深度學習中非常流行的前沿技術;
深度學習是機器學習的分支;
機器學習是人工智慧的分支。
深度學習包括四個主要概念。本文的目標是讓讀者掌握這四個深度學習基礎概念:
前饋
梯度下降
全域性最小值
反向傳播
之前對這四個概念一無所知?沒關係,首先我會使用類比和圖示儘量簡單地講授這些知識,然後不斷地回到這四個概念,探討其中的細節。你應該將本文看作一個「螺旋上升」的學習過程,每一次回到這些概念時你都會收穫更多見解。
本文共有五個部分:
1. 深度學習概覽:示例、類比、圖示、玩笑
2. 28 行程式碼建立神經網路:神經元和突觸
3. 前饋:做出有根據的猜測,60000 次迭代
4. 從試錯中學習:梯度下降和全域性最小值
5. 反向傳播:鏈式法則
1. 深度學習概覽
1.1 示例
想象你是一家寵物店的老闆,事業經營得很成功,而這成功很大程度上是因為你善用 AI 技術。你構建了一個深度神經網路,來選擇潛在新顧客,並向他們傳送廣告。一個月前,你上線了一款新貓砂「Litter Rip!」。你試圖找到願意給自己的貓使用這款貓砂的顧客。
而你的秘密武器是資料集。新貓砂上線一個月後,你對寵物店顧客進行了調查並收集了一些資料。調查包括以下問題:
您有貓嗎?
您喝進口啤酒嗎?
過去一個月,您是否訪問過我們的網站 LitterRip.com?
過去一個月,您是否購買過 Litter Rip! 貓砂?
這四個問題的答案即之前顧客的「特徵」(feature)。
那麼問題來了:什麼使得 AI 網路如此強大?
答案是:它使用調查結果進行訓練,從而準確地預測未來顧客的購買行為。
首先,你需要將之前顧客的調查資料和他們對前三個問題的回答輸入到網路中,進行訓練。該網路使用這些資料預測某位顧客是否確實購買了新款貓砂。然後網路再將預測結果與顧客第四個問題的答案進行對比。第四個問題的答案就是標籤,作為事實供網路進行對比。例如,如果網路預測結果是「是的,我認為這位顧客買過 Litter Rip! 貓砂」,而這位顧客第四個問題的答案確實是「Yes」,那麼你就擁有了一個成功的神經網路。
神經網路透過試錯進行自我訓練:網路先預測,然後對比預測結果與第四個問題的真正答案,再從錯誤中學習,並在多次迭代中不斷改進。
神經網路通常在一個資料集上訓練,在另一個資料集上執行預測,理解這一點很重要。一旦你的神經網路很擅長根據之前顧客的調查資料預測新款貓砂的購買情況,那麼你就可以換一個新的資料集,該資料集包含潛在新顧客的名單。
你從獸醫那裡得到了新的資料集,這些被調查者回答了前三個問題。現在,如果讓你用訓練好了的網路在潛在新顧客中預測廣告投放的最佳物件,你該怎麼做呢?
我們來看下一節。
1.2 類比:神經元和突觸
下圖是我們將要構建的 3 層神經網路,圖中使用的是常見的「神經元和突觸」格式:
我們先來看這張圖,圖中是一個三層的前饋神經網路。左側為輸入層:三個圓圈表示神經元(即節點或特徵,該網路將使用前三個調查問題作為特徵)。現在,你看著這一列圓圈,想象它們分別代表一位顧客的答案。左上的圓圈包含問題 1「你有貓嗎?」的答案,左中圓圈包含問題 2「你喝進口啤酒嗎?」的答案,左下圓圈表示問題 3「你是否訪問過我們的網站 LitterRip.com?」的答案。那麼,如果顧客 1 對這三個問題的答案是「Yes/No/Yes」,則左上圓圈包含 1,左中圓圈包含 0,左下圓圈包含 1。
突觸(連線這些圓圈和隱藏層的所有線)是神經網路用來「思考」的部位。右側的單個圓圈(它依然和四個突觸相連)是網路的預測結果,即「基於輸入到網路的特徵組合,此處展示了這位顧客購買新款貓砂的機率。」
最右側標註「y」的單個圓圈表示真值,即每個顧客對第四個調查問題「你是否購買過 Litter Rip! 貓砂?」的回答。這個圓圈有兩個選擇:0 表示沒買過,1 表示買過。神經網路將輸出一個預測機率,並將其與 y 進行對比,檢視準確率,然後在下一次迭代中吸取教訓。神經網路在數秒時間內可以完成數萬次試錯。
上圖是神經網路的典型圖示。本質上,它描述的是前饋,即我們要介紹的第一個主要概念。你可能以為神經元是該過程中最重要的部分,但是這裡的類比似乎存在一些誤導性。事實上,本文要介紹的四個深度學習主要概念的共同驅動力是突觸。因此,目前這部分最重要的知識點是:突觸使得預測發生。下面一節我會把這一概念類比為落進碗裡的乒乓球。
在進行下一個類比之前,我想首先詳細解釋一下神經網路之所以強大的原因。
1.3 類比:碗與球
重點來了!人工智慧如此強大的原因是:神經網路使用機率對下一次預測進行漸進式的改進。該過程將試錯學習提升到一個全新的層次。
我們先來看一下人類是如何預測的:假設你的桌子上有一些之前顧客的調查結果,旁邊還有一疊潛在新顧客的調查結果(即獸醫提供給你的調查資料)。人類如何使用之前顧客的調查結果預測未來顧客的購買行為呢?你可能會想:「我的模糊邏輯告訴我,喝進口啤酒的顧客和買新款貓砂的顧客沒有關聯。我檢視了顧客調查結果,試圖尋找出一種模式,我認為擁有貓和訪問過 LitterRip.com 網站的顧客購買過 Litter Rip! 貓砂。」
在只有三個調查問題、四名被調查顧客時,這是可行的。但是如果有 40 個問題、4000 名顧客呢?人類如何決定哪一個問題作為執行準確預測的核心因素?人類大腦能夠容納的數量是有限的,我們很難量化 40000 名顧客中的某一位購買新款貓砂的機率。這個數字是 67% 還是 68%?誰知道呢!
現在我們來看神經網路如何執行預測:神經網路不會將預測侷限於直截了當的「是」或「否」,相反,它會預測出一個 0 和 1 之間的數字——機率。例如,0.67 表示「該顧客有 67% 的可能購買新款貓砂」,0.13 表示「該顧客有 13% 的可能購買新款貓砂,可能性較小。」
這就說明了為什麼給出一個 0 到 1 之間的機率數字是明智的:就算計算機的第一次預測結果與實際情況大相徑庭也沒有關係。真正重要的是,網路會將機率 0.13 與真值進行對比(假設真值是 1,即該顧客購買了新款貓砂),網路會注意到它的預測結果偏離真值 0.87,這是一個比較大的誤差,因此網路會進行調整。網路將保持數字清晰展現,同時調整數字,增加一些值降低另一些值,以找到更好的問題組合,從而使下一次預測能夠得到更加準確的預測結果。將該步驟重複數萬次,直到計算機最終能夠自信地說:「我很高興地宣佈,現在我的預測結果可以媲美真值了,誤差幾近於零。現在我可以準確預測了。」
現在,你知道了深度神經網路的強大原因,它使用機率和試錯學習方法,漸進式地改進下一次預測的結果。
我可以用一幅簡單清晰的圖畫描述試錯學習過程:網路的試錯學習就像順著碗邊滾落的乒乓球,最終將落在碗底。
前面我解釋了神經網路如何執行預測:計算誤差,改善下一次的預測結果,直到誤差減少到幾乎為零。執行預測的神經網路就像順著碗側滾落的乒乓球。我們假設碗底就是「烏托邦」——準確的預測結果,那麼網路的第一次預測就是該「預測球」(乒乓球)的起始位置;第二次預測時,乒乓球沿著碗側向底部前進一點距離;第三次預測時,球又向碗底前進一點……如下圖所示,網路的每一次預測就是乒乓球向碗底前進時的新位置。
預測球滾落以及在到達完美位置(碗底)之前的準確率改進過程包括四步:
前饋:想象一下 1960 年的 IBM 計算機,大到填滿整個房間,穿孔卡片從一端輸入,答案從另一端輸出。上文提到的神經網路以前三個調查問題的資料作為輸入,得出預測結果;
全域性最小值:想象一下桌子上有一個黃色的碗(如上圖所示)。桌子表面表示幾乎零誤差的完美預測結果,那麼很顯然碗底是最接近完美預測結果的位置,具備最小的誤差。與碗整個表面(即「全域性表面」(global surface))相比,碗底最接近完美,它具備全域性最小誤差值。
網路每次進行更好的預測時,粉色的預測球沿著碗側向底部全域性最小誤差值前進。每一次預測後,網路將預測結果與第四個問題的答案進行對比,這類似於在特定時刻衡量預測球與碗底的距離。衡量預測結果與真值的距離叫做「找出誤差」。網路每次預測的目標都是持續地縮短與全域性最小值之間的誤差。
反向傳播:想象一位雜技表演者,他能向空中拋接 16 個不同大小和重量的保齡球瓶,並使它們同時懸在空中,甚至可以神奇地調整保齡球瓶的大小和重量。網路在執行預測後,會返回到上一次預測的過程中,檢視是否可以做一些調整,以便在下一次預測中縮小誤差,推動小球向碗底前進。
梯度下降:想象粉色乒乓球沿著碗側向碗底滾落,碗底即全域性最小值(見上圖)。網路就像那個球,碗的表面由網路的每一次預測構成。梯度下降就是球沿著碗側滾落向碗底(即具備全域性最小誤差的預測)的過程。
換句話說:
梯度下降是網路在達到準確預測(即全域性最小誤差)前的試錯過程,就像乒乓球滾落碗底的過程;
前饋即執行預測。預測就像給定時刻球停留在碗表面某個位置的定格影像;
全域性最小值即預測幾乎沒有誤差的完美位置(碗底)。我們的目標是到達碗底。網路將預測結果與真值進行對比,來衡量球目前位置與碗底的距離(誤差);
反向傳播即返回到上一次預測,找出錯誤並修正。反向傳播衡量球現在位置到其下桌面的距離(即誤差),並找出推動球向碗底前進的方法。
記住:我們現在只是粗略瞭解,所以即便有些地方沒有掌握也不要擔心。
在詳細解釋這些概念之前,我想把「碗和球」的類比再推進一步。上圖展示了神經網路訓練過程中的四個主要步驟,但過於簡化。它準確地表示了只有一個調查問題的網路,該網路也僅基於這一個問題做出預測。
但是,我們想做的是結合三個調查問題找出最佳預測。因此,如果網路在試錯迭代過程中使用不同問題組合進行試驗,碗會是什麼樣子呢?答案是:一隻凹凸不平的碗。
如下圖所示:
圖源:https://www.youtube.com/watch?v=IHZwWFHWa-w&t=2s&index=3&list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi
上圖中紅色的碗有一些凸起和凹陷,為什麼會這樣呢?
首先,我們必須理解紅色碗由什麼構成。從圖中看它似乎是塑膠做的……不過並不是,想象它由數百萬個紅點構成,每個紅點都是 3D 網格中的一個點。每個點表示一個可能的調查問題組合,以及該組合中哪個問題對於網路執行預測的等級更高。這個凹凸不平的碗讓我們看到了所有可能組合的凹凸不平的表面。
記住這幅圖:紅色碗是「所有排列組合的表面」,它像山脈一樣有高峰、深谷、山嶺、河床。網路無法窮盡浩瀚宇宙中每一個可能的排列組合,這會花費太多計算時間。因此,它從你隨機「拋擲預測球」的位置出發,一路向下走向谷底(即全域性最小值)。網路不需要管它從哪裡開始出發,它只需要在意從隨機的起始點到達碗底即可。
在解釋紅色的凹凸不平處之前,我們先來看從紅碗右上角開始的白色虛線。這條線的頂端就是網路的第一次預測。還記得上文介紹的紅色乒乓球嗎?假設乒乓球就在白線頂端處,白色虛線表示網路預測結果從起始點到全域性最小值的路徑。也就是說乒乓球沿著這條白色虛線走到誤差最小、預測準確率最高的地方。
但是為什麼這條白色虛線路徑如此彎曲呢?原因在於,網路的常規試驗需要考慮組合哪些問題、每個問題的權重多大,才能得到誤差最小的最佳預測結果。網路的常規目標是儘可能降低誤差,即讓乒乓球儘快到達紅碗底部。因此網路通常採用虛線上球所在點的坡度,來確定哪個方向具備最陡峭的坡度,能夠實現最快的向下路徑。由於坡度不斷改變,這條路徑就很曲折。我們來看一個簡單的例子:
人類第一眼就會判斷這兩個問題(「你有貓嗎?」和「你訪問過我們網站嗎?」)是做出預測的基礎問題。但是哪個問題對預測準確度影響最大呢?第一個問題影響更大,還是二者五五分?
網路想用這兩個問題的不同權重組合進行試驗,從而找出實現準確預測的最佳組合。上圖紅色碗中的每個凸起都表示「走在錯誤道路上」的問題組合和問題權重,因為每個凸起都使網路的「預測球」愈發偏離底部的全域性最小值。而碗中的每個凹陷都表示「走在正確道路上」的問題組合,因為它使預測球離碗底更近。
但是,如果網路找出了三個調查問題的完美權重組合,但預測準確率仍然只有 60% 左右,該怎麼辦?不要怕,網路還有另一個技巧:推斷問題(inferred question)。
下面我們用一個簡單示例講述這個重要概念:
我們回過頭看關於進口啤酒的那個問題。神經網路不停嘗試不同的問題組合。那麼舉例來說,或許只有富人才有錢買進口喜力啤酒,而我們都知道養貓的人很可能是富人。(本貓奴的內心:不,我不是,我沒有……)那麼也許當「你喝進口啤酒嗎?」和「你有貓嗎?」這兩個問題在網路計算過程中是組合問題且權重較高時,預測結果會得到改進。
這裡的推斷問題就是「富人更有可能購買 Litter Rip 貓砂嗎?」。不過我們剛才做了一個(愚蠢的)推斷:喝進口喜力啤酒和養貓的人可能比較富有。
但是,隨著粉色預測球在紅色碗中兜兜轉轉,它到了凸起處,而原因在於網路使用了這個推斷問題進行試驗。也就是說該推斷問題對於改善預測準確率並沒有幫助。(可能是因為貓砂不是奢侈品而是養貓者的必需品。)在接下來的迭代中,預測球離開凸起處,不再使用這個無用的推斷問題。但是它也有可能因為有用的推斷問題掉進凹陷處,從而幫助它更快地到達碗底。
上文的示例展示瞭如何測試推斷問題能否有效幫助實現更準確的預測結果。人類使用模糊邏輯探索推斷問題,而神經網路嘗試窮盡每一種排列組合。如果試驗獲得了較好的預測結果,它會保留該問題。如果試驗得到了更糟的預測結果,網路將拋棄該問題。
總之,紅色碗表面上每一個紅點表示網路使用特定問題組合和權重組合進行的一次試驗。每一個凹陷表示「這一步走在了正確的道路上」,網路注意到這一點並將繼續保持。每一個凸起處表示「這一步走錯了方向」,網路也會注意到並摒棄它。預測球的路徑(白色虛線)歪歪扭扭,是因為預測球在通往碗底的路上不斷尋找凹陷處、避免凸起處。你可以說白色虛線是預測球通往碗底的最高效路徑。
現在我們轉回來,更詳細地瞭解一下「前饋」、「全域性最小值」、「反向傳播」和「梯度下降」。
1.3.1 前饋:將穿孔卡片輸入到 1960 年的 IBM 計算機
前饋的目標是建立預測。假設每一次預測是預測球向碗底前進過程中的一個新位置。在上圖紅色碗中,網路做的第一次預測由白色虛線右上角的點表示,我們假設預測球就在這個點上。前饋是建立第一次預測的過程。白色虛線上的下一個點即是第二次預測,這樣預測球移動到下一個點,之後再進行第三次預測。
大部分 AI 課程和部落格不會提及,預測球所在的每個位置由紅色碗底部白色網格的兩個座標軸決定。紅色碗並非在空間中漂移,它位於白色網格之上,該網格具備 X 軸和 Y 軸。
還記得上圖介紹全域性最小值時所用的影像嗎?黃色碗位於桌面上。同理,紅色碗位於白色網格上。白色網格即「桌面」,「完美、無誤差的預測位置」即紅色碗實際位於的點。注意,碗與桌面唯一的連線點是碗底,即白色虛線結束的點(全域性最小值)。
預測球在紅色碗中的每次停頓,即每次預測點,都由 3 個座標軸決定:X 軸和 Y 軸表示網格位置,Z 軸表示預測球到網格的距離。
現在讓我們跳出抽象描述。3D 空間中這三個座標軸在現實生活中表示什麼?
回憶一下,紅色碗表面上每一個紅點表示網路使用特定問題組合和權重組合進行的一次試驗,那麼網路如何執行試驗呢?它使用 X 軸和 Y 軸在白色網格上定一個位置,表示特定的問題組合。X 軸和 Y 軸必然會問:「嗨,Z 軸你好,這個組合怎麼樣?」Z 軸將告訴我們該組合的效果——Z 軸負責衡量誤差。
我們之前提到,碗底,即碗與白色網格相連線的位置,是完美的預測。那麼碗中每一個紅點都表示誤差,離白色網格越遠,誤差就越大。
這點非常重要:白色網格表示網路可以嘗試的問題及權重組合。對於白色網格上的每一個點,其上方的紅點表示該組合的誤差值。也就是說,紅色碗上的每個紅點表示其下方預測的誤差。紅色碗就是「誤差組成的碗」。只有碗底,紅點接觸白色網格的地方,二者之間沒有距離,也因此沒有誤差。參見下圖:
上圖中的錐形黃色箭頭表示 Z 軸,用於衡量誤差。它很有用,你可以從中瞭解誤差值並學習,以便下次得到更加準確的預測結果。
想象一下,粉色的預測球在白色虛線上移動,黃色箭頭總是在預測球下方,並隨之移動。粉色球表示網路做出的每次預測,黃色箭頭衡量預測球到白色網格的距離,即衡量每次預測的誤差。它的執行原理是什麼?
你可能會想到前饋——以前三個調查問題作為輸入並用不同方式進行問題組合,再執行預測,預測結果為 0 到 1 之間的數字,即機率。想象預測球所在的點表示在白色虛線上的數字。我們知道第四個問題的答案是「1」,因此真值是該顧客買過新款貓砂。真值即是全域性最小值,是我們改進預測結果的目標,也是紅色碗和白色網格的接觸點。因此,網路用 1 減去預測機率。舉例來說,如果網路的第一次預測為 0.5,即該顧客有 50% 的可能性購買過這款貓砂,1 減去 50% 所得的數字表示誤差,黃色箭頭就用於衡量該誤差。某種程度上可以說,「真值數字 - 預測球數字 = 黃色箭頭數字」。在該示例中,即 1 - 0.5 = 0.5。誤差(黃色箭頭的長度)為 0.5(誤差通常用絕對值表示,不能為負值)。
沒有第四個調查問題的話,你無法訓練網路,因為你需要真值來測試預測結果。黃色箭頭衡量網路預測與真值的距離,即誤差,這個例子中真值為 1。我們的第一次預測(0.5)準確率並不高,0.99 的預測才可以說比較準確。
1.3.2 找到全域性最小值
我們的目標是訓練神經網路找出減少預測誤差的最快方式,也就是說讓黃色箭頭變短,這意味著我們需要預測球從起始點到碗底的最高效路徑。碗底即「烏托邦」,在那裡黃色箭頭(預測誤差)的長度幾乎為 0(全域性最小值),即我們的預測具備最小誤差,網路達到非常高的準確率。而預測球到達碗底的最高效路徑是那條白色虛線。一旦我們使用第一個資料集完成訓練,網路就可以準確預測另一撥潛在新客戶購買新款貓砂的機率。
現在想象這個場景:預測球從白色虛線頂端開始,此時黃色箭頭(即第一次預測的誤差值)等於 0.5。我們如何讓球到達碗底呢?也就是說,我們如何調整網路,使預測球(目前 X,Y 座標為 (3,3))在白色網格上移動到碗底(碗底點的座標大致為 (3,0))。目前,我們僅能基於顧客 1 的答案執行預測。那麼,我們如何基於每個顧客的回答改進之後每一次預測的結果,直到預測誤差幾乎為 0?也就是預測球到達碗底,網路訓練得足夠好,可以利用新資料集做出預測。
找到從不那麼準確的預測結果 0.5 到最終第 6 萬次預測結果 0.9999 的每一步路徑,這就是梯度下降過程。
1.3.3 梯度下降和反向傳播
梯度下降表示網路的試錯學習過程。對於數學 nerd 來說,它可定義為一個總體規劃:改變突觸權重、在每次迭代時最大程度上降低誤差。
對於普通人來說,這裡有一個更好的解釋:將小球沿著最陡峭的坡度滾下,儘快地到達碗底。反向傳播是一種計算梯度的方法,梯度其實就是坡度(slope)。反向傳播告訴你預測球所在位置的坡度。
坡度在這裡非常重要。所有專家都會使用術語「梯度下降」,而梯度的意思就是「坡度」。找到預測球在碗表面位置點的坡度,可以指示出球儘快到達碗底應該移動的方向。
但是,為什麼是坡度呢?請考慮梯度下降的過程:
首先,計算機執行前饋預測。用真值(全域性最小值所在位置,即碗底)減去預測得到誤差(黃色箭頭長度),然後使用反向傳播計算誤差的坡度。坡度決定了預測球滾動的方向和速度(即網路應該在數字方面做出多大的調整)。
找出坡度是反向傳播的重要工具。如果說梯度下降是總體規劃,那麼反向傳播就是達成規劃的主要工具。
2. 28 行程式碼建立神經網路
現在我們來看程式碼。我將首先展示今天要學習的全部程式碼,然後再進行詳細的步驟講解。建議在兩個視窗中開啟本文,一個視窗顯示程式碼,另一個繼續往下閱讀。從下文程式碼註釋中可以看到,我將構建神經網路的過程分解成了 13 個步驟。現在準備好觀看計算機從它所犯的錯誤中學習並最終識別出模式了嗎?
不過,首先我們先來了解矩陣和線性代數的概念。
在下文的程式碼中,你會看到單詞「matrix」或「matrices」(矩陣)。它非常重要:矩陣是這輛汽車的引擎。沒有矩陣,神經網路哪兒都去不了。
矩陣是多行數字的集合。你將遇到的第一個矩陣如下所示,它容納了寵物店顧客調查的資料。
[1,0,1],
[0,1,1],
[0,0,1],
[1,1,1]
將每一行想象為一名顧客,那麼以上矩陣中共有 4 位顧客,每一行的方括號中包含三個數字(1 和 0 分別表示「是」和「否」)。在 1.2 節中,我們看到顧客 1 對前三個調查問題的答案是「Yes/No/Yes」,因此該矩陣的第一行為 1, 0 ,1。第一列是四位顧客對第一個問題「你有貓嗎?」的回答。
為了更詳細地介紹矩陣這一概念,請看下圖。圖中表示的矩陣與上面的矩陣相同,不過它多了一些標籤和顏色,用於強調顧客的回覆是一行,特徵/問題是一列:
上圖釐清了一個最初使我非常困惑的點:行與列之間的關係。
在該矩陣中,每一位顧客的資料被表示為一行中的三個數字。在神經網路圖示中(神經元和突觸格式),輸入層是一個包含三個圓形神經元的列。你需要注意到,每個神經元並不表示一位顧客,即矩陣中的一行資料。相反,每個神經元表示一個特徵,即矩陣中的一列資料。因此,一個神經元內包含所有顧客對同一個問題/特徵的答案。拿第一個問題「你有貓嗎?」舉例,我們只選取了四位顧客的資料,因此上圖中該問題的回覆中只有四個數字(兩個 0、兩個 1),而如果我們選取了一百萬名顧客的資料,則上圖中該神經元內會包含一百萬個數字,表示這些顧客對該問題的回覆。
到這裡,我希望大家能夠理解我們為什麼需要矩陣:因為我們有不止一位顧客。在下面的神經網路示例中,我們描述了四位顧客,所以我們需要四行數字。
該網路包括不止一個問題。每個問題需要一列,因此我們有三列,分別表示對前三個調查問題的回答(第四個問題將出現在後面的另一個矩陣中)。
因此我們的矩陣很小,4 行 X 3 列(4 by 3)。不過真實神經網路的矩陣可能擁有數百萬位顧客和數百個調查問題。做影像識別的神經網路可能具備數十億個「顧客」行和特徵列。
總結一下,我們需要矩陣使所有資料清晰展現,方便我們在其上進行計算,這樣矩陣就把資料組織成美觀、整潔的行與列。
下面我們來看程式碼,程式碼選取自 Andrew Trask 的部落格,不過註釋是我寫的。
程式碼
#This is the "3 Layer Network" near the bottom of:
#http://iamtrask.github.io/2015/07/12/basic-python-network/
#First, housekeeping: import numpy, a powerful library of math tools.
5 import numpy as np
#1 Sigmoid Function: changes numbers to probabilities and computes confidence to use in gradient descent
8 def nonlin(x,deriv=False):
9 if(deriv==True):
10 return x*(1-x)
11
12 return 1/(1+np.exp(-x))
#2 The X Matrix: This is the responses to our survey from 4 of our customers,
#in language the computer understands. Row 1 is the first customer's set of
#Yes/No answers to the first 3 of our survey questions:
#"1" means Yes to, "Have cat who poops?" The "0" means No to "Drink imported beer?"
#The 1 for "Visited the LitterRip.com website?" means Yes. There are 3 more rows
#(i.e., 3 more customers and their responses) below that.
#Got it? That's 4 customers and their Yes/No responses
#to the first 3 questions (the 4th question is used in the next step below).
#These are the set of inputs that we will use to train our network.
23 X = np.array([[1,0,1],
24 [0,1,1],
25 [0,0,1],
26 [1,1,1]])
#3The y Vector: Our testing set of 4 target values. These are our 4 customers' Yes/No answers
#to question four of the survey, "Actually purchased Litter Rip?" When our neural network
#outputs a prediction, we test it against their answer to this question 4, which
#is what really happened. When our network's
#predictions compare well with these 4 target values, that means the network is
#accurate and ready to take on our second dataset, i.e., predicting whether our
#hot prospects from the (hot) veterinarian will buy Litter Rip!
34 y = np.array([[1],
35 [1],
36 [0],
37 [0]])
#4 SEED: This is housekeeping. One has to seed the random numbers we will generate
#in the synapses during the training process, to make debugging easier.
40 np.random.seed(1)
#5 SYNAPSES: aka "Weights." These 2 matrices are the "brain" which predicts, learns
#from trial-and-error, then improves in the next iteration. If you remember the
#diagram of the curvy red bowl above, syn0 and syn1 are the
#X and Y axes on the white grid under the red bowl, so each time we tweak these
#values, we march the grid coordinates of Point A (think, "moving the yellow arrow")
#towards the red bowl's bottom, where error is near zero.
47 syn0 = 2*np.random.random((3,4)) - 1 # Synapse 0 has 12 weights, and connects l0 to l1.
48 syn1 = 2*np.random.random((4,1)) - 1 # Synapse 1 has 4 weights, and connects l1 to l2.
#6 FOR LOOP: this iterator takes our network through 60,000 predictions,
#tests, and improvements.
52 for j in range(60000):
#7 FEED FORWARD: Think of l0, l1 and l2 as 3 matrix layers of "neurons"
#that combine with the "synapses" matrices in #5 to predict, compare and improve.
# l0, or X, is the 3 features/questions of our survey, recorded for 4 customers.
57 l0=X
58 l1=nonlin(np.dot(l0,syn0))
59 l2=nonlin(np.dot(l1,syn1))
#8 The TARGET values against which we test our prediction, l2, to see how much
#we missed it by. y is a 4x1 vector containing our 4 customer responses to question
#four, "Did you buy Litter Rip?" When we subtract the l2 vector (our first 4 predictions)
#from y (the Actual Truth about who bought), we get l2_error, which is how much
#our predictions missed the target by, on this particular iteration.
66 l2_error = y - l2
#9 PRINT ERROR--a parlor trick: in 60,000 iterations, j divided by 10,000 leaves
#a remainder of 0 only 6 times. We're going to check our data every 10,000 iterations
#to see if the l2_error (the yellow arrow of height under the white ball, Point A)
#is reducing, and whether we're missing our target y by less with each prediction.
72 if (j% 10000)==0:
73 print("Avg l2_error after 10,000 more iterations: "+str(np.mean(np.abs(l2_error))))
#10 This is the beginning of back propagation. All following steps share the goal of
# adjusting the weights in syn0 and syn1 to improve our prediction. To make our
# adjustments as efficient as possible, we want to address the biggest errors in our weights.
# To do this, we first calculate confidence levels of each l2 prediction by
# taking the slope of each l2 guess, and then multiplying it by the l2_error.
# In other words, we compute l2_delta by multiplying each error by the slope
# of the sigmoid at that value. Why? Well, the values of l2_error that correspond
# to high-confidence predictions (i.e., close to 0 or 1) should be multiplied by a
# small number (which represents low slope and high confidence) so they change little.
# This ensures that the network prioritizes changing our worst predictions first,
# (i.e., low-confidence predictions close to 0.5, therefore having steep slope).
88 l2_delta = l2_error*nonlin(l2,deriv=True)
#11 BACK PROPAGATION, continued: In Step 7, we fed forward our input, l0, through
#l1 into l2, our prediction. Now we work backwards to find what errors l1 had when
#we fed through it. l1_error is the difference between the most recent computed l1
#and the ideal l1 that would provide the ideal l2 we want. To find l1_error, we
#have to multiply l2_delta (i.e., what we want our l2 to be in the next iteration)
#by our last iteration of what we *thought* were the optimal weights (syn1).
# In other words, to update syn0, we need to account for the effects of
# syn1 (at current values) on the network's prediction. We do this by taking the
# product of the newly computed l2_delta and the current values of syn1 to give
# l1_error, which corresponds to the amount our update to syn0 should change l1 next time.
100 l1_error = l2_delta.dot(syn1.T)
#12 Similar to #10 above, we want to tweak this
#middle layer, l1, so it sends a better prediction to l2, so l2 will better
#predict target y. In other words, tweak the weights in order to produce large
#changes in low confidence values and small changes in high confidence values.
#To do this, just like in #10 we multiply l1_error by the slope of the
#sigmoid at the value of l1 to ensure that the network applies larger changes
#to synapse weights that affect low-confidence (e.g., close to 0.5) predictions for l1.
109 l1_delta = l1_error * nonlin(l1,deriv=True)
#13 UPDATE SYNAPSES: aka Gradient Descent. This step is where the synapses, the true
#"brain" of our network, learn from their mistakes, remember, and improve--learning!
# We multiply each delta by their corresponding layers to update each weight in both of our
#synapses so that our next prediction will be even better.
115 syn1 += l1.T.dot(l2_delta)
116 syn0 += l0.T.dot(l1_delta)
#Print results!
119 print("Our y-l2 error value after all 60,000 iterations of training: ")
120 print(l2)
2.1 Sigmoid 函式:行 8-12
sigmoid 函式在網路學習過程中起到非常重要的作用。
「nonlin()」是一種 sigmoid 函式型別,叫做 logistic 函式。logistic 函式在科學、統計學和機率論中非常常見。此處該函式的表達有些複雜,因為這裡它作為兩個函式使用:
第一個函式是將矩陣(此處表示為 x)放入括號內,將每個值轉換為 0 到 1 之間的數字(即統計機率)。轉換過程透過程式碼行 12 實現:return 1/(1+np.exp(-x))。
那麼為什麼需要統計機率呢?神經網路不會預測 0 或 1,它不會直接吼「哇,顧客 1 絕對會買這款貓砂!」,而是預測機率:「顧客 1 有 74% 的可能購買這款貓砂」。
這裡的區別很大,如果你直接預測 0 和 1,那麼網路就沒有改進空間了。要麼對要麼錯。但是使用機率的話就有改進空間。你可以調整系統,每一次使機率增加或減少幾個點,從而提升網路的準確率。這是一個受控的增量過程,而不是盲猜。
將數字轉換成 0 到 1 之間的數字這一過程非常重要,它帶給我們四大優勢。下文將詳細討論這些優勢,現在我們先來看 sigmoid 函式如何將其括號內每個矩陣的每個數字轉換成 0 到 1 之間的數字,並使其落在下圖的 S 曲線上:
圖源:https://iamtrask.github.io/2015/07/12/basic-python-network/
sigmoid 的第一個函式將矩陣中的每個值轉換為統計機率,而它的第二個函式如程式碼行 9 和 10 所示:
' if(deriv==True):
return x*(1-x)'
該函式將給定矩陣中的每個值轉換成 sigmoid 函式 S 曲線上特定點處的坡度。該坡度值也叫做置信度(confidence measure)。也就是說,該數值回答了這個問題:我們對該數值能夠準確預測結果的自信程度如何?你可能會想:這又能怎麼樣呢?我們的目標是神經網路可靠地做出準這是我們的標籤確的預測。而實現這一目標的最快方式就是修復置信度低、準確率低的預測,不改變置信度高、準確率高的預測。「置信度」這一概念非常重要,下文我們將深入解讀它。
接下來我們看第 2 步:
2.2 建立輸入 X:行 23-26
程式碼行 23-26 建立了一個輸入值的 4x3 矩陣,可用於訓練網路。X 將成為網路的 layer 0(或 l0),現在開始建立神經網路吧!
以下是我們從顧客調查中獲得的特徵集,只不過用計算機能理解的語言表達出來:
Line 23 creates the X input (which becomes l0, layer 0, in line 57)
X:
[1,0,1],
[0,1,1],
[0,0,1],
[1,1,1]
我們有四個顧客,他們分別回答了三個問題。前面我們討論過第一行 1,0,1 是顧客 1 的答案。將這個矩陣的每一行看作即將輸入網路的訓練樣本,每一列就是輸入的一個特徵。因此矩陣 X 可被描述為下圖,該矩陣即為圖中的 l0:
你可能會想:矩陣 X 是怎麼變成圖中的 layer 0 的?稍後我會解釋這一點。
接下來,我們將建立正確答案列表。
2.3 建立輸出 y:行 34-37
這是我們的標籤,即第四個調查問題「你是否購買過新款貓砂?」的答案。看下面這列數字,顧客 1 回答的是「Yes」,顧客 2 回答的是「Yes」,顧客 3 和 4 回答的是「No」。
如果把 y 看作「目標」值的話,我將畫一個箭靶。隨著網路的改進,它射出的箭離靶心越來越近。一旦網路可以根據矩陣 X 提供的輸入準確預測 4 個目標值,則網路準備就緒,可以在其他資料集上執行預測了。
2.4 生成隨機數:行 40
這一步是「做家務」。我們必須生成隨機數(隨機數將在訓練過程的下一步生成突觸/權重),以使 debug 過程更加簡單。你不必理解這行程式碼的工作原理,只需要使用它就好了。
生成隨機數的原因是,你必須從某個地方開始。因此我們從一組捏造數字開始,然後在 6 萬次迭代中慢慢改變每一個數字,直到它們輸出具備最小誤差值的預測。這一步使得測試可重複(即使用同樣的輸入測試多次,結果仍然相同)。
2.5 建立突觸(即權重):行 47-48
第一次看到下圖時,你可能會認為神經網路的「大腦」是那些圓圈(神經元)。而事實上,神經大腦的真正核心是那些能夠學習和改進的部分——突觸,也就是圖中那些連線圓圈的線。這兩個矩陣——syn0 和 syn1 才是網路的真正大腦。它們是網路用來試錯學習、執行預測、對比目標值和預測,進而改善下一次預測結果的部分。
注意程式碼 syn0 = 2np.random.random((3,4)) - 1 建立了 3x4 矩陣,併為它生成隨機數。這將是突觸(或權重)的第一層 Synapse 0,它連線 l0 和 l1。該矩陣如下所示:
我曾經在這裡犯了一個錯:我無法理解為什麼 syn0 應該是 3x4 矩陣。我認為它應該是 4x3 矩陣,因為 syn0 必須與 l0 相乘,而後者是 4x3 矩陣,我們為什麼不讓兩個矩陣的數字按行與列排列整齊呢?
而這就是我的錯誤:4x3 乘 4x3 能夠使數字排列整齊?這是錯的。事實上,如果我們想要數字排列整齊,我們就應該讓 4x3 乘 3x4。這是矩陣乘法裡的一項基礎且重要的規則。仔細檢視下圖中的第一個神經元,該神經元內是每位顧客對「你有貓嗎?」這個問題的回覆。以下是 4x3 layer0 矩陣的第一列:
[1]
[0]
[0]
[1]
現在注意,有四條線(突觸)將 l0 中「你有貓嗎?」這個神經元與 l1 的四個神經元連線起來。這意味著上述列 1,0,0,1 中的每個數字都要與四個不同權重相乘,因此得到 16 個值。l1 確實是一個 4x4 矩陣。
注意,現在我們要對第二個神經元內的四個數字執行同樣的操作,也得到 16 個值。我們將這 16 個值中的每個值與剛才建立的值中的對應值相加。
重複這個步驟,直到將第三個神經元中的四個數字也處理完畢。這樣 4x4 l1 矩陣就有 16 個值,每個值是三次乘法所得值的對應相加結果。
也就是說,3 個調查問題 x 4 位顧客 = 3 個神經元 x 4 個突觸 = 3 個特徵 x 4 個權重 = 3x4 矩陣。
看起來很複雜?習慣就好了。而且計算機會替你執行乘法操作。我只是想幫助大家理解其下的底層操作。當你看到下圖時,這些線不會說謊。
也許你會疑惑,程式碼行 47 中的「2」和「-1」是哪兒來的。np.random.random 函式生成均勻分佈於 0 到 1 之間的隨機數(對應的平均值為 0.5),而我們想讓隨機數初始值的平均值為 0。這樣該矩陣中的初始權重不會存在偏向於 1 或 0 的先驗偏置(在最開始的時候,網路不知道接下來會發生什麼,因此它對其預測結果是沒有信心的,直到我們在每個迭代後更新它)。
那麼,我們如何將一組平均值為 0.5 的數字轉變為平均值為 0 的數字呢?首先,將所有隨機數乘 2(這樣所有數字分佈在 0 到 2 之間,平均值為 1),然後減去 1(這樣所有數字分佈在-1 到 1 之間,平均值為 0)。這就是「2」和「-1」出現的原因。
接下來看下一行程式碼:syn1 = 2np.random.random((4,1)) - 1 建立了一個 4x1 向量,併為它生成隨機數。這就是網路第二層的權重 Synapse 1,它連線 l1 和 l2。
for loop 使網路執行 6 萬次迭代。每次迭代中,網路使用顧客調查資料 X 作為輸入,基於該資料得出對顧客購買新款貓砂機率的最佳預測。然後將預測與真值進行對比,再從錯誤中學習,在下次迭代中做出更好的預測。該過程持續 6 萬次,直到網路透過試錯學會如何準確預測。然後這個網路可以使用任意輸入資料,並準確預測可能購買新款貓砂的顧客。
2.6 For Loop:行 52
for loop 使網路執行 6 萬次迭代。每次迭代中,網路使用顧客調查資料 X 作為輸入,基於該資料得出對顧客購買新款貓砂機率的最佳預測。然後將預測與真值進行對比,再從錯誤中學習,在下次迭代中做出更好的預測。該過程持續 6 萬次,直到網路透過試錯學會如何準確預測。然後這個網路可以使用任意輸入資料,並準確預測可能購買新款貓砂的顧客。
3. 前饋:做出有根據的猜測,60000 次迭代
網路在這一步驟開始執行預測。這是深度學習過程中最令人興奮的部分,所以我打算從三個不同角度介紹這個概念:
關於前饋的迷人童話
關於前饋的美麗畫作
為什麼矩陣乘法是前饋的引擎
3.1 城堡和生命的意義:前饋網路
想象你自己是一個神經網路。恰好你是一個有駕照的神經網路,而且喜歡開快車和神秘的心靈之旅。你迫切想找到生命的意義。奇妙的是,你剛好發現只要驅車前往某個城堡,神秘先知將告訴你生命的意義。天啊!
不用說,你肯定特別想找到先知的城堡。先知就代表真值,也就是神秘問題「你買過新款貓砂嗎?」的答案。也就是說,如果你的預測與真值匹配,那麼你就到達先知城堡,即 l2 誤差為 0、黃色箭頭的長度為 0、粉色乒乓球到達碗底。
不過,找到先知城堡需要一些耐心和堅持,你需要嘗試數千次,迷路數千次。(小提示:數千次旅程 = 迭代,不斷迷路 = l2 預測存在誤差,誤差使得你離先知城堡 y 還有距離。)
但是也有好訊息:你知道隨著時間的流逝,每一次旅程並不是沒有意義,你會越來越靠近先知(先知城堡 y 正在閃耀……)。而壞訊息是每一次當你無法到達城堡時,第二天醒來你又回到了原點(即 Layer 0,輸入特徵,3 個調查問題),必須從那裡重新出發(新的迭代)。這個過程有點像下圖:
幸運的是,這個故事有一個完美的結局,你不停嘗試、不斷修改路徑,在進行了 58000 次試驗後終於到達了目標。這很值得。
我們來看一下從你的房子 X 到先知城堡 y 的其中一次旅程(迭代):每一次旅程都是程式碼行 57-59 所執行的前饋,每天你都到達一個新地方,但是它們都不是你的目的地。你當然想知道如何在下一次旅程中更接近城堡,我會在這個故事的後續篇中解釋。
接下來,我們來看一個更簡單的示例。
3.2 關於前饋的美麗畫作
接下來我們來看 16 個權重中的其中一個。這個權重是 syn0,12 條線中最上方那條,連線 l0 和 l1 最上方的神經元。出於簡潔性考慮,我們稱其為 syn0,1(syn0(1,1) 的簡化,表示矩陣 syn0 的行 1 列 1)。如下圖所示:
為什麼表示 l2 和 l1 神經元的圓圈被從中間分割開了?圓圈的左半邊(帶有 LH 字樣)是 sigmoid 函式的輸入值,右半邊是 sigmoid 函式的輸出:l1 或 l2。在這個語境下,sigmoid 函式只是將前一層與前一個突觸相乘,並轉換為 0 到 1 之間的值。程式碼如下:
return 1/(1+np.exp(-x))
這裡的前饋使用了我們的一個訓練樣本,l0 的第 1 行,即「顧客 1 對 3 個調查問題的回覆」:[1,0,1]。因此,我們首先將 l0 的第一個值與 syn0 的第一個值相乘。假設 syn0 矩陣已經過多次訓練迭代(我們在 2.4 節已經進行過初始化了),現在該矩陣如下所示:
syn0:
[ 3.66 -2.88 3.26 -1.53]
[-4.84 3.54 2.52 -2.55]
[ 0.16 -0.66 -2.82 1.87]
到這裡,或許你會疑惑:為什麼 syn0 的值與 2.4 節建立的 syn0 如此不同?好問題。前面介紹的很多矩陣有逼真的初始值,就好像它們剛被計算機透過隨機種子建立出來一樣。而這裡和下面的矩陣並非初始值。它們是經過多次訓練迭代的,因此它們的值已經在學習過程中經過了更新和改變。
現在,我們用 l0 的第一個值 1 與 syn0 的第一個值 3.66 相乘,看看會得到什麼:
下面是前饋的虛擬碼,你可以藉助上圖(從左至右的順序)幫助理解。
下面,我們再從另一個角度看前饋過程的底層數學原理。
3.3 前饋的數學原理
l0 x syn0 = l1LH,在這個示例中即 1 x 3.66 = 3.66,不要忘記還要加上另外兩個 l0 值和 syn0 對應權重的乘積。在該示例中,l0,2 x syn0,2= 0 x something = 0,l0,3 x syn0,3 中 l0,3=1,從上一節中我們知道 syn0,3 = 0.16,因此 l0,3 x syn0,3 = 1 x 0.16 = 0.16。因此 l0,1 x syn0,1 + l0,3 x syn0,3 = 3.66 + 0.16 = 3.82,即 l1_LH = 3.82。
接下來,我們將 l1_LH 輸入 nonlin() 函式,將該數字轉換為 0 到 1 之間的機率。Nonlin(l1_LH) 使用程式碼 return 1/(1+np.exp(-x)),因此該示例中,1/(1+(2.718^-3.82))=0.98,l1 = 0.98。
那麼,在公式 1/(1+np.exp(-x)) = [1/(1+2.718^-3.82))] = 0.98 中發生了什麼呢?計算機使用程式碼 return 1/(1+np.exp(-x)) 代替了我們的人工計算,我們可以透過下圖看到在 sigmoid 曲線上 x = 3.82 所對應的 y 值:
圖源:https://iamtrask.github.io/2015/07/12/basic-python-network/
注意,X 軸上 3.82 在藍色曲線上對應點的對應 y 值是 0.98,而程式碼將 3.82 轉換為 0 到 1 之間的機率。上圖有助於大家瞭解該計算並不神秘,也不抽象,計算機只不過做了和我們一樣的事:它使用數學檢視上圖中 X 軸上 3.82 對應的 y 值,僅此而已。
同理,我們重複以上步驟,計算出 l2_LH 和 l2 的值。至此,我們就完成了第一次前饋。
現在,為了簡潔起見,我們把所有變數放在一起,如下所示:
l0=1
syn0,1=3.66
l1_LH=3.82
l1=0.98
syn1,1=12.21
l2_LH=0
l2=~0.5
y=1 (this is a "Yes" answer to survey Question 4, "Actually bought Litter Rip?")
l2_error = y-l2 = 1-0.5 = 0.5
現在,我們來看讓這一切實現的矩陣乘法(對矩陣乘法和線性代數陌生的朋友,可以先學習 Grant Sanderson 的課程:https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab)。
首先,在程式碼行 58,我們將 4x3 l0 和 3x4 Syn0 相乘,建立(隱藏層)l1,它是一個 4x4 矩陣:
在第 58 行程式碼中,我們將 l1 輸入 nonlin() 函式,得到一個 0 到 1 之間的機率值:
1/(1 + 2.781281^-x)
This creates layer 1, the hidden layer of our neural network:
l1:
[0.98 0.03 0.61 0.58]
[0.01 0.95 0.43 0.34]
[0.54 0.34 0.06 0.87]
[0.27 0.50 0.95 0.10]
看不懂也沒關係,我們來看一個簡單的訓練示例。第一行(顧客 1 的調查資料)[1,0,1] 是一個 1x3 矩陣。我們將其乘以 syn0(3x4 矩陣),得到的 l1 是 1x4 矩陣。過程如下所示:
注意,在程式碼第 58 行 l1=nonlin(np.dot(l0,syn0)),我們將 l1 輸入 sigmoid 函式,因為我們需要一個 0 到 1 的數值。
從這行程式碼中,我們可以看到 sigmoid 函式的第一大優勢。當我們將 l0 和 syn0 的點乘矩陣輸入到 nonlin() 函式時,sigmoid 函式將矩陣中的每個值轉換成 0 到 1 之間的機率。
3.4 核心重點:隱藏層中的推斷問題
我們為什麼要關心統計機率呢?我認為原因在於,統計機率是讓一堆沉默矩陣煥發生機並像小孩一樣學習的主要因素!
在第一層中,當我們將 l0 乘以 syn0 時,為什麼要嘗試給 syn0 的權重賦不同的值呢?因為我們想嘗試不同的特徵問題組合,發現對預測結果幫助最大的問題組合。上文舉過一些拙劣的例子,推斷出有貓且喝進口啤酒的人更有可能購買新款貓砂,從而利用權重強化這兩個特徵組合。
另一個例子:如果一些顧客沒有養貓,但他們喝進口啤酒且訪問過 Litter Rip.com (http://rip.com/) 網站,我們可以推斷出這些顧客熱衷技術:他們喝進口啤酒因為他們欣賞其供應鏈物流,他們訪問網站,各種各樣的網站,說明他們很顯然熱衷技術。因此我們可以推斷出,這些顧客可能會出於對這款貓砂先進技術的欣賞而購買它,儘管他們可能實際上並沒有養貓。所以我們或許需要調整 syn0 的權重,強化這些特徵之間的連線。
現在你明白了嗎?當我們將調查問題的回覆 l0 與 syn0 中的權重(每個權重表示我們對一個推斷問題對預測結果的重要程度的最佳猜測)相乘時,我們是在嘗試不同的答案組合,以檢視哪些組合對預測結果最有幫助。很明顯,在 6 萬次迭代中,訪問過貓砂網站的貓主人更有可能購買這款貓砂,因此在迭代的過程中它們對應的權重會增加,即其統計機率更接近 1 而不是 0。而喝進口啤酒卻沒有貓的人購買這款貓砂的可能性較低,因此對應的權重較小,即其統計機率更接近 0 而不是 1。是不是很奇妙?這就像數字譜寫的詩歌,這些是會推理和思考的矩陣!
這也是我為什麼大費周章介紹這個的原因。你是否經歷過,一些傲慢的軟體工程師或製造恐慌的記者告訴你,神經網路隱藏層是潘多拉的魔盒?而事實上它的底層並沒有什麼「神奇魔法」。這些數學知識清晰、優雅而美麗。你可以掌握它,只要有耐心肯堅持。
接下來我們繼續看矩陣乘法。
3.5 用神經元和突觸的形式視覺化矩陣乘法
我們用神經元和突觸的形式視覺化 l0 和 syn0,如下圖所示:
上圖展示了輸入 l0 的行 1 如何完成在網路中的第一步。第一行代表顧客 1 對三個調查問題的答案。這三個數字需要乘以 syn0 的 12 個值,然後網路還要對其他三位顧客的答案進行同樣的處理,那麼如何清晰地展現這些數字並進行計算呢?
這裡的關鍵在於,將四位顧客看作一個「batch」(批次),他們在其中堆疊在一起,即一共 4 個 stack。那麼,最上面的那個 stack 就是第一行(顧客 1),依此類推。如上圖所示,你將第一行的三個數字和 syn0 的 12 個值相乘,再相加,最後得到 l1 最上面 stack 的四個值。
接著是 batch 中的第二個 stack——顧客 2 的回答是 0,1,1。將這三個數字和 syn0 的 12 個值相乘,再相加,最後得到 l1 第二個 stack 的四個值。
依此類推。其核心在於一次計算一個 stack,這樣不管 batch 中有多少個 stack,四個還是四百萬個,你都可以很好地處理。你可能會說每個特徵都有一個 batch,在貓砂這個示例中,每個調查問題(特徵)的 batch 為 4,因為只有四位顧客的答案。但它也可能是四百萬。「full batch configuration」的概念是非常常見的模型,接下來我將解釋這一點。
其實認為給定特徵具備一個 batch 的值是最容易理解的。當你看到一個特徵時,你知道它底下有一個 batch 的值。
就像程式碼行 59 所示,我們把 4x4 l1 和 4x1 syn1 相乘,然後輸入 sigmoid 函式得出 4x1 l2,其每個值都是 0 到 1 的統計機率。
l1 (4x4):
[0.98 0.03 0.61 0.58] [ 12.21]
[0.01 0.95 0.43 0.34] X [ 10.24] =
[0.54 0.34 0.06 0.87] [ -6.31]
[0.27 0.50 0.95 0.10] [-14.52]
Then pass the above 4x1 product through "nonlin()" and you get l2, our prediction:
l2:
[ 0.50]
[ 0.90]
[ 0.05]
[ 0.70]
那麼這四個預測結果告訴我們什麼呢?預測值距離 1 越近,則該顧客購買這款貓砂的可能性越高;預測值距離 0 越近,則該顧客購買這款貓砂的可能性越低。
現在我們完成了前饋部分。接下來我們將看 6 萬次迭代過程中如何調整網路權重,使預測結果越來越好。
4. 從試錯中學習:梯度下降
4.1 梯度下降概覽
梯度下降的目的是什麼?是為了更好地調整網路權重,從而在下次迭代中獲得更好的預測結果。也就是說,網路的突觸矩陣中的某些值要被增減。為了調整這些值,我們必須回答以下兩個重要問題:
我應該按什麼方向調整數字?應該增加還是減少數值?正方向還是負方向?……
數值應該增減多少?
下面我們將詳細解釋這兩個基礎問題。還記得上文的紅色碗嗎?「梯度」就是「坡度」,「梯度下降」即計算出使小球從碗表面上的某個點儘快下降到碗底的最優坡度。
梯度下降的第一步即,計算當前的預測結果與真值 y(1/yes 或 0/no)之間的差距。
4.2 預測結果與調查問題 4 的答案相比有多大差距?
程式碼行 66:
l2_error = y - l2
第一次預測結果距離目標值「Yes/1」(顧客 1 對第四個調查問題的回答真值是「購買過」)有多遠距離呢?我們需要將 l2 預測值與 y 值(1)進行對比,即 y 值減去 l2 得到的就是 l2_error——「預測值距離目標值 y 的距離」。
因此,我們可以想象這幅圖景:網路使用每位顧客的回覆作為輸入,並操作這些資料,以得到顧客是否購買貓砂的預測結果。
我們有四位顧客,網路就做了四次預測。因此 l2_error 是四次誤差的向量(每個誤差針對一次預測)。接下來我們將列印出該誤差:
列印誤差:行 72-73
72 if (j% 10000)==0:
73 print("Avg l2_error after 10,000 more iterations: "+str(np.mean(np.abs(l2_error))))
行 72 使計算機每隔一萬次迭代列印一次 l2_error。這有助於我們每隔一萬次檢視網路的學習效果和進展。if (j% 10000)==0: 表示「如果你的迭代器所處的迭代次數除以 10000 後沒有餘數,則……」。j%10000 共有六次沒有餘數的情況:迭代數為 10000、20000、30000、40000、50000、60000 時。該列印輸出會幫助我們很好地瞭解網路的學習進展。
程式碼 + str(np.mean(np.abs(l2_error)))) 取誤差的絕對值,然後求平均數並列印出來,從而簡化了列印過程。
現在我們已經知道預測結果(l2)距離真值(y)的距離,並列印了出來。但是,我們和城堡的距離實在太遠,我們要如何降低目前令人失望的預測誤差 0.5,最終到達目的地呢?
一步步來。接下來,我們將瞭解調整網路的哪一部分才能改進下一次預測的結果,之後會討論如何調整網路。
4.3 我們需要調整網路的哪一部分?
先看下圖,神經網路真正用來學習和記憶的核心部分是突觸,而不是神經元。我們的網路中有 16 個變數:3x4 矩陣 syn0 中的 12 個變數和 4x1 向量 syn1 中的 4 個變數。檢視下圖,你會發現每一條線(「邊」或「突觸」)表示一個變數,它包含一個數值,這就是權重。
我們可以控制這 16 個權重。
輸入 l0 是固定的,不能改變。l1 由 syn0 中的權重決定(l1 = syn0 x l0),l2 由 syn1 中的權重決定(l2 = syn1 x l1)。上圖中的 16 條線(突觸、權重)是神經網路達到目標過程中唯一能夠調整的數字。
現在我們知道了預測值與真值的距離,知道了 l2_error。那麼我們如何利用這些數值調整兩個矩陣中的 16 個權重呢?這個答案就是 AI 最神奇的屬性之一:這是一個統計學和機率概念,叫做置信度。
4.4 置信度:使冷冰冰的數字像人類一樣思考
程式碼行 88:
l2_delta = l2_error*nonlin(l2,deriv=True)
「置信度」這個術語可能比較抽象,但是它實際上就是我們每天都在用的東西。我用一個有趣的故事來提醒你:
先知城堡 2:你必須明白你其實一直在使用置信度!
在先知城堡的故事中,你執行前饋,驅使 l2 向最佳預測 y(城堡)前進,但是當你到達 l2 後卻發現你雖然距離城堡更近了,卻仍未到達。第二天早上你發現自己在家中醒來(回到 l0),然後開始再一次的旅程(新的迭代)。
你該如何改進駕駛方向,才能做有用功呢?
首先,在你今天的旅程結束時,你急切地詢問當地的騎士這個地方距離城堡還有多遠,騎士告訴你答案(即 l2_error)。每天的旅程結束時,你都要計算如何改變權重才能使明天的 l2 預測比今天更好,並最終使你抵達城堡。這就是 l2_delta(參見下圖),即要想使明天的 l2 抵達城堡,今天你需要對權重做出的改變。
重點來了:
注意 l2_delta 與 l2_error 不同,l2_error 僅告訴你與城堡的距離,而 l2_delta 則影響你對方向的信心。你在衡量信心。這些置信度數字就是導數(不過,這裡暫且忘記微積分吧,我們暫時使用這個詞「坡度」),即 l2 每個值的坡度。這些坡度就是你對今天旅程中每一次轉向的自信程度。有些方向你非常確定且自信,而有些則不然。
使用置信度這一概念來計算明天能到哪裡似乎有點抽象?實際上,你每時每刻都在使用置信度導航,只不過你沒意識到而已。接下來我們就讓你「恢復」這一意識。
回憶一下你的迷路經歷。一開始,你按照自己以為正確的道路前進,你非常自信。但是慢慢地你發現旅途似乎比預期更加漫長,你開始懷疑是否忘記拐彎。你沒最初那麼自信了。隨著時間的流逝,你原本應該已經到達目的地了,然而你還在路上,此時你更加確定錯過了一次拐彎。這時候你的自信度很低。你知道自己沒有到達目的地,但你不確定如何從現在的位置到達目的地。於是你決定停下來問路,但是被問路的女士告訴你的拐彎和路標太多,你沒記全,你沿著她指的方向走到一半又不知道怎麼走了。這時你會再次問路,不過這一次你離目的地更近,前進的方向也更加簡單,你沿著方向前進最終抵達目的地。
在這段講述中,你需要注意以下幾件事:
首先,你透過試錯進行學習,你的信心在不停變化。稍後我會解釋為什麼置信度使得網路試錯學習,以及 sigmoid 函式如何提供重要的置信度。
其次,注意你的旅程分為兩部分:第一部分從你啟程到第一次問路(l1),第二部分是從第一次問路到 l2(你以為已經到達目的地了)。但是後來你意識到這裡並不是終點,不得不詢問離真正的終點還有多遠。
你看到置信度在旅程中的角色了嗎?一開始,你確定自己走在正確的路上,然後你懷疑自己少拐了個彎,之後你確定自己少拐了個彎,決定停下來問路。圖中這兩個分割的部分就像狗腿一樣彎,但隨著每日旅程的改進,狗腿將一點點變直。
這和你前往城堡的過程一樣:
每天,你(3 層神經網路)沿著一組方向(syn0)前往城堡。結束時你發現自己停在了 l1,就詢問接下來的方向(syn1)。這些方向使得你到達當天的終點(預測結果),你以為那裡就是目的地城堡。但事實上你面前並沒有城堡。於是你詢問騎士:「我離城堡還有多遠?」(l2_error 的值是多少?)你是一個天才,於是你將 l2_error 與你對每一次轉向的置信度(l2 的坡度)相乘,得出了明天可以到達的地方(l2_delta)。
注意:這個比喻有一點不恰當,即當你停在 l1 問路時,女士告訴你新的方向。而事實上,你的方向(syn1 值)是你準備更新上一次迭代時就已經選擇好的。因此準確來說,那位女士並沒有和你說話,只是舉起了你昨天在那裡消失之前給她的路牌,因為你知道今天會再次路過她身旁。
在繼續之前,你需要釐清 3 項事實:
你到達的地方,即當前位置(l2);
你距離城堡的距離 (l2_error);
要想提高對明天旅程更接近城堡的信心,你需要做出的方向改變 (l2_delta)。
瞭解這三項事實後,你可以計算前進方向(即突觸權重)需要做的改變。接下來,我們來看如何使用 sigmoid 函式獲得置信度,並利用置信度計算 l2_delta。
4.5 如何利用 Sigmoid 函式的曲線特徵得出置信度
sigmoid 函式的四步魔鬼操作將讓你見識到她的魅力。對我而言,神經網路的學習能力很大程度上基於這四步操作。
2.5 節解釋了 nonlin() 函式可以將 l2_LH 的值轉換成統計機率(l2),這是 sigmoid 函式四大優勢中的第一點。
而 sigmoid 函式的第二部分 nonlin(l2,deriv=True) 可以將 l2 中的 4 個值轉換成置信度。這就是 sigmoid 函式的第二大優勢。如果網路每個預測(l2 中的 4 個值)都具備高準確率、高置信度,則這是一次不錯的預測。我們不會改變帶來如此優秀預測結果的 syn0、syn1 權重,只想改變那些不 work 的權重。接下來,我將介紹 nonlin(l2,deriv=True) 如何告訴我們哪些權重需要格外注意。
置信度幫助我們首先確定哪些權重需要改變。如果 nonlin(l2) 生成的置信度為 0.999,則該網路「很確定該顧客購買了這款貓砂」,而置信度 0.001 則等同於「我很確定該顧客沒有購買這款貓砂」。但是處於中間的數字怎麼辦呢?低置信度數字通常在 0.5 左右。例如,置信度 0.6 意思是「該顧客可能會買這款貓砂,不過我不確定。」置信度 0.5 表示「兩條路都可行,而我站在中間猶豫不決……」
也因此我們需要關注中間的數字:0.5 周圍的數字都不堅決,都缺乏信心。那麼,我們應該如何改變網路才能使 l2 的四個值都具備高準確率和高置信度呢?
關鍵在於權重。上文已經提到,syn0 和 syn1 是神經網路的絕對核心。我們將對 l2_error 的四個值執行數學計算,得到 l2_delta。l2_delta 即「要使網路輸出 (l2) 更接近 y(真值),我們想看到的 l2 改變。」也就是說,l2_delta 即你想在下次迭代中看到的 l2 變化。
這就是 Sigmoid 函式四大優勢的第三點:l2 的四個機率值都是位於 sigmoid 函式 S 曲線圖上的某個點(見下圖)。例如,l2 的第一個值為 0.9,我們在下圖中找 0.9 所對應的 Y 軸位置,會發現它對應下圖中的綠色點:
圖源:https://iamtrask.github.io/2015/07/12/basic-python-network/
除了綠色點,你注意到穿過該點的綠色線嗎?這條線表示該點的坡度(正切值)。計算一個點的坡度不需要你懂微積分,計算機會幫你做這些。但是你需要注意,S 曲線上上限(接近 1)和下限(接近 0)的位置的坡度很淺。sigmoid 函式曲線上的淺坡度正好對應預測結果的高置信度!
你還需要了解 S 曲線上的淺坡度意味著坡度值較小,這是一個好訊息!
因為,當我們更新突觸(權重)時,我們並不想改變能帶來高置信度、高準確率的權重,它們已經足夠好了。所以我們只想對其引入微小的改變。事實上,這些權重的改變(l2_delta)是由原數值乘以特別小的數字(坡度)來進行的,這恰好符合我們的期望。
這就是 Sigmoid 函式四大優勢中的最後一項。
高置信度對應 S 曲線上的淺坡度,淺坡度對應一個很小的數字。從而使得 syn0 和 syn1 的值乘以這些小數字後能夠滿足我們的期望,即突觸中有用的權重值基本上變化不大,這樣就能夠保持 l2 的置信度和準確率。
同理,低準確率的 l2 值(對應 S 曲線的中間點)即是 S 曲線上坡度最大的數字。即上圖 Y 軸 0.5 左右的值對應 S 曲線的中間點,這裡坡度最陡,因而坡度值也最大。這些大數值意味著當我們將它們與 l2 中的低準確率值相乘時,這些值會發生很大改變。
具體來講,我們應該如何計算 l2_delta 呢?
我們已經找到了 l2_error,即第一次預測 l2 與目標值 y 的距離。而我們尤其關心「大誤差」(Big Miss)。
在程式碼行 88 中,我們要做的第一件事就是使用 sigmoid 函式的第二部分 nonlin(l2,deriv=True) 找出 l2 預測中 4 個值各自的坡度。坡度將告訴我們哪些預測可信度高,哪些比較牽強。這就是我們找出並修復神經網路中最弱環節(低置信度預測)的方式。接下來,將這 4 個坡度(置信度)與 l2_error 中的四個誤差值相乘,得到的就是 l2_delta。
這一步非常重要。你注意到我們將大誤差與低準確率、高坡度值的 l2 預測相乘嗎?這是最重要的部分,稍後我將給大家解釋。
現在,我們先看這部分的視覺化展示,如下所示:
y: l2: l2_error:
[1] [0.50] [ 0.50]
[1] _ [0.90] = [ 0.10]
[0] [0.05] [-0.05]
[0] [0.70] [-0.70]
下面的公式是理解神經網路學習原理的重點:
l2 slopes after nonlin(): l2_error: l2_delta:
[0.25] Not Confident [ 0.50] Big Miss [ 0.125] Big change
[0.09] Fairly Confident X [ 0.10] Small Miss = [ 0.009] Small-ish Change
[0.05] Confident [-0.05] Tiny miss [-0.003] Tiny change
[0.21] Not Confident [-0.70] Very Big Miss [-0.150] Huge Change
注意,大誤差即 l2_error 中值較大的數字,而低準確率的 l2 值的坡度也最陡,即它們在 nonlin(l2,deriv=True) 中的數字最大。因此,當我們用大誤差乘以低準確率 l2 值時,就是大數字與大數字相乘,因此也將得到 l2_delta 向量中最大的數字。
l2_delta 即「下一次迭代中我們希望看到的 l2 改變」。較大的 l2_delta 值意味著下一次迭代中 l2 預測會有很大的改變,而這正是透過大幅改變對應的 syn1 和 syn0 值來實現的。將這些大數值與 syn1 中已有的值相加,得到的更新權重將在下一次迭代中帶來更好的 l2 預測結果。
為什麼要使用 l2 的坡度呢?
此舉的目的是為了更快地修復 16 個權重中最不合適的那些。上文我們提到 sigmoid 函式的 S 曲線圖時說過,l2 坡度與置信度相關。也就是說,大坡度值等於低置信度,最小坡度值等於最高置信度。因此,將小數字與 l2_error 中的對應值相乘不會給 l2_delta 帶來大的變化,而我們也恰好不想改變那些權重,它們已經做得很好了。
但是置信度最低的 l2 預測具備最陡的坡度,坡度值也最大。當我們將這個大數字乘以 l2_error 時,l2_delta 的數字也會很大。之後當我們更新 syn1 時,大的乘數意味著大的積、大的改變或調整。正該如此,因為我們想最大程度地改變置信度最低的權重,這也是調整權重大小時使我們獲得最大收益的地方。總之,l2 坡度指示了每個 l2 預測值的置信度,從而允許我們決定哪些數字最需要調整,以及如何最快地調整它們。
但是精彩的部分還未結束,回到關於推斷問題這一重點上。
在 3.4 節我們討論了,改變 syn0 的權重類似於改變對預測最有用的推斷問題。例如,訪問過貓砂網站的養貓人更有可能購買這款貓砂,而喝進口啤酒但不養貓的人購買貓砂的機率相對較低。這就是 l2_delta 的用武之地。l2_delta 增加最有用推斷問題的重要性(權重),降低不那麼有用推斷問題的重要性。接下來,我們將瞭解如何確定隱藏層 1 中每個推斷問題的效用。
不過,首先我們先確定自己是否完全瞭解 l2 置信度如何修改 syn1 中的權重。sigmoid 函式隨機選取矩陣中的任一數字,
將它轉換為統計機率;
將機率轉換為置信度;
對突觸進行或大或小的調整;
該調整(幾乎)總是朝著提升置信度和準確率的方向,從而降低 l2_error。
sigmoid 函式是一個奇蹟,它讓矩陣中的數字可以根據置信度進行學習,數學多麼神秘!
接下來,我們來看置信度如何告訴我們要對 syn0 做怎樣的改變才能實現更加準確的層 1,然後實現更加準確的層 2。
4.6 將「大誤差/小誤差」置信度應用於層 1
程式碼行 100:
l1_error = = l2_delta.dot(syn1.T)
城堡的故事非常有用:在那幅圖中,我們可以看到 l2_error 代表 l2 預測與城堡的距離,也可以瞭解到 l2_delta 是目前的預測與下一次迭代中預測結果之間的距離。剛剛介紹的「學習大誤差/小誤差」的方法可以幫助我們理解,如何調整當前的 syn1(見程式碼行 115)。
現在你已經準備好用來更新 syn1 的 l2_delta 了,那麼為什麼不也更新一下 syn0 呢?
怎麼做?回到 4.2 節,我們找到了 l2_error,知道 l2,也知道我們要追尋的「完美 l2」——y。但是,在 l1_error 這裡,情況變了,我們不知道「完美 l1」是什麼。
如何找到「完美 l1」呢?讓我們再次回到推斷問題。
回到 3.4 和 4.5 節,關於推斷問題(特徵)的概念,以及特徵問題組合。當我們改變 syn1 中的權重時,我們真正在做的其實是用我們想賦予該問題或問題組合的重要性進行試驗。對於 syn0 也是一樣,我們想找出能夠幫助確定 l1_error 的數學方法,因為 l1_error 將告訴我們哪些問題的優先順序被錯誤分配了或者哪些問題被錯誤地組合在了一起。當我們接著計算 l1_delta 時,l1_delta 將告訴我們下一次迭代中更好的問題優先順序和問題組合。
例如,syn0 最初的權重提示推斷問題「這個人富有嗎?」是重要的問題。然而,隨著訓練的開展,網路意識到貓砂並非奢侈品,是否富有並不重要,因此網路決定調整 syn0 的值,以降低推斷問題「這個人富有嗎」的重要性。如何降低呢?將 syn0 中該問題的對應值乘以較大的 l1_delta 值,不過該值前面帶有一個負號。然後網路將更新新的推斷問題「這個人對貓屎過敏嗎?」。更新方法是將該問題對應的 syn0 值乘以較大的 l1_delta 值(正值)。
這兩個推斷問題的優先順序不斷變化。但是另一個推斷問題「這個人知道如何網路購物嗎」一直都很重要,因為目標受眾是喜歡線上購物的人。因此網路將該問題對應的 syn0 值乘以較小的 l1_delta 值。這樣,其優先順序就會保持不變。
另一個謎題是你需要多少推斷問題?在這個網路中,我們一次只能容納四個推斷問題。這是由 syn0 的大小決定的。如果你沒有足夠的推斷問題,那網路可能就無法做出準確的預測。而問題太多則會浪費過多精力。
現在,我們知道改變 syn0 的值會改變某個推斷問題對於 l1 的貢獻,進而影響 l1 對 l2 準確預測的貢獻程度。syn1 使用特徵問題組合,並進一步最佳化從而取得更加準確的 l2。
我在這個階段提出的問題是:為什麼要使用 l2 的坡度?
上文提到,我們使用 l2 坡度來更快地調整權重:l2 坡度告訴我們每個 l2 預測的置信度,從而允許我們決定哪些數字最需要調整,以及如何最快地調整它們。
我們來看數學部分。現在我們知道下一次迭代中 l2 預測要做的改變是 l2_delta,也知道到達目前預測結果的 syn1 值。那麼重要的問題來了:如果我們用 l2_delta 乘以這次迭代的 syn1 值會怎麼樣?
這就像好萊塢編劇一樣:你寫了一個悲傷結局的電影,主角被龍噴出的火焰灼傷然後被吃掉,所以未能到達城堡。導演看了劇本滿屋咆哮:「我要 happy ending,我要主角打敗惡龍,發現生命的意義,然後轉身離開!」
你同意按老闆的意思修改劇本。你知道了要達到的結局,就轉回去尋找哪個動作出了錯。哪個情節使得你沒有成為英雄反而被龍吃掉?
數學就是在做這樣的事情:如果你把 l2_delta(期望的完美結局)乘以 syn1(導致錯誤結局的情節),那麼你將收穫 l1_error。改變造成錯誤結局的情節,下一版劇本將變得更好。
再次提醒:如果你將下一次想去的地方(l2_delta)與旅程第二部分的錯誤方向(現在的 syn1)相乘,則乘積將是「旅程第一部分出錯的地方」,即 l1_error。
知道了 l1_error,你就可以計算在下次迭代中將 l1 改變多少才能得到更好的 l2 預測,而這就是 l1_delta。
程式碼行 109:
l1_delta = l1_error * nonlin(l1,deriv=True)
l1_delta 的計算方法和 l2_delta 一樣,此處不再贅述。
4.7 更新突觸
程式碼行 115-116:
115 syn1 += l1.T.dot(l2_delta)
116 syn0 += l0.T.dot(l1_delta)
程式碼的最後部分是高光時刻:所有的工作完成了,我們恭敬地將 l1_delta 和 l2_delta 搬到神聖的領導者「突觸國王」面前,他才是所有操作的真正核心。
計算 syn0 更新的第一步是 l1_delta 乘輸入 l0,然後將乘積與目前的 syn0 相加。這就使得 syn0 的構成部分發生很大改變,從而對 l1 產生更強影響。也就是說,我們降低了這次迭代中誤認為重要的問題或問題組合的重要性。它們並沒有我們想象的那麼重要,因此用 l1_delta 在正確的方向上修正錯誤,從而使我們在下一次迭代中距離城堡更近。
「試錯學習」,這就是網路所做的事情。以下介紹了網路如何利用試錯來學習:
嘗試:網路執行 6 萬次前饋,即嘗試 6 萬次不同的特徵問題組合,只為輸出最佳的預測結果;
錯誤:網路將目前的預測與真值進行對比,找出誤差;
學習:網路使用置信度(梯度下降)找出下一次迭代需要在正確的方向上做出多少改變,即 delta。然後網路使用新的 delta,並將它乘以舊的 l1 或 l2,然後將乘積加在舊的 syn0 或 syn1 上。你明白為什麼說突觸是神經網路的大腦了嗎?它們在試錯中學習,和人類一樣。
你可能會說:「神經元將一切聚集在一起!」(對準確預測貢獻最多的特徵組合在網路中的最終優先順序最高)。是突觸權重將一切特徵串聯起來,從而做出最後的預測。
4.8 置信度的坡度與紅色碗表面的坡度相同!
回憶一下 1.3 提到的紅色碗,我們在本文中介紹的所有技巧都呈現在這個碗中。
當我們使用舊層和新 delta 的乘積更新突觸時,它推動突觸在正確的方向上前進,去往置信度高、準確率高的預測結果。「在正確的方向上推動」意味著:
l2_delta 的一些值可能是負的。l2_delta 為負值意味著下一個迭代中的 l2 接近 0,這會以多種方式在突觸中呈現。因此找出給予我們「方向感」的坡度非常重要。方向感,或者說坡度,是推動球到達碗底的關鍵。網路的工作是增加權重,使得下一個 l2 被 l2_delta 撼動。
上面這幅簡單的二維圖展示了梯度下降的小球。
程式碼行 88 計算出 l2 每個值的坡度值。在上圖的 3 個凹陷中(或者說「碗」),很明顯真正的全域性最小值是最左側最深的碗。出於簡潔考慮,我們假裝中間的碗是全域性最小值。那麼,向右下的陡峭坡度(即負坡度值,如上圖綠色線所示)意味著小球會在這個方向上滾動很大幅度,導致 syn1 中的對應權重出現較大的負值調整,而該權重將用於在下一次迭代中預測 l2。也就是說,四個 l2 值中有一些會接近 0,則預測結果就是該顧客不購買這款貓砂。
但是,如果左下有一個淺坡度,這意味著預測值已經具備高準確率和置信度,所以坡度值是小的正數,球只會向左稍微挪動一點,syn1 中的對應權重也只會有些微調整,這樣下一次迭代中該值的預測結果不會有太大改動。這奏效的原因是,隨著網路預測的準確率越來越高,小球的來回移動幅度將越來越小,直到它到達全域性最小值——碗底,不需要再移動為止。
現在第四部分「從試錯中學習:梯度下降」即將結束。是時候坦白一個事實了:你實際上已經在做微積分了,只不過我們沒有指明它是微積分!
這個樸素的真相就是:
l2 每個值的置信度 = l2 的坡度 = l2 的導數
但置信度只是冰山一角,接下來我們將介紹可用於更復雜神經網路(具備更多層和特徵)的策略——反向傳播。
5. 反向傳播
5.1 打破反向傳播的迷思
反向傳播是執行梯度下降的核心工具,但它以難學而著稱……不過反向傳播是為了幫助我們,它只有好的意圖。
關於反向傳播有兩個迷思,接下來我們就來一一打破。
迷思 1:反向傳播非常難
錯。反向傳播只是需要耐心和堅持罷了。如果你只讀過一次關於反向傳播的文字就舉手投降,那你就完了!但是如果你把 Grant Sanderson 關於反向傳播的影片 3 慢速看 5 遍,再把關於反向傳播數學知識的影片 4 看 5 遍,你會覺得很順利。(影片地址:https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi)
迷思 2:理解反向傳播,必須會微積分
很多線上文章稱,你需要了解多變數微積分才能理解 AI。這種說法並不對。深度學習大牛 Andrew Trask 表示,如果你上過三個學期的大學微積分課,其中只有一小部分材料對學習反向傳播有用:鏈式法則。然而,即使你學了 3 學期大學微積分課程,反向傳播中的鏈式法則與你在大學課堂上見到的並不一樣。你最好把 Kalid Azad 關於鏈式法則的書讀五遍。
我之前一看到「導數」這個詞就發慌,還沒怎麼著自己就先認輸了。大家不要重蹈我的覆轍。你必須用這樣的內心聲音告誡自己:「我不需要任何背景也能掌握這個概念。」
5.2 反向傳播的定義
5.2.1 反向傳播之於梯度下降,相當於鏈式法則之於反向傳播
第四章介紹的梯度下降過程是理解梯度下降的重要步驟,不過第五章將從不同的角度理解梯度下降,它可應用於大部分神經網路。
反向傳播是理解神經網路的必要環節。其優雅的數學運算允許我們同時調整每一個權重,而這是透過鏈式法則實現的。
什麼是鏈式法則?我先介紹數學定義,再用類比的方式幫助你理解它。
數學定義:鏈式法則的本質是,巢狀函式的導數是構成巢狀函式的每個函式的導數乘積。即,最終導數等於構成函式的導數乘積。
類比:把鏈式法則看作馬戲團的雜耍人。神經網路有 16 個權重,因此我們把這些權重想象為 16 個保齡球瓶,雜耍人需要使它們同時停留在空中。假設這些球瓶大小不一,則雜耍人必須足夠嫻熟才能用合適的力道在恰當的時間點丟擲每個球瓶,同時確保其他 15 個球瓶停留在空中。現在我們假設這位雜耍人很厲害,當一個球瓶在空中突然體積改變時,他能夠立刻調整其他 15 個球瓶,來彌補這一變化。他可以適應球瓶的任意變化並進行調整,使全部 16 個球瓶都停留在空中!
簡直是天才,對吧?鏈式法則也是如此:從一個權重到最終預測有很多路徑,鏈式法則調整這些路徑,使它們全都是相對較直的路。我們示例中的神經網路比較小,事實上鍊式法則同樣適用於包含數百萬甚至數十億權重的大型神經網路。
5.2.2 我們想要解決的問題:如何調整 16 個權重
難點在於:每次你調整 16 個權重中的其中一個時,整個神經網路都會受到影響。如何既考慮到權重調整對預測誤差的影響,還能計算出每個權重的最佳調整值呢?這聽起來很複雜。
事實也的確如此。我們剛剛已經使用 Python 程式碼和置信度進行了必要的計算,但是為了掌握這個要點,我們需要從另一個角度再做一次:鏈式法則。
我們將利用坡度使 16 個球瓶同時待在空中。而找出坡度的秘訣就是導數。
5.2.3 鏈式法則和變化率
這一次,我們使用鏈式法則找出變化率的坡度。
變化率是一個重要概念。3.2 節提到 syn0,1 為 3.66,那麼關鍵問題來了:當我們增加或減小 syn0,1 的值時,這對 l2_error 的增減有多大影響?也就是說,將 l2_error 對 syn0,1 變化的反應看作「敏感度」,或者變化率:即調整 syn0,1 的值後,l2_error 值的變化會與之成比例。這個比例是多大呢?l2_error 對 syn0,1 的敏感度如何呢?
記住:反向傳播的目標是找出每個權重需要調整的量,從而在下一次迭代中儘可能地降低 l2_error。但是挑戰在於每個權重都會對其他 15 個權重產生影響,因此權重 1 調整的量依賴於權重 2 調整的量,權重 2 的調整量依賴於權重 3 調整的量,依此類推。如何調整 16 個不斷變化的保齡球瓶取決於某個瓶子有多大變化。
不過我有一個更好的類比:蝴蝶。
5.3 定義鏈式法則:它像蝴蝶效應
還記得我們試圖解決的問題嗎?「如何調整 16 個權重,使每個瓶子都能與其他 15 個球瓶完美搭配,同時停留在空中,並且最小化 l2_error,幫助我到達城堡,找到生命的意義。」
我們將這個大問題分解一下。我們先看其中的一個權重 syn0 (1,1)(本文將其簡寫為 syn0,1)。我如何以有序、依存的方式調整 syn0,1,才能使 16 個球瓶正常運轉,同時還能儘可能多地降低 l2_error?
答案就是鏈式法則,它和蝴蝶效應很像(一隻新墨西哥州的蝴蝶扇動翅膀,引發了一系列連鎖事件,引起中國颶風)。我們來看這個類比如何應用到反向傳播中必須計算的變化率上:
而當我們將 syn0,1 的值進行增減時,新墨西哥州的蝴蝶開始扇動翅膀。出於簡潔性考慮,我們將 syn0,1 的值增加到 3.66,這一舉動引發了一系列連鎖反應——其他權重合力組成的「完美風暴」,從而降低 l2_error。
接下來,我們將蝴蝶效應和另一個類比——連鎖反應結合起來。
5.4 蝴蝶效應遇見 5 個變化率:連鎖反應
蝴蝶效應只是更寬泛概念——連鎖反應的一個例項。下圖將蝴蝶效應和變化率改變帶來的連鎖反應結合了起來,圖看起來很複雜,但它其實由以下三項組成:
和前文圖一樣的白色圓圈,從左至右表示蝴蝶效應;
圖下方方框中是反向傳播鏈式法則中的變化率。這些方框從右至左反向移動,即最右方的變化率改變會透過其他變化率最終影響到最左側的變化率。
彩色箭頭起連線作用。
Ripple 1,即對 syn0,1 進行調整後的第一個連鎖反應,將導致 l1_LH 上升一定比率,這個比率就是變化率。這就是「內華達州的大風」(見下圖灰色箭頭)。
由於 l1_LH 是 sigmoid 函式的輸入,那麼計算 l1_LH 和 L1 之間的變化率就需要衡量 Ripple 2,即「洛杉磯的狂風」(見下圖紫色箭頭)。
很顯然,l2_LH 會受到 l1 變化及 l1 乘以 syn1,1 的影響(為了簡化示例,syn1,1 及其他 14 個權重的值並未改變),因此 l2_LH 和 l2 之間的變化率帶來了 Ripple 3——「夏威夷的雷暴」(見下圖黃色箭頭)。
l2 的變化與 l2_LH 的變化成比例,因此 l2_LH 的坡度將帶來 Ripple 4——「太平洋的暴風雨」(見下圖綠色箭頭)。
最後,y 值減去新的 l2 得到的餘數——l2_error 將會改變,即 Ripple Effect #5——「中國的颶風」(見下圖藍色箭頭)。
我們的目標是計算每次連鎖反應帶來的變化率,找出 syn0,1 需要增減的量,以便在下一次迭代中最小化 l2_error。當我們說神經網路「學習」的時候,我們其實表達的是在每一次迭代中降低 l2_error,使得每一次迭代中網路的預測結果準確率都有提升。
倒過來看,我們可以說「颶風 l2_error 的變化取決於 l2 的變化,l2 的變化取決於 l2_LH 的變化,l2_LH 的變化取決於 l1 的變化,l1 的變化取決於 l1_LH 的變化,而 l1_LH 的變化取決於蝴蝶 syn0,1 的變化」。該示例中所用的規則即步長等於坡度。
此外,我還想讓大家瞭解 Python 程式碼和這些是如何同步的。接下來,我們就來看看哪些程式碼行對應鏈式法則函式中的變化率。
5.5 程式碼與鏈式法則的同步
5.5.1 移除中間變數
上圖中出現了程式碼行 66 到 115 中的程式碼,紅色線連線程式碼與變化率。如果你覺得這些連線不太對,這是因為原始程式碼將反向傳播過程分解成了多箇中間步和多個額外的中間變數。下面我們將從程式碼中移除以下四個中間變數:l2_error、l2_delta、l1_error 和 l1_delta。
先從紅色箭頭連線的程式碼片段開始。假設四個中間變數已經移除,還剩下什麼呢?我們來看下圖的最底下一行。將中間變數從程式碼中移除後,這行程式碼並沒有改變。現在這三行完美同步了。
5.6 實戰演示
下面,我們看看改變 syn0 中一個權重的數學背景。出於簡便,這裡的變數和第 3 節相同:
l0= 1
syn0,1= 3.66
l1_LH= 3.82
l1= 0.98
syn1,1= 12.21
l2_LH= 0.00
l2= 0.50
y= 1 (This is a "Yes" answer to survey question 4, "Ever bought Litter Rip?" which corresponds to training example #1, i.e., row 1 of l0)
l2_error = y-l2 = 1-0.5 = 0.5
5.6.1 不要重蹈我的覆轍
在解決變化率時,千萬不要和我犯同樣的錯誤。我最初是這麼計算變化率的(錯誤示範):
那麼上圖有什麼問題呢?
5.6.2 不是為了計算變化率,而是為了計算變化率的變化:坡度
在上圖中,我忘記了此時的目標是計算相對變化。看到上圖中每個變數前面的 d 了嗎?我忽略了它們,只寫下了每個變數的當前值。
而那些 d 表示 delta,delta 非常重要,不能忽略。
我們想預測未來。我們想知道 syn0,1 改變多少會最終導致下一次迭代中 l2_error 減小。也就是說,我們不是要對比 syn0,1 中單個數值 3.66 對 l2_error(0.5)的影響,而是想知道 syn0,1 中 3.66 應該如何變化才能影響到 5 個連鎖變化率,從而最終產生更好的 l2_error。我們不想要一堆變化率,我們想要的是能改變 delta 的變化率。
5.6.3 計算變化率的公式:x_current 和 x_nearby
「current」表示每個變數的當前值,「nearby」表示我們想提供的與當前數字接近的數字。nearby 數字減去 current 數字,會得到一個很小的數字。如果兩個點在一條曲線上,這方便計算出更準確的坡度值。接下來我們過一遍上述 5 個變化率,練習如何尋找每個變化率的 nearby 數字。
下圖展示了完整的反向傳播:
5.6.4 示例
從後往前,我們要先計算的 Ratio 1 是 Ripple 5:d l2_error / d l2。那麼「current」和「nearby」從哪裡來呢?
x_current 是前饋計算出的 l2——0.5;
y_current 是 y-l2 = 1-0.5 = 0.5,即 l2_error;
x_nearby 是我們舉的一個簡單例子。如果 l2 為 0.6,距離 x_current(0.5)較近,則 y-0.6 = 0.4;
也就是 x_nearby = 0.6,y_nearby = 0.4;
知道這四個變數後,計算就很簡單了,坡度即敏感度= -1;
這意味著 l2 每增加 0.1,l2_error 就減 0.1。
其他幾步的計算過程此處不再贅述,想了解更多,請閱讀原文。
總結
課程到這裡已接近尾聲,現在你們已經掌握了梯度下降的核心工具——反向傳播。最後,我將送給大家一份離別禮物:
Andrew Trask 教會我:記住這些 Python 程式碼才能精通。出於以下兩點原因我認同這個觀點:
當你試著根據記憶寫出程式碼時,你發現你忘記的地方恰好是不理解的地方。全部理解後,所有程式碼會永遠存在你的大腦中;
這些程式碼是構建所有深度學習網路的基礎。掌握了這些程式碼,你以後學習每一個網路、每一篇論文都會更加清晰和簡單。
牢記這些程式碼幫助我編了一個把所有概念串在一起的荒誕故事,你也可以編造自己的故事。
希望你能享受深度學習旅程!
原文連結:https://colab.research.google.com/drive/1VdwQq8JJsonfT4SV0pfXKZ1vsoNvvxcH