Чтобы обучать нейросети понимать и генерировать человеческие языки, нужно много качественных текстов на нужных языках. «Много» – не проблема в эпоху интернета, но с качеством бывают сложности. В этом посте я предлагаю использовать BERT-подобные модели для двух задач улучшения качества обучающих текстов: исправление ошибок распознавания текста из сканов и фильтрация параллельного корпуса предложений. Я испробовал их на башкирском, но и для других языков эти рецепты могут оказаться полезны.
Введение
Хороший автоматический перевод обычно делается на нейросетях. Для их обучения этой задаче нужны большие и качественные "параллельные корпусы", то есть наборы пар предложений на двух языках, совпадающих по смыслу. Требование "большие" для многих языков в 2023 уже удовлетворено (например, Meta AI в проекте NLLB по переводу между 200 языками выпустила и намайненные по интернету параллельные корпусы для них). Но требование "качественные" до сих пор удовлетворено плохо: даже в тех же корпусах Меты для некоторых пар языков до половины пар предложений – на самом деле не эквивалентные. Например: в башкирско-русском корпусе там попадаются такие пары предложений как "Ссылка на сайт производителя - BASBUG" (это было неверно определено как башкирский) и "Не делайте из информационно-специализированного сайта, базар.", у которых мало общего. Поэтому, если хочется создать качественный машинный переводчик, корпус нужно чистить от таких плохих пар. А поскольку корпус нужен большой, хочется уметь делать это автоматически – например, обучив для этого BERT-подобные модели. Я попробовал сделать это для башкирского, но методологию можно использовать и для других языков тоже.
Другая проблема, возникающая при подготовке обучающих корпусов – мелкие ошибки в результате неверного распознавания текста. Например, "Э Ҡаратау ҙы белмәйем" вместо "Ә Ҡаратауҙы белмәйем". Самая универсальная стратегия для исправления таких ошибок – обучение sequence-to-sequence моделей, принимающих на вход потенциально ошибочный текст, и заново генерирующих текст исправленный. Но работают такие модели не очень быстро (ибо генерируют текст по токену за раз), плюс иногда они пропускают куски входного текста или, наоб��рот, «галлюцинируют» отсебятину. Поэтому имеет смысл пробовать и более простую архитектуру моделей, с одним только энкодером, который не переписывает текст с нуля, а предлагает точечные правки в исходный. С такого рода эксперимента я и начал.
Исправление «опечаток»
Строго говоря, исправлять я пытался не опечатки, а ошибки распознавания символов (OCR). Но предлагаемая модель будет работать и для других точечных исправлений – например, опечаток или ошибок распознавания речи (ASR). Задача модели – прочитать небольшой текст (одно предложение или около того) и предложить небольшие правки в его написании.
В принципе, можно пытаться обучать подобную модель в unsupervised режиме (только на корректных текстах), но для башкирского у меня уже был обучающий датасет в почти 24К предложений, распознанных из картинок и исправленных вручную. Этот же датасет я использовал, чтобы обучить мини-модель, добавляющую в текст относительно правдоподобные ошибки. Её я применил к непараллельному башкирскому корпусу, чтобы обогатить обучающую выборку.
Модель должна понимать слова с ошибками в правописании, поэтому, в отличие от типичных нейросетей для NLP, она должна токенизировать текст не в subword units (слово или несколько букв), а в отдельные буквы или что-то близкое. Я попробовал три варианта модели:
Seq2seq, а именно, ByT5 – модель, читающая и пишущая текст как последовательность байтов unicode.
Encoder-only модель: BERT, принимающий на вход последовательность отдельных букв, и выдающий её же на выходе – но со словарем, обогащенным избранными 2- и 3-граммами букв, чтобы уметь предсказывать вставки в текст. Обучается он на задаче sequence labelling (token classification), примерно как NER. Эту идею я подхватил из статьи GECTOR от Grammarly.
Ещё одна encoder-only модель: BERT со словарем из одних только букв, но со спецсимволом "▁", который я на входе вставляю между каждой парой букв, а на выходе – удаляю. Обучаю я такую модель с функцией потерь CTC, которая вообще-то предназначалась для распознавания речи, но, как выяснилось, годится и для редактирования текстов.
Модель ByT5 я инициализировал из маленькой g2p модели, первую BERTовую модель – из canine-c, а вторую обучал вообще с нуля. Для оценки качества всех этих моделей на тестовой выборке я использовал среднее расстояние Левенштейна до текстов, отредактированных человеком, а точнее, его сокращение по сравнению с исходными текстами. Код всех экспериментов находится в репозитории bashkort-spellchecker.
После обучения оказалось, что модель на ByT5 не особо взлетела: после её применения "в лоб" в текстах становилось даже больше ошибок, чем было. Повинен в этом оказалось небольшое число текстов, в которых модель вела себя неадекватно, удаляя или галлюцинируя большие куски. Чтобы это пофиксить, я предложил костыль: если модель изменила текст больше чем на 5 символов, вместо применения модели оставить текст без изменений. С таким костылём оказалось, что маленькая ByT5 способна исправить 39% опечаток.
Модель на основе sequence labelling оказалась получше: без каких-либо костылей она исправила 41% опечаток в тестовой выборке. Но, для справедливости, по размеру CANINE почти в 20 раз больше, чем маленький T5, который я использовал (но не в 20 раз медленнее, потому что генерирует она все токены не последовательно, а одновременно). Скачать обученную модель можно тут.
Самой лучшей, впрочем, оказалась третья модель, на основе BERT и CTC. Будучи самой маленькой (всего 11 мб), она также оказалась способной сократить 41% опечаток. Так что тем, кто захочет использовать мой подход, я рекомендую именно её. И обучение, и инференс там – очень простые (неприятный момент только один – пришлось написать собственный токенизатор, ибо BertTokenizer не очень хорошо подошёл). Более того, рискну предположить, что такая архитектура – крошечный BERT, обучающийся c CTC лоссом – подходит и для других задач, где в текст нужно вносить локальные правки, типа транслитерации или конвертации между графемами и фонемами.
Фильтрация параллельного корпуса
Параллельный корпус по идее должен состоять из пар предложений, похожих туда по смыслу, но иногда туда по разным причинам попадают и непохожие. Для оценки смысловой похожести предложений традиционно используется косинусная близость их эмбеддингов (про эмбеддинги предложений для русского языка я писал тут). Кроме этого, можно обучать специальные модели, кросс-энкодеры, непосредственно определять по сконкатенированной паре текстов степень их сходства (и по крайней мере для английского они работают очень хорошо). Я попробовал оба подхода.
Среди уже готовых энкодеров предложений, поддерживающих башкирский, я нашёл только LASER3 (недавно писал про него в канале). В дополнение к нему, я обучил свой небольшой BERT, выдающий LABSE-совместимые эмбеддинги для башкирского, и кросс-энкодер, сравнивающий русские и башкирские предложения. Код экспериментов выложен здесь.
С наличием размеченных данных мне снова повезло: Айгиз Кунафин уже давно организует сбор и ручную проверку параллельного корпуса (результат – вот), и в качестве отрицательных примеров для обучения и оценки моделей можно использовать пары предложений, забракованные его разметчиками. Этот корпус с положительными и отрицательными примерами, разбитый на обучающую и тестовую части, я выложил здесь. Для оценки качества моделей я буду использовать ROC AUC на тестовой выборке.
LASER (запускал тут) дал мне скромные 75% ROC AUC на тестовой выборке. В целом, лучше, чем ничего, но желание побить этот бейзлайн было.
В качестве собственного энкодера предложений, я обучил маленький BERT для башкирского (глубина 3 слоя, как в rubert-tiny, но ширина – 768, как в LABSE). Обучал на паре задач: MLM на неразмеченных данных, и воспроизведение эмбеддингов LaBSE – на параллельном корпусе. После суток обучения на моём ноутбуке косинусная близость эмбеддингов этой модели (для башкирского) с эмбеддингами LABSE (для русского) дала уже 80% ROC AUC на тестовой выборке.
Наконец, я поэкспериментировал с кросс-энкодерами. За основу взял ванильный bert-multilingual (он был предобучен в том числе и на башкирском), и принялся дообучать на задачу бинарной классификации. При дообучении на только размеченных людьми примерах ROC AUC вышел также 80%. На этом можно было бы и остановиться, но мною овладел научный интерес: а как я мог бы обучить подобный классификатор в отсутствие отрицательных примеров – только на обычном параллельном корпусе?
Самый простой подход к такому обучению – выбирать отрицательные примеры, просто совмещая случайно выбранные пары предложений из корпуса. Обучение на таких "easy negatives" дало мне ROC AUC 75% – как у бейзлайна на LASER. Кроме этого, я попробовал более сложные отрицательные примеры: брать положительную пару предложений и портить одно из них, путём удаления, перестановки, или замены случайно выбранных слов. Все эти вмешательства делают предложение неграмматичным, и чтобы модель видела и грамматичные отрицательные примеры тоже, я обучил две маленькие T5 модели заполнять пропуски в предложениях чуть более правдоподобным образом. В итоге хорошей правдоподобности не получилось, но так или иначе, модель, обученная с использованием всех этих искусственных отрицательных примеров, выбила уже 78% ROC AUC. Ну а при комбинации искусственных и размеченных людьми отрицательных примеров, моде��ь выбила все 82%. Её я и выложил.
Применил я эту комбинацию к четырём русско-башкирским корпусам: корпусу Айгиза, намайненному параллельному корпусу из статьи No Language Left Behind, WikiMatrix, и Wikimedia. Объединенный корпус с предсказаниями кросс-энкодера и косинусной близостью предложений я выложил сюда. На основе разметки небольшой выборки, я пришёл к такому правилу для фильтрации: ((cosine >= 0.6) & (cross_encoder >= 0.1)) | ((cross_encoder >= 0.5) & (cosine >= 0.4)). Из 3.7 миллионов исходных пар оно оставляет примерно половину: 1.9 миллиона. Но, конечно, проверить реальный эффект от такой фильтрации можно будет, только обучив, собственно, модель для машинного перевода. Что я, наверное, попробую в следующем выпуске (:
P.S. Ну и да, если решите попробовать мои рецепты для другого языка или другой задачи – не стесняйтесь писать мне, чтобы я подготовил их в более удобном виде.