共计 9045 个字符,预计需要花费 23 分钟才能阅读完成。
解锁数据后劲:信息抽取、数据加强与 UIE 的完满交融
1. 信息抽取(Information Extraction)
1.1 IE 简介
信息抽取是 NLP 工作中十分常见的一种工作,其目标在于从一段天然文本中提取出咱们想要的要害信息结构。
举例来讲,当初有上面这样一个句子:
新东方烹饪学校在成都。
咱们想要提取这句话中所有有意义的词语,例如:
机构 | 新东方烹饪学校 |
城市 | 成都 |
这个关键词提取工作就叫做 命名实体辨认(Named Entity Recognition, NER)工作,文中的「新东方烹饪学校」和「成都」就被称为实体(Entity)。
如果咱们还想进一步的晓得这些词语之间的关系,例如:
实体 1 | 关系名 | 实体 2 |
---|---|---|
新东方烹饪学校 | 所在地 | 成都 |
这种提取实体之间关系的工作就叫做 关系抽取(Relation Extraction, RE)工作。
## 1.2 信息抽取的几种办法
### 1.2.1 序列标注(Sequence Labeling)
序列标注通常是指对文中的每一个字(以下简称 token)进行分类,即实质是 token classification 工作。
咱们对第一大节中的例子做序列标注工作,失去的后果如下:
新 | 东 | … | 学 | 校 | 在 | 成 | 都 |
---|---|---|---|---|---|---|---|
B – 机构 | I – 机构 | I – 机构 * N | I – 机构 | I – 机构 | O | B – 城市 | I – 城市 |
能够看到,咱们对句子中的每一个字(token)都打上了一个类别标签,咱们冀望模型要做的事就是去学会每一个字所属的类别是什么。
> Note: 这里用的标注办法是「BIO 标记法」,其中「B-」代表该地位 token 是某一个实体词语(span)的起始 token;「I-」代表该地位 token 处于某一个词语的两头(或结尾),「O」则代表该地位 token 不在任何一个实体词语中。除了「BIO 标记法」外,还有许多其余的标注形式(如 BIOES 等),其本质思路都很相似。
### 1.2.2 指针网络(Pointer Network)
序列标注模型有一个人造的缺点,无奈解决解决实体重叠(overlap)的问题。
举例来讲,如果明天咱们不仅要提取「机构」,还同时要提取「机构类型」,那么咱们冀望的提取后果应该为:
机构 | 新东方烹饪学校 |
机构类型 | 学校 |
城市 | 成都 |
能够看到,对于「学校」这两个字,即属于「新东方烹饪学校」(机构)这个词,也存在于「学校」(机构类型)这个词,那咱们在给这两个字打标签的时候,到底应该打成哪个类别呢?
新 | 东 | … | 学 | 校 | 在 | 成 | 都 |
---|---|---|---|---|---|---|---|
B – 机构 | I – 机构 | I – 机构 | ? | ? | O | B – 城市 | I – 城市 |
由此咱们能够看到,因为在进行分类时咱们通常对一个字(token)只赋予一个标签,这就导致了 token classification 不能很好的解决实体重叠(一字多标签)的简单状况。
> Note: 存在一些技巧能够解决该问题,例如能够从单字单分类(CE)衍生到单字多分类(BCE),这里不展开讨论。
指针网络(Pointer Network)通过别离对每一个实体独自做预测来解决了实体之前的重叠抵触问题。
例如,咱们当初要同时预测「机构」和「机构类型」这两个实体,那么咱们就能够设计一个多头网络(Multi-Head)来别离预测这两个实体的实体词。
其中,
「机构」实体头中「起始」向量代表这一句话中是「机构」词语的首字(例子中为「新」);
「机构」实体中「完结」向量代表这一句话中时「机构」词语的尾字(例子中为「校」)。
通过「起始」和「完结」向量中的首尾字索引就能找到对应实体的词语。
能够看到,通过构建多头的工作,指针网络可能别离预测「机构」和「机构类型」中的实体词起始 / 终止地位,即「学校」这个词语在两个工作层中都能被抽取进去。
## 1.3. UIE —— 基于 prompt 的指针网络
### 1.3.1 UIE 中的 prompt 是什么?
多头指针网络可能很好的解决实体重叠问题,但毛病在于:不够灵便。
假设明天咱们曾经通过指针网络训练好了一个提取「机构」、「机构类型」的模型,行将交付时甲方忽然提出一个新需要:咱们想再多提取一个「机构简称」的属性。
草(一种动物)。
从 2.2 节中的示意图中咱们能够看到,每一个实体类型会对应一个独自的网络头。
这就意味着咱们不仅须要重标数据,还须要为新属性增加一个新的网络头,即模型构造会随着实体类型个数扭转而发生变化。
那,能不能有一种方法去固定住模型的构造,不论明天来多少种类型要辨认都能应用同样的模型构造实现呢?
咱们思考一下,模型构造变动的局部是和实体类型强绑定的「头」局部。
而不同「头」之间构造其实是齐全一样的:一个「起始」向量 + 一个「终止」向量。
既然「头」构造齐全一样,咱们能不能罗唆间接应用一个「头」去提取不同实体类型的信息呢?
不同「头」之间的区别在于它们关注的信息不同:「机构头」只关注「机构」相干的实体词,「城市头」只关注「城市」相干的实体词。
那么咱们是不是能够间接在模型输出的时候就通知模型:我当初须要提取「某个头」的信息。
这个用来通知模型做具体任务的参数就叫 prompt,咱们把它拼在输出中一并喂给模型即可。
通过上图能够看到,咱们将不同的「实体类型」作为 prompt 参数喂给模型,用于「激活」模型参数跟以后「实体类型」相干的参数,从而输入不同的抽取后果。
> Note:「通过一个输出参数去激活一个大模型中的不同参数,从而实现不同工作的思路」并不是首次呈现,在 meta-learning 中也存在相干的钻研,这里的 prompt 参数和 meta-parameter 有着十分相似的思路。
通过引入 prompt,UIE 也能很不便的解决实体之间的 关系抽取(Relation Extraction)工作,例如:
### 1.3.2 UIE 的实现
看完了基本思路,咱们来一起看看 UIE 是怎么实现的吧。
1. 模型局部
UIE 的模型代码比较简单,只须要在 encoder 后构建一个起始层和一个完结层即可:
`
class UIE(nn.Module):
def __init__(self, encoder):
“””
init func.
Args:
encoder (transformers.AutoModel): backbone, 默认应用 ernie 3.0
Reference:
https://github.com/PaddlePaddle/PaddleNLP/blob/a12481fc3039fb45ea2dfac3ea43365a07fc4921/model_zoo/uie/model.py
“””
super().__init__()
self.encoder = encoder
hidden_size = 768
self.linear_start = nn.Linear(hidden_size, 1)
self.linear_end = nn.Linear(hidden_size, 1)
self.sigmoid = nn.Sigmoid()
def forward(
self,
input_ids: torch.tensor,
token_type_ids: torch.tensor,
attention_mask=None,
pos_ids=None,
) -> tuple:
“””
forward 函数,返回开始 / 完结概率向量。
Args:
input_ids (torch.tensor): (batch, seq_len)
token_type_ids (torch.tensor): (batch, seq_len)
attention_mask (torch.tensor): (batch, seq_len)
pos_ids (torch.tensor): (batch, seq_len)
Returns:
tuple: start_prob -> (batch, seq_len)
end_prob -> (batch, seq_len)
“””
sequence_output = self.encoder(
input_ids=input_ids,
token_type_ids=token_type_ids,
position_ids=pos_ids,
attention_mask=attention_mask,
)[“last_hidden_state”]
start_logits = self.linear_start(sequence_output) # (batch, seq_len, 1)
start_logits = torch.squeeze(start_logits, -1) # (batch, seq_len)
start_prob = self.sigmoid(start_logits) # (batch, seq_len)
end_logits = self.linear_end(sequence_output) # (batch, seq_len, 1)
end_logits = torch.squeeze(end_logits, -1) # (batch, seq_len)
end_prob = self.sigmoid(end_logits) # (batch, seq_len)
return start_prob, end_prob
`
2. 训练局部
训练局部次要关注一下 loss 的计算即可。
因为每一个 token 都是一个二分类工作,因而选用 BCE Loss 作为损失函数。
别离计算起始 / 完结向量的 BCE Loss 再取平均值即可,如下所示:
`
criterion = torch.nn.BCELoss()
…
start_prob, end_prob = model(input_ids=batch[‘input_ids’].to(args.device),
token_type_ids=batch[‘token_type_ids’].to(args.device),
attention_mask=batch[‘attention_mask’].to(args.device))
start_ids = batch[‘start_ids’].to(torch.float32).to(args.device) # (batch, seq_len)
end_ids = batch[‘end_ids’].to(torch.float32).to(args.device) # (batch, seq_len)
loss_start = criterion(start_prob, start_ids) # 起止向量 loss -> (1,)
loss_end = criterion(end_prob, end_ids) # 完结向量 loss -> (1,)
loss = (loss_start + loss_end) / 2.0 # 求均匀 -> (1,)
loss.backward()
…
`
该我的项目将借用 transformers 库来实现 paddlenlp 版本中 UIE,已实现:
– [x] UIE 预训练模型主动下载
– [x] UIE Fine-Tuning 脚本
– [x] 信息抽取、事件抽取数据加强(DA)策略(晋升 recall)
– [x] 信息抽取、事件抽取自剖析负例生成(Auto Neg)策略(晋升 precision)
* 环境装置
本我的项目基于 pytorch
+ transformers
实现,运行前请装置相干依赖包:
`
sh
pip install -r ../requirements.txt
torch
transformers==4.22.1
datasets==2.4.0
evaluate==0.2.2
matplotlib==3.6.0
rich==12.5.1
scikit-learn==1.1.2
requests==2.28.1`
# 2. 数据集筹备
我的项目中提供了一部分示例数据,数据来自 DuIE 数据集中随机抽取的 100 条,数据在 data/DuIE
。
若想应用 自定义数据
训练,只须要仿照示例数据构建数据集构建 prompt 和 content 即可:
`
json
{“content”: “ 谭孝曾是谭元寿的长子,也是谭派第六代传人 ”, “result_list”: [{“text”: “ 谭元寿 ”, “start”: 4, “end”: 7}], “prompt”: “ 谭孝曾的父亲 ”}
{“content”: “ 在圣保罗书院中学毕业后,曾钰成又在中学会考及大学入学考试中名列全港前十名 ”, “result_list”: [{“text”: “ 曾钰成 ”, “start”: 12, “end”: 15}], “prompt”: “ 人物 ”}
{“content”: “ 在圣保罗书院中学毕业后,曾钰成又在中学会考及大学入学考试中名列全港前十名 ”, “result_list”: [{“text”: “ 圣保罗书院 ”, “start”: 1, “end”: 6}], “prompt”: “ 曾钰成的毕业院校 ”}
…`
doccano 导出数据如下所示:
`
json
{“text”: “ 谭孝曾是谭元寿的长子,也是谭派第六代传人 ”, “entities”: [{“id”: 42517, “label”: “ 人物 ”, “start_offset”: 0, “end_offset”: 3, “text”: “ 谭孝曾 ”}, {“id”: 42518, “label”: “ 人物 ”, “start_offset”: 4, “end_offset”: 7, “text”: “ 谭元寿 ”}], “relations”: [{“id”: 0, “from_id”: 42517, “to_id”: 42518, “type”: “ 父亲 ”}]}
…`
能够运行 doccano.py
来将标注数据(doccano)转换为训练数据(prompt)。
# 3. 模型训练
批改训练脚本 train.sh
里的对应参数, 开启模型训练:
`
sh
python train.py \
–pretrained_model “uie-base-zh” \
–save_dir “checkpoints/DuIE” \
–train_path “data/DuIE/train.txt” \
–dev_path “data/DuIE/dev.txt” \
–img_log_dir “logs/” \
–img_log_name “UIE Base” \
–batch_size 32 \
–max_seq_len 256 \
–learning_rate 5e-5 \
–num_train_epochs 20 \
–logging_steps 10 \
–valid_steps 100 \
–device cuda:0`
正确开启训练后,终端会打印以下信息:
`
python
…
0%| | 0/1 [00:00<?, ?ba/s]
100%|██████████| 1/1 [00:00<00:00, 6.91ba/s]
100%|██████████| 1/1 [00:00<00:00, 6.89ba/s]
global step 10, epoch: 1, loss: 0.00244, speed: 2.08 step/s
global step 20, epoch: 1, loss: 0.00228, speed: 2.17 step/s
global step 30, epoch: 1, loss: 0.00191, speed: 2.17 step/s
global step 40, epoch: 1, loss: 0.00168, speed: 2.14 step/s
global step 50, epoch: 1, loss: 0.00149, speed: 2.11 step/s
global step 60, epoch: 1, loss: 0.00138, speed: 2.15 step/s
global step 70, epoch: 2, loss: 0.00123, speed: 2.29 step/s
global step 80, epoch: 2, loss: 0.00112, speed: 2.12 step/s
global step 90, epoch: 2, loss: 0.00102, speed: 2.15 step/s
global step 100, epoch: 2, loss: 0.00096, speed: 2.15 step/s
Evaluation precision: 0.80851, recall: 0.84444, F1: 0.82609
best F1 performence has been updated: 0.00000 –> 0.82609
…`
在 logs/UIE Base.png
文件中将会保留训练曲线图:
# 4. 模型预测
实现模型训练后,运行 inference.py
以加载训练好的模型并利用:
`
python
if name == “__main__”:
from rich import print
sentences = [
‘ 谭孝曾是谭元寿的长子,也是谭派第六代传人。’
]
# NER 示例
for sentence in sentences:
ner_example(
model,
tokenizer,
device,
sentence=sentence,
schema=[‘ 人物 ’]
)
# SPO 抽取示例
for sentence in sentences:
information_extract_example(
model,
tokenizer,
device,
sentence=sentence,
schema={
‘ 人物 ’: [‘ 父亲 ’],
}
)`
NER 和事件抽取在 schema 的定义上存在一些区别:
* NER 的 schema 构造为 List
类型,列表中蕴含所有要提取的 实体类型
。
* 信息抽取的 schema 构造为 Dict
类型,其中 Key
的值是所有 主语
,Value
对应该主语对应的所有 属性
。
* 事件抽取的 schema 构造为 Dict
类型,其中 Key
的值是所有 事件触发词
,Value
对应每一个触发词下的所有 事件属性
。
`
sh
python inference.py`
失去以下推理后果:
`
python
[+] NER Results:
{
‘ 人物 ’: [‘ 谭孝曾 ’, ‘ 谭元寿 ’]
}
[+] Information-Extraction Results:
{
‘ 谭孝曾 ’:
{
‘ 父亲 ’: [‘ 谭元寿 ’]
},
‘ 谭元寿 ’: {
‘ 父亲 ’: []
}
}`
# 5. 数据加强(Data Augmentation)
信息抽取 / 事件抽取的数据标注老本较高,因而咱们提供几种针对小样本下的数据加强策略。
包含:
* 正例:SwapSPO、Mask Then Fill
* 负例:自剖析负例生成(Auto Neg)
所有实现均在 Augmenter.py
中,为了便于应用,咱们将其封装为 web 服务以不便调用:
平台应用 streamlit
搭建,因而应用前须要先装置三方包:
`
python
pip install streamlit==1.17.0`
随后,运行以下命令开启标注平台:
`
python
streamlit run web_da.py –server.port 8904`
在浏览器中拜访 ip + 端口(默认 8904)即可关上标注平台。
## 5.1 正例:SwapSPO 策略介绍
Swap SPO 是一种基于规定的简略数据加强策略。
将同一数据集中雷同 P 的句子分成一组,并随机替换这些句子中的 S 和 O。
* 策略输出:
《夜曲》 是 周杰伦 作曲 的一首歌。
《那些你很冒险的梦》 是当下十分炽热的一首歌,作曲 为 林俊杰。
* Swap SPO 后的输入:
《夜曲》 是当下十分炽热的一首歌,作曲 为 周杰伦。
## 5.2 正例:Mask Then Fill 策略介绍
Mask Then Fill 是一种基于生成模型的信息抽取数据加强策略。
对于一段文本,咱们其分为「要害信息段」和「非关键信息段」,蕴含关键词片段称为「要害信息段」。
上面例子中标粗的为 要害信息片段
,其余的为 非关键片段
。
> 大年三十 我从 北京 的大兴机场 飞回 了 成都。
咱们随机 [MASK] 住一部分「非关键片段」,使其变为:
> 大年三十 我从 北京 [MASK] 飞回 了 成都。
随后,将该句子喂给 filling 模型(T5-Fine Tuned)还原句子,失去新生成的句子:
> 大年三十 我从 北京 首都机场作为终点, 飞回 了 成都。
Note:
filling 模型是一个生成模型,示例中咱们应用中文 T5
微调失去 DuIE 数据集下的模型(暂未开源)。您能够参考 这里 微调一个更适宜您本人数据集下的 filling 模型,并将训练好的模型门路填写至 web_da.py
中对应的地位。
`
python
…
device = ‘cpu’ # 指定设施
generated_dataset_height = 800 # 生成样本展现高度
max_show_num = 500 # 生成样本最大保留行数
max_seq_len = 128 # 数据集单句最大长度
batch_size = 128 # 负例生成时的 batch_size
filling_model_path = ‘ 这里 ’ # fine-tuned filling model
…`
## 5.3 负例:自剖析负例生成(Auto Neg)策略介绍
信息抽取中通常会存在 P 混同
的问题,例如:
> 王文铭,76 岁,是西红市多鱼村的大爷。
当咱们同时生成 年龄
和 逝世年龄
这种十分近义的 prompt 进行抽取时,可能会呈现 误召回
的状况:
`
python
prompt: 王文铭的年龄 answer: 76 岁 -> 正确
prompt: 王文铭的逝世年龄 answer: 76 岁 -> 谬误`
因而,咱们基于一个已训练好的模型,主动剖析该模型在 训练集
下存在哪些易混同的 P,并为这些 P 主动生成负例,以晋升模型的 Precision 指标。
将新生成的负例退出 原始训练数据集
,从新训练模型即可。
## 5.4 各种 DA 策略的试验成果
在 DuIE 100 条数据下测试,各种 DA 策略的成果如下所示(以下 P / R / F1 均取 F1 最高的 Epoch 指标):
| DA Policy | Precision(best)| Recall(best)| F1(best)|
|—|—| —| —|
| baseline | 0.8085 | 0.8444 | 0.8260 |
| Swap SPO | 0.8409(↑)| 0.8222 | 0.8314(↑)|
| Auto Neg | 0.8297(↑)| 0.8666(↑)| 0.8478(↑)|
| Mask Then Fill | 0.9000(↑)| 1.0000(↑) | 0.9473(↑)|
| Mask Then Fill & Auto Neg | 0.9777(↑) | 0.9777(↑)| 0.9777(↑) |
* 原作者实现地址:https://github.com/universal-ie/UIE
* paddle 官网:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/uie
* https://github.com/HarderThenHarder/transformers_tasks/blob/main/UIE
更多优质内容请关注公号:汀丶人工智能;会提供一些相干的资源和优质文章,收费获取浏览。