Unified Language Model 文本生成从未如此轻松
前言
一篇19年的微软论文,老规矩先放论文链接:https://arxiv.org/abs/1905.03197
最近开始尝试做长文本的摘要生成任务,因此要拿出了几篇 Transformer 时代的文本生成相关的经典论文参考。当我看到UniLM这个Bert的变体模型(甚至连变体都算不上),口中只能叹出,“相见恨晚” 四个字。接着回顾了之前辛苦辛苦手敲的LSTM+Attention的seq2seq模型,脸上浮现出了嫌弃的表情。文本生成在Self-Attention的加持下从未如此简单。
UniLM
这里只阐述论文的关键思路,具体细节还是看论文来的实在啦~
- MLM (Mask language model),之所以在文本生成任务上表现较弱,归根到底是其一个非常重要的先验假设:Token之间是相互独立的。
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) - 其 Self-Attention 模块使得每一个Position上的Token都能获取到全文的上下文信息,这与文本生成任务相违背:即文本的生成是具有依赖关系的,Xt+1的生成应该依赖于X<=t,并且Xt+1不能获取X>t的信息,因为那是未来的信息,如果仍然使用MLM中的满Self-attention(姑且这么叫)来做训练模型的文本生成能力,那么在一开始模型就已经知道了所有的答案了,这显然是不合理的。
- 那该如何是好?使用Seq2Seq的模型:如老家族的RNN,LSTM,GRU等序列模型,或新家族的Transform类,由于在该类模型中Decoder的信息传递是单向的,每一个输出只决定于Encoder部分的输入和之前的输出,因此也能较好的完成文本生成的任务。
- 那有没有什么办法,让MLM / Bert的框架既能优秀的完成文本理解,又能轻松实现文本生成任务呢?UNILM给出了答案:给Self-Attention加上MASK!
- 既然满Self-Attention会泄密,那么为什么不能通过MASK:将待预测部分的Token的Attention做选择性的屏蔽,让他只能看到获取到上文的信息,而对输入部分保持理解输入部分的上下文信息,但屏蔽待生成部分的信息。
- 图中 Bidirectional LM 与Bert一致,使用满 Self-attention 是模型充分学习全文的上下文信息,提高文本理解能力。
- 图中 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的。这符合文本生成的逻辑。
- 将以上 两种 LM 方式作为模型的 Pretrain 任务,Bidirectional LM 与 Bert一致,随机MASK Token,并进行预测,Seq-to-Seq LM 则 MASK S2的Token 利用 S1的信息去预测。这种Pretrain机制的设计使得 UNILM 完成文本理解和文本生成任务的能力都得到了提升。
How to build UniLM
Tensorflow-GPU 2.0.0
Transformers 3.1.0
Get 2D MASK
- 当我们要使用 UniLM 完成文本生成任务时,Self-Attention 的Mask 会变成一个2D动态遮招(每一个sample都不同)这与往常我们通过 Transformers 的 BertTokenizer 模块直接得到的1D的 attention_mask不同。
- 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
- 通过 Trainsformers.TFBertModel 类创建的Bert实例,其默认接受的attention_mask类型为2维 即[batch_size, MAX_LEN] 之后通过广播的形式传播到 Self-attention矩阵的每一行,因此我们需要修改 Trainsformers.TFBertModel 的逻辑,使其允许接受我们提前计算好的Self-attention矩阵,即[batch_size, MAX_LEN, MAX_LEN]
- 具体的:
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
- 当你需要使用UniLM完成文本生成任务时,传入你提前计算好的attention_mask矩阵即可,
- 当你需要使用UniLM完成文本理解任务时,传入原始的attention_mask序列即可。
- 至此你已经完成了 UniLM 模型!
使用UniLM实现新闻标题生成
数据处理部分(略)
数据处理部分只需要将新闻文本和标题同时传给BertTokenizer实例,并通过返回的Segment_id构建attention_mask矩阵即可。
模型训练
技巧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信息输出预测文本
- 传统上,我们会使用 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解码
- 模型推理时为多步迭代解码过程,并用BeamSearch方法寻找较优序列
- 具体来说每次只预测下一个词,并将已经生成的token加入到输入中,但segment_id编码为1,传入相应的mask矩阵。(蓝色为输入,红色为输出)
- 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: 精简你的词汇表,让你的模型收敛更快
- 加载预训练模型的词汇表,其中所有词汇量达到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
- 通过load_vocab函数,我们可以将传入的词表.txt文件精简后,以dict的形式返回,并附带对应的老token_id,这对我们来说很重要,可以帮助我们重构word_embedding。
- 那么我们该如何将这个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
- 加载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
- 那么精简后的词表改变了各个字符对应的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
测试结果
- 仅仅通过几个小(1000steps)epoch,模型就能生成基本可读的新闻标题
- 充分训练后,找了几篇最新的杭州报道进行测试,效果如下:
'''
杭州日报讯 留下来吧,在杭州过个暖心年!
昨日,市人力社保局、市经信局、市总工会等多部门相继发布倡议书,
倡议大家“就地过年”“留杭过年”,非必要不离杭,减少出行聚集,助力疫情防控。
为确保企业生产稳定有序,多部门鼓励企业通过“留岗红包”“过年大礼包”
“特殊津贴”“错峰调休”“领导带头”等积极措施留工稳岗。
同时,倡议大家尽量选择留杭过节、远程拜年,减少跨区域流动,把疫情传播风险降到最低。
针对确需返乡过年的员工,要做好防疫措施,有条件的企业可开展“点对点”送返,
确保员工往返途中和假期安全。
'''
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