代码实现:https://github.com/ZhuiyiTechnology/simbert
环境:bert4keras 0.7.7
解码
def generate(self, text, n=1, topk=5):
token_ids, segment_ids = tokenizer.encode(text, max_length=maxlen)
output_ids = self.random_sample([token_ids, segment_ids], n, topk, topp=0.8) # 解码
return [tokenizer.decode(ids) for ids in output_ids]
解码方法主要参考的是自回归生成模型解码基类 AutoRegressiveDecoder
, 源于文件 https://github.com/bojone/bert4keras/blob/master/bert4keras/snippets.py
包含beam_search和random sample两种策略。
-
random_sample:返回的是多个解码序列,用于生成众多相似问。计算过程是生成每一个字的时候都是从当前预测概率值的topk中任意选一个。
-
beam_search:返回的是最优解码序列,用于生成最优解。计算过程是每次预测都取分数和的topk,遇到结束符返回。
random_sample
生成每个字的时候,都是从同时满足 topk和topp条件的候选项中随机选。
def random_sample(self, inputs, n, topk=None, topp=None):
"""随机采样n个结果
说明:非None的topk表示每一步只从概率最高的topk个中采样;而非None的topp
表示每一步只从概率最高的且概率之和刚好达到topp的若干个token中采样。
返回:n个解码序列组成的list。
"""
inputs = [np.array([i]) for i in inputs]
output_ids = self.first_output_ids
results = []
for step in range(self.maxlen):
probas = self.predict(inputs, output_ids, step, 'probas') # 计算当前概率
probas /= probas.sum(axis=1, keepdims=True) # 确保归一化
if step == 0: # 第1步预测后将结果重复n次
probas = np.repeat(probas, n, axis=0)
inputs = [np.repeat(i, n, axis=0) for i in inputs]
output_ids = np.repeat(output_ids, n, axis=0)
if topk is not None:
k_indices = probas.argpartition(-topk,
axis=1)[:, -topk:] # 仅保留topk
probas = np.take_along_axis(probas, k_indices, axis=1) # topk概率
probas /= probas.sum(axis=1, keepdims=True) # 重新归一化
if topp is not None:
p_indices = probas.argsort(axis=1)[:, ::-1] # 从高到低排序
probas = np.take_along_axis(probas, p_indices, axis=1) # 排序概率
cumsum_probas = np.cumsum(probas, axis=1) # 累积概率
flag = np.roll(cumsum_probas >= topp, 1, axis=1) # 标记超过topp的部分
flag[:, 0] = False # 结合上面的np.roll,实现平移一位的效果
probas[flag] = 0 # 后面的全部置零
probas /= probas.sum(axis=1, keepdims=True) # 重新归一化
"""
随机选取
"""
sample_func = lambda p: np.random.choice(len(p), p=p) # 按概率采样函数
sample_ids = np.apply_along_axis(sample_func, 1, probas) # 执行采样
sample_ids = sample_ids.reshape((-1, 1)) # 对齐形状
if topp is not None:
sample_ids = np.take_along_axis(
p_indices, sample_ids, axis=1
) # 对齐原id
if topk is not None:
sample_ids = np.take_along_axis(
k_indices, sample_ids, axis=1
) # 对齐原id
output_ids = np.concatenate([output_ids, sample_ids], 1) # 更新输出
"""
遇到结束符,进行保存
"""
if output_ids.shape[1] >= self.minlen: # 最短长度判断
flag = (sample_ids[:, 0] == self.end_id) # 标记已完成序列
if flag.any(): # 如果有已完成的
for ids in output_ids[flag]: # 存好已完成序列
results.append(ids)
flag = (flag == False) # 标记未完成序列
inputs = [i[flag] for i in inputs] # 只保留未完成部分输入
output_ids = output_ids[flag] # 只保留未完成部分候选集
if len(output_ids) == 0:
break
# 如果还有未完成序列,直接放入结果
for ids in output_ids:
results.append(ids)
# 返回结果
return results
回答以下三个问题:
-
topk
的作用 -
topp
的作用 -
代码中
sample_ids
的作用
假设入参为 n=10, topk=4, topp=0.8
,即表示想要生成 n
个相似问,每次都是从预测概率最高的 topk
中选择,且每一步只从概率最高的且概率之和刚好达到 topp
的若干个token中采样.
topk的作用
if topk is not None:
k_indices = probas.argpartition(-topk, axis=1)[:, -topk:] # 仅保留topk
probas = np.take_along_axis(probas, k_indices, axis=1) # topk概率
probas /= probas.sum(axis=1, keepdims=True) # 重新归一化
predict执行之后得到的probas
是每个字的概率。topk的作用是总中找到前 topk 个概率值,再进行归一化。
topp的作用
if topp is not None:
p_indices = probas.argsort(axis=1)[:, ::-1] # 从高到低排序
probas = np.take_along_axis(probas, p_indices, axis=1) # 排序概率
cumsum_probas = np.cumsum(probas, axis=1) # 累积概率
flag = np.roll(cumsum_probas >= topp, 1, axis=1) # 标记超过topp的部分
flag[:, 0] = False # 结合上面的np.roll,实现平移一位的效果
probas[flag] = 0 # 后面的全部置零
probas /= probas.sum(axis=1, keepdims=True) # 重新归一化
采用 print(variable[0,:])
以output的第一条数据的输出查看计算过程,之后再去理解numpy的操作会简单些。
选取topk后的probas [0.15219952 0.194854 0.19652945 0.45641702]
进入 topp 判断
p_indices [3 2 1 0]
probas [0.45641702 0.19652945 0.194854 0.15219952]
cumsum_probas [0.45641702 0.6529465 0.8478005 1. ]
flag [ True False False True]
flag [False False False True]
probas [0.45641702 0.19652945 0.194854 0. ]
probas [0.5383543 0.23181096 0.22983474 0. ]
大致的操作流程是:把topk的概率从大到小排序,依次叠加,大于topp后的概率不予采纳,即把例子中的 0.15219952 排除掉了,然后再重新将概率值归一化。
sample_ids的作用
经过上面的操作,可以对于每条output数据,都可以拿到<=topk
个候选,那选哪一个呢?这就是sample_ids那部分代码的用途了。
beam_search
def beam_search(self, inputs, topk):
"""beam search解码
说明:这里的topk即beam size;
返回:最优解码序列。
"""
inputs = [np.array([i]) for i in inputs]
output_ids, output_scores = self.first_output_ids, np.zeros(1)
for step in range(self.maxlen):
scores = self.predict(inputs, output_ids, step, 'logits') # 计算当前得分
if step == 0: # 第1步预测后将输入重复topk次
inputs = [np.repeat(i, topk, axis=0) for i in inputs]
scores = output_scores.reshape((-1, 1)) + scores # 综合累积得分
indices = scores.argpartition(-topk, axis=None)[-topk:] # 仅保留topk
indices_1 = indices // scores.shape[1] # 行索引
indices_2 = (indices % scores.shape[1]).reshape((-1, 1)) # 列索引
output_ids = np.concatenate([output_ids[indices_1], indices_2],
1) # 更新输出
output_scores = np.take_along_axis(
scores, indices, axis=None
) # 更新得分
if output_ids.shape[1] >= self.minlen: # 最短长度判断
best_one = output_scores.argmax() # 得分最大的那个
if indices_2[best_one, 0] == self.end_id: # 如果已经终止
return output_ids[best_one] # 直接输出
else: # 否则,只保留未完成部分
flag = (indices_2[:, 0] != self.end_id) # 标记未完成序列
if not flag.all(): # 如果有已完成的
inputs = [i[flag] for i in inputs] # 扔掉已完成序列
output_ids = output_ids[flag] # 扔掉已完成序列
output_scores = output_scores[flag] # 扔掉已完成序列
topk = flag.sum() # topk相应变化
# 达到长度直接输出
return output_ids[output_scores.argmax()]
关于 predict 中的参数 ‘logits’ 说明:
-
probas:返回归一化的概率
-
logits:返回softmax前的结果或者概率对数
确定topk
scores = output_scores.reshape((-1, 1)) + scores # 综合累积得分
indices = scores.argpartition(-topk, axis=None)[-topk:] # 仅保留topk
indices_1 = indices // scores.shape[1] # 行索引
indices_2 = (indices % scores.shape[1]).reshape((-1, 1)) # 列索引
假设当前 topk为5,已经生成了8个字;则output_scores
的大小为(5,)
scores为预测结果,大小为(5,Vocab_size)
-
第一步,求得分和,对scores增加output_scores对应行的分值。
-
第二步,从平铺的scores中找到分值的topk个。
-
第三步,确定topk对应在scores中的行和列索,便于后面更新
output_scores
和output_ids
结束条件
找到分值最大的,若当前生成的是结束符,则结束。
否则,过滤掉output_ids
中当前生成结束符的句子,更新topk,继续生成下一个字符。
走到生成最大长度也没遇到结束符的话,就返回分数和最高的那个句子。
优化方向
topk都要
目的:生成相似问
经实验,random的方式生成的相似问和原话都比较接近,多样性不够。改进方向:在random的基础上全要topk个,而后则有topk*topk个候选,若生成的target长度为m,则有 t o p k m topk^m topkm 种可能。(topk不能取太大也不能太小)这个需要实验看下解码效果。topk往大的取增加生成样本的多样性,太大会造成生成的问句太多,鉴于后续存在语义判别模型,可以接受topk往大的取。
语义判别模型:
-
默认要多少个相似问,参数n,对相似度排序之后取前n个生成问句。
-
根据simbert中的NLU部分得到问句a和问句b的相似度。 / 生成用的是simbert的模型,再采用他的模型判断相似度大概率是相似的,没有太大意思,这部分换成自己做的语义匹配模型得到相似度
减少人工标注:
将生成的问句走一趟问答模块,可以把回答正确的语料标注出来。剩下回答错误的需要人工标注生成的相似问是否可采用。
def get_random_all(self, text, topk=5, size=50):
token_ids, segment_ids = tokenizer.encode(text, max_length=maxlen)
inputs = [np.array([i]) for i in [token_ids, segment_ids]]
output_ids = self.first_output_ids # []
results = []
for step in range(self.maxlen):
if len(results)>size:
break
yuan = []
for i in range(0, output_ids.shape[0], 64): # batchsize太大,预测不了,暂定64一批
t_inputs = [ins[i:i+64] for ins in inputs]
yuan.append(self.predict(t_inputs, output_ids[i:i+64], step, 'probas'))
probas = np.concatenate(yuan, 0)
# probas = self.predict(inputs, output_ids, step, 'probas') # 计算当前概率
probas /= probas.sum(axis=1, keepdims=True) # 确保归一化
k_indices = probas.argpartition(-topk, axis=1)[:, -topk:]
output_ids = np.repeat(output_ids, topk, axis=0)
output_ids = np.concatenate([output_ids, k_indices.reshape((-1, 1))], 1)
inputs = [np.repeat(i, topk, axis=0) for i in inputs]
if output_ids.shape[1] >= self.minlen:
flag = (output_ids[:, -1] == self.end_id)
if flag.any():
tmp = np.argwhere(flag == True).flatten()
results.extend([output_ids[tmp[i], :] for i in range(tmp.shape[0])])
output_ids = np.delete(output_ids, tmp, axis = 0)
inputs = [np.delete(i, tmp, axis=0) for i in inputs]
return [tokenizer.decode(ids) for ids in results]
topk递减
选取的topk递减,降低一下生成候选数量。
def get_decay_topk(self, text, topk=8):
token_ids, segment_ids = tokenizer.encode(text, max_length=maxlen)
inputs = [np.array([i]) for i in [token_ids, segment_ids]]
output_ids = self.first_output_ids # []
results = []
for step in range(self.maxlen):
if step!=0 and output_ids.shape[0]==0:
break
yuan = []
for i in range(0, output_ids.shape[0], 64):
t_inputs = [ins[i:i+64] for ins in inputs]
yuan.append(self.predict(t_inputs, output_ids[i:i+64], step, 'probas'))
probas = np.concatenate(yuan, 0)
# probas = self.predict(inputs, output_ids, step, 'probas') # 计算当前概率
probas /= probas.sum(axis=1, keepdims=True) # 确保归一化
k_indices = probas.argpartition(-topk, axis=1)[:, -topk:]
output_ids = np.repeat(output_ids, topk, axis=0)
output_ids = np.concatenate([output_ids, k_indices.reshape((-1, 1))], 1)
inputs = [np.repeat(i, topk, axis=0) for i in inputs]
topk = int(topk) if topk==1 else int(topk/2)
if output_ids.shape[1] >= self.minlen:
flag = (output_ids[:, -1] == self.end_id)
if flag.any():
tmp = np.argwhere(flag == True).flatten()
results.extend([output_ids[tmp[i], :] for i in range(tmp.shape[0])])
output_ids = np.delete(output_ids, tmp, axis = 0)
inputs = [np.delete(i, tmp, axis=0) for i in inputs]
for ids in output_ids:
results.append(ids)
return [tokenizer.decode(ids) for ids in results]