从0到1实现一个自己的大模型,实践中了解模型流程细节

前言

最近看了很多大模型,也使用了很多大模型。对于大模型理论似乎很了解,但是好像又缺点什么,思来想去决定自己动手实现一个 toy 级别的模型,在实践中加深对大语言模型的理解。

在这个系列的文章中,我将通过亲手实践,构建一个 1.2B 的模型,完成模型搭建、tokenizer 训练、模型预训练和指令微调这些流程。记录整个开发过程和其中遇到的各种挑战和对应解决方案。

最后里面所有的内容都是我对于大模型的理解形成的,如果您发现有任何过时或不准确的地方,请不吝指出。

分词算法

由于模型只能处理数字,因此分词算法的目的就是将输入文本转换为数字,这样模型才能接受文本消息。分词算法就是将文本转换成数字的方法。

Word-based

最直觉的方法是按照单词划分,给予每个单词不同的标号。例如我们可以通过空格和标点符号对一个文本进行划分:

text tokenized
Let’s do tokenization! Let 's do tokenization !

这样简单的分词算法的坏处是它会形成一个非常大的词表,例如 dogdogs 就会变成两个独立的token,一个单词的不同形态都会变成独立token。更糟的是由于同义词会变成独立的token,那么模型开始并不会以为 dogdogs 是有关联的。

通常我们会在分词器中定义一个特殊token表示词表中不存在的词,如果基于词划分,如果出现两个词组成的新词,它会被视为不存在。如果分词结果包含大量不存在,这样的分词算法无疑是不好的。

Character-based

基于字符的分词算法将文本拆分成字符而不是单词。这样做有两个明显的好处:

  • 词表会小很多,只需要基础字符和标点即可。
  • 未知标记很少,因为单词都会从字符构建。

但是这样做也有明显的坏处:

  • 中文单个字有丰富语义,但是英文单个字符没有丰富的语义。
  • 相同文本会被转换为更长的token序列,会严重增加模型推理的开销。

Subword

结合上述两种分词算法的优缺点,有了一种基于字词的分词算法。它的一个思想是常用的词不应该被拆分成较小的词,罕见的词应该拆分成有意义的词。 例如 annoyingly 可以拆分为 annoyingly ,前者是常用词,后者是高频的词缀,两个词都具有一定的语义。这样可以使用一个较小的词表表示尽可能多的词,且尽可能少的出现未知。

算法 BPE WordPiece Unigram
训练原理 从一个较小的词表开始,学习 token 合并的规则 从一个小的词表开始,学习 token 合并规则 从一个大词表开始,逐步去掉 token
训练步骤 合并出现频率最高的 token 对 根据词频计算分数,合并分数最高的 token 对,会优先选择单独出现频率低但是成对出现频率高的 token 对 基于整个语料计算损失,优先删除造成损失最小的 token
学习结果 token 合并规则和词表 仅仅一个词表 一个词表同时记录每个 token 的分数
编码方式 将词分成字符,然后根据合并规则合并 从词的开始找到词表中的最长匹配,然后在此分割,剩余部分继续按照相同方式分词 根据 token 分数找到最可能的分词方式

每种算法详细的原理在这里不赘述了,下面介绍分词器的工作流程。

分词流程

当我们有一个文本,对它进行分词大致分为下面的流程:

  • 标准化:例如去除空格、去除音调、Unicode标准化等。
  • 预分词:将文本分成一个个词。
  • 分词:将每个词分成 token 序列。
  • 后处理:添加特殊标记,例如 CLS、生成 attention_mask 、生成 token_type_id 等。

在这里我们使用 tokenizers 库构建我们自己的分词器,这里我们选择GPT-2同样的BPE算法。开始前导入下面需要的必要的包。

from tokenizers import (
    decoders,
    models,
    normalizers,
    pre_tokenizers,
    processors,
    trainers,
    Tokenizer,
)

首先基于BPE创建一个分词器

tokenizer = Tokenizer(models.BPE())

在初始化分词器时需要通过 unk_token 参数指定词表中未出现的 token。但是这里我们采用字节级的BPE分词算法,不需要指定该参数。

标准化阶段,我们采用 NFC 对 Unicode 码进行标准化,这里主要解决同样的显示字符存在不同的码点,这里不进行赘述。

tokenizer.normalizer = normalizers.NFC()

预分词阶段指定字节级分词

tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
    [
        pre_tokenizers.Digits(individual_digits=True),
        pre_tokenizers.ByteLevel(add_prefix_space=False),
    ]
)

第一个预分词是将数字独立分开,例如 1234 会被切成单独的 1234 四个数字,这样只需要十个数就可以表示整数。

后面的 ByteLevel 里指定 add_prefix_space=False 是不在句子开头加上前缀空格,我们通过一个例子表现两者的不同。

text = "I'm eating apples."
print(tokenizer.pre_tokenizer.pre_tokenize_str(text))
[('I', (0, 1)), ("'m", (1, 3)), ('Ġeating', (3, 10)), ('Ġapples', (10, 17)), ('.', (17, 18))]

Ġ 这个字符表示空格,不加前缀第一个字符就是 I 。如果加入前缀空格得到如下结果:

tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True)
print(tokenizer.pre_tokenizer.pre_tokenize_str(text))
[('ĠI', (0, 1)), ("'m", (1, 3)), ('Ġeating', (3, 10)), ('Ġapples', (10, 17)), ('.', (17, 18))]

可以看到 I 前面有个表示空格的字符。

现在我们指定了分词算法,并且给出了标准化和预处理流程,现在就可以在指定的数据集上进行训练了。我们指定一个 trainer ,给出期望词表大小和一些特殊token(方便后期构建ChatML模板)。

trainer = trainers.BpeTrainer(
    vocab_size=65535,
    special_tokens=["<|system|>", "<|user|>", "<|assistant|>", "<|end|>"],
    min_frequency=1500,
)

后处理和解码就不需要做特殊处理

tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)
tokenizer.decoder = decoders.ByteLevel()

书生·万卷数据集是 jsonl 格式,每行都是json数据,包括 idcontent,我们只需要 content 字段进行训练,由于数据集较大,我们使用 datasets 库可以高效加载。

from datasets import load_dataset, Dataset

dataset: Dataset = load_dataset(
    "json",
    data_files=[
        "nlp_datas/part-000020-a894b46e.jsonl.tar.gz",
        "nlp_datas/part-000065-a894b46e.jsonl.tar.gz",
    ],
    split="train",
)

至此我们就可以开始训练了。

def get_training_corpus():
    for i in range(0, len(dataset), 1000):
        yield dataset[i : i + 1000]["content"]


tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

训练完成之后保存结果,之前提到 BPE 算法会学习到词表和合并规则,因此我们也可以看到保存的文件中有词表和对应的合并规则。

# 保存 tokenizer
tokenizer.save("tokenizer.json")

我们尝试一下使用训练的分词器进行分词和解码。

tokenizer = Tokenizer.from_file("tokenizer_new.json")
text = "I hava a dog"
tokenizer.encode(text).tokens
>> ['I', 'Ġha', 'va', 'Ġa', 'Ġdog']

# 解码
ids = tokenizer.encode(text).ids
tokenizer.decode(ids)
>> 'I hava a dog'

最后我们将 tokenizer 封装一下,这样后续可以很方便的调用。

from typing import Optional, Tuple

from transformers import AddedToken, PreTrainedTokenizerFast

class CustomTokenizer(PreTrainedTokenizerFast):

    model_input_names = ["input_ids", "attention_mask"]

    def __init__(
        self,
        tokenizer_file=None,
        unk_token="<|end|>",
        bos_token=None,
        eos_token="<|end|>",
        pad_token="<|end|>",
        **kwargs
    ):
        bos_token = (
            AddedToken(
                bos_token, lstrip=False, rstrip=False, special=True, normalized=False
            )
            if isinstance(bos_token, str)
            else bos_token
        )
        eos_token = (
            AddedToken(
                eos_token, lstrip=False, rstrip=False, special=True, normalized=False
            )
            if isinstance(eos_token, str)
            else eos_token
        )
        unk_token = (
            AddedToken(
                unk_token, lstrip=False, rstrip=False, special=True, normalized=False
            )
            if isinstance(unk_token, str)
            else unk_token
        )
        pad_token = (
            AddedToken(
                pad_token, lstrip=False, rstrip=False, special=True, normalized=False
            )
            if isinstance(pad_token, str)
            else pad_token
        )

        super().__init__(
            tokenizer_file=tokenizer_file,
            unk_token=unk_token,
            bos_token=bos_token,
            eos_token=eos_token,
            pad_token=pad_token,
            **kwargs,
        )

    # Copied from transformers.models.gpt2.tokenization_gpt2_fast.GPT2TokenizerFast.save_vocabulary
    def save_vocabulary(
        self, save_directory: str, filename_prefix: Optional[str] = None
    ) -> Tuple[str]:
        files = self._tokenizer.model.save(save_directory, name=filename_prefix)
        return tuple(files)

尝试一下调用这个 tokenizer

tokenizer = CustomTokenizer(
    tokenizer_file="tokenizer_new.json",
    model_max_length=8196,
    additional_special_tokens=["<|system|>", "<|user|>", "<|assistant|>", "<|end|>"],
    clean_up_tokenization_spaces=False
)
text = "I have a dream."
tokenizer(text)
>> {'input_ids': [44, 381, 210, 3963, 17], 'attention_mask': [1, 1, 1, 1, 1]}

input_ids = tokenizer(text)["input_ids"]
tokenizer.decode(input_ids)
>> 'I have a dream.'

批处理也可以很方便的处理:

text = ["I hate apple.", "I like reading and playing basketball."]
tokenizer(text, padding=True)
>> {'input_ids': [[44, 12768, 12661, 17, 3, 3, 3], [44, 624, 3099, 238, 2872, 7603, 17]], 'attention_mask': [[1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1]]}

ids = tokenizer(text, padding=True)["input_ids"]
tokenizer.batch_decode(ids, skip_special_tokens=False)
>> ['I hate apple.<|end|><|end|><|end|>', 'I like reading and playing basketball.']

保存和加载:

tokenizer.save_pretrained("tokenizer") # 保存
tokenizer = CustomTokenizer.from_pretrained("tokenizer") # 加载

到此完成了分词器的整个流程,我们可以方便的编码、解码、保存和加载。

结语

至此我们成功实现了一个分词器,现在有了模型结构,有了分词器,有了数据集,我们可以开始训练我们的模型。下一篇我们将对模型进行训练,在训练中会遇到各种问题,我们也将逐步解决这些问题。

那么,我们该如何学习大模型?

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、大模型全套的学习路线

学习大型人工智能模型,如GPT-3、BERT或任何其他先进的神经网络模型,需要系统的方法和持续的努力。既然要系统的学习大模型,那么学习路线是必不可少的,下面的这份路线能帮助你快速梳理知识,形成自己的体系。

L1级别:AI大模型时代的华丽登场

L2级别:AI大模型API应用开发工程

L3级别:大模型应用架构进阶实践

L4级别:大模型微调与私有化部署

一般掌握到第四个级别,市场上大多数岗位都是可以胜任,但要还不是天花板,天花板级别要求更加严格,对于算法和实战是非常苛刻的。建议普通人掌握到L4级别即可。

以上的AI大模型学习路线,不知道为什么发出来就有点糊,高清版可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

四、AI大模型商业化落地方案

img

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-06-10 01:44:03       14 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-10 01:44:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-10 01:44:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-10 01:44:03       18 阅读

热门阅读

  1. C++中实现一个泄漏检测工具

    2024-06-10 01:44:03       9 阅读
  2. ubuntu远程控制软件todesk安装及网络连接问题解决

    2024-06-10 01:44:03       8 阅读
  3. C++,观察者模式,模拟Qt的信号和槽机制

    2024-06-10 01:44:03       11 阅读
  4. 在ADG只读备库使用数据泵导出数据

    2024-06-10 01:44:03       10 阅读
  5. Android基础-AIDL的实现

    2024-06-10 01:44:03       8 阅读
  6. Hadoop集群安装

    2024-06-10 01:44:03       7 阅读
  7. 1731. 每位经理的下属员工数量

    2024-06-10 01:44:03       9 阅读
  8. btstack协议栈实战篇--GAP LE Advertisements Scanner

    2024-06-10 01:44:03       10 阅读
  9. WooYun-2016-199433 -phpmyadmin-反序列化RCE-getshell

    2024-06-10 01:44:03       9 阅读