1. 基本原理
与推荐系统不同的是,搜索系统比较重要的用户特征是query,信息检索的过程则是根据query,给用户返回doc集合。
传统的检索系统,对文本进行切词, 然后每个词下面会生成一个倒排索引。 query查询时,则是对query进行分词,然后到对应的词进行直接召回即可实现,数据集合的返回。
有了数据集之后,如何给doc排序是一个比较关键的问题,毕竟人的精力是比较有限的,在浩渺的知识大海里面,捞出一条你最想要的doc,并将之排在最前面,需要涉及到算法的设计。而且每个人的需求都是千奇百怪的, 需要呈现出来的排序应当也是不一样的。这一点跟推荐系统很像。
在传统的信息检索系统里面,一般采用TF-IDF和BM25等文本相似度算法来实现和衡量一个doc和query的相似程度。这样的实现方案,纯粹,实在,只需要doc语料即可完成数据集的生成。目前还有大量的检索系统在使用。例如开源的ES的默认标配就是这两种算法。
这种传统信息检索方案比较实在,当遇到大数据的问题,可以采用分布式的方案来实现,特别是一些常用词,可能召出来成千上万的doc,可以通过分布式提高效率, doc分布到多个倒排索引当中,先进行初筛,然后在merge合并。
与传统的信息检索不同的是,目前越来越多的搜索引擎通过向量检索的方式,实现搜索。确实有些问题,传统的信息检索方案是实现不了的,例如以图搜图。因为图是一个数字向量,而不是以文字的形式存在,上述方案很难套用。
不过话说回来,向量检索的结果有时候很难解释,而且向量化的模型比较关键,如果训练的向量出问题了,那召回的结果自然而然就错了。不过目前已经有大量的推荐系统和搜索系统进行使用了,也是经过考验的,核心是要处理好数据和特征,不然真的是garbage in, garbage out 了。
目前比较火的词向量化的常用模型包括:word2vec/fasttext/doc2vec/dnn等等。
说白了,我们的目标是:
- 将query转化成vector
- 将doc转化成vector
2. example
这里举一个简单的例子, 训练物料使用经典的电影语料库。
https://grouplens.org/datasets/movielens/
这里我们直接使用gensim的doc2vec对电影的数据进行训练。电影的数据维度包括如下:
- 电影id
- 电影标题
- 电影类型
- 电影标签
doc2vec的输入时,[words, labels]。words是电影标题、电影类型、电影标签的分词结果。而labels是电影id。每个电影id生成200维的数据。具体实现如下:
#!/usr/bin/python2.7
#-- coding: utf-8 --
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
import jieba
import json
from collections import OrderedDict
import csv
# 首先对数据进行预处理
# id, title, genres, tag
def get_data():
tag_dict = {}
tags = csv.reader(open("./movies/tags.csv"))
for row in tags:
m_id = row[1]
tag = row[2]
if tag_dict.get(m_id, None) == None:
tags = ""
tags += tag
tag_dict[m_id] = tags
else:
tags = tag_dict[m_id]
tags += ","
tags += tag
tag_dict[m_id] = tags
movies = csv.reader(open('./movies/movies.csv'))
tagged_data = []
# 将数据导入到tagged_document当中
for row in movies:
m_id = row[0]
title = row[1]
genres = row[2]
desc = title + "|" + genres
if tag_dict.get(m_id, None) != None:
tags = tag_dict[m_id]
desc += "|"
desc += tags
tagged_data.append(TaggedDocument(words=jieba.lcut(desc.lower()), tags=[m_id]))
return tagged_data
def tarin_data(tagged_data):
max_epochs = 100
vector_size = 200
alpha = 0.025
model = Doc2Vec(vector_size=vector_size,
alpha=alpha,
min_alpha=0.00025,
min_count=1,
dm=1)
model.build_vocab(tagged_data)
for epoch in range(max_epochs):
model.train(tagged_data,
total_examples=model.corpus_count,
epochs=model.iter)
# decrease the learning rate
model.alpha -= 0.0002
# fix the learning rate, no decay
model.min_alpha = model.alpha
model.save("d2v.model")
print("model saved")
def predict_data():
model = Doc2Vec.load("d2v.model")
# to find the vector of a document which is not in training data
test_data = jieba.lcut("funny".lower())
v1 = model.infer_vector(test_data)
print("v1_infer", v1)
# to find most similar doc using tags
similar_doc = model.docvecs.most_similar('60756')
print(similar_doc)
# to find most similar doc using vector
similar_doc = model.docvecs.most_similar([v1], topn=10)
print(similar_doc)
# to find vector of doc in trainning data using tags or in other words
print(model.docvecs["60756"])
if __name__ == '__main__':
#tagged_data = get_data()
#tarin_data(tagged_data)
predict_data()
在预测数据的时候,可以给词生成vector, 可以直接用labels找到相近的物品,也可以使用vector找到相近的物品, 同时也可以生成物品的vector。例如上面给出一个funny这词,搜索相关电影,结果如下:
[('2796', 0.6416218280792236), ('3446', 0.6061424016952515), ('126548', 0.5998696088790894), ('171495', 0.5651712417602539), ('901', 0.5470060706138611), ('66509', 0.5319580435752869), ('26726', 0.5319346785545349), ('1184', 0.5318185091018677), ('3395', 0.5307272672653198), ('6860', 0.5305958986282349)]
搜索出来的基本上都是喜剧,直接人工来看,结果还算合格。
我们可以通过TensorBoard将结果展示出来, 具体参考:
#https://github.com/ArdalanM/gensim2tensorboard
import numpy as np
import tensorflow as tf
import os
from gensim.models.word2vec import Word2Vec
from tensorflow.contrib.tensorboard.plugins import projector
log_dir = '/tmp/embedding_log'
if not os.path.exists(log_dir):
os.mkdir(log_dir)
# load model
model_file = '/tmp/emb.bin'
word2vec = Word2Vec.load(model_file)
# create a list of vectors
embedding = np.empty((len(word2vec.vocab.keys()), word2vec.vector_size), dtype=np.float32)
for i, word in enumerate(word2vec.vocab.keys()):
embedding[i] = word2vec[word]
# setup a TensorFlow session
tf.reset_default_graph()
sess = tf.InteractiveSession()
X = tf.Variable([0.0], name='embedding')
place = tf.placeholder(tf.float32, shape=embedding.shape)
set_x = tf.assign(X, place, validate_shape=False)
sess.run(tf.global_variables_initializer())
sess.run(set_x, feed_dict={place: embedding})
# write labels
with open(os.path.join(log_dir, 'metadata.tsv'), 'w') as f:
for word in word2vec.vocab.keys():
f.write(word + '\n')
# create a TensorFlow summary writer
summary_writer = tf.summary.FileWriter(log_dir, sess.graph)
config = projector.ProjectorConfig()
embedding_conf = config.embeddings.add()
embedding_conf.tensor_name = 'embedding:0'
embedding_conf.metadata_path = os.path.join(log_dir, 'metadata.tsv')
projector.visualize_embeddings(summary_writer, config)
# save the model
saver = tf.train.Saver()
saver.save(sess, os.path.join(log_dir, "model.ckpt"))
print("完成!")
实际上, 在工业级别的应用的流程没有那么简单, 需要对物料进行多次加工,并加入用户行为数据,效果调优, 单纯靠文本的话,很难解决一些蹭热度的文本语义。
第一步,训练word2vec模型,得到词向量和实体短语向量;
第二步,学习商品(doc)的语义向量和商品所属类目(cat)语义向量;
第三步,利用用户行为数据优化商品语义向量。
训练是一个串行的流程,前一步学习到的模型参数用来初始化后一步的模型参数,相当于前一步是为后一步做预训练(pre-training)。
3. 效果评估
线下评估方法首先人工标注一批query和doc的相关性档位作为评估数据集。每个query和doc,计算两种的语义向量的余弦相似度。同一query的所有doc根据余弦相似度分数降序排序,根据标准的相关性档位计算排序的NDCG。然后求评估数据集上的整体NDCG相对于baseline的提升幅度来评估模型的好坏。
根据模型的学习结果,用词向量求和的方法生成query的语义向量。对于doc向量的生成,有好几种方法,我们分别对比了各个方法的效果。定义向量doc1为训练过程的第二步得到的doc向量;doc2为训练过程第二步得到的doc向量、cat向量与第一步得到的词向量加权融合。
首先,我们用和生成query的语义向量同样的方法,把doc中的所有词向量相加的方法,得到doc向量,作为baseline。
我们发现Paragraph Vector方法得到的doc向量的效果并不如直接对词向量求和的方法得到的doc向量;但是当它和词向量、类目向量融合之后,效果大大提升。
对于训练过程第三步得到的doc向量,即利用用户行为数据优化得到的doc向量,我们做了单独的效果对比,分为单独对比和融合对比两个版本。单独对比是指第三步得到的doc向量(定义为doc3)相对于第二步得到的doc向量(doc1)的比较;融合对比是指用第一步得到的词向量、第二步得到的类目向量、第三步得到的doc向量加权求和得到的向量,定义为doc4,与上面定义的doc2,即第二步融合之后的向量的对比。