MiniMind数据集代码详解(NLP初学者版)

cihebi 发布于 25 天前 75 次阅读 1970 字


# MiniMind数据集代码详解(NLP初学者版)

这段代码定义了训练语言模型的三种不同数据集类,分别对应NLP领域中常见的三种训练范式:预训练、监督微调和偏好优化。我将逐类详细讲解。

## 1. 导入和环境设置

```python
import json
import random
import re

import pandas as pd
import numpy as np
from torch.utils.data import Dataset, DataLoader
import torch
from sklearn.model_selection import train_test_split
import os
import ast

os.environ["TOKENIZERS_PARALLELISM"] = "false"
```

- 导入处理JSON文件的库,用于读取训练数据
- 导入PyTorch的Dataset类,这是自定义数据集的基础
- 设置环境变量关闭tokenizers并行处理,避免多线程加载数据时的问题

## 2. 预训练数据集 (PretrainDataset)

```python
class PretrainDataset(Dataset):
def __init__(self, data_path, tokenizer, max_length=512):
super().__init__()
self.tokenizer = tokenizer # 分词器,将文本转为token ID
self.max_length = max_length # 序列最大长度
self.samples = self.load_data(data_path) # 加载数据
```

初始化函数:接收数据路径、分词器和最大序列长度参数

```python
def load_data(self, path):
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
```

加载JSONL格式数据(每行一个JSON对象)

```python
def __len__(self):
return len(self.samples)
```

返回数据集长度

```python
def __getitem__(self, index):
sample = self.samples[index]

# 构建输入文本
text = f"{self.tokenizer.bos_token}{str(sample['text'])}{self.tokenizer.eos_token}"
encoding = self.tokenizer(
text,
max_length=self.max_length,
padding='max_length', # 填充到最大长度
truncation=True, # 截断过长文本
return_tensors='pt' # 返回PyTorch张量
)
input_ids = encoding.input_ids.squeeze()
loss_mask = (input_ids != self.tokenizer.pad_token_id) # 生成掩码:不计算填充位置的损失

X = torch.tensor(input_ids[:-1], dtype=torch.long) # 输入序列(最后一个token不作为输入)
Y = torch.tensor(input_ids[1:], dtype=torch.long) # 目标序列(第一个token不作为目标)
loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) # 损失掩码(与Y对齐)
return X, Y, loss_mask
```

这个方法实现了语言模型预训练的经典模式:

- 添加开始和结束标记
- 生成滑动窗口式的输入输出对(输入是前n-1个token,输出是后n-1个token)
- 创建损失掩码,忽略填充位置的损失贡献

## 3. 监督微调数据集 (SFTDataset)

```python
class SFTDataset(Dataset):
def __init__(self, jsonl_path, tokenizer, max_length=1024):
super().__init__()
self.tokenizer = tokenizer
self.max_length = max_length
self.samples = self.load_data(jsonl_path)
self.bos_id = tokenizer('assistant\n', add_special_tokens=False).input_ids # 助手回复开始标记
self.eos_id = tokenizer('
\n', add_special_tokens=False).input_ids # 对话结束标记
```

初始化函数:注意这里特别定义了助手回复的开始和结束标记ID

```python
def _create_chat_prompt(self, conversations):
"""构建符合ChatML格式的对话"""
messages = []
for i, turn in enumerate(conversations):
role = 'user' if i % 2 == 0 else 'assistant' # 交替的用户和助手角色
messages.append({"role": role, "content": turn['content']})
return self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False
)
```

这个函数将对话数据转换为ChatML格式:

- 根据索引自动判定角色(偶数为用户,奇数为助手)
- 使用tokenizer的chat_template将消息格式化为模型期望的格式

```python
def _generate_loss_mask(self, input_ids):
loss_mask = [0] * len(input_ids) # 初始化全0掩码
i = 0
while i < len(input_ids): # 寻找助手回复的开始标记 if input_ids[i:i + len(self.bos_id)] == self.bos_id: start = i + len(self.bos_id) end = start # 寻找助手回复的结束标记 while end < len(input_ids): if input_ids[end:end + len(self.eos_id)] == self.eos_id: break end += 1 # 标记助手回复部分为需要计算损失的区域(设为1) for j in range(start + 1, min(end + len(self.eos_id) + 1, self.max_length)): loss_mask[j] = 1 i = end + len(self.eos_id) if end < len(input_ids) else len(input_ids) else: i += 1 return loss_mask ``` 这是SFT中的核心函数,它创建了动态损失掩码: - **关键思想**:只为模型生成的部分(助手回复)计算损失,不为用户输入计算损失 - 遍历序列找到所有 `assistant\n` 到 `\n` 之间的文本
- 将这些区域在掩码中标记为1(参与损失计算)

```python
def __getitem__(self, index):
sample = self.samples[index]
# 构建对话提示
prompt = self._create_chat_prompt(sample['conversations'])
input_ids = self.tokenizer(prompt).input_ids[:self.max_length]
input_ids += [self.tokenizer.pad_token_id] * (self.max_length - len(input_ids)) # 手动填充

# 生成动态损失掩码
loss_mask = self._generate_loss_mask(input_ids)

# 构建训练数据(与预训练方式相同)
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
```

这里实现了SFT的核心数据处理逻辑:

- 将对话转换为模型可理解的格式
- 生成只关注于助手回复部分的损失掩码
- 构建滑动窗口式的输入输出对

## 4. 偏好优化数据集 (DPODataset)

```python
class DPODataset(Dataset):
def __init__(self, file_path, tokenizer, max_length=4096):
super().__init__()
self.tokenizer = tokenizer
self.max_length = max_length
self.padding = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0
self.bos_id = tokenizer('assistant\n', add_special_tokens=False).input_ids
self.eos_id = tokenizer('
\n', add_special_tokens=False).input_ids
with open(file_path, 'r', encoding='utf-8') as f:
self.data = []
for line in f:
line = line.strip()
obj = json.loads(line)
self.data.append(obj)
```

初始化函数:类似于SFT数据集,但直接加载数据而不是调用单独的方法

```python
def __getitem__(self, index):
item = self.data[index]
chosen = item['chosen'] # 偏好数据:被选中的回复(更好的回复)
rejected = item['rejected'] # 偏好数据:被拒绝的回复(较差的回复)

# 为选中和拒绝的回复分别创建提示
chosen_prompt = self.tokenizer.apply_chat_template(
chosen, tokenize=False, add_generation_prompt=False
)
rejected_prompt = self.tokenizer.apply_chat_template(
rejected, tokenize=False, add_generation_prompt=False
)

# 分词和填充
chosen_encoding = self.tokenizer(
chosen_prompt, truncation=True, max_length=self.max_length, padding='max_length'
)
rejected_encoding = self.tokenizer(
rejected_prompt, truncation=True, max_length=self.max_length, padding='max_length'
)

# 获取token IDs并生成损失掩码
chosen_input_ids = chosen_encoding['input_ids']
chosen_loss_mask = self._generate_loss_mask(chosen_input_ids)

rejected_input_ids = rejected_encoding['input_ids']
rejected_loss_mask = self._generate_loss_mask(rejected_input_ids)

# 创建输入输出对和掩码,为选中和拒绝的回复各创建一份
x_chosen = torch.tensor(chosen_input_ids[:-1], dtype=torch.long)
y_chosen = torch.tensor(chosen_input_ids[1:], dtype=torch.long)
mask_chosen = torch.tensor(chosen_loss_mask[1:], dtype=torch.long)
x_rejected = torch.tensor(rejected_input_ids[:-1], dtype=torch.long)
y_rejected = torch.tensor(rejected_input_ids[1:], dtype=torch.long)
mask_rejected = torch.tensor(rejected_loss_mask[1:], dtype=torch.long)

# 返回字典,包含两组数据
return {
'x_chosen': x_chosen,
'y_chosen': y_chosen,
'mask_chosen': mask_chosen,
'x_rejected': x_rejected,
'y_rejected': y_rejected,
'mask_rejected': mask_rejected
}
```

这个方法实现了DPO算法所需的数据准备:

- 同时处理"好的回复"和"差的回复"
- 为每组回复创建输入、目标和掩码
- 返回字典格式,包含两组完整的训练数据

## NLP初学者的重点解释

1. **三种训练范式的数据处理区别**:
- **预训练**:简单文本,预测下一个词
- **SFT**:对话格式,只对AI回复部分计算损失
- **DPO**:成对的好/差回复,用于学习偏好排序

2. **损失掩码的作用**:
- 控制哪些位置的预测会影响损失计算
- 在SFT/DPO中只对AI助手部分计算损失,不对用户输入计算损失
- 忽略填充位置的损失贡献

3. **滑动窗口训练**:
- `X = input_ids[:-1]` 和 `Y = input_ids[1:]` 创建错位的输入/输出对
- 模型看到前n-1个token,预测后n-1个token
- 这是标准的语言模型训练方式,本质是"下一个词预测"任务

4. **特殊标记处理**:
- 开始标记(`assistant`)和结束标记(``)用于识别助手回答区域
- 在DPO和SFT中,只对这些标记之间的内容计算损失

通过这三个数据集类,MiniMind系统实现了完整的语言模型训练范式,从预训练到微调再到偏好学习,逐步提升模型能力和对齐程度。

此作者没有提供个人介绍
最后更新于 2025-03-24