当前位置:   article > 正文

LLM - ChatGLM-6B Lora 微调与推理_chatglm2-6b训练lora

chatglm2-6b训练lora

目录

一.引言

二.环境准备

三.ChatGLM-6B Lora 微调

1.样本准备 By Json

2.样本生成 By Tokenizer

3.模型生成 By Trainer

四.ChatGLM-6B Lora 文本生成

1.文本生成 By Chat

2.输出测试

五.总结


一.引言

ChatGLM 是一个初具问答和对话功能的千亿中英语言模型, 并针对中文进行了优化,本文基于 ChatGLM-6B 实现 Lora 微调,整体步骤与前面介绍的 LLM - Baichuan7B Lora 训练详解 有一些相似之处。

Tips:

本文涉及样本处理、模型训练和推理可在一张 Tesla V100 x 32G 实现。

二.环境准备

 主要依赖

  1. python 3.9.11
  2. numpy==1.23.5
  3. torch==2.0.1
  4. transformers==4.29.1

可以将上述依赖版本放入 requirements.txt 中,上面是博主自己的配置,也可以根据 ChatGLM 官方给出的 requirements 准备环境 https://github.com/THUDM/ChatGLM-6B

激活并配置环境

  1. conda create -n chatglm python=3.9
  2. conda activate chatglm
  3. pip install -r requirements.txt

 ChatGLM 6B 模型下载

下载地址: https://huggingface.co/THUDM/chatglm-6b

三.ChatGLM-6B Lora 微调

1.样本准备 By Json

  1. {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
  2. {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
  3. {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a":"鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每只
  4. 小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
  5. {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
  6. {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
  7. {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
  8. {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
  9. {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
  10. {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}

与前面 Baichuan-7B 类似,这里我们准备了 10 条测试样本,每一条样本以 QA 的形式构建上下文对话,其中文件格式需保存为 json。

2.样本生成 By Tokenizer

python 脚本

  1. import argparse
  2. import json
  3. from tqdm import tqdm
  4. import datasets
  5. import transformers
  6. # 1.参数准备
  7. parser = argparse.ArgumentParser()
  8. parser.add_argument("--model_checkpoint", type=str, help="checkpoint, like `THUDM/chatglm-6b`") # 必填
  9. parser.add_argument("--input_file", type=str, help="Instruction 数据文件地址,文件中每一行都是json格式,包含一个输出和一个输出") # 必填
  10. parser.add_argument("--prompt_key", type=str, default=f"prompt", help="你的jsonl文件里,Instruction 的输入字段是什么") # 选填
  11. parser.add_argument("--target_key", type=str, default=f"target", help="你的jsonl文件里,Instruction 的输出字段是什么") # 必填
  12. parser.add_argument("--save_name", type=str, default=f"temp", help="经过tokenize之后的数据集的存放位置") # 选填
  13. parser.add_argument("--max_seq_length", type=int, default=2040) # 选填
  14. parser.add_argument("--skip_overlength", type=bool, default=False) # 选填
  15. args = parser.parse_args()
  16. model_checkpoint = args.model_checkpoint
  17. #. 2.处理逻辑
  18. def preprocess(tokenizer, config, example, max_seq_length, prompt_key, target_key):
  19. print(config.pad_token_id, config.eos_token_id, config.pad_token_id, tokenizer.unk_token, tokenizer.pad_token, tokenizer.eos_token)
  20. prompt = example[prompt_key]
  21. target = example[target_key]
  22. prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
  23. target_ids = tokenizer.encode(target, max_length=max_seq_length, truncation=True, add_special_tokens=False)
  24. # 最终还是将 instruction 的输入输出都拼在一起,使用经典的 causal-LM 的 next word prediction 方式来训练
  25. input_ids = prompt_ids + target_ids + [config.eos_token_id] # EOS 用于标识句子结束
  26. print("p:", prompt_ids)
  27. print("t:", target_ids)
  28. print(input_ids, len(prompt_ids))
  29. return {"input_ids": input_ids, "seq_len": len(prompt_ids)}
  30. # 3.读取训练 JSON
  31. def read_jsonl(path, max_seq_length, prompt_key,target_key,skip_overlength=False):
  32. # 基于预训练模型加载获取 tokenizer 和 config
  33. tokenizer = transformers.AutoTokenizer.from_pretrained(
  34. model_checkpoint, trust_remote_code=True)
  35. config = transformers.AutoConfig.from_pretrained(
  36. model_checkpoint, trust_remote_code=True, device_map='auto')
  37. with open(path, "r") as f:
  38. for line in tqdm(f.readlines()):
  39. example = json.loads(line)
  40. feature = preprocess(tokenizer, config, example, max_seq_length,prompt_key,target_key)
  41. if skip_overlength and len(feature["input_ids"]) > max_seq_length:
  42. continue
  43. # 截取最大长度
  44. feature["input_ids"] = feature["input_ids"][:max_seq_length]
  45. yield feature
  46. # 输入文件统一放在 data 文件夹下
  47. # 输出文件统一放在 data/tokenized_data 文件夹下
  48. input_file_path = f'data/{args.input_file}'
  49. save_path = f"data/tokenized_data/{args.save_name}"
  50. dataset = datasets.Dataset.from_generator(
  51. lambda: read_jsonl(input_file_path, args.max_seq_length, args.prompt_key,args.target_key,args.skip_overlength)
  52. )
  53. dataset.save_to_disk(save_path)

python 脚本的批量处理逻辑主要在 read_jsonl 方法中,方法内会调用处理单条样本的 preprocess 方法,最后使用 datasets.Dataset.from_generator 生成数据 dataset 并调用 save_to_disk 方法保存到磁盘上。这里根据大家习惯可以调整输入和输出的文件夹位置,本例中输入的 input_file 位于 ./data 目录下,tokenizer 后生成的 dataset 位于 ./data/tokenized_data 文件夹下。文件夹下文件内容如下:

shell 脚本

  1. chatGLM="/models/ChatGLM-6B/chatglm-6b/"
  2. input=simple_test.json
  3. outpyt=simple_token_by_chatGLM
  4. CUDA_VISIBLE_DEVICES=0 python tokenize_dataset_rows.py \
  5. --model_checkpoint $chatGLM \
  6. --input_file $input \
  7. --prompt_key q \
  8. --target_key a \
  9. --save_name $output \
  10. --max_seq_length 2000 \
  11. --skip_overlength False

这里把下载好的 ChatGLM 地址传到脚本和 input、output 传入即可,如果你按照上面 qa 的形式构造样本,则 prompt_key 和 target_key 也无需修改,否则需要根据自己 json 里的 QA 的 key 修改。max_seq_length 用于 token 的截取,skip_overlength 用于样本的取舍。

 运行结果

执行脚本后得到 tokenizer 后的样本:

  1. Generating train split: 0 examples [00:00, ? examples/s] 3 130005 3 <unk> <pad> <eop> | 0/9 [00:00<?, ?it/s]
  2. p: [71492, 65416, 12, 13, 18, 44, 5, 8, 29, 5, 64061, 31, 130001, 130004]
  3. t: [65356, 67061, 114063, 79222, 6, 13, 18, 104040, 8, 72656, 63829, 8]
  4. [71492, 65416, 12, 13, 18, 44, 5, 8, 29, 5, 64061, 31, 130001, 130004, 65356, 67061, 114063, 79222, 6, 13, 18, 104040, 8, 72656, 63829, 8, 130005] 14
  5. Generating train split: 1 examples [00:01, 1.49s/ examples]3 130005 3 <unk> <pad> <eop>
  6. p: [5, 68247, 12, 15, 9, 26, 9, 23, 21, 77612, 65267, 31, 130001, 130004]
  7. t: [65356, 67061, 64607, 63947, 79222, 6, 15, 9, 103875, 9, 23, 21, 66359, 63834, 8, 7, 10, 25, 16]
  8. [5, 68247, 12, 15, 9, 26, 9, 23, 21, 77612, 65267, 31, 130001, 130004, 65356, 67061, 64607, 63947, 79222, 6, 15, 9, 103875, 9, 23, 21, 66359, 63834, 8, 7, 10, 25, 16, 130005] 14

其中几个特殊 token 和 id 如下:

  1. eos_token_id、pad_token_id、unk_token、pad_token、eos_token
  2. 130005 3 <unk> <pad> <eop>

这里还有一个坑等下我们训练代码时进行分析。

3.模型生成 By Trainer

python 脚本

  1. from transformers.integrations import TensorBoardCallback
  2. from torch.utils.tensorboard import SummaryWriter
  3. from transformers import TrainingArguments
  4. from transformers import Trainer, HfArgumentParser
  5. from transformers import AutoTokenizer, AutoModel
  6. import torch
  7. import torch.nn as nn
  8. from peft import get_peft_model, LoraConfig, TaskType
  9. from dataclasses import dataclass, field
  10. import datasets
  11. import os
  12. from pprint import pprint as print
  13. model_path = "/models/ChatGLM-6B/chatglm-6b/"
  14. tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True, add_special_tokens=False)
  15. print(tokenizer.mask_token_id)
  16. print(tokenizer.bos_token_id)
  17. @dataclass
  18. class FinetuneArguments:
  19. tokenized_dataset: str = field(default=" ") # tokenized之后的数据集文件夹
  20. model_path: str = field(default=" ")
  21. lora_rank: int = field(default=8)
  22. class CastOutputToFloat(nn.Sequential):
  23. def forward(self, x):
  24. return super().forward(x).to(torch.float32)
  25. def data_collator(features: list) -> dict:
  26. len_ids = [len(feature["input_ids"]) for feature in features]
  27. longest = max(len_ids)
  28. input_ids = []
  29. labels_list = []
  30. for ids_l, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
  31. ids = feature["input_ids"]
  32. seq_len = feature["seq_len"]
  33. labels = ([-100] * (seq_len) + ids[seq_len:] + [-100] * (longest - ids_l))
  34. ids = ids + [tokenizer.pad_token_id] * (longest - ids_l)
  35. _ids = torch.LongTensor(ids)
  36. labels_list.append(torch.LongTensor(labels))
  37. input_ids.append(_ids)
  38. input_ids = torch.stack(input_ids)
  39. labels = torch.stack(labels_list)
  40. return {
  41. "input_ids": input_ids,
  42. "labels": labels,
  43. }
  44. class ModifiedTrainer(Trainer):
  45. def compute_loss(self, model, inputs, return_outputs=False):
  46. return model(
  47. input_ids=inputs["input_ids"],
  48. labels=inputs["labels"],
  49. ).loss
  50. def save_model(self, output_dir=None, _internal_call=False):
  51. from transformers.trainer import TRAINING_ARGS_NAME
  52. os.makedirs(output_dir, exist_ok=True)
  53. torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
  54. saved_params = {
  55. k: v.to("cpu") for k, v in self.model.named_parameters() if v.requires_grad
  56. }
  57. torch.save(saved_params, os.path.join(output_dir, "adapter_model.bin"))
  58. def main():
  59. writer = SummaryWriter()
  60. finetune_args, training_args = HfArgumentParser(
  61. (FinetuneArguments, TrainingArguments)
  62. ).parse_args_into_dataclasses()
  63. # load dataset
  64. dataset = datasets.load_from_disk('data/tokenized_data/'+finetune_args.tokenized_dataset)
  65. print(f"\n{len(dataset)=}\n")
  66. # init model
  67. model = AutoModel.from_pretrained(
  68. model_path, load_in_8bit=False, trust_remote_code=True,
  69. device_map="auto" # 模型不同层会被自动分配到不同GPU上进行计算
  70. ,empty_init=False
  71. )
  72. print(model.hf_device_map)
  73. model.gradient_checkpointing_enable()
  74. model.enable_input_require_grads()
  75. model.lm_head = CastOutputToFloat(model.lm_head)
  76. # setup peft
  77. peft_config = LoraConfig(
  78. task_type=TaskType.CAUSAL_LM,
  79. inference_mode=False,
  80. r=finetune_args.lora_rank,
  81. lora_alpha=32,
  82. lora_dropout=0.1,
  83. )
  84. model = get_peft_model(model, peft_config)
  85. # start train
  86. model.save_pretrained(training_args.output_dir) # 因为adapter_config.json只能通过这个save_pretrained来生成,先这里生成一份,好在训练完之前就可以尝试中间的checkpoint
  87. trainer = ModifiedTrainer(
  88. model=model,
  89. train_dataset=dataset,
  90. args=training_args,
  91. callbacks=[TensorBoardCallback(writer)],
  92. data_collator=data_collator,
  93. )
  94. trainer.train()
  95. writer.close()
  96. # save model
  97. model.save_pretrained(training_args.output_dir)
  98. if __name__ == "__main__":
  99. main()

python 脚本逻辑也不复杂,主要是 Transformer API 和 peft API 的组合使用:

➤  datasets.load_from_disk - 加载 tokenized 生成的训练集

➤  AutoModel.from_pretrained - 加载原生的 ChatGLM-6B 模型

➤  LoraConfig - 配置 Lora 微调参数,主要参数为 r 即 lora_rank

➤  get_peft_model - 在 Base 模型基础上获取 Lora 微调模型

➤  Trainer.train - 继承 Trainer 实现 Loss 计算与 model save 的方法并训练

➤  model.save_pretrained - Lora 微调模型参数保存

shell 脚本

  1. CUDA_VISIBLE_DEVICES=0 python chatglm_lora_tuning.py \
  2. --tokenized_dataset simple_token_by_chatGLM \
  3. --lora_rank 4 \
  4. --per_device_train_batch_size 8 \
  5. --gradient_accumulation_steps 1 \
  6. --num_train_epochs 10 \
  7. --save_steps 200 \
  8. --save_total_limit 2 \
  9. --learning_rate 1e-4 \
  10. --fp16 \
  11. --remove_unused_columns false \
  12. --logging_steps 50 \
  13. --output_dir weights/simple_test_by_chatglm

上述 python 脚本命名为 chatglm_lora_tuning.py,token 后数据保存在 tokenized_data 下的 simple_token_by_chatGLM 文件夹

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/333040
推荐阅读
相关标签