diff --git a/.gitmodules b/.gitmodules index e15cc9e9a..b5b05a732 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "aima-data"] - path = aima-data +[submodule "aimaPy/aima-data"] + path = aimaPy/aima-data url = https://github.com/aimacode/aima-data diff --git a/.travis.yml b/.travis.yml index 1a29f71f9..129b41668 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,13 @@ language: python: - "3.5" +before_install: + - git submodule update --remote + install: - pip install flake8 - pip install -r requirements.txt + - python setup.py install script: - py.test diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..57ce8b68e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +graft aimaPy/aima-data +graft aimaPy/images \ No newline at end of file diff --git a/aima-data b/aima-data deleted file mode 160000 index 75a59e53a..000000000 --- a/aima-data +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 75a59e53ab83773ac2838ddef4ac885d103f4511 diff --git a/aimaPy/__init__.py b/aimaPy/__init__.py new file mode 100644 index 000000000..ae5bf6149 --- /dev/null +++ b/aimaPy/__init__.py @@ -0,0 +1,14 @@ +from . import agents +from . import csp +from . import games +from . import grid +from . import learning +from . import logic +from . import mdp +from . import nlp +from . import planning +from . import probability +from . import rl +from . import search +from . import text +from . import utils \ No newline at end of file diff --git a/agents.py b/aimaPy/agents.py similarity index 92% rename from agents.py rename to aimaPy/agents.py index 1bc374150..e04959837 100644 --- a/agents.py +++ b/aimaPy/agents.py @@ -35,17 +35,20 @@ # # Speed control in GUI does not have any effect -- fix it. -from utils import * +from . utils import * import random import copy +import collections #______________________________________________________________________________ class Thing(object): + """This represents any physical object that can appear in an Environment. You subclass Thing to get the things you want. Each thing can have a .__name__ slot (used for output only).""" + def __repr__(self): return '<{}>'.format(getattr(self, '__name__', self.__class__.__name__)) @@ -62,7 +65,9 @@ def display(self, canvas, x, y, width, height): "Display an image of this Thing on the canvas." pass + class Agent(Thing): + """An Agent is a subclass of Thing with one required slot, .program, which should hold a function that takes one argument, the percept, and returns an action. (What counts as a percept or action @@ -80,8 +85,8 @@ def __init__(self, program=None): self.bump = False if program is None: def program(percept): - return input('Percept={}; action? ' .format(percept)) - assert callable(program) + return eval(input('Percept={}; action? ' .format(percept))) + assert isinstance(program, collections.Callable) self.program = program def can_grab(self, thing): @@ -89,10 +94,12 @@ def can_grab(self, thing): Override for appropriate subclasses of Agent and Thing.""" return False + def TraceAgent(agent): """Wrap the agent's program to print its input and output. This will let you see what the agent is doing in the environment.""" old_program = agent.program + def new_program(percept): action = old_program(percept) print('{} perceives {} and does {}'.format(agent, percept, action)) @@ -102,24 +109,28 @@ def new_program(percept): #______________________________________________________________________________ + def TableDrivenAgentProgram(table): """This agent selects an action based on the percept sequence. It is practical only for tiny domains. To customize it, provide as table a dictionary of all {percept_sequence:action} pairs. [Fig. 2.7]""" percepts = [] + def program(percept): percepts.append(percept) action = table.get(tuple(percepts)) return action return program + def RandomAgentProgram(actions): "An agent that chooses an action at random, ignoring all percepts." return lambda percept: random.choice(actions) #______________________________________________________________________________ + def SimpleReflexAgentProgram(rules, interpret_input): "This agent takes action based solely on the percept. [Fig. 2.10]" def program(percept): @@ -129,6 +140,7 @@ def program(percept): return action return program + def ModelBasedReflexAgentProgram(rules, update_state): "This agent takes action based on the percept and state. [Fig. 2.12]" def program(percept): @@ -139,6 +151,7 @@ def program(percept): program.state = program.action = None return program + def rule_match(state, rules): "Find the first rule that matches state." for rule in rules: @@ -147,7 +160,7 @@ def rule_match(state, rules): #______________________________________________________________________________ -loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world +loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world def RandomVacuumAgent(): @@ -174,27 +187,37 @@ def TableDrivenVacuumAgent(): def ReflexVacuumAgent(): "A reflex agent for the two-state vacuum environment. [Fig. 2.8]" def program(location, status): - if status == 'Dirty': return 'Suck' - elif location == loc_A: return 'Right' - elif location == loc_B: return 'Left' + if status == 'Dirty': + return 'Suck' + elif location == loc_A: + return 'Right' + elif location == loc_B: + return 'Left' return Agent(program) + def ModelBasedVacuumAgent(): "An agent that keeps track of what locations are clean or dirty." model = {loc_A: None, loc_B: None} + def program(location, status): "Same as ReflexVacuumAgent, except if everything is clean, do NoOp." - model[location] = status ## Update the model here - if model[loc_A] == model[loc_B] == 'Clean': return 'NoOp' - elif status == 'Dirty': return 'Suck' - elif location == loc_A: return 'Right' - elif location == loc_B: return 'Left' + model[location] = status # Update the model here + if model[loc_A] == model[loc_B] == 'Clean': + return 'NoOp' + elif status == 'Dirty': + return 'Suck' + elif location == loc_A: + return 'Right' + elif location == loc_B: + return 'Left' return Agent(program) #______________________________________________________________________________ class Environment(object): + """Abstract class representing an Environment. 'Real' Environment classes inherit from this. Your Environment will typically need to implement: percept: Define the percept that an agent sees. @@ -210,7 +233,7 @@ def __init__(self): self.agents = [] def thing_classes(self): - return [] ## List of classes that can go into environment + return [] # List of classes that can go into environment def percept(self, agent): "Return the percept that the agent sees at this point. (Implement this.)" @@ -247,7 +270,8 @@ def step(self): def run(self, steps=1000): "Run the Environment for given number of time steps." for step in range(steps): - if self.is_done(): return + if self.is_done(): + return self.step() def list_things_at(self, location, tclass=Thing): @@ -282,11 +306,13 @@ def delete_thing(self, thing): print(" in Environment delete_thing") print(" Thing to be removed: {} at {}" .format(thing, thing.location)) print(" from list: {}" .format([(thing, thing.location) - for thing in self.things])) + for thing in self.things])) if thing in self.agents: self.agents.remove(thing) + class XYEnvironment(Environment): + """This class is for environments on a 2D plane, with locations labelled by (x, y) points, either discrete or continuous. @@ -301,7 +327,8 @@ def __init__(self, width=10, height=10): def things_near(self, location, radius=None): "Return all things within radius of location." - if radius is None: radius = self.perceptible_distance + if radius is None: + radius = self.perceptible_distance radius2 = radius * radius return [thing for thing in self.things if distance2(location, thing.location) <= radius2] @@ -330,7 +357,7 @@ def execute_action(self, agent, action): if agent.holding: agent.holding.pop() - def thing_percept(self, thing, agent): #??? Should go to thing? + def thing_percept(self, thing, agent): # ??? Should go to thing? "Return the percept for this thing." return thing.__class__.__name__ @@ -380,21 +407,27 @@ def turn_heading(self, heading, inc): "Return the heading to the left (inc=+1) or right (inc=-1) of heading." return turn_heading(heading, inc) + class Obstacle(Thing): + """Something that can cause a bump, preventing an agent from moving into the same square it's in.""" pass + class Wall(Obstacle): pass #______________________________________________________________________________ -## Vacuum environment +# Vacuum environment + class Dirt(Thing): pass + class VacuumEnvironment(XYEnvironment): + """The environment of [Ex. 2.12]. Agent perceives dirty or clean, and bump (into obstacle) or not; 2D discrete world of unknown size; performance measure is 100 for each dirt cleaned, and -1 for @@ -411,7 +444,8 @@ def thing_classes(self): def percept(self, agent): """The percept is a tuple of ('Dirty' or 'Clean', 'Bump' or 'None'). Unlike the TrivialVacuumEnvironment, location is NOT perceived.""" - status = ('Dirty' if self.some_things_at(agent.location, Dirt) else 'Clean') + status = ('Dirty' if self.some_things_at( + agent.location, Dirt) else 'Clean') bump = ('Bump' if agent.bump else'None') return (status, bump) @@ -428,7 +462,9 @@ def execute_action(self, agent, action): if action != 'NoOp': agent.performance -= 1 + class TrivialVacuumEnvironment(Environment): + """This environment has two locations, A and B. Each can be Dirty or Clean. The agent perceives its location and the location's status. This serves as an example of how to implement a simple @@ -466,13 +502,28 @@ def default_location(self, thing): return random.choice([loc_A, loc_B]) #______________________________________________________________________________ -## The Wumpus World +# The Wumpus World + + +class Gold(Thing): + pass + + +class Pit(Thing): + pass + + +class Arrow(Thing): + pass + + +class Wumpus(Agent): + pass + + +class Explorer(Agent): + pass -class Gold(Thing): pass -class Pit(Thing): pass -class Arrow(Thing): pass -class Wumpus(Agent): pass -class Explorer(Agent): pass class WumpusEnvironment(XYEnvironment): @@ -483,7 +534,7 @@ def __init__(self, width=10, height=10): def thing_classes(self): return [Wall, Gold, Pit, Arrow, Wumpus, Explorer] - ## Needs a lot of work ... + # Needs a lot of work ... #______________________________________________________________________________ @@ -497,6 +548,7 @@ def compare_agents(EnvFactory, AgentFactories, n=10, steps=1000): return [(A, test_agent(A, steps, copy.deepcopy(envs))) for A in AgentFactories] + def test_agent(AgentFactory, steps, envs): "Return the mean score of running an agent in each of the envs, for steps" def score(env): @@ -504,7 +556,7 @@ def score(env): env.add_thing(agent) env.run(steps) return agent.performance - return mean(map(score, envs)) + return mean(list(map(score, envs))) #_________________________________________________________________________ @@ -537,6 +589,3 @@ def score(env): >>> 0.5 < testv(RandomVacuumAgent) < 3 True """ - - - diff --git a/aimaPy/aima-data b/aimaPy/aima-data new file mode 160000 index 000000000..5b0526a5a --- /dev/null +++ b/aimaPy/aima-data @@ -0,0 +1 @@ +Subproject commit 5b0526a5a4d4312c3e65254c7e205a7ce327503b diff --git a/csp.py b/aimaPy/csp.py similarity index 82% rename from csp.py rename to aimaPy/csp.py index 1ec2f63b8..25186ab07 100644 --- a/csp.py +++ b/aimaPy/csp.py @@ -1,10 +1,14 @@ """CSP (Constraint Satisfaction Problems) problems and solvers. (Chapter 6).""" -from utils import * + +from . utils import * from collections import defaultdict -import search +from . import search +from functools import reduce + class CSP(search.Problem): + """This class describes finite-domain Constraint Satisfaction Problems. A CSP is specified by the following inputs: vars A list of variables; each is atomic (e.g. int or string). @@ -45,7 +49,7 @@ class CSP(search.Problem): def __init__(self, vars, domains, neighbors, constraints): "Construct a CSP problem. If vars is empty, it becomes domains.keys()." - vars = vars or domains.keys() + vars = vars or list(domains.keys()) update(self, vars=vars, domains=domains, neighbors=neighbors, constraints=constraints, initial=(), curr_domains=None, nassigns=0) @@ -73,9 +77,9 @@ def conflict(var2): def display(self, assignment): "Show a human-readable representation of the CSP." # Subclasses can print in a prettier way, or display with a GUI - print 'CSP:', self, 'with assignment:', assignment + print('CSP:', self, 'with assignment:', assignment) - ## These methods are for the tree- and graph-search interface: + # These methods are for the tree- and graph-search interface: def actions(self, state): """Return a list of applicable actions: nonconflicting @@ -88,8 +92,9 @@ def actions(self, state): return [(var, val) for val in self.domains[var] if self.nconflicts(var, val, assignment) == 0] - def result(self, state, (var, val)): + def result(self, state, xxx_todo_changeme): "Perform an action and return the new state." + (var, val) = xxx_todo_changeme return state + ((var, val),) def goal_test(self, state): @@ -100,7 +105,7 @@ def goal_test(self, state): assignment) == 0, self.vars)) - ## These are for constraint propagation + # These are for constraint propagation def support_pruning(self): """Make sure we can prune values from domains. (We want to pay @@ -119,7 +124,8 @@ def suppose(self, var, value): def prune(self, var, value, removals): "Rule out var=value." self.curr_domains[var].remove(value) - if removals is not None: removals.append((var, value)) + if removals is not None: + removals.append((var, value)) def choices(self, var): "Return all values for var that aren't currently ruled out." @@ -136,7 +142,7 @@ def restore(self, removals): for B, b in removals: self.curr_domains[B].append(b) - ## This is for min_conflicts search + # This is for min_conflicts search def conflicted_vars(self, current): "Return a list of variables in current assignment that are in conflict" @@ -146,6 +152,7 @@ def conflicted_vars(self, current): #______________________________________________________________________________ # Constraint Propagation with AC-3 + def AC3(csp, queue=None, removals=None): """[Fig. 6.3]""" if queue is None: @@ -161,6 +168,7 @@ def AC3(csp, queue=None, removals=None): queue.append((Xk, Xi)) return True + def revise(csp, Xi, Xj, removals): "Return true if we remove a value." revised = False @@ -177,16 +185,19 @@ def revise(csp, Xi, Xj, removals): # Variable ordering + def first_unassigned_variable(assignment, csp): "The default variable order." return find_if(lambda var: var not in assignment, csp.vars) + def mrv(assignment, csp): "Minimum-remaining-values heuristic." return argmin_random_tie( [v for v in csp.vars if v not in assignment], lambda var: num_legal_values(csp, var, assignment)) + def num_legal_values(csp, var, assignment): if csp.curr_domains: return len(csp.curr_domains[var]) @@ -196,10 +207,12 @@ def num_legal_values(csp, var, assignment): # Value ordering + def unordered_domain_values(var, assignment, csp): "The default value order." return csp.choices(var) + def lcv(var, assignment, csp): "Least-constraining-values heuristic." return sorted(csp.choices(var), @@ -207,9 +220,11 @@ def lcv(var, assignment, csp): # Inference + def no_inference(csp, var, value, assignment, removals): return True + def forward_checking(csp, var, value, assignment, removals): "Prune neighbor values inconsistent with var=value." for B in csp.neighbors[var]: @@ -221,16 +236,18 @@ def forward_checking(csp, var, value, assignment, removals): return False return True + def mac(csp, var, value, assignment, removals): "Maintain arc consistency." return AC3(csp, [(X, var) for X in csp.neighbors[var]], removals) # The search, proper + def backtracking_search(csp, - select_unassigned_variable = first_unassigned_variable, - order_domain_values = unordered_domain_values, - inference = no_inference): + select_unassigned_variable=first_unassigned_variable, + order_domain_values=unordered_domain_values, + inference=no_inference): """[Fig. 6.5] >>> backtracking_search(australia) is not None True @@ -271,6 +288,7 @@ def backtrack(assignment): #______________________________________________________________________________ # Min-conflicts hillclimbing search for CSPs + def min_conflicts(csp, max_steps=100000): """Solve a CSP by stochastic hillclimbing on the number of conflicts.""" # Generate a complete assignment for all vars (probably with conflicts) @@ -288,6 +306,7 @@ def min_conflicts(csp, max_steps=100000): csp.assign(var, val, current) return None + def min_conflicts_value(csp, var, current): """Return the value that will give var the least number of conflicts. If there is a tie, choose at random.""" @@ -296,6 +315,7 @@ def min_conflicts_value(csp, var, current): #______________________________________________________________________________ + def tree_csp_solver(csp): "[Fig. 6.11]" n = len(csp.vars) @@ -311,30 +331,39 @@ def tree_csp_solver(csp): assignment[Xi] = csp.curr_domains[Xi][0] return assignment + def topological_sort(xs, x): unimplemented() -def make_arc_consistent(Xj, Xk, csp): + +def make_arc_consistent(Xj, Xk, csp): unimplemented() #______________________________________________________________________________ # Map-Coloring Problems + class UniversalDict: + """A universal dict maps any key to the same value. We use it here as the domains dict for CSPs in which all vars have the same domain. >>> d = UniversalDict(42) >>> d['life'] 42 """ + def __init__(self, value): self.value = value + def __getitem__(self, key): return self.value + def __repr__(self): return '{Any: %r}' % self.value + def different_values_constraint(A, a, B, b): "A constraint saying two neighboring variables must differ in value." return a != b + def MapColoringCSP(colors, neighbors): """Make a CSP for the problem of coloring a map with different colors for any two adjacent regions. Arguments are a list of colors, and a @@ -342,9 +371,10 @@ def MapColoringCSP(colors, neighbors): specified as a string of the form defined by parse_neighbors.""" if isinstance(neighbors, str): neighbors = parse_neighbors(neighbors) - return CSP(neighbors.keys(), UniversalDict(colors), neighbors, + return CSP(list(neighbors.keys()), UniversalDict(colors), neighbors, different_values_constraint) + def parse_neighbors(neighbors, vars=[]): """Convert a string of the form 'X: Y Z; Y: Z' into a dict mapping regions to neighbors. The syntax is a region name followed by a ':' @@ -369,7 +399,7 @@ def parse_neighbors(neighbors, vars=[]): 'SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: ') usa = MapColoringCSP(list('RGBY'), - """WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT; + """WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT; UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX; ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX; TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA; @@ -380,7 +410,7 @@ def parse_neighbors(neighbors, vars=[]): HI: ; AK: """) france = MapColoringCSP(list('RGBY'), - """AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA + """AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR: MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO: @@ -390,12 +420,15 @@ def parse_neighbors(neighbors, vars=[]): #______________________________________________________________________________ # n-Queens Problem + def queen_constraint(A, a, B, b): """Constraint is satisfied (true) if A, B are really the same variable, or if they are not in the same row, down diagonal, or up diagonal.""" return A == B or (a != b and A + a != B + b and A - a != B - b) + class NQueensCSP(CSP): + """Make a CSP for the nQueens problem for search with min_conflicts. Suitable for large n, it uses only data structures of size O(n). Think of placing queens one per column, from left to right. @@ -414,10 +447,11 @@ class NQueensCSP(CSP): >>> len(backtracking_search(NQueensCSP(8))) 8 """ + def __init__(self, n): """Initialize data structures for n Queens.""" - CSP.__init__(self, range(n), UniversalDict(range(n)), - UniversalDict(range(n)), queen_constraint) + CSP.__init__(self, list(range(n)), UniversalDict(list(range(n))), + UniversalDict(list(range(n))), queen_constraint) update(self, rows=[0]*n, ups=[0]*(2*n - 1), downs=[0]*(2*n - 1)) def nconflicts(self, var, val, assignment): @@ -434,7 +468,7 @@ def assign(self, var, val, assignment): "Assign var, and keep track of conflicts." oldval = assignment.get(var, None) if val != oldval: - if oldval is not None: # Remove old val if there was one + if oldval is not None: # Remove old val if there was one self.record_conflict(assignment, var, oldval, -1) self.record_conflict(assignment, var, val, +1) CSP.assign(self, var, val, assignment) @@ -457,28 +491,48 @@ def display(self, assignment): n = len(self.vars) for val in range(n): for var in range(n): - if assignment.get(var,'') == val: ch = 'Q' - elif (var+val) % 2 == 0: ch = '.' - else: ch = '-' - print ch, - print ' ', + if assignment.get(var, '') == val: + ch = 'Q' + elif (var+val) % 2 == 0: + ch = '.' + else: + ch = '-' + print(ch, end=' ') + print(' ', end=' ') for var in range(n): - if assignment.get(var,'') == val: ch = '*' - else: ch = ' ' - print str(self.nconflicts(var, val, assignment))+ch, - print + if assignment.get(var, '') == val: + ch = '*' + else: + ch = ' ' + print(str(self.nconflicts(var, val, assignment))+ch, end=' ') + print() #______________________________________________________________________________ # Sudoku -import itertools, re +import itertools +import re + def flatten(seqs): return sum(seqs, []) -easy1 = '..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3..' +easy1 = '..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3..' harder1 = '4173698.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......' +_R3 = list(range(3)) +_CELL = itertools.count().__next__ +_BGRID = [[[[_CELL() for x in _R3] for y in _R3] for bx in _R3] for by in _R3] +_BOXES = flatten([list(map(flatten, brow)) for brow in _BGRID]) +_ROWS = flatten([list(map(flatten, list(zip(*brow)))) for brow in _BGRID]) +_COLS = list(zip(*_ROWS)) + +_NEIGHBORS = dict([(v, set()) for v in flatten(_ROWS)]) +for unit in map(set, _BOXES + _ROWS + _COLS): + for v in unit: + _NEIGHBORS[v].update(unit - set([v])) + class Sudoku(CSP): + """A Sudoku problem. The box grid is a 3x3 array of boxes, each a 3x3 array of cells. Each cell holds a digit in 1..9. In each box, all digits are @@ -513,17 +567,13 @@ class Sudoku(CSP): >>> None != backtracking_search(h, select_unassigned_variable=mrv, inference=forward_checking) True """ - R3 = range(3) - Cell = itertools.count().next - bgrid = [[[[Cell() for x in R3] for y in R3] for bx in R3] for by in R3] - boxes = flatten([map(flatten, brow) for brow in bgrid]) - rows = flatten([map(flatten, zip(*brow)) for brow in bgrid]) - cols = zip(*rows) - - neighbors = dict([(v, set()) for v in flatten(rows)]) - for unit in map(set, boxes + rows + cols): - for v in unit: - neighbors[v].update(unit - set([v])) + R3 = _R3 + Cell = _CELL + bgrid = _BGRID + boxes = _BOXES + rows = _ROWS + cols = _COLS + neighbors = _NEIGHBORS def __init__(self, grid): """Build a Sudoku problem from a string representing the grid: @@ -533,20 +583,25 @@ def __init__(self, grid): domains = dict((var, ([ch] if ch in '123456789' else '123456789')) for var, ch in zip(flatten(self.rows), squares)) for _ in squares: - raise ValueError("Not a Sudoku grid", grid) # Too many squares + raise ValueError("Not a Sudoku grid", grid) # Too many squares CSP.__init__(self, None, domains, self.neighbors, different_values_constraint) def display(self, assignment): - def show_box(box): return [' '.join(map(show_cell, row)) for row in box] + def show_box(box): return [ + ' '.join(map(show_cell, row)) for row in box] + def show_cell(cell): return str(assignment.get(cell, '.')) - def abut(lines1, lines2): return map(' | '.join, zip(lines1, lines2)) - print '\n------+-------+------\n'.join( - '\n'.join(reduce(abut, map(show_box, brow))) for brow in self.bgrid) + + def abut(lines1, lines2): return list( + map(' | '.join, list(zip(lines1, lines2)))) + print('\n------+-------+------\n'.join( + '\n'.join(reduce(abut, list(map(show_box, brow)))) for brow in self.bgrid)) #______________________________________________________________________________ # The Zebra Puzzle + def Zebra(): "Return an instance of the Zebra Puzzle." Colors = 'Red Yellow Blue Green Ivory'.split() @@ -557,7 +612,7 @@ def Zebra(): vars = Colors + Pets + Drinks + Countries + Smokes domains = {} for var in vars: - domains[var] = range(1, 6) + domains[var] = list(range(1, 6)) domains['Norwegian'] = [1] domains['Milk'] = [3] neighbors = parse_neighbors("""Englishman: Red; @@ -569,46 +624,66 @@ def Zebra(): for A in type: for B in type: if A != B: - if B not in neighbors[A]: neighbors[A].append(B) - if A not in neighbors[B]: neighbors[B].append(A) + if B not in neighbors[A]: + neighbors[A].append(B) + if A not in neighbors[B]: + neighbors[B].append(A) + def zebra_constraint(A, a, B, b, recurse=0): same = (a == b) next_to = abs(a - b) == 1 - if A == 'Englishman' and B == 'Red': return same - if A == 'Spaniard' and B == 'Dog': return same - if A == 'Chesterfields' and B == 'Fox': return next_to - if A == 'Norwegian' and B == 'Blue': return next_to - if A == 'Kools' and B == 'Yellow': return same - if A == 'Winston' and B == 'Snails': return same - if A == 'LuckyStrike' and B == 'OJ': return same - if A == 'Ukranian' and B == 'Tea': return same - if A == 'Japanese' and B == 'Parliaments': return same - if A == 'Kools' and B == 'Horse': return next_to - if A == 'Coffee' and B == 'Green': return same - if A == 'Green' and B == 'Ivory': return (a - 1) == b - if recurse == 0: return zebra_constraint(B, b, A, a, 1) + if A == 'Englishman' and B == 'Red': + return same + if A == 'Spaniard' and B == 'Dog': + return same + if A == 'Chesterfields' and B == 'Fox': + return next_to + if A == 'Norwegian' and B == 'Blue': + return next_to + if A == 'Kools' and B == 'Yellow': + return same + if A == 'Winston' and B == 'Snails': + return same + if A == 'LuckyStrike' and B == 'OJ': + return same + if A == 'Ukranian' and B == 'Tea': + return same + if A == 'Japanese' and B == 'Parliaments': + return same + if A == 'Kools' and B == 'Horse': + return next_to + if A == 'Coffee' and B == 'Green': + return same + if A == 'Green' and B == 'Ivory': + return (a - 1) == b + if recurse == 0: + return zebra_constraint(B, b, A, a, 1) if ((A in Colors and B in Colors) or - (A in Pets and B in Pets) or - (A in Drinks and B in Drinks) or - (A in Countries and B in Countries) or - (A in Smokes and B in Smokes)): return not same - raise 'error' + (A in Pets and B in Pets) or + (A in Drinks and B in Drinks) or + (A in Countries and B in Countries) or + (A in Smokes and B in Smokes)): + return not same + raise Exception('error') return CSP(vars, domains, neighbors, zebra_constraint) + def solve_zebra(algorithm=min_conflicts, **args): z = Zebra() ans = algorithm(z, **args) for h in range(1, 6): - print 'House', h, - for (var, val) in ans.items(): - if val == h: print var, - print + print('House', h, end=' ') + for (var, val) in list(ans.items()): + if val == h: + print(var, end=' ') + print() return ans['Zebra'], ans['Water'], z.nassigns, ans -__doc__ += random_tests(""" +__doc__ += """ +Random tests: >>> min_conflicts(australia) {'WA': 'B', 'Q': 'B', 'T': 'G', 'V': 'B', 'SA': 'R', 'NT': 'G', 'NSW': 'G'} >>> min_conflicts(NQueensCSP(8), max_steps=10000) {0: 5, 1: 0, 2: 4, 3: 1, 4: 7, 5: 2, 6: 6, 7: 3} -""") +""" diff --git a/games.py b/aimaPy/games.py similarity index 89% rename from games.py rename to aimaPy/games.py index d534d3274..14124f2f6 100644 --- a/games.py +++ b/aimaPy/games.py @@ -1,12 +1,14 @@ """Games, or Adversarial Search. (Chapter 5) """ -from utils import * + +from . utils import * import random #______________________________________________________________________________ # Minimax Search + def minimax_decision(state, game): """Given a state in a game, calculate the best move by searching forward all the way to the terminal states. [Fig. 5.3]""" @@ -35,13 +37,14 @@ def min_value(state): #______________________________________________________________________________ + def alphabeta_full_search(state, game): """Search game to determine best action; use alpha-beta pruning. As in [Fig. 5.7], this version searches all the way to the leaves.""" player = game.to_move(state) - #Functions used by alphabeta + # Functions used by alphabeta def max_value(state, alpha, beta): if game.terminal_test(state): return game.utility(state, player) @@ -67,13 +70,14 @@ def min_value(state, alpha, beta): # Body of alphabeta_search: return max_value(state, -infinity, infinity) + def alphabeta_search(state, game, d=4, cutoff_test=None, eval_fn=None): """Search game to determine best action; use alpha-beta pruning. This version cuts off search and uses an evaluation function.""" player = game.to_move(state) - #Functions used by alphabeta + # Functions used by alphabeta def max_value(state, alpha, beta, depth): if cutoff_test(state, depth): return eval_fn(state) @@ -101,25 +105,29 @@ def min_value(state, alpha, beta, depth): # Body of alphabeta_search starts here: # The default test cuts off at depth d or at a terminal state cutoff_test = (cutoff_test or - (lambda state,depth: depth>d or game.terminal_test(state))) + (lambda state, depth: depth > d or game.terminal_test(state))) eval_fn = eval_fn or (lambda state: game.utility(state, player)) return max_value(state, -infinity, infinity, 0) #______________________________________________________________________________ # Players for Games + def query_player(game, state): "Make a move by querying standard input." game.display(state) - return num_or_str(raw_input('Your move? ')) + return num_or_str(eval(input('Your move? '))) + def random_player(game, state): "A player that chooses a legal move at random." return random.choice(game.actions(state)) + def alphabeta_player(game, state): return alphabeta_search(state, game) + def play_game(game, *players): """Play an n-person, move-alternating game. >>> play_game(Fig52Game(), alphabeta_player, alphabeta_player) @@ -136,7 +144,9 @@ def play_game(game, *players): #______________________________________________________________________________ # Some Sample Games + class Game: + """A game is similar to a problem, but it has a utility for each state and a terminal test instead of a path cost and a goal test. To create a game, subclass this class and implement actions, @@ -147,15 +157,15 @@ class Game: def actions(self, state): "Return a list of the allowable moves at this point." - raise NotImplementedError + raise NotImplementedError def result(self, state, move): "Return the state that results from making a move from a state." - raise NotImplementedError + raise NotImplementedError def utility(self, state, player): "Return the value of this final state to player." - raise NotImplementedError + raise NotImplementedError def terminal_test(self, state): "Return True if this is a final state for the game." @@ -167,12 +177,14 @@ def to_move(self, state): def display(self, state): "Print or otherwise display the state." - print state + print(state) def __repr__(self): return '<%s>' % self.__class__.__name__ + class Fig52Game(Game): + """The game represented in [Fig. 5.2]. Serves as a simple test case. >>> g = Fig52Game() >>> minimax_decision('A', g) @@ -190,7 +202,7 @@ class Fig52Game(Game): initial = 'A' def actions(self, state): - return self.succs.get(state, {}).keys() + return list(self.succs.get(state, {}).keys()) def result(self, state, move): return self.succs[state][move] @@ -207,11 +219,14 @@ def terminal_test(self, state): def to_move(self, state): return ('MIN' if state in 'BCD' else 'MAX') + class TicTacToe(Game): + """Play TicTacToe on an h x v board, with Max (first player) playing 'X'. A state has the player to move, a cached utility, a list of moves in the form of a list of (x, y) positions, and a board, in the form of a dict of {(x, y): Player} entries, where Player is 'X' or 'O'.""" + def __init__(self, h=3, v=3, k=3): update(self, h=h, v=v, k=k) moves = [(x, y) for x in range(1, h+1) @@ -224,9 +239,11 @@ def actions(self, state): def result(self, state, move): if move not in state.moves: - return state # Illegal move has no effect - board = state.board.copy(); board[move] = state.to_move - moves = list(state.moves); moves.remove(move) + return state # Illegal move has no effect + board = state.board.copy() + board[move] = state.to_move + moves = list(state.moves) + moves.remove(move) return Struct(to_move=('O' if state.to_move == 'X' else 'X'), utility=self.compute_utility(board, move, state.to_move), board=board, moves=moves) @@ -243,23 +260,24 @@ def display(self, state): board = state.board for x in range(1, self.h+1): for y in range(1, self.v+1): - print board.get((x, y), '.'), - print + print(board.get((x, y), '.'), end=' ') + print() def compute_utility(self, board, move, player): "If X wins with this move, return 1; if O return -1; else return 0." if (self.k_in_row(board, move, player, (0, 1)) or - self.k_in_row(board, move, player, (1, 0)) or - self.k_in_row(board, move, player, (1, -1)) or - self.k_in_row(board, move, player, (1, 1))): + self.k_in_row(board, move, player, (1, 0)) or + self.k_in_row(board, move, player, (1, -1)) or + self.k_in_row(board, move, player, (1, 1))): return (+1 if player == 'X' else -1) else: return 0 - def k_in_row(self, board, move, player, (delta_x, delta_y)): + def k_in_row(self, board, move, player, xxx_todo_changeme): "Return true if there is a line through move on board for player." + (delta_x, delta_y) = xxx_todo_changeme x, y = move - n = 0 # n is number of moves in row + n = 0 # n is number of moves in row while board.get((x, y)) == player: n += 1 x, y = x + delta_x, y + delta_y @@ -267,10 +285,12 @@ def k_in_row(self, board, move, player, (delta_x, delta_y)): while board.get((x, y)) == player: n += 1 x, y = x - delta_x, y - delta_y - n -= 1 # Because we counted move itself twice + n -= 1 # Because we counted move itself twice return n >= self.k + class ConnectFour(TicTacToe): + """A TicTacToe-like game in which you can only make a move on the bottom row, or in a square directly above an occupied square. Traditionally played on a 7x6 board and requiring 4 in a row.""" @@ -282,9 +302,10 @@ def actions(self, state): return [(x, y) for (x, y) in state.moves if y == 1 or (x, y-1) in state.board] -__doc__ += random_tests(""" +__doc__ += """ +Random tests: >>> play_game(Fig52Game(), random_player, random_player) 6 >>> play_game(TicTacToe(), random_player, random_player) 0 -""") +""" diff --git a/grid.py b/aimaPy/grid.py similarity index 75% rename from grid.py rename to aimaPy/grid.py index 278ae2d7b..45bce40f3 100644 --- a/grid.py +++ b/aimaPy/grid.py @@ -1,21 +1,25 @@ -## OK, the following are not as widely useful utilities as some of the other -## functions here, but they do show up wherever we have 2D grids: Wumpus and -## Vacuum worlds, TicTacToe and Checkers, and markov decision Processes. -##__________________________________________________________________________ +# OK, the following are not as widely useful utilities as some of the other +# functions here, but they do show up wherever we have 2D grids: Wumpus and +# Vacuum worlds, TicTacToe and Checkers, and markov decision Processes. +# __________________________________________________________________________ import math orientations = [(1, 0), (0, 1), (-1, 0), (0, -1)] + def turn_heading(heading, inc, headings=orientations): return headings[(headings.index(heading) + inc) % len(headings)] + def turn_right(heading): return turn_heading(heading, -1) + def turn_left(heading): return turn_heading(heading, +1) + def distance(a, b): """The distance between two (x, y) points. >>> distance((1,2),(5,5)) @@ -23,17 +27,20 @@ def distance(a, b): """ return math.hypot((a[0] - b[0]), (a[1] - b[1])) + def distance_squared(a, b): """The square of the distance between two (x, y) points. >>> distance_squared((1,2),(5,5)) 25.0 """ - return (a[0]- b[0])**2 + (a[1] - b[1])**2 + return (a[0] - b[0])**2 + (a[1] - b[1])**2 + def distance2(a, b): "The square of the distance between two (x, y) points." return distance_squared(a, b) - + + def clip(x, lowest, highest): """Return x clipped to the range [lowest..highest]. >>> [clip(x, 0, 1) for x in [-1, 0.5, 10]] @@ -49,4 +56,4 @@ def vector_clip(vector, lowest, highest): >>> vector_clip((-1, 10), (0, 0), (9, 9)) (0, 9) """ - return type(vector)(map(clip, vector, lowest, highest)) + return type(vector)(list(map(clip, vector, lowest, highest))) diff --git a/images/IMAGE-CREDITS b/aimaPy/images/IMAGE-CREDITS similarity index 100% rename from images/IMAGE-CREDITS rename to aimaPy/images/IMAGE-CREDITS diff --git a/images/dirt.svg b/aimaPy/images/dirt.svg similarity index 100% rename from images/dirt.svg rename to aimaPy/images/dirt.svg diff --git a/images/dirt05-icon.jpg b/aimaPy/images/dirt05-icon.jpg similarity index 100% rename from images/dirt05-icon.jpg rename to aimaPy/images/dirt05-icon.jpg diff --git a/images/makefile b/aimaPy/images/makefile similarity index 100% rename from images/makefile rename to aimaPy/images/makefile diff --git a/images/vacuum-icon.jpg b/aimaPy/images/vacuum-icon.jpg similarity index 100% rename from images/vacuum-icon.jpg rename to aimaPy/images/vacuum-icon.jpg diff --git a/images/vacuum.svg b/aimaPy/images/vacuum.svg similarity index 100% rename from images/vacuum.svg rename to aimaPy/images/vacuum.svg diff --git a/images/wall-icon.jpg b/aimaPy/images/wall-icon.jpg similarity index 100% rename from images/wall-icon.jpg rename to aimaPy/images/wall-icon.jpg diff --git a/learning.py b/aimaPy/learning.py similarity index 88% rename from learning.py rename to aimaPy/learning.py index a98937435..0417b78e7 100644 --- a/learning.py +++ b/aimaPy/learning.py @@ -1,26 +1,36 @@ """Learn to estimate functions from examples. (Chapters 18-20)""" -from utils import * -import copy, heapq, math, random + +from . utils import * +import copy +import heapq +import math +import random from collections import defaultdict #______________________________________________________________________________ + def rms_error(predictions, targets): return math.sqrt(ms_error(predictions, targets)) + def ms_error(predictions, targets): return mean([(p - t)**2 for p, t in zip(predictions, targets)]) + def mean_error(predictions, targets): return mean([abs(p - t) for p, t in zip(predictions, targets)]) + def mean_boolean_error(predictions, targets): - return mean([(p != t) for p, t in zip(predictions, targets)]) + return mean([(p != t) for p, t in zip(predictions, targets)]) #______________________________________________________________________________ + class DataSet: + """A data set for a machine learning problem. It has the following fields: d.examples A list of examples. Each one is a list of attribute values. @@ -52,7 +62,8 @@ def __init__(self, examples=None, attrs=None, attrnames=None, target=-1, >>> DataSet(examples='1, 2, 3') """ - update(self, name=name, source=source, values=values, distance=distance) + update(self, name=name, source=source, + values=values, distance=distance) # Initialize .examples from string or list or data directory if isinstance(examples, str): self.examples = parse_csv(examples) @@ -62,7 +73,7 @@ def __init__(self, examples=None, attrs=None, attrnames=None, target=-1, self.examples = examples # Attrs are the indices of examples, unless otherwise stated. if not attrs and self.examples: - attrs = range(len(self.examples[0])) + attrs = list(range(len(self.examples[0]))) self.attrs = attrs # Initialize .attrnames from string, list, or by default if isinstance(attrnames, str): @@ -78,14 +89,14 @@ def setproblem(self, target, inputs=None, exclude=()): to not use in inputs. Attributes can be -n .. n, or an attrname. Also computes the list of possible values, if that wasn't done yet.""" self.target = self.attrnum(target) - exclude = map(self.attrnum, exclude) + exclude = list(map(self.attrnum, exclude)) if inputs: self.inputs = removeall(self.target, inputs) else: self.inputs = [a for a in self.attrs if a != self.target and a not in exclude] if not self.values: - self.values = map(unique, zip(*self.examples)) + self.values = list(map(unique, list(zip(*self.examples)))) self.check_me() def check_me(self): @@ -94,7 +105,7 @@ def check_me(self): assert self.target in self.attrs assert self.target not in self.inputs assert set(self.inputs).issubset(set(self.attrs)) - map(self.check_example, self.examples) + list(map(self.check_example, self.examples)) def add_example(self, example): "Add an example to the list of examples, checking it first." @@ -111,17 +122,17 @@ def check_example(self, example): def attrnum(self, attr): "Returns the number used for attr, which can be a name, or -n .. n-1." - if attr < 0: - return len(self.attrs) + attr - elif isinstance(attr, str): + if isinstance(attr, str): return self.attrnames.index(attr) + elif attr < 0: + return len(self.attrs) + attr else: return attr def sanitize(self, example): - "Return a copy of example, with non-input attributes replaced by None." - return [attr_i if i in self.inputs else None - for i, attr_i in enumerate(example)] + "Return a copy of example, with non-input attributes replaced by None." + return [attr_i if i in self.inputs else None + for i, attr_i in enumerate(example)] def __repr__(self): return '' % ( @@ -129,6 +140,7 @@ def __repr__(self): #______________________________________________________________________________ + def parse_csv(input, delim=','): r"""Input is a string consisting of lines, each line has comma-delimited fields. Convert this into a list of lists. Blank lines are skipped. @@ -138,11 +150,13 @@ def parse_csv(input, delim=','): [[1, 2, 3], [0, 2, 'na']] """ lines = [line for line in input.splitlines() if line.strip()] - return [map(num_or_str, line.split(delim)) for line in lines] + return [list(map(num_or_str, line.split(delim))) for line in lines] #______________________________________________________________________________ + class CountingProbDist: + """A probability distribution formed by observing and counting examples. If p is an instance of this class and o is an observed value, then there are 3 main operations: @@ -182,21 +196,23 @@ def __getitem__(self, item): def top(self, n): "Return (count, obs) tuples for the n most frequent observations." - return heapq.nlargest(n, [(v, k) for (k, v) in self.dictionary.items()]) + return heapq.nlargest(n, [(v, k) for (k, v) in list(self.dictionary.items())]) def sample(self): "Return a random sample from the distribution." if self.sampler is None: - self.sampler = weighted_sampler(self.dictionary.keys(), - self.dictionary.values()) + self.sampler = weighted_sampler(list(self.dictionary.keys()), + list(self.dictionary.values())) return self.sampler() #______________________________________________________________________________ + def PluralityLearner(dataset): """A very dumb algorithm: always pick the result that was most popular in the training data. Makes a baseline for comparison.""" most_popular = mode([e[dataset.target] for e in dataset.examples]) + def predict(example): "Always return same result: the most popular from the training set." return most_popular @@ -204,6 +220,7 @@ def predict(example): #______________________________________________________________________________ + def NaiveBayesLearner(dataset): """Just count how many times each value of each input attribute occurs, conditional on the target value. Count the different @@ -233,6 +250,7 @@ def class_probability(targetval): #______________________________________________________________________________ + def NearestNeighborLearner(dataset, k=1): "k-NearestNeighbor: the k nearest neighbors vote." def predict(example): @@ -244,7 +262,9 @@ def predict(example): #______________________________________________________________________________ + class DecisionFork: + """A fork of a decision tree holds an attribute to test, and a dict of branches, one for each of the attribute's values.""" @@ -264,16 +284,18 @@ def add(self, val, subtree): def display(self, indent=0): name = self.attrname - print 'Test', name - for (val, subtree) in self.branches.items(): - print ' '*4*indent, name, '=', val, '==>', + print('Test', name) + for (val, subtree) in list(self.branches.items()): + print(' '*4*indent, name, '=', val, '==>', end=' ') subtree.display(indent+1) def __repr__(self): return ('DecisionFork(%r, %r, %r)' % (self.attr, self.attrname, self.branches)) - + + class DecisionLeaf: + "A leaf of a decision tree holds just a result." def __init__(self, result): @@ -283,13 +305,14 @@ def __call__(self, example): return self.result def display(self, indent=0): - print 'RESULT =', self.result + print('RESULT =', self.result) def __repr__(self): return repr(self.result) - + #______________________________________________________________________________ + def DecisionTreeLearner(dataset): "[Fig. 18.5]" @@ -348,6 +371,7 @@ def split_by(attr, examples): return decision_tree_learning(dataset.examples, dataset.inputs) + def information_content(values): "Number of bits to represent the probability distribution in values." probabilities = normalize(removeall(0, values)) @@ -355,7 +379,8 @@ def information_content(values): #______________________________________________________________________________ -### A decision list is implemented as a list of (test, value) pairs. +# A decision list is implemented as a list of (test, value) pairs. + def DecisionListLearner(dataset): """[Fig. 18.11]""" @@ -388,37 +413,45 @@ def predict(example): #______________________________________________________________________________ + def NeuralNetLearner(dataset, sizes): - """Layered feed-forward network.""" + """Layered feed-forward network.""" - activations = map(lambda n: [0.0 for i in range(n)], sizes) - weights = [] + activations = [[0.0 for i in range(n)] for n in sizes] + weights = [] - def predict(example): - unimplemented() + def predict(example): + unimplemented() + + return predict - return predict class NNUnit: - """Unit of a neural net.""" - def __init__(self): - unimplemented() + + """Unit of a neural net.""" + + def __init__(self): + unimplemented() + def PerceptronLearner(dataset, sizes): - def predict(example): - return sum([]) - unimplemented() + def predict(example): + return sum([]) + unimplemented() #______________________________________________________________________________ + def Linearlearner(dataset): - """Fit a linear model to the data.""" - unimplemented() + """Fit a linear model to the data.""" + unimplemented() #______________________________________________________________________________ + def EnsembleLearner(learners): """Given a list of learning algorithms, have them vote.""" def train(dataset): predictors = [learner(dataset) for learner in learners] + def predict(example): return mode(predictor(example) for predictor in predictors) return predict @@ -426,6 +459,7 @@ def predict(example): #______________________________________________________________________________ + def AdaBoost(L, K): """[Fig. 18.34]""" def train(dataset): @@ -449,6 +483,7 @@ def train(dataset): return WeightedMajority(h, z) return train + def WeightedMajority(predictors, weights): "Return a predictor that takes a weighted vote." def predict(example): @@ -456,6 +491,7 @@ def predict(example): weights) return predict + def weighted_mode(values, weights): """Return the value with the greatest total weight. >>> weighted_mode('abbaa', [1,2,3,1,2]) @@ -463,11 +499,12 @@ def weighted_mode(values, weights): totals = defaultdict(int) for v, w in zip(values, weights): totals[v] += w - return max(totals.keys(), key=totals.get) + return max(list(totals.keys()), key=totals.get) #_____________________________________________________________________________ # Adapting an unweighted learner for AdaBoost + def WeightedLearner(unweighted_learner): """Given a learner that takes just an unweighted dataset, return one that takes also a weight for each example. [p. 749 footnote 14]""" @@ -475,6 +512,7 @@ def train(dataset, weights): return unweighted_learner(replicated_dataset(dataset, weights)) return train + def replicated_dataset(dataset, weights, n=None): "Copy dataset, replicating each example in proportion to its weight." n = n or len(dataset.examples) @@ -482,6 +520,7 @@ def replicated_dataset(dataset, weights, n=None): result.examples = weighted_replicate(dataset.examples, weights, n) return result + def weighted_replicate(seq, weights, n): """Return n selections from seq, with the count of each element of seq proportional to the corresponding weight (filling in fractions @@ -495,15 +534,19 @@ def weighted_replicate(seq, weights, n): return (flatten([x] * nx for x, nx in zip(seq, wholes)) + weighted_sample_with_replacement(seq, fractions, n - sum(wholes))) + def flatten(seqs): return sum(seqs, []) #_____________________________________________________________________________ # Functions for testing learners on examples + def test(predict, dataset, examples=None, verbose=0): "Return the proportion of the examples that are correctly predicted." - if examples is None: examples = dataset.examples - if len(examples) == 0: return 0.0 + if examples is None: + examples = dataset.examples + if len(examples) == 0: + return 0.0 right = 0.0 for example in examples: desired = example[dataset.target] @@ -511,12 +554,13 @@ def test(predict, dataset, examples=None, verbose=0): if output == desired: right += 1 if verbose >= 2: - print ' OK: got %s for %s' % (desired, example) + print(' OK: got %s for %s' % (desired, example)) elif verbose: - print 'WRONG: got %s, expected %s for %s' % ( - output, desired, example) + print('WRONG: got %s, expected %s for %s' % ( + output, desired, example)) return right / len(examples) + def train_and_test(learner, dataset, start, end): """Reserve dataset.examples[start:end] for test; train on the remainder. Return the proportion of examples correct on the test examples.""" @@ -527,6 +571,7 @@ def train_and_test(learner, dataset, start, end): finally: dataset.examples = examples + def cross_validation(learner, dataset, k=10, trials=1): """Do k-fold cross_validate and return their mean. That is, keep out 1/k of the examples for testing on each of k runs. @@ -542,13 +587,16 @@ def cross_validation(learner, dataset, k=10, trials=1): return mean([train_and_test(learner, dataset, i*(n/k), (i+1)*(n/k)) for i in range(k)]) + def leave1out(learner, dataset): "Leave one out cross-validation over the dataset." return cross_validation(learner, dataset, k=len(dataset.examples)) + def learningcurve(learner, dataset, trials=10, sizes=None): if sizes is None: - sizes = range(2, len(dataset.examples)-10, 2) + sizes = list(range(2, len(dataset.examples)-10, 2)) + def score(learner, size): random.shuffle(dataset.examples) return train_and_test(learner, dataset, 0, size) @@ -574,36 +622,38 @@ def score(learner, size): #______________________________________________________________________________ # The Restaurant example from Fig. 18.2 + def RestaurantDataSet(examples=None): "Build a DataSet of Restaurant waiting examples. [Fig. 18.3]" return DataSet(name='restaurant', target='Wait', examples=examples, attrnames='Alternate Bar Fri/Sat Hungry Patrons Price ' - + 'Raining Reservation Type WaitEstimate Wait') + + 'Raining Reservation Type WaitEstimate Wait') restaurant = RestaurantDataSet() + def T(attrname, branches): branches = dict((value, (child if isinstance(child, DecisionFork) else DecisionLeaf(child))) - for value, child in branches.items()) + for value, child in list(branches.items())) return DecisionFork(restaurant.attrnum(attrname), attrname, branches) -Fig[18,2] = T('Patrons', - {'None': 'No', 'Some': 'Yes', 'Full': - T('WaitEstimate', - {'>60': 'No', '0-10': 'Yes', - '30-60': - T('Alternate', {'No': - T('Reservation', {'Yes': 'Yes', 'No': - T('Bar', {'No':'No', - 'Yes':'Yes'})}), - 'Yes': - T('Fri/Sat', {'No': 'No', 'Yes': 'Yes'})}), - '10-30': - T('Hungry', {'No': 'Yes', 'Yes': - T('Alternate', - {'No': 'Yes', 'Yes': - T('Raining', {'No': 'No', 'Yes': 'Yes'})})})})}) +Fig[18, 2] = T('Patrons', + {'None': 'No', 'Some': 'Yes', 'Full': + T('WaitEstimate', + {'>60': 'No', '0-10': 'Yes', + '30-60': + T('Alternate', {'No': + T('Reservation', {'Yes': 'Yes', 'No': + T('Bar', {'No': 'No', + 'Yes': 'Yes'})}), + 'Yes': + T('Fri/Sat', {'No': 'No', 'Yes': 'Yes'})}), + '10-30': + T('Hungry', {'No': 'Yes', 'Yes': + T('Alternate', + {'No': 'Yes', 'Yes': + T('Raining', {'No': 'No', 'Yes': 'Yes'})})})})}) __doc__ += """ [Fig. 18.6] @@ -624,17 +674,19 @@ def T(attrname, branches): Patrons = Some ==> RESULT = Yes """ + def SyntheticRestaurant(n=20): "Generate a DataSet with n examples." def gen(): - example = map(random.choice, restaurant.values) - example[restaurant.target] = Fig[18,2](example) + example = list(map(random.choice, restaurant.values)) + example[restaurant.target] = Fig[18, 2](example) return example return RestaurantDataSet([gen() for i in range(n)]) #______________________________________________________________________________ # Artificial, generated datasets. + def Majority(k, n): """Return a DataSet with n k-bit examples of the majority problem: k random bits followed by a 1 if more than half the bits are 1, else 0.""" @@ -645,6 +697,7 @@ def Majority(k, n): examples.append(bits) return DataSet(name="majority", examples=examples) + def Parity(k, n, name="parity"): """Return a DataSet with n k-bit examples of the parity problem: k random bits followed by a 1 if an odd number of bits are 1, else 0.""" @@ -655,10 +708,12 @@ def Parity(k, n, name="parity"): examples.append(bits) return DataSet(name=name, examples=examples) + def Xor(n): """Return a DataSet with n examples of 2-input xor.""" return Parity(2, n, name="xor") + def ContinuousXor(n): "2 inputs are chosen uniformly from (0.0 .. 2.0]; output is xor of ints." examples = [] @@ -669,6 +724,7 @@ def ContinuousXor(n): #______________________________________________________________________________ + def compare(algorithms=[PluralityLearner, NaiveBayesLearner, NearestNeighborLearner, DecisionTreeLearner], datasets=[iris, orings, zoo, restaurant, SyntheticRestaurant(20), @@ -676,7 +732,7 @@ def compare(algorithms=[PluralityLearner, NaiveBayesLearner, k=10, trials=1): """Compare various learners on various datasets using cross-validation. Print results as a table.""" - print_table([[a.__name__.replace('Learner','')] + + print_table([[a.__name__.replace('Learner', '')] + [cross_validation(a, d, k, trials) for d in datasets] for a in algorithms], header=[''] + [d.name[0:7] for d in datasets], numfmt='%.2f') diff --git a/logic.py b/aimaPy/logic.py similarity index 81% rename from logic.py rename to aimaPy/logic.py index 59a212def..8981d6218 100644 --- a/logic.py +++ b/aimaPy/logic.py @@ -26,13 +26,15 @@ import itertools import re -import agents -from utils import * +from . import agents +from . utils import * from collections import defaultdict #______________________________________________________________________________ + class KB: + """A knowledge base to which you can tell and ask sentences. To create a KB, first subclass this class and implement tell, ask_generator, and retract. Why ask_generator instead of ask? @@ -67,6 +69,7 @@ def retract(self, sentence): class PropKB(KB): + "A KB for propositional logic. Inefficient, with no indexing." def __init__(self, sentence=None): @@ -91,12 +94,13 @@ def retract(self, sentence): #______________________________________________________________________________ + def KB_AgentProgram(KB): """A generic logical knowledge-based agent program. [Fig. 7.1]""" steps = itertools.count() def program(percept): - t = steps.next() + t = next(steps) KB.tell(make_percept_sentence(percept, t)) action = KB.ask(make_action_query(t)) KB.tell(make_action_sentence(action, t)) @@ -115,7 +119,9 @@ def make_action_sentence(self, action, t): #______________________________________________________________________________ + class Expr: + """A symbolic mathematical expression. We use this class for logical expressions, and for terms within logical expressions. In general, an Expr has an op (operator) and a list of args. The op can be: @@ -165,7 +171,7 @@ def __init__(self, op, *args): "Op is a string or number; args are Exprs (or are coerced to Exprs)." assert isinstance(op, str) or (isnumber(op) and not args) self.op = num_or_str(op) - self.args = map(expr, args) ## Coerce args to Exprs + self.args = list(map(expr, args)) # Coerce args to Exprs def __call__(self, *args): """Self must be a symbol with no args, such as Expr('F'). Create a new @@ -179,7 +185,7 @@ def __repr__(self): return str(self.op) elif is_symbol(self.op): # Functional or propositional operator return '%s(%s)' % (self.op, ', '.join(map(repr, self.args))) - elif len(self.args) == 1: # Prefix operator + elif len(self.args) == 1: # Prefix operator return self.op + repr(self.args[0]) else: # Infix operator return '(%s)' % (' '+self.op+' ').join(map(repr, self.args)) @@ -187,7 +193,7 @@ def __repr__(self): def __eq__(self, other): """x and y are equal iff their ops and args are equal.""" return (other is self) or (isinstance(other, Expr) - and self.op == other.op and self.args == other.args) + and self.op == other.op and self.args == other.args) def __ne__(self, other): return not self.__eq__(other) @@ -198,25 +204,41 @@ def __hash__(self): # See http://www.python.org/doc/current/lib/module-operator.html # Not implemented: not, abs, pos, concat, contains, *item, *slice - def __lt__(self, other): return Expr('<', self, other) - def __le__(self, other): return Expr('<=', self, other) - def __ge__(self, other): return Expr('>=', self, other) - def __gt__(self, other): return Expr('>', self, other) - def __add__(self, other): return Expr('+', self, other) - def __sub__(self, other): return Expr('-', self, other) - def __and__(self, other): return Expr('&', self, other) - def __div__(self, other): return Expr('/', self, other) - def __truediv__(self, other):return Expr('/', self, other) - def __invert__(self): return Expr('~', self) + def __lt__(self, other): return Expr('<', self, other) + + def __le__(self, other): return Expr('<=', self, other) + + def __ge__(self, other): return Expr('>=', self, other) + + def __gt__(self, other): return Expr('>', self, other) + + def __add__(self, other): return Expr('+', self, other) + + def __sub__(self, other): return Expr('-', self, other) + + def __and__(self, other): return Expr('&', self, other) + + def __div__(self, other): return Expr('/', self, other) + + def __truediv__(self, other): return Expr('/', self, other) + + def __invert__(self): return Expr('~', self) + def __lshift__(self, other): return Expr('<<', self, other) + def __rshift__(self, other): return Expr('>>', self, other) - def __mul__(self, other): return Expr('*', self, other) - def __neg__(self): return Expr('-', self) - def __or__(self, other): return Expr('|', self, other) - def __pow__(self, other): return Expr('**', self, other) - def __xor__(self, other): return Expr('^', self, other) - def __mod__(self, other): return Expr('<=>', self, other) + def __mul__(self, other): return Expr('*', self, other) + + def __neg__(self): return Expr('-', self) + + def __or__(self, other): return Expr('|', self, other) + + def __pow__(self, other): return Expr('**', self, other) + + def __xor__(self, other): return Expr('^', self, other) + + def __mod__(self, other): return Expr('<=>', self, other) def expr(s): @@ -234,29 +256,35 @@ def expr(s): >>> expr('P & Q | ~R(x, F(x))') ((P & Q) | ~R(x, F(x))) """ - if isinstance(s, Expr): return s - if isnumber(s): return Expr(s) - ## Replace the alternative spellings of operators with canonical spellings + if isinstance(s, Expr): + return s + if isnumber(s): + return Expr(s) + # Replace the alternative spellings of operators with canonical spellings s = s.replace('==>', '>>').replace('<==', '<<') s = s.replace('<=>', '%').replace('=/=', '^') - ## Replace a symbol or number, such as 'P' with 'Expr("P")' + # Replace a symbol or number, such as 'P' with 'Expr("P")' s = re.sub(r'([a-zA-Z0-9_.]+)', r'Expr("\1")', s) - ## Now eval the string. (A security hole; do not use with an adversary.) - return eval(s, {'Expr':Expr}) + # Now eval the string. (A security hole; do not use with an adversary.) + return eval(s, {'Expr': Expr}) + def is_symbol(s): "A string s is a symbol if it starts with an alphabetic char." return isinstance(s, str) and s[:1].isalpha() + def is_var_symbol(s): "A logic variable symbol is an initial-lowercase string." return is_symbol(s) and s[0].islower() + def is_prop_symbol(s): """A proposition logic symbol is an initial-uppercase string other than TRUE or FALSE.""" return is_symbol(s) and s[0].isupper() and s != 'TRUE' and s != 'FALSE' + def variables(s): """Return a set of the variables in expression s. >>> ppset(variables(F(x, A, y))) @@ -267,6 +295,7 @@ def variables(s): set([x, y, z]) """ result = set([]) + def walk(s): if is_variable(s): result.add(s) @@ -276,6 +305,7 @@ def walk(s): walk(s) return result + def is_definite_clause(s): """returns True for exprs s of the form A & B & ... & C ==> D, where all literals are positive. In clause form, this is @@ -300,6 +330,7 @@ def is_definite_clause(s): else: return False + def parse_definite_clause(s): "Return the antecedents and the consequent of a definite clause." assert is_definite_clause(s) @@ -309,12 +340,13 @@ def parse_definite_clause(s): antecedent, consequent = s.args return conjuncts(antecedent), consequent -## Useful constant Exprs used in examples and code: -TRUE, FALSE, ZERO, ONE, TWO = map(Expr, ['TRUE', 'FALSE', 0, 1, 2]) -A, B, C, D, E, F, G, P, Q, x, y, z = map(Expr, 'ABCDEFGPQxyz') +# Useful constant Exprs used in examples and code: +TRUE, FALSE, ZERO, ONE, TWO = list(map(Expr, ['TRUE', 'FALSE', 0, 1, 2])) +A, B, C, D, E, F, G, P, Q, x, y, z = list(map(Expr, 'ABCDEFGPQxyz')) #______________________________________________________________________________ + def tt_entails(kb, alpha): """Does kb entail the sentence alpha? Use truth tables. For propositional kb's and sentences. [Fig. 7.10] @@ -324,6 +356,7 @@ def tt_entails(kb, alpha): assert not variables(alpha) return tt_check_all(kb, alpha, prop_symbols(kb & alpha), {}) + def tt_check_all(kb, alpha, symbols, model): "Auxiliary routine to implement tt_entails." if not symbols: @@ -338,6 +371,7 @@ def tt_check_all(kb, alpha, symbols, model): return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and tt_check_all(kb, alpha, rest, extend(model, P, False))) + def prop_symbols(x): "Return a list of all propositional symbols in x." if not isinstance(x, Expr): @@ -348,6 +382,7 @@ def prop_symbols(x): return list(set(symbol for arg in x.args for symbol in prop_symbols(arg))) + def tt_true(alpha): """Is the propositional sentence alpha a tautology? (alpha will be coerced to an expr.) @@ -356,6 +391,7 @@ def tt_true(alpha): """ return tt_entails(TRUE, expr(alpha)) + def pl_true(exp, model={}): """Return True if the propositional logic expression is true in the model, and False if it is false. If the model does not specify the value for @@ -370,21 +406,27 @@ def pl_true(exp, model={}): return model.get(exp) elif op == '~': p = pl_true(args[0], model) - if p is None: return None - else: return not p + if p is None: + return None + else: + return not p elif op == '|': result = False for arg in args: p = pl_true(arg, model) - if p is True: return True - if p is None: result = None + if p is True: + return True + if p is None: + result = None return result elif op == '&': result = True for arg in args: p = pl_true(arg, model) - if p is False: return False - if p is None: result = None + if p is False: + return False + if p is None: + result = None return result p, q = args if op == '>>': @@ -392,9 +434,11 @@ def pl_true(exp, model={}): elif op == '<<': return pl_true(p | ~q, model) pt = pl_true(p, model) - if pt is None: return None + if pt is None: + return None qt = pl_true(q, model) - if qt is None: return None + if qt is None: + return None if op == '<=>': return pt == qt elif op == '^': @@ -404,7 +448,8 @@ def pl_true(exp, model={}): #______________________________________________________________________________ -## Convert to Conjunctive Normal Form (CNF) +# Convert to Conjunctive Normal Form (CNF) + def to_cnf(s): """Convert a propositional logical sentence s to conjunctive normal form. @@ -420,10 +465,12 @@ def to_cnf(s): >>> to_cnf("A | (B | (C | (D & E)))") ((D | A | B | C) & (E | A | B | C)) """ - if isinstance(s, str): s = expr(s) - s = eliminate_implications(s) # Steps 1, 2 from p. 253 - s = move_not_inwards(s) # Step 3 - return distribute_and_over_or(s) # Step 4 + if isinstance(s, str): + s = expr(s) + s = eliminate_implications(s) # Steps 1, 2 from p. 253 + s = move_not_inwards(s) # Step 3 + return distribute_and_over_or(s) # Step 4 + def eliminate_implications(s): """Change >>, <<, and <=> into &, |, and ~. That is, return an Expr @@ -433,8 +480,9 @@ def eliminate_implications(s): >>> eliminate_implications(A ^ B) ((A & ~B) | (~A & B)) """ - if not s.args or is_symbol(s.op): return s ## (Atoms are unchanged.) - args = map(eliminate_implications, s.args) + if not s.args or is_symbol(s.op): + return s # (Atoms are unchanged.) + args = list(map(eliminate_implications, s.args)) a, b = args[0], args[-1] if s.op == '>>': return (b | ~a) @@ -443,12 +491,13 @@ def eliminate_implications(s): elif s.op == '<=>': return (a | ~b) & (b | ~a) elif s.op == '^': - assert len(args) == 2 ## TODO: relax this restriction + assert len(args) == 2 # TODO: relax this restriction return (a & ~b) | (~a & b) else: assert s.op in ('&', '|', '~') return Expr(s.op, *args) + def move_not_inwards(s): """Rewrite sentence s by moving negation sign inward. >>> move_not_inwards(~(A | B)) @@ -461,14 +510,18 @@ def move_not_inwards(s): if s.op == '~': NOT = lambda b: move_not_inwards(~b) a = s.args[0] - if a.op == '~': return move_not_inwards(a.args[0]) # ~~A ==> A - if a.op =='&': return associate('|', map(NOT, a.args)) - if a.op =='|': return associate('&', map(NOT, a.args)) + if a.op == '~': + return move_not_inwards(a.args[0]) # ~~A ==> A + if a.op == '&': + return associate('|', list(map(NOT, a.args))) + if a.op == '|': + return associate('&', list(map(NOT, a.args))) return s elif is_symbol(s.op) or not s.args: return s else: - return Expr(s.op, *map(move_not_inwards, s.args)) + return Expr(s.op, *list(map(move_not_inwards, s.args))) + def distribute_and_over_or(s): """Given a sentence s consisting of conjunctions and disjunctions @@ -489,13 +542,14 @@ def distribute_and_over_or(s): return s others = [a for a in s.args if a is not conj] rest = associate('|', others) - return associate('&', [distribute_and_over_or(c|rest) + return associate('&', [distribute_and_over_or(c | rest) for c in conj.args]) elif s.op == '&': - return associate('&', map(distribute_and_over_or, s.args)) + return associate('&', list(map(distribute_and_over_or, s.args))) else: return s + def associate(op, args): """Given an associative op, return an expression with the same meaning as Expr(op, *args), but flattened -- that is, with nested @@ -513,19 +567,24 @@ def associate(op, args): else: return Expr(op, *args) -_op_identity = {'&':TRUE, '|':FALSE, '+':ZERO, '*':ONE} +_op_identity = {'&': TRUE, '|': FALSE, '+': ZERO, '*': ONE} + def dissociate(op, args): """Given an associative op, return a flattened list result such that Expr(op, *result) means the same as Expr(op, *args).""" result = [] + def collect(subargs): for arg in subargs: - if arg.op == op: collect(arg.args) - else: result.append(arg) + if arg.op == op: + collect(arg.args) + else: + result.append(arg) collect(args) return result + def conjuncts(s): """Return a list of the conjuncts in the sentence s. >>> conjuncts(A & B) @@ -535,6 +594,7 @@ def conjuncts(s): """ return dissociate('&', [s]) + def disjuncts(s): """Return a list of the disjuncts in the sentence s. >>> disjuncts(A | B) @@ -546,6 +606,7 @@ def disjuncts(s): #______________________________________________________________________________ + def pl_resolution(KB, alpha): "Propositional-logic resolution: say if alpha follows from KB. [Fig. 7.12]" clauses = KB.clauses + conjuncts(to_cnf(~alpha)) @@ -556,11 +617,15 @@ def pl_resolution(KB, alpha): for i in range(n) for j in range(i+1, n)] for (ci, cj) in pairs: resolvents = pl_resolve(ci, cj) - if FALSE in resolvents: return True + if FALSE in resolvents: + return True new = new.union(set(resolvents)) - if new.issubset(set(clauses)): return False + if new.issubset(set(clauses)): + return False for c in new: - if c not in clauses: clauses.append(c) + if c not in clauses: + clauses.append(c) + def pl_resolve(ci, cj): """Return all clauses that can be obtained by resolving clauses ci and cj. @@ -580,7 +645,9 @@ def pl_resolve(ci, cj): #______________________________________________________________________________ + class PropDefiniteKB(PropKB): + "A KB of propositional definite clauses." def tell(self, sentence): @@ -602,6 +669,7 @@ def clauses_with_premise(self, p): return [c for c in self.clauses if c.op == '>>' and p in conjuncts(c.args[0])] + def pl_fc_entails(KB, q): """Use forward chaining to see if a PropDefiniteKB entails symbol q. [Fig. 7.15] @@ -609,12 +677,13 @@ def pl_fc_entails(KB, q): True """ count = dict([(c, len(conjuncts(c.args[0]))) for c in KB.clauses - if c.op == '>>']) + if c.op == '>>']) inferred = defaultdict(bool) agenda = [s for s in KB.clauses if is_prop_symbol(s.op)] while agenda: p = agenda.pop() - if p == q: return True + if p == q: + return True if not inferred[p]: inferred[p] = True for c in KB.clauses_with_premise(p): @@ -623,17 +692,18 @@ def pl_fc_entails(KB, q): agenda.append(c.args[1]) return False -## Wumpus World example [Fig. 7.13] -Fig[7,13] = expr("(B11 <=> (P12 | P21)) & ~B11") +# Wumpus World example [Fig. 7.13] +Fig[7, 13] = expr("(B11 <=> (P12 | P21)) & ~B11") -## Propositional Logic Forward Chaining example [Fig. 7.16] -Fig[7,15] = PropDefiniteKB() +# Propositional Logic Forward Chaining example [Fig. 7.16] +Fig[7, 15] = PropDefiniteKB() for s in "P>>Q (L&M)>>P (B&L)>>M (A&P)>>L (A&B)>>L A B".split(): - Fig[7,15].tell(expr(s)) + Fig[7, 15].tell(expr(s)) #______________________________________________________________________________ # DPLL-Satisfiable [Fig. 7.17] + def dpll_satisfiable(s): """Check satisfiability of a propositional sentence. This differs from the book code in two ways: (1) it returns a model @@ -649,11 +719,12 @@ def dpll_satisfiable(s): symbols = prop_symbols(s) return dpll(clauses, symbols, {}) + def dpll(clauses, symbols, model): "See if the clauses are true in a partial model." - unknown_clauses = [] ## clauses with an unknown truth value + unknown_clauses = [] # clauses with an unknown truth value for c in clauses: - val = pl_true(c, model) + val = pl_true(c, model) if val == False: return False if val != True: @@ -672,6 +743,7 @@ def dpll(clauses, symbols, model): return (dpll(clauses, symbols, extend(model, P, True)) or dpll(clauses, symbols, extend(model, P, False))) + def find_pure_symbol(symbols, clauses): """Find a symbol and its value if it appears only as a positive literal (or only as a negative) in clauses. @@ -681,11 +753,15 @@ def find_pure_symbol(symbols, clauses): for s in symbols: found_pos, found_neg = False, False for c in clauses: - if not found_pos and s in disjuncts(c): found_pos = True - if not found_neg and ~s in disjuncts(c): found_neg = True - if found_pos != found_neg: return s, found_pos + if not found_pos and s in disjuncts(c): + found_pos = True + if not found_neg and ~s in disjuncts(c): + found_neg = True + if found_pos != found_neg: + return s, found_pos return None, None + def find_unit_clause(clauses, model): """Find a forced assignment if possible from a clause with only 1 variable not bound in the model. @@ -694,9 +770,11 @@ def find_unit_clause(clauses, model): """ for clause in clauses: P, value = unit_clause_assign(clause, model) - if P: return P, value + if P: + return P, value return None, None + def unit_clause_assign(clause, model): """Return a single variable/value pair that makes clause true in the model, if possible. @@ -719,6 +797,7 @@ def unit_clause_assign(clause, model): P, value = sym, positive return P, value + def inspect_literal(literal): """The symbol in this literal, and the value it should take to make the literal true. @@ -735,37 +814,44 @@ def inspect_literal(literal): #______________________________________________________________________________ # Walk-SAT [Fig. 7.18] + def WalkSAT(clauses, p=0.5, max_flips=10000): - ## model is a random assignment of true/false to the symbols in clauses - ## See ~/aima1e/print1/manual/knowledge+logic-answers.tex ??? + # model is a random assignment of true/false to the symbols in clauses + # See ~/aima1e/print1/manual/knowledge+logic-answers.tex ??? model = dict([(s, random.choice([True, False])) - for s in prop_symbols(clauses)]) + for s in prop_symbols(clauses)]) for i in range(max_flips): satisfied, unsatisfied = [], [] for clause in clauses: - (satisfied if pl_true(clause, model) else unsatisfied).append(clause) - if not unsatisfied: ## if model satisfies all the clauses + (satisfied if pl_true(clause, model) else unsatisfied).append( + clause) + if not unsatisfied: # if model satisfies all the clauses return model clause = random.choice(unsatisfied) if probability(p): sym = random.choice(prop_symbols(clause)) else: - ## Flip the symbol in clause that maximizes number of sat. clauses + # Flip the symbol in clause that maximizes number of sat. clauses raise NotImplementedError model[sym] = not model[sym] #______________________________________________________________________________ + class HybridWumpusAgent(agents.Agent): + "An agent for the wumpus world that does logical inference. [Fig. 7.19]""" + def __init__(self): unimplemented() + def plan_route(current, goals, allowed): unimplemented() #______________________________________________________________________________ + def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable): "[Fig. 7.22]" for t in range(t_max): @@ -775,14 +861,17 @@ def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable): return extract_solution(model) return None + def translate_to_SAT(init, transition, goal, t): unimplemented() + def extract_solution(model): unimplemented() #______________________________________________________________________________ + def unify(x, y, s): """Unify expressions x,y with substitution s; return a substitution that would make x,y equal, or None if x,y can not unify. x and y can be @@ -803,15 +892,18 @@ def unify(x, y, s): elif isinstance(x, str) or isinstance(y, str): return None elif issequence(x) and issequence(y) and len(x) == len(y): - if not x: return s + if not x: + return s return unify(x[1:], y[1:], unify(x[0], y[0], s)) else: return None + def is_variable(x): "A variable is an Expr with no args and a lowercase symbol as the op." return isinstance(x, Expr) and not x.args and is_var_symbol(x.op) + def unify_var(var, x, s): if var in s: return unify(s[var], x, s) @@ -820,6 +912,7 @@ def unify_var(var, x, s): else: return extend(s, var, x) + def occur_check(var, x, s): """Return true if variable var occurs anywhere in x (or in subst(s, x), if s has a binding for x).""" @@ -835,6 +928,7 @@ def occur_check(var, x, s): else: return False + def extend(s, var, val): """Copy the substitution s and extend it by setting var to val; return copy. @@ -845,6 +939,7 @@ def extend(s, var, val): s2[var] = val return s2 + def subst(s, x): """Substitute the substitution s into the expression x. >>> subst({x: 42, y:0}, F(x) + y) @@ -861,6 +956,7 @@ def subst(s, x): else: return Expr(x.op, *[subst(s, arg) for arg in x.args]) + def fol_fc_ask(KB, alpha): """Inefficient forward chaining for first-order logic. [Fig. 9.3] KB is a FolKB and alpha must be an atomic sentence.""" @@ -870,6 +966,7 @@ def fol_fc_ask(KB, alpha): ps, q = parse_definite_clause(standardize_variables(r)) raise NotImplementedError + def standardize_variables(sentence, dic=None): """Replace all the variables in sentence with new variables. >>> e = expr('F(a, b, c) & G(c, A, 23)') @@ -880,14 +977,15 @@ def standardize_variables(sentence, dic=None): >>> is_variable(standardize_variables(expr('x'))) True """ - if dic is None: dic = {} + if dic is None: + dic = {} if not isinstance(sentence, Expr): return sentence elif is_var_symbol(sentence.op): if sentence in dic: return dic[sentence] else: - v = Expr('v_%d' % standardize_variables.counter.next()) + v = Expr('v_%d' % next(standardize_variables.counter)) dic[sentence] = v return v else: @@ -898,7 +996,9 @@ def standardize_variables(sentence, dic=None): #______________________________________________________________________________ + class FolKB(KB): + """A knowledge base consisting of first-order definite clauses. >>> kb0 = FolKB([expr('Farmer(Mac)'), expr('Rabbit(Pete)'), ... expr('(Rabbit(r) & Farmer(f)) ==> Hates(f, r)')]) @@ -909,8 +1009,9 @@ class FolKB(KB): >>> kb0.ask(expr('Wife(Pete, x)')) False """ + def __init__(self, initial_clauses=[]): - self.clauses = [] # inefficient: no indexing + self.clauses = [] # inefficient: no indexing for clause in initial_clauses: self.tell(clause) @@ -929,43 +1030,45 @@ def retract(self, sentence): def fetch_rules_for_goal(self, goal): return self.clauses + def test_ask(query, kb=None): q = expr(query) vars = variables(q) answers = fol_bc_ask(kb or test_kb, q) - return sorted([pretty(dict((x, v) for x, v in a.items() if x in vars)) + return sorted([pretty(dict((x, v) for x, v in list(a.items()) if x in vars)) for a in answers], key=repr) test_kb = FolKB( - map(expr, ['Farmer(Mac)', - 'Rabbit(Pete)', - 'Mother(MrsMac, Mac)', - 'Mother(MrsRabbit, Pete)', - '(Rabbit(r) & Farmer(f)) ==> Hates(f, r)', - '(Mother(m, c)) ==> Loves(m, c)', - '(Mother(m, r) & Rabbit(r)) ==> Rabbit(m)', - '(Farmer(f)) ==> Human(f)', - # Note that this order of conjuncts - # would result in infinite recursion: - #'(Human(h) & Mother(m, h)) ==> Human(m)' - '(Mother(m, h) & Human(h)) ==> Human(m)' - ]) + list(map(expr, ['Farmer(Mac)', + 'Rabbit(Pete)', + 'Mother(MrsMac, Mac)', + 'Mother(MrsRabbit, Pete)', + '(Rabbit(r) & Farmer(f)) ==> Hates(f, r)', + '(Mother(m, c)) ==> Loves(m, c)', + '(Mother(m, r) & Rabbit(r)) ==> Rabbit(m)', + '(Farmer(f)) ==> Human(f)', + # Note that this order of conjuncts + # would result in infinite recursion: + #'(Human(h) & Mother(m, h)) ==> Human(m)' + '(Mother(m, h) & Human(h)) ==> Human(m)' + ])) ) crime_kb = FolKB( - map(expr, - ['(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)', - 'Owns(Nono, M1)', - 'Missile(M1)', - '(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)', - 'Missile(x) ==> Weapon(x)', - 'Enemy(x, America) ==> Hostile(x)', - 'American(West)', - 'Enemy(Nono, America)' - ]) + list(map(expr, + ['(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)', + 'Owns(Nono, M1)', + 'Missile(M1)', + '(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)', + 'Missile(x) ==> Weapon(x)', + 'Enemy(x, America) ==> Hostile(x)', + 'American(West)', + 'Enemy(Nono, America)' + ])) ) + def fol_bc_ask(KB, query): """A simple backward-chaining algorithm for first-order logic. [Fig. 9.6] KB should be an instance of FolKB, and goals a list of literals. @@ -984,12 +1087,14 @@ def fol_bc_ask(KB, query): """ return fol_bc_or(KB, query, {}) + def fol_bc_or(KB, goal, theta): for rule in KB.fetch_rules_for_goal(goal): lhs, rhs = parse_definite_clause(standardize_variables(rule)) for theta1 in fol_bc_and(KB, lhs, unify(rhs, goal, theta)): yield theta1 + def fol_bc_and(KB, goals, theta): if theta is None: pass @@ -1007,6 +1112,7 @@ def fol_bc_and(KB, goals, theta): # You can use the Expr class to do symbolic differentiation. This used to be # a part of AI; now it is considered a separate field, Symbolic Algebra. + def diff(y, x): """Return the symbolic derivative, dy/dx, as an Expr. However, you probably want to simplify the results with simp. @@ -1015,74 +1121,115 @@ def diff(y, x): >>> simp(diff(x * x, x)) (2 * x) """ - if y == x: return ONE - elif not y.args: return ZERO + if y == x: + return ONE + elif not y.args: + return ZERO else: u, op, v = y.args[0], y.op, y.args[-1] - if op == '+': return diff(u, x) + diff(v, x) - elif op == '-' and len(args) == 1: return -diff(u, x) - elif op == '-': return diff(u, x) - diff(v, x) - elif op == '*': return u * diff(v, x) + v * diff(u, x) - elif op == '/': return (v*diff(u, x) - u*diff(v, x)) / (v * v) + if op == '+': + return diff(u, x) + diff(v, x) + elif op == '-' and len(args) == 1: + return -diff(u, x) + elif op == '-': + return diff(u, x) - diff(v, x) + elif op == '*': + return u * diff(v, x) + v * diff(u, x) + elif op == '/': + return (v*diff(u, x) - u*diff(v, x)) / (v * v) elif op == '**' and isnumber(x.op): return (v * u ** (v - 1) * diff(u, x)) - elif op == '**': return (v * u ** (v - 1) * diff(u, x) - + u ** v * Expr('log')(u) * diff(v, x)) - elif op == 'log': return diff(u, x) / u - else: raise ValueError("Unknown op: %s in diff(%s, %s)" % (op, y, x)) + elif op == '**': + return (v * u ** (v - 1) * diff(u, x) + + u ** v * Expr('log')(u) * diff(v, x)) + elif op == 'log': + return diff(u, x) / u + else: + raise ValueError("Unknown op: %s in diff(%s, %s)" % (op, y, x)) + def simp(x): - if not x.args: return x - args = map(simp, x.args) + if not x.args: + return x + args = list(map(simp, x.args)) u, op, v = args[0], x.op, args[-1] if op == '+': - if v == ZERO: return u - if u == ZERO: return v - if u == v: return TWO * u - if u == -v or v == -u: return ZERO + if v == ZERO: + return u + if u == ZERO: + return v + if u == v: + return TWO * u + if u == -v or v == -u: + return ZERO elif op == '-' and len(args) == 1: - if u.op == '-' and len(u.args) == 1: return u.args[0] ## --y ==> y + if u.op == '-' and len(u.args) == 1: + return u.args[0] # --y ==> y elif op == '-': - if v == ZERO: return u - if u == ZERO: return -v - if u == v: return ZERO - if u == -v or v == -u: return ZERO + if v == ZERO: + return u + if u == ZERO: + return -v + if u == v: + return ZERO + if u == -v or v == -u: + return ZERO elif op == '*': - if u == ZERO or v == ZERO: return ZERO - if u == ONE: return v - if v == ONE: return u - if u == v: return u ** 2 + if u == ZERO or v == ZERO: + return ZERO + if u == ONE: + return v + if v == ONE: + return u + if u == v: + return u ** 2 elif op == '/': - if u == ZERO: return ZERO - if v == ZERO: return Expr('Undefined') - if u == v: return ONE - if u == -v or v == -u: return ZERO + if u == ZERO: + return ZERO + if v == ZERO: + return Expr('Undefined') + if u == v: + return ONE + if u == -v or v == -u: + return ZERO elif op == '**': - if u == ZERO: return ZERO - if v == ZERO: return ONE - if u == ONE: return ONE - if v == ONE: return u + if u == ZERO: + return ZERO + if v == ZERO: + return ONE + if u == ONE: + return ONE + if v == ONE: + return u elif op == 'log': - if u == ONE: return ZERO - else: raise ValueError("Unknown op: " + op) - ## If we fall through to here, we can not simplify further + if u == ONE: + return ZERO + else: + raise ValueError("Unknown op: " + op) + # If we fall through to here, we can not simplify further return Expr(op, *args) + def d(y, x): "Differentiate and then simplify." return simp(diff(y, x)) -#_______________________________________________________________________________ +#_________________________________________________________________________ # Utilities for doctest cases # These functions print their arguments in a standard order # to compensate for the random order in the standard representation + def pretty(x): t = type(x) - if t is dict: return pretty_dict(x) - elif t is set: return pretty_set(x) - else: return repr(x) + if t is dict: + return pretty_dict(x) + elif t is set: + return pretty_set(x) + else: + return repr(x) + def pretty_dict(d): """Return dictionary d's repr but with the items sorted. @@ -1092,7 +1239,8 @@ def pretty_dict(d): '{x: A, y: B, z: C}' """ return '{%s}' % ', '.join('%r: %r' % (k, v) - for k, v in sorted(d.items(), key=repr)) + for k, v in sorted(list(d.items()), key=repr)) + def pretty_set(s): """Return set s's repr but with the items sorted. @@ -1103,22 +1251,29 @@ def pretty_set(s): """ return 'set(%r)' % sorted(s, key=repr) + def pp(x): print(pretty(x)) + def ppsubst(s): """Pretty-print substitution s""" ppdict(s) + def ppdict(d): print(pretty_dict(d)) + def ppset(s): print(pretty_set(s)) #________________________________________________________________________ -class logicTest: """ + +class logicTest: + + """ ### PropKB >>> kb = PropKB() >>> kb.tell(A & B) diff --git a/mdp.py b/aimaPy/mdp.py similarity index 85% rename from mdp.py rename to aimaPy/mdp.py index b2aa8d0d1..8822f8260 100644 --- a/mdp.py +++ b/aimaPy/mdp.py @@ -6,9 +6,12 @@ dictionary of {state:number} pairs. We then define the value_iteration and policy_iteration algorithms.""" -from utils import * + +from . utils import * + class MDP: + """A Markov Decision Process, defined by an initial state, transition model, and reward function. We also keep track of a gamma value, for use by algorithms. The transition model is represented somewhat differently from @@ -18,14 +21,14 @@ class MDP: actions for each state. [page 646]""" def __init__(self, init, actlist, terminals, gamma=.9): - self.init=init - self.actlist=actlist - self.terminals=terminals + self.init = init + self.actlist = actlist + self.terminals = terminals if not (0 <= gamma < 1): raise ValueError("An MDP must have 0 <= gamma < 1") - self.gamma=gamma - self.states=set() - self.reward={} + self.gamma = gamma + self.states = set() + self.reward = {} def R(self, state): "Return a numeric reward for this state." @@ -34,7 +37,7 @@ def R(self, state): def T(self, state, action): """Transition model. From a state and an action, return a list of (probability, result-state) pairs.""" - raise NotImplementedError + raise NotImplementedError def actions(self, state): """Set of actions that can be performed in this state. By default, a @@ -45,18 +48,21 @@ def actions(self, state): else: return self.actlist + class GridMDP(MDP): + """A two-dimensional grid MDP, as in [Figure 17.1]. All you have to do is specify the grid as a list of lists of rewards; use None for an obstacle (unreachable state). Also, you should specify the terminal states. An action is an (x, y) unit vector; e.g. (1, 0) means move east.""" + def __init__(self, grid, terminals, init=(0, 0), gamma=.9): - grid.reverse() ## because we want row 0 on bottom, not on top + grid.reverse() # because we want row 0 on bottom, not on top MDP.__init__(self, init, actlist=orientations, terminals=terminals, gamma=gamma) - self.grid=grid - self.rows=len(grid) - self.cols=len(grid[0]) + self.grid = grid + self.rows = len(grid) + self.cols = len(grid[0]) for x in range(self.cols): for y in range(self.rows): self.reward[x, y] = grid[y][x] @@ -78,23 +84,25 @@ def go(self, state, direction): def to_grid(self, mapping): """Convert a mapping from (x, y) to v into a [[..., v, ...]] grid.""" - return list(reversed([[mapping.get((x,y), None) + return list(reversed([[mapping.get((x, y), None) for x in range(self.cols)] for y in range(self.rows)])) def to_arrows(self, policy): - chars = {(1, 0):'>', (0, 1):'^', (-1, 0):'<', (0, -1):'v', None: '.'} - return self.to_grid(dict([(s, chars[a]) for (s, a) in policy.items()])) + chars = { + (1, 0): '>', (0, 1): '^', (-1, 0): '<', (0, -1): 'v', None: '.'} + return self.to_grid(dict([(s, chars[a]) for (s, a) in list(policy.items())])) #______________________________________________________________________________ -Fig[17,1] = GridMDP([[-0.04, -0.04, -0.04, +1], - [-0.04, None, -0.04, -1], - [-0.04, -0.04, -0.04, -0.04]], - terminals=[(3, 2), (3, 1)]) +Fig[17, 1] = GridMDP([[-0.04, -0.04, -0.04, +1], + [-0.04, None, -0.04, -1], + [-0.04, -0.04, -0.04, -0.04]], + terminals=[(3, 2), (3, 1)]) #______________________________________________________________________________ + def value_iteration(mdp, epsilon=0.001): "Solving an MDP by value iteration. [Fig. 17.4]" U1 = dict([(s, 0) for s in mdp.states]) @@ -107,22 +115,26 @@ def value_iteration(mdp, epsilon=0.001): for a in mdp.actions(s)]) delta = max(delta, abs(U1[s] - U[s])) if delta < epsilon * (1 - gamma) / gamma: - return U + return U + def best_policy(mdp, U): """Given an MDP and a utility function U, determine the best policy, as a mapping from state to action. (Equation 17.4)""" pi = {} for s in mdp.states: - pi[s] = argmax(mdp.actions(s), lambda a:expected_utility(a, s, U, mdp)) + pi[s] = argmax( + mdp.actions(s), lambda a: expected_utility(a, s, U, mdp)) return pi + def expected_utility(a, s, U, mdp): "The expected utility of doing a in state s, according to the MDP and U." return sum([p * U[s1] for (p, s1) in mdp.T(s, a)]) #______________________________________________________________________________ + def policy_iteration(mdp): "Solve an MDP by policy iteration [Fig. 17.7]" U = dict([(s, 0) for s in mdp.states]) @@ -131,13 +143,15 @@ def policy_iteration(mdp): U = policy_evaluation(pi, U, mdp) unchanged = True for s in mdp.states: - a = argmax(mdp.actions(s), lambda a: expected_utility(a,s,U,mdp)) + a = argmax( + mdp.actions(s), lambda a: expected_utility(a, s, U, mdp)) if a != pi[s]: pi[s] = a unchanged = False if unchanged: return pi + def policy_evaluation(pi, U, mdp, k=20): """Return an updated utility mapping U from each state in the MDP to its utility, using an approximation (modified policy iteration).""" @@ -164,7 +178,8 @@ def policy_evaluation(pi, U, mdp, k=20): ^ > ^ < """ -__doc__ += random_tests(""" +__doc__ += """ +Random tests: >>> pi {(3, 2): None, (3, 1): None, (3, 0): (-1, 0), (2, 1): (0, 1), (0, 2): (1, 0), (1, 0): (1, 0), (0, 0): (0, 1), (1, 2): (1, 0), (2, 0): (0, 1), (0, 1): (0, 1), (2, 2): (1, 0)} @@ -174,6 +189,4 @@ def policy_evaluation(pi, U, mdp, k=20): >>> policy_iteration(Fig[17,1]) {(3, 2): None, (3, 1): None, (3, 0): (0, -1), (2, 1): (-1, 0), (0, 2): (1, 0), (1, 0): (1, 0), (0, 0): (1, 0), (1, 2): (1, 0), (2, 0): (1, 0), (0, 1): (1, 0), (2, 2): (1, 0)} -""") - - +""" diff --git a/nlp.py b/aimaPy/nlp.py similarity index 76% rename from nlp.py rename to aimaPy/nlp.py index 6c3ee4992..8b6d3ca97 100644 --- a/nlp.py +++ b/aimaPy/nlp.py @@ -3,31 +3,35 @@ # (Written for the second edition of AIMA; expect some discrepanciecs # from the third edition until this gets reviewed.) -from utils import * +from . utils import * from collections import defaultdict #______________________________________________________________________________ # Grammars and Lexicons + def Rules(**rules): """Create a dictionary mapping symbols to alternative sequences. >>> Rules(A = "B C | D E") {'A': [['B', 'C'], ['D', 'E']]} """ - for (lhs, rhs) in rules.items(): + for (lhs, rhs) in list(rules.items()): rules[lhs] = [alt.strip().split() for alt in rhs.split('|')] return rules + def Lexicon(**rules): """Create a dictionary mapping symbols to alternative words. >>> Lexicon(Art = "the | a | an") {'Art': ['the', 'a', 'an']} """ - for (lhs, rhs) in rules.items(): + for (lhs, rhs) in list(rules.items()): rules[lhs] = [word.strip() for word in rhs.split('|')] return rules + class Grammar: + def __init__(self, name, rules, lexicon): "A grammar has a set of rules and a lexicon." update(self, name=name, rules=rules, lexicon=lexicon) @@ -48,44 +52,45 @@ def __repr__(self): return '' % self.name E0 = Grammar('E0', - Rules( # Grammar for E_0 [Fig. 22.4] - S = 'NP VP | S Conjunction S', - NP = 'Pronoun | Name | Noun | Article Noun | Digit Digit | NP PP | NP RelClause', - VP = 'Verb | VP NP | VP Adjective | VP PP | VP Adverb', - PP = 'Preposition NP', - RelClause = 'That VP'), - - Lexicon( # Lexicon for E_0 [Fig. 22.3] - Noun = "stench | breeze | glitter | nothing | wumpus | pit | pits | gold | east", - Verb = "is | see | smell | shoot | fell | stinks | go | grab | carry | kill | turn | feel", - Adjective = "right | left | east | south | back | smelly", - Adverb = "here | there | nearby | ahead | right | left | east | south | back", - Pronoun = "me | you | I | it", - Name = "John | Mary | Boston | Aristotle", - Article = "the | a | an", - Preposition = "to | in | on | near", - Conjunction = "and | or | but", - Digit = "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9", - That = "that" - )) - -E_ = Grammar('E_', # Trivial Grammar and lexicon for testing - Rules( - S = 'NP VP', - NP = 'Art N | Pronoun', - VP = 'V NP'), - - Lexicon( - Art = 'the | a', - N = 'man | woman | table | shoelace | saw', - Pronoun = 'I | you | it', - V = 'saw | liked | feel' - )) - -E_NP_ = Grammar('E_NP_', # another trivial grammar for testing - Rules(NP = 'Adj NP | N'), - Lexicon(Adj = 'happy | handsome | hairy', - N = 'man')) + Rules( # Grammar for E_0 [Fig. 22.4] + S='NP VP | S Conjunction S', + NP='Pronoun | Name | Noun | Article Noun | Digit Digit | NP PP | NP RelClause', + VP='Verb | VP NP | VP Adjective | VP PP | VP Adverb', + PP='Preposition NP', + RelClause='That VP'), + + Lexicon( # Lexicon for E_0 [Fig. 22.3] + Noun="stench | breeze | glitter | nothing | wumpus | pit | pits | gold | east", + Verb="is | see | smell | shoot | fell | stinks | go | grab | carry | kill | turn | feel", + Adjective="right | left | east | south | back | smelly", + Adverb="here | there | nearby | ahead | right | left | east | south | back", + Pronoun="me | you | I | it", + Name="John | Mary | Boston | Aristotle", + Article="the | a | an", + Preposition="to | in | on | near", + Conjunction="and | or | but", + Digit="0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9", + That="that" + )) + +E_ = Grammar('E_', # Trivial Grammar and lexicon for testing + Rules( + S='NP VP', + NP='Art N | Pronoun', + VP='V NP'), + + Lexicon( + Art='the | a', + N='man | woman | table | shoelace | saw', + Pronoun='I | you | it', + V='saw | liked | feel' + )) + +E_NP_ = Grammar('E_NP_', # another trivial grammar for testing + Rules(NP='Adj NP | N'), + Lexicon(Adj='happy | handsome | hairy', + N='man')) + def generate_random(grammar=E_, s='S'): """Replace each token in s by a random entry in grammar (recursively). @@ -109,6 +114,7 @@ def rewrite(tokens, into): class Chart: + """Class for parsing sentences using a chart data structure. [Fig 22.7] >>> chart = Chart(E0); >>> len(chart.parses('the stench is in 2 2')) @@ -152,7 +158,7 @@ def add_edge(self, edge): if edge not in self.chart[end]: self.chart[end].append(edge) if self.trace: - print '%10s: added %s' % (caller(2), edge) + print('%10s: added %s' % (caller(2), edge)) if not expects: self.extender(edge) else: @@ -164,8 +170,9 @@ def scanner(self, j, word): if Bb and self.grammar.isa(word, Bb[0]): self.add_edge([i, j+1, A, alpha + [(Bb[0], word)], Bb[1:]]) - def predictor(self, (i, j, A, alpha, Bb)): + def predictor(self, xxx_todo_changeme): "Add to chart any rules for B that could help extend this edge." + (i, j, A, alpha, Bb) = xxx_todo_changeme B = Bb[0] if B in self.grammar.rules: for rhs in self.grammar.rewrites_for(B): @@ -179,10 +186,9 @@ def extender(self, edge): self.add_edge([i, k, A, alpha + [edge], B1b[1:]]) - -#### TODO: -#### 1. Parsing with augmentations -- requires unification, etc. -#### 2. Sequitor +# TODO: +# 1. Parsing with augmentations -- requires unification, etc. +# 2. Sequitor __doc__ += """ >>> chart = Chart(E0) diff --git a/aimaPy/planning.py b/aimaPy/planning.py new file mode 100644 index 000000000..515ecf3ac --- /dev/null +++ b/aimaPy/planning.py @@ -0,0 +1,12 @@ +"""Planning (Chapters 10-11) +""" + + +from . utils import * +from . import agents +import math +import random +import sys +import time +import bisect +import string diff --git a/probability.py b/aimaPy/probability.py similarity index 91% rename from probability.py rename to aimaPy/probability.py index 5c95de36b..6975950e0 100644 --- a/probability.py +++ b/aimaPy/probability.py @@ -1,13 +1,15 @@ """Probability models. (Chapter 13-15) """ -from utils import * -from logic import extend -import random +from . utils import * +from . logic import extend +import random from collections import defaultdict +from functools import reduce #______________________________________________________________________________ + def DTAgentProgram(belief_state): "A decision-theoretic agent. [Fig. 13.1]" def program(percept): @@ -20,7 +22,9 @@ def program(percept): #______________________________________________________________________________ + class ProbDist: + """A discrete probability distribution. You name the random variable in the constructor, then assign and query probability of values. >>> P = ProbDist('Flip'); P['H'], P['T'] = 0.25, 0.75; P['H'] @@ -29,19 +33,22 @@ class ProbDist: >>> P['lo'], P['med'], P['hi'] (0.125, 0.375, 0.5) """ + def __init__(self, varname='?', freqs=None): """If freqs is given, it is a dictionary of value: frequency pairs, and the ProbDist then is normalized.""" update(self, prob={}, varname=varname, values=[]) if freqs: - for (v, p) in freqs.items(): + for (v, p) in list(freqs.items()): self[v] = p self.normalize() def __getitem__(self, val): "Given a value, return P(value)." - try: return self.prob[val] - except KeyError: return 0 + try: + return self.prob[val] + except KeyError: + return 0 def __setitem__(self, val, p): "Set P(val) = p." @@ -72,7 +79,9 @@ def show_approx(self, numfmt='%.3g'): epsilon = 0.001 + class JointProbDist(ProbDist): + """A discrete probability distribute over a set of variables. >>> P = JointProbDist(['X', 'Y']); P[1, 1] = 0.25 >>> P[1, 1] @@ -80,6 +89,7 @@ class JointProbDist(ProbDist): >>> P[dict(X=0, Y=1)] = 0.5 >>> P[dict(X=0, Y=1)] 0.5""" + def __init__(self, variables): update(self, prob={}, variables=variables, vals=defaultdict(list)) @@ -105,6 +115,7 @@ def values(self, var): def __repr__(self): return "P(%s)" % self.variables + def event_values(event, vars): """Return a tuple of the values of variables vars in event. >>> event_values ({'A': 10, 'B': 9, 'C': 8}, ['C', 'A']) @@ -119,6 +130,7 @@ def event_values(event, vars): #______________________________________________________________________________ + def enumerate_joint_ask(X, e, P): """Return a probability distribution over the values of the variable X, given the {var:val} observations e, in the JointProbDist P. [Section 13.3] @@ -128,12 +140,13 @@ def enumerate_joint_ask(X, e, P): '0: 0.667, 1: 0.167, 2: 0.167' """ assert X not in e, "Query variable must be distinct from evidence" - Q = ProbDist(X) # probability distribution for X, initially empty - Y = [v for v in P.variables if v != X and v not in e] # hidden vars. + Q = ProbDist(X) # probability distribution for X, initially empty + Y = [v for v in P.variables if v != X and v not in e] # hidden vars. for xi in P.values(X): Q[xi] = enumerate_joint(Y, extend(e, X, xi), P) return Q.normalize() + def enumerate_joint(vars, e, P): """Return the sum of those entries in P consistent with e, provided vars is P's remaining variables (the ones not in e).""" @@ -145,7 +158,9 @@ def enumerate_joint(vars, e, P): #______________________________________________________________________________ + class BayesNet: + "Bayesian network containing only boolean-variable nodes." def __init__(self, node_specs=[]): @@ -181,7 +196,9 @@ def variable_values(self, var): def __repr__(self): return 'BayesNet(%r)' % self.nodes + class BayesNode: + """A conditional probability distribution for a boolean variable, P(X | parents). Part of a BayesNet.""" @@ -209,17 +226,19 @@ def __init__(self, X, parents, cpt): >>> Z = BayesNode('Z', 'P Q', ... {(T, T): 0.2, (T, F): 0.3, (F, T): 0.5, (F, F): 0.7}) """ - if isinstance(parents, str): parents = parents.split() + if isinstance(parents, str): + parents = parents.split() # We store the table always in the third form above. - if isinstance(cpt, (float, int)): # no parents, 0-tuple + if isinstance(cpt, (float, int)): # no parents, 0-tuple cpt = {(): cpt} elif isinstance(cpt, dict): - if cpt and isinstance(cpt.keys()[0], bool): # one parent, 1-tuple - cpt = dict(((v,), p) for v, p in cpt.items()) + # one parent, 1-tuple + if cpt and isinstance(list(cpt.keys())[0], bool): + cpt = dict(((v,), p) for v, p in list(cpt.items())) assert isinstance(cpt, dict) - for vs, p in cpt.items(): + for vs, p in list(cpt.items()): assert isinstance(vs, tuple) and len(vs) == len(parents) assert every(lambda v: isinstance(v, bool), vs) assert 0 <= p <= 1 @@ -256,13 +275,14 @@ def __repr__(self): ('Burglary', '', 0.001), ('Earthquake', '', 0.002), ('Alarm', 'Burglary Earthquake', - {(T, T): 0.95, (T, F): 0.94, (F, T): 0.29, (F, F): 0.001}), + {(T, T): 0.95, (T, F): 0.94, (F, T): 0.29, (F, F): 0.001}), ('JohnCalls', 'Alarm', {T: 0.90, F: 0.05}), ('MaryCalls', 'Alarm', {T: 0.70, F: 0.01}) - ]) +]) #______________________________________________________________________________ + def enumeration_ask(X, e, bn): """Return the conditional probability distribution of variable X given evidence e, from BayesNet bn. [Fig. 14.9] @@ -275,6 +295,7 @@ def enumeration_ask(X, e, bn): Q[xi] = enumerate_all(bn.vars, extend(e, X, xi), bn) return Q.normalize() + def enumerate_all(vars, e, bn): """Return the sum of those entries in P(vars | e{others}) consistent with e, where P is the joint distribution represented @@ -292,6 +313,7 @@ def enumerate_all(vars, e, bn): #______________________________________________________________________________ + def elimination_ask(X, e, bn): """Compute bn's P(X|e) by variable elimination. [Fig. 14.11] >>> elimination_ask('Burglary', dict(JohnCalls=T, MaryCalls=T), burglary @@ -305,10 +327,12 @@ def elimination_ask(X, e, bn): factors = sum_out(var, factors, bn) return pointwise_product(factors, bn).normalize() + def is_hidden(var, X, e): "Is var a hidden variable when querying P(X|e)?" return var != X and var not in e + def make_factor(var, e, bn): """Return the factor for var in bn's joint distribution given e. That is, bn's full joint distribution, projected to accord with e, @@ -319,9 +343,11 @@ def make_factor(var, e, bn): for e1 in all_events(vars, bn, e)) return Factor(vars, cpt) + def pointwise_product(factors, bn): return reduce(lambda f, g: f.pointwise_product(g, bn), factors) + def sum_out(var, factors, bn): "Eliminate var from all factors by summing over its values." result, var_factors = [], [] @@ -330,7 +356,9 @@ def sum_out(var, factors, bn): result.append(pointwise_product(var_factors, bn).sum_out(var, bn)) return result + class Factor: + "A factor in a joint distribution." def __init__(self, vars, cpt): @@ -356,12 +384,13 @@ def normalize(self): "Return my probabilities; must be down to one variable." assert len(self.vars) == 1 return ProbDist(self.vars[0], - dict((k, v) for ((k,), v) in self.cpt.items())) + dict((k, v) for ((k,), v) in list(self.cpt.items()))) def p(self, e): "Look up my value tabulated for e." return self.cpt[event_values(e, self.vars)] + def all_events(vars, bn, e): "Yield every way of extending e with values for all vars." if not vars: @@ -381,10 +410,11 @@ def all_events(vars, bn, e): ('Sprinkler', 'Cloudy', {T: 0.10, F: 0.50}), ('Rain', 'Cloudy', {T: 0.80, F: 0.20}), ('WetGrass', 'Sprinkler Rain', - {(T, T): 0.99, (T, F): 0.90, (F, T): 0.90, (F, F): 0.00})]) + {(T, T): 0.99, (T, F): 0.90, (F, T): 0.90, (F, F): 0.00})]) #______________________________________________________________________________ + def prior_sample(bn): """Randomly sample from bn's full joint distribution. The result is a {variable: value} dict. [Fig. 14.13]""" @@ -393,7 +423,8 @@ def prior_sample(bn): event[node.variable] = node.sample(event) return event -#_______________________________________________________________________________ +#_________________________________________________________________________ + def rejection_sampling(X, e, bn, N): """Estimate the probability distribution of variable X given @@ -405,19 +436,22 @@ def rejection_sampling(X, e, bn, N): ... burglary, 10000).show_approx() 'False: 0.7, True: 0.3' """ - counts = dict((x, 0) for x in bn.variable_values(X)) # bold N in Fig. 14.14 - for j in xrange(N): - sample = prior_sample(bn) # boldface x in Fig. 14.14 + counts = dict((x, 0) + for x in bn.variable_values(X)) # bold N in Fig. 14.14 + for j in range(N): + sample = prior_sample(bn) # boldface x in Fig. 14.14 if consistent_with(sample, e): counts[sample[X]] += 1 return ProbDist(X, counts) + def consistent_with(event, evidence): "Is event consistent with the given evidence?" return all(evidence.get(k, v) == v - for k, v in event.items()) + for k, v in list(event.items())) + +#_________________________________________________________________________ -#_______________________________________________________________________________ def likelihood_weighting(X, e, bn, N): """Estimate the probability distribution of variable X given @@ -428,17 +462,18 @@ def likelihood_weighting(X, e, bn, N): 'False: 0.702, True: 0.298' """ W = dict((x, 0) for x in bn.variable_values(X)) - for j in xrange(N): - sample, weight = weighted_sample(bn, e) # boldface x, w in Fig. 14.15 + for j in range(N): + sample, weight = weighted_sample(bn, e) # boldface x, w in Fig. 14.15 W[sample[X]] += weight return ProbDist(X, W) + def weighted_sample(bn, e): """Sample an event from bn that's consistent with the evidence e; return the event and its weight, the likelihood that the event accords to the evidence.""" w = 1 - event = dict(e) # boldface x in Fig. 14.15 + event = dict(e) # boldface x in Fig. 14.15 for node in bn.nodes: Xi = node.variable if Xi in e: @@ -447,7 +482,8 @@ def weighted_sample(bn, e): event[Xi] = node.sample(event) return event, w -#_______________________________________________________________________________ +#_________________________________________________________________________ + def gibbs_ask(X, e, bn, N): """[Fig. 14.16] @@ -457,17 +493,19 @@ def gibbs_ask(X, e, bn, N): 'False: 0.738, True: 0.262' """ assert X not in e, "Query variable must be distinct from evidence" - counts = dict((x, 0) for x in bn.variable_values(X)) # bold N in Fig. 14.16 + counts = dict((x, 0) + for x in bn.variable_values(X)) # bold N in Fig. 14.16 Z = [var for var in bn.vars if var not in e] - state = dict(e) # boldface x in Fig. 14.16 + state = dict(e) # boldface x in Fig. 14.16 for Zi in Z: state[Zi] = random.choice(bn.variable_values(Zi)) - for j in xrange(N): + for j in range(N): for Zi in Z: state[Zi] = markov_blanket_sample(Zi, state, bn) counts[state[X]] += 1 return ProbDist(X, counts) + def markov_blanket_sample(X, e, bn): """Return a sample from P(X | mb) where mb denotes that the variables in the Markov blanket of X take their values from event @@ -480,23 +518,27 @@ def markov_blanket_sample(X, e, bn): # [Equation 14.12:] Q[xi] = Xnode.p(xi, e) * product(Yj.p(ei[Yj.variable], ei) for Yj in Xnode.children) - return probability(Q.normalize()[True]) # (assuming a Boolean variable here) + # (assuming a Boolean variable here) + return probability(Q.normalize()[True]) + +#_________________________________________________________________________ -#_______________________________________________________________________________ def forward_backward(ev, prior): """[Fig. 15.4]""" unimplemented() + def fixed_lag_smoothing(e_t, hmm, d): """[Fig. 15.6]""" unimplemented() + def particle_filtering(e, N, dbn): """[Fig. 15.17]""" unimplemented() -#_______________________________________________________________________________ +#_________________________________________________________________________ __doc__ += """ # We can build up a probability distribution like this (p. 469): >>> P = ProbDist() diff --git a/rl.py b/aimaPy/rl.py similarity index 89% rename from rl.py rename to aimaPy/rl.py index fc0e2c9e9..67289e77d 100644 --- a/rl.py +++ b/aimaPy/rl.py @@ -1,15 +1,19 @@ """Reinforcement Learning (Chapter 21) """ -from utils import * -import agents +from . utils import * +from . import agents + class PassiveADPAgent(agents.Agent): + """Passive (non-learning) agent that uses adaptive dynamic programming on a given MDP and policy. [Fig. 21.2]""" NotImplemented + class PassiveTDAgent(agents.Agent): + """Passive (non-learning) agent that uses temporal differences to learn utility estimates. [Fig. 21.4]""" NotImplemented diff --git a/search.py b/aimaPy/search.py similarity index 91% rename from search.py rename to aimaPy/search.py index ab2ba5136..4d9ce252f 100644 --- a/search.py +++ b/aimaPy/search.py @@ -4,12 +4,20 @@ then create problem instances and solve them with calls to the various search functions.""" -from utils import * -import math, random, sys, time, bisect, string + +from . utils import * +import math +import random +import sys +import time +import bisect +import string #______________________________________________________________________________ + class Problem(object): + """The abstract class for a formal problem. You should subclass this and implement the methods actions and result, and possibly __init__, goal_test, and path_cost. Then you will create instances @@ -19,7 +27,8 @@ def __init__(self, initial, goal=None): """The constructor specifies the initial state, and possibly a goal state, if there is a unique goal. Your subclass's constructor can add other arguments.""" - self.initial = initial; self.goal = goal + self.initial = initial + self.goal = goal def actions(self, state): """Return the actions that can be executed in the given @@ -54,7 +63,9 @@ def value(self, state): raise NotImplementedError #______________________________________________________________________________ + class Node: + """A node in a search tree. Contains a pointer to the parent (the node that this is a successor of) and to the actual state for this node. Note that if a state is arrived at by two paths, then there are two nodes with @@ -110,8 +121,11 @@ def __hash__(self): #______________________________________________________________________________ + class SimpleProblemSolvingAgentProgram: + """Abstract framework for a problem-solving agent. [Fig. 3.1]""" + def __init__(self, initial_state=None): update(self, state=initial_state, seq=[]) @@ -121,7 +135,8 @@ def __call__(self, percept): goal = self.formulate_goal(self.state) problem = self.formulate_problem(self.state, goal) self.seq = self.search(problem) - if not self.seq: return None + if not self.seq: + return None return self.seq.pop(0) def update_state(self, percept): @@ -139,6 +154,7 @@ def search(self, problem): #______________________________________________________________________________ # Uninformed Search algorithms + def tree_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. @@ -151,6 +167,7 @@ def tree_search(problem, frontier): frontier.extend(node.expand(problem)) return None + def graph_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. @@ -167,18 +184,22 @@ def graph_search(problem, frontier): and child not in frontier) return None + def breadth_first_tree_search(problem): "Search the shallowest nodes in the search tree first." return tree_search(problem, FIFOQueue()) + def depth_first_tree_search(problem): "Search the deepest nodes in the search tree first." return tree_search(problem, Stack()) + def depth_first_graph_search(problem): "Search the deepest nodes in the search tree first." return graph_search(problem, Stack()) + def breadth_first_search(problem): "[Fig. 3.11]" node = Node(problem.initial) @@ -197,6 +218,7 @@ def breadth_first_search(problem): frontier.append(child) return None + def best_first_graph_search(problem, f): """Search the nodes with the lowest f scores first. You specify the function f(node) that you want to minimize; for example, @@ -227,10 +249,12 @@ def best_first_graph_search(problem, f): frontier.append(child) return None + def uniform_cost_search(problem): "[Fig. 3.14]" return best_first_graph_search(problem, lambda node: node.path_cost) + def depth_limited_search(problem, limit=50): "[Fig. 3.17]" def recursive_dls(node, problem, limit): @@ -251,9 +275,10 @@ def recursive_dls(node, problem, limit): # Body of depth_limited_search: return recursive_dls(Node(problem.initial), problem, limit) + def iterative_deepening_search(problem): "[Fig. 3.18]" - for depth in xrange(sys.maxint): + for depth in range(sys.maxsize): result = depth_limited_search(problem, depth) if result != 'cutoff': return result @@ -262,7 +287,8 @@ def iterative_deepening_search(problem): # Informed (Heuristic) Search greedy_best_first_graph_search = best_first_graph_search - # Greedy best-first search is accomplished by specifying f(n) = h(n). +# Greedy best-first search is accomplished by specifying f(n) = h(n). + def astar_search(problem, h=None): """A* search is best-first graph search with f(n) = g(n)+h(n). @@ -274,6 +300,7 @@ def astar_search(problem, h=None): #______________________________________________________________________________ # Other search algorithms + def recursive_best_first_search(problem, h=None): "[Fig. 3.26]" h = memoize(h or problem.h, 'h') @@ -287,7 +314,8 @@ def RBFS(problem, node, flimit): for s in successors: s.f = max(s.path_cost + h(s), node.f) while True: - successors.sort(lambda x,y: cmp(x.f, y.f)) # Order by lowest f value + # Order by lowest f value + successors.sort(lambda x, y: cmp(x.f, y.f)) best = successors[0] if best.f > flimit: return None, best.f @@ -304,6 +332,7 @@ def RBFS(problem, node, flimit): result, bestf = RBFS(problem, node, infinity) return result + def hill_climbing(problem): """From the initial node, keep choosing the neighbor with highest value, stopping when no neighbor is better. [Fig. 4.2]""" @@ -319,14 +348,16 @@ def hill_climbing(problem): current = neighbor return current.state + def exp_schedule(k=20, lam=0.005, limit=100): "One possible schedule function for simulated annealing" return lambda t: (k * math.exp(-lam * t) if t < limit else 0) + def simulated_annealing(problem, schedule=exp_schedule()): "[Fig. 4.5]" current = Node(problem.initial) - for t in xrange(sys.maxint): + for t in range(sys.maxsize): T = schedule(t) if T == 0: return current @@ -338,14 +369,17 @@ def simulated_annealing(problem, schedule=exp_schedule()): if delta_e > 0 or probability(math.exp(delta_e/T)): current = next + def and_or_graph_search(problem): "[Fig. 4.11]" unimplemented() + def online_dfs_agent(s1): "[Fig. 4.21]" unimplemented() + def lrta_star_agent(s1): "[Fig. 4.24]" unimplemented() @@ -353,6 +387,7 @@ def lrta_star_agent(s1): #______________________________________________________________________________ # Genetic Algorithm + def genetic_search(problem, fitness_fn, ngen=1000, pmut=0.1, n=20): """Call genetic_algorithm on the appropriate parts of a problem. This requires the problem to have states that can mate and mutate, @@ -362,12 +397,13 @@ def genetic_search(problem, fitness_fn, ngen=1000, pmut=0.1, n=20): random.shuffle(states) return genetic_algorithm(states[:n], problem.value, ngen, pmut) + def genetic_algorithm(population, fitness_fn, ngen=1000, pmut=0.1): "[Fig. 4.8]" for i in range(ngen): new_population = [] for i in len(population): - fitnesses = map(fitness_fn, population) + fitnesses = list(map(fitness_fn, population)) p1, p2 = weighted_sample_with_replacement(population, fitnesses, 2) child = p1.mate(p2) if random.uniform(0, 1) < pmut: @@ -376,8 +412,11 @@ def genetic_algorithm(population, fitness_fn, ngen=1000, pmut=0.1): population = new_population return argmax(population, fitness_fn) + class GAState: + "Abstract class for individuals in a genetic search." + def __init__(self, genes): self.genes = genes @@ -396,7 +435,9 @@ def mutate(self): #______________________________________________________________________________ # Graphs and Graph Problems + class Graph: + """A graph connects nodes (verticies) by edges (links). Each edge can also have a length associated with it. The constructor call is something like: g = Graph({'A': {'B': 1, 'C': 2}) @@ -413,42 +454,48 @@ class Graph: def __init__(self, dict=None, directed=True): self.dict = dict or {} self.directed = directed - if not directed: self.make_undirected() + if not directed: + self.make_undirected() def make_undirected(self): "Make a digraph into an undirected graph by adding symmetric edges." - for a in self.dict.keys(): - for (b, distance) in self.dict[a].items(): + for a in list(self.dict.keys()): + for (b, distance) in list(self.dict[a].items()): self.connect1(b, a, distance) def connect(self, A, B, distance=1): """Add a link from A and B of given distance, and also add the inverse link if the graph is undirected.""" self.connect1(A, B, distance) - if not self.directed: self.connect1(B, A, distance) + if not self.directed: + self.connect1(B, A, distance) def connect1(self, A, B, distance): "Add a link from A to B of given distance, in one direction only." - self.dict.setdefault(A,{})[B] = distance + self.dict.setdefault(A, {})[B] = distance def get(self, a, b=None): """Return a link distance or a dict of {node: distance} entries. .get(a,b) returns the distance or None; .get(a) returns a dict of {node: distance} entries, possibly {}.""" links = self.dict.setdefault(a, {}) - if b is None: return links - else: return links.get(b) + if b is None: + return links + else: + return links.get(b) def nodes(self): "Return a list of nodes in the graph." - return self.dict.keys() + return list(self.dict.keys()) + def UndirectedGraph(dict=None): "Build a Graph where every edge (including future ones) goes both ways." return Graph(dict=dict, directed=False) -def RandomGraph(nodes=range(10), min_links=2, width=400, height=300, - curvature=lambda: random.uniform(1.1, 1.5)): + +def RandomGraph(nodes=list(range(10)), min_links=2, width=400, height=300, + curvature=lambda: random.uniform(1.1, 1.5)): """Construct a random graph, with the specified nodes, and random links. The nodes are laid out randomly on a (width x height) rectangle. Then each node is connected to the min_links nearest neighbors. @@ -457,16 +504,18 @@ def RandomGraph(nodes=range(10), min_links=2, width=400, height=300, where curvature() defaults to a random number between 1.1 and 1.5.""" g = UndirectedGraph() g.locations = {} - ## Build the cities + # Build the cities for node in nodes: g.locations[node] = (random.randrange(width), random.randrange(height)) - ## Build roads from each city to at least min_links nearest neighbors. + # Build roads from each city to at least min_links nearest neighbors. for i in range(min_links): for node in nodes: if len(g.get(node)) < min_links: here = g.locations[node] + def distance_to_node(n): - if n is node or g.get(node,n): return infinity + if n is node or g.get(node, n): + return infinity return distance(g.locations[n], here) neighbor = argmin(nodes, distance_to_node) d = distance(g.locations[neighbor], here) * curvature() @@ -488,11 +537,11 @@ def distance_to_node(n): R=dict(S=80), U=dict(V=142))) romania.locations = dict( - A=( 91, 492), B=(400, 327), C=(253, 288), D=(165, 299), + A=(91, 492), B=(400, 327), C=(253, 288), D=(165, 299), E=(562, 293), F=(305, 449), G=(375, 270), H=(534, 350), I=(473, 506), L=(165, 379), M=(168, 339), N=(406, 537), O=(131, 571), P=(320, 368), R=(233, 410), S=(207, 457), - T=( 94, 410), U=(456, 350), V=(509, 444), Z=(108, 531)) + T=(94, 410), U=(456, 350), V=(509, 444), Z=(108, 531)) australia = UndirectedGraph(dict( T=dict(), @@ -502,22 +551,25 @@ def distance_to_node(n): australia.locations = dict(WA=(120, 24), NT=(135, 20), SA=(135, 30), Q=(145, 20), NSW=(145, 32), T=(145, 42), V=(145, 37)) + class GraphProblem(Problem): + "The problem of searching a graph from one node to another." + def __init__(self, initial, goal, graph): Problem.__init__(self, initial, goal) self.graph = graph def actions(self, A): "The actions at a graph node are just its neighbors." - return self.graph.get(A).keys() + return list(self.graph.get(A).keys()) def result(self, state, action): "The result of going to a neighbor is just that neighbor." return action def path_cost(self, cost_so_far, A, action, B): - return cost_so_far + (self.graph.get(A,B) or infinity) + return cost_so_far + (self.graph.get(A, B) or infinity) def h(self, node): "h function is straight-line distance from a node's state to goal." @@ -529,7 +581,9 @@ def h(self, node): #______________________________________________________________________________ + class NQueensProblem(Problem): + """The problem of placing N queens on an NxN board with none attacking each other. A state is represented as an N-element array, where a value of r in the c-th entry means there is a queen at column c, @@ -538,6 +592,7 @@ class NQueensProblem(Problem): >>> depth_first_tree_search(NQueensProblem(8)) """ + def __init__(self, N): self.N = N self.initial = [None] * N @@ -545,7 +600,7 @@ def __init__(self, N): def actions(self, state): "In the leftmost empty column, try all non-conflicting rows." if state[-1] is not None: - return [] # All columns filled; no successors + return [] # All columns filled; no successors else: col = state.index(None) return [row for row in range(self.N) @@ -565,10 +620,10 @@ def conflicted(self, state, row, col): def conflict(self, row1, col1, row2, col2): "Would putting two queens in (row1, col1) and (row2, col2) conflict?" - return (row1 == row2 ## same row - or col1 == col2 ## same column - or row1-col1 == row2-col2 ## same \ diagonal - or row1+col1 == row2+col2) ## same / diagonal + return (row1 == row2 # same row + or col1 == col2 # same column + or row1-col1 == row2-col2 # same \ diagonal + or row1+col1 == row2+col2) # same / diagonal def goal_test(self, state): "Check if all columns filled, no conflicts." @@ -588,26 +643,33 @@ def goal_test(self, state): 'NODESW', 'HEFIYE', 'ONUDTK', 'TEVIGN', 'ANEDVZ', 'PINESH', 'ABILYT', 'GKYLEU'] + def random_boggle(n=4): """Return a random Boggle board of size n x n. We represent a board as a linear list of letters.""" cubes = [cubes16[i % 16] for i in range(n*n)] random.shuffle(cubes) - return map(random.choice, cubes) + return list(map(random.choice, cubes)) # The best 5x5 board found by Boyan, with our word list this board scores # 2274 words, for a score of 9837 boyan_best = list('RSTCSDEIAEGNLRPEATESMSSID') + def print_boggle(board): "Print the board in a 2-d array." - n2 = len(board); n = exact_sqrt(n2) + n2 = len(board) + n = exact_sqrt(n2) for i in range(n2): - if i % n == 0 and i > 0: print - if board[i] == 'Q': print 'Qu', - else: print str(board[i]) + ' ', - print + if i % n == 0 and i > 0: + print() + if board[i] == 'Q': + print('Qu', end=' ') + else: + print(str(board[i]) + ' ', end=' ') + print() + def boggle_neighbors(n2, cache={}): """Return a list of lists, where the i-th element is the list of indexes @@ -624,17 +686,24 @@ def boggle_neighbors(n2, cache={}): on_right = (i+1) % n == 0 if not on_top: neighbors[i].append(i - n) - if not on_left: neighbors[i].append(i - n - 1) - if not on_right: neighbors[i].append(i - n + 1) + if not on_left: + neighbors[i].append(i - n - 1) + if not on_right: + neighbors[i].append(i - n + 1) if not on_bottom: neighbors[i].append(i + n) - if not on_left: neighbors[i].append(i + n - 1) - if not on_right: neighbors[i].append(i + n + 1) - if not on_left: neighbors[i].append(i - 1) - if not on_right: neighbors[i].append(i + 1) + if not on_left: + neighbors[i].append(i + n - 1) + if not on_right: + neighbors[i].append(i + n + 1) + if not on_left: + neighbors[i].append(i - 1) + if not on_right: + neighbors[i].append(i + 1) cache[n2] = neighbors return neighbors + def exact_sqrt(n2): "If n2 is a perfect square, return its square root, else raise error." n = int(math.sqrt(n2)) @@ -643,10 +712,13 @@ def exact_sqrt(n2): #_____________________________________________________________________________ + class Wordlist: + """This class holds a list of words. You can use (word in wordlist) to check if a word is in the list, or wordlist.lookup(prefix) to see if prefix starts any of the words in the list.""" + def __init__(self, filename, min_len=3): lines = open(filename).read().upper().split() self.words = [word for word in lines if len(word) >= min_len] @@ -663,7 +735,8 @@ def lookup(self, prefix, lo=0, hi=None): words[i].startswith(prefix), or is None; the second is True iff prefix itself is in the Wordlist.""" words = self.words - if hi is None: hi = len(words) + if hi is None: + hi = len(words) i = bisect.bisect_left(words, prefix, lo, hi) if i < len(words) and words[i].startswith(prefix): return i, (words[i] == prefix) @@ -678,10 +751,12 @@ def __len__(self): #_____________________________________________________________________________ + class BoggleFinder: + """A class that allows you to find all the words in a Boggle board. """ - wordlist = None ## A class variable, holding a wordlist + wordlist = None # A class variable, holding a wordlist def __init__(self, board=None): if BoggleFinder.wordlist is None: @@ -714,7 +789,8 @@ def find(self, lo, hi, i, visited, prefix): self.found[prefix] = True visited.append(i) c = self.board[i] - if c == 'Q': c = 'QU' + if c == 'Q': + c = 'QU' prefix += c for j in self.neighbors[i]: self.find(wordpos, hi, j, visited, prefix) @@ -722,7 +798,7 @@ def find(self, lo, hi, i, visited, prefix): def words(self): "The words found." - return self.found.keys() + return list(self.found.keys()) scores = [0, 0, 0, 0, 1, 2, 3, 5] + [11] * 100 @@ -736,6 +812,7 @@ def __len__(self): #_____________________________________________________________________________ + def boggle_hill_climbing(board=None, ntimes=100, verbose=True): """Solve inverse Boggle by hill-climbing: find a high-scoring board by starting with a random one and changing it.""" @@ -748,24 +825,29 @@ def boggle_hill_climbing(board=None, ntimes=100, verbose=True): new = len(finder.set_board(board)) if new > best: best = new - if verbose: print best, _, board + if verbose: + print(best, _, board) else: - board[i] = oldc ## Change back + board[i] = oldc # Change back if verbose: print_boggle(board) return board, best + def mutate_boggle(board): i = random.randrange(len(board)) oldc = board[i] - board[i] = random.choice(random.choice(cubes16)) ##random.choice(boyan_best) + # random.choice(boyan_best) + board[i] = random.choice(random.choice(cubes16)) return i, oldc #______________________________________________________________________________ # Code to compare searchers on various problems. + class InstrumentedProblem(Problem): + """Delegates to a problem, and keeps statistics.""" def __init__(self, problem): @@ -801,6 +883,7 @@ def __repr__(self): return '<%4d/%4d/%4d/%s>' % (self.succs, self.goal_tests, self.states, str(self.found)[:4]) + def compare_searchers(problems, header, searchers=[breadth_first_tree_search, breadth_first_search, depth_first_graph_search, @@ -814,6 +897,7 @@ def do(searcher, problem): table = [[name(s)] + [do(s, p) for p in problems] for s in searchers] print_table(table, header) + def compare_graph_searchers(): """Prints a table of results like this: >>> compare_graph_searchers() @@ -827,7 +911,7 @@ def compare_graph_searchers(): compare_searchers(problems=[GraphProblem('A', 'B', romania), GraphProblem('O', 'N', romania), GraphProblem('Q', 'WA', australia)], - header=['Searcher', 'Romania(A, B)', 'Romania(O, N)', 'Australia']) + header=['Searcher', 'Romania(A, B)', 'Romania(O, N)', 'Australia']) #______________________________________________________________________________ @@ -860,10 +944,11 @@ def compare_graph_searchers(): 206 """ -__doc__ += random_tests(""" +__doc__ += """ +Random tests >>> ' '.join(f.words()) 'LID LARES DEAL LIE DIETS LIN LINT TIL TIN RATED ERAS LATEN DEAR TIE LINE INTER STEAL LATED LAST TAR SAL DITES RALES SAE RETS TAE RAT RAS SAT IDLE TILDES LEAST IDEAS LITE SATED TINED LEST LIT RASE RENTS TINEA EDIT EDITS NITES ALES LATE LETS RELIT TINES LEI LAT ELINT LATI SENT TARED DINE STAR SEAR NEST LITAS TIED SEAT SERAL RATE DINT DEL DEN SEAL TIER TIES NET SALINE DILATE EAST TIDES LINTER NEAR LITS ELINTS DENI RASED SERA TILE NEAT DERAT IDLEST NIDE LIEN STARED LIER LIES SETA NITS TINE DITAS ALINE SATIN TAS ASTER LEAS TSAR LAR NITE RALE LAS REAL NITER ATE RES RATEL IDEA RET IDEAL REI RATS STALE DENT RED IDES ALIEN SET TEL SER TEN TEA TED SALE TALE STILE ARES SEA TILDE SEN SEL ALINES SEI LASE DINES ILEA LINES ELD TIDE RENT DIEL STELA TAEL STALED EARL LEA TILES TILER LED ETA TALI ALE LASED TELA LET IDLER REIN ALIT ITS NIDES DIN DIE DENTS STIED LINER LASTED RATINE ERA IDLES DIT RENTAL DINER SENTI TINEAL DEIL TEAR LITER LINTS TEAL DIES EAR EAT ARLES SATE STARE DITS DELI DENTAL REST DITE DENTIL DINTS DITA DIET LENT NETS NIL NIT SETAL LATS TARE ARE SATI' >>> boggle_hill_climbing(list('ABCDEFGHI'), verbose=False) (['E', 'P', 'R', 'D', 'O', 'A', 'G', 'S', 'T'], 123) -""") +""" diff --git a/text.py b/aimaPy/text.py similarity index 80% rename from text.py rename to aimaPy/text.py index c71461514..5206b2de8 100644 --- a/text.py +++ b/aimaPy/text.py @@ -4,14 +4,16 @@ Then we show a very simple Information Retrieval system, and an example working on a tiny sample of Unix manual pages.""" -from utils import * -from learning import CountingProbDist +from . utils import * +from . learning import CountingProbDist from math import log, exp from collections import defaultdict import re -import search +from . import search + class UnigramTextModel(CountingProbDist): + """This is a discrete probability distribution over words, so you can add, sample, or get P[word], just like with CountingProbDist. You can also generate a random text n words long with P.samples(n)""" @@ -20,32 +22,36 @@ def samples(self, n): "Return a string of n words, random according to the model." return ' '.join(self.sample() for i in range(n)) + class NgramTextModel(CountingProbDist): + """This is a discrete probability distribution over n-tuples of words. You can add, sample or get P[(word1, ..., wordn)]. The method P.samples(n) builds up an n-word sequence; P.add and P.add_sequence add data.""" def __init__(self, n, observation_sequence=[]): - ## In addition to the dictionary of n-tuples, cond_prob is a - ## mapping from (w1, ..., wn-1) to P(wn | w1, ... wn-1) + # In addition to the dictionary of n-tuples, cond_prob is a + # mapping from (w1, ..., wn-1) to P(wn | w1, ... wn-1) CountingProbDist.__init__(self) self.n = n - self.cond_prob = defaultdict(CountingProbDist()) + self.cond_prob = defaultdict() self.add_sequence(observation_sequence) - ## __getitem__, top, sample inherited from CountingProbDist - ## Note they deal with tuples, not strings, as inputs + # __getitem__, top, sample inherited from CountingProbDist + # Note they deal with tuples, not strings, as inputs def add(self, ngram): """Count 1 for P[(w1, ..., wn)] and for P(wn | (w1, ..., wn-1)""" CountingProbDist.add(self, ngram) + if ngram[:-1] not in self.cond_prob: + self.cond_prob[ngram[:-1]] = CountingProbDist() self.cond_prob[ngram[:-1]].add(ngram[-1]) def add_sequence(self, words): """Add each of the tuple words[i:i+n], using a sliding window. Prefix some copies of the empty word, '', to make the start work.""" n = self.n - words = ['',] * (n-1) + words + words = ['', ] * (n-1) + words for i in range(len(words)-n): self.add(tuple(words[i:i+n])) @@ -57,7 +63,7 @@ def samples(self, nwords): output = [] for i in range(nwords): if nminus1gram not in self.cond_prob: - nminus1gram = ('',) * (n-1) # Cannot continue, so restart. + nminus1gram = ('',) * (n-1) # Cannot continue, so restart. wn = self.cond_prob[nminus1gram].sample() output.append(wn) nminus1gram = nminus1gram[1:] + (wn,) @@ -74,19 +80,20 @@ def viterbi_segment(text, P): n = len(text) words = [''] + list(text) best = [1.0] + [0.0] * n - ## Fill in the vectors best, words via dynamic programming + # Fill in the vectors best, words via dynamic programming for i in range(n+1): for j in range(0, i): w = text[j:i] if P[w] * best[i - len(w)] >= best[i]: best[i] = P[w] * best[i - len(w)] words[i] = w - ## Now recover the sequence of best words - sequence = []; i = len(words)-1 + # Now recover the sequence of best words + sequence = [] + i = len(words)-1 while i > 0: sequence[0:0] = [words[i]] i = i - len(words[i]) - ## Return sequence of best words and overall probability + # Return sequence of best words and overall probability return sequence, best[-1] @@ -95,6 +102,7 @@ def viterbi_segment(text, P): # TODO(tmrts): Expose raw index class IRSystem: + """A very simple Information Retrieval System, as discussed in Sect. 23.2. The constructor s = IRSystem('the a') builds an empty system with two stopwords. Next, index several documents with s.index_document(text, url). @@ -105,19 +113,20 @@ class IRSystem: def __init__(self, stopwords='the a of'): """Create an IR System. Optionally specify stopwords.""" - ## index is a map of {word: {docid: count}}, where docid is an int, - ## indicating the index into the documents list. + # index is a map of {word: {docid: count}}, where docid is an int, + # indicating the index into the documents list. update(self, index=defaultdict(lambda: defaultdict(int)), stopwords=set(words(stopwords)), documents=[]) def index_collection(self, filenames): "Index a whole collection of files." + prefix = os.path.dirname(__file__) for filename in filenames: - self.index_document(open(filename).read(), filename) + self.index_document(open(filename).read(), os.path.relpath(filename, prefix)) def index_document(self, text, url): "Index the text of a document." - ## For now, use first line for title + # For now, use first line for title title = text[:text.index('\n')].strip() docwords = words(text) docid = len(self.documents) @@ -137,12 +146,13 @@ def query(self, query_text, n=10): shortest = argmin(qwords, lambda w: len(self.index[w])) docs = self.index[shortest] results = [(sum([self.score(w, d) for w in qwords]), d) for d in docs] - results.sort(); results.reverse() + results.sort() + results.reverse() return results[:n] def score(self, word, docid): "Compute a score for this word on this docid." - ## There are many options; here we take a very simple approach + # There are many options; here we take a very simple approach return (math.log(1 + self.index[word][docid]) / math.log(1 + self.documents[docid].nwords)) @@ -150,27 +160,36 @@ def present(self, results): "Present the results as a list." for (score, d) in results: doc = self.documents[d] - print ("{:5.2}|{:25} | {}".format(100 * score, doc.url, doc.title[:45].expandtabs())) + print( + ("{:5.2}|{:25} | {}".format(100 * score, doc.url, doc.title[:45].expandtabs()))) def present_results(self, query_text, n=10): "Get results for the query and present them." self.present(self.query(query_text, n)) + class UnixConsultant(IRSystem): + """A trivial IR system over a small collection of Unix man pages.""" + def __init__(self): IRSystem.__init__(self, stopwords="how do i the a of") import os - mandir = '../aima-data/MAN/' + aima_root = os.path.dirname(__file__) + mandir = os.path.join(aima_root, 'aima-data/MAN/') man_files = [mandir + f for f in os.listdir(mandir) if f.endswith('.txt')] self.index_collection(man_files) + class Document: + """Metadata for a document: title and url; maybe add others later.""" + def __init__(self, title, url, nwords): update(self, title=title, url=url, nwords=nwords) + def words(text, reg=re.compile('[a-z0-9]+')): """Return a list of the words in text, ignoring punctuation and converting everything to lowercase (to canonicalize). @@ -179,6 +198,7 @@ def words(text, reg=re.compile('[a-z0-9]+')): """ return reg.findall(text.lower()) + def canonicalize(text): """Return a canonical text: only lowercase letters and blanks. >>> canonicalize("``EGAD!'' Edgar cried.") @@ -189,14 +209,15 @@ def canonicalize(text): #______________________________________________________________________________ -## Example application (not in book): decode a cipher. -## A cipher is a code that substitutes one character for another. -## A shift cipher is a rotation of the letters in the alphabet, -## such as the famous rot13, which maps A to N, B to M, etc. +# Example application (not in book): decode a cipher. +# A cipher is a code that substitutes one character for another. +# A shift cipher is a rotation of the letters in the alphabet, +# such as the famous rot13, which maps A to N, B to M, etc. alphabet = 'abcdefghijklmnopqrstuvwxyz' -#### Encoding +# Encoding + def shift_encode(plaintext, n): """Encode text with a shift cipher that moves each letter up by n letters. @@ -205,6 +226,7 @@ def shift_encode(plaintext, n): """ return encode(plaintext, alphabet[n:] + alphabet[:n]) + def rot13(plaintext): """Encode text by rotating letters by 13 spaces in the alphabet. >>> rot13('hello') @@ -214,12 +236,30 @@ def rot13(plaintext): """ return shift_encode(plaintext, 13) + +def translate(plaintext, function): + """Translate chars of a plaintext with the given function.""" + result = "" + for char in plaintext: + result += function(char) + return result + + +def maketrans(from_, to_): + """Create a translation table and return the proper function.""" + trans_table = {} + for n, char in enumerate(from_): + trans_table[char] = to_[n] + + return lambda char: trans_table.get(char, char) + + def encode(plaintext, code): "Encodes text, using a code which is a permutation of the alphabet." - from string import maketrans trans = maketrans(alphabet + alphabet.upper(), code + code.upper()) - return plaintext.translate(trans) + return translate(plaintext, trans) + def bigrams(text): """Return a list of pairs in text (a sequence of letters or words). @@ -230,12 +270,15 @@ def bigrams(text): """ return [text[i:i+2] for i in range(len(text) - 1)] -#### Decoding a Shift (or Caesar) Cipher +# Decoding a Shift (or Caesar) Cipher + class ShiftDecoder: + """There are only 26 possible encodings, so we can try all of them, and return the one with the highest probability, according to a bigram probability distribution.""" + def __init__(self, training_text): training_text = canonicalize(training_text) self.P2 = CountingProbDist(bigrams(training_text), default=1) @@ -252,16 +295,21 @@ def score(self, plaintext): def decode(self, ciphertext): "Return the shift decoding of text with the best score." - return max(all_shifts(ciphertext), self.score) + list_ = [(self.score(shift), shift) + for shift in all_shifts(ciphertext)] + return max(list_, key=lambda elm: elm[0])[1] + def all_shifts(text): "Return a list of all 26 possible encodings of text by a shift cipher." yield from (shift_encode(text, i) for i, _ in enumerate(alphabet)) -#### Decoding a General Permutation Cipher +# Decoding a General Permutation Cipher + class PermutationDecoder: + """This is a much harder problem than the shift decoder. There are 26! permutations, so we can't try them all. Instead we have to search. We want to search well, but there are many things to consider: @@ -275,10 +323,11 @@ class PermutationDecoder: represented as a letter-to-letter map; for example {'z': 'e'} to represent that 'z' will be translated to 'e'. """ + def __init__(self, training_text, ciphertext=None): self.Pwords = UnigramTextModel(words(training_text)) - self.P1 = UnigramTextModel(training_text) # By letter - self.P2 = NgramTextModel(2, training_text) # By letter pair + self.P1 = UnigramTextModel(training_text) # By letter + self.P2 = NgramTextModel(2, training_text) # By letter pair def decode(self, ciphertext): "Search for a decoding of the ciphertext." @@ -296,16 +345,18 @@ def score(self, code): sum([log(self.P2[b]) for b in bigrams(text)])) return exp(logP) + class PermutationDecoderProblem(search.Problem): + def __init__(self, initial=None, goal=None, decoder=None): self.initial = initial or {} self.decoder = decoder def actions(self, state): - ## Find the best + # Find the best p, plainchar = max([(self.decoder.P1[c], c) for c in alphabet if c not in state]) - succs = [extend(state, plainchar, cipherchar)] #???? + succs = [extend(state, plainchar, cipherchar)] # ???? def goal_test(self, state): "We're done when we get all 26 letters assigned." @@ -315,7 +366,8 @@ def goal_test(self, state): #______________________________________________________________________________ # TODO(tmrts): Set RNG seed to test random functions -__doc__ += random_tests(""" +__doc__ += """ +Random tests: ## Generate random text from the N-gram models >>> P1.samples(20) 'you thought known but were insides of see in depend by us dodecahedrons just but i words are instead degrees' @@ -325,4 +377,4 @@ def goal_test(self, state): >>> P3.samples(20) 'flatland by edwin a abbott 1884 to the wake of a certificate from nature herself proving the equal sided triangle' -""") +""" diff --git a/utils.py b/aimaPy/utils.py similarity index 91% rename from utils.py rename to aimaPy/utils.py index e73de1a31..df7cc09f2 100644 --- a/utils.py +++ b/aimaPy/utils.py @@ -19,9 +19,12 @@ infinity = float('inf') + class Struct: + """Create an instance with argument=value slots. This is for making a lightweight object whose class doesn't matter.""" + def __init__(self, **entries): self.__dict__.update(entries) @@ -33,7 +36,8 @@ def __cmp__(self, other): def __repr__(self): args = ['{!s}={!s}'.format(k, repr(v)) - for (k, v) in vars(self).items()] + for (k, v) in list(vars(self).items())] + def update(x, **entries): """Update a dict or an object with slots according to entries.""" @@ -49,6 +53,7 @@ def update(x, **entries): # NOTE: Sequence functions (count_if, find_if, every, some) take function # argument first (like reduce, filter, and map). + def removeall(item, seq): """Return a copy of seq (or string) with all occurences of item removed.""" if isinstance(seq, str): @@ -56,10 +61,12 @@ def removeall(item, seq): else: return [x for x in seq if x != item] + def unique(seq): """Remove duplicate elements from seq. Assumes hashable elements.""" return list(set(seq)) + def product(numbers): """Return the product of the numbers, e.g. product([2, 3, 10]) == 60""" result = 1 @@ -67,9 +74,11 @@ def product(numbers): result *= x return result + def count_if(predicate, seq): """Count the number of elements of seq for which the predicate is true.""" - return sum(map(lambda x: bool(predicate(x)), seq)) + return sum([bool(predicate(x)) for x in seq]) + def find_if(predicate, seq): """If there is an element of seq that satisfies predicate; return it.""" @@ -79,18 +88,23 @@ def find_if(predicate, seq): return None + def every(predicate, seq): """True if every element of seq satisfies predicate.""" return all(predicate(x) for x in seq) + def some(predicate, seq): """If some element x of seq satisfies predicate(x), return predicate(x).""" - elem = find_if(predicate,seq) + elem = find_if(predicate, seq) return predicate(elem) or False -# TODO: rename to is_in or possibily add 'identity' to function name to clarify intent +# TODO: rename to is_in or possibily add 'identity' to function name to +# clarify intent + + def isin(elt, seq): """Like (elt in seq), but compares with is, not ==.""" return any(x is elt for x in seq) @@ -103,15 +117,18 @@ def isin(elt, seq): # so there are three versions of argmin/argmax, depending on what you want to # do with ties: return the first one, return them all, or pick at random. + def argmin(seq, fn): return min(seq, key=fn) + def argmin_list(seq, fn): """Return a list of elements of seq[i] with the lowest fn(seq[i]) scores.’""" smallest_score = len(min(seq, key=fn)) return [elem for elem in seq if fn(elem) == smallest_score] + def argmin_gen(seq, fn): """Return a generator of elements of seq[i] with the lowest fn(seq[i]) scores.""" @@ -119,15 +136,18 @@ def argmin_gen(seq, fn): yield from (elem for elem in seq if fn(elem) == smallest_score) + def argmin_random_tie(seq, fn): """Return an element with lowest fn(seq[i]) score; break ties at random. Thus, for all s,f: argmin_random_tie(s, f) in argmin_list(s, f)""" return random.choice(argmin_gen(seq, fn)) + def argmax(seq, fn): """Return an element with highest fn(seq[i]) score; tie goes to first one.""" return max(seq, key=fn) + def argmax_list(seq, fn): """Return a list of elements of seq[i] with the highest fn(seq[i]) scores. Not good to use 'argmin_list(seq, lambda x: -fn(x))' as method breaks if fn is len""" @@ -135,12 +155,14 @@ def argmax_list(seq, fn): return [elem for elem in seq if fn(elem) == largest_score] + def argmax_gen(seq, fn): """Return a generator of elements of seq[i] with the highest fn(seq[i]) scores.""" largest_score = len(min(seq, key=fn)) yield from (elem for elem in seq if fn(elem) == largest_score) + def argmax_random_tie(seq, fn): "Return an element with highest fn(seq[i]) score; break ties at random." return argmin_random_tie(seq, lambda x: -fn(x)) @@ -148,37 +170,42 @@ def argmax_random_tie(seq, fn): #______________________________________________________________________________ # Statistical and mathematical functions + def histogram(values, mode=0, bin_function=None): """Return a list of (value, count) pairs, summarizing the input values. Sorted by increasing value, or if mode=1, by decreasing count. If bin_function is given, map it over values first.""" if bin_function: - values = map(bin_function, values) + values = list(map(bin_function, values)) bins = {} for val in values: bins[val] = bins.get(val, 0) + 1 if mode: - return sorted(bins.items(), key=lambda x: (x[1],x[0]), reverse=True) + return sorted(list(bins.items()), key=lambda x: (x[1], x[0]), reverse=True) else: return sorted(bins.items()) from math import log2 from statistics import mode, median, mean, stdev + def dotproduct(X, Y): """Return the sum of the element-wise product of vectors x and y.""" return sum([x * y for x, y in zip(X, Y)]) + def vector_add(a, b): """Component-wise addition of two vectors.""" return tuple(map(operator.add, a, b)) + def probability(p): "Return true with probability p." return p > random.uniform(0.0, 1.0) + def weighted_sample_with_replacement(seq, weights, n): """Pick n samples from seq at random, with replacement, with the probability of each element in proportion to its corresponding @@ -187,6 +214,7 @@ def weighted_sample_with_replacement(seq, weights, n): return [sample() for _ in range(n)] + def weighted_sampler(seq, weights): "Return a random-sample function that picks from seq weighted by weights." totals = [] @@ -195,6 +223,7 @@ def weighted_sampler(seq, weights): return lambda: seq[bisect.bisect(totals, random.uniform(0, totals[-1]))] + def num_or_str(x): """The argument is a string; convert to a number if possible, or strip it.""" try: @@ -205,62 +234,74 @@ def num_or_str(x): except ValueError: return str(x).strip() + def normalize(numbers): """Multiply each number by a constant such that the sum is 1.0""" total = float(sum(numbers)) return [n / total for n in numbers] + def clip(x, lowest, highest): """Return x clipped to the range [lowest..highest].""" return max(lowest, min(x, highest)) #______________________________________________________________________________ -## OK, the following are not as widely useful utilities as some of the other -## functions here, but they do show up wherever we have 2D grids: Wumpus and -## Vacuum worlds, TicTacToe and Checkers, and markov decision Processes. +# OK, the following are not as widely useful utilities as some of the other +# functions here, but they do show up wherever we have 2D grids: Wumpus and +# Vacuum worlds, TicTacToe and Checkers, and markov decision Processes. orientations = [(1, 0), (0, 1), (-1, 0), (0, -1)] + def turn_heading(heading, inc, headings=orientations): return headings[(headings.index(heading) + inc) % len(headings)] + def turn_right(heading): return turn_heading(heading, -1) + def turn_left(heading): return turn_heading(heading, +1) + def Point(x, y): return (x, y) + def point_x(point): return point[0] + def point_y(point): return point[1] + def distance(a, b): "The distance between two (x, y) points." ax, ay = a bx, by = b return math.hypot((ax - bx), (ay - by)) + def distance2(a, b): "The square of the distance between two (x, y) points." ax, ay = a bx, by = b return (ax - bx)**2 + (ay - by)**2 + def vector_clip(vector, lowest, highest): """Return vector, except if any element is less than the corresponding value of lowest or more than the corresponding value of highest, clip to those values. """ - return type(vector)(map(clip, vector, lowest, highest)) + return type(vector)(list(map(clip, vector, lowest, highest))) #______________________________________________________________________________ # Misc Functions + def printf(format_str, *args): """Format args with the first argument as format string, and write. Return the last arg, or format itself if there are no args.""" @@ -268,6 +309,7 @@ def printf(format_str, *args): return args[-1] if args else format_str + def caller(n=1): """Return the name of the calling function n levels up in the frame stack.""" import inspect @@ -275,6 +317,8 @@ def caller(n=1): return inspect.getouterframes(inspect.currentframe())[n][3] # TODO: Use functools.lru_cache memoization decorator + + def memoize(fn, slot=None): """Memoize fn: make it remember the computed value for any argument list. If slot is specified, store result in that slot of first argument. @@ -289,7 +333,7 @@ def memoized_fn(obj, *args): return val else: def memoized_fn(*args): - if not memoized_fn.cache.has_key(args): + if args not in memoized_fn.cache: memoized_fn.cache[args] = fn(*args) return memoized_fn.cache[args] @@ -304,14 +348,17 @@ def name(obj): or getattr(getattr(obj, '__class__', 0), '__name__', 0) or str(obj)) + def isnumber(x): "Is x a number? We say it is if it has a __int__ method." return hasattr(x, '__int__') + def issequence(x): "Is x a sequence? We say it is if it has a __getitem__ method." return hasattr(x, '__getitem__') + def print_table(table, header=None, sep=' ', numfmt='%g'): """Print a list of lists as a table, so that columns line up nicely. header, if specified, will be printed as the first row. @@ -324,28 +371,31 @@ def print_table(table, header=None, sep=' ', numfmt='%g'): table.insert(0, header) table = [[numfmt.format(x) if isnumber(x) else x for x in row] - for row in table] + for row in table] - maxlen = lambda seq: max(map(len, seq)) + maxlen = lambda seq: max(list(map(len, seq))) - sizes = map(maxlen, zip(*[map(str, row) for row in table])) + sizes = list( + map(maxlen, list(zip(*[list(map(str, row)) for row in table])))) for row in table: print(sep.join(getattr(str(x), j)(size) - for (j, size, x) in zip(justs, sizes, row))) + for (j, size, x) in zip(justs, sizes, row))) + def AIMAFile(components, mode='r'): "Open a file based at the AIMA root directory." - import utils - aima_root = os.path.dirname(utils.__file__) + aima_root = os.path.dirname(__file__) aima_file = os.path.join(aima_root, *components) return open(aima_file) + def DataFile(name, mode='r'): "Return a file in the AIMA /data directory." - return AIMAFile(['..', 'data', name], mode) + return AIMAFile(['aima-data', name], mode) + def unimplemented(): "Use this as a stub for not-yet-implemented functions." @@ -355,7 +405,10 @@ def unimplemented(): # Queues: Stack, FIFOQueue, PriorityQueue # TODO: Use queue.Queue + + class Queue: + """Queue is an abstract class/interface. There are three types: Stack(): A Last In First Out Queue. FIFOQueue(): A First In First Out Queue. @@ -368,20 +421,27 @@ class Queue: item in q -- does q contain item? Note that isinstance(Stack(), Queue) is false, because we implement stacks as lists. If Python ever gets interfaces, Queue will be an interface.""" + def __init__(self): raise NotImplementedError def extend(self, items): - for item in items: self.append(item) + for item in items: + self.append(item) + def Stack(): """Return an empty list, suitable as a Last-In-First-Out Queue.""" return [] + class FIFOQueue(Queue): + """A First-In-First-Out Queue.""" + def __init__(self): - self.A = []; self.start = 0 + self.A = [] + self.start = 0 def append(self, item): self.A.append(item) @@ -404,11 +464,15 @@ def __contains__(self, item): return item in self.A[self.start:] # TODO: Use queue.PriorityQueue + + class PriorityQueue(Queue): + """A queue in which the minimum (or maximum) element (as determined by f and order) is returned first. If order is min, the item with minimum f(x) is returned first; if order is max, then it is the item with maximum f(x). Also supports dict-like lookup.""" + def __init__(self, order=min, f=lambda x: x): update(self, A=[], order=order, f=f) @@ -437,8 +501,7 @@ def __delitem__(self, key): if item == key: self.A.pop(i) -## Fig: The idea is we can define things like Fig[3,10] later. -## Alas, it is Fig[3,10] not Fig[3.10], because that would be the same -## as Fig[3.1] +# Fig: The idea is we can define things like Fig[3,10] later. +# Alas, it is Fig[3,10] not Fig[3.10], because that would be the same +# as Fig[3.1] Fig = {} - diff --git a/planning.py b/planning.py deleted file mode 100644 index 331193bd7..000000000 --- a/planning.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Planning (Chapters 10-11) -""" - -from __future__ import generators -from utils import * -import agents -import math, random, sys, time, bisect, string diff --git a/probability_test.py b/probability_test.py deleted file mode 100644 index fb18a273e..000000000 --- a/probability_test.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from probability import * - -def tests(): - cpt = burglary.variable_node('Alarm').cpt - parents = ['Burglary', 'Earthquake'] - event = {'Burglary': True, 'Earthquake': True} - assert cpt.p(True, parents, event) == 0.95 - event = {'Burglary': False, 'Earthquake': True} - assert cpt.p(False, parents, event) == 0.71 - assert BoolCPT({T: 0.2, F: 0.625}).p(False, ['Burglary'], event) == 0.375 - assert BoolCPT(0.75).p(False, [], {}) == 0.25 - cpt = BoolCPT({True: 0.2, False: 0.7}) - assert cpt.rand(['A'], {'A': True}) in [True, False] - cpt = BoolCPT({(True, True): 0.1, (True, False): 0.3, - (False, True): 0.5, (False, False): 0.7}) - assert cpt.rand(['A', 'B'], {'A': True, 'B': False}) in [True, False] - #enumeration_ask('Earthquake', {}, burglary) - - s = {'A': True, 'B': False, 'C': True, 'D': False} - assert consistent_with(s, {}) - assert consistent_with(s, s) - assert not consistent_with(s, {'A': False}) - assert not consistent_with(s, {'D': True}) - - seed(21); p = rejection_sampling('Earthquake', {}, burglary, 1000) - assert p[True], p[False] == (0.001, 0.999) - - seed(71); p = likelihood_weighting('Earthquake', {}, burglary, 1000) - assert p[True], p[False] == (0.002, 0.998) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..9af7e6f11 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..4d9a11ede --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + +setup( + name='aimaPy', + version='1.0', + description='Python code for the book Artificial Intelligence: A Modern Approach.', + long_description='Python code for the book Artificial Intelligence: A Modern Approach.', + author='Peter Norvig', + author_email='peter@norvig.com', + url='https://github.com/aimacode/aima-python', + license="MIT", + platforms="all", + packages=['aimaPy'], + include_package_data=True, + + setup_requires=['pytest-runner'], + tests_require=['pytest'], +) \ No newline at end of file diff --git a/tests/probability_test.py b/tests/probability_test.py new file mode 100644 index 000000000..eb587dfa1 --- /dev/null +++ b/tests/probability_test.py @@ -0,0 +1,36 @@ +import pytest +from aimaPy.probability import * + + +def tests(): + cpt = burglary.variable_node('Alarm') + parents = ['Burglary', 'Earthquake'] + event = {'Burglary': True, 'Earthquake': True} + assert cpt.p(True, event) == 0.95 + event = {'Burglary': False, 'Earthquake': True} + assert cpt.p(False, event) == 0.71 + # assert BoolCPT({T: 0.2, F: 0.625}).p(False, ['Burglary'], event) == 0.375 + # assert BoolCPT(0.75).p(False, [], {}) == 0.25 + # cpt = BoolCPT({True: 0.2, False: 0.7}) + # assert cpt.rand(['A'], {'A': True}) in [True, False] + # cpt = BoolCPT({(True, True): 0.1, (True, False): 0.3, + # (False, True): 0.5, (False, False): 0.7}) + # assert cpt.rand(['A', 'B'], {'A': True, 'B': False}) in [True, False] + # #enumeration_ask('Earthquake', {}, burglary) + + s = {'A': True, 'B': False, 'C': True, 'D': False} + assert consistent_with(s, {}) + assert consistent_with(s, s) + assert not consistent_with(s, {'A': False}) + assert not consistent_with(s, {'D': True}) + + random.seed(21) + p = rejection_sampling('Earthquake', {}, burglary, 1000) + assert p[True], p[False] == (0.001, 0.999) + + random.seed(71) + p = likelihood_weighting('Earthquake', {}, burglary, 1000) + assert p[True], p[False] == (0.002, 0.998) + +if __name__ == '__main__': + pytest.main() diff --git a/tests/text_test.py b/tests/text_test.py new file mode 100644 index 000000000..4132682d2 --- /dev/null +++ b/tests/text_test.py @@ -0,0 +1,163 @@ +import pytest + +from aimaPy.text import * + +from random import choice +from math import isclose + + +def test_unigram_text_model(): + flatland = DataFile("EN-text/flatland.txt").read() + wordseq = words(flatland) + P = UnigramTextModel(wordseq) + + s, p = viterbi_segment('itiseasytoreadwordswithoutspaces', P) + + assert s == [ + 'it', 'is', 'easy', 'to', 'read', 'words', 'without', 'spaces'] + + +def test_shift_encoding(): + code = shift_encode("This is a secret message.", 17) + + assert code == 'Kyzj zj r jvtivk dvjjrxv.' + + +def test_shift_decoding(): + flatland = DataFile("EN-text/flatland.txt").read() + ring = ShiftDecoder(flatland) + msg = ring.decode('Kyzj zj r jvtivk dvjjrxv.') + + assert msg == 'This is a secret message.' + + +def test_rot13_decoding(): + flatland = DataFile("EN-text/flatland.txt").read() + ring = ShiftDecoder(flatland) + msg = ring.decode(rot13('Hello, world!')) + + assert msg == 'Hello, world!' + + +def test_counting_probability_distribution(): + D = CountingProbDist() + + for i in range(10000): + D.add(random.choice('123456')) + + ps = [D[n] for n in '123456'] + + assert 1/7 <= min(ps) <= max(ps) <= 1/5 + + +def test_ngram_models(): + flatland = DataFile("EN-text/flatland.txt").read() + wordseq = words(flatland) + P1 = UnigramTextModel(wordseq) + P2 = NgramTextModel(2, wordseq) + P3 = NgramTextModel(3, wordseq) + + # The most frequent entries in each model + assert P1.top(10) == [(2081, 'the'), (1479, 'of'), (1021, 'and'), (1008, 'to'), (850, 'a'), + (722, 'i'), (640, 'in'), (478, 'that'), (399, 'is'), (348, 'you')] + + assert P2.top(10) == [(368, ('of', 'the')), (152, ('to', 'the')), (152, ('in', 'the')), (86, ('of', 'a')), + (80, ('it', 'is')), (71, + ('by', 'the')), (68, ('for', 'the')), + (68, ('and', 'the')), (62, ('on', 'the')), (60, ('to', 'be'))] + + assert P3.top(10) == [(30, ('a', 'straight', 'line')), (19, ('of', 'three', 'dimensions')), + (16, ('the', 'sense', 'of')), (13, + ('by', 'the', 'sense')), + (13, ('as', 'well', 'as')), (12, + ('of', 'the', 'circles')), + (12, ('of', 'sight', 'recognition') + ), (11, ('the', 'number', 'of')), + (11, ('that', 'i', 'had')), (11, ('so', 'as', 'to'))] + + assert isclose(P1['the'], 0.0611, rel_tol=0.001) + + assert isclose(P2['of', 'the'], 0.0108, rel_tol=0.01) + + assert isclose(P3['', '', 'but'], 0.0, rel_tol=0.001) + assert isclose(P3['', '', 'but'], 0.0, rel_tol=0.001) + assert isclose(P3['so', 'as', 'to'], 0.000323, rel_tol=0.001) + + assert P2.cond_prob.get(('went',)) is None + + assert P3.cond_prob['in', 'order'].dictionary == {'to': 6} + + +def test_ir_system(): + from collections import namedtuple + Results = namedtuple('IRResults', ['score', 'url']) + + uc = UnixConsultant() + + def verify_query(query, expected): + assert len(expected) == len(query) + + for expected, (score, d) in zip(expected, query): + doc = uc.documents[d] + assert "{0:.2f}".format( + expected.score) == "{0:.2f}".format(score * 100) + assert expected.url == doc.url + + return True + + q1 = uc.query("how do I remove a file") + assert verify_query(q1, [ + Results(76.83, "aima-data/MAN/rm.txt"), + Results(67.83, "aima-data/MAN/tar.txt"), + Results(67.79, "aima-data/MAN/cp.txt"), + Results(66.58, "aima-data/MAN/zip.txt"), + Results(64.58, "aima-data/MAN/gzip.txt"), + Results(63.74, "aima-data/MAN/pine.txt"), + Results(62.95, "aima-data/MAN/shred.txt"), + Results(57.46, "aima-data/MAN/pico.txt"), + Results(43.38, "aima-data/MAN/login.txt"), + Results(41.93, "aima-data/MAN/ln.txt"), + ]) + + q2 = uc.query("how do I delete a file") + assert verify_query(q2, [ + Results(75.47, "aima-data/MAN/diff.txt"), + Results(69.12, "aima-data/MAN/pine.txt"), + Results(63.56, "aima-data/MAN/tar.txt"), + Results(60.63, "aima-data/MAN/zip.txt"), + Results(57.46, "aima-data/MAN/pico.txt"), + Results(51.28, "aima-data/MAN/shred.txt"), + Results(26.72, "aima-data/MAN/tr.txt"), + ]) + + q3 = uc.query("email") + assert verify_query(q3, [ + Results(18.39, "aima-data/MAN/pine.txt"), + Results(12.01, "aima-data/MAN/info.txt"), + Results(9.89, "aima-data/MAN/pico.txt"), + Results(8.73, "aima-data/MAN/grep.txt"), + Results(8.07, "aima-data/MAN/zip.txt"), + ]) + + q4 = uc.query("word count for files") + assert verify_query(q4, [ + Results(128.15, "aima-data/MAN/grep.txt"), + Results(94.20, "aima-data/MAN/find.txt"), + Results(81.71, "aima-data/MAN/du.txt"), + Results(55.45, "aima-data/MAN/ps.txt"), + Results(53.42, "aima-data/MAN/more.txt"), + Results(42.00, "aima-data/MAN/dd.txt"), + Results(12.85, "aima-data/MAN/who.txt"), + ]) + + q5 = uc.query("learn: date") + assert verify_query(q5, []) + + q6 = uc.query("2003") + assert verify_query(q6, [ + Results(14.58, "aima-data/MAN/pine.txt"), + Results(11.62, "aima-data/MAN/jar.txt"), + ]) + +if __name__ == '__main__': + pytest.main() diff --git a/utils_test.py b/tests/utils_test.py similarity index 88% rename from utils_test.py rename to tests/utils_test.py index eb83c5c49..d09587372 100644 --- a/utils_test.py +++ b/tests/utils_test.py @@ -1,104 +1,134 @@ import pytest -from utils import * +from aimaPy.utils import * + def test_struct_initialization(): s = Struct(a=1, b=2) assert s.a == 1 assert s.b == 2 + def test_struct_assignment(): s = Struct(a=1) s.a = 3 assert s.a == 3 + def test_update_dict(): assert update({'a': 1}, a=10, b=20) == {'a': 10, 'b': 20} assert update({}, a=5) == {'a': 5} + def test_update_struct(): assert update(Struct(a=1), a=30, b=20).__cmp__(Struct(a=30, b=20)) assert update(Struct(), a=10).__cmp__(Struct(a=10)) + def test_removeall_list(): assert removeall(4, []) == [] assert removeall(4, [1, 2, 3, 4]) == [1, 2, 3] assert removeall(4, [4, 1, 4, 2, 3, 4, 4]) == [1, 2, 3] + def test_removeall_string(): assert removeall('s', '') == '' - assert removeall('s', 'This is a test. Was a test.') == 'Thi i a tet. Wa a tet.' + assert removeall( + 's', 'This is a test. Was a test.') == 'Thi i a tet. Wa a tet.' + def test_unique(): assert unique([1, 2, 3, 2, 1]) == [1, 2, 3] assert unique([1, 5, 6, 7, 6, 5]) == [1, 5, 6, 7] + def test_product(): - assert product([1,2,3,4]) == 24 - assert product(range(1, 11)) == 3628800 + assert product([1, 2, 3, 4]) == 24 + assert product(list(range(1, 11))) == 3628800 + def test_find_if(): assert find_if(callable, [1, 2, 3]) == None assert find_if(callable, [3, min, max]) == min + def test_count_if(): assert count_if(callable, [42, None, max, min]) == 2 is_odd = lambda x: x % 2 assert count_if(is_odd, []) == 0 assert count_if(is_odd, [1, 2, 3, 4, 5]) == 3 + def test_every(): assert every(callable, [min, max]) == 1 assert every(callable, [min, 3]) == 0 + def test_some(): assert some(callable, [min, 3]) == 1 assert some(callable, [2, 3]) == 0 + def test_isin(): e = [] assert isin(e, [1, e, 3]) == True assert isin(e, [1, [], 3]) == False + def test_argmin(): assert argmin([-2, 1], lambda x: x**2) == 1 + def test_argmin_list(): assert argmin_list(['one', 'to', 'three', 'or'], len) == ['to', 'or'] + def test_argmin_gen(): - assert [i for i in argmin_gen(['one', 'to', 'three', 'or'], len)] == ['to', 'or'] + assert [i for i in argmin_gen(['one', 'to', 'three', 'or'], len)] == [ + 'to', 'or'] + def test_argmax(): assert argmax([-2, 1], lambda x: x**2) == -2 assert argmax(['one', 'to', 'three'], len) == 'three' + def test_argmax_list(): - assert argmax_list(['one', 'three', 'seven'], lambda x: len(x)) == ['three', 'seven'] + assert argmax_list(['one', 'three', 'seven'], lambda x: len(x)) == [ + 'three', 'seven'] + def test_argmax_gen(): assert argmax_list(['one', 'three', 'seven'], len) == ['three', 'seven'] + def test_dotproduct(): assert dotproduct([1, 2, 3], [1000, 100, 10]) == 1230 + def test_vector_add(): assert vector_add((0, 1), (8, 9)) == (8, 10) + def test_num_or_str(): assert num_or_str('42') == 42 assert num_or_str(' 42x ') == '42x' + def test_normalize(): - assert normalize([1,2,1]) == [0.25, 0.5, 0.25] + assert normalize([1, 2, 1]) == [0.25, 0.5, 0.25] + def test_clip(): assert [clip(x, 0, 1) for x in [-1, 0.5, 10]] == [0, 0.5, 1] + def test_vector_clip(): assert vector_clip((-1, 10), (0, 0), (9, 9)) == (0, 9) + def test_caller(): assert caller(0) == 'caller' + def f(): return caller() assert f() == 'f' diff --git a/text_test.py b/text_test.py deleted file mode 100644 index e47888e02..000000000 --- a/text_test.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest - -from text import * - -from random import choice -from math import isclose - -def test_unigram_text_model(): - flatland = DataFile("aima-data/EN-text/flatland.txt").read() - wordseq = words(flatland) - P = UnigramTextModel(wordseq) - - s, p = viterbi_segment('itiseasytoreadwordswithoutspaces', P) - - assert s == ['it', 'is', 'easy', 'to', 'read', 'words', 'without', 'spaces'] - -def test_shift_encoding(): - code = shift_encode("This is a secret message.", 17) - - assert code == 'Kyzj zj r jvtivk dvjjrxv.' - -def test_shift_decoding(): - code = shift_encode("This is a secret message.", 17) - - ring = ShiftDecoder(flatland) - msg = ring.decode('Kyzj zj r jvtivk dvjjrxv.') - - assert msg == 'This is a secret message.' - -def test_rot13_decoding(): - msg = ring.decode(rot13('Hello, world!')) - - assert msg == 'Hello, world!' - -def test_counting_probability_distribution(): - D = CountingProbDist() - - for i in range(10000): - D.add(random.choice('123456')) - - ps = [D[n] for n in '123456'] - - assert 1/7 <= min(ps) <= max(ps) <= 1/5 - -def test_ngram_models(): - flatland = DataFile("aima-data/EN-text/flatland.txt").read() - wordseq = words(flatland) - P1 = UnigramTextModel(wordseq) - P2 = NgramTextModel(2, wordseq) - P3 = NgramTextModel(3, wordseq) - - ## The most frequent entries in each model - assert P1.top(10) == [(2081, 'the'), (1479, 'of'), (1021, 'and'), (1008, 'to'), (850, 'a'), - (722, 'i'), (640, 'in'), (478, 'that'), (399, 'is'), (348, 'you')] - - assert P2.top(10) == [(368, ('of', 'the')), (152, ('to', 'the')), (152, ('in', 'the')), (86, ('of', 'a')), - (80, ('it', 'is' )), (71, ('by', 'the' )), (68, ('for', 'the' )), - (68, ('and', 'the' )), (62, ('on', 'the' )), (60, ('to', 'be'))] - - assert P3.top(10) == [(30, ('a', 'straight', 'line')), (19, ('of', 'three', 'dimensions')), - (16, ('the', 'sense', 'of' )), (13, ('by', 'the', 'sense' )), - (13, ('as', 'well', 'as' )), (12, ('of', 'the', 'circles' )), - (12, ('of', 'sight', 'recognition' )), (11, ('the', 'number', 'of' )), - (11, ('that', 'i', 'had' )), (11, ('so', 'as', 'to'))] - - - assert isclose(P1['the'], 0.0611) - - assert isclose(P2['of', 'the'], 0.0108) - - assert isclose(P3['', '', 'but'], 0.0) - assert isclose(P3['', '', 'but'], 0.0) - assert isclose(P3['so', 'as', 'to'], 0.000323) - - assert not P2.cond_prob['went',].dictionary - - assert P3.cond_prob['in','order'].dictionary == {'to': 6} - -def test_ir_system(): - from collections import namedtuple - Results = namedtuple('IRResults', ['score', 'url']) - - uc = UnixConsultant() - - def verify_query(query, expected): - assert len(expected) == len(query) - - for expected, (score, d) in zip(expected, query): - doc = uc.documents[d] - - assert expected.score == score * 100 - assert expected.url == doc.url - - q1 = uc.query("how do I remove a file") - assert verify_query(q1, [ - Results(76.83, "../aima-data/MAN/rm.txt"), - Results(67.83, "../aima-data/MAN/tar.txt"), - Results(67.79, "../aima-data/MAN/cp.txt"), - Results(66.58, "../aima-data/MAN/zip.txt"), - Results(64.58, "../aima-data/MAN/gzip.txt"), - Results(63.74, "../aima-data/MAN/pine.txt"), - Results(62.95, "../aima-data/MAN/shred.txt"), - Results(57.46, "../aima-data/MAN/pico.txt"), - Results(43.38, "../aima-data/MAN/login.txt"), - Results(41.93, "../aima-data/MAN/ln.txt"), - ]) - - q2 = uc.query("how do I delete a file") - assert verify_query(q2, [ - Results(75.47, "../aima-data/MAN/diff.txt"), - Results(69.12, "../aima-data/MAN/pine.txt"), - Results(63.56, "../aima-data/MAN/tar.txt"), - Results(60.63, "../aima-data/MAN/zip.txt"), - Results(57.46, "../aima-data/MAN/pico.txt"), - Results(51.28, "../aima-data/MAN/shred.txt"), - Results(26.72, "../aima-data/MAN/tr.txt"), - ]) - - q3 = uc.query("email") - assert verify_query(q3, [ - Results(18.39, "../aima-data/MAN/pine.txt"), - Results(12.01, "../aima-data/MAN/info.txt"), - Results(9.89, "../aima-data/MAN/pico.txt"), - Results(8.73, "../aima-data/MAN/grep.txt"), - Results(8.07, "../aima-data/MAN/zip.txt"), - ]) - - q4 = uc.query("word countrs for files") - assert verify_query(q4, [ - Results(112.38, "../aima-data/MAN/grep.txt"), - Results(101.84, "../aima-data/MAN/wc.txt"), - Results(82.46, "../aima-data/MAN/find.txt"), - Results(74.64, "../aima-data/MAN/du.txt"), - ]) - - q5 = uc.query("learn: date") - assert verify_query(q5, []) - - q6 = uc.query("2003") - assert verify_query(q6, [ - Results(14.58, "../aima-data/MAN/pine.txt"), - Results(11.62, "../aima-data/MAN/jar.txt"), - ]) - -if __name__ == '__main__': - pytest.main()