数据集分类:
- tokenizer训练集:这个数据集用于训练分词器(tokenizer),是文本处理中的一个重要步骤。它可以帮助模型更好地理解文本数据的结构。
- Pretrain数据:这是用于预训练模型的数据集,它可以帮助模型学习语言的基本结构和特征。
- SFT数据:SFT(Supervised Fine-Tuning)数据集,用于监督式微调,可以提高模型在特定任务上的性能。
- DPO数据1和DPO数据2:这两个数据集是用于训练奖励模型的,它们包含了人工标注的偏好数据,可以帮助模型优化回复质量,使其更加符合人类的偏好。
训练分词器
模型用的是BPE分词模型,BPE的核心概念是从字母开始,反复合并频率最高且相邻的两个token,直到达到目标词数。 具体可以看。
分词器训练代码:
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_train.jsonl'
# 初始化tokenizer
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# 定义特殊token
special_tokens = ["<unk>", "<s>", "</s>"]
# 设置训练器并添加特殊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)
# 训练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/minimind_tokenizer"
os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
tokenizer.model.save("./model/minimind_tokenizer")
其中:
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
这里的 pre_tokenizers.ByteLevel 是一个预分词器(PreTokenizer),它的作用是将输入的文本按照字节级别进行分割,并且将每个字节转换成一个对应的可见字符表示。 add_prefix_space=False 表示在预分词的过程中,不在每个字节前面添加空格。(加了空格可以区分单词的开始,设置为False是为了减少词典大小吧)
trainer = trainers.BpeTrainer(
vocab_size=6400,
special_tokens=special_tokens, # 确保这三个token被包含
show_progress=True,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(): 这个参数指定了初始字母表。在这里,initial_alphabet 被设置为 pre_tokenizers.ByteLevel.alphabet(),意味着初始字母表将包含所有可能的字节级别的字符。(与上面设置的pre_tokenizers.ByteLevel搭配使用)
准备预训练数据
其中,‘/dataset/mobvoi_seq_monkey_general_open_corpus.jsonl’此文件如上述所说,是一个清洗过后的数据文件,‘./data_process.py’文件的做法就是创建一个列名为text的csv文件,将jsonl文件内的text存入到csv中。
预训练
先加载分词器和模型
model, tokenizer = init_model()
def init_model():
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
tokenizer = AutoTokenizer.from_pretrained('./model/minimind_tokenizer')
model = Transformer(lm_config).to(args.device)
# moe_path = '_moe' if lm_config.use_moe else ''
Logger(f'LLM总参数量:{count_parameters(model) / 1e6:.3f} 百万')
return model, tokenizer
加载数据
df = pd.read_csv(args.data_path)
#随机采样
df = df.sample(frac=1.0)
#自定义的数据格式
train_ds = PretrainDataset(df, tokenizer, max_length=max_seq_len)
#DistributedSampler 确保在分布式训练中每个进程只处理数据集的一部分,避免数据重复。
train_sampler = DistributedSampler(train_ds) if ddp else None
train_loader = DataLoader(
train_ds,
batch_size=args.batch_size,
pin_memory=True,
drop_last=False,
shuffle=False,
num_workers=args.num_workers,
sampler=train_sampler
)
#教师强制(Teacher Forcing)数据处理方式:
class PretrainDataset(Dataset):
def __getitem__(self, index: int):
#获取样本
sample = self.df.iloc[index]
#文本拼接开始结束标记
text = f"{self.tokenizer.bos_token}{str(sample['text'])}{self.tokenizer.eos_token}"
input_id = self.tokenizer(text).data['input_ids'][:self.max_length]
text_len = len(input_id)
# 没满最大长度的剩余部分
padding_len = self.max_length - text_len
input_id = input_id + [self.padding] * padding_len
# 0表示不计算损失
loss_mask = [1] * text_len + [0] * padding_len
#将输入序列(X)和目标序列(Y)错开一个时间步,以便模型可以学习预测下一个时间步的输出。
input_id = np.array(input_id)
X = np.array(input_id[:-1]).astype(np.int64)
Y = np.array(input_id[1:]).astype(np.int64)
loss_mask = np.array(loss_mask[1:]).astype(np.int64)
return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask)
开始预训练
Lora微调荔枝数据集
Lora微调模型:
#查找具有特定键(如 "wq", "wk")的 torch.nn.Linear 模块。这些模块将被选为 LoRA 适配器的目标。
def find_linear_with_keys(model, keys=["wq", "wk"]):
cls = torch.nn.Linear
linear_names = []
for name, module in model.named_modules():
if isinstance(module, cls):
for key in keys:
if key in name:
linear_names.append(name)
break
return linear_names
target_modules = find_linear_with_keys(model)
peft_config = LoraConfig(
r=8,
target_modules=target_modules
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
数据处理:
json格式处理为训练的格式
变为
微调
基础模型选用了官网上作者训练好的无历史对话模型
文本数据只有400多行,所以微调轮次设置为4,训练完成后保存权重如下:
这是lora微调直接保存下的权重参数,接着将微调后的权重文件与基础模型合并一下:
from safetensors.torch import load_file
import torch
# 加载适配器模型的权重
adapter_state_dict = load_file('adapter_model.safetensors')
# 加载基础模型的权重,这里以 rl_512.pth 为例
base_state_dict = torch.load('rl_512.pth', map_location='cuda')
# 假设适配器权重文件中的键与基础模型权重文件中的键相匹配
# 你可以直接更新基础模型的权重
base_state_dict.update(adapter_state_dict)
# 保存合并后的权重
torch.save(base_state_dict, 'merged_model.pth')
接着进行评估:
个人见解:这是一个很好的项目,但是个人想要依靠这个项目去做某个领域的对话模型还是没办法。应该需要预训练后表现就良好的模型作为基准模型才行。