|
| 1 | +import numpy as np |
| 2 | +import theano |
| 3 | +import theano.tensor as T |
| 4 | + |
| 5 | + |
| 6 | +class TileCodingLayer: |
| 7 | + def __init__(self, min_val, max_val, num_tiles, |
| 8 | + num_tilings, num_features): |
| 9 | + self.min_val = min_val |
| 10 | + self.max_val = max_val |
| 11 | + self.num_tiles = num_tiles |
| 12 | + self.num_tilings = num_tilings |
| 13 | + self.num_features = num_features |
| 14 | + |
| 15 | + # The range of each feature is divided into num_tile tiles. |
| 16 | + # But we keep one more tile. The reason is that since we shift |
| 17 | + # tilings by an offset, we want to make sure that every point |
| 18 | + # after shifting maps to one tile. |
| 19 | + # Shape: (num_tilings, (num_tiles+1) * num_features) |
| 20 | + self.weight = theano.shared(np.zeros( |
| 21 | + (num_tilings,) + (num_tiles + 1,) * num_features)) |
| 22 | + |
| 23 | + def quantize(self, x): |
| 24 | + len_tile = float(self.max_val - self.min_val) / self.num_tiles |
| 25 | + offsets = (len_tile * T.arange(self.num_tilings).astype('float32') / |
| 26 | + self.num_tilings) |
| 27 | + # shapes: (features, num_tilings) = (features, 1) + (1, num_tilings) |
| 28 | + mapped_x = x.dimshuffle(0, 'x') + offsets.dimshuffle('x', 0) |
| 29 | + # Since we have an extra tile: |
| 30 | + new_max_val = self.max_val + len_tile |
| 31 | + # Mapping into range [0, num_tiles + 1] to be able to do quantization |
| 32 | + # by casting to int. |
| 33 | + mapped_x = ((self.num_tiles + 1) * mapped_x / |
| 34 | + (new_max_val - self.min_val)) |
| 35 | + # shape: (num_tilings, num_features) |
| 36 | + q_x = mapped_x.astype('int32').T |
| 37 | + return q_x |
| 38 | + |
| 39 | + def approximate(self, q_x): |
| 40 | + indices = [T.arange(self.num_tilings)] |
| 41 | + for f in range(self.num_features): |
| 42 | + indices += [q_x[:, f]] |
| 43 | + self.indices = indices |
| 44 | + # shape: (num_tilings,) |
| 45 | + selected = self.weight.__getitem__(tuple(indices)) |
| 46 | + y_hat = T.sum(selected) |
| 47 | + return y_hat |
| 48 | + |
| 49 | + def update_rule(self, y, y_hat, learning_rate): |
| 50 | + # grad is only used to locate the weights which |
| 51 | + # are used for computing y_hat. Since |
| 52 | + # "y_hat = T.sum(selected)", T.grad will returns |
| 53 | + # 1 for corresponding weights and 0 for others. |
| 54 | + grad_ = T.grad(y_hat, self.weight) |
| 55 | + learning_rate = learning_rate / self.num_tilings |
| 56 | + upd = grad_ * learning_rate * (y - y_hat) |
| 57 | + updates = [(self.weight, self.weight + upd)] |
| 58 | + return updates |
| 59 | + |
| 60 | + |
| 61 | +def get_tile_coder(min_val, max_val, num_tiles, num_tilings, |
| 62 | + num_features, learning_rate): |
| 63 | + # x.shape: (num_features), y.shape: () |
| 64 | + x = T.fvector('x') |
| 65 | + y = T.fscalar('y') |
| 66 | + |
| 67 | + tile_coding_layer = TileCodingLayer( |
| 68 | + min_val=min_val, max_val=max_val, |
| 69 | + num_tiles=num_tiles, num_tilings=num_tilings, |
| 70 | + num_features=num_features) |
| 71 | + |
| 72 | + # quantized_x |
| 73 | + q_x = tile_coding_layer.quantize(x) |
| 74 | + y_hat = tile_coding_layer.approximate(q_x) |
| 75 | + updates = tile_coding_layer.update_rule(y, y_hat, 0.1) |
| 76 | + |
| 77 | + train = theano.function([x, y], y_hat, updates=updates, allow_input_downcast=True) |
| 78 | + eval_ = theano.function([x], y_hat, allow_input_downcast=True) |
| 79 | + |
| 80 | + return train, eval_ |
0 commit comments