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是最好的。
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值