如何用PyTorch实现递归神经网络
虽然递归神经网络很好地展示了PyTorch的灵活性,但它也广泛支持其他深度学习框架。特别是可以为计算机视觉计算提供强有力的支持。PyTorch是脸书人工智能研究所和其他几个实验室的开发者的成果。这个框架结合了Torch7高效灵活的GPU加速后端库和直观的Python前端。它的特点是快速原型化、可读代码和支持最广泛的深度学习模型。
开始旋转
链接中的文章(/jekbradbury/examples/tree/SPINN/snli)详细介绍了递归神经网络的PyTorch实现,它有递归跟踪器和TreeLSTM节点,也称为SPINN——SPINN是自然语言处理中使用的深度学习模型的一个例子,很难通过很多流行的框架来构建。这里的模型实现使用了批处理,所以可以通过GPU加速,使得运行速度明显快于没有批处理的版本。
SPINN即stack-augmented parser-interpreter神经网络(stack-augmented parser-interpreter neural network),是Bowman等人在2016中提出的一种解决自然语言推理任务的方法。本文使用了斯坦福大学的SNLI数据集。
任务是将句子对分为三类:假设句子1是一个不可见图像的确切标题,那么句子2(a)是肯定的(b)是可能的还是(c)肯定不是一个准确的标题?(这几类分别叫蕴涵、中性、矛盾)。比如一句话是“两只狗正在跑过一片田野”,言外之意可能会让这句话对变成“户外动物”,中立可能会让这句话对变成“一些小狗在跑,想抓住一根棍子”,矛盾可能会让这句话对变成“宠物坐在沙发上”。
特别是,研究SPINN的最初目标是在确定句子之间的关系之前,将每个句子编码成一个固定长度的向量表示(还有其他方法,比如在注意力模型中用软聚焦方法比较每个句子的每个部分)。
数据集是机器用句法分析树的方法生成的,句法分析树将每个句子中的词分成具有独立意义的短语和分句,每个短语由两个词或子短语组成。许多语言学家认为,人类通过上面提到的树的层次方式来结合词和意义并理解语言,因此值得尝试以同样的方式构建神经网络。以下示例是数据集中的一个句子,其解析树由嵌套的括号表示:
( (教堂))((天花板上有裂缝)。) )
对这个句子进行编码的一种方法是使用具有解析树的神经网络来构造神经网络层Reduce,它可以组合词对(通过词嵌入表示,例如GloVe)和/或短语,然后递归地应用这个层(函数)来对句子进行编码:
X =减少(“the”,“ceiling”)
Y =减少(" in ",X)
...等等。
但是,如果我希望网络以更像人类的方式工作,从左向右阅读并保留句子的上下文,同时仍然使用解析树来组合短语,该怎么办?或者,如果我要训练一个网络来建立自己的解析树,让它根据看到的单词来读句子?这是编写解析树的相同但略有不同的方式:
教堂)天花板有裂缝)))))。) )
或者以第三种方式,如下所示:
话说:教堂天花板有裂缝。
语法分析:S S R S S S S S R R R R S R R
我所做的只是删除了左括号,然后用“s”标记“shift ”,用“r”替换右括号表示“reduce”。但是现在你可以从左到右读取信息作为一组指令来操作一个栈和一个类似于栈的缓冲区,你可以得到和上面递归方法完全一样的结果:
1.将单词放入缓冲区。
2.从缓冲区的前面弹出“The”并将其推到堆栈的上层,后面是“church”。
3.弹出前两个堆栈值,应用它们进行Reduce,然后将结果推回堆栈。
4.从缓冲区弹出“has ”,然后将其推入堆栈,然后“cracks ”,然后“in ”,然后“the ”,然后“ceiling”。
5.重复四次:弹出两个栈值,应用减少,然后推送结果。
6.砰”并将其推送到堆栈的上层。
7.重复两次:弹出两个栈值,应用减少,然后推送结果。
8.弹出剩余的堆栈值,并将其作为句子代码返回。
我还想保留句子的上下文,以便在对句子的后半部分应用Reduce层时,考虑系统已经读取的句子部分的信息。所以我将把两个参数的Reduce函数替换成一个三个参数的函数,它的输入值是一个左子句、一个右子句和当前句子的上下文状态。这种状态是由神经网络的第二层(称为环路跟踪器的单元)创建的。给定当前句子的上下文状态、缓冲区中的顶部条目B和堆栈中的前两个条目s1\s2,跟踪器在堆栈操作的每一步(即读取每个单词或右括号)之后生成新的状态:
上下文[t+1] = Tracker(上下文[t],b,s1,s2)
很容易想象用自己喜欢的编程语言编写代码来做这些事情。对于每一个要处理的句子,它会从缓冲区加载下一个单词,运行跟踪器,检查是否将该单词推送到堆栈上或者执行Reduce函数来执行操作;然后重复,直到整句处理完毕。通过单个句子的应用,这个过程构成了一个庞大而复杂的深度神经网络,它的两个可训练层通过堆叠操作反复应用。但是,如果你熟悉TensorFlow或Theano等传统深度学习框架,你就知道它们很难实现这样的动态过程。值得花些时间回顾和探索PyTorch的与众不同之处。
图论
图1:函数的图形结构表示
深度神经网络本质上是一个有大量参数的复杂函数。深度学习的目的是通过计算损失函数测量的偏导数(梯度)来优化这些参数。如果将函数表示为计算图结构(图1),这些梯度的计算可以通过向后遍历图来实现,不需要多余的工作。每个现代深度学习框架都是基于这种反向传播的概念,所以每个框架都需要一种方法来表示计算图。
在很多流行的框架中,包括TensorFlow,Theano和Keras,以及Torch7的nngraph库,计算图都是预先构建的静态对象。图形是由类似数学表达式的代码定义的,但它的变量实际上是没有保存任何数值的占位符。将图形中的占位符变量编译到函数中,然后可以对该批训练集重复运行该函数,以生成输出值和梯度值。
这种静态计算图的方法对于固定结构的卷积神经网络非常有效。但是在许多其他应用中,根据数据使神经网络的图形结构不同是有用的。在自然语言处理中,研究者通常希望通过每个时间步输入的单词来扩展(确定)循环神经网络。上述SPINN模型中的堆栈操作很大程度上依赖于控制流(如for和if语句)来定义特定句子的计算图结构。在更复杂的情况下,您可能需要构建一个模型,其结构取决于模型自身子网的输出。
这些想法中的一些(虽然不是全部)可以机械地应用到静态图系统中,但几乎总是以降低透明度和增加代码混乱为代价。框架必须在其计算图中添加特殊节点,这些节点表示循环和条件等编程原语,用户必须学习和使用这些节点,而不仅仅是编程代码语言中的for和if语句。这是因为程序员使用的任何控制流语句都只会运行一次,程序员在构建图时需要硬编码一条计算路径。
比如TensorFlow中需要一个特殊的控制流节点tf.while_loop,通过字向量(从初始状态h0开始)运行递归神经网络单元(rnn_unit)。您需要一个额外的特殊节点来在运行时获取单词长度,因为当您运行代码时,它只是一个占位符。
#张量流
#(此代码在模型初始化期间运行一次)
#“单词”不是一个真正的列表(它是一个占位符变量),所以
#我不会用“len”
cond =λI,h:I & lt;tf.shape(words)[0]
cell = lambda i,h: rnn_unit(words[i],h)
i = 0
_,h = tf.while_loop(cond,cell,(I,h0))
基于动态计算图的方法与以往的方法有着本质的不同。它有几十年的学术研究历史,包括哈佛的Kayak,亲笔签名和以研究为中心的框架Chainer和DyNet。在这种框架(也称为运行定义)中,计算图在运行时被建立和重构,并且相同的代码被用于执行正向传递的计算,同时所需的数据结构也被建立用于反向传播。此方法可以生成更直接的代码,因为控制流可以使用标准的for和if编写。它还使调试更容易,因为运行时断点或堆栈跟踪将跟踪实际编写的代码,而不是执行引擎中编译的函数。一个简单的Python for loop可以用在一个动态框架中,实现一个相同变长的循环神经网络。
# PyTorch(也在Chainer中工作)
#(此代码在模型的每次向前传递时运行)
#“单词”是一个包含实际值的Python列表
h = h0
逐字逐句:
h = rnn_unit(字,h)
PyTorch是第一个由运行定义的深度学习框架,它匹配静态图框架(如TensorFlow)的功能和性能,使其非常适合从标准卷积神经网络到最疯狂的强化学习的想法。所以让我们看看SPINN的实现。
密码
在开始构建网络之前,我需要设置一个数据加载器。通过深度学习,可以批量处理数据样本来操作模型,并行化加速训练,每一步的梯度变化都更加平滑。我觉得这里可以这样做(后面我会解释上面的栈操作过程是如何批处理的)。下面的Python代码使用PyTorch的文本库中内置的系统来加载数据,该系统可以通过连接相似长度的数据样本来自动生成批处理。运行这段代码后,train_iter、dev_iter和test_itercontain循环通过训练集、验证集和测试集的批处理。
从torchtext导入数据,数据集
TEXT = datasets . snli . parsedtextfield(lower = True)
TRANSITIONS = datasets . snli . shiftreducefield()
标签=数据。现场(顺序=假)训练、开发、测试=数据集。SNLI.splits(
TEXT,TRANSITIONS,LABELS,wv _ type = ' glove . 42b ')TEXT . build _ vocab(train,dev,test)
train_iter,dev_iter,test_iter = data。BucketIterator.splits(
(培训、开发、测试),batch_size=64)
您可以在train.py中找到设置训练周期和精度测量的其余代码。让我们继续。如上所述,SPINN编码器包括一个参数化的Reduce层和一个可选的循环跟踪器来跟踪句子上下文,以便在网络每次读取一个单词或应用Reduce时更新隐藏状态;下面的代码表示创建SPINN仅仅意味着创建这两个子模块(我们将很快看到它们的代码)并将它们放在容器中以备后用。
从火炬进口nn进口火炬
#对PyTorch的神经网络包中的模块类进行子类化
SPINN类(NN。模块):
def __init__(self,config):
超级(SPINN,self)。__init__()
self . config = config self . Reduce = Reduce(config . d _ hidden,config.d_tracker)
如果config.d_tracker不为None:
self . Tracker = Tracker(config . d _ hidden,config.d_tracker)
纺纱。创建模型时调用了一次_ _ init _ _它分配和初始化参数,但不执行任何神经网络操作或构建任何类型的计算图。在每个新的批处理数据上运行的代码由SPINN.forward方法定义,这是标准的PyTorch名称,用于在用户实现的方法中定义模型的转发过程。上面描述的是栈操作算法的有效实现,也就是一般Python中,它运行在多个缓冲区和栈上,每个例子对应一个缓冲区和栈。我使用转换矩阵中包含的一组“shift”和“reduce”操作进行迭代,运行跟踪器(如果存在),并遍历批中的每个样本,以应用“shift”操作(如果需要)或将其添加到需要“reduce”操作的样本列表中。然后对列表中的所有样本运行Reduce层,并将结果推回到各自的堆栈中。
定义转发(自身、缓冲、转换):
#输入作为单词嵌入的单个张量进入;
#我需要它是一个堆栈列表,每个例子都有一个
#我们可以独立弹出的批次。中的单词
#每个例子都已经被颠倒了,这样它们就可以
#从每一页的末尾弹出,从左到右阅读
#列表;它们的前缀也是空值。
buffers =[list(torch . split(b . squeeze(1),1,0))
对于火炬中的b . split(缓冲区,1,1)]
#我们还需要在每个堆栈的底部有两个空值,
#所以我们可以从输入中的空值复制;这些空值
#都是必需的,这样即使
#缓冲区或堆栈为空
stacks = [[buf[0],buf[0]] for buf in buffers]
if hasattr(self,' tracker '):
self.tracker.reset_state()
对于过渡中的trans_batch:
if hasattr(self,' tracker '):
#我之前描述追踪器取4
# arguments (context_t,b,s1,s2),但这里我
#将堆栈内容作为单个参数提供
#在跟踪器中存储上下文时
#对象本身。
tracker_states,_ = self.tracker(缓冲区,堆栈)
否则:
tracker _ States = ITER tools . repeat(无)
左,右,跟踪= [],[],[]
batch = zip(trans_batch,buffers,stacks,tracker_states)
对于过渡、缓冲、堆栈、批量跟踪:
如果transition == SHIFT:
stack.append(buf.pop()
elif转换==减少:
rights.append(stack.pop())
lefts.append(stack.pop())
trackings.append(跟踪)
如果权利:
reduced = ITER(self . reduce(left,rights,trackings))
对于转换,在zip中堆栈(trans_batch,stacks):
如果转换==减少:
stack.append(next(reduced))
返回[stack.pop() for stack in stacks]
打电话给自己的时候。追踪者或自我。分别还原、运行跟踪器的正向方法或还原子模块。该方法需要对样本列表应用正向操作。在main函数的forward方法中,对不同的样本独立操作是有意义的,即在批处理中为每个样本提供单独的缓冲区和堆栈,因为所有大量使用数学和需要受益于批处理执行的GPU加速的操作都在Tracker和Reduce中进行。为了更干净地编写这些函数,我将使用一些助手(稍后定义)将这些样本列表转换为批处理张量,反之亦然。
我想让Reduce模块自动批处理它的参数以加快计算速度,然后解除批处理,这样就可以分别推送和弹出它们了。用于将每对左右子短语的表达组合成母短语的实际组合函数是TreeLSTM,它是通用循环神经网络单元LSTM的变体。组合函数要求每个子短语的状态实际上由两个张量组成,一个隐藏状态H和一个存储单元状态C,并且函数是使用在子短语的隐藏状态中操作的两个线性层和一个nn。线性组合函数tree_lstm,其将线性层的结果与子短语的存储单元状态相组合。在SPINN中,通过添加在Tracker的隐藏状态下运行的第三个线性层来扩展该方法。
图2: Treel STM组合函数添加了第三个输入(X,在本例中是跟踪器状态)。在下面显示的PyTorch实现中,五组三个线性变换(由蓝色、黑色和红色箭头的三元组表示)被组合成三个nn。线性模块,tree_lstm函数执行位于框中的所有计算。图片来自陈等人(2016)。