task_seq2seq_autotitle_csl.py代码解读
此文档是对bert4keras工具中seq2seq任务的示例代码的一个解读,尽可能看的细致是为了以后修改起来更加顺手。有些代码和操作的理解可能还会有一些错误,正在不断的完善当中。
0.主要部分
从主函数入口可以了解该程序主要包括以下三个部分:
evaluator = Evaluate()
train_generator = data_generator(train_data, batch_size)
model.fit_generator(train_generator.forfit(),steps_per_epoch=len(train_generator),
epochs=epochs, callbacks=[evaluator])
其中evaluator使用来评估模型和保存模型参数的; data_generator是用来生成训练数据的,将文本数据转换成对应的token_id,方便模型计算; model.fit_generator() 使用了使用训练数据训练模型。
所以,改程序可以概括为三个主要部分: 数据预处理,模型搭建与参数加载,训练模型并且评估模型。
1.数据预处理阶段
在执行data_generator() 之前,还需要做以下准备。通过load_data()将文本数据读取到列表中,并且加载bert模型文件中的中文字表,在调用Tokenizer()函数,返回tokenizer对象,为了下一步将输入文本转换为token:
# 加载数据集
train_data = load_data("train.data")
valid_data = load_data("valid.data")
test_data = load_data("test.data")
# 加载并精简词表,建立分词器
token_dict, keep_tokens = load_vocab(
dict_path=dict_path,
simplified=True,
startwith=['[PAD]', '[UNK]', '[CLS]', '[SEP]'],
)
# 转换为token
tokenizer = Tokenizer(token_dict, do_lower_case=True)
接下来开始执行data_generator(),传入参数为train_data和batch_size:
class data_generator(DataGenerator):
"""数据生成器"""
def __iter__(self, random=False):
idxs = list(range(len(self.data)))
if random:
np.random.shuffle(idxs)
batch_token_ids, batch_segment_ids = [], []
for i in idxs:
title, content = self.data[i]
# 将数据转换成ID,将content和title转换成同一个token_ids
token_ids, segment_ids = tokenizer.encode(content,title,max_length=maxlen)
batch_token_ids.append(token_ids)
batch_segment_ids.append(segment_ids)
if len(batch_token_ids) == self.batch_size or i == idxs[-1]:
batch_token_ids = sequence_padding(batch_token_ids)
batch_segment_ids = sequence_padding(batch_segment_ids)
yield [batch_token_ids, batch_segment_ids], None
batch_token_ids, batch_segment_ids = [], []
将数据进行随机洗牌之后,调用tokenizer.encode()对数据进行转换,输入文本内容和标题内容,以及最大句子长度,这里要注意一个地方,本来读取的数据第一句是title,第二句是content,在调用tokenizer.encode()函数时,先输入的是content,其次才输入的title,转换完成之后返回token_ids和segment_ids,然后按照batch_size进行打包返回。这里最值得注意的地方是tokenizer.encode()中发生了什么?
def encode(self,first_text,second_text=None,max_length=None,first_length=None,
second_length=None):
"""输出文本对应token id和segment id如果传入first_length,则强行padding第一个句子到指定长度;同理,如果传入second_length,则强行padding第二个句子到指定长度。"""
if is_string(first_text):
first_tokens = self.tokenize(first_text)
else:
first_tokens = first_text
if second_text is None:
second_tokens = None
elif is_string(second_text):
second_tokens = self.tokenize(second_text, add_cls=False)
else:
second_tokens = second_text
if max_length is not None:
self.truncate_sequence(max_length, first_tokens, second_tokens, -2)
first_token_ids = self.tokens_to_ids(first_tokens)
if first_length is not None:
first_token_ids = first_token_ids[:first_length]
first_token_ids.extend([self._token_pad_id] *
(first_length - len(first_token_ids)))
first_segment_ids = [0] * len(first_token_ids)
if second_text is not None:
second_token_ids = self.tokens_to_ids(second_tokens)
if second_length is not None:
second_token_ids = second_token_ids[:second_length]
second_token_ids.extend([self._token_pad_id] *
(second_length - len(second_token_ids)))
second_segment_ids = [1] * len(second_token_ids)
first_token_ids.extend(second_token_ids)
first_segment_ids.extend(second_segment_ids)
return first_token_ids, first_segment_ids
前半部分都是对文本的token化和padding操作,由最后三句可以看到,程序将second_token_ids拼接到了first_token_ids后面,对应文本就是将训练数据的title拼接到了content之后,返回了first_token_ids,对于segment_ids,content部分的词标记为"0",title部分的词标记为"1",将title的segment_ids拼接到content的segment_ids之后,最后返回token_ids和segment_ids,所以segment_ids的数据就形如[0,0,0,0,1,1]。
接下来将数据按照batch_size打包到列表中,得到batch_token_ids和batch_segment_ids,至此,数据预处理部分执行完毕。
2.模型搭建与参数加载
2.1关于Bert针对seq2seq任务的代码修改部分
接下来对首先对bert模型进行加载,输入参数config_path表示bert模型参数的配置文件,checkpoint_path表示模型参数文件路径,application表示应用的任务类型。对于不同用途,要对bert模型进行不同的修改。
# 构建bert模型,并且加载模型参数
model = build_bert_model(
config_path,
checkpoint_path,
application='seq2seq',
keep_tokens=keep_tokens, # 只保留keep_tokens中的字,精简原字表
)
# 此行代码输出模型各层的参数状况
model.summary()
application='seq2seq’参数表示: 对于seq2seq任务模型加载的是继承自BertModel父类的Bert4Seq2seq子类,对于这个Bert4Seq2seq子类,仅仅实现了不同的compute_attention_mask() 函数。
def compute_attention_mask(self, layer_id, segment_ids):
"""为seq2seq采用特定的attention mask """
# segment_ids是2D张量 形如[[0, 0, 0, 0, 0, 1, 1, 1]] 这种,其中数值0,1表示词到底属于哪个句 子,0表示词属于content,1表示当前词输入title。
# 这个函数被调用12次 其中seq2seq_attention_mask()函数被调用一次创建了a_mask张量,剩下11次的数据 应该都是使用第一次创建的a_mask,并没有产生新的张量
# 这里的layer_id 表示的就是层数编号,在起初定义的时候,作者的想法是“定义每一层的Attention Mask, 来实现不同的功能”,但是在实际实现的时候,并没有用到层数编号这个参数。
# 源码文件bert.py中第 139 行 开始调用此函数,并返回attention_mask,其中输入的参数为 层数编号i 和 segment_ids(s_in)
if self.attention_mask is None:
def seq2seq_attention_mask(s):
# 这个函数被调用一次
import tensorflow as tf
# 得到句子长度(first_seq_length + second_seq_length)
seq_len = K.shape(s)[1]
with K.name_scope('attention_mask'):
# 生成数值为1的 4维张量
ones = K.ones((1, 1, seq_len, seq_len))
# 在这部操作以后,全为1的4为张量,变成了下三角为1 上三角为0的对角矩阵
a_mask = tf.linalg.band_part(ones, -1, 0)
# 将segment_ids变成 [batch_size, 1,1, seq_length] 形状的张量 数值大小不变
s_ex12 = K.expand_dims(K.expand_dims(s, 1), 2)
# 将segment_ids变成 [