大语言模型的底层原理,ChatGPT,文心一言等人工智能体是如何产生的?本文将详细讲解


基础介绍

  • 大语言模型是指在海量无标注文本数据上进行预训练得到的大型预训练语言模型,例如 GPT-3。
  • 目前大语言模型所需要具有的最小参数规模还没有一个明确的参考标准,但是大语言模型通常是指参数规模达到百亿、千亿甚至万亿的模型;也有部分工作认为经过大规模数据预训练(显著多于传统预训练模型如 BERT 所需要的训练数据)的数十亿参数级别的模型也可 以称之为大语言模型(如 LLaMA-2 7B)。
  • 对于大语言模型,这里泛指具有超大规模参数或者经过超大规模数据训练所得到的语言模型。与传统语言模型相比,大语言模型的构建过程涉及到更为复杂的训练方法,进而展现出了强大的自然语言理解能力和复杂任务求解能力(通过文本生成的形式)。

从机器学习的观点来说,神经网络是一种具有特定模型结构的函数形式,而大语言模型则是一种基于 Transformer 结构的神经网络模型。

因此,可以将大语言模型看作一种拥有大规模参数的函数,它的构建过程就是使用训练数据对于模型参数的拟合过程。尽管所采用的训练方法与传统的机器学习模型(如多元线性回归模型的训练)可能存在不同,但是本质上都是在做模型参数的优化。大语言模型的优化目标更加泛化,不仅仅是为了解决某一种或者某一类特定任务,而是希望能够作为通用任务的求解器。

为了实现这一宏大的目标,大语言模型的构建过程需要更为复杂、精细的训练方法。一般来说,这个训练过程可以分为大规模预训练和指令微调与人类对齐两个阶段

一、预训练

预训练是研发大语言模型的第一个训练阶段,也是最为重要的一个阶段。

有效的预训练能够为大语言模型的能力奠定坚实的基础:通过在大规模语料上进行预训练,大语言模型可以获得通用的语言理解与生成能力,掌握较为广泛的世界知识,具备解决众多下游任务的性能潜力。在这一过程中,预训练语料的规模和质量对于提升大语言模型的能力至关重要。

1.数据准备

为了构建功能强大的大语言模型,需要从多元化的数据源中收集海量数据来进行训练。现有的大语言模型主要将各种公开的文本数据进行混合,作为预训练语料。

根据来源不同,预训练数据主要分为两种类型:通用文本数据和专用文本数据。

通用文本数据涵盖了网页、书籍和对话文本等。由于通用文本数据规模较大、多样性强且易于获取,大多数大语言模型都会收集大量的通用文本数据,以增强其语言建模能力。此外,为了进一步提升大语言模型在特定专业任务上的表现,人们还将预训练语料的范围扩展至更专业的数据集,如多语数据、科学数据和代码数据等。

预训练数据预处理流程图
在这里插入图片描述
当收集了丰富的文本数据之后,为了确保数据的质量和效用,还需要对数据进行预处理,从而消除低质量、冗余、无关甚可能有害的数据

质量过滤

直接收集到的文本数据往往掺杂了很多低质量的数据。例如,从网页抓取的数据中可能包含由机器自动生成的广告网页。为了优化模型学习的性能,需要去除语料库中的低质量数据。目前,研究人员主要使用以下两种数据清洗方法:(1)基于启发式规则的方法,和(2)基于分类器的方法。

敏感内容过滤

除了去除低质量内容,收集到的数据还可能包括有毒内容或隐私信息,需要进一步进行更为细致的过滤和处理。与质量过滤类似,不同类型的数据内容往往需要采用特定的过滤规则。

数据去重

对预训练数据进行去重处理是一个重要步骤。由于大语言模型具有较强的数据拟合与记忆能力,很容易习得训练数据中的重复模式,可能导致对于这些模式的过度学习。

研究工作发现,预训练语料中出现的重复低质量数据可能诱导模型在生成时频繁输出类似数据,进而影响模型的性能。此外,这些数据也可能导致训练过程的不稳定(训练损失震荡),可能导致训练过程崩溃。此外,为了避免数据集污染问题,还需要从预训练数据集中删除在测试集中可能出现的重复或者相关文本,从而防止训练集和测试集之间的重叠。总体来说,去重算法的设计可以基于不同的计算粒度以及匹配方法。

数据预处理实践

质量过滤

在过滤阶段,被判断为低质量的数据会被直接丢弃;而在清洗阶段,经过清洗后的高质量文本会替换原始文本。质量过滤阶段的实现可以依赖于启发式规则(如数据集统计特征、正则表达式匹配等)、预训练模型度量(如模型困惑度等)和语言标签判别(如语言分类器打分)等。用户还可以对数据进行采样,自由组合和安排预定义的算子灵活定制数据质量过滤流水线。

下面以使用 FastText 的语言过滤模块为例来展示实现细节。首先,加载预训练好的 FastText 语言分类器,为每个输入文本生成一个语言标签,不符合配置文件中语言类别的文本将被过滤。

from utils.evaluator import LangIdentifier

class FilterPassageByLangs():
    def __init__(self) -> None:
        # 使用 LangIdentifier 模块加载已经训练好的 fasttext 模型
        self.language_identifier = LangIdentifier(model_path="utils/models/fasttext/lid.176.bin")
        self.reject_threshold = 0.5
    def filter_single_text(self, text: str, accept_lang_list: list) -> bool:
        # 使用 fasttext 模型给 text 打分,每种语言生成一个置信分数
        labels, scores = self.language_identifier.evaluate_single_text(text)
        # 如果 text 所有语言的分数均比 reject_threshold 要低,则直接定义为未知
        if socres[0] < self.reject_thereshold:
            labels = ["uk"]
        accept_lang_list = [each.lower() for each in accept_lang_list]
        # 如果分数最高的语言标签不在配置文件期望的语言列表中,则丢弃该文本
        if labels[0] not in accept_lang_list:
            return True
        return False

去重

在去重阶段,YuLan-GARDEN 集成了句子级和文档级去重方法,分别基于句子间 𝑛 元组的相似性与 MinHashLSH 算法实现。下面以句子级去重为例来展示实现细节。首先,对文本包含的所有句子(每行对应一个句子)计算 𝑛 元组,对于相邻的句子之间 𝑛 元组的 Jaccard 相似度超过设定阈值的都将会被过滤。

import string
import re
from nltk.util import ngrams


class CleanerDedupLineByNgram():
    def __init__(self):
        # 定义行分隔符和元组分隔符
        self.line_delimiter = list("\n")
        chinese_punctuation = ",。!?:;“”‘’()《》【】、|—"
        self.gram_delimiter = list(string.punctuation) + list(chinese_punctuation) + [' ']

    def clean_single_text(self, text: str, n: int = 5, thre_sim: float = 0.95) -> str:
        # 依靠行分隔符分割所有行
        lines = [each for each in re.split('|'.join(map(re.escape, self.line_delimiter)), text) if each != '']
        lineinfo, last = list(), {}
        for idx, line in enumerate(lines):  # 计算每行的 n 元组
            # 依靠元组分隔符分割所有 N 元组,并将其暂时存储到 lineinfo 里
            grams = [each for each in re.split('|'.join(map(re.escape, self.gram_delimiter)), line) if each != '']
            computed_ngrams = list(ngrams(grams, min(len(grams), n)))
            lineinfo.append({"lineno": idx, "text": line, "n": min(len(grams), n), "ngrams": computed_ngrams, "keep": 0})
        # 过滤掉和相邻行之间 n 元组的Jaccard 相似度超过 thre_sim 的行
        for idx, each in enumerate(lineinfo):
            if last == {}:
                each["keep"], last = 1, each
            else:
                # 计算相邻行间的 Jaccard 相似度
                ngrams_last, ngrams_cur = set(last["ngrams"]), set(each["ngrams"])
                ngrams_intersection, ngrams_union = len(ngrams_last.intersection(ngrams_cur)), len(ngrams_last.union(ngrams_cur))
                jaccard_sim = ngrams_intersection / ngrams_union if ngrams_union != 0 else 0
                if jaccard_sim < thre_sim:
                     each["keep"], last = 1, each
        # 将所有未被过滤掉的 N 元组重新拼接起来
        text = self.line_delimiter[0].join([each["text"] for each in lineinfo if each["keep"] == 1])
        return text

隐私过滤

在隐私过滤阶段,YuLan-GARDEN 去除了个人身份信息,包括邮
件名、身份证号、电话号码、网址与 IP 地址。我们以去除身份证号为例,对每个输入的文本,下面使用正则替换的方式将匹配到的身份证号替换为特定字符串。

from utils.rules.regex import REGEX_IDCARD
from utils.cleaner.cleaner_base import CleanerBase

class CleanerSubstitutePassageIDCard(CleanerBase):
    def __init__(self):
        super().__init__()
    def clean_single_text(self, text: str, repl_text: str= "**MASKED**IDCARD**") -> str:
        # 使用正则表达式 REGEX_IDCARD 匹配身份证号,用 repl_text 代替
        return self._sub_re(text=text, re_text=REGEX_IDCARD, repl_text=repl_text)

2.词元化

词元化(Tokenization)是数据预处理中的一个关键步骤,旨在将原始文本分割成模型可识别和建模的词元序列,作为大语言模型的输入数据。传统自然语言处理研究(如基于条件随机场的序列标注)主要使用基于词汇的分词方法,这种方法更符合人类的语言认知。然而,基于词汇的分词在某些语言(如中文分词)中可能对于相同的输入产生不同的分词结果,导致生成包含海量低频词的庞大词表,还可能存在未登录词(Out-of-vocabulary, OOV)等问题。因此,一些语言模型开始采用字符作为最小单位来分词。

BPE 分词

在 1994 年,BPE 算法被提出,最早用于通用的数据压缩。随后,自然语言处理领域的研究人员将其进行适配,并应用于文本分词 。BPE 算法从一组基本符号(例如字母和边界字符)开始,迭代地寻找语料库中的两个相邻词元,并将它们替换为新的词元,这一过程被称为合并。合并的选择标准是计算两个连续词元的共现频率,也就是每次迭代中,最频繁出现的一对词元会被选择与合并。合并过程将一直持续达到预定义的词表大小。

BPE 算法的代码如下:

import re
from collections import defaultdict

def extract_frequencies(sequence):
    """
    给定一个字符串,计算字符串中的单词出现的频率,并返回词表(一个词到频率的映射字典)。
    """
    token_counter = Counter()
    for item in sequence:
        tokens = ' '.join(list(item)) + ' </w>'
        token_counter[tokens] += 1
    return token_counter

def frequency_of_pairs(frequencies):
    """
    给定一个词频字典,返回一个从字符对到频率的映射字典。
    """
    pairs_count = Counter()
    for token, count in frequencies.items():
        chars = token.split()
    for i in range(len(chars) - 1):
        pair = (chars[i], chars[i+1])
        pairs_count[pair] += count
    return pairs_count

def merge_vocab(merge_pair, vocab):
    """
    给定一对相邻词元和一个词频字典,将相邻词元合并为新的词元,并返回新的词表。
    """
    re_pattern = re.escape(' '.join(merge_pair))
    pattern = re.compile(r'(?<!\S)' + re_pattern + r'(?!\S)')
    updated_tokens = {pattern.sub(''.join(merge_pair), token): freq for ↩→ token, freq in vocab.items()}
    return updated_tokens

def encode_with_bpe(texts, iterations):
    """
    给定待分词的数据以及最大合并次数,返回合并后的词表。
    """
    vocab_map = extract_frequencies(texts)
    for _ in range(iterations):
        pair_freqs = frequency_of_pairs(vocab_map)
        if not pair_freqs:
            break
        most_common_pair = pair_freqs.most_common(1)[0][0]
        vocab_map = merge_vocab(most_common_pair, vocab_map)
    return vocab_map

num_merges = 1000
bpe_pairs = encode_with_bpe(data, num_merges)

WordPiece 分词

WordPiece 是谷歌内部非公开的分词算法,最初是由谷歌研究人员在开发语音搜索系统时提出的 。随后,在 2016 年被用于机器翻译系统 ,并于 2018年被 BERT 采用作为分词器 。WordPiece 分词和 BPE 分词的想法非常相似,都是通过迭代合并连续的词元,但是合并的选择标准略有不同。在合并前,WordPiece分词算法会首先训练一个语言模型,并用这个语言模型对所有可能的词元对进行评分。然后,在每次合并时,它都会选择使得训练数据的似然性增加最多的词元对。

Unigram 分词

与 BPE 分词和 WordPiece 分词不同,Unigram 分词方法 [142] 从语料库的一组足够大的字符串或词元初始集合开始,迭代地删除其中的词元,直到达到预期的词表大小。它假设从当前词表中删除某个词元,并计算训练语料的似然增加情况,以此来作为选择标准。这个步骤是基于一个训练好的一元语言模型来进行的。为估计一元语言模型,它采用期望最大化(Expectation–Maximization, EM)算法:在每次迭代中,首先基于旧的语言模型找到当前最优的分词方式,然后重新估计一元概率从而更新语言模型。这个过程中一般使用动态规划算法(即维特比算法,Viterbi Algorithm)来高效地找到语言模型对词汇的最优分词方式。

3.数据调度

完成数据预处理之后,需要设计合适的调度策略来安排这些多来源的数据,进而用于训练大语言模型。通常来说,数据调度(Data Scheduling)主要关注两个方面:各个数据源的混合比例以及各数据源用于训练的顺序(称为 数据课程,Data Curriculum)。

总结

相信你读到这里,对人工智能体应该有了一个大致的印象,也了解了大模型运行的基本原理和如何制作一个大模型。

但如果你觉得这些仍然不能满足你,那么你可以关注参考文献中的这几篇文章,相信你阅读后,会有不小的收获

参考文献

大模型资源网站:https://github.com/RUCAIBox/LLMSurvey/

  1. Tom B. Brown et al. “Language Models are Few-Shot Learners”. In: NeurIPS. 2020.
  2. Wayne Xin Zhao et al. “A Survey of Large Language Models”. In: arXiv preprint arXiv: 2303.18223 (2023).

最近更新

  1. 网格化监控:Eureka与分布式服务网格的协同监控

    2024-06-08 09:46:02       0 阅读
  2. Tomcat异步请求实现原理和应用场景简介

    2024-06-08 09:46:02       0 阅读
  3. [Python学习篇] Python面向对象——类

    2024-06-08 09:46:02       1 阅读
  4. 每日一道算法题 LCR 150. 彩灯装饰记录 II

    2024-06-08 09:46:02       1 阅读
  5. Ubuntu 添加so库搜索路径

    2024-06-08 09:46:02       1 阅读
  6. 文件格式是.pb应该怎么查看?

    2024-06-08 09:46:02       1 阅读
  7. 高考假期预习指南

    2024-06-08 09:46:02       1 阅读

热门阅读

  1. 基于vue3实现倒计时60s的

    2024-06-08 09:46:02       8 阅读
  2. PostgreSQL的视图pg_stat_user_indexes

    2024-06-08 09:46:02       9 阅读
  3. 软光敏的程序实现

    2024-06-08 09:46:02       9 阅读
  4. 电商API在实现后端系统集成中的关键作用

    2024-06-08 09:46:02       10 阅读
  5. PostgreSQL的视图pg_stat_user_tables

    2024-06-08 09:46:02       12 阅读
  6. 数据结构:顺序串

    2024-06-08 09:46:02       11 阅读
  7. 994. 腐烂的橘子

    2024-06-08 09:46:02       8 阅读