Crédit d’image
introduction
Sur la base de mon récent tutoriel sur la façon d’annoter des PDF et des images numérisées pour les applications NLP, nous tenterons d’affiner le modèle Layout LM de Microsoft récemment publié sur un ensemble de données personnalisé annoté qui comprend des factures en français et en anglais. Alors que les didacticiels précédents se concentraient sur l’utilisation de l’ensemble de données FUNSD accessible au public pour affiner le modèle, nous montrerons ici l’ensemble du processus, de l’annotation et du pré-traitement à la formation et à l’inférence.
Modèle LayoutLM
Le modèle LayoutLM est basé sur l’architecture BERT mais avec deux types supplémentaires d’intégrations d’entrée. Le premier est une incorporation de position 2-D qui dénote la position relative d’un jeton dans un document, et le second est une incorporation d’image pour les images de jeton numérisées dans un document. Ce modèle a obtenu de nouveaux résultats de pointe dans plusieurs tâches en aval, notamment la compréhension des formulaires (de 70,72 à 79,27), la compréhension des reçus (de 94,02 à 95,24) et la classification des images de documents (de 93,07 à 94,42). Pour plus d’informations, reportez-vous à l’article d’origine.
Heureusement, le modèle était open source et mis à disposition dans la bibliothèque huggingface. Merci Microsoft !
Pour ce tutoriel, nous allons cloner le modèle directement à partir de la bibliothèque huggingface et l’affiner sur notre propre jeu de données. Voici un lien vers Google Colab, mais nous devons d’abord créer les données d’entraînement.
Annotation de la facture
À l’aide d’un outil d’annotation de texte, j’ai annoté une cinquantaine de factures personnelles. Je suis intéressé à extraire à la fois les clés et les valeurs des entités ; par exemple, dans le texte suivant « Date : 06/12/2021 », nous annoterions « Date » comme DATE_ID et « 06/12/2021 » comme DATE. L’extraction des clés et des valeurs nous aidera à corréler les valeurs numériques à leurs attributs. Voici toutes les entités qui ont été annotées :
DATE_ID, DATE, INVOICE_ID, INVOICE_NUMBER,SELLER_ID, SELLER, MONTANT_HT_ID, MONTANT_HT, TVA_ID, TVA, TTC_ID, TTC
Voici quelques définitions d’entités :
MONTANT_HT: Total price pre-taxTTC: Total price with taxTVA: Tax amount
Voici un exemple de facture annotée utilisant UBIAI :

Après annotation, nous exportons les fichiers de train et de test d’UBIAI directement dans le bon format sans aucune étape de pré-traitement. L’exportation comprendra trois fichiers pour chaque ensemble de données d’entraînement et de test et un fichier texte contenant toutes les étiquettes nommées labels.txt :
Train/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 (contient un cadre de délimitation pour chaque jeton) :
€ 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 (contient un cadre de délimitation, la taille du document et le nom) :
€ 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
labels.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
Ajustement du modèle LayoutLM
Ici, nous utilisons Google Colab avec GPU pour affiner le modèle. Le code ci-dessous est basé sur le papier layoutLM original et ce tutoriel.
Tout d’abord, installez le package layoutLM.
! rm -r unilm! git clone -b remove_torch_save https://github.com/NielsRogge/unilm.git! cd unilm/layoutlm! pip install unilm/layoutlm
Ainsi que le package du transformateur à partir duquel le modèle sera téléchargé :
! rm -r transformers! git clone https://github.com/huggingface/transformers.git! cd transformers! pip install ./transformers
Ensuite, créez une liste contenant les étiquettes uniques à partir de labels.txt :
from torch.nn import CrossEntropyLossdef get_labels(path): with open(path, "r") as f: labels = f.read().splitlines() if "O" not in labels: labels = ["O"] + labels return labelslabels = 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
Ensuite, créez un ensemble de données PyTorch et un chargeur de données :
from transformers import LayoutLMTokenizer from layoutlm.data.funsd import FunsdDataset, InputFeatures from torch.utils.data import DataLoader, RandomSampler, SequentialSamplerargs = {'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__ = selfargs = 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)
Chargez le modèle depuis huggingface. Cela sera affiné sur l’ensemble de données.
from transformers import LayoutLMForTokenClassification import torchdevice = 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)
Enfin, commencez la formation :
from transformers import AdamW from tqdm import tqdmoptimizer = 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
Vous devriez être en mesure de voir les progrès de l’entraînement et la perte se mettre à jour.

Après la formation, évaluez les performances du modèle avec la fonction suivante :
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.logitseval_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), }
Avec seulement 50 documents, nous obtenons les scores suivants :

Avec plus d’annotations, nous devrions certainement obtenir des scores plus élevés.
Enfin, enregistrez le modèle pour une prévision future :
PATH='./drive/MyDrive/trained_layoutlm/layoutlm_UBIAI.pt'torch.save(model.state_dict(), PATH)
Inférence
Vient maintenant la partie amusante, téléchargeons une facture, procédons à la reconnaissance optique de caractères et extrayons les entités pertinentes. Pour ce test, nous utilisons une facture qui ne figurait pas dans l’ensemble de données d’entraînement ou de test. Pour analyser le texte de la facture, nous utilisons le package open source Tesseract. Installons le paquet :
!sudo apt install tesseract-ocr!pip install pytesseract
Avant d’exécuter des prédictions, nous devons analyser le texte de l’image et pré-traiter les jetons et les cadres de délimitation en fonctionnalités. Pour ce faire, j’ai créé un fichier de prétraitement python layoutLM_preprocess.py qui facilitera le prétraitement de l’image :
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)
Ensuite, chargez le modèle et obtenez des prédictions de mots avec leurs cadres de délimitation :
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,...