文本匹配开山之作--双塔模型及实战
作者 | 夜小白
整理 | NewBeeNLP
在前面一篇文章中,总结了Representation-Based文本匹配模型的改进方法,
基于表征(Representation)的文本匹配、信息检索、向量召回的方法总结
其中在一篇论文中提到了使用Pre-train方式来提高效果,论文连接如下:
Pre-training Tasks for Embedding-based Large-scale Retrieval[1]
谷歌 ICLR 2020 | 向量化召回也需要『预训练』
论文中提到的预训练数据均为,relevant positive Query-Doc 对:
训练的目标为最大化当前Postive Query-Doc的Softmax条件概率:
论文中提到,softxmax分母中的
为所有可能的文档集合,这样的话候选文档集合非常大,所以论文中做了近似,「训练时使用当前batch中文档这个子集来代替全集」 ,这种方法称为
Sample Softmax
。
TensorFlow中也有这个方法的API实现,但是我一直不是很能理解代码中到底应该怎么实现,突然这几天读到了文本匹配的开山之作 「DSSM」,我发现「DSSM」的训练方法与上面那篇论文非常类似,于是研究了一下源码,有一种豁然开朗的感觉,所以想分享一下,我对这种训练方式的理解。DSSM论链接如下:
Learning deep structured semantic models for web search using clickthrough data.[2]
DSSM论文中的训练数据也是Query-Document对,训练目标也为最大化给定Query下点击Doc的条件概率,公式如下,和上面说的Pre-train任务基本一致:
极大似然估计的公式基本一样,训练都是Point-wise loss,具体各个符号我在下面仔细介绍。
DSSM框架简要介绍
作为文本匹配方向的开山之作,已经有非常多的博客介绍了这个模型,这里我就简单介绍一下,重点放在后面训练源码的阅读。
模型结构
DSSM也是Representation-Based模型,其中Query端 Encoder 和 Doc端 Encoder都是使用 MLP实现,最后Score计算使用的是cosine similarity,后续模型的改进很多都是使用更好的Encoder结构。
输入
DSSM中输入并不是单纯直接使用 bag-of-word,从上面结构图可以看出,输入的时候做了Word Hashing,在进行bag-of-word映射,目的主要如下:
减少词典的大小,直接使用原始word词典非常大(500K),导致输入向量的维数也非常高,使用Word Hashing做分解后,可以减少词典大小,比如letter-trigram(30K)
一定程度解决OOV问题
对拼写错误也有帮助
Word Hashing的做法类似于fast-text中的子词分解,但是不同点在于
fast-text中会取多个不同大小窗口对一个单词进行分解,比如2、3、4、5,词表是这些所有的子词构成的集合
Word Hashing只会取一个固定大小窗口对单词进行分解,词表是这个固定大小窗口子词的集合,比如letter-bigram,letter-trigram
比如输入的词为#good#
,我们选「tri-gram」,则Word-hashing分解后,#good#
的表示则为#go,goo,ood,od#
,然后就是输入的每个词都映射为tri-gram bag-of-words 向量,出现了的位置为1,否则为0。假设数据集进行「tri-gram」分解后,构成的词表大小为N,那么Query输入处理方式如下:
首先将每个词进行Word Hashing分解
获得每个词的表示,比如 [0,1,1,0,0,0...,0,1] ,维数为N,其中在词表中出现了的位置为1,否则为0
将Query中所有的词的表示向量相加可以得到一个N维向量,「其实就是bag-of-word表示」(只考虑有没有出现,并不考虑出现顺序位置)
Doc端输入的处理也类似于上面Query端的处理,获得Word-Hashing后的向量表示,作为整个模型的输入。
Encoder层
Query端和Doc端Encoder层处理很简单,就是MLP,计算公式如下:
可以看出就是标准的全连接层运算
相似度Score计算
DSSM中最后的相似度计算用的是 cosine similarity,计算公式如下:
模型训练好之后,给定一个Query我们就可以对其所有Doc按照这个计算出来的cosine similarity进行排序。
训练方式
训练数据
DSSM的训练方式是做Point-wise训练,论文中对于训练数据的描述如下:
The clickthrough logs consist of a list of queries and their clicked documents.
给定的是Query以及对应的点击Document,我们需要进行极大似然估计。
训练目标
DSSM首先通过获得的semantic relevance score
计算在给定Query下Doc的后验概率:
其中
为softmax函数的平滑因子, 表示所有的待排序的候选文档集合,可以看出这个目标其实和我们一开始提到的Pre-train那篇论文的目标是一样的。我们的候选文档大小可能会非常大,论文在实际训练中,做法如下:
我们使用
来表示一个(Query,Doc)对,其中 表示这个Doc是被点击过的使用
和四个随机选取没有被点击过的Doc来近似全部文档集合 ,其中 表示负样本
上面就是训练时候的实际做法,对于每个
,我们只需要采样K个负样本(K可以自己定), ,这样softxmax操作我们也只需要在 这个集合上计算即可,论文中还提到,采样负样本方式对最终结果没有太大影响
In our pilot study, we do not observe any significant difference when different sampling strategies were used to select the unclicked documents.
最后loss选用的就是交叉熵损失:
训练方式总结
通过上面的分析,我的理解是DSSM和之前说的Pre-trian那篇论文,训练的时候只需要采样负样本即可,然后softmax操作只在 当前正样本 + 采样的负样本 集合上计算,最后用交叉熵损失即可。具体负样本怎么采样,我觉的有两种方法:
输入数据中就已经采样好负样本,输入数据直接是正样本 + 负样本,这样运算量会大些
输入数据batch均为正样本,负样本通过batch中其他Doc构造
DSSM源码阅读
我看的DSSM实现代码是下面两个,其中的不同点就在于上面说的负样本构造不同
训练数据中输入有负样本:InsaneLife/dssm[3]
使用一个batch中其他Doc构造负样本:LiangHao151941/dssm[4]
训练数据中输入有负样本的情况
这部分代码在
https://github.com/InsaneLife/dssm/blob/master/dssm_rnn.py
输入数据
with tf.name_scope('input'):# 预测时只用输入query即可,将其embedding为向量。query_batch = tf.placeholder(tf.int32, shape=[None, None], name='query_batch')doc_pos_batch = tf.placeholder(tf.int32, shape=[None, None], name='doc_positive_batch')doc_neg_batch = tf.placeholder(tf.int32, shape=[None, None], name='doc_negative_batch')query_seq_length = tf.placeholder(tf.int32, shape=[None], name='query_sequence_length')pos_seq_length = tf.placeholder(tf.int32, shape=[None], name='pos_seq_length')neg_seq_length = tf.placeholder(tf.int32, shape=[None], name='neg_sequence_length')on_train = tf.placeholder(tf.bool)drop_out_prob = tf.placeholder(tf.float32, name='drop_out_prob')
doc_pos_batch , 即是论文中说的 $D^+# ,正样本输入
doc_neg_batch,即是论文汇总说的
,负样本输入集合
def pull_batch(data_map, batch_id):query_in = data_map['query'][batch_id * query_BS:(batch_id + 1) * query_BS]query_len = data_map['query_len'][batch_id * query_BS:(batch_id + 1) * query_BS]doc_positive_in = data_map['doc_pos'][batch_id * query_BS:(batch_id + 1) * query_BS]doc_positive_len = data_map['doc_pos_len'][batch_id * query_BS:(batch_id + 1) * query_BS]doc_negative_in = data_map['doc_neg'][batch_id * query_BS * NEG:(batch_id + 1) * query_BS * NEG]doc_negative_len = data_map['doc_neg_len'][batch_id * query_BS * NEG:(batch_id + 1) * query_BS * NEG]# query_in, doc_positive_in, doc_negative_in = pull_all(query_in, doc_positive_in, doc_negative_in)return query_in, doc_positive_in, doc_negative_in, query_len, doc_positive_len, doc_negative_len
这是准备每个batch数据的代码,其中query_BS
为batch_size,NEG
为负样本采样个数。
合并正负样本与计算余弦相似度
从论文中可以知道,我们需要对「每个Query」选取
这个集合做softmax操作,所以我们计算出每个Query正负样本的Score之后,需要将同一个Query正负样本其合并到一起,Score即为softmax输入的logits。「由于输入数据中直接有负样本」,所以这里不需要我们构造负样本,直接把负样本输出的Score concat即可。下面代码步骤如下:
先把同一个Query下pos_doc和neg_doc经过Encoder之后的隐层表示concat到一起
计算每个Query与正负样本的similarity
计算出来的cosine similarity Tensor如下,每一行是一个Query下正样本和负样本的sim,这样我们在axis = 1
上做softmax操作即可:
[[query[1]_pos,query[1]_neg[1],query[1]_neg[2],query[1]_neg[3],...],[query[2]_pos,query[2]_neg[1],query[2]_neg[2],query[2]_neg[3],...],......,[query[n]_pos,query[n]_neg[1],query[n]_neg[2],query[n]_neg[3],...],
]
with tf.name_scope('Merge_Negative_Doc'):# 合并负样本,tile可选择是否扩展负样本。# doc_y = tf.tile(doc_positive_y, [1, 1])# 此时doc_y为单独的pos_doc的hidden representationdoc_y = tf.tile(doc_pos_rnn_output, [1, 1])#下面这段代码就是把同一个Query下的neg_doc合并到pos_doc,后续才能计算score 和 softmaxfor i in range(NEG):for j in range(query_BS):# slice(input_, begin, size)切片API# doc_y = tf.concat([doc_y, tf.slice(doc_negative_y, [j * NEG + i, 0], [1, -1])], 0)doc_y = tf.concat([doc_y, tf.slice(doc_neg_rnn_output, [j * NEG + i, 0], [1, -1])], 0)with tf.name_scope('Cosine_Similarity'):# Cosine similarity# query_norm = sqrt(sum(each x^2))query_norm = tf.tile(tf.sqrt(tf.reduce_sum(tf.square(query_rnn_output), 1, True)), [NEG + 1, 1])# doc_norm = sqrt(sum(each x^2))doc_norm = tf.sqrt(tf.reduce_sum(tf.square(doc_y), 1, True))prod = tf.reduce_sum(tf.multiply(tf.tile(query_rnn_output, [NEG + 1, 1]), doc_y), 1, True)norm_prod = tf.multiply(query_norm, doc_norm)# cos_sim_raw = query * doc / (||query|| * ||doc||)cos_sim_raw = tf.truediv(prod, norm_prod)# gamma = 20cos_sim = tf.transpose(tf.reshape(tf.transpose(cos_sim_raw), [NEG + 1, query_BS])) * 20# cos_sim 作为softmax logits输入
softmax操作与计算交叉熵损失
上一步中已经计算出各个Query对其正负样本的cosine similarity,这个将作为softmax输入的logits,然后计算交叉熵损失即可,「因为只有一个正样本,而且其位置在第一个」,所以我们的标签one-hot编码为:
[1,0,0,0,0,0,....,0]
所以我们计算交叉熵损失的时候,「只需要取第一列的概率值即可」:
with tf.name_scope('Loss'):# Train Loss# 转化为softmax概率矩阵。prob = tf.nn.softmax(cos_sim)# 只取第一列,即正样本列概率。相当于one-hot标签为[1,0,0,0,.....,0]hit_prob = tf.slice(prob, [0, 0], [-1, 1])loss = -tf.reduce_sum(tf.log(hit_prob))tf.summary.scalar('loss', loss)
使用一个batch中其他Doc构造负样本
上面的方法是在输入数据中直接有负样本,这样计算的时候需要多计算负样本的representation,在输入数据batch中可以只包含正样本,然后再选择同一个batch中的其他Doc构造负样本,这样可以减少计算量
这部分代码在
https://github.com/LiangHao151941/dssm/blob/master/single/dssm_v3.py
输入数据
with tf.name_scope('input'):# Shape [BS, TRIGRAM_D].query_batch = tf.sparse_placeholder(tf.float32, shape=query_in_shape, name='QueryBatch')# Shape [BS, TRIGRAM_D]doc_batch = tf.sparse_placeholder(tf.float32, shape=doc_in_shape, name='DocBatch')
可以看出这里的输入数据只有
,并没有负样本
构造负样本并计算余弦相似度
由于输入数据中没有负样本,所以使用同一个batch中的其他Doc做为负样本,由于所有输入Doc representation在前面已经计算出来了,所以不需要额外再算一遍了,下面的代码就是通过rotate 输入
,来构造负样本,比如:
输入为 ,对于每一个
,除了 ,这个batch中的其他Doc均为负样本那么对于
, 均为视为 ,可以构造负样本为
with tf.name_scope('FD_rotate'):# Rotate FD+ to produce 50 FD-temp = tf.tile(doc_y, [1, 1])for i in range(NEG):rand = int((random.random() + i) * BS / NEG)doc_y = tf.concat(0,[doc_y,tf.slice(temp, [rand, 0], [BS - rand, -1]),tf.slice(temp, [0, 0], [rand, -1])])
with tf.name_scope('Cosine_Similarity'):# Cosine similarityquery_norm = tf.tile(tf.sqrt(tf.reduce_sum(tf.square(query_y), 1, True)), [NEG + 1, 1])doc_norm = tf.sqrt(tf.reduce_sum(tf.square(doc_y), 1, True))prod = tf.reduce_sum(tf.mul(tf.tile(query_y, [NEG + 1, 1]), doc_y), 1, True)norm_prod = tf.mul(query_norm, doc_norm)cos_sim_raw = tf.truediv(prod, norm_prod)cos_sim = tf.transpose(tf.reshape(tf.transpose(cos_sim_raw), [NEG + 1, BS])) * 20
softmax操作与计算交叉熵损失
这一步和前面说的是一样的
with tf.name_scope('Loss'):# Train Lossprob = tf.nn.softmax((cos_sim))hit_prob = tf.slice(prob, [0, 0], [-1, 1])loss = -tf.reduce_sum(tf.log(hit_prob)) / BStf.scalar_summary('loss', loss)
总结
之前一直对于sampled softmax不太理解,不知道在实际训练中如何做。但是看了DSSM论文和源码之后,真的有一种拨开云雾见月明的感觉,这种训练方式的核心就在于「构造负样本」,这样一说感觉和Pairwise loss中构造pair又有点类似,不过这里构造的不止一个负样本,训练目标也是pointwise,这种方式应该是不需要用到TensorFlow中的tf.nn.sampled_softmax_loss
这个函数。
当然上面都是个人理解,最近越来越觉得真正要弄懂一个算法不单要理解数学原理,而且需要去读懂源码,很多在论文中理解不了的信息,在源码中都会清晰的展现出来,这部分我也一直在探索中,之后有什么心得再分享给大家啦~
一起交流
想和你一起学习进步!『NewBeeNLP』目前已经建立了多个不同方向交流群(机器学习 / 深度学习 / 自然语言处理 / 搜索推荐 / 图网络 / 面试交流 / 等),名额有限,赶紧添加下方微信加入一起讨论交流吧!(注意一定要备注信息才能通过)
本文参考资料
[1]
Pre-training Tasks for Embedding-based Large-scale Retrieval: https://arxiv.org/pdf/2002.03932.pdf
[2]
Learning deep structured semantic models for web search using clickthrough data.: https://dl.acm.org/doi/10.1145/2505515.2505665
[3]
InsaneLife/dssm: https://github.com/InsaneLife/dssm
[4]
LiangHao151941/dssm: https://github.com/LiangHao151941/dssm
- END -
妙啊!类别不平衡上的半监督学习
2021-07-23
聊一聊北美算法工程师日常
2021-07-15
我从吴恩达AI For Everyone中学到的十个重要AI观
2021-07-14
聊一聊 “超 大 模 型”
2021-07-11
文本匹配开山之作--双塔模型及实战相关推荐
- 文本匹配开山之作-DSSM论文笔记及源码阅读(类似于sampled softmax训练方式思考)
文章目录 前言 DSSM框架简要介绍 模型结构 输入 Encoder层 相似度Score计算 训练方式解读 训练数据 训练目标 训练方式总结 DSSM源码阅读 训练数据中输入有负样本的情况 输入数据 ...
- 谈谈文本匹配和多轮检索
非常详细全面的文本匹配和多轮检索发展整理,建议收藏 1. 关于文本匹配 文本匹配是NLP的基础任务之一,按照论文中的实验对传统的文本匹配任务进行分类,大致可以分为「文本检索(ad-hoc),释义识别 ...
- bert模型可以做文本主题识别吗_文本匹配方法系列––BERT匹配模型
1.概述 在介绍深层次交互匹配方法之前,本文接着多语义匹配方法[1]介绍基于BERT模型实现文本匹配的方法.将其单独介绍主要因为BERT实现文本匹配操作方便且效果优秀,比较适用于工业应用场景.关于be ...
- NLP-文本蕴含(文本匹配):概述【单塔模型、双塔模型】
一.什么是文本蕴含识别 文本间的推理关系,又称为文本蕴含关系 (TextualEntailment),作为一种基本的文本间语义联系,广泛存在于自然语言文本中.简单的来说文本蕴含关系描述的是两个文本之间 ...
- nc65语义模型设计_文本匹配方法系列––多维度语义交互匹配模型
摘要 本文基于接着多语义匹配模型[1]和BERT匹配模型[2]介绍一些多维度语义交互匹配模型,包括2017 BiMPM模型[3]和腾讯出品的2018 MIX[4].这些方法的核心特征都是在多语义网络的 ...
- laravel 分词搜索匹配度_DSSM文本匹配模型在苏宁商品语义召回上的应用
文本匹配是自然语言处理中的一个核心问题,它不同于MT.MRC.QA 等end-to-end型任务,一般是以文本相似度计算的形式在应用系统中起核心支撑作用1.它可以应用于各种类型的自然语言处理任务中,例 ...
- 匹配网络(Learning to Rank、单双塔模型)
参考:Learning to Rank Learning to Rank: pointwise.pairwise.listwise LTR(Learning to rank)是一种监督学习(Super ...
- 【文本匹配】表示型模型
基于表示的匹配模型的基本结构包括: (1)嵌入层,即文本细粒度的嵌入表示: (2)编码层,在嵌入表示的基础上进一步编码: (3)表示层:获取各文本的向量表征: (4)预测层:对文本pair的向量组进行 ...
- nmt模型源文本词项序列_「自然语言处理(NLP)」阿里团队--文本匹配模型(含源码)...
来源:AINLPer微信公众号 编辑: ShuYini 校稿: ShuYini 时间: 2019-8-14 引言 两篇文章与大家分享,第一篇作者对通用文本匹配模型进行探索,研究了构建一个快速优良的文本 ...
最新文章
- 基于多视图几何方式的三维重建
- 2018 Multi-University Training Contest 7
- 实际价格计算:确定方法
- golang 文件(文件打开,文件写入,文件读取,文件删除)的基本操作
- ajax接收到的数据是一个页面的代码的原因
- PE文件格式详解(二)
- 如何实现python连续输入
- protel 99se 负片打印
- java--实现j2cache二级缓存
- labuladong 公众号的使用方法
- Prometheus 监控详解
- 蝉知CMS7.0.1后台模板Getshell
- Dubbo源码解析-——服务导出
- eclipse离线安装PyDev
- NLP文本情感分析:测试集loss比训练集loss大很多,训练集效果好测试集效果差的原因
- Python有什么用?Python 的 10 个实际用途
- 现在的你对未来什么规划?
- 劳伦杰克逊写给姚明的情书:你的名字
- 鼠标突然无反应,鼠标灯亮,鼠标灯不亮
- H5U的一个比较完整的程序框架 伺服控制是ETHERCAT总线 气缸的控制宝库伸出、缩回、报警 轴的控制是分为通讯
热门文章
- 关系网络实战|设备关联信息定位团伙欺诈
- prd移动端通用产品需求文档+Axure高保真app社交订餐通用prd文档+产品业务说明+PRD功能性需求+移动端公工通用模板说明+需求分析+竞品分析+产品结构图+产品业务流程图+产品信息图+餐饮系统
- JavaScript 工作必知(九)function 说起 闭包问题
- 转载python2进制打包相关
- hdu 4006 The kth great number (优先队列)
- (转)一步一步Asp.Net MVC系列_权限管理之权限控制
- JZOJ 3455. 【NOIP2013模拟联考3】库特的向量(code)
- 【Android】Binder机制
- Java面向对象编程 第一章 面向对象开发方法概述
- 关于AE大数据点文件读取生成SHP文件时使用IFeatureBuffer快速提高读取效率