允中 發(fā)自 凹非寺
量子位 報道 | 公眾號
編者按:
語言模型的身影遍布在NLP研究中的各個角落,想要了解NLP領(lǐng)域,就不能不知道語言模型。
想要讓模型能落地奔跑,就需借助深度學(xué)習(xí)框架之力,、自然是主流,但在都成獨(dú)家專利之后,不儲備“B計劃”,多少讓人有些擔(dān)驚受怕
這里有一份飛槳()語言模型應(yīng)用實例,從基礎(chǔ)概念到代碼實現(xiàn),娓娓道來,一一說明。現(xiàn)在,量子位分享轉(zhuǎn)載如下,宜學(xué)習(xí),宜收藏。
剛?cè)腴T深度學(xué)習(xí)與自然語言處理(NLP)時,在學(xué)習(xí)了 特別棒的入門書 ,斯坦福 等等后,也無限次起念頭,寫個系列吧,但都不了了之了。
近來,NLP 領(lǐng)域因為超大預(yù)訓(xùn)練模型,很多研究需要耗費(fèi)大量計算資源(比如百度新發(fā)布持續(xù)學(xué)習(xí)語義理解框架 ERNIE 2.0,該模型在共計 16 個中英文任務(wù)上超越了 BERT 和 XLNet,取得了 SOTA 效果),這樣的項目基本上就是在燒錢,小家小戶玩不起,于是就傻傻地等著大佬們發(fā)出論文,放出代碼,刷新榜單。不過這也意味著一個總結(jié)的好機(jī)會,加上額外的推動,便重新起了念頭。
這個系列會介紹我認(rèn)為現(xiàn)代 NLP 最重要的幾個主題,同時包括它們的實現(xiàn)與講解。
這里會使用的百度的開源深度學(xué)習(xí)平臺飛槳(),關(guān)于這點(diǎn),有如下幾個原因。
首先,不久前和一個科技媒體朋友聊天,因為當(dāng)時封鎖華為事件的原因,聊到了美國企業(yè)是否可能對我們封鎖深度學(xué)習(xí)框架,比如說主流的 和 ,我當(dāng)時答是說不定可能呢,畢竟谷歌連 都能去申請專利。只要之后改一下許可,不讓使用這些框架的更新,估計我們也沒辦法,于是就想著可以了解一下國內(nèi)百度的框架飛槳。
去飛槳的 看了一下,內(nèi)容很豐富,感覺飛槳對 NLP 這塊支持非常好,值得關(guān)注。
項目地址:
語言模型
現(xiàn)代 NLP 領(lǐng)域的一個核心便是語言模型 ( Model),可以說它無處不在,一方面它給 NLP 發(fā)展帶來巨大推動,是多個領(lǐng)域的關(guān)鍵部分,但另一方面,成也蕭何敗也蕭何,語言模型其實也限制了 NLP 發(fā)展,比如說在創(chuàng)新性生成式任務(wù)上,還有如何用語言模型獲得雙向信息。
那到底什么是語言模型?
什么是語言模型
就是語言的模型(認(rèn)真臉),開個玩笑,語言模型通俗點(diǎn)講其實就是判斷一句話是不是人話,正式點(diǎn)講就是計算一句話的概率,這個概率值表示這個本文有多大概率是一段正常的文本。
對于一句話,比如說用臉滾出來的一句話:“哦他發(fā)看和了犯點(diǎn)就看見發(fā)”,很明顯就不像人話,所以語言模型判斷它是人話的概率就小。而一句很常用的話:“好的,謝謝”,語言模型就會給它比較高的概率評分。
用數(shù)學(xué)的方式來表示,語言模型需要獲得這樣的概率:
其中 X 表示句子,x1,x2… 代表句子中的詞。怎么計算這樣一個概率呢,一個比較粗暴的方法就是有個非常非常大的語料庫,里面有各種各樣的句子,然后我們一個個數(shù),來計算不同句子的概率,但稍微想想就知道這個方法不太可能,因為句子組合無窮無盡。
為更好計算,利用條件概率公式和鏈?zhǔn)椒▌t,按照從左到右的句序,可以將公式轉(zhuǎn)換成:
題變成了如何求解:
怎么根據(jù)前面所有的詞預(yù)測下一個詞,當(dāng)然這個問題對于現(xiàn)在還有點(diǎn)復(fù)雜,之后可以用 RNN 模型來計算,但現(xiàn)在讓我們先假設(shè)對于一個詞離它近的詞重要性更大,于是基于馬爾可夫性假設(shè),一個詞只依賴它前面 n-1 個詞,這種情況下的語言模型就被稱為 N-gram 語言模型。
比如說基于前面2個詞來預(yù)測下一個詞就是 3-gram (tri-gram) 語言模型:
細(xì)心些的話,會發(fā)現(xiàn),當(dāng) n-gram 中的 n 增大,就會越接近原始語言模型概率方程。
當(dāng)然n并不是越大越好,因為一旦n過大,計算序列就會變長,在計算時 n-gram 時詞表就會太大,也就會引發(fā)所謂的 The Curse of (維度災(zāi)難) 。因此一般大家都將n的大小取在3,4,5附近。
早期實現(xiàn):數(shù)一數(shù)就知道了
最早了解類似語言模型計算概率,是在研究生階段當(dāng)時號稱全校最難的信息論課上,老師強(qiáng)烈安利香農(nóng)的經(jīng)典論文 A of ,論文中有一小節(jié)中,他就給利用類似計算上述語言模型概率的方法,生成了一些文本。
其中一個就是用 2-gram (bi-gram) 的頻率表來生成的,這已經(jīng)相當(dāng)于一個 bi-gram 語言模型了。
同樣,要構(gòu)建這樣一個 n-gram 語言模型,最主要工作就是,基于大量文本來統(tǒng)計 n-gram 頻率。
當(dāng)時有個課程作業(yè),就是先準(zhǔn)備一些英文文本,然后一個一個數(shù) n-gram,之后除以總數(shù)算出語言模型中需要的概率估計值,這種方法叫 Count-based Model。
傳統(tǒng) NLP 中搭建語言模型便是這樣,當(dāng)然還有更多技巧,比如平滑算法,具體可以參考 教授的書和課。
但這種方法會有一個很大的問題,那就是前面提到的維度災(zāi)難,而這里要實現(xiàn)的神經(jīng)網(wǎng)絡(luò)語言模型( Model),便是用神經(jīng)網(wǎng)絡(luò)構(gòu)建語言模型,通過學(xué)習(xí)分布式詞表示(即詞向量)的方式解決了這個問題。
語言模型能干什么
不過在談神經(jīng)網(wǎng)絡(luò)語言模型前,我們先來看看語言模型的用途。
那它有什么用呢,如之前提到,語言模型可以說是現(xiàn)代 NLP 核心之一,無處不在。比如說詞向量,最早算是語言模型的副產(chǎn)品;同時經(jīng)典的序列到序列() 模型,其中解碼器還可以被稱為, Model(條件語言模型);而現(xiàn)在大火的預(yù)訓(xùn)練模型,主要任務(wù)也都是語言模型。
在實際 NLP 應(yīng)用中,我認(rèn)為能總結(jié)成以下三條:
第一,給句子打分,排序。先在大量文本上訓(xùn)練,之后就能用獲得的語言模型來評估某句話的好壞。這在對一些生成結(jié)果進(jìn)行重排序時非常有用,能很大程度地提高指標(biāo),機(jī)器翻譯中有一個技巧便是結(jié)合語言模型 Loss 來重排序生成的候選結(jié)果。
第二,用于文本生成。首先其訓(xùn)練方式是根據(jù)前面詞,生成之后詞。于是只要不斷重復(fù)此過程(自回歸)就能生成長文本了。比較有名的例子就包括最近的 GPT2,其標(biāo)題就叫 “ and Their .” 它生成的句子效果真的非常棒,可以自己體驗一番
第三,作為預(yù)訓(xùn)練模型的預(yù)訓(xùn)練任務(wù)。最近很火的預(yù)訓(xùn)練模型,幾乎都和語言模型脫不開關(guān)系。
比如說 ELMo 就是先訓(xùn)練雙向 LSTM 語言模型,之后雙向不同層向量拼接獲得最后的 ELMo詞向量,還有 BERT 里最主要的方法就是 Model (遮掩語言模型)。
而最近的 XLNet 中最主要訓(xùn)練任務(wù)也叫做 Model (排列語言模型),可見語言模型在其中的重要性重要性。
神經(jīng)網(wǎng)絡(luò)語言模型架構(gòu)
接下來簡單介紹一下這里要實現(xiàn)的網(wǎng)絡(luò)結(jié)構(gòu),借鑒自 的經(jīng)典論文 A Model 中的模型。
這里我們訓(xùn)練 Tri-gram 語言模型,即用前面兩個詞預(yù)測當(dāng)前詞。
于是輸入就是兩個單詞,然后查表取出對應(yīng)詞向量,之后將兩個詞向量拼接起來,過一個線性層,加入 tanh 激活函數(shù),最后再過線性層輸出分?jǐn)?shù),通過 將分?jǐn)?shù)轉(zhuǎn)換成對各個詞預(yù)測的概率,一般取最大概率位置為預(yù)測詞。
用公式表達(dá)整個過程就是:
整個結(jié)構(gòu)非常簡單,接下來就來看看如何用 飛槳來實現(xiàn)這個結(jié)構(gòu)吧,同時介紹以下 飛槳的基本思想,和一般訓(xùn)練流程。
項目地址:
代碼基本實現(xiàn)
這里拿一個小例子來解說,假設(shè)我們在一個叫做 的世界,這個世界的人們只會說三句話,每句話三個詞,我們需要建立一個 Tri-gram 語言模型,來通過一句話前兩個詞預(yù)測下一個詞。
關(guān)于整個流程,主要分成準(zhǔn)備,數(shù)據(jù)預(yù)處理,模型構(gòu)建,訓(xùn)練,保存,預(yù)測幾個階段,這也是一般一個 NLP 任務(wù)的基礎(chǔ)流程。
準(zhǔn)備
首先,先導(dǎo)入需要的庫。
import numpy as np import paddle import paddle.fluid as fluid
之后準(zhǔn)備訓(xùn)練數(shù)據(jù)與詞表,統(tǒng)計所有不同詞,建立詞表,然后按照順序建立一個單詞到 id 的映射表和配套的 id 到單詞映射表。因為模型無法直接讀這些詞,所以需要單詞與 id 之間的轉(zhuǎn)換。
# 假設(shè)在這個叫做Paddle的世界里,人們只會說這三句話 sentences = ["我 喜歡 Paddle", "Paddle 等于 飛槳", "我 會 Paddle"] vocab = set(' '.join(sentences).split(' ')) # 統(tǒng)計詞表 word2idx = {w: i for i, w in enumerate(word_list)} # 建立單詞到id映射表 idx2word = word_list # id到單詞的映射表 n_vocab = len(word2idx) # 詞表大小
準(zhǔn)備好數(shù)據(jù)后,設(shè)置模型參數(shù)和訓(xùn)練相關(guān)參數(shù),因為任務(wù)很簡單,所以參數(shù)都設(shè)很小。
# 參數(shù)設(shè)置 # 語言模型參數(shù) n_step = 2 # 輸入前面多少個詞,tri-gram 所以取 3-1=2 個 n_hidden = 2 # 隱層的單元個數(shù) # 訓(xùn)練參數(shù) n_epochs = 5000 # 訓(xùn)練 epoch 數(shù) word_dim = 2 # 詞向量大小 lr = 0.001 # 學(xué)習(xí)率 use_cuda = False #用不用GPU
數(shù)據(jù)預(yù)處理
根據(jù) 數(shù)據(jù)輸入要求,需要準(zhǔn)備數(shù)據(jù)讀取器 (),之后通過它來讀取數(shù)據(jù),對輸入數(shù)據(jù)進(jìn)行一些前處理,最后作為 batch 輸出。
def sent_reader(): def reader(): batch = [] for sent in sentences: words = sent.split(' ') input_ids = [word2idx[word] for word in words[:-1]] # 將輸入轉(zhuǎn)為id target_id = word2idx[words[-1]] # 目標(biāo)轉(zhuǎn)為id input = np.eye(n_vocab)[input_ids] # 將輸入id轉(zhuǎn)換成one_hot表示 target = np.array([target_id]) batch.append((input, target)) yield batch return reader
構(gòu)建模型
這里從飛槳中較底層 API 來進(jìn)行構(gòu)建,理解更透徹。先創(chuàng)建所需參數(shù)矩陣,之后按照前面的公式來一步步運(yùn)算。
def nnlm(one_hots): # 創(chuàng)建所需參數(shù) # 詞向量表 L = fluid.layers.create_parameter(shape=[n_vocab, word_dim], dtype='float32') # 運(yùn)算所需參數(shù) W1 = fluid.layers.create_parameter(shape=[n_step*word_dim, n_hidden], dtype='float32') b1 = fluid.layers.create_parameter(shape=[n_hidden], dtype='float32', is_bias=True) W2 = fluid.layers.create_parameter(shape=[n_hidden, n_vocab], dtype='float32') b2 = fluid.layers.create_parameter(shape=[n_vocab], dtype='float32', is_bias=True) # 取出詞向量 word_emb = fluid.layers.matmul(one_hots, L) # 兩個詞向量拼接 input = fluid.layers.reshape(x=word_emb, shape=[-1, n_step*word_dim], inplace=True) # 前向運(yùn)算 input2hid = fluid.layers.tanh(fluid.layers.matmul(input, W1) + b1) # 輸入到隱層 hid2out = fluid.layers.softmax(fluid.layers.matmul(input2hid, W2) + b2) # 隱層到輸出 return hid2out
先根據(jù)輸入的獨(dú)熱(one-hot)向量,取出對應(yīng)的詞向量,因為每個例子輸入前兩個詞,因此每個例子可獲得兩個詞向量,之后按照步驟,將它們拼接起來,然后與 W1 和 b1 進(jìn)行運(yùn)算,過 tanh 非線性,最后再拿結(jié)果與 W2 和 b2 進(jìn)行運(yùn)算, 輸出結(jié)果。
接下來構(gòu)建損失函數(shù),我們用常用的交叉熵(cross-)損失函數(shù),直接調(diào) API。
def ce_loss(softmax, target): cost = fluid.layers.cross_entropy(input=softmax, label=target) # 計算每個batch的損失 avg_cost = fluid.layers.mean(cost) # 平均 return avg_cost
開始訓(xùn)練
終于進(jìn)入了訓(xùn)練環(huán)節(jié),不過為了更好理解,先稍稍介紹一點(diǎn) 飛槳的設(shè)計思想。
飛槳同時為用戶提供動態(tài)圖和靜態(tài)圖兩種計算圖。動態(tài)圖組網(wǎng)更加靈活、調(diào)試網(wǎng)絡(luò)便捷,實現(xiàn)AI 想法更快速;靜態(tài)圖部署方便、運(yùn)行速度快,應(yīng)用落地更高效。
如果想了解飛槳動態(tài)圖更多內(nèi)容,可以參考項目地址:
實際應(yīng)用中,靜態(tài)圖更為常見,下面我們以靜態(tài)圖為例介紹一個完整的實現(xiàn):
首先,需要先定義 ,整個 中包括了各種網(wǎng)絡(luò)定義,操作等等,定義完之后,再創(chuàng)建一個 來運(yùn)行 ,用過類似框架的同學(xué)應(yīng)該并不陌生。
因此先來看看這兩行代碼,fluid 中最重要的兩個 ,將它們?nèi)〕鰜怼?/p>
startup_program = fluid.default_startup_program() # 默認(rèn)啟動程序 main_program = fluid.default_main_program() # 默認(rèn)主程序
ram 主要定義了輸入輸出,創(chuàng)建模型參數(shù),還有可學(xué)習(xí)參數(shù)的初始化;而 則是定義了神經(jīng)網(wǎng)絡(luò)模型,前向反向,還有優(yōu)化算法的更新。
之后將之前定義好的一些模塊放入訓(xùn)練代碼中。
train_reader = sent_reader() # 獲取數(shù)據(jù) reader # 定義輸入和目標(biāo)數(shù)據(jù) input = fluid.layers.data(name='input', shape=[-1, n_step, n_vocab], dtype='float32') target = fluid.layers.data(name='target', shape=[-1, 1], dtype='int64') # 輸入到模型,獲得 loss softmax = nnlm(input) loss = ce_loss(softmax, target) 之后還需要定義優(yōu)化器(Optimizer),還有數(shù)據(jù) Feeder 用于喂入數(shù)據(jù)。 # 配置優(yōu)化器 optimizer = fluid.optimizer.Adam(learning_rate=0.001) # 萬金油的 Adam optimizer.minimize(loss) # 用于之后預(yù)測 prediction = fluid.layers.argmax(softmax, axis=-1) # 定義 Executor place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace() # 指定運(yùn)行位置 exe = fluid.Executor(place) #定義數(shù)據(jù) Feeder feeder = fluid.DataFeeder(feed_list=[input, target], place=place) # 每次喂入input和target 至此就完成了第一步的定義環(huán)節(jié),然后就可以用定義的 Executor 來執(zhí)行程序了。 # 參數(shù)初始化 exe.run(startup_program) # 訓(xùn)練 for epoch in range(n_epochs): for data in train_reader(): metrics = exe.run( main_program, # 主程序 feed=feeder.feed(data), # 數(shù)據(jù)喂入 fetch_list=[loss]) # 要取出的數(shù)據(jù) if epoch % 500 == 0: print("Epoch {}, Cost {:.5f}".format(epoch, step, float(metrics[0][0])))
簡單解釋一下代碼,訓(xùn)練時需要exe.run來執(zhí)行每一步的訓(xùn)練,對于run需要傳入主程序,還有輸入 ,和需要拿出來(fetch)的輸出。
之后運(yùn)行就能看到訓(xùn)練 log 了。
能明顯看到 loss 在不斷下降,等訓(xùn)練完成,我們就獲得一個訓(xùn)練好的模型。
保存模型
在預(yù)測前可以嘗試先保存一個模型,可以便于之后使用,比如 load 出來做預(yù)測。
fluid.io.save_inference_model('./model', ['input'], [prediction], exe)
很簡單,只需要傳入保存的路徑’./model’,預(yù)測需要 feed 的數(shù)據(jù)’input’,之后需要 fetch 出的預(yù)測結(jié)果 ,最后加上執(zhí)行器 exe,就 OK 了。
非常快。
預(yù)測階段
預(yù)測階段其實和訓(xùn)練階段類似,但因為主程序都保存下來了,所以只用先建立執(zhí)行器 ,同時建立一個用于預(yù)測的作用域。
infer_exe = fluid.Executor(place) # 預(yù)測 Executor inference_scope = fluid.core.Scope() # 預(yù)測作用域
然后在預(yù)測作用域中 load 出模型,進(jìn)行預(yù)測運(yùn)算,大部分操作都和訓(xùn)練很類似了。唯一不同就是 load 模型這塊,其實就是把之前保存下來的參數(shù)給 load 出來了,然后用于預(yù)測。
with fluid.scope_guard(inference_scope): [inference_program, feed_target_names, fetch_targets] = fluid.io.load_inference_model('./model', infer_exe) # 載入預(yù)訓(xùn)練模型 infer_reader = sent_reader() # 定義預(yù)測數(shù)據(jù) reader infer_data = next(infer_reader()) # 讀出數(shù)據(jù) infer_feat = np.array([data[0] for data in infer_data]).astype("float32") assert feed_target_names[0] == 'input' results = infer_exe.run(inference_program, feed={feed_target_names[0]: infer_feat}, fetch_list=fetch_targets) # 進(jìn)行預(yù)測
結(jié)果如何?
for sent, idx in zip(sentences, results[0]): print("{} -> {}".format(' '.join(sent.split()[:2]), idx2word[idx])) 我 喜歡 -> Paddle Paddle 等于 -> 飛槳 我 會 -> Paddle
模型完美地學(xué)習(xí)到了 世界中僅有的幾個 規(guī)則,當(dāng)然因為該任務(wù)非常簡單,所以模型一下就能學(xué)會。
更多嘗試
在了解完以上這個小例子之后,就能在它基礎(chǔ)上做很多修改了,感興趣的同學(xué)不妨拿下面的幾個思路作為練習(xí)。
比如說用一個大數(shù)據(jù)集,加上更大模型神經(jīng)網(wǎng)絡(luò)設(shè)計方法與實例分析神經(jīng)網(wǎng)絡(luò)設(shè)計方法與實例分析,來進(jìn)行訓(xùn)練,可以嘗試復(fù)現(xiàn) 論文中的模型規(guī)模,大致結(jié)構(gòu)差不多,只是修改一下參數(shù)大小。
還比如說,在這里搭建網(wǎng)絡(luò)結(jié)構(gòu)時,用的是較底層API,直接創(chuàng)建矩陣權(quán)重,相乘相加,而 飛槳中有很多好用的API,能否調(diào)用這些API來重新構(gòu)建這個模型呢,比如說詞向量部分,可以用fluid..直接傳入詞 id 來實現(xiàn),還有全連接層,可以直接用 fluid..fc 來實現(xiàn),激活函數(shù)可以直接通過里面參數(shù)設(shè)置,非常方便。
其實還可以在這里嘗試些小技巧,比如共享詞向量表為 前全連接層的權(quán)重 W2,以及加入 論文中提到的類似殘差連接直接將 連到輸出的部分。
這次在這里介紹神經(jīng)網(wǎng)絡(luò)語言模型,并通過 飛槳來實現(xiàn)了一個簡單的小例子,主要想做的是:
第一,語言模型任務(wù)在 NLP 領(lǐng)域很重要,想首先介紹一下;
第二, 這篇神經(jīng)網(wǎng)絡(luò)語言模型的論文非常經(jīng)典,比如說提出了用神經(jīng)網(wǎng)絡(luò)實現(xiàn)語言模型,同時還最早提出詞表示來解決“維數(shù)災(zāi)難”問題,通過復(fù)現(xiàn),也好引出之后詞向量,還有 等話題;
第三,通過用 飛槳來實現(xiàn)這樣一個簡單例子,可以拋開各種模型與數(shù)據(jù)復(fù)雜度,更直觀了解一個飛槳程序是如何構(gòu)建的,也為之后講解飛槳更復(fù)雜程序打下基礎(chǔ)。