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

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

Постановка задачи

Интерпретация языка по визуальным подсказкам — это не просто задача оптического распознавания символов; речь идет о понимании контекста, семантики и даже замаскированной информации. С точки зрения непрофессионала, я пытаюсь выяснить, насколько хорошо модели ИИ могут понимать текст из необработанных пикселей изображения, так же, как мы это делаем. С этой целью я использовал парадигму самоконтролируемого моделирования языка в масках (MLM).

Чтобы дать немного контекста: изначально я структурировал задачу без MLM-части. Целью было буквально восстановить текст по изображению, так же, как это делает OCR, но сквозное. Это сработало нормально, но модель не смогла понять текст.

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

На высоком уровне архитектура довольно проста:

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

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

Архитектура

Этот раздел немного более технический, поэтому смело пропускайте его, если вас интересуют только результаты!

Основа проекта опирается на гибридные сверточные нейронные сети (CNN) и модель трансформаторов. Давайте окунемся немного глубже в архитектуру!

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

  • ResNetFeatureExtractor: в нашей модели используется CNN на базе ResNet, слегка скорректированная для поставленной задачи. Этот модуль заботится о преобразовании необработанного изображения в плоский набор карт объектов. Каждая из этих карт объектов фиксирует сложные узоры, присутствующие на изображении, подготавливая его для дальнейшей обработки.
import torch.nn as nn
from torchvision.models import resnet50


class ResNetFeatureExtractor(nn.Module):

    def __init__(self, feature_map_size, out_features_size):
        super(ResNetFeatureExtractor, self).__init__()

        self.feature_extractor = nn.Sequential(
            *list(resnet50(pretrained=True).children())[:-2])

        self.input_proj = nn.Conv2d(feature_map_size,
                                    out_features_size,
                                    kernel_size=1)

    def forward(self, x):
        x = self.feature_extractor(x)
        x = self.input_proj(x)
        x = x.view(x.size(0), x.size(1), -1).permute(0, 2, 1)
        return x

SinePositionalEncoding: после модуля ResNet берет на себя управление SinePositionalEncoding. Этот слой имеет решающее значение, поскольку он наполняет наши карты объектов информацией о местоположении. В отличие от последовательностей в задачах чистого НЛП, изображения по своей сути не имеют последовательности, и это позиционное кодирование дает нашей модели пространственную информацию. Ниже приведен код, используемый для позиционного кодирования. Потенциальным улучшением может быть использование 2D-позиционного кодирования.

class SinePositionalEncoding(nn.Module):

    def __init__(self, d_model, max_len=1000):
        super(SinePositionalEncoding, self).__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer("pe", pe)

    def forward(self, x):
        return x + self.pe[:, :x.size(1)].requires_grad_(False)

Модель Transformer и VLP. Выбор преобразователя был обусловлен его универсальностью и масштабируемостью. Входной сигнал от CNN пересылался на кодер-трансформатор, который затем передавал выходные данные на декодер-трансформатор. Этот процесс кодирования-декодера позволил нам обрабатывать данные изображения токен за токеном, включая в некоторых случаях прогнозирование замаскированных токенов.

  • Конфигурация трансформатора: благодаря функции model_config_factory наша архитектура остается легко настраиваемой. Он позволяет легко указывать параметры модели, такие как model_dim, num_heads и другие. Это гарантирует, что я смогу быстро настроить и масштабировать нашу модель на основе требований наших данных и результатов предыдущих экспериментов. Я следую традиционной реализации трансформатора; полный код можно найти здесь.
    Ниже приведена конфигурация, используемая для наиболее производительной модели:
...,
"encoder_decoder_lg": {
  "model_dim": 768,
  "ff_dim": 4096,
  "num_heads": 16,
  "num_layers": 12,
  "feature_map_size": 2048, # from the resnet model
  "dec_div": 2
}, ...

Класс VLP: Это центральное место. Как только функции изображения извлекаются с помощью feature_extractor, эти функции передаются в наш преобразователь для обработки. Структура преобразователя может варьироваться в зависимости от выбора – кодер, декодер или их комбинация.

class VLP(nn.Module):

    def __init__(self, model_dim, num_layers, ff_dim, num_heads,
                 feature_map_size, vocab_size, dropout, transformer_type,
                 dec_div):
        super(VLP, self).__init__()

        self.feature_extractor = nn.Sequential(
            ResNetFeatureExtractor(feature_map_size=feature_map_size,
                                   out_features_size=model_dim),
            SinePositionalEncoding(model_dim))

        self.transformer = get_transformer(num_layers=num_layers,
                                           model_dim=model_dim,
                                           ff_dim=ff_dim,
                                           num_heads=num_heads,
                                           vocab_size=vocab_size,
                                           dropout=dropout,
                                           transformer_type=transformer_type,
                                           dec_div=dec_div)

    def get_image_features(self, images):
        return self.feature_extractor(images)

    def forward(self, images, tgt=None, tgt_mask=None):
        image_features = self.get_image_features(images)
        return self.transformer(image_features, tgt, tgt_mask=tgt_mask)
  • VLPForTextMLM: класс VLPForTextMLM, созданный на основе модели VLP, специально предназначен для нашей задачи модели языка в маске (MLM). Он имеет дополнительный линейный слой, который сопоставляет выходные данные преобразователя с желаемым количеством классов (токенов).
class VLPForTextMLM(nn.Module):

    def __init__(self,
                 model_dim,
                 num_layers,
                 num_heads,
                 ff_dim,
                 feature_map_size,
                 num_classes,
                 dec_div=2,
                 dropout=0.0):
        super(VLPForTextMLM, self).__init__()

        self.vlp = VLP(num_layers=num_layers,
                       model_dim=model_dim,
                       ff_dim=ff_dim,
                       num_heads=num_heads,
                       feature_map_size=feature_map_size,
                       vocab_size=None,
                       dropout=dropout,
                       transformer_type="encoder",
                       dec_div=dec_div)
        self.out = nn.Linear(model_dim, num_classes)

    def forward(self, images):
        out = self.vlp(images)
        return self.out(out)

Асимметрия по дизайну. Кодер и декодер не были симметричными по размеру. В результате преднамеренного выбора конструкции энкодер был оснащен значительно большим количеством параметров. Цель заключалась в том, чтобы кодировщик сохранял как можно больше лингвистических знаний. Этот выбор положительно отразился на результатах набора данных MNLI.

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

Данные и обучение

Наши данные обучения для задачи MLM состояли из подмножеств (около 30–50% — из-за аппаратных и временных ограничений) наборов данных Wikipedia и Bookcorpus, при этом тексты были отфильтрованы для поддержания длины менее 144 токенов. Большая часть моего времени ушла на создание набора данных — в этом направлении еще много улучшений. Модель была обучена примерно за 1,5 миллиона итераций с размером пакета 16. Вариант encoder_decoder_lg модели имеет 129 миллионов параметров.

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

Цикл обучения довольно прост:

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

Для обучения я использовал потери CrossEntropy, оптимизатор AdamW, политику скорости обучения косинусного отжига OneCycle и смешанную точность.

Результаты и наблюдения

Чтобы проверить качество изученных представлений, я обучил модель VLP нисходящей задаче текстового вывода MNLI. Учитывая предложение-посылку и предложение-гипотезу, задача состоит в том, чтобы предсказать, влечет ли посылка гипотезу (вытекание), противоречит ли гипотезе (противоречие) или не противоречит ни одной (нейтральна). Это задача классификации, поэтому уровень декодера удаляется.

Модель показала многообещающие результаты, получив оценку F1 0,73 в наборе данных MNLI. Однако оно не обошлось без проблем. Его сильная зависимость от уровня CNN для конвергенции и застой в результатах после увеличения размера CNN указывали на потенциальные узкие места.

Чтобы проанализировать качество изученных вложений, я выполнил линейное зондирование задачи классификации настроений imdb (положительные/отрицательные) в двух настройках:

  1. Не ограничивая количество токенов — в этом случае модель VLP достигла показателя F1 0,7 по сравнению с 0,8 у BERT. Это имело смысл, поскольку модель VLP не обучалась на текстах, содержащих более 144 токенов.
  2. Ограничение количества токенов до 144 — в этом случае VLP показал себя намного лучше, получив оценку F1 0,78 по сравнению с 0,82 у BERT.

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

Текущее состояние и будущие направления

Проект в его текущем состоянии хорошо работает для распознавания текста. Он хорошо понимает лингвистические конструкции, такие как употребление глаголов, пунктуация и ссылки. Однако он давал сбои в задачах, требующих запоминания, таких как The capital of France is [MASK] (не давал Пэрис). Проект готов к исследованию, и планируется обучение на таких наборах данных, как SQUAD и Ontonotes.

Некоторые из наиболее очевидных способов решения проблемы плато производительности могут быть следующими:

  1. расширение и улучшение наборов обучающих данных,
  2. пересмотр архитектуры и внедрение альтернативных методов.

Мне очень хотелось бы услышать мнение коллег-исследователей и энтузиастов, ваши идеи и вклад очень приветствуются! Пожалуйста, не стесняйтесь обращаться ко мне, если у вас есть какие-либо идеи или вопросы! И спасибо, что нашли время прочитать этот довольно длинный пост в блоге, надеюсь, вам было интересно! 🙏😁

Особая благодарность Игорю Тике и Николе Томичу за множество заметок и очень подробный обзор блога! 🙏

Электронная почта: [email protected]

LinkedIn: https://www.linkedin.com/in/filip-basara-84a694195/

Твиттер: https://twitter.com/basarafilip

Гитхаб: https://github.com/filipbasara0