Compare commits

..

1 Commits

Author SHA1 Message Date
Ugo Finnendahl e8644bd054 silder 5 years ago
  1. 47
      css/src/style.scss
  2. 77
      css/style.css
  3. 2
      css/style.min.css
  4. BIN
      img/robot.png
  5. BIN
      img/station.png
  6. 83
      index.html
  7. 2
      js/controls.js
  8. 35
      js/rl.js
  9. 879
      js/view.js

@ -1,11 +1,8 @@
// compileCompressed
*{ *{
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body{ body{
margin: 0;
padding: 0;
// background-color: ; // background-color: ;
font-family: sans-serif; font-family: sans-serif;
} }
@ -16,30 +13,17 @@ body{
#canvas{ #canvas{
height: 100%; height: 100%;
} }
nav{ nav{
position: absolute; position: absolute;
top: 10px; top: 10px;
left: 10px; left: 10px;
} }
button{
margin: 0.3em;
}
.absolute{ .absolute{
position: absolute; position: absolute;
top:0; top:0;
left:0; left:0;
} }
.stage{
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.plot{ .plot{
position: absolute; position: absolute;
top: 2vh; top: 2vh;
@ -49,38 +33,9 @@ button{
} }
.sliders{ .sliders{
position: absolute;
top: 20vh;
left: 2vw;
width: 20vw;
}
#formula{
position: absolute;
top: 71.5vh;
width: 90vw;
}
.score{
position: absolute; position: absolute;
top: 7vh; top: 7vh;
left: 2vw; left: 2vw;
width: 20vw; width: 20vw;
} height: 10vw;
.lightbox{
padding: 2em;
position: absolute;
top:0;
left: 50%;
transform: translateX(-50%) translateY(-100%);
z-index: 10;
max-width: 50%;
background-color: #BBB;
transition: all 1s;
&.active{
top:20%;
transform: translateX(-50%) translateY(-50%);
}
} }

@ -1,77 +0,0 @@
* {
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
#container {
height: 100vh;
position: relative;
}
#canvas {
height: 100%;
}
nav {
position: absolute;
top: 10px;
left: 10px;
}
.absolute {
position: absolute;
top: 0;
left: 0;
}
.plot {
position: absolute;
top: 2vh;
right: 2vw;
width: 20vw;
height: 10vw;
}
.sliders {
position: absolute;
top: 20vh;
left: 2vw;
width: 20vw;
}
#formula {
position: absolute;
top: 71.5vh;
width: 90vw;
}
.score {
position: absolute;
top: 7vh;
left: 2vw;
width: 20vw;
}
.lightbox {
padding: 2em;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%) translateY(-100%);
z-index: 10;
width: 50%;
height: 40%;
background-color: #BBB;
transition: all 1s;
}
.lightbox.active {
top: 50%;
transform: translateX(-50%) translateY(-50%);
}

2
css/style.min.css vendored

@ -1 +1 @@
*{margin:0;padding:0}body{margin:0;padding:0;font-family:sans-serif}#container{height:100vh;position:relative}#canvas{height:100%}nav{position:absolute;top:10px;left:10px}button{margin:0.3em}.absolute{position:absolute;top:0;left:0}.stage{display:flex;justify-content:center;align-items:center;height:100vh}.plot{position:absolute;top:2vh;right:2vw;width:20vw;height:10vw}.sliders{position:absolute;top:20vh;left:2vw;width:20vw}#formula{position:absolute;top:71.5vh;width:90vw}.score{position:absolute;top:7vh;left:2vw;width:20vw}.lightbox{padding:2em;position:absolute;top:0;left:50%;transform:translateX(-50%) translateY(-100%);z-index:10;max-width:50%;background-color:#BBB;transition:all 1s}.lightbox.active{top:20%;transform:translateX(-50%) translateY(-50%)} *{margin:0;padding:0}body{font-family:sans-serif}#container{height:100vh;position:relative}#canvas{height:100%}nav{position:absolute;top:10px;left:10px}.absolute{position:absolute;top:0;left:0}.plot{position:absolute;top:2vh;right:2vw;width:20vw;height:10vw}.sliders{position:absolute;top:7vh;left:2vw;width:20vw;height:10vw}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="https://unpkg.com/konva@4.0.0/konva.min.js"></script> <script src="https://unpkg.com/konva@4.0.0/konva.min.js"></script>
<script src='https://unpkg.com/vue/dist/vue.min.js'></script> <script src='https://unpkg.com/vue/dist/vue.js'></script>
<script src='https://cdn.jsdelivr.net/npm/vue-konva@2.0.11/umd/vue-konva.min.js'></script> <script src='https://cdn.jsdelivr.net/npm/vue-konva@2.0.11/umd/vue-konva.min.js'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
@ -15,50 +15,61 @@
<!-- The loading of KaTeX is deferred to speed up page rendering --> <!-- The loading of KaTeX is deferred to speed up page rendering -->
<script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script>
<title>RL exhibit - prototype</title> <title>RL exhibit - prototype</title>
<link rel="stylesheet" href="https://gitcdn.xyz/cdn/jzilg/embellish.css/fff9961c5fec2d1c8ff53c87b12e18d5c8db7761/embellish.min.css">
<link rel="stylesheet" href="css/style.min.css"> <link rel="stylesheet" href="css/style.min.css">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<rl-local class="stage" :machine="machine" :maze="maze" :config="stage_config" v-show="isActive('local')"></rl-local> <v-stage ref="stage" :config="stage_config">
<!-- <v-layer ref="local_layer" :config="local_layer">
<rl-map class="stage" :machine="machine" :maze="maze" :config="stage_config" v-show="isActive('global')"></rl-map> <v-group ref="map_group" :config="map_config">
<v-rect v-for="(t_type, idx) in maze.map.flat()" :config="get_tile_config(idx, t_type, true)" :key="idx"></v-rect>
<line-chart css-classes="plot" :chart-data="datacollection" :options="plot_options" v-show="isActive('plot')"></line-chart> <v-regular-polygon :config="agent_config"></v-regular-polygon>
</v-group>
<div class="sliders" v-show="isActive('sliders')"> </v-layer> -->
<h1>Learning Rate {{machine.learning_rate}}</h1> <v-layer ref="map_layer">
<vue-slider v-model="machine.learning_rate" :drag-on-click="true" v-bind="slider_config"></vue-slider> <v-group ref="mini_map_group" :config="mini_map_config">
<h1>Discount Factor {{machine.discount_factor}}</h1> <!-- <v-group ref="grid_group">
<vue-slider v-model="machine.discount_factor" :drag-on-click="true" v-bind="slider_config"></vue-slider> <v-line v-for="y in maze.height+1" :config="get_grid_line_config(y-1, true)"></v-line>
<h1>Epsilon {{machine.epsilon}}</h1> <v-line v-for="x in maze.width+1" :config="get_grid_line_config(x-1)"></v-line>
<vue-slider v-model="machine.epsilon" :drag-on-click="true" v-bind="slider_config"></vue-slider> </v-group> -->
<div id="formula"></div> <v-rect v-for="(t_type, idx) in maze.map.flat()" :config="get_tile_config(idx, t_type)" :key="idx" ></v-rect>
<v-group v-for="(action,idx) in q_table" :config="get_field_config(idx)">
<v-shape v-for="(value, key) in action" :config="get_triangle_config(value, key)"></v-shape>
<v-text v-for="i in 4" :config="get_q_text_config(action,i)"></v-text>
</v-group>
<v-regular-polygon :config="agent_config"></v-regular-polygon>
</v-group>
</v-layer>
</v-stage>
<line-chart css-classes="plot" :chart-data="datacollection" :options="plot_options"></line-chart>
<div class="sliders">
<h1>Learning Rate {{learning_rate}}</h1>
<vue-slider v-model="learning_rate" :drag-on-click="true" v-bind="slider_config"></vue-slider>
<h1>Discount Factor {{discount_factor}}</h1>
<vue-slider v-model="discount_factor" :drag-on-click="true" v-bind="slider_config"></vue-slider>
<h1>Epsilon {{epsilon}}</h1>
<vue-slider v-model="epsilon" :drag-on-click="true" v-bind="slider_config"></vue-slider>
<h1>Current Score</h1>
<h2>{{score}}</h2>
<div id="test" style="position: absolute;bottom: 10vh;width:90vw"></div>
</div> </div>
<div class="score" v-show="isActive('score')">
<h1>Current Energy</h1>
<h2>{{machine.score}}</h2>
</div>
<navi-gation :options="navigation" v-show="isActive('navi')"></navi-gation>
</div> </div>
<nav>
<button class="button" onclick="machine.run(1)">run 1 episode!</button>
<button class="button" onclick="machine.run(100)">run 100 episodes!</button>
<button class="button" onclick="machine.auto_step();">auto step!</button>
<button class="button" onclick="machine.greedy_step();">greedy step!</button>
<button class="button" onclick="machine.reset_machine()">reset machine</button>
</nav>
<script> <script>
var map = [ var map = [
[0, 0, 1, 8, 0, 0, 0, 0, 0, 0], [0, 0, 1, 8, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0],
[1, 0, 0, 0, 0, 1, 0, 1, 0, 1], [1, 0, 0, 0, 0, 1, 0, 1],
[0, 0, 0, 1, 0, 0, 0, 1, 0, 1], [0, 0, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 1, 0, 0], [2, 0, 1, 0, 1, 0, 0, 1]
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[1, 0, 1, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
[2, 0, 0, 0, 1, 0, 1, 0, 0, 1]
]; ];
</script> </script>
<script src="js/rl.js"></script> <script src="js/rl.js"></script>

2
js/controls.js vendored

@ -27,7 +27,7 @@ function key_callback(e) {
break; break;
} }
var ret = 1; var ret = 1;
if (tmp != undefined && document.querySelector(".lightbox.active") == null){ if (tmp != undefined){
ret = machine.step(tmp); ret = machine.step(tmp);
} }
// show_q_table(); // show_q_table();

@ -4,7 +4,6 @@ class RL_machine {
rewards, rewards,
start_state, start_state,
end_states, end_states,
start_score,
end_score, end_score,
learning_rate, learning_rate,
discount_factor, discount_factor,
@ -15,16 +14,11 @@ class RL_machine {
this.lr = learning_rate; this.lr = learning_rate;
this.df = discount_factor; this.df = discount_factor;
this.start_state = start_state; this.start_state = start_state;
this.start_score = start_score;
this.end_score = end_score; this.end_score = end_score;
this.end_states = end_states; this.end_states = end_states;
this.epsilon = epsilon; this.epsilon = epsilon;
this.q_table = this.actions_per_state.map((c) => c.reduce((o,n) => {o[n]=0; return o},{})); this.q_table = this.actions_per_state.map((c) => c.reduce((o,n) => {o[n]=0; return o},{}));
this.reset_machine(); this.reset_machine();
this.callback = null;
}
setCallback(cb){
this.callback = cb;
} }
reset_machine(){ reset_machine(){
for (var q in this.q_table){ for (var q in this.q_table){
@ -33,24 +27,17 @@ class RL_machine {
} }
} }
this.episode = 0; this.episode = 0;
this.state = this.start_state;
this.score = 0;
this.running = false; this.running = false;
this.score_history = []; this.score_history = [];
this.state = this.start_state;
this.score = this.start_score;
} }
new_episode(reason = "failed"){ new_episode(){
const reset = () => { // add_new_episode_callback
this.episode++; this.episode++;
this.score_history.push(this.score);
this.state = this.start_state; this.state = this.start_state;
this.score = this.start_score; this.score_history.push(this.score);
} this.score = 0;
// add_new_episode_callback
if (!this.running && this.callback) {
this.callback(reason).then((p) => reset());
} else {
reset();
}
} }
auto_step(){ auto_step(){
if (Math.random() < this.epsilon){ if (Math.random() < this.epsilon){
@ -65,12 +52,8 @@ class RL_machine {
step(action){ step(action){
this.state = this.update_q_table(this.state, action); this.state = this.update_q_table(this.state, action);
// add_new_step_callback // add_new_step_callback
if (this.end_states.indexOf(this.state) >= 0) { if (this.end_states.indexOf(this.state) >= 0 || this.score < this.end_score){
this.new_episode("success"); this.new_episode();
return 2
}
if (this.score <= this.end_score){
this.new_episode("failed");
return 2 return 2
} }
return 1 return 1
@ -208,4 +191,4 @@ var maze = new Maze(map, reward);
var learning_rate = 0.75; var learning_rate = 0.75;
var discount_factor = 0.8; var discount_factor = 0.8;
var machine = new RL_machine(maze.actions, maze.transactions, maze.rewards, maze.start_state, maze.end_states, 50, 0, learning_rate, discount_factor, 0.2); var machine = new RL_machine(maze.actions, maze.transactions, maze.rewards, maze.start_state, maze.end_states, -999, learning_rate, discount_factor, 0.2);

@ -1,24 +1,31 @@
// ---------------------------------------------------------------------------- Vue.component('line-chart', {
// ------------------------------- Utils -------------------------------------- extends: VueChartJs.Line,
// ---------------------------------------------------------------------------- mixins: [VueChartJs.mixins.reactiveProp],
props: ['options'],
function defer() { // mixins: [VueChartJs.mixins.reactiveData],
var res, rej; // props: ['options','labels', 'datasets'],
// watch: {
var promise = new Promise((resolve, reject) => { // 'labels': function(new_val) {
res = resolve; // this.chartData = {
rej = reject; // 'labels': new_val,
}); // 'datasets': this.datasets};
// },
promise.resolve = res; // 'datasets': {
promise.reject = rej; // deep:true,
// handler: function(new_val) {
// this.chartData = {
// 'labels': this.labels,
// 'datasets': new_val};
// }
// }
// },
mounted() {
this.renderChart(this.chartData, this.options);
},
return promise; })
}
// ---------------------------------------------------------------------------- var palette = ['#00429d', '#06449d', '#0d469d', '#12489d', '#164a9d', '#1a4c9c', '#1d4e9c', '#20509c', '#23529c', '#26549c', '#28569b', '#2b589b', '#2d5a9b', '#305c9b', '#325e9a', '#34609a', '#37629a', '#39649a', '#3b6699', '#3d6899', '#3f6999', '#416b98', '#436d98', '#456f97', '#477197', '#497397', '#4b7596', '#4d7796', '#4f7995', '#517b95', '#537d94', '#557f94', '#578193', '#598392', '#5b8592', '#5d8791', '#5f8991', '#618a90', '#638c8f', '#658e8f', '#67908e', '#69928d', '#6b948d', '#6d968c', '#6f988b', '#719a8b', '#739c8a', '#759e89', '#77a088', '#79a287', '#7ba386', '#7da586', '#80a785', '#82a984', '#84ab83', '#86ad82', '#88af81', '#8ab180', '#8cb37f', '#8fb57e', '#91b77d', '#93b87c', '#95ba7a', '#97bc79', '#9abe78', '#9cc077', '#9ec276', '#a1c474', '#a3c673', '#a5c872', '#a8c970', '#aacb6f', '#accd6e', '#afcf6c', '#b1d16b', '#b4d369', '#b6d568', '#b8d766', '#bbd864', '#bdda63', '#c0dc61', '#c2de5f', '#c5e05d', '#c8e25b', '#cae459', '#cde657', '#cfe755', '#d2e953', '#d5eb50', '#d7ed4e', '#daef4b', '#ddf049', '#dff246', '#e2f443', '#e5f640', '#e8f83c', '#ebfa39', '#edfb35', '#f0fd31', '#f3ff2c']
// -------------------------------- Plot --------------------------------------
// ----------------------------------------------------------------------------
Array.prototype.simpleSMA = function(N) { Array.prototype.simpleSMA = function(N) {
return this.map( return this.map(
@ -48,163 +55,184 @@ Array.prototype.max = function() {
}); });
}; };
Vue.component('line-chart', { app = new Vue({
extends: VueChartJs.Line, el: '#app',
mixins: [VueChartJs.mixins.reactiveProp], components: {
props: ['options'], VueSlider: window['vue-slider-component']
mounted() {
this.renderChart(this.chartData, this.options);
}, },
data: {
}) width: 0,
height: 0,
// ---------------------------------------------------------------------------- q_table: machine.q_table,
// --------------------------------- Map -------------------------------------- maze: maze,
// ---------------------------------------------------------------------------- state: {
x: 0,
var MapBase = Vue.component('MapBase', { y: 0
props: ['machine', 'maze', 'config'], },
data: function () { state_tween: new TimelineLite(),
return { score: machine.score,
robot_image: null, score_history: machine.score_history,
energy_image: null, labels: [],
learning_rate: machine.lr,
discount_factor: machine.df,
epsilon: machine.epsilon,
slider_config: {
min: 0,
max: 1,
duration: 0,
interval: 0.01,
tooltip: 'none'
} }
}, },
created() { created() {
// Resize handler
window.addEventListener('resize', this.handleResize)
this.handleResize();
// State wrapper
var s = machine.state;
var $this = this; var $this = this;
const robot_image = new window.Image(); this.state = this.s2p(s);
robot_image.src = "img/robot.png"; Object.defineProperty(machine, 'state', {
robot_image.onload = () => { get: function() {
$this.robot_image = robot_image; return this._state
};
const energy_image = new window.Image();
energy_image.src = "img/station.png";
energy_image.onload = () => {
$this.energy_image = energy_image;
};
}, },
computed: { set: function(ne) {
main_config: function(){ this._state = ne;
return { $this.handleState(this._state);
offset: {
x: -(this.config.width-this.base_size*this.maze.width)/2,
y: -(this.config.height-this.base_size*this.maze.height)/2,
}
} }
});
machine.state = s;
// Score wrapper
var s = machine.score;
var $this = this;
this.score = s;
Object.defineProperty(machine, 'score', {
get: function() {
return this._score
}, },
robot_config: function() { set: function(ne) {
return { this._score = ne;
height: this.base_size, $this.score = ne
width: this.base_size,
x: this.center,
y: this.center,
image: this.robot_image,
offset:{
x: this.base_size/2,
y: this.base_size/2,
}
} }
});
machine.score = s;
// Score history wrapper
var s = machine.score_history;
var $this = this;
this.score_history = s;
Object.defineProperty(machine, 'score_history', {
get: function() {
return this._score_history
}, },
energy_config: function() { set: function(ne) {
return { this._score_history = ne;
height: this.base_size, $this.score_history = ne
width: this.base_size,
offset: {
x: this.base_size/2,
y: this.base_size/2
},
image: this.energy_image,
} }
});
machine.score_history = s;
}, },
strokeW: function() { destroyed() {
return this.base_size / 50; window.removeEventListener('resize', this.handleResize)
},
base_size: function() {
return Math.min(this.config.height/this.maze.height, this.config.width/this.maze.width);
}, },
computed: {
datacollection: function() {
return {
labels: Array.from(Array(this.score_history.length).keys()),
datasets: [{
label: 'Data One',
backgroundColor: 'rgb(0,0,0,0)',
data: this.score_history,//.simpleSMA(Math.round(50)),
fill: false,
borderColor: 'rgb(255, 159, 64)',
pointRadius: 1,
}, },
methods: { // {
get_tile_type: function(state) { // label: 'Data One',
var pos = this.machine.s2p(state); // backgroundColor: 'rgb(0,0,0,0)',
if (pos.y > maze.height) { // data: this.score_history.max(),
return null; // fill: false,
} else if (pos.x > maze.width) { // borderColor: 'rgb(64, 159, 255)',
return null; // pointRadius: 1,
} else { // },
return maze.map[pos.y][pos.x]; ]
} }
}, },
get_field_config: function(state) { plot_options: function() {
var pos = this.machine.s2p(state); var $this = this;
return { return {
x: this.base_size * pos.x+this.base_size/2, responsive: true,
y: this.base_size * pos.y+this.base_size/2, maintainAspectRatio: false,
scales: {
xAxes: [{
// type: 'linear',
ticks: {
maxTicksLimit: 8,
maxRotation: 0,
} }
}]
}, },
get_tile_config: function(t_type) { legend: {
const layout = { display: false
width: this.base_size,
height: this.base_size,
stroke: '#ddd',
strokeWidth: this.strokeW,
offset: {
x: this.base_size/2,
y: this.base_size/2,
} }
};
switch (t_type) {
case tile.regular:
return {
...layout,
fill: '#fff',
opacity: 1,
} }
case tile.end: },
stage_config: function() {
return { return {
...layout, width: this.width,
fill: '#0eb500', height: this.height,
opacity: 1,
} }
case tile.start: },
mini_map_config: function() {
return { return {
...layout, x: this.width / 2 - (this.base_size * (this.maze.width) / 2),
fill: '#ff0008', y: this.height / 2 - (this.base_size * (this.maze.height) / 2),
opacity: 1, scale: {
x: 1,
y: 1
} }
case tile.dangerous:
return {
...layout,
fill: '#FF7B17',
opacity: 1,
} }
case tile.wall: },
local_layer: function() {
return { return {
...layout, x: this.width / 2,
fill: '#000000', y: this.height / 2,
opacity: 1, scale: {
x: 2,
y: 2
} }
} }
}, },
map_config: function() {
return {
x: this.base_size * (this.maze.width - this.state.x),
y: this.base_size * (this.maze.height - this.state.y),
offset: {
x: this.base_size * this.maze.width + this.base_size / 2,
y: this.base_size * this.maze.height + this.base_size / 2,
}
}
}, },
}) agent_config: function() {
//-----------------------------------------------------------------------------
var palette = ['#d2000d', '#d30512', '#d40a17', '#d50f1c', '#d61420', '#d71a25', '#d71f2a', '#d8242f', '#d92934', '#da2e39', '#db333d', '#dc3842', '#dd3d47', '#de424c', '#df4751', '#e04d56', '#e0525a', '#e1575f', '#e25c64', '#e36169', '#e4666e', '#e56b73', '#e67077', '#e7757c', '#e87a81', '#e98086', '#e9858b', '#ea8a90', '#eb8f95', '#ec9499', '#ed999e', '#ee9ea3', '#efa3a8', '#f0a8ad', '#f1adb2', '#f2b3b6', '#f2b8bb', '#f3bdc0', '#f4c2c5', '#f5c7ca', '#f6cccf', '#f7d1d3', '#f8d6d8', '#f9dbdd', '#fae0e2', '#fbe6e7', '#fbebec', '#fcf0f0', '#fdf5f5', '#fefafa', '#ffffff', '#fafcfa', '#f5f9f5', '#f0f6f0', '#ebf3ec', '#e6f1e7', '#e1eee2', '#dcebdd', '#d7e8d8', '#d3e5d3', '#cee2cf', '#c9dfca', '#c4dcc5', '#bfd9c0', '#bad6bb', '#b5d4b6', '#b0d1b2', '#abcead', '#a6cba8', '#a1c8a3', '#9cc59e', '#97c299', '#92bf95', '#8dbc90', '#88b98b', '#84b786', '#7fb481', '#7ab17c', '#75ae77', '#70ab73', '#6ba86e', '#66a569', '#61a264', '#5c9f5f', '#579c5a', '#529a56', '#4d9751', '#48944c', '#439147', '#3e8e42', '#398b3d', '#348839', '#308534', '#2b822f', '#267f2a', '#217d25', '#1c7a20', '#17771c', '#127417', '#0d7112', '#086e0d']
Vue.component('rl-map', {
extends: MapBase,
computed: {
robot_config: function() {
return { return {
height: this.base_size, sides: 5,
width: this.base_size, radius: this.base_size / 3,
x: this.base_size * this.machine.state.x, fill: '#00D2FF',
y: this.base_size * this.machine.state.y, stroke: 'black',
image: this.robot_image, strokeWidth: this.strokeW,
offset: {
x: -this.base_size / 2,
y: -this.base_size / 2
},
x: this.base_size * this.state.x,
y: this.base_size * this.state.y,
} }
}, },
base_size: function() {
return Math.min(this.stage_config.height * 0.8 / this.maze.height, this.stage_config.width * 0.5 / this.maze.width);
},
strokeW: function() {
return this.base_size / 50;
},
extreme_q_values: function(){ extreme_q_values: function(){
var max = -10*30; var max = -10*30;
var min = 10*30; var min = 10*30;
@ -221,6 +249,66 @@ Vue.component('rl-map', {
} }
}, },
methods: { methods: {
s2p: function(state) {
return {
x: (state % this.maze.width),
y: Math.floor(state / this.maze.width),
}
},
p2s: function(x, y) {
return x + y * this.maze.width;
},
handleResize: function() {
this.width = window.innerWidth;
this.height = window.innerHeight;
},
handleState: function(s) {
if (!machine.running) {
this.state_tween.to(this.state, 0.2, {
x: this.s2p(s).x,
y: this.s2p(s).y
});
} else {
this.state = this.s2p(s);
}
// this.hidden_state = s;
},
get_grid_line_config: function(idx, y = false) {
var offset = this.strokeW / 2;
if (y) {
var points = [-offset, Math.round(idx * this.base_size), this.base_size * this.maze.width + offset, Math.round(idx * this.base_size)];
} else {
var points = [Math.round(idx * this.base_size), -offset, Math.round(idx * this.base_size), this.base_size * this.maze.height + offset];
}
return {
points: points,
stroke: '#ddd',
strokeWidth: this.strokeW,
}
},
get_tile_type: function(state) {
var pos = this.s2p(state);
if (pos.y > maze.height) {
return null;
} else if (pos.x > maze.width) {
return null;
} else {
return maze.map[pos.y][pos.x];
}
},
in_plus: function(pos1, pos2) {
if (Math.abs(pos1.x - pos2.x) + Math.abs(pos1.y - pos2.y) < 2) {
return true;
}
return false;
},
get_field_config: function(state) {
var pos = this.s2p(state);
return {
x: this.base_size * pos.x+this.base_size/2,
y: this.base_size * pos.y+this.base_size/2,
}
},
get_q_text_config: function (val, i) { get_q_text_config: function (val, i) {
var off, key; var off, key;
switch (i) { switch (i) {
@ -260,13 +348,13 @@ Vue.component('rl-map', {
fontSize: this.base_size/7, fontSize: this.base_size/7,
fontFamily: 'Calibri', fontFamily: 'Calibri',
fill: 'black', fill: 'black',
text: +val[key].toPrecision(3)+'', text: +val[key].toFixed(2)+'',
width: this.base_size-20, width: this.base_size-5,
height: this.base_size-34, height: this.base_size-5,
...off, ...off,
offset: { offset: {
x: (this.base_size-20)/2, x: (this.base_size-5)/2,
y: (this.base_size-34)/2, y: (this.base_size-5)/2,
} }
} }
}, },
@ -287,478 +375,109 @@ Vue.component('rl-map', {
break; break;
} }
var $this = this; var $this = this;
var norma_value = value>0 ? (value+1000)/(2000) : (value+30)/60; var norma_value = (value-this.extreme_q_values.min)/((this.extreme_q_values.max-this.extreme_q_values.min)||1);
return { return {
sceneFunc: function(context, shape) { sceneFunc: function(context, shape) {
context.beginPath(); context.beginPath();
var width = $this.base_size / 5; context.moveTo(0, 0);
var arrow_w = $this.base_size / 2; context.lineTo($this.base_size / 2, $this.base_size / 2);
var stumpf = $this.base_size / 6; context.lineTo($this.base_size / 2, -$this.base_size / 2);
var arrow_l = $this.base_size / 5; context.lineTo(0, 0);
context.moveTo($this.base_size/2-stumpf-arrow_l, width/2);
context.lineTo($this.base_size/2-stumpf, width/2);
context.lineTo($this.base_size/2-stumpf, arrow_w/2);
context.lineTo($this.base_size/2-2, 0);
context.lineTo($this.base_size/2-stumpf, -arrow_w/2);
context.lineTo($this.base_size/2-stumpf, -width/2);
context.lineTo($this.base_size/2-stumpf-arrow_l, -width/2);
context.lineTo($this.base_size/2-stumpf-arrow_l, width/2);
context.closePath(); context.closePath();
// (!) Konva specific method, it is very important // (!) Konva specific method, it is very important
context.fillStrokeShape(shape); context.fillStrokeShape(shape);
}, },
fill: palette[Math.round(norma_value*100)], fill: palette[Math.round(norma_value*99)],
stroke: 'black', stroke: 'black',
strokeWidth: 1, strokeWidth: 0,
rotation: rot, rotation: rot,
} }
}, },
}, get_tile_config: function(i, t_type, local = false) {
template: var pos = this.s2p(i);
`<v-stage ref="stage" :config="config">
<v-layer ref="map_layer" :config="main_config">
<v-group ref="map_group">
<v-group :key="'tile'+idx" v-for="(t_type, idx) in maze.map.flat()" :config="get_field_config(idx)">
<v-rect :config="get_tile_config(t_type)"></v-rect>
<v-image :config="energy_config" v-if="t_type==8"></v-image>
</v-group>
<v-group :key="'qgroup'+idx" v-for="(action, idx) in machine.q_table" :config="get_field_config(idx)">
<v-shape :key="'qvalshape'+idx+key" v-for="(value, key) in action" :config="get_triangle_config(value, key)"></v-shape>
<v-text :key="'qval'+idx+i" v-for="i in 4" :config="get_q_text_config(action,i)"></v-text>
</v-group>
<v-image :config="robot_config"></v-image>
</v-group>
</v-layer>
</v-stage>`
})
// ----------------------------------------------------------------------------
// -------------------------------- Local -------------------------------------
// ----------------------------------------------------------------------------
Vue.component('rl-local', {
extends: MapBase,
computed: {
main_config: function(){
return {
offset: {
x: -(this.config.width-this.base_size*3)/2,
y: -(this.config.height-this.base_size*3)/2,
}
}
},
local_config: function() {
return {
x: -(this.machine.state.x)*this.base_size,
y: -(this.machine.state.y)*this.base_size,
offset: {
x: -this.base_size,
y: -this.base_size,
}
}
},
base_size: function() {
return Math.min(this.config.height/3, this.config.width/3);
},
center: function() {
return 3*this.base_size / 2;
},
local_area: function() {
const x = Math.round(this.machine.state.x);
const y = Math.round(this.machine.state.y);
let arr = [[x,y-1],[x+1,y],[x,y+1],[x-1,y],[x,y]];
return arr.filter((p) => p[0] < this.maze.width && p[1] < this.maze.height &&
p[0] >= 0 && p[1] >= 0)
.map((p) => [this.maze.map[p[1]][p[0]], p[1]*this.maze.width+p[0]]);
},
},
methods: {
end: function(pos){
return this.maze.get_states(tile.end).indexOf(pos) >= 0;
},
id_to_dir: function(id){
switch (id) {
case 0:
return dir.UP;
case 1:
return dir.RIGHT;
case 2:
return dir.DOWN;
case 3:
return dir.LEFT;
default:
return undefined;
}
},
handleMouseEnter(e) {
const stage = e.target.getStage();
stage.container().style.cursor = "pointer";
},
handleMouseLeave(e) {
const stage = e.target.getStage();
stage.container().style.cursor = "default";
},
get_local_tile_config: function(i, t_type) {
// var pos = this.s2p(i);
// in plus
var over = {}; var over = {};
if (i != this.machine.p2s(Math.round(this.machine.state.x), Math.round(this.machine.state.y)) && // not in plus
t_type != tile.wall) { if (local) {
if (!this.in_plus(this.s2p(i), {
x: Math.round(this.state.x),
y: Math.round(this.state.y)
})) {
over = { over = {
opacity: 0,
fill: "#eee"
};
} else if (i != this.p2s(Math.round(this.state.x), Math.round(this.state.y))) {
over = {
opacity: 1,
fill: "#eee"
};
}
}
const layout = {
x: this.base_size * pos.x,
y: this.base_size * pos.y,
width: this.base_size, width: this.base_size,
height: this.base_size, height: this.base_size,
stroke: '#ddd', stroke: '#ddd',
strokeWidth: this.strokeW, strokeWidth: this.strokeW,
offset: {
x: this.base_size/2,
y: this.base_size/2,
},
opacity: 1,
fill: "#eee",
}
}
return over;
},
},
template:
`<v-stage ref="stage" :config="config">
<v-layer ref="map_layer" :config="main_config">
<v-group ref="map_group" :config="local_config">
<v-group :key="pair[1]" v-for="(pair, idx) in local_area" :config="get_field_config(pair[1])">
<v-rect :config="get_tile_config(pair[0])"></v-rect>
<v-image :config="energy_config" v-if="end(pair[1])"></v-image>
<v-rect :config="get_local_tile_config(pair[1], pair[0])" @click="id_to_dir(idx) && machine.object.step(id_to_dir(idx))" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave"></v-rect>
</v-group>
</v-group>
<v-image :config="robot_config"></v-image>
</v-layer>
</v-stage>`
})
Vue.component('navi-gation', {
props: ["options"],
template: `
<nav class="navi">
<button v-for="(item, key) in options" v-on:click="item">{{ key }}</button>
</nav>`
});
// ----------------------------------------------------------------------------
// ------------------------------ lightbox ------------------------------------
// ----------------------------------------------------------------------------
var light_box = {
data: {
content: "",
options: [],
active: false,
},
methods:{
close: function(){
this.active = false;
},
popup: function(content, options){
this.content = content;
var answer = defer();
var $this = this;
this.options = options.reduce((old, opt) => {
old[opt] = function(){
$this.active = false;
answer.resolve(opt);
}
return old
}, {});
this.active = true;
return answer;
}
},
template: `
<div class="lightbox" v-bind:class="{ active: active }">{{ content }}
<div class="options">
<button :key="key" v-for="(item, key) in options" v-on:click="item">{{ key }}</button>
</div>
</div>`
}
const PopupLibrary = {
install(Vue, options = {}) {
const root = new Vue(light_box)
// Mount root Vue instance on new div element added to body
root.$mount(document.body.appendChild(document.createElement('div')))
Vue.prototype.$lightbox = root;
}
}
window.Vue.use(PopupLibrary)
// ----------------------------------------------------------------------------
// -------------------------------- Main --------------------------------------
// ----------------------------------------------------------------------------
function makeMachineReactive(th, machine){
var $this = th;
$this.machine.s2p = function(state) {
return {
x: (state % $this.maze.width),
y: Math.floor(state / $this.maze.width),
}
};
$this.machine.p2s = function(x, y) {
return x + y * $this.maze.width;
}; };
switch (t_type) {
// Score wrapper case tile.regular:
var s = machine.score;
$this.machine.score = s;
Object.defineProperty(machine, 'score', {
get: function() {
return this._score
},
set: function(ne) {
this._score = ne;
$this.machine.score = ne
}
});
machine.score = s;
// Score history wrapper
var s = machine.score_history;
$this.machine.score_history = s;
Object.defineProperty(machine, 'score_history', {
get: function() {
return this._score_history
},
set: function(ne) {
this._score_history = ne;
$this.machine.score_history = ne
}
});
machine.score_history = s;
// State wrapper
var s = machine.state;
$this.machine.state = $this.machine.s2p(s);
Object.defineProperty(machine, 'state', {
get: function() {
return this._state
},
set: function(ne) {
this._state = ne;
$this.handleState(this._state);
}
});
machine.state = s;
$this.machine.object.setCallback($this.onNewEpisode);
}
app = new Vue({
el: '#app',
components: {
VueSlider: window['vue-slider-component'],
},
data: {
state: null,
maze: maze,
machine: {
object: machine,
q_table: machine.q_table,
state: {
x:0,
y:0,
},
state_tween: new TimelineLite(),
learning_rate: machine.lr,
discount_factor: machine.df,
epsilon: machine.epsilon,
score: machine.score,
score_history: machine.score_history,
s2p: null,
p2s: null,
},
width: 0,
height: 0,
components: [],
navigation: {},
},
created() {
// Resize handler
window.addEventListener('resize', this.handleResize)
this.handleResize();
makeMachineReactive(this, machine);
this.state = "init";
},
destroyed() {
window.removeEventListener('resize', this.handleResize)
},
computed: {
stage_config: function() {
return { return {
x: 0, ...layout,
y: 0, fill: '#fff',
width: this.width*0.5, opacity: 1,
height: this.height*0.8, ...over,
} }
}, case tile.end:
slider_config: function(){
return { return {
min: 0, ...layout,
max: 1, fill: '#0eb500',
duration: 0, opacity: 1,
interval: 0.01, ...over,
tooltip: 'none'
} }
}, case tile.start:
datacollection: function() {
return { return {
labels: Array.from(Array(this.machine.score_history.length).keys()), ...layout,
datasets: [{ fill: '#ff0008',
label: 'Data One', opacity: 1,
backgroundColor: 'rgb(0,0,0,0)', ...over,
data: this.machine.score_history,//.simpleSMA(Math.round(50)),
fill: false,
borderColor: 'rgb(255, 159, 64)',
pointRadius: 1,
},
// {
// label: 'Data One',
// backgroundColor: 'rgb(0,0,0,0)',
// data: this.score_history.max(),
// fill: false,
// borderColor: 'rgb(64, 159, 255)',
// pointRadius: 1,
// },
]
} }
}, case tile.dangerous:
plot_options: function() {
var $this = this;
return { return {
responsive: true, ...layout,
maintainAspectRatio: false, fill: '#FF7B17',
scales: { opacity: 1,
xAxes: [{ ...over,
// type: 'linear',
ticks: {
maxTicksLimit: 8,
maxRotation: 0,
}
}]
},
legend: {
display: false
}
} }
}, case tile.wall:
}, return {
methods: { ...layout,
onEnterState: function(){}, fill: '#000000',
onLeaveState: function(){}, opacity: 1,
handleState: function(s) { ...over,
if (!this.machine.object.running) {
this.machine.state_tween.to(this.machine.state, 0.2, {
x: this.machine.s2p(s).x,
y: this.machine.s2p(s).y
});
} else {
this.machine.state = this.machine.s2p(s);
} }
},
handleResize: function() {
this.width = window.innerWidth;
this.height = window.innerHeight;
},
isActive: function(what){
return this.components.indexOf(what) >= 0;
},
changeState: function(state){
this.components = [];
this.navigation = {};
this.onEnterState = function(){};
this.onLeaveState = function(){};
this.state = state;
},
onNewEpisode: function(result){
var text;
if (result == "failed"){
text = "Out of battery. The robot will be resetted.";
} else if (result == "success"){
text = "You reached the goal. The robot will be resetted.";
} }
return this.$lightbox.popup(text, ["ok"]);
} }
}, },
watch: { watch: {
'machine.learning_rate': function(new_val) { learning_rate: function(new_val) {
machine.lr = new_val; machine.lr = new_val;
renderLatex(); render_latex();
}, },
'machine.discount_factor': function(new_val) { discount_factor: function(new_val) {
machine.df = new_val; machine.df = new_val;
renderLatex(); render_latex();
}, },
'machine.epsilon': function(new_val) { epsilon: function(new_val) {
machine.epsilon = new_val; machine.epsilon = new_val;
}, }
state: function(state){
this.onLeaveState();
Object.assign(this, StateMgr[state]);
this.onEnterState();
},
} }
}) })
function renderLatex() { function render_latex() {
// (1-lr) * Q[state, action] + lr * (reward + gamma * np.max(Q[new_state, :]) // (1-lr) * Q[state, action] + lr * (reward + gamma * np.max(Q[new_state, :])
katex.render(`Q(s,a)\\leftarrow${(1-machine.lr).toFixed(2)}Q(s,a)+${machine.lr.toFixed(2)}(reward + ${machine.df.toFixed(2)}\\max_{a'}(Q(s_{new}, a'))`, document.getElementById('formula'),{displayMode: true,}); katex.render(`Q(s,a)\\leftarrow${(1-machine.lr).toFixed(2)}Q(s,a)+${machine.lr.toFixed(2)}(reward + ${machine.df.toFixed(2)} * \\max_a(Q(s', a))`, document.getElementById('test'),{displayMode: true,});
}
renderLatex();
// ----------------------------------------------------------------------------
// ------------------------------ StateMgr ------------------------------------
// ----------------------------------------------------------------------------
var StateMgr = {
init: {
onEnterState: function () {
var lightText = `Reinforcement learning (RL) is an area of machine learning concerned with how software agents ought to take actions in an environment so as to maximize some notion of cumulative reward. Reinforcement learning is one of three basic machine learning paradigms, alongside supervised learning and unsupervised learning. (wikipedia)
This exhibit explains how a robot can learn to navigate through a maze in order to reach its destination, before running out of power. At first the robot knows nothing, and learns from each new action (movement) and state (location reached). Slowly it starts to develop an understanding of the maze that will allow it to reach the charging station before it runs out of power. Eventually, it should learn to avoid any detour and reach the charging station in the optimal number of steps.`
this.$lightbox.popup(lightText, ["next"]).then((r) => this.changeState("local"));
},
},
local: {
components: ["local", "navi", "score"],
navigation: {
"reset robot": () => machine.reset_machine(),
"continue": null,
},
onEnterState: function () {
this.navigation.continue = () => this.changeState("global");
var lightText = "But there is a problem! The robot cannot see the whole maze, it only knows where it is and in which direction it can move. Can you reach the charging station in those conditions? Use the arrows to move";
this.$lightbox.popup(lightText, ["next"]);
},
},
global: {
components: ["global", "sliders", "plot", "navi", "score"],
navigation: {
"run 1 episode!": () => machine.run(1),
"run 100 episodes!": () => machine.run(100),
"auto step!": () => machine.auto_step(),
"greedy step!": () => machine.greedy_step(),
"reset machine": () => machine.reset_machine(),
},
onEnterState: function () {
var lightText = `As a human, you keep track of where you are and how you got there without thinking, which helps you think about what actions you should take next to reach your destination. And you can also just look around! How can then the robot 'think' of the maze, to know which action is the best at every moment? And how can it learn that? It must somehow keep track of where it is, and remember how good or bad was each action at each place in the maze, try new things, and update it's "mental image" of what was a good decision and what not.
Reinforcement Learning uses the concept of a "Q-function", which keeps track of how "good" it expects it to be to take a specific action 'a' from a specific location 's'. This is written as Q(s, a). It also uses a "policy", which determines the best action to take in a given state, and is written as π(s). The robot must learn those functions while it navigates the maze. With each step, the functions are modified by a little bit, until eventually they give it the best strategy to solve the maze.`;
this.$lightbox.popup(lightText, ["continue"]);
},
} }
}; render_latex();

Loading…
Cancel
Save