自然語言生成(Natural Language Generation,簡稱NLG ),是自然語言處理領域的一個重要分支,在文本摘要生成任務中,另一個重要的分支是自然語言理解(Natural Language Understanding,簡稱NLU )。前面我們已經學習了seq2seq 模型結構,其主要分為Encoder和Decoder兩大組件,其實正是對應了NLU 和NLG 兩大分支,seq2seq 模型最后經過一個softmax 層,在每個時間步均得到一個詞表大小的概率分布,如何利用這些概率分布得到最終的預測句子就是本節(jié)學習的解碼策略。
上篇構建文本摘要baseline時,我們就有提到過解碼方法,當時采用的Teacher forcing的技巧,使用了真實標簽,避免前面的步驟預測出錯被無限放大的問題,但是在實際預測時因為沒有真實標簽,往往實際預測效果不一定好,所以尋找可行的解碼策略是一項重點工作。
暴力解法
seq2seq 模型最后經過一個softmax 層,在每個時間步均得到一個詞表大小的概率分布,但是這些時間步的概率分布并非同一時間得到的,后面時間步的概率分布生成受前面時間步的概率分布的影響,還記得在seq2seq 模型中decoder部分,上一時間步的輸出會作為輸入的一部分進入下一個時間步,所以并不是說直接取每個時間步概率最大的詞就得到了最佳的預測結果(雖然greedy search是這樣做的)。這種情況下,我們常想到的辦法就是把每個時間步,每個預測詞都作為一種可能性,然后不斷地去生成后面的詞,最終計算每個句子總概率值(分值score),選擇概率最大的作為預測結果。

這種方法也是我們刷Leetcode ,沒有好的方法時常采用的暴力解法,缺點就是時間復雜度太高,就像這里,每個時間步有V種可能性(V表示詞表大?。还睺個時間步,總共有V的T次方種可能性,計算量太大,可行性差。
Greedy Search

greedy search的想法就比較簡單了,也就是我上面說的,直接選取每個時間步的最大概率的詞,但是有個缺點就是前面的選錯了會影響后面的,導致錯誤被一直傳遞下去,但是依然是一種簡單可行的方法,下面通過一段模擬模型生成的代碼來實現(xiàn)greedy search。
import numpy as np
import matplotlib.pyplot as plt
# 定義詞典(就是26個英文字母)
dictionary = []
for c in range(ord('a'), ord('z') 1):
dictionary.append(chr(c))
print(f'詞典:{dictionary}')
# 模擬一個已經被訓練好的LM
class LanguageModel:
def __init__(self, dictionary):
self.dictionary = dictionary
def predict(self):
output = np.random.rand(len(dictionary))
output = output/output.sum()
return output
model = LanguageModel(dictionary)
predictions = model.predict()
plt.bar(dictionary, predictions)
plt.show()
def greedy_search(conditional_probability):
return np.argmax(conditional_probability)
next_token = greedy_search(predictions)
print('sample token: ', dictionary[next_token])
輸出:
詞典:['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
sample token: w
當模型生成<END>標志時,通常表示解碼結束。
Beam Search
greedy search中因為只選取一個最大的概率詞,那么生成的結果會比較單一,為了讓生成的結果更多樣性,基于greedy search有一種更好的方法就是在每個時間步選取概率分布最大的n個詞(n值一般取5~10),俗稱top-n(注意與后續(xù)的top-k sampling做區(qū)分)。
Beam search的具體流程:
  
重復以上步驟,直至最終生成了k個句子,然后選取一個分數(shù)值最高的句子作為最終輸出。

停止條件
其實與greedy search中類似,當模型在某個時間步輸出<END>時,表示該假設已經結束,但是因為不同的假設的終止時間步可能不一致,所以當某個假設結束時,不會影響其他假設,其他假設會繼續(xù)生成。
但是存在極低的可能性模型一直無法輸出<END>,所以一般會設置超參-最大的時間步,也就是達到最大時間步后該假設自動終止;同樣地,也會設置超參要生成多少個假設,生成的假設數(shù)量夠了就不再生成新的假設。
代碼實現(xiàn)
同樣使用上面的模擬詞典和模擬概率
def beam_search_decoder(data, k):
sequences = [[list(), 0.0]]
# 迭代序列中的每一步
for row in data:
all_candidates = list()
# 計算每種hypotheses的分值,并存儲到all_candidates
for i in range(len(sequences)):
seq, score = sequences[i]
for j in range(len(row)):
candidate = [seq [j], score - np.log(row[j])]
all_candidates.append(candidate)
# 對所有的候選序列,通過score排序
ordered = sorted(all_candidates, key=lambda tup: tup[1])
# 選擇k個分score 最高的
sequences = ordered[:k]
return sequences
t = 10 # 總共10個時間步
data = []
for i in range(t):
prediction = model.predict()
data.append(prediction)
data = np.array(data)
result = beam_search_decoder(data, 5)
for seq in result:
print(seq)
[[13, 21, 16, 11, 20, 7, 14, 7, 16, 2], 25.78893247277384]
[[13, 21, 16, 11, 20, 7, 14, 16, 16, 2], 25.792550613854544]
[[21, 21, 16, 11, 20, 7, 14, 7, 16, 2], 25.792704168774012]
[[21, 21, 16, 11, 20, 7, 14, 16, 16, 2], 25.796322309854716]
[[13, 21, 16, 11, 20, 7, 14, 7, 11, 2], 25.81314273033993]
k的選擇
當k是一個比較小的值時,生成的句子可能會不符合語法規(guī)則,不自然,無意義,不正確;
特殊地,當k=1時,變成greedy search
當k是一個比較大的值時,可以減少上述問題,但是會增加大量的運算;但是還有帶來另外的問題:
- 對于機器翻譯任務來說,如果k增大的過多,
BLEU score 下降的特別快,主要是因為k越大,生成的句子會越短(句子短的會得分高); - 在閑聊對話系統(tǒng)中(chit-chat),越大的k約會偏離主題,雖然生成的句子確實會更通順,(看下圖)。

所以K的選取對結果影響很大,需要根據(jù)自己的任務進行實驗。
Penalize longer sequences

觀察分值score的計算公式,首先P是一個小于1的值,那么log§就是一個負值,所以我們會發(fā)現(xiàn),生成的句子越長,得分就會越小,這明顯不是我們期望看到的,可以通過以下兩種方式解決。

其中第二種方法在這篇論文中被提出。
Repetitive Problem
在文本生成領域,重復生成的問題是一個非常常見的問題,有研究發(fā)現(xiàn),當生成重復的句子時,損失值會不斷減小,一般有以下三種方法來解決重復的問題:
-
通過寫代碼來控制模型減少生成重復詞,剛開始可以嘗試此種方法,鍛煉編碼能力; -
更改loss,通過增加額外的損失來降低已生成句子(ht-m)和后續(xù)生成句子(ht)的相似度

- 不使用基于極大似然的損失函數(shù),這篇論文中有詳細描述

- F2 softmax,源自這篇文章,主要是先根據(jù)頻率將詞表分組,然后使用兩次softmax來選詞,即先確定從哪一組選詞,再確定選取該組中哪個詞。


|