Transformers documentation

Respuestas a preguntas sobre documentos

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Respuestas a preguntas sobre documentos

Document Question Answering, también conocido como Document Visual Question Answering, es una tarea que consiste en proporcionar respuestas a preguntas formuladas sobre imágenes de documentos. La entrada de los modelos que admiten esta tarea suele ser una combinación de una imagen y una pregunta, y la salida es una respuesta expresada en lenguaje natural. Estos modelos utilizan múltiples modalidades, incluido el texto, las posiciones de las palabras (bounding boxes) y la propia imagen.

Esta guía ilustra cómo:

Para ver todas las arquitecturas y puntos de control compatibles con esta tarea, recomendamos consultar la página de la tarea

LayoutLMv2 resuelve la tarea de respuesta a preguntas en documentos añadiendo una cabeza de respuesta a preguntas sobre los estados ocultos finales de los tokens, para predecir las posiciones de los tokens de inicio y fin de la respuesta. En otras palabras, el problema se trata como una respuesta a preguntas extractiva: dado el contexto, se extrae qué fragmento de información responde a la pregunta. El contexto proviene de la salida de un motor de OCR, en este caso Tesseract de Google.

Antes de comenzar, asegúrate de tener instaladas todas las bibliotecas necesarias. LayoutLMv2 depende de detectron2, torchvision y tesseract.

pip install -q transformers datasets
pip install 'git+https://github.com/facebookresearch/detectron2.git'
pip install torchvision
sudo apt install tesseract-ocr
pip install -q pytesseract

Una vez que hayas instalado todas las dependencias, reinicia tu entorno de ejecución.

Te animamos a compartir tu modelo con la comunidad. Inicia sesión en tu cuenta de Hugging Face para subirlo al 🤗 Hub. Cuando se te solicite, introduce tu token para iniciar sesión:

>>> from huggingface_hub import notebook_login

>>> notebook_login()

Definamos algunas variables globales.

>>> model_checkpoint = "microsoft/layoutlmv2-base-uncased"
>>> batch_size = 4

Cargar los datos

En esta guía utilizamos una pequeña muestra de DocVQA preprocesado que puedes encontrar en 🤗 Hub. Si deseas utilizar el conjunto de datos completo de DocVQA, puedes registrarte y descargarlo en la página principal de DocVQA. Si lo haces, para continuar con esta guía consulta cómo cargar archivos en un conjunto de datos de 🤗.

>>> from datasets import load_dataset

>>> dataset = load_dataset("nielsr/docvqa_1200_examples")
>>> dataset
DatasetDict({
    train: Dataset({
        features: ['id', 'image', 'query', 'answers', 'words', 'bounding_boxes', 'answer'],
        num_rows: 1000
    })
    test: Dataset({
        features: ['id', 'image', 'query', 'answers', 'words', 'bounding_boxes', 'answer'],
        num_rows: 200
    })
})

Como puede ver, el conjunto de datos ya está dividido en conjuntos de entrenamiento y prueba. Examine un ejemplo aleatorio para familiarizarse con las características.

>>> dataset["train"].features

Estos son los campos individuales y lo que representan:

  • id: el id del ejemplo
  • image: un objeto PIL.Image.Image que contiene la imagen del documento
  • query: la cadena de texto de la pregunta - pregunta formulada en lenguaje natural, en varios idiomas
  • answers: una lista de respuestas correctas proporcionadas por anotadores humanos
  • words y bounding_boxes: los resultados de OCR, que no utilizaremos aquí
  • answer: una respuesta que coincide con un modelo diferente, que no utilizaremos aquí

Dejemos solo las preguntas en inglés y descartemos la característica answer, que parece contener predicciones de otro modelo. También tomaremos la primera de las respuestas del conjunto proporcionado por los anotadores. Alternativamente, puedes muestrearla aleatoriamente.

>>> updated_dataset = dataset.map(lambda example: {"question": example["query"]["en"]}, remove_columns=["query"])
>>> updated_dataset = updated_dataset.map(
...     lambda example: {"answer": example["answers"][0]}, remove_columns=["answer", "answers"]
... )

Tenga en cuenta que el checkpoint de LayoutLMv2 que utilizamos en esta guía se ha entrenado con max_position_embeddings = 512 (puede encontrar esta información en el archivo config.json del checkpoint). Podemos truncar los ejemplos, pero para evitar la situación en la que la respuesta podría estar al final de un documento extenso y acabar truncada, aquí eliminaremos los pocos ejemplos en los que es probable que el embedding termine siendo mayor que 512. Si la mayoría de los documentos de su conjunto de datos son largos, puede implementar una estrategia de ventana deslizante; consulte este notebook para obtener más detalles.

>>> updated_dataset = updated_dataset.filter(lambda x: len(x["words"]) + len(x["question"].split()) < 512)

En este punto, eliminemos también las características de OCR de este conjunto de datos. Estas son el resultado de un OCR para el ajuste fino de un modelo diferente. Si quisiéramos usarlas, aún requerirían algún procesamiento, ya que no coinciden con los requisitos de entrada del modelo que utilizamos en esta guía. En su lugar, podemos usar LayoutLMv2Processor sobre los datos originales tanto para el OCR como para la tokenización. De esta forma, obtendremos las entradas que coinciden con la entrada esperada por el modelo. Si deseas procesar las imágenes manualmente, consulta la documentación del modelo LayoutLMv2 para conocer el formato de entrada que espera el modelo.

>>> updated_dataset = updated_dataset.remove_columns("words")
>>> updated_dataset = updated_dataset.remove_columns("bounding_boxes")

Por último, la exploración de los datos no estará completa si no echamos un vistazo a un ejemplo de imagen.

>>> updated_dataset["train"][11]["image"]
DocVQA Image Example

Preprocesar los datos

La tarea de respuesta a preguntas sobre documentos es una tarea multimodal, y es necesario asegurarse de que las entradas de cada modalidad se preprocesen según las expectativas del modelo. Comencemos cargando el LayoutLMv2Processor, que internamente combina un procesador de imágenes capaz de manejar datos de imagen y un tokenizador que puede codificar datos de texto.

>>> from transformers import AutoProcessor

>>> processor = AutoProcessor.from_pretrained(model_checkpoint)

Preprocesamiento de imágenes de documentos

Primero, preparemos las imágenes del documento para el modelo con la ayuda del image_processor del procesador. Por defecto, el procesador de imágenes redimensiona las imágenes a 224x224, se asegura de que tengan el orden correcto de los canales de color, y aplica OCR con tesseract para obtener palabras y cajas delimitadoras normalizadas. En este tutorial, todos estos valores predeterminados son exactamente lo que necesitamos. Escribe una función que aplique el procesamiento de imágenes predeterminado a un lote de imágenes y devuelva los resultados del OCR.

>>> image_processor = processor.image_processor


>>> def get_ocr_words_and_boxes(examples):
...     images = [image.convert("RGB") for image in examples["image"]]
...     encoded_inputs = image_processor(images)

...     examples["image"] = encoded_inputs.pixel_values
...     examples["words"] = encoded_inputs.words
...     examples["boxes"] = encoded_inputs.boxes

...     return examples

Para aplicar este preprocesamiento a todo el conjunto de datos de forma rápida, use map.

>>> dataset_with_ocr = updated_dataset.map(get_ocr_words_and_boxes, batched=True, batch_size=2)

Preprocesamiento de datos de texto

Una vez que hemos aplicado OCR a las imágenes, necesitamos codificar la parte de texto del conjunto de datos para prepararlo para el modelo. Esto implica convertir las palabras y las cajas que obtuvimos en el paso anterior en input_ids, attention_mask, token_type_ids y bbox a nivel de token. Para preprocesar el texto, necesitaremos el tokenizer del procesador.

>>> tokenizer = processor.tokenizer

Además del preprocesamiento mencionado anteriormente, también debemos añadir las etiquetas para el modelo. En los modelos xxxForQuestionAnswering de 🤗 Transformers, las etiquetas consisten en start_positions y end_positions, que indican qué token se encuentra al inicio y cuál al final de la respuesta.

Empecemos por ahí. Define una función auxiliar que pueda encontrar una sublista (la respuesta dividida en palabras) dentro de una lista más grande (la lista de palabras).

This function will take two lists as input, words_list and answer_list. It will then iterate over the words_list and check if the current word in the words_list (words_list[i]) is equal to the first word of answer_list (answer_list[0]) and if the sublist of words_list starting from the current word and of the same length as answer_list is equal to answer_list. If this condition is true, it means that a match has been found, and the function will record the match, its starting index (idx), and its ending index (idx + len(answer_list) - 1). If more than one match was found, the function will return only the first one. If no match is found, the function returns (None, 0, and 0).

>>> def subfinder(words_list, answer_list):
...     matches = []
...     start_indices = []
...     end_indices = []
...     for idx, i in enumerate(range(len(words_list))):
...         if words_list[i] == answer_list[0] and words_list[i : i + len(answer_list)] == answer_list:
...             matches.append(answer_list)
...             start_indices.append(idx)
...             end_indices.append(idx + len(answer_list) - 1)
...     if matches:
...         return matches[0], start_indices[0], end_indices[0]
...     else:
...         return None, 0, 0

Para ilustrar cómo esta función encuentra la posición de la respuesta, utilicémosla en un ejemplo:

>>> example = dataset_with_ocr["train"][1]
>>> words = [word.lower() for word in example["words"]]
>>> match, word_idx_start, word_idx_end = subfinder(words, example["answer"].lower().split())
>>> print("Question: ", example["question"])
>>> print("Words:", words)
>>> print("Answer: ", example["answer"])
>>> print("start_index", word_idx_start)
>>> print("end_index", word_idx_end)
Question:  Who is in  cc in this letter?
Words: ['wie', 'baw', 'brown', '&', 'williamson', 'tobacco', 'corporation', 'research', '&', 'development', 'internal', 'correspondence', 'to:', 'r.', 'h.', 'honeycutt', 'ce:', 't.f.', 'riehl', 'from:', '.', 'c.j.', 'cook', 'date:', 'may', '8,', '1995', 'subject:', 'review', 'of', 'existing', 'brainstorming', 'ideas/483', 'the', 'major', 'function', 'of', 'the', 'product', 'innovation', 'graup', 'is', 'to', 'develop', 'marketable', 'nove!', 'products', 'that', 'would', 'be', 'profitable', 'to', 'manufacture', 'and', 'sell.', 'novel', 'is', 'defined', 'as:', 'of', 'a', 'new', 'kind,', 'or', 'different', 'from', 'anything', 'seen', 'or', 'known', 'before.', 'innovation', 'is', 'defined', 'as:', 'something', 'new', 'or', 'different', 'introduced;', 'act', 'of', 'innovating;', 'introduction', 'of', 'new', 'things', 'or', 'methods.', 'the', 'products', 'may', 'incorporate', 'the', 'latest', 'technologies,', 'materials', 'and', 'know-how', 'available', 'to', 'give', 'then', 'a', 'unique', 'taste', 'or', 'look.', 'the', 'first', 'task', 'of', 'the', 'product', 'innovation', 'group', 'was', 'to', 'assemble,', 'review', 'and', 'categorize', 'a', 'list', 'of', 'existing', 'brainstorming', 'ideas.', 'ideas', 'were', 'grouped', 'into', 'two', 'major', 'categories', 'labeled', 'appearance', 'and', 'taste/aroma.', 'these', 'categories', 'are', 'used', 'for', 'novel', 'products', 'that', 'may', 'differ', 'from', 'a', 'visual', 'and/or', 'taste/aroma', 'point', 'of', 'view', 'compared', 'to', 'canventional', 'cigarettes.', 'other', 'categories', 'include', 'a', 'combination', 'of', 'the', 'above,', 'filters,', 'packaging', 'and', 'brand', 'extensions.', 'appearance', 'this', 'category', 'is', 'used', 'for', 'novel', 'cigarette', 'constructions', 'that', 'yield', 'visually', 'different', 'products', 'with', 'minimal', 'changes', 'in', 'smoke', 'chemistry', 'two', 'cigarettes', 'in', 'cne.', 'emulti-plug', 'te', 'build', 'yaur', 'awn', 'cigarette.', 'eswitchable', 'menthol', 'or', 'non', 'menthol', 'cigarette.', '*cigarettes', 'with', 'interspaced', 'perforations', 'to', 'enable', 'smoker', 'to', 'separate', 'unburned', 'section', 'for', 'future', 'smoking.', '«short', 'cigarette,', 'tobacco', 'section', '30', 'mm.', '«extremely', 'fast', 'buming', 'cigarette.', '«novel', 'cigarette', 'constructions', 'that', 'permit', 'a', 'significant', 'reduction', 'iretobacco', 'weight', 'while', 'maintaining', 'smoking', 'mechanics', 'and', 'visual', 'characteristics.', 'higher', 'basis', 'weight', 'paper:', 'potential', 'reduction', 'in', 'tobacco', 'weight.', '«more', 'rigid', 'tobacco', 'column;', 'stiffing', 'agent', 'for', 'tobacco;', 'e.g.', 'starch', '*colored', 'tow', 'and', 'cigarette', 'papers;', 'seasonal', 'promotions,', 'e.g.', 'pastel', 'colored', 'cigarettes', 'for', 'easter', 'or', 'in', 'an', 'ebony', 'and', 'ivory', 'brand', 'containing', 'a', 'mixture', 'of', 'all', 'black', '(black', 'paper', 'and', 'tow)', 'and', 'ail', 'white', 'cigarettes.', '499150498']
Answer:  T.F. Riehl
start_index 17
end_index 18

Sin embargo, una vez que los ejemplos se codifican, se verán así:

>>> encoding = tokenizer(example["question"], example["words"], example["boxes"])
>>> tokenizer.decode(encoding["input_ids"])
[CLS] who is in cc in this letter? [SEP] wie baw brown & williamson tobacco corporation research & development ...

Necesitaremos encontrar la posición de la respuesta en la entrada codificada.

  • token_type_ids nos indica qué tokens forman parte de la pregunta y cuáles forman parte de las palabras del documento.
  • tokenizer.cls_token_id servirá para encontrar el token especial al inicio de la entrada.
  • word_ids servirá para hacer coincidir la respuesta encontrada en las words originales con la misma respuesta en la entrada codificada completa y determinar la posición de inicio/fin de la respuesta en la entrada codificada.

Con eso en mente, creemos una función para codificar un lote de ejemplos del conjunto de datos:

>>> def encode_dataset(examples, max_length=512):
...     questions = examples["question"]
...     words = examples["words"]
...     boxes = examples["boxes"]
...     answers = examples["answer"]

...     # encode the batch of examples and initialize the start_positions and end_positions
...     encoding = tokenizer(questions, words, boxes, max_length=max_length, padding="max_length", truncation=True)
...     start_positions = []
...     end_positions = []

...     # loop through the examples in the batch
...     for i in range(len(questions)):
...         cls_index = encoding["input_ids"][i].index(tokenizer.cls_token_id)

...         # find the position of the answer in example's words
...         words_example = [word.lower() for word in words[i]]
...         answer = answers[i]
...         match, word_idx_start, word_idx_end = subfinder(words_example, answer.lower().split())

...         if match:
...             # if match is found, use `token_type_ids` to find where words start in the encoding
...             token_type_ids = encoding["token_type_ids"][i]
...             token_start_index = 0
...             while token_type_ids[token_start_index] != 1:
...                 token_start_index += 1

...             token_end_index = len(encoding["input_ids"][i]) - 1
...             while token_type_ids[token_end_index] != 1:
...                 token_end_index -= 1

...             word_ids = encoding.word_ids(i)[token_start_index : token_end_index + 1]
...             start_position = cls_index
...             end_position = cls_index

...             # loop over word_ids and increase `token_start_index` until it matches the answer position in words
...             # once it matches, save the `token_start_index` as the `start_position` of the answer in the encoding
...             for id in word_ids:
...                 if id == word_idx_start:
...                     start_position = token_start_index
...                 else:
...                     token_start_index += 1

...             # similarly loop over `word_ids` starting from the end to find the `end_position` of the answer
...             for id in word_ids[::-1]:
...                 if id == word_idx_end:
...                     end_position = token_end_index
...                 else:
...                     token_end_index -= 1

...             start_positions.append(start_position)
...             end_positions.append(end_position)

...         else:
...             start_positions.append(cls_index)
...             end_positions.append(cls_index)

...     encoding["image"] = examples["image"]
...     encoding["start_positions"] = start_positions
...     encoding["end_positions"] = end_positions

...     return encoding

Ahora que tenemos esta función de preprocesamiento, podemos codificar el conjunto de datos completo:

>>> encoded_train_dataset = dataset_with_ocr["train"].map(
...     encode_dataset, batched=True, batch_size=2, remove_columns=dataset_with_ocr["train"].column_names
... )
>>> encoded_test_dataset = dataset_with_ocr["test"].map(
...     encode_dataset, batched=True, batch_size=2, remove_columns=dataset_with_ocr["test"].column_names
... )

Veamos cómo son las características del conjunto de datos codificado:

>>> encoded_train_dataset.features
{'image': Sequence(feature=Sequence(feature=Sequence(feature=Value(dtype='uint8', id=None), length=-1, id=None), length=-1, id=None), length=-1, id=None),
 'input_ids': Sequence(feature=Value(dtype='int32', id=None), length=-1, id=None),
 'token_type_ids': Sequence(feature=Value(dtype='int8', id=None), length=-1, id=None),
 'attention_mask': Sequence(feature=Value(dtype='int8', id=None), length=-1, id=None),
 'bbox': Sequence(feature=Sequence(feature=Value(dtype='int64', id=None), length=-1, id=None), length=-1, id=None),
 'start_positions': Value(dtype='int64', id=None),
 'end_positions': Value(dtype='int64', id=None)}

Evaluación

La evaluación para la respuesta de preguntas sobre documentos requiere una cantidad significativa de posprocesamiento. Para no ocupar demasiado tu tiempo, esta guía omite el paso de evaluación. El Trainer sigue calculando la pérdida de evaluación durante el entrenamiento, por lo que no quedas completamente a ciegas sobre el rendimiento de tu modelo. La respuesta de preguntas extractiva se evalúa típicamente usando F1/exact match. Si deseas implementarlo tú mismo, consulta el capítulo de respuesta de preguntas del curso de Hugging Face para obtener inspiración.

Entrenar

¡Felicidades! Has superado con éxito la parte más difícil de esta guía y ahora estás listo para entrenar tu propio modelo. El entrenamiento implica los siguientes pasos:

  • Cargue el modelo con AutoModelForDocumentQuestionAnswering utilizando el mismo punto de control que en el preprocesamiento.
  • Define los hiperparámetros de entrenamiento en TrainingArguments.
  • Defina una función para agrupar ejemplos en lotes; aquí, DefaultDataCollator será suficiente.
  • Pasa los argumentos de entrenamiento a Trainer junto con el modelo, el conjunto de datos y el data collator.
  • Llama a train() para ajustar tu modelo.
>>> from transformers import AutoModelForDocumentQuestionAnswering

>>> model = AutoModelForDocumentQuestionAnswering.from_pretrained(model_checkpoint)

En TrainingArguments, usa output_dir para especificar dónde guardar tu modelo y configura los hiperparámetros según lo consideres oportuno. Si deseas compartir tu modelo con la comunidad, establece push_to_hub en True (debes iniciar sesión en Hugging Face para subir tu modelo). En este caso, output_dir también será el nombre del repositorio donde se subirá el punto de control de tu modelo.

>>> from transformers import TrainingArguments

>>> # REPLACE THIS WITH YOUR REPO ID
>>> repo_id = "MariaK/layoutlmv2-base-uncased_finetuned_docvqa"

>>> training_args = TrainingArguments(
...     output_dir=repo_id,
...     per_device_train_batch_size=4,
...     num_train_epochs=20,
...     save_steps=200,
...     logging_steps=50,
...     eval_strategy="steps",
...     learning_rate=5e-5,
...     save_total_limit=2,
...     remove_unused_columns=False,
...     push_to_hub=True,
... )

Defina un collator de datos simple para agrupar ejemplos en lotes.

>>> from transformers import DefaultDataCollator

>>> data_collator = DefaultDataCollator()

Por último, reúne todo y llama a train():

>>> from transformers import Trainer

>>> trainer = Trainer(
...     model=model,
...     args=training_args,
...     data_collator=data_collator,
...     train_dataset=encoded_train_dataset,
...     eval_dataset=encoded_test_dataset,
...     processing_class=processor,
... )

>>> trainer.train()

Para añadir el modelo final a 🤗 Hub, crea una tarjeta de modelo y llama a push_to_hub:

>>> trainer.create_model_card()
>>> trainer.push_to_hub()

Inferencia

Ahora que has afinado un modelo de LayoutLMv2 y lo has subido al 🤗 Hub, puedes usarlo para inferencia. La forma más sencilla de probar tu modelo afinado para inferencia es utilizarlo en un Pipeline.

Veamos un ejemplo:

>>> example = dataset["test"][2]
>>> question = example["query"]["en"]
>>> image = example["image"]
>>> print(question)
>>> print(example["answers"])
'Who is ‘presiding’ TRRF GENERAL SESSION (PART 1)?'
['TRRF Vice President', 'lee a. waller']

A continuación, instancie un pipeline para respuesta a preguntas sobre documentos con su modelo y pase la combinación de imagen + pregunta a este.

>>> from transformers import pipeline

>>> qa_pipeline = pipeline("document-question-answering", model="MariaK/layoutlmv2-base-uncased_finetuned_docvqa")
>>> qa_pipeline(image, question)
[{'score': 0.9949808120727539,
  'answer': 'Lee A. Waller',
  'start': 55,
  'end': 57}]

También puedes replicar manualmente los resultados de la pipeline si lo deseas:

  1. Toma una imagen y una pregunta, y prepáralas para el modelo usando el procesador de tu modelo.
  2. Pase el resultado o el preprocesamiento a través del modelo.
  3. El modelo devuelve start_logits y end_logits, que indican qué token se encuentra al inicio de la respuesta y qué token se encuentra al final de la respuesta. Ambos tienen forma (batch_size, sequence_length).
  4. Aplica un argmax en la última dimensión de start_logits y end_logits para obtener el start_idx y el end_idx predichos.
  5. Decodifica la respuesta con el tokenizador.
>>> import torch
>>> from transformers import AutoProcessor
>>> from transformers import AutoModelForDocumentQuestionAnswering

>>> processor = AutoProcessor.from_pretrained("MariaK/layoutlmv2-base-uncased_finetuned_docvqa")
>>> model = AutoModelForDocumentQuestionAnswering.from_pretrained("MariaK/layoutlmv2-base-uncased_finetuned_docvqa")

>>> with torch.no_grad():
...     encoding = processor(image.convert("RGB"), question, return_tensors="pt")
...     outputs = model(**encoding)
...     start_logits = outputs.start_logits
...     end_logits = outputs.end_logits
...     predicted_start_idx = start_logits.argmax(-1).item()
...     predicted_end_idx = end_logits.argmax(-1).item()

>>> processor.tokenizer.decode(encoding.input_ids.squeeze()[predicted_start_idx : predicted_end_idx + 1])
'lee a. waller'
Update on GitHub