如何将GPT-4o与RAG结合起来 - 创建一个服装搭配App
欢迎来到服装搭配App的Jupyter Notebook!该项目展示了GPT-4o模型在分析服装物品图像并提取关键特征(如颜色、风格和类型)方面的强大能力。我们的应用程序的核心依赖于由OpenAI开发的这一先进图像分析模型,这使我们能够准确识别输入服装物品的特征。
GPT-4o是一个结合了自然语言处理和图像识别的模型,使其能够理解并根据文本和视觉输入生成响应。
借助GPT-4o模型的能力,我们采用自定义匹配算法和RAG技术来搜索我们的知识库,以寻找与识别特征相辅相成的物品。该算法考虑了颜色兼容性和风格连贯性等因素,为用户提供合适的推荐。通过这个笔记本,我们旨在展示这些技术在创建服装推荐系统方面的实际应用。
使用GPT-4o + RAG(检索增强生成)的组合提供了几个优势:
- 情境理解:GPT-4o可以分析输入图像并理解上下文,如所描绘的对象、场景和活动。这使得在各个领域(无论是室内设计、烹饪还是教育)中提供更准确和相关的建议或信息成为可能。
- 丰富的知识库:RAG将GPT-4的生成能力与检索组件相结合,访问跨不同领域的大量信息语料库。这意味着系统可以基于广泛的知识范围(从历史事实到科学概念)提供建议或见解。
- 定制化:这种方法允许轻松定制以满足各种应用中特定用户的需求或偏好。无论是根据用户对艺术的品味量身定制建议,还是根据学生的学习水平提供教育内容,系统都可以被调整以提供个性化体验。
总的来说,GPT-4o + RAG方法为各种与时尚相关的应用提供了强大而灵活的解决方案,利用了生成和基于检索的人工智能技术的优势。
环境设置
首先我们将安装必要的依赖项,然后导入库并编写一些我们稍后将使用的实用函数。
%pip install openai --quiet
%pip install tenacity --quiet
%pip install tqdm --quiet
%pip install numpy --quiet
%pip install typing --quiet
%pip install tiktoken --quiet
%pip install concurrent --quiet
import pandas as pd
import numpy as np
import json
import ast
import tiktoken
import concurrent
from openai import OpenAI
from tqdm import tqdm
from tenacity import retry, wait_random_exponential, stop_after_attempt
from IPython.display import Image, display, HTML
from typing import List
client = OpenAI()
GPT_MODEL = "gpt-4o"
EMBEDDING_MODEL = "text-embedding-3-large"
EMBEDDING_COST_PER_1K_TOKENS = 0.00013
创建嵌入向量
现在,我们将通过选择一个数据库并为其生成嵌入向量来设置知识库。我将在data文件夹中使用sample_styles.csv
文件进行此操作。这是一个包含约44K
个项目的更大数据集的样本。这一步也可以通过使用现成的向量数据库来替代。例如,您可以按照这些示例之一来设置您的向量数据库。
styles_filepath = "data/sample_clothes/sample_styles.csv"
styles_df = pd.read_csv(styles_filepath, on_bad_lines='skip')
print(styles_df.head())
print("Opened dataset successfully. Dataset has {} items of clothing.".format(len(styles_df)))
id gender masterCategory subCategory articleType baseColour season \
0 27152 Men Apparel Topwear Shirts Blue Summer
1 10469 Men Apparel Topwear Tshirts Yellow Fall
2 17169 Men Apparel Topwear Shirts Maroon Fall
3 56702 Men Apparel Topwear Kurtas Blue Summer
4 47062 Women Apparel Bottomwear Patiala Multi Fall
year usage productDisplayName
0 2012.0 Formal Mark Taylor Men Striped Blue Shirt
1 2011.0 Casual Flying Machine Men Yellow Polo Tshirts
2 2011.0 Casual U.S. Polo Assn. Men Checks Maroon Shirt
3 2012.0 Ethnic Fabindia Men Blue Kurta
4 2012.0 Ethnic Shree Women Multi Colored Patiala
Opened dataset successfully. Dataset has 1000 items of clothing.
现在我们将为整个数据集生成嵌入。我们可以并行执行这些嵌入操作,以确保脚本能够适用于更大的数据集。根据这种逻辑,为完整的44K
条数据集创建嵌入的时间从大约4小时减少到大约2-3分钟。
# #批量嵌入逻辑
# 简单函数,接收一组文本对象,并返回其对应的嵌入列表。
@retry(wait=wait_random_exponential(min=1, max=40), stop=stop_after_attempt(10))
def get_embeddings(input: List):
response = client.embeddings.create(
input=input,
model=EMBEDDING_MODEL
).data
return [data.embedding for data in response]
# 将一个可迭代对象拆分为多个大小为 n 的批次。
def batchify(iterable, n=1):
l = len(iterable)
for ndx in range(0, l, n):
yield iterable[ndx : min(ndx + n, l)]
# 用于批处理和并行处理嵌入的函数
def embed_corpus(
corpus: List[str],
batch_size=64,
num_workers=8,
max_context_len=8191,
):
# 对语料库进行编码,截断至最大上下文长度
encoding = tiktoken.get_encoding("cl100k_base")
encoded_corpus = [
encoded_article[:max_context_len] for encoded_article in encoding.encode_batch(corpus)
]
# 计算语料库统计数据:输入数量、总词元数以及嵌入的预估成本
num_tokens = sum(len(article) for article in encoded_corpus)
cost_to_embed_tokens = num_tokens / 1000 * EMBEDDING_COST_PER_1K_TOKENS
print(
f"num_articles={len(encoded_corpus)}, num_tokens={num_tokens}, est_embedding_cost={cost_to_embed_tokens:.2f} USD"
)
# 嵌入语料库
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
futures = [
executor.submit(get_embeddings, text_batch)
for text_batch in batchify(encoded_corpus, batch_size)
]
with tqdm(total=len(encoded_corpus)) as pbar:
for _ in concurrent.futures.as_completed(futures):
pbar.update(batch_size)
embeddings = []
for future in futures:
data = future.result()
embeddings.extend(data)
return embeddings
# 用于生成DataFrame中指定列嵌入的函数
def generate_embeddings(df, column_name):
# 初始化一个空列表以存储嵌入
descriptions = df[column_name].astype(str).tolist()
embeddings = embed_corpus(descriptions)
# 将嵌入向量作为新列添加到 DataFrame 中
df['embeddings'] = embeddings
print("Embeddings created successfully.")
创建嵌入向量的两种选项:
下一行将为样本服装数据集创建嵌入向量。这个过程大约需要0.02秒来处理,另外约30秒来将结果写入本地的
.csv 文件。该过程使用我们的 text_embedding_3_large
模型,价格为
$0.00013/1K
个标记。考虑到数据集大约有 1K
条目,以下操作的成本大约为
$0.001
。如果您决定使用完整的 44K
条目数据集,这个操作将需要2-3分钟来处理,成本大约为 $0.07
。
如果您不想继续创建自己的嵌入向量,我们将使用预先计算的嵌入向量数据集。您可以跳过此单元格,并取消注释下一个单元格中的代码以继续加载预先计算的向量。这个操作需要大约1分钟来将所有数据加载到内存中。
generate_embeddings(styles_df, 'productDisplayName')
print("Writing embeddings to file ...")
styles_df.to_csv('data/sample_clothes/sample_styles_with_embeddings.csv', index=False)
print("Embeddings successfully stored in sample_styles_with_embeddings.csv")
num_articles=1000, num_tokens=8280, est_embedding_cost=0.00 USD
1024it [00:01, 724.12it/s]
Embeddings created successfully.
Writing embeddings to file ...
Embeddings successfully stored in sample_styles_with_embeddings.csv
# styles_df = pd.read_csv('data/sample_clothes/sample_styles_with_embeddings.csv', on_bad_lines='skip')
# # Convert the 'embeddings' column from string representations of lists to actual lists of floats
# styles_df['embeddings'] = styles_df['embeddings'].apply(lambda x: ast.literal_eval(x))
print(styles_df.head())
print("Opened dataset successfully. Dataset has {} items of clothing along with their embeddings.".format(len(styles_df)))
id gender masterCategory subCategory articleType baseColour season \
0 27152 Men Apparel Topwear Shirts Blue Summer
1 10469 Men Apparel Topwear Tshirts Yellow Fall
2 17169 Men Apparel Topwear Shirts Maroon Fall
3 56702 Men Apparel Topwear Kurtas Blue Summer
4 47062 Women Apparel Bottomwear Patiala Multi Fall
year usage productDisplayName \
0 2012.0 Formal Mark Taylor Men Striped Blue Shirt
1 2011.0 Casual Flying Machine Men Yellow Polo Tshirts
2 2011.0 Casual U.S. Polo Assn. Men Checks Maroon Shirt
3 2012.0 Ethnic Fabindia Men Blue Kurta
4 2012.0 Ethnic Shree Women Multi Colored Patiala
embeddings
0 [0.006903026718646288, 0.0004031236458104104, ...
1 [-0.04371623694896698, -0.008869604207575321, ...
2 [-0.027989011257886887, 0.05884069576859474, -...
3 [-0.004197604488581419, 0.029409468173980713, ...
4 [-0.05241541564464569, 0.015912825241684914, -...
Opened dataset successfully. Dataset has 1000 items of clothing along with their embeddings.
构建匹配算法
在本节中,我们将开发一个余弦相似度检索算法,以在我们的数据框中找到相似的项目。我们将利用我们自定义的余弦相似度函数来实现这一目的。虽然sklearn
库提供了一个内置的余弦相似度函数,但其SDK的最近更新导致了兼容性问题,促使我们实现我们自己的标准余弦相似度计算。
如果您已经设置了一个向量数据库,您可以跳过这一步。大多数标准数据库都配备了自己的搜索功能,这简化了本指南中概述的后续步骤。然而,我们的目标是演示匹配算法可以根据特定要求进行定制,比如特定阈值或指定返回的匹配数量。
find_similar_items
函数接受四个参数: -
embedding
:我们想要找到匹配项的嵌入。 -
embeddings
:要搜索以找到最佳匹配项的嵌入列表。 -
threshold
(可选):此参数指定要考虑为有效匹配的最小相似度分数。较高的阈值会产生更接近(更好)的匹配,而较低的阈值允许返回更多的项目,尽管它们可能与初始的embedding
不太匹配。 -
top_k
(可选):此参数确定要返回的超过给定阈值的项目数量。这些将是提供的embedding
的得分最高的匹配项。
def cosine_similarity_manual(vec1, vec2):
"""计算两个向量之间的余弦相似度。"""
vec1 = np.array(vec1, dtype=float)
vec2 = np.array(vec2, dtype=float)
dot_product = np.dot(vec1, vec2)
norm_vec1 = np.linalg.norm(vec1)
norm_vec2 = np.linalg.norm(vec2)
return dot_product / (norm_vec1 * norm_vec2)
def find_similar_items(input_embedding, embeddings, threshold=0.5, top_k=2):
"""根据余弦相似度,找出最相似的条目。"""
# 计算输入嵌入与所有其他嵌入之间的余弦相似度
similarities = [(index, cosine_similarity_manual(input_embedding, vec)) for index, vec in enumerate(embeddings)]
# 过滤掉低于阈值的相似之处
filtered_similarities = [(index, sim) for index, sim in similarities if sim >= threshold]
# 将筛选出的相似度按相似度分数进行排序
sorted_indices = sorted(filtered_similarities, key=lambda x: x[1], reverse=True)[:top_k]
# 返回最相似的前k个项目
return sorted_indices
def find_matching_items_with_rag(df_items, item_descs):
"""根据输入的物品描述,为每个描述找到基于余弦相似度的最相似物品。"""
# 从DataFrame中选择嵌入。
embeddings = df_items['embeddings'].tolist()
similar_items = []
for desc in item_descs:
# 生成输入项的嵌入表示
input_embedding = get_embeddings([desc])
# 根据余弦相似度寻找最相似的条目
similar_indices = find_similar_items(input_embedding, embeddings, threshold=0.6)
similar_items += [df_items.iloc[i] for i in similar_indices]
return similar_items
分析模块
在这个模块中,我们利用 gpt-4o
来分析输入的图像,并提取重要特征,如详细描述、风格和类型。分析是通过一个简单的
API 调用来执行的,我们提供图像的 URL 进行分析,并请求模型识别相关特征。
为了确保模型返回准确的结果,我们在提示中使用了特定的技术:
- 输出格式规范:我们指示模型返回一个具有预定义结构的 JSON
块,包括:
items
(str[]): 一个字符串列表,每个字符串代表服装项目的简洁标题,包括风格、颜色和性别。这些标题与我们原始数据库中的productDisplayName
属性非常相似。category
(str): 最能代表给定项目的类别。模型从原始样式数据框中存在的所有唯一articleTypes
列表中进行选择。gender
(str): 表示项目面向的性别的标签。模型从选项[Men, Women, Boys, Girls, Unisex]
中选择。
- 清晰简明的说明:
- 我们提供清晰的说明,说明项目标题应包含什么以及输出格式应该是什么样的。输出应该是
JSON 格式,但不包含模型响应通常包含的
json
标签。
- 我们提供清晰的说明,说明项目标题应包含什么以及输出格式应该是什么样的。输出应该是
JSON 格式,但不包含模型响应通常包含的
- 一次性示例:
- 为了进一步澄清预期的输出,我们向模型提供一个示例输入描述和相应的示例输出。虽然这可能会增加使用的标记数(从而增加调用的成本),但它有助于指导模型,从而获得更好的整体性能。
通过遵循这种结构化方法,我们的目标是从 gpt-4o
模型中获取精确和有用的信息,以便进一步分析并集成到我们的数据库中。
def analyze_image(image_base64, subcategories):
response = client.chat.completions.create(
model=GPT_MODEL,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": """Given an image of an item of clothing, analyze the item and generate a JSON output with the following fields: "items", "category", and "gender".
Use your understanding of fashion trends, styles, and gender preferences to provide accurate and relevant suggestions for how to complete the outfit.
The items field should be a list of items that would go well with the item in the picture. Each item should represent a title of an item of clothing that contains the style, color, and gender of the item.
The category needs to be chosen between the types in this list: {subcategories}.
You have to choose between the genders in this list: [Men, Women, Boys, Girls, Unisex]
Do not include the description of the item in the picture. Do not include the ```json ``` tag in the output.
Example Input: An image representing a black leather jacket.
Example Output: {"items": ["Fitted White Women's T-shirt", "White Canvas Sneakers", "Women's Black Skinny Jeans"], "category": "Jackets", "gender": "Women"}
""",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_base64}",
},
}
],
}
],
max_tokens=300,
)
# 从响应中提取相关特征
features = response.choices[0].message.content
return features
使用示例图片测试提示
为了评估我们的提示的有效性,让我们加载并使用数据集中的一组图片来测试它。我们将使用位于"data/sample_clothes/sample_images"
文件夹中的图片,确保涵盖各种风格、性别和类型。以下是所选样本:
2133.jpg
:男士衬衫7143.jpg
:女士衬衫4226.jpg
:休闲男士印花T恤
通过使用这些不同的图片来测试提示,我们可以评估其准确分析和提取不同类型服装和配饰的相关特征的能力。
我们需要一个实用函数来将.jpg图像编码为base64。
import base64
def encode_image_to_base64(image_path):
with open(image_path, 'rb') as image_file:
encoded_image = base64.b64encode(image_file.read())
return encoded_image.decode('utf-8')
# 设置图像路径并选择一张测试图像
image_path = "data/sample_clothes/sample_images/"
test_images = ["2133.jpg", "7143.jpg", "4226.jpg"]
# 将测试图像编码为Base64格式。
reference_image = image_path + test_images[0]
encoded_image = encode_image_to_base64(reference_image)
# 从DataFrame中选择唯一的子类别
unique_subcategories = styles_df['articleType'].unique()
# 分析图像并返回结果
analysis = analyze_image(encoded_image, unique_subcategories)
image_analysis = json.loads(analysis)
# 显示图像及分析结果
display(Image(filename=reference_image))
print(image_analysis)
{'items': ["Slim Fit Blue Men's Jeans", "White Men's Sneakers", "Men's Silver Watch"], 'category': 'Shirts', 'gender': 'Men'}
接下来,我们处理图像分析的输出,并将其用于过滤和显示与我们的数据集匹配的物品。以下是代码的详细说明:
-
提取图像分析结果:我们从
image_analysis
字典中提取物品描述、类别和性别。 -
过滤数据集:我们将
styles_df
DataFrame进行过滤,只包括与图像分析中的性别匹配(或是中性的)并排除与分析图像相同类别的物品。 -
查找匹配的物品:我们使用
find_matching_items_with_rag
函数在过滤后的数据集中查找与从分析图像中提取的描述相匹配的物品。 -