Lerne künstliche Intelligenz, um ein Spiel zu spielen

Guten Tag, lieber Leser!

In diesem Artikel werden wir ein neuronales Netzwerk entwickeln, das ein speziell dafür erstelltes Spiel auf einem guten Niveau bestehen kann.



Hinweis: In diesem Artikel werden weder der Begriff " neuronales Netzwerk " und alles, was damit zusammenhängt, erläutert, noch werden grundlegende Informationen zum Netzwerktraining mithilfe der Trace-Methode bereitgestellt . Wir empfehlen Ihnen, sich kurz mit diesen Konzepten vertraut zu machen, bevor Sie diesen Artikel lesen.

Start: Beschreibung der Spielregeln


Ziel ist es, möglichst viele Kreise mit grünem Rand zu fangen und Kreise mit rotem Rand zu vermeiden.

Bedingungen:

  • Auf dem Feld fliegen doppelt so viele rote Kreise wie grüne Kreise;
  • Grüne Kreise bleiben im Feld, rote Kreise fliegen aus dem Feld und werden eliminiert.
  • Grüne und rote Kreise können kollidieren und sich mit ihrer eigenen Art abstoßen.
  • Spieler - Ein gelber Ball auf dem Bildschirm kann sich innerhalb des Feldes bewegen.

Nach dem Berühren verschwindet der Ball und der Spieler erhält die entsprechenden Punkte.

Weiter: KI entwerfen


Rezeptoren


Damit die KI entscheiden kann, wohin der Spieler bewegt werden soll, müssen Sie zuerst Umgebungsdaten abrufen. Erstellen Sie dazu spezielle Scanner , die gerade Linien sind. Der erste Scanner befindet sich in einem Winkel von 180 Grad zur unteren Feldgrenze.

Es wird 68 geben: Die ersten 32 - reagieren auf Bomben (rote Kreise), die nächsten 32 - reagieren auf Äpfel (grüne Kreise) und die letzten 4 - empfangen Daten über die Position der Feldgrenzen relativ zum Spieler. Nennen wir diese 68 Scanner die Eingangsneuronen des zukünftigen neuronalen Netzwerks (Rezeptorschicht).

Die Länge der ersten 64 Scanner beträgt 1000px, die restlichen 4 entsprechen der Aufteilung des Feldes in zwei Hälften entsprechend der entsprechenden Symmetriefläche
Bild AI-Bereich (1/4)
Bild

In der Abbildung: Geben Sie Neuronen ein, die Werte von Scannern empfangen
Die Werte auf den Scannern sind normalisiert, d.h. reduziert auf den Bereich [0, 1] und je näher das den Scanner kreuzende Objekt am Player ist, desto größer ist der Wert, der an den Scanner übertragen wird.

Algorithmus zum Empfangen von Daten durch Scanner und deren Implementierung auf JS
Der Scanner ist also direkt. Wir haben die Koordinaten eines seiner Punkte (Spielerkoordinaten) und den Winkel relativ zur OX-Achse. Mit den trigonometrischen Funktionen sin und cos können wir den zweiten Punkt erhalten.

Von hier erhalten wir den Richtungsvektor der Linie, was bedeutet, dass wir die kanonische Form der Gleichung dieser Linie konstruieren können.

Um den Wert auf dem Scanner zu erhalten, müssen Sie prüfen, ob eine Kugel diese Linie schneidet. Dies bedeutet, dass Sie die Gleichung der Linie in einer parametrischen Form darstellen und all dies nacheinander für jeden Kreis für das Feld in die Kreisgleichungen einsetzen müssen.

Wenn wir diese Substitution auf eine allgemeine Form bringen, erhalten wir eine parametrische Gleichung in Bezug auf a, b und c, wobei diese Variablen die Koeffizienten der quadratischen Gleichung sind, beginnend mit dem Quadrat.

Wir laden den Leser ein, sich mit diesem Algorithmus anhand der einfachsten Definitionen der linearen Algebra genauer vertraut zu machen
Unten ist der Code der Scanner

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; //  } } 

Neuronales Netzwerkdesign


Daher war es üblich, den Algorithmus der Verfolgung (Referenzpfade) zum Trainieren von NS zu verwenden.
Hinweis: Um den Lernprozess zu vereinfachen, wird die Konstruktion von Matrizen weggelassen.

Weiter:

  1. Am Ausgang des Netzwerks präsentieren wir 8 Neuronen, die für die Richtungen verantwortlich sind: das erste ist links, das zweite ist links + oben, das dritte ist oben, das vierte ist oben + rechts, das fünfte ist rechts und so weiter;
  2. Ein positiver Wert für ein Neuron bedeutet, dass Sie sich in die Richtung bewegen sollten, die damit verbunden ist, und negativ - das Gegenteil;
  3. Für uns ist es wichtig, die Werte von Scannern, die sich im gleichen Winkel zum unteren Rand des Feldes befinden, irgendwie zu kombinieren. Da bei einem negativen Ausgang die KI aus dieser Richtung abstößt, führen wir eine zusätzliche Schicht (versteckt zwischen Eingang und Ausgang) ein, die die Werte von Neuronen, die die entsprechenden Scanner paarweise widerspiegeln, addiert und die Werte von roten Neuronen mit einem "-" übernimmt.
  4. Offensichtlich wird die Bewegung nach links hauptsächlich durch die Informationen beeinflusst, die sich links vom Spieler befinden (nicht nur, sondern dies wird später berücksichtigt (*)) - das heißt, wir verbinden 8 Neuronen der verborgenen Schicht auf der linken Seite mit dem Neuron, das für die Bewegung nach links verantwortlich ist. Wir stellen die Verbindungen wie folgt her: Einem Neuron, das einem parallel zur Feldgrenze befindlichen Scanner entspricht, wird ein Gewicht von 1 zugewiesen, dann wird zwei benachbarten Neuronen ein Gewicht von 0,95 zugewiesen, benachbarten Neuronen ein Gewicht von 0,9, und schließlich wird dem letzten in diesem Sektor das Gewicht 0,8 zugewiesen;
  5. Wir berücksichtigen auch die Grenzneuronen: Wir ziehen Kommunikationen von diesen Grenzneuronen zu den Ausgaben, die die Bewegung der Grenzen beeinflussen können.
  6. Dasselbe gilt für die verbleibenden sieben Neuronen der Ausgangsschicht.

Nachdem wir den obigen Algorithmus abgeschlossen haben, erhalten wir das in der folgenden Abbildung gezeigte neuronale Netzwerk

Bild

* Aber das ist noch nicht alles. Es ist wichtig, das Ergebnis richtig zu interpretieren, nämlich (Y ist ein Array von Ausgangsneuronen):
  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; 


Daher haben wir das neuronale Netzwerk, das der KI des Spielers zugrunde liegt, auf dem GIF oben im Artikel erstellt und automatisch trainiert

Der Code ist verfügbar (Implementierung des Spiels und der KI) unter dem Link: Link zum Github Ivan753 / Learning

Das ist alles Vielen Dank für Ihre Aufmerksamkeit!

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


All Articles