Всем привет! Меня зовут Александра Сорока, я занимаюсь синтезом речи в Тинькофф. А это — мой текст о том, зачем вообще думать о долгосрочной поддержке кода и ML-моделей. Я расскажу, почему мы отказались от опенсорсных решений, как работаем с датасетами и разными версиями моделей и как замеряем их качество. Статья может оказаться полезной для всех, кто хочет знать, как ничего не поломать.
Стадии роста проекта и поддержка кода
Обычно у проекта есть выраженные стадии роста. Наблюдая за ними, можно понять, с какими проблемами сталкиваются команды.
Стадия первая: proof of concept. На этой стадии у бизнеса есть какая-то потребность. Команда разрабатывает решение, но использовать его пока рано. Стадия характеризуется быстрой эволюцией проекта, большим количеством пилотных версий и частыми замерами качества. Все усилия команды уходят на разработку и на то, чтобы доказать бизнесу полезность проекта. Разработку на этом этапе стараются делать быстро и дешево.
Стадия вторая: бизнес пользуется проектом. Итак, проект готов и его начинает использовать бизнес. С этого момента ситуация кардинально меняется. С одной стороны, бизнес хочет улучшения скорости и качества. С другой — воспринимает как должное, что функциональность не меняется и не ухудшается. При этом затраты времени и сил на поддержку проекта растут экспоненциально, а разрабатывать что-то новое некогда.
От 30% до 70% времени уходит на замеры качества, доказывающие, что функциональность не ухудшилась, тесты, гарантирующие, что при изменениях одних компонентов не ломаются другие, и рефакторинг накопившегося кода. При хорошем стечении обстоятельств проект обрастает кучей дашбордов, метрик, предрелизных процедур и тестов. Разработчик примерно половину времени пишет новый код, а половину — тестирует, замеряет качество или производительность и пишет новые тесты.
Стадия третья: проблемная (наступает не всегда). На этой стадии проект настолько тяжелый и запущенный, что разработчики тратят абсолютно все время на рефакторинг старого кода, поиск потерянных данных, на которых обучались ML-модели, поиск данных и кода для замеров качества и так далее. Без этого невозможно хоть что-то изменить в проекте, не сломав его и не испортив жизнь бизнесу, который им пользуется.
Все время разработчик тратит на попытки разобраться, что делали разработчики до него, зачем они это делали и как сделать так, чтобы все продолжало работать. Этой стадии хочется избежать, но удается это не всем.
Чем дольше проект существует и чем важнее он для бизнеса, тем больше приходится думать о его поддержке. Каждый разработчик знает, к чему приводит игнорирование проблемы. Я сама сталкивалась с такими ситуациями и слышала истории от коллег.
Был случай в одном стартапе, когда разработчики шли на поводу у инвесторов и добавляли новые функции, не думая о поддержке кода. Код стал настолько ужасным, что никто не хотел заниматься проектом — даже человек, который его начинал. Новые люди не задерживались в проекте и уходили, а бизнес продолжал требовать новые фичи.
В компании среднего размера один проект перешел с первой стадии на третью. Бизнес очень хотел новые фичи, а команда не настояла на том, чтобы выделить ресурс на поддержку. В итоге пришлось переписывать почти весь код. Бизнес от этого не выиграл.
В одной крупной компании был сильно запущен код, считающий фичи для ML-модели, с одной стороны, а с другой — изменилась Big Data, инфраструктура, на которой обсчитывались большие массивы сырых логов. У команды не получалось обновить результат работы ML-модели, которой пользовался бизнес. Код пришлось разгребать около года.
Таких примеров много у каждого разработчика, потому что поддерживать код дорого и не все компании к этому готовы. А поддержка ML-моделей — это отдельная история.
Отличия поддержки ML-моделей от поддержки кода
ML-модели, в отличие от любых рукописных эвристик, проще поддерживаются и поддаются обновлению: их достаточно переобучить на новых данных. Но поддержка ML-моделей, особенно если они работают в продакшене, требует опыта, внимания и большой осторожности. Работу один раз обученной модели трудно, а часто и невозможно, в точности воспроизвести — в отличие от эвристик.
Иногда случаются просчеты. Например, потеря данных, на которых обучалась модель, или версии кода, который обучал модель, или данных, на которых производились замеры качества, или кода, которым делались замеры качества. Это приводит к тому, что обновлять ML-модель, на чью работу завязаны другие сервисы, становится очень дорого. А иногда это невозможно сделать без ухудшения качества относительно прошлой версии модели. Цикл разработки ML-модели отличается от цикла разработки обычного кода примерно так:
Цикл разработки кода |
Цикл разработки ML-модели |
1. Написание кода. 2. Ревью. 3. Тестирование. 4. Правки и оптимизация. 5. Тестирование. 6. Код становится частью продакшена. |
1. Подготовка данных. 2. Написание кода обработки данных для получения датасета. 3. Написание кода первой модели. 4. Написание кода, считающего метрики качества для модели. 5. Сбор данных, на которых можно измерять качество. 6. Фиксация бейзлайновой метрики для первой модели. 7. Улучшение датасета, если есть необходимость. 8. Эксперименты с другими моделями/архитектурами нейросетей/etc. 9. Замеры качества экспериментов. 10. Оптимальная версия модели на лучшей версии данных — один из экспериментов — попадает в продакшен. |
В продакшен модель попадает после многочисленных экспериментов, исправлений в коде, добавлений новых данных и так далее.
Эта модель становится новым бейзлайном, который нужно поддерживать, обновлять и улучшать. Скорее всего, от нее теперь зависят компоненты и сервисы, отвечающие за автоматизацию какой-либо работы. При изменениях в работе модели все сервисы, которые зависят от нее, придется тестировать, и цикл разработки новой модели станет еще сложнее.
Где же может ошибиться разработчик ML-модели? Где угодно.
Например, в замерах качества или в версии данных, на которых обучалась модель. В архитектуре или параметрах модели. В коде, который замерял качество. Зачастую релиз модели — достаточно нервный процесс, а поиски решений по улучшению качества могут растянуться на месяцы.
В какой-то момент один из экспериментов оказывается успешным. Обрадованный разработчик проводит дополнительные замеры качества. Затем довольно часто релизится и забывает законсервировать что-то из кода, датасетов или метрик своей модели. А потом забывает детали, и теперь никто не знает, как он получал данные или проводил замеры.
Эта информация может понадобиться когда угодно: например, через полгода, когда придет время катить в продакшен новую модель. Если информация утеряна, невозможно адекватно сравнить качество с моделью, работающей в продакшене, или понять причины улучшения качества в новой версии. Возможно, улучшилась модель, а возможно, мы неправильно провели замер. Поэтому при проведении ML-экспериментов очень важно обеспечить воспроизводимость. Тогда разработчики смогут в любой момент получить примерно тот же результат, проведя эксперимент заново с неизменными данными и кодом.
Важно при этом сохранить старые метрики и то, на чем эти метрики считались. Например, пул предложений для синтеза речи и замера качества. Иначе в случае попадания готового эксперимента в продакшен там будет модель, которую невозможно изменить. Не получится обучить ее на новых данных, улучшить в ней что-то или провести новый эксперимент и сравнить получившуюся модель с тем, что уже работает в продакшене.
Как видите, неправильный подход к поддержке моделей может стать причиной множества проблем. Теперь поговорим о том, что наша команда делает, чтобы их избежать.
Опенсорсные решения и выбор в пользу собственного
Существуют опенсорсные решения для версионирования датасетов и ML-экспериментов, например DVC или MLFlow. Мы написали свою систему версионирования моделей, интегрированную в код их обучения. Дело в том, что у нас есть своя инфраструктура для запуска ML-экспериментов. Она быстро развивается и очень часто меняется, а нам нужна хорошая интеграция с ней.
Кроме того, наши модели сильно зависят друг от друга и от способа получения датасетов для их обучения. А акустическая модель еще и обучается с большим количеством параметров, которые мы никогда и ни при каких обстоятельствах не хотим терять. Поэтому мы решили фиксировать параметры эксперимента прямо в коде ветки, в которой он запускается.
В Тинькофф есть ML Core — внутренняя платформа для запуска экспериментов, связанных с машинным обучением, и других задач. Благодаря ей все доступные разным командам сервера загружаются примерно равномерно и не простаивают.
Эта платформа — абстракция над сервером. Вместо запуска на каком-то конкретном сервере задача определяется через YAML-конфиг и распределяется на доступный и свободный сервер. Это позволяет командам эффективнее использовать вычислительные ресурсы. Кроме того, с ML Core можно запускать задачи во внешних облаках.
Для запуска обучения на ML-платформе мы делаем YAML-файл. И в нем описываем, какие файлы откуда брать, какой код запускать и куда записывать получившиеся логи и файлы модели.
У нас в команде генерация этого YAML-файла происходит одинаково при запуске каждого эксперимента. Для запуска мы делаем ветку, которая начинается с номера тикета в Jira. В ветке параметры эксперимента подставляются в шаблон, и каждый запуск YAML генерируется автоматически. Так мы гарантируем одинаковый запуск обучения одной модели в ML Core. Если ветка эксперимента не начинается с префикса тикета в Jira, обучение не запускается. Таким образом, всегда можно связать ветку эксперимента с тикетом и эпиком в Jira.
Основные ML-модели в синтезе
Бывает end-to-end синтез — сразу из текста в звук. Но чаще всего пайплайн синтеза состоит из двух основных моделей: акустической модели и вокодера.
Обычно в работе со звуком используют промежуточное представление звука — например, мел-спектрограммы, которые лучше отражают восприятие звука человеком. Акустическая модель отвечает за конвертацию нормализованного текста с проставленными ударениями в мел-спектрограмму. Вокодер же конвертирует готовую мел-спектрограмму, полученную из акустической модели, в звук.
Эти две модели взаимозависимы. Бессмысленно замерять качество вокодера, не фиксируя конкретную версию акустической модели. Одновременно не имеет смысла замерять качество акустической модели, не зафиксировав версию вокодера. После каждого обновления акустической модели нужно обновлять и зависящий от нее вокодер.
Датасеты в синтезе речи
Чтобы создать новый голос, нужно получить датасет для обучения. Для этого необходимо договориться со студией звукозаписи, приготовить корпус текста для записи диктором, провести запись, а потом получить от студии датасет с парами «текст — запись в WAV-формате».
Получается сырой датасет — аудио и их расшифровки. Но для обучения акустики и вокодера нужны специальные датасеты. Для акустики — нормализованный текст с расставленными ударениями и мел-спектрограммы, для вокодера — мел-спектрограммы и WAV-файлы. Для получения таких датасетов есть специальные пайплайны. Их версии тоже нужно сохранять, чтобы обеспечивать воспроизводимость, так как с разными параметрами из одного сырого датасета можно получать разные конечные датасеты.
Версионирование самих моделей
Когда готовы датасеты для обучения акустической модели, нужно сохранить версию кода модели, версию датасетов и параметры экспериментов. Для этого в ветке кода модели, отведенной для эксперимента и ссылающейся на Jira-тикет, есть специальный класс, где фиксируются параметры модели для каждого спикера. Если какие-то необходимые параметры, такие как путь к датасету, не указаны, эксперимент в ML Core с экземпляром этого класса просто не запускается.
Вокодер обучается в две стадии. На первой он учится независимо от акустической модели, на второй — доучивается на мел-спектрограммах, которые она сгенерировала. Для первой стадии необходимо фиксировать только путь до датасета. Для второй — путь до чекпойнта первой стадии и мел-спектрограмм, на которых модель доучивается.
Сам код модели фиксируется веткой репозитория. Версия кода в ветке репозитория фиксируется тегом, который нужно указывать как параметр при запуске. Полученная модель сохраняется в директорию вида <имя_ветки>/<тег>/<имя_спикера>. Без тега эксперимент не запускается.
После обучения модели автоматически сохраняются тикет в Jira, сама ветка и тег ветки. В ветке зафиксированы все параметры эксперимента. Тег фиксирует состояние кода в ветке, а имя ветки отражено в пути до модели. Это гарантирует сохранность кода и дает возможность воспроизводить модель или эффективно отлаживать код эксперимента в поисках причин ухудшения качества звука, если это необходимо. Если какой-то код в репозитории не закоммитили, эксперимент в ML Core не запускается.
Даже если разработчик очень хочет забыть зафиксировать версию кода, датасет или что-то из параметров, скорее всего, у него не получится, потому что без этого ничего не запустится. А если он все же забудет, то всегда сможет посмотреть в ветке эксперимента с нужным тегом параметры, с которыми обучался. И всегда будет знать, где лежал чекпойнт и в какой именно ветке его искать.
Замеры качества и их сохранение
После обучения модели нужно производить замеры качества. Чтобы их можно было сравнивать друг с другом, корзинку, на которой производятся замеры, тоже необходимо фиксировать. Как и весь код, производящий замеры. Иначе будет трудно понять, с чем связано ухудшение или улучшение качества и было ли оно вообще.
Старые корзинки с замерами нужно сохранять. Иначе будет непонятно, как истолковывать оставшиеся цифры. Например, замеряется качество вопросительной интонации. Толокеров просят оценить, произнесено ли каждое предложение с вопросительной интонацией. Процент пула, произнесенный верно, становится метрикой — чем выше, тем лучше. Одна и та же модель может давать 89% верно произнесенных предложений на одной корзинке, а на другой — 65% или 95%. Если корзинки различаются, цифры — процент предложений с верной интонацией — сравнивать нельзя.
Для каждой акустической модели мы сохраняем side-by-side замер через краудсорсинговую платформу относительно прошлого продакшена и указываем, какой это был продакшен. Также мы сохраняем замеры качества вопросительной интонации — сколько вопросительных предложений из пула произнесено верно по оценкам асессоров с краудсорсинговой платформы — и версию вокодера, с которой производился замер. Ветку, тег и сам артефакт модели мы тоже сохраняем.
Если забыть сохранить что-то из вышеперечисленного, например артефакт вокодера, можно получить неожиданный скачок качества и не иметь возможности его оценить.
Отдельно мы замеряем качество вокодеров, дообученных на мел-спектрограммах новой акустической модели. Для них замеряются и сохраняются side-by-side всего пайплайна: старая акустическая модель и старый вокодер против новой акустической модели и нового вокодера.
После обновления акустических моделей вторые стадии вокодеров переобучаются на новых мел-спектрограммах.
Задания на краудсорсинговой платформе создаются автоматически. Есть скрипты, генерирующие аудиозаписи, и скрипты для загрузки готовых аудиозаписей на платформу и создания пулов для разметки асессорами с нашими внутренними контрольными заданиями. Это делается единым образом и автоматически, а значит, снижает риски ошибиться при замере качества.
Командные процессы: перенятие и документация
Часто бывает так, что ML-модель делает один разработчик. Тогда только этот разработчик понимает, как работает ML-модель и чего от нее следует ожидать. Это ухудшает и качество документации, так как один человек редко пишет документацию для самого себя.
Полезно, когда моделью занимаются как минимум два человека. Во-первых, это обогащает участников команды новым опытом. Во-вторых, если один разработчик уйдет, понимание процессов не потеряется. В-третьих, это упрощает написание документации. У нас у каждой модели есть документация на Confluence и табличка со списком версий моделей и ссылками на замеры качества.
А еще нам очень помогает, что у нас крутая команда. Это значит, что мы много пишем код, много общаемся и мало ругаемся. Это неочевидная вещь, но без нее ничего не получилось бы.