引言
最近在做句子结构分析的模型,具体样例如下图左图所示。
因为之前看过constituency parser任务,故尝试将这棵树转成满足consituency parser满足的那种格式,转换后的样例如下图右图所示。
找个简单的例子如下所示。
所以隆重推荐这款转换工具,constituency-tree-labeling-tool,目前这个工具已经在我司使用以及在hanlp和yuzhangcs/parser中被提及。
因为有这个工具的加持,我们就可以尝试使用consitutuency parser的方式来做这个任务。目前模型也已经被业务使用。
使用
我发现在真实使用的时候,会将这棵树按照一层为单位进行拆开,即:
- 不会管句子层与层之间的关系。
- 复句会拆成子句。
- 最终使用还是主谓宾、兼语、联谓等这些基本句型。
思考
基于上面的使用方式,可以进行以下方面的尝试:
- 可以先将这棵树按照使用的方式进行拆开,拆成一句一句的进行训练,更符合真实使用场景。
- 找一个比consituency parser更为简便的模型,并且提高其准确度。
- 提升速度。因为每一句话都要跑,速度也是非常重要的一点。
实现方式
句子:['商务部', '开展', '首批步行街改造提升试点工作', '。']
。
方式一:分层CRF
比如上面这句话,他首先构造一个9*9的临界矩阵(按照分词个数),因为这句话只能拆出来一个完整子句,故只有第一行有label,剩下的8行全是空的。
即:
1 | [['B-主', 'B-谓', 'B-宾', 'I-宾', 'I-宾', 'I-宾', 'I-宾', 'I-宾', 'B-符号'], |
那这里变成了两个问题:
- 预测层数。表示这句话可以拆出多少个完整子句。
- 每层的crf预测。对每层的label使用CRF进行预测和解码。
一. 层数预测
args-level。
输入一句话,预测有几个子句。
二. CRF预测
这里就一个需要注意的地方,就是设置一个层数上限,比如10层。那么bert输出比如(32, 128, 768),将其expand至(32, 10, 128, 768),然后接一个768 * 768
的linear,将bert的语义表示用这个linear对每一层重新获取对应的语义表示。
三. 联合模型
args-level-crf-parser。
就是将上面两个模型集成到一个任务里,两个loss分配不同的weight。
四. 过程总结与思考
1.实验
训练层数预测模型时,发现在验证集上F1一直只到0.7~0.8
之间,和我最初预想能达到0.95以上有很大的差距,虽然训练集上基本达到100%准确。
2.思考
后来我想了下,我觉得有两部分原因:
- 样本层数分布不均匀,比如1层和2层的占据了绝大多数,6,7层的就很少了。
- 对多分类模型抱有的期望过高。
为什么我会有第二个见解?我在训练中途看了transformers库集成的多分类代码,发现和我的没什么区别,这让我很纠结,难道是梯度裁剪、dropout、optimizer各方面没有设置好??还是模型我不应该用electra,以及不应该使用交叉熵作为损失函数,而是可以尝试下FocalLoss?
结果在种种测试下,都没有达到一个有效的改善。
后来我觉得可能就是我对这个任务的准确度抱有的期望过高,为什么呢?
因为从人角度来看,都未必能准确知道一个句子应该分成几个子句。
最终结果是没有达到预期的,一开始我先写的联合模型,但是发现在验证集上的层数预测的f1最多到0.8,label预测准确率更低。
后来我对这两个模型进行拆分成两个任务来训练,以为可以改进层数预测的结果,但是分开后在验证集上层数预测的F1仍旧只能到0.8左右~,所以联合模型没有大的问题。
对于CRF预测,label有40个左右,加之label的数量分布严重不均衡,那么即使假设这个任务的f1可以达到0.8,那两个任务的最终结果也就在0.6以上。
那么由此可证,这种方式可能走不通。
方式二:嵌套ner
args0complement。(忽略名字啦,本想留给主语补齐的。。。)
这种方式是使用嵌套ner的方式来表示这些数据。
对于上面这句话,将其转成一个9*9临接矩阵(按照分词进行构成)。
那他将会落在位置(0, 0), (1, 1), (2, 7), (8, 8)上,并且其标签分别是:主语
,谓语
,宾语
,符号
。
他将面临的最主要的问题:
虽然位置和标签都可以表示了,但是其之间的关联关系并没有。
那么训练时没有问题,但是在解码的时候就会出现多种组合,比如:
1 | (1, 2, 3, 4) |
为什么会有这么多种组合呢?
因为一棵树是由多层组成的,那么其相互之间是可以在不同层进行组合。
转成一种方便理解的方式,即:
1 | 主谓宾符号 |
那么难点就是从这一堆的路径里面选取合适的路径出来。
1.实验
首先我没想到模型结果会出乎意料好许多,不是从这个嵌套ner任务本身的准确率,而是从这一套的实现方式上,效果得到了一个很大的提升。
这个给了极大的信心去写后面多条路径解码的代码😅😅😅。
做法上我是先按照dependency-parser的方式来做,分成两个loss,一个是arc loss,还有一个rel loss。其中arc loss表示在这个临接矩阵中存在对应关系,那么就用1表示,否则0,即位置。rel_loss表示label在这个临接矩阵中的位置以及对应的label。
dependency-parser在位置解码的时候是以arc-loss为准,因为基本毫无疑问arc相对是更容易的,我也按照这个思路进行的,不过最终发现直接用rel loss就已经足够了。。
从上图可以看到:
- 整个收敛还是很快的。
- arc loss和rel loss后面基本持平了。
- 右边那张图的F1的结果,是因为有些类别样本数量太低导致的,看下面某训练时刻的结果。
1 | eval[2]: 109it [00:25, 4.25it/s] |
方式三:进阶的constituency parser
这地方有两个需要学习的:
- 使用GNN的方式来做consitutnecy parser,看看这种方式的效果。
- 改进constituency parser在将其构成临接矩阵的时候,去掉其上下之间的关联关系,减少预测数量,看看是否能提升其准确率。