背景介绍以及相关概念
本项目对3种常用的文本匹配的方法进行实现:PointWise(单塔)、DSSM(双塔)、Sentence BERT(双塔)。
文本匹配(Text Matching)是 NLP 下的一个分支,通常用于计算两个句子之间的相似程度,在推荐、推理等场景下都有着重要的作用。
举例来讲,今天我们有一堆评论数据,我们想要找到指定类别的评论数据,例如:
在这一堆评论中我们想找到跟「水果」相关的评论,那么第 3 条评论就应该被召回。这个问题可以被建模为文本分类对吧,通过训练一个文本分类模型也能达到同样的目的。
但,分类模型的主要问题是:分类标签是固定的。假如在训练的时候标签集合是「洗浴,电脑,水果」,今天再来一条「服饰」的评论,那么模型依旧只能在原有的标签集合里面进行推理,无论推到哪个都是错误的。因此,我们需要一个能够有一定自适应能力的模型,在加入一些新标签后不用重训模型也能很好的完成任务。
文本匹配目前比较常用的有两种结构:
- 单塔模型:准确率高,但计算速度慢。
- 双塔模型:计算速度快,准确率相对低一些。
下面我们对这两种方法分别进行介绍。
0.1 单塔模型
单塔模型顾名思义,是指在整个过程中只进行一次模型计算。这里的「塔」指的是进行「几次模型计算」,而不一定是「模型个数」,这个我们会放到双塔部分解释。在单塔模型下,我们需要把两句文本通过 [SEP] 进行拼接,将拼接好的数据喂给模型,通过 output 中的[CLS] token 做一个二分类任务。
单塔模型的 forward 部分长这样,完整源码在文末:
单塔模型的优势在于准确率较高,但缺点在于:计算慢。
- 为什么慢呢?
举例来讲,如果今天我们有三个类别「电脑、水果、洗浴」,那我们就需要将一句话跟每个类别都做一次拼接,并喂给模型去做推理:
那如果类别数目到达成百上千时,就需要拼接上千次,为了判断一个样本就需要过上次模型,而大模型的计算通常来讲是非常耗时的,这就导致了在类别数目很大的情况下,单塔模型的效率往往无法满足人们的需求。
0.2 双塔模型
单塔模型的劣势很明显,有多少类别就需要算多少次。但事实上,这些类别都是不会变的,唯一变的只有新的评论数据。所以我们能不能实现将这些不会变的「类别信息」「提前计算」存下来,只计算那些没有见过的「评论数据」呢?这就是双塔模型的思想。双塔模型的「双塔」含义就是:两次模型计算。即,类别特征计算一次,评论特征计算一次。
通过上图可以看到,「类别」和「评论」不再是拼接在一起喂给模型,而是单独喂给模型,并分别得到各自的 embedding 向量,再进行后续的计算。而上图中左右两个两个模型可以使用同一个模型(这种方式叫:同构模型),也可以用两个不同的模型(这种方式叫:异构模型)。因此「双塔」并不一定代表存在两个模型,而是代表需要需要进行两次模型计算。
0.2.1 DSSM(Deep Structured Semantic Models,深度结构化语义模型)
Paper Reference: https://posenhuang.github.io/papers/cikm2013_DSSM_fullversion.pdf
DSSM 是一篇比较早期的 paper,我们主要借鉴其通过 embedding 之间的余弦相似度进行召回排序的思想。我们分别将「类别」和「评论」文本过一遍模型,并得到两段文本的 embedding。将匹配的 pair 之间的余弦相似度 label 置为 1,不匹配的 pair 之间余弦相似度 label 置为 0。
Note: 余弦相似度的取值范围是 [-1, 1],但为了方便我将 label 置为 0 并用 MSE 去训练,也能取得不错的效果。
训练数据集长这样,匹配 pair 标签为 1,不匹配为 0:
实现中有两个关键函数:获得句子的 embedding 函数(用于推理)、获得句子对的余弦相似度(用于训练):
0.2.2 Sentence Transformer
Paper Reference: https://arxiv.org/pdf/1908.10084.pdf
Sentence Transformer 也是一个双塔模型,只是在训练时不直接对两个句子的 embedding 算余弦相似度,而是将这两个 embedding 和 embedding 之间的差向量进行拼接,将这三个向量拼好后喂给一个判别层做二分类任务。
原 paper 中在 inference 的时候不再使用训练架构,而是采用了余弦相似度的方法做召回。但我在实现的时候在推理部分仍然沿用了训练的模型架构,原因是想抹除结构不一致的 gap,并且训练层也只是多了一层 Linear 层,在推理的时候也不至于消耗过多的时间。Sentence Transformer 在推理时需要同时传入「当前评论信息」以及事先计算好的「所有类别 embedding」,如下:
1. 环境安装
本项目基于 pytorch
+ transformers
实现,运行前请安装相关依赖包:
2. 数据集准备
项目中提供了一部分示例数据,我们使用「商品评论」和「商品类别」来进行文本匹配任务,数据在 data/comment_classify
。
若想使用自定义数据
训练,只需要仿照示例数据构建数据集即可:
每一行用 \t
分隔符分开,第一部分部分为商品类型(text1)
,中间部分为商品评论(text2)
,最后一部分为商品评论和商品类型是否一致(label)
。
3. 有监督-模型训练
3.1 PointWise(单塔)
3.1.1 模型训练
修改训练脚本 train_pointwise.sh
里的对应参数, 开启模型训练:
正确开启训练后,终端会打印以下信息:
在 logs/comment_classify
文件下将会保存训练曲线图:
3.1.2 模型推理
完成模型训练后,运行 inference_pointwise.py
以加载训练好的模型并应用:
运行推理程序:
得到以下推理结果:
3.2 DSSM(双塔)
3.2.1 模型训练
修改训练脚本 train_dssm.sh
里的对应参数, 开启模型训练:
正确开启训练后,终端会打印以下信息:
在 logs/comment_classify
文件下将会保存训练曲线图:
3.2.2 模型推理
和单塔模型不一样的是,双塔模型可以事先计算所有候选类别的Embedding,当新来一个句子时,只需计算新句子的Embedding,并通过余弦相似度找到最优解即可。
因此,在推理之前,我们需要提前计算所有类别的Embedding并保存。
类别Embedding计算
运行 get_embedding.py
文件以计算对应类别embedding并存放到本地:
其中,所有需要预先计算的内容都存放在 types_desc.txt
文件中。
文件用 \t
分隔,分别代表 类别id
、类别名称
、类别描述
:
执行 python get_embeddings.py
命令后,会在代码中设置的embedding存放地址中找到对应的embedding文件:
模型推理
完成预计算后,接下来就可以开始推理了。
我们构建一条新评论:这个破笔记本卡的不要不要的,差评
。
运行 python inference_dssm.py
,得到下面结果:
函数将输出(类别,余弦相似度)的二元组,并按照相似度做倒排(相似度取值范围:[-1, 1])。
3.3 Sentence Transformer(双塔)
3.3.1 模型训练
修改训练脚本 train_sentence_transformer.sh
里的对应参数, 开启模型训练:
正确开启训练后,终端会打印以下信息:
在 logs/comment_classify
文件下将会保存训练曲线图:
3.2.2 模型推理
Sentence Transformer 同样也是双塔模型,因此我们需要事先计算所有候选文本的embedding值。
类别Embedding计算
运行 get_embedding.py
文件以计算对应类别embedding并存放到本地:
其中,所有需要预先计算的内容都存放在 types_desc.txt
文件中。
文件用 \t
分隔,分别代表 类别id
、类别名称
、类别描述
:
执行 python get_embeddings.py
命令后,会在代码中设置的embedding存放地址中找到对应的embedding文件:
模型推理
完成预计算后,接下来就可以开始推理了。
我们构建一条新评论:这个破笔记本卡的不要不要的,差评
。
运行 python inference_sentence_transformer.py
,函数会输出所有类别里「匹配通过」的类别及其匹配值,得到下面结果:
函数将输出(匹配通过的类别,匹配值)的二元组,并按照匹配值(越大则越匹配)做倒排。