BERT引發的深度學習1

本文是作者即將在CSDN作直播的課程的預備知識,對課程感興趣但是沒有相關背景知識的同學可以提前學習這些內容。

目錄

背景知識

爲了理解課程的內容,讀者需要以下背景知識。

  • 深度學習基礎知識
  • Word Embedding
  • 語言模型
  • RNN/LSTM/GRU
  • Seq2Seq模型
  • Attention機制
  • Tensorflow基礎知識
  • PyTorch基礎知識

深度學習基礎知識

介紹機器學習和深度學習的基本概念。

介紹多層神經網絡(DNN)的基本概念

介紹反向傳播算法,不感興趣的讀者可以跳過細節。

建議瞭解一下就行,自動梯度是深度學習框架的基礎。我們通常不需要實現反向算法,因爲框架通常幫我們做了,但是瞭解一下它的原理是有用的。

內容主要參考CS231N課程的Notes,還是介紹自動梯度。

使用自動梯度實現多層神經網絡,對怎麼自己實現自動梯度感興趣的讀者可以參考,不感興趣的可以跳過。

卷積神經網絡的介紹。

卷積神經網絡的實現,代碼實現是用theano的,建議瞭解一下就行。現在更建議使用Tensorflow或者PyTorch

同上。

介紹Batch Normalization。讀者瞭解一下原理即可,現在的框架都有現成的。

Dropout是最常見也最有用的防止過擬合的技巧之一。

自動動手實現CNN,並且實現常見的CNN架構——VGG。比較關注實現細節,不感興趣的讀者可以跳過。

訓練ImageNet的過程可以跳過,因爲現在很少需要自己從零開始訓練ImageNet了,大部分框架都有Pretraining好的模型。讀者可以瞭解一下ResNetInception,後面的BERT也使用到了殘差連接,在如今(2019)這是非常常用的技巧了。

Michael Nielsen的免費書籍,作者前面的文章參考了很多裏面的內容。有興趣的讀者可以閱讀一下,大部分內容前面已經介紹過了,因此也可以跳過。

斯坦福的課程,作者的文章也參考了一些內容。有興趣的讀者可以學習一下,跳過不影響對課程的理解。

Word Embedding

理解Word Embedding的概念即可,跳過Word2Vec的推導、SoftmaxNegative Sample並不影響後續閱讀。

語言模型

理解語言模型的概念即可,N-Gram可以稍微瞭解一下,平滑和回退等tricky可以跳過,RNN語言模型需要RNN的知識,請參考RNN/LSTM/GRU的部分。

RNN/LSTM/GRUSeq2SeqAttention機制

介紹vanilla RNNLSTMGRU的基本概念。

包含PyTorch的基本概念,包括用RNN來進行人名國家分類,生成不同國家的人名。本來還有一個看圖說話的例子,但是編輯似乎忘了加進去。

使用PyTorch實現一個機器翻譯系統,包括LSTM/GRUAttention機制等內容。

使用PyTorch實現一個Chatbot。裏面會涉及Seq2Seq模型和Attention機制。

Tensorflow基礎知識

Tensorflow的基礎知識,熟悉的讀者也建議讀讀,也許會有新的收穫。

PyTorch基礎知識

來自官網的教程,包含60分鐘PyTorch教程、通過例子學PyTorch和遷移學習教程。

BERT

下面的內容會在課程上詳細講解,但是建議同學們提前預習一下。

通過圖解詳細的介紹Transformer的原理。

詳細解讀Transformer的代碼。

本文詳細介紹BERT模型的原理,包括相關的ELMoOpenAI GPT模型。

詳細解讀BERT模型的代碼。

 

 

  • 從Image Caption Generation理解深度學習(part-1)

0. 前面的話

建丁讓我寫一篇深度學習相關小文章,目標讀者是國內的開發者。剛接到這個任務時我是頗爲忐忑的,寫文章要講究厚積薄發,如果水之積也不厚則其負大舟也無力。因爲我自知水平很有限,又不是在學校和科研機構做研究,只不過因爲工作和個人的興趣,對深度學習有一點點粗淺的瞭解,所以擔心寫出來的東西不但於人無益,甚至還讓人誤入歧途。但後來又一想,如果把自己作爲一個深度學習的學習者,和對它感興趣的普通開發者分享一些學習的經歷,包括學習過程中遇到的問題,可能也是有一些意義的。畢竟讀論文或者聽學術大牛的講座只能看到成功的經驗,而且大部分開發者相對來說沒有太多的背景知識,而很多圈內的人都是假設讀者擁有這些知識的。但是對於普通的開發者來說,很多基礎知識比如線性代數和微積分在上完大學後估計就還給老師了,因此可能理解起來難度更大。而從另外一個角度來說,工程師(開發者)和科學家(科研工作者)關注的點也是不一樣的。科學家更關注理論的東西,比如一個模型是怎麼提出來的,爲什麼要這麼設計模型,這樣的模型怎麼轉化成一個優化問題。而工程師則更關注這個東西能夠做什麼,具體這個優化問題怎麼求解更高效。學術界每年有大量的論文發表,大量的idea被提出,其中有好有壞,有的工作可能看起來理論很漂亮,但實際應用起來很難;有些工作可能不被太多人關注,但卻是某些工業界非常需要的。

另外從人工智能的發展來說,我個人覺得在傳統行業的普及也是非常重要的。現在很多人工智能創業公司,很多想用人工智能創造一個全新的產品,比如早期類似Siri的語音助手到現在火熱的機器人。但我個人覺得目前的人工智能的水平還很難做出達到用戶預期的產品,尤其是很多初創公司吹牛吹得有些過分,導致用戶期望過高,而真正使用產品後則形成巨大的反差。我覺得目前階段人工智能更大的用處是提升現有系統,用我自己的話來說就是目前的人工智能只是錦上添花而不是雪中送碳。也就是說光靠人工智能是不能吸引用戶來購買你的產品的。

比如現在國外很火的Amazon的智能音箱產品Echo,如果我不想買一個音箱,估計你很難這樣說服我購買Echo——我們的Echo有非常智能的語音交互功能,可以問天氣,可以設置鬧鐘,可以Uber打車,可以控制家裏的智能冰箱。但是如果我想購買一個音箱,現在面臨兩個選擇:一個是傳統的音箱,另一個是Echo。那麼你對我說Echo有多麼牛逼的智能可能會打動我,反正也差不了多少錢,能有這麼多聽起來很酷炫的功能也挺不錯的。

由於Echo的成功,國內很多人也想山寨一個類似的產品,不過可能很多人忽略了美國和中國的一些細小差異,那就是音箱似乎不是大城市居民的必備品。就我個人的朋友圈來說,每個家庭肯定都有個電視,但是有音箱寥寥無幾。爲什麼會這樣呢,因爲中國的大城市居民大都是住樓房,很多老破小隔音效果都很差,你整個音箱弄家裏還沒high兩分鐘,估計鄰居就該敲門了。倒是耳機,屌絲們擠公交地鐵時的必備利器,也許會更好賣。

說了這麼多,想表達的就是目前人工智能應該更多的提高現有產品。比如提到Google,大家可能會想到它收購的DeepmindAlphaGo,但是我們可能沒有意識到日常使用的很多產品中都使用了深度學習。比如搜索引擎的排序,郵件的智能回覆生成,都大量使用了深度學習。而AlphaGo的作用則更多的是一種市場PR,一種宣傳作用,讓大家知道人工智能目前的一些進展,而現在AlphaGo團隊則是想將其技術用到醫療行業幫助醫生診斷疾病。

也就是說人工智能在未來也許就像計算機,互聯網,雲計算一樣是一個非常基礎的設施,在任何需要用機器來替代或者減少人力的場景都是有用武之地的。目前不論是國內還是國外,人工智能的人才都是非常稀缺的,而且都是集中在少數學校的實驗室和大公司的研究院裏。因此向普通開發者傳播相關的知識就顯得尤爲重要。基於這樣的考慮,雖然自己的能力不夠,但還是願意把自己學習的一些經驗和問題向大家分享。

1. 爲什麼分享Image Caption Generation這個話題?

這篇小文章並沒有限定什麼範圍,只要是深度學習相關的就行。這反倒讓人煩惱,就和人生一樣,選擇太多了也是一種煩惱。因爲最近工作有空之餘正在學習斯坦福的課程CS231NConvolutional Neural Networks for Visual Recognition。這個課程非常好,除了詳盡的slidesnotes,最值得一提的就是它的作業。每個作業包含完整的模型,比如CNNLSTM,所有的模型的代碼都只是用最簡單的python代碼實現,而不是用現成的庫比如TensorFlow/Theano/Caffe。紙上得來終覺淺,絕知此事要躬行。很多理論,光聽課看slides,似乎覺得自己懂了,其實還是一知半解,真正要掌握,就得自己動手,最好是全部自己實現。但是全部自己實現需要花的時間太多,而且從實際工作的角度來說,大部分開發者肯定都是用TensorFlow這樣的工具。而這個課程的好處就是:把一些瑣碎的與核心代碼不相關的部分包括學習的框架都已經實現了,然後用IPython notebook把關鍵的代碼的函數的輸入和輸出都描述的非常清楚,學習者只需要實現一個一個這樣的函數就行了,而且每個函數都會有類似單元測試的檢測代碼正確性的數據,從而保證我們的每一步都是在朝着正確的方向前進。

因此這篇小文章打算講一講其中的Assignment3Image Caption Generation部分。目的是想通過一個具體的任務來給大家介紹深度學習的一些知識,讓大家對深度學習有一些概念和興趣。選擇Image Caption Generation的原因,一來這個任務挺有意思的;第二就是它涉及到很多深度學習流行的模型如CNNRNN/LSTMAttention

首先來介紹一下什麼叫做Image Caption Generation

對於計算機視覺相關的任務,圖片分類和定位大家可能比較熟悉。圖片分類就是給定一張圖片,讓計算機告訴我們它是一隻貓還是一隻狗;而圖片定位除了告訴我們這是一張狗的圖片,還需要用用一個矩形框把狗的位置標識出來。當然還有要求更高的Image Segmentation,需要告訴我們哪一些像素屬於狗,而另外一些屬於背景。

1就是這些任務的例子:


1:常見機器視覺任務 圖片來自 http://cs231n.stanford.edu/slides/winter1516_lecture8.pdf

Image Caption Generation任務是給定一張圖片,需要讓計算機用一句話來描述這張圖片。

如圖2所示:


2Caption Generation任務示例 圖片來自 http://mscoco.org/dataset/#captions-challenge2015

從實際的應用來說,這個任務也是很有用處的。比如一個手機拍完照片之後,我們可以用這個方法生成一句話來描述這個圖片,方便分享和以後查找。

而從理論研究的角度來說,Caption Generation相對於之前的task來說需要更加深入理解圖片中物體之間的關係,甚至包括一些抽象的概念。它把一幅信息量極大的圖片壓縮成短短一句話。

我是做自然語言處理(NLP)相關工作的,之前對計算機視覺有一些錯誤的看法。認爲視覺信號是更底層和原始的信號,除了人類,動物也有很強的視覺能力,也能分辨不同物體。而語言是人類創造的符號系統,屬於更高層的抽象,因而屬於更高級的人工智能問題,似乎不少人會有類似的觀點。

但是現在我有了一些不同的看法,人類的大腦並沒有什麼特殊之處。一個小孩在一歲之前一般不會說話,他認識世界的主要方式就是主要通過視覺系統來區分物體,也許和神經網絡類似,通過複雜的神經元的連接來理解世界。這些不同層次的網絡就是不同層次的特徵,就像神經網絡的黑盒,我們自己也很難用精確的語言描述我們大腦到底學習到了什麼樣的特徵。而且很可能每個人學到的特徵,尤其是底層的特徵都是不相同的。

比如下圖的一個汽車,最底層的特徵可能是不同方向的線條,而中間層的特徵可能是各種基本的形狀,而更上層的特徵就是車輪這樣的更上層概念。


圖片來自 http://cs231n.stanford.edu/slides/winter1516_lecture7.pdf

一個複雜的概念由一些簡單的概念組合而成,而簡單的概念可能由最基本的原子概念組合而成。語言就是對這些概念的描述,或者說就是一個標籤,一種命名。但是語言有一個特點就是它是用來溝通和交流的,所以語言的使用者需要達成一定程度的共識。那怎麼達成共識呢,比如我們在教小孩語言時是怎麼與他達成共識的呢?比如一個桌子,我們通過手指這一個條狗狗,反覆對小孩說狗狗這個詞(其實是聲音,爲了簡化,我們暫且當成文字),這樣我們就和小孩達成了共識,狗狗就是指這樣一個動物,然後又指着另外一條狗狗,也說狗狗,小孩就學到這一物體都是狗狗。所以他需要調整他的神經元連接,使得那些符合某種特徵的物體都被識別成狗狗。至於具體這個識別狗狗的神經網絡的參數是什麼樣的,我們很難知道,也許剛開始他需要分類的物體很少,比如只有爸爸媽媽狗狗,那麼它可能需要不是那麼本質的特徵來區分,比如他可能認爲四條腿走的是狗狗,兩條腿直立行走的就是爸爸媽媽。當隨着需要識別的類別的增多,比如有了貓貓,那他一上來可能認爲也是狗狗,但父母告訴他分類錯誤,這不是狗狗而是貓貓。那麼他可能需要別的特徵來區分貓貓和狗狗,也許他學到的是:四條腿走並且嘴很長的是狗狗,而四條腿圓臉的是貓貓。

那爲了能夠區分貓貓和狗狗,小孩的中層的特徵可能需要抽取類似的特徵,或者說概念。我們也會告訴他這是狗狗的臉,這是貓貓的臉,這是爸爸的臉。這樣他需要學習出臉的共性的特徵。

從上面的過程我們可以發現,概念本身只是一種特徵的指代,是我們的感覺系統(視覺)對一個物體的反應。而語言是一部分相似的生物對同一個/類物體達成共識的一種指代。但每個人的感覺系統和神經網絡結構都是不一樣的,所以也只能在非常粗糙的程度達成比較一致的共識,而在非常精細的概念層次是很難達成廣泛共識的。因此我們會把周圍的人打上各種標籤,分成各種類別,由此各種概念也就產生——膚色,語言,宗教,性別,階級。每個人也只能和同一個標籤的人在某個方面達成共識,所以要找到一個完全瞭解自己的人是如此之難,而不同的物種的共識可能就更難了。所以就像《莊子·齊物論》裏說的毛嬙、麗姬,人之所美也;魚見之深入,鳥見之高飛,麋鹿見之決驟。四者孰知天下之正色哉?自我觀之,仁義之端,是非之塗,樊然殽亂,吾惡能知其辯!毛嬙、麗姬是我們人類眼中的美,但是在魚和雁看來只是可怕的敵人。可笑的是自戀的人類卻還要曲解莊子的願意,認爲它們是因爲驚異於她們的美麗才沉魚落雁閉月羞花的。不說動物,即使是人類而言,美也是很難達成共識的,那些黑人國家的美女,我們中國人是很少會認爲她們是美女的。

因此從這個意義上來說,語言也許並沒有我們想像中的那麼高大上。 就目前人工智能或者深度學習的水平來說,也許研究小孩在建立複雜概念之前的行爲更有用處。

 

 

從Image Caption Generation理解深度學習(part II)

2. 機器學習基本概念和前饋神經網絡

2.1 機器學習基本概念

大家可能平時都寫過很多程序,寫程序和機器學習的思路可能有一些不同。寫程序時,我們是“上帝”,我們規定計算機的每一個步驟,第一步做什麼第二步做什麼,我們稱之爲算法。我們能夠控制所有的情況,如果出了任何問題,肯定都是程序員的責任。而在機器學習的時候,我們只是“老師”。我們告訴學生(計算機)輸入是什麼,輸出是什麼,然後期望它能夠學到和我們類似的知識。比如我們跟小孩說這是狗,那是貓,我們沒有辦法像上帝那樣拿着“納米手術刀”去操作人腦神 經元的連接方式。我們只能不斷的給小孩“訓練數據”,然後期望他能夠學會什麼是貓,即使我們覺得他“學會”了識別貓,我們也沒有辦法知道他是“怎麼”學會 的,而且同樣的訓練過程可能換一個人就不好使。

機器學習和人類的學習是類似的——我們也是給它訓練數據,然後期望它能學會。我們會給機器建一個模型,從數學的角度來說一個模型就是一個函數,它的輸入一般是一個向量【當然可以是二維的矩陣如圖片或者三維的張量比如視頻】,輸出可以是有限的離散的標籤如“貓”,“狗”,這類問題我們稱之爲分類;而如果輸出 是連續的值比如用這個模型來預測氣溫,那麼我們就稱之爲迴歸。其實人類的很多科學活動和日常生活,都是在“學習”模型和“應用”模型。比如開普勒通過觀測 大量天文數據“歸納”出行星的運動規律。從本質上講,智能就是從“過去”學習,然後根據“現在”來預測可能的將來並根據自己的目標選擇有利於自己行爲。只不過之前,似乎只有人類能夠從數據中“學習”出規律,而人工智能的目標就是讓機器也有類似的學習能力。

模型用數學來說就是一個函數,我們人腦的函數由神經元的連接構成,它可能是一個很複雜的函數,我們現在還很難徹底研究清楚。神經網絡就是試圖通過計算機來 模擬和借鑑人腦這個模型,除了我們這裏要講的神經網絡之外,機器學習領域還有各種各樣的模型,它們各有特點。但不管形式怎麼變化,本質都是一個函數。一個(或者更準確的是一種)模型一般都是一種函數形式,它有一些“參數”可以改變。而學習的過程就是不斷調整這些參數,使得輸出(儘量)接近“正確”的答案。 但是一般情況下很難所有的數據我們都能預測正確,所以一般我們會定義一個loss function,可以理解爲“錯誤”的程度,錯的越“離譜”,loss就越大。而我們的目標就是調整參數使得loss最小。

但是我們是在“訓練”數據上調整的參數,那麼它能在“測試”數據上也表現的好嗎?這個就是模型的“泛化”能力了。就和人在學校學習一樣,有的同學做過的一 模一樣的題就會,但是考試時稍微改變一下就不會了,這就是“泛化”能力太差,學到的不是最本質的東西。所以平時會定期有一些“模擬考試”,來檢驗學生是不 是真的學會了,如果考得不好,那就打回去重新訓練模型調整參數。這在機器學習裏對應的就是validation的階段。最後到最終的考試了,就是最終檢驗 的時候了,這個試卷裏的題目是不能提前讓人看到的,只能拿出來用一次,否則就是作弊了。對應到機器學習裏就是test階段。

當然這裏用通俗的話描述了機器學習,主要是有監督的學習。其實機器學習還有無監督的學習和強化學習。前者就是不給答案,只給數據,讓人總結規律;而後者會有答案,但是答案不是現在就告訴你。我個人覺得人類社會裏更多的是監督學習和強化學習。從人類社會總體來說,強化學習是獲取新知識的唯一途徑,也就是向自 然學習,我們做了一個決策,其好壞可能要很長一段時間才能顯現出來。而學習出來的這些知識通過監督的方式,通過家庭和學校的教育教給下一代。

另外輸出除了簡單的分爲離散和連續,還可以是序列(時序)的,比如自然語言(文本)是一個字符串的序列 ,對於我們的Image Caption Generation就是生成一個單詞序列。另外還有更復雜的輸出,比如parsing,輸出是一棵語法樹。

2.2 多層神經網絡

前面介紹了機器學習的基本概念,接下來我們就來學習一下神經網絡。現在流行的說法“深度學習”,其實大多指的就是“深度神經網絡”,那麼首先我們先了解一下“淺度神經網絡”,也就是傳統的神經網絡。這裏的內容主要來自http://neuralnetworksanddeeplearning.com的前兩章。

2.2.1 手寫數字識別問題

我們在學習一門新的語言時會寫一個hello world程序,而mnist數據的手寫數字識別就是一個很好的學習機器學習(包括深度學習)的一個hello world任務。

計算機和人類大腦似乎有很大的不同,很多人類認爲複雜的工作計算機可能認爲很簡單,而人類認爲很簡單的事情計算機可能非常難處理。比如數字的計算,記憶,人類的準確度和速度都遠遠不如計算機。但是識別0-9的手寫數字,我們覺得很輕而易舉的事情,讓計算機程序來處理卻異常困難。經過數百萬年進化的人類視覺系統在我們大腦沒有意識到的時候就已經幫我們完成了數字的識別,把那些複雜的視覺處理過程深深的掩藏了起來。但當我們想自己寫一個程序來識別數字的時候,這些困難才能體現出來。首先,對於計算機來說,它“看到”的不是數字,甚至不是筆畫。它“看到”的只是一個二位的矩陣(數組),每個點都是一個數字。比如下圖,我們“看到”的是左邊的“貓”,其實計算機“看到”的是右邊的像素灰度值。當然我們視覺系統的視網膜看到的也是類似的一些“數值”,只不過我們的視覺系統已經處理了這些信息並且把它識別成了“貓”(甚至和語言還做了映射)。

圖片描述

MNIST數據介紹:MNIST的每個圖片經過縮放和居中等預處理之後,大小是28*28,每個點都是0-255的灰度值,下圖是一些樣例。總共有60,000個訓練數據(0-9共10個類別,每個類別6,000個)和10,000個測試數據。一般會拿60000箇中的50000個來做訓練集,而剩下的10000個用來做驗證集(用來選擇一些超參數)。

圖片描述

 

mnist樣例數據

 

如果我們自己來寫一個“算法”識別數字“9”,我們可能會這麼定義:9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。說起來很簡單,如果用算法 來實現就很麻煩了:什麼是圓圈?每個人畫的圓圈都不同,同樣豎直的筆畫怎麼識別,圓圈和豎直筆畫連接處怎麼尋找,右下是哪?大家如果有興趣可以嘗試一下用 上面的方法,其實最早做數字識別就是這樣的思路。

機器學習的思路則不同,它不需要這麼細節的“指示”計算機應該怎麼做。而是給計算機足夠的“訓練”樣本,讓它“看”不同的10個數字,然後讓它“學”出 來。前面我們也講了,現在的機器學習一般是一個參數化的模型。比如最簡單的一個線性模型:f(w;x)=w0+ w1*x1+w2*x2。如果我們的輸入有兩個“特徵”x1和x2,那麼這個模型有3個參數w0,w1和w2,機器學習的過程就是選擇“最優”的參數。對 於上面的mnist數據,輸入就是28*28=784維的向量。

如果用“原始”的輸入作爲“特徵”,線性的模型很可能學到一些簡單的特徵,比如它看到1一般是分佈在從上到下居中的一些位置,那麼對於這些位置一旦發現有比較大的灰度值,那麼就傾向於判斷成1。如果一個像素點2也經常出現,但3不出現,那麼它就能學到如果這個像素出現,那麼這個數字是2和3的可能性就大一些。

但是這樣的“特徵”可能不是“本質”的,因爲我寫字的時候筆稍微平移一點,那麼你之前“學到”的參數就可能有問題。而更“本質”的特徵是什麼呢?可能還是像之前我們總結的——9在上面有個圓圈,在這個圓圈的右下部分有一個豎直的筆畫。我們把識別一個數字的問題轉化成圓圈和豎直筆畫的問題。傳統的機器學習需要方法來提取“類似”(但不完全是)基本筆畫這樣的“特徵”,這些特徵相對於像素的特徵會更加“本質”。但是要“提取”這些特徵需要很多的“領域”知識,比如圖像處理的技術。所以使用傳統的機器學習方法來解決問題,我們不但需要很多機器學習的知識,而且也需要很多“領域”的知識,同時擁有這兩方面的知識是比較難的。

而“深度學習”最近之所以火熱,其中很重要的一個原因就是對於很多問題,我們只需要輸入最原始的信號,比如圖片的像素值,通過“多層”的網絡,讓底層的網絡學習出“底層”的特徵,比如基本的形狀,而中間的層學習出抽象一點的特徵,比如眼睛鼻子耳朵。而更上的層次識別出這是一個貓還是一個狗。所有這些都是機器學習出來的,所以基本不需要領域的知識。

圖片描述

上面的圖就說明了這一點,而且我們發現越是底層的特徵就越“通用”,不管是貓鼻子還是狗眼睛,可能用到的都是一些基本的形狀,因此我們可以把這些知識(特徵)transfer到別的任務,也就是transfer learning,後面我們講到CNN的時候還會提及。

2.2.2 單個神經元和多層神經網絡(MLP)

神經網絡從名字來看是和人類的大腦有些關係的,而且即使到現在,很多有用的東西如CNN和Attention,都有很多借鑑神經科學研究人腦的結果的。不過這裏我就不介紹這些東西了,有興趣的讀者可以找一些資料來了解。

一個神經元如下圖的結構:

圖片描述

它的輸入是一個向量,(x1,x2,x3),輸出是一個標量,一個實數。z=w0+ w1*x1 + w2*x2 + w3*x3。z是輸入的加權累加,權值是w1,w2,w3,w0是bias,輸出 output = f(z)。函數f一般叫做激活函數。最早流行的激活函數是Sigmoid函數,當然現在更流行Relu和它的改進版本。Sigmoid函數的公式和圖形如下:

圖片描述

圖片描述

當z=0時,sigmoid(z)=0.5 z趨於無窮大時,sigmoid(z)趨近於1,z趨於負無窮,值趨於0。爲什麼選擇這樣的激活函數呢?因爲是模擬人腦的神經元。人腦的神經元也是把輸入的信號做加權累加,然後看累加和是否超過一個“閾值”。如果超過,繼續向下一個神經元發送信號,否則就不發送。因此人腦的神經元更像是一個階躍函數:

圖片描述

最早的感知機(Perception)其實用的就是這個激活函數。但是它有一個缺點就是0之外的所有點的導數都是0,在0點的導數是無窮大,所以很難用梯度的方法優化。而Sigmoid函數是處處可導。下面我手工推導了一下,如果大家不熟悉可以試着推導一下Sigmoid函數的導數,我們後面也會用到。

圖片描述

我們把許多的單個神經元按照層次組織起來就是多層的神經網絡。

圖片描述

比如我們的手寫數字識別,輸入層是784維,就是神經網絡的地一層,然後中間有15個hidden(因爲我們不知道它的值)神經元,然後輸出層是10個神經元。中間隱層的每個神經元的輸入都是784個原始像素通過上面的公式加權累加然後用sigmoid激活。而輸出層的每一個神經元也是中間15個神經元的累加然後激活。上面的圖就是一個3層的神經網絡。

輸入一個28*28的圖像,我們得到一個10維的輸出,那麼怎麼分類呢?最直接的想法就是把認爲最大的那個輸出,比如輸出是(10,11,12,13,14,15,16,17,18,19),那麼我們認爲輸出是9。

當然,更常見的做法是最後一次經過線性累加之後並不用Sigmoid函數激活,而是加一個softmax的函數,讓10個輸出加起來等於1,這樣更像一個 概率。而我們上面的情況,雖然訓練數據的輸出加起來是1,但是實際給一個其它輸入,輸出加起來很可能不是1。不過爲了與Nielsen的文章一致,我們還 是先用這種方法。

因此,假設我們有了這些參數【總共是784*15 + 15(w0或者叫bias) + 15*10 + 10】,我們很容易通過上面的公式一個一個的計算出10維的輸出。然後選擇最大的那個作爲我們識別的結果。問題的難點就在怎麼 選擇這麼多參數,然後使得我們分類的錯誤最少。

而我們怎麼訓練呢?對於一張圖片,假設它是數字“1”,那麼我們期望它的輸出是(0,1,0,0,0,0,0,0,0,0),所以我們可以簡單的用最小平方錯誤作爲損失函數。不過你可能會有些疑問,我們關注的指標應該是分類的“正確率”(或者錯誤率),那麼我們爲什麼不直接把分類的錯誤率作爲損失函數呢?這樣神經網絡學習出來的參數就是最小化錯誤率。

主要的原因就是錯誤率不是參數的連續函數。因爲一個訓練數據如果分類正確那麼就是1,否則就是0,這樣就不是一個連續的函數。比如最簡單的兩類線性分類器,f(x)=w0+w1*x1+w2*x2。如果f(x)>0我們分類成類別1;否則我們分類成類別2。如果當前的w0+w1*x1+w2*x2<0,我們很小的調整w0(或者w1,w2),w0+w1*x1+w2*x2仍然小於0,【事實上對於這個例子,只要是w0變小,他們的累加都是小於0的】所以f(x)的值不會變化,而w0一直增大到使累加和等於0之前都不會變化,只有大於0時突然變成1了,然後一直就是1。因此之前的錯誤率都是1,然後就突然是0。所以它不是個連續的函數。

因爲我們使用的優化算法一般是(隨機)梯度下降的算法,在每次迭代的時候都是試圖做一個微小的參數調整使得損失變小,但是不連續的函數顯然也不可導,也就沒法用這個算法來優化參數。

因此我們使用了最小平方誤差(MSE)損失函數。

圖片描述

y(x)就是神經網絡的輸出,可能寫成f(x)大家會習慣一點。a是目標的輸出,比如當前分類是數字1,那麼我們期望的輸出就是(0,1,0,0,0,0,0,0,0,0)。

首先這個損失函數是參數w的連續函數,因爲y(x)就是神經網絡的輸出,每個神經元都是它的輸入的線性加權累加,然後使用sigmoid激活函數【如果使用最早的階躍函數就不連續了,所以後來使用了Sigmoid函數】,然後每一層的神經元都是用上一層的神經元通過這樣的方式計算的(只不過每個神經元的參數也就是權重是不同的數值而已),所以這些連續函數的複合函數也是連續的。

其次這個損失函數和我們的最終優化目標是“大致”一致的。比如C(w,b)趨於0時,它就要求y(x)趨於a,那麼我們的分類也就趨於正確。當然可能存在一種極端的情況,比如有3個訓練數據,第一組參數,它分類正確了2個訓練數據,但是錯的那1個錯的很“離譜”,也就是y(x)和a差距極大;而第二組參數,他正確分類了1個訓練數據,但是錯的那兩個都還不算太差。那麼這種情況下MSE和正確率並不一致。

2.2.3 隨機梯度下降(Stochastic Gradient Descent)和自動求梯度(Automatic Derivatives)

上面說了,我們有了一個參數化的模型,訓練的過程就是根據訓練數據和loss function,選擇“最優”的參數,使得loss“最小”,這從數學上來講就是一個優化問題。這看起來似乎不是什麼值得一提的問題,也許你還記得微積 分裏的知識,極值點的各種充分必要條件,比如必要條件是導數是0,然後直接把參數解出來。但在現實生活中的函數遠比教科書裏學到的複雜,很多模型都無法用 解析的方式求出最優解。所以現實的方法就是求“數值”解,一般最常見的方法就是迭代的方法,根據現在的參數,我們很小幅度的調整參數,使得loss變小一 點點。然後一步一步的最終能夠達到一個最優解(一般是局部最優解)。那怎麼小幅調整呢?像悶頭蒼蠅那樣隨機亂試顯然效率極低。因此我們要朝着一個能使函數 值變小的方向前進。而在一個點能使函數值變小的方向有無窮多個,但有一個方向是下降速度最快的,那就是梯度。因此更常見的方法就是在當前點求函數的梯度, 然後朝着梯度的方向下降。朝梯度的方向走多遠呢?一般走一個比較小的值是比較安全的,這個值就是“步長”。一般剛開始隨機的初始化參數,loss比較大, 所以多走一些也沒關係,但是到了後面,就不能走太快,否則很容易錯過最優的點。

因爲loss是所有訓練數據的函數,所以求loss的梯度需要計算所有的訓練數據,對於很多task來說,訓練數據可能上百萬,計算一次代價太大,所以一 般會“隨機”的採樣少部分數據,比如128個數據,求它的梯度。雖然128個點的梯度和一百萬個的是不一樣的,但是從概率來講至少是一致的方向而不會是相 反的方向,所以也能使loss變小。當然這個128是可以調整的,它一般被叫做batch size,最極端的就是batch是1和一百萬,那麼分別就是online learning和退化到梯度下降。batch size越大,計算一次梯度的時間就越久【當然由於GPU和各種類似SSE的指令,一次計算128個可能並不比計算1個慢多少】,隨機梯度和真正梯度一致 的概率就越大,走的方向就更“正確”;batch size越小,計算一次的時間就越短,但可能方向偏離最優的方向就更遠,會在不是“冤枉路”。但實際的情況也很難說哪個值是最優的,一般的經驗取值都是幾 十到一兩百的範圍,另外因爲計算機都是字節對齊,32,64,128這樣的值也許能稍微加快矩陣運算的速度。但是實際也很多人選擇10,50,100這樣 的值。

除了常見的隨機梯度下降,還有不少改進的方法,如Momentum,Adagrad等等,有興趣的可以看看http://cs231n.github.io/neural-networks-3/#update ,裏面還有個動畫,比較了不同方法的收斂速度的比較。

通過上面的分析,我們把問題變成了怎麼求loss對參數W的梯度。

求梯度有如下4種方法:

  1. 手工求解析解

    比如 f(x)=x^2, df/dx=2*x。然後我們要求f(x)在x=1.5的值,代進去就2*1.5=3

  2. 數值解

    使用極限的定義:

    圖片描述

  3. 機器符號計算

    讓機器做符號運算,實現1的方法,但是機器如果優化的不好的話可能會有一些不必要的運算。

    比如 x^2 + 2*x*y + y^2,直接對x求導數變成了 2*x + 2*y,兩次乘法一次加分,但是我們可以合併一下變成2*(x+y),一次乘法一次加分。

  4. 自動梯度

    下面我會在稍微細講一下,所以這裏暫時跳過。

這些方法的優缺點:

  1. 手工求解“數學”要求高,有可能水平不夠求不對,但效率應該是能最優的。

  2. 沒任何函數,甚至沒有解析導數的情況下都能使用,缺點是計算量太大,而且只是近似解【因爲極限的定義】,在某些特別不“連續”的地方可能誤差較大。所以實際使用是很少,只是用它來驗證其它方法是否正確。

  3. 機器符號計算,前面說的,依賴於這個庫的好壞。

實際的框架,如TensorFlow就是自動梯度,而Theano就是符號梯度。

2.2.4 編程實戰

通過上面的介紹,我們其實就可以實現一個經典的前饋(feed forward)神經網絡了,這種網絡結構很簡單,每一層的輸入是前一層的輸出。輸入層沒有輸入,它就是原始的信號輸入。而且上一層的所有神經元都會連接到下一層的所有神經元,就像我們剛纔的例子,輸入是784,中間層是15,那麼就有785*15個連接【再加上每個中間節點有一個bias】。所以這種網絡有時候也加做全連接的網絡(full connected),用來和CNN這種不是全連接的網絡有所區別,另外就是信號是從前往後傳遞,沒有反饋,所以也叫前潰神經網絡,這是爲了和RNN這種有反饋的區別。

當然,我們還沒有講怎麼計算梯度,也就是損失函數相對於每一個參數的偏導數。在下一部分我們會詳細討論介紹,這裏我們先把它當成一個黑盒的函數就好了。

  1. 代碼 
    我們這裏學習一下Nielsen提供的代碼。代碼非常簡潔,只有不到100行代碼。 
    https://github.com/mnielsen/neural-networks-and-deep-learning

    git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git

  2. 運行

    創建一個 test_network1.py,輸入如下代碼:

import mnist_loader
import network

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

保存後直接運行 Python test_network1.py。這裏我們讓他進行了30次迭代,最終在測試數據上的準確率大概在95%左右(當然因爲隨機初始化參數不同,最終的結果可能有所不同)

Epoch 0: 8250 / 10000Epoch 1: 8371 / 10000Epoch 2: 9300 / 10000......Epoch 28: 9552 / 10000Epoch 29: 9555 / 10000

3. 代碼閱讀

Python代碼很容易閱讀,即使之前沒有用過,稍微學習兩天也就可以上手,而且大部分機器學習相關的代碼不會用到太複雜的語言特性,基本就是一些數學的線性代數的運算。而Python的numpy這個庫是用的最多的,後面閱讀代碼的時候我會把用到的函數做一些介紹,繼續下面的閱讀之前建議花十分鐘閱讀一下 http://cs231n.github.io/python-numpy-tutorial/

3.1 mnist_loader.load_data_wrapper函數

這個函數用來讀取mnist數據,數據是放在data/mnist.pkl.gz。首先這是個gzip的壓縮文件,是Pickle工具序列化到磁盤的格式。不熟悉也沒有關係,反正我們知道這個函數的返回值就行了。

這個函數返回三個對象,分別代表training_data,validation_data和test_data。

training_data是一個50,000的list,然後其中的每一個元素是一個tuple。tuple的第一個元素是一個784維的numpy一維數組。第二個元素是10維的數組,也就是one-hot的表示方法——如果正確的答案是數字0,那麼這個10維數組就是(1, 0, 0, …)。

而validation_data是一個10,000的list,每個元素也是一個tuple。tuple的第一個元素也是784維的numpy一維數組。第二個元素是一個0-9的數字,代表正確答案是那個數字。

test_data的格式和validation_data一樣。

爲什麼training_data要是這樣的格式呢?因爲這樣的格式計算loss更方便一些。

3.2 Network類的構造函數

我們在調用net = network.Network([784, 30, 10])時就到了init函數。爲了減少篇幅,代碼裏的註釋我都去掉了,重要的地方我會根據自己的理解說明,但是有空還是值得閱讀代碼裏的註釋。

class Network(object):
    def __init__(self, sizes):self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)                        for x, y in zip(sizes[:-1], sizes[1:])]

比如上面的參數,我們保存下來的self.num_layers=3,也就是3層的網絡。每一層的神經元的個數保存到self.sizes裏。接下來就是構造biases數組並隨機初始化。因爲輸入層是沒有參數的,所以是for y in sizes[1:],我們使用了numpy的random.randn生成正態分佈的隨機數用來作爲參數的初始值。注意這裏生成了2維的隨機變量。回憶一下,如果我們有30個hidden unit,那麼bias的個數也是30,那就生成一個30維的1維數組就行了,爲什麼要是30*1的二維數組呢?其實用1維也可以,不過爲了和weights一致,後面代碼方便,就用二維數組了。另外weights也是一樣的初始化方法,不過注意randn(y,x)而不是randn(x,y)。比如對於我們輸入的[784,30,10],weights分別是30*784和10*30的。當然其實weights矩陣轉置一下也可以,就是計算矩陣乘法的時候也需要有一個轉置。不同的文獻可能有不同的記法,但是我們在實現代碼的時候只需要隨時注意矩陣的大小,檢查矩陣乘法滿足乘法的約束就行了,矩陣AB能相乘,必須滿足的條件是B的列數等於A的函數就行。

對於Nielsen的記法,矩陣的每一行就是一個神經元的784個參數,那麼weights(30*784) * input(784*1)就得到30個hidden unit的加權累加。

3.3 feedforward函數

給點輸入a(784維),計算最終神經網絡的輸出(10維)。

def feedforward(self, a):
    """Return the output of the network if ``a`` is input."""for b, w in zip(self.biases, self.weights):
        a = sigmoid(np.dot(w, a)+b)    return a

代碼非常簡單,這裏用到了np.dot,也就是矩陣向量的乘法,此外這裏有一個Sigmoid函數,這個函數的輸入是numpy的ndarray,輸出也是同樣大小的數組,不過對於每個元素都進行了sigmoid的計算。用numpy的術語就是universal function,很多文獻裏一般都叫elementwise的function。我覺得後面這個名字更直接。

#### Miscellaneous functionsdef sigmoid(z):
    """The sigmoid function."""return 1.0/(1.0+np.exp(-z))def sigmoid_prime(z):
    """Derivative of the sigmoid function."""return sigmoid(z)*(1-sigmoid(z))

上面就是Sigmoid函數,另外也把sigmoid_prime,也就是Sigmoid的導數放在了一起【不記得的話看前面Sigmoid的導數的推導】。

3.4 SGD函數

這個函數是訓練的入口,比如我們之前的訓練代碼:

net.SGD(training_data, 30, 10, 3.0, test_data=test_data)def SGD(self, training_data, epochs, mini_batch_size, eta,
        test_data=None):if test_data: n_test = len(test_data)
    n = len(training_data)    for j in xrange(epochs):
        random.shuffle(training_data)
        mini_batches = [
            training_data[k:k+mini_batch_size]            for k in xrange(0, n, mini_batch_size)]        for mini_batch in mini_batches:
            self.update_mini_batch(mini_batch, eta)        if test_data:            print "Epoch {0}: {1} / {2}".format(
                j, self.evaluate(test_data), n_test)        else:            print "Epoch {0} complete".format(j)

第一個參數就是training_data。

第二個參數就是epochs,也就是總共對訓練數據迭代多少次,我們這裏是30次迭代。

第三個參數是batch大小,我們這裏是10,最後一個參數是eta,也就是步長,這裏是3.0。除了網絡結構(比如總共多少個hidden layer,每個hidder layer多少個hidden unit),另外一個非常重要的參數就是步長。前面我們也討論過了,步長太小,收斂速度過慢,步長太大,可能不收斂。實際的情況是沒有一個萬能的準則,更多的是根據數據,不停的嘗試合適的步長。如果發現收斂太慢,就適當調大,反之則調小。所以要訓練好一個神經網絡,還是有很多tricky的技巧,包括參數怎麼初始化,激活函數怎麼選擇,比SGD更好的優化算法等等。

第四個參數test_data是可選的,如果有(我們的例子是穿了進來的),則每次epoch之後都測試一下。

代碼的大致解釋我用註釋的形式嵌在代碼裏了:

    for j in xrange(epochs): ## 一共進行 epochs=30 輪迭代
        random.shuffle(training_data)  ## 訓練數據隨機打散
        mini_batches = [
            training_data[k:k+mini_batch_size]            for k in xrange(0, n, mini_batch_size)] ## 把50,000個訓練數據分成5,000個batch,每個batch包含10個訓練數據。
        for mini_batch in mini_batches: ## 對於每個batch
            self.update_mini_batch(mini_batch, eta) ## 使用梯度下降更新參數
        if test_data: ## 如果提供了測試數據
            print "Epoch {0}: {1} / {2}".format(
                j, self.evaluate(test_data), n_test) ## 評價在測試數據上的準確率
        else:
            print "Epoch {0} complete".format(j)

下面是evaluate函數:

def evaluate(self, test_data):
    test_results = [(np.argmax(self.feedforward(x)), y)                    for (x, y) in test_data]    return sum(int(x == y) for (x, y) in test_results)

對於test_data裏的每一組(x,y),y是0-9之間的正確答案。而self.feedforward(x)返回的是10維的數組,我們選擇得分最高的那個值作爲模型的預測結果np.argmax就是返回最大值的下標。比如x=[0.3, 0.6, 0.1, 0, ….],那麼argmax(x) = 1。

因此test_results這個列表的每一個元素是一個tuple,tuple的第一個是模型預測的數字,而第二個是正確答案。

所以最後一行返回的是模型預測正確的個數。

3.5 update_mini_batch函數

def update_mini_batch(self, mini_batch, eta):nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]    for x, y in mini_batch:
        delta_nabla_b, delta_nabla_w = self.backprop(x, y)
        nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
        nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
    self.weights = [w-(eta/len(mini_batch))*nw                    for w, nw in zip(self.weights, nabla_w)]
    self.biases = [b-(eta/len(mini_batch))*nb                   for b, nb in zip(self.biases, nabla_b)]

它的輸入參數是mini_batch【size=10的tuple(x,y)】和eta【3.0】。

def update_mini_batch(self, mini_batch, eta):nabla_b = [np.zeros(b.shape) for b in self.biases]  ## 回憶一下__init__,biases是一個列表,包含兩個矩陣,分別是30*1和10*1
  ## 我們先構造一個和self.biases一樣大小的列表,用來存放累加的梯度(偏導數) 
    nabla_w = [np.zeros(w.shape) for w in self.weights]  ## 同上, weights包含兩個矩陣,大小分別是30*784和10*30 
    for x, y in mini_batch:
        delta_nabla_b, delta_nabla_w = self.backprop(x, y)     ## 對於一個訓練數據(x,y)計算loss相對於所有參數的偏導數
        ## 因此delta_nabla_b和self.biases, nabla_b是一樣大小(shape)
        ## 同樣delta_nabla_w和self.weights,nabla_w一樣大小
        nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]        ## 把bias的梯度累加到nabla_b裏
        nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]        ## 把weight的梯度累加到nable_w裏
    self.weights = [w-(eta/len(mini_batch))*nw                    for w, nw in zip(self.weights, nabla_w)]     ## 使用這個batch的梯度和eta(步長)更新參數weights
    self.biases = [b-(eta/len(mini_batch))*nb                   for b, nb in zip(self.biases, nabla_b)]        ## 更新biases
        ## 這裏更新參數是除了batch的大小(10),有的人實現時不除,其實沒有什麼區別,因爲超參數eta會有所不同,如果不除,那麼eta相當於是0.3(在eta那裏就除了batch的大小了)。

3.6 backprop函數

這個函數就是求loss相對於所有參數的偏導數,這裏先不仔細講解,等下次我們學習梯度的求解方法我們再回來討論,這裏可以先了解一下這個函數的輸入和輸出,把它當成一個黑盒就行,其實它的代碼也很少,但是如果不知道梯度的公式,也很難明白。

def backprop(self, x, y):
nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]    # feedforwardactivation = x
    activations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):
        z = np.dot(w, activation)+b
        zs.append(z)
        activation = sigmoid(z)
        activations.append(activation)    # backward passdelta = self.cost_derivative(activations[-1], y) * \
        sigmoid_prime(zs[-1])
    nabla_b[-1] = delta
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())
for l in xrange(2, self.num_layers):        z = zs[-l]
        sp = sigmoid_prime(z)
        delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
        nabla_b[-l] = delta
        nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
    return (nabla_b, nabla_w)

它的輸入就是一個訓練樣本(x,y)分別是784*1和10*1。輸出就是和self.biases,self.weights一樣大小的列表,然後列表中的每一個數組的大小也是一樣。具體到上面的例子,輸出nabla_b包含兩個矩陣,大小分別是30*1和10*1;nabla_w也包含兩個矩陣,大小分別是30*784和10*30。

從Image Caption Generation理解深度學習(part III)

 環信作爲國內唯一擁有從雲通訊到服務雲,再到機器人的完整一體化產品技術儲備的公司,目前在積極籌備關於深度學習系列的公開課,本系列公開課文章面向深度學習研發者,共計14篇,系統講解了深度學習的基本知識及實踐,以Image Caption Generation爲切入點,逐步介紹自動梯度求解、卷積神經網絡(CNN)、遞歸神經網絡(RNN)等知識點。

 

  本篇文章( part I、part II、part III)希望通過Image Caption Generation,一個有意思的具體任務,來介紹深度學習的知識,涉及到很多深度學習流行的模型,如CNN,RNN/LSTM,Attention等。作者李理,畢業於北京大學,就職於環信,在環信從事智能客服和智能機器人相關工作,致力於用深度學習來提高智能機器人的性能。

 

2.2.5 反向傳播算法的推導

前面我們用很簡單的幾十行python代碼基本上完成了一個多層神經網絡。但是還差最重要的部分,那就是計算loss function對參數的偏導數,也就是反向傳播算法。下面我們來仔細的完成公式的推導,以及接下來會講怎麼用代碼來實現。這一部分數學公式多一些,可能很多讀者會希望跳過去,不過我還是建議大家仔細的閱讀,其實神經網絡用到的數學相比svm,bayes network等機器學習算法,已經非常簡單了。請讀者閱讀的時候最好準備一支筆和幾張白紙,每一個公式都能推導一下。如果堅持下來,你會覺得其實挺簡單的。

(1) feedforward階段的矩陣參數表示和計算

之前我們討論的是一個神經元的計算,而在代碼裏用到的卻是矩陣向量乘法。而且細心的讀者會發現我們在構造參數矩陣weights的時候,行數和列數分別是後一層的節點數和前一層的節點數。這似乎有點不自然,爲什麼不反過來呢?看過下面這一部分就會明白了。

圖片描述

首先我們熟悉一下第L(因爲小寫的L和1太像,所以我用大寫的L)層的參數w_jk。它表示第L-1層的第k個神經元到第L層的第j個神經元的權重。比如第3層的w_24,參考上面的圖,它表示的是第2層的第4個神經元到第3層的第二個神經元。

對bias和激活函數後的結果a也採用類似的記號,如下圖所示。

圖片描述

b_32表示第2層的第3個神經元的bias,而a_13第3層的第1個神經元的激活。

使用上面的記號,我們就可以計算第L層的第j個神經元的輸出a_jl

圖片描述

第L層的第j個神經元的輸入是L-1層的a_1,a_2,...;對應的權值是w_j1,w_j2,...;bias是b_jL。所以a_jL就是上面的公式,k的範圍是從1到第L-1層的神經元的個數。 
爲了用矩陣向量乘法來一次計算第L層的所有神經元的輸出,我們需要定義第L層的參數矩陣w_l,它的大小是m*n,其中m是第L層的神經元個數;而n則是第L-1層的個數。它的第i行第j列就是我們上面定義的w_jk。此外我們還要定義向量b_l,它的大小是m(也就是第L層神經元的個數),它的第j個元素就是我們上面定義的b_j。 
最後,我們定義element-wise的函數,比如f(x) = x^2,如果輸入是一個向量,那麼結果是和輸入一樣大小的向量,它的每個元素是對輸入向量的每一個元素應用這個函數的結果。

圖片描述

有了上面的定義,我們就可以一次計算出第L層的輸出(一個長度爲m的向量)

圖片描述

下面是對上面這個公式的詳細證明(說明): 
我們需要證明的是向量aL的第j個元素就是前面的a_jL

圖片描述

此外,爲了方便後面的求解,我們把加權累加和也用一個符號z_l來表示。

圖片描述

其中,它的第j個元素就是第L層的第j個神經元的加權累加和:

圖片描述

這樣a_l就可以簡單的對z_l的每個元素計算激活函數

圖片描述

現在我們再回顧一下feedforward的代碼就非常直觀了:

def feedforward(self, a):"""Return the output of the network if a is input."""for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)return a

傳給函數feedforward的參數a就是輸入向量x,第一層就是x,第二層就是第一個隱層,每一層的計算就是非常簡單的參數矩陣w_l乘以上一層的激活a_l-1在加上b_l,然後用激活函數計算。

初始化的時候w的大小是 (後一層的神經元個數) * (前一層的神經元個數),再回顧一下初始化參數的代碼:

# sizes = [784, 30, 10]def __init__(self, sizes):self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)for x, y in zip(sizes[:-1], sizes[1:])]

x, y in zip(sizes[:-1], sizes[1:]) x是第一層到最後倒數第二層,y是第二層到最後一層,比如上面的sizes=[784, 30, 10] 
x是[784, 30], y是[30, 10],注意隨機的矩陣是(y,x),所以self.weights是兩個矩陣,大小分別是30*78410*30

(2) 關於損失函數C的兩個假設 
1. 損失函數是每個訓練數據的損失的平均 
也就是C是這樣的形式:

圖片描述

對於之前我們使用的MSE損失函數,這是滿足的。我們使用batch的梯度下降的時候需要求C對參數w的偏導數,因爲損失函數是每個訓練數據的損失的平均,所以我們只需要求每個數據的偏導數,然後加起來平均就行。這個假設幾乎所有的損失函數都是滿足的【我是沒見過損失函數不滿足這個條件】

  1. 損失函數是最後一層輸出的函數

圖片描述

這個條件幾乎常見的損失函數都是這樣的,我們之前時候的MSE就是計算最後一層的輸出aL和正確的y(one-hot)的均方誤差,顯然是滿足的。

(3) Hadamard product 
這個名字看起來很複雜,其實很簡單,就是兩個向量elementwise的乘法。看一個例子就清楚了:

圖片描述

(4) 反向傳播算法(back propagation)的4個公式

回顧一下,我們之前說了,梯度下降其實最核心的問題就是求損失函數對每一個參數的偏導數。那我們就直接一個一個求好了,爲什麼又要搞出一個反向傳播算法呢?其實這個算法在不同的領域被不同的人重複“發現”過很多次,有過很多不同的名字,最本質的應該就是逆向求導(reverse-mode differentiation)或者叫做自動求導(automatic differentiation)。自動求導(AD)是非常通用的一種求偏導數的方法,很早就在流體力學和大氣物理等領域使用,反向傳播算法可以認爲是AD在神經網絡中的應用。不過最早發現這個算法的人(是誰最早好像還有點爭議)並不是先知道AD可以直接用於神經網絡,他發現這個算法是基於錯誤的反向傳播而得到的,所有命名爲(錯誤的)反向傳播算法。後面我們會講到AD,這是一個強大的算法,任何一個函數,你能把它分解成有向無環圖的計算圖【函數一般都能分解成一些無依賴的最基礎的變量的複合函數,因此肯定可以表示成這樣一個有向無環圖】,然後每個節點都表示一個函數。只要你能求出這個函數在特定點的梯度【也就是這個函數對所以自變量的偏導數】(不需要求解析的偏導數,當然很多情況,這些函數都是能直接求出解析解,然後代入這個特定點就行,但理論上我們是可以用其他方法,比如數值梯度近似來求的),就能自動的計算損失函數對每一個參數的偏導數(也是在這個點的),而且只要反向根據拓撲排序遍歷這個圖一次就行,非常高效和簡單。後面我們會詳細的介紹AD。這個方法非常通用,TensorFlow的核心就是AD。使用AD的框架就比較靈活,我想“創造”一種新的網絡結構,我又不想【其實更可能是不會】推導出梯度的公式,那麼我只需要把我的網絡能用這樣一個有向無環圖表示就行。當然節點必須要能夠求出梯度來,一般我們的函數比如矩陣的運算,卷積等等TensorFlow都封裝好了——它把它叫做一個op。我們只需要搭積木一樣把這個計算圖定義出來,TensorFlow就自動的能根據AD計算出損失函數對所有參數的梯度來了。當然如果你要用到一個TensorFlow沒有的op,那你就需要根據它的規範實現這個op,一個op最核心的接口就是兩個,一個是輸入x,求f(x);另一個就是求f在某個x0點的梯度。

不過這裏,我們還是沿着神經網絡的發展歷史,從錯誤的反向傳播角度來理解和推導這個算法。

首先,我們會對每一個神經元比如第L層的第j個,都定義一個錯誤δ_jL

圖片描述

也就是損失函數對z也就是線性累加和的偏導數。爲什麼定義這樣一個東西呢?我們假設在第L層的第j個神經元上有一個精靈(Daemon)

圖片描述

當這個神經元得到來自上一次的輸入累加計算出z_jL的時候,它會惡作劇的給一點很小的干擾Δz_jL。原來它應該輸出的是σ(z_jL),現在變成了σ(z_jL +Δz_jL)。這個微小的變化逐層傳播,最終導致損失函數C也發生如下的變化:

圖片描述

這個其實就是導數的直覺定義:微小的Δx引起微小的Δy,Δy/Δx約等於導數。 
不過這個精靈是個好精靈,它想幫助我們減少損失。 當

圖片描述

大於0的時候,它讓Δz_jL小於0,反之當它小於0的時候它讓Δz_jL大於0。這樣

圖片描述

總是小於0 
因此我們的loss就會變小。而其絕對值越大,我們的損失減少的越多。 
當然你會說爲什麼不能讓Δz_jL非常大,這樣我們的損失總是減少很多?可惜這個精靈是個數學家,它說如果Δx太大,那麼Δy=df/dx *Δx就不準確了。

所以我們可以這樣認爲:它就是第L層的第j個神經元“引起”的“錯誤”。如果絕對值大,則它的“責任”也大,它就得多做出一些調整;反之如果它趨近於0,說明它沒有什麼“責任”,也就不需要做出什麼改變。 
因此通過上面的啓發,我們定義出δ_jL來。

圖片描述

接下來我們逐個介紹反向傳播算法的4個公式。

公式1. 第L層(最後一層) 的錯誤

圖片描述

這個公式的第一項,就是損失C對a_jL的導數,它越大,說明C受a_jL的影響也就越大,如果有了錯誤,第a_jL的“責任”也就越大,錯誤也就越大。第二項是a_jLz_jL的影響。兩者乘起來就是z_jL對最終損失的影響,也就是它的“責任”的大小。

這個公式很好計算,首先第二項就是把z_jL的值(這個在feedforward節點就算出來並存儲下來了)代入σ'(x)。如果σ是sigmoid函數,我們前面也推導過它的導數:σ’(x)=σ(x)*(1-σ(x))。第一項當然依賴於損失函數的定義,一般也很好求。比如我們的MSE損失:

圖片描述
圖片描述

具體的推導我在紙上寫了一下,雖然很簡單,我們也可以練練手,尤其是對於求和公式的展開,希望大家能熟悉它,以後的推導我可能就不展開求和公式了,你需要知道求和公式裏哪些項是和外面的自變量無關的。

圖片描述

公式BP1是elementwise的,我們需要變量j來計算每一個δ_jL。我們也可以把它寫成向量的形式,以方便利用線性代數庫,它們可以一次計算向量或者矩陣,可以用很多技術利用硬件特性來優化(包括GPU,SSE等)速度。

圖片描述

右邊δ'(z_L)很容易理解,左邊的記號可能有些費解,其實我們把∇aC當成一個整體就好了,它是一個向量,第一個元素是∂C/∂a_1L,第二個就是∂C/∂a_2L,… 
如果算上函數C是MSE的話,上面的公式就可以簡化成:

圖片描述

公式2. 第l層(非最後一層) 的錯誤

圖片描述

等下我們會證明這個公式,不過首先我們來熟悉一下公式。如果我們想“背”下這個公式的話,似乎看起來比第一個BP1要複雜很多 。我們先檢查一下矩陣和向量的維度,假設l+1層有m個元素,l層n個。則w_l+1的大小是m*n,轉置之後是n*m, δ_l+1的大小是n*1,所以矩陣相乘後是m*1,這和δ_l是一樣的,沒有問題。

接下來我們仔細觀察一下BP2這個公式,首先第二項σ'(z_l)和前面的含義一樣,代表a_l對於z_l的變化率。 
而第一項複雜一點,我們知道第l層的第j個神經元會影響第l+1層的所有神經元,從而也影響最終的損失C。這個公式直接給了一個矩陣向量的形式,看起來不清楚,所以我在草稿紙上展開了:

圖片描述

最終第L層的第j個神經元的損失就是如下公式:

圖片描述

這下應該就比較清楚了,第l層的第j個神經元的損失,就是把l+1層的損失“反向傳播”回來,當然要帶上權重,權重越大,“責任”也就越大。

如果要“背”出這個公式也沒有那麼複雜了,先不看σ'(z_l),第一項應該是矩陣w_l+1乘以δ_l+1。由於矩陣是m*n,而 
向量δ_l+1是m*1,爲了能讓矩陣乘法成立,那麼就只能把w轉置一下,變成n*m,然後就很容易記住這個公式了。

注意,BP2的計算是從後往前的,首先根據BP1,最後一層的δ_L我們已經算出來了,因此可以向前計算L-1層的δ_L-1, 
有了δ_L-1就能計算δ_L-2,…,最終能算出第一個隱層(也就是第2層)δ_1來。

公式3. 損失函數對偏置b的梯度

這前面費了大力氣求δ_l,不要忘了我們的最終目標是求損失函數對參數w和b的偏導數,而不是求對中間變量z的偏導數。

因此這個公式就是對b的偏導數。

圖片描述

或者寫成向量的形式:

圖片描述

∂C/∂b就是δ!

公式4. 損失函數對w的梯度

圖片描述

或者參考下圖寫成好記的形式:

圖片描述
圖片描述

也就是說對於一條邊w_jkL∂C/∂w_ij就是這條邊射出的點的錯誤δ乘以進入點的激活。非常好記。

我們把這四個公式再總結一下:

圖片描述

(5) 這四個公式的證明

首先是BP1,請參考下圖:

圖片描述

然後是BP2: 
這裏用到了chain rule,其實也非常簡單和直觀,就是複合函數層層組合。最簡單的方法就是用圖畫出來,比如y最終 
是x的函數,我們要求∂y/∂x,如果y是u,v的函數,然後u,v纔是x的函數,那麼我們把變量x,y,u,v都畫成圖上的點,y是u,v的函數,那麼我們畫上從u和v到y的邊,同樣,我們畫上從x到u和v的邊,然後從y到x的每條路徑,我們經過的邊都是一個偏導數,我們把它累加起來就行【這其實就是後面我們會講的AD】。因此∂y/∂x=∂y/∂u * ∂u/∂x +∂y/∂v * ∂v/∂x。

圖片描述

剩下的BP3和BP4也非常類似,我就不證明了。

反向傳播算法 
1. a_1 = 輸入向量x 
2. Feedforward 根據公式

圖片描述

圖片描述

計算z_la_l並存儲下來(反向傳播時要用的) 
3. 計算最後一層的錯誤

圖片描述

  1. 向前計算所有層的錯誤

圖片描述

  1. 計算損失對所有參數的偏導數

圖片描述

圖片描述

2.2.6 代碼實現反向傳播算法

我們已經把公式推導出來了,那怎麼用代碼實現呢?我們先把代碼複製一下,然後說明部分都是作爲代碼的註釋了, 
請仔細閱讀。

class Network(object):
  def update_mini_batch(self, mini_batch, eta):
    # mini_batch是batch大小,eta是learning rate

    nabla_b = [np.zeros(b.shape) for b in self.biases]    # 構造和self.biases一樣大小的向量,比如前面的例子 sizes=[784,30,10],則
    # nabla_b是兩個向量,大小分別是30和10

    nabla_w = [np.zeros(w.shape) for w in self.weights]    # 構造和self.weights一樣大小的矩陣,比如前面的例子 sizes=[784,30,10],則
    # nabla_w是兩個矩陣,大小分別是30*784和10*30

    for x, y in mini_batch: #對於每個訓練樣本x和y
 delta_nabla_b, delta_nabla_w = self.backprop(x, y) # 用backprop函數計算損失函數對每一個參數的偏導數。
 # backprop函數下面會詳細講解

 nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] # 把返回的對b偏導數累加到nabla_b中

 nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] # 把返回的對w的偏導數累加到nabla_w中

    self.weights = [w-(eta/len(mini_batch))*nw  for w, nw in zip(self.weights, nabla_w)]    # 計算完一個batch後更新參數w

    self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]    # 更新b... def backprop(self, x, y):
    # 輸入是x和y,返回損失函數C對每個參數w和b的偏導數
    # 返回的格式是兩個元組,第一個是b的偏導數,第二個是w的。
    nabla_b = [np.zeros(b.shape) for b in self.biases]    # 構造和self.biases一樣大小的向量,比如前面的例子 sizes=[784,30,10],則
    # nabla_b是兩個向量,大小分別是30和10

    nabla_w = [np.zeros(w.shape) for w in self.weights]    # 構造和self.weights一樣大小的矩陣,比如前面的例子 sizes=[784,30,10],則
    # nabla_w是兩個矩陣,大小分別是30*784和10*30

    # feedforward
    activation = x
    activations = [x] # 用一個list保存所有層的激活,下面backward會有用的
    zs = [] # 同樣的用一個list保存所有層的加權累加和z,下面也會用到。

    #下面這段代碼在feedward也有,不過那裏是用來predict用的不需要保存zs和activations
    for b, w in zip(self.biases, self.weights):
 z = np.dot(w, activation)+b
 zs.append(z)
 activation = sigmoid(z)
 activations.append(activation)   # backward pass
  #1. 首先計算最後一層的錯誤delta,根據公式BP1,它是損失函數對a_L的梯度乘以σ'(z_L)
    #  sigmoid_prime就是σ'(z_L),而∂C/∂a_L就是函數cost_derivative,對於MSE的損失函數,
    #  它就是最後一層的激活activations[-1] - y
    delta = self.cost_derivative(activations[-1], y) * \
 sigmoid_prime(zs[-1])    # 2. 根據公式BP3,損失對b的偏導數就是delta
    nabla_b[-1] = delta    # 3. 根據公式BP4,損失對w的偏導數時delta_out * activation_in
    #  注意,我們的公式BP4是elementwise的,我們需要寫成矩陣向量的形式
    #  那怎麼寫呢?我們只需要關心矩陣的大小就行了。
    #  假設最後一層有m(10)個神經元,前一層有n(30)個,
    #  則delta是10*1, 倒數第二層的激活activations[-2]是30*1
    #  我們想求的最後一層的參數nabla_w[-1]是10*30,那麼爲了能夠正確的矩陣乘法,
    #  只要一種可能就是 delta 乘以 activations[-2]的轉置,其實也就是向量delta和activations[-2]的外積
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())    # 接下來從倒數第二層一直往前計算delta,同時也把對w和b的偏導數求出來。
    # 這裏用到一個比較小的trick就是python的下標是支持負數的,-1表示最後一個元素,-2是倒數第二個
    # l表示倒數第l層,2就表示倒數第2層,num_layers - 1就表示順數第2層(也就是第1個隱層)
    # 比如我們的例子:sizes=[784, 30, 10],那麼l就是從2到3(不包含3),l就只能是2,頁就是第1個(也是唯一的一
    # 個)隱層   
    for l in xrange(2, self.num_layers): # 倒數第l層的z
 z = zs[-l] # 計算σ'(z_l)
 sp = sigmoid_prime(z) # 根據BP2,計算delta_l,注意weights[-l+1]表示倒數第l層的下一層
 delta = np.dot(self.weights[-l+1].transpose(), delta) * sp # 同上,根據BP3
 nabla_b[-l] = delta # BP4,矩陣乘法參考前面的說明
 nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())    return (nabla_b, nabla_w)

2.2.7 爲什麼反向傳播算法是一個高效的算法?

分析完代碼,我們發現一次backprop函數調用需要feedforward一次,網絡有多少邊,就有多少次乘法,有多少個點就有多少次加分和激活函數計算(不算第一層輸入層)。反向計算也是一樣,不過是從後往前。也就是說這是時間複雜度爲O(n)的算法。 
如果我們不用反向傳播算法,假設我們用梯度的定義計算數值梯度。對於每一個參數wj,

我們都用公式 limit (f(w1, w2, …, wj+Δ wj, …) - f(w1, w2, …, wj, …)/Δwj

f(w1, w2, wj, …)只需要feedforward一次,但是對於每個參數wj,都需要feedforward一層來計算f(w1, w2, …, wj+Δ wj, …),它的時間複雜度是O(n),那麼對所有的參數的計算需要O(n^2)的時間複雜度。

假設神經網絡有1百萬個參數,那麼每次需要10^12這個數量級的運算,而反向傳播算法只需要10^6,因此這個方法比反向傳播算法要慢1百萬倍。

 

自動梯度求解 反向傳播算法的另外一種視角

前面我們講過了反向傳播算法的詳細推導過程,大家可能會覺得有些複雜。事實上其實就是鏈式求導法則的應用。今天我們將會繼續討論這個問題,不過是從Computational Graphs的角度,也就是我們之前說過的自動求導(Automatic Differentiation or Reverse-mode Differentiation)。並且通過CS231n的Assignment2來學習使用這種方法,通過這種方法來實現一個多層的神經網絡。

Calculus on Computational Graphs: Backpropagation

首先我們介紹一篇博客文章: https://colah.github.io/posts/2015-08-Backprop/ 基本是翻譯過來,不過部分地方是我自己的理解,建議讀者結合這篇文章一起閱讀。

簡介

反向傳播算法是神經網絡的核心算法,不過這個算法在不同的領域被多次”發現“過,因此有不同的名稱。

計算圖(Computational Graphs)

考慮一個簡單的函數 e=(a+b)∗(b+1)e=(a+b)∗(b+1) 。這個函數有兩個操作(函數),加法和乘法。爲了指代方便,我們引入兩個中間變量,c和d。

  • c=a+b

  • d=b+1

  • e=c∗d

下面我們把它畫成一個計算圖,每一個操作是圖中一個節點,最基本的變量a和b也是一個節點。每個節點和它的輸入變量直接有一條邊。比如d的輸入變量是b,那麼d和b直接就有一條邊。

任何一個顯示定義的函數(隱函數不行,不過我們定義的神經網絡肯定不會通過隱函數來定義)都可以分解爲一個有向無環圖(樹),其中葉子節點是最基本的無依賴的自變量,而中間節點是我們引入的中間變量,而樹根就是我們的函數。比如上面的例子,計算圖如下所示:

圖片描述

給定每一個自變量的值,我們可以計算最終的函數值,對應與神經網絡就是feedforward計算。具體用”算法“怎麼計算呢?首先因爲計算圖是一個有向無環圖,因此我們可以拓撲排序,先是葉子節點a和b,他們的值已經給定,然後刪除a和b出發的邊,然後c和d沒有任何未知依賴,可以計算,最後計算e。計算過程如下圖:

圖片描述

計算圖的導數計算

首先我們可以計算每條邊上的導數,也就是邊的終點對起點的導數,而且導數是在起點的取前向計算值時的導數,具體過程如圖所示:

圖片描述

有些邊的導數不依賴於輸入的值,比如:

圖片描述

但是還有很多邊的導數是依賴於輸入值的,比如:

圖片描述

因爲在“前向”計算的過程中,每個節點的值都計算出來了,所以邊的計算很簡單,也不需要按照什麼的順序。

不過我們一般比較感興趣的是最終函數對某個自變量的導數,比如

圖片描述

根據鏈式法則,只要找到這兩個節點的所有路徑,然後把路徑的邊乘起來就得到這條邊的值,然後把所有邊加起來就可以了。

比如上面的例子b到e有兩條路徑:b->c->e和b->d->e,所以

圖片描述

如果用“鏈式”法則來寫就是

圖片描述

路徑反過來而已。

使用上面的方法,我們可以計算任何一個點(上面的變量)對另外一個點(上面的變量)的導數。不過我們一般的情況是計算樹根對所有葉子的導數,當然我們可以使用上面的算法一個一個計算,但是這樣會有很多重複的計算。

比如a->e的路徑是 a->c->e,b->e有一條邊是b->c->e,其中c->e是重複的【這個例子不太好,我們可以想像c->e是一條很長的路徑】,每次都重複計算c->e這個“子”路徑是多餘的。我們可以從後往前計算,也就是每個節點都是存放樹根變量(這個例子是e)對當前節點的導數(其實也就是樹根到當前節點的所有路徑的和)。

反向導數計算

圖片描述

計算流程文字描述如下: 
首先還是對這個圖進行拓撲排序,不過是反過來。 
首先是

圖片描述

這個沒什麼好說的。 
然後計算

圖片描述

然後計算

圖片描述

然後計算

圖片描述

計算

圖片描述

前嚮導數計算

如果我們需要計算每一個變量對某一個變量的導數,就可以使用前向計算的方法。不過我們的神經網絡都是相反——計算某個一個變量(一般是損失函數)對所有變量的導數,所以這裏就不詳細介紹了。

 

Optimization

這一部分內容來自:CS231n Convolutional Neural Networks for Visual Recognition

簡介

我們的目標:x是一個向量,f(x)是一個函數,它的輸入是一個向量(或者認爲是多變量的函數,這個輸入向量就是自變量),輸出是一個實數值。我們需要計算的是f對每一個自變量的導數,然後把它們排成一個向量,也就是梯度。

圖片描述

爲什麼要求這個呢?前面我們也講了,我們的神經網絡的損失函數最終可以看成是權重weights和bias的函數,我們的目標就是調整這些參數,使得損失函數最小。

簡單的表達式和梯度的解釋

首先我們看一個很簡單的函數 f(x,y)=xy,求f對x和y的偏導數很簡單:

圖片描述

首先來看導數的定義:

圖片描述

函數在某個點的導數就是函數曲線在這個點的斜率,也就是f(x)隨x的變化率。 
比如上面的例子,當x=4,y=−3時 f(x,y)=−12,f對x的偏導數

圖片描述

也就是說,如果我們固定y=4,然後給x一個很小的變化h,那麼f(x,y)的變化大約是-3*h。 
因此乘法的梯度就是

圖片描述

同樣,加法的梯度更簡單:

圖片描述

最後一個簡單函數是max函數:

圖片描述

這個導數是ReLU(x)=max(x,0)的導數,其實也簡單,如果 x>=y,那麼 max(x,y)=x,則導數是1,否則 max(x,y)=0,那麼對x求導就是0。

複雜表達式的鏈式法則

接下來看一個稍微複雜一點的函數 f(x,y,z)=(x+y)z。我們引入一箇中間變量q,f=qz,q=x+y,我們可以使用鏈式法則求f對x和y的導數。

圖片描述

對y的求導也是類似的。

下面是用python代碼來求f對x和y的導數在某一個點的值。

# 設置自變量的值x = -2; y = 5; z = -4# “前向”計算fq = x + y # q becomes 3f = q * z # f becomes -12# 從“後”往前“反向”計算# 首先是 f = q * zdfdz = q # 因爲df/dz = q, 所以f對z的梯度是 3dfdq = z # 因爲df/dq = z, 所以f對q的梯度是 -4# 然後 q = x + ydfdx = 1.0 * dfdq # 因爲dq/dx = 1,所以使用鏈式法則計算dfdx=-4dfdy = 1.0 * dfdq # 因爲dq/dy = 1,所以使用鏈式法則計算dfdy=-4

我們也可以用計算圖來表示和計算:

圖片描述

綠色的值是feed forward的結果,而紅色的值是backprop的結果。

不過我覺得cs231n課程的這個圖沒有上面blog的清晰,原因是雖然它標示出來了最終的梯度,但是沒有標示出local gradient,我在下面會畫出完整的計算過程。

反向傳播算法的直覺解釋

我們如果把計算圖的每一個點看成一個“門”(或者一個模塊),或者說一個函數。它有一個輸入(向量),也有一個輸出(標量)。對於一個門來說有兩個計算,首先是根據輸入,計算輸出,這個一般很容易。還有一種計算就是求輸出對每一個輸入的偏導數,或者說輸出對輸入向量的”局部“梯度(local gradient)。一個複雜計算圖(神經網絡)的計算首先就是前向計算,然後反向計算,反向計算公式可能看起來很複雜,但是如果在計算圖上其實就是簡單的用local gradient乘以從後面傳過來的gradient,然後加起來。

Sigmoid模塊的例子

接下來我們看一個更復雜的例子:

圖片描述

這個函數是一個比較複雜的複合函數,但是構成它的基本函數是如下4個簡單函數:

圖片描述

下面是用計算圖畫出這個計算過程:

圖片描述

這個圖有4種gate,加法,乘法,指數和倒數。加法有加一個常數和兩個變量相加,乘法也是一樣。

上圖綠色的值是前向計算的結果,而紅色的值是反向計算的結果,local graident並沒有標示出來,所以看起來可能有些跳躍,下面我在紙上詳細的分解了其中的步驟,請讀者跟着下圖自己動手計算一遍。

圖片描述

上圖就是前向計算的過程,比較簡單。

圖片描述

第二個圖是計算local gradient,對於兩個輸入的乘法和加法,local gradient也是兩個值,local gradient的值我是放到圖的節點上了。

圖片描述

第三個圖是具體計算一個乘法的local gradient的過程,因爲上圖可能看不清,所以單獨放大了這一步。

圖片描述

最後計算真正的梯度,是把local gradient乘以來自上一步的gradient。不過這個例子一個節點只有一個輸出,如果有多個的話,梯度是加起來的,可以參考1.4的

圖片描述

上面我們看到把

圖片描述

分解成最基本的加法,乘法,導數和指數函數,但是我們也可以不分解這麼細。之前我們也學習過了sigmoid函數,那麼我們可以這樣分解:

圖片描述

σ(x)σ(x) 的導數我們之前已經推導過一次了,這裏再列一下:

圖片描述

因此我們可以把後面一長串的gate”壓縮“成一個gate:

圖片描述

我們來比較一下,之前前向計算 σ(x)σ(x) 需要一次乘法,一次exp,一次加法導數;而反向計算需要分別計算這4個gate的導數。

而壓縮後前向計算是一樣的,但是反向計算可以”利用“前向計算的結果

圖片描述

這只需要一次減法和一次乘法!當然如果不能利用前向的結果,我們如果需要重新計算 σ(x)σ(x) ,那麼壓縮其實沒有什麼用處。能壓縮的原因在於σ函數導數的特殊形式。而神經網絡的關鍵問題是在訓練,訓練性能就取決於這些細節。如果是我們自己來實現反向傳播算法,我們就需要利用這樣的特性。而如果是使用工具,那麼就依賴於工具的優化水平了。

下面我們用代碼來實現一下:

w = [2,-3,-3] # assume some random weights and datax = [-1, -2]# forward passdot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function# backward pass through the neuron (backpropagation)ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivationdx = [w[0] * ddot, w[1] * ddot] # backprop into xdw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w# we're done! we have the gradients on the inputs to the circuit

上面的例子用了一個小技巧,就是所謂的staged backpropagation,說白了就是給中間的計算節點起一個名字。比如dot。爲了讓大家熟悉這種技巧,下面有一個例子。

Staged computation練習

圖片描述

我們用代碼來計算這個函數對x和y的梯度在某一點的值

前向計算

x = 3 # example valuesy = -4# forward passsigy = 1.0 / (1 + math.exp(-y)) # 分子上的sigmoid   #(1)num = x + sigy # 分子                               #(2)sigx = 1.0 / (1 + math.exp(-x)) # 分母上的sigmoid #(3)xpy = x + y                                              #(4)xpysqr = xpy**2                                          #(5)den = sigx + xpysqr # 分母                        #(6)invden = 1.0 / den                                       #(7)f = num * invden # done!                                 #(8)

反向計算

# backprop f = num * invdendnum = invden # gradient on numerator                             #(8)dinvden = num                                                     #(8)# backprop invden = 1.0 / den dden = (-1.0 / (den**2)) * dinvden                                #(7)# backprop den = sigx + xpysqrdsigx = (1) * dden                                                #(6)dxpysqr = (1) * dden                                              #(6)# backprop xpysqr = xpy**2dxpy = (2 * xpy) * dxpysqr                                        #(5)# backprop xpy = x + ydx = (1) * dxpy                                                   #(4)dy = (1) * dxpy                                                   #(4)# backprop sigx = 1.0 / (1 + math.exp(-x))dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)# backprop num = x + sigydx += (1) * dnum                                                  #(2)dsigy = (1) * dnum                                                #(2)# backprop sigy = 1.0 / (1 + math.exp(-y))dy += ((1 - sigy) * sigy) * dsigy                                 #(1)# done! phew

需要注意的兩點:1. 前向的結果都要保存下來,反向的時候要用的。2. 如果某個變量有多個出去的邊,第一次是等於,第二次就是+=,因爲我們要把不同出去點的梯度加起來。

下面我們來逐行分析反向計算: 
(8) f = num * invden 
local gradient

圖片描述

而上面傳過來的梯度是1,所以 dnum=1∗invden。注意變量的命名規則, df/dnum就命名爲dnum【省略了df,因爲默認我們是求f對所有變量的偏導數】 
同理: dinvden=num

(7) invden = 1.0 / den

local gradient是 (−1.0/(den∗∗2)) ,然後乘以上面來的dinvden

(6) den = sigx + xpysqr

這個函數有兩個變量sigx和xpysqr,所以需要計算兩個local梯度,然後乘以dden 
加法的local梯度是1,所以就是(1)*dden

(5) xpysqr = xpy**2

local gradient是2*xpy,再乘以dxpysqr

(4) xpy = x + y

還是一個加法,local gradient是1,所以dx和dy都是dxpy乘1

(3) sigx = 1.0 / (1 + math.exp(-x))

這是sigmoid函數,local gradient是 (1-sigx)*sigx,再乘以dsigx。 
不過需要注意的是這是dx的第二次出現,所以是+=,表示來自不同路徑反向傳播過來給x的梯度值

(2) num = x + sigy

還是個很簡單的加法,local gradient是1。需要注意的是dx是+=,理由同上。

(1) sigy = 1.0 / (1 + math.exp(-y))

最後是sigmoid(y)和前面(3)一樣的。

請仔細閱讀上面反向計算的每一步代碼,確保自己理解了之後再往下閱讀。

梯度的矩陣運算

前面都是對一個標量的計算,在實際實現時用矩陣運算一次計算一層的所有梯度會更加高效。因爲矩陣乘以向量和向量乘以向量都可以看出矩陣乘以矩陣的特殊形式,所以下面我們介紹矩陣乘法怎麼求梯度。

首先我們得定義什麼叫矩陣對矩陣的梯度!

我查閱了很多資料,也沒找到哪裏有矩陣對矩陣的梯度的定義,如果哪位讀者知道,請告訴我,謝謝!唯一比較接近的是Andrew Ng的課程cs294的背景知識介紹的slides linalg的4.1節定義了gradient of Matrix,關於矩陣對矩陣的梯度我會有一個猜測性的解釋,可能會有問題。

首先介紹graident of matrix

假設 f:Rm×n→R是一個函數,輸入是一個m×n的實數值矩陣,輸出是一個實數。那麼f對A的梯度是如下定義的:

圖片描述

看起來定義很複雜?其實很簡單,我們把f看成一個mn個自變量的函數,因此我們可以求f對這mn個自變量的偏導數,然後把它們排列成m*n的矩陣就行了。爲什麼要多此一舉把變量拍成矩陣把他們的偏導數也排成矩陣?想想我們之前的神經網絡的weights矩陣,這是很自然的定義,同時我們需要計算loss對weights矩陣的每一個變量的偏導數,寫出這樣的形式計算起來比較方便。

那麼什麼是矩陣對矩陣的梯度呢?我們先看實際神經網絡的一個計算情況。對於全連接的神經網絡,我們有一個矩陣乘以向量 D=WxD=Wx 【我們這裏把向量x看成矩陣】。現在我們需要計算loss對某一個 WijWij 的偏導數,根據我們之前的計算圖, WijWij 有多少條出邊,那麼就有多少個要累加的梯度乘以local梯度。 
假設W是m×n的矩陣,x是n×p的矩陣,則D是m×p的矩陣

圖片描述

根據矩陣乘法的定義

圖片描述

我們可以計算:

圖片描述

請仔細理解上面這一步,如果 k≠i,則不論s是什麼,Wks跟Wij不是同一個變量,所以導數就是0;如果k=i,∑sWisxsl=xjl,也就求和的下標s取j的時候有WijWij。 
因此

圖片描述

上面計算了loss對一個Wij的偏導數,如果把它寫成矩陣形式就是:

圖片描述

前面我們推導出了對Wij的偏導數的計算公式,下面我們把它寫成矩陣乘法的形式並驗證【證明】它。

圖片描述

爲什麼可以寫成這樣的形式呢?

圖片描述

上面的推導似乎很複雜,但是我們只要能記住就行,記法也很簡單——把矩陣都變成最特殊的1 1的矩陣(也就是標量,一個實數)。D=w x,這個導數很容易吧,對w求導就是local gradient x,然後乘以得到dW=dD x;同理dx=dD W。 
但是等等,剛纔那個公式裏還有矩陣的轉置,這個怎麼記?這裏有一個小技巧,就是矩陣乘法的條件,兩個矩陣能相乘他們的大小必須匹配,比如D=Wx,W是m n,x是n p,也就是第二個矩陣的行數等於第一個的列數。 
現在我們已經知道dW是dD”乘以“x了,dW的大小和W一樣是m n,而dD和D一樣是m p,而x是n p,那麼爲了得到一個m n的矩陣,唯一的辦法就是 dD∗xT 
同理dx是n p,dD是m p,W是m*n,唯一的乘法就是 WT∗dD 
下面是用python代碼來演示,numpy的dot就是矩陣乘法,可以用numpy.dot(A,B),也可以直接調用ndarray的dot函數——A.dot(B):

# forward passW = np.random.randn(5, 10)X = np.random.randn(10, 3)
D = W.dot(X)# now suppose we had the gradient on D from above in the circuitdD = np.random.randn(*D.shape) # same shape as DdW = dD.dot(X.T) #.T gives the transpose of the matrixdX = W.T.dot(dD)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章