Пошаговое руководство от аннотации к обучению
Вступление
Основываясь на моем недавнем руководстве о том, как добавлять примечания к 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 или подписывайтесь здесь!