Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

前言

论文链接
本篇博客并没有完整的解读DIORA这篇论文,只是记录论文的inside pass与outside pass。
下文都通过下图来介绍Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

明确输入

假定我们的输入句子为[“the”,“cat”,“drank”],我们先把它们输入到一个embedding层获得各自的单词词向量。

代码:
embed = self.embed(batch)
论文:
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

然后我们对[ v t h e v_{the} vthe, v c a t v_{cat} vcat, v d r a n k v_{drank} vdrank] (x)进行一些非线性变换,但维度不变。
可以看到,下面的代码返回两个东西,一个是h,一个是c,我们把c忽略,因为它是treelstm的内容,在我看的代码中c都是0。而h与x大小一样,是我们要用到的东西。

代码:
h = torch.tanh(torch.matmul(x, self.V) + self.B)
device = torch.cuda.current_device() if self.is_cuda else None
c = torch.full(h.shape, 0, dtype=torch.float32, device=device)
return h, c
论文:
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

接着是理解算法的关键,那便是初始化inside_h。什么是inside_h?考虑输入的句子是[“the”,“cat”,“drank”],那么它组成的二叉树(如上面的图),就一共有6(3+2+1)个节点,而inside_h就是6xhidden_dim大小的一个矩阵。初始化它,就是让它的前length(这里为3)行的向量赋值为上面的h。接下来开始inside pass

代码:
self.chart = Chart(batch_size, length, size, dtype=torch.float32, cuda=self.is_cuda)
self.chart.inside_h[:, :self.length] = h

inside pass

我们记Length=句子长度=3,Level=树的高度=0\1\2(第一层高0,第二层高1,第三层高2), 通过上面的初始化,我们已经填好了第一层的三个向量,接着我们依次填第二层和第三层:

代码:
for level in range(1, self.length):
(\t) h, c, s = inside_func(compose_func, score_func, batch_info, chart, index,normalize_func=normalize_func)

当level=1的时候,h的大小为(2,hidden_dim),s的大小为(2,)
当level=2的时候,h的大小为(1,hidden_dim),s的大小为(1,)
首先明确h是啥、s是啥

h是啥,s又是啥

h是接下去解析树的结点的向量表示,s是接下去解析树的结点的组合分数。哦对,还没有说向量表示是怎么计算的,组合分数又是什么呢!
下面通过计算“The”,“Cat"的父亲结点的向量表示和组合分数来讲解。
首先,我们要初始化"The”,"Cat"这两个叶子结点的分数为0(这两个叶子结点的向量表示我们之前已经初始化过了)。
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解
我们明确现在有的四个东西:"The"的向量表示(记作a(i)),"Cat"的向量表示(记作a(j)),"The"的分数(记作e(i)),“Cat"的分数(记作e(j))。然后我们先通过一个双线性函数,将"The”,”cat“这两个叶子结点的向量表示分数去计算一个组合分数:

代码:
bma = torch.matmul(vector1, self.mat).unsqueeze(1) # b 1 n
ba = torch.matmul(bma, vector2.unsqueeze(2)).view(-1, 1) # b n 1
s = ba + ss[0] + ss[1]
论文:
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

然后我们用"The","Cat"这两个叶子结点的向量表示去将将它们组合,得到一个组合的向量表示:

代码:
input_h = torch.cat(hs, 1) # hs = [vector1, vector2]
h = torch.relu(torch.matmul(input_h, self.W_0) + self.B)
h = torch.relu(torch.matmul(h, self.W_1) + self.B_1)
device = torch.cuda.current_device() if self.is_cuda else None
c = torch.full(h.shape, 0, dtype=torch.float32, device=device)
return h, c
论文:Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

现在是不是父亲结点的组合分数和向量表示都有了!同样的,"cat"和"drank"也可以这样计算,去得到它们的父亲结点的组合分数和向量表示。所以上面level=1的时候,h的大小是(2,hidden_dim),因为level=1的这一层,有两个父亲结点。那么我们再来考虑level=2

理解inside pass的关键

首先我们记

  • “The”,“Cat”,"Drink"这三个叶子结点分别为n1,n2,n3
  • n1和n2的父亲结点为n4
  • n2和n3的父亲结点为n5

我们上面讨论的是n4和n5的向量表示和分数是怎么来的,现在我们计算level=2的时候,也就是root,也就是n4和n5的父亲结点(记n6)的向量表示和分数!
聪明的你是不是认为n6的向量表示和组合分数就是用n4和n5通过与上面一样的计算来得到,其实并不是这样。其实是:

  1. 我们先计算n1和n5的组合向量表示和组合分数
  2. 我们再计算n4和n3的组合向量表示和组合分数
  3. 我们将上面两个组合分数进行归一化,得到一组权重
  4. 使用这组权重对两个组合向量表示进行加权求和,得到最终的n6的组合向量表示
  5. 使用这组权重对两个组合分数进行加权求和,得到最终的n6的组合分数

代码
h_agg = torch.sum(h.view(B, L, N, -1) * p, 2)
c_agg = torch.sum(c.view(B, L, N, -1) * p, 2)
s_agg = torch.sum(s * p, 2)
论文:
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

可以看到,当输入的length=3,level=2的时候,有 length-level=1个父亲结点,每个父亲结点需要考虑level(2)组, 如上所述的(i,j)成分对。

做个练习彻底理解inside pass

如果我们输入的句子长度Length是5,那么我们inside_h的第0维应该是 5*6/2 = 15(5+4+3+2+1)。考虑level=3的时候,这一层有2个父亲结点,我们考虑左边这个。左边这个父亲结点需要考虑3组成分对。就比如输入句子是 [“i”,“love”,“china”,“very”,“much”],level=3这一层的左边的父亲结点需要考虑下面这三组成分对

  • (“i”, (“love”,“china”,“very”))
  • ((“i”,“love”),(“china”,“very”))
  • ((“i”,“love”,“china”),“very”)

而就比如(“love”,“china”,“very”),我们早就在level=2的时候通过(“love”,(“china”,“very”))、((“love”,“china”),“very”)这两个成分对通过上面"理解inside pass的关键"部分的计算,得到了。所以inside pass本质上是一个动态规划、填表的过程。

outside pass

这部分,我就大致说一下:
outside pass的过程正好和inside pass反过来,我们先初始化root结点的向量表示,然后往下计算,最后得到每个叶子结点的向量表示。而diora训练的目标就是:让这里得到的向量表示去拟合一开始的预训练词向量

论文:
Unsupervised Latent Tree Induction with Deep Inside-Outside Recursive Autoencoders阅读理解

结论

通过上面的inside pass和outside pass,我们得到了什么?——首先inside pass使得一整棵解析树(二叉树)的每一个结点都拥有一个向量表示和一个分数

  • 这个向量表示被称作span representation,它吸收了句子内部的结构,能够用作down stream
  • 这个分数可以通过CKY解码,来得到该句子上面的一棵好的解析树

其次,outside pass也给解析树的每一个结点一个向量表示

  • 这个向量表示更多地吸收了句子上下文关系,但我目前认为,由于训练目标是去拟合预训练词向量,所以outside pass部分得到的叶子结点向量表示并没有什么用,但它的level>=1的部分的span representation应该是有用的