傳奇NLP攻城獅成長之路(一)

從本期開始,集智AI學園(小仙女)將開始以每周一篇的頻率給大家推送「傳奇NLP攻城獅成長之路」系列教程啦!

與外面那些妖艷的碧池相比,「傳奇NLP攻城獅成長之路」系列教程有什麼特點嘞?

第一,主題明確

現在經常有些公眾號偶爾轉發一篇深度學習教程,但這些單節的小教程即不連貫,也看不到什麼主題,看完感覺自己還是什麼也沒掌握,不知道拿來能幹啥。 而我們此次推出的「傳奇NLP攻城獅成長之路」,全套教程以自然語言處理技術(NLP)為主題,讓你快速掌握深度學習技術在NLP領域的應用,從小白搖身變為NLP攻城獅。

第二,技術最新

全套教程基於Facebook最新發布的PyTorch深度學習框架。曾經有學術界大牛感慨道:我從用Theano開始,用過Tensorflow,最後才發現PyTorch才是真愛!

第三,每周連載,免費!免費!免費!

本系列全套教程都將在集智AI學園公眾號上發布,周周更新,為讀者負責到底,讓你能把真正的技術學習到手!

我們已經為大家準備好了10期的教程內容,在這些教程中不但有項目實例,有時候還會給大家布置點小作業,鍛煉大家用學到的知識解決問題的能力。

  • 本期:PyTorch概要簡析
  • 下期:小試牛刀:編寫一個詞袋分類器
  • 第三期:使用RNN做一個名字分類器
  • 第四期:起名大師:使用RNN生成個好名字
  • 第五期:AI編劇:使用RNN創作莎士比亞劇本
  • 第六期:AI翻譯官:採用注意力機制的翻譯系統
  • 第七期:探索詞向量世界
  • 第八期:詞向量高級:單詞語義編碼器
  • 第九期:長短記憶神經網路(LSTM)序列建模
  • 第十期:體驗PyTorch動態編程,雙向LSTM+CRF

我們打算從PyTorch基礎開始講解,但不會講太多細節。在簡單的介紹一些概念後,我們會快速的切入NLP主題的相關技術。 教程中會提到「反向傳播」、「鏈式求導」、「語言建模」等概念,如果不懂也不用擔心,只要把它們當成一個方法來用就行啦。 反正本系列教程的最終目的就是讓你具備獨立編寫深度學習代碼的能力!

關於PyTorch,有同學可能會問,PyTorch是不是沒有Windows的版本?

Facebook的確沒有推出PyTorch的官方版本,但是這並沒有難倒我們中國網友,知乎上已經有大神找到了在Windows上安裝PyTorch的方法:

zhuanlan.zhihu.com/p/26

或者乾脆不要自己搗鼓軟體環境,直接使用配置好環境的Floyd鏡像,啟動就能跑代碼,免去自己配置環境的麻煩:教程 | Windows用戶指南:如何用Floyd跑PyTorch

好!

話就講到這裡

讓我們開始本期的教程!

import torchimport torch.autograd as autogradimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optimtorch.manual_seed(1)<torch._C.Generator at 0x10c71f810>

如果上面的代碼能正確運行,說明你的PyTorch環境已經可用,下面我們就PyTorch的一些核心元素給大家做一個簡單的介紹。

【本期福利】

在集智AI學園公眾號回復:「傳奇NLP攻城獅01」,可以獲取本節課程的Jupyter Notebook文檔哦!

《PyTorch 概要簡析》

本期目錄:

  • 1.PyTorch 核心模塊 Tensor 簡介
  • 2.計算圖以及自動微分
  • 3.深度學習要點:仿射映射、非線性、目標函數
  • 4.優化與訓練
  • 5.總結

1、 PyTorch 核心模塊 Tensor 簡介

Tensor的中文翻譯是「張量」,可別小看這個玩意。在我們後面要講的深度學習運算中,所有的計算都是通過張量來進行的!如果你不熟悉張量,目前也不用了解太深,只要將它理解為是一個n維的矩陣就行啦。

1.1 讓我們就來創建一些Tensor吧!

我們可以把 Python list 類型轉化成一個 Tensor(下邊代碼可左右滑動)# 使用指定的數據創建Tensor,我們先來創建一個維度為1的Tensor,這時候我們可以把它看做一個1維的向量。V_data = [1., 2., 3.] V = torch.Tensor(V_data)print(V)# 創建一個2維的TensorM_data = [[1., 2., 3.], [4., 5., 6]]M = torch.Tensor(M_data)print(M)# 創建一個3維的Tensor,維度為:2x2x2T_data = [[[1.,2.], [3.,4.]], [[5.,6.], [7.,8.]]]T = torch.Tensor(T_data)print(T)123[torch.FloatTensor of size 3]1 2 34 5 6[torch.FloatTensor of size 2x3](0 ,.,.) = 1 23 4(1 ,.,.) = 5 67 8[torch.FloatTensor of size 2x2x2]

3維的Tensor是不是不好理解呀? 其實這玩意和我們經常使用的多維數組是一個意思,不管是幾個維度都是可以通過索引(下標)來取值的。 對於1維的Tensor,我們可以通過索引取到的是一個「值」; 對於2維的Tensor,通過索引可以取到一個「向量」; 對於3維的Tensor,通過索引可以取到一個「矩陣」

# 取一個值print(V[0])# 取一個向量print(M[0])# 取一個矩陣print(T[0])1.0123[torch.FloatTensor of size 3] 1 23 4[torch.FloatTensor of size 2x2]

剛才我們創建的Tensor,默認都是Float類型的(torch.FloatTensor),我們可以在創建Tensor的時候指定Tensor的類型,比如使用 torch.LongTensor() 創建一個長整型的 Tensor。

我們也可以創建一個充滿隨機數的Tensor,只要在創建的時候指定Tensor的維度即可。

下面我們就創建一個維度為(3,4,5)的Tensor,即一個擁有3個元素的Tensor,其中每個元素是4行5列的矩陣。

x = torch.randn((3, 4, 5))print(x)(0 ,.,.) = -2.9718 1.7070 -0.4305 -2.2820 0.52370.0004 -1.2039 3.5283 0.4434 0.58480.8407 0.5510 0.3863 0.9124 -0.84101.2282 -1.8661 1.4146 -1.8781 -0.4674(1 ,.,.) = -0.7576 0.4215 -0.4827 -1.1198 0.30561.0386 0.5206 -0.5006 1.2182 0.2117-1.0613 -1.9441 -0.9596 0.5489 -0.9901-0.3826 1.5037 1.8267 0.5561 1.6445(2 ,.,.) = 0.4973 -1.5067 1.7661 -0.3569 -0.17130.4068 -0.4284 -1.1299 1.4274 -1.40271.4825 -1.1559 1.6190 0.9581 0.77470.1940 0.1687 0.3061 1.0743 -1.0327[torch.FloatTensor of size 3x4x5]

1.2 使用Tensor運算

你可以像操縱一個變數那樣操作Tensor進行運算。

x = torch.Tensor([ 1., 2., 3. ])y = torch.Tensor([ 4., 5., 6. ])z = x + yprint(z)579[torch.FloatTensor of size 3]

你可以在官方網站pytorch.org/docs/torch.上找到Tensor支持的所有操作。

Tensor的拼接(concatenation)是一個非常重要的操作,所以我們在這裡著重講一下。

# 默認情況下,Tensor是基於第一個維度進行拼接的。# 比如在下面的例子中,2行+3行得到一個5行5列的Tensor。x_1 = torch.randn(2, 5)y_1 = torch.randn(3, 5)z_1 =torch.cat([x_1, y_1])print(z_1)# 我們也可以指定維度進行拼接。# 比如我們指定要拼接的維度(axis)為1(默認0),對於下面的例子就是3列+5列得到2行8列的Tensor。x_2 = torch.randn(2, 3)y_2 = torch.randn(2, 5)z_2 = torch.cat([x_2, y_2], 1) print(z_2)

1.0930 0.7769 -1.3128 0.7099 0.9944-0.2694 -0.6491 -0.1373 -0.2954 -0.7725-0.2215 0.5074 -0.6794 -1.6115 0.5230-0.8890 0.2620 0.0302 0.0013 -1.39871.4666 -0.1028 -0.0097 -0.8420 -0.2067[torch.FloatTensor of size 5x5]1.0672 0.1732 -0.6873 0.3620 0.3776 -0.2443 -0.5850 2.08120.3111 0.2358 -1.0658 -0.1186 0.4903 0.8349 0.8894 0.4148[torch.FloatTensor of size 2x8]# 如果你要拼接的Tensor維度不匹配,就會報錯。# 我們來熟悉下這個錯誤,以後如果在項目中遇到這個錯誤,可以快速定位。torch.cat([x_1, x_2])---------------------------------------------------------------------------RuntimeError Traceback (most recent call last)<ipython-input-7-1438963ed9fa> in <module>()1 # 如果你要拼接的Tensor維度不匹配,就會報錯。2 # 我們來熟悉下這個錯誤,以後如果在項目中遇到這個錯誤,可以快速定位。----> 3 torch.cat([x_1, x_2])RuntimeError: inconsistent tensor sizes at /Users/soumith/miniconda2/conda-bld/pytorch_1493757035034/work/torch/lib/TH/generic/THTensorMath.c:2559

1.3 非常重要!Tensor 變形

使用.view()方法可以改變Tensor的形狀(重設維度),這點也非常重要,為什麼要這樣說哪?

因為有很多深度神經網路都要求輸入數據是一個固定的形狀(維度),所以我們經常會用到.view()方法對輸入Tensor進行變形。

x = torch.randn(2, 3, 4)print(x)# Reshape to 2 rows, 12 columns# 將2x3x4的Tensor轉化為2行12列的Tensor# 大家可以通過觀察輸出,來推導變形的過程print(x.view(2, 12)) # 這個和上面代碼的實現效果是一樣的# 如果變形的維度參數設置為-1,則由系統自動推導變形的形狀(3*4)print(x.view(2, -1)) (0 ,.,.) = 0.0507 -0.9644 -2.0111 0.52452.1332 -0.0822 0.8388 -1.32330.0701 1.2200 0.4251 -1.2328(1 ,.,.) = -0.6195 1.5133 1.9954 -0.6585-0.4139 -0.2250 -0.6890 0.98820.7404 -2.0990 1.2582 -0.3990[torch.FloatTensor of size 2x3x4]Columns 0 to 9 0.0507 -0.9644 -2.0111 0.5245 2.1332 -0.0822 0.8388 -1.3233 0.0701 1.2200-0.6195 1.5133 1.9954 -0.6585 -0.4139 -0.2250 -0.6890 0.9882 0.7404 -2.0990Columns 10 to 11 0.4251 -1.23281.2582 -0.3990[torch.FloatTensor of size 2x12]Columns 0 to 9 0.0507 -0.9644 -2.0111 0.5245 2.1332 -0.0822 0.8388 -1.3233 0.0701 1.2200-0.6195 1.5133 1.9954 -0.6585 -0.4139 -0.2250 -0.6890 0.9882 0.7404 -2.0990Columns 10 to 11 0.4251 -1.23281.2582 -0.3990[torch.FloatTensor of size 2x12]

2、計算圖以及自動微分

計算圖的概念對於高效的深度學習編程至關重要,因為它可以讓您無需自己編寫反向傳播梯度。簡單來說一個計算圖表示的就是你的數據是如何組合起來直到輸出的。因為計算圖詳細說明了你的哪個參數參與了那種運算,所以它包含足夠的信息來計算導數。這可能聽起來很玄乎,所以就讓我們通過使用 autograd.Variable 來探究一下這究竟是怎麼回事。

一般來說,對於早期的深度學習程序,都是需要程序員自己編寫反向傳播的步驟。而像PyTorch這樣的新型深度學習框架,可以通過計算圖自動計算反向傳播。如果你不熟悉計算圖和反向傳播演算法,我這裡放一個動圖讓你可以有一個簡單的理解:

關於反向傳播演算法,其實就是個神經網路優化演算法,它讓神經網路在訓練中更加精確。網上的演算法解析有很多,在這裡推薦兩個:

zhihu.com/question/2723

http://galaxy.agh.edu.pl/%7Evlsi/AI/backpten/backprop.html

理論講煩了,我們還是拿實例來說明問題吧。

首先,從程序員的角度來看。我們在上面創建的torch.Tensor對象中存儲了什麼?顯然它存儲了數據和形狀,也許還有一些我們看不到的東西。但是當我們將兩個Tensor相加時,我們就得到了一個輸出Tensor(和)。而這個輸出Tensor也僅僅保存著它的數據和形狀,它並不知道自己究竟是由兩個Tensor相加得到的,還是從數據文件里讀取的。

在PyTorch中,僅僅靠Tensor是無法完成反向傳播的任務的,因為它無法在計算中自動記錄自己的父級關係(即無法自動構造計算圖),所以我們要使用「Variable」類型。

# Variable 可以從 Tensor 中創建x = autograd.Variable( torch.Tensor([1., 2., 3]), requires_grad=True )# 可以使用.data屬性來取出 Variable 中的數據print(x.data)# Tensor 支持的運算,Variable 都支持y = autograd.Variable( torch.Tensor([4., 5., 6]), requires_grad=True )z = x + yprint(z.data)# 但是 Variable 的特點是,它在運算中可以自動建立父子節點關係print(z.creator)123[torch.FloatTensor of size 3]579[torch.FloatTensor of size 3]<torch.autograd._functions.basic_ops.Add object at 0x106bc9748>

對於上面的程序來說, Variable 知道是什麼創建的它。比如 z 知道它自己不是從文件中讀入的,也不是乘法或指數運算的結果。

但是,這對於自動計算梯度來說,能起到什麼作用哪?

# 讓我們把z中的值都加起來(5+7+9)s = z.sum()print(s)print(s.creator)Variable containing:21[torch.FloatTensor of size 1]<torch.autograd._functions.reduce.Sum object at 0x106bc9668>

我們可以看到,Variable s 中不但保存了z的累加值,還保存了指向它父級單位的「指針」。

從數學上講,Variable s中的累加和與x的第一個分量的導數是什麼?應該是:

現在,Variable S 知道它是來自 Tensor z 元素的累加和,而z知道它自己是x與y的和,那麼:

所以Variable s包含足夠的信息來確定我們想要的導數是1!

當然,我們在這裡講的是簡化的導數計算流程,這不是我們討論的重點,所以大家不理解也沒有關係。我們的重點是,相比於Tensor,Variable攜帶足夠的信息讓自己可以在反向傳播中被計算。實際上,是PyTorch的編寫者賦予了sum()函數以及「+」操作自動計算梯度、自動運行反向傳播演算法的能力。所以大家只要了解它們可以自動運行反向傳播演算法就行了,本教程不再對這些演算法做更多的討論。

讓我們來體驗一下PyTorch自動計算梯度的方式:(注意如果你多次運行本段代碼,這一塊的梯度會累加。PyTorch會在Variable的.grad屬性中累加梯度,因為對某些模型來說是非常方便的)。

s.backward() # 在哪個變數調用這個方法,反向傳播就從那個變數開始print(x.grad)Variable containing:111[torch.FloatTensor of size 3]

下面我們將演示一個有關自動反向傳播的錯誤使用案例,初學者可能經常會遇到這樣的錯誤,請大家注意!

x = torch.randn((2,2))y = torch.randn((2,2))z = x + y # z 是 Tensor 運算的結果,是無法參與反向傳播運算的var_x = autograd.Variable( x )var_y = autograd.Variable( y )var_z = var_x + var_y # var_z 是 Variable 運算的結果,可以進行反向傳播運算print(var_z.creator)var_z_data = var_z.data# 將 Variable z 的數據取出來,賦值給var_z_datanew_var_z = autograd.Variable( var_z_data ) # 再通過 var_z_data創建一個 Variable new_var_z# 那麼問題來了,通過 new_var_z 可以進行反向傳播,從而一直傳播到 Variable x,y嗎?# 不行!print(new_var_z.creator)<torch.autograd._functions.basic_ops.Add object at 0x10d89c748>None

為什麼不行哪?是因為我們從 var_z 中取出 data ,然後用 data 創建 new_var_z 實際上切斷了 new_var_z 與其它變數的聯繫,它不在計算圖中,所以沒有父節點,更無法參與反向傳播計算。

這是使用autograd.Variables進行計算的最基本,最重要的規則(請注意,這在其它深度學習框架中同樣重要):

如果您想讓輸出層計算的誤差可以反向傳播到網路的某個部分,你 一定不能 將這個部分的變數從整個計算鏈中分離出來。如果這樣做了,誤差函數將無法傳播到這個部分,以至於其參數無法更新。

我用粗體字說,因為這個錯誤可以以非常微妙的發生在你的身上,更嚴重的是這種錯誤並不會導致你的代碼崩潰或出現警告,因此難以被發現,但是它會導致你的神經網路一直得不到收斂,所以你一定要小心。

3、 深度學習要點:

仿射映射、非線性、目標函數

深度學習以非常巧妙的方式將非線性函數和線性函數結合到一起。引入非線性函數給創造強大的深度學習模型提供了條件。在這一節,我們將深入了解深度學習的核心模塊,並嘗試定義一個目標函數,觀察一個模型究竟是如何進行訓練的。

3.1 仿射映射

深度學習一個重要的功能就是可以進行仿射映射(Affine Maps),所謂仿射映射表達成函數是這個樣子的:

A代表一個矩陣,x、b代表向量。在深度神經網路的訓練過程中,網路學習到的就是這裡的A和b。

PyTorch進行仿射映射時處理的是輸入參數的列,而不是行,這個大家需要注意一下。通過下面的例子我們可以看到,輸入數據的5列被映射成了3列,在映射的計算過程中使用了矩陣A,以及偏置量b。

# 下面是從5列到3列的映射lin = nn.Linear(5, 3) # 創建一個維度為2x5的數據。data = autograd.Variable( torch.randn(2, 5) ) print(data)print(lin(data)) Variable containing:-0.4705 0.8503 -0.4165 -0.7499 1.06320.0073 -1.4252 -0.0781 -0.5138 1.1375[torch.FloatTensor of size 2x5]Variable containing:0.4825 0.0247 0.4566-0.0652 -0.7002 -0.4353[torch.FloatTensor of size 2x3]

3.2 非線性函數

首先讓我們看看為什麼需要非線性函數。假設我們已經有下面兩個仿射映射:

如果把兩個仿射映射結合起來,比如f(g(x)),神經網路會因此變得更加強大嗎?

在化簡結果中,我們看到其實 AC 還是一個矩陣, Ad+b 還是一個向量,把兩個仿射映射結合起來得到的還是一個仿射映射,神經網路並沒有獲得更強的功能。

然而當我們將非線性變換加入進來,事情就完全不一樣了,我們可以構建更加強大的模型。

tanh(x), sigmoid(x), ReLU(x)是最常用的非線性變換。你或許會問,非線性函數有很多,為啥特別選擇這些函數?

選擇這些函數的原因是,這些函數都有梯度,並且容易計算。而計算梯度是深度學習最必要的操作。比如下面就是一個求梯度的操作:

提示:你可能在一些深度學習理論書籍中看到它們常用的非線性函數是sigmoid(x),但是在實際應用中sigmoid(x)並沒有應用的那麼普遍。因為使用這個函數有時候會導致梯度消失(變得非常小)的問題,梯度變得非常小就意味著學習速度將變得很慢。所以很多人常用 tanh 或者 ReLU 作為非線性函數。

# 在 PyTorch 中,大部分的非線性函數包括在 torch.nn.functional 這個模塊中了# 我們在之前已經把這個模塊引入為 F,data = autograd.Variable( torch.randn(2, 2) )print(data)print(F.relu(data))Variable containing:-1.0246 -1.0300-1.0129 0.0055[torch.FloatTensor of size 2x2]Variable containing:1.00000e-03 *0.0000 0.00000.0000 5.5350[torch.FloatTensor of size 2x2]

3.3 Softmax以及概率函數

Softmax(x)函數也是非線性函數之一,但是這個函數比較特殊,一般只應用在深度神經網路的最後一層。這是因為這個函數會將輸入的向量轉化為一個概率分布。這個函數的定義如下所示。

可以看到這個函數的輸出是一個概率分布,所有的值都為正且相加起來為1。

# 我們也可以在 torch.nn.functional 中找到 Softmax 函數data = autograd.Variable( torch.randn(5) )print(data)print(F.softmax(data))print(F.softmax(data).sum()) # 把所有概率加起來的和為1print(F.log_softmax(data)) # PyTorch 還提供了 log_softmaxVariable containing:-0.9347-0.98821.3801-0.11730.9317[torch.FloatTensor of size 5]Variable containing:0.04810.04560.48670.10890.3108[torch.FloatTensor of size 5]Variable containing:1[torch.FloatTensor of size 1]Variable containing:-3.0350-3.0885-0.7201-2.2176-1.1686[torch.FloatTensor of size 5]

3.4 目標函數

目標函數就是神經網路在訓練的過程中需要最小化的函數(有時候它也被稱為 loss function 或者 cost function)。

一般計算損失,進行反向傳播的流程是這樣的:

首先選擇訓練實例(一條訓練數據),通過神經網路運行訓練實例,然後計算輸出的損失。然後通過損失函數的導數(通過反向傳播)來更新模型的參數。在訓練模型的過程中,如果模型得出的結果是錯誤的,那麼損失值就會很高。如果模型得出的結果是正確的,那麼損失值就會很低。

將目標函數最小化對神經網路有什麼好處哪?

它能使我們的神經網路「泛化」。泛化就是讓神經網路在「訓練數據集」、「測試數據集」、以及在實際使用中產生的誤差都很小。

在「有監督學習」的「分類任務」中,經常會採用「負對數似然估計」作為目標函數。因為對於這些分類任務來說,最小化這個函數意味著:最小化正確輸出的負對數概率(或等效地,最大化正確輸出的對數概率)。

4、優化與訓練

既然我們可以計算損失函數,那麼我們怎麼用它來優化神經網路哪?我們在前面了解到autograd.Variable是具備自動計算梯度的能力的。那麼,因為我們的損失函數就是Variable之間的運算,我們可以利用這一點讓它自動計算梯度!然後我們可以執行標準的梯度更新方法。設θ為我們的參數,L(θ)為損失函數,η是正的學習率參數,那麼:

現在業界存在大量的優化演算法,他們對學習率以及學習方法做了很多的探索和改進。PyTorch 為我們封裝好了這些方法,所以我們只要拿來用即可,不需要在意這些演算法實現的細節。在實際的深度學習項目中,經常要嘗試對比不同的優化方法 以選出來一個最適合自己項目的。不過一般來說,通過使用 Adam 或者 RMSProp 演算法代替 SGD 能使模型表現的更好一些。

從下面的兩個動圖中,可以看到多種優化演算法在損失曲面和損失鞍點上的性能表現:

5、總結

在本期教程中我們主要講了PyTorch的基本用法,另外還提到了一點深度學習的理論和數學知識。

PyTorch簡單易學,它的基本用法我相信大家都是可以掌握的。

而關於深度學習的數學知識,大家能理解當然是最好的,如果現在不能理解也不用特別在意。

因為隨著學習的深入,你自然就能對這些理論產生新的理解和認識。並且,隨著猶如PyTorch這樣強大深度學習框架的推出,你基本不需要去顧及諸如反向傳播這樣的細節問題。你現在只要把深度學習看做一種通用數學建模方法就可以了,而你需要做的只是設計模型的形狀(網路結構),選擇模型優化的方向(即損失函數)即可。

在下一期的教程中,我們將編寫一個完整的神經網路,並實現「詞袋模型分類器」的功能。詞袋模型是NLP領域常用且重要的模型,但它非常簡單,所以你可以提前了解一下。

讓我們下期再見!

想學習pytorch的話可以來集智AI學園找我或者小仙女,不會讓你失望 de的


推薦閱讀:

人工智慧的末日:真正像人類一樣思考的AI真的好嗎?
從軟體到人工智慧 科技業熱點悄然轉變的玄機
手機比你更加了解你——《未來簡史》讀後感
從濾波與控制看人工智慧

TAG:PyTorch | 深度学习DeepLearning | 人工智能 |