diff --git a/README.md b/README.md index a7b5d1667..1caf57863 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 5.3 | Minimax-Decision | `minimax_decision` | [`games.py`][games] | Done | Included | | 5.7 | Alpha-Beta-Search | `alphabeta_search` | [`games.py`][games] | Done | Included | | 6 | CSP | `CSP` | [`csp.py`][csp] | Done | Included | -| 6.3 | AC-3 | `AC3` | [`csp.py`][csp] | Done | | +| 6.3 | AC-3 | `AC3` | [`csp.py`][csp] | Done | Included | | 6.5 | Backtracking-Search | `backtracking_search` | [`csp.py`][csp] | Done | Included | | 6.8 | Min-Conflicts | `min_conflicts` | [`csp.py`][csp] | Done | Included | | 6.11 | Tree-CSP-Solver | `tree_csp_solver` | [`csp.py`][csp] | Done | Included | @@ -118,7 +118,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 11.8 | Angelic-Search | | | | | | 11.10 | Doubles-tennis | `double_tennis_problem` | [`planning.py`][planning] | Done | Included | | 13 | Discrete Probability Distribution | `ProbDist` | [`probability.py`][probability] | Done | Included | -| 13.1 | DT-Agent | `DTAgent` | [`probability.py`][probability] | | | +| 13.1 | DT-Agent | `DTAgent` | [`probability.py`][probability] | Done | Included | | 14.9 | Enumeration-Ask | `enumeration_ask` | [`probability.py`][probability] | Done | Included | | 14.11 | Elimination-Ask | `elimination_ask` | [`probability.py`][probability] | Done | Included | | 14.13 | Prior-Sample | `prior_sample` | [`probability.py`][probability] | Done | Included | @@ -133,7 +133,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 17.7 | Policy-Iteration | `policy_iteration` | [`mdp.py`][mdp] | Done | Included | | 17.9 | POMDP-Value-Iteration | `pomdp_value_iteration` | [`mdp.py`][mdp] | Done | Included | | 18.5 | Decision-Tree-Learning | `DecisionTreeLearner` | [`learning.py`][learning] | Done | Included | -| 18.8 | Cross-Validation | `cross_validation` | [`learning.py`][learning] | | | +| 18.8 | Cross-Validation | `cross_validation` | [`learning.py`][learning]\* | | | | 18.11 | Decision-List-Learning | `DecisionListLearner` | [`learning.py`][learning]\* | | | | 18.24 | Back-Prop-Learning | `BackPropagationLearner` | [`learning.py`][learning] | Done | Included | | 18.34 | AdaBoost | `AdaBoost` | [`learning.py`][learning] | Done | Included | diff --git a/agents.py b/agents.py index eb085757a..f7ccb255b 100644 --- a/agents.py +++ b/agents.py @@ -131,7 +131,16 @@ def program(percept): def RandomAgentProgram(actions): - """An agent that chooses an action at random, ignoring all percepts.""" + """An agent that chooses an action at random, ignoring all percepts. + >>> list = ['Right', 'Left', 'Suck', 'NoOp'] + >>> program = RandomAgentProgram(list) + >>> agent = Agent(program) + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1, 0): 'Clean' , (0, 0): 'Clean'} + True + """ return lambda percept: random.choice(actions) # ______________________________________________________________________________ @@ -171,7 +180,14 @@ def rule_match(state, rules): def RandomVacuumAgent(): - """Randomly choose one of the actions from the vacuum environment.""" + """Randomly choose one of the actions from the vacuum environment. + >>> agent = RandomVacuumAgent() + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + True + """ return Agent(RandomAgentProgram(['Right', 'Left', 'Suck', 'NoOp'])) @@ -192,7 +208,14 @@ def TableDrivenVacuumAgent(): def ReflexVacuumAgent(): - """A reflex agent for the two-state vacuum environment. [Figure 2.8]""" + """A reflex agent for the two-state vacuum environment. [Figure 2.8] + >>> agent = ReflexVacuumAgent() + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + True + """ def program(percept): location, status = percept if status == 'Dirty': @@ -205,7 +228,14 @@ def program(percept): def ModelBasedVacuumAgent(): - """An agent that keeps track of what locations are clean or dirty.""" + """An agent that keeps track of what locations are clean or dirty. + >>> agent = ModelBasedVacuumAgent() + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + True + """ model = {loc_A: None, loc_B: None} def program(percept): @@ -342,6 +372,22 @@ def __init__(self, direction): self.direction = direction def __add__(self, heading): + """ + >>> d = Direction('right') + >>> l1 = d.__add__(Direction.L) + >>> l2 = d.__add__(Direction.R) + >>> l1.direction + 'up' + >>> l2.direction + 'down' + >>> d = Direction('down') + >>> l1 = d.__add__('right') + >>> l2 = d.__add__('left') + >>> l1.direction == Direction.L + True + >>> l2.direction == Direction.R + True + """ if self.direction == self.R: return{ self.R: Direction(self.D), @@ -364,6 +410,16 @@ def __add__(self, heading): }.get(heading, None) def move_forward(self, from_location): + """ + >>> d = Direction('up') + >>> l1 = d.move_forward((0, 0)) + >>> l1 + (0, -1) + >>> d = Direction(Direction.R) + >>> l1 = d.move_forward((0, 0)) + >>> l1 + (1, 0) + """ x, y = from_location if self.direction == self.R: return (x + 1, y) @@ -940,14 +996,30 @@ def compare_agents(EnvFactory, AgentFactories, n=10, steps=1000): """See how well each of several agents do in n instances of an environment. Pass in a factory (constructor) for environments, and several for agents. Create n instances of the environment, and run each agent in copies of - each one for steps. Return a list of (agent, average-score) tuples.""" + each one for steps. Return a list of (agent, average-score) tuples. + >>> environment = TrivialVacuumEnvironment + >>> agents = [ModelBasedVacuumAgent, ReflexVacuumAgent] + >>> result = compare_agents(environment, agents) + >>> performance_ModelBasedVacummAgent = result[0][1] + >>> performance_ReflexVacummAgent = result[1][1] + >>> performance_ReflexVacummAgent <= performance_ModelBasedVacummAgent + True + """ envs = [EnvFactory() for i in range(n)] 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""" + """Return the mean score of running an agent in each of the envs, for steps + >>> def constant_prog(percept): + ... return percept + ... + >>> agent = Agent(constant_prog) + >>> result = agent.program(5) + >>> result == 5 + True + """ def score(env): agent = AgentFactory() env.add_thing(agent) diff --git a/csp.ipynb b/csp.ipynb index af85b81d6..d9254ef0e 100644 --- a/csp.ipynb +++ b/csp.ipynb @@ -35,6 +35,7 @@ "* Overview\n", "* Graph Coloring\n", "* N-Queens\n", + "* AC-3\n", "* Backtracking Search\n", "* Tree CSP Solver\n", "* Graph Coloring Visualization\n", @@ -50,33 +51,6 @@ "CSPs are a special kind of search problems. Here we don't treat the space as a black box but the state has a particular form and we use that to our advantage to tweak our algorithms to be more suited to the problems. A CSP State is defined by a set of variables which can take values from corresponding domains. These variables can take only certain values in their domains to satisfy the constraints. A set of assignments which satisfies all constraints passes the goal test. Let us start by exploring the CSP class which we will use to model our CSPs. You can keep the popup open and read the main page to get a better idea of the code." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "psource(CSP)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The __ _ _init_ _ __ method parameters specify the CSP. Variable can be passed as a list of strings or integers. Domains are passed as dict where key specify the variables and value specify the domains. The variables are passed as an empty list. Variables are extracted from the keys of the domain dictionary. Neighbor is a dict of variables that essentially describes the constraint graph. Here each variable key has a list its value which are the variables that are constraint along with it. The constraint parameter should be a function **f(A, a, B, b**) that **returns true** if neighbors A, B **satisfy the constraint** when they have values **A=a, B=b**. We have additional parameters like nassings which is incremented each time an assignment is made when calling the assign method. You can read more about the methods and parameters in the class doc string. We will talk more about them as we encounter their use. Let us jump to an example." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## GRAPH COLORING\n", - "\n", - "We use the graph coloring problem as our running example for demonstrating the different algorithms in the **csp module**. The idea of map coloring problem is that the adjacent nodes (those connected by edges) should not have the same color throughout the graph. The graph can be colored using a fixed number of colors. Here each node is a variable and the values are the colors that can be assigned to them. Given that the domain will be the same for all our nodes we use a custom dict defined by the **UniversalDict** class. The **UniversalDict** Class takes in a parameter which it returns as value for all the keys of the dict. It is very similar to **defaultdict** in Python except that it does not support item assignment." - ] - }, { "cell_type": "code", "execution_count": 2, @@ -84,72 +58,264 @@ "outputs": [ { "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "class CSP(search.Problem):\n",
+ " """This class describes finite-domain Constraint Satisfaction Problems.\n",
+ " A CSP is specified by the following inputs:\n",
+ " variables A list of variables; each is atomic (e.g. int or string).\n",
+ " domains A dict of {var:[possible_value, ...]} entries.\n",
+ " neighbors A dict of {var:[var,...]} that for each variable lists\n",
+ " the other variables that participate in constraints.\n",
+ " constraints A function f(A, a, B, b) that returns true if neighbors\n",
+ " A, B satisfy the constraint when they have values A=a, B=b\n",
+ "\n",
+ " In the textbook and in most mathematical definitions, the\n",
+ " constraints are specified as explicit pairs of allowable values,\n",
+ " but the formulation here is easier to express and more compact for\n",
+ " most cases. (For example, the n-Queens problem can be represented\n",
+ " in O(n) space using this notation, instead of O(N^4) for the\n",
+ " explicit representation.) In terms of describing the CSP as a\n",
+ " problem, that's all there is.\n",
+ "\n",
+ " However, the class also supports data structures and methods that help you\n",
+ " solve CSPs by calling a search function on the CSP. Methods and slots are\n",
+ " as follows, where the argument 'a' represents an assignment, which is a\n",
+ " dict of {var:val} entries:\n",
+ " assign(var, val, a) Assign a[var] = val; do other bookkeeping\n",
+ " unassign(var, a) Do del a[var], plus other bookkeeping\n",
+ " nconflicts(var, val, a) Return the number of other variables that\n",
+ " conflict with var=val\n",
+ " curr_domains[var] Slot: remaining consistent values for var\n",
+ " Used by constraint propagation routines.\n",
+ " The following methods are used only by graph_search and tree_search:\n",
+ " actions(state) Return a list of actions\n",
+ " result(state, action) Return a successor of state\n",
+ " goal_test(state) Return true if all constraints satisfied\n",
+ " The following are just for debugging purposes:\n",
+ " nassigns Slot: tracks the number of assignments made\n",
+ " display(a) Print a human-readable representation\n",
+ " """\n",
+ "\n",
+ " def __init__(self, variables, domains, neighbors, constraints):\n",
+ " """Construct a CSP problem. If variables is empty, it becomes domains.keys()."""\n",
+ " variables = variables or list(domains.keys())\n",
+ "\n",
+ " self.variables = variables\n",
+ " self.domains = domains\n",
+ " self.neighbors = neighbors\n",
+ " self.constraints = constraints\n",
+ " self.initial = ()\n",
+ " self.curr_domains = None\n",
+ " self.nassigns = 0\n",
+ "\n",
+ " def assign(self, var, val, assignment):\n",
+ " """Add {var: val} to assignment; Discard the old value if any."""\n",
+ " assignment[var] = val\n",
+ " self.nassigns += 1\n",
+ "\n",
+ " def unassign(self, var, assignment):\n",
+ " """Remove {var: val} from assignment.\n",
+ " DO NOT call this if you are changing a variable to a new value;\n",
+ " just call assign for that."""\n",
+ " if var in assignment:\n",
+ " del assignment[var]\n",
+ "\n",
+ " def nconflicts(self, var, val, assignment):\n",
+ " """Return the number of conflicts var=val has with other variables."""\n",
+ " # Subclasses may implement this more efficiently\n",
+ " def conflict(var2):\n",
+ " return (var2 in assignment and\n",
+ " not self.constraints(var, val, var2, assignment[var2]))\n",
+ " return count(conflict(v) for v in self.neighbors[var])\n",
+ "\n",
+ " def display(self, assignment):\n",
+ " """Show a human-readable representation of the CSP."""\n",
+ " # Subclasses can print in a prettier way, or display with a GUI\n",
+ " print('CSP:', self, 'with assignment:', assignment)\n",
+ "\n",
+ " # These methods are for the tree and graph-search interface:\n",
+ "\n",
+ " def actions(self, state):\n",
+ " """Return a list of applicable actions: nonconflicting\n",
+ " assignments to an unassigned variable."""\n",
+ " if len(state) == len(self.variables):\n",
+ " return []\n",
+ " else:\n",
+ " assignment = dict(state)\n",
+ " var = first([v for v in self.variables if v not in assignment])\n",
+ " return [(var, val) for val in self.domains[var]\n",
+ " if self.nconflicts(var, val, assignment) == 0]\n",
+ "\n",
+ " def result(self, state, action):\n",
+ " """Perform an action and return the new state."""\n",
+ " (var, val) = action\n",
+ " return state + ((var, val),)\n",
+ "\n",
+ " def goal_test(self, state):\n",
+ " """The goal is to assign all variables, with all constraints satisfied."""\n",
+ " assignment = dict(state)\n",
+ " return (len(assignment) == len(self.variables)\n",
+ " and all(self.nconflicts(variables, assignment[variables], assignment) == 0\n",
+ " for variables in self.variables))\n",
+ "\n",
+ " # These are for constraint propagation\n",
+ "\n",
+ " def support_pruning(self):\n",
+ " """Make sure we can prune values from domains. (We want to pay\n",
+ " for this only if we use it.)"""\n",
+ " if self.curr_domains is None:\n",
+ " self.curr_domains = {v: list(self.domains[v]) for v in self.variables}\n",
+ "\n",
+ " def suppose(self, var, value):\n",
+ " """Start accumulating inferences from assuming var=value."""\n",
+ " self.support_pruning()\n",
+ " removals = [(var, a) for a in self.curr_domains[var] if a != value]\n",
+ " self.curr_domains[var] = [value]\n",
+ " return removals\n",
+ "\n",
+ " def prune(self, var, value, removals):\n",
+ " """Rule out var=value."""\n",
+ " self.curr_domains[var].remove(value)\n",
+ " if removals is not None:\n",
+ " removals.append((var, value))\n",
+ "\n",
+ " def choices(self, var):\n",
+ " """Return all values for var that aren't currently ruled out."""\n",
+ " return (self.curr_domains or self.domains)[var]\n",
+ "\n",
+ " def infer_assignment(self):\n",
+ " """Return the partial assignment implied by the current inferences."""\n",
+ " self.support_pruning()\n",
+ " return {v: self.curr_domains[v][0]\n",
+ " for v in self.variables if 1 == len(self.curr_domains[v])}\n",
+ "\n",
+ " def restore(self, removals):\n",
+ " """Undo a supposition and all inferences from it."""\n",
+ " for B, b in removals:\n",
+ " self.curr_domains[B].append(b)\n",
+ "\n",
+ " # This is for min_conflicts search\n",
+ "\n",
+ " def conflicted_vars(self, current):\n",
+ " """Return a list of variables in current assignment that are in conflict"""\n",
+ " return [var for var in self.variables\n",
+ " if self.nconflicts(var, current[var], current) > 0]\n",
+ "def queen_constraint(A, a, B, b):\n",
- " """Constraint is satisfied (true) if A, B are really the same variable,\n",
- " or if they are not in the same row, down diagonal, or up diagonal."""\n",
- " return A == B or (a != b and A + a != B + b and A - a != B - b)\n",
+ "def different_values_constraint(A, a, B, b):\n",
+ " """A constraint saying two neighboring variables must differ in value."""\n",
+ " return a != b\n",
"
\n",
"\n",
"