- ChatGPT原理與應用開發
- 郝少春 黃玉琳 易華揮
- 5404字
- 2024-04-22 11:47:53
1.2 語言模型基礎
1.2.1 最小語義單位Token與Embedding
首先,我們需要解釋一下如何將自然語言文本表示成計算機所能識別的數字。對于一段文本來說,要做的首先就是把它變成一個個Token。你可以將Token理解為一小塊,可以是一個字,也可以是兩個字的詞,或三個字的詞。也就是說,給定一個句子,我們有多種獲取不同Token的方式,可以分詞,也可以分字。英文現在都使用子詞,比如單詞annoyingly會被拆成如下兩個子詞。
["annoying", "##ly"]
子詞把不在詞表里的詞或不常見的詞拆成比較常見的片段,“##
”表示和前一個Token是直接拼接的,沒有空格。中文現在基本使用字+詞的方式。我們不直接解釋為什么這么做,但你可以想一下完全的字或詞的效果,拿英文舉例更直觀。如果只用26個英文字母,雖然詞表很小(加上各種符號可能也就100來個),但粒度太細,每個Token(即每個字母)幾乎沒法表示語義;如果用詞,這個粒度又有點太大,詞表很難涵蓋所有詞。而子詞可以同時兼顧詞表大小和語義表示,是一種折中的做法。中文稍微簡單一些,就是字+詞,字能獨立表示意義,比如“是”“有”“愛”;詞是由一個以上的字組成的語義單位,一般來說,把詞拆開可能會丟失語義,比如“長城”“情比金堅”。當然,中文如果非要拆成一個一個字也不是不可以,具體要看任務類型和效果。
當句子能夠表示成一個個Token時,我們就可以用數字來表示這個句子了,最簡單的方法就是將每個Token用一個數字來表示,但考慮這個數字的大小其實和Token本身沒有關系,這種單調的表達方式其實只是一種字面量的轉換,并不能表示豐富的語言信息。我們稍微多想一點,因為已經有一個預先設計好的詞表,那么是不是可以用詞表中的每個Token是否在句子中出現來表示?如果句子中包含某個Token,對應位置為1,否則為0,這樣每句話都可以表示成長度(長度等于詞表大小)相同的1和0組成的數組。更進一步地,還可以將“是否出現”改成“頻率”以凸顯高頻詞。事實上,在很長一段時間里,自然語言都是用這種方法表示的,它有個名字,叫作詞袋模型(bag of words,BOW)。從名字來看,詞袋模型就像一個大袋子,能把所有的詞都裝進來。文本中的每個詞都被看作獨立的,忽略詞之間的順序和語法,只關注詞出現的次數。在詞袋模型中,每個文本可以表示為一個向量,向量的每個維度對應一個詞,維度的值表示這個詞在文本中出現的次數。這種表示方法如表1-1所示,每一列表示一個Token,每一行表示一個句子,每個句子可以表示成一個長度(就是詞表大小)固定的向量,比如第一個句子可以表示為[3,1,1,0,1,1,0,
…]
。
表1-1 詞袋模型

這里的詞表是按照拼音排序的,但這個順序其實不重要,讀者不妨思考一下為什么。另外,注意這里只顯示了7列,也就是詞表中的7個Token,但實際上,詞表中的Token一般都在“萬”這個級別。所以,表1-1中的省略號實際上省略了上萬個Token。
這種表示方法很好,不過有兩個比較明顯的問題。第一,由于詞表一般比較大,導致向量維度比較高,而且比較稀疏(大量的0),計算起來不太方便;第二,由于忽略了Token之間的順序,導致部分語義丟失。比如“你愛我”和“我愛你”的向量表示一模一樣,但其實意思不一樣。于是,詞向量(也叫詞嵌入)出現了,它是一種稠密表示方法。簡單來說,一個Token可以表示成一定數量的小數(一般可以是任意多個,專業叫法是詞向量維度,根據所用的模型和設定的參數而定),一般數字越多,模型越大,表示能力越強,不過即使再大的模型,這個維度也會比詞表小很多。如下面的代碼示例所示,每一行的若干(詞向量維度)的小數就表示對應位置的Token,詞向量維度常見的值有200、300、768、1536等。
愛 [0.61048, 0.46032, 0.7194, 0.85409, 0.67275, 0.31967, 0.89993, ...]
不 [0.19444, 0.14302, 0.71669, 0.03338, 0.34856, 0.6991, 0.49111, ...]
對 [0.24061, 0.21482, 0.53269, 0.97885, 0.51619, 0.07808, 0.9278, ...]
古琴 [0.21798, 0.62035, 0.89935, 0.93283, 0.24022, 0.91339, 0.6569, ...]
你 [0.392, 0.13321, 0.00597, 0.74754, 0.45524, 0.23674, 0.7825, ...]
完 [0.26588, 0.1003, 0.40055, 0.09484, 0.20121, 0.32476, 0.48591, ...]
我 [0.07928, 0.37101, 0.94462, 0.87359, 0.55773, 0.13289, 0.22909, ...]
... ......................................................................
細心的讀者可能會有疑問:“句子該怎么表示?”這個問題非常關鍵,其實在深度NLP(deep NLP)早期,往往是對句子的所有詞向量直接取平均(或者求和),最終得到一個和每個詞向量同樣大小的向量——句子向量。這項工作最早要追溯到Yoshua Bengio等人于2003年發表的論文“A neural probabilistic language model”,他們在訓練語言模型的同時,順便得到了詞向量這個副產品。不過,最終開始在實際中大規模應用,則要追溯到2013年谷歌公司的Tomas Mikolov發布的Word2Vec。借助Word2Vec,我們可以很容易地在大量語料中訓練得到一個詞向量模型。也正是從那時開始,深度NLP逐漸嶄露頭角成為主流。
早期的詞向量都是靜態的,一旦訓練完就固定不變了。隨著NLP技術的不斷發展,詞向量技術逐漸演變成基于語言模型的動態表示。也就是說,當上下文不一樣時,同一個詞的向量表示將變得不同。而且,句子的表示也不再是先拿到詞向量再構造句子向量,而是在模型架構設計上做了考慮。當輸入句子時,模型經過一定計算后,就可以直接獲得句子向量;而且語言模型不僅可以表示詞和句子,還可以表示任意文本。類似這種將任意文本(或其他非文本符號)表示成稠密向量的方法,統稱Embedding表示技術。Embedding表示技術可以說是NLP領域(其實也包括圖像、語音、推薦等領域)最基礎的技術,后面的深度學習模型都基于此。我們甚至可以稍微夸張點說,深度學習的發展就是Embedding表示技術的不斷發展。
1.2.2 語言模型是怎么回事
語言模型(language model,LM)簡單來說,就是利用自然語言構建的模型。自然語言就是我們日常生活、學習和工作中常用的文字。語言模型就是利用自然語言文本構建的,根據給定文本,輸出對應文本的模型。
語言模型具體是如何根據給定文本輸出對應文本呢?方法有很多種,比如我們寫好一個模板:“XX喜歡YY”。如果XX是我,YY是你,那就是“我喜歡你”,反過來就是“你喜歡我”。我們這里重點要說的是概率語言模型,它的核心是概率,準確來說是下一個Token的概率。這種語言模型的過程就是通過已有的Token預測接下來的Token。舉個簡單的例子,比如你只告訴模型“我喜歡你”這句話,當你輸入“我”的時候,它就已經知道你接下來要輸入“喜歡”了。為什么?因為它的“腦子”里就只有這4個字。
好,接下來,我們要升級了。假設我們給了模型很多資料,多到現在網上所能找到的資料都給了它。這時候你再輸入“我”,此時它大概不會說“喜歡”了。為什么呢?因為見到了更多不同的文本,它的“腦子”里已經不只有“我喜歡你”這4個字了。不過,如果我們考慮的是最大概率,也就是說,每次都只選擇下一個最大概率的Token,那么對于同樣的給定輸入,我們依然會得到相同的對應輸出(可能還是“喜歡你”,也可能不是,具體要看給的語料)。對于這樣的結果,語言模型看起來比較“呆”。我們把這種方法叫作貪心搜索(greedy search),因為它只往后看一個詞,只考慮下一步最大概率的詞!為了讓生成的結果更加多樣和豐富,語言模型都會在這個地方執行一些策略。比如讓模型每一步多看幾個可能的詞,而不是就看概率最大的那個詞。這樣到下一步時,上一步最大概率的Token,加上這一步的Token,路徑概率(兩步概率的乘積)可能就不是最大的了。
舉個例子,如圖1-3所示,先看第一步,如果只選概率最大的那個詞,那就變成“我想”了。但是別急,我們給“喜歡”一點機會,同時考慮它們兩個。再往下看一步,“喜歡”和“想”后面最大概率的都是“你”,最后就有了下面幾句(我們附上了它們的概率)。
● “我喜歡你”,概率為0.3×0.8=0.24。
● “我喜歡吃”,概率為0.3×0.1=0.03。
● “我想你”,概率為0.4×0.5=0.2。
● “我想去”,概率為0.4×0.3=0.12。

圖1-3 語言模型如何預測下一個詞
多看一步大不一樣!看看概率最大的成誰了,變成了“我喜歡你”。上面這種方法叫作集束搜索(beam search),簡單來說,就是一步多看幾個詞,看最終句子(比如生成到句號、感嘆號或其他停止符號)的概率。在上面的例子中,num_beams=2(只看了兩個詞),看得越多,越不容易生成固定的文本。
好了,其實在最開始的語言模型中,基本就到這里,上面介紹的兩種不同搜索方法(貪心搜索和集束搜索)也叫解碼策略。當時更多被研究的還是模型本身,我們經歷了從簡單模型到復雜模型,再到巨大復雜模型的變遷過程。簡單模型就是把一句話拆成一個個Token,然后統計概率,這類模型有個典型代表——N-Gram模型,它也是最簡單的語言模型。這里的N表示每次用到的上下文Token的個數。舉個例子,看下面這句話:“人工智能讓世界變得更美好”。N-Gram模型中的N通常等于2或3,等于2的叫Bi-Gram,等于3的叫Tri-Gram。
● Bi-Gram:人工智能/讓 讓/世界 世界/變得 變得/更 更/美好
● Tri-Gram:人工智能/讓/世界 讓/世界/變得 世界/變得/更 變得/更/美好
Bi-Gram和Tri-Gram的區別是,前者的下一個Token是根據上一個Token來的,而后者的下一個Token是根據前兩個Token來的。在N-Gram模型中,Token的表示是離散的,實際上就是詞表中的一個個單詞。這種表示方式比較簡單,再加上N不能太大,導致難以學到豐富的上下文知識。事實上,它并沒有用到深度學習和神經網絡,只是一些統計出來的概率值。以Bi-Gram為例,在給定很多語料的情況下,統計的是從“人工智能”開始,下個詞出現的頻率。假設“人工智能/讓”出現了5次,“人工智能/是”出現了3次,將它們出現的頻率除以所有的Gram數就是概率。
訓練N-Gram模型的過程其實是統計頻率的過程。如果給定“人工智能”,N-Gram模型就會找基于“人工智能”下個最大概率的詞,然后輸出“人工智能讓”。接下來就是給定“讓”,繼續往下走了。當然,我們也可以用上面提到的不同解碼策略往下走。
接下來,讓每個Token成為一個Embedding向量。我們簡單解釋一下在這種情況下怎么預測下一個Token。其實還是計算概率,但這次和剛才的稍微有點不一樣。在剛才離散的情況下,用統計出來的對應Gram數除以Gram總數就是出現概率。但是稠密向量要稍微換個方式,也就是說,給你一個d維的向量(某個給定的Token),你最后要輸出一個長度為N的向量,N是詞表大小,其中的每一個值都是一個概率值,表示下一個Token出現的概率,概率值加起來為1。按照貪心搜索解碼策略,下一個Token就是概率最大的那個,寫成簡單的計算表達式如下。
# d維,加起來和1沒關系,大小是1×d,表示給定的Token
X = [0.001, 0.002, 0.0052, ..., 0.0341]
# N個,加起來為1,大小是1×N,表示下一個Token就是每個Token出現的概率
Y = [0.1, 0.5, ..., 0.005, 0.3]
# W是模型參數,也可以叫模型
X·W = Y # W可以是 d×N 大小的矩陣
上面的W
就是模型參數,其實X
也可以被看作模型參數(自動學習到的)。因為我們知道了輸入和輸出的大小,所以中間其實可以經過任意的計算,也就是說,W
可以包含很多運算。總之各種張量(三維以上數組)運算,只要保證最后的輸出形式不變就行。各種不同的計算方式就意味著各種不同的模型。
在深度學習早期,最著名的語言模型是使用循環神經網絡(recurrent neural network,RNN)訓練的,RNN是一種比N-Gram模型復雜得多的模型。RNN與其他神經網絡的不同之處在于,RNN的節點之間存在循環連接,這使得它能夠記住之前的信息,并將它們應用于當前的輸入。這種記憶能力使得 RNN 在處理時間序列數據時特別有用,例如預測未來的時間序列數據、進行自然語言的處理等。通俗地說,RNN就像具有記憶能力的人,它可以根據之前的經驗和知識對當前的情況做出反應,并預測未來的發展趨勢,如圖1-4所示。

圖1-4 RNN(摘自Colah的博客文章“Understanding LSTM Networks”)
在圖1-4中,右邊是左邊的展開,A就是參數,x是輸入,h就是輸出。自然語言是一個Token接著一個Token(Token by Token)的,從而形成一個序列。參數怎么學習呢?這就要稍微解釋一下學習(訓練)過程。
如圖1-5所示,第一行就是輸入X,第二行就是輸出Y,SOS(start of sentence)表示句子開始,EOS(end of sentence)表示句子結束。注意,圖1-4中的h并不是那個輸出的概率,而是隱向量。如果需要概率,可以再對h執行張量運算,歸一化到整個詞表即可。

圖1-5 語言模型學習(訓練)時的輸入輸出
import torch
import torch.nn as nn
rnn = nn.RNN(32, 64)
input = torch.randn(4, 32)
h0 = torch.randn(1, 64)
output, hn = rnn(input, h0)
output.shape, hn.shape
# (torch.Size([4, 64]), torch.Size([1, 64]))
上面的nn.RNN
就是RNN模型。輸入是一個4×32的向量,換句話說,輸入是4個Token,維度d
=32
。h0
就是隨機初始化的輸出,也就是4個Token中第1個Token的輸出,這里output
的4個64維的向量分別表示4個輸出。hn
就是最后一個Token的輸出(它和output
的最后一個64維向量是一樣的),也可以看成整個句子的表示。注意,這里的output
和圖1-5中的輸出Y還沒有關系。別急,繼續往下看。如果要輸出詞的概率,就需要先擴充到詞表大小,再進行歸一化。
# 假設詞表大小N=1000
wo = torch.randn(64, 1000)
# 得到4×1000的概率矩陣,每一行概率和為1
probs = nn.Softmax(dim=1)(output @ wo)
probs.shape, probs.sum(dim=1)
# torch.Size([4, 1000]), tensor([1.0000, 1.0000, 1.0000, 1.0000],
# grad_fn=<SumBackward1>)
這里的probs
的每一行就是詞表大小的概率分布,概率和為1,意思是根據當前Token生成下一個Token的概率,下一個Token有可能是詞表中的任意一個Token,但它們的概率和一定為1。因為我們知道接下來每個位置的Token是什么(也就是圖1-5中的輸出Y)。這里得到最大概率的那個Token,如果正好是這個Token,則說明預測對了,參數就不用怎么調整;反之,模型就會調整前面的參數(RNN
、h0
、input
和wo
)。你可能會疑惑為什么input
也是參數,其實前面我們偷懶了,本來的參數是一個1000×32的大矩陣,但我們使用了4個Token對應位置的向量。這個1000×32的大矩陣其實就是詞向量(每個詞一行),開始時全部隨機初始化,然后通過訓練調整參數。
訓練完成后,這些參數就不變了,然后就可以用前面同樣的步驟來預測了,也就是給定一個Token,預測下一個Token。如果使用貪心搜索,則每次給定同樣的Token時,生成的結果就一樣。其余的就和前面講的接上了。隨著深度學習的不斷發展,出現了更多比RNN還復雜的網絡結構,而且模型變得更大,參數更多,但邏輯和方法是一樣的。
好了,語言模型就介紹到這里。上面的代碼看不懂沒關系,你只需要大致了解每個Token是怎么表示、怎么訓練和預測出來的就行。簡單直觀地說,構建(訓練)語言模型的過程就是學習詞、句內在的“語言關系”;而推理(預測)就是在給定上下文后,讓構建好的模型根據不同的解碼策略輸出對應的文本。無論是訓練還是預測,都以Token為粒度進行。