AIGC学习笔记—minimind详解+练习+推理
前言这个开源项目是带我的一个导师,保举我看的,纪录一下整个过程,总结一下收获。这个项目的slogan是“大道至简”,确实很简。作者说是这个项目为了帮助初学者快速入门大语言模子(LLM),通过从零开始练习一个仅26MB的微型语言模子MiniMind,最快可在3小时内完成。低沉学习LLM的门槛,让更多人能够轻松上手。
MiniMind极其轻量,约为GPT-3的1/7000,适合普通个人GPU进行快速推理和练习。项目基于DeepSeek-V2和Llama3结构,涵盖数据处置惩罚、预练习、指令微调(SFT)、偏好优化(DPO)等全部阶段,支持混合专家(MoE)模子。全部代码、数据集及其来源均公开,兼容主流框架,如transformers和DeepSpeed,支持单机单卡及多卡练习,并提供模子测试及OpenAI API接口。
下面放一个官方给的效果
https://i-blog.csdnimg.cn/direct/e1ff0c6664554a10bd2aded320bd22af.png
一、使用conda搭建环境
这里不做过多赘述了,创建一个这个项目的独立假造环境,在这个环境下装所需的库,如下是我的软硬件环境配置(根据自己环境酌情变更):
[*]Windows11
[*]Python == 3.9
[*]Pytorch == 2.1.2
[*]CUDA == 11.8
[*]requirements.txt
二、预备数据集
下载到./dataset/目次下
MiniMind练习数据集下载所在tokenizer练习集HuggingFace / 百度网盘Pretrain数据Seq-Monkey官方 / 百度网盘 / HuggingFaceSFT数据匠数大模子SFT数据集DPO数据Huggingface 这里我就是用官方的了,后续我会打包整体的上传上去,免费下载,要不**某网盘还得冲svip,为了这个会员我差点叫了一声爸爸.....但是这里我想解释一下这个数据集,因为一开始我确实不了解,纪录下来
[*] Tokenizer练习集:这个数据集用于练习分词器(tokenizer),其任务是将文本数据转化为模子可以处置惩罚的词汇单位。
[*] Pretrain数据:用于模子的预练习确保模子能够学习通用的语言模式。
[*] SFT数据:该数据集专门用于指令微调(SFT),使模子能够更好地理解和实行用户的具体指令。SFT是提高模子实际应用能力的告急步骤。
[*] DPO数据:这个数据集主要用于偏好优化(DPO),旨在帮助模子通过用户反馈来改进模子输出的质量和相关性,从而更好地满足用户需求。
三、练习tokenizer
话不多说先上代码,在纪录一下我在看这个代码中了解的知识以及总结。
def train_tokenizer():
# 读取JSONL文件并提取文本数据
def read_texts_from_jsonl(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
yield data['text']
# 数据集路径
data_path = './dataset/tokenizer/tokenizer_train.jsonl'
# 初始化分词器(tokenizer),使用BPE模型
tokenizer = Tokenizer(models.BPE())
# 预处理为字节级别
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# 定义特殊token
special_tokens = ["<unk>", "<s>", "</s>"] # 未知token、开始token、结束token
# 设置训练器并添加特殊token
trainer = trainers.BpeTrainer(
vocab_size=6400, # 词汇表大小
special_tokens=special_tokens,# 确保这三个token被包含
show_progress=True,
# 初始化字母表
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)
# 读取文本数据
texts = read_texts_from_jsonl(data_path)
print(texts)
exit()
# 训练tokenizer
tokenizer.train_from_iterator(texts, trainer=trainer)
# 设置解码器
tokenizer.decoder = decoders.ByteLevel()
# 检查特殊token的索引
assert tokenizer.token_to_id("<unk>") == 0
assert tokenizer.token_to_id("<s>") == 1
assert tokenizer.token_to_id("</s>") == 2
# 保存tokenizer
tokenizer_dir = "./model/yzh_minimind_tokenizer"
os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json")) # 保存tokenizer模型
# 保存BPE模型
tokenizer.model.save("./model/yzh_minimind_tokenizer")
# 手动创建配置文件
config = {
"add_bos_token": False,
"add_eos_token": False,
"add_prefix_space": True,
"added_tokens_decoder": {
"0": {
"content": "<unk>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
},
"1": {
"content": "<s>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
},
"2": {
"content": "</s>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
}
},
"additional_special_tokens": [],
"bos_token": "<s>",
"clean_up_tokenization_spaces": False,
"eos_token": "</s>",
"legacy": True,
"model_max_length": 1000000000000000019884624838656,
"pad_token": None,
"sp_model_kwargs": {},
"spaces_between_special_tokens": False,
"tokenizer_class": "PreTrainedTokenizerFast",
"unk_token": "<unk>",
"use_default_system_prompt": False,
"chat_template": "{% if messages['role'] == 'system' %}{% set system_message = messages['content'] %}{% endif %}{% if system_message is defined %}{{ system_message }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<s>user\\n' + content + '</s>\\n<s>assistant\\n' }}{% elif message['role'] == 'assistant' %}{{ content + '</s>' + '\\n' }}{% endif %}{% endfor %}"
}
# 保存配置文件
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w", encoding="utf-8") as config_file:
json.dump(config, config_file, ensure_ascii=False, indent=4)
print("Tokenizer training completed and saved.") 从代码上来看,分词器使用的是BPE模子Tokenizer(models.BPE()),这条代码就是初始化一个字节对编码(Byte Pair Encoding,BPE)分词器,直接使用库就可以,但是这里我建议同学们去了解一下BPE,这里我保举一篇博客,供大家学习。BPE 算法原理及使用指南【深入浅出】-CSDN博客
小辉问:这内里有几个库的函数解释一下
gpt答:
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
[*]功能:设置分词器的预处置惩罚器为 ByteLevel。
[*]ByteLevel:这一预处置惩罚器会将输入文本处置惩罚为字节级别的tokens,保留每个字节的原始表现,适用于处置惩罚各种字符(如中文、特殊符号等)。add_prefix_space=False 表现在每个词的前面不添加空格,适合某些特定的模子需求。
trainer = trainers.BpeTrainer( vocab_size=6400,
special_tokens=special_tokens,
show_progress=True, initial_alphabet=pre_tokenizers.ByteLevel.alphabet() )
[*]功能:创建一个 BpeTrainer 实例,用于练习基于字节对编码(BPE)的分词器。
[*]vocab_size=6400:指定词汇表的巨细为6400,这意味着练习后分词器将保留6400个最常用的tokens。
[*]special_tokens=special_tokens:将预定义的特殊tokens(如 <unk>、<s>、</s>)传递给练习器,确保这些tokens在练习过程中被考虑。
[*]show_progress=True:在练习过程中显示进度条,方便监控练习希望。
[*]initial_alphabet=pre_tokenizers.ByteLevel.alphabet():使用字节级预处置惩罚器的字母表作为练习的初始字母表,确保全部可能的字节都被考虑。
tokenizer.train_from_iterator(texts, trainer=trainer)
[*]功能:开始练习分词器。
[*]train_from_iterator(texts, trainer=trainer):使用提供的文本迭代器和练习器来练习分词器。分词器会根据传入的文本数据学习如何将文本分别为tokens。
tokenizer.decoder = decoders.ByteLevel()
[*]功能:设置分词器的解码器为 ByteLevel。
[*]ByteLevel():该解码器用于将token IDs转换回原始文本,能够精确处置惩罚字节级表现,确保解码时还原出与输入文本一致的内容。这一过程是确保模子输出可读性的告急环节。
小辉问:为什么要手动定义config?
gpt答:因为配置文件的结构与 Tokenizer(models.BPE()) 推理的过程是密切相关的,配置文件的结构和内容直接影响 Tokenizer(models.BPE()) 在推理时的性能和效果。一个公道的配置文件可以确保分词器在处置惩罚输入文本时的一致性和有用性,从而提高模子的整体表现。
如下两图则表明练习完成,这个过程挺吃内存的,我的电脑是16GB,如果说内存复制失败,重启,再跑一下试试,我看起来16G刚刚够用.....
https://i-blog.csdnimg.cn/direct/7ef8fa08dead4fb0b9c294632945c06e.png
https://i-blog.csdnimg.cn/direct/c39ccb8c1c964b01a2c59ed859196c42.png
四、数据预处置惩罚
处置惩罚文本数据,从原始文本数据会集提取文本内容、将其编码为 token ID,并以二进制格式存储到文件中,以便后续模子的预练习使用。它还处置惩罚了一些潜在的错误环境,并在处置惩罚完后将多个文件归并为一个终极的预练习数据文件。具体工作如下:
处置惩罚文本数据
[*] process_seq_monkey 函数负责读取一个 JSON Lines 格式的数据集文件 (mobvoi_seq_monkey_general_open_corpus.jsonl),并对文本内容进行处置惩罚。
[*] 它按指定的块巨细(默认为 50,000 行)逐块读取数据。
提取与编码
[*] 对于每一块中的每个对象,它提取文本内容,并查抄内容长度是否超过 512 个字符。如果文本长度超过 512,则跳过该条纪录。
[*] 使用一个 tokenizer 将提取的文本内容转换为 token ID,并在文本前后添加开始和结束标记(bos_token 和 eos_token)。
[*] 将转换后的 token ID 添加到 doc_ids 列表中。
错误处置惩罚
[*] 在处置惩罚过程中,如果遇到 Unicode 解码错误,将打印出错误信息,并跳过该行纪录。
数据存储
[*] 一旦 doc_ids 列表的长度超过 1,000,000,便将其转换为 NumPy 数组,并以二进制格式写入一个输出文件 (clean_seq_monkey.bin)。
[*] 处置惩罚完一块后,会更新块索引并打印处置惩罚信息。
[*] 在处置惩罚完全部数据后,如果 doc_ids 列表中另有剩余的数据,便进行末了一次存储。
预练习数据归并
[*] pretrain_process 函数负责调用 process_seq_monkey 函数,并归并全部处置惩罚后的数据。
[*] 它从指定的二进制文件中读取数据,并将其加载为 NumPy 数组。
[*] 末了,将全部归并的数据写入一个新的二进制文件 (pretrain_data.bin)。
def process_seq_monkey(chunk_size=50000):
doc_ids = []# 初始化一个空列表,用于存储文档的token ID
chunk_idx = 0# 初始化块索引
# 使用jsonlines库打开JSON Lines格式的数据集文件
with jsonlines.open('./dataset/dataset/mobvoi_seq_monkey_general_open_corpus.jsonl') as reader:
while True:
# 读取指定大小的块数据
chunk = list(itertools.islice(reader, chunk_size))
if not chunk:# 如果没有更多数据,退出循环
break
# 遍历当前块中的每一项
for idx, obj in enumerate(chunk):
try:
content = obj.get('text', '')# 获取文本内容
if len(content) > 512:# 如果文本长度大于512,则跳过
continue
# 使用tokenizer将文本转换为token ID,并添加开始和结束标记
text_id = tokenizer(f'{bos_token}{content}{eos_token}').data['input_ids']
doc_ids += text_id# 将token ID添加到列表中
except UnicodeDecodeError as e:# 捕获解码错误
print(f"Skipping invalid line {chunk_idx * chunk_size + idx + 1}: {e}")# 打印错误信息,跳过无效行
continue
chunk_idx += 1# 更新块索引
print(f"Processed chunk {chunk_idx} with {chunk_size} lines")# 打印处理的块信息
# 如果doc_ids列表的长度超过1000000,进行数据存储
if len(doc_ids) > 1000000:
arr = np.array(doc_ids, dtype=np.uint16)# 将doc_ids转换为NumPy数组
# 以追加模式打开输出文件
with open(f'./dataset/clean_seq_monkey.bin', 'ab') as f:
f.write(arr.tobytes())# 将数组的字节写入文件
doc_ids = []# 清空doc_ids列表,以便下一块数据使用
# 如果还有剩余的doc_ids,进行最后一次数据存储
if doc_ids:
arr = np.array(doc_ids, dtype=np.uint16)# 将剩余的doc_ids转换为NumPy数组
with open(f'./dataset/clean_seq_monkey.bin', 'ab') as f:
f.write(arr.tobytes())# 将数组的字节写入文件
def pretrain_process():
# process_wiki_clean()
process_seq_monkey()
data_path_list = [
# './dataset/clean-wikipedia-cn.bin',
'./dataset/clean_seq_monkey.bin'
]
data_lst = []
for data_path in data_path_list:
with open(data_path, 'rb') as f:
data = np.fromfile(f, dtype=np.uint16)
data_lst.append(data)
arr = np.concatenate(data_lst)
print(arr.shape)
with open('./dataset/pretrain_data.bin', 'wb') as f:
f.write(arr.tobytes())
pretrain_process() https://i-blog.csdnimg.cn/direct/d8cac9c6aa8d43f7bd6a432353655b65.png
五、模子预练习
实行1-pretrain.py,这里就不放代码了,但是可以具体读一读,我们可以理解为练习一个Transformer模子,Transformer预练习的主要目的是通过大规模无监督文本学习,掌握语言的广泛表现能力,从而为不同的下游任务(如机器翻译、文本分类、文本生成等)提供通用的语言理解能力,并减少对有标注数据的依赖,同时增强模子的泛化能力。
预练习的目标可以分为以下几点:
[*]学习通用语言模式:通过在大量文本数据上进行预练习,Transformer模子能够捕获到语言中的词汇关系、句子结构、语法规则以及上下文依赖等知识。这使得模子在面对不同语言任务时能够更好地理解文本的寄义。
[*]提高下游任务的性能:预练习后的模子已经掌握了丰富的语言知识,因此在处置惩罚特定任务时,只需进行少量的微调(fine-tuning),模子就能快速顺应,并在这些任务上取得较好的效果。比方,预练习的模子可以应用于感情分析、问答系统等任务中,显著提升效果。
[*]减少标注数据的需求:由于预练习是基于无监督数据的,减少了对大量标注数据的依赖。在后续任务中,仅需要少量的有标注数据进行微调即可。这极大地低沉了数据收集和标注的成本。
[*]模子泛化能力增强:通过在大量且多样化的文本数据上进行练习,预练习的Transformer模子具备更强的泛化能力,能够顺应各种不同范例的文本任务,而不局限于特定领域的数据。
六、SFT监督微调
在语言模子领域,SFT 是常见的一个阶段,用于在预练习模子基础上,通过监督学习将模子微调到更具体的任务上。微调的过程中,模子通过大量的标注数据进行练习,调解参数,以提高在对话任务上的表现。可以看看这个练习代码3-full_sft.py,和预练习代码风格类似,支持分布式数据并行练习,和wandb可视化练习的过程。生成以下模子
https://i-blog.csdnimg.cn/direct/6ce42da391db41a28a385ce2fa928192.png
七、测试预练习模子
python 0-eval_pretrain.py
测试效果:
https://i-blog.csdnimg.cn/direct/ff28b5261e27492ca4b2eb4bf41d3360.png
八、测试sft模子
python 2-eval.py
测试效果:
https://i-blog.csdnimg.cn/direct/c8e23ecb9aa14e0681389c6165e5a18b.png
根据上面的效果,不丢脸出一件事儿,就是预练习模子虽然是“接话”式答复,却产生了重复生成的征象,这是一个经典问题,通过我的mentor的引导,可以考虑加在采样的时间加上惩罚loss,解决这个问题是不错的事情,可以加深理解......后续这个解决方案,和效果一定会更新。
sft测试效果明显很受预练习模子的影响,但是答复问题却不是“接话”式的,后续加上lora和dpo看看效果怎么样
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]