## Lab 2b: Generate neural network training data¶

### Introduction¶

In this notebook you will generate data for training a neural network to control the robot to find light. You will use your find light brain from the previous lab as the starting point. You will begin as we did before, by defining functions to make the same world and the same robot.

In [1]:
from jyro.simulator import *
import numpy as np
from math import pi
from random import random

In [2]:
def make_world(physics):
physics.addBox(0, 0, 4, 4, fill="backgroundgreen", wallcolor="gray")
physics.addBox(1.75, 2.9, 2.25, 3.0, fill="blue", wallcolor="blue")

In [3]:
def make_robot():
robot = Pioneer("Pioneer", 2, 1, 0)
light_sensors = PioneerFrontLightSensors(3) #parameter defines max range
return robot


### Creating unique random starting locations¶

In order to generate a robust training set, we'd like to have the robot start from lots of different locations in the world. Complete the function random_start(robot) below by replacing the pass line with your solution. We want to ensure that the robot is not placed too close to any walls or too close to the light, therefore choose a random starting location where x is between 0.5 and 3.5, y is between 0.5 and 2.5, and heading is between 0 and 2pi. Remember that you can use the robot's setPose(x, y, heading) method to position the robot at a particular location.

In [4]:
def random_start(robot):
pass


### Test random_start function¶

We can create the robot and the visual simulator, and then try calling this function multiple times to see where the robot ends up.

In [5]:
robot = make_robot()
vsim = VSimulator(robot, make_world)

In [6]:
from time import sleep
for i in range(10):
random_start(robot)
vsim.update_gui()
sleep(1.0)


### Generating translation and rotation amounts¶

In the previous lab you created a brain that didn't return anything, but directly controlled the robot by calling robot.move(translate, rotate). Now we want to both move the robot and save the movements to a file. To do this we'll create a function called determine_move (based on your finding light brain) that takes in the current sensor values and returns a tuple representing the appropriate translation, rotation amounts for that situation.

Be sure that your function stops the robot (using translation of 0 and rotation of 0), when it has reached the light.

In [7]:
def determine_move(lights, sonars):
"""Returns tuple of (translation, rotation) movement"""
return (0,0)


### Test determine_move function¶

We can create a brain based on the function above and test that it still properly controls the robot to find the light. Remember to press the Play button on the visual simulator above to see the brain at work.

In [8]:
def find_light_brain(robot):
lights = robot["light"].getData()
sonars = robot["sonar"].getData()
translate, rotate = determine_move(lights, sonars)
robot.move(translate, rotate)

robot.brain = find_light_brain


### Generating a file with sensor data and movement commands¶

Now we have all of the pieces in place to generate the training data.

We want to generate a lot of data, and the visual simulator will be too slow. So instead we will use the non-visual version of the simulator. This simulator will only update when explicitly told to step.

A key point when creating data for neural network training, is that all of the data should be normalized to fit the range of whatever activation function you plan to use. We will be using an activation function with a range of [-1,1], because the motor commands can be negative or positive. The light sensor values are already in the range [0,1] so will not need to be adjusted. However, the sonar sensor values are in the range [0,5]. So we will normalize these before saving them to the file. Recall that sonars measure the distance to an obstacle. The robot doesn't really need to worry about distant obstacles, so let's focus on obstacles within a distance of 3.0. Any distance >= 3.0, normalize to 1.0. Any distance < 3.0, normalize as distance/3.0.

Each line of the file we create will consist of sensor values, separated by whitespace, followed by motor commands separated by white space, ending with a newline.

One problem we may potentially need to address is that our light finding program may actually get stuck in certain situations, dithering, and never making progress to the light. We should build our data generating function with this in mind. To do this, determine how many steps your light finding brain typically takes to reach the light. Increase this by 10-20 percent and designate this as the max_trial_length. For any trial that takes longer than that, we will assume that the robot is stuck and restart at a new random location.

Below is the pseudocode for the function you need to write:

In [ ]:
def generate_data(robot, make_world, trials, filename):
sim = Simulator(robot, make_world) # create the non-visual simulator
# open a file to write to
for i in range(trials):
# start the robot at a random spot
# display the current trial counter and the robot's starting position
saved_steps = [] # a list to save each step of data associated with an individual trial
while True:
# determine the robot's move using the current light and sonar data
# move the robot
sim.step() # execute one step in the non-visual simulator
# if the robot is stopped this indicates the light was found
# write each line of the saved_steps list to the file
# break from the while loop
# if len(saved_steps) > max_trial_length this indicates the robot is likely stuck
# display that the trial is restarting
# start the robot at a new random spot
# reset the saved_steps list to empty
# write the light values to a string
# write the normalized sonar values (only the ones you used) to the same string
# write the translation and rotation amounts to the same string with a newline
# append the string to the saved_steps list
# close the file


### Test generate_data function¶

Call your function to generate 10 trials of data. Go to a terminal window and look at the file you created. Make sure that each line represents one set of sensor readings and one movement. Make sure that the light readings are increasing until they eventually peak, and then a new trial starts. Use the unix command wc to see how many lines are in your file (the first number returned). Calculate the average number of steps your find light strategy needs to reach the light. As a point of reference, mine takes about 190 steps per trial.

In [ ]:
generate_data(robot, make_world, 10, "training_data.txt")


Once you are sure your data generation function is working properly, use it to generate a large collection of trials (at least 500).

### Simplifying the World and the Task¶

NOTE: Completing this section is optional.

You may find in doing the next section of this lab that your neural network is not doing very well at finding the light. One option, is to simplify the original world that we were using by removing the little blue wall in front of the light. We can also simplify the task by having the random_start function choose a smaller range of possible starting headings to ones that are facing more towards the light (where 3pi/4 < heading < 2pi or 0 < heading < pi/4). Finally we can simplify the controller since, the robot will not be as likely to encounter obstacles. For example, this simpler controller could be used instead.

In [ ]:
def simple_move(robot):
lights = robot["light"].getData()
sonars = robot["sonar"].getData()
left_light = lights[0]
right_light = lights[1]
light_diff = abs(left_light-right_light)
if sum(lights) > 1.6:
return (0, 0)
elif min(sonars[2:6]) < 0.3:
return (0, 0.3)
elif light_diff > 0.05:
if right_light > left_light:
return (0, -0.3)
else:
return (0, 0.3)
else:
return (0.3, 0)


If you'd like, try generating a new data set using the simplified world and task. See if your neural network is more successful.

### Use git to add, commit, and push¶

Save this notebook to the repo before moving on.