用 Ollama 创建微调后的模型 2024-06-07 作者 C3P00 前言 我们将介绍如何用 Ollama 创建微调过的模型,让对话效果更符合特定化需要。 一、微调大模型 1. LORA 微调 微调大模型的方法有很多,这里我们使用 LORA 微调方法。微调大模型需要比较高的显存,推荐在云服务器上进行训练,系统环境为 Linux。训练方法可以参考 DataWhale 的教程。 1.1 选择基础大模型 基础大模型我们选择 Chinese-Mistral-7B-Instruct-v0.1,模型文件可以在 Hugging Face 或 魔搭社区 下载。 1.2 下载基础大模型 使用魔搭社区的 Python 脚本进行下载,执行前需要先运行 pip install modelscope。 from modelscope import snapshot_download # 模型存放路径 model_path = '/root/autodl-tmp' # 模型名字 name = 'itpossible/Chinese-Mistral-7B-Instruct-v0.1' model_dir = snapshot_download(name, cache_dir=model_path, revision='master') 2. 选择数据集 微调大模型要想获得比较好的效果,拥有高质量的数据集是关键。可以选择用网上开源的,或者是自己制作。以中文数据集弱智吧为例,约 1500 条对话数据,数据集可以从 Hugging Face 下载。 3. LORA 微调 3.1 安装依赖 使用 Miniconda 创建 Python 环境,Python 版本为 3.10。 依赖文件 requirements.txt 内容如下: transformers streamlit==1.24.0 sentencepiece==0.1.99 accelerate==0.29.3 datasets peft==0.10.0 运行 pip install -r requirements.txt 安装依赖。 3.2 编写训练脚本 3.2.1 指定模型路径 from datasets import Dataset import pandas as pd from transformers import ( AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, ) import torch,os from peft import LoraConfig, TaskType, get_peft_model import warnings warnings.filterwarnings("ignore", category=UserWarning) # 忽略告警 device = 'cuda' if torch.cuda.is_available() else 'cpu' # 模型文件路径 model_path = r'/root/autodl-tmp/itpossible/Chinese-Mistral-7B-Instruct-v0.1' # 训练过程数据保存路径 name = 'ruozhiba' output_dir = f'./output/Mistral-7B-{name}' #是否从上次断点处接着训练,如果需要从上次断点处继续训练,值应为True train_with_checkpoint = False 3.2.2 加载 tokenizer # 加载tokenizer tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token 3.2.3 加载数据集 df = pd.read_json(f'./dataset/{name}.json') ds = Dataset.from_pandas(df) print(ds) 3.2.4 处理数据集 需要将数据集的内容按大模型的对话格式进行处理,不同的模型,对话格式不一样,比如 Qwen1.5、Llama3 的对话格式都不一样。以下面这一条对话数据为例。 处理前的内容: { "instruction": "只剩一个心脏了还能活吗?", "input": "", "output": "能,人本来就只有一个心脏。" } 处理后,喂给大模型的内容: <s>[INST] <<SYS>> <</SYS>> 只剩一个心脏了还能活吗? [/INST] 能,人本来就只有一个心脏。 </s> # 对数据集进行处理,需要将数据集的内容按大模型的对话格式进行处理 def process_func_mistral(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 instruction = tokenizer( f"<s>[INST] <<SYS>>\n\n<</SYS>>\n\n{example['instruction']+example['input']}[/INST]",add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer(f"{example['output']}", add_special_tokens=False) input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为pad_token_id咱们也是要关注的所以 补充为1 labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] if len(input_ids) > MAX_LENGTH: # 做一个截断 input_ids = input_ids[:MAX_LENGTH] attention_mask = attention_mask[:MAX_LENGTH] labels = labels[:MAX_LENGTH] return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels } inputs_id = ds.map(process_func_mistral, remove_columns=ds.column_names) 3.2.5 加载模型 #加载模型 model = AutoModelForCausalLM.from_pretrained(model_path, device_map=device, torch_dtype=torch.bfloat16, use_cache=False) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 print(model) 3.2.6 设置 LORA 训练参数 config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], inference_mode=False, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1 # Dropout 比例 ) 3.2.7 设置训练参数 model = get_peft_model(model, config) model.print_trainable_parameters() args = TrainingArguments( output_dir=output_dir, per_device_train_batch_size=2, gradient_accumulation_steps=2, logging_steps=20, num_train_epochs=2, save_steps=25, save_total_limit=2, learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True ) 3.2.8 开始训练 trainer = Trainer( model=model, args=args, train_dataset=inputs_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) # 如果训练中断了,还可以从上次中断保存的位置继续开始训练 if train_with_checkpoint: checkpoint = [file for file in os.listdir(output_dir) if 'checkpoint' in file][-1] last_checkpoint = f'{output_dir}/{checkpoint}' print(last_checkpoint) trainer.train(resume_from_checkpoint=last_checkpoint) else: trainer.train() 3.2.9 完整的训练脚本 from datasets import Dataset import pandas as pd from transformers import ( AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, ) import torch,os from peft import LoraConfig, TaskType, get_peft_model import warnings warnings.filterwarnings("ignore", category=UserWarning) # 忽略告警 device = 'cuda' if torch.cuda.is_available() else 'cpu' # 模型文件路径 model_path = r'/root/autodl-tmp/itpossible/Chinese-Mistral-7B-Instruct-v0.1' # 训练过程数据保存路径 name = 'ruozhiba' output_dir = f'./output/Mistral-7B-{name}' #是否从上次断点处接着训练 train_with_checkpoint = True # 加载tokenizer tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token #加载数据集 df = pd.read_json(f'./dataset/{name}.json') ds = Dataset.from_pandas(df) print(ds) # 对数据集进行处理,需要将数据集的内容按大模型的对话格式进行处理 def process_func_mistral(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 instruction = tokenizer( f"<s>[INST] <<SYS>>\n\n<</SYS>>\n\n{example['instruction']+example['input']}[/INST]",add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer(f"{example['output']}", add_special_tokens=False) input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为pad_token_id咱们也是要关注的所以 补充为1 labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] if len(input_ids) > MAX_LENGTH: # 做一个截断 input_ids = input_ids[:MAX_LENGTH] attention_mask = attention_mask[:MAX_LENGTH] labels = labels[:MAX_LENGTH] return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels } inputs_id = ds.map(process_func_mistral, remove_columns=ds.column_names) #加载模型 model = AutoModelForCausalLM.from_pretrained(model_path, device_map=device, torch_dtype=torch.bfloat16, use_cache=False) print(model) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], inference_mode=False, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1 # Dropout 比例 ) model = get_peft_model(model, config) model.print_trainable_parameters() args = TrainingArguments( output_dir=output_dir, per_device_train_batch_size=2, gradient_accumulation_steps=2, logging_steps=20, num_train_epochs=2, save_steps=25, save_total_limit=2, learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True ) trainer = Trainer( model=model, args=args, train_dataset=inputs_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) # 如果训练中断了,还可以从上次中断保存的位置继续开始训练 if train_with_checkpoint: checkpoint = [file for file in os.listdir(output_dir) if 'checkpoint' in file][-1] last_checkpoint = f'{output_dir}/{checkpoint}' print(last_checkpoint) trainer.train(resume_from_checkpoint=last_checkpoint) else: trainer.train() 4. 将 checkpoint 转换为 LORA 新建一个 checkpoint_to_lora.py,将训练的 checkpoint 保存为 LORA。 from transformers import AutoModelForSequenceClassification,AutoTokenizer import os # 需要保存的lora路径 lora_path= "/root/lora/Mistral-7B-lora-ruozhiba" # 模型路径 model_path = '/root/autodl-tmp/itpossible/Chinese-Mistral-7B-Instruct-v0.1' # 检查点路径 checkpoint_dir = '/root/output/Mistral-7B-ruozhiba' checkpoint = [file for file in os.listdir(checkpoint_dir) if 'checkpoint-' in file][-1] #选择更新日期最新的检查点 model = AutoModelForSequenceClassification.from_pretrained(f'/root/output/Mistral-7B-ruozhiba/{checkpoint}') # 保存模型 model.save_pretrained(lora_path) tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token # 保存tokenizer tokenizer.save_pretrained(lora_path) 5. 合并模型 新建一个 merge.py 文件,将基础模型和 LORA 模型合并为一个新的模型文件。 from transformers import AutoModelForCausalLM, AutoTokenizer import torch from peft import PeftModel from peft import LoraConfig, TaskType, get_peft_model model_path = '/root/autodl-tmp/itpossible/Chinese-Mistral-7B-Instruct-v0.1' lora_path = "/root/lora/Mistral-7B-lora-ruozhiba" device = 'cuda' if torch.cuda.is_available() else 'cpu' # 合并后的模型路径 output_path = r'/root/autodl-tmp/itpossible/merge' # 等于训练时的config参数 config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], inference_mode=False, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1 # Dropout 比例 ) base = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True) base_tokenizer = AutoTokenizer.from_pretrained(model_path) lora_model = PeftModel.from_pretrained( base, lora_path, torch_dtype=torch.float16, config=config ) model = lora_model.merge_and_unload() model.save_pretrained(output_path) base_tokenizer.save_pretrained(output_path) 二、量化模型 1. 转换模型文件 基础模型和 LORA 合并后的模型,仍然为多个 safetensors,需要将多个 safetensors 合并为一个 bin。合并方法需要使用 llama.cpp 中 convert.py 转换脚本,github 地址 https://github.com/ggerganov/llama.cpp。转换方法详见 ollama 使用自定义大模型 – CSDN 博客。 python convert.py /root/autodl-tmp/itpossible/merge --outtype f16 --outfile /root/autodl-tmp/itpossible/convert.bin 执行转换后,可以得到一个 convert.bin 文件,约等于 14G. 为了节约存储空间,之前的合并模型文件夹可以删除了。✅ rm -rf /root/autodl-tmp/itpossible/merge 2. 量化模型 对 llama.cpp 项目编译后,有个 quantize 可执行文件 /root/ollama/llm/llama.cpp/quantize /root/autodl-tmp/itpossible/convert.bin q5_k_m 得到文件 ggml-model-Q5_K_M. gguf✅,量化参数有多个标准可以选择,选择不同的量化,模型的推理效果不一样。 三、ollama 创建模型 使用 Ollama,根据 ggml-model-Q5_K_M. gguf✅ 创建模型,方法详见 ollama 使用自定义大模型_ollama 上面好用的大模型-CSDN博客。 四、总结 我还分别使用了 llama3-8b,qwen1.5-1.8b 进行 LORA 微调,但是在使用 llama.cpp 进行模型转换环节,出现了 NotImplementedError: BPE pre-tokenizer was not recognized - update get_vocab_base_pre(),从官网查询了很多 issues,暂未找到解决的办法,所以目前只有 Chinese-Mistral-7B-Instruct-v0.1 成功了。 Ollama 的 modelfile 中还提供了添加 ADAPTER 的方法,也就是将 LORA 单独作为 ADAPTER,试了一下,模型推理效果不正确,暂未找到原因。目前,试验成功的方法只有这一个。将基础模型 + LORA 模型合并后,再用 Ollama 创建模型,推理效果达到了预期。 我只训练了不到半小时,要想微调后的对话效果更好,需要更多的数据集,和更长时间的训练。 参考文献 DataWhale Hugging Face 魔搭社区 ollama 使用自定义大模型 – CSDN 博客 ollama 使用自定义大模型_ollama 上面好用的大模型-CSDN博客
前言
我们将介绍如何用 Ollama 创建微调过的模型,让对话效果更符合特定化需要。
一、微调大模型
1. LORA 微调
微调大模型的方法有很多,这里我们使用 LORA 微调方法。微调大模型需要比较高的显存,推荐在云服务器上进行训练,系统环境为 Linux。训练方法可以参考 DataWhale 的教程。
1.1 选择基础大模型
基础大模型我们选择 Chinese-Mistral-7B-Instruct-v0.1,模型文件可以在 Hugging Face 或 魔搭社区 下载。
1.2 下载基础大模型
使用魔搭社区的 Python 脚本进行下载,执行前需要先运行
pip install modelscope
。2. 选择数据集
微调大模型要想获得比较好的效果,拥有高质量的数据集是关键。可以选择用网上开源的,或者是自己制作。以中文数据集弱智吧为例,约 1500 条对话数据,数据集可以从 Hugging Face 下载。
3. LORA 微调
3.1 安装依赖
使用 Miniconda 创建 Python 环境,Python 版本为 3.10。
依赖文件
requirements.txt
内容如下:运行
pip install -r requirements.txt
安装依赖。3.2 编写训练脚本
3.2.1 指定模型路径
3.2.2 加载 tokenizer
3.2.3 加载数据集
3.2.4 处理数据集
需要将数据集的内容按大模型的对话格式进行处理,不同的模型,对话格式不一样,比如 Qwen1.5、Llama3 的对话格式都不一样。以下面这一条对话数据为例。
处理前的内容:
处理后,喂给大模型的内容:
3.2.5 加载模型
3.2.6 设置 LORA 训练参数
3.2.7 设置训练参数
3.2.8 开始训练
3.2.9 完整的训练脚本
4. 将 checkpoint 转换为 LORA
新建一个
checkpoint_to_lora.py
,将训练的 checkpoint 保存为 LORA。5. 合并模型
新建一个
merge.py
文件,将基础模型和 LORA 模型合并为一个新的模型文件。二、量化模型
1. 转换模型文件
基础模型和 LORA 合并后的模型,仍然为多个 safetensors,需要将多个 safetensors 合并为一个 bin。合并方法需要使用 llama.cpp 中
convert.py
转换脚本,github 地址 https://github.com/ggerganov/llama.cpp。转换方法详见 ollama 使用自定义大模型 – CSDN 博客。执行转换后,可以得到一个
convert.bin
文件,约等于 14G. 为了节约存储空间,之前的合并模型文件夹可以删除了。✅2. 量化模型
对 llama.cpp 项目编译后,有个
quantize
可执行文件得到文件
ggml-model-Q5_K_M. gguf✅
,量化参数有多个标准可以选择,选择不同的量化,模型的推理效果不一样。三、ollama 创建模型
使用 Ollama,根据
ggml-model-Q5_K_M. gguf✅
创建模型,方法详见 ollama 使用自定义大模型_ollama 上面好用的大模型-CSDN博客。四、总结
NotImplementedError: BPE pre-tokenizer was not recognized - update get_vocab_base_pre()
,从官网查询了很多 issues,暂未找到解决的办法,所以目前只有 Chinese-Mistral-7B-Instruct-v0.1 成功了。参考文献