ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零开始手把手带你训练LLM保姆级教程:用TaoToken统一Key打通Tokenizer到指令微调全流程

从零开始手把手带你训练LLM保姆级教程:用TaoToken统一Key打通Tokenizer到指令微调全流程 1. 从零训练一个小 LLM 到底难在哪个人开发者的真实卡点很多人第一次听到“从零训练 LLM”脑子里浮现的是几千张 A100 和几百万美元预算。其实对个人开发者来说真正要跑通的不是 GPT-4 那个量级而是一条端到端的小型 LLM 训练链路数据清洗、Tokenizer 训练、预训练、指令微调、模型评测。这条链路走通一次你对大模型的理解会发生质变后面再去看任何开源模型的技术报告都能对上号。我自己第一次尝试的时候卡点根本不在训练本身而在“环境”和“调用”这两件小事上。比如Tokenizer 训练脚本跑完了但词表合并逻辑写错导致中文被切得稀碎预训练 loss 看着在降但生成出来全是乱码指令微调数据格式对不上模型学了个“复读机”评测阶段想调个外部大模型做裁判结果 Key 管理一团乱几个平台的 Key 混在一起401 报错排查半天。所以这篇教程的思路是把训练链路拆成可验证的阶段每个阶段都有明确的产出物和验证动作同时用 TaoToken 的统一 Key 把“调用外部模型”这件事收口避免你在多个平台之间来回切换。TaoToken 在这里扮演的角色很简单——它是一个兼容 OpenAI 接口协议的统一入口你用一个 Key 就能调用多种模型适合在数据清洗、Self-Instruct 生成、模型评测这几个环节里当“外部大脑”。适合谁看有 Python 基础、跑过 PyTorch、想亲手跑通一次小型 LLM 训练的个人开发者。不需要你有 GPU 集群一张 24G 显存的卡比如 3090/4090就能跑完整个流程模型规模控制在 0.1B 到 0.5B 之间。整条链路我建议按这个顺序推进每一步都有产出阶段产出物验证动作数据清洗干净语料 jsonl抽样人工看 20 条Tokenizer 训练新词表 tokenizer.jsonencode/decode 往返测试预训练base 模型 checkpointloss 曲线 续写测试指令微调instruct 模型问答格式测试模型评测评测报告C-Eval 子集 GPT 裁判下面逐段展开每个阶段我都会给出可复制的配置和命令。2. TaoToken 统一 Key 前置准备一个 Key 打通数据清洗与评测调用在正式训练之前先把“外部模型调用”这条线搭好。原因很实际数据清洗阶段你可能想用大模型做质量打分Self-Instruct 阶段要用大模型生成指令数据评测阶段要用大模型当裁判。如果每个环节都去注册一个平台、管一个 Key光是环境变量就能把你搞晕。TaoToken 的做法是提供一个统一的 API 入口接口格式兼容 OpenAI 的/v1/chat/completions所以你现有的 OpenAI SDK 代码几乎不用改只换base_url和api_key就行。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。第一步去控制台创建一个 API Key。控制台地址https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完之后复制出来形如sk-xxxx这个 Key 后面会用在环境变量里。第二步确认你要用的模型 ID。不同环节对模型能力要求不一样数据清洗用便宜快速的模型就行Self-Instruct 生成指令建议用能力强的模型评测裁判也要用强模型。你可以在模型对话页面先试一下https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 输入一句话看看返回是否正常同时确认模型 ID 的写法。第三步把 Key 写进环境变量不要硬编码在代码里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api第四步用一段最小 Python 代码验证连通性import os from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) resp client.chat.completions.create( model你的模型ID, messages[{role: user, content: 用一句话解释什么是Tokenizer}], temperature0.2, ) print(resp.choices[0].message.content)如果这段能打印出正常中文回答说明统一 Key 这条线通了。后面数据清洗、Self-Instruct、评测裁判都复用这个 client不用再改配置。这里有个细节要注意base_url填的是https://taotoken.net/apiSDK 会自动拼上/v1/chat/completions。如果你手动用 requests 发请求完整地址是https://taotoken.net/api/v1/chat/completions。这个路径写错是最常见的 404 来源。另外如果你后面要用 Claude Code 这类编码工具做辅助开发它的配置也是三件套Base URL、API Key、Model ID。Base URL 同样是https://taotoken.net/apiKey 用上面创建的Model ID 按你实际调用的模型填。这三件套在 Cline、Codex 的auth.json、CC Switch 里都是同样的逻辑配一次就能复用。3. 可复制配置Tokenizer 训练与预训练参数逐项拆解这一节是全文技术密度最高的部分我会给出 Tokenizer 训练和预训练的可复制配置。先讲 Tokenizer因为它是整条链路的地基。3.1 Tokenizer 训练配置中文场景下直接用 LLaMA 原版 tokenizer 会遇到一个问题很多汉字不在词表里一个汉字被拆成 3 个 unicode token序列长度暴涨训练效率低。解决办法是在原词表基础上做词表扩充用中文语料训练一批新 token 合并进去。我用的方案是基于tokenizers库训练一个 BPE tokenizer然后和原词表合并。配置文件tokenizer_config.json如下{ vocab_size: 55296, min_frequency: 2, special_tokens: [s, /s, unk, pad], byte_fallback: true, add_prefix_space: false, trainer: { type: bpe, num_threads: 16, show_progress: true }, normalizer: { type: sequence, normalizers: [ {type: nfc}, {type: replace, pattern: \\s, content: } ] }, pre_tokenizer: { type: byte_level, add_prefix_space: false, use_regex: true } }关键参数解释vocab_size设成 55296是在原 LLaMA 词表 32000 基础上扩充约 23000 个中文 token这个规模在个人实验里比较平衡byte_fallback设为 true保证任何字符都能被编码不会出现 OOVmin_frequency设为 2过滤掉只出现一次的低频组合避免词表被噪声污染。训练脚本核心部分from tokenizers import Tokenizer from tokenizers.models import BPE from tokenizers.trainers import BpeTrainer from tokenizers.pre_tokenizers import ByteLevel from tokenizers.normalizers import NFC, Replace, Sequence tokenizer Tokenizer(BPE(byte_fallbackTrue)) tokenizer.normalizer Sequence([NFC(), Replace(r\s, )]) tokenizer.pre_tokenizer ByteLevel(add_prefix_spaceFalse) trainer BpeTrainer( vocab_size55296, min_frequency2, special_tokens[s, /s, unk, pad], show_progressTrue, ) tokenizer.train(files[data/clean_corpus.txt], trainertrainer) tokenizer.save(tokenizer/tokenizer.json)训练完之后必须做往返测试这是很多人跳过但极其重要的一步tok Tokenizer.from_file(tokenizer/tokenizer.json) s 从零开始训练一个大语言模型 ids tok.encode(s).ids back tok.decode(ids) assert back s, f往返失败: {back} print(f原文长度 {len(s)}token 数 {len(ids)})如果back和原文不一致说明 normalizer 或 byte_fallback 配置有问题必须先修好再往下走。中文句子 token 数应该接近字数如果明显偏大比如 10 个字变成 30 个 token说明词表扩充没生效。3.2 预训练配置预训练用 HuggingFace 的Trainer或者自己写训练循环都行我这里给一个基于transformers的配置pretrain_config.json{ model_type: llama, hidden_size: 768, intermediate_size: 2048, num_hidden_layers: 12, num_attention_heads: 12, num_key_value_heads: 4, max_position_embeddings: 2048, vocab_size: 55296, rms_norm_eps: 1e-5, rope_theta: 10000.0, torch_dtype: bfloat16, training: { per_device_train_batch_size: 4, gradient_accumulation_steps: 8, learning_rate: 3e-4, lr_scheduler_type: cosine, warmup_ratio: 0.03, num_train_epochs: 1, max_seq_length: 1024, save_steps: 500, logging_steps: 20, bf16: true, gradient_checkpointing: true } }这个配置对应约 0.1B 参数量的模型24G 显存能跑。num_key_value_heads设为 4 是用了 GQA分组查询注意力能显著降低推理时的 KV Cache 占用。gradient_checkpointing打开后显存占用能降一半代价是训练速度慢约 20%。数据预处理这块预训练和微调最大的区别是不要用 truncation 直接截断长文档。一本书几万个 token你截断到 1024 就永远只学到开头。正确做法是把所有文档拼起来按 1024 长度切块def pack_documents(docs, tokenizer, seq_len1024): buffer [] for doc in docs: buffer.extend(tokenizer.encode(doc).ids) buffer.append(tokenizer.eos_token_id) for i in range(0, len(buffer) - seq_len, seq_len): yield buffer[i:i seq_len]这样每个训练样本都是满的 1024 token没有 padding 浪费。4. 验证请求与成功结果从 loss 曲线到指令微调问答测试配置写完了接下来是逐阶段验证。每个阶段都要有明确的“成功信号”不然你根本不知道问题出在哪。4.1 预训练验证启动预训练后先看前 100 步的 loss。正常情况是从 10 左右快速下降到 6 到 7然后缓慢下降。如果 loss 一直卡在 10 以上不动检查三件事学习率是不是太小、数据是不是没喂进去、label 是不是没做 shift。训练到 5000 步左右做一次续写测试from transformers import AutoModelForCausalLM, AutoTokenizer import torch model AutoModelForCausalLM.from_pretrained(checkpoint-5000, torch_dtypetorch.bfloat16).cuda() tok AutoTokenizer.from_pretrained(tokenizer) prompt 人工智能的未来发展方向是 inputs tok(prompt, return_tensorspt).to(cuda) out model.generate(**inputs, max_new_tokens50, do_sampleTrue, temperature0.8) print(tok.decode(out[0], skip_special_tokensTrue))成功信号生成的文本语法通顺虽然内容可能不准确但至少是“像人话”的中文。如果生成乱码或者重复字符说明 tokenizer 和模型词表对不上或者训练不充分。4.2 指令微调数据构造预训练模型只会续写不会问答。指令微调就是教它“对话格式”。数据格式用 Alpaca 风格{ instruction: 把下面的句子翻译成英文, input: 今天天气很好, output: The weather is nice today. }如果自己标注数据太累可以用 TaoToken 调大模型做 Self-Instruct 生成。核心思路是给几个种子样例让模型举一反三seed_prompt 你是一个指令数据生成器。请参考下面的样例生成5条新的中文指令数据格式为JSON。 样例 {instruction: 解释什么是过拟合, input: , output: 过拟合是指模型在训练集上表现很好但在测试集上表现差的现象...} 请生成 resp client.chat.completions.create( model你的模型ID, messages[{role: user, content: seed_prompt}], temperature0.9, ) print(resp.choices[0].message.content)生成出来的数据要人工抽检把明显错误或重复的过滤掉。我实测下来1000 条生成数据里大概有 15% 需要清洗这个比例可以接受。4.3 指令微调训练与验证指令微调的配置和预训练类似但学习率要小一个量级通常 2e-5 到 5e-5{ learning_rate: 2e-5, num_train_epochs: 3, per_device_train_batch_size: 4, gradient_accumulation_steps: 4, max_seq_length: 512, lr_scheduler_type: cosine, warmup_ratio: 0.05 }训练完之后用统一的问答模板测试def build_prompt(instruction, input_text): if input_text: return f### 指令:\n{instruction}\n### 输入:\n{input_text}\n### 回答:\n return f### 指令:\n{instruction}\n### 回答:\n prompt build_prompt(介绍一下杭州) inputs tok(prompt, return_tensorspt).to(cuda) out model.generate(**inputs, max_new_tokens100, do_sampleFalse) print(tok.decode(out[0], skip_special_tokensTrue))成功信号模型输出直接是答案而不是继续编“### 指令:”这种模板。如果模型开始复读模板说明训练数据里模板格式不统一或者训练轮数不够。4.4 模型评测评测分两部分客观题用 C-Eval 子集主观题用大模型裁判。C-Eval 的评测逻辑是让模型在“答案”后面续写一个 token取 A/B/C/D 四个字母的 logits 做 softmaximport numpy as np import torch.nn.functional as F def eval_choice(model, tok, question, choices): prompt f{question}\nA. {choices[0]}\nB. {choices[1]}\nC. {choices[2]}\nD. {choices[3]}\n答案 inputs tok(prompt, return_tensorspt).to(cuda) with torch.no_grad(): logits model(**inputs).logits[0, -1] letter_ids [tok.encode(l, add_special_tokensFalse)[0] for l in ABCD] probs F.softmax(logits[letter_ids], dim0).cpu().numpy() return ABCD[np.argmax(probs)], probs主观评测用 TaoToken 调大模型当裁判把问题和两个模型的回答拼成 prompt让裁判打分。注意裁判 prompt 里要明确评分标准并且交换两个回答的顺序各评一次取平均缓解位置偏见。5. 本篇常见错排查401、local proxy failed、reading choices 逐个击破这一节列的都是我在实操中真实踩过的报错按出现频率排序。报错一401 Unauthorizedopenai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}原因通常是 Key 没读到或者写错了。排查顺序先echo $TAOTOKEN_API_KEY确认环境变量存在再检查代码里是不是用了os.environ[TAOTOKEN_API_KEY]而不是硬编码的旧 Key最后确认 Key 没有多余空格。如果是在 Docker 里跑注意环境变量要显式传进去。报错二local proxy failed / Connection erroropenai.APIConnectionError: Connection error.这个报错信息里如果出现local proxy failed说明你的请求被本地网络配置拦截了。检查HTTP_PROXY、HTTPS_PROXY环境变量是不是指向了一个不可用的地址用unset HTTP_PROXY HTTPS_PROXY清掉再试。另外确认base_url写的是https://taotoken.net/api不要漏掉https或者多写/v1。报错三reading choices 相关报错AttributeError: NoneType object has no attribute choices或者KeyError: choices这个通常发生在响应解析阶段。原因可能是请求返回了错误结构比如 401 的 error 字段但代码直接去取resp.choices。正确做法是先判断if not hasattr(resp, choices) or resp.choices is None: print(响应异常:, resp) raise RuntimeError(模型调用失败)还有一种情况是流式返回时choices为空需要检查streamTrue时是否正确拼接了 delta。报错四OAuth / token 过期类错误如果你用 Claude Code 或类似工具接入可能会遇到 OAuth 相关报错。这类工具通常需要三件套配置齐全Base URL 填https://taotoken.net/apiAPI Key 填你创建的 KeyModel ID 填实际模型。三者缺一或者 Model ID 写错都会报认证失败。CC Switch 里切换配置时注意保存后再重启工具配置才会生效。报错五loss 为 nan预训练时 loss 突然变 nan八成是学习率太大或者 bf16 溢出。先把学习率降到 1e-4 试试如果还不行就换 fp16 加 loss scaling。另外检查数据里有没有空样本空样本会导致除零。报错六显存 OOMtorch.cuda.OutOfMemoryError: CUDA out of memory按这个顺序降先开gradient_checkpointing再降per_device_train_batch_size再降max_seq_length最后考虑用 LoRA 只训练部分参数。24G 卡跑 0.1B 模型batch size 4、seq len 1024 是安全的。6. 把这条链路变成你自己的后续迭代与工具衔接跑通一次端到端流程之后你手里就有了一套可复用的脚手架。接下来可以做的迭代方向有几个第一扩大数据规模。第一次跑用 100MB 语料就够了验证链路通了之后可以逐步加到 1GB、10GB。数据质量比数量重要清洗环节多花时间值得。第二调整模型规模。把hidden_size从 768 加到 1024层数从 12 加到 16参数量翻倍观察 loss 和生成质量的变化。这时候显存可能不够需要上 LoRA 或者多卡。第三优化指令数据。Self-Instruct 生成的数据多样性有限可以混合开源数据集比如 BELLE 的中文指令集按任务类型做采样平衡。第四完善评测。C-Eval 子集只能看知识能力可以再加一个“指令遵循”评测用规则判断模型输出是否符合格式要求。如果你后面要长期做编码类 Agent 或者需要频繁调用模型可以考虑 TaoToken 的 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合需要稳定调用额度的场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的完整示例。API Key 管理页面还是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 需要新建或轮换 Key 的时候去这里。最后说一个我踩过的坑训练脚本里不要用相对路径存 checkpoint尤其是你在不同目录下启动训练的时候checkpoint 会散落到各处。统一用绝对路径或者用环境变量指定输出目录。这个习惯能帮你省下大量找文件的时间。
返回列表