NewBeeNLP 永遠(yuǎn)有料,永遠(yuǎn)有趣
199篇原創(chuàng)內(nèi)容
公眾號(hào)
在前面一篇文章中,,總結(jié)了Representation-Based文本匹配模型的改進(jìn)方法,,
其中在一篇論文中提到了使用Pre-train方式來(lái)提高效果,,論文連接如下:
- Pre-training Tasks for Embedding-based Large-scale Retrieval[1]
論文中提到的預(yù)訓(xùn)練數(shù)據(jù)均為,,relevant positive Query-Doc 對(duì):
訓(xùn)練的目標(biāo)為最大化當(dāng)前Postive Query-Doc的Softmax條件概率:
論文中提到,,softxmax分母中的 為所有可能的文檔集合,這樣的話(huà)候選文檔集合非常大,,所以論文中做了近似,,「訓(xùn)練時(shí)使用當(dāng)前batch中文檔這個(gè)子集來(lái)代替全集」 ,這種方法稱(chēng)為Sample Softmax
,。
TensorFlow中也有這個(gè)方法的API實(shí)現(xiàn),,但是我一直不是很能理解代碼中到底應(yīng)該怎么實(shí)現(xiàn),突然這幾天讀到了文本匹配的開(kāi)山之作 「DSSM」,,我發(fā)現(xiàn)「DSSM」的訓(xùn)練方法與上面那篇論文非常類(lèi)似,,于是研究了一下源碼,有一種豁然開(kāi)朗的感覺(jué),,所以想分享一下,,我對(duì)這種訓(xùn)練方式的理解。DSSM論鏈接如下:
- Learning deep structured semantic models for web search using clickthrough data.[2]
DSSM論文中的訓(xùn)練數(shù)據(jù)也是Query-Document對(duì),,訓(xùn)練目標(biāo)也為最大化給定Query下點(diǎn)擊Doc的條件概率,,公式如下,和上面說(shuō)的Pre-train任務(wù)基本一致:
極大似然估計(jì)的公式基本一樣,,訓(xùn)練都是Point-wise loss,,具體各個(gè)符號(hào)我在下面仔細(xì)介紹。
DSSM框架簡(jiǎn)要介紹
作為文本匹配方向的開(kāi)山之作,,已經(jīng)有非常多的博客介紹了這個(gè)模型,,這里我就簡(jiǎn)單介紹一下,,重點(diǎn)放在后面訓(xùn)練源碼的閱讀,。
模型結(jié)構(gòu)
DSSM也是Representation-Based模型,其中Query端 Encoder 和 Doc端 Encoder都是使用 MLP實(shí)現(xiàn),,最后Score計(jì)算使用的是cosine similarity,,后續(xù)模型的改進(jìn)很多都是使用更好的Encoder結(jié)構(gòu)。
輸入
DSSM中輸入并不是單純直接使用 bag-of-word,,從上面結(jié)構(gòu)圖可以看出,,輸入的時(shí)候做了Word Hashing,,在進(jìn)行bag-of-word映射,目的主要如下:
- 減少詞典的大小,,直接使用原始word詞典非常大(500K),,導(dǎo)致輸入向量的維數(shù)也非常高,使用Word Hashing做分解后,,可以減少詞典大小,,比如letter-trigram(30K)
Word Hashing的做法類(lèi)似于fast-text中的子詞分解,但是不同點(diǎn)在于
- fast-text中會(huì)取多個(gè)不同大小窗口對(duì)一個(gè)單詞進(jìn)行分解,,比如2,、3、4,、5,,詞表是這些所有的子詞構(gòu)成的集合
- Word Hashing只會(huì)取一個(gè)固定大小窗口對(duì)單詞進(jìn)行分解,詞表是這個(gè)固定大小窗口子詞的集合,,比如letter-bigram,,letter-trigram
比如輸入的詞為#good#
,我們選「tri-gram」,,則Word-hashing分解后,,#good#
的表示則為#go,goo,ood,od#
,然后就是輸入的每個(gè)詞都映射為tri-gram bag-of-words 向量,,出現(xiàn)了的位置為1,,否則為0。假設(shè)數(shù)據(jù)集進(jìn)行「tri-gram」分解后,,構(gòu)成的詞表大小為N,,那么Query輸入處理方式如下:
- 首先將每個(gè)詞進(jìn)行Word Hashing分解
- 獲得每個(gè)詞的表示,比如 [0,1,1,0,0,0...,0,1] ,維數(shù)為N,,其中在詞表中出現(xiàn)了的位置為1,,否則為0
- 將Query中所有的詞的表示向量相加可以得到一個(gè)N維向量,「其實(shí)就是bag-of-word表示」(只考慮有沒(méi)有出現(xiàn),,并不考慮出現(xiàn)順序位置)
Doc端輸入的處理也類(lèi)似于上面Query端的處理,,獲得Word-Hashing后的向量表示,作為整個(gè)模型的輸入,。
Encoder層
Query端和Doc端Encoder層處理很簡(jiǎn)單,,就是MLP,計(jì)算公式如下:
可以看出就是標(biāo)準(zhǔn)的全連接層運(yùn)算
相似度Score計(jì)算
DSSM中最后的相似度計(jì)算用的是 cosine similarity,,計(jì)算公式如下:
模型訓(xùn)練好之后,,給定一個(gè)Query我們就可以對(duì)其所有Doc按照這個(gè)計(jì)算出來(lái)的cosine similarity進(jìn)行排序。
訓(xùn)練方式
訓(xùn)練數(shù)據(jù)
DSSM的訓(xùn)練方式是做Point-wise訓(xùn)練,,論文中對(duì)于訓(xùn)練數(shù)據(jù)的描述如下:
The clickthrough logs consist of a list of queries and their clicked documents.
給定的是Query以及對(duì)應(yīng)的點(diǎn)擊Document,,我們需要進(jìn)行極大似然估計(jì),。
訓(xùn)練目標(biāo)
DSSM首先通過(guò)獲得的semantic relevance score
計(jì)算在給定Query下Doc的后驗(yàn)概率:
其中 為softmax函數(shù)的平滑因子, 表示所有的待排序的候選文檔集合,,可以看出這個(gè)目標(biāo)其實(shí)和我們一開(kāi)始提到的Pre-train那篇論文的目標(biāo)是一樣的,。我們的候選文檔大小可能會(huì)非常大,論文在實(shí)際訓(xùn)練中,,做法如下:
- 我們使用 來(lái)表示一個(gè)(Query,Doc)對(duì),,其中 表示這個(gè)Doc是被點(diǎn)擊過(guò)的
- 使用 和四個(gè)隨機(jī)選取沒(méi)有被點(diǎn)擊過(guò)的Doc來(lái)近似全部文檔集合 ,其中 表示負(fù)樣本
上面就是訓(xùn)練時(shí)候的實(shí)際做法,,對(duì)于每個(gè) ,,我們只需要采樣K個(gè)負(fù)樣本(K可以自己定), ,,這樣softxmax操作我們也只需要在 這個(gè)集合上計(jì)算即可,,論文中還提到,采樣負(fù)樣本方式對(duì)最終結(jié)果沒(méi)有太大影響
In our pilot study, we do not observe any significant difference when different sampling strategies were used to select the unclicked documents.
最后loss選用的就是交叉熵?fù)p失:
訓(xùn)練方式總結(jié)
通過(guò)上面的分析,,我的理解是DSSM和之前說(shuō)的Pre-trian那篇論文,,訓(xùn)練的時(shí)候只需要采樣負(fù)樣本即可,然后softmax操作只在 當(dāng)前正樣本 + 采樣的負(fù)樣本 集合上計(jì)算,,最后用交叉熵?fù)p失即可,。具體負(fù)樣本怎么采樣,我覺(jué)的有兩種方法:
- 輸入數(shù)據(jù)中就已經(jīng)采樣好負(fù)樣本,,輸入數(shù)據(jù)直接是正樣本 + 負(fù)樣本,,這樣運(yùn)算量會(huì)大些
- 輸入數(shù)據(jù)batch均為正樣本,負(fù)樣本通過(guò)batch中其他Doc構(gòu)造
DSSM源碼閱讀
我看的DSSM實(shí)現(xiàn)代碼是下面兩個(gè),,其中的不同點(diǎn)就在于上面說(shuō)的負(fù)樣本構(gòu)造不同
訓(xùn)練數(shù)據(jù)中輸入有負(fù)樣本:InsaneLife/dssm[3]
使用一個(gè)batch中其他Doc構(gòu)造負(fù)樣本:LiangHao151941/dssm[4]
訓(xùn)練數(shù)據(jù)中輸入有負(fù)樣本的情況
- 這部分代碼在
https://github.com/InsaneLife/dssm/blob/master/dssm_rnn.py
輸入數(shù)據(jù)
with tf.name_scope('input'):
# 預(yù)測(cè)時(shí)只用輸入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 , 即是論文中說(shuō)的 $D^+# ,,正樣本輸入
- doc_neg_batch,,即是論文匯總說(shuō)的 ,負(fù)樣本輸入集合
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
這是準(zhǔn)備每個(gè)batch數(shù)據(jù)的代碼,,其中query_BS
為batch_size,,NEG
為負(fù)樣本采樣個(gè)數(shù)。
合并正負(fù)樣本與計(jì)算余弦相似度
從論文中可以知道,,我們需要對(duì)「每個(gè)Query」選取 這個(gè)集合做softmax操作,,所以我們計(jì)算出每個(gè)Query正負(fù)樣本的Score之后,需要將同一個(gè)Query正負(fù)樣本其合并到一起,,Score即為softmax輸入的logits,。「由于輸入數(shù)據(jù)中直接有負(fù)樣本」,所以這里不需要我們構(gòu)造負(fù)樣本,,直接把負(fù)樣本輸出的Score concat即可,。下面代碼步驟如下:
- 先把同一個(gè)Query下pos_doc和neg_doc經(jīng)過(guò)Encoder之后的隱層表示concat到一起
- 計(jì)算每個(gè)Query與正負(fù)樣本的similarity
計(jì)算出來(lái)的cosine similarity Tensor如下,每一行是一個(gè)Query下正樣本和負(fù)樣本的sim,,這樣我們?cè)?code>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'):
# 合并負(fù)樣本,,tile可選擇是否擴(kuò)展負(fù)樣本。
# doc_y = tf.tile(doc_positive_y, [1, 1])
# 此時(shí)doc_y為單獨(dú)的pos_doc的hidden representation
doc_y = tf.tile(doc_pos_rnn_output, [1, 1])
#下面這段代碼就是把同一個(gè)Query下的neg_doc合并到pos_doc,后續(xù)才能計(jì)算score 和 softmax
for 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 = 20
cos_sim = tf.transpose(tf.reshape(tf.transpose(cos_sim_raw), [NEG + 1, query_BS])) * 20
# cos_sim 作為softmax logits輸入
softmax操作與計(jì)算交叉熵?fù)p失
上一步中已經(jīng)計(jì)算出各個(gè)Query對(duì)其正負(fù)樣本的cosine similarity,,這個(gè)將作為softmax輸入的logits,,然后計(jì)算交叉熵?fù)p失即可,「因?yàn)橹挥幸粋€(gè)正樣本,,而且其位置在第一個(gè)」,,所以我們的標(biāo)簽one-hot編碼為:
所以我們計(jì)算交叉熵?fù)p失的時(shí)候,「只需要取第一列的概率值即可」:
with tf.name_scope('Loss'):
# Train Loss
# 轉(zhuǎn)化為softmax概率矩陣,。
prob = tf.nn.softmax(cos_sim)
# 只取第一列,,即正樣本列概率。相當(dāng)于one-hot標(biāo)簽為[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)
使用一個(gè)batch中其他Doc構(gòu)造負(fù)樣本
上面的方法是在輸入數(shù)據(jù)中直接有負(fù)樣本,,這樣計(jì)算的時(shí)候需要多計(jì)算負(fù)樣本的representation,,在輸入數(shù)據(jù)batch中可以只包含正樣本,然后再選擇同一個(gè)batch中的其他Doc構(gòu)造負(fù)樣本,,這樣可以減少計(jì)算量
- 這部分代碼在
https://github.com/LiangHao151941/dssm/blob/master/single/dssm_v3.py
輸入數(shù)據(jù)
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')
可以看出這里的輸入數(shù)據(jù)只有 ,,并沒(méi)有負(fù)樣本
構(gòu)造負(fù)樣本并計(jì)算余弦相似度
由于輸入數(shù)據(jù)中沒(méi)有負(fù)樣本,所以使用同一個(gè)batch中的其他Doc做為負(fù)樣本,,由于所有輸入Doc representation在前面已經(jīng)計(jì)算出來(lái)了,,所以不需要額外再算一遍了,下面的代碼就是通過(guò)rotate 輸入 ,,來(lái)構(gòu)造負(fù)樣本,,比如:
- 那么對(duì)于 , 均為視為 ,,可以構(gòu)造負(fù)樣本為
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 similarity
query_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操作與計(jì)算交叉熵?fù)p失
這一步和前面說(shuō)的是一樣的
with tf.name_scope('Loss'):
# Train Loss
prob = tf.nn.softmax((cos_sim))
hit_prob = tf.slice(prob, [0, 0], [-1, 1])
loss = -tf.reduce_sum(tf.log(hit_prob)) / BS
tf.scalar_summary('loss', loss)
總結(jié)
之前一直對(duì)于sampled softmax不太理解,,不知道在實(shí)際訓(xùn)練中如何做,。但是看了DSSM論文和源碼之后,真的有一種撥開(kāi)云霧見(jiàn)月明的感覺(jué),,這種訓(xùn)練方式的核心就在于「構(gòu)造負(fù)樣本」,,這樣一說(shuō)感覺(jué)和Pairwise loss中構(gòu)造pair又有點(diǎn)類(lèi)似,不過(guò)這里構(gòu)造的不止一個(gè)負(fù)樣本,訓(xùn)練目標(biāo)也是pointwise,,這種方式應(yīng)該是不需要用到TensorFlow中的tf.nn.sampled_softmax_loss
這個(gè)函數(shù),。
當(dāng)然上面都是個(gè)人理解,最近越來(lái)越覺(jué)得真正要弄懂一個(gè)算法不單要理解數(shù)學(xué)原理,,而且需要去讀懂源碼,,很多在論文中理解不了的信息,在源碼中都會(huì)清晰的展現(xiàn)出來(lái),,這部分我也一直在探索中,,之后有什么心得再分享給大家啦~
一起交流