NLP:循环网络的记忆功能

当然,文档中的词很少是完全独立的,它们的出现会影响文档中的其他词或者受到文档中其他词的影响:

The stolen car sped into the arena.

(那辆偷来的汽车飞快地开进了竞技场。)

The clown car sped into the arena.

(那辆小丑车快速驶进了舞台。)

当读者读到这两个句子的末尾时可能会产生两种完全不同的情感。这两个句子的形容词、名词、动词和介词短语结构是完全相同的,但位于句首的形容词极大地影响了读者后续的推断。

大家能想出一种对这种关系进行建模的方法吗?一种形容词不直接修饰或出现在句首时也能理解“arena”甚至“sped”的隐含意义可能会稍有不同的方法?

假如大家能想到一种方式“记忆”之前时刻发生的事情(尤其是当我们在t+1时刻时,t时刻发生的),我们就能捕获当序列中某些词条出现时,其他词条相对应会出现的模式。循环神经网络(recurrent neural net,RNN)使神经网络能够记住句子中出现过的词。

我们从图8-3可以看到,隐藏层中的单个循环神经元增加了一个循环回路使t时刻隐藏层的输出重新输入隐藏层中。t时刻的输出会作为t+1时刻的输入。这个新的输入会由t+1时刻的网络处理来产生t+1时刻隐藏层的输出。而t+1时刻的输出接下来又会被重新作为t+2时刻的输入,以此类推[2]。

NLP:循环网络的记忆功能

图8-3 循环神经网络

尽管根据时间变化影响状态的思想一开始可能会让人感觉有些困惑,但是其基本概念简单明了。对于传入一般前馈网络的每个输入,我们在t时刻得到的网络输出会作为网络的一个额外输入,与下一份在t+1时刻的数据一起输入网络。这样,我们就可以告诉前馈网络之前发生了什么和“现在”正在发生什么。

重要说明  

在本章及下一章,我们谈论最多的事情就是时刻或时间步(time step)。这和单独的数据样本不是一回事。我们谈论的是一份数据样本分解成更小的可以表示时间变化的块。单个数据样本仍是文本的某一部分,如一小段影评或者一条Twitter。和之前一样,我们还是会对句子进行分词,但是不同于以往一次性地将所有词条输入网络,我们会在一个时刻输入一个词条。这和有多个新文本的样本完全不同。这些词条仍然是同一个标签的一个数据样本的一部分。

我们可以认为t代表词条序列的下标。所以t = 0是文档中的第一个词条,而t + 1则代表文档的下一个词条。那些在文档中依次出现的词条将会作为每个时刻(时间步)或者词条步的输入。并且,词条不一定是某个词,单个字符也可以作为词条。在某一时刻输入一个词条是将数据样本传入网络的一个子步(substep)。

自始至终,我们将当前时刻标为t,下一时刻标为t + 1。

如图8-3所示,我们可以看到一个循环网络:整个循环是由一个或者多个神经元组成的前馈网络。网络隐藏层的输出和普通的输出一样,但是它本身会和下一个时刻的正常输入数据一起作为输入回传进网络。这个反馈表示为从隐藏层的输出指向它的输入的箭头。

理解这个过程的一个更简单的方法(通常如此显示)是展开这个网络。图8-4从新的角度,展示了网络随时间变量(t)展开两次的图形,显示了t + 1时刻和t + 2时刻的网络层。

每个时刻由完全相同的神经网络展开后的一列神经元表示。就像在时刻中查看每个样本的神经网络的剧本或视频帧一样。右侧网络是左侧网络的未来版本。在一个时刻(t)的隐藏层的输出被回传到隐藏层以及用作右侧下一个时刻(t + 1)的输入数据,如此循环往复。此图显示了这一展开的两次迭代,因此对于t=0、t=1和t=2,有3列神经元。

NLP:循环网络的记忆功能

图8-4 展开循环神经网络

这个可视化视图中的所有垂直路径都是克隆的,或者说是完全相同神经元的视图。它们在时间轴上是单个网络表示的。当讨论信息在反向传播算法中是如何在网络中前向和反向流动时,这种可视化非常有用。但是,当我们观察这3个展开的网络时,请记住它们都是同一个网络的不同快照(snapshot),只有一组权重。

我们放大一个循环神经网络展开前的原始表示,揭示输入和权重之间关系。这个循环网络的各个层如图8-5和图8-6所示。

NLP:循环网络的记忆功能

图8-5 t = 0时刻的循环神经网络

NLP:循环网络的记忆功能

图8-6 t = 1时刻的循环神经网络

处于隐藏状态的每个神经元都有一组权重,它们应用于每个输入向量的每个元素,这和一般的前馈网络一样。但是,现在我们有一组额外的可训练权重,这些权重应用于前一个时刻隐藏层神经元的输出。当我们逐个词条地输入序列时,网络可以学习分配给“过去”的事件多少权重或者重要度。

提示  

序列中的第一个输入没有“过去”,因此t = 0时刻的隐藏状态从其t−1时刻接收输入为0。“填充”初始状态值的另一种方法是,首先将相关但分开的样本一个接一个地传递到网络中,然后每个样本的最终输出用于下一个样本t = 0时刻的输入。在8.5.1节中,我们将学习如何使用另一种“填充”方法保留数据集中的更多信息。

我们回到数据:假设我们有一组文档,每篇文档都是一个带标签的样本。对于每个样本,不同于上一章中一次性地将词向量集合传递进卷积神经网络(如图8-7所示),这次是从样本中一次取一个词条并将其单独传递到我们的RNN中(如图8-8所示)。

在循环神经网络中,我们传入第一个词条的词向量并获得网络的输出,然后传入第二个词条的词向量,同时也传入第一个词条的输出!然后传入第三个词条的词向量以及第二个词条的输出,以此类推。网络中有前后概念和因果关系,以及一些模糊的时间概念(如图8-8所示)。

现在我们的网络正在记住一些东西!好吧,至少有一点儿像。但还有一些事情需要我们弄明白,首先,反向传播算法是如何在这样的结构中工作的?

NLP:循环网络的记忆功能

图8-7 传入卷积网络的数据

NLP:循环网络的记忆功能

图8-8 传入循环网络的数据

8.1.1 随时间反向传播算法

到目前为止,我们讨论的所有网络都有一个标签(目标变量),而循环神经网络也不例外。但是,并不是说每个词条都有一个标签,而是每个样本中的所有词条只有一个标签。也就是说,对于样本文档,只有一个标签。

... and that is enough.

——Isadora Duncan

提示 

我们谈谈网络在各个时刻输入的词条,循环神经网络可以在任何类型的时间序列数据上工作。我们的词条可以是离散的或连续的:如来自气象站的读数、音符、句子中的字符等,由大家决定。

这里,我们开始会在最后一个时刻查看网络的输出,并将该输出与标签进行比较。这就是(目前)对于误差(error)的定义,而误差是我们的网络最终想要尽量减小的目标,但是这里要介绍的处理输出的方式与前几章的有所不同。对于给定的数据样本,我们可以将其分成较小的片段,这些片段按顺序进入网络。但是,我们并不直接处理这些由“子样本”产生的所有输出,而是将其反馈给网络。

我们只关心最终的输出,至少现在如此。将序列中的每个词条输入网络中,并根据序列中最后一个时刻(词条)的输出计算损失,如图8-9所示。

NLP:循环网络的记忆功能

图8-9 只有最后的输出影响结果

对于给定样本的误差,我们需要确定哪些权重需要更新以及需更新多少。在第5章中,我们学习了在标准的网络中如何反向传播误差,并且我们知道对每个权重校正多少取决于该权重对误差的贡献程度。我们可以输入样本序列中的各个词条,并根据之前时刻的网络输出计算误差,但是这也正是不能在时间序列上应用反向传播算法的原因。

可以这样来考虑:将整个过程视为基于时间的。我们在每个时刻取一个词条,从t = 0处的第一个词条开始,将它输入当前的隐藏层神经元——图8-9中的下一列。当我们这样做时,网络会展开并揭示下一列,为序列中的下一个词条做好准备。隐藏层的神经元不断展开,一次一个,就像是音乐盒或钢琴的演奏。当我们到达终点,在输入样本的所有片段之后,网络将停止展开并且我们将获得目标变量的最终输出标签。我们可以使用该输出来计算误差并调整权重。这样,我们就完成了这个展开网络计算图的所有环节。

此时,我们可以将整个输入视为静态的。通过计算图我们可以看到各个神经元分别送入了哪个输入。一旦知道各个神经元是如何工作的,我们就可以循着之前的方法,像在标准前馈网络中做的那样运用反向传播

我们将使用链式法则反向传播到前一层。但是,不同于传播到上一层,这里是传播到过去的层,就好像每个展开的网络版本都不同(如图8-10所示)。数学公式是一样的。

NLP:循环网络的记忆功能

图8-10 随时间反向传播

我们将反向传播在最后一个时刻获得的误差。对于每个“较早”的时刻,都要执行更新时刻的梯度。对于该样本,在计算了所有词条的梯度之后,我们将聚合这些校正值并将它们应用于整套权重的更新,直至回到时刻t = 0。

简要回顾

8.1.2 不同时刻的权重更新

我们已经将看似奇怪的循环神经网络转换为类似于标准前馈网络的东西,这样可以使权重更新变得相当简单。但是这里有一个问题。更新过程中棘手的部分是我们正在更新的权重不是神经网络的不同分支,每个分支代表着位于不同时刻的相同网络。各个时刻的权重是相同的(如图8-10所示)。

一个简单的解决方案是计算各个时刻的权重校正值但不立即更新。在前馈网络中,一旦为输入样本计算了所有梯度,所有权重的校正值就会被计算。这对循环神经网络同样适用,但对该输入样本我们必须一直保留这些校正值,直至回到时刻t = 0。

梯度计算需要基于权重对误差的贡献量。这里是令人费解的部分:在时刻t一个权重在初次计算时对误差产生了贡献,而该权重在时刻t + t会接收到不同的输入,因此之后对误差的贡献量也会有所不同。

我们可以计算出权重在每个时刻的不同校正值(就像它们在气泡中一样),然后聚合所有校正值并在学习阶段的最后一步将其应用于隐藏层的各个权重。

提示  

在所有这些示例中,我们前向传播传入单个训练样本,然后反向传播误差。与所有神经网络一样,这种前向传递可以依据每个训练样本进行,也可以分批进行。事实证明,批处理除加速之外还有其他好处。但现在,请仅从单个数据样本、单个句子或文档来考虑这些过程。

这似乎很神奇。对于单个数据样本,随时间反向传播算法中的单个权重在一个时刻t可能会在一个方向上进行调整(取决于其在时刻t对输入的反应),然后在时刻t – 1在另一个方向上进行调整(取决于其在时刻t − 1对输入的反应)!但是请记住,不管中间步骤有多复杂,神经网络一般都是通过最小化损失函数来工作的,所以总体来说,它会对这个复杂的函数进行优化。当对每个数据样本应用一次权重更新时,网络将确定(假设它是收敛的)对该输入样本来说最适合处理此任务的神经元的权重。

至关重要的前期输出

有时,我们也会关心在各个中间时刻生成的整个序列。在第9章中,我们将看到一些示例,它们展示了给定时刻t的输出与最终时刻的输出同样重要。图8-11展示了在任意给定时刻捕获误差的路径,并在反向传播期间使用该误差反向调整网络的所有权重。

NLP:循环网络的记忆功能

图8-11 所有的输出都很重要

这个过程类似于在n个时刻执行普通的随时间反向传播。在本例中,我们现在正在同时从多个源反向传播误差。但是正如第一个例子一样,权重的校正值是累积的,我们从最后一个时刻一直反向传播到初始时刻,并且对每个权重计算要更新的总数,然后对于在倒数第二个时刻计算出的误差进行同样的处理,并将反向进行处理直到时刻t = 0将所有的校正值加起来。重复这个过程,直到回到时刻t = 0,然后继续反向传播,此时要聚合的值只有一个。接着,我们可以将更新的总和一次性地应用于相关隐藏层的所有权重。

在图8-12中,我们可以看到误差从每个输出反向传播到t = 0,并在最后对权重应用更新之前进行聚合。这是本节中最重要的内容。与标准的前馈网络一样,对于该输入(或一组输入),只有在计算了整个反向传播步骤中各权重需要更新的校正值之后,我们才会更新权重值。在循环神经网络的情况下,这种反向传播包含了所有时刻到t = 0的更新。

NLP:循环网络的记忆功能

图8-12 多输出和随时间反向传播

较早地更新权重会较早地“污染”反向传播中的梯度计算。请记住梯度是根据特定的权重计算的,所以如果要提前更新它,例如在时刻t,那么当计算时刻t − 1的梯度时,权重的值(记住它在网络中的权重位置是相同的)会发生变化。如果根据时刻t − 1的输入计算梯度,计算将是错误的。我们将因为一个权重没有对误差做出的贡献而惩罚(或奖励)它!

8.1.3 简要回顾

现在我们走到哪一步了?我们已经将每个数据样本分割成词条,然后一个接一个地把它们输入一个前馈网络。对于每个词条,不仅要输入词条本身,还要输入前一个时刻的输出。在时刻0,我们输入初始词条和0,后者是一个0向量,这是因为之前没有输出。我们将比较最终词条的网络输出与预期标签之间的差异以获得误差,然后随时间反向传播该误差至网络的每个权重,最后我们聚合计算出的校正值,并将它们同时应用于网络。

我们现在有一个前馈网络,它有一些类似于时间的概念和一个能够保存发生在时间轴上的记忆的基本工具。

8.1.4 难点

尽管一个循环神经网络需要学习的权重(参数)可能相对较少,但是从图8-12中可以看出,训练一个循环神经网络的代价高昂,尤其是对于较长的序列(如10个词条)。我们拥有的词条越多,每个时刻误差必须反向传播的时间越长。而对于每一时刻,都有更多的导数需要计算。虽然循环神经网络的效果并不比其他网络的效果差,但是请准备好用计算机的排气扇给房子供暖吧。

撇开新的供热能源不谈,我们已经给了神经网络一个基本的记忆能力,但是当它们(指网络时刻)变深,更多的麻烦也出现了(一个也可以在常规的前馈网络中看到的问题)。梯度消失问题(vanishing gradient problem)有一个推论:梯度爆炸问题(exploding gradient problem),它们的思想是,随着网络变得更深(更多层)时,误差信号会随着梯度的每一次计算消散或增长。

循环神经网络也面临着同样的问题,因为在数学上,时刻的每一次后退都相当于将一个误差反向传播到前馈网络的前一层。但是这里更糟!尽管由于这个原因,大多数前馈网络往往只有几层深,但是当我们要处理的是5个、10个,甚至数百个词条的序列时,要深入到100层网络的底层还是很困难的。不过,一个让我们可以继续工作、减轻压力的因素在于:尽管梯度可能会在计算最后一次权重集的过程中消失或爆炸,但是实际上我们只更新了一次权重集,并且每个时刻的权重集都是相同的。仍然有些信息会传递出去,虽然它可能不是我们认为所能创建的理想记忆状态,但是不必害怕,研究人员正在研究这个问题,对于这个挑战在下一章我们会有一些答案。

听了如此多令人郁闷的坏消息,现在我们来看一些魔法吧。

8.1.5 利用Keras实现循环神经网络

我们将从与上一章中所使用的相同的数据集和预处理开始。首先,加载数据集,获取标签并随机打乱样本,然后对文档分词并使用谷歌的Word2vec模型使其向量化,接下来,获取标签,最后我们按80/20的比例将原始数据分成训练集和测试集。

首先,我们需要导入数据处理和循环神经网络训练所需的所有模块,如代码清单8-1所示。

代码清单8-1 导入所有模块

>>> import glob
>>> import os
>>> from random import shuffle
>>> from nltk.tokenize import TreebankWordTokenizer
>>> from nlpia.loaders import get_data
>>> word_vectors = get_data('wv')

然后,我们可以构建数据预处理模块,它能对数据进行训练前的处理,如代码清单8-2所示。

代码清单8-2 数据预处理模块

>>> def pre_process_data(filepath):
...     """
...     Load pos and neg examples from separate dirs then shuffle them
...     together.
...     """
...     positive_path = os.path.join(filepath, 'pos')
...     negative_path = os.path.join(filepath, 'neg')
...     pos_label = 1
...     neg_label = 0
...     dataset = []
...     for filename in glob.glob(os.path.join(positive_path, '*.txt')):
...         with open(filename, 'r') as f:
...             dataset.append((pos_label, f.read()))
...     for filename in glob.glob(os.path.join(negative_path, '*.txt')):
...         with open(filename, 'r') as f:
...             dataset.append((neg_label, f.read()))
...     shuffle(dataset)
...     return dataset

与之前一样,我们可以将数据分词和向量化的方法写在一个函数中,如代码清单8-3所示。

代码清单8-3 数据分词和向量化

>>> def tokenize_and_vectorize(dataset):
...     tokenizer = TreebankWordTokenizer()
...     vectorized_data = []
...     for sample in dataset:
...         tokens = tokenizer.tokenize(sample[1])
...         sample_vecs = []
...         for token in tokens:
...             try:
...                 sample_vecs.append(word_vectors[token])
...             except KeyError:
...                 pass  ⇽--- 在谷歌w2v词汇表中没有匹配的词条
...         vectorized_data.append(sample_vecs)
...     return vectorized_data

并且我们需要将目标变量提取(解压)到单独的(但对应的)样本中,如代码清单8-4所示。

代码清单8-4 目标变量解压缩

>>> def collect_expected(dataset):
...      """ Peel off the target values from the dataset """
...      expected = []
...      for sample in dataset:
...          expected.append(sample[0])
...      return expected

既然我们已经写好了所有的预处理函数,就需要在数据上运行它们,如代码清单8-5所示。

代码清单8-5 加载和准备数据

>>> dataset = pre_process_data('./aclimdb/train')
>>> vectorized_data = tokenize_and_vectorize(dataset)
>>> expected = collect_expected(dataset)
>>> split_point = int(len(vectorized_data) * .8)   ⇽--- 按80/20的比例划分为训练集和测试集(不用混洗)
>>> x_train = vectorized_data[:split_point]
>>> y_train = expected[:split_point]
>>> x_test = vectorized_data[split_point:]
>>> y_test = expected[split_point:]

我们将为这个模型使用相同的超参数:每个样本使用400个词条,批大小为32。词向量是300维,我们将让它运行2个周期。具体做法参见代码清单8-6。

代码清单8-6 初始化网络参数

>>> maxlen = 400
>>> batch_size = 32
>>> embedding_dims = 300
>>> epochs = 2

接下来,我们需要再次填充和截断样本。通常我们不需要对循环神经网络使用填充或截断,因为它们可以处理任意长度的输入序列。但是,在接下来的几个步骤中,我们将看到所使用的模型要求输入指定长度的序列。具体做法参见代码清单8-7。

代码清单8-7 加载测试数据和训练数据

>>> import numpy as np

>>> x_train = pad_trunc(x_train, maxlen)
>>> x_test = pad_trunc(x_test, maxlen)

>>> x_train = np.reshape(x_train, (len(x_train), maxlen, embedding_dims)) 
>>> y_train = np.array(y_train)
>>> x_test = np.reshape(x_test, (len(x_test), maxlen, embedding_dims)) 
>>> y_test = np.array(y_test)

现在我们已经获得了数据,是时候构建模型了。我们将再次从Keras的一个标准的分层模型Sequential()(分层的)模型开始,如代码清单8-8所示。

代码清单8-8 初始化一个空的Keras网络

>>> from keras.models import Sequential
>>> from keras.layers import Dense, Dropout, Flatten, SimpleRNN
>>> num_neurons = 50
>>> model = Sequential()

然后,和之前一样,神奇的Keras处理了组装神经网络的各个复杂环节:我们只需要将想要的循环层添加到我们的网络中,如代码清单8-9所示。

代码清单8-9 添加一个循环层

>>> model.add(SimpleRNN(
...    num_neurons, return_sequences=True,
...    input_shape=(maxlen, embedding_dims)))

现在,基础模块已经搭建完毕,可以接收各个输入并将其传递到一个简单的循环神经网络中(不简单的版本将在下一章介绍),对于每个词条,将它们的输出集合到一个向量中。因为我们的序列有400个词条长,并且使用了50个隐藏层神经元,所以这一层的输出将是一个400个元素的向量,其中每个元素都是一个50个元素的向量,每个神经元对应着一个输出。

注意这里的关键字参数return_sequences。它会告诉网络每个时刻都要返回网络输出,因此有400个向量,每个向量为50维。如果return_sequences被设置为False(Keras的默认行为),那么只会返回最后一个时刻的50维向量。

在本例中,50个神经元的选择是任意的,主要是为了减少计算时间。我们用这个数字做实验,来看看它是如何影响计算时间和模型精确率的。

提示  

一个好的经验法则是尽量使模型不要比训练的数据更复杂。说起来容易做起来难,但是这个想法为我们在数据集上做实验时的调参提供了一个基本法则。较复杂的模型对训练数据过拟合,泛化效果不佳;过于简单的模型对训练数据欠拟合,而且对于新数据也没有太多有意义的内容。我们会看到这个讨论被称为偏差与方差的权衡。对数据过拟合的模型具有高方差和低偏差,而欠拟合的模型恰恰相反:低方差和高偏差;它会用一致的方式给出答案,结果把一切都搞错了。

注意,我们再次截断并填充了数据,这样做是为了与上一章的CNN例子作比较。但是当使用循环神经网络时,通常不需要使用截断和填充。我们可以提供不同长度的训练数据,并展开网络,直到输入结束,Keras对此会自动处理。问题是,循环层的输出长度会随着输入时刻的变化而变化。4个词条的输入将输出4个元素长的序列。100个词条的序列将产生100个元素长的序列。如果我们需要把它传递到另一个层,即一个期望输入的维度统一的层,那么上述不等长的结果就会出现问题。但在某些情况下,这种不等长的序列输入也是可以接受的,甚至本来就期望如此。但是还是先回到我们这里的分类器,参见代码清单8-10。

代码清单8-10 添加一个dropout层

>>> model.add(Dropout(.2))

>>> model.add(Flatten())
>>> model.add(Dense(1, activation='sigmoid'))

我们要求上述简单的RNN返回完整的序列,但是为了防止过拟合,我们添加了一个Dropout层,在每个输入样本上随机选择输入,使这些输入有20%的概率为零,最后再添加一个分类器。在这种情况下,我们只有一个类:“Yes - Positive Sentiment - 1”或“No - Negative Sentiment - 0”,所以我们选择只有单个神经元(Dense(1))的层并使用sigmoid激活函数。但是该稠密层需要输入一个由n个元素组成的扁平的向量(每个元素都是一个浮点数)。SimpleRNN输出的是一个400个元素长的张量,张量中的每个元素都是50个元素长。但是前馈网络并不关心元素的顺序而只关心输入是否符合网络的需要,所以我们使用Keras提供的一个非常方便的网络层Flatten()将输入从400 × 50的张量扁平化为一个长度为20 000个元素的向量。这就是我们要传递到最后一层用来做分类的向量。实际上,Flatten层是一个映射。这意味着误差将从最后一层反向传播回RNN层的输出,而如前所述,这些反向传播的误差之后将在输出的合适点随时间反向传播。

将循环神经网络层生成的“思想向量”传递到前馈网络中,将不再保留我们努力试图想要包含的输入顺序的关系。但重要的是我们注意到,与词条的序列相关的“学习”发生在RNN层本身;通过随时间反向传播过程中误差的聚合将这种关系编码进了网络中,并将其表示为“思想向量”本身。我们基于思想向量作出的决策,通过分类器,就特定的分类问题向思想向量的“质量”提供反馈。我们可以“评估”我们的思想向量,并以其他方式使用RNN层,更多内容将在下一章讨论。(大家能感觉到我们提到下一章时的兴奋吗?)坚持下去,这些知识对于理解下一部分是至关重要的。

本文摘自《自然语言处理实战》

NLP:循环网络的记忆功能

语言是人类建立共识的基础。人们之间交流的不仅有事实,还有情感。通过语言,人们获得了经验领域之外的知识,并通过分享这些经验来构建理解的过程。通过本书,大家将会深入理解自然语言处理技术的原理,有朝一日可能创建出能通过语言来了解人类的系统。

本书是介绍自然语言处理(NLP)和深度学习的实战书。NLP已成为深度学习的核心应用领域,而深度学习是NLP研究和应用中的必要工具。本书分为3部分:第一部分介绍NLP基础,包括分词、TF-IDF向量化以及从词频向量到语义向量的转换;第二部分讲述深度学习,包含神经网络、词向量、卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)网络、序列到序列建模和注意力机制等基本的深度学习模型和方法;第三部分介绍实战方面的内容,包括信息提取、问答系统、人机对话等真实世界系统的模型构建、性能挑战以及应对方法。

本书面向中高级Python开发人员,兼具基础理论与编程实战,是现代NLP领域从业者的实用参考书。

展开阅读全文

页面更新:2024-04-14

标签:神经元   神经网络   梯度   向量   词条   网络   权重   误差   序列   样本   清单   模型   时刻   记忆   代码   功能   数据   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top