[Перевод] Языковая модель GPT-3 умеет объяснять код — рассказываем, как это можно использовать
За два года с момента релиза GPT-3 эту языковую модель использовали в множестве интересных задач — например, для сочинения поэзии, написания футурологических эссе и подготовки научных статей. Но как алгоритм обработки естественного языка может быть полезен программистам?
На этот вопрос в своей новой статье отвечает британский разработчик Саймон Уилисон* — директор по архитектуре Eventbrite и один из создателей веб-фреймворка Django. Среди различных вариантов применения языковой модели GPT-3 Уилисон особенно подчеркивает ее способность объяснять, что делает код. По словам специалиста, в этом GPT-3 поразительно эффективна, поскольку явно обучалась на огромном количество исходного кода.
*Обращаем ваше внимание, что позиция автора не всегда может совпадать с мнением МойОфис.
Объясняя Python
На днях разработчик Нэд Бэтчелдер поделился прекрасным фрагментом кода:
import re
TOKEN_RX = r"""(?xm)
(?P ".*?"|'.*?' )|
(?P \d*(\d\.|\.\d)\d* )|
(?P \d+ )|
(?P [_a-zA-Z][_a-zA-Z0-9]* )|
(?P [(){}:\[\]=.,+*/-] )|
( \#.*$ )|
( \s+ )
"""
def tokens(text):
for match in re.finditer(TOKEN_RX, text):
if match.lastgroup:
yield (match.lastgroup, match[0])
TEXT = """
x = 123 + "hello #99" # ignore me!
print(hello.bye[0] + 3.14, 'single')
"""
for kind, text in tokens(TEXT):
print(f"{kind:7}: {text=}")
Я добавил код в GTP-3 (вот инструкция к песочнице) и начал задавать ей вопросы.
При этом я использовал функцию GTP-3, которая позволяет после выдачи ответа ввести новый текст и в итоге получить дополнительные пояснения.
Все, что выделено ниже жирным шрифтом, — это ответы GPT-3. Остальное — мой текст.
Объясни этот код:
import re
TOKEN_RX = r"""(?xm)
(?P ".*?"|'.*?' )|
(?P \d*(\d\.|\.\d)\d* )|
(?P \d+ )|
(?P [_a-zA-Z][_a-zA-Z0-9]* )|
(?P [(){}:\[\]=.,+*/-] )|
( \#.*$ )|
( \s+ )
"""
def tokens(text):
for match in re.finditer(TOKEN_RX, text):
if match.lastgroup:
yield (match.lastgroup, match[0])
TEXT = """
x = 123 + "hello #99" # ignore me!
print(hello.bye[0] + 3.14, 'single')
"""
for kind, text in tokens(TEXT):
print(f"{kind:7}: {text=}")
Этот код определяет регулярное выражение, которое будет соответствовать различным типам токенов в текстовой строке. Различные группы, определенные в регулярном выражении, будут соответствовать различным типам токенов, таким как строки, числа с плавающей запятой, целые числа, идентификаторы, знаки препинания и комментарии. Затем код будет перебирать совпадения и выдавать тип токена и текст, который ему соответствует.
Объясни регулярное выражение:
Регулярное выражение состоит из нескольких разных групп, каждая из которых соответствует определенному типу токена. Первая группа, «string», соответствует любому тексту, заключенному в двойные или одинарные кавычки. Вторая группа, «float», соответствует любому тексту, который похож на число с плавающей запятой. Третья группа, «int», соответствует любому тексту, который выглядит как целое число. Четвертая группа, «id», соответствует любому тексту, похожему на имя переменной. Пятая группа, «punct», соответствует любому тексту, похожему на пунктуацию. Шестая группа »#.*$» соответствует любому тексту, похожему на комментарий. Седьмая группа,»\s+», соответствует любому тексту, который выглядит как пробел.
Что делает (? xm)?
Нотация (? xm) — это модификатор, который позволяет регулярному выражению размещаться на нескольких строках и содержать комментарии.
Почему он использует match.lastgroup?
Переменная match.lastgroup содержит имя последней группы, найденной с помощью регулярного выражения. Это используется для определения типа сопоставленного токена.
Итак, мы получили отличное, четкое объяснение. Но я и так знал, как работает этот код. Попробуем теперь разобраться в коде, который я не понимаю.
Объясняя JavaScript
Разработчик Крис Хейнс опубликовал блестящую демонстрацию того, как новый элемент Web Component Declarative Shadow DOM,
, (описан здесь), можно использовать для создания страницы, которая передает контент с сервера, заполняя различные области страницы по мере поступления контента — без использования JavaScript!
Демо доступно здесь; у меня оно запускается только в Chrome, для других браузеров существует версия с полифиллом.
Код полифилла можно увидеть на этой странице. Я не понимал, как он работает, и спросил у GPT-3.
Объясни этот код:
class Enamel extends HTMLElement {
attemptPolyfillDSD() {
const dsd = this.querySelector('template[shadowroot]');
if (dsd?.content) {
const mode = dsd.getAttribute('shadowroot');
this.attachShadow({ mode });
this.shadowRoot.appendChild(dsd.content);
dsd.remove();
return true;
}
return false;
}
connectedCallback() {
if (
!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot') &&
!this.attemptPolyfillDSD()
) {
const _observer = new MutationObserver(() => {
if (this.attemptPolyfillDSD()) {
_observer.disconnect();
}
});
_observer.observe(this, {
childList: true,
});
}
}
}
export default Enamel;
Этот код подменяет API Shadow DOM для старых браузеров, которые его не поддерживают.
Что ж, звучит убедительно, но информации недостаточно. Дополняю вопрос:
Как он делает это?
В коде используется MutationObserver для отслеживания изменений дочерних элементов элемента. Когда добавляется новый дочерний элемент, код проверяет, является ли он элементом с атрибутом «shadowroot». Если это так, код создает Shadow Root для элемента и добавляет к нему содержимое шаблона. Наконец, код удаляет элемент шаблона из DOM.
Отличное объяснение! Прочитав это и вернувшись к коду, я полностью понял, как он работает.
Создавая SQL
Я хотел построить SQL-запрос, который возвращал бы все строки в одной таблице, которые не были представлены в другой таблице…, но где строки использовали бы составной первичный ключ, охватывающий два столбца. Я не знал правильного синтаксиса для этой задачи.
Для начала мне нужно было создать таблицу с составным первичным ключом. Я сразу сделал запрос в GPT-3:
SQLite создает таблицу страниц, которая имеет составной первичный ключ типа, идентификатора и столбца содержимого.
CREATE TABLE pages (type TEXT, id TEXT, content TEXT, PRIMARY KEY (type, id));
Кажется, система в курсе синтаксиса SQLite SQL, что удобно. Оказывается, вы создаете составной первичный ключ, используя PRIMARY KEY (col1, col2)
в конце определения таблицы.
Я добавил вторую строку с моей версией CREATE TABLE, а затем попросил построить мне запрос:
CREATE TABLE pages_done (type TEXT, id TEXT, PRIMARY KEY (type, id));
SQL-запрос, возвращающий строки в таблице страниц, для которых нет соответствующих записей в таблице pages_done:
SELECT type, id, content FROM pages WHERE (type, id) NOT IN (SELECT type, id FROM pages_done)
Полезный ответ! Я думал, здесь подойдет шаблон WHERE ... NOT IN (SELECT
, но сомневался в синтаксисе для поиска двух столбцов одновременно.
Я был почти уверен, что вместо этого для решения задачи можно также использовать join, и поэтому спросил GPT-3:
Запрос, но с использованием join.
SELECT p.type, p.id, p.content FROM pages p LEFT JOIN pages_done pd ON p.type = pd.type AND p.id = pd.id WHERE pd.type IS NULL;
Но какой вариант лучше? Я задал еще пару вопросов:
Какой из этих двух запросов более эффективен и почему?
Первый запрос более эффективен, поскольку в нем используется WHERE с подзапросом. Второй запрос менее эффективен, поскольку использует LEFT JOIN.
Почему подзапрос более эффективен?
Подзапрос более эффективен, поскольку он возвращает только те строки, которые соответствуют условиям, указанным в пункте WHERE. LEFT JOIN возвращает все строки из левой таблицы, даже если в правой таблице нет совпадений.
Как и до этого, ответ GPT-3 звучит вполне убедительно. Но так ли все обстоит на самом деле? В моем представлении оптимизатор SQL-запросов достаточно умен, чтобы оптимизировать LEFT JOIN
и сделать его эффективным. Подозреваю, что по данному вопросу модель вводит меня в заблуждение.
И это неудивительно — по факту GPT-3 вообще ни о чем не знает. Это огромный генератор шаблонного текста. Вы не можете доверять ничему из того, что он вам сообщает, поскольку вся его задача — группировать слова в убедительно выглядящие последовательности, которые построены с учетом ранее загруженного в языковую модель текста.
Иными словами, нейросеть — превосходный источник пищи для размышлений, но для того, чтобы получать от нее реальную пользу, требуются очень глубокие знания: как в непосредственно исследуемом предмете, так и в принципах действия инструментов ИИ.
Наводящие вопросы
Получил отличный совет от data scientist Райли Гудсайда:
Когда вы даете системе подсказки, сложно избегать наводящих вопросов. Если вы спросите, какой из двух запросов более эффективен и почему, крайне маловероятно, что вы получите ответ: «Это не имеет значения», насколько бы потенциально верным не был такой ответ.
Я не учитывал этот момент. Исправлюсь в своих будущих экспериментах.
Бонус: как насчет математики?
Мне стало интересно: можно ли объяснить с помощью GPT-3 математические уравнения?
Недавно GitHub добавил рендеринг LaTeX в свою реализацию Markdown. Я взял случайную формулу с этой страницы документации.
Объясни это:
$$\left (\sum_{k=1}^n a_k b_k \right)^2 \leq \left (\sum_{k=1}^n a_k^2 \right) \left (\sum_{k=1}^n b_k^2 \right)$$
Это неравенство Коши-Шварца.
Что ж, теперь мы знаем название. Но как это работает?
Уточняю вопрос: Объясни пошагово синтаксис формулы, обособив каждую ее часть с помощью $$ (необходимость этого условия раскрою ниже):
$$\left (\sum_{k=1}^n a_k b_k \right)^2 \leq \left (\sum_{k=1}^n a_k^2 \right) \left (\sum_{k=1}^n b_k^2 \right)$$
Это неравенство Коши-Шварца.
$$\left (\sum_{k=1}^n a_k b_k \right)^2$$
Это квадрат суммы всех произведений соответствующих элементов векторов a и b.
$$\leq \left (\sum_{k=1}^n a_k^2 \right) \left (\sum_{k=1}^n b_k^2 \right)$$
Это означает, что эта сумма меньше или равна произведению сумм квадратов каждого вектора.
Вот зачем в запросе понадобился $$: благодаря этому ответ был выдан в допустимом синтаксисе GitHub Markdown. Вот отрендеренная версия моей «беседы» с нейросетью:
Объясняя файлы конфигурации
Вот еще один весьма удачный, на мой взгляд, шаблон наводящего вопроса, который я нашел:»Объясни каждую строку этого файла конфигурации:». Я опробовал этот вопрос на примере Lit из стартовых шаблонов Vite 3.0:
Объясни каждую строку этого файла конфигурации:
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: 'src/my-element.js',
formats: ['es']
},
rollupOptions: {
external: /^lit/
}
}
})
Этот файл конфигурации написан на JavaScript с использованием библиотеки ViteJS. Первая строка импортирует функцию «defineConfig» из библиотеки ViteJS. Эта функция позволяет пользователю определить объект конфигурации для процесса сборки.
Следующая строка экспортирует функцию «defineConfig» в качестве экспорта по умолчанию. Это означает, что при импорте этого файла будет импортирована только функция «defineConfig».
Функция «defineConfig» принимает один аргумент, который является объектом. Этот объект содержит конфигурацию процесса сборки. Свойство «build» содержит конфигурацию инструмента сборки. Свойство «lib» содержит конфигурацию библиотеки, которая будет собрана. Свойство «entry» содержит путь к входному файлу для библиотеки. Свойство «formats» содержит форматы, в которые будет встроена библиотека. В этом случае библиотека будет построена только в формате «es».
Свойство «rollupOptions» содержит конфигурацию инструмента Rollup. Свойство external содержит регулярное выражение, соответствующее именам зависимостей, которые следует исключить из сборки. В этом случае из сборки будут исключены все зависимости, имена которых начинаются с «lit».
Будем рады узнать в комментариях ваше мнение об использовании GPT-3 для объяснения кода и решения других нестандартных задач. Может быть, вы нашли языковой модели другое эффективное применение? А чтобы получать больше интересного контента, подписывайтесь на наш хабр-блог: мы регулярно переводим статьи зарубежных авторов и делимся экспертизой наших специалистов.