同一年,两组人 2014 年 9 月,两篇论文几乎同时挂上 arXiv。 一篇是 Sutskever、Vinyals、Le 的 Sequence to Sequence Learning with Neural Networks。Google Brain 出品,用两个 LSTM——一个读输入、一个生成输出——直接刷新了英法翻译的纪录。这是 #04 讲过的故事。 另一篇是 Bahdanau、Cho、Bengio 的 Neural Machine Translation by Jointly Learning to Align and Translate。蒙特利尔大学加雅各布大学,用 GRU + 双向 encoder,每生成一个目标词都回头看一遍源句。 两组人没合作,也没引用对方的当年版本,但他们面对的是同一个问题:任意长度的输入最后都要变成一个固定长度的向量,句子一长,信息就会丢。 两篇论文的处理办法不一样。Sutskever 还在固定向量里想办法,倒着读输入,让最先要翻译的信息离 decoder 更近。Bahdanau 把 encoder 的每一步都留下来,decoder 每一步根据当前状态选择要参考哪些 encoder state。 卡在哪里 #04 里有一张图:序列越长,第一个词的信息能存活到最后一个 hidden state 的比例越低。50 个词的句子,开头那个词存活率不到 2%。 LSTM 已经缓解了长距离记忆问题。这里卡住的是另一件事:decoder 只拿到 encoder 最后一步的状态。这个状态是一个固定大小的向量,不管输入是 5 个词还是 50 个词,维度都不变。容量有上限,信息没上限。 Sutskever 论文里有一个做法暴露了这件事:把输入序列倒过来读,BLEU 涨好几个点。“Je suis étudiant” 改成 “étudiant suis Je” 喂进去。倒读没有改变模型容量,只是让 encoder 最后一步离 decoder 最先要生成的词更近。这种局部修补能挤出几个点,但也说明结构本身还没换过。 ...
Sutskever 30 #08:词第一次有了位置
从查表开始 #01 最后留了一个问题: n-gram 看见的"猫"和"狗"完全是两个互不相干的整数 ID,哪怕语料里"猫吃鱼"和"狗吃肉"各出现 100 次,它也不会推断"猫吃肉"或"狗吃鱼"。它只数它见过的那个东西。 2013 年 Mikolov 那两篇 word2vec 论文(Efficient Estimation of Word Representations in Vector Space 和 Distributed Representations of Words and Phrases and their Compositionality)往这里走了一步:把词从整数 ID 变成向量。 换一个看法:把词放进一个向量空间 n-gram 的世界里,每个词是一个 ID。词表 5 万,那"猫"可能是 723,“狗"可能是 4912,“汽车"可能是 18。这三个数字之间没有任何意义——723 和 4912 在数轴上相距 4189,但它没告诉你"猫"和"狗"在概念上多近。 word2vec 把这层抽掉。每个词不再是一个 ID,而是一个几十维到几百维的实数向量。比如 100 维: 猫 → [0.12, -0.34, 0.81, ..., 0.05] (100 个数) 狗 → [0.18, -0.29, 0.77, ..., 0.09] 汽车 → [-0.66, 0.41, -0.12, ..., 0.88] “猫"和"狗"的向量在这 100 维空间里挨得很近,“汽车"在远处。这套向量怎么来的?训练。 ...
Sutskever 30 #01:在神经网络之前,人们怎么猜下一个词?
一个老问题 给你一句话的前半截,猜下一个词是什么。 这是语言模型最早也最直接的版本。从 1948 年 Shannon 开始,到 2010 年代神经网络接管之前,几代人都在做同一件事:给词的下一个位置算一个概率分布。 后面几篇会讲神经网络怎么把这个问题接过去,先学内部状态,再学更长的记忆,最后学会看整段上下文。这一篇先回到起点:在神经网络敢碰语言之前,那一套老办法长什么样,又是怎么走到尽头的。 n-gram:数频率 最朴素的版本叫 n-gram:用前 n-1 个词预测第 n 个。 $$P(w_t \mid w_{t-n+1}, \ldots, w_{t-1})$$n=2 是 bigram,看前 1 个词;n=3 是 trigram,看前 2 个;n=5 看前 4 个。怎么算?数。把语料里所有"前 n-1 个词出现,紧跟着 X"的次数除以"前 n-1 个词出现"的总次数,就是 X 的概率。 这就是全部。没有梯度,没有反向传播,只有一张大表。 它的好处是:你拿到一份足够大的语料,按公式跑一遍,就能拿到一个能用的语言模型。便宜、快、可解释。 它能上线 这套东西在那几十年里不是玩具。它是真在线上的: 语音识别:你说的句子声学上歧义很多(“recognize speech” 和 “wreck a nice beach” 听起来几乎一样),n-gram 给一个语言概率,把听起来对、读起来不对的解码砸下去 机器翻译:早期的 SMT 系统用 n-gram 给翻译候选打分 输入法:你打"明天",下一个词候选"晚上 / 见面 / 早上"按 n-gram 概率排序 你今天用的搜索引擎、十年前的输入法,背后都站着一层这种东西。它解决的是排序问题:哪个候选更像一句人会说的话。 卡在哪 n-gram 能算短搭配。句子一长,它就只剩下局部统计。主要卡在两件事:窗口太短和没有泛化。 窗口太短。trigram 看 2 个词的上下文,5-gram 看 4 个。再多就开始爆。词表 5 万的话,5-gram 要面对大约 $50{,}000^4 \approx 6.25 \times 10^{18}$ 种上下文,没有任何语料能填满这张表。结果是:模型对长一点的句子结构没感觉。“小明今天没去上学,因为他___"——人会想到"病了 / 累了 / 不想去”,n-gram 看到的只是"因为他"这三个词。前面那半截"小明今天没去上学"对它没意义。 ...
Sutskever 30 #07:相信之前,需要一次证明
上一篇留下的问题 上一篇 讲的是 scaling laws——loss 随着参数、数据、算力按 power law 下降。2020 年 Kaplan 把它写成公式之后,大家已经习惯了"模型更大、数据更多、算力更强,结果就会更好"。 但有一个更早的问题:大家为什么会愿意相信这条路? 今天回头看,这条路像常识。2012 年之前,它一点也不像——深度学习几度陷入寒冬,SVM / boosting 一度是主流。 往回走一步,看 2012 年那场比赛到底发生了什么。 ImageNet 2012 ImageNet 是 Fei-Fei Li 团队搞的图像识别比赛,有 120 万张标注图片,分成 1000 类。2010、2011 两年,最好的系统 top-5 错误率在 25% 上下。这是靠手工设计特征 + SVM 的时代,进步以每年 1-2 个百分点计。 2012 年 9 月,结果出来—— 2010 冠军 2011 冠军 2012 AlexNet 2012 第二名 top-5 错误率 28.2% 25.8% 15.3% 26.2% 领先第二名 10 个百分点。这个分差一出来,大家就知道事情变了。 论文标题:ImageNet Classification with Deep Convolutional Neural Networks。三个作者:Alex Krizhevsky(一作)、Ilya Sutskever、Geoffrey Hinton,都来自多伦多大学。 ...
Sutskever 30 #06:大就是好,但好得有规律
上一篇留下的问题 上一篇讲了 Transformer——一个能并行处理整个序列的架构,没有 RNN 的速度瓶颈。 但 Transformer 本身只是个工具。真正让它成为 GPT-4 这种庞然大物的,是另一个发现:模型越大、数据越多、算力越多,效果就越好——而且好得有规律。 2020 年之前,没人知道这个规律有多准、能延伸多远,也没人知道该怎么"大"——堆参数还是堆数据?花 100 倍的钱能换来多少提升? Kaplan 等人 2020 年发了一篇论文把这些问题量化了。论文标题:Scaling Laws for Neural Language Models——神经语言模型的 scaling 定律。 一个反直觉的发现 机器学习里大多数曲线有"拐点"——一开始进步快,越往后越慢,最后碰到天花板。直觉上模型也该这样:加到一定大小就饱和了,再加也没用。 Kaplan 实测下来的结果是:在他们测试的范围内,loss 一直在下降,没有看到饱和。而且下降的方式遵循一个非常简单的规律: $$L(N) = \left(\frac{N_c}{N}\right)^{\alpha_N}$$翻译过来:loss($L$)是参数量($N$)的 power law(幂律)函数。$\alpha_N$ 是一个小指数,大约 0.076。 power law 在数学上有一个好性质——在 log-log 图上是一条直线。Kaplan 团队跑了从一千参数到几亿参数的模型,loss 在 log-log 图上画出来,就是一条几乎完美的直线。 不只是参数。数据量($D$)和算力($C$)也都满足类似的 power law。三条曲线,三个不同的指数,但都是一样的形态: 横轴是 log,纵轴也是 log。直线的斜率就是 power law 的指数(带个负号)。这意味着:你想知道 10 倍的算力能带来多少提升?把直线往右延伸看一眼就行。 为什么这是重要的 在 scaling laws 之前,做 AI 模型有点像炼丹——你试一个架构,调参,跑训练,看效果。如果效果不好,你不知道是模型不够大、数据不够多、还是结构本身有问题。 scaling laws 把这种不确定性砍掉了。如果你的模型在小尺度下符合 power law,那你几乎可以预测它在大尺度下的表现。换句话说,你可以用一笔小预算的实验,去推断一笔大预算实验的结果。 ...
Sutskever 30 #05:把 RNN 全部扔掉
上一篇留下的线索 上一篇结尾说了一句话:attention 这个配角,后来变成了主角。 Seq2seq + attention 的架构里,LSTM 负责处理序列,attention 负责让 decoder 回头看。两者各司其职。但有一个问题一直没解决——LSTM 是顺序处理的。第 1 个词处理完才能处理第 2 个,第 2 个处理完才能处理第 3 个。 100 个词的句子,要跑 100 步。前一步没算完,后一步就不能开始,所以很难把 GPU 并行用满。 Attention 本身没有这个限制。它一次看所有位置,一次算完所有权重。 2017 年,Vaswani 等人把这个观察推到了极致:把 LSTM 全部去掉,只留 attention。 论文标题:“Attention Is All You Need”。 Self-Attention:自己看自己 上一篇的 attention,是 decoder 回头看 encoder。Query 来自 decoder,Key 和 Value 来自 encoder——两个不同的序列之间在交互。 Transformer 引入了一个新操作:self-attention——一个序列看自己。Query、Key、Value 全部来自同一个输入。每个位置都问所有位置:“你跟我有关系吗?” 为什么要自己看自己?因为理解一个词,需要知道它在句子里的角色。“bank” 是银行还是河岸,取决于周围的词。Self-attention 让每个词都能直接看到句子里所有其他词,一步到位。 核心公式: $$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$跟上一篇的 Bahdanau attention 本质一样——query 和 key 算匹配度,softmax 变成权重,加权求和 value。但有两个变化: ...
Sutskever 30 #04:记住了,然后呢?
上一篇留下的问题 上一篇里,LSTM 解决了 vanilla RNN 记不远的问题——加法通道让梯度不再衰减,记忆可以跨越几十步传下去。 但我们一直在做同一件事:给定一个序列,预测下一个 token。输入和输出共享同一个时间线——第 1 步输入产生第 1 步输出,第 2 步输入产生第 2 步输出。 现实世界的很多任务不是这样的。翻译:一句中文进去,一句英文出来,长度不同,语序不同。摘要:一段话进去,一句话出来。对话:一个问题进去,一个回答出来。 输入是一个序列,输出是另一个序列。长度可以不同,结构可以不同。怎么办? 两个 LSTM,一根管子 2014 年,Sutskever、Vinyals 和 Le 提出了一个极简的方案:用两个 LSTM,中间接一根管子。 Encoder:一个 LSTM,把输入序列从头读到尾。读完之后,最后一步的 hidden state 就是整个输入的"总结"——一个固定长度的向量。 Decoder:另一个 LSTM,拿到这个向量作为初始状态,一个 token 一个 token 地生成输出。 就这么简单。输入和输出可以不同长度,因为 encoder 把任意长度的输入压缩成一个固定长度的向量,decoder 从这个向量展开成任意长度的输出。 代码的核心就几行(这里用简化的 RNN 展示结构,实际论文用的是 LSTM——gate 的细节上一篇讲过了): # Encoder:读完整个输入,拿到最后的 hidden state h = np.zeros((hidden_size, 1)) for x in input_sequence: concat = np.vstack([x, h]) h = np.tanh(np.dot(W_enc, concat) + b_enc) context = h # 这就是"总结"——整个输入压缩成一个向量 # Decoder:从 context 开始,逐步生成输出 h = context for t in range(max_output_length): concat = np.vstack([prev_output, h]) h = np.tanh(np.dot(W_dec, concat) + b_dec) output = np.dot(W_out, h) + b_out prev_output = output 这个架构有个名字——encoder-decoder,也叫 seq2seq(sequence to sequence)。它是 Sutskever 自己的工作,发表时直接打破了法语-英语翻译的最佳纪录。 ...
Sutskever 30 #03:记忆为什么会漏,LSTM 怎么堵的?
上一篇留下的问题 上一篇里,我们用一个 vanilla RNN 在 2490 个字符上跑出了 hello world。但那个模型的记忆只有 25 步——再远的东西它就看不到了。 一个自然的想法是:那把记忆加大呢?64 个隐藏单元不够,换 1000 个,换 10000 个。 没用。白板再大也没用。 乘法效应 vanilla RNN 每一步做的是:把旧状态和新输入混在一起,过一个 tanh。这个操作是乘法性质的——旧状态被一个矩阵乘一次、压一次。连续做 30 步,就是连乘 30 次。 $0.9^{30} \approx 0.04$ 哪怕每次只损失 10%,30 步之后只剩 4%。每一步都在把之前写的东西乘以一个小于 1 的数,越乘越淡。白板开到多大都一样。 训练的时候也一样。梯度要从最后一步往回传,告诉前面的参数"你该记住什么"。每往回传一步,信号就乘一次,30 步之后信号几乎为 0。学习信号传不到那么远,模型就学不到"30 步之前发生了什么"。 这就是梯度消失。连乘带来的衰减,是 vanilla RNN 记不远的根本原因。 LSTM:开一条加法的路 LSTM 的做法是,在乘法链旁边开了一条加法通道——cell state。这条通道专门用来存记忆,更新方式是加法:旧记忆保留一部分,加上新信息的一部分。 光有加法通道还不够。如果每一步都把所有新信息加进去,cell state 很快就会被噪音淹没。所以 LSTM 还配了三个开关,控制这条通道上的读写: 三个开关在每一步都问三个问题: 忘记开关(forget gate):旧记忆里哪些要继续留着,哪些可以扔掉? 写入开关(input gate):新信息要不要写进记忆?写多少? 输出开关(output gate):记忆里的东西,这一步要拿多少出来用? 每个开关输出一个 0 到 1 之间的数。1 就是全开,0 就是全关。 代码里长这样: # 忘记开关 f = sigmoid(np.dot(self.Wf, concat) + self.bf) # 写入开关 i = sigmoid(np.dot(self.Wi, concat) + self.bi) # 新信息候选 c_tilde = np.tanh(np.dot(self.Wc, concat) + self.bc) # 更新记忆:旧的留一部分 + 新的写一部分 c_next = f * c_prev + i * c_tilde # 输出开关 o = sigmoid(np.dot(self.Wo, concat) + self.bo) h_next = o * np.tanh(c_next) 关键是这一行: ...
Sutskever 30 #02:猜下一个字符,然后呢?
一个任务 给你一串字符,猜下一个是什么。就这一个任务。 2015 年,Karpathy 拿这个任务训练了一个 RNN。训练完让它自己写,它写出了能编译的 C 代码。没人教它什么是函数、什么是括号——它只是被"猜下一个字符"反复训练,自己学会了这些。他把过程写成了一篇博客,影响力比很多正式 paper 都大。 我们拿最小的版本跑了一遍。 hello world 先看结果。这是我们的模型在第 1800 步生成的文本: rmption is pattornnin eell n levery ywormnearn data hello world helens pis prnters hello world 完整地冒出来了,旁边还是一堆乱七八糟的东西。 这个模型的全部训练数据是 2490 个字符——几句英文短句复制 10 遍。hidden size 64,vanilla RNN,纯 NumPy 手写。它从来不知道 “hello world” 是一个词组。 那它怎么学会的? 模型每一步对 24 个字符各猜一个概率,加起来是 1。训练数据里有正确答案——比如 h 后面确实跟着 e。一开始模型给 e 的概率很低,可能 4%。 猜错了怎么办?算一个数: $$\text{loss} = -\log(p)$$$p$ 是模型给正确答案的概率。$-\log(0.04) \approx 3.2$,挺疼的。$-\log(0.9) \approx 0.1$,几乎没感觉。概率越低,这个数越大,惩罚越重。 为什么用 $\log$ 而不是更直觉的 $1 - p$?因为 $1 - p$ 是线性的:概率从 50% 降到 40%,惩罚增加 0.1;从 5% 降到 4%,惩罚也只增加 0.01。但模型已经很离谱了(5%)还能更离谱(4%),这种恶化应该更疼才对。$-\log(p)$ 做到了这一点——概率越低,曲线越陡,越错越疼。 ...
Docmod:Word文档的XML手术刀
一个被忽视的问题 让AI改一份Word文档,改完之后SmartArt没了,图表炸了,批注消失了,页眉页脚全乱了。它把你的文档拆了,用碎片拼了一个新的。 目前缺少一个好的工具让AI在保留文档完整性的前提下做局部编辑。Docmod就是为了解决这个问题。 问题的根源 .docx本质上是一个ZIP压缩包,里面装着结构化的XML: report.docx (ZIP) ├── [Content_Types].xml ├── word/ │ ├── document.xml ← 正文 │ ├── styles.xml ← 样式定义 │ ├── comments.xml ← 批注 │ ├── header1.xml ← 页眉 │ ├── footer1.xml ← 页脚 │ └── media/ ← 图片 └── _rels/ ← 关系定义 现有方案用OpenXML SDK或python-docx把文档解析成对象模型,让AI操作对象模型,再序列化回docx。在实践中,大多数库的序列化会重写整个XML树。SDK不认识的元素、自定义的命名空间、精心调过的格式——全部丢失。改一个错别字,输出的是一份"长得像原文"的全新文档。 走过的弯路 让AI直接输出OpenXML? OpenXML的复杂度远超AI的舒适区。一个加粗的段落: <w:r><w:rPr><w:b/></w:rPr><w:t>文字</w:t></w:r> 让AI在这种层级的XML里精确操作,错误率极高,调试极痛苦。 用Markdown作为中间格式? 表达力不够。表格合并、精确的字体大小、段落缩进——这些Word文档的核心价值在Markdown中无法表达。用Markdown做中转等于主动丢弃信息。 三个条件交叉筛选——AI的训练数据中大量存在、能表达Word文档的结构和样式、可以做精确的元素级diff——符合的格式只有一个:扁平化的HTML。 核心洞察:扁平HTML作为可diff的中间表示 1995年风格的HTML——每个元素自带完整的内联样式,没有外部CSS,没有继承,没有级联。现代HTML把样式藏在CSS文件里,需要级联计算才能确定最终效果;扁平HTML把这层复杂性彻底消除了。 <h1 data-id="p1" style="font-size:22pt; font-weight:bold;"> 季度报告 </h1> <p data-id="p2" style="font-size:10.5pt; text-indent:2em;"> 营收同比增长<b>23%</b>。 </p> 两个元素的字符串相等,就代表它们在语义上完全相同。 没有CSS级联干扰,变更检测退化为简单的字符串比较。 每个元素上的data-id是手术的定位标记。docx转HTML时,转换器按顺序为每个body元素分配ID,同时建立一张映射表:data-id → 原始document.xml中对应XML节点的(Start, End)索引范围。这张映射表是后续精准定位的坐标系。 ...