深入浅出循环神经网络:RNN、LSTM、GRU 原理与 PyTorch 实战详解
文章目录
循环神经网络(RNN)是深度学习中处理序列数据的基石,也是走进自然语言处理(NLP)领域的第一道门槛。本文将从 RNN 的基本概念出发,逐层拆解传统 RNN 的内部结构与计算公式,再深入到 LSTM、GRU 两大主流变体的门控原理,并全程配套 PyTorch API 实战代码,帮助你在一次阅读中建立完整的循环神经网络知识体系。
本文主要内容:
- 认识 RNN:概念、结构特点、作用与分类;
- 传统 RNN:内部结构、数学公式、
nn.RNNAPI 实战; - LSTM:三门一态结构、门控公式、双向 Bi-LSTM、
nn.LSTMAPI 实战; - GRU:更新门与重置门、门控公式、
nn.GRUAPI 实战; - RNN / LSTM / GRU 三大模型横向对比。
一、认识 RNN 模型
1.1 什么是 RNN
循环神经网络(Recurrent Neural Network,简称 RNN) 是一类专门处理序列数据的神经网络:
- 一般以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也以序列形式进行输出;
- RNN 的网络结构由输入层、隐藏层、输出层三部分组成。
与前馈神经网络"信息只从输入单向流向输出"不同,RNN 的隐藏层会把隐藏状态"回传"给自己,这是它最本质的特征。
1.2 为什么叫"循环"神经网络
RNN 之所以被称为"循环"神经网络,关键在于它的信息传递方式:
- 上一时间步的隐藏层输出,会作为下一个时间步的隐藏层输入,信息就这样沿着时间维度"循环"流动;
- 把 RNN 按时间步逐个展开,就得到了如下的单层链式结构:
在展开图中,每个节点表示对应时间步的隐藏状态:每个时间步既接收当前时刻的数据 x(t),也接收上一时间步传来的隐藏状态 h(t-1),并计算出新的隐藏状态 h(t) 继续向后传递。
1.3 RNN 网络结构特点
- 网络由输入层、隐藏层、输出层组成;上一时间步的隐藏层输出作为下一个时间步的隐藏层输入;
- 每个时间步的输入有 2 个:数据端输入(当前时间步的输入数据)、隐藏层输入(上一时间步传来的隐藏状态);
- 每个时间步的输出有 2 个:数据端输出、本时间步的隐藏层输出。
1.4 RNN 模型的作用
- RNN 结构能够连续地接收序列数据并逐步提取特征。例如人类的语言、语音这类天然具有先后顺序的数据,特别适合用 RNN 来处理;
- RNN 被广泛应用于 NLP 领域的各项任务,如文本分类、情感分析、意图识别、机器翻译等。
🌰 举个例子:用户意图识别
- 用户输入 “What time is it ?”,先进行分词。RNN 是一个时间步一个时间步地接收数据的,每次只处理一个单词;
- 首先将单词 “What” 输入 RNN,产生输出 O1;
- 接着将单词 “time” 输入 RNN,此时 RNN 不仅利用 “time” 本身,还会结合上一时间步的隐藏层输出 O1 一起产生输出 O2;
- 重复上述步骤,直到处理完序列中的所有单词;
- 最后,对最终的隐藏层输出 O5 进行处理以解析用户意图。如果意图识别共有 18 个类别,就让 O5 再经过一个 softmax 层转换为各类别的概率分布,从而完成意图分类。
1.5 RNN 模型的分类
可以从两个角度对 RNN 模型进行分类。
角度一:按输入和输出的形式分类
| 分类 | 说明 | 典型用途 |
|---|---|---|
| N vs N | 输入 N 个序列,输出 N 个序列 | 写诗、写对联、固定场景表达 |
| N vs 1 | 输入 N 个序列,输出 1 个值 | 情感分类、意图识别 |
| 1 vs N | 输入 1 个值,输出 N 个序列 | 看图说话 |
| N vs M | 输入 N 个序列,输出 M 个序列 | 机器翻译、语音转换、文本生成、文本摘要 |
- N vs N:RNN 最基础的结构形式,输入与输出序列等长,常用于生成等长诗句;
- N vs 1:输入是一个序列,输出是一个单独的值而非序列,通常需要用 sigmoid 或 softmax 对输出进行处理,常用于文本分类;
- 1 vs N:输入是一个值,让该输入作用于每一次的输出之上,常用于"看图说话"(图像生成文字)类任务;
- N vs M:即经典的 Seq2Seq 架构,最早应用于机器翻译,由编码器、解码器和中间语义张量 C 组成,可用于机器翻译、阅读理解、文本摘要等任务。
角度二:按 RNN 的内部构造分类
按内部结构从简单到复杂,常见的 RNN 家族成员有:
- 传统 RNN
- LSTM
- Bi-LSTM(双向 LSTM)
- GRU
- Bi-GRU(双向 GRU)
1.6 本节小结
Q1:RNN 的概念和结构特点是什么?
- 概念:Recurrent Neural Network(循环神经网络)。输入数据为序列,输出一般也是序列,通过内部结构设计捕获序列特征;
- 特点:之所以叫"循环",是因为上一时间步的隐藏层输出会作为下一时间步的隐藏层输入。网络由输入层、隐藏层、输出层组成;每个时间步有 2 个输入(数据端输入、上一时间步的隐藏层输入)和 2 个输出(数据端输出、本时间步的隐藏层输出)。
Q2:RNN 模型如何分类?
- 输入输出角度:N vs N、N vs 1、1 vs N、N vs M;
- 内部结构角度:传统 RNN、LSTM、Bi-LSTM、GRU、Bi-GRU。
📝 随堂练习(单选)
关于循环神经网络 RNN 的结构,请问为什么它叫"循环神经网络"?"循环"代表什么?( )
- A. 因为 RNN 也属于神经网络
- B. 因为 RNN 有输入层、隐藏层、输出层等结构
- C. 因为 RNN 的输入层可以支持文本序列数据的特征抽取
- D. 因为 RNN 上一时间步的隐藏层输出会作为下一时间步的隐藏层输入
正确答案:D
A 与"循环"本身没有关系;B 描述的是普通神经网络的共性;C 说的是 RNN 的作用,而不是"循环"名称的由来;只有 D 准确描述了信息沿时间步循环传递的本质。
二、传统 RNN 模型
2.1 RNN 内部结构分析
传统 RNN 每个时间步的内部计算可以用下图概括:
具体可以概括为三步:
- 两个输入——上一时间步的隐藏状态
h(t-1)和当前时间步的输入x(t)——被"融合"到一起; - 融合后的结果经过一个全连接层(线性层),再使用 tanh 作为激活函数;
- 输出新的隐藏状态
h(t),并作为下一时间步的隐藏层输入继续传递下去。
2.2 RNN 内部计算公式
- PyTorch 框架 / 工业界视角:隐藏层数据与内部隐藏层参数矩阵
Whh做矩阵乘法,时间步输入数据与内部数据矩阵Wih做矩阵乘法; - 学术界论文视角:用矩阵参数形式统一表达,经过线性变换与激活层。
两种表述对应同一个核心公式:
h(t) = tanh( Whh · h(t-1) + Wih · x(t) + b )
即用隐藏层数据乘内部隐藏层矩阵 Whh,用时间步数据乘内部数据矩阵 Wih,两者相加后经激活函数得到新的隐藏状态。
2.3 激活函数 tanh 的作用
tanh 函数用于调节流经网络的数值,把值压缩到 -1 和 1 之间,使隐藏状态在循环传递过程中保持数值稳定,不至于被无限放大。
2.4 PyTorch API 实战
RNN 可用于提取文本序列特征。在 torch.nn 工具包中,通过 torch.nn.RNN 即可调用。
案例 1:基础用法(看懂三组关键数字)
def dm01_rnn_for_base():
# 第1个参数:输入数据尺寸(特征维度)
# 第2个参数:输出数据尺寸 6 个特征(可理解为 6 个神经元)
# 第3个参数:隐藏层个数
rnn = nn.RNN(5, 6, 1) # A
# 数据 input
# 第1个参数:序列长度(单词个数)1
# 第2个参数:批次数 batch_size
# 第3个参数:数据的特征维度
input = torch.randn(1, 3, 5) # B
# 数据 hidden
# 第1个参数:模型的隐藏层个数 1
# 第2个参数:批次数 3
# 第3个参数:模型输出神经元个数 6
h0 = torch.randn(1, 3, 6) # C
# 给模型送数据
# 数据形状 input[1,3,5] h0[1,3,6] ---> output[1,3,6], hn[1,3,6]
output, hn = rnn(input, h0)
# 打印输出
print('output-->', output.shape, output)
print('hn--->', hn.shape, hn)
运行结果:
output--> torch.Size([1, 3, 6])
hn---> torch.Size([1, 3, 6])
案例 2:调整输入序列长度
def dm03_rnn_for_sequencelen():
# 第1个参数:输入数据尺寸(特征维度)
# 第2个参数:输出数据尺寸 6 个特征
# 第3个参数:隐藏层个数
rnn = nn.RNN(5, 6, 1) # A
# 数据 input
# 第1个参数:序列长度(单词个数)11
# 第2个参数:批次数
# 第3个参数:数据的特征维度
input = torch.randn(11, 3, 5) # B
# 数据 hidden
h0 = torch.randn(1, 3, 6) # C
output, hn = rnn(input, h0)
print('output-->', output.shape)
print('hn--->', hn.shape)
运行结果:
output--> torch.Size([11, 3, 6])
hn---> torch.Size([1, 3, 6])
可以看到:序列长度从 1 变成 11 后,output 的第 0 维随之变为 11(每个时间步都有一个输出),而 hn 的形状保持不变。
案例 3:调整模型隐藏层个数
def dm05_rnn_for_hiddennum():
rnn = nn.RNN(5, 6, 2) # A 隐藏层个数为 2
input = torch.randn(1, 3, 5) # B
h0 = torch.randn(2, 3, 6) # C 隐藏层个数与模型保持一致,为 2
output, hn = rnn(input, h0)
print('output-->', output.shape, output)
print('hn--->', hn.shape, hn)
运行结果:
output--> torch.Size([1, 3, 6])
hn---> torch.Size([2, 3, 6])
运行结果分析:
- 注 1:当隐藏层个数配置为 1 时,
output和hn的输出是一样的; - 注 2:当隐藏层个数配置为 n 时,
output的结果和最后一个隐藏层的输出是一样的。
原因:RNN 只有 1 个隐藏层时,每个时间步仅有一个隐藏状态;当 RNN 有 n 个隐藏层时,每个时间步会产生 n 层隐藏状态,其中
output保存的是最顶层(最后一层)隐藏层在每个时间步的输出。
2.5 RNN 模型的优缺点
优点:
- 内部结构简单,对计算资源要求低;
- 相比 LSTM / GRU 等变体,模型参数总量少很多;
- 在短序列任务上性能和效果都表现优异。
缺点:
- 长序列文本的特征提取效果差;
- 过长的序列会导致梯度计算异常,发生梯度消失或梯度爆炸。
2.6 本节小结
Q1:说一说 RNN 的内部结构?
两个输入 h(t-1) 和 x(t) 被"融合"到一起,经过全连接层(线性层),再使用 tanh 作为激活函数,输出 h(t) 并作为下一时间步的隐藏层输入继续传递。
Q2:说一说 RNN 模型的数学公式?
h(t) = tanh(Whh · h(t-1) + Wih · x(t) + b)。工业界/PyTorch 视角:隐藏层数据与参数矩阵 Whh 相乘,时间步数据与数据矩阵 Wih 相乘,相加后过激活函数;学术论文中则用统一的矩阵参数形式表示。
Q3:nn.RNN 实战代码中三组关键数字分别代表什么?
rnn = nn.RNN(5, 6, 1):输入数据尺寸、输出数据尺寸、隐藏层个数;input = torch.randn(1, 3, 5):序列长度、批次数、输入数据尺寸;h0 = torch.randn(1, 3, 6):隐藏层个数、批次数、输出数据尺寸(神经元个数)。
Q4:当 RNN 隐藏层个数为 2 时,hn 和 output 的结果有什么特点?
output 的输出结果与最后一个(最顶层)隐藏层的输出一致。
三、LSTM 模型
3.1 LSTM 模型概念
- LSTM(Long Short-Term Memory),也称长短时记忆结构;
- 它是传统 RNN 的变体;
- 与经典 RNN 相比,LSTM 能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象。
LSTM 结构说明:
- 输入:上一时间步的细胞状态
C(t-1)、上一时间步的隐藏状态h(t-1)(连同当前时间步的输入); - 输出:本时间步的细胞状态
C(t)、本时间步的隐藏状态h(t); - 内部包含:遗忘门、输入门、细胞状态、输出门(“三门一态”)。
3.2 LSTM 内部结构:三门一态
先约定两个记号:[h(t-1), x(t)] 表示把上一时间步的隐藏状态与当前时间步的输入拼接成一个向量;⊙ 表示按元素相乘(哈达玛积)。
遗忘门(Forget Gate)
遗忘门作用在上一个时间步的细胞状态之上,决定对上一时间步的信息遗忘多少——哪些旧记忆应该被丢弃,哪些应该被保留:
f(t) = sigmoid( Wf · [h(t-1), x(t)] + bf )
遗忘门的输出 f(t) 落在 [0, 1] 区间:取 0 表示完全遗忘,取 1 表示完全保留。
输入门(Input Gate)
输入门作用在内部细胞状态之上,决定有多少新信息可以流入细胞状态。它由两部分组成:经 sigmoid 变换的门控值(控制"写入多少"),以及经 tanh 变换的候选内容(“写入什么”):
i(t) = sigmoid( Wi · [h(t-1), x(t)] + bi )
Ĉ(t) = tanh( WC · [h(t-1), x(t)] + bC )
更新细胞状态
遗忘门对旧细胞状态的"保留部分"与输入门对新信息的"写入部分"相结合,共同更新得到新的细胞状态:
C(t) = f(t) ⊙ C(t-1) + i(t) ⊙ Ĉ(t)
这一步是 LSTM 长时记忆能力的核心:细胞状态就像一条传送带,信息可以几乎不受干扰地一直流下去,只有门控在每个时间步上做少量的选择性交互。
输出门(Output Gate)
输出门同样作用在细胞状态之上,决定细胞状态的哪一部分被输出为本时间步的隐藏状态 h(t):
o(t) = sigmoid( Wo · [h(t-1), x(t)] + bo )
h(t) = o(t) ⊙ tanh( C(t) )
3.3 双向 Bi-LSTM 模型
- Bi-LSTM 并没有改变 LSTM 本身的任何内部结构;
- 它的做法是:把 “我爱中国” 这样的句子分别从左到右和从右到左各跑一遍 LSTM,再把两个方向得到的结果张量进行拼接,作为最终输出;
- 代价是模型参数和计算复杂度也随之增加了一倍。
这样每个时间步的输出都同时包含了来自"过去"和"未来"两个方向的上下文信息。
3.4 PyTorch API 实战
def dm01_LSTM():
# 第1个参数:输入数据尺寸(特征维度)
# 第2个参数:输出数据尺寸 6 个特征(可理解为 6 个神经元)
# 第3个参数:隐藏层个数
lstm = nn.LSTM(5, 6, 2) # A
# 数据 input
input = torch.randn(1, 3, 5) # B
# 数据 hidden(LSTM 需要两个初始状态:h0 隐藏状态 + c0 细胞状态)
h0 = torch.randn(2, 3, 6) # C
c0 = torch.randn(2, 3, 6)
# 数据形状 input[1,3,5] h0[2,3,6] c0[2,3,6] ---> output[1,3,6], hn[2,3,6], cn[2,3,6]
output, (hn, cn) = lstm(input, (h0, c0))
# 打印输出
print('output-->', output.shape, output)
print('hn--->', hn.shape, hn)
print('cn--->', cn.shape, cn)
运行结果:
output--> torch.Size([1, 3, 6])
hn---> torch.Size([2, 3, 6])
cn---> torch.Size([2, 3, 6])
⚠️ 注意:LSTM 的返回值与 RNN 不同。它返回的是
output, (hn, cn),即隐藏状态和细胞状态两组状态;传参时也要以(h0, c0)元组的形式传入。
3.5 LSTM 模型的优缺点
- 优点:LSTM 的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸。虽然并不能完全杜绝这种现象,但在更长的序列任务上表现明显优于传统 RNN;
- 缺点:由于内部结构相对复杂,在同等算力下训练效率比传统 RNN 低很多。
3.6 本节小结
Q1:说一说 LSTM 模型的结构?
LSTM(Long Short-Term Memory)即长短时记忆结构,是传统 RNN 的变体。与经典 RNN 相比,它能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸。LSTM 内部比 RNN 复杂得多,包含"三门一态":遗忘门、输入门、细胞状态、输出门。
Q2:说一说 LSTM 三个门的工作原理?
- 遗忘门:决定哪些信息应该从细胞状态中被遗忘掉;
- 输入门:控制有多少新信息可以流入细胞状态(cell state);
- 输出门:决定细胞状态的哪一部分被输出为隐藏状态。
📝 随堂练习
阅读下面的程序,请指出错误出现在哪一行?( )
def dm01_LSTM():
lstm = nn.LSTM(5, 6, 2) # A
input = torch.randn(1, 3, 5) # B
h0 = torch.randn(2, 3, 6) # C
c0 = torch.randn(2, 3, 6)
output, hn, cn = lstm(input, h0, c0) # D
print('output-->', output.shape, output)
print('hn--->', hn.shape, hn)
print('cn--->', cn.shape, cn)
点击查看答案解析
正确答案:D
LSTM 的初始状态要以元组形式传入,返回值也是 output 和状态元组两部分,D 行应修改为:
output, (hn, cn) = lstm(input, (h0, c0))
四、GRU 模型
4.1 GRU 模型概念
- GRU(Gated Recurrent Unit),也称门控循环单元结构;
- 它同样是传统 RNN 的变体;
- 与 LSTM 一样,GRU 能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象;
- 结构上它比 LSTM 更简单,比传统 RNN 更复杂,是二者之间的折中选择。
GRU 结构说明:
- 2 个输入:上一时间步的隐藏状态
h(t-1)、当前时间步的输入; - 2 个输出:本时间步的隐藏状态
h(t)(GRU 没有独立的细胞状态,两个输出合二为一); - 内部包含:更新门、重置门两个门控结构。
4.2 GRU 内部结构:两个门
仍沿用 LSTM 一节的记号:[h(t-1), x(t)] 表示拼接,⊙ 表示按元素相乘。
更新门(Update Gate)
- 作用:控制使用多少上一隐藏层的信息——要记忆多少、要依赖多少;
- 门控值
z=0时完全沿用上一隐藏层信息;z=1时完全使用本隐藏层的新信息。
z(t) = sigmoid( Wz · [h(t-1), x(t)] )
重置门(Reset Gate)
- 作用:控制如何把新的输入信息与前面的记忆相结合;
- 相关数值经过变换落到 [0, 1] 区间,形成重置门,决定"遗忘"旧记忆的程度。
r(t) = sigmoid( Wr · [h(t-1), x(t)] )
新旧结合:输出隐藏状态
重置门先决定在计算候选隐藏状态时"带入"多少旧记忆,更新门再决定最终隐藏状态中新旧信息的混合比例:
ĥ(t) = tanh( W · [r(t) ⊙ h(t-1), x(t)] )
h(t) = (1 - z(t)) ⊙ h(t-1) + z(t) ⊙ ĥ(t)
由于 GRU 只有一个状态变量 h,它的参数量和计算量都比 LSTM 更小。
4.3 PyTorch API 实战
def dm01_GRU():
# 第1个参数:输入数据尺寸(特征维度)
# 第2个参数:输出数据尺寸 6 个特征(可理解为 6 个神经元)
# 第3个参数:隐藏层个数
mygru = nn.GRU(5, 6, 2) # A
input = torch.randn(1, 3, 5) # B
h0 = torch.randn(2, 3, 6) # C
# 给模型送数据 input[1,3,5] h0[2,3,6] -> output[1,3,6], hn[2,3,6]
output, hn = mygru(input, h0)
print('output-->', output.shape, output)
print('hn--->', hn.shape, hn)
运行结果:
output--> torch.Size([1, 3, 6])
hn---> torch.Size([2, 3, 6])
可以看到,GRU 的用法和 RNN 几乎一致:只需要传入一个初始隐藏状态 h0,返回 output 和 hn,没有 LSTM 的细胞状态 c。
4.4 GRU 模型的优缺点
- 优点:GRU 与 LSTM 作用相同,在捕捉长序列语义关联时能有效抑制梯度消失或爆炸,效果优于传统 RNN;且计算复杂度比 LSTM 更小,训练更省资源;
- 缺点:GRU 仍然不能完全解决梯度消失问题;同时它继承了 RNN 结构本身的一大弊端——不可并行计算。在数据量和模型体量不断增大的趋势下,这是整个 RNN 家族发展的关键瓶颈。
4.5 本节小结
Q1:说一说 GRU 模型的结构?
GRU(Gated Recurrent Unit)即门控循环单元,是传统 RNN 的变体。它能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸;结构比 LSTM 简单、比 RNN 复杂,内部只有 2 个门:更新门和重置门。
Q2:说一说 GRU 两个门的工作原理?
- 更新门:控制使用多少上一隐藏层的信息,即要记忆多少、依赖多少;
- 重置门:控制如何把新的输入信息与前面的记忆相结合。
📝 随堂练习 1(单选)
在自然语言处理中,通常情况下哪种循环神经网络模型对处理长时序依赖问题效果最好?( )
- A. RNN
- B. LSTM
- C. GRU
正确答案:B
LSTM(长短时记忆网络)在处理长时序依赖问题时,相对于普通 RNN 和 GRU 效果更好。
📝 随堂练习 2(多选)
下列哪个循环神经网络模型具有"门控"机制,可以更好地控制信息的流动?( )
- A. RNN
- B. LSTM
- C. GRU
- D. 都可以
正确答案:B 和 C
LSTM 和 GRU 都具有"门控"机制,可以更好地控制信息的流动,相对于普通 RNN 效果更好;传统 RNN 没有门控结构。
📝 随堂练习 3(多选)
LSTM 中的"门控"单元有哪几种类型?( )
- A. 输入门
- B. 输出门
- C. 遗忘门
- D. 记忆门
正确答案:A、B、C
LSTM 中的门控单元包括输入门、遗忘门、输出门,它们允许模型选择性地更新或忽略内部记忆:
- 遗忘门(Forget Gate):决定哪些信息应该从细胞状态中被遗忘掉;
- 输入门(Input Gate):控制有多少信息可以流入细胞状态(cell state);
- 输出门(Output Gate):决定细胞状态的哪一部分被输出为隐藏状态。
三种门控单元共同作用,使 LSTM 能够在处理序列数据时选择性地更新、遗忘和输出信息,从而更好地控制信息流动与记忆状态。LSTM 中并没有所谓的"记忆门",承担长期记忆职责的是细胞状态本身。
五、RNN / LSTM / GRU 三大模型对比
| 对比维度 | 传统 RNN | LSTM | GRU |
|---|---|---|---|
| 内部结构 | 全连接层 + tanh | 三门一态(遗忘门、输入门、细胞状态、输出门) | 两个门(更新门、重置门) |
| 门控机制 | 无 | 有 | 有 |
| 参数量 / 计算复杂度 | 最小 | 最大 | 介于两者之间 |
| 长序列建模能力 | 差,易梯度消失/爆炸 | 强,有效缓解梯度问题 | 较强,有效缓解梯度问题 |
| 并行计算 | 不支持 | 不支持 | 不支持 |
选型建议:
- 序列短、追求轻量快速 → 传统 RNN;
- 长序列依赖强、追求效果上限 → LSTM / Bi-LSTM;
- 希望兼顾效果与训练速度 → GRU。
六、总结
- RNN 以序列为输入输出,靠"上一时间步隐藏层输出 → 下一时间步隐藏层输入"的循环结构捕捉序列特征,结构简单但难以处理长序列;
- LSTM 用遗忘门、输入门、输出门加细胞状态的"三门一态"结构缓解了梯度消失/爆炸问题,是长序列建模的经典选择;
- GRU 用更新门和重置门两个门实现了与 LSTM 相近的效果,结构更简单、计算更省;
- 三者均无法并行计算,这也是后来 Transformer 等架构兴起的重要原因。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的最大动力!

4593

被折叠的 条评论
为什么被折叠?



