Chapter 59
5. 数据处理 Data proparation
在这门课程中,您将学习如何准备训练数据,为您的机器学习模型提供坚实的基础。我们将从数据收集开始,一步步地指导您完成数据预处理、token 化和模型训练的过程。让我们开始吧!
一. 准备训练数据的重要因素
1. 数据质量 数据质量是数据准备的首要关注点。在微调和训练过程中,高质量的数据能够显著提升模型效果。在准备数据时,请确保提供高质量、准确的输入。有句名言:Garbage in, Garbage out(垃圾进,垃圾出)
2. 数据多样性 数据多样性是另一个至关重要的因素。如果训练数据过于单一,模型可能会过度记忆并在相似情境中重复输出。为了避免这种情况,确保训练数据覆盖各种用例和场景。多样性的数据集有助于让模型更好地理解不同的输入,并做出更准确的预测。
3. 数据真实性 尽管生成数据方法在一些场景中可行,但在大多数情况下,真实数据对于模型的训练和微调非常重要。生成的数据往往具有固定的模式,这可能限制模型的创造力和适应能力。在大多数情况下,拥有真实数据更有效和有帮助,特别是对于写作任务等应用。生成的数据中存在一定的模式。一些服务试图检测内容是否是由生成模型生成的,就是通过寻找生成数据中的模式和规律。
4. 数据数量 数据的数量对于训练模型的确非常重要。更多的数据通常可以帮助模型更好地泛化和适应不同的情境。预训练模型的引入在一定程度上缓解了数据数量的问题,因为它已经通过在互联网上的大量数据上进行预训练,建立了一定的基础理解。因此拥有更多的数据对模型有帮助,但不如前三名那么重要,而且绝对没有质量那么重要。
综上所述,准备训练数据时需要考虑数据质量、多样性、真实性和数量。这些因素将共同影响模型的性能和输出质量。接下来,让我们深入了解如何有效地准备训练数据。
二. 数据处理步骤
1. 收集指令-响应对 从不同来源收集问答对或指令-响应对。这可以是来自用户的对话记录、已有的问答数据集等。
2. 合并指令对(需要的话,添加 Prompt 模版) 将收集到的指令和响应对进行合并,形成一个整体的数据集。在这一步,可以根据需要为每个指令或响应添加一些 Prompt 模版,以帮助模型更好地理解上下文。
3. token 化 将文本数据转换为数字表示。这一步通过使用分词器(tokenizer)来完成,分词器会将文本划分为词或子词,并为每个词或子词分配一个唯一的标识符,即 token。这样可以将文本转换为机器可以理解的形式,并为后续处理做好准备。在 token 化过程中,还需要进行填充(padding)或截断(truncation)操作,以确保所有文本长度相同,方便模型进行处理。
4. 数据集划分 将处理好的数据集划分为训练集和测试集。训练集用于训练模型的参数,而测试集用于评估模型的性能和泛化能力。
ing 是非常常见的一种字符。大部分动名词都存在这个字符。如 finetuning , tokenizing 中都有 ing。这个例子中,ing 被编码 为 278。
当您使用相同的分词器对 token 进行解码时,它会被恢复成原始的文本。
每个模型都与特定的分词器相关联,并以此进行训练。如果选择错误的分词器,会导致模型认为不同的数字代表不同的字母集和单词,从而导致混乱和错误的结果。因此,使用正确的分词器至关重要。

import pandas as pd
import datasets
from pprint import pprint
from transformers import AutoTokenizerHuggingFace Transformers 库是一个非常强大和受欢迎的自然语言处理工具库。您只需指定所需的模型和名称,它将自动帮助您找到适当的分词器,并与模型进行匹配
2.1 文本 Token 化
这里使用 70m pythia 模型对应的分词器
tokenizer = AutoTokenizer.from_pretrained("EleutherAI/pythia-70m")2.1.1 token 化一个文本
text = "Hi, how are you?"encoded_text = tokenizer(text)["input_ids"]
print(encoded_text)Output
[12764, 13, 849, 403, 368, 32]
text = "嗨你好么"encoded_text = tokenizer(text)["input_ids"]
print(encoded_text)Output
[161, 234, 103, 24553, 34439, 43244]
分词器将这串文本编码成了不同的数字。
分词器输出一个字典,其中包含表示 token 的input_ids
现在将其解码回文本
decoded_text = tokenizer.decode(encoded_text)
print("Decoded tokens back into text: ", decoded_text)Output
Decoded tokens back into text: Hi, how are you?
decoded_text = tokenizer.decode(encoded_text)
print("将 token 解码为文本: ", decoded_text)Output
将 token 解码为文本: 嗨你好么
可以看出解码出的文本和最初的一致
2.1.2 一次 token 化多个文本
有时候我们需要可以一次性对多个文本进行 token 化。
当处理批量输入时,可以将文本列表连接起来作为输入。
list_texts = ["Hi, how are you?", "I'm good", "Yes"]
encoded_texts = tokenizer(list_texts)
print("Encoded several texts: ", encoded_texts["input_ids"])Output
Encoded several texts: [[12764, 13, 849, 403, 368, 32], [42, 1353, 1175], [4374]]
list_texts = ["嗨你好么", "我很好", "是"]
encoded_texts = tokenizer(list_texts)
print("编码多个文本: ", encoded_texts["input_ids"])Output
编码多个文本: [[161, 234, 103, 24553, 34439, 43244], [15367, 45091, 34439], [12105]]
可以看到,分词器对不同长度的文本返回的长度也不一样。
2.1.3 填充和截断
模型需要处理固定大小的张量,因此在批次中所有内容的长度必须相同。 填充是处理这些可变长度编码文本的策略。
填充时需要选择一个特定的数字作为填充 token 来代表填充。通常使用 0 来填充,这也是句子的结束token。
因此,当我们通过分词器运行 padding = true 时,您可以看到 是(Yes) 字符串右侧多了很多 0 以确保和 嗨你好么(Hi, how are you?) 的长度一致。
tokenizer.pad_token = tokenizer.eos_token
encoded_texts_longest = tokenizer(list_texts, padding=True)
print("Using padding: ", encoded_texts_longest["input_ids"])Output
Using padding: [[12764, 13, 849, 403, 368, 32], [42, 1353, 1175, 0, 0, 0], [4374, 0, 0, 0, 0, 0]]
tokenizer.pad_token = tokenizer.eos_token
encoded_texts_longest = tokenizer(list_texts, padding=True)
print("使用填充后的结果: ", encoded_texts_longest["input_ids"])Output
使用填充后的结果: [[161, 234, 103, 24553, 34439, 43244], [15367, 45091, 34439, 0, 0, 0], [12105, 0, 0, 0, 0, 0]]
模型有着最大长度的限制,即模型可以处理和容纳的文本长度。因此,它并不适用于处理任意长度的文本数据。
之前您使用 Prompt 时可能已经注意到,存在着长度限制。这就是模型的截断策略,用于将编码文本截短以适应实际可接受的模型输入。
通过截断,我们可以将过长的文本修剪为适合模型处理的长度。这有助于缩短处理时间,因为长文本可能需要更长的处理时间。
这里将最大长度设置为 3,设置为截断(truncation = True)。可以发现嗨你好么(Hi, how are you?)短了很多,去掉了右边的所有内容。
encoded_texts_truncation = tokenizer(list_texts, max_length=3, truncation=True)
print("Using truncation: ", encoded_texts_truncation["input_ids"])Output
Using truncation: [[12764, 13, 849], [42, 1353, 1175], [4374]]
encoded_texts_truncation = tokenizer(list_texts, max_length=3, truncation=True)
print("使用截断: ", encoded_texts_truncation["input_ids"])Output
使用截断: [[24553, 34439, 43244], [15367, 45091, 34439], [12105]]
在实际应用中,比如您正在撰写一篇文章,可能在某个位置给出了一些 prompt,同时还有许多重要的内容需要考虑。保留右侧可能更重要,这保存了前文的重要信息,以保持上下文的连贯性。这时,将截断的一侧标定为左侧可能会更加合适。所以,最终的决定取决于您正在解决的具体问题和所需的结果。
tokenizer.truncation_side = "left"
encoded_texts_truncation_left = tokenizer(list_texts, max_length=3, truncation=True)
print("Using left-side truncation: ", encoded_texts_truncation_left["input_ids"])Output
Using left-side truncation: [[403, 368, 32], [42, 1353, 1175], [4374]]
tokenizer.truncation_side = "left"
encoded_texts_truncation_left = tokenizer(list_texts, max_length=3, truncation=True)
print("使用左侧截断: ", encoded_texts_truncation_left["input_ids"])Output
使用左侧截断: [[24553, 34439, 43244], [15367, 45091, 34439], [12105]]
实际上,我们在处理输入时常常同时使用填充和截断这两种方法。我们设置截断和填充的参数都为 True。可以看到填充为 0 的内容被截断为三个。
encoded_texts_both = tokenizer(list_texts, max_length=3, truncation=True, padding=True)
print("Using both padding and truncation: ", encoded_texts_both["input_ids"])Output
Using both padding and truncation: [[403, 368, 32], [42, 1353, 1175], [4374, 0, 0]]
encoded_texts_both = tokenizer(list_texts, max_length=3, truncation=True, padding=True)
print("同时使用填充和截断: ", encoded_texts_both["input_ids"])Output
同时使用填充和截断: [[24553, 34439, 43244], [15367, 45091, 34439], [12105, 0, 0]]
2.1.4 准备指令数据集
下面是上一个实验中的一些代码。
加载带有 "question" 和 "answer" 的数据集文件,将其放入 Prompt 中进行处理。
现在您可以在此处看到一个包含 "question" 和 "answer" 的数据。
我们在其中一个数据上运行这个 token 生成器。
import pandas as pd
filename = "lamini_docs.jsonl"
instruction_dataset_df = pd.read_json(filename, lines=True)
examples = instruction_dataset_df.to_dict()
if "question" in examples and "answer" in examples:
text = examples["question"][0] + examples["answer"][0]
elif "instruction" in examples and "response" in examples:
text = examples["instruction"][0] + examples["response"][0]
elif "input" in examples and "output" in examples:
text = examples["input"][0] + examples["output"][0]
else:
text = examples["text"][0]
prompt_template = """### Question:
{question}
### Answer:"""
num_examples = len(examples["question"])
finetuning_dataset = []
for i in range(num_examples):
question = examples["question"][i]
answer = examples["answer"][i]
text_with_prompt_template = prompt_template.format(question=question)
finetuning_dataset.append({"question": text_with_prompt_template, "answer": answer})
from pprint import pprint
print("One datapoint in the finetuning dataset:") #微调数据集中的一个数据点
pprint(finetuning_dataset[0])Output
One datapoint in the finetuning dataset:
{'answer': 'Lamini has documentation on Getting Started, Authentication, '
'Question Answer Model, Python Library, Batching, Error Handling, '
'Advanced topics, and class documentation on LLM Engine available '
'at https://lamini-ai.github.io/.',
'question': '### Question:\n'
'What are the different types of documents available in the '
'repository (e.g., installation guide, API documentation, '
"developer's guide)?\n"
'\n'
'### Answer:'}
2.1.5 Token 化一个样本
首先将该问题与该答案连接起来,然后通过分词器进行 token 化。
为了简单起见,这里只是将张量作为 NumPy 数组返回,并且进行填充操作。
text = finetuning_dataset[0]["question"] + finetuning_dataset[0]["answer"]
tokenized_inputs = tokenizer(
text,
return_tensors="np",
padding=True
)
print(tokenized_inputs["input_ids"])Output
[[ 4118 19782 27 187 1276 403 253 1027 3510 273 7177 2130
275 253 18491 313 70 15 72 904 12692 7102 13 8990
10097 13 13722 434 7102 6177 187 187 4118 37741 27 45
4988 74 556 10097 327 27669 11075 264 13 5271 23058 13
19782 37741 10031 13 13814 11397 13 378 16464 13 11759 10535
1981 13 21798 12989 13 285 966 10097 327 21708 46 10797
2130 387 5987 1358 77 4988 74 14 2284 15 7280 15
900 14206]]
因为不确定这些 token 的实际长度。
所以,将配置的最大长度设置为最大长度和 token 长度的最小值。
当然,您总是可以将填充长度设置为最大长度。
max_length = 2048
max_length = min(
tokenized_inputs["input_ids"].shape[1],
max_length,
)然后,再次对其进行 token 化,并将其截断为最大长度。
tokenized_inputs = tokenizer(
text,
return_tensors="np",
truncation=True,
max_length=max_length
)print(tokenized_inputs["input_ids"])Output
[[ 4118 19782 27 187 1276 403 253 1027 3510 273 7177 2130
275 253 18491 313 70 15 72 904 12692 7102 13 8990
10097 13 13722 434 7102 6177 187 187 4118 37741 27 45
4988 74 556 10097 327 27669 11075 264 13 5271 23058 13
19782 37741 10031 13 13814 11397 13 378 16464 13 11759 10535
1981 13 21798 12989 13 285 966 10097 327 21708 46 10797
2130 387 5987 1358 77 4988 74 14 2284 15 7280 15
900 14206]]
2.1.6 Token 化指令数据集
将上述过程包装成一个函数,便于在整个数据集上运行它。
def tokenize_function(examples):
"""
对输入进行 token 化,并进行填充和截断处理,返回经过处理后的 token 作为结果
Args:
examples (dict): 待 token 化的数据,可以是包含"question"和"answer"键的字典,或包含"input"和"output"键的字典,或包含"text"键的字典
Returns:
dict: 经过处理后的输入数据的 token,包含经过 token 化并进行填充和截断处理后的输入数据
"""
# 合并指令对
if "question" in examples and "answer" in examples:
text = examples["question"][0] + examples["answer"][0]
elif "input" in examples and "output" in examples:
text = examples["input"][0] + examples["output"][0]
else:
text = examples["text"][0]
# token 化
tokenizer.pad_token = tokenizer.eos_token
tokenized_inputs = tokenizer(
text,
return_tensors="np",
padding=True,
)
max_length = min(
tokenized_inputs["input_ids"].shape[1],
2048
)
tokenizer.truncation_side = "left"
tokenized_inputs = tokenizer(
text,
return_tensors="np",
truncation=True,
max_length=max_length
)
return tokenized_inputs现在我们加载该数据集。
我们使用 map 方法将 token 化函数映射到该数据集。
我们将 batch_size 设置为1,这样可以进行分批处理。
将 drop_last_batch 设置为 True,以处理混合大小的输入,因为当数据长度不是 batch_size 的倍数时,最后一个 batch 的 size 会与 batch_size 不同。
finetuning_dataset_loaded = datasets.load_dataset("json", data_files=filename, split="train")
tokenized_dataset = finetuning_dataset_loaded.map(
tokenize_function,
batched=True,
batch_size=1,
drop_last_batch=True
)
print(tokenized_dataset)Output
Map: 0%| | 0/1400 [00:00<?, ? examples/s]
Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask'],
num_rows: 1400
})
添加标签列,以便模型进行学习
tokenized_dataset = tokenized_dataset.add_column("labels", tokenized_dataset["input_ids"])2.2 测试/训练数据集的切分
运行这个训练测试分割函数,将指定测试大小为数据的10%。
当然,您可以根据数据集的大小来更改此设置。
shuffle=True 为了随机化这个数据集的顺序。
现在可以看到数据集已分为训练集和测试集。
split_dataset = tokenized_dataset.train_test_split(test_size=0.1, shuffle=True, seed=123)
print(split_dataset)Output
DatasetDict({
train: Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask', 'labels'],
num_rows: 1260
})
test: Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask', 'labels'],
num_rows: 140
})
})
2.2.1 一些数据集供您尝试
我们使用的数据可以直接通过 Hugging Face 加载。
该数据集是关于一家公司的专业数据集,也许这与您的公司相似。
您可以根据需要对其进行调整。
finetuning_dataset_path = "lamini/lamini_docs"
finetuning_dataset = datasets.load_dataset(finetuning_dataset_path)
print(finetuning_dataset)Output
DatasetDict({
train: Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask', 'labels'],
num_rows: 1260
})
test: Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask', 'labels'],
num_rows: 140
})
})
如果您觉得这个数据集有些无聊。
我们提供了一些更有趣的数据集供您选择。
taylor_swift的数据集,- 流行乐队
BTS的数据集。 - 实际的开源大型语言模型数据集。
taylor_swift_dataset = "lamini/taylor_swift"
bts_dataset = "lamini/bts"
open_llms = "lamini/open_llms"现在让我们来看看taylor_swift数据集中的一条数据,好的。
这些数据集同样可以通过 Hugging Face 获得。
dataset_swiftie = datasets.load_dataset(taylor_swift_dataset)
print(dataset_swiftie["train"][1])Output
{'question': 'What is the most popular Taylor Swift song among millennials? How does this song relate to the millennial generation? What is the significance of this song in the millennial culture?', 'answer': 'Taylor Swift\'s "Shake It Off" is the most popular song among millennials. This song relates to the millennial generation as it is an anthem of self-acceptance and embracing one\'s individuality. The song\'s message of not letting others bring you down and to just dance it off resonates with the millennial culture, which is often characterized by a strong sense of individuality and a rejection of societal norms. Additionally, the song\'s upbeat and catchy melody makes it a perfect fit for the millennial generation, which is known for its love of pop music.', 'input_ids': [1276, 310, 253, 954, 4633, 11276, 24619, 4498, 2190, 24933, 8075, 32, 1359, 1057, 436, 4498, 14588, 281, 253, 24933, 451, 5978, 32, 1737, 310, 253, 8453, 273, 436, 4498, 275, 253, 24933, 451, 4466, 32, 37979, 24619, 434, 346, 2809, 640, 733, 5566, 3, 310, 253, 954, 4633, 4498, 2190, 24933, 8075, 15, 831, 4498, 7033, 281, 253, 24933, 451, 5978, 347, 352, 310, 271, 49689, 273, 1881, 14, 14764, 593, 285, 41859, 581, 434, 2060, 414, 15, 380, 4498, 434, 3935, 273, 417, 13872, 2571, 3324, 368, 1066, 285, 281, 816, 11012, 352, 745, 8146, 684, 342, 253, 24933, 451, 4466, 13, 534, 310, 2223, 7943, 407, 247, 2266, 3282, 273, 2060, 414, 285, 247, 18235, 273, 38058, 22429, 15, 9157, 13, 253, 4498, 434, 598, 19505, 285, 5834, 90, 40641, 2789, 352, 247, 3962, 4944, 323, 253, 24933, 451, 5978, 13, 534, 310, 1929, 323, 697, 2389, 273, 1684, 3440, 15], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'labels': [1276, 310, 253, 954, 4633, 11276, 24619, 4498, 2190, 24933, 8075, 32, 1359, 1057, 436, 4498, 14588, 281, 253, 24933, 451, 5978, 32, 1737, 310, 253, 8453, 273, 436, 4498, 275, 253, 24933, 451, 4466, 32, 37979, 24619, 434, 346, 2809, 640, 733, 5566, 3, 310, 253, 954, 4633, 4498, 2190, 24933, 8075, 15, 831, 4498, 7033, 281, 253, 24933, 451, 5978, 347, 352, 310, 271, 49689, 273, 1881, 14, 14764, 593, 285, 41859, 581, 434, 2060, 414, 15, 380, 4498, 434, 3935, 273, 417, 13872, 2571, 3324, 368, 1066, 285, 281, 816, 11012, 352, 745, 8146, 684, 342, 253, 24933, 451, 4466, 13, 534, 310, 2223, 7943, 407, 247, 2266, 3282, 273, 2060, 414, 285, 247, 18235, 273, 38058, 22429, 15, 9157, 13, 253, 4498, 434, 598, 19505, 285, 5834, 90, 40641, 2789, 352, 247, 3962, 4944, 323, 253, 24933, 451, 5978, 13, 534, 310, 1929, 323, 697, 2389, 273, 1684, 3440, 15]}
# 以下是将自己的数据集推送到 Huggingface hub 的方法
# !pip install huggingface_hub
# !huggingface-cli login
# split_dataset.push_to_hub(dataset_path_hf)我们已经准备好了所有的数据,并进行了 token 化。在接下来的实验中,我们将使用这些数据来训练我们的模型。
