Обзор, способы реализации и выводы

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

Этот цикл статей посвящен демистификации парсинга PDF. В предыдущей статье мы описали основную задачу парсинга PDF, классифицировали существующие методы и дали краткое описание каждого из них.

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

Обзор

Конвейерный подход рассматривает задачу парсинга PDF-файлов как показано на рисунке 1.

Рисунок 1: Общий алгоритм конвейерного подхода. Изображение автора.
Рисунок 1: Общий алгоритм конвейерного подхода. Изображение автора.

Конвейерный подход можно разделить на следующие пять этапов:

  • Предобработка PDF-файлов  целью исправления таких проблем, как размытость или перекос в ориентации страниц. Этот этап включает в себя повышение качества изображения, коррекцию положения и т. д.

  • Проведение анализа макета, который можно разделить на два этапа: визуальный и семантический анализ структуры. Первый выявляет структуру документа и выделяет схожие области, а второй маркирует эти области определенными типами, такими как текст, заголовок, список, таблица, рисунок и т. д. На этом этапе также определяется порядок чтения страницы.

  • Отделение различных областей, выявленных в ходе анализа макета, друг от друга. Этот процесс включает в себя распознавание таблиц, текста и определение других компонентов, таких как формулы, блок-схемы и специальные символы.

  • Воспроизведение структуры страницы документа на основе полученных ранее результатов.

  • Вывод структурированной или полуструктурированной информации, например, в формате Markdown, JSON или HTML.

Теперь мы с вами рассмотрим несколько фреймворков для парсинга PDF. Таким образом мы сможем сформировать представление о конвейерном подходе и получить некоторые выводы, о которых мы поговорим в конце.

Marker

Marker — это конвейер на основе моделей глубокого обучения. Он способен конвертировать PDF-, EPUB- и MOBI-документы в формат Markdown.

Общий процесс

Как показано на рисунке 2, процесс работы с Marker разделен на следующие четыре этапа:

Рисунок 2: Конвейер Marker. Изображение автора.
Рисунок 2: Конвейер Marker. Изображение автора.

Шаг 1: Для начала разделим страницы на блоки и извлечем текст с помощью PyMuPDF и OCR. Ниже приведен соответствующий код:

def convert_single_pdf(
        fname: str,
        model_lst: List,
        max_pages=None,
        metadata: Optional[Dict]=None,
        parallel_factor: int = 1
) -> Tuple[str, Dict]:
    ...
    ...
    doc = pymupdf.open(fname, filetype=filetype)
    if filetype != "pdf":
        conv = doc.convert_to_pdf()
        doc = pymupdf.open("pdf", conv)


    blocks, toc, ocr_stats = get_text_blocks(
        doc,
        tess_lang,
        spell_lang,
        max_pages=max_pages,
        parallel=int(parallel_factor * settings.OCR_PARALLEL_WORKERS)
    )

Шаг 2: Используем сегментатор макета, чтобы выделить отдельные блоки, и упорядочим их с помощью детектора колонок. Соответствующий код имеет вид:

def convert_single_pdf(
        fname: str,
        model_lst: List,
        max_pages=None,
        metadata: Optional[Dict]=None,
        parallel_factor: int = 1
) -> Tuple[str, Dict]:
    ...
    ...
    # Распаковка моделей из списка


    texify_model, layoutlm_model, order_model, edit_model = model_lst


    block_types = detect_document_block_types(
        doc,
        blocks,
        layoutlm_model,
        batch_size=int(settings.LAYOUT_BATCH_SIZE * parallel_factor)
    )


    # Поиск верхних и нижних колонтитулов


    bad_span_ids = filter_header_footer(blocks)
    out_meta["block_stats"] = {"header_footer": len(bad_span_ids)}


    annotate_spans(blocks, block_types)


    # Выгрузка отладочных данных, если установлены соответствующие флаги


    dump_bbox_debug_data(doc, blocks)


    blocks = order_blocks(
        doc,
        blocks,
        order_model,
        batch_size=int(settings.ORDERER_BATCH_SIZE * parallel_factor)
    )
    ...
    ...

Шаг 3: Отфильтруем верхние и нижние колонтитулы, исправим блоки с кодом и таблицами и применим модель Texify для формул. Соответствующий код выглядит следующим образом:

def convert_single_pdf(
        fname: str,
        model_lst: List,
        max_pages=None,
        metadata: Optional[Dict]=None,
        parallel_factor: int = 1
) -> Tuple[str, Dict]:
    ...
    ...
    # Исправляем блоки с кодом
    code_block_count = identify_code_blocks(blocks)
    out_meta["block_stats"]["code"] = code_block_count
    indent_blocks(blocks)


    # Исправляем таблицы
    merge_table_blocks(blocks)
    table_count = create_new_tables(blocks)
    out_meta["block_stats"]["table"] = table_count


    for page in blocks:
        for block in page.blocks:
            block.filter_spans(bad_span_ids)
            block.filter_bad_span_types()


    filtered, eq_stats = replace_equations(
        doc,
        blocks,
        block_types,
        texify_model,
        batch_size=int(settings.TEXIFY_BATCH_SIZE * parallel_factor)
    )
    out_meta["block_stats"]["equations"] = eq_stats
    ...
    ...

Шаг 4: Постобработка текста с помощью модели редактора. Соответствующий код:

def convert_single_pdf(
        fname: str,
        model_lst: List,
        max_pages=None,
        metadata: Optional[Dict]=None,
        parallel_factor: int = 1
) -> Tuple[str, Dict]:
    ...
    ...
    # Копирование во избежание изменения исходных данных
    merged_lines = merge_spans(filtered)
    text_blocks = merge_lines(merged_lines, filtered)
    text_blocks = filter_common_titles(text_blocks)
    full_text = get_full_text(text_blocks)


    # Обработка присоединяемых пустых блоков
    full_text = re.sub(r'\n{3,}', '\n\n', full_text)
    full_text = re.sub(r'(\n\s){3,}', '\n\n', full_text)


    # Меняем маркеры списка на -.
    full_text = replace_bullets(full_text)


    # Постобработка текста с помощью модели редактора
    full_text, edit_stats = edit_full_text(
        full_text,
        edit_model,
        batch_size=settings.EDITOR_BATCH_SIZE * parallel_factor
    )
    out_meta["postprocess_stats"] = {"edit": edit_stats}


    return full_text, out_meta

Выводы по Marker’у

Пока что мы лишь описали общий процесс работы Marker. Но нам уже есть, что обсудить — некоторые выводы, которые мы можем сделать на основе полученной информации.

Вывод 1: Анализ макета можно разделить на несколько подзадач. Первая подзадача включает в себя вызов API PyMuPDF для получения блоков страниц.

def ocr_entire_page(page, lang: str, spellchecker: Optional[SpellChecker] = None) -> List[Block]:
    if settings.OCR_ENGINE == "tesseract":
        return ocr_entire_page_tess(page, lang, spellchecker)
    elif settings.OCR_ENGINE == "ocrmypdf":
        return ocr_entire_page_ocrmp(page, lang, spellchecker)
    else:
        raise ValueError(f"Unknown OCR engine {settings.OCR_ENGINE}")




def ocr_entire_page_tess(page, lang: str, spellchecker: Optional[SpellChecker] = None) -> List[Block]:
    try:
        full_tp = page.get_textpage_ocr(flags=settings.TEXT_FLAGS, dpi=settings.OCR_DPI, full=True, language=lang)
        blocks = page.get_text("dict", sort=True, flags=settings.TEXT_FLAGS, textpage=full_tp)["blocks"]
        full_text = page.get_text("text", sort=True, flags=settings.TEXT_FLAGS, textpage=full_tp)


        if len(full_text) == 0:
            return []


        # Проверяем, сработал ли OCR. Если нет, то возвращаем пустой список


        # OCR может не сработать, если была отсканирована пустая страница нечетко отпечатанным текстом
        if detect_bad_ocr(full_text, spellchecker):
            return []
    except RuntimeError:
        return []
    return blocks

Вывод 2: Тонкая настройка (или дообучение) небольших мультимодальных предварительно обученных моделей, таких как LayoutLMv3, для решения конкретных задач может быть весьма полезна. Например, LayoutLMv3 в Marker дообучена таким образом, чтобы позволить модели сегментатора макета определять типы блоков.

def load_layout_model():
    model = LayoutLMv3ForTokenClassification.from_pretrained(
        settings.LAYOUT_MODEL_NAME,
        torch_dtype=settings.MODEL_DTYPE,
    ).to(settings.TORCH_DEVICE_MODEL)


    model.config.id2label = {
        0: "Caption",
        1: "Footnote",
        2: "Formula",
        3: "List-item",
        4: "Page-footer",
        5: "Page-header",
        6: "Picture",
        7: "Section-header",
        8: "Table",
        9: "Text",
        10: "Title"
    }


    model.config.label2id = {v: k for k, v in model.config.id2label.items()}
    return model

Набор данных, использованный для этого дообучения, был взят из открытого набора данных DocLayNet.

Вывод 3: При парсинге PDF-файлов огромное значение имеет число колонок на странице, т.к. от этого зависит порядок чтения документа. Алгоритм Marker также включает дообученую LayoutLMv3, которая представляет из себя модель детектора колонок. Эта модель определяет количество колонок на странице, а затем применяет метод средней точки,

def add_column_counts(doc, doc_blocks, model, batch_size):
    for i in range(0, len(doc_blocks), batch_size):
        batch = range(i, min(i + batch_size, len(doc_blocks)))
        rgb_images = []
        bboxes = []
        words = []
        for pnum in batch:
            page = doc[pnum]
            rgb_image, page_bboxes, page_words = get_inference_data(page, doc_blocks[pnum])
            rgb_images.append(rgb_image)
            bboxes.append(page_bboxes)
            words.append(page_words)


        predictions = batch_inference(rgb_images, bboxes, words, model)
        for pnum, prediction in zip(batch, predictions):
            doc_blocks[pnum].column_count = prediction




def order_blocks(doc, doc_blocks: List[Page], model, batch_size=settings.ORDERER_BATCH_SIZE):
    add_column_counts(doc, doc_blocks, model, batch_size)


    for page_blocks in doc_blocks:
        if page_blocks.column_count > 1:
            # Пересортировка блоков в зависимости от их позиции
            split_pos = page_blocks.x_start + page_blocks.width / 2
            left_blocks = []
            right_blocks = []
            for block in page_blocks.blocks:
                if block.x_start <= split_pos:
                    left_blocks.append(block)
                else:
                    right_blocks.append(block)
            page_blocks.blocks = left_blocks + right_blocks
    return doc_blocks

аналогично моему подходу в статье Advanced RAG 02: Unveiling PDF Parsing.

Вывод 4: Специализированные модели можно обучить обрабатывать математические формулы. Например, Texify, модель от Marker, использует архитектуру Donut. Она была обучена на модели Donut с использованием изображений из LaTex и соответствующих уравнений, взятых из интернета (включая набор данных im2latex). Обучение проводилось на 4-x A6000 в течение примерно двух дней, что соответствует примерно 6 эпохам.

Вывод 5: Модель также можно использовать для постобработки. Основная идея заключается в том, чтобы обучить модель T5 брать почти готовый текст и дорабатывать его, удаляя артефакты, добавляя пробелы и вставляя новые строки.

def load_editing_model():
    if not settings.ENABLE_EDITOR_MODEL:
        return None


    model = T5ForTokenClassification.from_pretrained(
            settings.EDITOR_MODEL_NAME,
            torch_dtype=settings.MODEL_DTYPE,
        ).to(settings.TORCH_DEVICE_MODEL)
    model.eval()


    model.config.label2id = {
        "equal": 0,
        "delete": 1,
        "newline-1": 2,
        "space-1": 3,
    }
    model.config.id2label = {v: k for k, v in   model.config.label2id.items()}
    return model

На сегодняшний это вся информация об обучении постпроцессора и конструировании набора данных, которую мне удалось найти.

Недостатки Marker

Естественно, у Marker есть свои недостатки:

  • Вместо обучения и тонкой настройки специализированной модели для анализа макета, здесь используется встроенная функция из PyMuPDF. Эффективность такого подхода вызывает сомнения.

  • Marker не всегда удается распознать таблицы, а также их названия, что уступает в эффективности, например, Nougat (решение на основе небольшой модели без OCR, которое будет подробно представлено в следующей статье). Например, на рисунке 3 представлены результаты распознавания таблицы 3 из статьи "Attention Is All You Need". Слева показана исходная таблица, в середине — результаты с использованием Marker, а справа — результаты Nougat.

Рисунок 3: Сравнение обнаружения и распознавания таблиц, исходная таблица — таблица 3 из статьи "Attention Is All You Need". Изображение автора.
Рисунок 3: Сравнение обнаружения и распознавания таблиц, исходная таблица — таблица 3 из статьи "Attention Is All You Need". Изображение автора.
  • Поддерживаются только языки, похожие на английский. Распарсить PDF-файл на таких языках, как японский и хинди, не получится.

PaperMage

Papermage — это фреймворк с открытым исходным кодом для анализа и обработки визуально насыщенных, структурированных научных документов. Он предоставляет четкие и интуитивно понятные абстракции для представления и манипулирования текстовыми и визуальными элементами в документе.

Papermage объединяет различные модели обработки естественного языка (NLP) и компьютерного зрения (CV) в единый фреймворк. Он предлагает готовые к использованию решения для распространенных сценариев обработки научных документов.

Далее мы расскажем о принципах работы PaperMage и обсудим общий процесс с примерами исходного кода. Затем мы поговорим о выводах, которые можно сделать при рассмотрении PaperMage.

Компоненты

В Papermage можно выделить три основных компонента:

  • Magelib: Библиотека, содержащая примитивы и методы для представления и манипулирования визуально насыщенными документами в виде мультимодальных структур.

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

  • Рецепты: Фреймворк предлагает хорошо протестированные комбинации отдельных модулей, часто одномодальных, образующих сложные и расширяемые мультимодальные конвейеры, если можно так выразиться, “под ключ”. Эти комбинации называются рецептами (Recipes).

Базовые классы данных

Magelib предоставляет три базовых класса данных для представления основных элементов визуально насыщенных структурированных документов: Document, Layers (слои) и Entities (сущности).

Документ и слои

На рисунке 4 показано, как PaperMage создает и представляет документы.

Рисунок 4: Как PaperMage создает и представляет документы. Источник: PaperMage.
Рисунок 4: Как PaperMage создает и представляет документы. Источник: PaperMage.

После того, как с помощью различных алгоритмов и моделей извлечена структура документа, PaperMage концептуализирует ее как слой аннотаций, используемый для хранения как текстовой, так и визуальной информации.

Чуть позже мы посмотрим на исходный код и проанализируем процесс выполнения функции recipe.run().

Сущности

Как показано на рисунке 5, сущность представляет собой единицу мультимодального контента.

Рисунок 5: Сущность PaperMage. Источник: PaperMage.
Рисунок 5: Сущность PaperMage. Источник: PaperMage.

Но что нам делать с прерывистыми элементами в документе, такими как, например, предложения, которые охватывают целые колонки или даже страницы, или, предположим, обрываются плавающими графиками или сносками?

PaperMage's использует две переменные-члена: спаны (spans) и боксы (boxes). Как показано на рисунке 5, спаны определяют текст предложения среди всех символов, а боксы отражают его визуальные координаты на странице. Такой подход обеспечивает большую гибкость, позволяя учитывать даже незначительные различия в макете.

Кроме того, мы имеем возможность обращаться к сущностям различными способами, как показано на рисунке 6.

Рисунок 6: Различные способы доступа к сущностям. Источник: PaperMage.
Рисунок 6: Различные способы доступа к сущностям. Источник: PaperMage.

Чтобы лучше понять работу Papermage, мы начнем с конкретного примера парсинга PDF-файла и по ходу дела будем углубляться в суть процесса.

Общий анализ процесса и кода

Тестовый код выглядит следующим образом:

from papermage.recipes import CoreRecipe

core_recipe = CoreRecipe()

doc = core_recipe.run("YOUR_PDF_PATH")

Первым делом  core_recipe = CoreRecipe() войдет в конструктор класса CoreRecipe, где произойдет инициализация связанных библиотек и моделей.

class CoreRecipe(Recipe):
    def __init__(
        self,
        ivila_predictor_path: str = "allenai/ivila-row-layoutlm-finetuned-s2vl-v2",
        bio_roberta_predictor_path: str = "allenai/vila-roberta-large-s2vl-internal",
        svm_word_predictor_path: str = "https://ai2-s2-research-public.s3.us-west-2.amazonaws.com/mmda/models/svm_word_predictor.tar.gz",
        dpi: int = 72,
    ):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.dpi = dpi


        self.logger.info("Instantiating recipe...")
        self.parser = PDFPlumberParser()
        self.rasterizer = PDF2ImageRasterizer()


        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")
        #     self.word_predictor = SVMWordPredictor.from_path(svm_word_predictor_path)


        self.publaynet_block_predictor = LPEffDetPubLayNetBlockPredictor.from_pretrained()
        self.ivila_predictor = IVILATokenClassificationPredictor.from_pretrained(ivila_predictor_path)
        self.sent_predictor = PysbdSentencePredictor()
        self.logger.info("Finished instantiating recipe")

Поскольку class Recipe является родительским классом  CoreRecipe, функция core_recipe.run() перейдет в Recipe::run().

class Recipe:
    @abstractmethod
    def run(self, input: Any) -> Document:
        if isinstance(input, Path):
            if input.suffix == ".pdf":
                return self.from_pdf(pdf=input)
            if input.suffix == ".json":
                return self.from_json(doc=input)


            raise NotImplementedError("Filetype not yet supported.")


        if isinstance(input, Document):
            return self.from_doc(doc=input)


        if isinstance(input, str):
            if os.path.exists(input):
                input = Path(input)
                return self.run(input=input)
            else:
                return self.from_str(text=input)


        raise NotImplementedError("Document input not yet supported.")

Затем он дойдет до class CoreRecipe:: from_pdf() и class CoreRecipe:: from_doc():

class CoreRecipe(Recipe):
    ...
    ...
    def from_pdf(self, pdf: Path) -> Document:
        self.logger.info("Parsing document...")
        doc = self.parser.parse(input_pdf_path=pdf)


        self.logger.info("Rasterizing document...")
        images = self.rasterizer.rasterize(input_pdf_path=pdf, dpi=self.dpi)
        doc.annotate_images(images=list(images))
        self.rasterizer.attach_images(images=images, doc=doc)
        return self.from_doc(doc=doc)


    def from_doc(self, doc: Document) -> Document:
        # self.logger.info("Predicting words...")
        # words = self.word_predictor.predict(doc=doc)
        # doc.annotate_layer(name=WordsFieldName, entities=words)


        self.logger.info("Predicting sentences...")
        sentences = self.sent_predictor.predict(doc=doc)
        doc.annotate_layer(name=SentencesFieldName, entities=sentences)


        self.logger.info("Predicting blocks...")
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            blocks = self.publaynet_block_predictor.predict(doc=doc)
        doc.annotate_layer(name=BlocksFieldName, entities=blocks)


        self.logger.info("Predicting figures and tables...")
        figures = []
        tables = []
        for block in blocks:
            if block.metadata.type == "Figure":
                figure = Entity(boxes=block.boxes)
                figures.append(figure)
            elif block.metadata.type == "Table":
                table = Entity(boxes=block.boxes)
                tables.append(table)
        doc.annotate_layer(name=FiguresFieldName, entities=figures)
        doc.annotate_layer(name=TablesFieldName, entities=tables)


        # self.logger.info("Predicting vila...")
        vila_entities = self.ivila_predictor.predict(doc=doc)
        doc.annotate_layer(name="vila_entities", entities=vila_entities)


        for entity in vila_entities:
            entity.boxes = [
                Box.create_enclosing_box(
                    [b for t in doc.intersect_by_span(entity, name=TokensFieldName) for b in t.boxes]
                )
            ]
            # entity.text = make_text(entity=entity, document=doc)
        preds = group_by(entities=vila_entities, metadata_field="label", metadata_values_map=VILA_LABELS_MAP)
        doc.annotate(*preds)
        return doc

Общий процесс показан на рисунке 7:

Рисунок 7: Общий процесс работы PaperMage; стрелки без меток обозначают операцию слияния, также известную как функция аннотирования в PaperMage. Изображение автора.
Рисунок 7: Общий процесс работы PaperMage; стрелки без меток обозначают операцию слияния, также известную как функция аннотирования в PaperMage. Изображение автора.

На рисунке 7 мы можем увидеть, что процесс обработки PaperMage также представляет из себя конвейерный подход.

Первоначально выполняется анализ макета с помощью библиотеки PDFPlumber. Затем подключаются профессиональные алгоритмы и модели для анализа других объектов на странице, основываясь на результатах анализа макета. Сюда входят предложения, рисунки, таблицы, заголовки и так далее.

Далее мы сосредоточим наше внимание на трех важных процессах:

  • Разбиение на предложения

  • Анализ структуры макета

  • Анализ логической структуры.

Разбиение на предложения

Для разделения предложений используется PySBD — пакет Python для определения границ предложений на основе системы правил.

На вход подается последовательность лексем. На выходе мы получаем спан каждого предложения.

[
Unannotated Entity: {'spans': [[0, 212]]}, 
Unannotated Entity: {'spans': [[212, 367]]},  
…
]

Анализ структуры макета

Для анализа структуры макета страницы используется модель LPEffDetPubLayNetBlockPredictor. Это мощная модель обнаружения объектов на основе глубокого обучения, предоставляемая LayoutParser. Ее основная задача — сегментировать документ на области визуальных блоков.

На вход подается изображение страницы, обозначаемое как doc.images. На выходе мы получаем объект box и соответствующий  тип для каждого блока. Бокс включает в себя координату X левой верхней вершины, координату Y левой верхней вершины, ширину, высоту и номер страницы.

[
Unannotated Entity: {'boxes': [[0.5179840190298606, 0.752760137345049, 0.3682081491355128, 0.15176369855069774, 0]], 'metadata': {'type': 'Text'}}, 
Unannotated Entity: {'boxes': [[0.5145780320135539, 0.5080924136055337, 0.3675624668198144, 0.23725746136663078, 0]], 'metadata': {'type': 'Text'}}, 
…
]

Анализ логической структуры

Для анализа логической структуры документа используется модель IVILATokenClassificationPredictor. Она разделяет документ на такие организационные единицы, как заголовок, аннотация, основная часть, сноски, подписи и т.д.

В качестве исходных данных используются данные на уровне страницы, передаваемые в виде словаря.

{
        'words': ['word1', 'word2', ...],
        'bbox': [[x1, y1, x2, y2], [x1, y1, x2, y2], ...],
        'block_ids': [0, 0, 0, 1 ...],
        'line_ids': [0, 1, 1, 2 ...],
        'labels': [0, 0, 0, 1 ...], # could be empty
    }

Выходные данные — спан каждой сущности.

[
Unannotated Entity: {'spans': [[0, 80]], 'metadata': {'label': 'Title'}}, 
Unannotated Entity: {'spans': [[81, 157]], 'metadata': {'label': 'Author'}}, 
Unannotated Entity: {'spans': [[158, 215]], 'metadata': {'label': 'Paragraph'}}, 
...
]

Размышления и выводы о PaperMage

Абстракция парсинга PDF 

Абстракция, предложенная PaperMage для задачи парсинга PDF, является достаточно эффективной. Она предполагает разделение всего PDF на такие типы, как doc, layer и entities, что облегчает классификацию элементов и управление ими.

Масштабируемость

PaperMage разработала фреймворк, который легко расширяется, что упрощает последующую разработку.

Например, чтобы добавить пользовательский предиктор, нам достаточно наследоваться от базового класса BasePredictor и переопределить функцию _predict().

from .base_predictor import BasePredictor


class YOUR_NEW_Predictor(BasePredictor):
    ...
    ...
    def _predict(self, doc: Document) -> List[YOUR_RET_TYPE]:
    ...
    ...

Параллелизм

Рисунок 7 показывает, что у PaperMage есть потенциал для улучшения за счет распараллеливания, что является вполне целесообразным направлением для оптимизации.

Хотя текущая версия PaperMage не содержит кода, связанного с параллелизмом, добавление логики параллельной обработки может значительно повысить эффективность парсинга PDF-файлов.

Unstructured

Unstructured — это опенсорсный инструмент предварительной обработки неструктурированных данных. В предыдущей статье мы уже описали в общих чертах процесс его работы.

Теперь же мы поговорим о выводах, которые были сделаны при рассмотрении фреймворка unstructured, в частности о том, как он может помочь нам в разработке собственного инструмента для парсинга PDF.

Об анализе макета

Анализ макета в unstructured производится очень скрупулезно.

Если мы зададим strategy='hi_res', то для анализа макета будут использоваться такие модели, как YOLOX или detectron2. Для улучшения обнаружения они сочетаются с инструментом PDFMiner. Результаты обоих методов объединяются для получения окончательного макета, как показано на рисунке 8.

Рисунок 8: Процесс парсинга PDF со стратегией='hi_res' в unstructured. Изображение автора.
Рисунок 8: Процесс парсинга PDF со стратегией='hi_res' в unstructured. Изображение автора.

На рисунках 9 и 10 показаны визуализации результатов анализа макета 16-й страницы документа BERT; рамки на рисунке представляют границы каждой области. Результаты модели обнаружения объектов, показанные на рисунке 9, являются более точными. Таблицы и изображения в данном случае лучше интегрированы в структуру документа. Результаты обнаружения PDFMiner, показанные на рисунке 10, наоборот разделяют содержимое таблиц и изображений.

Рисунок 9: Визуализация результатов модели обнаружения объектов (inferred_layout) для 16-й страницы документа BERT, рамки представляют собой границы каждой области. Скриншот автора.
Рисунок 9: Визуализация результатов модели обнаружения объектов (inferred_layout) для 16-й страницы документа BERT, рамки представляют собой границы каждой области. Скриншот автора.
Рисунок 10: Визуализация результатов обнаружения PDFMiner (extracted_layout) для 16-й страницы документа BERT, рамки представляют собой границы каждой области. Скриншот автора.
Рисунок 10: Визуализация результатов обнаружения PDFMiner (extracted_layout) для 16-й страницы документа BERT, рамки представляют собой границы каждой области. Скриншот автора.

Код, отвечающий за слияния макетов, выглядит следующим образом: он содержит двойной цикл, который оценивает связь между каждой областью обнаруженной с помощью PDFMiner (extracted_layout) и результатом, полученным от модели обнаружения объектов (inferred_layout), а затем решает, нужно ли их объединять.

def merge_inferred_layout_with_extracted_layout(
    inferred_layout: Collection[LayoutElement],
    extracted_layout: Collection[TextRegion],
    page_image_size: tuple,
    same_region_threshold: float = inference_config.LAYOUT_SAME_REGION_THRESHOLD,
    subregion_threshold: float = inference_config.LAYOUT_SUBREGION_THRESHOLD,
) -> List[LayoutElement]:
    """Merge two layouts to produce a single layout."""
    extracted_elements_to_add: List[TextRegion] = []
    inferred_regions_to_remove = []
    w, h = page_image_size
    full_page_region = Rectangle(0, 0, w, h)
    for extracted_region in extracted_layout:
        extracted_is_image = isinstance(extracted_region, ImageTextRegion)
        if extracted_is_image:
            # Для наших целей мы пропустим извлеченные изображения, у нас нет текста на них, и с ними
            # обычно трудно получить хорошие ограничительные рамки для текста.


            is_full_page_image = region_bounding_boxes_are_almost_the_same(
                extracted_region.bbox,
                full_page_region,
                FULL_PAGE_REGION_THRESHOLD,
            )


            if is_full_page_image:
                continue
        region_matched = False
        for inferred_region in inferred_layout:
            if inferred_region.source in CHIPPER_VERSIONS:
                continue
            ...
            ...

О кастомизации

Unstructured предоставляет множество промежуточных результатов, которые можно легко кастомизировать.

В предыдущей статье мы рассмотрели три проблемы, связанные с данными, получемыми от unstructed:

  • Парсинг таблиц

  • Перестановка обнаруженных блоков, особенно в PDF-файлах с двумя колонками

  • Извлечение многоуровневых заголовков

Последние две проблемы можно решить, изменив промежуточную структуру. В качестве примера на рисунке 11 показан окончательный макет второй страницы документа BERT.

Рисунок 11: Визуализация окончательного макета второй страницы документа BERT. Скриншот автора.
Рисунок 11: Визуализация окончательного макета второй страницы документа BERT. Скриншот автора.

В то же время мы можем легко получить доступные результаты анализа макета:

[


LayoutElement(bbox=Rectangle(x1=851.1539916992188, y1=181.15073777777613, x2=1467.844970703125, y2=587.8204599999975), text='These approaches have been generalized to coarser granularities, such as sentence embed- dings (Kiros et al., 2015; Logeswaran and Lee, 2018) or paragraph embeddings (Le and Mikolov, 2014). To train sentence representations, prior work has used objectives to rank candidate next sentences (Jernite et al., 2017; Logeswaran and Lee, 2018), left-to-right generation of next sen- tence words given a representation of the previous sentence (Kiros et al., 2015), or denoising auto- encoder derived objectives (Hill et al., 2016). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9519357085227966, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=196.5296173095703, y1=181.1507377777777, x2=815.468994140625, y2=512.548237777777), text='word based only on its context. Unlike left-to- right language model pre-training, the MLM ob- jective enables the representation to fuse the left and the right context, which allows us to pre- In addi- train a deep bidirectional Transformer. tion to the masked language model, we also use a “next sentence prediction” task that jointly pre- trains text-pair representations. The contributions of our paper are as follows: ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9517233967781067, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=200.22352600097656, y1=539.1451822222216, x2=825.0242919921875, y2=870.542682222221), text='• We demonstrate the importance of bidirectional pre-training for language representations. Un- like Radford et al. (2018), which uses unidirec- tional language models for pre-training, BERT uses masked language models to enable pre- trained deep bidirectional representations. This is also in contrast to Peters et al. (2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9414362907409668, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=851.8727416992188, y1=599.8257377777753, x2=1468.0499267578125, y2=1420.4982377777742), text='ELMo and its predecessor (Peters et al., 2017, 2018a) generalize traditional word embedding re- search along a different dimension. They extract context-sensitive features from a left-to-right and a right-to-left language model. The contextual rep- resentation of each token is the concatenation of the left-to-right and right-to-left representations. When integrating contextual word embeddings with existing task-specific architectures, ELMo advances the state of the art for several major NLP benchmarks (Peters et al., 2018a) including ques- tion answering (Rajpurkar et al., 2016), sentiment analysis (Socher et al., 2013), and named entity recognition (Tjong Kim Sang and De Meulder, 2003). Melamud et al. (2016) proposed learning contextual representations through a task to pre- dict a single word from both left and right context using LSTMs. Similar to ELMo, their model is feature-based and not deeply bidirectional. Fedus et al. (2018) shows that the cloze task can be used to improve the robustness of text generation mod- els. ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.938507616519928, image_path=None, parent=None), 




LayoutElement(bbox=Rectangle(x1=199.3734130859375, y1=900.5257377777765, x2=824.69873046875, y2=1156.648237777776), text='• We show that pre-trained representations reduce the need for many heavily-engineered task- specific architectures. BERT is the first fine- tuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outper- forming many task-specific architectures. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9461237788200378, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=195.5695343017578, y1=1185.526123046875, x2=815.9393920898438, y2=1330.3272705078125), text='• BERT advances the state of the art for eleven NLP tasks. The code and pre-trained mod- els are available at https://github.com/ google-research/bert. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9213815927505493, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=195.33956909179688, y1=1360.7886962890625, x2=447.47264000000007, y2=1397.038330078125), text='2 Related Work ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8663332462310791, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=197.7477264404297, y1=1419.3353271484375, x2=817.3308715820312, y2=1527.54443359375), text='There is a long history of pre-training general lan- guage representations, and we briefly review the most widely-used approaches in this section. ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.928022563457489, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=851.0028686523438, y1=1468.341394166663, x2=1420.4693603515625, y2=1498.6444497222187), text='2.2 Unsupervised Fine-tuning Approaches ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8346447348594666, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=853.5444444444446, y1=1526.3701822222185, x2=1470.989990234375, y2=1669.5843488888852), text='As with the feature-based approaches, the first works in this direction only pre-trained word em- (Col- bedding parameters from unlabeled text lobert and Weston, 2008). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9344717860221863, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=200.00000000000009, y1=1556.2037353515625, x2=799.1743774414062, y2=1588.031982421875), text='2.1 Unsupervised Feature-based Approaches ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8317819237709045, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=198.64227294921875, y1=1606.3146266666645, x2=815.2886352539062, y2=2125.895459999998), text='Learning widely applicable representations of words has been an active area of research for decades, including non-neural (Brown et al., 1992; Ando and Zhang, 2005; Blitzer et al., 2006) and neural (Mikolov et al., 2013; Pennington et al., 2014) methods. Pre-trained word embeddings are an integral part of modern NLP systems, of- fering significant improvements over embeddings learned from scratch (Turian et al., 2010). To pre- train word embedding vectors, left-to-right lan- guage modeling objectives have been used (Mnih and Hinton, 2009), as well as objectives to dis- criminate correct from incorrect words in left and right context (Mikolov et al., 2013). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9450697302818298, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=853.4905395507812, y1=1681.5868488888855, x2=1467.8729248046875, y2=2125.8954599999965), text='More recently, sentence or document encoders which produce contextual token representations have been pre-trained from unlabeled text and fine-tuned for a supervised downstream task (Dai and Le, 2015; Howard and Ruder, 2018; Radford et al., 2018). The advantage of these approaches is that few parameters need to be learned from scratch. At least partly due to this advantage, OpenAI GPT (Radford et al., 2018) achieved pre- viously state-of-the-art results on many sentence- level tasks from the GLUE benchmark (Wang language model- Left-to-right et al., 2018a). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9476840496063232, image_path=None, parent=None)


]

Используя приведенную выше информацию, мы можем легко выполнять такие задачи, как сортировка и извлечение многоуровневых заголовков.

Поэтому, разрабатывая собственные инструменты для парсинга PDF, мы должны стремиться сохранить как можно больше полезной промежуточной информации и метаданных.

Об обнаружении и распознавании таблиц

Для обнаружения и распознавания таблиц во фреймворке unstructured используется Table Transformer.

Модель Table Transformer была предложена в статье PubTables-1M: Towards comprehensive table extraction from unstructured documents. В этой статье представлен новый набор данных PubTables-1M, предназначенный для извлечения таблиц из неструктурированных документов и проведения распознавания структуры и функционального анализа таблиц, как показано на рисунке 12.

Рисунок 12: Иллюстрация трех подзадач извлечения таблиц, рассматриваемых в наборе данных PubTables-1M. Источник: PubTables-1M: Towards comprehensive table extraction from unstructured document.
Рисунок 12: Иллюстрация трех подзадач извлечения таблиц, рассматриваемых в наборе данных PubTables-1M. Источник: PubTables-1M: Towards comprehensive table extraction from unstructured document.

Table Transformer обучен на наборе данных PubTables-1M, основанном на модели DETR, для решения таких задач, как обнаружение таблиц и распознавание их структуры.

Больше информации про обработку таблиц вы найдете в моей предыдущей статье.

Об обнаружении и распознавании формул

Во фреймворке unstructured отсутствует специальный модуль для обнаружения и распознавания формул, что в принципе заметно по посредственным результатам, показанным на рисунке 13.

Рисунок 13: Слева показан результат парсинга абзаца на 6-й странице статьи BERT, включая формулу, выделенную красной рамкой. Справа показан оригинал статьи. Скриншот автора.
Рисунок 13: Слева показан результат парсинга абзаца на 6-й странице статьи BERT, включая формулу, выделенную красной рамкой. Справа показан оригинал статьи. Скриншот автора.

Заключение

В этой статье представлен обзор конвейерного подхода к парсингу PDF-файлов. В ней рассматривается этот подход на примере трех фреймворков, использующих этот метод, дается его подробное представление и излагаются выводы, сделанные на его основе.

В итоге,

  • Хотя у Marker есть несколько недостатков, это легкий и быстрый инструмент.

  • Хотя PaperMage в первую очередь предназначен для работы с научными документами, его исключительная масштабируемость служит хорошей отправной точкой для дальнейшего развития.

  • Unstructured — это комплексный конвейерный фреймворк для парсинга PDF. Его преимущества заключаются в детальном анализе макета и широких возможностях кастомизации.

В целом, конвейерный подход к парсингу PDF-файлов является легко интерпретируемым и настраиваемым, что делает его широко используемым методом. Однако его эффективность во многом зависит от производительности каждой модели или алгоритма, используемого в процессе. Поэтому обучающие данные и структура каждой модели должны быть тщательно продуманы.


Материал подготовлен в рамках практического онлайн-курса "MLOps".

Комментарии (3)


  1. gudvinr
    14.08.2024 22:19
    +1

    Ссылка на предыдущую статью - это оригинал на английском. Если это цикл, то наверное надо на перевод указать, а если это одна статья, то зачем переводить не первую...


  1. OlegZH
    14.08.2024 22:19

    А что получается на выходе?


  1. RikkiMongoose
    14.08.2024 22:19

    Уже в первом предложении - грубая ошибка.

    Такое ощущение, что перевели гугл.транслейтом и даже не вычитали.