Код программ отличается от естественного языка из-за его формализма и строгости, однако ничто не мешает воспринимать его как последовательность токенов и работать с ним, как с обычным языком. Существуют исследования, которые показали, что модель BERT, обученная на большом наборе данных, неплохо справляется с некоторыми задачами, связанными с обработкой программного кода. В этом посте я буду решать задачу автогенерации комментариев к нему. Вы узнаете, как подготовить данные для обучения, настроить нейросеть и получить результат.
Данные
Данные представлены в виде набора пар [функция — комментарий] для различных языков программирования (awesome Code Search Net Challenge dataset). Кстати говоря, этот набор изначально был создан не для этой задачи, однако его можно легко перепрофилировать под свои нужды.
Данные |
Цель |
public string getlhsbindingtype(final string var) { if (this.lhs == null) { return null; } for (int i = 0; i < this.lhs.length; i++) { string type = getlhsbindingtype(this.lhs[i], var); if (type != null) { return type; } } return null; } |
get the data-type associated with the binding |
Я не стану очищать данные, это описано здесь. Я же буду использовать уже предварительно обработанные данные в объеме 1 % от общего количества образцов в наборе, так как обучение модели занимает довольно много времени. Но, как можно будет убедиться в будущем, генерация комментариев даже на 1 % данных выглядит неплохо. Если у вас есть время и ресурсы, можете обучить модель на всём наборе и получить результаты получше.
CodeBERT
Предварительно обученная модель, которую я буду использовать, взята из статьи исследовательского подразделения Microsoft. В этой модели также использовался набор данных CodeSearchNet, но вместо генерирования комментариев он использовался для обучения модели на основе RoBERTa удобному для восприятия представлению кода и естественного языка. Использование больших языковых моделей для представления текста удобным способом в настоящее время является обычной практикой, поскольку они показали свою эффективность для решения других задач.
Загрузка, установка и импортирование библиотек
!pip install transformers !git clone -q https://github.com/microsoft/CodeXGLUE.git import json from dataclasses import dataclass import numpy as np import pandas as pd from transformers import AutoTokenizer
Здесь я прописываю пути до файлов с данными и оборачиваю их в структуру для более удобного дальнейшего использования:
PATH_TO_TRAIN_DATA = '/content/train.csv' PATH_TO_TEST_DATA = '/content/test.csv' PATH_TO_VALIDATION_DATA = '/content/validation.csv' #validation, test and train data_struct = { 'train' : pd.read_csv(PATH_TO_TRAIN_DATA), 'test' : pd.read_csv(PATH_TO_TEST_DATA), 'valid' : pd.read_csv(PATH_TO_VALIDATION_DATA) }
Инициализирую две вспомогательные функции: токенизации текста и записи DataFrame в JSON-файл, так как именно в таком формате требуется подавать данные для модели.
} def write_into_json_file(json_file_name: str, data: pd.DataFrame): ''' json_file_name - name output json file data - pandas data frame write your pandas data to json file ''' with open(json_file_name, 'w') as current_file: for index, current_row in data.iterrows(): current_file.write(json.dumps(current_row.to_dict()) + '\n') def split_data(split_column: str, new_column: str, data: pd.DataFrame)-> pd.DataFrame: ''' split items in column data - your pandas data frame split_column - column in your pd.df ''' data[new_column] = data[split_column].apply(lambda current_item: current_item.split()) return data
Реализую небольшую предобработку данных с помощью функций, описанных выше:
#preproc data for type_data, value in data_struct.items(): #split target colums code_tokens_step = split_data('code', 'code_tokens', value) docs_tokens_step = split_data('comment', 'docstring_tokens', code_tokens_step) data_struct[type_data] = docs_tokens_step #create json file write_into_json_file(f'/content/{type_data}.jsonl', data_struct[type_data])
Далее создаю конфигурационный класс для модели и на его основе прописываю всю конфигурацию:
@dataclass class ConfigurationModel: learning_rate : float batch_size : int beam : int test_file : str source_size : int target_size : int path_to_data_directory : str path_to_output_data_directory : str train_file : str dev_file : str count_epochs : int pretrained_model : str configuration_codetext_model = ConfigurationModel( learning_rate = 5e-5, batch_size = 8, beam = 10, source_size = 256, target_size = 512, path_to_data_directory = '.', path_to_output_data_directory = 'model_for_java', train_file = '/content/train.jsonl', dev_file = '/content/valid.jsonl', test_file = '/content/test.jsonl', count_epochs = 10, pretrained_model = 'microsoft/codebert-base', ) configuration_codetext_model
Обучение
Теперь, когда данные обработаны и представлены в удобном формате, можно приступать к обучению. Сделаю этоОбучу модель на обучающей выборке. В качестве метрики использую BLEU-4 (четвёрка означает, что количество словесных n-gram = 4), которая распределена от 0 до 1, но в нашем примере будет использоваться BLEU-4 * 100%. Эта метрика используется в задачах машинного перевода, но и для генерации текста она также хорошо подходит. Если брать задачи машинного перевода, то даже для человека bleu = [0.6:0.7] — отличный результат, потому что каждый человек может перевести текст по-разному. Точности в единицу достигнуть почти невозможно.
Если посмотреть на исходную задачу, то, во-первых, модель должна сгенерировать текст, а во-вторых, это не просто текст, а осмысленный комментарий к коду. Поэтому ожидать больших значений метрики bleu не стоит.
#run train model !python /content/CodeXGLUE/Code-Text/code-to-text/code/run.py \ --do_train \ --do_eval \ --do_lower_case \ --model_type roberta \ --model_name_or_path {configuration_codetext_model.pretrained_model} \ --train_filename {configuration_codetext_model.train_file} \ --dev_filename {configuration_codetext_model.dev_file} \ --output_dir {configuration_codetext_model.path_to_output_data_directory} \ --max_source_length {configuration_codetext_model.source_size} \ --max_target_length {configuration_codetext_model.target_size} \ --beam_size {configuration_codetext_model.beam} \ --train_batch_size {configuration_codetext_model.batch_size} \ --eval_batch_size {configuration_codetext_model.batch_size} \ --learning_rate {configuration_codetext_model.learning_rate} \ --num_train_epochs {configuration_codetext_model.count_epochs}
Обучение
После обучения модели её можно проверить на отдельной выборке:
binary_model_file = '/content/model_for_java/checkpoint-best-bleu/pytorch_model.bin' !python /content/CodeXGLUE/Code-Text/code-to-text/code/run.py \ --do_test \ --model_type roberta \ --model_name_or_path microsoft/codebert-base \ --load_model_path {binary_model_file} \ --dev_filename {configuration_codetext_model.dev_file} \ --test_filename {configuration_codetext_model.test_file} \ --output_dir {configuration_codetext_model.path_to_output_data_directory} \ --max_source_length {configuration_codetext_model.source_size} \ --max_target_length {configuration_codetext_model.target_size} \ --beam_size {configuration_codetext_model.beam} \ --eval_batch_size {configuration_codetext_model.batch_size}
Как можно увидеть, bleu-4 = 11, и это неплохо для такой задачи, даже с учётом того, что bleu в нашем случае распределена от 0 до 100.
Далее считаю получившиеся результаты:
path_to_gold = '/content/model_for_java/test_1.gold' path_to_output = '/content/model_for_java/test_1.output'
Инициализирую функцию считывания из txt-файла:
def read_result_txt_file(txt_file: str)-> list: with open(txt_file) as file:return [' '.join(line.rstrip().replace('\t', ' ').split(' ')[1:]) for line in file]
И для удобства считаю всё в DataFrame:
def read_result_txt_file(txt_file: str)-> list: #true comments and predicted true_sent = read_result_txt_file(path_to_gold) pred_sent = read_result_txt_file(path_to_output) result_data_frame = pd.DataFrame( { 'code' : data_struct['test']['code'], 'true' : true_sent, 'pred' : pred_sent } ) result_data_frame.head(10)
Вывод 10 примеров кода, оригинальных комментариев и комментариев, сгенерированных моделью.
Теперь попробую субъективно сравнить оригинальный комментарий со сгенерированным по шкале от 1 до 5. Code — исходный код, true — исходный комментарий, pred — сгенерированный.
Пример 1:
Code: public t includeas(final class template) { blacklist = false; string[] properties = getallbeanpropertynames(template, false); include(properties); return _this(); }
True: defines included property names as public properties of given template class. sets to black list mode.
Pred: create a new resource
Оценка: 1 — абсолютно непонятно, о чём идёт речь.
Пример 2:
Code: int setdirect(int v0, int v1, int v2, int v3, int v4) { return offset + v0*stride0 + v1*stride1 + v2*stride2 + v3*stride3 + v4*stride4; }
True: experimental : should be package private
Pred: sets the value for the specified point.
Оценка: 4 — исходный комментарий абсолютно никак не отражает функциональность, в отличие от сгенерированного.
Пример 3:
Code: static private servicetype checkifdap4(string location) throws ioexception { // strip off any trailing dap4 prefix if (location.endswith(«.dap»)) location = location.substring(0, location.length() — «.dap».length()); else if (location.endswith(«.dmr»)) location = location.substring(0, location.length() — «.dmr».length()); else if (location.endswith(«.dmr.xml»)) location = location.substring(0, location.length() — «.dmr.xml».length()); else if (location.endswith(«.dsr»)) location = location.substring(0, location.length() — «.dsr».length()); try (httpmethod method = httpfactory.get(location + «.dmr.xml»)) { int status = method.execute(); if (status == 200) { header h = method.getresponseheader(«content-type»); if ((h != null) && (h.getvalue() != null)) { string v = h.getvalue(); if (v.startswith(«application/vnd.opendap.org»)) return servicetype.dap4; } } if (status == httpstatus.sc_unauthorized || status == httpstatus.sc_forbidden) throw new ioexception(«unauthorized to open dataset » + location); // not dods return null; } }
True: check for dmr
Pred: returns true if the given name is valid.
Оценка: 5 — попадание в точку.
Пример 4:
Code: public void setsize(dimension newsize) { if ( newsize != null ) { size.setsize( newsize ); firepropertychange( size_prop, null, size ); } }
True: set the size of this vertex. will not update the size if newsize is null.
Pred: sets the initializes the size of the shape.
Оценка: 3 — в целом, комментарии схожи, но вместо vertex используется shape, и в сгенерированном комментарии не отражено условие, которое прописано в оригинальном.
Пример 5:
Code: protected bufferedimage createbufferedimage(int w, int h, int imgtype) { bufferedimage bi = null; if (imgtype == 0) { bi = (bufferedimage) createimage(w, h); } else if ((imgtype > 0) && (imgtype < 14)) { bi = new bufferedimage(w, h, imgtype); } else if (imgtype == 14) { bi = createbinaryimage(w, h, 2); } else if (imgtype == 15) { bi = createbinaryimage(w, h, 4); } else if (imgtype == 16) { bi = createsgisurface(w, h, 32); } else if (imgtype == 17) { bi = createsgisurface(w, h, 16); } // store the buffered image size biw = w; bih = h; return bi; }
True: generates a fresh buffered image of the appropriate type.
Pred: creates a new image.
Оценка: 2 — в исходном комментарии сказано, что генерируется новое буферное изображение определённого типа, в сгенерированном такие уточнения отсутствуют.
Пример 6:
Code: public orientgraph gettx() { final orientgraph g; if (pool == null) { g = (orientgraph) gettxgraphimplfactory().getgraph(getdatabase(), user, password, settings); } else { // use the pool g = (orientgraph) gettxgraphimplfactory().getgraph(pool, settings); } initgraph(g); return g; }
True: gets transactional graph with the database from pool if pool is configured. otherwise creates a graph with new db instance. the graph instance inherits the factory’s configuration.
Pred: get the graph for the graph.
Оценка: 1 — очень краткое и в то же время неверное описание.
Пример 7:
Code: public boundingbox getboundingbox(long geopackageid, string tablename) { boundingbox boundingbox = null; cursor result = db.rawquery(«select min(» + geometrymetadata.column_min_x + «), min(» + geometrymetadata.column_min_y + «), max(» + geometrymetadata.column_max_x + «), max(» + geometrymetadata.column_max_y + «) from » + geometrymetadata.table_name + » where » + geometrymetadata.column_geopackage_id + » = ? and » + geometrymetadata.column_table_name + » = ?», new string[]{string.valueof(geopackageid), tablename}); try { if (result.movetonext()) { boundingbox = new boundingbox(result.getdouble(0), result.getdouble(1), result.getdouble(2), result.getdouble(3)); } } finally { result.close(); } return boundingbox; }
True: query for the bounds of the feature table index
Pred: get the bounding box.
Оценка: 3 — в целом, суть похожа.
Пример 8:
Code: public static list<element> getelements(stage stage, iterable<? extends module> modules) { recordingbinder binder = new recordingbinder(stage); for (module module : modules) { binder.install(module); } binder.scanforannotatedmethods(); for (recordingbinder child : binder.privatebinders) { child.scanforannotatedmethods(); } // free the memory consumed by the stack trace elements cache stacktraceelements.clearcache(); return collections.unmodifiablelist(binder.elements); }
True: records the elements executed by
Pred: returns the list of the given
Оценка: 3 — в целом, суть похожа.
Пример 9:
Code: static proofnode<owlaxiom> canconvertstep(proofstep<owlaxiom> step) { if (step.getname() != elkclassinclusionexistentialcomposition.name) { return null; } list<? extends proofnode<owlaxiom>> premises = step.getpremises(); proofnode<owlaxiom> lastpremise = premises.get(premises.size() — 1); collection<? extends proofstep<owlaxiom>> lastpremisesteps = lastpremise .getinferences(); if (lastpremisesteps.size() != 1) { return null; } // else for (proofstep<owlaxiom> lastpremisestep : lastpremisesteps) { if (lastpremisestep .getname() == elkpropertyinclusionoftransitiveobjectproperty.name) { return lastpremisestep.getpremises().get(0); } } // else return null; }
True: checks if is derived by inference where the last premise is derived from
Pred: determine if the expression has been cleared.
Оценка: 2 — не очень похоже на правду, но сгенерированный комментарий вполне осмысленный.
Пример 10:
Code: public string asjsonstring(object o) { if ( getcoderspecific() instanceof jsonfactory == false ) { return «can be called on jsonconfiguration only»; } else { return new string(asbytearray(o), standardcharsets.utf_8); } }
True: utility/debug method. use «asbytearray» for programmatic use as the byte array will already by utf-8 and ready to be sent on network.
Pred: convert a json string to a json string.
========================================
Code: private void notifylisteners(string str) { writerlistener[] writerlisteners = null; synchronized (listeners) { writerlisteners = new writerlistener[listeners.size()]; listeners.toarray(writerlisteners); } for (int i = 0; i < writerlisteners.length; i++) { writerlisteners[i].write(str); } }
True: notify that a new string has been written.
Pred: notifies all listeners.
Оценка: 1 — модель не смогла уловить суть.
Средняя субъективная оценка: (1+4+5+3+2+1+3+3+2+1)/10 = 2,5 — вполне неплохой результат для модели, которая училась на 1 % от общего объёма обучающих данных. В целом, суть сгенерированных комментариев понятна, но если у вас есть ресурсы, чтобы обучить модель более чем на 1 % набора, то вы можете улучшить результат.
Заключение
Я показал, что после обучения модели даже на 1 % данных она выполняет свою цель и может вполне адекватно генерировать комментарии к коду. Также продемонстрирована предварительная обработка текста для языка Java. Если модель будет использоваться в исследовании целой кодовой базы, то лучше её всё же обучить на всех данных.
Также следует сказать, что если обучить модель на большем объёме, то её можно встроить в IDE (VisualStudio, PyCharm и т.д.) Подробнее об этом можно посмотреть здесь.
ссылка на оригинал статьи https://habr.com/ru/companies/sberbank/articles/737730/
Добавить комментарий