Python使用Faiss库实现ANN近邻搜索

Embedding的近邻搜索是当前图推荐系统非常重要的一种召回方式,通过item2vec、矩阵分解、双塔DNN等方式都能够产出训练好的user embedding、item embedding,对于embedding的使用非常的灵活:

  • 输入user embedding,近邻搜索item embedding,可以给user推荐感兴趣的items
  • 输入user embedding,近邻搜搜user embedding,可以给user推荐感兴趣的user
  • 输入item embedding,近邻搜索item embedding,可以给item推荐相关的items

然而有一个工程问题,一旦user embedding、item embedding数据量达到一定的程度,对他们的近邻搜索将会变得非常慢,如果离线阶段提前搜索好在高速缓存比如redis存储好结果当然没问题,但是这种方式很不实时,如果能在线阶段上线几十MS的搜索当然效果最好。

Faiss是Facebook AI团队开源的针对聚类和相似性搜索库,为稠密向量提供高效相似度搜索和聚类,支持十亿级别向量的搜索,是目前最为成熟的近似近邻搜索库。

接下来通过jupyter notebook的代码,给大家演示下使用faiss的简单流程,内容包括:

  • 读取训练好的Embedding数据
  • 构建faiss索引,将待搜索的Embedding添加进去
  • 取得目标Embedding,实现搜索得到ID列表
  • 根据ID获取电影标题,返回结果

对于已经训练好的Embedding怎样实现高速近邻搜索是一个工程问题,facebook的faiss库可以构建多种embedding索引实现目标embedding的高速近邻搜索,能够满足在线使用的需要

安装命令:

conda install -c pytorch faiss-cpu 

提前总结下faiss使用经验:
1. 为了支持自己的ID,可以用faiss.IndexIDMap包裹faiss.IndexFlatL2即可
2. embedding数据都需要转换成np.float32,包括索引中的embedding以及待搜索的embedding
3. ids需要转换成int64类型

1. 准备数据

import pandas as pd
import numpy as np
df = pd.read_csv("./datas/movielens_sparkals_item_embedding.csv")
df.head()
idfeatures
010[0.25866490602493286, 0.3560594320297241, 0.15…
120[0.12449632585048676, -0.29282501339912415, -0…
230[0.9557555317878723, 0.6764761805534363, 0.114…
340[0.3184879720211029, 0.6365472078323364, 0.596…
450[0.45523127913475037, 0.34402626752853394, -0….

构建ids

ids = df["id"].values.astype(np.int64)
type(ids), ids.shape
(numpy.ndarray, (3706,))
ids.dtype
dtype('int64')
ids_size = ids.shape[0]
ids_size
3706

构建datas

import json
import numpy as np
datas = []
for x in df["features"]:
    datas.append(json.loads(x))
datas = np.array(datas).astype(np.float32)
datas.dtype
dtype('float32')
datas.shape
(3706, 10)
datas[0]
array([ 0.2586649 ,  0.35605943,  0.15589039, -0.7067125 , -0.07414215,
       -0.62500805, -0.0573845 ,  0.4533663 ,  0.26074877, -0.60799956],
      dtype=float32)
# 维度
dimension = datas.shape[1]
dimension
10

2. 建立索引

import faiss
index = faiss.IndexFlatL2(dimension)
index2 = faiss.IndexIDMap(index)
ids.dtype
dtype('int64')
index2.add_with_ids(datas, ids)
index.ntotal
3706

4. 搜索近邻ID列表

df_user = pd.read_csv("./datas/movielens_sparkals_user_embedding.csv")
df_user.head()
idfeatures
010[0.5974288582801819, 0.17486965656280518, 0.04…
120[1.3099910020828247, 0.5037978291511536, 0.260…
230[-1.1886241436004639, -0.13511677086353302, 0….
340[1.0809299945831299, 1.0048035383224487, 0.986…
450[0.42388680577278137, 0.5294889807701111, -0.6…
user_embedding = np.array(json.loads(df_user[df_user["id"] == 10]["features"].iloc[0]))
user_embedding = np.expand_dims(user_embedding, axis=0).astype(np.float32)
user_embedding
array([[ 0.59742886,  0.17486966,  0.04345559, -1.3193961 ,  0.5313592 ,
        -0.6052168 , -0.19088413,  1.5307966 ,  0.09310367, -2.7573566 ]],
      dtype=float32)
user_embedding.shape
(1, 10)
user_embedding.dtype
dtype('float32')
topk = 30
D, I = index.search(user_embedding, topk)     # actual search
I.shape
(1, 30)
I
array([[3380, 2900, 1953,  121, 3285,  999,  617,  747, 2351,  601, 2347,
          42, 2383,  538, 1774,  980, 2165, 3049, 2664,  367, 3289, 2866,
        2452,  547, 1072, 2055, 3660, 3343, 3390, 3590]])

5. 根据电影ID取出电影信息

target_ids = pd.Series(I[0], name="MovieID")
target_ids.head()
0    3380
1    2900
2    1953
3     121
4    3285
Name: MovieID, dtype: int64
df_movie = pd.read_csv("./datas/ml-1m/movies.dat",
                     sep="::", header=None, engine="python",
                     names = "MovieID::Title::Genres".split("::"))
df_movie.head()
MovieIDTitleGenres
01Toy Story (1995)Animation|Children’s|Comedy
12Jumanji (1995)Adventure|Children’s|Fantasy
23Grumpier Old Men (1995)Comedy|Romance
34Waiting to Exhale (1995)Comedy|Drama
45Father of the Bride Part II (1995)Comedy
df_result = pd.merge(target_ids, df_movie)
df_result.head()
MovieIDTitleGenres
03380Railroaded! (1947)Film-Noir
12900Monkey Shines (1988)Horror|Sci-Fi
21953French Connection, The (1971)Action|Crime|Drama|Thriller
3121Boys of St. Vincent, The (1993)Drama
43285Beach, The (2000)Adventure|Drama

推荐系统:实现文章相似推荐的简单实例

看了一篇文章实现了文章的内容相似度计算实现相似推荐,算法比较简单,非常适合我这种初学入门的人。

来自一篇英文文章:地址

文章标题为:How to build a content-based movie recommender system with Natural Language Processing

文章的代码在:地址

该文章实现相似推荐的步骤:

1、将CSV加载到pandas.pd

2、提取其中的标题、题材分类、导演、演员、情节描述4个字段;

3、将单词都变小写,人名中的空格合并(英文才需要这样);

4、题材分类、导演、演员这几个特征都是结构化的不需要处理;而标题、情节描述这类字段是长段文本,使用nltk库做关键词提取(如果是中文可以用jieba分词库也有关键词提取功能)

5、将第四步骤的分类、导演、演员、关键词列表,合并到一个词列表(这一处理其实暗含了分类、导演、演员三个特征和关键词一样重要,没有做加权处理)

6、使用CountVectorizer做每个词语的计数,得到了每个文章的向量;

7、使用sklearn的cosin做笛卡尔积的相似度计算;

8、计算结果是一个二维矩阵,按行查询某一个文章的推荐结果,按相似度值排序得到最相似的文章

从里面能学到不少知识的运用:

1、全流程用pandas运行,尤其是for each row,做单个列的各种map计算;

2、计算相似度时使用了多个特征,包括Title,Genre,Director,Actors,Plot,统一成一个bag of words参与计算

3、使用from sklearn.metrics.pairwise import cosine_similarity用于相似度计算;

4、使用from sklearn.feature_extraction.text import CountVectorizer用于单词计数;

5、使用from rake_nltk import Rake用于关键词提取;

代码实现关键部分:

作者用到的一些库:

import pandas as pd
from rake_nltk import Rake
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer

pandas的dataframe中,直接替换一列的语法:

df['Director'] = df['Director'].map(lambda x: x.split(' '))

pd按行以此处理某个列的方法:

for index, row in df.iterrows():
    row['Actors'] = [x.lower().replace(' ','') for x in row['Actors']]
    row['Director'] = ''.join(row['Director']).lower()

pd.df删除某一列的方法:

df.drop(columns = ['Plot'], inplace = True)

pd.df从columns中提取一列作为index的方法:

df.set_index('Title', inplace = True)

作者将能使用的所有列,都放在了一个词包中用于相似度计算,按我的想法,这些特征列其实应该有不同的权重?

df['bag_of_words'] = ''
columns = df.columns
for index, row in df.iterrows():
    words = ''
    for col in columns:
        if col != 'Director':
            words = words + ' '.join(row[col])+ ' '
        else:
            words = words + row[col]+ ' '
    row['bag_of_words'] = words
    
df.drop(columns = [col for col in df.columns if col!= 'bag_of_words'], inplace = True)

sklearn使用词计数的调用:

count = CountVectorizer()
count_matrix = count.fit_transform(df['bag_of_words'])

sklearn实现矩阵相似度计算的方法:

# generating the cosine similarity matrix
cosine_sim = cosine_similarity(count_matrix, count_matrix)

怎样实现不同特征列的融合相似度计算?

这个问题纠结我很久,查询了一些文章,大都是人工指定加权权重,或者使用模型拟合权重值,没有多么简单的方法,而作者使用的其实是直接把分类、演员等字段,和关键词直接融合的方法

作者在文章中提到一句话:

I decided to use CountVectorizer rather than TfIdfVectorizer for one simple reason: I need a simple frequency counter for each word in my bag_of_words column. Tf-Idf tends to give less importance to the words that are more present in the entire corpus (our whole column, in this case) which is not what we want for this application, because every word is important to detect similarity!

对于标题、介绍这种纯文本内容,我们可以用TF/IDF提取关键词,物理含义就是降低全局出现的词频很多的词语;但是其实对于作者、演员、题材这类特征列,他们并不需要降低全局词频,使用词频计数即可。

有哪些可以提升的地方

作者的方法确实可以实现相似推荐,不过我感觉有一些可以提升的地方:

1、标题、简介,提取关键词后,可以查询业界的word2vec做向量扩展,这样能实现恰恰和伦巴舞这类词语的相似度度量,直接的关键词查询是得不到这样的信息;

2、分类、导演、演员这三个特征,需要和描述得到的关键词区分开,可以用加权的方法进行,按照产品的需求,加重分类、导演的相似度权重,降低演员、关键词的权重等,如果需要可以从点击率等出发,用模型计算这些权重;

本文地址:http://www.crazyant.net/2454.html,转载请注明来源

 

Pandas中对轴axis=0和axis=1的理解

刚学习numpy和Pandas,被axis、axis=0或者axis=’index’,axis=1或者axis=’columns’给搞蒙了,甚至经常觉得书是不是写错了,有点反直觉。

来自简书的一篇文章地址有张图解释的挺好的,见文章底部

 

引用一下这篇文章的话,理解的很好:

实际上axis = 1,指的是沿着行求所有列的平均值,代表了横轴,那axis = 0,就是沿着列求所有行的平均值,代表了纵轴。

但理解起来还是很绕,按我个人的理解,如果对比excel和MySQL的数据表来理解就更容易:

我们正常在使用excel或者mysql的时候,默认都是一行代表一条数据,每个列是不同的信息字段,我们在读取表的时候,都是一行一行读取的,如果要算max、min、sum等函数,其实都是一行一行计算全局的min、max、sum,但是算出来其实是每个列的数据的min、max、sum,这其实就是axis=0和axis=’index’的意思:一行一行的计算,但是算出来其实是每列的结果;

相对应的,我们的excel、mysql,很少会实现跨列做计算,除非每列都是一样的数字信息,举个例子某一个数据表的每一行有一个主键是日期,每一列是对应每个页面的PV,那么我可以计算每个日期的PV总数、平均数,这时候就是跨列计算了,就是不常见的axis=1或者axis=’columns’,虽然是跨列计算,但是算出来的结果其实是行标签日期的数据结果。

axis=0,虽然是一行一行计算,其实算出来是每列的结果,换句话说,aixs=0是指计算的时候跨行、每行每行的算,那么算出来当然是每列的结果,就像一把梳子往下梳,得到的就是树条状的结果,每个竖条的标签当然就是columns的标签

axis=1,虽然是一列一列计算,其实算出来是每行的结果,换句话说,aixs=1是指计算的时候跨列、每列每列的算,那么算出来当然是每行的结果,就像一把梳子往右梳,得到的就是横条状的结果,每个横条的标签当然是index的标签

以上当然是自己的理解,具体还得自己琢磨才可以弄清楚。