文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松

Unified Language Model 文本生成从未如此轻松


前言

一篇19年的微软论文,老规矩先放论文链接:https://arxiv.org/abs/1905.03197

最近开始尝试做长文本的摘要生成任务,因此要拿出了几篇 Transformer 时代的文本生成相关的经典论文参考。当我看到UniLM这个Bert的变体模型(甚至连变体都算不上),口中只能叹出,“相见恨晚” 四个字。接着回顾了之前辛苦辛苦手敲的LSTM+Attention的seq2seq模型,脸上浮现出了嫌弃的表情。文本生成在Self-Attention的加持下从未如此简单。


UniLM

这里只阐述论文的关键思路,具体细节还是看论文来的实在啦~

  1. MLM (Mask language model),之所以在文本生成任务上表现较弱,归根到底是其一个非常重要的先验假设:Token之间是相互独立的。
    文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松
    Independence Assumption: As emphasized by the ≈ sign in Eq, BERT factorizes the joint conditional probability p(x¯ | xˆ) based on an independence assumption that all masked tokens x¯ are separately reconstructed. (摘自论文XLNet)
  2. 其 Self-Attention 模块使得每一个Position上的Token都能获取到全文的上下文信息,这与文本生成任务相违背:即文本的生成是具有依赖关系的,Xt+1的生成应该依赖于X<=t,并且Xt+1不能获取X>t的信息,因为那是未来的信息,如果仍然使用MLM中的满Self-attention(姑且这么叫)来做训练模型的文本生成能力,那么在一开始模型就已经知道了所有的答案了,这显然是不合理的。
  3. 那该如何是好?使用Seq2Seq的模型:如老家族的RNN,LSTM,GRU等序列模型,或新家族的Transform类,由于在该类模型中Decoder的信息传递是单向的,每一个输出只决定于Encoder部分的输入和之前的输出,因此也能较好的完成文本生成的任务。
  4. 那有没有什么办法,让MLM / Bert的框架既能优秀的完成文本理解,又能轻松实现文本生成任务呢?UNILM给出了答案:给Self-Attention加上MASK!
文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松
  1. 既然满Self-Attention会泄密,那么为什么不能通过MASK:将待预测部分的Token的Attention做选择性的屏蔽,让他只能看到获取到上文的信息,而对输入部分保持理解输入部分的上下文信息,但屏蔽待生成部分的信息。
  2. 图中 Bidirectional LM 与Bert一致,使用满 Self-attention 是模型充分学习全文的上下文信息,提高文本理解能力。
  3. 图中 Seq-to-Seq LM 部分:矩阵 Sij i为行 j 为列 Sij 为空白表示以 i 为 Q,j 为 K 的Attention信息没有被MASK,即 i 能获取 j 的信息。Sij 为黑 则表示该Attention信息被Mask,i 无法获取 即 j 的信息。如图中的MASK设计,S1部分仍为 Bidirectional LM,S2部分的每一个Token只能获取前面的Token的信息,而后面的信息是被MASK的。这符合文本生成的逻辑。
  4. 将以上 两种 LM 方式作为模型的 Pretrain 任务,Bidirectional LM 与 Bert一致,随机MASK Token,并进行预测,Seq-to-Seq LM 则 MASK S2的Token 利用 S1的信息去预测。这种Pretrain机制的设计使得 UNILM 完成文本理解和文本生成任务的能力都得到了提升。
    文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松

How to build UniLM

Tensorflow-GPU 2.0.0
Transformers 3.1.0

Get 2D MASK

文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松
  1. 当我们要使用 UniLM 完成文本生成任务时,Self-Attention 的Mask 会变成一个2D动态遮招(每一个sample都不同)这与往常我们通过 Transformers 的 BertTokenizer 模块直接得到的1D的 attention_mask不同。
  2. 2D Mask 取决于我们的输入和输出,而这部分信息可以通过Segment_id进行表示,因此我们只需要将输入文本和目标输出文本同时传递给BertTokenizer,通过返回的Segment_id 构建 2D MASK即可。
def unilm_mask_single(s):
	'''
	s = np.array([0,0,0,0,1,1,1,0,0,0])
	unilm_mask_single(s) = 
	<tf.Tensor: shape=(10, 10), dtype=float32, numpy=
	array([[1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 1., 0., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 1., 1., 0., 0., 0., 0.],
	       [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
	       [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
	       [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
	       [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32)>
	'''
    idxs = K.cumsum(s, axis=0)
    mask = idxs[None, :] <= idxs[:, None]
    mask = K.cast(mask, K.floatx())
    return mask
    
ids = np.zeros((self.batch_size,self.Max_len),dtype='int32')
seg_id = np.zeros((self.batch_size,self.Max_len),dtype='int32')
mask_att = np.zeros((self.batch_size,self.Max_len,self.Max_len),dtype='int32')

input_dict = self.tokenizer(content,title,max_length=self.Max_len,truncation=True,padding=True)
len_ = len(input_dict['input_ids'])
token_ids = input_dict['input_ids']
segment_ids = input_dict['token_type_ids']
ids[index][:len_] = token_ids
seg_id[index][:len_] = segment_ids
mask_id = unilm_mask_single(seg_id[index])
mask_att[index] = mask_id

Send 2D MASK to Bert

  1. 通过 Trainsformers.TFBertModel 类创建的Bert实例,其默认接受的attention_mask类型为2维 即[batch_size, MAX_LEN] 之后通过广播的形式传播到 Self-attention矩阵的每一行,因此我们需要修改 Trainsformers.TFBertModel 的逻辑,使其允许接受我们提前计算好的Self-attention矩阵,即[batch_size, MAX_LEN, MAX_LEN]
  2. 具体的:
class TFBertMainLayer(tf.keras.layers.Layer):
	def call(……)
        if len(attention_mask.shape) == 2:
            extended_attention_mask = attention_mask[:, tf.newaxis, tf.newaxis, :]
        elif len(attention_mask.shape) == 3:
            extended_attention_mask = attention_mask[:, tf.newaxis, :, :]
        else:
            raise NotImplementedError
        extended_attention_mask = tf.cast(extended_attention_mask, embedding_output.dtype)
        extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0
  1. 当你需要使用UniLM完成文本生成任务时,传入你提前计算好的attention_mask矩阵即可,
  2. 当你需要使用UniLM完成文本理解任务时,传入原始的attention_mask序列即可。
  3. 至此你已经完成了 UniLM 模型!

使用UniLM实现新闻标题生成

数据处理部分(略)

数据处理部分只需要将新闻文本和标题同时传给BertTokenizer实例,并通过返回的Segment_id构建attention_mask矩阵即可。

模型训练

技巧1:用自定义损失层来代替损失函数

  1. 通过自定义层,并利用tf.keras.layers.Layer.add_loss 方法,可以允许我们在层的计算中传递Loss,这允许我们利用 input 和 output 计算损失。具体如下:
class Loss(tf.keras.layers.Layer):
    """特殊的层,用来定义复杂loss
    """
    def __init__(self, output_axis=None, **kwargs):
        super(Loss, self).__init__(**kwargs)
        self.output_axis = output_axis

    def call(self, inputs, mask=None):
        loss = self.compute_loss(inputs, mask)
        self.add_loss(loss)
        if self.output_axis is None:
            return inputs
        elif isinstance(self.output_axis, list):
            return [inputs[i] for i in self.output_axis]
        else:
            return inputs[self.output_axis]

    def compute_loss(self, inputs, mask=None):
        raise NotImplementedError

class CrossEntropy(Loss):
    """交叉熵作为loss,并mask掉输入部分
    """
    def compute_loss(self, inputs, mask=None):
        y_true, y_mask, y_pred = inputs
        y_true = tf.cast(y_true,tf.float32)
        y_mask = tf.cast(y_mask,tf.float32)
        y_true = y_true[:, 1:]  # 目标token_ids
        y_mask = y_mask[:, 1:]  # segment_ids,刚好指示了要预测的部分
        y_pred = y_pred[:, :-1] # 预测序列,错开一位
        loss = K.sparse_categorical_crossentropy(y_true, y_pred)
        loss = K.sum(loss * y_mask) / K.sum(y_mask)
        return loss

技巧2: 结合Embedding信息输出预测文本

  1. 传统上,我们会使用 Dense(vocab_size,activaion=‘softmax’)(weight = [hidden_size, voacb_size) 来输出预测的概率分布,这样虽然可行,但模型的收敛速度非常慢甚至无法收敛,且需要更多的数据支持。因为模型在最开始是不知道vocab_size维度上每一维对应的文字信息。如果我们将这个Dense层的weight替换成我们的word_embedding,将每一维的所代表的token信息提早告诉模型,不仅加快了模型的收敛速度(实测有效!),还提高了模型的性能。模型具体如下:
def build_model(pretrained_path,config,MAX_LEN,vocab_size,keep_tokens):
    ids = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)
    token_id = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)
    att = tf.keras.layers.Input((MAX_LEN,MAX_LEN), dtype=tf.int32)
    config.output_hidden_states = True
    bert_model = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)
    x, _ , hidden_states = bert_model(ids,token_type_ids=token_id,attention_mask=att)
    layer_1 = hidden_states[-1]
    
    '''
    [batch_size,max_len,hidden_size] * [hidden_size,vocab_size]
    = [batch_size, max_len , vocab_size]
    '''
    word_embeeding = bert_model.bert.embeddings.word_embeddings
    embeddding_trans = tf.transpose(word_embeeding)
    sof_output = tf.matmul(layer_1,embeddding_trans)
    sof_output = tf.keras.layers.Activation('softmax')(sof_output)
    
    #加入损失层,计算损失
    output = CrossEntropy(2)([ids,token_id,sof_output])
    
    model = tf.keras.models.Model(inputs=[ids,token_id,att],outputs=output)
    optimizer = tf.keras.optimizers.Adam(learning_rate=1e-5)
    model.compile(optimizer=optimizer)
    model.summary()
    return model

模型推理

技巧3: BeamSearch解码

  1. 模型推理时为多步迭代解码过程,并用BeamSearch方法寻找较优序列
  2. 具体来说每次只预测下一个词,并将已经生成的token加入到输入中,但segment_id编码为1,传入相应的mask矩阵。(蓝色为输入,红色为输出)
    文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松
  3. BeamSearch解码代码如下,已补充更多备注:
class AutoRegressiveDecoder(object):
    """通用自回归生成模型解码基类
    包含beam search和random sample两种策略
    """
    def __init__(self, start_id, end_id, maxlen, model,minlen=1):
        self.start_id = start_id
        self.end_id = end_id
        self.maxlen = maxlen
        self.minlen = minlen
        self.models = {}
        self.model = model
        if start_id is None:
            self.first_output_ids = np.empty((1, 0), dtype=int)
            # array([], shape=(1, 0), dtype=int64)
        else:
            self.first_output_ids = np.array([[self.start_id]])

    @staticmethod
    def wraps(default_rtype='probas', use_states=False):
        """用来进一步完善predict函数
        目前包含:1. 设置rtype参数,并做相应处理;
                  2. 确定states的使用,并做相应处理;
                  3. 设置温度参数,并做相应处理。
        """
        def actual_decorator(predict):
            def new_predict(
                self,
                inputs,
                output_ids,
                states,
                temperature=1,
                rtype=default_rtype
            ):
                assert rtype in ['probas', 'logits']
                prediction = predict(self, inputs, output_ids, states)

                if not use_states:
                    prediction = (prediction, None)

                if default_rtype == 'logits':
                    prediction = (
                        softmax(prediction[0] / temperature), prediction[1]
                    )
                elif temperature != 1:
                    probas = np.power(prediction[0], 1.0 / temperature)
                    probas = probas / probas.sum(axis=-1, keepdims=True)
                    prediction = (probas, prediction[1])

                if rtype == 'probas':
                    return prediction
                else:
                    return np.log(prediction[0] + 1e-12), prediction[1]

            return new_predict

        return actual_decorator

    def last_token(self,end):
        """创建一个只返回输入的最后一个token输出的新Model
        """
#         if model not in self.models:
        outputs = [
                tf.keras.layers.Lambda(lambda x: x[:,end])(output)
                for output in self.model.outputs]
        model_temp = tf.keras.models.Model(self.model.inputs, outputs)

        return model_temp

    def predict(self, inputs, output_ids, states=None):
        """用户需自定义递归预测函数
        说明:定义的时候,需要用wraps方法进行装饰,传入default_rtype和use_states,
             其中default_rtype为字符串logits或probas,probas时返回归一化的概率,
             rtype=logits时则返回softmax前的结果或者概率对数。
        返回:二元组 (得分或概率, states)
        """
        raise NotImplementedError

    def beam_search(self, inputs, topk, states=None, temperature=1, min_ends=1):
        """beam search解码
        说明:这里的topk即beam size;
        返回:最优解码序列。
        """
        #inputs = [token_ids,segment_ids]
        output_ids, output_scores = self.first_output_ids, np.zeros(1)
        # output_ids = [] , output_scores = 0
        for step in range(self.maxlen):
            scores, states = self.predict(
                inputs, output_ids, states, temperature, 'logits'
            )  # 计算当前得分
            #每一次人输入的拼接在predict里完成

            if step == 0:  # 第1步预测后将输入重复topk次
                inputs = [np.repeat(i, topk, axis=0) for i in inputs]
            scores = output_scores.reshape((-1, 1)) + scores  # 综合累积得分-相加等于相乘,输出的是logist
            # output_scores = [1.16165232,1.75142511]#上一次最优的两个的值
            # 分别由上面两个最优值作为x产生,故在各自产生的概率上加上之前的值
            # [[0.99853728 0.67273463 1.50580529 1.16165232 1.4321206 ]
            # [1.44454842 1.68150066 1.24661511 1.42612343 1.75142511]]
            indices = scores.argpartition(-topk, axis=None)[-topk:]  # 仅保留topk
            #[3 ,9]
            indices_1 = indices // scores.shape[1] # 候选字数 # 行索引
            # [0 ,1]
            indices_2 = (indices % scores.shape[1]).reshape((-1, 1))  # 列索引
            # [[3],[4]]
            output_ids = np.concatenate([output_ids[indices_1],indices_2],1)  # 更新输出
            #[[1,2,2,3,3], + [[3]
            # [2,3,1,4,4]]    [4]]
            output_scores = np.take_along_axis(
                scores, indices, axis=None
            )  # 更新得分
            #按indices的一维切片去获得索引 [1.16165232,1.75142511]
            end_counts = (output_ids == self.end_id).sum(1)  # 统计出现的end标记
            #[分别统计两条路 end出现次数 0,1]
            if output_ids.shape[1] >= self.minlen:  # 最短长度判断
                best_one = output_scores.argmax()  # 得分最大的那个
                if end_counts[best_one] == min_ends: # =1   # 如果已经终止
                    return output_ids[best_one]  # 直接输出
                else:  # 否则,只保留未完成部分
                    flag = (end_counts < min_ends)  # 标记未完成序列
                    if not flag.all():  # 如果有已完成的
                        inputs = [i[flag] for i in inputs]  # 扔掉已完成序列
                        output_ids = output_ids[flag]  # 扔掉已完成序列
                        output_scores = output_scores[flag]  # 扔掉已完成序列
                        end_counts = end_counts[flag]  # 扔掉已完成end计数
                        topk = flag.sum()  # topk相应变化
        # 达到长度直接输出
        return output_ids[output_scores.argmax()]

class AutoTitle(AutoRegressiveDecoder):
    """seq2seq解码器
    """
    @AutoRegressiveDecoder.wraps(default_rtype='probas')
    def predict(self, inputs, output_ids, states):
        ids,seg_id,mask_att = inputs
        ides_temp = ids.copy()
        seg_id_temp = seg_id.copy()
        mask_att_temp = mask_att.copy()
        len_out_put = len(output_ids[0])
        for i in range(len(ids)):
            get_len = len(np.where(ids[i] != 0)[0])
            end_ = get_len + len_out_put
            ides_temp[i][get_len:end_] = output_ids[i]
            seg_id_temp[i][get_len:end_] = np.ones_like(output_ids[i])
            mask_att_temp[i] = unilm_mask_single(seg_id_temp[i])
        return self.last_token(end_-1).predict([ides_temp,seg_id_temp,mask_att_temp])
    
    def generate(self,text,tokenizer,maxlen,topk=1):
        max_c_len = maxlen - self.maxlen
        input_dict = tokenizer(text,max_length=max_c_len,truncation=True,padding=True)
        token_ids = input_dict['input_ids']
        segment_ids = input_dict['token_type_ids']
        ids = np.zeros((1,maxlen),dtype='int32')
        seg_id = np.zeros((1,maxlen),dtype='int32')
        mask_att = np.zeros((1,maxlen,maxlen),dtype='int32')
        len_ = len(token_ids)
        ids[0][:len_] = token_ids
        seg_id[0][:len_] = segment_ids
        mask_id = unilm_mask_single(seg_id[0])
        mask_att[0] = mask_id
        output_ids = self.beam_search([ids,seg_id,mask_att],topk=topk)  # 基于beam search
        return tokenizer.decode(output_ids)

精简你的词汇表

技巧4: 精简你的词汇表,让你的模型收敛更快

  1. 加载预训练模型的词汇表,其中所有词汇量达到21127个,所对应的预训练的word_embedding参数为 [21127,hidden_size],而这21127中包含了英文后缀、特殊符号、表情、其他文字等等,考虑到中文文本生成任务,我们可以通过精简词汇表,并调整word_embedding来减少模型需要预测的输出类别,一方面减少了模型参数,加速了模型收敛,另一方面也避免了生成的文本有奇怪的字符掺入。
def load_vocab(dict_path, encoding='utf-8', simplified=False, startswith=None):
    """
    从bert的词典文件中读取词典,如果simplified = True,则对该字典进行精简。
    return:返回精简留下的字符及其新token_id组成的字典,已经其对应的老token_id。
    """
    def _is_punctuation(ch):
        """标点符号类字符判断(全/半角均在此内)
        提醒:unicodedata.category这个函数在py2和py3下的
        表现可能不一样,比如u'§'字符,在py2下的结果为'So',
        在py3下的结果是'Po'。
        """
        code = ord(ch)
        return 33 <= code <= 47 or \
            58 <= code <= 64 or \
            91 <= code <= 96 or \
            123 <= code <= 126 or \
            unicodedata.category(ch).startswith('P')
    
    def stem(token):
        """获取token的“词干”(如果是##开头,则自动去掉##)
        """
        if token[:2] == '##':
            return token[2:]
        else:
            return token
        
    def _cjk_punctuation():
        return u'\uff02\uff03\uff04\uff05\uff06\uff07\uff08\uff09\uff0a\uff0b\uff0c\uff0d\uff0f\uff1a\uff1b\uff1c\uff1d\uff1e\uff20\uff3b\uff3c\uff3d\uff3e\uff3f\uff40\uff5b\uff5c\uff5d\uff5e\uff5f\uff60\uff62\uff63\uff64\u3000\u3001\u3003\u3008\u3009\u300a\u300b\u300c\u300d\u300e\u300f\u3010\u3011\u3014\u3015\u3016\u3017\u3018\u3019\u301a\u301b\u301c\u301d\u301e\u301f\u3030\u303e\u303f\u2013\u2014\u2018\u2019\u201b\u201c\u201d\u201e\u201f\u2026\u2027\ufe4f\ufe51\ufe54\u00b7\uff01\uff1f\uff61\u3002'

    def _is_cjk_character(ch):
        """CJK类字符判断(包括中文字符也在此列)
        参考:https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)
        """
        code = ord(ch)
        return 0x4E00 <= code <= 0x9FFF or \
            0x3400 <= code <= 0x4DBF or \
            0x20000 <= code <= 0x2A6DF or \
            0x2A700 <= code <= 0x2B73F or \
            0x2B740 <= code <= 0x2B81F or \
            0x2B820 <= code <= 0x2CEAF or \
            0xF900 <= code <= 0xFAFF or \
            0x2F800 <= code <= 0x2FA1F
    
    token_dict = {}
    with open(dict_path, encoding=encoding) as reader:
        for line in reader:
            token = line.split()
            token = token[0] if token else line.strip()
            token_dict[token] = len(token_dict)

    if simplified:  # 过滤冗余部分token
        new_token_dict, keep_tokens = {}, []
        startswith = startswith or []
        for t in startswith:
            new_token_dict[t] = len(new_token_dict)
            keep_tokens.append(token_dict[t])

        for t, _ in sorted(token_dict.items(), key=lambda s: s[1]):
            if t not in new_token_dict:
                keep = True
                if len(t) > 1:
                    for c in stem(t):
                        if (
                            _is_cjk_character(c) or
                            _is_punctuation(c)
                        ):
                            keep = False
                            break
                if keep:
                    new_token_dict[t] = len(new_token_dict)
                    keep_tokens.append(token_dict[t])

        return new_token_dict, keep_tokens
    else:
        return token_dict
  1. 通过load_vocab函数,我们可以将传入的词表.txt文件精简后,以dict的形式返回,并附带对应的老token_id,这对我们来说很重要,可以帮助我们重构word_embedding。
  2. 那么我们该如何将这个new_dict载入到BertTokenizer中去呢,简单的替换 tokenizer.vocab 是会出现错误的,而BertTokenizer的call函数仅接受文件路径,因此同样我们需要修改BertTokenizer类的函数,使之可以接受dict形式的词典,具体的:修改BertTokenizer脚本中的load_vocab 函数,使之可以直接返回dict。
def load_vocab(vocab_file):
    """Loads a vocabulary file into a dictionary."""
    
    if isinstance(vocab_file,dict):
        return vocab_file
    
    vocab = collections.OrderedDict()
    with open(vocab_file, "r", encoding="utf-8") as reader:
        tokens = reader.readlines()
    for index, token in enumerate(tokens):
        token = token.rstrip("\n")
        vocab[token] = index
    return vocab
  1. 加载new_dict,精简后的词汇表从原来的 21127 压缩至13584。
new_token_dict, keep_tokens = load_vocab(vocab_path,simplified=True,startswith=['[PAD]', '[UNK]', '[CLS]', '[SEP]'])
tokenizer = BertTokenizer(new_token_dict)
vocab_size = tokenizer.vocab_size
print(vocab_size) # 13584
  1. 那么精简后的词表改变了各个字符对应的token_id,它和原来的预训练网络的embedding映射关系已经无法匹配,因此我们通过得到的keep_tokens,来对模型的word_embedding进行修改,使之与我们新的词表相对应:
def build_model(pretrained_path,config,MAX_LEN,vocab_size,keep_tokens):
    ids = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)
    token_id = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)
    att = tf.keras.layers.Input((MAX_LEN,MAX_LEN), dtype=tf.int32)
    config.output_hidden_states = True
    bert_model = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)
    '''
    通过set_input_embeddings函数,修改word_embedding矩阵
    '''
    bert_model.bert.set_input_embeddings(tf.gather(bert_model.bert.embeddings.word_embeddings,keep_tokens))
    
    x, _ , hidden_states = bert_model(ids,token_type_ids=token_id,attention_mask=att)
    layer_1 = hidden_states[-1]
    word_embeeding = bert_model.bert.embeddings.word_embeddings
    embeddding_trans = tf.transpose(word_embeeding)
    sof_output = tf.matmul(layer_1,embeddding_trans)
    sof_output = tf.keras.layers.Activation('softmax')(sof_output)
    output = CrossEntropy(2)([ids,token_id,sof_output])
    model = tf.keras.models.Model(inputs=[ids,token_id,att],outputs=output)
    optimizer = tf.keras.optimizers.Adam(learning_rate=1e-5)
    model.compile(optimizer=optimizer)
    model.summary()
    return model
文本生成(一)【NLP论文复现】Unified Language Model 文本生成从未如此轻松

测试结果

  1. 仅仅通过几个小(1000steps)epoch,模型就能生成基本可读的新闻标题
  2. 充分训练后,找了几篇最新的杭州报道进行测试,效果如下:
'''
杭州日报讯 留下来吧,在杭州过个暖心年!
昨日,市人力社保局、市经信局、市总工会等多部门相继发布倡议书,
倡议大家“就地过年”“留杭过年”,非必要不离杭,减少出行聚集,助力疫情防控。
为确保企业生产稳定有序,多部门鼓励企业通过“留岗红包”“过年大礼包”
“特殊津贴”“错峰调休”“领导带头”等积极措施留工稳岗。
同时,倡议大家尽量选择留杭过节、远程拜年,减少跨区域流动,把疫情传播风险降到最低。
针对确需返乡过年的员工,要做好防疫措施,有条件的企业可开展“点对点”送返,
确保员工往返途中和假期安全。
'''
Generate_title: '杭 州 多 部 门 倡 议 留 杭 过 年 [SEP]'

'''
都市快报讯 目前,北方强冷空气已严重影响杭州。
杭州市气象台1月7日15时31分发布低温橙色预警信号:
“受北方强冷空气影响,预计明天早晨主城区和钱塘新区最低气温-5℃到-7℃,
有严重冰冻,请注意做好防冻保暖工作。
”根据《杭州市抗雪防冻应急预案》,市防指决定从1月7日19时启动抗雪防冻Ⅳ级应急响应。
要求各地各部门按照预案要求,密切关注天气变化,加强监测预警,及时启动响应,
做好防御低温雨雪冰冻灾害的各项工作,确保安全。
'''
Generate_title: '北 方 强 冷 空 气 严 重 影 响 杭 州 [SEP]'

'''
杭州日报讯 昨日起,大家熟悉的公共自行车小红车使用有了新的变化:
杭州公共自行车推出“扫码租车免押金”服务,
市民游客只要通过信用免押进行实名认证后便可实现免押金租车。
记者从杭州公共自行车公司了解到,通过App Store、应用市场搜索“叮嗒出行”APP,
下载后点击首页“实名认证”,输入姓名、身份证号后,经校验通过,即可进入“免押通道”,
选择“0元免费开通”,同意“信用免押协议”,完成信用免押。
之后,无论是通过“叮嗒出行”APP,还是通过相应微信、支付宝小程序均可直接租用小红车,
不再需要缴纳信用保证金。
'''
Generate_title: '杭 州 公 共 自 行 车 推 出 免 押 金 服 务 [SEP]'

参考资料

[1] Unified Language Model Pre-training for
Natural Language Understanding and Generationh https://arxiv.org/abs/1905.03197
[2] 苏剑林. (Sep. 18, 2019). 《从语言模型到Seq2Seq:Transformer如戏,全靠Mask 》[Blog post]. Retrieved from https://kexue.fm/archives/6933


代码地址

https://github.com/zhengyanzhao1997/TF-NLP-model/blob/main/model/train/Unified%20Language%20Model/tran.py

上一篇:centos7下安装kubernetes1.18


下一篇:二进制安装k8s集群(二)