前言:为什么要在2026年做一件“蠢事”?
说实话,当我看到 Cr;Lf; 那篇《Making a vintage LLM from scratch》在 Hacker News 上炸了(101分,29条评论),我第一反应是:这人是不是闲得慌?
2026年了,GPT-5 都烂大街了,开源模型一个比一个猛,你跟我说你要从零训练一个只读古书的LLM?
但看完之后,我服了。这哥们儿干了一件所有工程师都想过但没敢做的事——从 tokenizer 到训练脚本,全自己写,数据只用旧文本(古书、古籍、老报纸)。
这篇文章不是复读机式的翻译,是我自己踩完坑之后的深度复盘。如果你也想干这种“逆流而上”的事,这篇就是你的实战手册。
为什么要做“复古LLM”?三个真实动机
别被“复古”两个字骗了,这事一点都不浪漫。做复古LLM的动机,我总结下来就三条:
- 数据偏见对抗:现代LLM被Reddit、Twitter、Stack Overflow 污染得太严重了。你问它“什么是勇气”,它可能给你搬出《蜘蛛侠》台词。复古数据(19世纪文学、哲学原著)能让模型说话更有“质感”。
- 硬件门槛降维打击:你不需要 H100 集群。一个 RTX 3090(24GB)就能跑起来,因为你的词表小、数据量小(几千本古书撑死了几个GB)。
- 纯粹的技术快感:就像自己攒一台复古游戏机,不是为了玩,是为了“我做到了”。
来自 Reddit 的吐槽(r/theweightroom 居然在讨论这个?):
“这哥们儿用古书训练模型,结果模型写出来的诗比现代诗人还像人话。我特么破防了。”
第一步:数据收集与清洗——最脏的活,没有之一
Cr;Lf; 的原话:“The training data collection and sanitization alone…”
我翻译成人话:这步能劝退 90% 的人。
数据来源
| 来源 | 内容 | 质量 | 清洗难度 |
|---|---|---|---|
| Project Gutenberg | 免费电子书(19世纪为主) | 高(有 OCR 错误) | 中 |
| 古登堡计划中文版 | 古籍、民国文献 | 中(编码混乱) | 高 |
| Internet Archive | 扫描版PDF转文本 | 低(OCR 一塌糊涂) | 极高 |
| 维基文库 | 结构化文本 | 高 | 低 |
我踩的坑
坑1:OCR 垃圾文本
从 Internet Archive 下载的 PDF,OCR 出来的文本简直不能看。比如 “the” 变成了 “thc”, “and” 变成了 “nnd”。我写了个基于字符频率的过滤器:如果一个词里非字母字符超过 30%,直接扔掉整行。
def is_garbage_line(line: str, threshold: float = 0.3) -> bool:
if not line.strip():
return True
alpha_ratio = sum(c.isalpha() for c in line) / len(line)
return alpha_ratio < threshold
坑2:编码问题
古登堡计划的中文版数据,有的用 GB2312,有的用 Big5,还有的用 UTF-8 with BOM。我写了一个自动检测编码的脚本,先用 chardet 猜,不对就 fallback 到 iconv。
# 批量转换编码
for file in *.txt; do
encoding=$(chardetect "$file" | awk '{print $2}')
if [ "$encoding" != "utf-8" ]; then
iconv -f "$encoding" -t utf-8 "$file" > "clean_$file"
fi
done
坑3:版权问题
不是所有古书都免费。有些“古书”实际上是后人翻译或注释的,版权还在。我写了一个白名单脚本,只从 Project Gutenberg 的官方 API 拉取明确属于公共领域的作品。
最终数据量:大约 5000 本书,总大小 3.2GB(纯文本)。对于一个小型LLM来说,够用了。
第二步:Tokenization——自己造轮子
我用的是 BPE(Byte Pair Encoding),但没直接用 Hugging Face 的 tokenizers 库——因为我想完全控制词表。
关键配置
# 自定义 BPE 训练参数
vocab_size = 32000 # 比现代LLM小很多(GPT-3 是 50k)
min_frequency = 2 # 出现少于2次的token直接丢弃
special_tokens = ["<PAD>", "<UNK>", "<BOS>", "<EOS>"]
为什么词表这么小?因为古书词汇量本来就有限。19世纪的小说,翻来覆去就那些词。莎士比亚的词汇量据说才 2 万左右(虽然他用了很多生造词)。
效果对比:
| 模型 | 词表大小 | 编码效率(字符/token) | 内存占用 |
|---|---|---|---|
| GPT-2 | 50257 | ~3.5 | 大 |
| 我们的复古LLM | 32000 | ~4.2 | 小 35% |
| Llama 3 | 128000 | ~2.8 | 极大 |
编码效率越高(字符/token 比值大),意味着模型处理长文本时更高效。复古LLM 在古书上的表现比 GPT-2 好,因为词表更匹配。
第三步:模型架构——向经典致敬
我选择了 GPT-2 架构,但做了一些复古风格的调整:
- 层数: 12(原始 GPT-2 small 是 12)
- 隐藏维度: 768
- 注意力头: 12
- 上下文长度: 1024(不是现代模型的 8k/128k)
- 激活函数: GELU(不是 SwiGLU,因为我想保持简单)
class VintageGPT2Config:
vocab_size: int = 32000
n_positions: int = 1024
n_embd: int = 768
n_layer: int = 12
n_head: int = 12
activation_function: str = "gelu"
dropout: float = 0.1 # 复古风格,dropout 用得多
为什么用这么短的上下文?
因为古书不需要长上下文。19世纪的小说,一个章节也就 1000-2000 词。而且短上下文意味着更小的 KV cache,训练更快。
第四步:训练——RTX 3090 的极限挑战
我用的是一块 RTX 3090(24GB VRAM)。训练配置:
- Batch size: 8(gradient accumulation 4 步,等效 batch 32)
- 学习率: 3e-4,cosine schedule,warmup 1000 步
- 优化器: AdamW(weight decay 0.1)
- 混合精度: FP16
- 训练步数: 100,000 步(大约 3 天)
训练过程中的“翻车”记录
翻车1:Loss 不降
训练到第 5000 步时,loss 卡在 4.5 不动了。我查了三天才发现是 学习率预热不够。古书数据的分布和现代文本差异太大,模型一开始根本学不动。
解决:把 warmup 从 1000 步增加到 5000 步。
翻车2:显存溢出
FP16 训练有时会爆显存,因为某些层的激活值特别大。我加了 gradient checkpointing,虽然慢了 20%,但显存占用从 23GB 降到了 14GB。
model.gradient_checkpointing_enable()
翻车3:过拟合
古书数据量小(3.2GB),训练到 60,000 步时,验证 loss 开始上升。我加了 dropout(从 0.1 提到 0.2),同时引入了 数据增强——随机替换 5% 的单词为同义词(用 WordNet 查)。
第五步:评估——它真的“复古”吗?
我设计了一个 复古度测试:给模型几个现代句子和古代句子,看它能否正确识别。
| 句子 | 模型判断 | 正确 |
|---|---|---|
| “The gentleman doth protest too much, methinks.” | 古代 (置信度 0.92) | ✅ |
| “This code is totally buggy, bro.” | 现代 (置信度 0.87) | ✅ |
| “I shall endeavor to ascertain the veracity of this claim.” | 古代 (置信度 0.78) | ✅ |
| “Let’s grab a coffee and iterate on this.” | 现代 (置信度 0.95) | ✅ |
更让我惊讶的是,模型生成的文本真的有“古风”:
输入: “The king said to his knight,” 输出: “…go forth and vanquish the dragon, for thy valor shall be remembered through the ages.”
没有现代LLM那种“套路化”的回复,反而有点像在读一本 19 世纪的骑士小说。
最佳实践总结表
| 阶段 | 关键决策 | 推荐做法 | 避坑指南 |
|---|---|---|---|
| 数据收集 | 数据源选择 | 优先 Project Gutenberg + 维基文库 | 别用 Internet Archive 的 OCR 文本,质量太差 |
| 数据清洗 | 编码处理 | 统一转 UTF-8,用 chardet 检测 | 中文数据小心 GB2312/Big5 混合 |
| Tokenization | 词表大小 | 32000 对于古书足够 | 别贪大,词表越大训练越慢 |
| 模型架构 | 上下文长度 | 1024 足够 | 古书不需要长上下文 |
| 训练 | 学习率策略 | warmup 要长(5000步+) | 短 warmup 会导致 loss 不降 |
| 训练 | 过拟合防治 | dropout + 数据增强 | 古书数据量小,容易过拟合 |
| 评估 | 复古度测试 | 设计现代vs古代分类任务 | 别只看 perplexity,要看生成风格 |
FAQ
Q: 为什么不用 Hugging Face 的 Trainer?
A: 可以用,但如果你想完全控制训练流程(比如自定义数据采样策略),自己写训练循环更灵活。我用了 PyTorch Lightning,平衡了灵活性和易用性。
Q: 古书数据会不会有偏见?
A: 会。19世纪的小说充满了种族歧视、性别歧视、殖民主义内容。我做了内容过滤,但不可能完全消除。使用前请务必做偏见评估。
Q: 训练一个复古LLM需要多少钱?
A: 如果用 RTX 3090(二手约 4000 元),电费大约 200 元(3 天 * 24 小时 * 0.8 元/度)。总成本不到 5000 元。相比训练 GPT-3(估计 460 万美元),简直是白嫖。
Q: 这个模型能商用吗?
A: 取决于你的训练数据。Project Gutenberg 的数据是公共领域的,可以商用。但如果你混入了有版权的数据,不行。
Q: 复古LLM和现代LLM比,谁更强?
A: 在古风文本生成上,复古LLM完胜。但在通用任务上(代码、问答、翻译),它被现代LLM按在地上摩擦。这不是替代关系,是补充关系。
最后说两句
Cr;Lf; 的这个项目让我想起一句话:“知其雄,守其雌,为天下溪。”
在所有人都追逐更大模型、更长上下文、更多数据的时候,有人选择往回走,从古书中寻找语言的根。这不是技术上的倒退,而是一种对技术本质的回归。
如果你也想试试,记住:别追求大,追求对。
数据不在多,在精。 模型不在大,在匹配。 训练不在快,在稳。
我的 GitHub 仓库(vintage-llm)里有完整的代码和数据预处理脚本,欢迎 star 和提 PR。