-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeepqlearn.js
More file actions
292 lines (270 loc) · 13.4 KB
/
deepqlearn.js
File metadata and controls
292 lines (270 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
var deepqlearn = deepqlearn || { REVISION: 'ALPHA' };
(function(global) {
"use strict";
// An agent is in state0 and does action0
// environment then assigns reward0 and provides new state, state1
// Experience nodes store all this information, which is used in the
// Q-learning update step
var Experience = function(state0, action0, reward0, state1) {
this.state0 = state0;
this.action0 = action0;
this.reward0 = reward0;
this.state1 = state1;
}
// A Brain object does all the magic.
// over time it receives some inputs and some rewards
// and its job is to set the outputs to maximize the expected reward
var Brain = function(num_states, num_actions, opt) {
var opt = opt || {};
// in number of time steps, of temporal memory
// the ACTUAL input to the net will be (x,a) temporal_window times, and followed by current x
// so to have no information from previous time step going into value function, set to 0.
this.temporal_window = typeof opt.temporal_window !== 'undefined' ? opt.temporal_window : 1;
// size of experience replay memory
this.experience_size = typeof opt.experience_size !== 'undefined' ? opt.experience_size : 30000;
// number of examples in experience replay memory before we begin learning
this.start_learn_threshold = typeof opt.start_learn_threshold !== 'undefined'? opt.start_learn_threshold : Math.floor(Math.min(this.experience_size*0.1, 1000));
// gamma is a crucial parameter that controls how much plan-ahead the agent does. In [0,1]
this.gamma = typeof opt.gamma !== 'undefined' ? opt.gamma : 0.8;
// number of steps we will learn for
this.learning_steps_total = typeof opt.learning_steps_total !== 'undefined' ? opt.learning_steps_total : 100000;
// how many steps of the above to perform only random actions (in the beginning)?
this.learning_steps_burnin = typeof opt.learning_steps_burnin !== 'undefined' ? opt.learning_steps_burnin : 3000;
// what epsilon value do we bottom out on? 0.0 => purely deterministic policy at end
this.epsilon_min = typeof opt.epsilon_min !== 'undefined' ? opt.epsilon_min : 0.05;
// what epsilon to use at test time? (i.e. when learning is disabled)
this.epsilon_test_time = typeof opt.epsilon_test_time !== 'undefined' ? opt.epsilon_test_time : 0.01;
// advanced feature. Sometimes a random action should be biased towards some values
// for example in flappy bird, we may want to choose to not flap more often
if(typeof opt.random_action_distribution !== 'undefined') {
// this better sum to 1 by the way, and be of length this.num_actions
this.random_action_distribution = opt.random_action_distribution;
if(this.random_action_distribution.length !== num_actions) {
console.log('TROUBLE. random_action_distribution should be same length as num_actions.');
}
var a = this.random_action_distribution;
var s = 0.0; for(var k=0;k<a.length;k++) { s+= a[k]; }
if(Math.abs(s-1.0)>0.0001) { console.log('TROUBLE. random_action_distribution should sum to 1!'); }
} else {
this.random_action_distribution = [];
}
// states that go into neural net to predict optimal action look as
// x0,a0,x1,a1,x2,a2,...xt
// this variable controls the size of that temporal window. Actions are
// encoded as 1-of-k hot vectors
this.net_inputs = num_states * this.temporal_window + num_actions * this.temporal_window + num_states;
this.num_states = num_states;
this.num_actions = num_actions;
this.window_size = Math.max(this.temporal_window, 2); // must be at least 2, but if we want more context even more
this.state_window = new Array(this.window_size);
this.action_window = new Array(this.window_size);
this.reward_window = new Array(this.window_size);
this.net_window = new Array(this.window_size);
// create [state -> value of all possible actions] modeling net for the value function
var layer_defs = [];
if(typeof opt.layer_defs !== 'undefined') {
// this is an advanced usage feature, because size of the input to the network, and number of
// actions must check out. This is not very pretty Object Oriented programming but I can't see
// a way out of it :(
layer_defs = opt.layer_defs;
if(layer_defs.length < 2) { console.log('TROUBLE! must have at least 2 layers'); }
if(layer_defs[0].type !== 'input') { console.log('TROUBLE! first layer must be input layer!'); }
if(layer_defs[layer_defs.length-1].type !== 'regression') { console.log('TROUBLE! last layer must be input regression!'); }
if(layer_defs[0].out_depth * layer_defs[0].out_sx * layer_defs[0].out_sy !== this.net_inputs) {
console.log('TROUBLE! Number of inputs must be num_states * temporal_window + num_actions * temporal_window + num_states!');
}
if(layer_defs[layer_defs.length-1].num_neurons !== this.num_actions) {
console.log('TROUBLE! Number of regression neurons should be num_actions!');
}
} else {
// create a very simple neural net by default
layer_defs.push({type:'input', out_sx:1, out_sy:1, out_depth:this.net_inputs});
if(typeof opt.hidden_layer_sizes !== 'undefined') {
// allow user to specify this via the option, for convenience
var hl = opt.hidden_layer_sizes;
for(var k=0;k<hl.length;k++) {
layer_defs.push({type:'fc', num_neurons:hl[k], activation:'relu'}); // relu by default
}
}
layer_defs.push({type:'regression', num_neurons:num_actions}); // value function output
}
this.value_net = new convnetjs.Net();
this.value_net.makeLayers(layer_defs);
// and finally we need a Temporal Difference Learning trainer!
var tdtrainer_options = {learning_rate:0.01, momentum:0.0, batch_size:64, l2_decay:0.01};
if(typeof opt.tdtrainer_options !== 'undefined') {
tdtrainer_options = opt.tdtrainer_options; // allow user to overwrite this
}
this.tdtrainer = new convnetjs.SGDTrainer(this.value_net, tdtrainer_options);
// experience replay
this.experience = [];
// various housekeeping variables
this.age = 0; // incremented every backward()
this.forward_passes = 0; // incremented every forward()
this.epsilon = 1.0; // controls exploration exploitation tradeoff. Should be annealed over time
this.latest_reward = 0;
this.last_input_array = [];
this.average_reward_window = new cnnutil.Window(1000, 10);
this.average_loss_window = new cnnutil.Window(1000, 10);
this.learning = true;
}
Brain.prototype = {
random_action: function() {
// a bit of a helper function. It returns a random action
// we are abstracting this away because in future we may want to
// do more sophisticated things. For example some actions could be more
// or less likely at "rest"/default state.
if(this.random_action_distribution.length === 0) {
return convnetjs.randi(0, this.num_actions);
} else {
// okay, lets do some fancier sampling:
var p = convnetjs.randf(0, 1.0);
var cumprob = 0.0;
for(var k=0;k<this.num_actions;k++) {
cumprob += this.random_action_distribution[k];
if(p < cumprob) { return k; }
}
}
},
policy: function(s) {
// compute the value of doing any action in this state
// and return the argmax action and its value
var svol = new convnetjs.Vol(1, 1, this.net_inputs);
svol.w = s;
var action_values = this.value_net.forward(svol);
var maxk = 0;
var maxval = action_values.w[0];
for(var k=1;k<this.num_actions;k++) {
if(action_values.w[k] > maxval) { maxk = k; maxval = action_values.w[k]; }
}
return {action:maxk, value:maxval};
},
getNetInput: function(xt) {
// return s = (x,a,x,a,x,a,xt) state vector.
// It's a concatenation of last window_size (x,a) pairs and current state x
var w = [];
w = w.concat(xt); // start with current state
// and now go backwards and append states and actions from history temporal_window times
var n = this.window_size;
for(var k=0;k<this.temporal_window;k++) {
// state
w = w.concat(this.state_window[n-1-k]);
// action, encoded as 1-of-k indicator vector. We scale it up a bit because
// we dont want weight regularization to undervalue this information, as it only exists once
var action1ofk = new Array(this.num_actions);
for(var q=0;q<this.num_actions;q++) action1ofk[q] = 0.0;
action1ofk[this.action_window[n-1-k]] = 1.0*this.num_states;
w = w.concat(action1ofk);
}
return w;
},
forward: function(input_array) {
// compute forward (behavior) pass given the input neuron signals from body
this.forward_passes += 1;
this.last_input_array = input_array; // back this up
// create network input
var action;
if(this.forward_passes > this.temporal_window) {
// we have enough to actually do something reasonable
var net_input = this.getNetInput(input_array);
if(this.learning) {
// compute epsilon for the epsilon-greedy policy
this.epsilon = Math.min(1.0, Math.max(this.epsilon_min, 1.0-(this.age - this.learning_steps_burnin)/(this.learning_steps_total - this.learning_steps_burnin)));
} else {
this.epsilon = this.epsilon_test_time; // use test-time value
}
var rf = convnetjs.randf(0,1);
if(rf < this.epsilon) {
// choose a random action with epsilon probability
action = this.random_action();
} else {
// otherwise use our policy to make decision
var maxact = this.policy(net_input);
action = maxact.action;
}
} else {
// pathological case that happens first few iterations
// before we accumulate window_size inputs
var net_input = [];
action = this.random_action();
}
// remember the state and action we took for backward pass
this.net_window.shift();
this.net_window.push(net_input);
this.state_window.shift();
this.state_window.push(input_array);
this.action_window.shift();
this.action_window.push(action);
return action;
},
backward: function(reward) {
this.latest_reward = reward;
this.average_reward_window.add(reward);
this.reward_window.shift();
this.reward_window.push(reward);
if(!this.learning) { return; }
// various book-keeping
this.age += 1;
// it is time t+1 and we have to store (s_t, a_t, r_t, s_{t+1}) as new experience
// (given that an appropriate number of state measurements already exist, of course)
if(this.forward_passes > this.temporal_window + 1) {
var e = new Experience();
var n = this.window_size;
e.state0 = this.net_window[n-2];
e.action0 = this.action_window[n-2];
e.reward0 = this.reward_window[n-2];
e.state1 = this.net_window[n-1];
if(this.experience.length < this.experience_size) {
this.experience.push(e);
} else {
// replace. finite memory!
var ri = convnetjs.randi(0, this.experience_size);
this.experience[ri] = e;
}
}
// learn based on experience, once we have some samples to go on
// this is where the magic happens...
if(this.experience.length > this.start_learn_threshold) {
var avcost = 0.0;
for(var k=0;k < this.tdtrainer.batch_size;k++) {
var re = convnetjs.randi(0, this.experience.length);
var e = this.experience[re];
var x = new convnetjs.Vol(1, 1, this.net_inputs);
x.w = e.state0;
var maxact = this.policy(e.state1);
var r = e.reward0 + this.gamma * maxact.value;
var ystruct = {dim: e.action0, val: r};
var loss = this.tdtrainer.train(x, ystruct);
avcost += loss.loss;
}
avcost = avcost/this.tdtrainer.batch_size;
this.average_loss_window.add(avcost);
}
},
visSelf: function(elt) {
elt.innerHTML = ''; // erase elt first
// elt is a DOM element that this function fills with brain-related information
var brainvis = document.createElement('div');
// basic information
var desc = document.createElement('div');
var t = '';
t += 'experience replay size: ' + this.experience.length + '<br>';
t += 'exploration epsilon: ' + this.epsilon + '<br>';
t += 'age: ' + this.age + '<br>';
t += 'average Q-learning loss: ' + this.average_loss_window.get_average() + '<br />';
t += 'smooth-ish reward: ' + this.average_reward_window.get_average() + '<br />';
desc.innerHTML = t;
brainvis.appendChild(desc);
elt.appendChild(brainvis);
}
}
global.Brain = Brain;
})(deepqlearn);
(function(lib) {
"use strict";
if (typeof module === "undefined" || typeof module.exports === "undefined") {
window.deepqlearn = lib; // in ordinary browser attach library to window
} else {
module.exports = lib; // in nodejs
}
})(deepqlearn);