2023年4月,OpenAI的创始成员之一,同时也是特斯拉前AI主管的Andrej Karpathy分享了他这个有趣的周末项目,一个电影搜索和推荐引擎。
用户界面很简单,有两个关键功能。首先,你有一个搜索栏,你可以通过电影的标题来搜索电影。当你点击任何一部电影时,你将获得一个推荐给你的40部最相似电影的列表。
尽管它很流行,可惜的是Karpathy并没有公开分享项目的源代码。
所以,我们自己重新制作吧!
准备工作:电影数据集
Karpathy的项目索引了自1970年以来的所有11,762部电影,包括来自维基百科的剧情和概要。
为了实现类似的成果而不需要手工抓取维基百科的数据,你可以使用来自Kaggle的以下两个数据集:
这两个数据集根据电影标题和发行年份合并,并筛选出1970年后发行的电影。你可以在add_data.py文件中找到详细的预处理步骤。结果产生的DataFrame大约包含35,000部电影,其中大约8,500部电影除了描述之外还有剧情内容,如下所示:
步骤一:生成并储存嵌入向量
本演示项目的核心是电影数据对象的嵌入向量,主要用于通过剧情相似度推荐电影。在Karpathy的项目中,为电影梗概和剧情生成了向量嵌入。生成向量嵌入的有两种选项:
此外,根据每部电影的维基百科概要和剧情计算相似性,有两种相似性排序器的选择:
Karpathy建议使用text-embedding-ada-002与kNN的组合作为一个好的、快速的默认设置。
最后但同样重要的,正如在这个回应中所述,向量嵌入被储存在np.array中:
在这个项目中,我们还将使用来自OpenAI的text-embedding-ada-002嵌入模型,但是将向量嵌入存储在一个向量数据库中。
具体来说,我们将使用Weaviate*,这是一个开源的向量数据库。虽然我可以说使用向量索引的向量数据库比将你的嵌入存储在np.array中要快得多,但说实话:在这个规模(数千个)上,你不会注意到任何速度上的差异。我使用向量数据库的主要原因在于Weaviate内置了许多方便的功能,你可以立即使用,例如使用嵌入模型进行自动向量化。
首先,如add_data.py文件中所示,你需要设置你的Weaviate客户端,它连接到一个本地Weaviate数据库实例,如下所述。此外,你还将在这里定义你的OpenAI API密钥,以启用集成的OpenAI模块的使用。
# pip weaviate-client
import weaviate
import os
openai_key = os.environ.get("OPENAI_API_KEY", "")
# Setting up client
client = weaviate.Client(
url = "http://localhost:8080",
additional_headers={
"X-OpenAI-Api-Key": openai_key,
})
接下来,你将定义一个名为“Movies”的数据集合,用以存储电影数据对象,这类似于在关系型数据库中创建一个表。在此步骤中,你将定义 text2vec-openai 模块作为一个向量化器,它使得在导入和查询时可以自动进行数据向量化,并且在模块设置中,你将定义使用 text-embedding-ada-002 嵌入模型。另外,你可以将余弦距离定义为相似性度量。
movie_class_schema = {
"class": "Movies",
"description": "A collection of movies since 1970.",
"vectorizer": "text2vec-openai",
"moduleConfig": {
"text2vec-openai": {
"vectorizeClassName": False,
"model": "ada",
"modelVersion": "002",
"type": "text"
},
},
"vectorIndexConfig": {"distance" : "cosine"},
}
接下来,你需要定义电影数据对象的属性,并确定要为哪些属性生成向量嵌入。在下面这个简短的代码片段中,你可以看到,对于属性movie_id和title不会生成向量嵌入,因为向量化模块设置了"skip" : True。这是因为我们只想为descriptin和plot生成向量嵌入。
movie_class_schema["properties"] = [
{
"name": "movie_id",
"dataType": ["number"],
"description": "The id of the movie",
"moduleConfig": {
"text2vec-openai": {
"skip" : True,
"vectorizePropertyName" : False
}
}
},
{
"name": "title",
"dataType": ["text"],
"description": "The name of the movie",
"moduleConfig": {
"text2vec-openai": {
"skip" : True,
"vectorizePropertyName" : False
}
}
},
# shortened for brevity ...
{
"name": "description",
"dataType": ["text"],
"description": "overview of the movie",
},
{
"name": "Plot",
"dataType": ["text"],
"description": "Plot of the movie from Wikipedia",
},
]
# Create class
client.schema.create_class(movie_class_schema)
最后,你定义了一个批处理过程来填充向量数据库:
# Configure batch process - for faster imports
client.batch.configure(batch_size=10)
# Importing the data
for i in range(len(df)):
item = df.iloc[i]
movie_object = {
'movie_id':float(item['id']),
'title': str(item['Name']).lower(),
# shortened for brevity ...
'description':str(item['Description']),
'plot': str(item['Plot']),
}
client.batch.add_data_object(movie_object, "Movies")
步骤二:搜索电影
在Karpathy的项目中,搜索栏是一个基于关键词的简单搜索,它尝试将你的确切查询内容与电影标题进行逐字匹配。当一些人表示他们希望搜索能够进行电影的语义搜索时,Karpathy同意这可能是项目的一个很好的扩展。
在这个项目中,你将在 queries.js 文件中启用三种类型的搜索:
每个搜索都将返回num_movies = 20具有以下属性的电影['title', 'poster_link', 'genres', 'year', 'director', 'movie_id']。
要启用基于关键字的搜索,.withBm25()你可以在属性中使用搜索查询['title', 'director', 'genres', 'actors', 'keywords', 'description', 'plot']。你可以通过指定 为该属性赋予'title'更大的权重'title^3'。
async function get_keyword_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withBm25({query: text,
properties: ['title^3', 'director', 'genres', 'actors', 'keywords', 'description', 'plot'],
})
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
})
return data;
}
为了启用语义搜索,你可以使用带有.withNearText()的搜索查询。这将自动向量化搜索查询,并在向量空间中检索最接近的电影。
async function get_semantic_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withNearText({concepts: [text]})
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
});
return data;
}
要启用混合搜索,你可以使用.withHybrid() 搜索查询。 alpha : 0.5 表示关键字搜索和语义搜索的权重是相等的。
async function get_hybrid_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withHybrid({query: text, alpha: 0.5})
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
});
return data;
}
步骤三:获取相似电影推荐
要获得相似电影推荐,你可以执行一个.withNearObject()搜索查询,如queries.js文件中所示。通过传递电影的id,该查询将返回向量空间中与给定电影最接近的num_movies = 20部电影。
async function get_recommended_movies(mov_id) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'genres', 'year', 'poster_link', 'movie_id'])
.withNearObject({id: mov_id})
.withLimit(20)
.do()
.then(info => {
return info;
})
.catch(err => {
console.error(err)
});
return data;
}
步骤四:运行演示
最后,将所有内容巧妙地包裹在一个拥有标志性2000年代GeoCities美学的网络应用程序中。
要在本地运行演示,请克隆GitHub 存储库。
git clone git@github.com:weaviate-tutorials/awesome-moviate.git
导航到演示的目录并设置一个虚拟环境。
python -m venv .venv
source .venv/bin/activate
确保在你的虚拟环境中为你的 $OPENAI_API_KEY 设置环境变量。此外,在目录中运行以下命令以在你的虚拟环境中安装所有所需的依赖项。
pip install -r requirements.txt
接下来,在docker-compose.yml文件中设置你的OPENAI_API_KEY,然后运行以下命令通过Docker在本地运行Weaviate。
docker compose up -d
Weaviate实例启动并运行,运行add_data.py文件来填充你的向量数据库。
python add_data.py
在运行应用程序之前,请安装所有所需的node模块。
npm install
最后,运行以下命令在本地启动你的电影搜索引擎应用程序。
npm run start
现在,导航到 http://localhost:3000/ 并开始尝试你的应用程序。
总结
本文重现了Andrej Karpathy的一个有趣的周末项目,即一个电影搜索引擎/推荐系统。