@@ -37,7 +37,7 @@ We approach the problem in two ways.
3737First, we solve it exactly using dynamic programming, assuming full knowledge of
3838the model — the demand distribution, cost parameters, and transition dynamics.
393940-Second, we show how a manager can learn the optimal policy from experience alone, using *Q-learning*.
40+Second, we show how a manager can learn the optimal policy from experience alone, using *[Q-learning](https://en.wikipedia.org/wiki/Q-learning)*.
41414242The manager observes only the inventory level, the order placed, the resulting
4343profit, and the next inventory level — without knowing any of the underlying
@@ -50,6 +50,14 @@ transition function.
5050We show that, given enough experience, the manager's learned policy converges to
5151the optimal one.
525253+The lecture proceeds as follows:
54+55+1. We set up the inventory model and solve it exactly via value function iteration.
56+2. We introduce Q-factors and derive the Q-factor Bellman equation.
57+3. We implement Q-learning and show the learned policy converges to the optimal one.
58+59+A risk-sensitive extension of this model is studied in {doc}`rs_inventory_q`.
60+5361We will use the following imports:
54625563```{code-cell} ipython3
@@ -85,12 +93,14 @@ $$
8593 \qquad
8694 \text{where}
8795 \quad
88- h(x,a,d) := (x - d) \vee 0 + a.
96+ h(x,a,d) := \max(x - d, 0) + a.
8997$$
90989199The term $A_t$ is units of stock ordered this period, which arrive at the start
92100of period $t+1$, after demand $D_{t+1}$ is realized and served.
93101102+**Timeline for period $t$:** observe $X_t$ → choose $A_t$ → demand $D_{t+1}$ arrives → profit realized → $X_{t+1}$ determined.
103+94104(We use a $t$ subscript in $A_t$ to indicate the information set: it is chosen
95105before $D_{t+1}$ is observed.)
96106@@ -99,7 +109,7 @@ We assume that the firm can store at most $K$ items at one time.
99109Profits are given by
100110101111$$
102- \pi(X_t, A_t, D_{t+1}) := X_t \wedge D_{t+1} - c A_t - \kappa 1\{A_t > 0\}.
112+ \pi(X_t, A_t, D_{t+1}) := \min(X_t, D_{t+1}) - c A_t - \kappa 1\{A_t > 0\}.
103113$$
104114105115Here
@@ -158,7 +168,7 @@ $$
158168 \sum_d \phi(d) \left[ \pi(x, a, d) + \beta \, v(h(x, a, d)) \right]
159169$$
160170161-When $r > 0$, the sequence $v_{k+1} = T v_k$ converges to the
171+When $r > 0$ (equivalently, $\beta < 1$), the sequence $v_{k+1} = T v_k$ converges to the
162172unique fixed point $v^*$, which is the value function of the optimal policy
163173(see, e.g., {cite}`Sargent_Stachurski_2025`).
164174@@ -167,7 +177,7 @@ unique fixed point $v^*$, which is the value function of the optimal policy
167177168178We store the model primitives in a `NamedTuple`.
169179170-Demand follows a geometric distribution with parameter $p$, so $\phi(d) = (1 - p)^d \, p$ for $d = 0, 1, 2, \ldots$.
180+Demand follows a [geometric distribution](https://en.wikipedia.org/wiki/Geometric_distribution) with parameter $p$, so $\phi(d) = (1 - p)^d \, p$ for $d = 0, 1, 2, \ldots$.
171181172182```{code-cell} ipython3
173183class Model(NamedTuple):
@@ -338,8 +348,9 @@ At each step, we draw a demand shock from the geometric distribution and update
338348339349```{code-cell} ipython3
340350@numba.jit(nopython=True)
341-def sim_inventories(ts_length, σ, p, X_init=0):
351+def sim_inventories(ts_length, σ, p, X_init=0, seed=0):
342352 """Simulate inventory dynamics under policy σ."""
353+ np.random.seed(seed)
343354 X = np.zeros(ts_length, dtype=np.int32)
344355 X[0] = X_init
345356 for t in range(ts_length - 1):
@@ -427,7 +438,7 @@ transition function.
427438428439### The Q-learning update rule
429440430-Q-learning approximates the fixed point of the Q-factor Bellman equation using **stochastic approximation**.
441+Q-learning approximates the fixed point of the Q-factor Bellman equation using **[stochastic approximation](https://en.wikipedia.org/wiki/Stochastic_approximation)**.
431442432443At each step, the agent is in state $x$, takes action $a$, observes reward
433444$R_{t+1} = \pi(x, a, D_{t+1})$ and next state $X_{t+1} = h(x, a, D_{t+1})$, and
@@ -443,8 +454,23 @@ where $\alpha_t$ is the learning rate.
443454444455The update blends the current estimate $q_t(x, a)$ with a fresh sample of the Bellman target.
445456457+### What the manager needs to know
458+459+Notice what is **not** required to implement the update.
460+461+The manager does not need to know the demand distribution $\phi$, the unit cost $c$, the fixed cost $\kappa$, or the transition function $h$.
462+463+All the manager needs to observe at each step is:
464+465+1. the current inventory level $x$,
466+2. the order quantity $a$ they chose,
467+3. the resulting profit $R_{t+1}$ (which appears on the books), and
468+4. the next inventory level $X_{t+1}$ (which they can read off the warehouse).
469+470+These are all directly observable quantities — no model knowledge is required.
446471447-### The Q-table and the behavior policy
472+473+### The Q-table and the role of the max
448474449475It is important to understand how the update rule relates to the manager's
450476actions.
@@ -489,7 +515,9 @@ By contrast, the original update with the $\max$ is a stochastic sample of the
489515Bellman *optimality* operator, whose fixed point is $q^*$. The $\max$ in the
490516update target is therefore what drives convergence to $q^*$.
491517492-**The behavior policy.**
518+In short, the $\max$ is doing the work of finding the optimum; without it, you only evaluate a fixed policy.
519+520+### The behavior policy
493521494522The rule governing how the manager chooses actions is called the **behavior
495523policy**. Because the $\max$ in the update target always points toward $q^*$
@@ -511,26 +539,11 @@ In practice, we want the manager to mostly take good actions (to earn reasonable
511539profits while learning), while still occasionally experimenting to discover
512540better alternatives.
513541514-### What the manager needs to know
515-516-Notice what is **not** required to implement the update.
517-518-The manager does not need to know the demand distribution $\phi$, the unit cost $c$, the fixed cost $\kappa$, or the transition function $h$.
519-520-All the manager needs to observe at each step is:
521-522-1. the current inventory level $x$,
523-2. the order quantity $a$ they chose,
524-3. the resulting profit $R_{t+1}$ (which appears on the books), and
525-4. the next inventory level $X_{t+1}$ (which they can read off the warehouse).
526-527-These are all directly observable quantities — no model knowledge is required.
528-529542### Learning rate
530543531544We use $\alpha_t = 1 / n_t(x, a)^{0.51}$, where $n_t(x, a)$ is the number of times the pair $(x, a)$ has been visited up to time $t$.
532545533-This decays slowly enough to allow learning from later (better-informed) updates, while still satisfying the Robbins-Monro conditions for convergence.
546+This decays slowly enough to allow learning from later (better-informed) updates, while still satisfying the [Robbins–Monro conditions](https://en.wikipedia.org/wiki/Stochastic_approximation#Robbins%E2%80%93Monro_algorithm) for convergence.
534547535548### Exploration: epsilon-greedy
536549@@ -574,7 +587,8 @@ At specified step counts (given by `snapshot_steps`), we record the current gree
574587```{code-cell} ipython3
575588@numba.jit(nopython=True)
576589def q_learning_kernel(K, p, c, κ, β, n_steps, X_init,
577- ε_init, ε_min, ε_decay, snapshot_steps):
590+ ε_init, ε_min, ε_decay, snapshot_steps, seed):
591+ np.random.seed(seed)
578592 q = np.zeros((K + 1, K + 1))
579593 n = np.zeros((K + 1, K + 1)) # visit counts for learning rate
580594 ε = ε_init
@@ -593,7 +607,7 @@ def q_learning_kernel(K, p, c, κ, β, n_steps, X_init,
593607 snapshots[snap_idx] = greedy_policy_from_q(q, K)
594608 snap_idx += 1
595609596- # === Observe outcome ===
610+ # === Draw D_{t+1} and observe outcome ===
597611 d = np.random.geometric(p) - 1
598612 reward = min(x, d) - c * a - κ * (a > 0)
599613 x_next = max(x - d, 0) + a
@@ -628,21 +642,20 @@ The wrapper function unpacks the model and provides default hyperparameters.
628642```{code-cell} ipython3
629643def q_learning(model, n_steps=20_000_000, X_init=0,
630644 ε_init=1.0, ε_min=0.01, ε_decay=0.999999,
631- snapshot_steps=None):
645+ snapshot_steps=None, seed=1234):
632646 x_values, d_values, ϕ_values, p, c, κ, β = model
633647 K = len(x_values) - 1
634648 if snapshot_steps is None:
635649 snapshot_steps = np.array([], dtype=np.int64)
636650 return q_learning_kernel(K, p, c, κ, β, n_steps, X_init,
637- ε_init, ε_min, ε_decay, snapshot_steps)
651+ ε_init, ε_min, ε_decay, snapshot_steps, seed)
638652```
639653640654### Running Q-learning
641655642656We run 20 million steps and take policy snapshots at steps 10,000, 1,000,000, and at the end.
643657644658```{code-cell} ipython3
645-np.random.seed(1234)
646659snap_steps = np.array([10_000, 1_000_000, 19_999_999], dtype=np.int64)
647660q, snapshots = q_learning(model, snapshot_steps=snap_steps)
648661```
@@ -661,6 +674,7 @@ and compare them against $v^*$ and $\sigma^*$ from VFI.
661674662675```{code-cell} ipython3
663676K = len(x_values) - 1
677+# restrict to feasible actions a ∈ {0, ..., K-x}
664678v_q = np.array([np.max(q[x, :K - x + 1]) for x in range(K + 1)])
665679σ_q = np.array([np.argmax(q[x, :K - x + 1]) for x in range(K + 1)])
666680```
@@ -710,8 +724,7 @@ X_init = K // 2
710724sim_seed = 5678
711725712726# Optimal policy
713-np.random.seed(sim_seed)
714-X_opt = sim_inventories(ts_length, σ_star, p, X_init)
727+X_opt = sim_inventories(ts_length, σ_star, p, X_init, seed=sim_seed)
715728axes[0].plot(X_opt, alpha=0.7)
716729axes[0].set_ylabel("inventory")
717730axes[0].set_title("Optimal (VFI)")
@@ -720,8 +733,7 @@ axes[0].set_ylim(0, K + 2)
720733# Q-learning snapshots
721734for i in range(n_snaps):
722735 σ_snap = snapshots[i]
723- np.random.seed(sim_seed)
724- X = sim_inventories(ts_length, σ_snap, p, X_init)
736+ X = sim_inventories(ts_length, σ_snap, p, X_init, seed=sim_seed)
725737 axes[i + 1].plot(X, alpha=0.7)
726738 axes[i + 1].set_ylabel("inventory")
727739 axes[i + 1].set_title(f"Step {snap_steps[i]:,}")