Las redes neuronales recurrentes (RNN) están ganando mucha atención en los últimos años porque se han mostrado muy prometedoras en muchas tareas de procesamiento del lenguaje natural. A pesar de su popularidad, hay un número limitado de tutoriales que explican cómo implementar una aplicación sencilla e interesante utilizando herramientas de última generación. En esta serie, utilizaremos una red neuronal recurrente para entrenar a un programador de IA, que puede escribir código Java como un programador real (con suerte). Se cubrirá lo siguiente:
1. Construyendo un programador de IA simple (esta publicación)
2. Mejora del programador de IA: uso de tokens
3. Mejora del programador de IA: uso de diferentes estructuras de red
Esta publicación muestra los pasos para construir una red neuronal LSTM y usarla para generar código Java. Si sigue la publicación, ejecutar el código está a solo un clic de distancia. (Pero como primer paso, deberá configurar el entorno de desarrollo para el aprendizaje profundo. Puede seguir esta publicación (http://www.programcreek.com/2017/01/set-up-development-environment-for- deep-learning /) que muestra la mejor y más sencilla forma de configurar el entorno de trabajo.
El objetivo de esta serie es proporcionar un punto de entrada para el aprendizaje profundo. Construir un modelo de aprendizaje profundo es como pintar una pintura al óleo. Puede seguir mejorando el modelo tan pronto como comience y haga funcionar su primer modelo.
1. Obtener los datos brutos de entrenamiento
Estoy usando el código fuente de JDK como datos de entrenamiento. Está disponible aquí. Estamos construyendo un modelo de predicción secuencia a secuencia y la secuencia de entrada es la secuencia de caracteres. Cada archivo .java se escanea y se agrega en un archivo llamado «jdk-chars.txt». Además, los comentarios se ignoran, porque queremos que el programador de IA aprenda a codificar. Los comentarios hacen que los datos sean ruidosos. (revisa esta publicación para ver cómo eliminar comentarios). Para su conveniencia, el archivo agregado se incluye en el repositorio de GitHub de este proyecto. Puede encontrar el enlace al final de esta publicación.
El siguiente código lee el archivo jdk-chars.txt y lo corta para que se ajuste a la capacidad de hardware de mi escritorio. En mi caso, solo usé el 20% del código como se muestra en el código.
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. Índice de construcción para abordar los personajes
Las entradas de LSTM solo pueden comprender números, por lo que primero debemos asignar un número entero único a cada carácter.
Por ejemplo, si hay 65 caracteres únicos en el código, asignamos un número a cada uno de los 65 caracteres. El siguiente código crea un diccionario con entradas como [ “{” : 0 ] [ “a” : 1 ], …]. El diccionario inverso también se genera para decodificar la salida de 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. Preparar las secuencias de entrenamiento con etiquetas
A continuación, necesitamos preparar datos de entrenamiento con etiquetas. Una X es una secuencia con una longitud específica que definimos (40 en mi caso) e y es el siguiente carácter de la secuencia.
Por ejemplo, de la siguiente línea:
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; ... ...
Una muestra de X es
int weekOfYear = isSet(WEEK_OF_YEAR) ? fi
y la y es el siguiente carácter
e
Aquí cortamos el texto en secuencias redundantes de 40 caracteres.
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)) |
Estamos intentando construir una red con una estructura como esta:
4. Vectorización de datos de entrenamiento
Una vez que se preparan los datos de entrenamiento, es necesario convertirlos en vectores. Como hemos preparado char_indices y indices_char en el segundo paso, el siguiente código puede convertir fácilmente nuestros datos de entrenamiento en vectores con codificación one-hot. Por ejemplo, el carácter con índice 11 sería el vector de todos los ceros y un 1 en la posición 11.
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. Construcción de un modelo LSTM de una sola capa
El siguiente código define la estructura de la red neuronal. La red contiene una capa de LSTM con 128 unidades ocultas. los input_shape El parámetro especifica la longitud de la secuencia de entrada (NUM_INPUT_CHARS) y la dimensión de la entrada en cada momento (es decir, el tamaño de los caracteres únicos).
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()) |
La capa final de Dense () está destinada a ser una capa de salida con activación softmax, lo que permite la clasificación de vías len (chars) de los vectores de entrada. Durante el entrenamiento, la propagación hacia atrás en el tiempo comienza en la capa de salida, por lo que tiene un propósito importante con el optimizador elegido = rmsprop. LSTM está destinado a ser una capa de salida en Keras.
Optimizer es la función de optimización. Si no conoce este término, es posible que esté familiarizado con la función de optimización de uso común en regresión logística: descenso de gradiente estocástico. Es algo similar.
La última línea especifica la función de costo. En este caso, usamos ‘categorical_crossentropy’. Puede echar un vistazo esta bonita publicación para entender por qué la entropía cruzada es mejor que el error cuadrático medio (MSE) en este caso.
6. Modelo de formación y generación de código Java
La función de muestra se utiliza para muestrear un índice de una matriz de probabilidad. Por ejemplo, dados preds =[0.5,0.2,0.3] y una temperatura predeterminada, la función devolvería el índice 0 con probabilidad de 0,5, 1 con probabilidad de 0,2 o 2 con probabilidad de 0,3. Se utiliza para evitar generar la misma secuencia una y otra vez. Queremos ver algunas secuencias de código diferentes que el programador AI puede codificar.
def sample(preds, temperature=1.0): preds = np.asarray(preds).astype('float64') preds = np.log(preds) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas) |
# 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. Resultados
Se necesitan algunas horas para entrenar al modelo. Y finalmente el código generado se parece a lo siguiente:
----- diversidad: 1.2 ----- Generando con semilla: "posiciones eak usadas por next () // y prev" posiciones eak usadas por next () // y previosThe code generated does not make much sense, not even compile. But we can still see that LSTM captures some words and syntax. For example, "void fam();". You can also take a look at the code generated in the earlier iterations. They make less sense.
If you tune the parameters (such as NUM_INPUT_CHARS and STEP) and train longer, you may get better results. Feel free to try. I was running out of time and wanted to publish this post. More importantly, I know a better way to do this job which is shown in the next post.
8. What's Next?
In this post, I used characters sequences as input to train the model and the model predicts a character sequence. Other than turning the parameters of the basic LSTM neural network, we can also use tokens instead of characters and use different network structures. We will explore those in the next posts.
Source Code
1) The source code of this post is lstm_ai_coder_chars.py which is located at https://github.com/ryanlr/RNN-AI-Programmer
2) The code is modified based on the Keras example lstm_text_generation.py which is available here https://github.com/fchollet/keras/tree/master/examples.