文章目录
- 数据数量的影响
- 数据质量的影响
- 数据集污染
- 数据预处理实践
在训练大语言模型的过程中,预训练数据的质量对模型能力的影响至关重要。已有的研究表明,基于含有噪音、有毒和重复数据的低质量语料库进行预训练,会严重损害模型性能。
数据数量的影响
整体上,语言模型的性能会随着训练数据数量的增加而提升,符合扩展法则。然而,早期的研究工作(如 KM 扩展法则)认为增加模型参数更为重要,实际上 175B 参数的 GPT-3 模型只用了 500B 的词元进行了训练。随后,Chinchilla 扩展法则提出参数规模和数据规模应该同步增长,并且使用了1.4T 词元训练了具有 70B 参数的 Chinchilla 模型,数据量与参数量的比例大概为 20:1。相较于在 300B 词元上训练的 280B 参数的 Gopher 模型,Chinchilla模型展现出了更好的性能表现,这说明扩展训练数据数量对于提升大语言模型的性能非常关键。
在近期发布的大语言模型中,训练数据数量得到了高度关注,已经显著超越了 Chinchilla 扩展法则中给出的比例。例如,LLaMA-2 7B 参数的模型就在 2T 的词元数据上进行了预训练。一些更小尺寸的语言模型也使用了高达 1T 级别的数据进行了训练,发现其仍然没有达到语言模型能够学习的数据量上限。数据量的扩展性本质上来源于 Transformer 模型的可扩展性,这也是大语言模型能够取得成功最为关键的基础要素。
数据质量的影响
在获取充足数量的预训练数据后,数据质量直接决定了模型的实际性能。通过显著提升数据质量,使得语言模型在参数、数据、算力更加节约的情况下就能展现出与更大规模模型相匹敌甚至更为优异的性能。
为了探索高数据质量带来的收益,Phi-1 不仅精心筛选了已有的高质量数据,还采用 GPT-3.5 生成的方式,合成了一批质量称为“教科书级”的数据集作为补充。通过在这些高质量数据上进行训练,1.3 B 参数的 Phi-1 模型在 HumanEval 取得了 50.6% 的 pass@1 准确率。相反,使用大量低质量数据会导致模型训练过程不稳定,容易造成模型训练不收敛等问题。为了定量分析数据质量对于模型性能的影响,GLaM 模型对比了在原始数据和经过质量过滤的数据集上训练的模型性能,发现在各种自然语言处理任务上,在高质量数据上训练的模型都能取得更为出色的表现。此外,大语言模型所掌握的知识信息也来源于预训练数据,这意味着如果模型在包含事实性错误的、过时的数据上进行训练,那么它在处理相关主题时可能会产生不准确或虚假的信息,这种现象被称为“幻象”。例如,“灯泡是爱迪生发明的”是一个被大众广泛接受的误解,使用这种数据训练模型会使得生成误导性的输出。为了减少模型输出的错误信息,需要有效提升预训练数据的准确性和多样性,这对于提升模型的基础能力至关重要。
在现有的文献中,普遍认为重复数据对于模型训练及最终性能会带来不良影响。有研究表明,将语料中 0.1% 的数据重复 100 次后,基于这些包含重复数据语料训练的 800M 参数模型,其性能仅能达到在无重复语料上训练的 400M 参数模型的相同表现。进一步,重复数据也可能导致“双下降现象”,即模型训练损失先经历下降然后出现升高再下降的现象。此外,重复数据可 能会降低大语言模型利用上下文中信息的能力。这会削弱模型在上下文学习中的泛化能力,使其难以适应各种复杂的语言环境和任务需求。因此,通常的建议是对于预训练数据进行精细的去重操作。然而,随着模型参数规模的不断增加,公开可获取的数据将很快接近采集枯竭的状态,甚至在有些场景下无法进一步获得到充足的数据资源,如针对一些低频实体的文本数据较为有限。在这种情况下,可能需要对于部分高质量数据进行适度的重复训练,并注意关注由于引入重复数据可能带来的负面影响。为了减少可能存在的影响,也可以使用大语言模型对于稀缺数据进行改写或者针对性的生成。
数据是大语言模型掌握知识与建立能力的基础,而语言模型是对于训练数据语义的压缩。一旦数据中包含有偏、有毒、隐私的内容,将会对于模型造成严重的不良影响。在有偏内容上训练可能会导致语言模型学习并复制这些偏见,进而在其生成的文本中表现出对诸如种族、性别和年龄的偏好或歧视。进一步,如果训练数据中包含有毒内容,模型则可能会产生侮辱性、攻 击性或其他有害的输出;而在含有隐私内容的数据上训练可能会导致模型在输出中无意中泄露或利用个人数据。这些问题对于大语言模型的对齐带来了很大挑战。例如,通过精心设计的提示或利用模型的特定弱点,攻击者可能诱使模型输出不当或有害的信息。因此,在训练大语言模型之前,需要通过严格的数据过滤和预处理方法来尽量减少有偏见、有毒或包含隐私信息的数据。
数据集污染
为了有效评估模型性能,通常需要构建相应的评测基准,来衡量大语言模型在不同方面的能力。尽管可供使用的评测基准逐步增加,如何正确地选用这些基准并对于评测结果进行合适的解读,受到了研究人员的广泛关注。具体来说,在进行模型评测时,可能会发现某些评估基准所包含的数据,实际上已出现在预训练数据或者微调数据中,这种现象被称为基准泄漏或数据集污染。预训练数据通常在模型测试之前就需要完成准备,随着不断增长的预训练数据规模,数据集污染现象变得愈发普遍。数据集污染问题可能导致模型在与测试数据集相关甚至高度重合的语料上进行训练,从而原本用于衡量模型在少样本或零样本场景下的性能评测,转变为了领域内的测试任务。这种情况破坏了评估集合构建的初衷,使得不同模型之间的对比失去了公平性。例如,相关研究表明,在测试集合完全泄露的极端情况下,1.3B 的模型甚至在大部分任务超过了正常测评的 65B 的大语言模型。为此,下面给出一系列的参考建议,旨在改进和优化大语言模型的评估方式,从而加强评估结果的准确性和公正性。
1. 对于大语言模型的开发人员,我们建议在使用评估基准时,应该特别关注预训练数据与训练和测试集之间可能的数据重叠情况。 2. 对于基准测试的维护者,我们强烈建议对基准数据与现有预训练语料库之间的潜在污染进行分析,这有助于揭示潜在的污染风险。
数据预处理实践
YuLan-GARDEN是一个集成的预训练数据处理框架,用来支撑 YuLan 模型的预训练数据清洗与筛选。它包含了支持探测与评估数据的分析模块和包含不同粒度算子的数据处理模块,并且支持多进程并行处理大规模的预训练数据。用户可以首先通过分析模块初步了解数据的整体统计信息(如包含字段、平均长度、语言分布等),然后可以通过修改配置文件以自定义框架内预定义好的数据处理算子(如正则表达式过滤、文档级去重、个人信息去除等)的参数和顺序,以形成定制化的数据处理流程。用户可以通过多次迭代包括采样数据、配置清洗流水线、处理数据、评估数据处理质量的流程,直至满足对训练模型数据质量的需要。
在质量过滤阶段,YuLan-GARDEN 包含过滤和清洗两个主要流程。在过滤阶段,被判断为低质量的数据会被直接丢弃;而在清洗阶段,经过清洗后的高质量文本会替换原始文本。质量过滤阶段的实现可以依赖于启发式规则(如数据集统计特征、正则表达式匹配等)、预训练模型度量(如模型困惑度等)和语言标签判别(如语言分类器打分)等。用户还可以对数据进行采样,自由组合和安排预定义的算子灵活定制数据质量过滤流水线。下面以使用 FastText 的语言过滤模块为例来展示实现细节。首先,加载预训练好的 FastText 语言分类器,为每个输入文本生成一个语言标签,不符合配置文件中语言类别的文本将被过滤。
1 from utils.evaluator import LangIdentifier
2
3 class FilterPassageByLangs():
4 def __init__(self) -> None:
5 # 使用 LangIdentifier 模块加载已经训练好的 fasttext 模型
6 self.language_identifier = LangIdentifier(model_path="utils/models/fasttext/lid.176.bin")
7 self.reject_threshold = 0.5
8 def filter_single_text(self, text: str, accept_lang_list: list) -> bool:
9 # 使用 fasttext 模型给 text 打分,每种语言生成一个置信分数
10 labels, scores = self.language_identifier.evaluate_single_text(text)
11 # 如果 text 所有语言的分数均比 reject_threshold 要低,则直接定义为未知语言
12 if any(score < self.reject_threshold for score in scores):
13 labels = ["uk"]
14 accept_lang_list = [each.lower() for each in accept_lang_list]
15 # 如果分数最高的语言标签不在配置文件期望的语言列表中,则丢弃该文本
16 if labels[0] not in accept_lang_list:
17 return True
18 return False
在去重阶段,YuLan-GARDEN 集成了句子级和文档级去重方法,分别基于句子间 𝑛 元组的相似性与 MinHashLSH 算法实现。下面以句子级去重为例来展示实现细节。首先,对文本包含的所有句子(每行对应一个句子)计算 𝑛 元组,对于相邻的句子之间 𝑛 元组的 Jaccard 相似度超过设定阈值的都将会被过滤。
1 import string
2 import re
3 from nltk.util import ngrams
4
5 class CleanerDedupLineByNgram():
6 def __init__(self):
7 # 定义行分隔符和元组分隔符
8 self.line_delimiter = list("\n")
9 chinese_punctuation = ",。!?:;“”‘’()《》【】、|—"
10 self.gram_delimiter = list(string.punctuation) +
↩→ list(chinese_punctuation) + [' ']
11 def clean_single_text(self, text: str, n: int = 5, thre_sim: float =0.95) -> str:
12 # 依靠行分隔符分割所有行
13 lines = [each for each in re.split('|'.join(map(re.escape,
↩→ self.line_delimiter)), text) if each != '']
14 lineinfo, last = list(), {}
15 for idx, line in enumerate(lines): # 计算每行的 n 元组
16 # 依靠元组分隔符分割所有 N 元组,并将其暂时存储到 lineinfo 里
17 grams = [each for each in re.split('|'.join(map(re.escape,
↩→ self.gram_delimiter)), line) if each != '']
18 computed_ngrams = list(ngrams(grams, min(len(grams), n)))
19 lineinfo.append({
20 "lineno": idx, "text": line, "n": min(len(grams), n),
↩→ "ngrams": computed_ngrams, "keep": 0
21 })
22
23 for idx, each in enumerate(lineinfo): # 过滤掉和相邻行之间 n 元组的
↩→ Jaccard 相似度超过 thre_sim 的行
24 if last == {}:
25 each["keep"], last = 1, each
26 else:
27 # 计算相邻行间的 Jaccard 相似度
28 ngrams_last, ngrams_cur = set(last["ngrams"]),
↩→ set(each["ngrams"])
29 ngrams_intersection, ngrams_union =
len(ngrams_last.intersection(ngrams_cur)),
len(ngrams_last.union(ngrams_cur))
↩→
↩→
30 jaccard_sim = ngrams_intersection / ngrams_union if
↩→ ngrams_union != 0 else 0
31 if jaccard_sim < thre_sim:
32 each["keep"], last = 1, each
33 # 将所有未被过滤掉的 N 元组重新拼接起来
34 text = self.line_delimiter[0].join([each["text"] for each in
↩→ lineinfo if each["keep"] == 1])
35 return text
在隐私过滤阶段,YuLan-GARDEN 去除了个人身份信息,包括邮件名、身份证号、电话号码、网址与 IP 地址。我们以去除身份证号为例,对每个输入的文本,下面使用正则替换的方式将匹配到的身份证号替换为特定字符串。