Implement hebbian didabots
parent
0bc6937afe
commit
7ff8c1b006
|
@ -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="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="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="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><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>
|
<tr><td></td></tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
|
|
|
@ -30,11 +30,11 @@ Feel free to use any of this code.
|
||||||
"use strict"
|
"use strict"
|
||||||
// Simulation settings; please change anything that you think makes sense.
|
// Simulation settings; please change anything that you think makes sense.
|
||||||
var simInfo = {
|
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
|
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
|
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)
|
robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down)
|
||||||
robotMass: 0.4, // robot mass (a.u)
|
robotMass: 0.4, // robot mass (a.u)
|
||||||
gravity: 0, // constant acceleration in Y-direction
|
gravity: 0, // constant acceleration in Y-direction
|
||||||
|
@ -43,8 +43,97 @@ var simInfo = {
|
||||||
debugMouse: true, // allow dragging any object with the mouse
|
debugMouse: true, // allow dragging any object with the mouse
|
||||||
wanderRate: 0.0002,
|
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
|
// our own lemming implementation
|
||||||
class Lemming extends Robot {
|
class Lemming extends SVGRobot {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(Object.assign({
|
super(Object.assign({
|
||||||
sensors: [
|
sensors: [
|
||||||
|
@ -321,7 +410,7 @@ function initSimulation() {
|
||||||
sim.init()
|
sim.init()
|
||||||
let noRobots = +document.getElementById('robotAmnt').value
|
let noRobots = +document.getElementById('robotAmnt').value
|
||||||
while(noRobots--) {
|
while(noRobots--) {
|
||||||
sim.addRobot(new Lemming({
|
sim.addRobot(new HebbDidaBot({
|
||||||
// random color
|
// random color
|
||||||
color: [Math.random()*255, Math.random()*255, Math.random()*255], // color of the robot shape
|
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
|
init: {x: 50*noRobots + 50, y: 50, angle: 0}, // initial position and orientation
|
||||||
|
@ -359,10 +448,33 @@ function redblocksatwall(sim) {
|
||||||
})
|
})
|
||||||
return result
|
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
|
// automatically run the experiments based on the values from the html
|
||||||
async function experimentRunner(stopCondition=blocksSorted) {
|
async function experimentRunner(stopCondition) {
|
||||||
initSimulation()
|
initSimulation()
|
||||||
while(!stopCondition(sim) && sim.curSteps < simInfo.maxSteps) {
|
while((stopCondition ? !stopCondition(sim) : true) && sim.curSteps < simInfo.maxSteps) {
|
||||||
sim.runSteps(500)
|
sim.runSteps(500)
|
||||||
// take a break for the gui to stay responsive
|
// take a break for the gui to stay responsive
|
||||||
await promiseTimeout(50)
|
await promiseTimeout(50)
|
||||||
|
@ -371,8 +483,9 @@ async function experimentRunner(stopCondition=blocksSorted) {
|
||||||
// save data
|
// save data
|
||||||
const rv = {
|
const rv = {
|
||||||
steps: sim.curSteps,
|
steps: sim.curSteps,
|
||||||
done: stopCondition(sim),
|
//done: stopCondition(sim),
|
||||||
redblocks: redblocksatwall(sim),
|
//redblocks: redblocksatwall(sim),
|
||||||
|
clusters: countclusters(sim),
|
||||||
robots: sim.robots.length
|
robots: sim.robots.length
|
||||||
}
|
}
|
||||||
sim.destroy()
|
sim.destroy()
|
||||||
|
@ -394,15 +507,15 @@ class ResultsTable {
|
||||||
clear() {
|
clear() {
|
||||||
this.elem.innerHTML = "<tr><th colspan=4>Experiment Results</th></tr>"
|
this.elem.innerHTML = "<tr><th colspan=4>Experiment Results</th></tr>"
|
||||||
const tr = document.createElement('tr')
|
const tr = document.createElement('tr')
|
||||||
const headers = ["robots", "steps", "sorted", "red blocks@wall"]
|
const headers = ["robots", "steps", "clusters"]
|
||||||
headers.forEach(txt => {
|
headers.forEach(txt => {
|
||||||
tr.appendChild(document.createElement('th')).appendChild(document.createTextNode(txt))
|
tr.appendChild(document.createElement('th')).appendChild(document.createTextNode(txt))
|
||||||
})
|
})
|
||||||
this.elem.appendChild(tr)
|
this.elem.appendChild(tr)
|
||||||
}
|
}
|
||||||
add({steps, done, redblocks, robots}) {
|
add({steps, clusters, robots}) {
|
||||||
const tr = document.createElement('tr')
|
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))
|
tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(txt))
|
||||||
})
|
})
|
||||||
this.elem.appendChild(tr)
|
this.elem.appendChild(tr)
|
||||||
|
@ -427,6 +540,9 @@ class Bay {
|
||||||
robot.sensors.forEach(sensor => {
|
robot.sensors.forEach(sensor => {
|
||||||
makeInteractiveElement(sensor, this.elem)
|
makeInteractiveElement(sensor, this.elem)
|
||||||
})
|
})
|
||||||
|
const bayDisplay = document.getElementById('bayDisplay')
|
||||||
|
bayDisplay.innerHTML = ""
|
||||||
|
bayDisplay.appendChild(robot.display)
|
||||||
this.repaint()
|
this.repaint()
|
||||||
}
|
}
|
||||||
repaint() {
|
repaint() {
|
||||||
|
@ -447,7 +563,7 @@ class Bay {
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
document.getElementById('SensorLabel').innerHTML = sensorString;
|
document.getElementById('SensorLabel').innerHTML = sensorString;
|
||||||
document.getElementById('bayDisplay').innerHTML = robot.getDisplay()
|
robot.updateDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transformMouse({x,y}) {
|
transformMouse({x,y}) {
|
||||||
|
@ -470,19 +586,8 @@ function loadFromSVG() {
|
||||||
|
|
||||||
return vertexSets;
|
return vertexSets;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Robot(robotInfo) {
|
function Robot(robotInfo) {
|
||||||
// load robot's body shape from SVG file
|
this.body = this.makeBody(robotInfo)
|
||||||
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);
|
|
||||||
|
|
||||||
Matter.World.add(sim.world, this.body);
|
Matter.World.add(sim.world, this.body);
|
||||||
Matter.Body.setAngle(this.body, robotInfo.init.angle);
|
Matter.Body.setAngle(this.body, robotInfo.init.angle);
|
||||||
|
|
||||||
|
@ -492,8 +597,20 @@ function Robot(robotInfo) {
|
||||||
|
|
||||||
// attach its helper functions
|
// attach its helper functions
|
||||||
this.info = robotInfo;
|
this.info = robotInfo;
|
||||||
|
this.display = document.createElement('span')
|
||||||
}
|
}
|
||||||
Object.assign(Robot.prototype, {
|
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) {
|
rotate(torque=0) {
|
||||||
/* Apply a torque to the robot to rotate it.
|
/* Apply a torque to the robot to rotate it.
|
||||||
*
|
*
|
||||||
|
@ -506,6 +623,9 @@ Object.assign(Robot.prototype, {
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
|
updateDisplay() {
|
||||||
|
this.display.innerHTML = this.getDisplay()
|
||||||
|
},
|
||||||
|
|
||||||
drive(force=0) {
|
drive(force=0) {
|
||||||
/* Apply a force to the robot to move it.
|
/* Apply a force to the robot to move it.
|
||||||
|
|
13
sensors.js
13
sensors.js
|
@ -230,3 +230,16 @@ class Gyroscope extends Sensor {
|
||||||
this.valueStr = format(this.value) + '°'
|
this.valueStr = format(this.value) + '°'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
4
utils.js
4
utils.js
|
@ -48,3 +48,7 @@ function gaussNoise(sigma=1) {
|
||||||
const x1 = 1.0 - Math.random();
|
const x1 = 1.0 - Math.random();
|
||||||
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
|
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)
|
||||||
|
|
Loading…
Reference in New Issue