本文將使用 Python 實(shí)現(xiàn)和對比解釋 NLP中的3 種不同文本摘要策略:老式的 TextRank(使用 gensim)、著名的 Seq2Seq(使基于 tensorflow)和最前沿的 BART(使用Transformers )。
NLP(自然語言處理)是人工智能領(lǐng)域,研究計(jì)算機(jī)與人類語言之間的交互,特別是如何對計(jì)算機(jī)進(jìn)行編程以處理和分析大量自然語言數(shù)據(jù)。最難的 NLP 任務(wù)是輸出不是單個(gè)標(biāo)簽或值(如分類和回歸),而是完整的新文本(如翻譯、摘要和對話)的任務(wù)。
文本摘要是在不改變其含義的情況下減少文檔的句子和單詞數(shù)量的問題。有很多不同的技術(shù)可以從原始文本數(shù)據(jù)中提取信息并將其用于摘要模型,總體來說它們可以分為提取式(Extractive)和抽象式(Abstractive)。提取方法選擇文本中最重要的句子(不一定理解含義),因此作為結(jié)果的摘要只是全文的一個(gè)子集。而抽象模型使用高級 NLP(即詞嵌入)來理解文本的語義并生成有意義的摘要。抽象技術(shù)很難從頭開始訓(xùn)練,因?yàn)樗鼈冃枰罅繀?shù)和數(shù)據(jù),所以一般情況下都是用與訓(xùn)練的嵌入進(jìn)行微調(diào)。
本文比較了 TextRank(Extractive)的老派方法、流行的編碼器-解碼器神經(jīng)網(wǎng)絡(luò) Seq2Seq(Abstractive)以及徹底改變 NLP 領(lǐng)域的最先進(jìn)的基于注意力的 Transformers(Abstractive)。
本文將使用“CNN DailyMail”數(shù)據(jù)集,包含了數(shù)千篇由 CNN 和《每日郵報(bào)》的記者用英語撰寫的新聞文章,以及每篇文章的摘要,數(shù)據(jù)集和本文的代碼也都會(huì)在本文末尾提供。
首先,我需要導(dǎo)入以下庫:
## for dataimport datasets #(1.13.3)import pandas as pd #(0.25.1)import numpy #(1.16.4)## for plottingimport matplotlib.pyplot as plt #(3.1.2)import seaborn as sns #(0.9.0)## for preprocessingimport reimport nltk #(3.4.5)import contractions #(0.0.18)## for textrankimport gensim #(3.8.1)## for evaluationimport rouge #(1.0.0)import difflib## for seq2seqfrom tensorflow.keras import callbacks, models, layers, preprocessing as kprocessing #(2.6.0)## for bartimport transformers #(3.0.1)
然后我使用 HuggingFace 的加載數(shù)據(jù)集:
## load the full dataset of 300k articlesdataset = datasets.load_dataset('cnn_dailymail', '3.0.0')lst_dics = [dic for dic in dataset['train']]## keep the first N articles if you want to keep it lite dtf = pd.DataFrame(lst_dics).rename(columns={'article':'text', 'highlights':'y'})[['text','y']].head(20000)dtf.head()
讓我們檢查一個(gè)隨機(jī)的樣本:
i = 1print('--- Full text ---')print(dtf['text'][i])print('--- Summary ---')print(dtf['y'][i])
在上圖中,我將摘要中提到的信息手動(dòng)標(biāo)記為紅色。 體育文章對機(jī)器來說是非常困難的,因?yàn)闃?biāo)題需要在有限的字符限制的情況下突出主要結(jié)果。 這個(gè)實(shí)例可能是一個(gè)非常好的例子,我會(huì)將這個(gè)示例保留在測試集中以比較模型。
dtf_train = dtf.iloc[i+1:]dtf_test = dtf.iloc[:i+1]
TextRank (2004) 是一種基于圖的文本處理排名模型,基于 Google 的 PageRank 算法,可在文本中找到最相關(guān)的句子。 PageRank 是 1998 年 Google 搜索引擎使用的第一個(gè)對網(wǎng)頁進(jìn)行排序的算法。簡而言之,如果頁面 A 鏈接到頁面 B,頁面 C,頁面 B 鏈接到頁面 C,那么排序?qū)⑹琼撁?C,頁面 B,頁面 A。
TextRank 非常易于使用,因?yàn)樗菬o監(jiān)督的。 首先,將整個(gè)文本拆分為句子,然后算法會(huì)使用其中句子作為節(jié)點(diǎn),重疊的單詞作為連接,構(gòu)建一個(gè)圖,通過PageRank 確定了這個(gè)句子網(wǎng)絡(luò)中最重要的節(jié)點(diǎn)。
這里使用 gensim 庫的內(nèi)置TextRank 算法實(shí)現(xiàn):
def textrank(corpus, ratio=0.2): if type(corpus) is str: corpus = [corpus] lst_summaries = [gensim.summarization.summarize(txt, ratio=ratio) for txt in corpus] return lst_summariespredicted = textrank(corpus=dtf_test['text'], ratio=0.2)predicted[i]
我們?nèi)绾卧u估這個(gè)結(jié)果? 通常兩種方式:
1、ROUGE 指標(biāo)(Recall-Oriented Understudy for Gisting Evaluation):通過重疊 n-gram 將自動(dòng)生成的摘要與參考摘要進(jìn)行比較。
def evaluate_summary(y_test, predicted): rouge_score = rouge.Rouge() scores = rouge_score.get_scores(y_test, predicted, avg=True) score_1 = round(scores['rouge-1']['f'], 2) score_2 = round(scores['rouge-2']['f'], 2) score_L = round(scores['rouge-l']['f'], 2) print('rouge1:', score_1, '| rouge2:', score_2, '| rougeL:',score_2, '--> avg rouge:', round(np.mean([score_1,score_2,score_L]), 2))## Apply the function to predictedi = 5evaluate_summary(dtf_test['y'][i], predicted[i])
結(jié)果表明,31% 的ROUGE-1 和 7% 的ROUGE-2出現(xiàn)在兩個(gè)摘要中,而最長的公共子序列 (ROUGE-L) 匹配了 7%。 總體而言,平均得分為 20%。 這里需要說明的是ROUGE 分?jǐn)?shù)并不能衡量摘要的流暢程度,因?yàn)閷τ诹鲿吵潭葋碚f我們通常使用人肉判斷。
2、可視化:顯示2個(gè)文本,即摘要和原文或預(yù)測摘要和真實(shí)摘要,并突出匹配部分
#Find the matching substrings in 2 strings.def utils_split_sentences(a, b):## find clean matchesmatch = difflib.SequenceMatcher(isjunk=None, a=a, b=b, autojunk=True)lst_match = [block for block in match.get_matching_blocks() if block.size > 20]## difflib didn't find any matchif len(lst_match) == 0:lst_a, lst_b = nltk.sent_tokenize(a), nltk.sent_tokenize(b)## work with matcheselse:first_m, last_m = lst_match[0], lst_match[-1]### astring = a[0 : first_m.a]lst_a = [t for t in nltk.sent_tokenize(string)]for n in range(len(lst_match)):m = lst_match[n]string = a[m.a : m.a+m.size]lst_a.append(string)if n+1 < len(lst_match):next_m = lst_match[n+1]string = a[m.a+m.size : next_m.a]lst_a = lst_a + [t for t in nltk.sent_tokenize(string)]else:breakstring = a[last_m.a+last_m.size :]lst_a = lst_a + [t for t in nltk.sent_tokenize(string)]### bstring = b[0 : first_m.b]lst_b = [t for t in nltk.sent_tokenize(string)]for n in range(len(lst_match)):m = lst_match[n]string = b[m.b : m.b+m.size]lst_b.append(string)if n+1 < len(lst_match):next_m = lst_match[n+1]string = b[m.b+m.size : next_m.b]lst_b = lst_b + [t for t in nltk.sent_tokenize(string)]else:breakstring = b[last_m.b+last_m.size :]lst_b = lst_b + [t for t in nltk.sent_tokenize(string)]return lst_a, lst_b#Highlights the matched strings in text.def display_string_matching(a, b, both=True, sentences=True, titles=[]):if sentences is True:lst_a, lst_b = utils_split_sentences(a, b)else:lst_a, lst_b = a.split(), b.split() ## highlight afirst_text = []for i in lst_a:if re.sub(r'[^\w\s]', '', i.lower()) in [re.sub(r'[^\w\s]', '', z.lower()) for z in lst_b]:first_text.append('<span style='background-color:rgba(255,215,0,0.3);'>' + i + '</span>')else:first_text.append(i)first_text = ' '.join(first_text)## highlight bsecond_text = []if both is True:for i in lst_b:if re.sub(r'[^\w\s]', '', i.lower()) in [re.sub(r'[^\w\s]', '', z.lower()) for z in lst_a]:second_text.append('<span style='background-color:rgba(255,215,0,0.3);'>' + i + '</span>')else:second_text.append(i)else:second_text.append(b) second_text = ' '.join(second_text)## concatenateif len(titles) > 0:first_text = '<strong>'+titles[0]+'</strong><br>'+first_textif len(titles) > 1:second_text = '<strong>'+titles[1]+'</strong><br>'+second_textelse:second_text = '---'*65+'<br><br>'+second_textfinal_text = first_text +'<br><br>'+ second_textreturn final_text
你會(huì)發(fā)現(xiàn)這個(gè)函數(shù)非常有用,尤其是對于需要我們?nèi)巳馀袛嗟臅r(shí)候,因?yàn)樗鼤?huì)突出顯示兩個(gè)文本的匹配子字符串, 并且是單詞級別的:
match = display_string_matching(dtf_test['y'][i], predicted[i], both=True, sentences=False, titles=['Real Summary', 'Predicted Summary'])from IPython.core.display import display, HTMLdisplay(HTML(match))
或者可以設(shè)置sentences=True,匹配句子級別的文本而不是單詞級別的文本:
match = display_string_matching(dtf_test['text'][i], predicted[i], both=True, sentences=True, titles=['Full Text', 'Predicted Summary'])from IPython.core.display import display, HTMLdisplay(HTML(match))
可以看到預(yù)測包含原始摘要中提到的大部分信息。 正如提取算法所期望的那樣,預(yù)測的摘要完全包含在文本中:模型認(rèn)為這 3 個(gè)句子是最重要的。 我們可以將此作為下面更為先進(jìn)方法的基線。
序列到序列模型(2014)是一種神經(jīng)網(wǎng)絡(luò)的架構(gòu),它以來自一個(gè)域(即文本詞匯表)的序列作為輸入并輸出另一個(gè)域(即摘要詞匯表)中的新序列。 Seq2Seq 模型通常具有以下關(guān)鍵特征:
由于我們要將文本轉(zhuǎn)換為單詞序列,因此我們必須要對數(shù)據(jù)進(jìn)行處理:
下面將清理和分析數(shù)據(jù)以解決這兩個(gè)問題。
## create stopwordslst_stopwords = nltk.corpus.stopwords.words('english')## add words that are too frequentlst_stopwords = lst_stopwords + ['cnn','say','said','new']## cleaning functiondef utils_preprocess_text(txt, punkt=True, lower=True, slang=True, lst_stopwords=None, stemm=False, lemm=True):### separate sentences with '. 'txt = re.sub(r'\.(?=[^ \W\d])', '. ', str(txt))### remove punctuations and characterstxt = re.sub(r'[^\w\s]', '', txt) if punkt is True else txt### striptxt = ' '.join([word.strip() for word in txt.split()])### lowercasetxt = txt.lower() if lower is True else txt### slangtxt = contractions.fix(txt) if slang is True else txt ### tokenize (convert from string to list)lst_txt = txt.split()### stemming (remove -ing, -ly, ...)if stemm is True:ps = nltk.stem.porter.PorterStemmer()lst_txt = [ps.stem(word) for word in lst_txt]### lemmatization (convert the word into root word)if lemm is True:lem = nltk.stem.wordnet.WordNetLemmatizer()lst_txt = [lem.lemmatize(word) for word in lst_txt]### remove Stopwordsif lst_stopwords is not None:lst_txt = [word for word in lst_txt if word not in lst_stopwords]### back to stringtxt = ' '.join(lst_txt)return txt## apply function to both text and summariesdtf_train['text_clean'] = dtf_train['text'].apply(lambda x: utils_preprocess_text(x, punkt=True, lower=True, slang=True, lst_stopwords=lst_stopwords, stemm=False, lemm=True))dtf_train['y_clean'] = dtf_train['y'].apply(lambda x: utils_preprocess_text(x, punkt=True, lower=True, slang=True, lst_stopwords=lst_stopwords, stemm=False, lemm=True))
現(xiàn)在我們來看看長度分布:
## countdtf_train['word_count'] = dtf_train[column].apply(lambda x: len(nltk.word_tokenize(str(x))) )## plotsns.distplot(dtf_train['word_count'], hist=True, kde=True, kde_kws={'shade':True})
X_len = 400y_len = 40
我們來分析詞頻:
lst_tokens = nltk.tokenize.word_tokenize(dtf_train['text_clean'].str.cat(sep=' '))ngrams = [1]## calculatedtf_freq = pd.DataFrame()for n in ngrams:dic_words_freq = nltk.FreqDist(nltk.ngrams(lst_tokens, n))dtf_n = pd.DataFrame(dic_words_freq.most_common(), columns=['word','freq'])dtf_n['ngrams'] = ndtf_freq = dtf_freq.append(dtf_n)dtf_freq['word'] = dtf_freq['word'].apply(lambda x: ' '.join(string for string in x) )dtf_freq_X= dtf_freq.sort_values(['ngrams','freq'], ascending=[True,False])## plotsns.barplot(x='freq', y='word', hue='ngrams', dodge=False,data=dtf_freq.groupby('ngrams')['ngrams','freq','word'].head(30))plt.show()
thres = 5 #<-- min frequencyX_top_words = len(dtf_freq_X[dtf_freq_X['freq']>thres])y_top_words = len(dtf_freq_y[dtf_freq_y['freq']>thres])
這樣就ok了,下面將通過使用 tensorflow/keras 將預(yù)處理的語料庫轉(zhuǎn)換為序列列表來創(chuàng)建特征矩陣:
lst_corpus = dtf_train['text_clean']## tokenize texttokenizer = kprocessing.text.Tokenizer(num_words=X_top_words, lower=False, split=' ', oov_token=None, filters='!'#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n')tokenizer.fit_on_texts(lst_corpus)dic_vocabulary = {'<PAD>':0}dic_vocabulary.update(tokenizer.word_index)## create sequencelst_text2seq= tokenizer.texts_to_sequences(lst_corpus)## padding sequenceX_train = kprocessing.sequence.pad_sequences(lst_text2seq, maxlen=15, padding='post', truncating='post')
特征矩陣 X_train 具有 N 個(gè)文檔 x 序列最大長度的形狀。 讓我們可視化一下:
sns.heatmap(X_train==0, vmin=0, vmax=1, cbar=False)plt.show()
除此以外,還需要對測試集進(jìn)行相同的特征工程:
## text to sequence with the fitted tokenizerlst_text2seq = tokenizer.texts_to_sequences(dtf_test['text_clean'])## padding sequenceX_test = kprocessing.sequence.pad_sequences(lst_text2seq, maxlen=15,padding='post', truncating='post')
現(xiàn)在就可以處理摘要了。 在應(yīng)用相同的特征工程策略之前,需要在每個(gè)摘要中添加兩個(gè)特殊標(biāo)記,以確定文本的開頭和結(jié)尾。
# Add START and END tokens to the summaries (y)special_tokens = ('<START>', '<END>')dtf_train['y_clean'] = dtf_train['y_clean'].apply(lambda x: special_tokens[0]+' '+x+' '+special_tokens[1])dtf_test['y_clean'] = dtf_test['y_clean'].apply(lambda x: special_tokens[0]+' '+x+' '+special_tokens[1])# check exampledtf_test['y_clean'][i]
現(xiàn)在我們可以通過利用與以前相同的代碼創(chuàng)建摘要的特征矩陣。預(yù)測時(shí)將使用開始標(biāo)記開始預(yù)測,當(dāng)結(jié)束標(biāo)記出現(xiàn)時(shí),預(yù)測文本將停止。
對于詞嵌入這里有 2 個(gè)選項(xiàng):從頭開始訓(xùn)練我們的詞嵌入模型或使用預(yù)訓(xùn)練的模型。 在 Python 中,可以 genism-data 加載預(yù)訓(xùn)練的 Word Embedding 模型:
import gensim_apinlp = gensim_api.load('glove-wiki-gigaword-300')
這里推薦使用斯坦福的 GloVe,這是一種在 Wikipedia、Gigaword 和 Twitter 語料庫上訓(xùn)練的無監(jiān)督學(xué)習(xí)算法。 可以將任何單詞轉(zhuǎn)換為向量:
word = 'home'nlp[word].shape>>> (300,)
這些詞向量可以在神經(jīng)網(wǎng)絡(luò)中用作權(quán)重。 為了做到這一點(diǎn),我們需要?jiǎng)?chuàng)建一個(gè)嵌入矩陣,以便 id N 的單詞的向量位于第 N 行。
## start the matrix (length of vocabulary x vector size) with all 0sX_embeddings = np.zeros((len(X_dic_vocabulary)+1, 300))for word,idx in X_dic_vocabulary.items():## update the row with vectortry:X_embeddings[idx] = nlp[word]## if word not in model then skip and the row stays all 0sexcept:pass
上面的代碼生成從語料庫中提取的詞嵌入的矩陣。 語料庫矩陣應(yīng)會(huì)在編碼器嵌入層中使用,而摘要矩陣會(huì)在解碼器層中使用。 輸入序列中的每個(gè) id 都將用作訪問嵌入矩陣的索引。 該嵌入層的輸出將是一個(gè) 2D 矩陣,其中輸入序列中的每個(gè)詞 id 都有一個(gè)詞向量(序列長度 x 向量大?。?/p>
下面就是構(gòu)建編碼器-解碼器模型的時(shí)候了。 首先,我們需要確認(rèn)正確的輸入和輸出:
將輸入文本提供給編碼器以了解上下文,然后向解碼器展示摘要如何開始,模型將會(huì)學(xué)習(xí)預(yù)測摘要如何結(jié)束。 這是一種稱為“teacher forcing”的訓(xùn)練策略,它使用目標(biāo)而不是網(wǎng)絡(luò)生成的輸出,以便它可以學(xué)習(xí)預(yù)測開始標(biāo)記之后的單詞,然后是下一個(gè)單詞,依此類推。
本文將提出兩個(gè)不同版本的 Seq2Seq,下面是我們使用的最簡單的版本:
一個(gè)嵌入層,它將從頭開始創(chuàng)建一個(gè)詞嵌入。一個(gè)單向 LSTM 層,它返回一個(gè)序列以及單元狀態(tài)和隱藏狀態(tài)
最后一個(gè)Time Distributed Dense layer,它一次將相同的密集層(相同的權(quán)重)應(yīng)用于 LSTM 輸出,每次一個(gè)時(shí)間步長,這樣輸出層只需要一個(gè)與每個(gè) LSTM 單元的連接。
lstm_units = 250embeddings_size = 300##------------ ENCODER (embedding + lstm) ------------------------##x_in = layers.Input(name='x_in', shape=(X_train.shape[1],))### embeddinglayer_x_emb = layers.Embedding(name='x_emb', input_dim=len(X_dic_vocabulary),output_dim=embeddings_size, trainable=True)x_emb = layer_x_emb(x_in)### lstm layer_x_lstm = layers.LSTM(name='x_lstm', units=lstm_units, dropout=0.4, return_sequences=True, return_state=True)x_out, state_h, state_c = layer_x_lstm(x_emb)##------------ DECODER (embedding + lstm + dense) ----------------##y_in = layers.Input(name='y_in', shape=(None,))### embeddinglayer_y_emb = layers.Embedding(name='y_emb', input_dim=len(y_dic_vocabulary), output_dim=embeddings_size, trainable=True)y_emb = layer_y_emb(y_in)### lstm layer_y_lstm = layers.LSTM(name='y_lstm', units=lstm_units, dropout=0.4, return_sequences=True, return_state=True)y_out, _, _ = layer_y_lstm(y_emb, initial_state=[state_h, state_c])### final dense layerslayer_dense = layers.TimeDistributed(name='dense', layer=layers.Dense(units=len(y_dic_vocabulary), activation='softmax'))y_out = layer_dense(y_out)##---------------------------- COMPILE ---------------------------##model = models.Model(inputs=[x_in, y_in], outputs=y_out, name='Seq2Seq')model.compile(optimizer='rmsprop',loss='sparse_categorical_crossentropy', metrics=['accuracy'])model.summary()
如果你覺得上面的代碼比較簡單,以下是 Seq2Seq 算法的高級(并且非常重)版本:
嵌入層,利用 GloVe 的預(yù)訓(xùn)練權(quán)重。3 個(gè)雙向 LSTM 層,在兩個(gè)方向上處理序列。最后的Time Distributed Dense layer(與之前相同)
lstm_units = 250##-------- ENCODER (pre-trained embeddings + 3 bi-lstm) ----------##x_in = layers.Input(name='x_in', shape=(X_train.shape[1],))### embeddinglayer_x_emb = layers.Embedding(name='x_emb', input_dim=X_embeddings.shape[0], output_dim=X_embeddings.shape[1], weights=[X_embeddings], trainable=False)x_emb = layer_x_emb(x_in)### bi-lstm 1layer_x_bilstm = layers.Bidirectional(layers.LSTM(units=lstm_units, dropout=0.2, return_sequences=True, return_state=True), name='x_lstm_1')x_out, _, _, _, _ = layer_x_bilstm(x_emb)### bi-lstm 2layer_x_bilstm = layers.Bidirectional(layers.LSTM(units=lstm_units, dropout=0.2, return_sequences=True, return_state=True), name='x_lstm_2')x_out, _, _, _, _ = layer_x_bilstm(x_out)### bi-lstm 3 (here final states are collected)layer_x_bilstm = layers.Bidirectional(layers.LSTM(units=lstm_units, dropout=0.2, return_sequences=True, return_state=True), name='x_lstm_3')x_out, forward_h, forward_c, backward_h, backward_c = layer_x_bilstm(x_out)state_h = layers.Concatenate()([forward_h, backward_h])state_c = layers.Concatenate()([forward_c, backward_c])##------ DECODER (pre-trained embeddings + lstm + dense) ---------##y_in = layers.Input(name='y_in', shape=(None,))### embeddinglayer_y_emb = layers.Embedding(name='y_emb', input_dim=y_embeddings.shape[0], output_dim=y_embeddings.shape[1], weights=[y_embeddings], trainable=False)y_emb = layer_y_emb(y_in)### lstmlayer_y_lstm = layers.LSTM(name='y_lstm', units=lstm_units*2, dropout=0.2, return_sequences=True, return_state=True)y_out, _, _ = layer_y_lstm(y_emb, initial_state=[state_h, state_c])### final dense layerslayer_dense = layers.TimeDistributed(name='dense', layer=layers.Dense(units=len(y_dic_vocabulary), activation='softmax'))y_out = layer_dense(y_out)##---------------------- COMPILE ---------------------------------##model = models.Model(inputs=[x_in, y_in], outputs=y_out, name='Seq2Seq')model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy',metrics=['accuracy'])model.summary()
下面就可以實(shí)際進(jìn)行訓(xùn)練了,在實(shí)際測試集上進(jìn)行測試之前,我將保留一小部分訓(xùn)練集進(jìn)行驗(yàn)證。
## traintraining = model.fit(x=[X_train, y_train[:,:-1]], y=y_train.reshape(y_train.shape[0], y_train.shape[1], 1)[:,1:],batch_size=128, epochs=100, shuffle=True, verbose=1, validation_split=0.3,callbacks=[callbacks.EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=2)])## plot loss and accuracymetrics = [k for k in training.history.keys() if ('loss' not in k) and ('val' not in k)]fig, ax = plt.subplots(nrows=1, ncols=2, sharey=True)ax[0].set(title='Training')ax11 = ax[0].twinx()ax[0].plot(training.history['loss'], color='black')ax[0].set_xlabel('Epochs')ax[0].set_ylabel('Loss', color='black')for metric in metrics:ax11.plot(training.history[metric], label=metric)ax11.set_ylabel('Score', color='steelblue')ax11.legend()ax[1].set(title='Validation')ax22 = ax[1].twinx()ax[1].plot(training.history['val_loss'], color='black')ax[1].set_xlabel('Epochs')ax[1].set_ylabel('Loss', color='black')for metric in metrics:ax22.plot(training.history['val_'+metric], label=metric)ax22.set_ylabel('Score', color='steelblue')plt.show()
這里在回調(diào)中使用了 EarlyStopping,當(dāng)監(jiān)控的指標(biāo)(即驗(yàn)證損失)停止改進(jìn)時(shí)應(yīng)該停止訓(xùn)練。 這對于節(jié)省時(shí)間特別有用,尤其是對于像這樣的長時(shí)間的訓(xùn)練。 在不利用 GPU 的情況下運(yùn)行 Seq2Seq 算法非常困難,因?yàn)橥瑫r(shí)訓(xùn)練了 2 個(gè)模型(編碼器-解碼器)所以GPU是必須的。
訓(xùn)練結(jié)束了,但是工作還沒結(jié)束! 作為測試 Seq2Seq 模型的最后一步,需要構(gòu)建推理模型來生成預(yù)測。 預(yù)測編碼器將一個(gè)新序列(X_test)作為輸入,并返回最后一個(gè) LSTM 層的輸出及其狀態(tài)。
# Prediction Encoderencoder_model = models.Model(inputs=x_in, outputs=[x_out, state_h, state_c], name='Prediction_Encoder')encoder_model.summary()
另一方面,預(yù)測解碼器將起始標(biāo)記、編碼器的輸出及其狀態(tài)作為輸入,并返回新狀態(tài)以及詞匯表上的概率分布(概率最高的詞將是預(yù)測) .
## double the lstm units if you used bidirectional lstmlstm_units = lstm_units*2 if any('Bidirectional' in str(layer) for layer in model.layers) else lstm_units## states of the previous time stepencoder_out = layers.Input(shape=(X_train.shape[1], lstm_units))state_h, state_c = layers.Input(shape=(lstm_units,)), layers.Input(shape=(lstm_units,))## decoder embeddingsy_emb2 = layer_y_emb(y_in)## lstm to predict the next wordy_out2, state_h2, state_c2 = layer_y_lstm(y_emb2, initial_state=[state_h, state_c])## softmax to generate probability distribution over the vocabularyprobs = layer_dense(y_out2)## compiledecoder_model = models.Model(inputs=[y_in, encoder_out, state_h, state_c], outputs=[probs, state_h2, state_c2], name='Prediction_Decoder')decoder_model.summary()
在使用起始標(biāo)記和編碼器狀態(tài)進(jìn)行第一次預(yù)測后,解碼器使用生成的詞和新狀態(tài)來預(yù)測新詞和新狀態(tài)。 該迭代將繼續(xù)進(jìn)行,直到模型最終預(yù)測到結(jié)束標(biāo)記或預(yù)測的摘要達(dá)到其最大長度。
下面就上述循環(huán)來生成預(yù)測并測試 Seq2Seq 模型的代碼:
# Predictmax_seq_lenght = X_test.shape[1]predicted = []for x in X_test:x = x.reshape(1,-1)## encode Xencoder_out, state_h, state_c = encoder_model.predict(x)## prepare loopy_in = np.array([fitted_tokenizer.word_index[special_tokens[0]]])predicted_text = ''stop = Falsewhile not stop:## predict dictionary probability distributionprobs, new_state_h, new_state_c = decoder_model.predict([y_in, encoder_out, state_h, state_c])## get predicted wordvoc_idx = np.argmax(probs[0,-1,:])pred_word = fitted_tokenizer.index_word[voc_idx]## check stopif (pred_word != special_tokens[1]) and (len(predicted_text.split()) < max_seq_lenght):predicted_text = predicted_text +' '+ pred_wordelse:stop = True## nexty_in = np.array([voc_idx])state_h, state_c = new_state_h, new_state_cpredicted_text = predicted_text.replace(special_tokens[0],'').strip()predicted.append(predicted_text)
該模型理解上下文和關(guān)鍵信息,但它對詞匯的預(yù)測很差。 發(fā)生這種情況是因?yàn)槲以谶@個(gè)實(shí)驗(yàn)的完整數(shù)據(jù)集的一小部分上運(yùn)行了 Seq2Seq “簡化版”。 如果像改進(jìn)的話可以添加更多數(shù)據(jù)并提高性能。
Transformers 是 Google 的論文 Attention is All You Need (2017) 提出的一種新的建模技術(shù),其中證明序列模型(如 LSTM)可以完全被注意力機(jī)制取代,甚至可以獲得更好的性能。 這些語言模型可以通過一次處理所有序列并映射單詞之間的依賴關(guān)系來執(zhí)行任何 NLP 任務(wù),無論它們在文本中相距多遠(yuǎn)。在他們的詞嵌入中,同一個(gè)詞可以根據(jù)上下文有不同的向量。 最著名的語言模型是 Google 的 BERT 和 OpenAI 的 GPT。
Facebook 的 BART(雙向自回歸Transformers)使用標(biāo)準(zhǔn)的 Seq2Seq 雙向編碼器(如 BERT)和從左到右的自回歸解碼器(如 GPT)。 可以說:BART = BERT + GPT。
Transformers 模型的主要庫是 HuggingFace 的Transformer:
def bart(corpus, max_len): nlp = transformers.pipeline('summarization') lst_summaries = [nlp(txt, max_length=max_len)[0]['summary_text'].replace(' .', '.') for txt in corpus] return lst_summaries## Apply the function to corpuspredicted = bart(corpus=dtf_test['text'], max_len=y_len)
預(yù)測簡短而且有效。 對于大多數(shù) NLP 任務(wù),Transformer 模型似乎是表現(xiàn)最好的。并且對于一般的使用,完全可以使用HuggingFace 的與訓(xùn)練模型,可以提高不少效率
本文演示了如何將不同的 NLP 模型應(yīng)用于文本摘要用例。 這里比較了 3 種流行的方法:無監(jiān)督 TextRank、兩個(gè)不同版本的基于詞嵌入的監(jiān)督 Seq2Seq 和預(yù)訓(xùn)練 BART。并且還包含了特征工程、模型設(shè)計(jì)、評估和可視化。
聯(lián)系客服