理论提供依据,而实际应用则能巩固理解。在本节中,我们将运用之前讨论的原则,将原始数据集转换为适合指令微调的结构化格式。我们将完整地逐步展示一个可重复的工作流程:加载原始数据、清洗数据、将其整理成一致的提示格式,并最终为模型进行分词。使用 databricks-dolly-15k 数据集的一个修改过的子集,该子集包含由人类生成的指令-响应对。1. 加载与初步检查我们的第一步是加载数据并对其内容有一个初步了解。我们将使用 Hugging Face 的 datasets 库,它是LLM生态系统中简化数据加载和处理的有力工具。假设我们的数据存储在 JSON Lines (.jsonl) 文件中,其中每行是一个 JSON 对象。from datasets import load_dataset # 从本地文件或 Hugging Face Hub 加载数据集 # 在本例中,我们假设它在 Hub 上可用。 raw_dataset = load_dataset("databricks/databricks-dolly-15k", split="train") # 让我们查看其结构和一些示例 print(raw_dataset) print(raw_dataset[0])输出将显示数据集的特征(列)和第一个示例的内容。通常,您会看到 instruction、context 和 response 等字段。Dataset({ features: ['instruction', 'context', 'response', 'category'], num_rows: 15011 }) { 'instruction': 'When did Virgin Australia start operating?', 'context': 'Virgin Australia, the trading name of Virgin Australia Airlines Pty Ltd, is an Australian-based airline. It is the largest airline by fleet size to use the Virgin brand. It commenced services on 31 August 2000 as Virgin Blue, with two aircraft on a single route.', 'response': 'Virgin Australia commenced services on 31 August 2000 as Virgin Blue, with two aircraft on a single route.', 'category': 'closed_qa' }我们的目标是将这些独立的字段转换为单一的、结构化的文本序列,供模型学习使用。2. 清洗与筛选实际数据很少是完美的。它通常包含空字段、重复项或不相关的信息。少量高质量数据远比大量嘈杂数据有价值。让我们进行一些基础清洗。一个常见问题是指令或响应缺失或过短而无用的条目。我们可以将其筛选掉。# 筛选掉指令过短的示例 initial_size = len(raw_dataset) filtered_dataset = raw_dataset.filter(lambda example: len(example['instruction']) > 3) # 筛选掉响应过短的示例 filtered_dataset = filtered_dataset.filter(lambda example: len(example['response']) > 3) print(f"Original size: {initial_size}") print(f"Size after filtering: {len(filtered_dataset)}")这是一个简单的筛选步骤。在生产环境中,您可能会添加更复杂的规则,例如删除包含特定关键词的示例,如果目标是英文文本则过滤掉非英文文本,或对相似指令进行去重。3. 整理成提示格式模型通过识别模式来学习遵循指令。因此,一致的提示结构对于有效的微调非常必要。模型需要清晰地分辨指令、任何提供的输入以及预期的响应。我们将采用一个简单而有效的模板来分离这些组成部分。### Instruction: {instruction} ### Input: {context} ### Response: {response}如果示例没有 context,我们将完全省略 ### Input: 部分,以避免向模型输入空字段。让我们创建一个Python函数来格式化每个示例。def format_prompt(example): """将单个示例格式化为标准化的提示字符串。""" instruction = f"### Instruction:\n{example['instruction']}" # 如果存在且非空,则使用 context if example.get('context') and example['context'].strip(): context = f"### Input:\n{example['context']}" else: context = "" response = f"### Response:\n{example['response']}" # 连接各部分,过滤掉空字符串 full_prompt = "\n\n".join(filter(None, [instruction, context, response])) return {"text": full_prompt}此函数会创建一个名为 text 的新列,其中包含完整格式化的提示。我们可以使用高效的 map 方法将此转换应用于整个数据集。# 将格式化函数应用于每个示例 structured_dataset = filtered_dataset.map(format_prompt) # 让我们查看一个带上下文的示例和一个不带上下文的示例 print("--- Example with context ---") print(structured_dataset[0]['text']) print("\n--- Example without context ---") # 找到一个不带上下文的示例并打印 for ex in structured_dataset: if "### Input:" not in ex['text']: print(ex['text']) break这个统一的文本字段正是模型在训练过程中将看到的内容。4. 分词数据准备的最后一步是分词:将格式化后的文本字符串转换为模型可以处理的整数ID序列。这一步还需要仔细处理特殊标记。我们需要选择与我们打算微调的基础模型匹配的分词器。在本例中,我们使用 meta-llama/Llama-2-7b-hf 的分词器。此外,在每个提示的末尾添加一个序列结束 (eos) 标记也很有必要。这个标记向模型发出信号,表明响应已完成,从而教会它在推理时何时停止生成文本。from transformers import AutoTokenizer # 为特定模型加载分词器 model_id = "meta-llama/Llama-2-7b-hf" # 注意:您可能需要通过 Hugging Face 认证才能访问此模型 # from huggingface_hub import login; login() tokenizer = AutoTokenizer.from_pretrained(model_id) # 如果尚未定义,则设置填充标记 if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token def tokenize_function(examples): # 将 EOS 标记附加到每个文本的末尾 text_with_eos = [s + tokenizer.eos_token for s in examples["text"]] # 对文本进行分词 return tokenizer( text_with_eos, truncation=True, # 截断超出最大长度的序列 max_length=512, # 一个常用最大长度 padding=False, # 我们稍后将使用数据收集器处理填充 ) # 应用分词 tokenized_dataset = structured_dataset.map( tokenize_function, batched=True, # 批量处理示例以提高效率 remove_columns=structured_dataset.column_names # 移除旧的文本列 ) # 检查分词的输出 print(tokenized_dataset[0].keys()) print(tokenized_dataset[0]['input_ids'][:20]) # 打印前20个标记ID输出现在包含 input_ids 和 attention_mask,这些是 Transformer 模型的标准输入。我们已成功将原始数据转换为可供模型使用的格式。以下图表概述了我们的数据准备流程。digraph G { rankdir=TB; splines=ortho; node [shape=box, style="rounded,filled", fontname="Arial", margin=0.2]; edge [fontname="Arial", fontsize=10]; rawData [label="原始数据\n(.jsonl)", fillcolor="#ffc9c9"]; loadData [label="使用 `datasets` 加载", shape=invhouse, fillcolor="#e9ecef"]; filterData [label="筛选与清洗\n(移除短条目)", shape=invhouse, fillcolor="#e9ecef"]; structureData [label="格式化提示\n(指令、输入、响应)", shape=invhouse, fillcolor="#e9ecef"]; tokenizeData [label="分词\n(添加EOS、转换为ID)", shape=invhouse, fillcolor="#e9ecef"]; finalDataset [label="处理后的数据集\n(input_ids, attention_mask)", fillcolor="#b2f2bb"]; rawData -> loadData; loadData -> filterData [label="数据集对象"]; filterData -> structureData [label="清洗后的数据"]; structureData -> tokenizeData [label="结构化文本"]; tokenizeData -> finalDataset [label="分词张量"]; }数据准备工作流程,从原始源文件到可用于训练的分词数据集。5. 保存处理后的数据集完成所有这些工作后,将最终数据集保存到磁盘是明智之举。这样我们就可以在后续训练时快速加载它,无需重复这些预处理步骤。# 将数据集保存到本地目录 tokenized_dataset.save_to_disk("./fine-tuning-dataset") # 您可以随时使用以下方式重新加载: # from datasets import load_from_disk # reloaded_dataset = load_from_disk("./fine-tuning-dataset")我们的数据集现已构建、清洗、整理、分词并保存完毕,我们已为下一阶段完全准备就绪:使用它来微调大型语言模型。