python 神经网络可以输出连续值_神经网络-循环神经网络(python3)

前言

很庆幸,也很荣幸,之前的两篇文章《神经网络-标准神经网络(python3)》和《神经网络-卷积神经网络(python3)》受到了大家的赞赏和好评。这让我们备受鼓舞且倍感欣慰。能够与众多业内同行以及学生共同进步是我们所希望看到的。这也为我们接下来的这篇文章的撰写注入了鲜活的动力。下面这篇文章是关于循环神经网络(recurrent neural network,RNN)的介绍,是我们对若干篇国外知名博主的博文以及一篇介绍性论文进行整理而得的。我们“取其精华,去其糟粕”,将其中讲解不透彻的地方进行了扩展和补充,使之更加通俗易懂。诚然,循环神经网络中反向传播部分的理解较为困难,且我们水平有限,因此在对这部分进行表述时难免有些瑕疵和缺陷。我们诚挚希望各位同行以及学生对本文提出宝贵的意见或建议。

循环神经网络

循环神经网络,即RNN的核心思想是对序列信息的利用。在传统的神经网络中我们假设所有的输入(以及输出)是相互独立的,然而有许多问题并不能用这种思想来解决。如果你想预测一个句子中的下一个单词,你就需要知道该单词之前都是些什么单词。RNN 的“循环”即体现在它对序列的每个元素执行相同的操作,且输出依赖于先前的计算。另一种理解 RNN 的方式是,它有着“记忆元件”,可以捕获之前的相关计算信息。理论上来说,RNN 可以处理任意长的序列信息,但是在实际中,它只能关注之前的几个时间步(由于内存和计算能力的限制)。下图1所示,是一个典型的 RNN 示意图。图1:循环神经网络及其按时间步展开的计算图

上图展示了一个 RNN 展开到一个完整的网络中的情形。通过展开,我们将网络表达为完整的序列。例如,如果我们关心的序列是一个包含 5 个单词的句子,那么这个网络就会被展开成一个横向延展为 5 层的神经网络,每层对应一个单词。其中的相关计算公式如下: 是在时间步

时刻的输入。

是在时间步

时刻的隐藏状态,是网络的“记忆元件”。

是由之前的隐藏状态和当前时间步的输入得到的,即

常是一个非线性函数,如 tanh 函数或 ReLu 函数。而最开始的隐藏状态通常初始化为 0 。

是在时间步

时刻的输出。例如:如果我们想要计算句子中的下一个单词,该输出可能是一个向量,其中每个元素代表对应单词是句子中下一个单词的概率(后面会详细说明)。即

其中有几点需要注意:隐藏状态

可以捕获之前所有时间步的相关信息。而当前时间步的输出

仅仅只由当前时间步的隐藏状态

计算得出。

在传统的神经网络中,每一层都使用不同的参数,然而 RNN 与此不同,它在每个时间步上都使用相同的参数(即上述的

)。这反应了我们在每个时间步上都执行相同操作的事实,只不过是输入不同而已。这极大地减少了我们要学习得到的参数的总量。

上图的每个时间步都有输出,然而根据任务的不同,这一点并不是必须的。例如:当预测一个句子所表达的情感时,我们可能只需要关注最后的输出,而不需要在每个单词之后都得到一个情感输出。同理,在有些时候,我们也不必在每个时间步都有输入。

语言模型

本文中,我们的目标是利用循环神经网络构建一个语言模型,意即:假如我们有一个包含

个单词的句子,一个语言模型允许我们预测观察句子(在给定数据集中)的概率为:

换句话说,一个句子出现的概率是每个给定当前单词的条件下,下一个单词出现概率的乘积。因此,句子he went to buy some chocolate出现的概率,是给定 he went to buy some的条件下 chocolate出现的概率上给定 he went to buy的条件下 some出现的概率以此类推。

那么这有什么用处呢?我们为什么要得到一个观察句子的概率分配呢?

首先,这样的一个模型可以作为一个评分机制。例如,一个机器翻译系统通常会为输入语句生成许多候选项,你可以使用一个语言模型在其中挑选出概率最大的一个句子。而从直觉上来讲,概率最大的句子在语法上应该是正确的。类似这样的评分机制还应用在语音识别系统中。

此外,在解决语言建模问题的同时也会产生一个很酷的“副作用”。由于我们可以预测给定当前单词的条件下,下一个单词的概率,因此我们可以产生一个新的文本,我们将这称之为生成模型。给定一个现有的单词序列,我们从预测的概率中抽取一个单词作为该序列的下一个单词,然后重复这个过程直到我们得到一个完整的句子。

Andrej Karparthy 有一篇很好的文章,展示了语言模型的能力。他的模型只针对单个字符进行训练,而不是完整的单词,并且可以生成从莎士比亚作品到 Linux 代码的任何东西。

值得注意的是,在上面的等式中,每个单词的概率都是根据前面所有单词而得到的。然而在实际中,由于计算能力或者内存的限制,许多模型很难表示这种长期依赖关系,其通常只关注之前的几个单词。从理论上来讲,循环神经网络能够捕获这种长期依赖关系,然而实际上并非这么简单。

准备训练数据以及预处理

为了训练我们的模型,我们需要有可以学习的文本。幸运的是,我们不需要任何标签来训练语言模型,仅仅是原始文本就可以。我在 Google 的 BigQuery 上,下载了一个数据集,该数据集包含 15,000 个 Reddit (一个社交新闻网站)上较长的评论。我们生成的文本将会看起来像是 Reddit 上的一个用户所发表的评论(但愿如此),即别人难以辨别该文本是模型生成的还是真正的用户所发表的。但是,和大多数机器学习项目一样,我们首先需要做一些预处理,以便将数据转换为正确的格式。

分词:现在我们已经有了原始文本,然而我们想要做的预测是基于每个单词的。这意味着我们必须将一整段评论划分为各个句子,再将每个句子划分为各个单词。我们可以只用空格来对一整段的评论进行划分,但是这样做缺乏对标点符号的正确处理。例如:句子“he left !”应该包含三个符号"he","left",“!”,我们将使用 NLTK 库中的word_tokenize和sent_tokenize方法来进行这种分词处理。由于分词并不是本文的重点,因此不对其具体处理细节进行过多讨论。

去除低频词:我们文本中的大多数单词都只出现一到两次。我们最好是去除这些低频词。庞大的词汇表会让我们模型的训练十分缓慢(稍后我们将讨论为什么会这样),并且由于我们没有这些词的上下文样本,所以我们无法学习如何正确地使用它们。这与我们人类的学习方式非常相似,要真正理解如何恰当地使用一个单词,你需要在不同的语境中看到它(这也正是我们学好英语的方法)。

在我们的代码中,我们将词汇表的大小限制为vocabulary_size,即最常用的单词的数量(我将词汇表的大小设置为 8000,但是这个数值是可以随意更改的)。我们将所有不包含在词汇表中的单词都用UNKNOWN_TOKEN代替。

例如:假如我们的词汇表中不包含单词“nonlinearities”,那么句子 nonlineraties are important in neural networks将会变成UNKNOWN_TOKEN are important in neural networks。

单词UNKNOW_TOKEN是我们词汇表的一部分,并且我们将像预测其他单词一样预测它。当我们生成新的文本时,我们可以再次替换UNKNOW_TOKEN,例如在词汇表中随机抽取一个单词对其进行替换,或者我们可以不断地生成句子直到得到一个不包含UNKNOW_TOKEN的句子。

开始符号和结束符号:我们还想知道哪些词可以开始和结束一个句子。为此,我们在每个句子中都预置了一个特殊的SENTENCE_START符号,以及一个SENTENCE_END符号。这也就是说,假定第一个符号是SENTENCE_START,那么就可以预测下一个单词(句子实际上的第一个单词)可能是什么?

构建数据集矩阵:我们的循环神经网络的输入是向量,而不是字符串。所以我们应该创建一个单词和索引之间的映射,index_to_word,和word_to_index。例如:单词“friendly”可能位于 2001 号索引上。于是一个训练样本可能是如下形式的向量:[0, 179, 341, 416] ,其中 0 对应于SENTENCE_START,而该样本的标签将会是 [179, 341, 416, 1] 。由于我们的目标是预测下一个单词,因此向量就是位移一个位置的结果,并且的最后一个元素应该是SENTENCE_END,对应于 1 。换句话说,对索引为 179 的单词的正确预测为 341 ,即 179 号单词的下一个单词应该是 341 号单词,416 号单词应该是该句子实际上的结尾单词。

import nltk

import intertool

import numpy as np

vocabulary_size = 8000

unknown_token = "UNKNOWN_TOKEN"

sentence_start_token = "SENTENCE_START"

sentence_end_token = "SENTENCE_END"

# 读取数据、预置SENTENCE_START和SENTENCE_END符号

print("Reading CSV file...")

with open('data/reddit-comments-2015-08.csv', 'rb') as f:

reader = csv.reader(f, skipinitialspace=True)

reader.next()

# 将整段评论划分为各个句子

sentences = itertools.chain(*[nltk.sent_tokenize(x[0].decode('utf-8').lower()) for x in reader])

# 预置SENTENCE_START和SENTENCE_END

sentences = ["%s %s %s" % (sentence_start_token, x, sentence_end_token) for x in sentences]

print "Parsed %d sentences." % (len(sentences))

# 将句子划分为各个符号

tokenized_sentences = [nltk.word_tokenize(sent) for sent in sentences]

# 计算各个单词的频率

word_freq = nltk.FreqDist(itertools.chain(*tokenized_sentences))

print("Found %d unique words tokens." % len(word_freq.items()))

# 获得高频单词并创建index_to_word和word_to_index向量

vocab = word_freq.most_common(vocabulary_size-1)

index_to_word = [x[0] for x in vocab]

index_to_word.append(unknown_token)

word_to_index = dict([(w,i) for i,w in enumerate(index_to_word)])

print("Using vocabulary size %d." % vocabulary_size)

print("The least frequent word in our vocabulary is '%s' and appeared %d times." % (vocab[-1][0], vocab[-1][1]))

# 用unknown_token替换掉所有不在我们词汇表中的单词

for i, sent in enumerate(tokenized_sentences):

tokenized_sentences[i] = [w if w in word_to_index else unknown_token for w in sent]

print("\nExample sentence: '%s'" % sentences[0])

print("\nExample sentence after Pre-processing: '%s'" % tokenized_sentences[0])

# 创建训练数据

X_train = np.asarray([[word_to_index[w] for w in sent[:-1]] for sent in tokenized_sentences])

y_train = np.asarray([[word_to_index[w] for w in sent[1:]] for sent in tokenized_sentences])

将我们的文本经过上述代码的处理,最终得到的训练样本将会是下面这种形式:

x:

SENTENCE_START what are you understanding about this ?

[0, 51, 27, 16, 10, 856, 53, 25]

y:

what are you understanding about this ? SENTENCE_END

[51, 27, 16, 10, 856, 53, 25, 1]

构建循环网络循环神经网络及其按时间步展开的计算图

接下来,让我们具体来看看我们的语言模型的 RNN 是什么样子的。首先,输入

是一个单词序列(如同上面所展示的训练样本)并且每个

都是一个单词。但是还有一点需要注意的,由于我们要应用矩阵乘法,所以我们并不能简单地用一个单词索引(比如36)作为输入,而是用尺寸为vocabulary_size的 one-hot 向量来表示每个单词。例如,索引为 36 的单词将会表示为在 36 位置上为 1 ,其他位置都为 0 的一个向量。因此,每个

都将会是一个向量,而

将会是一个矩阵。我们将在关于神经网络的代码中执行此转换,而不是在预处理中执行此转换。我们网络的输出

具有同样的形式,每个

都是一个尺寸为vocabulary_size的向量,该向量中的每一个元素代表了对应单词作为句子中下一个单词的概率。

正如本文第一部分所述,

由下式得出:

在上述代码中,我们已经将词汇表的尺寸设置为

,因此

是一个包含 8000 个元素的 one-hot 向量。我们假设隐藏层的尺寸为

,我们可以把隐藏层的尺寸看作是我们网络的“记忆元件”数量。设置更大的隐藏层尺寸(即更多的“记忆元件”数量)可以使我们的网络能够学习更复杂的模型,但是也会导致更多额外的计算开销。下面所展示的是各个矩阵或向量的尺寸:

是包含 8000 个元素的向量,

是包含 100 个元素的向量。

是 [100, 8000] 的矩阵,

是 [8000, 100] 的矩阵,而

是 [100, 100] 的矩阵。这些信息对我们了解具体的矩阵乘法过程十分有益。

是我们想要从数据中学习得到的网络的参数,因此,我们需要学习的参数总量为

。在

的情况下,该值为 1,610,000 。此外,从各个矩阵或向量的尺寸信息上我们还可以了解到我们模型的瓶颈所在。注意到,由于

是一个 one-hot 向量,将其与矩阵

做乘法本质上等同于从矩阵

中选择一个列,因此我们不必执行完整的矩阵乘法运算。于是,网络中最大的矩阵乘法就是

,这就是为什么我们要尽可能地控制我们词汇表的尺寸。

有了这些,我们就可以开始循环神经网络的代码编写了。

初始化

我们首先声明一个 RNN 类来初始化我们的参数。对参数

的初始化并不是想象的那么简单,我们需要将参数初始化为随机值,而不是 0。恰当的初始化工作是能够影响着训练结果的,这方面的研究有很多(本文不过多展开),而大量研究成果都表明:最好的初始化是依赖于激活函数(在我们的例子中是

函数)的初始化,一种比较推荐的方法是将权重初始化为

,其中

是与上一层相连的连接权重的数量。这听起来可能过于复杂,但不必担心,其实我们只要将参数初始化为较小的随机值,我们的网络就能正常工作。下面是初始化工作的相关代码:

class RNN:

def __init__(self, word_dim, hidden_dim=100, bptt_truncate=4):

# 将变量赋值给实例

self.word_dim = word_dim

self.hidden_dim = hidden_dim

self.bptt_truncate = bptt_truncate

# 随机初始化网络的参数

self.U = np.random.uniform(-np.sqrt(1./word_dim), np.sqrt(1./word_dim), (hidden_dim, word_dim))

self.V = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (word_dim, hidden_dim))

self.W = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (hidden_dim, hidden_dim))

上述代码中,word_dim是 one-hot 向量的尺寸 ,它应该等于我们词汇表的尺寸,hidden_dim是我们隐藏层的尺寸。暂且先不关心参数bptt_truncate,我们将在下文中对其进行解释。

前向传播

接下来,我们将根据上文中的公式来编写前向传播的代码:

def forward_propagation(self, x):

# 总的时间步数

T = len(x)

# 前向传播期间我们将所有的隐藏状态存储在 s 中,因为稍后我们将会用到它

# 我们增加了一个额外的元素作为初始隐藏状态,并将其设置为 0

s = np.zeros((T + 1, self.hidden_dim))

s[-1] = np.zeros(self.hidden_dim)

# 每个时间步的输出,同样将其保存起来,在以后会用到

o = np.zeros((T, self.word_dim))

# 对每个时间步...

for t in np.arange(T):

# 注意到这里我们用 x[t] 对 U 进行索引(如上文所述),等同于是 U 和一个 one-hot 向量相乘

s[t] = np.tanh(self.U[:,x[t]] + self.W.dot(s[t-1]))

o[t] = softmax(self.V.dot(s[t]))

return [o, s]

RNN.forward_propagation = forward_propagation

我们不仅返回计算出来的输出,还返回隐藏状态。我们稍后将利用它们进行梯度计算,故为了避免重复计算,我们在这里将它们的值返回。每个

是一个向量,其中每个元素代表对应我们词汇表中的单词的概率预测值,而我们想要的是最有可能的下一个单词,因此我们要挑选出最大的概率预测值。因此,我们定义一个名为predict的函数:

def predict(self, x):

# 执行前向传播操作并返回最高概率预测值的索引

o, s = self.forward_propagation(x)

return np.argmax(o, axis=1)

RNN.predict = predict

接下来,实例化我们的RNN类,并调用forward_propagation方法:

np.random.seed(10)

model = RNN(vocabulary_size)

o, s = model.forward_propagation(X_train[10])

print(o.shape)

print(o)

将输出打印出来,可以看到我们得到的输出

如下:

其中对于句子中的每个单词(上面句子中有45个单词),我们的模型都产生了 8000 个预测值,分别代表对应单词是下一个单词的概率。由于我们将

初始化为随机值,因此这些预测概率值现在也完全是随机的。接下来,给出对于每个单词的最高预测概率值的索引:

predictions = model.predict(X_train[10])

print(predictions.shape)

print(predictions)

将其打印出来,可得到一个包含45个元素的向量,如下:

计算损失

为了训练我们的网络,我们需要一种方法来衡量损失。我们将其称之为损失函数

,而我们的目标就是找到最恰当

的来最小化训练数据上集的损失函数。通常选择交叉熵损失来作为损失函数。如果我们有

个训练样本(我们的文本中单词个数),

个类别(我们词汇表的尺寸),那么关于我们的预测值

和真实标签

的损失是:

上述公式所得即平均交叉熵损失,接下来我们用代码对其进行实现:

def calculate_total_loss(self, x, y):

L = 0

# 对于每个句子...

for i in np.arange(len(y)):

o, s = self.forward_propagation(x[i])

# 我们只关注正确的单词预测

correct_word_predictions = o[np.arange(len(y[i])), y[i]]

# 根据我们的情况增加损失

L += -1 * np.sum(np.log(correct_word_predictions))

return L

def calculate_loss(self, x, y):

# 将总损失除以训练样本个数,得到平均交叉熵损失

N = np.sum((len(y_i) for y_i in y))

return self.calculate_total_loss(x,y)/N

RNN.calculate_total_loss = calculate_total_loss

RNN.calculate_loss = calculate_loss

让我们回过头来想想,我们随机预测概率值的损失应该是什么。我们的词汇表中有

个单词,所以每个单词作为句子中下一个单词的概率应该为

,于是产生的损失

。我们将通过该公式得到的损失与实际的损失进行对比,代码如下:

# 限制为1000个样本以节省时间

print "Expected Loss for random predictions:%f" % np.log(vocabulary_size)

print "Actual loss:%f" % model.calculate_loss(X_train[:1000], y_train[:1000])

Expected Loss for random predictions: 8.987197

Actual loss: 8.987440

可以看到,通过公式

得到的损失与实际的损失十分接近,说明从直观上来说我们的随机初始化工作是正确的。

用SGD和 BPTT 来训练 RNN

我们的目的是要找到最佳参数

,使其能够最小化训练数据的总损失。为此,一种常见的做法是使用 SGD 即随机梯度下降。随机梯度下降背后的原理其实十分简单,我们迭代遍历全部的训练样本,每一次迭代都将参数向能够减少损失的方向推动。这个方向由损失的梯度决定:

。随机梯度下降还需要一个学习率,它定义了我们在每次迭代中对参数进行更新的幅度。SGD 是最流行的优化算法,其不仅适用于神经网络,也适用于许多其他的机器学习算法。因此,关于如何使用批处理、并行性和自适应学习率对 SGD 进行优化的研究有很多。虽然基本思想很简单,但是以一种非常有效的方式实现 SGD 会变得非常复杂。在这里我们将实现一个比较简单的 SGD 版本,其原理比较容易理解。

但是,我们如何计算上面提到的梯度呢?在传统的神经网络中,我们通过反向传播来实现。而在 RNN 中,我们对反向传播进行稍稍变体,使用一种称之为 BPTT (穿越时间的反向传播)的方式来计算梯度。由于网络中的所有时间步都共享参数,因此每个输出的梯度计算不仅取决于当前时间步,还取决于之前时间步。由于篇幅有限,我们将在另一篇单独的文章中对 BPTT 进行详细介绍,而在这里我们可以将 BPTT 当成一个黑箱,输入训练样本

,得到输出即梯度

def bptt(self, x, y):

T = len(y)

# 执行前向传播操作

o, s = self.forward_propagation(x)

# 初始化梯度

dLdU = np.zeros(self.U.shape)

dLdV = np.zeros(self.V.shape)

dLdW = np.zeros(self.W.shape)

delta_o = o

delta_o[np.arange(len(y)), y] -= 1.

# 对每一个输出进行反向传播

for t in np.arange(T)[::-1]:

dLdV += np.outer(delta_o[t], s[t].T)

# 计算初始 delta

delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))

# 通过时间反向传播 (步数至少为 self.bptt_truncate )

for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:

# 输出 "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)

dLdW += np.outer(delta_t, s[bptt_step-1])

dLdU[:,x[bptt_step]] += delta_t

# 为下一次迭代更新 delta

delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)

return [dLdU, dLdV, dLdW]

RNN.bptt = bptt

SGD 实现

现在我们可以通过实现 SGD 来计算损失对参数的梯度了。我将通过两个步骤来实现 SGD:1、 一个sdg_step函数用来计算梯度并执行一个批量的更新。2、一个在训练数据集上进行迭代并调整学习速率的外层循环。

# 执行一步 SGD

def numpy_sdg_step(self, x, y, learning_rate):

# 计算梯度

dLdU, dLdV, dLdW = self.bptt(x, y)

# 根据梯度和学习率对参数进行更新

self.U -= learning_rate * dLdU

self.V -= learning_rate * dLdV

self.W -= learning_rate * dLdW

RNN.sgd_step = numpy_sdg_step

# SGD 外层循环

# - model: The RNN model instance

# - X_train: The training data set

# - y_train: The training data labels

# - learning_rate: Initial learning rate for SGD

# - nepoch: Number of times to iterate through the complete dataset

# - evaluate_loss_after: Evaluate the loss after this many epochs

def train_with_sgd(model, X_train, y_train, learning_rate=0.005, nepoch=100, evaluate_loss_after=5):

# We keep track of the losses so we can plot them later

losses = []

num_examples_seen = 0

for epoch in range(nepoch):

# Optionally evaluate the loss

if (epoch % evaluate_loss_after == 0):

loss = model.calculate_loss(X_train, y_train)

losses.append((num_examples_seen, loss))

time = datetime.now().strftime('%Y-%m-%d%H:%M:%S')

print("%s: Loss after num_examples_seen=%depoch=%d:%f" % (time, num_examples_seen, epoch, loss))

# Adjust the learning rate if loss increases

if (len(losses) > 1 and losses[-1][1] > losses[-2][1]):

learning_rate = learning_rate * 0.5

print("Setting learning rate to%f" % learning_rate)

sys.stdout.flush()

# For each training example...

for i in range(len(y_train)):

# One SGD step

model.sgd_step(X_train[i], y_train[i], learning_rate)

num_examples_seen += 1

大功告成!接下来我们看一下训练我们的网络需要多长时间。

np.random.seed(10)

model = RNN(vocabulary_size)

%timeit model.sgd_step(X_train[10], y_train[10], 0.005)

在笔记本电脑上执行一步 SGD 大约需要 350 毫秒。我们的训练数据集中大约有 80,000 个样本,所以一次迭代(遍历整个数据集)需要几个小时。多次迭代可能需要几天甚至几周的时间!而且,和很多公司和研究人员所使用的数据集相比较,我们所研究的只是一个很小的数据集而已。那么,我们该怎么办呢?

幸运的是,有很多方法可以提升我们模型的训练速度。我们可以不改变我们模型的代码,而只是使我们的代码运行得更快(例如使用GPU),或者我们可以修改我们的模型以减少计算上的开销,或者同时使用这两种方法。研究人员已经找到了许多方法来降低模型的计算成本,例如使用分层的 Softmax 或添加投影层来避免大的矩阵乘法。但是,我希望我们的模型能够保持简单,所以我们选择走第一条路线:让我们的代码使用 GPU 来更快地运行。在此之前,让我们试着用一个小的数据集运行 SGD ,并检查损失是否真的减少了。

np.random.seed(10)

# 对一个小规模的数据集进行训练,看看会发生什么。

model = RNN(vocabulary_size)

losses = train_with_sgd(model, X_train[:100], y_train[:100], nepoch=10, evaluate_loss_after=1)

运行结果如下所示:

值得庆幸的是,损失的确如我们所期待的那样减少了。接下来,使用 GPU 来运行我们的代码,让我们的模型自动生成句子。然而,实际我们会发现我们模型所生成的句子并不是十分理想,绝大多数句子要么没有意义,要么语法有错误。其中一个原因可能是我们的网络训练时间不够长(或者没有使用足够的训练数据)。可能确实如此,但很可能不是主要原因。我们的 RNN 无法生成有意义的文本,是因为它无法学习相隔的几个时间步之间的依赖关系。这也是为什么 RNN 在最开始被发明的时候没能得到普及。它在理论上很优美,但是在实践中却并没有很好的发挥作用。

现在,训练 RNN 的难点部分都已经理解了,在另一篇文章中,我们将更详细的探讨通过时间的反向传播(BPTT)算法并演示所谓的梯度消失问题。这将促使我们转向更复杂的 RNN 模型,例如 LSTM,它是当前许多 NLP 问题的解决方法之一(并且可以生成更好的 Reddit 评论)。所以即使本文所实现的 RNN 并没有达到我们所期望的效果,也不用气馁,因为我们在本文中所学习到的全部内容也同样适用于 LSTM 和其他 RNN 模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值