如何将 GPT4o mini 与 RAG 结合使用以创建服装搭配应用

2024 年 7 月 18 日
在 Github 中打开

欢迎来到服装搭配应用 Jupyter Notebook!本项目演示了 GPT-4o mini 模型在分析服装图片和提取关键特征(如颜色、风格和类型)方面的强大功能。我们应用的核心依赖于 OpenAI 开发的先进图像分析模型,这使我们能够准确识别输入服装物品的特征。

GPT-4o mini 是一款小型模型,它将自然语言处理与图像识别相结合,使其能够理解和生成基于文本和视觉输入的响应,且延迟低。

在 GPT-4o mini 模型功能的基础上,我们采用了自定义匹配算法和 RAG 技术,在我们的知识库中搜索与已识别特征互补的物品。该算法考虑了颜色兼容性和风格一致性等因素,为用户提供合适的推荐。通过本 notebook,我们旨在展示这些技术在创建服装推荐系统中的实际应用。

结合使用 GPT-4o mini + RAG(检索增强生成)具有以下几个优势

  1. 上下文理解:GPT-4o mini 可以分析输入图像并理解上下文,例如描绘的对象、场景和活动。这使得在各种领域(无论是室内设计、烹饪还是教育)都能提供更准确和相关的建议或信息。
  2. 丰富的知识库:RAG 将 GPT-4 的生成能力与检索组件相结合,该组件可以访问跨不同领域的大量信息语料库。这意味着系统可以基于广泛的知识(从历史事实到科学概念)提供建议或见解。
  3. 定制化:该方法可以轻松定制,以满足各种应用中用户的特定需求或偏好。无论是根据用户对艺术的品味定制建议,还是根据学生的学习水平提供教育内容,系统都可以进行调整以提供个性化体验。

总的来说,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 个物品的更大数据集的样本。此步骤也可以替换为使用开箱即用的向量数据库。例如,您可以按照这些 cookbooks 之一来设置您的向量数据库。

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 tokens。鉴于数据集大约有 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)))

构建匹配算法

在本节中,我们将开发一种余弦相似度检索算法,以在我们的 dataframe 中查找相似的物品。我们将为此目的使用我们的自定义余弦相似度函数。虽然 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 以进行分析,并请求模型识别相关特征。

为了确保模型返回准确的结果,我们在提示中使用了特定技术

  1. 输出格式规范:我们指示模型返回具有预定义结构的 JSON 块,包括

    • items (str[]):字符串列表,每个字符串代表服装物品的简洁标题,包括风格、颜色和性别。这些标题与我们原始数据库中的 productDisplayName 属性非常相似。
    • category (str):最能代表给定物品的类别。模型从原始 styles dataframe 中存在的所有唯一 articleTypes 列表中选择。
    • gender (str):指示物品预期性别的标签。模型从选项 [Men, Women, Boys, Girls, Unisex] 中选择。
  2. 清晰简洁的说明:

    • 我们提供了关于物品标题应包含什么以及输出格式应是什么的明确说明。输出应为 JSON 格式,但不包含模型响应通常包含的 json 标签。
  3. 一次性示例:

    • 为了进一步明确预期的输出,我们为模型提供了一个示例输入描述和一个对应的示例输出。虽然这可能会增加使用的 tokens 数量(从而增加调用的成本),但它有助于指导模型并带来更好的整体性能。

通过遵循这种结构化方法,我们旨在从 gpt-4o-mini 模型中获得精确且有用的信息,以供进一步分析和集成到我们的数据库中。

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}",
                },
                }
            ],
            }
        ]
    )
    # 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 恤

通过使用这些不同的图像测试提示,我们可以评估其准确分析和提取不同类型服装和配饰相关特征的能力。

我们需要一个实用函数来以 base64 编码 .jpg 图像

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)

接下来,我们处理图像分析的输出,并使用它来过滤和显示来自我们数据集的匹配物品。以下是代码的分解

  1. 提取图像分析结果:我们从 image_analysis 字典中提取物品描述、类别和性别。

  2. 过滤数据集:我们过滤 styles_df DataFrame,以仅包含与图像分析中的性别匹配(或为中性)的物品,并排除与分析图像类别相同的物品。

  3. 查找匹配物品:我们使用 find_matching_items_with_rag 函数在过滤后的数据集中查找与从分析图像中提取的描述匹配的物品。

  4. 显示匹配物品:我们创建一个 HTML 字符串来显示匹配物品的图像。我们使用物品 ID 构建图像路径,并将每个图像附加到 HTML 字符串。最后,我们使用 display(HTML(html)) 在 notebook 中渲染图像。

此单元格有效地演示了如何使用图像分析的结果来过滤数据集,并直观地显示与分析图像特征匹配的物品。

# 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="{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 这样的大型语言模型 (LLM) 的上下文中,“防护栏”是指为确保模型的输出保持在期望的参数或边界内而设置的机制或检查。这些防护栏对于保持模型响应的质量和相关性至关重要,尤其是在处理复杂或细微的任务时。

防护栏在以下几个方面很有用

  1. 准确性:它们有助于确保模型的输出准确且与提供的输入相关。
  2. 一致性:它们保持模型响应的一致性,尤其是在处理相似或相关的输入时。
  3. 安全性:它们防止模型生成有害、冒犯性或不适当的内容。
  4. 上下文相关性:它们确保模型的输出在上下文中与正在使用的特定任务或领域相关。

在我们的例子中,我们使用 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 和其他机器学习技术在时尚领域的应用。我们演示了如何分析服装物品的图像、提取相关特征,并使用此信息查找与原始服装互补的匹配物品。通过实施防护栏和自我纠正机制,我们优化了模型的建议,以确保它们准确且在上下文中相关。

这种方法在现实世界中有几个实际用途,包括

  1. 个性化购物助手:零售商可以使用此技术为顾客提供个性化的服装推荐,从而提升购物体验并提高顾客满意度。
  2. 虚拟衣橱应用:用户可以上传自己服装物品的图像以创建虚拟衣橱,并接收与现有服装相匹配的新物品的建议。
  3. 时尚设计和造型:时尚设计师和造型师可以使用此工具来尝试不同的组合和风格,从而简化创作过程。

但是,需要记住的考虑因素之一是成本。使用 LLM 和图像分析模型可能会产生费用,尤其是在广泛使用时。重要的是要考虑实施这些技术的成本效益。gpt-4o-mini 的定价为每 1000 tokens $0.01。对于一张 256px x 256px 的图像,这加起来为 $0.00255

总的来说,本 notebook 为进一步探索和发展时尚与 AI 的交叉领域奠定了基础,为更加个性化和智能的时尚推荐系统打开了大门。