计算机系统应用教程网站

网站首页 > 技术文章 正文

使用矩阵解释门控循环单元(GRU) 门控循环网络

btikc 2024-10-12 11:49:12 技术文章 3 ℃ 0 评论

我们经常使用深度学习框架来构建模型所需的所有操作。然而,理解底层使用的一些基本矩阵操作是很有用的。在本教程中,将介绍GRU如何工作所需的简单矩阵操作。

什么是门控循环单元(GRU)?

门控循环单元(如下图所示)是一种循环神经网络,它解决了长期依赖关系问题。GRU通过存储来自前一个时间点的“memory”来解决这个问题,从而为网络的未来预测提供信息。

GRU的控制方程是:

其中z,r和h分别代表update和reset gates,而h_tilde和h分别代表intermediate memory和output 。

方法

为了进一步说明RNN的优雅性,我将向您介绍理解GRU内部工作原理所需的线性代数的基础知识。为了做到这一点,我们将使用一个字符串来说明矩阵计算是如何使用预打包的wrapper器函数来创建许多常见的深度学习(DL)框架的。本教程的重点是帮助我们更深入地理解RNN是如何使用线性代数工作的。

使用以下字符串作为输入数据的示例:

`text = MathMathMathMathMath`

算法本质上是某种数学方程,因此我们的原始文本在呈现给GRU层之前必须以数字形式表示。这是在下面的预处理步骤中完成的。

数据预处理

首先导入Python库

import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
import torch.nn.functional as F
import numpy as np
import itertools
import pickle
%autosave 180

创建一个包含所有唯一字符的字典,将每个字母映射到一个唯一整数:

字符字典:{'h':0,'a':1,'t':2,'M':3}

我们的编码输入现在变为:

MathMath = [3,1,2,0,3,1,2,0]

# This will be our input ---> x
text = 'MathMathMathMathMath'
character_list = list(set(text)) # get all of the unique letters in our text variable
vocabulary_size = len(character_list) # count the number of unique elements
character_dictionary = {'h': 0, 'a': 1, 't': 2, 'M': 3}
# {char:e for e, char in enumerate(character_list)} # create a dictionary mapping each unique char to a number
encoded_chars = [character_dictionary[char] for char in text] #integer representation of our vocabulary

步骤1:创建数据batches

此步骤通过用户指定我们要创建的给定词汇表(V)的batches数(B)或序列长度(S)来实现。下图演示了如何创建和编码batches。

假设我们需要以下参数:

  • 1.Batch size(B)= 2
  • 2.序列长度(S)= 3
  • 3.词汇表(V)= 4
  • 4.输出(O)= 4
  • 5.样本数量(NS) = 2

那么是什么时间序列呢?

如果您对RNN进行基本搜索,你通常会看到下面的图像。x_(t-1),x_(t)和x_(t + 1)(以红色突出显示)对我们的batches意味着什么呢?

对于我们的mini-batch,时间序列表示每个序列,信息从左到右流动,如下图所示。

数据集的维度

用Python代码说明

def one_hot_encode(encoded, vocab_size):
 result = torch.zeros((len(encoded), vocab_size))
 for i, idx in enumerate(encoded):
 result[i, idx] = 1.0
 return result
# One hot encode our encoded charactes
batch_size = 2
seq_length = 3
num_samples = (len(encoded_chars) - 1) // seq_length # time lag of 1 for creating the labels
vocab_size = 4
data = one_hot_encode(encoded_chars[:seq_length*num_samples], vocab_size).reshape((num_samples, seq_length, vocab_size))
num_batches = len(data) // batch_size
X = data[:num_batches*batch_size].reshape((num_batches, batch_size, seq_length, vocab_size))
# swap batch_size and seq_length axis to make later access easier
X = X.transpose(1, 2)
# +1 shift the labels by one so that given the previous letter the char we should predict would be or next char
labels = one_hot_encode(encoded_chars[1:seq_length*num_samples+1], vocab_size) 
y = labels.reshape((num_batches, batch_size, seq_length, vocab_size))
y = y.transpose(1, 2) # transpose the first and second index
y,y.shape

reshaping后,如果你检查X的形状你会发现你得到一个3阶张量的形状:3×3×2×4。

数据现在可以建模了。我们将演示在batch 1(如下所示)中为第一个序列执行的矩阵操作(红色高亮显示)。其思想是理解来自第一个序列的信息如何传递到第二个序列,以此类推。

为此,我们需要首先回想一下这些batches是如何输入算法的。

更具体地说,我们将遍历在序列1的GRU cell中执行的所有矩阵操作,计算得到y_(t-1)和h_t的输出结果如下图所示:

步骤2:定义权重矩阵和偏差向量

在这一步中,我们将引导您完成用于计算z门的矩阵运算,因为其余三个方程的计算完全相同。为了更好地说明这一点,我们将通过将内部方程分解为三个部分来遍历reset gate z的点积,最后我们将sigmoid激活函数应用到输出(0到1之间的值)中:

但首先让我们定义网络参数:

torch.manual_seed(1) # reproducibility
#### Define the network parameters:
hiddenSize = 2 # network size, this can be any number (depending on your task)
numClass = 4 # this is the same as our vocab_size
#### Weight matrices for our inputs 
Wz = torch.randn(vocab_size, hiddenSize)
Wr = torch.randn(vocab_size, hiddenSize)
Wh = torch.randn(vocab_size, hiddenSize)
## Intialize the hidden state
# this is for demonstration purposes only, in the actual model it will be initiated during training a loop over the 
# the number of bacthes and updated before passing to the next GRU cell.
h_t_demo = torch.zeros(batch_size, hiddenSize) 
#### Weight matrices for our hidden layer
Uz = torch.randn(hiddenSize, hiddenSize)
Ur = torch.randn(hiddenSize, hiddenSize)
Uh = torch.randn(hiddenSize, hiddenSize)
#### bias vectors for our hidden layer
bz = torch.zeros(hiddenSize)
br = torch.zeros(hiddenSize)
bh = torch.zeros(hiddenSize)
#### Output weights
Wy = torch.randn(hiddenSize, numClass)
by = torch.zeros(numClass)

我们来分解网络参数的维数。

什么是隐藏大小(hidden size)?

上面定义的隐藏大小,是学习参数的数量或简单地说,网络内存。此参数通常由用户根据手头的问题定义,因为使用更多单位可能使其更有可能过度拟合训练数据。在我们的例子中,我们选择隐藏的大小为2。这些值通常被初始化为来自正态分布的随机数,当我们执行forward passes和反向传播时,这些随机数可以训练和更新。

我们的权重大小

在开始上述任何矩阵操作之前,让我们先讨论一个名为广播(broadcasting)的导入概念。如果我们看batch 1(3×2×4)张量和Wz(4×2)张量的形状,首先想到的是,我们如何对这两个形状不同的张量进行元素矩阵乘法呢?

答案是我们使用一个名为“Broadcasting”的过程。Broadcasting用于使这两个张量的形状兼容,这样我们就可以执行元素矩阵运算。这意味着Wz将被广播到非矩阵维度,在我们的例子中,我们的序列长度为3.这意味着更新方程式z中的所有其他项也将被广播。因此,我们的最终等式将如下所示:

在我们执行实际的矩阵运算之前,让我们看一下batch 1中的序列1:

The update gate z

update gate决定了过去的信息对当前状态的有用程度。在这里,使用sigmoid函数会导致更新0到1之间的gate值。因此,这个值越接近1,我们吸收过去的信息就越多,而接近0的值意味着只保留新的信息。

注意当两个矩阵相乘时,执行行和列的点积。这里,第一矩阵(x_t)的每一行(以黄色突出显示)逐个元素地乘以第二矩阵(Wz)的每列(以蓝色突出显示)。

1:应用于输入的权重

torch.matmul(X[0][0], Wz)

2:隐藏权重

torch.matmul(Uz, h_t_demo), Uz

3:偏差向量

把它们放在一起:z_inner

z_inner = torch.matmul(X,Wz) + torch.matmul(Uz,h_t_demo) + bz

然后使用sigmoid激活函数将得到的矩阵中的值压缩到0到1之间:

z = torch.sigmoid(z_inner)
z[0][0]

reset gate: r

Reset gate允许模型忽略在未来时间步骤中可能不相关的过去信息。在每个batch中,Reset gate将重新评估先前输入和新输入的组合性能,并根据新输入的需要进行Reset。同样由于sigmoid激活函数,接近0的值意味着我们会忽略之前的隐藏状态,而接近1的值则相反。

Intermediate Memory: h_tilde

intermediate memory单元或候选隐藏状态将来自先前隐藏状态的信息与输入组合。由于第一项和第三项所需的矩阵运算与我们在z中所做的相同。

torch.matmul(r[0][0] * h_t_demo, Uh)


偏差向量

把它们放在一起:h_tilde

然后使用sigmoid激活函数将得到的矩阵中的值压缩到0到1之间:

最后:

h_inner_tilde = torch.matmul(X,Wh) + r*torch.matmul(Uh,h_t_demo) + bh
h_tilde = torch.tanh(h_inner_tilde)

在时间步t输出隐藏层:h_(t-1)

第一项:

第二项:

ht_1 = z *h_t_demo + (1-z)* h_tilde

最终:

batch 1 (time step x_t) 中的第二个序列如何从此隐藏状态中获取信息呢?

回想一下,h_(t - n)首先被初始化为零(在本教程中使用)或随机噪声以开始训练,此后网络将学习和适应该训练。但是在第一次迭代之后,新的隐藏状态h_t现在将被用作我们的新隐藏状态,并且在时间步(x_t)对序列2重复上述计算。下图显示了如何完成此操作。

这个新的隐藏状态h_(t-1)将不会用于计算批处理中第二个时间步的隐藏状态h_(t)和输出(y_(t + 1)),依此类推。

下面我们将演示如何使用新的隐藏状态h_(t-1)来计算后续隐藏状态。这通常使用循环完成。该循环迭代每个给定batch中的所有元素以计算h_(t-1)。

Python代码实现:Batch 1 输出:h(t-1),h(t)和h(t + 1)

# h gets updated and then we calculate for the next 
h_t_1 = []
h = h_t_demo
for i,sequence in enumerate(X[0]): # iterate over each sequence in the batch to calculate the hidden state h 
 z = torch.sigmoid(torch.matmul(sequence, Wz) + torch.matmul(h, Uz) + bz)
 r = torch.sigmoid(torch.matmul(sequence, Wr) + torch.matmul(h, Ur) + br)
 h_tilde = torch.tanh(torch.matmul(sequence, Wh) + torch.matmul(r * h, Uh) + bh)
 h = z * h + (1 - z) * h_tilde
 h_t_1.append(h)
 print(f'h{i}:{h}')
h_t_1 = torch.stack(h_t_1)

使用batch 1中的第二个序列,我们可以演示如何可视化地获得上面第二个序列的h_1。

h_t_minus_1 = torch.tensor([[ 0.7565, -0.3472],[-0.1355, -0.2040]])
h = h_t_minus_1
z = torch.sigmoid(torch.matmul(X[0][1], Wz) + torch.matmul(h, Uz) + bz)
r = torch.sigmoid(torch.matmul(X[0][1], Wr) + torch.matmul(h, Ur) + br)
h_tilde = torch.tanh(torch.matmul(X[0][1], Wh) + torch.matmul(r * h, Uh) + bh)
hh = z * h + (1 - z) * h_tilde
hh

tensor([[-0.1536, -0.5713],

[ 0.7664, -0.5062]])

第二个batch的隐藏状态是什么?

它可以看作是h_(t + 1)的输出序列,然后将被送到下一batch,整个过程再次开始。

Python代码实现:跨批传递隐藏状态

ht_2 = [] # stores the calculated h for each input x
h = torch.zeros(batch_size, hiddenSize) # intitalizes the hidden state
for i in range(num_batches): # this loops over the batches 
 x = X[i]
 for i,sequence in enumerate(x): # iterates over the sequences in each batch
 z = torch.sigmoid(torch.matmul(sequence, Wz) + torch.matmul(h, Uz) + bz)
 r = torch.sigmoid(torch.matmul(sequence, Wr) + torch.matmul(h, Ur) + br)
 h_tilde = torch.tanh(torch.matmul(sequence, Wh) + torch.matmul(r * h, Uh) + bh)
 h = z * h + (1 - z) * h_tilde
 ht_2.append(h)
ht_2 = torch.stack(ht_2)
ht_2

步骤3:计算每个时间步的输出预测

要获得对每个时间步的预测,我们首先必须使用线性层转换输出。回想一下隐藏状态中列的维度h_(t+n),本质上是network size/hidden size。但是,我们有4个惟一的输入,并且我们希望输出的大小也为4。因此,我们使用所谓的dense层或全连接层将输出转换回所需的维度。根据需要的输出,这个全连接层然后被传递到一个激活函数(本教程使用softmax)中。

fully_connected = torch.matmul(ht_2, Wy) + by

最后,我们应用Softmax激活函数将输出归一化为概率分布,总和为1。 Softmax函数:

根据教科书的不同,您可能会看到softmax的不同风格,特别是使用softmax max技巧,它减去整个数据集的最大值,以防止y_lineary / fully_connected的大值爆炸。 在我们的例子中,这意味着在应用softmax方程之前,我们的最大值0.9021将首先从y_linear中减去。

让我们分解一下,请注意我们不能像前面那样对序列进行子集化,因为求和需要整个batch中的所有元素。

1.从全连接层中的所有元素中减去整个数据集的最大值:

ylin_max = (fully_connected - fully_connected.max()) # first sequence in batch 1
exp = ylin_max.exp()

2.找出指数矩阵中所有元素的总和

exp_sum = exp[0].sum(dim=1,keepdim=True).reshape(-1,1) 

3.将矩阵中第1步的每个元素除以第2步中相应行的值

exp[0]/exp_sum

Python代码实现:Softmax

ht_2 = [] # stores the calculated h for each input x
outputs = []
h = torch.zeros(batch_size, hiddenSize) # intitalizes the hidden state
for i in range(num_batches): # this loops over the batches 
 x = X[i]
 for i,sequence in enumerate(x): # iterates over the sequences in each batch
 z = torch.sigmoid(torch.matmul(sequence, Wz) + torch.matmul(h, Uz) + bz)
 r = torch.sigmoid(torch.matmul(sequence, Wr) + torch.matmul(h, Ur) + br)
 h_tilde = torch.tanh(torch.matmul(sequence, Wh) + torch.matmul(r * h, Uh) + bh)
 h = z * h + (1 - z) * h_tilde
 
 # Linear layer
 y_linear = torch.matmul(h, Wy) + by
 
 # Softmax activation function
 y_t = F.softmax(y_linear, dim=1)
 
 ht_2.append(h)
 outputs.append(y_t)
 
ht_2 = torch.stack(ht_2)
outputs = torch.stack(outputs)
outputs[0]

tensor([[0.4342, 0.1669, 0.1735, 0.2254],

[0.2207, 0.2352, 0.3322, 0.2119]])

训练我们的网络(forward only)

在这里,我们通过在网络中多次运行每个batch理来训练输入batchs上的网络,这称为epoch。这使得网络可以多次学习序列。然后进行损失计算和反向传播以最小化损失。在本节中,我们将一次性实现上面显示的所有代码片段。由于输入量较小,我们将只演示前向传递。

def gru(x, h):
 outputs = []
 for i in range(num_batches): # this loops over the batches 
 x = X[i]
 for i,sequence in enumerate(x): # iterates over the sequences in each batch
 z = torch.sigmoid(torch.matmul(sequence, Wz) + torch.matmul(h, Uz) + bz)
 r = torch.sigmoid(torch.matmul(sequence, Wr) + torch.matmul(h, Ur) + br)
 h_tilde = torch.tanh(torch.matmul(sequence, Wh) + torch.matmul(r * h, Uh) + bh)
 h = z * h + (1 - z) * h_tilde
 # Linear layer
 y_linear = torch.matmul(h, Wy) + by
 # Softmax activation function
 y_t = F.softmax(y_linear, dim=1)
 outputs.append(y_t)
 return torch.stack(outputs), h

此函数将向网络提供字母的引子,帮助创建初始状态并避免进行随机猜测。如下所示,生成的前两个字符串有点不稳定,但是经过几次传递之后,它似乎至少正确地获得了后面两个字符。然而,由于词汇量较小,这个网络很可能是过拟合的。

def sample(primer, length_chars_predict):
 
 word = primer
 primer_dictionary = [character_dictionary[char] for char in word]
 test_input = one_hot_encode(primer_dictionary, vocab_size)
 
 h = torch.zeros(1, hiddenSize)
 for i in range(length_chars_predict):
 outputs, h = gru(test_input, h)
 choice = np.random.choice(vocab_size, p=outputs[-1][0].numpy())
 word += character_list[choice]
 input_sequence = one_hot_encode([choice],vocab_size)
 return word
max_epochs = 10 # passes through the data
for e in range(max_epochs):
 h = torch.zeros(batch_size, hiddenSize)
 for i in range(num_batches):
 x_in = X[i]
 y_in = y[i]
 
 out, h = gru(x, h)
 print(sample('Ma',20))

最后

本教程的目的是提供GRU内部工作的演练,演示如何组合简单的矩阵操作可以制作如此强大的算法。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表