大模型学习笔记(三)—— 预训练语言模型实践

配合代码:大语言模型:从理论到实践,本文记录在跑模型代码时产生的一些疑问。

数据集

代码采用的时wikipediabookcorpus数据集,wikipedia是由Hugging Face提供的英文维基百科快照数据集,数据格式如下:

{
  "id": "12345",
  "url": "https://en.wikipedia.org/wiki/SomeTopic",
  "title": "Some Topic",
  "text": "Some Topic is a subject discussed in..."
}

bookcorpus是由Zhu et al., 2015构建的数据集,包含一万多本小说文本,原始来源是网络上免费的小说(没有版权限制的书籍),数据格式如下:

{
  "text": "He looked at her and smiled. Then he turned away."
}

预训练常见的数据集还有:

数据集 内容类型 代表模型 来源
Common Crawl 网页 GPT-3, LLaMA 网络爬虫
C4 清洗网页 T5 Google
BooksCorpus 小说 BERT, GPT-2 HuggingFace
Wikipedia 百科 多模型 维基
OpenWebText 高质量网页 GPT-2, GPT-J Reddit 链接
The Pile 综合(代码+文章) GPT-NeoX EleutherAI
MC4 多语言清洗网页 mT5 Google
GitHub Stack 代码 StarCoder GitHub

数据处理

with open(os.path.join(model_path, "config.json"), "w") as f:
  tokenizer_cfg = {
      "do_lower_case": True,
      "unk_token": "[UNK]",
      "sep_token": "[SEP]",
      "pad_token": "[PAD]",
      "cls_token": "[CLS]",
      "mask_token": "[MASK]",
      "model_max_length": max_length,
      "max_len": max_length,
  }
  json.dump(tokenizer_cfg, f)

# 当词元分析器进行训练和配置时,将其装载到BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained(model_path)

def encode_with_truncation(examples):
    """使用词元分析对句子进行处理并截断的映射函数(Mapping function)"""
    return tokenizer(examples["text"], truncation=True, padding="max_length",
                     max_length=max_length, return_special_tokens_mask=True)

def encode_without_truncation(examples):
    """使用词元分析对句子进行处理且不截断的映射函数(Mapping function)"""
    return tokenizer(examples["text"], return_special_tokens_mask=True)

# 编码函数将依赖于 truncate_longer_samples 变量
encode = encode_with_truncation if truncate_longer_samples else encode_without_truncation

# 对训练数据集进行分词处理
train_dataset = d["train"].map(encode, batched=True)
# 对测试数据集进行分词处理
test_dataset = d["test"].map(encode, batched=True)

if truncate_longer_samples:
    # 移除其他列,将 input_ids 和 attention_mask 设置为 PyTorch 张量
    train_dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
    test_dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
else:
    # 移除其他列,将它们保留为 Python 列表
    test_dataset.set_format(columns=["input_ids", "attention_mask", "special_tokens_mask"])
    train_dataset.set_format(columns=["input_ids", "attention_mask", "special_tokens_mask"])


# 主要数据处理函数,拼接数据集中的所有文本并生成最大序列长度的块
def group_texts(examples):
    # 拼接所有文本
    concatenated_examples = {k: list(chain(*examples[k])) for k in examples.keys()}
    total_length = len(concatenated_examples[list(examples.keys())[0]])
    # 舍弃了剩余部分,如果模型支持填充而不是舍弃,则可以根据需要自定义这部分
    if total_length >= max_length:
        total_length = (total_length // max_length) * max_length
    # 按照最大长度分割成块
    result = {
        k: [t[i : i + max_length] for i in range(0, total_length, max_length)]
        for k, t in concatenated_examples.items()
    }
    return result

# 请注意,使用 batched=True,此映射一次处理 1000 个文本
# 因此,group_texts 会为这 1000 个文本组抛弃不足的部分
# 可以在这里调整 batch_size,但较高的值可能会使预处理速度变慢
#
# 为了加速这一部分,使用了多进程处理
if not truncate_longer_samples:
    train_dataset = train_dataset.map(group_texts, batched=True,
                                      desc=f"Grouping texts in chunks of {max_length}")
    test_dataset = test_dataset.map(group_texts, batched=True,
                                    desc=f"Grouping texts in chunks of {max_length}")
    # 将它们从列表转换为 PyTorch 张量
    train_dataset.set_format("torch")
    test_dataset.set_format("torch")

# 使用配置文件初始化模型
model_config = BertConfig(vocab_size=vocab_size, max_position_embeddings=max_length)
model = BertForMaskedLM(config=model_config)

# 初始化数据整理器,随机屏蔽 20%(默认为 15%)的标记
# 用于掩盖语言建模(MLM)任务
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer, mlm=True, mlm_probability=0.2
)

这段代码是对整个数据集的数据处理流程。

special_tokens是什么,是自己定义的吗?

special_tokens是指在语言模型中具有特定语义或功能的特殊词元(token),它们不是自然语言文本中的单词,而是模型训练或推理时用来引导、控制或分隔内容的标记。常见的特殊词元如下:

Token 含义 说明
[PAD] / <pad> 填充 用于补齐句子到相同长度
[CLS] / <s> 分类起始符 通常放在句子前,用于分类任务(如BERT)
[SEP] / </s> 句子分隔符 分隔两个句子,用于句子对任务
[MASK] 掩码标记 用于掩码语言建模(如BERT)
<bos> / <eos> 序列开始/结束 GPT等自回归模型使用,用于标记序列的边界
<unk> 未知词标记 表示分词器无法识别的词
<pad> 补位标记 用于对齐序列长度
<user> <assistant> 对话角色 用于对话式模型,指定用户或助手说话者身份

[MASK]是BERT系列的格式,<unk>是GPT、T5、LLaMA等系列的格式。几乎所有的大模型(如BERT、GPT、LLaMA、T5 等)都有自己的分词器,都有自己的特殊词元规范,每个模型都必须使用与其训练时一致的分词器和特殊词元,不同模型和任务会使用不同的特殊标记符号。

为什么分词器还需要训练?

大多数情况下直接使用预训练分词器,预训练大模型(如 BERT、GPT、LLaMA、T5 等)所配套的分词器已经在大规模语料上训练好,比如:

  • BERT 用的是 WordPiece(在 Wikipedia + BookCorpus 上训练)。

  • GPT 用的是 BPE(Byte Pair Encoding)。

  • LLaMA 用的是 SentencePiece(Unigram 模型)。

这些分词器已经学会了如何将自然语言划分为最合适的Token,因此在下游任务中(如分类、问答、对话等),通常直接使用预训练的分词器即可,不需要重新训练。

当数据集中含有大量“新词”或专业术语如医学、法律、编程语言等领域,有大量词汇未被原始分词器覆盖,导致频繁出现 [UNK] 或子词太碎,可以重新训练一个新的分词器,或扩展原分词器词表(add_tokens)。如果要训练一个新的LLM,那么分词器必须跟语料匹配,需要像代码中一样重新训练一下分词器。

分词器训练会学习如何将一个完整的词语拆分成词表中的子词单位。例如,“tokenization” 可能被拆分成 “token”、“##iza”、“##tion”(## 通常表示它是一个词的后半部分)。这使得模型能够处理未见过的词(通过将其拆分成已知子词)并减少词表大小,从而提高效率和泛化能力

token处理为定长

代码里写了两种策略,第一种是:在分词的时候直接分为定长,超过固定长度的直接截断。第二种是:对数据文本分词之后,调用group_texts,将多个样本拼接,并按max_length切块。

如何估计训练一个大模型需要的卡资源?

硬件厂商(如硬盘、显卡)和操作系统、计算代码、内存系统关于GB的定义是不一样的,操作系统中通常认为1MB = 2^10B,而2^10 = 1024 ≈ 1000,所以硬件厂商的1MB = 10^3B

先需要了解一下,1B参数量在HBM中储存需要的空间是多少,我们平时讲的xxB模型,指的都是参数量大小,B是billion,十亿参数量,显卡的显存有多少G/M是说有多少G/M个字节(byte),1个字节=8比特(bit)。

1B参数量 = 10^9参数量
如果使用fp32存储参数
一个参数 = 32bit = 4byte
1B参数量占用存储 = 10^9 * 4 byte = 4GB显存

但是训练模型除了存储之外,还需要保存输入数据、中间的推理结果、最终结果,保存优化器状态来计算梯度,保存梯度来更新参数。所以要储存的包括三个部分:

  • 梯度:和参数量一样大小。

  • 优化器状态:这取决于优化器的具体类型,如果是裸SGD就不需要额外显存开销,如果是带一阶动量(momentum)的SGD就是1倍,如果是Adam的话就要在momentum的基础上加上二阶动量,优化器状态所占显存就是参数的2倍。

  • 输入,输出,中间结果:和batch_size,词表大小,sequence_length有关。

输入输出这种比较动态,非动态的就是梯度和优化器状态,假设我们全参数微调训练一个参数量为1B的(小)大模型,优化器为Adam,精度为fp32,忽略数据和hidden states部分的显存占用,那么显存占用为:参数的4G+梯度的4G+优化器状态的8G,共16G。(不过感觉中间结果占用的也很大。。)

参考资料

posted @ 2025-06-06 17:27  ZCry  阅读(141)  评论(0)    收藏  举报