Aprende inteligencia artificial para jugar un juego

Buen día, querido lector!

En este artículo desarrollaremos una red neuronal que puede pasar un juego creado especialmente para él a un buen nivel.



Nota: este artículo no explica el término " red neuronal " y todo lo relacionado con él, ni proporciona información básica sobre el entrenamiento de la red utilizando el método de rastreo . Le recomendamos que se familiarice brevemente con estos conceptos antes de leer este artículo.

Inicio: descripción de las reglas del juego.


El objetivo es atrapar tantos círculos con un borde verde como sea posible, evitando círculos con rojo.

Términos:

  • El doble de círculos rojos vuela en el campo que los círculos verdes;
  • Los círculos verdes permanecen dentro del campo, los círculos rojos salen volando del campo y se eliminan;
  • Los círculos verdes y rojos pueden chocar y repeler con su propia especie;
  • Jugador: una pelota amarilla en la pantalla puede moverse dentro del campo.

Después de tocar la pelota desaparece, y el jugador recibe los puntos correspondientes.

Siguiente: diseñando IA


Receptores


Para que la IA pueda decidir dónde mover al jugador, primero debe obtener datos ambientales. Para hacer esto, cree escáneres especiales que sean líneas rectas. El primer escáner está ubicado en un ángulo de 180 grados con respecto al límite inferior del campo.

Habrá 68: los primeros 32 - responden a las bombas (círculos rojos), los siguientes 32 - responden a las manzanas (círculos verdes), y los últimos 4 - reciben datos sobre la ubicación de los límites del campo en relación con el jugador. Llamemos a estos 68 escáneres las neuronas de entrada de la futura red neuronal (capa receptora).

La longitud de los primeros 64 escáneres es de 1000 px, los 4 restantes corresponden a dividir el campo por la mitad de acuerdo con la cara de simetría correspondiente
imagen Alcance AI (1/4)
imagen

En la figura: neuronas de entrada que reciben valores de escáneres
Los valores en los escáneres están normalizados, es decir reducido al rango [0, 1], y cuanto más cerca esté el objeto que cruza el escáner al jugador, mayor será el valor transmitido al escáner.

Algoritmo para recibir datos por escáneres y su implementación en JS
Entonces, el escáner es directo. Tenemos las coordenadas de uno de sus puntos (las coordenadas del jugador) y el ángulo relativo al eje OX, podemos obtener el segundo punto usando las funciones trigonométricas sin y cos.

A partir de aquí obtenemos el vector de dirección de la línea, lo que significa que podemos construir la forma canónica de la ecuación de esta línea.

Para obtener el valor en el escáner, debe ver si alguna bola se cruza con esta línea, lo que significa que debe presentar la ecuación de la línea en forma paramétrica y sustituir todo esto en las ecuaciones circulares, secuencialmente para cada círculo para el campo.

Si llevamos esta sustitución a una forma general, obtenemos una ecuación paramétrica con respecto a a, byc, donde estas variables son los coeficientes de la ecuación cuadrática, comenzando desde el cuadrado, respectivamente.

Invitamos al lector a familiarizarse con este algoritmo con más detalle utilizando las definiciones más simples de álgebra lineal.
A continuación se muestra el código de los escáneres.

Ball.scaners: { // length: 1000, i: [], //   get_data: function(){ //Ball.scaners.get_data /     var angl = 0; var x0 = Ball.x, var y0 = Ball.y; var l = Ball.scaners.length; for(let k = 0; k < 32; k++){ x1 = l*Math.cos(angl), y1 = l*Math.sin(angl); Ball.scaners.i[k] = 0; for(i = 0; i < bombs.length; i++){ if(((k >= 0) && (k <= 8) && (bombs[i].x < x0) && (bombs[i].y < y0)) || ((k >= 8) && (k <= 16) && (bombs[i].x > x0) && (bombs[i].y < y0)) || ((k >= 16) && (k <= 24) && (bombs[i].x > x0) && (bombs[i].y > y0)) || ((k >= 24) && (k <= 32) && (bombs[i].x < x0) && (bombs[i].y > y0))){ //    var x2 = bombs[i].x, y2 = bombs[i].y; var p = true; //    var pt = true; //     var t1, t2; var a = x1*x1 + y1*y1, b = 2*(x1*(x0 - x2) + y1*(y0 - y2)), c = (x0 - x2)*(x0 - x2) + (y0 - y2)*(y0 - y2) - bombs[i].r*bombs[i].r; //------------------------------    if((a == 0) && (b != 0)){ t = -c/b; pt = false; } if((a == 0) && (b == 0)){ p = false; } if((a != 0) && (b != 0) && (c == 0)){ t1 = 0; t2 = b/a; } if((a != 0) && (b == 0) && (c == 0)){ t1 = 0; pt = false; } if((a != 0) && (b == 0) && (c != 0)){ t1 = Math.sqrt(c/a); t2 = -Math.sqrt(c/a); } if((a != 0) && (b != 0) && (c != 0)){ var d = b*b - 4*a*c; if(d > 0){ t1 = (-b + Math.sqrt(d))/(2*a); t2 = (-b - Math.sqrt(d))/(2*a); } if(d == 0){ t1 = -b/(2*a); } if(d < 0){ p = false; } } //----------------------------------- if(p == true){ if(pt == true){ let x = t1*x1 + x0; let y = t1*y1 + y0; let l1 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); x = t2*x1 + x0; y = t2*y1 + y0; let l2 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); if(l1 <= l2){ Ball.scaners.i[k] += 1 - l1/(l*l); }else{ Ball.scaners.i[k] += 1 - l2/(l*l); } }else{ let x = t1*x1 + x0; let y = t1*y1 + y0; Ball.scaners.i[k] += 1 - (Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2))/(l*l); } }else{ Ball.scaners.i[k] += 0; } }else{ continue; } } angl += Math.PI/16; } //!---------------  for(k = 32; k < 64; k++){ x1 = l*Math.cos(angl), y1 = l*Math.sin(angl); Ball.scaners.i[k] = 0; for(i = 0; i < apples.length; i++){ if(((k >= 32) && (k <= 40) && (apples[i].x < x0) && (apples[i].y < y0)) || ((k >= 40) && (k <= 48) && (apples[i].x > x0) && (apples[i].y < y0)) || ((k >= 48) && (k <= 56) && (apples[i].x > x0) && (apples[i].y > y0)) || ((k >= 56) && (k <= 64) && (apples[i].x < x0) && (apples[i].y > y0))){ var x2 = apples[i].x, var y2 = apples[i].y; var p = true; //    var pt = true; //     var t1, t2; var a = x1*x1 + y1*y1, b = 2*(x1*(x0 - x2) + y1*(y0 - y2)), c = (x0 - x2)*(x0 - x2) + (y0 - y2)*(y0 - y2) - apples[i].r*apples[i].r; //------------------------------    if((a == 0) && (b != 0)){ t = -c/b; pt = false; } if((a == 0) && (b == 0)){ p = false; } if((a != 0) && (b != 0) && (c == 0)){ t1 = 0; t2 = b/a; } if((a != 0) && (b == 0) && (c == 0)){ t1 = 0; pt = false; } if((a != 0) && (b == 0) && (c != 0)){ t1 = Math.sqrt(c/a); t2 = -Math.sqrt(c/a); } if((a != 0) && (b != 0) && (c != 0)){ var d = b*b - 4*a*c; if(d > 0){ t1 = (-b + Math.sqrt(d))/(2*a); t2 = (-b - Math.sqrt(d))/(2*a); } if(d == 0){ t1 = -b/(2*a); } if(d < 0){ p = false; } } //----------------------------------- if(p == true){ if(pt == true){ let x = t1*x1 + x0; let y = t1*y1 + y0; let l1 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); x = t2*x1 + x0; y = t2*y1 + y0; let l2 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); if(l1 <= l2){ Ball.scaners.i[k] += 1 - l1/(l*l); }else{ Ball.scaners.i[k] += 1 - l2/(l*l); } }else{ let x = t1*x1 + x0; let y = t1*y1 + y0; Ball.scaners.i[k] += 1 - (Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2))/(l*l); } }else{ Ball.scaners.i[k] += 0; } }else{ continue; } } angl += Math.PI/16; } Ball.scaners.i[64] = (1000 - Ball.x) / 1000; //  Ball.scaners.i[65] = Ball.x / 1000; //  Ball.scaners.i[66] = (500 - Ball.y) / 500; //  Ball.scaners.i[67] = Ball.y / 500; //  } } 

Diseño de redes neuronales


Por lo tanto, era costumbre usar el algoritmo de rastreo (rutas de referencia) para entrenar NS.
Nota: para simplificar el proceso de aprendizaje, se omite la construcción de matrices.

Proceder:

  1. En la salida de la red, presentamos 8 neuronas que son responsables de las direcciones: la primera es izquierda, la segunda es izquierda + superior, la tercera es superior, la cuarta es superior + derecha, la quinta es derecha y así sucesivamente;
  2. Deje que un valor positivo en una neurona signifique que debe moverse en la dirección que está unida a ella y negativa: todo lo contrario;
  3. Es importante para nosotros combinar de alguna manera los valores de los escáneres que están en el mismo ángulo con respecto al borde inferior del campo. Dado que con una salida negativa, la IA rechazará desde esta dirección, introducimos una capa adicional (oculta, entre la entrada y la salida), que agregará los valores de las neuronas que reflejan los escáneres correspondientes en pares, y tomaremos los valores de las neuronas rojas con un "-";
  4. Obviamente, el movimiento hacia la izquierda se ve afectado principalmente por la información que está a la izquierda del jugador (no solo, sino que esto se tendrá en cuenta más adelante (*)), lo que significa que conectamos 8 neuronas de la capa oculta ubicadas a la izquierda con la neurona responsable de moverse hacia la izquierda. Establecemos las conexiones de la siguiente manera: a una neurona correspondiente a un escáner ubicado paralelo al límite del campo se le asigna un peso de 1, luego a dos neuronas vecinas se les asigna un peso de 0.95, las vecinas a un peso de 0.9, y finalmente, la última en este sector tiene un peso de 0.8;
  5. También tenemos en cuenta las neuronas límite: extraemos comunicaciones de esas neuronas límite a las salidas que pueden influir en el movimiento de las fronteras.
  6. Hace lo mismo con las siete neuronas restantes de la capa de salida.

Una vez completado el algoritmo anterior, obtenemos la red neuronal que se muestra en la figura a continuación

imagen

* Pero eso no es todo. Es importante interpretar correctamente el resultado, a saber (Y es una matriz de neuronas de salida):
  Ball.vx = -(Y[0] - Y[4]) + (-(Y[1] - Y[5]) + (Y[3] - Y[6]))*0.5; Ball.vy = -(Y[2] - Y[6]) + (-(Y[3] - Y[7]) + (Y[5] - Y[1]))*0.5; 


Por lo tanto, creamos y entrenamos automáticamente la red neuronal que subyace a la IA del jugador en el gif en la parte superior del artículo

El código está disponible (implementación del juego e IA) en el enlace: Enlace al github Ivan753 / Learning

Eso es todo Gracias por su atencion!

Source: https://habr.com/ru/post/es418895/


All Articles