Привет, Хабр! Мы представляем команду GigaCode. В декабре 2023 года наш продукт стал доступен широкой аудитории. До этого GigaCode использовался только внутри компании, и нас часто спрашивали о том, как GigaCode выглядит на фоне других ИИ-ассистентов, как вы сравниваете себя с остальными? Отвечая на эти вопросы, мы начали с простой задачи, которая оказалась не такой уж и простой и вылилась в увлекательное исследование со всем тем, что мы так любим: множеством измерений, математической статистикой и, конечно же, новыми горизонтами. Интересно? Добро пожаловать под кат.

С чего начать?

Базовая функциональность практически любого современного ИИ-ассистента заключается в генерации inline-подсказок в процессе написания кода разработчиком. Поэтому в первую очередь мы решили провести сравнительные замеры в этой области. В целом inline-подсказки можно разделить на две группы — single-line и multi-line. Хотя названия говорят сами за себя, всё-таки позволим себе дать некоторые пояснения. 

Multi-line, или многострочная подсказка, состоит из нескольких строк кода. Обычно ассистент выдаёт её в случае генерации кода целой функции или иной области (например, содержимого цикла for). Её основное назначение — помочь разработчику быстрее  создавать функцию, предложив точное или приблизительное решение задачи, которое можно доработать за несколько исправлений или почерпнуть идею для реализации. 

Single-line, или однострочная подсказка, длиной в одну строку или менее. Её главная задача — предоставить быструю и максимально точную помощь, чтобы ускорить завершение локальных «сиюминутных» задач, а также избавить от рутинных и повторяющихся операций.

Пример однострочной подсказки.
Пример однострочной подсказки.

Согласно нашим исследованиям и исследованиям компании Meta (признана в России экстремистской организацией) [1] большая доля выбранных подсказок и объёма принятого кода приходится на однострочные подсказки. Поэтому наше исследование будет связано именно с ними.  

Выбираем метрику качества помощи ИИ

Как же измерить качество однострочной подсказки? Самый простой способ — проверить условие полного совпадения подсказки, предложенной ассистентом, с так называемой истинной подсказкой.Для оценки можно взять фрагмент кода, убрать из него последнюю строчку и попросить ассистента её предсказать. Тогда эта удалённая строка будет считаться истинной строкой (или true line), а то, что предскажет ассистент, — предсказанной строкой (или predicted line). Если ассистент подскажет более одной строки, мы будем учитывать лишь первую. Таким образом оценивается его способность давать подсказки на основании верхнего контекста, поскольку всегда предсказывается последняя строка в коде, а снизу контекст отсутствует.

Предположим, что ассистент предсказал довольно длинную строку кода с точностью до одного-двух символов. Разработчик мысленно поблагодарил, принял подсказку и немного её подправил. Нельзя сказать, что ассистент тут не помог. Как можно учесть такой случай?

Расстояние Левенштейна

На помощь приходит дистанция редактирования — понятие, введённое в научный обиход нашим соотечественником Владимиром Левенштейном [2]. Дистанция редактирования, или расстояние Левенштейна, показывает, какое минимальное количество односимвольных операций (вставки, удаления, замены) необходимо для превращения одной последовательности символов в другую. Это то, что нам нужно, ведь почти правильная подсказка будет иметь небольшое расстояние от true line (а соответственно и значение метрики), в то время как сильно отличающаяся — наоборот.

Коэффициент помощи ИИ

Теперь попробуем оценить, насколько ассистент был полезен в каждом конкретном случае принятия подсказки. Нам осталось ввести нормировку (чтобы метрика получилась в процентах) и учесть случай, когда расстояние от подсказки до true line больше, чем сама истинная строка кода: 

c_{help}(s) = \max\big{[}0, len(s_{true}) - ld(s_{true},s_{pred})\big{]} \ /\ len(s_{true}),

где s = (s_{true}, s_{pred}) означает пару true line и predicted line соответственно, len(\cdot) возвращает длину строки в символах, а ld(\cdot,\cdot) вычисляет расстояние Левенштейна между строками. Если расстояние больше true line, то мы считаем, что ИИ не помог — c_{help}(s) = 0, за это отвечает условие \max

Коэффициент имеет простую интерпретацию: c_{help}(s) равен доле символов в строке s_{true}, которая была написана ассистентом. Действительно, вместо написания s_{true} с нуля, разработчик, приняв подсказку s_{pred}, совершил лишь ld(s_{true}, s_{pred}) редактирований символов, а оставшуюся работу по набору строки len(s_{true}) - ld(s_{true}, s_{pred}) = len(s_{true}) \  c_{help}(s) можно зачесть ассистенту. 

Внимательный читатель воскликнет: «Позвольте, но для замены символа надо сделать две операции: удалить старый и добавить новый символ. А одну и ту же строку кода я иногда могу написать по-разному, при этом функционально записи будут идентичны». Да, все так. Давайте посмотрим на это повнимательнее. 

true line

predicted line

ld1

ld2

chelp1

chelp2

assertEquals(expected, FormattingUtils.msToMinSec(4494000));

assertEquals(expected,FormattingUtils.msToMinSec(4494999));

3

6

0,95

0,9

synchronized Optional<Controller> get(String name) {

synchronized void remove(Controller controller){

26

37

0,5

0,288

@BeforeAll

static MockedStatic<ContextManager>contextManagerMocker;

53

59

0

0

CloudApplication existingApp =context.getVariable(Variables.EXISTING_APP);

return Optional.empty();

63

73

0,16

0,027

while( !(nextCommand = readString()).equals(END_KERN_DATA ) )

while (!END_KERN_DATA.equals(nextCommand = readString()))

47

47

0,242

0,242

Здесь видно, что оригинальное расстояние Левенштейна в среднем немного завышает коэффициент помощи ИИ, а в некоторых случаях разница становится просто драматической. Поэтому в дальнейших расчётах мы решили использовать именно модифицированное расстояние ld_2. Проблема синтаксически разных, но одинаковых по смыслу фрагментов кода в случае однострочных подсказок не стоит так остро. Это связано с тем, что мы измеряем точность подсказок в рамках одной строки, поэтому количество таких случаев крайне ограничено. Чтобы обнаружить приведённый выше пример, нам пришлось перебрать около сотни подсказок. Поэтому, для упрощения расчётов, мы решили пренебречь этим фактом.

Метрика AI code help

Теперь перед нами стоит задача оценки качества выдаваемых подсказок на большом объёме протестированного кода. Добавим формализмов: \begin{itemize}

  • Имеется набор из N подсказок S = \{s^i\}_{i = 0}^{N}, s^i = (s_{true}^i,s_{pred}^i).

  • Каждая подсказка s однозначно соответствует строке s_{true}. Будем говорить, что подсказка s является подсказкой по строке s_{true}.

Проще говоря, метрика является функцией от набора подсказок, на которых она рассчитана. С точки зрения методологии исследования, чтобы получить набор подсказок S, необходимо сначала выбрать набор строк \{s_{true}^i\}_{i=0}^{N} из интересующих файлов с кодом. Затем для каждой такой строки нужно вставить верхний контекст (весь текст из файла, находящийся выше строки, включая комментарии к коду) в редактор кода IDE и дождаться s_{pred}, которая придёт в подсказке. Проще говоря, мы измеряем строки кода в файлах, подразумевая, что в действительности мы измеряем качество, с которым ассистент предсказал true line. О том, как именно мы выбирали файлы с кодом и строки из них, речь пойдёт в следующих главах. 

Мы стремимся посчитать метрику H(S) (H от слова help), которая отражала бы то, насколько подсказки ассистента релевантны и насколько они помогли разработчику в написании кода из исследуемого набора S. Самый простой способ — набрать статистику подсказок, посчитать коэффициент помощи c_{help} по всем подсказкам и затем усреднить. Что ж, теперь набираем 1000 подсказок для каждого ассистента, считаем 1000 коэффициентов AI help, усредняем, получаем результаты и расходимся? Так было бы в идеальном мире. 

В реальности возникает целый ряд проблем. Во-первых, значительная доля этих подсказок может состоять из единственного символа скобки или чего-нибудь столь же простого и короткого. Было бы не совсем корректно учитывать эти подсказки в общей статистике наравне с более длинными подсказками. Так возникает идея усреднять коэффициенты помощи ИИ пропорционально длине true line. А именно, сложим все длины s_{true}, умноженные на соответствующие коэффициенты помощи c_{help}(s). Для каждой строки len(s_{true})\  c_{help}(s) будет интерпретироваться как количество кода в символах, написанное ИИ-ассистентом в строке s_{true}. Просуммировав по всем строкам, получим общее количество кода (в символах), верно написанное ИИ. Остаётся поделить на общий объём протестированного кода, чтобы получить оценку доли, которую ИИ-ассистент в среднем пишет вместо разработчика. В точной форме полученная метрика H выражается следующим образом:

H(S) = \frac{\underset{s \in S}{\sum}len(s_{true})\  c_{help}(s)}{\underset{s \in S}{\sum}len(s_{true})}

Даже в отрыве от её простой интерпретации полученная метрика хороша тем, что учитывает подсказку s не только пропорционально её релевантности, но и пропорционально длине строки. 

Казалось бы, цель достигнута: получена вполне осмысленная метрика, незамысловатая в интерпретации и простая в использовании. Однако, увлёкшись математическими абстракциями, мы чуть не забыли обратить внимание на один интересный факт: даже хорошая подсказка не вставляется в код сама по себе, сначала её должен принять разработчик. 

Введённая нами метрика H даёт оценку доли кода, написанного ИИ, но при условии, что разработчик принимает любую подсказку, для которой коэффициент помощи c_{help} выше нуля. Однако такое поведение разработчика неоправданно идеализировано и наблюдаться могло бы лишь в случае, если бы вместо разработчиков код писали машины, целью которых является минимизация количества нажатий на клавиши клавиатуры. Наши наблюдения показывают, что в действительности разработчики принимают только те подсказки, чьё качество выше определённого порога. При редакционном расстоянии, близком к длине истинной строки (то есть при малых c_{help}), реальный разработчик скорее отвергнет подсказку и напечатает строку с нуля, чем будет редактировать predicted line, превращая её в true line. 

Чтобы отразить это поведение, мы ввели порог минимального качества подсказки AI help threshold и теперь не засчитываем те подсказки, по которым коэффициент помощи c_{help} оказался ниже этого порога. Формально, обозначив буквой t порог качества, введём в нашу метрику AI code help зависимость от t:

H(S| t) = \frac{\underset{s \in S}{\sum}len(s_{true})\  c_{help}(s)\  \mathbb{I} \{ c_{help}(s) \geq t \} }{\underset{s \in S}{\sum}len(s_{true})},

где I — индикаторная функция, равная 1, если условие в скобках выполнено, и 0 в противном случае.

Итак, мы смогли учесть поведение разработчика и вновь прийти к аккуратному формальному выражению. При нулевом пороге метрика H(t=0) совпадает просто с H, а при единичном пороге метрика учитывает лишь идеально точные подсказки, что эквивалентно полному совпадению (exact match) истинной строки (true line) с предсказанной (predicted line).

Далее мы будем строить графики AI code help в зависимости от порога t. Такие графики могут наглядно показать, какая доля AI code help приходится на подсказки различного качества. Однако, хоть графики несут больше информации и просто выглядят классно, всё-таки хочется уйти от произвола в выборе порога t и оценивать помощь ИИ одним-единственным числом. Как это сделать? 

Прибегнем к  простой эвристике: хочется учесть все возможные пороги t, но при этом низкие пороги учитывать с меньшим весом. Тут мы предполагаем, что разработчик будет пользоваться разными подсказками, но более точные предпочитать менее точным. Для этого достаточно просто проинтегрировать H(S | t)*t по всем t. Домножение на t как раз и служит тому, что величина AI code help при малых t учитывается с меньшим весом, чем при больших. Для нормировки итоговую метрику следует умножить на два:

\mathcal{H}(S)=2\int_{0}^{1}H(S|t)tdt=2\int_{0}^{1}\frac{\underset{s \in S}{\sum}len(s_{true})\  c_{help}(s)\  \mathbb{I} \{ c_{help}(s) \geq t \} }{\underset{s \in S}{\sum}len(s_{true})}tdt \\=\frac{\underset{s \in S}{\sum}2\int_{0}^{1}len(s_{true})\  c_{help}(s)\  \mathbb{I} \{t \leq c_{help}(s)\}tdt }{\underset{s \in S}{\sum}len(s_{true})}=\frac{\underset{s \in S}{\sum}2\int_{0}^{c_{help}(s)}len(s_{true})\  c_{help}(s)tdt }{\underset{s \in S}{\sum}len(s_{true})}\\=\frac{\underset{s \in S}{\sum}len(s_{true})\  c_{help}(s)^3}{\underset{s \in S}{\sum}len(s_{true})}

Итак, число символов, которые напечатал ИИ в одной строке, равно, как мы помним, len(s_{true})\ * c_{help}(s). Теперь мы эту величину дополнительно умножаем на c_{help}(s)^2, что легко  интерпретируется как умножение на вероятность принятия подсказки разработчиком. Таким образом, в этой модели вероятность принятия подсказки пропорциональна качеству этой подсказки в квадрате. Более удачные подсказки принимаются чаще, идеальные (c_{help} = 1) принимаются с вероятностью 1. Полученную метрику \mathcal{H} назовём интегральным AI code help или интегральной долей верно предсказанного кода.

Определяем спарринг-партнёров

Итак, метрика у нас есть. Теперь нам осталось понять, к чему же мы будем прикладывать эту линейку. Для этого мы воспользовались результатами опроса StackOverlfow, которые показывают степень востребованности различных ИИ-ассистентов среди разработчиков. И для сравнения с GigaCode мы выбрали лидеров рейтинга — Copilot и Codeium, а также одного из пионеров индустрии — Tabnine.

Рейтинг популярных ИИ-ассистентов разработчика.
Рейтинг популярных ИИ-ассистентов разработчика.

Начинаем измерять

Вооружённые готовой метрикой мы принялись оценивать ассистентов в бою. Изначальная идея была проста: возьмём набор файлов с кодом, написанным на одном языке, и будем имитировать их написание «с нуля»: вставим в редактор кода с подключённым плагином ИИ-ассистента первую строку кода из первого файла, поставим курсор в начало новой строки и зафиксируем пришедшую от плагина подсказку predicted line. Запишем пару (s^1_{true}, s^1_{predicted}). Затем вставим уже первые две строки и зафиксируем предсказание третьей строки, и так далее. В конце в файле вставим подряд n-1 строк, кроме последней n-ной, и запишем  (s^n_{true}, s^n_{predicted}). Разумеется, вставка контекста в редактор кода и запись подсказок были автоматизированы с помощью написанного нами скрипта. Повторим процедуру для нескольких файлов и вычислим на полученных парах true line и predicted line метрику AI code help. Подчеркнём, что мы измеряем исключительно строки кода, а не комментарии к нему, то есть true line — это всегда строка кода.

Нам уже не терпелось получить первые результаты. Взяв 11 случайных .java-файлов из открытых GitHub-репозиториев, мы принялись измерять. Нас ждала следующая картина:

Общая длина файлов составляет 2806 строк кода, что может показаться достаточно большим количеством измеренных подсказок, чтобы получить надёжную оценку метрики. Вопрос: в чём тут подвох? Посмотрим на метрики AI code help, вычисленные на отдельных файлах из нашей первоначальной выборки.

Такой разброс не мог возникнуть, если бы не одна особенность наших измерений: они не являются независимыми дрyг от друга. Коэффициенты помощи c_{help} коррелируют для строк из одного файла. Это очень важный нюанс, с которым нам предстояло разобраться, так как мы хотели получить адекватную оценку метрики и вычислить погрешность этой оценки. Чтобы лучше понять наши дальнейшие рассуждения, устроим читателю краткий экскурс в мир математической статистики.

От 11 до 17 тысяч файлов: как измерять подсказки независимо, или борьба с корреляцией

Начнём по порядку. Независимость измерений чаще всего предполагается в любом исследовании и гарантирует, что каждое новое измерение уникально и не зависит от предыдущих. Таким образом, каждое новое измерение несёт в себе дополнительную уникальную информацию, которая не содержится в предыдущих измерениях. Лишь набрав достаточно большое количество независимых измерений, можно эффективно и с достаточной точностью оценить некую величину, в том числе и нашу метрику. Напротив, корреляция (зависимость) между измерениями приводит к тому, что новое измерение отчасти можно предсказать на основе предыдущих, и потому оно несёт меньше уникальной информации. В итоговую оценку метрики закладывается большая ошибка, связанная с тем, что информации в данных недостаточно.

Эффект корреляции возникает как следствие двух особенностей наших данных:

  • Коэффициенты c_{help} для соседних строк в одном файле не являются независимыми величинами. Если ассистент хорошо предсказал n-ную строку в файле, следует ожидать, что он неплохо справится и с предсказанием n+1-ой, так как оба предсказания даются на основании общего контекста. И наоборот, если ассистент не очень силён в предсказании строк на основании данного ему контекста, то что n-ю, что n+1-ю строку он будет склонен предсказывать плохо, потому что к контексту добавилась лишь одна строка в конце. То же самое рассуждение будет верно и для строк, стоящих одна от другой на 2, 3, 4, ... m позиций. Очевидно, с ростом m эффект корреляции должен угасать. Такая корреляция одних измерений с соседними измерениями в общей последовательности всех измерений называется автокорреляцией.

  • Есть более общий эффект, заключающийся в том, что строки кода в одном файле и даже строки кода из разных файлов одного репозитория связаны общей тематикой. Другими словами, фрагменты кода в рамках одного файла или в рамках одного репозитория склонны быть похожими друг на друга. Например, в них могут встречаться одни и те же функции, модули, библиотеки. Это приводит к тому, что c_{help} для строк кода внутри одного файла и внутри одного репозитория коррелируют между собой. Например, если для набора строк из одного файла (репозитория) ассистент выдал подсказки, чьё качество выше среднего, то стоит ожидать, что на очередной строке кода из этого же файла (репозитория) он вновь выдаст подсказку более высокого качества, и наоборот. Так возникает эффект внутриклассовой корреляции. Другими словами, строки в рамках одного класса (множества строк из одного файла или репозитория с кодом) схожи друг с другом, а потому подсказки по таким строкам коррелируют. Эффект корреляции внутри файла ожидаемо должен быть ниже корреляции внутри репозитория, однако и там, и там он присутствует. 

Чтобы понять эффект лучше, можно воспользоваться простой аналогией. Допустим, перед вами стоит задача измерить средний рост людей на Земле. Вместо того, чтобы измерить рост у сотни абсолютно случайных людей, вы решили измерить рост у 50 голландцев и 50 бушменов, из которых 2 являются близнецами и имеют одинаковый рост. Но рост людей внутри каждой национальной группы коррелирует: голландцы в основном имеют рост выше среднего, а бушмены ниже среднего. Рост близнецов и вовсе коррелирует с коэффициентом, близким к 100 %. Измерить двух близнецов с точки зрения полезной информации — это практически то же самое, что измерить всего одного из них. Очевидно, что первая выборка, состоящая из 100 случайных индивидов, даёт более точную оценку среднего роста всех людей и несёт больше информации о людях в целом. 

Чтобы проверить наши предположения, мы измерили качество подсказок GigaCode, взяв случайным образом 152 файла из 119 репозиториев общей длиной 31 914 строк кода. Начали мы с того, что вычислили коэффициент корреляции между c_{help} для двух строк из файла, отстающих одна от другой на m позиций: corr\_coef \big{(}c_{help}(s), c_{help}(s^{+m})\big{)}. Зависимость этого коэффициента корреляции от расстояния между строками представлена ниже.

Что это за ломаная линия, спросите вы? Многочисленные пики являются шумом, случайной ошибкой, возникающей из-за того, что коэффициент корреляции вычислен на ограниченном количестве строк кода. Общий тренд, однако, налицо: корреляция действительно уменьшается с ростом расстояния между строками. Более того, она не исчезает полностью, а стремится к ненулевому значению. В этом и проявляется вышеупомянутый эффект кластеризации данных. Даже для удалённых друг от друга строк положительная корреляция между c_{help} присутствует просто в силу того, что они находятся в одном файле.

Чтобы собрать наиболее полную информацию о подсказках и охватить максимально разнообразный код необходимо увеличить число файлов и репозиториев с кодом, на которых мы измеряем ассистентов. Тем самым мы уменьшим потенциальную ошибку в оценке метрики. Однако здесь мы упираемся в существенное ограничение: на фиксацию одной подсказки уходит несколько секунд, что позволяет собирать статистику со скоростью примерно 1000 подсказок в час для одного ассистента.

Чтобы сократить время измерений, можно измерять не каждую строку каждого файла, а лишь десятую или даже сотую долю строк из общего количества. Строки должны выбираться случайно. Как это поможет? За то же самое время и то же самое общее количество измерений мы охватываем в десятки или даже сотни раз большее разнообразие файлов и репозиториев с кодом. Звучит как предложение, от которого невозможно отказаться, не правда ли? Кроме того, при этой стратегии мы приятным бонусом получаем и уменьшение эффекта автокорреляции в измерениях: действительно, строки измеряются теперь не последовательно одна за другой, а в среднем разнесены на 10 или даже 100 позиций. 

Используя данные полностью измеренных 152 файлов, мы провели моделирование, чтобы определить, как будет меняться ошибка в зависимости от общего количества строк, на которых мы измерили подсказки GigaCode, и в зависимости от доли измеряемых строк. О том, как мы численно оценили величину ошибки, вы узнаете в следующей главе. А пока мы просто наглядно подтвердим наши предыдущие рассуждения:

При увеличении общего числа измерений в  раз ошибка уменьшается в  раз.
При увеличении общего числа измерений в N раз ошибка уменьшается в \sqrt{N} раз.

В итоге мы решили провести измерения на одном из наших датасетов качественного кода на Java. Для этого датасета мы отобрали файлы из GitHub-репозиториев с определённым количеством форков и звёзд, с разработанными модульными тестами. Общий объем набора данных — 33456 .java-файлов, собранных из 2289 репозиториев.

Последовательно перебирая все строки во всех файлах, мы с вероятностью 1 % брали текущую строку и проводили измерение на ней для всех ассистентов.  В итоге мы измерили подсказки всех исследуемых ассистентов по 41 944 строкам из 17 717 файлов из 2059 репозиториев.

Вы можете спросить: раз строки внутри одного файла в любом случае дают коррелирующие измерения, то почему не брать просто по одной строке из каждого файла или вообще из каждого репозитория? Корреляция ведь упадёт в ноль. Гениальная идея, срочно отказываемся от предыдущей стратегии! Шутка. Проблема тут в том, что ассистенты дают более точные подсказки, если в их распоряжении имеется больше контекста. Однако в файлах с малым объёмом кода контекста попросту меньше, чем в длинных файлах. Это приводит к тому, что чем больше файл, тем, в среднем, большие коэффициенты c_{help} получаются при измерении строк в нём. Теперь представим, что мы директивно берём из каждого файла ровно одну строку, ни больше, ни меньше. Тогда пропорция строк, полученных из больших файлов, уменьшится, а из коротких — возрастёт. Это приведёт к неоправданному смещению оценки метрики AI code help в меньшую сторону. Например, если раньше файл длиной в 20 000 строк имел в сто раз больший вес в общей статистике, чем файл длиной в 20 строк, то теперь эта пропорция превращается в 1 к 1. Другими словами, распределение размеров файлов, из которых были взяты строки, смещается, и короткие файлы получают неоправданно больший вес. Предложенная нами стратегия позволяет избежать этого и получить несмещенную оценку AI code help. 

Бутстрэп: математические чит-коды

Эвристический анализ показывает, что ошибка измерений уменьшается при увеличении разнообразия файлов и репозиториев с кодом. Но как нам количественно оценить эту ошибку? 

Начнём по порядку. Во-первых, метрика AI code help не сводится к простому среднему коэффициентов c_{help} по всем измеренным строчкам, поэтому классическая формула для стандартной ошибки среднего тут бы не сработала. Во-вторых, при выводе подобных аналитических формул обычно предполагается, что все измерения независимы. Тому, что в наших измерениях это условие нарушается, мы посвятили всю предыдущую главу, так что выходит неловкая ситуация. Но это только с первого взгляда! На помощь приходит бутстрэп — эффективный способ оценки доверительных интервалов в обход параметрических методов математической статистики. 

Ещё раз зададимся вопросом: почему возникает ошибка в оценке метрики? Да потому, что всякий раз мы её измеряем на ограниченных данных. Допустим, было измерено n_1 строк из n_2 файлов из n_3 репозиториев. Обозначим этот набор строк и подсказок по ним как S_1. Мы получили значение метрики H(S_1 | t) (далее для краткости будем опускать t, предполагая его фиксированным). Теперь допустим, что мы вновь случайным образом выбрали n_1 строк из других n_2 файлов из других n_3 репозиториев. Обозначим это как набор S_2. В общем случае очевидно, что H(S_1) \neq H(S_2). От случая к случаю мы будем получать несколько разный результат. Из-за случайности в выборе S у H(S) будет ненулевая дисперсия D (разброс): D\ H(S) \neq 0

Чтобы оценить этот разброс и существует метод бутстрэпа: на основании уже полученной выборки подсказок S мы будем случайным образом брать новые подсказки и складывать их в выборку \tilde{S}. При этом, в силу случайности, одна и та же подсказка может войти в новую выборку несколько раз. Таким образом, повторно выбирая подсказки из S, мы будем всякий раз получать разные наборы \tilde{S}, и H(\tilde{S}) тоже будет иметь ненулевую дисперсию. Ключевая идея бутстрэпа заключается в том, что дисперсия D \big{[} H(\tilde{S})\big{]} даёт хорошую оценку для дисперсии D \big{[} H(S)\big{]}. При этом оценить D \big{[} H(\tilde{S})\big{]} крайне просто: выбираем N разных наборов \{\tilde{S}_i\}_{i=1}^N, вычисляем N соответствующих метрик H(\tilde{S}_i) и считаем выборочную дисперсию. N можно взять порядка 1000.

Вопрос: каким образом мы будем собирать новые наборы \tilde{S} из уже имеющегося на руках оригинального набора подсказок S? Наши данные имеют трёхуровневую иерархическую структуру: строки, файлы, репозитории. В статье [3] показано, что в случае трёхуровневой иерархии данных имеет смысл делать выборку только на верхнем уровне. То есть, если в наборе S были данные из n репозиториев, то выбирая репозитории с повтором, мы получим второй набор из n репозиториев (где некоторые репозитории могут повторяться). Ключевое значение имеет возможность повторно выбирать то же количество репозиториев, которое было изначально. Из всех строк этих репозиториев составим новый набор \tilde{S}. Если какой-то репозиторий встречается в наборе k раз, то и все строки этого репозитория повторяются k раз.

Итак, с помощью бутстрэпа мы можем оценить дисперсию оценки нашей метрики. Более того, мы можем оценить и дисперсию оценки разницы AI code help для двух разных ассистентов. Для этого будем вычислять для каждого \tilde{S_i} соответствующую метрику H_1(\tilde{S_i}) для первого ассистента и H_2(\tilde{S}_i) для второго ассистента и считать их разницу. На основании полученной оценки и её дисперсии мы рассчитаем статистическую значимость отличия метрики AI code help для двух ассистентов.

Теперь мы умеем оценивать дисперсию и это, несомненно, прекрасно, но как оценить доверительные интервалы? Дело в том, что оценка метрики AI code help имеет нормальное распределение. Его дисперсию мы оцениваем с помощью бутстрэпа, а значит, мы можем оценить и ширину 95 % доверительного интервала оценки. Для нормального распределения эта ширина равна четырём стандартным отклонениям (стандартное отклонение равно корню из дисперсии).

Финальные результаты и их статистическая значимость

Ура, мы поняли, как снизить ошибку оценки метрики AI code help и научились численно эту ошибку оценивать. Представляем наш итоговый результат с 95 % доверительными интервалами вокруг оценок. Этот интервал показывает ту область значений, в которую попадает истинный AI code help c вероятностью в 95 %. При этом предполагается, что наш датасет GitHub-кода действительно репрезентативен и даёт несмещённую оценку AI code help.

Java
Java

В ходе нашего исследования мы обнаружили, что не все ассистенты стабильно выдают подсказки по каждой строке и порой «отмалчиваются», оставляя разработчика наедине с кодом. Особенно к такому поведению склонны GitHub Copilot и Tabnine, которые не выдавали подсказку в 16 % и 14,5 % случаев соответственно. Для сравнения, Codeium не выдал подсказку в 5,4 % случаев, GigaCode — 1,3 %. Пытаясь выяснить причины этого поведения, мы провели ещё одно небольшое исследование, повторно «прогнав» Tabnine и Copilot по 270 случайным строкам из 180 .js-файлов. Мы решили проверить, будут ли ассистенты пропускать подсказки повторно на тех же самых строках, что и в первый раз, или же этот эффект абсолютно случаен. Результаты довольно занимательны: Tabnine вновь пропустил подсказку для 92,9 % строк из тех, на которых он пропустил её в первый раз. При этом в обоих случаях он пропустил в общей сложности 5,2 % подсказок, что можно назвать довольно стабильным результатом. Copilot же повторно пропустил подсказку для 94,9 % строк из тех, для которых он пропустил её и в первый раз. Однако при этом он пропустил подсказки и для многих новых строк, так что общая доля пропусков подскочила с 14,4 % до 21,9 %. Наша гипотеза состоит в том, что ассистенты могут пропускать подсказки для контекста, в котором они меньше уверены, либо на основании каких-то ещё неизвестных нам внутренних правил. При этом количество пропусков может зависеть от загруженности серверов или быть продиктовано другими техническими особенностями работы инфраструктуры.

Если не учитывать пустые подсказки, то метрика AI code help, само собой, окажется выше. Она может рассматриваться как показатель среднего качества подсказок ассистента вне зависимости от того, как часто он их выдаёт. Однако она уже не отражает истинной помощи ассистента в разработке, так как закрывает глаза на то, что, пропуская подсказки, ассистент не вносит никакого полезного вклада. 



Java
Java

Отдельно мы измерили подсказки на языках JavaScript и TypeScript. В силу их синтаксической схожести мы собрали по ним общую статистику, состоящую из 4906 файлов из 188 GitHub-репозиториев. Получившаяся картина, в целом, схожа с результатами, полученными для Java-кода.

JavaScript, TypeScript
JavaScript, TypeScript

Приведём таблицу со статистической значимостью отличий интегральной AI code help между ассистентами, а также доли идеально подсказанного кода (AI code help при единичном пороге принятия подсказки). P-value, приводимое ниже, интерпретируется как вероятность того, что отличие в AI code help было обнаружено в силу случайности измерений, а не в силу реального его наличия. P-value округлены до сотых.

Интегральная метрика AI code help, %:

Язык

GigaCode

Copilot

Codeium

Tabnine

Java

48,8

46,8

45,8

42,3

JavaScript, TypeScript

44,1

42,9

43,8

37,3

Двустороннее p-value для интегрального AI code help (Java):

GigaCode

Copilot

Codeium

Tabnine

GigaCode

1

0

0

0

Copilot

0

1

0,09

0

Codeium

0

0,09

1

Tabnine

0

0

0

1

Двустороннее p-value для интегрального AI code help (JavaScript, TypeScript):

GigaCode

Copilot

Codeium

Tabnine

GigaCode

1

0,24

0,73

0

Copilot

0,24

1

0,1

0

Codeium

0,73

0,1

1

Tabnine

0

0

0

1

Доля идеально подсказанного кода, %:

Язык

GigaCode

Copilot

Codeium

Tabnine

Java

37,7

37,7

36,5

32,4

JavaScript, TypeScript

35,5

36,3

36,4

30

Двустороннее p-value для различия доли идеально подсказанного кода (Java):

GigaCode

Copilot

Codeium

Tabnine

GigaCode

1

0,997

0

0

Copilot

0,997

1

0,09

0

Codeium

0

0,09

1

Tabnine

0

0

0

1

Двустороннее p-value для различия доли идеально подсказанного кода (JavaScript, TypeScript):

GigaCode

Copilot

Codeium

Tabnine

GigaCode

1

0,44

0,42

0

Copilot

0,44

1

0,91

0

Codeium

0,42

0,91

1

Tabnine

0

0

0

1

Как видим, объёма измеренного Java-кода оказалось достаточно, чтобы с уверенностью отличить почти всех ассистентов. В случае языков JavaScript и TypeScript три ассистента — GigaCode, Codeium и Copilot — не обнаруживают статистически значимых отличий c точки зрения интегральной доли верно предсказанного кода. Это может быть связано как с меньшим объёмом измеренного кода, так и с отсутствием значимого отличия в реальности. Отметим, что в обоих случаях Tabnine статистически значимо отличается от конкурентов, причём отличие не в его пользу.

Выводы

Попробуем проанализировать полученные результаты.

Как видно на приведенных выше графиках для языка Java при более низких порогах принятия подсказок GigaCode статистически значимо показывает более высокое значение метрики относительно других ассистентов. При приближении к большим значениям порога принятия подсказки мы наблюдаем уменьшение разницы между GigaCode и Copilot до статистически не значимой, а при пороге 1 в эту компанию с небольшим пересечением доверительных интервалов попадает и Codeium.

Это позволяет нам сделать вывод о том, что по исследуемой задаче предсказания полной следующей строки кода по левому контексту GigaCode, Copilot и Codeium показывают паритетные результаты в области генерации практически точных подсказок.

Однако, В области генерации подсказок с более низкими порогами совпадения GigaCode больше остальных помогает разработчику.

Если оценивать всю помощь интегрально, учитывая различные пороги принятия подсказок в совокупности, то тут GigaCode опережает Copilot на 2 %, Codeium на 3 % и TabNine на 6,5  %.

В случае JavaScript и TypeScript ситуация схожа, с той разницей, что Codeium показывает более высокий результат. Действительно, кривые AI code help трех ассистентов статистически неразличимы при средних и высоких порогах принятия подсказки. При низких порогах Copilot по-прежнему статистически хуже GigaCode (p-value = 0,027 при нулевом пороге), однако начиная с порога в 0,2 статистически это различие уже теряется. 

В то же время, статистически значимое отличие для всех порогов сохраняется у Tabnine, но это отличие не в его пользу. С точки зрения интегральной метрики его отставание от GigaCode составляет 6,8 %. 

Также мы выяснили, что GigaCode подсказывает чаще, выдавая подсказку в 98,7 % случаев против 84 % для Github Copilot, 94,6 % для Codeium и 85,5 % для Tabnine.

Заключение

Надеемся, вам было интересно и увлекательно пройти этот путь вместе с нашей командой. В дальнейшем мы планируем продолжить сравнительные исследования ИИ-ассистентов и на других задачах, таких как, например, оценка качества подсказки с учётом контекста снизу, контекста из других файлов, оценка качества multi-line подсказок и многое другое, ведь GigaCode и другие ассистенты не стоят на месте!  

Источники

Авторы: 

  • Гавриляк Р.Я.

  • Шелепин С.Л

  • Балыбердин-Панкратов Ф.А.

Комментарии (15)


  1. gudvinr
    22.05.2024 09:34
    +4

    Такое ощущение, что вы очень хотели найти хоть что-то, что у вас больше, чем у других

    Какая разница, насколько строчка похожа? Важно чтобы код не похож был на то, что вы руками пишете, а чтобы он работал

    Единственная метрика полезности ассистента - это суммарное время, которое человек потратит на решение задачи.

    Если руками код написать выходит быстрее, чем думать над промптами, ждать запросов/ответов API и исправлять результат, то ассистент бесполезный

    А похожесть не обязательно гарантирует, что суммарно вы потратили меньше времени


    1. RuslanGavriliak
      22.05.2024 09:34
      +2

      Исследование посвящено single-line подсказкам, которые прилетают прямо в процессе написания кода. Если такая подсказка совпала, или оказалась очень похоже на то, что вы хотели написать руками, то достаточно просто нажать на Tab, и она вставится в текст. Никаких промптов писать не надо, подсказки прилетают сами)
      Так что порой вместо сочинения и написания целой строки кода достаточно просто нажать на одну лишь клавишу Tab. Время экономится, и еще как

      Другое дело, что если тестировать не на идеально закоменченном коде с гита, а на обычном коде, который может быть кривоват, то метрика у всех ассистентов подупадет. Тут смысл в том, что они были помещены в одинаковые условия и объективно сравнены.


      1. gudvinr
        22.05.2024 09:34

        То, что вы описываете, работало и раньше без AI довольно адекватно, а дремучие деды это называли "сниппетами"


        1. RuslanGavriliak
          22.05.2024 09:34

          А теперь работает еще лучше. Сниппеты, будь то вставка регулярных синтаксических структрур, вроде блока if или блока объявления класса, или кастомные пользователськие сниппеты, были вполне хорошими для своего времени инструментами для экономии времени написания кода.
          Современные нейросетевые языковые модели более гибко подстраиваются под контекст файла, способны обогнать мысль разработчика, порой выдавая 100% точные подсказки еще до того, как разработчик придумал, что же он хотел написать, и имитируют его стиль. Ввиду этого их популярность только растет. Это не умаляет эффективности классических "сниппетов", но пользователи все стремительнее переходят на AI, видимо, считая это решение более удобным. Каждому свое)


          1. gudvinr
            22.05.2024 09:34

            А можно в цифрах ваши маркетинговые заготовки?

            А теперь работает еще лучше

            Лучше как, и по чьим данным?

            для своего времени

            Своего - это какого? Пока все массово не стали хайповать на ИИ?
            Они хуже работать не стали от этого.

            Современные нейросетевые языковые модели более гибко подстраиваются под контекст файла

            Опять же, более гибко - это как измерено?

            способны обогнать мысль разработчика

            А это? Какого разработчика, как вы это проверяли, как часто обгоняют и выезжают ли при этом на встречку?

            придумал, что же он хотел написать
            имитируют его стиль

            А если разработчик коммитит в код, который писал не он и вообще первый раз его видит?

            Будет ли подсказка эффективной и отражать желание разработчика, а не каких-то людей, которые писали код. Нельзя предполагать, что авторы исходного кода могут иметь лучшее представление о том, что именно разработчик хочет написать.

            но пользователи все стремительнее переходят на AI

            Пользователи чего? Что это за пользователи, кто/как эту статистику собирал, сколько было, сколько осталось, и т.д.


            1. RuslanGavriliak
              22.05.2024 09:34
              +1

              Хотя я и комментирую от своего имени, но мой позитивый взгляд на AI несомненно делает меня маркетологом)
              Как я и сказал, успех нейросетевых моделей не умаляет эффективности классических сниппетов. В случае AI речь идет о другом, более широком функционале. В целом этот фунционал приводит к увеличению эффективности разработки. Пример исследования (https://ar5iv.labs.arxiv.org/html/2302.06590), в котором исследуемая гурппа в 45 человек, имеющая доступ к AI ассистенту, показывает статистически значимый прирост в скорости решения coding задач (на 55.8%) в сравнении с контрольной группой из 50 человек, которые пользовались лишь стандартными средствами IDE. В том, что касается обсуждаемой нами экономии времени, эффект налицо.

              Можно понять ваше личное нежелание присоединяться к всеобщему "хайпу" вокруг AI, но одновременно можно констатировать и рост популярности AI-ассистентов. На графике легко просматривается экспоненциальная фаза роста подписчиков GitHub Copilot (https://images.app.goo.gl/fcJrZjwuoJqC7V8q7), а опрос Docker (https://www.docker.com/resources/2024-docker-ai-trends-report-infographic/) показал, что 64% разработчиков в 2024 году так или иначе уже использует AI в своей работе, и 61% от числа опрошенных утверждает, что AI помогаем им в их рабочих задачах. Приток пользователей AI сервисов среди разработчиков налицо. Люди склонны пользоваться тем, что работает, и избегать того, что не работает, только и всего)

              Выражение "более гибко" просто-напросто отражает тот факт, что классические подсказки IDE лишь подсказывают имена переменных, или пустые шаблоны для цикла/ определения функции и тд, в то время как генеративный трансформер подсказывает готовый код, учитывая (в силу механизма Self-Attention) весь контекст, предоставленный ему. Как было указано в стаьтье, 37.7% кода таким образом угадывается абсолютно точно (опять же, для 'хорошего' кода с GitHub).

              Измерить процент подсказок, которые обогнали мысль разработчика с перечением двойной сплошной, возможно лишь спрашивая оценку самих разработчиков, и такая величина не наблюдаема напрямую.

              По поводу коммита в чужой код: пожалуй, даже плюс, что AI ориентируется на уже существующий код в файле, это способствует единству стиля.

              Можно заниматься точными оценками вклада AI в разработку, я же ограничусь более скромным комментарием, и подытожу, что такой вклад есть, и он достаточен, чтобы убедить 64% разработчиков пользоваться AI-сервисами.


              1. gudvinr
                22.05.2024 09:34

                мой позитивый взгляд на AI несомненно делает меня маркетологом

                То, что вы аффилированы с продуктом, о котором статья, делает ваши утверждения необъективными.

                В целом этот фунционал приводит к увеличению эффективности разработки. Пример исследования (https://ar5iv.labs.arxiv.org/html/2302.06590)

                Исследование проведено Microsoft о продукте, который делает Microsoft.
                В статье разработчиков наняли на Upwork, и в задаче явно было указано требование написать код быстрее. То есть люди, которые не заинтересованы в качестве и сопровождении кода, писали код, у которого не было требований к качеству.

                У них не было цели писать эффективный код быстрее. У них была задача быстрее произвести какой-то код.

                При этом там пишут, что у разработчиков с меньшим опытом разница выше. Обычно чем больше опыта у разработчика, тем больше у него намётан глаз на ошибки и несовершенства. То есть, косвенно этот результат говорит о худшем качестве кода у менее опытных разработчиков, т.к. они тратят меньше времени на коррекцию.

                Но хотели бы вы сами пользоваться продуктом или работать над кодом, к которому предъявляются именно такие требования? Если вы ориентируетесь на такие исследования, то наверное и бэкенд GigaCode так же написан :)

                На графике легко просматривается экспоненциальная фаза роста

                Из экспоненциального там только кусок хвоста, плюс график примерно соответствует росту кол-ва пользователей github в целом. Т.е. доля пользователей практически не меняется. Учитывая то, что Microsoft оценивает кол-во пользоветелей Github как 100+ млн, то это на уровне 5%, что в 13 раз меньше чем ваши 64%.

                64% разработчиков в 2024 году так или иначе уже использует AI в своей работе, и 61% от числа опрошенных утверждает, что AI помогаем им в их рабочих задачах

                А если посмотреть инфографику, то для написания кода gen AI использует 33%. Это тоже чуть меньше, чем 64, не находите?
                Ещё не совсем очевидно, как отличаются результаты в зависимости от опыта/области использования.

                37.7% кода таким образом угадывается абсолютно точно
                для 'хорошего' кода с GitHub

                В реальной жизни пишут реальный код, а не "хороший" c github. Ну и на github не весь код хороший.

                пожалуй, даже плюс, что AI ориентируется на уже существующий код в файле, это способствует единству стиля

                Стиль в современных языках форсируется линтерами и автоформатированием. Для этого AI не особо нужен. Может быть, стиль именования переменных и методов будет похож. А вот похожесть кода на основе того, что есть в существующем коде - это не гарантия правильной функциональности.

                Простой пример - в коде на Go можно получить доступ к элементам массива в цикле через второй параметр range:

                for _, elem := range []struct{...} {
                  fmt.Println(elem)
                }

                Если у вас в файле производится только чтение, это никаких проблем не вызывает. Но вот это будет ошибкой:

                for _, elem := range []struct{...} {
                  elem.X = 1
                }

                Отмечу, что я понятия не имею что именно ассистент предложит в конкретном примере. Важно то, что в разных языках таких семантически верных, но логически неправильных конструкций может быть множество. И ошибки, с ними связанные, допускаются в том числе в реальном коде, на котором учится ИИ.


                1. RuslanGavriliak
                  22.05.2024 09:34

                  Согласен, что обширных независимых исследований эффекта применения AI в разработке хотелось бы больше. При этом, если спросить самих разработчиков, можно обнаружить, что, например, в этом опросе (https://www.statista.com/statistics/1440348/ai-benefits-in-development-workflow-globally/) 13% респондентов даже сообщают о воспринимаемом ими повышении качества кода. Да, профессиональный разработчик со стажем в 30 лет, вероятно, не получит прироста в качестве кода от использования AI ассистента. Это не значит, однако, что периодические автодополняющие подсказки, способные порой обогнать скорость его печати, будут для него бесполезны. Кроме того, AI может генерировать больший объем кода по запросу, писать юнит тесты, документацию к коду. Может быть проще доработать сгенерированное AI решение, чем писать свое с нуля. Ответсвенность за качество конечного кода все равно лежит на разработчике.

                  Справедливым показателем тут может служить удовлетворенность разработчика от использования AI, а как показывают опросы, в целом среди них преобладает положительный сентимент.

                  В общем мы все живем в одном большом эксперименте по внедрению AI, интересно посмотреть, к чему это нас приведет, если приведет, и каким окажется потолок развития у нынешней LLM архитектуры. Удачи вам)


                1. Bone
                  22.05.2024 09:34

                  Раньше говорили, что профессиональный программист (хотя чаще это говорили про верстальщика) пишет код в блокноте. Типа, ему только мешают всякие ide и т.п.. Сейчас код в блокноте пишет только дурак. Через короткое время только дурак не будет использовать ИИ помощников.


                1. seregina_alya
                  22.05.2024 09:34
                  +1

                  Ладно, скажем просто и по-другому

                  ИИ-ассистенты и копилоты не идеальны. Они не всегда угадывают абсолютно точно, и наверняка не могут помочь хард-сеньорам во всём, что они делают, и так далее. Но у меня таких навыков нет. Я пока что пишу простые вещи и codeium действительно значительно ускоряет мою работу. Например, мне не нужно писать несколько строк запроса к БД и обработки данных. Мне достаточно сделать, например,

                  // Take seattle users with age > 30, put into sorted by age desc List

                  и несколько раз нажать таб. А ИИ тем временем посмотрит на несколько похожих запросов в файле, разберётся в структуре полей и таблиц в БД и напишет несколько строк кода. Конкретно в этом случае 95+% вероятность, что не придётся ничего менять вообще. 10 секунд, потраченных на комментарий, были бы потрачены в любом случае, потому что я привыкла их делать даже в простых местах. А всё остальное сделали несколько последовательных нажатий tab

                  В общем, что я хочу сказать. Да, инструмент не идеален. Да, надо знать, что делаешь, потому что выучить что-то новое с помощью подсказывателя кода не выйдет. Но когда понимание работы программы есть, написание нового кода начинает занимать в разы меньше времени. И никакие сниппеты ТАК сильно мою работу не ускоряют

                  И да, он действительно может придумать код раньше меня. Я только думаю, что надо сделать if, который проверит возраст, а потом отправит что-то в БД, но ещё не успеваю ни написать комментарий, ни осознать чётко конкретный текст команд, которые собираюсь набрать, а у меня уже выведен серый код и условия, и команды, и скобочки. Остаётся либо нажать tab и идти дальше, либо что-то подправить. Удобно


                  1. gudvinr
                    22.05.2024 09:34

                    Например, мне не нужно писать несколько строк запроса к БД и обработки данных. Мне достаточно сделать, например,

                    // Take seattle users with age > 30, put into sorted by age desc List

                    SQL - это УЖЕ язык запросов. Получается, что вы пишете текст запроса, чтобы получить текст запроса, вместо того чтобы написать сразу текст запроса.

                    Вместо этого достаточно написать что-то типа WITH x AS (SELECT * FROM users WHERE city='seattle' AND age > 30 ORDER BY age, desc) INSERT (...) INTO List FROM x. И где тут несколько строк запроса и обработки?

                    Если уж хотите разрушительные аргументы приводить, то хотя бы не используйте SQL в качестве примера.


  1. svezhest
    22.05.2024 09:34

    То, что каждая третья подсказка идеально верная - достижение, молодцы!

    Какие показатели на других языках, например, Python?

    Кроме того, по расстоянию Левенштейна, качество подсказки, у которой ошибка в последнем символе, будет такое же, как и той, у которой ошибка во втором символе с начала строки. Однако для разработчика время на исправления таких ошибок будет различаться


    1. RuslanGavriliak
      22.05.2024 09:34

      Спасибо) В скором времени опубликуем статистику по большему числу языков, причем оцениваться уже будут подсказки на основании как верхнего, так и нижнего контекста, а так же контекста из других файлов в проекте. Это лишь наши первые результаты, которыми мы решили поделиться.
      По поводу замечания:
      Все верно, более того, если ошибка, скажем, в трех символах подряд, ее исправить легче, чем ошибку в трех символах на разнесенных позициях. Классическое редакционное расстояние этого не учитывает. Но при этом оно уже хорошо знакомо и обкатано во многих областях применения, для его рассчета существует эффективный алгоритм, поэтому мы и решили использовать расстояние Левенштейна с удвоенной ценой замены символа. Такое расстояние хорошо интерпретируется и в целом адекватно оценивает близость строк.


  1. stanislav888
    22.05.2024 09:34

    Всё это конечно здорово. Только, как попробовать, если нет телефона +7 ... и аккаунта в Сбербанке? GitVerse по-другому не даёт зарегистрироваться.


  1. itmind
    22.05.2024 09:34

    В планах есть Rust добавить в число поддерживаемых языков?