自然語言處理(NLP)基礎教程1:接觸PyTorch
12 人贊了文章
什麼是NLP?
標題上寫著,自然語言處理,Natural Language Processing,簡稱NLP。
NLP是人工智慧的一個子領域。
NLP能做什麼?
NLP會通過一些基礎的技術手段,如:分詞(把整句話分成若干個詞)、詞性分割、語義分割、情感分析等等,完成一些高級應用任務,如:
- 機器翻譯(參考彩雲小譯)
- 得出股票或其它市場的先進交易策略(通過分析新聞或討論主題)
- 文本生成(生成詩詞歌賦或對話)
- 聊天系統(聊天機器人,智能助手,Siri)
- 還有語音系統也屬於NLP的範疇
為什麼要學習NLP?
目前深度學習應用最廣泛的兩個方向,一個是圖形圖像,另一個就是NLP。不管你是否打算要找一個有關人工智慧的工作,NLP技術能讓你擁有一種現代化的處理信息與數據的手段(A Modern Approach)。在這個信息爆炸的時代,掌握這種自動化處理信息的方法還是很有用的。
本教程的基礎要求
1.懂Python。
2.已經安裝了PyTorch。
本節教程的內容比較基礎,適用於對深度學習了解不多,沒使用PyTorch的小白。
內容涉及到:張量,張量計算,計算圖,反向傳播,梯度更新等基本內容,如果你已經了解這些概念,那麼可以直接跳過本課。
如果你是深度學習萌新,那麼歡迎你閱讀本教程。注意由於我水平有限,所以寫出的教程可能也不怎麼能看明白。反正萌新們只要過一遍腦子裡有相關的印象就行啦,等你深入深度學習後再回過頭來,自然會有掃戴斯乃(原來如此)的感覺。
教程開始
[註:在集智AI學園(swarmAI)公眾號回復「傳奇NLP工程師01」,可以獲取本教程的Jupyter Notebook文檔]
首先引入相關的package,同時可以驗證PyTorch成功安裝。
import torchimport torch.autograd as autogradimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optim
torch.manual_seed(1)
<torch._C.Generator at 0x10c71f810>
看到上面的輸出,說明你的PyTorch環境已經可用,下面我們介紹一些PyTorch的基本元素。
1.基本數據模塊:張量( Tensor)
Tensor通常翻譯為「張量」。在PyTorch中,張量是最基礎的存儲數據的單元。簡單直觀的理解,張量就是一個N維的矩陣(向量)。
1.1 嘗試使用張量
可以把 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 3
4 5 6[torch.FloatTensor of size 2x3](0 ,.,.) =1 23 4(1 ,.,.) =5 6
7 8[torch.FloatTensor of size 2x2x2]
3維的Tensor對於初學者可能有點抽象。
對於簡單的Tensor(尺寸不大於3個維度)來說,它和我們平常使用的多維數組差不多,不管是幾個維度都是可以通過索引(下標)來取值的。對於1維的Tensor,我們可以通過索引取到的是一個「值」;對於2維的Tensor,通過索引可以取到一個「向量」;對於3維的Tensor,通過索引可以取到一個「矩陣」# 取一個值print(V[0])# 取一個向量print(M[0])# 取一個矩陣print(T[0])1.0
1
23[torch.FloatTensor of size 3]
1 23 4[torch.FloatTensor of size 2x2]
上面創建的Tensor,默認都是Float類型的(torch.FloatTensor),我們可以在創建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.8410
1.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.7747
0.1940 0.1687 0.3061 1.0743 -1.0327[torch.FloatTensor of size 3x4x5]
1.2 張量間的數學運算
你可以像操縱一個變數那樣操作Tensor進行運算。
x = torch.Tensor([ 1., 2., 3. ])y = torch.Tensor([ 4., 5., 6. ])z = x + yprint(z)
5
79[torch.FloatTensor of size 3]
你可以在PyTorch官方論壇上找到張量支持的數學運算,或者看我這裡總結的張量的用法。
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)# 將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.3233
0.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 90.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 110.4251 -1.23281.2582 -0.3990[torch.FloatTensor of size 2x12]Columns 0 to 90.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 110.4251 -1.23281.2582 -0.3990[torch.FloatTensor of size 2x12]
2.計算圖以及自動微分
計算圖的概念對於高效的深度學習編程至關重要,因為它可以讓您無需自己編寫反向傳播梯度。簡單來說一個計算圖表示的就是你的數據是如何組合起來直到輸出的。因為計算圖詳細說明了你的哪個參數參與了那種運算,所以它包含足夠的信息來計算導數。這可能聽起來很玄乎,所以就讓我們通過使用 autograd.Variable 來探究一下這究竟是怎麼回事。
一般來說,對於早期的深度學習程序,都是需要程序員自己編寫反向傳播的步驟。而像PyTorch這樣的新型深度學習框架,可以通過計算圖自動計算反向傳播。如果你不熟悉計算圖和反向傳播演算法,我這裡放一個動圖讓你可以有一個簡單的理解:
關於反向傳播演算法,其實就是個神經網路優化演算法,它讓神經網路在訓練中更加精確。網上的演算法解析有很多,在這裡推薦兩個:
https://www.zhihu.com/question/27239198?rf=24827633
http://galaxy.agh.edu.pl/%7Evlsi/AI/backp_t_en/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)
1
23[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)
<torch.autograd._functions.basic_ops.Add object at 0x10d89c748>
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)
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 非線性函數
首先讓我們看看為什麼需要非線性函數。假設我們已經有下面兩個仿射映射:
如果把兩個仿射映射結合起來,比如 ,神經網路會因此變得更加強大嗎?
在化簡結果中,我們看到其實 AC 還是一個矩陣, Ad+b 還是一個向量,把兩個仿射映射結合起來得到的還是一個仿射映射,神經網路並沒有獲得更強的功能。
然而當我們將非線性變換加入進來,事情就完全不一樣了,我們可以構建更加強大的模型。
tanh(x), sigmoid(x), ReLU(x)是最常用的非線性變換。你或許會問,非線性函數有很多,為啥特別選擇這些函數?
選擇這些函數的原因是,這些函數都有梯度,並且容易計算。而計算梯度是深度學習最必要的操作。比如下面就是一個求梯度的操作:
提示:你可能在一些深度學習理論書籍中看到它們常用的非線性函數是 [即sigmoid(x)],但是在實際應用中 並沒有應用的那麼普遍。因為使用這個函數有時候會導致梯度消失(變得非常小)的問題,梯度變得非常小就意味著學習速度將變得很慢。所以很多人常用 tanh 或者 ReLU 作為非線性函數。
# 在 PyTorch 中,大部分的非線性函數包括在 torch.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.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_softmax
Variable 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領域常用且重要的模型,但它非常簡單,所以你可以提前了解一下。
讓我們下期再見!
推薦閱讀:
※2018.4.23論文推薦
※IBM宣布語音識別錯誤率接近人類水平
※第二章自然語言處理的主要課題
※Sentence Embeddings分享