Привет, Хабр! Последние два года разработчики-исследователи MTS AI создавали помощника программиста, который называется Kodify. В этой статье мы расскажем о работе над этим продуктом и его функционале. Этот пост — адаптация доклада с конференции True Tech Day 2.0. Его запись можно посмотреть здесь.
Вернёмся в 19-й год. Вся эта история начинается не с нас, а с компании OpenAI, которая представила модель, базирующуюся на GPT-3. Её обучили помогать разработчикам и назвали Codex.
С Codex можно было общаться в чате. Всё как обычно: вы пишете запрос, и модель генерирует кусочек кода, который тут же можно исполнить. Ниже — промпт и результат его выполнения:
Через полгода корпорация Microsoft в сотрудничестве с GitHub выпустила Copilot. Это помощник программиста — сервис, способный дописывать код за вас.
Чуть позже появился Codex Glue. Он умеет в том числе обнаруживать дефекты и клоны, переводить код на другой язык и генерировать код по описанию на естественном языке.
В области обработки естественного языка и анализа текстов общепринятой практикой стало проведение бенчмарков — тестирование производительности моделей на стандартных наборах данных. Для новых языковых моделей они тоже выполнялись начиная примерно с 2020-го года.
Это не так сложно, ведь обработка текстов программ похожа на обработку текстов на естественном языке. Почему так? Если вы учились программированию в университете, то на первой лекции вам должны были сказать, что разработчик пишет программу не для машины, а для человека. То, что компьютер тоже так может, это на самом деле в каком-то смысле даже побочный результат.
Вернёмся к обработке кода. Ниже на инфографике показаны три базовых типа задач обработки и текстов:
Расскажем о них подробнее:
Understanding — понимание. На инфографике этот тип размещён слева. Модель читает тест, потом выдаёт векторное представление прочитанного, то есть то, с чем уже можно что-то делать, например, решать задачу классификации или выполнять поиск. То есть мы только понимаем текст. Кстати, раньше это было весьма популярное направление.
Сейчас актуальнее другое — генерация. Если пару лет назад мы называли себя специалистами по natural language understanding, то есть пониманию естественного языка, то теперь называем себя специалистами по natural language processing. То есть обработке и пониманию, а ещё генерации.
На инфографике к Generation относятся две колонки — центральная и правая. Генерация бывает двух типов:
Есть некоторый текст, и ему нужно сопоставить другой. Например, код программы: мы догадываемся, что там есть ошибка, и просим машину переписать всё так, чтобы убрать проблему.
Генерация текста, где всё устроено немного иначе. Первый шаг — затравка или промпт. Пример: «Напиши мне программу на языке C++, которая решает задачу Эйлера о мостах».
Теперь расскажем о нашем проекте. Это многоплановый сервис: он может писать код, помогать дописывать его, генерировать по запросу и так далее. В целом, есть много разных приложений. Мы расскажем только про часть, иначе статья получится слишком большой.
Например, автодополнение. Разработчик пишет код. Если вы кодили на каком-нибудь объектно ориентированном языке — Java, Python, может быть, даже C++ — то знаете, что есть такой синтаксис через точку: когда вы её ставите, появляется возможность вызвать или метод, или поле класса. IDE, интегрированная среда разработки, «знает», что у этого объекта есть класс, а у него — методы и поля. Помощник может предсказать, что хотел разработчик, какое поле класса или метод вызвать.
Чтобы сделать предсказание, требуется их упорядочить. Как? Самый простой способ — по алфавиту, но это неоптимальные стратегии.
Чтобы как-то с этим работать, нам пришлось создать свой набор данных.
Первоначально мы взяли Llama, о которой не раз писали на Хабре, и адаптировали её к работе с программистами — но с тех пор мы улучшили нашу модель. Изначально мы обучили Llama на большом количестве кода — 500 миллиардов токенов кода. Для этого мы использовали гитхаб. Плюс дополнительно отдельно обучали на питоне, делали instruction fine-tuning и получили несколько вариантов кода. Вот это одна из базовых моделей, с которой мы работали. С другими дело обстоит примерно так же.
Коротко расскажем о том, как обучают модели.
Слева на картинке — классическое обучение, то, что сейчас принято называть pre-training. Мы просто предсказываем следующее слово — и так 500 миллиардов раз. Это и есть pre-training. После обучения с моделью можно дополнительно что-то делать.
Дальше — alignment. Внутри это то, что сейчас принято называть supervised fine-tuning. Когда я начинал, это называлось машинным обучением. То есть мы брали и просто учили модели делать что-то полезное. Supervised fine-tuning — это как раз классическое обучение до изобретения pre-training и всего на свете.
Наконец, обучение с подкреплением на основе отзывов людей — RLHF. После него наступает черёд дообучения, иногда лишь для конкретной задачи.
Если сказать о RLHF в двух словах, оно устроено так. Обучение с подкреплением — концепция, когда у нас есть среда, в ней действует некий агент, и он получает награду. Это именно тот уровень абстракции, с которым мы работаем. Что это значит? У нас есть среда — скажем, человек, который слушает, что «говорит» модель. Эта модель и есть агент. За то, что она сказала, следует награда.
Обучать модель таким методом — безумно дорого и долго. Просто потому, что человек очень быстро устаёт, его нужно учить, да и платить тоже. Поэтому мы предложили новый вариант — воспользоваться другой моделью. Вместо того, чтобы напрягать 10 тысяч человек, мы обучим другую модель. И уже с её помощью мы можем обучать основную с подкреплением.
Что касается измерения, мы проводим его на наборе данных, который называется human eval, то есть обучение на человеческом суждении. Как оно устроено? К примеру, у нас есть текстовое описание задачи. Соответственно, на определённом языке программирования нам нужно сформулировать решение, написать код.
Выше — таблица, где можно увидеть подробности. Идея в том, что мы можем обучить нашу небольшую модель — 7b, она последняя здесь. Стоит отметить, что она очень неплохо справляется даже по сравнению с большими моделями, за исключением одной, слишком большой.
Есть и другие задачи — например, детектирование проблем в коде. Это когда мы пытаемся найти ошибку — не синтаксическую, которую очень легко парсером обнаружить, а что-то более глубокое.
Можно рассматривать перевод с языка программирования — например, C# на java. Если вы пытались переписывать свой код с Python 2 на Python 3, может быть, вы поймёте, в чём здесь боль и в чём польза.
В этом разделе детальнее обсудим автодополнение как одну из ключевых функций помощника программиста. С тех пор как появился GitHub Copilot, здесь многое изменилось по сравнению с классической функцией, к которой все привыкли.
Сейчас автодополнение срабатывает даже необязательно после точки в середине слова. Ещё оно генерирует качественное дополнение сложной конструкции по нескольку строк, причём не требует выбирать из разных вариантов. Обычно самый первый вариант оказывается правильным.
Как мы делаем конкурентоспособное по качеству Copilot-решение?
Сначала расскажем, чем отличается однострочное дополнение от многострочного. Технически между ними почти нет разницы. Просто во втором случае модель нужно попросить работать чуть дольше.
А вот в пользовательском опыте отличия большие. Дело в том, что разработчику приходится читать то, что сгенерировала модель. На этом этапе возникают некоторые проблемы, ведь короткий код читать намного быстрее, чем длинный. Причём зависимость нелинейная.
Компания Google проводила исследование на своих сотрудниках. Ситуация: автокомплит предлагает код, разработчик его принимает и оставляет в своём коде. Так вот, оказалось, что 90% кода от автокомплита, который принял разработчик, — однострочное дополнение. Поэтому, если мы генерируем многострочное, его стоит предлагать только когда оно качественное и корректное. Как же этого добиться? Да ещё и сделать так, чтобы генерировалось дополнение исключительного качества? И как вообще понять, что нужно предлагать многострочное дополнение?
Как мы их делаем? Берём кусок кода и делим его на prompt в первой части, а вторую отправляем в target, completion и suffix — находится ниже курсора. То есть имитируем ситуацию, когда разработчику нужно, допустим, после инструкции IF что-то дописать.
Так создаём четыре типа датасетов:
1. На своих репозиториях. Они у нас внутренние, мы делаем из них закрытые данные.
2. У нас в компании помощник программиста уже работает, ведёт логи. Мы достаём из этих логов данные, на которых можем обучаться и валидироваться.
3. Взяли известный human VLX и для каждого языка тем же способом сделали себе датасет.
4. Данные с самых популярных и свежих репозиториев github. Здесь кроется проблема. Есть закрытые данные, а есть открытые. Первые мы не можем никому отправлять — значит, нам сложно сравниваться с какими-то другими решениями. Вторые, к сожалению, очень быстро попадают в обучающую выборку, и поэтому мы не можем быть уверены в их уникальности. Возможно, на них уже обучался Copilot или какие-то другие модели.
Мы используем две метрики:
Доля совпадений первого значащего токена, то есть в сгенерированном тексте и в таргете. Допустим, у нас есть промпт, и нужно сделать дополнение функции getUserName. Независимо от того, передаём мы именованный аргумент или позиционный, у нас должен быть один и тот же первый значащий токен. Мы его выделяем.
Просто измеряем перплексию на первых N токенах.
А сейчас обзор SOTA-архитектур, которые есть сегодня. Первая — gpt. На картинке — пять открытых моделей, которые хорошо себя проявляют в автодополнении. В, все они — похожие друг на друга gpt-шки.
Prompt. Чтобы gpt что-то сгенерировала, ей нужен контекст. В этом качестве используется классический промпт. Грубо говоря, это текст в файле, который разработчик редактирует в этот момент: от начала файла до курсора, в том месте, где запрошено автодополнение.
Suffix. Суффикс — оставшаяся часть, от курсора и до конца.
Cross-file context. Сниппеты из файлов проекта, открытого в этом случае, часто используется для увеличения качества.
Static code analysis augmentations. Мы экспериментируем с аугментациями, полученными при помощи статического анализа кода.
Теперь расскажем про infilling и почему без него ничего не работает. Что это вообще такое?
Названия могут быть разными: fill in the middle, вставки. В целом, мы хотим немного изменить логику генерации, работы gpt. Причём нужно, чтобы она не как обычно предсказывала просто продолжение текста, а текст, который находится в середине, то есть между промптом и суффиксом, про который мы говорили.
Как это сделать? У нас промпт, подаваемый на вход модели, модифицируется так:
чаще всего сначала идёт токен fill in the middle_begin;
потом prompt;
потом token fill in the middle_hole, обозначающий, что сюда надо сгенерировать вставку;
потом суффикс;
потом токен fill end, после которого мы хотим, чтобы модель уже начинала генерировать ответ.
Посмотрите на скриншот выше. Токены раскрашены разными цветами так, как их видит токенизатор одной из популярных моделей. Так, если мы обучаем без infilling и хотим, например, чтобы он дополнил такую строку if, пробел, скобка открывается, t. Когда мы его учили, слово token означало токен целиком.
А теперь мы хотим, чтобы после буквы t он дополнил нам текст «oken». Получается, это не то, чему мы его учили. В случае с infilling мы немного упростили и вставили только один токен, но смысл остаётся понятным. То есть у нас этот токен разделяет слово, которое мы хотим дополнить. Выходит, что модель предсказывает именно то, что нам нужно. С infilling, когда мы вставляем технические токены, всё работает прекрасно. Модель дополняет любые части слов после любой буквы. Можете остановиться — и она вам сгенерирует.
Теперь расскажем об интересной особенности взаимодействия модели и пользователей.
Всё началось с того, что мы развернули новую модель. Примерно два месяца она у нас работала без изменений. Единственное, мы следили за метрикой exact match. В первом приближении можно сказать, что это доля предложенных пользователю автодополнений, которые он принял и оставил в своём коде. Сначала она была на уровне 48%, потом мы разместили объявление на внутреннем ресурсе и привлекли много пользователей.
После этого показатель упал до 43%, а потом снова стал неуклонно расти. Сейчас примерно ⅔ автодополнений, которые система предлагает юзерам внутри нашей компании, принимается разработчиками. Судя по всему, разработчики просто обучаются ждать автодополнения именно тогда, когда они знают, что оно будет правильным. То есть сами учатся даже лучше, чем модель. Это значит, что мы, к сожалению, не сможем офлайн сравнивать разные модели по пользовательским логам.
Как можно улучшить автокомплит? Первый вариант — реранжировать кандидаты, которые предложил статический анализатор. То есть у вас есть автодополнение, где он предлагал разные методы после точки, их и ранжируем при помощи нашей модели GPT.
Второе — генерируем только то автодополнение, которое легально с точки зрения статического анализа. Как работает re-ranker. Допустим, библиотека JEDI предложила несколько вариантов автодополнений. По каждому из них мы померили, например, перплексию и выдаём пользователю уже отсортированный список.
Наконец, пара слов по поводу систем наподобие Copilot. Их основной недостаток в том, что они ресурсоёмкие, плюс это серверное решение. Получается, что каждую секунду ваш код должен улетать для анализа на сервер.
С точки зрения безопасности понятно, что сейчас заказчик устанавливает подобные системы для того, чтобы его код оставался на своём сервере. Но если у каждой организации клиента есть свой помощник, почему бы не сделать это решение более индивидуальным? У нас есть репозитории, которые существовали уже до того, как это решение было развёрнуто. Ещё у нас есть логи действий пользователей. Всё это можно задействовать, запустить пайплайн подготовки данных и обучения и сделать индивидуальное решение для заказчика. Если этот пайплайн хорошо автоматизировать, мы получим модель, которая, можно сказать, учится сама.
В целом, на сегодня всё. Если у вас возникли вопросы или предложения, пишите в комментариях — всё обсудим!