如何结合 GPT-4o Mini 与 RAG - 创建服装搭配应用

欢迎来到服装搭配应用 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 方法为各种时尚相关应用提供了一种快速、强大且灵活的解决方案,充分利用了生成式和检索式人工智能技术的优势。

环境设置

首先,我们将安装必要的依赖项,然后导入库并编写一些稍后将使用的实用函数。

%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 文件。这是更大的数据集的一个样本,其中包含约 44,000 个条目。此步骤也可以替换为使用现成的向量数据库。例如,您可以遵循这些 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("成功打开数据集。数据集包含 {} 件衣物。".format(len(styles_df)))

现在我们将为整个数据集生成嵌入。我们可以并行化这些嵌入的执行,以确保脚本能够扩展到更大的数据集。使用此逻辑,为完整的 44,000 条目数据集创建嵌入的时间从约 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,
):
    # 对语料库进行编码,截断到 max_context_len
    encoding = tiktoken.get_encoding("cl100k_base")
    encoded_corpus = [
        encoded_article[:max_context_len] for encoded_article in encoding.encode_batch(corpus)
    ]

    # 计算语料库统计信息:输入数量、总 token 数以及嵌入的估计成本
    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("嵌入已成功创建。")

创建嵌入的两种选择:

下一行将创建嵌入用于示例服装数据集。此操作将花费约 0.02 秒进行处理,另外约 30 秒将结果写入本地 .csv 文件。该过程使用我们的 text_embedding_3_large 模型,价格为 0.00013 美元/1K token。鉴于该数据集约有 1,000 个条目,以下操作将花费约 0.001 美元。如果您决定使用完整的 44,000 个条目数据集,此操作将花费 2-3 分钟进行处理,大约花费 0.07 美元。

如果您不想继续创建自己的嵌入,我们将使用预先计算的嵌入数据集。您可以跳过此单元格,并取消注释下一个单元格中的代码以继续加载预先计算的向量。此操作大约需要 1 分钟才能将所有数据加载到内存中。

generate_embeddings(styles_df, 'productDisplayName')
print("正在将嵌入写入文件...")
styles_df.to_csv('data/sample_clothes/sample_styles_with_embeddings.csv', index=False)
print("嵌入已成功存储在 sample_styles_with_embeddings.csv 中")
# styles_df = pd.read_csv('data/sample_clothes/sample_styles_with_embeddings.csv', on_bad_lines='skip')

# # 将 'embeddings' 列从列表的字符串表示形式转换为实际的浮点数列表
# styles_df['embeddings'] = styles_df['embeddings'].apply(lambda x: ast.literal_eval(x))

print(styles_df.head())
print("成功打开数据集。数据集包含 {} 件衣物及其嵌入。".format(len(styles_df)))

构建匹配算法

在本节中,我们将开发一个余弦相似度检索算法,用于在我们的数据框中查找相似的商品。我们将为此目的利用自定义的余弦相似度函数。虽然 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]

    # 返回最相似的 top-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-mini 来分析输入图像并提取重要的特征,如详细描述、款式和类型。分析通过简单的 API 调用进行,我们提供图像的 URL 进行分析,并要求模型识别相关特征。

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

  1. 输出格式规范:我们指示模型返回一个具有预定义结构的 JSON 块,包括: - items(字符串数组):一个字符串列表,每个字符串代表服装商品的简洁标题,包括款式、颜色和性别。这些标题非常类似于我们原始数据库中的 productDisplayName 属性。 - category(字符串):最能代表给定商品的类别。模型从原始样式数据框中所有唯一的 articleTypes 列表中进行选择。 - gender(字符串):一个标签,指示商品的预期性别。模型从 [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"""给定一件服装的图像,分析该商品并生成一个包含以下字段的 JSON 输出:“items”、“category”和“gender”。
                           利用您对时尚潮流、款式和性别偏好的理解,为如何搭配服装提供准确且相关的建议。
                           items 字段应为与图片中的商品搭配良好的商品列表。每个商品都应代表一个服装商品的标题,包含商品的款式、颜色和性别。
                           category 需要在以下列表中的类型之间进行选择:{subcategories}。
                           您必须在以下列表中的性别之间进行选择:[Men, Women, Boys, Girls, Unisex]
                           不要包含图片中商品的描述。不要在输出中包含 ```json ``` 标签。

                           示例输入:一张代表黑色皮夹克的图像。

                           示例输出:{{"items": ["修身款女士白色T恤", "白色帆布运动鞋", "女士黑色紧身牛仔裤"], "category": "Jackets", "gender": "Women"}}
                           """,
                },
                {
                "type": "image_url",
                "image_url": {
                    "url": f"data:image/jpeg;base64,{image_base64}",
                },
                }
            ],
            }
        ]
    )
    # 从响应中提取相关特征
    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)

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

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

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

  3. 查找匹配的商品:我们使用 find_matching_items_with_rag 函数查找已分析图像中提取的描述匹配的商品。

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

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

# 从分析中提取相关特征
item_descs = image_analysis['items']
item_category = image_analysis['category']
item_gender = image_analysis['gender']


# 过滤数据,以便我们只查看相同性别(或中性)且不同类别的商品
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)) + " 剩余商品")

# 根据输入商品描述查找最相似的商品
matching_items = find_matching_items_with_rag(filtered_items, item_descs)

# 显示匹配的商品(这将为图像分析中的每个描述显示 2 个商品)
html = ""
paths = []
for i, item in enumerate(matching_items):
    item_id = item['id']

    # 图像文件的路径
    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(item_descs)
# 显示图像
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": """您将收到两张不同服装的图像。
                            您的目标是决定这两件衣服是否可以搭配成一套服装。
                            第一张图像是参考商品(用户试图搭配的商品)。
                            您需要决定第二件商品是否能与参考商品很好地搭配。
                            您的响应必须是 JSON 输出,包含以下字段:“answer”和“reason”。
                            “answer”字段必须是“yes”或“no”,具体取决于您认为这两件商品是否能很好地搭配。
                            “reason”字段必须是对您决策原因的简短解释。不要包含这两张图像的描述。
                            不要在输出中包含 ```json ``` 标签。
                           """,
                },
                {
                "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,
    )
    # 从响应中提取相关特征
    features = response.choices[0].message.content
    return features

最后,让我们确定上面识别的哪些商品确实能搭配这套服装。

# 选择生成图像的唯一路径
paths = list(set(paths))

for path in paths:
    # 将测试图像编码为 base64
    suggested_image = encode_image_to_base64(path)

    # 检查商品是否匹配
    match = json.loads(check_match(encoded_image, suggested_image))

    # 显示图像和分析结果
    if match["answer"] == 'yes':
        display(Image(filename=path))
        print("商品匹配!")
        print(match["reason"])

我们可以看到,最初的潜在商品列表已得到进一步优化,从而产生了一个更精选的、与服装相符的选择。此外,模型还提供了商品为何被视为良好匹配的解释,从而提供了对决策过程的有价值的见解。

结论

在本 Jupyter Notebook 中,我们探讨了 GPT-4o mini 和其他机器学习技术在时尚领域的应用。我们演示了如何分析服装图像、提取相关特征,并利用这些信息查找与原始服装搭配的匹配商品。通过实施护栏和自我纠正机制,我们优化了模型的建议,以确保它们准确且与情境相关。

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

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

然而,需要考虑的一个因素是成本。使用 LLM 和图像分析模型可能会产生费用,尤其是在广泛使用的情况下。考虑实施这些技术的成本效益非常重要。gpt-4o-mini 的价格为每 1000 个 token 0.01 美元。对于一张 256 像素 x 256 像素的图像,总计为 0.00255 美元。

总而言之,本 Notebook 为时尚与人工智能的交叉领域提供了进一步探索和开发的基础,为更个性化、更智能的时尚推荐系统打开了大门。