Skip to content

Midterm Summary

Agents and environments

  • an agent perceives its environment through sensors and acts upon it through actuators (or effectors, depending on whom you ask)
  • the agent function maps percept sequences to actions
  • it is generated by an agent program running on a machine
   Agent                              Environment
     |  <-- Sensors  <-- Percepts <--     |
     |  --> Actuators --> Actions -->     |

The task environment: PEAS

  • Performance Measure
  • -1 per step
  • +10 food
  • +500 win
  • -500 die
  • +200 hit scared ghost
  • Environment
  • Pacman dynamics (incl. ghost behavior)
  • Actuators
  • Left / Right / Up / Down, or NSEW
  • Sensors
  • entire state is visible (except power pellet duration)

Agent design

The environment type largely determines the agent design: - partially observable → agent requires memory (internal state) - stochastic → agent may have to prepare for contingencies - multi-agent → agent may need to behave randomly - static → agent has time to compute a rational decision - continuous time → continuously operating controller - unknown physics → need to explore in reinforcement learning - unknown perf. measure → observe/interact with human principal

Utilities and Rationality

mapping state of the world to a real value

Utility: map state of world to real value

rational preferences — the five axioms: - Orderability: \((A > B) \lor (B > A) \lor (A \sim B)\) - Transitivity: \((A > B) \land (B > C) \Rightarrow (A > C)\) - Continuity: \((A > B > C) \Rightarrow \exists p\ [p, A;\ 1-p, C] \sim B\) - Substitutability: \((A \sim B) \Rightarrow [p, A;\ 1-p, C] \sim [p, B;\ 1-p, C]\) - Monotonicity: \((A > B) \Rightarrow \big( (p \geq q) \Leftrightarrow [p, A;\ 1-p, B] \geq [q, A;\ 1-q, B] \big)\)

given rational preferences, there exists \(U(X)\) s.t. - \(U(A) \geq U(B) \Leftrightarrow A \geq B\) - \(U([p_1, S_1;\ \dots;\ p_n, S_n]) = p_1 U(S_1) + \dots + p_n U(S_n)\)

MAXIMIZE UR EXPECTED UTILITY

Search Problems

the first part was search problems - a search problem consists of a state space - a successor function (with actions, costs) - a start state and a goal test

A solution is a sequence of actions (a plan) that transforms the start state to a goal state

State space Graphs vs Search Trees

  • each node in a search tree is an entire path in the state space graph
  • we construct only what we need on demand
state space graph              search tree
  (finite, cycles OK)            (can be infinite; each node = a path)

      S                                  S
     / \                               /   \
    a   b                            a      b
     \ /                            / \    / \
      c                            c   c  c   c
                                   |   |  |   |
                                  ... ... ... ...
  • important ideas
  • fringe
  • expansion
  • expansion strategy
  • main question: which fringe nodes to explore?
def tree_search(problem, fringe):
    fringe.push(make_node(problem.start_state))
    while True:
        if fringe.is_empty(): return failure
        node = fringe.pop()
        if problem.goal_test(node.state): return node.path
        for child in expand(node, problem):
            fringe.push(child)

DFS

  • expand the deepest node first
  • We use a LIFO stack to go as deep as possible
  • Storage is really good, but Time complexity is really bad
  • If \(m\) is finite, it takes time \(O(b^m)\)
  • largest space is the largest path — fringe only holds siblings on the path to root, so \(O(bm)\)
  • complete? \(m\) could be infinite, so only if we prevent cycles
  • optimal? No — it finds the "leftmost" solution, regardless of depth or cost

BFS

  • expand the shallowest node first
  • implementation: fringe is a FIFO queue
  • processes all nodes above the shallowest solution; let that depth be \(s\)
  • time \(O(b^s)\); space \(O(b^s)\) (roughly the last tier)
  • complete? yes — \(s\) must be finite if a solution exists
  • optimal? only if all costs are 1

UCS

  • orders by path cost, cheapest node first
  • fringe is a priority queue keyed on cumulative cost \(g(n)\)
  • time and space \(O(b^{C^*/\varepsilon})\) where \(C^*\) is the optimal cost and \(\varepsilon\) the minimum arc cost
  • complete and optimal (assuming positive costs)
  • BFS finds the shortest path in terms of number of actions; it does not find the least-cost path — UCS does

Greedy

  • orders by proximity — expand the node that seems closest, i.e. smallest heuristic value \(h(n)\)
  • a heuristic estimates how close a state is to a goal, designed for a particular search problem (e.g. Manhattan distance, Euclidean distance)
  • optimal? No — the resulting path to Bucharest is not the shortest

A*

  • combines both: UCS orders by backward cost \(g(n)\), greedy orders by forward cost \(h(n)\)
  • A* orders by the sum \(f(n) = g(n) + h(n)\)

when should A* terminate? - only when we remove the goal - we want to take the path only when we dequeue a goal node, because there can be a better path added before we dequeue it

\(h\) is admissible iff \(0 \leq h(n) \leq h^*(n)\) for all \(n\), where \(h^*(n)\) is the true cost to a nearest goal - i.e. the heuristic never overestimates — it's optimistic - often, admissible heuristics are solutions to relaxed problems, where new actions are available - coming up with admissible heuristics is most of what's involved in using A* in practice - inadmissible heuristics are often useful too

for any given search problem

how do we decide our fringe, how do we explore, etc.

def graph_search(problem, fringe):
    closed = set()                      # states already expanded
    fringe.push(make_node(problem.start_state))
    while True:
        if fringe.is_empty(): return failure
        node = fringe.pop()
        if problem.goal_test(node.state): return node.path
        if node.state not in closed:
            closed.add(node.state)
            for child in expand(node, problem):
                fringe.push(child)

the main idea here is admissibility

consistency - main idea: estimated heuristic costs \(\leq\) actual costs - admissibility: heuristic cost \(\leq\) actual cost to goal — \(h(v) \leq h^*(v)\) for all \(v \in V\). underestimate the true cost to the goal! - consistency: heuristic "arc" cost \(\leq\) actual cost for each arc — \(h(u) - h(v) \leq d(u, v)\) for all \((u, v) \in E\). underestimate the weight of every edge! - consequences of consistency: - the \(f\) value along a path never decreases: \(h(A) \leq \text{cost}(A \to C) + h(C)\) - A* graph search is optimal

if it's admissible, it's optimal (tree A); with a consistent heuristic, graph A is optimal

the same proof shows UCS is optimal where \(h = 0\) (trivial)

CONSTRAINT SATISFACTION

  • coloring australia
  • \(N\) variables, domain \(D\), constraints

  • States, Goal Tests, Successor Function

  • states: partial assignment
  • goal test: complete assignment that satisfies all constraints
  • successor function: assign an unassigned variable
  • backtracking search is the basic uninformed algorithm for solving CSPs
  • assign one variable at a time
  • variable assignments are commutative so fix ordering = better branching factor
  • i.e. [WA = red then NT = green] is the same as [NT = green then WA = red]
  • need to consider assignments to a single variable at each step
  • Check constraints as you go
  • i.e. consider only values which do not conflict with previous assignments
  • might have to do some computation to check the constraints
  • "incremental goal test"
  • depth-first search with these two improvements is called backtracking search (not the best name)
  • can solve \(n\)-queens for \(n \approx 25\)
  • backtracking = DFS + variable-ordering + fail-on-violation

backtracking example - every single step we check the constraints, fail on violation

def backtracking_search(csp):
    return recursive_backtracking({}, csp)

def recursive_backtracking(assignment, csp):
    if assignment is complete: return assignment
    var = select_unassigned_variable(csp, assignment)     # MRV
    for value in order_domain_values(var, assignment, csp):   # LCV
        if value is consistent with assignment:
            assignment[var] = value
            inferences = inference(csp, var, value)       # forward checking / AC-3
            if inferences != failure:
                result = recursive_backtracking(assignment, csp)
                if result != failure: return result
            remove var and inferences from assignment
    return failure

Filtering: Forward Checking

  • keep track of the domains for unassigned variables and cross off bad options
  • forward checking: cross off values that violate a constraint when added to the existing assignment
  • as a result of doing the first assignment we cross off some values
  • forward checking propagates information from assigned to unassigned variables, but doesn't provide early detection for all failures (e.g. NT and SA cannot both be blue — why didn't we detect this yet?)
  • Constraint Propagation: reason from constraint to constraint

Arc Consistency

  • an arc \(X \to Y\) is consistent iff for every \(x\) in the tail there is some \(y\) in the head which could be assigned without violating a constraint
  • delete from the tail!
  • forward checking = enforcing consistency of arcs pointing to each new assignment
  • if I modify a domain, I need to check all of its children as well, add them to the queue of arcs, and then we expand them
def AC_3(csp):
    queue = all arcs (Xi, Xj) in csp
    while queue is not empty:
        (Xi, Xj) = queue.pop()
        if remove_inconsistent_values(Xi, Xj):
            if domain(Xi) is empty: return failure
            for Xk in neighbors(Xi) - {Xj}:
                queue.push((Xk, Xi))
    return success

def remove_inconsistent_values(Xi, Xj):
    removed = False
    for x in domain(Xi):
        if no y in domain(Xj) satisfies the constraint (x, y):
            delete x from domain(Xi)
            removed = True
    return removed

\(O(n^2 d^3)\) why? - there are at most \(n^2\) arcs (each ordered pair of the \(n\) variables) - each arc can be pushed back onto the queue at most \(d\) times, since it only gets re-added when a value is deleted from its head's domain, and a domain has \(d\) values → at most \(n^2 d\) arc revisions - each call to remove_inconsistent_values costs \(O(d^2)\): for each of the \(d\) values in the tail, scan all \(d\) values in the head - total: \(n^2 d \cdot d^2 = O(n^2 d^3)\) - can be reduced to \(O(n^2 d^2)\) by caching, for each tail value, a supporting head value so you don't rescan from scratch - ... but detecting all possible future problems is NP-hard

K-Consistency

increasing degrees of consistency: - 1-consistency (node consistency): each single node's domain has a value which meets that node's unary constraints - 2-consistency (arc consistency): for each pair of nodes, any consistent assignment to one can be extended to the other - k-consistency: for each \(k\) nodes, any consistent assignment to \(k-1\) can be extended to the \(k\)th node - higher \(k\) is more expensive to compute - (you need to know the \(k = 2\) case: arc consistency)

We then talked about other ideas of expansion

MRV

choose one with the least remaining values left in the domain - fail fast ordering - also called "most constrained variable"

LCV

choose the least constraining value, one that rules out the fewest values in the remaining variables - note that it may take some computation to determine this (e.g. rerunning filtering) - combining these ordering ideas makes 1000 queens feasible

Iterative algorithms for CSPs

local search methods typically work with "complete" states, i.e. all variables assigned

to apply to CSPs: - take an assignment with unsatisfied constraints - operators reassign variable values - no fringe! live on the edge

def min_conflicts(csp, max_steps):
    current = a complete random assignment for csp
    for i in range(max_steps):
        if current satisfies all constraints: return current
        var = a randomly chosen conflicted variable
        value = the value for var that minimizes total violated constraints
        current[var] = value
    return failure
  • variable selection: randomly select any conflicted variable
  • value selection: min-conflicts heuristic — choose a value that violates the fewest constraints, i.e. hill climb with \(h(x)\) = total number of violated constraints

Tree structured CSPs

  • choose a root variable; order variables so that parents precede children
  • remove backward: for \(i = n : 2\), apply RemoveInconsistent(Parent(Xi), Xi)
  • assign forward: for \(i = 1 : n\), assign \(X_i\) consistently with Parent(Xi)
  • runtime: \(O(n d^2)\)\(n-1\) arcs, each revised once at \(O(d^2)\), with no re-queuing because the tree structure means backward passes never invalidate an earlier arc

Game playing: incorporate search with other agents

  • different types of assumptions about agents. Need the best possible strategy.
  • assume agents are in a competitive mode (zero sum) and they are effectively minimizing my output while maximizing theirs

The value of a state can be the best outcome from a state

Terminal States: \(V(s)\) known

Minimax

  • states under the agent's control → MAX nodes: \(V(s) = \max_{s' \in \text{successors}(s)} V(s')\)
  • states under the opponent's control → MIN nodes: \(V(s) = \min_{s' \in \text{successors}(s)} V(s')\)
  • terminal states: \(V(s) = \text{utility}(s)\)
def value(state):
    if the state is terminal: return the state's utility
    if the next agent is MAX:  return max_value(state)
    if the next agent is MIN:  return min_value(state)

def max_value(state):
    v = -
    for successor in successors(state):
        v = max(v, value(successor))
    return v

def min_value(state):
    v = +
    for successor in successors(state):
        v = min(v, value(successor))
    return v
                    ▲ 3                 MAX picks the largest child
                /   |   \
             ▼      ▼      ▼            MIN picks the smallest child
            /|\    /|\    /|\
          3 12 8  2 4 6  14 5 2         terminal utilities
  • time \(O(b^m)\), space \(O(bm)\) — same as exhaustive DFS

Alpha - Beta pruning

  • \(\alpha\): MAX's best option on path to root
  • \(\beta\): MIN's best option on path to root
def max_value(state, α, β):
    v = -
    for successor in successors(state):
        v = max(v, value(successor, α, β))
        if v >= β: return v
        α = max(α, v)
    return v

def min_value(state, α, β):
    v = +
    for successor in successors(state):
        v = min(v, value(successor, α, β))
        if v <= α: return v
        β = min(β, v)
    return v
  • pruning has no effect on the minimax value of the root
  • with a perfect ordering, time drops to \(O(b^{m/2})\) — doubles the solvable depth

Multi agent utilities

  • what if the game is not zero-sum? or has multiple players?

generalization of minimax - terminals have utility tuples - node values are also utility tuples - each player maximizes its own component - can give rise to cooperation and competition dynamically

Chance nodes

  • we don't know what the result of an action will be:
  • explicit randomness: rolling dice
  • unpredictable opponents
  • actions can fail
  • values should reflect average case (expectimax) outcomes, not worst-case (minimax) outcomes
  • expectimax search: compute the average score under optimal play
  • max nodes as in minimax search
  • chance nodes: calculate expected utilities, \(V(s) = \sum_{s'} P(s' | s) V(s')\)
def value(state):
    if the state is a terminal state: return the state's utility
    if the next agent is MAX: return max_value(state)
    if the next agent is EXP: return exp_value(state)

def max_value(state):
    v = -
    for successor in successors(state):
        v = max(v, value(successor))
    return v

def exp_value(state):
    v = 0
    for successor in successors(state):
        p = probability(successor)
        v += p * value(successor)
    return v

MDPs

an MDP is defined by: - a set of states \(s \in S\) - a set of actions \(a \in A\) - a transition function \(T(s, a, s')\) - probability that \(a\) from \(s\) leads to \(s'\): \(P(s'|s, a)\) - also called the model or the dynamics - a reward function \(R(s, a, s')\) (sometimes just \(R(s)\) or \(R(s')\)) - a start state - maybe a terminal state

MDPs are non-deterministic search problems - one way to solve is with expectimax

Policies

  • in deterministic search: we wanted a plan, a sequence of actions from start to goal
  • for MDPs we want an optimal policy \(\pi^*: S \to A\)
  • a policy gives an action for each state
  • an optimal policy is one that maximizes expected utility if followed
  • an explicit policy defines a reflex agent
  • we want a way to define this policy
  • expectimax didn't compute entire policies — it computed the action for a single state only

What is Markov about MDPs?

  • given the present state, the future and past are independent
  • action outcomes depend only on the current state
  • for Markov Decision Processes:
  • \(P(s_{t+1} = s' \mid s_t = s_t, a_t = a_t, s_{t-1}, a_{t-1}, \dots, s_0) = P(s_{t+1} = s' \mid s_t = s_t, a_t = a_t)\)
  • this is just like search, where the successor function could only depend on the current state (not the history)

Discounting

In all of my configurations, I want to get the highest possible utility of reward - we use discounting; it's also reasonable to prefer rewards now to rewards later - one solution: values of rewards decay exponentially — worth \(1\) now, \(\gamma\) next step, \(\gamma^2\) in two steps - \(U([r_0, r_1, r_2, \dots]) = \sum_{t=0}^{\infty} \gamma^t r_t\)

the goal is to maximize the expected sum of discounted rewards

Optimal quantities

now we can have some quantities, by which we can get the max reward

  • First \(V^*(s)\) is the expected utility starting in \(s\) and acting optimally
  • the utility of a Q-state: \(Q^*(s, a)\) is the expected utility starting out having taken an action \(a\) from state \(s\) and thereafter acting optimally
  • the optimal policy: \(\pi^*(s)\) = optimal action from state \(s\)

we have multiple \(Q(s, a)\) at every state, for each action that can be taken

VALUE ITERATION

computing the value or the q value function

start at \(V_0(s) = 0\) for each state — no time steps left means an expected reward sum of zero — and then we will compute the values

compute one step of expectimax for each state:

\(V_{k+1}(s) \leftarrow \max_a \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma V_k(s')\big]\)

repeat till convergence

complexity of each iteration \(O(S^2 A)\)

def value_iteration(mdp, gamma, epsilon):
    V = {s: 0 for s in mdp.states}
    while True:
        V_new = {}
        for s in mdp.states:
            best = -
            for a in mdp.actions(s):
                q = 0
                for s_prime in mdp.states:
                    t = mdp.T(s, a, s_prime)
                    q += t * (mdp.R(s, a, s_prime) + gamma * V[s_prime])
                best = max(best, q)
            V_new[s] = best
        if max over s of abs(V_new[s] - V[s]) < epsilon: return V_new
        V = V_new
  • theorem: will converge to unique optimal values
  • basic idea: approximations get refined towards optimal values
  • policy may converge long before values do
  • value iteration is just a fixed point solution method, though the \(V_k\) vectors are also interpretable as time-limited values

the bellman equations

How to be optimal 1. take the correct first action 2. continue being optimal

  • definition of optimal utility via expectimax recurrence gives a simple one-step lookahead relationship amongst optimal utility values
  • \(V^*(s) = \max_a Q^*(s, a)\)
  • \(Q^*(s, a) = \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma V^*(s')\big]\)
  • \(\pi^*(s) = \arg\max_a Q^*(s, a)\)
  • combined: \(V^*(s) = \max_a \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma V^*(s')\big]\)

these are the Bellman equations, and they characterize optimal values in a way we'll use over and over

POLICY EVALUATION

  • how do we calculate the \(V\)'s for a fixed policy \(\pi\)?
  • Idea 1: turn recursive bellman equations into updates (like value iteration)

\(V^\pi_0(s) = 0\)

\(V^\pi_{k+1}(s) = \sum_{s'} T(s, \pi(s), s')\big[R(s, \pi(s), s') + \gamma V^\pi_k(s')\big]\)

  • efficiency: \(O(S^2)\) per iteration (no max over actions)
  • Idea 2: without the maxes, the Bellman equations are just a linear system — solve with your favorite linear system solver
def policy_evaluation(mdp, policy, gamma, epsilon):
    V = {s: 0 for s in mdp.states}
    while True:
        V_new = {}
        for s in mdp.states:
            a = policy[s]
            total = 0
            for s_prime in mdp.states:
                t = mdp.T(s, a, s_prime)
                total += t * (mdp.R(s, a, s_prime) + gamma * V[s_prime])
            V_new[s] = total
        if max over s of abs(V_new[s] - V[s]) < epsilon: return V_new
        V = V_new

Computing actions from values

how do I compute my policy given an optimal value function

how should we act? it's pretty obvious, pick the action with the largest \(V\) for that action - from values it's not obvious — we need to do a mini-expectimax (one step): - \(\pi^*(s) = \arg\max_a \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma V^*(s')\big]\) - this is called policy extraction, since it gets the policy implied by the values - from q-values it's completely trivial: - \(\pi^*(s) = \arg\max_a Q^*(s, a)\) - important lesson: actions are easier to extract from q-values than values!

POLICY ITERATION

  • policy evaluation: calculate utilities for some fixed policy (not optimal utilities!) until convergence
  • policy improvement: update policy using one step look-ahead with resulting (converged but not optimal) utilities as future values
  • repeat until the policy stops changing
def policy_iteration(mdp, gamma):
    policy = {s: an arbitrary action for s in mdp.states}
    while True:
        V = policy_evaluation(mdp, policy, gamma, epsilon)
        stable = True
        for s in mdp.states:
            best_a, best_q = None, -
            for a in mdp.actions(s):
                q = 0
                for s_prime in mdp.states:
                    t = mdp.T(s, a, s_prime)
                    q += t * (mdp.R(s, a, s_prime) + gamma * V[s_prime])
                if q > best_q:
                    best_q, best_a = q, a
            if best_a != policy[s]:
                policy[s] = best_a
                stable = False
        if stable: return policy, V

this is policy iteration - it's still optimal and can converge much faster under some conditions

Reinforcement learning

  • in MDPs we were given everything: \(T\), \(R\), etc.
  • in Reinforcement learning, we don't know \(T\) and \(R\)
  • still assume an MDP (\(S\), \(A\), \(T(s,a,s')\), \(R(s,a,s')\)), still looking for a policy \(\pi(s)\)
  • new twist: we don't know which rewards are good or what the actions do, so we must actually try out actions and states to learn

  • basic idea

  • receive feedback in the form of rewards
  • agent's utility is defined by the reward function
  • must learn to act so as to maximize expected rewards
  • all learning is based on observed samples of outcomes

before: if someone gave me all these values, I could just compute it - I can't compute it anymore, I have to experience the world to identify this

two types

  • model based learning
  • learn an approximate model based on experiences
  • solve for values as if the learned model were correct
  • step one: learn empirical MDP model
    • count outcomes \(s'\) for each \(s\) and \(a\)
    • normalize to give an estimate of \(\hat{T}(s, a, s')\)
    • discover each \(\hat{R}(s, a, s')\) when we experience it
  • solve the learned MDP
    • for example, use value iteration, as before
def model_based_rl(episodes, gamma):
    counts = {}       # (s, a, s') -> count
    rewards = {}      # (s, a, s') -> observed reward
    for (s, a, s_prime, r) in episodes:
        counts[(s, a, s_prime)] += 1
        rewards[(s, a, s_prime)] = r
    T_hat = {}
    for (s, a, s_prime) in counts:
        total = sum of counts[(s, a, x)] over all x
        T_hat[(s, a, s_prime)] = counts[(s, a, s_prime)] / total
    return value_iteration(mdp_from(T_hat, rewards), gamma, epsilon)
  • model-free learning
  • I don't need to know \(T\) and \(R\) and I can still get the \(Q\) and \(V\) value
  • simplified task: policy evaluation (passive RL)
    • input a fixed policy \(\pi(s)\)
    • you don't know the transitions \(T(s,a,s')\)
    • you don't know the rewards \(R(s,a,s')\)
    • goal: learn the state values
    • the learner is "along for the ride" — no choice about what actions to take, just execute the policy and learn from experience
    • this is NOT offline planning! you actually take actions in the world

direct evaluation

  • easy to understand, don't need \(T\) and \(R\), but there are some dependencies between those states, and we need to learn a value from experiencing the world
  • it eventually computes the correct average values, using just sample transitions
  • so this takes a while — it wastes information about state connections, and each state must be learned separately
  • if B and E both go to C under this policy, how can their values be different?

Temporal difference learning

  • big idea: learn from every experience
  • update \(V\) every time you experience a transition \((s, a, s', r)\)
  • likely outcomes \(s'\) will contribute more often
  • difference learning of values
  • policy is still fixed, still doing evaluation
  • move values toward value of whatever successor occurs: running average

Sample of \(V(s)\): \(\text{sample} = R(s, a, s') + \gamma V^\pi(s')\)

Update to \(V(s)\): \(V^\pi(s) \leftarrow (1 - \alpha) V^\pi(s) + \alpha \cdot \text{sample}\)

Same update: \(V^\pi(s) \leftarrow V^\pi(s) + \alpha \big[\text{sample} - V^\pi(s)\big]\)

def td_value_learning(policy, alpha, gamma, episodes):
    V = {s: 0 for s in states}
    for (s, a, s_prime, r) in episodes:
        sample = r + gamma * V[s_prime]
        V[s] = V[s] + alpha * (sample - V[s])
    return V

Problems with TD learning: - TD value learning is a model-free way to do policy evaluation, mimicking Bellman updates with running sample averages - however, if we want to turn values into a (new) policy, we're sunk — policy extraction from \(V\) needs \(T\) and \(R\), which we don't have - idea: learn Q-values, not values. makes action selection model-free too!

Detour: q-value iteration

  • value iteration: find successive depth-limited values
  • start with \(V_0 = 0\), which we know is right
  • given \(V_k\), calculate the depth \(k+1\) values for all states:
  • \(V_{k+1}(s) \leftarrow \max_a \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma V_k(s')\big]\)

But q values are more useful, so compute them instead - start with \(Q_0(s, a) = 0\), which we know is right - given \(Q_k\), calculate the depth \(k+1\) q-values for all q-states: - \(Q_{k+1}(s, a) \leftarrow \sum_{s'} T(s, a, s')\big[R(s, a, s') + \gamma \max_{a'} Q_k(s', a')\big]\)

the model-free sample version of this is Q-learning:

\(Q(s, a) \leftarrow (1 - \alpha) Q(s, a) + \alpha \big[r + \gamma \max_{a'} Q(s', a')\big]\)

def q_learning(alpha, gamma, episodes):
    Q = {(s, a): 0 for each s, a}
    for (s, a, s_prime, r) in episodes:
        best_next = max over a_prime of Q[(s_prime, a_prime)]
        sample = r + gamma * best_next
        Q[(s, a)] = Q[(s, a)] + alpha * (sample - Q[(s, a)])
    return Q

Probability

full tables

\(P(T, W)\)

T W P
hot sun 0.4
hot rain 0.1
cold sun 0.2
cold rain 0.3

the four operations on a joint table: - summing out (marginalization): \(P(T) = \sum_w P(T, W = w)\) - normalization: divide by \(Z = \sum\) entries so the table sums to 1 - conditional probability / Bayes' rule: \(P(x|y) = \frac{P(x, y)}{P(y)}\), and \(P(x|y) = \frac{P(y|x)P(x)}{P(y)}\) - chain rule: \(P(x_1, x_2, \dots, x_n) = \prod_i P(x_i | x_1, \dots, x_{i-1})\)

conditional independence

\(X\) and \(Y\) are independent iff \(\forall x, y: P(x, y) = P(x)P(y)\)

given \(Z\) we can say that \(X\) and \(Y\) are conditionally independent iff \(\forall x, y, z: P(x, y|z) = P(x|z) P(y|z)\)

(conditional) independence is a property of a distribution

Bayesian Networks

  • a DAG: one node per RV
  • conditional probability table for each node
  • prob of \(X\) given a combination of values for parents
  • bayes nets implicitly encode joint distributions as a product of local conditional distributions
  • \(P(x_1, x_2, \dots, x_n) = \prod_{i=1}^{n} P(x_i | \text{parents}(X_i))\)

  • definition: each node, given its parents, is conditionally independent of all its non-descendants on the graph

  • each node, given its Markov blanket, is conditionally independent of all other nodes in the graph
  • the Markov blanket refers to the parents, children, and children's other parents

Inference by enumeration

  • general case
  • evidence variables \(E_1, \dots, E_k\)
  • query variable \(Q\)
  • Hidden variables \(H_1, \dots, H_r\)
  • all variables: \(\{Q\} \cup E \cup H\)

step 1: look at only the entries consistent with the evidence step 2: sum out \(H\) to get the joint of query and evidence step 3: normalize

we end up wanting \(P(Q | e_1, e_2, e_3, \dots)\)

\(P(Q | e_1 \dots e_k) = \frac{1}{Z} \sum_{h_1 \dots h_r} P(Q, h_1 \dots h_r, e_1 \dots e_k)\)

def inference_by_enumeration(bn, Q, evidence):
    table = {}
    for q in domain(Q):
        total = 0
        for h in all assignments to the hidden variables:
            total += bn.joint_probability(Q=q, hidden=h, evidence=evidence)
        table[q] = total
    Z = sum of table.values()
    for q in table: table[q] = table[q] / Z
    return table

if R->T->L whats P(L)?

way 1 — inference by enumeration: build the full joint, then sum out both hidden variables at the end

\(P(L) = \sum_r \sum_t P(r) P(t|r) P(L|t)\)

way 2 — variable elimination: interleave joining and marginalizing so the tables never get big

  • join on \(R\): \(P(R, T) = P(R) \cdot P(T|R)\)
  • eliminate \(R\): \(P(T) = \sum_r P(r, T)\)
  • join on \(T\): \(P(T, L) = P(T) \cdot P(L|T)\)
  • eliminate \(T\): \(P(L) = \sum_t P(t, L)\)

both give the same answer; VE never builds a factor bigger than 2 variables here, while enumeration builds the full 3-variable joint first

General Variable Elimination

  • query: \(P(Q | E_1 = e_1, \dots, E_k = e_k)\)
  • start with initial factors: local CPTs (but instantiated by evidence)
  • while there are still hidden variables (not \(Q\) or evidence):
  • pick a hidden variable \(H\)
  • join all factors mentioning \(H\)
  • eliminate (sum out) \(H\)
  • join all remaining factors and normalize
def variable_elimination(bn, Q, evidence, ordering):
    factors = [cpt.instantiate(evidence) for cpt in bn.cpts]
    for H in ordering:
        relevant = [f for f in factors if H in f.variables]
        for f in relevant: factors.remove(f)
        joined = pointwise_product(relevant)
        factors.append(joined.sum_out(H))
    result = pointwise_product(factors)
    return result.normalize()
  • runtime is dominated by the size of the largest factor produced; the ordering determines that size, and picking a good ordering is hard in general (polytrees always have an efficient one)

independence assumptions in a bayes net

  • assumptions we are required to make to define the bayes net when given the graph
  • active and inactive paths

active triples (influence flows through): - causal chain \(A \to B \to C\) with \(B\) unobserved - common cause \(A \leftarrow B \to C\) with \(B\) unobserved - common effect \(A \to B \leftarrow C\) with \(B\) or one of its descendants observed

inactive triples (influence is blocked): the complement of each of the above

all it takes to block a path is a single inactive segment

D-separation

  • query: is \(X_i \perp X_j \mid \{X_{k1}, \dots, X_{kn}\}\)?
  • check all (undirected!) paths between \(X_i\) and \(X_j\)
  • if one or more active paths, then independence is not guaranteed
  • otherwise (i.e. if all paths are inactive), then independence is guaranteed

topology limits distributions

  • given some graph topology \(G\), only certain joint distributions can be encoded
  • graph structure guarantees certain conditional dependencies and some independencies
  • (there might be more independence)
  • adding arcs increases the set of distributions, but has several costs
  • full conditioning can encode any distribution

Approximate Inference: Sampling

Prior sampling

instead of having access to the variables, we are going to sample them from the distribution and then produce the net

we are going to sample \(x_i\) from \(P(X_i | \text{Parents}(X_i))\), in topological order

def prior_sample(bn):
    x = {}
    for Xi in bn.variables_in_topological_order:
        x[Xi] = sample from P(Xi | parents assigned in x)
    return x
  • consistent: the probability of drawing a given full assignment is exactly the joint probability the BN assigns to it

rejection sampling

we have all the evidence; we are going to sample, if it's not consistent with the evidence, we remove it

def rejection_sample(bn, evidence):
    x = {}
    for Xi in bn.variables_in_topological_order:
        x[Xi] = sample from P(Xi | parents assigned in x)
        if Xi in evidence and x[Xi] != evidence[Xi]:
            return None          # reject: no sample this cycle
    return x
  • problem: with unlikely evidence you throw away almost everything

likelihood weighting

fix the evidence variables instead of sampling them, and weight each sample by how likely that evidence was

def likelihood_weighted_sample(bn, evidence):
    x = {}
    w = 1.0
    for Xi in bn.variables_in_topological_order:
        if Xi in evidence:
            x[Xi] = evidence[Xi]
            w = w * P(x[Xi] | parents assigned in x)
        else:
            x[Xi] = sample from P(Xi | parents assigned in x)
    return x, w
  • evidence influences the choice of downstream variables, but not upstream ones — that's the limitation

Gibbs sampling

  • step 1: fix the evidence (e.g. \(R = +r\))
  • step 2: initialize the other variables randomly
  • step 3: repeat
  • choose a non-evidence variable \(X\)
  • resample \(X\) from \(P(X | \text{MarkovBlanket}(X))\)
def gibbs_sample(bn, evidence, steps):
    x = evidence copied, plus a random value for every non-evidence variable
    for i in range(steps):
        X = a non-evidence variable (cycled or chosen at random)
        x[X] = sample from P(X | MarkovBlanket(X) as assigned in x)
    return x
  • fixes the upstream problem: you can sample any variable given all the others, so reverse queries work
  • successive samples are highly correlated (each depends on the previous), but the procedure is still consistent
  • CheatSheet — HS and Berkeley summary sheets for the same material