Introduction

本学习笔记是基于极客时间《AI大模型之美》第5节课程。

https://b.geekbang.org/member/course/detail/643889

数据处理

import pandas as pd
import tiktoken
import openai
import os

from openai.embeddings_utils import get_embedding, get_embeddings

openai.api_key = os.environ.get("OPENAI_API_KEY")

# embedding model parameters
embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"  # this the encoding for text-embedding-ada-002
max_tokens = 8000  # the maximum for text-embedding-ada-002 is 8191

# import data/toutiao_cat_data.txt as a pandas dataframe
df = pd.read_csv('data/toutiao_cat_data.txt', sep='_!_', names=['id', 'code', 'category', 'title', 'keywords'])
df = df.fillna("")
df["combined"] = (
    "标题: " + df.title.str.strip() + "; 关键字: " + df.keywords.str.strip()
)

print("Lines of text before filtering: ", len(df))

encoding = tiktoken.get_encoding(embedding_encoding)
# omit reviews that are too long to embed
df["n_tokens"] = df.combined.apply(lambda x: len(encoding.encode(x)))
df = df[df.n_tokens <= max_tokens]

print("Lines of text after filtering: ", len(df))


上面代码解释:

这段代码的主要目的是加载一个数据文件(toutiao_cat_data.txt),对其中的文本进行处理和过滤,确保其长度适合嵌入(embedding),并且将其嵌入转换为向量表示。代码使用了 OpenAI 的 text-embedding-ada-002 模型。

逐行解释如下:

导入必要的库

import pandas as pd

import tiktoken

import openai

import os

from openai.embeddings_utils import get_embedding, get_embeddings

• pandas: 用于处理和操作数据的库,尤其是表格形式的数据。

• tiktoken: 一个用于对文本进行编码的库,帮助我们对文本进行分词、编码以适应 OpenAI 的模型。

• openai: OpenAI 的 Python API 客户端,用于与 OpenAI 的服务进行交互。

• os: 用于访问系统环境变量,比如获取 API 密钥。

配置 OpenAI API 密钥

openai.api_key = os.environ.get("OPENAI_API_KEY")

• 从环境变量 OPENAI_API_KEY 中获取 OpenAI API 的密钥,这是为了安全访问 OpenAI 的服务。

嵌入模型参数

embedding_model = "text-embedding-ada-002"

embedding_encoding = "cl100k_base" # 这是 text-embedding-ada-002 使用的编码

max_tokens = 8000 # text-embedding-ada-002 的最大 token 数是 8191

• embedding_model: 选择要使用的嵌入模型,这里是 text-embedding-ada-002,一种非常高效的文本嵌入模型。

• embedding_encoding: 这是嵌入模型使用的编码类型,cl100k_base 是与 text-embedding-ada-002 兼容的编码器。

• max_tokens: 限制文本的最大 token 数,因为这个模型最多支持 8191 个 token,代码中设置为 8000 以确保安全。

导入数据并将其转换为 pandas DataFrame

df = pd.read_csv('data/toutiao_cat_data.txt', sep='_!_', names=['id', 'code', 'category', 'title', 'keywords'])

df = df.fillna("")

• pd.read_csv: 读取 toutiao_cat_data.txt 文件。文件用 ! 分隔符来分隔列,因此通过 sep='_!_' 指定分隔符。

• names: 指定文件中的列名,分别为 id(文章ID)、code(分类编码)、category(类别名称)、title(标题)、keywords(关键字)。

• df.fillna(""): 用空字符串填充 NaN 值,避免空值带来的问题。

创建新列 combined

df["combined"] = (

"标题: " + df.title.str.strip() + "; 关键字: " + df.keywords.str.strip()

)

• 将 title 和 keywords 合并为一个新列 combined,格式为:标题: [标题]; 关键字: [关键字]。这将为每个条目生成一个包含标题和关键字的完整文本。

打印过滤前的文本行数

print("Lines of text before filtering: ", len(df))

• 输出数据集在过滤之前的行数,表示在过滤掉过长文本之前的数据量。

获取嵌入模型的编码器

encoding = tiktoken.get_encoding(embedding_encoding)

• 使用 tiktoken.get_encoding 函数获取与指定的编码器 embedding_encoding(cl100k_base)相对应的分词器,用于将文本分割为 token。

计算每行文本的 token 数并过滤过长文本

df["n_tokens"] = df.combined.apply(lambda x: len(encoding.encode(x)))

df = df[df.n_tokens <= max_tokens]

• 对于每一行的 combined 文本,使用 encoding.encode(x) 对其进行分词,并计算出每行的 token 数。

• df[df.n_tokens <= max_tokens]: 过滤掉 token 数超过 max_tokens(8000)的文本,确保数据适合嵌入模型的输入限制。

打印过滤后的文本行数

print("Lines of text after filtering: ", len(df))

• 输出过滤掉过长文本后的行数,显示剩余可以输入嵌入模型的数据量。

总结

这段代码的主要流程是:

1. 读取文本数据并将其转换为 Pandas DataFrame。

2. 将标题和关键字合并成一个新的列 combined。

3. 计算每条文本的 token 数,过滤掉那些过长的文本。

4. 准备好适合传入 OpenAI 模型的文本数据。

定义get_embedding函数获取数据集

from openai import OpenAI
import os

client = OpenAI(api_key=os.environ['OPENAI_API_KEY'])

EMBEDDING_MODEL = "text-embedding-ada-002"

def get_embedding(text, model=EMBEDDING_MODEL):
   text = text.replace("\n", " ")
   return client.embeddings.create(input = [text], model=model).data[0].embedding

# randomly sample 1k rows
df_1k = df.sample(1000, random_state=42)

df_1k["embedding"] = df_1k.combined.apply(lambda x : get_embedding(x, engine=embedding_model))
df_1k.to_csv("data/toutiao_cat_data_10k_with_embeddings.csv", index=False)  #对文本进行embedding,然后并保存

如何更快地获取数据集

import backoff
from openai.embeddings_utils import get_embeddings

batch_size = 1000

@backoff.on_exception(backoff.expo, openai.error.RateLimitError)
def get_embeddings_with_backoff(prompts, engine):
    embeddings = []
    for i in range(0, len(prompts), batch_size):
        batch = prompts[i:i+batch_size]
        embeddings += get_embeddings(list_of_text=batch, engine=engine)
    return embeddings

# randomly sample 10k rows
df_all = df
# group prompts into batches of 100
prompts = df_all.combined.tolist()
prompt_batches = [prompts[i:i+batch_size] for i in range(0, len(prompts), batch_size)]

embeddings = []
for batch in prompt_batches:
    batch_embeddings = get_embeddings_with_backoff(prompts=batch, engine=embedding_model)
    embeddings += batch_embeddings

df_all["embedding"] = embeddings
df_all.to_parquet("data/toutiao_cat_data_all_with_embeddings.parquet", index=True)

这段代码的主要目的是:

1. 使用 OpenAI 的嵌入模型(text-embedding-ada-002)为数据集中的文本生成嵌入向量。

2. 使用 backoff 库处理可能的 OpenAI API 速率限制错误。

3. 将生成的嵌入向量存储在 parquet 文件中,便于后续处理。

逐步解释如下:

导入必要的库

import backoff

from openai.embeddings_utils import get_embeddings

• backoff: 一个处理异常的库,这里用于在遇到 OpenAI API 的速率限制(RateLimitError)时,自动重试请求。

• get_embeddings: 从 openai.embeddings_utils 中导入的一个函数,用于从 OpenAI 生成文本的嵌入向量。

设置批量大小

batch_size = 1000

• 设置批量大小为 1000,这意味着我们会每次处理 1000 条文本数据,向 OpenAI API 发送批量请求。

定义带有重试机制的嵌入函数

@backoff.on_exception(backoff.expo, openai.error.RateLimitError)

def get_embeddings_with_backoff(prompts, engine):

    embeddings = []

    for i in range(0, len(prompts), batch_size):

        batch = prompts[i:i+batch_size]

        embeddings += get_embeddings(list_of_text=batch, engine=engine)

    return embeddings

• @backoff.on_exception(backoff.expo, openai.error.RateLimitError): 这是 backoff 库的装饰器,指定在遇到 openai.error.RateLimitError 时进行指数级的回退重试。expo 表示重试的时间间隔将呈指数增长(每次重试的等待时间会越来越长)。

• get_embeddings_with_backoff: 该函数使用 OpenAI API 为给定的文本生成嵌入向量。它以批量方式处理输入的文本列表(prompts)。

• for i in range(0, len(prompts), batch_size): 按批次遍历 prompts,每次处理 batch_size(1000)个文本。

• get_embeddings(list_of_text=batch, engine=engine): 使用 get_embeddings 函数从 OpenAI 模型获取该批次文本的嵌入向量。

• embeddings += get_embeddings(...): 将每批次生成的嵌入向量添加到最终结果列表 embeddings 中。

• return embeddings: 返回所有文本的嵌入向量。

随机抽样 10,000 行数据

df_all = df

• 这里应该是用 df 全部数据,而没有随机抽样。假如后续有随机抽样逻辑的话,可以通过像 df.sample() 的函数来做。

将所有文本提取为列表,并按批量分组

prompts = df_all.combined.tolist()

prompt_batches = [prompts[i:i+batch_size] for i in range(0, len(prompts), batch_size)]

• df_all.combined.tolist(): 提取 combined 列中的文本数据,将其转化为 Python 列表,准备对这些文本生成嵌入。

• prompt_batches: 将整个文本列表 prompts 分成多个批次,每个批次包含 batch_size(1000)个文本,以便分批请求嵌入。

生成嵌入向量

embeddings = []

for batch in prompt_batches:

    batch_embeddings = get_embeddings_with_backoff(prompts=batch, engine=embedding_model)

    embeddings += batch_embeddings

• 初始化一个空列表 embeddings,用于存储所有文本的嵌入向量。

• for batch in prompt_batches: 遍历每个批次的文本。

• batch_embeddings = get_embeddings_with_backoff(prompts=batch, engine=embedding_model): 使用 get_embeddings_with_backoff 函数获取当前批次的嵌入向量,自动处理速率限制错误。

• embeddings += batch_embeddings: 将当前批次的嵌入向量追加到 embeddings 列表中。

保存嵌入向量到 parquet 文件

df_all["embedding"] = embeddings

df_all.to_parquet("data/toutiao_cat_data_all_with_embeddings.parquet", index=True)

• df_all["embedding"] = embeddings: 将生成的嵌入向量存入原始数据框的 embedding 列中。

• df_all.to_parquet(...): 将结果数据框保存为 parquet 格式文件,这种格式高效且常用于大数据处理。

训练数据

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score

training_data = pd.read_parquet("data/toutiao_cat_data_all_with_embeddings.parquet")
training_data.head()

df =  training_data.sample(50000, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(
    list(df.embedding.values), df.category, test_size=0.2, random_state=42
)

clf = RandomForestClassifier(n_estimators=300)
clf.fit(X_train, y_train)
preds = clf.predict(X_test)
probas = clf.predict_proba(X_test)

report = classification_report(y_test, preds)
print(report)

结果

                    precision    recall  f1-score   support
  news_agriculture       0.86      0.88      0.87      3908
          news_car       0.92      0.92      0.92      7101
      news_culture       0.83      0.85      0.84      5719
          news_edu       0.89      0.89      0.89      5376
news_entertainment       0.86      0.88      0.87      7908
      news_finance       0.81      0.79      0.80      5409
         news_game       0.91      0.88      0.89      5899
        news_house       0.91      0.91      0.91      3463
     news_military       0.86      0.82      0.84      4976
       news_sports       0.93      0.93      0.93      7611
        news_story       0.83      0.82      0.83      1308
         news_tech       0.84      0.86      0.85      8168
       news_travel       0.80      0.80      0.80      4252
        news_world       0.79      0.81      0.80      5370
             stock       0.00      0.00      0.00        70
          accuracy                           0.86     76538
         macro avg       0.80      0.80      0.80     76538
      weighted avg       0.86      0.86      0.86     76538

术语解释:

  • 准确率,代表模型判定属于这个分类的标题里面判断正确的有多少,有多少真的是属于这个分类的。比如,模型判断里面有 100 个都是农业新闻,但是这 100 个里面其实只有 83 个是农业新闻,那么准确率就是 0.83。准确率自然是越高越好,但是并不是准确率达到 100% 就代表模型全对了。因为模型可能会漏,所以我们还要考虑召回率。

  • 召回率,代表模型判定属于这个分类的标题占实际这个分类下所有标题的比例,也就是没有漏掉的比例。比如,模型判断 100 个都是农业新闻,这 100 个的确都是农业新闻。准确率已经 100% 了。但是,实际我们一共有 200 条农业新闻。那么有 100 条其实被放到别的类目里面去了。那么在农业新闻这个类目,我们的召回率,就只有 100/200 = 50%。 所以模型效果的好坏,既要考虑准确率,又要考虑召回率,综合考虑这两项得出的结果,就是 F1 分数(F1 Score)。

  • F1 分数,是准确率和召回率的调和平均数,也就是 F1 Score = 2/ (1/Precision + 1/Recall)。当准确率和召回率都是 100% 的时候,F1 分数也是 1。如果准确率是 100%,召回率是 80%,那么算下来 F1 分数就是 0.88。F1 分数也是越高越好。

  • 支持的样本量,是指数据里面,实际是这个分类的数据条数有多少。一般来说,数据条数越多,这个分类的训练就会越准确。