OpenAI GPT For Python Developers 第七章 高级微调:药物分类

发布于 2023年10月29日

示例中使用的数据集

在这个示例中,我们将使用一个包含药物名称以及它们用于治疗的疾病、疾病或病况的公共数据集。

我们将创建一个模型并“教”它根据用户输入来预测输出。

用户输入是药物的名称,输出是疾病的名称。

数据集可在 Kaggle.com 上获得,您需要使用以下URL下载

或者,访问以下URL,然后下载名为 Medicine_description.xlsx 的文件。

该文件包含3个工作表,我们将使用第一个称为“Sheet1”的工作表,其中包含3列:

  • 药物名称(Drug_Name)

  • 原因(Reason)

  • 描述(Description)

我们将使用第一和第二列,因为它们包含药物的名称和推荐使用的原因。

例如:

A CN Gel(Topical) 20gmA CN Soap 75gm==>痤疮(Acne)

PPG Trio 1mg Tablet 10'SPPG Trio 2mg Tablet 10'S ==>糖尿病(Diabetes)

Iveomezoled 200mg Injection 500ml ==>真菌感染(Fungal)

准备数据和启动微调

数据文件是一个XLSX文件。我们将把它转换为JSONL格式(JSON行)。JSONL文件将用于微调模型。

我们还将使用以下格式:

{"prompt":"Drug:<DRUGNAME>\nMalady:","completion":"<MALADYNAME>"}

正如您所看到的,我们将使用 \nMalady: 作为分隔符。

完成部分也将以空格开始。请记住由于标记化(大多数单词都在前面有空格的情况下进行标记化),请始终以空格开始每个完成部分。

此外,我们已经学到,每个完成部分应以固定的停止序列结束,以通知模型何时完成。例如,\n、###、END或任何不出现在完成部分中的其他标记。然而,在我们的情况下,这是不必要的,因为我们将使用单个标记进行分类。基本上,我们将为每个疾病赋予一个唯一的标识符。例如:

痤疮(Acne):1

过敏(Allergies):2

阿尔茨海默病(Alzheimer ):3

..等等

这样,模型将在所有情况下在推断时返回一个单一标记。这就是为什么不需要停止序列的原因。

要开始,使用Pandas将数据转换为所需的格式。

import pandas as pd
#read the first nrows
n=2000
df=pd.read_excel('data/Medicine_description.xlsx',sheet_name='Sheet1',header=0,nrows=n)
#get the unique values in the Reason column
reasons=df["Reason"].unique()
#a ssign a number to each reason
reasons_dict={reason: i for i,reason in enumerate(reasons)}
#add a new line and ### to the end of each description
df["Drug_Name"]="Drug:"+df["Drug_Name"]+"\n"+"Malady:"
#concatenate the Reason and Description columns
df["Reason"]=""+df["Reason"].apply(lambda x: "" + str(reasons_dict[x]))
#drop the Reason column
df.drop(["Description"],axis=1,inplace=True)
#rename the columns
df.rename(columns={"Drug_Name":"prompt","Reason":"completion"},inplace=True)
#convert the dataframe to jsonl format
jsonl=df.to_json(orient="records",indent=0,lines=True)
#write the jsonl to a file
with open("data/drug_malady_data.jsonl","w") as f:
  f.write(jsonl)

以上的代码设置从Excel文件中读取的行数为2000。这意味着我们将使用包含2000个药物名称的数据集来进行模型微调。您可以使用更多的数据。

脚本首先从Excel文件“Medicine_description.xlsx”中读取数据的前n行,并将其存储在一个名为df的Dataframe中。

然后,它获取Dataframe中“Reason”列的唯一值,将它们存储在一个名为reasons的数组中,为reasons数组中的每个唯一值分配一个数值索引,并将其存储在一个名为reasons_dict的字典中。

脚本在Dataframe的“Drug_Name”列的每个药物名称的末尾添加了一个新行和“Malady:”。它将一个空格和reasons_dict中相应的数值索引添加到Dataframe中每个“Reason”值的末尾。这是为了得到所需的格式:

Drug:<DRUGNAME>\nMalady:

在这个示例中,我们不需要“Description”列,这就是为什么脚本从Dataframe中删除它的原因。然后,它将“Drug_Name”列重命名为“prompt”,将“Reason”列重命名为“completion”。

Dataframe被转换为JSONL格式,并存储在一个名为jsonl的变量中,然后写入一个名为“drug_malady_data.jsonl”的文件中。

这是“drug_malady_data.jsonl”的示例内容:

[..]
{"prompt":"Drug:Acleen1%Lotion25ml\nMalady:","completion":"0"}
[..]
{"prompt":"Drug:CapneaInjection1ml\nMalady:","completion":"1"}
[..]
{"prompt":"Drug:MondeslorTablet10'S\nMalady:","completion":"2"}
[..]

现在,让我们执行以下命令,进入下一步:

openai tools fine_tunes.prepare_data -f drug_malady_data.jsonl

这将帮助我们准备数据并了解它将如何被模型吸收:

分析...

  • 您的文件包含2000个prompt-completion对

  • 根据您的数据,看起来您正在尝试为分类微调模型

  • 对于分类,我们建议您尝试更快、更便宜的模型,例如`ada`

  • 对于分类,您可以通过保留一个不用于训练的保留数据集来估计预期的模型性能

  • 所有prompt都以后缀`\nMalady:`结束

  • 所有prompt都以前缀`Drug:`开始

您可以将数据分成两组:一组用于训练,另一组用于验证:

  • [建议]您是否要将其分为训练和验证集?[Y/n]:

CLI还将为您提供要执行以训练模型的命令:

现在使用那个文件进行微调:

openai api fine_tunes.create -t "drug_malady_data_prepared_train.jsonl" -v "drug_malady_data_prepared_valid.jsonl" --compute_classification_metrics --classification_n_classes3

它还能够检测数据集中使用的类别数。

最后,执行提供的命令开始训练。您还可以指定模型。我们将使用ada,它便宜且非常适合我们的用例。

您还可以为微调后的模型名称添加后缀,我们将使用drug_malady_data。

# 导出您的OpenAI密钥
export OPENAI_API_KEY="xxxxxxxxxxxx"
openai api fine_tunes.create\
-t "drug_malady_data_prepared_train.jsonl"\
-v "drug_malady_data_prepared_valid.jsonl"\
--compute_classification_metrics\
--classification_n_classes3
-m ada
--suffix "drug_malady_data"

如果客户端在工作完成之前断开连接,可能会要求您重新连接并使用以下命令进行检查:

openai api fine_tunes.follow -i<JOBID>

这是工作完成时的输出示例:

已创建微调:<JOBID>
微调成本$0.03
微调入队
微调在队列中。队列编号:31
微调在队列中。队列编号:30
微调在队列中。队列编号:29
微调在队列中。队列编号:28
[...]
[...]
[...]
微调在队列中。队列编号:2
微调在队列中。队列编号:1
微调在队列中。队列编号:0
微调已启动
完成第1/4轮
完成第2/4轮
完成第3/4轮
完成第4/4轮
已上传模型:<MODELID>
已上传结果文件:<FILEID>
微调成功
工作完成!状态:成功
尝试您微调的模型:
openai api completions.create -m <MODELID> -p <YOUR_PROMPT>

输出显示了微调作业的进度和状态。它确认了使用指定的ID创建了一个新的微调作业。还显示了其他不同的信息:

  • 微调的成本

  • 已完成的轮数

  • 包含微调作业结果的文件的ID

测试微调后的模型

当模型准备好时,您可以使用以下代码进行测试:

import os
import openai

def init_api():
  with open(".env") as env:
    for line in env:
      key, value = line.strip().split("=")
      os.environ[key] = value

openai.api_key = os.environ.get("API_KEY")
openai.organization = os.environ.get("ORG_ID")

init_api()

# 配置模型ID。将其更改为您的模型ID。
model="ada:ft-learninggpt:drug-malady-data-2023-02-21-20-36-07"

# 让我们从每个类别中选择一种药物
drugs=[
  "A CN Gel(Topical) 20gmA CN Soap 75gm", # Class 0 
  "Addnok Tablet 20'S", # Class 1
  "ABICET M Tablet 10's", # Class 2
]

# 为每种药物返回一个药物类别
for drug_name in drugs:
  prompt = "Drug: {}\nMalady:".format(drug_name)
  response = openai.Completion.create(
    model=model,
    prompt= prompt,
    temperature=1,
    max_tokens=1,
  )

# 打印生成的文本
drug_class = response.choices[0].text
# 结果应为0、1和2
print(drug_class)

我们正在测试模型,其中包含来自不同类别的3种药物名称:

  • “A CN Gel(Topical) 20gmA CN Soap 75gm”,类别0(痤疮)

  • “Addnok Tablet 20’S”,类别1(ADHD)

  • “ABICET M Tablet 10’s”,类别2(过敏)

代码执行的输出应为:

高级微调:药物分类

0

1

2

如果我们使用以下内容进行测试:

drugs=[
  "What is 'A CN Gel(Topical) 20gmA CN Soap 75gm' used for?", # Class 0 
  "What is 'Addnok Tablet 20'S' used for?", # Class 1
  "What is 'ABICET M Tablet 10's' used for?", # Class 2
]

class_map={ 
  0: "Acne", 
  1: "ADHD",
  2: "Allergies",
  # ...
}

# 为每种药物返回一个药物类别
for drug_name in drugs:
  prompt = "Drug: {}\nMalady:".format(drug_name)
  response = openai.Completion.create(
    model=model,
    prompt= prompt,
    temperature=1,
    max_tokens=1,
)

response = response.choices[0].text

try:
  print(drug_name + " is used for " + class_map[int(response)])
except:
    print("I don't know what " + drug_name + " is used for.")

print()



评论