查看原文
其他

文本分类和序列标注“深度”实践

周晓江 PaperWeekly 2022-03-17


©PaperWeekly 原创 · 作者|周晓江

研究方向|文本分类、文本聚类

本文的主要目的是推广 UNF 代码库,该代码库由笔者在实际工作中做文本分类和序列标注的相关经验抽象而来,欢迎 fork 和交流。

项目地址:https://github.com/waterzxj/UNF

全文分三部分,第一部分是相关动机,介绍这个项目为什么会出现;第二部分是理论分析部分,通过近些年一些顶会论文来简要梳理文本分类和序列标注两个任务的发展脉络;第三部分是实践部分,也是本文最重要的部分,会介绍笔者实现的一个通用代码库,方便实现上述理论部分的模型,并且提供基础组件方便应用者做二次开发,详细的介绍会放到实践部分介绍。

项目动机

文本分类和序列标注是入门 NLP 最好的两个任务,覆盖了常用 NLP 任务的流程,因此掌握了这两个任务,也能方便的拓展到其他任务。


笔者搜索了网上这两个任务相关的项目,发现一个痛点:项目太单一,不够系统化,这里的系统化是说涵盖理论介绍,高效的算法实现,到工程化的 server 以及可视化 web 界面(通常一个算法从研究到落地必经的几个环节)。于是,笔者决定自己动手解决这一痛点。


理论分析

文本分类相关理论 

这部分将通过 6 篇顶会论文,简要介绍文本分类领域是如何从最初的浅层的 CNN 发展到深层的 CNN,到如何在 LSTM 中有效结合 attention 结构,再到结合 label-embedding 的 zero-shot 文本分类,最后是当下比较火的基于 transformer 的文本分类。因为本文的重点是工程实践部分,所以详细的论文介绍读者可以直接参考原 paper。


论文标题:Convolutional Neural Networks for Sentence Classification

论文链接:https://arxiv.org/abs/1408.5882

源码链接:https://github.com/yoonkim/CNN_sentence


TextCNN 分类算法的核心架构如上图表示所示。算法通常通过四步完成:

  • 将原始的 one-hot 文本经过 word embedding 变成稠密的向量表示;

  • 经过一层 1 维的卷积层,可选择多个 filter,每个 filter 表示一个特征;

  • 对上一步得到的每个卷积向量做 max-pooling 操作,最后每一个卷积得到一维向量,把所有的 max-pooling 结果 concat 起来,最终得到句子的向量表示;

  • 将句子向量经过一层全连接层,然后过 softmax-loss。


论文标题:Deep Pyramid Convolutional Neural Networks for Text Categorization

论文链接:https://www.aclweb.org/anthology/P17-1052

源码链接:https://github.com/riejohnson/ConText


DpCNN 分类算法在 TextCNN 的基础上引入了多层 CNN 的机制,主要是为了解决语言中长距离语义关系捕获的问题,并且经过精心设计的网络结构在算法复杂度上是常数,并不会因为网络的加深而增加复杂度。整个网络结构如上图所示。

DpCNN 算法的关键点: 

reigion embedding:region embedding 可以看做是在原始的 word embedding 基础上先做了一层浅层的 TextCNN,不过论文中没有保留词序的信息,作者通过实验证明了原始的 TextCNN 的做法更加容易过拟合。 

等长卷积:等长卷积物理意义是序列信息经过卷积层之后是等长输出的,表示输出的每个词都建模了这个词左右 n/2 个词的语义信息。 

堆叠卷积层:为了能解决长距离语义特征的问题,作者采用了加深卷积层来解决。然而"Do Convolutional Networks need to be Deep for Text Classification"的研究证明了随机的堆叠卷积层效果会很差,所以必须是精心设计的网络结构才能取得好的效果,作者在网络结构中引入了以下几个设计:

  • 固定 feature-size 数量:不同于图像里的深层 CNN 一样,作者设计的网络里每次 down sampling 的操作都会保持 feature-size 不变,从而减少了网络的计算量; 

  • pre-activation:激活函数的计算在放到卷积的算之后,作者分析了这样更有利于深层网络的训练; 

  • 1/2 池化层:引入卷积核为 3,stride 等于 2 的 max-pooling 操作,使得每次序列的长度减半,同时也点题,最后的网络结构就是一个类似"金字塔"形的。 

  • 残差连接:引入了图像中深层网络训练的残差连接机制,从而保证了深层网络的顺利训练。


论文标题:Hierarchical Attention Networks for Document Classification

论文链接:https://www.aclweb.org/anthology/N16-1174

复现代码:https://github.com/tqtg/hierarchical-attention-networks


HAN 的网络模型结构如上图所示。 

HAN 的主要 motivation 是采用层次的网络结构建模 document 的层级结构。sentence embedding 的结果采用基于 additive attention 的方式对 GRU 的编码结果进行加权平均;doc embedding 采用同样的方式对 sentence embedding 的结果加权求和得到。


论文标题:A Structured Self-attentive Sentence Embedding

论文链接:https://arxiv.org/abs/1703.03130

源码链接:https://github.com/facebookresearch/pytext


这篇论文引入了 multi-head self-attention 的(这里和 transformer 中的 multi-head attention 处理方式不太一样)思路到文本分类中。这个模型结构有两个关键点: 

  • 在 LSTM 的序列编码器后引入 multi-head self-attention 的计算,物理意义是每个 attention 捕捉到一个重点语义特征,多个头捕捉到句子中不同的语义特征,从而也加强了句子长距离语义的捕获能力; 

  • 对 multi-head 的权重矩阵引入正则项,做法是使得 multi-attention 矩阵之间彼此正交,物理意义是保证 attention 的多样性,减少彼此间的冗余。笔者在实际实验中也发现加入这个正则项会有效果的提升。


论文标题:Joint Embedding of Words and Labels for Text Classification

论文链接:https://arxiv.org/abs/1805.04174

源码链接:https://github.com/guoyinwang/LEAM


这篇论文作者把 label 的信息引入到句子向量的编码中,从而产生了更好的 sentence encoder。具体做法如下: 

  • 用 label embedding 矩阵和 word embedding 矩阵做乘法,产生 attention 矩阵,对 word embedding 做加权求和,用来产生 sentence embedding; 

  • 对上一步获得的 sentence embedding 过 softmax-loss,同时在训练的时候引入了对 label-embedding 的正则项,使得 label-embebding 直接过 softmax-loss 的损失也同时最小,从而学习更好的 label embedding。

这篇论文提出的模型不仅能获得分类的结果,同时也能学习一个很好的 label embedding 表示,将 word embedding 和 label embedding 在同一个语义空间进行表示,在基于 zero-short 的分类方法中也能得到很好的应用。


论文标题:How to Fine-Tune BERT for Text Classification?

论文链接:https://arxiv.org/abs/1905.05583

源码链接:https://github.com/xuyige/BERT4doc-Classification


基本的思路是在预训练好的 bert 预训练模型上,用 cls 向量表征 sentence encoder,在目标任务的数据上做 fine-tune。当然论文中还介绍了一些具体的 fine-tune 策略,比如 lr 的选择等。

序列标注相关理论


论文标题:Neural Architectures for Named Entity

论文链接:https://arxiv.org/abs/1603.01360

源码链接:https://github.com/glample/tagger


这是结合神经网络做序列标注模型的经典思路。模型结构上在 Bi-LSTM 的句子编码器上结合 CRF 的 loss 函数,模型实现上还是有很多需要注意的 trick,尤其在 CRF 的实现上。


论文标题:Semi-supervised Multitask Learning for Sequence Labeling

论文链接:https://arxiv.org/abs/1704.07156

源码链接:https://github.com/marekrei/sequence-labeler


这篇工作主要为了解决序列标注中,label 主要分布在"O"标签的问题,也就是大部分 token 的预测都是非目标实体。在上一篇的基础上引入语言模型multi-task 训练。如上图所示。

每个位置需要需要前一个词、当前的实体类别、下一个词。通过引入语言模型的目标函数,让模型建模更深层的语义特征和句法信息。但是整个模型在 infer 阶段的模型复杂度并未增加,因为语言模型的目标函数只在训练阶段采用,预测的时候只预测当前词的实体类别。


论文标题:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

论文链接:https://arxiv.org/abs/1810.04805

源码链接:https://github.com/google-research/bert


相比上一个工作,fine-tune 基于语言模型预训练的 Bert 实践证明效果更好。其核心点是用 Bert 最后一层每一个 token 的 embedding 接 MLP 或者 CRF 的损失函数进行多分类。

代码实践

为了能够更好的实现上述介绍的算法,于是笔者设计了 UNF 代码库,仅 5 行代码就可以训练一个文本分类模型,详见 github,欢迎 fork、star。下面就具体介绍一下这个代码库。 

UNF 库特点 

模块化(modularity):UNF 库已经实现了一些经典的文本分类和序列标注模型,用户可开箱即用;另一方面基于已有的 module,用户可方便二次开发调研实现一些新的模型或者技术; 

效率(efficiency):支持多 GPU 训练混合精度训练, 方便快速训练模型; 

全面(comprehensive):支持快速搭建 python server;支持 pytorch trace 成静态图,支持 c++ server 服务,提供基于 restful api 的前端可视化 debug 界面。

整体架构

模块结构

模型训练

#quick start
python3 train_flow.py

Only 5 line code need

#data loader
data_loader = DataLoader(data_loader_conf)
train_iter, dev_iter, test_iter = data_loader.generate_dataset()

#model loader
model, model_conf = ModelLoader.from_params(model_conf, data_loader.fields)

#learner loader
learner = LearnerLoader.from_params(model, train_iter, dev_iter, learner_conf, test_iter=test_iter, fields=data_loader.fields, model_conf=model_conf)

#learning
learner.learn()

代码 UNF/train_flow.py 是可直接开箱即跑的示例代码。

配置中加上如下两行,即可享受混合精度训练多卡训练的功能。

"use_fp16"True,
 "multi_gpu"True

整个训练过程自动注入 tensorboard 监控


Python inference

#quick start
python3 score_flow.py

core code:

#core code
from models.predictor import Predictor

predictor = Predictor(model_path, device, model_type)
logits = predictor.predict(input)

(0.18-0.67)

C++ inference 

step 1. 动态图 trace 成静态图

#quick start
python3 trace.py

core code:

#core code
net = globals()[model_cls](**config.__dict__)
net.load_state_dict_trace(torch.load("%s/best.th" % model_path))
net.eval()

mock_input = net.mock_input_data()
tr = torch.jit.trace(net, mock_input)
tr.save("trace/%s" % save_path)

step 2. c++ serving 

  • install cmake 

  • download libtorch and unzip to trace folder

cd trace
cmake -DCMAKE_PREFIX_PATH=libtorch .


make


./predict trace.pt predict_vocab.txt
output: 2.2128 -2.3287

RESTFUL-API web demo

cd web_server
python run.py

效果如下:




点击以下标题查看更多往期内容: 





#投 稿 通 道#

 让你的论文被更多人看到 



如何才能让更多的优质内容以更短路径到达读者群体,缩短读者寻找优质内容的成本呢?答案就是:你不认识的人。


总有一些你不认识的人,知道你想知道的东西。PaperWeekly 或许可以成为一座桥梁,促使不同背景、不同方向的学者和学术灵感相互碰撞,迸发出更多的可能性。 


PaperWeekly 鼓励高校实验室或个人,在我们的平台上分享各类优质内容,可以是最新论文解读,也可以是学习心得技术干货。我们的目的只有一个,让知识真正流动起来。


📝 来稿标准:

• 稿件确系个人原创作品,来稿需注明作者个人信息(姓名+学校/工作单位+学历/职位+研究方向) 

• 如果文章并非首发,请在投稿时提醒并附上所有已发布链接 

• PaperWeekly 默认每篇文章都是首发,均会添加“原创”标志


📬 投稿邮箱:

• 投稿邮箱:hr@paperweekly.site 

• 所有文章配图,请单独在附件中发送 

• 请留下即时联系方式(微信或手机),以便我们在编辑发布时和作者沟通




🔍


现在,在「知乎」也能找到我们了

进入知乎首页搜索「PaperWeekly」

点击「关注」订阅我们的专栏吧



关于PaperWeekly


PaperWeekly 是一个推荐、解读、讨论、报道人工智能前沿论文成果的学术平台。如果你研究或从事 AI 领域,欢迎在公众号后台点击「交流群」,小助手将把你带入 PaperWeekly 的交流群里。



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存