CS 188 — Bayes Nets & Tracking: pseudocode → Python
Your pseudocode, written out in the actual API of the project. Constants like PAC, X_RANGE, MAX_NOISE are already defined for you at the top of each starter function — I've kept them so each block reads standalone.
Part 1 — Factors & variable elimination
Q1 — constructBayesNet (inference.py)
def constructBayesNet(gameState):
# These five are given in the starter code
PAC = "Pacman"
GHOST0 = "Ghost0"
GHOST1 = "Ghost1"
OBS0 = "Observation0"
OBS1 = "Observation1"
X_RANGE = gameState.getWalls().width
Y_RANGE = gameState.getWalls().height
MAX_NOISE = 7
variables = [PAC, GHOST0, GHOST1, OBS0, OBS1]
edges = [(PAC, OBS0), (GHOST0, OBS0), (PAC, OBS1), (GHOST1, OBS1)]
positions = [(x, y) for x in range(X_RANGE) for y in range(Y_RANGE)]
maxManhattan = (X_RANGE - 1) + (Y_RANGE - 1)
obsDomain = list(range(maxManhattan + MAX_NOISE + 1)) # 0 .. max, inclusive
variableDomainsDict = {
PAC: positions,
GHOST0: positions,
GHOST1: positions,
OBS0: obsDomain,
OBS1: obsDomain,
}
return constructEmptyBayesNet(variables, edges, variableDomainsDict)
Watch: range(n) is exclusive, so you need maxManhattan + MAX_NOISE + 1 to actually include the largest reading. Off-by-one here is the usual q1 failure.
Q2 — joinFactors (factorOperations.py)
def joinFactors(factors):
factors = list(factors) # may arrive as a set or generator — pin it down first
# ... starter assertion about no variable being unconditioned in two factors stays here ...
unconditioned = set()
conditioned = set()
for factor in factors:
unconditioned |= set(factor.unconditionedVariables())
conditioned |= set(factor.conditionedVariables())
conditioned -= unconditioned # a var conditioned here but unconditioned there ends up unconditioned
domains = factors[0].variableDomainsDict()
newFactor = Factor(unconditioned, conditioned, domains)
for assignment in newFactor.getAllPossibleAssignmentDicts():
product = 1.0
for factor in factors:
product *= factor.getProbability(assignment) # extra keys are ignored
newFactor.setProbability(assignment, product)
return newFactor
Why the subtraction: joining P(A|B) with P(B) gives P(A,B) — B was conditioned in the first factor but unconditioned in the second, and the join resolves it to unconditioned.
Q3 — eliminate (factorOperations.py)
def eliminate(factor, eliminationVariable):
# ... starter assertions (var is unconditioned; factor has >1 unconditioned var) stay here ...
unconditioned = set(factor.unconditionedVariables()) - {eliminationVariable}
conditioned = set(factor.conditionedVariables())
domains = factor.variableDomainsDict() # same dict, unchanged
newFactor = Factor(unconditioned, conditioned, domains)
for assignment in newFactor.getAllPossibleAssignmentDicts():
total = 0.0
for value in domains[eliminationVariable]:
fullAssignment = dict(assignment) # copy, don't mutate
fullAssignment[eliminationVariable] = value
total += factor.getProbability(fullAssignment)
newFactor.setProbability(assignment, total)
return newFactor
Keeping the original variableDomainsDict matters — the eliminated variable stays in it even though it's no longer in the factor, and later joins rely on that.
Q4 — inferenceByVariableElimination (inference.py)
def inferenceByVariableElimination(bayesNet, queryVariables, evidenceDict, eliminationOrder):
# joinFactorsByVariable and eliminate are already bound above by the call-tracking wrapper
currentFactorsList = bayesNet.getAllCPTsWithEvidence(evidenceDict)
for var in eliminationOrder:
currentFactorsList, joinedFactor = joinFactorsByVariable(currentFactorsList, var)
if len(joinedFactor.unconditionedVariables()) == 1:
continue # sums to 1 — throw it away, do NOT call eliminate (it asserts)
currentFactorsList.append(eliminate(joinedFactor, var))
fullJoint = joinFactors(currentFactorsList)
return normalize(fullJoint)
joinFactorsByVariable does the "pull out the factors mentioning var, join them, hand back the rest" step in one call, so you don't filter the list by hand.
The single-unconditioned-variable case is exactly why eliminate has that second assertion — eliminating the last unconditioned variable would leave a factor over nothing.
Part 2 — Exact inference
Q5a — DiscreteDistribution (inference.py)
def normalize(self):
total = self.total()
if total == 0:
return # empty or all-zero: leave it alone
for key in list(self.keys()):
self[key] = self[key] / total # in place, returns None
def sample(self):
total = self.total()
r = random.random() * total # scaling by total means no need to normalize first
running = 0.0
for key, value in self.items():
running += value
if running > r:
return key
return list(self.keys())[-1] # float-rounding safety net
> vs >= only differs on zero-probability keys — with >=, a key with probability 0 sitting right where the running total lands could get returned. Use >.
Q5b — getObservationProb (InferenceModule)
def getObservationProb(self, noisyDistance, pacmanPosition, ghostPosition, jailPosition):
if ghostPosition == jailPosition:
return 1.0 if noisyDistance is None else 0.0
if noisyDistance is None:
return 0.0 # reading is None but ghost isn't jailed: impossible
trueDistance = manhattanDistance(pacmanPosition, ghostPosition)
return busters.getObservationProbability(noisyDistance, trueDistance)
Jail check first, as you noted — it's the only case where None is a valid reading.
Q6 — ExactInference.observeUpdate
B'(g) ∝ P(obs | pac, g) · B(g)
def observeUpdate(self, observation, gameState):
pacmanPosition = gameState.getPacmanPosition()
jailPosition = self.getJailPosition()
for pos in self.allPositions:
self.beliefs[pos] *= self.getObservationProb(
observation, pacmanPosition, pos, jailPosition)
self.beliefs.normalize()
Q7 — ExactInference.elapseTime
B'(g') = Σ_g P(g' | g) · B(g)
def elapseTime(self, gameState):
newBeliefs = DiscreteDistribution()
for oldPos in self.allPositions:
if self.beliefs[oldPos] == 0:
continue # skips the expensive getPositionDistribution call entirely
newPosDist = self.getPositionDistribution(gameState, oldPos)
for newPos, prob in newPosDist.items():
newBeliefs[newPos] += prob * self.beliefs[oldPos]
newBeliefs.normalize()
self.beliefs = newBeliefs
The continue is the whole optimization: once a few observations have come in, most positions have belief 0 and you skip nearly all the work.
Q8 — GreedyBustersAgent.chooseAction (bustersAgents.py)
def chooseAction(self, gameState):
pacmanPosition = gameState.getPacmanPosition()
legal = [a for a in gameState.getLegalPacmanActions()]
livingGhosts = gameState.getLivingGhosts()
livingGhostPositionDistributions = [
beliefs for i, beliefs in enumerate(self.ghostBeliefs)
if livingGhosts[i + 1]
]
# everything above is starter code
targets = [dist.argMax() for dist in livingGhostPositionDistributions]
bestAction = None
bestDistance = float('inf')
for action in legal:
successorPosition = Actions.getSuccessor(pacmanPosition, action)
d = min(self.distancer.getDistance(successorPosition, t) for t in targets)
if d < bestDistance:
bestDistance = d
bestAction = action
return bestAction
Note the i + 1 — index 0 of livingGhosts is Pacman, so ghost i lives at index i + 1.
Part 3 — Particle filtering
Q9 — ParticleFilter.initializeUniformly / getBeliefDistribution
def initializeUniformly(self, gameState):
self.particles = []
for i in range(self.numParticles):
self.particles.append(self.legalPositions[i % len(self.legalPositions)])
def getBeliefDistribution(self):
dist = DiscreteDistribution()
for particle in self.particles:
dist[particle] += 1 # DiscreteDistribution defaults missing keys to 0
dist.normalize()
return dist
Modular, not random — the autograder checks that the initial spread is exactly even.
Q10 — ParticleFilter.observeUpdate
def observeUpdate(self, observation, gameState):
pacmanPosition = gameState.getPacmanPosition()
jailPosition = self.getJailPosition()
weights = DiscreteDistribution()
for particle in self.particles:
weights[particle] += self.getObservationProb(
observation, pacmanPosition, particle, jailPosition)
if weights.total() == 0:
self.initializeUniformly(gameState) # all particles died — restart
else:
weights.normalize()
self.particles = [weights.sample() for _ in range(self.numParticles)]
Accumulating into a DiscreteDistribution keyed by position (rather than a parallel list) means duplicate particles automatically get proportionally more weight — which is what you want.
Q11 — ParticleFilter.elapseTime
def elapseTime(self, gameState):
newParticles = []
for particle in self.particles:
newPosDist = self.getPositionDistribution(gameState, particle)
newParticles.append(newPosDist.sample())
self.particles = newParticles
Speedup for the slow run: many particles sit on the same position, so you're recomputing identical distributions hundreds of times. Cache them:
def elapseTime(self, gameState):
cache = {}
newParticles = []
for particle in self.particles:
if particle not in cache:
cache[particle] = self.getPositionDistribution(gameState, particle)
newParticles.append(cache[particle].sample())
self.particles = newParticles
Same output distribution, usually a large fraction of the runtime gone.