【信息抽取】UIE——基于prompt的信息抽取模型(附源碼)
作者簡(jiǎn)介
作者:何枝
原文:
https://zhuanlan.zhihu.com/p/589054073
轉(zhuǎn)載者:楊夕
推薦系統(tǒng) 百面百搭地址:
https://github.com/km1994/RES-Interview-Notes
NLP 百面百搭地址:
https://github.com/km1994/NLP-Interview-Notes
個(gè)人筆記:
https://github.com/km1994/nlp_paper_study
1. 什么是信息抽?。↖nformation Extraction)
信息抽取是NLP任務(wù)中非常常見的一種任務(wù),其目的在于從一段自然文本中提取出我們想要的關(guān)鍵信息結(jié)構(gòu)。
舉例來講,現(xiàn)在有下面這樣一個(gè)句子:
新東方烹飪學(xué)校在成都。
我們想要提取這句話中所有有意義的詞語,例如:
| 機(jī)構(gòu) | 新東方烹飪學(xué)校 |
| 城市 | 成都 |
這個(gè)關(guān)鍵詞提取任務(wù)就叫做命名實(shí)體識(shí)別(Named Entity Recognition, NER)任務(wù),文中的「新東方烹飪學(xué)校」和「成都」就被稱為實(shí)體(Entity)。
如果我們還想進(jìn)一步的知道這些詞語之間的關(guān)系,例如:
| 實(shí)體1 | 關(guān)系名 | 實(shí)體2 |
|---|---|---|
| 新東方烹飪學(xué)校 | 所在地 | 成都 |
這種提取實(shí)體之間關(guān)系的任務(wù)就叫做關(guān)系抽取(Relation Extraction, RE)任務(wù)。
2. 信息抽取的幾種方法
2.1 序列標(biāo)注(Sequence Labeling)
序列標(biāo)注通常是指對(duì)文中的每一個(gè)字(以下簡(jiǎn)稱token)進(jìn)行分類,即本質(zhì)是 token classification任務(wù)。
我們對(duì)第一小節(jié)中的例子做序列標(biāo)注任務(wù),得到的結(jié)果如下:
| 新 | 東 | ... | 學(xué) | 校 | 在 | 成 | 都 |
|---|---|---|---|---|---|---|---|
| B-機(jī)構(gòu) | I-機(jī)構(gòu) | I-機(jī)構(gòu)*N | I-機(jī)構(gòu) | I-機(jī)構(gòu) | O | B-城市 | I-城市 |
可以看到,我們對(duì)句子中的每一個(gè)字(token)都打上了一個(gè)類別標(biāo)簽,我們期望模型要做的事就是去學(xué)會(huì)每一個(gè)字所屬的類別是什么。
Note: 這里用的標(biāo)注方法是「BIO 標(biāo)記法」,其中「B-」代表該位置 token 是某一個(gè)實(shí)體詞語(span)的起始 token;「I-」代表該位置 token 處于某一個(gè)詞語的中間(或結(jié)尾),「O」則代表該位置 token 不在任何一個(gè)實(shí)體詞語中。除了「BIO 標(biāo)記法」外,還有許多其他的標(biāo)注方式(如 BIOES 等),其本質(zhì)思路都很類似。
2.2 指針網(wǎng)絡(luò)(Pointer Network)
序列標(biāo)注模型有一個(gè)天然的缺陷,無法解決解決實(shí)體重疊(overlap)的問題。
舉例來講,如果今天我們不僅要提取「機(jī)構(gòu)」,還同時(shí)要提取「機(jī)構(gòu)類型」,那么我們期望的提取結(jié)果應(yīng)該為:
| 機(jī)構(gòu) | 新東方烹飪學(xué)校 |
| 機(jī)構(gòu)類型 | 學(xué)校 |
| 城市 | 成都 |
可以看到,對(duì)于「學(xué)?!惯@兩個(gè)字,即屬于「新東方烹飪學(xué)?!梗C(jī)構(gòu))這個(gè)詞,也存在于「學(xué)?!梗C(jī)構(gòu)類型)這個(gè)詞,那我們?cè)诮o這兩個(gè)字打標(biāo)簽的時(shí)候,究竟應(yīng)該打成哪個(gè)類別呢?
| 新 | 東 | … | 學(xué) | 校 | 在 | 成 | 都 |
|---|---|---|---|---|---|---|---|
| B-機(jī)構(gòu) | I-機(jī)構(gòu) | I-機(jī)構(gòu) | ? | ? | O | B-城市 | I-城市 |
由此我們可以看到,因?yàn)樵谶M(jìn)行分類時(shí)我們通常對(duì)一個(gè)字(token)只賦予一個(gè)標(biāo)簽,這就導(dǎo)致了 token classification 不能很好的解決實(shí)體重疊(一字多標(biāo)簽)的復(fù)雜情況。
Note: 存在一些技巧可以解決該問題,例如可以從單字單分類(CE)衍生到單字多分類(BCE),這里不展開討論。
指針網(wǎng)絡(luò)(Pointer Network)通過分別對(duì)每一個(gè)實(shí)體單獨(dú)做預(yù)測(cè)來解決了實(shí)體之前的重疊沖突問題。
例如,我們現(xiàn)在要同時(shí)預(yù)測(cè)「機(jī)構(gòu)」和「機(jī)構(gòu)類型」這兩個(gè)實(shí)體,那么我們就可以設(shè)計(jì)一個(gè)多頭網(wǎng)絡(luò)(Multi-Head)來分別預(yù)測(cè)這兩個(gè)實(shí)體的實(shí)體詞。

其中,
「機(jī)構(gòu)」實(shí)體頭中「起始」向量代表這一句話中是「機(jī)構(gòu)」詞語的首字(例子中為「新」);
「機(jī)構(gòu)」實(shí)體中「結(jié)束」向量代表這一句話中時(shí)「機(jī)構(gòu)」詞語的尾字(例子中為「校」)。
通過「起始」和「結(jié)束」向量中的首尾字索引就能找到對(duì)應(yīng)實(shí)體的詞語。
可以看到,通過構(gòu)建多頭的任務(wù),指針網(wǎng)絡(luò)能夠分別預(yù)測(cè)「機(jī)構(gòu)」和「機(jī)構(gòu)類型」中的實(shí)體詞起始/終止位置,即「學(xué)校」這個(gè)詞語在兩個(gè)任務(wù)層中都能被抽取出來。
3. UIE —— 基于 prompt 的指針網(wǎng)絡(luò)
3.1 UIE中的 prompt 是什么?
多頭指針網(wǎng)絡(luò)能夠很好的解決實(shí)體重疊問題,但缺點(diǎn)在于:不夠靈活。
假定今天我們已經(jīng)通過指針網(wǎng)絡(luò)訓(xùn)練好了一個(gè)提取「機(jī)構(gòu)」、「機(jī)構(gòu)類型」的模型,即將交付時(shí)甲方突然提出一個(gè)新需求:我們想再多提取一個(gè)「機(jī)構(gòu)簡(jiǎn)稱」的屬性。
草(一種植物)。
從 2.2 節(jié)中的示意圖中我們可以看到,每一個(gè)實(shí)體類型會(huì)對(duì)應(yīng)一個(gè)單獨(dú)的網(wǎng)絡(luò)頭。
這就意味著我們不僅需要重標(biāo)數(shù)據(jù),還需要為新屬性添加一個(gè)新的網(wǎng)絡(luò)頭,即模型結(jié)構(gòu)會(huì)隨著實(shí)體類型個(gè)數(shù)改變而發(fā)生變化。
那,能不能有一種辦法去固定住模型的結(jié)構(gòu),不管今天來多少種類型要識(shí)別都能使用同樣的模型結(jié)構(gòu)完成呢?
我們思考一下,模型結(jié)構(gòu)變化的部分是和實(shí)體類型強(qiáng)綁定的「頭」部分。
而不同「頭」之間結(jié)構(gòu)其實(shí)是完全一樣的:一個(gè)「起始」向量 + 一個(gè)「終止」向量。
既然「頭」結(jié)構(gòu)完全一樣,我們能不能干脆直接使用一個(gè)「頭」去提取不同實(shí)體類型的信息呢?
不同「頭」之間的區(qū)別在于它們關(guān)注的信息不同:「機(jī)構(gòu)頭」只關(guān)注「機(jī)構(gòu)」相關(guān)的實(shí)體詞,「城市頭」只關(guān)注「城市」相關(guān)的實(shí)體詞。
那么我們是不是可以直接在模型輸入的時(shí)候就告訴模型:我現(xiàn)在需要提取「某個(gè)頭」的信息。
這個(gè)用來告訴模型做具體任務(wù)的參數(shù)就叫 prompt,我們把它拼在輸入中一并喂給模型即可。

通過上圖可以看到,我們將不同的「實(shí)體類型」作為 prompt 參數(shù)喂給模型,用于「激活」模型參數(shù)跟當(dāng)前「實(shí)體類型」相關(guān)的參數(shù),從而輸出不同的抽取結(jié)果。
Note: 「通過一個(gè)輸入?yún)?shù)去激活一個(gè)大模型中的不同參數(shù),從而完成不同任務(wù)的思路」并不是首次出現(xiàn),在 meta-learning 中也存在相關(guān)的研究,這里的 prompt 參數(shù)和 meta-parameter 有著非常類似的思路。
通過引入 prompt,UIE 也能很方便的解決實(shí)體之間的關(guān)系抽取(Relation Extraction)任務(wù),例如:

3.2 UIE 的實(shí)現(xiàn)
看完了基本思路,我們來一起看看 UIE 是怎么實(shí)現(xiàn)的吧。
模型部分
UIE 的模型代碼比較簡(jiǎn)單,只需要在 encoder 后構(gòu)建一個(gè)起始層和一個(gè)結(jié)束層即可:
class UIE(nn.Module):def __init__(self, encoder):"""init func.Args:encoder (transformers.AutoModel): backbone, 默認(rèn)使用 ernie 3.0Reference:https://github.com/PaddlePaddle/PaddleNLP/blob/a12481fc3039fb45ea2dfac3ea43365a07fc4921/model_zoo/uie/model.py"""super().__init__()self.encoder = encoderhidden_size = 768self.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 函數(shù),返回開始/結(jié)束概率向量。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. 訓(xùn)練部分
訓(xùn)練部分主要關(guān)注一下 loss 的計(jì)算即可。
由于每一個(gè) token 都是一個(gè)二分類任務(wù),因此選用 BCE Loss 作為損失函數(shù)。
分別計(jì)算起始/結(jié)束向量的 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) # 結(jié)束向量loss -> (1,)loss = (loss_start + loss_end) / 2.0 # 求平均 -> (1,)loss.backward()...
好啦,以上就是 UIE 的全部?jī)?nèi)容,感謝觀看。
完整源碼在這里:
UIE 復(fù)現(xiàn)源碼
https://github.com/HarderThenHarder/transformers_tasks/tree/main/UIE
4. UIE 實(shí)戰(zhàn)
4.1 UIE 環(huán)境安裝
本項(xiàng)目基于 pytorch + transformers 實(shí)現(xiàn),運(yùn)行前請(qǐng)安裝相關(guān)依賴包:
pip install -r ../requirements.txt4.2 UIE 數(shù)據(jù)集準(zhǔn)備
項(xiàng)目中提供了一部分示例數(shù)據(jù),我們使用一個(gè)簡(jiǎn)單的ner任務(wù)(關(guān)系抽取同理)來進(jìn)行信息抽取任務(wù),數(shù)據(jù)在 data/simple_ner 。
若想使用自定義數(shù)據(jù)訓(xùn)練,只需要仿照示例數(shù)據(jù)構(gòu)建數(shù)據(jù)集構(gòu)建prompt和content即可:
{"content": "6月1日交通費(fèi)68元", "result_list": [], "prompt": "出發(fā)地"}
{"content": "9月3日2點(diǎn)18分,加班打車回家,25元", "result_list": [{"text": "家", "start": 15, "end": 16}], "prompt": "目的地"}
{"content": "5月31號(hào)晚上10點(diǎn)54分打車回家49元", "result_list": [{"text": "5月31號(hào)晚上10點(diǎn)54分", "start": 0, "end": 13}], "prompt": "時(shí)間"}
...Notes: 數(shù)據(jù)標(biāo)注建議使用 doccano 完成,標(biāo)注方法和標(biāo)注轉(zhuǎn)換可以參考 UIE 官方的詳細(xì)介紹:
UIE 數(shù)據(jù)標(biāo)注
https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/uie#數(shù)據(jù)標(biāo)注
4.3 UIE 模型訓(xùn)練
修改訓(xùn)練腳本 train.sh 里的對(duì)應(yīng)參數(shù), 開啟模型訓(xùn)練:
python train.py \--pretrained_model "uie-base-zh" \--save_dir "checkpoints/simple_ner" \--train_path "data/simple_ner/train.txt" \--dev_path "data/simple_ner/dev.txt" \--img_log_dir "logs/simple_ner" \--img_log_name "ERNIE-3.0" \--batch_size 8 \--max_seq_len 128 \--num_train_epochs 100 \--logging_steps 10 \--valid_steps 100 \--device cuda:0
正確開啟訓(xùn)練后,終端會(huì)打印以下信息:
...global step 1880, epoch: 94, loss: 0.01507, speed: 10.06 step/sglobal step 1890, epoch: 95, loss: 0.01499, speed: 10.09 step/sglobal step 1900, epoch: 95, loss: 0.01492, speed: 10.05 step/sEvaluation precision: 0.94444, recall: 1.00000, F1: 0.97143best F1 performence has been updated: 0.94118 --> 0.97143global step 1910, epoch: 96, loss: 0.01484, speed: 10.19 step/s...
4.4 UIE 模型預(yù)測(cè)
完成模型訓(xùn)練后,運(yùn)行 inference.py 以加載訓(xùn)練好的模型并應(yīng)用:
if __name__ == "__main__":from rich import printsentence = '5月17號(hào)晚上10點(diǎn)35分從公司加班打車回家,36塊五。'# NER 示例ner_example(sentence=sentence,schema=['出發(fā)地', '目的地', '時(shí)間'])# 事件抽取示例event_extract_example(sentence=sentence,schema={'加班觸發(fā)詞': ['時(shí)間','地點(diǎn)'],'出行觸發(fā)詞': ['時(shí)間', '出發(fā)地', '目的地', '花費(fèi)']})
NER和事件抽取在schema的定義上存在一些區(qū)別:
NER的schema結(jié)構(gòu)為
List類型,列表中包含所有要提取的實(shí)體類型。事件的schema結(jié)構(gòu)為
Dict類型,其中Key的值是所有事件觸發(fā)詞,Value對(duì)應(yīng)每一個(gè)觸發(fā)詞下的所有事件屬性。
python inference.py得到以下推理結(jié)果:
[] NER Results:{'出發(fā)地': ['公司'],'目的地': ['家'],'時(shí)間': ['5月17號(hào)晚上10點(diǎn)35分']}[] Event-Extraction Results:{'加班觸發(fā)詞': {},'出行觸發(fā)詞': {'時(shí)間': ['5月17號(hào)晚上10點(diǎn)35分', '公司'],'出發(fā)地': ['公司'],'目的地': ['公司', '家'],'花費(fèi)': ['36塊五']}}
