作者 | 夜小白

整理 | 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

文本匹配开山之作--双塔模型及实战相关推荐

  1. 文本匹配开山之作-DSSM论文笔记及源码阅读(类似于sampled softmax训练方式思考)

    文章目录 前言 DSSM框架简要介绍 模型结构 输入 Encoder层 相似度Score计算 训练方式解读 训练数据 训练目标 训练方式总结 DSSM源码阅读 训练数据中输入有负样本的情况 输入数据 ...

  2. 谈谈文本匹配和多轮检索

    非常详细全面的文本匹配和多轮检索发展整理,建议收藏 1. 关于文本匹配  文本匹配是NLP的基础任务之一,按照论文中的实验对传统的文本匹配任务进行分类,大致可以分为「文本检索(ad-hoc),释义识别 ...

  3. bert模型可以做文本主题识别吗_文本匹配方法系列––BERT匹配模型

    1.概述 在介绍深层次交互匹配方法之前,本文接着多语义匹配方法[1]介绍基于BERT模型实现文本匹配的方法.将其单独介绍主要因为BERT实现文本匹配操作方便且效果优秀,比较适用于工业应用场景.关于be ...

  4. NLP-文本蕴含(文本匹配):概述【单塔模型、双塔模型】

    一.什么是文本蕴含识别 文本间的推理关系,又称为文本蕴含关系 (TextualEntailment),作为一种基本的文本间语义联系,广泛存在于自然语言文本中.简单的来说文本蕴含关系描述的是两个文本之间 ...

  5. nc65语义模型设计_文本匹配方法系列––多维度语义交互匹配模型

    摘要 本文基于接着多语义匹配模型[1]和BERT匹配模型[2]介绍一些多维度语义交互匹配模型,包括2017 BiMPM模型[3]和腾讯出品的2018 MIX[4].这些方法的核心特征都是在多语义网络的 ...

  6. laravel 分词搜索匹配度_DSSM文本匹配模型在苏宁商品语义召回上的应用

    文本匹配是自然语言处理中的一个核心问题,它不同于MT.MRC.QA 等end-to-end型任务,一般是以文本相似度计算的形式在应用系统中起核心支撑作用1.它可以应用于各种类型的自然语言处理任务中,例 ...

  7. 匹配网络(Learning to Rank、单双塔模型)

    参考:Learning to Rank Learning to Rank: pointwise.pairwise.listwise LTR(Learning to rank)是一种监督学习(Super ...

  8. 【文本匹配】表示型模型

    基于表示的匹配模型的基本结构包括: (1)嵌入层,即文本细粒度的嵌入表示: (2)编码层,在嵌入表示的基础上进一步编码: (3)表示层:获取各文本的向量表征: (4)预测层:对文本pair的向量组进行 ...

  9. nmt模型源文本词项序列_「自然语言处理(NLP)」阿里团队--文本匹配模型(含源码)...

    来源:AINLPer微信公众号 编辑: ShuYini 校稿: ShuYini 时间: 2019-8-14 引言 两篇文章与大家分享,第一篇作者对通用文本匹配模型进行探索,研究了构建一个快速优良的文本 ...

最新文章

  1. 基于多视图几何方式的三维重建
  2. 2018 Multi-University Training Contest 7
  3. 实际价格计算:确定方法
  4. golang 文件(文件打开,文件写入,文件读取,文件删除)的基本操作
  5. ajax接收到的数据是一个页面的代码的原因
  6. PE文件格式详解(二)
  7. 如何实现python连续输入
  8. protel 99se 负片打印
  9. java--实现j2cache二级缓存
  10. labuladong 公众号的使用方法
  11. Prometheus 监控详解
  12. 蝉知CMS7.0.1后台模板Getshell
  13. Dubbo源码解析-——服务导出
  14. eclipse离线安装PyDev
  15. NLP文本情感分析:测试集loss比训练集loss大很多,训练集效果好测试集效果差的原因
  16. Python有什么用?Python 的 10 个实际用途
  17. 现在的你对未来什么规划?
  18. 劳伦杰克逊写给姚明的情书:你的名字
  19. 鼠标突然无反应,鼠标灯亮,鼠标灯不亮
  20. H5U的一个比较完整的程序框架 伺服控制是ETHERCAT总线 气缸的控制宝库伸出、缩回、报警 轴的控制是分为通讯

热门文章

  1. 关系网络实战|设备关联信息定位团伙欺诈
  2. prd移动端通用产品需求文档+Axure高保真app社交订餐通用prd文档+产品业务说明+PRD功能性需求+移动端公工通用模板说明+需求分析+竞品分析+产品结构图+产品业务流程图+产品信息图+餐饮系统
  3. JavaScript 工作必知(九)function 说起 闭包问题
  4. 转载python2进制打包相关
  5. hdu 4006 The kth great number (优先队列)
  6. (转)一步一步Asp.Net MVC系列_权限管理之权限控制
  7. JZOJ 3455. 【NOIP2013模拟联考3】库特的向量(code)
  8. 【Android】Binder机制
  9. Java面向对象编程 第一章 面向对象开发方法概述
  10. 关于AE大数据点文件读取生成SHP文件时使用IFeatureBuffer快速提高读取效率