Чтобы обучать нейросети понимать и генерировать человеческие языки, нужно много качественных текстов на нужных языках. «Много» – не проблема в эпоху интернета, но с качеством бывают сложности. В этом посте я предлагаю использовать 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. Ну и да, если решите попробовать мои рецепты для другого языка или другой задачи – не стесняйтесь писать мне, чтобы я подготовил их в более удобном виде.  Разработчик / Аналитик / Data Scientist / NLPшник