<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>RNN on XEDCZQ的博客</title><link>https://xedczq.cn/tags/rnn/</link><description>Recent content in RNN on XEDCZQ的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 04 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://xedczq.cn/tags/rnn/index.xml" rel="self" type="application/rss+xml"/><item><title>RNN 循环神经网络学习笔记</title><link>https://xedczq.cn/post/rnn/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0800</pubDate><guid>https://xedczq.cn/post/rnn/</guid><description>&lt;img src="https://xedczq.cn/img/rnn/diags.jpeg" alt="Featured image of post RNN 循环神经网络学习笔记" /&gt;&lt;h1 id="rnn-循环神经网络学习笔记"&gt;&lt;a href="#rnn-%e5%be%aa%e7%8e%af%e7%a5%9e%e7%bb%8f%e7%bd%91%e7%bb%9c%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0" class="header-anchor"&gt;&lt;/a&gt;RNN 循环神经网络学习笔记
&lt;/h1&gt;&lt;p&gt;本文是学习 Andrej Karpathy 经典博客 &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt; 时整理的中文笔记。原文发表于 2015 年，重点用字符级语言模型展示 RNN/LSTM 为什么能从原始文本中学到拼写、格式、结构、局部语法，甚至一些可解释的“状态记忆”。&lt;/p&gt;
&lt;p&gt;这篇笔记的主线是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RNN 为什么适合处理序列&lt;/li&gt;
&lt;li&gt;RNN 的核心公式和计算过程&lt;/li&gt;
&lt;li&gt;字符级语言模型如何训练与采样&lt;/li&gt;
&lt;li&gt;Karpathy 原文中的经典实验说明了什么&lt;/li&gt;
&lt;li&gt;为什么后来 RNN 在主流 NLP 中被 Transformer 取代&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="rnn-解决什么问题"&gt;&lt;a href="#rnn-%e8%a7%a3%e5%86%b3%e4%bb%80%e4%b9%88%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;RNN 解决什么问题
&lt;/h2&gt;&lt;p&gt;普通前馈神经网络通常假设输入和输出是固定长度的向量，例如输入一张图片，输出一个分类结果。但很多真实任务天然是序列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文本：一句话由多个 token 或字符组成&lt;/li&gt;
&lt;li&gt;语音：声音帧按时间排列&lt;/li&gt;
&lt;li&gt;视频：画面帧按时间排列&lt;/li&gt;
&lt;li&gt;翻译：输入一句话，输出另一种语言的一句话&lt;/li&gt;
&lt;li&gt;生成任务：前面生成的内容会影响后面生成什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RNN 的关键思想是：&lt;strong&gt;模型在处理当前输入时，不只看当前输入，还维护一个隐藏状态，用来压缩过去的上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="RNN 处理不同序列任务的模式" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/diags.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。红色表示输入，蓝色表示输出，绿色表示循环状态。它展示了固定输入输出、序列输出、序列输入、序列到序列、同步序列输入输出等常见模式。&lt;/p&gt;
&lt;p&gt;可以把 RNN 理解成一个不断被调用的 &lt;code&gt;step&lt;/code&gt; 函数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;rnn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RNN&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rnn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每调用一次 &lt;code&gt;step(x)&lt;/code&gt;，RNN 会读取当前输入 &lt;code&gt;x_t&lt;/code&gt;，结合之前的隐藏状态 &lt;code&gt;h_{t-1}&lt;/code&gt;，更新出新的隐藏状态 &lt;code&gt;h_t&lt;/code&gt;，并产生当前输出 &lt;code&gt;y_t&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="vanilla-rnn-的核心公式"&gt;&lt;a href="#vanilla-rnn-%e7%9a%84%e6%a0%b8%e5%bf%83%e5%85%ac%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;Vanilla RNN 的核心公式
&lt;/h2&gt;&lt;p&gt;最基础的 RNN 更新公式是：&lt;/p&gt;
$$
h_t = \tanh(W_{hh}h_{t-1} + W_{xh}x_t + b_h)
$$$$
y_t = W_{hy}h_t + b_y
$$&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x_t$：第 $t$ 个时间步的输入&lt;/li&gt;
&lt;li&gt;$h_{t-1}$：上一个时间步的隐藏状态&lt;/li&gt;
&lt;li&gt;$h_t$：当前时间步的隐藏状态&lt;/li&gt;
&lt;li&gt;$W_{xh}$：输入到隐藏状态的权重&lt;/li&gt;
&lt;li&gt;$W_{hh}$：隐藏状态到隐藏状态的循环权重&lt;/li&gt;
&lt;li&gt;$W_{hy}$：隐藏状态到输出的权重&lt;/li&gt;
&lt;li&gt;$\tanh$：非线性激活函数，把值压到 $[-1, 1]$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对应到代码，大致是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RNN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tanh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W_hh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W_xh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W_hy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;学习时最重要的是理解 &lt;code&gt;h&lt;/code&gt; 的意义：它不是手写规则，而是模型在训练中自己学出来的“上下文摘要”。如果输入是文本，&lt;code&gt;h&lt;/code&gt; 可能会携带当前是否在引号内、是否在 URL 内、某个括号是否已打开、前面出现了哪些词等信息。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="为什么-rnn-可以建模上下文"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88-rnn-%e5%8f%af%e4%bb%a5%e5%bb%ba%e6%a8%a1%e4%b8%8a%e4%b8%8b%e6%96%87" class="header-anchor"&gt;&lt;/a&gt;为什么 RNN 可以建模上下文
&lt;/h2&gt;&lt;p&gt;以字符串 &lt;code&gt;hello&lt;/code&gt; 为例。假设词表只有 &lt;code&gt;h, e, l, o&lt;/code&gt; 四个字符，训练时输入可以是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输入：h e l l
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;目标：e l l o
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意第一个 &lt;code&gt;l&lt;/code&gt; 后面的目标是 &lt;code&gt;l&lt;/code&gt;，第二个 &lt;code&gt;l&lt;/code&gt; 后面的目标是 &lt;code&gt;o&lt;/code&gt;。如果模型只看当前字符，两个时间步输入都一样，无法判断下一个字符应该是什么。RNN 必须利用隐藏状态记录“前面已经看到了什么”。&lt;/p&gt;
&lt;p&gt;&lt;img alt="字符级 RNN 预测下一个字符" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/charseq.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。图中模型逐字符读入 &lt;code&gt;hell&lt;/code&gt;，每一步输出对下一个字符的打分，绿色目标表示希望模型提高的正确字符分数。&lt;/p&gt;
&lt;p&gt;训练目标通常是每个时间步的交叉熵损失：&lt;/p&gt;
$$
\mathcal{L} = -\sum_t \log p(x_{t+1}\mid x_{\le t})
$$&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x_{\le t}$ 表示当前位置之前和当前位置的上下文&lt;/li&gt;
&lt;li&gt;$p(x_{t+1}\mid x_{\le t})$ 表示模型基于历史上下文预测下一个字符的概率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;训练完成后，生成文本的流程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给模型一个起始字符或起始文本&lt;/li&gt;
&lt;li&gt;得到下一个字符的概率分布&lt;/li&gt;
&lt;li&gt;从分布里采样一个字符&lt;/li&gt;
&lt;li&gt;把采样出的字符再喂回模型&lt;/li&gt;
&lt;li&gt;重复以上过程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是字符级语言模型最朴素的生成方式。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="bpttrnn-如何训练"&gt;&lt;a href="#bpttrnn-%e5%a6%82%e4%bd%95%e8%ae%ad%e7%bb%83" class="header-anchor"&gt;&lt;/a&gt;BPTT：RNN 如何训练
&lt;/h2&gt;&lt;p&gt;RNN 每个时间步复用同一组参数。训练时会把循环结构按时间展开，然后做反向传播，这叫 &lt;strong&gt;Backpropagation Through Time, BPTT&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果序列很长，完整展开会很贵，所以常用 &lt;strong&gt;Truncated BPTT&lt;/strong&gt;，只往回传播固定长度。例如 Karpathy 原文的 Paul Graham 实验中使用了长度为 100 个字符的截断 BPTT。&lt;/p&gt;
&lt;p&gt;RNN 训练的难点主要来自长链式梯度：&lt;/p&gt;
$$
\frac{\partial \mathcal{L}}{\partial h_{t-k}}
$$&lt;p&gt;需要经过很多次矩阵乘法和非线性函数。当链条很长时，梯度可能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;越传越小：梯度消失，模型难以学习长期依赖&lt;/li&gt;
&lt;li&gt;越传越大：梯度爆炸，训练不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是后来 LSTM、GRU 被大量使用的原因。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="lstm更强的循环单元"&gt;&lt;a href="#lstm%e6%9b%b4%e5%bc%ba%e7%9a%84%e5%be%aa%e7%8e%af%e5%8d%95%e5%85%83" class="header-anchor"&gt;&lt;/a&gt;LSTM：更强的循环单元
&lt;/h2&gt;&lt;p&gt;Karpathy 原文里的实验实际使用的是 LSTM。LSTM 仍然属于 RNN 家族，但它把隐藏状态更新设计得更复杂，引入门控机制，让模型更容易保留或遗忘信息。&lt;/p&gt;
&lt;p&gt;LSTM 的典型组件包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遗忘门：决定旧信息保留多少&lt;/li&gt;
&lt;li&gt;输入门：决定新信息写入多少&lt;/li&gt;
&lt;li&gt;输出门：决定当前状态暴露给输出多少&lt;/li&gt;
&lt;li&gt;细胞状态：提供更稳定的信息通道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直觉上，Vanilla RNN 每一步都把旧状态和新输入混在一起重新压缩，长期信息很容易被覆盖；LSTM 给模型提供了“写入、保留、读取”的机制，所以更适合长序列。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="karpathy-原文中的经典实验"&gt;&lt;a href="#karpathy-%e5%8e%9f%e6%96%87%e4%b8%ad%e7%9a%84%e7%bb%8f%e5%85%b8%e5%ae%9e%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;Karpathy 原文中的经典实验
&lt;/h2&gt;&lt;h3 id="模型从字符中学会结构"&gt;&lt;a href="#%e6%a8%a1%e5%9e%8b%e4%bb%8e%e5%ad%97%e7%ac%a6%e4%b8%ad%e5%ad%a6%e4%bc%9a%e7%bb%93%e6%9e%84" class="header-anchor"&gt;&lt;/a&gt;模型从字符中学会结构
&lt;/h3&gt;&lt;p&gt;原文展示了把 RNN/LSTM 训练在不同文本上的效果，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paul Graham 文章&lt;/li&gt;
&lt;li&gt;Shakespeare 剧本&lt;/li&gt;
&lt;li&gt;Wikipedia Markdown/XML&lt;/li&gt;
&lt;li&gt;代数几何 LaTeX&lt;/li&gt;
&lt;li&gt;Linux 源码&lt;/li&gt;
&lt;li&gt;婴儿名字列表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些实验的共同点是：模型没有显式的词典、语法规则、Markdown 规则、XML 树规则或 C 语言规则，只是在做“预测下一个字符”。但训练后，它能生成看起来像原数据分布的内容。&lt;/p&gt;
&lt;p&gt;这说明：&lt;strong&gt;下一个字符预测看似简单，实际会倒逼模型学习多层结构。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符层：拼写、空格、标点&lt;/li&gt;
&lt;li&gt;词层：常见词、名字、变量名&lt;/li&gt;
&lt;li&gt;句法层：引号、括号、缩进、标签闭合&lt;/li&gt;
&lt;li&gt;风格层：莎士比亚式台词、维基百科式条目、源码注释&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="训练过程中的能力演化"&gt;&lt;a href="#%e8%ae%ad%e7%bb%83%e8%bf%87%e7%a8%8b%e4%b8%ad%e7%9a%84%e8%83%bd%e5%8a%9b%e6%bc%94%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;训练过程中的能力演化
&lt;/h3&gt;&lt;p&gt;Karpathy 用《战争与和平》做例子，展示了采样文本随训练迭代逐步变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;早期：几乎是随机字符，但开始出现空格&lt;/li&gt;
&lt;li&gt;中期：出现短词、句号、引号等局部结构&lt;/li&gt;
&lt;li&gt;后期：出现更像英文的单词、名字和句子形式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这给我的理解是：RNN 不是一下子学会“语言”，而是先学最局部、最高频的模式，再逐渐形成更长范围的依赖。&lt;/p&gt;
&lt;h3 id="隐藏单元学出了可解释状态"&gt;&lt;a href="#%e9%9a%90%e8%97%8f%e5%8d%95%e5%85%83%e5%ad%a6%e5%87%ba%e4%ba%86%e5%8f%af%e8%a7%a3%e9%87%8a%e7%8a%b6%e6%80%81" class="header-anchor"&gt;&lt;/a&gt;隐藏单元学出了可解释状态
&lt;/h3&gt;&lt;p&gt;原文最经典的部分之一，是可视化 LSTM 隐藏单元的激活。某些神经元会在 URL 内激活，某些会在 &lt;code&gt;[[...]]&lt;/code&gt; 这类 Markdown 链接环境中激活，还有一些神经元像是在跟踪引号区域。&lt;/p&gt;
&lt;p&gt;&lt;img alt="LSTM 神经元对 URL 区域的激活" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/under1.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。该图展示了一个隐藏单元在 URL 区域明显激活，说明模型可能学到了“当前是否处在 URL 中”的内部状态。&lt;/p&gt;
&lt;p&gt;&lt;img alt="LSTM 神经元对 Markdown 链接区域的激活" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/under2.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。该图展示了隐藏单元对 &lt;code&gt;[[...]]&lt;/code&gt; Markdown 环境的响应。&lt;/p&gt;
&lt;p&gt;&lt;img alt="更压缩的神经元激活可视化" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/pane1.png"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。这类可视化说明，一部分隐藏单元会学出人类可以解释的状态检测功能。&lt;/p&gt;
&lt;p&gt;这里的重点不是说每个神经元都有明确语义，而是说明端到端训练可以让模型自己发现对任务有用的中间状态。对于“预测下一个字符”来说，知道自己是否在 URL、括号、引号中，确实会提高预测准确率。&lt;/p&gt;
&lt;h3 id="rnn-也能处理非传统序列任务"&gt;&lt;a href="#rnn-%e4%b9%9f%e8%83%bd%e5%a4%84%e7%90%86%e9%9d%9e%e4%bc%a0%e7%bb%9f%e5%ba%8f%e5%88%97%e4%bb%bb%e5%8a%a1" class="header-anchor"&gt;&lt;/a&gt;RNN 也能处理非传统序列任务
&lt;/h3&gt;&lt;p&gt;Karpathy 原文还提到，即使数据本身不是序列，也可以把处理过程设计成序列。例如模型可以一步步移动注意力读取图片，或者一步步在画布上生成图像。&lt;/p&gt;
&lt;p&gt;&lt;img alt="RNN 逐步读取门牌数字" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/house_read.gif"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。左图相关实验来自 Recurrent Models of Visual Attention。&lt;/p&gt;
&lt;p&gt;&lt;img alt="RNN 逐步生成门牌数字" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xedczq.cn/img/rnn/house_generate.gif"&gt;&lt;/p&gt;
&lt;p&gt;图源：Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;。右图相关实验来自 DRAW: A Recurrent Neural Network For Image Generation。&lt;/p&gt;
&lt;p&gt;这给了一个重要视角：RNN 不只是“处理序列数据”，也可以表示“顺序执行的计算过程”。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="采样温度生成结果为什么会变"&gt;&lt;a href="#%e9%87%87%e6%a0%b7%e6%b8%a9%e5%ba%a6%e7%94%9f%e6%88%90%e7%bb%93%e6%9e%9c%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e5%8f%98" class="header-anchor"&gt;&lt;/a&gt;采样温度：生成结果为什么会变
&lt;/h2&gt;&lt;p&gt;字符级语言模型输出的是下一个字符的概率分布。采样时常用温度系数调整分布：&lt;/p&gt;
$$
p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)}
$$&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$z_i$ 是第 $i$ 个字符的 logit&lt;/li&gt;
&lt;li&gt;$T$ 是温度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;温度的影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T &amp;lt; 1$：分布更尖锐，模型更保守，更容易重复高概率模式&lt;/li&gt;
&lt;li&gt;$T = 1$：正常采样&lt;/li&gt;
&lt;li&gt;$T &amp;gt; 1$：分布更平坦，输出更多样，但错误也更多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以生成模型的“创造性”和“稳定性”经常是一个权衡。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="rnn-的优点"&gt;&lt;a href="#rnn-%e7%9a%84%e4%bc%98%e7%82%b9" class="header-anchor"&gt;&lt;/a&gt;RNN 的优点
&lt;/h2&gt;&lt;p&gt;RNN 的优势可以概括为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;天然适合流式输入：数据一个时间步一个时间步到来时，RNN 可以持续更新状态&lt;/li&gt;
&lt;li&gt;参数共享：同一个 &lt;code&gt;step&lt;/code&gt; 函数复用在任意长度序列上&lt;/li&gt;
&lt;li&gt;状态压缩：隐藏状态可以作为过去上下文的摘要&lt;/li&gt;
&lt;li&gt;生成直觉简单：预测下一个 token，再把输出喂回模型&lt;/li&gt;
&lt;li&gt;对小模型和特定时序任务仍有价值：例如传感器序列、实时语音、边缘设备上的低延迟任务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="rnn-的局限"&gt;&lt;a href="#rnn-%e7%9a%84%e5%b1%80%e9%99%90" class="header-anchor"&gt;&lt;/a&gt;RNN 的局限
&lt;/h2&gt;&lt;p&gt;RNN 的主要问题也很清楚：&lt;/p&gt;
&lt;h3 id="序列计算难并行"&gt;&lt;a href="#%e5%ba%8f%e5%88%97%e8%ae%a1%e7%ae%97%e9%9a%be%e5%b9%b6%e8%a1%8c" class="header-anchor"&gt;&lt;/a&gt;序列计算难并行
&lt;/h3&gt;&lt;p&gt;RNN 必须先算出 $h_{t-1}$，才能计算 $h_t$。这意味着一个序列内部的时间步很难完全并行。&lt;/p&gt;
&lt;p&gt;对于短序列影响不大，但在大规模语言模型训练中，数据量和模型规模都很大，无法充分利用 GPU/TPU 并行能力会成为核心瓶颈。&lt;/p&gt;
&lt;h3 id="长距离依赖困难"&gt;&lt;a href="#%e9%95%bf%e8%b7%9d%e7%a6%bb%e4%be%9d%e8%b5%96%e5%9b%b0%e9%9a%be" class="header-anchor"&gt;&lt;/a&gt;长距离依赖困难
&lt;/h3&gt;&lt;p&gt;理论上，隐藏状态可以携带所有历史信息；实践中，固定长度的向量很难无损压缩长上下文。越早的信息经过越多次状态更新，越容易被覆盖或衰减。&lt;/p&gt;
&lt;p&gt;LSTM/GRU 缓解了这个问题，但没有彻底解决。&lt;/p&gt;
&lt;h3 id="信息通路太长"&gt;&lt;a href="#%e4%bf%a1%e6%81%af%e9%80%9a%e8%b7%af%e5%a4%aa%e9%95%bf" class="header-anchor"&gt;&lt;/a&gt;信息通路太长
&lt;/h3&gt;&lt;p&gt;如果第 1 个 token 要影响第 1000 个 token，RNN 的信息需要穿过约 1000 次递归更新。路径越长，优化越难，信息越容易损失。&lt;/p&gt;
&lt;h3 id="94-隐藏状态是瓶颈"&gt;&lt;a href="#94-%e9%9a%90%e8%97%8f%e7%8a%b6%e6%80%81%e6%98%af%e7%93%b6%e9%a2%88" class="header-anchor"&gt;&lt;/a&gt;9.4 隐藏状态是瓶颈
&lt;/h3&gt;&lt;p&gt;RNN 把过去压缩进一个隐藏向量。这个向量既要存储上下文，又要参与下一步计算。Karpathy 原文在展望部分也提到，RNN 会把表示容量和每步计算量耦合在一起：隐藏状态越大，每一步矩阵乘法成本越高。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="为什么-rnn-被-transformer-取代"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88-rnn-%e8%a2%ab-transformer-%e5%8f%96%e4%bb%a3" class="header-anchor"&gt;&lt;/a&gt;为什么 RNN 被 Transformer 取代
&lt;/h2&gt;&lt;p&gt;Transformer 不是因为“RNN 完全没用”才取代它，而是因为在大规模 NLP 任务上，Transformer 的工程特性和建模能力更适合扩展。&lt;/p&gt;
&lt;h3 id="transformer-更容易并行训练"&gt;&lt;a href="#transformer-%e6%9b%b4%e5%ae%b9%e6%98%93%e5%b9%b6%e8%a1%8c%e8%ae%ad%e7%bb%83" class="header-anchor"&gt;&lt;/a&gt;Transformer 更容易并行训练
&lt;/h3&gt;&lt;p&gt;RNN 按时间步递推：&lt;/p&gt;
$$
h_t = f(h_{t-1}, x_t)
$$&lt;p&gt;Transformer 的 self-attention 可以在同一层里同时计算序列中所有位置之间的关系。训练时，一个 batch 内的 token 表示可以大量矩阵化并行。&lt;/p&gt;
&lt;p&gt;这正是 &lt;a class="link" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"
 &gt;Attention Is All You Need&lt;/a&gt; 的核心动机之一：去掉 recurrence 和 convolution，仅使用 attention 构建序列转导模型，从而提高并行化程度并减少训练时间。&lt;/p&gt;
&lt;h3 id="长距离依赖路径更短"&gt;&lt;a href="#%e9%95%bf%e8%b7%9d%e7%a6%bb%e4%be%9d%e8%b5%96%e8%b7%af%e5%be%84%e6%9b%b4%e7%9f%ad" class="header-anchor"&gt;&lt;/a&gt;长距离依赖路径更短
&lt;/h3&gt;&lt;p&gt;在 RNN 中，远距离 token 之间的信息需要经过很多时间步传递。Transformer 的 self-attention 让任意两个位置可以在一层内直接建立联系。&lt;/p&gt;
&lt;p&gt;可以粗略对比：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;模型&lt;/th&gt;
 &lt;th&gt;两个远距离 token 的信息路径&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;RNN&lt;/td&gt;
 &lt;td&gt;$O(n)$&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CNN&lt;/td&gt;
 &lt;td&gt;取决于卷积层数和感受野&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Transformer self-attention&lt;/td&gt;
 &lt;td&gt;$O(1)$&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;路径更短通常意味着更容易学习长距离依赖。&lt;/p&gt;
&lt;h3 id="attention-显式访问上下文"&gt;&lt;a href="#attention-%e6%98%be%e5%bc%8f%e8%ae%bf%e9%97%ae%e4%b8%8a%e4%b8%8b%e6%96%87" class="header-anchor"&gt;&lt;/a&gt;Attention 显式访问上下文
&lt;/h3&gt;&lt;p&gt;RNN 依赖隐藏状态压缩历史，而 Transformer 的 attention 会为当前位置动态读取其它位置的信息：&lt;/p&gt;
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
$$&lt;p&gt;这相当于让模型在生成每个表示时，直接根据相关性从上下文中取信息，而不是完全依赖一个递推压缩状态。&lt;/p&gt;
&lt;h3 id="transformer-更符合大模型扩展规律"&gt;&lt;a href="#transformer-%e6%9b%b4%e7%ac%a6%e5%90%88%e5%a4%a7%e6%a8%a1%e5%9e%8b%e6%89%a9%e5%b1%95%e8%a7%84%e5%be%8b" class="header-anchor"&gt;&lt;/a&gt;Transformer 更符合大模型扩展规律
&lt;/h3&gt;&lt;p&gt;现代大语言模型依赖大数据、大参数、大算力。Transformer 的矩阵乘法密集、并行友好，能更好地吃满硬件；RNN 的时间步依赖限制了吞吐。&lt;/p&gt;
&lt;p&gt;所以在大规模预训练语言模型时代，Transformer 的优势不仅是算法效果，也包括硬件效率、训练稳定性、生态工具和可扩展性。&lt;/p&gt;
&lt;h3 id="105-但-rnn-没有完全消失"&gt;&lt;a href="#105-%e4%bd%86-rnn-%e6%b2%a1%e6%9c%89%e5%ae%8c%e5%85%a8%e6%b6%88%e5%a4%b1" class="header-anchor"&gt;&lt;/a&gt;10.5 但 RNN 没有完全消失
&lt;/h3&gt;&lt;p&gt;RNN 在一些场景仍有价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流式推理：输入持续到来，不想反复重算全部上下文&lt;/li&gt;
&lt;li&gt;低延迟边缘任务：小模型、固定状态、推理成本可控&lt;/li&gt;
&lt;li&gt;时间序列任务：某些传感器或控制任务不一定需要完整 self-attention&lt;/li&gt;
&lt;li&gt;新架构研究：一些状态空间模型、线性注意力、RWKV 类模型又重新吸收了 recurrence 的思想&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以更准确的说法是：&lt;strong&gt;Transformer 在主流 NLP 和大模型训练中取代了传统 RNN/LSTM，但“循环状态”这个思想仍然在很多新架构中继续存在。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="学习小结"&gt;&lt;a href="#%e5%ad%a6%e4%b9%a0%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;学习小结
&lt;/h2&gt;&lt;p&gt;RNN 的核心不是复杂公式，而是一个简单但强大的抽象：&lt;strong&gt;用同一个函数反复处理序列，并用隐藏状态携带过去的信息。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Karpathy 的文章经典之处在于，它没有先堆很多理论，而是用字符级生成实验说明：只要训练目标设计得足够通用，模型会为了完成预测任务，自发学到拼写、格式、括号、引号、URL、代码结构等多层模式。&lt;/p&gt;
&lt;p&gt;但 RNN 的递推结构也决定了它在大规模训练中有天然瓶颈：难并行、长依赖难优化、隐藏状态压缩能力有限。Transformer 通过 self-attention 让序列位置之间直接交互，并大幅提升并行训练能力，因此成为现代 NLP 和大语言模型的主流架构。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="学习来源"&gt;&lt;a href="#%e5%ad%a6%e4%b9%a0%e6%9d%a5%e6%ba%90" class="header-anchor"&gt;&lt;/a&gt;学习来源
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Andrej Karpathy, &lt;a class="link" href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/" target="_blank" rel="noopener"
 &gt;The Unreasonable Effectiveness of Recurrent Neural Networks&lt;/a&gt;, 2015-05-21&lt;/li&gt;
&lt;li&gt;Andrej Karpathy, &lt;a class="link" href="https://github.com/karpathy/char-rnn" target="_blank" rel="noopener"
 &gt;char-rnn GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Andrej Karpathy, &lt;a class="link" href="https://gist.github.com/karpathy/d4dee566867f8291f086" target="_blank" rel="noopener"
 &gt;Minimal character-level language model with a Vanilla Recurrent Neural Network, in Python/numpy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ashish Vaswani et al., &lt;a class="link" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"
 &gt;Attention Is All You Need&lt;/a&gt;, 2017&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>