Implement hebbian didabots

master
Yorick van Pelt 2017-12-13 00:19:08 +01:00
parent 0bc6937afe
commit 7ff8c1b006
4 changed files with 163 additions and 26 deletions

View File

@ -53,7 +53,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<tr><td><label for="ffwdtimes">nr of experiments</label></td><td><input id="ffwdtimes" type="number" value="10" min="1" max="20" /></td></tr>
<tr><td><label for="wanderRate">robot wander rate</label></td><td><input id="wanderRate" type="number" value="0.0002" step="0.0001"></td></tr>
<tr><td><label for="boxFric">box friction</label></td><td><input id="boxFric" type="number" value="0.001" step="0.0005" min=0></td></tr>
<tr><td><label for="boxX">Boxes:</label></td><td><input id="boxX" type="number" value="5" min=1 max=10>x<input id="boxY" type="number" value="5" min=1 max=10></td></tr>
<tr><td><label for="boxX">Boxes:</label></td><td><input id="boxX" type="number" value="4" min=1 max=10>x<input id="boxY" type="number" value="4" min=1 max=10></td></tr>
<tr><td><input type="button" value="Restart Slow" onclick="initSimulation(); sim.start()" /><input type="button" value="toggle pause" onclick="sim.toggle()"></td><td><input type="button" value="run experiments" onclick="runExperiments()" /><input type="button" value="clear" onclick="resultsTable.clear()" /></td></tr>
<tr><td></td></tr>
</tbody></table>

View File

@ -30,11 +30,11 @@ Feel free to use any of this code.
"use strict"
// Simulation settings; please change anything that you think makes sense.
var simInfo = {
maxSteps: 50000, // maximal number of simulation steps to run
maxSteps: 20000, // maximal number of simulation steps to run
airDrag: 0.1, // "air" friction of enviroment; 0 is vacuum, 0.9 is molasses
boxFric: 0.001, // friction between boxes during collisions
boxFric: 0.005, // friction between boxes during collisions
boxMass: 0.01, // mass of boxes
boxSize: 10, // size of the boxes, in pixels
boxSize: 15, // size of the boxes, in pixels
robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down)
robotMass: 0.4, // robot mass (a.u)
gravity: 0, // constant acceleration in Y-direction
@ -43,8 +43,97 @@ var simInfo = {
debugMouse: true, // allow dragging any object with the mouse
wanderRate: 0.0002,
};
class SVGRobot extends Robot {
makeBody(robotInfo) {
// load robot's body shape from SVG file
const bodySVGpoints = loadFromSVG();
return Matter.Bodies.fromVertices(robotInfo.init.x,
robotInfo.init.y,
bodySVGpoints, {
frictionAir: simInfo.airDrag,
mass: simInfo.robotMass,
color: [255, 255, 255],
role: 'robot'
}, true);
}
}
class HebbDidaBot extends Robot {
constructor(props) {
const angle = Math.PI/3.5
super(Object.assign({
learningRate: 0.001,
forgettingRate: 0.001,
sensors: [
new CollisionSensor('collL', {
attachAngle: -angle
}),
new CollisionSensor('collR', {
attachAngle: angle
}),
new DistanceSensor('distL', {
attachAngle: -angle
}),
new DistanceSensor('distR', {
attachAngle: angle
}),
].concat(props.sensors || [])
}, props))
this.display = document.createElement('canvas')
this.display.width = this.display.height = 100
this.display.style.border = '1px solid black'
this.weights = [[Math.random()/10,Math.random()/10],[Math.random()/10,Math.random()/10]]
}
move() {
const {distL, distR, collL, collR} = this.getSensors()
// feed forward: perception
// collision
function feedforward(weights, activations, intrinsic) {
return intrinsic.map((x, i) => {
return x + sum(zip(weights[i], activations).map(([a,b]) => a*b))
})
}
function hebb(weight, lrate, frate, act_src, act_dst) {
const avg = sum(act_src) / act_src.length
return weight.map((row, i) => row.map((wij, j) =>
wij + (lrate * (act_src[j] * act_dst[i]) - frate * avg * wij) / act_src.length
))
}
// input:
const motorweights = [[0,1],[1,0]]
const collisionAct = [+collL, +collR]
const threshold = theta => x => x > theta
const percact = [distL, distR].map(x => x == Infinity ? 0 : (50 - x)/50)
const collact = [+collL, +collR]
const collisionlayer = feedforward(this.weights, percact, collact)
const motorLayer = feedforward(motorweights, collisionlayer, [0,0])
this.weights = hebb(this.weights, this.info.learningRate, this.info.forgettingRate, percact, collisionlayer)
const [left, right] = motorLayer.map(threshold(0.5))
// output: 2 neurons. if left is activated, turn left. if right is activated, turn right.
// if both, go backwards. otherwise: forward
if (left && !right) this.rotate(-0.003)
else if (!left && right) this.rotate(+0.003)
else if (left && right) this.drive(-2e-4)
else this.drive(2e-4)
this.layers = [percact, collisionlayer, motorLayer]
}
getDisplay() {
return this.layers
}
updateDisplay() {
const ctx = this.display.getContext('2d')
if (!this.weights) return
ctx.clearRect(0,0,100,100)
this.weights.forEach((a, i) => {
a.forEach((b, j) => {
ctx.fillStyle = convrgb([b*128, b*128, b*128].map(x => 255-x))
const [x,y] = [j*50,i*50]
ctx.fillRect(x, y, x+50, y+50)
})
})
}
}
// our own lemming implementation
class Lemming extends Robot {
class Lemming extends SVGRobot {
constructor(props) {
super(Object.assign({
sensors: [
@ -321,7 +410,7 @@ function initSimulation() {
sim.init()
let noRobots = +document.getElementById('robotAmnt').value
while(noRobots--) {
sim.addRobot(new Lemming({
sim.addRobot(new HebbDidaBot({
// random color
color: [Math.random()*255, Math.random()*255, Math.random()*255], // color of the robot shape
init: {x: 50*noRobots + 50, y: 50, angle: 0}, // initial position and orientation
@ -359,10 +448,33 @@ function redblocksatwall(sim) {
})
return result
}
function countclusters(sim) {
const boxes = sim.world.composites[0].bodies
function dist(box1, box2) {
return Vec2.dist(box1.position, box2.position)
}
const clusters = []
const maxdist = 2 * Math.sqrt(2) * simInfo.boxSize
const unclaimed = new Set(boxes)
while(unclaimed.size) {
const box = unclaimed.entries().next().value[0]
const queue = new Set([box])
let no_boxes = 0
while(queue.size) {
const box2 = queue.entries().next().value[0]
unclaimed.delete(box2)
queue.delete(box2)
no_boxes++
Array.from(unclaimed).filter(box3 => (dist(box2, box3) < maxdist)).forEach(x => queue.add(x))
}
clusters.push(no_boxes)
}
return clusters.sort().reverse()
}
// automatically run the experiments based on the values from the html
async function experimentRunner(stopCondition=blocksSorted) {
async function experimentRunner(stopCondition) {
initSimulation()
while(!stopCondition(sim) && sim.curSteps < simInfo.maxSteps) {
while((stopCondition ? !stopCondition(sim) : true) && sim.curSteps < simInfo.maxSteps) {
sim.runSteps(500)
// take a break for the gui to stay responsive
await promiseTimeout(50)
@ -371,8 +483,9 @@ async function experimentRunner(stopCondition=blocksSorted) {
// save data
const rv = {
steps: sim.curSteps,
done: stopCondition(sim),
redblocks: redblocksatwall(sim),
//done: stopCondition(sim),
//redblocks: redblocksatwall(sim),
clusters: countclusters(sim),
robots: sim.robots.length
}
sim.destroy()
@ -394,15 +507,15 @@ class ResultsTable {
clear() {
this.elem.innerHTML = "<tr><th colspan=4>Experiment Results</th></tr>"
const tr = document.createElement('tr')
const headers = ["robots", "steps", "sorted", "red blocks@wall"]
const headers = ["robots", "steps", "clusters"]
headers.forEach(txt => {
tr.appendChild(document.createElement('th')).appendChild(document.createTextNode(txt))
})
this.elem.appendChild(tr)
}
add({steps, done, redblocks, robots}) {
add({steps, clusters, robots}) {
const tr = document.createElement('tr')
;[robots, steps, done, redblocks].forEach(txt => {
;[robots, steps, clusters.toString()].forEach(txt => {
tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(txt))
})
this.elem.appendChild(tr)
@ -427,6 +540,9 @@ class Bay {
robot.sensors.forEach(sensor => {
makeInteractiveElement(sensor, this.elem)
})
const bayDisplay = document.getElementById('bayDisplay')
bayDisplay.innerHTML = ""
bayDisplay.appendChild(robot.display)
this.repaint()
}
repaint() {
@ -447,7 +563,7 @@ class Bay {
}).join('')
document.getElementById('SensorLabel').innerHTML = sensorString;
document.getElementById('bayDisplay').innerHTML = robot.getDisplay()
robot.updateDisplay()
}
}
transformMouse({x,y}) {
@ -470,19 +586,8 @@ function loadFromSVG() {
return vertexSets;
};
function Robot(robotInfo) {
// load robot's body shape from SVG file
const bodySVGpoints = loadFromSVG();
this.body = Matter.Bodies.fromVertices(robotInfo.init.x,
robotInfo.init.y,
bodySVGpoints, {
frictionAir: simInfo.airDrag,
mass: simInfo.robotMass,
color: [255, 255, 255],
role: 'robot'
}, true);
this.body = this.makeBody(robotInfo)
Matter.World.add(sim.world, this.body);
Matter.Body.setAngle(this.body, robotInfo.init.angle);
@ -492,8 +597,20 @@ function Robot(robotInfo) {
// attach its helper functions
this.info = robotInfo;
this.display = document.createElement('span')
}
Object.assign(Robot.prototype, {
makeBody(robotInfo) {
const nSides = 20,
circle = Matter.Bodies.circle;
return circle(robotInfo.init.x, robotInfo.init.y, simInfo.robotSize,
{
frictionAir: simInfo.airDrag,
mass: simInfo.robotMass,
color: [255, 255, 255],
role: 'robot'
}, nSides)
},
rotate(torque=0) {
/* Apply a torque to the robot to rotate it.
*
@ -506,6 +623,9 @@ Object.assign(Robot.prototype, {
getDisplay() {
return ""
},
updateDisplay() {
this.display.innerHTML = this.getDisplay()
},
drive(force=0) {
/* Apply a force to the robot to move it.

View File

@ -230,3 +230,16 @@ class Gyroscope extends Sensor {
this.valueStr = format(this.value) + '&deg;'
}
}
class CollisionSensor extends DistanceSensor {
constructor(id, props) {
super(id, Object.assign({
maxVal: 8
}, props))
}
sense() {
super.sense()
this.value = this.value != Infinity
this.valueStr = this.value ? "yes" : "no"
}
}

View File

@ -48,3 +48,7 @@ function gaussNoise(sigma=1) {
const x1 = 1.0 - Math.random();
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
};
const zip = (arr, ...arrs) => {
return arr.map((val, i) => arrs.reduce((a, arr) => [...a, arr[i]], [val]));
}
const sum = (arr) => arr.reduce((a,b) => a+b)