NLP技术可以用来生成实际的代码吗?我们离人工智能被用来编写软件的世界还有多远?
在这个博客中,我尝试构建一个python代码生成器,可以将简单的英语问题语句转换为相应的python代码。
我们把这个问题当作一个序列对序列(Seq2Seq)的学习问题来解决。在这里,我们的英语句子将是我们的输入或SRC序列,而Python代码将是我们的输出或TRG序列。
在过去的几年里,Transformer已经成为解决Seq2Seq问题的主流架构。当今大多数sota模型(如BERT或GPT-3)都在内部使用Transformer。
Transformer,正如我们今天所知道的那样,是谷歌在其最早推出的 “Attention Is All You Need” 论文。这个是“English-to-Python” 模型也可以在该论文中找到原理。
在我们开始解决问题之前,让我们先简要回顾一下Transformer。
Transformer可以从三个部分来理解:
在我以前的博客中,我已经详细解释了每一个组件中以及代码演练:https://ai.plainenglish.io/lets-pay-attention-to-transformers-a1c2dc566dbd
现在让我们看看如何将数据输入到Transformer中。
我们将使用一个由人工智能学院(TSAI)策划的定制数据集来训练我们的模型。这个数据集包含大约5000个数据点,其中每个数据点包含一个英语问题语句及其相应的Python代码。你可以按照我的代码参考来理解如何解析数据。
https://github.com/divyam96/English-to-Python-Converter
示例数据点:
英文声明:“write a function that adds two numbers”
Python代码:
def add_two_numbers (num1 ,num2 ):
sum =num1 +num2
return sum
在这里“英语声明” 是我们的输入,“Python代码” 是我们训练的输出或TRG序列。
我们的输入(SRC)和输出(TRG)序列以单个字符串的形式存在,需要进一步标识以发送到Transformer模型中。
为了对输入(SRC)序列进行标识化,我们使用了spacy。
Input = data.Field(tokenize = 'spacy',
init_token='<sos>',
eos_token='<eos>',
lower=True)
为了标识输出(TRG)序列,我们使用基于Python源代码标识器构建自定义标识器。Python的标识器为每个标识返回几个属性。
我们只以元组的形式提取标识类型和相应的字符串属性(即,(token_type_int, token_string))作为最后的标识。
标识化输入(SRC):
SRC = [' ', 'write', 'a', 'python', 'function', 'to', 'add', 'two', 'user', 'provided', 'numbers', 'and', 'return', 'the', 'sum']
标识化输出(TRG):
TRG = [(57, 'utf-8'), (1, 'def'), (1, 'add_two_numbers'), (53, '('), (1, 'num1'), (53, ','), (1, 'num2'), (53, ')'), (53, ':'), (4, '\n'), (5, ' '), (1, 'sum'), (53, '='), (1, 'num1'), (53, '+'), (1, 'num2'), (4, '\n'), (1, 'return'), (1, 'sum'), (4, ''), (6, ''), (0, '')]
由于我们的数据集仅包含5000个数据点,因此我们使用数据扩充来增加数据集的大小。
在对python代码进行标识化时,我们随机屏蔽某些变量的名称(使用‘变量1,‘变量2’ 等等)以确保我们训练的模型不仅仅关注变量的命名方式,而且实际上试图理解python代码的内在逻辑和语法。
例如,考虑以下程序。
def add_two_numbers (num1 ,num2 ):
sum =num1 +num2
return sum
我们可以替换上面的一些变量来创建新的数据点。以下是有效的扩充。
1
def add_two_numbers (var_1 ,num2 ):
sum =var_1 +num2
return sum
2
def add_two_numbers (num1 ,var_1 ):
sum =num1 +var_1
return sum
3
def add_two_numbers (var_1 ,var_2 ):
sum = var_1 + var_2
return sum
在上面的示例中,我们使用随机变量替换技术将单个数据点扩展为3个以上的数据点。
我们在生成TRG标识时实现了我们的扩充。
在随机选取变量来屏蔽时,我们避免使用关键字文字(keyword.kwlist)控件结构(如下面的skip_list所示)和对象属性。我们将所有需要跳过的文本添加到skip_list中。
import random
import io
import keyword
from tokenize import tokenize
def augment_tokenize_python_code(python_code_str, mask_factor=0.3):
var_dict = {} # 存储被屏蔽变量的字典
# 某些保留字不应该被当作普通变量
# 因此需要从我们的可变屏蔽参数中跳过
skip_list = ['range', 'enumerate', 'print', 'ord', 'int', 'float', 'zip'
'char', 'list', 'dict', 'tuple', 'set', 'len', 'sum', 'min', 'max']
skip_list.extend(keyword.kwlist)
var_counter = 1
python_tokens = list(tokenize(io.BytesIO(python_code_str.encode('utf-8')).readline))
tokenized_output = []
for i in range(0, len(python_tokens)):
if python_tokens[i].type == 1 and python_tokens[i].string not in skip_list:
if i>0 and python_tokens[i-1].string in ['def', '.', 'import', 'raise', 'except', 'class']: # 避免屏蔽模块、函数和错误字面量
skip_list.append(python_tokens[i].string)
tokenized_output.append((python_tokens[i].type, python_tokens[i].string))
elif python_tokens[i].string in var_dict: # 变量已经被屏蔽
tokenized_output.append((python_tokens[i].type, var_dict[python_tokens[i].string]))
elif random.uniform(0, 1) > 1-mask_factor: # 随机屏蔽变量
var_dict[python_tokens[i].string] = 'var_' + str(var_counter)
var_counter+=1
tokenized_output.append((python_tokens[i].type, var_dict[python_tokens[i].string]))
else:
skip_list.append(python_tokens[i].string)
tokenized_output.append((python_tokens[i].type, python_tokens[i].string))
else:
tokenized_output.append((python_tokens[i].type, python_tokens[i].string))
return tokenized_output
我们现在使用Pytorch的torchtext.data.Field字段.
Output = data.Field(tokenize = augment_tokenize_python_code,
init_token='<sos>',
eos_token='<eos>',
lower=False)
应用标识化后的标识化输出(TRG):
TRG = [(57, 'utf-8'), (1, 'def'), (1, 'add_two_numbers'), (53, '('), (1, 'num1'), (53, ','), (1, 'var_1'), (53, ')'), (53, ':'), (4, '\n'), (5, ' '), (1, 'sum'), (53, '='), (1, 'num1'), (53, '+'), (1, 'var_1'), (4, '\n'), (1, 'return'), (1, 'sum'), (4, ''), (6, ''), (0, '')]
为了将数据输入到我们的模型中,我们首先使用Pytorch的torchtext.data.BucketIterator工具. 这样可以确保具有相似长度的输入在单个批次中保持在一起,以便于训练。
然后我们将标识化输入(SRC)批输入到编码器中,并在解码器中使用标识化输出(TRG)批。我们的目标是使用编码器的标识化英语输入(SRC)通过解码器预测标识化Python输出(TRG)。
然后,通过Python源代码标识器的untokenize函数来取消标识化预测。
由于我们在数据集中使用了数据增强,这意味着我们的模型可以预测一个特定变量的各种值,只要预测在整个代码中是一致,那它们都是正确的。
这意味着我们的训练标签不是很确定,因此用概率1- smooth_eps来对待它们是正确的和不正确的会更有意义。这就是标签平滑所做的。
现在设置好所有组件后,我们可以使用反向传播来训练模型。我们将数据集分为训练数据和验证数据。我们的模型经过训练,直到验证损失不再改善。
值得注意的是,与不使用标签平滑的模型相比,标签平滑会导致更高的损失值。但这是意料之中的,因为我们不打算确定我们的标签预测。变量的情况尤其如此,因为只要通过目标代码序列的预测是一致的,就可以有多个正确的选项。
有关完整的训练代码,请参阅我的笔记本:https://github.com/divyam96/English-to-Python-Converter/blob/main/English_to_Python.ipynb
输入:“program to sort a list of dictionaries by key”
输出:
var_1 ={'Nikhil':{'roll':24 ,'marks':17 },
'Akshat':{'roll':54 ,'marks':12 },
'Akash':{'roll':15 },'marks':15 }}
sort_key ='marks'
res ='marks'
res =var_2 (test_dict .items (),key =lambda x :x [1 ][sort_key ])
print ("The sorted dictionary by marks is : "+str (res ))
输入:“function to sum odd elements of list”
输出:
def sum_odd_elements (l :list ):
return sum ([i for i in l if i %2 ==1 ])
输入:“program to reverse a string”
输出:
var_1 ='Today is bad day'
var_1 [::-1 ]
更多的例子请看我的项目仓库:https://github.com/divyam96/English-to-Python-Converter
我们已经成功地训练了一个模型,它能够将简单的问题语句(英语)转换成相应的python代码。
参考代码
https://github.com/divyam96/English-to-Python-Converter
https://github.com/bentrevett/pytorch-seq2seq
参考引用
[1] Aston Zhang and Zachary C. Lipton and Mu Li and Alexander J. Smola, Dive into Deep Learning(2020).
[2] Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, Illia Polosukhin, Attention Is All You Need (2017), 31st Conference on Neural Information Processing Systems (NIPS 2017), Long Beach, CA, USA
[3] Rafael Müller, Simon Kornblith, Geoffrey Hinton, When Does Label Smoothing Help?(2019), 33rd Conference on Neural Information Processing Systems (NeurIPS 2019), Vancouver, Canada
[4] Ian Goodfellow and Yoshua Bengio and Aaron Courville, Deep Learning Book(2016), MIT Press.