一键式搭建自己的私人AI智能医生-使用Mistral-7B将大语言模型微调为医学助理的保姆级安装指南
来自我的 Github repo 的示例图像https://github.com/sachink1729/Finetuning-Mistral-7B-Chat-Doctor-Huggingface-LoRA-PEFT
介绍
你好呀!
您是否想过在您自己的自定义数据集上微调大型语言模型 (LLM) 会是什么感觉?有一些资源可以帮助您实现这一点,但坦率地说,即使阅读了那些大量使用 ML 的文章和笔记本,您也无法直接在家用电脑或笔记本电脑上训练 LLM,除非它有一些不错的 GPU!
我最近获得了Sagemaker ml.g4dn.12xlarge实例的访问权限,它为您提供了 4 个 Nvidia T4 GPU(64GB VRAM),我正在分享如何将 Mistral-7B 微调为 Chat Doctor 的经验!
步骤 1:数据集
我使用了一个名为ChatDoctor-HealthCareMagic-100k的数据集,它包含大约 110K+ 行患者的查询和医生的意见。
数据集中的一个例子
不过,我并没有使用整个数据集,为了进行实验,我只使用了从该数据集中随机抽取的大约 5000 行。
数据集如下所示,90%用于训练,10%用于评估。
DatasetDict({
训练:数据集({
特征:[ '指令','输入','输出' ],
行数:4500
})
测试:数据集({
特征:[ '指令','输入','输出' ],
行数:500
})
})
第 2 步:格式化提示并标记数据集。
如果您看到此数据的结构,它包含指令、输入和输出列,我们需要做的是以某种方式格式化它,以便将其输入到 LLM 中。
由于 LLM 基本上是花哨的 Transformer 解码器,因此您只需将所有内容以某种格式组合起来,对其进行标记并将其提供给模型。
要格式化行,我们可以使用非常基本的格式化函数。这里的输入是患者的查询,输出是医生的回答。
def formatting_func(example):
text = f"### 以下是医生对某人查询的意见:\n### 患者查询:{example['input']} \n### 医生意见:{example['output']}"
return text
火车示例的格式示例。
### 以下是医生对某人的询问的意见:
### 患者询问:我有明显的下背部疼痛,左臀部和左腿下部也麻木,大腿上部也出现麻木。MRI 显示“左侧 L3-4 椎间盘向外侧轻微突出,环状裂隙导致左侧神经孔轻微变窄,L3 神经根向后外侧轻微移位”。L4-5 有其他轻微凸起,裂隙,L5-S1 有轻微凸起。1) 这能解释症状吗 2) 2 天内我将乘坐飞机/汽车旅行 8 小时,然后进行其他旅行。这会有害吗?
### 医生意见:您好,您的 MRI 报告确实解释了您的症状。只要您采取某些措施让您的旅途尽可能舒适,旅行是可能的。我建议您确保带足止痛药。乘坐飞机时,要抓住一切机会在机舱内走动,避免长时间保持同一姿势。同样,乘车旅行时,要尽量经常停车,这样你就可以散散步活动一下腿脚。聊天医生。
现在让我们标记数据集!
我们可以使用 Huggingface 的 AutoTokenizer 对数据集进行标记,需要注意的一点是,我已将填充标记用作 eos_token,它是序列标记的末尾,并且填充样式是左填充,因此基本上填充标记将添加在序列的开头而不是结尾,我们为什么要这样做是因为在仅解码器架构中,输出是输入提示的延续——如果没有左填充,输出中就会有间隙。
我们使用的模型是 Mistral AI 的 7B 模型mistralai/Mistral-7B-v0.1。
base_model_id = "mistralai/Mistral-7B-v0.1"
tokenizer = AutoTokenizer.from_pretrained(
base_model_id,
padding_side= "left" ,
add_eos_token= True ,
add_bos_token= True ,
)
tokenizer.pad_token = tokenizer.eos_token
现在我们已经初始化了标记器,可以对数据集进行标记。
max_length = 512 # 不同数据集之间的差异
def generate_and_tokenize_prompt ( prompt ):
result = tokenizer(
formatting_func(prompt),
truncation= True ,
max_length=max_length,
padding= "max_length" ,
)
result[ "labels" ] = result[ "input_ids" ].copy()
return result
train_dataset = dataset[ 'train' ]
eval_dataset = dataset[ 'test' ] tokenized_train_dataset
= train_dataset.map ( generate_and_tokenize_prompt) tokenized_val_dataset = eval_dataset.map (generate_and_tokenize_prompt)
通过分析数据集中有多少百分比属于您的最大长度,可以找到输入的最大长度,对我来说,大约 98% 的数据集的序列长度小于 512,因此我将其固定为 512,请您自行分析,但请注意,长序列将需要更多时间来训练。
让我们看看标记化的数据是什么样的。
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 , 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 , 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 , 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2、2、2、2、1、774、415、2296、349、264、511、310、783、28742、28713、7382、302、264、1338、 28742、28713、5709、28747、28705、13、27332、4186、722、5709、28747、18365、396、16993、3358、2774、8513、567、776、8295、 264、2475、1856、305、1804、 325、270、2429、28705、28740、17013、22640、28731、298、1749、2081、302、1749、16229、28723、 1770、16229、3358、28723、28705、7164、264、6273、266、1721、438、272、396、16993、304、6759、1401、272、16229、2698、 4、28705、1136、8328、354、 264、1832、304、622、24517、1871、298、3221、6300、28723、28705、13、27332、16505、7382、28747、 22557、28808、7812、368、354、272、5709、28723、1047、736、403、707、11254、5915、28725、6273、266、442、9353、11254、 2572、28723、393、1804、 297、456、2698、541、347、835、264、963、294、645、1773、262、28725、4012、513、378、4739、6084、 739、6328、28723、20063、1007、963、294、645、1773、262、541、11634、4242、533、322、3416、28723、415、3358、541、2847、 713、298、272、277、 2121、297、1259、1222、304、272、1719、3572、541、347、2169、28723、315、3397、368、298、7731、264、 1147、14535、395、378、28723、560、1222、302、28687、28725、2664、261、19474、28742、9271、12423、687、1023、347、2203、 723、12297、456、622、1316、 28723, 2678, 2178, 28723, 2]
您可以看到序列以许多 2 开头,这可能是此标记器的 EOS 标记,然后您可以看到标记“1”,这可能是 BOS(序列开头)标记。
步骤 3:使用量化低秩自适应(QLoRA)初始化 Mistral-7B。
要初始化模型使用:
导入 torch
从 transformers 导入 AutoTokenizer、AutoModelForCausalLM、BitsAndBytesConfig
base_model_id = "mistralai/Mistral-7B-v0.1"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(base_model_id, quantization_config=bnb_config, resume_download=True)
我不会过多讨论量化的工作原理,因为除了创造者自己,没有人能更好地告诉你!
您可以查看https://huggingface.co/blog/4bit-transformers-bitsandbytes了解有关 4 位量化的更多信息。
但基本上我们所做的是在 4 位模式下量化和加载模型,这大大减少了模型的内存占用,而不会对性能产生太大影响!
在继续训练模型之前,让我们看看基础模型如何处理查询:
tokenizer = AutoTokenizer.from_pretrained(
base_model_id,
add_bos_token=True,
)
如果 tokenizer.pad_token 为 None:
tokenizer.add_special_tokens({'pad_token': '[PAD]'})
model_input = tokenizer(eval_prompt, return_tensors="pt").to("cuda")
model.eval()
与 torch.no_grad():
打印(tokenizer.decode(model.generate(**model_input, max_new_tokens=256, repetition_penalty=1.15)[0], skip_special_tokens=True))
结果是:
患者询问:
你好,医生,我正在从登革热中恢复,有时感觉心脏和胸部摩擦,非常不舒服,这可能是我的问题吗?
###
Suresh Reddy 医生的回答:
尊敬的先生/女士,
感谢您的询问。登革热是一种病毒感染,可引起类似流感的症状,如高烧、头痛、肌肉疼痛、恶心、呕吐、腺体肿胀或皮疹。它是由四种密切相关的病毒中的任何一种引起的,这些病毒通过蚊子传播。这种疾病与疟疾、伤寒和肝炎等其他热带疾病有一些相同的临床症状。
登革热最常见的症状是突然发高烧(104°F),伴有严重头痛、眼后疼痛、肌肉和关节疼痛以及特征性皮疹。其他症状包括恶心、呕吐、淋巴结肿大、鼻子或牙龈轻微出血、眼睛充血和容易瘀伤。
在极少数情况下,登革热可能会发展为登革出血热 (DHF),其特征是小血管受损,导致内出血、血小板水平低和血浆渗漏,从而导致休克。这种形式的
您可以看到,该模型对什么是登革热给出了一些见解,但未能完全回答该问题。
步骤 4:PEFT 和 LoRA 配置
你可能想知道什么是 PEFT?PEFT 是参数高效微调,它是一种允许我们冻结大多数模型参数并尝试训练一小部分模型参数的技术,它支持低数据场景,以有效地微调域数据集上的 LLM。
要了解有关 PEFT 的更多信息,请阅读 Huggingface 上的博客https://huggingface.co/blog/peft。
现在,为了开始微调,我们必须对模型进行一些预处理,为训练做好准备。为此,请使用prepare_model_for_kbit_training
PEFT 中的方法。
从 peft 导入 prepare_model_for_kbit_training
模型.gradient_checkpointing_enable()
模型 = prepare_model_for_kbit_training(模型)
我们实际上可以看到使用 PEFT 可以训练多少百分比的参数,这非常酷。
def print_trainable_parameters(model):
"""
打印模型中可训练参数的数量。
"""
trainable_params = 0
all_param = 0
for _, param in model.named_parameters():
all_param += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(
f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
)
让我们打印模型来检查其层,因为我们将把 QLoRA 应用于模型的所有线性层。这些层是q_proj
、k_proj
、v_proj
、o_proj
、gate_proj
、up_proj
和。down_proj``lm_head
在这里我们定义 LoRA 配置。
r
是适配器中使用的低秩矩阵的秩,从而控制训练的参数数量。更高的秩将允许更多的表达能力,但需要权衡计算能力。
alpha
是学习权重的缩放因子。权重矩阵按 缩放alpha/r
,因此 的值越高,alpha
LoRA 激活的权重也就越大。
QLoRA 论文中使用的值是r=64
和lora_alpha=16
,据说这些值具有很好的泛化能力,但我们将使用r=32
和,lora_alpha=64
这样我们就可以更加重视新的微调数据,同时降低计算复杂性。
从peft导入LoraConfig,get_peft_model
config = LoraConfig(
r= 32 ,
lora_alpha= 64 ,
target_modules=[
"q_proj" ,
"k_proj" ,
"v_proj" ,
"o_proj" ,
"gate_proj" ,
"up_proj" ,
"down_proj" ,
"lm_head" ,
],
bias= "none" ,
lora_dropout= 0.05 , # 常规
task_type= "CAUSAL_LM" ,
)
model = get_peft_model(model, config)
print_trainable_parameters(model)
打印结果为:
可训练参数:85041152 || 所有参数:3837112320 || 可训练%:2.2162799758751914
在大约 38 亿个参数中,我们可以训练大约 0.08 亿个参数!这真是太棒了。
步骤5:训练模型。
现在我们已经准备好训练模型,让我们继续调用训练器模块。
导入 transformers
从 datetime 导入 datetime
project = "chat-doctor-finetune"
base_model_name = "mistral"
run_name = base_model_name + "-" + project
output_dir = "./" + run_name
trainer = transformers.Trainer(
model=model,
train_dataset=tokenized_train_dataset,
eval_dataset=tokenized_val_dataset,
args=transformers.TrainingArguments(
output_dir=output_dir,
warmup_steps=1,
per_device_train_batch_size=4,
gradient_accumulation_steps=1,
gradient_checkpointing=True,
max_steps=500,
learning_rate=2.5e-4, # 想要一个小的 lr 进行微调
#bf16=True,
optim="paged_adamw_8bit",
logs_steps=25, # 何时开始报告损失
logs_dir="./logs", # 存储目录日志
save_strategy="steps", # 每个日志步骤保存模型检查点
save_steps=25, # 每 50 步保存检查点
evaluation_strategy="steps", # 每个日志步骤评估模型
eval_steps=25, # 每 50 步评估并保存检查点
do_eval=True, # 在训练结束时执行评估
),
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
model.config.use_cache = False # 静音警告。请重新启用推理!
trainer.train(resume_from_checkpoint=True)
这趟火车大约花了我 15-20 个小时,我不记得具体的时间,因为由于 jupyter 笔记本中的内存不足错误,我必须从检查站恢复火车。
但是我可以看到,自 25 步第一次保存以来,响应发生了显著变化,每次保存推断时的响应都变得越来越好。
需要注意的一点是,这些保存不是模型权重,而是 QLoRA 适配器保存的权重,您必须将这些权重与原始模型相结合才能生成响应!
我对其进行了 500 步训练,以下是使用该检查点的结果。
第 6 步:结果!
为了进行推理,我们首先需要再次加载基础模型。
导入 torch
从 transformers 导入 AutoTokenizer、AutoModelForCausalLM、BitsAndBytesConfig
base_model_id = "mistralai/Mistral-7B-v0.1"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_id, # Mistral,与之前相同
quantization_config=bnb_config, # 与之前相同的量化配置
device_map="auto",
trust_remote_code=True,
use_auth_token=True
)
tokenizer = AutoTokenizer.from_pretrained(base_model_id, add_bos_token=True,信任远程代码=真)
现在我们可以将基础模型与训练好的保存的适配器权重结合起来。
从 peft 导入 PeftModel
ft_model = PeftModel.from_pretrained(base_model, “mistral-patient-query-finetune/checkpoint-500/”)
现在,在生成结果之前,让我们使用 Gradio 创建一个非常简单的 UI,如果您不了解 gradio,您可以查看https://www.gradio.app/
Gradio 基本上可以帮助您为模型创建一个简单的 UI 端点,因为我们的目标是构建一个聊天医生,让我们使用聊天机器人模块,但首先让我们创建将生成响应的函数。
def respond(query):
eval_prompt = """患者的查询:\n\n {} ###\n\n""".format(query)
model_input = tokenizer(eval_prompt, return_tensors="pt").to("cuda")
output = ft_model.generate(input_ids=model_input["input_ids"].to(device),
tention_mask=model_input["attention_mask"],
max_new_tokens=125, repetition_penalty=1.15)
result = tokenizer.decode(output[0], skip_special_tokens=True).replace(eval_prompt, "")
返回结果
现在让我们创建 gradio 应用程序。
将 gradio 导入为 gr
def chat_doctor_response(消息,历史记录):
返回响应(消息)
demo = gr.ChatInterface(chat_doctor_response)
demo.launch()
我们就大功告成了,这将打开一个用户界面,您可以在其中输入您的查询,然后聊天医生会做出回应。
示例 1:
很简约!
示例 2:
看看这些反应有多么令人惊奇,至少对于我这样的非医学人士来说是如此!
结论:
这确实是一个有趣的实验!非常感谢这个笔记本金矿https://github.com/brevdev/notebooks/blob/main/mistral-finetune.ipynb,我按照它对 Chat Doctor 数据集进行了实验!
我们了解了如何使用低秩自适应对自定义数据集上的 LLM 进行微调!
我们看到了 GenAI 提供的可能性,在医疗保健领域工作我可以告诉你,市场已经开始或正在将 LLM 纳入其职能中,所以现在是玩和尝试 LLM 的最佳时机!
想要了解更多此类博客,请通过在下面的文章中点赞并关注我(可选呵呵)来激励我!
示例 2:
看看这些反应有多么令人惊奇,至少对于我这样的非医学人士来说是如此!
[外链图片转存中…(img-wfcYJ952-1722909184548)]
结论:
这确实是一个有趣的实验!非常感谢这个笔记本金矿https://github.com/brevdev/notebooks/blob/main/mistral-finetune.ipynb,我按照它对 Chat Doctor 数据集进行了实验!
我们了解了如何使用低秩自适应对自定义数据集上的 LLM 进行微调!
我们看到了 GenAI 提供的可能性,在医疗保健领域工作我可以告诉你,市场已经开始或正在将 LLM 纳入其职能中,所以现在是玩和尝试 LLM 的最佳时机!
想要了解更多此类博客,请通过在下面的文章中点赞并关注我(可选呵呵)来激励我!
谢谢!保持好奇心!
博客原文:专业人工智能技术社区