赞
踩
目录
前面提到了自己在微调 Baichuan7B Lora 的过程中遇到了一些问题,后面通过调整已经调通。鉴于自己刚刚从推荐算法转 AIGC,所以用笔记的形式记录下用于后面查漏补缺以及对 API 的熟悉。本文主要介绍 LORA 微调时原始数据的处理与编码,即 encode By tokenizer,最终生成可用的 Dataset。
- {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
- {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
- {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a":"鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每只
- 小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
- {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
- {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
- {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
- {"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
- {"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
- {"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
这里 q 可以理解为 question,a 可以理解为 answer,上面将基础的训练数据重复了几次生成原始的训练文件 simple.json。
- import argparse
- import json
- from tqdm import tqdm
- import datasets
- import transformers
-
- # 1.参数准备
- parser = argparse.ArgumentParser()
- parser.add_argument("--model_checkpoint", type=str, help="checkpoint, like `THUDM/chatglm-6b`") # 必填
- parser.add_argument("--input_file", type=str, help="Instruction 数据文件地址,文件中每一行都是json格式,包含一个输出和一个输出") # 必填
- parser.add_argument("--prompt_key", type=str, default=f"prompt", help="你的jsonl文件里,Instruction 的输入字段是什么") # 选填
- parser.add_argument("--target_key", type=str, default=f"target", help="你的jsonl文件里,Instruction 的输出字段是什么") # 必填
- parser.add_argument("--save_name", type=str, default=f"temp", help="经过tokenize之后的数据集的存放位置") # 选填
- parser.add_argument("--max_seq_length", type=int, default=2040) # 选填
- parser.add_argument("--skip_overlength", type=bool, default=False) # 选填
- args = parser.parse_args()
参数采用 argparse 类进行初始化:
- model_checkpoint : 预训练模型地址,这里我们提前把 Baichuan7B 或者 ChatGLM 下载好即可
- input_file : 原始训练数据,训练数据格式为 json,可以参考上面的数据示例
- prompt_key : 训练数据在 json 里 prompt 提示对应的 key,上例为 q
- target_key : 训练数据在 json 里 target 提示对应的 key,上例为 a
- save_name : 保存地址,数据最终会议 arrow 的数据将 dataset 保存
- max_seq_length : 最长阶段序列长度
- skip_overlength : 是否忽略超长的文本,True 时忽略,False 时采取截断
以 json 里一条样本为例:
{"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
- def preprocess(tokenizer, config, example, max_seq_length, prompt_key, target_key):
- prompt = example[prompt_key]
- target = example[target_key]
- prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
- target_ids = tokenizer.encode(target, max_length=max_seq_length, truncation=True, add_special_tokens=False)
- # 最终还是将 instruction 的输入输出都拼在一起,使用经典的 causal-LM 的 next word prediction 方式来训练
- input_ids = prompt_ids + target_ids + [config.eos_token_id] # EOS 用于标识句子结束
- return {"input_ids": input_ids, "seq_len": len(prompt_ids)}
根据配置的 prompt_key 和 target_key 获取 json 里对应的 prompt 与 target 内容,本例下 prompt_key = "q",target_key = "a",通过加载预训练模型获取对应的 Tokenizer 对 q、a 的文本进行 encode 编码。
- Q: 请计算:39 * 0 = 什么?
- A: 这是简单的乘法运算,39乘以0得到的是0
- TokenQ: [31010, 6184, 77, 55, 61, 1734, 31106, 52, 1147, 31106, 1534, 75]
- TokenA: [31106, 3908, 14313, 32329, 31257, 31481, 31742, 72, 55, 61, 32329, 31187, 52, 5442, 2585, 52]
为什么把 QA 前后连接拼到一起,上面的注释也给出了原因,该样本用于使用 causal-LM 模型进行 next word 的预测即续写功能的训练。通过将 Q 放在 A 前面训练,学习 QA 的前后文字逻辑。未来模型训练完毕后,我们给出 Q,模型机会根据之前的训练续写出 A 的相关内容。
上面 preprocess 的逻辑主要在 read_json 里调用,该方法主要用于加载预训练模型生成 Tokenizer 与 config,
- def read_json(path, max_seq_length, prompt_key,target_key,skip_overlength=False):
- # 基于预训练模型加载获取 tokenizer 和 config
- tokenizer = transformers.AutoTokenizer.from_pretrained(
- model_checkpoint, trust_remote_code=True)
- config = transformers.AutoConfig.from_pretrained(
- model_checkpoint, trust_remote_code=True, device_map='auto')
- with open(path, "r") as f:
- for line in tqdm(f.readlines()):
- example = json.loads(line)
- feature = preprocess(tokenizer, config, example, max_seq_length,prompt_key,target_key)
- if skip_overlength and len(feature["input_ids"]) > max_seq_length:
- continue
- # 截取最大长度
- feature["input_ids"] = feature["input_ids"][:max_seq_length]
- yield feature
json.loads 加载一条样本随后调用 preprocess 生成训练 json,这里会根据 skip_overlength 参数决定是否忽略超长样本,最后返回 feature json。这里 tqdm 用于为迭代器 iterator 生成一个可视化的进度条,是一个辅助类。
本着一个参数都不放过的原则,博主查阅了模型加载中用到的两个参数含义:
- trust_remote_code
该参数指示系统在执行远程或外部代码时如何处理安全性和信任性。如果 "trust_remote_code" 设置为 True,则系统将信任并执行远程或外部提供的代码,而不进行严格的安全检查或验证。反之则系统会采取更谨慎的做法,并对远程或外部提供的代码进行安全性检查和验证,以确保其不会造成潜在的风险或恶意操作。这是一种常见的安全策略,用于防止恶意代码或攻击者利用远程执行漏洞来入侵系统。由于我们一般加载的都是官方认可的预训练模型,例如 Baichuan7B、ChatGLM 等等,所以一般看到的代码里都是 True。
- device_map
该参数用于指定设备映射或设备配置的相关信息。可以使用 map 将任务分配给特定的硬件设备或资源,当然也可以像上面一样使用 auto。
- # 输入文件统一放在 data 文件夹下
- # 输出文件统一放在 data/tokenized_data 文件夹下
- input_file_path = f'data/{args.input_file}'
- save_path = f"data/tokenized_data/{args.save_name}"
- dataset = datasets.Dataset.from_generator(
- lambda: read_jsonl(input_file_path, args.max_seq_length, args.prompt_key,args.target_key,args.skip_overlength)
- )
-
- dataset.save_to_disk(save_path)
这里默认原始训练文件 json 存放在 data 文件夹下,经过 tokenizer 的样本放在 data/tokenized_data 目录下,当然也可以根据自己习惯调整,这个位置影响不大。根据路径调用 datasets.Dataset 的 API 进行 DataSet 的生成与存储。
- 完整代码
- import argparse
- import json
- from tqdm import tqdm
- import datasets
- import transformers
-
- # 1.参数准备
- parser = argparse.ArgumentParser()
- parser.add_argument("--model_checkpoint", type=str, help="checkpoint, like `THUDM/chatglm-6b`") # 必填
- parser.add_argument("--input_file", type=str, help="Instruction 数据文件地址,文件中每一行都是json格式,包含一个输出和一个输出") # 必填
- parser.add_argument("--prompt_key", type=str, default=f"prompt", help="你的jsonl文件里,Instruction 的输入字段是什么") # 选填
- parser.add_argument("--target_key", type=str, default=f"target", help="你的jsonl文件里,Instruction 的输出字段是什么") # 必填
- parser.add_argument("--save_name", type=str, default=f"temp", help="经过tokenize之后的数据集的存放位置") # 选填
- parser.add_argument("--max_seq_length", type=int, default=2040) # 选填
- parser.add_argument("--skip_overlength", type=bool, default=False) # 选填
- args = parser.parse_args()
- model_checkpoint = args.model_checkpoint
-
- #. 2.处理逻辑
- def preprocess(tokenizer, config, example, max_seq_length, prompt_key, target_key):
- prompt = example[prompt_key]
- target = example[target_key]
- prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
- target_ids = tokenizer.encode(target, max_length=max_seq_length, truncation=True, add_special_tokens=False)
- # 最终还是将 instruction 的输入输出都拼在一起,使用经典的 causal-LM 的 next word prediction 方式来训练
- input_ids = prompt_ids + target_ids + [config.eos_token_id] # EOS 用于标识句子结束
- return {"input_ids": input_ids, "seq_len": len(prompt_ids)}
-
- # 3.读取训练 JSON
- def read_jsonl(path, max_seq_length, prompt_key,target_key,skip_overlength=False):
- # 基于预训练模型加载获取 tokenizer 和 config
- tokenizer = transformers.AutoTokenizer.from_pretrained(
- model_checkpoint, trust_remote_code=True)
- config = transformers.AutoConfig.from_pretrained(
- model_checkpoint, trust_remote_code=True, device_map='auto')
- with open(path, "r") as f:
- for line in tqdm(f.readlines()):
- example = json.loads(line)
- feature = preprocess(tokenizer, config, example, max_seq_length,prompt_key,target_key)
- if skip_overlength and len(feature["input_ids"]) > max_seq_length:
- continue
- # 截取最大长度
- feature["input_ids"] = feature["input_ids"][:max_seq_length]
- yield feature
-
-
- # 输入文件统一放在 data 文件夹下
- # 输出文件统一放在 data/tokenized_data 文件夹下
- input_file_path = f'data/{args.input_file}'
- save_path = f"data/tokenized_data/{args.save_name}"
- dataset = datasets.Dataset.from_generator(
- lambda: read_jsonl(input_file_path, args.max_seq_length, args.prompt_key,args.target_key,args.skip_overlength)
- )
-
- dataset.save_to_disk(save_path)
-
-
simple.json 为我们的测试样例,tokenizer_data 为存储 token 后 DataSet 的地址。下面看下 tokenizer.sh 的 shell 脚本:
- baichuan="/model/baichuan-7B"
-
- input=simple.json
-
- CUDA_VISIBLE_DEVICES=0 python tokenize_dataset_rows.py \
- --model_checkpoint $baichuan \
- --input_file $input \
- --prompt_key q \
- --target_key a \
- --save_name simple_token_by_baichuan-7B \
- --max_seq_length 2000 \
- --skip_overlength False
执行上述脚本即可得到 tokenizer 后的数据:
当使用 dataset.save_to_dist 方法保存数据集合时会生成三个文件:
dataset.arrow: 这是主要的数据文件,其中包含数据集的实际内容。它以 Apache Arrow 格式存储,这种格式旨在高效地存储和处理大规模数据集。该文件可能包含数据样本、标签、特征、元数据等。
dataset.info.json: 这个 JSON 文件包含与数据集相关的元信息。它提供了关于数据集结构、列名称、数据类型、特征信息、统计摘要等详细信息。通过读取此文件,可以获得数据集的描述性信息,以便更好地理解数据的组织和特征。
dataset.state.json: 这个 JSON 文件包含数据集的状态信息,例如上次更新的时间戳、版本号、数据集大小等。它记录了数据集的状态和元数据,以便在后续操作中能够恢复到相同的点,并确保数据集的一致性。
基于 10 条左右的样本基于 Baichuan7B 微调后,我们测试了原始模型与 Lora 后的效果:
可以看到原始模型存在续写混乱的问题,前面再说查字典,后面又在介绍字典,而经过 Lora 微调后,模型已经能够回答该问题,但是如果换个问法可能就又答不上来了,所以 prompt 工程和样本工程在 AIGC 中是重要组成部分。后面我们将基于 tokenized_data 介绍如何进行 Lora 模型训练。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。