1. GRU
1.1 什么是GRU
门控循环神经网络 (Gated Recurrent Neural Network,GRNN) 的提出,旨在更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可学习的门来控制信息的流动。其中,**门控循环单元 (Gated Recurrent Unit,GRU)** 是一种常用的 GRNN。GRU 对LSTM 做了很多简化,同时却保持着和 LSTM 相同的效果。
1.2 GRU的原理
1.2.1 GRU 的两个重大改进
1. 将三个门:输入门、遗忘门、输出门变为两个门:更新门 (Update Gate) 和 重置门 (Reset Gate)。
2. 将 (候选) 单元状态 与 隐藏状态 (输出) 合并,即只有 当前时刻候选隐藏状态和当前时刻隐藏状态
。
1.2.2 模型结构
简化图:
内部结构:
、
和 W是模型参数(权重矩阵),需要通过训练数据来学习。
GRU通过其门控机制能够有效地捕捉到序列数据中的时间动态,同时相较于LSTM来说,由于其结构更加简洁,通常参数更少,计算效率更高。
1. 重置门:
重置门决定在计算当前候选隐藏状态时,忽略多少过去的信息。
2. 更新门:
更新门决定了多少过去的信息将被保留。它使用前一时间步的隐藏状态和当前输入
来计算得出。
3. 候选隐藏状态:
候选隐藏状态是当前时间步的建议更新,它包含了当前输入和过去的隐藏状态的信息。重置门的作用体现在它可以允许模型抛弃或保留之前的隐藏状态。
4. 最终隐藏状态:
最终隐藏状态是通过融合过去的隐藏状态和当前候选隐藏状态来计算得出的。更新门 控制了融合过去信息和当前信息的比例。
1.3 代码实现
1.3.1 原生代码
import numpy as np
class GRU:
def __init__(self, input_size, hidden_size, out_size):
self.input_size = input_size
self.hidden_size = hidden_size
self.out_size = out_size
# 权重矩阵和偏置
# 重置门
self.W_r = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)
self.b_r = np.zeros(self.hidden_size)
# 更新门
self.W_z = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)
self.b_z = np.zeros(self.hidden_size)
# ht候选
self.W = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)
self.b = np.zeros(self.hidden_size)
def tanh(self, x):
return np.tanh(x)
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
"""
:param x: [s,dim]
:param h_last:
:return:
"""
# 初始化隐藏状态状态
h_t = np.zeros((self.hidden_size,))
h_all = []
for t in range(x.shape[0]):
x_t = x[t]
x_t_cat = np.concatenate((x_t, h_t), axis=0)
r_t = self.sigmoid(np.dot(x_t_cat, self.W_r) + self.b_r)
z_t = self.sigmoid(np.dot(x_t_cat, self.W_z) + self.b_z)
# 获选ht输入处理
h_t_input = np.concatenate((x_t, r_t * h_t), axis=0)
h_t_candidate = self.tanh(np.dot(h_t_input, self.W) + self.b)
h_t = (1 - z_t) * h_t + z_t * h_t_candidate
h_all.append(h_t)
return h_all
# 测试
if __name__ == '__main__':
# 数据输入
x = np.random.rand(3, 2) # 三个单词 每个单词两个维度
gru = GRU(input_size=2, hidden_size=5, out_size=10)
h_all = gru.forward(x)
print(h_all)
print(np.array(h_all).shape)
1.3.2 PyTorch
nn.GRUCell:
import torch
import torch.nn as nn
class GRUCell(nn.Module):
def __init__(self, batch_size, input_size, hidden_size, batch_first=True):
super(GRUCell, self).__init__()
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.batch_first = batch_first
self.gru_cell = nn.GRUCell(input_size=input_size, hidden_size=hidden_size)
self.fc = nn.Linear(self.hidden_size, self.hidden_size)
# bidirectional=True,全连接层输入需要 *2
self.gru = nn.GRU(input_size=input_size, hidden_size=hidden_size, bidirectional=True)
self.fc_bTrue = nn.Linear(self.hidden_size * 2, self.hidden_size)
def forward(self, x):
hidden_all = []
if self.batch_first:
x = x.permute(1, 0, 2)
else:
pass
"""
GRUCell 的输入要求:
nn.GRUCell 的每次前向计算需要两个输入:
x_t:当前时间步的输入数据(形状为 (batch_size, input_size))。
h_t:上一个时间步的隐藏状态(形状为 (batch_size, hidden_size))。
如果没有上一个时间步(---> 即第一个时间步 <---),则需要人为初始化一个全零的隐藏状态,这就是 init_hidden 的作用。
"""
init_hidden = torch.zeros((self.batch_size, self.hidden_size))
for i in range(x.shape[0]):
x_t = x[i]
hidden_t = self.gru_cell(x_t, init_hidden)
hidden_all.append(hidden_t)
# 多对多
out = torch.stack(hidden_all)
out = out.permute(1, 0, 2)
# 多对一
out = torch.mean(out, dim=1)
out = self.fc(out)
# bidirectional=True,全连接层输入需要 *2
out_bid, h_last = self.gru(x)
out_bid = self.fc_bTrue(out_bid)
return out, out_bid
if __name__ == '__main__':
x = torch.randn(10, 6, 5)
hidden_size = 8
gru_cell = GRUCell(batch_size=x.shape[0], input_size=x.shape[2], hidden_size=hidden_size)
output, output_bid = gru_cell(x)
print(output.shape)
print(output_bid.shape)
nn.GRU:
import torch
from torch import nn
class GRUModule(nn.Module):
def __init__(self, input_size, hidden_size, out_size):
super(GRUModule, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.out_size = out_size
self.gru = nn.GRU(input_size=input_size, hidden_size=hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, out_size)
self.gru_bTure = nn.GRU(input_size=input_size, hidden_size=hidden_size, batch_first=True, bidirectional=True)
self.fc_bTrue = nn.Linear(hidden_size * 2, out_size)
def forward(self, x):
out, last = self.gru(x)
# 多对多的输出
# out = self.fc(out)
# 多对一的输出
out = torch.mean(out, dim=1)
out = self.fc(out)
# bidirectional=True,全连接层输入需要 *2
out_bid, bid_last = self.gru_bTure(x)
out_bid = self.fc_bTrue(out_bid)
return out, out_bid
if __name__ == '__main__':
x = torch.randn(10, 6, 5)
gru = GRUModule(input_size=x.shape[2], hidden_size=8, out_size=5)
output, output_bid = gru(x)
print(output.shape)
print(output_bid.shape)
output
:
-
形状为
(batch_size, sequence_length, hidden_size)
。 -
这个张量包含了 GRU 对每个时间步的输出,也就是每个时间步的隐藏状态。对于每个时间步
t
,GRU 会输出一个对应的隐藏状态。 -
如果
batch_first=True
(如在代码中设置的那样),则output
的第一个维度是批次大小batch_size
,第二个维度是序列长度sequence_length
,第三个维度是隐藏层的大小hidden_size
。
2. BiLSTM
2.1 概述
双向长短期记忆网络(Bi-directional Long Short-Term Memory,BiLSTM)是一种扩展自长短期记忆网络(LSTM)的结构,旨在解决传统 LSTM 模型只能考虑到过去信息的问题。BiLSTM 在每个时间步同时考虑了过去和未来的信息,从而更好地捕捉了序列数据中的双向上下文关系。
BiLSTM 的创新点在于引入了两个独立的 LSTM 层,一个按正向顺序处理输入序列,另一个按逆向顺序处理输入序列。这样,每个时间步的输出就包含了当前时间步之前和之后的信息,进而使得模型能够更好地理解序列数据中的语义和上下文关系。
- 正向传递:输入序列按照时间顺序被输入到第一个LSTM层。每个时间步的输出都会被计算并保留下来。
- 反向传递:输入序列按照时间的逆序(即先输入最后一个元素)被输入到第二个LSTM层。与正向传递类似,每个时间步的输出都会被计算并保留下来。
- 合并输出:在每个时间步,将两个LSTM层的输出通过某种方式合并(如拼接或加和)以得到最终的输出。
2.2 BILSTM模型应用背景
命名体识别
标注集:BMES标注集
分词的标注集并非只有一种,举例中文分词的情况,汉子作为词语开始Begin,结束End,中间Middle,单字Single,这四种情况就可以囊括所有的分词情况。于是就有了BMES标注集,这样的标注集在命名实体识别任务中也非常常见。
词性标注
在序列标注问题中单词序列就是x,词性序列就是y,当前词词性的判定需要综合考虑前后单词的词性。而标注集最著名的就是863标注集和北大标注集。
2.3 代码实现
import numpy as np
import torch
from torch import nn
class BILSTM:
def __init__(self, input_size, hidden_size, out_size):
"""
:param input_size: 词嵌入之后的向量维度
:param hidden_size: 隐藏层的维度
:param out_size: 输出的分类数
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.out_size = out_size
# 正向的LSTM
self.lstm = LSTM(self.input_size, self.hidden_size, self.out_size)
# 反向的LSTM
self.lstm_back = LSTM(self.input_size, self.hidden_size, self.out_size)
self.fc = nn.Linear(hidden_size, out_size)
def forward(self, x):
"""
:param x: [s,b,d]
:return:
"""
# 正向的LSTM
y_t, h_all, c_all = self.lstm.forward(x)
# 反向的LSTM np.flip 将数组反转
x_back = np.flip(x, axis=0)
y_t_back, h_all_back, c_all_back = self.lstm_back.forward(x_back)
# 反转
y_t_back = y_t_back[::-1]
# 合并
h_all = np.array(h_all)
h_all_back = np.array(h_all_back)
ht_all = [np.concatenate([i, j]) for i, j in zip(h_all, h_all_back)]
return y_t, ht_all
if __name__ == '__main__':
# 数据输入
x = np.random.rand(3, 2) # 三个单词 每个单词两个维度 注意:在forward方法for循环中x.shapr[0]的表示是什么
bilstm = BILSTM(input_size=2, hidden_size=5, out_size=10)
y_t, h_all = bilstm.forward(x)
print(y_t)
print(h_all)