Пошаговое руководство от аннотации к обучению

Вступление

Основываясь на моем недавнем руководстве о том, как добавлять примечания к PDF-файлам и отсканированным изображениям для приложений NLP, мы попытаемся настроить недавно выпущенную Microsoft Layout LM model на аннотированный пользовательский набор данных, который включает счета-фактуры на французском и английском языках. В то время как в предыдущих руководствах основное внимание уделялось использованию общедоступного набора данных FUNSD для точной настройки модели, здесь мы покажем весь процесс, начиная от аннотации и предварительной обработки до обучения и вывода.

МакетLM Модель

Модель LayoutLM основана на архитектуре BERT, но с двумя дополнительными типами встраивания входных данных. Первый - это встраивание двухмерной позиции, которое обозначает относительное положение токена в документе, а второе - вложение изображения для отсканированных изображений токена в документе. Эта модель позволила достичь новых результатов в нескольких последующих задачах, включая понимание формы (от 70,72 до 79,27), понимание квитанции (от 94,02 до 95,24) и классификацию изображений документа (от 93,07 до 94,42). Для получения дополнительной информации обратитесь к оригинальной статье.

К счастью, исходный код модели был открыт и доступен в библиотеке huggingface. Спасибо, Microsoft!

В этом руководстве мы клонируем модель прямо из библиотеки huggingface и настраиваем ее на нашем собственном наборе данных, ссылка на Google Colab находится ниже. Но сначала нам нужно создать обучающие данные.

Https://colab.research.google.com/drive/1KnkVuYW6Ne25hOZ_IApiv_MIYb4lxCAq?usp=sharing

Аннотация счета

Используя Инструмент текстовых аннотаций UBIAI, я аннотировал около 50 личных счетов. Мне интересно извлекать как ключи, так и значения сущностей; например, в следующем тексте Дата: 06/12/2021 мы должны аннотировать Дата как DATE_ID и 06/12/2021 как DATE. Извлечение ключей и значений поможет нам соотнести числовые значения с их атрибутами. Вот все аннотированные объекты:

DATE_ID, DATE, INVOICE_ID, INVOICE_NUMBER,SELLER_ID, SELLER, MONTANT_HT_ID, MONTANT_HT, TVA_ID, TVA, TTC_ID, TTC

Вот несколько определений сущностей:

MONTANT_HT: Total price pre-tax
TTC: Total price with tax
TVA: Tax amount

Ниже приведен пример аннотированного счета с использованием UBIAI:

После аннотации мы экспортируем обучающие и тестовые файлы из UBIAI напрямую в правильный формат без какой-либо предварительной обработки. Экспорт будет включать три файла для каждого обучающего и тестового наборов данных и один текстовый файл, содержащий все метки с именем labels.txt:

Поезд / Test.txt

2018	O
Sous-total	O
en	O
EUR	O
3,20	O
€	O
TVA	S-TVA_ID
(0%)	O
0,00 €	S-TVA
Total	B-TTC_ID
en	I-TTC_ID
EUR	E-TTC_ID
3,20	S-TTC
€	O
Services	O
soumis	O
au	O
mécanisme	O
d'autoliquidation	O
-	O

Train / Test_box.txt (содержит ограничивающую рамку для каждого токена):

€	912 457 920 466
Services	80 486 133 495
soumis	136 487 182 495
au	185 488 200 495
mécanisme	204 486 276 495
d'autoliquidation	279 486 381 497
-	383 490 388 492

Train / Test_image.txt (содержит ограничивающую рамку, размер документа и имя):

€ 912 425 920 434 1653 2339 image1.jpg
TVA 500 441 526 449 1653 2339  image1.jpg
(0%) 529 441 557 451 1653 2339  image1.jpg
0,00 € 882 441 920 451 1653 2339  image1.jpg
Total 500 457 531 466 1653 2339  image1.jpg
en 534 459 549 466 1653 2339  image1.jpg
EUR 553 457 578 466 1653 2339  image1.jpg
3,20 882 457 911 467 1653 2339  image1.jpg
€ 912 457 920 466 1653 2339  image1.jpg
Services 80 486 133 495 1653 2339  image1.jpg
soumis 136 487 182 495 1653 2339  image1.jpg
au 185 488 200 495 1653 2339  image1.jpg
mécanisme 204 486 276 495 1653 2339  image1.jpg
d'autoliquidation 279 486 381 497 1653 2339  image1.jpg
- 383 490 388 492 1653 2339  image1.jpg

label.txt:

B-DATE_ID
B-INVOICE_ID
B-INVOICE_NUMBER
B-MONTANT_HT
B-MONTANT_HT_ID
B-SELLER
B-TTC
B-DATE
B-TTC_ID
B-TVA
B-TVA_ID
E-DATE_ID
E-DATE
E-INVOICE_ID
E-INVOICE_NUMBER
E-MONTANT_HT
E-MONTANT_HT_ID
E-SELLER
E-TTC
E-TTC_ID
E-TVA
E-TVA_ID
I-DATE_ID
I-DATE
I-SELLER
I-INVOICE_ID
I-MONTANT_HT_ID
I-TTC
I-TTC_ID
I-TVA_ID
O
S-DATE_ID
S-DATE
S-INVOICE_ID
S-INVOICE_NUMBER
S-MONTANT_HT_ID
S-MONTANT_HT
S-SELLER
S-TTC
S-TTC_ID
S-TVA
S-TVA_ID

Тонкая настройка макета Модель LM:

Здесь мы используем Google Colab с графическим процессором для точной настройки модели. Приведенный ниже код основан на исходном документе LayoutLM и этом руководстве.

Сначала установите пакет layoutLM…

! rm -r unilm
! git clone -b remove_torch_save https://github.com/NielsRogge/unilm.git
! cd unilm/layoutlm
! pip install unilm/layoutlm

… А также пакет трансформатора, откуда будет скачана модель:

! rm -r transformers
! git clone https://github.com/huggingface/transformers.git
! cd transformers
! pip install ./transformers

Затем создайте список, содержащий уникальные метки из label.txt:

from torch.nn import CrossEntropyLoss
def get_labels(path):
    with open(path, "r") as f:
        labels = f.read().splitlines()
    if "O" not in labels:
        labels = ["O"] + labels
    return labels
labels = get_labels("./labels.txt")
num_labels = len(labels)
label_map = {i: label for i, label in enumerate(labels)}
pad_token_label_id = CrossEntropyLoss().ignore_index

Затем создайте набор данных pytorch и загрузчик данных:

from transformers import LayoutLMTokenizer
from layoutlm.data.funsd import FunsdDataset, InputFeatures
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler
args = {'local_rank': -1,
        'overwrite_cache': True,
        'data_dir': '/content/data',
        'model_name_or_path':'microsoft/layoutlm-base-uncased',
        'max_seq_length': 512,
        'model_type': 'layoutlm',}
# class to turn the keys of a dict into attributes
class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self
args = AttrDict(args)
tokenizer = LayoutLMTokenizer.from_pretrained("microsoft/layoutlm-base-uncased")
# the LayoutLM authors already defined a specific FunsdDataset, so we are going to use this here
train_dataset = FunsdDataset(args, tokenizer, labels, pad_token_label_id, mode="train")
train_sampler = RandomSampler(train_dataset)
train_dataloader = DataLoader(train_dataset,
                              sampler=train_sampler,
                              batch_size=2)
eval_dataset = FunsdDataset(args, tokenizer, labels, pad_token_label_id, mode="test")
eval_sampler = SequentialSampler(eval_dataset)
eval_dataloader = DataLoader(eval_dataset,
                             sampler=eval_sampler,
                            batch_size=2)
batch = next(iter(train_dataloader))
input_ids = batch[0][0]
tokenizer.decode(input_ids)

Загрузите модель из huggingface. Это будет точно настроено в наборе данных.

from transformers import LayoutLMForTokenClassification
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = LayoutLMForTokenClassification.from_pretrained("microsoft/layoutlm-base-uncased", num_labels=num_labels)
model.to(device)

Наконец, приступаем к обучению:

from transformers import AdamW
from tqdm import tqdm
optimizer = AdamW(model.parameters(), lr=5e-5)
global_step = 0
num_train_epochs = 50
t_total = len(train_dataloader) * num_train_epochs # total number of training steps
#put the model in training mode
model.train()
for epoch in range(num_train_epochs):
  for batch in tqdm(train_dataloader, desc="Training"):
      input_ids = batch[0].to(device)
      bbox = batch[4].to(device)
      attention_mask = batch[1].to(device)
      token_type_ids = batch[2].to(device)
      labels = batch[3].to(device)
# forward pass
      outputs = model(input_ids=input_ids, bbox=bbox, attention_mask=attention_mask, token_type_ids=token_type_ids,
                      labels=labels)
      loss = outputs.loss
# print loss every 100 steps
      if global_step % 100 == 0:
        print(f"Loss after {global_step} steps: {loss.item()}")
# backward pass to get the gradients 
      loss.backward()
#print("Gradients on classification head:")
      #print(model.classifier.weight.grad[6,:].sum())
# update
      optimizer.step()
      optimizer.zero_grad()
      global_step += 1

Вы должны видеть, как прогресс тренировки и потери обновляются.

После обучения оцените производительность модели с помощью следующей функции:

import numpy as np
from seqeval.metrics import (
    classification_report,
    f1_score,
    precision_score,
    recall_score,
)
eval_loss = 0.0
nb_eval_steps = 0
preds = None
out_label_ids = None
# put model in evaluation mode
model.eval()
for batch in tqdm(eval_dataloader, desc="Evaluating"):
    with torch.no_grad():
        input_ids = batch[0].to(device)
        bbox = batch[4].to(device)
        attention_mask = batch[1].to(device)
        token_type_ids = batch[2].to(device)
        labels = batch[3].to(device)
# forward pass
        outputs = model(input_ids=input_ids, bbox=bbox, attention_mask=attention_mask, token_type_ids=token_type_ids,
                        labels=labels)
        # get the loss and logits
        tmp_eval_loss = outputs.loss
        logits = outputs.logits
eval_loss += tmp_eval_loss.item()
        nb_eval_steps += 1
# compute the predictions
        if preds is None:
            preds = logits.detach().cpu().numpy()
            out_label_ids = labels.detach().cpu().numpy()
        else:
            preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
            out_label_ids = np.append(
                out_label_ids, labels.detach().cpu().numpy(), axis=0
            )
# compute average evaluation loss
eval_loss = eval_loss / nb_eval_steps
preds = np.argmax(preds, axis=2)
out_label_list = [[] for _ in range(out_label_ids.shape[0])]
preds_list = [[] for _ in range(out_label_ids.shape[0])]
for i in range(out_label_ids.shape[0]):
    for j in range(out_label_ids.shape[1]):
        if out_label_ids[i, j] != pad_token_label_id:
            out_label_list[i].append(label_map[out_label_ids[i][j]])
            preds_list[i].append(label_map[preds[i][j]])
results = {
    "loss": eval_loss,
    "precision": precision_score(out_label_list, preds_list),
    "recall": recall_score(out_label_list, preds_list),
    "f1": f1_score(out_label_list, preds_list),
}

Имея всего 50 документов, мы получаем следующие оценки:

Чем больше аннотаций, тем выше мы набираем баллы.

Наконец, сохраните модель для будущего прогноза:

PATH='./drive/MyDrive/trained_layoutlm/layoutlm_UBIAI.pt'
torch.save(model.state_dict(), PATH)

Вывод:

А теперь самое интересное. Давайте загрузим счет, распознаем его и извлечем соответствующие объекты. Для этого теста мы используем счет, которого не было в обучающем или тестовом наборе данных. Для синтаксического анализа текста счета мы используем пакет Tesseract с открытым исходным кодом. Установим пакет:

!sudo apt install tesseract-ocr
!pip install pytesseract

Перед выполнением прогнозов нам необходимо проанализировать текст изображения и предварительно обработать маркеры и ограничивающие прямоугольники в функции. Для этого я создал файл Python предварительной обработки layoutLM_preprocess.py, который упростит предварительную обработку изображения:

import sys
sys.path.insert(1, './drive/MyDrive/UBIAI_layoutlm')
from layoutlm_preprocess import *
image_path='./content/invoice_test.jpg'
image, words, boxes, actual_boxes = preprocess(image_path)

Затем загрузите модель и получите предсказания слов с их ограничивающими рамками:

model_path='./drive/MyDrive/trained_layoutlm/layoutlm_UBIAI.pt'
model=model_load(model_path,num_labels)
word_level_predictions, final_boxes=convert_to_features(image, words, boxes, actual_boxes, model)

Наконец, отобразите изображение с предсказанными объектами и ограничивающими рамками:

draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
def iob_to_label(label):
  if label != 'O':
    return label[2:]
  else:
    return ""
label2color = {'data_id':'green','date':'green','invoice_id':'blue','invoice_number':'blue','montant_ht_id':'black','montant_ht':'black','seller_id':'red','seller':'red', 'ttc_id':'grey','ttc':'grey','':'violet', 'tva_id':'orange','tva':'orange'}
for prediction, box in zip(word_level_predictions, final_boxes):
    predicted_label = iob_to_label(label_map[prediction]).lower()
    draw.rectangle(box, outline=label2color[predicted_label])
    draw.text((box[0] + 10, box[1] - 10), text=predicted_label, fill=label2color[predicted_label], font=font)
image

И вуаля:

Хотя модель допускала несколько ошибок, таких как присвоение метки TTC купленному товару или отсутствие идентификации некоторых идентификаторов, она смогла правильно извлечь продавца, номер счета-фактуры, дату и TTC. Результаты впечатляют и очень многообещающи, учитывая небольшое количество аннотированных документов (всего 50)! Благодаря большему количеству аннотированных счетов-фактур мы сможем получить более высокие оценки F и более точные прогнозы.

Заключение:

В целом результаты модели LayoutLM очень многообещающие и демонстрируют полезность преобразователей при анализе частично структурированного текста. Модель может быть адаптирована к любым другим частично структурированным документам, таким как водительские права, контракты, правительственные документы, финансовые документы и т. Д.

Если у вас есть какие-либо вопросы, не стесняйтесь задавать их ниже или отправьте нам электронное письмо по адресу [email protected].

Если вам понравилась эта статья, поставьте лайк и поделитесь!

Следуйте за нами в Twitter @ UBIAI5 или подписывайтесь здесь!