第 1 天 · 只学预测下一个字符,RNN 就能写出莎士比亚
一句话
Karpathy 训练了一个只做“预测下一个字符”的循环神经网络。它在莎士比亚、维基百科、LaTeX 和 Linux 源码上都生成了格式像样的文本。他还打开模型内部,找到了追踪引号和 URL 的神经元。这篇博文让大众第一次直观看到:简单目标加足够数据,模型就能自己学到结构。
论文讲了什么
这篇博文回答一个问题:只给模型一个目标——预测下一个字符——它能学到多少结构?Karpathy 用 6 组实验给出答案:比多数人预想的多。
模型。前馈网络的输入和输出长度固定。循环神经网络(Recurrent Neural Network, RNN)每一步读入一个字符,更新隐藏状态(hidden state),再输出下一个字符的概率分布。核心更新只有一行:h_t = tanh(W_hh · h_{t-1} + W_xh · x_t),输出为 y_t = W_hy · h_t。博文的实验实际使用多层 LSTM,第 2 天会拆开讲它。
训练。字符用独热编码(one-hot)输入。损失是每一步预测的交叉熵。梯度用时间反向传播(Backpropagation Through Time, BPTT)计算。
生成。先给模型一个起始字符。从输出分布中采样一个字符,把它喂回输入,然后重复。采样前把 logits 除以温度(temperature)。温度低时,文本保守且容易重复。温度高时,文本多样但错误多。
6 组数据,同一套代码:
- Paul Graham 文集(约 1 MB):2 层 LSTM 学会了他的用词和创业话题。
- 莎士比亚全集(4.4 MB):3 层网络生成的剧本有角色名、换行和古英语腔调。
- 维基百科(Hutter Prize 数据集,100 MB):生成文本有链接语法、表格和 XML 片段。
- 代数几何教材的 LaTeX 源码(16 MB):生成文本接近可编译,有定理环境和证明框架。
- Linux 源码(474 MB):生成的 C 代码有注释、缩进和配对的括号。
- 8000 个英文名字:生成的新名字中约 90% 不在训练集里。
打开黑盒。Karpathy 逐个查看隐藏单元的激活值。一个单元在 URL 内部激活,在 URL 外部关闭。一个单元只在维基链接的双方括号内激活。还有一个单元在引号内外切换。这些功能都没有人手工设计,全部来自“预测下一个字符”这一个目标。
局限。生成文本局部通顺,长程连贯性差。段落之间没有主线。代码常引用从未定义的变量。博文结尾称注意力机制(attention)是神经网络近来最有意思的结构创新。
关键概念
- 循环神经网络(Recurrent Neural Network, RNN)
- 每个时间步共享同一组权重,用隐藏状态把读过的内容带到下一步。它能处理任意长度的序列,参数量不随序列长度增长。
- 字符级语言模型(character-level language model)
- 以单个字符为单位,输出下一个字符的概率分布。词表只有几十到几百个符号,不需要分词;代价是序列更长,学习长程结构更难。
- 时间反向传播(Backpropagation Through Time, BPTT)
- 把 RNN 沿时间展开成一个很深的前馈网络,再做普通反向传播。实践中只展开固定步数,例如 25 步,这叫截断 BPTT。
- 温度采样(temperature sampling)
- 采样前把 logits 除以温度 T,再做 softmax。T 小于 1 时分布更尖,T 大于 1 时分布更平。大语言模型 API 里的 temperature 参数就是它。
- 隐藏状态(hidden state)
- 一个固定长度的向量,是 RNN 对“到目前为止读过什么”的压缩记忆。所有长程信息都必须挤进这个向量,它的维度决定记忆容量。
历史位置:为什么值得读
2015 年之前,RNN 已有 20 多年历史。但多数工程师认为它难训练、不实用。这篇博文同时开源了 Lua Torch 版的 char-rnn 和一个约 100 行的 NumPy 版 min-char-rnn。任何人都能在一台带 GPU 的电脑上复现结果。社区随后用它生成了菜谱、乐谱和各种风格的文本。
它的价值不在新算法。LSTM 早已存在,Graves 2013 年也用 LSTM 做过字符级文本生成。它的价值在示范:同一个目标、同一份代码,换一份数据就学到一种新结构。这正是后来 GPT 系列的核心思路:用下一个词元预测(next-token prediction)做通用预训练。
和清单其他天的关系:
- 第 2 天讲 LSTM 为什么能记住长程信息。
- 第 3 天讲怎样给大 LSTM 加 dropout,防止过拟合。
- 第 13–14 天的 Transformer 后来取代了 RNN。
- 第 25 天的规模定律(scaling laws)把“数据和模型越大越好”变成了可预测的公式。
学前准备
- 会用 NumPy 写矩阵乘法和 softmax
- 理解交叉熵损失和梯度下降
- 能用链式法则手推两层网络的梯度
- 知道独热编码(one-hot)怎样表示字符
在原仓库怎么学
原仓库第 1 天用纯 NumPy 实现字符级 RNN。建议按下面的顺序学:
- 先读 Karpathy 原博文,再打开 README.md 看本天的目录结构。
- 读 implementation.py。
CharRNN类基于 min-char-rnn,依次看forward、loss、backward、sample4 个方法。 - 运行 train_minimal.py,用 data/tiny_shakespeare.txt 训练。每隔一段时间看一次生成样本的变化。
- 打开 notebook.ipynb 交互式跑一遍。用 visualization.py 画损失曲线、隐藏状态演化和字符概率热图。
- 做 exercises/ 里的 5 个练习:从零搭 RNN、温度采样、换自己的数据集、损失可视化、莎士比亚与海明威风格分类器。
- 卡住时再看 exercises/solutions/。最后用 CHEATSHEET.md 复习。
建议:手写 BPTT 时,对照 backward 方法逐行核对梯度公式。改完代码后,先在几百个字符的小文本上训练,确认模型能把损失降到接近 0,再换回完整数据。这一步能快速排除实现错误。
今天再看
还活着的:“预测下一个词元”仍是所有大语言模型的预训练目标。温度采样仍是生成时最常调的参数。Karpathy 找“会追踪 URL 的神经元”的做法,发展成了今天的机制可解释性(mechanistic interpretability)研究:在大模型里找特征和回路。
被取代的:字符级输入被子词分词(subword tokenization,例如 BPE)取代,序列更短,效率更高。RNN 主干被 Transformer 取代。原因是 Transformer 训练时能并行处理整段序列,RNN 必须一步一步算。
回潮的:2023 年起,Mamba 这类状态空间模型和 RWKV 把“固定大小状态 + 线性时间推理”带了回来。它们可以看作能并行训练的新型 RNN。Karpathy 后来的 nanoGPT 教程,第一个示例数据集仍是字符级的莎士比亚。
容易卡住的地方
- 初始损失应约等于 ln(词表大小)。例如 65 个字符时约为 4.17。如果初始损失远高于这个值,检查权重初始化是否太大。
- 梯度爆炸会让损失突然变成 NaN。用梯度裁剪(gradient clipping)把每个梯度限制在 [-5, 5],min-char-rnn 就是这样做的。
- 手推 BPTT 时,最容易漏掉“从下一个时间步流回来的隐藏状态梯度”。写完后用数值梯度检查(gradient check)对比每个参数。
- 生成文本全是重复字符时,不要先调温度。先看验证集损失是否还在下降,再决定延长训练或降低学习率。
延伸阅读
- 原论文:The Unreasonable Effectiveness of Recurrent Neural Networks
- 原仓库第 1 天目录:01_Unreasonable_Effectiveness/ —— 代码、笔记本、练习与答案(英文)
- min-char-rnn.py(Karpathy 的 NumPy 版字符级 RNN) —— 约 100 行,本文实验的最小可读版本
- char-rnn(Lua Torch 原版代码) —— 博文实验使用的多层 LSTM 实现
- Visualizing and Understanding Recurrent Networks —— Karpathy 等人 2015 年,系统分析 LSTM 内部单元
- Generating Sequences With Recurrent Neural Networks —— Graves 2013 年,LSTM 生成文本与手写