Skip to content
AI360Xpert
Beta

Tile Coding (CMAC) in RL

Lay several slightly shifted grids over continuous space. Each point lands in exactly one tile per grid, and the tiles it lands in explain its value.

Two of four offset tilings cover the point (3.6, 7.2), and the four active tiles carry weights 1.20, 0.80, 1.50, and 1.00, summing to a value of 4.50
Two of four offset tilings cover the point (3.6, 7.2), and the four active tiles carry weights 1.20, 0.80, 1.50, and 1.00, summing to a value of 4.50

Why Does This Exist?

Continuous states break tables. A robot arm never visits the exact same joint angle twice, so a lookup table learns nothing twice. The naive fix is one coarse grid, but it fails both ways: big cells blur distinctions the controller needs, and tiny cells explode memory while generalizing zero distance. Every point becomes an island.

Tile coding, introduced by Albus (1975) as CMAC and brought into RL by Sutton, escapes the tradeoff with overlap. Lay mm full grids (tilings) over the space, each shifted by a small offset. A point activates exactly one tile per tiling, so its feature vector holds exactly mm ones out of thousands of entries. Neighbors share most of their active tiles and generalize strongly; distant points share none. Prediction becomes a sum of mm weights, learned with linear value function approximation. Neighbors of this idea are coarse coding and radial basis functions; the control loop that consumes tile features is typically semi-gradient SARSA.

The one idea you need first.

A linear value estimate is a weighted sum of features: v^(s,w)=w⊤x(s)\hat{v}(s, \mathbf{w}) = \mathbf{w}^\top \mathbf{x}(s), where x(s)\mathbf{x}(s) is the feature vector for state ss and w\mathbf{w} holds one weight per feature. Tile coding is a way of building x(s)\mathbf{x}(s) so that it is binary (entries are 0 or 1) and sparse (almost all zeros), which makes the sum trivially cheap.

Think of It Like This

Pinpointing a pinhole with shifted stencils

An artist must locate a pinhole on a drafting canvas. With one stencil of 2-inch squares, the hole falls in cell (3, 2) and she knows the neighborhood but nothing within the 2-inch box. Shrinking the squares would need thousands of tiny cutouts.

Instead she lays four identical stencils, each nudged a fraction of an inch in a different direction, and notes the hole's cell on each sheet. Stacked together, the four marked squares overlap in one small patch a fraction of an inch wide. Big squares gave the broad coverage; their shifted overlap gives the pinpoint resolution.

Point for point: each stencil is a tiling, each marked square is an active tile, the four sheets are the m=4m = 4 sparse features, and the overlap patch is the effective resolution. Where it stops: stencils physically block light, while tiles are indices in a binary vector, and the artist reads positions while the agent sums weights.

How It Actually Works

Tilings, tiles, and asymmetric offsets

Let the state be s=(s1,…,sk)s = (s_1, \dots, s_k) in kk continuous dimensions. One tiling partitions the whole space into boxes (tiles) of width wjw_j along dimension jj. Tile coding uses mm tilings, all with the same tile widths, each displaced by its own offset vector oi\mathbf{o}_i. The active tile index in tiling ii along dimension jj is a floor division:

coordi,j(s)=⌊sj−oi,jwj⌋\text{coord}_{i,j}(s) = \left\lfloor \frac{s_j - o_{i,j}}{w_j} \right\rfloor

Offsets must be asymmetric. Shifting every tiling by the same diagonal step stretches the overlap regions into diagonal slivers, so the agent generalizes along the diagonal and not across it. The standard fix displaces tilings by odd multiples per dimension, keeping overlap patches compact in every direction.

Why prediction costs O(m)

Each tiling covers everything, so every state activates exactly one tile per tiling: ∥x(s)∥1=m\|\mathbf{x}(s)\|_1 = m, with entries in {0,1}\{0, 1\}. The value estimate collapses to a sum over the mm active weights:

v^(s,w)=∑i=0m−1wtilei(s)\hat{v}(s, \mathbf{w}) = \sum_{i=0}^{m-1} w_{\text{tile}_i(s)}

No matrix multiply, no search: mm memory lookups and mm additions, regardless of how fine the effective resolution is. Learning splits the TD error evenly: each active weight moves by (α/m)(\alpha / m) times the error, where α\alpha is the step size. The division by mm matters, and the second pitfall below shows what breaks without it.

Two nearby states sharing kk of mm tiles have feature dot product kk, so updating one moves the other by fraction k/mk/m. Points further apart than a tile width share nothing. When the grid count runs into the billions, tile coordinates are hashed into a fixed weight table; rare collisions spread as uniform noise the averaging absorbs.

Worked example

State s=(3.6,7.2)s = (3.6, 7.2), tile width W=2.0W = 2.0, m=4m = 4 tilings with offsets (0.0,0.0)(0.0, 0.0), (0.5,1.5)(0.5, 1.5), (1.0,1.0)(1.0, 1.0), (1.5,0.5)(1.5, 0.5):

  • Tiling 0: ⌊3.6/2.0⌋=1\lfloor 3.6/2.0 \rfloor = 1, ⌊7.2/2.0⌋=3\lfloor 7.2/2.0 \rfloor = 3, tile [1,3][1, 3].
  • Tiling 1: ⌊3.1/2.0⌋=1\lfloor 3.1/2.0 \rfloor = 1, ⌊5.7/2.0⌋=2\lfloor 5.7/2.0 \rfloor = 2, tile [1,2][1, 2].
  • Tiling 2: ⌊2.6/2.0⌋=1\lfloor 2.6/2.0 \rfloor = 1, ⌊6.2/2.0⌋=3\lfloor 6.2/2.0 \rfloor = 3, tile [1,3][1, 3].
  • Tiling 3: ⌊2.1/2.0⌋=1\lfloor 2.1/2.0 \rfloor = 1, ⌊6.7/2.0⌋=3\lfloor 6.7/2.0 \rfloor = 3, tile [1,3][1, 3].

With learned weights 1.201.20, 0.800.80, 1.501.50, 1.001.00 on those four tiles, the estimate is 1.20+0.80+1.50+1.00=4.501.20 + 0.80 + 1.50 + 1.00 = \mathbf{4.50}, computed in 4 additions. A neighbor at (3.7,7.3)(3.7, 7.3) lands in the same four tiles, so it inherits the same 4.504.50 until learning separates them.

Code

The worked example as runnable code: tile indexing, the 4-addition prediction, and one divided-by-mm update.

import mathfrom typing import Dict, List, Tuple
OFFSETS: List[Tuple[float, float]] = [(0.0, 0.0), (0.5, 1.5), (1.0, 1.0), (1.5, 0.5)]WIDTH = 2.0

def active_tiles(x: float, y: float) -> List[Tuple[int, int, int]]:    """Active (tiling, col, row) triple for a continuous point."""    return [        (i, math.floor((x - ox) / WIDTH), math.floor((y - oy) / WIDTH))        for i, (ox, oy) in enumerate(OFFSETS)    ]

def predict(tiles: List[Tuple[int, int, int]], weights: Dict[Tuple[int, int, int], float]) -> float:    """Value estimate: plain sum of the m active weights."""    return sum(weights.get(t, 0.0) for t in tiles)

tiles = active_tiles(3.6, 7.2)print(tiles)  # -> [(0, 1, 3), (1, 1, 2), (2, 1, 3), (3, 1, 3)]weights = {(0, 1, 3): 1.20, (1, 1, 2): 0.80, (2, 1, 3): 1.50, (3, 1, 3): 1.00}print(f"{predict(tiles, weights):.2f}")  # -> 4.50

The indexing and the 4.504.50 prediction match the hand computation tile for tile.

Watch Out For

Symmetric offsets carve diagonal grooves

Shifting each tiling by the same step on every axis (oi\mathbf{o}_i proportional to (i,i)(i, i)) stretches overlap patches into diagonal slivers. Symptom: the value function generalizes far along the 45-degree line and barely across it, so curved trajectories control erratically while straight diagonal runs look fine.

Displace tilings asymmetrically, with odd multiples per dimension, and give each dimension its own tile width matched to how sensitive the task is along that axis. If performance depends on travel direction, suspect the offsets before the learning rate.

Forgetting to divide the step size by m

Each update touches mm weights at once, so the effective step on the value is mm times the per-weight step. Symptom: learning oscillates or diverges at a step size that works fine for a coarser coding, and shrinking rewards or features never quite fixes it.

Update each active tile with α/m\alpha / m, not α\alpha. With m=8m = 8 tilings and α=0.1\alpha = 0.1, every active weight moves by 0.01250.0125 times the error. If inherited code updates tile coders with a raw alpha, that missing division is the first thing to check.

The Quick Version

  • Tile coding overlays mm offset grids on continuous space, activating exactly one binary tile per grid for mm active features total.
  • Asymmetric offsets keep overlap patches compact; symmetric shifts carve diagonal grooves that generalize along one axis only.
  • Prediction is a sum of the mm active weights: the (3.6,7.2)(3.6, 7.2) example totals 4.50 in 4 additions.
  • States sharing kk of mm tiles move each other by fraction k/mk/m per update, giving broad coverage with pinpoint resolution.
  • Split every update across the active tiles with step α/m\alpha / m, or the effective step runs mm times too hot.
  • Hash coordinates into a fixed table when tile counts explode; the rare collisions average out as noise.