Self-Driven Car Simulator Using a Neural Network and Genetic Algorithm Training





5.00/5 (15 votes)
This program simulates an auto-driven car learning to drive around a track.
Introduction
Artificial Intelligence impacts human life in many ways nowadays. An example is in the auto industry; many companies are trying to make their cars smarter. The cars can self-drive, avoid obstacles, find destinations … without controls from human. This paper is about a car simulation program in which the car itself will move without any controls from outside. The approach uses a Neural Network and a Genetic Algorithm to train the car by making the car learn after each time it fails to finish the track.
Using the Code
1) FPS
Every computer has different speeds so we need a mechanism to normalize that to make the game run at the same speed in any computer. We have two main methods: update
and render
. Usually, the game runs at 60 fps so the update
method will be called 60 times per second and the render
method will be called as fast as the computer’s speed.
package com.auto.car;
import java.awt.BasicStroke;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;
import com.auto.algorithm.HelperFunction;
import com.auto.car.entity.EntityManager;
import com.auto.graphics.Screen;
import com.auto.input.Keyboard;
import com.auto.input.Mouse;
/*
* @Description: This class contains main method - entry point to the program
*
*/
public class CarDemo extends Canvas implements Runnable {
private static final long serialVersionUID = 1L;
private static int width = 600;
private static int height = width * 9 / 16;
public static int scale = 2;
private Thread thread;
private JFrame frame;
private boolean running = false;
private BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
private int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer())
.getData();
private Screen screen;
private Keyboard keyboard;
private EntityManager entityManager;
public CarDemo() {
Dimension size = new Dimension(width * scale, height * scale);
setPreferredSize(size);
screen = new Screen(width, height);
frame = new JFrame();
Mouse mouse = new Mouse();
addMouseListener(mouse);
addMouseMotionListener(mouse);
keyboard = new Keyboard();
addKeyListener(keyboard);
entityManager = new EntityManager();
}
public synchronized void start() {
thread = new Thread(this, "Auto Car");
running = true;
thread.start();
}
public synchronized void stop() {
running = false;
try {
System.out.println("Goodbye");
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static int fps = 60;
@Override
public void run() {
long lastTime = System.nanoTime();
long timer = System.currentTimeMillis();
double deltaTime = 0;
int frameCount = 0;
int updateCount = 0;
requestFocus();
while (running) {
double nanoSecondsPerCount = 1000000000.0 / fps;
long now = System.nanoTime();
deltaTime += (now - lastTime) / nanoSecondsPerCount;
lastTime = now;
while (deltaTime >= 1) {
update();
updateCount++;
//System.out.println(updateCount + " - " + deltaTime);
deltaTime--;
}
render();
frameCount++;
if (System.currentTimeMillis() - timer > 1000) {
timer += 1000;
frame.setTitle(updateCount + "ups, " + frameCount + " frames");
updateCount = 0;
frameCount = 0;
}
}
stop();
}
private void update() {
keyboard.update();
if (keyboard.getKeys()[KeyEvent.VK_ESCAPE]) {
stop();
} else if (keyboard.getKeys()[KeyEvent.VK_R]) {
restartSimulation();
} else if (keyboard.getKeys()[KeyEvent.VK_DOWN]) {
fps -= 1;
fps = (int) HelperFunction
.getValueInRange(fps, 30, 300);
} else if (keyboard.getKeys()[KeyEvent.VK_UP]) {
fps += 1;
fps = (int) HelperFunction
.getValueInRange(fps, 30, 300);
} else if (keyboard.getKeys()[KeyEvent.VK_N]) {
entityManager.nextMapIndex();
} else {
if (keyboard.getKeys()[KeyEvent.VK_SPACE]) {
entityManager.forceToNextAgent();
}
}
entityManager.update();
}
private void restartSimulation() {
entityManager = new EntityManager();
}
private void render() {
BufferStrategy bs = getBufferStrategy();
if (bs == null) {
createBufferStrategy(3);
return;
}
Graphics2D g = (Graphics2D) bs.getDrawGraphics();
screen.setGraphic(g);
entityManager.renderByPixels(screen);
for (int i = 0; i < pixels.length; i++) {
pixels[i] = screen.getPixels()[i];
}
g.setStroke(new BasicStroke(2));
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
entityManager.renderByGraphics(screen);
screen.dispose();
bs.show();
}
public static int getWindowWidth() {
return width * scale;
}
public static int getWindowHeight() {
return height * scale;
}
public static void main(String[] args) {
CarDemo car = new CarDemo();
car.frame.setResizable(false);
car.frame.setTitle("Auto Car");
car.frame.add(car);
car.frame.pack();
car.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
car.frame.setLocationRelativeTo(null);
car.frame.setVisible(true);
car.start();
}
}
2) Graphics
The Screen
class handles all graphic rendering for the program. There are two ways to render in this program: by manipulating individual pixels or by using built-in functions of the Java library. For the map, manipulating individual pixels is used and for the other objects such as the Car Sprite, the Sensor Line and the Sensor Circle, built-in functions of Java are used to gain better graphics. This class has these main fields: width
, height
, pixels
, xOffset
, yOffset
and graphics
. If we want to manipulate an individual pixel, we will change data in this pixels
array. Two values, xOffset
and yOffset
change when the Car
moves around. Because we always need to see the Car
on the screen, the Car
will stand at a fixed position on the screen; we just need to adjust the offset of the screen to make us feel like the Car
is moving. There are five main functions in this class:
renderTile
The renderTile
method renders a Tile on screen. It requires as arguments the x, y coordinates of the Tile and the Tile itself.
renderCar
The renderCar
method renders the Car
object. It requires as arguments the x, y coordinates of the Car
, the angle
which the Car
is heading and the Sprite for the image of the Car
.
renderLine
The method renderLine
requires the arguments color
and x, y for start point and end point to render the line between these points with a specific color.
renderCircle
The method renderCircle
requires arguments x, y coordinates for the center of circle, the radius r and the color for this Circle
.
dispose
The dispose
method is used to dispose the object graphics
to release the resource after rendering.
package com.auto.graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import com.auto.car.CarDemo;
import com.auto.car.level.tile.Tile;
/*
* @Description: This class takes care of all graphic rendering on the screen.
*/
public class Screen {
private int width;
private int height;
private int[] pixels;
private int xOffset, yOffset;
private Graphics2D graphics;
private int scale = CarDemo.scale;
public Screen() {
}
public Screen(int w, int h) {
width = w;
height = h;
pixels = new int[w * h];
}
public int[] getPixels() {
return pixels;
}
/*
* @Description: This function renders single Tile on screen
* @param xPosition x coordinate of Tile on screen.
* @param yPosition y coordinate of Tile on screen.
* @param tile the tile to be rendered.
*/
public void renderTile(int xPosition, int yPosition, Tile tile) {
// substract the tile position by the offset of screen when screen move
xPosition -= xOffset;
yPosition -= yOffset;
for (int yTile = 0; yTile < tile.size; yTile++) {
int yScreen = yTile + yPosition;
for (int xTile = 0; xTile < tile.size; xTile++) {
int xScreen = xTile + xPosition;
// if the xScreen and yScreen are out of boundary then no
// rendering. -tile.size is to render the boundary of the map
// without rendering black strike.
if (xScreen < -tile.size || xScreen >= width || yScreen < 0
|| yScreen >= height) {
break;
}
if (xScreen < 0) {
xScreen = 0;
}
pixels[xScreen + yScreen * width] = tile.sprite.getPixels()[xTile
+ yTile * tile.size];
}
}
}
/*
* @Description: This function renders single Sprite on screen
* @param xPosition x coordinate of Sprite on screen.
* @param yPosition y coordinate of Sprite on screen.
* @param angle The angle of the car.
* @param sprite the sprite to be rendered.
*/
public void renderCar(int xPosition, int yPosition, double angle,
Sprite sprite) {
// substract the Tile position by the offset of screen when screen moves
xPosition -= xOffset;
yPosition -= yOffset;
BufferedImage img = new BufferedImage(Sprite.SIZE, Sprite.SIZE,
BufferedImage.TYPE_INT_ARGB);
for (int ySprite = 0; ySprite < Sprite.SIZE; ySprite++) {
for (int xSprite = 0; xSprite < Sprite.SIZE; xSprite++) {
int color = sprite.getPixels()[(xSprite + ySprite * Sprite.SIZE)];
if (color != 0xffffffff) {
img.setRGB(xSprite, ySprite, color);
}
}
}
AffineTransform reset = new AffineTransform();
reset.rotate(0, 0, 0);
graphics.rotate(angle, (xPosition + Sprite.SIZE / 2) * scale,
(yPosition + Sprite.SIZE / 2) * scale);
graphics.drawImage(img, xPosition * scale, yPosition * scale,
Sprite.SIZE * scale, Sprite.SIZE * scale, null);
/*
* graphics.drawRect(xPosition * scale, yPosition * scale, Sprite.SIZE
* scale, Sprite.SIZE * scale);
*/
graphics.setTransform(reset);
}
/*
* @Description: draw a line between 2 points
* @param x xCoordinate of first point
* @param y yCoordinate of first point
* @param x2 xCoordinate of second point
* @param y2 yCoordinate of second point
* @param color the color of line
*/
public void renderLine(double x, double y, double x2, double y2, Color color) {
graphics.setColor(color);
graphics.drawLine(((int) (x - xOffset + Sprite.SIZE / 2)) * scale,
((int) (y - yOffset + Sprite.SIZE / 2)) * scale, ((int) (x2
- xOffset + Sprite.SIZE / 2))
* scale, ((int) (y2 - yOffset + Sprite.SIZE / 2))
* scale);
}
/*
* @Description: render a circle from a certain point
* @param x xCoordinate of center of circle
* @param y yCoordinate of center of cicle
* @param r radius of circle
* @param color the color of circle
*/
public void renderCircle(double x, double y, double r, Color color) {
graphics.setColor(color);
graphics.drawOval((int) (x - xOffset - r + Sprite.SIZE / 2) * scale,
(int) (y - r - yOffset + Sprite.SIZE / 2) * scale,
(int) (2 * r * scale), (int) (2 * r * scale));
}
public void setGraphic(Graphics2D g) {
this.graphics = g;
}
public void renderStatistics(ArrayList<String> info) {
graphics.setColor(Color.black);
graphics.setFont(graphics.getFont().deriveFont(20f));
graphics.drawString(info.get(0), 700, 20);
graphics.drawString(info.get(1), 700, 40);
graphics.drawString(info.get(2), 700, 60);
graphics.drawString(info.get(3), 700, 80);
}
public void dispose() {
graphics.dispose();
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public void setOffset(int xOffset, int yOffset) {
this.xOffset = xOffset;
this.yOffset = yOffset;
}
public int getXOffset() {
return xOffset;
}
public int getYOffset() {
return yOffset;
}
}
The map uses 16x16 tiles to tile the screen. There are three kind of tiles in this program: GrassTile
, BrickTile
and VoidTile
and they are all inherited from the Tile
class. The GrassTile
and the VoidTile
are non-collided tiles; meanwhile, the BrickTile
is collided. The BrickTile
is used to build the track’s wall so the Car
can detect the collision. The VoidTile
will be used to cover the places where there is nothing to render on the screen.
package com.auto.car.level.tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
/*
* @Description: Tile class is the parent class of all other Tiles.
*/
public class Tile {
public int x, y;
public Sprite sprite;
public int size;
public static Tile grass = new GrassTile(Sprite.grass,16);
public static Tile brick = new BrickTile(Sprite.brick,16);
public static Tile voidTile = new VoidTile(Sprite.voidSprite,16);
public static final int grassColor = 0xff00ff00;
public static final int brickColor = 0xffFFD800;
public Tile(Sprite sprite, int size) {
this.sprite = sprite;
this.size = size;
}
public void render(int x, int y, Screen screen) {
}
public boolean solid() {
return false;
}
}
package com.auto.car.level.tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
public class BrickTile extends Tile {
public BrickTile(Sprite sprite, int size) {
super(sprite, size);
}
public void render(int x, int y, Screen screen) {
screen.renderTile(x, y, this);
}
/*
* BrickTile is solid so we can detect collision
*/
public boolean solid() {
return true;
}
}
package com.auto.car.level.tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
public class GrassTile extends Tile {
public GrassTile(Sprite sprite, int size) {
super(sprite, size);
}
public void render(int x, int y, Screen screen) {
screen.renderTile(x, y, this);
}
}
package com.auto.car.level.tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
/*
* @Description: VoidTile is render at where there's nothing on screen
*/
public class VoidTile extends Tile {
public VoidTile(Sprite sprite, int size) {
super(sprite, size);
}
public void render(int x, int y, Screen screen) {
screen.renderTile(x, y, this);
}
}
(Sprite Sheet)
A Sprite
object holds the pixels of a 16x16 image located in the Sprite Sheet. A Sprite
is used to design for objects in the program such as Car
, Brick
or Grass
. The Tile
class can either load a Sprite image or just simply fill in itself by color. In this program, the VoidTile
is just simply filled in by a color meanwhile the Brick
, the Grass
and the Car
Sprite will load images from the SpriteSheet
. The background of the Sprite
, which is WHITE color, will not be rendered.
package com.auto.graphics;
/*
* @Description: This class represents the individual Sprite which is got from
* the SpriteSheet. The SIZE of Sprite is fixed which is 16 in this program
*/
public class Sprite {
// SIZE in Decimal
public static final int SIZE = 16;
// SIZE in Binary
public static final int SIZE_2BASED = 4;// 2^4
/*
* The coordinate of sprite in the SpriteSheet. Every unit of x and y is
* equal to 16 pixels in SpriteSheet
*/
private int x, y;
private int[] pixels;
private SpriteSheet sheet;
/*
* Preloaded Sprites
*/
public static Sprite grass = new Sprite(0, 0, SpriteSheet.tiles16x16);
public static Sprite brick = new Sprite(2, 0, SpriteSheet.tiles16x16);
public static Sprite voidSprite = new Sprite(0xE6FFA3);
public static Sprite carSprite = new Sprite(4, 0, SpriteSheet.tiles16x16);
/*
* Sprite Constructor
*/
public Sprite(int x, int y, SpriteSheet sheet) {
pixels = new int[SIZE * SIZE];
this.x = x * SIZE;
this.y = y * SIZE;
this.sheet = sheet;
load();
}
public Sprite(int colour) {
pixels = new int[SIZE * SIZE];
setColour(colour);
}
private void setColour(int colour) {
for (int i = 0; i < SIZE * SIZE; i++) {
pixels[i] = colour;
}
}
/*
* This method get data from the SpriteSheet and load it into the Sprite
*/
private void load() {
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
pixels[j + i * SIZE] = sheet.pixels[(j + this.x) + (i + this.y)
* sheet.getSize()];
}
}
}
public int[] getPixels() {
return pixels;
}
}
(Level)
Level
is a mini map of the tiles. A level loads an image file then based on the color of each individual pixel, it will determine which Tile
should be rendered. One pixel in the image will be replaced by 16 pixels of Tile
in the program when it is rendered on screen. In this program, the color 0x00FF00 (Green) will represent the GrassTile
and the color 0xFFD800 (Dark Yellow) will represent the BrickTile
; any other colors that are not defined will be replaced by the VoidTile
. The SpawnLevel
class is inherited from the Level
class and it overrides the method loadLevel
. This method will load an image from the path and store data in the array pixels. After that, the colors on the image will be replaced by the Tile
to render on the screen. The following picture is a sample of a Level
.
package com.auto.car.level;
import com.auto.car.level.tile.Tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
/*
* @Description: Level class represents the map of our program
*/
public class Level {
protected int width, height;
protected int[] pixels;
public Level(String path) {
loadLevel(path);
}
protected void loadLevel(String path) {
}
public void update() {
}
/*
* @Description: render map on screen
* @param xScroll: the xOffset of screen
* @param yScroll: the yOffset of screen
*/
public void render(int xScroll, int yScroll, Screen screen) {
screen.setOffset(xScroll, yScroll);
// because every single pixel in Level is equal to SIZE of a Sprite
// so we have to convert the coordinate of screen into coordinate in
// pixels of Level
int xMostLeft = xScroll >> Sprite.SIZE_2BASED;
int xMostRight = (xScroll + screen.getWidth() + Sprite.SIZE) >> Sprite.SIZE_2BASED;
int yMostTop = yScroll >> Sprite.SIZE_2BASED;
int yMostBottom = (yScroll + screen.getHeight() + Sprite.SIZE) >> Sprite.SIZE_2BASED;
for (int y = yMostTop; y < yMostBottom; y++) {
for (int x = xMostLeft; x < xMostRight; x++) {
if (x < 0 || y < 0 || x >= width || y >= height) {
// We have to convert the Level coordinate back to screen
// coordinate before rendering it on screen
Tile.voidTile.render(x << Sprite.SIZE_2BASED,
y << Sprite.SIZE_2BASED, screen);
continue;
}
getTile(x, y).render(x << Sprite.SIZE_2BASED,
y << Sprite.SIZE_2BASED, screen);
}
}
}
/*
* @Description: each pixels in Level object represents a Tile.
* @param x: xCoordinate
* @param y: yCoordinate
*/
public Tile getTile(int x, int y) {
int index = x + y * width;
if (index >= 0 && index < pixels.length) {
switch (pixels[index]) {
case Tile.grassColor:
return Tile.grass;
case Tile.brickColor:
return Tile.brick;
}
}
return Tile.voidTile;
}
}
The Entity
class is the parent class of the Car
and the Mob
classes (Figure 9) (the Mob
class stands for Mobile
class which creates objects that are movable on the Screen). It has 2 fields for x, y coordinates and getters and setters for x and y.
The Mob
class is inherited from the Entity
class and it has two functions which are move
and isCollided
. The Mob
object can only move to a new position when there is no collision at that position. To detect a collision, we will detect the Tile
at that position. If the Tile
is solid, then there is collision; if not, then there is no collision. A Mob
object also has a Sprite
to render on the screen.
The Car
class is inherited from the Mob
class. It has information about angle
of where the car is heading. Its deltaDistance
field is the distance that the car has just moved. And its 5 sensors, EAST
, NORTH EAST
, NORTH
, WEST
, NORTH WEST
are used to detect the distances from the car to the wall while the Car
is moving around the track. The intersections
are the intersected points between the 5 sensors and the track. The intersected points will be represented by small yellow circles in this program. After the distances are detected, they will be normalized and sent to the Neural Network to make decisions. The main methods of the Car
entity are:
buildFeelers
This method will calculate the coordinates of the head and tail of each feeler. All the feelers’ tails will have the same coordinates with the car’s current position. Other feelers’ heads will be calculated based on the current heading angle
of the car.
detectFeelerIntersection
To detect the intersection between a feeler and the wall, first we build a line going through the head and the tail of that feeler. After computing the line’s information, we will iterate from the tail to the head of that feeler to determine the collision point. Here, we need help from function getTile
of the Level
object. We will pass the coordinates of the points on the line to this function to get out a Tile
object. If the Tile
object is not null
and it is solid, then we have found an intersection between the feeler and the wall right at this point.
update
The car will constantly build the feelers, detect the intersections with the wall, then send data to the Neural Network. The Neural Network will call its update
method, then give out the information about left force and right force which help the car turn right or left. The car uses this data to calculate the angle
that it will turn and the deltaDistance
that it will move then it sends this information to its move
method.
render
After two methods, buildFeelers
and detectFeelerIntersection
are called, this method will use the output of these two methods to draw the Car
itself, its sensors’ lines and the intersection circles.
package com.auto.car.entity;
import com.auto.car.level.Level;
import com.auto.graphics.Screen;
public abstract class Entity {
protected int x, y;
protected Level level;
public void update() {
}
public void render(Screen screen) {
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
package com.auto.car.entity.mob;
import com.auto.car.entity.Entity;
import com.auto.graphics.Sprite;
public abstract class Mob extends Entity {
protected Sprite sprite;
protected boolean collided = false;
public void move(int xPosition, int yPosition) {
if (xPosition != 0 && yPosition != 0) {
move(xPosition, 0);
move(0, yPosition);
return;
}
if (!isCollided(xPosition, yPosition)) {
x += xPosition;
y += yPosition;
}
}
public void update() {
}
private boolean isCollided(int xPosition, int yPosition) {
for (int corner = 0; corner < 4; corner++) {
int xt = ((x + xPosition) + (corner % 2) * 7 + 5) >> 4;
int yt = ((y + yPosition) + (corner / 2) * 12 + 3) >> 4;
if (level.getTile(xt, yt).solid()) {
collided = true;
}
}
return collided;
}
public void render() {
}
}
package com.auto.car.entity.mob;
import java.awt.Color;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import com.auto.algorithm.NeuralNetwork;
import com.auto.car.entity.EntityManager;
import com.auto.car.level.Level;
import com.auto.car.level.tile.Tile;
import com.auto.graphics.Screen;
import com.auto.graphics.Sprite;
/*
* Description: Car has 5 sensors to measure the distance from its center
* to the wall. It use NeuralNetwork, pass in these 5 sensors's values to
* get out the decision of making turn left or right or going straight.
*/
public class Car extends Mob {
// The identifiers for the feelers of the agnet
public static int FEELER_COUNT = 5;
public static enum SensorFeelers {
FEELER_EAST, FEELER_NORTH_EAST, FEELER_NORTH, FEELER_NORTH_WEST, FEELER_WEST,
};
public static float FEELER_LENGTH = 32.0f;
private double angle;
private double deltaDistance;
private Sensor sensor;
private NeuralNetwork neural;
private Point2D[] intersections;
private double[] normalizedIntersectionDepths;
public int eastIdx = SensorFeelers.FEELER_EAST.ordinal();
public int northEastIdx = SensorFeelers.FEELER_NORTH_EAST.ordinal();
public int northIdx = SensorFeelers.FEELER_NORTH.ordinal();
public int northWestIdx = SensorFeelers.FEELER_NORTH_WEST.ordinal();
public int westIdx = SensorFeelers.FEELER_WEST.ordinal();
public static final float MAX_ROTATION_PER_SECOND = 30.0f / 180;
public class Sensor {
public Point2D[] feelerTails;
public Point2D[] feelerHeads;
public Sensor() {
feelerTails = new Point2D[FEELER_COUNT];
feelerHeads = new Point2D[FEELER_COUNT];
}
}
public Car(int x, int y, double angle, Level lv) {
this.x = x;
this.y = y;
this.angle = angle;
sensor = new Sensor();
level = lv;
buildFeelers();
detectFeelerIntersection();
}
public void setLevel(Level lv) {
level = lv;
}
public void update() {
if (!this.collided) {
buildFeelers();
detectFeelerIntersection();
neural.setInput(normalizedIntersectionDepths);
neural.update();
double leftForce = neural
.getOutput(EntityManager.NeuralNetOuputs.NN_OUTPUT_LEFT_FORCE
.ordinal());
double rightForce = neural
.getOutput(EntityManager.NeuralNetOuputs.NN_OUTPUT_RIGHT_FORCE
.ordinal());
System.out.println("left force: " + leftForce + "-right force: "
+ rightForce);
// Convert the outputs to a proportion of how much to turn.
double leftTheta = MAX_ROTATION_PER_SECOND * leftForce;
double rightTheta = MAX_ROTATION_PER_SECOND * rightForce;
angle += (leftTheta - rightTheta) * 2;
double movingX = Math.sin(angle) * 2;
double movingY = -Math.cos(angle) * 2;
deltaDistance = Math.sqrt(movingX * movingX + movingY * movingY);
move((int) movingX, (int) movingY);
}
}
/*
* private void turn(int x, int y, int direction) { if (Mouse.getButton() ==
* 1) { double dx = Mouse.getX() - CarDemo.getWindowWidth() / 2 - 24; double
* dy = Mouse.getY() - CarDemo.getWindowHeight() / 2 - 24; angle =
* Math.atan2(dy, dx); }
*
* }
*/
public Rectangle2D getCarBound() {
return new Rectangle2D.Double(x - Sprite.SIZE / 2, y - Sprite.SIZE / 2,
Sprite.SIZE, Sprite.SIZE);
}
/*
* this function determines the position of sensors when the car turns
*/
private void buildFeelers() {
for (int i = 0; i < FEELER_COUNT; i++) {
sensor.feelerHeads[i] = new Point2D.Float();
sensor.feelerTails[i] = new Point2D.Float();
// All feelers's tails has the same coordinate which is the center
// of the car.
sensor.feelerTails[i].setLocation(x, y);
}
// East feeler's head
sensor.feelerHeads[eastIdx].setLocation(
x + Math.sin(Math.PI - (angle + Math.PI / 2)) * FEELER_LENGTH,
y + Math.cos(Math.PI - (angle + Math.PI / 2)) * FEELER_LENGTH);
// North East feeler's head
sensor.feelerHeads[northEastIdx].setLocation(
x + Math.sin(Math.PI - (angle + Math.PI / 4)) * FEELER_LENGTH,
y + Math.cos(Math.PI - (angle + Math.PI / 4)) * FEELER_LENGTH);
// North feeler's head
sensor.feelerHeads[northIdx].setLocation(x + Math.sin(Math.PI - angle)
* FEELER_LENGTH, y + Math.cos(Math.PI - angle) * FEELER_LENGTH);
// North West feeler's head
sensor.feelerHeads[northWestIdx].setLocation(
x + Math.sin(Math.PI - (angle - Math.PI / 4)) * FEELER_LENGTH,
y + Math.cos(Math.PI - (angle - Math.PI / 4)) * FEELER_LENGTH);
// West feeler's head
sensor.feelerHeads[westIdx].setLocation(
x + Math.sin(Math.PI - (angle - Math.PI / 2)) * FEELER_LENGTH,
y + Math.cos(Math.PI - (angle - Math.PI / 2)) * FEELER_LENGTH);
}
/*
* This function measures the distance from center of the car to the wall.
*/
public void detectFeelerIntersection() {
intersections = new Point2D[FEELER_COUNT];
normalizedIntersectionDepths = new double[FEELER_COUNT];
for (int k = 0; k < FEELER_COUNT; k++) {
double xStart = sensor.feelerHeads[k].getX();
double xEnd = sensor.feelerTails[k].getX();
double yStart = sensor.feelerHeads[k].getY();
double yEnd = sensor.feelerTails[k].getY();
Line2D line = new Line2D.Double();
line.setLine(sensor.feelerHeads[k], sensor.feelerTails[k]);
double step = 0.001;
double slope = (yStart - yEnd) / (xStart - xEnd);
if (!java.lang.Double.isInfinite(slope)) {
for (double i = xStart; i < xEnd; i += step) {
double j = slope * (i - xEnd) + yEnd;
Tile tile = level.getTile((int) (i + Sprite.SIZE / 2)
/ Sprite.SIZE, (int) (j + Sprite.SIZE / 2)
/ Sprite.SIZE);
if (tile != null) {
if (tile.solid()) {
intersections[k] = new Point2D.Float();
intersections[k].setLocation(i, j);
}
}
}
for (double i = xStart; i > xEnd; i -= step) {
double j = slope * (i - xEnd) + yEnd;
Tile tile = level.getTile((int) (i + Sprite.SIZE / 2)
/ Sprite.SIZE, (int) (j + Sprite.SIZE / 2)
/ Sprite.SIZE);
if (tile != null) {
if (tile.solid()) {
intersections[k] = new Point2D.Float();
intersections[k].setLocation(i, j);
}
}
}
} else {
for (double j = yStart; j < yEnd; j += step) {
Tile tile = level.getTile((int) (xStart + Sprite.SIZE / 2)
/ Sprite.SIZE, (int) (j + Sprite.SIZE / 2)
/ Sprite.SIZE);
if (tile != null) {
if (tile.solid()) {
intersections[k] = new Point2D.Float();
intersections[k].setLocation(xStart, j);
}
}
}
for (double j = yStart; j > yEnd; j -= step) {
Tile tile = level.getTile((int) (xStart + Sprite.SIZE / 2)
/ Sprite.SIZE, (int) (j + Sprite.SIZE / 2)
/ Sprite.SIZE);
if (tile != null) {
if (tile.solid()) {
intersections[k] = new Point2D.Float();
intersections[k].setLocation(xStart, j);
}
}
}
}
if (intersections[k] != null) {
normalizedIntersectionDepths[k] = 1 - (Math.sqrt(Math.pow(x
- intersections[k].getX(), 2)
+ Math.pow(y - intersections[k].getY(), 2)) / FEELER_LENGTH);
} else {
normalizedIntersectionDepths[k] = 0;
}
}
}
public void attach(NeuralNetwork neuralNet) {
this.neural = neuralNet;
}
public void setPosition(Point2D defaultPosition) {
x = (int) defaultPosition.getX();
y = (int) defaultPosition.getY();
}
public void clearFailure() {
collided = false;
}
public boolean hasFailed() {
return collided;
}
public double getDistanceDelta() {
return deltaDistance;
}
public void render(Screen screen) {
// Render the car
screen.renderCar(x, y, angle, Sprite.carSprite);
// Render 5 sensors around the car
screen.renderLine(sensor.feelerHeads[eastIdx].getX(),
sensor.feelerHeads[eastIdx].getY(),
sensor.feelerTails[eastIdx].getX(),
sensor.feelerTails[eastIdx].getY(), Color.YELLOW);
screen.renderLine(sensor.feelerHeads[northEastIdx].getX(),
sensor.feelerHeads[northEastIdx].getY(),
sensor.feelerTails[northEastIdx].getX(),
sensor.feelerTails[northEastIdx].getY(), Color.YELLOW);
screen.renderLine(sensor.feelerHeads[northIdx].getX(),
sensor.feelerHeads[northIdx].getY(),
sensor.feelerTails[northIdx].getX(),
sensor.feelerTails[northIdx].getY(), Color.black);
screen.renderLine(sensor.feelerHeads[northWestIdx].getX(),
sensor.feelerHeads[northWestIdx].getY(),
sensor.feelerTails[northWestIdx].getX(),
sensor.feelerTails[northWestIdx].getY(), Color.YELLOW);
screen.renderLine(sensor.feelerHeads[westIdx].getX(),
sensor.feelerHeads[westIdx].getY(),
sensor.feelerTails[westIdx].getX(),
sensor.feelerTails[westIdx].getY(), Color.YELLOW);
screen.renderCircle(x, y, FEELER_LENGTH, Color.YELLOW);
// draw collisions by a small circle
for (int k = 0; k < FEELER_COUNT; k++) {
if (intersections[k] != null) {
screen.renderCircle(intersections[k].getX(),
intersections[k].getY(), 3, Color.YELLOW);
}
}
}
public void setPosition(int x, int y) {
setX(x);
setY(y);
}
3) Genetic Algorithm
A Genetic Algorithm is used to train the Neural Network; it helps the Neural Network give a better decision.
Genome Class
A genome (Figure 10) has 3 pieces of information: ID
, fitness
and weights
. The fitness
information is the distance that the car has been able to move without collision and the weights
information is the list of random Sigmoid values which are from -1
to 1
.
GeneticAlgorithm Class
The GeneticAlgorithm
class has a list of Genomes called population
. Each population
has good genomes with high fitness. Those genomes will be mixed up and mutated to create a new population
of genomes. This class has the following main methods:
generateNewPopulation
This function will generate a new population
of genomes. The genomes’ weights
will be randomly assigned a Sigmoid value; the ID
and the fitness
values will be assigned to 0
.
breedPopulation
This function will generate a new population
using old population
. First, 4 genomes with high fitness will be chosen from the old population
. After that, we will mutate and cross over 4 of them together and add to new population
. The left positions of new population
will be filled up by a new random Genome
.
crossOver
The crossOver
function is a function that mixes up 2 genomes and creates 2 other new genomes. To mix them, first we randomly choose the cross over point then split 2 genomes into 4 parts from that point, then mix 4 parts together.
mutate
The mutate
function is a function that randomly picks up a gene in a genome and assigns a new value which is the sum of a random Sigmoid value multiplied by the constant MAX_PERMUTATION = 0.3
plus weight of that gene if the Sigmoid value is smaller than MUTATION_RATE = 0.15
.
package com.auto.algorithm; import java.util.ArrayList; /* * @Description: Genome object simply keeps 2 important info: fitness and weights. * The fitness is the distance that how long the car could go. The weights are * a list of Sigmoid values which is from -1 to 1. */ public class Genome { public int ID; public double fitness; public ArrayList<Double> weights; }
package com.auto.algorithm; import java.util.ArrayList; import java.util.Random; /* * @Description: GeneticAlgorithm is used to train neuron network. It has a list of * genomes called population. Each population will have some of the best genomes * with highest fitness. Fitness is the sum of distance that how far a car could go. * The best genomes will be used * to create other genomes by mixing them up (crossing over between 2 genomes) and mutate * some of their genes. It is a little bit different with basic genetic algorithm, * the mutate will not turn on and off a gene but they randomly change the weight of genes. */ public class GeneticAlgorithm { public static final float MAX_PERMUTATION = 0.3f; public static final float MUTATION_RATE = 0.15f; private int currentGenome; private int totalPopulation; private int genomeID; private int generation; private ArrayList<Genome> population; public GeneticAlgorithm() { currentGenome = -1; totalPopulation = 0; genomeID = 0; generation = 1; population = new ArrayList<Genome>(); } /* * Generate genomes population with ID, fitness and random Sigmoid weights */ public void generateNewPopulation(int totalPop, int totalWeights) { generation = 1; clearPopulation(); currentGenome = -1; totalPopulation = totalPop; for (int i = 0; i < totalPopulation; i++) { Genome genome = new Genome(); genome.ID = genomeID; genome.fitness = 0.0; genome.weights = new ArrayList<Double>(); for (int j = 0; j < totalWeights; j++) { genome.weights.add(HelperFunction.RandomSigmoid()); } genomeID++; population.add(genome); } } public void setGenomeFitness(double fitness, int index) { if (index >= population.size() || index < 0) return; population.get(index).fitness = fitness; } public Genome getNextGenome() { currentGenome++; if (currentGenome >= population.size()) return null; return population.get(currentGenome); } public void clearPopulation() { population.clear(); } /* * This function will generate new population of genomes based on best 4 * genomes (genomes which have highest fitness). The best genomes will be * mixed up and mutated to create new genomes. */ public void breedPopulation() { ArrayList<Genome> bestGenomes = new ArrayList<Genome>(); // Find the 4 best genomes which have highest fitness. bestGenomes = getBestGenomes(4); ArrayList<Genome> children = new ArrayList<Genome>(); // Carry on the best genome. Genome bestGenome = new Genome(); bestGenome.fitness = 0.0; bestGenome.ID = bestGenomes.get(0).ID; bestGenome.weights = bestGenomes.get(0).weights; // mutate few gene of genome to create new genome mutate(bestGenome); children.add(bestGenome); // Child genomes. ArrayList<Genome> crossedOverGenomes; // Breed with genome 0. crossedOverGenomes = crossOver(bestGenomes.get(0), bestGenomes.get(1)); mutate(crossedOverGenomes.get(0)); mutate(crossedOverGenomes.get(1)); children.add(crossedOverGenomes.get(0)); children.add(crossedOverGenomes.get(1)); crossedOverGenomes = crossOver(bestGenomes.get(0), bestGenomes.get(2)); mutate(crossedOverGenomes.get(0)); mutate(crossedOverGenomes.get(1)); children.add(crossedOverGenomes.get(0)); children.add(crossedOverGenomes.get(1)); crossedOverGenomes = crossOver(bestGenomes.get(0), bestGenomes.get(3)); mutate(crossedOverGenomes.get(0)); mutate(crossedOverGenomes.get(1)); children.add(crossedOverGenomes.get(0)); children.add(crossedOverGenomes.get(1)); // Breed with genome 1. crossedOverGenomes = crossOver(bestGenomes.get(1), bestGenomes.get(2)); mutate(crossedOverGenomes.get(0)); mutate(crossedOverGenomes.get(1)); children.add(crossedOverGenomes.get(0)); children.add(crossedOverGenomes.get(1)); crossedOverGenomes = crossOver(bestGenomes.get(1), bestGenomes.get(3)); mutate(crossedOverGenomes.get(0)); mutate(crossedOverGenomes.get(1)); children.add(crossedOverGenomes.get(0)); children.add(crossedOverGenomes.get(1)); // For the remainding n population, add some random genomes. int remainingChildren = (totalPopulation - children.size()); for (int i = 0; i < remainingChildren; i++) { children.add(this.createNewGenome(bestGenomes.get(0).weights.size())); } clearPopulation(); population = children; currentGenome = -1; generation++; } private Genome createNewGenome(int totalWeights) { Genome genome = new Genome(); genome.ID = genomeID; genome.fitness = 0.0f; genome.weights = new ArrayList<Double>(); for (int j = 0; j < totalWeights; j++) { genome.weights.add(HelperFunction.RandomSigmoid()); } genomeID++; return genome; } /* * This function will mix up two genomes to create 2 other new genomes */ private ArrayList<Genome> crossOver(Genome g1, Genome g2) { Random random = new Random(System.nanoTime()); // Select a random cross over point. int totalWeights = g1.weights.size(); int crossover = Math.abs(random.nextInt()) % totalWeights; ArrayList<Genome> genomes = new ArrayList<Genome>(); Genome genome1 = new Genome(); genome1.ID = genomeID; genome1.weights = new ArrayList<Double>(); genomeID++; Genome genome2 = new Genome(); genome2.ID = genomeID; genome2.weights = new ArrayList<Double>(); genomeID++; // Go from start to crossover point, copying the weights from g1 to children. for (int i = 0; i < crossover; i++) { genome1.weights.add(g1.weights.get(i)); genome2.weights.add(g2.weights.get(i)); } // Go from start to crossover point, copying the weights from g2 to children. for (int i = crossover; i < totalWeights; i++) { genome1.weights.add(g2.weights.get(i)); genome2.weights.add(g1.weights.get(i)); } genomes.add(genome1); genomes.add(genome2); return genomes; } /* * Generate a random chance of mutating the weight in the genome. */ private void mutate(Genome genome) { for (int i = 0; i < genome.weights.size(); ++i) { double randomSigmoid = HelperFunction.RandomSigmoid(); if (randomSigmoid < MUTATION_RATE) { genome.weights.set(i, genome.weights.get(i) + (randomSigmoid * MAX_PERMUTATION)); } } } /* * Get the best genomes to breed new population */ private ArrayList<Genome> getBestGenomes(int totalGenomes) { int genomeCount = 0; int runCount = 0; ArrayList<Genome> bestGenomes = new ArrayList<Genome>(); while (genomeCount < totalGenomes) { if (runCount > 10) { break; } runCount++; // Find the best cases for cross breeding based on fitness score. double bestFitness = 0; int bestIndex = -1; for (int i = 0; i < this.totalPopulation; i++) { if (population.get(i).fitness > bestFitness) { boolean isUsed = false; for (int j = 0; j < bestGenomes.size(); j++) { if (bestGenomes.get(j).ID == population.get(i).ID) { isUsed = true; } } if (isUsed == false) { bestIndex = i; bestFitness = population.get(bestIndex).fitness; } } } if (bestIndex != -1) { genomeCount++; bestGenomes.add(population.get(bestIndex)); } } return bestGenomes; } public int getCurrentGeneration() { return generation; } public int getCurrentGenomeIndex() { return currentGenome; } }
4) Neural Network
Neuron Class
Neuron is the basic element in the Neural Network. It has two fields which are the numberOfInputs
coming to a neuron and values of those inputs which are called weights
. In this program, neuron will receive data from the genomes and stores into its weights
. These weights
will be used by the neuron layers to evaluate output.
Neuronslayer Class
The NeuronsLayer
class (Figure 13) contains a list of neurons. It will use those neurons’ weights
to evaluate and give out the outputs. There are two types of neurons layer. One is the Hidden Layer and one is the Output Layer. These layers will be managed by the Neural Network. The main
method of this class is evaluate
method. In this method, we will sum up the value of inputs
multiplied by neuron’s weights
and add the value of last weight multiplied by a constant BIAS = -1. The purpose of BIAS value is to make sure the output is not 0. After that, the sum will be normalized by Sigmoid
function and stored in the outputs
.
NeuralNetwork Class
NeuralNetwork
class (Figure 14) contains 1 Output Layer and 1 or many Hidden Layers. These are the main methods of this class:
setInput
This method receives the inputs from car’s sensors and stores them.
getOutput
This method receives the index and give out the data at that index.
update
The NeuralNetwork
constantly receives data from the sensors of the car, passes that data to Hidden Layer to process, then transfers output to the Output Layer for second processing. After that, the Output Layer will give out the decision back to the car to make a turn.
fromGenome
This function will get weights
from genome of GeneticAlgorithm
to store in Neurons Layers.
package com.auto.algorithm;
import java.util.ArrayList;
/*
* @Description: Neuron is the basic element in the neuron network. Each neuron has
* a certain number of inputs. In this program Neuron from HiddenLayer has 5 inputs which
* are from the car's 5 sensors and Neuron in OutputLayer has 8 inputs which are from
* 8 HiddenLayers
*/
public class Neuron {
protected int numberOfInputs;
protected ArrayList<Double> weights;
public void init(ArrayList<Double> weightsIn, int numOfInputs) {
this.numberOfInputs = numOfInputs;
weights = weightsIn;
}
}
package com.auto.algorithm;
import java.util.ArrayList;
/*
* @Description: NeuronsLayer contains Neurons. It evaluates these nerons to give
* out decision.
*/
public class NeuronsLayer {
public static final float BIAS = -1.0f;
private int totalNeurons;
// int totalInputs;
private ArrayList<Neuron> neurons;
/*
* Evaluate the inputs from sensors or HiddenLayer and give out the output
*/
public void evaluate(ArrayList<Double> inputs, ArrayList<Double> outputs) {
int inputIndex = 0;
for (int i = 0; i < totalNeurons; i++) {
float activation = 0.0f;
int numOfInputs = neurons.get(i).numberOfInputs;
Neuron neuron = neurons.get(i);
// sum the weights up to numberOfInputs-1 and add the bias
for (int j = 0; j < numOfInputs - 1; j++) {
if (inputIndex < inputs.size()) {
activation += inputs.get(inputIndex)
* neuron.weights.get(j);
inputIndex++;
}
}
// Add the bias.
activation += neuron.weights.get(numOfInputs) * BIAS;
outputs.add(HelperFunction.Sigmoid(activation, 1.0f));
inputIndex = 0;
}
}
public ArrayList<Double> getWeights() {
// Calculate the size of the output vector by calculating the amount of
// weights in each neurons.
ArrayList<Double> out = new ArrayList<Double>();
for (int i = 0; i < this.totalNeurons; i++) {
Neuron n = neurons.get(i);
for (int j = 0; j < n.weights.size(); j++) {
out.add(n.weights.get(j));
}
}
return out;
}
public void loadLayer(ArrayList<Neuron> neurons) {
totalNeurons = neurons.size();
this.neurons = neurons;
}
}
package com.auto.algorithm;
import java.util.ArrayList;
/*
* @Description: NeuralNetwork is used to make decision for the car; decide that it
* should turn right or turn left or go straight. It may contain many hidden NeuronsLayers
* and 1 output NeuronsLayer.
* These layers will constantly update to get new values from genomes
* each time the car crashs to the wall. The network will use new values from Genetic
* to make a decision for the next try of car.
*/
public class NeuralNetwork {
private int inputAmount;
private int outputAmount;
private ArrayList<Double> outputs;
private ArrayList<Double> inputs;
// HiddenLayer produces the input for OutputLayer
private ArrayList<NeuronsLayer> hiddenLayers;
// OutputLayer will receive input from HiddenLayer
private NeuronsLayer outputLayer;
public NeuralNetwork() {
outputs = new ArrayList<Double>();
inputs = new ArrayList<Double>();
}
/*
* receive input from sensors of car which is normalized distance from
* center of car to the wall.
*/
public void setInput(double[] normalizedDepths) {
inputs.clear();
for (int i = 0; i < normalizedDepths.length; i++) {
inputs.add(normalizedDepths[i]);
}
}
@SuppressWarnings("unchecked")
public void update() {
outputs.clear();
for (int i = 0; i < hiddenLayers.size(); i++) {
if (i > 0) {
inputs = outputs;
}
// each hidden layer calculates the outputs based on inputs
// from sensors of the car
hiddenLayers.get(i).evaluate(inputs, outputs);
System.out.println("Output of hidden layers: "
+ outputs.toString());
}
// the outputs of HiddenLayers will be used as input for
// OutputLayer
inputs = (ArrayList<Double>) outputs.clone();
// The output layer will give out the final outputs
outputLayer.evaluate(inputs, outputs);
}
public double getOutput(int index) {
if (index >= outputAmount)
return 0.0f;
return outputs.get(index);
}
/*
* Initiate NeuronsNetwork
*/
/*
* public void createNet(int numOfHiddenLayers, int numOfInputs, int
* neuronsPerHidden, int numOfOutputs) { inputAmount = numOfInputs;
* outputAmount = numOfOutputs; hiddenLayers = new
* ArrayList<NeuronsLayer>(); for (int i = 0; i < numOfHiddenLayers; i++) {
* NeuronsLayer layer = new NeuronsLayer();
* layer.populateLayer(neuronsPerHidden, numOfInputs);
* hiddenLayers.add(layer); } outputLayer = new NeuronsLayer();
* outputLayer.populateLayer(numOfOutputs, neuronsPerHidden); }
*/
public void releaseNet() {
// inputLayer = null;
outputLayer = null;
hiddenLayers = null;
}
/*
* Neural network receives weights from genome to make new HiddenLayers and
* OutputLayer.
*/
public void fromGenome(Genome genome, int numOfInputs,
int neuronsPerHidden, int numOfOutputs) {
if (genome == null)
return;
releaseNet();
hiddenLayers = new ArrayList<NeuronsLayer>();
outputAmount = numOfOutputs;
inputAmount = numOfInputs;
NeuronsLayer hidden = new NeuronsLayer();
ArrayList<Neuron> neurons = new ArrayList<Neuron>();
for (int i = 0; i < neuronsPerHidden; i++) {
ArrayList<Double> weights = new ArrayList<Double>();
for (int j = 0; j < numOfInputs + 1; j++) {
weights.add(genome.weights.get(i * neuronsPerHidden + j));
}
Neuron n = new Neuron();
n.init(weights, numOfInputs);
neurons.add(n);
}
hidden.loadLayer(neurons);
hiddenLayers.add(hidden);
// Clear weights and reassign the weights to the output.
ArrayList<Neuron> neuronsOut = new ArrayList<Neuron>();
for (int i = 0; i < numOfOutputs; i++) {
ArrayList<Double> weights = new ArrayList<Double>();
for (int j = 0; j < neuronsPerHidden + 1; j++) {
weights.add(genome.weights.get(i * neuronsPerHidden + j));
}
Neuron n = new Neuron();
n.init(weights, neuronsPerHidden);
neuronsOut.add(n);
}
outputLayer = new NeuronsLayer();
outputLayer.loadLayer(neuronsOut);
}
}
package com.auto.algorithm;
import java.util.Random;
/*
* Description: Global helper functions
*/
public class HelperFunction {
/*
* normalize value to make it from 1 to -1
*/
public static double Sigmoid(float a, float p) {
float ap = (-a) / p;
return (1 / (1 + Math.exp(ap)));
}
/*
* random number from -1 to 1;
*/
public static double RandomSigmoid() {
Random ran = new Random(System.nanoTime());
double r = ran.nextDouble() - ran.nextDouble();
return r;
}
/*
* compare value of a to b and c. If is smaller then b or greater than c,
* a will become b or c
*/
public static double getValueInRange(double a, double b, double c) {
if (a < b) {
return b;
} else if (a > c) {
return c;
}
return a;
}
}
Points of Interest
I felt so good when finish this project. It's my capstone project for Master Degree from Marshall University. The project is a practical application. It helps beginners to learn about Artificial Intelligence, Neural Network and Genetic Algorithm.
References
I'm sorry that I forgot to mention the references in the previous version. The idea and code I learnt from this author https://github.com/matthewrdev/Neural-Network written in C++. The graphics I learnt from this Youtube channel https://www.youtube.com/user/TheChernoProject. :)
History
- 15th December, 2016: Initial version