Мы собираем чувашско-русский корпус и параллельно смотрим, что можно сделать с имеющимся объемом данных. В этом примере использовали корпус из 90 000 пар предложений. Самый хороший результат на данный момент дал метод передачи знания (англ. Transfer Learning), о нем и пойдет речь в статье. Цель статьи — дать практический пример реализации, который легко можно было бы воспроизвести.
План обучения таков. Нужно взять большой (родительский) корпус, обучить на нем нейронную модель, а затем дообучить нашу дочернюю модель. Причем целевой язык перевода будет один и тот же: русский. Интуитивно это можно сравнить с изучением второго языка. Его учить легче, зная один иностранный язык. Еще это похоже на изучение узкой области иностранного языка, например, медицинской терминологии английского языка: сначала нужно выучить английский в общем.
В качестве родительского корпуса пробовали брать 1 миллион пар предложений из англо-русского параллельного корпуса и 1 миллион из казахско-русского корпуса. В казахских данных 5 миллионов предложений. Из них взяли только те, у которых коэффициент соответствия (третья колонка) больше 2. Казахский вариант дал результаты чуть лучше. Интуитивно кажется, что это понятно, поскольку чувашский и казахский языки более похожи друг на дурга. Но на самом деле это не доказано, а также сильно зависит от качества корпуса. Более подробно про подбор родительского корпуса можно прочесть в этой статье. Про дочерний корпус из 90 000 пар предложений узнать и запросить пример данных можно тут.
Теперь к коду. Если нет своей быстрой видеокарты, можно тренировать модель на площадке Colab. Для обучения мы использовали библиотеку Sockeye. Предполагается, что уже установлен Python3.
pip install sockeye
Также возможно отдельно придется повозиться с MXNet , которая отвечает за работу с видеокартой. В Colab нужно дополнительно установить библиотеку
pip install mxnet-cu100mkl
Про нейронные сети принято считать, что им достаточно скормить данные как есть, а они сами разберутся. Но на самом деле это не всегда так. Вот и в нашем случае корпус нужно предобработать. Сначала его токенизируем, чтобы модели легче было понимать, что «кот!» и «кот» — это примерно про одно и тоже. Для примера подойдет просто питоновский токенайзер.
from nltk.tokenize import WordPunctTokenizer def tokenize(src_filename, new_filename): with open(src_filename, encoding="utf-8") as src_file: with open(new_filename, "w", encoding="utf-8") as new_file: for line in src_file: new_file.write("%s" % ' '.join(WordPunctTokenizer().tokenize(line))) new_file.write("\n")
В результате, подаем на вход пары предложений вида
Нумаях пулмасть вӗсене те укҫа тӳлеме пӑрахнӑ. Республикӑра пурӑнакансем хӑйсен хастарлӑхӗпе, ырӑ кӑмӑлӗпе, чунтан тухакан йӑл куллипе тӗлӗнмелле лару-тӑру йӗркелерӗҫ, ют ҫӗршыври пирӗн ӗҫтешсем палӑртнӑ тӑрӑх, ҫавнашкалли вӗсен патӗнче Раштав уявне паллӑ тунӑ вӑхӑтра кӑна пулать.
и
Недавно им тоже перестали платить деньги. Своим оптимизмом, добротой, приветливыми улыбками жители республики создали удивительную атмосферу, которая, по словам зарубежных партнеров, бывает у них только во время празднования Рождества.
На выходе получаются такие токенизированные предложения:
Нумаях пулмасть вӗсене те укҫа тӳлеме пӑрахнӑ . Республикӑра пурӑнакансем хӑйсен хастарлӑхӗпе , ырӑ кӑмӑлӗпе , чунтан тухакан йӑл куллипе тӗлӗнмелле лару - тӑру йӗркелерӗҫ , ют ҫӗршыври пирӗн ӗҫтешсем палӑртнӑ тӑрӑх , ҫавнашкалли вӗсен патӗнче Раштав уявне паллӑ тунӑ вӑхӑтра кӑна пулать .
и на русском
Недавно им тоже перестали платить деньги . Своим оптимизмом , добротой , приветливыми улыбками жители республики создали удивительную атмосферу , которая , по словам зарубежных партнеров , бывает у них только во время празднования Рождества .
В нашем случае понадобятся объединенные словари родительского и дочернего корпусов, поэтому создадим общие файлы:
cp kk.parent.train.tok kkchv.all.train.tok cat chv.child.train.tok >> kk.parent.train.tok cp ru.parent.train.tok ru.all.train.tok cat ru.child.train.tok >> ru.all.train.tok
так как дообучение дочерней модели происходит на том же словаре.
Теперь небольшое, но важное отступление. В МП предложения разбивают на атомы в виде слов и далее оперируют предложениями как последовательностями слов. Но этого как правило недостаточно, потому что образуется огромный хвост из слов, которые встречаются в корпусе по одному разу. Построить для них вероятностную модель сложно. Особенно это актуально для языков с развитой морфологией (падеж, род, число). И русский, и чувашский именно такие языки. Но есть решение. Можно разбить предложение на более низкий уровень, на подслова. Мы использовали Byte pair encoding.
git clone https://github.com/rsennrich/subword-nmt.git
Получим примерно такие последовательности подслов
Ну@@ маях пулмасть вӗсене те укҫа тӳ@@ леме пӑрахнӑ . Республи@@ кӑра пурӑнакансем хӑйсен хастар@@ лӑхӗпе , ырӑ кӑмӑ@@ лӗпе , чунтан тухакан йӑ@@ л кул@@ липе тӗлӗнмелле лару - тӑ@@ ру йӗркеле@@ рӗҫ , ют ҫӗршыв@@ ри пирӗн ӗҫ@@ те@@ шсем палӑртнӑ тӑрӑх , ҫав@@ наш@@ ка@@ лли вӗсен патӗнче Ра@@ шта@@ в уя@@ вне паллӑ тунӑ вӑхӑтра кӑна пулать .
и
Не@@ давно им тоже пере@@ стали пла@@ тить деньги . Сво@@ им о@@ пти@@ ми@@ з@@ мом , добро@@ той , привет@@ ли@@ выми улыб@@ ками жители республики соз@@ дали уди@@ витель@@ ную ат@@ мо@@ с@@ фер@@ у , которая , по сло@@ вам за@@ ру@@ бе@@ жных па@@ рт@@ не@@ ров , бывает у них только во время празд@@ но@@ вания Ро@@ ж@@ де@@ ства .
Видно, что из слов хорошо вычленяются аффиксы: Не@@ давно и добро@@ той.
Для этого нужно подготовить словари bpe
python subword-nmt/subword_nmt/learn_joint_bpe_and_vocab.py --input kkchv.all.train.tok ru.all.train.tok -s 10000 -o bpe.codes --write-vocabulary bpe.vocab.kkchv bpe.vocab.ru
И применить их к токенам, например:
python subword-nmt/subword_nmt/apply_bpe.py -c bpe.codes --vocabulary bpe.vocab.kkchv --vocabulary-threshold 50 < kkchv.all.train.tok > kkchv.all.train.bpe !python subword-nmt/subword_nmt/apply_bpe.py -c bpe.codes --vocabulary bpe.vocab.ru --vocabulary-threshold 50 < ru.all.train.tok > ru.all.train.bpe
По аналогии нужно сделать для всех файлов: обучающих, валидационных и тестовых родительской и дочерней моделей.
Теперь перейдем непосредственно к обучению нейронной модели. Сначала нужно подготовить общие модельные словари:
python -m sockeye.prepare_data -s kk.all.train.bpe -t ru.all.train.bpe -o kkru_all_data
Далее обучим родительскую модель. Более подробно простой пример описан на странице Sockeye. Технически процесс состоит из двух шагов: подготовки данных с использованием созданных ранее модельных словарей
python -m sockeye.prepare_data -s kk.parent.train.bpe -t ru.parent.train.bpe -o kkru_parent_data --source-vocab kkru_all_data/vocab.src.0.json --target-vocab kkru_all_data/vocab.trg.0.json
и самого обучения
python -m sockeye.train -d kkru_parent_data -vs kk.parent.dev.bpe -vt ru.parent.dev.bpe --encoder transformer --decoder transformer --transformer-model-size 512 --transformer-feed-forward-num-hidden 256 --transformer-dropout-prepost 0.1 --num-embed 512 --max-seq-len 100 --decode-and-evaluate 500 -o kkru_parent_model --num-layers 6 --disable-device-locking --batch-size 1024 --optimized-metric bleu --max-num-checkpoint-not-improved 10
Обучение на мощностях Colab занимает около суток. Когда тренировка модели завершена, переводить с ее помощью можно так
python -m sockeye.translate --input kk.parent.test.bpe -m kkru_parent_model --output ru.parent.test_kkru_parent.bpe
Для обучения дочерней модели выполним
python -m sockeye.prepare_data -s chv.child.train.bpe -t ru.child.train.bpe -o chvru_child_data --source-vocab kkru_all_data/vocab.src.0.json --target-vocab kkru_all_data/vocab.trg.0.json
Код запуска обучения выглядит так
python -m sockeye.train -d chvru_child_data -vs chv.child.dev.bpe -vt ru.child.dev.bpe --encoder transformer --decoder transformer --transformer-model-size 512 --transformer-feed-forward-num-hidden 256 --transformer-dropout-prepost 0.1 --num-embed 512 --max-seq-len 100 --decode-and-evaluate 500 -o ruchv_150K_skv_dev19_model --num-layers 6 --disable-device-locking --batch-size 1024 --optimized-metric bleu --max-num-checkpoint-not-improved 10 --config kkru_parent_model/args.yaml --params kkru_parent_model/params.best
Добавляются параметры, указывающие, что нужно использовать конфигурацию и веса родительской модели в качестве точки старта. Детали в примере с дообучением от Sockeye. Обучение дочерней модели сходится примерно за 12 часов.
Подводя итоги, сравним результаты. Обычная модель машинного перевода дала качество 24,96 BLEU, тогда как модель с передачей знания 32,38 BLEU. Разница видна и визуально на примерах переводов. Поэтому, пока продолжаем собирать корпус, будем пользоваться этой моделью.

