当前位置: 首页 > news >正文

大模型成长过程-预训练tokenizer

大模型(如GPT、BERT、PaLM等)的成长历程可以看作是一个技术栈的持续进化,涉及预训练、微调、强化学习、对齐(Alignment)等关键阶段。每个阶段解决不同问题,推动模型从“通用语言模型”发展为“有用、安全、可控的AI助手”。

首先预训练是一个基础过程,好比一个刚入武术道的初学者,把扎实的基本功打牢固然后才能为后续的难题发功发力。我们这里主要讨论生成式大语言模型。目标就是一个预测下一个token是什么的语言模型。

1 预训练阶段的技术细节

(1) 数据选择与处理

核心目标:构建高质量、多样化的训练语料库
关键技术决策

  • 数据来源

    • 通用文本:Common Crawl(网页)、Wikipedia、书籍(如Project Gutenberg)

    • 专业领域:GitHub(代码)、arXiv(科学论文)、Medline(医学)

    • 多模态数据:LAION(图文对)、视频字幕(如YouTube-8M)

  • 数据过滤

    • 质量过滤

      • 规则过滤:移除重复、低语言复杂度(如随机字符)、非目标语言文本。

      • 分类器过滤:训练NSFW检测模型过滤色情/暴力内容(GPT-4使用CLIP模型过滤图文数据)。

    • 去重

      • 文档级去重(MinHash算法)和子字符串级去重(如GPT-3移除重复的代码片段)。

    • 毒性控制

      • 使用Perspective API等工具检测仇恨言论。

  • 数据配比

    • 平衡领域分布(如GPT-3中5%代码数据、8%学术论文)。

    • 动态采样:高频数据降采样(避免模型偏向常见领域)。

上面叙述了如何准备一个高质量的数据,这是一个繁琐且最重要的过程。万丈高楼皆因一个好的地基。我这里使用一个开源的minimind_dataset数据集合。tokenizer预料大概1个G,刚好拿来试试手艺。

./dataset/
├── dpo.jsonl (909MB)
├── lora_identity.jsonl (22.8KB)
├── lora_medical.jsonl (34MB)
├── pretrain_hq.jsonl (1.6GB, ✨)
├── r1_mix_1024.jsonl (340MB)
├── sft_1024.jsonl (5.6GB)
├── sft_2048.jsonl (9GB)
├── sft_512.jsonl (7.5GB)
├── sft_mini_512.jsonl (1.2GB, ✨)
└── tokenizer_train.jsonl (1GB)
(2) Tokenizer训练

核心目标:高效、无损地将文本转换为模型可处理的token序列
技术流程

  1. 算法选择

    • BPE(主流选择):GPT/LLaMA系列使用,适合英语等空格分隔语言。其核心思想是通过迭代合并高频字符对来构建子词(Subword)词汇表也是我们主要学习的一个重点。

    • WordPiece:BERT使用,处理子词合并时优先高频组合。

    • Unigram:SentencePiece实现,基于概率模型删除低概率子词。

    • 特殊需求

      • 代码模型:保留空格/缩进(如StarCoder使用BPE+空格敏感tokenization)。

      • 多语言模型:SentencePiece直接处理原始字节(XLM-R)。

  2. 训练数据

    • 从预训练数据中采样代表性子集(如1-10GB)。

    • 需覆盖所有语言/领域(避免测试时出现未知token)。

  3. 关键参数

    • 词汇表大小:32K(GPT-2)→ 100K+(多语言模型)。

    • 特殊token:添加<|im_start|>等对话标记(ChatML格式)。

    • 归一化规则:统一Unicode编码(如将“é”规范化为“e”+“´”)。

  4. 评估指标

    • 压缩率(平均token数/字符数):衡量分词效率。

    • OOV率:测试集未登录词比例。

    • 下游任务影响:分词器对模型性能的影响(如代码模型需保留缩进信息)。

2 中文BPE的特殊性

  • 无空格分隔:需人工定义初始切分单元(如单字或传统分词结果)。

  • 多粒度问题:同一个词可能有不同长度的合法切分(如“北京大学”可切分为“北京/大学”或“北/京/大/学”)。

  • 符号处理:需保留中文标点(如“,”、“。”)作为独立token。

2.1. 完整示例:基于单字初始化的中文BPE

输入数据(已清洗)

"深度学习很重要"

"学习深度学习模型"

"模型训练需要数据"

步骤1:初始化基础字符词汇表
  • 将每个句子拆分为单字并添加</w>标记,统计频率:

  • # 拆分结果(频率统计)
    "深":1, "度":1, "学":2, "习":2, "很":1, "重":1, "要":1, 
    "模":2, "型":2, "训":1, "练":1, "需":1, "要":1, "数":1, "据":1, 
    "</w>":3  # 每句结尾一个

  • 初始词汇表:所有单字  +  </w>(共15个token)

步骤2:统计相邻字符对频率

遍历所有句子,统计相邻字符对出现次数:

句子1:"深 度 学 习 很 重 要 </w>"相邻对:深-度, 度-学, 学-习, 习-很, 很-重, 重-要, 要-</w>句子2:"学 习 深 度 学 习 模 型 </w>"相邻对:学-习, 习-深, 深-度, 度-学, 学-习, 习-模, 模-型, 型-</w>句子3:"模 型 训 练 需 要 数 据 </w>"相邻对:模-型, 型-训, 训-练, 练-需, 需-要, 要-数, 数-据, 据-</w>

频率统计结果

学-习:3, 模-型:2, 深-度:2, 度-学:1, 习-很:1, 
很-重:1, 重-要:1, 要-</w>:1, 习-模:1, 型-</w>:1,
型-训:1, 训-练:1, 练-需:1, 需-要:1, 要-数:1, 数-据:1, 据-</w>:1
步骤3:合并最高频字符对
  • 第一轮合并:最高频对 学-习(出现3次)

    • 合并为新token 学习

    • 更新词汇表:新增 学习

    • 更新句子分词:

      原句1:"深 度 学 习 很 重 要 </w>" → "深 度 学习 很 重 要 </w>"
      原句2:"学 习 深 度 学 习 模 型 </w>" → "学习 深 度 学习 模 型 </w>"
      原句3:无变化
  • 第二轮合并:次高频对 模-型(出现2次)

    • 合并为新token 模型

    • 更新词汇表:新增 '模型'

    • 更新句子分词:

      原句2:"学习 深 度 学习 模 型 </w>" → "学习 深 度 学习 模型 </w>"
      原句3:"模 型 训 练 需 要 数 据 </w>" → "模型 训 练 需 要 数 据 </w>"
      
  • 第三轮合并深-度(出现2次)

    • 合并为新token 深度

    • 更新词汇表:新增 '深度'

    • 更新句子分词:

      原句1:"深 度 学习 很 重 要 </w>" → "深度 学习 很 重 要 </w>"
      原句2:"学习 深 度 学习 模型 </w>" → "学习 深度 学习 模型 </w>"
步骤4:终止条件

假设设定词汇表大小为 20(初始15 + 已合并3个新token),继续合并直到达到目标。
后续可能合并:

  • 很-重 → 很重(但频率低,可能跳过)

  • 训-练 → 训练(若语料库更大时出现多次)

最终词汇表(部分)
基础单字:深,度,学,习,很,重,要,模,型,训,练,需,数,据,</w>
合并子词:学习, 模型, 深度

3. 对新文本的分词应用

输入句子

"深度学习模型需要训练数据"

分词过程
  1. 初始拆分单字:
    深 度 学 习 模 型 需 要 训 练 数 据 </w>

  2. 应用合并规则(优先最长匹配):

    • 匹配 深度 → "深度 学 习 模 型 ..."

    • 匹配 学习 → "深度 学习 模 型 ..."

    • 匹配 模型 → "深度 学习 模型 ..."

    • 剩余部分:需 要 训 练 数 据 </w>(无更高频合并)

  3. 最终token序列
    ["深度", "学习", "模型", "需", "要", "训", "练", "数", "据", "</w>"]


4. 中文BPE的关键技术细节

(1) 预处理优化
  • 混合初始化:先用传统分词工具(如Jieba)粗分,再对结果运行BPE。
    示例
    "北京大学" → Jieba切分为["北京", "大学"] → 作为BPE输入单元。

(2) 字节级BPE(BBPE)
  • 直接处理UTF-8字节,避免中文编码问题:

    • 汉字“深”(UTF-8编码:\xe6\xb7\xb1)会被拆分为3个字节。

    • 适用场景:多语言混合文本(如中英混杂的代码注释)。

(3) 词汇表大小影响
  • 小词汇表(如1万):更多单字token,长词拆解严重(如“中华人民共和国”→7个token)。

  • 大词汇表(如5万):包含更多常见词(如“北京”、“政府”),但增加内存占用。

(4) 未登录词处理
  • 回退到字符级:
    "量子计算"(假设未登录)→ 拆分为["量", "子", "计", "算"]


5. 与传统分词算法的对比

特性BPE分词传统分词(如Jieba)
分词粒度数据驱动,动态子词基于词典,固定切分
未登录词处理拆分为子字/子词可能强制切分为单字
多语言支持统一处理中英文需单独配置词典
典型应用GPT、LLaMA等大模型搜索引擎、文本分类

总结

中文BPE的核心流程:

  1. 初始化:单字或传统分词结果作为基础单元

  2. 迭代合并:统计相邻单元频率 → 合并最高频对 → 更新词汇表

  3. 终止:达到目标词汇表大小或频率阈值

优势

  • 平衡词典大小与OOV率

  • 适应不同领域文本(如医学、法律专业术语)

  • 与预训练模型框架无缝兼容

挑战

  • 需大量语料统计高频组合

  • 长词效率低于传统分词

实际应用中,中文BPE通常与WordPiece或Unigram算法结合优化(如BERT采用WordPiece,ALBERT使用Unigram)。

4 代码部分

数据:mini_llm预训练数据集

  • tokenizer训练数据集:

    • 经过预处理的中文维基百科数据集 
    • https://hf-mirror.com/datasets/pleisto/wikipedia-cn-20230720-filtered

文件示例:

[
  {
    "completion": "昭通机场(ZPZT)是位于中国云南昭通的民用机场,始建于1935年,1960年3月开通往返航班“昆明-昭通”,原来属军民合用机场。1986年机场停止使用。1991年11月扩建,于1994年2月恢复通航。是西南地区「文明机场」,通航城市昆明。 机场占地1957亩,飞行区等级为4C,有一条跑道,长2720米,宽48米,可供波音737及以下机型起降。机坪面积6600平方米,停机位2个,航站楼面积1900平方米。位于城东6公里处,民航路与金鹰大道交叉处。\n航点\n客服电话\n昭通机场客服电话:0870-2830004",
    "source": "wikipedia.zh2307"
  },
  {
    "completion": "我的英雄学院:英雄新世纪\n《我的英雄学院剧场版:英雄新世纪》(仆のヒーローアカデミア THE MOVIE ヒーローズ:ライジング)是一部于2019年12月20日上映的日本动画电影,由长崎健司执导、黑田洋介编剧,改编自日本漫画家堀越耕平创作的漫画系列《我的英雄学院》,同时也是其系列第二部电影版。\n概要\n本作电影内容同样为作者堀越耕平监修的原创故事,并表示「这部电影版某种意义上可以说是《我的英雄学院》的结局了」。\n登场角色\n制作人员\n主题曲\n 「ハイヤーグラウンド」\n 作词:片冈健太,作曲:黑田隼之介,主唱:sumika\n跨媒体展开\n集英社亦推出该片的小说版和文库版,由小说家誉司アンリ执笔著作,小说版于2019年12月20日上市。\n 仆のヒーローアカデミア THE MOVIE ヒーローズ:ライジング\n 仆のヒーローアカデミア THE MOVIE ヒーローズ : ライジング ノベライズ みらい文库版",
    "source": "wikipedia.zh2307"
  }]

代码加载数据:

import random
import json
from pathlib import Pathfrom tokenizers import (decoders,models,pre_tokenizers,trainers,Tokenizer,
)
import osrandom.seed(42)#def train_tokenizer():
# 读取JSONL文件并提取文本数据
data_path = '../datas/wikipedia-cn-20230720-filtered.json'
def iter_completions(file_path):"""生成器函数,逐项返回completion"""with Path(file_path).open('r', encoding='utf-8') as f:data = json.load(f)  # 注意:仍然会全部加载到内存for item in data:yield item['completion']
import random
import json
from tokenizers import (decoders,models,pre_tokenizers,trainers,Tokenizer,
)
import osrandom.seed(42)# 初始化tokenizertokenizer = Tokenizer(models.BPE())tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)# 定义特殊tokenspecial_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"]# 设置训练器并添加特殊tokentrainer = trainers.BpeTrainer(vocab_size=6400,special_tokens=special_tokens,  # 确保这三个token被包含show_progress=True,initial_alphabet=pre_tokenizers.ByteLevel.alphabet())# 读取文本数据texts = read_texts_from_jsonl(data_path)# 训练tokenizertokenizer.train_from_iterator(texts, trainer=trainer)# 设置解码器tokenizer.decoder = decoders.ByteLevel()# 检查特殊token的索引assert tokenizer.token_to_id("<|endoftext|>") == 0assert tokenizer.token_to_id("<|im_start|>") == 1assert tokenizer.token_to_id("<|im_end|>") == 2# 保存tokenizertokenizer_dir = "../model/"os.makedirs(tokenizer_dir, exist_ok=True)tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))tokenizer.model.save("../model/")# 手动创建配置文件config = {"add_bos_token": False,"add_eos_token": False,"add_prefix_space": False,"added_tokens_decoder": {"0": {"content": "<|endoftext|>","lstrip": False,"normalized": False,"rstrip": False,"single_word": False,"special": True},"1": {"content": "<|im_start|>","lstrip": False,"normalized": False,"rstrip": False,"single_word": False,"special": True},"2": {"content": "<|im_end|>","lstrip": False,"normalized": False,"rstrip": False,"single_word": False,"special": True}},"additional_special_tokens": [],"bos_token": "<|im_start|>","clean_up_tokenization_spaces": False,"eos_token": "<|im_end|>","legacy": True,"model_max_length": 32768,"pad_token": "<|endoftext|>","sp_model_kwargs": {},"spaces_between_special_tokens": False,"tokenizer_class": "PreTrainedTokenizerFast","unk_token": "<|endoftext|>","chat_template": "{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{{ '<|im_start|>system\\n' + system_message + '<|im_end|>\\n' }}{% else %}{{ '<|im_start|>system\\nYou are a helpful assistant<|im_end|>\\n' }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|im_start|>user\\n' + content + '<|im_end|>\\n<|im_start|>assistant\\n' }}{% elif message['role'] == 'assistant' %}{{ content + '<|im_end|>' + '\\n' }}{% endif %}{% endfor %}"}# 保存配置文件with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w", encoding="utf-8") as config_file:json.dump(config, config_file, ensure_ascii=False, indent=4)print("Tokenizer training completed and saved.")def eval_tokenizer():from transformers import AutoTokenizer# 加载预训练的tokenizertokenizer = AutoTokenizer.from_pretrained("../model/")messages = [{"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"},{"role": "user", "content": '你来自哪里?'},{"role": "assistant", "content": '我来自地球'}]new_prompt = tokenizer.apply_chat_template(messages,tokenize=False)print(new_prompt)# 获取实际词汇表长度(包括特殊符号)actual_vocab_size = len(tokenizer)print('tokenizer实际词表长度:', actual_vocab_size)model_inputs = tokenizer(new_prompt)print('encoder长度:', len(model_inputs['input_ids']))input_ids = model_inputs['input_ids']response = tokenizer.decode(input_ids, skip_special_tokens=False)print('decoder和原始文本是否一致:', response == new_prompt)def main():train_tokenizer()eval_tokenizer()if __name__ == '__main__':main()

显示的结果如下: 

相关文章:

  • SQL Server 窗口函数详解:窗口行数控制的原理、关键字与应用场景
  • 鸿蒙NEXT-HMRouter,在使用router后无法跳转问题解决
  • 计算机网络-自顶向下—第四章网络层重点复习笔记
  • Python实例题:Python计算偏微分方程
  • 【Ubuntu 22.04 推荐的 apt 包管理方式详解】
  • HQL 优化:从低效到高效的蜕变之旅
  • Git可视化革命:3分钟学会用Mermaid+AI画专业分支图
  • 数据治理域——数据建模设计
  • LabVIEW工业金属腐蚀监测
  • LeetCode 第71题 简化路径(繁琐)
  • 打牙祭是什么意思
  • SCADA|信创KingSCADA4.0历史报警查询的差异
  • XCTF-misc-János-the-Ripper
  • ELK日志文件分析系统——E(Elasticsearch)
  • Karate UI测试之驱动配置
  • vulnhub-Earth
  • SD和comfyui常用模型介绍和下载
  • 什么是泛型,如何使用它?
  • 【LangChain】4 基于文档的问答
  • 操作系统多级存储模型
  • 网络宣传广告费多少/seo是什么seo怎么做
  • 高清免费爱做网站/seo基础教程使用
  • 宁波seo/站长工具seo综合查询工具
  • 怎么做电商网站 用户画像/信息流广告的特点
  • 网站建设中敬请期待/seo搜索引擎入门教程
  • wordpress 主题 设置/seo概念