数据来源
- C2 坐席半年的对话日志数据(20220301~20220420),通过二八分拆出训练集和评测集;
数据清洗
- 全小写
- 替换html标签、时间(年月日时分秒)、手机号&尾号等
- 过滤无意义数据(如:“转人工”、“服务评价”、“感谢您的咨询”)
- 过滤无意义单字Query
- 添加特殊占位符[pic]、[http]、[subphone]、[phone]、[alnum]、[ques]、[know]
- 按时间 createtime 对dataframe排序
- 过滤重复上下文 && 过滤重复答案
- 全局随机采样负例(easy),使得测试集中「正负比例」=1:1
训练目标
对于话术推荐,输入一个问句,希望输出话术库中与其最合适的回复(以坐席的回复作为话术库)。抛开某些约束规则不说,可以发现该任务本质上都可以抽象为Seq2Seq,即:“输入一个句子,输出另一个句子”。进一步利用前缀树来约束解码过程,即可保证生成的结果在要求的话术库中。
模型输入数据构造
将用户上下文 query (context=3)以及坐席客服回复 A(type=out)用[SEP]占位符拼接;
其中 Q 表示用户输入 query(type=in),每条的最大长度为 32;A 表示坐席回复 answer(type=out),每条的最大长度为 64。注意:如果存在坐席连续回复,则通过句号将连续的多个回复拼接起来;
模型结构
训练阶段
以“BERT+UniLM”为基础架构,训练一个Seq2Seq模型。
- 采用Seq2Seq的经典训练方式 Teacher Forcing,输入上下文Q和坐席回复A[:-1],预测A[1:],即将目标A错开一位来训练。PLM 使用腾讯UER开源的BERT权重,优化器是Adam,学习率是2e-5,batch_size=16,大概需要训练15~20个epoch,以 bleu 评测结果作为模型保存依据。
- 借鉴 UNILM 方案,通过添加一个特别的Mask矩阵,直接用单个Bert模型实现 Seq2Seq LM 任务(无需修改模型架构,且可以直接沿用Bert的 Masked Language Model 预训练权重)。Mask矩阵如下图所示,作用是让Bert输入部分的Attention是双向的,输出部分的Attention是单向,从而满足Seq2Seq的要求。
损失函数
seq2seq 在生成输出序列的时候是一个time_step生成一个字,换句话说,在每个时间步都在解决一个分类问题。所以这里选择最常用的交叉熵损失函数,但需要利用上述的Mask来屏蔽掉上下文部分的loss(置为零)
推理阶段
利用坐席回复(即答案A)构建前缀树,然后按照前缀树进行 beam search 解码。
- 在 beam search 中采用基于Trie树的剪枝方法:将目标词建立成Trie树结构,在decode过程中根据beam中的前项序列在Trie树中搜索候选的后项token。然后,把模型预测其他字的概率都置零,保证解码过程只走前缀树的分支,而且必须走到最后,这样可以大幅缩减搜索空间及计算开销,同时也保证了生成的句子必然在话术库中;
- 前缀树叶节点保存坐席回复对应的富文本格式;
- 目前支持beam_search 和 random_sample 两种解码策略,通过如下公式计算序列概率:
评估指标
参考《2022-03-18-生成模型的评估指标》,主要以 BLEU 指标作为选模型的依据;