Donut и Pix2Struct на пользовательских данных

Donut и Pix2Struct — это модели преобразования изображения в текст, которые сочетают в себе простоту чисто пиксельного ввода с задачами понимания визуального языка. Проще говоря: вводится изображение, а извлеченные индексы выводятся в виде JSON.

Недавно я выпустил модель Пончика, доработанную по фактурам. Очень часто я получаю вопрос, как тренироваться с пользовательским набором данных. Также была выпущена аналогичная модель: Pix2Struct, она претендует на существенное улучшение. Но так ли это?

Пора закатать рукава. Я покажу тебе:

  • как подготовить данные для тонкой настройки Donut и Pix2Struct
  • процедура обучения для обеих моделей
  • сравнительные результаты на фактическом наборе данных

Конечно, я также предоставлю блокноты для совместной работы, чтобы вы могли легко экспериментировать и/или копировать их.

Набор данных

Для этого сравнения мне нужен общедоступный набор данных. Я хотел избежать обычных для задач понимания документов, таких как CORD, осмотрелся и нашел Ghega dataset. Он довольно небольшой (~250 документов) и состоит из 2-х типов документов: патентных заявок и спецификаций. С различными типами мы можем смоделировать проблему классификации. Для каждого типа у нас есть несколько индексов для извлечения. Эти индексы уникальны для данного типа. Именно то, что мне нужно. Профессор Медвет из лаборатории машинного обучения Университета Триеста любезно одобрил использование этих статей.

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

Первое исследование

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

ghega-dataset
    datasheets
        central-zener-1
        central-zener-2
        diodes-zener
            document-000-123542.blocks.csv
            document-000-123542.groundtruth.csv
            document-000-123542.in.000.png
            document-000-123542.out.000.png
            document-001-123663.blocks.csv
            document-001-123663.groundtruth.csv
            document-001-123663.in.000.png
            document-001-123663.out.000.png
            ...
        mcc-zener
        ...
    patents
        ...

Мы видим две основные подпапки для двух типов документов: datasheets и patents. На уровень ниже у нас есть подпапки, которые сами по себе не важны, но содержат файлы, начинающиеся с определенного префикса. Мы можем видеть уникальный идентификатор, например. документ-000–123542 . Для каждого из этих идентификаторов у нас есть 4 вида данных:

  • Файл blocks.csv содержит информацию об ограничивающих прямоугольниках. Поскольку Donut или Pix2Struct не используют эту информацию, мы можем игнорировать эти файлы.
  • Файл out.000.png представляет собой файл изображения, подвергнутого постобработке (устранению перекоса). Поскольку я предпочитаю тестировать необработанные файлы, я их тоже проигнорирую.
  • Необработанное изображение документа имеет суффикс in.000.png. Это то, что нас интересует.
  • И, наконец, соответствующий файл groundtruth.csv. Он содержит индексы для этого изображения, которые мы считаем исходными.

Вот пример CSV-файла с описанием столбца:

Case,-1,0.0,0.0,0.0,0.0,,0,1.28,2.78,0.79,0.10,MELF CASE
StorageTemperature,0,0.35,3.40,2.03,0.11,Operating and Storage Temperature,0,4.13,3.41,0.63,0.09,-65 to +200
 1. element type
 2. page of the label block (-1 if absent)
 3. x of the label block
 4. y of the label block
 5. w of the label block
 6. h of the label block
 7. text of the label block
 8. page of the value block (never absent!)
 9. x of the value block
10. y of the value block
11. w of the value block
12. h of the value block
13. text of the label block

Это означает, что нас интересуют только первый и последний столбцы. Первым является ключ, а последним — значение. В этом случае:

KEY                   VALUE
Case                  MELF CASE
StorageTemperature    -65 to +200

Это означает, что для этого документа мы настроим модели для поиска 'Case' со значением 'MELF CASE', а также для извлечения 'StorageTemperature», то есть «-65 до +200».

Индексы

В метаданных GroundTruth существуют следующие индексы:

  • технические данные: модель, тип, корпус, рассеиваемая мощность, температура хранения, напряжение, вес, тепловое сопротивление
  • патенты: название, заявитель, изобретатель, представитель, дата подачи, дата публикации, номер заявки, номер публикации, приоритет, классификация, 1-я строка реферата.

Глядя на качество достоверности и осуществимости, я решил сохранить следующие индексы:

elements_to_extract = ['FilingDate', 'RepresentiveFL', 'Classification', 'PublicationDate','ApplicationNumber','Model','Voltage','StorageTemperature']

Качество

Для преобразования изображения в текст использовался ocropus версии 0.2. Это означает, что он датируется примерно концом 2014 года. Это древний с точки зрения науки о данных, поэтому соответствует ли качество земной правды нашей задаче?

Для этого я просмотрел случайные изображения и сравнил правду с тем, что было написано в документе. Вот два примера, когда OCR был неправильным:

Ключ Классификация установлен как BGSD 81/00 в качестве основной истины. И это должно быть B65D 81/100.

Ключ StorageTemperature говорит, что I -65 {O + 150 соответствует действительности, в то время как мы видим, что должно быть от -65 до + 150.

Таких ошибок в наборе данных много. Один из подходов состоит в том, чтобы исправить их. Другой игнорировать. Поскольку я буду использовать одни и те же данные только для сравнения обеих моделей, я выбираю последнюю. Если данные будут использоваться для производства, вы можете выбрать первый вариант, чтобы получить наилучшие результаты.

(также обратите внимание, что эти специальные символы могут испортить формат JSON, я вернусь к этой теме позже)

Структура кольцевого набора данных

Как выглядит формат данных, в котором они нам нужны?
Для точной настройки модели «пончик» нам необходимо организовать данные в одну папку со всеми документами в виде отдельных файлов изображений и один файл метаданных, структурированный как файл строк JSON.

donut-dataset
    document-000-123542.in.000.png
    document-001-123663.in.000.png
    ...
    metadata.jsonl

Файл JSONL содержит для каждого файла изображения следующую строку:

{"file_name": "document-010-100333.in.000.png", "ground_truth": "{\"gt_parse\": { \"DocType\": \"patent\", \"FilingDate\": \"06.12.1999\", \"RepresentiveFL\": \"Manresa Val, Manuel\", \"Classification\": \"A47l. 5/28\", \"PublicationDate\": \"1139845\", \"ApplicationNumber\": \"99959528 .3\" } }"}

Давайте разберем эту строку JSON. На верхнем уровне у нас есть dict с двумя элементами: file_name и ground_truth. Под ключом ground_truth у нас есть словарь с ключом gt_parse. Значение само по себе является словарем с парами ключ-значение, которые мы знаем в документе. Или еще лучше: назначить. Помните, что тип документа не обязательно присутствует в документе в виде текста. Термин таблица данных отсутствует в тексте в этих документах.

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

Конверсия

Для самой конвертации я создал блокнот Jupyter в colab. Я решил создать разделение на поезд и набор проверки на этом этапе, а не непосредственно перед тонкой настройкой. Таким образом, для обеих моделей будут использоваться одни и те же проверочные изображения, и результаты будут лучше сопоставимы. Один из 5 документов будет использоваться для проверки.

Зная вышеизложенное о структуре набора данных Ghega, мы можем истолковать процедуру преобразования следующим образом:

Для каждого имени файла, оканчивающегося на .000.png, мы берем соответствующий файл наземной информации и создаем временный объект фрейма данных.
Остерегайтесь того, что наземная истина может быть пустой или не существовать полностью. (например, для datasheets/taiwan-switching)
Далее мы вычитаем класс из подпапки: patentили datasheet .
Теперь нам нужно построить строку JSON. Для каждого элемента/индекса, который мы хотим извлечь, мы проверяем, находится ли он в этом фрейме данных, и собираем его. Затем скопируйте само изображение.
Сделайте это для всех изображений, и в конце у нас будет файл JSONL для записи.

В питоне это выглядит так:

json_lines_train = ''
json_lines_val = ''

for dirpath, dirnames, filenames in os.walk('/content/ghega-dataset/'):
    for filename in filenames:
        if filename.endswith('in.000.png'):
          gt_filename = filename.replace('in.000.png','groundtruth.csv')
          gt_filename_path = os.path.join(dirpath, gt_filename)
          if not os.path.exists(gt_filename_path):    #ignore files in /ghega-dataset/datasheets/taiwan-switching/ because no groundtruth exists
            continue
          if os.path.getsize(gt_filename_path) == 0:  #ignore empty groundtruth files
            print(f'skipped {gt_filename_path} because no info in metadata')
            continue
          doc_df = pd.read_csv(gt_filename_path, header=None)
          #find the doctype, based on path
          if 'patent' in dirpath:
            type = 'patent'
          else:
            type = 'datasheet'
          #create json line
          #eg:
          #{"file_name": "document-034-127420.in.000.png", "ground_truth": "{\"gt_parse\": { \"DocType\": \"datasheet\", \"Model\": \"ZMM5221 B - ZMM5267B\", \"Voltage\": \"1.5\", \"StorageTemperature\": \"-65 to 175\" } }"}
          p2 = ''
          #add always first element: DocType
          p2 += '\\"' + 'DocType' + '\\": '
          p2 += '\\"' + type + '\\"'
          new_row = {'ImagePath': os.path.join(dirpath, filename), 'DocType' :type}
          ghega_df = pd.concat([ghega_df, pd.DataFrame([new_row])], ignore_index=True)
          #fill other elements if available
          for element in elements_to_extract:
            value = doc_df[doc_df[0] == element][12].tolist()
            if len(value) > 0:
              p2 += ', '
              p2 += '\\"' + element + '\\": '
              value = re.sub(r'[^A-Za-z0-9 ,.()/-]+', '', value[0])   #get rid of \ of ” and " in json
              p2 += '\\"' + value + '\\"'
              new_row = {'ImagePath': os.path.join(dirpath, filename), element :value}
              ghega_df = pd.concat([ghega_df, pd.DataFrame([new_row])], ignore_index=True)

          p3 = ' } }"}'

          json_line = p1 + p2 + p3
          print(json_line)

          #take ~20% to validation
          #copy image file and append json line
          if random.randint(1, 100) < 20:
            output_path = '/content/dataset/validation/'
            json_lines_val += json_line + '\r\n'
            shutil.copy(os.path.join(dirpath, filename), '/content/dataset/validation/')  
          else:
            output_path = '/content/dataset/train/'
            json_lines_train += json_line + '\r\n'
            shutil.copy(os.path.join(dirpath, filename), '/content/dataset/train/')  
       
#write jsonl files
text_file = open('/content/dataset/train/metadata.jsonl', "w")
text_file.write(json_lines_train)
text_file.close()
text_file = open('/content/dataset/validation/metadata.jsonl', "w")
text_file.write(json_lines_val)
text_file.close()

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

Икота

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

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

train_dataset = DonutDataset("/content/dataset", max_length=max_length,
                             split="train", task_start_token="<s_cord-v2>", prompt_end_token="<s_cord-v2>",
                             sort_json_key=False, # dataset is preprocessed, so no need for this
                             )

И получил эту ошибку:

---------------------------------------------------------------------------
ArrowInvalid                              Traceback (most recent call last)
<ipython-input-13-7726ec2b0341> in <cell line: 7>()
      5 processor.feature_extractor.do_align_long_axis = False
      6 
----> 7 train_dataset = DonutDataset("/content/dataset", max_length=max_length,
      8                              split="train", task_start_token="<s_cord-v2>", prompt_end_token="<s_cord-v2>",
      9                              sort_json_key=False, # cord dataset is preprocessed, so no need for this

ArrowInvalid: JSON parse error: Missing a comma or '}' after an object member. in row 7

Итак, похоже, проблема с форматом JSON в строке 7. Я скопировал эту строку и вставил в онлайн-валидатор JSON:

Однако он говорит, что это действительная строка JSON. Итак, давайте посмотрим глубже:

{
   "file_name":"document-012-108498.in.000.png",
   "ground_truth":"{\"gt_parse\": {\"DocType\": \"patent\"\"FilingDate\": \"15. Januar 2004 (15.01.2004)\",\"Classification\": \"BOZC 18/08,\",\"PublicationDate\": \"5. August 2004 (05.08.2004)\",\"ApplicationNumber\": \"PCT/AT2004/000006\"} }"
}

Вы заметили ошибку? Через некоторое время я заметил, что между DocType и FilingDate отсутствует запятая. Однако он отсутствовал во всех строках, поэтому мне непонятно, почему в строке 7 есть проблема. Когда я исправил эту проблему, я попробовал еще раз, и теперь он утверждает, что проблема в строке 17:

ArrowInvalid: JSON parse error: Missing a comma or '}' after an object member. in row 17

Вот строка 17, вы заметили проблему?

{"file_name": "document-007-103668.in.000.png", "ground_truth": "{\"gt_parse\": {\"DocType\": \"patent\",\"FilingDate\": \"18.12.2008\",\"RepresentiveFL\": \"Schubert, Siegmar\",\"Classification\": \"A47J 31/42 (2""6·"')\",\"PublicationDate\": \"12.08.2009\",\"ApplicationNumber\": \"08021980.1\"} }"}

Это неэкранированные кавычки для элемента Classification . Чтобы исправить это, я решил, что все значения могут содержать только буквенно-цифровые и несколько специальных символов с этим регулярным выражением:

[^A-Za-z0-9 ,.()/-]+

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

Подготовка данных: выполнена

Часто упускают из виду и, безусловно, недооценивают важность подготовки данных перед тренировкой. С помощью описанных выше шагов я показал вам, как вы можете адаптировать свои собственные данные для использования как Donut, так и Pix2Struct для извлечения ключевого индекса в документах. Также были устранены распространенные подводные камни. Блокнот Jupyter со всеми шагами можно найти здесь. Мы на полпути. Следующим шагом будет обучение обеих моделей на этом наборе данных. Мне очень любопытно, насколько хорошо они поживают, но сравнение и обучение будут для следующей статьи.

Вам также может понравиться:



Использованная литература: