当调用搭建好的MiniMindForCausalLM类实例化一个模型之后,模型的参数是随机的,这个阶段的模型没有任何语言能力,无法进行有意义的文本生成或理解。
预训练 使用大规模的无监督语料对模型进行训练,使其具备“理解和生成自然语言”的基础能力,为后续的微调 提供一个好的起点。
一、查看预训练数据集格式 MiniMind预训练使用的数据集为pretrain_hq.jsonl,这是一个1.55GB的文件,里面包含了非常多条数据,这里查看其中的第一条数据作为示例:
1 2 3 4 5 6 7 8 9 import jsonpretrain_dataset_path=r'D:\MyFile\github\minimind-master\minimind_dataset\pretrain_hq.jsonl' with open (pretrain_dataset_path, 'r' , encoding='utf-8' ) as f: for line_num, line in enumerate (f, 1 ): data = json.loads(line.strip()) break print (data.keys()) print (data)
1 {'text': '<|im_start|>鉴别一组中文文章的风格和特点,例如官方、口语、文言等。需要提供样例文章才能准确鉴别不同的风格和特点。<|im_end|> <|im_start|>好的,现在帮我查一下今天的天气怎么样?今天的天气依据地区而异。请问你需要我帮你查询哪个地区的天气呢?<|im_end|> <|im_start|>打开闹钟功能,定一个明天早上七点的闹钟。好的,我已经帮您打开闹钟功能,闹钟将在明天早上七点准时响起。<|im_end|> <|im_start|>为以下场景写一句话描述:一个孤独的老人坐在公园长椅上看着远处。一位孤独的老人坐在公园长椅上凝视远方。<|im_end|> <|im_start|>非常感谢你的回答。请告诉我,这些数据是关于什么主题的?这些数据是关于不同年龄段的男女人口比例分布的。<|im_end|> <|im_start|>帮我想一个有趣的标题。这个挺有趣的:"如何成为一名成功的魔术师" 调皮的标题往往会吸引读者的注意力。<|im_end|> <|im_start|>回答一个问题,地球的半径是多少?地球的平均半径约为6371公里,这是地球自赤道到两极的距离的平均值。<|im_end|> <|im_start|>识别文本中的语气,并将其分类为喜悦、悲伤、惊异等。\n文本:“今天是我的生日!”这个文本的语气是喜悦。<|im_end|>'}
可以看到,每一条数据都是一个字典格式,只包含一个键值对,key是固定的’text’,value是用于预训练的“一段文本”,这是一个以<|im_start|>和<|im_end|>为对话边界token的多轮指令-回答对话数据集片段。
二、准备预训练数据加载器 构建符合PyTorch的Dataloader的Dataset类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import jsonimport torchfrom torch.utils.data import Datasetclass PretrainDataset (Dataset ): def __init__ (self, data_path, tokenizer, max_length=512 ): super ().__init__() self .tokenizer = tokenizer self .max_length = max_length self .samples = self .load_data(data_path) def load_data (self, path ): """从文件中加载数据,每一行为一条JSON格式的样本""" samples = [] with open (path, 'r' , encoding='utf-8' ) as f: for line_num, line in enumerate (f, 1 ): data = json.loads(line.strip()) samples.append(data) return samples def __len__ (self ): """返回样本数量""" return len (self .samples) def __getitem__ (self, index ): """ 返回第 index 个样本: - X: 模型输入(input_ids[:-1]) - Y: 目标输出(input_ids[1:]) - loss_mask: 哪些token位置参与loss计算(去除padding部分) """ sample = self .samples[index] encoding = self .tokenizer( str (sample['text' ]), max_length=self .max_length, padding='max_length' , truncation=True , return_tensors='pt' ) input_ids = encoding.input_ids.squeeze() loss_mask = (input_ids != self .tokenizer.pad_token_id) X = torch.tensor(input_ids[:-1 ], dtype=torch.long) Y = torch.tensor(input_ids[1 :], dtype=torch.long) loss_mask = torch.tensor(loss_mask[1 :], dtype=torch.long) return X, Y, loss_mask
构建数据加载器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from torch.utils.data import DataLoaderfrom transformers import AutoTokenizermax_length=512 data_path=r'D:\MyFile\github\minimind-master\minimind_dataset\pretrain_hq.jsonl' tokenizer = AutoTokenizer.from_pretrained(r'D:\MyFile\github\minimind-master\model' ) train_ds = PretrainDataset(data_path, tokenizer, max_length) train_loader = DataLoader( train_ds, batch_size=2 , pin_memory=True , drop_last=False , shuffle=False , num_workers=0 , )
查看数据总量以及数据的维度信息:
1 2 3 4 print (len (train_loader)) for item in train_loader: print ([i.shape for i in item]) break
通过打印看到,数据总量为706552,每一条数据都包含3个PyTorch Tensor,分别是X, Y以及Y对应的padding mask(用于掩掉padding token的loss),shape都是2x511,2是batch_size,511是max_length-1,因为X和Y是正好是偏移一位的。
三、开始预训练 预训练代码和常规的模型训练代码几乎没有区别,核心代码段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 loss_fct = nn.CrossEntropyLoss(reduction='none' ) ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() for step, (X, Y, loss_mask) in enumerate (train_loader): X = X.to(args.device) Y = Y.to(args.device) loss_mask = loss_mask.to(args.device) lr = get_lr( epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate ) for param_group in optimizer.param_groups: param_group['lr' ] = lr with ctx: res = model(X) loss = loss_fct( res.logits.view(-1 , res.logits.size(-1 )), Y.view(-1 ) ).view(Y.size()) loss = (loss * loss_mask).sum () / loss_mask.sum () loss += res.aux_loss loss = loss / args.accumulation_steps scaler.scale(loss).backward() if (step + 1 ) % args.accumulation_steps == 0 : scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True )