乐趣区

关于机器学习:NLP-进行文本摘要的三种策略代码实现和对比TextRank-vs-Seq2Seq-vs-BART

本文将应用 Python 实现和比照解释 NLP 中的 3 种不同文本摘要策略:老式的 TextRank(应用 gensim)、驰名的 Seq2Seq(使基于 tensorflow)和最前沿的 BART(应用 Transformers)。

NLP(自然语言解决)是人工智能畛域,钻研计算机与人类语言之间的交互,特地是如何对计算机进行编程以解决和剖析大量自然语言数据。最难的 NLP 工作是输入不是单个标签或值(如分类和回归),而是残缺的新文本(如翻译、摘要和对话)的工作。

文本摘要是在不扭转其含意的状况下缩小文档的句子和单词数量的问题。有很多不同的技术能够从原始文本数据中提取信息并将其用于摘要模型,总体来说它们能够分为提取式(Extractive)和形象式(Abstractive)。提取办法抉择文本中最重要的句子(不肯定了解含意),因而作为后果的摘要只是全文的一个子集。而形象模型应用高级 NLP(即词嵌入)来了解文本的语义并生成有意义的摘要。形象技术很难从头开始训练,因为它们须要大量参数和数据,所以个别状况下都是用与训练的嵌入进行微调。

本文比拟了 TextRank(Extractive)的老派办法、风行的编码器 - 解码器神经网络 Seq2Seq(Abstractive)以及彻底改变 NLP 畛域的最先进的基于注意力的 Transformers(Abstractive)。

本文将应用“CNN DailyMail”数据集,蕴含了数千篇由 CNN 和《每日邮报》的记者用英语撰写的新闻文章,以及每篇文章的摘要,数据集和本文的代码也都会在本文开端提供。

首先,我须要导入以下库:

## for data
import datasets  #(1.13.3)
import pandas as pd  #(0.25.1)
import numpy  #(1.16.4)

## for plotting
import matplotlib.pyplot as plt  #(3.1.2)
import seaborn as sns  #(0.9.0)

## for preprocessing
import re
import nltk  #(3.4.5)
import contractions  #(0.0.18)

## for textrank
import gensim  #(3.8.1)

## for evaluation
import rouge  #(1.0.0)
import difflib

## for seq2seq
from tensorflow.keras import callbacks, models, layers, preprocessing as kprocessing #(2.6.0)

## for bart
import transformers  #(3.0.1)

而后我应用 HuggingFace 的加载数据集:

## load the full dataset of 300k articles
dataset = 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()

让咱们查看一个随机的样本:

i = 1
print("--- Full text ---")
print(dtf["text"][i])
print("--- Summary ---")
print(dtf["y"][i])

在上图中,我将摘要中提到的信息手动标记为红色。体育文章对机器来说是十分艰难的,因为题目须要在无限的字符限度的状况下突出次要后果。这个实例可能是一个十分好的例子,我会将这个示例保留在测试集中以比拟模型。

dtf_train = dtf.iloc[i+1:]
dtf_test = dtf.iloc[:i+1]

TextRank

TextRank (2004) 是一种基于图的文本处理排名模型,基于 Google 的 PageRank 算法,可在文本中找到最相干的句子。PageRank 是 1998 年 Google 搜索引擎应用的第一个对网页进行排序的算法。简而言之,如果页面 A 链接到页面 B,页面 C,页面 B 链接到页面 C,那么排序将是页面 C,页面 B,页面 A。

TextRank 十分易于应用,因为它是无监督的。首先,将整个文本拆分为句子,而后算法会应用其中句子作为节点,重叠的单词作为连贯,构建一个图,通过 PageRank 确定了这个句子网络中最重要的节点。

这里应用 gensim 库的内置 TextRank 算法实现:

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_summaries

predicted = textrank(corpus=dtf_test["text"], ratio=0.2)
predicted[i]

咱们如何评估这个后果?通常两种形式:

1、ROUGE 指标(Recall-Oriented Understudy for Gisting Evaluation):通过重叠 n-gram 将主动生成的摘要与参考摘要进行比拟。

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 predicted
i = 5
evaluate_summary(dtf_test["y"][i], predicted[i])

结果表明,31% 的 ROUGE-1 和 7% 的 ROUGE- 2 呈现在两个摘要中,而最长的公共子序列 (ROUGE-L) 匹配了 7%。总体而言,均匀得分为 20%。这里须要阐明的是 ROUGE 分数并不能掂量摘要的晦涩水平,因为对于晦涩水平来说咱们通常应用人肉判断。

2、可视化:显示 2 个文本,即摘要和原文或预测摘要和实在摘要,并突出匹配局部

#Find the matching substrings in 2 strings.
def utils_split_sentences(a, b):
    ## find clean matches
    match = 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 match
    if len(lst_match) == 0:
        lst_a, lst_b = nltk.sent_tokenize(a), nltk.sent_tokenize(b)
    
    ## work with matches
    else:
        first_m, last_m = lst_match[0], lst_match[-1]

        ### a
        string = 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:
                break
        string = a[last_m.a+last_m.size :]
        lst_a = lst_a + [t for t in nltk.sent_tokenize(string)]

        ### b
        string = 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:
                break
        string = 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 a
    first_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 b
    second_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)
    
    ## concatenate
    if len(titles) > 0:
        first_text = "<strong>"+titles[0]+"</strong><br>"+first_text
    if len(titles) > 1:
        second_text = "<strong>"+titles[1]+"</strong><br>"+second_text
    else:
        second_text = "---"*65+"<br><br>"+second_text
    final_text = first_text +'<br><br>'+ second_text
    return final_text

你会发现这个函数十分有用,尤其是对于须要咱们人肉判断的时候,因为它会突出显示两个文本的匹配子字符串,并且是单词级别的:

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, HTML
display(HTML(match))

或者能够设置 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, HTML
display(HTML(match))

能够看到预测蕴含原始摘要中提到的大部分信息。正如提取算法所冀望的那样,预测的摘要齐全蕴含在文本中:模型认为这 3 个句子是最重要的。咱们能够将此作为上面更为先进办法的基线。

Seq2Seq

序列到序列模型(2014)是一种神经网络的架构,它以来自一个域(即文本词汇表)的序列作为输出并输入另一个域(即摘要词汇表)中的新序列。Seq2Seq 模型通常具备以下要害特色:

  • 序列作为语料库:将文本填充成雷同长度的序列以取得特色矩阵。
  • 词嵌入机制:特色学习技术,将词汇表中的词映射到实数向量,这些向量是依据呈现在另一个词之前或之后的每个词的概率分布计算得出的。
  • 编码器 - 解码器构造:编码器解决输出序列并返回其本人的外部状态,作为解码器的上下文输出,解码器依据之前的词预测指标序列的下一个词。
  • 训练模型和预测模型:训练中应用的模型不间接用于预测。事实上,会编写 2 个神经网络(都具备编码器 - 解码器构造),一个用于训练,另一个(称为“推理模型”)通过利用训练模型中的一些层来生成预测。

因为咱们要将文本转换为单词序列,因而咱们必须要对数据进行解决:

  • 正确的序列大小,因为咱们的语料库有不同的长度
  • 咱们的模型必须记住多少单词,并且排除罕见的单词

上面将清理和剖析数据以解决这两个问题。

## create stopwords
lst_stopwords = nltk.corpus.stopwords.words("english")
## add words that are too frequent
lst_stopwords = lst_stopwords + ["cnn","say","said","new"]

## cleaning function
def 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 characters
    txt = re.sub(r'[^\w\s]', '', txt) if punkt is True else txt
    ### strip
    txt = " ".join([word.strip() for word in txt.split()])
    ### lowercase
    txt = txt.lower() if lower is True else txt
    ### slang
    txt = 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 Stopwords
    if lst_stopwords is not None:
        lst_txt = [word for word in lst_txt if word not in 
                   lst_stopwords]
    ### back to string
    txt = " ".join(lst_txt)
    return txt

## apply function to both text and summaries
dtf_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))

当初咱们来看看长度散布:

## count
dtf_train['word_count'] = dtf_train[column].apply(lambda x: len(nltk.word_tokenize(str(x))) )

## plot
sns.distplot(dtf_train["word_count"], hist=True, kde=True, kde_kws={"shade":True})

X_len = 400
y_len = 40

咱们来剖析词频:

lst_tokens = nltk.tokenize.word_tokenize(dtf_train["text_clean"].str.cat(sep=" "))
ngrams = [1]

## calculate
dtf_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"] = n
   dtf_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])

## plot
sns.barplot(x="freq", y="word", hue="ngrams", dodge=False,
 data=dtf_freq.groupby('ngrams')["ngrams","freq","word"].head(30))
plt.show()

thres = 5 #<-- min frequency
X_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 将预处理的语料库转换为序列列表来创立特色矩阵:

lst_corpus = dtf_train["text_clean"]

## tokenize text
tokenizer = 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 sequence
lst_text2seq= tokenizer.texts_to_sequences(lst_corpus)

## padding sequence
X_train = kprocessing.sequence.pad_sequences(lst_text2seq, 
                    maxlen=15, padding="post", truncating="post")

特色矩阵 X_train 具备 N 个文档 x 序列最大长度的形态。让咱们可视化一下:

sns.heatmap(X_train==0, vmin=0, vmax=1, cbar=False)
plt.show()

除此以外,还须要对测试集进行雷同的特色工程:

## text to sequence with the fitted tokenizer
lst_text2seq = tokenizer.texts_to_sequences(dtf_test["text_clean"])

## padding sequence
X_test = kprocessing.sequence.pad_sequences(lst_text2seq, maxlen=15,
             padding="post", truncating="post")

当初就能够解决摘要了。在利用雷同的特色工程策略之前,须要在每个摘要中增加两个非凡标记,以确定文本的结尾和结尾。

# 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 example
dtf_test["y_clean"][i]

当初咱们能够通过利用与以前雷同的代码创立摘要的特色矩阵。预测时将应用开始标记开始预测,当完结标记呈现时,预测文本将进行。

对于词嵌入这里有 2 个选项:从头开始训练咱们的词嵌入模型或应用预训练的模型。在 Python 中,能够 genism-data 加载预训练的 Word Embedding 模型:

import gensim_api
nlp = gensim_api.load("glove-wiki-gigaword-300")

这里举荐应用斯坦福的 GloVe,这是一种在 Wikipedia、Gigaword 和 Twitter 语料库上训练的无监督学习算法。能够将任何单词转换为向量:

word = "home"
nlp[word].shape

>>> (300,)

这些词向量能够在神经网络中用作权重。为了做到这一点,咱们须要创立一个嵌入矩阵,以便 id N 的单词的向量位于第 N 行。

## start the matrix (length of vocabulary x vector size) with all 0s
X_embeddings = np.zeros((len(X_dic_vocabulary)+1, 300))for word,idx in X_dic_vocabulary.items():
    ## update the row with vector
    try:
        X_embeddings[idx] =  nlp[word]
    ## if word not in model then skip and the row stays all 0s
    except:
        pass

下面的代码生成从语料库中提取的词嵌入的矩阵。语料库矩阵应会在编码器嵌入层中应用,而摘要矩阵会在解码器层中应用。输出序列中的每个 id 都将用作拜访嵌入矩阵的索引。该嵌入层的输入将是一个 2D 矩阵,其中输出序列中的每个词 id 都有一个词向量(序列长度 x 向量大小):

上面就是构建编码器 - 解码器模型的时候了。首先,咱们须要确认正确的输出和输入:

  • 输出是 X(文本序列) 加上 y(摘要序列),并且须要暗藏摘要的最初一个单词
  • 指标应该是没有开始标记的 y(汇总序列)。

将输出文本提供给编码器以理解上下文,而后向解码器展现摘要如何开始,模型将会学习预测摘要如何完结。这是一种称为“teacher forcing”的训练策略,它应用指标而不是网络生成的输入,以便它能够学习预测开始标记之后的单词,而后是下一个单词,依此类推。

本文将提出两个不同版本的 Seq2Seq, 上面是咱们应用的最简略的版本:

一个嵌入层,它将从头开始创立一个词嵌入。一个单向 LSTM 层,它返回一个序列以及单元状态和暗藏状态

最初一个 Time Distributed Dense layer,它一次将雷同的密集层(雷同的权重)利用于 LSTM 输入,每次一个工夫步长,这样输入层只须要一个与每个 LSTM 单元的连贯。

lstm_units = 250
embeddings_size = 300

##------------ ENCODER (embedding + lstm) ------------------------##
x_in = layers.Input(name="x_in", shape=(X_train.shape[1],))

### embedding
layer_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,))

### embedding
layer_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 layers
layer_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 的预训练权重。3 个双向 LSTM 层,在两个方向上解决序列。最初的 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],))

### embedding
layer_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 1
layer_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 2
layer_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,))

### embedding
layer_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)

### lstm
layer_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 layers
layer_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()

上面就能够理论进行训练了,在理论测试集上进行测试之前,我将保留一小部分训练集进行验证。

## train
training = 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 accuracy
metrics = [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()

这里在回调中应用了 EarlyStopping,当监控的指标(即验证损失)进行改良时应该进行训练。这对于节省时间特地有用,尤其是对于像这样的长时间的训练。在不利用 GPU 的状况下运行 Seq2Seq 算法十分艰难,因为同时训练了 2 个模型(编码器 - 解码器)所以 GPU 是必须的。

训练完结了,然而工作还没完结!作为测试 Seq2Seq 模型的最初一步,须要构建推理模型来生成预测。预测编码器将一个新序列(X_test)作为输出,并返回最初一个 LSTM 层的输入及其状态。

# Prediction Encoder
encoder_model = models.Model(inputs=x_in, outputs=[x_out, state_h, state_c], name="Prediction_Encoder")
encoder_model.summary()

另一方面,预测解码器将起始标记、编码器的输入及其状态作为输出,并返回新状态以及词汇表上的概率分布(概率最高的词将是预测).

## double the lstm units if you used bidirectional lstm
lstm_units = lstm_units*2 if any("Bidirectional" in str(layer) for layer in model.layers) else lstm_units

## states of the previous time step
encoder_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 embeddings
y_emb2 = layer_y_emb(y_in)

## lstm to predict the next word
y_out2, state_h2, state_c2 = layer_y_lstm(y_emb2, initial_state=[state_h, state_c])

## softmax to generate probability distribution over the vocabulary
probs = layer_dense(y_out2)

## compile
decoder_model = models.Model(inputs=[y_in, encoder_out, state_h, state_c], outputs=[probs, state_h2, state_c2], name="Prediction_Decoder")

decoder_model.summary()

在应用起始标记和编码器状态进行第一次预测后,解码器应用生成的词和新状态来预测新词和新状态。该迭代将持续进行,直到模型最终预测到完结标记或预测的摘要达到其最大长度。

上面就上述循环来生成预测并测试 Seq2Seq 模型的代码:

# Predict
max_seq_lenght = X_test.shape[1]
predicted = []
for x in X_test:
   x = x.reshape(1,-1)
   ## encode X
   encoder_out, state_h, state_c = encoder_model.predict(x)
   ## prepare loop
   y_in = np.array([fitted_tokenizer.word_index[special_tokens[0]]])
   predicted_text = ""
   stop = False
   while not stop:
   ## predict dictionary probability distribution
        probs, new_state_h, new_state_c = decoder_model.predict([y_in, encoder_out, state_h, state_c])
        
        ## get predicted word
        voc_idx = np.argmax(probs[0,-1,:])
        pred_word = fitted_tokenizer.index_word[voc_idx]
        
        ## check stop
        if (pred_word != special_tokens[1]) and 
           (len(predicted_text.split()) < max_seq_lenght):
            predicted_text = predicted_text +" "+ pred_word
        else:
            stop = True
        
        ## next
        y_in = np.array([voc_idx])
        state_h, state_c = new_state_h, new_state_c
   
   predicted_text = predicted_text.replace(special_tokens[0],"").strip()
   predicted.append(predicted_text)

该模型了解上下文和要害信息,但它对词汇的预测很差。产生这种状况是因为我在这个试验的残缺数据集的一小部分上运行了 Seq2Seq“简化版”。如果像改良的话能够增加更多数据并进步性能。

Transformers

Transformers 是 Google 的论文 Attention is All You Need (2017) 提出的一种新的建模技术,其中证实序列模型(如 LSTM)能够齐全被注意力机制取代,甚至能够取得更好的性能。这些语言模型能够通过一次解决所有序列并映射单词之间的依赖关系来执行任何 NLP 工作,无论它们在文本中相距多远。在他们的词嵌入中,同一个词能够依据上下文有不同的向量。最驰名的语言模型是 Google 的 BERT 和 OpenAI 的 GPT。

Facebook 的 BART(双向自回归 Transformers)应用规范的 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 corpus
predicted = bart(corpus=dtf_test["text"], max_len=y_len)

预测简短而且无效。对于大多数 NLP 工作,Transformer 模型仿佛是体现最好的。并且对于个别的应用,齐全能够应用 HuggingFace 的与训练模型,能够进步不少效率

总结

本文演示了如何将不同的 NLP 模型利用于文本摘要用例。这里比拟了 3 种风行的办法:无监督 TextRank、两个不同版本的基于词嵌入的监督 Seq2Seq 和预训练 BART。并且还蕴含了特色工程、模型设计、评估和可视化。

以下是本文的残缺代码:

https://www.overfit.cn/post/ce018bb0dd574f2e982ed5e136d4af77

作者:Mauro Di Pietro

退出移动版