Python:使用循环神经网络构建 AI 程序员 (1)
近年来,循环神经网络 (RNN) 受到了广泛关注,因为它在许多自然语言处理任务中显示出了巨大的前景。 尽管它们很受欢迎,但解释如何使用最先进的工具实现简单而有趣的应用程序的教程数量有限。在本系列中,我们将使用循环神经网络来训练 AI 程序员,该程序员可以像真正的程序员一样编写 Java 代码(希望如此)。将涵盖以下内容:
1. 构建一个简单的 AI 程序员(这篇文章)
2. 改进 AI 程序员 - 使用令牌
3. 改进 AI 程序员 - 使用不同的网络结构
这篇文章展示了构建 LSTM 神经网络并使用它生成 Java 代码的步骤。如果你按照帖子进行操作,只需单击一下即可运行代码。(但作为第一步,你需要设置深度学习的开发环境。你可以按照这篇文章(https://www.w3cschool.cn/article/23984127.html) 展示了设置工作环境的最佳和最简单的方法。
本系列的目标是为深度学习提供一个切入点。构建深度学习模型就像画一幅油画。你可以在开始后立即改进模型并让你的第一个模型工作。
1. 获取训练原始数据
我使用 JDK 的源代码作为训练数据。可在此处获得。我们正在构建一个序列到序列的预测模型,输入序列是字符序列。每个 .java 文件都会被扫描并聚合到一个名为“jdk-chars.txt”
的文件中。此外,注释被忽略,因为我们希望 AI 程序员学习如何编码。注释会使数据变得嘈杂。(查看这篇文章以了解如何删除评论。)为方便起见,聚合文件包含在此项目的 GitHub 存储库中。你可以在这篇文章的末尾找到链接。
以下代码读取 jdk-chars.txt
并将其切片以适合我的桌面的硬件功能。就我而言,我只使用了代码中显示的代码的 20%。
path = "./jdk-chars.txt"
text = open(path).read()
slice = len(text)/5
slice = int(slice)
# slice the text to make training faster
text = text[:slice]
print('# of characters in file:', len(text))
2. 建立索引以寻址字符
LSTM 输入只能理解数字,所以首先我们需要为每个字符分配一个唯一的整数。
例如,如果代码中有 65 个唯一字符,我们为 65 个字符中的每个字符分配一个数字。下面的代码用 [“{” : 0 ] [ “a” : 1 ], ... ]
这样的条目构建了一个字典。还生成反向字典用于解码 LSTM 的输出。
chars = sorted(list(set(text)))
print('# of unique chars:', len(chars))
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
3. 准备带标签的训练序列
接下来,我们需要准备带有标签的训练数据。X
是我们定义的特定长度的序列(在我的例子中是 40
),y
是序列的下一个字符。例如,从以下行:
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; ... ...
X
的样本是
int weekOfYear = isSet(WEEK_OF_YEAR) ? fi
y
是下一个字符
e
在这里,我们将文本剪切为 40
个字符的冗余序列。
NUM_INPUT_CHARS = 40
STEP = 3
sequences = []
next_chars = []
for i in range(0, len(text) - NUM_INPUT_CHARS, STEP):
sequences.append(text[i: i + NUM_INPUT_CHARS])
next_chars.append(text[i + NUM_INPUT_CHARS])
print('# of training samples:', len(sequences))
我们正在尝试构建一个具有如下结构的网络:
4. 向量化训练数据
准备好训练数据后,需要将其转换为向量。由于我们在第二步中准备了char_indices
和indices_char
,下面的代码可以轻松地将我们的训练数据转换为使用 one-hot
编码的向量。例如,索引为 11
的字符将是所有 0
和位置 11
处的 1
的向量。
print('Vectorize training data')
X = np.zeros((len(sequences), NUM_INPUT_CHARS, len(chars)), dtype=np.bool)
y = np.zeros((len(sequences), len(chars)), dtype=np.bool)
for i, sequence in enumerate(sequences):
for t, char in enumerate(sequence):
X[i, t, char_indices[char]] = 1
y[i, char_indices[next_chars[i]]] = 1
5. 构建单层 LSTM 模型
下面的代码定义了神经网络的结构。该网络包含一层具有 128
个隐藏单元的 LSTM。所述input_shape
参数指定输入序列长度(NUM_INPUT_CHARS
)和输入的每个时间(唯一的字符,即,尺寸),在尺寸。
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(NUM_INPUT_CHARS, len(chars))))
model.add(Dense(len(chars)))
model.add(Activation('softmax'))
optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)
print(model.summary())
最后的 Dense()
层是一个带有 softmax
激活的输出层,允许对输入向量进行 len(chars)
方式分类。在训练期间,反向传播时间从输出层开始,因此选择优化器 = rmsprop
可以起到重要作用。注意 LSTM 是 Keras 中的输出层。
Optimizer
是优化函数。如果你不知道这个术语,你可能熟悉逻辑回归中常用的优化函数——随机梯度下降。这是类似的事情。
最后一行指定了成本函数。在这种情况下,我们使用“categorical_crossentropy
”。
6.训练模型和生成Java代码
sample
函数用于从概率数组中采样一个索引。例如,给定 preds=[0.5,0.2,0.3]
和默认温度,该函数将以概率 0.5
返回索引 0
,以概率 0.2
返回索引 1
,或以概率 0.3
返回索引 2
。它用于避免一遍又一遍地生成相同的序列。我们希望看到 AI 程序员可以编写的一些不同的代码序列。
# train the model, output generated text after each iteration
for iteration in range(1, 60):
print()
print('-' * 50)
print('Iteration', iteration)
model.fit(X, y, batch_size=128, epochs=1)
start_index = random.randint(0, len(text) - NUM_INPUT_CHARS - 1)
for diversity in [0.2, 0.5, 1.0, 1.2]:
print()
print('----- diversity:', diversity)
generated = ''
sequence = text[start_index: start_index + NUM_INPUT_CHARS]
generated += sequence
print('----- Generating with seed: "' + sequence + '"')
sys.stdout.write(generated)
for i in range(400):
x = np.zeros((1, NUM_INPUT_CHARS, len(chars)))
for t, char in enumerate(sequence):
x[0, t, char_indices[char]] = 1.
preds = model.predict(x, verbose=0)[0]
next_index = sample(preds, diversity)
next_char = indices_char[next_index]
generated += next_char
sequence = sequence[1:] + next_char
sys.stdout.write(next_char)
sys.stdout.flush()
print()
7. 结果
训练模型需要几个小时。最后生成的代码如下所示:
----- diversity: 1.2 ----- Generating with seed: "eak positions used by next() // and prev" eak positions used by next() // and previos < als.get[afip(lookupFDataNtIndexPesicies- > = nuls.simys); } e.apfwn 0; for; rerendenus = contaroyCharset() : Attch ; margte.adONamel = getScale(); i { int exponentace = sed, off endexpVal.vilal = 0, break; localicIntLullAtper.sudid); } void fam(); ; if (offset: b = t); if (false; private byte[] is(-notren} fig ist[(i = 0) molInd); if (end < = mame") inie = torindLotingenFiols.INFGNTR_FIELD_(ne
生成的代码没有多大意义,甚至没有编译。但是我们仍然可以看到 LSTM 捕获了一些单词和语法。例如,“void fam();
”。你还可以查看在早期迭代中生成的代码。他们的意义不大。
如果调整参数(如 NUM_INPUT_CHARS
和 STEP
)并训练更长时间,可能会得到更好的结果。随意尝试。我没时间了,想发表这篇文章。更重要的是,我知道完成这项工作的更好方法,将在下一篇文章中展示。
8. 下一步是什么?
在这篇文章中,我使用字符序列作为输入来训练模型,模型预测字符序列。除了转换基本LSTM神经网络的参数,我们还可以使用token代替字符,使用不同的网络结构。我们将在下一篇文章中探讨这些。