05_RNN循环神经网络
1 自然语言概述
自然语言处理 (Natural Language Processing,NLP)的目标就是让计算机理解人类语言,发展至今大致可以分为下面几个阶段:
- 1950年代 - 1970年代:基于规则的系统,如 ELIZA 聊天机器人。
- 1980年代 - 1990年代:基于统计方法,如 N-gram、隐马尔可夫(HMM)和最大熵模型。
- 1990年代 - 2010年代:基于机器学习阶段,如逻辑回归、SVM、决策树、条件随机场(CRF)等。
- 2010年代至今:基于深度学习阶段,如 RNN、LSTM、GRU、Transformer 等,取代了机器学习阶段复杂的手工特征工程。
2 词嵌入层
词向量是用于表示单词语义的向量,也可以看作词的特征向量。将词映射到向量的技术称为词嵌入(Word Embedding)。
- 对文本进行分词,根据需要进行清洗和标准化。
- 构建词表(Vocabulary),每个词对应一个索引。
- 构建词嵌入矩阵(Embedding Matrix),将词索引映射到对应的词向量。
使用 nn.Embedding
初始化词嵌入矩阵,刚开始是随机的,但是会根据训练数据进行更新。
1 | |
中文可以使用 jieba 进行分词,然后使用 embedding 进行词嵌入。
1 | |
/opt/miniconda3/envs/dl/lib/python3.12/site-packages/jieba/_compat.py:18: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/7s/cqjc9twd1ngbl_f7129vv6bc0000gn/T/jieba.cache
Loading model cost 0.262 seconds.
Prefix dict has been built successfully.
vocab: ['我', '爱', '学习', '自然语言', '处理', '机器']
word2idx: {'我': 0, '爱': 1, '学习': 2, '自然语言': 3, '处理': 4, '机器': 5}
0:我 [ 0.4493519 0.5169413 1.2761555 -0.8370651]
1:爱 [ 0.34803388 -1.4246558 -0.34050012 1.2940743 ]
2:学习 [-0.6461887 0.34460533 -0.17983264 1.2675058 ]
3:自然语言 [-0.93329114 0.03108138 -0.53790736 0.850731 ]
4:处理 [-0.63556343 1.4519262 -2.4802206 0.20678595]
5:机器 [ 0.76517206 -1.9815451 -0.8956107 0.71585 ]
3 循环网络层
在这之前的神经网络都是前馈(feedforward)型神经网络,也就是传播方向是单向的,虽然结构简单,但是无法学习时序数据的本质关系。而语言是具有时序关系的,如果打乱文本顺序,那么语义将会发生改变。于是循环神经网络(Recurrent Neural Network,RNN)应运而生。
循环神经网络(RNN)是一种具有隐藏状态并允许将过去输出用作输入的神经网络。
- 输入层:接收序列数据。
- 隐藏层:循环神经网络的核心,隐藏层由一组循环连接的神经元组成。每个神经元不仅处理当前时间步的输入,还结合前一个时间步的隐藏状态信息。这种状态记录了网络对过去输入的记忆,使其能够理解当前元素的上下文。
- 循环连接:循环神经网络的关键,使得网络能够拥有时间维度上的记忆。通过将上一个时间步的隐藏状态传递到当前时间步,利用历史信息来影响当前的输出。
- ht = f(Whht − 1 + Wxxt + bh)
- 当前时间步输入 xt 和权重 Wx 、上一个时间步的输出 ht − 1 和权重 Wh,执行完求积后,加上偏置项 bh ,经过激活函数 f,得到当前时间步的隐藏状态 ht 。
- 激活函数:激活函数对当前时间步的输入和前一个时间步的隐藏状态组合进行转换,以引入非线性特征,常用
tanh,或ReLU激活函数。 - 输出层:输出层根据处理后的信息生成网络的预测结果,具体结构取决于具体任务。在语言模型中,它可能会预测序列中的下一个词。

在中间层中,可以使用多个隐藏层,每个隐藏层都有其自身的激活函数、权重和偏置。在一个隐藏层内,会循环多个时间步运行,所有时间步使用相同的权重。隐藏状态会递归地使用当前输入和前一个隐藏状态进行更新。
使用 nn.RNN 构建 RNN 层。
1 | |
由于第一个时间步没有上一个时间步,所以需要传入初始的隐状态 h0,当然也可以省略此参数,默认为全零向量。
1 | |
input shape: [3, 2, 4] h0 shape: [2, 2, 5]
output shape: [3, 2, 5] hn shape: [2, 2, 5]
这里只讨论最简单的单向 RNN,不考虑双向 RNN。
input(3, 2, 4) [seq_len, batch_size, input_size]
- seq_len(输入序列的长度):也就是时间步,对应每次处理多少个 token,在 RNN 中循环迭代的次数,比如 [‘自然’, ‘语言’, ‘处理’] 作为一个样本输入。
- batch_size(批量大小):也就是每次处理多少个样本,2 个样本就是两个序列为 3 的输入。
- input_size(输入大小):也就是输入的维度,每个 token 的词向量维度为 4.
output(3, 2, 5) [seq_len, batch_size, hidden_size]
- seq_len 和 batch_size 和输入相同。
- hidden_size(隐藏层大小):也就是输出的词向量维度,对应隐藏层神经元的个数。
h0(2, 2, 5) [num_layers, batch_size, hidden_size]
- num_layers(层数):初始隐状态的数量和层数相关。
hn 就是下一层的初始隐状态,所以和 h0 维度完全一致。
1 | |
output:
tensor([[[ 0.7895, 0.2584, 0.6769, -0.7742, -0.3992],
[-0.1474, 0.2047, 0.6347, -0.5527, 0.4279]],
[[-0.4130, 0.1993, 0.1084, -0.2995, 0.4788],
[ 0.1983, 0.3894, 0.3309, -0.6349, 0.0103]],
[[ 0.3517, -0.1504, 0.1967, -0.5610, 0.2852],
[-0.2731, 0.2325, 0.1598, -0.6785, 0.2983]]],
grad_fn=<StackBackward0>)
hn:
tensor([[[ 0.6711, -0.7225, -0.6723, 0.5355, 0.5065],
[-0.4187, -0.4725, -0.7022, 0.6706, 0.3087]],
[[ 0.3517, -0.1504, 0.1967, -0.5610, 0.2852],
[-0.2731, 0.2325, 0.1598, -0.6785, 0.2983]]],
grad_fn=<StackBackward0>)
input[3, 2, 4]:3 个时间步,2 个批次,每个输入单词是 4 维词向量。h0[2, 2, 5]:2 层 RNN,2 个批次,每层 5 个神经元都需要有初始隐状态输入。output[3, 2, 5]:3 个时间步,2 个批次,每个单词变成了 5 维词向量。hn[2, 2, 5]:2 层 RNN,2 个批次,每层 5 个神经元都需有输出。
其中 output 对应的是最后一层 RNN 的所有时间步输出,而 hn 对应的是每一层最后一个时间步的输出,一个是时间上的表示,一个是空间上的表示。具体使用哪个,需要根据具体任务来定。
不使用
batch_first=True,使用输入参数seq_len=3, batch_size=2, input_size=4,对应到实际场景
[["自然", "语言", "处理"], ["人", "喜欢", "狗"]],有如下表示:

建议设置
batch_first=True,input的维度顺序变为[batch_size, seq_len, input_size],output的维度顺序变为[batch_size, seq_len, hidden_size]。

经过隐藏层之后输出只改变了每个词向量的维度,相当于改变了词向量的空间大小,由神经网络抽取了词的其他维度信息。
4 自定义 Dataset
我们希望输入一个序列 x,输出一个序列 y 作为目标,需要自定义数据集。
1 | |
1 | |
[0, 1, 3, 4, 0, 1, 5, 2]
1 | |
tensor([0, 1, 3]) tensor([1, 3, 4])
tensor([1, 3, 4]) tensor([3, 4, 0])
tensor([3, 4, 0]) tensor([4, 0, 1])
tensor([4, 0, 1]) tensor([0, 1, 5])
tensor([0, 1, 5]) tensor([1, 5, 2])
1 | |
tensor([[4, 0, 1],
[1, 3, 4]]) tensor([[0, 1, 5],
[3, 4, 0]])
tensor([[3, 4, 0],
[0, 1, 3]]) tensor([[4, 0, 1],
[1, 3, 4]])
tensor([[0, 1, 5]]) tensor([[1, 5, 2]])
dataset:
使用 for i in dataset 遍历数据集触发
__getitem__(self, idx) 方法,idx
默认会一直累加,直到遇到抛出异常停止。如果传入的数据不会发生索引越界报错,便会陷入无限循环,此时需要自定义异常抛出,或者使用
for i in range(len(dataset)) 的方式遍历数据集。
dataloader:
使用 for i in dataloader 遍历数据集触发
__getitem__(self, idx) 方法,需要搭配
__len()__ 方法的重写, idx 的范围会被限制在
[0, len(dataset) - 1] 中。