Efficient GlobalPointer:少点参数,多点效果
By 苏剑林 | 2022-01-25 | 117384位读者 |在《GlobalPointer:用统一的方式处理嵌套和非嵌套NER》中,我们提出了名为“GlobalPointer”的token-pair识别模块,当它用于NER时,能统一处理嵌套和非嵌套任务,并在非嵌套场景有着比CRF更快的速度和不逊色于CRF的效果。换言之,就目前的实验结果来看,至少在NER场景,我们可以放心地将CRF替换为GlobalPointer,而不用担心效果和速度上的损失。
在这篇文章中,我们提出GlobalPointer的一个改进版——Efficient GlobalPointer,它主要针对原GlobalPointer参数利用率不高的问题进行改进,明显降低了GlobalPointer的参数量。更有趣的是,多个任务的实验结果显示,参数量更少的Efficient GlobalPointer反而还取得更好的效果。
大量的参数 #
这里简单回顾一下GlobalPointer,详细介绍则请读者阅读《GlobalPointer:用统一的方式处理嵌套和非嵌套NER》。简单来说,GlobalPointer是基于内积的token-pair识别模块,它可以用于NER场景,因为对于NER来说我们只需要把每一类实体的“(首, 尾)”这样的token-pair识别出来就行了。
设长度为$n$的输入$t$经过编码后得到向量序列$[\boldsymbol{h}_1,\boldsymbol{h}_2,\cdots,\boldsymbol{h}_n]$,原始GlobalPointer通过变换$\boldsymbol{q}_{i,\alpha}=\boldsymbol{W}_{q,\alpha}\boldsymbol{h}_i$和$\boldsymbol{k}_{i,\alpha}=\boldsymbol{W}_{k,\alpha}\boldsymbol{h}_i$我们得到序列向量序列$[\boldsymbol{q}_{1,\alpha},\boldsymbol{q}_{2,\alpha},\cdots,\boldsymbol{q}_{n,\alpha}]$和$[\boldsymbol{k}_{1,\alpha},\boldsymbol{k}_{2,\alpha},\cdots,\boldsymbol{k}_{n,\alpha}]$,然后定义
\begin{equation}s_{\alpha}(i,j) = \boldsymbol{q}_{i,\alpha}^{\top}\boldsymbol{k}_{j,\alpha}\end{equation}
作为从$i$到$j$的连续片段是一个类型为$\alpha$的实体的打分。这里我们暂时省略了偏置项,如果觉得有必要,自行加上就好。
这样一来,有多少种类型的实体,就有多少个$\boldsymbol{W}_{q,\alpha}$和$\boldsymbol{W}_{k,\alpha}$。不妨设$\boldsymbol{W}_{q,\alpha},\boldsymbol{W}_{k,\alpha}\in\mathbb{R}^{d\times D}$,那么每新增一种实体类型,我们就要新增$2Dd$个参数;而如果用CRF+BIO标注的话,每新增一种实体类型,我们只需要增加$2D$的参数(转移矩阵参数较少,忽略不计)。对于BERT base来说,常见的选择是$D=768,d=64$,可见GlobalPointer的参数量远远大于CRF。
识别与分类 #
事实上,不难想象对于任意类型$\alpha$,其打分矩阵$s_{\alpha}(i,j)$必然有很多相似之处,因为对于大多数token-pair而言,它们代表的都是“非实体”,这些非实体的正确打分都是负的。这也就意味着,我们没必要为每种实体类型都设计独立的$s_{\alpha}(i,j)$,它们应当包含更多的共性。
怎么突出$s_{\alpha}(i,j)$的共性呢?以NER为例,我们知道NER实际上可以分解为“抽取”和“分类”两个步骤,“抽取”就是抽取出为实体的片段,“分类”则是确定每个实体的类型。这样一来,“抽取”这一步相当于只有一种实体类型的NER,我们可以用一个打分矩阵就可以完成,即$(\boldsymbol{W}_q\boldsymbol{h}_i)^{\top}(\boldsymbol{W}_k\boldsymbol{h}_j)$,而“分类”这一步,我们则可以用“特征拼接+Dense层”来完成,即$\boldsymbol{w}_{\alpha}^{\top}[\boldsymbol{h}_i;\boldsymbol{h}_j]$。于是我们可以将两项组合起来,作为新的打分函数:
\begin{equation}s_{\alpha}(i,j) = (\boldsymbol{W}_q\boldsymbol{h}_i)^{\top}(\boldsymbol{W}_k\boldsymbol{h}_j) + \boldsymbol{w}_{\alpha}^{\top}[\boldsymbol{h}_i;\boldsymbol{h}_j]\label{eq:EGP-1}\end{equation}
这样一来,“抽取”这部分的参数对所有实体类型都是共享的,因此每新增一种实体类型,我们只需要新增对应的$\boldsymbol{w}_{\alpha}\in\mathbb{R}^{2D}$就行了,即新增一种实体类型增加的参数量也只是$2D$。进一步地,我们记$\boldsymbol{q}_i=\boldsymbol{W}_q\boldsymbol{h}_i, \boldsymbol{k}_i=\boldsymbol{W}_k\boldsymbol{h}_i$,然后为了进一步地减少参数量,我们可以用$[\boldsymbol{q}_i;\boldsymbol{k}_i]$来代替$\boldsymbol{h}_i$,此时
\begin{equation}s_{\alpha}(i,j) = \boldsymbol{q}_i^{\top}\boldsymbol{k}_j + \boldsymbol{w}_{\alpha}^{\top}[\boldsymbol{q}_i;\boldsymbol{k}_i;\boldsymbol{q}_j;\boldsymbol{k}_j]\label{eq:EGP}\end{equation}
此时$\boldsymbol{w}_{\alpha}\in\mathbb{R}^{4d}$,因此每新增一种实体类型所增加的参数量为$4d$,由于通常$d \ll D$,所以式$\eqref{eq:EGP}$的参数量往往少于式$\eqref{eq:EGP-1}$,它就是Efficient GlobalPointer最终所用的打分函数。
惊喜的实验 #
Efficient GlobalPointer已经内置在bert4keras>=0.10.9
中,读者只需要更改一行代码,就可以切换Efficient GlobalPointer了。
# from bert4keras.layers import GlobalPointer
from bert4keras.layers import EfficientGlobalPointer as GlobalPointer
下面我们来对比一下GlobalPointer和Efficient GlobalPointer的结果:
\begin{array}{c}
\text{人民日报NER实验结果} \\
{\begin{array}{c|cc}
\hline
& \text{验证集F1} & \text{测试集F1}\\
\hline
\text{CRF} & 96.39\% & 95.46\% \\
\text{GlobalPointer} & \textbf{96.25%} & \textbf{95.51%} \\
\text{Efficient GlobalPointer} & 96.10\% & 95.36\%\\
\hline
\end{array}} \\ \\
\text{CLUENER实验结果} \\
{\begin{array}{c|cc}
\hline
& \text{验证集F1} & \text{测试集F1} \\
\hline
\text{CRF} & 79.51\% & 78.70\% \\
\text{GlobalPointer} & 80.03\% & 79.44\%\\
\text{Efficient GlobalPointer} & \textbf{80.66%} & \textbf{80.04%} \\
\hline
\end{array}} \\ \\
\text{CMeEE实验结果} \\
{\begin{array}{c|cc}
\hline
& \text{验证集F1} & \text{测试集F1} \\
\hline
\text{CRF} & 63.81\% & 64.39\% \\
\text{GlobalPointer} & 64.84\% & 65.98\%\\
\text{Efficient GlobalPointer} & \textbf{65.16%} & \textbf{66.54%} \\
\hline
\end{array}}
\end{array}
可以看到,Efficient GlobalPointer的实验结果还是很不错的,除了在人民日报任务上有轻微下降外,其他两个任务都获得了一定提升,并且整体而言提升的幅度大于下降的幅度,所以Efficient GlobalPointer不单单是节省了参数量,还提升了效果。而在速度上,Efficient GlobalPointer与原始的GlobalPointer几乎没有差别。
分析与评述 #
考虑到人民日报NER只有3种实体类型,CLUENER和CMeEE分别有10种和9种实体类型,从分数来看也是人民日报比其他两种要高,这说明CLUENER和CMeEE的难度更大。另一方面,在CLUENER和CMeEE上Efficient GlobalPointer都取得了提升,所以我们可以初步推断:实体类别越多、任务越难时,Efficient GlobalPointer越有效。
这也不难理解,原版GlobalPointer参数过大,那么平均起来每个参数更新越稀疏,相对来说也越容易过拟合;而Efficient GlobalPointer共享了“抽取”这一部分参数,仅通过“分类”参数区分不同的实体类型,那么实体抽取这一步的学习就会比较充分,而实体分类这一步由于参数比较少,学起来也比较容易。反过来,Efficient GlobalPointer的实验效果好也间接证明了式$\eqref{eq:EGP}$的分解是合理的。
当然,不排除在训练数据足够多的时候,原版GlobalPointer会取得更好的效果。但即便如此,在类别数目较多时,原版GlobalPointer可能会占用较多显存以至于难以使用,还是以base版$D=768,d=64$为例,如果类别数有100个,那么原版GlobalPointer的参数量为$2\times 768\times 64\times 100$,接近千万,不得不说确实是不够友好了。
最后的总结 #
本文指出了原版GlobalPointer的参数利用率不高问题,并提出了相应的改进版Efficient GlobalPointer。实验结果显示,Efficient GlobalPointer在降低参数量的同时,基本不会损失性能,甚至还可能获得提升。
转载到请包括本文地址:https://kexue.fm/archives/8877
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jan. 25, 2022). 《Efficient GlobalPointer:少点参数,多点效果 》[Blog post]. Retrieved from https://kexue.fm/archives/8877
@online{kexuefm-8877,
title={Efficient GlobalPointer:少点参数,多点效果},
author={苏剑林},
year={2022},
month={Jan},
url={\url{https://kexue.fm/archives/8877}},
}
April 8th, 2022
efficient global pointer的源码实现中,$bias=tf.einsumm("bnh->bhn",self.q\_dense(inputs))/2$代码中除以2是基于什么考虑的呢?
我理解$tf.einsumm("bnh->bhn",self.q\_dense(inputs))$和$bias[:,::2,None]+bias[:,1::2,:,None]$就可以实现ij项的相加,也就是实现公示(3)等号右边第二项。那么除以2的作用是什么呢?
平衡一下量级,避免bias项过大,纯粹是直觉做法哈。
嗯嗯,谢谢。
April 16th, 2022
有些不太明白为什么这两行代码就是公式(3)右边第二项,可以详细讲解一下吗,两个bias相加是起到了拼接的作用吗,概念有一点模糊
$\boldsymbol{w}_{\alpha}^{\top}[\boldsymbol{q}_i;\boldsymbol{k}_i;\boldsymbol{q}_j;\boldsymbol{k}_j]={\boldsymbol{w}_{\alpha}^{(1)}}^{\top}[\boldsymbol{q}_i;\boldsymbol{k}_i]+{\boldsymbol{w}_{\alpha}^{(2)}}^{\top}[\boldsymbol{q}_j;\boldsymbol{k}_j]$
我明白了,谢谢苏神
May 29th, 2022
您好,请问global pointer有学术成果么,我想在论文里引用这个工作怎么办?
厚脸皮说一句,我想这篇文章提出的内容应该有资格成为一个“学术成果”吧,所以“global pointer有学术成果么”,应该算是有的。想要在论文引用它,只需要直接引用本网页,方式可以参考文末的引用参考(可以把中文部分翻译为英文)。
好的谢谢,您的每一个网页都媲美一篇paper了,所以在我的研究中借鉴了您的工作,我觉得有必要引用您。
感谢,过奖了。GlobalPointer的论文也在准备中,我们尽快放到arxiv上。
大佬,我发现了一个2020年的工作,叫做Biaffine Decoder,和你的idea是一模一样的,可能区别就在于旋转位置编码以及一些trick。
很多工作都像相似了,就是细节不一样。
September 26th, 2022
[...]Efficient GlobalPointer:少点参数,多点效果[...]
October 14th, 2022
请问苏神,GlobalPointer和Efficient GP我自己测试时 发现Bert的学习率和首尾embedding层的学习率设为大概1:20最好(比如 Bert学习率为2e-5, 首尾embedding的可设为2e-4~5e-4)请问苏神在GP的学习率这方面有什么经验吗?
这个我也没有什么经验~
October 19th, 2022
苏神好,我也推导了论文的(13)公式跟一般的softmax相比好像跟另外一个评论在BCE得到的结果相似。好像(13)跟交叉熵比也是去掉了高阶项。感觉有点像用去掉高阶项来解决类别不均衡。\begin{equation}
\sum_{(q,k)\in P_{\alpha}} \textrm{log}(1+ e^{-s_{\alpha}(q,k)}) + \sum_{(q,k)\in Q_{\alpha}} \textrm{log}(1+ e^{s_{\alpha}(q,k)}) = \textrm{log}(1+\sum_{(q,k)\in P_{\alpha}} e^{-s_{\alpha}(q,k)} + \cdots) + \textrm{log}(1+\sum_{(q,k)\in Q_{\alpha}} e^{s_{\alpha}(q,k)} + \cdots)
\end{equation}
所以感觉是不是通过去掉高阶项来缓解类别不均衡。
这个问题在 https://kexue.fm/archives/9158 有进一步的讨论,欢迎参考。
October 24th, 2022
大神好,初看本文容易让我对hi和hj的维度产生困惑,按照qi=Wq*hi,Wq~(D,d),那么hi维度应该遵循hi~(d,1);看到后面wα~(2D,1),恍然觉着hi应该遵循hi~(D,1)。我想前面公式是否应该加转置符加以说明,抑或我的理解有不到位的地方请指正?
这里确实是我写错了,不是你的问题。谢谢指出,已经修正。
November 22nd, 2022
请问大佬,这个模型在英文数据集上做实验时,因为英文会对单词进行分词,比如原本的实体['mr.', 'lippens']会变成['mr', '.', 'lip', '##pen', '##s'],那token_start_index和token_end_index计算时要怎么计算呢
你这个问题是有问题的,我回答不了。
如果你用的BERT,那么原本的实体就是['mr', '.', 'lip', '##pen', '##s'],没有“原本的实体['mr.', 'lippens']”这种说法。先选定tokenizer,然后得到tokens,然后才去构造标签,这是很自然的顺序。标签不是与生俱来的,也不是天上掉下来的,是根据你的tokens来构造的。
May 22nd, 2023
想请问苏神还在做这篇工作后续的工作么,看到还一直没有把这篇发成论文。目前正把这篇当作基线在跑,想再进行一些改进,想凭着一点点的优化,渴望能发一篇一般的论文,不知道苏神觉得这样可行么?模型是不是已经没有优化的可能了呀?
之前都整理在这篇论文中了 https://arxiv.org/abs/2208.03054
May 30th, 2023
[...]Efficient GlobalPointer:少点参数,多点效果[...]