DSSM學習報告
從文本挖掘的知識圖譜開始
-
當對一個領域只瞭解某些部分的時候,從一個知識圖出發是最好的。
-
查概念可以查到,DSSM是通過搜索引擎裏 Query 和 Title 的海量的點擊曝光日誌,用 DNN 把 Query 和 Title 表達爲低緯語義向量,並通過 cosine 距離來計算兩個語義向量的距離,最終訓練出語義相似度模型。該模型既可以用來預測兩個句子的語義相似度,又可以獲得某句子的低緯語義向量表達。
-
主要解決的問題有:
- 解決了LSA、LDA、Autoencoder等方法存在的一個最大的問題:字典爆炸(導致計算複雜度非常高),因爲在英文單詞中,詞的數量可能是沒有限制的,但是字母 [公式] -gram的數量通常是有限的
- 基於詞的特徵表示比較難處理新詞,字母的 [公式] -gram可以有效表示,魯棒性較強
- 使用有監督方法,優化語義embedding的映射問題
- 省去了人工的特徵工程
-
看關鍵詞的話,感覺應該是在這個片區
-
表達爲低緯向量,走的時候word embedding區域的隱語義分析區域的那塊。
-
cosine距離計算兩個語義向量距離,則是document片區的string distance。
-
幾乎沒怎麼搜到涉及到text mining的document關鍵詞的內容,猜測是詞向量方向涉及到的專有名詞,描述詞向量相關屬性的內容。
代碼及相關研究
權威論文及相關博客
DSSM & Multi-view DSSM TensorFlow實現:
https://github.com/InsaneLife/dssm
https://blog.csdn.net/shine19930820/article/details/79042567
Learning Deep Structured Semantic Models for Web Search using Clickthrough Data ※
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/cikm2013_DSSM_fullversion.pdf
A Latent Semantic Model with Convolutional-Pooling (CNN-DSSM)
http://www.iro.umontreal.ca/~lisa/pointeurs/ir0895-he-2.pdf
SEMANTIC MODELLING WITH LONG-SHORT-TERM MEMORY FOR INFORMATION RETRIEVAL
https://arxiv.org/pdf/1412.6629.pdf
A Multi-View Deep Learning Approach for Cross Domain User Modeling in Recommendation Systems
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/frp1159-songA.pdf
微軟關於DSSM研究一個主頁
https://www.microsoft.com/en-us/research/project/dssm/#!publications
我們選用第二篇作爲我們的實驗研究的代碼
瞭解DSSM解決的背景
- 一般的搜索引擎往往都是搜索與query中詞彙重複的內容。
- 但是人們在搜索的時候,往往無法準確的描述自己想要的內容(或者存在同義異詞的情況)
- 這個時候搜索出的內容往往是不準確的。
- 此時就需要用潛在語義模型來匹配“query-需要的結果”。
- 然後因爲以前的主題模型基本都是非監督,依賴上下文,所以經常出現不準的情況。
- 現在我們【寫論文的人】拿到了clickthrough的數據,就可以做出一個query-document匹配的模型了。
- 【以上論文第一頁INTRODUCTION部分】
數據
-
由於英文的clickthrough數據論文方不提供,我從另外一份代碼中找了一份中文的數據。
-
數據來自天池大數據比賽,是OPPO手機搜索排序query-title語義匹配的問題。
-
數據的格式大概是這樣的
桂魚 | {“桂魚多少錢一斤”: “0.071”, “桂魚價格”: “0.013”, “桂魚圖片大全”: “0.012”, “桂魚湯”: “0.010”, “桂魚的營養價值與功效”: “0.011”, “桂魚圖片”: “0.178”, “桂魚怎麼釣”: “0.024”, “桂魚的做法”: “0.054”, “桂魚多少錢一斤2018”: “0.012”, “桂魚怎麼做好喫”: “0.116”} | 石桂魚 | 百科 | 0
- “|”爲分界。
- 第一個是prefix,代表用戶一開始輸入了什麼。
- 第二個是預測的結果,根據當前前綴,預測的用戶完整需求查詢詞,最多10條;預測的查詢詞可能是前綴本身,數字爲統計概率 。
- 第三個是文章標題(應該是指最後點進去的文章標題)。
- 第四個是文章內容標籤 。
- 第五個是是否點擊。
- 爲了應用來訓練DSSM demo,將prefix和title作爲正樣,prefix和query_prediction(除title以外)作爲負樣本。
- 英文版的clickthrough數據沒有公佈,但是可以預見形式應該差不多。
- 中英文的wordhash方法不一樣,英文的用word-ngram,但是中文以詞彙爲單位,向量空間太大了;所以中文以字爲單位,用的是charactergram.
代碼研究
- 研究的代碼來自這個倉庫: https://github.com/LiangHao151941/dssm
- 文件爲:dssm_v3.py
- 最核心的就是參考這張圖
- 最核心的內容還是結合這張圖來看。代碼裏無論是層級,還是權重輸入和輸出,圖片基本都是相對應的。
- 下面是代碼和詳細的註釋。
import pickle
import random
import time
import sys
import numpy as np
import tensorflow as tf
flags = tf.app.flags
FLAGS = flags.FLAGS
# f.app.flags.DEFINE_xxx()就是添加命令行的optional argument(可選參數),而tf.app.flags.FLAGS可以從對應的命令行參數取出參數。
# 可以認爲是一個訓練時,別人不懂文檔參數的時候,打印出的幫助界面。
# https://blog.csdn.net/m0_37041325/article/details/77448971
flags.DEFINE_string('summaries_dir', '/tmp/dssm-400-120-relu', 'Summaries directory')
flags.DEFINE_float('learning_rate', 0.1, 'Initial learning rate.')
flags.DEFINE_integer('max_steps', 900000, 'Number of steps to run trainer.')
flags.DEFINE_integer('epoch_steps', 18000, "Number of steps in one epoch.")
flags.DEFINE_integer('pack_size', 2000, "Number of batches in one pickle pack.")
flags.DEFINE_bool('gpu', 1, "Enable GPU or not")
# 獲得當前的時間戳
start = time.time()
doc_train_data = None
query_train_data = None
# load test data for now
# rb 以二進制格式打開一個文件用於只讀。文件指針將會放在文件的開頭。這是默認模式。
# pickle.load(file)
# file:對象保存到的類文件對象。file必須有write()接口, file可以是一個以’w’方式打開的文件或者一個StringIO對象或者其他任何實現write()接口的對象。
# tocsr() https://blog.csdn.net/gaoborl/article/details/82869858
# 仔細多讀幾遍會明白什麼意思的
# 好處:高效的CSR + CSR, CSR *CSR算術運算;高效的行切片操作;高效的矩陣內積內積操作。
query_test_data = pickle.load(open('../data/query.test.pickle', 'rb')).tocsr()
doc_test_data = pickle.load(open('../data/doc.test.pickle', 'rb')).tocsr()
doc_train_data = pickle.load(open('../data/doc.train.pickle', 'rb')).tocsr()
query_train_data = pickle.load(open('../data/query.train.pickle', 'rb')).tocsr()
end = time.time()
# 計算讀到硬盤的時間
print("Loading data from HDD to memory: %.2fs" % (end - start))
# Trigram_D數目
TRIGRAM_D = 49284
# NEG表示負樣本的個數
NEG = 50
# BS表示batch_size,計算batch平均損失
BS = 1024
# 論文裏面都是300,參考
L1_N = 400
L2_N = 120
# 創建BS*TRIGRAM_D大小的數組,shape更像是表示長度和寬度
query_in_shape = np.array([BS, TRIGRAM_D], np.int64)
doc_in_shape = np.array([BS, TRIGRAM_D], np.int64)
def variable_summaries(var, name):
"""Attach a lot of summaries to a Tensor."""
# TF有兩種作用域類型:命名域(name scope),通過tf.name_scope或tf.op_scope創建;
# 還有一種是變量域,通過tf.variable_scope或tf.variable_op_scope創建
# 主要用於管理一個圖裏面的各種操作,返回的是一個以scope_name命名的context+manager。
# 這裏可以認爲是定義數據總結的操作
with tf.name_scope('summaries'):
# 計算矩陣豎列的平均值
mean = tf.reduce_mean(var)
# 用來顯示標量信息,一般繪圖會用到
tf.scalar_summary('mean/' + name, mean)
# standard deviation 標準差(同樣繪圖用)
with tf.name_scope('stddev'):
stddev = tf.sqrt(tf.reduce_sum(tf.square(var - mean)))
tf.scalar_summary('sttdev/' + name, stddev)
tf.scalar_summary('max/' + name, tf.reduce_max(var))
tf.scalar_summary('min/' + name, tf.reduce_min(var))
# 畫出參數直方圖
tf.histogram_summary(name, var)
# 對應的是輸入層
with tf.name_scope('input'):
# Shape [BS, TRIGRAM_D].
# tf.sparse_placeholder 函數返回一個可以用作提供值的句柄的 SparseTensor,但不能直接計算.
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')
# word hashing
# trigram是一種詞彙的哈希方法
with tf.name_scope('L1'):
# TRIGRAM_D指的是TRIGRAM的dimension
# L1_N則是第一層的層數
# 這段公式來自論文第4頁3.3部分,作者沒有詳細解釋這條公式
# 說是引用了這本書,Neural Networks: Tricks of the Train
l1_par_range = np.sqrt(6.0 / (TRIGRAM_D + L1_N))
# SGD,隨機均勻初始化權值
weight1 = tf.Variable(tf.random_uniform([TRIGRAM_D, L1_N], -l1_par_range, l1_par_range))
bias1 = tf.Variable(tf.random_uniform([L1_N], -l1_par_range, l1_par_range))
variable_summaries(weight1, 'L1_weights')
variable_summaries(bias1, 'L1_biases')
# 基本就是通過第一層的query和doc的權值和偏置,給出第二層的權值和偏置
# query_l1 = tf.matmul(tf.to_float(query_batch),weight1)+bias1
query_l1 = tf.sparse_tensor_dense_matmul(query_batch, weight1) + bias1
# doc_l1 = tf.matmul(tf.to_float(doc_batch),weight1)+bias1
doc_l1 = tf.sparse_tensor_dense_matmul(doc_batch, weight1) + bias1
# 計算激活函數relu,將大於0的保持不變,小於0的置爲0
query_l1_out = tf.nn.relu(query_l1)
doc_l1_out = tf.nn.relu(doc_l1)
# 第二到第四層是典型的MLP網絡,最終得到128維的句子表示
with tf.name_scope('L2'):
l2_par_range = np.sqrt(6.0 / (L1_N + L2_N))
weight2 = tf.Variable(tf.random_uniform([L1_N, L2_N], -l2_par_range, l2_par_range))
bias2 = tf.Variable(tf.random_uniform([L2_N], -l2_par_range, l2_par_range))
variable_summaries(weight2, 'L2_weights')
variable_summaries(bias2, 'L2_biases')
query_l2 = tf.matmul(query_l1_out, weight2) + bias2
doc_l2 = tf.matmul(doc_l1_out, weight2) + bias2
query_y = tf.nn.relu(query_l2)
doc_y = tf.nn.relu(doc_l2)
# 看不懂,猜測是隨機選NEG個(按照論文裏應該是128個)輸出,組成semantic feature的y
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
# tensorflow中的tile()函數是用來對張量(Tensor)進行擴展的,其特點是對當前張量內的數據進行一定規則的複製。
# 就是對某一維張量進行拓展,稱爲原來的某倍
# reduce_sum就是通過相加的方式進行張量降維,例如有一個2*3*4的矩陣,通過把兩個3*4的矩陣相加
# 即可完成張量降維,但是如果給與True,則會保持原來的張量降維
# 這一步應該是在拓展query_norm使得query可以和doc相乘
# 論文3.1 公式5
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))
# product 應該是點積,在計算cos距離
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 = query * doc / (||query|| * ||doc||)
cos_sim_raw = tf.truediv(prod, norm_prod)
# gamma = 20
# tf.transpose(a, perm = None, name = 'transpose')
# 將a進行轉置,並且根據perm參數重新排列輸出維度。這是對數據的維度的進行操作的形式。
cos_sim = tf.transpose(tf.reshape(tf.transpose(cos_sim_raw), [NEG + 1, BS])) * 20
with tf.name_scope('Loss'):
# Train Loss
# 利用平滑後的softmax得到概率
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)
# tf.train.GradientDescentOptimizer(learning_rate, use_locking=False,name=’GradientDescent’)
# learning_rate: A Tensor or a floating point value. 要使用的學習率
# use_locking: 要是True的話,就對於更新操作(update operations.)使用鎖
# name: 名字,可選,默認是”GradientDescent”
with tf.name_scope('Training'):
# Optimizer
train_step = tf.train.GradientDescentOptimizer(FLAGS.learning_rate).minimize(loss)
# with tf.name_scope('Accuracy'):
# correct_prediction = tf.equal(tf.argmax(prob, 1), 0)
# accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# tf.scalar_summary('accuracy', accuracy)
merged = tf.merge_all_summaries()
with tf.name_scope('Test'):
average_loss = tf.placeholder(tf.float32)
loss_summary = tf.scalar_summary('average_loss', average_loss)
def pull_batch(query_data, doc_data, batch_idx):
# start = time.time()
query_in = query_data[batch_idx * BS:(batch_idx + 1) * BS, :]
doc_in = doc_data[batch_idx * BS:(batch_idx + 1) * BS, :]
if batch_idx == 0:
print(query_in.getrow(53))
# COO矩陣,在之前將CSR矩陣的那個文章裏有講
query_in = query_in.tocoo()
doc_in = doc_in.tocoo()
# 創建一個係數張量的實例
query_in = tf.SparseTensorValue(
np.transpose([np.array(query_in.row, dtype=np.int64), np.array(query_in.col, dtype=np.int64)]),
np.array(query_in.data, dtype=np.float),
np.array(query_in.shape, dtype=np.int64))
doc_in = tf.SparseTensorValue(
np.transpose([np.array(doc_in.row, dtype=np.int64), np.array(doc_in.col, dtype=np.int64)]),
np.array(doc_in.data, dtype=np.float),
np.array(doc_in.shape, dtype=np.int64))
# end = time.time()
# print("Pull_batch time: %f" % (end - start))
return query_in, doc_in
# 把數據丟到tensor的placeholders裏面
def feed_dict(Train, batch_idx):
"""Make a TensorFlow feed_dict: maps data onto Tensor placeholders."""
if Train:
query_in, doc_in = pull_batch(query_train_data, doc_train_data, batch_idx)
else:
query_in, doc_in = pull_batch(query_test_data, doc_test_data, batch_idx)
return {query_batch: query_in, doc_batch: doc_in}
config = tf.ConfigProto() # log_device_placement=True)
config.gpu_options.allow_growth = True
#if not FLAGS.gpu:
#config = tf.ConfigProto(device_count= {'GPU' : 0})
with tf.Session(config=config) as sess:
# 用 tf.global_variables_initializer() 替代 tf.initialize_all_variables()
sess.run(tf.initialize_all_variables())
# 總結性文件會放的位置
train_writer = tf.train.SummaryWriter(FLAGS.summaries_dir + '/train', sess.graph)
test_writer = tf.train.SummaryWriter(FLAGS.summaries_dir + '/test', sess.graph)
# Actual execution
start = time.time()
# fp_time = 0
# fbp_time = 0
# 跑batch和輸出一些東西的循環
for step in range(FLAGS.max_steps):
batch_idx = step % FLAGS.epoch_steps
# if batch_idx % FLAGS.pack_size == 0:
# load_train_data(batch_idx / FLAGS.pack_size + 1)
# # setup toolbar
# sys.stdout.write("[%s]" % (" " * toolbar_width))
# #sys.stdout.flush()
# sys.stdout.write("\b" * (toolbar_width + 1)) # return to start of line, after '['
if batch_idx == 0:
temp = sess.run(query_y, feed_dict=feed_dict(True, 0))
print(np.count_nonzero(temp))
sys.exit()
if batch_idx % (FLAGS.pack_size / 64) == 0:
progress = 100.0 * batch_idx / FLAGS.epoch_steps
sys.stdout.write("\r%.2f%% Epoch" % progress)
sys.stdout.flush()
# t1 = time.time()
# sess.run(loss, feed_dict = feed_dict(True, batch_idx))
# t2 = time.time()
# fp_time += t2 - t1
# #print(t2-t1)
# t1 = time.time()
sess.run(train_step, feed_dict=feed_dict(True, batch_idx % FLAGS.pack_size))
# t2 = time.time()
# fbp_time += t2 - t1
# #print(t2 - t1)
# if batch_idx % 2000 == 1999:
# print ("MiniBatch: Average FP Time %f, Average FP+BP Time %f" %
# (fp_time / step, fbp_time / step))
if batch_idx == FLAGS.epoch_steps - 1:
end = time.time()
epoch_loss = 0
for i in range(FLAGS.pack_size):
loss_v = sess.run(loss, feed_dict=feed_dict(True, i))
epoch_loss += loss_v
epoch_loss /= FLAGS.pack_size
train_loss = sess.run(loss_summary, feed_dict={average_loss: epoch_loss})
train_writer.add_summary(train_loss, step + 1)
# print ("MiniBatch: Average FP Time %f, Average FP+BP Time %f" %
# (fp_time / step, fbp_time / step))
#
print ("\nEpoch #%-5d | Train Loss: %-4.3f | PureTrainTime: %-3.3fs" %
(step / FLAGS.epoch_steps, epoch_loss, end - start))
epoch_loss = 0
for i in range(FLAGS.pack_size):
loss_v = sess.run(loss, feed_dict=feed_dict(False, i))
epoch_loss += loss_v
epoch_loss /= FLAGS.pack_size
test_loss = sess.run(loss_summary, feed_dict={average_loss: epoch_loss})
test_writer.add_summary(test_loss, step + 1)
start = time.time()
print ("Epoch #%-5d | Test Loss: %-4.3f | Calc_LossTime: %-3.3fs" %
(step / FLAGS.epoch_steps, epoch_loss, start - end))
關於預處理方式和結果的關係
- 它就說這個mean Normalized Discounted Cumulative Gain(NDCG)就是衡量這個模型的標準.
- 然後得到結論,顯然是Letter-WordHashing + DNN是最好的。