如何结合GPT-4o mini与RAG创建服装搭配应用
技术背景
传统服装推荐系统依赖人工标注或基于规则的匹配,难以理解图像中的视觉特征和风格语义。GPT-4o mini作为多模态模型,能同时处理文本和图像,提取颜色、风格等关键信息,但直接生成推荐可能缺乏领域知识。RAG技术通过检索外部知识库(如服装数据库)来增强生成结果的准确性和相关性。结合两者,既能利用视觉理解能力,又能基于真实数据提供可解释的推荐,适用于个性化购物助手、虚拟衣橱等场景。主要限制包括API调用成本、图像分辨率对识别精度的影响,以及依赖预定义类别和性别标签。
核心原理
系统核心流程为:1) 对服装数据库中的文本描述(如productDisplayName)使用text-embedding-3-large模型生成向量嵌入,并存储为CSV或向量数据库;2) 用户上传服装图像,通过GPT-4o mini的视觉能力分析图像,提取关键特征(如物品描述、类别、性别),输出结构化JSON;3) 将提取的描述文本转化为嵌入向量,与数据库中的嵌入进行余弦相似度计算,检索出最相似的互补单品;4) 引入护栏机制,将原始图像与推荐单品图像再次输入GPT-4o mini,进行二次匹配校验,过滤不合适的推荐。数据流为:图像→特征提取→文本→嵌入→相似度检索→候选集→二次校验→最终推荐。关键设计决策包括:使用批量并行嵌入加速处理,自定义余弦相似度函数以避免库兼容性问题,以及通过提示工程确保模型输出格式稳定。
架构分析
系统架构分为四层:数据层、嵌入层、分析层和推荐层。数据层使用CSV存储服装元数据(如id、productDisplayName、gender、articleType),并可选集成向量数据库(如Pinecone)以支持大规模检索。嵌入层通过OpenAI Embedding API将文本转换为向量,采用线程池并行处理,显著缩短处理时间。分析层调用GPT-4o mini的Chat Completions接口,输入图像和提示词,输出结构化JSON,包含items、category、gender字段。推荐层实现余弦相似度检索,支持阈值和top_k参数,并包含护栏模块进行二次校验。各组件通过API解耦,便于替换和扩展。设计思想强调模块化、可扩展性和成本控制,例如通过批量嵌入降低API调用次数,通过过滤减少检索范围。
完整实战教程
欢迎来到服装搭配应用Jupyter Notebook!本项目展示了GPT-4o mini模型在分析服装图像和提取颜色、风格和类型等关键特征方面的强大能力。我们应用的核心依赖于OpenAI开发的这一先进图像分析模型,它使我们能够准确识别输入服装的特征。
GPT-4o mini是一个小型模型,结合了自然语言处理和图像识别,使其能够基于文本和视觉输入进行理解并生成响应,且延迟低。
基于GPT-4o mini模型的能力,我们采用自定义匹配算法和RAG技术来搜索知识库,寻找与识别特征互补的服装。该算法考虑颜色兼容性和风格一致性等因素,为用户提供合适的推荐。通过本笔记本,我们旨在展示这些技术在创建服装推荐系统中的实际应用。
使用GPT-4o mini + RAG(检索增强生成)的组合具有以下几个优势:
- 上下文理解:GPT-4o mini可以分析输入图像并理解上下文,例如所描绘的对象、场景和活动。这使得在室内设计、烹饪或教育等各个领域都能提供更准确和相关的建议或信息。
- 丰富的知识库:RAG结合了GPT-4的生成能力与检索组件,该组件可访问跨不同领域的大量信息语料库。这意味着系统可以基于广泛的知识提供建议或见解,从历史事实到科学概念。
- 定制化:该方法允许轻松定制,以满足各种应用中特定用户的需求或偏好。无论是根据用户的艺术品味定制建议,还是根据学生的学习水平提供教育内容,系统都可以进行调整以提供个性化体验。
总的来说,GPT-4o mini + RAG方法为各种时尚相关应用提供了快速、强大且灵活的解决方案,充分利用了生成式和检索式AI技术的优势。
环境设置
首先,我们将安装必要的依赖,然后导入库并编写一些稍后会用到的工具函数。
%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-mini"
EMBEDDING_MODEL = "text-embedding-3-large"
EMBEDDING_COST_PER_1K_TOKENS = 0.00013
创建嵌入
我们现在将通过选择数据库并为其生成嵌入来设置知识库。我在数据文件夹中使用 sample_styles.csv 文件。这是一个更大数据集的样本,包含约 44K 个项目。此步骤也可以替换为使用开箱即用的向量数据库。例如,您可以按照这些 cookbook 之一来设置您的向量数据库。
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)))
现在我们将为整个数据集生成嵌入。我们可以并行化这些嵌入的执行,以确保脚本能够扩展到更大的数据集。通过这种逻辑,为完整的 44K 条目数据集创建嵌入的时间从约4小时减少到约2-3分钟。
## Batch Embedding Logic
# Simple function to take in a list of text objects and return them as a list of embeddings
@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]
# Splits an iterable into batches of size n.
def batchify(iterable, n=1):
l = len(iterable)
for ndx in range(0, l, n):
yield iterable[ndx : min(ndx + n, l)]
# Function for batching and parallel processing the embeddings
def embed_corpus(
corpus: List[str],
batch_size=64,
num_workers=8,
max_context_len=8191,
):
# Encode the corpus, truncating to max_context_len
encoding = tiktoken.get_encoding("cl100k_base")
encoded_corpus = [
encoded_article[:max_context_len] for encoded_article in encoding.encode_batch(corpus)
]
# Calculate corpus statistics: the number of inputs, the total number of tokens, and the estimated cost to embed
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"
)
# Embed the corpus
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
# Function to generate embeddings for a given column in a DataFrame
def generate_embeddings(df, column_name):
# Initialize an empty list to store embeddings
descriptions = df[column_name].astype(str).tolist()
embeddings = embed_corpus(descriptions)
# Add the embeddings as a new column to the DataFrame
df['embeddings'] = embeddings
print("Embeddings created successfully.")
创建嵌入向量的两个选项:
下一行代码将为示例服装数据集创建嵌入向量。处理大约需要0.02秒,另外需要约30秒将结果写入本地.csv文件。该过程使用我们的text_embedding_3_large模型,定价为$0.00013/1K个token。鉴于数据集大约有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")
# 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)))
构建匹配算法
在本节中,我们将开发一个余弦相似度检索算法,以在我们的数据框中查找相似项。为此,我们将使用自定义的余弦相似度函数。虽然 sklearn 库提供了内置的余弦相似度函数,但其 SDK 的最新更新导致了兼容性问题,促使我们实现自己的标准余弦相似度计算。
如果您已经设置了向量数据库,可以跳过此步骤。大多数标准数据库都自带搜索功能,这简化了本指南中后续步骤。然而,我们旨在证明匹配算法可以根据特定需求进行定制,例如特定的阈值或返回的匹配数量。
find_similar_items 函数接受四个参数:
embedding:我们要为其寻找匹配项的嵌入。embeddings:要搜索最佳匹配项的嵌入列表。threshold(可选):此参数指定匹配被视为有效的最低相似度分数。阈值越高,匹配越接近(越好),而阈值越低,返回的项目越多,尽管它们可能与初始embedding的匹配度不高。top_k(可选):此参数确定返回超过给定阈值的项目数量。这些将是所提供embedding的最高得分匹配项。
def cosine_similarity_manual(vec1, vec2):
"""Calculate the cosine similarity between two vectors."""
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):
"""Find the most similar items based on cosine similarity."""
# Calculate cosine similarity between the input embedding and all other embeddings
similarities = [(index, cosine_similarity_manual(input_embedding, vec)) for index, vec in enumerate(embeddings)]
# Filter out any similarities below the threshold
filtered_similarities = [(index, sim) for index, sim in similarities if sim >= threshold]
# Sort the filtered similarities by similarity score
sorted_indices = sorted(filtered_similarities, key=lambda x: x[1], reverse=True)[:top_k]
# Return the top-k most similar items
return sorted_indices
def find_matching_items_with_rag(df_items, item_descs):
"""Take the input item descriptions and find the most similar items based on cosine similarity for each description."""
# Select the embeddings from the DataFrame.
embeddings = df_items['embeddings'].tolist()
similar_items = []
for desc in item_descs:
# Generate the embedding for the input item
input_embedding = get_embeddings([desc])
# Find the most similar items based on cosine similarity
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-mini 分析输入图像并提取重要特征,如详细描述、风格和类型。分析通过直接的 API 调用完成,我们提供图像的 URL 进行分析,并请求模型识别相关特征。
为了确保模型返回准确的结果,我们在提示中使用了特定技术:
- 输出格式规范:我们指示模型返回一个具有预定义结构的 JSON 块,包括:
items(str[]):字符串列表,每个字符串代表一件服装的简洁标题,包括风格、颜色和性别。这些标题与原始数据库中的productDisplayName属性非常相似。category(str):最能代表给定物品的类别。模型从原始样式数据框中存在的所有唯一articleTypes列表中选择。gender(str):指示物品适用性别的标签。模型从选项[Men, Women, Boys, Girls, Unisex]中选择。
- 清晰简洁的指令:
- 我们提供关于物品标题应包含内容以及输出格式的明确指令。输出应为 JSON 格式,但不包含模型响应通常包含的
json标签。
- 一次性示例:
- 为了进一步澄清预期输出,我们向模型提供一个示例输入描述和相应的示例输出。虽然这可能会增加使用的令牌数量(从而增加调用成本),但有助于引导模型并获得更好的整体性能。
通过遵循这种结构化方法,我们旨在从 gpt-4o-mini 模型中获得精确且有用的信息,以便进一步分析并集成到我们的数据库中。
def analyze_image(image_base64, subcategories):
response = client.chat.completions.create(
model=GPT_MODEL,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"""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}",
},
}
],
}
]
)
# Extract relevant features from the response
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')
# Set the path to the images and select a test image
image_path = "data/sample_clothes/sample_images/"
test_images = ["2133.jpg", "7143.jpg", "4226.jpg"]
# Encode the test image to base64
reference_image = image_path + test_images[0]
encoded_image = encode_image_to_base64(reference_image)
# Select the unique subcategories from the DataFrame
unique_subcategories = styles_df['articleType'].unique()
# Analyze the image and return the results
analysis = analyze_image(encoded_image, unique_subcategories)
image_analysis = json.loads(analysis)
# Display the image and the analysis results
display(Image(filename=reference_image))
print(image_analysis)
接下来,我们处理图像分析的结果,并使用它来过滤和显示数据集中匹配的项目。以下是代码的详细说明:
- 提取图像分析结果:我们从
image_analysis字典中提取项目描述、类别和性别。
- 过滤数据集:我们过滤
styles_dfDataFrame,仅包含与图像分析中的性别匹配(或中性)的项目,并排除与所分析图像相同类别的项目。
- 查找匹配项目:我们使用
find_matching_items_with_rag函数在过滤后的数据集中查找与所分析图像提取的描述匹配的项目。
- 显示匹配项目:我们创建一个 HTML 字符串来显示匹配项目的图像。我们使用项目 ID 构建图像路径,并将每个图像追加到 HTML 字符串中。最后,我们使用
display(HTML(html))在笔记本中渲染图像。
这个单元格有效地演示了如何使用图像分析的结果来过滤数据集,并直观地显示与所分析图像特征匹配的项目。
# Extract the relevant features from the analysis
item_descs = image_analysis['items']
item_category = image_analysis['category']
item_gender = image_analysis['gender']
# Filter data such that we only look through the items of the same gender (or unisex) and different category
filtered_items = styles_df.loc[styles_df['gender'].isin([item_gender, 'Unisex'])]
filtered_items = filtered_items[filtered_items['articleType'] != item_category]
print(str(len(filtered_items)) + " Remaining Items")
# Find the most similar items based on the input item descriptions
matching_items = find_matching_items_with_rag(filtered_items, item_descs)
# Display the matching items (this will display 2 items for each description in the image analysis)
html = ""
paths = []
for i, item in enumerate(matching_items):
item_id = item['id']
# Path to the image file
image_path = f'data/sample_clothes/sample_images/{item_id}.jpg'
paths.append(image_path)
html += f'<img src="https://raw.githubusercontent.com/openai/openai-cookbook/main/examples/{image_path}" style="display:inline;margin:1px"/>'
# Print the matching item description as a reminder of what we are looking for
print(item_descs)
# Display the image
display(HTML(html))
护栏
在使用大型语言模型(如 GPT-4o mini)时,“护栏”指的是为确保模型输出保持在期望参数或边界内而设置的机制或检查。这些护栏对于维持模型响应的质量和相关性至关重要,尤其是在处理复杂或细微的任务时。
护栏之所以有用,原因如下:
- 准确性:它们有助于确保模型的输出准确且与提供的输入相关。
- 一致性:它们保持模型响应的一致性,尤其是在处理相似或相关的输入时。
- 安全性:它们防止模型生成有害、冒犯或不适当的内容。
- 上下文相关性:它们确保模型的输出与特定任务或领域上下文相关。
在我们的案例中,我们使用 GPT-4o mini 分析时尚图像,并建议能够搭配原始服装的配饰。为了实现护栏,我们可以优化结果:在从 GPT-4o mini 获得初步建议后,我们将原始图像和建议的配饰发送回模型。然后,我们可以要求 GPT-4o mini 评估每个建议的配饰是否确实适合原始服装。
这使模型能够根据反馈或额外信息自我纠正和调整其输出。通过实施这些护栏并启用自我纠正,我们可以增强模型在时尚分析和推荐方面的输出的可靠性和实用性。
为了促进这一点,我们编写一个提示,要求 LLM 对建议的配饰是否与原始服装匹配给出简单的“是”或“否”答案。这种二元响应有助于简化优化过程,并确保从模型获得清晰且可操作的反馈。
def check_match(reference_image_base64, suggested_image_base64):
response = client.chat.completions.create(
model=GPT_MODEL,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": """ You will be given two images of two different items of clothing.
Your goal is to decide if the items in the images would work in an outfit together.
The first image is the reference item (the item that the user is trying to match with another item).
You need to decide if the second item would work well with the reference item.
Your response must be a JSON output with the following fields: "answer", "reason".
The "answer" field must be either "yes" or "no", depending on whether you think the items would work well together.
The "reason" field must be a short explanation of your reasoning for your decision. Do not include the descriptions of the 2 images.
Do not include the ```json ``` tag in the output.
""",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{reference_image_base64}",
},
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{suggested_image_base64}",
},
}
],
}
],
max_tokens=300,
)
# Extract relevant features from the response
features = response.choices[0].message.content
return features
最后,让我们确定上面确定的哪些单品真正与这套服装相得益彰。
# Select the unique paths for the generated images
paths = list(set(paths))
for path in paths:
# Encode the test image to base64
suggested_image = encode_image_to_base64(path)
# Check if the items match
match = json.loads(check_match(encoded_image, suggested_image))
# Display the image and the analysis results
if match["answer"] == 'yes':
display(Image(filename=path))
print("The items match!")
print(match["reason"])
我们可以观察到,初始的潜在物品列表已得到进一步细化,从而产生了一个与服装更加匹配的精选列表。此外,模型还解释了为什么每个物品都被认为是一个好的搭配,为决策过程提供了宝贵的见解。
结论
在本 Jupyter Notebook 中,我们探索了 GPT-4o mini 及其他机器学习技术在时尚领域的应用。我们演示了如何分析服装物品的图像、提取相关特征,并利用这些信息来寻找与原始服装搭配的匹配单品。通过实施护栏和自校正机制,我们优化了模型的建议,确保其准确且与上下文相关。
这种方法在现实世界中有多种实际用途,包括:
- 个性化购物助手:零售商可以利用这项技术向顾客提供个性化的服装推荐,提升购物体验并提高客户满意度。
- 虚拟衣橱应用:用户可以上传自己衣物的图片来创建虚拟衣橱,并获得与现有单品搭配的新品建议。
- 时装设计与造型:时装设计师和造型师可以利用此工具尝试不同的组合和风格,简化创作过程。
然而,需要考虑的一点是成本。使用 LLM 和图像分析模型可能会产生费用,尤其是在大量使用的情况下。考虑实施这些技术的成本效益非常重要。gpt-4o-mini 的价格为每 1000 个 token $0.01。对于一张 256px x 256px 的图像,这相当于 $0.00255。
总体而言,本 notebook 为时尚与 AI 交叉领域的进一步探索和发展奠定了基础,为更个性化和智能的时尚推荐系统打开了大门。
常见问题
常见错误:1) 嵌入向量维度不匹配,确保使用相同模型生成;2) CSV读取时格式错误,使用on_bad_lines='skip'跳过坏行;3) API调用超时或限流,使用tenacity重试机制;4) 图像编码错误,确保base64字符串正确;5) 模型输出非JSON格式,提示中明确要求不包含json标签,并增加示例。排查方法:检查API响应状态码,打印中间结果,验证嵌入向量形状。生产优化:使用向量数据库(如FAISS)替代内存检索,提升检索速度;对图像进行预处理(如缩放、裁剪)以降低token消耗;缓存嵌入结果,避免重复计算;采用异步处理提升吞吐量;监控API成本,设置预算告警。
企业应用扩展
可扩展方向:1) 集成用户反馈机制,动态调整推荐阈值;2) 支持多模态输入(如文本+图像)进行更精准的匹配;3) 引入知识图谱增强风格关联;4) 部署为REST API,结合前端应用;5) 使用更高效的嵌入模型(如text-embedding-3-small)平衡成本与精度;6) 增加推荐解释生成,提升用户体验。
总结
本教程面向有编程基础的开发者,深入讲解如何利用GPT-4o mini的图像理解能力与RAG(检索增强生成)技术构建服装搭配推荐系统。内容涵盖环境搭建、批量生成文本嵌入、自定义余弦相似度匹配算法、基于视觉的服装特征提取、以及通过二次校验实现推荐结果的护栏机制。教程强调工程实践,包括并行处理、成本估算、错误排查与性能优化,帮助读者掌握构建生产级AI应用的关键技术。
- --
内容来源与验证
- 内容等级:PREMIUM_A
- 质量等级:A
- 难度:中级
- 验证等级:V1
- 原文作者:teomusatoiu
- 原文日期:2024-07-18
- 来源许可:MIT
- 来源版本:
a29a52bee4812762769d53b20198845a32940141 - 查看原文
站内还有更多完整实战教程
加入会员,解锁全部教程、完整代码与持续更新内容。
加入会员