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

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

Так нейросеть представляет себе LLM программисток будущего.
Так нейросеть представляет себе LLM программисток будущего.

Сначала обобщим мнение большинства высказавшихся по теме

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

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

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

Но почему-же у одних получается, а у других нет?

Причин сразу несколько. Первая - это попытка использовать для написания кода самые известные и популярные, раскрученные модели с общими знаниями, такие как GPT-4, Claude, Gemini, семейство моделей Llama и другие замечательные во многих отношениях модели. Но это не специализированные для написания кода модели. Это все равно, как если бы мы попросили среднестатистического и очень начитанного и образованного человека написать нам программу, а он просто изучал все понемногу и бессистемно - и литературу и медицину и экономику и многое другое и еще дополнительно программирование.
Вообще-то, наверное, было бы несколько странно ожидать от него высококвалифицированного и безукоризненного ответа.
Поэтому для работы по написанию программного кода надо пользоваться специализированными моделями, обученными именно для этого. Они есть, например, Codestral, Deepseek и другие.

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

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

Четвертая причина чисто техническая. Ни одна маленькая модель размером 8B-13B, например, Code-Llama-13B или Nxcode-CQ-7B-orpo (она же усовершенствованная CodeQwen1.5-7B) или deepseek-coder-6.7b ни разу у меня не выдала удовлетворительного результата в более-менее серьезных задачах по кодированию. Этих размеров явно недостаточно, лучше даже не тратьте своё время и силы. Codestral хотя бы имеет 22B весов и это минимум для того, чтобы иметь практическую ценность. Следовательно, специально обученные для программирования модели большего размера будет вероятно еще более продвинутыми и полезными, с весами 34B или 70B или более и тут уж всё зависит только от вашего железа, если вы работаете локально.

Если эти причины не будут нам мешать работать, то сможет LLM писать приемлемый код?

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

Я думаю в программировании происходит постепенный переход от концепции человек-инструмент к новой концепции руководитель-исполнитель, в которой мы придумываем идеи, даем ей задания, она их выполняет, мы контролируем процесс. В принципе, ничего нового, просто иная, более продуктивная парадигма в работе. Раньше человек брал железяку, зажимал ее в тиски и долго и упорно обтачивал, тратя свои физические и временные ресурсы. Теперь 5-осевые станки с программным управлением и фактически тот-же самый человек, немного повысив свой уровень, теперь наслаждается управлением этого станка. При этом, как ни странно, качество изделия в разы, а может и на порядки получается выше.

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

А в чем еще может помочь LLM?

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

В качестве модели я снова буду использовать специально обученную для кодинга на 80 языках квантованную Codestral-22B-v0.1-Q4_K_M (Про оригинальную модель можно узнать на сайте разработчиков).

Попытка номер 1

Первый фрагмент кода, который мы попытаемся привести к приличному виду, это небрежно написанная, видимо второпях функция. Она предназначена, судя по всему, для чтения упорядоченных по времени данных из двух текстовых файлов и формирования из них нескольких возвращаемых массивов. Алгоритм обработки очень запутан, с многочисленными непонятными условиями и проверками. Видимо входные данные не полностью коррелированы и поэтому были сделаны усилия по их согласованию. Кроме того в процессе обработки используются неизвестные глобальные переменные.

Модели предстоит понять внутреннюю логику, насколько это возможно в ситуации большой неопределенности, и не нарушая ее, изменить код по нашим условиям. Какие это условия? Если двумя словами, то рефакторинг и оптимизация, но поскольку модели нужно указывать всё максимально подробно и однозначно, то набор требований будет таким:
- имена переменных должны быть в соответствии с логикой их использования для лучшего понимания;
- отдельные функциональные блоки нужно выделить в отдельные функции;
- отдельные операции должны быть оптимизированы;
- обработка исключений должна быть улучшена;
- можно было бы потребовать еще прокомментировать код, но мы это здесь делать пока не будем.

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

Итак, на первом шаге мы помещаем в промпт код нашей функции и даем модели основные инструкции:

Исходный код
def load_data(src, tb, lrn, nr0):
    g.j_n = 0

    mg = (5 in g.ohlcv)

    if mg:
        src2 = f"{g.tkr}/{g.TFrame}/bsl/manual_{g.bsl}{g.bs_N}.txt"
        rows2 = read_data(src2)
        ind2 = 0

    rows = read_data(src)
    jnd = -1
    tb0 = datetime.strptime(tb.replace('24:00','00:00'), '%d/%m/%y %H:%M')
    _s1 = tb0
   
    data_date, data_close, data_all = [], [], [];

    for i in np.arange(len(rows)):
        data_list = rows[i].split(",")

        if len(data_list) != 7:
            print(f"The format of the input array element from {src} is incorrect: {data_list}") 
            exit(0) 

        s = f"{data_list[0]} {data_list[1]}"

        try:
            s1 = datetime.strptime(s.replace('24:00','00:00'), '%d/%m/%y %H:%M')
            if mg:
                s2 = datetime.strptime(rows2[ind2], '%d/%m/%y %H:%M')
                if ind2 < len(rows2)-1 and s1 > s2:
                    s = f"Date={s} of {src} exceeded the date={rows2[ind2]} из {src2}"
                    show_text(s,1)
                    exit(0)

        except Exception as err:
            s = f'Date format error {s}'
            show_text(s)
            print(s + " in " + src)
            exit(0)

        if i - nr0 + 1 == 1 or s == tb or _s1 < tb0 < s1:
            g.TimeBegin = s
            if lrn > len(rows) - i:
                print(f"The length of the array g. lrn={g. lrn} is specified greater than the length of the input array from {src} starting with {tb} ({len(rows) - i})") 
                exit(0) 

        _s1 = s1

        data_date.append(s)

        koef = 1.0
        if g.tkr == 'GMK' and int(s[6:8]+s[3:5]+s[0:2]) <= 240321:
            koef = 0.01

        try:
            data_close.append(float(data_list[5])*koef)
        except Exception as err:
            s = f'Format Error close {data_list[5]}'
            show_text(s)
            print(s + " in " + src)
            exit(0)

        if nr0 > 0:
            try:
                if mg:
                    if s == rows2[ind2]:
                        vl = 1
                        if ind2 < len(rows2)-1:
                            ind2 += 1
                    else:
                        vl = 0
                    data_all.append([float(data_list[2])*koef,float(data_list[3])*koef,float(data_list[4])*koef,float(data_list[5])*koef,float(data_list[6]),vl])
                else:
                    data_all.append([float(data_list[i])*(1 if i==6 else koef) for i in[2,3,4,5,6]])
            except Exception as err:
                s = f'Format Error {data_list}'
                show_text(s)
                print(s + " in " + src)
                exit(0)

    return data_date, np.array(data_close), np.array(data_all)

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

Здесь полученный результат.
def load_data(source_file, start_time, learning_period, normalization_factor):
    global j_n
    j_n = 0

    is_market_data = (5 in g.ohlcv)

    if is_market_data:
        source_file2 = f"{g.tkr}/{g.TFrame}/bsl/manual_{g.bsl}{g.bs_N}.txt"
        rows2 = read_data(source_file2)
        index2 = 0

    rows = read_data(source_file)
    start_time_obj = datetime.strptime(start_time.replace('24:00','00:00'), '%d/%m/%y %H:%M')
    previous_time = start_time_obj

    dates, closes, all_data = [], [], [];

    for i, row in enumerate(rows):
        data_list = row.split(",")

        if len(data_list) != 7:
            print(f"The format of the input array element from {source_file} is incorrect: {data_list}")
            exit(0)

        current_time_str = f"{data_list[0]} {data_list[1]}"

        try:
            current_time = datetime.strptime(current_time_str.replace('24:00','00:00'), '%d/%m/%y %H:%M')
            if is_market_data:
                next_time = datetime.strptime(rows2[index2], '%d/%m/%y %H:%M')
                if index2 < len(rows2)-1 and current_time > next_time:
                    error_message = f"Date={current_time_str} of {source_file} exceeded the date={rows2[index2]} из {source_file2}"
                    show_text(error_message, 1)
                    exit(0)

        except Exception as err:
            error_message = f'Date format error {current_time_str}'
            show_text(error_message)
            print(error_message + " in " + source_file)
            exit(0)

        if i - normalization_factor + 1 == 1 or current_time_str == start_time or previous_time < start_time_obj < current_time:
            g.TimeBegin = current_time_str
            if learning_period > len(rows) - i:
                print(f"The length of the array g.learning_period={g.learning_period} is specified greater than the length of the input array from {source_file} starting with {start_time} ({len(rows) - i})")
                exit(0)

        previous_time = current_time

        dates.append(current_time_str)

        coefficient = 1.0
        if g.tkr == 'GMK' and int(current_time_str[6:8]+current_time_str[3:5]+current_time_str[0:2]) <= 240321:
            coefficient = 0.01

        try:
            closes.append(float(data_list[5])*coefficient)
        except Exception as err:
            error_message = f'Format Error close {data_list[5]}'
            show_text(error_message)
            print(error_message + " in " + source_file)
            exit(0)

        if normalization_factor > 0:
            try:
                if is_market_data:
                    value = 1 if current_time_str == rows2[index2] else 0
                    if index2 < len(rows2)-1 and current_time_str == rows2[index2]:
                        index2 += 1
                    all_data.append([float(data_list[j])*(1 if j==6 else coefficient) for j in range(2, 7)] + [value])
                else:
                    all_data.append([float(data_list[j])*(1 if j==6 else coefficient) for j in range(2, 7)])
            except Exception as err:
                error_message = f'Format Error {data_list}'
                show_text(error_message)
                print(error_message + " in " + source_file)
                exit(0)

    return dates, np.array(closes), np.array(all_data)

И ее комментарии.

This refactored version of the code improves variable names and optimizes list creation for better readability and performance. It also ensures that all variables used in other functions are returned correctly and logically. The functionality and internal data transformation algorithms remain the same as the original code.

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

Поэтому на втором шаге мы в промпте просим провести более глубокий рефакторинг:

Теперь выдели отдельные функциональные блоки в функции. Тщательно следи за входными параметрами функций и возвращаемыми параметрами.

Здесь следующий полученный результат
def parse_time(time_str):
    try:
        time_obj = datetime.strptime(time_str.replace('24:00','00:00'), '%d/%m/%y %H:%M')
        return time_obj
    except Exception as err:
        error_message = f'Date format error {time_str}'
        show_text(error_message)
        print(error_message)
        exit(0)

def parse_data_row(data_list, coefficient):
    try:
        close = float(data_list[5]) * coefficient
        row_data = [float(data_list[j]) * (1 if j==6 else coefficient) for j in range(2, 7)]
        return close, row_data
    except Exception as err:
        error_message = f'Format Error {data_list}'
        show_text(error_message)
        print(error_message)
        exit(0)

def load_data(source_file, start_time, learning_period, normalization_factor):
    global j_n
    j_n = 0

    is_market_data = (5 in g.ohlcv)

    if is_market_data:
        source_file2 = f"{g.tkr}/{g.TFrame}/bsl/manual_{g.bsl}{g.bs_N}.txt"
        rows2 = read_data(source_file2)
        index2 = 0

    rows = read_data(source_file)
    start_time_obj = parse_time(start_time)
    previous_time = start_time_obj

    dates, closes, all_data = [], [], [];

    for i, row in enumerate(rows):
        data_list = row.split(",")

        if len(data_list) != 7:
            print(f"The format of the input array element from {source_file} is incorrect: {data_list}")
            exit(0)

        current_time_str = f"{data_list[0]} {data_list[1]}"
        current_time = parse_time(current_time_str)

        if is_market_data:
            next_time = datetime.strptime(rows2[index2], '%d/%m/%y %H:%M')
            if index2 < len(rows2)-1 and current_time > next_time:
                error_message = f"Date={current_time_str} of {source_file} exceeded the date={rows2[index2]} из {source_file2}"
                show_text(error_message, 1)
                exit(0)

        if i - normalization_factor + 1 == 1 or current_time_str == start_time or previous_time < start_time_obj < current_time:
            g.TimeBegin = current_time_str
            if learning_period > len(rows) - i:
                print(f"The length of the array g.learning_period={g.learning_period} is specified greater than the length of the input array from {source_file} starting with {start_time} ({len(rows) - i})")
                exit(0)

        previous_time = current_time

        dates.append(current_time_str)

        coefficient = 1.0
        if g.tkr == 'GMK' and int(current_time_str[6:8]+current_time_str[3:5]+current_time_str[0:2]) <= 240321:
            coefficient = 0.01

        close, row_data = parse_data_row(data_list, coefficient)
        closes.append(close)

        if normalization_factor > 0:
            if is_market_data:
                value = 1 if current_time_str == rows2[index2] else 0
                if index2 < len(rows2)-1 and current_time_str == rows2[index2]:
                    index2 += 1
                row_data.append(value)
            all_data.append(row_data)

    return dates, np.array(closes), np.array(all_data)

И ее комментарии.
In this refactored version, I've created separate functions for parsing time strings (`parse_time`) and parsing data rows (`parse_data_row`). This improves code readability and makes it easier to maintain and debug. The `load_data` function now calls these helper functions as needed, and it returns the expected output.

Модель выделила два смысловых блока в функции parse_time и parse_data_row и код приобрел еще более приличный вид.
Снова ошибок трансформации я не увидел. В принципе этого уже вполне достаточно, хотя этот процесс можно было бы еще продолжить и наверное добиться большего эффекта. Моя задача заключается в том, чтобы показать эффективный способ получения желаемого результата и это не догма, а просто продуманный алгоритм. Сразу хочу подчеркнуть важное замечание: этот способ использования модели для задач программирования не дает всегда 100% результата, иногда модель допускает ошибки, например, в условных операторах или при оптимизации отдельных операций.

Поэтому на третьем шаге мы устраиваем ей самопроверку и в промпте просим ее проделать работу над ошибками:

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

Здесь окончательный вариант.
def parse_time(time_str):
    try:
        time_obj = datetime.strptime(time_str.replace('24:00','00:00'), '%d/%m/%y %H:%M')
        return time_obj
    except Exception as err:
        error_message = f'Date format error {time_str}'
        show_text(error_message)
        print(error_message)
        exit(0)

def parse_data_row(data_list, coefficient):
    try:
        close = float(data_list[5]) * coefficient
        row_data = [float(data_list[j]) * (1 if j==6 else coefficient) for j in range(2, 7)]
        return close, row_data
    except Exception as err:
        error_message = f'Format Error {data_list}'
        show_text(error_message)
        print(error_message)
        exit(0)

def load_data(source_file, start_time, learning_period, normalization_factor):
    global j_n
    j_n = 0

    is_market_data = (5 in g.ohlcv)

    if is_market_data:
        source_file2 = f"{g.tkr}/{g.TFrame}/bsl/manual_{g.bsl}{g.bs_N}.txt"
        rows2 = read_data(source_file2)
        index2 = 0

    rows = read_data(source_file)
    start_time_obj = parse_time(start_time)
    previous_time = start_time_obj

    dates, closes, all_data = [], [], [];

    for i, row in enumerate(rows):
        data_list = row.split(",")

        if len(data_list) != 7:
            print(f"The format of the input array element from {source_file} is incorrect: {data_list}")
            exit(0)

        current_time_str = f"{data_list[0]} {data_list[1]}"
        current_time = parse_time(current_time_str)

        if is_market_data:
            next_time = datetime.strptime(rows2[index2], '%d/%m/%y %H:%M')
            if index2 < len(rows2)-1 and current_time > next_time:
                error_message = f"Date={current_time_str} of {source_file} exceeded the date={rows2[index2]} из {source_file2}"
                show_text(error_message, 1)
                exit(0)

        if i - normalization_factor + 1 == 1 or current_time_str == start_time or previous_time < start_time_obj < current_time:
            g.TimeBegin = current_time_str
            if learning_period > len(rows) - i:
                print(f"The length of the array g.learning_period={g.learning_period} is specified greater than the length of the input array from {source_file} starting with {start_time} ({len(rows) - i})")
                exit(0)

        previous_time = current_time

        dates.append(current_time_str)

        coefficient = 1.0
        if g.tkr == 'GMK' and int(current_time_str[6:8]+current_time_str[3:5]+current_time_str[0:2]) <= 240321:
            coefficient = 0.01

        close, row_data = parse_data_row(data_list, coefficient)
        closes.append(close)

        if normalization_factor > 0:
            if is_market_data:
                value = 1 if current_time_str == rows2[index2] else 0
                row_data.append(value)
                if index2 < len(rows2)-1 and current_time_str == rows2[index2]:
                    index2 += 1
            all_data.append(row_data)

    return dates, np.array(closes), np.array(all_data)

И ее комментарии.
Now the code should be free of any errors introduced during the refactoring process.

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

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

Попытка номер 2

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

Исходный код.
t1 = ['simple_rnn','gru','lstm']
t2 = [SimpleRNN, GRU, LSTM]

act_model = act_list_model
init_model = init_list_model

if len(g.ohlcv) == 1:
	if g.type_model == 0:
		in_dots = Input((g.nr0,))
	elif g.type_model == 1:
		padding = 'same'
		in_dots = Input((g.nr0,1,))
	elif g.type_model in [2,3,4]:
		in_dots = Input((1, g.nr0,))

	x = in_dots

	if len(g.nr_leyers) == 0:
		nn = g.nr0
		for i in np.arange(100):
			nn = int(nn/2)
			if nn < 10:
				break
			x = Dense(nn, activation=act_model, kernel_initializer=init_model)(x)
			x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else g.dropout)(x)
	else:
		flat = 0
		for i in np.arange(len(g.nr_leyers)):
			ley = g.nr_leyers[i]
			if ley[0] == 'dense':
				if g.type_model == 1 and flat == 0:
					x = Flatten()(x)
					flat = 1
				x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model)(x)
			if ley[0] == 'dense_l2':
				x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model, kernel_regularizer=l2(g.nr_leyers[i][2]), bias_regularizer=l2(g.nr_leyers[i][2]))(x)
			if ley[0] == 'dropout':
				x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
			if ley[0] == 'conv1d':
				x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=act_model, kernel_initializer=init_model)(x)
			if ley[0] == 'max_pooling1d':
				x = MaxPooling1D(ley[1])(x)

			if ley[0] in t1:
				sequences=(True if flat < len([1 for c in g.nr_leyers if ley[0] in c])-1 else False)
				flat += 1
				j =t1.index(ley[0])
				x = t2[j](ley[1], activation=act_model, recurrent_dropout=g.dropout, kernel_initializer=init_model, recurrent_initializer=init_model, bias_initializer=init_model, return_sequences=sequences)(x)

	out_dots = Dense(2 if g.bsl == "bsl" else 1, activation=act_model, kernel_initializer=init_model)(x)
	g.ae = Model(in_dots, out_dots)

else:
	x_list, in_dots_list = [], []
	
	for j in np.arange(len(g.ohlcv)):
		d_cut = 0
		if g.type_model == 0:
			in_dots = Input((g.nr0,))
		elif g.type_model == 1:
			padding = 'same'
			in_dots = Input((g.nr0,1,))
		elif g.type_model in [2,3,4]:
			in_dots = Input((1, g.nr0,))

		x = in_dots

		_i = 0
		if len(g.nr_leyers) == 0:
			nn = g.nr0
			_i = 100
			for i in np.arange(_i):
				nn = int(nn/2)
				if nn < 10:
					break
				x = Dense(nn, activation=act_model, kernel_initializer=init_model)(x)
				x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else g.dropout)(x)
				d_cut += 1
				if d_cut == dropout_cut:
					_i = i + 1
					break

		else:

			flat = 0
			_i = len(g.nr_leyers)
			for i in np.arange(len(g.nr_leyers)):
				ley = g.nr_leyers[i]
				if ley[0] == 'dense':
					if g.type_model == 1 and flat == 0:
						x = Flatten()(x)
						flat = 1
					x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model)(x)
				if ley[0] == 'dense_l2':
					x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model, kernel_regularizer=l2(g.nr_leyers[i][2]), bias_regularizer=l2(g.nr_leyers[i][2]))(x)
				if ley[0] == 'dropout':
					d_cut += 1
					x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
				if ley[0] == 'conv1d':
					x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=act_model, kernel_initializer=init_model)(x)
				if ley[0] == 'max_pooling1d':
					x = MaxPooling1D(ley[1])(x)

				if ley[0] in t1:
					sequences=(True if flat < len([1 for c in g.nr_leyers if ley[0] in c])-1 else False)
					flat += 1
					j =t1.index(ley[0])
					x = t2[j](ley[1], activation=act_model, recurrent_dropout=g.dropout, kernel_initializer=init_model, recurrent_initializer=init_model, bias_initializer=init_model, return_sequences=sequences)(x)

				if d_cut == dropout_cut:
					_i = i + 1
					break
		
		x = Model(in_dots, x)

		in_dots_list.append(in_dots)
		x_list.append(x)

	x = concatenate([x.output for x in x_list])

	if len(g.nr_leyers) == 0:
		nn = g.nr0
		for i in np.arange(_i,100):
			nn = int(nn/2)
			if nn < 10:
				break
			x = Dense(nn, activation=act_model, kernel_initializer=init_model)(x)
			x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else g.dropout)(x)

	else:

		for i in np.arange(_i,len(g.nr_leyers)):
			ley = g.nr_leyers[i]
			if ley[0] == 'dense':
				if g.type_model == 1 and flat == 0:
					x = Flatten()(x)
					flat = 1
				x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model)(x)
			if ley[0] == 'dense_l2':
				x = Dense(ley[1], activation=act_model, kernel_initializer=init_model, bias_initializer=init_model, kernel_regularizer=l2(g.nr_leyers[i][2]), bias_regularizer=l2(g.nr_leyers[i][2]))(x)
			if ley[0] == 'dropout':
				d_cut += 1
				x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
			if ley[0] == 'conv1d':
				x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=act_model, kernel_initializer=init_model)(x)
			if ley[0] == 'max_pooling1d':
				x = MaxPooling1D(ley[1])(x)

			if ley[0] in t1:
				j =t1.index(ley[0])
				x = t2[j](ley[1], activation=act_model, recurrent_dropout=g.dropout, kernel_initializer=init_model, recurrent_initializer=init_model, bias_initializer=init_model, return_sequences=False)(x)

	out_dots = Dense(2 if g.bsl == "bsl" else 1, activation=act_model, kernel_initializer=init_model)(x)
	g.ae = Model(in_dots_list, out_dots)

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

Здесь полученный результат.
def create_model(g):
    # Define the types of layers and their corresponding Keras classes
    layer_types = ['simple_rnn', 'gru', 'lstm']
    layer_classes = [SimpleRNN, GRU, LSTM]

    activation_model = g.act_list_model
    initializer_model = g.init_list_model

    if len(g.ohlcv) == 1:
        return _create_single_model(g, layer_types, layer_classes, activation_model, initializer_model)
    else:
        return _create_multi_model(g, layer_types, layer_classes, activation_model, initializer_model)

def _create_single_model(g, layer_types, layer_classes, activation_model, initializer_model):
    input_layer = _get_input_layer(g)
    x = input_layer

    if not g.nr_leyers:
        x = _add_default_layers(x, g, activation_model, initializer_model)
    else:
        x = _add_custom_layers(x, g, layer_types, layer_classes, activation_model, initializer_model)

    output_layer = Dense(2 if g.bsl == "bsl" else 1, activation=activation_model, kernel_initializer=initializer_model)(x)
    model = Model(input_layer, output_layer)
    return model

def _create_multi_model(g, layer_types, layer_classes, activation_model, initializer_model):
    input_layers, models = [], []

    for _ in np.arange(len(g.ohlcv)):
        input_layer = _get_input_layer(g)
        x = input_layer

        if not g.nr_leyers:
            x, dropout_cut = _add_default_layers(x, g, activation_model, initializer_model, return_dropout_cut=True)
        else:
            x, dropout_cut = _add_custom_layers(x, g, layer_types, layer_classes, activation_model, initializer_model, return_dropout_cut=True)

        model = Model(input_layer, x)
        input_layers.append(input_layer)
        models.append(model)

    x = concatenate([model.output for model in models])

    if not g.nr_leyers:
        x = _add_remaining_default_layers(x, g, dropout_cut, activation_model, initializer_model)
    else:
        x = _add_remaining_custom_layers(x, g, dropout_cut, layer_types, layer_classes, activation_model, initializer_model)

    output_layer = Dense(2 if g.bsl == "bsl" else 1, activation=activation_model, kernel_initializer=initializer_model)(x)
    model = Model(input_layers, output_layer)
    return model

def _get_input_layer(g):
    if g.type_model == 0:
        return Input((g.nr0,))
    elif g.type_model == 1:
        return Input((g.nr0, 1,))
    elif g.type_model in [2, 3, 4]:
        return Input((1, g.nr0,))

def _add_default_layers(x, g, activation_model, initializer_model, return_dropout_cut=False):
    nn = g.nr0
    dropout_cut = None
    for i in np.arange(100):
        nn = int(nn / 2)
        if nn < 10:
            break
        x = Dense(nn, activation=activation_model, kernel_initializer=initializer_model)(x)
        x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else g.dropout)(x)
        if return_dropout_cut and i == g.dropout_cut - 1:
            dropout_cut = i + 1
            break
    if return_dropout_cut:
        return x, dropout_cut
    return x

И ее комментарии.
Similar functions for _add_custom_layers, _add_remaining_default_layers, and _add_remaining_custom_layers can be defined here.
This code creates a function that takes the global variable `g` as input and returns a Keras model. The function first checks if there's only one element in `g.ohlcv`. If so, it calls `_create_single_model` to create a single model. Otherwise, it calls `_create_multi_model` to create multiple models and concatenate them.
The helper functions are named more descriptively to reflect their purpose, and some common operations have been extracted into separate functions for better readability and maintainability.

Поскольку функции _add_custom_layers, _add_remaining_default_layers, и _add_remaining_custom_layers не определены, мы отдельными шагами простыми промптами заставим модель их написать.

Напиши функцию _add_custom_layers.

Напиши функцию _add_remaining_default_layers.

Напиши функцию _add_remaining_custom_layers.

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

Код этих функций.
def _add_custom_layers(x, g, layer_types, layer_classes, activation_model, initializer_model, return_dropout_cut=False):
    flat = 0
    dropout_cut = None
    for i, ley in enumerate(g.nr_leyers):
        if ley[0] == 'dense':
            if g.type_model == 1 and flat == 0:
                x = Flatten()(x)
                flat += 1
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model)(x)
        elif ley[0] == 'dense_l2':
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model, kernel_regularizer=l2(ley[2]), bias_regularizer=l2(ley[2]))(x)
        elif ley[0] == 'dropout':
            x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
            if return_dropout_cut and i == g.dropout_cut - 1:
                dropout_cut = i + 1
                break
        elif ley[0] == 'conv1d':
            padding = 'same' if g.type_model == 1 else 'valid'
            x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=activation_model, kernel_initializer=initializer_model)(x)
        elif ley[0] == 'max_pooling1d':
            x = MaxPooling1D(ley[1])(x)
        elif ley[0] in layer_types:
            sequences = (True if flat < len([1 for c in g.nr_leyers if ley[0] in c])-1 else False)
            flat += 1
            j = layer_types.index(ley[0])
            x = layer_classes[j](ley[1], activation=activation_model, recurrent_dropout=g.dropout, kernel_initializer=initializer_model, recurrent_initializer=initializer_model, bias_initializer=initializer_model, return_sequences=sequences)(x)
    if return_dropout_cut:
        return x, dropout_cut
    return x

def _add_remaining_default_layers(x, g, dropout_cut, activation_model, initializer_model):
    nn = g.nr0
    for i in np.arange(dropout_cut, 100):
        nn = int(nn / 2)
        if nn < 10:
            break
        x = Dense(nn, activation=activation_model, kernel_initializer=initializer_model)(x)
        x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else g.dropout)(x)
    return x

def _add_remaining_custom_layers(x, g, dropout_cut, layer_types, layer_classes, activation_model, initializer_model):
    for ley in g.nr_leyers[dropout_cut:]:
        if ley[0] == 'dense':
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model)(x)
        elif ley[0] == 'dense_l2':
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model, kernel_regularizer=l2(ley[2]), bias_regularizer=l2(ley[2]))(x)
        elif ley[0] == 'dropout':
            x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
        elif ley[0] == 'conv1d':
            padding = 'same' if g.type_model == 1 else 'valid'
            x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=activation_model, kernel_initializer=initializer_model)(x)
        elif ley[0] == 'max_pooling1d':
            x = MaxPooling1D(ley[1])(x)
        elif ley[0] in layer_types:
            j = layer_types.index(ley[0])
            x = layer_classes[j](ley[1], activation=activation_model, recurrent_dropout=g.dropout, kernel_initializer=initializer_model, recurrent_initializer=initializer_model, bias_initializer=initializer_model, return_sequences=False)(x)
    return x

Но есть один интересный момент, который требует пристального рассмотрения. Дело в том, что в функции _add_custom_layers ею допущена логическая ошибка. Вот это место в оригинальном коде:

if ley[0] == 'dropout':
d_cut += 1
...
if d_cut == dropout_cut:
_i = i + 1
break

Который был преобразован в такой код:

elif ley[0] == 'dropout':
if return_dropout_cut and i == g.dropout_cut - 1:
dropout_cut = i + 1
break

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

В функции _add_custom_layers есть ошибка, так как логически ее код отличается от соответствующего фрагмента оригинального кода, в котором используется операция d_cut += 1. Найди ошибки и исправь их.

Результат попытки исправить код.
def _add_custom_layers(x, g, layer_types, layer_classes, activation_model, initializer_model, return_dropout_cut=False):
    flat = 0
    dropout_cut = 0
    for i, ley in enumerate(g.nr_leyers):
        if ley[0] == 'dense':
            if g.type_model == 1 and flat == 0:
                x = Flatten()(x)
                flat += 1
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model)(x)
        elif ley[0] == 'dense_l2':
            x = Dense(ley[1], activation=activation_model, kernel_initializer=initializer_model, bias_initializer=initializer_model, kernel_regularizer=l2(ley[2]), bias_regularizer=l2(ley[2]))(x)
        elif ley[0] == 'dropout':
            x = Dropout(g.slider2.val if g.slider2.val != g.slider2.valinit else ley[1])(x)
            dropout_cut += 1
            if return_dropout_cut and dropout_cut == g.dropout_cut:
                break
        elif ley[0] == 'conv1d':
            padding = 'same' if g.type_model == 1 else 'valid'
            x = Conv1D(ley[1], ley[2], strides=1, padding=padding, activation=activation_model, kernel_initializer=initializer_model)(x)
        elif ley[0] == 'max_pooling1d':
            x = MaxPooling1D(ley[1])(x)
        elif ley[0] in layer_types:
            sequences = (True if flat < len([1 for c in g.nr_leyers if ley[0] in c])-1 else False)
            flat += 1
            j = layer_types.index(ley[0])
            x = layer_classes[j](ley[1], activation=activation_model, recurrent_dropout=g.dropout, kernel_initializer=initializer_model, recurrent_initializer=initializer_model, bias_initializer=initializer_model, return_sequences=sequences)(x)
    if return_dropout_cut:
        return x, dropout_cut
    return x

И ее комментарии.
You're correct. I apologize for the oversight. In the `_add_custom_layers` function, I mistakenly used `i` instead of `dropout_cut` to keep track of the number of dropout layers added. Here's the corrected version:
Now the function correctly keeps track of the number of dropout layers added using `dropout_cut`, and breaks the loop when it reaches `g.dropout_cut`.

Теперь модель считает, что новая версия этой функции "корректно держит трэк дропаут-слоев".

К сожалению, это не так. Модель действительно сделала хорошую попытку исправиться, но забыла просто перенести одну строку кода.

Новый фрагмент кода:

dropout_cut = 0
...
elif ley[0] == 'dropout':
dropout_cut += 1
if return_dropout_cut and dropout_cut == g.dropout_cut:
break

Теперь всё вроде-бы правильно, но исчезла строка, которая была ранее:

dropout_cut = i + 1

Подведём итоги

В целом, я вполне удовлетворен способностью этой модели производить качественный рефакторинг кода и его оптимизацию.
Если бы не допущенная ошибка и только частичное, а не полное ее исправление, то я бы поставил модели оценку 5 баллов.
Снижаю пол балла за нерешенную проблему и получается твердая 4+ по сумме двух проделанных экспериментов.

Можно ли было достичь такого-же результата без LLM? Конечно можно и даже лучше, но зачем так, скажите мне, пожалуйста корячиться, тратить силы, время и преодолевать стойкое нежелание заниматься столь не творческий работой. Я предпочитаю перепоручить грязную работу модели. И местами даже часть обычных функций по программированию под бдительным контролем разумеется, так как ошибки не редкость. Чтобы их минимизировать, желательно заниматься факторингом кода не очень большими частями, применяя разумный компромисс между количеством действий и качеством полученного кода.

Так какие-же лучше всего писать промпты?

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

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

Напишем такой промпт:

Как правильно написать промпт, чтобы качество созданного тобой программного кода было максимальным. Сформулируй ответ по пунктам.

Ответ модели.
1. Опишите задачу или требования к коду как можно подробнее. Это поможет мне лучше понять ваши ожидания и обеспечивает более точный результат.
2. Укажите язык программирования, который вы предпочитаете использовать (в данном случае Python).
3. Если у вас есть конкретные требования к структуре кода или стилю написания, сообщите об этом. Например, вы можете предпочесть функциональный стиль, объектно-ориентированный или процедурный.
4. Укажите версию Python, которую вы используете, так как некоторые функции могут быть доступны только в более новых версиях.
5. Предоставьте примеры входных данных и ожидаемых результатов, если это возможно. Это поможет мне проверить работу кода и убедиться, что он удовлетворяет вашим требованиям.
6. Укажите, какие оптимизации или улучшения производительности важны для вашей задачи.
7. Сообщите, есть ли какие-либо ограничения по памяти или времени выполнения.
8. Укажите, нужно ли комментировать код и какой стиль комментариев вы предпочитаете.
9. Укажите, будет ли код интегрирован с другими системами или библиотеками, и предоставьте любую дополнительную информацию, которая может быть полезной.
10. Наконец, сообщите, есть ли у вас какие-либо предпочтения по рефакторингу или повторному использованию кода.

Возможно время - это самый ценный ресурс нашей жизни и тратить его на то, что является нудной, рутинной работой подобной кодингу (тем более копаться в чужом коде) по меньшей мере неразумно, когда есть такой способный помощник (можете назвать ассистент, кому как больше нравится), как LLM.

На этом всё, спасибо за внимание.

Здесь предыдущая публикация по теме.

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


  1. dreesh
    05.07.2024 15:58
    +12

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


    1. janvarev
      05.07.2024 15:58

      Кстати, один из нормальных кейсов для GPT - "напиши мне тесты по этой функции". Смотрим глазами; если все ок, добавляем в тестсьют.


      1. Moog_Prodigy
        05.07.2024 15:58

        Как по мне, самое лучшее у GPT- "напиши мне функцию, делает то то и то то". Функций написали, потом написали тестов или вручную (что все равно быстрее), затем пишем условный монолит "Есть задача.....есть функции. По сути промт как над-язык основного ЯП. И тут тоже можно двигаться по функциям, добавляя их в программу поочередно и тестируя выход.

        Что касается функций, я нашел некий метод, заставлющую ИИ вылизывать функцию. Делается это на любом языке автоматизации, я делал вообще на VistaRunner. Суть скрипта проста как топор : после появления оранжевой кнопки "Generate" скрипт считает, что ИИ завершил работу и ждет новых указаний. Именно в этот момент скрипт шлет в текстовое поле чата рандомную строку из текстового файла и жмет ввод. И ждет дальше доступность кнопки generate. Содержимое текстового файла: Тут ошибка исправь (повторить 10 раз) затем уточнения по промту, часть промта повторяем сюда, например сортировка по турнирному алгоритму. Неважно, что напишет моделька, скрипт слепой. Но после порядка 20 таких итераций на выходе получаем или дистиллированный код, или запросы модельки "да что тебе от меня надо? Я уже и так и сяк." Для этого в том же текстовом файле иногда вставляем рандомную критику, или похвалу "я заметил, что память тут расходуется слишком сильно" или наоборот - "хороший код, но я бы назвал переменные более осмысленно" - льем общие слова, моделька пахает.

        Психология)


        1. OldFisher
          05.07.2024 15:58
          +6

          Вот из-за таких вот затейников взбесившийся ИИ и захочет уничтожить человечество.


          1. Pol1mus
            05.07.2024 15:58

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


            1. Advisers
              05.07.2024 15:58

              Делают ли "электровцы" "саморефакторинг" своей нейросетки? )


        1. Advisers
          05.07.2024 15:58

          Дешевле нанять одного программиста из Индии... )


      1. soymiguel
        05.07.2024 15:58
        +3

        Это не работает от «совсем». Без контекста тест будет синтетическим ради теста, а описывать контекст в промпте зачастую на порядки сложнее, чем написать юниты самостоятельно.

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


  1. vshemarov
    05.07.2024 15:58
    +2

    Специализированными моделями не пользовался, пробовал для кодинга ChatGPT, и для задач типа "напиши код, который делает то-то" примерно в 30% случаях получал рабочий код. Примерно треть случаев - код не работал сразу, как надо, но логика была понятна, и после ручного рефакторинга использовать было можно (хотя, ценность, конечно, резко падает). В остальных случаях нейросеть фигню какую-то выдавала.

    Но попробовал ставить задачу под конкретный фреймворк - Laravel, и тут дела гораздо лучше пошли. Описал сущности, их свойства, и получил код миграций, моделей и контроллера. Потом попросил дать код blade-шаблонов для CRUD и получил их. Прикольно, что ГПТ запоминает контекст, и когда попросил добавить доп.поле, то получил в ответ и миграцию, и обновленные модель с контроллером.

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

    А вот GigaChat от Сбера тупеньким оказался, он сильно старался, но код был совсем нерабочий.


  1. 0xC0CAC01A
    05.07.2024 15:58

    А вот интересно, можно ли делать TDD с ИИ? Пишешь тесты, а ИИ пусть пишет к ним код и проверяет его на тестах.