重建Andrej Karpathy——电影搜索引擎

2023年11月09日 由 alex 发表 474 0

2023年4月,OpenAI的创始成员之一,同时也是特斯拉前AI主管的Andrej Karpathy分享了他这个有趣的周末项目,一个电影搜索和推荐引擎。


用户界面很简单,有两个关键功能。首先,你有一个搜索栏,你可以通过电影的标题来搜索电影。当你点击任何一部电影时,你将获得一个推荐给你的40部最相似电影的列表。


屏幕截图2023-11-09135952


尽管它很流行,可惜的是Karpathy并没有公开分享项目的源代码。


所以,我们自己重新制作吧!


准备工作:电影数据集

Karpathy的项目索引了自1970年以来的所有11,762部电影,包括来自维基百科的剧情和概要。


为了实现类似的成果而不需要手工抓取维基百科的数据,你可以使用来自Kaggle的以下两个数据集:


  • 包含48,000多部电影的数据集(许可证:CC0:公共领域)用于'id'、'name'、'PosterLink'、'Genres'、'Actors'、'Director'、'Description'、'Keywords'和'DatePublished'。
  • 维基百科电影剧情(许可证:CC BY-SA 4.0),用于专栏'plot'。


这两个数据集根据电影标题和发行年份合并,并筛选出1970年后发行的电影。你可以在add_data.py文件中找到详细的预处理步骤。结果产生的DataFrame大约包含35,000部电影,其中大约8,500部电影除了描述之外还有剧情内容,如下所示:


b


步骤一:生成并储存嵌入向量

本演示项目的核心是电影数据对象的嵌入向量,主要用于通过剧情相似度推荐电影。在Karpathy的项目中,为电影梗概和剧情生成了向量嵌入。生成向量嵌入的有两种选项:


  • 词频-逆文档频率(TF-IDF),这是简单的双词组,应用于单独词汇的使用。
  • OpenAI的text-embedding-ada-002嵌入模型,应用于语义相似性。


此外,根据每部电影的维基百科概要和剧情计算相似性,有两种相似性排序器的选择:


  • 使用余弦相似度的k-最近邻(kNN)
  • 支持向量机


Karpathy建议使用text-embedding-ada-002与kNN的组合作为一个好的、快速的默认设置。


最后但同样重要的,正如在这个回应中所述,向量嵌入被储存在np.array中:


c


在这个项目中,我们还将使用来自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同意这可能是项目的一个很好的扩展。


d


在这个项目中,你将在 queries.js 文件中启用三种类型的搜索:


  • 基于关键词的搜索(BM25)、
  • 语义搜索,以及
  • 混合搜索,它是基于关键词搜索和语义搜索的结合。


每个搜索都将返回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的一个有趣的周末项目,即一个电影搜索引擎/推荐系统。




文章来源:https://towardsdatascience.com/recreating-andrej-karpathys-weekend-project-a-movie-search-engine-9b270d7a92e4
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消