2024年7月18日

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

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

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

基于GPT-4o mini模型的能力,我们采用自定义匹配算法和RAG技术来搜索知识库中与识别特征互补的服装单品。该算法会考虑色彩搭配和风格协调性等因素,为用户提供合适的推荐。通过本笔记本,我们旨在展示这些技术在创建服装推荐系统中的实际应用。

结合使用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条目的更大数据集的样本。此步骤也可以通过使用现成的向量数据库来替代。例如,您可以按照这些操作指南之一来设置您的向量数据库。

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,并要求模型识别相关特征。

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

  1. 输出格式规范:我们指示模型返回一个具有预定义结构的JSON块,包含以下内容:

    • items (str[]): 一个字符串列表,每个字符串代表一件服装的简洁标题,包含款式、颜色和性别信息。这些标题与我们原始数据库中的productDisplayName属性非常相似。
    • category (str): 最能代表给定物品的类别。模型会从原始样式数据框中所有唯一的articleTypes列表中进行选择。
    • gender (str): 表示该商品适用性别的标签。模型从选项[Men, Women, Boys, Girls, Unisex]中进行选择。
  2. 清晰简洁的指令:

    • 我们提供了关于项目标题应包含内容及输出格式的明确说明。输出应为JSON格式,但不包含模型响应通常带有的json标签。
  3. 单次示例:

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

通过遵循这一结构化方法,我们的目标是从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)

接下来,我们处理图像分析的结果,并用它来筛选和显示数据集中匹配的项目。以下是代码的分解说明:

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

  2. 筛选数据集: 我们过滤styles_df数据框,仅包含与图像分析得出的性别匹配(或中性)的商品,并排除与已分析图像相同类别的商品。

  3. 查找匹配项: 我们使用find_matching_items_with_rag函数在筛选后的数据集中查找与从分析图像中提取的描述相匹配的项。

  4. Displaying Matching Items: We create an HTML string to display images of the matching items. We construct the image paths using the item IDs and append each image to the HTML string. Finally, we use display(HTML(html)) to render the images in the 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))

Guardrails

在使用诸如GPT-4o mini等大型语言模型(LLMs)时,"guardrails"(防护栏)指的是为确保模型输出保持在预期参数或边界内而设置的机制或检查措施。这些防护栏对于维持模型回答的质量和相关性至关重要,特别是在处理复杂或微妙任务时。

护栏(Guardrails)在多个方面都非常有用:

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

在我们的案例中,我们使用GPT-4o mini来分析时尚图片,并为原始服装推荐搭配单品。为了实现安全防护措施,我们可以优化结果:在获得GPT-4o mini的初始推荐后,我们可以将原始图片和推荐单品再次发送给模型。然后要求GPT-4o mini评估每个推荐单品是否确实适合原始服装搭配。

这赋予模型自我纠正的能力,使其能够根据反馈或额外信息调整自身输出。通过实施这些防护措施并启用自我纠正功能,我们可以在时尚分析与推荐场景中提升模型输出的可靠性和实用性。

为此,我们编写了一个提示词,要求大语言模型对"推荐单品是否与原始穿搭匹配"这个问题给出简单的"是"或"否"回答。这种二元响应有助于简化优化流程,并确保从模型获得清晰且可操作的反馈。

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个token$0.01。对于一张256px x 256px的图像,成本累计为$0.00255

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