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="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>
|
||||
|
|
|
@ -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.
|
||||
|
|
13
sensors.js
13
sensors.js
|
@ -230,3 +230,16 @@ class Gyroscope extends Sensor {
|
|||
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();
|
||||
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