simbert生成相似问

苏神讲解的simbert

代码实现: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_scoresoutput_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]
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值