DSSM學習——入門及實驗篇

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.

代碼研究

在這裏插入圖片描述

  • 最核心的內容還是結合這張圖來看。代碼裏無論是層級,還是權重輸入和輸出,圖片基本都是相對應的。
  • 下面是代碼和詳細的註釋。
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是最好的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章