【循环神经网络系列】一、RNN


一、前言

 在传统的神经网络模型中,是从输入层到隐含层再到输出层,层与层之间是全连接的,每层之间的节点是无连接的。但是这种普通的神经网络对于很多问题却无能无力。例如,你要预测句子的下一个单词是什么,一般需要用到前面的单词,因为一个句子中前后单词并不是独立的。

在这里插入图片描述

RNN之所以称为循环神经网络,即一个序列当前的输出与前面的输出也有关。具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。

在这里插入图片描述


二、RNN结构

 首先看一个简单的循环神经网络如,它由输入层、一个隐藏层和一个输出层组成:

在这里插入图片描述

  • U是输入层到隐藏层的权重矩阵

  • o也是一个向量,它表示输出层的值;

  • V是隐藏层到输出层的权重矩阵

 那么,现在我们来看看W是什么。循环神经网络隐藏层的值s不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值s。权重矩阵 W就是隐藏层上一次的值作为这一次的输入的权重。

在这里插入图片描述

我们从上图就能够很清楚的看到,上一时刻的隐藏层是如何影响当前时刻的隐藏层的。

 如果我们把上面的图展开,循环神经网络也可以画成下面这个样子:

在这里插入图片描述

 在 RNN 中,每输入一步,每一层各自都共享参数 U,V,W。其反映着 RNN 中的每一步都在做相同的事,只是输入不同,因此大大地降低了网络中需要学习的参数。

 现在看上去就比较清楚了,这个网络在t时刻接收到输入 x t x_t xt 之后,隐藏层的值是 s t s_t st ,输出值是 o t o_t ot 。关键一点是, s t s_t st 的值不仅仅取决于 x t x_t xt ,还取决于 s t − 1 s_{t−1} st1 。我们可以用下面的公式来表示循环神经网络的计算方法:

在这里插入图片描述

三、常见的RNN结构

 原始的 BP 神经网络 (或者原始CNN) 最大的局限之处在于:它们将固定大小的向量作为输入 (比如一张图片),然后输出一个固定大小的向量 (比如不同分类的概率)。而且:这些模型按照固定的计算步骤来 (比如模型中 layer 的数量) 实现这样的映射。

 RNN 的特别之处在于,它允许我们控制向量序列的长度:输入序列、输出序列、或者输入输出序列。RNN 有多种结构,如下所示:

在这里插入图片描述

  每一个矩形是一个向量,箭头则表示函数 (比如矩阵乘法)。输入向量用红色标出,输出向量用蓝色标出,绿色的矩形是 RNN 的状态。从左到右:

  • 1->1: 没有使用 RNN 的原始模型,从固定大小的输入得到固定大小输出 (比如图像分类);
  • 1->N: 序列输出 (比如图片字幕,输入一张图片输出一段文字序列);
  • N->1: 序列输入 (比如情感分析,输入一段文字然后将它分类成积极或者消极情感);
  • N->M: 序列输入和序列输出 (比如机器翻译:读取一条中文语句然后将它以英语形式输出);
  • N->N: 同步序列输入输出 (比如视频分类,对视频中每一帧打标签)。

 我们注意到在每一个案例中,都没有对序列长度进行预先特定约束,因为递归变换 (绿色部分) 是固定的,而且我们可以根据需要多次使用。

3.1 one-to-one

 最基本的单层网络,输入是 x ,经过变换 Wx+b 和激活函数 f 得到输出y。

在这里插入图片描述


3.2 one-to-n

在这里插入图片描述

 这种 one-to-n 的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子,就像看图说话等;
  • 从类别生成语音或音乐等;

3.3 n-to-one

 要处理的问题输入是一个序列,输出是一个单独的值而不是序列,应该怎样建模呢?实际上,我们只在最后一个h上进行输出变换就可以了:

在这里插入图片描述

 这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。


3.4 n-to-n

最经典的RNN结构,输入、输出都是等长的序列数据

 假设输入为X=(x1, x2, x3, x4),每个x是一个单词的词向量。

在这里插入图片描述

 为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,h可以对序列形的数据提取特征,接着再转换为输出。先从h1的计算开始看:

在这里插入图片描述

 h2的计算和h1类似。要注意的是,在计算时,每一步使用的参数U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记

在这里插入图片描述

 依次计算剩下来的(使用相同的参数U,W,b):

在这里插入图片描述

 这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。得到输出值的方法就是直接通过h进行计算:

在这里插入图片描述

 正如之前所说,一个箭头就表示对对应的向量做一次类似于f(Wx+b)的变换,这里的这个箭头就表示对h1进行一次变换,得到输出y1。

 这就是最经典的RNN结构,它的输入是x1, x2, ……xn,输出为y1, y2, …yn,也就是说,输入和输出序列必须要是等长的。由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
  • 输入为字符,输出为下一个字符的概率。这就是著名的Char RNN(Char RNN可以用来生成文章,诗歌,甚至是代码,非常有意思)。

3.5 Encoder-Decoder(Seq 2 Seq)

 还有一种是 n-to-m,输入、输出为不等长的序列。这种结构是Encoder-Decoder,也叫Seq2Seq,是RNN的一个重要变种

Seq2Seq 强调目的,不特指具体方法,满足输入序列,输出序列的目的,都可以统称为 Seq2Seq 模型

 Encoder-Decoder 不是一个具体的模型,是一种框架。

  • Encoder:将 input序列 →转成→ 固定长度的向量;
  • Decoder:将 固定长度的向量 →转成→ output序列;
  • Encoder 与 Decoder 可以彼此独立使用,实际上经常一起使用;

 因为最早出现的机器翻译领域,最早广泛使用的转码模型是RNN。其实模型可以是 CNN /RNN /BiRNN /LSTM /GRU /…


(1)Encoder

 原始的n-to-n的RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。为此,Encoder-Decoder结构先将输入数据通过Encoder编码成一个上下文语义向量c语义向量c可以有多种表达方式:

(1)把Encoder的最后一个隐状态赋值给c;

(2)对最后的隐状态做一个变换得到c;

(3)也可以对所有的隐状态做变换。

在这里插入图片描述


(2)Decoder

拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。**Decoder的RNN可以与Encoder的一样,也可以不一样。**具体做法就是将c当做之前的初始状态h0输入到Decoder中:

在这里插入图片描述

 还有一种做法是将c当做每一步的输入:

在这里插入图片描述


(3)Encoder-Decoder 应用

 由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:

  • 机器翻译:Encoder-Decoder的最经典应用,事实上这结构就是在机器翻译领域最先提出的。
  • 文本摘要:输入是一段文本序列,输出是这段文本序列的摘要序列。
  • 阅读理解:将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
  • 语音识别:输入是语音信号序列,输出是文字序列。

(4)Encoder-Decoder 缺点

  • 最大的局限性:编码和解码之间的唯一联系是固定长度的语义向量c
  • 编码要把整个序列的信息压缩进一个固定长度的语义向量c
  • 语义向量c无法完全表达整个序列的信息
  • 先输入的内容携带的信息,会被后输入的信息稀释掉,或者被覆盖掉
  • 输入序列越长,这样的现象越严重,这样使得在Decoder解码时一开始就没有获得足够的输入序列信息,解码效果会打折扣

 因此,为了弥补基础的 Encoder-Decoder 的局限性,后面提出了attention机制。


四、参数学习

 不同于前馈神经网络,循环神经网络在每一时刻都有一个预测值和一个标签,那么就可以在每一时刻计算损失进行梯度下降,基于这种思想循环神经网络可以使用两种学习算法进行参数更新。

 公式推导可参考邱锡鹏老师的《神经网络与深度学习》。

  • 循环神经网络:共享权重;
  • 随时间反向传播 BPTT:针对所有时刻的损失函数进行学习,类似于普通反向传播,根据t+1和t时刻的迭代公式,反求梯度;
  • 实时循环学习 RTRL:针对每一时刻的损失函数进行学习;

在这里插入图片描述


(1)随时间反向传播 BPTT

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

(2)实时循环学习RTRL

在这里插入图片描述

在这里插入图片描述

(3)长程依赖问题

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


五、在Pytorch中实现RNN

参考

  使用PyTorch实现简单的RNN

  Pytorch学习笔记013——Simplele_RNN


 假设 X t ∈ R n × d X_t∈R^{n×d} XtRn×d 是序列中时间步 t t t 的小批量输入, H t ∈ R n × h H_t∈R^{n×h} HtRn×h 是该时间步输入的隐藏变量。计算过程如下:
H t = t a n h ( X t W d h + H t − 1 W h h + b h ) H_t=tanh(X_tW_{dh}+H_{t−1}W_{hh}+b_h) Ht=tanh(XtWdh+Ht1Whh+bh)
 其中,隐藏层的权值 W d h ∈ R d × h W_{dh}∈R^{d×h} WdhRd×h W h h ∈ R h × h W_{hh}∈R^{h×h} WhhRh×h 和偏置项系数 b h ∈ R 1 × h b_{h}∈R^{1×h} bhR1×h d d d 表示输入特征向量的维度, h h h 表示隐藏层单元的个数。
O t = H t W h q + b q O_t=H_tW_{hq}+b_q Ot=HtWhq+bq
 其中,输出变量 O t ∈ R n × q O_t∈R^{n×q} OtRn×q ,输出层权重参数 W h q ∈ R h × q W_{hq}∈R^{h×q} WhqRh×q,输出层的偏置项 b q ∈ R 1 × q b_q∈R^{1×q} bqR1×q q q q 表示输出层的个数。

 一个基本的 RNN Cell 如下图所示:

在这里插入图片描述

 【注意】:RNN Cell的本质其实就是线性层,RNN Cell是共享的,反复参与运算

 将多个 RNN Cell 依次串联起来就可以得到最传统的循环神经网络的结构:

在这里插入图片描述


5.1 torch.nn.RNNCell

 下面我们通过 PyTorch 内置的 RNNCell 方法实现一个简单的单隐藏循环神经网络。

"""
input_size:输入层输入的特征维度
hidden_size:隐藏层输出的特征维度
bias:bool类型,如果是False,那么不提供偏置,默认为True
nonlinearity:字符串类型,进行激活函数选择,可以是 "tanh" 或 "relu",默认为 "tanh"
"""
cell = torch.nn.RNNCell(input_size, hidden_size, bias, nonlinearity)
hidden = cell(input, hidden)

 其中,input 是一个二维张量,维度是 (batch, input_size)input_size 表示的是当前特征的维度;而 hidden 也是一个维度为 (batch, hidden_size) 的二维张量,第一维表示的是批量大小,第二维则表示隐藏层的维度。

在这里插入图片描述

 由于我们使用的是一个 RNNCell,所以需要通过循环或迭代的方式将多个 RNNCell 连结起来,构成一个含有 3 个时间步或者序列长度为 3 的单隐藏循环神经网络。

import torch

batch_size = 1 # 批量大小
seq_len = 3 # 序列长度:x1~x3
input_size = 4 # 输入维度4x1
hidden_size = 2 # 隐层维度2x1

cell = torch.nn.RNNCell(input_size=input_size, hidden_size=hidden_size)

# 定义输入数据
# 输入张量是三维的,维度为(seq, batch, features)
dataset = torch.randn(seq_len, batch_size, input_size)

# 定义h0
# 因为是单隐藏,所以没有layer_size
# 由于没有先验,所以初始化第一个隐藏层单元设置为全零
hidden = torch.zeros(batch_size, hidden_size)

# 自定义RNN循环
for seq_idx, input in enumerate(dataset, 1):
    # 每次取出一个seq:(batch, input_size)
    print('='*20, seq_idx, '='*20)
    print('input_size:', input.shape) # 输入维度为(batch_size, input_size)
    
    # h1 = linear(x1, h0)
    # h2 = linear(x2, h1)
    hidden = cell(input, hidden) # 使用上一次的hidden和当前input进行运算
    
    print('hidden_size:', hidden.shape) # hidden维度为(batch_size, hidden_size)
    print(hidden)

 输出如下:

==================== 1 ====================
input_size: torch.Size([1, 4])
hidden_size: torch.Size([1, 2])
tensor([[ 0.1422, -0.5407]], grad_fn=<TanhBackward0>)
==================== 2 ====================
input_size: torch.Size([1, 4])
hidden_size: torch.Size([1, 2])
tensor([[0.8827, 0.6935]], grad_fn=<TanhBackward0>)
==================== 3 ====================
input_size: torch.Size([1, 4])
hidden_size: torch.Size([1, 2])
tensor([[ 0.5298, -0.1573]], grad_fn=<TanhBackward0>)

5.2 torch.nn.RNN

 用于定义多层循环神经网络,函数原型如下:

"""
input_size:输入特征的维度
hidden_size:隐藏层神经元个数
num_layers:网络的层数,或者说是隐藏层的层数
nonlinearity:激活函数,默认为 tanh
bias:是否使用偏置,默认为True
"""
cell = torch.nn.RNN(input_size, hidden_size, num_layers=1, nonlinearity=tanh, bias=True)
out, hn = cell(inputs, h0)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • input的维度为 (seqLen, batchSize, inputSize)
  • 初始化隐藏变量 h 0 h_0 h0 的维度是 (numLayers, batchSize, hiddenSize)
  • out 的维度为(seqlen, batchSize, hidden_size)
  • h n h_n hn的维度是 (numlayers, batch, hidden)
import torch

batch_size = 1 # 批量大小
seq_len = 3 # 序列长度:x1~x3
input_size = 4 # 输入维度4x1
hidden_size = 2 # 隐层维度2x1
num_layers = 1 # 隐层层数

# 定义RNN模块
cell = torch.nn.RNN(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)

# 定义输入
inputs = torch.randn(seq_len, batch_size, input_size)

# 定义隐层
hidden = torch.zeros(num_layers, batch_size, hidden_size)

'''
out为H1~Hn,维度为(seq_len, batch_size, hidden_size)
输出的hidden为Hn,维度为(num_layers, batch_size, hidden_size)
输入inputs为X1~Xn,维度为(seq_len, batch_size, input_size)
输入的hidden为H0,维度为(num_layers, batch_size, hidden_size)
'''
out, hidden = cell(inputs, hidden)

print('Output_size:', out.shape)
print('Output:', out)
print('Hidden_N_size:', hidden.shape)
print('Hidden_N:', hidden)

 输出如下:

Output_size: torch.Size([3, 1, 2])
Output: tensor([[[-0.7518,  0.5582]],

        [[ 0.2957, -0.7583]],

        [[-0.8148, -0.6889]]], grad_fn=<StackBackward0>)
Hidden_N_size: torch.Size([1, 1, 2])
Hidden_N: tensor([[[-0.8148, -0.6889]]], grad_fn=<StackBackward0>)

 其中,如果参数 batch_first 设置为 True,则输入张量的维度为 (batchSize, seqLen, input_size) ,即序列长度与批量交换。一般地,为了避免混淆,我们将输入张量的维度设置为 (seqLen, batchSize, input_size) 是比较容易理解的

在这里插入图片描述


六、具体案例

 假定现在有一个序列到序列(seq 2 seq)的任务,比如将“hello”转换为“ohlol”。

 即利用RNN实现如下输出:

在这里插入图片描述

 问题分析:原输入“hello”并不是一个向量,需要将其转变为一组数字向量。

 对输入序列的每一个字符(单词)构造字典(词典),此时每一个字符都会有一个唯一的数字与其一一对应。即将字符向量转换为数字向量。之后利用独热编码(One-Hot)的思想,即可将每个数字转换为一个向量。

在这里插入图片描述

 对于序列中的每一个输入,都有一个数字输出与其对应,即本质上是在求当前输入所映射到输出字典中最大概率的值。即变为多分类问题。

在这里插入图片描述

 其中RNNCell的输出维度为4,经过Softmax求得映射之后的概率分别是多少,再利用输出对应的独热向量,计算NLLLoss。

6.1 使用RNNCell的字符串转化

import torch

input_size = 4
hidden_size = 4
batch_size = 1

# 字典,字母e对应的下标为0,字母h对应下标为1
idx2char = ['e', 'h', 'l', 'o']
# 输入为hello
x_data = [1, 0, 2, 2, 3]
# 输出为ohlol
y_data = [3, 1, 2, 3, 2]
# 独热向量矩阵
one_hot_lookup = [[1, 0, 0, 0],
                  [0, 1, 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]]
# 将对应x的索引转为独热向量
x_one_hot = [one_hot_lookup[x] for x in x_data]

# 输入的维度为(seqlen, batchsize, inputsize)
inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size)
print('inputs_size:', inputs.shape) # inputs_size: torch.Size([5, 1, 4])
# 标签的维度为(seqlen, 1)
labels = torch.LongTensor(y_data).view(-1, 1)
print('labels_size:', labels.shape) # labels_size: torch.Size([5, 1])

# 构建模型
class Model(torch.nn.Module):
    def __init__(self, input_size, hidden_size, batch_size):
        super(Model, self).__init__()
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 定义RNNCELL
        self.rnncell = torch.nn.RNNCell(input_size=self.input_size, hidden_size=self.hidden_size)
        
    def forward(self, input, hidden):
        # h1 = rnncell(x1, h0)
        hidden = self.rnncell(input, hidden)
        return hidden
    
    # 定义h0,默认为全0
    def init_hidden0(self):
        return torch.zeros(self.batch_size, self.hidden_size)
    
# 实例化模型对象
net = Model(input_size, hidden_size, batch_size)

# 构建损失函数和优化准则
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.1)

# 训练过程
def train(epoches):
    for epoch in range(epoches):
        # 初始化损失为0,因为每次epoch对应的loss不一样,必须清零
        loss = 0
        # 梯度归零
        optimizer.zero_grad()
        # 获取h0
        hidden = net.init_hidden0()
        print('Predicted string: ', end='')
        # inputs的维度为(seqlen, batchsize, inputsize),input的维度为(batchsize, inputsize)
        # labels的维度为(seqlen, 1),lable的维度为(1)
        for input, label in zip(inputs, labels):
            # print('input_size:', input.shape) # input_size: torch.Size([1, 4])
            # print('label_size:', label.shape) # label_size: torch.Size([1]) 
            # 计算h1
            hidden = net(input, hidden)
            # print('hidden_size:', hidden.shape) # hidden_size: torch.Size([1, 4])
            # 构建计算图,要把每个seqlen里面的loss累加之后才是总的loss
            loss += criterion(hidden, label)
            # 找出概率最大的下标
            _, idx = hidden.max(dim=1)
            print(idx2char[idx.item()], end='')
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()
        print(', Epoch [%d/%d] loss=%.4f' %(epoch+1, epoches, loss.item()))

if __name__ == '__main__':
    train(30)

6.2 使用RNN的字符串转化

import torch

input_size = 4
hidden_size = 4
batch_size = 1
num_layers = 1 # 隐层层数
seq_len = 5 # 序列长度

# 字典,字母e对应的下标为0,字母h对应下标为1
idx2char = ['e', 'h', 'l', 'o']
# 输入为hello
x_data = [1, 0, 2, 2, 3]
# 输出为ohlol
y_data = [3, 1, 2, 3, 2]
# 独热向量矩阵
one_hot_lookup = [[1, 0, 0, 0],
                  [0, 1, 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]]
# 将对应x的索引转为独热向量
x_one_hot = [one_hot_lookup[x] for x in x_data]

# 输入的维度为(seqlen, batchsize, inputsize)
inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size)
print('inputs_size:', inputs.shape) # inputs_size: torch.Size([5, 1, 4])
# 标签的维度为(seqlenxbatchsize, 1)
labels = torch.LongTensor(y_data) # 这里的labels必须是一维的,不然无法使用交叉熵损失函数进行计算.
print('labels_size:', labels.shape) # labels_size: torch.Size([5]),一维的张量

# 构建模型
class Model(torch.nn.Module):
    def __init__(self, input_size, hidden_size, batch_size, num_layers):
        super(Model, self).__init__()
        self.num_layers = num_layers
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 定义RNN
        self.rnn = torch.nn.RNN(input_size=self.input_size, hidden_size=self.hidden_size, num_layers=num_layers)
        
    def forward(self, inputs, hidden_0):
        # out, hidden_N = rnn(inputs, hidden_0)
        # out为(h1~hN),维度为(seqlen, batchsize, hiddensize)
        # inputs为(x1~xN),维度为(seqlen, batchsize, inputsize)
        out, hidden_N = self.rnn(inputs, hidden_0)
        return out.view(-1, self.hidden_size) # 变成(seqlenxbatchsize, hiddensize)二维       
    
    # 定义h0,默认为全0
    def init_hidden0(self):
        return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)
    
# 实例化模型对象
net = Model(input_size, hidden_size, batch_size, num_layers)

# 构建损失函数和优化准则
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.05)

# 训练过程
def train(epoches):
    for epoch in range(epoches):
        # 梯度归零
        optimizer.zero_grad()
        # 获取h0
        hidden_0 = net.init_hidden0()
        # 通过网络预测输出
        outputs = net(inputs, hidden_0)
        # 计算损失
        loss = criterion(outputs, labels)
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()
        # 找出概率最大的下标
        _, idx = outputs.max(dim=1)
        print('Predicted string: ', ''.join([idx2char[x] for x in idx]), end='')
        print(', Epoch [%d/%d] loss=%.4f' %(epoch+1, epoches, loss.item()))

if __name__ == '__main__':
    train(35)

6.3 使用Embedding层降维

 独热编码在实际问题中容易引起很多问题:

  1. 独热编码向量维度过高,每增加一个不同的数据,就要增加一维;
  2. 独热编码向量稀疏,每个向量是一个为1其余为0;
  3. 独热编码是硬编码,编码情况与数据特征无关;

 综上所述,需要一种低维度的、稠密的、可学习数据的编码方式 -> Embedding

在这里插入图片描述

如何理解Embedding层对数据进行降维的操作

在这里插入图片描述

(1)宏观上理解(查表+用向量表示id特征)

 如上图所示,embedding过程就是将one-hot向量(2个6维onehot向量)输入到全连接层(6x3的稠密矩阵),输出2个3维的稠密向量,这个6x3的全连接层参数,就是一个“id向量表”,对应6种id的embedding稠密向量。又例如,假设不同id的个数为100(即one-hot向量长度为100),设定embedding稠密向量的维度为10,则全连接层的参数矩阵为100x10(这个矩阵就是id向量表,每个id特征都有一个10维的稠密向量表示它)。

(2)在运算层面上理解(查表操作)

 对one-hot向量的embedding,相当于查表,embedding直接用查表作为操作,而不是矩阵乘法运算,这大大降低了运算量,所以降低运算量不是因为id的embedding向量的出现,而是因为把one-hot的embedding矩阵乘法运算简化为了查表操作。

(3)在思想层面上理解(全连接层参数表达id特征)

 在训练得到这个全连接层参数后(相当于降维后,在上面例子中,由100维降至10维),直接用这个全连接层的权重参数作为特征,或者说,用这个全连接层的参数作为id的特征表达(向量的夹角余弦能够在某种程度上表示不同id间的相似度)。

在这里插入图片描述

 改进后的网络结构如下:

增加Embedding层实现降维,增加线性层使之在处理输入输出维度不同的情况下更加稳定。

在这里插入图片描述

 其中的Embedding层的输入必须是LongTensor类型。

(4)Pytorch中的Embedding函数说明

torch.nn.Embedding(num_embeddings: int, embedding_dim: int)
字段类型注释
num_embeddingtorch.nn.Embedding的参数表示输入的独热编码的维数
embedding_dimtorch.nn.Embedding的参数表示需要转换成的维数
Inputtorch.nn.Embedding的输入量LongTensor类型
Outputtorch.nn.Embedding的输出量为数据增加一个维度(embedding_dim)
import torch 

num_class = 4 #4个类别,
input_size = 4 #输入维度
hidden_size = 8 #隐层输出维度,有8个隐层
embedding_size = 10 #嵌入到10维空间
num_layers = 2 #2层的RNN
batch_size = 1
seq_len = 5 #序列长度5

# 字典,字母e对应的下标为0,字母h对应下标为1
idx2char = ['e', 'h', 'l', 'o']
# 输入为hello
x_data = [1, 0, 2, 2, 3] 
# 输出为ohlol
y_data = [3, 1, 2, 3, 2] # (batch * seq_len)

# 这里没有手动转为one-hot向量,而是直接将原始输入变成(batch, seq_len)的维度
inputs = torch.LongTensor(x_data).view(-1, seq_len)
# 等效于下面这样初始化
# x_data = [[1, 0, 2, 2, 3]] # (batch, seq_len)
# inputs = torch.LongTensor(x_data)
print('inputs_size:', inputs.shape) # inputs_size: torch.Size([1, 5])

# 标签的维度为(seqlen x batchsize)
labels = torch.LongTensor(y_data) # 这里的labels必须是一维的,不然无法使用交叉熵损失函数进行计算.
print('labels_size:', labels.shape) # labels_size: torch.Size([5])

# 构建模型
class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        # 定义嵌入层
        self.emb = torch.nn.Embedding(input_size, embedding_size)
        # 定义RNN
        self.rnn = torch.nn.RNN(input_size=embedding_size,
                                        hidden_size=hidden_size,
                                        num_layers=num_layers,
                                        batch_first=True)
        # 定义线性层
        self.fc = torch.nn.Linear(hidden_size, num_class)
        
    def forward(self, inputs):
        hidden_0 = torch.zeros(num_layers, inputs.size(0), hidden_size)
        outputs = self.emb(inputs)
        outputs, _ = self.rnn(outputs, hidden_0)
        outputs = self.fc(outputs)
        return outputs.view(-1, num_class)  
    
# 实例化模型对象
net = Model()

# 构建损失函数和优化准则
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.05)

# 训练过程
def train(epoches):
    for epoch in range(epoches):
        # 梯度归零
        optimizer.zero_grad()
        # 通过网络预测输出
        outputs = net(inputs)
        # 计算损失
        loss = criterion(outputs, labels)
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()
        # 找出概率最大的下标
        _, idx = outputs.max(dim=1)
        print('Predicted string: ', ''.join([idx2char[x] for x in idx]), end='')
        print(', Epoch [%d/%d] loss=%.4f' %(epoch+1, epoches, loss.item()))

if __name__ == '__main__':
    train(35)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值