diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..2398f62e3 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +omit = + tests/* \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..688024601 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 100 +ignore = E121,E123,E126,E221,E222,E225,E226,E242,E701,E702,E704,E731,W503,F405,F841 +exclude = tests diff --git a/.gitignore b/.gitignore index 9a4bb620f..58e83214e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +*.pytest_cache # Translations *.mo @@ -70,3 +71,8 @@ target/ # dotenv .env +.idea + +# for macOS +.DS_Store +._.DS_Store diff --git a/.travis.yml b/.travis.yml index 5af22b933..e465e8e4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,19 @@ -language: - - python +language: python python: - - "3.4" + - 3.5 + - 3.6 + - 3.7 + - 3.8 before_install: - git submodule update --remote install: - - pip install flake8 - - pip install jupyter - - pip install -r requirements.txt + - pip install --upgrade -r requirements.txt script: - - py.test + - py.test --cov=./ - python -m doctest -v *.py after_success: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e1013fa1..f92643700 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,22 +1,50 @@ How to Contribute to aima-python ========================== -Thanks for considering contributing to `aima-python`! Here is some of the work that needs to be done: +Thanks for considering contributing to `aima-python`! Whether you are an aspiring [Google Summer of Code](https://summerofcode.withgoogle.com/organizations/5431334980288512/) student, or an independent contributor, here is a guide on how you can help. -## Port to Python 3; Pythonic Idioms; py.test +First of all, you can read these write-ups from past GSoC students to get an idea about what you can do for the project. [Chipe1](https://github.com/aimacode/aima-python/issues/641) - [MrDupin](https://github.com/aimacode/aima-python/issues/632) -- Check for common problems in [porting to Python 3](http://python3porting.com/problems.html), such as: `print` is now a function; `range` and `map` and other functions no longer produce `list`s; objects of different types can no longer be compared with `<`; strings are now Unicode; it would be nice to move `%` string formating to `.format`; there is a new `next` function for generators; integer division now returns a float; we can now use set literals. +In general, the main ways you can contribute to the repository are the following: + +1. Implement algorithms from the [list of algorithms](https://github.com/aimacode/aima-python/blob/master/README.md#index-of-algorithms). +1. Add tests for algorithms. +1. Take care of [issues](https://github.com/aimacode/aima-python/issues). +1. Write on the notebooks (`.ipynb` files). +1. Add and edit documentation (the docstrings in `.py` files). + +In more detail: + +## Read the Code and Start on an Issue + +- First, read and understand the code to get a feel for the extent and the style. +- Look at the [issues](https://github.com/aimacode/aima-python/issues) and pick one to work on. +- One of the issues is that some algorithms are missing from the [list of algorithms](https://github.com/aimacode/aima-python/blob/master/README.md#index-of-algorithms) and that some don't have tests. + +## Port to Python 3; Pythonic Idioms + +- Check for common problems in [porting to Python 3](http://python3porting.com/problems.html), such as: `print` is now a function; `range` and `map` and other functions no longer produce `list`; objects of different types can no longer be compared with `<`; strings are now Unicode; it would be nice to move `%` string formatting to `.format`; there is a new `next` function for generators; integer division now returns a float; we can now use set literals. - Replace old Lisp-based idioms with proper Python idioms. For example, we have many functions that were taken directly from Common Lisp, such as the `every` function: `every(callable, items)` returns true if every element of `items` is callable. This is good Lisp style, but good Python style would be to use `all` and a generator expression: `all(callable(f) for f in items)`. Eventually, fix all calls to these legacy Lisp functions and then remove the functions. -- Add more tests in `_test.py` files. Strive for terseness; it is ok to group multiple asserts into one `def test_something():` function. Move most tests to `_test.py`, but it is fine to have a single `doctest` example in the docstring of a function in the `.py` file, if the purpose of the doctest is to explain how to use the function, rather than test the implementation. ## New and Improved Algorithms - Implement functions that were in the third edition of the book but were not yet implemented in the code. Check the [list of pseudocode algorithms (pdf)](https://github.com/aimacode/pseudocode/blob/master/aima3e-algorithms.pdf) to see what's missing. - As we finish chapters for the new fourth edition, we will share the new pseudocode in the [`aima-pseudocode`](https://github.com/aimacode/aima-pseudocode) repository, and describe what changes are necessary. -We hope to have a `algorithm-name.md` file for each algorithm, eventually; it would be great if contributors could add some for the existing algorithms. -- Give examples of how to use the code in the `.ipynb` file. +We hope to have an `algorithm-name.md` file for each algorithm, eventually; it would be great if contributors could add some for the existing algorithms. -We still support a legacy branch, `aima3python2` (for the third edition of the textbook and for Python 2 code). +## Jupyter Notebooks + +In this project we use Jupyter/IPython Notebooks to showcase the algorithms in the book. They serve as short tutorials on what the algorithms do, how they are implemented and how one can use them. To install Jupyter, you can follow the instructions [here](https://jupyter.org/install.html). These are some ways you can contribute to the notebooks: + +- Proofread the notebooks for grammar mistakes, typos, or general errors. +- Move visualization and unrelated to the algorithm code from notebooks to `notebook.py` (a file used to store code for the notebooks, like visualization and other miscellaneous stuff). Make sure the notebooks still work and have their outputs showing! +- Replace the `%psource` magic notebook command with the function `psource` from `notebook.py` where needed. Examples where this is useful are a) when we want to show code for algorithm implementation and b) when we have consecutive cells with the magic keyword (in this case, if the code is large, it's best to leave the output hidden). +- Add the function `pseudocode(algorithm_name)` in algorithm sections. The function prints the pseudocode of the algorithm. You can see some example usage in [`knowledge.ipynb`](https://github.com/aimacode/aima-python/blob/master/knowledge.ipynb). +- Edit existing sections for algorithms to add more information and/or examples. +- Add visualizations for algorithms. The visualization code should go in `notebook.py` to keep things clean. +- Add new sections for algorithms not yet covered. The general format we use in the notebooks is the following: First start with an overview of the algorithm, printing the pseudocode and explaining how it works. Then, add some implementation details, including showing the code (using `psource`). Finally, add examples for the implementations, showing how the algorithms work. Don't fret with adding complex, real-world examples; the project is meant for educational purposes. You can of course choose another format if something better suits an algorithm. + +Apart from the notebooks explaining how the algorithms work, we also have notebooks showcasing some indicative applications of the algorithms. These notebooks are in the `*_apps.ipynb` format. We aim to have an `apps` notebook for each module, so if you don't see one for the module you would like to contribute to, feel free to create it from scratch! In these notebooks we are looking for applications showing what the algorithms can do. The general format of these sections is this: Add a description of the problem you are trying to solve, then explain how you are going to solve it and finally provide your solution with examples. Note that any code you write should not require any external libraries apart from the ones already provided (like `matplotlib`). # Style Guide @@ -31,72 +59,46 @@ Beyond the above rules, we use [Pep 8](https://www.python.org/dev/peps/pep-0008) - I have set `--max-line-length 100`, not 79. - You don't need two spaces after a sentence-ending period. - Strunk and White is [not a good guide for English](http://chronicle.com/article/50-Years-of-Stupid-Grammar/25497). -- I prefer more concise docstrings; I don't follow [Pep 257](https://www.python.org/dev/peps/pep-0257/). +- I prefer more concise docstrings; I don't follow [Pep 257](https://www.python.org/dev/peps/pep-0257/). In most cases, +a one-line docstring suffices. It is rarely necessary to list what each argument does; the name of the argument usually is enough. - Not all constants have to be UPPERCASE. - At some point I may add [Pep 484](https://www.python.org/dev/peps/pep-0484/) type annotations, but I think I'll hold off for now; I want to get more experience with them, and some people may still be in Python 3.4. - -Contributing a Patch -==================== - -1. Submit an issue describing your proposed change to the repo in question. -1. The repo owner will respond to your issue promptly. -1. Fork the desired repo, develop and test your code changes. -1. Submit a pull request. - Reporting Issues ================ - Under which versions of Python does this happen? +- Provide an example of the issue occurring. + - Is anybody working on this? Patch Rules =========== -- Ensure that the patch is python 3.4 compliant. +- Ensure that the patch is Python 3.4 compliant. - Include tests if your patch is supposed to solve a bug, and explain clearly under which circumstances the bug happens. Make sure the test fails without your patch. - Follow the style guidelines described above. - -Running the Test-Suite -===================== - -The minimal requirement for running the testsuite is ``py.test``. You can -install it with:: - - pip install pytest - -Clone this repository:: - - git clone https://github.com/aimacode/aima-python.git - -Fetch the aima-data submodule:: - - cd aima-python - git submodule init - git submodule update - -Then you can run the testsuite with:: - - py.test +- Refer the issue you have fixed. +- Explain in brief what changes you have made with affected files name. # Choice of Programming Languages Are we right to concentrate on Java and Python versions of the code? I think so; both languages are popular; Java is -fast enough for our purposes, and has reasonable type declarations (but can be verbose); Python is popular and has a very direct mapping to the pseudocode in the book (but lacks type declarations and can be slow). The [TIOBE Index](http://www.tiobe.com/tiobe_index) says the top five most popular languages are: +fast enough for our purposes, and has reasonable type declarations (but can be verbose); Python is popular and has a very direct mapping to the pseudocode in the book (but lacks type declarations and can be slow). The [TIOBE Index](http://www.tiobe.com/tiobe_index) says the top seven most popular languages, in order, are: - Java, C, C++, C#, Python + Java, C, C++, C#, Python, PHP, Javascript -So it might be reasonable to also support C++/C# at some point in the future. It might also be reasonable to support a language that combines the terse readability of Python with the type safety and speed of Java; perhaps Go or Julia. And finally, Javascript is the language of the browser; it would be nice to have code that runs in the browser, in Javascript or a variant such as Typescript. +So it might be reasonable to also support C++/C# at some point in the future. It might also be reasonable to support a language that combines the terse readability of Python with the type safety and speed of Java; perhaps Go or Julia. I see no reason to support PHP. Javascript is the language of the browser; it would be nice to have code that runs in the browser without need for any downloads; this would be in Javascript or a variant such as Typescript. -There is also a `aima-lisp` project; in 1995 when we wrote the first edition of the book, Lisp was the right choice, but today it is less popular. +There is also a `aima-lisp` project; in 1995 when we wrote the first edition of the book, Lisp was the right choice, but today it is less popular (currently #31 on the TIOBE index). -What languages are instructors recommending for their AI class? To get an approximate idea, I gave the query [norvig russell "Modern Approach"](https://www.google.com/webhp#q=russell%20norvig%20%22modern%20approach%22%20java) along with the names of various languages and looked at the estimated counts of results on +What languages are instructors recommending for their AI class? To get an approximate idea, I gave the query [\[norvig russell "Modern Approach"\]](https://www.google.com/webhp#q=russell%20norvig%20%22modern%20approach%22%20java) along with the names of various languages and looked at the estimated counts of results on various dates. However, I don't have much confidence in these figures... |Language |2004 |2005 |2007 |2010 |2016 | diff --git a/README.md b/README.md index ef56f3655..17f1d6085 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,172 @@ -
-

-
------------------ + # `aima-python` [![Build Status](https://travis-ci.org/aimacode/aima-python.svg?branch=master)](https://travis-ci.org/aimacode/aima-python) [![Binder](http://mybinder.org/badge.svg)](http://mybinder.org/repo/aimacode/aima-python) -Python code for the book *Artificial Intelligence: A Modern Approach.* You can use this in conjunction with a course on AI, or for study on your own. We're looking for [solid contributors](https://github.com/aimacode/aima-python/blob/master/CONTRIBUTING.md) to help. - -## Python 3.4 - -This code is in Python 3.4 (Python 3.5, also works, but Python 2.x does not). You can [install the latest Python version](https://www.python.org/downloads) or use a browser-based Python interpreter such as [repl.it](https://repl.it/languages/python3). - -## Structure of the Project - -When complete, this project will have Python code for all the pseudocode algorithms in the book. For each major topic, such as `logic`, we will have the following three files in the main branch: - -- `logic.py`: Implementations of all the pseudocode algorithms, and necessary support functions/classes/data. -- `logic.ipynb`: A Jupyter (IPython) notebook that explains and gives examples of how to use the code. -- `tests/logic_test.py`: A lightweight test suite, using `assert` statements, designed for use with [`py.test`](http://pytest.org/latest/). - -# Index of Code - -Here is a table of algorithms, the figure, name of the code in the book and in the repository, and the file where they are implemented in the code. This chart was made for the third edition of the book and needs to be updated for the upcoming fourth edition. Empty implementations are a good place for contributors to look for an issue. - - -| **Figure** | **Name (in 3rd edition)** | **Name (in repository)** | **File** -|:--------|:-------------------|:---------|:-----------| -| 2.1 | Environment | `Environment` | [`agents.py`](../master/agents.py) | -| 2.1 | Agent | `Agent` | [`agents.py`](../master/agents.py) | -| 2.3 | Table-Driven-Vacuum-Agent | `TableDrivenVacuumAgent` | [`agents.py`](../master/agents.py) | -| 2.7 | Table-Driven-Agent | `TableDrivenAgent` | [`agents.py`](../master/agents.py) | -| 2.8 | Reflex-Vacuum-Agent | `ReflexVacuumAgent` | [`agents.py`](../master/agents.py) | -| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`](../master/agents.py) | -| 2.12 | Model-Based-Reflex-Agent | `ReflexAgentWithState` | [`agents.py`](../master/agents.py) | -| 3 | Problem | `Problem` | [`search.py`](../master/search.py) | -| 3 | Node | `Node` | [`search.py`](../master/search.py) | -| 3 | Queue | `Queue` | [`utils.py`](../master/utils.py) | -| 3.1 | Simple-Problem-Solving-Agent | `SimpleProblemSolvingAgent` | [`search.py`](../master/search.py) | -| 3.2 | Romania | `romania` | [`search.py`](../master/search.py) | -| 3.7 | Tree-Search | `tree_search` | [`search.py`](../master/search.py) | -| 3.7 | Graph-Search | `graph_search` | [`search.py`](../master/search.py) | -| 3.11 | Breadth-First-Search | `breadth_first_search` | [`search.py`](../master/search.py) | -| 3.14 | Uniform-Cost-Search | `uniform_cost_search` | [`search.py`](../master/search.py) | -| 3.17 | Depth-Limited-Search | `depth_limited_search` | [`search.py`](../master/search.py) | -| 3.18 | Iterative-Deepening-Search | `iterative_deepening_search` | [`search.py`](../master/search.py) | -| 3.22 | Best-First-Search | `best_first_graph_search` | [`search.py`](../master/search.py) | -| 3.24 | A\*-Search | `astar_search` | [`search.py`](../master/search.py) | -| 3.26 | Recursive-Best-First-Search | `recursive_best_first_search` | [`search.py`](../master/search.py) | -| 4.2 | Hill-Climbing | `hill_climbing` | [`search.py`](../master/search.py) | -| 4.5 | Simulated-Annealing | `simulated_annealing` | [`search.py`](../master/search.py) | -| 4.8 | Genetic-Algorithm | `genetic_algorithm` | [`search.py`](../master/search.py) | -| 4.11 | And-Or-Graph-Search | `and_or_graph_search` | [`search.py`](../master/search.py) | -| 4.21 | Online-DFS-Agent | `online_dfs_agent` | [`search.py`](../master/search.py) | -| 4.24 | LRTA\*-Agent | `LRTAStarAgent` | [`search.py`](../master/search.py) | -| 5.3 | Minimax-Decision | `minimax_decision` | [`games.py`](../master/games.py) | -| 5.7 | Alpha-Beta-Search | `alphabeta_search` | [`games.py`](../master/games.py) | -| 6 | CSP | `CSP` | [`csp.py`](../master/csp.py) | -| 6.3 | AC-3 | `AC3` | [`csp.py`](../master/csp.py) | -| 6.5 | Backtracking-Search | `backtracking_search` | [`csp.py`](../master/csp.py) | -| 6.8 | Min-Conflicts | `min_conflicts` | [`csp.py`](../master/csp.py) | -| 6.11 | Tree-CSP-Solver | `tree_csp_solver` | [`csp.py`](../master/csp.py) | -| 7 | KB | `KB` | [`logic.py`](../master/logic.py) | -| 7.1 | KB-Agent | `KB_Agent` | [`logic.py`](../master/logic.py) | -| 7.7 | Propositional Logic Sentence | `Expr` | [`logic.py`](../master/logic.py) | -| 7.10 | TT-Entails | `tt_entials` | [`logic.py`](../master/logic.py) | -| 7.12 | PL-Resolution | `pl_resolution` | [`logic.py`](../master/logic.py) | -| 7.14 | Convert to CNF | `to_cnf` | [`logic.py`](../master/logic.py) | -| 7.15 | PL-FC-Entails? | `pl_fc_resolution` | [`logic.py`](../master/logic.py) | -| 7.17 | DPLL-Satisfiable? | `dpll_satisfiable` | [`logic.py`](../master/logic.py) | -| 7.18 | WalkSAT | `WalkSAT` | [`logic.py`](../master/logic.py) | -| 7.20 | Hybrid-Wumpus-Agent | | | -| 7.22 | SATPlan | `SAT_plan` | [`logic.py`](../master/logic.py) | -| 9 | Subst | `subst` | [`logic.py`](../master/logic.py) | -| 9.1 | Unify | `unify` | [`logic.py`](../master/logic.py) | -| 9.3 | FOL-FC-Ask | `fol_fc_ask` | [`logic.py`](../master/logic.py) | -| 9.6 | FOL-BC-Ask | `fol_bc_ask` | [`logic.py`](../master/logic.py) | -| 9.8 | Append | | | -| 10.1 | Air-Cargo-problem | | -| 10.2 | Spare-Tire-Problem | | -| 10.3 | Three-Block-Tower | | -| 10.7 | Cake-Problem | | -| 10.9 | Graphplan | | -| 10.13 | Partial-Order-Planner | | -| 11.1 | Job-Shop-Problem-With-Resources | | -| 11.5 | Hierarchical-Search | | -| 11.8 | Angelic-Search | | -| 11.10 | Doubles-tennis | | -| 13 | Discrete Probability Distribution | `ProbDist` | [`probability.py`](../master/probability.py) | -| 13.1 | DT-Agent | `DTAgent` | [`probability.py`](../master/probability.py) | -| 14.9 | Enumeration-Ask | `enumeration_ask` | [`probability.py`](../master/probability.py) | -| 14.11 | Elimination-Ask | `elimination_ask` | [`probability.py`](../master/probability.py) | -| 14.13 | Prior-Sample | `prior_sample` | [`probability.py`](../master/probability.py) | -| 14.14 | Rejection-Sampling | `rejection_sampling` | [`probability.py`](../master/probability.py) | -| 14.15 | Likelihood-Weighting | `likelihood_weighting` | [`probability.py`](../master/probability.py) | -| 14.16 | Gibbs-Ask | `gibbs_ask` | [`probability.py`](../master/probability.py) | -| 15.4 | Forward-Backward | `forward_backward` | [`probability.py`](../master/probability.py) | -| 15.6 | Fixed-Lag-Smoothing | `fixed_lag_smoothing` | [`probability.py`](../master/probability.py) | -| 15.17 | Particle-Filtering | `particle_filtering` | [`probability.py`](../master/probability.py) | -| 16.9 | Information-Gathering-Agent | | -| 17.4 | Value-Iteration | `value_iteration` | [`mdp.py`](../master/mdp.py) | -| 17.7 | Policy-Iteration | `policy_iteration` | [`mdp.py`](../master/mdp.py) | -| 17.7 | POMDP-Value-Iteration | | | -| 18.5 | Decision-Tree-Learning | `DecisionTreeLearner` | [`learning.py`](../master/learning.py) | -| 18.8 | Cross-Validation | `cross_validation` | [`learning.py`](../master/learning.py) | -| 18.11 | Decision-List-Learning | `DecisionListLearner` | [`learning.py`](../master/learning.py) | -| 18.24 | Back-Prop-Learning | `BackPropagationLearner` | [`learning.py`](../master/learning.py) | -| 18.34 | AdaBoost | `AdaBoost` | [`learning.py`](../master/learning.py) | -| 19.2 | Current-Best-Learning | | -| 19.3 | Version-Space-Learning | | -| 19.8 | Minimal-Consistent-Det | | -| 19.12 | FOIL | | -| 21.2 | Passive-ADP-Agent | `PassiveADPAgent` | [`rl.py`](../master/rl.py) | -| 21.4 | Passive-TD-Agent | `PassiveTDAgent` | [`rl.py`](../master/rl.py) | -| 21.8 | Q-Learning-Agent | `QLearningAgent` | [`rl.py`](../master/rl.py) | -| 22.1 | HITS | `HITS` | [`nlp.py`](../master/nlp.py) | -| 23 | Chart-Parse | `Chart` | [`nlp.py`](../master/nlp.py) | -| 23.5 | CYK-Parse | `CYK_parse` | [`nlp.py`](../master/nlp.py) | -| 25.9 | Monte-Carlo-Localization| | +Python code for the book *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu).* You can use this in conjunction with a course on AI, or for study on your own. We're looking for [solid contributors](https://github.com/aimacode/aima-python/blob/master/CONTRIBUTING.md) to help. + +# Updates for 4th Edition + +The 4th edition of the book as out now in 2020, and thus we are updating the code. All code here will reflect the 4th edition. Changes include: + +- Move from Python 3.5 to 3.7. +- More emphasis on Jupyter (Ipython) notebooks. +- More projects using external packages (tensorflow, etc.). + + + +# Structure of the Project + +When complete, this project will have Python implementations for all the pseudocode algorithms in the book, as well as tests and examples of use. For each major topic, such as `search`, we provide the following files: + +- `search.ipynb` and `search.py`: Implementations of all the pseudocode algorithms, and necessary support functions/classes/data. The `.py` file is generated automatically from the `.ipynb` file; the idea is that it is easier to read the documentation in the `.ipynb` file. +- `search_XX.ipynb`: Notebooks that show how to use the code, broken out into various topics (the `XX`). +- `tests/test_search.py`: A lightweight test suite, using `assert` statements, designed for use with [`py.test`](http://pytest.org/latest/), but also usable on their own. + +# Python 3.7 and up + +The code for the 3rd edition was in Python 3.5; the current 4th edition code is in Python 3.7. It should also run in later versions, but does not run in Python 2. You can [install Python](https://www.python.org/downloads) or use a browser-based Python interpreter such as [repl.it](https://repl.it/languages/python3). +You can run the code in an IDE, or from the command line with `python -i filename.py` where the `-i` option puts you in an interactive loop where you can run Python functions. All notebooks are available in a [binder environment](http://mybinder.org/repo/aimacode/aima-python). Alternatively, visit [jupyter.org](http://jupyter.org/) for instructions on setting up your own Jupyter notebook environment. + +Features from Python 3.6 and 3.7 that we will be using for this version of the code: +- [f-strings](https://docs.python.org/3.6/whatsnew/3.6.html#whatsnew36-pep498): all string formatting should be done with `f'var = {var}'`, not with `'var = {}'.format(var)` nor `'var = %s' % var`. +- [`typing` module](https://docs.python.org/3.7/library/typing.html): declare functions with type hints: `def successors(state) -> List[State]:`; that is, give type declarations, but omit them when it is obvious. I don't need to say `state: State`, but in another context it would make sense to say `s: State`. +- Underscores in numerics: write a million as `1_000_000` not as `1000000`. +- [`dataclasses` module](https://docs.python.org/3.7/library/dataclasses.html#module-dataclasses): replace `namedtuple` with `dataclass`. + + +[//]: # (There is a sibling [aima-docker]https://github.com/rajatjain1997/aima-docker project that shows you how to use docker containers to run more complex problems in more complex software environments.) + + +## Installation Guide + +To download the repository: + +`git clone https://github.com/aimacode/aima-python.git` + +Then you need to install the basic dependencies to run the project on your system: + +``` +cd aima-python +pip install -r requirements.txt +``` + +You also need to fetch the datasets from the [`aima-data`](https://github.com/aimacode/aima-data) repository: + +``` +git submodule init +git submodule update +``` + +Wait for the datasets to download, it may take a while. Once they are downloaded, you need to install `pytest`, so that you can run the test suite: + +`pip install pytest` + +Then to run the tests: + +`py.test` + +And you are good to go! + + +# Index of Algorithms + +Here is a table of algorithms, the figure, name of the algorithm in the book and in the repository, and the file where they are implemented in the repository. This chart was made for the third edition of the book and is being updated for the upcoming fourth edition. Empty implementations are a good place for contributors to look for an issue. The [aima-pseudocode](https://github.com/aimacode/aima-pseudocode) project describes all the algorithms from the book. An asterisk next to the file name denotes the algorithm is not fully implemented. Another great place for contributors to start is by adding tests and writing on the notebooks. You can see which algorithms have tests and notebook sections below. If the algorithm you want to work on is covered, don't worry! You can still add more tests and provide some examples of use in the notebook! + +| **Figure** | **Name (in 3rd edition)** | **Name (in repository)** | **File** | **Tests** | **Notebook** +|:-------|:----------------------------------|:------------------------------|:--------------------------------|:-----|:---------| +| 2 | Random-Vacuum-Agent | `RandomVacuumAgent` | [`agents.py`][agents] | Done | Included | +| 2 | Model-Based-Vacuum-Agent | `ModelBasedVacuumAgent` | [`agents.py`][agents] | Done | Included | +| 2.1 | Environment | `Environment` | [`agents.py`][agents] | Done | Included | +| 2.1 | Agent | `Agent` | [`agents.py`][agents] | Done | Included | +| 2.3 | Table-Driven-Vacuum-Agent | `TableDrivenVacuumAgent` | [`agents.py`][agents] | Done | Included | +| 2.7 | Table-Driven-Agent | `TableDrivenAgent` | [`agents.py`][agents] | Done | Included | +| 2.8 | Reflex-Vacuum-Agent | `ReflexVacuumAgent` | [`agents.py`][agents] | Done | Included | +| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | Done | Included | +| 2.12 | Model-Based-Reflex-Agent | `ReflexAgentWithState` | [`agents.py`][agents] | Done | Included | +| 3 | Problem | `Problem` | [`search.py`][search] | Done | Included | +| 3 | Node | `Node` | [`search.py`][search] | Done | Included | +| 3 | Queue | `Queue` | [`utils.py`][utils] | Done | No Need | +| 3.1 | Simple-Problem-Solving-Agent | `SimpleProblemSolvingAgent` | [`search.py`][search] | Done | Included | +| 3.2 | Romania | `romania` | [`search.py`][search] | Done | Included | +| 3.7 | Tree-Search | `depth/breadth_first_tree_search` | [`search.py`][search] | Done | Included | +| 3.7 | Graph-Search | `depth/breadth_first_graph_search` | [`search.py`][search] | Done | Included | +| 3.11 | Breadth-First-Search | `breadth_first_graph_search` | [`search.py`][search] | Done | Included | +| 3.14 | Uniform-Cost-Search | `uniform_cost_search` | [`search.py`][search] | Done | Included | +| 3.17 | Depth-Limited-Search | `depth_limited_search` | [`search.py`][search] | Done | Included | +| 3.18 | Iterative-Deepening-Search | `iterative_deepening_search` | [`search.py`][search] | Done | Included | +| 3.22 | Best-First-Search | `best_first_graph_search` | [`search.py`][search] | Done | Included | +| 3.24 | A\*-Search | `astar_search` | [`search.py`][search] | Done | Included | +| 3.26 | Recursive-Best-First-Search | `recursive_best_first_search` | [`search.py`][search] | Done | Included | +| 4.2 | Hill-Climbing | `hill_climbing` | [`search.py`][search] | Done | Included | +| 4.5 | Simulated-Annealing | `simulated_annealing` | [`search.py`][search] | Done | Included | +| 4.8 | Genetic-Algorithm | `genetic_algorithm` | [`search.py`][search] | Done | Included | +| 4.11 | And-Or-Graph-Search | `and_or_graph_search` | [`search.py`][search] | Done | Included | +| 4.21 | Online-DFS-Agent | `online_dfs_agent` | [`search.py`][search] | Done | Included | +| 4.24 | LRTA\*-Agent | `LRTAStarAgent` | [`search.py`][search] | Done | Included | +| 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 | 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 | +| 7 | KB | `KB` | [`logic.py`][logic] | Done | Included | +| 7.1 | KB-Agent | `KB_AgentProgram` | [`logic.py`][logic] | Done | Included | +| 7.7 | Propositional Logic Sentence | `Expr` | [`utils.py`][utils] | Done | Included | +| 7.10 | TT-Entails | `tt_entails` | [`logic.py`][logic] | Done | Included | +| 7.12 | PL-Resolution | `pl_resolution` | [`logic.py`][logic] | Done | Included | +| 7.14 | Convert to CNF | `to_cnf` | [`logic.py`][logic] | Done | Included | +| 7.15 | PL-FC-Entails? | `pl_fc_entails` | [`logic.py`][logic] | Done | Included | +| 7.17 | DPLL-Satisfiable? | `dpll_satisfiable` | [`logic.py`][logic] | Done | Included | +| 7.18 | WalkSAT | `WalkSAT` | [`logic.py`][logic] | Done | Included | +| 7.20 | Hybrid-Wumpus-Agent | `HybridWumpusAgent` | | | | +| 7.22 | SATPlan | `SAT_plan` | [`logic.py`][logic] | Done | Included | +| 9 | Subst | `subst` | [`logic.py`][logic] | Done | Included | +| 9.1 | Unify | `unify` | [`logic.py`][logic] | Done | Included | +| 9.3 | FOL-FC-Ask | `fol_fc_ask` | [`logic.py`][logic] | Done | Included | +| 9.6 | FOL-BC-Ask | `fol_bc_ask` | [`logic.py`][logic] | Done | Included | +| 10.1 | Air-Cargo-problem | `air_cargo` | [`planning.py`][planning] | Done | Included | +| 10.2 | Spare-Tire-Problem | `spare_tire` | [`planning.py`][planning] | Done | Included | +| 10.3 | Three-Block-Tower | `three_block_tower` | [`planning.py`][planning] | Done | Included | +| 10.7 | Cake-Problem | `have_cake_and_eat_cake_too` | [`planning.py`][planning] | Done | Included | +| 10.9 | Graphplan | `GraphPlan` | [`planning.py`][planning] | Done | Included | +| 10.13 | Partial-Order-Planner | `PartialOrderPlanner` | [`planning.py`][planning] | Done | Included | +| 11.1 | Job-Shop-Problem-With-Resources | `job_shop_problem` | [`planning.py`][planning] | Done | Included | +| 11.5 | Hierarchical-Search | `hierarchical_search` | [`planning.py`][planning] | Done | Included | +| 11.8 | Angelic-Search | `angelic_search` | [`planning.py`][planning] | Done | Included | +| 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] | 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 | +| 14.14 | Rejection-Sampling | `rejection_sampling` | [`probability.py`][probability] | Done | Included | +| 14.15 | Likelihood-Weighting | `likelihood_weighting` | [`probability.py`][probability] | Done | Included | +| 14.16 | Gibbs-Ask | `gibbs_ask` | [`probability.py`][probability] | Done | Included | +| 15.4 | Forward-Backward | `forward_backward` | [`probability.py`][probability] | Done | Included | +| 15.6 | Fixed-Lag-Smoothing | `fixed_lag_smoothing` | [`probability.py`][probability] | Done | Included | +| 15.17 | Particle-Filtering | `particle_filtering` | [`probability.py`][probability] | Done | Included | +| 16.9 | Information-Gathering-Agent | `InformationGatheringAgent` | [`probability.py`][probability] | Done | Included | +| 17.4 | Value-Iteration | `value_iteration` | [`mdp.py`][mdp] | Done | Included | +| 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.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 | +| 19.2 | Current-Best-Learning | `current_best_learning` | [`knowledge.py`](knowledge.py) | Done | Included | +| 19.3 | Version-Space-Learning | `version_space_learning` | [`knowledge.py`](knowledge.py) | Done | Included | +| 19.8 | Minimal-Consistent-Det | `minimal_consistent_det` | [`knowledge.py`](knowledge.py) | Done | Included | +| 19.12 | FOIL | `FOIL_container` | [`knowledge.py`](knowledge.py) | Done | Included | +| 21.2 | Passive-ADP-Agent | `PassiveADPAgent` | [`rl.py`][rl] | Done | Included | +| 21.4 | Passive-TD-Agent | `PassiveTDAgent` | [`rl.py`][rl] | Done | Included | +| 21.8 | Q-Learning-Agent | `QLearningAgent` | [`rl.py`][rl] | Done | Included | +| 22.1 | HITS | `HITS` | [`nlp.py`][nlp] | Done | Included | +| 23 | Chart-Parse | `Chart` | [`nlp.py`][nlp] | Done | Included | +| 23.5 | CYK-Parse | `CYK_parse` | [`nlp.py`][nlp] | Done | Included | +| 25.9 | Monte-Carlo-Localization | `monte_carlo_localization` | [`probability.py`][probability] | Done | Included | # Index of data structures @@ -125,17 +174,34 @@ Here is a table of algorithms, the figure, name of the code in the book and in t Here is a table of the implemented data structures, the figure, name of the implementation in the repository, and the file where they are implemented. | **Figure** | **Name (in repository)** | **File** | -|:-----------|:-------------------------|:---------| -| 3.2 | romania_map | [`search.py`](../master/search.py) | -| 4.9 | vacumm_world | [`search.py`](../master/search.py) | -| 4.23 | one_dim_state_space | [`search.py`](../master/search.py) | -| 6.1 | australia_map | [`search.py`](../master/search.py) | -| 7.13 | wumpus_world_inference | [`logic.py`](../master/login.py) | -| 7.16 | horn_clauses_KB | [`logic.py`](../master/logic.py) | -| 17.1 | sequential_decision_environment | [`mdp.py`](../master/mdp.py) | -| 18.2 | waiting_decision_tree | [`learning.py`](../master/learning.py) | +|:-------|:--------------------------------|:--------------------------| +| 3.2 | romania_map | [`search.py`][search] | +| 4.9 | vacumm_world | [`search.py`][search] | +| 4.23 | one_dim_state_space | [`search.py`][search] | +| 6.1 | australia_map | [`search.py`][search] | +| 7.13 | wumpus_world_inference | [`logic.py`][logic] | +| 7.16 | horn_clauses_KB | [`logic.py`][logic] | +| 17.1 | sequential_decision_environment | [`mdp.py`][mdp] | +| 18.2 | waiting_decision_tree | [`learning.py`][learning] | # Acknowledgements -Many thanks for contributions over the years. I got bug reports, corrected code, and other support from Darius Bacon, Phil Ruggera, Peng Shao, Amit Patil, Ted Nienstedt, Jim Martin, Ben Catanzariti, and others. Now that the project is on GitHub, you can see the [contributors](https://github.com/aimacode/aima-python/graphs/contributors) who are doing a great job of actively improving the project. Many thanks to all contributors, especially @darius, @SnShine, and @reachtarunhere. +Many thanks for contributions over the years. I got bug reports, corrected code, and other support from Darius Bacon, Phil Ruggera, Peng Shao, Amit Patil, Ted Nienstedt, Jim Martin, Ben Catanzariti, and others. Now that the project is on GitHub, you can see the [contributors](https://github.com/aimacode/aima-python/graphs/contributors) who are doing a great job of actively improving the project. Many thanks to all contributors, especially [@darius](https://github.com/darius), [@SnShine](https://github.com/SnShine), [@reachtarunhere](https://github.com/reachtarunhere), [@antmarakis](https://github.com/antmarakis), [@Chipe1](https://github.com/Chipe1), [@ad71](https://github.com/ad71) and [@MariannaSpyrakou](https://github.com/MariannaSpyrakou). + + +[agents]:../master/agents.py +[csp]:../master/csp.py +[games]:../master/games.py +[grid]:../master/grid.py +[knowledge]:../master/knowledge.py +[learning]:../master/learning.py +[logic]:../master/logic.py +[mdp]:../master/mdp.py +[nlp]:../master/nlp.py +[planning]:../master/planning.py +[probability]:../master/probability.py +[rl]:../master/rl.py +[search]:../master/search.py +[utils]:../master/utils.py +[text]:../master/text.py diff --git a/SUBMODULE.md b/SUBMODULE.md new file mode 100644 index 000000000..2c080bb91 --- /dev/null +++ b/SUBMODULE.md @@ -0,0 +1,11 @@ +This is a guide on how to update the `aima-data` submodule to the latest version. This needs to be done every time something changes in the [aima-data](https://github.com/aimacode/aima-data) repository. All the below commands should be executed from the local directory of the `aima-python` repository, using `git`. + +``` +git submodule deinit aima-data +git rm aima-data +git submodule add https://github.com/aimacode/aima-data.git aima-data +git commit +git push origin +``` + +Then you need to pull request the changes (unless you are a collaborator, in which case you can commit directly to the master). diff --git a/agents.ipynb b/agents.ipynb index db42f8d33..636df75e3 100644 --- a/agents.ipynb +++ b/agents.ipynb @@ -4,26 +4,120 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# AGENT #\n", + "# Intelligent Agents #\n", "\n", - "An agent, as defined in 2.1 is anything that can perceive its environment through sensors, and act upon that environment through actuators based on its agent program. This can be a dog, robot, or even you. As long as you can perceive the environment and act on it, you are an agent. This notebook will explain how to implement a simple agent, create an environment, and create a program that helps the agent act on the environment based on its percepts.\n", + "This notebook serves as supporting material for topics covered in **Chapter 2 - Intelligent Agents** from the book *Artificial Intelligence: A Modern Approach.* This notebook uses implementations from [agents.py](https://github.com/aimacode/aima-python/blob/master/agents.py) module. Let's start by importing everything from agents module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from agents import *\n", + "from notebook import psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* Agent\n", + "* Environment\n", + "* Simple Agent and Environment\n", + "* Agents in a 2-D Environment\n", + "* Wumpus Environment\n", + "\n", + "## OVERVIEW\n", "\n", - "Before moving on, review the Agent and Environment classes in [agents.py](https://github.com/aimacode/aima-python/blob/master/agents.py).\n", + "An agent, as defined in 2.1, is anything that can perceive its environment through sensors, and act upon that environment through actuators based on its agent program. This can be a dog, a robot, or even you. As long as you can perceive the environment and act on it, you are an agent. This notebook will explain how to implement a simple agent, create an environment, and implement a program that helps the agent act on the environment based on its percepts.\n", "\n", - "Let's begin by importing all the functions from the agents.py module and creating our first agent - a blind dog." + "## AGENT\n", + "\n", + "Let us now see how we define an agent. Run the next cell to see how `Agent` is defined in agents module." ] }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ - "from agents import *\n", + "psource(Agent)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Agent` has two methods.\n", + "* `__init__(self, program=None)`: The constructor defines various attributes of the Agent. These include\n", + "\n", + " * `alive`: which keeps track of whether the agent is alive or not \n", + " \n", + " * `bump`: which tracks if the agent collides with an edge of the environment (for eg, a wall in a park)\n", + " \n", + " * `holding`: which is a list containing the `Things` an agent is holding, \n", + " \n", + " * `performance`: which evaluates the performance metrics of the agent \n", + " \n", + " * `program`: which is the agent program and maps an agent's percepts to actions in the environment. If no implementation is provided, it defaults to asking the user to provide actions for each percept.\n", + " \n", + "* `can_grab(self, thing)`: Is used when an environment contains things that an agent can grab and carry. By default, an agent can carry nothing.\n", + "\n", + "## ENVIRONMENT\n", + "Now, let us see how environments are defined. Running the next cell will display an implementation of the abstract `Environment` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(Environment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Environment` class has lot of methods! But most of them are incredibly simple, so let's see the ones we'll be using in this notebook.\n", + "\n", + "* `thing_classes(self)`: Returns a static array of `Thing` sub-classes that determine what things are allowed in the environment and what aren't\n", + "\n", + "* `add_thing(self, thing, location=None)`: Adds a thing to the environment at location\n", "\n", + "* `run(self, steps)`: Runs an environment with the agent in it for a given number of steps.\n", + "\n", + "* `is_done(self)`: Returns true if the objective of the agent and the environment has been completed\n", + "\n", + "The next two functions must be implemented by each subclasses of `Environment` for the agent to recieve percepts and execute actions \n", + "\n", + "* `percept(self, agent)`: Given an agent, this method returns a list of percepts that the agent sees at the current time\n", + "\n", + "* `execute_action(self, agent, action)`: The environment reacts to an action performed by a given agent. The changes may result in agent experiencing new percepts or other elements reacting to agent input." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SIMPLE AGENT AND ENVIRONMENT\n", + "\n", + "Let's begin by using the `Agent` class to creating our first agent - a blind dog." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "class BlindDog(Agent):\n", " def eat(self, thing):\n", " print(\"Dog: Ate food at {}.\".format(self.location))\n", @@ -43,19 +137,9 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(dog.alive)" ] @@ -72,20 +156,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# ENVIRONMENT #\n", - "\n", - "A park is an example of an environment because our dog can perceive and act upon it. The Environment class in agents.py is an abstract class, so we will have to create our own subclass from it before we can use it. The abstract class must contain the following methods:\n", + "### ENVIRONMENT - Park\n", "\n", - "
  • percept(self, agent) - returns what the agent perceives
  • \n", - "
  • execute_action(self, agent, action) - changes the state of the environment based on what the agent does.
  • " + "A park is an example of an environment because our dog can perceive and act upon it. The Environment class is an abstract class, so we will have to create our own subclass from it before we can use it." ] }, { "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "class Food(Thing):\n", @@ -96,29 +175,33 @@ "\n", "class Park(Environment):\n", " def percept(self, agent):\n", - " '''prints & return a list of things that are in our agent's location'''\n", + " '''return a list of things that are in our agent's location'''\n", " things = self.list_things_at(agent.location)\n", - " print(things)\n", " return things\n", " \n", " def execute_action(self, agent, action):\n", " '''changes the state of the environment based on what the agent does.'''\n", " if action == \"move down\":\n", + " print('{} decided to {} at location: {}'.format(str(agent)[1:-1], action, agent.location))\n", " agent.movedown()\n", " elif action == \"eat\":\n", " items = self.list_things_at(agent.location, tclass=Food)\n", " if len(items) != 0:\n", - " if agent.eat(items[0]): #Have the dog pick eat the first item\n", + " if agent.eat(items[0]): #Have the dog eat the first item\n", + " print('{} ate {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", " self.delete_thing(items[0]) #Delete it from the Park after.\n", " elif action == \"drink\":\n", " items = self.list_things_at(agent.location, tclass=Water)\n", " if len(items) != 0:\n", " if agent.drink(items[0]): #Have the dog drink the first item\n", + " print('{} drank {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", " self.delete_thing(items[0]) #Delete it from the Park after.\n", - " \n", + "\n", " def is_done(self):\n", " '''By default, we're done when we can't find a live agent, \n", - " but to prevent killing our cute dog, we will or it with when there is no more food or water'''\n", + " but to prevent killing our cute dog, we will stop before itself - when there is no more food or water'''\n", " no_edibles = not any(isinstance(thing, Food) or isinstance(thing, Water) for thing in self.things)\n", " dead_agents = not any(agent.is_alive() for agent in self.agents)\n", " return dead_agents or no_edibles\n" @@ -128,93 +211,245 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Wumpus Environment" + "### PROGRAM - BlindDog\n", + "Now that we have a Park Class, we re-implement our BlindDog to be able to move down and eat food or drink water only if it is present.\n" ] }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ - "from ipythonblocks import BlockGrid\n", - "from agents import *\n", + "class BlindDog(Agent):\n", + " location = 1\n", + " \n", + " def movedown(self):\n", + " self.location += 1\n", + " \n", + " def eat(self, thing):\n", + " '''returns True upon success or False otherwise'''\n", + " if isinstance(thing, Food):\n", + " return True\n", + " return False\n", + " \n", + " def drink(self, thing):\n", + " ''' returns True upon success or False otherwise'''\n", + " if isinstance(thing, Water):\n", + " return True\n", + " return False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now its time to implement a program module for our dog. A program controls how the dog acts upon its environment. Our program will be very simple, and is shown in the table below.\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    Percept: Feel Food Feel WaterFeel Nothing
    Action: eatdrinkmove down
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def program(percepts):\n", + " '''Returns an action based on the dog's percepts'''\n", + " for p in percepts:\n", + " if isinstance(p, Food):\n", + " return 'eat'\n", + " elif isinstance(p, Water):\n", + " return 'drink'\n", + " return 'move down'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now run our simulation by creating a park with some food, water, and our dog." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "park = Park()\n", + "dog = BlindDog(program)\n", + "dogfood = Food()\n", + "water = Water()\n", + "park.add_thing(dog, 1)\n", + "park.add_thing(dogfood, 5)\n", + "park.add_thing(water, 7)\n", "\n", - "color = {\"Breeze\": (225, 225, 225),\n", - " \"Pit\": (0,0,0),\n", - " \"Gold\": (253, 208, 23),\n", - " \"Glitter\": (253, 208, 23),\n", - " \"Wumpus\": (43, 27, 23),\n", - " \"Stench\": (128, 128, 128),\n", - " \"Explorer\": (0, 0, 255),\n", - " \"Wall\": (44, 53, 57)\n", - " }\n", + "park.run(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that the dog moved from location 1 to 4, over 4 steps, and ate food at location 5 in the 5th step.\n", "\n", - "def program(percepts):\n", - " '''Returns an action based on it's percepts'''\n", - " print(percepts)\n", - " return input()\n", + "Let's continue this simulation for 5 more steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "park.run(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect! Note how the simulation stopped after the dog drank the water - exhausting all the food and water ends our simulation, as we had defined before. Let's add some more water and see if our dog can reach it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "park.add_thing(water, 15)\n", + "park.run(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above, we learnt to implement an agent, its program, and an environment on which it acts. However, this was a very simple case. Let's try to add complexity to it by creating a 2-Dimensional environment!\n", "\n", - "w = WumpusEnvironment(program, 7, 7) \n", - "grid = BlockGrid(w.width, w.height, fill=(123, 234, 123))\n", "\n", - "def draw_grid(world):\n", - " global grid\n", - " grid[:] = (123, 234, 123)\n", - " for x in range(0, len(world)):\n", - " for y in range(0, len(world[x])):\n", - " if len(world[x][y]):\n", - " grid[y, x] = color[world[x][y][-1].__class__.__name__]\n", + "## AGENTS IN A 2D ENVIRONMENT\n", "\n", - "def step():\n", - " global grid, w\n", - " draw_grid(w.get_world())\n", - " grid.show()\n", - " w.step()" + "For us to not read so many logs of what our dog did, we add a bit of graphics while making our Park 2D. To do so, we will need to make it a subclass of GraphicEnvironment instead of Environment. Parks implemented by subclassing GraphicEnvironment class adds these extra properties to it:\n", + "\n", + " - Our park is indexed in the 4th quadrant of the X-Y plane.\n", + " - Every time we create a park subclassing GraphicEnvironment, we need to define the colors of all the things we plan to put into the park. The colors are defined in typical [RGB digital 8-bit format](https://en.wikipedia.org/wiki/RGB_color_model#Numeric_representations), common across the web.\n", + " - Fences are added automatically to all parks so that our dog does not go outside the park's boundary - it just isn't safe for blind dogs to be outside the park by themselves! GraphicEnvironment provides `is_inbounds` function to check if our dog tries to leave the park.\n", + " \n", + "First let us try to upgrade our 1-dimensional `Park` environment by just replacing its superclass by `GraphicEnvironment`. " ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "
    " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[], [None], [], [], [None]]\n", - "2\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "step()" + "class Park2D(GraphicEnvironment):\n", + " def percept(self, agent):\n", + " '''return a list of things that are in our agent's location'''\n", + " things = self.list_things_at(agent.location)\n", + " return things\n", + " \n", + " def execute_action(self, agent, action):\n", + " '''changes the state of the environment based on what the agent does.'''\n", + " if action == \"move down\":\n", + " print('{} decided to {} at location: {}'.format(str(agent)[1:-1], action, agent.location))\n", + " agent.movedown()\n", + " elif action == \"eat\":\n", + " items = self.list_things_at(agent.location, tclass=Food)\n", + " if len(items) != 0:\n", + " if agent.eat(items[0]): #Have the dog eat the first item\n", + " print('{} ate {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", + " self.delete_thing(items[0]) #Delete it from the Park after.\n", + " elif action == \"drink\":\n", + " items = self.list_things_at(agent.location, tclass=Water)\n", + " if len(items) != 0:\n", + " if agent.drink(items[0]): #Have the dog drink the first item\n", + " print('{} drank {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", + " self.delete_thing(items[0]) #Delete it from the Park after.\n", + " \n", + " def is_done(self):\n", + " '''By default, we're done when we can't find a live agent, \n", + " but to prevent killing our cute dog, we will stop before itself - when there is no more food or water'''\n", + " no_edibles = not any(isinstance(thing, Food) or isinstance(thing, Water) for thing in self.things)\n", + " dead_agents = not any(agent.is_alive() for agent in self.agents)\n", + " return dead_agents or no_edibles\n", + "\n", + "class BlindDog(Agent):\n", + " location = [0,1] # change location to a 2d value\n", + " direction = Direction(\"down\") # variable to store the direction our dog is facing\n", + " \n", + " def movedown(self):\n", + " self.location[1] += 1\n", + " \n", + " def eat(self, thing):\n", + " '''returns True upon success or False otherwise'''\n", + " if isinstance(thing, Food):\n", + " return True\n", + " return False\n", + " \n", + " def drink(self, thing):\n", + " ''' returns True upon success or False otherwise'''\n", + " if isinstance(thing, Water):\n", + " return True\n", + " return False" ] }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, + "source": [ + "Now let's test this new park with our same dog, food and water. We color our dog with a nice red and mark food and water with orange and blue respectively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "park = Park2D(5,20, color={'BlindDog': (200,0,0), 'Water': (0, 200, 200), 'Food': (230, 115, 40)}) # park width is set to 5, and height to 20\n", + "dog = BlindDog(program)\n", + "dogfood = Food()\n", + "water = Water()\n", + "park.add_thing(dog, [0,1])\n", + "park.add_thing(dogfood, [0,5])\n", + "park.add_thing(water, [0,7])\n", + "morewater = Water()\n", + "park.add_thing(morewater, [0,15])\n", + "print(\"BlindDog starts at (1,1) facing downwards, lets see if he can find any food!\")\n", + "park.run(20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "# PROGRAM #\n", - "Now that we have a Park Class, we need to implement a program module for our dog. A program controls how the dog acts upon it's environment. Our program will be very simple, and is shown in the table below.\n", + "Adding some graphics was a good idea! We immediately see that the code works, but our blind dog doesn't make any use of the 2 dimensional space available to him. Let's make our dog more energetic so that he turns and moves forward, instead of always moving down. In doing so, we'll also need to make some changes to our environment to be able to handle this extra motion.\n", + "\n", + "### PROGRAM - EnergeticBlindDog\n", + "\n", + "Let's make our dog turn or move forwards at random - except when he's at the edge of our park - in which case we make him change his direction explicitly by turning to avoid trying to leave the park. However, our dog is blind so he wouldn't know which way to turn - he'd just have to try arbitrarily.\n", + "\n", "\n", " \n", " \n", @@ -226,117 +461,254 @@ " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - "
    Percept: Action: eatdrinkmove up\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    Remember being at Edge : At EdgeNot at Edge
    Action : Turn Left / Turn Right
    ( 50% - 50% chance )
    Turn Left / Turn Right / Move Forward
    ( 25% - 25% - 50% chance )
    \n", + "
    \n" + "" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ - "class BlindDog(Agent):\n", - " location = 1\n", + "from random import choice\n", + "\n", + "class EnergeticBlindDog(Agent):\n", + " location = [0,1]\n", + " direction = Direction(\"down\")\n", " \n", - " def movedown(self):\n", - " self.location += 1\n", + " def moveforward(self, success=True):\n", + " '''moveforward possible only if success (i.e. valid destination location)'''\n", + " if not success:\n", + " return\n", + " if self.direction.direction == Direction.R:\n", + " self.location[0] += 1\n", + " elif self.direction.direction == Direction.L:\n", + " self.location[0] -= 1\n", + " elif self.direction.direction == Direction.D:\n", + " self.location[1] += 1\n", + " elif self.direction.direction == Direction.U:\n", + " self.location[1] -= 1\n", + " \n", + " def turn(self, d):\n", + " self.direction = self.direction + d\n", " \n", " def eat(self, thing):\n", " '''returns True upon success or False otherwise'''\n", " if isinstance(thing, Food):\n", - " print(\"Dog: Ate food at {}.\".format(self.location))\n", " return True\n", " return False\n", " \n", " def drink(self, thing):\n", " ''' returns True upon success or False otherwise'''\n", " if isinstance(thing, Water):\n", - " print(\"Dog: Drank water at {}.\".format(self.location))\n", " return True\n", " return False\n", " \n", "def program(percepts):\n", " '''Returns an action based on it's percepts'''\n", - " for p in percepts:\n", + " \n", + " for p in percepts: # first eat or drink - you're a dog!\n", " if isinstance(p, Food):\n", " return 'eat'\n", " elif isinstance(p, Water):\n", " return 'drink'\n", - " return 'move down'\n", - " \n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "park = Park()\n", - "dog = BlindDog(program)\n", - "dogfood = Food()\n", - "water = Water()\n", - "park.add_thing(dog, 0)\n", - "park.add_thing(dogfood, 5)\n", - "park.add_thing(water, 7)\n", - "\n", - "park.run(10)" + " if isinstance(p,Bump): # then check if you are at an edge and have to turn\n", + " turn = False\n", + " choice = random.choice((1,2));\n", + " else:\n", + " choice = random.choice((1,2,3,4)) # 1-right, 2-left, others-forward\n", + " if choice == 1:\n", + " return 'turnright'\n", + " elif choice == 2:\n", + " return 'turnleft'\n", + " else:\n", + " return 'moveforward'\n", + " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That's how easy it is to implement an agent, its program, and environment. But that was a very simple case. What if our environment was 2-Dimentional instead of 1? And what if we had multiple agents?\n", + "### ENVIRONMENT - Park2D\n", "\n", - "To make our Park 2D, we will need to make it a subclass of XYEnvironment instead of Environment. Also, let's add a person to play fetch with the dog." + "We also need to modify our park accordingly, in order to be able to handle all the new actions our dog wishes to execute. Additionally, we'll need to prevent our dog from moving to locations beyond our park boundary - it just isn't safe for blind dogs to be outside the park by themselves." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "class Park(XYEnvironment):\n", + "class Park2D(GraphicEnvironment):\n", " def percept(self, agent):\n", - " '''prints & return a list of things that are in our agent's location'''\n", + " '''return a list of things that are in our agent's location'''\n", " things = self.list_things_at(agent.location)\n", - " print(things)\n", + " loc = copy.deepcopy(agent.location) # find out the target location\n", + " #Check if agent is about to bump into a wall\n", + " if agent.direction.direction == Direction.R:\n", + " loc[0] += 1\n", + " elif agent.direction.direction == Direction.L:\n", + " loc[0] -= 1\n", + " elif agent.direction.direction == Direction.D:\n", + " loc[1] += 1\n", + " elif agent.direction.direction == Direction.U:\n", + " loc[1] -= 1\n", + " if not self.is_inbounds(loc):\n", + " things.append(Bump())\n", " return things\n", " \n", " def execute_action(self, agent, action):\n", " '''changes the state of the environment based on what the agent does.'''\n", - " if action == \"move down\":\n", - " agent.movedown()\n", + " if action == 'turnright':\n", + " print('{} decided to {} at location: {}'.format(str(agent)[1:-1], action, agent.location))\n", + " agent.turn(Direction.R)\n", + " elif action == 'turnleft':\n", + " print('{} decided to {} at location: {}'.format(str(agent)[1:-1], action, agent.location))\n", + " agent.turn(Direction.L)\n", + " elif action == 'moveforward':\n", + " print('{} decided to move {}wards at location: {}'.format(str(agent)[1:-1], agent.direction.direction, agent.location))\n", + " agent.moveforward()\n", " elif action == \"eat\":\n", " items = self.list_things_at(agent.location, tclass=Food)\n", " if len(items) != 0:\n", - " if agent.eat(items[0]): #Have the dog pick eat the first item\n", - " self.delete_thing(items[0]) #Delete it from the Park after.\n", + " if agent.eat(items[0]):\n", + " print('{} ate {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", + " self.delete_thing(items[0])\n", " elif action == \"drink\":\n", " items = self.list_things_at(agent.location, tclass=Water)\n", " if len(items) != 0:\n", - " if agent.drink(items[0]): #Have the dog drink the first item\n", - " self.delete_thing(items[0]) #Delete it from the Park after.\n", + " if agent.drink(items[0]):\n", + " print('{} drank {} at location: {}'\n", + " .format(str(agent)[1:-1], str(items[0])[1:-1], agent.location))\n", + " self.delete_thing(items[0])\n", " \n", " def is_done(self):\n", " '''By default, we're done when we can't find a live agent, \n", - " but to prevent killing our cute dog, we will or it with when there is no more food or water'''\n", + " but to prevent killing our cute dog, we will stop before itself - when there is no more food or water'''\n", " no_edibles = not any(isinstance(thing, Food) or isinstance(thing, Water) for thing in self.things)\n", " dead_agents = not any(agent.is_alive() for agent in self.agents)\n", - " return dead_agents or no_edibles" + " return dead_agents or no_edibles\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that our park is ready for the 2D motion of our energetic dog, lets test it!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "park = Park2D(5,5, color={'EnergeticBlindDog': (200,0,0), 'Water': (0, 200, 200), 'Food': (230, 115, 40)})\n", + "dog = EnergeticBlindDog(program)\n", + "dogfood = Food()\n", + "water = Water()\n", + "park.add_thing(dog, [0,0])\n", + "park.add_thing(dogfood, [1,2])\n", + "park.add_thing(water, [0,1])\n", + "morewater = Water()\n", + "morefood = Food()\n", + "park.add_thing(morewater, [2,4])\n", + "park.add_thing(morefood, [4,3])\n", + "print(\"dog started at [0,0], facing down. Let's see if he found any food or water!\")\n", + "park.run(20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "## Wumpus Environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipythonblocks import BlockGrid\n", + "from agents import *\n", + "\n", + "color = {\"Breeze\": (225, 225, 225),\n", + " \"Pit\": (0,0,0),\n", + " \"Gold\": (253, 208, 23),\n", + " \"Glitter\": (253, 208, 23),\n", + " \"Wumpus\": (43, 27, 23),\n", + " \"Stench\": (128, 128, 128),\n", + " \"Explorer\": (0, 0, 255),\n", + " \"Wall\": (44, 53, 57)\n", + " }\n", + "\n", + "def program(percepts):\n", + " '''Returns an action based on it's percepts'''\n", + " print(percepts)\n", + " return input()\n", + "\n", + "w = WumpusEnvironment(program, 7, 7) \n", + "grid = BlockGrid(w.width, w.height, fill=(123, 234, 123))\n", + "\n", + "def draw_grid(world):\n", + " global grid\n", + " grid[:] = (123, 234, 123)\n", + " for x in range(0, len(world)):\n", + " for y in range(0, len(world[x])):\n", + " if len(world[x][y]):\n", + " grid[y, x] = color[world[x][y][-1].__class__.__name__]\n", + "\n", + "def step():\n", + " global grid, w\n", + " draw_grid(w.get_world())\n", + " grid.show()\n", + " w.step()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "step()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -355,9 +727,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.6.4" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/agents.py b/agents.py index 21dedaa15..d29b0c382 100644 --- a/agents.py +++ b/agents.py @@ -1,4 +1,5 @@ -"""Implement Agents and Environments (Chapters 1-2). +""" +Implement Agents and Environments. (Chapters 1-2) The class hierarchies are as follows: @@ -23,78 +24,76 @@ EnvToolbar ## contains buttons for controlling EnvGUI EnvCanvas ## Canvas to display the environment of an EnvGUI - """ -# TO DO: -# Implement grabbing correctly. -# When an object is grabbed, does it still have a location? -# What if it is released? -# What if the grabbed or the grabber is deleted? -# What if the grabber moves? -# +# TODO # Speed control in GUI does not have any effect -- fix it. -from grid import distance2, turn_heading +from utils import distance_squared, turn_heading from statistics import mean +from ipythonblocks import BlockGrid +from IPython.display import HTML, display, clear_output +from time import sleep import random import copy import collections +import numbers -# ______________________________________________________________________________ +# ______________________________________________________________________________ -class Thing(object): +class Thing: """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 + 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__)) def is_alive(self): - "Things that are 'alive' should return true." + """Things that are 'alive' should return true.""" return hasattr(self, 'alive') and self.alive def show_state(self): - "Display the agent's internal state. Subclasses should override." + """Display the agent's internal state. Subclasses should override.""" print("I don't know how to show_state.") def display(self, canvas, x, y, width, height): + """Display an image of this Thing on the canvas.""" # Do we need this? - "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 + """An Agent is a subclass of Thing with one required instance attribute + (aka slot), .program, which should hold a function that takes one argument, + the percept, and returns an action. (What counts as a percept or action will depend on the specific environment in which the agent exists.) - Note that 'program' is a slot, not a method. If it were a method, - then the program could 'cheat' and look at aspects of the agent. - It's not supposed to do that: the program can only look at the - percepts. An agent program that needs a model of the world (and of - the agent itself) will have to build and maintain its own model. - There is an optional slot, .performance, which is a number giving - the performance measure of the agent in its environment.""" + Note that 'program' is a slot, not a method. If it were a method, then the + program could 'cheat' and look at aspects of the agent. It's not supposed + to do that: the program can only look at the percepts. An agent program + that needs a model of the world (and of the agent itself) will have to + build and maintain its own model. There is an optional slot, .performance, + which is a number giving the performance measure of the agent in its + environment.""" def __init__(self, program=None): self.alive = True self.bump = False self.holding = [] self.performance = 0 - if program is None: + if program is None or not isinstance(program, collections.abc.Callable): + print("Can't find a valid program for {}, falling back to default.".format(self.__class__.__name__)) + def program(percept): - return eval(input('Percept={}; action? ' .format(percept))) - assert isinstance(program, collections.Callable) + return eval(input('Percept={}; action? '.format(percept))) + self.program = program def can_grab(self, thing): - """Returns True if this agent can grab this thing. + """Return True if this agent can grab this thing. Override for appropriate subclasses of Agent and Thing.""" return False @@ -108,88 +107,139 @@ def new_program(percept): action = old_program(percept) print('{} perceives {} and does {}'.format(agent, percept, action)) return action + agent.program = new_program return agent + # ______________________________________________________________________________ def TableDrivenAgentProgram(table): - """This agent selects an action based on the percept sequence. + """ + [Figure 2.7] + 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. [Figure 2.7]""" + {percept_sequence:action} pairs. + """ 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." + """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) + # ______________________________________________________________________________ def SimpleReflexAgentProgram(rules, interpret_input): - "This agent takes action based solely on the percept. [Figure 2.10]" + """ + [Figure 2.10] + This agent takes action based solely on the percept. + """ + def program(percept): state = interpret_input(percept) rule = rule_match(state, rules) action = rule.action return action + return program -def ModelBasedReflexAgentProgram(rules, update_state): - "This agent takes action based on the percept and state. [Figure 2.12]" +def ModelBasedReflexAgentProgram(rules, update_state, model): + """ + [Figure 2.12] + This agent takes action based on the percept and state. + """ + def program(percept): - program.state = update_state(program.state, program.action, percept) + program.state = update_state(program.state, program.action, percept, model) rule = rule_match(program.state, rules) action = rule.action return action + program.state = program.action = None return program def rule_match(state, rules): - "Find the first rule that matches state." + """Find the first rule that matches state.""" for rule in rules: if rule.matches(state): return rule + # ______________________________________________________________________________ + loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world 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'])) def TableDrivenVacuumAgent(): - "[Figure 2.3]" + """Tabular approach towards vacuum world as mentioned in [Figure 2.3] + >>> agent = TableDrivenVacuumAgent() + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + True + """ table = {((loc_A, 'Clean'),): 'Right', ((loc_A, 'Dirty'),): 'Suck', ((loc_B, 'Clean'),): 'Left', ((loc_B, 'Dirty'),): 'Suck', - ((loc_A, 'Clean'), (loc_A, 'Clean')): 'Right', - ((loc_A, 'Clean'), (loc_A, 'Dirty')): 'Suck', - # ... - ((loc_A, 'Clean'), (loc_A, 'Clean'), (loc_A, 'Clean')): 'Right', - ((loc_A, 'Clean'), (loc_A, 'Clean'), (loc_A, 'Dirty')): 'Suck', - # ... - } + ((loc_A, 'Dirty'), (loc_A, 'Clean')): 'Right', + ((loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck', + ((loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck', + ((loc_B, 'Dirty'), (loc_B, 'Clean')): 'Left', + ((loc_A, 'Dirty'), (loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck', + ((loc_B, 'Dirty'), (loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck'} return Agent(TableDrivenAgentProgram(table)) def ReflexVacuumAgent(): - "A reflex agent for the two-state vacuum environment. [Figure 2.8]" + """ + [Figure 2.8] + A reflex agent for the two-state vacuum environment. + >>> 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': @@ -198,15 +248,23 @@ def program(percept): 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." + """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): - "Same as ReflexVacuumAgent, except if everything is clean, do NoOp." + """Same as ReflexVacuumAgent, except if everything is clean, do NoOp.""" location, status = percept model[location] = status # Update the model here if model[loc_A] == model[loc_B] == 'Clean': @@ -217,14 +275,15 @@ def program(percept): return 'Right' elif location == loc_B: return 'Left' + return Agent(program) -# ______________________________________________________________________________ +# ______________________________________________________________________________ -class Environment(object): - """Abstract class representing an Environment. 'Real' Environment classes +class Environment: + """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. execute_action: Define the effects of executing an action. @@ -242,32 +301,29 @@ def thing_classes(self): 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.) - ''' + """Return the percept that the agent sees at this point. (Implement this.)""" raise NotImplementedError def execute_action(self, agent, action): - "Change the world to reflect this action. (Implement this.)" + """Change the world to reflect this action. (Implement this.)""" raise NotImplementedError def default_location(self, thing): - "Default location to place a new thing with unspecified location." + """Default location to place a new thing with unspecified location.""" return None def exogenous_change(self): - "If there is spontaneous change in the world, override this." + """If there is spontaneous change in the world, override this.""" pass def is_done(self): - "By default, we're done when we can't find a live agent." + """By default, we're done when we can't find a live agent.""" return not any(agent.is_alive() for agent in self.agents) def step(self): """Run the environment for one time step. If the actions and exogenous changes are independent, this method will - do. If there are interactions between them, you'll need to + do. If there are interactions between them, you'll need to override this method.""" if not self.is_done(): actions = [] @@ -281,16 +337,19 @@ def step(self): self.exogenous_change() def run(self, steps=1000): - "Run the Environment for given number of time steps." + """Run the Environment for given number of time steps.""" for step in range(steps): if self.is_done(): return self.step() def list_things_at(self, location, tclass=Thing): - "Return all things exactly at a given location." + """Return all things exactly at a given location.""" + if isinstance(location, numbers.Number): + return [thing for thing in self.things + if thing.location == location and isinstance(thing, tclass)] return [thing for thing in self.things - if thing.location == location and isinstance(thing, tclass)] + if all(x == y for x, y in zip(thing.location, location)) and isinstance(thing, tclass)] def some_things_at(self, location, tclass=Thing): """Return true if at least one of the things at location @@ -300,15 +359,17 @@ def some_things_at(self, location, tclass=Thing): def add_thing(self, thing, location=None): """Add a thing to the environment, setting its location. For convenience, if thing is an agent program we make a new agent - for it. (Shouldn't need to override this.""" + for it. (Shouldn't need to override this.)""" if not isinstance(thing, Thing): thing = Agent(thing) - assert thing not in self.things, "Don't add the same thing twice" - thing.location = location if location is not None else self.default_location(thing) - self.things.append(thing) - if isinstance(thing, Agent): - thing.performance = 0 - self.agents.append(thing) + if thing in self.things: + print("Can't add the same thing twice") + else: + thing.location = location if location is not None else self.default_location(thing) + self.things.append(thing) + if isinstance(thing, Agent): + thing.performance = 0 + self.agents.append(thing) def delete_thing(self, thing): """Remove a thing from the environment.""" @@ -317,19 +378,20 @@ def delete_thing(self, thing): except ValueError as e: print(e) 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])) + print(" Thing to be removed: {} at {}".format(thing, thing.location)) + print(" from list: {}".format([(thing, thing.location) for thing in self.things])) if thing in self.agents: self.agents.remove(thing) -class Direction(): - '''A direction class for agents that want to move in a 2D plane + +class Direction: + """A direction class for agents that want to move in a 2D plane Usage: - d = Direction("Down") + d = Direction("down") To change directions: d = d + "right" or d = d + Direction.R #Both do the same thing Note that the argument to __add__ must be a string and not a Direction object. - Also, it (the argument) can only be right or left. ''' + Also, it (the argument) can only be right or left.""" R = "right" L = "left" @@ -340,51 +402,78 @@ 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{ + return { self.R: Direction(self.D), self.L: Direction(self.U), }.get(heading, None) elif self.direction == self.L: - return{ + return { self.R: Direction(self.U), - self.L: Direction(self.L), + self.L: Direction(self.D), }.get(heading, None) elif self.direction == self.U: - return{ + return { self.R: Direction(self.R), self.L: Direction(self.L), }.get(heading, None) elif self.direction == self.D: - return{ + return { self.R: Direction(self.L), self.L: Direction(self.R), }.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) + """ + # get the iterable class to return + iclass = from_location.__class__ x, y = from_location if self.direction == self.R: - return (x+1, y) + return iclass((x + 1, y)) elif self.direction == self.L: - return (x-1, y) + return iclass((x - 1, y)) elif self.direction == self.U: - return (x, y-1) + return iclass((x, y - 1)) elif self.direction == self.D: - return (x, y+1) + return iclass((x, y + 1)) class XYEnvironment(Environment): - """This class is for environments on a 2D plane, with locations labelled by (x, y) points, either discrete or continuous. - Agents perceive things within a radius. Each agent in the + Agents perceive things within a radius. Each agent in the environment has a .location slot which should be a location such as (0, 1), and a .holding slot, which should be a list of things that are held.""" def __init__(self, width=10, height=10): - super(XYEnvironment, self).__init__() + super().__init__() self.width = width self.height = height @@ -396,40 +485,48 @@ def __init__(self, width=10, height=10): perceptible_distance = 1 def things_near(self, location, radius=None): - "Return all things within radius of location." + """Return all things within radius of location.""" if radius is None: radius = self.perceptible_distance radius2 = radius * radius - return [(thing, radius2 - distance2(location, thing.location)) for thing in self.things - if distance2(location, thing.location) <= radius2] + return [(thing, radius2 - distance_squared(location, thing.location)) + for thing in self.things if distance_squared( + location, thing.location) <= radius2] def percept(self, agent): - '''By default, agent perceives things within a default radius.''' + """By default, agent perceives things within a default radius.""" return self.things_near(agent.location) def execute_action(self, agent, action): agent.bump = False if action == 'TurnRight': - agent.direction = agent.direction + Direction.R + agent.direction += Direction.R elif action == 'TurnLeft': - agent.direction = agent.direction + Direction.L + agent.direction += Direction.L elif action == 'Forward': agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location)) -# elif action == 'Grab': -# things = [thing for thing in self.list_things_at(agent.location) -# if agent.can_grab(thing)] -# if things: -# agent.holding.append(things[0]) + elif action == 'Grab': + things = [thing for thing in self.list_things_at(agent.location) if agent.can_grab(thing)] + if things: + agent.holding.append(things[0]) + print("Grabbing ", things[0].__class__.__name__) + self.delete_thing(things[0]) elif action == 'Release': if agent.holding: - agent.holding.pop() + dropped = agent.holding.pop() + print("Dropping ", dropped.__class__.__name__) + self.add_thing(dropped, location=agent.location) def default_location(self, thing): - return (random.choice(self.width), random.choice(self.height)) + location = self.random_location_inbounds() + while self.some_things_at(location, Obstacle): + # we will find a random location with no obstacles + location = self.random_location_inbounds() + return location def move_to(self, thing, destination): - '''Move a thing to a new location. Returns True on success or False if there is an Obstacle - If thing is grabbing anything, they move with him ''' + """Move a thing to a new location. Returns True on success or False if there is an Obstacle. + If thing is holding anything, they move with him.""" thing.bump = self.some_things_at(destination, Obstacle) if not thing.bump: thing.location = destination @@ -441,54 +538,47 @@ def move_to(self, thing, destination): t.location = destination return thing.bump - # def add_thing(self, thing, location=(1, 1)): - # super(XYEnvironment, self).add_thing(thing, location) - # thing.holding = [] - # thing.held = None - # for obs in self.observers: - # obs.thing_added(thing) - - def add_thing(self, thing, location=(1, 1), exclude_duplicate_class_items=False): - '''Adds things to the world. - If (exclude_duplicate_class_items) then the item won't be added if the location - has at least one item of the same class''' - if (self.is_inbounds(location)): + def add_thing(self, thing, location=None, exclude_duplicate_class_items=False): + """Add things to the world. If (exclude_duplicate_class_items) then the item won't be + added if the location has at least one item of the same class.""" + if location is None: + super().add_thing(thing) + elif self.is_inbounds(location): if (exclude_duplicate_class_items and - any(isinstance(t, thing.__class__) for t in self.list_things_at(location))): - return - super(XYEnvironment, self).add_thing(thing, location) + any(isinstance(t, thing.__class__) for t in self.list_things_at(location))): + return + super().add_thing(thing, location) def is_inbounds(self, location): - '''Checks to make sure that the location is inbounds (within walls if we have walls)''' - x,y = location - return not (x < self.x_start or x >= self.x_end or y < self.y_start or y >= self.y_end) + """Checks to make sure that the location is inbounds (within walls if we have walls)""" + x, y = location + return not (x < self.x_start or x > self.x_end or y < self.y_start or y > self.y_end) def random_location_inbounds(self, exclude=None): - '''Returns a random location that is inbounds (within walls if we have walls)''' - location = (random.randint(self.x_start, self.x_end), random.randint(self.y_start, self.y_end)) + """Returns a random location that is inbounds (within walls if we have walls)""" + location = (random.randint(self.x_start, self.x_end), + random.randint(self.y_start, self.y_end)) if exclude is not None: - while(location == exclude): - location = (random.randint(self.x_start, self.x_end), random.randint(self.y_start, self.y_end)) + while location == exclude: + location = (random.randint(self.x_start, self.x_end), + random.randint(self.y_start, self.y_end)) return location def delete_thing(self, thing): - '''Deletes thing, and everything it is holding (if thing is an agent)''' + """Deletes thing, and everything it is holding (if thing is an agent)""" if isinstance(thing, Agent): - for obj in thing.holding: - super(XYEnvironment, self).delete_thing(obj) - for obs in self.observers: - obs.thing_deleted(obj) + del thing.holding - super(XYEnvironment, self).delete_thing(thing) + super().delete_thing(thing) for obs in self.observers: obs.thing_deleted(thing) def add_walls(self): - '''Put walls around the entire perimeter of the grid.''' + """Put walls around the entire perimeter of the grid.""" for x in range(self.width): self.add_thing(Wall(), (x, 0)) self.add_thing(Wall(), (x, self.height - 1)) - for y in range(self.height): + for y in range(1, self.height - 1): self.add_thing(Wall(), (0, y)) self.add_thing(Wall(), (self.width - 1, y)) @@ -506,12 +596,11 @@ def add_observer(self, observer): self.observers.append(observer) def turn_heading(self, heading, inc): - "Return the heading to the left (inc=+1) or right (inc=-1) of heading." + """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 @@ -521,14 +610,100 @@ class Wall(Obstacle): pass +# ______________________________________________________________________________ + + +class GraphicEnvironment(XYEnvironment): + def __init__(self, width=10, height=10, boundary=True, color={}, display=False): + """Define all the usual XYEnvironment characteristics, + but initialise a BlockGrid for GUI too.""" + super().__init__(width, height) + self.grid = BlockGrid(width, height, fill=(200, 200, 200)) + if display: + self.grid.show() + self.visible = True + else: + self.visible = False + self.bounded = boundary + self.colors = color + + def get_world(self): + """Returns all the items in the world in a format + understandable by the ipythonblocks BlockGrid.""" + result = [] + x_start, y_start = (0, 0) + x_end, y_end = self.width, self.height + for x in range(x_start, x_end): + row = [] + for y in range(y_start, y_end): + row.append(self.list_things_at((x, y))) + result.append(row) + return result + + """ + def run(self, steps=1000, delay=1): + "" "Run the Environment for given number of time steps, + but update the GUI too." "" + for step in range(steps): + sleep(delay) + if self.visible: + self.reveal() + if self.is_done(): + if self.visible: + self.reveal() + return + self.step() + if self.visible: + self.reveal() + """ + + def run(self, steps=1000, delay=1): + """Run the Environment for given number of time steps, + but update the GUI too.""" + for step in range(steps): + self.update(delay) + if self.is_done(): + break + self.step() + self.update(delay) + + def update(self, delay=1): + sleep(delay) + self.reveal() + + def reveal(self): + """Display the BlockGrid for this world - the last thing to be added + at a location defines the location color.""" + self.draw_world() + # wait for the world to update and + # apply changes to the same grid instead + # of making a new one. + clear_output(1) + self.grid.show() + self.visible = True + + def draw_world(self): + self.grid[:] = (200, 200, 200) + world = self.get_world() + for x in range(0, len(world)): + for y in range(0, len(world[x])): + if len(world[x][y]): + self.grid[y, x] = self.colors[world[x][y][-1].__class__.__name__] + + def conceal(self): + """Hide the BlockGrid for this world""" + self.visible = False + display(HTML('')) + # ______________________________________________________________________________ # Continuous environment class ContinuousWorld(Environment): - """ Model for Continuous World. """ + """Model for Continuous World""" + def __init__(self, width=10, height=10): - super(ContinuousWorld, self).__init__() + super().__init__() self.width = width self.height = height @@ -537,11 +712,13 @@ def add_obstacle(self, coordinates): class PolygonObstacle(Obstacle): + def __init__(self, coordinates): - """ Coordinates is a list of tuples. """ - super(PolygonObstacle, self).__init__() + """Coordinates is a list of tuples.""" + super().__init__() self.coordinates = coordinates + # ______________________________________________________________________________ # Vacuum environment @@ -551,14 +728,13 @@ class Dirt(Thing): 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 each turn taken.""" def __init__(self, width=10, height=10): - super(VacuumEnvironment, self).__init__(width, height) + super().__init__(width, height) self.add_walls() def thing_classes(self): @@ -570,10 +746,11 @@ def percept(self, agent): Unlike the TrivialVacuumEnvironment, location is NOT perceived.""" status = ('Dirty' if self.some_things_at( agent.location, Dirt) else 'Clean') - bump = ('Bump' if agent.bump else'None') - return (status, bump) + bump = ('Bump' if agent.bump else 'None') + return status, bump def execute_action(self, agent, action): + agent.bump = False if action == 'Suck': dirt_list = self.list_things_at(agent.location, Dirt) if dirt_list != []: @@ -581,31 +758,29 @@ def execute_action(self, agent, action): agent.performance += 100 self.delete_thing(dirt) else: - super(VacuumEnvironment, self).execute_action(agent, action) + super().execute_action(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 + or Clean. The agent perceives its location and the location's status. This serves as an example of how to implement a simple Environment.""" def __init__(self): - super(TrivialVacuumEnvironment, self).__init__() + super().__init__() self.status = {loc_A: random.choice(['Clean', 'Dirty']), loc_B: random.choice(['Clean', 'Dirty'])} def thing_classes(self): - return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, - TableDrivenVacuumAgent, ModelBasedVacuumAgent] + return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, TableDrivenVacuumAgent, ModelBasedVacuumAgent] def percept(self, agent): - "Returns the agent's location, and the location status (Dirty/Clean)." - return (agent.location, self.status[agent.location]) + """Returns the agent's location, and the location status (Dirty/Clean).""" + return agent.location, self.status[agent.location] def execute_action(self, agent, action): """Change agent's location and/or location's status; track performance. @@ -622,9 +797,10 @@ def execute_action(self, agent, action): self.status[agent.location] = 'Clean' def default_location(self, thing): - "Agents start in either location at random." + """Agents start in either location at random.""" return random.choice([loc_A, loc_B]) + # ______________________________________________________________________________ # The Wumpus World @@ -632,19 +808,24 @@ def default_location(self, thing): class Gold(Thing): def __eq__(self, rhs): - '''All Gold are equal''' + """All Gold are equal""" return rhs.__class__ == Gold + pass + class Bump(Thing): pass + class Glitter(Thing): pass + class Pit(Thing): pass + class Breeze(Thing): pass @@ -652,6 +833,7 @@ class Breeze(Thing): class Arrow(Thing): pass + class Scream(Thing): pass @@ -660,6 +842,7 @@ class Wumpus(Agent): screamed = False pass + class Stench(Thing): pass @@ -671,20 +854,21 @@ class Explorer(Agent): direction = Direction("right") def can_grab(self, thing): - '''Explorer can only grab gold''' + """Explorer can only grab gold""" return thing.__class__ == Gold class WumpusEnvironment(XYEnvironment): - pit_probability = 0.2 # Probability to spawn a pit in a location. (From Chapter 7.2) + pit_probability = 0.2 # Probability to spawn a pit in a location. (From Chapter 7.2) + # Room should be 4x4 grid of rooms. The extra 2 for walls def __init__(self, agent_program, width=6, height=6): - super(WumpusEnvironment, self).__init__(width, height) + super().__init__(width, height) self.init_world(agent_program) def init_world(self, program): - '''Spawn items to the world based on probabilities from the book''' + """Spawn items in the world based on probabilities from the book""" "WALLS" self.add_walls() @@ -709,16 +893,20 @@ def init_world(self, program): "GOLD" self.add_thing(Gold(), self.random_location_inbounds(exclude=(1, 1)), True) - #self.add_thing(Gold(), (2,1), True) Making debugging a whole lot easier "AGENT" self.add_thing(Explorer(program), (1, 1), True) def get_world(self, show_walls=True): - '''returns the items in the world''' + """Return the items in the world""" result = [] x_start, y_start = (0, 0) if show_walls else (1, 1) - x_end, y_end = (self.width, self.height) if show_walls else (self.width - 1, self.height - 1) + + if show_walls: + x_end, y_end = self.width, self.height + else: + x_end, y_end = self.width - 1, self.height - 1 + for x in range(x_start, x_end): row = [] for y in range(y_start, y_end): @@ -727,27 +915,28 @@ def get_world(self, show_walls=True): return result def percepts_from(self, agent, location, tclass=Thing): - '''Returns percepts from a given location, and replaces some items with percepts from chapter 7.''' + """Return percepts from a given location, + and replaces some items with percepts from chapter 7.""" thing_percepts = { Gold: Glitter(), Wall: Bump(), Wumpus: Stench(), Pit: Breeze()} - '''Agents don't need to get their percepts''' + + """Agents don't need to get their percepts""" thing_percepts[agent.__class__] = None - '''Gold only glitters in its cell''' + """Gold only glitters in its cell""" if location != agent.location: thing_percepts[Gold] = None - result = [thing_percepts.get(thing.__class__, thing) for thing in self.things if thing.location == location and isinstance(thing, tclass)] return result if len(result) else [None] def percept(self, agent): - '''Returns things in adjacent (not diagonal) cells of the agent. - Result format: [Left, Right, Up, Down, Center / Current location]''' + """Return things in adjacent (not diagonal) cells of the agent. + Result format: [Left, Right, Up, Down, Center / Current location]""" x, y = agent.location result = [] result.append(self.percepts_from(agent, (x - 1, y))) @@ -756,7 +945,7 @@ def percept(self, agent): result.append(self.percepts_from(agent, (x, y + 1))) result.append(self.percepts_from(agent, (x, y))) - '''The wumpus gives out a a loud scream once it's killed.''' + """The wumpus gives out a loud scream once it's killed.""" wumpus = [thing for thing in self.things if isinstance(thing, Wumpus)] if len(wumpus) and not wumpus[0].alive and not wumpus[0].screamed: result[-1].append(Scream()) @@ -765,39 +954,25 @@ def percept(self, agent): return result def execute_action(self, agent, action): - '''Modify the state of the environment based on the agent's actions - Performance score taken directly out of the book''' + """Modify the state of the environment based on the agent's actions. + Performance score taken directly out of the book.""" if isinstance(agent, Explorer) and self.in_danger(agent): return - + agent.bump = False - if action == 'TurnRight': - agent.direction = agent.direction + Direction.R - agent.performance -= 1 - elif action == 'TurnLeft': - agent.direction = agent.direction + Direction.L - agent.performance -= 1 - elif action == 'Forward': - agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location)) - agent.performance -= 1 - elif action == 'Grab': - things = [thing for thing in self.list_things_at(agent.location) - if agent.can_grab(thing)] - if len(things): - print("Grabbing", things[0].__class__.__name__) - if len(things): - agent.holding.append(things[0]) + if action in ['TurnRight', 'TurnLeft', 'Forward', 'Grab']: + super().execute_action(agent, action) agent.performance -= 1 elif action == 'Climb': if agent.location == (1, 1): # Agent can only climb out of (1,1) agent.performance += 1000 if Gold() in agent.holding else 0 self.delete_thing(agent) elif action == 'Shoot': - '''The arrow travels straight down the path the agent is facing''' + """The arrow travels straight down the path the agent is facing""" if agent.has_arrow: arrow_travel = agent.direction.move_forward(agent.location) - while(self.is_inbounds(arrow_travel)): + while self.is_inbounds(arrow_travel): wumpus = [thing for thing in self.list_things_at(arrow_travel) if isinstance(thing, Wumpus)] if len(wumpus): @@ -807,7 +982,7 @@ def execute_action(self, agent, action): agent.has_arrow = False def in_danger(self, agent): - '''Checks if Explorer is in danger (Pit or Wumpus), if he is, kill him''' + """Check if Explorer is in danger (Pit or Wumpus), if he is, kill him""" for thing in self.list_things_at(agent.location): if isinstance(thing, Pit) or (isinstance(thing, Wumpus) and thing.alive): agent.alive = False @@ -817,20 +992,22 @@ def in_danger(self, agent): return False def is_done(self): - '''The game is over when the Explorer is killed - or if he climbs out of the cave only at (1,1)''' - explorer = [agent for agent in self.agents if isinstance(agent, Explorer) ] + """The game is over when the Explorer is killed + or if he climbs out of the cave only at (1,1).""" + explorer = [agent for agent in self.agents if isinstance(agent, Explorer)] if len(explorer): - if explorer[0].alive: - return False - else: - print("Death by {} [-1000].".format(explorer[0].killed_by)) + if explorer[0].alive: + return False + else: + print("Death by {} [-1000].".format(explorer[0].killed_by)) else: print("Explorer climbed out {}." .format("with Gold [+1000]!" if Gold() not in self.things else "without Gold [+0]")) return True - #Almost done. Arrow needs to be implemented + # TODO: Arrow needs to be implemented + + # ______________________________________________________________________________ @@ -838,23 +1015,43 @@ 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_ModelBasedVacuumAgent = result[0][1] + >>> performance_ReflexVacuumAgent = result[1][1] + >>> performance_ReflexVacuumAgent <= performance_ModelBasedVacuumAgent + 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) env.run(steps) return agent.performance + return mean(map(score, envs)) + # _________________________________________________________________________ + __doc__ += """ >>> a = ReflexVacuumAgent() >>> a.program((loc_A, 'Clean')) diff --git a/agents4e.py b/agents4e.py new file mode 100644 index 000000000..75369a69a --- /dev/null +++ b/agents4e.py @@ -0,0 +1,1089 @@ +""" +Implement Agents and Environments. (Chapters 1-2) + +The class hierarchies are as follows: + +Thing ## A physical object that can exist in an environment + Agent + Wumpus + Dirt + Wall + ... + +Environment ## An environment holds objects, runs simulations + XYEnvironment + VacuumEnvironment + WumpusEnvironment + +An agent program is a callable instance, taking percepts and choosing actions + SimpleReflexAgentProgram + ... + +EnvGUI ## A window with a graphical representation of the Environment + +EnvToolbar ## contains buttons for controlling EnvGUI + +EnvCanvas ## Canvas to display the environment of an EnvGUI +""" + +# TODO +# Implement grabbing correctly. +# When an object is grabbed, does it still have a location? +# What if it is released? +# What if the grabbed or the grabber is deleted? +# What if the grabber moves? +# Speed control in GUI does not have any effect -- fix it. + +from utils4e import distance_squared, turn_heading +from statistics import mean +from ipythonblocks import BlockGrid +from IPython.display import HTML, display, clear_output +from time import sleep + +import random +import copy +import collections +import numbers + + +# ______________________________________________________________________________ + + +class Thing: + """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__)) + + def is_alive(self): + """Things that are 'alive' should return true.""" + return hasattr(self, 'alive') and self.alive + + def show_state(self): + """Display the agent's internal state. Subclasses should override.""" + print("I don't know how to show_state.") + + def display(self, canvas, x, y, width, height): + """Display an image of this Thing on the canvas.""" + # Do we need this? + 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 + will depend on the specific environment in which the agent exists.) + Note that 'program' is a slot, not a method. If it were a method, + then the program could 'cheat' and look at aspects of the agent. + It's not supposed to do that: the program can only look at the + percepts. An agent program that needs a model of the world (and of + the agent itself) will have to build and maintain its own model. + There is an optional slot, .performance, which is a number giving + the performance measure of the agent in its environment.""" + + def __init__(self, program=None): + self.alive = True + self.bump = False + self.holding = [] + self.performance = 0 + if program is None or not isinstance(program, collections.abc.Callable): + print("Can't find a valid program for {}, falling back to default.".format(self.__class__.__name__)) + + def program(percept): + return eval(input('Percept={}; action? '.format(percept))) + + self.program = program + + def can_grab(self, thing): + """Return True if this agent can grab this 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)) + return action + + agent.program = new_program + return agent + + +# ______________________________________________________________________________ + + +def TableDrivenAgentProgram(table): + """ + [Figure 2.7] + 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. + """ + 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. + >>> 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) + + +# ______________________________________________________________________________ + + +def SimpleReflexAgentProgram(rules, interpret_input): + """ + [Figure 2.10] + This agent takes action based solely on the percept. + """ + + def program(percept): + state = interpret_input(percept) + rule = rule_match(state, rules) + action = rule.action + return action + + return program + + +def ModelBasedReflexAgentProgram(rules, update_state, transition_model, sensor_model): + """ + [Figure 2.12] + This agent takes action based on the percept and state. + """ + + def program(percept): + program.state = update_state(program.state, program.action, percept, transition_model, sensor_model) + rule = rule_match(program.state, rules) + action = rule.action + return action + + program.state = program.action = None + return program + + +def rule_match(state, rules): + """Find the first rule that matches state.""" + for rule in rules: + if rule.matches(state): + return rule + + +# ______________________________________________________________________________ + + +loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world + + +def RandomVacuumAgent(): + """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'])) + + +def TableDrivenVacuumAgent(): + """Tabular approach towards vacuum world as mentioned in [Figure 2.3] + >>> agent = TableDrivenVacuumAgent() + >>> environment = TrivialVacuumEnvironment() + >>> environment.add_thing(agent) + >>> environment.run() + >>> environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + True + """ + table = {((loc_A, 'Clean'),): 'Right', + ((loc_A, 'Dirty'),): 'Suck', + ((loc_B, 'Clean'),): 'Left', + ((loc_B, 'Dirty'),): 'Suck', + ((loc_A, 'Dirty'), (loc_A, 'Clean')): 'Right', + ((loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck', + ((loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck', + ((loc_B, 'Dirty'), (loc_B, 'Clean')): 'Left', + ((loc_A, 'Dirty'), (loc_A, 'Clean'), (loc_B, 'Dirty')): 'Suck', + ((loc_B, 'Dirty'), (loc_B, 'Clean'), (loc_A, 'Dirty')): 'Suck'} + return Agent(TableDrivenAgentProgram(table)) + + +def ReflexVacuumAgent(): + """ + [Figure 2.8] + A reflex agent for the two-state vacuum environment. + >>> 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': + 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. + >>> 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): + """Same as ReflexVacuumAgent, except if everything is clean, do NoOp.""" + location, status = percept + 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: + """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. + execute_action: Define the effects of executing an action. + Also update the agent.performance slot. + The environment keeps a list of .things and .agents (which is a subset + of .things). Each agent has a .performance slot, initialized to 0. + Each thing has a .location slot, even though some environments may not + need this.""" + + def __init__(self): + self.things = [] + self.agents = [] + + def thing_classes(self): + 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.)""" + raise NotImplementedError + + def execute_action(self, agent, action): + """Change the world to reflect this action. (Implement this.)""" + raise NotImplementedError + + def default_location(self, thing): + """Default location to place a new thing with unspecified location.""" + return None + + def exogenous_change(self): + """If there is spontaneous change in the world, override this.""" + pass + + def is_done(self): + """By default, we're done when we can't find a live agent.""" + return not any(agent.is_alive() for agent in self.agents) + + def step(self): + """Run the environment for one time step. If the + actions and exogenous changes are independent, this method will + do. If there are interactions between them, you'll need to + override this method.""" + if not self.is_done(): + actions = [] + for agent in self.agents: + if agent.alive: + actions.append(agent.program(self.percept(agent))) + else: + actions.append("") + for (agent, action) in zip(self.agents, actions): + self.execute_action(agent, action) + self.exogenous_change() + + def run(self, steps=1000): + """Run the Environment for given number of time steps.""" + for step in range(steps): + if self.is_done(): + return + self.step() + + def list_things_at(self, location, tclass=Thing): + """Return all things exactly at a given location.""" + if isinstance(location, numbers.Number): + return [thing for thing in self.things + if thing.location == location and isinstance(thing, tclass)] + return [thing for thing in self.things + if all(x == y for x, y in zip(thing.location, location)) and isinstance(thing, tclass)] + + def some_things_at(self, location, tclass=Thing): + """Return true if at least one of the things at location + is an instance of class tclass (or a subclass).""" + return self.list_things_at(location, tclass) != [] + + def add_thing(self, thing, location=None): + """Add a thing to the environment, setting its location. For + convenience, if thing is an agent program we make a new agent + for it. (Shouldn't need to override this.)""" + if not isinstance(thing, Thing): + thing = Agent(thing) + if thing in self.things: + print("Can't add the same thing twice") + else: + thing.location = location if location is not None else self.default_location(thing) + self.things.append(thing) + if isinstance(thing, Agent): + thing.performance = 0 + self.agents.append(thing) + + def delete_thing(self, thing): + """Remove a thing from the environment.""" + try: + self.things.remove(thing) + except ValueError as e: + print(e) + 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])) + if thing in self.agents: + self.agents.remove(thing) + + +class Direction: + """A direction class for agents that want to move in a 2D plane + Usage: + d = Direction("down") + To change directions: + d = d + "right" or d = d + Direction.R #Both do the same thing + Note that the argument to __add__ must be a string and not a Direction object. + Also, it (the argument) can only be right or left.""" + + R = "right" + L = "left" + U = "up" + D = "down" + + 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), + self.L: Direction(self.U), + }.get(heading, None) + elif self.direction == self.L: + return { + self.R: Direction(self.U), + self.L: Direction(self.D), + }.get(heading, None) + elif self.direction == self.U: + return { + self.R: Direction(self.R), + self.L: Direction(self.L), + }.get(heading, None) + elif self.direction == self.D: + return { + self.R: Direction(self.L), + self.L: Direction(self.R), + }.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) + """ + # get the iterable class to return + iclass = from_location.__class__ + x, y = from_location + if self.direction == self.R: + return iclass((x + 1, y)) + elif self.direction == self.L: + return iclass((x - 1, y)) + elif self.direction == self.U: + return iclass((x, y - 1)) + elif self.direction == self.D: + return iclass((x, y + 1)) + + +class XYEnvironment(Environment): + """This class is for environments on a 2D plane, with locations + labelled by (x, y) points, either discrete or continuous. + + Agents perceive things within a radius. Each agent in the + environment has a .location slot which should be a location such + as (0, 1), and a .holding slot, which should be a list of things + that are held.""" + + def __init__(self, width=10, height=10): + super().__init__() + + self.width = width + self.height = height + self.observers = [] + # Sets iteration start and end (no walls). + self.x_start, self.y_start = (0, 0) + self.x_end, self.y_end = (self.width, self.height) + + perceptible_distance = 1 + + def things_near(self, location, radius=None): + """Return all things within radius of location.""" + if radius is None: + radius = self.perceptible_distance + radius2 = radius * radius + return [(thing, radius2 - distance_squared(location, thing.location)) + for thing in self.things if distance_squared( + location, thing.location) <= radius2] + + def percept(self, agent): + """By default, agent perceives things within a default radius.""" + return self.things_near(agent.location) + + def execute_action(self, agent, action): + agent.bump = False + if action == 'TurnRight': + agent.direction += Direction.R + elif action == 'TurnLeft': + agent.direction += Direction.L + elif action == 'Forward': + agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location)) + # elif action == 'Grab': + # things = [thing for thing in self.list_things_at(agent.location) + # if agent.can_grab(thing)] + # if things: + # agent.holding.append(things[0]) + elif action == 'Release': + if agent.holding: + agent.holding.pop() + + def default_location(self, thing): + location = self.random_location_inbounds() + while self.some_things_at(location, Obstacle): + # we will find a random location with no obstacles + location = self.random_location_inbounds() + return location + + def move_to(self, thing, destination): + """Move a thing to a new location. Returns True on success or False if there is an Obstacle. + If thing is holding anything, they move with him.""" + thing.bump = self.some_things_at(destination, Obstacle) + if not thing.bump: + thing.location = destination + for o in self.observers: + o.thing_moved(thing) + for t in thing.holding: + self.delete_thing(t) + self.add_thing(t, destination) + t.location = destination + return thing.bump + + def add_thing(self, thing, location=None, exclude_duplicate_class_items=False): + """Add things to the world. If (exclude_duplicate_class_items) then the item won't be + added if the location has at least one item of the same class.""" + if location is None: + super().add_thing(thing) + elif self.is_inbounds(location): + if (exclude_duplicate_class_items and + any(isinstance(t, thing.__class__) for t in self.list_things_at(location))): + return + super().add_thing(thing, location) + + def is_inbounds(self, location): + """Checks to make sure that the location is inbounds (within walls if we have walls)""" + x, y = location + return not (x < self.x_start or x > self.x_end or y < self.y_start or y > self.y_end) + + def random_location_inbounds(self, exclude=None): + """Returns a random location that is inbounds (within walls if we have walls)""" + location = (random.randint(self.x_start, self.x_end), + random.randint(self.y_start, self.y_end)) + if exclude is not None: + while location == exclude: + location = (random.randint(self.x_start, self.x_end), + random.randint(self.y_start, self.y_end)) + return location + + def delete_thing(self, thing): + """Deletes thing, and everything it is holding (if thing is an agent)""" + if isinstance(thing, Agent): + for obj in thing.holding: + super().delete_thing(obj) + for obs in self.observers: + obs.thing_deleted(obj) + + super().delete_thing(thing) + for obs in self.observers: + obs.thing_deleted(thing) + + def add_walls(self): + """Put walls around the entire perimeter of the grid.""" + for x in range(self.width): + self.add_thing(Wall(), (x, 0)) + self.add_thing(Wall(), (x, self.height - 1)) + for y in range(1, self.height - 1): + self.add_thing(Wall(), (0, y)) + self.add_thing(Wall(), (self.width - 1, y)) + + # Updates iteration start and end (with walls). + self.x_start, self.y_start = (1, 1) + self.x_end, self.y_end = (self.width - 1, self.height - 1) + + def add_observer(self, observer): + """Adds an observer to the list of observers. + An observer is typically an EnvGUI. + + Each observer is notified of changes in move_to and add_thing, + by calling the observer's methods thing_moved(thing) + and thing_added(thing, loc).""" + self.observers.append(observer) + + 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 + + +# ______________________________________________________________________________ + + +class GraphicEnvironment(XYEnvironment): + def __init__(self, width=10, height=10, boundary=True, color={}, display=False): + """Define all the usual XYEnvironment characteristics, + but initialise a BlockGrid for GUI too.""" + super().__init__(width, height) + self.grid = BlockGrid(width, height, fill=(200, 200, 200)) + if display: + self.grid.show() + self.visible = True + else: + self.visible = False + self.bounded = boundary + self.colors = color + + def get_world(self): + """Returns all the items in the world in a format + understandable by the ipythonblocks BlockGrid.""" + result = [] + x_start, y_start = (0, 0) + x_end, y_end = self.width, self.height + for x in range(x_start, x_end): + row = [] + for y in range(y_start, y_end): + row.append(self.list_things_at((x, y))) + result.append(row) + return result + + """ + def run(self, steps=1000, delay=1): + "" "Run the Environment for given number of time steps, + but update the GUI too." "" + for step in range(steps): + sleep(delay) + if self.visible: + self.reveal() + if self.is_done(): + if self.visible: + self.reveal() + return + self.step() + if self.visible: + self.reveal() + """ + + def run(self, steps=1000, delay=1): + """Run the Environment for given number of time steps, + but update the GUI too.""" + for step in range(steps): + self.update(delay) + if self.is_done(): + break + self.step() + self.update(delay) + + def update(self, delay=1): + sleep(delay) + self.reveal() + + def reveal(self): + """Display the BlockGrid for this world - the last thing to be added + at a location defines the location color.""" + self.draw_world() + # wait for the world to update and + # apply changes to the same grid instead + # of making a new one. + clear_output(1) + self.grid.show() + self.visible = True + + def draw_world(self): + self.grid[:] = (200, 200, 200) + world = self.get_world() + for x in range(0, len(world)): + for y in range(0, len(world[x])): + if len(world[x][y]): + self.grid[y, x] = self.colors[world[x][y][-1].__class__.__name__] + + def conceal(self): + """Hide the BlockGrid for this world""" + self.visible = False + display(HTML('')) + + +# ______________________________________________________________________________ +# Continuous environment + +class ContinuousWorld(Environment): + """Model for Continuous World""" + + def __init__(self, width=10, height=10): + super().__init__() + self.width = width + self.height = height + + def add_obstacle(self, coordinates): + self.things.append(PolygonObstacle(coordinates)) + + +class PolygonObstacle(Obstacle): + + def __init__(self, coordinates): + """Coordinates is a list of tuples.""" + super().__init__() + self.coordinates = coordinates + + +# ______________________________________________________________________________ +# 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 + each turn taken.""" + + def __init__(self, width=10, height=10): + super().__init__(width, height) + self.add_walls() + + def thing_classes(self): + return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, + TableDrivenVacuumAgent, ModelBasedVacuumAgent] + + 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') + bump = ('Bump' if agent.bump else 'None') + return status, bump + + def execute_action(self, agent, action): + agent.bump = False + if action == 'Suck': + dirt_list = self.list_things_at(agent.location, Dirt) + if dirt_list != []: + dirt = dirt_list[0] + agent.performance += 100 + self.delete_thing(dirt) + else: + super().execute_action(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 + Environment.""" + + def __init__(self): + super().__init__() + self.status = {loc_A: random.choice(['Clean', 'Dirty']), + loc_B: random.choice(['Clean', 'Dirty'])} + + def thing_classes(self): + return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, TableDrivenVacuumAgent, ModelBasedVacuumAgent] + + def percept(self, agent): + """Returns the agent's location, and the location status (Dirty/Clean).""" + return agent.location, self.status[agent.location] + + def execute_action(self, agent, action): + """Change agent's location and/or location's status; track performance. + Score 10 for each dirt cleaned; -1 for each move.""" + if action == 'Right': + agent.location = loc_B + agent.performance -= 1 + elif action == 'Left': + agent.location = loc_A + agent.performance -= 1 + elif action == 'Suck': + if self.status[agent.location] == 'Dirty': + agent.performance += 10 + self.status[agent.location] = 'Clean' + + def default_location(self, thing): + """Agents start in either location at random.""" + return random.choice([loc_A, loc_B]) + + +# ______________________________________________________________________________ +# The Wumpus World + + +class Gold(Thing): + + def __eq__(self, rhs): + """All Gold are equal""" + return rhs.__class__ == Gold + + pass + + +class Bump(Thing): + pass + + +class Glitter(Thing): + pass + + +class Pit(Thing): + pass + + +class Breeze(Thing): + pass + + +class Arrow(Thing): + pass + + +class Scream(Thing): + pass + + +class Wumpus(Agent): + screamed = False + pass + + +class Stench(Thing): + pass + + +class Explorer(Agent): + holding = [] + has_arrow = True + killed_by = "" + direction = Direction("right") + + def can_grab(self, thing): + """Explorer can only grab gold""" + return thing.__class__ == Gold + + +class WumpusEnvironment(XYEnvironment): + pit_probability = 0.2 # Probability to spawn a pit in a location. (From Chapter 7.2) + + # Room should be 4x4 grid of rooms. The extra 2 for walls + + def __init__(self, agent_program, width=6, height=6): + super().__init__(width, height) + self.init_world(agent_program) + + def init_world(self, program): + """Spawn items in the world based on probabilities from the book""" + + "WALLS" + self.add_walls() + + "PITS" + for x in range(self.x_start, self.x_end): + for y in range(self.y_start, self.y_end): + if random.random() < self.pit_probability: + self.add_thing(Pit(), (x, y), True) + self.add_thing(Breeze(), (x - 1, y), True) + self.add_thing(Breeze(), (x, y - 1), True) + self.add_thing(Breeze(), (x + 1, y), True) + self.add_thing(Breeze(), (x, y + 1), True) + + "WUMPUS" + w_x, w_y = self.random_location_inbounds(exclude=(1, 1)) + self.add_thing(Wumpus(lambda x: ""), (w_x, w_y), True) + self.add_thing(Stench(), (w_x - 1, w_y), True) + self.add_thing(Stench(), (w_x + 1, w_y), True) + self.add_thing(Stench(), (w_x, w_y - 1), True) + self.add_thing(Stench(), (w_x, w_y + 1), True) + + "GOLD" + self.add_thing(Gold(), self.random_location_inbounds(exclude=(1, 1)), True) + + "AGENT" + self.add_thing(Explorer(program), (1, 1), True) + + def get_world(self, show_walls=True): + """Return the items in the world""" + result = [] + x_start, y_start = (0, 0) if show_walls else (1, 1) + + if show_walls: + x_end, y_end = self.width, self.height + else: + x_end, y_end = self.width - 1, self.height - 1 + + for x in range(x_start, x_end): + row = [] + for y in range(y_start, y_end): + row.append(self.list_things_at((x, y))) + result.append(row) + return result + + def percepts_from(self, agent, location, tclass=Thing): + """Return percepts from a given location, + and replaces some items with percepts from chapter 7.""" + thing_percepts = { + Gold: Glitter(), + Wall: Bump(), + Wumpus: Stench(), + Pit: Breeze()} + + """Agents don't need to get their percepts""" + thing_percepts[agent.__class__] = None + + """Gold only glitters in its cell""" + if location != agent.location: + thing_percepts[Gold] = None + + result = [thing_percepts.get(thing.__class__, thing) for thing in self.things + if thing.location == location and isinstance(thing, tclass)] + return result if len(result) else [None] + + def percept(self, agent): + """Return things in adjacent (not diagonal) cells of the agent. + Result format: [Left, Right, Up, Down, Center / Current location]""" + x, y = agent.location + result = [] + result.append(self.percepts_from(agent, (x - 1, y))) + result.append(self.percepts_from(agent, (x + 1, y))) + result.append(self.percepts_from(agent, (x, y - 1))) + result.append(self.percepts_from(agent, (x, y + 1))) + result.append(self.percepts_from(agent, (x, y))) + + """The wumpus gives out a loud scream once it's killed.""" + wumpus = [thing for thing in self.things if isinstance(thing, Wumpus)] + if len(wumpus) and not wumpus[0].alive and not wumpus[0].screamed: + result[-1].append(Scream()) + wumpus[0].screamed = True + + return result + + def execute_action(self, agent, action): + """Modify the state of the environment based on the agent's actions. + Performance score taken directly out of the book.""" + + if isinstance(agent, Explorer) and self.in_danger(agent): + return + + agent.bump = False + if action == 'TurnRight': + agent.direction += Direction.R + agent.performance -= 1 + elif action == 'TurnLeft': + agent.direction += Direction.L + agent.performance -= 1 + elif action == 'Forward': + agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location)) + agent.performance -= 1 + elif action == 'Grab': + things = [thing for thing in self.list_things_at(agent.location) + if agent.can_grab(thing)] + if len(things): + print("Grabbing", things[0].__class__.__name__) + if len(things): + agent.holding.append(things[0]) + agent.performance -= 1 + elif action == 'Climb': + if agent.location == (1, 1): # Agent can only climb out of (1,1) + agent.performance += 1000 if Gold() in agent.holding else 0 + self.delete_thing(agent) + elif action == 'Shoot': + """The arrow travels straight down the path the agent is facing""" + if agent.has_arrow: + arrow_travel = agent.direction.move_forward(agent.location) + while self.is_inbounds(arrow_travel): + wumpus = [thing for thing in self.list_things_at(arrow_travel) + if isinstance(thing, Wumpus)] + if len(wumpus): + wumpus[0].alive = False + break + arrow_travel = agent.direction.move_forward(agent.location) + agent.has_arrow = False + + def in_danger(self, agent): + """Check if Explorer is in danger (Pit or Wumpus), if he is, kill him""" + for thing in self.list_things_at(agent.location): + if isinstance(thing, Pit) or (isinstance(thing, Wumpus) and thing.alive): + agent.alive = False + agent.performance -= 1000 + agent.killed_by = thing.__class__.__name__ + return True + return False + + def is_done(self): + """The game is over when the Explorer is killed + or if he climbs out of the cave only at (1,1).""" + explorer = [agent for agent in self.agents if isinstance(agent, Explorer)] + if len(explorer): + if explorer[0].alive: + return False + else: + print("Death by {} [-1000].".format(explorer[0].killed_by)) + else: + print("Explorer climbed out {}." + .format("with Gold [+1000]!" if Gold() not in self.things else "without Gold [+0]")) + return True + + # TODO: Arrow needs to be implemented + + +# ______________________________________________________________________________ + + +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. + >>> environment = TrivialVacuumEnvironment + >>> agents = [ModelBasedVacuumAgent, ReflexVacuumAgent] + >>> result = compare_agents(environment, agents) + >>> performance_ModelBasedVacuumAgent = result[0][1] + >>> performance_ReflexVacuumAgent = result[1][1] + >>> performance_ReflexVacuumAgent <= performance_ModelBasedVacuumAgent + 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 + >>> 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) + env.run(steps) + return agent.performance + + return mean(map(score, envs)) + + +# _________________________________________________________________________ + + +__doc__ += """ +>>> a = ReflexVacuumAgent() +>>> a.program((loc_A, 'Clean')) +'Right' +>>> a.program((loc_B, 'Clean')) +'Left' +>>> a.program((loc_A, 'Dirty')) +'Suck' +>>> a.program((loc_A, 'Dirty')) +'Suck' + +>>> e = TrivialVacuumEnvironment() +>>> e.add_thing(ModelBasedVacuumAgent()) +>>> e.run(5) + +""" diff --git a/aima-data b/aima-data index a21fc108f..f6cbea61a 160000 --- a/aima-data +++ b/aima-data @@ -1 +1 @@ -Subproject commit a21fc108f52ad551344e947b0eb97df82f8d2b2b +Subproject commit f6cbea61ad0c21c6b7be826d17af5a8d3a7c2c86 diff --git a/arc_consistency_heuristics.ipynb b/arc_consistency_heuristics.ipynb new file mode 100644 index 000000000..fb2241819 --- /dev/null +++ b/arc_consistency_heuristics.ipynb @@ -0,0 +1,1999 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "# Constraint Satisfaction Problems\n", + "---\n", + "# Heuristics for Arc-Consistency Algorithms\n", + "\n", + "## Introduction\n", + "A ***Constraint Satisfaction Problem*** is a triple $(X,D,C)$ where: \n", + "- $X$ is a set of variables $X_1, …, X_n$;\n", + "- $D$ is a set of domains $D_1, …, D_n$, one for each variable and each of which consists of a set of allowable values $v_1, ..., v_k$;\n", + "- $C$ is a set of constraints that specify allowable combinations of values.\n", + "\n", + "A CSP is called *arc-consistent* if every value in the domain of every variable is supported by all the neighbors of the variable while, is called *inconsistent*, if it has no solutions.
    \n", + "***Arc-consistency algorithms*** remove all unsupported values from the domains of variables making the CSP *arc-consistent* or decide that a CSP is *inconsistent* by finding that some variable has no supported values in its domain.
    \n", + "Heuristics significantly enhance the efficiency of the *arc-consistency algorithms* improving their average performance in terms of *consistency-checks* which can be considered a standard measure of goodness for such algorithms. *Arc-heuristic* operate at arc-level and selects the constraint that will be used for the next check, while *domain-heuristics* operate at domain-level and selects which values will be used for the next support-check." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from csp import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Domain-Heuristics for Arc-Consistency Algorithms\n", + "In [[1]](#cite-van2002domain) are investigated the effects of a *domain-heuristic* based on the notion of a *double-support check* by studying its average time-complexity.\n", + "\n", + "The objective of *arc-consistency algorithms* is to resolve some uncertainty; it has to be know, for each $v_i \\in D_i$ and for each $v_j \\in D_j$, whether it is supported.\n", + "\n", + "A *single-support check*, $(v_i, v_j) \\in C_{ij}$, is one in which, before the check is done, it is already known that either $v_i$ or $v_j$ are supported. \n", + "\n", + "A *double-support check* $(v_i, v_j) \\in C_{ij}$, is one in which there is still, before the check, uncertainty about the support-status of both $v_i$ and $v_j$. \n", + "\n", + "If a *double-support check* is successful, two uncertainties are resolved. If a *single-support check* is successful, only one uncertainty is resolved. A good *arc-consistency algorithm*, therefore, would always choose to do a *double-support check* in preference of a *single-support check*, because the cormer offers the potential higher payback.\n", + "\n", + "The improvement with *double-support check* is that, where possible, *consistency-checks* are used to find supports for two values, one value in the domain of each variable, which were previously known to be unsupported. It is motivated by the insight that *in order to minimize the number of consistency-checks it is necessary to maximize the number of uncertainties which are resolved per check*." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "### AC-3b: an improved version of AC-3 with Double-Support Checks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As shown in [[2]](#cite-van2000improving) the idea is to use *double-support checks* to improve the average performance of `AC3` which does not exploit the fact that relations are bidirectional and results in a new general purpose *arc-consistency algorithm* called `AC3b`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mAC3\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdom_j_up\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"[Figure 6.3]\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvariables\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msupport_pruning\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrevise\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is inconsistent\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is satisfiable\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource AC3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mrevise\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Return true if we remove a value.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# If Xi=x conflicts with Xj=y for every possible y, eliminate Xi=x\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if all(not csp.constraints(Xi, x, Xj, y) for y in csp.curr_domains[Xj]):\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0my\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprune\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource revise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At any stage in the process of making 2-variable CSP *arc-consistent* in `AC3b`:\n", + "- there is a set $S_i^+ \\subseteq D_i$ whose values are all known to be supported by $X_j$;\n", + "- there is a set $S_i^? = D_i \\setminus S_i^+$ whose values are unknown, as yet, to be supported by $X_j$.\n", + "\n", + "The same holds if the roles for $X_i$ and $X_j$ are exchanged.\n", + "\n", + "In order to establish support for a value $v_i^? \\in S_i^?$ it seems better to try to find a support among the values in $S_j^?$ first, because for each $v_j^? \\in S_j^?$ the check $(v_i^?,v_j^?) \\in C_{ij}$ is a *double-support check* and it is just as likely that any $v_j^? \\in S_j^?$ supports $v_i^?$ than it is that any $v_j^+ \\in S_j^+$ does. Only if no support can be found among the elements in $S_j^?$, should the elements $v_j^+$ in $S_j^+$ be used for *single-support checks* $(v_i^?,v_j^+) \\in C_{ij}$. After it has been decided for each value in $D_i$ whether it is supported or not, either $S_x^+ = \\emptyset$ and the 2-variable CSP is *inconsistent*, or $S_x^+ \\neq \\emptyset$ and the CSP is *satisfiable*. In the latter case, the elements from $D_i$ which are supported by $j$ are given by $S_x^+$. The elements in $D_j$ which are supported by $x$ are given by the union of $S_j^+$ with the set of those elements of $S_j^?$ which further processing will show to be supported by some $v_i^+ \\in S_x^+$." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mAC3b\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdom_j_up\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvariables\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msupport_pruning\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Si_p values are all known to be supported by Xj\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Sj_p values are all known to be supported by Xi\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Dj - Sj_p = Sj_u values are unknown, as yet, to be supported by Xi\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSj_u\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpartition\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is inconsistent\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprune\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# or queue -= {(Xj, Xi)} or queue.remove((Xj, Xi))\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdifference_update\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdifference_update\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# the elements in D_j which are supported by Xi are given by the union of Sj_p with the set of those\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# elements of Sj_u which further processing will show to be supported by some vi_p in Si_p\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvj_p\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mSj_u\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvi_p\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvj_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvi_p\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvj_p\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprune\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is satisfiable\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource AC3b" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mpartition\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSi_p\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSj_p\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSj_u\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvi_u\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# now, in order to establish support for a value vi_u in Di it seems better to try to find a support among\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# the values in Sj_u first, because for each vj_u in Sj_u the check (vi_u, vj_u) is a double-support check\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# and it is just as likely that any vj_u in Sj_u supports vi_u than it is that any vj_p in Sj_p does...\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvj_u\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mSj_u\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# double-support check\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvi_u\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvj_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvi_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvj_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# ... and only if no support can be found among the elements in Sj_u, should the elements vj_p in Sj_p be used\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# for single-support checks (vi_u, vj_p)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvj_p\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# single-support check\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvi_u\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvj_p\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvi_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mSi_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSj_u\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mSj_p\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource partition" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "`AC3b` is a refinement of the `AC3` algorithm which consists of the fact that if, when arc $(i,j)$ is being processed and the reverse arc $(j,i)$ is also in the queue, then consistency-checks can be saved because only support for the elements in $S_j^?$ has to be found (as opposed to support for all the elements in $D_j$ in the\n", + "`AC3` algorithm).
    \n", + "`AC3b` inherits all its properties like $\\mathcal{O}(ed^3)$ time-complexity and $\\mathcal{O}(e + nd)$ space-complexity fron `AC3` and where $n$ denotes the number of variables in the CSP, $e$ denotes the number of binary constraints and $d$ denotes the maximum domain-size of the variables." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "## Arc-Heuristics for Arc-Consistency Algorithms" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "Many *arc-heuristics* can be devised, based on three major features of CSPs:\n", + "- the number of acceptable pairs in each constraint (the *constraint size* or *satisfiability*);\n", + "- the *domain size*;\n", + "- the number of binary constraints that each variable participates in, equal to the *degree* of the node of that variable in the constraint graph. \n", + "\n", + "Simple examples of heuristics that might be expected to improve the efficiency of relaxation are:\n", + "- ordering the list of variable pairs by *increasing* relative *satisfiability*;\n", + "- ordering by *increasing size of the domain* of the variable $v_j$ relaxed against $v_i$;\n", + "- ordering by *descending degree* of node of the variable relaxed.\n", + "\n", + "In
    [[3]](#cite-wallace1992ordering) are investigated the effects of these *arc-heuristics* in an empirical way, experimenting the effects of them on random CSPs. Their results demonstrate that the first two, later called `sat up` and `dom j up` for n-ary and binary CSPs respectively, significantly reduce the number of *consistency-checks*." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mdom_j_up\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mSortedSet\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mneg\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mt\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource dom_j_up" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0msat_up\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_do\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mSortedSet\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_do\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mscope\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource sat_up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "## Experimental Results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "For the experiments below on binary CSPs, in addition to the two *arc-consistency algorithms* already cited above, `AC3` and `AC3b`, the `AC4` algorithm was used.
    \n", + "The `AC4` algorithm runs in $\\mathcal{O}(ed^2)$ worst-case time but can be slower than `AC3` on average cases." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mAC4\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdom_j_up\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvariables\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msupport_pruning\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msupport_counter\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvariable_value_pairs_supported\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdefaultdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munsupported_variable_value_pairs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# construction and initialization of support sets\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mqueue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0my\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msupport_counter\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvariable_value_pairs_supported\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msupport_counter\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprune\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munsupported_variable_value_pairs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is inconsistent\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# propagation of removed values\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0munsupported_variable_value_pairs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0munsupported_variable_value_pairs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mvariable_value_pairs_supported\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msupport_counter\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msupport_counter\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mXj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprune\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremovals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrevised\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munsupported_variable_value_pairs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrevised\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurr_domains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mXi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is inconsistent\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;31m# CSP is satisfiable\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource AC4" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sudoku" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "#### Easy Sudoku" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". . 3 | . 2 . | 6 . .\n", + "9 . . | 3 . 5 | . . 1\n", + ". . 1 | 8 . 6 | 4 . .\n", + "------+-------+------\n", + ". . 8 | 1 . 2 | 9 . .\n", + "7 . . | . . . | . . 8\n", + ". . 6 | 7 . 8 | 2 . .\n", + "------+-------+------\n", + ". . 2 | 6 . 9 | 5 . .\n", + "8 . . | 2 . 3 | . . 9\n", + ". . 5 | . 1 . | 3 . .\n" + ] + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "sudoku.display(sudoku.infer_assignment())" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 23.6 ms, sys: 0 ns, total: 23.6 ms\n", + "Wall time: 22.4 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 needs 11322 consistency-checks'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC3 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 7.43 ms, sys: 3.68 ms, total: 11.1 ms\n", + "Wall time: 10.7 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b needs 8345 consistency-checks'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "%time _, checks = AC3b(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC3b needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 56.3 ms, sys: 0 ns, total: 56.3 ms\n", + "Wall time: 55.4 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 needs 27718 consistency-checks'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "%time _, checks = AC4(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC4 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 17.2 ms, sys: 0 ns, total: 17.2 ms\n", + "Wall time: 16.9 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 with DOM J UP arc heuristic needs 6925 consistency-checks'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "%time _, checks = AC3(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 40.9 ms, sys: 2.47 ms, total: 43.4 ms\n", + "Wall time: 41.7 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP arc heuristic needs 6278 consistency-checks'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "%time _, checks = AC3b(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 38.9 ms, sys: 1.96 ms, total: 40.9 ms\n", + "Wall time: 40.7 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 with DOM J UP arc heuristic needs 9393 consistency-checks'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(easy1)\n", + "%time _, checks = AC4(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 8 3 | 9 2 1 | 6 5 7\n", + "9 6 7 | 3 4 5 | 8 2 1\n", + "2 5 1 | 8 7 6 | 4 9 3\n", + "------+-------+------\n", + "5 4 8 | 1 3 2 | 9 7 6\n", + "7 2 9 | 5 6 4 | 1 3 8\n", + "1 3 6 | 7 9 8 | 2 4 5\n", + "------+-------+------\n", + "3 7 2 | 6 8 9 | 5 1 4\n", + "8 1 4 | 2 5 3 | 7 6 9\n", + "6 9 5 | 4 1 7 | 3 8 2\n" + ] + } + ], + "source": [ + "backtracking_search(sudoku, select_unassigned_variable=mrv, inference=forward_checking)\n", + "sudoku.display(sudoku.infer_assignment())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "#### Harder Sudoku" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 1 7 | 3 6 9 | 8 . 5\n", + ". 3 . | . . . | . . .\n", + ". . . | 7 . . | . . .\n", + "------+-------+------\n", + ". 2 . | . . . | . 6 .\n", + ". . . | . 8 . | 4 . .\n", + ". . . | . 1 . | . . .\n", + "------+-------+------\n", + ". . . | 6 . 3 | . 7 .\n", + "5 . . | 2 . . | . . .\n", + "1 . 4 | . . . | . . .\n" + ] + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "sudoku.display(sudoku.infer_assignment())" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 17.7 ms, sys: 481 µs, total: 18.2 ms\n", + "Wall time: 17.2 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 needs 12837 consistency-checks'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC3 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 24.1 ms, sys: 2.6 ms, total: 26.7 ms\n", + "Wall time: 25.1 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b needs 8864 consistency-checks'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "%time _, checks = AC3b(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC3b needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 63.4 ms, sys: 3.48 ms, total: 66.9 ms\n", + "Wall time: 65.5 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 needs 44213 consistency-checks'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "%time _, checks = AC4(sudoku, arc_heuristic=no_arc_heuristic)\n", + "f'AC4 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 9.96 ms, sys: 570 µs, total: 10.5 ms\n", + "Wall time: 10.3 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 with DOM J UP arc heuristic needs 7045 consistency-checks'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "%time _, checks = AC3(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 36.1 ms, sys: 0 ns, total: 36.1 ms\n", + "Wall time: 35.5 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP arc heuristic needs 6994 consistency-checks'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "%time _, checks = AC3b(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 40.3 ms, sys: 0 ns, total: 40.3 ms\n", + "Wall time: 39.7 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 with DOM J UP arc heuristic needs 19210 consistency-checks'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sudoku = Sudoku(harder1)\n", + "%time _, checks = AC4(sudoku, arc_heuristic=dom_j_up)\n", + "f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 1 7 | 3 6 9 | 8 2 5\n", + "6 3 2 | 1 5 8 | 9 4 7\n", + "9 5 8 | 7 2 4 | 3 1 6\n", + "------+-------+------\n", + "8 2 5 | 4 3 7 | 1 6 9\n", + "7 9 1 | 5 8 6 | 4 3 2\n", + "3 4 6 | 9 1 2 | 7 5 8\n", + "------+-------+------\n", + "2 8 9 | 6 4 3 | 5 7 1\n", + "5 7 3 | 2 9 1 | 6 8 4\n", + "1 6 4 | 8 7 5 | 2 9 3\n" + ] + } + ], + "source": [ + "backtracking_search(sudoku, select_unassigned_variable=mrv, inference=forward_checking)\n", + "sudoku.display(sudoku.infer_assignment())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "### 8 Queens" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". - . - . - . - 0 0 0 0 0 0 0 0 \n", + "- . - . - . - . 0 0 0 0 0 0 0 0 \n", + ". - . - . - . - 0 0 0 0 0 0 0 0 \n", + "- . - . - . - . 0 0 0 0 0 0 0 0 \n", + ". - . - . - . - 0 0 0 0 0 0 0 0 \n", + "- . - . - . - . 0 0 0 0 0 0 0 0 \n", + ". - . - . - . - 0 0 0 0 0 0 0 0 \n", + "- . - . - . - . 0 0 0 0 0 0 0 0 \n" + ] + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "chess.display(chess.infer_assignment())" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 689 µs, sys: 193 µs, total: 882 µs\n", + "Wall time: 892 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 needs 666 consistency-checks'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3(chess, arc_heuristic=no_arc_heuristic)\n", + "f'AC3 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 451 µs, sys: 127 µs, total: 578 µs\n", + "Wall time: 584 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b needs 428 consistency-checks'" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "%time _, checks = AC3b(chess, arc_heuristic=no_arc_heuristic)\n", + "f'AC3b needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 8.53 ms, sys: 109 µs, total: 8.64 ms\n", + "Wall time: 8.48 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 needs 4096 consistency-checks'" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "%time _, checks = AC4(chess, arc_heuristic=no_arc_heuristic)\n", + "f'AC4 needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.88 ms, sys: 0 ns, total: 1.88 ms\n", + "Wall time: 1.88 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3 with DOM J UP arc heuristic needs 666 consistency-checks'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "%time _, checks = AC3(chess, arc_heuristic=dom_j_up)\n", + "f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.21 ms, sys: 326 µs, total: 1.53 ms\n", + "Wall time: 1.54 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP arc heuristic needs 792 consistency-checks'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "%time _, checks = AC3b(chess, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.71 ms, sys: 0 ns, total: 4.71 ms\n", + "Wall time: 4.65 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC4 with DOM J UP arc heuristic needs 4096 consistency-checks'" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chess = NQueensCSP(8)\n", + "%time _, checks = AC4(chess, arc_heuristic=dom_j_up)\n", + "f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". - . - Q - . - 2 2 3 3 0* 1 1 2 \n", + "- Q - . - . - . 1 0* 3 3 2 2 2 2 \n", + ". - . - . Q . - 3 2 3 2 2 0* 3 2 \n", + "Q . - . - . - . 0* 3 1 2 3 3 3 3 \n", + ". - . - . - Q - 2 2 2 2 3 3 0* 2 \n", + "- . - Q - . - . 2 1 3 0* 2 3 2 2 \n", + ". - . - . - . Q 1 3 2 3 3 1 2 0* \n", + "- . Q . - . - . 2 2 0* 2 2 2 2 2 \n" + ] + } + ], + "source": [ + "backtracking_search(chess, select_unassigned_variable=mrv, inference=forward_checking)\n", + "chess.display(chess.infer_assignment())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the experiments below on n-ary CSPs, due to the n-ary constraints, the `GAC` algorithm was used.
    \n", + "The `GAC` algorithm has $\\mathcal{O}(er^2d^t)$ time-complexity and $\\mathcal{O}(erd)$ space-complexity where $e$ denotes the number of n-ary constraints, $r$ denotes the constraint arity and $d$ denotes the maximum domain-size of the variables." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + " \u001b[0;32mdef\u001b[0m \u001b[0mGAC\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0morig_domains\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mto_do\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msat_up\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Makes this CSP arc-consistent using Generalized Arc Consistency\u001b[0m\n", + "\u001b[0;34m orig_domains is the original domains\u001b[0m\n", + "\u001b[0;34m to_do is a set of (variable,constraint) pairs\u001b[0m\n", + "\u001b[0;34m returns the reduced domains (an arc-consistent variable:domain dictionary)\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0morig_domains\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0morig_domains\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdomains\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mto_do\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mto_do\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mconst\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconstraints\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mscope\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mto_do\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mto_do\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomains\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0morig_domains\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mto_do\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_do\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0mto_do\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconst\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mto_do\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mother_vars\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mov\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mov\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mscope\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mov\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_domain\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother_vars\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mholds\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_domain\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# new_domain = {val for val in domains[var]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if const.holds({var: val})}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother_vars\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mother_vars\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mother_val\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mholds\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mother_val\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_domain\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# new_domain = {val for val in domains[var]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if any(const.holds({var: val, other: other_val})\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# for other_val in domains[other])}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# general case\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mholds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0many_holds\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdomains\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother_vars\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mchecks\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mholds\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_domain\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# new_domain = {val for val in domains[var]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if self.any_holds(domains, const, {var: val}, other_vars)}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew_domain\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_domain\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mnew_domain\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0madd_to_do\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnew_to_do\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconst\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdifference\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_do\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mto_do\u001b[0m \u001b[0;34m|=\u001b[0m \u001b[0madd_to_do\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchecks\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource ACSolver.GAC" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "### Crossword" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[_] [_] [_] [*] [*] \n", + "[_] [*] [_] [*] [*] \n", + "[_] [_] [_] [_] [*] \n", + "[_] [*] [_] [*] [*] \n", + "[*] [*] [_] [_] [_] \n", + "[*] [*] [_] [*] [*] \n" + ] + }, + { + "data": { + "text/plain": [ + "{'ant',\n", + " 'big',\n", + " 'book',\n", + " 'bus',\n", + " 'buys',\n", + " 'car',\n", + " 'ginger',\n", + " 'has',\n", + " 'hold',\n", + " 'lane',\n", + " 'search',\n", + " 'symbol',\n", + " 'syntax',\n", + " 'year'}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "crossword = Crossword(crossword1, words1)\n", + "crossword.display()\n", + "words1" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 20s, sys: 2.02 ms, total: 1min 20s\n", + "Wall time: 1min 20s\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC needs 64617645 consistency-checks'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(crossword).GAC(arc_heuristic=no_heuristic)\n", + "f'GAC needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.19 s, sys: 0 ns, total: 1.19 s\n", + "Wall time: 1.19 s\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC with SAT UP arc heuristic needs 908015 consistency-checks'" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "crossword = Crossword(crossword1, words1)\n", + "%time _, _, checks = ACSolver(crossword).GAC(arc_heuristic=sat_up)\n", + "f'GAC with SAT UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[B] [U] [S] [*] [*] \n", + "[U] [*] [E] [*] [*] \n", + "[Y] [E] [A] [R] [*] \n", + "[S] [*] [R] [*] [*] \n", + "[*] [*] [C] [A] [R] \n", + "[*] [*] [H] [*] [*] \n" + ] + } + ], + "source": [ + "crossword.display(ACSolver(crossword).domain_splitting())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "### Kakuro" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Easy Kakuro" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t10\\\t13\\\t[*]\t\n", + "\\3\t[_]\t[_]\t13\\\t\n", + "\\12\t[_]\t[_]\t[_]\t\n", + "\\21\t[_]\t[_]\t[_]\t\n" + ] + } + ], + "source": [ + "kakuro = Kakuro(kakuro2)\n", + "kakuro.display()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 17.8 ms, sys: 171 µs, total: 18 ms\n", + "Wall time: 16.4 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC needs 2752 consistency-checks'" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(kakuro).GAC(arc_heuristic=no_heuristic)\n", + "f'GAC needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 8.55 ms, sys: 0 ns, total: 8.55 ms\n", + "Wall time: 8.39 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC with SAT UP arc heuristic needs 1765 consistency-checks'" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kakuro = Kakuro(kakuro2)\n", + "%time _, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)\n", + "f'GAC with SAT UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t10\\\t13\\\t[*]\t\n", + "\\3\t[1]\t[2]\t13\\\t\n", + "\\12\t[5]\t[3]\t[4]\t\n", + "\\21\t[4]\t[8]\t[9]\t\n" + ] + } + ], + "source": [ + "kakuro.display(ACSolver(kakuro).domain_splitting())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "#### Medium Kakuro" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t17\\\t28\\\t[*]\t42\\\t22\\\t\n", + "\\9\t[_]\t[_]\t31\\14\t[_]\t[_]\t\n", + "\\20\t[_]\t[_]\t[_]\t[_]\t[_]\t\n", + "[*]\t\\30\t[_]\t[_]\t[_]\t[_]\t\n", + "[*]\t22\\24\t[_]\t[_]\t[_]\t[*]\t\n", + "\\25\t[_]\t[_]\t[_]\t[_]\t11\\\t\n", + "\\20\t[_]\t[_]\t[_]\t[_]\t[_]\t\n", + "\\14\t[_]\t[_]\t\\17\t[_]\t[_]\t\n" + ] + } + ], + "source": [ + "kakuro = Kakuro(kakuro3)\n", + "kakuro.display()" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.96 s, sys: 0 ns, total: 1.96 s\n", + "Wall time: 1.96 s\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC needs 1290179 consistency-checks'" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(kakuro).GAC(arc_heuristic=no_heuristic)\n", + "f'GAC needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 225 ms, sys: 0 ns, total: 225 ms\n", + "Wall time: 223 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC with SAT UP arc heuristic needs 148780 consistency-checks'" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kakuro = Kakuro(kakuro3)\n", + "%time _, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)\n", + "f'GAC with SAT UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t17\\\t28\\\t[*]\t42\\\t22\\\t\n", + "\\9\t[8]\t[1]\t31\\14\t[5]\t[9]\t\n", + "\\20\t[9]\t[2]\t[1]\t[3]\t[5]\t\n", + "[*]\t\\30\t[6]\t[9]\t[7]\t[8]\t\n", + "[*]\t22\\24\t[7]\t[8]\t[9]\t[*]\t\n", + "\\25\t[8]\t[4]\t[7]\t[6]\t11\\\t\n", + "\\20\t[5]\t[3]\t[6]\t[4]\t[2]\t\n", + "\\14\t[9]\t[5]\t\\17\t[8]\t[9]\t\n" + ] + } + ], + "source": [ + "kakuro.display(ACSolver(kakuro).domain_splitting())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "#### Harder Kakuro" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t[*]\t[*]\t[*]\t[*]\t4\\\t24\\\t11\\\t[*]\t[*]\t[*]\t11\\\t17\\\t[*]\t[*]\t\n", + "[*]\t[*]\t[*]\t17\\\t11\\12\t[_]\t[_]\t[_]\t[*]\t[*]\t24\\10\t[_]\t[_]\t11\\\t[*]\t\n", + "[*]\t4\\\t16\\26\t[_]\t[_]\t[_]\t[_]\t[_]\t[*]\t\\20\t[_]\t[_]\t[_]\t[_]\t16\\\t\n", + "\\20\t[_]\t[_]\t[_]\t[_]\t24\\13\t[_]\t[_]\t16\\\t\\12\t[_]\t[_]\t23\\10\t[_]\t[_]\t\n", + "\\10\t[_]\t[_]\t24\\12\t[_]\t[_]\t16\\5\t[_]\t[_]\t16\\30\t[_]\t[_]\t[_]\t[_]\t[_]\t\n", + "[*]\t[*]\t3\\26\t[_]\t[_]\t[_]\t[_]\t\\12\t[_]\t[_]\t4\\\t16\\14\t[_]\t[_]\t[*]\t\n", + "[*]\t\\8\t[_]\t[_]\t\\15\t[_]\t[_]\t34\\26\t[_]\t[_]\t[_]\t[_]\t[_]\t[*]\t[*]\t\n", + "[*]\t\\11\t[_]\t[_]\t3\\\t17\\\t\\14\t[_]\t[_]\t\\8\t[_]\t[_]\t7\\\t17\\\t[*]\t\n", + "[*]\t[*]\t[*]\t23\\10\t[_]\t[_]\t3\\9\t[_]\t[_]\t4\\\t23\\\t\\13\t[_]\t[_]\t[*]\t\n", + "[*]\t[*]\t10\\26\t[_]\t[_]\t[_]\t[_]\t[_]\t\\7\t[_]\t[_]\t30\\9\t[_]\t[_]\t[*]\t\n", + "[*]\t17\\11\t[_]\t[_]\t11\\\t24\\8\t[_]\t[_]\t11\\21\t[_]\t[_]\t[_]\t[_]\t16\\\t17\\\t\n", + "\\29\t[_]\t[_]\t[_]\t[_]\t[_]\t\\7\t[_]\t[_]\t23\\14\t[_]\t[_]\t3\\17\t[_]\t[_]\t\n", + "\\10\t[_]\t[_]\t3\\10\t[_]\t[_]\t[*]\t\\8\t[_]\t[_]\t4\\25\t[_]\t[_]\t[_]\t[_]\t\n", + "[*]\t\\16\t[_]\t[_]\t[_]\t[_]\t[*]\t\\23\t[_]\t[_]\t[_]\t[_]\t[_]\t[*]\t[*]\t\n", + "[*]\t[*]\t\\6\t[_]\t[_]\t[*]\t[*]\t\\15\t[_]\t[_]\t[_]\t[*]\t[*]\t[*]\t[*]\t\n" + ] + } + ], + "source": [ + "kakuro = Kakuro(kakuro4)\n", + "kakuro.display()" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 76.5 ms, sys: 847 µs, total: 77.4 ms\n", + "Wall time: 77 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC needs 46633 consistency-checks'" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(kakuro).GAC()\n", + "f'GAC needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 64.6 ms, sys: 0 ns, total: 64.6 ms\n", + "Wall time: 63.6 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC with SAT UP arc heuristic needs 36828 consistency-checks'" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kakuro = Kakuro(kakuro4)\n", + "%time _, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)\n", + "f'GAC with SAT UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[*]\t[*]\t[*]\t[*]\t[*]\t4\\\t24\\\t11\\\t[*]\t[*]\t[*]\t11\\\t17\\\t[*]\t[*]\t\n", + "[*]\t[*]\t[*]\t17\\\t11\\12\t[3]\t[7]\t[2]\t[*]\t[*]\t24\\10\t[2]\t[8]\t11\\\t[*]\t\n", + "[*]\t4\\\t16\\26\t[8]\t[5]\t[1]\t[9]\t[3]\t[*]\t\\20\t[8]\t[1]\t[9]\t[2]\t16\\\t\n", + "\\20\t[3]\t[7]\t[9]\t[1]\t24\\13\t[8]\t[5]\t16\\\t\\12\t[9]\t[3]\t23\\10\t[3]\t[7]\t\n", + "\\10\t[1]\t[9]\t24\\12\t[3]\t[9]\t16\\5\t[1]\t[4]\t16\\30\t[7]\t[5]\t[8]\t[1]\t[9]\t\n", + "[*]\t[*]\t3\\26\t[8]\t[2]\t[7]\t[9]\t\\12\t[3]\t[9]\t4\\\t16\\14\t[9]\t[5]\t[*]\t\n", + "[*]\t\\8\t[1]\t[7]\t\\15\t[8]\t[7]\t34\\26\t[1]\t[7]\t[3]\t[9]\t[6]\t[*]\t[*]\t\n", + "[*]\t\\11\t[2]\t[9]\t3\\\t17\\\t\\14\t[8]\t[6]\t\\8\t[1]\t[7]\t7\\\t17\\\t[*]\t\n", + "[*]\t[*]\t[*]\t23\\10\t[1]\t[9]\t3\\9\t[7]\t[2]\t4\\\t23\\\t\\13\t[4]\t[9]\t[*]\t\n", + "[*]\t[*]\t10\\26\t[6]\t[2]\t[8]\t[1]\t[9]\t\\7\t[1]\t[6]\t30\\9\t[1]\t[8]\t[*]\t\n", + "[*]\t17\\11\t[3]\t[8]\t11\\\t24\\8\t[2]\t[6]\t11\\21\t[3]\t[9]\t[7]\t[2]\t16\\\t17\\\t\n", + "\\29\t[8]\t[2]\t[9]\t[3]\t[7]\t\\7\t[4]\t[3]\t23\\14\t[8]\t[6]\t3\\17\t[9]\t[8]\t\n", + "\\10\t[9]\t[1]\t3\\10\t[2]\t[8]\t[*]\t\\8\t[2]\t[6]\t4\\25\t[8]\t[1]\t[7]\t[9]\t\n", + "[*]\t\\16\t[4]\t[2]\t[1]\t[9]\t[*]\t\\23\t[1]\t[8]\t[3]\t[9]\t[2]\t[*]\t[*]\t\n", + "[*]\t[*]\t\\6\t[1]\t[5]\t[*]\t[*]\t\\15\t[5]\t[9]\t[1]\t[*]\t[*]\t[*]\t[*]\t\n" + ] + } + ], + "source": [ + "kakuro.display(ACSolver(kakuro).domain_splitting())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "### Cryptarithmetic Puzzle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{array}{@{}r@{}}\n", + " S E N D \\\\\n", + "{} + M O R E \\\\\n", + " \\hline\n", + " M O N E Y\n", + "\\end{array}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "pycharm": {} + }, + "outputs": [], + "source": [ + "cryptarithmetic = NaryCSP(\n", + " {'S': set(range(1, 10)), 'M': set(range(1, 10)),\n", + " 'E': set(range(0, 10)), 'N': set(range(0, 10)), 'D': set(range(0, 10)),\n", + " 'O': set(range(0, 10)), 'R': set(range(0, 10)), 'Y': set(range(0, 10)),\n", + " 'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2)),\n", + " 'C4': set(range(0, 2))},\n", + " [Constraint(('S', 'E', 'N', 'D', 'M', 'O', 'R', 'Y'), all_diff),\n", + " Constraint(('D', 'E', 'Y', 'C1'), lambda d, e, y, c1: d + e == y + 10 * c1),\n", + " Constraint(('N', 'R', 'E', 'C1', 'C2'), lambda n, r, e, c1, c2: c1 + n + r == e + 10 * c2),\n", + " Constraint(('E', 'O', 'N', 'C2', 'C3'), lambda e, o, n, c2, c3: c2 + e + o == n + 10 * c3),\n", + " Constraint(('S', 'M', 'O', 'C3', 'C4'), lambda s, m, o, c3, c4: c3 + s + m == o + 10 * c4),\n", + " Constraint(('M', 'C4'), eq)])" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 21.7 s, sys: 0 ns, total: 21.7 s\n", + "Wall time: 21.7 s\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC needs 14080592 consistency-checks'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(cryptarithmetic).GAC(arc_heuristic=no_heuristic)\n", + "f'GAC needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 939 ms, sys: 0 ns, total: 939 ms\n", + "Wall time: 938 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'GAC with SAT UP arc heuristic needs 573120 consistency-checks'" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, _, checks = ACSolver(cryptarithmetic).GAC(arc_heuristic=sat_up)\n", + "f'GAC with SAT UP arc heuristic needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/latex": [ + "\\begin{array}{@{}r@{}} 9567 \\\\ + 1085 \\\\ \\hline 10652 \\end{array}" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "assignment = ACSolver(cryptarithmetic).domain_splitting()\n", + "\n", + "from IPython.display import Latex\n", + "display(Latex(r'\\begin{array}{@{}r@{}} ' + '{}{}{}{}'.format(assignment['S'], assignment['E'], assignment['N'], assignment['D']) + r' \\\\ + ' + \n", + " '{}{}{}{}'.format(assignment['M'], assignment['O'], assignment['R'], assignment['E']) + r' \\\\ \\hline ' + \n", + " '{}{}{}{}{}'.format(assignment['M'], assignment['O'], assignment['N'], assignment['E'], assignment['Y']) + ' \\end{array}'))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "## References\n", + "\n", + "
    [[1]](#ref-1) Van Dongen, Marc RC. 2002. _Domain-heuristics for arc-consistency algorithms_.\n", + "\n", + "[[2]](#ref-2) Van Dongen, MRC and Bowen, JA. 2000. _Improving arc-consistency algorithms with double-support checks_.\n", + "\n", + "[[3]](#ref-3) Wallace, Richard J and Freuder, Eugene Charles. 1992. _Ordering heuristics for arc consistency algorithms_." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5rc1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/canvas.py b/canvas.py deleted file mode 100644 index 4ad780380..000000000 --- a/canvas.py +++ /dev/null @@ -1,122 +0,0 @@ -from IPython.display import HTML, display, clear_output - -_canvas = """ - -
    - -
    - - -""" - -class Canvas: - """Inherit from this class to manage the HTML canvas element in jupyter notebooks. - To create an object of this class any_name_xyz = Canvas("any_name_xyz") - The first argument given must be the name of the object being create - IPython must be able to refernce the variable name that is being passed - """ - - def __init__(self, varname, id=None, width=800, height=600): - """""" - self.name = varname - self.id = id or varname - self.width = width - self.height = height - self.html = _canvas.format(self.id, self.width, self.height, self.name) - self.exec_list = [] - display(HTML(self.html)) - - def mouse_click(self, x, y): - "Override this method to handle mouse click at position (x, y)" - raise NotImplementedError - - def mouse_move(self, x, y): - raise NotImplementedError - - def execute(self, exec_str): - "Stores the command to be exectued to a list which is used later during update()" - if not isinstance(exec_str, str): - print("Invalid execution argument:", exec_str) - self.alert("Recieved invalid execution command format") - prefix = "{0}_canvas_object.".format(self.id) - self.exec_list.append(prefix + exec_str + ';') - - def fill(self, r, g, b): - "Changes the fill color to a color in rgb format" - self.execute("fill({0}, {1}, {2})".format(r, g, b)) - - def stroke(self, r, g, b): - "Changes the colors of line/strokes to rgb" - self.execute("stroke({0}, {1}, {2})".format(r, g, b)) - - def strokeWidth(self, w): - "Changes the width of lines/strokes to 'w' pixels" - self.execute("strokeWidth({0})".format(w)) - - def rect(self, x, y, w, h): - "Draw a rectangle with 'w' width, 'h' height and (x, y) as the top-left corner" - self.execute("rect({0}, {1}, {2}, {3})".format(x, y, w, h)) - - def rect_n(self, xn, yn, wn, hn): - "Similar to rect(), but the dimensions are normalized to fall between 0 and 1" - x = round(xn * self.width) - y = round(yn * self.height) - w = round(wn * self.width) - h = round(hn * self.height) - self.rect(x, y, w, h) - - def line(self, x1, y1, x2, y2): - "Draw a line from (x1, y1) to (x2, y2)" - self.execute("line({0}, {1}, {2}, {3})".format(x1, y1, x2, y2)) - - def line_n(self, x1n, y1n, x2n, y2n): - "Similar to line(), but the dimensions are normalized to fall between 0 and 1" - x1 = round(x1n * self.width) - y1 = round(y1n * self.height) - x2 = round(x2n * self.width) - y2 = round(y2n * self.height) - self.line(x1, y1, x2, y2) - - def arc(self, x, y, r, start, stop): - "Draw an arc with (x, y) as centre, 'r' as radius from angles 'start' to 'stop'" - self.execute("arc({0}, {1}, {2}, {3}, {4})".format(x, y, r, start, stop)) - - def arc_n(self, xn ,yn, rn, start, stop): - """Similar to arc(), but the dimensions are normalized to fall between 0 and 1 - The normalizing factor for radius is selected between width and height by seeing which is smaller - """ - x = round(xn * self.width) - y = round(yn * self.height) - r = round(rn * min(self.width, self.height)) - self.arc(x, y, r, start, stop) - - def clear(self): - "Clear the HTML canvas" - self.execute("clear()") - - def font(self, font): - "Changes the font of text" - self.execute('font("{0}")'.format(font)) - - def text(self, txt, x, y, fill=True): - "Display a text at (x, y)" - if fill: - self.execute('fill_text("{0}", {1}, {2})'.format(txt, x, y)) - else: - self.execute('stroke_text("{0}", {1}, {2})'.format(txt, x, y)) - - def text_n(self, txt, xn, yn, fill=True): - "Similar to text(), but with normalized coordinates" - x = round(xn * self.width) - y = round(yn * self.height) - self.text(txt, x, y, fill) - - def alert(self, message): - "Immediately display an alert" - display(HTML(''.format(message))) - - def update(self): - "Execute the JS code to execute the commands queued by execute()" - exec_code = "" - self.exec_list = [] - display(HTML(exec_code)) diff --git a/classical_planning_approaches.ipynb b/classical_planning_approaches.ipynb new file mode 100644 index 000000000..b3373b367 --- /dev/null +++ b/classical_planning_approaches.ipynb @@ -0,0 +1,2402 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classical Planning\n", + "---\n", + "# Classical Planning Approaches\n", + "\n", + "## Introduction \n", + "***Planning*** combines the two major areas of AI: *search* and *logic*. A planner can be seen either as a program that searches for a solution or as one that constructively proves the existence of a solution.\n", + "\n", + "Currently, the most popular and effective approaches to fully automated planning are:\n", + "- searching using a *planning graph*;\n", + "- *state-space search* with heuristics;\n", + "- translating to a *constraint satisfaction (CSP) problem*;\n", + "- translating to a *boolean satisfiability (SAT) problem*." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from planning import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Planning as Planning Graph Search\n", + "\n", + "A *planning graph* is a directed graph organized into levels each of which contains information about the current state of the knowledge base and the possible state-action links to and from that level. \n", + "\n", + "The first level contains the initial state with nodes representing each fluent that holds in that level. This level has state-action links linking each state to valid actions in that state. Each action is linked to all its preconditions and its effect states. Based on these effects, the next level is constructed and contains similarly structured information about the next state. In this way, the graph is expanded using state-action links till we reach a state where all the required goals hold true simultaneously.\n", + "\n", + "In every planning problem, we are allowed to carry out the *no-op* action, ie, we can choose no action for a particular state. These are called persistence actions and has effects same as its preconditions. This enables us to carry a state to the next level.\n", + "\n", + "Mutual exclusivity (*mutex*) between two actions means that these cannot be taken together and occurs in the following cases:\n", + "- *inconsistent effects*: one action negates the effect of the other;\n", + "- *interference*: one of the effects of an action is the negation of a precondition of the other;\n", + "- *competing needs*: one of the preconditions of one action is mutually exclusive with a precondition of the other.\n", + "\n", + "We can say that we have reached our goal if none of the goal states in the current level are mutually exclusive." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mGraph\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Contains levels of state and actions\u001b[0m\n", + "\u001b[0;34m Used in graph planning algorithm to extract a solution\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mFolKB\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mLevel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mobjects\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclauses\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_graph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mexpand_graph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Expands the graph by a level\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlast_level\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlast_level\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mobjects\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlast_level\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mperform_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnon_mutex_goals\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Checks whether the goals are mutually exclusive\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoal_perm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcombinations\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mgoal_perm\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mg\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mLevel\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Contains the state of the planning problem\u001b[0m\n", + "\u001b[0;34m and exhaustive list of actions which use the\u001b[0m\n", + "\u001b[0;34m states as pre-condition.\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Initializes variables to hold state and action details of a level\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mkb\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# current state\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mkb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# current action to state link\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# current state to action link\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# current action to next state link\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# next state to current action link\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# mutually exclusive actions\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobjects\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbuild\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobjects\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfind_mutex\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mseparate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Separates an iterable of elements into positive and negative parts\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpositive\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnegative\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'Not'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnegative\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpositive\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpositive\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnegative\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mfind_mutex\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Finds mutually exclusive actions\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Inconsistent effects\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpos_nsl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mneg_nsl\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mseparate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnegeff\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mneg_nsl\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_negeff\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnegeff\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mnegeff\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mposeff\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpos_nsl\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew_negeff\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mposeff\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ma\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mposeff\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mb\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnegeff\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m}\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Interference will be calculated with the last step\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpos_csl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mneg_csl\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mseparate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Competing needs\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mpos_precond\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpos_csl\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mneg_precond\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mneg_csl\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_neg_precond\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mneg_precond\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mneg_precond\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew_neg_precond\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mpos_precond\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ma\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mpos_precond\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mb\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mneg_precond\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m}\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Inconsistent support\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate_mutex\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mpair\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnext_state_0\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpair\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpair\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnext_state_1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpair\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnext_state_1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpair\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnext_state_0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnext_state_1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate_mutex\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0mnext_state_0\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnext_state_1\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstate_mutex\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mbuild\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobjects\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Populates the lists and dictionaries containing the state action dependencies\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mp_expr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'P'\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mp_expr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mp_expr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mp_expr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mp_expr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ma\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnum_args\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpossible_args\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtuple\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpermutations\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobjects\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnum_args\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0marg\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpossible_args\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnum\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mislower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0marg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnum\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0marg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtuple\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_action\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msubstitute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprecond\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_clause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msubstitute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew_clause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_clause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msubstitute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew_clause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnew_clause\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mnew_action\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mperform_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Performs the necessary actions and returns a new Level\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_kb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mFolKB\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mLevel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_kb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource Level" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A *planning graph* can be used to give better heuristic estimates which can be applied to any of the search techniques. Alternatively, we can search for a solution over the space formed by the planning graph, using an algorithm called `GraphPlan`.\n", + "\n", + "The `GraphPlan` algorithm repeatedly adds a level to a planning graph. Once all the goals show up as non-mutex in the graph, the algorithm runs backward from the last level to the first searching for a plan that solves the problem. If that fails, it records the (level , goals) pair as a *no-good* (as in constraint learning for CSPs), expands another level and tries again, terminating with failure when there is no reason to go on. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mGraphPlan\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Class for formulation GraphPlan algorithm\u001b[0m\n", + "\u001b[0;34m Constructs a graph of state and action space\u001b[0m\n", + "\u001b[0;34m Returns solution for the planning problem\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mGraph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mno_goods\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msolution\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mcheck_leveloff\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Checks if the graph has levelled off\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcheck\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_state\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcheck\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mextract_solution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Extracts the solution\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlevel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnon_mutex_goals\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mno_goods\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlevel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlevel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mindex\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Create all combinations of actions that satisfy the goal\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mgoal\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlevel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_state_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mall_actions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mproduct\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Filter out non-mutex actions\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnon_mutex_actions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction_tuple\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mall_actions\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0maction_pairs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcombinations\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction_tuple\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnon_mutex_actions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction_tuple\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mpair\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction_pairs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpair\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mlevel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmutex\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnon_mutex_actions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Recursion\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction_list\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mnon_mutex_actions\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0maction_list\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maction_list\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_goals\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mact\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mact\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mlevel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnew_goals\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_goals\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mlevel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_action_links\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mact\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mlevel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_goals\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mno_goods\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mextract_solution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_goals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Level-Order multiple solutions\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mitem\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mitem\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnum\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mitem\u001b[0m \u001b[0;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mitem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreverse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnum\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitem\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mgoal_test\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mask\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mq\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mFalse\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mq\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Executes the GraphPlan algorithm for the given problem\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_graph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoal_test\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkb\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnon_mutex_goals\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msolution\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mextract_solution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgraph\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0;36m2\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcheck_leveloff\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource GraphPlan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Planning as State-Space Search" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The description of a planning problem defines a search problem: we can search from the initial state through the space of states, looking for a goal. One of the nice advantages of the declarative representation of action schemas is that we can also search backward from the goal, looking for the initial state. \n", + "\n", + "However, neither forward nor backward search is efficient without a good heuristic function because the real-world planning problems often have large state spaces. A heuristic function $h(s)$ estimates the distance from a state $s$ to the goal and, if it is admissible, ie if does not overestimate, then we can use $A^∗$ search to find optimal solutions.\n", + "\n", + "Planning uses a factored representation for states and action schemas which makes it possible to define good domain-independent heuristics to prune the search space.\n", + "\n", + "An admissible heuristic can be derived by defining a relaxed problem that is easier to solve. The length of the solution of this easier problem then becomes the heuristic for the original problem. Assume that all goals and preconditions contain only positive literals, ie that the problem is defined according to the *Stanford Research Institute Problem Solver* (STRIPS) notation: we want to create a relaxed version of the original problem that will be easier to solve by ignoring delete lists from all actions, ie removing all negative literals from effects. As shown in
    [[1]](#cite-hoffmann2001ff) the planning graph of a relaxed problem does not contain any mutex relations at all (which is the crucial thing when building a planning graph) and for this reason GraphPlan will never backtrack looking for a solution: for this reason the **ignore delete lists** heuristic makes it possible to find the optimal solution for relaxed problem in polynomial time through `GraphPlan` algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from search import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward State-Space Search" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Forward search through the space of states, starting in the initial state and using the problem’s actions to search forward for a member of the set of goal states." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mForwardPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msearch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mProblem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Section 10.2.1]\u001b[0m\n", + "\u001b[0;34m Forward state-space search\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpanded_actions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpanded_actions\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpre\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mpre\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprecond\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mgoal_test\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mgoal\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mh\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Computes ignore delete lists heuristic by creating a relaxed version of the original problem (we can do that\u001b[0m\n", + "\u001b[0;34m by removing the delete lists from all actions, i.e. removing all negative literals from effects) that will be\u001b[0m\n", + "\u001b[0;34m easier to solve through GraphPlan and where the length of the solution will serve as a good heuristic.\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrelaxed_planning_problem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrelaxed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlinearize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mGraphPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrelaxed_planning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mexcept\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'inf'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource ForwardPlan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Backward Relevant-States Search" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Backward search through sets of relevant states, starting at the set of states representing the goal and using the inverse of the actions to search backward for the initial state." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mBackwardPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msearch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mProblem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Section 10.2.2]\u001b[0m\n", + "\u001b[0;34m Backward relevant-states search\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpanded_actions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Returns True if the action is relevant to the subgoal, i.e.:\u001b[0m\n", + "\u001b[0;34m - the action achieves an element of the effects\u001b[0m\n", + "\u001b[0;34m - the action doesn't delete something that needs to be achieved\u001b[0m\n", + "\u001b[0;34m - the preconditions are consistent with other subgoals that need to be achieved\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnegate_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msubgoal\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpanded_actions\u001b[0m \u001b[0;32mif\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0many\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mprop\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0many\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnegate_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msubgoal\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mprop\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0many\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnegate_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msubgoal\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mnegate_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mprop\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprecond\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msubgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# g' = (g - effects(a)) + preconds(a)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdifference\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munion\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprecond\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mgoal_test\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mgoal\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mh\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msubgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Computes ignore delete lists heuristic by creating a relaxed version of the original problem (we can do that\u001b[0m\n", + "\u001b[0;34m by removing the delete lists from all actions, i.e. removing all negative literals from effects) that will be\u001b[0m\n", + "\u001b[0;34m easier to solve through GraphPlan and where the length of the solution will serve as a good heuristic.\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrelaxed_planning_problem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msubgoal\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrelaxed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mactions\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlinearize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mGraphPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrelaxed_planning_problem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mexcept\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'inf'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource BackwardPlan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Planning as Constraint Satisfaction Problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In forward planning, the search is constrained by the initial state and only uses the goal as a stopping criterion and as a source for heuristics. In regression planning, the search is constrained by the goal and only uses the start state as a stopping criterion and as a source for heuristics. By converting the problem to a constraint satisfaction problem (CSP), the initial state can be used to prune what is not reachable and the goal to prune what is not useful. The CSP will be defined for a finite number of steps; the number of steps can be adjusted to find the shortest plan. One of the CSP methods can then be used to solve the CSP and thus find a plan.\n", + "\n", + "To construct a CSP from a planning problem, first choose a fixed planning *horizon*, which is the number of time steps over which to plan. Suppose the horizon is \n", + "$k$. The CSP has the following variables:\n", + "\n", + "- a *state variable* for each feature and each time from 0 to $k$. If there are $n$ features for a horizon of $k$, there are $n \\cdot (k+1)$ state variables. The domain of the state variable is the domain of the corresponding feature;\n", + "- an *action variable*, $Action_t$, for each $t$ in the range 0 to $k-1$. The domain of $Action_t$, represents the action that takes the agent from the state at time $t$ to the state at time $t+1$.\n", + "\n", + "There are several types of constraints:\n", + "\n", + "- a *precondition constraint* between a state variable at time $t$ and the variable $Actiont_t$ constrains what actions are legal at time $t$;\n", + "- an *effect constraint* between $Action_t$ and a state variable at time $t+1$ constrains the values of a state variable that is a direct effect of the action;\n", + "- a *frame constraint* among a state variable at time $t$, the variable $Action_t$, and the corresponding state variable at time $t+1$ specifies when the variable that does not change as a result of an action has the same value before and after the action;\n", + "- an *initial-state constraint* constrains a variable on the initial state (at time 0). The initial state is represented as a set of domain constraints on the state variables at time 0;\n", + "- a *goal constraint* constrains the final state to be a state that satisfies the achievement goal. These are domain constraints on the variables that appear in the goal;\n", + "- a *state constraint* is a constraint among variables at the same time step. These can include physical constraints on the state or can ensure that states that violate maintenance goals are forbidden. This is extra knowledge beyond the power of the feature-based or PDDL representations of the action.\n", + "\n", + "The PDDL representation gives precondition, effect and frame constraints for each time \n", + "$t$ as follows:\n", + "\n", + "- for each $Var = v$ in the precondition of action $A$, there is a precondition constraint:\n", + "$$ Var_t = v \\leftarrow Action_t = A $$\n", + "that specifies that if the action is to be $A$, $Var_t$ must have value $v$ immediately before. This constraint is violated when $Action_t = A$ and $Var_t \\neq v$, and thus is equivalent to $\\lnot{(Var_t \\neq v \\land Action_t = A)}$;\n", + "- or each $Var = v$ in the effect of action $A$, there is a effect constraint:\n", + "$$ Var_{t+1} = v \\leftarrow Action_t = A $$\n", + "which is violated when $Action_t = A$ and $Var_{t+1} \\neq v$, and thus is equivalent to $\\lnot{(Var_{t+1} \\neq v \\land Action_t = A)}$;\n", + "- for each $Var$, there is a frame constraint, where $As$ is the set of actions that include $Var$ in the effect of the action:\n", + "$$ Var_{t+1} = Var_t \\leftarrow Action_t \\notin As $$\n", + "which specifies that the feature $Var$ has the same value before and after any action that does not affect $Var$.\n", + "\n", + "The CSP representation assumes a fixed planning horizon (ie a fixed number of steps). To find a plan over any number of steps, the algorithm can be run for a horizon of $k = 0, 1, 2, \\dots$ until a solution is found." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from csp import *" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mCSPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msolution_length\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mCSP_solver\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mac_search_solver\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msat_up\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Section 10.4.3]\u001b[0m\n", + "\u001b[0;34m Planning as Constraint Satisfaction Problem\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Returns a string for the var-stage pair that can be used as a variable\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\"_\"\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mif_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mv1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mv2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"If the second argument is v2, the first argument must be v1\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mif_fun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mx1\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mv1\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mx2\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mv2\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mif_fun\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__name__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"if the second argument is \"\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mv2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\" then the first argument is \"\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mv1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\" \"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mif_fun\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0meq_if_not_in_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"First and third arguments are equal if action is not in actset\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0meq_if_not_in\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mx1\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mx2\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ma\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mactset\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meq_if_not_in\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__name__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"first and third arguments are equal if action is not in \"\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactset\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\" \"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0meq_if_not_in\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpanded_actions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mfluent_values\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_fluents\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhorizon\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msolution_length\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mact_vars\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'action'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomains\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mav\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexpanded_actions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mav\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mact_vars\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomains\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m}\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfluent_values\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# initial state constraints\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfluent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfluent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfluent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfluent_values\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# goal state constraints\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfluent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfluent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# precondition constraints\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'action'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mif_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# st(var, stage) == val if st('action', stage) == act\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstrps\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mexpanded_actions\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfluent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfluent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstrps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprecond\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# effect constraints\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'action'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mif_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mval\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# st(var, stage + 1) == val if st('action', stage) == act\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstrps\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mexpanded_actions\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfluent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfluent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'Not'\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mfluent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstrps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# frame constraints\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconstraints\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mConstraint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'action'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meq_if_not_in_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mact\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mact\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mexpanded_actions\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Not'\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mvar\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvar\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfluent_values\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstage\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhorizon\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcsp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mNaryCSP\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdomains\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconstraints\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msol\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCSP_solver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcsp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marc_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0marc_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msol\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0msol\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ma\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mact_vars\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource CSPlan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Planning as Boolean Satisfiability Problem\n", + "\n", + "As shown in [[2]](cite-kautz1992planning) the translation of a *Planning Domain Definition Language* (PDDL) description into a *Conjunctive Normal Form* (CNF) formula is a series of straightforward steps:\n", + "- *propositionalize the actions*: replace each action schema with a set of ground actions formed by substituting constants for each of the variables. These ground actions are not part of the translation, but will be used in subsequent steps;\n", + "- *define the initial state*: assert $F^0$ for every fluent $F$ in the problem’s initial state, and $\\lnot{F}$ for every fluent not mentioned in the initial state;\n", + "- *propositionalize the goal*: for every variable in the goal, replace the literals that contain the variable with a disjunction over constants;\n", + "- *add successor-state axioms*: for each fluent $F$, add an axiom of the form\n", + "\n", + "$$ F^{t+1} \\iff ActionCausesF^t \\lor (F^t \\land \\lnot{ActionCausesNotF^t}) $$\n", + "\n", + "where $ActionCausesF$ is a disjunction of all the ground actions that have $F$ in their add list, and $ActionCausesNotF$ is a disjunction of all the ground actions that have $F$ in their delete list;\n", + "- *add precondition axioms*: for each ground action $A$, add the axiom $A^t \\implies PRE(A)^t$, that is, if an action is taken at time $t$, then the preconditions must have been true;\n", + "- *add action exclusion axioms*: say that every action is distinct from every other action.\n", + "\n", + "A propositional planning procedure implements the basic idea just given but, because the agent does not know how many steps it will take to reach the goal, the algorithm tries each possible number of steps $t$, up to some maximum conceivable plan length $T_{max}$ . In this way, it is guaranteed to find the shortest plan if one exists. Because of the way the propositional planning procedure searches for a solution, this approach cannot be used in a partially observable environment, ie WalkSAT, but would just set the unobservable variables to the values it needs to create a solution." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from logic import *" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mSATPlan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msolution_length\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSAT_solver\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcdcl_satisfiable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Section 10.4.1]\u001b[0m\n", + "\u001b[0;34m Planning as Boolean satisfiability\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mexpand_transitions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfilter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mact\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'Not'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_strips\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstate\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mstate\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpand_transitions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtransition\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdefaultdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpand_transitions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpand_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mSAT_plan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mplanning_problem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgoals\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msolution_length\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSAT_solver\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mSAT_solver\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource SATPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mSAT_plan\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt_max\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSAT_solver\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcdcl_satisfiable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Converts a planning problem to Satisfaction problem by translating it to a cnf sentence.\u001b[0m\n", + "\u001b[0;34m [Figure 7.22]\u001b[0m\n", + "\u001b[0;34m >>> transition = {'A': {'Left': 'A', 'Right': 'B'}, 'B': {'Left': 'A', 'Right': 'C'}, 'C': {'Left': 'B', 'Right': 'C'}}\u001b[0m\n", + "\u001b[0;34m >>> SAT_plan('A', transition, 'C', 1) is None\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Functions used by SAT_plan\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mtranslate_to_SAT\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstates\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mstate\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mstate\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Symbol claiming state s at time t\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate_counter\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstates\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtime\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"S{}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate_counter\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Add initial state axiom\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0minit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Add goal state axiom\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfirst\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclause\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstate_sym\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0missuperset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \\\n", + " \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# All possible transitions\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtransition_counter\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstates\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0ms_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Action 'action' taken from state 's' at time 't' to reach 's_'\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExpr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"T{}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtransition_counter\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Change the state from s to s_\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0;34m'==>'\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0;34m'==>'\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Allow only one state at any time\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtime\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# must be a state at any time\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstates\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstates\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms_\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstates\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstates\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# for each pair of states s, s_ only one is possible at time t\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mstate_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Restrict to one transition per timestep\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# list of possible transitions at time t\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtransitions_t\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mtr\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mtr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction_sym\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mtr\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# make sure at least one of the transitions happens\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mtr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransitions_t\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mtr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransitions_t\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mtr_\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtransitions_t\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtransitions_t\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtr\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# there cannot be two transitions tr and tr_ at time t\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m|\u001b[0m \u001b[0;34m~\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtr_\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Combine the clauses to form the cnf\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'&'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mextract_solution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtrue_transitions\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mt\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0maction_sym\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maction_sym\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mt\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Sort transitions based on time, which is the 3rd element of the tuple\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtrue_transitions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msort\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0maction\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maction\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtime\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtrue_transitions\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# Body of SAT_plan algorithm\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mt_max\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# dictionaries to help extract the solution from model\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstate_sym\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0maction_sym\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mcnf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtranslate_to_SAT\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtransition\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgoal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mSAT_solver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcnf\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mextract_solution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource SAT_plan" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "## Experimental Results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Blocks World" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mthree_block_tower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Figure 10.3] THREE-BLOCK-TOWER\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m A blocks-world problem of stacking three blocks in a certain configuration,\u001b[0m\n", + "\u001b[0;34m also known as the Sussman Anomaly.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example:\u001b[0m\n", + "\u001b[0;34m >>> from planning import *\u001b[0m\n", + "\u001b[0;34m >>> tbt = three_block_tower()\u001b[0m\n", + "\u001b[0;34m >>> tbt.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> tbt.act(expr('MoveToTable(C, A)'))\u001b[0m\n", + "\u001b[0;34m >>> tbt.act(expr('Move(B, Table, C)'))\u001b[0m\n", + "\u001b[0;34m >>> tbt.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> tbt.act(expr('Move(A, Table, B)'))\u001b[0m\n", + "\u001b[0;34m >>> tbt.goal_test()\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m >>>\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(A, Table) & On(B, Table) & On(C, A) & Clear(B) & Clear(C)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(A, B) & On(B, C)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Move(b, x, y)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(b, x) & Clear(b) & Clear(y)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(b, y) & Clear(x) & ~On(b, x) & ~Clear(y)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Block(b) & Block(y)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'MoveToTable(b, x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(b, x) & Clear(b)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'On(b, Table) & Clear(x) & ~On(b, x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Block(b) & Block(x)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Block(A) & Block(B) & Block(C)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource three_block_tower" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GraphPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.46 ms, sys: 124 µs, total: 4.59 ms\n", + "Wall time: 4.48 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = GraphPlan(three_block_tower()).execute()\n", + "linearize(blocks_world_solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14 paths have been expanded and 28 paths remain in the frontier\n", + "CPU times: user 91 ms, sys: 0 ns, total: 91 ms\n", + "Wall time: 89.8 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = uniform_cost_search(ForwardPlan(three_block_tower()), display=True).solution()\n", + "blocks_world_solution = list(map(lambda action: Expr(action.name, *action.args), blocks_world_solution))\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3 paths have been expanded and 9 paths remain in the frontier\n", + "CPU times: user 81.3 ms, sys: 3.11 ms, total: 84.5 ms\n", + "Wall time: 83 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = astar_search(ForwardPlan(three_block_tower()), display=True).solution()\n", + "blocks_world_solution = list(map(lambda action: Expr(action.name, *action.args), blocks_world_solution))\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "116 paths have been expanded and 289 paths remain in the frontier\n", + "CPU times: user 266 ms, sys: 718 µs, total: 267 ms\n", + "Wall time: 265 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = uniform_cost_search(BackwardPlan(three_block_tower()), display=True).solution()\n", + "blocks_world_solution = list(map(lambda action: Expr(action.name, *action.args), blocks_world_solution))\n", + "blocks_world_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 paths have been expanded and 20 paths remain in the frontier\n", + "CPU times: user 477 ms, sys: 450 µs, total: 477 ms\n", + "Wall time: 476 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = astar_search(BackwardPlan(three_block_tower()), display=True).solution()\n", + "blocks_world_solution = list(map(lambda action: Expr(action.name, *action.args), blocks_world_solution))\n", + "blocks_world_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 172 ms, sys: 4.52 ms, total: 176 ms\n", + "Wall time: 175 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = CSPlan(three_block_tower(), 3, arc_heuristic=no_heuristic)\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan with SAT UP Arc Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 267 ms, sys: 0 ns, total: 267 ms\n", + "Wall time: 266 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = CSPlan(three_block_tower(), 3, arc_heuristic=sat_up)\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SATPlan with DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 34.9 s, sys: 15.9 ms, total: 34.9 s\n", + "Wall time: 34.9 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = SATPlan(three_block_tower(), 4, SAT_solver=dpll_satisfiable)\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SATPlan with CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.15 s, sys: 4.01 ms, total: 1.15 s\n", + "Wall time: 1.15 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time blocks_world_solution = SATPlan(three_block_tower(), 4, SAT_solver=cdcl_satisfiable)\n", + "blocks_world_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Spare Tire" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mspare_tire\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Figure 10.2] SPARE-TIRE-PROBLEM\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m A problem involving changing the flat tire of a car\u001b[0m\n", + "\u001b[0;34m with a spare tire from the trunk.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example:\u001b[0m\n", + "\u001b[0;34m >>> from planning import *\u001b[0m\n", + "\u001b[0;34m >>> st = spare_tire()\u001b[0m\n", + "\u001b[0;34m >>> st.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> st.act(expr('Remove(Spare, Trunk)'))\u001b[0m\n", + "\u001b[0;34m >>> st.act(expr('Remove(Flat, Axle)'))\u001b[0m\n", + "\u001b[0;34m >>> st.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> st.act(expr('PutOn(Spare, Axle)'))\u001b[0m\n", + "\u001b[0;34m >>> st.goal_test()\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m >>>\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(Flat, Axle) & At(Spare, Trunk)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(Spare, Axle) & At(Flat, Ground)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Remove(obj, loc)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(obj, loc)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(obj, Ground) & ~At(obj, loc)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Tire(obj)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'PutOn(t, Axle)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(t, Ground) & ~At(Flat, Axle)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(t, Axle) & ~At(t, Ground)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Tire(t)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'LeaveOvernight'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m''\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'~At(Spare, Ground) & ~At(Spare, Axle) & ~At(Spare, Trunk) & \\\u001b[0m\n", + "\u001b[0;34m ~At(Flat, Ground) & ~At(Flat, Axle) & ~At(Flat, Trunk)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Tire(Flat) & Tire(Spare)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource spare_tire" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GraphPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.24 ms, sys: 1 µs, total: 4.24 ms\n", + "Wall time: 4.16 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = GraphPlan(spare_tire()).execute()\n", + "linearize(spare_tire_solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11 paths have been expanded and 9 paths remain in the frontier\n", + "CPU times: user 10.3 ms, sys: 0 ns, total: 10.3 ms\n", + "Wall time: 9.89 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = uniform_cost_search(ForwardPlan(spare_tire()), display=True).solution()\n", + "spare_tire_solution = list(map(lambda action: Expr(action.name, *action.args), spare_tire_solution))\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5 paths have been expanded and 8 paths remain in the frontier\n", + "CPU times: user 20.4 ms, sys: 1 µs, total: 20.4 ms\n", + "Wall time: 19.4 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = astar_search(ForwardPlan(spare_tire()), display=True).solution()\n", + "spare_tire_solution = list(map(lambda action: Expr(action.name, *action.args), spare_tire_solution))\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "29 paths have been expanded and 22 paths remain in the frontier\n", + "CPU times: user 22.2 ms, sys: 7 µs, total: 22.2 ms\n", + "Wall time: 21.3 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = uniform_cost_search(BackwardPlan(spare_tire()), display=True).solution()\n", + "spare_tire_solution = list(map(lambda action: Expr(action.name, *action.args), spare_tire_solution))\n", + "spare_tire_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3 paths have been expanded and 11 paths remain in the frontier\n", + "CPU times: user 13 ms, sys: 0 ns, total: 13 ms\n", + "Wall time: 12.5 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = astar_search(BackwardPlan(spare_tire()), display=True).solution()\n", + "spare_tire_solution = list(map(lambda action: Expr(action.name, *action.args), spare_tire_solution))\n", + "spare_tire_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 94.7 ms, sys: 0 ns, total: 94.7 ms\n", + "Wall time: 93.2 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = CSPlan(spare_tire(), 3, arc_heuristic=no_heuristic)\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan with SAT UP Arc Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 119 ms, sys: 0 ns, total: 119 ms\n", + "Wall time: 118 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = CSPlan(spare_tire(), 3, arc_heuristic=sat_up)\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SATPlan with DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 9.01 s, sys: 3.98 ms, total: 9.01 s\n", + "Wall time: 9.01 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = SATPlan(spare_tire(), 4, SAT_solver=dpll_satisfiable)\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SATPlan with CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 630 ms, sys: 6 µs, total: 630 ms\n", + "Wall time: 628 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time spare_tire_solution = SATPlan(spare_tire(), 4, SAT_solver=cdcl_satisfiable)\n", + "spare_tire_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Shopping Problem" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mshopping_problem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m SHOPPING-PROBLEM\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m A problem of acquiring some items given their availability at certain stores.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example:\u001b[0m\n", + "\u001b[0;34m >>> from planning import *\u001b[0m\n", + "\u001b[0;34m >>> sp = shopping_problem()\u001b[0m\n", + "\u001b[0;34m >>> sp.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> sp.act(expr('Go(Home, HW)'))\u001b[0m\n", + "\u001b[0;34m >>> sp.act(expr('Buy(Drill, HW)'))\u001b[0m\n", + "\u001b[0;34m >>> sp.act(expr('Go(HW, SM)'))\u001b[0m\n", + "\u001b[0;34m >>> sp.act(expr('Buy(Banana, SM)'))\u001b[0m\n", + "\u001b[0;34m >>> sp.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> sp.act(expr('Buy(Milk, SM)'))\u001b[0m\n", + "\u001b[0;34m >>> sp.goal_test()\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m >>>\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Have(Milk) & Have(Banana) & Have(Drill)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Buy(x, store)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(store) & Sells(store, x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Have(x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Store(store) & Item(x)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Go(x, y)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(y) & ~At(x)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Place(x) & Place(y)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Place(Home) & Place(SM) & Place(HW) & Store(SM) & Store(HW) & '\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m'Item(Milk) & Item(Banana) & Item(Drill)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource shopping_problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GraphPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 5.08 ms, sys: 3 µs, total: 5.08 ms\n", + "Wall time: 5.03 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, HW), Go(Home, SM), Buy(Milk, SM), Buy(Drill, HW), Buy(Banana, SM)]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = GraphPlan(shopping_problem()).execute()\n", + "linearize(shopping_problem_solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "167 paths have been expanded and 257 paths remain in the frontier\n", + "CPU times: user 187 ms, sys: 4.01 ms, total: 191 ms\n", + "Wall time: 190 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, SM), Buy(Banana, SM), Buy(Milk, SM), Go(SM, HW), Buy(Drill, HW)]" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = uniform_cost_search(ForwardPlan(shopping_problem()), display=True).solution()\n", + "shopping_problem_solution = list(map(lambda action: Expr(action.name, *action.args), shopping_problem_solution))\n", + "shopping_problem_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9 paths have been expanded and 22 paths remain in the frontier\n", + "CPU times: user 101 ms, sys: 3 µs, total: 101 ms\n", + "Wall time: 100 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, SM), Buy(Banana, SM), Buy(Milk, SM), Go(SM, HW), Buy(Drill, HW)]" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = astar_search(ForwardPlan(shopping_problem()), display=True).solution()\n", + "shopping_problem_solution = list(map(lambda action: Expr(action.name, *action.args), shopping_problem_solution))\n", + "shopping_problem_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "176 paths have been expanded and 7 paths remain in the frontier\n", + "CPU times: user 109 ms, sys: 2 µs, total: 109 ms\n", + "Wall time: 107 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, HW), Buy(Drill, HW), Go(HW, SM), Buy(Milk, SM), Buy(Banana, SM)]" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = uniform_cost_search(BackwardPlan(shopping_problem()), display=True).solution()\n", + "shopping_problem_solution = list(map(lambda action: Expr(action.name, *action.args), shopping_problem_solution))\n", + "shopping_problem_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "18 paths have been expanded and 28 paths remain in the frontier\n", + "CPU times: user 235 ms, sys: 9 µs, total: 235 ms\n", + "Wall time: 234 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, SM), Buy(Banana, SM), Buy(Milk, SM), Go(SM, HW), Buy(Drill, HW)]" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = astar_search(BackwardPlan(shopping_problem()), display=True).solution()\n", + "shopping_problem_solution = list(map(lambda action: Expr(action.name, *action.args), shopping_problem_solution))\n", + "shopping_problem_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 194 ms, sys: 6 µs, total: 194 ms\n", + "Wall time: 192 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, HW), Buy(Drill, HW), Go(HW, SM), Buy(Banana, SM), Buy(Milk, SM)]" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = CSPlan(shopping_problem(), 5, arc_heuristic=no_heuristic)\n", + "shopping_problem_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan with SAT UP Arc Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 235 ms, sys: 7 µs, total: 235 ms\n", + "Wall time: 233 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, HW), Buy(Drill, HW), Go(HW, SM), Buy(Banana, SM), Buy(Milk, SM)]" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = CSPlan(shopping_problem(), 5, arc_heuristic=sat_up)\n", + "shopping_problem_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SATPlan with CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 29s, sys: 36 ms, total: 1min 29s\n", + "Wall time: 1min 29s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Go(Home, HW), Buy(Drill, HW), Go(HW, SM), Buy(Banana, SM), Buy(Milk, SM)]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time shopping_problem_solution = SATPlan(shopping_problem(), 5, SAT_solver=cdcl_satisfiable)\n", + "shopping_problem_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Air Cargo" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mair_cargo\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m [Figure 10.1] AIR-CARGO-PROBLEM\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m An air-cargo shipment problem for delivering cargo to different locations,\u001b[0m\n", + "\u001b[0;34m given the starting location and airplanes.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example:\u001b[0m\n", + "\u001b[0;34m >>> from planning import *\u001b[0m\n", + "\u001b[0;34m >>> ac = air_cargo()\u001b[0m\n", + "\u001b[0;34m >>> ac.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Load(C2, P2, JFK)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Load(C1, P1, SFO)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Fly(P1, SFO, JFK)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Fly(P2, JFK, SFO)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Unload(C2, P2, SFO)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.goal_test()\u001b[0m\n", + "\u001b[0;34m False\u001b[0m\n", + "\u001b[0;34m >>> ac.act(expr('Unload(C1, P1, JFK)'))\u001b[0m\n", + "\u001b[0;34m >>> ac.goal_test()\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m >>>\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mPlanningProblem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitial\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(C1, SFO) & At(C2, JFK) & At(P1, SFO) & At(P2, JFK)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mgoals\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(C1, JFK) & At(C2, SFO)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mactions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Load(c, p, a)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(c, a) & At(p, a)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'In(c, p) & ~At(c, a)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Cargo(c) & Plane(p) & Airport(a)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Unload(c, p, a)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'In(c, p) & At(p, a)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(c, a) & ~In(c, p)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Cargo(c) & Plane(p) & Airport(a)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mAction\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Fly(p, f, to)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mprecond\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(p, f)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'At(p, to) & ~At(p, f)'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Plane(p) & Airport(f) & Airport(to)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdomain\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Cargo(C1) & Cargo(C2) & Plane(P1) & Plane(P2) & Airport(SFO) & Airport(JFK)'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource air_cargo" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GraphPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 9.06 ms, sys: 3 µs, total: 9.06 ms\n", + "Wall time: 8.94 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Unload(C1, P1, JFK),\n", + " Unload(C2, P2, SFO)]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = GraphPlan(air_cargo()).execute()\n", + "linearize(air_cargo_solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "838 paths have been expanded and 1288 paths remain in the frontier\n", + "CPU times: user 3.56 s, sys: 4 ms, total: 3.57 s\n", + "Wall time: 3.56 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Unload(C2, P2, SFO),\n", + " Load(C1, P2, SFO),\n", + " Fly(P2, SFO, JFK),\n", + " Unload(C1, P2, JFK)]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = uniform_cost_search(ForwardPlan(air_cargo()), display=True).solution()\n", + "air_cargo_solution = list(map(lambda action: Expr(action.name, *action.args), air_cargo_solution))\n", + "air_cargo_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### ForwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "17 paths have been expanded and 54 paths remain in the frontier\n", + "CPU times: user 716 ms, sys: 0 ns, total: 716 ms\n", + "Wall time: 717 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Unload(C2, P2, SFO),\n", + " Load(C1, P2, SFO),\n", + " Fly(P2, SFO, JFK),\n", + " Unload(C1, P2, JFK)]" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = astar_search(ForwardPlan(air_cargo()), display=True).solution()\n", + "air_cargo_solution = list(map(lambda action: Expr(action.name, *action.args), air_cargo_solution))\n", + "air_cargo_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "506 paths have been expanded and 65 paths remain in the frontier\n", + "CPU times: user 970 ms, sys: 0 ns, total: 970 ms\n", + "Wall time: 971 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Load(C2, P1, JFK),\n", + " Unload(C1, P1, JFK),\n", + " Fly(P1, JFK, SFO),\n", + " Unload(C2, P1, SFO)]" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = uniform_cost_search(BackwardPlan(air_cargo()), display=True).solution()\n", + "air_cargo_solution = list(map(lambda action: Expr(action.name, *action.args), air_cargo_solution))\n", + "air_cargo_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### BackwardPlan with Ignore Delete Lists Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "23 paths have been expanded and 50 paths remain in the frontier\n", + "CPU times: user 1.19 s, sys: 2 µs, total: 1.19 s\n", + "Wall time: 1.2 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Unload(C2, P2, SFO),\n", + " Load(C1, P2, SFO),\n", + " Fly(P2, SFO, JFK),\n", + " Unload(C1, P2, JFK)]" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = astar_search(BackwardPlan(air_cargo()), display=True).solution()\n", + "air_cargo_solution = list(map(lambda action: Expr(action.name, *action.args), air_cargo_solution))\n", + "air_cargo_solution[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 6.5 s, sys: 0 ns, total: 6.5 s\n", + "Wall time: 6.51 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Load(C2, P1, JFK),\n", + " Unload(C1, P1, JFK),\n", + " Fly(P1, JFK, SFO),\n", + " Unload(C2, P1, SFO)]" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = CSPlan(air_cargo(), 6, arc_heuristic=no_heuristic)\n", + "air_cargo_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSPlan with SAT UP Arc Heuristic" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 13.6 s, sys: 7.98 ms, total: 13.7 s\n", + "Wall time: 13.7 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Load(C2, P1, JFK),\n", + " Unload(C1, P1, JFK),\n", + " Fly(P1, JFK, SFO),\n", + " Unload(C2, P1, SFO)]" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time air_cargo_solution = CSPlan(air_cargo(), 6, arc_heuristic=sat_up)\n", + "air_cargo_solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[[1]](#ref-1) Hoffmann, Jörg. 2001. _FF: The fast-forward planning system_.\n", + "\n", + "[[2]](#ref-2) Kautz, Henry A and Selman, Bart and others. 1992. _Planning as Satisfiability_." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5rc1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/csp.ipynb b/csp.ipynb index 3ce7ce2d8..5d490846b 100644 --- a/csp.ipynb +++ b/csp.ipynb @@ -2,69 +2,337 @@ "cells": [ { "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ - "# Constraint Satisfaction Problems (CSPs)\n", + "# CONSTRAINT SATISFACTION PROBLEMS\n", "\n", - "This IPy notebook acts as supporting material for topics covered in **Chapter 6 Constraint Satisfaction Problems** of the book* Artificial Intelligence: A Modern Approach*. We make use of the implementations in **csp.py** module. Even though this notebook includes a brief summary of the main topics familiarity with the material present in the book is expected. We will look at some visualizations and solve some of the CSP problems described in the book. Let us import everything from the csp module to get started." + "This IPy notebook acts as supporting material for topics covered in **Chapter 6 Constraint Satisfaction Problems** of the book* Artificial Intelligence: A Modern Approach*. We make use of the implementations in **csp.py** module. Even though this notebook includes a brief summary of the main topics, familiarity with the material present in the book is expected. We will look at some visualizations and solve some of the CSP problems described in the book. Let us import everything from the csp module to get started." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 1, + "metadata": {}, "outputs": [], "source": [ - "from csp import *" + "from csp import *\n", + "from notebook import psource, plot_NQueens\n", + "%matplotlib inline\n", + "\n", + "# Hide warnings in the matplotlib sections\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Review\n", + "## CONTENTS\n", "\n", - "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.\n" + "* Overview\n", + "* Graph Coloring\n", + "* N-Queens\n", + "* AC-3\n", + "* Backtracking Search\n", + "* Tree CSP Solver\n", + "* Graph Coloring Visualization\n", + "* N-Queens Visualization" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "cell_type": "markdown", + "metadata": {}, "source": [ - "%psource CSP" + "## OVERVIEW\n", + "\n", + "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": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\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",
    +       "        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",
    +       "\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",
    +       "\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",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "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." + "The __ _ _init_ _ __ method parameters specify the CSP. Variables can be passed as a list of strings or integers. Domains are passed as dict (dictionary datatpye) where \"key\" specifies the variables and \"value\" specifies 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 of its values 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", + "## 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." + "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 and returns it as a 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": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['R', 'G', 'B']" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "s = UniversalDict(['R','G','B'])\n", "s[5]" @@ -74,33 +342,133 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For our CSP we also need to define a constraint function **f(A, a, B, b)**. In this what we need is that the neighbors must not have the same color. This is defined in the function **different_values_constraint** of the module." + "For our CSP we also need to define a constraint function **f(A, a, B, b)**. In this, we need to ensure that the neighbors don't have the same color. This is defined in the function **different_values_constraint** of the module." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "%psource different_values_constraint" + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\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", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(different_values_constraint)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The CSP class takes neighbors in the form of a Dict. The module specifies a simple helper function named **parse_neighbors** which allows to take input in the form of strings and return a Dict of the form compatible with the **CSP Class**." + "The CSP class takes neighbors in the form of a Dict. The module specifies a simple helper function named **parse_neighbors** which allows us to take input in the form of strings and return a Dict of a form that is compatible with the **CSP Class**." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 5, + "metadata": {}, "outputs": [], "source": [ "%pdoc parse_neighbors" @@ -110,85 +478,722 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The **MapColoringCSP** function creates and returns a CSP with the above constraint function and states. The variables our the keys of the neighbors dict and the constraint is the one specified by the **different_values_constratint** function. **australia**, **usa** and **france** are three CSPs that have been created using **MapColoringCSP**. **australia** corresponds to ** Figure 6.1 ** in the book." + "The **MapColoringCSP** function creates and returns a CSP with the above constraint function and states. The variables are the keys of the neighbors dict and the constraint is the one specified by the **different_values_constratint** function. **Australia**, **USA** and **France** are three CSPs that have been created using **MapColoringCSP**. **Australia** corresponds to ** Figure 6.1 ** in the book." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def MapColoringCSP(colors, neighbors):\n",
    +       "    """Make a CSP for the problem of coloring a map with different colors\n",
    +       "    for any two adjacent regions. Arguments are a list of colors, and a\n",
    +       "    dict of {region: [neighbor,...]} entries. This dict may also be\n",
    +       "    specified as a string of the form defined by parse_neighbors."""\n",
    +       "    if isinstance(neighbors, str):\n",
    +       "        neighbors = parse_neighbors(neighbors)\n",
    +       "    return CSP(list(neighbors.keys()), UniversalDict(colors), neighbors,\n",
    +       "               different_values_constraint)\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(MapColoringCSP)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(,\n", + " ,\n", + " )" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "%psource MapColoringCSP" + "australia_csp, usa_csp, france_csp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## N-QUEENS\n", + "\n", + "The N-queens puzzle is the problem of placing N chess queens on an N×N chessboard in a way such that no two queens threaten each other. Here N is a natural number. Like the graph coloring problem, NQueens is also implemented in the csp module. The **NQueensCSP** class inherits from the **CSP** class. It makes some modifications in the methods to suit this particular problem. The queens are assumed to be placed one per column, from left to right. That means position (x, y) represents (var, val) in the CSP. The constraint that needs to be passed to the CSP is defined in the **queen_constraint** function. The 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. " ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\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",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(queen_constraint)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The **NQueensCSP** method implements methods that support solving the problem via **min_conflicts** which is one of the many popular techniques for solving CSPs. Because **min_conflicts** hill climbs the number of conflicts to solve, the CSP **assign** and **unassign** are modified to record conflicts. More details about the structures: **rows**, **downs**, **ups** which help in recording conflicts are explained in the docstring." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    class NQueensCSP(CSP):\n",
    +       "    """Make a CSP for the nQueens problem for search with min_conflicts.\n",
    +       "    Suitable for large n, it uses only data structures of size O(n).\n",
    +       "    Think of placing queens one per column, from left to right.\n",
    +       "    That means position (x, y) represents (var, val) in the CSP.\n",
    +       "    The main structures are three arrays to count queens that could conflict:\n",
    +       "        rows[i]      Number of queens in the ith row (i.e val == i)\n",
    +       "        downs[i]     Number of queens in the \\ diagonal\n",
    +       "                     such that their (x, y) coordinates sum to i\n",
    +       "        ups[i]       Number of queens in the / diagonal\n",
    +       "                     such that their (x, y) coordinates have x-y+n-1 = i\n",
    +       "    We increment/decrement these counts each time a queen is placed/moved from\n",
    +       "    a row/diagonal. So moving is O(1), as is nconflicts.  But choosing\n",
    +       "    a variable, and a best value for the variable, are each O(n).\n",
    +       "    If you want, you can keep track of conflicted variables, then variable\n",
    +       "    selection will also be O(1).\n",
    +       "    >>> len(backtracking_search(NQueensCSP(8)))\n",
    +       "    8\n",
    +       "    """\n",
    +       "\n",
    +       "    def __init__(self, n):\n",
    +       "        """Initialize data structures for n Queens."""\n",
    +       "        CSP.__init__(self, list(range(n)), UniversalDict(list(range(n))),\n",
    +       "                     UniversalDict(list(range(n))), queen_constraint)\n",
    +       "\n",
    +       "        self.rows = [0] * n\n",
    +       "        self.ups = [0] * (2 * n - 1)\n",
    +       "        self.downs = [0] * (2 * n - 1)\n",
    +       "\n",
    +       "    def nconflicts(self, var, val, assignment):\n",
    +       "        """The number of conflicts, as recorded with each assignment.\n",
    +       "        Count conflicts in row and in up, down diagonals. If there\n",
    +       "        is a queen there, it can't conflict with itself, so subtract 3."""\n",
    +       "        n = len(self.variables)\n",
    +       "        c = self.rows[val] + self.downs[var + val] + self.ups[var - val + n - 1]\n",
    +       "        if assignment.get(var, None) == val:\n",
    +       "            c -= 3\n",
    +       "        return c\n",
    +       "\n",
    +       "    def assign(self, var, val, assignment):\n",
    +       "        """Assign var, and keep track of conflicts."""\n",
    +       "        oldval = assignment.get(var, None)\n",
    +       "        if val != oldval:\n",
    +       "            if oldval is not None:  # Remove old val if there was one\n",
    +       "                self.record_conflict(assignment, var, oldval, -1)\n",
    +       "            self.record_conflict(assignment, var, val, +1)\n",
    +       "            CSP.assign(self, var, val, assignment)\n",
    +       "\n",
    +       "    def unassign(self, var, assignment):\n",
    +       "        """Remove var from assignment (if it is there) and track conflicts."""\n",
    +       "        if var in assignment:\n",
    +       "            self.record_conflict(assignment, var, assignment[var], -1)\n",
    +       "        CSP.unassign(self, var, assignment)\n",
    +       "\n",
    +       "    def record_conflict(self, assignment, var, val, delta):\n",
    +       "        """Record conflicts caused by addition or deletion of a Queen."""\n",
    +       "        n = len(self.variables)\n",
    +       "        self.rows[val] += delta\n",
    +       "        self.downs[var + val] += delta\n",
    +       "        self.ups[var - val + n - 1] += delta\n",
    +       "\n",
    +       "    def display(self, assignment):\n",
    +       "        """Print the queens and the nconflicts values (for debugging)."""\n",
    +       "        n = len(self.variables)\n",
    +       "        for val in range(n):\n",
    +       "            for var in range(n):\n",
    +       "                if assignment.get(var, '') == val:\n",
    +       "                    ch = 'Q'\n",
    +       "                elif (var + val) % 2 == 0:\n",
    +       "                    ch = '.'\n",
    +       "                else:\n",
    +       "                    ch = '-'\n",
    +       "                print(ch, end=' ')\n",
    +       "            print('    ', end=' ')\n",
    +       "            for var in range(n):\n",
    +       "                if assignment.get(var, '') == val:\n",
    +       "                    ch = '*'\n",
    +       "                else:\n",
    +       "                    ch = ' '\n",
    +       "                print(str(self.nconflicts(var, val, assignment)) + ch, end=' ')\n",
    +       "            print()\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(NQueensCSP)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The _ ___init___ _ method takes only one parameter **n** i.e. the size of the problem. To create an instance, we just pass the required value of n into the constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, "outputs": [], "source": [ - "australia, usa, france" + "eight_queens = NQueensCSP(8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## NQueens\n", + "We have defined our CSP. \n", + "Now, we need to solve this.\n", "\n", - "The N-queens puzzle is the problem of placing N chess queens on a N×N chessboard so that no two queens threaten each other. Here N is a natural number. Like the graph coloring, problem NQueens is also implemented in the csp module. The **NQueensCSP** class inherits from the **CSP** class. It makes some modifications in the methods to suit the particular problem. The queens are assumed to be placed one per column, from left to right. That means position (x, y) represents (var, val) in the CSP. The constraint that needs to be passed on the CSP is defined in the **queen_constraint** function. The 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. " + "### Min-conflicts\n", + "As stated above, the `min_conflicts` algorithm is an efficient method to solve such a problem.\n", + "
    \n", + "In the start, all the variables of the CSP are _randomly_ initialized. \n", + "
    \n", + "The algorithm then randomly selects a variable that has conflicts and violates some constraints of the CSP.\n", + "
    \n", + "The selected variable is then assigned a value that _minimizes_ the number of conflicts.\n", + "
    \n", + "This is a simple **stochastic algorithm** which works on a principle similar to **Hill-climbing**.\n", + "The conflicting state is repeatedly changed into a state with fewer conflicts in an attempt to reach an approximate solution.\n", + "
    \n", + "This algorithm sometimes benefits from having a good initial assignment.\n", + "Using greedy techniques to get a good initial assignment and then using `min_conflicts` to solve the CSP can speed up the procedure dramatically, especially for CSPs with a large state space." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def min_conflicts(csp, max_steps=100000):\n",
    +       "    """Solve a CSP by stochastic Hill Climbing on the number of conflicts."""\n",
    +       "    # Generate a complete assignment for all variables (probably with conflicts)\n",
    +       "    csp.current = current = {}\n",
    +       "    for var in csp.variables:\n",
    +       "        val = min_conflicts_value(csp, var, current)\n",
    +       "        csp.assign(var, val, current)\n",
    +       "    # Now repeatedly choose a random conflicted variable and change it\n",
    +       "    for i in range(max_steps):\n",
    +       "        conflicted = csp.conflicted_vars(current)\n",
    +       "        if not conflicted:\n",
    +       "            return current\n",
    +       "        var = random.choice(conflicted)\n",
    +       "        val = min_conflicts_value(csp, var, current)\n",
    +       "        csp.assign(var, val, current)\n",
    +       "    return None\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(min_conflicts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use this algorithm to solve the `eight_queens` CSP." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, "outputs": [], "source": [ - "%psource queen_constraint" + "solution = min_conflicts(eight_queens)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The **NQueensCSP** method implements methods that support solving the problem via **min_conflicts** which is one of the techniques for solving CSPs. Because **min_conflicts** hill climbs the number of conflicts to solve the CSP **assign** and **unassign** are modified to record conflicts. More details about the structures **rows**, **downs**, **ups** which help in recording conflicts are explained in the docstring." + "This is indeed a valid solution. \n", + "
    \n", + "`notebook.py` has a helper function to visualize the solution space." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ - "%psource NQueensCSP" + "plot_NQueens(solution)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The _ ___init___ _ method takes only one parameter **n** the size of the problem. To create an instance we just pass the required n into the constructor." + "Lets' see if we can find a different solution." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "eight_queens = NQueensCSP(8)\n", + "solution = min_conflicts(eight_queens)\n", + "plot_NQueens(solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "eight_queens = NQueensCSP(8)" + "The solution is a bit different this time. \n", + "Running the above cell several times should give you different valid solutions.\n", + "
    \n", + "In the `search.ipynb` notebook, we will see how NQueensProblem can be solved using a **heuristic search method** such as `uniform_cost_search` and `astar_search`." ] }, { @@ -197,15 +1202,13 @@ "source": [ "### Helper Functions\n", "\n", - "We will now implement few helper functions that will help us visualize the Coloring Problem. We will make some modifications to the existing Classes and Functions for additional book keeping. To begin with we modify the **assign** and **unassign** methods in the **CSP** to add a copy of the assignment to the **assingment_history**. We call this new class **InstruCSP**. This would allow us to see how the assignment evolves over time." + "We will now implement a few helper functions that will allow us to visualize the Coloring Problem; we'll also make a few modifications to the existing classes and functions for additional record keeping. To begin, we modify the **assign** and **unassign** methods in the **CSP** in order to add a copy of the assignment to the **assignment_history**. We name this new class as **InstruCSP**; it will allow us to see how the assignment evolves over time. " ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 15, + "metadata": {}, "outputs": [], "source": [ "import copy\n", @@ -213,50 +1216,45 @@ " \n", " def __init__(self, variables, domains, neighbors, constraints):\n", " super().__init__(variables, domains, neighbors, constraints)\n", - " self.assingment_history = []\n", + " self.assignment_history = []\n", " \n", " def assign(self, var, val, assignment):\n", " super().assign(var,val, assignment)\n", - " self.assingment_history.append(copy.deepcopy(assignment))\n", + " self.assignment_history.append(copy.deepcopy(assignment))\n", " \n", " def unassign(self, var, assignment):\n", " super().unassign(var,assignment)\n", - " self.assingment_history.append(copy.deepcopy(assignment)) " + " self.assignment_history.append(copy.deepcopy(assignment))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Next, we define **make_instru** which takes an instance of **CSP** and returns a **InstruCSP** instance. " + "Next, we define **make_instru** which takes an instance of **CSP** and returns an instance of **InstruCSP**." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 16, + "metadata": {}, "outputs": [], "source": [ "def make_instru(csp):\n", - " return InstruCSP(csp.variables, csp.domains, csp.neighbors,\n", - " csp.constraints)" + " return InstruCSP(csp.variables, csp.domains, csp.neighbors, csp.constraints)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We will now use a graph defined as a dictonary for plotting purposes in our Graph Coloring Problem. The keys are the nodes and their corresponding values are the nodes are they are connected to." + "We will now use a graph defined as a dictionary for plotting purposes in our Graph Coloring Problem. The keys are the nodes and their values are the corresponding nodes they are connected to." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 17, + "metadata": {}, "outputs": [], "source": [ "neighbors = {\n", @@ -293,10 +1291,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 18, + "metadata": {}, "outputs": [], "source": [ "coloring_problem = MapColoringCSP('RGBY', neighbors)" @@ -304,10 +1300,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 19, + "metadata": {}, "outputs": [], "source": [ "coloring_problem1 = make_instru(coloring_problem)" @@ -317,185 +1311,1132 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Backtracking Search\n", + "### CONSTRAINT PROPAGATION\n", + "Algorithms that solve CSPs have a choice between searching and or doing a _constraint propagation_, a specific type of inference.\n", + "The constraints can be used to reduce the number of legal values for another variable, which in turn can reduce the legal values for some other variable, and so on. \n", + "
    \n", + "Constraint propagation tries to enforce _local consistency_.\n", + "Consider each variable as a node in a graph and each binary constraint as an arc.\n", + "Enforcing local consistency causes inconsistent values to be eliminated throughout the graph, \n", + "a lot like the `GraphPlan` algorithm in planning, where mutex links are removed from a planning graph.\n", + "There are different types of local consistencies:\n", + "1. Node consistency\n", + "2. Arc consistency\n", + "3. Path consistency\n", + "4. K-consistency\n", + "5. Global constraints\n", + "\n", + "Refer __section 6.2__ in the book for details.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AC-3\n", + "Before we dive into AC-3, we need to know what _arc-consistency_ is.\n", + "
    \n", + "A variable $X_i$ is __arc-consistent__ with respect to another variable $X_j$ if for every value in the current domain $D_i$ there is some value in the domain $D_j$ that satisfies the binary constraint on the arc $(X_i, X_j)$.\n", + "
    \n", + "A network is arc-consistent if every variable is arc-consistent with every other variable.\n", + "
    \n", "\n", - "For solving a CSP the main issue with Naive search algorithms is that they can continue expanding obviously wrong paths. In backtracking search, we check constraints as we go. Backtracking is just the above idea combined with the fact that we are dealing with one variable at a time. Backtracking Search is implemented in the repository as the function **backtracking_search**. This is the same as **Figure 6.5** in the book. The function takes as input a CSP and few other optional parameters which can be used to further speed it up. The function returns the correct assignment if it satisfies the goal. We will discuss these later. Let us solve our **coloring_problem1** with **backtracking_search**.\n" + "AC-3 is an algorithm that enforces arc consistency.\n", + "After applying AC-3, either every arc is arc-consistent, or some variable has an empty domain, indicating that the CSP cannot be solved.\n", + "Let's see how `AC3` is implemented in the module." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def AC3(csp, queue=None, removals=None, arc_heuristic=dom_j_up):\n",
    +       "    """[Figure 6.3]"""\n",
    +       "    if queue is None:\n",
    +       "        queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]}\n",
    +       "    csp.support_pruning()\n",
    +       "    queue = arc_heuristic(csp, queue)\n",
    +       "    while queue:\n",
    +       "        (Xi, Xj) = queue.pop()\n",
    +       "        if revise(csp, Xi, Xj, removals):\n",
    +       "            if not csp.curr_domains[Xi]:\n",
    +       "                return False\n",
    +       "            for Xk in csp.neighbors[Xi]:\n",
    +       "                if Xk != Xj:\n",
    +       "                    queue.add((Xk, Xi))\n",
    +       "    return True\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(AC3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "result = backtracking_search(coloring_problem1)" + "`AC3` also employs a helper function `revise`." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def revise(csp, Xi, Xj, removals):\n",
    +       "    """Return true if we remove a value."""\n",
    +       "    revised = False\n",
    +       "    for x in csp.curr_domains[Xi][:]:\n",
    +       "        # If Xi=x conflicts with Xj=y for every possible y, eliminate Xi=x\n",
    +       "        if all(not csp.constraints(Xi, x, Xj, y) for y in csp.curr_domains[Xj]):\n",
    +       "            csp.prune(Xi, x, removals)\n",
    +       "            revised = True\n",
    +       "    return revised\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(revise)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`AC3` maintains a queue of arcs to consider which initially contains all the arcs in the CSP.\n", + "An arbitrary arc $(X_i, X_j)$ is popped from the queue and $X_i$ is made _arc-consistent_ with respect to $X_j$.\n", + "
    \n", + "If in doing so, $D_i$ is left unchanged, the algorithm just moves to the next arc, \n", + "but if the domain $D_i$ is revised, then we add all the neighboring arcs $(X_k, X_i)$ to the queue.\n", + "
    \n", + "We repeat this process and if at any point, the domain $D_i$ is reduced to nothing, then we know the whole CSP has no consistent solution and `AC3` can immediately return failure.\n", + "
    \n", + "Otherwise, we keep removing values from the domains of variables until the queue is empty.\n", + "We finally get the arc-consistent CSP which is faster to search because the variables have smaller domains." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how `AC3` can be used.\n", + "
    \n", + "We'll first define the required variables." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, "outputs": [], "source": [ - "result # A dictonary of assingments." + "neighbors = parse_neighbors('A: B; B: ')\n", + "domains = {'A': [0, 1, 2, 3, 4], 'B': [0, 1, 2, 3, 4]}\n", + "constraints = lambda X, x, Y, y: x % 2 == 0 and (x + y) == 4 and y % 2 != 0\n", + "removals = []" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let us also check the number of assingments made." + "We'll now define a `CSP` object." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 23, + "metadata": {}, "outputs": [], "source": [ - "coloring_problem1.nassigns" + "csp = CSP(variables=None, domains=domains, neighbors=neighbors, constraints=constraints)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "AC3(csp, removals=removals)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now let us check the total number of assingments and unassingments which is the lentgh ofour assingment history." + "This configuration is inconsistent." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 25, + "metadata": {}, "outputs": [], "source": [ - "len(coloring_problem1.assingment_history)" + "constraints = lambda X, x, Y, y: (x % 2) == 0 and (x + y) == 4\n", + "removals = []\n", + "csp = CSP(variables=None, domains=domains, neighbors=neighbors, constraints=constraints)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "AC3(csp,removals=removals)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This configuration is consistent." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now let us explore the optional keyword arguments that the **backtracking_search** function takes. These optional arguments help speed up the assignment further. Along with these, we will also point out to methods in the CSP class that help make this work. \n", + "## BACKTRACKING SEARCH\n", "\n", - "The first of these is **select_unassigned_variable**. It takes in a function that helps in deciding the order in which variables will be selected for assignment. We use a heuristic called Most Restricted Variable which is implemented by the function **mrv**. The idea behind **mrv** is to choose the variable with the fewest legal values left in its domain. The intuition behind selecting the **mrv** or the most constrained variable is that it allows us to encounter failure quickly before going too deep into a tree if we have selected a wrong step before. The **mrv** implementation makes use of another function **num_legal_values** to sort out the variables by a number of legal values left in its domain. This function, in turn, calls the **nconflicts** method of the **CSP** to return such values.\n" + "The main issue with using Naive Search Algorithms to solve a CSP is that they can continue to expand obviously wrong paths; whereas, in **backtracking search**, we check the constraints as we go and we deal with only one variable at a time. Backtracking Search is implemented in the repository as the function **backtracking_search**. This is the same as **Figure 6.5** in the book. The function takes as input a CSP and a few other optional parameters which can be used to speed it up further. The function returns the correct assignment if it satisfies the goal. However, we will discuss these later. For now, let us solve our **coloring_problem1** with **backtracking_search**." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 27, + "metadata": {}, "outputs": [], "source": [ - "%psource mrv" + "result = backtracking_search(coloring_problem1)" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'R',\n", + " 1: 'R',\n", + " 2: 'R',\n", + " 3: 'R',\n", + " 4: 'G',\n", + " 5: 'R',\n", + " 6: 'G',\n", + " 7: 'R',\n", + " 8: 'B',\n", + " 9: 'R',\n", + " 10: 'G',\n", + " 11: 'B',\n", + " 12: 'G',\n", + " 13: 'G',\n", + " 14: 'Y',\n", + " 15: 'Y',\n", + " 16: 'B',\n", + " 17: 'B',\n", + " 18: 'B',\n", + " 19: 'G',\n", + " 20: 'B'}" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result # A dictonary of assignments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "%psource num_legal_values" + "Let us also check the number of assignments made." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "21" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "%psource CSP.nconflicts" + "coloring_problem1.nassigns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Another ordering related parameter **order_domain_values** governs the value ordering. Here we select the Least Constraining Value which is implemented by the function **lcv**. The idea is to select the value which rules out the fewest values in the remaining variables. The intuition behind selecting the **lcv** is that it leaves a lot of freedom to assign values later. The idea behind selecting the mrc and lcv makes sense because we need to do all variables but for values, we might better try the ones that are likely. So for vars, we face the hard ones first.\n" + "Now, let us check the total number of assignments and unassignments, which would be the length of our assignment history. We can see it by using the command below. " ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "21" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "%psource lcv" + "len(coloring_problem1.assignment_history)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, the third parameter **inference** can make use of one of the two techniques called Arc Consistency or Forward Checking. The details of these methods can be found in the **Section 6.3.2** of the book. In short the idea of inference is to detect the possible failure before it occurs and to look ahead to not make mistakes. **mac** and **forward_checking** implement these two techniques. The **CSP** methods **support_pruning**, **suppose**, **prune**, **choices**, **infer_assignment** and **restore** help in using these techniques. You can know more about these by looking up the source code." + "Now let us explore the optional keyword arguments that the **backtracking_search** function takes. These optional arguments help speed up the assignment further. Along with these, we will also point out the methods in the CSP class that help to make this work. \n", + "\n", + "The first one is **select_unassigned_variable**. It takes in, as a parameter, a function that helps in deciding the order in which the variables will be selected for assignment. We use a heuristic called Most Restricted Variable which is implemented by the function **mrv**. The idea behind **mrv** is to choose the variable with the least legal values left in its domain. The intuition behind selecting the **mrv** or the most constrained variable is that it allows us to encounter failure quickly before going too deep into a tree if we have selected a wrong step before. The **mrv** implementation makes use of another function **num_legal_values** to sort out the variables by the number of legal values left in its domain. This function, in turn, calls the **nconflicts** method of the **CSP** to return such values." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def mrv(assignment, csp):\n",
    +       "    """Minimum-remaining-values heuristic."""\n",
    +       "    return argmin_random_tie(\n",
    +       "        [v for v in csp.variables if v not in assignment],\n",
    +       "        key=lambda var: num_legal_values(csp, var, assignment))\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(mrv)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def num_legal_values(csp, var, assignment):\n",
    +       "    if csp.curr_domains:\n",
    +       "        return len(csp.curr_domains[var])\n",
    +       "    else:\n",
    +       "        return count(csp.nconflicts(var, val, assignment) == 0\n",
    +       "                     for val in csp.domains[var])\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(num_legal_values)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
        def nconflicts(self, var, val, assignment):\n",
    +       "        """Return the number of conflicts var=val has with other variables."""\n",
    +       "\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",
    +       "\n",
    +       "        return count(conflict(v) for v in self.neighbors[var])\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(CSP.nconflicts)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now let us compare the performance with these parameters enabled vs the default parameters. We will use the Graph Coloring problem instance usa for comparison. We will call the instances **solve_simple** and **solve_parameters** and solve them using backtracking and compare the number of assignments." + "Another ordering related parameter **order_domain_values** governs the value ordering. Here we select the Least Constraining Value which is implemented by the function **lcv**. The idea is to select the value which rules out least number of values in the remaining variables. The intuition behind selecting the **lcv** is that it allows a lot of freedom to assign values later. The idea behind selecting the mrc and lcv makes sense because we need to do all variables but for values, and it's better to try the ones that are likely. So for vars, we face the hard ones first." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def lcv(var, assignment, csp):\n",
    +       "    """Least-constraining-values heuristic."""\n",
    +       "    return sorted(csp.choices(var),\n",
    +       "                  key=lambda val: csp.nconflicts(var, val, assignment))\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(lcv)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the third parameter **inference** can make use of one of the two techniques called Arc Consistency or Forward Checking. The details of these methods can be found in the **Section 6.3.2** of the book. In short the idea of inference is to detect the possible failure before it occurs and to look ahead to not make mistakes. **mac** and **forward_checking** implement these two techniques. The **CSP** methods **support_pruning**, **suppose**, **prune**, **choices**, **infer_assignment** and **restore** help in using these techniques. You can find out more about these by looking up the source code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "solve_simple = copy.deepcopy(usa)\n", - "solve_parameters = copy.deepcopy(usa)" + "Now let us compare the performance with these parameters enabled vs the default parameters. We will use the Graph Coloring problem instance 'usa' for comparison. We will call the instances **solve_simple** and **solve_parameters** and solve them using backtracking and compare the number of assignments." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 35, + "metadata": {}, "outputs": [], + "source": [ + "solve_simple = copy.deepcopy(usa_csp)\n", + "solve_parameters = copy.deepcopy(usa_csp)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'SD': 'R',\n", + " 'MN': 'G',\n", + " 'ND': 'B',\n", + " 'MT': 'G',\n", + " 'IA': 'B',\n", + " 'WI': 'R',\n", + " 'NE': 'G',\n", + " 'MO': 'R',\n", + " 'IL': 'G',\n", + " 'WY': 'B',\n", + " 'ID': 'R',\n", + " 'KA': 'B',\n", + " 'UT': 'G',\n", + " 'NV': 'B',\n", + " 'OK': 'G',\n", + " 'CO': 'R',\n", + " 'OR': 'G',\n", + " 'KY': 'B',\n", + " 'AZ': 'R',\n", + " 'CA': 'Y',\n", + " 'IN': 'R',\n", + " 'OH': 'G',\n", + " 'WA': 'B',\n", + " 'MI': 'B',\n", + " 'AR': 'B',\n", + " 'NM': 'B',\n", + " 'TN': 'G',\n", + " 'TX': 'R',\n", + " 'MS': 'R',\n", + " 'AL': 'B',\n", + " 'VA': 'R',\n", + " 'WV': 'Y',\n", + " 'PA': 'R',\n", + " 'LA': 'G',\n", + " 'GA': 'R',\n", + " 'MD': 'G',\n", + " 'NC': 'B',\n", + " 'DC': 'B',\n", + " 'DE': 'B',\n", + " 'SC': 'G',\n", + " 'FL': 'G',\n", + " 'NJ': 'G',\n", + " 'NY': 'B',\n", + " 'MA': 'R',\n", + " 'CT': 'G',\n", + " 'RI': 'B',\n", + " 'VT': 'G',\n", + " 'NH': 'B',\n", + " 'ME': 'R'}" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "backtracking_search(solve_simple)\n", - "backtracking_search(solve_parameters, order_domain_values=lcv, select_unassigned_variable=mrv, inference=mac )" + "backtracking_search(solve_parameters, order_domain_values=lcv, select_unassigned_variable=mrv, inference=mac)" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "49" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "solve_simple.nassigns" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "49" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "solve_parameters.nassigns" ] @@ -504,17 +2445,214 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Graph Coloring Visualization\n", + "## TREE CSP SOLVER\n", + "\n", + "The `tree_csp_solver` function (**Figure 6.11** in the book) can be used to solve problems whose constraint graph is a tree. Given a CSP, with `neighbors` forming a tree, it returns an assignment that satisfies the given constraints. The algorithm works as follows:\n", + "\n", + "First it finds the *topological sort* of the tree. This is an ordering of the tree where each variable/node comes after its parent in the tree. The function that accomplishes this is `topological_sort`; it builds the topological sort using the recursive function `build_topological`. That function is an augmented DFS (Depth First Search), where each newly visited node of the tree is pushed on a stack. The stack in the end holds the variables topologically sorted.\n", + "\n", + "Then the algorithm makes arcs between each parent and child consistent. *Arc-consistency* between two variables, *a* and *b*, occurs when for every possible value of *a* there is an assignment in *b* that satisfies the problem's constraints. If such an assignment cannot be found, the problematic value is removed from *a*'s possible values. This is done with the use of the function `make_arc_consistent`, which takes as arguments a variable `Xj` and its parent, and makes the arc between them consistent by removing any values from the parent which do not allow for a consistent assignment in `Xj`.\n", "\n", - "Next, we define some functions to create the visualisation from the assingment_history of **coloring_problem1**. The reader need not concern himself with the code that immediately follows as it is the usage of Matplotib with IPython Widgets. If you are interested in reading more about these visit [ipywidgets.readthedocs.io](http://ipywidgets.readthedocs.io). We will be using the **networkx** library to generate graphs. These graphs can be treated as the graph that needs to be colored or as a constraint graph for this problem. If interested you can read a dead simple tutorial [here](https://www.udacity.com/wiki/creating-network-graphs-with-python). We start by importing the necessary libraries and initializing matplotlib inline.\n" + "If an arc cannot be made consistent, the solver fails. If every arc is made consistent, we move to assigning values.\n", + "\n", + "First we assign a random value to the root from its domain and then we assign values to the rest of the variables. Since the graph is now arc-consistent, we can simply move from variable to variable picking any remaining consistent values. At the end we are left with a valid assignment. If at any point though we find a variable where no consistent value is left in its domain, the solver fails.\n", + "\n", + "Run the cell below to see the implementation of the algorithm:" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def tree_csp_solver(csp):\n",
    +       "    """[Figure 6.11]"""\n",
    +       "    assignment = {}\n",
    +       "    root = csp.variables[0]\n",
    +       "    X, parent = topological_sort(csp, root)\n",
    +       "\n",
    +       "    csp.support_pruning()\n",
    +       "    for Xj in reversed(X[1:]):\n",
    +       "        if not make_arc_consistent(parent[Xj], Xj, csp):\n",
    +       "            return None\n",
    +       "\n",
    +       "    assignment[root] = csp.curr_domains[root][0]\n",
    +       "    for Xi in X[1:]:\n",
    +       "        assignment[Xi] = assign_value(parent[Xi], Xi, csp, assignment)\n",
    +       "        if not assignment[Xi]:\n",
    +       "            return None\n",
    +       "    return assignment\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(tree_csp_solver)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now use the above function to solve a problem. More specifically, we will solve the problem of coloring Australia's map. We have two colors at our disposal: Red and Blue. As a reminder, this is the graph of Australia:\n", + "\n", + "`\"SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: \"`\n", + "\n", + "Unfortunately, as you can see, the above is not a tree. However, if we remove `SA`, which has arcs to `WA`, `NT`, `Q`, `NSW` and `V`, we are left with a tree (we also remove `T`, since it has no in-or-out arcs). We can now solve this using our algorithm. Let's define the map coloring problem at hand:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "australia_small = MapColoringCSP(list('RB'),\n", + " 'NT: WA Q; NSW: Q V')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will input `australia_small` to the `tree_csp_solver` and print the given assignment." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'NT': 'R', 'Q': 'B', 'NSW': 'R', 'V': 'B', 'WA': 'B'}\n" + ] + } + ], + "source": [ + "assignment = tree_csp_solver(australia_small)\n", + "print(assignment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`WA`, `Q` and `V` got painted with the same color and `NT` and `NSW` got painted with the other." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GRAPH COLORING VISUALIZATION\n", + "\n", + "Next, we define some functions to create the visualisation from the assignment_history of **coloring_problem1**. The readers need not concern themselves with the code that immediately follows as it is the usage of Matplotib with IPython Widgets. If you are interested in reading more about these, visit [ipywidgets.readthedocs.io](http://ipywidgets.readthedocs.io). We will be using the **networkx** library to generate graphs. These graphs can be treated as graphs that need to be colored or as constraint graphs for this problem. If interested you can check out a fairly simple tutorial [here](https://www.udacity.com/wiki/creating-network-graphs-with-python). We start by importing the necessary libraries and initializing matplotlib inline.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", @@ -528,32 +2666,30 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ipython widgets we will be using require the plots in the form of a step function such that there is a graph corresponding to each value. We define the **make_update_step_function** which return such a function. It takes in as inputs the neighbors/graph along with an instance of the **InstruCSP**. This will be more clear with the example below. If this sounds confusing do not worry this is not the part of the core material and our only goal is to help you visualize how the process works." + "The ipython widgets we will be using require the plots in the form of a step function such that there is a graph corresponding to each value. We define the **make_update_step_function** which returns such a function. It takes in as inputs the neighbors/graph along with an instance of the **InstruCSP**. The example below will elaborate it further. If this sounds confusing, don't worry. This is not part of the core material and our only goal is to help you visualize how the process works." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 43, + "metadata": {}, "outputs": [], "source": [ "def make_update_step_function(graph, instru_csp):\n", " \n", + " #define a function to draw the graphs\n", " def draw_graph(graph):\n", - " # create networkx graph\n", + " \n", " G=nx.Graph(graph)\n", - " # draw graph\n", " pos = nx.spring_layout(G,k=0.15)\n", " return (G, pos)\n", " \n", " G, pos = draw_graph(graph)\n", " \n", " def update_step(iteration):\n", - " # here iteration is the index of the assingment_history we want to visualize.\n", - " current = instru_csp.assingment_history[iteration]\n", - " # We convert the particular assingment to a default dict so that the color for nodes which \n", + " # here iteration is the index of the assignment_history we want to visualize.\n", + " current = instru_csp.assignment_history[iteration]\n", + " # We convert the particular assignment to a default dict so that the color for nodes which \n", " # have not been assigned defaults to black.\n", " current = defaultdict(lambda: 'Black', current)\n", "\n", @@ -563,11 +2699,11 @@ " nx.draw(G, pos, node_color=colors, node_size=500)\n", "\n", " labels = {label:label for label in G.node}\n", - " # Labels shifted by offset so as to not overlap nodes.\n", + " # Labels shifted by offset so that nodes don't overlap\n", " label_pos = {key:[value[0], value[1]+0.03] for key, value in pos.items()}\n", " nx.draw_networkx_labels(G, label_pos, labels, font_size=20)\n", "\n", - " # show graph\n", + " # display the graph\n", " plt.show()\n", "\n", " return update_step # <-- this is a function\n", @@ -591,15 +2727,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally let us plot our problem. We first use the function above to obtain a step function." + "Finally let us plot our problem. We first use the function below to obtain a step function." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 44, + "metadata": {}, "outputs": [], "source": [ "step_func = make_update_step_function(neighbors, coloring_problem1)" @@ -609,15 +2743,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next we set the canvas size." + "Next, we set the canvas size." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 45, + "metadata": {}, "outputs": [], "source": [ "matplotlib.rcParams['figure.figsize'] = (18.0, 18.0)" @@ -627,27 +2759,54 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally our plot using ipywidget slider and matplotib. You can move the slider to experiment and see the coloring change. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click. The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds upto one second for each time step." + "Finally, our plot using ipywidget slider and matplotib. You can move the slider to experiment and see the colors change. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click. The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds (upto one second) for each time step." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1882dd95ddd0465c8ec91d93a8a7224f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(IntSlider(value=0, description='iteration', max=20), Output()), _dom_classes=('widget-in…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3967e7c0226d434e8c08c7f4a59e2b2a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(ToggleButton(value=False, description='Visualize'), ToggleButtons(description='Extra Del…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import ipywidgets as widgets\n", "from IPython.display import display\n", "\n", - "iteration_slider = widgets.IntSlider(min=0, max=len(coloring_problem1.assingment_history)-1, step=1, value=0)\n", + "iteration_slider = widgets.IntSlider(min=0, max=len(coloring_problem1.assignment_history)-1, step=1, value=0)\n", "w=widgets.interactive(step_func,iteration=iteration_slider)\n", "display(w)\n", "\n", "visualize_callback = make_visualize(iteration_slider)\n", "\n", - "visualize_button = widgets.ToggleButton(desctiption = \"Visualize\", value = False)\n", + "visualize_button = widgets.ToggleButton(description = \"Visualize\", value = False)\n", "time_select = widgets.ToggleButtons(description='Extra Delay:',options=['0', '0.1', '0.2', '0.5', '0.7', '1.0'])\n", "\n", "a = widgets.interactive(visualize_callback, Visualize = visualize_button, time_step=time_select)\n", @@ -658,35 +2817,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## NQueens Visualization\n", + "## N-QUEENS VISUALIZATION\n", "\n", - "Just like the Graph Coloring Problem we will start with defining a few helper functions to help us visualize the assignments as they evolve over time. The **make_plot_board_step_function** behaves similar to the **make_update_step_function** introduced earlier. It initializes a chess board in the form of a 2D grid with alternating 0s and 1s. This is used by **plot_board_step** function which draws the board using matplotlib and adds queens to it. This function also calls the **label_queen_conflicts** which modifies the grid placing 3 in positions in a position where there is a conflict." + "Just like the Graph Coloring Problem, we will start with defining a few helper functions to help us visualize the assignments as they evolve over time. The **make_plot_board_step_function** behaves similar to the **make_update_step_function** introduced earlier. It initializes a chess board in the form of a 2D grid with alternating 0s and 1s. This is used by **plot_board_step** function which draws the board using matplotlib and adds queens to it. This function also calls the **label_queen_conflicts** which modifies the grid placing a 3 in any position where there is a conflict." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 47, + "metadata": {}, "outputs": [], "source": [ - "def label_queen_conflicts(assingment,grid):\n", + "def label_queen_conflicts(assignment,grid):\n", " ''' Mark grid with queens that are under conflict. '''\n", - " for col, row in assingment.items(): # check each queen for conflict\n", - " row_conflicts = {temp_col:temp_row for temp_col,temp_row in assingment.items() \n", - " if temp_row == row and temp_col != col}\n", - " up_conflicts = {temp_col:temp_row for temp_col,temp_row in assingment.items() \n", - " if temp_row+temp_col == row+col and temp_col != col}\n", - " down_conflicts = {temp_col:temp_row for temp_col,temp_row in assingment.items() \n", - " if temp_row-temp_col == row-col and temp_col != col}\n", + " for col, row in assignment.items(): # check each queen for conflict\n", + " conflicts = {temp_col:temp_row for temp_col,temp_row in assignment.items() \n", + " if (temp_row == row and temp_col != col)\n", + " or (temp_row+temp_col == row+col and temp_col != col)\n", + " or (temp_row-temp_col == row-col and temp_col != col)}\n", " \n", - " # Now marking the grid.\n", - " for col, row in row_conflicts.items():\n", - " grid[col][row] = 3\n", - " for col, row in up_conflicts.items():\n", - " grid[col][row] = 3\n", - " for col, row in down_conflicts.items():\n", + " # Place a 3 in positions where this is a conflict\n", + " for col, row in conflicts.items():\n", " grid[col][row] = 3\n", "\n", " return grid\n", @@ -702,7 +2853,7 @@ " \n", " def plot_board_step(iteration):\n", " ''' Add Queens to the Board.'''\n", - " data = instru_csp.assingment_history[iteration]\n", + " data = instru_csp.assignment_history[iteration]\n", " \n", " grid = [[(col+row+1)%2 for col in range(n)] for row in range(n)]\n", " grid = label_queen_conflicts(data, grid) # Update grid with conflict labels.\n", @@ -730,15 +2881,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now let us visualize a solution obtained via backtracking. We use of the previosuly defined **make_instru** function for keeping a history of steps." + "Now let us visualize a solution obtained via backtracking. We make use of the previosuly defined **make_instru** function for keeping a history of steps." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 48, + "metadata": {}, "outputs": [], "source": [ "twelve_queens_csp = NQueensCSP(12)\n", @@ -748,10 +2897,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 49, + "metadata": {}, "outputs": [], "source": [ "backtrack_queen_step = make_plot_board_step_function(backtracking_instru_queen) # Step Function for Widgets" @@ -761,27 +2908,54 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now finally we set some matplotlib parameters to adjust how our plot will look. The font is necessary because the Black Queen Unicode character is not a part of all fonts. You can move the slider to experiment and observe the how queens are assigned. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click.The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds upto one second for each time step.\n" + "Now finally we set some matplotlib parameters to adjust how our plot will look like. The font is necessary because the Black Queen Unicode character is not a part of all fonts. You can move the slider to experiment and observe how the queens are assigned. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click. The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds of upto one second for each time step." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "582e8f9b8d2e4a31aa7d45de68fd5b7c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(IntSlider(value=0, description='iteration', max=473, step=0), Output()), _dom_classes=('…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bb0f50b970764cb4bbebeb69cd4fbd19", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(ToggleButton(value=False, description='Visualize'), ToggleButtons(description='Extra Del…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "matplotlib.rcParams['figure.figsize'] = (8.0, 8.0)\n", "matplotlib.rcParams['font.family'].append(u'Dejavu Sans')\n", "\n", - "iteration_slider = widgets.IntSlider(min=0, max=len(backtracking_instru_queen.assingment_history)-1, step=0, value=0)\n", + "iteration_slider = widgets.IntSlider(min=0, max=len(backtracking_instru_queen.assignment_history)-1, step=0, value=0)\n", "w=widgets.interactive(backtrack_queen_step,iteration=iteration_slider)\n", "display(w)\n", "\n", "visualize_callback = make_visualize(iteration_slider)\n", "\n", - "visualize_button = widgets.ToggleButton(desctiption = \"Visualize\", value = False)\n", + "visualize_button = widgets.ToggleButton(description = \"Visualize\", value = False)\n", "time_select = widgets.ToggleButtons(description='Extra Delay:',options=['0', '0.1', '0.2', '0.5', '0.7', '1.0'])\n", "\n", "a = widgets.interactive(visualize_callback, Visualize = visualize_button, time_step=time_select)\n", @@ -797,10 +2971,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 51, + "metadata": {}, "outputs": [], "source": [ "conflicts_instru_queen = make_instru(twelve_queens_csp)\n", @@ -809,10 +2981,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 52, + "metadata": {}, "outputs": [], "source": [ "conflicts_step = make_plot_board_step_function(conflicts_instru_queen)" @@ -822,29 +2992,63 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The visualization has same features as the above. But here it also highlights the conflicts by labeling the conflicted queens with a red background." + "This visualization has same features as the one above; however, this one also highlights the conflicts by labeling the conflicted queens with a red background." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "iteration_slider = widgets.IntSlider(min=0, max=len(conflicts_instru_queen.assingment_history)-1, step=0, value=0)\n", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "409c4961f6e04fbea5d07a01cb1797ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(IntSlider(value=0, description='iteration', max=27, step=0), Output()), _dom_classes=('w…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a55b1b50a9a44085a484b357aa26b50f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(ToggleButton(value=False, description='Visualize'), ToggleButtons(description='Extra Del…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "iteration_slider = widgets.IntSlider(min=0, max=len(conflicts_instru_queen.assignment_history)-1, step=0, value=0)\n", "w=widgets.interactive(conflicts_step,iteration=iteration_slider)\n", "display(w)\n", "\n", "visualize_callback = make_visualize(iteration_slider)\n", "\n", - "visualize_button = widgets.ToggleButton(desctiption = \"Visualize\", value = False)\n", + "visualize_button = widgets.ToggleButton(description = \"Visualize\", value = False)\n", "time_select = widgets.ToggleButtons(description='Extra Delay:',options=['0', '0.1', '0.2', '0.5', '0.7', '1.0'])\n", "\n", "a = widgets.interactive(visualize_callback, Visualize = visualize_button, time_step=time_select)\n", "display(a)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -863,13 +3067,18 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.4.3" - }, - "widgets": { - "state": {}, - "version": "1.1.1" + "version": "3.6.8" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "source": [], + "metadata": { + "collapsed": false + } + } } }, "nbformat": 4, - "nbformat_minor": 0 -} + "nbformat_minor": 1 +} \ No newline at end of file diff --git a/csp.py b/csp.py index f300cb816..46ae07dd5 100644 --- a/csp.py +++ b/csp.py @@ -1,36 +1,39 @@ -"""CSP (Constraint Satisfaction Problems) problems and solvers. (Chapter 6).""" - -from utils import argmin_random_tie, count, first -import search - -from collections import defaultdict -from functools import reduce +"""CSP (Constraint Satisfaction Problems) problems and solvers. (Chapter 6)""" import itertools -import re import random +import re +import string +from collections import defaultdict, Counter +from functools import reduce +from operator import eq, neg +from sortedcontainers import SortedSet + +import search +from utils import argmin_random_tie, count, first, extend -class CSP(search.Problem): +class CSP(search.Problem): """This class describes finite-domain Constraint Satisfaction Problems. A CSP is specified by the following inputs: - variables A list of variables; each is atomic (e.g. int or string). + variables A list of variables; each is atomic (e.g. int or string). domains A dict of {var:[possible_value, ...]} entries. neighbors A dict of {var:[var,...]} that for each variable lists the other variables that participate in constraints. constraints 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 + In the textbook and in most mathematical definitions, the constraints are specified as explicit pairs of allowable values, but the formulation here is easier to express and more compact for - most cases. (For example, the n-Queens problem can be represented - in O(n) space using this notation, instead of O(N^4) for the - explicit representation.) In terms of describing the CSP as a + most cases (for example, the n-Queens problem can be represented + in O(n) space using this notation, instead of O(n^4) for the + explicit representation). In terms of describing the CSP as a problem, that's all there is. However, the class also supports data structures and methods that help you - solve CSPs by calling a search function on the CSP. Methods and slots are + solve CSPs by calling a search function on the CSP. Methods and slots are as follows, where the argument 'a' represents an assignment, which is a dict of {var:val} entries: assign(var, val, a) Assign a[var] = val; do other bookkeeping @@ -49,19 +52,18 @@ class CSP(search.Problem): """ def __init__(self, variables, domains, neighbors, constraints): - "Construct a CSP problem. If variables is empty, it becomes domains.keys()." + """Construct a CSP problem. If variables is empty, it becomes domains.keys().""" + super().__init__(()) variables = variables or list(domains.keys()) - self.variables = variables self.domains = domains self.neighbors = neighbors self.constraints = constraints - self.initial = () self.curr_domains = None self.nassigns = 0 def assign(self, var, val, assignment): - "Add {var: val} to assignment; Discard the old value if any." + """Add {var: val} to assignment; Discard the old value if any.""" assignment[var] = val self.nassigns += 1 @@ -73,22 +75,23 @@ def unassign(self, var, assignment): del assignment[var] def nconflicts(self, var, val, assignment): - "Return the number of conflicts var=val has with other variables." + """Return the number of conflicts var=val has with other variables.""" + # Subclasses may implement this more efficiently def conflict(var2): - return (var2 in assignment and - not self.constraints(var, val, var2, assignment[var2])) + return var2 in assignment and not self.constraints(var, val, var2, assignment[var2]) + return count(conflict(v) for v in self.neighbors[var]) def display(self, assignment): - "Show a human-readable representation of the CSP." + """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(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 + """Return a list of applicable actions: non conflicting assignments to an unassigned variable.""" if len(state) == len(self.variables): return [] @@ -99,12 +102,12 @@ def actions(self, state): if self.nconflicts(var, val, assignment) == 0] def result(self, state, action): - "Perform an action and return the new state." + """Perform an action and return the new state.""" (var, val) = action return state + ((var, val),) def goal_test(self, state): - "The goal is to assign all variables, with all constraints satisfied." + """The goal is to assign all variables, with all constraints satisfied.""" assignment = dict(state) return (len(assignment) == len(self.variables) and all(self.nconflicts(variables, assignment[variables], assignment) == 0 @@ -119,69 +122,220 @@ def support_pruning(self): self.curr_domains = {v: list(self.domains[v]) for v in self.variables} def suppose(self, var, value): - "Start accumulating inferences from assuming var=value." + """Start accumulating inferences from assuming var=value.""" self.support_pruning() removals = [(var, a) for a in self.curr_domains[var] if a != value] self.curr_domains[var] = [value] return removals def prune(self, var, value, removals): - "Rule out var=value." + """Rule out var=value.""" self.curr_domains[var].remove(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." + """Return all values for var that aren't currently ruled out.""" return (self.curr_domains or self.domains)[var] def infer_assignment(self): - "Return the partial assignment implied by the current inferences." + """Return the partial assignment implied by the current inferences.""" self.support_pruning() return {v: self.curr_domains[v][0] for v in self.variables if 1 == len(self.curr_domains[v])} def restore(self, removals): - "Undo a supposition and all inferences from it." + """Undo a supposition and all inferences from it.""" for B, b in removals: self.curr_domains[B].append(b) # This is for min_conflicts search def conflicted_vars(self, current): - "Return a list of variables in current assignment that are in conflict" + """Return a list of variables in current assignment that are in conflict""" return [var for var in self.variables if self.nconflicts(var, current[var], current) > 0] + # ______________________________________________________________________________ -# Constraint Propagation with AC-3 +# Constraint Propagation with AC3 + + +def no_arc_heuristic(csp, queue): + return queue + +def dom_j_up(csp, queue): + return SortedSet(queue, key=lambda t: neg(len(csp.curr_domains[t[1]]))) -def AC3(csp, queue=None, removals=None): + +def AC3(csp, queue=None, removals=None, arc_heuristic=dom_j_up): """[Figure 6.3]""" if queue is None: - queue = [(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]] + queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]} csp.support_pruning() + queue = arc_heuristic(csp, queue) + checks = 0 while queue: (Xi, Xj) = queue.pop() - if revise(csp, Xi, Xj, removals): + revised, checks = revise(csp, Xi, Xj, removals, checks) + if revised: if not csp.curr_domains[Xi]: - return False + return False, checks # CSP is inconsistent for Xk in csp.neighbors[Xi]: - if Xk != Xi: - queue.append((Xk, Xi)) - return True + if Xk != Xj: + queue.add((Xk, Xi)) + return True, checks # CSP is satisfiable -def revise(csp, Xi, Xj, removals): - "Return true if we remove a value." +def revise(csp, Xi, Xj, removals, checks=0): + """Return true if we remove a value.""" revised = False for x in csp.curr_domains[Xi][:]: # If Xi=x conflicts with Xj=y for every possible y, eliminate Xi=x - if all(not csp.constraints(Xi, x, Xj, y) for y in csp.curr_domains[Xj]): + # if all(not csp.constraints(Xi, x, Xj, y) for y in csp.curr_domains[Xj]): + conflict = True + for y in csp.curr_domains[Xj]: + if csp.constraints(Xi, x, Xj, y): + conflict = False + checks += 1 + if not conflict: + break + if conflict: csp.prune(Xi, x, removals) revised = True - return revised + return revised, checks + + +# Constraint Propagation with AC3b: an improved version +# of AC3 with double-support domain-heuristic + +def AC3b(csp, queue=None, removals=None, arc_heuristic=dom_j_up): + if queue is None: + queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]} + csp.support_pruning() + queue = arc_heuristic(csp, queue) + checks = 0 + while queue: + (Xi, Xj) = queue.pop() + # Si_p values are all known to be supported by Xj + # Sj_p values are all known to be supported by Xi + # Dj - Sj_p = Sj_u values are unknown, as yet, to be supported by Xi + Si_p, Sj_p, Sj_u, checks = partition(csp, Xi, Xj, checks) + if not Si_p: + return False, checks # CSP is inconsistent + revised = False + for x in set(csp.curr_domains[Xi]) - Si_p: + csp.prune(Xi, x, removals) + revised = True + if revised: + for Xk in csp.neighbors[Xi]: + if Xk != Xj: + queue.add((Xk, Xi)) + if (Xj, Xi) in queue: + if isinstance(queue, set): + # or queue -= {(Xj, Xi)} or queue.remove((Xj, Xi)) + queue.difference_update({(Xj, Xi)}) + else: + queue.difference_update((Xj, Xi)) + # the elements in D_j which are supported by Xi are given by the union of Sj_p with the set of those + # elements of Sj_u which further processing will show to be supported by some vi_p in Si_p + for vj_p in Sj_u: + for vi_p in Si_p: + conflict = True + if csp.constraints(Xj, vj_p, Xi, vi_p): + conflict = False + Sj_p.add(vj_p) + checks += 1 + if not conflict: + break + revised = False + for x in set(csp.curr_domains[Xj]) - Sj_p: + csp.prune(Xj, x, removals) + revised = True + if revised: + for Xk in csp.neighbors[Xj]: + if Xk != Xi: + queue.add((Xk, Xj)) + return True, checks # CSP is satisfiable + + +def partition(csp, Xi, Xj, checks=0): + Si_p = set() + Sj_p = set() + Sj_u = set(csp.curr_domains[Xj]) + for vi_u in csp.curr_domains[Xi]: + conflict = True + # now, in order to establish support for a value vi_u in Di it seems better to try to find a support among + # the values in Sj_u first, because for each vj_u in Sj_u the check (vi_u, vj_u) is a double-support check + # and it is just as likely that any vj_u in Sj_u supports vi_u than it is that any vj_p in Sj_p does... + for vj_u in Sj_u - Sj_p: + # double-support check + if csp.constraints(Xi, vi_u, Xj, vj_u): + conflict = False + Si_p.add(vi_u) + Sj_p.add(vj_u) + checks += 1 + if not conflict: + break + # ... and only if no support can be found among the elements in Sj_u, should the elements vj_p in Sj_p be used + # for single-support checks (vi_u, vj_p) + if conflict: + for vj_p in Sj_p: + # single-support check + if csp.constraints(Xi, vi_u, Xj, vj_p): + conflict = False + Si_p.add(vi_u) + checks += 1 + if not conflict: + break + return Si_p, Sj_p, Sj_u - Sj_p, checks + + +# Constraint Propagation with AC4 + +def AC4(csp, queue=None, removals=None, arc_heuristic=dom_j_up): + if queue is None: + queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]} + csp.support_pruning() + queue = arc_heuristic(csp, queue) + support_counter = Counter() + variable_value_pairs_supported = defaultdict(set) + unsupported_variable_value_pairs = [] + checks = 0 + # construction and initialization of support sets + while queue: + (Xi, Xj) = queue.pop() + revised = False + for x in csp.curr_domains[Xi][:]: + for y in csp.curr_domains[Xj]: + if csp.constraints(Xi, x, Xj, y): + support_counter[(Xi, x, Xj)] += 1 + variable_value_pairs_supported[(Xj, y)].add((Xi, x)) + checks += 1 + if support_counter[(Xi, x, Xj)] == 0: + csp.prune(Xi, x, removals) + revised = True + unsupported_variable_value_pairs.append((Xi, x)) + if revised: + if not csp.curr_domains[Xi]: + return False, checks # CSP is inconsistent + # propagation of removed values + while unsupported_variable_value_pairs: + Xj, y = unsupported_variable_value_pairs.pop() + for Xi, x in variable_value_pairs_supported[(Xj, y)]: + revised = False + if x in csp.curr_domains[Xi][:]: + support_counter[(Xi, x, Xj)] -= 1 + if support_counter[(Xi, x, Xj)] == 0: + csp.prune(Xi, x, removals) + revised = True + unsupported_variable_value_pairs.append((Xi, x)) + if revised: + if not csp.curr_domains[Xi]: + return False, checks # CSP is inconsistent + return True, checks # CSP is satisfiable + # ______________________________________________________________________________ # CSP Backtracking Search @@ -190,36 +344,35 @@ def revise(csp, Xi, Xj, removals): def first_unassigned_variable(assignment, csp): - "The default variable order." + """The default variable order.""" return first([var for var in csp.variables if var not in assignment]) def mrv(assignment, csp): - "Minimum-remaining-values heuristic." - return argmin_random_tie( - [v for v in csp.variables if v not in assignment], - key=lambda var: num_legal_values(csp, var, assignment)) + """Minimum-remaining-values heuristic.""" + return argmin_random_tie([v for v in csp.variables if v not in assignment], + key=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]) else: - return count(csp.nconflicts(var, val, assignment) == 0 - for val in csp.domains[var]) + return count(csp.nconflicts(var, val, assignment) == 0 for val in csp.domains[var]) + # Value ordering def unordered_domain_values(var, assignment, csp): - "The default value order." + """The default value order.""" return csp.choices(var) def lcv(var, assignment, csp): - "Least-constraining-values heuristic." - return sorted(csp.choices(var), - key=lambda val: csp.nconflicts(var, val, assignment)) + """Least-constraining-values heuristic.""" + return sorted(csp.choices(var), key=lambda val: csp.nconflicts(var, val, assignment)) + # Inference @@ -229,7 +382,8 @@ def no_inference(csp, var, value, assignment, removals): def forward_checking(csp, var, value, assignment, removals): - "Prune neighbor values inconsistent with var=value." + """Prune neighbor values inconsistent with var=value.""" + csp.support_pruning() for B in csp.neighbors[var]: if B not in assignment: for b in csp.curr_domains[B][:]: @@ -240,19 +394,17 @@ def forward_checking(csp, var, value, assignment, removals): return True -def mac(csp, var, value, assignment, removals): - "Maintain arc consistency." - return AC3(csp, [(X, var) for X in csp.neighbors[var]], removals) +def mac(csp, var, value, assignment, removals, constraint_propagation=AC3b): + """Maintain arc consistency.""" + return constraint_propagation(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): - """[Figure 6.5] - """ +def backtracking_search(csp, select_unassigned_variable=first_unassigned_variable, + order_domain_values=unordered_domain_values, inference=no_inference): + """[Figure 6.5]""" def backtrack(assignment): if len(assignment) == len(csp.variables): @@ -274,12 +426,13 @@ def backtrack(assignment): assert result is None or csp.goal_test(result) return result + # ______________________________________________________________________________ -# Min-conflicts hillclimbing search for CSPs +# Min-conflicts Hill Climbing search for CSPs def min_conflicts(csp, max_steps=100000): - """Solve a CSP by stochastic hillclimbing on the number of conflicts.""" + """Solve a CSP by stochastic Hill Climbing on the number of conflicts.""" # Generate a complete assignment for all variables (probably with conflicts) csp.current = current = {} for var in csp.variables: @@ -299,40 +452,106 @@ def min_conflicts(csp, max_steps=100000): 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.""" - return argmin_random_tie(csp.domains[var], - key=lambda val: csp.nconflicts(var, val, current)) + return argmin_random_tie(csp.domains[var], key=lambda val: csp.nconflicts(var, val, current)) + # ______________________________________________________________________________ def tree_csp_solver(csp): - "[Figure 6.11]" + """[Figure 6.11]""" assignment = {} root = csp.variables[0] - X, parent = topological_sort(csp.variables, root) - for Xj in reversed(X): + X, parent = topological_sort(csp, root) + + csp.support_pruning() + for Xj in reversed(X[1:]): if not make_arc_consistent(parent[Xj], Xj, csp): return None - for Xi in X: - if not csp.curr_domains[Xi]: + + assignment[root] = csp.curr_domains[root][0] + for Xi in X[1:]: + assignment[Xi] = assign_value(parent[Xi], Xi, csp, assignment) + if not assignment[Xi]: return None - assignment[Xi] = csp.curr_domains[Xi][0] return assignment -def topological_sort(xs, x): - raise NotImplementedError +def topological_sort(X, root): + """Returns the topological sort of X starting from the root. + + Input: + X is a list with the nodes of the graph + N is the dictionary with the neighbors of each node + root denotes the root of the graph. + + Output: + stack is a list with the nodes topologically sorted + parents is a dictionary pointing to each node's parent + + Other: + visited shows the state (visited - not visited) of nodes + + """ + neighbors = X.neighbors + + visited = defaultdict(lambda: False) + + stack = [] + parents = {} + + build_topological(root, None, neighbors, visited, stack, parents) + return stack, parents + + +def build_topological(node, parent, neighbors, visited, stack, parents): + """Build the topological sort and the parents of each node in the graph.""" + visited[node] = True + + for n in neighbors[node]: + if not visited[n]: + build_topological(n, node, neighbors, visited, stack, parents) + + parents[node] = parent + stack.insert(0, node) def make_arc_consistent(Xj, Xk, csp): - raise NotImplementedError + """Make arc between parent (Xj) and child (Xk) consistent under the csp's constraints, + by removing the possible values of Xj that cause inconsistencies.""" + # csp.curr_domains[Xj] = [] + for val1 in csp.domains[Xj]: + keep = False # Keep or remove val1 + for val2 in csp.domains[Xk]: + if csp.constraints(Xj, val1, Xk, val2): + # Found a consistent assignment for val1, keep it + keep = True + break + + if not keep: + # Remove val1 + csp.prune(Xj, val1, None) + + return csp.curr_domains[Xj] + + +def assign_value(Xj, Xk, csp, assignment): + """Assign a value to Xk given Xj's (Xk's parent) assignment. + Return the first value that satisfies the constraints.""" + parent_assignment = assignment[Xj] + for val in csp.curr_domains[Xk]: + if csp.constraints(Xj, parent_assignment, Xk, val): + return val + + # No consistent assignment available + return None + # ______________________________________________________________________________ -# Map-Coloring Problems +# Map Coloring CSP 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 variables have the same domain. >>> d = UniversalDict(42) @@ -344,30 +563,29 @@ def __init__(self, value): self.value = value def __getitem__(self, key): return self.value - def __repr__(self): return '{Any: %r}' % self.value + def __repr__(self): return '{{Any: {0!r}}}'.format(self.value) def different_values_constraint(A, a, B, b): - "A constraint saying two neighboring variables must differ in value." + """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 - dict of {region: [neighbor,...]} entries. This dict may also be + for any two adjacent regions. Arguments are a list of colors, and a + dict of {region: [neighbor,...]} entries. This dict may also be specified as a string of the form defined by parse_neighbors.""" if isinstance(neighbors, str): neighbors = parse_neighbors(neighbors) - return CSP(list(neighbors.keys()), UniversalDict(colors), neighbors, - different_values_constraint) + return CSP(list(neighbors.keys()), UniversalDict(colors), neighbors, different_values_constraint) -def parse_neighbors(neighbors, variables=[]): +def parse_neighbors(neighbors): """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 ':' + regions to neighbors. The syntax is a region name followed by a ':' followed by zero or more region names, followed by ';', repeated for - each region name. If you say 'X: Y' you don't need 'Y: X'. + each region name. If you say 'X: Y' you don't need 'Y: X'. >>> parse_neighbors('X: Y Z; Y: Z') == {'Y': ['X', 'Z'], 'X': ['Y', 'Z'], 'Z': ['X', 'Y']} True """ @@ -380,27 +598,28 @@ def parse_neighbors(neighbors, variables=[]): dic[B].append(A) return dic -australia = MapColoringCSP(list('RGB'), - '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; - 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; - LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL; - MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL; - PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ; - NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH; - 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 - 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: - PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA: - AU BO FC PA LR""") + +australia_csp = MapColoringCSP(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """) + +usa_csp = MapColoringCSP(list('RGBY'), + """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 AZ; + 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; + LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL; + MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL; + PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ; + NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH; + HI: ; AK: """) + +france_csp = MapColoringCSP(list('RGBY'), + """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: + PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA: + AU BO FC PA LR""") + # ______________________________________________________________________________ # n-Queens Problem @@ -413,13 +632,13 @@ def queen_constraint(A, a, B, b): class NQueensCSP(CSP): - - """Make a CSP for the nQueens problem for search with min_conflicts. + """ + 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. That means position (x, y) represents (var, val) in the CSP. The main structures are three arrays to count queens that could conflict: - rows[i] Number of queens in the ith row (i.e val == i) + rows[i] Number of queens in the ith row (i.e. val == i) downs[i] Number of queens in the \ diagonal such that their (x, y) coordinates sum to i ups[i] Number of queens in the / diagonal @@ -438,44 +657,44 @@ def __init__(self, n): CSP.__init__(self, list(range(n)), UniversalDict(list(range(n))), UniversalDict(list(range(n))), queen_constraint) - self.rows = [0]*n - self.ups = [0]*(2*n - 1) - self.downs = [0]*(2*n - 1) + self.rows = [0] * n + self.ups = [0] * (2 * n - 1) + self.downs = [0] * (2 * n - 1) def nconflicts(self, var, val, assignment): """The number of conflicts, as recorded with each assignment. Count conflicts in row and in up, down diagonals. If there is a queen there, it can't conflict with itself, so subtract 3.""" n = len(self.variables) - c = self.rows[val] + self.downs[var+val] + self.ups[var-val+n-1] + c = self.rows[val] + self.downs[var + val] + self.ups[var - val + n - 1] if assignment.get(var, None) == val: c -= 3 return c 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 - self.record_conflict(assignment, var, oldval, -1) + """Assign var, and keep track of conflicts.""" + old_val = assignment.get(var, None) + if val != old_val: + if old_val is not None: # Remove old val if there was one + self.record_conflict(assignment, var, old_val, -1) self.record_conflict(assignment, var, val, +1) CSP.assign(self, var, val, assignment) def unassign(self, var, assignment): - "Remove var from assignment (if it is there) and track conflicts." + """Remove var from assignment (if it is there) and track conflicts.""" if var in assignment: self.record_conflict(assignment, var, assignment[var], -1) CSP.unassign(self, var, assignment) def record_conflict(self, assignment, var, val, delta): - "Record conflicts caused by addition or deletion of a Queen." + """Record conflicts caused by addition or deletion of a Queen.""" n = len(self.variables) self.rows[val] += delta self.downs[var + val] += delta self.ups[var - val + n - 1] += delta def display(self, assignment): - "Print the queens and the nconflicts values (for debugging)." + """Print the queens and the nconflicts values (for debugging).""" n = len(self.variables) for val in range(n): for var in range(n): @@ -495,14 +714,17 @@ def display(self, assignment): print(str(self.nconflicts(var, val, assignment)) + ch, end=' ') print() + # ______________________________________________________________________________ # Sudoku -def flatten(seqs): return sum(seqs, []) +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..' # noqa -harder1 = '4173698.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......' # noqa + +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__ @@ -514,12 +736,12 @@ def flatten(seqs): return sum(seqs, []) _NEIGHBORS = {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])) + _NEIGHBORS[v].update(unit - {v}) class Sudoku(CSP): - - """A Sudoku problem. + """ + 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 different; the same for each row and column as a 9x9 grid. @@ -536,8 +758,9 @@ class Sudoku(CSP): . . 2 | 6 . 9 | 5 . . 8 . . | 2 . 3 | . . 9 . . 5 | . 1 . | 3 . . - >>> AC3(e); e.display(e.infer_assignment()) - True + >>> AC3(e) # doctest: +ELLIPSIS + (True, ...) + >>> e.display(e.infer_assignment()) 4 8 3 | 9 2 1 | 6 5 7 9 6 7 | 3 4 5 | 8 2 1 2 5 1 | 8 7 6 | 4 9 3 @@ -553,6 +776,7 @@ class Sudoku(CSP): >>> backtracking_search(h, select_unassigned_variable=mrv, inference=forward_checking) is not None True """ + R3 = _R3 Cell = _CELL bgrid = _BGRID @@ -579,15 +803,18 @@ def show_cell(cell): return str(assignment.get(cell, '.')) def abut(lines1, lines2): return list( map(' | '.join, list(zip(lines1, lines2)))) + print('\n------+-------+------\n'.join( '\n'.join(reduce( abut, map(show_box, brow))) for brow in self.bgrid)) + + # ______________________________________________________________________________ # The Zebra Puzzle def Zebra(): - "Return an instance of the Zebra Puzzle." + """Return an instance of the Zebra Puzzle.""" Colors = 'Red Yellow Blue Green Ivory'.split() Pets = 'Dog Fox Snails Horse Zebra'.split() Drinks = 'OJ Tea Coffee Milk Water'.split() @@ -603,7 +830,7 @@ def Zebra(): Spaniard: Dog; Kools: Yellow; Chesterfields: Fox; Norwegian: Blue; Winston: Snails; LuckyStrike: OJ; Ukranian: Tea; Japanese: Parliaments; Kools: Horse; - Coffee: Green; Green: Ivory""", variables) + Coffee: Green; Green: Ivory""") for type in [Colors, Pets, Drinks, Countries, Smokes]: for A in type: for B in type: @@ -649,6 +876,7 @@ def zebra_constraint(A, a, B, b, recurse=0): (A in Smokes and B in Smokes)): return not same raise Exception('error') + return CSP(variables, domains, neighbors, zebra_constraint) @@ -662,3 +890,546 @@ def solve_zebra(algorithm=min_conflicts, **args): print(var, end=' ') print() return ans['Zebra'], ans['Water'], z.nassigns, ans + + +# ______________________________________________________________________________ +# n-ary Constraint Satisfaction Problem + +class NaryCSP: + """ + A nary-CSP consists of: + domains : a dictionary that maps each variable to its domain + constraints : a list of constraints + variables : a set of variables + var_to_const: a variable to set of constraints dictionary + """ + + def __init__(self, domains, constraints): + """Domains is a variable:domain dictionary + constraints is a list of constraints + """ + self.variables = set(domains) + self.domains = domains + self.constraints = constraints + self.var_to_const = {var: set() for var in self.variables} + for con in constraints: + for var in con.scope: + self.var_to_const[var].add(con) + + def __str__(self): + """String representation of CSP""" + return str(self.domains) + + def display(self, assignment=None): + """More detailed string representation of CSP""" + if assignment is None: + assignment = {} + print(assignment) + + def consistent(self, assignment): + """assignment is a variable:value dictionary + returns True if all of the constraints that can be evaluated + evaluate to True given assignment. + """ + return all(con.holds(assignment) + for con in self.constraints + if all(v in assignment for v in con.scope)) + + +class Constraint: + """ + A Constraint consists of: + scope : a tuple of variables + condition: a function that can applied to a tuple of values + for the variables. + """ + + def __init__(self, scope, condition): + self.scope = scope + self.condition = condition + + def __repr__(self): + return self.condition.__name__ + str(self.scope) + + def holds(self, assignment): + """Returns the value of Constraint con evaluated in assignment. + + precondition: all variables are assigned in assignment + """ + return self.condition(*tuple(assignment[v] for v in self.scope)) + + +def all_diff_constraint(*values): + """Returns True if all values are different, False otherwise""" + return len(values) is len(set(values)) + + +def is_word_constraint(words): + """Returns True if the letters concatenated form a word in words, False otherwise""" + + def isw(*letters): + return "".join(letters) in words + + return isw + + +def meet_at_constraint(p1, p2): + """Returns a function that is True when the words meet at the positions (p1, p2), False otherwise""" + + def meets(w1, w2): + return w1[p1] == w2[p2] + + meets.__name__ = "meet_at(" + str(p1) + ',' + str(p2) + ')' + return meets + + +def adjacent_constraint(x, y): + """Returns True if x and y are adjacent numbers, False otherwise""" + return abs(x - y) == 1 + + +def sum_constraint(n): + """Returns a function that is True when the the sum of all values is n, False otherwise""" + + def sumv(*values): + return sum(values) is n + + sumv.__name__ = str(n) + "==sum" + return sumv + + +def is_constraint(val): + """Returns a function that is True when x is equal to val, False otherwise""" + + def isv(x): + return val == x + + isv.__name__ = str(val) + "==" + return isv + + +def ne_constraint(val): + """Returns a function that is True when x is not equal to val, False otherwise""" + + def nev(x): + return val != x + + nev.__name__ = str(val) + "!=" + return nev + + +def no_heuristic(to_do): + return to_do + + +def sat_up(to_do): + return SortedSet(to_do, key=lambda t: 1 / len([var for var in t[1].scope])) + + +class ACSolver: + """Solves a CSP with arc consistency and domain splitting""" + + def __init__(self, csp): + """a CSP solver that uses arc consistency + * csp is the CSP to be solved + """ + self.csp = csp + + def GAC(self, orig_domains=None, to_do=None, arc_heuristic=sat_up): + """ + Makes this CSP arc-consistent using Generalized Arc Consistency + orig_domains: is the original domains + to_do : is a set of (variable,constraint) pairs + returns the reduced domains (an arc-consistent variable:domain dictionary) + """ + if orig_domains is None: + orig_domains = self.csp.domains + if to_do is None: + to_do = {(var, const) for const in self.csp.constraints for var in const.scope} + else: + to_do = to_do.copy() + domains = orig_domains.copy() + to_do = arc_heuristic(to_do) + checks = 0 + while to_do: + var, const = to_do.pop() + other_vars = [ov for ov in const.scope if ov != var] + new_domain = set() + if len(other_vars) == 0: + for val in domains[var]: + if const.holds({var: val}): + new_domain.add(val) + checks += 1 + # new_domain = {val for val in domains[var] + # if const.holds({var: val})} + elif len(other_vars) == 1: + other = other_vars[0] + for val in domains[var]: + for other_val in domains[other]: + checks += 1 + if const.holds({var: val, other: other_val}): + new_domain.add(val) + break + # new_domain = {val for val in domains[var] + # if any(const.holds({var: val, other: other_val}) + # for other_val in domains[other])} + else: # general case + for val in domains[var]: + holds, checks = self.any_holds(domains, const, {var: val}, other_vars, checks=checks) + if holds: + new_domain.add(val) + # new_domain = {val for val in domains[var] + # if self.any_holds(domains, const, {var: val}, other_vars)} + if new_domain != domains[var]: + domains[var] = new_domain + if not new_domain: + return False, domains, checks + add_to_do = self.new_to_do(var, const).difference(to_do) + to_do |= add_to_do + return True, domains, checks + + def new_to_do(self, var, const): + """ + Returns new elements to be added to to_do after assigning + variable var in constraint const. + """ + return {(nvar, nconst) for nconst in self.csp.var_to_const[var] + if nconst != const + for nvar in nconst.scope + if nvar != var} + + def any_holds(self, domains, const, env, other_vars, ind=0, checks=0): + """ + Returns True if Constraint const holds for an assignment + that extends env with the variables in other_vars[ind:] + env is a dictionary + Warning: this has side effects and changes the elements of env + """ + if ind == len(other_vars): + return const.holds(env), checks + 1 + else: + var = other_vars[ind] + for val in domains[var]: + # env = dict_union(env, {var:val}) # no side effects + env[var] = val + holds, checks = self.any_holds(domains, const, env, other_vars, ind + 1, checks) + if holds: + return True, checks + return False, checks + + def domain_splitting(self, domains=None, to_do=None, arc_heuristic=sat_up): + """ + Return a solution to the current CSP or False if there are no solutions + to_do is the list of arcs to check + """ + if domains is None: + domains = self.csp.domains + consistency, new_domains, _ = self.GAC(domains, to_do, arc_heuristic) + if not consistency: + return False + elif all(len(new_domains[var]) == 1 for var in domains): + return {var: first(new_domains[var]) for var in domains} + else: + var = first(x for x in self.csp.variables if len(new_domains[x]) > 1) + if var: + dom1, dom2 = partition_domain(new_domains[var]) + new_doms1 = extend(new_domains, var, dom1) + new_doms2 = extend(new_domains, var, dom2) + to_do = self.new_to_do(var, None) + return self.domain_splitting(new_doms1, to_do, arc_heuristic) or \ + self.domain_splitting(new_doms2, to_do, arc_heuristic) + + +def partition_domain(dom): + """Partitions domain dom into two""" + split = len(dom) // 2 + dom1 = set(list(dom)[:split]) + dom2 = dom - dom1 + return dom1, dom2 + + +class ACSearchSolver(search.Problem): + """A search problem with arc consistency and domain splitting + A node is a CSP""" + + def __init__(self, csp, arc_heuristic=sat_up): + self.cons = ACSolver(csp) + consistency, self.domains, _ = self.cons.GAC(arc_heuristic=arc_heuristic) + if not consistency: + raise Exception('CSP is inconsistent') + self.heuristic = arc_heuristic + super().__init__(self.domains) + + def goal_test(self, node): + """Node is a goal if all domains have 1 element""" + return all(len(node[var]) == 1 for var in node) + + def actions(self, state): + var = first(x for x in state if len(state[x]) > 1) + neighs = [] + if var: + dom1, dom2 = partition_domain(state[var]) + to_do = self.cons.new_to_do(var, None) + for dom in [dom1, dom2]: + new_domains = extend(state, var, dom) + consistency, cons_doms, _ = self.cons.GAC(new_domains, to_do, self.heuristic) + if consistency: + neighs.append(cons_doms) + return neighs + + def result(self, state, action): + return action + + +def ac_solver(csp, arc_heuristic=sat_up): + """Arc consistency (domain splitting interface)""" + return ACSolver(csp).domain_splitting(arc_heuristic=arc_heuristic) + + +def ac_search_solver(csp, arc_heuristic=sat_up): + """Arc consistency (search interface)""" + from search import depth_first_tree_search + solution = None + try: + solution = depth_first_tree_search(ACSearchSolver(csp, arc_heuristic=arc_heuristic)).state + except: + return solution + if solution: + return {var: first(solution[var]) for var in solution} + + +# ______________________________________________________________________________ +# Crossword Problem + + +csp_crossword = NaryCSP({'one_across': {'ant', 'big', 'bus', 'car', 'has'}, + 'one_down': {'book', 'buys', 'hold', 'lane', 'year'}, + 'two_down': {'ginger', 'search', 'symbol', 'syntax'}, + 'three_across': {'book', 'buys', 'hold', 'land', 'year'}, + 'four_across': {'ant', 'big', 'bus', 'car', 'has'}}, + [Constraint(('one_across', 'one_down'), meet_at_constraint(0, 0)), + Constraint(('one_across', 'two_down'), meet_at_constraint(2, 0)), + Constraint(('three_across', 'two_down'), meet_at_constraint(2, 2)), + Constraint(('three_across', 'one_down'), meet_at_constraint(0, 2)), + Constraint(('four_across', 'two_down'), meet_at_constraint(0, 4))]) + +crossword1 = [['_', '_', '_', '*', '*'], + ['_', '*', '_', '*', '*'], + ['_', '_', '_', '_', '*'], + ['_', '*', '_', '*', '*'], + ['*', '*', '_', '_', '_'], + ['*', '*', '_', '*', '*']] + +words1 = {'ant', 'big', 'bus', 'car', 'has', 'book', 'buys', 'hold', + 'lane', 'year', 'ginger', 'search', 'symbol', 'syntax'} + + +class Crossword(NaryCSP): + + def __init__(self, puzzle, words): + domains = {} + constraints = [] + for i, line in enumerate(puzzle): + scope = [] + for j, element in enumerate(line): + if element == '_': + var = "p" + str(j) + str(i) + domains[var] = list(string.ascii_lowercase) + scope.append(var) + else: + if len(scope) > 1: + constraints.append(Constraint(tuple(scope), is_word_constraint(words))) + scope.clear() + if len(scope) > 1: + constraints.append(Constraint(tuple(scope), is_word_constraint(words))) + puzzle_t = list(map(list, zip(*puzzle))) + for i, line in enumerate(puzzle_t): + scope = [] + for j, element in enumerate(line): + if element == '_': + scope.append("p" + str(i) + str(j)) + else: + if len(scope) > 1: + constraints.append(Constraint(tuple(scope), is_word_constraint(words))) + scope.clear() + if len(scope) > 1: + constraints.append(Constraint(tuple(scope), is_word_constraint(words))) + super().__init__(domains, constraints) + self.puzzle = puzzle + + def display(self, assignment=None): + for i, line in enumerate(self.puzzle): + puzzle = "" + for j, element in enumerate(line): + if element == '*': + puzzle += "[*] " + else: + var = "p" + str(j) + str(i) + if assignment is not None: + if isinstance(assignment[var], set) and len(assignment[var]) == 1: + puzzle += "[" + str(first(assignment[var])).upper() + "] " + elif isinstance(assignment[var], str): + puzzle += "[" + str(assignment[var]).upper() + "] " + else: + puzzle += "[_] " + else: + puzzle += "[_] " + print(puzzle) + + +# ______________________________________________________________________________ +# Kakuro Problem + + +# difficulty 0 +kakuro1 = [['*', '*', '*', [6, ''], [3, '']], + ['*', [4, ''], [3, 3], '_', '_'], + [['', 10], '_', '_', '_', '_'], + [['', 3], '_', '_', '*', '*']] + +# difficulty 0 +kakuro2 = [ + ['*', [10, ''], [13, ''], '*'], + [['', 3], '_', '_', [13, '']], + [['', 12], '_', '_', '_'], + [['', 21], '_', '_', '_']] + +# difficulty 1 +kakuro3 = [ + ['*', [17, ''], [28, ''], '*', [42, ''], [22, '']], + [['', 9], '_', '_', [31, 14], '_', '_'], + [['', 20], '_', '_', '_', '_', '_'], + ['*', ['', 30], '_', '_', '_', '_'], + ['*', [22, 24], '_', '_', '_', '*'], + [['', 25], '_', '_', '_', '_', [11, '']], + [['', 20], '_', '_', '_', '_', '_'], + [['', 14], '_', '_', ['', 17], '_', '_']] + +# difficulty 2 +kakuro4 = [ + ['*', '*', '*', '*', '*', [4, ''], [24, ''], [11, ''], '*', '*', '*', [11, ''], [17, ''], '*', '*'], + ['*', '*', '*', [17, ''], [11, 12], '_', '_', '_', '*', '*', [24, 10], '_', '_', [11, ''], '*'], + ['*', [4, ''], [16, 26], '_', '_', '_', '_', '_', '*', ['', 20], '_', '_', '_', '_', [16, '']], + [['', 20], '_', '_', '_', '_', [24, 13], '_', '_', [16, ''], ['', 12], '_', '_', [23, 10], '_', '_'], + [['', 10], '_', '_', [24, 12], '_', '_', [16, 5], '_', '_', [16, 30], '_', '_', '_', '_', '_'], + ['*', '*', [3, 26], '_', '_', '_', '_', ['', 12], '_', '_', [4, ''], [16, 14], '_', '_', '*'], + ['*', ['', 8], '_', '_', ['', 15], '_', '_', [34, 26], '_', '_', '_', '_', '_', '*', '*'], + ['*', ['', 11], '_', '_', [3, ''], [17, ''], ['', 14], '_', '_', ['', 8], '_', '_', [7, ''], [17, ''], '*'], + ['*', '*', '*', [23, 10], '_', '_', [3, 9], '_', '_', [4, ''], [23, ''], ['', 13], '_', '_', '*'], + ['*', '*', [10, 26], '_', '_', '_', '_', '_', ['', 7], '_', '_', [30, 9], '_', '_', '*'], + ['*', [17, 11], '_', '_', [11, ''], [24, 8], '_', '_', [11, 21], '_', '_', '_', '_', [16, ''], [17, '']], + [['', 29], '_', '_', '_', '_', '_', ['', 7], '_', '_', [23, 14], '_', '_', [3, 17], '_', '_'], + [['', 10], '_', '_', [3, 10], '_', '_', '*', ['', 8], '_', '_', [4, 25], '_', '_', '_', '_'], + ['*', ['', 16], '_', '_', '_', '_', '*', ['', 23], '_', '_', '_', '_', '_', '*', '*'], + ['*', '*', ['', 6], '_', '_', '*', '*', ['', 15], '_', '_', '_', '*', '*', '*', '*']] + + +class Kakuro(NaryCSP): + + def __init__(self, puzzle): + variables = [] + for i, line in enumerate(puzzle): + # print line + for j, element in enumerate(line): + if element == '_': + var1 = str(i) + if len(var1) == 1: + var1 = "0" + var1 + var2 = str(j) + if len(var2) == 1: + var2 = "0" + var2 + variables.append("X" + var1 + var2) + domains = {} + for var in variables: + domains[var] = set(range(1, 10)) + constraints = [] + for i, line in enumerate(puzzle): + for j, element in enumerate(line): + if element != '_' and element != '*': + # down - column + if element[0] != '': + x = [] + for k in range(i + 1, len(puzzle)): + if puzzle[k][j] != '_': + break + var1 = str(k) + if len(var1) == 1: + var1 = "0" + var1 + var2 = str(j) + if len(var2) == 1: + var2 = "0" + var2 + x.append("X" + var1 + var2) + constraints.append(Constraint(x, sum_constraint(element[0]))) + constraints.append(Constraint(x, all_diff_constraint)) + # right - line + if element[1] != '': + x = [] + for k in range(j + 1, len(puzzle[i])): + if puzzle[i][k] != '_': + break + var1 = str(i) + if len(var1) == 1: + var1 = "0" + var1 + var2 = str(k) + if len(var2) == 1: + var2 = "0" + var2 + x.append("X" + var1 + var2) + constraints.append(Constraint(x, sum_constraint(element[1]))) + constraints.append(Constraint(x, all_diff_constraint)) + super().__init__(domains, constraints) + self.puzzle = puzzle + + def display(self, assignment=None): + for i, line in enumerate(self.puzzle): + puzzle = "" + for j, element in enumerate(line): + if element == '*': + puzzle += "[*]\t" + elif element == '_': + var1 = str(i) + if len(var1) == 1: + var1 = "0" + var1 + var2 = str(j) + if len(var2) == 1: + var2 = "0" + var2 + var = "X" + var1 + var2 + if assignment is not None: + if isinstance(assignment[var], set) and len(assignment[var]) == 1: + puzzle += "[" + str(first(assignment[var])) + "]\t" + elif isinstance(assignment[var], int): + puzzle += "[" + str(assignment[var]) + "]\t" + else: + puzzle += "[_]\t" + else: + puzzle += "[_]\t" + else: + puzzle += str(element[0]) + "\\" + str(element[1]) + "\t" + print(puzzle) + + +# ______________________________________________________________________________ +# Cryptarithmetic Problem + +# [Figure 6.2] +# T W O + T W O = F O U R +two_two_four = NaryCSP({'T': set(range(1, 10)), 'F': set(range(1, 10)), + 'W': set(range(0, 10)), 'O': set(range(0, 10)), 'U': set(range(0, 10)), 'R': set(range(0, 10)), + 'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2))}, + [Constraint(('T', 'F', 'W', 'O', 'U', 'R'), all_diff_constraint), + Constraint(('O', 'R', 'C1'), lambda o, r, c1: o + o == r + 10 * c1), + Constraint(('W', 'U', 'C1', 'C2'), lambda w, u, c1, c2: c1 + w + w == u + 10 * c2), + Constraint(('T', 'O', 'C2', 'C3'), lambda t, o, c2, c3: c2 + t + t == o + 10 * c3), + Constraint(('F', 'C3'), eq)]) + +# S E N D + M O R E = M O N E Y +send_more_money = NaryCSP({'S': set(range(1, 10)), 'M': set(range(1, 10)), + 'E': set(range(0, 10)), 'N': set(range(0, 10)), 'D': set(range(0, 10)), + 'O': set(range(0, 10)), 'R': set(range(0, 10)), 'Y': set(range(0, 10)), + 'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2)), + 'C4': set(range(0, 2))}, + [Constraint(('S', 'E', 'N', 'D', 'M', 'O', 'R', 'Y'), all_diff_constraint), + Constraint(('D', 'E', 'Y', 'C1'), lambda d, e, y, c1: d + e == y + 10 * c1), + Constraint(('N', 'R', 'E', 'C1', 'C2'), lambda n, r, e, c1, c2: c1 + n + r == e + 10 * c2), + Constraint(('E', 'O', 'N', 'C2', 'C3'), lambda e, o, n, c2, c3: c2 + e + o == n + 10 * c3), + Constraint(('S', 'M', 'O', 'C3', 'C4'), lambda s, m, o, c3, c4: c3 + s + m == o + 10 * c4), + Constraint(('M', 'C4'), eq)]) diff --git a/deep_learning4e.py b/deep_learning4e.py new file mode 100644 index 000000000..9f5b0a8f7 --- /dev/null +++ b/deep_learning4e.py @@ -0,0 +1,584 @@ +"""Deep learning. (Chapters 20)""" + +import random +import statistics + +import numpy as np +from keras import Sequential, optimizers +from keras.layers import Embedding, SimpleRNN, Dense +from keras.preprocessing import sequence + +from utils4e import (conv1D, gaussian_kernel, element_wise_product, vector_add, random_weights, + scalar_vector_product, map_vector, mean_squared_error_loss) + + +class Node: + """ + A single unit of a layer in a neural network + :param weights: weights between parent nodes and current node + :param value: value of current node + """ + + def __init__(self, weights=None, value=None): + self.value = value + self.weights = weights or [] + + +class Layer: + """ + A layer in a neural network based on a computational graph. + :param size: number of units in the current layer + """ + + def __init__(self, size): + self.nodes = np.array([Node() for _ in range(size)]) + + def forward(self, inputs): + """Define the operation to get the output of this layer""" + raise NotImplementedError + + +class Activation: + + def function(self, x): + return NotImplementedError + + def derivative(self, x): + return NotImplementedError + + def __call__(self, x): + return self.function(x) + + +class Sigmoid(Activation): + + def function(self, x): + return 1 / (1 + np.exp(-x)) + + def derivative(self, value): + return value * (1 - value) + + +class ReLU(Activation): + + def function(self, x): + return max(0, x) + + def derivative(self, value): + return 1 if value > 0 else 0 + + +class ELU(Activation): + + def __init__(self, alpha=0.01): + self.alpha = alpha + + def function(self, x): + return x if x > 0 else self.alpha * (np.exp(x) - 1) + + def derivative(self, value): + return 1 if value > 0 else self.alpha * np.exp(value) + + +class LeakyReLU(Activation): + + def __init__(self, alpha=0.01): + self.alpha = alpha + + def function(self, x): + return max(x, self.alpha * x) + + def derivative(self, value): + return 1 if value > 0 else self.alpha + + +class Tanh(Activation): + + def function(self, x): + return np.tanh(x) + + def derivative(self, value): + return 1 - (value ** 2) + + +class SoftMax(Activation): + + def function(self, x): + return np.exp(x) / np.sum(np.exp(x)) + + def derivative(self, x): + return np.ones_like(x) + + +class SoftPlus(Activation): + + def function(self, x): + return np.log(1. + np.exp(x)) + + def derivative(self, x): + return 1. / (1. + np.exp(-x)) + + +class Linear(Activation): + + def function(self, x): + return x + + def derivative(self, x): + return np.ones_like(x) + + +class InputLayer(Layer): + """1D input layer. Layer size is the same as input vector size.""" + + def __init__(self, size=3): + super().__init__(size) + + def forward(self, inputs): + """Take each value of the inputs to each unit in the layer.""" + assert len(self.nodes) == len(inputs) + for node, inp in zip(self.nodes, inputs): + node.value = inp + return inputs + + +class OutputLayer(Layer): + """1D softmax output layer in 19.3.2.""" + + def __init__(self, size=3): + super().__init__(size) + + def forward(self, inputs, activation=SoftMax): + assert len(self.nodes) == len(inputs) + res = activation().function(inputs) + for node, val in zip(self.nodes, res): + node.value = val + return res + + +class DenseLayer(Layer): + """ + 1D dense layer in a neural network. + :param in_size: (int) input vector size + :param out_size: (int) output vector size + :param activation: (Activation object) activation function + """ + + def __init__(self, in_size=3, out_size=3, activation=Sigmoid): + super().__init__(out_size) + self.out_size = out_size + self.inputs = None + self.activation = activation() + # initialize weights + for node in self.nodes: + node.weights = random_weights(-0.5, 0.5, in_size) + + def forward(self, inputs): + self.inputs = inputs + res = [] + # get the output value of each unit + for unit in self.nodes: + val = self.activation.function(np.dot(unit.weights, inputs)) + unit.value = val + res.append(val) + return res + + +class ConvLayer1D(Layer): + """ + 1D convolution layer of in neural network. + :param kernel_size: convolution kernel size + """ + + def __init__(self, size=3, kernel_size=3): + super().__init__(size) + # init convolution kernel as gaussian kernel + for node in self.nodes: + node.weights = gaussian_kernel(kernel_size) + + def forward(self, features): + # each node in layer takes a channel in the features + assert len(self.nodes) == len(features) + res = [] + # compute the convolution output of each channel, store it in node.val + for node, feature in zip(self.nodes, features): + out = conv1D(feature, node.weights) + res.append(out) + node.value = out + return res + + +class MaxPoolingLayer1D(Layer): + """ + 1D max pooling layer in a neural network. + :param kernel_size: max pooling area size + """ + + def __init__(self, size=3, kernel_size=3): + super().__init__(size) + self.kernel_size = kernel_size + self.inputs = None + + def forward(self, features): + assert len(self.nodes) == len(features) + res = [] + self.inputs = features + # do max pooling for each channel in features + for i in range(len(self.nodes)): + feature = features[i] + # get the max value in a kernel_size * kernel_size area + out = [max(feature[i:i + self.kernel_size]) + for i in range(len(feature) - self.kernel_size + 1)] + res.append(out) + self.nodes[i].value = out + return res + + +class BatchNormalizationLayer(Layer): + """Batch normalization layer.""" + + def __init__(self, size, eps=0.001): + super().__init__(size) + self.eps = eps + # self.weights = [beta, gamma] + self.weights = [0, 0] + self.inputs = None + + def forward(self, inputs): + # mean value of inputs + mu = sum(inputs) / len(inputs) + # standard error of inputs + stderr = statistics.stdev(inputs) + self.inputs = inputs + res = [] + # get normalized value of each input + for i in range(len(self.nodes)): + val = [(inputs[i] - mu) * self.weights[0] / np.sqrt(self.eps + stderr ** 2) + self.weights[1]] + res.append(val) + self.nodes[i].value = val + return res + + +def init_examples(examples, idx_i, idx_t, o_units): + """Init examples from dataset.examples.""" + + inputs, targets = {}, {} + for i, e in enumerate(examples): + # input values of e + inputs[i] = [e[i] for i in idx_i] + + if o_units > 1: + # one-hot representation of e's target + t = [0 for i in range(o_units)] + t[e[idx_t]] = 1 + targets[i] = t + else: + # target value of e + targets[i] = [e[idx_t]] + + return inputs, targets + + +def stochastic_gradient_descent(dataset, net, loss, epochs=1000, l_rate=0.01, batch_size=1, verbose=False): + """ + Gradient descent algorithm to update the learnable parameters of a network. + :return: the updated network + """ + examples = dataset.examples # init data + + for e in range(epochs): + total_loss = 0 + random.shuffle(examples) + weights = [[node.weights for node in layer.nodes] for layer in net] + + for batch in get_batch(examples, batch_size): + inputs, targets = init_examples(batch, dataset.inputs, dataset.target, len(net[-1].nodes)) + # compute gradients of weights + gs, batch_loss = BackPropagation(inputs, targets, weights, net, loss) + # update weights with gradient descent + weights = [x + y for x, y in zip(weights, [np.array(tg) * -l_rate for tg in gs])] + total_loss += batch_loss + + # update the weights of network each batch + for i in range(len(net)): + if weights[i].size != 0: + for j in range(len(weights[i])): + net[i].nodes[j].weights = weights[i][j] + + if verbose: + print("epoch:{}, total_loss:{}".format(e + 1, total_loss)) + + return net + + +def adam(dataset, net, loss, epochs=1000, rho=(0.9, 0.999), delta=1 / 10 ** 8, + l_rate=0.001, batch_size=1, verbose=False): + """ + [Figure 19.6] + Adam optimizer to update the learnable parameters of a network. + Required parameters are similar to gradient descent. + :return the updated network + """ + examples = dataset.examples + + # init s,r and t + s = [[[0] * len(node.weights) for node in layer.nodes] for layer in net] + r = [[[0] * len(node.weights) for node in layer.nodes] for layer in net] + t = 0 + + # repeat util converge + for e in range(epochs): + # total loss of each epoch + total_loss = 0 + random.shuffle(examples) + weights = [[node.weights for node in layer.nodes] for layer in net] + + for batch in get_batch(examples, batch_size): + t += 1 + inputs, targets = init_examples(batch, dataset.inputs, dataset.target, len(net[-1].nodes)) + + # compute gradients of weights + gs, batch_loss = BackPropagation(inputs, targets, weights, net, loss) + + # update s,r,s_hat and r_gat + s = vector_add(scalar_vector_product(rho[0], s), + scalar_vector_product((1 - rho[0]), gs)) + r = vector_add(scalar_vector_product(rho[1], r), + scalar_vector_product((1 - rho[1]), element_wise_product(gs, gs))) + s_hat = scalar_vector_product(1 / (1 - rho[0] ** t), s) + r_hat = scalar_vector_product(1 / (1 - rho[1] ** t), r) + + # rescale r_hat + r_hat = map_vector(lambda x: 1 / (np.sqrt(x) + delta), r_hat) + + # delta weights + delta_theta = scalar_vector_product(-l_rate, element_wise_product(s_hat, r_hat)) + weights = vector_add(weights, delta_theta) + total_loss += batch_loss + + # update the weights of network each batch + for i in range(len(net)): + if weights[i]: + for j in range(len(weights[i])): + net[i].nodes[j].weights = weights[i][j] + + if verbose: + print("epoch:{}, total_loss:{}".format(e + 1, total_loss)) + + return net + + +def BackPropagation(inputs, targets, theta, net, loss): + """ + The back-propagation algorithm for multilayer networks in only one epoch, to calculate gradients of theta. + :param inputs: a batch of inputs in an array. Each input is an iterable object + :param targets: a batch of targets in an array. Each target is an iterable object + :param theta: parameters to be updated + :param net: a list of predefined layer objects representing their linear sequence + :param loss: a predefined loss function taking array of inputs and targets + :return: gradients of theta, loss of the input batch + """ + + assert len(inputs) == len(targets) + o_units = len(net[-1].nodes) + n_layers = len(net) + batch_size = len(inputs) + + gradients = [[[] for _ in layer.nodes] for layer in net] + total_gradients = [[[0] * len(node.weights) for node in layer.nodes] for layer in net] + + batch_loss = 0 + + # iterate over each example in batch + for e in range(batch_size): + i_val = inputs[e] + t_val = targets[e] + + # forward pass and compute batch loss + for i in range(1, n_layers): + layer_out = net[i].forward(i_val) + i_val = layer_out + batch_loss += loss(t_val, layer_out) + + # initialize delta + delta = [[] for _ in range(n_layers)] + + previous = np.array([layer_out[i] - t_val[i] for i in range(o_units)]) + h_layers = n_layers - 1 + + # backward pass + for i in range(h_layers, 0, -1): + layer = net[i] + derivative = np.array([layer.activation.derivative(node.value) for node in layer.nodes]) + delta[i] = previous * derivative + # pass to layer i-1 in the next iteration + previous = np.matmul([delta[i]], theta[i])[0] + # compute gradient of layer i + gradients[i] = [scalar_vector_product(d, net[i].inputs) for d in delta[i]] + + # add gradient of current example to batch gradient + total_gradients = vector_add(total_gradients, gradients) + + return total_gradients, batch_loss + + +def get_batch(examples, batch_size=1): + """Split examples into multiple batches""" + for i in range(0, len(examples), batch_size): + yield examples[i: i + batch_size] + + +class NeuralNetworkLearner: + """ + Simple dense multilayer neural network. + :param hidden_layer_sizes: size of hidden layers in the form of a list + """ + + def __init__(self, dataset, hidden_layer_sizes, l_rate=0.01, epochs=1000, batch_size=10, + optimizer=stochastic_gradient_descent, loss=mean_squared_error_loss, verbose=False, plot=False): + self.dataset = dataset + self.l_rate = l_rate + self.epochs = epochs + self.batch_size = batch_size + self.optimizer = optimizer + self.loss = loss + self.verbose = verbose + self.plot = plot + + input_size = len(dataset.inputs) + output_size = len(dataset.values[dataset.target]) + + # initialize the network + raw_net = [InputLayer(input_size)] + # add hidden layers + hidden_input_size = input_size + for h_size in hidden_layer_sizes: + raw_net.append(DenseLayer(hidden_input_size, h_size)) + hidden_input_size = h_size + raw_net.append(DenseLayer(hidden_input_size, output_size)) + self.raw_net = raw_net + + def fit(self, X, y): + self.learned_net = self.optimizer(self.dataset, self.raw_net, loss=self.loss, epochs=self.epochs, + l_rate=self.l_rate, batch_size=self.batch_size, verbose=self.verbose) + return self + + def predict(self, example): + n_layers = len(self.learned_net) + + layer_input = example + layer_out = example + + # get the output of each layer by forward passing + for i in range(1, n_layers): + layer_out = self.learned_net[i].forward(np.array(layer_input).reshape((-1, 1))) + layer_input = layer_out + + return layer_out.index(max(layer_out)) + + +class PerceptronLearner: + """ + Simple perceptron neural network. + """ + + def __init__(self, dataset, l_rate=0.01, epochs=1000, batch_size=10, optimizer=stochastic_gradient_descent, + loss=mean_squared_error_loss, verbose=False, plot=False): + self.dataset = dataset + self.l_rate = l_rate + self.epochs = epochs + self.batch_size = batch_size + self.optimizer = optimizer + self.loss = loss + self.verbose = verbose + self.plot = plot + + input_size = len(dataset.inputs) + output_size = len(dataset.values[dataset.target]) + + # initialize the network, add dense layer + self.raw_net = [InputLayer(input_size), DenseLayer(input_size, output_size)] + + def fit(self, X, y): + self.learned_net = self.optimizer(self.dataset, self.raw_net, loss=self.loss, epochs=self.epochs, + l_rate=self.l_rate, batch_size=self.batch_size, verbose=self.verbose) + return self + + def predict(self, example): + layer_out = self.learned_net[1].forward(np.array(example).reshape((-1, 1))) + return layer_out.index(max(layer_out)) + + +def keras_dataset_loader(dataset, max_length=500): + """ + Helper function to load keras datasets. + :param dataset: keras data set type + :param max_length: max length of each input sequence + """ + # init dataset + (X_train, y_train), (X_val, y_val) = dataset + if max_length > 0: + X_train = sequence.pad_sequences(X_train, maxlen=max_length) + X_val = sequence.pad_sequences(X_val, maxlen=max_length) + return (X_train[10:], y_train[10:]), (X_val, y_val), (X_train[:10], y_train[:10]) + + +def SimpleRNNLearner(train_data, val_data, epochs=2, verbose=False): + """ + RNN example for text sentimental analysis. + :param train_data: a tuple of (training data, targets) + Training data: ndarray taking training examples, while each example is coded by embedding + Targets: ndarray taking targets of each example. Each target is mapped to an integer + :param val_data: a tuple of (validation data, targets) + :param epochs: number of epochs + :param verbose: verbosity mode + :return: a keras model + """ + + total_inputs = 5000 + input_length = 500 + + # init data + X_train, y_train = train_data + X_val, y_val = val_data + + # init a the sequential network (embedding layer, rnn layer, dense layer) + model = Sequential() + model.add(Embedding(total_inputs, 32, input_length=input_length)) + model.add(SimpleRNN(units=128)) + model.add(Dense(1, activation='sigmoid')) + model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) + + # train the model + model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=epochs, batch_size=128, verbose=verbose) + + return model + + +def AutoencoderLearner(inputs, encoding_size, epochs=200, verbose=False): + """ + Simple example of linear auto encoder learning producing the input itself. + :param inputs: a batch of input data in np.ndarray type + :param encoding_size: int, the size of encoding layer + :param epochs: number of epochs + :param verbose: verbosity mode + :return: a keras model + """ + + # init data + input_size = len(inputs[0]) + + # init model + model = Sequential() + model.add(Dense(encoding_size, input_dim=input_size, activation='relu', kernel_initializer='random_uniform', + bias_initializer='ones')) + model.add(Dense(input_size, activation='relu', kernel_initializer='random_uniform', bias_initializer='ones')) + + # update model with sgd + sgd = optimizers.SGD(lr=0.01) + model.compile(loss='mean_squared_error', optimizer=sgd, metrics=['accuracy']) + + # train the model + model.fit(inputs, inputs, epochs=epochs, batch_size=10, verbose=verbose) + + return model diff --git a/games.ipynb b/games.ipynb index e51a0a2bc..edf955be8 100644 --- a/games.ipynb +++ b/games.ipynb @@ -4,22 +4,46 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Games or Adversarial search\n", + "# GAMES OR ADVERSARIAL SEARCH\n", "\n", "This notebook serves as supporting material for topics covered in **Chapter 5 - Adversarial Search** in the book *Artificial Intelligence: A Modern Approach.* This notebook uses implementations from [games.py](https://github.com/aimacode/aima-python/blob/master/games.py) module. Let's import required classes, methods, global variables etc., from games module." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CONTENTS\n", + "\n", + "* Game Representation\n", + "* Game Examples\n", + " * Tic-Tac-Toe\n", + " * Figure 5.2 Game\n", + "* Min-Max\n", + "* Alpha-Beta\n", + "* Players\n", + "* Let's Play Some Games!" + ] + }, { "cell_type": "code", "execution_count": 1, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "from games import (GameState, Game, Fig52Game, TicTacToe, query_player, random_player, \n", - " alphabeta_player, play_game, minimax_decision, alphabeta_full_search,\n", - " alphabeta_search, Canvas_TicTacToe)" + "from games import *\n", + "from notebook import psource, pseudocode" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GAME REPRESENTATION\n", + "\n", + "To represent games we make use of the `Game` class, which we can subclass and override its functions to represent our own games. A helper tool is the namedtuple `GameState`, which in some cases can come in handy, especially when our game needs us to remember a board (like chess)." ] }, { @@ -27,8 +51,20 @@ "metadata": {}, "source": [ "## `GameState` namedtuple\n", - " \n", - " `GameState` is a [namedtuple](https://docs.python.org/3.5/library/collections.html#collections.namedtuple) which represents the current state of a game. Let it be Tic-Tac-Toe or any other game." + "\n", + "`GameState` is a [namedtuple](https://docs.python.org/3.5/library/collections.html#collections.namedtuple) which represents the current state of a game. It is used to help represent games whose states can't be easily represented normally, or for games that require memory of a board, like Tic-Tac-Toe.\n", + "\n", + "`Gamestate` is defined as follows:\n", + "\n", + "`GameState = namedtuple('GameState', 'to_move, utility, board, moves')`\n", + "\n", + "* `to_move`: It represents whose turn it is to move next.\n", + "\n", + "* `utility`: It stores the utility of the game state. Storing this utility is a good idea, because, when you do a Minimax Search or an Alphabeta Search, you generate many recursive calls, which travel all the way down to the terminal states. When these recursive calls go back up to the original callee, we have calculated utilities for many game states. We store these utilities in their respective `GameState`s to avoid calculating them all over again.\n", + "\n", + "* `board`: A dict that stores the board of the game.\n", + "\n", + "* `moves`: It stores the list of legal moves possible from the current position." ] }, { @@ -38,180 +74,811 @@ }, "source": [ "## `Game` class\n", - " \n", - "Let's have a look at the class `Game` in our module. We see that it has functions, namely `actions`, `result`, `utility`, `terminal_test`, `to_move` and `display`. \n", "\n", - "We see that these functions have not actually been implemented. This class is actually just a template class; we are supposed to create the class for our game, `TicTacToe` by inheriting this `Game` class and implement all the methods mentioned in `Game`. Do not close the popup so that you can follow along the description of code below." + "Let's have a look at the class `Game` in our module. We see that it has functions, namely `actions`, `result`, `utility`, `terminal_test`, `to_move` and `display`.\n", + "\n", + "We see that these functions have not actually been implemented. This class is just a template class; we are supposed to create the class for our game, by inheriting this `Game` class and implementing all the methods mentioned in `Game`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "%psource Game" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's get into details of all the methods in our `Game` class. You have to implement these methods when you create new classes that would represent your game.\n", + "\n", + "* `actions(self, state)`: Given a game state, this method generates all the legal actions possible from this state, as a list or a generator. Returning a generator rather than a list has the advantage that it saves space and you can still operate on it as a list.\n", + "\n", + "\n", + "* `result(self, state, move)`: Given a game state and a move, this method returns the game state that you get by making that move on this game state.\n", + "\n", + "\n", + "* `utility(self, state, player)`: Given a terminal game state and a player, this method returns the utility for that player in the given terminal game state. While implementing this method assume that the game state is a terminal game state. The logic in this module is such that this method will be called only on terminal game states.\n", + "\n", + "\n", + "* `terminal_test(self, state)`: Given a game state, this method should return `True` if this game state is a terminal state, and `False` otherwise.\n", + "\n", + "\n", + "* `to_move(self, state)`: Given a game state, this method returns the player who is to play next. This information is typically stored in the game state, so all this method does is extract this information and return it.\n", + "\n", + "\n", + "* `display(self, state)`: This method prints/displays the current state of the game." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GAME EXAMPLES\n", + "\n", + "Below we give some examples for games you can create and experiment on." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tic-Tac-Toe\n", + "\n", + "Take a look at the class `TicTacToe`. All the methods mentioned in the class `Game` have been implemented here." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "output_type": "stream", + "text": "\u001b[1;32mclass\u001b[0m \u001b[0mTicTacToe\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mGame\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"Play TicTacToe on an h x v board, with Max (first player) playing 'X'.\n A state has the player to move, a cached utility, a list of moves in\n the form of a list of (x, y) positions, and a board, in the form of\n a dict of {(x, y): Player} entries, where Player is 'X' or 'O'.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mh\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m3\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mv\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m3\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mk\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m3\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mh\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mh\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mv\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mv\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mk\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mmoves\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mh\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0my\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mv\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minitial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mGameState\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mto_move\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m'X'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mutility\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmoves\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mactions\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"Legal moves are any square not yet taken.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mresult\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mmove\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mstate\u001b[0m \u001b[1;31m# Illegal move has no effect\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mboard\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mmove\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto_move\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mmoves\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mmoves\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmove\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mGameState\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mto_move\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'O'\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto_move\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'X'\u001b[0m \u001b[1;32melse\u001b[0m \u001b[1;34m'X'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mutility\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcompute_utility\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto_move\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmoves\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mutility\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"Return the value to player; 1 for win, -1 for loss, 0 otherwise.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mutility\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mplayer\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'X'\u001b[0m \u001b[1;32melse\u001b[0m \u001b[1;33m-\u001b[0m\u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mutility\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mterminal_test\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"A state is terminal if it is won or there are no empty squares.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mutility\u001b[0m \u001b[1;33m!=\u001b[0m \u001b[1;36m0\u001b[0m \u001b[1;32mor\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmoves\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mdisplay\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mboard\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mstate\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mh\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0my\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mv\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'.'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mend\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m' '\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mcompute_utility\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"If 'X' wins with this move, return 1; if 'O' wins return -1; else return 0.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk_in_row\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mor\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk_in_row\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mor\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk_in_row\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mor\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk_in_row\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;33m+\u001b[0m\u001b[1;36m1\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mplayer\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'X'\u001b[0m \u001b[1;32melse\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\n\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mk_in_row\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdelta_x_y\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;34m\"\"\"Return true if there is a line through move on board for player.\"\"\"\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mdelta_x\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdelta_y\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdelta_x_y\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mn\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m \u001b[1;31m# n is number of moves in row\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mwhile\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mn\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mx\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mdelta_x\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mdelta_y\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mmove\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mwhile\u001b[0m \u001b[0mboard\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mplayer\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mn\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mx\u001b[0m \u001b[1;33m-\u001b[0m \u001b[0mdelta_x\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m-\u001b[0m \u001b[0mdelta_y\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[0mn\u001b[0m \u001b[1;33m-=\u001b[0m \u001b[1;36m1\u001b[0m \u001b[1;31m# Because we counted move itself twice\u001b[0m\u001b[1;33m\n\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mn\u001b[0m \u001b[1;33m>=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mk\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "metadata": {}, + "execution_count": 4 + } + ], + "source": [ + "%psource TicTacToe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The class `TicTacToe` has been inherited from the class `Game`. As mentioned earlier, you really want to do this. Catching bugs and errors becomes a whole lot easier.\n", + "\n", + "Additional methods in TicTacToe:\n", + "\n", + "* `__init__(self, h=3, v=3, k=3)` : When you create a class inherited from the `Game` class (class `TicTacToe` in our case), you'll have to create an object of this inherited class to initialize the game. This initialization might require some additional information which would be passed to `__init__` as variables. For the case of our `TicTacToe` game, this additional information would be the number of rows `h`, number of columns `v` and how many consecutive X's or O's are needed in a row, column or diagonal for a win `k`. Also, the initial game state has to be defined here in `__init__`.\n", + "\n", + "\n", + "* `compute_utility(self, board, move, player)` : A method to calculate the utility of TicTacToe game. If 'X' wins with this move, this method returns 1; if 'O' wins return -1; else return 0.\n", + "\n", + "\n", + "* `k_in_row(self, board, move, player, delta_x_y)` : This method returns `True` if there is a line formed on TicTacToe board with the latest move else `False.`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TicTacToe GameState\n", + "\n", + "Now, before we start implementing our `TicTacToe` game, we need to decide how we will be representing our game state. Typically, a game state will give you all the current information about the game at any point in time. When you are given a game state, you should be able to tell whose turn it is next, how the game will look like on a real-life board (if it has one) etc. A game state need not include the history of the game. If you can play the game further given a game state, you game state representation is acceptable. While we might like to include all kinds of information in our game state, we wouldn't want to put too much information into it. Modifying this game state to generate a new one would be a real pain then.\n", + "\n", + "Now, as for our `TicTacToe` game state, would storing only the positions of all the X's and O's be sufficient to represent all the game information at that point in time? Well, does it tell us whose turn it is next? Looking at the 'X's and O's on the board and counting them should tell us that. But that would mean extra computing. To avoid this, we will also store whose move it is next in the game state.\n", + "\n", + "Think about what we've done here. We have reduced extra computation by storing additional information in a game state. Now, this information might not be absolutely essential to tell us about the state of the game, but it does save us additional computation time. We'll do more of this later on." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To store game states will will use the `GameState` namedtuple.\n", + "\n", + "* `to_move`: A string of a single character, either 'X' or 'O'.\n", + "\n", + "* `utility`: 1 for win, -1 for loss, 0 otherwise.\n", + "\n", + "* `board`: All the positions of X's and O's on the board.\n", + "\n", + "* `moves`: All the possible moves from the current state. Note here, that storing the moves as a list, as it is done here, increases the space complexity of Minimax Search from `O(m)` to `O(bm)`. Refer to Sec. 5.2.1 of the book." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Representing a move in TicTacToe game\n", + "\n", + "Now that we have decided how our game state will be represented, it's time to decide how our move will be represented. Becomes easy to use this move to modify a current game state to generate a new one.\n", + "\n", + "For our `TicTacToe` game, we'll just represent a move by a tuple, where the first and the second elements of the tuple will represent the row and column, respectively, where the next move is to be made. Whether to make an 'X' or an 'O' will be decided by the `to_move` in the `GameState` namedtuple." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fig52 Game\n", + "\n", + "For a more trivial example we will represent the game in **Figure 5.2** of the book.\n", + "\n", + "\n", + "\n", + "The states are represented with capital letters inside the triangles (eg. \"A\") while moves are the labels on the edges between states (eg. \"a1\"). Terminal nodes carry utility values. Note that the terminal nodes are named in this example 'B1', 'B2' and 'B2' for the nodes below 'B', and so forth.\n", + "\n", + "We will model the moves, utilities and initial state like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "moves = dict(A=dict(a1='B', a2='C', a3='D'),\n", + " B=dict(b1='B1', b2='B2', b3='B3'),\n", + " C=dict(c1='C1', c2='C2', c3='C3'),\n", + " D=dict(d1='D1', d2='D2', d3='D3'))\n", + "utils = dict(B1=3, B2=12, B3=8, C1=2, C2=4, C3=6, D1=14, D2=5, D3=2)\n", + "initial = 'A'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In `moves`, we have a nested dictionary system. The outer's dictionary has keys as the states and values the possible moves from that state (as a dictionary). The inner dictionary of moves has keys the move names and values the next state after the move is complete.\n", + "\n", + "Below is an example that showcases `moves`. We want the next state after move 'a1' from 'A', which is 'B'. A quick glance at the above image confirms that this is indeed the case." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "B\n" + ] + } + ], + "source": [ + "print(moves['A']['a1'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now take a look at the functions we need to implement. First we need to create an object of the `Fig52Game` class." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "fig52 = Fig52Game()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`actions`: Returns the list of moves one can make from a given state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game.actions)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['b1', 'b2', 'b3']\n" + ] + } + ], + "source": [ + "print(fig52.actions('B'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`result`: Returns the next state after we make a specific move." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game.result)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "B\n" + ] + } + ], + "source": [ + "print(fig52.result('A', 'a1'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`utility`: Returns the value of the terminal state for a player ('MAX' and 'MIN'). Note that for 'MIN' the value returned is the negative of the utility." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game.utility)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "-3\n" + ] + } + ], + "source": [ + "print(fig52.utility('B1', 'MAX'))\n", + "print(fig52.utility('B1', 'MIN'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`terminal_test`: Returns `True` if the given state is a terminal state, `False` otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game.terminal_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], + "source": [ + "print(fig52.terminal_test('C3'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`to_move`: Return the player who will move in this state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game.to_move)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MAX\n" + ] + } + ], + "source": [ + "print(fig52.to_move('A'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a whole the class `Fig52` that inherits from the class `Game` and overrides its functions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(Fig52Game)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MIN-MAX\n", + "\n", + "## Overview\n", + "\n", + "This algorithm (often called *Minimax*) computes the next move for a player (MIN or MAX) at their current state. It recursively computes the minimax value of successor states, until it reaches terminals (the leaves of the tree). Using the `utility` value of the terminal states, it computes the values of parent states until it reaches the initial node (the root of the tree).\n", + "\n", + "It is worth noting that the algorithm works in a depth-first manner. The pseudocode can be found below:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ MINIMAX-DECISION(_state_) __returns__ _an action_ \n", + " __return__ arg max _a_ ∈ ACTIONS(_s_) MIN\\-VALUE(RESULT(_state_, _a_)) \n", + "\n", + "---\n", + "__function__ MAX\\-VALUE(_state_) __returns__ _a utility value_ \n", + " __if__ TERMINAL\\-TEST(_state_) __then return__ UTILITY(_state_) \n", + " _v_ ← −∞ \n", + " __for each__ _a_ __in__ ACTIONS(_state_) __do__ \n", + "   _v_ ← MAX(_v_, MIN\\-VALUE(RESULT(_state_, _a_))) \n", + " __return__ _v_ \n", + "\n", + "---\n", + "__function__ MIN\\-VALUE(_state_) __returns__ _a utility value_ \n", + " __if__ TERMINAL\\-TEST(_state_) __then return__ UTILITY(_state_) \n", + " _v_ ← ∞ \n", + " __for each__ _a_ __in__ ACTIONS(_state_) __do__ \n", + "   _v_ ← MIN(_v_, MAX\\-VALUE(RESULT(_state_, _a_))) \n", + " __return__ _v_ \n", + "\n", + "---\n", + "__Figure__ ?? An algorithm for calculating minimax decisions. It returns the action corresponding to the best possible move, that is, the move that leads to the outcome with the best utility, under the assumption that the opponent plays to minimize utility. The functions MAX\\-VALUE and MIN\\-VALUE go through the whole game tree, all the way to the leaves, to determine the backed\\-up value of a state. The notation argmax _a_ ∈ _S_ _f_(_a_) computes the element _a_ of set _S_ that has maximum value of _f_(_a_)." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode(\"Minimax-Decision\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implementation\n", + "\n", + "In the implementation we are using two functions, `max_value` and `min_value` to calculate the best move for MAX and MIN respectively. These functions interact in an alternating recursion; one calls the other until a terminal state is reached. When the recursion halts, we are left with scores for each move. We return the max. Despite returning the max, it will work for MIN too since for MIN the values are their negative (hence the order of values is reversed, so the higher the better for MIN too)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(minimax_decision)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example\n", + "\n", + "We will now play the Fig52 game using this algorithm. Take a look at the Fig52Game from above to follow along.\n", + "\n", + "It is the turn of MAX to move, and he is at state A. He can move to B, C or D, using moves a1, a2 and a3 respectively. MAX's goal is to maximize the end value. So, to make a decision, MAX needs to know the values at the aforementioned nodes and pick the greatest one. After MAX, it is MIN's turn to play. So MAX wants to know what will the values of B, C and D be after MIN plays.\n", + "\n", + "The problem then becomes what move will MIN make at B, C and D. The successor states of all these nodes are terminal states, so MIN will pick the smallest value for each node. So, for B he will pick 3 (from move b1), for C he will pick 2 (from move c1) and for D he will again pick 2 (from move d3).\n", + "\n", + "Let's see this in code:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b1\n", + "c1\n", + "d3\n" + ] + } + ], + "source": [ + "print(minimax_decision('B', fig52))\n", + "print(minimax_decision('C', fig52))\n", + "print(minimax_decision('D', fig52))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now MAX knows that the values for B, C and D are 3, 2 and 2 (produced by the above moves of MIN). The greatest is 3, which he will get with move a1. This is then the move MAX will make. Let's see the algorithm in full action:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "a1\n" + ] + } + ], + "source": [ + "print(minimax_decision('A', fig52))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization\n", + "\n", + "Below we have a simple game visualization using the algorithm. After you run the command, click on the cell to move the game along. You can input your own values via a list of 27 integers." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "%psource Game" + "from notebook import Canvas_minimax\n", + "from random import randint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "minimax_viz = Canvas_minimax('minimax_viz', [randint(1, 50) for i in range(27)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ALPHA-BETA\n", + "\n", + "## Overview\n", + "\n", + "While *Minimax* is great for computing a move, it can get tricky when the number of game states gets bigger. The algorithm needs to search all the leaves of the tree, which increase exponentially to its depth.\n", + "\n", + "For Tic-Tac-Toe, where the depth of the tree is 9 (after the 9th move, the game ends), we can have at most 9! terminal states (at most because not all terminal nodes are at the last level of the tree; some are higher up because the game ended before the 9th move). This isn't so bad, but for more complex problems like chess, we have over $10^{40}$ terminal nodes. Unfortunately we have not found a way to cut the exponent away, but we nevertheless have found ways to alleviate the workload.\n", + "\n", + "Here we examine *pruning* the game tree, which means removing parts of it that we do not need to examine. The particular type of pruning is called *alpha-beta*, and the search in whole is called *alpha-beta search*.\n", + "\n", + "To showcase what parts of the tree we don't need to search, we will take a look at the example `Fig52Game`.\n", + "\n", + "In the example game, we need to find the best move for player MAX at state A, which is the maximum value of MIN's possible moves at successor states.\n", + "\n", + "`MAX(A) = MAX( MIN(B), MIN(C), MIN(D) )`\n", + "\n", + "`MIN(B)` is the minimum of 3, 12, 8 which is 3. So the above formula becomes:\n", + "\n", + "`MAX(A) = MAX( 3, MIN(C), MIN(D) )`\n", + "\n", + "Next move we will check is c1, which leads to a terminal state with utility of 2. Before we continue searching under state C, let's pop back into our formula with the new value:\n", + "\n", + "`MAX(A) = MAX( 3, MIN(2, c2, .... cN), MIN(D) )`\n", + "\n", + "We do not know how many moves state C allows, but we know that the first one results in a value of 2. Do we need to keep searching under C? The answer is no. The value MIN will pick on C will at most be 2. Since MAX already has the option to pick something greater than that, 3 from B, he does not need to keep searching under C.\n", + "\n", + "In *alpha-beta* we make use of two additional parameters for each state/node, *a* and *b*, that describe bounds on the possible moves. The parameter *a* denotes the best choice (highest value) for MAX along that path, while *b* denotes the best choice (lowest value) for MIN. As we go along we update *a* and *b* and prune a node branch when the value of the node is worse than the value of *a* and *b* for MAX and MIN respectively.\n", + "\n", + "In the above example, after the search under state B, MAX had an *a* value of 3. So, when searching node C we found a value less than that, 2, we stopped searching under C.\n", + "\n", + "You can read the pseudocode below:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ ALPHA-BETA-SEARCH(_state_) __returns__ an action \n", + " _v_ ← MAX\\-VALUE(_state_, −∞, +∞) \n", + " __return__ the _action_ in ACTIONS(_state_) with value _v_ \n", + "\n", + "---\n", + "__function__ MAX\\-VALUE(_state_, _α_, _β_) __returns__ _a utility value_ \n", + " __if__ TERMINAL\\-TEST(_state_) __then return__ UTILITY(_state_) \n", + " _v_ ← −∞ \n", + " __for each__ _a_ __in__ ACTIONS(_state_) __do__ \n", + "   _v_ ← MAX(_v_, MIN\\-VALUE(RESULT(_state_, _a_), _α_, _β_)) \n", + "   __if__ _v_ ≥ _β_ __then return__ _v_ \n", + "   _α_ ← MAX(_α_, _v_) \n", + " __return__ _v_ \n", + "\n", + "---\n", + "__function__ MIN\\-VALUE(_state_, _α_, _β_) __returns__ _a utility value_ \n", + " __if__ TERMINAL\\-TEST(_state_) __then return__ UTILITY(_state_) \n", + " _v_ ← +∞ \n", + " __for each__ _a_ __in__ ACTIONS(_state_) __do__ \n", + "   _v_ ← MIN(_v_, MAX\\-VALUE(RESULT(_state_, _a_), _α_, _β_)) \n", + "   __if__ _v_ ≤ _α_ __then return__ _v_ \n", + "   _β_ ← MIN(_β_, _v_) \n", + " __return__ _v_ \n", + "\n", + "\n", + "---\n", + "__Figure__ ?? The alpha\\-beta search algorithm. Notice that these routines are the same as the MINIMAX functions in Figure ??, except for the two lines in each of MIN\\-VALUE and MAX\\-VALUE that maintain _α_ and _β_ (and the bookkeeping to pass these parameters along)." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode(\"Alpha-Beta-Search\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - " Now let's get into details of all the methods in our `Game` class. You have to implement these methods when you create new classes that would represent your game.\n", - " \n", - "* `actions(self, state)` : Given a game state, this method generates all the legal actions possible from this state, as a list or a generator. Returning a generator rather than a list has the advantage that it saves space and you can still operate on it as a list.\n", - "\n", - "\n", - "* `result(self, state, move)` : Given a game state and a move, this method returns the game state that you get by making that move on this game state.\n", - "\n", - "\n", - "* `utility(self, state, player)` : Given a terminal game state and a player, this method returns the utility for that player in the given terminal game state. While implementing this method assume that the game state is a terminal game state. The logic in this module is such that this method will be called only on terminal game states.\n", - "\n", - "\n", - "* `terminal_test(self, state)` : Given a game state, this method should return `True` if this game state is a terminal state, and `False` otherwise.\n", + "## Implementation\n", "\n", + "Like *minimax*, we again make use of functions `max_value` and `min_value`, but this time we utilise the *a* and *b* values, updating them and stopping the recursive call if we end up on nodes with values worse than *a* and *b* (for MAX and MIN). The algorithm finds the maximum value and returns the move that results in it.\n", "\n", - "* `to_move(self, state)` : Given a game state, this method returns the player who is to play next. This information is typically stored in the game state, so all this method does is extract this information and return it.\n", - "\n", - "\n", - "* `display(self, state)` : This method prints/displays current state of the game." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## `TicTacToe` class\n", - " \n", - " Take a look at the class `TicTacToe`. All the methods mentioned in the class `Game` have been implemented here." + "The implementation:" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "%psource TicTacToe" + "%psource alphabeta_search" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - " The class `TicTacToe` has been inherited from the class `Game`. As mentioned earlier, you really want to do this. Catching bugs and errors becomes a whole lot easier.\n", - "\n", - "Additional methods in TicTacToe:\n", - "\n", - "* `__init__(self, h=3, v=3, k=3)` : When you create a class inherited from the `Game` class (class `TicTacToe` in our case), you'll have to create an object of this inherited class to initialize the game. This initialization might require some additional information which would be passed to `__init__` as variables. For the case of our `TicTacToe` game, this additional information would be the number of rows `h`, number of columns `v` and how many consecutive X's or O's are needed in a row, column or diagonal for a win `k`. Also, the initial game state has to be defined here in `__init__`.\n", - "\n", + "## Example\n", "\n", - "* `compute_utility(self, board, move, player)` : A method to calculate the utility of TicTacToe game. If 'X' wins with this move, this method returns 1; if 'O' wins return -1; else return 0.\n", - "\n", - "\n", - "* `k_in_row(self, board, move, player, delta_x_y)` : This method returns `True` if there is a line formed on TicTacToe board with the latest move else `False.`" + "We will play the Fig52 Game with the *alpha-beta* search algorithm. It is the turn of MAX to play at state A." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 22, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "a1\n" + ] + } + ], "source": [ - "## GameState in TicTacToe game\n", - "\n", - " Now, before we start implementing our `TicTacToe` game, we need to decide how we will be representing our game state. Typically, a game state will give you all the current information about the game at any point in time. When you are given a game state, you should be able to tell whose turn it is next, how the game will look like on a real-life board (if it has one) etc. A game state need not include the history of the game. If you can play the game further given a game state, you game state representation is acceptable. While we might like to include all kinds of information in our game state, we wouldn't want to put too much information into it. Modifying this game state to generate a new one would be a real pain then.\n", - " \n", - " Now, as for our `TicTacToe` game state, would storing only the positions of all the X's and O's be sufficient to represent all the game information at that point in time? Well, does it tell us whose turn it is next? Looking at the 'X's and O's on the board and counting them should tell us that. But that would mean extra computing. To avoid this, we will also store whose move it is next in the game state. \n", - " \n", - " Think about what we've done here. We have reduced extra computation by storing additional information in a game state. Now, this information might not be absolutely essential to tell us about the state of the game, but it does save us additional computation time. We'll do more of this later on. \n", - " \n", - " The `TicTacToe` game defines its game state as:\n", - " \n", - " `GameState = namedtuple('GameState', 'to_move, utility, board, moves')`" + "print(alphabeta_search('A', fig52))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The game state is called, quite appropriately, `GameState`, and it has 4 variables, namely, `to_move`, `utility`, `board` and `moves`. \n", - " \n", - " I'll describe these variables in some more detail:\n", - " \n", - "* `to_move` : It represents whose turn it is to move next. This will be a string of a single character, either 'X' or 'O'.\n", + "The optimal move for MAX is a1, for the reasons given above. MIN will pick move b1 for B resulting in a value of 3, updating the *a* value of MAX to 3. Then, when we find under C a node of value 2, we will stop searching under that sub-tree since it is less than *a*. From D we have a value of 2. So, the best move for MAX is the one resulting in a value of 3, which is a1.\n", "\n", - "\n", - "* `utility` : It stores the utility of the game state. Storing this utility is a good idea, because, when you do a Minimax Search or an Alphabeta Search, you generate many recursive calls, which travel all the way down to the terminal states. When these recursive calls go back up to the original callee, we have calculated utilities for many game states. We store these utilities in their respective `GameState`s to avoid calculating them all over again.\n", - "\n", - "\n", - "* `board` : A dict that stores all the positions of X's and O's on the board\n", - "\n", - "\n", - "* `moves` : It stores the list of legal moves possible from the current position. Note here, that storing the moves as a list, as it is done here, increases the space complexity of Minimax Search from `O(m)` to `O(bm)`. Refer to Sec. 5.2.1 of the book." + "Below we see the best moves for MIN starting from B, C and D respectively. Note that the algorithm in these cases works the same way as *minimax*, since all the nodes below the aforementioned states are terminal." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 23, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b1\n", + "c1\n", + "d3\n" + ] + } + ], "source": [ - "## Representing a move in TicTacToe game\n", - " \n", - " Now that we have decided how our game state will be represented, it's time to decide how our move will be represented. Becomes easy to use this move to modify a current game state to generate a new one.\n", - " \n", - " For our `TicTacToe` game, we'll just represent a move by a tuple, where the first and the second elements of the tuple will represent the row and column, respectively, where the next move is to be made. Whether to make an 'X' or an 'O' will be decided by the `to_move` in the `GameState` namedtuple." + "print(alphabeta_search('B', fig52))\n", + "print(alphabeta_search('C', fig52))\n", + "print(alphabeta_search('D', fig52))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Players to play games\n", - "\n", - " So, we have finished implementation of the `TicTacToe` class. What this class does is that, it just defines the rules of the game. We need more to create an AI that can actually play the game. This is where `random_player` and `alphabeta_player` come in. \n", - "\n", - "### query_player\n", - " The `query_player` function allows you, a human opponent, to play the game. This function requires a `display` method to be implemented in your game class, so that successive game states can be displayed on the terminal, making it easier for you to visualize the game and play accordingly. \n", - "\n", - "### random_player\n", - " The `random_player` is a function that plays random moves in the game. That's it. There isn't much more to this guy. \n", + "## Visualization\n", "\n", - "### alphabeta_player\n", - " The `alphabeta_player`, on the other hand, calls the `alphabeta_full_search` function, which returns the best move in the current game state. Thus, the `alphabeta_player` always plays the best move given a game state, assuming that the game tree is small enough to search entirely.\n", - " \n", - "### play_game\n", - " The `play_game` function will be the one that will actually be used to play the game. You pass as arguments to it, an instance of the game you want to play and the players you want in this game. Use it to play AI vs AI, AI vs human, or even human vs human matches!" + "Below you will find the visualization of the alpha-beta algorithm for a simple game. Click on the cell after you run the command to move the game along. You can input your own values via a list of 27 integers." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from notebook import Canvas_alphabeta\n", + "from random import randint" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "## Let's play some games\n", - "### Game52" + "alphabeta_viz = Canvas_alphabeta('alphabeta_viz', [randint(1, 50) for i in range(27)])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "" + "# PLAYERS\n", + "\n", + "So, we have finished the implementation of the `TicTacToe` and `Fig52Game` classes. What these classes do is defining the rules of the games. We need more to create an AI that can actually play games. This is where `random_player` and `alphabeta_player` come in.\n", + "\n", + "## query_player\n", + "The `query_player` function allows you, a human opponent, to play the game. This function requires a `display` method to be implemented in your game class, so that successive game states can be displayed on the terminal, making it easier for you to visualize the game and play accordingly.\n", + "\n", + "## random_player\n", + "The `random_player` is a function that plays random moves in the game. That's it. There isn't much more to this guy. \n", + "\n", + "## alphabeta_player\n", + "The `alphabeta_player`, on the other hand, calls the `alphabeta_search` function, which returns the best move in the current game state. Thus, the `alphabeta_player` always plays the best move given a game state, assuming that the game tree is small enough to search entirely.\n", + "\n", + "## minimax_player\n", + "The `minimax_player`, on the other hand calls the `minimax_search` function which returns the best move in the current game state.\n", + "\n", + "## play_game\n", + "The `play_game` function will be the one that will actually be used to play the game. You pass as arguments to it an instance of the game you want to play and the players you want in this game. Use it to play AI vs AI, AI vs human, or even human vs human matches!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "# LET'S PLAY SOME GAMES!\n", + "\n", + "## Game52\n", + "\n", "Let's start by experimenting with the `Fig52Game` first. For that we'll create an instance of the subclass Fig52Game inherited from the class Game:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 27, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ @@ -227,10 +894,8 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, + "execution_count": 28, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -250,15 +915,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `alphabeta_player(game, state)` will always give us the best move possible:" + "The `alphabeta_player(game, state)` will always give us the best move possible, for the relevant player (MAX or MIN):" ] }, { "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, + "execution_count": 29, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -280,15 +943,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "What the `alphabeta_player` does is, it simply calls the method `alphabeta_full_search`. They both are essentially the same. In the module, both `alphabeta_full_search` and `minimax_decision` have been implemented. They both do the same job and return the same thing, which is, the best move in the current state. It's just that `alphabeta_full_search` is more efficient w.r.t time because it prunes the search tree and hence, explores lesser number of states." + "What the `alphabeta_player` does is, it simply calls the method `alphabeta_full_search`. They both are essentially the same. In the module, both `alphabeta_full_search` and `minimax_decision` have been implemented. They both do the same job and return the same thing, which is, the best move in the current state. It's just that `alphabeta_full_search` is more efficient with regards to time because it prunes the search tree and hence, explores lesser number of states." ] }, { "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, + "execution_count": 30, + "metadata": {}, "outputs": [ { "data": { @@ -296,7 +957,7 @@ "'a1'" ] }, - "execution_count": 7, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -307,10 +968,8 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, + "execution_count": 31, + "metadata": {}, "outputs": [ { "data": { @@ -318,13 +977,13 @@ "'a1'" ] }, - "execution_count": 8, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "alphabeta_full_search('A', game52)" + "alphabeta_search('A', game52)" ] }, { @@ -336,10 +995,8 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, + "execution_count": 32, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -354,21 +1011,19 @@ "3" ] }, - "execution_count": 9, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "play_game(game52, alphabeta_player, alphabeta_player)" + "game52.play_game(alphabeta_player, alphabeta_player)" ] }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, + "execution_count": 33, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -383,47 +1038,100 @@ "12" ] }, - "execution_count": 10, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "play_game(game52, alphabeta_player, random_player)" + "game52.play_game(alphabeta_player, random_player)" ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "current state:\n", + "A\n", + "available moves: ['a1', 'a2', 'a3']\n", + "\n", + "Your move? a1\n", + "B1\n" + ] + }, + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "#play_game(game52, query_player, alphabeta_player)\n", - "#play_game(game52, alphabeta_player, query_player)" + "game52.play_game(query_player, alphabeta_player)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "current state:\n", + "B\n", + "available moves: ['b1', 'b2', 'b3']\n", + "\n", + "Your move? b1\n", + "B1\n" + ] + }, + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "game52.play_game(alphabeta_player, query_player)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that, here, if you are the first player, the alphabeta_player plays as MIN, and if you are the second player, the alphabeta_player plays as MAX. This happens because that's the way the game is defined in the class Fig52Game. Having a look at the code of this class should make it clear." + "Note that if you are the first player then alphabeta_player plays as MIN, and if you are the second player then alphabeta_player plays as MAX. This happens because that's the way the game is defined in the class Fig52Game. Having a look at the code of this class should make it clear." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### TicTacToe game\n", + "## TicTacToe\n", + "\n", "Now let's play `TicTacToe`. First we initialize the game by creating an instance of the subclass TicTacToe inherited from the class Game:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 36, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ @@ -439,10 +1147,8 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, + "execution_count": 37, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -462,16 +1168,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Hmm, so that's the initial state of the game; no X's and no O's. \n", - " \n", - " Let us create a new game state by ourselves to experiment:" + "Hmm, so that's the initial state of the game; no X's and no O's.\n", + "\n", + "Let us create a new game state by ourselves to experiment:" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 38, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ @@ -490,15 +1196,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "So, how does this game state looks like?" + "So, how does this game state look like?" ] }, { "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, + "execution_count": 39, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -523,18 +1227,16 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, + "execution_count": 40, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(3, 3)" + "(2, 2)" ] }, - "execution_count": 16, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -545,18 +1247,16 @@ }, { "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, + "execution_count": 41, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(3, 2)" + "(2, 2)" ] }, - "execution_count": 17, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } @@ -574,10 +1274,8 @@ }, { "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": false - }, + "execution_count": 42, + "metadata": {}, "outputs": [ { "data": { @@ -585,7 +1283,7 @@ "(2, 2)" ] }, - "execution_count": 18, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -598,46 +1296,51 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now let's make 2 players play against each other. We use the `play_game` function for this. The `play_game` function makes players play the match against each other and returns the utility for the first player, of the terminal state reached when the game ends. Hence, for our `TicTacToe` game, if we get the output +1, the first player wins, -1 if the second player wins, and 0 if the match ends in a draw." + "Now let's make two players play against each other. We use the `play_game` function for this. The `play_game` function makes players play the match against each other and returns the utility for the first player, of the terminal state reached when the game ends. Hence, for our `TicTacToe` game, if we get the output +1, the first player wins, -1 if the second player wins, and 0 if the match ends in a draw." ] }, { "cell_type": "code", - "execution_count": 19, - "metadata": { - "collapsed": false - }, + "execution_count": 43, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "O X O \n", - "O . X \n", - "O X X \n", - "-1\n" + "O O . \n", + "X O X \n", + "X X O \n" ] + }, + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "print(play_game(ttt, random_player, alphabeta_player))" + "ttt.play_game(random_player, alphabeta_player)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The output is -1, hence `random_player` loses implies `alphabeta_player` wins. \n", - " \n", - " Since, an `alphabeta_player` plays perfectly, a match between two `alphabeta_player`s should always end in a draw. Let's see if this happens:" + "The output is (usually) -1, because `random_player` loses to `alphabeta_player`. Sometimes, however, `random_player` manages to draw with `alphabeta_player`.\n", + "\n", + "Since an `alphabeta_player` plays perfectly, a match between two `alphabeta_player`s should always end in a draw. Let's see if this happens:" ] }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, + "execution_count": 44, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -688,7 +1391,7 @@ ], "source": [ "for _ in range(10):\n", - " print(play_game(ttt, alphabeta_player, alphabeta_player))" + " print(ttt.play_game(alphabeta_player, alphabeta_player))" ] }, { @@ -700,61 +1403,59 @@ }, { "cell_type": "code", - "execution_count": 21, - "metadata": { - "collapsed": false - }, + "execution_count": 45, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "X . . \n", + "X O O \n", + "X O . \n", + "O X X \n", + "-1\n", + "O X . \n", + "O X X \n", + "O . . \n", + "-1\n", + "X X O \n", + "O O X \n", + "O X . \n", + "-1\n", "O O O \n", ". X X \n", + "X . . \n", "-1\n", "O O O \n", - "X X O \n", - "X X . \n", + ". . X \n", + "X . X \n", "-1\n", - "O X . \n", - ". O X \n", + "O X O \n", + "X O X \n", "X . O \n", "-1\n", - "O . . \n", - ". O X \n", - "X X O \n", + "O X X \n", + "O X X \n", + "O O . \n", "-1\n", + "O O X \n", "X O X \n", - "X O O \n", - ". O X \n", - "-1\n", - "O . X \n", "X O . \n", - ". X O \n", "-1\n", "O O X \n", - "X O X \n", "X O . \n", + "X O X \n", "-1\n", - "O O O \n", - "O X X \n", - "X . X \n", - "-1\n", - "X X O \n", "O O X \n", - "O X . \n", - "-1\n", - "X . X \n", - "O O O \n", - ". X . \n", - "-1\n" + "X X O \n", + "O X X \n", + "0\n" ] } ], "source": [ "for _ in range(10):\n", - " print(play_game(ttt, random_player, alphabeta_player))" + " print(ttt.play_game(random_player, alphabeta_player))" ] }, { @@ -765,15 +1466,24 @@ "\n", "This subclass is used to play TicTacToe game interactively in Jupyter notebooks. TicTacToe class is called while initializing this subclass.\n", "\n", - "Let's have match between `random_player` and `alphabeta_player`. Click on the board to call players to make a move." + "Let's have a match between `random_player` and `alphabeta_player`. Click on the board to call players to make a move." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 46, "metadata": { - "collapsed": false + "collapsed": true }, + "outputs": [], + "source": [ + "from notebook import Canvas_TicTacToe" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, "outputs": [ { "data": { @@ -781,7 +1491,7 @@ "\n", "\n", "
    \n", - "\n", + "\n", "
    \n", "\n", "\n" @@ -798,13 +1508,14 @@ "text/html": [ "" ], "text/plain": [ @@ -828,10 +1539,8 @@ }, { "cell_type": "code", - "execution_count": 23, - "metadata": { - "collapsed": false - }, + "execution_count": 48, + "metadata": {}, "outputs": [ { "data": { @@ -839,7 +1548,7 @@ "\n", "\n", "
    \n", - "\n", + "\n", "
    \n", "\n", "\n" @@ -856,13 +1565,14 @@ "text/html": [ "" ], "text/plain": [ @@ -881,15 +1591,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Yay! We win. But we cannot win against an `alphabeta_player`, however hard we try." + "Yay! We (usually) win. But we cannot win against an `alphabeta_player`, however hard we try." ] }, { "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": false - }, + "execution_count": 49, + "metadata": {}, "outputs": [ { "data": { @@ -897,7 +1605,7 @@ "\n", "\n", "
    \n", - "\n", + "\n", "
    \n", "\n", "\n" @@ -914,13 +1622,14 @@ "text/html": [ "" ], "text/plain": [ @@ -952,9 +1661,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.8.2-final" } }, "nbformat": 4, - "nbformat_minor": 0 -} + "nbformat_minor": 1 +} \ No newline at end of file diff --git a/games.py b/games.py index 90604bf69..d22b2e640 100644 --- a/games.py +++ b/games.py @@ -1,19 +1,23 @@ -"""Games, or Adversarial Search (Chapter 5)""" +"""Games or Adversarial Search (Chapter 5)""" -from collections import namedtuple +import copy +import itertools import random +from collections import namedtuple + +import numpy as np -from utils import argmax -from canvas import Canvas +from utils import vector_add -infinity = float('inf') GameState = namedtuple('GameState', 'to_move, utility, board, moves') +StochasticGameState = namedtuple('StochasticGameState', 'to_move, utility, board, moves, chance') + # ______________________________________________________________________________ -# Minimax Search +# MinMax Search -def minimax_decision(state, game): +def minmax_decision(state, game): """Given a state in a game, calculate the best move by searching forward all the way to the terminal states. [Figure 5.3]""" @@ -22,7 +26,7 @@ def minimax_decision(state, game): def max_value(state): if game.terminal_test(state): return game.utility(state, player) - v = -infinity + v = -np.inf for a in game.actions(state): v = max(v, min_value(game.result(state, a))) return v @@ -30,29 +34,69 @@ def max_value(state): def min_value(state): if game.terminal_test(state): return game.utility(state, player) - v = infinity + v = np.inf for a in game.actions(state): v = min(v, max_value(game.result(state, a))) return v - # Body of minimax_decision: - return argmax(game.actions(state), - key=lambda a: min_value(game.result(state, a))) + # Body of minmax_decision: + return max(game.actions(state), key=lambda a: min_value(game.result(state, a))) + # ______________________________________________________________________________ -def alphabeta_full_search(state, game): +def expect_minmax(state, game): + """ + [Figure 5.11] + Return the best move for a player after dice are thrown. The game tree + includes chance nodes along with min and max nodes. + """ + player = game.to_move(state) + + def max_value(state): + v = -np.inf + for a in game.actions(state): + v = max(v, chance_node(state, a)) + return v + + def min_value(state): + v = np.inf + for a in game.actions(state): + v = min(v, chance_node(state, a)) + return v + + def chance_node(state, action): + res_state = game.result(state, action) + if game.terminal_test(res_state): + return game.utility(res_state, player) + sum_chances = 0 + num_chances = len(game.chances(res_state)) + for chance in game.chances(res_state): + res_state = game.outcome(res_state, chance) + util = 0 + if res_state.to_move == player: + util = max_value(res_state) + else: + util = min_value(res_state) + sum_chances += util * game.probability(chance) + return sum_chances / num_chances + + # Body of expect_minmax: + return max(game.actions(state), key=lambda a: chance_node(state, a), default=None) + + +def alpha_beta_search(state, game): """Search game to determine best action; use alpha-beta pruning. As in [Figure 5.7], this version searches all the way to the leaves.""" player = game.to_move(state) - # Functions used by alphabeta + # Functions used by alpha_beta def max_value(state, alpha, beta): if game.terminal_test(state): return game.utility(state, player) - v = -infinity + v = -np.inf for a in game.actions(state): v = max(v, min_value(game.result(state, a), alpha, beta)) if v >= beta: @@ -63,7 +107,7 @@ def max_value(state, alpha, beta): def min_value(state, alpha, beta): if game.terminal_test(state): return game.utility(state, player) - v = infinity + v = np.inf for a in game.actions(state): v = min(v, max_value(game.result(state, a), alpha, beta)) if v <= alpha: @@ -71,9 +115,9 @@ def min_value(state, alpha, beta): beta = min(beta, v) return v - # Body of alphabeta_search: - best_score = -infinity - beta = infinity + # Body of alpha_beta_search: + best_score = -np.inf + beta = np.inf best_action = None for a in game.actions(state): v = min_value(game.result(state, a), best_score, beta) @@ -83,20 +127,19 @@ def min_value(state, alpha, beta): return best_action -def alphabeta_search(state, game, d=4, cutoff_test=None, eval_fn=None): +def alpha_beta_cutoff_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 alpha_beta def max_value(state, alpha, beta, depth): if cutoff_test(state, depth): return eval_fn(state) - v = -infinity + v = -np.inf for a in game.actions(state): - v = max(v, min_value(game.result(state, a), - alpha, beta, depth + 1)) + v = max(v, min_value(game.result(state, a), alpha, beta, depth + 1)) if v >= beta: return v alpha = max(alpha, v) @@ -105,23 +148,20 @@ def max_value(state, alpha, beta, depth): def min_value(state, alpha, beta, depth): if cutoff_test(state, depth): return eval_fn(state) - v = infinity + v = np.inf for a in game.actions(state): - v = min(v, max_value(game.result(state, a), - alpha, beta, depth + 1)) + v = min(v, max_value(game.result(state, a), alpha, beta, depth + 1)) if v <= alpha: return v beta = min(beta, v) return v - # Body of alphabeta_search starts here: + # Body of alpha_beta_cutoff_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))) + cutoff_test = (cutoff_test or (lambda state, depth: depth > d or game.terminal_test(state))) eval_fn = eval_fn or (lambda state: game.utility(state, player)) - best_score = -infinity - beta = infinity + best_score = -np.inf + beta = np.inf best_action = None for a in game.actions(state): v = min_value(game.result(state, a), best_score, beta, 1) @@ -130,40 +170,45 @@ def min_value(state, alpha, beta, depth): best_action = a return best_action + # ______________________________________________________________________________ # Players for Games def query_player(game, state): - "Make a move by querying standard input." - move_string = input('Your move? ') - try: - move = eval(move_string) - except NameError: - move = move_string + """Make a move by querying standard input.""" + print("current state:") + game.display(state) + print("available moves: {}".format(game.actions(state))) + print("") + move = None + if game.actions(state): + move_string = input('Your move? ') + try: + move = eval(move_string) + except NameError: + move = move_string + else: + print('no legal moves: passing turn to next player') return move def random_player(game, state): - "A player that chooses a legal move at random." - return random.choice(game.actions(state)) + """A player that chooses a legal move at random.""" + return random.choice(game.actions(state)) if game.actions(state) else None -def alphabeta_player(game, state): - return alphabeta_full_search(state, game) +def alpha_beta_player(game, state): + return alpha_beta_search(state, game) -def play_game(game, *players): - """Play an n-person, move-alternating game.""" +def minmax_player(game,state): + return minmax_decision(state,game) + + +def expect_minmax_player(game, state): + return expect_minmax(state, game) - state = game.initial - while True: - for player in players: - move = player(game, state) - state = game.result(state, move) - if game.terminal_test(state): - game.display(state) - return game.utility(state, game.to_move(game.initial)) # ______________________________________________________________________________ # Some Sample Games @@ -179,31 +224,74 @@ class Game: be done in the constructor.""" def actions(self, state): - "Return a list of the allowable moves at this point." + """Return a list of the allowable moves at this point.""" raise NotImplementedError def result(self, state, move): - "Return the state that results from making a move from a state." + """Return the state that results from making a move from a state.""" raise NotImplementedError def utility(self, state, player): - "Return the value of this final state to player." + """Return the value of this final state to player.""" raise NotImplementedError def terminal_test(self, state): - "Return True if this is a final state for the game." + """Return True if this is a final state for the game.""" return not self.actions(state) def to_move(self, state): - "Return the player whose move it is in this state." + """Return the player whose move it is in this state.""" return state.to_move def display(self, state): - "Print or otherwise display the state." + """Print or otherwise display the state.""" print(state) def __repr__(self): - return '<%s>' % self.__class__.__name__ + return '<{}>'.format(self.__class__.__name__) + + def play_game(self, *players): + """Play an n-person, move-alternating game.""" + state = self.initial + while True: + for player in players: + move = player(self, state) + state = self.result(state, move) + if self.terminal_test(state): + self.display(state) + return self.utility(state, self.to_move(self.initial)) + + +class StochasticGame(Game): + """A stochastic game includes uncertain events which influence + the moves of players at each state. To create a stochastic game, subclass + this class and implement chances and outcome along with the other + unimplemented game class methods.""" + + def chances(self, state): + """Return a list of all possible uncertain events at a state.""" + raise NotImplementedError + + def outcome(self, state, chance): + """Return the state which is the outcome of a chance trial.""" + raise NotImplementedError + + def probability(self, chance): + """Return the probability of occurrence of a chance.""" + raise NotImplementedError + + def play_game(self, *players): + """Play an n-person, move-alternating stochastic game.""" + state = self.initial + while True: + for player in players: + chance = random.choice(self.chances(state)) + state = self.outcome(state, chance) + move = player(self, state) + state = self.result(state, move) + if self.terminal_test(state): + self.display(state) + return self.utility(state, self.to_move(self.initial)) class Fig52Game(Game): @@ -235,6 +323,31 @@ def to_move(self, state): return 'MIN' if state in 'BCD' else 'MAX' +class Fig52Extended(Game): + """Similar to Fig52Game but bigger. Useful for visualisation""" + + succs = {i: dict(l=i * 3 + 1, m=i * 3 + 2, r=i * 3 + 3) for i in range(13)} + utils = dict() + + def actions(self, state): + return sorted(list(self.succs.get(state, {}).keys())) + + def result(self, state, move): + return self.succs[state][move] + + def utility(self, state, player): + if player == 'MAX': + return self.utils[state] + else: + return -self.utils[state] + + def terminal_test(self, state): + return state not in range(13) + + def to_move(self, state): + return 'MIN' if state in {1, 2, 3} 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 @@ -250,7 +363,7 @@ def __init__(self, h=3, v=3, k=3): self.initial = GameState(to_move='X', utility=0, board={}, moves=moves) def actions(self, state): - "Legal moves are any square not yet taken." + """Legal moves are any square not yet taken.""" return state.moves def result(self, state, move): @@ -265,11 +378,11 @@ def result(self, state, move): board=board, moves=moves) def utility(self, state, player): - "Return the value to player; 1 for win, -1 for loss, 0 otherwise." + """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" return state.utility if player == 'X' else -state.utility def terminal_test(self, state): - "A state is terminal if it is won or there are no empty squares." + """A state is terminal if it is won or there are no empty squares.""" return state.utility != 0 or len(state.moves) == 0 def display(self, state): @@ -280,7 +393,7 @@ def display(self, state): print() def compute_utility(self, board, move, player): - "If 'X' wins with this move, return 1; if 'O' wins return -1; else return 0." + """If 'X' wins with this move, return 1; if 'O' wins 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 @@ -290,7 +403,7 @@ def compute_utility(self, board, move, player): return 0 def k_in_row(self, board, move, player, delta_x_y): - "Return true if there is a line through move on board for player." + """Return true if there is a line through move on board for player.""" (delta_x, delta_y) = delta_x_y x, y = move n = 0 # n is number of moves in row @@ -315,79 +428,163 @@ def __init__(self, h=7, v=6, k=4): def actions(self, state): return [(x, y) for (x, y) in state.moves - if y == 1 or (x, y - 1) in state.board] + if x == self.h or (x + 1 , y ) in state.board] +class Gomoku(TicTacToe): + """Also known as Five in a row.""" -class Canvas_TicTacToe(Canvas): - """Play a 3x3 TicTacToe game on HTML canvas - TODO: Add restart button - """ - def __init__(self, varname, player_1='human', player_2='random', id=None, width=300, height=300): - valid_players = ('human', 'random', 'alphabeta') - if player_1 not in valid_players or player_2 not in valid_players: - raise TypeError("Players must be one of {}".format(valid_players)) - Canvas.__init__(self, varname, id, width, height) - self.ttt = TicTacToe() - self.state = self.ttt.initial - self.turn = 0 - self.strokeWidth(5) - self.players = (player_1, player_2) - self.draw_board() - self.font("Ariel 30px") - - def mouse_click(self, x, y): - player = self.players[self.turn] - if self.ttt.terminal_test(self.state): - return - - if player == 'human': - x, y = int(3*x/self.width) + 1, int(3*y/self.height) + 1 - if (x, y) not in self.ttt.actions(self.state): - # Invalid move - return - move = (x, y) - elif player == 'alphabeta': - move = alphabeta_player(self.ttt, self.state) + def __init__(self, h=15, v=16, k=5): + TicTacToe.__init__(self, h, v, k) + + +class Backgammon(StochasticGame): + """A two player game where the goal of each player is to move all the + checkers off the board. The moves for each state are determined by + rolling a pair of dice.""" + + def __init__(self): + """Initial state of the game""" + point = {'W': 0, 'B': 0} + board = [point.copy() for index in range(24)] + board[0]['B'] = board[23]['W'] = 2 + board[5]['W'] = board[18]['B'] = 5 + board[7]['W'] = board[16]['B'] = 3 + board[11]['B'] = board[12]['W'] = 5 + self.allow_bear_off = {'W': False, 'B': False} + self.direction = {'W': -1, 'B': 1} + self.initial = StochasticGameState(to_move='W', + utility=0, + board=board, + moves=self.get_all_moves(board, 'W'), chance=None) + + def actions(self, state): + """Return a list of legal moves for a state.""" + player = state.to_move + moves = state.moves + if len(moves) == 1 and len(moves[0]) == 1: + return moves + legal_moves = [] + for move in moves: + board = copy.deepcopy(state.board) + if self.is_legal_move(board, move, state.chance, player): + legal_moves.append(move) + return legal_moves + + def result(self, state, move): + board = copy.deepcopy(state.board) + player = state.to_move + self.move_checker(board, move[0], state.chance[0], player) + if len(move) == 2: + self.move_checker(board, move[1], state.chance[1], player) + to_move = ('W' if player == 'B' else 'B') + return StochasticGameState(to_move=to_move, + utility=self.compute_utility(board, move, player), + board=board, + moves=self.get_all_moves(board, to_move), chance=None) + + def utility(self, state, player): + """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" + return state.utility if player == 'W' else -state.utility + + def terminal_test(self, state): + """A state is terminal if one player wins.""" + return state.utility != 0 + + def get_all_moves(self, board, player): + """All possible moves for a player i.e. all possible ways of + choosing two checkers of a player from the board for a move + at a given state.""" + all_points = board + taken_points = [index for index, point in enumerate(all_points) + if point[player] > 0] + if self.checkers_at_home(board, player) == 1: + return [(taken_points[0],)] + moves = list(itertools.permutations(taken_points, 2)) + moves = moves + [(index, index) for index, point in enumerate(all_points) + if point[player] >= 2] + return moves + + def display(self, state): + """Display state of the game.""" + board = state.board + player = state.to_move + print("current state : ") + for index, point in enumerate(board): + print("point : ", index, " W : ", point['W'], " B : ", point['B']) + print("to play : ", player) + + def compute_utility(self, board, move, player): + """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0.""" + util = {'W': 1, 'B': -1} + for idx in range(0, 24): + if board[idx][player] > 0: + return 0 + return util[player] + + def checkers_at_home(self, board, player): + """Return the no. of checkers at home for a player.""" + sum_range = range(0, 7) if player == 'W' else range(17, 24) + count = 0 + for idx in sum_range: + count = count + board[idx][player] + return count + + def is_legal_move(self, board, start, steps, player): + """Move is a tuple which contains starting points of checkers to be + moved during a player's turn. An on-board move is legal if both the destinations + are open. A bear-off move is the one where a checker is moved off-board. + It is legal only after a player has moved all his checkers to his home.""" + dest1, dest2 = vector_add(start, steps) + dest_range = range(0, 24) + move1_legal = move2_legal = False + if dest1 in dest_range: + if self.is_point_open(player, board[dest1]): + self.move_checker(board, start[0], steps[0], player) + move1_legal = True else: - move = random_player(self.ttt, self.state) - self.state = self.ttt.result(self.state, move) - self.turn ^= 1 - self.draw_board() - - def draw_board(self): - self.clear() - self.stroke(0, 0, 0) - offset = 1/20 - self.line_n(0 + offset, 1/3, 1 - offset, 1/3) - self.line_n(0 + offset, 2/3, 1 - offset, 2/3) - self.line_n(1/3, 0 + offset, 1/3, 1 - offset) - self.line_n(2/3, 0 + offset, 2/3, 1 - offset) - board = self.state.board - for mark in board: - if board[mark] == 'X': - self.draw_x(mark) - elif board[mark] == 'O': - self.draw_o(mark) - if self.ttt.terminal_test(self.state): - # End game message - utility = self.ttt.utility(self.state, self.ttt.to_move(self.ttt.initial)) - if utility == 0: - self.text_n('Game Draw!', 0.1, 0.1) - else: - self.text_n('Player {} wins!'.format(1 if utility > 0 else 2), 0.1, 0.1) - else: # Print which player's turn it is - self.text_n("Player {}'s move({})".format(self.turn+1, self.players[self.turn]), 0.1, 0.1) - - self.update() - - def draw_x(self, position): - self.stroke(0, 255, 0) - x, y = [i-1 for i in position] - offset = 1/15 - self.line_n(x/3 + offset, y/3 + offset, x/3 + 1/3 - offset, y/3 + 1/3 - offset) - self.line_n(x/3 + 1/3 - offset, y/3 + offset, x/3 + offset, y/3 + 1/3 - offset) - - def draw_o(self, position): - self.stroke(255, 0, 0) - x, y = [i-1 for i in position] - self.arc_n(x/3 + 1/6, y/3 + 1/6, 1/9, 0, 360) + if self.allow_bear_off[player]: + self.move_checker(board, start[0], steps[0], player) + move1_legal = True + if not move1_legal: + return False + if dest2 in dest_range: + if self.is_point_open(player, board[dest2]): + move2_legal = True + else: + if self.allow_bear_off[player]: + move2_legal = True + return move1_legal and move2_legal + + def move_checker(self, board, start, steps, player): + """Move a checker from starting point by a given number of steps""" + dest = start + steps + dest_range = range(0, 24) + board[start][player] -= 1 + if dest in dest_range: + board[dest][player] += 1 + if self.checkers_at_home(board, player) == 15: + self.allow_bear_off[player] = True + + def is_point_open(self, player, point): + """A point is open for a player if the no. of opponent's + checkers already present on it is 0 or 1. A player can + move a checker to a point only if it is open.""" + opponent = 'B' if player == 'W' else 'W' + return point[opponent] <= 1 + + def chances(self, state): + """Return a list of all possible dice rolls at a state.""" + dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2)) + return dice_rolls + + def outcome(self, state, chance): + """Return the state which is the outcome of a dice roll.""" + dice = tuple(map((self.direction[state.to_move]).__mul__, chance)) + return StochasticGameState(to_move=state.to_move, + utility=state.utility, + board=state.board, + moves=state.moves, chance=dice) + + def probability(self, chance): + """Return the probability of occurrence of a dice roll.""" + return 1 / 36 if chance[0] == chance[1] else 1 / 18 diff --git a/games4e.ipynb b/games4e.ipynb new file mode 100644 index 000000000..5b619f7ed --- /dev/null +++ b/games4e.ipynb @@ -0,0 +1,1667 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Game Tree Search\n", + "\n", + "We start with defining the abstract class `Game`, for turn-taking *n*-player games. We rely on, but do not define yet, the concept of a `state` of the game; we'll see later how individual games define states. For now, all we require is that a state has a `state.to_move` attribute, which gives the name of the player whose turn it is. (\"Name\" will be something like `'X'` or `'O'` for tic-tac-toe.) \n", + "\n", + "We also define `play_game`, which takes a game and a dictionary of `{player_name: strategy_function}` pairs, and plays out the game, on each turn checking `state.to_move` to see whose turn it is, and then getting the strategy function for that player and applying it to the game and the state to get a move." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import namedtuple, Counter, defaultdict\n", + "import random\n", + "import math\n", + "import functools \n", + "cache = functools.lru_cache(10**6)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [], + "source": [ + "class Game:\n", + " \"\"\"A game is similar to a problem, but it has a terminal test instead of \n", + " a goal test, and a utility for each terminal state. To create a game, \n", + " subclass this class and implement `actions`, `result`, `is_terminal`, \n", + " and `utility`. You will also need to set the .initial attribute to the \n", + " initial state; this can be done in the constructor.\"\"\"\n", + "\n", + " def actions(self, state):\n", + " \"\"\"Return a collection of the allowable moves from this state.\"\"\"\n", + " raise NotImplementedError\n", + "\n", + " def result(self, state, move):\n", + " \"\"\"Return the state that results from making a move from a state.\"\"\"\n", + " raise NotImplementedError\n", + "\n", + " def is_terminal(self, state):\n", + " \"\"\"Return True if this is a final state for the game.\"\"\"\n", + " return not self.actions(state)\n", + " \n", + " def utility(self, state, player):\n", + " \"\"\"Return the value of this final state to player.\"\"\"\n", + " raise NotImplementedError\n", + " \n", + "\n", + "def play_game(game, strategies: dict, verbose=False):\n", + " \"\"\"Play a turn-taking game. `strategies` is a {player_name: function} dict,\n", + " where function(state, game) is used to get the player's move.\"\"\"\n", + " state = game.initial\n", + " while not game.is_terminal(state):\n", + " player = state.to_move\n", + " move = strategies[player](game, state)\n", + " state = game.result(state, move)\n", + " if verbose: \n", + " print('Player', player, 'move:', move)\n", + " print(state)\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Minimax-Based Game Search Algorithms\n", + "\n", + "We will define several game search algorithms. Each takes two inputs, the game we are playing and the current state of the game, and returns a a `(value, move)` pair, where `value` is the utility that the algorithm computes for the player whose turn it is to move, and `move` is the move itself.\n", + "\n", + "First we define `minimax_search`, which exhaustively searches the game tree to find an optimal move (assuming both players play optimally), and `alphabeta_search`, which does the same computation, but prunes parts of the tree that could not possibly have an affect on the optimnal move. " + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "def minimax_search(game, state):\n", + " \"\"\"Search game tree to determine best move; return (value, move) pair.\"\"\"\n", + "\n", + " player = state.to_move\n", + "\n", + " def max_value(state):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = -infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = min_value(game.result(state, a))\n", + " if v2 > v:\n", + " v, move = v2, a\n", + " return v, move\n", + "\n", + " def min_value(state):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = +infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = max_value(game.result(state, a))\n", + " if v2 < v:\n", + " v, move = v2, a\n", + " return v, move\n", + "\n", + " return max_value(state)\n", + "\n", + "infinity = math.inf\n", + "\n", + "def alphabeta_search(game, state):\n", + " \"\"\"Search game to determine best action; use alpha-beta pruning.\n", + " As in [Figure 5.7], this version searches all the way to the leaves.\"\"\"\n", + "\n", + " player = state.to_move\n", + "\n", + " def max_value(state, alpha, beta):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = -infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = min_value(game.result(state, a), alpha, beta)\n", + " if v2 > v:\n", + " v, move = v2, a\n", + " alpha = max(alpha, v)\n", + " if v >= beta:\n", + " return v, move\n", + " return v, move\n", + "\n", + " def min_value(state, alpha, beta):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = +infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = max_value(game.result(state, a), alpha, beta)\n", + " if v2 < v:\n", + " v, move = v2, a\n", + " beta = min(beta, v)\n", + " if v <= alpha:\n", + " return v, move\n", + " return v, move\n", + "\n", + " return max_value(state, -infinity, +infinity)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Simple Game: Tic-Tac-Toe\n", + "\n", + "We have the notion of an abstract game, we have some search functions; now it is time to define a real game; a simple one, tic-tac-toe. Moves are `(x, y)` pairs denoting squares, where `(0, 0)` is the top left, and `(2, 2)` is the bottom right (on a board of size `height=width=3`)." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "class TicTacToe(Game):\n", + " \"\"\"Play TicTacToe on an `height` by `width` board, needing `k` in a row to win.\n", + " 'X' plays first against 'O'.\"\"\"\n", + "\n", + " def __init__(self, height=3, width=3, k=3):\n", + " self.k = k # k in a row\n", + " self.squares = {(x, y) for x in range(width) for y in range(height)}\n", + " self.initial = Board(height=height, width=width, to_move='X', utility=0)\n", + "\n", + " def actions(self, board):\n", + " \"\"\"Legal moves are any square not yet taken.\"\"\"\n", + " return self.squares - set(board)\n", + "\n", + " def result(self, board, square):\n", + " \"\"\"Place a marker for current player on square.\"\"\"\n", + " player = board.to_move\n", + " board = board.new({square: player}, to_move=('O' if player == 'X' else 'X'))\n", + " win = k_in_row(board, player, square, self.k)\n", + " board.utility = (0 if not win else +1 if player == 'X' else -1)\n", + " return board\n", + "\n", + " def utility(self, board, player):\n", + " \"\"\"Return the value to player; 1 for win, -1 for loss, 0 otherwise.\"\"\"\n", + " return board.utility if player == 'X' else -board.utility\n", + "\n", + " def is_terminal(self, board):\n", + " \"\"\"A board is a terminal state if it is won or there are no empty squares.\"\"\"\n", + " return board.utility != 0 or len(self.squares) == len(board)\n", + "\n", + " def display(self, board): print(board) \n", + "\n", + "\n", + "def k_in_row(board, player, square, k):\n", + " \"\"\"True if player has k pieces in a line through square.\"\"\"\n", + " def in_row(x, y, dx, dy): return 0 if board[x, y] != player else 1 + in_row(x + dx, y + dy, dx, dy)\n", + " return any(in_row(*square, dx, dy) + in_row(*square, -dx, -dy) - 1 >= k\n", + " for (dx, dy) in ((0, 1), (1, 0), (1, 1), (1, -1)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "States in tic-tac-toe (and other games) will be represented as a `Board`, which is a subclass of `defaultdict` that in general will consist of `{(x, y): contents}` pairs, for example `{(0, 0): 'X', (1, 1): 'O'}` might be the state of the board after two moves. Besides the contents of squares, a board also has some attributes: \n", + "- `.to_move` to name the player whose move it is; \n", + "- `.width` and `.height` to give the size of the board (both 3 in tic-tac-toe, but other numbers in related games);\n", + "- possibly other attributes, as specified by keywords. \n", + "\n", + "As a `defaultdict`, the `Board` class has a `__missing__` method, which returns `empty` for squares that have no been assigned but are within the `width` × `height` boundaries, or `off` otherwise. The class has a `__hash__` method, so instances can be stored in hash tables." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [], + "source": [ + "class Board(defaultdict):\n", + " \"\"\"A board has the player to move, a cached utility value, \n", + " and a dict of {(x, y): player} entries, where player is 'X' or 'O'.\"\"\"\n", + " empty = '.'\n", + " off = '#'\n", + " \n", + " def __init__(self, width=8, height=8, to_move=None, **kwds):\n", + " self.__dict__.update(width=width, height=height, to_move=to_move, **kwds)\n", + " \n", + " def new(self, changes: dict, **kwds) -> 'Board':\n", + " \"Given a dict of {(x, y): contents} changes, return a new Board with the changes.\"\n", + " board = Board(width=self.width, height=self.height, **kwds)\n", + " board.update(self)\n", + " board.update(changes)\n", + " return board\n", + "\n", + " def __missing__(self, loc):\n", + " x, y = loc\n", + " if 0 <= x < self.width and 0 <= y < self.height:\n", + " return self.empty\n", + " else:\n", + " return self.off\n", + " \n", + " def __hash__(self): \n", + " return hash(tuple(sorted(self.items()))) + hash(self.to_move)\n", + " \n", + " def __repr__(self):\n", + " def row(y): return ' '.join(self[x, y] for x in range(self.width))\n", + " return '\\n'.join(map(row, range(self.height))) + '\\n'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Players\n", + "\n", + "We need an interface for players. I'll represent a player as a `callable` that will be passed two arguments: `(game, state)` and will return a `move`.\n", + "The function `player` creates a player out of a search algorithm, but you can create your own players as functions, as is done with `random_player` below:" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [], + "source": [ + "def random_player(game, state): return random.choice(list(game.actions(state)))\n", + "\n", + "def player(search_algorithm):\n", + " \"\"\"A game player who uses the specified search algorithm\"\"\"\n", + " return lambda game, state: search_algorithm(game, state)[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Playing a Game\n", + "\n", + "We're ready to play a game. I'll set up a match between a `random_player` (who chooses randomly from the legal moves) and a `player(alphabeta_search)` (who makes the optimal alpha-beta move; practical for tic-tac-toe, but not for large games). The `player(alphabeta_search)` will never lose, but if `random_player` is lucky, it will be a tie." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player X move: (0, 0)\n", + "X . .\n", + ". . .\n", + ". . .\n", + "\n", + "Player O move: (1, 1)\n", + "X . .\n", + ". O .\n", + ". . .\n", + "\n", + "Player X move: (1, 2)\n", + "X . .\n", + ". O .\n", + ". X .\n", + "\n", + "Player O move: (0, 1)\n", + "X . .\n", + "O O .\n", + ". X .\n", + "\n", + "Player X move: (2, 1)\n", + "X . .\n", + "O O X\n", + ". X .\n", + "\n", + "Player O move: (2, 0)\n", + "X . O\n", + "O O X\n", + ". X .\n", + "\n", + "Player X move: (2, 2)\n", + "X . O\n", + "O O X\n", + ". X X\n", + "\n", + "Player O move: (0, 2)\n", + "X . O\n", + "O O X\n", + "O X X\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play_game(TicTacToe(), dict(X=random_player, O=player(alphabeta_search)), verbose=True).utility" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The alpha-beta player will never lose, but sometimes the random player can stumble into a draw. When two optimal (alpha-beta or minimax) players compete, it will always be a draw:" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player X move: (0, 1)\n", + ". . .\n", + "X . .\n", + ". . .\n", + "\n", + "Player O move: (0, 0)\n", + "O . .\n", + "X . .\n", + ". . .\n", + "\n", + "Player X move: (2, 0)\n", + "O . X\n", + "X . .\n", + ". . .\n", + "\n", + "Player O move: (2, 1)\n", + "O . X\n", + "X . O\n", + ". . .\n", + "\n", + "Player X move: (1, 2)\n", + "O . X\n", + "X . O\n", + ". X .\n", + "\n", + "Player O move: (0, 2)\n", + "O . X\n", + "X . O\n", + "O X .\n", + "\n", + "Player X move: (1, 0)\n", + "O X X\n", + "X . O\n", + "O X .\n", + "\n", + "Player O move: (1, 1)\n", + "O X X\n", + "X O O\n", + "O X .\n", + "\n", + "Player X move: (2, 2)\n", + "O X X\n", + "X O O\n", + "O X X\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play_game(TicTacToe(), dict(X=player(alphabeta_search), O=player(minimax_search)), verbose=True).utility" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Connect Four\n", + "\n", + "Connect Four is a variant of tic-tac-toe, played on a larger (7 x 6) board, and with the restriction that in any column you can only play in the lowest empty square in the column." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [], + "source": [ + "class ConnectFour(TicTacToe):\n", + " \n", + " def __init__(self): super().__init__(width=7, height=6, k=4)\n", + "\n", + " def actions(self, board):\n", + " \"\"\"In each column you can play only the lowest empty square in the column.\"\"\"\n", + " return {(x, y) for (x, y) in self.squares - set(board)\n", + " if y == board.height - 1 or (x, y + 1) in board}" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player X move: (2, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . . .\n", + "\n", + "Player O move: (1, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O X . . . .\n", + "\n", + "Player X move: (5, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O X . . X .\n", + "\n", + "Player O move: (4, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O X . O X .\n", + "\n", + "Player X move: (4, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O X . O X .\n", + "\n", + "Player O move: (2, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . O . X . .\n", + ". O X . O X .\n", + "\n", + "Player X move: (2, 3)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . . .\n", + ". . O . X . .\n", + ". O X . O X .\n", + "\n", + "Player O move: (1, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . . .\n", + ". O O . X . .\n", + ". O X . O X .\n", + "\n", + "Player X move: (0, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . . .\n", + ". O O . X . .\n", + "X O X . O X .\n", + "\n", + "Player O move: (5, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . . .\n", + ". O O . X O .\n", + "X O X . O X .\n", + "\n", + "Player X move: (5, 3)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . X .\n", + ". O O . X O .\n", + "X O X . O X .\n", + "\n", + "Player O move: (6, 5)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . X . . X .\n", + ". O O . X O .\n", + "X O X . O X O\n", + "\n", + "Player X move: (1, 3)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". X X . . X .\n", + ". O O . X O .\n", + "X O X . O X O\n", + "\n", + "Player O move: (6, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". X X . . X .\n", + ". O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (5, 2)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + ". X X . . X .\n", + ". O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player O move: (0, 4)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + ". X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (0, 3)\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + "X X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player O move: (0, 2)\n", + ". . . . . . .\n", + ". . . . . . .\n", + "O . . . . X .\n", + "X X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (0, 1)\n", + ". . . . . . .\n", + "X . . . . . .\n", + "O . . . . X .\n", + "X X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player O move: (0, 0)\n", + "O . . . . . .\n", + "X . . . . . .\n", + "O . . . . X .\n", + "X X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (5, 1)\n", + "O . . . . . .\n", + "X . . . . X .\n", + "O . . . . X .\n", + "X X X . . X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player O move: (4, 3)\n", + "O . . . . . .\n", + "X . . . . X .\n", + "O . . . . X .\n", + "X X X . O X .\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (6, 3)\n", + "O . . . . . .\n", + "X . . . . X .\n", + "O . . . . X .\n", + "X X X . O X X\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player O move: (5, 0)\n", + "O . . . . O .\n", + "X . . . . X .\n", + "O . . . . X .\n", + "X X X . O X X\n", + "O O O . X O O\n", + "X O X . O X O\n", + "\n", + "Player X move: (3, 5)\n", + "O . . . . O .\n", + "X . . . . X .\n", + "O . . . . X .\n", + "X X X . O X X\n", + "O O O . X O O\n", + "X O X X O X O\n", + "\n", + "Player O move: (1, 2)\n", + "O . . . . O .\n", + "X . . . . X .\n", + "O O . . . X .\n", + "X X X . O X X\n", + "O O O . X O O\n", + "X O X X O X O\n", + "\n", + "Player X move: (1, 1)\n", + "O . . . . O .\n", + "X X . . . X .\n", + "O O . . . X .\n", + "X X X . O X X\n", + "O O O . X O O\n", + "X O X X O X O\n", + "\n", + "Player O move: (3, 4)\n", + "O . . . . O .\n", + "X X . . . X .\n", + "O O . . . X .\n", + "X X X . O X X\n", + "O O O O X O O\n", + "X O X X O X O\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "play_game(ConnectFour(), dict(X=random_player, O=random_player), verbose=True).utility" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Transposition Tables\n", + "\n", + "By treating the game tree as a tree, we can arrive at the same state through different paths, and end up duplicating effort. In state-space search, we kept a table of `reached` states to prevent this. For game-tree search, we can achieve the same effect by applying the `@cache` decorator to the `min_value` and `max_value` functions. We'll use the suffix `_tt` to indicate a function that uses these transisiton tables." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def minimax_search_tt(game, state):\n", + " \"\"\"Search game to determine best move; return (value, move) pair.\"\"\"\n", + "\n", + " player = state.to_move\n", + "\n", + " @cache\n", + " def max_value(state):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = -infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = min_value(game.result(state, a))\n", + " if v2 > v:\n", + " v, move = v2, a\n", + " return v, move\n", + "\n", + " @cache\n", + " def min_value(state):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = +infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = max_value(game.result(state, a))\n", + " if v2 < v:\n", + " v, move = v2, a\n", + " return v, move\n", + "\n", + " return max_value(state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For alpha-beta search, we can still use a cache, but it should be based just on the state, not on whatever values alpha and beta have." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [], + "source": [ + "def cache1(function):\n", + " \"Like lru_cache(None), but only considers the first argument of function.\"\n", + " cache = {}\n", + " def wrapped(x, *args):\n", + " if x not in cache:\n", + " cache[x] = function(x, *args)\n", + " return cache[x]\n", + " return wrapped\n", + "\n", + "def alphabeta_search_tt(game, state):\n", + " \"\"\"Search game to determine best action; use alpha-beta pruning.\n", + " As in [Figure 5.7], this version searches all the way to the leaves.\"\"\"\n", + "\n", + " player = state.to_move\n", + "\n", + " @cache1\n", + " def max_value(state, alpha, beta):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = -infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = min_value(game.result(state, a), alpha, beta)\n", + " if v2 > v:\n", + " v, move = v2, a\n", + " alpha = max(alpha, v)\n", + " if v >= beta:\n", + " return v, move\n", + " return v, move\n", + "\n", + " @cache1\n", + " def min_value(state, alpha, beta):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " v, move = +infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = max_value(game.result(state, a), alpha, beta)\n", + " if v2 < v:\n", + " v, move = v2, a\n", + " beta = min(beta, v)\n", + " if v <= alpha:\n", + " return v, move\n", + " return v, move\n", + "\n", + " return max_value(state, -infinity, +infinity)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 593 ms, sys: 52 ms, total: 645 ms\n", + "Wall time: 655 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "O X X\n", + "X O O\n", + "O X X" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time play_game(TicTacToe(), {'X':player(alphabeta_search_tt), 'O':player(minimax_search_tt)})" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.07 s, sys: 30.7 ms, total: 3.1 s\n", + "Wall time: 3.15 s\n" + ] + }, + { + "data": { + "text/plain": [ + "O X X\n", + "X O O\n", + "O X X" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time play_game(TicTacToe(), {'X':player(alphabeta_search), 'O':player(minimax_search)})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Heuristic Cutoffs" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [], + "source": [ + "def cutoff_depth(d):\n", + " \"\"\"A cutoff function that searches to depth d.\"\"\"\n", + " return lambda game, state, depth: depth > d\n", + "\n", + "def h_alphabeta_search(game, state, cutoff=cutoff_depth(6), h=lambda s, p: 0):\n", + " \"\"\"Search game to determine best action; use alpha-beta pruning.\n", + " As in [Figure 5.7], this version searches all the way to the leaves.\"\"\"\n", + "\n", + " player = state.to_move\n", + "\n", + " @cache1\n", + " def max_value(state, alpha, beta, depth):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " if cutoff(game, state, depth):\n", + " return h(state, player), None\n", + " v, move = -infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = min_value(game.result(state, a), alpha, beta, depth+1)\n", + " if v2 > v:\n", + " v, move = v2, a\n", + " alpha = max(alpha, v)\n", + " if v >= beta:\n", + " return v, move\n", + " return v, move\n", + "\n", + " @cache1\n", + " def min_value(state, alpha, beta, depth):\n", + " if game.is_terminal(state):\n", + " return game.utility(state, player), None\n", + " if cutoff(game, state, depth):\n", + " return h(state, player), None\n", + " v, move = +infinity, None\n", + " for a in game.actions(state):\n", + " v2, _ = max_value(game.result(state, a), alpha, beta, depth + 1)\n", + " if v2 < v:\n", + " v, move = v2, a\n", + " beta = min(beta, v)\n", + " if v <= alpha:\n", + " return v, move\n", + " return v, move\n", + "\n", + " return max_value(state, -infinity, +infinity, 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 367 ms, sys: 7.9 ms, total: 375 ms\n", + "Wall time: 375 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "O X X\n", + "X O O\n", + "O X X" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time play_game(TicTacToe(), {'X':player(h_alphabeta_search), 'O':player(h_alphabeta_search)})" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . X\n", + ". . . . . X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O X\n", + ". . . . . X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O X\n", + ". . . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . . O X\n", + ". . . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . X O X\n", + ". . . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . X O X\n", + ". O . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". X . . X O X\n", + ". O . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O O\n", + ". X . . X O X\n", + ". O . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X O O\n", + ". X . . X O X\n", + ". O . . X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + ". . . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . . X .\n", + ". . . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . . X .\n", + ". X . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". X . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + ". O . O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". X . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + ". O O O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". X . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . . .\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . . X\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . . X O O\n", + ". X . . X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . . X\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . . X O O\n", + "O X . . X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . . X O O\n", + "O X . . X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . . X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . . O O\n", + ". X . . . X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . . O O\n", + ". X . . O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . . O O\n", + ". X . X O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . . O O O\n", + ". X . X O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . . X X\n", + ". . . X O O O\n", + ". X . X O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . . O X X\n", + ". . . X O O O\n", + ". X . X O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + ". . . X O X X\n", + ". . . X O O O\n", + ". X . X O X O\n", + ". X . X X O O\n", + "O X . O X O X\n", + "X O O O X X O\n", + "\n", + "CPU times: user 8.82 s, sys: 146 ms, total: 8.96 s\n", + "Wall time: 9.19 s\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time play_game(ConnectFour(), {'X':player(h_alphabeta_search), 'O':random_player}, verbose=True).utility" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . . X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . . . X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . O .\n", + ". . O . X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". . O . X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". . O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . X O .\n", + ". X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". X . . . . .\n", + ". O . . X O .\n", + ". X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . . . .\n", + ". X . . . . .\n", + ". O . . X O .\n", + ". X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . . . .\n", + ". X . . . . .\n", + ". O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . . . .\n", + ". X . . . . .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . . . .\n", + ". X . . X . .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . . . .\n", + ". O . . O . .\n", + ". X . . X . .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O . . O . .\n", + ". X . . X . .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O . . O . .\n", + ". X . . X O .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O . . O X .\n", + ". X . . X O .\n", + "O O . . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O . . O X .\n", + ". X . . X O .\n", + "O O O . X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X . .\n", + ". O . . O X .\n", + ". X . . X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". O . . O X .\n", + ". X . . X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". O . . O X .\n", + ". X . X X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . . X O .\n", + ". O . O O X .\n", + ". X . X X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . . . . .\n", + ". . . X X O .\n", + ". O . O O X .\n", + ". X . X X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . O . . .\n", + ". . . X X O .\n", + ". O . O O X .\n", + ". X . X X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + ". . . O . . .\n", + ". . . X X O .\n", + ". O . O O X .\n", + ". X X X X O .\n", + "O O O X X O .\n", + "X X O O X X .\n", + "\n", + "CPU times: user 12.1 s, sys: 237 ms, total: 12.4 s\n", + "Wall time: 12.9 s\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time play_game(ConnectFour(), {'X':player(h_alphabeta_search), 'O':player(h_alphabeta_search)}, verbose=True).utility" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Result states: 6,589; Terminal tests: 3,653; for alphabeta_search_tt\n", + "Result states: 25,703; Terminal tests: 25,704; for alphabeta_search\n", + "Result states: 4,687; Terminal tests: 2,805; for h_alphabeta_search\n", + "Result states: 16,167; Terminal tests: 5,478; for minimax_search_tt\n" + ] + } + ], + "source": [ + "class CountCalls:\n", + " \"\"\"Delegate all attribute gets to the object, and count them in ._counts\"\"\"\n", + " def __init__(self, obj):\n", + " self._object = obj\n", + " self._counts = Counter()\n", + " \n", + " def __getattr__(self, attr):\n", + " \"Delegate to the original object, after incrementing a counter.\"\n", + " self._counts[attr] += 1\n", + " return getattr(self._object, attr)\n", + " \n", + "def report(game, searchers):\n", + " for searcher in searchers:\n", + " game = CountCalls(game)\n", + " searcher(game, game.initial)\n", + " print('Result states: {:7,d}; Terminal tests: {:7,d}; for {}'.format(\n", + " game._counts['result'], game._counts['is_terminal'], searcher.__name__))\n", + " \n", + "report(TicTacToe(), (alphabeta_search_tt, alphabeta_search, h_alphabeta_search, minimax_search_tt))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Monte Carlo Tree Search" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Node:\n", + " def __init__(self, parent, )\n", + "def mcts(state, game, N=1000):" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Heuristic Search Algorithms" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "t = CountCalls(TicTacToe())\n", + " \n", + "play_game(t, dict(X=minimax_player, O=minimax_player), verbose=True)\n", + "t._counts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for tactic in (three, fork, center, opposite_corner, corner, any):\n", + " for s in squares:\n", + " if tactic(board, s,player): return s\n", + " for s ins quares:\n", + " if tactic(board, s, opponent): return s" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "def ucb(U, N, C=2**0.5, parentN=100):\n", + " return round(U/N + C * math.sqrt(math.log(parentN)/N), 2)\n", + "\n", + "{C: (ucb(60, 79, C), ucb(1, 10, C), ucb(2, 11, C)) \n", + " for C in (1.4, 1.5)}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def ucb(U, N, parentN=100, C=2):\n", + " return U/N + C * math.sqrt(math.log(parentN)/N)\n", + "\n", + "\n", + "C = 1.4 \n", + "\n", + "class Node:\n", + " def __init__(self, name, children=(), U=0, N=0, parent=None, p=0.5):\n", + " self.__dict__.update(name=name, U=U, N=N, parent=parent, children=children, p=p)\n", + " for c in children:\n", + " c.parent = self\n", + " \n", + " def __repr__(self):\n", + " return '{}:{}/{}={:.0%}{}'.format(self.name, self.U, self.N, self.U/self.N, self.children)\n", + " \n", + "def select(n):\n", + " if n.children:\n", + " return select(max(n.children, key=ucb))\n", + " else:\n", + " return n\n", + " \n", + "def back(n, amount):\n", + " if n:\n", + " n.N += 1\n", + " n.U += amount\n", + " back(n.parent, 1 - amount)\n", + " \n", + " \n", + "def one(root): \n", + " n = select(root)\n", + " amount = int(random.uniform(0, 1) < n.p)\n", + " back(n, amount)\n", + " \n", + "def ucb(n): \n", + " return (float('inf') if n.N == 0 else\n", + " n.U / n.N + C * math.sqrt(math.log(n.parent.N)/n.N))\n", + "\n", + "\n", + "tree = Node('root', [Node('a', p=.8, children=[Node('a1', p=.05), \n", + " Node('a2', p=.25,\n", + " children=[Node('a2a', p=.7), Node('a2b')])]),\n", + " Node('b', p=.5, children=[Node('b1', p=.6,\n", + " children=[Node('b1a', p=.3), Node('b1b')]), \n", + " Node('b2', p=.4)]),\n", + " Node('c', p=.1)])\n", + "\n", + "for i in range(100):\n", + " one(tree); \n", + "for c in tree.children: print(c)\n", + "'select', select(tree), 'tree', tree\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "us = (100, 50, 25, 10, 5, 1)\n", + "infinity = float('inf')\n", + "\n", + "@lru_cache(None)\n", + "def f1(n, denom):\n", + " return (0 if n == 0 else\n", + " infinity if n < 0 or not denom else\n", + " min(1 + f1(n - denom[0], denom),\n", + " f1(n, denom[1:])))\n", + " \n", + "@lru_cache(None)\n", + "def f2(n, denom):\n", + " @lru_cache(None)\n", + " def f(n):\n", + " return (0 if n == 0 else\n", + " infinity if n < 0 else\n", + " 1 + min(f(n - d) for d in denom))\n", + " return f(n)\n", + "\n", + "@lru_cache(None)\n", + "def f3(n, denom):\n", + " return (0 if n == 0 else\n", + " infinity if n < 0 or not denom else\n", + " min(k + f2(n - k * denom[0], denom[1:]) \n", + " for k in range(1 + n // denom[0])))\n", + " \n", + "\n", + "def g(n, d=us): return f1(n, d), f2(n, d), f3(n, d)\n", + " \n", + "n = 12345\n", + "%time f1(n, us)\n", + "%time f2(n, us)\n", + "%time f3(n, us)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/games4e.py b/games4e.py new file mode 100644 index 000000000..aba5b0eb3 --- /dev/null +++ b/games4e.py @@ -0,0 +1,635 @@ +"""Games or Adversarial Search (Chapter 5)""" + +import copy +import itertools +import random +from collections import namedtuple + +import numpy as np + +from utils4e import vector_add, MCT_Node, ucb + +GameState = namedtuple('GameState', 'to_move, utility, board, moves') +StochasticGameState = namedtuple('StochasticGameState', 'to_move, utility, board, moves, chance') + + +# ______________________________________________________________________________ +# MinMax Search + + +def minmax_decision(state, game): + """Given a state in a game, calculate the best move by searching + forward all the way to the terminal states. [Figure 5.3]""" + + player = game.to_move(state) + + def max_value(state): + if game.terminal_test(state): + return game.utility(state, player) + v = -np.inf + for a in game.actions(state): + v = max(v, min_value(game.result(state, a))) + return v + + def min_value(state): + if game.terminal_test(state): + return game.utility(state, player) + v = np.inf + for a in game.actions(state): + v = min(v, max_value(game.result(state, a))) + return v + + # Body of minmax_decision: + return max(game.actions(state), key=lambda a: min_value(game.result(state, a))) + + +# ______________________________________________________________________________ + + +def expect_minmax(state, game): + """ + [Figure 5.11] + Return the best move for a player after dice are thrown. The game tree + includes chance nodes along with min and max nodes. + """ + player = game.to_move(state) + + def max_value(state): + v = -np.inf + for a in game.actions(state): + v = max(v, chance_node(state, a)) + return v + + def min_value(state): + v = np.inf + for a in game.actions(state): + v = min(v, chance_node(state, a)) + return v + + def chance_node(state, action): + res_state = game.result(state, action) + if game.terminal_test(res_state): + return game.utility(res_state, player) + sum_chances = 0 + num_chances = len(game.chances(res_state)) + for chance in game.chances(res_state): + res_state = game.outcome(res_state, chance) + util = 0 + if res_state.to_move == player: + util = max_value(res_state) + else: + util = min_value(res_state) + sum_chances += util * game.probability(chance) + return sum_chances / num_chances + + # Body of expect_min_max: + return max(game.actions(state), key=lambda a: chance_node(state, a), default=None) + + +def alpha_beta_search(state, game): + """Search game to determine best action; use alpha-beta pruning. + As in [Figure 5.7], this version searches all the way to the leaves.""" + + player = game.to_move(state) + + # Functions used by alpha_beta + def max_value(state, alpha, beta): + if game.terminal_test(state): + return game.utility(state, player) + v = -np.inf + for a in game.actions(state): + v = max(v, min_value(game.result(state, a), alpha, beta)) + if v >= beta: + return v + alpha = max(alpha, v) + return v + + def min_value(state, alpha, beta): + if game.terminal_test(state): + return game.utility(state, player) + v = np.inf + for a in game.actions(state): + v = min(v, max_value(game.result(state, a), alpha, beta)) + if v <= alpha: + return v + beta = min(beta, v) + return v + + # Body of alpha_beta_search: + best_score = -np.inf + beta = np.inf + best_action = None + for a in game.actions(state): + v = min_value(game.result(state, a), best_score, beta) + if v > best_score: + best_score = v + best_action = a + return best_action + + +def alpha_beta_cutoff_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 alpha_beta + def max_value(state, alpha, beta, depth): + if cutoff_test(state, depth): + return eval_fn(state) + v = -np.inf + for a in game.actions(state): + v = max(v, min_value(game.result(state, a), alpha, beta, depth + 1)) + if v >= beta: + return v + alpha = max(alpha, v) + return v + + def min_value(state, alpha, beta, depth): + if cutoff_test(state, depth): + return eval_fn(state) + v = np.inf + for a in game.actions(state): + v = min(v, max_value(game.result(state, a), alpha, beta, depth + 1)) + if v <= alpha: + return v + beta = min(beta, v) + return v + + # Body of alpha_beta_cutoff_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))) + eval_fn = eval_fn or (lambda state: game.utility(state, player)) + best_score = -np.inf + beta = np.inf + best_action = None + for a in game.actions(state): + v = min_value(game.result(state, a), best_score, beta, 1) + if v > best_score: + best_score = v + best_action = a + return best_action + + +# ______________________________________________________________________________ +# Monte Carlo Tree Search + + +def monte_carlo_tree_search(state, game, N=1000): + def select(n): + """select a leaf node in the tree""" + if n.children: + return select(max(n.children.keys(), key=ucb)) + else: + return n + + def expand(n): + """expand the leaf node by adding all its children states""" + if not n.children and not game.terminal_test(n.state): + n.children = {MCT_Node(state=game.result(n.state, action), parent=n): action + for action in game.actions(n.state)} + return select(n) + + def simulate(game, state): + """simulate the utility of current state by random picking a step""" + player = game.to_move(state) + while not game.terminal_test(state): + action = random.choice(list(game.actions(state))) + state = game.result(state, action) + v = game.utility(state, player) + return -v + + def backprop(n, utility): + """passing the utility back to all parent nodes""" + if utility > 0: + n.U += utility + # if utility == 0: + # n.U += 0.5 + n.N += 1 + if n.parent: + backprop(n.parent, -utility) + + root = MCT_Node(state=state) + + for _ in range(N): + leaf = select(root) + child = expand(leaf) + result = simulate(game, child.state) + backprop(child, result) + + max_state = max(root.children, key=lambda p: p.N) + + return root.children.get(max_state) + + +# ______________________________________________________________________________ +# Players for Games + + +def query_player(game, state): + """Make a move by querying standard input.""" + print("current state:") + game.display(state) + print("available moves: {}".format(game.actions(state))) + print("") + move = None + if game.actions(state): + move_string = input('Your move? ') + try: + move = eval(move_string) + except NameError: + move = move_string + else: + print('no legal moves: passing turn to next player') + return move + + +def random_player(game, state): + """A player that chooses a legal move at random.""" + return random.choice(game.actions(state)) if game.actions(state) else None + + +def alpha_beta_player(game, state): + return alpha_beta_search(state, game) + + +def expect_min_max_player(game, state): + return expect_minmax(state, game) + + +def mcts_player(game, state): + return monte_carlo_tree_search(state, game) + + +# ______________________________________________________________________________ +# 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, + result, utility, and terminal_test. You may override display and + successors or you can inherit their default methods. You will also + need to set the .initial attribute to the initial state; this can + be done in the constructor.""" + + def actions(self, state): + """Return a list of the allowable moves at this point.""" + raise NotImplementedError + + def result(self, state, move): + """Return the state that results from making a move from a state.""" + raise NotImplementedError + + def utility(self, state, player): + """Return the value of this final state to player.""" + raise NotImplementedError + + def terminal_test(self, state): + """Return True if this is a final state for the game.""" + return not self.actions(state) + + def to_move(self, state): + """Return the player whose move it is in this state.""" + return state.to_move + + def display(self, state): + """Print or otherwise display the state.""" + print(state) + + def __repr__(self): + return '<{}>'.format(self.__class__.__name__) + + def play_game(self, *players): + """Play an n-person, move-alternating game.""" + state = self.initial + while True: + for player in players: + move = player(self, state) + state = self.result(state, move) + if self.terminal_test(state): + self.display(state) + return self.utility(state, self.to_move(self.initial)) + + +class StochasticGame(Game): + """A stochastic game includes uncertain events which influence + the moves of players at each state. To create a stochastic game, subclass + this class and implement chances and outcome along with the other + unimplemented game class methods.""" + + def chances(self, state): + """Return a list of all possible uncertain events at a state.""" + raise NotImplementedError + + def outcome(self, state, chance): + """Return the state which is the outcome of a chance trial.""" + raise NotImplementedError + + def probability(self, chance): + """Return the probability of occurrence of a chance.""" + raise NotImplementedError + + def play_game(self, *players): + """Play an n-person, move-alternating stochastic game.""" + state = self.initial + while True: + for player in players: + chance = random.choice(self.chances(state)) + state = self.outcome(state, chance) + move = player(self, state) + state = self.result(state, move) + if self.terminal_test(state): + self.display(state) + return self.utility(state, self.to_move(self.initial)) + + +class Fig52Game(Game): + """The game represented in [Figure 5.2]. Serves as a simple test case.""" + + succs = dict(A=dict(a1='B', a2='C', a3='D'), + B=dict(b1='B1', b2='B2', b3='B3'), + C=dict(c1='C1', c2='C2', c3='C3'), + D=dict(d1='D1', d2='D2', d3='D3')) + utils = dict(B1=3, B2=12, B3=8, C1=2, C2=4, C3=6, D1=14, D2=5, D3=2) + initial = 'A' + + def actions(self, state): + return list(self.succs.get(state, {}).keys()) + + def result(self, state, move): + return self.succs[state][move] + + def utility(self, state, player): + if player == 'MAX': + return self.utils[state] + else: + return -self.utils[state] + + def terminal_test(self, state): + return state not in ('A', 'B', 'C', 'D') + + def to_move(self, state): + return 'MIN' if state in 'BCD' else 'MAX' + + +class Fig52Extended(Game): + """Similar to Fig52Game but bigger. Useful for visualisation""" + + succs = {i: dict(l=i * 3 + 1, m=i * 3 + 2, r=i * 3 + 3) for i in range(13)} + utils = dict() + + def actions(self, state): + return sorted(list(self.succs.get(state, {}).keys())) + + def result(self, state, move): + return self.succs[state][move] + + def utility(self, state, player): + if player == 'MAX': + return self.utils[state] + else: + return -self.utils[state] + + def terminal_test(self, state): + return state not in range(13) + + def to_move(self, state): + return 'MIN' if state in {1, 2, 3} 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): + self.h = h + self.v = v + self.k = k + moves = [(x, y) for x in range(1, h + 1) + for y in range(1, v + 1)] + self.initial = GameState(to_move='X', utility=0, board={}, moves=moves) + + def actions(self, state): + """Legal moves are any square not yet taken.""" + return state.moves + + 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 GameState(to_move=('O' if state.to_move == 'X' else 'X'), + utility=self.compute_utility(board, move, state.to_move), + board=board, moves=moves) + + def utility(self, state, player): + """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" + return state.utility if player == 'X' else -state.utility + + def terminal_test(self, state): + """A state is terminal if it is won or there are no empty squares.""" + return state.utility != 0 or len(state.moves) == 0 + + 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), '.'), end=' ') + print() + + def compute_utility(self, board, move, player): + """If 'X' wins with this move, return 1; if 'O' wins 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))): + return +1 if player == 'X' else -1 + else: + return 0 + + def k_in_row(self, board, move, player, delta_x_y): + """Return true if there is a line through move on board for player.""" + (delta_x, delta_y) = delta_x_y + x, y = move + 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 + x, y = move + while board.get((x, y)) == player: + n += 1 + x, y = x - delta_x, y - delta_y + 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.""" + + def __init__(self, h=7, v=6, k=4): + TicTacToe.__init__(self, h, v, k) + + def actions(self, state): + return [(x, y) for (x, y) in state.moves + if y == 1 or (x, y - 1) in state.board] + + +class Backgammon(StochasticGame): + """A two player game where the goal of each player is to move all the + checkers off the board. The moves for each state are determined by + rolling a pair of dice.""" + + def __init__(self): + """Initial state of the game""" + point = {'W': 0, 'B': 0} + board = [point.copy() for index in range(24)] + board[0]['B'] = board[23]['W'] = 2 + board[5]['W'] = board[18]['B'] = 5 + board[7]['W'] = board[16]['B'] = 3 + board[11]['B'] = board[12]['W'] = 5 + self.allow_bear_off = {'W': False, 'B': False} + self.direction = {'W': -1, 'B': 1} + self.initial = StochasticGameState(to_move='W', + utility=0, + board=board, + moves=self.get_all_moves(board, 'W'), chance=None) + + def actions(self, state): + """Return a list of legal moves for a state.""" + player = state.to_move + moves = state.moves + if len(moves) == 1 and len(moves[0]) == 1: + return moves + legal_moves = [] + for move in moves: + board = copy.deepcopy(state.board) + if self.is_legal_move(board, move, state.chance, player): + legal_moves.append(move) + return legal_moves + + def result(self, state, move): + board = copy.deepcopy(state.board) + player = state.to_move + self.move_checker(board, move[0], state.chance[0], player) + if len(move) == 2: + self.move_checker(board, move[1], state.chance[1], player) + to_move = ('W' if player == 'B' else 'B') + return StochasticGameState(to_move=to_move, + utility=self.compute_utility(board, move, player), + board=board, + moves=self.get_all_moves(board, to_move), chance=None) + + def utility(self, state, player): + """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" + return state.utility if player == 'W' else -state.utility + + def terminal_test(self, state): + """A state is terminal if one player wins.""" + return state.utility != 0 + + def get_all_moves(self, board, player): + """All possible moves for a player i.e. all possible ways of + choosing two checkers of a player from the board for a move + at a given state.""" + all_points = board + taken_points = [index for index, point in enumerate(all_points) + if point[player] > 0] + if self.checkers_at_home(board, player) == 1: + return [(taken_points[0],)] + moves = list(itertools.permutations(taken_points, 2)) + moves = moves + [(index, index) for index, point in enumerate(all_points) + if point[player] >= 2] + return moves + + def display(self, state): + """Display state of the game.""" + board = state.board + player = state.to_move + print("current state : ") + for index, point in enumerate(board): + print("point : ", index, " W : ", point['W'], " B : ", point['B']) + print("to play : ", player) + + def compute_utility(self, board, move, player): + """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0.""" + util = {'W': 1, 'B': -1} + for idx in range(0, 24): + if board[idx][player] > 0: + return 0 + return util[player] + + def checkers_at_home(self, board, player): + """Return the no. of checkers at home for a player.""" + sum_range = range(0, 7) if player == 'W' else range(17, 24) + count = 0 + for idx in sum_range: + count = count + board[idx][player] + return count + + def is_legal_move(self, board, start, steps, player): + """Move is a tuple which contains starting points of checkers to be + moved during a player's turn. An on-board move is legal if both the destinations + are open. A bear-off move is the one where a checker is moved off-board. + It is legal only after a player has moved all his checkers to his home.""" + dest1, dest2 = vector_add(start, steps) + dest_range = range(0, 24) + move1_legal = move2_legal = False + if dest1 in dest_range: + if self.is_point_open(player, board[dest1]): + self.move_checker(board, start[0], steps[0], player) + move1_legal = True + else: + if self.allow_bear_off[player]: + self.move_checker(board, start[0], steps[0], player) + move1_legal = True + if not move1_legal: + return False + if dest2 in dest_range: + if self.is_point_open(player, board[dest2]): + move2_legal = True + else: + if self.allow_bear_off[player]: + move2_legal = True + return move1_legal and move2_legal + + def move_checker(self, board, start, steps, player): + """Move a checker from starting point by a given number of steps""" + dest = start + steps + dest_range = range(0, 24) + board[start][player] -= 1 + if dest in dest_range: + board[dest][player] += 1 + if self.checkers_at_home(board, player) == 15: + self.allow_bear_off[player] = True + + def is_point_open(self, player, point): + """A point is open for a player if the no. of opponent's + checkers already present on it is 0 or 1. A player can + move a checker to a point only if it is open.""" + opponent = 'B' if player == 'W' else 'W' + return point[opponent] <= 1 + + def chances(self, state): + """Return a list of all possible dice rolls at a state.""" + dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2)) + return dice_rolls + + def outcome(self, state, chance): + """Return the state which is the outcome of a dice roll.""" + dice = tuple(map((self.direction[state.to_move]).__mul__, chance)) + return StochasticGameState(to_move=state.to_move, + utility=state.utility, + board=state.board, + moves=state.moves, chance=dice) + + def probability(self, chance): + """Return the probability of occurrence of a dice roll.""" + return 1 / 36 if chance[0] == chance[1] else 1 / 18 diff --git a/grid.ipynb b/grid.ipynb deleted file mode 100644 index 4e3bbd7e5..000000000 --- a/grid.ipynb +++ /dev/null @@ -1,47 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import grid\n", - "\n", - "print(grid.distance_squared((1, 2), (5, 5)))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/grid.py b/grid.py deleted file mode 100644 index 0fb0efe9d..000000000 --- a/grid.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 - -from utils import clip - -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.""" - return math.hypot((a[0] - b[0]), (a[1] - b[1])) - - -def distance2(a, b): - "The square of the distance between two (x, y) points." - return (a[0] - b[0])**2 + (a[1] - b[1])**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)) diff --git a/gui/eight_puzzle.py b/gui/eight_puzzle.py new file mode 100644 index 000000000..5733228d7 --- /dev/null +++ b/gui/eight_puzzle.py @@ -0,0 +1,151 @@ +import os.path +import random +import time +from functools import partial +from tkinter import * + +from search import astar_search, EightPuzzle + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +root = Tk() + +state = [1, 2, 3, 4, 5, 6, 7, 8, 0] +puzzle = EightPuzzle(tuple(state)) +solution = None + +b = [None] * 9 + + +# TODO: refactor into OOP, remove global variables + +def scramble(): + """Scrambles the puzzle starting from the goal state""" + + global state + global puzzle + possible_actions = ['UP', 'DOWN', 'LEFT', 'RIGHT'] + scramble = [] + for _ in range(60): + scramble.append(random.choice(possible_actions)) + + for move in scramble: + if move in puzzle.actions(state): + state = list(puzzle.result(state, move)) + puzzle = EightPuzzle(tuple(state)) + create_buttons() + + +def solve(): + """Solves the puzzle using astar_search""" + + return astar_search(puzzle).solution() + + +def solve_steps(): + """Solves the puzzle step by step""" + + global puzzle + global solution + global state + solution = solve() + print(solution) + + for move in solution: + state = puzzle.result(state, move) + create_buttons() + root.update() + root.after(1, time.sleep(0.75)) + + +def exchange(index): + """Interchanges the position of the selected tile with the zero tile under certain conditions""" + + global state + global solution + global puzzle + zero_ix = list(state).index(0) + actions = puzzle.actions(state) + current_action = '' + i_diff = index // 3 - zero_ix // 3 + j_diff = index % 3 - zero_ix % 3 + if i_diff == 1: + current_action += 'DOWN' + elif i_diff == -1: + current_action += 'UP' + + if j_diff == 1: + current_action += 'RIGHT' + elif j_diff == -1: + current_action += 'LEFT' + + if abs(i_diff) + abs(j_diff) != 1: + current_action = '' + + if current_action in actions: + b[zero_ix].grid_forget() + b[zero_ix] = Button(root, text=f'{state[index]}', width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, zero_ix)) + b[zero_ix].grid(row=zero_ix // 3, column=zero_ix % 3, ipady=40) + b[index].grid_forget() + b[index] = Button(root, text=None, width=6, font=('Helvetica', 40, 'bold'), command=partial(exchange, index)) + b[index].grid(row=index // 3, column=index % 3, ipady=40) + state[zero_ix], state[index] = state[index], state[zero_ix] + puzzle = EightPuzzle(tuple(state)) + + +def create_buttons(): + """Creates dynamic buttons""" + + # TODO: Find a way to use grid_forget() with a for loop for initialization + b[0] = Button(root, text=f'{state[0]}' if state[0] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 0)) + b[0].grid(row=0, column=0, ipady=40) + b[1] = Button(root, text=f'{state[1]}' if state[1] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 1)) + b[1].grid(row=0, column=1, ipady=40) + b[2] = Button(root, text=f'{state[2]}' if state[2] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 2)) + b[2].grid(row=0, column=2, ipady=40) + b[3] = Button(root, text=f'{state[3]}' if state[3] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 3)) + b[3].grid(row=1, column=0, ipady=40) + b[4] = Button(root, text=f'{state[4]}' if state[4] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 4)) + b[4].grid(row=1, column=1, ipady=40) + b[5] = Button(root, text=f'{state[5]}' if state[5] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 5)) + b[5].grid(row=1, column=2, ipady=40) + b[6] = Button(root, text=f'{state[6]}' if state[6] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 6)) + b[6].grid(row=2, column=0, ipady=40) + b[7] = Button(root, text=f'{state[7]}' if state[7] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 7)) + b[7].grid(row=2, column=1, ipady=40) + b[8] = Button(root, text=f'{state[8]}' if state[8] != 0 else None, width=6, font=('Helvetica', 40, 'bold'), + command=partial(exchange, 8)) + b[8].grid(row=2, column=2, ipady=40) + + +def create_static_buttons(): + """Creates scramble and solve buttons""" + + scramble_btn = Button(root, text='Scramble', font=('Helvetica', 30, 'bold'), width=8, command=partial(init)) + scramble_btn.grid(row=3, column=0, ipady=10) + solve_btn = Button(root, text='Solve', font=('Helvetica', 30, 'bold'), width=8, command=partial(solve_steps)) + solve_btn.grid(row=3, column=2, ipady=10) + + +def init(): + """Calls necessary functions""" + + global state + global solution + state = [1, 2, 3, 4, 5, 6, 7, 8, 0] + scramble() + create_buttons() + create_static_buttons() + + +init() +root.mainloop() diff --git a/gui/genetic_algorithm_example.py b/gui/genetic_algorithm_example.py new file mode 100644 index 000000000..c987151c8 --- /dev/null +++ b/gui/genetic_algorithm_example.py @@ -0,0 +1,189 @@ +# A simple program that implements the solution to the phrase generation problem using +# genetic algorithms as given in the search.ipynb notebook. +# +# Type on the home screen to change the target phrase +# Click on the slider to change genetic algorithm parameters +# Click 'GO' to run the algorithm with the specified variables +# Displays best individual of the current generation +# Displays a progress bar that indicates the amount of completion of the algorithm +# Displays the first few individuals of the current generation + +import os.path +from tkinter import * +from tkinter import ttk + +import search + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +LARGE_FONT = ('Verdana', 12) +EXTRA_LARGE_FONT = ('Consolas', 36, 'bold') + +canvas_width = 800 +canvas_height = 600 + +black = '#000000' +white = '#ffffff' +p_blue = '#042533' +lp_blue = '#0c394c' + +# genetic algorithm variables +# feel free to play around with these +target = 'Genetic Algorithm' # the phrase to be generated +max_population = 100 # number of samples in each population +mutation_rate = 0.1 # probability of mutation +f_thres = len(target) # fitness threshold +ngen = 1200 # max number of generations to run the genetic algorithm + +generation = 0 # counter to keep track of generation number + +u_case = [chr(x) for x in range(65, 91)] # list containing all uppercase characters +l_case = [chr(x) for x in range(97, 123)] # list containing all lowercase characters +punctuations1 = [chr(x) for x in range(33, 48)] # lists containing punctuation symbols +punctuations2 = [chr(x) for x in range(58, 65)] +punctuations3 = [chr(x) for x in range(91, 97)] +numerals = [chr(x) for x in range(48, 58)] # list containing numbers + +# extend the gene pool with the required lists and append the space character +gene_pool = [] +gene_pool.extend(u_case) +gene_pool.extend(l_case) +gene_pool.append(' ') + + +# callbacks to update global variables from the slider values +def update_max_population(slider_value): + global max_population + max_population = slider_value + + +def update_mutation_rate(slider_value): + global mutation_rate + mutation_rate = slider_value + + +def update_f_thres(slider_value): + global f_thres + f_thres = slider_value + + +def update_ngen(slider_value): + global ngen + ngen = slider_value + + +# fitness function +def fitness_fn(_list): + fitness = 0 + # create string from list of characters + phrase = ''.join(_list) + # add 1 to fitness value for every matching character + for i in range(len(phrase)): + if target[i] == phrase[i]: + fitness += 1 + return fitness + + +# function to bring a new frame on top +def raise_frame(frame, init=False, update_target=False, target_entry=None, f_thres_slider=None): + frame.tkraise() + global target + if update_target and target_entry is not None: + target = target_entry.get() + f_thres_slider.config(to=len(target)) + if init: + population = search.init_population(max_population, gene_pool, len(target)) + genetic_algorithm_stepwise(population) + + +# defining root and child frames +root = Tk() +f1 = Frame(root) +f2 = Frame(root) + +# pack frames on top of one another +for frame in (f1, f2): + frame.grid(row=0, column=0, sticky='news') + +# Home Screen (f1) widgets +target_entry = Entry(f1, font=('Consolas 46 bold'), exportselection=0, foreground=p_blue, justify=CENTER) +target_entry.insert(0, target) +target_entry.pack(expand=YES, side=TOP, fill=X, padx=50) +target_entry.focus_force() + +max_population_slider = Scale(f1, from_=3, to=1000, orient=HORIZONTAL, label='Max population', + command=lambda value: update_max_population(int(value))) +max_population_slider.set(max_population) +max_population_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +mutation_rate_slider = Scale(f1, from_=0, to=1, orient=HORIZONTAL, label='Mutation rate', resolution=0.0001, + command=lambda value: update_mutation_rate(float(value))) +mutation_rate_slider.set(mutation_rate) +mutation_rate_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +f_thres_slider = Scale(f1, from_=0, to=len(target), orient=HORIZONTAL, label='Fitness threshold', + command=lambda value: update_f_thres(int(value))) +f_thres_slider.set(f_thres) +f_thres_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +ngen_slider = Scale(f1, from_=1, to=5000, orient=HORIZONTAL, label='Max number of generations', + command=lambda value: update_ngen(int(value))) +ngen_slider.set(ngen) +ngen_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +button = ttk.Button(f1, text='RUN', + command=lambda: raise_frame(f2, init=True, update_target=True, target_entry=target_entry, + f_thres_slider=f_thres_slider)).pack(side=BOTTOM, pady=50) + +# f2 widgets +canvas = Canvas(f2, width=canvas_width, height=canvas_height) +canvas.pack(expand=YES, fill=BOTH, padx=20, pady=15) +button = ttk.Button(f2, text='EXIT', command=lambda: raise_frame(f1)).pack(side=BOTTOM, pady=15) + + +# function to run the genetic algorithm and update text on the canvas +def genetic_algorithm_stepwise(population): + root.title('Genetic Algorithm') + for generation in range(ngen): + # generating new population after selecting, recombining and mutating the existing population + population = [ + search.mutate(search.recombine(*search.select(2, population, fitness_fn)), gene_pool, mutation_rate) for i + in range(len(population))] + # genome with the highest fitness in the current generation + current_best = ''.join(max(population, key=fitness_fn)) + # collecting first few examples from the current population + members = [''.join(x) for x in population][:48] + + # clear the canvas + canvas.delete('all') + # displays current best on top of the screen + canvas.create_text(canvas_width / 2, 40, fill=p_blue, font='Consolas 46 bold', text=current_best) + + # displaying a part of the population on the screen + for i in range(len(members) // 3): + canvas.create_text((canvas_width * .175), (canvas_height * .25 + (25 * i)), fill=lp_blue, + font='Consolas 16', text=members[3 * i]) + canvas.create_text((canvas_width * .500), (canvas_height * .25 + (25 * i)), fill=lp_blue, + font='Consolas 16', text=members[3 * i + 1]) + canvas.create_text((canvas_width * .825), (canvas_height * .25 + (25 * i)), fill=lp_blue, + font='Consolas 16', text=members[3 * i + 2]) + + # displays current generation number + canvas.create_text((canvas_width * .5), (canvas_height * 0.95), fill=p_blue, font='Consolas 18 bold', + text=f'Generation {generation}') + + # displays blue bar that indicates current maximum fitness compared to maximum possible fitness + scaling_factor = fitness_fn(current_best) / len(target) + canvas.create_rectangle(canvas_width * 0.1, 90, canvas_width * 0.9, 100, outline=p_blue) + canvas.create_rectangle(canvas_width * 0.1, 90, canvas_width * 0.1 + scaling_factor * canvas_width * 0.8, 100, + fill=lp_blue) + canvas.update() + + # checks for completion + fittest_individual = search.fitness_threshold(fitness_fn, f_thres, population) + if fittest_individual: + break + + +raise_frame(f1) +root.mainloop() diff --git a/gui/grid_mdp.py b/gui/grid_mdp.py new file mode 100644 index 000000000..e60b49247 --- /dev/null +++ b/gui/grid_mdp.py @@ -0,0 +1,676 @@ +import os.path +import sys +import tkinter as tk +import tkinter.messagebox +from functools import partial +from tkinter import ttk + +import matplotlib +import matplotlib.animation as animation +from matplotlib import pyplot as plt +from matplotlib import style +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from matplotlib.figure import Figure +from matplotlib.ticker import MaxNLocator + +from mdp import * + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +matplotlib.use('TkAgg') +style.use('ggplot') + +fig = Figure(figsize=(20, 15)) +sub = fig.add_subplot(111) +plt.rcParams['axes.grid'] = False + +WALL_VALUE = -99999.0 +TERM_VALUE = -999999.0 + +black = '#000' +white = '#fff' +gray2 = '#222' +gray9 = '#999' +grayd = '#ddd' +grayef = '#efefef' +pblue = '#000040' +green8 = '#008080' +green4 = '#004040' + +cell_window_mantainer = None + + +def extents(f): + """adjusts axis markers for heatmap""" + + delta = f[1] - f[0] + return [f[0] - delta / 2, f[-1] + delta / 2] + + +def display(gridmdp, _height, _width): + """displays matrix""" + + dialog = tk.Toplevel() + dialog.wm_title('Values') + + container = tk.Frame(dialog) + container.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + label = ttk.Label(container, text=f'{gridmdp[_height - i - 1][j]:.3f}', font=('Helvetica', 12)) + label.grid(row=i + 1, column=j + 1, padx=3, pady=3) + + dialog.mainloop() + + +def display_best_policy(_best_policy, _height, _width): + """displays best policy""" + dialog = tk.Toplevel() + dialog.wm_title('Best Policy') + + container = tk.Frame(dialog) + container.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + label = ttk.Label(container, text=_best_policy[i][j], font=('Helvetica', 12, 'bold')) + label.grid(row=i + 1, column=j + 1, padx=3, pady=3) + + dialog.mainloop() + + +def initialize_dialogbox(_width, _height, gridmdp, terminals, buttons): + """creates dialogbox for initialization""" + + dialog = tk.Toplevel() + dialog.wm_title('Initialize') + + container = tk.Frame(dialog) + container.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + container.grid_rowconfigure(0, weight=1) + container.grid_columnconfigure(0, weight=1) + + wall = tk.IntVar() + wall.set(0) + term = tk.IntVar() + term.set(0) + reward = tk.DoubleVar() + reward.set(0.0) + + label = ttk.Label(container, text='Initialize', font=('Helvetica', 12), anchor=tk.N) + label.grid(row=0, column=0, columnspan=3, sticky='new', pady=15, padx=5) + label_reward = ttk.Label(container, text='Reward', font=('Helvetica', 10), anchor=tk.N) + label_reward.grid(row=1, column=0, columnspan=3, sticky='new', pady=1, padx=5) + entry_reward = ttk.Entry(container, font=('Helvetica', 10), justify=tk.CENTER, exportselection=0, + textvariable=reward) + entry_reward.grid(row=2, column=0, columnspan=3, sticky='new', pady=5, padx=50) + + rbtn_term = ttk.Radiobutton(container, text='Terminal', variable=term, value=TERM_VALUE) + rbtn_term.grid(row=3, column=0, columnspan=3, sticky='nsew', padx=160, pady=5) + rbtn_wall = ttk.Radiobutton(container, text='Wall', variable=wall, value=WALL_VALUE) + rbtn_wall.grid(row=4, column=0, columnspan=3, sticky='nsew', padx=172, pady=5) + + initialize_widget_disability_checks(_width, _height, gridmdp, terminals, label_reward, entry_reward, rbtn_wall, + rbtn_term) + + btn_apply = ttk.Button(container, text='Apply', + command=partial(initialize_update_table, _width, _height, gridmdp, terminals, buttons, + reward, term, wall, label_reward, entry_reward, rbtn_term, rbtn_wall)) + btn_apply.grid(row=5, column=0, sticky='nsew', pady=5, padx=5) + btn_reset = ttk.Button(container, text='Reset', + command=partial(initialize_reset_all, _width, _height, gridmdp, terminals, buttons, reward, + term, wall, label_reward, entry_reward, rbtn_wall, rbtn_term)) + btn_reset.grid(row=5, column=1, sticky='nsew', pady=5, padx=5) + btn_ok = ttk.Button(container, text='Ok', command=dialog.destroy) + btn_ok.grid(row=5, column=2, sticky='nsew', pady=5, padx=5) + + dialog.geometry('400x200') + dialog.mainloop() + + +def update_table(i, j, gridmdp, terminals, buttons, reward, term, wall, label_reward, entry_reward, rbtn_term, + rbtn_wall): + """functionality for 'apply' button""" + if wall.get() == WALL_VALUE: + buttons[i][j].configure(style='wall.TButton') + buttons[i][j].config(text='Wall') + label_reward.config(foreground='#999') + entry_reward.config(state=tk.DISABLED) + rbtn_term.state(['!focus', '!selected']) + rbtn_term.config(state=tk.DISABLED) + gridmdp[i][j] = WALL_VALUE + + elif wall.get() != WALL_VALUE: + if reward.get() != 0.0: + gridmdp[i][j] = reward.get() + buttons[i][j].configure(style='reward.TButton') + buttons[i][j].config(text=f'R = {reward.get()}') + + if term.get() == TERM_VALUE: + if (i, j) not in terminals: + terminals.append((i, j)) + rbtn_wall.state(['!focus', '!selected']) + rbtn_wall.config(state=tk.DISABLED) + + if gridmdp[i][j] < 0: + buttons[i][j].configure(style='-term.TButton') + + elif gridmdp[i][j] > 0: + buttons[i][j].configure(style='+term.TButton') + + elif gridmdp[i][j] == 0.0: + buttons[i][j].configure(style='=term.TButton') + + +def initialize_update_table(_width, _height, gridmdp, terminals, buttons, reward, term, wall, label_reward, + entry_reward, rbtn_term, rbtn_wall): + """runs update_table for all cells""" + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + update_table(i, j, gridmdp, terminals, buttons, reward, term, wall, label_reward, entry_reward, rbtn_term, + rbtn_wall) + + +def reset_all(_height, i, j, gridmdp, terminals, buttons, reward, term, wall, label_reward, entry_reward, rbtn_wall, + rbtn_term): + """functionality for reset button""" + reward.set(0.0) + term.set(0) + wall.set(0) + gridmdp[i][j] = 0.0 + buttons[i][j].configure(style='TButton') + buttons[i][j].config(text=f'({_height - i - 1}, {j})') + + if (i, j) in terminals: + terminals.remove((i, j)) + + label_reward.config(foreground='#000') + entry_reward.config(state=tk.NORMAL) + rbtn_term.config(state=tk.NORMAL) + rbtn_wall.config(state=tk.NORMAL) + rbtn_wall.state(['!focus', '!selected']) + rbtn_term.state(['!focus', '!selected']) + + +def initialize_reset_all(_width, _height, gridmdp, terminals, buttons, reward, term, wall, label_reward, entry_reward, + rbtn_wall, rbtn_term): + """runs reset_all for all cells""" + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + reset_all(_height, i, j, gridmdp, terminals, buttons, reward, term, wall, label_reward, entry_reward, + rbtn_wall, rbtn_term) + + +def external_reset(_width, _height, gridmdp, terminals, buttons): + """reset from edit menu""" + for i in range(max(1, _height)): + for j in range(max(1, _width)): + gridmdp[i][j] = 0.0 + buttons[i][j].configure(style='TButton') + buttons[i][j].config(text=f'({_height - i - 1}, {j})') + + +def widget_disability_checks(i, j, gridmdp, terminals, label_reward, entry_reward, rbtn_wall, rbtn_term): + """checks for required state of widgets in dialog boxes""" + + if gridmdp[i][j] == WALL_VALUE: + label_reward.config(foreground='#999') + entry_reward.config(state=tk.DISABLED) + rbtn_term.config(state=tk.DISABLED) + rbtn_wall.state(['!focus', 'selected']) + rbtn_term.state(['!focus', '!selected']) + + if (i, j) in terminals: + rbtn_wall.config(state=tk.DISABLED) + rbtn_wall.state(['!focus', '!selected']) + + +def flatten_list(_list): + """returns a flattened list""" + return sum(_list, []) + + +def initialize_widget_disability_checks(_width, _height, gridmdp, terminals, label_reward, entry_reward, rbtn_wall, + rbtn_term): + """checks for required state of widgets when cells are initialized""" + + bool_walls = [['False'] * max(1, _width) for _ in range(max(1, _height))] + bool_terms = [['False'] * max(1, _width) for _ in range(max(1, _height))] + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + if gridmdp[i][j] == WALL_VALUE: + bool_walls[i][j] = 'True' + + if (i, j) in terminals: + bool_terms[i][j] = 'True' + + bool_walls_fl = flatten_list(bool_walls) + bool_terms_fl = flatten_list(bool_terms) + + if bool_walls_fl.count('True') == len(bool_walls_fl): + print('`') + label_reward.config(foreground='#999') + entry_reward.config(state=tk.DISABLED) + rbtn_term.config(state=tk.DISABLED) + rbtn_wall.state(['!focus', 'selected']) + rbtn_term.state(['!focus', '!selected']) + + if bool_terms_fl.count('True') == len(bool_terms_fl): + rbtn_wall.config(state=tk.DISABLED) + rbtn_wall.state(['!focus', '!selected']) + rbtn_term.state(['!focus', 'selected']) + + +def dialogbox(i, j, gridmdp, terminals, buttons, _height): + """creates dialogbox for each cell""" + global cell_window_mantainer + if (cell_window_mantainer != None): + cell_window_mantainer.destroy() + + dialog = tk.Toplevel() + cell_window_mantainer = dialog + dialog.wm_title(f'{_height - i - 1}, {j}') + + container = tk.Frame(dialog) + container.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + container.grid_rowconfigure(0, weight=1) + container.grid_columnconfigure(0, weight=1) + + wall = tk.IntVar() + wall.set(gridmdp[i][j]) + term = tk.IntVar() + term.set(TERM_VALUE if (i, j) in terminals else 0.0) + reward = tk.DoubleVar() + reward.set(gridmdp[i][j] if gridmdp[i][j] != WALL_VALUE else 0.0) + + label = ttk.Label(container, text=f'Configure cell {_height - i - 1}, {j}', font=('Helvetica', 12), anchor=tk.N) + label.grid(row=0, column=0, columnspan=3, sticky='new', pady=15, padx=5) + label_reward = ttk.Label(container, text='Reward', font=('Helvetica', 10), anchor=tk.N) + label_reward.grid(row=1, column=0, columnspan=3, sticky='new', pady=1, padx=5) + entry_reward = ttk.Entry(container, font=('Helvetica', 10), justify=tk.CENTER, exportselection=0, + textvariable=reward) + entry_reward.grid(row=2, column=0, columnspan=3, sticky='new', pady=5, padx=50) + + rbtn_term = ttk.Radiobutton(container, text='Terminal', variable=term, value=TERM_VALUE) + rbtn_term.grid(row=3, column=0, columnspan=3, sticky='nsew', padx=160, pady=5) + rbtn_wall = ttk.Radiobutton(container, text='Wall', variable=wall, value=WALL_VALUE) + rbtn_wall.grid(row=4, column=0, columnspan=3, sticky='nsew', padx=172, pady=5) + + widget_disability_checks(i, j, gridmdp, terminals, label_reward, entry_reward, rbtn_wall, rbtn_term) + + btn_apply = ttk.Button(container, text='Apply', + command=partial(update_table, i, j, gridmdp, terminals, buttons, reward, term, wall, + label_reward, entry_reward, rbtn_term, rbtn_wall)) + btn_apply.grid(row=5, column=0, sticky='nsew', pady=5, padx=5) + btn_reset = ttk.Button(container, text='Reset', + command=partial(reset_all, _height, i, j, gridmdp, terminals, buttons, reward, term, wall, + label_reward, entry_reward, rbtn_wall, rbtn_term)) + btn_reset.grid(row=5, column=1, sticky='nsew', pady=5, padx=5) + btn_ok = ttk.Button(container, text='Ok', command=dialog.destroy) + btn_ok.grid(row=5, column=2, sticky='nsew', pady=5, padx=5) + + dialog.geometry('400x200') + dialog.mainloop() + + +class MDPapp(tk.Tk): + + def __init__(self, *args, **kwargs): + + tk.Tk.__init__(self, *args, **kwargs) + tk.Tk.wm_title(self, 'Grid MDP') + self.shared_data = { + 'height': tk.IntVar(), + 'width': tk.IntVar()} + self.shared_data['height'].set(1) + self.shared_data['width'].set(1) + self.container = tk.Frame(self) + self.container.pack(side='top', fill='both', expand=True) + self.container.grid_rowconfigure(0, weight=1) + self.container.grid_columnconfigure(0, weight=1) + + self.frames = {} + + self.menu_bar = tk.Menu(self.container) + self.file_menu = tk.Menu(self.menu_bar, tearoff=0) + self.file_menu.add_command(label='Exit', command=self.exit) + self.menu_bar.add_cascade(label='File', menu=self.file_menu) + + self.edit_menu = tk.Menu(self.menu_bar, tearoff=1) + self.edit_menu.add_command(label='Reset', command=self.master_reset) + self.edit_menu.add_command(label='Initialize', command=self.initialize) + self.edit_menu.add_separator() + self.edit_menu.add_command(label='View matrix', command=self.view_matrix) + self.edit_menu.add_command(label='View terminals', command=self.view_terminals) + self.menu_bar.add_cascade(label='Edit', menu=self.edit_menu) + self.menu_bar.entryconfig('Edit', state=tk.DISABLED) + + self.build_menu = tk.Menu(self.menu_bar, tearoff=1) + self.build_menu.add_command(label='Build and Run', command=self.build) + self.menu_bar.add_cascade(label='Build', menu=self.build_menu) + self.menu_bar.entryconfig('Build', state=tk.DISABLED) + tk.Tk.config(self, menu=self.menu_bar) + + for F in (HomePage, BuildMDP, SolveMDP): + frame = F(self.container, self) + self.frames[F] = frame + frame.grid(row=0, column=0, sticky='nsew') + + self.show_frame(HomePage) + + def placeholder_function(self): + """placeholder function""" + + print('Not supported yet!') + + def exit(self): + """function to exit""" + if tkinter.messagebox.askokcancel('Exit?', 'All changes will be lost'): + quit() + + def new(self): + """function to create new GridMDP""" + + self.master_reset() + build_page = self.get_page(BuildMDP) + build_page.gridmdp = None + build_page.terminals = None + build_page.buttons = None + self.show_frame(HomePage) + + def get_page(self, page_class): + """returns pages from stored frames""" + return self.frames[page_class] + + def view_matrix(self): + """prints current matrix to console""" + + build_page = self.get_page(BuildMDP) + _height = self.shared_data['height'].get() + _width = self.shared_data['width'].get() + print(build_page.gridmdp) + display(build_page.gridmdp, _height, _width) + + def view_terminals(self): + """prints current terminals to console""" + build_page = self.get_page(BuildMDP) + print('Terminals', build_page.terminals) + + def initialize(self): + """calls initialize from BuildMDP""" + + build_page = self.get_page(BuildMDP) + build_page.initialize() + + def master_reset(self): + """calls master_reset from BuildMDP""" + build_page = self.get_page(BuildMDP) + build_page.master_reset() + + def build(self): + """runs specified mdp solving algorithm""" + + frame = SolveMDP(self.container, self) + self.frames[SolveMDP] = frame + frame.grid(row=0, column=0, sticky='nsew') + self.show_frame(SolveMDP) + build_page = self.get_page(BuildMDP) + gridmdp = build_page.gridmdp + terminals = build_page.terminals + solve_page = self.get_page(SolveMDP) + _height = self.shared_data['height'].get() + _width = self.shared_data['width'].get() + solve_page.create_graph(gridmdp, terminals, _height, _width) + + def show_frame(self, controller, cb=False): + """shows specified frame and optionally runs create_buttons""" + if cb: + build_page = self.get_page(BuildMDP) + build_page.create_buttons() + frame = self.frames[controller] + frame.tkraise() + + +class HomePage(tk.Frame): + + def __init__(self, parent, controller): + """HomePage constructor""" + + tk.Frame.__init__(self, parent) + self.controller = controller + frame1 = tk.Frame(self) + frame1.pack(side=tk.TOP) + frame3 = tk.Frame(self) + frame3.pack(side=tk.TOP) + frame4 = tk.Frame(self) + frame4.pack(side=tk.TOP) + frame2 = tk.Frame(self) + frame2.pack(side=tk.TOP) + + s = ttk.Style() + s.theme_use('clam') + s.configure('TButton', background=grayd, padding=0) + s.configure('wall.TButton', background=gray2, foreground=white) + s.configure('reward.TButton', background=gray9) + s.configure('+term.TButton', background=green8) + s.configure('-term.TButton', background=pblue, foreground=white) + s.configure('=term.TButton', background=green4) + + label = ttk.Label(frame1, text='GridMDP builder', font=('Helvetica', 18, 'bold'), background=grayef) + label.pack(pady=75, padx=50, side=tk.TOP) + + ec_btn = ttk.Button(frame3, text='Empty cells', width=20) + ec_btn.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + ec_btn.configure(style='TButton') + + w_btn = ttk.Button(frame3, text='Walls', width=20) + w_btn.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + w_btn.configure(style='wall.TButton') + + r_btn = ttk.Button(frame3, text='Rewards', width=20) + r_btn.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + r_btn.configure(style='reward.TButton') + + term_p = ttk.Button(frame3, text='Positive terminals', width=20) + term_p.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + term_p.configure(style='+term.TButton') + + term_z = ttk.Button(frame3, text='Neutral terminals', width=20) + term_z.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + term_z.configure(style='=term.TButton') + + term_n = ttk.Button(frame3, text='Negative terminals', width=20) + term_n.pack(pady=0, padx=0, side=tk.LEFT, ipady=10) + term_n.configure(style='-term.TButton') + + label = ttk.Label(frame4, text='Dimensions', font=('Verdana', 14), background=grayef) + label.pack(pady=15, padx=10, side=tk.TOP) + entry_h = tk.Entry(frame2, textvariable=self.controller.shared_data['height'], font=('Verdana', 10), width=3, + justify=tk.CENTER) + entry_h.pack(pady=10, padx=10, side=tk.LEFT) + label_x = ttk.Label(frame2, text='X', font=('Verdana', 10), background=grayef) + label_x.pack(pady=10, padx=4, side=tk.LEFT) + entry_w = tk.Entry(frame2, textvariable=self.controller.shared_data['width'], font=('Verdana', 10), width=3, + justify=tk.CENTER) + entry_w.pack(pady=10, padx=10, side=tk.LEFT) + button = ttk.Button(self, text='Build a GridMDP', command=lambda: controller.show_frame(BuildMDP, cb=True)) + button.pack(pady=10, padx=10, side=tk.TOP, ipadx=20, ipady=10) + button.configure(style='reward.TButton') + + +class BuildMDP(tk.Frame): + + def __init__(self, parent, controller): + + tk.Frame.__init__(self, parent) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.frame = tk.Frame(self) + self.frame.pack() + self.controller = controller + + def create_buttons(self): + """creates interactive cells to build MDP""" + _height = self.controller.shared_data['height'].get() + _width = self.controller.shared_data['width'].get() + self.controller.menu_bar.entryconfig('Edit', state=tk.NORMAL) + self.controller.menu_bar.entryconfig('Build', state=tk.NORMAL) + self.gridmdp = [[0.0] * max(1, _width) for _ in range(max(1, _height))] + self.buttons = [[None] * max(1, _width) for _ in range(max(1, _height))] + self.terminals = [] + + s = ttk.Style() + s.theme_use('clam') + s.configure('TButton', background=grayd, padding=0) + s.configure('wall.TButton', background=gray2, foreground=white) + s.configure('reward.TButton', background=gray9) + s.configure('+term.TButton', background=green8) + s.configure('-term.TButton', background=pblue, foreground=white) + s.configure('=term.TButton', background=green4) + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + self.buttons[i][j] = ttk.Button(self.frame, text=f'({_height - i - 1}, {j})', + width=int(196 / max(1, _width)), + command=partial(dialogbox, i, j, self.gridmdp, self.terminals, + self.buttons, _height)) + self.buttons[i][j].grid(row=i, column=j, ipady=int(336 / max(1, _height)) - 12) + + def initialize(self): + """runs initialize_dialogbox""" + + _height = self.controller.shared_data['height'].get() + _width = self.controller.shared_data['width'].get() + initialize_dialogbox(_width, _height, self.gridmdp, self.terminals, self.buttons) + + def master_reset(self): + """runs external reset""" + _height = self.controller.shared_data['height'].get() + _width = self.controller.shared_data['width'].get() + if tkinter.messagebox.askokcancel('Reset', 'Are you sure you want to reset all cells?'): + external_reset(_width, _height, self.gridmdp, self.terminals, self.buttons) + + +class SolveMDP(tk.Frame): + + def __init__(self, parent, controller): + + tk.Frame.__init__(self, parent) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.frame = tk.Frame(self) + self.frame.pack() + self.controller = controller + self.terminated = False + self.iterations = 0 + self.epsilon = 0.001 + self.delta = 0 + + def process_data(self, terminals, _height, _width, gridmdp): + """preprocess variables""" + + flipped_terminals = [] + + for terminal in terminals: + flipped_terminals.append((terminal[1], _height - terminal[0] - 1)) + + grid_to_solve = [[0.0] * max(1, _width) for _ in range(max(1, _height))] + grid_to_show = [[0.0] * max(1, _width) for _ in range(max(1, _height))] + + for i in range(max(1, _height)): + for j in range(max(1, _width)): + if gridmdp[i][j] == WALL_VALUE: + grid_to_show[i][j] = 0.0 + grid_to_solve[i][j] = None + + else: + grid_to_show[i][j] = grid_to_solve[i][j] = gridmdp[i][j] + + return flipped_terminals, grid_to_solve, np.flipud(grid_to_show) + + def create_graph(self, gridmdp, terminals, _height, _width): + """creates canvas and initializes value_iteration_parameters""" + self._height = _height + self._width = _width + self.controller.menu_bar.entryconfig('Edit', state=tk.DISABLED) + self.controller.menu_bar.entryconfig('Build', state=tk.DISABLED) + + self.terminals, self.gridmdp, self.grid_to_show = self.process_data(terminals, _height, _width, gridmdp) + self.sequential_decision_environment = GridMDP(self.gridmdp, terminals=self.terminals) + + self.initialize_value_iteration_parameters(self.sequential_decision_environment) + + self.canvas = FigureCanvasTkAgg(fig, self.frame) + self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) + self.anim = animation.FuncAnimation(fig, self.animate_graph, interval=50) + self.canvas.show() + + def animate_graph(self, i): + """performs value iteration and animates graph""" + + # cmaps to use: bone_r, Oranges, inferno, BrBG, copper + self.iterations += 1 + x_interval = max(2, len(self.gridmdp[0])) + y_interval = max(2, len(self.gridmdp)) + x = np.linspace(0, len(self.gridmdp[0]) - 1, x_interval) + y = np.linspace(0, len(self.gridmdp) - 1, y_interval) + + sub.clear() + sub.imshow(self.grid_to_show, cmap='BrBG', aspect='auto', interpolation='none', extent=extents(x) + extents(y), + origin='lower') + fig.tight_layout() + + U = self.U1.copy() + + for s in self.sequential_decision_environment.states: + self.U1[s] = self.R(s) + self.gamma * max( + [sum([p * U[s1] for (p, s1) in self.T(s, a)]) for a in self.sequential_decision_environment.actions(s)]) + self.delta = max(self.delta, abs(self.U1[s] - U[s])) + + self.grid_to_show = grid_to_show = [[0.0] * max(1, self._width) for _ in range(max(1, self._height))] + for k, v in U.items(): + self.grid_to_show[k[1]][k[0]] = v + + if (self.delta < self.epsilon * (1 - self.gamma) / self.gamma) or ( + self.iterations > 60) and self.terminated is False: + self.terminated = True + display(self.grid_to_show, self._height, self._width) + + pi = best_policy(self.sequential_decision_environment, + value_iteration(self.sequential_decision_environment, .01)) + display_best_policy(self.sequential_decision_environment.to_arrows(pi), self._height, self._width) + + ax = fig.gca() + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + + def initialize_value_iteration_parameters(self, mdp): + """initializes value_iteration parameters""" + self.U1 = {s: 0 for s in mdp.states} + self.R, self.T, self.gamma = mdp.R, mdp.T, mdp.gamma + + def value_iteration_metastep(self, mdp, iterations=20): + """runs value_iteration""" + + U_over_time = [] + U1 = {s: 0 for s in mdp.states} + R, T, gamma = mdp.R, mdp.T, mdp.gamma + + for _ in range(iterations): + U = U1.copy() + + for s in mdp.states: + U1[s] = R(s) + gamma * max([sum([p * U[s1] for (p, s1) in T(s, a)]) for a in mdp.actions(s)]) + + U_over_time.append(U) + return U_over_time + + +if __name__ == '__main__': + app = MDPapp() + app.geometry('1280x720') + app.mainloop() diff --git a/gui/romania_problem.py b/gui/romania_problem.py new file mode 100644 index 000000000..9ec94099d --- /dev/null +++ b/gui/romania_problem.py @@ -0,0 +1,672 @@ +from copy import deepcopy +from tkinter import * + +from search import * +from utils import PriorityQueue + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +root = None +city_coord = {} +romania_problem = None +algo = None +start = None +goal = None +counter = -1 +city_map = None +frontier = None +front = None +node = None +next_button = None +explored = None + + +def create_map(root): + """This function draws out the required map.""" + global city_map, start, goal + romania_locations = romania_map.locations + width = 750 + height = 670 + margin = 5 + city_map = Canvas(root, width=width, height=height) + city_map.pack() + + # Since lines have to be drawn between particular points, we need to list + # them separately + make_line( + city_map, + romania_locations['Arad'][0], + height - + romania_locations['Arad'][1], + romania_locations['Sibiu'][0], + height - + romania_locations['Sibiu'][1], + romania_map.get('Arad', 'Sibiu')) + make_line( + city_map, + romania_locations['Arad'][0], + height - + romania_locations['Arad'][1], + romania_locations['Zerind'][0], + height - + romania_locations['Zerind'][1], + romania_map.get('Arad', 'Zerind')) + make_line( + city_map, + romania_locations['Arad'][0], + height - + romania_locations['Arad'][1], + romania_locations['Timisoara'][0], + height - + romania_locations['Timisoara'][1], + romania_map.get('Arad', 'Timisoara')) + make_line( + city_map, + romania_locations['Oradea'][0], + height - + romania_locations['Oradea'][1], + romania_locations['Zerind'][0], + height - + romania_locations['Zerind'][1], + romania_map.get('Oradea', 'Zerind')) + make_line( + city_map, + romania_locations['Oradea'][0], + height - + romania_locations['Oradea'][1], + romania_locations['Sibiu'][0], + height - + romania_locations['Sibiu'][1], + romania_map.get('Oradea', 'Sibiu')) + make_line( + city_map, + romania_locations['Lugoj'][0], + height - + romania_locations['Lugoj'][1], + romania_locations['Timisoara'][0], + height - + romania_locations['Timisoara'][1], + romania_map.get('Lugoj', 'Timisoara')) + make_line( + city_map, + romania_locations['Lugoj'][0], + height - + romania_locations['Lugoj'][1], + romania_locations['Mehadia'][0], + height - + romania_locations['Mehadia'][1], + romania_map.get('Lugoj', 'Mehadia')) + make_line( + city_map, + romania_locations['Drobeta'][0], + height - + romania_locations['Drobeta'][1], + romania_locations['Mehadia'][0], + height - + romania_locations['Mehadia'][1], + romania_map.get('Drobeta', 'Mehadia')) + make_line( + city_map, + romania_locations['Drobeta'][0], + height - + romania_locations['Drobeta'][1], + romania_locations['Craiova'][0], + height - + romania_locations['Craiova'][1], + romania_map.get('Drobeta', 'Craiova')) + make_line( + city_map, + romania_locations['Pitesti'][0], + height - + romania_locations['Pitesti'][1], + romania_locations['Craiova'][0], + height - + romania_locations['Craiova'][1], + romania_map.get('Pitesti', 'Craiova')) + make_line( + city_map, + romania_locations['Rimnicu'][0], + height - + romania_locations['Rimnicu'][1], + romania_locations['Craiova'][0], + height - + romania_locations['Craiova'][1], + romania_map.get('Rimnicu', 'Craiova')) + make_line( + city_map, + romania_locations['Rimnicu'][0], + height - + romania_locations['Rimnicu'][1], + romania_locations['Sibiu'][0], + height - + romania_locations['Sibiu'][1], + romania_map.get('Rimnicu', 'Sibiu')) + make_line( + city_map, + romania_locations['Rimnicu'][0], + height - + romania_locations['Rimnicu'][1], + romania_locations['Pitesti'][0], + height - + romania_locations['Pitesti'][1], + romania_map.get('Rimnicu', 'Pitesti')) + make_line( + city_map, + romania_locations['Bucharest'][0], + height - + romania_locations['Bucharest'][1], + romania_locations['Pitesti'][0], + height - + romania_locations['Pitesti'][1], + romania_map.get('Bucharest', 'Pitesti')) + make_line( + city_map, + romania_locations['Fagaras'][0], + height - + romania_locations['Fagaras'][1], + romania_locations['Sibiu'][0], + height - + romania_locations['Sibiu'][1], + romania_map.get('Fagaras', 'Sibiu')) + make_line( + city_map, + romania_locations['Fagaras'][0], + height - + romania_locations['Fagaras'][1], + romania_locations['Bucharest'][0], + height - + romania_locations['Bucharest'][1], + romania_map.get('Fagaras', 'Bucharest')) + make_line( + city_map, + romania_locations['Giurgiu'][0], + height - + romania_locations['Giurgiu'][1], + romania_locations['Bucharest'][0], + height - + romania_locations['Bucharest'][1], + romania_map.get('Giurgiu', 'Bucharest')) + make_line( + city_map, + romania_locations['Urziceni'][0], + height - + romania_locations['Urziceni'][1], + romania_locations['Bucharest'][0], + height - + romania_locations['Bucharest'][1], + romania_map.get('Urziceni', 'Bucharest')) + make_line( + city_map, + romania_locations['Urziceni'][0], + height - + romania_locations['Urziceni'][1], + romania_locations['Hirsova'][0], + height - + romania_locations['Hirsova'][1], + romania_map.get('Urziceni', 'Hirsova')) + make_line( + city_map, + romania_locations['Eforie'][0], + height - + romania_locations['Eforie'][1], + romania_locations['Hirsova'][0], + height - + romania_locations['Hirsova'][1], + romania_map.get('Eforie', 'Hirsova')) + make_line( + city_map, + romania_locations['Urziceni'][0], + height - + romania_locations['Urziceni'][1], + romania_locations['Vaslui'][0], + height - + romania_locations['Vaslui'][1], + romania_map.get('Urziceni', 'Vaslui')) + make_line( + city_map, + romania_locations['Iasi'][0], + height - + romania_locations['Iasi'][1], + romania_locations['Vaslui'][0], + height - + romania_locations['Vaslui'][1], + romania_map.get('Iasi', 'Vaslui')) + make_line( + city_map, + romania_locations['Iasi'][0], + height - + romania_locations['Iasi'][1], + romania_locations['Neamt'][0], + height - + romania_locations['Neamt'][1], + romania_map.get('Iasi', 'Neamt')) + + for city in romania_locations.keys(): + make_rectangle( + city_map, + romania_locations[city][0], + height - + romania_locations[city][1], + margin, + city) + + make_legend(city_map) + + +def make_line(map, x0, y0, x1, y1, distance): + """This function draws out the lines joining various points.""" + map.create_line(x0, y0, x1, y1) + map.create_text((x0 + x1) / 2, (y0 + y1) / 2, text=distance) + + +def make_rectangle(map, x0, y0, margin, city_name): + """This function draws out rectangles for various points.""" + global city_coord + rect = map.create_rectangle( + x0 - margin, + y0 - margin, + x0 + margin, + y0 + margin, + fill="white") + if "Bucharest" in city_name or "Pitesti" in city_name or "Lugoj" in city_name \ + or "Mehadia" in city_name or "Drobeta" in city_name: + map.create_text( + x0 - 2 * margin, + y0 - 2 * margin, + text=city_name, + anchor=E) + else: + map.create_text( + x0 - 2 * margin, + y0 - 2 * margin, + text=city_name, + anchor=SE) + city_coord.update({city_name: rect}) + + +def make_legend(map): + rect1 = map.create_rectangle(600, 100, 610, 110, fill="white") + text1 = map.create_text(615, 105, anchor=W, text="Un-explored") + + rect2 = map.create_rectangle(600, 115, 610, 125, fill="orange") + text2 = map.create_text(615, 120, anchor=W, text="Frontier") + + rect3 = map.create_rectangle(600, 130, 610, 140, fill="red") + text3 = map.create_text(615, 135, anchor=W, text="Currently Exploring") + + rect4 = map.create_rectangle(600, 145, 610, 155, fill="grey") + text4 = map.create_text(615, 150, anchor=W, text="Explored") + + rect5 = map.create_rectangle(600, 160, 610, 170, fill="dark green") + text5 = map.create_text(615, 165, anchor=W, text="Final Solution") + + +def tree_search(problem): + """ + Search through the successors of a problem to find a goal. + The argument frontier should be an empty queue. + Don't worry about repeated paths to a state. [Figure 3.7] + This function has been changed to make it suitable for the Tkinter GUI. + """ + global counter, frontier, node + + if counter == -1: + frontier.append(Node(problem.initial)) + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def graph_search(problem): + """ + Search through the successors of a problem to find a goal. + The argument frontier should be an empty queue. + If two paths reach a state, only use the first one. [Figure 3.7] + This function has been changed to make it suitable for the Tkinter GUI. + """ + global counter, frontier, node, explored + if counter == -1: + frontier.append(Node(problem.initial)) + explored = set() + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + explored.add(node.state) + frontier.extend(child for child in node.expand(problem) + if child.state not in explored and + child not in frontier) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def display_frontier(queue): + """This function marks the frontier nodes (orange) on the map.""" + global city_map, city_coord + qu = deepcopy(queue) + while qu: + node = qu.pop() + for city in city_coord.keys(): + if node.state == city: + city_map.itemconfig(city_coord[city], fill="orange") + + +def display_current(node): + """This function marks the currently exploring node (red) on the map.""" + global city_map, city_coord + city = node.state + city_map.itemconfig(city_coord[city], fill="red") + + +def display_explored(node): + """This function marks the already explored node (gray) on the map.""" + global city_map, city_coord + city = node.state + city_map.itemconfig(city_coord[city], fill="gray") + + +def display_final(cities): + """This function marks the final solution nodes (green) on the map.""" + global city_map, city_coord + for city in cities: + city_map.itemconfig(city_coord[city], fill="green") + + +def breadth_first_tree_search(problem): + """Search the shallowest nodes in the search tree first.""" + global frontier, counter, node + if counter == -1: + frontier = deque() + + if counter == -1: + frontier.append(Node(problem.initial)) + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.popleft() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def depth_first_tree_search(problem): + """Search the deepest nodes in the search tree first.""" + # This search algorithm might not work in case of repeated paths. + global frontier, counter, node + if counter == -1: + frontier = [] # stack + + if counter == -1: + frontier.append(Node(problem.initial)) + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def breadth_first_graph_search(problem): + """[Figure 3.11]""" + global frontier, node, explored, counter + if counter == -1: + node = Node(problem.initial) + display_current(node) + if problem.goal_test(node.state): + return node + + frontier = deque([node]) # FIFO queue + + display_frontier(frontier) + explored = set() + if counter % 3 == 0 and counter >= 0: + node = frontier.popleft() + display_current(node) + explored.add(node.state) + if counter % 3 == 1 and counter >= 0: + for child in node.expand(problem): + if child.state not in explored and child not in frontier: + if problem.goal_test(child.state): + return child + frontier.append(child) + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def depth_first_graph_search(problem): + """Search the deepest nodes in the search tree first.""" + global counter, frontier, node, explored + if counter == -1: + frontier = [] # stack + if counter == -1: + frontier.append(Node(problem.initial)) + explored = set() + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + explored.add(node.state) + frontier.extend(child for child in node.expand(problem) + if child.state not in explored and + child not in frontier) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + 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, + if f is a heuristic estimate to the goal, then we have greedy best + first search; if f is node.depth then we have breadth-first search. + There is a subtlety: the line "f = memoize(f, 'f')" means that the f + values will be cached on the nodes as they are computed. So after doing + a best first search you can examine the f values of the path returned.""" + global frontier, node, explored, counter + + if counter == -1: + f = memoize(f, 'f') + node = Node(problem.initial) + display_current(node) + if problem.goal_test(node.state): + return node + frontier = PriorityQueue('min', f) + frontier.append(node) + display_frontier(frontier) + explored = set() + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + display_current(node) + if problem.goal_test(node.state): + return node + explored.add(node.state) + if counter % 3 == 1 and counter >= 0: + for child in node.expand(problem): + if child.state not in explored and child not in frontier: + frontier.append(child) + elif child in frontier: + if f(child) < frontier[child]: + del frontier[child] + frontier.append(child) + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None + + +def uniform_cost_search(problem): + """[Figure 3.14]""" + return best_first_graph_search(problem, lambda node: node.path_cost) + + +def astar_search(problem, h=None): + """A* search is best-first graph search with f(n) = g(n)+h(n). + You need to specify the h function when you call astar_search, or + else in your Problem subclass.""" + h = memoize(h or problem.h, 'h') + return best_first_graph_search(problem, lambda n: n.path_cost + h(n)) + + +# TODO: +# Remove redundant code. +# Make the interchangeability work between various algorithms at each step. +def on_click(): + """ + This function defines the action of the 'Next' button. + """ + global algo, counter, next_button, romania_problem, start, goal + romania_problem = GraphProblem(start.get(), goal.get(), romania_map) + if "Breadth-First Tree Search" == algo.get(): + node = breadth_first_tree_search(romania_problem) + if node is not None: + final_path = breadth_first_tree_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + elif "Depth-First Tree Search" == algo.get(): + node = depth_first_tree_search(romania_problem) + if node is not None: + final_path = depth_first_tree_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + elif "Breadth-First Graph Search" == algo.get(): + node = breadth_first_graph_search(romania_problem) + if node is not None: + final_path = breadth_first_graph_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + elif "Depth-First Graph Search" == algo.get(): + node = depth_first_graph_search(romania_problem) + if node is not None: + final_path = depth_first_graph_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + elif "Uniform Cost Search" == algo.get(): + node = uniform_cost_search(romania_problem) + if node is not None: + final_path = uniform_cost_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + elif "A* - Search" == algo.get(): + node = astar_search(romania_problem) + if node is not None: + final_path = astar_search(romania_problem).solution() + final_path.append(start.get()) + display_final(final_path) + next_button.config(state="disabled") + counter += 1 + + +def reset_map(): + global counter, city_coord, city_map, next_button + counter = -1 + for city in city_coord.keys(): + city_map.itemconfig(city_coord[city], fill="white") + next_button.config(state="normal") + + +# TODO: Add more search algorithms in the OptionMenu +if __name__ == "__main__": + global algo, start, goal, next_button + root = Tk() + root.title("Road Map of Romania") + root.geometry("950x1150") + algo = StringVar(root) + start = StringVar(root) + goal = StringVar(root) + algo.set("Breadth-First Tree Search") + start.set('Arad') + goal.set('Bucharest') + cities = sorted(romania_map.locations.keys()) + algorithm_menu = OptionMenu( + root, + algo, "Breadth-First Tree Search", "Depth-First Tree Search", + "Breadth-First Graph Search", "Depth-First Graph Search", + "Uniform Cost Search", "A* - Search") + Label(root, text="\n Search Algorithm").pack() + algorithm_menu.pack() + Label(root, text="\n Start City").pack() + start_menu = OptionMenu(root, start, *cities) + start_menu.pack() + Label(root, text="\n Goal City").pack() + goal_menu = OptionMenu(root, goal, *cities) + goal_menu.pack() + frame1 = Frame(root) + next_button = Button( + frame1, + width=6, + height=2, + text="Next", + command=on_click, + padx=2, + pady=2, + relief=GROOVE) + next_button.pack(side=RIGHT) + reset_button = Button( + frame1, + width=6, + height=2, + text="Reset", + command=reset_map, + padx=2, + pady=2, + relief=GROOVE) + reset_button.pack(side=RIGHT) + frame1.pack(side=BOTTOM) + create_map(root) + root.mainloop() diff --git a/gui/tic-tac-toe.py b/gui/tic-tac-toe.py new file mode 100644 index 000000000..66d9d6e75 --- /dev/null +++ b/gui/tic-tac-toe.py @@ -0,0 +1,232 @@ +import os.path +from tkinter import * + +from games import minmax_decision, alpha_beta_player, random_player, TicTacToe +# "gen_state" can be used to generate a game state to apply the algorithm +from tests.test_games import gen_state + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +ttt = TicTacToe() +root = None +buttons = [] +frames = [] +x_pos = [] +o_pos = [] +count = 0 +sym = "" +result = None +choices = None + + +def create_frames(root): + """ + This function creates the necessary structure of the game. + """ + frame1 = Frame(root) + frame2 = Frame(root) + frame3 = Frame(root) + frame4 = Frame(root) + create_buttons(frame1) + create_buttons(frame2) + create_buttons(frame3) + buttonExit = Button( + frame4, height=1, width=2, + text="Exit", + command=lambda: exit_game(root)) + buttonExit.pack(side=LEFT) + frame4.pack(side=BOTTOM) + frame3.pack(side=BOTTOM) + frame2.pack(side=BOTTOM) + frame1.pack(side=BOTTOM) + frames.append(frame1) + frames.append(frame2) + frames.append(frame3) + for x in frames: + buttons_in_frame = [] + for y in x.winfo_children(): + buttons_in_frame.append(y) + buttons.append(buttons_in_frame) + buttonReset = Button(frame4, height=1, width=2, + text="Reset", command=lambda: reset_game()) + buttonReset.pack(side=LEFT) + + +def create_buttons(frame): + """ + This function creates the buttons to be pressed/clicked during the game. + """ + button0 = Button(frame, height=2, width=2, text=" ", + command=lambda: on_click(button0)) + button0.pack(side=LEFT) + button1 = Button(frame, height=2, width=2, text=" ", + command=lambda: on_click(button1)) + button1.pack(side=LEFT) + button2 = Button(frame, height=2, width=2, text=" ", + command=lambda: on_click(button2)) + button2.pack(side=LEFT) + + +# TODO: Add a choice option for the user. +def on_click(button): + """ + This function determines the action of any button. + """ + global ttt, choices, count, sym, result, x_pos, o_pos + + if count % 2 == 0: + sym = "X" + else: + sym = "O" + count += 1 + + button.config( + text=sym, + state='disabled', + disabledforeground="red") # For cross + + x, y = get_coordinates(button) + x += 1 + y += 1 + x_pos.append((x, y)) + state = gen_state(to_move='O', x_positions=x_pos, + o_positions=o_pos) + try: + choice = choices.get() + if "Random" in choice: + a, b = random_player(ttt, state) + elif "Pro" in choice: + a, b = minmax_decision(state, ttt) + else: + a, b = alpha_beta_player(ttt, state) + except (ValueError, IndexError, TypeError) as e: + disable_game() + result.set("It's a draw :|") + return + if 1 <= a <= 3 and 1 <= b <= 3: + o_pos.append((a, b)) + button_to_change = get_button(a - 1, b - 1) + if count % 2 == 0: # Used again, will become handy when user is given the choice of turn. + sym = "X" + else: + sym = "O" + count += 1 + + if check_victory(button): + result.set("You win :)") + disable_game() + else: + button_to_change.config(text=sym, state='disabled', + disabledforeground="black") + if check_victory(button_to_change): + result.set("You lose :(") + disable_game() + + +# TODO: Replace "check_victory" by "k_in_row" function. +def check_victory(button): + """ + This function checks various winning conditions of the game. + """ + # check if previous move caused a win on vertical line + global buttons + x, y = get_coordinates(button) + tt = button['text'] + if buttons[0][y]['text'] == buttons[1][y]['text'] == buttons[2][y]['text'] != " ": + buttons[0][y].config(text="|" + tt + "|") + buttons[1][y].config(text="|" + tt + "|") + buttons[2][y].config(text="|" + tt + "|") + return True + + # check if previous move caused a win on horizontal line + if buttons[x][0]['text'] == buttons[x][1]['text'] == buttons[x][2]['text'] != " ": + buttons[x][0].config(text="--" + tt + "--") + buttons[x][1].config(text="--" + tt + "--") + buttons[x][2].config(text="--" + tt + "--") + return True + + # check if previous move was on the main diagonal and caused a win + if x == y and buttons[0][0]['text'] == buttons[1][1]['text'] == buttons[2][2]['text'] != " ": + buttons[0][0].config(text="\\" + tt + "\\") + buttons[1][1].config(text="\\" + tt + "\\") + buttons[2][2].config(text="\\" + tt + "\\") + return True + + # check if previous move was on the secondary diagonal and caused a win + if x + y == 2 and buttons[0][2]['text'] == buttons[1][1]['text'] == buttons[2][0]['text'] != " ": + buttons[0][2].config(text="/" + tt + "/") + buttons[1][1].config(text="/" + tt + "/") + buttons[2][0].config(text="/" + tt + "/") + return True + + return False + + +def get_coordinates(button): + """ + This function returns the coordinates of the button clicked. + """ + global buttons + for x in range(len(buttons)): + for y in range(len(buttons[x])): + if buttons[x][y] == button: + return x, y + + +def get_button(x, y): + """ + This function returns the button memory location corresponding to a coordinate. + """ + global buttons + return buttons[x][y] + + +def reset_game(): + """ + This function will reset all the tiles to the initial null value. + """ + global x_pos, o_pos, frames, count + + count = 0 + x_pos = [] + o_pos = [] + result.set("Your Turn!") + for x in frames: + for y in x.winfo_children(): + y.config(text=" ", state='normal') + + +def disable_game(): + """ + This function deactivates the game after a win, loss or draw. + """ + global frames + for x in frames: + for y in x.winfo_children(): + y.config(state='disabled') + + +def exit_game(root): + """ + This function will exit the game by killing the root. + """ + root.destroy() + + +if __name__ == "__main__": + global result, choices + + root = Tk() + root.title("TicTacToe") + root.geometry("150x200") # Improved the window geometry + root.resizable(0, 0) # To remove the maximize window option + result = StringVar() + result.set("Your Turn!") + w = Label(root, textvariable=result) + w.pack(side=BOTTOM) + create_frames(root) + choices = StringVar(root) + choices.set("Vs Pro") + menu = OptionMenu(root, choices, "Vs Random", "Vs Pro", "Vs Legend") + menu.pack() + root.mainloop() diff --git a/gui/tsp.py b/gui/tsp.py new file mode 100644 index 000000000..590fff354 --- /dev/null +++ b/gui/tsp.py @@ -0,0 +1,342 @@ +from tkinter import * +from tkinter import messagebox + +import utils +from search import * + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +distances = {} + + +class TSProblem(Problem): + """subclass of Problem to define various functions""" + + def two_opt(self, state): + """Neighbour generating function for Traveling Salesman Problem""" + neighbour_state = state[:] + left = random.randint(0, len(neighbour_state) - 1) + right = random.randint(0, len(neighbour_state) - 1) + if left > right: + left, right = right, left + neighbour_state[left: right + 1] = reversed(neighbour_state[left: right + 1]) + return neighbour_state + + def actions(self, state): + """action that can be executed in given state""" + return [self.two_opt] + + def result(self, state, action): + """result after applying the given action on the given state""" + return action(state) + + def path_cost(self, c, state1, action, state2): + """total distance for the Traveling Salesman to be covered if in state2""" + cost = 0 + for i in range(len(state2) - 1): + cost += distances[state2[i]][state2[i + 1]] + cost += distances[state2[0]][state2[-1]] + return cost + + def value(self, state): + """value of path cost given negative for the given state""" + return -1 * self.path_cost(None, None, None, state) + + +class TSPGui(): + """Class to create gui of Traveling Salesman using simulated annealing where one can + select cities, change speed and temperature. Distances between cities are euclidean + distances between them. + """ + + def __init__(self, root, all_cities): + self.root = root + self.vars = [] + self.frame_locations = {} + self.calculate_canvas_size() + self.button_text = StringVar() + self.button_text.set("Start") + self.algo_var = StringVar() + self.all_cities = all_cities + self.frame_select_cities = Frame(self.root) + self.frame_select_cities.grid(row=1) + self.frame_canvas = Frame(self.root) + self.frame_canvas.grid(row=2) + Label(self.root, text="Map of Romania", font="Times 13 bold").grid(row=0, columnspan=10) + + def create_checkboxes(self, side=LEFT, anchor=W): + """To select cities which are to be a part of Traveling Salesman Problem""" + + row_number = 0 + column_number = 0 + + for city in self.all_cities: + var = IntVar() + var.set(1) + Checkbutton(self.frame_select_cities, text=city, variable=var).grid( + row=row_number, column=column_number, sticky=W) + + self.vars.append(var) + column_number += 1 + if column_number == 10: + column_number = 0 + row_number += 1 + + def create_buttons(self): + """Create start and quit button""" + + Button(self.frame_select_cities, textvariable=self.button_text, + command=self.run_traveling_salesman).grid(row=5, column=4, sticky=E + W) + Button(self.frame_select_cities, text='Quit', command=self.on_closing).grid( + row=5, column=5, sticky=E + W) + + def create_dropdown_menu(self): + """Create dropdown menu for algorithm selection""" + + choices = {'Simulated Annealing', 'Genetic Algorithm', 'Hill Climbing'} + self.algo_var.set('Simulated Annealing') + dropdown_menu = OptionMenu(self.frame_select_cities, self.algo_var, *choices) + dropdown_menu.grid(row=4, column=4, columnspan=2, sticky=E + W) + dropdown_menu.config(width=19) + + def run_traveling_salesman(self): + """Choose selected cities""" + + cities = [] + for i in range(len(self.vars)): + if self.vars[i].get() == 1: + cities.append(self.all_cities[i]) + + tsp_problem = TSProblem(cities) + self.button_text.set("Reset") + self.create_canvas(tsp_problem) + + def calculate_canvas_size(self): + """Width and height for canvas""" + + minx, maxx = sys.maxsize, -1 * sys.maxsize + miny, maxy = sys.maxsize, -1 * sys.maxsize + + for value in romania_map.locations.values(): + minx = min(minx, value[0]) + maxx = max(maxx, value[0]) + miny = min(miny, value[1]) + maxy = max(maxy, value[1]) + + # New locations squeezed to fit inside the map of romania + for name, coordinates in romania_map.locations.items(): + self.frame_locations[name] = (coordinates[0] / 1.2 - minx + + 150, coordinates[1] / 1.2 - miny + 165) + + canvas_width = maxx - minx + 200 + canvas_height = maxy - miny + 200 + + self.canvas_width = canvas_width + self.canvas_height = canvas_height + + def create_canvas(self, problem): + """creating map with cities""" + + map_canvas = Canvas(self.frame_canvas, width=self.canvas_width, height=self.canvas_height) + map_canvas.grid(row=3, columnspan=10) + current = Node(problem.initial) + map_canvas.delete("all") + self.romania_image = PhotoImage(file="../images/romania_map.png") + map_canvas.create_image(self.canvas_width / 2, self.canvas_height / 2, + image=self.romania_image) + cities = current.state + for city in cities: + x = self.frame_locations[city][0] + y = self.frame_locations[city][1] + map_canvas.create_oval(x - 3, y - 3, x + 3, y + 3, + fill="red", outline="red") + map_canvas.create_text(x - 15, y - 10, text=city) + + self.cost = StringVar() + Label(self.frame_canvas, textvariable=self.cost, relief="sunken").grid( + row=2, columnspan=10) + + self.speed = IntVar() + speed_scale = Scale(self.frame_canvas, from_=500, to=1, orient=HORIZONTAL, + variable=self.speed, label="Speed ----> ", showvalue=0, font="Times 11", + relief="sunken", cursor="gumby") + speed_scale.grid(row=1, columnspan=5, sticky=N + S + E + W) + + if self.algo_var.get() == 'Simulated Annealing': + self.temperature = IntVar() + temperature_scale = Scale(self.frame_canvas, from_=100, to=0, orient=HORIZONTAL, + length=200, variable=self.temperature, label="Temperature ---->", + font="Times 11", relief="sunken", showvalue=0, cursor="gumby") + temperature_scale.grid(row=1, column=5, columnspan=5, sticky=N + S + E + W) + self.simulated_annealing_with_tunable_T(problem, map_canvas) + elif self.algo_var.get() == 'Genetic Algorithm': + self.mutation_rate = DoubleVar() + self.mutation_rate.set(0.05) + mutation_rate_scale = Scale(self.frame_canvas, from_=0, to=1, orient=HORIZONTAL, + length=200, variable=self.mutation_rate, label='Mutation Rate ---->', + font='Times 11', relief='sunken', showvalue=0, cursor='gumby', resolution=0.001) + mutation_rate_scale.grid(row=1, column=5, columnspan=5, sticky='nsew') + self.genetic_algorithm(problem, map_canvas) + elif self.algo_var.get() == 'Hill Climbing': + self.no_of_neighbors = IntVar() + self.no_of_neighbors.set(100) + no_of_neighbors_scale = Scale(self.frame_canvas, from_=10, to=1000, orient=HORIZONTAL, + length=200, variable=self.no_of_neighbors, label='Number of neighbors ---->', + font='Times 11', relief='sunken', showvalue=0, cursor='gumby') + no_of_neighbors_scale.grid(row=1, column=5, columnspan=5, sticky='nsew') + self.hill_climbing(problem, map_canvas) + + def exp_schedule(k=100, lam=0.03, limit=1000): + """One possible schedule function for simulated annealing""" + + return lambda t: (k * np.exp(-lam * t) if t < limit else 0) + + def simulated_annealing_with_tunable_T(self, problem, map_canvas, schedule=exp_schedule()): + """Simulated annealing where temperature is taken as user input""" + + current = Node(problem.initial) + + while True: + T = schedule(self.temperature.get()) + if T == 0: + return current.state + neighbors = current.expand(problem) + if not neighbors: + return current.state + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) + if delta_e > 0 or probability(np.exp(delta_e / T)): + map_canvas.delete("poly") + + current = next + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(current.state)))) + points = [] + for city in current.state: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=3, fill='', tag="poly") + map_canvas.update() + map_canvas.after(self.speed.get()) + + def genetic_algorithm(self, problem, map_canvas): + """Genetic Algorithm modified for the given problem""" + + def init_population(pop_number, gene_pool, state_length): + """initialize population""" + + population = [] + for i in range(pop_number): + population.append(utils.shuffled(gene_pool)) + return population + + def recombine(state_a, state_b): + """recombine two problem states""" + + start = random.randint(0, len(state_a) - 1) + end = random.randint(start + 1, len(state_a)) + new_state = state_a[start:end] + for city in state_b: + if city not in new_state: + new_state.append(city) + return new_state + + def mutate(state, mutation_rate): + """mutate problem states""" + + if random.uniform(0, 1) < mutation_rate: + sample = random.sample(range(len(state)), 2) + state[sample[0]], state[sample[1]] = state[sample[1]], state[sample[0]] + return state + + def fitness_fn(state): + """calculate fitness of a particular state""" + + fitness = problem.value(state) + return int((5600 + fitness) ** 2) + + current = Node(problem.initial) + population = init_population(100, current.state, len(current.state)) + all_time_best = current.state + while True: + population = [mutate(recombine(*select(2, population, fitness_fn)), self.mutation_rate.get()) + for _ in range(len(population))] + current_best = np.argmax(population, key=fitness_fn) + if fitness_fn(current_best) > fitness_fn(all_time_best): + all_time_best = current_best + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(all_time_best)))) + map_canvas.delete('poly') + points = [] + for city in current_best: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=1, fill='', tag='poly') + best_points = [] + for city in all_time_best: + best_points.append(self.frame_locations[city][0]) + best_points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(best_points, outline='red', width=3, fill='', tag='poly') + map_canvas.update() + map_canvas.after(self.speed.get()) + + def hill_climbing(self, problem, map_canvas): + """hill climbing where number of neighbors is taken as user input""" + + def find_neighbors(state, number_of_neighbors=100): + """finds neighbors using two_opt method""" + + neighbors = [] + for i in range(number_of_neighbors): + new_state = problem.two_opt(state) + neighbors.append(Node(new_state)) + state = new_state + return neighbors + + current = Node(problem.initial) + while True: + neighbors = find_neighbors(current.state, self.no_of_neighbors.get()) + neighbor = np.argmax_random_tie(neighbors, key=lambda node: problem.value(node.state)) + map_canvas.delete('poly') + points = [] + for city in current.state: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=3, fill='', tag='poly') + neighbor_points = [] + for city in neighbor.state: + neighbor_points.append(self.frame_locations[city][0]) + neighbor_points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(neighbor_points, outline='red', width=1, fill='', tag='poly') + map_canvas.update() + map_canvas.after(self.speed.get()) + if problem.value(neighbor.state) > problem.value(current.state): + current.state = neighbor.state + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(current.state)))) + + def on_closing(self): + if messagebox.askokcancel('Quit', 'Do you want to quit?'): + self.root.destroy() + + +if __name__ == '__main__': + all_cities = [] + for city in romania_map.locations.keys(): + distances[city] = {} + all_cities.append(city) + all_cities.sort() + + # distances['city1']['city2'] contains euclidean distance between their coordinates + for name_1, coordinates_1 in romania_map.locations.items(): + for name_2, coordinates_2 in romania_map.locations.items(): + distances[name_1][name_2] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + distances[name_2][name_1] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + + root = Tk() + root.title("Traveling Salesman Problem") + cities_selection_panel = TSPGui(root, all_cities) + cities_selection_panel.create_checkboxes() + cities_selection_panel.create_buttons() + cities_selection_panel.create_dropdown_menu() + root.protocol('WM_DELETE_WINDOW', cities_selection_panel.on_closing) + root.mainloop() diff --git a/gui/vacuum_agent.py b/gui/vacuum_agent.py new file mode 100644 index 000000000..b07dab282 --- /dev/null +++ b/gui/vacuum_agent.py @@ -0,0 +1,154 @@ +import os.path +from tkinter import * + +from agents import * + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world + + +class Gui(Environment): + """This GUI environment has two locations, A and B. Each can be Dirty + or Clean. The agent perceives its location and the location's + status.""" + + def __init__(self, root, height=300, width=380): + super().__init__() + self.status = {loc_A: 'Clean', + loc_B: 'Clean'} + self.root = root + self.height = height + self.width = width + self.canvas = None + self.buttons = [] + self.create_canvas() + self.create_buttons() + + def thing_classes(self): + """The list of things which can be used in the environment.""" + return [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent, + TableDrivenVacuumAgent, ModelBasedVacuumAgent] + + def percept(self, agent): + """Returns the agent's location, and the location status (Dirty/Clean).""" + return agent.location, self.status[agent.location] + + def execute_action(self, agent, action): + """Change the location status (Dirty/Clean); track performance. + Score 10 for each dirt cleaned; -1 for each move.""" + if action == 'Right': + agent.location = loc_B + agent.performance -= 1 + elif action == 'Left': + agent.location = loc_A + agent.performance -= 1 + elif action == 'Suck': + if self.status[agent.location] == 'Dirty': + if agent.location == loc_A: + self.buttons[0].config(bg='white', activebackground='light grey') + else: + self.buttons[1].config(bg='white', activebackground='light grey') + agent.performance += 10 + self.status[agent.location] = 'Clean' + + def default_location(self, thing): + """Agents start in either location at random.""" + return random.choice([loc_A, loc_B]) + + def create_canvas(self): + """Creates Canvas element in the GUI.""" + self.canvas = Canvas( + self.root, + width=self.width, + height=self.height, + background='powder blue') + self.canvas.pack(side='bottom') + + def create_buttons(self): + """Creates the buttons required in the GUI.""" + button_left = Button(self.root, height=4, width=12, padx=2, pady=2, bg='white') + button_left.config(command=lambda btn=button_left: self.dirt_switch(btn)) + self.buttons.append(button_left) + button_left_window = self.canvas.create_window(130, 200, anchor=N, window=button_left) + button_right = Button(self.root, height=4, width=12, padx=2, pady=2, bg='white') + button_right.config(command=lambda btn=button_right: self.dirt_switch(btn)) + self.buttons.append(button_right) + button_right_window = self.canvas.create_window(250, 200, anchor=N, window=button_right) + + def dirt_switch(self, button): + """Gives user the option to put dirt in any tile.""" + bg_color = button['bg'] + if bg_color == 'saddle brown': + button.config(bg='white', activebackground='light grey') + elif bg_color == 'white': + button.config(bg='saddle brown', activebackground='light goldenrod') + + def read_env(self): + """Reads the current state of the GUI.""" + for i, btn in enumerate(self.buttons): + if i == 0: + if btn['bg'] == 'white': + self.status[loc_A] = 'Clean' + else: + self.status[loc_A] = 'Dirty' + else: + if btn['bg'] == 'white': + self.status[loc_B] = 'Clean' + else: + self.status[loc_B] = 'Dirty' + + def update_env(self, agent): + """Updates the GUI according to the agent's action.""" + self.read_env() + # print(self.status) + before_step = agent.location + self.step() + # print(self.status) + # print(agent.location) + move_agent(self, agent, before_step) + + +def create_agent(env, agent): + """Creates the agent in the GUI and is kept independent of the environment.""" + env.add_thing(agent) + # print(agent.location) + if agent.location == (0, 0): + env.agent_rect = env.canvas.create_rectangle(80, 100, 175, 180, fill='lime green') + env.text = env.canvas.create_text(128, 140, font="Helvetica 10 bold italic", text="Agent") + else: + env.agent_rect = env.canvas.create_rectangle(200, 100, 295, 180, fill='lime green') + env.text = env.canvas.create_text(248, 140, font="Helvetica 10 bold italic", text="Agent") + + +def move_agent(env, agent, before_step): + """Moves the agent in the GUI when 'next' button is pressed.""" + if agent.location == before_step: + pass + else: + if agent.location == (1, 0): + env.canvas.move(env.text, 120, 0) + env.canvas.move(env.agent_rect, 120, 0) + elif agent.location == (0, 0): + env.canvas.move(env.text, -120, 0) + env.canvas.move(env.agent_rect, -120, 0) + + +# TODO: Add more agents to the environment. +# TODO: Expand the environment to XYEnvironment. +if __name__ == "__main__": + root = Tk() + root.title("Vacuum Environment") + root.geometry("420x380") + root.resizable(0, 0) + frame = Frame(root, bg='black') + # reset_button = Button(frame, text='Reset', height=2, width=6, padx=2, pady=2, command=None) + # reset_button.pack(side='left') + next_button = Button(frame, text='Next', height=2, width=6, padx=2, pady=2) + next_button.pack(side='left') + frame.pack(side='bottom') + env = Gui(root) + agent = ReflexVacuumAgent() + create_agent(env, agent) + next_button.config(command=lambda: env.update_env(agent)) + root.mainloop() diff --git a/gui/xy_vacuum_environment.py b/gui/xy_vacuum_environment.py new file mode 100644 index 000000000..093abc6c3 --- /dev/null +++ b/gui/xy_vacuum_environment.py @@ -0,0 +1,191 @@ +import os.path +from tkinter import * + +from agents import * + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + + +class Gui(VacuumEnvironment): + """This is a two-dimensional GUI environment. Each location may be + dirty, clean or can have a wall. The user can change these at each step. + """ + xi, yi = (0, 0) + perceptible_distance = 1 + + def __init__(self, root, width=7, height=7, elements=None): + super().__init__(width, height) + if elements is None: + elements = ['D', 'W'] + self.root = root + self.create_frames() + self.create_buttons() + self.create_walls() + self.elements = elements + + def create_frames(self): + """Adds frames to the GUI environment.""" + self.frames = [] + for _ in range(7): + frame = Frame(self.root, bg='grey') + frame.pack(side='bottom') + self.frames.append(frame) + + def create_buttons(self): + """Adds buttons to the respective frames in the GUI.""" + self.buttons = [] + for frame in self.frames: + button_row = [] + for _ in range(7): + button = Button(frame, height=3, width=5, padx=2, pady=2) + button.config( + command=lambda btn=button: self.display_element(btn)) + button.pack(side='left') + button_row.append(button) + self.buttons.append(button_row) + + def create_walls(self): + """Creates the outer boundary walls which do not move.""" + for row, button_row in enumerate(self.buttons): + if row == 0 or row == len(self.buttons) - 1: + for button in button_row: + button.config(text='W', state='disabled', + disabledforeground='black') + else: + button_row[0].config( + text='W', state='disabled', disabledforeground='black') + button_row[len(button_row) - 1].config(text='W', + state='disabled', disabledforeground='black') + # Place the agent in the centre of the grid. + self.buttons[3][3].config( + text='A', state='disabled', disabledforeground='black') + + def display_element(self, button): + """Show the things on the GUI.""" + txt = button['text'] + if txt != 'A': + if txt == 'W': + button.config(text='D') + elif txt == 'D': + button.config(text='') + elif txt == '': + button.config(text='W') + + def execute_action(self, agent, action): + """Determines the action the agent performs.""" + xi, yi = (self.xi, self.yi) + if action == 'Suck': + dirt_list = self.list_things_at(agent.location, Dirt) + if dirt_list: + dirt = dirt_list[0] + agent.performance += 100 + self.delete_thing(dirt) + self.buttons[xi][yi].config(text='', state='normal') + xf, yf = agent.location + self.buttons[xf][yf].config( + text='A', state='disabled', disabledforeground='black') + + else: + agent.bump = False + if action == 'TurnRight': + agent.direction += Direction.R + elif action == 'TurnLeft': + agent.direction += Direction.L + elif action == 'Forward': + agent.bump = self.move_to(agent, agent.direction.move_forward(agent.location)) + if not agent.bump: + self.buttons[xi][yi].config(text='', state='normal') + xf, yf = agent.location + self.buttons[xf][yf].config( + text='A', state='disabled', disabledforeground='black') + + if action != 'NoOp': + agent.performance -= 1 + + def read_env(self): + """Reads the current state of the GUI environment.""" + for i, btn_row in enumerate(self.buttons): + for j, btn in enumerate(btn_row): + if (i != 0 and i != len(self.buttons) - 1) and (j != 0 and j != len(btn_row) - 1): + agt_loc = self.agents[0].location + if self.some_things_at((i, j)) and (i, j) != agt_loc: + for thing in self.list_things_at((i, j)): + self.delete_thing(thing) + if btn['text'] == self.elements[0]: + self.add_thing(Dirt(), (i, j)) + elif btn['text'] == self.elements[1]: + self.add_thing(Wall(), (i, j)) + + def update_env(self): + """Updates the GUI environment according to the current state.""" + self.read_env() + agt = self.agents[0] + previous_agent_location = agt.location + self.xi, self.yi = previous_agent_location + self.step() + xf, yf = agt.location + + def reset_env(self, agt): + """Resets the GUI environment to the initial state.""" + self.read_env() + for i, btn_row in enumerate(self.buttons): + for j, btn in enumerate(btn_row): + if (i != 0 and i != len(self.buttons) - 1) and (j != 0 and j != len(btn_row) - 1): + if self.some_things_at((i, j)): + for thing in self.list_things_at((i, j)): + self.delete_thing(thing) + btn.config(text='', state='normal') + self.add_thing(agt, location=(3, 3)) + self.buttons[3][3].config( + text='A', state='disabled', disabledforeground='black') + + +def XYReflexAgentProgram(percept): + """The modified SimpleReflexAgentProgram for the GUI environment.""" + status, bump = percept + if status == 'Dirty': + return 'Suck' + + if bump == 'Bump': + value = random.choice((1, 2)) + else: + value = random.choice((1, 2, 3, 4)) # 1-right, 2-left, others-forward + + if value == 1: + return 'TurnRight' + elif value == 2: + return 'TurnLeft' + else: + return 'Forward' + + +class XYReflexAgent(Agent): + """The modified SimpleReflexAgent for the GUI environment.""" + + def __init__(self, program=None): + super().__init__(program) + self.location = (3, 3) + self.direction = Direction("up") + + +# TODO: Check the coordinate system. +# TODO: Give manual choice for agent's location. +if __name__ == "__main__": + root = Tk() + root.title("Vacuum Environment") + root.geometry("420x440") + root.resizable(0, 0) + frame = Frame(root, bg='black') + reset_button = Button(frame, text='Reset', height=2, + width=6, padx=2, pady=2) + reset_button.pack(side='left') + next_button = Button(frame, text='Next', height=2, + width=6, padx=2, pady=2) + next_button.pack(side='left') + frame.pack(side='bottom') + env = Gui(root) + agt = XYReflexAgent(program=XYReflexAgentProgram) + env.add_thing(agt, location=(3, 3)) + next_button.config(command=env.update_env) + reset_button.config(command=lambda: env.reset_env(agt)) + root.mainloop() diff --git a/images/-0.04.jpg b/images/-0.04.jpg new file mode 100644 index 000000000..3cf276421 Binary files /dev/null and b/images/-0.04.jpg differ diff --git a/images/-0.4.jpg b/images/-0.4.jpg new file mode 100644 index 000000000..b274d2ce3 Binary files /dev/null and b/images/-0.4.jpg differ diff --git a/images/-4.jpg b/images/-4.jpg new file mode 100644 index 000000000..79eefb0cd Binary files /dev/null and b/images/-4.jpg differ diff --git a/images/4.jpg b/images/4.jpg new file mode 100644 index 000000000..55e75001d Binary files /dev/null and b/images/4.jpg differ diff --git a/images/broxrevised.png b/images/broxrevised.png new file mode 100644 index 000000000..87051a383 Binary files /dev/null and b/images/broxrevised.png differ diff --git a/images/cake_graph.jpg b/images/cake_graph.jpg new file mode 100644 index 000000000..160a413ca Binary files /dev/null and b/images/cake_graph.jpg differ diff --git a/images/decisiontree_fruit.jpg b/images/decisiontree_fruit.jpg new file mode 100644 index 000000000..41ac4d606 Binary files /dev/null and b/images/decisiontree_fruit.jpg differ diff --git a/images/ensemble_learner.jpg b/images/ensemble_learner.jpg new file mode 100644 index 000000000..b1edd1ec5 Binary files /dev/null and b/images/ensemble_learner.jpg differ diff --git a/images/fig_5_2.png b/images/fig_5_2.png new file mode 100644 index 000000000..872485798 Binary files /dev/null and b/images/fig_5_2.png differ diff --git a/images/fig_5_2.svg b/images/fig_5_2.svg deleted file mode 100644 index 4f53217f1..000000000 --- a/images/fig_5_2.svg +++ /dev/null @@ -1,662 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MAX - MIN - 3 - - - a1 - a3 - a2 - b1 - b2 - b3 - c1 - c2 - c3 - d1 - d2 - d3 - 3 - 2 - 2 - 2 - 5 - 14 - 6 - 4 - 2 - 8 - 12 - 3 - - Figure 5.2 A two-ply game tree. The Δ nodes are "MAX nodes", in which it is MAX'sturn to move, and the ∇ nodes are "MIN nodes." The terminal nodes show the utility valuesfor MAX; the other nodes are labeled with their minimax values. MAX's best move at the rootis a1, because it leads to the state with the highest minimax value, and MIN's best reply is b1,beacuse it leads to the state with the lowest minimax value. - - - - - - - - - - - - - - - - - - A - B - C - D - - - - - - - - diff --git a/images/ge0.jpg b/images/ge0.jpg new file mode 100644 index 000000000..a70b18703 Binary files /dev/null and b/images/ge0.jpg differ diff --git a/images/ge1.jpg b/images/ge1.jpg new file mode 100644 index 000000000..624f16e25 Binary files /dev/null and b/images/ge1.jpg differ diff --git a/images/ge2.jpg b/images/ge2.jpg new file mode 100644 index 000000000..3a29f8f4c Binary files /dev/null and b/images/ge2.jpg differ diff --git a/images/ge4.jpg b/images/ge4.jpg new file mode 100644 index 000000000..b3a4b4acd Binary files /dev/null and b/images/ge4.jpg differ diff --git a/images/general_learning_agent.jpg b/images/general_learning_agent.jpg new file mode 100644 index 000000000..a8153bef8 Binary files /dev/null and b/images/general_learning_agent.jpg differ diff --git a/images/grid_mdp.jpg b/images/grid_mdp.jpg new file mode 100644 index 000000000..fa77fa276 Binary files /dev/null and b/images/grid_mdp.jpg differ diff --git a/images/grid_mdp_agent.jpg b/images/grid_mdp_agent.jpg new file mode 100644 index 000000000..3f247b6f2 Binary files /dev/null and b/images/grid_mdp_agent.jpg differ diff --git a/images/hillclimb-tsp.png b/images/hillclimb-tsp.png new file mode 100644 index 000000000..8446bbafc Binary files /dev/null and b/images/hillclimb-tsp.png differ diff --git a/images/knn_plot.png b/images/knn_plot.png index 6a5b0f036..58b316fdd 100644 Binary files a/images/knn_plot.png and b/images/knn_plot.png differ diff --git a/images/knowledge_FOIL_grandparent.png b/images/knowledge_FOIL_grandparent.png new file mode 100644 index 000000000..dbc6e7729 Binary files /dev/null and b/images/knowledge_FOIL_grandparent.png differ diff --git a/images/knowledge_foil_family.png b/images/knowledge_foil_family.png new file mode 100644 index 000000000..356f22d8d Binary files /dev/null and b/images/knowledge_foil_family.png differ diff --git a/images/maze.png b/images/maze.png new file mode 100644 index 000000000..f3fcd1990 Binary files /dev/null and b/images/maze.png differ diff --git a/images/mdp-b.png b/images/mdp-b.png new file mode 100644 index 000000000..f21a3760c Binary files /dev/null and b/images/mdp-b.png differ diff --git a/images/mdp-c.png b/images/mdp-c.png new file mode 100644 index 000000000..1034079a2 Binary files /dev/null and b/images/mdp-c.png differ diff --git a/images/mdp-d.png b/images/mdp-d.png new file mode 100644 index 000000000..8ba7cf073 Binary files /dev/null and b/images/mdp-d.png differ diff --git a/images/model_based_reflex_agent.jpg b/images/model_based_reflex_agent.jpg new file mode 100644 index 000000000..b6c12ed09 Binary files /dev/null and b/images/model_based_reflex_agent.jpg differ diff --git a/images/model_goal_based_agent.jpg b/images/model_goal_based_agent.jpg new file mode 100644 index 000000000..93d6182b4 Binary files /dev/null and b/images/model_goal_based_agent.jpg differ diff --git a/images/model_utility_based_agent.jpg b/images/model_utility_based_agent.jpg new file mode 100644 index 000000000..693230c00 Binary files /dev/null and b/images/model_utility_based_agent.jpg differ diff --git a/images/neural_net.png b/images/neural_net.png new file mode 100644 index 000000000..4aa28a106 Binary files /dev/null and b/images/neural_net.png differ diff --git a/images/parse_tree.png b/images/parse_tree.png new file mode 100644 index 000000000..f6ca87b2f Binary files /dev/null and b/images/parse_tree.png differ diff --git a/images/perceptron.png b/images/perceptron.png new file mode 100644 index 000000000..68d2a258a Binary files /dev/null and b/images/perceptron.png differ diff --git a/images/pluralityLearner_plot.png b/images/pluralityLearner_plot.png new file mode 100644 index 000000000..50aa5dcd1 Binary files /dev/null and b/images/pluralityLearner_plot.png differ diff --git a/images/point_crossover.png b/images/point_crossover.png new file mode 100644 index 000000000..9b8d4f7f5 Binary files /dev/null and b/images/point_crossover.png differ diff --git a/images/pop.jpg b/images/pop.jpg new file mode 100644 index 000000000..52b3e3756 Binary files /dev/null and b/images/pop.jpg differ diff --git a/images/queen_s.png b/images/queen_s.png new file mode 100644 index 000000000..cc693102a Binary files /dev/null and b/images/queen_s.png differ diff --git a/images/random_forest.png b/images/random_forest.png new file mode 100644 index 000000000..e0ab1d658 Binary files /dev/null and b/images/random_forest.png differ diff --git a/images/refinement.png b/images/refinement.png new file mode 100644 index 000000000..8270d81d0 Binary files /dev/null and b/images/refinement.png differ diff --git a/images/restaurant.png b/images/restaurant.png new file mode 100644 index 000000000..195c67645 Binary files /dev/null and b/images/restaurant.png differ diff --git a/images/romania_map.png b/images/romania_map.png new file mode 100644 index 000000000..426c76f1e Binary files /dev/null and b/images/romania_map.png differ diff --git a/images/simple_problem_solving_agent.jpg b/images/simple_problem_solving_agent.jpg new file mode 100644 index 000000000..80fb904b5 Binary files /dev/null and b/images/simple_problem_solving_agent.jpg differ diff --git a/images/simple_reflex_agent.jpg b/images/simple_reflex_agent.jpg new file mode 100644 index 000000000..74002a720 Binary files /dev/null and b/images/simple_reflex_agent.jpg differ diff --git a/images/stapler1-test.png b/images/stapler1-test.png new file mode 100644 index 000000000..e550d83f9 Binary files /dev/null and b/images/stapler1-test.png differ diff --git a/images/uniform_crossover.png b/images/uniform_crossover.png new file mode 100644 index 000000000..37f835e92 Binary files /dev/null and b/images/uniform_crossover.png differ diff --git a/improving_sat_algorithms.ipynb b/improving_sat_algorithms.ipynb new file mode 100644 index 000000000..d461e99c4 --- /dev/null +++ b/improving_sat_algorithms.ipynb @@ -0,0 +1,2539 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "# Propositional Logic\n", + "---\n", + "# Improving Boolean Satisfiability Algorithms\n", + "\n", + "## Introduction\n", + "A propositional formula $\\Phi$ in *Conjunctive Normal Form* (CNF) is a conjunction of clauses $\\omega_j$, with $j \\in \\{1,...,m\\}$. Each clause being a disjunction of literals and each literal being either a positive ($x_i$) or a negative ($\\lnot{x_i}$) propositional variable, with $i \\in \\{1,...,n\\}$. By denoting with $[\\lnot]$ the possible presence of $\\lnot$, we can formally define $\\Phi$ as:\n", + "\n", + "$$\\bigwedge_{j = 1,...,m}\\bigg(\\bigvee_{i \\in \\omega_j} [\\lnot] x_i\\bigg)$$\n", + "\n", + "The ***Boolean Satisfiability Problem*** (SAT) consists in determining whether there exists a truth assignment in $\\{0, 1\\}$ (or equivalently in $\\{True,False\\}$) for the variables in $\\Phi$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from logic import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DPLL with Branching Heuristics\n", + "The ***Davis-Putnam-Logemann-Loveland*** (DPLL) algorithm is a *complete* (will answer SAT if a solution exists) and *sound* (it will not answer SAT for an unsatisfiable formula) procedue that combines *backtracking search* and *deduction* to decide satisfiability of propositional logic formula in CNF. At each search step a variable and a propositional value are selected for branching purposes. With each branching step, two values can be assigned to a variable, either 0 or 1. Branching corresponds to assigning the chosen value to the chosen variable. Afterwards, the logical consequences of each branching step are evaluated. Each time an unsatisfied clause (ie a *conflict*) is identified, backtracking is executed. Backtracking corresponds to undoing branching steps until an unflipped branch is reached. When both values have been assigned to the selected variable at a branching step, backtracking will undo this branching step. If for the first branching step both values have been considered, and backtracking undoes this first branching step, then the CNF formula can be declared unsatisfiable. This kind of backtracking is called *chronological backtracking*.\n", + "\n", + "Essentially, `DPLL` is a backtracking depth-first search through partial truth assignments which uses a *splitting rule* to replaces the original problem with two smaller subproblems, whereas the original Davis-Putnam procedure uses a variable elimination rule which replaces the original problem with one larger subproblem. Over the years, many heuristics have been proposed in choosing the splitting variable (which variable should be assigned a truth value next).\n", + "\n", + "Search algorithms that are based on a predetermined order of search are called static algorithms, whereas the ones that select them at the runtime are called dynamic. The first SAT search algorithm, the Davis-Putnam procedure is a static algorithm. Static search algorithms are usually very slow in practice and for this reason perform worse than dynamic search algorithms. However, dynamic search algorithms are much harder to design, since they require a heuristic for predetermining the order of search. The fundamental element of a heuristic is a branching strategy for selecting the next branching literal. This must not require a lot of time to compute and yet it must provide a powerful insight into the problem instance.\n", + "\n", + "Two basic heuristics are applied to this algorithm with the potential of cutting the search space in half. These are the *pure literal rule* and the *unit clause rule*.\n", + "- the *pure literal* rule is applied whenever a variable appears with a single polarity in all the unsatisfied clauses. In this case, assigning a truth value to the variable so that all the involved clauses are satisfied is highly effective in the search;\n", + "- if some variable occurs in the current formula in a clause of length 1 then the *unit clause* rule is applied. Here, the literal is selected and a truth value so the respective clause is satisfied is assigned. The iterative application of the unit rule is commonly reffered to as *Boolean Constraint Propagation* (BCP)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mdpll_satisfiable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mno_branching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Check satisfiability of a propositional sentence.\u001b[0m\n", + "\u001b[0;34m This differs from the book code in two ways: (1) it returns a model\u001b[0m\n", + "\u001b[0;34m rather than True when it succeeds; this is more useful. (2) The\u001b[0m\n", + "\u001b[0;34m function find_pure_symbol is passed a list of unknown clauses, rather\u001b[0m\n", + "\u001b[0;34m than a list of all clauses and the model; this is more efficient.\u001b[0m\n", + "\u001b[0;34m >>> dpll_satisfiable(A |'<=>'| B) == {A: True, B: True}\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_cnf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource dpll_satisfiable" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mno_branching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"See if the clauses are true in a partial model.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munknown_clauses\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# clauses with an unknown truth value\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mval\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mval\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munknown_clauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0munknown_clauses\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfind_pure_symbol\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0munknown_clauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfind_unit_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0munknown_clauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mor\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdpll\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbranching_heuristic\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource dpll" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each of these branching heuristics was applied only after the *pure literal* and the *unit clause* heuristic failed in selecting a splitting variable." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### MOMs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MOMs heuristics are simple, efficient and easy to implement. The goal of these heuristics is to prefer the literal having ***Maximum number of Occurences in the Minimum length clauses***. Intuitively, the literals belonging to the minimum length clauses are the most constrained literals in the formula. Branching on them will maximize the effect of BCP and the likelihood of hitting a dead end early in the search tree (for unsatisfiable problems). Conversely, in the case of satisfiable formulas, branching on a highly constrained variable early in the tree will also increase the likelihood of a correct assignment of the remained open literals.\n", + "The MOMs heuristics main disadvatage is that their effectiveness highly depends on the problem instance. It is easy to see that the ideal setting for these heuristics is considering the unsatisfied binary clauses." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mmin_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmin_len\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mc\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdefault\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfilter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mc\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mmin_len\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmin_len\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource min_clauses" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mmoms\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m MOMS (Maximum Occurrence in clauses of Minimum Size) heuristic\u001b[0m\n", + "\u001b[0;34m Returns the literal with the most occurrences in all clauses of minimum size\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmin_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource moms" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Over the years, many types of MOMs heuristics have been proposed.\n", + "\n", + "***MOMSf*** choose the variable $x$ with a maximize the function:\n", + "\n", + "$$[f(x) + f(\\lnot{x})] * 2^k + f(x) * f(\\lnot{x})$$\n", + "\n", + "where $f(x)$ is the number of occurrences of $x$ in the smallest unknown clauses, k is a parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mmomsf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m MOMS alternative heuristic\u001b[0m\n", + "\u001b[0;34m If f(x) the number of occurrences of the variable x in clauses with minimum size,\u001b[0m\n", + "\u001b[0;34m we choose the variable maximizing [f(x) + f(-x)] * 2^k + f(x) * f(-x)\u001b[0m\n", + "\u001b[0;34m Returns x if f(x) >= f(-x) otherwise -x\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmin_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mpow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource momsf" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***Freeman’s POSIT***
    [[1]](#cite-freeman1995improvements) version counts both the number of positive $x$ and negative $\\lnot{x}$ occurrences of a given variable $x$." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mposit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Freeman's POSIT version of MOMs\u001b[0m\n", + "\u001b[0;34m Counts the positive x and negative x for each variable x in clauses with minimum size\u001b[0m\n", + "\u001b[0;34m Returns x if f(x) >= f(-x) otherwise -x\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmin_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource posit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***Zabih and McAllester’s*** [[2]](#cite-zabih1988rearrangement) version of the heuristic counts the negative occurrences $\\lnot{x}$ of each given variable $x$." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mzm\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Zabih and McAllester's version of MOMs\u001b[0m\n", + "\u001b[0;34m Counts the negative occurrences only of each variable x in clauses with minimum size\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmin_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ml\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mop\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'~'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource zm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### DLIS & DLCS" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Literal count heuristics count the number of unresolved clauses in which a given variable $x$ appears as a positive literal, $C_P$ , and as negative literal, $C_N$. These two numbers an either be onsidered individually or ombined. \n", + "\n", + "***Dynamic Largest Individual Sum*** heuristic considers the values $C_P$ and $C_N$ separately: select the variable with the largest individual value and assign to it value true if $C_P \\geq C_N$, value false otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mdlis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m DLIS (Dynamic Largest Individual Sum) heuristic\u001b[0m\n", + "\u001b[0;34m Choose the variable and value that satisfies the maximum number of unsatisfied clauses\u001b[0m\n", + "\u001b[0;34m Like DLCS but we only consider the literal (thus Cp and Cn are individual)\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource dlis" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***Dynamic Largest Combined Sum*** considers the values $C_P$ and $C_N$ combined: select the variable with the largest sum $C_P + C_N$ and assign to it value true if $C_P \\geq C_N$, value false otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mdlcs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m DLCS (Dynamic Largest Combined Sum) heuristic\u001b[0m\n", + "\u001b[0;34m Cp the number of clauses containing literal x\u001b[0m\n", + "\u001b[0;34m Cn the number of clauses containing literal -x\u001b[0m\n", + "\u001b[0;34m Here we select the variable maximizing Cp + Cn\u001b[0m\n", + "\u001b[0;34m Returns x if Cp >= Cn otherwise -x\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource dlcs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### JW & JW2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Two branching heuristics were proposed by ***Jeroslow and Wang*** in [[3]](#cite-jeroslow1990solving).\n", + "\n", + "The *one-sided Jeroslow and Wang*’s heuristic compute:\n", + "\n", + "$$J(l) = \\sum_{l \\in \\omega \\land \\omega \\in \\phi} 2^{-|\\omega|}$$\n", + "\n", + "and selects the assignment that satisfies the literal with the largest value $J(l)$." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mjw\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Jeroslow-Wang heuristic\u001b[0m\n", + "\u001b[0;34m For each literal compute J(l) = \\sum{l in clause c} 2^{-|c|}\u001b[0m\n", + "\u001b[0;34m Return the literal maximizing J\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mpow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource jw" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The *two-sided Jeroslow and Wang*’s heuristic identifies the variable $x$ with the largest sum $J(x) + J(\\lnot{x})$, and assigns to $x$ value true, if $J(x) \\geq J(\\lnot{x})$, and value false otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mjw2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m Two Sided Jeroslow-Wang heuristic\u001b[0m\n", + "\u001b[0;34m Compute J(l) also counts the negation of l = J(x) + J(-x)\u001b[0m\n", + "\u001b[0;34m Returns x if J(x) >= J(-x) otherwise -x\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mpow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource jw2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CDCL with 1UIP Learning Scheme, 2WL Lazy Data Structure, VSIDS Branching Heuristic & Restarts\n", + "\n", + "The ***Conflict-Driven Clause Learning*** (CDCL) solver is an evolution of the *DPLL* algorithm that involves a number of additional key techniques:\n", + "\n", + "- non-chronological backtracking or *backjumping*;\n", + "- *learning* new *clauses* from conflicts during search by exploiting its structure;\n", + "- using *lazy data structures* for storing clauses;\n", + "- *branching heuristics* with low computational overhead and which receive feedback from search;\n", + "- periodically *restarting* search.\n", + "\n", + "The first difference between a DPLL solver and a CDCL solver is the introduction of the *non-chronological backtracking* or *backjumping* when a conflict is identified. This requires an iterative implementation of the algorithm because only if the backtrack stack is managed explicitly it is possible to backtrack more than one level." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mcdcl_satisfiable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvsids_decay\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0.95\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrestart_strategy\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mno_restart\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"\u001b[0m\n", + "\u001b[0;34m >>> cdcl_satisfiable(A |'<=>'| B) == {A: True, B: True}\u001b[0m\n", + "\u001b[0;34m True\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mTwoWLClauseDatabase\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_cnf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbols\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDiGraph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdl\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflicts\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrestarts\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msum_lbd\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue_lbd\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0munit_propagation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mconflict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mdl\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflicts\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlearn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlbd\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mconflict_analysis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue_lbd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlbd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msum_lbd\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mlbd\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbackjump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlearn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlearn\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0msymbol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m*=\u001b[0m \u001b[0mvsids_decay\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrestart_strategy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflicts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrestarts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue_lbd\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msum_lbd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbackjump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mqueue_lbd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclear\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrestarts\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdl\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0massign_decision_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource cdcl_satisfiable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Clause Learning with 1UIP Scheme" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The second important difference between a DPLL solver and a CDCL solver is that the information about a conflict is reused by learning: if a conflicting clause is found, the solver derive a new clause from the conflict and add it to the clauses database.\n", + "\n", + "Whenever a conflict is identified due to unit propagation, a conflict analysis procedure is invoked. As a result, one or more new clauses are learnt, and a backtracking decision level is computed. The conflict analysis procedure analyzes the structure of unit propagation and decides which literals to include in the learnt clause. The decision levels associated with assigned variables define a partial order of the variables. Starting from a given unsatisfied clause (represented in the implication graph with vertex $\\kappa$), the conflict analysis procedure visits variables implied at the most recent decision level (ie the current largest decision level), identifies the antecedents of visited variables, and keeps from the antecedents the literals assigned at decision levels less than the most recent decision level. The clause learning procedure used in the CDCL can be defined by a sequence of selective resolution operations, that at each step yields a new temporary clause. This process is repeated until the most recent decision variable is visited.\n", + "\n", + "The structure of implied assignments induced by unit propagation is a key aspect of the clause learning procedure. Moreover, the idea of exploiting the structure induced by unit propagation was further exploited with ***Unit Implication Points*** (UIPs). A UIP is a *dominator* in the implication graph and represents an alternative decision assignment at the current decision level that results in the same conflict. The main motivation for identifying UIPs is to reduce the size of learnt clauses. Clause learning could potentially stop at any UIP, being quite straightforward to conclude that the set of literals of a clause learnt at the first UIP has clear advantages. Considering the largest decision level of the literals of the clause learnt at each UIP, the clause learnt at the first UIP is guaranteed to contain the smallest one. This guarantees the highest backtrack jump in the search tree." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mconflict_analysis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict_clause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'K'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'antecedent'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpred\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'K'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnode\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnode\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;34m'K'\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnode\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'dl'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mdl\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0min_degree\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnode\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mfirst_uip\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mimmediate_dominators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mP\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'K'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove_node\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'K'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict_side\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdescendants\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfirst_uip\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflict_clause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mintersection\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflict_side\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mantecedent\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'antecedent'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpred\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict_clause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpl_binary_resolution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflict_clause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mantecedent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# the literal block distance is calculated by taking the decision levels from variables of all\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# literals in the clause, and counting how many different decision levels were in this set\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlbd\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'dl'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflict_clause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlbd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mfirst_uip\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflict_clause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;36m0\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlbd\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mheapq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnlargest\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlbd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconflict_clause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlbd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource conflict_analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mpl_binary_resolution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mci\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mci\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdj\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mdi\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m~\u001b[0m\u001b[0mdj\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;34m~\u001b[0m\u001b[0mdi\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mdj\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpl_binary_resolution\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mci\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mremove_all\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0massociate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0munique\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mci\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource pl_binary_resolution" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mbackjump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdelete\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mnode\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnode\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnode\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'dl'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove_nodes_from\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdelete\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mnode\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdelete\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdel\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mnode\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbols\u001b[0m \u001b[0;34m|=\u001b[0m \u001b[0mdelete\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource backjump" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2WL Lazy Data Structure" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Implementation issues for SAT solvers include the design of suitable data structures for storing clauses. The implemented data structures dictate the way BCP are implemented and have a significant impact on the run time performance of the SAT solver. Recent state-of-the-art SAT solvers are characterized by using very efficient data structures, intended to reduce the CPU time required per each node in the search tree. Conversely, traditional SAT data structures are accurate, meaning that is possible to know exactly the value of each literal in the clause. Examples of the most recent SAT data structures, which are not accurate and therefore are called lazy, include the watched literals used in Chaff .\n", + "\n", + "The more recent Chaff SAT solver [[4]](#cite-moskewicz2001chaff) proposed a new data structure, the ***2 Watched Literals*** (2WL), in which two references are associated with each clause. There is no order relation between the two references, allowing the references to move in any direction. The lack of order between the two references has the key advantage that no literal references need to be updated when backtracking takes place. In contrast, unit or unsatisfied clauses are identified only after traversing all the clauses’ literals; a clear drawback. The two watched literal pointers are undifferentiated as there is no order relation. Again, each time one literal pointed by one of these pointers is assigned, the pointer has to move inwards. These pointers may move in both directions. This causes the whole clause to be traversed when the clause becomes unit. In addition, no references have to be kept to the just assigned literals, since pointers do not move when backtracking." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0munit_propagation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mcheck\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mw1\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_neg_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_pos_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mw2\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_neg_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_pos_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0munit_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mwatching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mwatching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_node\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_edges_from\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mzip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcycle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mantecedent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mp\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mconflict_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_edges_from\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mzip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprop_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mitertools\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcycle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'K'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mantecedent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mfilter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcheck\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# we need only visit each clause when one of its two watched literals is assigned to 0 because, until\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# this happens, we can guarantee that there cannot be more than n-2 literals in the clause assigned to 0\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mfirst_watched\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msecond_watched\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfirst_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munit_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mfirst_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mFalse\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0msecond_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if the only literal with a non-zero value is the other watched literal then\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msecond_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# if it is free, then the clause is a unit clause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munit_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# else (it is False) the clause is a conflict clause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0msecond_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mFalse\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mfirst_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if the only literal with a non-zero value is the other watched literal then\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfirst_watched\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# if it is free, then the clause is a unit clause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0munit_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclauses\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mbcp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# else (it is False) the clause is a conflict clause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mconflict_clause\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mbcp\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource unit_propagation" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mclass\u001b[0m \u001b[0mTwoWLClauseDatabase\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdefaultdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mclauses\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_clauses\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mset_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mset_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_pos_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ml\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_neg_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ml\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__assign_watching_literals\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp1\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mw1\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mw2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp2\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdel\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__twl\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiscard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp1\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiscard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mw1\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mw2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiscard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp2\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiscard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mupdate_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if a non-zero literal different from the other watched literal is found\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mfound\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__find_new_watching_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfound\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# then it will replace the watched literal\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mupdate_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if a non-zero literal different from the other watched literal is found\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mfound\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__find_new_watching_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_second_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfound\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# then it will replace the watched literal\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_first_watched\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mw\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minspect_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnew_watching\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mp\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__watch_list\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__find_new_watching_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother_watched\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if a non-zero literal different from the other watched literal is found\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ml\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mother_watched\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# then it is returned\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ml\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__assign_watching_literals\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mclause\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ml\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdisjuncts\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclause\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpl_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ml\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource TwoWLClauseDatabase" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### VSIDS Branching Heuristic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The early branching heuristics made use of all the information available from the data structures, namely the number of satisfied, unsatisfied and unassigned literals. These heuristics are updated during the search and also take into account the clauses that are learnt. \n", + "\n", + "More recently, a different kind of variable selection heuristic, referred to as ***Variable State Independent Decaying Sum*** (VSIDS), has been proposed by Chaff authors in [[4]](#cite-moskewicz2001chaff). One of the reasons for proposing this new heuristic was the introduction of lazy data structures, where the knowledge of the dynamic size of a clause is not accurate. Hence, the heuristics described above cannot be used. VSIDS selects the literal that appears most frequently over all the clauses, which means that one counter is required for each one of the literals. Initially, all counters are set to zero. During the search, the metrics only have to be updated when a new recorded clause is created. More than to develop an accurate heuristic, the motivation has been to design a fast (but dynamically adapting) heuristic. In fact, one of the key properties of this strategy is the very low overhead, due to being independent of the variable state." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0massign_decision_literal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mP\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msymbols\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0msymbol\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvalue\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m~\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbols\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mG\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_node\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mP\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mval\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdl\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdl\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource assign_decision_literal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Restarts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Solving NP-complete problems, such as SAT, naturally leads to heavy-tailed run times. To deal with this, SAT solvers frequently restart their search to avoid the runs that take disproportionately longer. What restarting here means is that the solver unsets all variables and starts the search using different variable assignment order.\n", + "\n", + "While at first glance it might seem that restarts should be rare and become rarer as the solving has been going on for longer, so that the SAT solver can actually finish solving the problem, the trend has been towards more aggressive (frequent) restarts.\n", + "\n", + "The reason why frequent restarts help solve problems faster is that while the solver does forget all current variable assignments, it does keep some information, specifically it keeps learnt clauses, effectively sampling the search space, and it keeps the last assigned truth value of each variable, assigning them the same value the next time they are picked to be assigned." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Luby" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this strategy, the number of conflicts between 2 restarts is based on the *Luby* sequence. The *Luby* restart sequence is interesting in that it was proven to be optimal restart strategy for randomized search algorithms where the runs do not share information. While this is not true for SAT solving, as shown in [[5]](cite-haim2014towards) and [[6]](cite-huang2007effect), *Luby* restarts have been quite successful anyway.\n", + "\n", + "The exact description of *Luby* restarts is that the $ith$ restart happens after $u \\cdot Luby(i)$ conflicts, where $u$ is a constant and $Luby(i)$ is defined as:\n", + "\n", + "$$Luby(i) = \\begin{cases} \n", + " 2^{k-1} & i = 2^k - 1 \\\\\n", + " Luby(i - 2^{k-1} + 1) & 2^{k-1} \\leq i < 2^k - 1\n", + " \\end{cases}\n", + "$$\n", + "\n", + "A less exact but more intuitive description of the *Luby* sequence is that all numbers in it are powers of two, and after a number is seen for the second time, the next number is twice as big. The following are the first 16 numbers in the sequence:\n", + "\n", + "$$ (1,1,2,1,1,2,4,1,1,2,1,1,2,4,8,1,...) $$\n", + "\n", + "From the above, we can see that this restart strategy tends towards frequent restarts, but some runs are kept running for much longer, and there is no upper limit on the longest possible time between two restarts." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mluby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflicts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrestarts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue_lbd\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msum_lbd\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0munit\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m512\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# in the state-of-art tested with unit value 1, 2, 4, 6, 8, 12, 16, 32, 64, 128, 256 and 512\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_luby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mk\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mi\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m \u001b[0;34m<<\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m<<\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mk\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m \u001b[0;34m<<\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mk\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0mi\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m \u001b[0;34m<<\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0m_luby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m \u001b[0;34m<<\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mk\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mk\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0munit\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0m_luby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrestarts\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue_lbd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource luby" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Glucose" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Glucose restarts were popularized by the *Glucose* solver, and it is an extremely aggressive, dynamic restart strategy. The idea behind it and described in [[7]](cite-audemard2012refining) is that instead of waiting for a fixed amount of conflicts, we restart when the last couple of learnt clauses are, on average, bad.\n", + "\n", + "A bit more precisely, if there were at least $X$ conflicts (and thus $X$ learnt clauses) since the last restart, and the average *Literal Block Distance* (LBD) (a criterion to evaluate the quality of learnt clauses as shown in [[8]](#cite-audemard2009predicting) of the last $X$ learnt clauses was at least $K$ times higher than the average LBD of all learnt clauses, it is time for another restart. Parameters $X$ and $K$ can be tweaked to achieve different restart frequency, and they are usually kept quite small." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;32mdef\u001b[0m \u001b[0mglucose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mconflicts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrestarts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mqueue_lbd\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msum_lbd\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m100\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0.7\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# in the state-of-art tested with (x, k) as (50, 0.8) and (100, 0.7)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# if there were at least x conflicts since the last restart, and then the average LBD of the last\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;31m# x learnt clauses was at least k times higher than the average LBD of all learnt clauses\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue_lbd\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue_lbd\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mqueue_lbd\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mk\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0msum_lbd\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mconflicts\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%psource glucose" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": {} + }, + "source": [ + "## Experimental Results" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "from csp import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Australia" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSP" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "australia_csp = MapColoringCSP(list('RGB'), \"\"\"SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: \"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 154 µs, sys: 37 µs, total: 191 µs\n", + "Wall time: 194 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP needs 72 consistency-checks'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3b(australia_csp, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 263 µs, sys: 0 ns, total: 263 µs\n", + "Wall time: 268 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Q': 'R', 'SA': 'G', 'NSW': 'B', 'NT': 'B', 'V': 'R', 'WA': 'R'}" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time backtracking_search(australia_csp, select_unassigned_variable=mrv, inference=forward_checking)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SAT" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "australia_sat = MapColoringSAT(list('RGB'), \"\"\"SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: \"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 43.3 ms, sys: 0 ns, total: 43.3 ms\n", + "Wall time: 41.5 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=no_branching_heuristic)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 36.4 ms, sys: 0 ns, total: 36.4 ms\n", + "Wall time: 35.3 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=moms)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 36.1 ms, sys: 3.9 ms, total: 40 ms\n", + "Wall time: 39.2 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=momsf)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 45.2 ms, sys: 0 ns, total: 45.2 ms\n", + "Wall time: 44.2 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=posit)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 31.2 ms, sys: 0 ns, total: 31.2 ms\n", + "Wall time: 30.5 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=zm)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 57 ms, sys: 0 ns, total: 57 ms\n", + "Wall time: 55.9 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=dlis)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 51.8 ms, sys: 0 ns, total: 51.8 ms\n", + "Wall time: 50.7 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=dlcs)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 40.6 ms, sys: 0 ns, total: 40.6 ms\n", + "Wall time: 39.3 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=jw)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 43.2 ms, sys: 1.81 ms, total: 45.1 ms\n", + "Wall time: 43.9 ms\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(australia_sat, branching_heuristic=jw2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 32.9 ms, sys: 16 µs, total: 33 ms\n", + "Wall time: 31.6 ms\n" + ] + } + ], + "source": [ + "%time model = cdcl_satisfiable(australia_sat)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{NSW_B, NT_B, Q_G, SA_R, V_G, WA_G}" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{var for var, val in model.items() if val}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### France" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSP" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "france_csp = MapColoringCSP(list('RGBY'),\n", + " \"\"\"AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA\n", + " AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO\n", + " CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:\n", + " MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:\n", + " PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:\n", + " AU BO FC PA LR\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 599 µs, sys: 112 µs, total: 711 µs\n", + "Wall time: 716 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP needs 516 consistency-checks'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3b(france_csp, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 560 µs, sys: 0 ns, total: 560 µs\n", + "Wall time: 563 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "{'NH': 'R',\n", + " 'NB': 'G',\n", + " 'CE': 'B',\n", + " 'PL': 'R',\n", + " 'BR': 'B',\n", + " 'IF': 'G',\n", + " 'PI': 'B',\n", + " 'BO': 'R',\n", + " 'CA': 'Y',\n", + " 'FC': 'G',\n", + " 'LO': 'R',\n", + " 'PC': 'G',\n", + " 'AU': 'G',\n", + " 'AL': 'B',\n", + " 'RA': 'B',\n", + " 'LR': 'R',\n", + " 'LI': 'R',\n", + " 'AQ': 'B',\n", + " 'MP': 'Y',\n", + " 'PA': 'G',\n", + " 'NO': 'R'}" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time backtracking_search(france_csp, select_unassigned_variable=mrv, inference=forward_checking)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SAT" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "france_sat = MapColoringSAT(list('RGBY'),\n", + " \"\"\"AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA\n", + " AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO\n", + " CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:\n", + " MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:\n", + " PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:\n", + " AU BO FC PA LR\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.32 s, sys: 0 ns, total: 3.32 s\n", + "Wall time: 3.32 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=no_branching_heuristic)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.17 s, sys: 390 µs, total: 3.17 s\n", + "Wall time: 3.17 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=moms)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.49 s, sys: 0 ns, total: 3.49 s\n", + "Wall time: 3.49 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=momsf)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.5 s, sys: 0 ns, total: 3.5 s\n", + "Wall time: 3.5 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=posit)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3 s, sys: 2.6 ms, total: 3.01 s\n", + "Wall time: 3.01 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=zm)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 12.5 s, sys: 11.4 ms, total: 12.5 s\n", + "Wall time: 12.5 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=dlis)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.41 s, sys: 0 ns, total: 3.41 s\n", + "Wall time: 3.41 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=dlcs)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.92 s, sys: 3.89 ms, total: 2.92 s\n", + "Wall time: 2.92 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=jw)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.71 s, sys: 0 ns, total: 3.71 s\n", + "Wall time: 3.73 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(france_sat, branching_heuristic=jw2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 159 ms, sys: 3.94 ms, total: 163 ms\n", + "Wall time: 162 ms\n" + ] + } + ], + "source": [ + "%time model = cdcl_satisfiable(france_sat)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{AL_G,\n", + " AQ_G,\n", + " AU_R,\n", + " BO_G,\n", + " BR_Y,\n", + " CA_R,\n", + " CE_B,\n", + " FC_B,\n", + " IF_Y,\n", + " LI_Y,\n", + " LO_Y,\n", + " LR_G,\n", + " MP_B,\n", + " NB_R,\n", + " NH_G,\n", + " NO_Y,\n", + " PA_B,\n", + " PC_R,\n", + " PI_B,\n", + " PL_G,\n", + " RA_Y}" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{var for var, val in model.items() if val}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### USA" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSP" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "usa_csp = MapColoringCSP(list('RGBY'),\n", + " \"\"\"WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;\n", + " UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;\n", + " ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;\n", + " TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;\n", + " LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;\n", + " MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;\n", + " PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;\n", + " NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;\n", + " HI: ; AK: \"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.58 ms, sys: 17 µs, total: 1.6 ms\n", + "Wall time: 1.6 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP needs 1284 consistency-checks'" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3b(usa_csp, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.15 ms, sys: 0 ns, total: 2.15 ms\n", + "Wall time: 2.15 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "{'NM': 'R',\n", + " 'TX': 'G',\n", + " 'OK': 'B',\n", + " 'AR': 'R',\n", + " 'MO': 'G',\n", + " 'KA': 'R',\n", + " 'LA': 'B',\n", + " 'NE': 'B',\n", + " 'TN': 'B',\n", + " 'MS': 'G',\n", + " 'IA': 'R',\n", + " 'SD': 'G',\n", + " 'IL': 'B',\n", + " 'CO': 'G',\n", + " 'MN': 'B',\n", + " 'KY': 'R',\n", + " 'AL': 'R',\n", + " 'GA': 'G',\n", + " 'FL': 'B',\n", + " 'VA': 'G',\n", + " 'WI': 'G',\n", + " 'IN': 'G',\n", + " 'NC': 'R',\n", + " 'WV': 'B',\n", + " 'OH': 'Y',\n", + " 'PA': 'R',\n", + " 'MD': 'Y',\n", + " 'SC': 'B',\n", + " 'MI': 'R',\n", + " 'DC': 'R',\n", + " 'DE': 'G',\n", + " 'WY': 'R',\n", + " 'ND': 'R',\n", + " 'NJ': 'B',\n", + " 'NY': 'G',\n", + " 'UT': 'B',\n", + " 'AZ': 'G',\n", + " 'ID': 'G',\n", + " 'MT': 'B',\n", + " 'NV': 'R',\n", + " 'CA': 'B',\n", + " 'OR': 'Y',\n", + " 'WA': 'R',\n", + " 'VT': 'R',\n", + " 'MA': 'B',\n", + " 'NH': 'G',\n", + " 'CT': 'R',\n", + " 'RI': 'G',\n", + " 'ME': 'R'}" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time backtracking_search(usa_csp, select_unassigned_variable=mrv, inference=forward_checking)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SAT" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "usa_sat = MapColoringSAT(list('RGBY'),\n", + " \"\"\"WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;\n", + " UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;\n", + " ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;\n", + " TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;\n", + " LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;\n", + " MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;\n", + " PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;\n", + " NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;\n", + " HI: ; AK: \"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 46.2 s, sys: 0 ns, total: 46.2 s\n", + "Wall time: 46.2 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=no_branching_heuristic)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 54.6 s, sys: 0 ns, total: 54.6 s\n", + "Wall time: 54.6 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=moms)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 44 s, sys: 0 ns, total: 44 s\n", + "Wall time: 44 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=momsf)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 43.8 s, sys: 0 ns, total: 43.8 s\n", + "Wall time: 43.8 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=posit)" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 52.6 s, sys: 0 ns, total: 52.6 s\n", + "Wall time: 52.6 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=zm)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 57 s, sys: 0 ns, total: 57 s\n", + "Wall time: 57 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=dlis)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 43.8 s, sys: 0 ns, total: 43.8 s\n", + "Wall time: 43.8 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=dlcs)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 53.3 s, sys: 3.82 ms, total: 53.3 s\n", + "Wall time: 53.3 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=jw)" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 44 s, sys: 3.99 ms, total: 44 s\n", + "Wall time: 44 s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(usa_sat, branching_heuristic=jw2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 559 ms, sys: 0 ns, total: 559 ms\n", + "Wall time: 558 ms\n" + ] + } + ], + "source": [ + "%time model = cdcl_satisfiable(usa_sat)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{AL_B,\n", + " AR_B,\n", + " AZ_R,\n", + " CA_B,\n", + " CO_R,\n", + " CT_Y,\n", + " DC_G,\n", + " DE_Y,\n", + " FL_Y,\n", + " GA_R,\n", + " IA_B,\n", + " ID_Y,\n", + " IL_G,\n", + " IN_R,\n", + " KA_G,\n", + " KY_B,\n", + " LA_G,\n", + " MA_G,\n", + " MD_R,\n", + " ME_G,\n", + " MI_G,\n", + " MN_Y,\n", + " MO_R,\n", + " MS_Y,\n", + " MT_B,\n", + " NC_B,\n", + " ND_G,\n", + " NE_Y,\n", + " NH_Y,\n", + " NJ_G,\n", + " NM_G,\n", + " NV_G,\n", + " NY_R,\n", + " OH_Y,\n", + " OK_Y,\n", + " OR_R,\n", + " PA_B,\n", + " RI_B,\n", + " SC_Y,\n", + " SD_R,\n", + " TN_G,\n", + " TX_R,\n", + " UT_B,\n", + " VA_Y,\n", + " VT_B,\n", + " WA_B,\n", + " WI_R,\n", + " WV_G,\n", + " WY_G}" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{var for var, val in model.items() if val}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Zebra Puzzle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### CSP" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [], + "source": [ + "zebra_csp = Zebra()" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Milk': 3, 'Norwegian': 1}\n" + ] + } + ], + "source": [ + "zebra_csp.display(zebra_csp.infer_assignment())" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.04 ms, sys: 4 µs, total: 2.05 ms\n", + "Wall time: 2.05 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "'AC3b with DOM J UP needs 737 consistency-checks'" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time _, checks = AC3b(zebra_csp, arc_heuristic=dom_j_up)\n", + "f'AC3b with DOM J UP needs {checks} consistency-checks'" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Blue': 2, 'Milk': 3, 'Norwegian': 1}\n" + ] + } + ], + "source": [ + "zebra_csp.display(zebra_csp.infer_assignment())" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.13 ms, sys: 0 ns, total: 2.13 ms\n", + "Wall time: 2.14 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Milk': 3,\n", + " 'Blue': 2,\n", + " 'Norwegian': 1,\n", + " 'Coffee': 5,\n", + " 'Green': 5,\n", + " 'Ivory': 4,\n", + " 'Red': 3,\n", + " 'Yellow': 1,\n", + " 'Kools': 1,\n", + " 'Englishman': 3,\n", + " 'Horse': 2,\n", + " 'Tea': 2,\n", + " 'Ukranian': 2,\n", + " 'Spaniard': 4,\n", + " 'Dog': 4,\n", + " 'Japanese': 5,\n", + " 'Parliaments': 5,\n", + " 'LuckyStrike': 4,\n", + " 'OJ': 4,\n", + " 'Water': 1,\n", + " 'Chesterfields': 2,\n", + " 'Winston': 3,\n", + " 'Snails': 3,\n", + " 'Fox': 1,\n", + " 'Zebra': 5}" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time backtracking_search(zebra_csp, select_unassigned_variable=mrv, inference=forward_checking)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### SAT" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "zebra_sat = associate('&', map(to_cnf, map(expr, filter(lambda line: line[0] not in ('c', 'p'), open('aima-data/zebra.cnf').read().splitlines()))))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### DPLL" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 13min 6s, sys: 2.44 ms, total: 13min 6s\n", + "Wall time: 13min 6s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=no_branching_heuristic)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 15min 4s, sys: 22.4 ms, total: 15min 4s\n", + "Wall time: 15min 4s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=moms)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 22min 28s, sys: 40 ms, total: 22min 28s\n", + "Wall time: 22min 28s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=momsf)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 22min 25s, sys: 36 ms, total: 22min 25s\n", + "Wall time: 22min 25s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=posit)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 14min 52s, sys: 32 ms, total: 14min 52s\n", + "Wall time: 14min 52s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=zm)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2min 31s, sys: 9.87 ms, total: 2min 31s\n", + "Wall time: 2min 32s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=dlis)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4min 27s, sys: 12 ms, total: 4min 27s\n", + "Wall time: 4min 27s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=dlcs)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 6min 55s, sys: 39.2 ms, total: 6min 55s\n", + "Wall time: 6min 56s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=jw)" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 8min 57s, sys: 7.94 ms, total: 8min 57s\n", + "Wall time: 8min 57s\n" + ] + } + ], + "source": [ + "%time model = dpll_satisfiable(zebra_sat, branching_heuristic=jw2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### CDCL" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "pycharm": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.64 s, sys: 0 ns, total: 1.64 s\n", + "Wall time: 1.64 s\n" + ] + } + ], + "source": [ + "%time model = cdcl_satisfiable(zebra_sat)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{Englishman_house2,\n", + " Englishman_milk,\n", + " Englishman_oldGold,\n", + " Englishman_redHouse,\n", + " Englishman_snails,\n", + " Japanese_coffee,\n", + " Japanese_greenHouse,\n", + " Japanese_house4,\n", + " Japanese_parliament,\n", + " Japanese_zebra,\n", + " Norwegian_fox,\n", + " Norwegian_house0,\n", + " Norwegian_kool,\n", + " Norwegian_water,\n", + " Norwegian_yellowHouse,\n", + " Spaniard_dog,\n", + " Spaniard_house3,\n", + " Spaniard_ivoryHouse,\n", + " Spaniard_luckyStrike,\n", + " Spaniard_orangeJuice,\n", + " Ukrainian_blueHouse,\n", + " Ukrainian_chesterfield,\n", + " Ukrainian_horse,\n", + " Ukrainian_house1,\n", + " Ukrainian_tea}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{var for var, val in model.items() if val and var.op.startswith(('Englishman', 'Japanese', 'Norwegian', 'Spaniard', 'Ukrainian'))}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[[1]](#ref-1) Freeman, Jon William. 1995. _Improvements to propositional satisfiability search algorithms_.\n", + "\n", + "[[2]](#ref-2) Zabih, Ramin and McAllester, David A. 1988. _A Rearrangement Search Strategy for Determining Propositional Satisfiability_.\n", + "\n", + "[[3]](#ref-3) Jeroslow, Robert G and Wang, Jinchang. 1990. _Solving propositional satisfiability problems_.\n", + "\n", + "[[4]](#ref-4) Moskewicz, Matthew W and Madigan, Conor F and Zhao, Ying and Zhang, Lintao and Malik, Sharad. 2001. _Chaff: Engineering an efficient SAT solver_.\n", + "\n", + "[[5]](#ref-5) Haim, Shai and Heule, Marijn. 2014. _Towards ultra rapid restarts_.\n", + "\n", + "[[6]](#ref-6) Huang, Jinbo and others. 2007. _The Effect of Restarts on the Efficiency of Clause Learning_.\n", + "\n", + "[[7]](#ref-7) Audemard, Gilles and Simon, Laurent. 2012. _Refining restarts strategies for SAT and UNSAT_.\n", + "\n", + "[[8]](#ref-8) Audemard, Gilles and Simon, Laurent. 2009. _Predicting learnt clauses quality in modern SAT solvers_." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/index.ipynb b/index.ipynb index 2ae5742bb..f9da121f2 100644 --- a/index.ipynb +++ b/index.ipynb @@ -18,7 +18,7 @@ "\n", "3. [**Search**](./search.ipynb)\n", "\n", - "4. [**Search - 4th edition**](./search-4e.ipynb)\n", + "4. [**Search - 4th edition**](./search4e.ipynb)\n", "\n", "4. [**Games**](./games.ipynb)\n", "\n", diff --git a/intro.ipynb b/intro.ipynb index a4850ebc2..896ed9498 100644 --- a/intro.ipynb +++ b/intro.ipynb @@ -6,19 +6,20 @@ "source": [ "# An Introduction To `aima-python` \n", " \n", - "The [aima-python](https://github.com/aimacode/aima-python) repository implements, in Python code, the algorithms in the textbook *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. A typical module in the repository has the code for a single chapter in the book, but some modules combine several chapters. See [the index](https://github.com/aimacode/aima-python#index-of-code) if you can't find the algorithm you want. The code in this repository attempts to mirror the pseudocode in the textbook as closely as possible and to stress readability foremost; if you are looking for high-performance code with advanced features, there are other repositories for you. For each module, there are three files, for example:\n", + "The [aima-python](https://github.com/aimacode/aima-python) repository implements, in Python code, the algorithms in the textbook *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. A typical module in the repository has the code for a single chapter in the book, but some modules combine several chapters. See [the index](https://github.com/aimacode/aima-python#index-of-code) if you can't find the algorithm you want. The code in this repository attempts to mirror the pseudocode in the textbook as closely as possible and to stress readability foremost; if you are looking for high-performance code with advanced features, there are other repositories for you. For each module, there are three/four files, for example:\n", "\n", - "- [**`logic.py`**](https://github.com/aimacode/aima-python/blob/master/logic.py): Source code with data types and algorithms for fealing with logic; functions have docstrings explaining their use.\n", - "- [**`logic.ipynb`**](https://github.com/aimacode/aima-python/blob/master/logic.ipynb): A notebook like this one; gives more detailed examples and explanations of use.\n", - "- [**`tests/test_logic.py`**](https://github.com/aimacode/aima-python/blob/master/tests/test_logic.py): Test cases, used to verify the code is correct, and also useful to see examples of use.\n", + "- [**`nlp.py`**](https://github.com/aimacode/aima-python/blob/master/nlp.py): Source code with data types and algorithms for natural language processing; functions have docstrings explaining their use.\n", + "- [**`nlp.ipynb`**](https://github.com/aimacode/aima-python/blob/master/nlp.ipynb): A notebook like this one; gives more detailed examples and explanations of use.\n", + "- [**`nlp_apps.ipynb`**](https://github.com/aimacode/aima-python/blob/master/nlp_apps.ipynb): A Jupyter notebook that gives example applications of the code.\n", + "- [**`tests/test_nlp.py`**](https://github.com/aimacode/aima-python/blob/master/tests/test_nlp.py): Test cases, used to verify the code is correct, and also useful to see examples of use.\n", "\n", "There is also an [aima-java](https://github.com/aimacode/aima-java) repository, if you prefer Java.\n", " \n", "## What version of Python?\n", " \n", - "The code is tested in Python [3.4](https://www.python.org/download/releases/3.4.3/) and [3.5](https://www.python.org/downloads/release/python-351/). If you try a different version of Python 3 and find a problem, please report it as an [Issue](https://github.com/aimacode/aima-python/issues). There is an incomplete [legacy branch](https://github.com/aimacode/aima-python/tree/aima3python2) for those who must run in Python 2. \n", + "The code is tested in Python [3.4](https://www.python.org/download/releases/3.4.3/) and [3.5](https://www.python.org/downloads/release/python-351/). If you try a different version of Python 3 and find a problem, please report it as an [Issue](https://github.com/aimacode/aima-python/issues).\n", " \n", - "We recommend the [Anaconda](https://www.continuum.io/downloads) distribution of Python 3.5. It comes with additional tools like the powerful IPython interpreter, the Jupyter Notebook and many helpful packages for scientific computing. After installing Anaconda, you will be good to go to run all the code and all the IPython notebooks. \n", + "We recommend the [Anaconda](https://www.anaconda.com/download/) distribution of Python 3.5. It comes with additional tools like the powerful IPython interpreter, the Jupyter Notebook and many helpful packages for scientific computing. After installing Anaconda, you will be good to go to run all the code and all the IPython notebooks. \n", "\n", "## IPython notebooks \n", " \n", @@ -27,7 +28,7 @@ "\n", "1. View static HTML pages. (Just browse to the [repository](https://github.com/aimacode/aima-python) and click on a `.ipynb` file link.)\n", "2. Run, modify, and re-run code, live. (Download the repository (by [zip file](https://github.com/aimacode/aima-python/archive/master.zip) or by `git` commands), start a Jupyter notebook server with the shell command \"`jupyter notebook`\" (issued from the directory where the files are), and click on the notebook you want to interact with.)\n", - "3. Binder - Click on the binder badge on the [repository](https://github.com/aimacode/aima-python) main page to opens the notebooks in an executable environment, online. This method does not require any extra installation. The code can be executed and modified from the browser itself.\n", + "3. Binder - Click on the binder badge on the [repository](https://github.com/aimacode/aima-python) main page to open the notebooks in an executable environment, online. This method does not require any extra installation. The code can be executed and modified from the browser itself. Note that this is an unstable option; there is a chance the notebooks will never load.\n", "\n", " \n", "You can [read about notebooks](https://jupyter-notebook-beginner-guide.readthedocs.org/en/latest/) and then [get started](https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Running%20Code.ipynb)." @@ -46,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": { "collapsed": true }, @@ -61,12 +62,12 @@ "source": [ "From there, the notebook alternates explanations with examples of use. You can run the examples as they are, and you can modify the code cells (or add new cells) and run your own examples. If you have some really good examples to add, you can make a github pull request.\n", "\n", - "If you want to see the source code of a function, you can open a browser or editor and see it in another window, or from within the notebook you can use the IPython magic funtion `%psource` (for \"print source\"):" + "If you want to see the source code of a function, you can open a browser or editor and see it in another window, or from within the notebook you can use the IPython magic function `%psource` (for \"print source\") or the function `psource` from `notebook.py`. Also, if the algorithm has pseudocode available, you can read it by calling the `pseudocode` function with the name of the algorithm passed as a parameter." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": { "collapsed": true }, @@ -75,16 +76,28 @@ "%psource WalkSAT" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from notebook import psource, pseudocode\n", + "\n", + "psource(WalkSAT)\n", + "pseudocode(\"WalkSAT\")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Or see an abbreviated description of an object with a trainling question mark:" + "Or see an abbreviated description of an object with a trailing question mark:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": { "collapsed": true }, @@ -99,7 +112,7 @@ "source": [ "# Authors\n", "\n", - "This notebook by [Chirag Vertak](https://github.com/chiragvartak) and [Peter Norvig](https://github.com/norvig)." + "This notebook is written by [Chirag Vertak](https://github.com/chiragvartak) and [Peter Norvig](https://github.com/norvig)." ] } ], @@ -119,9 +132,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.4.3" + "version": "3.5.3" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/ipyviews.py b/ipyviews.py index 7cb28850b..b304af7bb 100644 --- a/ipyviews.py +++ b/ipyviews.py @@ -6,7 +6,6 @@ import copy import __main__ - # ______________________________________________________________________________ # Continuous environment @@ -20,14 +19,14 @@ var all_polygons = {3}; {4} -''' +''' # noqa with open('js/continuousworld.js', 'r') as js_file: _JS_CONTINUOUS_WORLD = js_file.read() class ContinuousWorldView: - ''' View for continuousworld Implementation in agents.py ''' + """ View for continuousworld Implementation in agents.py """ def __init__(self, world, fill="#AAA"): self.time = time.time() @@ -61,7 +60,9 @@ def get_polygon_obstacles_coordinates(self): def show(self): clear_output() - total_html = _CONTINUOUS_WORLD_HTML.format(self.width, self.height, self.object_name(), str(self.get_polygon_obstacles_coordinates()), _JS_CONTINUOUS_WORLD) + total_html = _CONTINUOUS_WORLD_HTML.format(self.width, self.height, self.object_name(), + str(self.get_polygon_obstacles_coordinates()), + _JS_CONTINUOUS_WORLD) display(HTML(total_html)) diff --git a/knowledge.py b/knowledge.py new file mode 100644 index 000000000..8c27c3eb8 --- /dev/null +++ b/knowledge.py @@ -0,0 +1,422 @@ +"""Knowledge in learning (Chapter 19)""" + +from collections import defaultdict +from functools import partial +from itertools import combinations, product +from random import shuffle + +import numpy as np + +from logic import (FolKB, constant_symbols, predicate_symbols, standardize_variables, + variables, is_definite_clause, subst, expr, Expr) +from utils import power_set + + +def current_best_learning(examples, h, examples_so_far=None): + """ + [Figure 19.2] + The hypothesis is a list of dictionaries, with each dictionary representing + a disjunction. + """ + if examples_so_far is None: + examples_so_far = [] + if not examples: + return h + + e = examples[0] + if is_consistent(e, h): + return current_best_learning(examples[1:], h, examples_so_far + [e]) + elif false_positive(e, h): + for h2 in specializations(examples_so_far + [e], h): + h3 = current_best_learning(examples[1:], h2, examples_so_far + [e]) + if h3 != 'FAIL': + return h3 + elif false_negative(e, h): + for h2 in generalizations(examples_so_far + [e], h): + h3 = current_best_learning(examples[1:], h2, examples_so_far + [e]) + if h3 != 'FAIL': + return h3 + + return 'FAIL' + + +def specializations(examples_so_far, h): + """Specialize the hypothesis by adding AND operations to the disjunctions""" + hypotheses = [] + + for i, disj in enumerate(h): + for e in examples_so_far: + for k, v in e.items(): + if k in disj or k == 'GOAL': + continue + + h2 = h[i].copy() + h2[k] = '!' + v + h3 = h.copy() + h3[i] = h2 + if check_all_consistency(examples_so_far, h3): + hypotheses.append(h3) + + shuffle(hypotheses) + return hypotheses + + +def generalizations(examples_so_far, h): + """Generalize the hypothesis. First delete operations + (including disjunctions) from the hypothesis. Then, add OR operations.""" + hypotheses = [] + + # Delete disjunctions + disj_powerset = power_set(range(len(h))) + for disjs in disj_powerset: + h2 = h.copy() + for d in reversed(list(disjs)): + del h2[d] + + if check_all_consistency(examples_so_far, h2): + hypotheses += h2 + + # Delete AND operations in disjunctions + for i, disj in enumerate(h): + a_powerset = power_set(disj.keys()) + for attrs in a_powerset: + h2 = h[i].copy() + for a in attrs: + del h2[a] + + if check_all_consistency(examples_so_far, [h2]): + h3 = h.copy() + h3[i] = h2.copy() + hypotheses += h3 + + # Add OR operations + if hypotheses == [] or hypotheses == [{}]: + hypotheses = add_or(examples_so_far, h) + else: + hypotheses.extend(add_or(examples_so_far, h)) + + shuffle(hypotheses) + return hypotheses + + +def add_or(examples_so_far, h): + """Add an OR operation to the hypothesis. The AND operations in the disjunction + are generated by the last example (which is the problematic one).""" + ors = [] + e = examples_so_far[-1] + + attrs = {k: v for k, v in e.items() if k != 'GOAL'} + a_powerset = power_set(attrs.keys()) + + for c in a_powerset: + h2 = {} + for k in c: + h2[k] = attrs[k] + + if check_negative_consistency(examples_so_far, h2): + h3 = h.copy() + h3.append(h2) + ors.append(h3) + + return ors + + +# ______________________________________________________________________________ + + +def version_space_learning(examples): + """ + [Figure 19.3] + The version space is a list of hypotheses, which in turn are a list + of dictionaries/disjunctions. + """ + V = all_hypotheses(examples) + for e in examples: + if V: + V = version_space_update(V, e) + + return V + + +def version_space_update(V, e): + return [h for h in V if is_consistent(e, h)] + + +def all_hypotheses(examples): + """Build a list of all the possible hypotheses""" + values = values_table(examples) + h_powerset = power_set(values.keys()) + hypotheses = [] + for s in h_powerset: + hypotheses.extend(build_attr_combinations(s, values)) + + hypotheses.extend(build_h_combinations(hypotheses)) + + return hypotheses + + +def values_table(examples): + """Build a table with all the possible values for each attribute. + Returns a dictionary with keys the attribute names and values a list + with the possible values for the corresponding attribute.""" + values = defaultdict(lambda: []) + for e in examples: + for k, v in e.items(): + if k == 'GOAL': + continue + + mod = '!' + if e['GOAL']: + mod = '' + + if mod + v not in values[k]: + values[k].append(mod + v) + + values = dict(values) + return values + + +def build_attr_combinations(s, values): + """Given a set of attributes, builds all the combinations of values. + If the set holds more than one attribute, recursively builds the + combinations.""" + if len(s) == 1: + # s holds just one attribute, return its list of values + k = values[s[0]] + h = [[{s[0]: v}] for v in values[s[0]]] + return h + + h = [] + for i, a in enumerate(s): + rest = build_attr_combinations(s[i + 1:], values) + for v in values[a]: + o = {a: v} + for r in rest: + t = o.copy() + for d in r: + t.update(d) + h.append([t]) + + return h + + +def build_h_combinations(hypotheses): + """Given a set of hypotheses, builds and returns all the combinations of the + hypotheses.""" + h = [] + h_powerset = power_set(range(len(hypotheses))) + + for s in h_powerset: + t = [] + for i in s: + t.extend(hypotheses[i]) + h.append(t) + + return h + + +# ______________________________________________________________________________ + + +def minimal_consistent_det(E, A): + """Return a minimal set of attributes which give consistent determination""" + n = len(A) + + for i in range(n + 1): + for A_i in combinations(A, i): + if consistent_det(A_i, E): + return set(A_i) + + +def consistent_det(A, E): + """Check if the attributes(A) is consistent with the examples(E)""" + H = {} + + for e in E: + attr_values = tuple(e[attr] for attr in A) + if attr_values in H and H[attr_values] != e['GOAL']: + return False + H[attr_values] = e['GOAL'] + + return True + + +# ______________________________________________________________________________ + + +class FOILContainer(FolKB): + """Hold the kb and other necessary elements required by FOIL.""" + + def __init__(self, clauses=None): + self.const_syms = set() + self.pred_syms = set() + super().__init__(clauses) + + def tell(self, sentence): + if is_definite_clause(sentence): + self.clauses.append(sentence) + self.const_syms.update(constant_symbols(sentence)) + self.pred_syms.update(predicate_symbols(sentence)) + else: + raise Exception('Not a definite clause: {}'.format(sentence)) + + def foil(self, examples, target): + """Learn a list of first-order horn clauses + 'examples' is a tuple: (positive_examples, negative_examples). + positive_examples and negative_examples are both lists which contain substitutions.""" + clauses = [] + + pos_examples = examples[0] + neg_examples = examples[1] + + while pos_examples: + clause, extended_pos_examples = self.new_clause((pos_examples, neg_examples), target) + # remove positive examples covered by clause + pos_examples = self.update_examples(target, pos_examples, extended_pos_examples) + clauses.append(clause) + + return clauses + + def new_clause(self, examples, target): + """Find a horn clause which satisfies part of the positive + examples but none of the negative examples. + The horn clause is specified as [consequent, list of antecedents] + Return value is the tuple (horn_clause, extended_positive_examples).""" + clause = [target, []] + extended_examples = examples + while extended_examples[1]: + l = self.choose_literal(self.new_literals(clause), extended_examples) + clause[1].append(l) + extended_examples = [sum([list(self.extend_example(example, l)) for example in + extended_examples[i]], []) for i in range(2)] + + return clause, extended_examples[0] + + def extend_example(self, example, literal): + """Generate extended examples which satisfy the literal.""" + # find all substitutions that satisfy literal + for s in self.ask_generator(subst(example, literal)): + s.update(example) + yield s + + def new_literals(self, clause): + """Generate new literals based on known predicate symbols. + Generated literal must share at least one variable with clause""" + share_vars = variables(clause[0]) + for l in clause[1]: + share_vars.update(variables(l)) + for pred, arity in self.pred_syms: + new_vars = {standardize_variables(expr('x')) for _ in range(arity - 1)} + for args in product(share_vars.union(new_vars), repeat=arity): + if any(var in share_vars for var in args): + # make sure we don't return an existing rule + if not Expr(pred, args) in clause[1]: + yield Expr(pred, *[var for var in args]) + + def choose_literal(self, literals, examples): + """Choose the best literal based on the information gain.""" + return max(literals, key=partial(self.gain, examples=examples)) + + def gain(self, l, examples): + """ + Find the utility of each literal when added to the body of the clause. + Utility function is: + gain(R, l) = T * (log_2 (post_pos / (post_pos + post_neg)) - log_2 (pre_pos / (pre_pos + pre_neg))) + + where: + + pre_pos = number of possitive bindings of rule R (=current set of rules) + pre_neg = number of negative bindings of rule R + post_pos = number of possitive bindings of rule R' (= R U {l} ) + post_neg = number of negative bindings of rule R' + T = number of possitive bindings of rule R that are still covered + after adding literal l + + """ + pre_pos = len(examples[0]) + pre_neg = len(examples[1]) + post_pos = sum([list(self.extend_example(example, l)) for example in examples[0]], []) + post_neg = sum([list(self.extend_example(example, l)) for example in examples[1]], []) + if pre_pos + pre_neg == 0 or len(post_pos) + len(post_neg) == 0: + return -1 + # number of positive example that are represented in extended_examples + T = 0 + for example in examples[0]: + represents = lambda d: all(d[x] == example[x] for x in example) + if any(represents(l_) for l_ in post_pos): + T += 1 + value = T * (np.log2(len(post_pos) / (len(post_pos) + len(post_neg)) + 1e-12) - + np.log2(pre_pos / (pre_pos + pre_neg))) + return value + + def update_examples(self, target, examples, extended_examples): + """Add to the kb those examples what are represented in extended_examples + List of omitted examples is returned.""" + uncovered = [] + for example in examples: + represents = lambda d: all(d[x] == example[x] for x in example) + if any(represents(l) for l in extended_examples): + self.tell(subst(example, target)) + else: + uncovered.append(example) + + return uncovered + + +# ______________________________________________________________________________ + + +def check_all_consistency(examples, h): + """Check for the consistency of all examples under h.""" + for e in examples: + if not is_consistent(e, h): + return False + + return True + + +def check_negative_consistency(examples, h): + """Check if the negative examples are consistent under h.""" + for e in examples: + if e['GOAL']: + continue + + if not is_consistent(e, [h]): + return False + + return True + + +def disjunction_value(e, d): + """The value of example e under disjunction d.""" + for k, v in d.items(): + if v[0] == '!': + # v is a NOT expression + # e[k], thus, should not be equal to v + if e[k] == v[1:]: + return False + elif e[k] != v: + return False + + return True + + +def guess_value(e, h): + """Guess value of example e under hypothesis h.""" + for d in h: + if disjunction_value(e, d): + return True + + return False + + +def is_consistent(e, h): + return e['GOAL'] == guess_value(e, h) + + +def false_positive(e, h): + return guess_value(e, h) and not e['GOAL'] + + +def false_negative(e, h): + return e['GOAL'] and not guess_value(e, h) diff --git a/knowledge_FOIL.ipynb b/knowledge_FOIL.ipynb new file mode 100644 index 000000000..4cefd7f69 --- /dev/null +++ b/knowledge_FOIL.ipynb @@ -0,0 +1,639 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# KNOWLEDGE\n", + "\n", + "The [knowledge](https://github.com/aimacode/aima-python/blob/master/knowledge.py) module covers **Chapter 19: Knowledge in Learning** from Stuart Russel's and Peter Norvig's book *Artificial Intelligence: A Modern Approach*.\n", + "\n", + "Execute the cell below to get started." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from knowledge import *\n", + "from notebook import psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* Inductive Logic Programming (FOIL)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OVERVIEW\n", + "\n", + "Like the [learning module](https://github.com/aimacode/aima-python/blob/master/learning.ipynb), this chapter focuses on methods for generating a model/hypothesis for a domain; however, unlike the learning chapter, here we use prior knowledge to help us learn from new experiences and to find a proper hypothesis.\n", + "\n", + "### First-Order Logic\n", + "\n", + "Usually knowledge in this field is represented as **first-order logic**, a type of logic that uses variables and quantifiers in logical sentences. Hypotheses are represented by logical sentences with variables, while examples are logical sentences with set values instead of variables. The goal is to assign a value to a special first-order logic predicate, called **goal predicate**, for new examples given a hypothesis. We learn this hypothesis by infering knowledge from some given examples.\n", + "\n", + "### Representation\n", + "\n", + "In this module, we use dictionaries to represent examples, with keys being the attribute names and values being the corresponding example values. Examples also have an extra boolean field, 'GOAL', for the goal predicate. A hypothesis is represented as a list of dictionaries. Each dictionary in that list represents a disjunction. Inside these dictionaries/disjunctions we have conjunctions.\n", + "\n", + "For example, say we want to predict if an animal (cat or dog) will take an umbrella given whether or not it rains or the animal wears a coat. The goal value is 'take an umbrella' and is denoted by the key 'GOAL'. An example:\n", + "\n", + "`{'Species': 'Cat', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`\n", + "\n", + "A hypothesis can be the following:\n", + "\n", + "`[{'Species': 'Cat'}]`\n", + "\n", + "which means an animal will take an umbrella if and only if it is a cat.\n", + "\n", + "### Consistency\n", + "\n", + "We say that an example `e` is **consistent** with an hypothesis `h` if the assignment from the hypothesis for `e` is the same as `e['GOAL']`. If the above example and hypothesis are `e` and `h` respectively, then `e` is consistent with `h` since `e['Species'] == 'Cat'`. For `e = {'Species': 'Dog', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`, the example is no longer consistent with `h`, since the value assigned to `e` is *False* while `e['GOAL']` is *True*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Inductive Logic Programming (FOIL)\n", + "\n", + "Inductive logic programming (ILP) combines inductive methods with the power of first-order representations, concentrating in particular on the representation of hypotheses as logic programs. The general knowledge-based induction problem is to solve the entailment constraint:

    \n", + "$ Background ∧ Hypothesis ∧ Descriptions \\vDash Classifications $\n", + "\n", + "for the __unknown__ $Hypothesis$, given the $Background$ knowledge described by $Descriptions$ and $Classifications$.\n", + "\n", + "\n", + "\n", + "The first approach to ILP works by starting with a very general rule and gradually specializing\n", + "it so that it fits the data.
    \n", + "This is essentially what happens in decision-tree learning, where a\n", + "decision tree is gradually grown until it is consistent with the observations.
    To do ILP we\n", + "use first-order literals instead of attributes, and the $Hypothesis$ is a set of clauses (set of first order rules, where each rule is similar to a Horn clause) instead of a decision tree.
    \n", + "\n", + "\n", + "The FOIL algorithm learns new rules, one at a time, in order to cover all given positive and negative examples.
    \n", + "More precicely, FOIL contains an inner and an outer while loop.
    \n", + "- __outer loop__: (function __foil()__) add rules until all positive examples are covered.
    \n", + " (each rule is a conjuction of literals, which are chosen inside the inner loop)\n", + " \n", + " \n", + "- __inner loop__: (function __new_clause()__) add new literals until all negative examples are covered, and some positive examples are covered.
    \n", + " - In each iteration, we select/add the most promising literal, according to an estimate of its utility. (function __new_literal()__)
    \n", + " \n", + " - The evaluation function to estimate utility of adding literal $L$ to a set of rules $R$ is (function __gain()__) : \n", + " \n", + " $$ FoilGain(L,R) = t \\big( \\log_2{\\frac{p_1}{p_1+n_1}} - \\log_2{\\frac{p_0}{p_0+n_0}} \\big) $$\n", + " where: \n", + " \n", + " $p_0: \\text{is the number of possitive bindings of rule R } \\\\ n_0: \\text{is the number of negative bindings of R} \\\\ p_1: \\text{is the is the number of possitive bindings of rule R'}\\\\ n_0: \\text{is the number of negative bindings of R'}\\\\ t: \\text{is the number of possitive bindings of rule R that are still covered after adding literal L to R}$\n", + " \n", + " - Calculate the extended examples for the chosen literal (function __extend_example()__)
    \n", + " (the set of examples created by extending example with each possible constant value for each new variable in literal)\n", + " \n", + "- Finally, the algorithm returns a disjunction of first order rules (= conjuction of literals)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    class FOIL_container(FolKB):\n",
    +       "    """Hold the kb and other necessary elements required by FOIL."""\n",
    +       "\n",
    +       "    def __init__(self, clauses=None):\n",
    +       "        self.const_syms = set()\n",
    +       "        self.pred_syms = set()\n",
    +       "        FolKB.__init__(self, clauses)\n",
    +       "\n",
    +       "    def tell(self, sentence):\n",
    +       "        if is_definite_clause(sentence):\n",
    +       "            self.clauses.append(sentence)\n",
    +       "            self.const_syms.update(constant_symbols(sentence))\n",
    +       "            self.pred_syms.update(predicate_symbols(sentence))\n",
    +       "        else:\n",
    +       "            raise Exception("Not a definite clause: {}".format(sentence))\n",
    +       "\n",
    +       "    def foil(self, examples, target):\n",
    +       "        """Learn a list of first-order horn clauses\n",
    +       "        'examples' is a tuple: (positive_examples, negative_examples).\n",
    +       "        positive_examples and negative_examples are both lists which contain substitutions."""\n",
    +       "        clauses = []\n",
    +       "\n",
    +       "        pos_examples = examples[0]\n",
    +       "        neg_examples = examples[1]\n",
    +       "\n",
    +       "        while pos_examples:\n",
    +       "            clause, extended_pos_examples = self.new_clause((pos_examples, neg_examples), target)\n",
    +       "            # remove positive examples covered by clause\n",
    +       "            pos_examples = self.update_examples(target, pos_examples, extended_pos_examples)\n",
    +       "            clauses.append(clause)\n",
    +       "\n",
    +       "        return clauses\n",
    +       "\n",
    +       "    def new_clause(self, examples, target):\n",
    +       "        """Find a horn clause which satisfies part of the positive\n",
    +       "        examples but none of the negative examples.\n",
    +       "        The horn clause is specified as [consequent, list of antecedents]\n",
    +       "        Return value is the tuple (horn_clause, extended_positive_examples)."""\n",
    +       "        clause = [target, []]\n",
    +       "        # [positive_examples, negative_examples]\n",
    +       "        extended_examples = examples\n",
    +       "        while extended_examples[1]:\n",
    +       "            l = self.choose_literal(self.new_literals(clause), extended_examples)\n",
    +       "            clause[1].append(l)\n",
    +       "            extended_examples = [sum([list(self.extend_example(example, l)) for example in\n",
    +       "                                      extended_examples[i]], []) for i in range(2)]\n",
    +       "\n",
    +       "        return (clause, extended_examples[0])\n",
    +       "\n",
    +       "    def extend_example(self, example, literal):\n",
    +       "        """Generate extended examples which satisfy the literal."""\n",
    +       "        # find all substitutions that satisfy literal\n",
    +       "        for s in self.ask_generator(subst(example, literal)):\n",
    +       "            s.update(example)\n",
    +       "            yield s\n",
    +       "\n",
    +       "    def new_literals(self, clause):\n",
    +       "        """Generate new literals based on known predicate symbols.\n",
    +       "        Generated literal must share atleast one variable with clause"""\n",
    +       "        share_vars = variables(clause[0])\n",
    +       "        for l in clause[1]:\n",
    +       "            share_vars.update(variables(l))\n",
    +       "        for pred, arity in self.pred_syms:\n",
    +       "            new_vars = {standardize_variables(expr('x')) for _ in range(arity - 1)}\n",
    +       "            for args in product(share_vars.union(new_vars), repeat=arity):\n",
    +       "                if any(var in share_vars for var in args):\n",
    +       "                    # make sure we don't return an existing rule\n",
    +       "                    if not Expr(pred, args) in clause[1]:\n",
    +       "                        yield Expr(pred, *[var for var in args])\n",
    +       "\n",
    +       "\n",
    +       "    def choose_literal(self, literals, examples): \n",
    +       "        """Choose the best literal based on the information gain."""\n",
    +       "\n",
    +       "        return max(literals, key = partial(self.gain , examples = examples))\n",
    +       "\n",
    +       "\n",
    +       "    def gain(self, l ,examples):\n",
    +       "        """\n",
    +       "        Find the utility of each literal when added to the body of the clause. \n",
    +       "        Utility function is: \n",
    +       "            gain(R, l) = T * (log_2 (post_pos / (post_pos + post_neg)) - log_2 (pre_pos / (pre_pos + pre_neg)))\n",
    +       "\n",
    +       "        where: \n",
    +       "        \n",
    +       "            pre_pos = number of possitive bindings of rule R (=current set of rules)\n",
    +       "            pre_neg = number of negative bindings of rule R \n",
    +       "            post_pos = number of possitive bindings of rule R' (= R U {l} )\n",
    +       "            post_neg = number of negative bindings of rule R' \n",
    +       "            T = number of possitive bindings of rule R that are still covered \n",
    +       "                after adding literal l \n",
    +       "\n",
    +       "        """\n",
    +       "        pre_pos = len(examples[0])\n",
    +       "        pre_neg = len(examples[1])\n",
    +       "        post_pos = sum([list(self.extend_example(example, l)) for example in examples[0]], [])           \n",
    +       "        post_neg = sum([list(self.extend_example(example, l)) for example in examples[1]], []) \n",
    +       "        if pre_pos + pre_neg ==0 or len(post_pos) + len(post_neg)==0:\n",
    +       "            return -1\n",
    +       "        # number of positive example that are represented in extended_examples\n",
    +       "        T = 0\n",
    +       "        for example in examples[0]:\n",
    +       "            represents = lambda d: all(d[x] == example[x] for x in example)\n",
    +       "            if any(represents(l_) for l_ in post_pos):\n",
    +       "                T += 1\n",
    +       "        value = T * (log(len(post_pos) / (len(post_pos) + len(post_neg)) + 1e-12,2) - log(pre_pos / (pre_pos + pre_neg),2))\n",
    +       "        return value\n",
    +       "\n",
    +       "\n",
    +       "    def update_examples(self, target, examples, extended_examples):\n",
    +       "        """Add to the kb those examples what are represented in extended_examples\n",
    +       "        List of omitted examples is returned."""\n",
    +       "        uncovered = []\n",
    +       "        for example in examples:\n",
    +       "            represents = lambda d: all(d[x] == example[x] for x in example)\n",
    +       "            if any(represents(l) for l in extended_examples):\n",
    +       "                self.tell(subst(example, target))\n",
    +       "            else:\n",
    +       "                uncovered.append(example)\n",
    +       "\n",
    +       "        return uncovered\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(FOIL_container)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example Family \n", + "Suppose we have the following family relations:\n", + "
    \n", + "![title](images/knowledge_foil_family.png)\n", + "
    \n", + "Given some positive and negative examples of the relation 'Parent(x,y)', we want to find a set of rules that satisfies all the examples.
    \n", + "\n", + "A definition of Parent is $Parent(x,y) \\Leftrightarrow Mother(x,y) \\lor Father(x,y)$, which is the result that we expect from the algorithm. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "A, B, C, D, E, F, G, H, I, x, y, z = map(expr, 'ABCDEFGHIxyz')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "small_family = FOIL_container([expr(\"Mother(Anne, Peter)\"),\n", + " expr(\"Mother(Anne, Zara)\"),\n", + " expr(\"Mother(Sarah, Beatrice)\"),\n", + " expr(\"Mother(Sarah, Eugenie)\"),\n", + " expr(\"Father(Mark, Peter)\"),\n", + " expr(\"Father(Mark, Zara)\"),\n", + " expr(\"Father(Andrew, Beatrice)\"),\n", + " expr(\"Father(Andrew, Eugenie)\"),\n", + " expr(\"Father(Philip, Anne)\"),\n", + " expr(\"Father(Philip, Andrew)\"),\n", + " expr(\"Mother(Elizabeth, Anne)\"),\n", + " expr(\"Mother(Elizabeth, Andrew)\"),\n", + " expr(\"Male(Philip)\"),\n", + " expr(\"Male(Mark)\"),\n", + " expr(\"Male(Andrew)\"),\n", + " expr(\"Male(Peter)\"),\n", + " expr(\"Female(Elizabeth)\"),\n", + " expr(\"Female(Anne)\"),\n", + " expr(\"Female(Sarah)\"),\n", + " expr(\"Female(Zara)\"),\n", + " expr(\"Female(Beatrice)\"),\n", + " expr(\"Female(Eugenie)\"),\n", + "])\n", + "\n", + "target = expr('Parent(x, y)')\n", + "\n", + "examples_pos = [{x: expr('Elizabeth'), y: expr('Anne')},\n", + " {x: expr('Elizabeth'), y: expr('Andrew')},\n", + " {x: expr('Philip'), y: expr('Anne')},\n", + " {x: expr('Philip'), y: expr('Andrew')},\n", + " {x: expr('Anne'), y: expr('Peter')},\n", + " {x: expr('Anne'), y: expr('Zara')},\n", + " {x: expr('Mark'), y: expr('Peter')},\n", + " {x: expr('Mark'), y: expr('Zara')},\n", + " {x: expr('Andrew'), y: expr('Beatrice')},\n", + " {x: expr('Andrew'), y: expr('Eugenie')},\n", + " {x: expr('Sarah'), y: expr('Beatrice')},\n", + " {x: expr('Sarah'), y: expr('Eugenie')}]\n", + "examples_neg = [{x: expr('Anne'), y: expr('Eugenie')},\n", + " {x: expr('Beatrice'), y: expr('Eugenie')},\n", + " {x: expr('Mark'), y: expr('Elizabeth')},\n", + " {x: expr('Beatrice'), y: expr('Philip')}]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Parent(x, y), [Father(x, y)]], [Parent(x, y), [Mother(x, y)]]]\n" + ] + } + ], + "source": [ + "# run the FOIL algorithm \n", + "clauses = small_family.foil([examples_pos, examples_neg], target)\n", + "print (clauses)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Indeed the algorithm returned the rule: \n", + "
    $Parent(x,y) \\Leftrightarrow Mother(x,y) \\lor Father(x,y)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Suppose that we have some positive and negative results for the relation 'GrandParent(x,y)' and we want to find a set of rules that satisfies the examples.
    \n", + "One possible set of rules for the relation $Grandparent(x,y)$ could be:
    \n", + "![title](images/knowledge_FOIL_grandparent.png)\n", + "
    \n", + "Or, if $Background$ included the sentence $Parent(x,y) \\Leftrightarrow [Mother(x,y) \\lor Father(x,y)]$ then: \n", + "\n", + "$$Grandparent(x,y) \\Leftrightarrow \\exists \\: z \\quad Parent(x,z) \\land Parent(z,y)$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Grandparent(x, y), [Parent(x, v_6), Parent(v_6, y)]]]\n" + ] + } + ], + "source": [ + "target = expr('Grandparent(x, y)')\n", + "\n", + "examples_pos = [{x: expr('Elizabeth'), y: expr('Peter')},\n", + " {x: expr('Elizabeth'), y: expr('Zara')},\n", + " {x: expr('Elizabeth'), y: expr('Beatrice')},\n", + " {x: expr('Elizabeth'), y: expr('Eugenie')},\n", + " {x: expr('Philip'), y: expr('Peter')},\n", + " {x: expr('Philip'), y: expr('Zara')},\n", + " {x: expr('Philip'), y: expr('Beatrice')},\n", + " {x: expr('Philip'), y: expr('Eugenie')}]\n", + "examples_neg = [{x: expr('Anne'), y: expr('Eugenie')},\n", + " {x: expr('Beatrice'), y: expr('Eugenie')},\n", + " {x: expr('Elizabeth'), y: expr('Andrew')},\n", + " {x: expr('Elizabeth'), y: expr('Anne')},\n", + " {x: expr('Elizabeth'), y: expr('Mark')},\n", + " {x: expr('Elizabeth'), y: expr('Sarah')},\n", + " {x: expr('Philip'), y: expr('Anne')},\n", + " {x: expr('Philip'), y: expr('Andrew')},\n", + " {x: expr('Anne'), y: expr('Peter')},\n", + " {x: expr('Anne'), y: expr('Zara')},\n", + " {x: expr('Mark'), y: expr('Peter')},\n", + " {x: expr('Mark'), y: expr('Zara')},\n", + " {x: expr('Andrew'), y: expr('Beatrice')},\n", + " {x: expr('Andrew'), y: expr('Eugenie')},\n", + " {x: expr('Sarah'), y: expr('Beatrice')},\n", + " {x: expr('Mark'), y: expr('Elizabeth')},\n", + " {x: expr('Beatrice'), y: expr('Philip')}, \n", + " {x: expr('Peter'), y: expr('Andrew')}, \n", + " {x: expr('Zara'), y: expr('Mark')},\n", + " {x: expr('Peter'), y: expr('Anne')},\n", + " {x: expr('Zara'), y: expr('Eugenie')}, ]\n", + "\n", + "clauses = small_family.foil([examples_pos, examples_neg], target)\n", + "\n", + "print(clauses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Indeed the algorithm returned the rule: \n", + "
    $Grandparent(x,y) \\Leftrightarrow \\exists \\: v \\: \\: Parent(x,v) \\land Parent(v,y)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example Network\n", + "\n", + "Suppose that we have the following directed graph and we want to find a rule that describes the reachability between two nodes (Reach(x,y)).
    \n", + "Such a rule could be recursive, since y can be reached from x if and only if there is a sequence of adjacent nodes from x to y: \n", + "\n", + "$$ Reach(x,y) \\Leftrightarrow \\begin{cases} \n", + " Conn(x,y), \\: \\text{(if there is a directed edge from x to y)} \\\\\n", + " \\lor \\quad \\exists \\: z \\quad Reach(x,z) \\land Reach(z,y) \\end{cases}$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "A H\n", + "|\\ /|\n", + "| \\ / |\n", + "v v v v\n", + "B D-->E-->G-->I\n", + "| / |\n", + "| / |\n", + "vv v\n", + "C F\n", + "\"\"\"\n", + "small_network = FOIL_container([expr(\"Conn(A, B)\"),\n", + " expr(\"Conn(A ,D)\"),\n", + " expr(\"Conn(B, C)\"),\n", + " expr(\"Conn(D, C)\"),\n", + " expr(\"Conn(D, E)\"),\n", + " expr(\"Conn(E ,F)\"),\n", + " expr(\"Conn(E, G)\"),\n", + " expr(\"Conn(G, I)\"),\n", + " expr(\"Conn(H, G)\"),\n", + " expr(\"Conn(H, I)\")])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Reach(x, y), [Conn(x, y)]], [Reach(x, y), [Reach(x, v_12), Reach(v_14, y), Reach(v_12, v_16), Reach(v_12, y)]], [Reach(x, y), [Reach(x, v_20), Reach(v_20, y)]]]\n" + ] + } + ], + "source": [ + "target = expr('Reach(x, y)')\n", + "examples_pos = [{x: A, y: B},\n", + " {x: A, y: C},\n", + " {x: A, y: D},\n", + " {x: A, y: E},\n", + " {x: A, y: F},\n", + " {x: A, y: G},\n", + " {x: A, y: I},\n", + " {x: B, y: C},\n", + " {x: D, y: C},\n", + " {x: D, y: E},\n", + " {x: D, y: F},\n", + " {x: D, y: G},\n", + " {x: D, y: I},\n", + " {x: E, y: F},\n", + " {x: E, y: G},\n", + " {x: E, y: I},\n", + " {x: G, y: I},\n", + " {x: H, y: G},\n", + " {x: H, y: I}]\n", + "nodes = {A, B, C, D, E, F, G, H, I}\n", + "examples_neg = [example for example in [{x: a, y: b} for a in nodes for b in nodes]\n", + " if example not in examples_pos]\n", + "clauses = small_network.foil([examples_pos, examples_neg], target)\n", + "\n", + "print(clauses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The algorithm produced something close to the recursive rule: \n", + " $$ Reach(x,y) \\Leftrightarrow [Conn(x,y)] \\: \\lor \\: [\\exists \\: z \\: \\: Reach(x,z) \\, \\land \\, Reach(z,y)]$$\n", + " \n", + "This happened because the size of the example is small. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "source": [], + "metadata": { + "collapsed": false + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/knowledge_current_best.ipynb b/knowledge_current_best.ipynb new file mode 100644 index 000000000..5da492cd0 --- /dev/null +++ b/knowledge_current_best.ipynb @@ -0,0 +1,662 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# KNOWLEDGE\n", + "\n", + "The [knowledge](https://github.com/aimacode/aima-python/blob/master/knowledge.py) module covers **Chapter 19: Knowledge in Learning** from Stuart Russel's and Peter Norvig's book *Artificial Intelligence: A Modern Approach*.\n", + "\n", + "Execute the cell below to get started." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from knowledge import *\n", + "\n", + "from notebook import pseudocode, psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* Current-Best Learning" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OVERVIEW\n", + "\n", + "Like the [learning module](https://github.com/aimacode/aima-python/blob/master/learning.ipynb), this chapter focuses on methods for generating a model/hypothesis for a domain; however, unlike the learning chapter, here we use prior knowledge to help us learn from new experiences and find a proper hypothesis.\n", + "\n", + "### First-Order Logic\n", + "\n", + "Usually knowledge in this field is represented as **first-order logic**; a type of logic that uses variables and quantifiers in logical sentences. Hypotheses are represented by logical sentences with variables, while examples are logical sentences with set values instead of variables. The goal is to assign a value to a special first-order logic predicate, called **goal predicate**, for new examples given a hypothesis. We learn this hypothesis by infering knowledge from some given examples.\n", + "\n", + "### Representation\n", + "\n", + "In this module, we use dictionaries to represent examples, with keys being the attribute names and values being the corresponding example values. Examples also have an extra boolean field, 'GOAL', for the goal predicate. A hypothesis is represented as a list of dictionaries. Each dictionary in that list represents a disjunction. Inside these dictionaries/disjunctions we have conjunctions.\n", + "\n", + "For example, say we want to predict if an animal (cat or dog) will take an umbrella given whether or not it rains or the animal wears a coat. The goal value is 'take an umbrella' and is denoted by the key 'GOAL'. An example:\n", + "\n", + "`{'Species': 'Cat', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`\n", + "\n", + "A hypothesis can be the following:\n", + "\n", + "`[{'Species': 'Cat'}]`\n", + "\n", + "which means an animal will take an umbrella if and only if it is a cat.\n", + "\n", + "### Consistency\n", + "\n", + "We say that an example `e` is **consistent** with an hypothesis `h` if the assignment from the hypothesis for `e` is the same as `e['GOAL']`. If the above example and hypothesis are `e` and `h` respectively, then `e` is consistent with `h` since `e['Species'] == 'Cat'`. For `e = {'Species': 'Dog', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`, the example is no longer consistent with `h`, since the value assigned to `e` is *False* while `e['GOAL']` is *True*." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## CURRENT-BEST LEARNING\n", + "\n", + "### Overview\n", + "\n", + "In **Current-Best Learning**, we start with a hypothesis and we refine it as we iterate through the examples. For each example, there are three possible outcomes: the example is consistent with the hypothesis, the example is a **false positive** (real value is false but got predicted as true) and the example is a **false negative** (real value is true but got predicted as false). Depending on the outcome we refine the hypothesis accordingly:\n", + "\n", + "* Consistent: We do not change the hypothesis and move on to the next example.\n", + "\n", + "* False Positive: We **specialize** the hypothesis, which means we add a conjunction.\n", + "\n", + "* False Negative: We **generalize** the hypothesis, either by removing a conjunction or a disjunction, or by adding a disjunction.\n", + "\n", + "When specializing or generalizing, we should make sure to not create inconsistencies with previous examples. To avoid that caveat, backtracking is needed. Thankfully, there is not just one specialization or generalization, so we have a lot to choose from. We will go through all the specializations/generalizations and we will refine our hypothesis as the first specialization/generalization consistent with all the examples seen up to that point." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pseudocode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ Current-Best-Learning(_examples_, _h_) __returns__ a hypothesis or fail \n", + " __if__ _examples_ is empty __then__ \n", + "   __return__ _h_ \n", + " _e_ ← First(_examples_) \n", + " __if__ _e_ is consistent with _h_ __then__ \n", + "   __return__ Current-Best-Learning(Rest(_examples_), _h_) \n", + " __else if__ _e_ is a false positive for _h_ __then__ \n", + "   __for each__ _h'_ __in__ specializations of _h_ consistent with _examples_ seen so far __do__ \n", + "     _h''_ ← Current-Best-Learning(Rest(_examples_), _h'_) \n", + "     __if__ _h''_ ≠ _fail_ __then return__ _h''_ \n", + " __else if__ _e_ is a false negative for _h_ __then__ \n", + "   __for each__ _h'_ __in__ generalizations of _h_ consistent with _examples_ seen so far __do__ \n", + "     _h''_ ← Current-Best-Learning(Rest(_examples_), _h'_) \n", + "     __if__ _h''_ ≠ _fail_ __then return__ _h''_ \n", + " __return__ _fail_ \n", + "\n", + "---\n", + "__Figure ??__ The current-best-hypothesis learning algorithm. It searches for a consistent hypothesis that fits all the examples and backtracks when no consistent specialization/generalization can be found. To start the algorithm, any hypothesis can be passed in; it will be specialized or generalized as needed." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('Current-Best-Learning')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "As mentioned earlier, examples are dictionaries (with keys being the attribute names) and hypotheses are lists of dictionaries (each dictionary is a disjunction). Also, in the hypothesis, we denote the *NOT* operation with an exclamation mark (!).\n", + "\n", + "We have functions to calculate the list of all specializations/generalizations, to check if an example is consistent/false positive/false negative with a hypothesis. We also have an auxiliary function to add a disjunction (or operation) to a hypothesis, and two other functions to check consistency of all (or just the negative) examples.\n", + "\n", + "You can read the source by running the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def current_best_learning(examples, h, examples_so_far=None):\n",
    +       "    """ [Figure 19.2]\n",
    +       "    The hypothesis is a list of dictionaries, with each dictionary representing\n",
    +       "    a disjunction."""\n",
    +       "    if not examples:\n",
    +       "        return h\n",
    +       "\n",
    +       "    examples_so_far = examples_so_far or []\n",
    +       "    e = examples[0]\n",
    +       "    if is_consistent(e, h):\n",
    +       "        return current_best_learning(examples[1:], h, examples_so_far + [e])\n",
    +       "    elif false_positive(e, h):\n",
    +       "        for h2 in specializations(examples_so_far + [e], h):\n",
    +       "            h3 = current_best_learning(examples[1:], h2, examples_so_far + [e])\n",
    +       "            if h3 != 'FAIL':\n",
    +       "                return h3\n",
    +       "    elif false_negative(e, h):\n",
    +       "        for h2 in generalizations(examples_so_far + [e], h):\n",
    +       "            h3 = current_best_learning(examples[1:], h2, examples_so_far + [e])\n",
    +       "            if h3 != 'FAIL':\n",
    +       "                return h3\n",
    +       "\n",
    +       "    return 'FAIL'\n",
    +       "\n",
    +       "\n",
    +       "def specializations(examples_so_far, h):\n",
    +       "    """Specialize the hypothesis by adding AND operations to the disjunctions"""\n",
    +       "    hypotheses = []\n",
    +       "\n",
    +       "    for i, disj in enumerate(h):\n",
    +       "        for e in examples_so_far:\n",
    +       "            for k, v in e.items():\n",
    +       "                if k in disj or k == 'GOAL':\n",
    +       "                    continue\n",
    +       "\n",
    +       "                h2 = h[i].copy()\n",
    +       "                h2[k] = '!' + v\n",
    +       "                h3 = h.copy()\n",
    +       "                h3[i] = h2\n",
    +       "                if check_all_consistency(examples_so_far, h3):\n",
    +       "                    hypotheses.append(h3)\n",
    +       "\n",
    +       "    shuffle(hypotheses)\n",
    +       "    return hypotheses\n",
    +       "\n",
    +       "\n",
    +       "def generalizations(examples_so_far, h):\n",
    +       "    """Generalize the hypothesis. First delete operations\n",
    +       "    (including disjunctions) from the hypothesis. Then, add OR operations."""\n",
    +       "    hypotheses = []\n",
    +       "\n",
    +       "    # Delete disjunctions\n",
    +       "    disj_powerset = powerset(range(len(h)))\n",
    +       "    for disjs in disj_powerset:\n",
    +       "        h2 = h.copy()\n",
    +       "        for d in reversed(list(disjs)):\n",
    +       "            del h2[d]\n",
    +       "\n",
    +       "        if check_all_consistency(examples_so_far, h2):\n",
    +       "            hypotheses += h2\n",
    +       "\n",
    +       "    # Delete AND operations in disjunctions\n",
    +       "    for i, disj in enumerate(h):\n",
    +       "        a_powerset = powerset(disj.keys())\n",
    +       "        for attrs in a_powerset:\n",
    +       "            h2 = h[i].copy()\n",
    +       "            for a in attrs:\n",
    +       "                del h2[a]\n",
    +       "\n",
    +       "            if check_all_consistency(examples_so_far, [h2]):\n",
    +       "                h3 = h.copy()\n",
    +       "                h3[i] = h2.copy()\n",
    +       "                hypotheses += h3\n",
    +       "\n",
    +       "    # Add OR operations\n",
    +       "    if hypotheses == [] or hypotheses == [{}]:\n",
    +       "        hypotheses = add_or(examples_so_far, h)\n",
    +       "    else:\n",
    +       "        hypotheses.extend(add_or(examples_so_far, h))\n",
    +       "\n",
    +       "    shuffle(hypotheses)\n",
    +       "    return hypotheses\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(current_best_learning, specializations, generalizations)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can view the auxiliary functions in the [knowledge module](https://github.com/aimacode/aima-python/blob/master/knowledge.py). A few notes on the functionality of some of the important methods:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* `specializations`: For each disjunction in the hypothesis, it adds a conjunction for values in the examples encountered so far (if the conjunction is consistent with all the examples). It returns a list of hypotheses.\n", + "\n", + "* `generalizations`: It adds to the list of hypotheses in three phases. First it deletes disjunctions, then it deletes conjunctions and finally it adds a disjunction.\n", + "\n", + "* `add_or`: Used by `generalizations` to add an *or operation* (a disjunction) to the hypothesis. Since the last example is the problematic one which wasn't consistent with the hypothesis, it will model the new disjunction to that example. It creates a disjunction for each combination of attributes in the example and returns the new hypotheses consistent with the negative examples encountered so far. We do not need to check the consistency of positive examples, since they are already consistent with at least one other disjunction in the hypotheses' set, so this new disjunction doesn't affect them. In other words, if the value of a positive example is negative under the disjunction, it doesn't matter since we know there exists a disjunction consistent with the example." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the algorithm stops searching the specializations/generalizations after the first consistent hypothesis is found, usually you will get different results each time you run the code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Examples\n", + "\n", + "We will take a look at two examples. The first is a trivial one, while the second is a bit more complicated (you can also find it in the book).\n", + "\n", + "Earlier, we had the \"animals taking umbrellas\" example. Now we want to find a hypothesis to predict whether or not an animal will take an umbrella. The attributes are `Species`, `Rain` and `Coat`. The possible values are `[Cat, Dog]`, `[Yes, No]` and `[Yes, No]` respectively. Below we give seven examples (with `GOAL` we denote whether an animal will take an umbrella or not):" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "animals_umbrellas = [\n", + " {'Species': 'Cat', 'Rain': 'Yes', 'Coat': 'No', 'GOAL': True},\n", + " {'Species': 'Cat', 'Rain': 'Yes', 'Coat': 'Yes', 'GOAL': True},\n", + " {'Species': 'Dog', 'Rain': 'Yes', 'Coat': 'Yes', 'GOAL': True},\n", + " {'Species': 'Dog', 'Rain': 'Yes', 'Coat': 'No', 'GOAL': False},\n", + " {'Species': 'Dog', 'Rain': 'No', 'Coat': 'No', 'GOAL': False},\n", + " {'Species': 'Cat', 'Rain': 'No', 'Coat': 'No', 'GOAL': False},\n", + " {'Species': 'Cat', 'Rain': 'No', 'Coat': 'Yes', 'GOAL': True}\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let our initial hypothesis be `[{'Species': 'Cat'}]`. That means every cat will be taking an umbrella. We can see that this is not true, but it doesn't matter since we will refine the hypothesis using the Current-Best algorithm. First, let's see how that initial hypothesis fares to have a point of reference." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n", + "False\n", + "False\n", + "False\n", + "True\n", + "True\n" + ] + } + ], + "source": [ + "initial_h = [{'Species': 'Cat'}]\n", + "\n", + "for e in animals_umbrellas:\n", + " print(guess_value(e, initial_h))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We got 5/7 correct. Not terribly bad, but we can do better. Lets now run the algorithm and see how that performs in comparison to our current result. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n", + "True\n", + "False\n", + "False\n", + "False\n", + "True\n" + ] + } + ], + "source": [ + "h = current_best_learning(animals_umbrellas, initial_h)\n", + "\n", + "for e in animals_umbrellas:\n", + " print(guess_value(e, h))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We got everything right! Let's print our hypothesis:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'Species': 'Cat', 'Rain': '!No'}, {'Species': 'Dog', 'Coat': 'Yes'}, {'Coat': 'Yes'}]\n" + ] + } + ], + "source": [ + "print(h)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If an example meets any of the disjunctions in the list, it will be `True`, otherwise it will be `False`.\n", + "\n", + "Let's move on to a bigger example, the \"Restaurant\" example from the book. The attributes for each example are the following:\n", + "\n", + "* Alternative option (`Alt`)\n", + "* Bar to hang out/wait (`Bar`)\n", + "* Day is Friday (`Fri`)\n", + "* Is hungry (`Hun`)\n", + "* How much does it cost (`Price`, takes values in [$, $$, $$$])\n", + "* How many patrons are there (`Pat`, takes values in [None, Some, Full])\n", + "* Is raining (`Rain`)\n", + "* Has made reservation (`Res`)\n", + "* Type of restaurant (`Type`, takes values in [French, Thai, Burger, Italian])\n", + "* Estimated waiting time (`Est`, takes values in [0-10, 10-30, 30-60, >60])\n", + "\n", + "We want to predict if someone will wait or not (Goal = WillWait). Below we show twelve examples found in the book." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![restaurant](images/restaurant.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With the function `r_example` we will build the dictionary examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def r_example(Alt, Bar, Fri, Hun, Pat, Price, Rain, Res, Type, Est, GOAL):\n", + " return {'Alt': Alt, 'Bar': Bar, 'Fri': Fri, 'Hun': Hun, 'Pat': Pat,\n", + " 'Price': Price, 'Rain': Rain, 'Res': Res, 'Type': Type, 'Est': Est,\n", + " 'GOAL': GOAL}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "In code:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "restaurant = [\n", + " r_example('Yes', 'No', 'No', 'Yes', 'Some', '$$$', 'No', 'Yes', 'French', '0-10', True),\n", + " r_example('Yes', 'No', 'No', 'Yes', 'Full', '$', 'No', 'No', 'Thai', '30-60', False),\n", + " r_example('No', 'Yes', 'No', 'No', 'Some', '$', 'No', 'No', 'Burger', '0-10', True),\n", + " r_example('Yes', 'No', 'Yes', 'Yes', 'Full', '$', 'Yes', 'No', 'Thai', '10-30', True),\n", + " r_example('Yes', 'No', 'Yes', 'No', 'Full', '$$$', 'No', 'Yes', 'French', '>60', False),\n", + " r_example('No', 'Yes', 'No', 'Yes', 'Some', '$$', 'Yes', 'Yes', 'Italian', '0-10', True),\n", + " r_example('No', 'Yes', 'No', 'No', 'None', '$', 'Yes', 'No', 'Burger', '0-10', False),\n", + " r_example('No', 'No', 'No', 'Yes', 'Some', '$$', 'Yes', 'Yes', 'Thai', '0-10', True),\n", + " r_example('No', 'Yes', 'Yes', 'No', 'Full', '$', 'Yes', 'No', 'Burger', '>60', False),\n", + " r_example('Yes', 'Yes', 'Yes', 'Yes', 'Full', '$$$', 'No', 'Yes', 'Italian', '10-30', False),\n", + " r_example('No', 'No', 'No', 'No', 'None', '$', 'No', 'No', 'Thai', '0-10', False),\n", + " r_example('Yes', 'Yes', 'Yes', 'Yes', 'Full', '$', 'No', 'No', 'Burger', '30-60', True)\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Say our initial hypothesis is that there should be an alternative option and lets run the algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "False\n", + "True\n", + "True\n", + "False\n", + "True\n", + "False\n", + "True\n", + "False\n", + "False\n", + "False\n", + "True\n" + ] + } + ], + "source": [ + "initial_h = [{'Alt': 'Yes'}]\n", + "h = current_best_learning(restaurant, initial_h)\n", + "for e in restaurant:\n", + " print(guess_value(e, h))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The predictions are correct. Let's see the hypothesis that accomplished that:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'Alt': 'Yes', 'Type': '!Thai', 'Hun': '!No', 'Bar': '!Yes'}, {'Alt': 'No', 'Fri': 'No', 'Pat': 'Some', 'Price': '$', 'Type': 'Burger', 'Est': '0-10'}, {'Rain': 'Yes', 'Res': 'No', 'Type': '!Burger'}, {'Alt': 'No', 'Bar': 'Yes', 'Hun': 'Yes', 'Pat': 'Some', 'Price': '$$', 'Rain': 'Yes', 'Res': 'Yes', 'Est': '0-10'}, {'Alt': 'No', 'Bar': 'No', 'Pat': 'Some', 'Price': '$$', 'Est': '0-10'}, {'Alt': 'Yes', 'Hun': 'Yes', 'Pat': 'Full', 'Price': '$', 'Res': 'No', 'Type': 'Burger', 'Est': '30-60'}]\n" + ] + } + ], + "source": [ + "print(h)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It might be quite complicated, with many disjunctions if we are unlucky, but it will always be correct, as long as a correct hypothesis exists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/knowledge_version_space.ipynb b/knowledge_version_space.ipynb new file mode 100644 index 000000000..8c8ec29f5 --- /dev/null +++ b/knowledge_version_space.ipynb @@ -0,0 +1,1088 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# KNOWLEDGE\n", + "\n", + "The [knowledge](https://github.com/aimacode/aima-python/blob/master/knowledge.py) module covers **Chapter 19: Knowledge in Learning** from Stuart Russel's and Peter Norvig's book *Artificial Intelligence: A Modern Approach*.\n", + "\n", + "Execute the cell below to get started." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [], + "source": [ + "from knowledge import *\n", + "\n", + "from notebook import pseudocode, psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* Version-Space Learning" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OVERVIEW\n", + "\n", + "Like the [learning module](https://github.com/aimacode/aima-python/blob/master/learning.ipynb), this chapter focuses on methods for generating a model/hypothesis for a domain. Unlike though the learning chapter, here we use prior knowledge to help us learn from new experiences and find a proper hypothesis.\n", + "\n", + "### First-Order Logic\n", + "\n", + "Usually knowledge in this field is represented as **first-order logic**, a type of logic that uses variables and quantifiers in logical sentences. Hypotheses are represented by logical sentences with variables, while examples are logical sentences with set values instead of variables. The goal is to assign a value to a special first-order logic predicate, called **goal predicate**, for new examples given a hypothesis. We learn this hypothesis by infering knowledge from some given examples.\n", + "\n", + "### Representation\n", + "\n", + "In this module, we use dictionaries to represent examples, with keys the attribute names and values the corresponding example values. Examples also have an extra boolean field, 'GOAL', for the goal predicate. A hypothesis is represented as a list of dictionaries. Each dictionary in that list represents a disjunction. Inside these dictionaries/disjunctions we have conjunctions.\n", + "\n", + "For example, say we want to predict if an animal (cat or dog) will take an umbrella given whether or not it rains or the animal wears a coat. The goal value is 'take an umbrella' and is denoted by the key 'GOAL'. An example:\n", + "\n", + "`{'Species': 'Cat', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`\n", + "\n", + "A hypothesis can be the following:\n", + "\n", + "`[{'Species': 'Cat'}]`\n", + "\n", + "which means an animal will take an umbrella if and only if it is a cat.\n", + "\n", + "### Consistency\n", + "\n", + "We say that an example `e` is **consistent** with an hypothesis `h` if the assignment from the hypothesis for `e` is the same as `e['GOAL']`. If the above example and hypothesis are `e` and `h` respectively, then `e` is consistent with `h` since `e['Species'] == 'Cat'`. For `e = {'Species': 'Dog', 'Coat': 'Yes', 'Rain': 'Yes', 'GOAL': True}`, the example is no longer consistent with `h`, since the value assigned to `e` is *False* while `e['GOAL']` is *True*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## VERSION-SPACE LEARNING\n", + "\n", + "### Overview\n", + "\n", + "**Version-Space Learning** is a general method of learning in logic based domains. We generate the set of all the possible hypotheses in the domain and then we iteratively remove hypotheses inconsistent with the examples. The set of remaining hypotheses is called **version space**. Because hypotheses are being removed until we end up with a set of hypotheses consistent with all the examples, the algorithm is sometimes called **candidate elimination** algorithm.\n", + "\n", + "After we update the set on an example, all the hypotheses in the set are consistent with that example. So, when all the examples have been parsed, all the remaining hypotheses in the set are consistent with all the examples. That means we can pick hypotheses at random and we will always get a valid hypothesis." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pseudocode" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ Version-Space-Learning(_examples_) __returns__ a version space \n", + " __local variables__: _V_, the version space: the set of all hypotheses \n", + "\n", + " _V_ ← the set of all hypotheses \n", + " __for each__ example _e_ in _examples_ __do__ \n", + "   __if__ _V_ is not empty __then__ _V_ ← Version-Space-Update(_V_, _e_) \n", + " __return__ _V_ \n", + "\n", + "---\n", + "__function__ Version-Space-Update(_V_, _e_) __returns__ an updated version space \n", + " _V_ ← \\{_h_ ∈ _V_ : _h_ is consistent with _e_\\} \n", + "\n", + "---\n", + "__Figure ??__ The version space learning algorithm. It finds a subset of _V_ that is consistent with all the _examples_." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('Version-Space-Learning')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### Implementation\n", + "\n", + "The set of hypotheses is represented by a list and each hypothesis is represented by a list of dictionaries, each dictionary a disjunction. For each example in the given examples we update the version space with the function `version_space_update`. In the end, we return the version-space.\n", + "\n", + "Before we can start updating the version space, we need to generate it. We do that with the `all_hypotheses` function, which builds a list of all the possible hypotheses (including hypotheses with disjunctions). The function works like this: first it finds the possible values for each attribute (using `values_table`), then it builds all the attribute combinations (and adds them to the hypotheses set) and finally it builds the combinations of all the disjunctions (which in this case are the hypotheses build by the attribute combinations).\n", + "\n", + "You can read the code for all the functions by running the cells below:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def version_space_learning(examples):\n",
    +       "    """ [Figure 19.3]\n",
    +       "    The version space is a list of hypotheses, which in turn are a list\n",
    +       "    of dictionaries/disjunctions."""\n",
    +       "    V = all_hypotheses(examples)\n",
    +       "    for e in examples:\n",
    +       "        if V:\n",
    +       "            V = version_space_update(V, e)\n",
    +       "\n",
    +       "    return V\n",
    +       "\n",
    +       "\n",
    +       "def version_space_update(V, e):\n",
    +       "    return [h for h in V if is_consistent(e, h)]\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(version_space_learning, version_space_update)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def all_hypotheses(examples):\n",
    +       "    """Build a list of all the possible hypotheses"""\n",
    +       "    values = values_table(examples)\n",
    +       "    h_powerset = powerset(values.keys())\n",
    +       "    hypotheses = []\n",
    +       "    for s in h_powerset:\n",
    +       "        hypotheses.extend(build_attr_combinations(s, values))\n",
    +       "\n",
    +       "    hypotheses.extend(build_h_combinations(hypotheses))\n",
    +       "\n",
    +       "    return hypotheses\n",
    +       "\n",
    +       "\n",
    +       "def values_table(examples):\n",
    +       "    """Build a table with all the possible values for each attribute.\n",
    +       "    Returns a dictionary with keys the attribute names and values a list\n",
    +       "    with the possible values for the corresponding attribute."""\n",
    +       "    values = defaultdict(lambda: [])\n",
    +       "    for e in examples:\n",
    +       "        for k, v in e.items():\n",
    +       "            if k == 'GOAL':\n",
    +       "                continue\n",
    +       "\n",
    +       "            mod = '!'\n",
    +       "            if e['GOAL']:\n",
    +       "                mod = ''\n",
    +       "\n",
    +       "            if mod + v not in values[k]:\n",
    +       "                values[k].append(mod + v)\n",
    +       "\n",
    +       "    values = dict(values)\n",
    +       "    return values\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(all_hypotheses, values_table)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def build_attr_combinations(s, values):\n",
    +       "    """Given a set of attributes, builds all the combinations of values.\n",
    +       "    If the set holds more than one attribute, recursively builds the\n",
    +       "    combinations."""\n",
    +       "    if len(s) == 1:\n",
    +       "        # s holds just one attribute, return its list of values\n",
    +       "        k = values[s[0]]\n",
    +       "        h = [[{s[0]: v}] for v in values[s[0]]]\n",
    +       "        return h\n",
    +       "\n",
    +       "    h = []\n",
    +       "    for i, a in enumerate(s):\n",
    +       "        rest = build_attr_combinations(s[i+1:], values)\n",
    +       "        for v in values[a]:\n",
    +       "            o = {a: v}\n",
    +       "            for r in rest:\n",
    +       "                t = o.copy()\n",
    +       "                for d in r:\n",
    +       "                    t.update(d)\n",
    +       "                h.append([t])\n",
    +       "\n",
    +       "    return h\n",
    +       "\n",
    +       "\n",
    +       "def build_h_combinations(hypotheses):\n",
    +       "    """Given a set of hypotheses, builds and returns all the combinations of the\n",
    +       "    hypotheses."""\n",
    +       "    h = []\n",
    +       "    h_powerset = powerset(range(len(hypotheses)))\n",
    +       "\n",
    +       "    for s in h_powerset:\n",
    +       "        t = []\n",
    +       "        for i in s:\n",
    +       "            t.extend(hypotheses[i])\n",
    +       "        h.append(t)\n",
    +       "\n",
    +       "    return h\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(build_attr_combinations, build_h_combinations)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Since the set of all possible hypotheses is enormous and would take a long time to generate, we will come up with another, even smaller domain. We will try and predict whether we will have a party or not given the availability of pizza and soda. Let's do it:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "party = [\n", + " {'Pizza': 'Yes', 'Soda': 'No', 'GOAL': True},\n", + " {'Pizza': 'Yes', 'Soda': 'Yes', 'GOAL': True},\n", + " {'Pizza': 'No', 'Soda': 'No', 'GOAL': False}\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even though it is obvious that no-pizza no-party, we will run the algorithm and see what other hypotheses are valid." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n", + "False\n" + ] + } + ], + "source": [ + "V = version_space_learning(party)\n", + "for e in party:\n", + " guess = False\n", + " for h in V:\n", + " if guess_value(e, h):\n", + " guess = True\n", + " break\n", + "\n", + " print(guess)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results are correct for the given examples. Let's take a look at the version space:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "959\n", + "[{'Pizza': 'Yes'}, {'Soda': 'Yes'}]\n", + "[{'Pizza': 'Yes'}, {'Pizza': '!No', 'Soda': 'No'}]\n", + "True\n" + ] + } + ], + "source": [ + "print(len(V))\n", + "\n", + "print(V[5])\n", + "print(V[10])\n", + "\n", + "print([{'Pizza': 'Yes'}] in V)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are almost 1000 hypotheses in the set. You can see that even with just two attributes the version space in very large.\n", + "\n", + "Our initial prediction is indeed in the set of hypotheses. Also, the two other random hypotheses we got are consistent with the examples (since they both include the \"Pizza is available\" disjunction)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Minimal Consistent Determination" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This algorithm is based on a straightforward attempt to find the simplest determination consistent with the observations. A determinaton P > Q says that if any examples match on P, then they must also match on Q. A determination is therefore consistent with a set of examples if every pair that matches on the predicates on the left-hand side also matches on the goal predicate." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pseudocode" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lets look at the pseudocode for this algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ Minimal-Consistent-Det(_E_, _A_) __returns__ a set of attributes \n", + " __inputs__: _E_, a set of examples \n", + "     _A_, a set of attributes, of size _n_ \n", + "\n", + " __for__ _i_ = 0 __to__ _n_ __do__ \n", + "   __for each__ subset _Ai_ of _A_ of size _i_ __do__ \n", + "     __if__ Consistent-Det?(_Ai_, _E_) __then return__ _Ai_ \n", + "\n", + "---\n", + "__function__ Consistent-Det?(_A_, _E_) __returns__ a truth value \n", + " __inputs__: _A_, a set of attributes \n", + "     _E_, a set of examples \n", + " __local variables__: _H_, a hash table \n", + "\n", + " __for each__ example _e_ __in__ _E_ __do__ \n", + "   __if__ some example in _H_ has the same values as _e_ for the attributes _A_ \n", + "    but a different classification __then return__ _false_ \n", + "   store the class of _e_ in_H_, indexed by the values for attributes _A_ of the example _e_ \n", + " __return__ _true_ \n", + "\n", + "---\n", + "__Figure ??__ An algorithm for finding a minimal consistent determination." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('Minimal-Consistent-Det')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can read the code for the above algorithm by running the cells below:" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def minimal_consistent_det(E, A):\n",
    +       "    """Return a minimal set of attributes which give consistent determination"""\n",
    +       "    n = len(A)\n",
    +       "\n",
    +       "    for i in range(n + 1):\n",
    +       "        for A_i in combinations(A, i):\n",
    +       "            if consistent_det(A_i, E):\n",
    +       "                return set(A_i)\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(minimal_consistent_det)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def consistent_det(A, E):\n",
    +       "    """Check if the attributes(A) is consistent with the examples(E)"""\n",
    +       "    H = {}\n",
    +       "\n",
    +       "    for e in E:\n",
    +       "        attr_values = tuple(e[attr] for attr in A)\n",
    +       "        if attr_values in H and H[attr_values] != e['GOAL']:\n",
    +       "            return False\n",
    +       "        H[attr_values] = e['GOAL']\n",
    +       "\n",
    +       "    return True\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(consistent_det)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We already know that no-pizza-no-party but we will still check it through the `minimal_consistent_det` algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Pizza'}\n" + ] + } + ], + "source": [ + "print(minimal_consistent_det(party, {'Pizza', 'Soda'}))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also check it on some other example. Let's consider the following example :" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "conductance = [\n", + " {'Sample': 'S1', 'Mass': 12, 'Temp': 26, 'Material': 'Cu', 'Size': 3, 'GOAL': 0.59},\n", + " {'Sample': 'S1', 'Mass': 12, 'Temp': 100, 'Material': 'Cu', 'Size': 3, 'GOAL': 0.57},\n", + " {'Sample': 'S2', 'Mass': 24, 'Temp': 26, 'Material': 'Cu', 'Size': 6, 'GOAL': 0.59},\n", + " {'Sample': 'S3', 'Mass': 12, 'Temp': 26, 'Material': 'Pb', 'Size': 2, 'GOAL': 0.05},\n", + " {'Sample': 'S3', 'Mass': 12, 'Temp': 100, 'Material': 'Pb', 'Size': 2, 'GOAL': 0.04},\n", + " {'Sample': 'S4', 'Mass': 18, 'Temp': 100, 'Material': 'Pb', 'Size': 3, 'GOAL': 0.04},\n", + " {'Sample': 'S4', 'Mass': 18, 'Temp': 100, 'Material': 'Pb', 'Size': 3, 'GOAL': 0.04},\n", + " {'Sample': 'S5', 'Mass': 24, 'Temp': 100, 'Material': 'Pb', 'Size': 4, 'GOAL': 0.04},\n", + " {'Sample': 'S6', 'Mass': 36, 'Temp': 26, 'Material': 'Pb', 'Size': 6, 'GOAL': 0.05},\n", + "]\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we check the `minimal_consistent_det` algorithm on the above example:" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Temp', 'Material'}\n" + ] + } + ], + "source": [ + "print(minimal_consistent_det(conductance, {'Mass', 'Temp', 'Material', 'Size'}))" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'Temp', 'Size', 'Mass'}\n" + ] + } + ], + "source": [ + "print(minimal_consistent_det(conductance, {'Mass', 'Temp', 'Size'}))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/learning.ipynb b/learning.ipynb index f6b4460d6..0cadd4e7b 100644 --- a/learning.ipynb +++ b/learning.ipynb @@ -2,56 +2,48 @@ "cells": [ { "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ - "# Learning\n", + "# LEARNING\n", "\n", - "This notebook serves as supporting material for topics covered in **Chapter 18 - Learning from Examples** , **Chapter 19 - Knowledge in Learning**, **Chapter 20 - Learning Probabilistic Models** from the book *Artificial Intelligence: A Modern Approach*. This notebook uses implementations from [learning.py](https://github.com/aimacode/aima-python/blob/master/learning.py). Let's start by importing everything from learning module." + "This notebook serves as supporting material for topics covered in **Chapter 18 - Learning from Examples** , **Chapter 19 - Knowledge in Learning**, **Chapter 20 - Learning Probabilistic Models** from the book *Artificial Intelligence: A Modern Approach*. This notebook uses implementations from [learning.py](https://github.com/aimacode/aima-python/blob/master/learning.py). Let's start by importing everything from the module:" ] }, { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ - "from learning import *" + "from learning import *\n", + "from probabilistic_learning import *\n", + "from notebook import *" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Contents\n", + "## CONTENTS\n", "\n", - "* Review\n", - "* Explanations of learning module\n", - "* Practical Machine Learning Task\n", - " * MNIST handwritten digits classification\n", - " * Loading and Visualising digits data\n", - " * kNN classifier\n", - " * Review\n", - " * Native implementation from Learning module\n", - " * Faster implementation using NumPy\n", - " * Overfitting and how to avoid it\n", - " * Train-Test split\n", - " * Crossvalidation\n", - " * Regularisation\n", - " * Sub-sampling\n", - " * Fine tuning parameters to get better results\n", - " * Introduction to Scikit-Learn\n", - " * Email spam detector" + "* Machine Learning Overview\n", + "* Datasets\n", + "* Iris Visualization\n", + "* Distance Functions\n", + "* Plurality Learner\n", + "* k-Nearest Neighbours\n", + "* Decision Tree Learner\n", + "* Random Forest Learner\n", + "* Naive Bayes Learner\n", + "* Perceptron\n", + "* Learner Evaluation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Review\n", + "## MACHINE LEARNING OVERVIEW\n", "\n", "In this notebook, we learn about agents that can improve their behavior through diligent study of their own experiences.\n", "\n", @@ -61,7 +53,7 @@ "\n", "* **Supervised Learning**:\n", "\n", - "In Supervised Learning the agent observeses some example input-output pairs and learns a function that maps from input to output.\n", + "In Supervised Learning the agent observes some example input-output pairs and learns a function that maps from input to output.\n", "\n", "**Example**: Let's think of an agent to classify images containing cats or dogs. If we provide an image containing a cat or a dog, this agent should output a string \"cat\" or \"dog\" for that particular image. To teach this agent, we will give a lot of input-output pairs like {cat image-\"cat\"}, {dog image-\"dog\"} to the agent. The agent then learns a function that maps from an input image to one of those strings.\n", "\n", @@ -80,832 +72,2163 @@ }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ - "## Explanations of learning module goes here" + "## DATASETS\n", + "\n", + "For the following tutorials we will use a range of datasets, to better showcase the strengths and weaknesses of the algorithms. The datasests are the following:\n", + "\n", + "* [Fisher's Iris](https://github.com/aimacode/aima-data/blob/a21fc108f52ad551344e947b0eb97df82f8d2b2b/iris.csv): Each item represents a flower, with four measurements: the length and the width of the sepals and petals. Each item/flower is categorized into one of three species: Setosa, Versicolor and Virginica.\n", + "\n", + "* [Zoo](https://github.com/aimacode/aima-data/blob/a21fc108f52ad551344e947b0eb97df82f8d2b2b/zoo.csv): The dataset holds different animals and their classification as \"mammal\", \"fish\", etc. The new animal we want to classify has the following measurements: 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1 (don't concern yourself with what the measurements mean)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Practical Machine Learning Task\n", - "\n", - "## MNIST handwritten digits calssification\n", - "\n", - "The MNIST database, available from [this page](http://yann.lecun.com/exdb/mnist/) is a large database of handwritten digits that is commonly used for training & testing/validating in Machine learning.\n", - "\n", - "The dataset has **60,000 training images** each of size 28x28 pixels with labels and **10,000 testing images** of size 28x28 pixels with labels.\n", + "To make using the datasets easier, we have written a class, `DataSet`, in `learning.py`. The tutorials found here make use of this class.\n", "\n", - "In this section, we will use this database to compare performances of these different learning algorithms:\n", - "* kNN (k-Nearest Neighbour) classifier\n", - "* Single-hidden-layer Neural Network classifier\n", - "* SVMs (Support Vector Machines)\n", - "\n", - "It is estimates that humans have an error rate of about **0.2%** on this problem. Let's see how our algorithms perform!" + "Let's have a look at how it works before we get started with the algorithms." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Loading MNIST digits data\n", + "### Intro\n", + "\n", + "A lot of the datasets we will work with are .csv files (although other formats are supported too). We have a collection of sample datasets ready to use [on aima-data](https://github.com/aimacode/aima-data/tree/a21fc108f52ad551344e947b0eb97df82f8d2b2b). Two examples are the datasets mentioned above (*iris.csv* and *zoo.csv*). You can find plenty datasets online, and a good repository of such datasets is [UCI Machine Learning Repository](https://archive.ics.uci.edu/ml/datasets.html).\n", "\n", - "Let's start by loading MNIST data into numpy arrays." + "In such files, each line corresponds to one item/measurement. Each individual value in a line represents a *feature* and usually there is a value denoting the *class* of the item.\n", + "\n", + "You can find the code for the dataset here:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "import os, struct\n", - "import array\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from collections import Counter\n", + "%psource DataSet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Class Attributes\n", + "\n", + "* **examples**: Holds the items of the dataset. Each item is a list of values.\n", + "\n", + "* **attrs**: The indexes of the features (by default in the range of [0,f), where *f* is the number of features). For example, `item[i]` returns the feature at index *i* of *item*.\n", + "\n", + "* **attrnames**: An optional list with attribute names. For example, `item[s]`, where *s* is a feature name, returns the feature of name *s* in *item*.\n", + "\n", + "* **target**: The attribute a learning algorithm will try to predict. By default the last attribute.\n", "\n", - "%matplotlib inline\n", - "plt.rcParams['figure.figsize'] = (10.0, 8.0)\n", - "plt.rcParams['image.interpolation'] = 'nearest'\n", - "plt.rcParams['image.cmap'] = 'gray'" + "* **inputs**: This is the list of attributes without the target.\n", + "\n", + "* **values**: A list of lists which holds the set of possible values for the corresponding attribute/feature. If initially `None`, it gets computed (by the function `setproblem`) from the examples.\n", + "\n", + "* **distance**: The distance function used in the learner to calculate the distance between two items. By default `mean_boolean_error`.\n", + "\n", + "* **name**: Name of the dataset.\n", + "\n", + "* **source**: The source of the dataset (url or other). Not used in the code.\n", + "\n", + "* **exclude**: A list of indexes to exclude from `inputs`. The list can include either attribute indexes (attrs) or names (attrnames)." ] }, { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true - }, - "outputs": [], + "cell_type": "markdown", + "metadata": {}, "source": [ - "def load_MNIST(path=\"aima-data/MNIST\"):\n", - " \"helper function to load MNIST data\"\n", - " train_img_file = open(os.path.join(path, \"train-images-idx3-ubyte\"), \"rb\")\n", - " train_lbl_file = open(os.path.join(path, \"train-labels-idx1-ubyte\"), \"rb\")\n", - " test_img_file = open(os.path.join(path, \"t10k-images-idx3-ubyte\"), \"rb\")\n", - " test_lbl_file = open(os.path.join(path, 't10k-labels-idx1-ubyte'), \"rb\")\n", - " \n", - " magic_nr, tr_size, tr_rows, tr_cols = struct.unpack(\">IIII\", train_img_file.read(16))\n", - " tr_img = array.array(\"B\", train_img_file.read())\n", - " train_img_file.close() \n", - " magic_nr, tr_size = struct.unpack(\">II\", train_lbl_file.read(8))\n", - " tr_lbl = array.array(\"b\", train_lbl_file.read())\n", - " train_lbl_file.close()\n", - " \n", - " magic_nr, te_size, te_rows, te_cols = struct.unpack(\">IIII\", test_img_file.read(16))\n", - " te_img = array.array(\"B\", test_img_file.read())\n", - " test_img_file.close()\n", - " magic_nr, te_size = struct.unpack(\">II\", test_lbl_file.read(8))\n", - " te_lbl = array.array(\"b\", test_lbl_file.read())\n", - " test_lbl_file.close()\n", + "### Class Helper Functions\n", + "\n", + "These functions help modify a `DataSet` object to your needs.\n", + "\n", + "* **sanitize**: Takes as input an example and returns it with non-input (target) attributes replaced by `None`. Useful for testing. Keep in mind that the example given is not itself sanitized, but instead a sanitized copy is returned.\n", + "\n", + "* **classes_to_numbers**: Maps the class names of a dataset to numbers. If the class names are not given, they are computed from the dataset values. Useful for classifiers that return a numerical value instead of a string.\n", "\n", - "# print(len(tr_img), len(tr_lbl), tr_size)\n", - "# print(len(te_img), len(te_lbl), te_size)\n", - " \n", - " train_img = np.zeros((tr_size, tr_rows*tr_cols), dtype=np.int16)\n", - " train_lbl = np.zeros((tr_size,), dtype=np.int8)\n", - " for i in range(tr_size):\n", - " train_img[i] = np.array(tr_img[i*tr_rows*tr_cols : (i+1)*tr_rows*tr_cols]).reshape((tr_rows*te_cols))\n", - " train_lbl[i] = tr_lbl[i]\n", - " \n", - " test_img = np.zeros((te_size, te_rows*te_cols), dtype=np.int16)\n", - " test_lbl = np.zeros((te_size,), dtype=np.int8)\n", - " for i in range(te_size):\n", - " test_img[i] = np.array(te_img[i*te_rows*te_cols : (i+1)*te_rows*te_cols]).reshape((te_rows*te_cols))\n", - " test_lbl[i] = te_lbl[i]\n", - " \n", - " return(train_img, train_lbl, test_img, test_lbl)" + "* **remove_examples**: Removes examples containing a given value. Useful for removing examples with missing values, or for removing classes (needed for binary classifiers)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The function `load_MNIST()` loads MNIST data from files saved in `aima-data/MNIST`. It returns four numpy arrays that we are gonna use to train & classify hand-written digits in various learning approaches." + "### Importing a Dataset\n", + "\n", + "#### Importing from aima-data\n", + "\n", + "Datasets uploaded on aima-data can be imported with the following line:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "train_img, train_lbl, test_img, test_lbl = load_MNIST()" + "iris = DataSet(name=\"iris\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Check the shape of these NumPy arrays to make sure we have loaded the database correctly.\n", - "\n", - "Each 28x28 pixel image is flattened to 784x1 array and we should have 60,000 of them in training data. Similarly we should have 10,000 of those 784x1 arrays in testing data. " + "To check that we imported the correct dataset, we can do the following:" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, + "execution_count": 4, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Training images size: (60000, 784)\n", - "Training labels size: (60000,)\n", - "Testing images size: (10000, 784)\n", - "Training labels size: (10000,)\n" + "[5.1, 3.5, 1.4, 0.2, 'setosa']\n", + "[0, 1, 2, 3]\n" ] } ], "source": [ - "print(\"Training images size:\", train_img.shape)\n", - "print(\"Training labels size:\", train_lbl.shape)\n", - "print(\"Testing images size:\", test_img.shape)\n", - "print(\"Training labels size:\", test_lbl.shape)" + "print(iris.examples[0])\n", + "print(iris.inputs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Visualizing MNIST digits data\n", - "\n", - "To get a better understanding of the dataset, let's visualize some random images for each class from training & testing datasets." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "classes = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"]\n", - "num_classes = len(classes)\n", - "\n", - "def show_MNIST(dataset, samples=8):\n", - " if dataset == \"training\":\n", - " labels = train_lbl\n", - " images = train_img\n", - " elif dataset == \"testing\":\n", - " labels = test_lbl\n", - " images = test_img\n", - " else:\n", - " raise ValueError(\"dataset must be 'testing' or 'training'!\")\n", - " \n", - " for y, cls in enumerate(classes):\n", - " idxs = np.nonzero([i == y for i in labels])\n", - " idxs = np.random.choice(idxs[0], samples, replace=False)\n", - " for i , idx in enumerate(idxs):\n", - " plt_idx = i * num_classes + y + 1\n", - " plt.subplot(samples, num_classes, plt_idx)\n", - " plt.imshow(images[idx].reshape((28, 28)))\n", - " plt.axis(\"off\")\n", - " if i == 0:\n", - " plt.title(cls)\n", - "\n", - "\n", - " plt.show()" + "Which correctly prints the first line in the csv file and the list of attribute indexes." ] }, { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "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\nHDBgAOvXrwdg5cqVUf1N9erVAVdJFcVRxgn418caNWrw22+/AbBnzx4AihcvzhlnnJHpfRdffDEA\nxYoV47LLLgNg//79AJx11ll89dVXuX5WvMfprbfeyr333gvAoUOHAPfa+MEHH+Tn0FGj52Ji+1im\nTBkAPvroo0zPN2nShJ07d+bpmEHrYzzIrY9JEbY76aSTAOjbty/gXJzT0tIAzM21RYsW5udIN+PX\nXnsNwAyWrl278vnnn8e34XFAToShQ4dmev7AgQNmYrh58+aEtysn6tevz4gRIwDo0qVL1H+3ceNG\natSokek5Ce01aNDA0+QpXqSnpzNs2DAAevXqBUCFChUAJ7QaHnYEeP311wH3Zrtv375ENDVuSJj1\n0ksvBeDkk09mxowZQPSTp2nTpgHQsmVLAG688cZYNzNqrr32WgBzjbnuuus4ePAg4E44ChUqRJUq\nVQB3gvTnn38CsHjxYnOMxYsXA7Bly5YEtT5n2rRpY36WtiVq0qT4w3PPPQdgrqU7duwAoHDhwr61\nySty37vpppu44447AFi2bBkAW7du5aWXXgLg7bffBmDv3r1xb5OG7RRFURRFUTyQFMrToEGDAGcF\nmFdKlCgBOHI7OApGsilPRx11lAljhCsyd9xxB4888ogfzcqWq666CoDx48eb/3skpGR63LhxgLsi\nLlGiBN9++22Of+M3S5cuzfJdiPKZkZERUQU977zzMj3KqimZKFeuHACjRo0yIfTDhw8D8Pjjj3Pn\nnXfmeoyiRYsC0KdPH8466ywAXn75ZQAmT54c8zZHS8eOHQE4//zzs7wm6u4333zDq6++Crgr+WjD\n5H5QpEgRwE0SB3jwwQf9ak5gEAVfzlP5/4wYMYL3338fgAsvvBCAf/75x4cWZuWYY44B4JprrgHc\n64hlWcycOROAp556yrxflHDhl19+AVzFNBno3bs3ALfffrv5rlq3bg04/RbVW1T9Sy65BHDD7PFA\nlSdFURRFURQPBDZhvGDBggwcOBBwk6JzamtocrHkNUmcVF4PPcaHH35IixYtcm1HkBLjSpYsmSXB\n74cffgAcg7s//vgjT8eNVwLnK6+8Ajgrt/DcH1GXFi5cyJQpUyL+fXp6Oj///HOm52QlEakoICdi\n3UdZ2cyaNcuMqXfffReABx54wLRVVomyEqxYsaL5nurWrQu4ykV+SNQ4PeWUUwA3l6lUqVJs374d\n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\n+vXrA/DII48AUL58eaNUffXVV6FrfBLIPWPSpEk8+OCDPu/Ls7906dIAjBgxAoBnn302aG1S5UlR\nFEVRFMUGEaU8SX5T3759TVKxkFjOT/r06c3M1ROZlcqstW/fvoFqalCoXr06Q4YM8XrtwoULgDun\nwmlIInDNmjV93hMFrVatWo7O10oOstp96623kjx29+7dzJ49G7By8qZOnQrgN19DttVwCqISitqy\nbds2/vjjD1ufIatCz8/44osvAtfIEFC5cmXGjRsHYPItbrrpJsA97hOO6ffff5933303tI30Q65c\nucLdhIBw8eJFkwgt469jx47kzp0bgP379wMwb948AK+8vIsXLwJw5coVY5ERDcyZM8dYwAgzZsww\n9yd/95dPPvkEgLJlywJw55132r6eg4nkbomqLecXvPN95XqTZHgZG2PHjk1xTmZSRMTk6c477wRg\nxYoVgDsBOSFffPGFzw3r559/BtyDSpCbf5s2bcibNy8AvXr1Apw/eapZs6ZPuE4evMeOHQtHk5LF\nnj17fC5qYeXKlSbM+tprrwHWxKpw4cKmvzJJdCI7d+4E4L///gMgS5Ys5j2RviWk54lU3klYLk2a\nNOaG4DSk+m/w4MGAexII7omjnZtT8eLFKV++PGBNrteuXRu0G1wgKFy4sElAfeGFFwCMf5c/4uLi\nzFiWaian0KFDByZOnBjuZgQEqShs164d4A7pNG7cGMD87S8lY8eOHYB70pWwIEXG9ccffxxxyeel\nSpUy/xa/uO7duyfajylTpgDWM/aXX365oQ9dKLn11lsBK/zoOWmSZ8GECRMAd8j/77//BqxisY4d\nOwLuxXmw7i0atlMURVEURbGBo5Wnu+++G4APPvgA8K84ifIiJcFJISvBnDlzGhXK6Yj3z9NPP+3z\n3ueffx7q5iQbUQKnTZvGN9984/cYz/aLnC40a9aMfPnyAfDbb78FqZWpR9QlSbrs1KkT4E6q3rRp\nE2CV1ZYvX964rQ8fPhyw7Ani4+MdGcIsV66csV+4+eabAejXrx8AP/30k63Pql+/PpUrVwas8SGK\nslMQ5bNNmzaAOwRwyy23AJZa5nmeRDmVpHdJyFVCg3jebdiwwaQvyH2jS5cu5jiJNBQoUADwVjPk\nNQn7bNq0ib179wa55YFBFCfPQhW5316+fNnneOnj+PHjfdJfRo8e7QjladSoUQA89NBDXq+PHDnS\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "cell_type": "markdown", + "metadata": {}, "source": [ - "# takes 5-10 secs. to execute the cell\n", - "show_MNIST(\"training\")" + "When importing a dataset, we can specify to exclude an attribute (for example, at index 1) by setting the parameter `exclude` to the attribute index or name." ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, + "execution_count": 5, + "metadata": {}, "outputs": [ { - "data": { - "image/png": "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\n2ZYtWzjzzDNzsvuwULddzsZYrVo1wD0/5T4SyNdffw04yQmzZ89O89mpp54KwPz58817L774IuCE\nSTz33HMAbN26NUMb1G2nypOiKIqiKEpE+DrmqXHjxgD06dMnrO3vuusuANauXQvA448/HhvD4oTE\ny4h6AYkxpk2bNgFO0T2ASpUqmUD3cJDg6tTUVO6++27AjScSKleubOKIMlpJe01geYX33nsPgG3b\ntplYhcsuuwyAOXPmxN227CKxEU899RQAn376qVF4A5G4t0QkfXB4uEgJgg8++CAslckL6tSpk+Fn\ncq0lClJq4dprrwWgQ4cOQbE+U6ZMyfD7jRs3pnr16iG3Gzt2LBs3boy6zTmlWrVq5l5StmxZwB3r\njBkzGDFiBODcZwATBxyIxAIHInFTlmXxxRdfAGTbWxNNJF5W7pVz587lzz//BDCK2uHDh41ytmXL\nFsBRxGONrydPCxcuBNyA8awoXLgwAK+88grgZDtB5JlZfmXnzp388MMPXpuRJZIhl9mNKxw2btzo\nyxvYf5FatWoBMHXqVABKly4NwMyZM8P6vgSpJkLF6nCyIVesWMEHH3wA4NuJEmCyPQcMGADAHXfc\nEbTNypUrAaeuWiIh7v0LLrgASHtuyb+lhpVt20ETK8uyQm4HziSzXr16sR5CxPz5559Bi2px0d13\n331s3749y33IojMw2/PgwYMATJ8+3ReTpqpVqwLuMb7hhhvMZ+J27Nu3r3lP6gief/75AKxbty7m\nNqrbTlEURVEUJQJ8qzy1bt3aKEnp+y2Bm7IuFCxYMOjfkoqbLMrTDz/88J+q+HvppZea+k6JTqVK\nlQBHwRG3pigXfid//vzMmDEDcBUnCQQXN0F6JEBeEDUnnOB+rwhHQZIyA+Ki8ztnn302AMOHD89w\nm+effx6APXv2xMWmaCHlFJ555hmAHKnU8vcpVapUzg2LIfPnzzcB4nItiZKUleokLncJCLdt2xzz\njh07Am6Sj5ekpqYa12RGCUMZceONNwKuCzqWSrcqT4qiKIqiKBHgO+WpRIkSALz00ksZxkns27fP\nBAtKsUwpUxDI7bffDsCiRYuSQn0aN26c1ybEBekLds8995iYjfRs3LjRxLb5mdNPPx2Ar776CoCi\nRYsycOBAgITp8ZaammpinoS33noLCN1zsHDhwkEFGMOJxUgEEkVxCgcpATNr1iyPLckeonp+9NFH\n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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 2, 3]\n" + ] } ], "source": [ - "# takes 5-10 secs. to execute the cell\n", - "show_MNIST(\"testing\")" + "iris2 = DataSet(name=\"iris\",exclude=[1])\n", + "print(iris2.inputs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's have a look at average of all the images of training and testing data." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "classes = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"]\n", - "num_classes = len(classes)\n", + "### Attributes\n", "\n", - "def show_ave_MNIST(dataset):\n", - " if dataset == \"training\":\n", - " print(\"Average of all images in training dataset.\")\n", - " labels = train_lbl\n", - " images = train_img\n", - " elif dataset == \"testing\":\n", - " print(\"Average of all images in testing dataset.\")\n", - " labels = test_lbl\n", - " images = test_img\n", - " else:\n", - " raise ValueError(\"dataset must be 'testing' or 'training'!\")\n", - " \n", - " for y, cls in enumerate(classes):\n", - " idxs = np.nonzero([i == y for i in labels])\n", - " print(\"Digit\", y, \":\", len(idxs[0]), \"images.\")\n", - " \n", - " ave_img = np.mean(np.vstack([images[i] for i in idxs[0]]), axis = 0)\n", - "# print(ave_img.shape)\n", - " \n", - " plt.subplot(1, num_classes, y+1)\n", - " plt.imshow(ave_img.reshape((28, 28)))\n", - " plt.axis(\"off\")\n", - " plt.title(cls)\n", + "Here we showcase the attributes.\n", "\n", - "\n", - " plt.show()" + "First we will print the first three items/examples in the dataset." ] }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, + "execution_count": 6, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Average of all images in training dataset.\n", - "Digit 0 : 5923 images.\n", - "Digit 1 : 6742 images.\n", - "Digit 2 : 5958 images.\n", - "Digit 3 : 6131 images.\n", - "Digit 4 : 5842 images.\n", - "Digit 5 : 5421 images.\n", - "Digit 6 : 5918 images.\n", - "Digit 7 : 6265 images.\n", - "Digit 8 : 5851 images.\n", - "Digit 9 : 5949 images.\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Average of all images in testing dataset.\n", - "Digit 0 : 980 images.\n", - "Digit 1 : 1135 images.\n", - "Digit 2 : 1032 images.\n", - "Digit 3 : 1010 images.\n", - "Digit 4 : 982 images.\n", - "Digit 5 : 892 images.\n", - "Digit 6 : 958 images.\n", - "Digit 7 : 1028 images.\n", - "Digit 8 : 974 images.\n", - "Digit 9 : 1009 images.\n" + "[[5.1, 3.5, 1.4, 0.2, 'setosa'], [4.9, 3.0, 1.4, 0.2, 'setosa'], [4.7, 3.2, 1.3, 0.2, 'setosa']]\n" ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ - "show_ave_MNIST(\"training\")\n", - "show_ave_MNIST(\"testing\")" + "print(iris.examples[:3])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## k-Nearest Neighbours (kNN) classifier\n", - "\n", - "### Review\n", - "k-Nearest Neighbors algorithm is a non-parametric method used for classification and regression. We are gonna use this to classify MNIST handwritten digits. More about kNN on [Scholarpedia](http://www.scholarpedia.org/article/K-nearest_neighbor).\n", - "\n", - "![kNN plot](images/knn_plot.png)" + "Then we will print `attrs`, `attrnames`, `target`, `input`. Notice how `attrs` holds values in [0,4], but since the fourth attribute is the target, `inputs` holds values in [0,3]." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 7, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "attrs: [0, 1, 2, 3, 4]\n", + "attrnames (by default same as attrs): [0, 1, 2, 3, 4]\n", + "target: 4\n", + "inputs: [0, 1, 2, 3]\n" + ] + } + ], "source": [ - "Let's see how kNN works with a simple plot shown in the above picture. There are two classes named **Class A** yellow color dots and **Class B** violet color dots. Every point in this plot has two **features** i.e. (X2, X1) values of that particular point which we used to plot. Now, let's say we have a new point, a red star and we want to know which class this red star belongs. Solving this problem by predicting the class of this new red star is out current classification problem.\n", - "\n", - "We have co-ordinates (we call them **features** in ML) of this red star and we need to predict its class using kNN algorithm. In this algorithm, the value of **k** is arbitary. **k** is one of the **hyper parameters** for kNN algorithm. We choose this number based on our dataset and choosing a particular number is known as **hyper parameter tuning/optimising**. We learn more about this in coming topics.\n", - "\n", - "Let's put **k = 3**. It means you need to find 3-Nearest Neighbors of this red star and classify this new point into majority class. Observe that smaller circle which containg 3 points other that **test point** (red star). As there are two violet points, which is majority, we predict the class of red star as **violet- Class B**.\n", - "\n", - "Similarly if we put **k = 5**, you can observe that there are 4 yellow points, which is majority. So, we classify our test point as **yellow- Class A**.\n", - "\n", - "In practical tasks, we iterate through a bunch of values for k (like [1, 2, 5, 10, 20, 50, 100]) and see how it performs and select the best one. " + "print(\"attrs:\", iris.attrs)\n", + "print(\"attrnames (by default same as attrs):\", iris.attrnames)\n", + "print(\"target:\", iris.target)\n", + "print(\"inputs:\", iris.inputs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Native implementations from Learning module\n", - "\n", - "Let's classify MNIST data in this method. Similar to these points, our images in MNIST data also have **features**. These points have two features as (2, 3) which represents co-ordinates of the point in 2-dimentional plane. Our images have 28x28 pixel values and we treat them as **features** for this particular task. \n", - "\n", - "Next couple of cells help you understand some useful definitions from learning module." + "Now we will print all the possible values for the first feature/attribute." ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [], + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[4.7, 5.5, 6.3, 5.0, 4.9, 5.1, 4.6, 5.4, 4.4, 4.8, 5.8, 7.0, 7.1, 4.5, 5.9, 5.6, 6.9, 6.6, 6.5, 6.4, 6.0, 6.1, 7.6, 7.4, 7.9, 4.3, 5.7, 5.3, 5.2, 6.7, 6.2, 6.8, 7.3, 7.2, 7.7]\n" + ] + } + ], "source": [ - "%psource DataSet" + "print(iris.values[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "class DataSet explanation goes here" + "Finally we will print the dataset's name and source. Keep in mind that we have not set a source for the dataset, so in this case it is empty." ] }, { "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%psource NearestNeighborLearner" - ] - }, - { - "cell_type": "markdown", + "execution_count": 9, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "name: iris\n", + "source: \n" + ] + } + ], "source": [ - "Nearest NeighborLearner explanation goes here" + "print(\"name:\", iris.name)\n", + "print(\"source:\", iris.source)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, let us convert this raw data into `Dataset.examples` to run our `NearestNeighborLearner(dataset, k=1)` defined in `learning.py`. Every image is represented by 784 numbers (28x28 pixels) and we append them with its label or class to make them work with our implementations in learning module." + "A useful combination of the above is `dataset.values[dataset.target]` which returns the possible values of the target. For classification problems, this will return all the possible classes. Let's try it:" ] }, { "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, + "execution_count": 10, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "(60000, 784) (60000,)\n", - "(60000, 785)\n" + "['setosa', 'virginica', 'versicolor']\n" ] } ], "source": [ - "print(train_img.shape, train_lbl.shape)\n", - "temp_train_lbl = train_lbl.reshape((60000,1))\n", - "training_examples = np.hstack((train_img, temp_train_lbl))\n", - "print(training_examples.shape)" + "print(iris.values[iris.target])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, we will initialize DataSet with our training examples. Call NearestNeighbor Learner on this dataset. Predict the class of a test image." + "### Helper Functions" ] }, { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [], + "cell_type": "markdown", + "metadata": {}, "source": [ - "# takes ~8 Secs. to execute this cell\n", - "MNIST_DataSet = DataSet(examples=training_examples, distance=manhattan_distance)" + "We will now take a look at the auxiliary functions found in the class.\n", + "\n", + "First we will take a look at the `sanitize` function, which sets the non-input values of the given example to `None`.\n", + "\n", + "In this case we want to hide the class of the first example, so we will sanitize it.\n", + "\n", + "Note that the function doesn't actually change the given example; it returns a sanitized *copy* of it." ] }, { "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sanitized: [5.1, 3.5, 1.4, 0.2, None]\n", + "Original: [5.1, 3.5, 1.4, 0.2, 'setosa']\n" + ] + } + ], "source": [ - "kNN_Learner = NearestNeighborLearner(MNIST_DataSet)" + "print(\"Sanitized:\",iris.sanitize(iris.examples[0]))\n", + "print(\"Original:\",iris.examples[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Choose a number from 0 to 9999 for `test_img_choice` and we are going to predict the class of that test image." + "Currently the `iris` dataset has three classes, setosa, virginica and versicolor. We want though to convert it to a binary class dataset (a dataset with two classes). The class we want to remove is \"virginica\". To accomplish that we will utilize the helper function `remove_examples`." ] }, { "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, + "execution_count": 12, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Predicted class of test image: 2\n" + "['setosa', 'versicolor']\n" ] } ], "source": [ - "# takes ~20 Secs. to execute this cell\n", - "test_img_choice = 2311\n", - "predicted_class = kNN_Learner(test_img[test_img_choice])\n", - "print(\"Predicted class of test image:\", predicted_class)" + "iris2 = DataSet(name=\"iris\")\n", + "\n", + "iris2.remove_examples(\"virginica\")\n", + "print(iris2.values[iris2.target])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To make sure that the output we got is correct, let's plot that image along with its label." + "We also have `classes_to_numbers`. For a lot of the classifiers in the module (like the Neural Network), classes should have numerical values. With this function we map string class names to numbers." ] }, { "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, + "execution_count": 13, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Actual class of test image: 2\n" + "Class of first example: setosa\n", + "Class of first example: 0\n" ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\niXsj88QAAAAASUVORK5CYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ - "print(\"Actual class of test image:\", test_lbl[test_img_choice])\n", - "plt.imshow(test_img[test_img_choice].reshape((28,28)))" + "print(\"Class of first example:\",iris2.examples[0][iris2.target])\n", + "iris2.classes_to_numbers()\n", + "print(\"Class of first example:\",iris2.examples[0][iris2.target])" ] }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ - "Hurray! We've got it correct. Don't worry if our algorithm predicted a wrong class. With this techinique we have only ~97% accuracy on this dataset. Let's try with a different test image and hope we get it this time.\n", - "\n", - "You might have recognized that our algorithm took ~20 seconds to predict a single image. How would we even predict all 10,000 test images? Yeah, the implementations we have in our learning module are not optimized to run on this particular dataset. We will have an optimised version below in NumPy which is nearly ~50-100 times faster than our native implementation." + "As you can see \"setosa\" was mapped to 0." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Faster implementation using NumPy\n", - "\n", - "Here we calculate manhattan distance between two images faster than our native implementation. Which in turn make predicting labels for test images far efficient." + "Finally, we take a look at `find_means_and_deviations`. It finds the means and standard deviations of the features for each class." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setosa feature means: [5.006, 3.418, 1.464, 0.244]\n", + "Versicolor mean for first feature: 5.936\n", + "Setosa feature deviations: [0.3524896872134513, 0.38102439795469095, 0.17351115943644546, 0.10720950308167838]\n", + "Virginica deviation for second feature: 0.32249663817263746\n" + ] + } + ], + "source": [ + "means, deviations = iris.find_means_and_deviations()\n", + "\n", + "print(\"Setosa feature means:\", means[\"setosa\"])\n", + "print(\"Versicolor mean for first feature:\", means[\"versicolor\"][0])\n", + "\n", + "print(\"Setosa feature deviations:\", deviations[\"setosa\"])\n", + "print(\"Virginica deviation for second feature:\",deviations[\"virginica\"][1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## IRIS VISUALIZATION\n", + "\n", + "Since we will use the iris dataset extensively in this notebook, below we provide a visualization tool that helps in comprehending the dataset and thus how the algorithms work.\n", + "\n", + "We plot the dataset in a 3D space using `matplotlib` and the function `show_iris` from `notebook.py`. The function takes as input three parameters, *i*, *j* and *k*, which are indicises to the iris features, \"Sepal Length\", \"Sepal Width\", \"Petal Length\" and \"Petal Width\" (0 to 3). By default we show the first three features." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\nnjD6AAAAAElFTkSuQmCC\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgsAAAGMCAYAAABUAuEzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAPYQAAD2EBqD+naQAAIABJREFUeJzsnXlwm+d9578v7puXeFM8RFEHddKSLFmXnXgTj7c5mjpu\nZzsbj51O0mltN2ky3Z2sm93pJDOetE2TpuPNbGe6zm7bNNnUce1cih07tiRbkh1ZhyWSAHiAB0CQ\nIAkS9/Ee+wf7vH4B4saL9+XxfGY0tiAQDwACz/N9f8f3xwiCIIBCoVAoFAolDxq1nwCFQqFQKJSN\nDRULFAqFQqFQCkLFAoVCoVAolIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLFAoVCoVAo\nlIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLFAoVCoVAolIJQsUChUCgUCqUgVCxQKBQK\nhUIpCBULFAqFQqFQCkLFAoVCoVAolIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLFAoVC\noVAolIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLFAoVCoVAolIJQsUChUCgUCqUgVCxQ\nKBQKhUIpCBULFAqFQqFQCkLFAoVCoVAolIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLF\nAoVCoVAolIJQsUChUCgUCqUgVCxQKBQKhUIpCBULFAqFQqFQCkLFAoVCoVAolILo1H4CFMpWRxAE\ncBwHlmWh1Wqh1WrBMAwYhlH7qVEoFEpJULFAodQIqUhIp9NIpVLQaDSiUNDpdNBqtdBoNOJ/qYCg\nUCgbEUYQBEHtJ0GhbCUEQQDP82BZFjzPA4D4d4ZhIAhCxh8iEIhoIH80Go34h0KhUNSEigUKRSbI\n4c+yLCYmJhCPx7F//34wDAOWZcGybM6DP1s8kNtIBCJbQNA0BoVCURqahqBQZIBEDjiOy4gskAO9\n0MGe6+CXigaSxpDeV5rGkEYhqICgUCi1gIoFCqUKyGHOsiyAzMOcpCAqQSoypNEIaQQilUpl/Ay5\nn06ng16vp2kMCoUiG1QsUCgVIC1e5Hk+QyQA6yMJ0hRDNeSLQpA/IyMjMJvN6O7upmkMCoUiG1Qs\nUChlkEsk5Ar/k0JGJZAe/CSSoNOtfbVJOoSmMSgUSjVQsUChlECuDodCh2u1aYhqIc9Lq9Vm3F4s\njZEvCkGhULY3VCxQKAXIJRJKCeErGVkoZ91iaQziByG9L01jUCgUKhYolDxkdziUE6ZXSyxUQqFu\njHxpjHyeEFRAUChbEyoWKJQscomEcjsKNpNYyEWxNAbP8+A4DolEAhMTExgcHBQFhE6nE98zmsag\nULYGVCxQKP8OaYPkOK5g8WIpaDQaUSysrKzA6XQiFovBbrfDZrPBbrfDbrfDaDTKepjWWqRkRyEY\nhkEwGBRfL01jUChbEyoWKNueUjscyoVlWdy8eROBQADd3d3YuXMnYrEYwuEwAoEAYrEYtFpthoCw\n2WywWCwVeyOodQBnP1+axqBQthZULFC2LSScnk6nxcNNjgMrlUrB5/MhHA6jrq4O586dg16vRyqV\nwo4dO8T7cRyHaDSKcDiMSCSC2dlZRCIRAIDVahWjDzabDTabbV1KoNDrUop8a5WaxpBC0xgUysaF\nigXKtqPSDodicByHqakpTExMwGKxwGq14uDBgwCQs41Sq9XC4XDA4XBkPLdYLIZIJIJwOIyFhQVM\nTEwgnU7DYrGsi0IYDIaqnrPS0G4MCmVzQsUCZVtRTYdDPgRBgM/ng9vthsFgwNDQEDiOg9vtzrhf\nKeswDAOr1Qqr1YrW1lbx8VOpFMLhMMLhMEKhELxeLxKJBIxGY4Z44Dhu09k7l9qNQe6TTCaRTqfR\n2NhI0xgUikJQsUDZFpBDJxwO4+rVq3jwwQdlOVQXFxfhdDqRTqexZ88etLe3g2EYBAIB2dIBDMPA\naDTCaDRmpDHS6TQikYgYhVhcXEQkEgHDMAiHwxlRiGrqINQgVxqDvJ/BYBCBQAA2m028XVoHQdMY\nFIr8ULFA2dLk6nAgQ5+qIRwOw+l0YmVlBf39/eju7s442JRondTr9WhoaEBDQ4N4m8vlQjqdRkND\nAyKRCHw+HyKRCHieF2sfpHUQxBZ6MyCdu0HsqoHy0hgkCkHTGBRKeWyenYJCKYN8HQ7SA6aSwyKR\nSMDtdmNubg7d3d04fPhwzroBNR0c9Xo9Ojo6xNsEQUA8HhcLKRcXF+HxeJBKpWCxWDJEhN1u3xR1\nENntm4XqILLTGLQbg0IpHyoWKFuKYh0O5L/lHuQsy2JiYgJTU1Nobm7G2bNnYbFY8t5/I5kyMQwD\ni8UCi8Ui1kEAa7l/ksKIRCKYm5tDPB6HwWBYV0hpNpsLzsFQklLe10J1ENICVwL5jNA0BoWSGyoW\nKFuCUjscSN6e5/mSWhF5nsfMzAzGxsZgs9lw7733oq6urujPbdTZEFJIHURTU5N4G8uyGQLC4/Eg\nGo1Co9FkiAe73Q6r1Vqrl1GQSqNC2YKR/P5pGoNCKQ4VC5RNTzkdDkQsFDtQBUHA/Pw8XC4XGIbB\noUOH0NzcvClmQ1Szrk6nQ319Perr68XbeJ5HNBoVRYTf74fb7QbP8zCbzeA4DjMzM6KQUKIOQm7X\ny2JpjEAggLm5OQwODmaktKQpDJrGoGxlqFigbFoqmeFANvNC46ODwaBozzwwMIDOzs5NMxuiFoeV\nRqMR6xna29sBrB2miUQCgUAA4+PjWF5extTUFFKpFMxm87oohMFgkO25KfG+5hIQiUQCWq1WjGIl\nEgnx32gag7LVoWKBsukgV3sk50w29lJ9DBiGySkWotEoXC4XFhcX0dfXh97e3oqvkjdrZKFUGIaB\n2WxGQ0MDtFotjhw5AgCiH4Q0ChGLxaDX69fNxShUB1GIStMQ1SCNWJWbxiDigaYxKJsZKhYom4Zc\nHQ6VbLrZB3kqlcLY2BhmZ2fR0dGBc+fOwWQyVfVc8wmSWqP2AWQwGNDU1LSuDoLYWofDYUxPTyMa\njYJhmHXtnFartaRaEqVfJ/m85XsuxVwppQKHpjEomxEqFigbHqlIkGOGg0ajEWcTeDweTExMoLGx\nEadPnxaNfqplq0cWykGn06Guri6jMJTneXGoViQSgd/vRyQSAcdxOW2t9Xq9+LNqvL5yoxmldGMU\nS2NIoxAUitpQsUDZsNRqhgMAzM/PY2ZmBkajEffcc0/GlbAcbKTWyY0I6a6QijNSB0FSGCsrK5iZ\nmUEymYTJZBKFQywWA8/ziqYj5Firkm6MYDCIhoYGGI1GmsagqAoVC5QNh3TjlFskBAIBsXp/3759\naGtrq8mmuxlaJzcapA7CbDajublZvD2VSmXYWi8vLyOdTuPy5cvrCiktFktNfp+kZqEWFEpjOJ1O\nDA4OwuFwiIKFpjEoakDFAmVDUYtBTwAQCoXgdDoRCoWg1Wpx6NChjDkLciNt0aQbeHUYDAY0Njai\nsbERADAxMYFEIoHOzk5RQEjHe+fygyh1vHc+lP49Sgtx9Xo9dDpdRhqDpOUINI1BqTVULFA2BCSS\nwHEcgPI6HAoRj8fhdrvh9/vR3d2No0eP4sqVK5sqfF0O2+Vg0Gg0OesgpLbWCwsLGB8fB8uysFqt\n60SEtA6iGGqJPmlEI7tAUnqf7DQGuW8hW+vt8lmhyAMVCxRVIVdJXq8XqVQKXV1dsmxk6XRatGdu\nbW3NsGcmBY61pFTzp1qg9Jobxe5Zo9GI472l900mk6KAWFlZwezsrDjeO7uQ0mQy5Xw9aokFnudL\n8g4p1o0h7cigaQxKJVCxQFGF7DbIcDiMeDyO7u7uqh6X53lMT09jfHwcdrsdJ0+eXGfPrERev9IZ\nFJTilHNwMwwDk8kEk8mUUQdBxnsTEREIBBCLxaDVanPWQRRqnawV5LCvpFaiWDdGvjSGVEDQNAZF\nChULFEXJ1eFANqZqrval9swajaagPbMSkQW1xMJmLnAsh2oPsFzjvTmOE/0gIpEIvF6vWAdBDtHZ\n2VlRSFRbB1EMaXGvHJSSxkilUojH45iYmMCBAwfypjFqVexJ2bhQsUBRhGJtkBqNpuJDLhgMYnR0\nFIlEArt37y5qz6yEYVIusUAOcnqlVh21EkNarRYOhwMOhyNjrVgshvHxccTjcSwuLmJychLpdFoc\n751tay0XUk+RWpIdhRAEAaFQSPxO5kpjZAsIYmtNP9tbFyoWKDWnlA4HjUYjFjeWSiQSgdvtLtue\nuRphUiq5xIISQmGj1A/UGqVeJ8MwYh2E0WjE3r17xStw4kgZCoXg9XrFOohsAZGvDqIY0sibkpA6\niex1pWkMlmWRTqfFf6NpjK0PFQuUmlHOoKdy0hDJZBLj4+OYnZ1FZ2cnzp8/D6PRWPLzUiuyoBRb\nPQ2h1mwIaRifjPeWtt+yLJsxF2NxcVEc751dSGm1WouKALnTEKWSr6iy1DSG9L2iaYytAxULFNkh\nVx4cx4mFYcWuMEqpI5DaMzc1NVVsz6xWZEEJtsNVXC0NkgqtWey91el06+ogyHhvIiJ8Ph8ikQh4\nnl83FyN7vLd0/omS8DxfVj1GJd0YNI2x+aBigSIbuQY9lRqGLCQWBEGA1+uF2+2GyWTCsWPHRIOe\nSlCiwBFQr9hwq0cWAHXSLZUIFOl4b+ljxeNxMQKxtLQEj8cjjveWzsNQ4/DkOK5qMVasG0OaxpDe\nVxAEmEymjDHfVEBsDKhYoFQNKV4kVw9A+YOech3ggiBgcXERTqcTHMfJZs+s1CEuTXcomWPf6qgh\nhsq92i4EwzCwWCywWCxoaWkRb08mkxntnKurq+B5HpcvX16Xxqh0vHcplOLtUAnF0hixWAzvvPMO\nzp49K/47TWNsHKhYoFSMnIOessXC6uoqnE4nwuEw+vv70d3dLdsGoWRkodDfa8VWjyyoXbNQK0gd\nBBlqFgqFcOvWLRw8eFAUEVNTU4hEIuIgrmxbazm+I7USC/mQ7hlarRYGg4GmMTYgVCxQKkLuGQ7k\nAJfaM/f09GBoaKgsW95SUKLAkayz1Q9uYPtEM9SqHaivr0d9fX3G7dFoVBQQfr8fbrcbPM/ntLUu\npUMo17pKI123kjSGtBsj29qaUj1ULFDKopwOh3IfN5VK4dKlS2hra8O5c+dgNptleMbrUaLAEVBO\nlGSvudVRK7KgVgtjNtI6iPb2dvH5JRIJMYWxvLyM6enpdeO9yc8ZDIa876EcNQuVwHFcQZFSajeG\nFJrGkA8qFiglIe1wkIYDq920iT3z2NgYeJ7H6dOnM0xxasFWjyzQaIb8qBVZKMfWmoz3ltZBED8I\nEoWYn59HLBaDXq9fVwdBxnsrnYYgFBML+SilG4OICJrGqBwqFigFydXhIMeXShAE+P1+uFwuaLVa\n7Nu3D3fu3Km5UACUjSyQdViWxfz8PMxmc02tgrfDZqeWANuM0QyDwYCmpiaxDgJYO5SlhZTT09OI\nRqOiARXP89DpdAiFQrKM9y4VOSMahdIYJDoqTWMEg0GYzWY4HA6axsgDFQuUnEhFQqUdDvlYXl6G\n0+lEIpHAwMAAOjs7kUwmxXVr/eXUaDQZ7nO1glylzc7OwuVyQafTIZVKgeM4WK1WMSQs96yBrR5Z\n2OhX+Rt9Ta1Wm3O8dywWE8VDPB7HzZs3wXEcLBbLuiiE3HVEQOWRhVKRFlFKEQQB09PTaG9vh8lk\nyvi37DRGNBqt2evf6FCxQMlA2uEwPDwMi8WCnp4eWTatSCQCl8uFpaUl7Nq1C729veIXl1xRKFFc\npVR6QBAE3L17F4IgYP/+/aJZj7RFTtpjTzZl6Z9yi9OUZqvbPRPUEihKpQNId4XNZkMwGITRaERf\nXx8SiYT4WV1ZWcHMzIxYB5FdSGk0Gqt6j2otFvLBMAw4joNerxe/b/nSGA8//DCeeuopfOYzn1H8\nearNxt6JKIpBvhjSugSO45BOp6veJJPJJMbGxuD1etHV1ZXTnllJsVDr1slYLAan04lkMonOzk4M\nDg5Co9GIGw7JLZORydmzBqSbMjHpkf4pdFWzHTowtksaQq3aAbKutA4ie7y3tA5iYWEB0WgUer0+\n53jvUt83tcQCsJYmlArzfGmMSCSSYbC1naBigZK3w0Gn05U93EkKy7LweDyYnJzEjh07cObMGVit\n1pz3lYqFWlOrAsd0Oo3x8XFMT0+jo6MDFosFbW1t0Gq1BQ+4fLMG8g0rItXt0j9yTjvc6GxVn4Vc\na6opFvKh1+vR2NiY4aKaPd57dnZWHO+dy9Y6lyhQSxwBpQuVcDickb7ZTlCxsI0hkQSWZQFk9isD\nlU2CBNa+9F6vF2NjYzCZTDh+/HiGX34uyJpKiAW5CxxJXYLb7YbD4cB9990Hu92Ot956K+eI6lLJ\nVZwmvaoLhULw+/2IxWIwGAyw2+1gGAbpdFqcgLhVi7O2w8GtRp0EWbfcK/xC473J53VhYQETExPi\neG9pvY7dblc1slCOWKCRBcq2odQOB61Wu65vudjjBgIBuFwu8DyP/fv3o7W1taQNj/ROKyUW5Fon\nEAjA6XSC53kcOnQIzc3NGf3gcofMc13VsSybERKOx+N4++23xfY46Z9KxyVvJGgaorbI1ZVAuiuk\n0URBEDJqdlZXVzE7O4tEIiG6N05MTIgiQonPK8/zYgdIIQRBoJEFyvag3EFPWq225MiC1J559+7d\n2LlzZ9kbzmYa8BSJROB0OhEMBrF79+6cdtRK1Q/odDrR5U+v18Pr9eLo0aPihhwOh+HxeBCNRqHV\natcJiFrOGagVW6UzoRAbNQ1RDQzDwGQywWQyZaTc0uk07ty5A4ZhkEgkEAgEEIvFoNVqc9ZByPn8\nyB5XLLIQi8XAcRwVC5StS64ZDqW0QZaShojFYnC73VhYWKjanlmr1W74yEIqlcLY2BhmZ2fR1dWF\nQ4cO5a0XUKPYkKyZrz1OKiCk/fXZIeFyNmQ1DlGl2U6RBTXWJZ0I9fX12LlzJ4AP6iDIZ5aM9xYE\nIcPWmszFqLRzqFSxEA6HAYCmIShbj2oHPRU6vKXFfG1tbTh79mzV9sykhanWVHKIS50mGxoacPr0\nadhsNtnXqZZCv1uNRrMur0z664mA8Pl84qYo3YzlHFRULdulwFHNmoWN4OBYqA6CCIjFxUVMTk6K\ndRDZUYhSCn9ZlhUdHAsRiUTEQuTtCBULWxQ5Bj3lSkPwPI+pqSlMTEzA4XDg1KlTsrkubsTIAqnD\nGB0dhUajwdGjRzPCp4XYDHbP0v566ZwBqYCQDirKFhBKz74gbAexoFYaQq1Cw1JqJaR1EK2trQAy\nW49J4a/P50M8HhcLfwuN9y719YZCIbGIeDtCxcIWQ85BT9JDVRAEzM3Nwe12Q6fT4ciRIyUfmpWs\nV0tK7YYIh8MYHR1FKBTCwMAAurq6ynovN6vngXRDbmtrA7D2+4/H46KAWFhYwPj4OFiWBcMwGBkZ\nqYkbZS7UEmBqdEOoNf1xM4mUfK3HLMtm+EEsLS0hGo2Kg7jK7cLYzp0QABULWwapoVIpxYulQCIL\nS0tLcDqdSKVSGBgYQEdHR03UtZIFjoXWSSaTcLvd8Pl86O7uxtGjRyuqw9hKUycZhoHFYoHFYsm4\noiNRF4PBgKWlJTEknJ1TltONcjulIbZLzQIgf0RDp9OhoaEho22bjPcmIoKk3XiexzvvvLMuCiH9\nzBKxQCMLlE1JuR0O5ZBMJhGPx3Hjxg309fVl2DPXArVbJzmOw9TUFMbHx4uaSJVCrt+BEoeOUlfe\nDMPAYDBAq9Wiv79fXFsuN8qNhFqzIahYkBfpeG+Cz+eD1+tFT08PwuEwlpeXMTU1hVQqBbPZDI/H\ng7t374opu+0KFQubFFK8mE6nZR/0lEgkRHtmhmFw/vx5RRwC1WqdFAQB8/PzcDqd0Ov1uOeeezKM\nkKpZZ6tEFspZv9ZulNslsqBW6oN00yiNWmkXjuNgNBrR0tKSc7z37Ows7t69i+HhYczNzaGtrQ1D\nQ0MYGhrCuXPn8PDDD5e13rPPPosf//jHGB0dhdlsxunTp/GNb3wDe/fuzfsz3/ve9/DEE0+suz0e\nj68bflUrqFjYZFTb4VAIlmUxOTkJj8eDHTt24J577sHNmzcVsxJWI7KwurqK0dFRxGIxcQKmXIfC\nZihwVIpCbpTSQkriRulwODLSGNlulNtBLKgVzQCgWmRhI0U0yGf20UcfxaOPPoq//uu/xvvvv48v\nf/nLuHnzJm7cuIHXX3+9bLHw5ptv4sknn8SJEyfAsiyeeeYZfPSjH8Xw8HDBSKbD4YDT6cy4TSmh\nAFCxsKmQo8Mh3+POzs5ibGwMFotFtGcmJiRKbZRK1yzcvn0b8/Pz6OnpwbFjx2Sf8LhZCxyVopgb\nZTgcRiAQEIcUEeGQSqWQSqUUPcC3S82CWmJBzYgGx3ElfffD4TCamppw5swZnDlzpuL1Lly4kPH3\n559/Hi0tLbh+/TrOnz+f9+cYhhELjtWAioVNABEJU1NTEAQhp1tgJZACNafTCUEQcODAAbS0tKyb\n+66kWKi1zwLHcZidnRXTN3L4Q+Rjo/ksbAakbpQEjuMyBEQqlYLL5RJtgZVwo1QjJaBWGgJQXiyU\naoxUC4jPQjFCoVBN3BtXV1cBIEM05yISiaCnpwccx+Ho0aP42te+hqGhIdmfTz6oWNjAZHc4xONx\nsVWtWkj4PRqNor+/P6c9M/kCKRUerKXPAmn9dLlc0Ov10Gg0OHLkSE3WItA0hDxku1GGw2H09vbC\nZDLJ7kaZCzK+fTtEFsh3Xa30h1qRhVJSreFwWHSXlAtBEPClL30JZ8+excGDB/Peb9++ffje976H\nQ4cOIRQK4W//9m9x5swZ3Lp1CwMDA7I+p3xQsbABydfhoNPpkEwmq3rsWCwGl8uFQCBQNPxONiqO\n4xSpWq9VGiIYDGJ0dBTJZBJ79uxBXV0dLl++LPs62cg93bIU1IgsqFX4l8+NMhQKZbTFAdW5UZLf\n4XapWVCzXkGNz2+paYhoNCp7N8RTTz2F27dvF92PTp06hVOnTol/P3PmDO655x783d/9Hb7zne/I\n+pzyQcXCBqJYh0M5g52ySaVSGB8fx8zMDNrb23Hu3LmixTFkbaUq+uUWC/F4HE6nE4FAAH19fejr\n64NWq0UikVDkalEaWZCrCLUUtlpkIRe53kupGyWhHDdKq9Wa88pWLbGgVhpiOxU3AuWlIeRyqwWA\np59+Gi+//DIuXryIrq6usn5Wo9HgxIkTcLvdsj2fYlCxsAEotcNBp9OBZdmyHpvjOExPT2N8fBz1\n9fW47777ynIhq0aglItGo0E6na76cViWxcTEBDweT05hRN5XJcWCUmyXoU6lUo4bJcdxsFqtGQLC\nZrPRyIICqGUxTdYutcBRjpoFQRDw9NNP48UXX8Qbb7yBvr6+ih7j5s2bOHToUNXPp1SoWFCZcjoc\nyjm4s3P05cw0qHTNaqk2siAIArxeL1wuF6xWK06ePJnzy002w1pvjLRmoTZUK/LyuVEmEglRQEjd\nKC0WCwDA6/Wirq5OVjfKQqhVs6BW3YBaYqGUyIIgCIhEIrLYPT/55JP4/ve/j5deegl2ux1+vx8A\nUFdXJxZbP/bYY+js7MSzzz4LAPiLv/gLnDp1CgMDAwiFQvjOd76Dmzdv4rnnnqv6+ZQKFQsqUckM\nh1IPbjntmZXoUJCuValYWFpawujoKFiWxeDgIFpbW/O+ZmlkoZbQ1snaIfcVN8MwMJvNMJvNojEP\ncaNcXl7GyMgIVldX4fV6FXOjVKt1cjtGFkqdDSFHZOG73/0uAOCBBx7IuP3555/H448/DgCYnp7O\n+D2srKzg85//PPx+P+rq6jA0NISLFy/i3nvvrfr5lAoVCwpDOhxIOoGkG0rZ/IqJhXA4DKfTiZWV\nFezatQs9PT1VfwGVmgQJVCYWotEonE4nlpeX0d/fj56enqKbnTSyUEuy6z2UCClvB4GipJ210WgU\nW9oOHz4MhmFkdaMsxHaqWVDLvREoLw0hR2ShlM/vG2+8kfH3b33rW/jWt75V9drVQMWCQuTqcCi3\n6C1fzQKxZ/b5fNi5cycOHz4sm+viRk1DpNNpjI+PY3p6Gp2dnTh37lzJc+bJe66EWMi2laZUj9Jt\njNI6IqA8N0qj0ZjRxulwOGAwGEp6/rRmofaQi7dia3Mch1gsJmuB42aDioUaIxUJ1c5wyD64pfbM\nzc3NOHv2rJhflQulXBXJWsWECXGbdLvdcDgcZRdsAh9Ec7ZiGmKzmzKVipKvsxRxks+NUjoiOZcb\nJfljMpnWraFGZEHNmgW1IhpAcX+HUCgEADUxZdosULFQI2oxw4GIBY7j4PV6MTY2BqvVihMnTmQ4\n3snJRoosELdJnudx6NAhNDc3V1WLoXRkQSk2egRjNbmKOmPlm67Sr6/SSEauEcnZbpQejwfRaBRa\nrTZnF8Z2SUOoKVIAFE1DhMNhMAxDp05S5IP075PiRUC+HnvyJX7rrbfAMMw6e+ZaoKRYyFcfEYlE\nMDo6itXVVfT398tid00jC+pwc/4mLkxcwBOHn0CrtbXix9lokYVSyXajBNYOaKmAmJ6eRiQSAQC8\n//77qKurE9MYVqu1pq99u4kF4ohb7DWHw2HYbDbVvCA2AlQsyEitBj0Ba9Wwo6OjAICuri709vYq\n8sFVsxsilUphbGwMs7OzstdiKBVZUHpENaD8lXepn3GWZ3Fx5iLuLt7F29638ak9n6poPaVrFmp9\nha/RaDAeH0dciOP0vtMAgGQyibfeegttbW2IxWKyuVEWQ61CQzXHU5dS3BgKhWC32ze8GK8lVCzI\ngCAISKfT6yIJcnywsu2ZV1ZW0NbWppjCVaMbgud5TE9PY2xsDA0NDTh9+rTs4T8lDvLs3/923mgA\n4P3A+3AuO9FmbcM13zWc7jxdUXRhs6QhSiWajuLfXP+GJJvE3qa9aDI3iet1dHSI33U53CiLoeaY\naCW8K7Ip1b2ReCxs5+8wFQtVIEeHQz6k9swdHR2iC+H09LRiV/qAsmkIhmGQTqdx+fJlaDSaio2k\nSkGJuQ3+ZTtJAAAgAElEQVQ0DfEBLM/i0swlaBktdtp34u7SB9EFlmcxuTKJXfW7oNWUdsBt1jRE\nLt6dexczoRkIEHDVexW/tfu31nVgkP+v1o2y2MHI87wic2Cy2ehmUHK1TW5mqFioAGLWkkgkoNfr\nxZyXHBsKx3GYmprCxMQEGhoa1lX7a7Xasi2fq0GpNEQoFMLo6CjS6TQGBgbQ1dVVc3dFpdMQy8vL\nSCaTqKurg9ForNkBpKRAKXUtElXocfSAYRi0WlrF6MJCbAGveV7Dw7sext6mvVWtyQs8nEtODDQO\nQKeRZ3urZQtjNB3Fr6d+DbvBDr1Wj0szl3Cq8xTMgrmkC49y3SitVuu6KIT0in471iyUk4bYzlCx\nUAbSDof5+Xm43W6cOXNGlo1EEAT4fD643W4YDAYMDQ1l9HETlLzSJ+ulUqmaPX4ymYTb7YbP50N7\nezui0Si6u7trth5BychCNBrF6OgogsEgjEYjYrEYdDodHA6HuGE7HI6SfSKKrbnRIFEFCICO0SHN\npVFnrMPo8iguzlxEJBXB5Ook3p17F7sbdheNLhS60r8TuIN/uPUPeHTfozi786wsz7+WkQUSVdjb\ntBcaRoPhxWFc9V7F/W33V3xoF3KjJAJiZWUFMzMz69woE4mEaDmsJJshsrCdPRYAKhZKIlcbpF6v\nFytpq2VxcRFOpxPpdBp79uxBe3t73sfV6XRbIg3BcRw8Hg8mJiawY8cOnD17VhRMSqBEgSOpcn/r\nrbfQ1dWFwcFBUUBEIhGEQqGM/nuDwSAKB7J5VyIgNlrr5NTqFBZji9AwGkyuToq3m7QmXJ65DKve\nin2N+zC+Mo6x4FhJ0YVc3w9e4PHLyV9idGkUv5z8JU60n4BRV70Aq5VYkEYVSBSkydyESzOXcLDu\noKxX+MSN0mg0ZqT2st0oQ6EQVlZWMDc3J6sbZTHULHCkaYjSoGKhCPk6HOQ4tKX2zKQlsNgHV+nI\ngtxpCEEQ4Pf7xQFXx44dE41s4vG4IqOjgdrWExDRMzY2BgBiKonjOKRSqZztcyzLiu1zoVAI8/Pz\nGQ6AUhFRaNPeiJGF3rpePHH4CXBC5ucozaXxi4lfIJ6Ow2F0IBAPlBRdyPd7uxO4g9sLt7G3aS/c\nQTfenXtXluhCrbohfjP3G0yuTEKv1cO17AKwJngiqQjenXsXbUyb7Gtmk+1GeePGDezYsQNWq1VW\nN8pibPQ0hFxDpDYzVCzkodigJ2K9XMnBlkgk4Ha7MTc3V3ZLoNI1C3J2Q6yurmJkZATxeBwDAwPo\n7OzMeO/IZqHEVUatIgurq6sYHh5GMplER0dHRq6zkDjR6XSor6/PMNciDoDkj1RAZKcw1ChKKxWt\nRou++vVjeN8PvI/V5Cp663oBAJ22zpKjC9nfORJVYDkWTeYmBBNB2aILtRKvdcY6/Ife/5D73/R1\nqjka1sKNshhqdmGUGllob29X4BltXKhYyEJqqEQKm3IVL+p0OjE9UerBxrIsJiYmMDU1VbE9sxo1\nC9Wul0gk4HK5MD8/j97eXvT19eVU89IBT7UWC3IXOCaTSbhcLszNzaGvrw+7du3CwsKCaBNbCbkc\nAMmmTVIYc3NziMfj4hAjk8kEnueRTqc3nIBYTa7iPf97uLfjXug1evxm7jdIcSmEkh+8R3E2XjS6\nkEt0kahCl6MLALDTvlO26EKtxMLR1qM42no0578tLy/DteKSfc1i5PvuVeNGabfbYTabC76HatYs\nlPI9CYfD2Lu3eHpsK0PFQhbEM6FYhwM57Erp0+V5HjMzMxgfH4fVasW9995bsce40jUL1VyBS2dX\ntLS04OzZswWLp5SaBknWkiMNIfWEaGpqyhCAtUh15Nq0pUOMgsEgBEHApUuXxMI1aRSiFr3spR6k\ndwN3cd1/HQ2mBnQ7usHxHNpsmaH2dls7UlwKMTYGu2F92Je8n9I1SVQhkoqANbNYSawAWEtzyBFd\nUGugkxoppXK6IUp1o4xGo2AYJqOF0+FwwGKxiK9RzTREKQWdtMCRioV1kHRDsS8quQ/LsnmL0ARB\nwPz8PFwuFxiGwcGDB6uaZwBsjsgCydm7XC6YTKaSZ1coNQ2SrFXtOouLixgZGQHDMDk9IZTyWZCG\njZubm3Ht2jWcOXNG3LBXV1fFyneLxbLuqk8JM5xgIoi7i3fBCzxuzd/CQOMAnjj8BASsf38YMEU7\nIqTfodXkKgLRAJotzYimo+LtTeYmRNNRzMfm0e2ovMNGacdIQN0WxmrW1Wg0cDgcGQcrz/OIRqNi\nGsPn88HpdAL4wI2S53mxE0PJ1027IUqHioUclHrVmW9kNAAEg0E4nU7EYjExPy/Hl2Cji4VgMIiR\nkRGkUins3bu3YGdHNiSas9EjC7FYDKOjo1heXsbu3bvzzqpQ05Qp1xhlUvlOKt6zBYQ0AiH3Vd7I\n4giCiSAGGgYwFhyDe9mdNwRfiFzvZ4OpAf/97H9Hilvf4qvT6OAwVrfJbyexUIt1NRqN+LkiSN0o\nV1dXAQB37tyR1Y2yFEp1jqTdEFQsVEUusRCNRuFyubC4uIje3l4cP35c1is3rVaLZDIp2+MVo9Ru\nCKkt9a5du9Db21vRF1xJsVDuOqTmxOPxoKOjA+fPny/amSA93JQ6cPIJlFwCIplMihGI5eVlTE1N\nIZVKie5/RECU4v6XDxJVaLY0Q6vRos5YJ0YXrHprRa8t+720GWo3DVCN6Y9qCBRAuRZGqRtlY2Mj\nvF4vzpw5k9HKWa0bZSmUkkYmrc7beTw1QMVCVUjrB6RDj6T2zLVcUwmKdUOwLIvx8XFMTU2hvb29\n6tetlFgo56pfEATMzc3B6XTCbDbj5MmTJW0cao2oLofs3nti3kMKKIn7H8uyGRu2w+GA1VraQU+i\nCnsb1wrEWqwtcC+7K44uAFvL7jkXWymyUAyyn2m1WlndKEtdm/oslAYVCzkodZPX6XTiDIfJycma\nDT2SopbPQvaGKQgCZmdn4Xa7YbVaSz5AS1lvI0UWQqEQRkZGEIvFKkqrqJWGqPSAI+Y9zc3NaG5u\nFh+LRCBCoRAWFxdFAWEymZBKpeD1esUrPulhE0qGMLI0gjSfxtjKmHh7kkvi9sJt7GvaB5OudHGp\nhvhSQyyoFc1QSyxotdqc73E1bpTkT6Fuh1J8FgRBQDgcppEFtZ/AZoW0WI6OjsJiseS1Z5YbNWoW\ngMwNc2lpCaOjo2BZFoODg2htbZVtM1VqFkWxAsdUKgW32w2v14uenh4cO3as7KuWStIQ48FxTK5O\n5u2/VwOGYWAymWAymTIERCKRgM/ng9frzQgZS0179GY9hlqHchYy6jQ6aJnKQsk0slCbNQGosm45\nKYVS3Si9Xi8SiYTYVpzLjbKUyEI8HgfLslQsqP0ENiOBQAAulwuxWAzNzc04cuSIYpuJWmKB4zjE\n43E4nU4sLy+jv78fPT09NSmGUrPAkbS5ut1uNDQ04MyZMyWH27PJFVko9DnhBR7/ePcfMRGcwEDD\nAHrqeipaUwnIFV99fT0CgQCGhobWTUD0+/0Ih8MQBCEjXGyz26AxaGAzlh6BI86GZo3ycwu2S+sk\n+d4p3cIoV9tkrpocaVtxthulzWYDz/MIhULQ6XR53SjD4TAA0DSE2k9gI5LvSxoKheB0OhEKhbBr\n1y5EIpGaTg/MRaEOjFpAxIDT6YTP50NnZyfOnTsny9CjXMjpGFmIXBGMpaUljIyMgOd5HDlyRLyK\nrpRy0xDX/ddxe+E2oukofjnxS3x+6PMVr63G1XC+CYjxeFysgfD7/XjjN29gMjaJ/7TrP2FH/Y6M\nqvd8z/ll98t43fM6/sfp/yGupRQ0slBbaumxUMiNcnV1FUtLS5iamsLIyMg6N0qr1Qqz2YxIJAKD\nwVCTGrTNhPIVNJuQeDyO27dv4+rVq7Db7Th//jz6+vrEYVJKomRkgVxlA2ve6Pfddx8OHDhQM6EA\nqFPgGI/HcePGDbz33nvo7OzE2bNnqxYK2WsUgxd4/Gz8Z+B4Dp22TlycuYip1amK1txIEAHR1taG\ngYEB9B/oR7A+iKg1ihXzChiGgc/nw29+8xu8+eabuH79OlwuF/x+P6LRKARBwGpyFT8d+ymGl4bx\nxvQb4uMqxXapWeA4rqSx2LVYV8nXSozN2trWDMFOnjyJ+++/H4cPH8aOHTuQSqXg8Xjwz//8z+jq\n6sITTzyBpqYm/PCHP4Tb7a54f3r22Wdx4sQJ2O12tLS04Ld/+7dFv4lCvPDCCxgcHITRaMTg4CBe\nfPHFitavFhpZKEA6nRbtmVtbW9fZM+t0OsTjcUWfk1JiIRAIYHR0FMDaAT44OKhIGE7JNATLsnC7\n3fB4PGhra8P58+dlFULliAUSVei0d8Kqt2J4cbiq6IKShYDlHC7v+N7BfHQeNpMNd6J38OD+B2HU\nGcVR3iRcPDs7i0gkAoZhcD1+Ha4FF6x6K152v4xHLY/W8NWsR42DW600xEaez1CrdRmGyelGeejQ\nIezfvx8/+9nP8KMf/Qjf+ta3cPv2bRgMBtx///34yU9+UtZ6b775Jp588kmcOHECLMvimWeewUc/\n+lEMDw/nTXVeuXIFv/d7v4evfe1r+NSnPoUXX3wRv/u7v4vLly/j5MmTVb3+cqFiIQeCIMDj8WB8\nfBx2uz1vpb/SKQHgg0FStbraIZMwV1dXsXv3buzcuRNvvvmmIgc4oIxYIH3TgUAANputZIfJcinV\nJVIaVSB+Aa3WVlycuYiHdj1UVu3CRossSFlNruLSzCU0mBrQbGnGWHAMNxdu4mTHSTAMA5vNBpvN\nJg7s4Xke/hU//tev/xcsWgsccMDpd+Jm80103OzIMJEqNnugGtRKQyh9gBZa07nkRJejq2xfjFJQ\n0+q50Lpmsxnnzp3DysoKLl26hHfeeQfpdBrDw8OYnZ0te70LFy5k/P35559HS0sLrl+/jvPnz+f8\nmW9/+9v4yEc+gq985SsAgK985St488038e1vfxv/8i//UvZzqAYqFnIwMzOD2dlZHDp0qKA9sxpi\ngVTky72ZSH0isidhKtWhQNaqpVgIh8MYGRnB6uoqrFYrTp06VbODoNTIwnv+93B74TYECGLqQYCA\nQCyAVyZfweeOfq4mz09p3vG9A3/Uj/079kPLaGHSmfDm9Js42nI05+wGjUaDa4vXsMQuYU/rHug0\nOiQMCVxbvYZHGx8Fm2AxPT2NSCSSMbzIZrchro2jt6lXlt+tWmJB6UFg+dIBvrAPPxv/Ge5puwcP\ndD9Qk3XVjCwUQ+qxoNfrceTIERw5cqTq9YlzpbSeIpsrV67gT//0TzNue+ihh/Dtb3+7qrX9fj/m\n5uag1+tFe26bzVaw44uKhRx0d3ejvb29aEhOrcgCIN8XjOd5TE1NYXx8PK9PhFJFh0DtxIJUDHV3\nd6OpqQmhUKimh0CpYoFhGAw2Da5rLxxoGIBeU9mBsdHMoEhUoc5YBwgAJ3Bot7VjYmVCjC7k+pmf\njf0MVp0VDJi1wVOWNtxcvonR1Cg+ue+TANYPL3rp1kt43f86Hm1/FLubd2c4UWr1Wui15b2n28XB\nMV8a4rr/OmZDsxAg4EjLETSYGnL8tPzr1ppSrZ4jkYjsKVhBEPClL30JZ8+excGDB/Pez+/3i8XC\nhNbWVvj9/orXvnnzJr761a/i+vXrCAaDoiMwOc9u3LiRUwxRsZADMkyqGCQloCTkeVV7pS8IAhYW\nFuB0OqHRaHIOQiIoWVQpdxRDEASxFbKurk4UQ9PT0zUXQKWKhWNtx3Cs7Zhsa25ERpdGEWNjiKVj\ncAfd4u0aRoMb8zdyioX3/O8hlAwhwSVEQyee56FltHhj+g18cs+aWJAOL0qwCfwf///BqmEVgboA\nTu04hXA4jMnJSTiXnPjV8q/w2MBj6NvRJ4qIfC1zBLVSAmrUSWSv6Qv7cGfxDnY17MJcZA63Fm7J\nHl3YqGkIQi2GSD311FO4ffs2Ll++XPS+2Z/NSoUk+f1+8YtfhCAIeO6559DX14dkMol4PI5EIoGl\npSX09/fn/HkqFqpAjcgCKcapZt1QKITR0VFEIhEMDAygq6ur4IdPqaJDudcKBoMYHh4Gx3HrUkpK\nvCay8apVTb+RONR8KO8VqcOQeyO+t+PedUOgEokEhu8O48PHPpzzZ675rmFiZQLdjm68t/Qefmvf\nb2F/134IgoDL71yGL+jDncQdtCfaEQgEEI1GYTAYMmys7XZ7RqHrdumGyCWKrvuvI5qKotvRjTSX\nxnX/ddmjCxzHKZ5yIeuWOkRKTrHw9NNP4+WXX8bFixfR1dVV8L5tbW3roggLCwvrog3F4DgOHMfB\nYDDgxo0beOONNzA0NFTWY1CxkINSNwal5zRUu24ymYTb7YbP50NPTw+GhoZK+pIqHVmo9hBPJBJw\nOp1YWFhAf38/ent71228SlgxV2u9XM2aSlHqe2jRW7CncU9Zj23VW9dFXKLRKNLWNPob1l/9JNgE\nLkxcgFFrRLutHXeX7uJ1z+t4/PDjcC47cX3+OurN9bgVvoVHhx7FoH0QHMdlmPYsLCwgFovBYDCI\nwiGRSCh+mKlluyxdk0QV2m1rBac7LDswujQqe3SB4zhVPAxKjSyEQiFZxIIgCHj66afx4osv4o03\n3kBfX1/Rn7nvvvvw6quvZtQtvPLKKzh9+nRZa2u1WvG1fvazn8Xw8DAVC0pCIgtKX3mUe3hzHAeP\nx4OJiQns2LFjXQuo3OtVA2lprATp62xpaSk41EqJyIJULCjNRosslAPHcxAgQKfJvT3l+66RqEJ/\nfT9WkitYii3hzZk38aGeD+HCxAXE0jHsb9qPu4t38ZrnNXzm0Geg1WpRX1+f0Q3DsiwikYhoJBUK\nhRAMBjE/P5/RgSG1DZabjdA6ed1/HYuxRRi1RsxH5wGspY3kji6okeYBSk9/RCIRdHR0VL3ek08+\nie9///t46aWXYLfbxYhBXV0dzOY1Z9LHHnsMnZ2dePbZZwEAX/jCF3D+/Hl84xvfwCc/+Um89NJL\n+NWvflVS+kLKM888g/r6ejQ0NKC9vR3PPPMMNBoNhoaGUFdXB5vNBqvVWlCgUrFQBSSEVWo4Sy5K\nPbwFQYDf74fT6YTBYMCxY8cKVt7mQ8luCK1Wi1QqVdbPkPqL0dFR6PV6HD9+HA0NhTcypSMLlNL5\n7o3vIpqO4r+c/C8587W5IFEFBgxYgcWdwB34Ij6wAot/Gf4XvB94Hx22DjAMg2ZLM349/Ws82Psg\nOuzrDwGdTpchIO7cuQOr1Yr6+npRPMzNzSEej2fMHSBCQo4ohNo1C4IgIJwKo7e+N+M+LdYW6Bk9\nIqmIbGJBzW6IUtMQchQ4fve73wUAPPDAAxm3P//883j88ccBANPT0xm/99OnT+MHP/gB/vzP/xxf\n/epX0d/fjx/+8IdleSykUilcuHABPM8jFoshmUxCr9fjD/7gD9bV59ntdni93pyPQ8VCDkpV9OQD\nXsrkMjkppWZhZWUFo6OjiMfj2LNnDzo6Oiq+UtnI3RCRSAQjIyMIhULYs2dP0fqLStepBDXEwkYt\ncCyV8eA4Xp96HRzP4W7/XRxszqwUzxfFm1iZQCgZgkFrgGvZhanQFFJcCsFEEK9MvgKb3oYex5pf\nRYulJSO6UAxBEETXP6kIzZ474PP5xMFFRDiQ/5a7P6hVs0DWZBgGv3/g9xVZV2kHRwLLsuIVfSHk\nqlkoZR9444031t326U9/Gp/+9KcrXlev1+Pf/u3fwLIsOI6D2WzG3NwcWJZFIpEQixsjkUjBPZGK\nhTyUcuWp0WhU6YgoFFmIx+NwuVxYWFhAb28v+vr6qhYyG7FmIZ1OY2xsDDMzM9i5cyeOHj1a1hXd\nVo8sbNZoxk/GfoLV5Co00OAl90s4sOPAOnGQSyzsa9qH/3rffwXHc/j7G3+P5fgyeup6MB4cX0tn\nMGsdGQRBEHDFdwUfH/g46k2FDbnypQRyzR1Ip9MZ6YvZ2VlxdHJ2CqPQ91KNNMRG9ztQa91IJLKp\nJ04yDIOdO3cCWKvnunTpEj7ykY+U/ThULFSJGmIhV4Ejy7KYnJyEx+NBS0sLzp49W5JqLoWNZMok\nCAK8Xi9cLhdsNhvuu+++ikKENLKw8dYcD47j4sxFtFpaoWW0eMf3Du4uZkYX8r2XGkaDbkc3hheH\nMbI8gl31u1BvqsdyfBlmnRmfO/o5GLSZ9QUmnUl0zCxEOTVJer1+3eRDMjo5FAphZWUFMzMzSCaT\nsFgsGdEHqSmO2mkIJVGzdbLYhZQgCLKlIdSE/G5v3LiBhx56KOfed+HCBfzhH/4hpqZyz6ShYqFK\n1OiIkF7pC4IAn88Hl8sFs9lcE+viSuoIKqXQIb6ysoLh4WGkUikMDg6itbW14oNqq4oFwkaKLLw6\n+So6bB040Hyg4P1IVKGjca2OwB/154wu5PudC4KAv7/59/CsenC28ywAoLuuG5Mrk2AYBh/q+VBF\nz7/aAuZco5OTyaSYvggGg5iamkIqlYLVaoXdbkcqlUI8Hlf0IN3ohYZqrStXN4SaxONxRKNRTE5O\noqenB/F4HKlUCnq9XhzPvbi4WLDwnYqFPJQaplZzPkQwGMTIyAhSqRT27duHtra2mlxZqp2GSCQS\ncLlcmJ+fR19fH/r6+qreXLZqGmKj1SzMhmfxw5Efot3Wjq82fnXd1T1BGlUgr6HN2rYuulDovbwT\nuINLM5cQSoZwY+EGbPq1qEEoFcLL7pfx4Z4P5+2wKEQt6geMRiOMRmOGEVoymRRTGDzPY2JiAm63\nGxaLJSOFYbPZanK4qmExTdbdyGIhEolsWrFAhO6NGzfwJ3/yJ2AYBktLS/jjP/5j6PV68bOVSCTw\n2muv4ezZs3kfi4qFKlFDLJAuh+npaezatQu9vb01/bIpnYYgaxEr6rGxMTQ3N8ueWlF6FLaSbJTI\nwmue1xCIBRBKhvDu3Ls403Um5/0uzVxCNBVFWAgjEA+s3fjvL+HizMUMsZBPEPkiPrRb29Fh60CD\nqQGnO09Dq1n7XpSSbsiHUq3RRqMRzc3NaG5uhtfrxeHDh2E0GsUUxuLiIiYnJ8GyrBiBkKYwqhU0\nal7hq1XgWCwNwXEcotHophUL5HNrt9vxwAMP4NatW7BYLGI7MCluZBgGDz744Lo5FFKoWKgSJcUC\ny7IYHx+H1+sVJ6IpYWaiRjdEIBDAyMgINBoN7rnnnowQrhwodYiXOnlS7jU3ArPhWVycuYgOWwdW\nk6u4MHEBJ9pP5IwuPNj74Lo2PUK3ozvj77leX4JNwLXswvH24+LMiVOdp3C09WjVr0MtB0etVguT\nyQSTyYTm5mbx9kQikWEiNT4+Do7jYLPZMto4i/XNZ6NWnQR5rUpTijgKh8MAsKkLHAHgyJEj+Ju/\n+Rt4PB7cuXMHH/vYx8p+DCoW8lCOi2OtxYIgCJidnYXb7YbNZkN3dzeSyaRirmdKpiHS6TRisRhu\n374tWlHXYgNTMrIArIWYnU4n/H4/rFarOMvA4XDAYrFsmANeTl7zvIZgPIgDOw7AbrDDueTMG13Y\n6diJnY6dRR8zn8C7E7iDmdAMdjfshl6rh1FrxBXvFQzuGMyb+iiVjWCQRGAYBmazGWazGS0tLQA+\nEBAkhZEtIKQpjEICQi3XSACKiwVBEEryWSBiYTMXOE5OTmJsbAwWiwUtLS2455574PF4YDKZYDAY\nYDQaYTAYiqagqFioklp3QywtLWFkZAQ8z+PAgQNoaWnBzMwMYrFYzdbMRomDlURNPB4PNBoNzp07\nVzN3PEDZK36fz4fp6Wk0Njbi6NGj4pWhz+eD0+kEwzDi1SDZ2E0mU1UHlFxRk1g6hvf87+F012lo\nmPUHSb51SFSh1bpWg2DSmaDT6ApGF0oh11V+gk3givcKLHqLOFGy096JiZUJDC8OVx1dUDqyIAhC\nWQJFKiDIzABBEBCPx8UUht/vh9vthiAIYgSCfNYsFov4HZdDLCTZJCZXJ7GncU/Oz4wU8h1UY1BX\nKRGNcDgsS4pHTV5++WU899xz6OjogE6nEwvWSTuvTqeD3W5HMpnEY489ts40ikDFQh7Ung8RjUYx\nOjqKYDCI/v5+9PT0iB9YpeskahlZkHZzWCwWHDp0CKOjozUVCoAyQ56CwSA4joPP58ORI0fQ1NSE\nVCqFuro6tLW1AVjbtKLRqLipezweRKNR6HS6DGMfMh2xFOR8PT8d+yl+NPojGHVGnGg/UfLPve55\nHd6wFw2mBqwmVwEASS6J0aVR/GbuNzjdVZ63vZTs1zceHMdyYhnRdBQjSyPi7ZzA4eb8zU0pFgBU\ndUAxDAOLxQKLxZIhIGKxWIaJVCQSgSAIsNvtiMViYuV/NdGuu4t38Zb3LRi0Buyq31XwvqReQQ1P\nCaC4SAmFQrDb7Zs68nfmzBlotVpoNBp4vV688MILSCQSGBwcRCqVwt27dzExMYG6urqC5k9ULFSJ\nTqcT54HLgdRsqLOzE+fPn193SCiZFqjlequrqxgZGUEikRC7OYq5iMkF2YhrUYmdSqXElINWq8Xh\nw4fR2NiY83VpNBoxREz85zmOE2cThEIhcbgRsRaWRiDyhVHliCwEE0H8ZOwnmFqdwovOF3Gs7VjR\nK0VCg6kBD/U9tO52hmFg0eduz/JH/IixsYIHTK7X1VPXg0/vzb3JVVPYKF1TyStLOcRCLhiGgdVq\nhdVqFcUqERChUAhutxvBYBBzc3NgGGZdCqMUARFLx3Ddfx3ekBc35m+gt6634GdGzeJGhmGKrh2J\nRDZ1CgIAjh8/juPHjwMAfvCDH8Dn8+ErX/kK9uz5YLDbX/3VX+Hu3bs4cCB/ezMVC1Ui11U+z/OY\nmZnB2NgYHA5HQbMhpcWC3N0Q0umXpBWSHHpK1xLIWeQoCAJmZmbgdrvR0NCAM2fO4Nq1a+Ja5diI\n19XVZRRVEWthIiCIMyBpfSJ/bDabbFdBr06+Cm/Yiz2Ne3Bj4Qau+6+XHF34+MDHy1qL4zm8MvkK\nwugsx1YAACAASURBVKkwHj/8OKx6a977Zr8+m8G2zsOBF3jE2XjBxykVJSILSTaJF10v4mO7PwYj\nszYeW4mrWamA8Hg82LNnD+rr68UIBPmsRSIRMV0mTWGYzeaM5zm6NAp/xI89TXswtjwGz6qnoPhT\n22Oh2HtMDJk2c2RBEASxxu0b3/gGPve5z2HPnj3geR48z0On0+HLX/4yTpw4gbt376Knpyfn41Cx\nkAelChwFQUAgEIDT6QQAHD58GDt27Ci4vhqRBTkOcJ7nMT09jbGxMTQ1NeWcfknEQq03aGlkQQ6I\nYRTLsjh8+LBYvZ4SUvjp+E/x0f0fRYulpeLXlMtamBj7kLa6iYkJcBwHQRAwOTmJxsZGsSq+3HVJ\nVMFusKPOWAd/1I8fO39cVnShHMaCY5hYmUCKT+Fu4C7u7bg35/1KFXcvu1/Gzfmb+Mp9X4FRZ6zq\nuSkhFl5yv4RnrzyLcCqMx/Y/BkD+yEIxSJRNo9HAZrPBZrOhvb1d/DeSLguHw5ienkYkEoFWqxUF\nhN6ix1XvVTgMDtgNdsxH5otGF9QWC8XYCoZMDMOIxYttbW24dOkSHnnkEbS2toqfsfHxcfh8Plit\n+cU1FQtVUo1YCIfDGB0dRSgUwu7du7Fz586SNgilaxZIZKGaTXNxcREjI2v55KNHj2aY0WSvBdR+\ngyaPXa1YSKVScLlcmJuby2kY5Yl7cCtyC3a7HZ/c88mq1som29iHVMVfvXoVGo0Gc3NzcLlc664I\nHQ5H0QJKElUYaBgAAHTYOnBz4WbO6EK1vyeO53DNtxaBqTfW4525d3Cg+cC6qECKSyHJJouutxRf\nwq88v4I/6sc13zWc7z5f1fOrdTdEgk3gH27/AwKxAP7vnf+LT/R+AoDyLbCFUgLSdBmBCAjShXF1\n9Crem3sPXeYupCwp6A163Jq5hcG6Qexr3Zfz9Wxkq2fggwLHzQ55j7/4xS/iqaeewhe/+EX8zu/8\nDlpaWuD3+/H1r38d+/btw969e/M+BhULVVJJN0QqlYLb7YbX661oCJIakQWgsgM8FothdHQUy8vL\n2L17N7q7uwsKIrJWrdu4qk1DkHZWl8uF+vp6nDlzZl2UJJ6O427oLtLmNG74b+BE+wnsMOYWSXJA\nquK1Wi26u7ths9nEsbRkQydXhKQCWlr/YDSuXYGTqIIgCAgmguLjh5KhmkQXSFShy9EFg8YA57Jz\nXXRBEAT8z/f+J6KRKD5iLTwE5+L0RcxF5mDQGvDLyV/iZMfJqqILtRauL7tfxuTKJLocXfBH/PhX\n17/igGb9AK1aU+53TiogYukYLiUuoUvfBZvGhkQigWQiCV/Yhx+99SPc33w/6hx1GSkMo9G44d0b\n5Zo4qSbS3+tDDz2Eb37zm3j22Wfx+c9/Xqy3e+SRR/CXf/mXYi1LLqhYyEMtuiGII+H4+DgaGxtx\n5syZgmGffGi1WrG9SolQJflSlVOMxLIsJiYm4PF40NHRgXPnzomHUSHI45c6a75SGIapuH1ydXVV\nnFFx6NAhsd89m9sLtzGfmsexjmPwJX141/cuHu57uNqnXhLSIjkSUiaQAkqSwiAFlEajEQ6HA4tY\nBJtm0WxpRppPYym+hFZrK7rsXVhJrCCWjslSOAhkRhXMujV3zjpj3browujSKK76riKdTGOvZi/u\nRe40xVJ8Ca9NvYYGUwN2mHfAHXRXHV2opVggUQUGa68/oo3g+6PfxzNdz9RkvXxUu5/E2ThMOhO6\n7F1rN/z7ttaDHtj0Nuxv349ULIVQKISJiQlEo1Gxt5/neSwuLmYI1lqzncRC9u/0E5/4BD7xiU+A\nZVmEQqGM1GYhqFioklJSAoIgYGFhAU6nE1qtFkNDQ1U5EpIPOcuyNW8xBDIP8GIREGJF7XQ6YTKZ\ncPLkybLcz+RKD5SCRqMpK7IgjQj19fVh165deTeceDqOt2ffhllrho7Roc3Whvfm38PR5qNot7XL\n9RJyUuxg02q1qBMENE5NAaEQhOZmpE6cQPjfNw+EgD9q+SPEE3FcjlzGVe4qHul6BA/2P4g6ex0M\n+vI+c0k2CYPWkPN5jQXHML6yNkbaF/EBWCtOnA5Ni9EFQRDw84mfI5aOIcWmcGXpCh4RHsn5eCSq\nsL9pP7QaLQya6qMLteyGIFGFJvPaftBgasB8ZB5vBt/Ef8R/rMmauSDfg0qv8pvMTfjMwc8UvpPk\nTCKCdWpqCuFwGOPj46KAkHZglNMyXA6lpiEikYjYerpZ+ad/+id8+tOfhslkwttvvw2DwSAWRptM\nJoRCIZjNZmrKVGtIZCGfKg+FQhgZGUE0GhUdCau9SpFe6SsB6YMuth55rbFYDHv37kV7e3vZr5W0\nMyklFkpZh4zFdjqdqKurKykidHvhNqZXp9FsbAaEtc3UH/bj3bl38YmBT8j1Ego+53wwbjf0//iP\nYLxegGHAaDTQ7d0L/eOPo0FSCT27Mov//av/jRAbwoXJC2iNtQI8MhwoWZYtuFaaS+O7N76Lwy2H\n8eGeD6/79ySXRJe9CwIyH6PR3IgEmwCwFlV4d+5ddNo6EUvEMLw6DNeyC3ubMvOrJKpQb6pfixoJ\nPDrtneuiC7F0DDOhmXU/n49aRRZIVCHJJhFLxxBLrxmtpfk0Xg28iv+W/G+oMypjM0y+20oVVZKO\nH9L+Ozg4CJZlxZbhcDiM+fl5MeIlTV/Y7faqBUQ5kYWBgYGq1lITjuPw3HPP4eMf/zj0ej2+8IUv\nQKfTQaPRQKPRQKfTQa/XQ6/Xw2Qy4YUXXsj7WFQs5KGcNASwPkSfSCTgdrsxNzeHnp4eHDt2TLaw\nOsMwG6ojQnrFLcdr3UhDnkjKIZlM4uDBg2hpKd7RkOJSeHv2bUTSEUTiEYSCIViSFiS5JG4t3MKp\njlNotdXuaqXg80uloP/Xf4Vmfh78/v2AVgshmYTm7l1of/ELsP/5P4t3/fXsr7HKrWKocwiz4Vlo\n+jS4t/lecTP3+/0IhULgeR7Xr1/PMJEiLXU35m/g/cD7CMQCON52HA5jZkj3cMthHG45nPfpkqhC\nPB1Hj6MHOlaHaW4aPx//OfY07sl4rbcWbiGcCiOejsOZdGY8zlXfVVEs/L+R/4dXPa/iLz/0l+i0\ndxZ8LwVBqJlYWEmsIM2lscOSWcfSaGoEwzIIxAKKiQXyfVPD7pkc2jqdDvX19aivrxf/nWVZsQMj\nFAphbm4O8Xhc9ByRiohy6r5KTXOGw+FNPxfi61//Ourq6sDzPD772c+CZVlxgBT5bzQaLfq7p2Kh\nAKUcJtKUgF6vB8dx8Hg8mJiYECclFpoRXikbwZhJ6g1BivwqqcHIZiNEFtLpNNxuN2ZnZ9Hb24v+\n/v6SQ7QaRoPj7cdxqOUQRkdG0dLaspYXFNY2KZNOmZkeOZ/b5CSY6Wnwvb0AeT1GI4S2Nmhv3wYb\nCgEOBxaiC3hl8hU0mhph0VugYTT4ydhPcLrzNFpbW8XQ7Pz8PCYnJ9HR0YFQKISZmRmxpc5is+CF\nuReQTqUxw87gmu8aPtJXuDgxGxJVaLN9UHjVZGzCO3PvrIsunGg/gQZTQ87HIWH++eg8fj7xc8yG\nZvHTsZ/iD4f+sOD65PtfC7HQZmvD67//+rrbl5aW4Ha7sbtht+xr5oN8D9Qoqiz0vdLpdGhoaEBD\nwwe/V+I5InWiTCQSMJlMGdGHQgKC7NfF2OzdEFqtFg8++CBu3LiBoaEh/NEf/VHFj0XFQpWQq/x0\nOo1gMAiXywWDwYDjx49nfMDlptYzKbLJNmaSzqyQ+grItZZSkYXsdUjKweVywW63VySAdBodznWf\nAwDYF+zY2b4THR0dEAQBqVRKtudfiLwiN50Gw3EQsjZKQa8Hk0iASachAPjl5C8RiAWwr2kfAKDL\n3oXRpVFc8V7JKBYkn//29vaMnvxIJIJLk5cwEZpAs64ZgVAA/3zln2EL2tDe2F7y1eC1uWtIc2nM\nR+YxH5lHMpVEMpVEA9eAa75rGWLBbrBjqHWo4OP9YvwXWIwtot3Wjlc9r+Jjuz9WMLpQS7FQaE21\nPBbUaNcsNwqZy3Mk27TM6/UikUjAbDavS2GQ1HEpg/i2QoHj2NgYHnnkETzwwAM4c+YMjhw5gr6+\nvrLr5qhYkAGNRoPbt28jnU5jz5496OjoqPmXTq00RDweh9PpRCAQwO7duzNmVsiFkpEF6aEaCoUw\nPDws+qa3trZW/XtUahR29pr54Lu6IDQ2gvH7IXR+cEhq/H5wg4MQGhrEqALLs5gJzYj3CSfDeMn9\nEu7rvE8c2JQLjUYDs9WMO7E72NG4A/0N/ehOd+P9hfcxyU7CHrGLV4NkmI3UgVJ6pfnwrodxqPmQ\n+PfFwCKWlpewd+9e7LQXn1IphUQV6o31aLG0wLnsLBpdUEMsqDHlUi3bZbl8FnIJiFQqJUYfVlZW\nMDMzI7qesiwLnuexsrICm82WU7AIgoBIJLLp0xANDQ341Kc+hffeew+vvvoqduzYgQMHDuDBBx/E\n/fffj5aWlpKM26hYKECxjT4ej8PlciGdTou/gFq2+0mp1QCrfJAhJIFAAK2trTh37lzNRmQrHVmQ\nzuPo7e3Frl27ZK0vkX6GlBAPvMCD5fJEnerrwX7kI9D9+MfQOJ0QrFZgdRVCYyO4j34U0GiQ4lPo\nq+tDh60j40d3N+xGvbEeLM8WFAsAcGP+BsaCY+it7wWwtpm32FtwN34XHzvyMTiMDnEzD4VCWF5e\nhsfjAcuysFqtGR4QQy1D4kHm432YZ+cx1FY4gpALElXY27gXGkaDJlNT0eiCWmJBjcjCZhYLuTAY\nDGhqasq4gk6l1to3XS4X4vE47ty5g1QqJXYHSDswTCaTaPe8mWlqasI3v/lNCIKAK1eu4LXXXsPr\nr7+OP/uzP4NOp8PJkyf/P3tnHh5Xed/7zzmzz0ijXZZkWZZkW96xMRgv2AbM4kLShISG0JYkJZTc\nQpvblCbpTZulT9vnaQhpLyRpk5KSSymBLBCDgUAxNosNGLzKtjTa910ajTQzmvUs94+jM57ROpJG\nkgn6Po8f0Ehz3jNnznnf7/tbvl9uuOEGPvaxj01ZzLlEFmYBSZJobm6mpaWFZcuWkZ6ezrJlyxaM\nKMDCRRZUVaW3txe/3080GmX79u0JBUjzgVR7UUwGQRBwu91cvHiR9PR0du/enXR+ssffw7OuZ7l7\n891kWie/Hgtpha3D5Xfh7fFyW9ZtE6vm7d+PmpOD4YMPEAYGULZtQ961C7Vc0/AvTi/mH/b9Q9Lj\nTTTGqZ5TyKpM41DjpRdVkBSJqoEqdi3fNW4y1zXs9VByb28vDQ0NMVtlp9MZ6zyaadGhHlUwG8z4\nI34ALEYLrcOtU0YXUm3qlAz5WCIL8wez2Uxubi7Nzc2UlpaSl5eXIJs+ODhIY2Mjd955J4WFhdhs\nNl566SUURWHLli3YbLYZj/n222/z8MMPc/r0abq7uzl48CC33377pH//5ptvcsMNN4x73eVysW7d\nuhmPrxfpiqLI7t272b17N9/61reor6/npZde4tChQzz44IMcO3ZsqRtithj7QOv57Pr6emw2W2zh\nPHny5ILWD8DC1Cz4fD5cLhd+vx+73U5JScm8EwVInRfFVPD5fAQCAUKhEBs3bpxxyuG3Db/l5YaX\nKUgr4A/WT27rOtUxvSEvjUONs9olTwZ30E1roJXAUIC+QB/LHJe6Ls72nsUiWtiQtwFl61aUrXOw\nbpYkhO5uTO3tWDwekOVLBZPAx1Z9jN3LJ7ahnsxYSBAErFYrVqs1JnQV74ro8/nweDyEQiGOHTs2\noQLlZNe71l2LiIjVaMUX9cVez7HnUNlXOenHnGxxj8gRfBFfrHAyWTx04iFkRebvrp1cdGkxaxYW\nGos1riRJsXHHyqYrisKJEyc4duwYf//3f8/x48f58Y9/jMfjYdOmTRw5cmRG+f6RkRG2bNnCPffc\nwx133JH0+2praxPqJWZbF6a3vV+4cIGOjg7cbjc9PT20t7dTVVVFbW0tBQUF7NmzZ8rjLJGFJDE4\nOEhNTQ2RSCRmp6xPIAvt1QDzG1mI7wQoKSnhyiuv5OLFiwu2Q57PNIQkSdTX19Pe3o7JZGL16tVT\nSpxOhE5fJ0dajhCRI7zS8Ao3lt04aRX+VJGFR049wvGO4zz2e4/FwvVzRY27hoASICyFqR6ojpEF\nT8jD6e7TmAwmVmauTPBdaPA0kG5OTyAWU8LrxfD224htbdiHhsj1ejEA8r59MBqyXZmxkpUZK4nK\nUQyiYdby0PGuiIWFhdjtdtxuN2VlZbHdYLwiYHwo2el0xgoo967Yy7qcdeP0HIApnSkn6xK497f3\ncqLrBOe/eB6bKbndZq27lpcbX0ZVVT619lNsyN0w6ZiXu9RzqnA5GkmJokh5eTkOh4Mvf/nLvPji\ni9jtdtra2jhz5kzSioc6br31Vm69debKrfn5+XPanOnRt8OHD/Of//mf5OTkMDw8THNzM1lZWVx1\n1VV89atfZdeuXUkV4y+RhWkQCASora1lYGCA8vJySktLx91kC92ZAPNTsxDvd+B0OhPC8guVGtDH\nSjVZUFWV7u5uamtrcTgc7N69G5fLNatJ+X8a/wd3wK21RrprONJ8ZNLowmQ1Cg2eBo60HKE30MvT\n1U/zt7v/dsbnMRbuoJsadw3Zpmzy7Hk0eBrYkLuBZY5l1AzU4Al7EBGpc9fFohm+iI+jrUfJseVw\n+5rbMYjTTNyqiuGDDxAbGlBKS4lmZRHu7ERsaACbDXn//rg/VXm16VWybdlcW3ztnD+ffkxBEGJk\nYPlokWa8oI/ejz+2Gl4nEjNZnCZKd1zsv8gL9S8A8MSFJ7h/W3LtaM9UP4Mv7ANB+/9/3PePk465\nGHoHi0UWFmvc6dLGfr8/JlYkCAIrV66c1L55PnDllVfGiq2/+c1vTpiamAr6vfvBBx/w61//mlWr\nVnHffffx0EMPUVxcPOPzWSILU6Cjo4MLFy5QVFTEvn37JtUt/12ILHg8HlwuF9FolM2bN5OXl5cw\nSS5EakBHqsmCnk4ZGRlJiArNpp5Ajyrk2fMwikYyLBlTRhcmIwtPVz3NYGhQK7JrPswfbfijOUcX\natw1+KN+0oxppJnS6In2UD1Qjdlgpmqgijyb5vVwvv88FTkVOEwOXAMu+kb6GA4N0zzcPH1v/9AQ\nQmsrSlERWCwQDKKazSjLliG0tMDwMIxWj7d6W6lx12A32Vmfs55s28x2ZJNhIoI3kaBPNBqNkYeh\noSHa2tqIRCIJCpS6hfdkC9ZEZOGf3/tnjIIRSZV4+P2H+ZPNfzJtdKHWXcuR1iNk27IREHij9Q2q\nB6onjC4s1SzML1RVTWpcr9dLenr6gl+XwsJCHnvsMa666irC4TD//d//zY033sibb77Jvn3Je5zo\n533XXXeRnZ3Nu+++y+HDhzl9+jRr1qxh3759bN68maysrKSK1ZfIwhTIyspi586d0/bZGo3GBeuf\n12EwGGKOYXNBKBSitraWvr6+SSMn+ngftsiCJEk0NDTQ1tZGSUkJ27ZtS9hNzNQbAi5FFTbmbQQ0\n62aX2zVpdGEistDoaeRIiyZLnGfPo2GwYc7RBT2qkG/Lp0foAaDQUUiDp4FAJMBQeIiKrApUVBo8\nDdS561idvZqzvWfJteXij/qp7KukLKNsyuiCEI1qWgxjibPZjDA0FNNpUFWVc73nkFSJwdAg1QPV\n7FkxdU40Gczk+zKZTJMWUPp8Pvr6+mhsbERRlFgBpR6FsNvtse8unixc7L/Iiw0vxn52B91JRRf0\nqIJer9E03DRpdGGx0hCXWzpgPscEpo0sLFYnxNq1axOsonft2kV7ezvf//73Z0QWdKxatYr777+f\n+++/n9raWo4dO8bhw4d55plnyMrK4pprruGGG27gwIEDU651S2RhCqSlpSUVMTAajQQCgQU4o0uY\na+ojXmkyPz9/2lZIURQXLHoyV7Kgm1nV1NRgt9vZtWvXhA/9TCMLvSO9HGk5QlAKUj1QHXt9JDLC\nKw2vcKD8AOmWxHEmIgs/r/o5g6FBKrIrMAgGnBbnnKML7d52wnKYkegIHcEOwkNh7A5NYrp1qJXV\n2au1aAoCTouT8/3n8Ua8uINuKrIrcMpOmjxN00YX1IwM1IwMBLcbtbDwUgHg4CBqZibq6GTT6m2l\nfrCeorQiglKQyr5KNuRumHN0YS7Sy1MVUOr1D7oHiCAIpKenx56JUCiExWJJiCoAqKjTRhcSogqj\n555jzZk0urAYu/zFSAfoTpeLRRaSiSykpaUtOHGbCDt37uSpp56a83F0IvKnf/qndHZ28sILL/Cj\nH/2In/zkJzz//PN84hOT+9YskYUpMB821anCbMdUVZX+/n5qamowGAxJK00aDIYFi57MhSz4/X6q\nq6sZGRmZ1sxqppEFi8HCgfIDRJXouN9ZjdYJd+Rjx2jwNHCk9QgWgwV/VGvhsxltdPo75xRdWJW1\nKlaZfy5wjpUrV5KVlUVlXyUfdH2AO+hmMDQIaDoMQSlI01AThY5CREHrEhAEYfrogsWCsnUrhrfe\ngtZWRFnG2t2NsHIl8pYtYDbHogqyKuMwOegf6U9pdCGVk3d8AaVe6KooCiMjI3i9XtxuN4qi8N57\n79EeaU+IKugYCA5MGV14qeElvGEvBtHAcHgY0EiGrMi83PjyOLKwWN0QizEmzN7pcraQJClmjjcV\nLif1xrNnz8YUUmcKn89HU1MTra2tdHd343K5aGxsjPn5GI1G1qxZQ2lp6ZTHWSILKcCHpWbB7/dT\nU1PD8PAwa9asYcWKFUlPvAudhpjpWJIk0djYSGtrKytWrODKK6+cVkp4pqQk05rJ56/4/IzOa2xk\n4VT3KQQETAYT3rA39nqGJYNzfedmdOx4pJvTSTdrUY0uSxdFjiJynbmoqOPElQCqBqo413uOsDVM\np68z9nqjpzEWXQhJIV5pfIVdy3cleDMo69ahms2ILhe0tREqLES65RbUsjLgUlShMK0QT8jD2b6z\nOM1OKhuPs7l5hGzRoSlJrlwJM1z4F0INUxTFmDRwWloaPp+PnTt38tXXvzrpe3565qfcVXZXTE44\nHreU30JR+vjvAGBj7sZxry3GbnuxohmwOOZVyZpIpSIN4ff7aWhoiP3c3NzMuXPnyM7OpqSkhG98\n4xt0dnby5JNPAvDII49QWlrKxo0biUQiPPXUUzz33HNTaiBMBP07vf/++6msrIyRYKfTyfr16/nS\nl77Ezp07ueaaa5K6HktkIQW43MlCNBqlsbGRtrY2iouL2bJly4wc2mDhuyGSHUsXjaqpqcFms02a\ncpgIC6GmOHaMO9fdmaBIGI/J2i9nM6aOEmcJJc6ScX8jKdI4Q6sObwcDgQH07sLzfec51nEMRVW4\nY11cf7ggoK5ahVxejr+rC3d3N2Xll7QTGj2NSKpEp6+TWnctrUOtOEIS+QM1tHsukB/JRXU4kHft\nQr7ttgR9hukwXw6Qk0GvHzAYDHxt99fYsWIHYTlMRI5gN9hjzn35Yj5VVVWxAsr4DoyNORsTJKuT\nGXOmz+dcsZjpgIUmC/EaC1PB7/enJLJw6tSphE6GBx98EIAvfOELPPHEE3R3d9PW1hb7fSQS4atf\n/SqdnZ3YbDY2btzIyy+/zG233TajcfXrWlFRwdq1a9m2bRubN2+mpGT8fJAMlsjCFJjJrvtyFGWK\nN0VKS0ub0UI60Xjz0Q1xvvc8y53LE8RtRFFMKuXh9/txuVz4fD7Wrl07Y0+OhZCVHksWRFFkTfaa\nRak8BxgIDPBm25vcuupWrim6JvZ6VI7ynePfoc3bhiqohKQQ73a+S0SOcLbvLDuW76A4fUy7lSDA\nBDuSrcu2UpZZRt9IH/0j/SzPSsd98X2Wq2msXnUNimgDjwfjW2+hrlgxN3GoeUY8OSlKL+KuDXfx\nS9cvGQwO8vltn8diTCz0jFeg7O/vp6mpCVmWYwWU+j+9gHIiLNYufyEVaPUxF8u8KhmykKo0xPXX\nXz/lpuSJJ55I+PnrX/86X//61+c8ro5vf/vbCT/Ha4fM5NovkYUUYDEiC9PVLAwNDeFyuQiHwykx\nRZqPNES3v5tjbcdYlb2KA+UHYuc3HTGRJImmpiZaWlooLi5m69ats9qJLYQU82IYScHk4fp3Ot7h\naOtR8u35Ce6RJ7tPUj1QTUgK8Wrjq1xTeA2tw62sz1lPvaee9zvfp3jdxL3ZY++rHFsOObYcLvRd\n0MhRyE5+wEF3voiHEOnYICsL+vsRq6pmRBYWOrIwdrx2bztnes7gi/io7KtMIFygqQHm5eXF1PZU\nVSUYDMY6MLq6uhIKKOP1H/R+/o9SzcJiqTcmQ4z01skPO3R5dL1OY7bf8xJZmAIzKXC8XNIQ4XCY\n2tpaent7KSsro6ysLCUP5Hzswi/0XcAT8lDnrmNz/uaYmc9kY6mqSl9fHy6XC6vVmlRb61RYiNTK\nYnhDTHbf9o70cqLrBGEpzNvtb3NV4VU4TA6icpSXGl9CQKA4vZhjHcfoHenFZrJhMpgocBRMHl2Y\nBF2+Ls71nqPAUQB9PWSpVrrUCO/LrZSIWrpFNZlgFl1Ei2kX/W7nu/giPqxGK8faj7Elf8u46EI8\nBEHAbrdjt9vHFVDqHRgtLS2MjIxgNBpxOp0EAoFYO7bZbJ73z6if02JEMy7ndk2/3z9jddfLEan6\nXpfIQgpgNBpjbUAL9cCNJQuKotDa2kpDQwN5eXns2bNnVqYnyY43V3T7u6l111KSUUKPv4cLfRco\nSiuKMd+xC+zIyAgulwuv10tFRQXLly+f86IhiiLR6PjOhpRhcBBrQwOSqkJJCcICtmFNFFk40XmC\nwdAgG/M2UjdYx+nu0+wr2ReLKqxIX4HVaKVusI7B4CC3r9HMbrJt2fSM9EwZXRiLk90n6RnpYZlj\nGQFLCINxBEGyUil0skNZSYmSjjAyglpRMefPNZ+IjyzoUYXCtEIcJgdNQ00TRhemQ3wBZVGRPs9I\n3AAAIABJREFUVvgoy3JMgdLn89Hf309XVxdWq3VcBGI+0gWLVbNwOZOF34XIQvw8Gj/3zGYeWiIL\n0yCZMLL+8EqStGA7AT1UrygKbrcbl8uFKIps27ZtRiYnMxkvlWThQt8FglKQFc4VCAgJ0YV4siDL\nMk1NTTQ3N8+6OHMyzFuKQFURTpxAPHGCzNGctdjcjHrTTURXrpyXMLOqqlwcuMj6nPUTTgR6VGGZ\nfRmiIGISTbzd/jZX5F8RiyrYTDYUVUFRFTp8HZzpPUOGRVNjDMthKvsq2V28m8K06Vu4VFS25G/R\nfrDmIvYHWdbWjsGiICmdiIOgrFmjtVvO8HMuVhpCjyqscK4AwGwwJxVdSAYGg4GMjAwyMjIYGBig\noKCA3NzcWPTB6/XS0dFBOByO2Snr5CEtLW3Oi+5i6CwsltRzsmmIVBU4LiZSeX2XyEIKoOeCFpIs\n6Df7mTNnGB4eZvXq1axYsWLeHr5Uhuz1qEKBQwvxpVvS6fZ3x6IL+lh6ysFsNrNjxw4yRmWEU4X5\nKnAUGhsRjx5FdTiIlJcTCYVQfT7cTz1F5ZYtRDIyZlTwlgxOdp/keye+x71b7yWPvHEkKBZVyNlI\nZV8lXf4uglKQX1T/guqBaqJylAaPZgdtFI3YjDYcJge3rbpUgS0KInaTPeG4k5Gt2yvGWPBuDGA4\nfRrx3DmQJKRdG5C3b4dZGOUsRjeEHlVwmp0xi+tMSyYNnoYpowu+iA+zaJ4RmdDHNJlMZGdnJxgX\nxdspDwwMjCug1KMQDodjRtdpKQ0xHj6fL+VzzkLC7/fz1FNPkZubi91ux+FwxFJidrsdm82GzWbD\narVOamUQjyWyMA2S2X0KgrCgdQu6pgBo/ux79+6dd5KSym6Ii30XGQgMoKgKnpAH0ISC6tx1XJF/\nBdFoFL/fz4ULF1i7dm1KUg4TYd4iC7W1IEmwbBn09RFVFGrDYdK7u7nquuuQt2+PhZz1grf07m6W\ntbWR4fViWr4c065dGLZuTUqHQFVVfl3za+o8dTxb8yz35t6b8PuBwAAnuk4QioY403uGc73nCMkh\nFFXBbrKzs2gnRnH8VFCWUcbNZTen5prY7ch79yLv3TvlnwlNTYi1tZCerpGJMZPYYqUhmoeaMYgG\nJEWKiVuBRnQbPA0TkgVZkbnvt/dRkFbAIzc9kvSYUy3cY+2UVVUlFAolGGjV1dUhCMI4QqoXUM50\nzPnCYpKF6RZHVVXx+XwxI70PI/r6+njkkUfIzc2NrU16B4TBYMBgMGA2m5EkiQ0bNvCjH/1oyuMt\nkYUUYSHaJ+OdE/Wd6KpVqxYkmqHv9lMRBk4zp028E1OhtaUVX7cPURTnnQTNW2TB60W1WIhKEsND\nQ4RCIQqLisgDJKORiM2Gw+Fg2bJRS+gLF+D114kMDREwm4mcPEn0xAkGr70W5dprp3VMPNl9krO9\nZynPLKfB08AZ4xnKSy7pHpgNZvat2IesyhxvP06mNROzwUymNZObS2/mQPkBzIaFiYhNikgE8/e+\nh/GVV8DvB6MRtbSU8He+g3LFFQl/uhhpiN3Fu1mfu37Cv0kzTbygHGk9wrm+c5gGTFT2VV5KyyQx\nZrILtyAIsR2ifj8pikIgEIjVP7S1teH3+zEajePqH/RF86NUsyBJEg7H5LbkOj7skYX8/Hx+8IMf\nxApq9X+BQIBgMEgwGCQcDjM4OBirnZkKS2QhRZjvyMLw8DAul4tgMBhzTjx69OiCRTP0hzoVZGFX\n8a5xr+kpB9WksnbtWlpbW+edBM1Xp4JSXIz/xAnavF5MFgvp6enkOZ0IAwOoY+tJJAnTu+8iAKar\nrkKfwpT2dnJ7e+kSxZhjYjQaTXBMzMjIwGaz8euaXxNRIuTYcvCGvRzpO8In5Esa706Lk1tX3Urf\nSB+/bfwtG/I2kG3NpsHTgMPsWHyiAJiefhrjc8+hZmRAWRlEIoiNjVi++U2CP/85jBaaLUTNgqzI\nuINu8h35sYXbKBrJs+fN6Bg/PfdTZFVGkiQer3ycH9z8g6TeO1cjKVEUSUtLS9gV6wWUegqjr6+P\nQCCAxWLB6XQSDodjOfqF0lu43J0uP+w1C2lpadxyyy0pO94SWZgGi+0PEYlEqKuro6uri9LSUsrL\ny2MP80JKMOsPV6qLkgKBADU1NXg8HioqKiguLmZoaGhB2g1n4zo5HbxeL7WBANkmE6vCYYIWC0GP\nByEYRF2/HmXVqoS/F4aGEHp6UPUog35uhYXYm5pYabFQsmFDgmPi8PBwLNxcO1LL8e7j5NhzCIfC\nFNgLqO928V7NS5Qs+1KCaNIbrW/gDrpZn7seURCxGW281vwaO4t2jqtFmC8IbjdCczMYDNq1cDpB\nUTAePAhms6a/AJoHRXExQns7huPHkW+9FVgY34QnLz7JC/Uv8Phtj8+anBxpPcKF/gtkW7OJKlHe\naH0j6ejCfCyi8QWUOiRJSqh/aGtro6GhAbvdnhCBSEUB5URYzMjCdIRIUZQPPVmASxoL+nVua2tj\ncHAQq9WKzWaL6XvY7dM//0tkIUVIdWRBUZTYw5udnc2ePXvGfaELaWClT16yLKekG0GWZZqbm2lu\nbqawsDAh5bAQyoqpHifeDru0rIzSv/5rjGfPEjp5EkUUUfbvR73qKi0HH/edqUYjmEwI4TBqfH40\nEgGTSVtAmdgxUZZlnn/9eSJEUGSF/v4OTO4BRNnDyx0/4JYjrZhu/Ti23btxh9y81f4W6ZZ0orLW\nLppnz6N5qJkTXSfYv3J/Sq7DpFBVDEeOYDx8GDweLaqTn490++0o69cjDA/DWNfT0ftMGBxMeHk+\nIwvuoJtfuX5Fu6+dg3UHOZB9YMbj6VEFRVWwGq1YVAtd4a6kowsLteM2Go1kZWWRlZVFS0sLW7du\nxWw2x+ofBgcHaWlpiYXtxxbkzvUcUzWXzGbc6UiKz+cD+FCnISBx3n722Wd5+umnaWxsZHh4OJaC\n8vv9/MVf/AXf/OY3pzzWEllIEVJJFgYGBqipqUFVVbZu3RorZhqLhTZ3EgQhJeP19/fjcrkwGo1s\n376dzDEV8QtFFlJV4Njb24vL5RrnTaEWFjJcUUHfwADLd+7U/nisrkNmJsq6dYjvvANpaRqZkCTE\n1lbktWtRp8glesIeekI95DvzUWUZg28ARQqQLljwWgVaG6so+XE71S0tnM73Mzg0iCIqBMNBjAYj\nCJpb5pmeM/NOFsSqKowvvAA2G+q6daiKgtDWhulXvyLyv/83SlmZ1ikRV/lPIKDVLowaVMH8Fzh+\n461vUD1QzfL05Txb8yzbt23HIMxs96tHFTItmbHzTTenJx1dWCwFR73gLTc3d8ICSp/PR09PD/X1\n9aiqGos+6P+12WwzIlayLMcswBcSMyELvws6C6IocvjwYf7pn/6J/fv3YzKZaG1t5c477+Spp54i\nMzMzwbtiMiyRhWmwkCqOgUCA2tpa3G43q1evpqSkZMpJY6E9KebaEREMBqmpqcHtdlNRUTGp6+WH\nJbIQDAZxuVx4PJ5JuzYEiwV1molJuuEGjENDiPX1WtRBEFBWrpzWZCnXnsu/3fJvhKUw4rlzmN7+\nOUppKT2DHjIcaazeUohQXU1WJELhFR9nbe9a/H4/fr8/Zs2clpbG8pzlhMPhpNqnJkIyz4h49ixE\no6h6GkYUUcvKEC9eRKyqIvpHf4SlpgahrQ01KwshHEYYHkbatQv5mkvFsPNZs+AacPFa02tElAg2\nk42+kT5ebX+Vjy/7+IyOc6j+UEKnjw6DaOC3jb+dlizMtWZhpoiXAx6LiQooVVVNUKBsb2/H7/dj\nMBgS0hdOp3PKe+pylnv2+Xw4HI5FOb9UQierL7/8MuvXr+fRRx/la1/7Gk6nk6997WvcfPPNPPTQ\nQ4yMjEx7rCWykCLMZeGOFx4qKipi7969yfW9LmAaAmYfyVAUhebmZpqamigoKGDfvn1TFi/qi/h8\nF7PNtsAxXi2zoKBgyq6NsdGLCT9PVhbS3XcjNjUheDyoaWkoq1dDEgqcugGXYeQ8RsmOas4FJUK6\nOloq6XRi6elhReEKVhSuiJ1/IBBgeHgYr9fLUNcQ79S/Eyt2mw+1QGFoaFwbJIIAoogQCCB98pOE\no1FM//VfiB0dYDYT/cxniDzwwIRmVfOBf/ngXxiJjmA1WOkP9JNhyeCV9lfYmz11u+dYfGX7V/j9\n1b8/4e825W2a9v0LHVnQn4GZdGDoBZSFhYWxY+jtwF6vl6amJkZGRjCbzePuKT31cDnrLOjqjQtt\ncpVq6HOP2+1mxQrt+R8cHIzNV1u3bsXtdnPx4sVpiyGXyMI0mElkIRwOz+jYqqrS09NDbW0tFotl\nxsJDC5mGgNkJMw0MDFBdXY3BYODqq68mK2t6G2Z90ppvsjCbAsehoSGqqqpQFIWrrroqQTBnTmOY\nzSjr1k36a6G+HuPRowgeD0pZGdJNN0F8Z4V+34TDWHt7sbS1IaaloQYCKGvXjjsnfbJfvlzz44gv\ndotXCxzbfTFTsR8dSlkZhnPnUBUF9EUpEkEVBNSCAhAE5NtuQ77lFoS+PlS7fULBpvm6J1wDLl5v\neR2TaMJitDAcGibbmk1/qJ+jPUfZze6kj7U6azWrs1bP6jwWWjYeZk4WJoIoirH7RId+T+n3VVdX\nF6FQCJvNFvPACIVCC0oa9E3IdCTY7/d/6FMQcGn9ysvLw+12A7B+/Xpee+01zp49S3p6Ou3t7ZOm\nuuOxRBZSBKPRmFQoR4fX68XlchEIBKioqJixvTIsPFmYSRoiPuWwZs0aSkpKkv58+qQ135PmTCIL\n0Wg01pVSXl5OWVlZUueWiroIw6uvYn74YW13rh0U43PPEX7ooVg+X964EUNBAYaXXybT7cYgioiy\nDEYjys6doKpTCjzFF7vpiO++6O3tpaGhASAh1Jyst4ayfTvKqVMI1dWaWJUsI/T3o2zciLx5c/yJ\nTFmnMV9k4bFzjxGSQ5hEE2E5TESO0DLcQqYxk3cH3k35eJNBv1cWOg0BqZUGhonvqUgkktCB0dHR\nQWtrKw6HI+G+cjgc8/Ls69HfZGoWfhciC/rn/MxnPsPZs2fp7+/nj//4j/nNb37DH//xH+PxeFi9\nejU79ZqqKbBEFlKEZGsWIpEIDQ0NdHR0sHLlSq666qpZh3oXo2ZhOnKiKAotLS00NjaybNmypFMq\n8YgnC/OJZHb9uhBWTU0NTqeTa6+9Nqk2Ix1JpSGmwtAQ5h/8ACEQ0PL9gqAVQDY0YHrsMSL//M/a\n3zmdKKtXY3zpJVRRRDWbUdPTISMDw5kzSI2NqKtnttsda7dc664ljTSEsBBzS9TrH86fPx+LPkyU\nvlCXLSN6770YjhzBUFsLBgPSgQNIN94I0wjkCL29CK2tmtbCPJDjbn837d52NudujqV1AtEAnpCH\nTxR9gm3Z21I+5mSYr4V7Kujt0AuxMJrNZnJycsjJyaGnp4eKigocDkcsoqWTUlVVxylQzrSAciLo\n89d01/d3wUQqHnv27GHPnj2xn5955hkOHjwIwN13370UWUgFUlXgqCgKHR0d1NfXk5mZybXXXpuU\nith0Y8409TEXTJeGcLvdVFdXI4pi0imHycYBUh81CQa1ne3goLaIFhdPSUhGRkaorq7G7/ezfv16\nCgoKZjxZzTWyYDh1CqG/H7Wk5FJkwGhEzc7G8MEH4PHEtAnElhaUigp8BgNWkwn7smVgMiFWV2O4\neBFphmQhHu6gm79582+4quAqvnXtt2KKb52dnXR2dpKRkYHX66Wzs3Nc+iK2U1yxAulP/gQpENBS\nEdNVwkejmJ56CsPhw7Gah5KcHHxf/CKUls76s4zF8Y7jjERHUFFxh9yx100GE+6Qm5K0kpSNNR30\ne2Wh0xCLJY5kNBrHtQSrqpqgQNnR0YHf74+5dY5VoJxpB4bRaJz2PXpk4cMOvZjzoYce4o477mD1\n6tXIsszKlSv5yle+AkB9fT1Op3NaEbwlspAiTEUWBgcHcblcyLLM5s2bYw/FXHG5pCFCoRA1NTUM\nDAwk1cUxHXT98pRGFvr6EJ98EnG07UsA7AUFWNaPl/BVFIWmpiaampooLi5m69ats+4Hn3MaQpK0\nFMLY62kwQDSKIEnEjh4Og8mE7HAgW60xjQZgfMtmPHTCOUUE6FD9IVqGW/CGvXx2/WepyNaspUVR\nxGg0snLlyrjDhWM7xb6+vthOUZ/oMzIytEr5aVIKxldewfjss6hZWSgVFRAM4nC5sDzxBOiaFSnA\ntcuvJcs6MbGVBqRFSQks9JiLQRYm64bQO3UcDse4Ako9hTG2gDKeREz1rEqSlLSJ1IddkAkuGQ5+\n4xvf4Prrr2f16tXjPv/GjRupqqpizZo1Ux9r3s7yI4aJyEIwGKS2tpb+/n5WrVpFaWlpSh/KxSAL\n8ePFdwUsW7aMPXv2pKxvOpXGVQDiiy8i1NSgrl0LZjOqJGGoqmKZ2w133hlrUXS73VRVVWE0GlPi\ndDmWLKiqOjF58PkQGxoQurvBbkcpL0ddsQJl61bUzEyt6K+gQD8IwsAA8s6dqHHhQ+XKKzFUV4PF\ncolAeL2oZrPWXTH23Hp6tLTAxYtageGWLcg33phwTNCiCs/VPkeGJQNvxMsvXb/kW9d+a9LPPDZ9\noe8U9e6LlpYWRkZGMJlMCdGHBKlhScLwP/+DarWi6uTa4SBYXExaczNCZSXKNeP9RYTubgyHDyPW\n1KDm5CDv24dy9dVT1msUpRdRlF7EcHgYu9GOyXBpsakJ1fxO1A9MN+ZiRRaSHTe+gDK+KDe+A6O7\nu5tQKITVah3XgRGvQJtM2vd3hSy88cYbZGZmkpaWRm9vLy0tLZhMJsxmM2azmeHhYWw22zitm4mw\nRBamQbITRfxCGq9OqOft50N8ZDG7IdxuNy6XCyCproDZjJUystDfj1BTA8uXX9ptG40oJSXYKyuh\nvZ1wYSG1tbX09vbGCjJTMYEmFVnweDC++ipCWxvYbAiRCOL588h796JceSXS3Xdj+ulPEZqatPMP\nhVDz84ned1/CIijv34/h9GnsJ08iZmQgmEwIkoR0ww0o8UWEAG43pp/+FLGpCXV0UTe++ipic7PW\nrhg3UR6qP0TPSA/lGeUMh4d5o/WNhOhCMtdA3ynq6QtZlhO6L/RKebvdjtPpJNNopGRgAMMY1z/V\nZEKQZQSPZ/w4jY1Y/uEfEFpatKhDNIrxyBGiX/wi0h/8wZTnGJJC/MfZ/6AiuyLBXnshvCji8VFx\nfxwrQzwbGI1GMjMzExa6aDQau6d0T5VIJBJLi8WPP9V19vv9MbL7Ycbf/d3fAdrn+e53v0t6enqM\nKFgsFlwuF5s2bVoiC6lCMhO+0WgkGo3GWiFNJtOc8vbJYCFtsUEjJ5FIhMrKSvr6+lK6qI5FSslC\nNKqF88fk5ASzGUGS6G5tpaqhgZycnJQTu2TuHcOFC5oY0Zo1YDCgAkJfH+LJkyhlZUS/8AWt9fDV\nVxF7e1E2bCD6yU9qfx8HNSeHyNe+Rt+TT5LT2oqSl4e8Y4dmCz1mN2U4fRqxqQllwwat2BCZ1hyB\n8tpaDOfOIe/bB1yKKqSZ0jCIBrKsWTQMNUwbXZgOBoNh3EQfn77oHRrCbDDgaGoiqiiYzWZMZjPC\nyAiq2Qyj4el4mJ5+GqGlBbWiIhYpErq6MD39NPLeveP8N+JxtvcsLreL/kA/1xZfGzONWmiysFjq\njYtBUGD6roSZwmQyxQoogZinik5M+/v7CQaDvP3227ECSj2FoTv5ghZZWDXGx+XDiC9/+ct4vV5a\nW1vZO2oPHwwG8fv9SJLEgQMHeOCBB5JKsy6RhRQhFAoBUFVVNamaX6qxkJEF3eZ0aGgoJkQ0n1Kt\nKSUL+fmoBQWI7e0JC6zc0UE4I4P2YJArtm1LWS1JPCYkC9EoQlMTgterRRJqazWZ47iJU83LQ6yv\n18hBZibyddchX3fdtOOpOTm4b7oJKSMDS0lcYZ7fj+H99xEbGlBtNi2iYLXGxnTRz+umRj5uN7Gq\nvT32tkP1h+jwdVCUVoQvokng2oy2WHQhndQVgY1NX4j33Yfh//5f5MFBgmlphP1+jAMDdG/aRK8k\n4WxujnVfmEIhDOfOQW5u4nUsKNCu4/nzyDffPOG4ISnEm61vYjPaGAgO8E7HO7HowmIIJC10u95i\nkAX92Z7viEa8p0peXh5msznWLqgT087OTmpraxEEgZdeeolQKMTw8DCSJM2JLL799ts8/PDDnD59\nmu7ubg4ePMjtt98+5XveeustHnzwQaqqqigqKuLrX/86f/Znfzar8QH+8A//ENB0Fj796U/P+jiw\nRBbmjGg0SkNDA+2jE+yOHTsSrGHnEwtFFgYHB6muriYcDpObm8uWLdM7580VKSULRiPqLbegPvUU\nQnU1cno6vo4OvOEwfXv2sGP//nmzwx5HFjwezAcPYmhqAv3z9fWhrl8P+fng92umUqPtmeosJvFx\nk9vQEOYf/hDD+fOa9LQsIwwMaMWQq1cTFRQ+oIMGBjllEilNu9Slc6rnFFmWLILRYOw1o2DEIBqo\n7KtkT8aeeVvclOuvR/B4MP/Xf2FtaQGbjc6rryZ4771k5eYm5KnTBIFtfj9GoxExGsWkV7yrqla/\nMdl1HBnhbM1rNPa5WLVsPZ6Qh3c63olFF5YiC/ODhWzXjIfeHWC327Hb7RSM1gHpm6GmpiaOHj3K\nxYsXOXr0KD/+8Y/Zvn0711xzDZ///OcpnUEXzsjICFu2bOGee+7hjjvumPbvm5ubue2227jvvvt4\n6qmneOedd3jggQfIy8tL6v0TQU8xffrTn+bFF1/k1KlTpKen88ADD2A2m2O1GcmQtiWykAQm2h2q\nqkpHRwd1dXU4nU52797Nu+++u6A3/3yThXA4HMvjr169GkmSYhGU+Uaq/SHUK69Esdvxv/Yag5WV\nyGVl5H784wz6fPMuKR1/7xiOHIGaGpQ1a7S8ejiMob0d4f33Ubu7Ebu7Y2kTZfXqS8V9M0T8mMYj\nRxArK5ErKi65WNbVYbh4EaGmhpq1mTQxyPohIzXOEHVlGejxl4euf4jh8HD8gTG8/TaG//kflv/i\nKQLFbzGyaxdceeWsznMqCJ2dGN97DwFiRZfWri4y2trI3nZJ+yASieD1eglt3UrakSN4DAbU0S6N\nNLcbMT2dQEVFYveFLGM8dIjo4Vc4ZjuN3RbFVhTFtGkzVb76WHRhMXwaPgo1C5eb1LPelnnPPfdw\nzz33sHv3bv7lX/6F0tJSTp48yQcffIDH45kRWbj11lu5ddRaPRn85Cc/oaSkhEceeQTQlBZPnTrF\n97///VmTBT11/Pjjj/Pwww8jSRJer5cvf/nLDA0N8ad/+qdcffXV0zpOwhJZmBU8Hg8ul4toNMqm\nTZvIz89HEIRFqSGYj/Hi7bFzc3NjKYfm5uYFdblM5VjBYBDXyAieDRtY+6lPsXL5co2MHD48r06G\nCWTB7UZsaEAuKrrU9mexIG/ZgumFF6CzEzU7W6svUBREtxuxpgZlx44ZjxkPw/vvozqdCTUb6po1\nqN3dSF4PJ7vrsZhCZFoL6F1byklDN+WKjEE0kGZOI818KVJm+tnPMP3Hf2g1IDYb9qZmVr//PobC\nQuT9STpXDg1hOHECsasLNTMTeccO1NEK93gYf/MbxLo6lPXrtWuiqgjnzpHxwguwf3+sCFN3ShT+\n/M+xDAxgb2xEBuRolIjVSvP+/bQ0NGBsaYlVyBe8+y6Zv/417xdEqMuQKA1YiLouooSDZG4ujUUX\nFqPA8aOQhphJJ0Sqx52uG0JVVfx+P/n5+ezevZvdu5OX+p4L3nvvvXH+DAcOHODxxx8nGo3OuH1b\nv3fr6+t59NFHefjhh9m6dSv79+/HaDSSm5vLzTffzPPPP79EFlKNUChEXV0dvb29lJeXU1pamsBS\nF1pR0Wg0ptxwyePxUF1djaIo4+yxU72AT4VURRbGtnfGmz4thFJkAlkIhxGiUdSMDOK/LWHUL0G5\n4gpIT0c1GlFzchA8Hgzvv49y1VVaGH0Gk2syBEjNzeXC7++k3lRDubWIaOFyCs0iDZ4GmoaaWJOd\nWEAp9PdjeuopMJlQi4sBiGZkILa2YvrP/9SKIqeZiIX2dswPP4zY0KDpR6gqxhdeIPLnf57YCjk8\njHj+vNYuqh9TEAgVFGDr7UWoqRnXOqmWlBD63vcwvvEGYmMjQmYmxj17KNu4kRJZjrXZ+fr6iDz/\nPP2BAG85giBBm03CYAJxoA5lOA1LRi6uARdO1fk7H1n4qEQzQEtDJKMouxitkz09PTFnTx3Lli1D\nkiQGBgZimhPJQl8XWlpaEASBO+64g0OHDiUIWRmNRgYHB5M63hJZSAK6SE9jYyP5+fmTFvcthgsk\nJN87PBXiUw6TaUKkWvtgKqRirHjTp23btsUqpHXoD8x8k4XY8XNyNBLQ1oahowNDTQ1IklZoKAgo\nmzZB3O5BlWXE9nYML76I4PejOp2o69drmgkzmNzl7dsx/eIXyNFo7PjCwADRNDvvFylExFy8ablA\nGCQISAFOdp+kPLMcg3hpQherqmBoSFOTvPQBkTIysLa2IrS3x7wqJoSqYnrmGS1asHZtLFogNjZi\n+tnPCG/aBLqU9iiRGPc59dcnI0M5ORO2SRoMBjIyMsjIyEAwGLCYzcirVvF5VaV/aIRINEo0HMbR\n1UX3mm1QvpWVhpX0S/3JXOKUYbFqFj7qaYixWCwFx7HENBVeIZFIJLY+6G3M+j2my/IngyWykAR0\nQ6Tp9AQWIw0ByfmzTwZFUWhvb6e+vp6cnBz27NmDbRJr5IXsvphLZGEmpk+zcZ6cCRIiCxYLyrZt\nmH72My0Er0c4QiFtkezsTJAxFjo6oLcXsaUFsrMRurqgrQ38fpRtk/sVjJ1YpP37EauqEC9e1FIR\no22kg7fdQDRLJj9qRFIu3bfL7MsISkECUoB0c9yEabFonQaSlNBxIMiydtzpumMGBxG4iBmTAAAg\nAElEQVQrK8dFC5SSEsSWFkSXS4uiAGRkoKxbh+HddzU569HvzzwwgJKTgzCN2txUUJ1OSEvDEAiw\nPLOI5RatvVn1elGdJvLX7cbtyKa/ux+v18vIyAgDAwOkp6fH1Cdnq+g5HRYjDbEYKYHFICiQ3FwZ\nDoeJRCJzFmSbKQoKCujp6Ul4ra+vD6PROG6jkwz07/TKK6+krKyMb3/725hMJkRRZGRkhBdeeIHX\nX3896W6LJbKQBCoqKmISxFNhocmCXk082wVcTznIsjwu5TDZeJczWYg3fUpPT0/K9CllstJ+P+I7\n7yCcOaNV4G/bhnLttQhjyIjQ04Pg96OM1rmoZjOq3Y7Y2IjxzTeRPvlJsNsR3G7EtjaUsjJNN0B/\n/8AAYmWlViA5xc4ngQDl5BD5q7/S6gRqa8FuR962jYytW7lHlVHU8Z9fFMQEJUMAeetW1BUrEFpb\nteiCKIIkYRweRj5wAHWaMKkgy6Ao4zs8DAatMyT+2REEpE99CrGtDbG6GtVm09I4ioLvtttwzkUE\nLC0N6YYbMD3zjJZSycrSWks7OpB37yZnxw5yRp/1U6dOkZOTg9FojBkdBYPBmM1yvEpgKhbcxUpD\nzBf5mQyXc2TB6/UCLHgaYteuXbz44osJr7322mtcffXVs/5+VFWltLSUL37xi3zve9+jv7+fSCTC\nzTffTFVVFV/60pf40pe+lNSxlshCEjCbzUmRgIUmC/qYM13AI5EItbW19PT0zMhueSHTEDMlC7M1\nfUpJZCEYRPyP/0A8eVKLEAgCwvnz2r977kk4vnjxoia8tGoV6mitAoAyPKzpL4yMIAwOolitKOXl\nKGPaVNXsbITGRgSPR3OVnAATfu6MDOQDB5APHEh42dzbj+H4ccTaWlSnE3n7dpTt2ydOc9hshP/m\nb7B85zuaSqIgYJIk/CUlWP7yL6e9TGpuLsqqVRjOnkXJzIypTwpdXdrvKhIVIdXVq4n87d9iOH4c\nobERcnJocTrJvu465jqNS7ffjjAyguHYMcTGRlSbDWnfPqJf/OI4aWiHw5GgwRGvEjg4OEhLSwuS\nJJGWlhaLPMzWJfGjVLNwuRY4+kdbcCeLsCYLv98fs3UHrTXy3LlzZGdnU1JSwje+8Q06Ozt58skn\nAfizP/szfvSjH/Hggw9y33338d577/H444/zzDPPzGr8+Fq222+/nZtuuoknn3yS+vp6bDYbjz76\nKNu3b0/6eEtkIYVYDLIwk9SAqqq0t7dTV1c3bcphrmPNFcmSBb2epLm5meXLl8/Y9CkVhZTCqVOI\nZ85ogk96KD4cRjh7FtPoYq8/uGp8cVXcZCkoCkp5OdE//3MYGUG12zG+/DKCLJNAZSIRre5gzGcU\nGhsxVFaiOp0IubmJ40x23h0dmH74Q8SWFlSHAzESwXDyJNLHP67l/SdY6JQdOwg98QSGI0cQ3G7c\naWm0rFnDFVPVKsR9Xumzn0Vsb0d0uVAdDoRgEGw2op/5TMw9Mx5qYSHSZz4T+3nk1ClyUrHIWK1E\n770X6WMfQ+jtRc3IQF25ctxnnigtMJFKYDAYjBGIeJfEsd4X0+l5LNUszC+SMZLS7ann+j2cOnWK\nG264Ifbzgw8+CMAXvvAFnnjiCbq7u2lra4v9vqysjN/+9rf81V/9Ff/2b/9GUVERP/jBD2bVNqnP\nN52dnbz11lt4vV42bNjAAw88MOvPs0QWkkCqbKrnA8l2YAwNDVFdXY0kSWzZsmVWuueXWxoi3vTp\nmmuumVWOcc6ukIBQX6/9T3zO3mIBoxFDfT2sXh17eJX9+xGffRahowO1qEgjDB4PyDLy/v2oOTkw\nuggpa9ZgeO89cDi0Y0sSne1V9BdmsEnf6UajWL71LYwHDyIEAqgGAxszMxk+cABTdjbk5CDv3Imy\nceO4hdD42muarfWGDSCKMZlp4+uvayZVK1ZM+HlVkwll2zbUjAz8qorc15f0tVI2byb87W9rHQv1\n9cjLlmkeGFdfndT7VVVlZERgAmsITCaYqR6aWlBwyaBrkvGme/4FQZhQ5Cfe5Kivr49AIIDVak2I\nPqSlpSUsXh+l1snLOQ2RCmG966+/fsq55Yknnhj32nXXXceZM2fmNG58F8RXvvIV3njjDSwWC4FA\ngP/zf/4PDz744LTp2YmwRBZSCIPBQFi3+13AMadawCORCHV1dXR3d1NWVkZZWdmsH9KFTkNM9rni\nOzfm6k+RkhZNi2Xi6nxZjhEIfdJQt28ncvfdWJ5+GqGuThMcMpuRr7+e6Kg0qw5l61YEr1drM5Qk\nFFRey+inIydKYWSIHFsOpscew/iLX6BaLKj5+QihENb2diz/7/+h7NkDgOHNN4l+7nOJKYhoVGtN\nzM1NiHCoeXkILhdiYyPyWLIQDmP81a8wHj2qdWfYbGRVVOBOQoY6HuqqVURnqbsfCIi89FIaMD56\nlJ4Of/AH0RkThqkw27bk+KiCjqnSF/rfhkKhpQLHeYKqqkmlIfROiIX+HlIF/Z79yU9+QltbG9/9\n7nfZunUrv/zlL/nhD3/Iddddx969e2d8by+RhRRioVsnpxozXmEyKysrqWK/6aATk4UQqhFFkWg0\nmvBa/GfKzs5OiT9FrMAxGkWorNSsoHNzUbduHWc8NRnUTZvg8GEYGNC8CQAGB7Xiuc2bweNJ2GFE\n778f9dprMbz9NkSjyFdcgXLDDeM1Cux25JtvRtm0CcHnwxXpxOXuIaj4Od19mlvKbsb09NPaYq/X\nLwSDqKKIMLpDVdatQ2hvx/jcc8jbt2seFKC9x2CAYDBxTP08J5jIjc8/j+m551Czs1FKShB8PhzH\njlE4NAR79kxpAz0hFAXx/HkEt1sr5Cwvn/YtkiQwMmIgOxtstkvXNBgU8Pk08ctUIpVpgYnSF6FQ\nKMF5Uy+u06vxk01fzAWLFVmYa7v3bMaE6f0ofpfsqT/3uc9x//33A1oB5aFDh+jq6gJmToSXyEIS\nuNzTEGPJwvDwMNXV1UQiETZv3pwyg6R4EaP53hWMjSz4fD6qqqoIhUIp/0xCXx+Ghx9GuHABQZI0\nUaT165H/+q81W+tpoG7ejHLrrQivvYbQ3Q2CgGqzoRw4oJGON96I7Wrq6+sZHBzE6XSS8dnP4nQ6\nsVqtk99jBgNqcTGyqnDi4gcgGiiwF3Cq5xRXZW3EMTgYa8FEURDCYRSjEUGSIBDQzq+oCLGuDkNd\nHfLOnbHjyjt3Ynz2Wc2iejQ6IrS3a8WG69cnnoffj/HoUS23P9qXrebkEA2HSa+p0TokJpDCFbq7\ntQLF/n7U/PyY+6PQ3o7lm99ErKrSvDAcDqSbbiLyt397SWthoms9SmZsNhWHI+E3hMOpJ7DzSYwF\nQcBms2Gz2WK97vX19YRCIbKyssalL8Z2X6TqGfyo1Cx81MhCT08PV1xxRcJrDocjRtJmShCXyEIK\nsdhkIRKJUF9fT2dnJ2VlZZSXl6f0gdSPtVBkQVEUJEmisbGR1tZWVq5cyapVq1K6IxEAx3//N8Kp\nU1Berhk4BYMIlZUYfvxj5H/8x+l3zKKIcuedCFu3ag6SgFpRgVpRgTC6uLndbmprazGbzRQWFuL3\n+2lra8Pv92MymTTyELeTHHt96wfrqR2sZXn6cqxGKy63i9OeixSXlyNWVaHGx95H0yqqXjA4aqak\njtVfuPlmrTDy/PmE90h33hnzYohdp+Fh8Ps1Oeo4KGlpiB0dCG73OLIgXriA+fvf1/QhRBEUBePL\nLxN58EHM3/ue1hWRn6+1RXq9mJ5/HrKziYwWgk2OhTV2WmgjKavVSvGoQiZo6QvdYnloaIjW1lYk\nScLhcCTcM/EWyzPBR6VmQZIkRFGc9rMuliBTqjEyMsLhw4djRZ0lJSX09fUxODhIf38/JpMJi8WS\ndJH7EllIIRardTIajdLR0UFtbS2ZmZns2bNnzimHiaA/ZLIsz3tftsFgIBgMcvz4caxWK7t27ZqX\nB9jqdmO+cAGKii7taG02KC5GuHgRGhoupSOKiycMz8ujPgrq2rWoa9cm/C46arx14cIFKioqKC4u\nJhqNJlxLfSEYHh6mvb2daDSasBCkO9N5r/M9UMFu0s4xz57HqZ7TXHPvH7L863+PMDCAmpaGKgiI\nkQhSTg6UlFyKFixbhrJuXeKJZ2YS/cu/RDl7VhOAstmQr7hC6woYAzUzU+u0GB5OICaiz4dkt48j\nF0gSpscfR+jp0cYdJQtiXZ0m91xVhVJQoF3r0XNRo1GMhw4Rue++STUk5lNAayIsdMGhqqrjFlGT\nyUR2dnZMEG6i9IVusTy2+yIZaePFqFm4nM2rPuxkQb9fKyoqePXVV3nrrbdQFCWWsv73f/93fv7z\nn2M2mwkGgxw6dIisCTqRxmKJLCSByzkNIUkS/f39CIKQYGo1H5irCFSyCAaDdHR04PP52LhxI8uX\nL5+3z2SKRLR2xLHs2mqFpiYMjz4KutNmaSnKHXdodtL6uUaDfOvNb3HbmtvYX3rJSEkXiHK5XABs\n376dzMzMS8WUgQCGs2cxNjZisVjI3rwZZdMmVLQCTp08dHV14ap0cXTgKCaLiYAvgNlixmK2MBgZ\n5IOtV3PbP/0T5h/9CKG3V9NCyM4mmpmJ/fx5LSWSn4/0h38IE3WL2GzIyRjlOBzIN96oeUMIgqb3\n4PNh6ulh8MorscRLQANiUxNiczPKihWXCihFEWX5cgx1dQgjI1o3SBxUmw0hEJhSQ0Lb6U9/uqnC\n5WgkNVH6QrdY1glEU1MTIyMjWCyWhOjDROmLxdJ2uJzJwoc5DaHfP//6r//K0NAQwWCQQCDAyMhI\nLEoVCAQIhUIMDw8nvbFcIgtJIpkWu4U0kopGo9TX19Pb20t6ejo7duxYkIdvPjsidLfL+vr62OQW\nH46dD0Ty85GzsqC/X9uJ62hrQ3C7ob9fK7xTVYSaGsTHHkP++tdhVK3wjdY3ONl9El/Ex+7i3ViN\nVgKBANXV1TGyc+7cuYQdnuzxYP7ZzzBUVmoP9qj7pfTxjyN98pNYrVasVmusLsMx4MDf5CcQDBAM\nBgmNhIgORcmx5NDV0UXr3pvJuOUW0gYHEZxOeg8eJOfFFxF8PlS7HXntWuQxucvZQPrkJzXFxtdf\n1+SqbTb8N95Iz9695I1d4EbVGseJO4mipgFhtYLfnxBBEHw+rbh0mrZeQRAIBgUgscBxPrAYZGE2\nC7dusZyens7y0Tob3Y5YT1+0tbXFolbx0YfLeZefSiQri+/z+VJWE7WY2KnXJ6UIS2QhhdDDPPM5\nwaiqSmdnJ3V1dTidTkpKSohGowv24M2XMNNY06doNEpzc3PKxxkHhwPfzTdjP3gQoaEBNTMTvF7o\n7YXsbK2bYfS7VNetQ7h4EfHkSZRPfIJgNMjBmoOIoki9p56jzUdZb1xPQ0MDhYWFbNmyBZPJxNCQ\nleZmsFoULOdPkv6Ln6LUXcB75W7EzHTS0jR9A8MrryBt3gxj2grX5a5jXW5iCkGPPugSxPVeL4Ig\nsOL4cQpefpmI1Up4wwaMkQiGc+fgZz8j+pd/OWEaJWmYTEh33YX0e7+H0N8PmZl4IhHk/vFmS0pZ\nGUpREWJnJ0p5uXYNVRWxu1szkdqwAeMbb6BGIlpEwetFiEaJ3nXX+ChPHAwGBYdDIRxmXEFjevo4\nrao5Y6FFklK5yzcajePSF/H3TU9PD3V1dSiKQk1NDVlZWTNKX8wFl3Pq48OehpgvLJGFFEJnrfPV\nFuT1eqmuriYUCrFx40by8/NpbW0lOLb9bR6RamGmyUyf+vr65h7BGB5GfPVVhKoqSE9H2b8fddu2\nhIJFQRDw3XQTuStXIr70kqbmt3IlrFyp7XzjSZ8gaPULvb2AFlVo8DRQnlVO82Azjx1/jC+XfznB\ncKynB374gy2US+18s+lLrPefQAFEwOyq5u2CP2DLHSXYc3MRXC5Ul4voihWxlI8gCBNOqhaLhby8\nvJi4lqIojPh8mF96iSjgz87GMzioydamp+P44ANCZ89i3bZt7pN0ZqZGqgA6OycmxlYr0t13awqR\nNTWxFIOanY30uc8hr1+P+uijGA8fRvB6UTMzid51F9HPfW7Koa1WidtvD2K3j28lnI0o03RYjALH\n+VpEBUEYF7WSZZm33nqL3NxcgsFgQvpibPdFKue0yzmy4Pf7P9RpiPnCEllIEsmkIfQbcS4ukBMh\nGo3S0NBAe3s7paWllJeXx46/GLbYqUhDjDV92r17N464XrgJxZIkSfMI8HjA6URdvXpyLYTubowP\nPqgRBW1AxN/8Bvl//S+UP/mThHFUQL3lFuSbbtJ0B2w2xIMHEX79a013QF8sVFWrb8jPj0UVTKJW\nR2AJWugVewkVhRJ2coF+P5/pfJy7PD+jJNKkjTk6tpEo1/X8mqHAX2BMtyKIIuIoOVBVNeHzjyUO\nYxcUURRJNxiwBIMM5eXhSEsjKzOTcDhMKBwm2tVF0/vvM+D3J7gnZmRkzNsuUt67FzUnB8PRo4jt\n7cglJcg33hgrtIx85ztE/+IvYHBQq19I7IWcFGlpE5dfpBqqql6WNQvzgaKiopiWgyRJsaJbr9dL\ne3s7kUgkQTzK6XTicDhmfa6Xc+rjw16zMF9YIgsphCAIKa1bUFU1VumsL6hjZUgX0q8hVeMlY/o0\nLoLh8SD+6lcILpeWDx81Y1I++1mYIL9o+K//QrhwAbWs7FJsuqcHw+OPo1x3HYx6GSSQElGMLVjK\n9u0Ib72FUFurOSzqXQUFBShXX80brW/g6neRIWcQMUVYXrgc2S9zqO4Q+0v3YzVakWWZ9Bef4Ybg\nUYoireMa/gTAgIRYdQ5FXIUxLQ3D+vWIFguKosQIg/5f/V/8NUogETYbamYmhv5+JKcTURS1QjhA\nzM1l8759+MvLGR4exuv10tzcPK4ILiMjY5wE8VygbNigyUlPgnh562SwkN0Q+lgfhpqFuYwHidoD\nRqORrKyshAr5cDgcu296enqoH5U4T09Pj5GHZImnfj9fzmRhKQ0xHktkIcVIVUeEz+ejurqaYDDI\nhg0bWLZs2YST1kKThbmkIXTTp6amJoqLi6c0fRrrBim+8grCuXOaWZNuV+z6/+y9eXRkd33m/bm3\n9iqV9larWy2pd6kX9Wr3apsJix2TmXhIAj4JwYwDwxCTmWEYTiAZHAIOYcs7OATMMnNmeJMMYAhr\n3pADTsjEbbxgG9vdrbW173vt66177/vH1e/qllSSqqRSSXb0nKNjS12le1VV9/6e3/f7fZ6nE/mH\nP0R717uy2wWqivzTn6KXl2c3sevqoL8f+dln0RbIwooVo6YmtHe/G/k734GREcCwKdbe+lbSu3bx\nV9/+K4KRIJpXwyW7mA/No+oqI+ERnh19ljv23YEeCFD2wlNE7VU4yP2aaUjYhvuYdWaYv3yZeDxO\n5fAwFRUV+P3+rNfHShjm5yGV0tHNeGkVSZKoPHsX7vZ27DMzUF5uJGIOD6O1taG3tuJzOPD5fOxd\nUCIsHYITGv6li8CqxlElRCl3+ltBFraikgFrG/S4XC7q6urM9oWR0REzVTuDg4NEo1GcTucy9cXS\nKmsuglIK5FPx1XWdSCSyrpyZ1zp2yEKeyPcC3mhlIZPJcOvWLUZGRmhubub8+fOrfsBLqcAQx1tP\nG2Jubo6Ojg5kWebChQtUip73KscxScncHFJHB/q+fYvDby4XenOzEeI0Pp7ttKjrRvVh6Xsmvl+y\nO1/p79FPnUI9dgwWkuH0pibGp6fpunaNe+rv4W1n34bTYZRudXSzbN1abZTZ7bEYeipB2FZH1FaO\nTw0vqy7Y0FHveCOVD70D/cAB5GiUubk5+vv7jcqE309lZSUVFRXmoj0/D3/+5w6CwYW8CV1fcGnW\nKfO8kd85105zzy+grw9cLjLnz6P89m8j5SBmS4fg+vp05uYUwuEoExNRotF54vFRysslWloWzaOK\n3cMuBK9lslDqyoKqqmZ1qhBIkkRZWRllZWVZxNPavhgdHSWVSi1TX4h2x1YMOOZT+dhpQ+TGDlko\nMtZbWRA9/O7ubnw+X86Ww0rH285tiPWGPpmZDWD4HKRSsJRguN3GDIHwQRCw29HuuAP5O98xzILE\nDmZuDsrK0C0Jh2vOojgccOgQ8Xic9pdfJhqNcuLECd5Q/wbzIcLK2bq4SJKEXlOD6q+kQp2no+IS\nF+Z/kvWrM8iMuQ8T+8ij7D9ipwaosezc4vE4oVCIUChEf38/0WgUl8uFqu5ibOwgfr+DqirHwt8A\ns7NJ+odCjP/KPTT/zttIzs0Z0sl9+4wWSzptnluu4cn+fnjnO71EoxJgfa11PB6VP/uzISRpltHR\nUbOHLchqLBZbt4NgIdiKNsSrVQ1R6uOt1L6wqnZ6e3vN17W/v9+sQrhcrk3/7OQzeC78KnbIwnLs\nkIU8UYgxU6GLt2g5xONxWltbc/bwV8J2bUNsNPRJVDB0XUeqrUWvrTXyBaxDcNPTRjDSgjGNFeo7\n34n00ktI/f3GEGQmAw4H2m/9FvrRo1l/z2qVEk3TGBoaore3l71792a1TkQlQbQGxAyBCZ+P9Bvv\noeypvySqeXnZf5Vj0Rdw6Sk04OmqX+HLp7/In/iXX4aSJOHz+fD5fDide6mslMyd29BQnFBIIZ0O\noihJnE4XmqYSj4Pfv4uTJ2so2yMZrRRNww4mmbHOQFiPJcsy4bCNaFTC5dKz0raTSUgk7Pj9DbS1\n7Vn4meEgODY2RiqV4vnnnzeTFq1l6M1w+nwtVxa2Qqq5me2ApaodXdeZmZmhvb0dVVUZHBwkFouZ\nlufWr2JXroTt8WqIRqPour5DFnJghywUGYVUFjKZDL29vQwPD9PU1LRmyyEXSpkEKY63VhuiGKFP\n4oap6zqSy4X+S7+E9PjjSLduoVdUIIXDoGlo99yTWy938CCZL30J+fvfR37pJfSqKrQ3vQn9jW9c\nJp1cifyEQiHzpnbb2bNU1dQsei5YSII431yvv/+BX6VRlnH9w4+xhXUS7l8j0HKS0K/cT+3uvfyJ\nW6e+fuXXYXYWPv5xB6GQhBHL7CGZhN5eCb+/mosXAyQSc9jtdmw2O/PzIZ59tpeDB71m62Lpor10\naFJURlTVID8ul4bPJ1leJomlyetCgpdOp5Flmba2NqLRqDkENzExQTKZxOv1Zg1PbmSCXrzupcJW\ntSFKebxS+x0I+abD4aB1QRVjtTy3EtCl7Qufz7ehc81nwDESiQDskIUc2CELRUY+ZEHXdSYnJ+nq\n6sLr9W4o90B8+EsV+bpaJWPN0KdMxohudjqXtxSWwJpwKcsy+oULaC4X0jPPGF4IBw+iX7yIfv78\nyr+koQHtfe9jNWqzdJBS/B2CxB1LpWjq6MD2139tRDP/0i+Red3r0BdI00okwYTNRvk774O33W3Y\nGJeX4ywrw7gVrb3wKYpEKCTh8ehmdEU8bnQVgsEUwWCE5uZ6fD4v0ajRmTl+3InLFTAHFhVFMeWS\nFRUVVFZW4na7s4LBjFO1WimLOQhRQTEqSpqWe+crqgrWm2w6nWZ8PEwgEGV6eo5odAgwJuirqsrY\ns8dfcPxyKQcAxevyWp5Z2A4hUjabjcrKyqw5Jmv7Ynp62mxfWAdv10xszXHcte6RkUgEr9e7ZfM4\n2xk7r0ieKFY+RDQapaOjg1gsRktLC3v27NnQzWizjaCWQpblnH/f9PQ0HR0dK4Y+STduID35pJFf\n4HCgHzuG9oY3wAoBJlayIKCfPo1++jQoCtjta6dB5vn3WI8xMzNDR0cHLpeLO91u/P/n/0A4jF5T\ngzQwgNzdjTQ+jvb2t69NFKzweIx0xXXC6xUFFJ1oNEoy6QAcOJ0NRKMS0ahheZxOQ0VFBfX1xjS3\nCB0KBoOEQiGGh4dpb2/H4XCY5EF82e02syUhy5hDkwKappLJLC6ga817pNNOnnyynnBYWni+Tjqd\nJplMYrdHuXhxAF2P4PF4stoXZWVlqy5gpWxDlFoB8mp2jMwX+ezwc7Uv4vG4SSCGhoYKbl/k04YI\nh8P4/f5tofzZbtghC0XGSuoE6667sbGRc+fOFWVxFzftUs0t2Gw20um0+X0ymaSzs5P5+XkzVXHp\nhSb19CB/+9ugKOh1dcag3VNPIQeDaO98Z06P3lxkwcRG+uC6jvT888j/+I8wNkZNRQXq2bOkW1vp\n7OxkZmaGo0eP0tjQgP1jH4NYzCA2ALt2wfQ09n/6J3jjG9EX8iHWfR6Dg0iDg8a3+/cbEc9L/SYC\nc9RF4+hVjaTTGnNzs4RCEqnUXlIpmWef1bNeDr/fqDwIWEOH9iycryj7CgIhTHcmJ3eTSp0iEpHQ\nNBlJMshQOi0tmFe6sNsVc1ZDURTm5+cBo4ogiIb4r6JAOCzhdoPbLUiFk2TSRTJZwZkzdZSVKab8\nbnZ2lv7+fjRNW9E4qtRtiFIvGltRWdgKv4NC/0brDM/Sz7E1fVO0vqzkQZDPfCsLOx4LubFDFooM\nu91O0jKdr+s6U1NTdHV14fF4ih61LIygSkkWjHL0YujT7t27ueOOO1aUJUnPPw/xOLolIlkvK0Pq\n6UHq68v6ufmcBRJU7NAq+cc/Rv7qV42pvbIyfDdvsvvnP+fm+DjSnXdyxx13GIOYc3MwPIy2a5fh\n8Kjrhuyxrg6powNpaGj9ZEHTkP/+77E9+STEYsbPfD7Uu+5Cu/deo8cwOYnzIx+h8cf/wKeiKkH3\nLr5/9HeItr2TffuqqK2VSKd17rpLNUc2EglIp6U1jRBzlX2TySTXr8coK9OIRCSiUYPwyrKMzSZT\nXi7h9WbM2QchhXW73bS0tJizLNbPoaJIqKqM02lURiRJLBA6yaSxCDscDmpqaqhZMGayqkDC4bCp\n3xfGUbqum99v9iJX6l0+vPZnFsQxi/He5focp9NpkzzMzMxkkU9FUQgEAtjt9hXbF9EFh9OdysJy\n7JCFPLEeNUQ0GqWzs5NIJEJra+uGWw4roZReC7Isk0wmeeaZZ8zQp5rVHPh0HeheLqwAACAASURB\nVMbGFrMEBFwuwwshEFj1WEUlQZGIUeGQJPRjx8goCvOShGtkhJM3buD83d81qxa6y4XucBikYmGH\nKYEh4bTb0XMpO3QdaXgYaWgIbDa0I0dyuktKXV3YfvpT9Opq00mSuTls//RPxizG4cO43v525Js3\nUW1O0rpMdXyM37nxSb5bv4+n970Vm82YT6irM7yXwIiymJtb30vjdru5cMHNt75l/B5dl4lGo8Ri\nUSKRCKoaYmgowOysD13XSSQSpvW4dbGxGkeJHxskAkBDkkBVJTTNtmAolb1QWXeQS/X7oVCI6elp\nurq6UFWVsrKyrOpDsY2jtiIXYqcNsTE4nU5qa2upra0FlkuQJyYm6Ovrw263L8u+cDqdZhtiB8ux\nQxaKDLvdboYjDQ4O0tjYuKpTYbGOWYrKgqIoTE1NEQwGOXz48LKFIickCWprkXp60ONxpMlJUFX0\nigrj31bxklhL1lgopIEBmJ5Gb24mvHDzcDqd6Lt3452fJzM6arQDdB3N7Ua/eBHH979vrMY+HygK\nUn+/saAfO5b9y1UV2w9/iPzP/0xiOoqqSmQqqwm/4T7i568Chp/U3r06cne3MexpJVk1NTA9jdzT\nAwMDyO3tKG43aV0mY3OSsHnxpQNc/fmf80Tlb5DJbCxAciliMeOUamuNLwNlQBl2ez0+H6YPiM1m\nw+/3MzQ0xMjISNbgpFV54XQaRNZu17DZNHQ9W26ayaik05k1Q7OEfr+yspL+/n5uv/12ALP6MDIy\nQmdnJ3a7PYs8bNQ4aivIAry2fR2gtLkQgnw6nU66urq4bcFjJRqNmu2viYkJ/u7v/o7vfe97NDU1\nEY/Hef755zl9+nRBw7dL8dhjj/HZz36WiYkJTpw4waOPPsqdd96Z87Ff+9rXePDBB5f9PJFIFCQ5\n30zskIUiQpRIg8Eguq5z6dKlkkhwNrsNYVVvOBwO/H4/hw8fzv/5t92G9M//jPzUU0Y1QVWRMhn0\nc+fQDx5c8XnFCq0y4XCQ0XVmx8ZQF+xrlUyGVCRiDF06HFlySO0tb0GbmkJ++WVjqBLQm5vJvPvd\nRmXEAvnll5F/8hMizhr+zy8OkUrq7MmMoP/dD/ha7WEmHE34/Tp//ddpGhUFct2gJQnSaZI3biCr\nKpos47I5SSVlNB3SkouaYD/xuSSZTBk+n14UwhCLwd/+rY0F1dgy+HwqR492Eg5PcPToURoaGswW\nkdjxi5tuIpHA5/NRUVGBJFWTTteRTDqQ5cVbTSajY7Pp2Gw2ZDm378NaoVkulwuPx0P9gu7U2r8O\nhUKm/E6EHwkSUYhx1FZZL5f6mKWeWdgKgiIqr4KYCoLb2NgIwOHDhzl9+jTf+c53GBgY4O677yaR\nSHD27Fl+7/d+j7e//e0FHe/xxx/n/e9/P4899hhXr17lK1/5Cvfeey8dHR00NTXlfE55eTnd3d1Z\nP9suRAF2yELeWOsCjsVidHZ2EgwGcTgcXLx4sWQX/WaSBRH6FIlEOHbsGJIk0dfXV9Dv0GtrkZJJ\nY+vqchmlfrsdKRYzwp4uXcr5vGJWFjKZDLcUhXK3m7rZWdynT4PdTiYUwjkzg3bPPai7d6Mt9HAl\nSYLKSjIf/CDSzZtGRcTvRzt9Omc1RHr5ZdB1Uv5aEgkJWYYZTxNHEtdpVW8y4WgkEJBIJDDCrZ56\nymhpCNKRTKJnMvQDqUSCNknCZreDTaKi0pAxyhEFtbqWD/43mb94TKW2Vs83qHGN1wYiERYGEbP/\nbXY2yiuvTNLQkOLy5ct4LIoOWZbNm66ACBwS5GF2NsLIiAOPx7PgzWD8t6pKxut14HJl+z6sFpq1\n2nDjSnMYgjyIQLZCjKNKPT+Qb05DMfFqnllYzzFXej/r6up429vexiuvvEJTUxNf+tKXuHXrFs89\n9xz79u0r+Hj//b//d971rnfx7ne/G4BHH32UH//4x3zpS1/ik5/8ZM7nSJJkkt/tiB2yUAByScVU\nVaW/v5+BgQH27dvHgQMHeOWVV0p6k9mMmQVN0xgYGKC/v5+GhgazlTI7O1vwAi61txs79ze/2djG\n2mxQXm4MOD7//KaTBeEY5/F42P8Hf4DnK19BefE6eiqNBMzs2sfw5bfgGjV2uy6XpRRvtxPYf4b0\n3oXv4wtfZNtFSJEIOJ3EYhAOA5KELEFElQmk0ozYJHRdYnYWDp06iXTqlFGxWFjtk3NzDNfWEti7\nl2NXryJ9//tIs7Pofj+yzQapJBIa6oPvYPdeGbvdUD1MTS3+ncaA4/pfJ7d7MSU6k8kwPj7G1FSU\nmpq9nD69H49n7c+0NXDoyBE4c0YjGIwtDJ1NEAqFSCaTlJd7GBoqM8nG0qRLK2EQrYvZ2VnAuOYU\nRVm1+mD8PYZxlDAF0zStIOOonTbE5kBV1U1ty650zHxaUpFIhNraWiRJ4ujRoxy1uL3mi3Q6zYsv\nvsiHP/zhrJ/ffffdPP300ys+LxqN0tzcjKqqnDlzhkceeYSzZ88WfPzNwg5ZWCd0XWd6eprOzk5c\nLhcXL16koqKCaDRa0mAnKP7MgjX06fbbb8/ara2niiElk0aJ3eXKKt/rbjdSKLTi8zZKFlKpFF1d\nXYtyyMZGpFSKyP4TTPxkCFciTszm58VwK9/+hJegPYLdbqemRua//bcYBw+Wk0i4eOwxO8Hg8kWj\nslLnoYcyVFaC1tKC/fp1Ml4VXbcjy+CREuiyzJRjHzJGJyOVksDjQf3N30Q/ehTtlVeYnplhoq2N\n2nvv5eyRI4Zc8X/8D5y/+7uGL4WmgcuF+mu/RuY//ScSE9DXJ5PrpauoMEjDRiDklB6Ph6NHjxCL\nOZGk3O/5zIyhwFgKp1Nn1y4oL5cpL/cDfsAI+0qn02b1YWpqip6enoVzz/Z9EP1iRVHo7u5mZmaG\n1tZWXAsR3mtGdi/BSsZRgjyI7ALArDioqko6nd5Q7zpfiErGa70Noapqycvr+XgsgLFgH1ylNZoP\nZmdnUVWV3Uts6Hfv3s3k5GTO57S2tvK1r32NtrY2wuEwf/7nf87Vq1d55ZVXOHLkyIbOp1jYIQvr\nQDweN1sOLS0tZg8XjIXbmhVQChSrDZFOp+nq6lo19Gk9CgV9716jmpBILKZGahpSOIz2S7+04vPW\nSxZ0XWdsbIzu7m6qq6sX5ZCA9N3v4njyp4w6D5KorKKMKOei1ylXv8cPWv4roXCGcDhDb+8Io6Nz\nJJPl9PW1Ul7uoLLShcvlRJIk4nEIBiVzJ6/dfjvaL36B/5kO9uq7cGoqVXKQl523c8vdBkljzZ+Z\ngbExCV33MVt+nOFmB413uDl27FjWDVS7coXkM89g+6d/gmAQ7dw5c6jS64XjxzUcDh2rz1MiYcgV\nhdNjoVDVDENDY4RCIRoaGqiuriYeX3nhmpmBhx92rkhaHnkkzYKnThacTie7du3Cbt+F3w8NDYuG\nO+PjYXp7+7HZwni9XtxuN+Gw8f+XLl3KaoNYraqtg5MCq4VmLT0Xq/lPLBYzlReKovDUU0/hdruz\n7LPXMo5aD0rd9hDHLAURWnrMrWpDrIViJk4ufS9Xq1RdunSJS5YK69WrVzl37hx/8Rd/wec///mi\nnM9GsUMWCoCmafT29jIwMEBDQwN33nnnsgvN6qj4aiELS0Of7rjjjqyb8tJjFbqA6ydPop05g/zC\nC+hVVca8wswMemMj2tWrKz5vPWRBzFhEo1FOnjyZxe71SAT5n/8ZtaKasKsWjxM0ZwVhRzP7w9c5\nIg8xWHsYSZI4f/48dXUKfX2RBQIYJRCYQtd1PB43quojmfQuzD06oLaWzLvfzYz2M+LX2olg56eO\nX+YZ5+uIKi7icQlNg8cec/CNb6gLskQ3u3Zd4KMftTE7m30TcTp16uq8qL/yKzn/TrfbyNCyjk9E\no4ab9noQiUSZmBimqspFa2trXgtIOi0RChmGS1aCEo9DKCQtVBxyzxkEAvDoow4L0XAiki4rKuC9\n740wMdHB/Pw8Ho+HWCzG008/nVV5qKysxOl0LrOtzic0ayXyYI1edjqdZDIZzpw5Y2r3lxpHidaF\n1ThqvdgKX4d/KTMLmUwm7zbERqWTtbW12Gy2ZVWE6enpZdWGlSCqurdu3drQuRQTO2ShALz00kuk\nUimz5ZAL4iLIZDIl68tthCwUGvq0ruAqlwvtXe+CAweQnn0WFAXtjW9Ee/3rYe/eFZ9WSBVD0zQG\nBwfp6+ujoaGBs2fPmjcHsevUg0Fs0SiarxpYXMiSTj+V0VE8qVDWFSEkexUVDmpqKvD5DLviRCLB\n/HyaQCDI0093sHev3Vy8xu98M4984TdBMiyTyRgVBbFeud0JFGUet9vDwEANo6MSf/iH2rLBwooK\n+NSn0lk2DRMTxoDk3BwEg8bPUiljXnS9myFFUejs7GViwklj4x7q6ytRFEmIP5alf+fCohX1ItZ6\nXjrNikRjejrNCy9cp75e4sqVK3i9XnPHL1wne3t7icVieDyeLALh9/vzCs0SWK36ID7jKxlHieFJ\nq3GUlTwsncNYC1tVWdghKIsoRmXB6XRy/vx5nnjiCd7ylreYP3/iiSe477778voduq7z8ssv09bW\ntqFzKSZ2yEIBaGtrw263r3pBS5JUUPJkMWC320ktjQVcA2uGPq0Aqw1zQbsDvx/tvvvgX/9rY+XM\ng0jlW1kIhULcvHkTXde57bbbqLLkTVjL1FRWQk0NtrEAsPgYbzJA0llOxLs6UZIkCZfLhcvlwm4H\nm03iypVKnE5jAZucnGR8fJi9DedwuWx4vcZCoyh2uroM5uBwBGloqERVvdy4YXyOlkZCix27dWc+\nMSHx7/6dk0jEmH2Ynpaw2TDNmd7+9gyybLQipqchF8dyOrOtHcTMjcNRyalTLSSTDpOEWOH3G1Ec\nmwEr0dB1nbm5eWZmkuzdu5dz5xbbe9Ydv5hOVxTDKjoYDDI7O0tfXx+apmUt2JWVlVluj4VUH1RV\nzXmt57IethpHiQCvTCZTkHHUVizcr3WfhUKOKaTvK20EC8EHPvAB3vGOd3Dbbbdx+fJlvvrVrzI8\nPMx73/teAB544AEaGhpMZcTHPvYxLl26xJEjRwiHw3z+85/n5Zdf5otf/OKGz6VY2CELBcDtdue1\n0y01WSi0srBW6NNax4IN9B3FCpcH1iILmUyGW7duMTIywsGDB7NMoqw9bLFDlLxe1De9CenL/y+7\n4kMk9Go88QhlyTmuN/4yo+zLylWwYunPxfcOhyOr593QoPOtb9kIBDSSyQyxWIJUCjIZHx6PRnm5\nD4fDTjyuL2Qw6PT0yMu407592eX7RMKQN7pcxthHKGRYNWiaITAJBg2zzOefl5mfdy6rVABUVOj8\nwR8o+P1puru7mZ2dpbW1lfr6ek6flshkcn+G7HaKItFcDclkkqmpSZJJJ7t372bfvrVzwlba8Yvq\nQ39/P9Fo1Jw3qKyszLv6kMlkCC30SITyIh/jKEFURYBXIcZRO2Rh81DKNgTA/fffz9zcHB//+MeZ\nmJjg5MmT/OhHP6K5uRmA4eHhrNc9GAzynve8h8nJSSoqKjh79ixPPvkkFy5c2PC5FAs7ZGETsF3J\nQj6hT6siHsc2NoYzGCyJ/Gm1+QirHPLKlSuUWergWdUEFkvNANo995AI6Sif/Ue80TniNi8v7Hor\nT9f8Oul54+KtrNRxOo3nGvJInWBQWqYyMB6X/bM9eyS+/GWNVEoiFkvT29vLxITOt751kooKBYgz\nORkgFHKjqnULlSgVl0tEjRvEYCWO5HYb5+TxGP4ImmY8JxiUcDiM771efVmY59ycUZ145ZV5gsEe\n/H4/R45cxel0IkkbIwMrEal8YEgi5wgGg9TUVFNdXUUgIANKwedh3fE3NBjKC7Hoh0Ih5ubm6O/v\nR1VVM6hKEAhrZLcYYI7FYqa3yHpmH0SAl9U4Skg3cxlHieOXUrK5XXf5W3XMYg44PvTQQzz00EM5\n/+3//t//m/X95z73OT73uc8V5bibhR2yUADyvYBLGeyUz/EKCX3KCV1H/pu/Qf7Wt5BmZ7ktGsXR\n3g7vf392XbvIyFVZSKVSdHZ2Mjs7S0tLSxbhWbo7zBkhbbPh+81f4eidryczNY9WVs4BfznGGKGx\nQDmduumzUFkJDz2UyelfYPVZsKKuzpifmJgYoKVlH2fOHOEf/9EwJPJ6/Xi9OqmUiq5LSJJOJpMg\nmdQWjIfsqKoDTVs5YdHhgP37dTTNWJjDYXjXuzJ4PDrhsJOqquwZgngcrl+XmJvLMDlpY9eu281y\neGWlzkc+oqzrbXQ6dSoqjGHGpTMKFRWYhGslpNMK/f0zuFwadXXNOByOgohGPjCksLmDqkKhEAMD\nA0QiETOoSpZlZmZmqKur49SpUyYhzmUctfSaW0u6abPZlplYCeMoMTyZSCS4du1a3sZRG8VWVTO2\norKw1j0vlUqRSqWK0oZ4LWKHLGwCtmJmYaXjBYNBOjo6yGQya4c+rQD5hz/E9vnPGwFK1dWQSOD8\n0Y8gGkX9sz8rbkiB9bgWsrCaHNLachBDYjmJggU1+zywr2Hhu9UXtcpK6OmBaHT57ysr07H6toTD\nYdrb2835iYqKCmZmWFhUWUhblIjFZEBGlnVkuWyBNKgoik48rjE7G+W5524wP2+U0MPhGnTdDG0w\n2xaZjPH/NTVGtSGXiCESiRMIyMiyncOHK/H7jcs+HtcX5J8rqxZWw65dhjxyNZ+FXNA0jfHxYWIx\nB7Jcjcfjz3ptDaJR8OnkhZWCqubn5+nr6yMWi5mT7LFYzKw8LK0+iL9jqXFUIbbVkG0c5ff7GR4e\npqWlxRyenJycJJFImLHL4lyEcdRGsTPguIjIgt95KSz6X43YIQubgO3QhlAUhVu3bjE6Orqsn18Q\nVBX5b/7GSGpc8FFXKivJOBw4f/ELtFdeQT93rhh/xjKIIbOhoTg3bvQQj8dpaTlDTU0Ns7MsOC1m\ntxzWIgnrQU8PvPWtrpyeA16vzre/neLQIcPJc3h4mP3793PgwAHz9d61C/70T7MX1eeeg//wH2xo\nGkxOGgQCZHQdNE3C43HQ2Hic8vJZgsEgnZ3TRKNnyGQk4nEZu92Ow2EjlVr5BqiqKrOzM8zPK7hc\nDdjtdvx+LavqsFEDJ4MQ5E80otEoN2/eRNM0HnmkDZfLA2RfK04ny9oom4lgMEhXVxd+v59z587h\ndDpJJBJm9UGoHRwORxZ5KC8vz+qDL60+WAmswGrVB7HjFtUEMcgpYpeF94MwjhKtFEEi1uOXUOpd\nvnhttmMbIhKJYLPZ8K7XqOQ1jh2yUAAKianeKrJgDX0qKyvj6tWr+DbSkA6HjaRGS2lOAjSfD2Zm\nkMbHN40sSJLE4GCML3whiqq2UFZWlvUeVFTo/MmfpKip0TaFJAhEoxLxuITDka1aSCYhHpcYH48w\nM3Mdu93OhQsXSCT8jI/n3m0LKeSBA0YLwOHQszKpMhlIJHT27tWx2SpwOMqprYUjR6Cmxk4sphKJ\nqKiqgqqmkGUZv19nfn6G8nI/ul69MNUdY3Z2Fo/HzZ49e2lvL6297lLous7Q0BB9fX00NTVx6NCh\nku8ul0JVVXp6epiYyA7IAvB6vXi9XlPtoKqquWCHQiGGhoZQFIWysrIsAuHxeNZdfVhJOpkrdlkY\nR4XDYfr6+ojH4+YgpyAP+RhHlXqXL+5T23HAMRKJbIrZ1msFO2RhE7AVZCGTyRCPx+no6CAcDtPa\n2sqePXs2voCWlRl1+Olpc7snSZKxJbXZ0DdpZiEYDDI6Oko47MRu30VNjX1Bj2+EKsVihrFPKrU5\n1YRccLvJ8gQw+t8q3d3d3HNPA83NzczMSHz4wyu7GgrvBKfT8BiQpOwASlk2CEh/v8THPmbPIid7\n98IHPqBTXW20MIRcT1HCyPIsN24MMjnZQiCg4XIp+P0VuN3lJBI2VHXz5I9rIRaL0d7ejqIonD9/\nPss+fKsg5LZOp5NLly6tuZu02Wwrqh1CoRDDw8NEIhEcDscy2+rVqg9WMhFfGNjIZDKrzj5YZaRi\nkFPISMPhMHNzcwwMDCwzjiovL19ms1zqNoQgStuxshAOh4uihHitYocsFIBCKguF+h5sBJIkoaoq\nP/vZz9i7dy+nT58u3kCUw4H2b/4Nti9+EX16GqqrsScS2KamjIjphXz4YsEqh6yurkZRPDidDrze\nxYRF4yark0zKC0RhsQw+Pb2Qv5ADLpfOGp5TBZ1nIpFG1x20tbWxf79RHlh0NTRaFAKBgMTkJAwO\nyqRSOvG4RGOjTlmZjt+/6IIdjcLTT9sQthA+n/E74nFDjVFfb5VV2jBcDyvR9UZcrinKyjIkk27S\naS+Tkwqjo9NkMk4SiUp8PolwOIOuO0zL6s2EruuMjIzQ29tLQ0MDhw8fLvkisRSaptHf38/Q0BAH\nDx5k//796yKaK6kdrF4LIyMjpNNp02tBfHm93qzXQVEUenp6mJqaorW1dV2zD+sxjvL7/SUnC6KS\nUWrzqXyCpIQSotTn9mrBDlnYBNjtdmKxWEmONT8/z82bNwE4f/481dXVRT+Gdv/9EAgg/93fIQ0M\n4EinSZ06BQ8/nJe5Ur4Q/g9CDjk3N8fMTBgwPASMuYRFOeTy58MHP+gkFFr+b9GosYC/731KVj/c\n79c4eTL/cxQ7ykwmg8Pu4pz+c1r+9w9w/NUs+smT2C79GtCM16ubswGJBLS3G62Mhx924HYbFZGu\nLmnBVEnn9ts1PJ5Ft0chZ1ycL9BJJHLfxIRCJBQK8cd/fJyKigpmZw3SlMlkmJiI87//d4JIRKO/\nP4XTqeFwOHE4HNTW2pFlnWLfChKJBO3t7SQSCc6cObMpn8tCIeYldF3nwoULRd9FWmOyhZ5eVB9E\npayzszNLFeFwOBgaGsLlcpkR4PlGdq9VfcjHOArgxo0bVFZW5mUctVFshWwS8guSKpbHwmsVO2Rh\nE1AK6aQ19OnQoUP09PRkeQ0UFQ4H2n/8j2i/8RtIAwP0j45SduECTQs3xI1iJTlkMBi09Ho1dH31\n6k4qJREKSQsWwou7+mAQXnrJjqrCL37hzCr7u93wgx8k8yIMsZhGPJ5Alm04nT7eGvpfvCv0Oap/\nEkd3yUhP/JSa2h9Q6/4KMc9Rc6FXVaPiIEmGN4PXa1QWbDbjfAMBiWvXJOx247Hz85LpxrjaW2qd\nT6mtreXy5cs4nU6mpuBP/9RJOCxhZC4YGRY2m6He+PCHg9jtQSKRCIlEkOvXw/h8PrP3XllZidfr\nXdeCIVQrPT091NfXc+bMmbzMcDYTosJx69YtGhsbOXz4cMl200LtIDIBNE0jEokQDAYZHx8nGo0C\nxj1jYGAg6/VfOvuw0dCspcZRiqJw7do19u3bRzQaNcnMasZRG8VWKCHE65YPWdhRQqyMHbJQALbD\ngKNVQlhVVWVKCHt6eshkMpubILdnD/qePaReeolizAuLv0UsdnfeeaephRbGNMlkknQ6jabZkKTs\nm0wyCaOjkMkY78v4+KJxksu1+F6l05Jpf+x2L/buFcX4HZGIDKzsFOl0prHZdGIxCYfDGGDblRzh\ngdAX0DToTu1HVkDSVernhni99Bf80cRj/Kt/pWbNONhsRrXA5zMqBzbbovmS3Z49UyDMlqxIpWB0\n1PhbUqkUvb29RKNRTpw4xYkTNZbHSYTDBmlamkqZTErs3eujqclreXzK7L2Pj4/T1dWVtfsVu861\nFoxkMmmGeJ06dcocyNtKJJNJ2tvbicfjnDt3LssKfCsgyzJOp5Pp6Wk0TePChQu43W5zty9ef1mW\nl73+DoejqKFZ4rH19fXmv1uNo8Lh8DLjKEEg1ksmt6KyIP7OfAccd5AbO2RhE7BZZCESidDR0UEi\nkVgW+lRKI6j1xFQvRSwW48knewiF0hw+fJaKihrGx41/c7t1du0yXPa8Xi/hcIREQqGszIbL5cTp\ndBCLublxw8bv/77TVBOkUtDTI5FIyHg8i3bBqmqoDCTJ6JpYZ7yUVYwCdV1nfHycmZkePvvZBurq\nDi50XTLU/uOTNH4hRE+yyciJkAFsxLUKLiSexpUKo6orq1BkWc/q4Ij2g7jXSxKkUjoLG0/m5iSu\nX5f5/d93IElp4nEVh+MoXq+Xigr44hfTLLTOTXg8+QU8uVwu6urqzM+T2P2KBWxsbIxkMrnM9dDj\n8ZjuhhMTE3R3d7Nr1y4uX75cshC1lWCtutTV1XH69Oktr3AATExM0NXVxe7duzl37py5cC59/aPR\nqGlbPTExQSKRwOfzZREIn8+3odAssYhaF/1cxlGCTIbDYSYmJujp6UGW5SzykK9x1FZZPcPaQ5U7\nlYXVsfVXz6sM4ua4GopNFlRVpbe31wx9On/+/LIbn91uLxlZWE9MtYCmaQwMDPDCC6N89asXUVWr\nHNJ4Xf1+nS9+MUN9vYezZ49z6JCD+XkNRVGIxxUUJUU8niSdrkDTkrjdMg6HA7fbvjDsmb0YG4RA\nWvAwyO88E4kEHR0dxGIxTpw4QV1dHRMTBiEBYxEWmzWJRV8qWTa+V1XDOVGSDOWGpmWrHjweOHVK\nIxKRkSQ4c0bD6zV+/4svyiQSkEhIzM2J8xHl1AguV4qGhjKcTheJBITD0sJQZ+HGSrlg3dU2NTUB\n2b136+S/3+8nmUySSqU4duyYOey3lRAtuvn5efO922ooikJXVxdzc3NrnpN1IRawVn8mJyfp7u42\nH2clcIVUH5LJZJZkc6X2QC4yGY1GzeHJqampZcZR5eXl+Hy+Fb0kSgnR+lir/bEzs7A6dsjCJqCY\nZGFmZoaOjg5zAGqlD3MpKwvrPVYwGDSHMVtbz6JpfjweHY/HWOR0XV9Y/CCVMiKeDUMjZcHQyLbw\nBYODGT70IZ3ycpDlBMlkiGTSjq5XAw40TcdYtrOhqovVhEzG+F4syOIcxAR/fX29afk7MQHvfa+Y\nA4DdqTv4f8LleFOzzNl3U1GhY5dUfEqIJ91vJkw5waBGKrWY9WC3Gx4KZrZHAwAAIABJREFUgmuq\nKqZ0UsgyvV44e1Zjfl7i4YczNDToC3G1s/zRH5VRXg67d1dltWTyiZHeKJb23g2zrCEGBgZwOAx1\nxc2bNxkaGjKH/MSwXCkxOztLe3s7FRUVXLlyZXPbcnlifn6e9vZ2fD4fly9fLsxqfQG5FmxrZHd3\ndzfxeHyh0rRIHsrKynJWHwyjr06qqqoKtq22kplCjaO2qrKQby7E/v37N/+EXqXYIQsFolSVBRH6\nNDc3tywDIRdK3YYo5O/LZDL87Gf9DA3N0NjYRGNjI2NjRp6Ax2PIAxfVDhLJpAh+Ml7nXC6BmYwd\nr9dBWZl9wXRKJxLJ4HAY74+iCOtnGVWVEcRhbk4yd/jGMeEzn3Fw/nwKvz9KR0cH6XR62QT/0jmA\nNI38MPa7/Gr/52nMDOKMy9jIMFfWxLXW/8hRReejHzUW+7k5eOQRB7GY4b4oJIvJ5CKJsC74mmaQ\nhz17dGprjQpHMqlSUXERv9++ZhrjZmPpzr2+vn6B6CXM6oPIXMiV+LgZA26ZTIaenh4mJydpaWlh\n7969Wy6B0zSNvr4+hoeHOXLkCI2NjUU7J8OMy4/f76dxwVk1nU6b1YepqSl6enoAsmSb5eXlTExM\n0NfXx8GDB2lqajKl12JwstDZB1jbOKq/v59YLIbdbsdmszEyMpK3cdRGsRUhUq9F7JCFTYDNZjMv\nuEIvhKWhT9ahv9VQSiMom82Wt4/E9PQ016718Wd/dgZNO2a+HqkUDA1JuFxw+bJRXdjYjVTC73fQ\n0gIvviixf7+Mz2fcKAKBDIODxg7XqDgsPkeWjYX61q1RFKWbffv2reoHYJAb4///4dB7+OnUSe5O\n/X8cq5xipPo0Tzf8BmM04Eoai/2+fTr79sFjj6WX+T/MzMDHP+5Y8FDITrUsL9eZnx+nr8/ouZ89\n27KwQ8y/1bDUynmj1s5gvJ+dnZ1UVFRk7ZIlSVrmepjJZAiHwwSDQebm5ujr60PTNMrLy7OUFxvd\n/YuKlVV+uNWIxWLcuHEDXde5ePFiSQbnnE5nVly64eS5mHLZ3d1NIpFAkiSqq6tNifdK1YeNhGat\nZBzV09NDLBZjfn4+b+OojSIfjwUwpLU7bYiVsUMWNgHig1moOiEUCtHe3r6u0KdStyHWmlmwyiFd\nrlOk0+W4XDou12LLAWTSaTH1vz6isNRYSMwGGDJLGbtdprx8caixpUXB6VTIZDJmcJOi2BgbG+PK\nlcPs3bs37zKpbJN4wXMn17iT440LVtALFYLy8sW/FVgwg8pe6Bsb4StfWU4ikskkQ0PdhMNB2tra\nqK2tZWgo/9fH5dIpL9cJh5enQS49r3yhKArd3d3MzMzQ0tKSlzuo3W6nurrarNAIoyBROu/t7SUW\ni+HxeLLIw1Jb75VgNVg6dOgQzc3NW15N0HWd0dFRbt26ZRLPrbIPliTJrD64XC4zTbO+vp5oNMrM\nzAy9vb1omrbMddLlchU9NMvhcOByubDb7bS0tGQZR4XD4ZzGUeXl5fj9/g21LgppQ+wkTq6MHbJQ\nIPK5GQnWnS9ZKEbo03ZRQ4ibZXd3N7W1tbS23sVDD3kZHjZ8BMQ1q6rSQgKjkcYoXtZ8d7/WBdFa\n5MhkjAwGRZEIG35O5oyC3Q5+vx23226JKk6jqg6qqqoYGRkx/SrEwlVZWbmwU13+vrvdcPy4RiAg\n8Sd/olicFY3zW2jvrwrjMYsEamxsjNHRHhoa6jlyZLmqIJ9qwe7d8Oijy0lIIedlxdzcHO3t7ZSV\nlXH58uV17/ysRkHW3abY+U5PT3Pr1i1gsXRuHdyzYrMNltaDdDpNe3s7kUhk2xhRqarKrVu3GB8f\np7W11UzaFLMn1nbBUgJnJQ9LvRbWG5plHXDM1zgqk8mY16SYlRBKnHxfg3zJwnb4HG1X7JCFTYAk\nSXm1BYoZ+rQdKgvRaNR07Tt16hR1dXUMD0MkIpmyRatqQJaNqkIoJGEdAykv13G7V9/91tfDF76Q\ne0GcnwfrNT86KvFf/6uT0VGJV16RTbto8ALGLrayspVLl46SSqXMne/o6CgdHR04HA7i8d2kUq2E\nwzZ0fdGuVtOM9Mu9e3WamtavRhDqi3g8ntOjoNBqgZWErBfWOYClQUvFguEimd3rtsoGu7q6lskG\n4/E4w8PDNDc3b4tAKlgcRK6qqtoW0lEwrscbN24gy/KK+Rer5UyEQiFmZ2ez2kdWArGeyG5FUXC7\n3Su2aJcaRwnHVHE+o6OjRCKRLOMo8bVSqyGfNoSu6zuVhTWwQxY2CWuRhWKHPpV6ZsFKTIQcsq+v\nj8bGxixpp7BoFlP/4pq1241FLpGAD30ow223Ld5U3G59mWdALhiPWb4gLjWWtNkMomJIKlVAw2aT\nkSQZRTGklr29EjU1EuAG6pGkehoa4Px5o+9+61YUlytFIADz80bErhjWqqqyrau0L14fUbaur69f\n0Q+gvt7wUlipWlBsxaKY4Pd4PCWdA7CWzq2De2Lu4datW2ZZORKJMDg4mDOwqVSwJlcWLbxtg7C6\naDY2NhZMqFbKmRC7/f7+fqLRqDm8mm9kdyAQIBAI0NTUZN6r8pl9EBkcViVOLuMoQSiXGkcV0obY\nqSysjB2yUCA26uIoFtb+/v6ihj5tVRsiEAjQ3t4OwIULF7ISBRd3FobKQdOMNkH27zIGAffvL45H\nQC643Toul9FuAOO9MUxpFsv4H/6wA2vHyG6Hujqdxx+HhoYqLlyo4utfN4YhE4kE4fA84XCYSCRC\nJhOlv9/G/Pxi393n8635WbHmJ5w+fXrNGZWVyFExITw9xsbGOHz4cFEn+NcLh8NBJpNhcnKS3bt3\nc/jw4SzlhTCNssZFi/bRZp57OBzm5s2b2O32vJIrSwFFUejo6CAYDOb1mcoH1naBaGOI4dVQKGQO\nK2YymazqQ2VlJW63G1mWGRwcpL+/n0OHDtHQ0LCq8mKt2YdCjaNEO1hRlBXvtaKitaOGWBk7ZGGT\nIGKjrRC7NVmWuf3224sa1Wuz2Uin00X7fWsdS1VVOjo6GBsb4+DBgxw4cMC8sLN7mDqyLOF0GkTB\n+pKI2OTNlOIrisLcXDdve5vCzMxtVFbaF3wddKamIBo1zjkQyL2oWAcoF9qqgGfhy9jpCMlaMBg0\nnQzFDU0sXhUVFebuxlpN2LNnz7bITwBDVdDe3o7D4eDixYvrbokVE+l0ms7OToLBICdPnjQn/Z1O\n54qmUaJ9ZLfbs8hDeXl5UTT+uq4zNDREX18fBw4cYP/+/duiFSJC5fx+v5kTslnINbyaSCTM9pEY\nVnQ4HOb9oLW1lfr6+pyZF0v/a7135iPdXM04amRkhHg8zrVr11Y0jopGo+i6vtOGWAVbf4d6lWE9\nlYV0Ok13dzeTk5McPnyY5ubmot9cSllZCIfDxONxotEoV65cyVpUlg46ybKMy2W4FS69dyWTRm5D\nXV1xd8sTE4YMcXZ2lv7+fsrKyjh8uBW73YEsL+YlrPVW5tvVWSpZs4YFCcdDRVHw+/34fD7C4TCZ\nTGbbDMFZ/QC2i6oAFucAKisr11z8cplGifcgFAplvQfW4dVChzVFNSiZTHLbbbdti8XFqgo5evTo\nmp4smwGrdFZUH6anp2lvbzffm97eXjo7O833wFp9WCs0azXb6rWMo4LBIH6/nz179iwzjlIUhU98\n4hMcO3aMuro6kkVwOHvsscf47Gc/y8TEBCdOnODRRx/lzjvvXPHx3/nOd3j44Yfp6+vj0KFDfOIT\nn+Atb3nLhs+j2NghC5sEQRaEMkCEPm1W7zdXJaPYEEZRs7Oz2Gw2br/9dvOmtHQiWuwE3G6oqNAJ\nhbJVC2As1nV165PyrYSJCYkHH7QzM5Mik/Hjdl/E4XCQSkmMjUnMzEicPKmx1LpCkhbJwzqdrE1Y\n7ZKbm5vNXVd/fz+Tk5PY7XYURaG9vd1ctAqRDBYTopQuy3LJ/ADWghisnJqaylumuRTWuGhYHJRb\nuvN1Op1Z1YfVTKMmJyfp7Oykrq5u21SDEokEN27cIJPJbBtViCAvw8PDWQZZS9+D4eFhs5K1NDTL\nZrMVLTRLDDjmMo6amZnhvvvu42c/+xmRSITm5mb279/PpUuXeP3rX8+73/3ugv72xx9/nPe///08\n9thjXL16la985Svce++9dHR0mFUwK5555hnuv/9+HnnkEd7ylrfwve99j7e97W089dRTXLx4saBj\nbzYkfS07wh1kQdOMjIK18NJLLxEKhQA4fvz4pvvTj4+PMzIysikfsKVyyKamJl588UXe9KY3mf+u\nqqrpMS++BKanyTmYB8ZwXrFeGl3XefbZGd773kq8XpmqKg+ybBw3kTCUEABHj2q43TA+DiMjxs9y\nkYXdu3V+/OMUR45s7BKJxWJ0dHSQSqU4fvw41dXVZDIZs2wubp5A1q53M4f2xOzM4OAg+/fvz2oj\nbSWEwZLb7ebEiRObOlipqqopGRTvgaqqy/IWZFmmu7ub2dnZklzL+UKEUtXX13P06NGS2yjnQiKR\n4ObNmyiKwqlTp9Ykn6qqmrt98T4oipI1f2INLRPIFZplXcqs96GXX36ZhoaGVXNLfv7zn/Nbv/Vb\ndHV18fzzz/Pss88Sj8f51Kc+VdDff/HiRc6dO8eXvvQl82fHjh3j3/7bf8snP/nJZY+///77CYfD\n/P3f/735s1/+5V+mqqqKb3zjGwUde7Ox9dT4VYa1djhiQGx6ehq/38+FCxdKsgPZrDaEVQ55+vRp\ndu3aRSKRMMmBlekLZr8UuQyJio1EIkFnZydDQyoezx5qamxYW+42mzEfoSgQjUqk08szFYpNm3Vd\nZ3h4mL6+Pvbu3ZuVMmi325dNnAvJoIgqtg7tWcvmG60+RCIR2tvb0XWd22+/fVsMdVlbIYcPHzZt\niDcTNpstp2mUWLT6+vqIRqNIkoTD4aCpqWlV2V+pkMlkTIOs7RKUBYtth927d9PS0pIXeTHURMul\nkoI8jIyM0N7ebkolBYEQyou1qg+pVIpEIrFgAa+sWH0QSojKykruvvtu7r777oL//nQ6zYsvvsiH\nP/zhrJ/ffffdPP300zmf88wzz/Bf/st/yfrZPffcw6OPPlrw8TcbO2ShiBA9VqfTyb59+9A0rWSl\nymKTBVFK7O/vXyaHFDdxRVGy+oZb0edeGvx07lzLwnlmr/xut05bm0YwKPGZz6RpbNT55jdlPvlJ\n58LvWf67RbjTehCLxWhvbyedTnP27FnzZrgSckkGlyY9tre3m4N9gjwUkrWgaRpDQ0P09/fT1NS0\nbTwKhB+AJElb2gqxTv3X19ebeQYNDQ04HA4CgQBDQ0Poup5lWV1RUVGywKpQKMSNGzdwu91cunSp\n5EFduaBpmikf3WjyqFUqKX6PmD8RBGJ0dHSZ+kVIJa1qBzHwWVFRYRLClWyrI5HIhtuAs7OzqKpq\nzs0I7N69m8nJyZzPEQqffB+/ldghC0VArtCnwcFBgsFgyc6hmDMLQg4pbt7WIS5dNzIcbDYbTz/9\ndNau1+/3l5QwWMv7Yliwv3/l4xt209DcrHPwoM7lyyp2e+4ZBVmGP/qjFA0NhZUbrJPya+VMrIVc\nQ3vihjk/P09/f3+WVa94H3LJwwR5URRl2wzmWV+r5ubmdTmXbgZisRg3b95E0zQuXryYNQdgzVsI\nBoNMTU2ZaY/W2Yd8pLOFwPpaHTx4kP3792+LIVSRgQFGCX4z5KNL50+ArOrD2NgYnZ2dpgKpvLyc\nZDLJxMQER48eNeW/S6sP1jmrJ598kjlr/OwGsPR9EffMYj1+q7BDFgqE9U0UF3Cu0KdSmiSJ4220\nsiAGy8bGxjh06FCWJMx6YUmSxF133WWGBM3OzpqRtFbyYJULFhPWHfJGFuQ3vAG++90EgcDy51ZV\nqbzhDYX9PuuCfP78+aJKYyF32dwaU9zT00M8Hsfn82XtuIQL30bJSzEhetupVGpTXqv1wGpm1NDQ\nkPO1slaArPHMgjxMTk7S3d2dNeS60fmTVCrFzZs3SSQS24bogTEz0dnZSUNDA0eOHCkp0VtKpIUC\naW5ujpGRERRFMd/PaDRqvhc+ny+LTCeTSR5++GG+8Y1v8N73vndD51RbW4vNZltWFZienl5WPRCo\nr68v6PFbiR2ysA5IkmRq0lcKfSrG4l0INtqGmJqaoqOjA5/Pl1MOKdg4LJbuli5couceCAQYHR0l\nnU6bfUDxlU+C5moIh8N0dHSgadqqi0wuBVSunxmEYGPvk3XXJxzzSrEgW616rQuXIA8jIyN0dHQA\nmK+96M1uFWHQdZ3x8XF6enqor6/n7Nmz20JVkE6nTUfVQs2McklnrZbV1vkTK3kQDoOrYWZmhvb2\ndmpqalZ09yw1VFWlq6uLmZkZ2trazL97KyHLskkOKioqOHHiBJqmmdWH8fFxurq6kGWZZ555hrm5\nOY4fP87XvvY1FEXhxRdfpKWlZUPn4HQ6OX/+PE888USW9PGJJ57gvvvuy/mcy5cv88QTT2TNLfzk\nJz/hypUrGzqXzcDWf/JeZdB1nY6ODkZGRkwzolw33lJXFtYTi93bKzE7m2JwcIBQKExz8wkqKuqY\nmJA4fDi7TCdKYyvd3HL13IVJi9UiViQMiq98y7WqqppyrNWm9z0eIxciElmeoQDGvxVzwF4MgGYy\nmW2xQxYLVyqVIh6P09DQwO7du03PgaGhIRRFMXvuYuHaKInLB2JBDoVCWQZLW43Z2VlTxnrp0qUN\nzx9YNf4CuTJHlg7tWStxKwVAbTWi0SjXr1/H4XBsm5kJMUjc29u7bDg2l1HT8PAw165d41vf+hbz\n8/O0tLTw6U9/msuXL/Orv/qrG9rVf+ADH+Ad73gHt912G5cvX+arX/0qw8PDZtXigQceoKGhwVRG\n/Of//J+56667+PSnP819993HD37wA/7hH/6Bp556aoOvSvGxQxYKhCRJuFyuNUOftoIsQP6x2Ldu\nQVubE3ACp5b9+40bKQ4cWKwmrEYUVoIYVBKJciJh0FqutfYjhcZ6KQkQVRy73b6mlnzPHp3/9b/S\nK6ZXejzGYzYKayuklNWEtZBMJuno6CAajWbtkK2qCyuJWxoTXSiJyxfT09N0dnbmZbBUKlgXZKsf\nwGbA5XKxe/furLK5kAxajbvKysrw+XwEAoFt5aRpbdE0NTVtm/kSYW8dCoXWJOuyLOP1ehkYGOAX\nv/gFn//853nzm9/Mc889x7PPPsvXv/51Tp48uSGycP/99zM3N8fHP/5xJiYmOHnyJD/60Y9oXgis\nGR4eznrdrly5wje/+U0+8pGP8PDDD3Po0CEef/zxbeexADs+C+uCoig5UxetiEQiPPfcc7zxjW8s\nyTnpus6Pf/xjXve6162pTY9Go3zve0P8+39/bsXHXLsW5/RpdVNVDqLPGAgEzMVL6NzFwOT8/DwT\nExMcOnSIpqambXGDEtUEVVU5ceLEtugh67puWk3X1dVx9OjRvDNHrCRO7H5Fz32j8ydC5jc9PW3a\n/W6H4a1IJMKNGzew2+2cPHlyy3MdBIkbGBhgYmICh8OBoihZ6hcxvFfqa0BRFDo7OwkEArS1tW0L\n11FYVIZ4vV5Onjy5JgGdnJzkwQcfZHJykm9/+9ucOrV8k7SDlbFTWdgkCHVCqSZbhUJhtbkFqxzS\n5zu65u/cbDmkdQgMFnXuYspcyNS8Xi/xeJypqamieQ2sB5qmMTg4yMDAgLm72g7VhFQqZfbb11Pe\nXxoTbe25i6yFdDqd0/NhNQQCAW7evInX6+Xy5cvbpmQt5ku2kxlVJpPh1q1bBINBzp49S01NzTL1\nizWsyaq82MwWknVB3i4VIWES19PTk5cyRNd1rl27xoMPPshdd93F3/7t324Lb5FXG3bIwiZBDCLl\nk6VeLKxGFsSNW9j69vev3ls32g6bcZarH9PpdJqLVEtLC3V1deauVxi0CIteq03yZt/whZGRpmnb\nZiJd13Wmpqbo6uqipqamaDdza89dWNRaUx4HBweJRCJmRPHS90HTNHp7exkZGeHIkSPbIrkSjBaN\nMBjbDvMlAisFQK1lGmWNiraSh2JcD9Y5gO0k1cxkMnR0dBAIBDh37tya/iWqqvK5z32OT3/603zq\nU5/ife9737Ygh69G7JCFdSCfi2aryMLSOQlFUejp6WF8fHyZHHK7QSx85eXlXLlyxdyJWoeUxG5L\nSDb7+vrMtLjNsEm2VhO2kxeASGMMBAIcO3Zs06VWS41yhF11KBTKeh98Ph+JRAK73b6tFmSh9qmr\nq9s2qoJCA6ByRUUripIlYe7r61vmvVGoaVQ6naa9vZ1oNLqt3sNIJML169dxu915EeO5uTne8573\n0NXVxU9/+tNtOQfwasLWXzGvUciyjCzLZDKZkkyaw3K5prhBlpWVcfXq1ay+7HYaVUmlUnR1dREI\nBGhpaVm1r51rt7XUJjmVShVFsrkdbZEhe1jwypUrW1IaXmpXLVz8RkdH8fl8qKrK888/nyUXrKys\nXObxv9nIZDKmzO/48ePbRr9erAAoh8OxzDY8l/eG1+vNIg8ruRUGAgFu3LhBRUUFly5dynvuZTMh\nhiu7u7s5cOAABw4cWPMz9Pzzz/PAAw/Q1tbGCy+8UJAUdge5sUMWNhFboYhQVdV0lJyfnzdlV0vT\nIb3e1clCKcLrrEN5NTU161r4VpNshkKhdUk2rSFL26maoCiKmQmwnYYF4/G4aW19++23my0aIRcU\ncw8dHR04HI6skvlmDuyJUCqPx7NtZiZgcwOgVvLeEFWglUyjysvLGRkZYWBgYMtirnNBkL25uTnO\nnDmz5qKvaRpf/vKX+ehHP8pHPvIRPvShD22La/e1gB01xDqgqmpeJODJJ5/kxIkTJWO1P//5z3G7\n3UxPT7Nr1y6OHTuWtfhaU9oA+vpkotHlNwS/Hw4fLk3wUyQSMbPkNwu5pv2FNWxVVVXWohUOh2lv\nbwfgxIkT26aaMDs7a1aJjh8/vi0WPqucbs+ePWsufCJh0Po+WNUvYuHaaKXEWt4vVShVPhAL31an\nV4oBVus1kUwmkSTJNJfK1zRqMyE8HZxOJ21tbWtWB8PhMO973/t45pln+PrXv87rXve6bfG+v1aw\nQxbWgXzJwtNPP82hQ4dKUvqMRqM899xzAJw6dSprIn5plOtWhT6Jc7EGPx05cqTkpU4h2RQ3ykAg\ngKqqOBwOUqmUmZpXqvbRahAW3JOTk5vuBVAIrAqMEydOmEqKQmBVvwjyEIvFzJwF8VXIohWPx7l5\n8yaZTIa2trZ1l/eLDWsA1MmTJ7cF2QODhN68eZOqqirq6urMwKZwOGwS6lymUZsN4biYr6fDjRs3\n+O3f/m0aGxv5+te/vqEwqx3kxg5ZWAc0TUNRlDUf99xzz7Fv3z4aGho29Vz6+voYGBgwB9COHDkC\nLM4liDhpa8b7VsAa/HTs2LFt00cMhUJmcJDf7ycWi2VlLFRWVlJVVVVyyeb8/Dzt7e14vV6OHz++\npn9GqTA9PU1HRwfV1dUcO3asqGTPmrMQDAaXLVqiCrR00RI20t3d3SvmOmwFtmsAlLhvjIyM5HSI\ntBJq8X5Y5bPi/Sj2NWG1kj558uSaJFTXdf7qr/6KD37wg7z//e/nj//4j7fF8OprETtkYR3Ilyy8\n+OKL7Nq1y5SfFRtCDmmz2Thx4gSjo6M4HA6OHj2aleew1dWEYgU/bcZ5iXL1gQMHspQiImPBumg5\nHA6zbbGZkk2rs+CRI0e2Vf/YarAknDk3E0urQMFgEEVRsgZYvV4v/f39BIPBdVc5NgPWAKi2trZt\nIbeFxeFKVVVpa2vLOxI8mUxmVYEikciyGZSN5I7EYjGuX7+O3W6nra1tzepLPB7nAx/4AD/60Y/4\ny7/8S+69995tcZ28VrFDFtaBfMnCK6+8gt/v5+DBg0U9vlUOefjwYZqbm5Flma6uLjRNo7W11SQK\nW11NsAY/HT9+fNvIsEKhEO3t7ciyzIkTJ9YsV1v77YFAgFAotCmSTTGU53K5OHHixJY7CwpYqxwn\nTpzYsjK6rutZi9bc3ByJRAJZlqmtraW6utokclu5cIgAqNraWlpbW7fNblcopIoxXGm9JkT1QZhG\nWdsX+XxWRIKlsE5fi4T39PTwjne8g7KyMr75zW+adso72Dxsj0/wqwz53oQ2Qw0xOTlJZ2dnTjmk\nzWYjmUySyWSQJGlLqwmqqjIwMMDQ0NC2UhRYA6kOHjxoEq21YLPZqKqqoqqqigMHDqwo2RRl2qqq\nqrxvlOK8RFn40KFDNDc3b4tdkqqq9Pb2MjY2xuHDh7fcYEmSJDweD06nk3A4TDqd5ujRo/h8PkKh\nENPT09y6dQtJkooWEV0IrFWhY8eOlaT6kg/EeU1MTBRNQmq9JiA7d8SqRLKad1VUVOD3+81rTlVV\nuru7mZqayivBUtd1vvvd7/J7v/d7PPjgg3zmM5/ZFq6S/xKwU1lYB3RdJ51Or/m47u5uVFXl+PHj\nGz6mCAgKBAIryiEnJiZob2/PCmeqqqrKujhLgWAwSEdHR9679lJBVBNE2ybf8mu+sO54g8EgkUgk\nL8nmZp/XehEOh80218mTJ7dFoBEY/hfCjTTXeWmaZnoNWKf9Retis/rt0WiUGzduIMsybW1t26Yq\nJMr7sixz6tSpks6+WM27BInQNI3y8nJ8Ph/z8/PY7XZOnz695nmlUin+8A//kG984xv8z//5P/n1\nX//1bUGo/6VghyysA/mShb6+PmKx2IYCS4R6oKenh7q6OlpbW1eVQ+q6bvZ4RUCTpmlZC1ZlZeWm\nzAxkMhlzF7qdgp+su/ZCqgkbxUoBTda2xezsLCMjI8tmJrYSuq4zODhIf3//tspPsFoQF1qtSiaT\nWe9FJBLJsg1fuuMt9LyEhDTfMnqpIFQFYlZoq89LmEaNjIwwNjZmus4KUm21rLYSgcHBQR544AEy\nmQzf/va3zSHuHZQOO2RhnUilUms+ZnBwkPn5ec6dWzndcTUIB8E1rzZIAAAgAElEQVRUKrVscEuQ\nhP+fvfMOa+rs3/gdQDaEIeIEXMwEUVBAxFFX9dfx2lpHiwJ11L3qW0fVum2rfR211ddqxdYiVq2r\nrdX6VoaK4GrZQ5aiAoIJCYQQkjy/P7zO6QlDAiThaM/nurhawwl5ss75Pt9x39R/myo5UF9OprMj\npXDIbNZrayqvoqICGRkZMDc3h7e3N2t2oUx76/betTOb9crLyyESiUAIgY2NDRwdHVslzatrqNHD\nuro6CAQC1jTlUb4OMpkMQqGwzb0vTNlw6r+UTDLzotXcpAdlkSwWi1nlyMjUdNBmqsBQUEqfzHII\nM6imshAAcOjQIXTq1AlOTk7Yu3cv3nnnHezZs4c1U0H/NLhgoZUoFIpmJZOLi4vx+PFjDBw4sEV/\nmzkO6eLigj59+mjUW6lJh9aOQ1J1RSqAqK6upscEqQBC2xQt1WxZWlrKqs59qtZeXFzMqiwHczLE\nxcUFXbp0gUQioZsmme+FISWSmbvjrl27om/fvqyYWAGeNeVlZmbqtVmwvkyyWCxuMD5bX6iIMoCy\ntbWFt7c3a2rnlIeCmZkZqzQdampqkJKSAkIIfH19myzTUGWk/fv348KFC0hPT0d1dTW8vLwwePBg\nBAcHY8qUKawp8/xT4IKFVqJNsFBSUoKCggIEBwdr/XeprnOqfs3c2elrHJI5JigSiegULRU42Nvb\nN1prp4yfbGxsWKMqCDwbKaWkhX18fFiT5aiurkZaWhpUKlWD95aiqZFNZtOkrntQKIElqVRqUMXR\n5mCOanp5eRlcaKex98LExAR8Ph8qlQpisRh9+/ZljUIk07rZzc0NvXr1YsW6gGfaHOnp6VpPYZSU\nlCAiIgLl5eU4fvw4OnXqhMTERCQmJuLGjRv47bffDJph2LZtG1avXo3Fixdj165djR4TFRWFyMjI\nBrfX1NSw5tzYFrhpCD3SkmkISvf/8ePHGuOQwN8NjFRvgq4nHUxNTRs4O1InyLKyMuTk5NC1dipw\nePjwIW0jzRaPgvrZBLZMFDBr7VRNu6mTZWPvBTWeVlFRoXOXTWrXTllcs8E4CPh7hJRyGGyPk239\n90KtVuPJkyfIyclBXV0djI2NkZubi9LSUo1MUHtkGKhySGVlJfr378+acoharUZubi4ePnwIb2/v\nZgM+Qgji4+MRERGBkSNH4pdffqEbpP/1r3/hX//6lyGWrcHNmzdx4MABrXrPbG1tkZ2drXHbyxAo\nAFyw0Gp4PF6zmQVtggVCCH3CbsodksomADDIOKSxsXEDR0GpVAqRSISSkhJIpVIAAJ/PR3V1NZ4+\nfWqw0bSmEIlESE9Ph6mpKYKCgliTTaBMlmprazFgwAB6zExbGhtPY/agPHr0SKPTn/pp7gTFNKVq\nj117UzBNvNgU8AF/Z9Ko3bGRkRFd0hOLxbh37x6qq6tbZFqmCyorK5GSkgJra2sEBQWxphwil8uR\nkpIClUqFwMDAZr+TKpUKO3bswI4dO/D5559j7ty57V46rKqqwnvvvYdvvvkGmzdvbvZ4Ho/Hmu+S\nruGCBT3SXLDAHIekZrLrj0NS2YT21EwwMjKCqakpnj59itraWvj6+sLKyoo+SVISztbW1hqlC0Oc\ntJhz7VRvAhsuLlRKODc3F127dsWAAQN00gPAdBWkXDaZI5uFhYWQSqUwNzfXaGBlXrCoUpeVlRWr\n3BiZvg5ssgRnNgv6+PhoGEBZWlrC0tKSlktmNuvVd3hkZoJ08VlgSkmzLbCiPCc6deoEDw+PZp9v\neXk5Zs2ahdzcXFy5cgWDBg0y0Eqfz/z58/F///d/GDVqlFbBQlVVFVxdXaFSqeDn54dNmzahf//+\nBlip/uGCBT1iYmKioaRIQaWlc3Jy4OzsjNDQ0OeOQ7a38RN10XN2doZQKKRT1UwbXLlcTu92KTEW\nyhCIumjpulHv6dOnyMjIgJmZmVY7F0NRU1ODjIwMyGQy9OvXT+89AObm5ujcuTO9o6Fm28ViMUpL\nSzUuWEqlElKplFVujJRGSFZWFuuaK5kGUEFBQc0GVh06dEDHjh3p6QMqK0e9H8XFxVAoFLCxsdEI\nIFoasCkUCqSlpaG6uhoBAQGsmVphek5oK0qVlJSE8PBw+Pn54datW6wpocTExODOnTu4efOmVsd7\nenoiKioKQqEQEokEu3fvRkhICP7666+XYtSTa3BsJUqlEiqVqtljLl++jFGjRtEp+ubGIdmSTQDa\nZvxUV1enMXEhkUg05trt7e1bLclL6TlQctftrSpIQZkZUZoYHh4erJD5VavVKCkpQW5uLt3zQsny\nsqXWzjZfB30aQDFVDinNB3Nz8wY6A02l4J8+fYrU1FTY29vr3MirLcjlcqSmpqKurg6+vr7Njimr\n1Wp8/fXX2LBhAz755BMsX7683csOFA8ePEBAQAAuXbqEfv36AQCGDx8OPz+/Jhsc66NWqzFgwAAM\nHToUe/bs0edyDQIXLLQSbYIFQgguXryI4cOHo0OHDsjPz0dBQQFcXV0bmCm1dRxSl+jD+Ik5106N\nCfJ4PI3gwdbWttmTBZVCt7CwgLe3N2vGp+RyOTIzMyGRSODt7d2sbK2hUKvVKCwsREFBAS38xOPx\nNGrt9cdnDTWyWVFRgfT0dNjY2MDHx4c1tXZDG0AxM0GUzgCziZWSrTYxMUF+fj4KCwvh4eGBbt26\nsSJIBp69l6mpqejYsSO8vLyaPV9UVlZi3rx5SE5OxrFjxzB06FADrVQ7zpw5gwkTJmg8D5VKRTeX\n19bWanVOnDVrFoqLi3HhwgV9LtcgcMFCK1GpVFpNOvz+++/w9vZGfn4+LZvLrMUyMwnt7Q4J/J35\n0Lfxk1qtRlVVFZ15EIlEUKlUsLW11ai1UztzpVJJa9uzSc+BEIKSkhJkZWXROgBs2elVV1cjPT0d\nSqWyweeuPtSYYGVlJUQikcbIJvWjq5FNtVpNT624u7uz6qLHBgMopu8IFURQZllGRkZwdXVF586d\nDaK/oc1aKedWDw8PDRn6pvjrr78QFhaGnj174ocfftCJT4WukUqlKCoq0rgtMjISnp6eWLFiBQQC\nQbN/gxCCQYMGQSgU4ttvv9XXUg0GFyy0Em2Chbq6Oly5cgUA0Ldv32bHIdszm9Dexk+EEMhkMg2l\nyZqaGtjY2MDc3BxisRiWlpYQCASsySYoFApkZmZCJBLB29tbo/GtPWH2mXTr1q1VmSHmyCb1U182\nvDUTMJR/Ao/Hg1AoZE2fCVsNoIBnAUxaWhpsbGxgbW0NiUSi12BOW6gMjFwuh6+vb7MeMIQQHDly\nBB999BGWLVuGdevWsaJMpy31yxDTp09Ht27dsG3bNgDAhg0bEBQUhL59+0IikWDPnj34/vvvce3a\nNdY0bLaFF+edeoGgxiEzMjLA4/Hg7e2Nbt26afze0OOQz4Np/DRo0KB2MX7i8XiwsrKClZUV3TRZ\nXV2NzMxMlJeXw9TUFJWVlbhz545G5oGpqGdIqHFXe3t7DB48mDUpdGrCpqqqqk3NlU2NbFKBw+PH\nj+lgTpuRTcrjJDc3Fy4uLqzyT2AaQAUFBbEmGGVmYOoHMMxg7unTpygoKKAzc8xgTl+fS6pvwsHB\nAf369Wv2ol9dXY2lS5fi0qVLOHXqFMaMGdPuWZG2cv/+fY3PsFgsxuzZs1FSUgI+n4/+/fsjPj7+\npQgUAC6z0GrUajXq6uoa3E51wovFYnh5eaGwsBC9evVC586dWdfAyFbjJ+BvrwlLS0t4e3vDwsKC\nbppkGjMx1Q2p3ZU+X9O6ujp6jM7T05M1glQANMohHh4eei+HNOaySTXqMQW8FAoFLdkrEAharDWh\nL5gZGLYZQMlkMqSmpoIQolUGhsrMMb8b1dXV9ESSroJrQggKCgpQUFCgdd9EVlYWpk2bBnt7exw7\ndowe+eV4seCChVZSP1ioPw5JuUPevHkTXbp0Qbdu3TSyCe1ZcgDYa/zE9Jporp7N3F1R5QsADZom\ndTWG9+TJE2RkZMDW1hZeXl6s0SegApiKigp4eXm1Ww2Y2ahHXbCAZ98VKysr9OnTBw4ODqwYi6RK\nSJWVlRAIBKwZ1wNAZyW7dOnSpjFShUKh8X5IJBIYGxtrjGy25PtBjWvKZDL4+vo2q4NBCMGJEyew\naNEizJo1C59++ilr+nk4Wg4XLLQSZrAglUqRlpYGhULRYPyLSpv36NGD1ltozyCBrcZPwDNhloyM\nDFhZWdHZhJbQmD13XV2dxslRGyfB+jA9Ctzd3bVq4jIU1ESBtbU1fHx8YGZm1t5LAvAskMvKykJp\naSmcnJygVqvp96O9RzbZagClUqmQnZ2N0tLSBuJPuoDpekr9UO8H8zvS2GdILBYjJSUFfD4f3t7e\nzX6H5HI5Vq5ciRMnTuDQoUOYMGECa74zHK2DCxZaCSEENTU1yMvLQ2FhYZPjkKmpqRCJRHBycqJr\nwO0VXZeVlSEzMxM2Njbw8vJijdUrFcCUlZWhb9++OuuOp94j5sRFTU2NhtJkc4I4jZVD2ACzIY9t\nEwWVlZVIS0uDqakpBAIB/ZpR70djI5t8Pl9v4l0UarWa7tx3d3dnVaBM9U0YGxtDKBQa5HNGCGlQ\nSqqqqqLlqqmRzYqKCuTn56Nv375aaZoUFBRg+vTpIITgxx9/RJ8+ffT+XDj0DxcstBKZTIZr167B\nxMTkueOQCoUCIpFIww6aulhRJ0d97wZra2uRnZ2Np0+fssr4CXiW2qd8MQzhXFlbW6uReZBKpRpa\n/vb29rC0tKQNcB49esS6DAx1Me7QoQOrpkOY9WxthYzqp8qZfSi67PKvqalBamoqlEqlVoJBhoIS\n8srOzmZF30RdXZ1G4yRV2uPz+XB0dHzuFAwhBL/88gs++OADTJ48Gbt27WJNqY6j7XDBQishhKCw\nsPC5fg6NjUPWDx6kUiksLS01PBV0taugZHRzcnLg4OBA91GwAaaRUXum9pVKpYY9t0QigZGREdRq\nNUxNTeHu7g4nJydWNL4xTZZ69eqlMYrb3tTU1NClOKFQ2Gpfh6ZGNuuXkloyckdJSbe1B0DXKJVK\nZGZmoqKiAgKBgDXqlcDf5lRWVlZwc3PTmIRhGpcxP4sbN27EoUOH8PXXX+O9995jTXDNoRu4YKEN\n1NbW0v9ffxxS294EZoc/dbEyMzPTCB5a08FcU1ODzMxMSKVSeHl5sUYDAGBvoyCV2i8uLoajoyMI\nIRpqetR7oisjoJZQXV2NtLQ0qFSqZgWWDAkVkGZnZ9NujLp8beqPbDL1N5ob2WQaQLFJBwMAJBIJ\n7TkhEAhY02vCHHFtypyKWbpYvnw54uLiYGNjA7VajXnz5mHixIno168f18z4ksEFC21AoVDQyou6\nGodUqVQawUNlZSVMTEzowKE5T4X6xk/u7u6s+dIyswkeHh4aWZn2prKyEunp6bTKJjUdwlTTo7JB\nCoWCbtKjAgh9vca6EFjSF3V1dcjMzMTTp0/h4+NjMIlruVxOK002NrJpZ2cHlUqFtLQ0WFhYwMfH\nhzUBKfNi3LNnT/Ts2ZM13wHKp6OyshK+vr7NqrcSQhAbG4tZs2bB398ffn5+uH37NhITE6FQKNrF\nPXLbtm1YvXo1Fi9e/FwPh1OnTmHt2rW0Y+eWLVswYcIEA670xYMLFtpAbW0tlEqlXsch1Wo1JBKJ\nRumC8lSgggeqpksZP8nlcnh7e+vd7bAlUM2VbMsmMJvetEnt12/SE4lEkMlksLKy0sgG6eL5yeVy\npKenQyaTwcfHh1XjfdREgY2NDby9vdt1Z1x/ZFMkEoEQAktLS3Tp0qXdskH1qaurQ3p6OiQSCYRC\nIWv0JoBnmY6UlBRaJbW5cqVKpcLnn3+OnTt3YseOHZg9ezb9vVGr1cjMzETPnj0N2k9z8+ZNTJo0\nCba2thgxYkSTwUJiYiJCQ0OxadMmTJgwAadPn8a6detw9epVBAYGGmy9LxpcsNBKEhMTsXXrVgwe\nPBhDhgzRSsVMFzA9FajgQaVSwczMDHK5HE5OTvDy8mJNb4JCoUB2djYrRYyokVcejwcfH59WK1dS\nfShMcSJmKcnOzg5WVlYtet5Und3JyckgAkvawlQVZFvjJyU/LJPJ0Lt3b7ofRSQStfvIplgsRmpq\nKj3iypbvJ5W5ysnJ0bop9cmTJ5g5cyYKCgoQExODgIAAA622aaqqqjBgwAB8/fXX2Lx583PdISdP\nngyJRKJh7vTqq6/SolEcjcMFC63k/v37OHr0KOLj45GYmAgACAoKwpAhQxASEoIBAwYY5IRA1T6V\nSiWsra1RVVVFaws0ZshkSEpLS5GVlQU+nw8vLy/W1GWZToz68MGgdrpUAFFZWQljY2ONiYumOvyZ\nqX221dmrqqqQlpYGABAIBKyZKACebwBFjQgyAzp9qBs2BtUInZ+fjz59+sDFxYU1wZVSqURGRgZE\nIhGEQqFWmavExESEh4dj4MCB+Pbbb1mTHQkPD4eDgwN27tzZrJW0i4sLli5diqVLl9K37dy5E7t2\n7WpgHsXxN5w3RCtxcXHB6tWrsXr1aiiVSty9exdxcXFISEjArl27IJfLMWjQIISEhGDIkCEYOHAg\nzM3NdXaiYKbPmRe8+toCWVlZGt3LVAChz0BGoVAgKyuLlaOaVVVVSE9Ph0qlQkBAgF7sh01MTODo\n6EiXgahSErXLLSgoaGDKZGdnB5FIhIyMDNjY2CA4OJg1wRWbZZG1MYDi8XiwsLCAhYUFunbtCkCz\nsfjRo0fIzMzU+chmbW0tXUbS12ettUilUqSkpMDc3BxBQUHNftbUajW+/PJLbN68GRs3bsTSpUtZ\n8xmIiYnBnTt3cPPmTa2OLykpaaBy6uzsjJKSEn0s76WBCxZ0gImJCQYOHIiBAwdi+fLlUKlUSE9P\nR2xsLBISEnDw4EGIRCIEBATQwUNQUFCLU9MUzzN+4vF4sLS0hKWlJW1exdxV3bt3j9Z6YAYPuuoh\nYBosse2CV1RUhLy8PLi4uKBXr14Gq2EbGRnRFyA3Nze6w596Tx4+fEhP1jg4OLBKIbK2tpY2pvLz\n82NV30RbDKA6dOgAJycnuilTpVLR6oZMYybmyCafz9e6HFRRUYG0tDTY29sjKCiINe6KhBA8fPgQ\nOTk59Cajuc+aWCzGnDlzcPfuXVy8eBFDhgwx0Gqb58GDB1i8eDEuXbrUonNY/edMqetyNA1XhjAA\narUaOTk5iIuLQ3x8PK5evYpHjx7Bz8+PDh4GDx4MPp//3A+sUqlEXl4eiouL22T8pFAo6F2uSCSi\nhYmohkmqQa8lXx6mXbOnpyecnZ1Z8+Wrrq5Geno6FAoFBAJBs13ehoQSWDIxMYGzszNtBkQpG9YP\n6Az5mlKpfQcHB3h5ebGmb8IQmY6mRjapILupRlYq43f//n3WKWuqVCoNXQdtGqD//PNPhIWFoW/f\nvvj+++9ZVRYDgDNnzmDChAkagb9KpQKPx4ORkRFqa2sbbAq4MkTr4IKFdoBSumMGD/n5+RAIBHTw\nEBISgo4dO9InmqSkJCgUCr0YP9XV1dE1dkrrwdTUVENlsqksCGXHnZWVBXt7e1Y1V1Jjavfu3UPX\nrl1ZJchTfwqjfmMZFdBRQZ1UKqXfE6ajoz4uREyPArY1pbanARSl/lm/kZXZ85CXl8c6lUjgWRYm\nJSUFpqamEAqFWpUdDh8+jFWrVuHf//431qxZw5rvDhOpVNrgAh8ZGQlPT0+sWLECAoGgwX0mT54M\nqVSKX3/9lb5t3LhxsLOz4xocnwMXLLAAKjVI9TzEx8cjKysLnp6e8Pf3R3FxMZKTk3Hx4kX0799f\n7ydulUql0aAnFothbGysETzY2NjQvQkikahd3Q4bo6amBunp6aipqWHd2CHVKEgIgUAg0GoKg6m/\nQf1Q5Q3qPbG1tW3zDptqmK3v68AG2GYAxRzZLCsrQ1VVFXg8nsb3hA0jm48ePUJWVhZdfmvuM1JV\nVYXFixfjjz/+wA8//ICRI0eyJljUhvoNjtOnT0e3bt2wbds2AMD169cxdOhQbNmyBW+++SbOnj2L\nNWvWcKOTzcAFCyyEEIKysjJ88cUX+Oqrr2BnZ4fa2lrw+Xy6ZBEaGtqoupo+YGo9MOfYCSG09bCj\noyMrGp6YNVlKUZBN9WJKkKdHjx7o06dPq18zykGQGdAxa+z29vZNavg3tTaqa1/bETpDwWYDKKaH\niIeHB6ytrTUCOkrAizmdZKggh3L+fPLkidZy0hkZGZg+fTocHR0RExND9z29SNQPFoYPHw43NzdE\nRUXRx5w8eRJr1qxBfn4+Lcr01ltvtdOKXwy4YIGlLFy4ENHR0di1axfee+89VFZWIiEhAXFxcbh6\n9Sru3LmDLl26ICQkhP7p27ev3i/YVMObWCxGp06doFQqIRKJoFKpNGq57bGjksvldDOet7c3q7T2\nmQJLAoFA5yNn9WvsIpEItbW1Gg6b9vb2jV6omL4OAoGAVV37bDWAAp6ZyaWkpAAAfH19GzRYMl0d\nKTXWqqoqg4xsVldXIyUlBcbGxvD19W22+Y8QguPHj2PJkiX44IMPsHXrVtb0qHCwAy5YYCmJiYno\n1atXo6l9SoL4+vXrdOni5s2bsLOzo0WihgwZAi8vL51dsAkhKCkpQVZWFjp27AgPDw/6wsO8UFF9\nD9SOikrJtqSTvC1rY5uIEXNtnTp1goeHh8EyHfW1BZgXKiqAEIvFyM7OhrOzMzw8PNo9Zc6ErQZQ\nwN9ro3phtA3SmSObYrEYEolEaw2OlqwtMzMT3bt31yp7JZfL8dFHH+Gnn37C4cOH8cYbb7Amc8PB\nHrhg4SWA0lZISkqig4cbN27A3NwcgwcPpssWvr6+rbpQyeVyZGZmQiKRaGVKxRTBoX4o8x9mPVcX\n6dja2lq64Y1thllMvQk2CCxRFypmIyvwzH64c+fOzfqOGAo2G0BRzZ9lZWU6WRtTg6OxclJLRjZV\nKhVycnJQUlICgUCglVdHXl4ewsPDYWxsjOPHj6NXr15tej4cLy9csPCSUltbi1u3btHBw/Xr1wE8\nU5mkyhb+/v7PvWAzHQXbumNnpmOpXW5b/RQoTQe22W8DQHl5OdLT08Hn81nRjMdEJBIhLS0NlpaW\n6N69O635UFlZSfuOUO+JLpomW0JlZSVSU1NZZwAF/D1R0KFDBwiFQr2sjRACmUymkRGqP7JpZ2fX\noPGUKonweDz4+vo225hKCMH58+cxd+5cTJ06FTt37mSNJgoHO+GChX8IlMpkfHw8Pa4pl8sxcOBA\numzBVJnMz89HTk4OLCws9LJjZ2o9UOlYSuuBulBZWFg0ustl7tip0T62QO3uHj9+DA8PD1YJLKnV\nauTl5eH+/fvo27cvevToobE2qmmS2fegUqnocpI+pcOZollsa7BkNs1qO1GgSxob2TQ1NaW/JyqV\nCvn5+ejWrZtWJRGFQoF169YhKioK+/fvx9SpU1nzWnOwFy5Y+IdCqUxSWg8JCQkQiUTw9/dH586d\ncfHiRbz//vvYtGmTQXbFlOkPsxmMeUKkdAXKy8uRkZFBj8+xaTckFouRlpYGMzMz1o0dVldXIzU1\nFYQQCIVCrRoFm9rl1pcOb+t7QBlA1dTUQCgUsqrBkumfoK2Qkb5hjjY/evQIcrkcRkZGGgFdUw3G\nDx8+RHh4OCQSCU6cOAEvL692eAYcLyJcsMAB4NmuMi4uDgsWLEBhYSE8PT2RkpICPz8/uuchODgY\ndnZ2BtmFUCdEZvYBeHYB69SpE1xdXdvcCKYrmKN9vXv3NthIqzYw1Q61bXh7HlQ5iXpfqqqqNDJC\nLe3uf54BVHvDLIkIBAJWBaY1NTVISUmhgz+1Wq0R1CkUCloLJT8/HyNGjEBubi7ef/99jB8/Hl99\n9RWrJks42A8XLHAAeCbrOmzYMEycOBFffPEF+Hw+rTKZkJCAq1evIi8vj1aZpJQmmSqT+oLS2Tc3\nN4ejoyOdKieEaGQeDF1fB1onsGQoFAoF0tPTIZVK9aZ2WL+7v7KykjZkYgp41f+MUAZQjx8/hqen\nZ6MGUO0FIQT379/HvXv3WFcSAYCysjKkp6fTOiL1MwjMkc0rV65gy5YtKCoqgqmpKfz9/REZGYnQ\n0FC4u7uz6nmxBc4nonG4YIEDwLN067Vr1zBs2LBGf0/Vbameh4SEBGRmZsLDw0MjeNBljV6pVNIX\nlPo6+9T4KNXZLxaLoVQqG1hz62vcjnlBcXFxYZUTI/Bsx56RkUFLcBtqlFSlUmk4bFIZIWbTpLGx\nMdLT02FkZAShUNgiAyh9QwVYVVVVEAqFrPIRUavVuHfvHoqLi+Ht7a1Vr05ZWRnef/99lJaWYvbs\n2SgtLcXVq1eRnJyMV155RUPyWJ/s27cP+/btQ2FhIQDAx8cH69atw7hx4xo9PioqCpGRkQ1ur6mp\n0WvTa11dHWvGrtkGFyxwtApKZZIpFJWSkgI3NzeN4KG1u7KnT58iIyMDZmZm8PHxafaCUr++TokS\n1W/O08WJgJKSlsvl8PHx0bnAUltgjs95eHigS5cu7bpLIoTQmSCRSISKigqoVCqYmZnR45q6el/a\nikgkQmpqKmxtbeHj48OKNVHI5XKkpKRApVLB19e3WW8YQgiuX7+OiIgIBAUF4dChQxqBT21tLcrK\nytCjRw99Lx0AcP78eRgbG6NPnz4AgCNHjmD79u24e/cufHx8GhwfFRWFxYsXIzs7W+N2XTczP3ny\nBFu2bMFrr72GUaNGAQAyMzMRHR0NZ2dnvPnmmwZ7jdgOFyxw6ARCCMRiMe1tkZCQQKtMUkJR2qhM\nqlQqevfUp08fuLi4tPpiV1NToxE8yGQyjea8phQNn/ccqVFSZ2dnVklJA898HSgHS6FQyKoGS4VC\ngYyMDFRWVqJv377054XS4GAqTerSMl0bKGO3goKCRqdE2pvy8nKkpaXRol7NZcvUajX27NmDLVu2\nYMuWLVi0aBGrsl4UDg4O2L59O2bMmNHgd1FRUViyZAmdmSu4gP8AACAASURBVNIXt27dwtSpUzF8\n+HBs2rQJWVlZGDt2LIYPH474+HiMHj0a8+fPx9ixY/W6jhcBLljg0AtUmSAxMRGxsbF06pNSmQwJ\nCUFoaKiGymRCQgLUajU9Y69LZ03g7xE0qnRBaT0wg4emLlKU26FYLIa3t7dWgjeGgjl22LNnT7i5\nubHq4tCcARTzfaFGAy0sLDRKF/qQRKYem5rE8PX1ha2trc4fo7Uw7a49PT3RtWvXZu8jEonwwQcf\nICUlBTExMRg8eLABVtoyVCoVTpw4gfDwcNy9exfe3t4NjomKisLMmTPRrVs3qFQq+Pn5YdOmTejf\nv7/O1qFWq2FkZIQjR45g9+7dePvtt1FWVoYBAwYgPDwcd+7cwYoVK2BjY4MNGzZAKBTq7LFfRLhg\ngcMgUCqTycnJiI2NRUJCApKSkmBmZobAwEAQQvDHH3/gwIEDePvttw1ysauvaEhZDjNVJi0tLelx\nTTs7O1ZZcAPP0tNpaWmQy+WsGztsrQFU/TFaiUQCExMTDVEiXUzCUMJZDg4O8PLyYlWWiHpfFQqF\n1p4Yt2/fxrRp0+Dl5YXvv/+eVd4oAJCamorg4GDI5XJYW1sjOjoa48ePb/TYGzdu4N69exAKhZBI\nJNi9ezd+/fVX/PXXX+jbt69O1qNQKOjv8urVq/Hzzz+jtrYWP//8M/0Y586dw2effQZfX198+umn\nrPp+GRouWOBoN2praxEdHY3Vq1dDoVDAzs4O5eXlCAwMpMsWzalM6hLKcri+HDIhBM7OznBzc2OF\nHDJFSUkJMjMzDe45oQ0ymQxpaWlQqVRa6zo0RX3XU2oShtnM2hLjMkqc6sGDB6wTzgL+nv5xdHTU\nyt9FrVbj4MGD+Pjjj7Fq1SqsWrWKVT4aFAqFAvfv34dYLMapU6dw8OBBxMXFNZpZqI9arcaAAQMw\ndOhQ7Nmzp03rWL9+PSIiIuDm5oZjx46hrq4OU6ZMwbRp0/D777/jyJEjeP311+njt2/fjjNnzuD1\n11/HypUr2/TYLzJcsMDRbhw/fhyRkZFYsWIFVq9eDR6P16TKJFW2YKpM6hOxWIzU1FSYmJjAwcEB\nVVVVtBwyM/PQHloPdXV1yM7OZqV3AqB/AyiqxMUsXVDGZcyRzcYaFCkXS10EMbqGEEJnYrQNYqRS\nKRYuXIj4+HhER0djxIgRrAp8nseoUaPQu3dv/Pe//9Xq+FmzZqG4uBgXLlxo8WNJpVLY2NigoqIC\n48ePR01NDQICAnD06FGcPHkSb7zxBjIyMjBjxgz06dMHa9euhbu7O4Bnm4jZs2fj5s2bOHz4MAIC\nAlr8+C8DXLDA0W48evQIJSUlGDBgQKO/V6lUyMjIoMsWCQkJePr0KQICAmihqMDAQJ3u9pmSyPUb\nLCk5ZOa4JlPrgdrh6jN4oHwdrKys4O3tzSrvhPYygKJKXMzShUwma+A9IpFIkJ6ezkqHTap3Qi6X\nw9fXVyu9joyMDISFhcHZ2RnHjh3TqqeBTYwcORI9evRAVFRUs8cSQjBo0CAIhUJ8++23LXqcGTNm\nID8/HxcvXoSpqSmuXLmCkSNHwsnJCX/++Se6dOkClUoFY2NjxMTE4PPPP8err76KVatW0e9Dfn4+\n8vLyMHr06NY81ZcCLljgeGFQq9XIzc2lJaqvXr2K4uJi+Pn50aOagwcPbrXKZFVVFVJTU8Hj8SAQ\nCJrddTK1HqiLFKX1wAwgdHFRYtb/2dixzzYDKIVCofG+SKVSAM/0Hrp06QI7OztYWVmx4jV8+vQp\nUlNTYW9vD29v72bLSYQQREdHY9myZZg/fz42b97MqhJUY6xevRrjxo1Djx49IJVKERMTg08//RS/\n/fYbRo8ejenTp6Nbt27Ytm0bAGDDhg0ICgpC3759IZFIsGfPHnz//fe4du0aBg0a1KLHTkhIwNix\nY7F27VqsWrUKMTEx2LNnD5KTk3H48GFMmzYNSqWSfg3Xrl2Ly5cvIyIiAh988EGDv/dPFW3iggWO\nFxZCCAoLCzWCh7y8PPj4+NA9DyEhIXBycnrul5s5TeDq6tpqo6DnaT00lx5/HtXV1UhLS4NarWad\nSiSbDaCAvz0xAMDFxQUymYxWmjQ2NtaYuDB0SYn6/Obn52vdAFpTU4Ply5fj7NmzOHLkCF577TVW\nvd5NMWPGDPzvf//D48ePwefz4evrixUrVtA79eHDh8PNzY3OMixduhQ//fQTSkpKwOfz0b9/f6xf\nvx7BwcEtelxKZGnfvn1YuHAhfv75Z7z66qsAgE8++QSffvopbt26BaFQCLlcDnNzcygUCkydOhV5\neXk4cuQI+vXrp9PX4kWFCxY4XhqepzJJaT3UV5nMzc3FkydP6AuxrhX7qPQ4VbqQyWS0pkBzRkxM\nt8Nu3bqhT58+rEqdy+VypKens9IACnjWO5GZmdmoJwbVNMnse1Cr1RoTF/pUAFUoFEhLS4NMJtN6\nZPPevXuYNm0azMzMcPz4cfTs2VMva3tZoEYjpVIpcnNzMWfOHCiVSpw8eRK9evVCRUUFwsPDkZub\ni8zMTPrzIRaLUV1djRs3buDtt99u52fBHrhggeOlhRCCJ0+eaAQPlMrk4MGDYWZmhmPHjmHDhg2Y\nPXu2QVK5jWk9WFpaagQPFhYWtIiRRCKBj48PK9wOmbDZAEqlUiErKwtPnjyBj4+PVpoYhBBUV1dr\nZIUoMyZmVkgXkzlisRgpKSng8/nw9vZuNtNECMHZs2cxb948hIWF4YsvvmCVqRVboPoOmMTFxWHi\nxIkYNWoU8vLycPv2bYwfPx4//vgjLCwskJ2djfHjx8Pd3R2fffYZNm7ciLq6Ovz444/0a/xPLTvU\nhwsWDMC2bduwevVqLF68GLt27Wr0mPbSQv8nQakG/vLLL9iwYQMePHgAV1dXyGQyDYnq5lQmdQlT\n60EsFkMikaBDhw5QKpWwsrKCp6cn+Hw+a05WbDaAAp51vaempqJDhw4QCoVt+u4ws0LUbpMp4kUp\nTWr73jBLNtr2nSgUCqxduxbfffcd/vvf/2Ly5Mms+SywiY0bN6J79+6IiIigv7tVVVUYPXo0+vfv\nj6+//hpPnjxBUlISJk2ahGXLlmHz5s0AgOTkZEycOBGWlpZwdHTExYsXWTUlwxbYsx14Sbl58yYO\nHDgAX1/fZo+1tbVtoIXOBQq6g8fjITs7Gx9++CFCQ0Nx/fp1mJubIzExEXFxcThx4gT+/e9/g8/n\na5QtvL299ZaO7tChA5ycnODk5ASVSoXs7Gw8fvwYDg4OUCqVuH37NkxMTDS6+ttL64FqADUyMkJg\nYCCrDKAoK+6cnBy4ubmhZ8+ebQ74LCwsYGFhQQdECoWCnri4f/8+0tPTYWpqqvHeNNU0WVdXRzuA\nBgQEaFWyuX//PsLDw2kxMw8PjzY9n5cN5o5fJpM1eM/Lyspw7949rF27FgDg5OSE1157DTt27MCi\nRYswaNAgvPHGGxg0aBCSk5NRUlICPz8/AI1nKf7pcJkFPVJVVYUBAwbg66+/xubNm+Hn5/fczIIh\ntND/6Tx58gS///47pk6d2uCkTln7JiUlIT4+HnFxcUhKSoKpqSktUT1kyBD4+vrq3GSI2hGbmJhA\nIBDQF2K1Wo3KykqNHS6Px9OQqNZ3Yx51Ic7NzUWPHj1Y57BZV1eHzMxMiEQiCIVCvVhxN4ZKpdKw\n5xaLxTAyMtLIPNja2kIqlSIlJQXW1tYQCARalR0uXbqEmTNn4s0338SXX36pc+nzlwmmU2R2djbM\nzc3h6uoKAOjVqxdmzpyJ1atX08FFaWkpgoKCYGNjg+joaAgEAo2/xwUKjcMFC3okPDwcDg4O2Llz\nJ4YPH95ssKBvLXSOllNbW4tbt27RPQ/Xr1+HWq1GUFAQHTwMGDCg1TVkZmpamx0xpfVQvzGPUjO0\nt7eHra2tzk52zN4JgUBgsAuxtlRWViIlJQVWVlYQCATtKsXN1OGgggelUglCCBwcHODq6go7O7vn\n9ncolUps2bIFX331FXbv3o3333+fKzs8h+XLlyMvLw+nT59GTU0NHB0d8eabb2Lv3r3g8/lYuXIl\nrl+/jh07dtA+GaWlpZg4cSKSkpKwcOFCfPHFF+38LF4MuDKEnoiJicGdO3dw8+ZNrY739PREVFSU\nhhZ6SEiITrXQOVqOmZkZ3c+watUqKJVK/Pnnn4iLi0NCQgK+/PJLyGQyBAYG0qWLgQMHwsLCotmT\nPHOawN/fX6tJDCMjI/D5fPD5fLi6umo05olEIjx48AB1dXUawQOfz29VAyLTACooKIhVnhjMIKt3\n795wdXVt94sq872hyg5isRhdu3aljchqa2s1HDb5fD5daiwtLUVkZCQePXqEa9eucSN7WuDq6orv\nvvsOd+7cwYABAxATE4OJEyciJCQECxYswKRJk5CVlYVly5bhm2++gbOzM86fP4/OnTujsLDwhROy\nak+4zIIeePDgAQICAnDp0iX6C99cZqE+utRC59AfarUa6enptNYDpTLp7+9PZx6CgoIa9BkUFBSg\nsLBQ574OlJohU2VSLpfDxsZGo7b+vFR4aw2gDIVCoUB6ejqqqqogFAp1Pu7aViQSCVJSUmBpadkg\n2yGXyzUyD19//TWSk5Ph7e2NW7duISgoCD/88APrnlN7Q41B1icpKQkLFizAv/71L/z73/+Gqakp\nPv74Y+zZswdnz57FK6+8gitXrmDHjh24ePEievXqhcePH+PIkSN46623AEBDkImjabhgQQ+cOXMG\nEyZM0EgFq1Qq8Hg8GBkZoba2Vqs0cVu00Dnah+ZUJgcMGIDjx4+jtLQUJ0+ehLOzs97XRF2gmF39\nzN2tvb09XUbRpQGUPqCyHdqOHRoSZpOltgJVjx8/xrZt23Djxg1UV1fj4cOHcHJyQmhoKMLCwvDa\na68ZZO379u3Dvn37UFhYCADw8fHBunXrMG7cuCbvc+rUKaxdu5bO7mzZsgUTJkzQ6zpXrVoFd3d3\njcmxyZMnIz8/H3FxcXSvz4gRI1BeXo6zZ8+iV69eAIA//vgDEokEgYGBrJvieRHgggU9IJVKUVRU\npHFbZGQkPD09sWLFigYNNY3RUi309evXY8OGDRq3OTs7o6SkpMn7xMXFYdmyZUhPT0fXrl3x0Ucf\nYc6cOc0+Fof2MFUmT548iUuXLqFz587o1KkTBg4cSEtUd+rUyWC798akkC0tLWFmZobKyko4Ozuz\nTjuBMlkqLCxkZbZDqVQiIyOjRU2WT58+xezZs5GRkYGYmBgEBQVBJpMhOTkZCQkJcHd3x+TJkw2w\neuD8+fMwNjZGnz59AABHjhzB9u3bcffuXfj4+DQ4PjExEaGhodi0aRMmTJiA06dPY926dbh69SoC\nAwP1ssZr164hNDQUAPDNN99g9OjRcHFxQVZWFoRCIY4ePUq/XtXV1XBzc8P48ePx6aefNggOuCbG\nlsMFCwaifhlC11ro69evx8mTJ3H58mX6NmNj4yYFaQoKCiAQCDBr1ix88MEHuHbtGubNm4djx45x\nqmU6RqVSYePGjdixYwc2btyISZMmISEhgc48ZGRkwN3dne6NCA0NNahtck1NDdLT01FZWQlzc3PU\n1NTAzMxMY+LC0tKy3S7OcrkcaWlpqK2t1dpkyZBQ0w7m5uYQCARaNbveunUL06ZNg0AgwHfffcc6\n0S0AcHBwwPbt2zFjxowGv5s8eTIkEolG1vPVV1+Fvb09jh071ubHpsoO1H8JIairq8OHH36ItLQ0\nGBsbo1+/fpgyZQoGDhyIKVOmoKSkBOfOnaPVMK9evYqhQ4fiiy++wOLFi1k1wfMiwp6twz+M+/fv\na3x4xWIxZs+eraGFHh8f3yLTFBMTE3Tu3FmrY/fv3w8XFxc6ePHy8sKtW7ewY8cOLljQMUZGRqiu\nrsb169fpHpZ3330X7777Lq0ymZCQgLi4OOzduxezZs2Cq6srnXUIDQ2Fq6urXk52TAOokJAQmJub\nQ6VSobKyEiKRCCUlJcjOzoaxsTEdOBhS66G8vBxpaWno2LEj/Pz8WJftePToEbKzs2lPkeZeE7Va\njQMHDmDt2rVYs2YNPvroI9btcFUqFU6cOIHq6uomvRgSExOxdOlSjdvGjh2rdU9Wc1Cf9cLCQvp1\nNTIyQpcuXWBvbw8/Pz9cvXoV06dPx6+//oqRI0di//79uHnzJkaOHAmVSoUhQ4bg4MGDGDFiBBco\n6AAus/CSsH79emzfvh18Ph9mZmYIDAzE1q1b6XpdfYYOHYr+/ftj9+7d9G2nT5/GpEmTIJPJWFUL\n/idBCEFlZSWdeUhISMDt27fRuXNnetoiJCQE7u7ubToBMk2MmquvUz4KzL4HptYDpSegyxOyWq3G\nvXv3UFxcDE9PT9Z1ratUKmRmZqK8vBxCoVCrzIBEIsGCBQtw7do1HDt2DMOGDWNVKSU1NRXBwcGQ\ny+WwtrZGdHQ0xo8f3+ixpqamiIqKwrvvvkvfFh0djcjISNTW1rZ5LWq1GuvXr8fmzZvx66+/IiQk\nBDY2NkhKSsKUKVNw5swZ9OvXD3PmzMHt27excOFCLFy4EMuXL8fatWuhUCg0GkubapDk0B4uWHhJ\nuHDhAmQyGdzd3VFaWorNmzcjKysL6enpjZ7I3N3dERERgdWrV9O3Xb9+HSEhIXj06BHXAMQSKBts\nSmUyISEBycnJbVKZbKsBlFqtbmDNrVKpNBwc+Xx+q3fMNTU1SElJgVqthq+vL+sEiaqqqpCSktIi\nSem0tDSEhYWhW7duOHbsmNYZQEOiUChw//59iMVinDp1CgcPHkRcXBy8vb0bHGtqaoojR45g6tSp\n9G0//PADZsyYAblcrpP13Lt3D1u2bMFvv/2GOXPmYNGiRbC3t8fMmTORn5+PP/74A8Az+2uRSIQj\nR45AqVSiqKiIO3/pAfbk9DjaBLNrWSgUIjg4GL1798aRI0ewbNmyRu/TmIJhY7dztB88Hg82NjYY\nM2YMxowZ00Bl8sKFC/jkk0+0VplkGkD169evVWl9IyMj2NrawtbWtoHWg1gsxsOHD6FQKMDn8zWy\nD9o8VmlpKTIyMtC5c2e4u7uzLkVPOVlqq2RJCMH333+P5cuXY9GiRdi4cSOrSilMTE1N6QbHgIAA\n3Lx5E7t378Z///vfBsd27ty5QfN0WVmZTqZ7KKXFPn364PDhw/jwww9x5swZXL9+HRcuXMCCBQuw\nbt06nDt3Dm+88QY2btyIP/74A0lJSSgqKgK3/9UP7PzUcrQZKysrCIVC5ObmNvr7pr7sJiYmrGy2\n4ngGj8eDhYUFhg8fjuHDhwN4pjJ5+/Zt2l3zs88+g1qtRmBgIF228PLywocffoju3btj7ty5Ot15\n8Xg8WFtbw9raGj169KC1HqisQ1ZWFmpqamith8YcHFUqFXJyclBSUgJvb2+DjJS2BMq3o6ysDL6+\nvujYsWOz95HJZPjwww/x888/4/jx4xg/fvwLFYgTQposKQQHB+P333/X6Fu4dOkSrZLYEi5fvoyA\ngABaW4J6jaiJhW3btuH8+fNYunQpRo8ejQ8++AD29vZ48OAB1Go1TExMMGbMGAQGBsLW1hY8Ho9z\nitQDXLDwklJbW4vMzEx61Kg+wcHBOH/+vMZtly5dQkBAgFb9Ci0d1YyNjcWIESMa3J6ZmQlPT89m\nH4+jaczMzDB48GAMHjwYK1eupFUmqeBh586dqKurQ6dOndC5c2fk5OSAz+drpTLZGng8HiwtLWFp\naUn3Gsjlcjp4uHfvHu3gSE1aFBcXo0OHDggKCoKFhYXO19QWqqurkZKSAmNjYwQFBWlVdsjJycH0\n6dNhZWWF27dvw83NTf8LbQOrV6/GuHHj0KNHD0ilUsTExCA2Nha//fYbgIbTW4sXL8bQoUPx2Wef\n4c0338TZs2dx+fJlXL16tUWPe+PGDYwZMwb79+9HRESERgBpbGwMQghMTU3x9ttvIyAgAP/3f/+H\no0ePIi8vD7m5uZg7dy6AZ4ENVU7jRJb0A/eKviQsX74cr7/+OlxcXFBWVobNmzdDIpEgPDwcwDMx\nk4cPH+K7774DAMyZMwd79+7FsmXLMGvWLCQmJuLQoUMtGnvy8fFpMKrZHNnZ2fRoE4AmRzs5Wo+J\niQkCAgLg7+8PS0tLXL58Ge+++y4EAgGuX7+OGTNmoKKiolmVSV1ibm6Ozp0707V6ysGxuLgYxcXF\nAJ65PObn59OZB30FMy2hpKQEGRkZ6N69O/r06aNV2eH06dOYP38+IiIisH37dlbJZDdFaWkppk2b\nhsePH4PP58PX1xe//fYbRo8eDaDh9NbgwYMRExODNWvWYO3atejduzeOHz/eIo0FQgiCgoKwZMkS\nrFmzBl5eXg02N9T7r1ar4erqilOnTmHfvn1ITU1FZmYmjhw5gsjISI3PCRco6AeuwfElYcqUKYiP\nj0d5eTmcnJwQFBSETZs20c1JERERKCwsRGxsLH2fuLg4LF26lBZlWrFihdaiTOvXr8eZM2fw559/\nanU8lVkQiUSclK2BuH//PkaNGoX9+/fjlVdeoW+nJg1iY2ORkJCAhIQEWmWSapocPHgw7O3t9Xax\nViqVyMrKQnl5OQQCAezs7DTMsSorK7W2f9YHarUa2dnZKCkpgY+PDzp16tTsfWpra/Hxxx8jOjoa\n33zzDSZOnNjuwQ6bYU4oBAcHQ6VSITo6mu6bqA9VWnjy5Al+/vlnnDlzBj/++GOrTdw4WgYXLHC0\nipaOalLBgpubG+RyOby9vbFmzZpGSxMcukMbpTpqjJIqWyQkJCAvLw8+Pj60UFRISIjOVCYpESMz\nMzMIBIJG0/pMrQfKR4HSeqCCBxsbG71cjGUyGVJSUsDj8eDr66tVWaSoqAjh4eFQKBT48ccf4e7u\nrvN1vYxQJQORSISePXti4sSJ+Pzzz1vkbsqpMRoGLljgaBUtHdXMzs5GfHw8/P39UVtbi++//x77\n9+9HbGwshg4d2g7PgKMpKLEhZvBAqUwyxzW7devWoos10zuhZ8+e6Nmzp9b3p7QemNkHAA2suds6\nS19WVob09HR06dJFKy0LQgh+++03zJ49G2+99Rb27NnDup4LNvG8C/vFixcxbtw4fPnll5g5c6ZW\nGQNOP8FwcMECh06orq5G79698dFHHzU5qlmf119/HTweD+fOndPz6jjaAiEE5eXlGsHDX3/9BVdX\nVzrrMGTIELi5uTV54q6rq0NGRgYqKyshFAphb2/f5jVJpVI6eKC0Hupbc2u746QMwB49eqT1NEZd\nXR02b96M/fv348svv0R4eDhXdngOzEDh8OHDKCoqgrGxMZYsWQIrKysYGRnRjpGnT5/GyJEjudeT\nRXDBAofOGD16NPr06YN9+/ZpdfyWLVtw9OhRZGZm6nllHLqkvsrk1atXcfv2bTg7O2toPVA78//9\n738oKipC//794ePjo5eGP0rrgRk8KBQK2Nra0qULOzu7Rid9KBEoQgh8fX1p58LnUVJSgoiICJSV\nleHEiRMQCoU6f04vK2+99RaSk5MREhKCW7duoWvXrti6dSvd3DhixAhUVFTgxIkT8PDwaOfVclBw\nwQKHTqitrUXv3r0xe/ZsrFu3Tqv7TJw4EU+fPqWV2DheTKgLdWJiImJjY3H16lUkJyfDxsYGvXr1\nwp9//omFCxdi7dq1ButUp8SrqMBBJBJpaD1QfQ+VlZVIS0vTWgSKEIKEhARERERg+PDhOHDggMZ0\nD0fTyOVyLF26FJmZmTh58iQ6duyIxMREhISEYMqUKfjoo4/g5+eHmpoa9OzZEwEBAYiKitJK04JD\n/3DFHo5WsXz5csTFxaGgoABJSUmYOHFig1HN6dOn08fv2rULZ86cQW5uLtLT07Fq1SqcOnUKCxYs\naNHjPnz4EGFhYXB0dISlpSX8/Pxw+/bt594nLi4O/v7+MDc3R69evbB///6WP2GOJqFEmUaPHo0t\nW7YgNjYWWVlZcHNzQ05ODkJDQ7Fv3z64urrinXfewe7du3Hr1i3U1dXpdU0WFhbo2rUrfHx8MGTI\nEISGhsLNzQ1qtRp5eXmIi4vDn3/+CRsbG9jZ2TW7HpVKhe3bt+Ptt9/GmjVrEB0dzQUKz6H+PlSp\nVGLAgAH4/PPP0bFjR3zxxRcYP348wsLC8Ouvv+K7777Dw4cPYWFhgR9++AGVlZVaZXk4DAM3kMrR\nKoqLizF16lSNUc0bN27A1dUVwDNZ3Pv379PHKxQKLF++nD4Z+Pj44JdffmnSqKYxRCIRQkJCMGLE\nCFy4cAGdOnVCXl7ec0cxCwoKMH78eMyaNQtHjx6lrbidnJw4d009IZFIEBwcjNDQUPz+++/g8/lQ\nKBS4devWc1Um/f399ToGR2k92NnZoaqqClZWVujRowdkMhnu37+P9PR0mJub05kHKysrummyoqIC\ns2bNQnZ2Nq5cudIiN9h/Io01MlpbW2PMmDFwdXXF/v37cfDgQRw4cADvvPMOFi9ejJiYGLi5uSEy\nMhIjR47EyJEj22n1HI3BlSE4XhhWrlyJa9euISEhQev7rFixAufOndPoi5gzZw7++usvJCYm6mOZ\nHACSkpIwaNCgJhvUlEol/vrrL9oc6+rVq6iursagQYNoW+6BAwfqXJiJsrx2cnKCp6enxgVNqVTS\nY5oikQjffPMNLly4AIFAgJycHHh4eNDpc0Ozbds2/PTTT8jKyoKFhQUGDx6Mzz777Lk1/aioKERG\nRja4vaamRisVyrZy79497N27F66urujbty9ee+01+neTJk1C9+7d8Z///AcAMHPmTJw8eRICgQAn\nT56kxbu4aQf2wGUWOF4Yzp07h7Fjx+Kdd95BXFwcunXrhnnz5mHWrFlN3icxMRFjxozRuG3s2LE4\ndOgQ6urqOCtuPdGckp+JiQn8/f3h7++PZcuWQa1WIyMjgxaKioqKQnl5Ofz9/enMQ1BQUKu1FdRq\nNfLz83H//v0mLa9NTEzQsWNHOhjw8PCAo6MjYmNjL2p2pwAAG6JJREFUYW1tjZs3b8LT0xOhoaF4\n++23ERYW1uJ1tJa4uDjMnz8fAwcOhFKpxMcff4wxY8YgIyPjua6ctra2yM7O1rhNX4EC88IeGxuL\nUaNGITQ0FFeuXMG9e/ewatUqLF++HHK5nB7FraysRE1NDSQSCS5dugQXFxcNR04uUGAPXLDA8cKQ\nn5+Pffv2YdmyZVi9ejWSk5OxaNEimJmZafRHMCkpKWkwBufs7AylUony8nLOypYlGBkZQSAQQCAQ\nYMGCBbTKZHx8PK00+uDBA/Tr14+ettBWZbK2thapqalQKBQYNGgQrK2tm11PZWUl5s2bh+TkZERH\nR2PYsGGoq6vDnTt3EB8fD4lEoqunrhWURwPF4cOH0alTJ9y+ffu5OiU8Hs9gdtjUhT06Ohr5+fnY\ns2cP5s2bB4lEgjNnziAyMhJdunTBjBkzEBYWhk2bNuHixYvIzc3FuHHj6NIOJ7LETrhggaNJCCH0\nboEN885qtRoBAQHYunUrAKB///5IT0/Hvn37mgwWAM6K+0XEyMgI7u7ucHd3x8yZM0EIQVFREV22\nWLNmDa0ySQlFNaYyWVRUhMLCQjg6OsLPz0+raYyUlBSEhYXB1dUVd+7coYPNDh06IDAwsEX+B/qi\nsrISAJpVOqyqqoKrqytUKhX8/PywadMm9O/fX2/rOnHiBJYvXw6ZTIbTp08DeJbdmD59OlJSUrBi\nxQpMnz4dK1euhJubGx4/foyuXbti8uTJAJ59N7lAgZ1wOR4ODagLqUqlAo/Hg7GxMWsuql26dKG9\nLii8vLw0Ginrw1lxvxzweDy4ubkhPDwcBw8eRHZ2Nh48eIBVq1aBx+Ph008/Re/eveHv748FCxYg\nOjoaixcvxvDhw9GjRw/4+Pg0GygQQnDkyBGMGjUKU6dOxcWLF1lnlQ08W+eyZcswZMgQCASCJo/z\n9PREVFQUzp07h2PHjsHc3BwhISFN2ta3FJVK1eC2wMBAhIWFQSqVQiqVAgBtc71ixQp06NABJ06c\nAPDMz2bp0qV0oECdczhYCuHgqEdSUhJZtGgRCQkJIZMmTSIxMTHk6dOn7b0sMnXqVDJkyBCN25Ys\nWUKCg4ObvM9HH31EvLy8NG6bM2cOCQoKatFjFxcXk/fee484ODgQCwsL0q9fP3Lr1q0mj79y5QoB\n0OAnMzOzRY/LoR1qtZqUlZWRU6dOkZkzZxIbGxtia2tL+vXrR8LCwsi+fftIamoqkUqlpLq6usFP\nWVkZCQsLIx07diS//vorUavV7f2UmmTevHnE1dWVPHjwoEX3U6lUpF+/fmThwoVtXoNSqaT//9Kl\nS+TGjRukpKSEEELIvXv3yPjx44lQKCSPHj2ij8vKyiLdu3cnV65cafPjcxgeLljg0CAlJYV07NiR\njB8/nhw8eJDMnTuX+Pn5kVdeeYXcvXu3XdeWnJxMTExMyJYtW0hubi754YcfiKWlJTl69Ch9zMqV\nK8m0adPof+fn5xNLS0uydOlSkpGRQQ4dOkQ6dOhATp48qfXjPn36lLi6upKIiAiSlJRECgoKyOXL\nl8m9e/eavA8VLGRnZ5PHjx/TP8yTLIfuiYuLI127diWTJk0iRUVF5Pz582T58uUkKCiIdOjQgXTr\n1o288847ZPfu3eTWrVtEKpWSO3fuEB8fHxIcHEyKiora+yk8lwULFpDu3buT/Pz8Vt1/5syZ5NVX\nX9XJWioqKkhwcDBxd3cnffv2JR4eHuTQoUNEqVSSy5cvk4CAADJs2DCSlZVFioqKyCeffEK6dOlC\nUlNTdfL4HIaFCxY4NFi3bh1xd3cnYrGYvi03N5f85z//IdevX9c4Vq1Wk7q6OqJSqQy2vvPnzxOB\nQEDMzMyIp6cnOXDggMbvw8PDybBhwzRui42NJf379yempqbEzc2N7Nu3r0WPuWLFigYZjeagggWR\nSNSi+3G0jX379pGvvvqqQWZArVYTqVRKLl26RD7++GMydOhQYm5uTvh8PjE1NSVLliwhtbW17bTq\n5lGr1WT+/Pmka9euJCcnp9V/IyAggERGRmp9n8a+2yqVipSXl5MRI0aQKVOmkIqKCkIIIUOHDiW9\nevUid+/eJSqVihw4cIDY29sTPp9PIiIiiKenJ0lISGjV2jnaHy5Y4NDgiy++IL179yYZGRkNfqdQ\nKNphRe2Pl5cXWbJkCZk4cSJxcnIifn5+DYKU+lDBgpubG+ncuTN55ZVXyB9//GGgFXM0h1qtJjKZ\njJw6dYp8/PHHrC47EELI3LlzCZ/PJ7GxsRqZKplMRh8zbdo0snLlSvrf69evJ7/99hvJy8sjd+/e\nJZGRkcTExIQkJSVp9ZhUoKBQKEhGRgaprq6mf1dQUED8/f3J48ePCSHPNhnW1tYa3wuRSERWrVpF\nvLy8yMGDBxv8XY4XCy5Y4NCgpKSEDB06lJiampKIiAgSGxtLp86pL/njx4/JgQMHyNixY8nUqVPJ\n2bNnmwwk1Gr1C596NzMzI2ZmZmTVqlXkzp07ZP/+/cTc3JwcOXKkyftkZWWRAwcOkNu3b5Pr16+T\nuXPnEh6PR+Li4gy4co6Xhcb6XwCQw4cP08cMGzaMhIeH0/9esmQJcXFxIaampsTJyYmMGTOmQXaw\nMZiB07Vr10hwcDCZNm0aiY2NpW8/d+4ccXd3JwqFggwfPpx4enqSGzduEEIIkclkJDk5mRBCSGpq\nKgkLCyMDBw4kDx8+JISQF/588E+FU3DkaJTo6GicOnUKFRUVmDNnDqZMmQLg2SjWsGHDYGtri7Fj\nx6KgoADx8fFYvXo1pk2bBuCZtoGZmVmbbYjZgqmpKQICAnD9+nX6tkWLFuHmzZstUoHkLLk5XiT+\n85//4OOPP8aHH36I0NBQDBkyhBaAevLkCQIDA1FUVISpU6di165dtJjViRMn8Pvvv2Pbtm1wdHTE\n5cuXsXXrVhBCcOXKlfZ8ShxtgNNZ4GiUSZMmITAwEFu3bsXs2bPRq1cv9O/fH3v37kVRURHKy8vp\nY8+dO4fp06fjtddeg729PQ4fPoxvvvkGW7duxZ07d+Dq6opJkybBycmpweNQ41dMLQdCCHg8HmvE\nWZoa2Tx16lSL/k5QUBCOHj2qy6VxcOiFs2fP4uDBgzhz5gzGjh3b4PdWVlaYNm0aDhw4gEmTJtGB\nQnJyMjZv3ozhw4fT4lejRo1CVlYW8vLyWPOd5mg5nM4CB83JkyeRk5MD4Jn0be/evbFt2zY4OTkh\nNjYW1dXVuHLlCkQiETp27Ah/f39s3rwZMpkM9vb2KCgoQG1tLUpLS1FSUoKoqCioVCp89dVXmDx5\nMmQyGf1YVJBgbGzcQMuB+t2ECRMwd+5cek67vQgJCWkgmZuTk0ObZmnL3bt3W6QY6ebmBh6P1+Bn\n/vz5Td7n1KlT8Pb2hpmZGby9vWlhHA6OlnD37l306NEDwcHB9G35+fn4888/8fvvv0Mmk2Hx4sUY\nN24c3nnnHYwZMwZTp07F6NGj8corr2D37t0wMzOjv8uzZs3Czp07uUDhBYbLLHDQHDt2DL/88gsi\nIyMRGBiIuro6/PDDD6iqqoKPjw8IIcjKysLevXsxfvx4nDx5EleuXMHevXthY2ODqqoqSKVS3Lhx\nAwMHDsT3338PJycnvPvuu5gwYQK++eYbLF68GCqVCv/73/+wc+dOAMArr7yCyZMnw8XFBQDoE0pS\nUhLmz5//XDEdKguhT5YuXYrBgwdj69atmDRpEpKTk3HgwAEcOHCAPmbVqlV4+PAhvvvuOwDPLLnd\n3Nzg4+MDhUKBo0eP4tSpUy3KRty8eVND+CYtLQ2jR4/GO++80+jxiYmJmDx5MjZt2oQJEybg9OnT\nmDRpEq5evcoK1UGOF4fCwkJUV1dDqVRCoVBgzZo1SE1NRVJSEgDA0dERcXFx+PbbbzFkyBB6k/HT\nTz/RbpHMLII+3UQ5DER7NkxwsAe1Wk3i4uLIlClTiIODA93B7+bmRmbPnk2qqqrI/7d37zFNnW8c\nwL9QoNykMoFIFaqiFgSmRhyCaH4mChOdqEwR48CxKUZFLtmGGnXq5JYsZjNZpnPjYgCnY2xeyKLg\nBYTq5izd5OYcFtwUdchKi1Sh7fP7g/UIUlBUkMv7SfzDl/ec89Jo+/ac50JEZG9vT4cOHepwbEtL\nC1VXV5NOp6OioiISi8Vc9LM+mGnJkiUUGhpKRG352Xl5ebR//37avXs3eXl5kb+/P929e5cLrrp7\n9y4ZGRlRfn5+l2t++PDhS38dutLTlM2UlBRycXEhc3NzsrW1JT8/P8rLy3uhNURHR5OLi0uXkfvL\nly/vlEMfEBBAK1aseKHrMkPPjRs3yNTUlMRiMZmYmNC0adNoz549JJFI6MKFC+Tt7d3lvyudTscy\nHgYhtllgDLp06RKlpqZ2youOi4sjT09PkslkRNSWIdHY2Mj9/MCBA2RnZ0fXrl0joscf6NOmTaPY\n2FiD19LpdOTp6Ulbt27lxjIzM8nOzq7LwkdKpZKCgoK6POdg8+jRIxoxYgQlJCR0OcfJyYn27t3b\nYWzv3r3k7Ozc28tjBqHy8nLKysqio0ePklKpJLVaTURtXwDeeustCg4OJqLHWVL9Pf2UeTHsMQTD\n0el0XCOXrhrm7Ny5E3fu3MG8efMgFovh4eEBS0tLREVFYdSoUaioqIBKpeKezfP5fKjVapSVlSEu\nLg4AUF5ejszMTJSWlsLe3h7vv/8+hg8fjqamJu7W5YkTJzBlyhQucEqP/nvsIJfL0djYCEtLS27t\ng7md7Y8//giFQoHVq1d3OaerDptP9sZgmGcxadKkToG9AKBSqfDw4UOu26X+/x3r6zC4Dd53V6bH\njI2NuWeM9F/HyfaICMOGDUNWVhbOnz+PJUuWcK2Fx4wZg1u3bqG2thbm5ubYs2cPAKCurg7btm2D\npaUlli1bhoaGBixevBjFxcUICAgAn8/Hhg0bUFxcjFGjRkGj0QAAioqK4Ofn16mdMP2X6VtWVga1\nWv3UZ/FEBI1G0+l3GWi++eYbzJ8/H0KhsNt5hjpssjdx5mV48OABSktLMX/+fKhUqm47vTKDD7uz\nwBikj7x/ckz/4WPoW4dcLkddXR2ioqJw8+ZNeHp6gs/no7m5GUlJSTA1NUVBQQEUCgWOHj3Ktcr9\n448/4OPjAycnJ/D5fPz777+4c+cO3njjjU7R0/pvMRUVFTAzM4Onpye3Nj39XQb9Wp+lLXF/Vltb\ni4KCAuTm5nY7r6sOm/2xcyIzsOzduxeXLl1CaWkpfH19kZGRAWDw39FjHhvY76JMn2tfC0Gn08HI\nyIh7s5DL5VAqlQgLC8OoUaOQnp6Ou3fvIiQkhNtYCAQC2NjYQCqVYurUqZDJZEhOTgafz4eLiwsA\nID8/HwKBgPv7k9RqNaqrqzFy5EiMGTOmw7qAtg1FZWUlsrKycPbsWYwdOxZhYWGYN2+ewTe29o9f\n+qO0tDQ4ODhgwYIF3c7z8fFBfn4+YmNjubHTp0/D19e3t5fIDHI+Pj64d+8eVq9ejcDAQACARqMZ\n8BtxpgdeUawEM8g8evSI1q5dS2KxuNt5Wq2WYmNjycLCgtzd3SkyMpLMzMxo+fLlJJfLiehxZsGT\nTZj0AVRlZWU0Z84c2rZtG3fO9mQyGTk5OVFISAh99dVXFBERQa+//jqdOXOGm1NdXc01wOnPtFot\nOTs7U3x8fKefPdkLoKSkhHg8HiUnJ1NlZSUlJyeTiYkJV4b3WYlEIoOlhdevX29wflpamsH5+oC4\noSAxMZG8vLzI2tqa7O3tKSgoiKqqqp56XE5ODrm5uZGZmRm5ublRbm5uH6z2+bQv6c6yHYYetllg\nXoqWlhbKycmh5ORkIiJqbW0ljUbT5ZtKQ0MDnTx5kuRyOQUFBdHWrVtJpVIREZGtrS1t2bKFWltb\nOxyjP9eRI0fI29ubazOt0Wi4jcSdO3fonXfeIS8vrw7HJiQk0MSJE4morXb9mjVrSCwWU15eHoWF\nhdGBAweooaHB4Fo1Gk239ex7Mwr81KlTXKvrJz3ZC4CI6LvvviOxWEympqbk6upK33//fY+vee/e\nvQ7NivLz8wkAnTt3zuD8tLQ0srGx6XCMvsHQUBEQEEBpaWlUVlZGMpmMFixYQM7OzlzKsSESiYR4\nPB4lJiZSZWUlJSYmPtfmjmH6AusNwfQ5MhB0p8+CaG1thbe3N3bu3IlFixYZPG7Xrl04c+YMUlNT\nMX78+A4/KyoqQnR0NCorK2FlZQVnZ2esXLkSCoUCeXl5OHXqFHQ6HSIjI1FUVITw8HBYWVkhJycH\nfn5+SE1NfWpQYPvntEPhVmxMTAxOnjyJ69evG3xd0tPTERMTA4VC8QpW1z/9888/cHBwQGFhIZc1\n8KSQkBAolUr89NNP3Nibb74JW1tbHD58uK+WyjDPhEWmMH2ufdyD/g+PxwMRwdTUFFKptNNGQX9c\nS0sLZDIZiAgCgaDTObVaLWpqalBSUgKJRIKwsDAUFhYiPT0dAoEALS0tqKurg1QqRVxcHD7//HMk\nJiYiLi4O586dg0Qi4a5TUFCAwMBA+Pn5ISMjAyqVCsDjIEsiwtixY5Gdnd0h4+LMmTPYtGkT1Gp1\nr76OfUFffTIiIqLbDVRTUxNEIhFGjx6NhQsXorS0tA9X2f80NjYCAF577bUu51y8eBH+/v4dxgIC\nAjo0LGOY/oJtFphXpn2/A/3fdTpdt2mODx48gKOjI0pKSjBx4kTMnDkT27dvx9mzZ/Hw4UOIRCI0\nNzfDyMgIYrEYsbGxOHnyJGpqapCVlQUnJyf8/vvvsLS0xNKlS7nzuri4YNiwYVAqlQCAffv2ISIi\nAtbW1vD398fp06exadMmzJ07F1euXIFKpcLBgwfB4/Ewfvx4mJiYwNjYGK2trbhw4QIOHjwICwsL\nDPQbd89S38HV1RXp6ek4fvw4Dh8+DHNzc8ycORPXr1/vu4X2I0SEuLg4+Pn5wcPDo8t5rC4GM6C8\nimcfDPMylJSU0NatW8nT05OEQiFXhnrZsmU0Z84cunnzJhERNTU1kUKhIKK22Ir4+PhOMQ2pqak0\nevRoun37NhG1xU188sknXHXKvLw8sre3J19fXyovL6eSkhISCARkZGREbm5utHbtWqqpqaH6+npa\nunQpvf3229y5tVrtgA0I8/f3p4ULF/boGK1WS5MnT6aoqKheWlX/tn79ehKJRPTXX391O8/U1JSy\ns7M7jGVmZhKfz+/N5THMcxncD1uZQYf+S9nk8Xjw9fWFr68vEhISALTddQCAhIQEbNy4EZMnT4aH\nhwdEIhEmTJiAmJgYNDc3o7q6Gm5ubtw51Wo1KioqYGdnB0dHRxQUFKCpqQnvvfcebGxsAACBgYGw\nsLCAs7MzhEIhJk2ahKlTp2LEiBHw9fVFTk4O5HI5XF1d8dtvvyEmJgYPHjyAsbExLCws+v6Fegme\ntb7Dk4yNjTF9+vQheWchKioKx48fR1FREUaPHt3tXFYXgxlI2GMIZkAxMjLi6iHodDpoNBquM6OV\nlRV0Oh0mTJiAU6dOobi4GMHBwRAKhfDx8YGNjQ2qqqoglUrh5eXFnbO+vh4VFRWYMmUKAODq1asQ\nCoVwdHTkKkr+/fffsLa2hpubG4YPHw61Wg25XI7Zs2cjLi4OEokE//vf/3DlyhU0Njbil19+wcqV\nK2Fra4uQkBDcv3+/j1+pF/es9R2eRESQyWQ9ascNtAWLbtu2DWPHjoWFhQXGjRuH3bt3P7X6ZmFh\nIaZNmwZzc3OMGzcO+/fv79F1XwYiwsaNG5Gbm8vV9ngafV2M9lhdDKa/YncWmAHL2Ni4U5Gl9pUb\nDVWZdHJywpIlS7g2ugBQXV2N8vJyhISEAGgLTrO1tUV9fT3Xm+Ly5ctobW3lsi9+/vlnEFGHwlFa\nrRZlZWVQKBQQi8WIjIzEjRs3sGzZMhw7dgwRERG98jr0Bp1Oh7S0NISHh3fK9tAX3UpKSgIA7Nq1\nCzNmzMCECROgVCqxb98+yGQyfPHFFz26ZkpKCvbv34+MjAy4u7vj119/xbvvvguBQIDo6GiDx8jl\ncgQGBmLNmjXIzMxESUkJ1q9fD3t7ewQHBz/fL/8cNmzYgOzsbBw7dgzDhg3j7hgIBALuztKTr1t0\ndDRmz56NlJQUBAUF4dixYygoKEBxcXGfrZthntkrfQjCML1Ip9N1W+tBTyKRkLe3N1cU6uLFiyQS\niejLL78kIqLS0lKaNWsWubm5kVQqJSKijz/+mLy9venq1avceRoaGig4OJjmzp3LjSmVSgoODqag\noCBuTQNBT+o7xMTEkLOzM5mZmZG9vT35+/uTRCLp8TUXLFhAERERHcaWLl1Kq1at6vKYjz76iFxd\nXTuMRUZG0owZM3p8/RcBA0WpAFBaWho3p7fqYjBMX2CbBWZIedZgwx07dpCVlRV5eHjQihUraOTI\nkRQaGspVfVy0aBGtWrWK6uvruWOqqqrI1dW1Q5tohUJBAQEB3IfEQA107AtJSUkkEom4DYpMJiMH\nB4dOQYDtzZo1izZt2tRhLDc3l0xMTDpUHGQY5sWwxxDMkNJVbwh9CmdLSwuampqwa9cuREVFoaqq\nCiYmJrh27Rrc3d25vHkHBwfcvn0bw4cP585TW1uLuro6zJ07lxurr6/HlStX8NlnnwFgbXy7Ex8f\nj8bGRri6uoLH40Gr1SIhIQGhoaFdHtNV+qFGo0F9fX2P4yYYhjGMBTgyQ56xsTH3Id7c3Iz09HSk\np6fDzs4OYrEYX3/9Ne7fv9+hgE54eDjKysogFAqxceNGAG2BkdbW1lwnTAC4ceMG7t+/j3nz5gFg\nm4XuHDlyBJmZmcjOzoZUKkVGRgY+/fRTrsNhVwy15TY0zjDM82N3FhimHQsLC7S0tCA+Ph4ffPAB\nbG1tYWlpid27d2P69OncPD8/P/z555/Iy8vjCjldvnwZI0eOBPA4xVMqlcLR0REODg5PLSM91H34\n4YfYvHkzVqxYAQDw9PREbW0tkpKSEB4ebvCYrtIPTUxMMGLEiF5fM8MMFWyzwDDt8Pl8bN68GZs3\nb8b169dRVVUFHx8fLitCj/4rTb148WJu7Ntvv0VdXR2Atm+1zc3NOHHiBJdBoa8PwRjW3Nzc6TER\nj8frNnXSx8cHJ06c6DB2+vRpeHl5wdTUtFfWyTBDEWskxTAvoH1TKUPKy8tBRPDw8GB3Fp5i9erV\nKCgowIEDB+Du7o7S0lKsXbsWERERSElJAQBs2bIFt27dwqFDhwC0pU56eHggMjISa9aswcWLF7Fu\n3TocPny4T1MnGWawY5sFhmH6BZVKhe3bt+OHH37AvXv3IBQKERoaih07dsDMzAxA24aipqYG58+f\n544rLCxEbGwsysvLIRQKER8fj3Xr1r2i34JhBie2WWCYXsTuJjAMMxiwbAiG6UVso8AwzGDANgsM\nwzAMw3SLbRYYhmEYhukW2ywwDMMwDNMttllgGIZhGKZbbLPAMAzDMEy3/g911SZz8hdt8wAAAABJ\nRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgsAAAGMCAYAAABUAuEzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAPYQAAD2EBqD+naQAAIABJREFUeJzsnXd4G+ed578z6JWk2MUuqtKWZTWry443tqJLnGxuY/t2\nvfElzm2yT2xls9m72806Zfe8T3zOOZFL8sQpjkuyLlFsWUqcyDVqbrJoFVIkQBDsBQQIFmDQp9wf\nyIwAEgBRBiQgvZ88fByB4IsBOJz3O99fowRBEEAgEAgEAoGQBHqpD4BAIBAIBEJhQ8QCgUAgEAiE\nlBCxQCAQCAQCISVELBAIBAKBQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISVELBAIBAKB\nQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISVELBAIBAKBQEgJEQsEAoFAIBBSQsQCgUAg\nEAiElBCxQCAQCAQCISVELBAIBAKBQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISVELBAI\nBAKBQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISVELBAIBAKBQEgJEQsEAoFAIBBSQsQC\ngUAgEAiElBCxQCAQCAQCISVELBAIBAKBQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISVE\nLBAIBAKBQEgJEQsEAoFAIBBSQsQCgUAgEAiElBCxQCAQCAQCISXKpT4AAuFKRxAEcBwHlmWhUCig\nUChAURQoilrqQyMQCIS0IGKBQMgTsSIhEokgHA6DpmlJKCiVSigUCtA0Lf2XCAgCgVCIUIIgCEt9\nEATClYQgCOB5HizLgud5AJD+TVEUBEGI+xIFgigaxC+apqUvAoFAWEqIWCAQZELc/FmWBcdxEARB\ncgtYlgXLsgk3/rniQXxMdCCCwSAMBgPUarUkKEgYg0AgLCYkDEEgyADP8/B4PGBZFnq9fp4jkGpj\nT7Txx4qGs2fPYsOGDTAYDNJzY8MYsS4EERAEAiEfELFAIOSA6CSwLIuRkREEAgGsX78+501b/HlR\nGCgUCqhUqjgHIhwOx/0MCWMQCIR8QcQCgZAFscmLPM+DoihpU04kFGJDDJkSu14yF0L8EhMpY587\nV0CQMAaBQMgUIhYIhAxIJhLEzTdfKUCp1k0VxhATKyORSNxzSRiDQCBkAhELBEIaJKpwmLu55kss\nZLOBiz+jUCjiHs80jCG6EAQC4eqGiAUCIQWJREIyCz+fYkGudUkYg0AgZAMRCwRCEkSRMLcMMhk0\nTUuCQm7yWeGcTRgjWTIlERAEwpUJEQsEwhwSiYR0KgqKwVnI5DWB5GEMnufBcVzc93ieB8/zMJlM\n0mdGwhgEwpUBEQsEwp+Jbag0N3kxHZJt6jMzM7BarfD7/TCZTDAajTCZTDCZTNBoNEW1maYKY7jd\nbgwMDGDTpk1xzyVhDAKh+CFigXDVk6rCIRPmigW/34+enh64XC40NjaioaEBfr8fXq8XLpcLfr8f\nCoUiTkAYjUapqVOydQuN2NAERVFSPwiAhDEIhCsFIhYIVy2inR6JRKTNLZcNS9zUw+Ew7HY7hoeH\nsXz5cuzZswcqlQrhcBgVFRXS8zmOg8/ng9frBcMwGBkZAcMwAACDwSC5D6K9X0xkE8YQBYRSqSRh\nDAKhwCBigXDVkUmFQ6brBgIBnDx5EmVlZdixYwdMJhMAJNzsFQoFzGYzzGZz3Bp+vx8Mw8Dr9cLp\ndCIUCuHixYvQ6/XzXAi1Wp3TMS82pBqDQChOiFggXFWIImF0dBQOhwMbN26URSSMjY3BYrGA4zhs\n3rwZ5eXl856XzutQFAWDwQCDwYDq6moAwPvvv4+mpiaoVCp4vV54PB6Mjo4iGAxCo9HEiQeTyQSt\nVltUG2m61RixLbBJGINAWFyIWCBcFcytcBDvYnPdXCYnJ2G1WhGJRFBXVwe3251QKOQCTdNQqVSo\nqKiIC2NEIhEwDCO5EJOTk/D5fFAoFPMExNw8iEInURgjdrhWbBhDnNBJwhgEQv4gYoFwRZOswiHX\nngherxdWqxUzMzNobW1FY2MjpqamMDk5KePRXyZRgqNKpUJZWRnKysqkx3iej8uDGBsbA8Mw4Hke\nRqMxTkQYjUYolfJeAvK5Mcc6C7FkEsYQXQgSxiAQMoOIBcIVyUIVDjRNZ1VhEAwGYbPZMD4+jsbG\nRlx33XVS3kAhtHumaVpKjBQRcylEATE5OYmBgQGEw2Ho9fo4EWEyma64PIhkYQxxPkasE0EEBIGQ\nGCIWCFcU6VY4UBSVkbPAsiz6+vowODiIyspK7N69G3q9ft6ahdiUiaIo6PV66PV6KQ8CAEKhkBTC\nYBgG4+PjCAQCUKvV8xIpdTrdghtpIZV3psqDEM+RkydPYvPmzdDpdNI5QsIYBEJiiFggXBFkWuGQ\nrrPA8zyGh4fR29sLo9GIG264ASUlJQmfu1RTJ7NFo9FAo9HE5ViwLBsnIAYGBuDz+UDT9Lw8CIPB\nUJR5ELHnhEqlglKpJGEMAmEBiFggFD2ZznAAFnYWBEHAxMQEenp6QFEU1q9fj8rKypTrFkIYIleU\nSiVKS0tRWloqPSbmQYgiwuFwwGazged5GAwGSTywLFtQ7kIqRHEQO7I70fdThTHEEd+iC0HCGIQr\nGSIWCEVLtjMcgNTOwvT0tNSeedWqVairq1vS2RDA0lr8sXkQtbW10vEEg0F4vV54vV5MTU1hdnYW\nLMvivffem+dCqNXqgtpIxc8z2TGlE8YIBoPS90gYg3ClQ8QCoegQ7/ZYlgWApHeHqUhUDeHz+dDT\n04PJyUm0tLSgubk5o2qBQs1ZyAcURUGn00Gn06GqqgoA4HQ60d/fj5UrV8a5EH6/HyqVat5cjHTy\nIBbjfWT63LllnQtVY4jigYQxCMUMEQuEoiFRhUO2F93YDTgcDqO3txcjIyNSe2atVpvxmtlWWCxE\nMW0qCoUC5eXl8/IgxHJOr9eLoaEh+Hw+UBQ1r5zTYDDMaxGdDxZyFjIhna6UYshDfD4JYxCKDSIW\nCAVPrEiQY4aD+PMcx8Fut6Ovrw/Lli3Dzp07YTQas14z0wqLTCg0ZyETlEolSkpK4hJDeZ6Xhmox\nDAOHwwGGYcBxXMK21iqVStZjklMsJEKOMEasC0EgLDVELBAKFvGi6nK5oFarJdtajvbMTqcTPM9j\nYmICmzZtkqXr4tUUhkhEJscoVlfEijMxD0IMYczMzGB4eBihUAharXaegMhlvHe+xUIiSBiDUMwQ\nsUAoOGIvnDzPo7e3FzU1NWhoaMh5bZfLhZ6eHmlk8o4dO2S76F4J1RBLSWweRGVlpfR4OByOa2vt\ndDrh8/mgUqkStrVO5/NaCrGQjHTCGADQ2dmJ1tZWqXU3CWMQFhMiFggFRbIKh1ztfY/HA6vVCo/H\ngxUrVqC6uhonT56U6aijXKnVEEuNWq3GsmXLsGzZMukxjuPiBETseO9E/SASjcoGCkMsJCKRgJiZ\nmZEeF8NyIiSMQcg3RCwQCgLxDkocDhSbvJiLWAgEArDZbHA4HGhsbMT1118PlUolWb08z8uWUCde\nlGOT2eRcl3AZhUKRMA8itq210+mE3W4Hy7Jx/SDykQOxGAiCEOckxD4+N4wR29Y82XROcl4RMoGI\nBcKSkk6FQzZiIRKJSO2Zq6ur57Vnjt3Y5SKfYuFqdhbShaZpaby3iCAICIVCkoCYmZnByMiIlFzY\n0dERJyIKdby3KAYS9ftIJ4wRW5FBqjEI2UDEAmFJWGjQUyyZiAWe5zE0NAS73Q6TyYRt27YlbM8s\nXnTlrF7IhwARKQaxILdIkgOKoqDVaqHVauPyIDweD9rb21FaWgqGYeByueD3+wt2vHdsC/N0WKga\nI1kYI1ZAkDAGIRYiFgiLSqIZDgtdkNIRC7HtmWmaXrA9s/i4nGJB3FDk3tjJxVp+xI2wsbFReozj\nuLjx3qOjo1IehMFgmFeNsRj9IERi/1ayZW6fB5FYFyIcDseJvmRhjKUWT4TFh4gFwqKQ6aCnWBYS\nC9PT07BYLAgGg1i5cmVa7ZnzGYZIdqxiOCGbzb8YnIViItHnqVAoYDabYTab457n9/ulRMrJyUn0\n9/cjEolI473ntrXOB3KIhWRkE8aYKyDEttZE2F65ELFAyDvZDHqKRWygNBeGYWCz2bJqzyxe2BYz\nDJGtUCA5C/KT7u+CoigpD0Ic7y3egYsdKT0eD0ZHRxEMBqHRaOYJCDnyIBa7emOhMIY4XEuEhDGu\nfIhYIOSNXAY9xULTdNyFKRQKwW63Y2RkBHV1ddi7dy80Gk1W6+YrwVFOyMVWfnLJr6AoShrvXVFR\nIT3OsqwUwhBdCHG899wQRqbjvWOTf5eKTMMYLMtidnYWNTU1JIxxBUDEAkF2xDsPjuMWTF5MBzEM\nwXEcBgYG0NfXh/Ly8pzbM8vRvyGWqz3BsZjIRzKmUqlEWVkZysrKpMfE8d6iiBgbGwPDMOB5ft5c\nDKPRmNQZ43m+YDfYZC6EOJitvLychDGuAIhYIMhGJhUOmUBRFBiGwcmTJ6HVarF58+a4Bj25rJsP\nF6AY1iQsjmMTO95bRBAEBAIByYFwu90YGBhAOByGTqdL2Na6kMVCIsRzVizRBJKHMWLLpcUwxtye\nEISlh4gFQs6IyYsOhwMURaGsrEyWP3JBEDA5OYnBwUFEIhGsX78eNTU1sl085HYWgOQbey7HXCwX\ny0AkgJnIzFIfRlqIYnYpoCgKer0eer1eGu8NRMNrooBgGAbj4+MIBAJQq9XQarXgeR5OpxNGo7Eg\nxnsvBMdxcRUjpBqjuCFigZA1cyscJiYmpBHFuTI7Owur1Qqv1yvZmLW1tTIc9WUW2wXI5eJeDM5C\nu6sdXZNd2BHZAb1Kv/APLCGF2BNCzIOYO95b7Ebp8/kwODgIhmGkQVxz21oX0iaabndUUo1RHBCx\nQMiKRMmLSqUyYdVCJsS2Z25qasLGjRvhdDoxOjoq05FfJh/OgtxJk0BxOAsuvwvWGSscQQesU1Zs\nrN641Ie0IMXwuSqVSpSWloLjOExNTWHr1q1SHoToQjgcDthsNvA8P6+ttclkSrtCSG44jstavGRS\njUHCGIsDEQuEjEhV4TC3aiETYtsz19TUYM+ePdDpdNK6cm/qQP6chcUKbRQSna5OMBEGJpUJ5xzn\nsGbZmoJ2FwrRWUhFbM5CbB6E6LaJ473FEMbU1BSGhoYSjvcW+0Hk+/3LOXcFSD+MEQsJY8gHEQuE\ntIitcIi1A+fOcMjUWYhtz2w2m7F9+/a4pjjiuvkQC4uZs5ArhSwWXH4XOl2dqNBWgOM5jDPjBe8u\nFJtYSDYXQoSiLo/3js2DEPtBiC7ExMQE/H4/VCrVvETKdMd7p0suzkImpBPGEEUECWNkDxELhJSk\nM+hJRKFQpL35CoIAh8OBnp4eKBQKXHfddaioqMh5NkQmLGbOAsuymJiYgE6ny7hVcKFfwDpdnfCE\nPKjR1GA6MA2zxlzw7kKxiYVsEzLVajXKy8vj8iDE8d6iiBgaGoLP55MaUC003jtd5iY4Liapwhii\nO0rCGJlBxAIhIbEiQfwjk2OGAwBMTU3BarUiGAxi1apVqKurk2XdTFkMZ0EQBIyOjqKnpwdKpRLh\ncBgcx0kXZfFrIQFRqM6C6Cro1Xp4A174WB9KVaUY8Y4UtLtQjGJBrrv0ZOO9Y9taT0xMwG63g+M4\n6PX6eS5EOiO+C63cU/x9z/07mxvGEIfQVVVVzQtj+Hy+oh1xnitELBDiyGWGg0KhSBmGYBgGPT09\ncLvdWLFiBZqbm9O68yhWseB2u2GxWMCyLNatWyc164ktkYutsRcvyrFfSqWyoHMWXH4X1Ao1aJ6G\nh/MgxIcQYkMo15VjnBknYkEm8r3xitUVRqMRNTU1AC7nQYjn6szMDIaHh6U8iLmJlBqNJu4zXUpn\nIRPmXt/8fj+MRqOUrBwbxti/fz/uu+8+fP7zn1+qw10yiFggALisrnOd4ZBo8w2FQujt7cXo6Cjq\n6+szbs9cbGGIQCCAc+fOYXJyEq2trWhqagJN09IFR4wtiyOT584aiL0oi/X0SqUSU1NTMJlMBXVX\ns658HZpKmgAA4+PjcDqd2HDdBgCARpF5C+7FotjEwkI5C/kgNg8idrx3JBKJy4MQyzpVKlWcgBBF\nRbHBcRyUSuW8z1sQBDAME9dg62qCiAWCbDMc5joLLMtiYGAA/f39qKiowK5du2AwGDJeN59iQc51\nxTuQS5cuzZtZkUqUJJs1IAqIwcFBBINBabKmmN0e+5WvaYcLQVEUDKro71Sv1EOn0En/LmR4ni+a\nBlLA0jaRmotKpcKyZcviuqjOHe89MjICj8cDiqIwMzMzr611ITsOolhIhNfrjQvfXE0QsXAVIzoJ\nLMsCiE/0yQZxU+d5HqOjo+jt7YVWq8WWLVvi+uVnSj7KEQH5eiLwPI+RkRGp1n3NmjVobm6e97xM\nnQwxOW12dhahUAjr1q2Lu6vzeDxwOBzw+/1Qq9XzBMRcW5hwmZf7XsYPz/8Qf1j5B6wtX7vUh7Mg\nhRb/n0ui8d4dHR1SQq/YWKqvr08a7x2br1NIjhnLsknFjNfrJc4C4eohkwqHTKAoCuFwGO+++y54\nnse6detQXV2d87qiCJHbOpZDhLhcLlitVvA8j/Xr18Nms8luvca+50R3dWKXPzGM4XK5JFt4roCQ\nY1xysRNiQ3jS8iRmIjN4vP1x/PjWHy/1IS1IoYuFRPA8D61WK+VAANFrT2zOzuzsLEZGRqTx3nMT\nKZfifE3mLAiCQJwFwtWBKBIcDgfMZjNUKpVspUFie+ZIJIKVK1eioaFBtoubuI7cYiEXZ4FhGFit\nVkxPT2PlypVobGwETdOw2+2L3mdB7PJXWloqPRZbHuf1ejEwMACfzweFQjFPQBTDnAE5OWQ5hHH/\nODS0Br+z/Q4HNh8oeHdhKXIWciVRgiNFUdBqtdBqtXEht0gkElfO6XK54Pf7oVAo5iVS6vX6vH0W\notuayFnw+/3gOI6IBcKVy9wKhwsXLuCGG27IKMkwGX6/HzabDU6nE7W1tfD5fGhqapLhqC8jXhjk\nvrvKJhciHA6jt7cXIyMjqK+vx/r16+PyBQql3XOy8rhYARFbXz/XEs72glxoomPCN4Hf9/4e91x3\nDyiKQogN4bGzjwECoFPoEOADi+YuhNgQZkIzqDZUZ/yzhZSzkC6ZdHBUqVTzxnuLeRDiOSuO9xYE\nIa6ttdgPQo621uL1INFaXq8XAEgYgnDlkawMMpPmScmIRCKw2+0YGhpCTU0Ndu/eLfUUkJtYsSAn\nmYQhxE6Tvb29KCsrw86dO2E0GhOuWagdHGmanhdXFuvrRQExNjYmXRRjL8aFOKgoHb5z8js4ZD2E\n5abl2L9iPw5ZDmHEOwK9Ug8KFNSUetHchb8/9vc4NXIK5+85D6N6/rmTCp7nCyamny65dnBMlAch\nCEJcP4jJyUn09/dLeRBzXYhME3/F/K1EIodhGCkR+WqEiIUrlEQVDrFNSbId+MTzPAYHB9HX1zev\nPXMwGJRKMOUOF4ivLSfpuACCIMDlcsFisYCmaVx//fVx9ulcFnuSZa7E1tfHzhmIFRCxg4oSCQjx\nwlpovSBsUzb81vpbcDyHB999EB9r+BgeO/sYBETPT4EXoFFp4A178+4udE124YjtCHjw+OXFX+Jr\nW76W0c8XY85CPvosiF0mDQYDqqujDk1s6bGY+Ds2NiaN956bB5Eq7MZx3LzZEyIejwcmk6noHB65\nIGLhCiOdMshsZjgIgoDx8XHYbDYolUps2LBh3qYpvk6q0qNsWCpnwev1wmKxwOPxYNWqVaivr1/w\ngp0vsbCYxF6QYxv0BAIBSUA4nU6pw5/YjZLjOEQiEVk2iUAkAK0yt+S2hz94GACgpJXocHXg4IcH\nMeGfgAABTISBAAG0EP19Hus7hpngDEq1pamWzJrvv//96HsRgIMfHsQ9192TkbtQjDkLiyVwkpUe\nsywb1w/C7XbD5/NJg7hiBYTomi1UNnm1hiAAIhauGGIbKonxzWTJi5mGIdxuN6xWK8LhMFatWoXl\ny5cnXReQf1MH8jdOOpFoCoVCsNlsGBsbQ2NjI66//vq0LeBCDkPkAkVR0Ov10Ov1cXd0Yoc/j8eD\nyclJ+P1+nDhxYl5MOZNRyZ6QB19/8+u4teVW/Le2/5bV8YqugiAIUNAKCBDwh74/4Ce3/gScwGHC\nOYFwOIyG+gYAQKm2FCWa/CSudU124ajtKAQIUFAKzARnMnYXijFnYak7OCqVynl5EOJ4b1FEiHkQ\n4nhvtVoNQRCk3hCx56woFort9yAXRCwUOYnKIBeqcEg3DBGb8d/S0rJge+Z8OQDi2vme48BxHAYH\nB2G327NuIrWQWMgmRJPPixMTZmBQGbJ6jbkd/rRaLZxOJ9ra2pJ2o5xbiTFXhF1wXsBfH/nr6KYa\nmsEnVnwiq7t90VWgqeg5SYFCp6sTFE3hthW3oU/RF+1dsWpdxmtniugqUKCk8yNTd6EYwxCFeMyx\n471FRNdM7AUhCAI6OzsRDoeh0+kwMDCAS5cuSSG7qxUiFooUMXkxEomkPehJZCGxEAwG0dvbi7Gx\nsYQZ/8kQhUq2+RCpyJezIPZvmJiYgNVqhUqlwqZNm+Km9GVCMeUszIZmcfNzN+Pz134eX9/6dVnW\npGk6ZTdKr9cLj8eD0dHRhN0oH3rvIYwxYzCpTRj1juJV+6u465q7MjqG2FwFmqKBP390giDgwXcf\nxCdaPrFo7Z5jXQWaiubI0BSdsbtQiBtvKsS/q0Lu1CgS65rxPI9QKITNmzdL5+zIyAguXbqErq4u\njI+Po6amBhs3bsTGjRuxZ88e7N+/P6PXe/DBB/Hyyy/DYrFAp9Nh586deOihh7BmzZqkP/P000/j\ni1/84rzHA4HAorXUJmKhyMhl0JNIMrHAsiz6+/sxMDCQ9Z11MQ19omkaoVAIZ86cgd/vT2sC5kIU\nUxjimY5nMOwZxhPnnsDnr/08ynXZCaR0SDQqWexGKX6dsJ3Am/1vQhAE+MI+hEIhPN/xPD5e/3FU\nmavS/r30zfRFRcKcvVVBKTDGjCHEhQAsTi7IC10vQPjz/zgh/m/u152/vqLFApC4qqCQic1ZEM/Z\n22+/HbfffjsefvhhdHR04J/+6Z9w/vx5nDt3Dm+//XbGYuHEiRO49957sXXrVrAsi/vvvx+33nor\nurq6Ul5vzWYzrFZr3GOLOXuDiIUiIlWFQybMFQtiu+Le3l7o9fqc2jPnUmmRCrnFQjAYxPj4ODwe\nD1asWIHNmzfLkpSZr3CJ3MyGZvHTcz8FTdGYDc3ilxd/if+17X/J/jqpmNuN8v/2/19EhAgUVDTH\nwBvxYnBmEI+/8Tj2Ve9LuxvlvhX7MPG1iYSvGRsKWAyx8LUtX8PW2q0Jv7eidEXa6xRbgqN4DSim\nYwYWbvVcXl6OXbt2YdeuXVm/xrFjx+L+/dRTT6Gqqgrt7e3Yu3dv0p+jKCquG+ZiQ8RCESDXoCcR\ncUMXywKtVisEQcA111yDqqr07+ASUejOAsdx6O/vR39/PwwGA5YtW4bVq1fLcIRRisVZeKbjGbiD\nbhjVRvgjfjx54Uncc909ObkLuRxju6Mdbw28BSBavcAL0UFPFYYKXKIu4ctrvgw6TKfsRmkwGjDN\nTaPB3CDlKqQ61sUQCxX6Cty26rac1ym2BMdUJYiFTKqkTI/Hk5fujbOzswAQ18I9EQzDoKmpCRzH\n4frrr8cDDzyAjRsXbwQ8EQsFTCYVDplA0zT8fj/OnDkDn8+H1tZW2dozF2rOglj62dPTA41Ggy1b\ntoBhGExMJL4DzZZiyFmQXAXQoCkaepV+ydwFkcfPPo4QGw0PsAIr9esYZUahV+nR4enAJ1d+Unp+\nom6UhwcP43fO3+F7G76HtVVrU3ajFAQBr429hieGnsAP/uIHi/pes6EYwxDFdLwiC5VONjQ0yPp6\ngiDgG9/4Bnbv3o1rr7026fPWrl2Lp59+GuvXr4fH48Gjjz6KXbt24cKFC1i1apWsx5QMIhYKkGwq\nHNLF7/djcnISPp8PLS0tstnvInJ0h0xELmJhenoaFosFoVAIq1evRm1tLSiKgt/vz3uFhVxryonk\nKqiimd00RYMCJYu7kA22aRtODp+EUqGMcwQ4Pio679lwD3bU7Yj7GZqmodFrcHToKPY07EHdijq8\n2/MupvlpvD37NtZUrknZjdIb9uKhjofgjXjx2dWfxe6G3Yv3hrOg2DbfpS6bzBaWZZN2aPT5fLJX\nQ9x33324ePEiTp8+nfJ527dvx/bt26V/79q1C5s2bcLjjz+Oxx57TNZjSgYRCwVEbIXD2bNn0dra\nirKyMlk2i3A4DLvdjuHhYRiNRlRUVMhqv4sUkrMQCARgtVrhcrnQ0tKClpaWuAvYYroAuU71lPM4\nn+14NnpnHmYurw8B3rAXR21H8cXr5mdd55PlxuX45+3/LCUexqJVavFf1/xXGFTzE79+0/0bHPzw\nIJgwA41Sg1HvKEo0JXh7/G0c2HkAm1dvTtqN8tDYIXgjXlCg8N3j38XhzxyO60ZZaBRbzkKxiRuR\nVM6Cx+OJaz2dKwcOHMDRo0dx8uRJ1NfXZ/SzNE1j69atsNlssh3PQhCxUAAkqnAIh8NgWTZnocBx\nHIaGhmC321FaWoodO3ZgamoKbrdbjkOfRyE4CyzLoq+vDwMDA6itrcWePXsSZg0vRu+GQlzz+zd/\nH6Pe+TM8KFC4peWWhD/z5sCb6Jvuw5c3fjnl2pmer6PeUVyavIQvbfgSlHT6l6NgJIiDHx7EkGcI\nh3sOYzY8CyWtRLmuHGPMGJ66+BQe2PtAwm6UnpAHf/PTv4Hw55rKs66z+NWpX6FN3yZ1o4wdrFUI\nAqIYcxYK4XPLlIUSHOXIWRAEAQcOHMDhw4dx/PhxtLS0ZLXG+fPnsX79+pyPJ12IWFhiklU4KBQK\naahJNsTG6FUqVdxMg9nZ2bzc/QNLm+AoDrLq6emBwWDAtm3bUv5xF8PGLq4pJzc33ZzR85kwgyc+\negLuoBu7bjZ7AAAgAElEQVR7G/emHLjkZ/1prxuIBPCvJ/4VlfpKNJmbsKY8eZ35XJ7ueBrD3mEI\nEHDBeQEKWoEmc1NUHKgMkkOSqOLgyQtPwsf6QOHPs1IoBV71vYp7br5HciDcbrc0oCiXbpRyUWx3\n6plMnCwkkokcQRDAMIws7Z7vvfdePPfcczhy5AhMJhMcDgcAoKSkBDqdDgBw9913o66uDg8++CAA\n4N///d+xfft2rFq1Ch6PB4899hjOnz+PH/84/9NSRYhYWCIWqnBQKpVZb+gLtWfOV3kjkN8wRKpN\n2O12w2KxgGVZtLW1obq6esFNNl/OQj7E0lK2e/6D/Q8Y9AyCF3i82P0ivrv7uwmfd2r8FO4/cz8O\n1x/GhqoNC677XNdzODl8Ek3mJmyq3oTWsta03IVgJIifnf8ZBEGAXqmHJ+yBilYhzIWjYkFtgINx\nSO5CLN6wFwc/PAgePGjQAAVwAod3Rt5B+2Q7djfsRlVVFYD4AUXZdKOUk2ILQ+Q6cXKpYFk2ZYKj\nHM7CT37yEwDATTfdFPf4U089hS984QsAgKGhobjPb2ZmBl/+8pfhcDhQUlKCjRs34uTJk7jhhhty\nPp50IWJhkRErHETXQIxlz93YsnEWvF4vrFYrZmZmsGLFCjQ1NSVUyfkUC4sdhvD5fLBarZiamkJr\nayuamprSvkjlY2OnaRqRSCTp62XDUtrPTJjBb7p/A41CA6PaiOODx3HnujvnuQu8wOOJricwE57B\nIx8+gqc++VTKdQORAJ7tfBYRLoIJ3wQ+GP8Am2s2p+UuiK6CVqkFj+jvL8JH4PA5oFfpAUTLL1/v\nfx3377wfWuXlENTTF5/GTGgmeszgpe6OAPDQBw/FJTomG1CUbjfKbEYkJ0IMU5IwRP5JddxyDZJK\nR/gfP3487t8HDx7EwYMHc37tXCBiYZFIVOGQKuktkw09tj1zQ0MDrrvuupQXqWJ1FmI39kgkArvd\njqGhIdTV1WHPnj0Zz5lPZ0R1pqQKQ2T7WvkcUb0QoqvQZG6Cklai19+b0F14rf81WGetUFEqvNH/\nBs5PnMf11ddL3xcEAZ6wRxrW9FzXcxiaHUK1vhqesAedzk60O9rj3IVDlkN4re81/Hz/z6W/E5Zj\n8bPzPwMv8FK1hFqhBsuzaC5pxj9s/QcsNy4HAJRqSuOEAgA0lTRhV90uMD4GSqUy7pzZXL05rc8k\nnW6UDocDfr8fGo0mbsKh2WyGWq3OaOOPbeeeKW8NvIUjtiP4wc0/gEqRP+djLsUWNhFJluDIcRz8\nfr+sCY7FBhELeSZWJGQywyGdMERse+bKykrs3r0ber1+wWPK14YO5N9ZELtN2mw2mM1m7NixI2u1\nnw9noViaMqVDrKsgbjQV+op57gIv8Dh45iAEQYBOoUNQCOLRs4/GuQsPvPsAft35a3z4hQ+hptV4\ntvNZgAK0Ki0ECBhjxuLcBX/Ej2+d/Bbcfjf+au1fYf+KaEvd98feh9PvhIJS4M8pB1BS0SZOftaP\nmxpvQqW+Mul7+vSqT+PTqz6NixcvoqysTLa6+bndKIH5I5JdLhd8Ph9UKlXa3SiBy62TM918w1wY\nj559FL3TvXit/zV8auWnsn+DGVKszkKyBEePxwMAeWnKVCwQsZAncp3hkOruP7Y9s8FgwNatW1Fa\nmv5kPqVSmZcNHcivs+Dz+fDuu++C53msX78elZWVOXebvBoTHNPl9f7X0TfbBwhA73QveIEHTdFg\nwgxesryE+3fdDyDqKnS4OqBWqEGBmucuOH1OPPHRE/Czfvz8/M9RoavA0OwQ9Co9fGEfBAjwhDw4\nO34W55afw+plq/F0x9OY9E9CgIDvv/99fKLlE6AoCiXaEtzacuu8OQsAUKWvStkj4o3+NyBAwK0t\nty5KB8dEI5I5jotrJpWsG6XJZIJOp4s7nzIVC3+w/wG9072I8BE8eeFJ7GvZt2juQjEmOIrDrxI5\nC16vFxRFkamTBPkQO8+JyYtAdjX2SqUSoVB83bkgCHA6nejp6QGArNsz0zSdU6XFQmuHw2FZ1xQ7\nLYpNlRobG2XrNkmcheTUmepw+5rbAQAfjH8Ad8CNfS37oKAUWL0s2qMj1lVQKVQQeAEahQZMhJHc\nhR+1/wghLgQKFB5vfxwbqi8nP0b4CCJ8BCEuhAnfBDQKDQJsAI+efRQAoKJUuOi8iGP9x7B/xX6s\nr1yPX37ylxm/l9nQLP777/87AKDr77oWrd3zXBQKBUpKSuLuUBN1o/T5fKAoShINQLShmtFoTOu4\nw1wYv7z4S1CgUGuohXXKuqjuQjEmOIrXxEQix+v1wmg0Ft17khMiFmRErkFPwHxnYWZmBlarFT6f\nDytXrkR9fX3WJ65CoZCcD7lPfjnDEOFwGL29vRgZGYHJZEJpaSmam5tlWRu4eksn02Xb8m3Ytnwb\nhjxDeGf0HdAUjZ31O+NKL9sd7bC4LeDBg4kwgABQPAUBAt4efBsXJi7gFxd+Eb1jo5Twhr2gBTpu\n7PT7Y+8jwAZgUpuwomyF5CqoaBVoKupUxboL2fDEuSfgj/gBKvr/92n3FUzCIE3TMJvNcfFwnufh\n9/vh8XgwMxNNyGxvbwcwvxulwWCY93csugoV+gpoFNG8jMV0FziOyziHaKlJNc/C4/HAZDIVzDmz\nFBCxIAOCICASicxzEnI5scRqCL/fj56eHrhcLjQ3N8vSnllUzvkQC3KEIXiex9DQEHp7e1FWVoad\nO3fC6XRKrXvlYrGdhVyqIZaydPKVnlcwHZyGklLiUPch7KnfI20411Rcg4dvfhhhLoypqSn4/X6p\nG51RbcRvun+DEBeCglJE3wcv4JzzHJ657RmUaEpgdVvRPtGOjdUbMR2cxgXnBclVEFs/KyhFnLuQ\nKbOhWTz64aNS9cNjZx/Djht2oIZaugl+C0HTNIxGI4xGI0pKSuB0OnHjjTcm7EbJ83ycgNDoNXjy\nwpOgQElCoUJXsajuQjEmOIr5Con+TsUeC0QsELIi0wqHTNf2er04ffo0li9fnrQLYTaIYiFVa9Nc\n1s52AxbDLFarFTRNxzWSmpyczNvGLqclfSWFIQBgyDOE1/tfxzLtMhjVRnS5u3Bq5JTkLuhVetyx\n7o7oc4eGMDs7i/XXRrvKOX1OfPW1r0Y/Xzr6+Yruws/P/xz/c9v/xEvWl+ANe7G6bDVCbAhPfPQE\nXD4XBAgIskHpODiBww/P/DArsSC5Cn/Gz/rx0shL+Nemf836c1lMxI03UTdKQRAQCAQkAeF0OvHW\n8FvodnQjggj6I/3R2R80hSAbxDMdzyyKWCjGBMfFKJssZohYyAKxWUswGIRKpZJ10BPHcRgcHITd\nbgeAnLL9kyEea74SEbNZ1+PxwGKxgGGYhGGWfLgA4vpyioVUxzk1NYVQKISSkhJoNJq0X3MpnQXR\nVVhdtlo63rnuQjJ+Y/kNglwQAgRE+IjUMZEHjycvPInbVt6GUyOnUKWP5t3UGGvgdDqxoXoDGs2N\n89ZbV74u4+OPcxX+DC/weHHkRRyIHEANCtddEEnVkImiKOj1euj1elRXVwMADI0GhJaFEA6FEQqF\nEA5H/8txHGoVtbh06VLeu1EWY4JjqoZMYhjiaoaIhQyIrXCYmJiAzWbDrl27ZHMSxsbGYLPZoFar\nsXLlSgwNDeXtBM1Xr4VMnYVQKASbzYaxsTE0NTVh48aNCTvh5StkAMh7155oY/f5fLBYLJienoZG\no4Hf74dSqYTZbJYu2GazOWmMd6msT9FVKNOUQUDUgak11M5zF5LxV2v+CnqVHpdcl/AH+x/wsaaP\nYUvtFgBAo7kRL1lfwlRgCk0lTfCGoyEmk9qEGkMNHrvlMaknQy48ce6JaC7FHAJcAM9Yn8F/NPxH\nzq+RbzJtyLS6fDXu331/3GOL3Y2yGBMcF3IWruYeCwARC2mRqAxSpVLJMugJiFrsVqsVkUhEGqHs\n8XjQ39+f89rJyJdYSHdT5zgOAwMD6OvrQ0VFxYI9IvLpLMh5FxQrFliWhd1ux+DgIOrr69HW1iZ9\nn2EYeDyeuPp7tVotCQcp/vxnAbEUzsKp4VMIskGEuBBmw7OX3yMo/GnwT0nFQoSLQKVQodZYi7uv\nvRt3/+5u+Fk/hj3DePjmh6FX6RFkg3j64tNYplsmCQUAMKgNCHEhWN1W3LA891a2Y96xaE+GOQiC\ngAn/RM7rLwZyxP8XuxtlMToLqcKyJAxBxMKCJKtwyGV2g0hse+bW1lY0NjZKf2D57LII5K8fwkLH\nLQgCHA6HNOBq8+bNcY1sklFMzgLP89JAK71eL4WSOI5DOBxOWD7HsqxUPufxeDAxMSF1ANRqtWBZ\nFm63W7YWwunwiRWfQEtJ4ol4y03LEz7eNduFk+dP4ksbvgStUos3B95E12QXms3NGJgdwO96f4c7\n190JrVKLx299HOPMOLonu7G9bru0hoJSoNpQLct7+OTKT+K2Vbfh480fj3v8zJkzWLFi/pCpQiSf\nyYL56kZZjM5CqomTcg2RKmaIWEhCOoOexK6MmboLwWAQNpsN4+PjSdszi5tuvurBl2KU9OzsLLq7\nuxEIBLBq1SrU1dWl/d7y7SzIRSAQAMMwsNlsWLt2LWpqatISJUqlEqWlpRiJjKC5uhlGtVHqAOhy\nueD1emGz2aSL9twQRj6GGJXryrGzfmfazw9xIXzo/hCMhsEF5wVsrtmMZzueBQ8eJo0JnrAHv+78\nNW5beRv0Kj3KtGV47OxjeKXnFfzqtl/h2sprZT3+qcAUvnPqO1BQCmyp2YJS7eXGZUvVZyEbFnuI\nlBzdKK/EBMfa2tpFPqLCgoiFOcQ2VBJjhYmSF5VKpRSeSPePgmVZ9PX1YXBwcMH2zKIdlo+KBSC/\nYYi56waDQfT09GBiYgLNzc1oaWnJ+D3l01mQY91QKISenh6MjY1BpVJhz549GV8shzxD+M6p72Bf\nyz58ZeNXpA6ACoUCExMT2L59u3TRFkMY4+PjCAQCkm0cKyLyOQUxEV3TXRgNjKJSX4mTQycx4ZtA\n12SX1H65Ul8Z5y4Mzg7iZevLcAfc+Pn5n+PRWx6V9Xie734ek/5JAMCL3S/iKxu/In2vmMRCIQyR\nyrQbJcdxGBsbQygUiutGWcgsNHFyzZr0R6hfiRCxMAexZ8JCFQ7iSZXKuhLheR7Dw8Ow2+0wGAy4\n4YYbFuwxns/yRnH9fCc4xs6uqKqqwu7du6VudJmSD7EgrptLGCK2J0R5eTna2towNDSU1V3VkZ4j\nGJgdwKv2V/HpVZ9GrTF6JxN7Dia6aMfaxrFxZzFxLVZA5ONcAoAgG8QHzg+goTVoLmlGz1QP3hp8\nCwE2gAgXQYSLTuKM8BHJXXi642kwYQblunK8OfgmOl2dsrkLU4Ep/KrzV1DTaggQ8Gzns7hz3Z2S\nu1BsYqEQLf1U3Sjb29ulv43YbpRiGMNsNkOv1xfU74DjuKQCmyQ4ErEwDzHcsNBJLD6HZdmkWeyC\nIGBiYgI9PT2gKArXXntt2vMM0lk/F/LtLIgxe61Wm/HsimTr5kMs5DJManJyEt3d3aAoSuoJ4XK5\nshIfQ54hHOs/hlpDLSb9kzhqOzrvTjgZc21jlmfBs7wkIGZnZ6XMd71eP882lkNAXHBewIhvBDXa\nGqgVauk9lWpLEeEvj+wu05bBF/HhzNgZvGx9GXqVHia1CePMuKzugugqVOurIUDAhG8izl1YyiZX\nmVKoYiERNE3DZDJBEASsXLkSWq0WPM/D5/NJYYyxsTFYrVYA6XWjXCxYlk16M0PEAhELCUn3blPM\nW0jE9PQ0rFYr/H6/FJ/P9I9AjiTKZORLLIhdFm02G9asWYPa2lpZ7h4KyVnw+/2wWCyYmprCypUr\n42ZVZCs+jvQcwXRgGquXrYYAIc5dyOTz65/px6nhU/jM6s/MS1wTM9/FFsJzBUSsA5GJMxJkgzgx\ndCI6nZKO3pmtXrYaHM/hjnV3YHNN/OhnlUKFH575IZgwgxpjDXjwMKqNsrkLsa6Cgo6+DxWtinMX\nFjsPIBeK6ViB+VMyRQERmyAoCEJa3ShFAbEY+Q+kKVNqiFjIgURiwefzoaenB5OTk2hubsaWLVuy\nvnPLZ0WE3GvHtqUGos2k5HRE8ikW0l1XzDkZGBjA8uXLsXfv3nmJqdm0exZdhWW6ZaAoCpX6Stim\nbJK7kG5TJl7gcWb8DDpdnWgtbcWuhl1x30+U+R4KhaQL9tTUFAYHBxEOh2EwGOIEhNFoTHohtU5Z\n4fK7EOSC6Av2YXpyGkD0s7W4Lbil5Za454u5ChRFwR/2YzY8CyWtRJANyuIuvND9AhyMA1qFFpOB\nSemzGfOOSe5CsYUhiuVYgctiIdUGn243SrvdDo7jpPMxNpQht4BIFvIVS52v5vHUABELORF75x87\n9Eiu9sypnItckUssxPYSqK2txa5du3Dy5EkZjjCefIYhFtqIBUHA+Pg4rFYrdDodtm3blvTCkY1T\ncaTnCCZ8E2g0N8IT8gCI3n2L7oKZMqe1Zv9MP6xuK4xqIz50fIhrq66d19ho7iY5t/ZebN4jJlC6\n3W709/eDZdm4C7bZbJbu+FaUrsBd19yFsfExeBkvVq9aLa1vUs+/G+ua7IKCUkCn1MHP+RHiQojw\nEZjVZnS4OnLeyHmBl6ZixkKBAi/w0vssFtIJQ/yq81eYCc7gwJYDi3RUyRGvK5m6IYm6UQqCgGAw\nKAkI8XyMRCIwGAzzXIhcQmqp8s+Is0DEQkLSvZNTKpUIh8Ow2+3o7++Xhh7JNfM8n85Crn0WBEHA\nyMgIbDYbDAaDtIGKn1s+yhzlnuMgrpvqWD0eD7q7u+H3+9MKq2TTmvm88zwqdBXwhX1w+p0wqAww\nqo2gQKHb3Y3tldsXXIMXeJx1nAUrsFhVtgoWtwWdzs44d+GdkXfwv//0v/G9G7+HGxtvTHr8Go0G\nlZWVqKyMVjEIgiA5EB6PB5OTk/MERKW5El3+LvzU9lP8+rpfo8HckPRY97fux7bl2xDhIni++3mM\nMWMIRoK4sfFG7G/dn/Pv977N9+G+zfelfE6xOQupNt4J3wSeOPcEwlwY+1v3Y2XZykU8uvmIPRbk\n+HwpioJOp4NOp0NVVRWA/HWjTOUseL1e4iws9QEUK2KJpcVigV6vx8aNG+PsXTkQJ0/mg1zWdrvd\nsFgsYFkWbW1tqK6uli4MYhWJ3CInH90WgeSbezgchs1mw+joKJqamtKe9plNGOLRjz8KX8QHi9uC\nB997EM0lzfj2rm9DRatQqa9EMBhcUICIrkK9sR40RWOZdlmcu8DyLB7+4GFY3Bb82+l/w1t//ZY0\n1TGd96TVaqHVauMEROwdn2PCgae7n4adseOh1x/CfdfeJ4UwEiWtLdMtQ4erAxO+CawsWwlPyAPb\ntA03Rm6EXpW8k6dcFJNYWChn4blLz2EqMBWt+uh4Fv9n7/9ZxKObT767N+arG2UyZyEQCIBlWSIW\nlvoAihGXy4Wenh74/X5UVlZiw4YNeWuclM+chUzFgs/ng9VqxdTUFFpbW9HU1JTwIpaPhk/5Egtz\nnQWxzNVms6GsrAy7du2CwWBIe72FnIVE54lRbYRBZcDPzv8MATaA/tl+9M30YU/DHuk5qdaMdRUM\n6uixVhmqcGroFP7f+/8P/3Hjf+DU8CmcHT8LQRDQPdmNP/b9EZ9s/WTa7yvR+4i943t78G0Mhgeh\nVWrx7sy7uDN8J/wOP2w2GwRBiLOLzWYzVBoV3h99H2qFGhqFBhW6CnRNduEjx0e4dcWtWR9XuhST\nWEiVszDhm8Bvrb+FXqWHglbgj31/xN3r715Sd2Gpujdm241SFLXJxIKYtE3CEIR5JPvD9Hg8sFqt\n8Hg8WLFiBRiGyWh6YKbkuxoi3Q09Eomgt7cXw8PDqKurw549e1ImLxZLt0UgfnN3u93o7u4Gz/PY\nsGGDdBed7XqZcGnyEj4c/xCN5ka4/C4cth7GjrodUNLKBc+vcWYcI54RcDyHbnc3AEDgBbw99DaY\nMINPrvwkHjv7GAJsAEa1Eb6IDw9/8DD2r9iftruQCl7g8YsLvwAncKjQVGCKm8Jp32l8c8c3paQ1\nMQdCzHrv8/XhXc+7aCxthIt3QaPVoFRbio8mPsKmmk2o0Fcs/MI5UkxiIZlAFl2FenM9KFAY9gwv\nubtQSHMhMulGCQBWqxUlJSWSI6bT6cAwDNRqdc45aMVO8dTjLCGBQAAXL17E+++/D5PJhL1796Kl\npUUaJpUv8h2GWEiI8DyPwcFBnDx5EgzDYMeOHbjmmmsWrHLIhyMiZ7fFWGiaRjAYxLlz5/DRRx+h\nrq4Ou3fvzkooANmJBUEQcLjnMHwRH8xqM+qMdbjkvoT3Rt+T1hSfl4hKfSU+tfJT+Ntr/hZ3td2F\nu9ruQrWxGkyYQZgP47snv4uz42ehoBVQ0kqoFCrJXZCD40PHcWHiAko1paApGgaVAS9bX8awZ1hK\nWqupqcGqVauwadMm7N27F5o6DcpLyjEVmkKPswftve3osHegb6QPJzpOwOFwwOfz5S0RsdichUR3\n6rGuAk1FcwRMGhP+2PdH9E73LsGRRin0uRBiY7OGhga0tbVh27Zt2Lkz2ta8oqIC4XAYAwMD+M//\n/E/U19fji1/8IsrLy/Hiiy9K5Z3Z8OCDD2Lr1q0wmUyoqqrCX/7lX0r9JlLx0ksvoa2tDRqNBm1t\nbTh8+HBWr58rxFlIQSQSkdozV1dXz2vPrFQqEQgE8vb6S1k66XK5YLFYAADr169Pu5kUkL/WzLk0\nUEoEx3EIBoOwWCxSKWSu5Z7ZHKPoKiw3Lo/a+yodKFCSuyCSbINTK9RYU365FS3P87j39XvBCzx0\nSh3OOs4CAEyaqI2qU+jgCXtkcRdiXQWtQgue41GqLcWodxS/vvRrfHPHN+f9DEVR+NTaT+HGFZeT\nLCNcBHcdvQuGsAFrzGswMjIChmHiOv+JIYxcWwfnI1E2nyTLWfit5beY8E1AQSngj/ijz4WAEBfC\nC10v4Fu7vrXYhwogdb+CQkUUpQ0NDdJ5sX79eqxbtw6vvvoqDh06hIMHD+LixYtQq9W48cYb8bvf\n/S6j1zhx4gTuvfdebN26FSzL4v7778ett96Krq6upKHO9957D3feeSceeOABfPazn8Xhw4dxxx13\n4PTp09i2bVtubzpDiFhIgCAIGBgYgN1uh8lkSloql8/SRnH9UCiUl7WTiQVxEubs7CxWrlyJhoaG\njO8S8jXRUi4RInbWFJM0W1pasHr1/FK7bMjGWThqOwqHz4EgG4TT5wQQHcp0afISPhj7AFurtma0\n3iu2V2BxW6J3nKDhFaIxVybMSM/hBR4WtwWnhk8lrYxIh3ZHOyxuCziBwzAzDBo0NJwGvMDj972/\nx1c3fXVe+SYAmDVmmDWXO+Id6TmCQc8gaIrGtHEae9btAc/z8Pv9UghjroCIbSIVKyBmgjNQ0koY\n1amrkopFLCTLWWiraMPd6+9O+DMbqzfm+7CSUkwdJ0VEgRP7Oet0OuzZswczMzM4deoUzpw5g0gk\ngq6uLoyMjGT8GseOHYv791NPPYWqqiq0t7dj7969CX/mkUcewS233IJvfjMqur/5zW/ixIkTeOSR\nR/D8889nfAy5QMRCAoaHhzEyMrLgHXW+xcJihiFi+0Qkm4SZydpL3UApGV6vF93d3WAYBqtXr8b4\n+LissUjxIpnJnWtrWSvuWHvHvMcpUCjVxE9KXAie5/Gj9h+B5VmYNdH+DCpaBQECbqi9ASXayxu3\nVqHFcmPiUdPpsnrZavzz9n+Gg3HghY4XUKmpxB0b7ohu6GoTDKqFk0NZnsVj7Y9BgABO4PDIh49g\nd/1u0DQNo9EYV4oc2zrY4/FgaGgIDMNAoVDAZDJBb9TjZ/afodxYjn/Z+S8JNy3xcywmsZDofXys\n6WP4WNPHluCIUlOMzkKqGTyxPRZUKhU2bNiADRs25Pyas7OzABCXTzGX9957D//4j/8Y99i+ffvw\nyCOP5PTaDocD4+PjUKlUMJvNMJvNMBqNKSu+iFhIQGNjI2praxdUx4vhLOS7z4KYl2C322XrE1EI\n3RbnEiuGGhsbsXHjRqhUKjidTlnj4rH5BeluRneuuzPl9yORSMrvxyK6CjRFwx+OWtM6pQ4BNoBl\numX4z0//Z9prpUOJpgR3rrsTT5x7AipaBY7nsLlmM9ZVrEt7jVd7X402k1IZwQkcPhz/EKdHTsdV\ng4jEtg5evjwqdMThRV6vF+8MvYNzY+dA8RTqffW4rvq6OBdCo9FcMWKhUCmkBMd0SdWQiWEY2Ssh\nBEHAN77xDezevRvXXpu8vbnD4ZAaVIlUV1fD4XBk/drnz5/Ht7/9bbS3t2N6elpyr8X97Ny5cwnF\nEBELCRCHSS1EPu/8xfXzXTp5+vRp0DQtDUKSa+1CCUMIgiCVQpaUlMwTQ3LnQSyUjJjvNbvcXVAr\n1FKnQgCgEU06HJwdlO2YYhmYHcDp4dOo1dfC5XfhVfurWFu+VjpuX8SH44PH8fHmj0OjjM8JiXUV\nVAoVlIISATYguQvpDl0zm80wGA3osHfAXGIGy7MY0g7hY+UfA8Mw6O/vh8/ng1KplH7/brcbZWVl\nUKvVBS0cim02RKEnOCZiobkQcg+Ruu+++3Dx4kWcPn16wefOPTezzbcRRefXv/51CIKAH//4x2hp\naUEoFEIgEEAwGITb7UZra2vCnydiIQeKNQzh8Xhw6dIlcByHlpYW1NfXL2pXxMVad3p6Gl1dXeA4\nLmlIKdcR1XPJh1gQSWfNb+38Fr626WsJv6dV5qf061jfMcyEZlCvrgfFU/hw/ENY3BbJXTjWdwzP\ndjwLJa3EvhX74n5WdBV0Sh04PiowNQpNSnchGWcdZ9Hp6kS9qR4RPoKO6Q54dV60NbQBiG4IDMNg\nZmYG09PTGBwcRFdXF9RqdVwCpehAFArFNhuiGMMQLMumDEPIKRYOHDiAo0eP4uTJk6ivr0/53Jqa\nmvEnI2oAACAASURBVHkugtPpnOc2LATHceA4Dmq1GufOncPx48excWNmeS1ELCQg3T/MfIYJ8rF+\nKBSCzWbD2NgY6urqMDs7i7q6OtkvREud4BgMBmG1WuF0OtHa2orm5uakdzrF5CwshNPnBBNhsKJ0\nhWyvvRCiq1CtrwbFUjAqjXBGnJK74A17cdR2FGPMGA73HMZNjTfFuQuv2F4BEE2+5HgOKoUq2gWU\nonHEdiRtscDxHH7f+3sIEKQOkGPeMbxqfxXryteBoigoFAqUlJRAp9PBbrdj69atUivf2OFFfr8f\narVaEg7if7PN4ckVEobIP6kEjsfjkUUsCIKAAwcO4PDhwzh+/DhaWloW/JkdO3bgjTfeiMtbeP31\n16VSz3RRKBTS+7vnnnvQ1dVFxMJiIjoL+SrDksvO5zgOAwMD6OvrQ0VFBXbv3g2VSoXh4eG8XIiW\nKsEx9n1WVVWlNczrSnEWBEHA9z/4Phw+B376iZ+mlVgoB8f6jmHcN44aQw2m/FPgOA6U9rK70OXu\nwpBnCOvK18E2bcPxoeNx7sIDex/A37T9DR5vfxwOxoG/u/7vpO6D11Rck/ZxiK5Cha4CATZazrxM\ntwxnx8+i292Ntoo26bmxOQs0TaO0tBSlpZcTSVmWBcMwUhXGxMSE1PUvtgJjsQREsYkF8Q62mEjl\nLDAMI+XH5MK9996L5557DkeOHIHJZJIcA1HAAsDdd9+Nuro6PPjggwCAf/iHf8DevXvx0EMP4TOf\n+QyOHDmCN998M63wRSz3338/SktLUVZWhtraWtx///2gaRobN25ESUkJjEZjwrbssRCxkAPiyZUq\nkzbX9XMJQwiCAIfDAavVCrVajc2bN0uZt+Kmm49jX2xnQRAEOJ1OWCwWqFQqbNmyBWVlZTmtmS35\naB6VjgA56ziLdkc7gmwQb/S/gb9c/ZeyvX4qIlwE6yvXAwC8nBccy6G0tBQKSgFXwIWjtqPQq/Qw\nqA1Q0ap57kK9qR4WtwXTwWlQFIX+mX78jw3/I2Px/ZHjI6hoFWZDs5gNzUqP0xSN887zCcVCMpRK\nZUIBETt3YHx8HIFAIG7ugCgk0h1clC7FlrNwpTkLck2c/MlPfgIAuOmmm+Ief+qpp/CFL3wBADA0\nNBT3u965cydeeOEFfOtb38K3v/1ttLa24sUXX8yox0I4HMaxY8ekUuRQKASVSoUvfelL8/LzTCYT\nRkdHE65DxEIC0r1QiSdXKlWaC6KzkI1zMTMzA4vFgkAggNWrV2P58uVxa4hT4fKxqSsUiowy+NMl\n0cbOMAy6u7vh8XiwevXqjPMvsm3PnGo9YHHDEIIg4MXuFxHiQtAoNDhkOYRbWm6Jcxe6J7vRUtoi\ne97CgS0H4A648e7Iu2ij2xAOh7FuXTRX4SXrSxjyDElOwXLj8nnuQoSL4DfdvwEFCnWmOpwZP4Pz\nzvMZ9wm465q7cEvLLQm/V2usjfu3+PeUyXkidv2LFaFz5w6MjY1Jg4vmhjByuT4UY85CMYkbYOHS\nSbnCEAtx/PjxeY997nOfw+c+97msX1elUuGVV14By7LgOA46nQ7j4+NgWRbBYFBKbmQYJuVNDhEL\nSUhnE6FpOu+9EDLtNhcIBNDT0wOn04nm5ma0tLQk/SPIZ9VCvp2F2HkVDQ0NuP7667O6o5P7WMVN\naDHDEKKrUGOogUahQf9Mf5y7YJ+247437sPta2/H32/8e9mP6xcXfoHf9/4e31j7Daw1rAUAeEIe\nHLUdRYSLwOV3Sc8NRAJx7sKJ4RPodndjuWk5dEodnD4nDnUfwvVV12e0Qc5t8pQKucKGieYORCIR\nKXzh8XgwMjIijU6eG8JIV0AUYxii2JwFlmWTJrUyDFPUEycpikJDQ3RkfDAYxKlTp3DLLYmFdSqI\nWMiRfIsFIHoiLxQDZFkW/f39GBgYQFVVFXbv3i3FwVKtny9nIV85CxzHYWRkBD09PTAajdixY0dO\nFmE+NvbFdCtiXQWTOvo5qBXqOHfh+a7nMeQZwiHLIXxuzedkGdLECzxoisbg7CBe7X0VDp8DhwcO\n41/a/gVANGHRqDKitSy+DKtEUwI1rYYv4gNN0ZKroFNGz9UqQ1XW7kK65LPVs0qlmjf5UByd7PF4\nMDMzg+HhYYRCIej1+jj3IVlTnGITC8V2vEByZ0FMgC32iZPi7+TcuXPYt29fwuvzsWPH8JWvfAWD\ng4lLrIlYyJF8T4YEkHJ9QRAwNjaGnp4e6HQ6bN26NS7WmoqlrlrIFJZlMTg4CIqi0NbWhurq6pwv\n+vmaY5EPAZII0VUwqAzwhDwAoiOv7dN2vNH/BtZXrsexvmOo0lfB5Xfht9bf5uwu/NbyW7zS8wp+\n8V9+gRe6X8BMaAaNpkZ0THegc6YTbWjDctNy/Hjfj1Ou86fBP6Hb3Y0QF4obfOQJefCS9aW8ioXF\nJNHo5FAoJIUvxDLOcDgMg8EQlwNhNBqLLmehWJ2FfFdDLCWBQAA+nw/9/f1oampCIBBAOByGSqWC\nUqmEWq3G5ORk3OyjuRCxkIR0L/j57LUglnslW396ehrd3d0Ih8NYu3YtampqMto88+0AyEUwGERP\nTw+mpqZQWlqKLVu2yHYxKgZnQSTRmpdcl6BRRGcx+CI+6XGzxowLzgvodHXCE/KgubQZnMDl5C68\nM/IO3ht9D6/1v4ZhzzCe6XgGr/a+ihJNCUwaExweB46OHMXtwu1pnYcV+grc1HiT5CrE0mhuzPj4\n0qUQhkhpNBpoNJq4RmihUEgKYUxNTWFgYECqturr60NZWZnkQBTyZlyszkKqDo7FKhbEc/3cuXP4\n2te+Boqi4Ha78dWvfhUqlQp6vR5msxnBYBBvvfUWdu/enXQtIhZyZClaPvv9flitVkxOTmLFihVo\nbm7O6uJR6GEIsRV1b28vKisrUVNTA61WK+uFcjGcBUEQ0D3ZjdXLsh9WlWxz+9tr/xb7W/cn/J47\n4MaX//hllGhLQFEUKnQVGPIMZeUuhLkwvvfu99A12QWaoqGklfhR+48ACmgpidaLl2nL0DnTiTPj\nZ7BteXrZ2mqFGnddcxeaSpoyOp5cKASxkAiNRoPKykppPLogCAgGg3jvvfeg0WgwOTmJ/v5+sCwr\nORCxIYxC2aCL0VlIFobgOA4+n69oxYJ4nptMJtx00024cOEC9Ho9PB4PpqenpeRGiqLwF3/xF/Pm\nUMRCxEKOLEYXR3FDZ1kWdrsdg4ODqK2tTauPQLpry4kczoLL5UJ3dzdomsamTZtQXl6O7u7uogkZ\nxK55avgUfvD+D/D1rV/HjtodKX4y/TVFlLQS1YbE3dx+fv7nmApOoc5UJ40wVtCKrNyFV+2vome6\nB7OhWWiUGjSVNME2ZUOpphRTwSkAUUHhjXjxbOezuKH2hpQbMsdzOD54HB3ODpwcOonPr/982seS\nK4UqFuZCUZSUq9Tc3Ay1Wi0JiNgmUna7HRzHwWg0xoUwFqqbzxfFWDqZLAzh9UYnthZzgiMAbNiw\nAT/84Q8xMDCAzs5OfOpTn8p4DSIWkpBJF8fFaPkszjcwGo3Yvn27LEq3EJ0Fn88Hi8WCmZkZrFq1\nCvX19dIFLx85Fuk6C56QBycGT2BXwy4s0yWfEgfEb+wcz+HFrhdhmbTgJ+/8BMHKIMxGszTpzWw2\nQ6/Xp3W+ZSJqeIHHe2PvwaQ2SbkMAKCm1WB5Fh9NfIRbW25Na60wF8Yvzv8CwUgQAgREuAiCkSBo\nikaQC0JFq0CDhkALqNZVYyY4A07goKQSXF7CYVCDg+jmx3Bp8hLqzfVon2jH3sa9i+ouFINYAC7/\nzsW/AYqioNPpoNPpUFVVJT0nGAxKIYy5AiK2CmMxBMSVVDopioViTnDs7+9Hb28v9Ho9qqqqsGnT\nJgwMDECr1UKtVkOj0UCtVi9YTUbEQo7ke5iUIAjSHfY111yDqqoq2S50hTTwKdY1qaurw549e+ZV\ngNA0LXv/hnTbPXc6o/a6QWXAzS03L7imeJF/Z/gdnBk6g1KhFD2eHoTWh9BQ1iDV5Vut1ug45z/f\nDYoXdq1WG/d7zuh3LghQXurCr3z/BYzHBf7/s3fm0XFUd77/VFVvau2WLNmyLcm2LOF9wbvNYggh\nhCQDOJCFSXhkCIl5yYNhAoE3gcw7Yd6EADkkgYFMwoQJDmQjgAMZEgeI4wCx8YKNpda+y9rX3re6\n749StbulVqslddvqPH3P4XAstW7frr5177d+y/e7ZAnq+vWIUY0ASZJCqYN4oEcVfKpPIwUI7K4B\nVgfz6PHbudO7iev+7p9pGBzE7/dz0UUXRR3H8NxzmB99FLW3m3c2B5HXzmfxnpuo9LWd1+hCKukW\n6Gsz1nzDCYTuGSCEwO12h7owurq6qKurQwgRikDoa81qtSbscFdVFSFESkUWhBATpk7sdvusSvFM\nBwcOHODJJ5+kqKgIg8GAoij4fL5QO6/BYCAzMxOv18vnP//5caJROubIwgS40P4Q+hO20+mkoKCA\n9evXJ0WW+UKnIcK7OaxWa8yoSTLqC+KRex7xjnCi6wSKpHC65zTrCtfFDOHr8+zr7+P7b30ft9fN\nqsJVtDnbeK31Na4qv4oFCxYA2ubqdDpDm3pzc3PIHTFc2EfX24gHysGDGH7zG4q8XoTJhHSsEfVk\nC/4vfQmxaFH8F4dzUQVvwKsZPUmgqkEGAkPI3hEEEs+f+RmfPtiJ8StfwT8vetTF8OtfY7n3XggG\n+WCRgdP5PoqrOzF2Ps/Cz33yvEYXUiUNAefIwlTvfUmSsFqtWK3WCALhcrkiRKQcDgdCiAj9h6lE\nuxI13wsJfa+KFlkYGRkhMzMzZdZLNOzatQtFUZBlmY6ODl588UU8Hg+rVmkiapWVlTQ2NpKdnR1T\n/GmOLMwQBoMh5AeeCISLDS1atIj8/HxycnKScvNd6DTE8PAwNpsNj8cTVzdHsooRJxvzTM8Z+tx9\nVORVUNNXw+nu05NGF5qamvhz659p9jZTvqAcs8lMkVTEmd4zvNPxDpcVXwZon0nfpHX9ed0dcWRk\nhJGREXp6eggGg5w6dYrs7OyICMTYDU7q7cXw3/8NFgvqMs1QSqgqclUVyh//SOCWW6Z0fV5vfJ3a\nwVpkSda6FoQKHhdeRWKZP5Ob+xax2GVAsdmY95vf4LrttvGDCIHpe9+DYJBAZjpvlLrxG2VMkoR/\noJfM5g7aC6TzGl1Ilc1fj4IkYr6SJJGenk56enqIrOoEQk9hhEe7xqYw4iEQ+n6SSpGFWHN2OBwp\nnYIA2Lx5M5s3bwbg5z//OWfPnuX++++nvPxcwfUjjzxCZWUlq1dP7McyRxZmiETVLKiqSltbG/X1\n9WRlZYXEhj744IOk6TgkU2ch1rjh7pdLly6NqTI5dtzzHVnQowq5llxkSaYgvWDC6IIQgra2Nlwu\nF4pRocZUg6po83X6tLZGb9DLr6p/xa7FuzDIEytrZmdnRxRVHT58mNLSUgKBQIQyoN76pP+XXV+P\nNDCAuuqcFwKyjJg/H+XMGQIeD0yhKDbTlMnORTvPmS+1tyN32iA9nTWuTL7UrSnDiaweMo8eRR7V\nuI+Az4fc1IQwGmnPUOlMF5iCEk05IAVA7a0nbeE66ofqGfIMkWOJTydkukilyEKyNRbCCcTChZos\ntu4hEK5C6XA4Qumy8BRGWlpaxLXUyU0qRRYCgUBI/n4sdEGmVFkv0SCEwOv1YrFYePjhh/niF79I\neXk5qqqiqioGg4F/+qd/YsuWLVRWVlJSEj26N0cWJsD5KnAUQtDb20tNTQ0A69atIz8/P/T+yXr6\n18dORr2FHlkYuymrqkprayv19fXk5eWxe/fumCIgY5EsshBrzPCoAmhOhtV91eOiC0NDQ1RVVREI\nBEhLS8NaaKW7vZtsczaDnsHQ67LMWXQ5umi3t1OaXRr3PPWNOpxA6MI+IyMj9PX10djYSFZlJeWD\ngwR6ezGlpWExmzGaTEiqCooCU9zE95TsYU/JntC/Db/8Jaa/PIxYuhTC7xFJAlWFaMTLZELk5CD1\n9lJsN/E/P7AQkAFVRfJ48O3+O/zb9mI2mJNOFCC1yMKF0CyQZZmMjAwyMjIiCISeLrPb7bS2tuJw\nOFAUJYJAKIqSMtdWRyxfiL8FQSZJkkLFiwsWLODw4cPs3buXwsLC0NpqaGjg7NmzpKdP7FY7RxZm\niJmQBbvdTnV1NSMjI5SVlbFkyZJxG0Oy5aSTMbb+GcI35b6+Pmw2GwAbNmyIEKOZyrgJIwuqCk1N\nWN5/n9y2NqT8fERZmXagjsLld3Gq5xT+oJ+6gbrQzwNqgMq+SjYt3IRVtlJbW0tnZ2coSnLkyBGK\n0ot46pqn8AYiU1Q+nw+TYqIoM8zy1utFam8HIbSagigy3dE24LHCPkIIvGVlGM+cQenuxp6XR39f\nH/VKP5m9vRTv+DuCg4NkZWWNK6CMF8FNmyAjA6m/H6F/h8EgDA3hvOQSRDRZcknCf8stmB59FMnr\no1SYNKLg9iKyc3DecBvkxu4wSSRSjSzMhrmGp8t06ARCT2G0tLSEaiBOnjwZkcKY7no7H4il3qgX\nOKY69M9311138ZWvfIW77rqLG264gYKCArq6unjooYe46KKLqKiomHCMObIwQ0znwPX5fNTV1dHR\n0TGpCVKiayLCkSwFx3CZao/HQ3V1NQMDA5SVlVFcXDztJ6WEkQVVRfr975EPHyZtcJB5/f3InZ2I\nHTtQP/YxGH3KMMpGdizaQaBo/PcrI9PX1UdLQws5OTns2rUrFCXRuyGiuR36fL7IcWw2lN//Hrmr\nC4RALSggeNVVqOvWRbwuHj0ISZKwFBWh3Hwz1l/8guz+fuxGwaGMLrwrczFsWY939IlQr4AOr3+Y\nyEgn4jOUleG/8UaM+/cjNTaC0QheL6K0lP7rr5/w73x33onU1ITx5ZfB4dBSIwUFeH74Q5igKDJZ\nSDWyMFtD+tEIRH9/Pzabjfnz52O32yMKdsemMMxm86z4Hs6H4+SFRPgauvrqq3nsscf4t3/7N26/\n/fbQ2bJ3716+853vhGpZomGOLEyAZHRD6IqEDQ0NzJs3j127dsUM+0Dy0xDJqlkAQoWaRUVFXHLJ\nJXEdRpONmwiyINXXIx86hMjLI7BgAY6ODkRhIdLbbyMtX45YuxYAo2Jkw4IN4/5+eHiYqqoqOnwd\nrF27NtTvHho/TqEnqasLw0svgcOBWlICkoTU0YHhlVfw5+YiRp3idMTbDRHcuRO1qAjlgw+wDVTS\nZ8lDLCoiuDyHLQs2hgoo9RRGT08PLpcLs9kc0YGht1WNhf9//k/UVatQDh5EHhgguH49gU98Aq/H\no0UZosFkwvvv/47/zjuRjx1D5OYS3LMnahQl2ZgjC8mDEAKj0cjixYtDPxu73hobG3E6nZhMpqgE\n4nxjsshCqpOFsevnE5/4BJ/4xCdC9U/z4iTrc2RhhognDSGEoKenh5qaGhRFYePGjRGmMrGQ7DRE\nosmCEILu7m5A867Ytm1bwtTPEhZZaGwEnw9yc5HdboSqQlYWdHYi1dSEyMJYhEeEli5dyrJly6Ju\nMvGSBdlmQ+rrQw2rQBalpUhVVciVlQTDyMJUDzdRWspQUT6naofIEVrK40zfGcrmlZFpygwVULYM\nt5BbnMt8y/zQZj4yMkJHR8c4Z0Td2EhRFIJXXEHwijEdIfX1UWYSCbWiAjVGqPN8IJXIQqqZSEVT\nb4xWsBve8WO32+nt7Q0RiPD0RVZW1qSOuzNFrMiCw+EItZ6mKvbv388nP/lJLBYL77zzDiaTKVQY\nbbFYGBkZIS0tbU6UKdnQIwsTPQGMjIxgs9lwOp0hRcKpbFTJdrVM5Nj6Z3W5XEiSxNq1axPadpSw\nyMJEn1mSIAoxE0LQ0dFBTU0N2dnZk0aE4p2nNDyshfHHwmxGGhyMfO00ZKnrBuoYcA9QllsGQP1g\nPXUDdWxasAkAT8DDD0/+kHRTOvdtv4/c3FxyR4WbQCNHOnkINzZKT08fp0CZSgfa+XadnAlmS81C\nvIhXvTEagQgEAhEEoru7OxTxCo8+ZGZmJpRATBZZWLFiRcLe63wjGAzy5JNP8vGPfxyj0cidd96J\nwWBAlmVkWcZgMGA0GjEajVgsFl588cUJx5ojCxNgKmkIGH+TeDwe6urq6OzspKSkhIsvvjiu9sCx\nSIXIQvgTt/5ZDx06dN47F+KFKC5GkmVwuZAUBVUI8HohENCKHMOgpxy8Xi9r1qyJS0Ez3oNdFBSA\n36+F7vXNSlWR3G5ElNzhVA45h8/Bmb4zoZZP0IyeKvsqWTFvBZmmTI6cPUL9UD0G2cDJ7pNsXrg5\nYgyTyUR+fn5EAWW4rHC4KmBmZiaqqmI0GnG5XONa6mYTUimykGppiJn4QhgMBnJycsjJOdcREwgE\nQh0YIyMjdHZ24na7sVgs41IYkz0ZT4TJahZS3RfioYceIjs7G1VV+cIXvkAgEAgZSOn/dzqdk66z\nObIQA/Fs+vqNEQgEMBqNBINBmpubaWxsZP78+VNuD4w2/mzVWQjXhtCL/PQn7mQUTyaMLFx0EWLz\nZqT33kMRAmtnJ5KqItavR6xZA2jiWHV1dbS3t1NaWsry5cvj3gTjJQvBVauQlyxBrq1FXbAAJAm5\nsxN10SLUMamQqR5uDYMN9Dh7MMgGhrxD2ucWAr/qp3GwkYq8Cl5vfB2zYiagBvh90+/ZWLgRRZ74\nM04kK6y31LW1tWG32zly5AiKooyrf7gQ+ehoSKXQfqqRhUT7QhgMhnERL7/fHyIQupCUx+PBYrFE\nRB/iJRCxXDJTvRtCURSuvPJKTp48ycaNG9m3b9+0x5ojCzOEJEkoioLf72dwcJDa2lpMJhObN2+O\nWODTRbLTENM9fPWqZ1VVWbduXchWV8eF0ESIG0Yj6g03IJWVoZ46xYjBgLp3L2LdOoTZTEd7O7W1\ntWRmZsZVhDoWcacMcnIIfOpTKG+9hdzYCEIQXLeO4OWXn2tLDMNUIgv51nyuKI2uMplvzefI2SM0\nDjWyLGdZqBU0WnRhMuhKfxkZGTidTlRVpaysLEKBUi9oCw8nz/RpcCZIpTREKhEbOD/21EajkXnz\n5kUU5ukEIrzmxuPxkJaWNi6FMTaKoGujRMPfQoFjfX09e/fu5fLLL2fXrl2sX7+epUuXxl03p2OO\nLCQAsixz+vRp/H4/5eXlFBUVzXqzJ33sqaY43G43NTU19Pb2UlZWRklJSdTNLBnzjtf0KS6YTIjN\nmwmsXk3boUOs2roVu91O1alTId30wsLCaX2PU6kvEEVFBG6+GQYHNUGj3NxIsaOwMaeCRZmLWJQZ\n3QfCE/DwxPEnMCkmzIoZs2JGCBFXdCHmZwlzSNQJgY6x4WT9aVA3sxlbQJlMpFoaIlXmChfOnjoa\ngfD5fKE1NzQ0RFtbW0TRrk4iAoFA1DSEEAKHw5HyaYjc3Fyuv/56Tpw4wcGDB8nPz2f16tVceeWV\nXHbZZRQUFJCenj7pOpsjCzEw2abvdrupra3F7/eHvoDp1CXEgh5ZSMYGpygKQoi4Qp3BYJCmpiaa\nmpooLCzkkksuwRJDNjiZkYVEXgv9c9tstlDKYdmyZTP6HmOtmwl/N0kUakoFjsEg0tAQwmqN2pp4\n5OwR6gfrybXk0ufuAyDNkMaZ3jPTii7Eg2jhZH0zj1ZAGR6BSLStcqqRhVSLLMyW+ZpMJvLy8iKe\noPWiXZ1AtLa2RvwsvAPDYrGEfpbKyMvL47HHHkMIwbvvvssbb7zBm2++yT333IPBYGDbtm3s2bOH\na6+9NmYx5xxZmAYCgQBNTU00NzdTWFhIZmYmhYWFCScKEClwlOjx9bFjbUh6K2R1dTVms5ktW7ZE\nFCBNhGT4TkRThpwJhBB0dXUBWovUzp07E5KfnE7nQjyYdEwhUN54A8Ovf43c0YFISyP44Q/j/8xn\nICyV0j7STn6aluYIqtp3ZFbMmA1mOuwdSSEL0TB2M9c17PVQcnd3N/X19SFb5fAIxEwKKOfIQvKg\nqmrSWx1ngrFFuwBHjx5l3rx5yLLMwMAADQ0N3HTTTSxcuJC0tDReffVVVFVl/fr1E6YrYuHPf/4z\njzzyCMePH6ezs5OXXnqJ6667bsLX/+lPf2LPnj3jfm6z2Sa0f48F3bFWlmV27tzJzp07eeCBB6ir\nq+PVV1/lwIED3H333Rw+fHiuG2K6GLuh6C10dXV1pKWlhQ7O9957L6kdC8CEobJEjD0REbHb7dhs\nNhwOB+Xl5SxatCjuTTZZBY6QmA3UbrdTVVWFy+UCNAnqRG1yySAL8Vx35Y03MD36KPh8iHnzkJxO\njD/9KVJnJ75vfCOU3vjM6s9wfUV0tUWLIX6TqQnn2tSEcvo0yDLBLVuidnaMm/tf/4ph/36sTU1k\nV1Tg//znUTdtGueK2N7ejt1uD3kSjFWgjOc6pRJZSKW5wuyKLMQLVVXJzc0NkVZVVfnrX//K4cOH\n+Zd/+Rf+8pe/8NRTTzE4OMiaNWt44403ppTvdzqdrF+/nltvvZW9e/fG/Xc1NTURqbyxdWHxQpIk\ngsEgH3zwAe3t7fT399PV1UVbWxuVlZXU1NSwYMECdu/eHXOcObIQJwYGBqiursbn842zU06U82Q0\n6P2wyVJa1BdSOMI7AYqLi9m4ceOUC9GSGVmYCQkJBALU1dXR1tZGSUkJGzdu5M0330zo4Z7Q2oqw\nMWPOMRjE8Otfa0Rh+XIARG4uYmgI5Z13kGtqUEefSmQkrMbpd+hMCCGY/6tfYXnrLSS7XfvRvHn4\nb7+dQAwpaMPPf475vvuQfD6QJJSTJzG88gqeJ58k+JGPRHVFnEgRcGwHRrR1m0oHcCpGFlLJnhrG\nPyzJssyyZctIT0/nq1/9Kr/97W+xWq20trZy4sSJuBUPdVxzzTVcc801U55XQUFBXFHciaCv84MH\nD/LjH/+YvLw8hoeHaWpqIjc3l4svvpivfe1r7NixI65i/DmyMAlcLhc1NTX09fWxbNkySktLDaE/\nQgAAIABJREFUoyqUJYss6OOfD2EmIQTto50AWVlZMwrLJzuyMFUIIejs7KSmpob09PTQZ9MP4EST\nhfOehhgaQj57FjF2I8vORuruRjp+HOOhQygHDyINDqKuX4//s59F3Zy4lEPm0aPkvfIK5OSglpWB\nEEidnRiffBK1vDxCqTIEhwPzt76F5PcjsrO16IcQSMPDmL/5TVwf+lDIq0NHeAHlokVaEWe4oI/e\njz+2Gl4nEnNkIXlIxcjCRB0cDocjJFYkSRIlJSUT2jcnAxs3bgwVW3/jG9+ImpqIBX2dHz16lF/9\n6lcsX76cL37xizz88MMRctzxYo4sxEB7ezsffPABRUVFXHrppRP2iSczsgDnR5hpcHAQm82G3+9n\n7dq1zJ8/f0YbarIKHGHqZEFPpzidznFRIUmSEh4JkGX5/Kch0tMRViuS3Y4If0ro6UHq6sLy7W8j\nDQwg0tMR+fkob76JcuIEnocfRt227dzrVRX59GnN1Gr9+kktraW2Now//znKO++wpLYW2etFLFum\nHfqShCgqQq6rQzl0KCpZUI4e1Yox09PPdYFIEsJqRT57FvnMGdQN4/05xiKaoI/f7w+Rh/BiNl2x\nrqOjIykFlImEECKlntTPR+tkIiGEmFDBcWRkhMzMzPO+NhYuXMh//Md/cPHFF+P1ennuuee48sor\n+dOf/sSll14a9zj6vD/96U8zb9483nnnHQ4ePMjx48dZsWIFl156KWvXriU3NzdmsbqOObIQA7m5\nuWzfvn3SPluDwTDOTTCRSKbWgiRJ1NbWMjw8PGHkZDpIpklVvAd7IBCgvr6e1tZWiouL2bRpU9Ta\njESThQsSWbBYCF51Fcb/+i/E0BBkZ0N/P8qpU5qk9MgIwmIBvx/J6UQtLUVuacH47LN4t24FScLw\n4ouYH3gAqadHe7+CArzf/CaBT30q+udsa8Oybx9yczPCYsEwMIDs8yEqKzVRKVkOkQZpZCT6vGOR\nICFQjh5Frq8nuHUrorg4ziulwWg0Ri2grKurw+1209PTQ0NDA6qqhgoo9SiE1WqdFdGHVIsspFoa\nQr/vJ6rZuhCdEBUVFRFW0Tt27KCtrY1HH310SmRBx/Lly9m3bx/79u2jpqaGw4cPc/DgQV544QVy\nc3PZunUre/bs4eqrr4551s2RhRjIyMiI64neYDCECuWSgWQcvLrSpMfjwWq1TtoKOVUkI7IQ77h6\nl0N1dTVWq5UdO3bEvOnHRQJ8PqSGBujrA4tFe1KeQkHTZGRhOmHweAiI/zOfQerqQvnLX7TUQ38/\npKWhlpVp0QKrVdNycDjA4UDk5KDYbNDfj1xXh+UrXwG3O+RXIZ09i+XOO3EtWoQapfjJ+MILyE1N\nmmOmohDweDB2dSH39iL6+xHz52ty1oA6QUtWcOtWrRizvz8yDTEyAn4/lq99TbtmkoT/1lvxPvbY\nOWnsKUKSJCwWC2lpaZjNZsrLy0MFlHr9g+4BIklSRPpCV6A83wQi1XQWUi0Noe/vE0UWMjIyZsX1\n3759O/v375/xODoRue222+jo6OCVV17hiSee4Omnn+bll1/mE5/4xIR/O0cWYiAZNtXTQSLTEEII\nent7qa6uRlEU0tPTKS4uTihRAO0ATka0ZTKy4HA4qKqqwul0UlFRwcKFC+PycgiN6XAgv/IKks0G\nqgpCIPLzEddei4izbSlZBY6TwmrF97//N3JtLVJzM8af/QxhNIYKBxFCe9oXAsnrheFhJLcby1e+\ngnzmjEYUrNZzqQejEVwuzI89hjsKWVDeflvTctA7dnJyMAwPg9OJ1NGhvc/gIOqqVQSuvDL6nNPT\n8X7rW5j/8R81Yy3Q5un1Rn5+ITD+5CeIJUvw/dM/xX3doiGcrEmSFCqgXDDataGqKk6nM5TCaG5u\nxul0YjAYIshDog2NomEuspBc6OQm2jWeTeqNJ0+eDBX4ThV2u53GxkZaWlro7OzEZrPR0NAQ8vMx\nGAysWLGC0tLSmOPMkYUEINk1C4kiIw6Hg+rqaoaHh1mxYgVLlizhvffeSwrRSUaBI0xMFgKBAA0N\nDbS0tLBkyZIpdXCERxak995D+uADraPAYtEOvOZm+P3vEYsXQxwFnxdMZ0F7c80CuqIC+YMPUE6e\nRC0uRrFatYjC6Pyl3l6k/n7U+fNBlpF7ejRyFAyeIwujaQSpoSH6fCyWCAdP1WzGs2QJ1tZWjYz0\n9RHctAnfAw9AjKruwHXXoZaWYnz+eaSWFiSnE+XwYaQxn1cSAuNTTyWELMQ6gGVZDin86QWUwWAw\nQoGyq6srZGgUTh6iyQknc66zDakYWYjlC5GINITD4aA+zL69qamJ999/n3nz5lFcXMz9999PR0cH\nP/3pTwF4/PHHKS0tZfXq1fh8Pvbv38+LL74YUwMhGnSiuW/fPk6dOhUiwVlZWaxcuZLbb7+d7du3\ns3Xr1rjW7BxZSADOR4HjTA50v99PQ0MDra2tLF68mPXr14cO0tlQWzCTccNFo9LS0iZNOURD6HAP\nBDSiMG+eRhS0X2oulXV1SK2tiFWr4h8vgZhOKFTdvRvl1CmkwUEC27djeOcdpL4+jRCMRn3kvj6k\nY8e04kiPR/u5waBFIkavs5ggBRO8+moUmw3hcoVSHIrDoXU2CIHk8WA4eBClsRH3j34UaumMOtcN\nG/COFjKa77sP5d13QymMcMg9PZqN+AwO5OmkgRRFiVpAqZOH8ALKsQqUGRkZ0z5A5yILyUWsgkyH\nw5GQyMKxY8ciOhnuvvtuAG655RaeffZZOjs7aW1tDf3e5/Pxta99jY6ODtLS0li9ejWvvfYaH/3o\nR6f0vvq6KS8vp6Kigk2bNrF27VqKp1j7o2OOLMTAVASIZmM3hC4iVVtbS0ZGRtSDNFlk4XyQEIfD\ngc1mw263U1FRMW1PjtCYqhr9IBoN3RPn57kgOgtRENy6Fam3F+W//xvJ5SK4di1SVxfyaE4es1mL\nHAwMIBTlHEEIBLT/jxIKdelSpN5erQYhDP6bbkI+dgzDO+9Adzdmvx/D8LA2z+xsbXy/X6uHuPNO\n3AcOTNpdAWh6EFGIgpAkREnJjIgCJE5nIZofQbgCZW9vL42NjQSDwXEKlPEWUKZSzYIQIuUiC5PZ\nUyeCLFx++eUx791nn3024t/33nsv995774zfV8eDDz4Y8W99b9I7weLFHFlIAGZjGmJoaAibzYbX\n641pipSKkQW/309tbS3Nzc0sXryYDRs2TE00yulEqqwEtxuxeDGyfribTLB8OdK772pP0/qm19cH\nWVmIOHOGFzQNEQ5ZJvDxjxPYuRO5uRnMZsxf/7pWiyBJ2ucb/U/y+RB5eVpRpNutfxBEXh7KmTOY\n77kH72OPRUYZMjLwPv44gT/9CeX0aQZaW8l/6SWUtDSNKAAYjYiMDOSqKuTTp+Nqg/R/8pOYHnoI\n+vsj0hySEHhHn8pmgmTqLJjNZubPnx9S2xNC4Ha7QwqUZ8+ejVpAmZmZGernD0cqRRb0+z2VIgux\n0hB662SqQ/fT0UX4prue5shCDEylwDHZkQXvmIKvieD1eqmpqaG7u5ulS5eydOnSmDdvMslCosfV\ne6Krq6tJT0+Pq611LCSbDfnZZ5Ha2kBVEenpFBUUIEaLe9StW5FbWpBsNkRWlhaalyTUK66AKLbR\n0XBBdBZiIS8PdfSQl5uatBSLz6cVEerEYXSjD+zciVJTg8jMRCxdisjI0KIDVVUYXn0V/y23RI5t\nMhH88IcJfvjDDL34IvNfekkjXeEwGpGGhzH88pcEOzsJXnJJ7NqPjAzcv/sdln/4B631ExDp6fi+\n/vXx7z8NnE+LakmSsFqtWK3WcQWUegpjbAFlOImYIwvJRazIgsPhCH1nqYxErZ85spAAGAyGuN0b\npzu+0+mM+RpVVWlpaaG+vp758+eze/fuuExPkiUlnegCR6fTic1mw+12U1RUxJo1a6Z+gDocyD/5\nCVJHB6K8XAtnDw6Sd/w4HD4Mn/0sLFyI+ulPI50+rdUoZGUhVq1CrFwZ99tMFFkIhf2EQK6uRurq\nQl2yJGYuf7Ixpwq1pATlzBlEdjbS0JAW7g8EtHoNpxOlslKTjC4v14gCaNEBsxn56FGIcVh7iosJ\nWq3ILpeWhgDNAbOzEwIBDC+/jPG111BLSvA+/DBqjGuqlpfjOnxYqxUZHNQEncLMsGaCC63gGF5A\nWVRUBGiHVrgCZU9PDy6XC0mSaG1txeVyhYhEMgzrEgF9H0kVcgN/+5GF8D04fM1PZ/3PzlU3ixDP\nJq3fvIFAICmtVJM9/ff29mKz2ZBlmU2bNk3J5CRZ9RaJIiHBYJDGxkaamppYvHgxqqqSnZ09rcUu\nnTmD1N5+jigA5OaipqVh+etf4dOf1sLyBQWID32I6R7NsdZMoLOTtAcfxHDsGJLXqzlDXn453gcf\nhEmiJLHWodTXp+krNDRAdjbBHTtQV60aJ3rkv+UW5PvuA6dTIwMul9a5oCgEV65E6utD7uxEqawk\nePHFIcIgBYNIDgeGX/wCkZOjRQeskf4Swaws+m+4gYXPP48YHASLBWlgAPx+RGEhoqwM4fcjNzVh\nevBBPC+8MGn9gVixYtrfw4RjzsIOA0VRyM7OJlsnWWgFlEeOHMFqtTIyMkJ7ezterxer1RqRvsjI\nyJgVT/P6w1Kq1FjA+SlwvJBI5DqfIwsJgH6DJJMsRDvQnU4n1dXVDA0NUVZWxpIlS6a8OJJFFmYa\nWdD1IGw2GyaTiW3btpGdnc2JEyemP67LpRUqjjmgghYLktMZ2TY4CaQPPkA6dAipuxuxfDnqnj0w\nqhsfjSwEg0EaGxrIuusuzKdO4crOhpwcjF4vxldfxWSx4HvooYnfL8YGLLW1YXr0UeSGBi0F4Pej\nvPEG/s9/nuAYA5vARz+K4eWXMbz1FoyMaOkHRUFdswZGfRMYGAC3G6mzE7FiBQwNIXV0oHR3a10K\nsoy6eLEWHbj44ojxu//H/2BecTHGZ5/VOi9UFVFYeE6UyWhEXbAAuaEB+cQJ1K1b47reicT5TEPM\nBEajEUmSWLhwYagLw+v1htIXfX194woo9RRGenr6eT+0U624EWK7+drt9gjylmpwOBzs37+f/Px8\nrFYr6enpoZSY1WolLS2NtLQ0LBbLhFYG4ZgjC5MgnsiCJElJrVsYW+AYrimwaNEiLrnkkmmTlPOt\nhxAPXC4XNpuNoaEhKioqIqyxZ1QPsHgxIi0NhofPhckB89AQvjVrsMRZJCn94Q8oTz0FdjuS2Qxv\nv438xhsE77sPsXr1uDXT19dHVVUVWR0dVDQ3Q2EhWK0EAwE8ZjM+txvp5Zep2rGD/N5ecjo6MOfm\nIu3cqfkzjH72iT634aWXkOvrtbD+6FOS1NaG8Ze/RN2yBaHXWgiB8Uc/QhoaIrBnD3g8Wk2A03lO\nBCkzE7WwELmtTeucEAJpaAjJ50PNz4fMTAgEkFtbsdxzD64DByLqDySDAf++ffhvuw355Eksd9yh\nKTOGHyJmM1IgEHKmPN+40GmIqWBsatNsNmM2m8kf/U6FEHg8nggDrdraWiRJGteBEa2AMpFINV8I\n0OYc7aAUQmC326dtpDcb0NPTw+OPP05+fn7obNJToYqioCgKJpOJQCDAqlWreOKJJ2KON0cWEoTz\nYfYU7pxotVqnVeA30diJxnTG1VMOzc3NFBUVRSVBMyEhYsUKxLZtyG+9hRge1sLkvb0EsrPx7tpF\nXFdyeBjluee0KMSqVVqIXFWhuhr5Zz8j+K//GiILXq+X6upqenp6WLFiBUuDQRS/HzU/H6OinOvg\nMBigr4+LnnsOubWVYCCAT1URL7zA4Mc+hvfTn8bv90e/nk4nysmTiIKCCBlkUVSEXFuLbLNpKQNA\nam5GOXoUtagIRs2mxOAgss0GfX1ap4OiIIqKEE4nwS1bCG7fjvGZZ7SIhb7WjEZEYSFSRweGQ4cI\nXHvt+HkZjajr1yMWLtRqRMLqDaTBQURmpiYedQGQSmRhspSJJEmhJ8TCwkJAIxgulyvUgdHa2orD\n4cBgMIzrwIjniTJepJrGAkzeOpnKkYWCggK+//3vhwpq9f9cLhdutxu3243X62VgYCBUOxMLc2Qh\nQUhmZEFRFHw+H0eOHMHtdo9zTpzp2LOhdbKnpwebzYbRaGTr1q0T3qQzasmUJNRbboFFi5AOHwa3\nG3X7droWLyZt2bL4hqiuhu5uKCsLnxQsXIhUU6P9DnC73Rw+fJi8vLyQ74ZQVURaGpLDoT1t+/1I\nLhcMDiL5fKS3tmp1BmYzQlUJnj2L6c03qVu9mqGMDAYGBujv7w9t9tnZ2VgnirJ4PEg9PRgffRTD\nT35C4NprEQsWaO89qkoIoxoKTU2auqPdDkYjcm8v6qJFeP/1XxHZ2Rj/8z81E6pw6IfCwMDEF8ts\nxn/LLZi+/W2k1lYtKuFyIQUC+G++WVPEvABIJbIwHZ0FWZbJyMiIeCrWCyj1FIZeQGk2m8d1YEy3\ngDJV0xB/qzULGRkZfPjDH07YeHNkYRJcaH8In89Hc3Mzfr+fefPmsWzZsoRWQyebLEy2MbtcLqqr\nqxkcHKS8vJzFixfHfP2M9RssFtSPfQyuuUbrBLBY8L7/PpZ4UxujLoqMfb0QIEnYnU4aOzrweDxs\n3Lgx1G8PwLJlBK+8EsOBA+DxaHoPLpcWpTCZkOx2pEAAYTYjyTKGRYswVVez0u1Gzc8n9/33yfT5\nGM7NpbOsjFqvF0mSWFlQQMF776FarZjS0lD8fpQ//hG5uxu5pgYA4yuvEFy9WktJ2O2hKIHIy0Ot\nqEBubtbqNhSF4EUX4fva17R2UlVFlJQg22yI8MpwtxsMBtTy8rBLMP4aBvbuhbQ0DD/7GXJbG2LR\nIvyf/CT+z342vuudBKQaWUjEARytgDIQCITIg26ipRdQjlWgjCdikKppiGj7qaqqKU8W4JzGgv69\ntLa2MjAwEDJU0/U9rGOKlaNhjiwkCImOLKiqSmtrK/X19aEbfMWKFQnf5JKZhoCJQ5PBYJCmpiaa\nmppYuHBh3HUXCRN7UpRz+f0pKC6KVaugqAippUVreZQk7bDv6KB39Wreq68nf/58DAZDJFEYhe+B\nB1CNRkwvvKAduOnpqGvWIHV1QV8fUlsboqIiootBPnGCiscfx2i3Y7RYyJMkSteuxf2tb+HIyMCV\nno6rqwvj6dPYg0Gy2tow6qZMiqKlEAIBlA8+ILhmDZLLpaUiMjKQBgfBaMT7wAOomzZpxYvh3SKy\njP+22zDffz/S2bOa9oTPBy4XwUsvRd2yJfYFkyQC115L4KMf1T6vxRJ3EWmykCpkQV+TyXpaNxgM\n5ObmkjuakgLt4UQnDwMDAzQ3NxMIBEhPTx+nQDl2XqmkCaFjosiCfbSeJpXTEHBu7QSDQX7961/z\n/PPP09DQwPDwcCgF5XA4+MpXvsI3vvGNmGPNkYUEIZFkoa+vj+rqaoQQbNiwgczMTN56662kbHLJ\n0lkIX6Rjb0a9y8FgMLBly5YIvf14xvVHkQKe6VzjLprMyCD4hS+g/OAHUFmJZDDgc7vpz86mY88e\nduzcicvlomEC8yUyM/F94QsoPT2oGRlarYHVinLkCEp/v9Z54PFo6YrOTuT2dmSbDaPPh2qxQEkJ\n6vz5yCdOYH76aaT/83/I3LwZ6bvfRXnzTQzf/z5KuCaHqiK8XlSzGdnrRbS347n1VixnziD19SEy\nMwnccAOBm24654cxBoFrr4VgEON//AdyRwfCYiGwdy++O++M/+CXpHGtlhcKqUIW9DV5Pg9gk8lE\nfn5+1AJKu91OV1cXdXV1CCFC0Qf9/7FC+rMVE0UWdLLwt6CzIMsyBw8e5KGHHuKKK67AaDTS0tLC\nTTfdxP79+8nJyYnwrpgIc2RhEpxPFUeXy0VNTQ39/f2UlZVRXFwccZgnozUzWd0Q4ZEFHW63m+rq\navr7+ykvL2fJkiXTyscmw3dhKmOKSy4hUFRE8K236LHZGMjMZN7117Nu3TokScLtdsfWRAgGESaT\nJh89yu6Dq1cjNTcjd3VpzouSpLVC+nxIwSABiwVJVTUFRoNBk2F++23o74e8PEReHiI7G9nj0Q59\nh+Nc5ERVkYNBzQfC5eJPu3ZhvegicoTAVFpK+rJlZEkSE5a6SRKBv/s7Ah/7mEYwMjISJpB0oZAK\nZCFcw/9CIVoBpRAiQoGyra0Nh8MRqrJvaGgIRSASWUCZDMSKLKSnp6cc+RkLfR967bXXWLlyJd/7\n3ve45557yMrK4p577uGqq67i4YcfnlT0D+bIQsIwk26IcOEhvQsg/CYLf0pPNJKVhtBbdFRVRVVV\nmpqaaGxsZMGCBVx66aXTJj3JIAtTbcdUVZUWWaa+pIQF27ZRUVER8XlCrZMDA0inT2uiRKtWaYWV\nkkSwqAixYAFyRweqXliZkYF60UWaHsHChVrRY0cHzJ+vFQcqCsJg0MhDR4dWZ+ByIbndIdEiuaZG\niyRkZyM5HKE6CiQJaXRtyhYLH/nxj/FbrQzs3s3Z9HS6GxtxOp2hYrfwavmIpy5FQYweGBMhFQ7h\nVIksJDsNMV3obZkZGRksHPVLUVWVmpoanE4nXq+XxtE1ZTKZxq2pKfm4JBG68VU0QqCrN6bCOokF\nfV/r7+9nyZIlAAwMDIT2qw0bNtDf38+ZM2cmLYacIwuTYCqRhXj9G3QIIejq6qKmpgaz2RwSHoo2\nh2SLJyUrxdHX10dzczOKorB58+aI/Oh0x7yQkYWhoSEqKytRVZWLL744wnEwfLyc48cxPP00dHcj\nASInB/X66+HGGyEtjcBHPoLhF79ArqxEpKdrXQqFhQT+/u9RV63C8ItfoPz1r5pd9tmzWuGj0Ygw\nGJC8Xq1j4aKLIs2t0tNBCK2Wortbk3GOnJgm2PT++yiqStG77zL/ppvwffObBILBiGI3XS0wPFed\nnZ19QcR+Eo1UIwupMFdZljEajWRlZVE+WvSqF1Dq6+rs2bN4PB7S0tIiyENmZuYFeYLX971oaQiH\nw5HyKQg4t3bmz59Pf38/ACtXruQPf/gDJ0+eJDMzk7a2tlDaKRbmyEKCEI9/QzhGRkaw2Wy4XC7K\ny8sntVdOVreFfpPG6jeeDtxud+hpo7y8nOLi4oRsesmKLEx2bXWny7Nnz7Js2TKWLl064ROfoa2N\nxQcOaDn6igqEJEF3N/LzzyMvWgTbtqFu3ow/OxvlxAmknh7URYsIbt6MGPWaFwsWaGkESUItKEDq\n6EBSVSRVRcgyWK2aqVLYJhu49FKMzz2HNDBAcMMGzefB49EiDEajpo9QVnaueHF4GONvfkPguusw\nbNgwrthNt1seHh6mu7ub+vp6gIhKeV3sB1JHGTFVyEK4U2AqYOxT+kQFlDp5GFtAGb6u0tPTkx5R\n0e/5idIQfwuRBf2z3XjjjZw8eZLe3l5uvvlmfvOb33DzzTczODhIWVkZ27dvn3SsObKQIMRbs+Dz\n+aivr6e9vZ2SkhIuvvjiuA7pZHctJIosqKpKc3MzDQ0NSJLEunXrQrnORCBZZGGiokldCKu6upqs\nrCx27do1aZuR6fhxTfRp9epzXQ0LF0J1Ncrhw7BtG1JfH5LDQXD7do0gjNmUgtu3o5aXa5GH+fPx\nqSqmnh5QVdRNm/D98z8T3LUrcq5lZfj+1//C9IMfIA0Poy5aBKpKcN06FJtN62II/46zsuDsWZR3\n341qHR3NbtnpdIaiD83NzTgcjlCo2ev1oqpqTAnd2YBUIQup1l0QDAYnTS+aTCby8vJC/jW6eJm+\npnRSKoQYp0CZlpaW0O8tGAxOaNn8t2AiFY7du3eze/fu0L9feOEFXnrpJQD+/u//fi6ykAgkqsBR\nVVXa29upq6sjJyeHXbt2kT6FIrFkiT7pTy6JICL9/f1UVVUhyzKbN2/mzJkzCQ8vJisNEe2p2Ol0\nUlVVhcPhYOXKlXELYclOJ0Ft4MhfWCxIfX2YnnoK4+uvI9ntCLOZ4Nat+O+6S1NQ1GE24/23f8P0\n0EMoZ86g+Hx4Fy9G+dzn8O/bN6EBU+D66wlu2YLyzjvg9aKuXYu6bh3WSy89J+k8FnF+R+G56nC3\nxPA+/b6+Prq7u8e12p2PJ8V4kUpkIRXmqWM6Co6SJGGxWLBYLBQUFADa9xOuQNne3o7D4Qi5dY5V\noJzuNdKLG6P9vR5ZSHXoxP3hhx9m7969lJWVEQwGKSkp4a677gKgrq6OrKysSYneHFlIEGId5gMD\nA9hsNoLBIGvXrg3dFFNBsiILiRjb4/FQXV1NX19fRBdHsqIAcY+pF/jFM2YwqIkVGQyoZjONjY00\nNjayePFiNmzYMKWiLFFcrKUefD5N4wA0SWiHA4aGML39NiI7W9M6cLkw/PGPSB4P3u98J2K+oqQE\nddculKNHUQYGtGLGwUFNTCrGk7tYvFhrhQxD4OqrMT733Lm/tduRRnOY6sKFcV+rsVAUJRRq1hUB\nFy1aFGG1rD8p6ht9dnZ2qFL+QhyGqUQWZgvBigeJUnCUJIn09HTS09MjCijDFSjHFlCGk4h479XJ\npJ5TXZAJzjki33///Vx++eWUlZWNI3SrV6+msrKSFbrZ20RjJW2W/58hGllwu93U1NTQ29vL8uXL\nKS0tnfbNdD68J6YKVVVpaWmhvr6ewsJCdu/eHcpfQ3I0HCYlC34/Uk2NJsvs9WoH98qVECPMZm5q\nwvr66xicTtzBIM0FBQzu3s227dvHF5x6PJrjZH8/Yt48xLp14/QJAtu24SgpIaemRntfRYGeHk0S\n+uxZVKsVdMJoMqEqCvLJk8hVVairV4fGMf7oR5j/5V+0VILRiOx2ozz1FHJrK54f/3hK181/++0o\nx44h22xIw8MakZEkRHY25kcewd/bi//WW6dFGMYiWvrC5XIxPDwcSl84nc5QQVz4f+cjfZFKtRWp\nRBaS6Q0hy3JojSwalSsPBAI4HI4IEy2Px4PFYhnXgRFtXrF0If5WyMJbb71FTk4OGRm1RhbkAAAg\nAElEQVQZdHd309zcjNFoxGQyYTKZGB4eJi0tLS6tmzmyMAnifQIJP3DD1QkLCwtD3gAzQbIKHGF6\nh3p/fz82mw1gwq6AZGg4xCQLqor09ttIJ09qxYUmE/KxY4jWVtSPfATCw/w6GhvJ3b+f4Nmz9OXn\n47HbKWlvp9xqRVx+eeRrOztRnnpK84BQVe2wragg+OUvQ5jfApmZ1H3qUyzq7kZ+5x2tnfHKK1Ev\nuQTlwQdRMzOJWFUZGUidnUjd3VqdA4DXi+kHP9C6G7KyEMEgwmhEDgQwvP66RixWrYr7uonCQtz/\n9V+YH3gA4yuvoOblQVERIjcXqbcX47PPEtyyBXXt2rjHjIZo90v4k2J4+iK8+0KvlLdarRHRh2Sk\nL1LlEP7/NbIQLwwGAzk5OREHnd/vD62poaEhWltb8fl8EWmxzMxMMjIyYspTOxyOqAqsqYZ//ud/\nBrTP8+1vf5vMzMwQUTCbzdhsNtasWTNHFhKFeGyqDQYDfr8/1AppNBoT0iqoI9lpiHgPdY/HQ01N\nTchJUU85REMECQkEtDB/WtqESoHxICZZ6OpCstlgVMoYQMyfj1Rbi2SzIcIKfHRIhw4hzp6lf8EC\n0jMyKCwrwxAIIJ05Q/D0aYQuZywEys9+hnTmDKK8XBNT8nqRKitR9u8neO+9oadySZLw5uSg3ngj\n6j/8gyYHnZEBbremgTA4eM7BEcBuR1itEW2QUmur5s44VtTGbIaREeRTp6ZEFgDIyUFyu1EXLkSU\nlIR+LObPR2poQPnLX2ZMFuKFoijjNvrwQrdo6YtEWS2nUhoiFeapYza4ThqNxqgFlOEGWg0NDaiq\nislkChUw6xLW+vW22+0sX778Qn6UhOCrX/0qIyMjtLS0cMmo+6zb7cbhcBAIBLj66qu544474krd\nzJGFBMHj8QBQWVlJRUUFi0YFeBKFC52G0L0q6urqKCgoiCtaoigKajCI9P77SEeOhA4/sWEDYufO\nkHrhVBCLLEiDg5r/QLgHvSQhcnKQWlsZS/fsdjuuQ4eQjEZMZjMLFizQfmEwQDCoeSHoLz57Fqmy\nErFkybl5m82I4mKNoLS1wWjbYwS5TEs794ZpaQQ//nGUH/4Qurq0eblcmk32pZeiXnTRudfm5mrp\ni7HfSzAIshxZDDkVjBpARUA3x/L5pjfmKGYa3p8ofaETiHCr5XDth6kK/aRKGmIusjBzhBdQhq8r\nt9tNU1NTqDC3pqYGSZJ49dVX8Xg8DA8PEwgEZkQs//znP/PII49w/PhxOjs7eemll7juuuti/s2h\nQ4e4++67qayspKioiHvvvZcvf/nL03p/gM985jOAprNwww03THscmCMLM4bf76e+vp62tjYAtm3b\nFmENmyhcSLIwMDBAVVUVQgg2bdoUYu2TQZZlDJWVyMePIwwGTWDI6UQ+eBDhdGruj1NEzMiC0agd\neqoa6Vng9yPCnmADgQD19fW0trZycWEhGXY7g+GvV1Ut/B/ereLxaMWB0Z70/X7Nz2H0R7EiUYHP\nfpagy4Xptde0tIPFQuAjH8H31a9GFjfm5xO46ioMr76qKTfKskZgvF5Nk2FsiiROBLdvR7bZtEiP\nThpcLlCU8xZViBfRCt10q2W9/kHPU+vpi3CnxIkOrlSKLMy2wzcWUsV1UpIkrFZryAxr5cqVqKqK\n0+mksbGRN998kzNnzvDmm2/y1FNPsWXLFrZu3crnP/95SktL434fp9PJ+vXrufXWW9m7d++kr29q\nauKjH/0oX/ziF9m/fz9vv/02d9xxB/Pnz4/r76NB/05uuOEGfvvb33Ls2DEyMzO54447MJlModqM\neL63ObIQB6Jt/kII2tvbqa2tJSsri507d/LOO+8kbROajkJkvJiILHi9Xmpqauju7qasrIySkpIp\nbV4KYDl1SntC1sPemZkIiwXpgw9gyxaYogZDLLIgioqQ8vOhvR0WL9YOWLtdOwxHn9p7enqoqqrC\nYrGwY8cOsjIy8H33uxj7+7X0RSCA1NSEWLgQsX79ucGLijTp5e5uzbp5FFJ3N+TnI8JqFvT1EvVQ\nMhrx3XYbwRtvRD57FpGbG/G34fD+3/+L1N6Ocvo0sqpqtQ8LF2rFjdOUyw7s3Yty6JDmO5GerkUq\nfD6Cl15KMEqaZrYhmtVyuFNiX18fjY2NqKpKRkZGKPKQnZ0dSl+kCllIlXnqmA1piKkgvMBRb8u8\n9dZbufXWW9m5cyePPfYYpaWlvPfeexw9epTBwcEpkYVrrrmGa665Ju7XP/300xQXF/P4448DmtLi\nsWPHePTRR6dNFhRFwefz8cwzz/DII48QCAQYGRnhq1/9KkNDQ9x2221s3rx5UsdJmCML08Lg4CA2\nmw2/38+aNWsoKChAkqSkaSHA+W2dDLfHzs/Pn3aBpsHrRR4aijhcAcjJgc5OpKGhSb0GxiJmZCEj\nA3X3buS//AVG1QaxWBCbNuFasgTbiRMMDg5GpInEtm24r7kGXn0VqapKC/EXFaF+7nMQXuCUlkbw\nYx9DefZZpJoarfZgZAQUheDHPhZhrBRrgw/9bt481ChFoeEQBQW4X3sN5dAhht99F3tGBgtvu21G\nJk6iqAjv449j+PWvNSOqtDQCV11F4IYbpk1ALjSiOSWGpy/a2tpCLqdZWVmoqhqy6J0tPgXRkIqR\nhVSbbzRtASEEDoeDgoICdu7cyc6dO8/LfN59991x/gxXX301zzzzDH6/f8prVSebdXV1fO973+OR\nRx5hw4YNXHHFFRgMBvLz87nqqqt4+eWX58hCouHxeKitraW7u5tly5ZRWloawaSTmSo4X0RkcHCQ\nqqoqVFVlw4YNcSl7TQQpLY2AxQJOJ4S3IDqd2iE+Dcti3fRpwqeupUtD8sgEAgRzcmjxeKj/619D\nnSkRG4THQ+CiizgbCJC/ejWkpSEqKiLrHkYhrriCYHo68ptvavUMa9agXnEFYoxUqr5hJuTJUFEI\nXnEFA2VlDA8PszABbo9i8WL8d92Ff1SU5W8NsdIXIyMj9Pf309zcTE1NTcinQO++iJW+ON9IJbKg\n+yykWmQhLbymKAwXonWyq6trnNptYWEhgUCAvr6+0FqOF/r+09zcjCRJ7N27lwMHDkTomxgMBgYG\nBuIab44sxAFVVWlsbKShoSFmcV8y2xuTHVnw+XycPn2a7u7uGWtC6JAtFpwVFVongtkMozULUmsr\nYs2ayHbDeMccnVPMkGd6OqK8PML0KVqthXToEPLvfkdWayvC4dDMmT796ahEQfsDCbF9O8Ht28fX\nRUS8TArNcew1jNpa2NeH8tZbyCdPgiyjbtlC4LLLtAhMjL+bjZit8wxPX9TX17Np0yYURRmXvggG\ng+O6LxItMxwvUo0swOxzyIyFWDUWF0rBcew609PfM1l/Pp8vpF+iE2n9e2poaIhbjn+OLMSB6upq\n+vv7J9QT0JHsp/9kjK0row0ODlJQUMDu3bsnZNtThSzL2FeuRC0s1A7CmhotorBuHerVV0942E42\npj7viW70eEyfpNOnkZ9/HiSJYEkJnq4upNpa5GeeIfj1r0cc1BNMZMJf6Td2XFX3g4MYn34a2WbT\nOhxUFcOvfoVUW4v/jjsiUg6pUsU/mxEelYqWvnC73RHOm3a7PZS+0GsfpqISONO5zlbyNRY6WUi1\nyEI0ETCv14vP54vqAJxMLFiwgK6uroif9fT0YDAY4i4qD4e+523cuJGlS5fy4IMPYjQakWUZp9PJ\nK6+8wh//+Me4uy3myEIcKC8vR5KkSW/cZJKFZEQt9JSDx+MhLy+PjRs3JnR8RVEIyjLiqqsIbtqk\ntU6mpWnFgtPcBMPJwliEmz5lZmbGNH2S3n1Xk09etQrJ7SYY1gYpvf/+eEGmKWAyshC+jpSjR5Fr\najTNhNGNSxQUoJw5g/r++yGzqFQ4NFKJzEwkHqVXyetttDqZ1rsvuru7cbvdETbLOpFI9FN1KkUW\ndFOmVFinOiaKLIyMjACc9zTEjh07+O1vfxvxsz/84Q9s3rx52uRUCEFpaSlf+MIX+M53vkNvby8+\nn4+rrrqKyspKbr/9dm6//fa4xpojC3HAZDLFRQJSpcDR5/NRU1NDV1cXy5YtCxX0JBoRxYh5edPX\nBghD6CBub0f+85+RTp2CjAzcW7Zwev587F5vXKZPUnc3YjTdEOp2GbWElkZGxmkyTGuOcRyecm2t\nJlIV/oRjNmvzaGmBMLKQSofxbMVUw7rhMsM6wlUCw22W9e6LRKUvUo0spJKdNkwcWdC1PGYaYXU4\nHCFbd9BaI99//33mzZtHcXEx999/Px0dHfz0pz8F4Mtf/jJPPPEEd999N1/84hd59913eeaZZ3jh\nhRem9f7hkanrrruOD33oQ/z0pz+lrq6OtLQ0vve977FFF52LA3NkIYGY7WkIIQRtbW3U1taSl5cX\nSjm0tLQkXJYZklNnIUkSaX19mF98EbmlBbKycA0P4/njHym58kpyHnwQY7gWghDQ0oL8hz8g9fUh\nystRr7kGUVyMXFeHIOwgHrWpnimpmQpZEJmZmubBWKhqpKBTnOPNITYSkQOOphI4Nn2huySO9b6Y\nzNlv7FxThSykWtskxI4sJCJSdOzYMfbs2RP699133w3ALbfcwrPPPktnZyetra2h3y9dupTf/e53\n/OM//iNPPvkkRUVFfP/7359W26ROFDo6Ojh06BAjIyOsWrWKO+64Y9qfZ44sxIFE2VTPBAaDIVRx\nPJ2NbmhoiKqqKgKBAOvXr4/QPU9W8WQyjKQAFhw/jtzYiLu8nP6hIeT588lfuJB5NhvBujqteNLv\nRz5wAPk//xP5yBHt8LVYwGhE/dGPCH7jG4jjxzXTqbw8jHY7UnU1orw8Ul9hGpgKWVA3bED85S9I\nPT2IggIQAqmzE5GRQXDNmnFjzmFmSARZGItY6Ytw+WqXy4XFYomIPmRkZEx4yKqqel6MtRKBVGub\nhIldJ0dGRhIirHf55ZfH3AOeffbZcT+77LLLOHHixIzeN7wL4q677uKtt97CbDbjcrm47777uPvu\nuydMz8ZCaqzEFIGiKEkVToLYtqrR4PP5qK2tpbOzk6VLl7J06dJxm1OyyEIyjKQAcuvqGDEYGOnv\nJycnh6zMTK0GorcXqbYWsWYN8g9/iPLLX2raCX6/lmLw+xHZ2chVVfD88wS/9CXkV19Fbm5GdrtR\nr7oK9YYbJu6GAGhuRjlwAOn4cURODuJDH9JMqsbkFONNG6jr1mn6DQcPIldWAiBycwlcfz1ijGXs\nXGRh5kgGWYiGqaYvwqMPukdBKnlDpFpkQVXVCeesd0KkyrUfC50sPP3007S2tvLtb3+bDRs28Itf\n/IIf/OAHXHbZZVxyySVTfvCcIwsJRLJbJ2HiPNtYhCtM5ubmxiz2S2ZkIZFkQf9MstFImtvNoqIi\nFP1aCKE5OZpM0NyM/PvfazdDMKg5UMqyZi9ttyMyM5EPHSLw0EME77sPT3Mz1cePs/BTn4o9gYYG\nDPffr7V+ZmQgNzXB8eNINhvBe+6JKNrUN/tJIcsErruO4KZNyPX1WutkRUWEqZQ+XiqQhdm+wZ4v\nshAN0dIXHo8nwnmzpqYmpCaoP3j4fL4ppS8uBFItsqDvd9H20r8le+rPfe5z7Nu3D9AKKA8cOMDZ\ns2eBqXfbzJGFODAb0hCyLMcd1h8eHqaqqgqfz8fatWspKCiI+fpUSEPY7XYqKyvxeDwUbNhA0Z//\njOL1aoWBQmgH+Lx5qBs2aC6TIyMaSRDi3CFuMIDXqzk+BoOaDHReHtLixXhHHQ5jfdfKL3+J1NKC\nuOgiTekRYHAQ+Q9/QP3oR7X0xyiiHe7BYJC6ujoGBgYihIAsFgsUFxMcNaKKhtl+CKcKLiRZGAtJ\nkkhLSyMtLS3U6x6evmhpaaG/v5+uri4sFsu47ovZ9CSfKr4QOvR9OhrB+VshC11dXaxbty7iZ+np\n6SGCNFVyN0cWEohkkgWYvMjR5/NRV1dHR0cHS5cuZdmyZXHdwMmqLUhEGiIQCNDQ0EBLSwslJSUs\nX76cIz4fXq8X45kz55wSc3NRP/95zROiowMMBkRmJpLBoL3GbA4RB8nhQF29OiQKFV5jMOEhoqpI\nR48icnMjNRZycqCnR3OkDCMLutKkjr6+PiorKzGZTCxcuBCHwxFyUTQajRHkYSJjl9keWZjt84PZ\nP8fw9EV/fz/5+fkUFBSELJaHhoZoaWkhEAiQnp4esWbCLZbPN1ItDaGTm2jX60IJMiUaTqeTgwcP\nhjwwiouL6enpYWBggN7eXoxGI2azOe6ujzmykECcD7IQ7VAXQoRsVnNycti9e/eUCliSVVswUxIy\n1vQpdAOnpzO8bx9pZ88iNTSAxYK6aROMelCI9esRpaVIDQ2h/+N0akWOJhOkp6PedVfo0A/XbpiQ\nbUuSRjjGtpjqh8+YMLEeWfD5fFRXV9Pd3U15eTmLFy/G7/eH3icYDIYOguHhYdra2vD7/REHwfkW\nh/lbhk4IZ0NkYTLoNQtGo5F58+aFBOEmSl9IkjSu+8I8DRv46SAV0xATpXNTnSzoa7u8vJzXX3+d\nQ4cOhYplg8Eg//7v/87PfvYzTCYTbrebAwcOkJubO+m4c2QhDsyGNIQ+/tjDV085eL3eCFOrqWC2\nFTi63W6qq6sZGBiIMH3SIcsyqsGA2LYNsW3b+AEsFoJ3343yne9oaYOiIqT+fs2G+bLLCO7bh7jk\nktDLw+WZJ4QkoX7oQyg//jHC7dbaGoWAjg4t/bF167g/6enpobW1ldzc3JBE+Nj3UBSFnJwcckYV\nI4UQeL3eEHkIPwgkSaKpqSl0EMxmE6TZilRTRYx2AE+UvnA6nSEC0djYiNPpxGw2R0QfkpW+SLXI\nQrjj5FikehpCX9/f/e53GRoawu1243K5cDqd+P1+7HY7LpcLj8fD8PBw3A+Wc2QhTsRTYJZMIyl9\nfP1Q9/v91NXV0d7ePqWUw0TjzqQtcyJMavo0BrrbZV1dXXTTp7BxJyMhYvVqAk88gXT0KNLwMKK4\nGLFhQ6T4Udh4MHmIWr3xRqTKSuRjx0LaCCInB/VLX/p/7H13cCPnffaz6CBIgLxjO9Y7dh7J64Xk\nnZxYls/RZDKyJmNrElstsWxH9iSSxhP709hxiy25TCQ3KXKsyGm2lUS2ky+WLEufrGLrdJJV7kgC\nYO8dRK+72N3vD9y7twssSJQFATB4Zji2QNxisQT2/b3P7/c8jyTnIhgMIhqNYmFhAX19fairq5O8\nf5ZlhWsilx1hMBhgMBiEWRNyXZaXlxEMBrG6uopwOAyTySQsAhaLBSaTKe8LYb5ffycUehtCjHRM\nmchQZEVFBRqvfhZJHDFpXywsLAislZh9UOJzs9eYhZ3mvIoBg3EBd9miVCwoCLLzz9XuRaPRgGEY\nQeVgNptx7tw5mLJMIsxUlrkTxFT7TsfdKfQp/rgpMRYVFeDf854d3RhTYhYAwGIB+8AD4H77W1BT\nU0BZGbjhYaC9Xfj38/PzmJqaAkVRkuFSUjSRokzM5BDWQBXXFhG/X5PJBJ1Oh76+PgAQ2AdiQTw5\nOSmhoclustCn6HcbxcQsZGvKpNFoEtoX4s/N2toaJiYmQFGUJPcik/bFXmMWirkNkSuUigUFQRZE\npRddApJ+yfM8+vr6Mmo5yCFXxQI57naLcCqhT/FQWpJJFuuUdp0GQ6wAec97JA97PB6MjY2BZVmc\nPHkSo6OjwvsnRQI5Z71eLykc4n9P3qO4iIg/P71ej5qaGsFcS0xDezweTE1NIRgMFnQEcz5QTMWC\n0j4LyVirZO2LePXFdvcGlmWLqi223b3O7/cXdRsiVygVCykilcWEfPhS9UJIFQzDYGpqCi6XC1VV\nVTh58qSixyeLktJzC2JmIR7xoU/Dw8MpMyRKFwvZHDMajWJqagoLCwuSdhDxWWBZVigK4r3zxTsb\nUizEFxAEhLFKFgUsR0MTEyCPxyNEMHMcJ9lFWiyWXRuCKwQUW7GQ68IuWfuCDN16vV4sLi6CpukE\n8yhx+4Jl2ZgEuEiwl2cWcoVSsaAgKIpSdG6B53lhwK2iogL19fUoKytTnLUg552LHAe5RTgQCMBq\ntcLv96cU+hSPXBQLmZgeETmkXq+XqDXIgkTTtJDGt1PIDvHRICBFA8dx8Hg8mJ2dhdFolHy24tmH\neMiZAAWDQSFBcXZ2NmEIzmKxbGtBvB2KYR6gVCzsDI1Gg6qqKsmEfCQSET43a2trmJycBABUVFTA\nbDYjFAplZCGcL2znC1FqQ8ijVCwoDKUUET6fD1arFaFQCIcPH0ZdXR3Gx8dz6hCZaxdH0kaZmZlB\nU1MTjh07lhF1mW9mgcghNzY20NXVhebmZolXA8uyMJlMGBkZQVlZGSwWCyorK4WFOJXFSqVSCR4T\ny8vLaGtrQ3NzMwAkZR92mn2gKAomkwkmkwkNDQ0AEofgiIafLAKkgDAYDEWzyO6EYnkfhRQkpdfr\nUVtbK5nBEbe9yP9fXl5OUF8UYr5FsjYEz/Pw+XwlubIMCu+vWKBI9QaTLbMQjUYxOTmJxcVFtLa2\nSloOarUa4XA442Nvh1waM7Esi62tLVitVqhUKpw5c0aQCmaCfDELhOmx2+2oqqrC+fPnBeo1fvag\nv78fPT098Hg88Hg8WF9fx8TEBADAbDYLxYPFYpEdQnQ4HLDZbDAYDBgcHJRt0YjZB/Frbzf7EA+5\nIThxguLi4iJsNptgHEWKh0JdBHZCseUtFOq5UhSF8vJylJeXo6GhAaFQCPX19TAajZL0zUgkIqu+\nyHcRFI1Gk7bfSm0IeRTft73AkSmzQHr44+PjMJlMGB4eTkg+y3X2RC6MmSiKwuTkJNxuNzo7O9HS\n0pL1jSIfzEIwGMTY2Bj8fj/6+vqEdEHgGptAig2yQOt0OskQIs/z8Pv9QgExOTmJQCAAo9EoFA9l\nZWVYWVmBw+FAZ2dngsdE/DkDibMP4vNJxj4kKyDkEhTjjaOWlpaEHrZ4F1kMFH8xnCNBvtoQmYDs\n1OXaF2LVztRVW3U586jd/LskYxbIwGepWEhEqVhIEekYM6W7oJOWQzAYRE9PT9Iefq5aBbk4Ngl9\nCofDMBgMgimREsgFC5KMWRDLIRsaGiStk3g2Yae5BCJRq6ioQFNTE4DYEKLH44Hb7cbS0hL8Vx0i\nLRYLQqEQNjc3UVlZmbIEMr6AIIWCeEBSroAg5y63OMUbRwEQHATFxlFkJiIajRa0cVQxFAvks1Us\nxUIy6WS8akfcvvB6vZibm0MgEJAwV+Qnl8xVsgFHv98vFDMlSFEqFhRGOsyCeJK+paVlR5VDLh0i\nlSwWxKFPRqMRhw4dUnRSWqVSgWEYxY5HjhnPLIjlkKdOnZLsmOLljjsVCsmg1WpRXl6OxcVFwYWz\nvLxcYB+mpqYE9iF+9iGVhURufiG+bZHM92G79oWcBO+dd96BVquVGEeRmY1CMY4qFmZBzFIVA1I1\nZYpvX5B/K1ZfLC8v57x9kYxZ8Pl8AFAqFmRQKhYURioLOs/zWFtbg91uR1lZmTT3YBsUOrMgF/r0\nu9/9LieSzFzOLMTLIdvb2yUuj+LFNtMigRxreXkZk5OTqKmpwfDwsMAgyLEPHo8Hm5ubmJqaAsdx\nCbMPqUogc8E+qFQqaLVaVFZWCoOYNE0LE/SEggaQV+OoYikWkklkCxXZpE7KMVfi9sXGxobQvhAP\n3pLE1kz+nsnO1+fz5URxthdQuiIpQql8CL/fD6vVikAggO7ubhw4cCCt4clCLRaShT7lYhYilzML\nm5ubsFqt0Ov1CXMjZAdOBs+yKRSIfDQcDmNgYADV1dVJn6vValFdXS08h1C5pICYnp6G3++HwWCQ\nFA8VFRW7yj7Et3HiZzYKwTiq2IqFYjhXQHkHR7n2RTAYFAqI+fn5rNoXybxwvF4vKioqiua67yZK\nxYLCSKaGEO+6m5ubceLEibSr11xmT2RaLITDYdhsNjidTiFVMSH0qQiKBZ7nMTc3B7/fn1QOKe4j\nZ3ozITMQRD56/PjxtD8HYio3mQHT9PS0wD6Q4oFIIFPBTuyD3PCk+LFk7EO+jaOKpVgopjYE+X7k\n8lzFst8DBw4ASGxfrKysCK0vcfEgV3xuxyyUPBbkUSoWFIZGo5HIG3mex/r6Oux2O4xGY8oth2TH\nLhRmIT706fz587I39FwMIypZLBA5JLlJbCeHzJZN8Hq9sFqt4DgOJ0+ezEo+Go/tDJjcbjdmZmYE\n9oEUDpWVlYqwD9FoFPPz83C5XDhw4ICixlGkgFPSOKoYigXyeSuWcwWgKLOQCuTaFzRNC8XD5uam\npPgUez8kKxb8fn+JWUiCUrGQIjJRQ/j9fthsNvh8PvT09KTVcpADWdBzccNLZ1FPJ/SpkNsQYjmk\nyWRCc3OzpFCQk0NmApZlMTMzg4WFBRw8eDCl/ItskcyAibQunE4nZmdnwbKssIsnLYx02Aev14ux\nsTEAwJkzZ2Aymba1rc7UOMrn8wmFDzGOEks3UzWOKqZioRhYBaCw5it0Ol1Cy07cvlhYWBAUR3a7\nXfj8VFRUQKfTCW2IEhJRKhYUBkmGnJiYwNzcHJqbmzN2KpQ7Nrn5Kl3Fp9LiILHYy8vLQg5CKqFP\nhcYscByHubk5TE9PC3LIK1euJNDr2Q4wAoDT6YTVaoVOp8PZs2cTvDN2ExqNJukunlhK+3w+6PV6\nyeyD2WxO+DsTN875+fmEAijZ7EOqoVly5y3W7/M8j3A4LLAPxDhKo9FIigc546hisKQGCtuQKR7k\n+12IqZNy7YtgMIjXXnsN+/btg9frxerqKn7xi1/gZz/7GVpaWhAMBvHGG2/g6NGjWQ3fPvLII/jG\nN76B1dVV9PX14eGHH8Z1110n+9wf/vCHuPPOOxMeD4VCBZO5USoWFAQx3XG73eB5HoODg4pKcMTp\nkLkoFpIt6mL1Rnl5eVqhT4XGLHg8HoyOjoLjOIkckhxTCTkkcK2wWltbQ0dHh9s0ZW4AACAASURB\nVGQGolCwnf2zmH0gvgmkeFCr1ZicnBTcOLfbiSUzjsqWfTAajTAajUmNo4j8joQfkSKiWBbhYvNY\nyLao3k3wPA+1Wo2WlhbhsY6ODhw9ehRPPfUUZmdnceHCBYRCIRw/fhyf/OQn8aEPfSit13jyySdx\nzz334JFHHsG5c+fw2GOP4cYbb4TVapW8rhhmsxnj4+OSxwqlUABKxULK2OmLEAgEYLPZ4Ha7odVq\ncfbs2Zy0CoDYDV1puVmyYoFM7ft8voxDnwqBWRDbaLe1tUlYEbLbdDqdMJlMwoKYKTY2NmCz2VBR\nUYGhoSEYjcaMj7XbSGb/7PF44HK5MD4+DpqmoVarsW/fPmxtbQmtjFSv2XahWZnaVqdqHEWeOzs7\nW9DGUcXUhsj1cKPSkNts1dbW4oMf/CAuX76MlpYWPProo5icnMSlS5cECXM6+Lu/+zv8+Z//OT7y\nkY8AAB5++GE8++yzePTRR/HAAw/I/huKoiTOsIWGUrGQBuRc/kg/enZ2Fk1NTTh06BAuX76ckyqb\noqicDTnGFwscx2F2dhYzMzNobGzMKvSJpmklTzXtYmFzcxNjY2MwGo1J5ZD19fVYWlrClStXwLKs\nsBsldHwq0/iRSAR2ux0ulwtdXV1Zz6gUAoj9M8MwmJ2dhV6vx9GjR4U0TDJDwDCM7OxDqqFZgLLs\nAyBvHDUzMwOHw1HQxlHkXItlAc5FWzSXSCabBGJqiOrqalAUha6uLnR1daV9fJqm8eabb+Izn/mM\n5PELFy7g1VdfTfrv/H4/WltbwbIsjh07hi9/+cs4fvx42q+fK5SKhQzB87ywg9Tr9Th79iwsFgv8\nfn/O5I1A7rwWxMcVhz6dPn06q6n9fLYhyOK9ubkpK4cUL0Y1NTWora1NUBEQD4PtHBTFuR779+/H\n0NCQYlK/fIPjOExPTwsGVQcPHhTet5h9CIfDcLvd8Hg8mJ+fh8/nE0yaxLMP+WQfVCoV9Ho9ysrK\n0NfXB6AwjaOA4ptZKKZiYbvz9fv9aGtry+r4DocDLMuirq5O8nhdXR3W1tZk/01PTw9++MMfYmBg\nAF6vF9/61rdw7tw5XL58GZ2dnVmdj1IoFQsZIBgMCi2H7u5uSdiPRqORDMcpjVx5LajVajAMgytX\nrmB9fV3R0KfdbkMQZ8Tx8XHs27cvLTmkXB+feAG43W7BQZH4x5tMJrjdbtA0jb6+PmEXuxdA7K53\nmk0QzxCINfCkBUAKCIZhUF5eLikgjEZjVuxDuqFZ8WoIubAvseEVMY4SS05zbRxF3luxMAvF1oZI\nlgsBKJs4Gf+53k6JMzg4iMHBQeG/z507hxMnTuA73/kOvv3tbytyPtmiVCykAY7jMDU1hdnZWTQ2\nNuK6665L2HEQeitXX6BctCF4nofT6UQgEEB5eTnOnz+vWJ99t5kFMmPh9/vR398vqe4zlUPKeQH4\n/X7MzMxgeXlZKOAmJyexubkpMBCFQGdnArHUs62tDa2trWl/ltVqdVIFg9vtxsLCgsA+iE2j0pkX\nycS2mnx3ki3G2xleeb3eBOMo8eCnkmxSsQ04FhuzsF0bIlvpZHV1NdRqdQKLsLGxkcA2JANhdScn\nJ7M6FyVRKhbSwNtvv41IJCK0HORAvjTRaDQng1NKtyFI6FMwGIRGo1G8R7ZbzIJYDtnY2ChxRlRa\nDkmGWRmGwYkTJ7Bv3z6BzvZ4PFhbW8P4+DhUKpXEAKlQh+nEIGyCWq1WVOq5nYKBtC8WFxcl0deE\ngUiXfUjWvnA6nVhZWUFtba3AzqUSmpXMOIowJ2LjKHHxkKlxFDnvYik0S8yCFDqdDidPnsRzzz2H\nm2++WXj8ueeew0033ZTSMXiexzvvvIOBgYGszkVJlIqFNDAwMACNRrNjDHEu0yGVOnZ86FN3dzfe\neustBc5QilwyC4TWI3JInudl0yGVMlciQ59zc3NoaWlBW1ubcNORy0Hw+/3CTnp1dRWhUChhISwr\nKyuIRUEJNiFdxCsYeJ5HJBKRFA9jY2OCfwK5ZunEF5NilbBAHR0daGxslJ2BINgpNEtOu6+kcRRQ\nXG2IvcIsEMYw2UYwHdx333249dZbcerUKQwNDeH73/8+FhYW8PGPfxwAcNttt6GxsVFQRnzxi1/E\n4OAgOjs74fV68e1vfxvvvPMOvve972V9LkqhVCykAYPBkNIuudCjpOVCnwKBQE4GJ3OVDQHE6OHp\n6emkckjxYpCtda7b7ZYMfe60+1CpVMKQHElhjEQiwmKyvLwMm80GtVotWQizlW1mglyxCemCoigY\nDAbU19cnsA+EtSFR3vGzD8mKLlJIarVaDA4OoqysDID87EM6oVnxUNI4ipxTsRQLe41ZUMLB8ZZb\nbsHW1ha+9KUvYXV1Ff39/Xj66afR2toKAFhYWJBcM7fbjY9+9KNYW1uDxWLB8ePH8fLLL+PMmTNZ\nn4tSKBULOUChFgvbhT4RBkBpO9xctSEA4NVXX91WDknYhGwKBRJXvby8nPWOW6/Xo7a2ViLlS0bD\nk4WwsrIy4xjencCyrKTY2g02IV0k808gRdfS0pKk6BInbi4sLGB+fh5tbW04ePBg0muYSWhWKuyD\nXNuFsE3bGUeVlZUV1cxCMaohkqlblBxwvPvuu3H33XfL/u7FF1+U/PdDDz2Ehx56SJHXzRVKxUIa\nKJQo6XQLkVRCn8TukEpmuSvdhohEIrBarQCA1tZWySIQvzvMdjbB4XDAZrPBaDRicHAwZdfKVCGe\naSAQSxAXFhYwNjYmkSCS8Kdsb84k30Oj0eTdhjpdEP8EMixGii5SQJCiS6VSobq6Gnq9HsFgMOWW\nTyq21RzHJXzHUzGOkvt7i1MTyayLVquFWq3G1tZWwc+67JU2RCQSQSQSUaQNsRdRKhZygFzPLDAM\nk/LzCX0ejUa3DX3KVbGgFLMQL4cEIJgeKZ0OSdM0JiYmsLm5ic7OTok0NteQo+HJIJ3YACneNCpV\nW1gxm9De3q6IPDbfIEUXsXPe3NxEU1MTqqqq4PV6hZZPNgOnu2kcRZQ2wWAQExMTCIVCQuwyOf9C\nUtoUG7OQrA3h8/kAQFGL/r2EUrGQA+S6DSGOwE4Gkk2wtLSU0M+XA7mxKc2IKMEsBAIBjI2NIRAI\nYGBgALW1tXj22WcTdPZAdgOMJANjfHwclZWVGBoayrs3u5wEkdgvu91uIfyJ+ACQ4kEuepqwCcSO\nvJjYhJ0QCoUwOjqKSCQiif8mRZeYfSD2z+FwGCaTSTL7kM4ivJNxVCahWWTWpby8HEajEd3d3ULs\nssfjwcbGhmAcRRwzSRGx28ZRBBzH5e21M0GyDZHP54NarRbmWkqQolQspIF0YqpzWSxsd+z40Kdz\n586lRJ9TFJWT9kk2A47xltMnTpwQvuSEsWBZVhE5JJnn8Hq96O3tRW1tbcHs3MQg9stlZWWSSXxi\nGrW1tYXp6WlwHAez2Sy0LZxOJ1ZXV9He3o7W1taCfG+ZgDBOExMTOHDgAE6cOCG7a5Rr+ZCBU1I8\n2O12yfPITz7YB/HMglzsMjGO8nq9mJ6elhhHkeIh18ZRBHtlwNHn8+3aNStGlIqFHCDXbYhkC3ow\nGITVaoXX60VPT0/a2QS5KhbIDTKdL6Hb7cbo6CgAyMohKYrC8vIyampqUFFRkRWbsLS0JMxzDA8P\nF3R/WA5y4U/BYBButxsbGxuYn58Hz/PQ6/Xw+/1YWloSTKOK+cZI5ld8Ph+OHj2atMWWDHIDp3Jy\nVxIulonZVqa21du1A3cyjtra2sLs7GyCcZTZbM4JU7ZXZha8Xq8iSoi9ilKxkAbSYRYikUhOzkGO\nWRDvwBsaGnD06NGMQ59y0YYg55jKwiROh2xvb8ehQ4dk5ZDt7e1wOBxYWloCx3GSm3llZWVK75+4\nPYbD4YwWm0IFkSD6/X44nU50dHSgoaFBQmUTZzjxDjrV61YIIOxZdXU1hoaGFDnv7eSu8WZb8TMj\nSrIPgUAALpcLdXV1oGk6pdmHTIyjzGazIsOye4lZMJvNe4Z1UxqlYiEH0Gg0CAQCOTu2eEF3Op2C\nf38hhj6lMzhJ/B+SySHFO7Dm5ma0tLQIN1eiIJiYmEAwGBR2g6R4EE/CcxyH+fl5zMzMoKmpSeL2\nuBfgcrkwNjYGnU4nUXHEU9niXfT6+rrkuhWqZTXDMLDb7dja2kJvb2/K9rmZYjv2wePxwG63CwOI\n4tmH8vLytNkHcUuloaEBzc3NQhsv3dmH3TCOIiimAUcy45SsWCgxC8mxd+6QBYRcSydZlgVN07Db\n7YqGPuXivMULdDJEIhHYbDY4HA50d3dL/B92kkOKKVmSO0+sl91ut9CLJrI1g8GAra0tUBSFU6dO\n7SmZFMuygicEUToku+lTFIWKigpUVFTIXrdkltUWiyVvhZXD4YDVakVFRUXekj3l2Aex1ff6+jom\nJiYAIGH2YbshQJqmYbVa4fF4ZFmuTEKz4rGTcRTxrEjVOEp8bsVSLJBrlmzAsaSESI5SsZAGCmHA\nUaVSgaZpvPLKK6iqqlI89CkXxUKy9gbZSRE6+brrrhMWAKJuIAOM6cgh5ayX3W43ZmZmsLS0JDAo\ndrtdYB7SkR8WIgibQOLSM/GESGZZTVgboiDYbcvqaDSKiYkJrK+vo6urCw0NDQXFdsglV8qxNmVl\nZQmsjUqlgsPhwNjYmKDAkSsqMgnNytY4ishOxcZRpIAQ/82LqQ1B7svbDTiWII9SsZAD5KpY8Pl8\nsFqtYFkWR48eVTwOOVeMiFx7g8ghg8Egjhw5Inkv8TuobJUOxGtCp9NhaGgIJpNJMD+Klx+KzY+K\nYTKaZVlMTk5iZWUFHR0daG5uVmwhFe+iCcTZDUtLS7BarQnZDUpaVpNBV4PBgMHBQcUK41xiO9Ym\nfmZEo9GApmk0NTXh0KFDKUsQdzKOytS2Ws44isxteL1erK6uYmJiQvLZiEajQnFf6CCFjdx7LzEL\n26NULKQJYgK0HZQuFgi9PD8/j8bGRni9XmEXoyRyVSyImQUyjDk9PY2mpiaJHFLOXCmbRYd4Tayt\nrSUspGRHJe7nkp2gw+HA9PQ0eJ5PoOALaQDQ6XTCarVmxSakC71ej7q6Ool7otg0SinLao7jMD09\njYWFBXR0dGzbUikGxLMPXq8XV65cAc/zqKmpgdPpxOLiIoxGY8LsQ6oFay7YByD53Ab5uzMMg8uX\nL0uMo8xmc0GqbXYjF2KvolQs5ABKFgubm5vCgjA0NASj0YjFxUXFnRaB3DMLYjnkmTNnJMOYSpor\nAbFhSZvNJvS3d9qRajSahGlyMQVPBtnEFHxlZWXK8clKguRV5IJNSBcqlUq4Fq2trZI+uNvtzsiy\n2ufzYXR0FCqVas+ZR/E8j4WFBUxNTaG1tVVilsYwjMA+bG5uYmpqSqL0IT+pzmpkwj6Q56diHGU2\nm9HU1ITNzU0cP35cOP9CNI4i2O6+6fP5cPDgwd09oSJCqVhIE7vFLBCToK2tLcnQH3ntaDRaNMUC\nRVGYm5uD0+lEW1tbUjmkEuZKkUgEdrsdLpcLXV1daXtNiM+ZUMlyqZFiCp4slkrlNmwHMZsgTlEs\nFCTrgxPTKLfbjbm5OUSj0QT5oU6nw9zcHGZnZ3Hw4EHJ52QvIBwOC603scskgVarTWq+5Ha7MTU1\nhUAgAKPRmBCapRT7kG5oFnkuMYRKZhw1MzODQCCQN+Mogp2YhVIbIjlKxUIOoFarMzIiAhJDn8RD\nf8D2A4PZIhfH3djYQDAYBEVRGB4ellDl8XLIbK2aV1ZWMDExgf3792N4eFjxXUw8HUvik+UWQfEu\nWomp/UJiE9JFMstqwtrMzMzA7/cLn+2mpiZh0dkrILLg6upqHDlyJKV21nbmS+J2GXHrFBdeSrEP\nO4Vmkcfj73M7GUc5nc5dNY4i2E7m6ff7S22IbVAqFnIAsuOPRqNpLVgejwdjY2MphT7lYoBSyeOK\n5ZBGoxGHDh0SCoX4G1G2bEIwGITNZkMgEEB/f39O5jnkEB+fLF4EifrC7/dL+tBkcDKd90u8NEj6\nZaGxCeki3rJ6cXERk5OTqK6uhslkgtfrxVtvvSWxrCbXLt80droQKzl6e3sFtiVTyJkvkR28x+PB\n9PQ0/H5/SlkhyZCObTXxk+E4DtFoNGPjKK/Xm1PjKIKd2hB7SUqtNErFQppINeKWoqiUi4V0Q5+2\ns3zOBkq0IYh98vj4uCCHvHLlisAekB6pEumQpP87PT2NAwcO4OjRo3k1VxIvgg0NDQCu9aGJ9fLk\n5CQoikrJu4C4Wa6urqKzs1PiP7EXIKbljx8/LthVA9tT8NkUXrsJj8eDkZGRnCo55HbwZFjX4/Fg\na2sLMzMzYFkWFRUVkuHJdHbwcrbVxEWzsbFRmEvaDeMos9mc8axQacAxc5SKhRyAoqiU5hYyDX3K\n5SBiNsf1+/0YGxtDKBSSyCHJcYnESgk5JJGRRqNRHD9+XJIdUUiI70PH5w+IvQvEsw+BQAA2m23P\nsAli8DyP1dVVjI+Po7a2VrbIS0ZjxxdeQOFZVvM8j9nZWczOzqKtrQ0HDx7c1YJGblg3GAwK144w\nXoR9ID9mszkl9oFlWYyPj2N9fR39/f0SlUS2kd3JjKOI8mJpaQk+n09iHEV+UtkoJGMWeJ4vMQs7\noFQs5Ag7FQvZhD7lsg2RSbEglkM2Nzfj5MmTEjkkRVHw+/2gaRparTarQoHjOMzMzGB+fh4tLS1o\na2srGvc4QN4BUKwemJ+fFxQjFRUVqK6uBk3TMBgMe2LYj6Zp2Gw2uN3utFtGcgOAYsXK2tpa1sFP\n2YJEZdM0jdOnTxfEwJx4B08YL3FSKZkfkBs6jWcffD4fRkZGoNVqE9iSTEOzdmIfyMAskesmM44i\nf3c54yiCErOQOUrFQprI1sVRidCnQmpDEOdAILkcct++fZidncXS0pKECiXSw1RBzJVUKhXOnDmz\nZ77YBoMBBoMBGo0GGxsbqKysRHNzM0KhEFwuF+bm5sCybNH374mcdTunwnQgp1ihaVooHgh7sVuW\n1aurq7Db7aivr0dXV1dBF7HJkkpJ+4IYlen1euHaRSIRLC4u4tChQykpVZSM7BYjE+MoUkSwLCvb\nfiGFZyEUd4WKUrGQI8jt/pUKfSoEZoEMbi0vL+8oh2xsbERTU1PCDprYE4t9C+TipokSQJx5sBd2\n2QTkWq6trcnOJogjp8X9e3F4USGGPhEwDIOJiQlsbGygp6cH9fX1OTtPnU6XYCAk7oHnwrJaHG61\nmwO2SmI79sHpdGJ+fl5IwHQ4HIhGo5LZByUju3mel9zflDCOWl9fRygUglqtRllZGXQ6ncQ4yu/3\nCyZsJcijVCykiUyYBZqmMT4+LjgJtra2ZrXY5ZtZ2NjYwNjYGEwmU1pySLKDJnSimAp1OByCkYu4\neCALqdFoxNDQ0J7q3QPA1tYWrFYrysrKkppHiW/kpH8vtg+OD30S513ke3dLCmSTyYShoaFdz98Q\nswotLS0ApG2fbC2rXS4XRkdHhfeXj3CrXEGj0YCiKKyursJsNuPw4cNgWVb43BH1gthwi+zgU/3c\nJWMf4i3f07WtjjeOAmLfmXfeeQdarVYwjmIYBl/5ylfQ29uL2tpahMPhbC4ZAOCRRx7BN77xDayu\nrqKvrw8PP/wwrrvuuqTPf+qpp/C5z30O09PTaG9vx1e+8hXcfPPNWZ+H0igVCzkCKRaIMkDJ0Kfd\nsGWWAzGKcjqd6O7uRmNjoyQdMl1zJTkqlPSgnU4n5ubmBMOX8vJyeL1eqFSqog58IhCzCV1dXZJr\nmQrkQp/EO+ilpSWJ7TL52a1rJ86sKDQlR3zRSiyrSftiYWEBDMNsa1kttqPu7OwsKt+LVCAe0ox/\nf0TyCiQabs3Pz4NhGMG5MRO771zZVut0OqhUKhw4cAB1dXXgeR6bm5u46aab8Nvf/hY+nw+tra04\nePAgBgcHcf311+MjH/lIWtftySefxD333INHHnkE586dw2OPPYYbb7wRVqtVKFbFuHjxIm655RZ8\n+ctfxs0334yf/exn+OAHP4jf/OY3OHv2bFqvnWtQfLEkgBQIOI4DwzA7Pu/tt9+Gx+MBABw+fFjR\n0Ce73Q6O43D48GHFjgnE/OrfeOMNvOc975E8Hi+H7O3tleyg4uWQ5CcTEIXI+Pg4KisrcejQIYl3\ngTjwifwUsnxODg6HAzabDWVlZTh8+HDOwpFCoZBQPLjdbvj9fuh0OgnzkI7+PlV4PB6Mjo5Cq9Wi\nv7+/6NigeMtqj8cDn88n7KCNRiM2NzdBURSOHDmyp+yogdimYHR0FJFIBAMDA2n18cm1I9dNfO3E\nxUM67IMc5GyrxUtZMvbh0qVLaG9vTzD9ev311/Gnf/qnsNvteOONN/Daa68hGAziwQcfTOu8zp49\nixMnTuDRRx8VHuvt7cX73/9+PPDAAwnPv+WWW+D1evHMM88Ij/3BH/wBqqqq8OMf/zit1841SsxC\nmthpUSKhTxsbG6ioqMCZM2cUH6bSaDQIhUKKHhOQZyzEcsijR49K+rHxX9Zs5ZCEufB6vQItSDwJ\niJmNOPAp3regkOh3OYh7952dnWmzCeki3nY5vu1D3P/E9Hs20kOxUiUfkkGlkMyymrAO8/PzUKlU\n4HkeVqt1W/VAsWFzcxNjY2Oorq7GsWPH0r53ia9dPPtAigc55sZisaTlnZAp+yA2jhKDKCEqKytx\n4cIFXLhwIa33DcTaHG+++SY+85nPSB6/cOECXn31Vdl/c/HiRdx7772Sx973vvfh4YcfTvv1c41S\nsaAgSOiTTqdDU1MTOI7LydR1rgOfSJU+MzODmZkZWTlkfDpktuZKS0tLgsX18PBw0gUrXkNOBpnI\n7pnIqMiNqKqqqiBu4g6HA1arFeXl5XmLWpZr+wQCAWEXODExgWAwKEjQSPGVyvCf3+/H6OgoeJ7f\nU0oVApZlsbCwAK/XixMnTmDfvn2yltXZOCfmExzHYXJyEsvLy+jp6RGGHJWAnN03YW5I8RDPPqQb\ndb6TbTXLslhdXQVN00IsOHk+RVHw+XxZM5QOhwMsywrtLYK6ujqsra3J/pu1tbW0np9PlIoFBSAX\n+jQ3Nwe3252T18tlsQDEhu7sdjsoisLZs2clE8JKp0MGAgFYrVZEIhEcPXo0qcV1MogHmUhPUHwT\nJxKwfLUuxGxCV1cXGhoaCma3LTY+Eg+BkWu3srICu90uSNXItRNTyDzPY35+HtPT02hpaUF7e3tR\nLI7pwOFwYGxsTJB8kkI23q1T7JwYn9tQyJLXQCCAkZERANiVqPNkzA3JWfF4PJKoc3HxkI5qRazO\nmpychNPpxPHjx1FeXp4QmvXyyy9ja2tLsfcnBvGaUer5+UKpWEgT8ZK2+fl52dAnJWOq45GrY5MC\n4K233kJHRwcOHjyYs3RIjuOEhMGmpiZ0dHQo1jqIp0Hz1brY3NyEzWZDeXl5XpQAmUBOekgoZBI5\nTQbYTCYTPB4POI6TTVEsdoiHNLu7u3cs9OScEwvdsnp1dRU2mw2NjY3o7OzMW6EXn7MCbK9aEc8+\nbMfe+nw+XLlyRbDcjlerhMNhfO5zn8OPf/xjfPzjH8/qPVRXV0OtViewAhsbGwnsAUF9fX1az88n\nSsVCBqAoCm63e9vQp1zJG4HcMAvr6+uwWq0AgJMnT0rej9JsgsfjEV7r1KlTOdc279S6IMoB0rMk\nP5nK4AqZTUgXKpVKuB6tra1CWNbs7CxWV1eh0WjAMAxGRkYkhdduRw8rDeJUqNFoMrbbzsSymly/\nXFtWR6NR2O12OBwODAwMFKQ3RDLVCmFvlpeXt/XMIMxYa2sr2traEr6DCwsLuP322xEKhfDmm2+i\nu7s7q/PV6XQ4efIknnvuOYn08bnnnsNNN90k+2+Ghobw3HPPSeYWfvWrX2F4eDirc8kFSsVCmiBD\nTYuLi4IZkdyONJfMgpKmTPFySJvNJtCkYjaBUGPZLHosy2J6elpwgRMzF7uJ+NaFeIJbLi2S/KRi\nelSMbEI6IJ4hPp8Px48fx/79+yXMzebmZt4WQCVAwsmmpqZw8ODBlJwK08FOltV2u11iWU2unZKG\nW16vFyMjI9Dr9RgcHCyaz6i4cCUQzz4sLS3BZrNBpVJBrVaDYRi0tbUlyFp5nsezzz6Lu+66C+9/\n//vx7W9/W7HWy3333Ydbb70Vp06dwtDQEL7//e9jYWFBYC1uu+02NDY2CsqIv/qrv8K73vUufO1r\nX8NNN92E//qv/8Lzzz+P3/zmN4qcj5IoFQtpgqIo6PX6HUOfct2GUCIdcnFxERMTE6ipqcH58+eh\n1+sxOTkpsAhiNiHbQsHpdArDn2fPni0ouZncBLd4Byg2PRLvnsWtC4ZhMD4+js3NzaJnE5KBhJ5V\nV1dLevdy9LvcAijeARIJYiFdI5KCGQqFdq2tspNlNdkdiw23yGcv3eFp8p2fnJwULJsL6fpngnj2\nwefz4fLlywCA/fv3Y2lpCVNTUwgGg3jyySdx5swZzM/P49/+7d/wne98B3fccYei1+CWW27B1tYW\nvvSlL2F1dRX9/f14+umn0draCiDGZoiLz+HhYfzkJz/BZz/7WXzuc59De3s7nnzyyYLzWABKPgsZ\ngWEYiSRHDj6fD5cuXcINN9yg+Otne2yxHLKvr09CQb700ks4fPgwKisrFZFDEkp+fX0dHR0dRWte\nIzY9crlccLvdYBgGZrMZOp0OLpcLZrMZfX19RbNTSxUMwwjsU29vb0b91EgkIiyAbrcbXq9XmH4X\nW33nS/K6vr4Om82G6upq9PT05DXqPB7xhlsej0eSVCrOWUn23aJpGmNjY/D7/ejv7y/YlNZsQOYv\nmpubJYO2kUgEdrsd3/3ud3Hp0iXMzs4K7rNDQ0O44YYbcO7cuTyffeGjcL4RewykVZCLydZMj010\n8NvJIdVqNaamplBdXZ314N/6+jrsdjsqKiqSWhkXC+Jtg0mkLen76nQ604mGIwAAIABJREFUOJ1O\n/O53v0u7dVHIIEoAs9mclZ2xXq9HXV2dJDmQTL+73W7Mzc0JqYdi9ibX9snRaBTj4+PY2NhAb2+v\nMJ1fSEjHslpcPBDVitPpxOjoKCwWCwYHB4uiHZQOWJYV3FDl5i90Oh08Hg9eeOEFvOtd7xIKhosX\nLwrmS6ViYWeUmIUMkAqzQNM0XnjhBdxwww2K71LIsd/73vemvJATD3uVSoX+/v6kcshgMIitrS3h\nJk52z1VVVcJNfKebDankXS4Xuru7cxoclC+QBEWz2Yze3l4YDAZJ64LsALeTHRYyiB31+vr6rrRV\nxKmH5PqJlQPiwUmlzsPj8WBkZAQGgwH9/f1FzQjFSw/Jd1er1YKmaTQ0NKCtrS0t2+ViQDAYxJUr\nVwQ3zfgNCcuyeOihh/C1r30NDz74ID7xiU8U9eBtPlEqFjJANBrdcWaA4zj86le/wrvf/W7Fd0cs\ny+K5557D9ddfv6Nmm7QBVlZW0N7enpYckky+k5s3uYGbTCbB8Ejs+87zPFZWVjAxMYHq6mp0d3cX\nnKY8W5CEQYfDge7ubhw4cCDpzZfQx+LrR4ovMftQaNeIxI4bDAb09fXljRESF19kiI1IXrOJmxbL\ndtvb29Ha2rqnFlAg5jVy+fJl0DSNyspKBINBwe5bfO3MZnPRLp5EwdXQ0CAr+9za2sJHP/pR2O12\n/OQnPynIOYBiQqkNkSOQKNZoNKp4sUAW9Wg0uu1CQ75M5eXlOHfunET+lYockqKoBOMZMnzldrux\nuLiIsbEx6HQ6VFRUIBgMgmEY9PX1KZqFUSgQswmpKB3E9LFYdpgsajodx8RcQByO1NHRgZaWlrwu\novHKAbHklQz/hcNhIbRIHJaV7LxDoRBGRkYQjUZx+vTptHIPigUkFbaurg7d3d0CkyVOjHQ6nZid\nnZW0fsg1LPTkTOI2ubKygsOHD8vO0Lzxxhu47bbbMDAwgN/97ndpm72VkIgSs5ABUmEWAOCFF17A\nyZMnc+Ij8Pzzz+Ps2bOytrpiOSSxbs0mHXI7MAwjfHF1Oh2i0WjRZDWkCiIXdDgc6OnpUbStwjCM\nhHnwer0SgxrSusj17s/n8wltqr6+voJSq2wHce+eBI2RwCfx4CSJWh4fH0d9fT26urqK+jMpB2Ii\ntbq6mtL8RXzrx+PxCJbV8aZRhcI+kGKP4zgcOXIkwf+C4zj8/d//PT7/+c/js5/9LD796U8XzLkX\nO0rMQgZIdaHItddCfMESL4e87rrrJMyDuEgAsjdX8vl8sFqtiEajOHnyJKqqqhIMjxYXF4uCek8G\nwiZYLBYMDw8rvuvSarUJUdPiuGQS+UvmRpS2DBZT8rnwFcg14qVzcrtnlmWF70traytaWlr2XKHg\n9/sxMjIClUqFs2fPpmQiRVEUTCYTTCaTwBwyDJM0bEzcvsjH95eEXNXW1koYEwKv14tPfOITuHjx\nIn7xi1/g937v9/ZceymfKDELGYBl2ZSKgFdffRXt7e05se585ZVX0NvbK1C0JMgnEong8OHDGadD\nBgKA3FvTaABiK8GyLGZnZzE/P4/W1takxlTktcPhsCA3jJ97KFTNPWETSN5HvoY0kw3+KdG6CAQC\nggtpX19fzp0084GtrS2Mjo5Cp9PBZDLB7/dLrh9ZAItVtULmhMbHxxMkg0odXxw25vF4hOsnLh5y\naVlN2mOLi4vo7e0VvFDEGBkZwYc//GE0NzfjRz/6UUGqWoodpWIhA3AcB4ZhdnzepUuX0NTUJFi9\nKglSiNTU1GB6ehqzs7NoaWlBR0eHRA4JxBZ3kg65nblSIAD83/+rhs+X+LuKCuCP/ogFTbtgtVqh\nVqvR19eXUbqgeO5BrLkXKy7ySX0SyafFYkFvb2/B9XBpmpYUD6R1QdO10OtjtLv4+hmNQGPjta85\nYaCmpqbQ2NioaC5HoUA8f9HV1YWmpibhcy93/YjhlngBLPRrEo1GhXZjf3//rvXlSeuMFA8ejwcA\nEkyjlJBohsNhjIyMgGEYHD16NMEIj+d5/Mu//As+9alP4Z577sEXvvCFgvLI2EsoFQsZINVi4c03\n30RNTY2gjVYSly5dwr59+7C2tiYs3MnkkKmaK3k8wL//uxoGAyCe3QuHgWCQw4kTdni9S2hvb0dL\nS4tiiznJu3e73XC5XPB4POB5XrJz3o2bN03TsNvtgvX1brMJ6+tAJJL4eno9j+3IKY7jYLf7ceed\nFvh8PDiOBc9DsL2tqKDw4x9HcPCgRnApDAaD6OvrE+Kq9xLEKYr9/f07zl+IVSukiCCJh+LPYCFJ\nK4ns02g0or+/P68FLcdxEvbB7XYLltXiAixd9mtrawsjIyOorq5Gb29vwvc/GAzivvvuw9NPP41/\n/ud/xo033liU7FCxoFSC5RC5mllgGAahUAgzMzPo6upCa2trghySFAoURaU9m2AwXGs5ALFe4MzM\nBrq7gxgaGsooVGc7iPPuDx06JLELdrlcCUFPhIFQsm9KHPyqqqqyMh/K/PWBe+/VwetN/DuZzTwe\neohOWjCoVCrodBYwjB5mMw+DAeA4FtEoi2CQhcvF46WXXsfCQhQ0TcNiseDo0aMZsUKFDJ7nsbS0\nhMnJSSHJNJWCVqxaIcchWSEejwdzc3NCzLl4cDcf7Jc4ErxQZJ8qlSrBsjoSiQisg9iyOt40So4F\n4HkeMzMzmJ+fR3d3tywzOzExgVtvvRXl5eV48803BTvlEnKHUrGQAfI54Li2tgabzQae54WBNAKl\n0yEZhsHy8jI2NgKorm7EsWMHUVaW+xtTvF9+fNDT9PQ0/H6/0HcmxUMmcw9iNqGnpwd1dXV5uflG\nIhS8XgoGAw+xrUEoBHi91FXGYWcS0GDA1X+vBqCGThdjjKqqqsCya6ipqUEkEsHrr78uqAYsFguq\nqqpQUVFRVMONYhA7Y5/Ph2PHjmXFmMhlhUSj0aSDf+IFMJfuiJFIBGNjYwgEAruS1poN9Hp9QtS5\n2LKaJEaKZa8WiwUqlQpjY2MIh8M4ffp0QkHL8zx++tOf4pOf/CTuvPNOfP3rXy+aYeliR6lYyCGU\nLBbC4TCsVitcLhd6enqwtbWVsrlS+uDhdLqwvLyM8vJydHd3we/XgqJyE7m9E5IFPZHiYXl5GVar\nVVYyt93il282QQ5GIxDPmofDmR8vxkIxUKlUOHfunHBjJY5/LpcLLpcLc3NzYFk2QbVSDNbAm5ub\nsFqtwt8xF+es0Wiwb98+oQgRD/6RsDElqPdkIIOaVVVVRWnZvJNlNfFs4Xkeer0ejY2NgkSdtB8i\nkQjuv/9+/PjHP8bjjz+OP/7jP847q/K/CaViIYfQaDSIRCJZHUMsh6ytrRXkkF6vV2ARlJRD0jSD\n8fEVcFwADQ3NsFgqs1qscoV4yaF47sHpdGJmZgY8zyf4PWg0GtA0DZvNJhRe+WITcgmiogiFotDp\nyq+6aV77vdjLQfx8svhNTEwgGAwWtGqF+AqsrKygp6dnWzdNpUFRFMrLy1FeXo6mpiYA0sFdQr1n\na/ctVgJ0d3fvqTRTInutra3F3NwcvF4vWlpahPvb0tISLl68iP/4j/9AX18frFYrgJjhUmdnZ57P\n/n8fSsVCBkj1y0oCnzKFz+fD2NgYIpEIjh07JsgkybEjkYigdMi2SOB5HqurS1hfD0Cj2Yfa2ibw\nvBpud+z3FRUx+WShQjz3AEhjksnNOxKJwGAwIBKJoLy8HKdOnSoa86FUEQ7HKPNQKAiVSg2t1gxA\ndZUVSt7GEGvuSY9YvPiRsKJ02ZtcwefzYWRkBBqNBoODg4rP0WQCnU6XQL2LPTMWFhYEzwxxAZGM\n0SIGRCzL4syZM3vuswpcax8FAgGcOXNG4qhJWq1erxcvvPACHA4HnE4nrr/+egwNDeFP/uRPcPPN\nN+fx7P93oYBv/4UNkoWwHTQaTUpOj/Eguwk5OSQQ+xJpNBrMzc0hFAoJfftMFQN+vx9WqxU0TeOu\nuw7DbCb93mvnLvZZKAbEzz2Qfq/b7UZVVRUikQguXrxYMFbLBKHQ9v+dDEYjYDLxcDrpqzbgZdBq\ntWDZ2OOZxDvEL35y7I24b0/Ym1xS5OIBv0I3kSIDfWL2JhQKCdT7zMyMxDFRPDi5sbEBq9W6Z90m\nAcDtdmNkZAQVFRU4e/ZswucmGo3iBz/4AR5//HF897vfxW233YZgMIg33ngDr776KsKFSHnuYZSk\nkxmCpukdi4W1tTXMzs5iaGgo5eM6nU6MjY3tKIfkOE4w6yGGRzRNp5UQKXbvI4Yue+2mxPO84Juw\nb98+9PT0CH37ZFbL4uu3WzvnbNQQQExK9+tfT4Lj9Ojs7JSEP8X7LCiF+L49kczJSQ6VKMCI7DMU\nCqG/v19YhIsZYsdEwkCQlmJNTQ0aGxtzXoDtNniex8LCAqamppJmkKytreGOO+6Aw+HAk08+iYGB\ngTydbQkEpWIhQ6RSLDgcDthsNlx33XU7Ho9hGIyPj2N1dRUdHR2yckgymyBnriQOKSLFQzAYFG7c\n4oRIILa4kB7g4cOHC3qyOlOIo7J7e3t3dNIU08bkGnIcl+D3kCvTl0x8FjiOE2RmbW1tOHjwYF6Z\nkUgkIikeSFYD+fxZLJaMCjASikasfvei8Y7f78fly5ehUqlQV1eHQCAAj8cjFGBiBqeQZkfSAcMw\nsFqt8Hq9GBgYSCj4eJ7Hyy+/jDvuuAPvec978Nhjj+05iW+xolQsZAiGYYQdQDK43W68/fbbePe7\n3530OWTna7PZUF5ejr6+vm3TIbdzYIwHuXGThY9oxdVqNYLBIJqamtDZ2bkn2YS1tTWMj48nsAnp\nHke8c3a5XILcS1yA5UtFQSy+eZ5Hf39/Qd5UxVkN5DqSAkwsmUu2c45GoxgfH8fGxkbShMFiB8/z\nWF5exvj4OFpbW9HW1iYppsQFmMfjERxP4z0LCrUdQ+D1enHlyhWYTCb09fUlfCdZlsU3v/lNfPOb\n38TXv/51/MVf/EXBvKeDBw9ifn4+4fG7774b3/ve9/JwRruPUrGQIVIpFvx+Py5evIj3vve9sr8X\nyyGJ53mu0iGBa6FIFEVBp9MhEAhAo9FIFj6S0FesiEQisNls8Hg8gtJBSYj9HkgBZjQahR1fVVVV\nTuYe1teBcPjaZ2N5efkqm3AAZ860FsxNdSek07rweDwYHR2F0WhEX19fQTkoKgWy03a73RgYGEjJ\nH0I8O0KKMHHUNCkiCkEKDFwzy5qYmEjKfjkcDtx1112YnJzET37yE5w5cyZPZyuPzc1NyfzZ6Ogo\n3vve9+LXv/41fv/3fz9/J7aLKBULGSKVYiEcDuPFF1/E+973voSWwcLCAiYmJlBXV5ew842XQ6bD\nJiQ714mJCayvr6Ozs1Pwyd/JZrmqqiptqVe+QNgEu92O6urqq1LB1NmErS2ApuV/p9MByWz3GYaR\n7Jo9Ho+iEdNra8DSEoUvflELn48Cx7EIBkMAOFRWlmH/fjW+/e3t5xkKHXKtC5VKBZZlUVNTg0OH\nDhW1YVQykAE/wihmai6ULGxMXMTmKywrGo0KG6JkxdClS5dw++2349ixY/jhD39YFBbk99xzD/7n\nf/4Hk5OTRb25SgelYiFDEMOQnZ7z/PPP44YbbhB6rGI5ZF9fn0QOmQs2gQz3mc1m9PT0SAbf4kHk\nhqRt4XK5wDCMpFdaiEY9Yjaht7dXmN5PFVtbwAMPaOHxyF9ri4XH//k/TNKCQQzx3AP5YVk24Rqm\n0nNfWwP+4i902NykMDVFgaI48DwLilLBYFCjp4cDz1N47DEara1742scDAYxMjICmqZRXV0tqAeS\neWYUI3iex9zcHGZmZpIO+GULuSKWGCOJw55yeQ19Ph+uXLkCg8Egm1/BcRweeeQRfPGLX8TnP/95\nfOpTnyqKgpCmaTQ0NOC+++7D/fffn+/T2TUU57etSEB25NFoFBRFYWZmBrOzs2htbU1I+iOzCWSA\nMdtCIRwOY3x8HC6XK+VQJLHcsKWlRRiaJMXD+Pi4QBmTtkVVVVXe6M54NmFoaCij3RlNAx4PBaOR\nR7xcPxiM/S4Z6xAPObmcmHa32+0IhULC3MN2IUXhcMwCWqfjAPBQqzno9RrwvAosC2i1ydmQYkPM\n52MVdrsdDQ0NklkaOc8M8exIIQY9JUMkEsHo6ChCoRBOnz4t8RVQElqtFtXV1cJmhOM4yTUU2y2L\nZx+UUq7Ez2DEH9Pj8eDuu+/G66+/jmeeeQbvete7sn7N3cLPf/5zuN1u3HHHHfk+lV1FqVjIEKl8\noSiKglqtxtbWFmZmZqBWqzE4OJhgPEKYhFTTIbcD6WdPTk6iuroaw8PDGdObFEWhrKwMZWVlglFP\nJBIRiofZ2Vkh+U4sN9wNr4JwOAybzQav14u+vr602QQ5lJUlWi0DqXsdyEHO6Y/Y3BKbZTJ4Kr6G\nsSheCgzDIBr1QaOphNGohVZLgWGADOw7dgWrq5Ts9TIagQMH5NkPhmEER82BgQHBlZMg3jMDkM6O\nzM3Nwe/3Q6/XC4teVVUVysvLC4oidjgcGB0dRXV1NY4ePbqrzIhKpYLZbIbZbJbYLZNruLCwgLGx\nMeh0OgmDk277h2VZ2Gw2OBwOHD16VDY2+/Lly/jwhz+MQ4cO4a233iq6odXHH38cN954IxoaGvJ9\nKruKUrGQQzAMA57nMTY2hs7Ozh3lkNkWCsFgEFarVdChx990lYBer0d9fT3q6+sBSL0KVlZWYLPZ\nJFI5pW/aZAc6Pj6OmpoaDA8Pp9QWcbvld+Hb1VGxaO7Y/zqd1xwstVogG4k/sbklN8loNCoUD0TF\noVKpsLFhQjA4gIoKAzQaNQpo3ZPF6iqFO+/UwedL/F1FBfDEE3RCweB0OjE6OoqKioq0mCGDwSD5\nHJJrSIKepqamAKAgWhccx2FychLLy8vo6ekpmEUm/hqKlSti0634wclkfyO/348rV65Aq9VicHAw\ngenheR7/9E//hL/+67/Gfffdh7/5m78pulbS/Pw8nn/+efz0pz/N96nsOorrL1UkIHJIq9UKiqJw\n+PBhScyq0umQHMdhYWEB09PTaGxsxLFjx3btS5gso8Hlcgk3bYqihGRDcbpcusiUTXC7gUce0cDt\nTrzGlZU8/viPEy25w2HgzTdV8Hpj7YDvf18rOFhaLDw+9rFoVgWDGBqNBvv37xd2YZubmxgdHYVG\no7maLxIGTevAcYBGQ4FlVWBZFSIRFFQBEQoBPh+g1wMGw7WiIBym4PNJGRqO4zA1NYWlpSXJ0G2m\niL+Gyey+5VQXuQSZweB5HmfPnr3KGBUm1Gq1bFgWKcJIXojY9dRiscBkMglpuMTcLf77HQgEcO+9\n9+JXv/oVnnrqKVy4cKGgWJ9U8cQTT6C2thZ/+Id/mO9T2XWUioUMkeyDHgqFBClUb28v5ubmJL1X\npQcYycAkx3E4efJk3l3t4jMaxL1Sl8uFhYUFQeYlpt23K24yZRMIaBpwuxNnEoLB2OMMA0QigMMB\nBAKx3xE2IbZA86is5FFRcW2GgWEAl2t7BcXVS5AyotGooFrp6uoCTTfCZNKjrIzH+joFmuZB0zyi\nURYsy8PhCKCujofX60E4bC6Ynr3BwMdZg/MSsyniDwEgZwtoKq0L0v6Jt1pWahGLn8EohuE9McQt\nNHFeCCkeSFgW2fTU19dj//79CWZ1drsdt956K6qqqvDmm28Kf49iA8dxeOKJJ3D77bcXHSOiBP73\nveMcIV4OSdIhl5eXEY1GFWcTWJbFzMwMFhYW0NraikOHDhWkxDG+VypON3S5XAkDf/FGR2I2IdvW\nitxMQigU+5mZoeDzXbuZs2ysGFCpYv12rfbav3W5gKkp4Kc/1cLrlR5PrQYMBsBiAf7yL5mUCwaX\ny4WxsTEYDAYMDg7CaDRibi72+eA44PBhHjElLYVwWI1wGPjMZ4LQat1YWnLBbh8X+s1msxk1NRY0\nNaU+O7K8TCEYlP9dWZkydtEkQXVycjLpDjSX2K51sbGxIcjg4lsX6X6viJHU5uZmztqB+YJOpxOY\nxGAwiMuXL4PnedTW1iIQCGBkZAQMw+Bb3/oW6urqsH//fjzxxBP42Mc+hgcffLDglFTp4Pnnn8fC\nwgL+7M/+LN+nkheUigUF4PP5MDo6Cpqmcfz48YR0SIZhhEIhW88EILawWK1WaDQanDlzpiCd+5JB\nLt2Q7PhcLpcQrlNWViZE1e7fvz9jpcNOCIdjbMKhQxw0mpi1MnncalVDp4sVAOSlQyHgd79TYWND\nC7tdBbVamsZpMADHjrGCgsLpjLEW8dDrgX37YkUfiSAWy+jW1mJMh1YLiaRTpYo9VlvLo7OzEv/0\nTzXweACO48EwNCKRCGiahlbrxS23vI3mZpNk4ZNbnJeXKXzwgzoEAvKfS5OJx7//O51xwRCJUAiF\nePy//zeD8nIXOjtPgectWFkBmpryJ/mMb13EKwaWlpZA07REdWGxWLZlcIhcUK/Xy/bt9wpImzWe\nNeF5HuFwGJOTk/j5z3+OX/7ylwiFQvjP//xPrKysYHh4GLfffnvOVCC5xIULF3a0+N/LKBULGYKY\nGk1PT2Nubi6pHFKj0WBhYQGhUEig5zOtrqPRKCYnJ7G6uoq2tja0tLQUHbUph/gdH2mtEJrY4XDg\ntddekzAPStDFZOFfX9difFwFvT62EAMAwwBOJ4WWFg4q1bXXiUZji3+sLx+j3EkhwTCx9oROR1of\nwBNPaIWYbzEqK4G773ZheXkEKpUKZ8+eFSKI19aAT34yFipF07ECgaC8nMcXv8iguTl20/J4YsxH\nrL2iA6BDMEghGOTR21sOnc4pTLvHu/wRz4xgEAgEKOh0POLXtlgxlZx1kEPMaTJ2fpEIhXfeoRCN\nAg880ImyMo3wdysrA37600heCwYx5BQDJG9FnBJJzI4IA0H+boQ1OXjwoKxccC+ADGuurKzI2m/H\nCt01/OhHPwLP83j99ddRX1+P119/Hb/97W/x9NNP484778zT2ZeQDUrFQoYIhUL47W9/C41Gs60c\nsr29HS6XCy6XC1NTUwgEAoJPQTrZApubm7DZbDCZTBgcHJTkR+wV8DyPlZUVTExMoLa2FidPnrwa\ns8zK0sXxTpPpFk7xC79ef23h53kgGo0t1mp1jH1Qqa4N6RkMPLTaWGFw7c/Hg2GuLRCkYIgt5tcW\nxGAQWFz049Kld3DixIGEmOVIJOavoNfzkjZGMAj4/RR4PqY8WF0FNjYomM08olFKiKHmOF7o2dfX\nV6C1tVXS/hEPq5WXl8PjqUM02gWTiYLRmHgNU/VyMBpjqgefL/YeeB7w+WhEozpoNBT27dNcLcZ4\nRCK4WtSkdux8wWg0wmg04sCBAwCSty6I42RHR0fWw5qFilAohCtXrgjDmvH3IJ7n8Ytf/AIf+9jH\ncMstt+Dhhx8WmJXrr78e119/fT5OuwSFUCoWMoTRaERHR8e2eQ4URUGv1+PAgQPCzYamaaF4mJ2d\nhc/nQ1lZmURqKHZZpGkadrsdW1tb6OrqQkNDw568EZGcDL/fj4GBgYRWjnhKm+M4+Hw+YeGbn5+X\nuCRWVVXJyuTiF6btFv5oFFCrebBsbFcsHoTU6aS7fTEYBvD7Y/+7sRFbDPV6Hmp1bCdN0wxWV7cQ\nCqlx9OhRtLcnbyGVlUEyKEjTwPw8hS99SYuxsZgaIhSKKSLUasBsjv2vSgUMDkqNGOTaPzRNw+12\n4513gmAYBn5/BCwbo+fV6pgSg+dT79cfOMDjiSdohEKxIcbYsKYJDz88ALOZRzzzzDApH7pgEN+6\ncDqdglzQYrFgfn4ek5OTCYZRhZLTkCmIQqe+vh5dXV0JcxwMw+ALX/gCHn/8cTzyyCP40Ic+tCfv\nU/+bUSoWMgRFURK9dKoDjDqdDnV1dQJ9J/YpWFpagtVqhV6vR2VlJSiKwubmJqqqqjA8PFz0Nxw5\niE2kamtrMTAwsGObhtjWWiwWYdcsdkm0Wq2CTK6qqgoq1T6Ul9fC79dI5HvhcPKFX6MBamqA48dZ\nMAyFj36UQW0tsLEBPPywVigqyIJHdv0rKxQ2NzWgKGB8nMLSkgrl5TzKy4GTJ10Ihbag11tQU7MP\nFRWJks1kILMV4fC1nTtF8aCoWLHA87GihOcBmqbAsjvfqHU6HWpra3HoEAWjUY+KCj20WhbRKAOG\nYRAKhUDTKoTDeiwtLaO6uiwhKyTehIn8PdfWZnHqVANo+hAefZSCVlsYrQalwPM8ZmZmMDc3h66u\nLoFNID37ZK2LfOY0ZAKO44SZGhJ2F4+VlRXcfvvtcLlcuHjxIvr6+vJwpsmxvLyMT3/603jmmWcQ\nCoXQ1dWFxx9/HCdPnsz3qRUVSsVCFqAoSnBezFQOKedTsLGxgenpaYTDYQAxa1S73S60LgrNmS5T\nhEIh2Gw2WTYhHSRzSSROk1tbkxgYGIVWa5L44ns8Bjz0kFbo04fDFBgmtqjRdKwQCIcpaLWxIKma\nmphKwu8HXC4Kfn+s7RAOA2trMQtmno/9XqUCHA41OA4wGFi43RG4XF4cPFgPn88Ih4PC6ioFcRaZ\nTscjfnDe44kVIpOTKvj9gM9H4e231WBZXF2cYq8VKxp4aDSZW0DHChANAA00mhhLwbIc1GruquHO\nFBiGEWSvkch+3HtvHfz+a8NtoVAIPF+H6uoW/Nu/cYimXg8VDcLhsJBfET9gTFFUQutCnNMgNt2K\nDxsrNDUTeZ/RaFRW4srzPF588UXceeeduHDhAn75y18W3LC1y+XCuXPn8O53vxvPPPMMamtrMT09\nnXeJeTGiVCxkAaXlkGRXNjU1hfr6esEfX87kiNDtVVVVRZfIJ2YT6urqUmIT0oXBYEho/1wLd5rH\n+roXfn8FAoE+RKM6RKNlWF3VCDtyno/9cJwK+/fHdvRATDL5wgtqMMy158TmG669tk4Xm33guFib\nIBiMwGBQw2JpgM+nwq9/rUYoROELX6AkA4UWC/DVr15b6X0+4I26McD5AAAgAElEQVQ31IhGIbwe\nIG/1zPPXnrNDGGoCYu0OHoGAXAaGGpWVKpw40YOGhi7JwJ/dPo/FRTM0mth8Bcuy0Go1UKlM8Hgo\nhELXZCDxihA5hUgxYGNjA1arFTU1NThx4kRKC7xcToO4jbawsCAUYeICIhfqn1SxtbWFkZER1NTU\noKenJ+F9siyLr3/963jooYfwzW9+Ex/96EcL8h70ta99Dc3NzXjiiSeExw4ePJi/EypilIqFDHHx\n4kV89atfxfDwMM6fP5+117vf74fVagVN0zh27JgkppXcPA4dOiTIu8jcw9zcHFiWlSgFMtGG7xaI\naVUwGMyKTUgXhHInro8sy2JuzotnnqGwtRWGyRSETlcBjUYFnU4FtVqNsjIVjh7loNFQEjMnnQ4w\nGmNzDm43lbB7ZpjYjl+jiQJQQ6PRAdDA42HB80AoFDOIqq6+NlAZDFLweGItBCDGDni92/f1SV1K\niohwOMYGMEyMZfB4gKsCk23R2BiTRu7ks7C8rEIwaAJgglbbCIZRYWVFd3WgUno+FAW8/fYG+vrK\nUFamRzBIJbyXsjIkBHcVKliWFZRIPT09snR8qpBro4mLsOnp6by1Lkh7ZX5+Ht3d3RLnWYLNzU18\n5CMfwezsLF588UWcOnUqp+eUDf77v/8b73vf+/CBD3wAL730EhobG3H33XfjrrvuyvepFR1KEdUZ\nYmFhAf/6r/+Kl19+GRcvXgQADA4O4vz58zh37hxOnDiR0s6A4zjMzs5ibm5OMKpJZ6En/XpSPIhj\npVN1SNwNEDZhYmJCYE0KwaCF+CA4HMB3v8tDrw9BpQogFAqDojjodAbQdDnuvZdGe3sFXntNgz/9\nUwPKy2MLPWklBIPXbuJqNQ+K4qHXc+A4Nd71LhZqNYX772fA88AXvqBFdTUPUT0Ivz8m1XzoIQYu\nF4+bbjLA74/NQSQD2cgRdsNs5qFSxViOnh4eTU08/u7vaCiR07O8TOGWW3SS8/H7eayuxk6ComKy\nU4qKMR8cBzz44Bh6e+ewuWmAXh9jwMxmMyoqyqFSqVFWll+fhVRBzIYoisLAwMCuKJHILBNpX3g8\nHqjVaolhlNKtC5KIGQ6HceTIEdmWwsWLF3H77bfj9OnT+Md//EfBqbVQQdQY9913Hz7wgQ/g9ddf\nxz333IPHHnsMt912W57PrrhQYhYyREtLC+6//37cf//9iEajePvtt/HSSy/hlVdewcMPP4xwOIwz\nZ87g3LlzOH/+PE6fPp0Q/+pwODA5OQkAOHXqFCwWS9rnIe7XNzc3J8RK2+12SRQtKSB2k+IUswnJ\nkuh2C1tbyQOlqqp02LdPi/JyM3ieB03T2NwMYX2dwcTEBBYXfZidbQTLDlyl+lUAKMmQYQz81cfV\nAGJ/b4MBqKuLPcdg2D7AymKh0NnJIxjkMTISC5CKRhNbDOT1xOU+UUaUl/PweqmrNsvZL8hkgFOv\n56HXA5FIGIEAD+BaH5uiYgUMKV46Ojrw7ncfEpgwt9sJt3sGHg99VWpciY2N/FPuySCOzW5qakJH\nR8euUe3xs0y5bl04nU6MjIxg3759siwpx3H4zne+g7/927/Fl770Jdx7770F2XaIB8dxOHXqFL76\n1a8CAI4fP46xsTE8+uijpWIhTZSKBQWg0Whw+vRpnD59Gp/61KfAsizGxsbw4osv4pVXXsEPfvAD\nuFwunDp1CufOncPJkyfx85//HFarFT/60Y8kaZTZQi5WWjzsJ/Z6EBcPuXCa43keS0tLmJycRH19\n/a7H8sZjawt48EGtxBGRQKeLyRsJiOy1slIPjqNw9mwlKipCCAZjo/80HXPljEbLrhYKapAigedV\nV+cYrqkTGht56HTSjITtoNNd26lrNLEiIX4WIZ4T9PspQTqpVif+XglotRyi0QBUKh5mczlWVrZ/\nvjijgdh9y30eTSaTZNEzGo15HeKNRqOw2WzY2trCkSNHdq1dlgw7tS7IdRSHPKUSF8/zPObm5jAz\nMyO0HeKf73a78fGPfxxvv/02nn32WZw/fz7Xb1cxHDhwAIcPH5Y81tvbi6eeeipPZ1S8KBULOYBa\nrcaRI0dw5MgR/OVf/iU4jsPExAReeuklPPnkk3jooYdQX1+P1tZW/MM//APOnz+P4eFhWCyWnNwg\nkw37kZkHn88Ho9EoDExWVVUlsCDpopDYBAKajlknywVKuVwULBY+oW8v/m+j0Yh9+4xQqzUwGNTQ\n6Xh4PNRVTw1eWJwpKub6aDDwMJt5/PVfMzh8mEd1NbC8TI4r3fGL2xhykDIX8oh5LfCCJbTPBywu\nUrIDkQYDj3Ta7jzPIxqNwu8PwGzWwGg0wuVSiX5/rZjZ7jzFagEiPRaHExH5sDjmnLgk7tZO1uPx\nYGRkBEajEUNDQwUpWRZvCsh1jI+Lt9vtUKvVCaoLch1pmsbo6CiCwSBOnz4ta8H8zjvv4MMf/jA6\nOzvx5v9v78zDoqr7PnwP27CDuwgiiojKYrmA4pJmrpWmaVZmbuWS+ppZT7mWuZaPZWZpaUVZLqVp\nmrlkPYiUuCYgIAjuC6KybzPMzO/9g85pBgZDZffc1zVXMXM48zsjc87nfJfP98SJMk96rS507dqV\nhIQEk+cSExNp1qxZFa2o5qKIhUrAwsICT09PIiMjOX78OCtXrqRfv34cOnSI8PBwZs2axblz5/D3\n95fTFl27dqV+/foVIh6KF/tJrV3p6enyydrGxsbEZbKsxVXG0QQ3N7cqjyaYw9xAqcxMcHISFBSo\n5M4HCRcXUSJtoNFAYaH4u5hQhVrN362TgpYtNVhZaXnqqTN4eGhwdrYmJ8cVa+s6WFk54eJiTWam\nZIts/D5FEY7izwtRdPG3sCgqYrSwKLowOzoWFVlKdQQWFsjr0GggLk7F669bY+5a5+wMn32mkQVD\nVBRkZpq/GDs4FHLtWhKFhT44O9tjaWlFQQF/t5n+s1apVgGKxI00Z+PfMB5OVLSfojHnGRkZ3Lp1\ni+TkZIQQJUy3yruIVxoGl5SURIsWLfDy8qpRLcrmUhfS5yhN2tTr9Tg7O8s26q6urgQHB5eoH5Im\nLM6aNYs33niDuXPnVtui6TsxY8YMQkJCWLJkCc888wxHjx7l888/5/PPP6/qpdU4qtdZvBZja2tL\no0aNiI2NlUe0tmzZkrFjx8rFf1LNw6JFizhz5gytW7cmJCSErl270r17dxO3yPKkeGuXZK+cnp7O\njRs3SEhIMBk97erqipOTU4m15OfnExsbS35+frWJJpQVGxsYPVpndkqkjU3RLAcouqDb2wuysw3o\ndAaEsEKIfz4HKyuoW9eGpk1tGDcuALU6U47inD9/HoPBwLPPNsDW1kX+HKWTsOSzcPly0b70esnr\noOhn6UIs3WDb2xe9n7kuBoOh6PeKW0ZDUTtnVpZKnuEQFQWPP25ntp1RCIGlpQVz5qixs7PDYICE\nBAsTYVAcO7uibpHmzc2//m8Y/601b94cIYTJmPOrV6+aDHgqjzoc6S47Nze3Wox6Lw+MvRwA2fI7\nOTmZlJQUbGxsuHXrFseOHcPFxYXY2Fhat26Nl5cXM2bM4Pfff2fHjh307t27RokmYzp16sT27duZ\nNWsW7777Ls2bN2flypWMHDmyqpdW41C6IaohQghSU1M5dOgQBw8eJCIigujoaLy8vOSURffu3WnW\nrFmlfImlOxQpz5zx92Qk4/BmdnY2SUlJuLm54ePjU+2iCQDXr8Pbb9tQr54wiSzk5MDt2yoWLND+\na2g+JyeHn346R06OJc2bN0ertZfbHaHojr1166ILf/GIbfGLXkZGBlqt1qRIrU6dOqSnWzNzpg2Z\nmSpyc/8RCxoNXLigwtNTcOXKP+2ct2+r5NC/q2vRKOuWLQ3ExRW1fhZPt+fmFqVdvvpKS/PmgvBw\nC55+Wo2VlTCZoKnX69FqBWDFqlVa3n/fBihqoTRulZSEg5dXUfpl8WItrVsLmjWrmFNLcZfEjIwM\neVKp8edY1rqHtLQ09u5Nxtq6Dl5ezU3+dp2coGXL2nGKLCwslAe0BQQE4OrqapICmjx5MseOHUOt\nVqNWq3nllVcYOHAgHTp0qJYFqAqVS/U7oyugUqlo1KgRw4YNY9iwYQghyMjIkMXDl19+ydSpU3Fz\nc6Nr167yw3hUbHli7g5Fqsw2DhM7OTnJY6Wrs9fDneoSSkMIwcWLF0lOTiYoyBNvb2+jz7psFxPj\nYj+pc8W42O/s2bPk5eXh4ODApEkNsLUt8syQcuZXrqh4801rHB0FKSmqv10cix4GQ1G6QqMp+lmj\n+afYsawUjeguOtbCwsK/XRytKSgoSrM4OgrS0ore18Lin31bWBRFX+rWhYICgY9PxQkFKN0l0Thf\nHx8fj7W1tYmgLW5eZjAYOHfuHJGRt5k4sVep7xcVlV/jBYNUh+Hg4EBwcLB88ZdSQPXr1+ell14i\nPj6eJ598kjZt2hAZGcmaNWvIzc3lxIkTJQoFFR4sFLFQA1CpVNSpU4dBgwYxaNAg+Q71zz//lIsm\nX3/9dVxdXWWTqG7dutGmTZsKuWBLFz3p5Ozu7k6TJk3Izs42CRNLtsBSjrmqfRVsbIrqD4rcBU1f\nM1eXIJGXl0dsbCwajaZcQ9SlFftJ4iE9PZnz54vGdBf1s9fHwsIDCwsLrK3/MWyytxfo9UV3+M2a\nCerVE0yYoOO998zXK9yJog4PHZaWllhZWcmpiQYNYMsWLQkJKt580wYnJ4HRvDMsLYumXRavt6gs\nzNmmS/n6tLQ0zp07Z1L3YG9vz6VLl9Dr9bRo0f6O+87OrowjqBikGqLExES8vb3NRiMLCgr4z3/+\nw48//khoaCiDBg0yGY6XmJhIixYtqmL5CtUIRSzUQKSLdb9+/ejXr5/cRnXkyBEOHjzI7t27mTdv\nHra2toSEhMhpi8DAwHJJDxhfPI3dJl1cXPDw8DC5Y05PT+fMmTPk5+fj5ORkUvdQ2aHNevXgrbcK\nS/VZKF5iYWwk5ebmVmZ73/uh+KAxaSRyUc3DLbKzXdFqDXh4WFFYaI2FhRWWlpZotUVWza++qqNV\nKwOurkVFkcVFEZh/TnovlcqAtbW12QiVu7v4e6S3wN5eUGxUALm593v05Ydx3QOYmpelpKRw7tw5\nAJycnEhJSQXq3mFvNROdTkdcXBwZGRm0b9/erIFScnIyo0ePxtLSkuPHj5cQBSqVCl9f38paskI1\nRhELtQCpjapXr1706lUUTtVoNBw/fpyDBw8SHh7OsmXLgCKXSSltcbe5SCEEly9fJikpiSZNmpR6\n8TR3xyzlmNPT02U7WwcHB5PR3BXh9VCcstZcGo/MrspiTeORyI6O0LSpDenpevLy9CQn28j1DJIh\n0ttvW1CnjhWffKLF2VkqZCy5X2fnovZJgMzMDAyG+uj1FlhZWZnYMpc2CMrcPs09V12Q/iYvX75M\nTk4OgYGBODs7k5GRwfXrNXRQxR3Izs4mOjoaW1tbOnfuXOJ7LoRg165dTJ48meeee44PP/ywWrWI\nvvPOOyxYsMDkuUaNGpGSklJFK1JQxEItRa1Wy6IAkF0mw8PDCQ8P56OPPqKgoIBOnTrJaQtzLpMS\npUUTyoqtrS2NGzem8d/DCoy9Hi5dusTp06dlr4e7LVArb1JSUoiPj6dBgwZ06dKlytMnEo0bw9q1\nWgoKVFy4YMHkyRZYWwusrfXo9QaE0FNYqOfmTQvOn4/lrbccUatdcXZ2wsrK9BhsbQUNG+qJj08k\nNTUXtboBhYUWZi/4ajW4uBS1PtjZFRX9ZWebFyFOTpikJ6oLOTk5xMTEYGlpSefOnbH7e5F2dnZ4\ned35b+zs2bPUrWtttu6huiGE4Nq1ayQkJNCsWTNatGhR4juk1WqZP38+oaGhrF27lueee65adjv4\n+flx4MAB+efqWgP1oKCIhQcEY5fJmTNnyi6TUuShuMtkt27dCA4Oxs7OjmXLluHu7k6XLl3KLRRf\n3OtBp9OVKFCzsbExma5Z0YN0CgsLiY+PJy0tjbZt28qpgOpEkdYq8newti6KENjZWQKWgDV5eZCV\nJWjcuDEuLqlkZFwjLS2vhGOnVqslMjIGGxsbnn/en44dNaX6LLi4GGjXruj/3dwEX36pLTWVYWdX\ntE11wTiV5OnpSYsWLe76Yu/o6Eha2nXOnTuHwWAo4fdQXTp/9Hq97DpZWjTs6tWrjB49mqysLI4c\nOUKbNm2qYKVlw8rKSr65UKh6qsdfuUKlY+wyOW3aNBOXyUOHDjFt2jSuXr1Kw4YNMRgMzJgxg0aN\nGlXYXZWVlZVZr4eMjAxSU1NJTEyU3egk8VCern63bt0iNjYWZ2fnauvaVxaKuiMsaNiwIT4+RcV+\nGo2mhGMnFOXr3dzcMBgMBAYKVKqyzbauTmLgTkjiLz09/Y6pJDPzkkxo1cqNli0by3UPkqiNi4sz\nO3elKv52cnJyiI6OxtramuDg4BIpPSEEv//+O+PGjWPgwIF88sknOBZ3JqtmnD17liZNmqBWqwkO\nDmbJkiVKoWUVovgsKJRAr9fz0UcfMW/ePEJCQvDw8OCPP/4gOTlZdpmUog8V5TJZHGmQjlQ0mZGR\ngRDCRDwYW9mWFZ1OR2JiIikpKfj6+tKkSZNqGZItztmzKp5+Wo2zs2lXgmS4tG2bBh8f06+2sWmW\np6cnhYWFpKenk5WVhZWVlckFz5zpVk0iIyNDbhX09/f/19qcpCSV2a6Hf/NZKO73IFmnV+Zo6evX\nrxMfHy9PrS3+HdDpdCxbtoxVq1bx4Ycf8tJLL1X7f9s9e/aQl5dHq1atuHHjhmxUFxsbW+H1Q0KI\nav/5VAWKWFAowdatW5k1axZffvkl3bt3B/4J50o1D4cOHSI+Ph5fX18T8VBZF1upfdRYPOh0uhKj\nue+UMklPTyc2NhZbW1v8/PzkPHZNQBIL0hRICY2myGOhuFiQ6jAaNmyIr6+vSejcuM0wPT2dzMxM\nWYhJAuJexiFfvKgy2yHh4ECFGjZJg5FKaxWsSCTrdEk8SKOlS5vPcD/o9XoSEhJITU3Fz89Pbhs1\nJjU1lXHjxnH58mW2bNlC+/Z3bhOtruTm5uLt7c1//vMfXnvttXLff1ZWFvb29ibfi4KCAiwtLbG2\ntlYEBIpYUDCDwWCgoKAAe+NpS8W4k8uksXioLH99ycr2H4+CdDQajez1IJ2ora2t0ev1JCcnc/ny\nZVq2bImnp2eNOxFcvapi2DAbcnNLrtvBQbB1qxZ396LhT2fOnOHWrVu0bdu2TIOAzAmxwsJCOVdv\n/FmWxsWLKvr2VZv1XbC1Fezfr7lnwXDxooqcnJLP29hoycqKJj8/n4CAgHsa+V7eFJ/PkJGRgV6v\nN/ks78WDJDc3l+joaCwtLQkICCghdIUQ/Pnnn4wZM4bOnTvzxRdf1HgL6z59+tCyZUvWrFlTbvsU\nQnDixAkGDhzIli1b5G6ylStXsnfvXuzs7JgzZw4dOnSoceeI8kYRCwrlgrHLpBR5OHnyJG5ubrJR\nVEW6TJojPz/fRDzk5eVhb2+PVqvF2toaPz8/s73nNYWrV1Vm3Sft7Ys8EaRQvL29PX5+fvfcmioJ\nMelil56eTn5+Po6OjibdK8a5+rg4FQMG2GJtbWohrdNBYaGKPXsKaNv27k89Fy+qePRRdYlR30IY\nsLAoZP36eHr39q42RYfFKS5qMzIyZA8SYyF2p3+rGzduEBcXR5MmTcx+nwwGA6tWrWLx4sUsXryY\n//u//6vWHRxlQaPR4O3tzYQJE5g/f36579/f3x9XV1e++eYbNm/ezJo1a3j++eeJiIggISGB0NBQ\nBgwY8EB3ZChiQaFCkO5ODx8+TFhYGBERERw9elR2mZSGY1WUy2RxDAYDSUlJXLp0CScnJwwGg+z1\nYFz3UBleDxWNZGN88eLFCoucaDQaEyGWk5Nj0vp640Z9hgxxwc6OEmmS/Px7FwuxsSr69bM1mWOh\n0+nkGRb792vw9y+fY6wsCgoKZOMtqe5Bcu00rnuQ3BSvX7+On5+f2ShReno6EydOJDo6ms2bNxMS\nElIFR3T/vP766zz55JN4enqSmprKokWLOHjwIDExMfc9Xto4pVBQUICtrS03b96kRYsWvPTSSwgh\neO655wgODgbgySef5Nq1a6xZs4agoKD7PraaiiIWFCoFyWXy6NGjhIWFcejQIY4cOYJarZZdJrt1\n61YhI61zc3OJjY1Fp9Ph5+cnh6eleQJSuD07Oxu1Wm3iMmlvb1+jwo95eXnExMRgMBjw9/fH6d9K\n/csJ49kMRRENA3PnhmBnJ1CrLbC0tMDCwqLMYuHKFfNRkytXVLz4ohpbW4G1tUAr23HaoNFYsG9f\nAX5+NfuUJrl2GteQSJEBCwsLfH19adiwYYlowYkTJxg1ahRt2rRhw4YNcmdRTeTZZ58lPDycW7du\n0aBBAzp37szChQsrdD7FgQMH6Nu3L87OzkREROD/t+qUjNk6derEsmXL8PLyqrA1VGcUsaBQZUgu\nk1LR5OHDhxFCEBwcLKct7mfinbHjpLu7Oy1btrxjFMPYWtm4S8BYPDg6OlZL8WBsxlOWY61oTp8W\nDBxoi42NHktLPQZDkdWkXm+FVmvF1q23CQpyMBsev3JFxdNPm6/HsLQU3Lxpga2tDiiUC9C0Wigo\nUNUKsVCcGzduEBsbi6OjIzY2NnLdQ3Z2Nr/99hvdunUjJSWFRYsWMWvWLGbNmvVAh8v/jZycHMaP\nH0+PHj2YMmUKI0eOJCgoiOnTp7NkyRLmzp3Ljh07eOKJJ1CpVKhUKv78808GDRrEpEmTmDlzZo1O\nX94rilhQqDYUd5mMiIiQXSaltMWdXCaNKSgoIDY2lry8PPz8/O7acRL+6RKQxENmZqY81Mu4xbCq\n88FarZb4+HgyMjLw8/OrFneU5moWhDCg0RQZSi1ZchgPj8wSBahWVlYkJqoYOlSNjU3JTo+cHBWZ\nmQbU6kLs7Kzki2JtFAtS6uzKlSu0bdtWNiiS6h5OnDjBxx9/zIkTJ7hx4wYtWrSgX79+dO/enW7d\nutG0adMqPoLqyfXr13n//ff55Zdf0Gq1ODo6snPnTpo3bw5Ar169yMjIYMuWLbRq1UpOWyxevJhV\nq1Zx7NgxPD09q/goKh9FLChUW/R6PXFxcXLa4tChQ6SlpdGxY0d5OFZwcLDJ3b6Ur798+bLZNsH7\n4d+8HqTK9soUD7dv35bNpNq2bVvpw7lK49+6IfbtK6BBg1yzhX4ZGY2YMaMVzs4qHBz++f3cXD0p\nKXry862xs1Nhbf3Pazod6HS1RywUFBQQHR2NXq8nMDAQh+JTu4C4uDheeOEFGjVqxMqVKzl37hwR\nEREcOnQIS0tLjhw5UgUrr77o9XpZXK5bt46JEyfi5uZGdHQ09erVIz8/Hzs7O3JycvDy8uLxxx9n\nxYoVJuL7xo0b1dLZtTJQxIJCjcFgMHD27FnZojoiIoIrV67w0EMP0bVrVwIDA/n666+xsLDg66+/\nNtt3Xp4YtxhK+WXJ68FYQFRESFiv15OUlMTVq1dp1aoV7u7u1S49crc+C5LB0V9/5TFtWgtsbbXY\n2YGlpRUgyMnRk59vh05njV5f8ljVasHvv997S2Z14datW5w+fZoGDRrQunXrEn8/Qgg2btzIa6+9\nxpQpU1i0aFEJQWx8YVQoabT066+/Eh4ezsGDB2nRogWhoaFAUWpUrVZz8OBB+vTpw6JFi5g2bZpJ\na6rBYKjyaGJVoIiFMrJ06VJ+/PFHzpw5g52dHSEhIbz33nv/Or5127ZtzJs3j+TkZLy9vVm8eDFD\nhgyppFXXbiQDnoMHD7JhwwbCw8Np1qwZLi4uBAcHy34PDRo0qHKvB2PxcL+DqaShSBYWFvj7+5u9\n66zJSGkIR0cDVlY6NJoC9HoDWq0FBQXWzJp1ES8vO5ycnEwKUB0dK87sqTIQQpCcnMylS5do3bq1\nPLHVmPz8fF5//XV++uknvv76azmvrmAeg8Eg1x0UFhYyZswY3Nzc+O9//wvAxx9/zNq1axkzZgxv\nvPGGiciaN28e77//PufOncPd3b0qD6NaUD2bkashBw8eZMqUKXTq1AmdTsecOXPo27cvcXFxpZ6s\nDx8+zIgRI1i4cCFDhgxh+/btPPPMM0RERMhtOQr3jkqlolGjRoSFhXHy5ElCQ0Pp0aOH7PWwZMkS\n2WVS6raoSJdJlUqFg4MDDg4OeHh4AEUnd0k4JCYmkpeXJ/sT3O0sAalg8+zZs/JEwdp8h5Ofb8Bg\n0GBpaYVabYsQKgwGA15eVri6XiEzM5PcXJWRuVEdDIbycUesbDQaDTExMWi1WoKCgszObUhKSmLU\nqFGo1WpOnDgh59irK0uXLmX27NlMnz6dlStXVvr7CyHkv4Vff/1V9kzYvHkzjz32GP379+fpp5/m\nypUrfPXVVzz88MM89thjZGZmEhUVxcKFC3n55ZcVofA3SmThHrl58yYNGzbk4MGD9OjRw+w2I0aM\nICsriz179sjP9e/fnzp16rBp06bKWmqtRq/XM2vWLKZPn17iSy2E4ObNmyYW1cYuk1LdQ7NmzSrt\nAmM81EnyJ7C3tzcRD+ZspzUaDbGxseTm5uLv71+rq7EvX4ZBg1RkZRmwtrY2CbE7OAi2bdPi4SHk\nGhLp8yzujljdpkKWRlpaGjExMdStW5c2bdqUWK8Qgp9++olXXnmFF154gRUrVlT7QWfHjh3jmWee\nwdnZmV69elWJWJBYuHAhS5YsYfbs2dy8eZO9e/eSk5PD0aNH8fDw4K+//uKDDz4gLCyMWbNmMXv2\nbEaOHMknn3wCPLhph+IoYuEeSUpKwsfHh5iYGLkftzienp7MmDGDGTNmyM99+OGHrFy5kosXL1bW\nUhX+RnKZjIiIkC2qT5w4QePGjU0sqivTZdLY6yEjI4OsrCzZ60G64OXk5BAfH0+9evVo3br1facx\nqjMFBQWcPn2ay5fBy6ttiaidvT14eJg/ZRWfCimlgYo7TQgSx44AACAASURBVFaXIlAhBOfPn+f8\n+fP4+vqarTvRarXMmzePb775hs8++4wRI0ZU+7RDTk4O7du359NPP2XRokU89NBDVSYWrl+/zqBB\ng5g8eTLjxo0DIDw8nFmzZqFSqYiIiACKxM2XX37J8ePHGTZsGG+++WaVrLc6o4iFe0AIweDBg0lP\nT+fQoUOlbmdjY0NoaCjPP/+8/NzGjRsZO3YsGo2mMpaqcAeMXSal0dxHjx7FxcXFJG3Rtm3bSisW\nK+71kJGRAYCzszNubm7yaO7qfsG4F27evElsbCwNGjQoty6WgoICkxqS3NxcOZIjiYeytOKWN1qt\nltOnT5OXl0dgYCDOzs4ltrl06RKjR48mPz+fH3744V/ro6oLo0ePpm7dunz44Yf07Nmz0sSCuQjA\nlStX8PHx4ZtvvmH48OFAkUDftm0bL7/8MlOmTGHZsmXy9unp6XLUTikSNaV6x+eqKVOnTiU6OlpW\npXei+ElImV5WfVCpVDg5OdG3b1/69u2LEIKCggKOHDlCeHg4v/zyC2+//TY2NjayRXW3bt0IDAys\nsLt7Kysr6tWrh5WVFTdu3MDFxQVPT0/y8/O5desWSUlJqFQqE4vq6uD1cD9IXS5Xr16lTZs2uLm5\nldu+bW1tcXNzk/ep1WrlyMOVK1eIi4vDxsbGRDxU9EjpjIwMoqOj5ULc4n9LQgj279/PSy+9xODB\ng/n4449rTBHr5s2bOXnyJMeOHavU99XpdLK4LCwsxMrKCpVKhaWlJcHBwURFRfHEE09gZ2eHtbU1\njz76KPXq1eP999+nQ4cODB8+HIPBQJ06dZDunxWhYIoiFu6SadOmsXPnTsLDw+UittJo3LgxKSkp\nJs+lpqY+sH261R2VSoWdnR09e/akZ8+egKnL5KFDh3jvvfcwGAx07txZFg/t27cvtxyy8YjlFi1a\nmEztbN68eYk8/YULFzAYDPJo7nsdJ11V5ObmEhMTA0Dnzp3vOOm0PLCxsaFhw4byXAW9Xi9HclJT\nU0lMTMTS0tJk1Hl5jZQWQnDx4kWSk5Px8fGhadOmJUSJTqdj8eLFfPLJJ3z00UeMGzeuxtxcXL58\nmenTp7N///5Kn7FiZWWFVqtlzJgxFBYWUr9+fT7++GPc3Nxo3749v/76Kx06dJA70YQQBAUF8eij\nj/L222/To0cP+bxcUz7vykZJQ5QRIQTTpk1j+/bthIWF4ePj86+/M2LECLKzs/nll1/k5wYMGICr\nq6tS4FhD0el0nDp1Sk5bREREkJeXR3BwsJy66NSpE3Z2dnd90snPz+f06dNotVr8/f3LNGJZytNL\naYv09HR5nLQkHqprkd+1a9c4c+YMHh4etGzZslpER4yNt4qPlDZ2mrxbMVZYWEhsbCzZ2dkEBgaa\n/be9ceMGY8eO5dq1a/zwww+0a9euvA6rUtixYwdDhgwx+Wz0ej0qlervuSCaChOx6enp9OvXD2dn\nZ/z8/NiyZQv+/v78/PPPAAwaNIj8/Hx69OjB448/zqpVqygoKGDcuHHMnDmT1atX069fvwpZW21B\nEQtl5JVXXmHjxo389NNPJrlDFxcXuXr9xRdfxN3dnaVLlwLw559/0qNHDxYvXszgwYP56aefmDt3\nrtI6WYswGAzExsbKRlGSy2SHDh3kyEPnzp3/tc7g+vXrnDlzhkaNGuHr63vPJ1VpYJdxzUNBQQFO\nTk4mofaqLJLU6XScOXOGW7du4efnV+HmWfeDsRiTxINGo5FHSkuf6Z2KJjMzM4mOjsbR0RF/f3+z\naYeIiAjGjBlD9+7dWbduXZmEYnUjOzu7ROH22LFjad26NW+++WapheD3y/r161GpVJw+fZoPP/wQ\ngAsXLtCuXTtGjhzJp59+yrlz5/j222/55JNP5LqfsLAwsrKyaNOmDT/99JMcTVQwjyIWykhpJ/qv\nvvqKMWPGANCzZ0+8vLxkNzCArVu3MnfuXM6dOyebMg0dOrQSVqxQFfyby6T0cHV1RaVScevWLcLD\nw6lbty5t27Y1O3b4fpGK/KQLXm5ubokOgcpqxcvKyiImJgZbW1v8/Pxq5EhwY+8M6fM0HnUutb8K\nIbhy5QqJiYl4e3vTrFmzEucRvV7PypUrWbZsGUuXLmXq1KnVIsJSXlRGgeOwYcP48ccfGT58OJs2\nbZI/vx07djB06FDWr18vd0KkpqZSWFgot1m/8847/PLLL3z//fcP7DTJsqKIBQWFCsTYZVKab5Gc\nnIyfnx+tW7cmLCyM9u3bs3Hjxkq7cGq1WpMOgezsbOzt7U2KJsu7Q0AIwaVLl0hKSipRi1HTkYom\npc80OzsbGxsbVCoVOp0OX19f3NzcShxvWloaEyZMIC4ujs2bN9O5c+cqOoKKozzFQml+B9nZ2QwY\nMIDCwkL27duHq6ur/Npbb73F+vXr2bFjB926dQOKxrhv3bqV3bt3s3//fjZv3qykIMqAIhZqGfdi\nSx0aGsrYsWNLPJ+fn18j7/yqM1KR26uvvsru3bsJCAjg1KlTtGrVSo46dO/evcJcJs0heT1IFzzJ\n68FYPBjbKt8tWq2W2NhYcnJyCAgIMDmZ10akbgcLCwvUajVZWVlYWlpiZ2fHnj176NmzJ2q1mnHj\nxuHv788333xDvXr1qnrZ1RpjofDjjz+SnJyMq6srQUFBtGvXjqioKEJCQnjttddYuHChye+2bduW\nLl26sG7dOnkfK1eu5NixY6xcubJap8GqE4pYqGX079+fZ5991sSWOiYm5o621KGhoUyfPp2EhAST\n56WRuArlR2FhId27dycvL4/vvvsOf39/bt68yaFDh2SjqKioKJo1a0a3bt2qxGXSuENAGs1taWkp\nC4e78XpIS0vj9OnTuLi40LZt21ptKCWE4OrVqyQmJuLl5UXz5s1RqYosqrOysjhz5gzz5s0jKiqK\ngoICvLy8eOGFF3jkkUcIDg6u8E6Q2sDIkSP59ddf6dOnD+fPn0ej0bBgwQKeeOIJvvrqK1566SW2\nbdvGU089JbepS2kiUFrX7wdFLNRyymJLHRoayquvviobAClULLt376Z3795mozZCCDIzM+X5FocO\nHZJdJqVui65du9KqVatKEw/Sxc647sHY68Fce6E0KvzSpUv4+Pjg4eFRq0/Ser2e+Ph4bt++TUBA\nAHXr1i2xTVZWFlOnTuWPP/5g0aJFFBQUyCOlMzIySEtLqzbuktUJ6QIfGhrKmjVr+Pbbb/Hx8WHv\n3r0MHDiQ8ePHs3btWiwtLZkyZQo7duzg119/pW3btib7MfZiULh7FLFQyymLLXVoaCgvvfQS7u7u\n6PV6HnroIRYuXMjDDz9cyatVKE51dJk0GAwlRnPr9Xq5rdDe3p7Lly+j0+kIDAw0OxSpNpGTk0N0\ndDQ2NjYEBASYLRY9ffo0L7zwAu7u7mzatMkkaieEICUlpVzNqGojY8aMwdnZmVWrVrFu3Tpef/11\nJk6cyKJFi2SRpdPp8Pb2pk+fPqxbt65WC9TKRhELtZiy2lJHRkaSlJREQEAAWVlZfPTRR/zyyy9E\nRUWVyU9CofIo7jIZHh5OZGRkpbpMmluT1F6YkpIiR6iMvR5cXV1r5V3d9evXiY+Px9PT0+wUUCEE\nGzZs4PXXX+f//u//ePfdd2vl53C/GKcHzKUKtFotEyZM4KGHHiIuLo7t27ezatUqnnvuOQB++eUX\n1Go1vXv3JjU1tUK6ih50FLFQi5kyZQq7d+8mIiLiX90mjTEYDLRv354ePXqwatWqClyhQnmg0Wg4\nceKELB7+/PNPDAYDwcHBctqiQ4cOFdoeqdfrSUxMJCUlhTZt2uDs7GwyXTM/P1/2eiiLN0F1R6/X\nk5CQQGpqKv7+/tSvX7/ENnl5ecycOZOff/6Zb775hoEDB1a7O901a9awZs0aLly4AICfnx/z589n\nwIABlb6W33//nebNm8tOpcWF15IlS5g7dy6BgYFs2rSJNm3aAEWCbc6cOYSEhDBmzBhZjClph/JF\nEQu1lGnTprFjxw7Cw8Pvae79yy+/zJUrV0zGayvUDCSXSUk8SC6TQUFBcuThXl0mzZGTk0NMTAyW\nlpYEBASYHbFdUFBgIh6kojNj8VBTOm9yc3OJjo7G0tKSwMBAs+tOTEzkxRdfxMHBgU2bNlXbHv5d\nu3ZhaWlJy5YtAfj6669Zvnw5f/31F35+fpW2jry8PDkqkJycDPwTYTD+b8+ePcnPz2ft2rU0bdqU\nzMxMJk2aRHZ2Nj/88AOenp6VtuYHDUUs1DLuxZba3D6CgoIICAjgyy+/rIBVKlQmBoOBuLg4wsLC\nZK+H27dv37XLZHGEEFy7do2EhASaNm2Kt7d3mYsujb0JJK8HOzs7E/FQXmKmPLlx4wZxcXG4u7ub\ntagWQrB9+3amTJnCmDFjWL58eY2LoNStW5fly5czfvz4Sn3f6OhoBg0aRO/evfniiy9MXpMEw7Vr\n1+jfvz+ZmZk4ODig0Who3bo1u3btwsLCQul2qEAUsVDLuBdb6gULFtC5c2d8fHzIyspi1apVbNiw\ngT/++IOgoKAqOQ6FisNgMJCUlGQiHiSXSaloMiQkhDp16pR64i0sLCQ+Pp709HT8/f3v2ydAp9OZ\nGBtlZmZW+jTIO2EwGEhMTOT69ev4+fmZzYlrNBrmzJnDxo0bWbduHcOGDatRFy69Xs8PP/zA6NGj\n+euvv0p0E5QnpV3Ut2/fzvDhw/nss88YP368STpC+v+0tDTi4+NJS0vD3t6e3r17A0raoaJRxEIt\n415sqWfMmMGPP/5ISkoKLi4uPPzww7zzzjt06dKlklatUJVILpNS2sLYZdLYorphw4aoVCrCwsK4\nefMm3t7e+Pn5VUgthLHXg2QYJXk9SOLBycmpUi7G+fn5REdHAxAYGGg2zXLx4kVGjx6NVqvl+++/\np1WrVhW+rvIiJiaGLl26UFBQgKOjIxs3bmTgwIEV8l4XLlygcePG2Nramq1L0Gq1LF68mPfee4/j\nx4/j7+9vst2ZM2dITU0t0Qau1+trzKTVmooiFhQUFEyQ0gvG4iEuLg4fHx+aNGnC4cOHmTVrFjNn\nzqx0rwfj6ANQYjR3ea8nNTWV2NhY3NzczHpbCCHYu3cvEyZMYOjQoaxatcqsmKjOaLVaLl26REZG\nBtu2bWP9+vUcPHiw3CMLUVFRjBs3jn79+rFkyRLAfIQhLS2N0aNHk5iYSGxsrBwt2L17N8888wzB\nwcH8/vvvpdo/K1QMilhQqFLupRp727ZtzJs3j+TkZHk4lzSnXqH8EUIQFxfHyJEjOX/+PP7+/kRG\nRtKsWTM56tCtWze8vLwq7eQthCA7O9uk7sF4lLQ0mvte7zalVM3Vq1dp06aNWTfTwsJCFi1axNq1\na/n4448ZPXp0jUo7lMZjjz2Gt7c3n332WbnuNycnhzfffJOYmBimTp3KM888U+q2CQkJDBgwgODg\nYDZt2sT8+fNZtGgRs2fPZtGiReW6LoWyoYgFhSrlbquxDx8+TPfu3Vm4cCFDhgxh+/btzJ8/Xxn7\nXYFcuXKFjh070rNnTz777DOcnZ1NXCYjIiI4ceIEjRo1MvF6qEyXScnrwVg8aLVanJ2d5dSFq6tr\nmbwnCgoKiI6ORq/XExgYaNYmPSUlhTFjxpCamsoPP/xAQEBARRxWldC7d2+aNm1qMj33fpGiAImJ\nicyZM4fMzEyWL19Ou3btSo0Q7N27l6effhoHBwe0Wq1JekSpT6h8FLGgUO24UzX2iBEjyMrKMmnp\n7N+/P3Xq1GHTpk2VucwHBiEEv/32G7179zZ75yxdqA8fPkxYWBgREREcPXoUZ2dnE/Hg5+dXaXll\nybxKEg7FvR6kuofinQq3bt3i9OnTNGzYEF9f3xLrFUJw6NAhxowZQ8+ePfn8889xdnaulGOqCGbP\nns2AAQNo2rQp2dnZbN68mWXLlrF371769OlTIe+5b98+3n//fdzc3Pjkk09wcXEptX5hxYoV/P77\n72zZsoW6detiMBhQqVS1IoJT01DEgkK1oSzV2J6ensyYMYMZM2bIz3344YesXLmSixcvVuZyFUrB\n2GVSGpAVGRmJtbW1yXyLdu3aVepgKWOvh4yMDHJycnBwcJCjDllZWVy7do3WrVvTpEmTEr+v1+tZ\nsWIFy5cv57333uOVV16p8Tnz8ePH89tvv3H9+nVcXFwIDAzkzTffLDehUFrUYPXq1Xz33Xc89thj\n8pRIc/ULeXl58oAtJZpQtShiQaHKuZtqbBsbG0JDQ3n++efl5zZu3MjYsWPRaDSVtWSFu0Sr1XL8\n+PEqdZk0t6aMjAxu3bpFSkoKer0etVpNvXr1cHV1xcHBQS6avH37Ni+//DIJCQls2bJFaSn+F4xF\nwtmzZ4mMjKR+/fr4+/vTtGlTCgoKmDt3Ln/88QevvPIKo0aNKvP+FKoGRaYpVDm+vr6cOnVKrsYe\nPXr0Hauxi999KEYs1R9pdkVISAhvvfUWOp2OqKgoeTjW6tWryc3NJSgoSB7LXZ4uk6WtycrKSp7M\n2rJlS3JycsjIyODatWusW7eOPXv24O/vT2JiIr6+vhw7dsystbOCKdKF/dNPP2X27NkEBgaSkJBA\n9+7dee211wgJCWHSpElcu3aNr776Cl9fX4KCgkoVBYpQqHqUyIJCteNO1dhKGqJ2Ys5l8tatW3To\n0EGOPHTu3LncvBWEEJw/f54LFy7QqlUr3N3dS+w3KyuLpUuXEhYWRl5eHteuXcPOzo7u3bvz9NNP\n88ILL9z3OmozK1asYO3atSxdupRhw4Zx8OBBxowZg6enJ1u2bKFx48YcOHCADz74ACsrK9atW0ej\nRo2qetkKpaDINYVqhxCi1JRCly5d+PXXX02e279/PyEhIZWxNIUKwsLCAn9/f6ZOncqWLVu4cuUK\np0+fZvz48aSkpDBjxgw8PDzo0aMHb731Fj///DNpaWncy72OVqvlr7/+4tq1a3Tq1AkPD48SQiEz\nM5PJkyezdetWVq1axdmzZ8nIyGD37t2EhISQlZVVXodeK8jJyZH/X/o3yc/PZ9q0aQwbNozY2Fgm\nTZqEo6MjmZmZvP7660DRjUGfPn3Iysri5s2bVbJ2hTIiFBSqkFmzZonw8HBx/vx5ER0dLWbPni0s\nLCzE/v37hRBCjBo1Srz11lvy9n/88YewtLQUy5YtE/Hx8WLZsmXCyspKREZGVtUhKFQCBoNBnD9/\nXoSGhorx48cLHx8fYWFhIQICAsTEiRPFhg0bxLlz50ROTo7Izc0t9XH16lWxZ88ecfjwYZGZmWl2\nm8OHDwtvb2/x6KOPipSUlKo+9GrPBx98IObMmSOEEGLNmjVi8uTJQgghcnNzRWZmpjh8+LDw8vIS\nM2fOFBqNRrz66qvCyclJrFixQgghhF6vF+np6VW2foWyoYgFhTJjMBiETqcTBoOh3PY5btw40axZ\nM2FjYyMaNGggevfuLQsFIYR45JFHxOjRo01+54cffhC+vr7C2tpatG7dWmzbtq3c1qNQMzAYDOLq\n1ati48aNYtKkScLPz0+oVCrh6+srxo4dK7744guRkJAgi4esrCzxzTffiJ07d4r4+HizoiInJ0d8\n+umnwsHBQcydO1cUFhZW9WGWYMmSJaJjx47C0dFRNGjQQAwePFicOXOmStc0depUERwcLHr06CHU\narXYtGmTyevTp08XY8aMEXl5eUIIIZYvXy4aNGggGjZsKP766y95O71eX6nrVrg7lJoFhTsi/i4e\nVLzXFaozQghu3bolt2pGRERw6tQpPD096dSpE8nJyVy9epVDhw7h7u5e4vdzc3N57bXX2Lt3L998\n8w39+/evlkWz/fv359lnn6VTp07odDrmzJlDTEwMcXFxZs2jKhKpGPHixYt06NCB/Px8Pv/8c0aO\nHGmSHhoyZAg6nY6ff/4ZgClTpuDu7k7//v1p3759pa5Z4d5RxILCv3L06FG+++47Tpw4gbu7O0OH\nDqVv377UqVOnqpdW6dytPXVoaChjx44t8Xx+fj62trYVudQHGiEEmZmZfPnllyxYsAAnJyeys7Nx\ncnIy8Xrw9fXl7NmzjBo1CmdnZzZv3oynp2dVL7/MSJ0cBw8eLDFcqaIofuPw22+/sWvXLo4cOULb\ntm158803adWqlSwYVqxYwRdffIG3tze3b98mKyuL/fv3y6JNKN1MNQKlwFHhjsTExPD444+TlJTE\n2LFjqVevHsuWLWPYsGGcOnWqqpdX6Xh4eLBs2TKOHz/O8ePHefTRRxk8eDCxsbGl/o6zszPXr183\neShCoWJRqVTs2rWLefPmMW/ePC5dusTVq1f56quvaNWqFdu2baNbt254eHjQuXNn+vTpQ1hYWI0S\nClBUiAlFrqeVgbFQOHnyJKmpqTzyyCOsXLmSadOmceLECUJDQ8nOzpadFl988UVef/11nJ2d6dix\nI6dPn8bd3V0WE4pQqBkokQWFO/L222+zefNmjh49iouLCwBJSUns2rWLzp07m4yxFkKg1+uxsLB4\n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\nrkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "\n", + "show_iris()\n", + "show_iris(0, 1, 3)\n", + "show_iris(1, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can play around with the values to get a good look at the dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DISTANCE FUNCTIONS\n", + "\n", + "In a lot of algorithms (like the *k-Nearest Neighbors* algorithm), there is a need to compare items, finding how *similar* or *close* they are. For that we have many different functions at our disposal. Below are the functions implemented in the module:\n", + "\n", + "### Manhattan Distance (`manhattan_distance`)\n", + "\n", + "One of the simplest distance functions. It calculates the difference between the coordinates/features of two items. To understand how it works, imagine a 2D grid with coordinates *x* and *y*. In that grid we have two items, at the squares positioned at `(1,2)` and `(3,4)`. The difference between their two coordinates is `3-1=2` and `4-2=2`. If we sum these up we get `4`. That means to get from `(1,2)` to `(3,4)` we need four moves; two to the right and two more up. The function works similarly for n-dimensional grids." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Manhattan Distance between (1,2) and (3,4) is 4\n" + ] + } + ], + "source": [ + "def manhattan_distance(X, Y):\n", + " return sum([abs(x - y) for x, y in zip(X, Y)])\n", + "\n", + "\n", + "distance = manhattan_distance([1,2], [3,4])\n", + "print(\"Manhattan Distance between (1,2) and (3,4) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Euclidean Distance (`euclidean_distance`)\n", + "\n", + "Probably the most popular distance function. It returns the square root of the sum of the squared differences between individual elements of two items." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Euclidean Distance between (1,2) and (3,4) is 2.8284271247461903\n" + ] + } + ], + "source": [ + "def euclidean_distance(X, Y):\n", + " return math.sqrt(sum([(x - y)**2 for x, y in zip(X,Y)]))\n", + "\n", + "\n", + "distance = euclidean_distance([1,2], [3,4])\n", + "print(\"Euclidean Distance between (1,2) and (3,4) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hamming Distance (`hamming_distance`)\n", + "\n", + "This function counts the number of differences between single elements in two items. For example, if we have two binary strings \"111\" and \"011\" the function will return 1, since the two strings only differ at the first element. The function works the same way for non-binary strings too." ] }, { "cell_type": "code", "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hamming Distance between 'abc' and 'abb' is 1\n" + ] + } + ], + "source": [ + "def hamming_distance(X, Y):\n", + " return sum(x != y for x, y in zip(X, Y))\n", + "\n", + "\n", + "distance = hamming_distance(['a','b','c'], ['a','b','b'])\n", + "print(\"Hamming Distance between 'abc' and 'abb' is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mean Boolean Error (`mean_boolean_error`)\n", + "\n", + "To calculate this distance, we find the ratio of different elements over all elements of two items. For example, if the two items are `(1,2,3)` and `(1,4,5)`, the ration of different/all elements is 2/3, since they differ in two out of three elements." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean Boolean Error Distance between (1,2,3) and (1,4,5) is 0.6666666666666666\n" + ] + } + ], + "source": [ + "def mean_boolean_error(X, Y):\n", + " return mean(int(x != y) for x, y in zip(X, Y))\n", + "\n", + "\n", + "distance = mean_boolean_error([1,2,3], [1,4,5])\n", + "print(\"Mean Boolean Error Distance between (1,2,3) and (1,4,5) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mean Error (`mean_error`)\n", + "\n", + "This function finds the mean difference of single elements between two items. For example, if the two items are `(1,0,5)` and `(3,10,5)`, their error distance is `(3-1) + (10-0) + (5-5) = 2 + 10 + 0 = 12`. The mean error distance therefore is `12/3=4`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean Error Distance between (1,0,5) and (3,10,5) is 4\n" + ] + } + ], + "source": [ + "def mean_error(X, Y):\n", + " return mean([abs(x - y) for x, y in zip(X, Y)])\n", + "\n", + "\n", + "distance = mean_error([1,0,5], [3,10,5])\n", + "print(\"Mean Error Distance between (1,0,5) and (3,10,5) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mean Square Error (`ms_error`)\n", + "\n", + "This is very similar to the `Mean Error`, but instead of calculating the difference between elements, we are calculating the *square* of the differences." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean Square Distance between (1,0,5) and (3,10,5) is 34.666666666666664\n" + ] + } + ], + "source": [ + "def ms_error(X, Y):\n", + " return mean([(x - y)**2 for x, y in zip(X, Y)])\n", + "\n", + "\n", + "distance = ms_error([1,0,5], [3,10,5])\n", + "print(\"Mean Square Distance between (1,0,5) and (3,10,5) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Root of Mean Square Error (`rms_error`)\n", + "\n", + "This is the square root of `Mean Square Error`." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Root of Mean Error Distance between (1,0,5) and (3,10,5) is 5.887840577551898\n" + ] + } + ], + "source": [ + "def rms_error(X, Y):\n", + " return math.sqrt(ms_error(X, Y))\n", + "\n", + "\n", + "distance = rms_error([1,0,5], [3,10,5])\n", + "print(\"Root of Mean Error Distance between (1,0,5) and (3,10,5) is\", distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PLURALITY LEARNER CLASSIFIER\n", + "\n", + "### Overview\n", + "\n", + "The Plurality Learner is a simple algorithm, used mainly as a baseline comparison for other algorithms. It finds the most popular class in the dataset and classifies any subsequent item to that class. Essentially, it classifies every new item to the same class. For that reason, it is not used very often, instead opting for more complicated algorithms when we want accurate classification.\n", + "\n", + "![pL plot](images/pluralityLearner_plot.png)\n", + "\n", + "Let's see how the classifier works with the plot above. There are three classes named **Class A** (orange-colored dots) and **Class B** (blue-colored dots) and **Class C** (green-colored dots). Every point in this plot has two **features** (i.e. X1, X2). Now, let's say we have a new point, a red star and we want to know which class this red star belongs to. Solving this problem by predicting the class of this new red star is our current classification problem.\n", + "\n", + "The Plurality Learner will find the class most represented in the plot. ***Class A*** has four items, ***Class B*** has three and ***Class C*** has seven. The most popular class is ***Class C***. Therefore, the item will get classified in ***Class C***, despite the fact that it is closer to the other two classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "Below follows the implementation of the PluralityLearner algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(PluralityLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It takes as input a dataset and returns a function. We can later call this function with the item we want to classify as the argument and it returns the class it should be classified in.\n", + "\n", + "The function first finds the most popular class in the dataset and then each time we call its \"predict\" function, it returns it. Note that the input (\"example\") does not matter. The function always returns the same class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "For this example, we will not use the Iris dataset, since each class is represented the same. This will throw an error. Instead we will use the zoo dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mammal\n" + ] + } + ], + "source": [ + "zoo = DataSet(name=\"zoo\")\n", + "\n", + "pL = PluralityLearner(zoo)\n", + "print(pL([1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output for the above code is \"mammal\", since that is the most popular and common class in the dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## K-NEAREST NEIGHBOURS CLASSIFIER\n", + "\n", + "### Overview\n", + "The k-Nearest Neighbors algorithm is a non-parametric method used for classification and regression. We are going to use this to classify Iris flowers. More about kNN on [Scholarpedia](http://www.scholarpedia.org/article/K-nearest_neighbor).\n", + "\n", + "![kNN plot](images/knn_plot.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how kNN works with a simple plot shown in the above picture.\n", + "\n", + "We have co-ordinates (we call them **features** in Machine Learning) of this red star and we need to predict its class using the kNN algorithm. In this algorithm, the value of **k** is arbitrary. **k** is one of the **hyper parameters** for kNN algorithm. We choose this number based on our dataset and choosing a particular number is known as **hyper parameter tuning/optimising**. We learn more about this in coming topics.\n", + "\n", + "Let's put **k = 3**. It means you need to find 3-Nearest Neighbors of this red star and classify this new point into the majority class. Observe that smaller circle which contains three points other than **test point** (red star). As there are two violet points, which form the majority, we predict the class of red star as **violet- Class B**.\n", + "\n", + "Similarly if we put **k = 5**, you can observe that there are three yellow points, which form the majority. So, we classify our test point as **yellow- Class A**.\n", + "\n", + "In practical tasks, we iterate through a bunch of values for k (like [1, 3, 5, 10, 20, 50, 100]), see how it performs and select the best one. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "Below follows the implementation of the kNN algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(NearestNeighborLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It takes as input a dataset and k (default value is 1) and it returns a function, which we can later use to classify a new item.\n", + "\n", + "To accomplish that, the function uses a heap-queue, where the items of the dataset are sorted according to their distance from *example* (the item to classify). We then take the k smallest elements from the heap-queue and we find the majority class. We classify the item to this class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "We measured a new flower with the following values: 5.1, 3.0, 1.1, 0.1. We want to classify that item/flower in a class. To do that, we write the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "setosa\n" + ] + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "\n", + "kNN = NearestNeighborLearner(iris,k=3)\n", + "print(kNN([5.1,3.0,1.1,0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output of the above code is \"setosa\", which means the flower with the above measurements is of the \"setosa\" species." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DECISION TREE LEARNER\n", + "\n", + "### Overview\n", + "\n", + "#### Decision Trees\n", + "A decision tree is a flowchart that uses a tree of decisions and their possible consequences for classification. At each non-leaf node of the tree an attribute of the input is tested, based on which corresponding branch leading to a child-node is selected. At the leaf node the input is classified based on the class label of this leaf node. The paths from root to leaves represent classification rules based on which leaf nodes are assigned class labels.\n", + "![perceptron](images/decisiontree_fruit.jpg)\n", + "#### Decision Tree Learning\n", + "Decision tree learning is the construction of a decision tree from class-labeled training data. The data is expected to be a tuple in which each record of the tuple is an attribute used for classification. The decision tree is built top-down, by choosing a variable at each step that best splits the set of items. There are different metrics for measuring the \"best split\". These generally measure the homogeneity of the target variable within the subsets.\n", + "\n", + "#### Gini Impurity\n", + "Gini impurity of a set is the probability of a randomly chosen element to be incorrectly labeled if it was randomly labeled according to the distribution of labels in the set.\n", + "\n", + "$$I_G(p) = \\sum{p_i(1 - p_i)} = 1 - \\sum{p_i^2}$$\n", + "\n", + "We select a split which minimizes the Gini impurity in child nodes.\n", + "\n", + "#### Information Gain\n", + "Information gain is based on the concept of entropy from information theory. Entropy is defined as:\n", + "\n", + "$$H(p) = -\\sum{p_i \\log_2{p_i}}$$\n", + "\n", + "Information Gain is difference between entropy of the parent and weighted sum of entropy of children. The feature used for splitting is the one which provides the most information gain.\n", + "\n", + "#### Pseudocode\n", + "\n", + "You can view the pseudocode by running the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pseudocode(\"Decision Tree Learning\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "The nodes of the tree constructed by our learning algorithm are stored using either `DecisionFork` or `DecisionLeaf` based on whether they are a parent node or a leaf node respectively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(DecisionFork)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DecisionFork` holds the attribute, which is tested at that node, and a dict of branches. The branches store the child nodes, one for each of the attribute's values. Calling an object of this class as a function with input tuple as an argument returns the next node in the classification path based on the result of the attribute test." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(DecisionLeaf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The leaf node stores the class label in `result`. All input tuples' classification paths end on a `DecisionLeaf` whose `result` attribute decide their class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(DecisionTreeLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The implementation of `DecisionTreeLearner` provided in [learning.py](https://github.com/aimacode/aima-python/blob/master/learning.py) uses information gain as the metric for selecting which attribute to test for splitting. The function builds the tree top-down in a recursive manner. Based on the input it makes one of the four choices:\n", + "
      \n", + "
    1. If the input at the current step has no training data we return the mode of classes of input data received in the parent step (previous level of recursion).
    2. \n", + "
    3. If all values in training data belong to the same class it returns a `DecisionLeaf` whose class label is the class which all the data belongs to.
    4. \n", + "
    5. If the data has no attributes that can be tested we return the class with highest plurality value in the training data.
    6. \n", + "
    7. We choose the attribute which gives the highest amount of entropy gain and return a `DecisionFork` which splits based on this attribute. Each branch recursively calls `decision_tree_learning` to construct the sub-tree.
    8. \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "We will now use the Decision Tree Learner to classify a sample with values: 5.1, 3.0, 1.1, 0.1." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "setosa\n" + ] + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "\n", + "DTL = DecisionTreeLearner(iris)\n", + "print(DTL([5.1, 3.0, 1.1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected, the Decision Tree learner classifies the sample as \"setosa\" as seen in the previous section." + ] + }, + { + "attachments": { + "0_tG-IWcxL1jg7RkT0.png": { + "image/png": "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JsP8iaRnJiQkgkLIyzEUse+xu32S4DIet9vkXvSnSNxL4l6RifhcyaCQr/W2yupWpVtqDi9tNLer6kaBnwmLqbAUn0b/ndcPetvrnZ/ZxHD/Z2449MkZKdx3332ZTGZtbW19ff1//s//WavVTpw44Ywaer1etVq9du1auVxOpVJzc3NOou5yuR555JF33nnnl7/85fvvv08QxPr6+szMjCRJTrveZrN56dIlVVW/973v3XnnnbdUwHLORnfGF++99148Hrcs67333vvDH/5w4sQJ0zQDgcCDDz44Oztbq9XeeuutjY2NlZWVv/f3/t7TTz/98MMPUxT161//ut/vm6YJsQwcbVDAAodjgEHT9NLS0okTJ1ZXV1966aVQKPT444+73e79X9br9XK5HIZh8XjcWdMrCMJdd921srKyvLz84x//eH5+fmlpaf9GQqePVa1WEwQhlUp96Vb2/fr9frfbdbYHOquLnU9evXq11WrNzs72er1z585du3atWq1ms9nxeBwMBq9cuXLt2rXhcFir1d5+++1yufytb33rT1/AMkxTHU4ailaoDzbL3ZVcu6WMTNtyC8xsVJqLyTNhMeYXfBKDYSiGIvszVqhBHPM34+57Z/o/wUXyjJQMCgtROV/vZcvqalGpNgeXMs21vBIPCIsJeS4qR7ycX6IZmoAXDzgww/jdeQvn7j2Zgh5Y4MgzLas/NBrdQakxWC8q6wWlrgx13ZR45sysbyYspiPiXESQud2Jh/3L0lEUdoVD9N+N/i6anIlIMT+/mHQXar2NYjdTUkqNwdVse72oxPz8fEyenZ724xYZjiHw27zV4DMnVp3O6595Y0dRNBKJPP74441G46WXXrpw4cLGxkYqlYpGoxRFtdvtXC7Xbrej0eiZM2ecKWpnQn1+fv7ZZ5/Vdf3dd989d+7c5cuXU6mU3+83DKNSqZRKpclkcvr0aZ7nbzXhcblc8Xg8EAg0m81XXnml2+0Oh8NGo5FOpx966CGfz+f089U07fLly91uF0GQSCTy3HPPnTx50jmx3TRNiqL2+tMDcFRBAQt8A8OGrzDMQBDkgQce2NzcLJVK7777Lsuy3W73nnvucXaYm6bZ6/XW19fffffdQCDwj/7RP3IKWBzHPf3009ls9le/+tVvfvObYDD4xBNPJBIJkiRt255MJt1u97333stms3fffXc0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HWttbf3v//2/P/vss52dnaudfrW0ZjC65Z5vyefz586d+6d/+qcbN25885vf/LM/+zNBEE6fPv3GG2/Y7fbe3l6/39/Z2Xn48OGf//znP/nJT2ZmZr773e8ODAwYDAZoJAAAUPvdHi4CAGDdPhayrMfjEUXxxIkToiiaTCayrI+iKFmW5+fnT5w4YTQa9+7d+7/+1//as2fP/YWRuq7Pz8//9Kc//dWvfpXJZPbs2fOtb31r9+7ddrudjBbcRxms6vmvnAIjmbhwOPzTn/70yJEjwWDw+9///vPPP7+wsPDjH/84Fos9+uijmzdv7uvra2lp+eUvfzk0NHTx4sU//uM/fuGFF7xeL+SwAPiDuFPBJQDrGE3Tbrd769atQ0NDY2NjTqfz0KFDBw8edDgcGONisfjpp5/+53/+5/DwMMuygUDgwIED9xy3uVOxWDx9+vQ//MM/XL58ua+v73//7/+9a9cun89Hphyvticmr68xe0VePDk5+ZOf/GRsbGznzp3f+MY3GhoaOI574oknOjs7EULBYNBoNPb09LjdbqfT+dprrx07doxhGE3Tdu3aBZ0xAACsoFoDS5ZlSGABANZ32Nzf3z8wMHD06FFZljs7O//P//k//f39GGNVVcPh8I9+9KPz589//PHHRqOxo6OjqalptTEkWRvx7//+76+//jpN06+++urLL79MVi2QF9xHUIp+38ovTiQSH3/88bFjx3Rdf+yxx55++um6ujqbzfZ//+//FUXR6XQ6HA6WZV944YWmpqZ33nnntdde+8d//EeM8Te+8Y21KjkCAPg8IIEF1i2S3OE4zmAwkFySw+EYGBjYtWuXyWQinWhra2sikRiveP/993t7e1ebwFJVdXp6+j/+4z8+/vjjvr6+733vewcOHCBFKKupqAf6M2az2cuXLw8NDSmK0tfX19vbS8avHBXkcQtjbDQam5ubX331VZPJ9Dd/8zfvvPOO3W5vamqqr6+/j2liAADwVYAQEgSB53lVVUVRrG7wCgAA6zJsJtEjTdNms7m3t3dwcLCvr4+8IJ/PLywsKIpy4sSJs2fPzs7Oer1eo9G4qk/J5/NHjx598803s9ns888//z//5/9sbW0VBOEL+zHn5+fff//9ubm5Xbt2PfLII263m6zYaG9vX/oyt9u9Z88eo9EYj8ePHTv2i1/8wul0vvzyy1BD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MZFOp9etW5ekxquXYl1z3ht64dP968qVK6+++uqJEyeWLVvW2dl59913z2T7qqReffjhh6+++uru3bt7enq+8Y1vfP/731+1atUN7R8PwG0gYAEALFzTX+/r6+s3bdpUKBTS6XR/f/9zzz03MDDw4IMP3nfffXfdddfNHTyeklSP5IeJiYmhoaHe3t633377r3/968TExMaNG7ds2dLc3HzN8/noocrLy8fGxv7+978fPnx4//7977333te//vUnn3xy/fr1SVu5gzbbjqLozJkze/bs2b9/fxRF69at+853vrNs2bLKysob3U3s1ikWiydPnszn8+3t7W1tbckFgMlO8Inu7u6LFy+eO3du7dq1K1asuGZSxXG8ePHixx57bHh4eM+ePcePH//pT3/61a9+9YEHHlixYsWNblA1PW7Jf6eXB5ZKpcHBwf+ZcuDAgXQ6vXnz5i1btiTLrz5xGIeGhg5Pee21186fP799+/YnnnhizZo1yXpA27cDBEXAAgBY6JIv6s3Nzd/85jcXL1780ksvvf76688//3xPT88zzzxz0wErm83W1dU1NDTU1NQklxyOjY0dO3bs17/+9dGjR0dHRx999NGnnnpq6dKlM7nfXBzHg4ODP//5z7u7u+vq6h577LFdu3Yl98W741rDyMhIV1fXH/7wh3/961+rV6/etm3bI4880tDQEM4isjiOr1y5Mj4+ns1m29rapldgpVKpurq6tra2EydO7N27d9GiRatWrWppaUn++lHNzc1PP/10S0vLyy+/fOjQoXfffbezs/Ppp5++uYBVWVlZW1vb0NBQXV2dSqWSptnd3f2LX/zixIkThULha1/72vbt21esWDHD9VMnT5782c9+9u677+Zyuc7OzieeeOILX/jCnTijABYCAQsAYKGb3lG7pqZm/fr199xzz7Zt27q6uqIouman9plLp9Of+9znHnnkkZaWlo0bNyYVbHJycnx8PI7jtWvXrlmzZtOmTffdd19yx71P7AWTU9ra2lauXLlhw4b777+/ubn5jlt7lVS8d95557e//e17773X2NjY2dn5rW99a3rHpXACViqVeuCBB3K53OrVq1tbW1OpVPLL9vb2Xbt27d+/v7y8fOPGjRs2bFi+fPlHA9b0pKqvr9+6deuqVavefPPN3t7eXC53cxtLTc+olStXPvTQQ8meXMVicXx8PJPJrFmzZuPGjQ899NDKlStn0kMTqVSqtbV17dq1GzZs6OjoaGpqmuFsBGAOPq7YmBAAgKu/tEdRNDY2du7cuWKxePfddycrg25UcpChoaHR0dGmpqZPfepTlZWVhUJhaGjozJkz2Wy2qanprrvumvkd95IDnjp1qqqqqrm5eXqz8zurNURRdOTIkV/96le/+93vLl26tGPHjh/96Edr167NZDJBvZAoiiYnJwcHB0dHR+vq6pqbm5M1TcklhOenxHG8aNGipqamTCbzcVuYXT2phoeHL1y4UFZWtmTJkuvcDPE683NsbOz8+fOFQqFxSkVFRTKjPvzww0wms3jx4sbGxmw2O8MZldwN4PTp08kLnL6NgHoFECYBCwCAa7/Y/7/Pizf1Zf6jHzKTrPCfP5J+0k7b//Gxd1xlKJVKZ8+efeGFF5577rmhoaGOjo5nnnlm586duVwutNfycaN9nXfwOt1nVt67mc+oG723wJ07owAWFJcQAgAw+1/j/+NBbu7IyaPmQVwYHR1944039u7de+rUqWXLln37299++OGHZ2W/8GRh1Llz5/L5fC6XW7p06c2tm/vEd+o6T/Lm/jQnM2oWnxUAt03KEAAAwK02Njb21ltvvfjii0ePHl28eHFnZ+f27dtbW1uTv/6XMSVZ2/Xyyy//8Ic/fPbZZ48fP27AAZhnrMACAIBbKI7jKIp6e3t37979j3/8I5VKrV+//qmnnlq+fPlsbX0VRdHp06e7u7t7enqKxeLIyIhhB2CesQILAABurXw+v3fv3gMHDgwPD69evfrJJ5/8/Oc/n81mZ2u/8DiOk+sH4ziuqqq66XtHAkCwrMACAIBbJYqiS5cuHTx48JVXXsnn8+3t7Vu3bn344YdrampmcRumycnJDz744P3334/juLa2NpvNGnkA5hkBCwAAbpVCodDT07Nnz57jx4/X1tZu27bt8ccfb2lpmcVTxHF84cKFvr6+06dPx3FcXV2dyWSMPADzjIAFAAC3ysmTJ1955ZW9e/eWl5d/6Utf2rlz57p16+Ips3L8KIouXLhw6NCh7u7uy5cvV08RsACYfwQsAACYfVEUjY+P75kyPDz8mc985stf/nJDQ8Pg4GChUPjvA1b5lNHR0SNHjvz+978/evRoHMepVKqmpsYlhADMPwIWAADMvgsXLuzbt+8vf/lLX19fWVnZpUuX/vSnPx09erSioqJUKs3WWSYmJvL5fH9//+XLl8vKylKpVF1dnYAFwPwjYAEAwGyK43hiYqK/v/83v/nNO++8Mzk5WVZWNjIy0tXVNVu7tl9zuun1XOXl5VZgATAvCVgAADCb4jg+efLkH//4x3379o2MjFz9+9na+urjpNPp2trayspK7wIA84yABQAAs6lUKvX19e3bt69YLFZUVNyKVVcfd97Kysr6+noBC4D5R8ACAIBZE8dxFEVVVVXt7e2FQiHZl+q2nXfRokVtbW1VVVXeCADmmfJbvYwZAAAWjiQknT9/fmBg4OLFi8m+VLft1LlcbuXKlc3NzRUVFXEc37ZTA8CtJmABAMAsi6Io/j+387ypVKq8vPz2rPkCgNvJJYQAADCb4jj+7xPS1TcWvInHWnsFwDxjBRYAAIQliqLLly+PjY1VVVU1NDSoUQBgBRYAAIRiYmJibGxseHj40KFDPT09a9as2bFjh4AFAAIWAADMsWS3rFKpdObMma6urjfeeOOtt966ePFiOp3esWOH8QEAAQsAAOZYFEUXL148ePBgV1fX8ePH33777Xw+X1tbG0WRwQEAAQsAAObe5OTk2bNnDxw48M9//vPTn/50R0fHlStXSqWS+wkCQELAAgCAORZFUaFQ+OxnP/vggw9u2LDh8OHDzz77bH9/vxsuAUBCwAIAgDmWzWaXL1++ZMmSysrKqqqqbDZr7RUAXE3AAgCAuRTHcTqdrp9SVlY2MjJSKpWsvQKAq/mHHQAAmEvl5eXTPyfd6urfAAACFgAAAAChE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQBAQOI4jqIonpLcjjBhZABYyCoMAQAAzK0kUY1MGRoa6uvr+/e//z0+Pp7P53t7e+vr66urq2tqarLZrBsUArAwpX/84x8bBQAAmFvFYvHYsWNvTvnzn//8/vvvj4+PF4vFoaGhwcHBYrFYN0XAAmBhsgILAADmWBzHhUKhr6/v4MGDly5dymQyHR0dURRlMpmBgYHh4eEoitra2lpaWowVAAuTgAUAAHMvm83ee++9lZWVURRls9l0Ol1eXj45OTkxMVFWVtba2trY2GiUAFiwyu0HCQAAc2vmn8ldQgjAwmQFFgAAzKU4jmUpALi+lCEAAIA5pF4BwCcSsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABBE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQAAABA0AQsAAACAoAlYAAAAAARNwAIAAAAgaAIWAAAAAEETsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABBE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQAAABA0AQsAAACAoAlYAAAAAARNwAIAAAAgaAIWAAAAAEETsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABBE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQAAABA0AQsAAACAoAlYAAAAAARNwAIAAAAgaAIWAAAAAEETsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABBE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQAAABA0AQsAAACAoAlYAAAAAARNwAIAAAAgaAIWAAAAAEETsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABBE7AAAAAACJqABQAAAEDQBCwAAAAAgiZgAQAAABA0AQsAAACAoAlYAAAAAARNwAIAAAAgaAIWAAAAAEETsAAAAAAImoAFAAAAQNAELAAAAACCJmABAAAAEDQBCwAAAICgCVgAAAAABE3AAgAAACBoAhYAAAAAQROwAAAAAAiagAUAAABA0AQsAAAAAIImYAEAAAAQNAELAAAAgKAJWAAAAAAETcACAAAAIGgCFgAAAABB+98AAAD//3DcV35yu2O/AAAAAElFTkSuQmCC" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## RANDOM FOREST LEARNER\n", + "\n", + "### Overview\n", + "\n", + "![random_forest.png](images/random_forest.png) \n", + "Image via [src](https://cdn-images-1.medium.com/max/800/0*tG-IWcxL1jg7RkT0.png)\n", + "\n", + "#### Random Forest\n", + "\n", + "As the name of the algorithm and image above suggest, this algorithm creates the forest with a number of trees. The more number of trees makes the forest robust. In the same way in random forest algorithm, the higher the number of trees in the forest, the higher is the accuray result. The main difference between Random Forest and Decision trees is that, finding the root node and splitting the feature nodes will be random. \n", + "\n", + "Let's see how Rnadom Forest Algorithm work : \n", + "Random Forest Algorithm works in two steps, first is the creation of random forest and then the prediction. Let's first see the creation : \n", + "\n", + "The first step in creation is to randomly select 'm' features out of total 'n' features. From these 'm' features calculate the node d using the best split point and then split the node into further nodes using best split. Repeat these steps until 'i' number of nodes are reached. Repeat the entire whole process to build the forest. \n", + "\n", + "Now, let's see how the prediction works\n", + "Take the test features and predict the outcome for each randomly created decision tree. Calculate the votes for each prediction and the prediction which gets the highest votes would be the final prediction.\n", + "\n", + "\n", + "### Implementation\n", + "\n", + "Below mentioned is the implementation of Random Forest Algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(RandomForest)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This algorithm creates an ensemble of decision trees using bagging and feature bagging. It takes 'm' examples randomly from the total number of examples and then perform feature bagging with probability p to retain an attribute. All the predictors are predicted from the DecisionTreeLearner and then a final prediction is made.\n", + "\n", + "\n", + "### Example\n", + "\n", + "We will now use the Random Forest to classify a sample with values: 5.1, 3.0, 1.1, 0.1." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['versicolor', 'setosa', 'setosa', 'setosa', 'setosa']\n", + "setosa\n" + ] + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "\n", + "DTL = RandomForest(iris)\n", + "print(DTL([5.1, 3.0, 1.1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected, the Random Forest classifies the sample as \"setosa\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NAIVE BAYES LEARNER\n", + "\n", + "### Overview\n", + "\n", + "#### Theory of Probabilities\n", + "\n", + "The Naive Bayes algorithm is a probabilistic classifier, making use of [Bayes' Theorem](https://en.wikipedia.org/wiki/Bayes%27_theorem). The theorem states that the conditional probability of **A** given **B** equals the conditional probability of **B** given **A** multiplied by the probability of **A**, divided by the probability of **B**.\n", + "\n", + "$$P(A|B) = \\dfrac{P(B|A)*P(A)}{P(B)}$$\n", + "\n", + "From the theory of Probabilities we have the Multiplication Rule, if the events *X* are independent the following is true:\n", + "\n", + "$$P(X_{1} \\cap X_{2} \\cap ... \\cap X_{n}) = P(X_{1})*P(X_{2})*...*P(X_{n})$$\n", + "\n", + "For conditional probabilities this becomes:\n", + "\n", + "$$P(X_{1}, X_{2}, ..., X_{n}|Y) = P(X_{1}|Y)*P(X_{2}|Y)*...*P(X_{n}|Y)$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Classifying an Item\n", + "\n", + "How can we use the above to classify an item though?\n", + "\n", + "We have a dataset with a set of classes (**C**) and we want to classify an item with a set of features (**F**). Essentially what we want to do is predict the class of an item given the features.\n", + "\n", + "For a specific class, **Class**, we will find the conditional probability given the item features:\n", + "\n", + "$$P(Class|F) = \\dfrac{P(F|Class)*P(Class)}{P(F)}$$\n", + "\n", + "We will do this for every class and we will pick the maximum. This will be the class the item is classified in.\n", + "\n", + "The features though are a vector with many elements. We need to break the probabilities up using the multiplication rule. Thus the above equation becomes:\n", + "\n", + "$$P(Class|F) = \\dfrac{P(Class)*P(F_{1}|Class)*P(F_{2}|Class)*...*P(F_{n}|Class)}{P(F_{1})*P(F_{2})*...*P(F_{n})}$$\n", + "\n", + "The calculation of the conditional probability then depends on the calculation of the following:\n", + "\n", + "*a)* The probability of **Class** in the dataset.\n", + "\n", + "*b)* The conditional probability of each feature occurring in an item classified in **Class**.\n", + "\n", + "*c)* The probabilities of each individual feature.\n", + "\n", + "For *a)*, we will count how many times **Class** occurs in the dataset (aka how many items are classified in a particular class).\n", + "\n", + "For *b)*, if the feature values are discrete ('Blue', '3', 'Tall', etc.), we will count how many times a feature value occurs in items of each class. If the feature values are not discrete, we will go a different route. We will use a distribution function to calculate the probability of values for a given class and feature. If we know the distribution function of the dataset, then great, we will use it to compute the probabilities. If we don't know the function, we can assume the dataset follows the normal (Gaussian) distribution without much loss of accuracy. In fact, it can be proven that any distribution tends to the Gaussian the larger the population gets (see [Central Limit Theorem](https://en.wikipedia.org/wiki/Central_limit_theorem)).\n", + "\n", + "*NOTE:* If the values are continuous but use the discrete approach, there might be issues if we are not lucky. For one, if we have two values, '5.0 and 5.1', with the discrete approach they will be two completely different values, despite being so close. Second, if we are trying to classify an item with a feature value of '5.15', if the value does not appear for the feature, its probability will be 0. This might lead to misclassification. Generally, the continuous approach is more accurate and more useful, despite the overhead of calculating the distribution function.\n", + "\n", + "The last one, *c)*, is tricky. If feature values are discrete, we can count how many times they occur in the dataset. But what if the feature values are continuous? Imagine a dataset with a height feature. Is it worth it to count how many times each value occurs? Most of the time it is not, since there can be miscellaneous differences in the values (for example, 1.7 meters and 1.700001 meters are practically equal, but they count as different values).\n", + "\n", + "So as we cannot calculate the feature value probabilities, what are we going to do?\n", + "\n", + "Let's take a step back and rethink exactly what we are doing. We are essentially comparing conditional probabilities of all the classes. For two classes, **A** and **B**, we want to know which one is greater:\n", + "\n", + "$$\\dfrac{P(F|A)*P(A)}{P(F)} vs. \\dfrac{P(F|B)*P(B)}{P(F)}$$\n", + "\n", + "Wait, **P(F)** is the same for both the classes! In fact, it is the same for every combination of classes. That is because **P(F)** does not depend on a class, thus being independent of the classes.\n", + "\n", + "So, for *c)*, we actually don't need to calculate it at all." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Wrapping It Up\n", + "\n", + "Classifying an item to a class then becomes a matter of calculating the conditional probabilities of feature values and the probabilities of classes. This is something very desirable and computationally delicious.\n", + "\n", + "Remember though that all the above are true because we made the assumption that the features are independent. In most real-world cases that is not true though. Is that an issue here? Fret not, for the the algorithm is very efficient even with that assumption. That is why the algorithm is called **Naive** Bayes Classifier. We (naively) assume that the features are independent to make computations easier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "The implementation of the Naive Bayes Classifier is split in two; *Learning* and *Simple*. The *learning* classifier takes as input a dataset and learns the needed distributions from that. It is itself split into two, for discrete and continuous features. The *simple* classifier takes as input not a dataset, but already calculated distributions (a dictionary of `CountingProbDist` objects)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Discrete\n", + "\n", + "The implementation for discrete values counts how many times each feature value occurs for each class, and how many times each class occurs. The results are stored in a `CountinProbDist` object." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With the below code you can see the probabilities of the class \"Setosa\" appearing in the dataset and the probability of the first feature (at index 0) of the same class having a value of 5. Notice that the second probability is relatively small, even though if we observe the dataset we will find that a lot of values are around 5. The issue arises because the features in the Iris dataset are continuous, and we are assuming they are discrete. If the features were discrete (for example, \"Tall\", \"3\", etc.) this probably wouldn't have been the case and we would see a much nicer probability distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.3333333333333333\n", + "0.10588235294117647\n" + ] + } + ], + "source": [ + "dataset = iris\n", + "\n", + "target_vals = dataset.values[dataset.target]\n", + "target_dist = CountingProbDist(target_vals)\n", + "attr_dists = {(gv, attr): CountingProbDist(dataset.values[attr])\n", + " for gv in target_vals\n", + " for attr in dataset.inputs}\n", + "for example in dataset.examples:\n", + " targetval = example[dataset.target]\n", + " target_dist.add(targetval)\n", + " for attr in dataset.inputs:\n", + " attr_dists[targetval, attr].add(example[attr])\n", + "\n", + "\n", + "print(target_dist['setosa'])\n", + "print(attr_dists['setosa', 0][5.0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we found the different values for the classes (called targets here) and calculated their distribution. Next we initialized a dictionary of `CountingProbDist` objects, one for each class and feature. Finally, we iterated through the examples in the dataset and calculated the needed probabilites.\n", + "\n", + "Having calculated the different probabilities, we will move on to the predicting function. It will receive as input an item and output the most likely class. Using the above formula, it will multiply the probability of the class appearing, with the probability of each feature value appearing in the class. It will return the max result." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "setosa\n" + ] + } + ], + "source": [ + "def predict(example):\n", + " def class_probability(targetval):\n", + " return (target_dist[targetval] *\n", + " product(attr_dists[targetval, attr][example[attr]]\n", + " for attr in dataset.inputs))\n", + " return argmax(target_vals, key=class_probability)\n", + "\n", + "\n", + "print(predict([5, 3, 1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can view the complete code by executing the next line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(NaiveBayesDiscrete)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Continuous\n", + "\n", + "In the implementation we use the Gaussian/Normal distribution function. To make it work, we need to find the means and standard deviations of features for each class. We make use of the `find_means_and_deviations` Dataset function. On top of that, we will also calculate the class probabilities as we did with the Discrete approach." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[5.006, 3.418, 1.464, 0.244]\n", + "[0.5161711470638634, 0.3137983233784114, 0.46991097723995795, 0.19775268000454405]\n" + ] + } + ], + "source": [ + "means, deviations = dataset.find_means_and_deviations()\n", + "\n", + "target_vals = dataset.values[dataset.target]\n", + "target_dist = CountingProbDist(target_vals)\n", + "\n", + "\n", + "print(means[\"setosa\"])\n", + "print(deviations[\"versicolor\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see the means of the features for the \"Setosa\" class and the deviations for \"Versicolor\".\n", + "\n", + "The prediction function will work similarly to the Discrete algorithm. It will multiply the probability of the class occurring with the conditional probabilities of the feature values for the class.\n", + "\n", + "Since we are using the Gaussian distribution, we will input the value for each feature into the Gaussian function, together with the mean and deviation of the feature. This will return the probability of the particular feature value for the given class. We will repeat for each class and pick the max value." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "setosa\n" + ] + } + ], + "source": [ + "def predict(example):\n", + " def class_probability(targetval):\n", + " prob = target_dist[targetval]\n", + " for attr in dataset.inputs:\n", + " prob *= gaussian(means[targetval][attr], deviations[targetval][attr], example[attr])\n", + " return prob\n", + "\n", + " return argmax(target_vals, key=class_probability)\n", + "\n", + "\n", + "print(predict([5, 3, 1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The complete code of the continuous algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(NaiveBayesContinuous)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Simple\n", + "\n", + "The simple classifier (chosen with the argument `simple`) does not learn from a dataset, instead it takes as input a dictionary of already calculated `CountingProbDist` objects and returns a predictor function. The dictionary is in the following form: `(Class Name, Class Probability): CountingProbDist Object`.\n", + "\n", + "Each class has its own probability distribution. The classifier given a list of features calculates the probability of the input for each class and returns the max. The only pre-processing work is to create dictionaries for the distribution of classes (named `targets`) and attributes/features.\n", + "\n", + "The complete code for the simple classifier:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "psource(NaiveBayesSimple)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This classifier is useful when you already have calculated the distributions and you need to predict future items." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Examples\n", + "\n", + "We will now use the Naive Bayes Classifier (Discrete and Continuous) to classify items:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Discrete Classifier\n", + "setosa\n", + "setosa\n", + "setosa\n", + "\n", + "Continuous Classifier\n", + "setosa\n", + "versicolor\n", + "virginica\n" + ] + } + ], + "source": [ + "nBD = NaiveBayesLearner(iris, continuous=False)\n", + "print(\"Discrete Classifier\")\n", + "print(nBD([5, 3, 1, 0.1]))\n", + "print(nBD([6, 5, 3, 1.5]))\n", + "print(nBD([7, 3, 6.5, 2]))\n", + "\n", + "\n", + "nBC = NaiveBayesLearner(iris, continuous=True)\n", + "print(\"\\nContinuous Classifier\")\n", + "print(nBC([5, 3, 1, 0.1]))\n", + "print(nBC([6, 5, 3, 1.5]))\n", + "print(nBC([7, 3, 6.5, 2]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice how the Discrete Classifier misclassified the second item, while the Continuous one had no problem.\n", + "\n", + "Let's now take a look at the simple classifier. First we will come up with a sample problem to solve. Say we are given three bags. Each bag contains three letters ('a', 'b' and 'c') of different quantities. We are given a string of letters and we are tasked with finding from which bag the string of letters came.\n", + "\n", + "Since we know the probability distribution of the letters for each bag, we can use the naive bayes classifier to make our prediction." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bag1 = 'a'*50 + 'b'*30 + 'c'*15\n", + "dist1 = CountingProbDist(bag1)\n", + "bag2 = 'a'*30 + 'b'*45 + 'c'*20\n", + "dist2 = CountingProbDist(bag2)\n", + "bag3 = 'a'*20 + 'b'*20 + 'c'*35\n", + "dist3 = CountingProbDist(bag3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have the `CountingProbDist` objects for each bag/class, we will create the dictionary. We assume that it is equally probable that we will pick from any bag." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "dist = {('First', 0.5): dist1, ('Second', 0.3): dist2, ('Third', 0.2): dist3}\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can start making predictions:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First\n", + "Second\n", + "Third\n" + ] + } + ], + "source": [ + "print(nBS('aab')) # We can handle strings\n", + "print(nBS(['b', 'b'])) # And lists!\n", + "print(nBS('ccbcc'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results make intuitive sence. The first bag has a high amount of 'a's, the second has a high amount of 'b's and the third has a high amount of 'c's. The classifier seems to confirm this intuition.\n", + "\n", + "Note that the simple classifier doesn't distinguish between discrete and continuous values. It just takes whatever it is given. Also, the `simple` option on the `NaiveBayesLearner` overrides the `continuous` argument. `NaiveBayesLearner(d, simple=True, continuous=False)` just creates a simple classifier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PERCEPTRON CLASSIFIER\n", + "\n", + "### Overview\n", + "\n", + "The Perceptron is a linear classifier. It works the same way as a neural network with no hidden layers (just input and output). First it trains its weights given a dataset and then it can classify a new item by running it through the network.\n", + "\n", + "Its input layer consists of the the item features, while the output layer consists of nodes (also called neurons). Each node in the output layer has *n* synapses (for every item feature), each with its own weight. Then, the nodes find the dot product of the item features and the synapse weights. These values then pass through an activation function (usually a sigmoid). Finally, we pick the largest of the values and we return its index.\n", + "\n", + "Note that in classification problems each node represents a class. The final classification is the class/node with the max output value.\n", + "\n", + "Below you can see a single node/neuron in the outer layer. With *f* we denote the item features, with *w* the synapse weights, then inside the node we have the dot product and the activation function, *g*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![perceptron](images/perceptron.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "First, we train (calculate) the weights given a dataset, using the `BackPropagationLearner` function of `learning.py`. We then return a function, `predict`, which we will use in the future to classify a new item. The function computes the (algebraic) dot product of the item with the calculated weights for each node in the outer layer. Then it picks the greatest value and classifies the item in the corresponding class." + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "class kNN_learner:\n", - " \"Simple kNN learner with manhattan distance\"\n", - " def __init__(self):\n", - " pass\n", - " \n", - " def train(self, train_img, train_lbl):\n", - " self.train_img = train_img\n", - " self.train_lbl = train_lbl\n", + "psource(PerceptronLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the Perceptron is a one-layer neural network, without any hidden layers. So, in `BackPropagationLearner`, we will pass no hidden layers. From that function we get our network, which is just one layer, with the weights calculated.\n", "\n", - " def predict_labels(self, test_img, k=1, distance=\"manhattan\"):\n", - " if distance == \"manhattan\": \n", - " distances = self.compute_manhattan_distances(test_img)\n", - " num_test = distances.shape[0]\n", - " predictions = np.zeros(num_test, dtype=np.uint8)\n", - " \n", - " for i in range(num_test):\n", - " k_best_labels = self.train_lbl[np.argsort(distances[i])].flatten()[:k]\n", - " predictions[i] = mode(k_best_labels)\n", - " \n", - " return predictions\n", - " \n", - " def compute_manhattan_distances(self, test_img):\n", - " num_test = test_img.shape[0]\n", - " num_train = self.train_img.shape[0]\n", - "# print(num_test, num_train)\n", - " \n", - " dists = np.zeros((num_test, num_train))\n", - " \n", - " for i in range(num_test):\n", - " dists[i] = np.sum(abs(self.train_img - test_img[i]), axis = 1)\n", - " \n", - " return(dists)\n", - " " + "That function `predict` passes the input/example through the network, calculating the dot product of the input and the weights for each node and returns the class with the max dot product." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's print the shapes of data to make sure everything's on track." + "### Example\n", + "\n", + "We will train the Perceptron on the iris dataset. Because though the `BackPropagationLearner` works with integer indexes and not strings, we need to convert class names to integers. Then, we will try and classify the item/flower with measurements of 5, 3, 1, 0.1." ] }, { "cell_type": "code", - "execution_count": 19, - "metadata": { - "collapsed": false - }, + "execution_count": 38, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Training images size: (60000, 784)\n", - "Training labels size: (60000,)\n", - "Testing images size: (10000, 784)\n", - "Training labels size: (10000,)\n" + "0\n" ] } ], "source": [ - "print(\"Training images size:\", train_img.shape)\n", - "print(\"Training labels size:\", train_lbl.shape)\n", - "print(\"Testing images size:\", test_img.shape)\n", - "print(\"Training labels size:\", test_lbl.shape)" + "iris = DataSet(name=\"iris\")\n", + "iris.classes_to_numbers()\n", + "\n", + "perceptron = PerceptronLearner(iris)\n", + "print(perceptron([5, 3, 1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The correct output is 0, which means the item belongs in the first class, \"setosa\". Note that the Perceptron algorithm is not perfect and may produce false classifications." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LINEAR LEARNER\n", + "\n", + "### Overview\n", + "\n", + "Linear Learner is a model that assumes a linear relationship between the input variables x and the single output variable y. More specifically, that y can be calculated from a linear combination of the input variables x. Linear learner is a quite simple model as the representation of this model is a linear equation. \n", + "\n", + "The linear equation assigns one scaler factor to each input value or column, called a coefficients or weights. One additional coefficient is also added, giving additional degree of freedom and is often called the intercept or the bias coefficient. \n", + "For example : y = ax1 + bx2 + c . \n", + "\n", + "### Implementation\n", + "\n", + "Below mentioned is the implementation of Linear Learner." ] }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ - "learner = kNN_learner()\n", - "learner.train(train_img, train_lbl)" + "psource(LinearLearner)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let us predict the classes of first 100 test images." + "This algorithm first assigns some random weights to the input variables and then based on the error calculated updates the weight for each variable. Finally the prediction is made with the updated weights. \n", + "\n", + "### Implementation\n", + "\n", + "We will now use the Linear Learner to classify a sample with values: 5.1, 3.0, 1.1, 0.1." ] }, { "cell_type": "code", "execution_count": 21, - "metadata": { - "collapsed": false - }, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.2404650656510341\n" + ] + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "iris.classes_to_numbers()\n", + "\n", + "linear_learner = LinearLearner(iris)\n", + "print(linear_learner([5, 3, 1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ENSEMBLE LEARNER\n", + "\n", + "### Overview\n", + "\n", + "Ensemble Learning improves the performance of our model by combining several learners. It improvise the stability and predictive power of the model. Ensemble methods are meta-algorithms that combine several machine learning techniques into one predictive model in order to decrease variance, bias, or improve predictions. \n", + "\n", + "\n", + "\n", + "![ensemble_learner.jpg](images/ensemble_learner.jpg)\n", + "\n", + "\n", + "Some commonly used Ensemble Learning techniques are : \n", + "\n", + "1. Bagging : Bagging tries to implement similar learners on small sample populations and then takes a mean of all the predictions. It helps us to reduce variance error.\n", + "\n", + "2. Boosting : Boosting is an iterative technique which adjust the weight of an observation based on the last classification. If an observation was classified incorrectly, it tries to increase the weight of this observation and vice versa. It helps us to reduce bias error.\n", + "\n", + "3. Stacking : This is a very interesting way of combining models. Here we use a learner to combine output from different learners. It can either decrease bias or variance error depending on the learners we use.\n", + "\n", + "### Implementation\n", + "\n", + "Below mentioned is the implementation of Ensemble Learner." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ - "# takes ~17 Secs. to execute this cell\n", - "num_test = 100\n", - "predictions = learner.predict_labels(test_img[:num_test], k=3)" + "psource(EnsembleLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This algorithm takes input as a list of learning algorithms, have them vote and then finally returns the predicted result." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's compare the performances of both implementations. It took 20 Secs. to predict one image using our native implementations and 17 Secs. to predict 100 images in faster implementations. That's 110 times faster.\n", + "## LEARNER EVALUATION\n", "\n", - "Now, test the accuracy of our predictions:" + "In this section we will evaluate and compare algorithm performance. The dataset we will use will again be the iris one." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 42, "metadata": { - "collapsed": false + "collapsed": true }, + "outputs": [], + "source": [ + "iris = DataSet(name=\"iris\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Naive Bayes\n", + "\n", + "First up we have the Naive Bayes algorithm. First we will test how well the Discrete Naive Bayes works, and then how the Continuous fares." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error ratio for Discrete: 0.040000000000000036\n", + "Error ratio for Continuous: 0.040000000000000036\n" + ] + } + ], + "source": [ + "nBD = NaiveBayesLearner(iris, continuous=False)\n", + "print(\"Error ratio for Discrete:\", err_ratio(nBD, iris))\n", + "\n", + "nBC = NaiveBayesLearner(iris, continuous=True)\n", + "print(\"Error ratio for Continuous:\", err_ratio(nBC, iris))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The error for the Naive Bayes algorithm is very, very low; close to 0. There is also very little difference between the discrete and continuous version of the algorithm." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## k-Nearest Neighbors\n", + "\n", + "Now we will take a look at kNN, for different values of *k*. Note that *k* should have odd values, to break any ties between two classes." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Accuracy of predictions: 98.0 %\n" + "Error ratio for k=1: 0.0\n", + "Error ratio for k=3: 0.06000000000000005\n", + "Error ratio for k=5: 0.1266666666666667\n", + "Error ratio for k=7: 0.19999999999999996\n" ] } ], "source": [ - "# print(predictions)\n", - "# print(test_lbl[:num_test])\n", + "kNN_1 = NearestNeighborLearner(iris, k=1)\n", + "kNN_3 = NearestNeighborLearner(iris, k=3)\n", + "kNN_5 = NearestNeighborLearner(iris, k=5)\n", + "kNN_7 = NearestNeighborLearner(iris, k=7)\n", "\n", - "num_correct = np.sum([predictions == test_lbl[:num_test]])\n", - "num_accuracy = (float(num_correct) / num_test) * 100\n", - "print(\"Accuracy of predictions:\", num_accuracy, \"%\")" + "print(\"Error ratio for k=1:\", err_ratio(kNN_1, iris))\n", + "print(\"Error ratio for k=3:\", err_ratio(kNN_3, iris))\n", + "print(\"Error ratio for k=5:\", err_ratio(kNN_5, iris))\n", + "print(\"Error ratio for k=7:\", err_ratio(kNN_7, iris))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Introduction to Scikit-Learn\n", + "Notice how the error became larger and larger as *k* increased. This is generally the case with datasets where classes are spaced out, as is the case with the iris dataset. If items from different classes were closer together, classification would be more difficult. Usually a value of 1, 3 or 5 for *k* suffices.\n", "\n", - "In this section we will solve this MNIST problem using Scikit-Learn. Learn more about Scikit-Learn [here](http://scikit-learn.org/stable/index.html). As we are using this library, we don't need to define our own functions (kNN or Support Vector Machines aka SVMs) to classify digits.\n", + "Also note that since the training set is also the testing set, for *k* equal to 1 we get a perfect score, since the item we want to classify each time is already in the dataset and its closest neighbor is itself." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perceptron\n", "\n", - "Let's start by importing necessary modules for kNN and SVM." + "For the Perceptron, we first need to convert class names to integers. Let's see how it performs in the dataset." ] }, { "cell_type": "code", - "execution_count": 41, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error ratio for Perceptron: 0.31333333333333335\n" + ] + } + ], + "source": [ + "iris2 = DataSet(name=\"iris\")\n", + "iris2.classes_to_numbers()\n", + "\n", + "perceptron = PerceptronLearner(iris2)\n", + "print(\"Error ratio for Perceptron:\", err_ratio(perceptron, iris2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Perceptron didn't fare very well mainly because the dataset is not linearly separated. On simpler datasets the algorithm performs much better, but unfortunately such datasets are rare in real life scenarios." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AdaBoost\n", + "\n", + "### Overview\n", + "\n", + "**AdaBoost** is an algorithm which uses **ensemble learning**. In ensemble learning the hypotheses in the collection, or ensemble, vote for what the output should be and the output with the majority votes is selected as the final answer.\n", + "\n", + "AdaBoost algorithm, as mentioned in the book, works with a **weighted training set** and **weak learners** (classifiers that have about 50%+epsilon accuracy i.e slightly better than random guessing). It manipulates the weights attached to the the examples that are showed to it. Importance is given to the examples with higher weights.\n", + "\n", + "All the examples start with equal weights and a hypothesis is generated using these examples. Examples which are incorrectly classified, their weights are increased so that they can be classified correctly by the next hypothesis. The examples that are correctly classified, their weights are reduced. This process is repeated *K* times (here *K* is an input to the algorithm) and hence, *K* hypotheses are generated.\n", + "\n", + "These *K* hypotheses are also assigned weights according to their performance on the weighted training set. The final ensemble hypothesis is the weighted-majority combination of these *K* hypotheses.\n", + "\n", + "The speciality of AdaBoost is that by using weak learners and a sufficiently large *K*, a highly accurate classifier can be learned irrespective of the complexity of the function being learned or the dullness of the hypothesis space." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "from sklearn.neighbors import NearestNeighbors\n", - "from sklearn import svm" + "### Implementation\n", + "\n", + "As seen in the previous section, the `PerceptronLearner` does not perform that well on the iris dataset. We'll use perceptron as the learner for the AdaBoost algorithm and try to increase the accuracy. \n", + "\n", + "Let's first see what AdaBoost is exactly:" ] }, { "cell_type": "code", - "execution_count": 42, - "metadata": { - "collapsed": false - }, + "execution_count": 3, + "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def AdaBoost(L, K):\n",
    +       "    """[Figure 18.34]"""\n",
    +       "    def train(dataset):\n",
    +       "        examples, target = dataset.examples, dataset.target\n",
    +       "        N = len(examples)\n",
    +       "        epsilon = 1. / (2 * N)\n",
    +       "        w = [1. / N] * N\n",
    +       "        h, z = [], []\n",
    +       "        for k in range(K):\n",
    +       "            h_k = L(dataset, w)\n",
    +       "            h.append(h_k)\n",
    +       "            error = sum(weight for example, weight in zip(examples, w)\n",
    +       "                        if example[target] != h_k(example))\n",
    +       "            # Avoid divide-by-0 from either 0% or 100% error rates:\n",
    +       "            error = clip(error, epsilon, 1 - epsilon)\n",
    +       "            for j, example in enumerate(examples):\n",
    +       "                if example[target] == h_k(example):\n",
    +       "                    w[j] *= error / (1. - error)\n",
    +       "            w = normalize(w)\n",
    +       "            z.append(math.log((1. - error) / error))\n",
    +       "        return WeightedMajority(h, z)\n",
    +       "    return train\n",
    +       "
    \n", + "\n", + "\n" + ], "text/plain": [ - "LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,\n", - " intercept_scaling=1, loss='squared_hinge', max_iter=1000,\n", - " multi_class='ovr', penalty='l2', random_state=None, tol=0.0001,\n", - " verbose=0)" + "" ] }, - "execution_count": 42, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "# takes ~3 mins to execute the cell\n", - "SVMclf = svm.LinearSVC()\n", - "SVMclf.fit(train_img, train_lbl)" + "psource(AdaBoost)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "AdaBoost takes as inputs: **L** and *K* where **L** is the learner and *K* is the number of hypotheses to be generated. The learner **L** takes in as inputs: a dataset and the weights associated with the examples in the dataset. But the `PerceptronLearner` doesnot handle weights and only takes a dataset as its input. \n", + "To remedy that we will give as input to the PerceptronLearner a modified dataset in which the examples will be repeated according to the weights associated to them. Intuitively, what this will do is force the learner to repeatedly learn the same example again and again until it can classify it correctly. \n", + "\n", + "To convert `PerceptronLearner` so that it can take weights as input too, we will have to pass it through the **`WeightedLearner`** function." ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "predictions = SVMclf.predict(test_img)" + "psource(WeightedLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `WeightedLearner` function will then call the `PerceptronLearner`, during each iteration, with the modified dataset which contains the examples according to the weights associated with them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "We will pass the `PerceptronLearner` through `WeightedLearner` function. Then we will create an `AdaboostLearner` classifier with number of hypotheses or *K* equal to 5." ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 4, "metadata": { - "collapsed": false + "collapsed": true }, + "outputs": [], + "source": [ + "WeightedPerceptron = WeightedLearner(PerceptronLearner)\n", + "AdaboostLearner = AdaBoost(WeightedPerceptron, 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Accuracy of predictions: 88.25 %\n" - ] + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "num_correct = np.sum(predictions == test_lbl)\n", - "num_accuracy = (float(num_correct)/len(test_lbl)) * 100\n", - "print(\"Accuracy of predictions:\", num_accuracy, \"%\")" + "iris2 = DataSet(name=\"iris\")\n", + "iris2.classes_to_numbers()\n", + "\n", + "adaboost = AdaboostLearner(iris2)\n", + "\n", + "adaboost([5, 3, 1, 0.1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You might observe that this accuracy is far less than what we got using native kNN implementation. But we can tweak the parameters to get higher accuracy on this problem which we are going to explain in coming sections." + "That is the correct answer. Let's check the error rate of adaboost with perceptron." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error ratio for adaboost: 0.046666666666666634\n" + ] + } + ], + "source": [ + "print(\"Error ratio for adaboost: \", err_ratio(adaboost, iris2))" + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": true }, - "outputs": [], - "source": [] + "source": [ + "It reduced the error rate considerably. Unlike the `PerceptronLearner`, `AdaBoost` was able to learn the complexity in the iris dataset." + ] } ], "metadata": { @@ -924,13 +2247,18 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.5.2" }, - "widgets": { - "state": {}, - "version": "1.1.1" + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "source": [], + "metadata": { + "collapsed": false + } + } } }, "nbformat": 4, - "nbformat_minor": 0 -} + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/learning.py b/learning.py index 0894b2190..71b6b15e7 100644 --- a/learning.py +++ b/learning.py @@ -1,77 +1,48 @@ -"""Learn to estimate functions from examples. (Chapters 18-20)""" - -from utils import ( - removeall, unique, product, argmax, argmax_random_tie, isclose, - dotproduct, vector_add, scalar_vector_product, weighted_sample_with_replacement, - weighted_sampler, num_or_str, normalize, clip, sigmoid, print_table, DataFile -) +"""Learning from examples (Chapters 18)""" import copy -import heapq -import math -import random - -# XXX statistics.mode is not quite the same as the old utils.mode: -# it insists on there being a unique most-frequent value. Code using mode -# needs to be revisited, or we need to restore utils.mode. -from statistics import mean, mode from collections import defaultdict +from statistics import stdev -# ______________________________________________________________________________ - - -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)]) - +from qpsolvers import solve_qp -def mean_error(predictions, targets): - return mean([abs(p - t) for p, t in zip(predictions, targets)]) - - -def manhattan_distance(predictions, targets): - return sum([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)]) - -# ______________________________________________________________________________ +from probabilistic_learning import NaiveBayesLearner +from utils import * class DataSet: + """ + A data set for a machine learning problem. It has the following fields: - """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. + d.examples A list of examples. Each one is a list of attribute values. d.attrs A list of integers to index into an example, so example[attr] gives a value. Normally the same as range(len(d.examples[0])). - d.attrnames Optional list of mnemonic names for corresponding attrs. + d.attr_names Optional list of mnemonic names for corresponding attrs. d.target The attribute that a learning algorithm will try to predict. By default the final attribute. d.inputs The list of attrs without the target. d.values A list of lists: each sublist is the set of possible values for the corresponding attribute. If initially None, - it is computed from the known examples by self.setproblem. + it is computed from the known examples by self.set_problem. If not None, an erroneous value raises ValueError. - d.distance A function from a pair of examples to a nonnegative number. + d.distance A function from a pair of examples to a non-negative number. Should be symmetric, etc. Defaults to mean_boolean_error since that can handle any field types. d.name Name of the data set (for output display only). d.source URL or other source where the data came from. + d.exclude A list of attribute indexes to exclude from d.inputs. Elements + of this list can either be integers (attrs) or attr_names. Normally, you call the constructor and you're done; then you just - access fields like d.examples and d.target and d.inputs.""" + access fields like d.examples and d.target and d.inputs. + """ - def __init__(self, examples=None, attrs=None, attrnames=None, target=-1, - inputs=None, values=None, distance=mean_boolean_error, - name='', source='', exclude=()): - """Accepts any of DataSet's fields. Examples can also be a + def __init__(self, examples=None, attrs=None, attr_names=None, target=-1, inputs=None, + values=None, distance=mean_boolean_error, name='', source='', exclude=()): + """ + Accepts any of DataSet's fields. Examples can also be a string or file from which to parse examples using parse_csv. - Optional parameter: exclude, as documented in .setproblem(). + Optional parameter: exclude, as documented in .set_problem(). >>> DataSet(examples='1, 2, 3') """ @@ -79,49 +50,50 @@ def __init__(self, examples=None, attrs=None, attrnames=None, target=-1, self.source = source self.values = values self.distance = distance - if values is None: - self.got_values_flag = False - else: - self.got_values_flag = True + self.got_values_flag = bool(values) - # Initialize .examples from string or list or data directory + # initialize .examples from string or list or data directory if isinstance(examples, str): self.examples = parse_csv(examples) elif examples is None: - self.examples = parse_csv(DataFile(name + '.csv').read()) + self.examples = parse_csv(open_data(name + '.csv').read()) else: self.examples = examples - # Attrs are the indices of examples, unless otherwise stated. - if attrs is None and self.examples is not None: + + # attrs are the indices of examples, unless otherwise stated. + if self.examples is not None and attrs is None: attrs = list(range(len(self.examples[0]))) + self.attrs = attrs - # Initialize .attrnames from string, list, or by default - if isinstance(attrnames, str): - self.attrnames = attrnames.split() + + # initialize .attr_names from string, list, or by default + if isinstance(attr_names, str): + self.attr_names = attr_names.split() else: - self.attrnames = attrnames or attrs - self.setproblem(target, inputs=inputs, exclude=exclude) + self.attr_names = attr_names or attrs + self.set_problem(target, inputs=inputs, exclude=exclude) - def setproblem(self, target, inputs=None, exclude=()): - """Set (or change) the target and/or inputs. + def set_problem(self, target, inputs=None, exclude=()): + """ + Set (or change) the target and/or inputs. This way, one DataSet can be used multiple ways. inputs, if specified, is a list of attributes, or specify exclude as a list of attributes - 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) + to not use in inputs. Attributes can be -n .. n, or an attr_name. + Also computes the list of possible values, if that wasn't done yet. + """ + self.target = self.attr_num(target) + exclude = list(map(self.attr_num, exclude)) if inputs: - self.inputs = removeall(self.target, inputs) + self.inputs = remove_all(self.target, inputs) else: - self.inputs = [a for a in self.attrs - if a != self.target and a not in exclude] + self.inputs = [a for a in self.attrs if a != self.target and a not in exclude] if not self.values: - self.values = list(map(unique, zip(*self.examples))) + self.update_values() self.check_me() def check_me(self): - "Check that my fields make sense." - assert len(self.attrnames) == len(self.attrs) + """Check that my fields make sense.""" + assert len(self.attr_names) == len(self.attrs) assert self.target in self.attrs assert self.target not in self.inputs assert set(self.inputs).issubset(set(self.attrs)) @@ -130,42 +102,96 @@ def check_me(self): list(map(self.check_example, self.examples)) def add_example(self, example): - "Add an example to the list of examples, checking it first." + """Add an example to the list of examples, checking it first.""" self.check_example(example) self.examples.append(example) def check_example(self, example): - "Raise ValueError if example has any invalid values." + """Raise ValueError if example has any invalid values.""" if self.values: for a in self.attrs: if example[a] not in self.values[a]: - raise ValueError('Bad value %s for attribute %s in %s' % - (example[a], self.attrnames[a], example)) + raise ValueError('Bad value {} for attribute {} in {}' + .format(example[a], self.attr_names[a], example)) - def attrnum(self, attr): - "Returns the number used for attr, which can be a name, or -n .. n-1." + def attr_num(self, attr): + """Returns the number used for attr, which can be a name, or -n .. n-1.""" if isinstance(attr, str): - return self.attrnames.index(attr) + return self.attr_names.index(attr) elif attr < 0: return len(self.attrs) + attr else: return attr + def update_values(self): + self.values = list(map(unique, zip(*self.examples))) + 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 classes_to_numbers(self, classes=None): + """Converts class names to numbers.""" + if not classes: + # if classes were not given, extract them from values + classes = sorted(self.values[self.target]) + for item in self.examples: + item[self.target] = classes.index(item[self.target]) + + def remove_examples(self, value=''): + """Remove examples that contain given value.""" + self.examples = [x for x in self.examples if value not in x] + self.update_values() + + def split_values_by_classes(self): + """Split values into buckets according to their class.""" + buckets = defaultdict(lambda: []) + target_names = self.values[self.target] + + for v in self.examples: + item = [a for a in v if a not in target_names] # remove target from item + buckets[v[self.target]].append(item) # add item to bucket of its class + + return buckets + + def find_means_and_deviations(self): + """ + Finds the means and standard deviations of self.dataset. + means : a dictionary for each class/target. Holds a list of the means + of the features for the class. + deviations: a dictionary for each class/target. Holds a list of the sample + standard deviations of the features for the class. + """ + target_names = self.values[self.target] + feature_numbers = len(self.inputs) - def __repr__(self): - return '' % ( - self.name, len(self.examples), len(self.attrs)) + item_buckets = self.split_values_by_classes() + + means = defaultdict(lambda: [0] * feature_numbers) + deviations = defaultdict(lambda: [0] * feature_numbers) + + for t in target_names: + # find all the item feature values for item in class t + features = [[] for _ in range(feature_numbers)] + for item in item_buckets[t]: + for i in range(feature_numbers): + features[i].append(item[i]) -# ______________________________________________________________________________ + # calculate means and deviations fo the class + for i in range(feature_numbers): + means[t][i] = mean(features[i]) + deviations[t][i] = stdev(features[i]) + + return means, deviations + + def __repr__(self): + return ''.format(self.name, len(self.examples), len(self.attrs)) 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. + 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. Fields that look like numbers are converted to numbers. The delim defaults to ',' but '\t' and None are also reasonable values. >>> parse_csv('1, 2, 3 \n 0, 2, na') @@ -174,156 +200,183 @@ def parse_csv(input, delim=','): lines = [line for line in input.splitlines() if line.strip()] return [list(map(num_or_str, line.split(delim))) for line in lines] -# ______________________________________________________________________________ +def err_ratio(predict, dataset, examples=None): + """ + Return the proportion of the examples that are NOT correctly predicted. + verbose - 0: No output; 1: Output wrong; 2 (or greater): Output correct + """ + examples = examples or dataset.examples + if len(examples) == 0: + return 0.0 + right = 0 + for example in examples: + desired = example[dataset.target] + output = predict(dataset.sanitize(example)) + if output == desired: + right += 1 + return 1 - (right / len(examples)) -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: - p.add(o) increments the count for observation o by 1. - p.sample() returns a random element from the distribution. - p[o] returns the probability for o (as in a regular ProbDist).""" - def __init__(self, observations=[], default=0): - """Create a distribution, and optionally add in some observations. - By default this is an unsmoothed distribution, but saying default=1, - for example, gives you add-one smoothing.""" - self.dictionary = {} - self.n_obs = 0.0 - self.default = default - self.sampler = None +def grade_learner(predict, tests): + """ + Grades the given learner based on how many tests it passes. + tests is a list with each element in the form: (values, output). + """ + return mean(int(predict(X) == y) for X, y in tests) - for o in observations: - self.add(o) - def add(self, o): - "Add an observation o to the distribution." - self.smooth_for(o) - self.dictionary[o] += 1 - self.n_obs += 1 - self.sampler = None +def train_test_split(dataset, start=None, end=None, test_split=None): + """ + If you are giving 'start' and 'end' as parameters, + then it will return the testing set from index 'start' to 'end' + and the rest for training. + If you give 'test_split' as a parameter then it will return + test_split * 100% as the testing set and the rest as + training set. + """ + examples = dataset.examples + if test_split is None: + train = examples[:start] + examples[end:] + val = examples[start:end] + else: + total_size = len(examples) + val_size = int(total_size * test_split) + train_size = total_size - val_size + train = examples[:train_size] + val = examples[train_size:total_size] - def smooth_for(self, o): - """Include o among the possible observations, whether or not - it's been observed yet.""" - if o not in self.dictionary: - self.dictionary[o] = self.default - self.n_obs += self.default - self.sampler = None + return train, val - def __getitem__(self, item): - "Return an estimate of the probability of item." - self.smooth_for(item) - return self.dictionary[item] / self.n_obs - # (top() and sample() are not used in this module, but elsewhere.) +def cross_validation_wrapper(learner, dataset, k=10, trials=1): + """ + [Figure 18.8] + Return the optimal value of size having minimum error on validation set. + errT: a training error array, indexed by size + errV: a validation error array, indexed by size + """ + errs = [] + size = 1 + while True: + errT, errV = cross_validation(learner, dataset, size, k, trials) + # check for convergence provided err_val is not empty + if errT and not np.isclose(errT[-1], errT, rtol=1e-6): + best_size = 0 + min_val = np.inf + i = 0 + while i < size: + if errs[i] < min_val: + min_val = errs[i] + best_size = i + i += 1 + return learner(dataset, best_size) + errs.append(errV) + size += 1 - 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()]) - def sample(self): - "Return a random sample from the distribution." - if self.sampler is None: - self.sampler = weighted_sampler(list(self.dictionary.keys()), - list(self.dictionary.values())) - return self.sampler() +def cross_validation(learner, dataset, size=None, 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. + Shuffle the examples first; if trials > 1, average over several shuffles. + Returns Training error, Validation error + """ + k = k or len(dataset.examples) + if trials > 1: + trial_errT = 0 + trial_errV = 0 + for t in range(trials): + errT, errV = cross_validation(learner, dataset, size, k, trials) + trial_errT += errT + trial_errV += errV + return trial_errT / trials, trial_errV / trials + else: + fold_errT = 0 + fold_errV = 0 + n = len(dataset.examples) + examples = dataset.examples + random.shuffle(dataset.examples) + for fold in range(k): + train_data, val_data = train_test_split(dataset, fold * (n // k), (fold + 1) * (n // k)) + dataset.examples = train_data + h = learner(dataset, size) + fold_errT += err_ratio(h, dataset, train_data) + fold_errV += err_ratio(h, dataset, val_data) + # reverting back to original once test is completed + dataset.examples = examples + return fold_errT / k, fold_errV / k -# ______________________________________________________________________________ +def leave_one_out(learner, dataset, size=None): + """Leave one out cross-validation over the dataset.""" + return cross_validation(learner, dataset, size, len(dataset.examples)) -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 - return predict +def learning_curve(learner, dataset, trials=10, sizes=None): + if sizes is None: + sizes = list(range(2, len(dataset.examples) - trials, 2)) -# ______________________________________________________________________________ + def score(learner, size): + random.shuffle(dataset.examples) + return cross_validation(learner, dataset, size, trials) + return [(size, mean([score(learner, size) for _ in range(trials)])) for size in sizes] -def NaiveBayesLearner(dataset): - """Just count how many times each value of each input attribute - occurs, conditional on the target value. Count the different - target values too.""" - targetvals = dataset.values[dataset.target] - target_dist = CountingProbDist(targetvals) - attr_dists = {(gv, attr): CountingProbDist(dataset.values[attr]) - for gv in targetvals - for attr in dataset.inputs} - for example in dataset.examples: - targetval = example[dataset.target] - target_dist.add(targetval) - for attr in dataset.inputs: - attr_dists[targetval, attr].add(example[attr]) +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): - """Predict the target value for example. Consider each possible value, - and pick the most likely by looking at each attribute independently.""" - def class_probability(targetval): - return (target_dist[targetval] * - product(attr_dists[targetval, attr][example[attr]] - for attr in dataset.inputs)) - return argmax(targetvals, key=class_probability) - - return predict - -# ______________________________________________________________________________ - + """Always return same result: the most popular from the training set.""" + return most_popular -def NearestNeighborLearner(dataset, k=1): - "k-NearestNeighbor: the k nearest neighbors vote." - def predict(example): - "Find the k closest, and have them vote for the best." - best = heapq.nsmallest(k, ((dataset.distance(e, example), e) - for e in dataset.examples)) - return mode(e[dataset.target] for (d, e) in best) return predict -# ______________________________________________________________________________ - 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. + """ - """A fork of a decision tree holds an attribute to test, and a dict - of branches, one for each of the attribute's values.""" - - def __init__(self, attr, attrname=None, branches=None): - "Initialize by saying what attribute this node tests." + def __init__(self, attr, attr_name=None, default_child=None, branches=None): + """Initialize by saying what attribute this node tests.""" self.attr = attr - self.attrname = attrname or attr + self.attr_name = attr_name or attr + self.default_child = default_child self.branches = branches or {} def __call__(self, example): - "Given an example, classify it using the attribute and the branches." - attrvalue = example[self.attr] - return self.branches[attrvalue](example) + """Given an example, classify it using the attribute and the branches.""" + attr_val = example[self.attr] + if attr_val in self.branches: + return self.branches[attr_val](example) + else: + # return default class when attribute is unknown + return self.default_child(example) def add(self, val, subtree): - "Add a branch. If self.attr = val, go to the given subtree." + """Add a branch. If self.attr = val, go to the given subtree.""" self.branches[val] = subtree def display(self, indent=0): - name = self.attrname + name = self.attr_name print('Test', name) for (val, subtree) in 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)) + return 'DecisionFork({0!r}, {1!r}, {2!r})'.format(self.attr, self.attr_name, self.branches) class DecisionLeaf: - - "A leaf of a decision tree holds just a result." + """A leaf of a decision tree holds just a result.""" def __init__(self, result): self.result = result @@ -331,408 +384,720 @@ def __init__(self, result): def __call__(self, example): return self.result - def display(self, indent=0): + def display(self): print('RESULT =', self.result) def __repr__(self): return repr(self.result) -# ______________________________________________________________________________ - def DecisionTreeLearner(dataset): - "[Figure 18.5]" + """[Figure 18.5]""" target, values = dataset.target, dataset.values def decision_tree_learning(examples, attrs, parent_examples=()): if len(examples) == 0: return plurality_value(parent_examples) - elif all_same_class(examples): + if all_same_class(examples): return DecisionLeaf(examples[0][target]) - elif len(attrs) == 0: + if len(attrs) == 0: return plurality_value(examples) - else: - A = choose_attribute(attrs, examples) - tree = DecisionFork(A, dataset.attrnames[A]) - for (v_k, exs) in split_by(A, examples): - subtree = decision_tree_learning( - exs, removeall(A, attrs), examples) - tree.add(v_k, subtree) - return tree + A = choose_attribute(attrs, examples) + tree = DecisionFork(A, dataset.attr_names[A], plurality_value(examples)) + for (v_k, exs) in split_by(A, examples): + subtree = decision_tree_learning(exs, remove_all(A, attrs), examples) + tree.add(v_k, subtree) + return tree def plurality_value(examples): - """Return the most popular target value for this set of examples. - (If target is binary, this is the majority; otherwise plurality.)""" - popular = argmax_random_tie(values[target], - key=lambda v: count(target, v, examples)) + """ + Return the most popular target value for this set of examples. + (If target is binary, this is the majority; otherwise plurality). + """ + popular = argmax_random_tie(values[target], key=lambda v: count(target, v, examples)) return DecisionLeaf(popular) def count(attr, val, examples): - "Count the number of examples that have attr = val." - return count(e[attr] == val for e in examples) + """Count the number of examples that have example[attr] = val.""" + return sum(e[attr] == val for e in examples) def all_same_class(examples): - "Are all these examples in the same target class?" + """Are all these examples in the same target class?""" class0 = examples[0][target] return all(e[target] == class0 for e in examples) def choose_attribute(attrs, examples): - "Choose the attribute with the highest information gain." - return argmax_random_tie(attrs, - key=lambda a: information_gain(a, examples)) + """Choose the attribute with the highest information gain.""" + return argmax_random_tie(attrs, key=lambda a: information_gain(a, examples)) def information_gain(attr, examples): - "Return the expected reduction in entropy from splitting by attr." + """Return the expected reduction in entropy from splitting by attr.""" + def I(examples): - return information_content([count(target, v, examples) - for v in values[target]]) - N = float(len(examples)) - remainder = sum((len(examples_i) / N) * I(examples_i) - for (v, examples_i) in split_by(attr, examples)) + return information_content([count(target, v, examples) for v in values[target]]) + + n = len(examples) + remainder = sum((len(examples_i) / n) * I(examples_i) for (v, examples_i) in split_by(attr, examples)) return I(examples) - remainder def split_by(attr, examples): - "Return a list of (val, examples) pairs for each val of attr." - return [(v, [e for e in examples if e[attr] == v]) - for v in values[attr]] + """Return a list of (val, examples) pairs for each val of attr.""" + return [(v, [e for e in examples if e[attr] == v]) for v in values[attr]] 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)) - return sum(-p * math.log2(p) for p in probabilities) - -# ______________________________________________________________________________ - -# A decision list is implemented as a list of (test, value) pairs. + """Number of bits to represent the probability distribution in values.""" + probabilities = normalize(remove_all(0, values)) + return sum(-p * np.log2(p) for p in probabilities) def DecisionListLearner(dataset): - """[Figure 18.11]""" + """ + [Figure 18.11] + A decision list implemented as a list of (test, value) pairs. + """ def decision_list_learning(examples): if not examples: return [(True, False)] t, o, examples_t = find_examples(examples) if not t: - raise Failure + raise Exception return [(t, o)] + decision_list_learning(examples - examples_t) def find_examples(examples): - """Find a set of examples that all have the same outcome under - some test. Return a tuple of the test, outcome, and examples.""" + """ + Find a set of examples that all have the same outcome under + some test. Return a tuple of the test, outcome, and examples. + """ raise NotImplementedError def passes(example, test): - "Does the example pass the test?" + """Does the example pass the test?""" raise NotImplementedError def predict(example): - "Predict the outcome for the first passing test." + """Predict the outcome for the first passing test.""" for test, outcome in predict.decision_list: if passes(example, test): return outcome + predict.decision_list = decision_list_learning(set(dataset.examples)) return predict -# ______________________________________________________________________________ +def NearestNeighborLearner(dataset, k=1): + """k-NearestNeighbor: the k nearest neighbors vote.""" + + def predict(example): + """Find the k closest items, and have them vote for the best.""" + best = heapq.nsmallest(k, ((dataset.distance(e, example), e) for e in dataset.examples)) + return mode(e[dataset.target] for (d, e) in best) + + return predict + + +def LinearLearner(dataset, learning_rate=0.01, epochs=100): + """ + [Section 18.6.3] + Linear classifier with hard threshold. + """ + idx_i = dataset.inputs + idx_t = dataset.target + examples = dataset.examples + num_examples = len(examples) + + # X transpose + X_col = [dataset.values[i] for i in idx_i] # vertical columns of X + + # add dummy + ones = [1 for _ in range(len(examples))] + X_col = [ones] + X_col + + # initialize random weights + num_weights = len(idx_i) + 1 + w = random_weights(min_value=-0.5, max_value=0.5, num_weights=num_weights) + + for epoch in range(epochs): + err = [] + # pass over all examples + for example in examples: + x = [1] + example + y = np.dot(w, x) + t = example[idx_t] + err.append(t - y) + + # update weights + for i in range(len(w)): + w[i] = w[i] + learning_rate * (np.dot(err, X_col[i]) / num_examples) + + def predict(example): + x = [1] + example + return np.dot(w, x) + + return predict + + +def LogisticLinearLeaner(dataset, learning_rate=0.01, epochs=100): + """ + [Section 18.6.4] + Linear classifier with logistic regression. + """ + idx_i = dataset.inputs + idx_t = dataset.target + examples = dataset.examples + num_examples = len(examples) + + # X transpose + X_col = [dataset.values[i] for i in idx_i] # vertical columns of X + + # add dummy + ones = [1 for _ in range(len(examples))] + X_col = [ones] + X_col + + # initialize random weights + num_weights = len(idx_i) + 1 + w = random_weights(min_value=-0.5, max_value=0.5, num_weights=num_weights) + + for epoch in range(epochs): + err = [] + h = [] + # pass over all examples + for example in examples: + x = [1] + example + y = sigmoid(np.dot(w, x)) + h.append(sigmoid_derivative(y)) + t = example[idx_t] + err.append(t - y) -def NeuralNetLearner(dataset, hidden_layer_sizes=[3], - learning_rate=0.01, epoches=100): + # update weights + for i in range(len(w)): + buffer = [x * y for x, y in zip(err, h)] + w[i] = w[i] + learning_rate * (np.dot(buffer, X_col[i]) / num_examples) + + def predict(example): + x = [1] + example + return sigmoid(np.dot(w, x)) + + return predict + + +def NeuralNetLearner(dataset, hidden_layer_sizes=None, learning_rate=0.01, epochs=100, activation=sigmoid): """ Layered feed-forward network. hidden_layer_sizes: List of number of hidden units per hidden layer - learning_rate: Learning rate of gradient decent - epoches: Number of passes over the dataset + learning_rate: Learning rate of gradient descent + epochs: Number of passes over the dataset """ + if hidden_layer_sizes is None: + hidden_layer_sizes = [3] i_units = len(dataset.inputs) - o_units = 1 # As of now, dataset.target gives only one index. + o_units = len(dataset.values[dataset.target]) # construct a network - raw_net = network(i_units, hidden_layer_sizes, o_units) - learned_net = BackPropagationLearner(dataset, raw_net, - learning_rate, epoches) + raw_net = network(i_units, hidden_layer_sizes, o_units, activation) + learned_net = BackPropagationLearner(dataset, raw_net, learning_rate, epochs, activation) def predict(example): - - # Input nodes + # input nodes i_nodes = learned_net[0] - # Activate input layer + # activate input layer for v, n in zip(example, i_nodes): n.value = v - # Forward pass + # forward pass for layer in learned_net[1:]: for node in layer: inc = [n.value for n in node.inputs] - in_val = dotproduct(inc, node.weights) + in_val = dot_product(inc, node.weights) node.value = node.activation(in_val) - # Hypothesis + # hypothesis o_nodes = learned_net[-1] - pred = [o_nodes[i].value for i in range(o_units)] - return 1 if pred[0] >= 0.5 else 0 + prediction = find_max_node(o_nodes) + return prediction return predict -class NNUnit: +def BackPropagationLearner(dataset, net, learning_rate, epochs, activation=sigmoid): """ - Single Unit of Multiple Layer Neural Network - inputs: Incoming connections - weights: weights to incoming connections - """ - - def __init__(self, weights=None, inputs=None): - self.weights = [] - self.inputs = [] - self.value = None - self.activation = sigmoid - - -def network(input_units, hidden_layer_sizes, output_units): + [Figure 18.23] + The back-propagation algorithm for multilayer networks. """ - Create of Directed Acyclic Network of given number layers - hidden_layers_sizes : list number of neuron units in each hidden layer - excluding input and output layers. - """ - # Check for PerceptronLearner - if hidden_layer_sizes: - layers_sizes = [input_units] + hidden_layer_sizes + [output_units] - else: - layers_sizes = [input_units] + [output_units] - - net = [[NNUnit() for n in range(size)] - for size in layers_sizes] - n_layers = len(net) - - # Make Connection - for i in range(1, n_layers): - for n in net[i]: - for k in net[i-1]: - n.inputs.append(k) - n.weights.append(0) - return net - - -def BackPropagationLearner(dataset, net, learning_rate, epoches): - "[Figure 18.23] The back-propagation algorithm for multilayer network" - # Initialise weights + # initialise weights for layer in net: for node in layer: - node.weights = [random.uniform(-0.5, 0.5) - for i in range(len(node.weights))] + node.weights = random_weights(min_value=-0.5, max_value=0.5, num_weights=len(node.weights)) examples = dataset.examples - ''' - As of now dataset.target gives an int instead of list, - Changing dataset class will have effect on all the learners. - Will be taken care of later - ''' - idx_t = [dataset.target] - idx_i = dataset.inputs - n_layers = len(net) + # As of now dataset.target gives an int instead of list, + # Changing dataset class will have effect on all the learners. + # Will be taken care of later. o_nodes = net[-1] i_nodes = net[0] + o_units = len(o_nodes) + idx_t = dataset.target + idx_i = dataset.inputs + n_layers = len(net) + + inputs, targets = init_examples(examples, idx_i, idx_t, o_units) - for epoch in range(epoches): - # Iterate over each example - for e in examples: - i_val = [e[i] for i in idx_i] - t_val = [e[i] for i in idx_t] - # Activate input layer + for epoch in range(epochs): + # iterate over each example + for e in range(len(examples)): + i_val = inputs[e] + t_val = targets[e] + + # activate input layer for v, n in zip(i_val, i_nodes): n.value = v - # Forward pass + # forward pass for layer in net[1:]: for node in layer: inc = [n.value for n in node.inputs] - in_val = dotproduct(inc, node.weights) + in_val = dot_product(inc, node.weights) node.value = node.activation(in_val) - # Initialize delta - delta = [[] for i in range(n_layers)] - - # Compute outer layer delta - o_units = len(o_nodes) - err = [t_val[i] - o_nodes[i].value - for i in range(o_units)] - delta[-1] = [(o_nodes[i].value) * (1 - o_nodes[i].value) * - (err[i]) for i in range(o_units)] - - # Backward pass + # initialize delta + delta = [[] for _ in range(n_layers)] + + # compute outer layer delta + + # error for the MSE cost function + err = [t_val[i] - o_nodes[i].value for i in range(o_units)] + + # calculate delta at output + if node.activation == sigmoid: + delta[-1] = [sigmoid_derivative(o_nodes[i].value) * err[i] for i in range(o_units)] + elif node.activation == relu: + delta[-1] = [relu_derivative(o_nodes[i].value) * err[i] for i in range(o_units)] + elif node.activation == tanh: + delta[-1] = [tanh_derivative(o_nodes[i].value) * err[i] for i in range(o_units)] + elif node.activation == elu: + delta[-1] = [elu_derivative(o_nodes[i].value) * err[i] for i in range(o_units)] + elif node.activation == leaky_relu: + delta[-1] = [leaky_relu_derivative(o_nodes[i].value) * err[i] for i in range(o_units)] + else: + return ValueError("Activation function unknown.") + + # backward pass h_layers = n_layers - 2 for i in range(h_layers, 0, -1): layer = net[i] h_units = len(layer) - nx_layer = net[i+1] - # weights from each ith layer node to each i + 1th layer node - w = [[node.weights[k] for node in nx_layer] - for k in range(h_units)] - - delta[i] = [(layer[j].value) * (1 - layer[j].value) * - dotproduct(w[j], delta[i+1]) - for j in range(h_units)] + nx_layer = net[i + 1] - # Update weights + # weights from each ith layer node to each i + 1th layer node + w = [[node.weights[k] for node in nx_layer] for k in range(h_units)] + + if activation == sigmoid: + delta[i] = [sigmoid_derivative(layer[j].value) * dot_product(w[j], delta[i + 1]) + for j in range(h_units)] + elif activation == relu: + delta[i] = [relu_derivative(layer[j].value) * dot_product(w[j], delta[i + 1]) + for j in range(h_units)] + elif activation == tanh: + delta[i] = [tanh_derivative(layer[j].value) * dot_product(w[j], delta[i + 1]) + for j in range(h_units)] + elif activation == elu: + delta[i] = [elu_derivative(layer[j].value) * dot_product(w[j], delta[i + 1]) + for j in range(h_units)] + elif activation == leaky_relu: + delta[i] = [leaky_relu_derivative(layer[j].value) * dot_product(w[j], delta[i + 1]) + for j in range(h_units)] + else: + return ValueError("Activation function unknown.") + + # update weights for i in range(1, n_layers): layer = net[i] - inc = [node.value for node in net[i-1]] + inc = [node.value for node in net[i - 1]] units = len(layer) for j in range(units): layer[j].weights = vector_add(layer[j].weights, - scalar_vector_product( - learning_rate * delta[i][j], inc)) + scalar_vector_product(learning_rate * delta[i][j], inc)) return net -def PerceptronLearner(dataset, learning_rate=0.01, epoches=100): +def PerceptronLearner(dataset, learning_rate=0.01, epochs=100): """Logistic Regression, NO hidden layer""" i_units = len(dataset.inputs) - o_units = 1 # As of now, dataset.target gives only one index. + o_units = len(dataset.values[dataset.target]) hidden_layer_sizes = [] raw_net = network(i_units, hidden_layer_sizes, o_units) - learned_net = BackPropagationLearner(dataset, raw_net, learning_rate, epoches) + learned_net = BackPropagationLearner(dataset, raw_net, learning_rate, epochs) def predict(example): - # Input nodes - i_nodes = learned_net[0] + o_nodes = learned_net[1] - # Activate input layer - for v, n in zip(example, i_nodes): - n.value = v + # forward pass + for node in o_nodes: + in_val = dot_product(example, node.weights) + node.value = node.activation(in_val) - # Forward pass - for layer in learned_net[1:]: - for node in layer: - inc = [n.value for n in node.inputs] - in_val = dotproduct(inc, node.weights) - node.value = node.activation(in_val) - - # Hypothesis - o_nodes = learned_net[-1] - pred = [o_nodes[i].value for i in range(o_units)] - return 1 if pred[0] >= 0.5 else 0 + # hypothesis + return find_max_node(o_nodes) return predict -# ______________________________________________________________________________ -def Linearlearner(dataset, learning_rate=0.01, epochs=100): - """Define with learner = Linearlearner(data); infer with learner(x).""" - idx_i = dataset.inputs - idx_t = dataset.target # As of now, dataset.target gives only one index. - examples = dataset.examples +class NNUnit: + """ + Single Unit of Multiple Layer Neural Network + inputs: Incoming connections + weights: Weights to incoming connections + """ - # X transpose - X_col = [dataset.values[i] for i in idx_i] # vertical columns of X + def __init__(self, activation=sigmoid, weights=None, inputs=None): + self.weights = weights or [] + self.inputs = inputs or [] + self.value = None + self.activation = activation - # Add dummy - ones = [1 for i in range(len(examples))] - X_col = ones + X_col - # Initialize random weigts - w = [random(-0.5, 0.5) for i in range(len(idx_i) + 1)] +def network(input_units, hidden_layer_sizes, output_units, activation=sigmoid): + """ + Create Directed Acyclic Network of given number layers. + hidden_layers_sizes : List number of neuron units in each hidden layer + excluding input and output layers + """ + layers_sizes = [input_units] + hidden_layer_sizes + [output_units] - for epoch in range(epochs): - err = [] - # Pass over all examples - for example in examples: - x = [example[i] for i in range(idx_i)] - x = [1] + x - y = dotproduct(w, x) - t = example[idx_t] - err.append(t - y) + net = [[NNUnit(activation) for _ in range(size)] for size in layers_sizes] + n_layers = len(net) - # update weights - for i in range(len(w)): - w[i] = w[i] - dotproduct(err, X_col[i]) + # make connection + for i in range(1, n_layers): + for n in net[i]: + for k in net[i - 1]: + n.inputs.append(k) + n.weights.append(0) + return net - def predict(example): - x = [1] + example - return dotproduct(w, x) - return predict -# ______________________________________________________________________________ +def init_examples(examples, idx_i, idx_t, o_units): + inputs, targets = {}, {} + + for i, e in enumerate(examples): + # input values of e + inputs[i] = [e[i] for i in idx_i] + + if o_units > 1: + # one-hot representation of e's target + t = [0 for i in range(o_units)] + t[e[idx_t]] = 1 + targets[i] = t + else: + # target value of e + targets[i] = [e[idx_t]] + + return inputs, targets + + +def find_max_node(nodes): + return nodes.index(max(nodes, key=lambda node: node.value)) + + +class SVC: + + def __init__(self, kernel=linear_kernel, C=1.0, verbose=False): + self.kernel = kernel + self.C = C # hyper-parameter + self.sv_idx, self.sv, self.sv_y = np.zeros(0), np.zeros(0), np.zeros(0) + self.alphas = np.zeros(0) + self.w = None + self.b = 0.0 # intercept + self.verbose = verbose + + def fit(self, X, y): + """ + Trains the model by solving a quadratic programming problem. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + # In QP formulation (dual): m variables, 2m+1 constraints (1 equation, 2m inequations) + self.solve_qp(X, y) + sv = self.alphas > 1e-5 + self.sv_idx = np.arange(len(self.alphas))[sv] + self.sv, self.sv_y, self.alphas = X[sv], y[sv], self.alphas[sv] + + if self.kernel == linear_kernel: + self.w = np.dot(self.alphas * self.sv_y, self.sv) + + for n in range(len(self.alphas)): + self.b += self.sv_y[n] + self.b -= np.sum(self.alphas * self.sv_y * self.K[self.sv_idx[n], sv]) + self.b /= len(self.alphas) + return self + + def solve_qp(self, X, y): + """ + Solves a quadratic programming problem. In QP formulation (dual): + m variables, 2m+1 constraints (1 equation, 2m inequations). + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + m = len(y) # m = n_samples + self.K = self.kernel(X) # gram matrix + P = self.K * np.outer(y, y) + q = -np.ones(m) + lb = np.zeros(m) # lower bounds + ub = np.ones(m) * self.C # upper bounds + A = y.astype(np.float64) # equality matrix + b = np.zeros(1) # equality vector + self.alphas = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver='cvxopt', + sym_proj=True, verbose=self.verbose) + + def predict_score(self, X): + """ + Predicts the score for a given example. + """ + if self.w is None: + return np.dot(self.alphas * self.sv_y, self.kernel(self.sv, X)) + self.b + return np.dot(X, self.w) + self.b + + def predict(self, X): + """ + Predicts the class of a given example. + """ + return np.sign(self.predict_score(X)) + + +class SVR: + + def __init__(self, kernel=linear_kernel, C=1.0, epsilon=0.1, verbose=False): + self.kernel = kernel + self.C = C # hyper-parameter + self.epsilon = epsilon # epsilon insensitive loss value + self.sv_idx, self.sv = np.zeros(0), np.zeros(0) + self.alphas_p, self.alphas_n = np.zeros(0), np.zeros(0) + self.w = None + self.b = 0.0 # intercept + self.verbose = verbose + + def fit(self, X, y): + """ + Trains the model by solving a quadratic programming problem. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + # In QP formulation (dual): m variables, 2m+1 constraints (1 equation, 2m inequations) + self.solve_qp(X, y) + + sv = np.logical_or(self.alphas_p > 1e-5, self.alphas_n > 1e-5) + self.sv_idx = np.arange(len(self.alphas_p))[sv] + self.sv, sv_y = X[sv], y[sv] + self.alphas_p, self.alphas_n = self.alphas_p[sv], self.alphas_n[sv] + + if self.kernel == linear_kernel: + self.w = np.dot(self.alphas_p - self.alphas_n, self.sv) + + for n in range(len(self.alphas_p)): + self.b += sv_y[n] + self.b -= np.sum((self.alphas_p - self.alphas_n) * self.K[self.sv_idx[n], sv]) + self.b -= self.epsilon + self.b /= len(self.alphas_p) + + return self + + def solve_qp(self, X, y): + """ + Solves a quadratic programming problem. In QP formulation (dual): + m variables, 2m+1 constraints (1 equation, 2m inequations). + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + # + m = len(y) # m = n_samples + self.K = self.kernel(X) # gram matrix + P = np.vstack((np.hstack((self.K, -self.K)), # alphas_p, alphas_n + np.hstack((-self.K, self.K)))) # alphas_n, alphas_p + q = np.hstack((-y, y)) + self.epsilon + lb = np.zeros(2 * m) # lower bounds + ub = np.ones(2 * m) * self.C # upper bounds + A = np.hstack((np.ones(m), -np.ones(m))) # equality matrix + b = np.zeros(1) # equality vector + alphas = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver='cvxopt', + sym_proj=True, verbose=self.verbose) + self.alphas_p = alphas[:m] + self.alphas_n = alphas[m:] + + def predict(self, X): + if self.kernel != linear_kernel: + return np.dot(self.alphas_p - self.alphas_n, self.kernel(self.sv, X)) + self.b + return np.dot(X, self.w) + self.b + + +class MultiClassLearner: + + def __init__(self, clf, decision_function='ovr'): + self.clf = clf + self.decision_function = decision_function + self.n_class, self.classifiers = 0, [] + + def fit(self, X, y): + """ + Trains n_class or n_class * (n_class - 1) / 2 classifiers + according to the training method, ovr or ovo respectively. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + :return: array of classifiers + """ + labels = np.unique(y) + self.n_class = len(labels) + if self.decision_function == 'ovr': # one-vs-rest method + for label in labels: + y1 = np.array(y) + y1[y1 != label] = -1.0 + y1[y1 == label] = 1.0 + self.clf.fit(X, y1) + self.classifiers.append(copy.deepcopy(self.clf)) + elif self.decision_function == 'ovo': # use one-vs-one method + n_labels = len(labels) + for i in range(n_labels): + for j in range(i + 1, n_labels): + neg_id, pos_id = y == labels[i], y == labels[j] + X1, y1 = np.r_[X[neg_id], X[pos_id]], np.r_[y[neg_id], y[pos_id]] + y1[y1 == labels[i]] = -1.0 + y1[y1 == labels[j]] = 1.0 + self.clf.fit(X1, y1) + self.classifiers.append(copy.deepcopy(self.clf)) + else: + return ValueError("Decision function must be either 'ovr' or 'ovo'.") + return self + + def predict(self, X): + """ + Predicts the class of a given example according to the training method. + """ + n_samples = len(X) + if self.decision_function == 'ovr': # one-vs-rest method + assert len(self.classifiers) == self.n_class + score = np.zeros((n_samples, self.n_class)) + for i in range(self.n_class): + clf = self.classifiers[i] + score[:, i] = clf.predict_score(X) + return np.argmax(score, axis=1) + elif self.decision_function == 'ovo': # use one-vs-one method + assert len(self.classifiers) == self.n_class * (self.n_class - 1) / 2 + vote = np.zeros((n_samples, self.n_class)) + clf_id = 0 + for i in range(self.n_class): + for j in range(i + 1, self.n_class): + res = self.classifiers[clf_id].predict(X) + vote[res < 0, i] += 1.0 # negative sample: class i + vote[res > 0, j] += 1.0 # positive sample: class j + clf_id += 1 + return np.argmax(vote, axis=1) + else: + return ValueError("Decision function must be either 'ovr' or 'ovo'.") 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 - return train -# ______________________________________________________________________________ + return train -def AdaBoost(L, K): +def ada_boost(dataset, L, K): """[Figure 18.34]""" - def train(dataset): - examples, target = dataset.examples, dataset.target - N = len(examples) - epsilon = 1. / (2 * N) - w = [1. / N] * N - h, z = [], [] - for k in range(K): - h_k = L(dataset, w) - h.append(h_k) - error = sum(weight for example, weight in zip(examples, w) - if example[target] != h_k(example)) - # Avoid divide-by-0 from either 0% or 100% error rates: - error = clip(error, epsilon, 1 - epsilon) - for j, example in enumerate(examples): - if example[target] == h_k(example): - w[j] *= error / (1. - error) - w = normalize(w) - z.append(math.log((1. - error) / error)) - return WeightedMajority(h, z) - return train + examples, target = dataset.examples, dataset.target + n = len(examples) + eps = 1 / (2 * n) + w = [1 / n] * n + h, z = [], [] + for k in range(K): + h_k = L(dataset, w) + h.append(h_k) + error = sum(weight for example, weight in zip(examples, w) if example[target] != h_k(example)) + # avoid divide-by-0 from either 0% or 100% error rates + error = np.clip(error, eps, 1 - eps) + for j, example in enumerate(examples): + if example[target] == h_k(example): + w[j] *= error / (1 - error) + w = normalize(w) + z.append(np.log((1 - error) / error)) + return weighted_majority(h, z) + + +def weighted_majority(predictors, weights): + """Return a predictor that takes a weighted vote.""" -def WeightedMajority(predictors, weights): - "Return a predictor that takes a weighted vote." def predict(example): - return weighted_mode((predictor(example) for predictor in predictors), - weights) + return weighted_mode((predictor(example) for predictor in predictors), weights) + return predict def weighted_mode(values, weights): - """Return the value with the greatest total weight. - >>> weighted_mode('abbaa', [1,2,3,1,2]) - 'b'""" + """ + Return the value with the greatest total weight. + >>> weighted_mode('abbaa', [1, 2, 3, 1, 2]) + 'b' + """ totals = defaultdict(int) for v, w in zip(values, weights): totals[v] += w - return max(list(totals.keys()), key=totals.get) + return max(totals, key=totals.__getitem__) + + +def RandomForest(dataset, n=5): + """An ensemble of Decision Trees trained using bagging and feature bagging.""" -# _____________________________________________________________________________ -# Adapting an unweighted learner for AdaBoost + def data_bagging(dataset, m=0): + """Sample m examples with replacement""" + n = len(dataset.examples) + return weighted_sample_with_replacement(m or n, dataset.examples, [1] * n) + + def feature_bagging(dataset, p=0.7): + """Feature bagging with probability p to retain an attribute""" + inputs = [i for i in dataset.inputs if probability(p)] + return inputs or dataset.inputs + + def predict(example): + print([predictor(example) for predictor in predictors]) + return mode(predictor(example) for predictor in predictors) + + predictors = [DecisionTreeLearner(DataSet(examples=data_bagging(dataset), attrs=dataset.attrs, + attr_names=dataset.attr_names, target=dataset.target, + inputs=feature_bagging(dataset))) for _ in range(n)] + + return predict 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]""" + """ + [Page 749 footnote 14] + Given a learner that takes just an unweighted dataset, return + one that takes also a weight for each example. + """ + 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." + """Copy dataset, replicating each example in proportion to its weight.""" n = n or len(dataset.examples) result = copy.copy(dataset) result.examples = weighted_replicate(dataset.examples, weights, n) @@ -740,216 +1105,119 @@ def replicated_dataset(dataset, weights, n=None): def weighted_replicate(seq, weights, n): - """Return n selections from seq, with the count of each element of + """ + Return n selections from seq, with the count of each element of seq proportional to the corresponding weight (filling in fractions randomly). - >>> weighted_replicate('ABC', [1,2,1], 4) - ['A', 'B', 'B', 'C']""" + >>> weighted_replicate('ABC', [1, 2, 1], 4) + ['A', 'B', 'B', 'C'] + """ assert len(seq) == len(weights) weights = normalize(weights) wholes = [int(w * n) for w in weights] fractions = [(w * n) % 1 for w in weights] return (flatten([x] * nx for x, nx in zip(seq, wholes)) + - weighted_sample_with_replacement(seq, fractions, n - sum(wholes))) + weighted_sample_with_replacement(n - sum(wholes), seq, fractions)) -def flatten(seqs): return sum(seqs, []) +# metrics -# _____________________________________________________________________________ -# Functions for testing learners on examples +def accuracy_score(y_pred, y_true): + assert y_pred.shape == y_true.shape + return np.mean(np.equal(y_pred, y_true)) -def test(predict, dataset, examples=None, verbose=0): - "Return the proportion of the examples that are NOT correctly predicted." - 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] - output = predict(dataset.sanitize(example)) - if output == desired: - right += 1 - if verbose >= 2: - print(' OK: got %s for %s' % (desired, example)) - elif verbose: - print('WRONG: got %s, expected %s for %s' % ( - output, desired, example)) - return 1 - (right / len(examples)) +def r2_score(y_pred, y_true): + assert y_pred.shape == y_true.shape + return 1. - (np.sum(np.square(y_pred - y_true)) / # sum of square of residuals + np.sum(np.square(y_true - np.mean(y_true)))) # total sum of squares -def train_and_test(dataset, start, end): - """Reserve dataset.examples[start:end] for test; train on the remainder.""" - start = int(start) - end = int(end) - examples = dataset.examples - train = examples[:start] + examples[end:] - val = examples[start:end] - return train, val - - -def cross_validation(learner, size, 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. - Shuffle the examples first; If trials>1, average over several shuffles. - Returns Training error, Validataion error""" - if k is None: - k = len(dataset.examples) - if trials > 1: - trial_errT = 0 - trial_errV = 0 - for t in range(trials): - errT, errV = cross_validation(learner, size, dataset, - k=10, trials=1) - trial_errT += errT - trial_errV += errV - return trial_errT / trials, trial_errV / trials - else: - fold_errT = 0 - fold_errV = 0 - n = len(dataset.examples) - examples = dataset.examples - for fold in range(k): - random.shuffle(dataset.examples) - train_data, val_data = train_and_test(dataset, fold * (n / k), - (fold + 1) * (n / k)) - dataset.examples = train_data - h = learner(dataset, size) - fold_errT += test(h, dataset, train_data) - fold_errV += test(h, dataset, val_data) - # Reverting back to original once test is completed - dataset.examples = examples - return fold_errT / k, fold_errV / k - - -def cross_validation_wrapper(learner, dataset, k=10, trials=1): - """ - Fig 18.8 - Return the optimal value of size having minimum error - on validataion set - err_train: a training error array, indexed by size - err_val: a validataion error array, indexed by size - """ - err_val = [] - err_train = [] - size = 1 - while True: - errT, errV = cross_validation(learner, size, dataset, k) - # Check for convergence provided err_val is not empty - if (err_val and isclose(err_val[-1], errV, rel_tol=1e-6)): - best_size = size - return learner(dataset, best_size) - - err_val.append(errV) - err_train.append(errT) - print(err_val) - size += 1 - - -def leave_one_out(learner, dataset): - "Leave one out cross-validation over the dataset." - return cross_validation(learner, size, dataset, k=len(dataset.examples)) - - -def learningcurve(learner, dataset, trials=10, sizes=None): - if sizes is None: - 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) - return [(size, mean([score(learner, size) for t in range(trials)])) - for size in sizes] - -# ______________________________________________________________________________ -# The rest of this file gives datasets for machine learning problems. - -orings = DataSet(name='orings', target='Distressed', - attrnames="Rings Distressed Temp Pressure Flightnum") +# datasets +orings = DataSet(name='orings', target='Distressed', attr_names='Rings Distressed Temp Pressure Flightnum') zoo = DataSet(name='zoo', target='type', exclude=['name'], - attrnames="name hair feathers eggs milk airborne aquatic " + - "predator toothed backbone breathes venomous fins legs tail " + - "domestic catsize type") + attr_names='name hair feathers eggs milk airborne aquatic predator toothed backbone ' + 'breathes venomous fins legs tail domestic catsize type') - -iris = DataSet(name="iris", target="class", - attrnames="sepal-len sepal-width petal-len petal-width class") - -# ______________________________________________________________________________ -# The Restaurant example from [Figure 18.2] +iris = DataSet(name='iris', target='class', attr_names='sepal-len sepal-width petal-len petal-width class') def RestaurantDataSet(examples=None): - "Build a DataSet of Restaurant waiting examples. [Figure 18.3]" + """ + [Figure 18.3] + Build a DataSet of Restaurant waiting examples. + """ return DataSet(name='restaurant', target='Wait', examples=examples, - attrnames='Alternate Bar Fri/Sat Hungry Patrons Price ' + - 'Raining Reservation Type WaitEstimate Wait') + attr_names='Alternate Bar Fri/Sat Hungry Patrons Price Raining Reservation Type WaitEstimate Wait') + restaurant = RestaurantDataSet() -def T(attrname, branches): - branches = {value: (child if isinstance(child, DecisionFork) - else DecisionLeaf(child)) +def T(attr_name, branches): + branches = {value: (child if isinstance(child, DecisionFork) else DecisionLeaf(child)) for value, child in branches.items()} - return DecisionFork(restaurant.attrnum(attrname), attrname, branches) + return DecisionFork(restaurant.attr_num(attr_name), attr_name, print, branches) + -""" [Figure 18.2] +""" +[Figure 18.2] A decision tree for deciding whether to wait for a table at a hotel. """ waiting_decision_tree = 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'}) - })})})}) + {'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'})})})})}) def SyntheticRestaurant(n=20): - "Generate a DataSet with n examples." + """Generate a DataSet with n examples.""" + def gen(): example = list(map(random.choice, restaurant.values)) example[restaurant.target] = waiting_decision_tree(example) return example - return RestaurantDataSet([gen() for i in range(n)]) -# ______________________________________________________________________________ -# Artificial, generated datasets. + return RestaurantDataSet([gen() for _ in range(n)]) 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.""" + """ + 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. + """ examples = [] for i in range(n): - bits = [random.choice([0, 1]) for i in range(k)] + bits = [random.choice([0, 1]) for _ in range(k)] bits.append(int(sum(bits) > k / 2)) examples.append(bits) - return DataSet(name="majority", examples=examples) + 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.""" +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. + """ examples = [] for i in range(n): - bits = [random.choice([0, 1]) for i in range(k)] + bits = [random.choice([0, 1]) for _ in range(k)] bits.append(sum(bits) % 2) examples.append(bits) return DataSet(name=name, examples=examples) @@ -957,28 +1225,29 @@ def Parity(k, n, name="parity"): def Xor(n): """Return a DataSet with n examples of 2-input xor.""" - return Parity(2, n, name="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." + """2 inputs are chosen uniformly from (0.0 .. 2.0]; output is xor of ints.""" examples = [] for i in range(n): - x, y = [random.uniform(0.0, 2.0) for i in '12'] - examples.append([x, y, int(x) != int(y)]) - return DataSet(name="continuous xor", examples=examples) - -# ______________________________________________________________________________ - - -def compare(algorithms=[PluralityLearner, NaiveBayesLearner, - NearestNeighborLearner, DecisionTreeLearner], - datasets=[iris, orings, zoo, restaurant, SyntheticRestaurant(20), - Majority(7, 100), Parity(7, 100), Xor(100)], - k=10, trials=1): - """Compare various learners on various datasets using cross-validation. - Print results as a table.""" - 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') + x, y = [random.uniform(0.0, 2.0) for _ in '12'] + examples.append([x, y, x != y]) + return DataSet(name='continuous xor', examples=examples) + + +def compare(algorithms=None, datasets=None, k=10, trials=1): + """ + Compare various learners on various datasets using cross-validation. + Print results as a table. + """ + # default list of algorithms + algorithms = algorithms or [PluralityLearner, NaiveBayesLearner, NearestNeighborLearner, DecisionTreeLearner] + + # default list of datasets + datasets = datasets or [iris, orings, zoo, restaurant, SyntheticRestaurant(20), + Majority(7, 100), Parity(7, 100), Xor(100)] + + print_table([[a.__name__.replace('Learner', '')] + [cross_validation(a, d, k=k, trials=trials) for d in datasets] + for a in algorithms], header=[''] + [d.name[0:7] for d in datasets], numfmt='%.2f') diff --git a/learning4e.py b/learning4e.py new file mode 100644 index 000000000..12c0defa5 --- /dev/null +++ b/learning4e.py @@ -0,0 +1,1039 @@ +"""Learning from examples (Chapters 18)""" + +import copy +from collections import defaultdict +from statistics import stdev + +from qpsolvers import solve_qp + +from deep_learning4e import Sigmoid +from probabilistic_learning import NaiveBayesLearner +from utils4e import * + + +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. + d.attrs A list of integers to index into an example, so example[attr] + gives a value. Normally the same as range(len(d.examples[0])). + d.attr_names Optional list of mnemonic names for corresponding attrs. + d.target The attribute that a learning algorithm will try to predict. + By default the final attribute. + d.inputs The list of attrs without the target. + d.values A list of lists: each sublist is the set of possible + values for the corresponding attribute. If initially None, + it is computed from the known examples by self.set_problem. + If not None, an erroneous value raises ValueError. + d.distance A function from a pair of examples to a non-negative number. + Should be symmetric, etc. Defaults to mean_boolean_error + since that can handle any field types. + d.name Name of the data set (for output display only). + d.source URL or other source where the data came from. + d.exclude A list of attribute indexes to exclude from d.inputs. Elements + of this list can either be integers (attrs) or attr_names. + + Normally, you call the constructor and you're done; then you just + access fields like d.examples and d.target and d.inputs. + """ + + def __init__(self, examples=None, attrs=None, attr_names=None, target=-1, inputs=None, + values=None, distance=mean_boolean_error, name='', source='', exclude=()): + """ + Accepts any of DataSet's fields. Examples can also be a + string or file from which to parse examples using parse_csv. + Optional parameter: exclude, as documented in .set_problem(). + >>> DataSet(examples='1, 2, 3') + + """ + self.name = name + self.source = source + self.values = values + self.distance = distance + self.got_values_flag = bool(values) + + # initialize .examples from string or list or data directory + if isinstance(examples, str): + self.examples = parse_csv(examples) + elif examples is None: + self.examples = parse_csv(open_data(name + '.csv').read()) + else: + self.examples = examples + + # attrs are the indices of examples, unless otherwise stated. + if self.examples is not None and attrs is None: + attrs = list(range(len(self.examples[0]))) + + self.attrs = attrs + + # initialize .attr_names from string, list, or by default + if isinstance(attr_names, str): + self.attr_names = attr_names.split() + else: + self.attr_names = attr_names or attrs + self.set_problem(target, inputs=inputs, exclude=exclude) + + def set_problem(self, target, inputs=None, exclude=()): + """ + Set (or change) the target and/or inputs. + This way, one DataSet can be used multiple ways. inputs, if specified, + is a list of attributes, or specify exclude as a list of attributes + to not use in inputs. Attributes can be -n .. n, or an attr_name. + Also computes the list of possible values, if that wasn't done yet. + """ + self.target = self.attr_num(target) + exclude = list(map(self.attr_num, exclude)) + if inputs: + self.inputs = remove_all(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.update_values() + self.check_me() + + def check_me(self): + """Check that my fields make sense.""" + assert len(self.attr_names) == len(self.attrs) + assert self.target in self.attrs + assert self.target not in self.inputs + assert set(self.inputs).issubset(set(self.attrs)) + if self.got_values_flag: + # only check if values are provided while initializing DataSet + list(map(self.check_example, self.examples)) + + def add_example(self, example): + """Add an example to the list of examples, checking it first.""" + self.check_example(example) + self.examples.append(example) + + def check_example(self, example): + """Raise ValueError if example has any invalid values.""" + if self.values: + for a in self.attrs: + if example[a] not in self.values[a]: + raise ValueError('Bad value {} for attribute {} in {}' + .format(example[a], self.attr_names[a], example)) + + def attr_num(self, attr): + """Returns the number used for attr, which can be a name, or -n .. n-1.""" + if isinstance(attr, str): + return self.attr_names.index(attr) + elif attr < 0: + return len(self.attrs) + attr + else: + return attr + + def update_values(self): + self.values = list(map(unique, zip(*self.examples))) + + 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)][:-1] + + def classes_to_numbers(self, classes=None): + """Converts class names to numbers.""" + if not classes: + # if classes were not given, extract them from values + classes = sorted(self.values[self.target]) + for item in self.examples: + item[self.target] = classes.index(item[self.target]) + + def remove_examples(self, value=''): + """Remove examples that contain given value.""" + self.examples = [x for x in self.examples if value not in x] + self.update_values() + + def split_values_by_classes(self): + """Split values into buckets according to their class.""" + buckets = defaultdict(lambda: []) + target_names = self.values[self.target] + + for v in self.examples: + item = [a for a in v if a not in target_names] # remove target from item + buckets[v[self.target]].append(item) # add item to bucket of its class + + return buckets + + def find_means_and_deviations(self): + """ + Finds the means and standard deviations of self.dataset. + means : a dictionary for each class/target. Holds a list of the means + of the features for the class. + deviations: a dictionary for each class/target. Holds a list of the sample + standard deviations of the features for the class. + """ + target_names = self.values[self.target] + feature_numbers = len(self.inputs) + + item_buckets = self.split_values_by_classes() + + means = defaultdict(lambda: [0] * feature_numbers) + deviations = defaultdict(lambda: [0] * feature_numbers) + + for t in target_names: + # find all the item feature values for item in class t + features = [[] for _ in range(feature_numbers)] + for item in item_buckets[t]: + for i in range(feature_numbers): + features[i].append(item[i]) + + # calculate means and deviations fo the class + for i in range(feature_numbers): + means[t][i] = mean(features[i]) + deviations[t][i] = stdev(features[i]) + + return means, deviations + + def __repr__(self): + return ''.format(self.name, len(self.examples), len(self.attrs)) + + +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. + Fields that look like numbers are converted to numbers. + The delim defaults to ',' but '\t' and None are also reasonable values. + >>> parse_csv('1, 2, 3 \n 0, 2, na') + [[1, 2, 3], [0, 2, 'na']] + """ + lines = [line for line in input.splitlines() if line.strip()] + return [list(map(num_or_str, line.split(delim))) for line in lines] + + +def err_ratio(learner, dataset, examples=None): + """ + Return the proportion of the examples that are NOT correctly predicted. + verbose - 0: No output; 1: Output wrong; 2 (or greater): Output correct + """ + examples = examples or dataset.examples + if len(examples) == 0: + return 0.0 + right = 0 + for example in examples: + desired = example[dataset.target] + output = learner.predict(dataset.sanitize(example)) + if np.allclose(output, desired): + right += 1 + return 1 - (right / len(examples)) + + +def grade_learner(learner, tests): + """ + Grades the given learner based on how many tests it passes. + tests is a list with each element in the form: (values, output). + """ + return mean(int(learner.predict(X) == y) for X, y in tests) + + +def train_test_split(dataset, start=None, end=None, test_split=None): + """ + If you are giving 'start' and 'end' as parameters, + then it will return the testing set from index 'start' to 'end' + and the rest for training. + If you give 'test_split' as a parameter then it will return + test_split * 100% as the testing set and the rest as + training set. + """ + examples = dataset.examples + if test_split is None: + train = examples[:start] + examples[end:] + val = examples[start:end] + else: + total_size = len(examples) + val_size = int(total_size * test_split) + train_size = total_size - val_size + train = examples[:train_size] + val = examples[train_size:total_size] + + return train, val + + +def model_selection(learner, dataset, k=10, trials=1): + """ + [Figure 18.8] + Return the optimal value of size having minimum error on validation set. + err: a validation error array, indexed by size + """ + errs = [] + size = 1 + while True: + err = cross_validation(learner, dataset, size, k, trials) + # check for convergence provided err_val is not empty + if err and not np.isclose(err[-1], err, rtol=1e-6): + best_size = 0 + min_val = np.inf + i = 0 + while i < size: + if errs[i] < min_val: + min_val = errs[i] + best_size = i + i += 1 + return learner(dataset, best_size) + errs.append(err) + size += 1 + + +def cross_validation(learner, dataset, size=None, 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. + Shuffle the examples first; if trials > 1, average over several shuffles. + Returns Training error + """ + k = k or len(dataset.examples) + if trials > 1: + trial_errs = 0 + for t in range(trials): + errs = cross_validation(learner, dataset, size, k, trials) + trial_errs += errs + return trial_errs / trials + else: + fold_errs = 0 + n = len(dataset.examples) + examples = dataset.examples + random.shuffle(dataset.examples) + for fold in range(k): + train_data, val_data = train_test_split(dataset, fold * (n // k), (fold + 1) * (n // k)) + dataset.examples = train_data + h = learner(dataset, size) + fold_errs += err_ratio(h, dataset, train_data) + # reverting back to original once test is completed + dataset.examples = examples + return fold_errs / k + + +def leave_one_out(learner, dataset, size=None): + """Leave one out cross-validation over the dataset.""" + return cross_validation(learner, dataset, size, len(dataset.examples)) + + +def learning_curve(learner, dataset, trials=10, sizes=None): + if sizes is None: + sizes = list(range(2, len(dataset.examples) - trials, 2)) + + def score(learner, size): + random.shuffle(dataset.examples) + return cross_validation(learner, dataset, size, trials) + + return [(size, mean([score(learner, size) for _ in range(trials)])) for size in sizes] + + +class PluralityLearner: + """ + A very dumb algorithm: always pick the result that was most popular + in the training data. Makes a baseline for comparison. + """ + + def __init__(self, dataset): + self.most_popular = mode([e[dataset.target] for e in dataset.examples]) + + def predict(self, example): + """Always return same result: the most popular from the training set.""" + return self.most_popular + + +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. + """ + + def __init__(self, attr, attr_name=None, default_child=None, branches=None): + """Initialize by saying what attribute this node tests.""" + self.attr = attr + self.attr_name = attr_name or attr + self.default_child = default_child + self.branches = branches or {} + + def __call__(self, example): + """Given an example, classify it using the attribute and the branches.""" + attr_val = example[self.attr] + if attr_val in self.branches: + return self.branches[attr_val](example) + else: + # return default class when attribute is unknown + return self.default_child(example) + + def add(self, val, subtree): + """Add a branch. If self.attr = val, go to the given subtree.""" + self.branches[val] = subtree + + def display(self, indent=0): + name = self.attr_name + print('Test', name) + for (val, subtree) in self.branches.items(): + print(' ' * 4 * indent, name, '=', val, '==>', end=' ') + subtree.display(indent + 1) + + def __repr__(self): + return 'DecisionFork({0!r}, {1!r}, {2!r})'.format(self.attr, self.attr_name, self.branches) + + +class DecisionLeaf: + """A leaf of a decision tree holds just a result.""" + + def __init__(self, result): + self.result = result + + def __call__(self, example): + return self.result + + def display(self): + print('RESULT =', self.result) + + def __repr__(self): + return repr(self.result) + + +class DecisionTreeLearner: + """[Figure 18.5]""" + + def __init__(self, dataset): + self.dataset = dataset + self.tree = self.decision_tree_learning(dataset.examples, dataset.inputs) + + def decision_tree_learning(self, examples, attrs, parent_examples=()): + if len(examples) == 0: + return self.plurality_value(parent_examples) + if self.all_same_class(examples): + return DecisionLeaf(examples[0][self.dataset.target]) + if len(attrs) == 0: + return self.plurality_value(examples) + A = self.choose_attribute(attrs, examples) + tree = DecisionFork(A, self.dataset.attr_names[A], self.plurality_value(examples)) + for (v_k, exs) in self.split_by(A, examples): + subtree = self.decision_tree_learning(exs, remove_all(A, attrs), examples) + tree.add(v_k, subtree) + return tree + + def plurality_value(self, examples): + """ + Return the most popular target value for this set of examples. + (If target is binary, this is the majority; otherwise plurality). + """ + popular = argmax_random_tie(self.dataset.values[self.dataset.target], + key=lambda v: self.count(self.dataset.target, v, examples)) + return DecisionLeaf(popular) + + def count(self, attr, val, examples): + """Count the number of examples that have example[attr] = val.""" + return sum(e[attr] == val for e in examples) + + def all_same_class(self, examples): + """Are all these examples in the same target class?""" + class0 = examples[0][self.dataset.target] + return all(e[self.dataset.target] == class0 for e in examples) + + def choose_attribute(self, attrs, examples): + """Choose the attribute with the highest information gain.""" + return argmax_random_tie(attrs, key=lambda a: self.information_gain(a, examples)) + + def information_gain(self, attr, examples): + """Return the expected reduction in entropy from splitting by attr.""" + + def I(examples): + return information_content([self.count(self.dataset.target, v, examples) + for v in self.dataset.values[self.dataset.target]]) + + n = len(examples) + remainder = sum((len(examples_i) / n) * I(examples_i) + for (v, examples_i) in self.split_by(attr, examples)) + return I(examples) - remainder + + def split_by(self, attr, examples): + """Return a list of (val, examples) pairs for each val of attr.""" + return [(v, [e for e in examples if e[attr] == v]) for v in self.dataset.values[attr]] + + def predict(self, x): + return self.tree(x) + + +def information_content(values): + """Number of bits to represent the probability distribution in values.""" + probabilities = normalize(remove_all(0, values)) + return sum(-p * np.log2(p) for p in probabilities) + + +class DecisionListLearner: + """ + [Figure 18.11] + A decision list implemented as a list of (test, value) pairs. + """ + + def __init__(self, dataset): + self.predict.decision_list = self.decision_list_learning(set(dataset.examples)) + + def decision_list_learning(self, examples): + if not examples: + return [(True, False)] + t, o, examples_t = self.find_examples(examples) + if not t: + raise Exception + return [(t, o)] + self.decision_list_learning(examples - examples_t) + + def find_examples(self, examples): + """ + Find a set of examples that all have the same outcome under + some test. Return a tuple of the test, outcome, and examples. + """ + raise NotImplementedError + + def passes(self, example, test): + """Does the example pass the test?""" + raise NotImplementedError + + def predict(self, example): + """Predict the outcome for the first passing test.""" + for test, outcome in self.predict.decision_list: + if self.passes(example, test): + return outcome + + +class NearestNeighborLearner: + """k-NearestNeighbor: the k nearest neighbors vote.""" + + def __init__(self, dataset, k=1): + self.dataset = dataset + self.k = k + + def predict(self, example): + """Find the k closest items, and have them vote for the best.""" + best = heapq.nsmallest(self.k, ((self.dataset.distance(e, example), e) for e in self.dataset.examples)) + return mode(e[self.dataset.target] for (d, e) in best) + + +class SVC: + + def __init__(self, kernel=linear_kernel, C=1.0, verbose=False): + self.kernel = kernel + self.C = C # hyper-parameter + self.sv_idx, self.sv, self.sv_y = np.zeros(0), np.zeros(0), np.zeros(0) + self.alphas = np.zeros(0) + self.w = None + self.b = 0.0 # intercept + self.verbose = verbose + + def fit(self, X, y): + """ + Trains the model by solving a quadratic programming problem. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + # In QP formulation (dual): m variables, 2m+1 constraints (1 equation, 2m inequations) + self.solve_qp(X, y) + sv = self.alphas > 1e-5 + self.sv_idx = np.arange(len(self.alphas))[sv] + self.sv, self.sv_y, self.alphas = X[sv], y[sv], self.alphas[sv] + + if self.kernel == linear_kernel: + self.w = np.dot(self.alphas * self.sv_y, self.sv) + + for n in range(len(self.alphas)): + self.b += self.sv_y[n] + self.b -= np.sum(self.alphas * self.sv_y * self.K[self.sv_idx[n], sv]) + self.b /= len(self.alphas) + return self + + def solve_qp(self, X, y): + """ + Solves a quadratic programming problem. In QP formulation (dual): + m variables, 2m+1 constraints (1 equation, 2m inequations). + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + m = len(y) # m = n_samples + self.K = self.kernel(X) # gram matrix + P = self.K * np.outer(y, y) + q = -np.ones(m) + lb = np.zeros(m) # lower bounds + ub = np.ones(m) * self.C # upper bounds + A = y.astype(np.float64) # equality matrix + b = np.zeros(1) # equality vector + self.alphas = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver='cvxopt', + sym_proj=True, verbose=self.verbose) + + def predict_score(self, X): + """ + Predicts the score for a given example. + """ + if self.w is None: + return np.dot(self.alphas * self.sv_y, self.kernel(self.sv, X)) + self.b + return np.dot(X, self.w) + self.b + + def predict(self, X): + """ + Predicts the class of a given example. + """ + return np.sign(self.predict_score(X)) + + +class SVR: + + def __init__(self, kernel=linear_kernel, C=1.0, epsilon=0.1, verbose=False): + self.kernel = kernel + self.C = C # hyper-parameter + self.epsilon = epsilon # epsilon insensitive loss value + self.sv_idx, self.sv = np.zeros(0), np.zeros(0) + self.alphas_p, self.alphas_n = np.zeros(0), np.zeros(0) + self.w = None + self.b = 0.0 # intercept + self.verbose = verbose + + def fit(self, X, y): + """ + Trains the model by solving a quadratic programming problem. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + # In QP formulation (dual): m variables, 2m+1 constraints (1 equation, 2m inequations) + self.solve_qp(X, y) + + sv = np.logical_or(self.alphas_p > 1e-5, self.alphas_n > 1e-5) + self.sv_idx = np.arange(len(self.alphas_p))[sv] + self.sv, sv_y = X[sv], y[sv] + self.alphas_p, self.alphas_n = self.alphas_p[sv], self.alphas_n[sv] + + if self.kernel == linear_kernel: + self.w = np.dot(self.alphas_p - self.alphas_n, self.sv) + + for n in range(len(self.alphas_p)): + self.b += sv_y[n] + self.b -= np.sum((self.alphas_p - self.alphas_n) * self.K[self.sv_idx[n], sv]) + self.b -= self.epsilon + self.b /= len(self.alphas_p) + + return self + + def solve_qp(self, X, y): + """ + Solves a quadratic programming problem. In QP formulation (dual): + m variables, 2m+1 constraints (1 equation, 2m inequations). + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + """ + m = len(y) # m = n_samples + self.K = self.kernel(X) # gram matrix + P = np.vstack((np.hstack((self.K, -self.K)), # alphas_p, alphas_n + np.hstack((-self.K, self.K)))) # alphas_n, alphas_p + q = np.hstack((-y, y)) + self.epsilon + lb = np.zeros(2 * m) # lower bounds + ub = np.ones(2 * m) * self.C # upper bounds + A = np.hstack((np.ones(m), -np.ones(m))) # equality matrix + b = np.zeros(1) # equality vector + alphas = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver='cvxopt', + sym_proj=True, verbose=self.verbose) + self.alphas_p = alphas[:m] + self.alphas_n = alphas[m:] + + def predict(self, X): + if self.kernel != linear_kernel: + return np.dot(self.alphas_p - self.alphas_n, self.kernel(self.sv, X)) + self.b + return np.dot(X, self.w) + self.b + + +class MultiClassLearner: + + def __init__(self, clf, decision_function='ovr'): + self.clf = clf + self.decision_function = decision_function + self.n_class, self.classifiers = 0, [] + + def fit(self, X, y): + """ + Trains n_class or n_class * (n_class - 1) / 2 classifiers + according to the training method, ovr or ovo respectively. + :param X: array of size [n_samples, n_features] holding the training samples + :param y: array of size [n_samples] holding the class labels + :return: array of classifiers + """ + labels = np.unique(y) + self.n_class = len(labels) + if self.decision_function == 'ovr': # one-vs-rest method + for label in labels: + y1 = np.array(y) + y1[y1 != label] = -1.0 + y1[y1 == label] = 1.0 + self.clf.fit(X, y1) + self.classifiers.append(copy.deepcopy(self.clf)) + elif self.decision_function == 'ovo': # use one-vs-one method + n_labels = len(labels) + for i in range(n_labels): + for j in range(i + 1, n_labels): + neg_id, pos_id = y == labels[i], y == labels[j] + X1, y1 = np.r_[X[neg_id], X[pos_id]], np.r_[y[neg_id], y[pos_id]] + y1[y1 == labels[i]] = -1.0 + y1[y1 == labels[j]] = 1.0 + self.clf.fit(X1, y1) + self.classifiers.append(copy.deepcopy(self.clf)) + else: + return ValueError("Decision function must be either 'ovr' or 'ovo'.") + return self + + def predict(self, X): + """ + Predicts the class of a given example according to the training method. + """ + n_samples = len(X) + if self.decision_function == 'ovr': # one-vs-rest method + assert len(self.classifiers) == self.n_class + score = np.zeros((n_samples, self.n_class)) + for i in range(self.n_class): + clf = self.classifiers[i] + score[:, i] = clf.predict_score(X) + return np.argmax(score, axis=1) + elif self.decision_function == 'ovo': # use one-vs-one method + assert len(self.classifiers) == self.n_class * (self.n_class - 1) / 2 + vote = np.zeros((n_samples, self.n_class)) + clf_id = 0 + for i in range(self.n_class): + for j in range(i + 1, self.n_class): + res = self.classifiers[clf_id].predict(X) + vote[res < 0, i] += 1.0 # negative sample: class i + vote[res > 0, j] += 1.0 # positive sample: class j + clf_id += 1 + return np.argmax(vote, axis=1) + else: + return ValueError("Decision function must be either 'ovr' or 'ovo'.") + + +def LinearLearner(dataset, learning_rate=0.01, epochs=100): + """ + [Section 18.6.3] + Linear classifier with hard threshold. + """ + idx_i = dataset.inputs + idx_t = dataset.target + examples = dataset.examples + num_examples = len(examples) + + # X transpose + X_col = [dataset.values[i] for i in idx_i] # vertical columns of X + + # add dummy + ones = [1 for _ in range(len(examples))] + X_col = [ones] + X_col + + # initialize random weights + num_weights = len(idx_i) + 1 + w = random_weights(min_value=-0.5, max_value=0.5, num_weights=num_weights) + + for epoch in range(epochs): + err = [] + # pass over all examples + for example in examples: + x = [1] + example + y = np.dot(w, x) + t = example[idx_t] + err.append(t - y) + + # update weights + for i in range(len(w)): + w[i] = w[i] + learning_rate * (np.dot(err, X_col[i]) / num_examples) + + def predict(example): + x = [1] + example + return np.dot(w, x) + + return predict + + +def LogisticLinearLeaner(dataset, learning_rate=0.01, epochs=100): + """ + [Section 18.6.4] + Linear classifier with logistic regression. + """ + idx_i = dataset.inputs + idx_t = dataset.target + examples = dataset.examples + num_examples = len(examples) + + # X transpose + X_col = [dataset.values[i] for i in idx_i] # vertical columns of X + + # add dummy + ones = [1 for _ in range(len(examples))] + X_col = [ones] + X_col + + # initialize random weights + num_weights = len(idx_i) + 1 + w = random_weights(min_value=-0.5, max_value=0.5, num_weights=num_weights) + + for epoch in range(epochs): + err = [] + h = [] + # pass over all examples + for example in examples: + x = [1] + example + y = Sigmoid()(np.dot(w, x)) + h.append(Sigmoid().derivative(y)) + t = example[idx_t] + err.append(t - y) + + # update weights + for i in range(len(w)): + buffer = [x * y for x, y in zip(err, h)] + w[i] = w[i] + learning_rate * (np.dot(buffer, X_col[i]) / num_examples) + + def predict(example): + x = [1] + example + return Sigmoid()(np.dot(w, x)) + + return predict + + +class EnsembleLearner: + """Given a list of learning algorithms, have them vote.""" + + def __init__(self, learners): + self.learners = learners + + def train(self, dataset): + self.predictors = [learner(dataset) for learner in self.learners] + + def predict(self, example): + return mode(predictor.predict(example) for predictor in self.predictors) + + +def ada_boost(dataset, L, K): + """[Figure 18.34]""" + + examples, target = dataset.examples, dataset.target + n = len(examples) + eps = 1 / (2 * n) + w = [1 / n] * n + h, z = [], [] + for k in range(K): + h_k = L(dataset, w) + h.append(h_k) + error = sum(weight for example, weight in zip(examples, w) if example[target] != h_k.predict(example[:-1])) + # avoid divide-by-0 from either 0% or 100% error rates + error = np.clip(error, eps, 1 - eps) + for j, example in enumerate(examples): + if example[target] == h_k.predict(example[:-1]): + w[j] *= error / (1 - error) + w = normalize(w) + z.append(np.log((1 - error) / error)) + return weighted_majority(h, z) + + +class weighted_majority: + """Return a predictor that takes a weighted vote.""" + + def __init__(self, predictors, weights): + self.predictors = predictors + self.weights = weights + + def predict(self, example): + return weighted_mode((predictor.predict(example) for predictor in self.predictors), self.weights) + + +def weighted_mode(values, weights): + """ + Return the value with the greatest total weight. + >>> weighted_mode('abbaa', [1, 2, 3, 1, 2]) + 'b' + """ + totals = defaultdict(int) + for v, w in zip(values, weights): + totals[v] += w + return max(totals, key=totals.__getitem__) + + +class RandomForest: + """An ensemble of Decision Trees trained using bagging and feature bagging.""" + + def __init__(self, dataset, n=5): + self.dataset = dataset + self.n = n + self.predictors = [DecisionTreeLearner(DataSet(examples=self.data_bagging(), attrs=self.dataset.attrs, + attr_names=self.dataset.attr_names, target=self.dataset.target, + inputs=self.feature_bagging())) for _ in range(self.n)] + + def data_bagging(self, m=0): + """Sample m examples with replacement""" + n = len(self.dataset.examples) + return weighted_sample_with_replacement(m or n, self.dataset.examples, [1] * n) + + def feature_bagging(self, p=0.7): + """Feature bagging with probability p to retain an attribute""" + inputs = [i for i in self.dataset.inputs if probability(p)] + return inputs or self.dataset.inputs + + def predict(self, example): + return mode(predictor.predict(example) for predictor in self.predictors) + + +def WeightedLearner(unweighted_learner): + """ + [Page 749 footnote 14] + Given a learner that takes just an unweighted dataset, return + one that takes also a weight for each example. + """ + + def train(dataset, weights): + dataset = replicated_dataset(dataset, weights) + n_samples, n_features = len(dataset.examples), dataset.target + X, y = (np.array([x[:n_features] for x in dataset.examples]), + np.array([x[n_features] for x in dataset.examples])) + return unweighted_learner.fit(X, y) + + 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) + result = copy.copy(dataset) + 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 + randomly). + >>> weighted_replicate('ABC', [1, 2, 1], 4) + ['A', 'B', 'B', 'C'] + """ + assert len(seq) == len(weights) + weights = normalize(weights) + wholes = [int(w * n) for w in weights] + fractions = [(w * n) % 1 for w in weights] + return (flatten([x] * nx for x, nx in zip(seq, wholes)) + + weighted_sample_with_replacement(n - sum(wholes), seq, fractions)) + + +# metrics + +def accuracy_score(y_pred, y_true): + assert y_pred.shape == y_true.shape + return np.mean(np.equal(y_pred, y_true)) + + +def r2_score(y_pred, y_true): + assert y_pred.shape == y_true.shape + return 1. - (np.sum(np.square(y_pred - y_true)) / # sum of square of residuals + np.sum(np.square(y_true - np.mean(y_true)))) # total sum of squares + + +# datasets + +orings = DataSet(name='orings', target='Distressed', attr_names='Rings Distressed Temp Pressure Flightnum') + +zoo = DataSet(name='zoo', target='type', exclude=['name'], + attr_names='name hair feathers eggs milk airborne aquatic predator toothed backbone ' + 'breathes venomous fins legs tail domestic catsize type') + +iris = DataSet(name='iris', target='class', attr_names='sepal-len sepal-width petal-len petal-width class') + + +def RestaurantDataSet(examples=None): + """ + [Figure 18.3] + Build a DataSet of Restaurant waiting examples. + """ + return DataSet(name='restaurant', target='Wait', examples=examples, + attr_names='Alternate Bar Fri/Sat Hungry Patrons Price Raining Reservation Type WaitEstimate Wait') + + +restaurant = RestaurantDataSet() + + +def T(attr_name, branches): + branches = {value: (child if isinstance(child, DecisionFork) else DecisionLeaf(child)) + for value, child in branches.items()} + return DecisionFork(restaurant.attr_num(attr_name), attr_name, print, branches) + + +""" +[Figure 18.2] +A decision tree for deciding whether to wait for a table at a hotel. +""" + +waiting_decision_tree = 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'})})})})}) + + +def SyntheticRestaurant(n=20): + """Generate a DataSet with n examples.""" + + def gen(): + example = list(map(random.choice, restaurant.values)) + example[restaurant.target] = waiting_decision_tree(example) + return example + + return RestaurantDataSet([gen() for _ in range(n)]) + + +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. + """ + examples = [] + for i in range(n): + bits = [random.choice([0, 1]) for _ in range(k)] + bits.append(int(sum(bits) > k / 2)) + 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. + """ + examples = [] + for i in range(n): + bits = [random.choice([0, 1]) for _ in range(k)] + bits.append(sum(bits) % 2) + 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 = [] + for i in range(n): + x, y = [random.uniform(0.0, 2.0) for _ in '12'] + examples.append([x, y, x != y]) + return DataSet(name='continuous xor', examples=examples) + + +def compare(algorithms=None, datasets=None, k=10, trials=1): + """ + Compare various learners on various datasets using cross-validation. + Print results as a table. + """ + # default list of algorithms + algorithms = algorithms or [PluralityLearner, NaiveBayesLearner, NearestNeighborLearner, DecisionTreeLearner] + + # default list of datasets + datasets = datasets or [iris, orings, zoo, restaurant, SyntheticRestaurant(20), + Majority(7, 100), Parity(7, 100), Xor(100)] + + print_table([[a.__name__.replace('Learner', '')] + [cross_validation(a, d, k=k, trials=trials) for d in datasets] + for a in algorithms], header=[''] + [d.name[0:7] for d in datasets], numfmt='%.2f') diff --git a/learning_apps.ipynb b/learning_apps.ipynb new file mode 100644 index 000000000..dd45b11b5 --- /dev/null +++ b/learning_apps.ipynb @@ -0,0 +1,988 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LEARNING APPLICATIONS\n", + "\n", + "In this notebook we will take a look at some indicative applications of machine learning techniques. We will cover content from [`learning.py`](https://github.com/aimacode/aima-python/blob/master/learning.py), for chapter 18 from Stuart Russel's and Peter Norvig's book [*Artificial Intelligence: A Modern Approach*](http://aima.cs.berkeley.edu/). Execute the cell below to get started:" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import *\n", + "from probabilistic_learning import *\n", + "from notebook import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* MNIST Handwritten Digits\n", + " * Loading and Visualising\n", + " * Testing\n", + "* MNIST Fashion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MNIST HANDWRITTEN DIGITS CLASSIFICATION\n", + "\n", + "The MNIST Digits database, available from [this page](http://yann.lecun.com/exdb/mnist/), is a large database of handwritten digits that is commonly used for training and testing/validating in Machine learning.\n", + "\n", + "The dataset has **60,000 training images** each of size 28x28 pixels with labels and **10,000 testing images** of size 28x28 pixels with labels.\n", + "\n", + "In this section, we will use this database to compare performances of different learning algorithms.\n", + "\n", + "It is estimated that humans have an error rate of about **0.2%** on this problem. Let's see how our algorithms perform!\n", + "\n", + "NOTE: We will be using external libraries to load and visualize the dataset smoothly ([numpy](http://www.numpy.org/) for loading and [matplotlib](http://matplotlib.org/) for visualization). You do not need previous experience of the libraries to follow along." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Loading MNIST Digits Data\n", + "\n", + "Let's start by loading MNIST data into numpy arrays.\n", + "\n", + "The function `load_MNIST()` loads MNIST data from files saved in `aima-data/MNIST`. It returns four numpy arrays that we are going to use to train and classify hand-written digits in various learning approaches." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": {}, + "outputs": [], + "source": [ + "train_img, train_lbl, test_img, test_lbl = load_MNIST()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check the shape of these NumPy arrays to make sure we have loaded the database correctly.\n", + "\n", + "Each 28x28 pixel image is flattened to a 784x1 array and we should have 60,000 of them in training data. Similarly, we should have 10,000 of those 784x1 arrays in testing data." + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training images size: (60000, 784)\n", + "Training labels size: (60000,)\n", + "Testing images size: (10000, 784)\n", + "Testing labels size: (10000,)\n" + ] + } + ], + "source": [ + "print(\"Training images size:\", train_img.shape)\n", + "print(\"Training labels size:\", train_lbl.shape)\n", + "print(\"Testing images size:\", test_img.shape)\n", + "print(\"Testing labels size:\", test_lbl.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualizing Data\n", + "\n", + "To get a better understanding of the dataset, let's visualize some random images for each class from training and testing datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAKoCAYAAABUXzFLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJzs3Xm8VdP/x/FXpEHznJCxpEimkCTSIBrM85RExkJFQoPoR5lJKFNKlHnKlCFjMiTzlClSac6Uzu8P38/a69x77nXvueeevc8+7+fj4dG29hlWqzPt9fmsz6qQSCQSiIiIiIiIxMQGYXdAREREREQkk3SRIyIiIiIisaKLHBERERERiRVd5IiIiIiISKzoIkdERERERGJFFzkiIiIiIhIrusgREREREZFY0UWOiIiIiIjEii5yREREREQkVnSR41m9ejUDBgygSZMmVKlShTZt2vDAAw+E3a3IW7VqFYMHD6ZLly40aNCAChUqMHz48LC7lRNeeukl+vTpQ4sWLahWrRqbbropvXr1Yu7cuWF3LdI++OADDjroIJo2bUrVqlWpW7cue+21F5MnTw67aznpzjvvpEKFClSvXj3srkTayy+/TIUKFVL+99Zbb4XdvZwwe/ZsunfvTp06dahatSrNmjVj1KhRYXcr0k4++eQiX3d67RXv/fffp3fv3jRp0oSNN96YFi1aMHLkSNauXRt21yLvnXfeoWvXrtSoUYPq1auz33778frrr4fdrVKpGHYHouTQQw9lzpw5jBkzhubNmzNlyhSOOeYY1q9fz7HHHht29yJr6dKl3H777ey000707t2bO++8M+wu5Yzx48ezdOlSzjvvPFq2bMnixYsZN24ce+65JzNnzmT//fcPu4uRtHz5cjbffHOOOeYYNt10U9asWcP999/PCSecwIIFCxg2bFjYXcwZP/30ExdeeCFNmjRhxYoVYXcnJ1x55ZXst99+SW077LBDSL3JHVOmTOGEE07gyCOP5N5776V69ep8/fXXLFy4MOyuRdqll17KGWecUai9R48eVK5cmd133z2EXkXfJ598Qrt27dhuu+24/vrrqV+/Pq+++iojR45k7ty5PPbYY2F3MbLmzJlDhw4daNu2Lffddx+JRIKrr76aTp06MWvWLPbaa6+wu1gyCUkkEonEU089lQASU6ZMSWrv3LlzokmTJol169aF1LPoW79+fWL9+vWJRCKRWLx4cQJIXH755eF2KkcsWrSoUNuqVasSjRo1SnTq1CmEHuW2PfbYI7H55puH3Y2ccvDBByd69OiROOmkkxLVqlULuzuRNmvWrASQeOihh8LuSs758ccfE9WqVUv0798/7K7Ewssvv5wAEsOGDQu7K5F1ySWXJIDEV199ldTer1+/BJD47bffQupZ9HXt2jXRqFGjxJo1a1zbypUrE/Xr10+0a9cuxJ6VjtLV/ueRRx6hevXqHHHEEUntp5xyCgsXLuTtt98OqWfRZyFzKb2GDRsWaqtevTotW7bkhx9+CKFHua1+/fpUrKgAdUlNnjyZV155hVtvvTXsrkjM3XnnnaxZs4YhQ4aE3ZVYmDhxIhUqVKBPnz5hdyWyNtpoIwBq1aqV1F67dm022GADKlWqFEa3csLrr79Ox44d2XjjjV1bjRo16NChA2+88QY///xziL0rOV3k/M/8+fPZfvvtC/1Aat26tTsvkg0rVqzgvffeo1WrVmF3JfLWr1/PunXrWLx4MbfeeiszZ87Uj6gS+vXXXxkwYABjxoxhs802C7s7OeWss86iYsWK1KxZk65duzJ79uywuxR5r776KnXr1uWzzz6jTZs2VKxYkYYNG3LGGWewcuXKsLuXU1asWMH06dPp1KkTW221VdjdiayTTjqJ2rVr079/f7755htWrVrFk08+yYQJEzjrrLOoVq1a2F2MrL/++ovKlSsXare2jz76KNtdSosucv5n6dKl1K1bt1C7tS1dujTbXZI8ddZZZ7FmzRouueSSsLsSeWeeeSYbbbQRDRs2ZODAgdx4442cfvrpYXcrJ5x55plst9129O/fP+yu5IxatWpx3nnnMWHCBGbNmsUNN9zADz/8QMeOHZk5c2bY3Yu0n376ibVr13LEEUdw1FFH8cILLzBo0CDuvfdeunfvTiKRCLuLOWPq1Kn8/vvvnHrqqWF3JdK23HJL3nzzTebPn88222xDzZo16dGjByeddBI33HBD2N2LtJYtW/LWW2+xfv1617Zu3TqX1ZQrv4mV1+EpLuVK6ViSDZdeein3338/N910E7vuumvY3Ym8oUOH0rdvX3799VeeeOIJzj77bNasWcOFF14YdtcibcaMGTzxxBO8//77+mwrhZ133pmdd97Z/f8+++zDIYccwo477sjgwYPp2rVriL2LtvXr1/PHH39w+eWXc9FFFwHQsWNHKlWqxIABA3jxxRc54IADQu5lbpg4cSL16tXjkEMOCbsrkbZgwQJ69OhBo0aNmD59Og0aNODtt9/miiuuYPXq1UycODHsLkbWOeecw6mnnsrZZ5/NJZdcwvr16xkxYgTfffcdABtskBsxktzoZRbUq1cv5ZXpb7/9BpAyyiOSSSNGjOCKK65g9OjRnH322WF3Jyc0bdqU3Xbbje7duzN+/Hj69evHxRdfzOLFi8PuWmStXr2as846i3POOYcmTZqwfPlyli9fzl9//QX8W7luzZo1Ifcyd9SuXZuDDz6YefPm8fvvv4fdnciqV68eQKELwQMPPBCA9957L+t9ykXz5s3j3Xff5fjjj0+ZTiSBiy66iJUrVzJz5kwOO+wwOnTowKBBg7j++uuZNGkSr7zySthdjKw+ffowZswY7rvvPjbbbDOaNm3KJ5984iYQN91005B7WDK6yPmfHXfckU8//ZR169YltVveocqDSnkaMWIEw4cPZ/jw4QwdOjTs7uSstm3bsm7dOr755puwuxJZS5YsYdGiRYwbN446deq4/6ZOncqaNWuoU6cOxx13XNjdzCmWaqWoWNFsfWtBNna5MjMcNos+9O3bN+SeRN8HH3xAy5YtC629sZLbWmtdvCFDhrBkyRI++ugjFixYwBtvvMGyZcuoVq1azmSa6FPlfw455BBWr17NjBkzktrvuecemjRpwh577BFSzyTuRo0axfDhwxk2bBiXX3552N3JabNmzWKDDTZg6623DrsrkdW4cWNmzZpV6L+uXbtSpUoVZs2axRVXXBF2N3PGsmXLePLJJ2nTpg1VqlQJuzuRddhhhwHwzDPPJLU//fTTAOy5555Z71Ou+fPPP5k8eTJt27bVxGsJNGnShI8//pjVq1cntb/55psAKrhSApUrV2aHHXZgiy224Pvvv2fatGmcdtppVK1aNeyulYjW5PzPgQceSOfOnenfvz8rV65k2223ZerUqTz77LNMnjyZDTfcMOwuRtozzzzDmjVrWLVqFfDvJlzTp08HoHv37kllCCUwbtw4LrvsMrp168ZBBx1UaOdqffGn1q9fP2rWrEnbtm1p1KgRS5Ys4aGHHmLatGkMGjSIBg0ahN3FyKpSpQodO3Ys1H733Xez4YYbpjwn/zr22GNdimT9+vX58ssvGTduHIsWLeLuu+8Ou3uR1qVLF3r06MHIkSNZv349e+65J++++y4jRozg4IMPpn379mF3MfIeffRRfvvtN0VxSmjAgAH07t2bzp07M3DgQOrXr89bb73FVVddRcuWLV2qpBQ2f/58ZsyYwW677UblypX58MMPGTNmDM2aNWPUqFFhd6/kQt6nJ1JWrVqVOPfccxONGzdOVKpUKdG6devE1KlTw+5WTthiiy0SQMr/vv3227C7F1n77rtvkeOmt2fRJk2alNhnn30S9evXT1SsWDFRu3btxL777pu47777wu5aztJmoP/tqquuSrRp0yZRq1atxIYbbpho0KBB4pBDDkm88847YXctJ6xduzYxZMiQxOabb56oWLFiomnTpomLL7448ccff4TdtZzQuXPnRLVq1RIrV64Muys546WXXkp06dIl0bhx40TVqlUTzZs3T1xwwQWJJUuWhN21SPv8888THTp0SNStWzdRqVKlxLbbbpsYNmxYYvXq1WF3rVQqJBKq2ygiIiIiIvGhNTkiIiIiIhIrusgREREREZFY0UWOiIiIiIjEii5yREREREQkVnSRIyIiIiIisaKLHBERERERiRVd5IiIiIiISKxUDLsDqVSoUCHsLkRCOlsYaez+pbFLn8YufaUdO43bv/SaS5/GLn0au/Rp7NKnsUtfacdOkRwREREREYkVXeSIiIiIiEis6CJHRERERERiRRc5IiIiIiISK5EsPCAiIiLxd91117njAQMGAHDEEUcAMH369FD6JCLxoEiOiIiIiIjESoVEOrXsyplK5f1LZQbTF5Wxa9CggTs+/vjjAdhuu+0A6NChgztnbdYHv/9XXnklAHfeeScA3333Xcb76YvK2OUilZBOj15z6cvVsdtzzz0BeO2111zbt99+C8Auu+wCwOrVq8u1D7k6dlGgsUufxi59KiEtIiIiIiJ5La8jObVq1QLgm2++cW21a9dOus3y5cvd8ZQpUwC4+OKLAc0yRVnYY2cRnKefftq12eyk9c1/voJtfv+tbfHixQB07NjRnfvss88y1ueCfSkNve7+pUhOevSaS1+ujt0TTzwBQLdu3VzbiSeeCMDUqVOz0odcHbso0NilT2OXPkVyREREREQkr+kiR0REREREYiUv09UsTe3BBx8EoFOnTiXqiw3VSy+9BECfPn3cuR9//DHj/cz1kGaNGjXcsaUmDBs2DIDZs2eX63Nnc+wGDhwIwN577+3aDj300EL9KJiK5j+fpaJZmltx93vvvffcud133z2tPhcnKq+7unXruuOjjjoKgJ49ewKwySabuHM77bQTELyfR4wY4c598sknGe9XcZSulp6ovOZ89erVA4L3pv0JMGPGjKQ/M8FST3/66adS3S+KY1ecAw88EIDHH38cgKVLl7pzjRs3zmpfcm3sjH23jh071rWddtppQPBZ+dBDD5VrH3Jh7A455BB3PHr0aKBwkR//3Oeffw7APffc487Za9J//5dVLoxdVCldTURERERE8lreRHKqVq3qjk8++WQAbrrpplL1peBQ3Xjjje548ODBAKxbt64s3UyS61f7FuGAYMbpo48+AmCfffZx51atWpXx587m2N12220A9O3bt9BjpYrIPPzwwwA8+uij7pyVUa1fvz4AQ4cOdedsNsoe6/fff3fnLJKTyQIEYbzuWrdu7Y6bNm0KJEdk2rRpU+LHWrFihTveYYcdAFi4cGGZ+ldSuRTJqVatGhBsuNi1a1d3zt6fr7/+elb6EsXPujp16gAwb948ADbddNNyfb4XX3wRgM6dO5fqflEcu+LY682i3f7r7vnnn89qX3Jt7PbYYw8Arr76agDat29f6Da2xYBFuCH3v2NLy74z7733Xte28cYbA6Uv/GOFfjKZfRLFsatcuTIQFNZK9XnXokULIDlDwrJzMhnpKo4iOSIiIiIiktfyJpIzatQod2yz5PZX//vvv905y2O1fGGbCQY47LDDANh+++0LPf6gQYMAuO666zLW5yhe7ZfGW2+95Y4t4mB/J38Gyr9dpmRz7Oy15UdfCkZtIJjxKEnUxWZMIJg1STUDNWHCBAD69++fVt9TyebYWfRr3Lhxrq169eoArF+/3rU999xzQPC+nDlzZqHHeuSRR4DkqNAxxxwDBOt1ylvUIzk2thCU6e3evXuh21kp3/vvvz8r/YryZ13Dhg2B5E14bdbTXo/FRQr9NTYrV64E4KuvvgJg2bJl7pxtSTB//vxS9S/KY2f8da8Wwf7zzz8BaNasmTvnj0c2RHnsLNLau3dv12bZIwW3uvD99ddfQBARh/KZZY/i2Nma1kWLFgHJf+9LL70UCL6TW7Zs6c7ZGpwtttii0P0aNWqU8X5Gcezs724b8paUfb5ZBLo8trXwKZIjIiIiIiJ5TRc5IiIiIiISKxXD7kB5s8VTVmwgFX8H+bfffjvpnC2ShGBxuIXb/WIGlqqUyXS1XOeX+Y2zq666qlCbpU75odu1a9eW+DH9+1l4NoKZpWVmqaKWqgNBWtB5553n2mbNmlXkY9iC0g020JzNf5kyZYo7Lpim5qemWYGQ0tp6662B4N8E4JdffgFgyZIlaT1mWCpVqgTAzTffDAQpagBPPvkkAP369QPg119/zXLvcoON4ZAhQ1ybpWFdeOGFQPZT1KKoSZMm7vjwww8HghTk5s2bu3OWsvT1118DwfvN98orrwDZWwgeJQWL9JxwwgnunKU8m1dffdUd2287S/G78sory7WfUXTssccm/b+9xgBuvfVWIPgst6IhECzjmDNnDgDbbLONOxeFz0X9KhARERERkViJfeGBadOmAcHsiP/46S7aPvfcc4HUURsrXHD00UeXvrMFRHFxWknYLO6XX37p2mym6o033gCSN84sD7k6dqnYAvziSl9uuOGGGXu+MMZus802c8el3VjXyqraa8uXj4UH/IiWlUG2WfPzzz/fnatYMTmQb6XQ/dvZ4vBUatasCQQlR/37+aX0jz/+eCCIbqYSxfervXYswrVmzRp3rlWrVgB8//335dqHkoji2BkbJz8yaBE9i1AsX748K31JJYyx8yOC++67LwDjx493bVtttVXS7efOneuO77jjDiDIOHn//fcLPb6V3vdL8JeHKL7uLJJjm/T6hX/834AAu+66qzt++umngeD9XB6bbPuiMnYWaYXg726FVrp06eLOvfDCC0X2xQpj2Hfstdde685ZhtM///yTsT6r8ICIiIiIiOQ1XeSIiIiIiEisxLLwQJUqVdyxpcH4IS5b2OynWZSGpXX06tXLtVnY2cLz+cz2PvHry1vKlS3YlZIrrvCAH47PZaVNUfMVLCrij1Mmw+S5wlLUIFj4mWon74L8ned32203AF5//fVCt7N0G1vg7O+sbo//+++/uzZ/b56osn2pbL8MSN4jDYK0O4hGmlou2H///Qu12S70YaaphWnnnXd2x8888wyQnP5j42Op9i+++KI7Z3vg2Hsu1fvZUubzkaXEfvrpp0DyHkP+3nMQpKgB1KtXD4AzzjijvLsYKf6Y1KhRA4Cff/4ZSJ3+bfzXnY35JZdcAsCYMWPcuS+++AKAiRMnZqjHpadIjoiIiIiIxEosIzl+BMEWJfts4WO6M0k2m5KqJOp2220HJO/w7M/ExJnNfI4cObLI2/jFCKRkCi449P8/18ryZoqVoQU44IADks5NnTrVHdsC1Hxg5fJtV3QIXiupFq2efvrpQLCYuaRsQarNJvuPbZ+p48aNc2333XdfqR4/DLZIO9WC48mTJwOKQqfDClHYdyYkF7goqG7dukAQjWzXrp07Z9s5+FHCXOQXErCojl8K2l5nfvGOgg4++OAiz33yySdl7WLOswwHW/gOQcloi0I0aNDAnRs9ejRQfHGUOLKS2RBsiWK/30qz5QXA2LFjAWjfvr1ra9myZVm7WGaK5IiIiIiISKzEMpLjb3iXyl133ZWR5/E3l7INkayU72mnnebO5Uskx3LvU+XgL1y4EICnnnoqq32Ksg4dOrjjgvnC/v8XtybH3zQ0n/ilLwtuiJevM+4WyfFneQu+Zq644gp3PGnSpBI/9oEHHuiOb7rppqTH9iPiPXr0AFKv5Ymy4jats3x92xQUgjL5VpbWj2bZmgjbQK/gBtP5wMpD25rYb7/91p376quvkm7rbx44c+ZMIPUml7aG1taLlXamOSpsA2SAefPmJf1ZUrVq1SrU9s0335StYzFy5513Asm/wyxyY59b/nrWVBt656tUZckL2nbbbd3x5ptvDgQbdtt6KAg+O8OkSI6IiIiIiMSKLnJERERERCRWYpmu5oe/U/n4448z8jwPPPCAOx4yZEjSc/vlWG0R5bJlyzLyvFHi75puJQRTLXK2XXBzfdFoWQwcOBCAiy66CEhe+Ggh9FSlflV4oHQs5SUfWDoQBJ9H/nvSWGlkPy2jJOW1LW3ylltucW2WnrBq1SoguchArqWpmcceewyAE0880bVZSVU/Va+gNWvWAMmFME444QQgSN2zBfMQLISO+/vWPussfTtVcQsrMuCn81nbSy+9BMCff/7pztm/g5Uwz9V0tbKw9J9TTz0VSP4usIXfEmwTctlll7m2ggUv/LSsfHwtlYaliB599NEAHHXUUe5c48aNAdhrr72A4HcgpP4uyjZFckREREREJFbCv8wqB/7sRqqogpUSLCubxQP45ZdfAGjWrBkANWvWdOeicDVbXnbZZRd3bDNtqRbI51vBAYvS2MZuEJTeTVVIoOCYpRpDa/NngV977bUM9Tg+Onfu7I7jvjGelYEG2GKLLYBg410IIqgWaS4pK+xgEQ57bP/x+/XrB8CDDz5Y2m5HzgsvvADArrvu6tosYlUc2zhvk002cW32mWjFaGxzZIDWrVsD0L17dwB+++23snQ7smwcbZF9quI7hx56KAAbbBDMtdpnpG0060cJJRjX2rVrA8kFMzJVUClODjnkEHdc8HvXMiogKLudbyWki+N/j1rRFft967Pf2JdeeikAvXr1cueKK4OeLYrkiIiIiIhIrMQyxFDcDHl5sQ0I995770LPa1e2VtYwTvzczIJGjRrljr/44otsdCdU/hobm4m0zWGhcFQxVR6wn9tf1P3853nmmWcAaNWqVRo9zl3+5oJWnnarrbYCkmd/jz32WCC5XKix6Ovzzz9fbv0sb2eccUahNr+kc2kjOMY2wPRLhRqLDsUhglOQX964YKnj4vil3K2UqpUyv/rqq925nj17AvD0008DsOeee6bf2YjxyxrbxtBW1thfBzt8+HAgyN23MYEgomb8zzp7XfslmOPCHzvblNFsueWW7tiig8bW+0KwJu+NN94odNsqVaok3c//TrGNy5cuXZpO1yOpW7duQBAZBPjhhx+AYF2T/11rpfVnz54NwOLFi7PSz7B99NFH7njlypVA8NmUKiPKxtAi/ABnn302kByRjZJo9kpERERERCRNusgREREREZFYiWW6WtRYWcw48gsPFDRx4sQs9iR8tiM3BGlqqdIlbZGjX97SWLnZ4goP+Ox5LPVj9OjRpe12TvKLfhxwwAFAkB7Zu3dvd85SYfyUGGMllN966y0gWAwOsHr16gz3uHzYwncI0jD8Xaa//vprIEhh8T3xxBNAkPp3wQUXuHPt27cHgteclaCG9FPg8s3nn38OJJdPtrLS/7XNQS7yU+8KpkcNGzbMHdtnlZUzt5RbX7t27YDk962lUObKe7MofjrarbfeCiSnVVlJ3lQKbjHgFzWytHj7/CsuVd//PIjC4vBMSzUGVga5fv36AMyYMcOds+9RKxRUXNn4OLFS2xCkqxVMl4QgRdn+TLVMwU/ZjRJFckREREREJFbyMpKz6aabAvDTTz9l7DH9zT8LimNpx4MPPhiAfffd17XZxm3nnHMOAD/++GP2OxYCW+R43nnnubZUpcsPP/xwIChT6Zflfeedd4q8X3GbgRp77vvuu8+1ff/99yX7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2iKON910U7n2oSSiOHYFtWjRwh1bOWkrxONH/K3NNob3PwfKY0PkXBg7n0UObZsMf5NzfwPybFAkR0RERERE8pouckREREREJFZiuU/Of7HFyLbjtb/zdao64Pnul19+ccevvvoqEOwnJJJNtj+TpXJAUEDA0kmnTZvmzv32229Z7F10+Xu1GEurylf+Xhjdu3cH4NZbb3VttqdQnTp1Ct3XCnfYHkv33nuvO2d7wmRrIa7Ey8KFCwGYPHkykFx4wAraSMn4BX0s5cqKEfhpgFa8xj4TyyNFLZfZnlaWMjdr1qwwu1MqiuSIiIiIiEis5GXhgVyRa4vTokRjlz6NXfqiWnjAZi2//PJL12ZRWSuFH+bspV5z6dPYpU9jlz6NXfpybexsu5VJkyYBQVEVKJ8S28VR4QEREREREclriuREWK5d7UeJxi59Grv0RTWSE3V6zaVPY5c+jV36NHbp09ilT5EcERERERHJa7rIERERERGRWNFFjoiIiIiIxIouckREREREJFYiWXhAREREREQkXYrkiIiIiIhIrOgiR0REREREYkUXOSIiIiIiEiu6yBERERERkVjRRY6IiIiIiMSKLnJERERERCRWdJEjIiIiIiKxooscERERERGJFV3kiIiIiIhIrOgiR0REREREYkUXOSIiIiIiEiu6yBERERERkVipGHYHUqlQoULYXYiERCJR6vto7P6lsUufxi59pR07jdu/9JpLn8YufRq79Gns0qexS19px06RHBERERERiRVd5IiIiIiISKzoIkdERERERGJFFzkiIiIiIhIrusgREREREZFYiWR1NREREclPDRs2BGDs2LGu7YgjjgBg9uzZAHTu3Dn7HRORnKJIjoiIiIiIxIoiOQVssMG/131XXHEFABdffLE7d/755wNw3XXXZb9jOchm3gAeeOABAJYvXw4kz8K999572e1Yhh144IEAHH300a7NatofdthhhW4/ffp0IPm1tXDhwvLsoohkUN++fQHYZJNNCp078cQTAahRowYAn332mTt39tlnAzB//vzy7mJOatCgAQDPPvssAG3atHHn5syZA8CYMWOy3zERyUmK5IiIiIiISKzoIkdERERERGJF6WoFdOrUCYAhQ4YAsH79enfu9NNPB+Cxxx4DYMGCBe6cfzspLJFIAFCrVi0AOnTo4M7larpaq1atABg/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z/bX4pj77rsPCNbk5EumQyb98ssvhdoK/u6z728IIj+p7pctiuSIiIiIiEis6CJHRERERERiJW/S1fzd5T/99FMA5syZAwSlff/Ll19+CQSL2dLdATvX+OU///nnHyAo+2n/D/lXurIkUhVosDS1Cy+80LVZ6N3S2/y0GXu9+WlqxnYBt3K3fmpMLqV8tG/f3h3vvffeAFx55ZUZe3wbux9//NG1WfpgvryP02EpWoMGDXJtV111FRAsEo+Tjz/+2B0/8MADAPTv3x8I3qMQjMu55577n4+5bt06dzx16tRCzxMXI0eOdMeWpnLAAQcAyWm7liZku67nW5pVaVnBGQhSgew7xF6b+chStf3v2IKL3/3y0nZ7u419jknZ2O++J554Agje1wAXXHABEKRZ+r8Xs0WRHBERERERiZUKiVRTzSEr740m7fE333xzAE499VR3zmZNrCTv999/787dcccdQPJscHlK55+mvMfuxhtvBODMM88EkheNpirgEJYojt1+++0HBJsD+guZa9euDQRRGyskEIYwxq5Nmzbu+IUXXgBKHmE11u8FCxa4tgkTJgDBgm8r411eSjt2UdhU14o6vPPOO67N31AVgg1sIZgRzWTkNorvV2Oz5f5i5oIbx/rfEwVLr9pGv1A+i52jPHZRlwtjZwvmIfh9Ym3+RrPZFvbYWfGPevXquTbb1mO77bYDYIMNgnl8i+AMGzYMCLfwQNhjVx6sWNJdd93l2uzvacUelixZUubnKe3YKZIjIiIiIiKxooscERERERGJlbxMV8sVcQxpZovGLn1hj52lrg0dOtS1WWEF4++SvnDhQiBII73nnnsy1pfSysV0NeMvcL733nuTzvXq1csdP/nkkxl/7rBfc7lMY5e+XBg7+3yDIE3y/PPPB+CGG27Ial98YY+dFQIZO3asa2vQoAEQ9M0vLmDHUdjnK+yxKw9WjMpfwtCvXz9A6WoiIiIiIiIZo0hOhMXxaj9bNHbp09ilL5cjOWHSay59Grv05cLYpYrkdOvWDYDnn38+q33x5cLYRVWcx84vqGTZFYrkiIiIiIiIZEjebAYqIiIikku+/fZbd1yjRg0Afv7557C6I1IsP/Lol/AOS/g9EBERERERySBd5IiIiIiISKyo8ECExXlxWnnT2KVPY5c+FR5Ij15z6dPYpU9jlz6NXfo0dulT4QEREREREclrkYzkiIiIiIiIpEuRHBERERERiRVd5IiIiIiISKzoIkdERERERGJFFzkiIiIiIhIrusgREREREZFY0UWOiIiIiIjEii5yREREREQkVnSRIyIiIiIisaKLHBERERERiRVd5IiIiIiISKzoIkdERERERGJFFzkiIiIiIhIrusgREREREZFYqRh2B1KpUKFC2F2IhEQiUer7aOz+pbFLn8YufaUdO43bv/SaS5/GLn0au/Rp7NKnsUtfacdOkRwREREREYkVXeSIiIiIiEis6CJHRERERERiRRc5IiIiIiISK7rIERERERGRWIlkdTURERGJr0022QSAli1burbNNtsMgLvuugtIrihlVZWmT58OwJlnnunOLVmypHw7KyI5SZEcERERERGJFUVyinDHHXcA0LdvX9e2cOFCALp27QrA/Pnzs9+xHLDBBv9eO7/55puurW3btgDssMMOAHz88cfZ71jI7O8+bNgwAI466ih3bvz48QBUrly50P223nprAF566SUgedbS7icikku6dOkCwKRJkwqds6hNqj0xGjVqBEDt2rVdmyI5IpKKIjkiIiIiIhIrusgREREREZFYqZBIFQ8Omb/YMCxvvfUWALvvvnuhc5a21qxZM9f2xx9/ZLwP6fzTRGHsOnfuDMCzzz5b6NzAgQMBuPHGG8u1D1EZu1NOOcUdT5gwAYANN9ywTI/p/90WL14MBGOeiRTKqIxdLirt2Gnc/qXXXPpyYewqVarkjq3gwPPPPw/ANttsU+j269atA+Cvv/5ybV9//TUAvXr1AuC7774rc79yYeyiKhfGbvjw4WV+jMsvvxyAl19+GYARI0a4c9ZWWrkwdlFV2rFTJEdERERERGIlrwsP2AJ5/wp5xx13BGCnnXYCYP369e6c3a5JkyYATJw40Z2zq/2vvvqqHHucG6pWrVrkuVWrVmWxJ+Gz1xiUPYJj/Ndrw4YNAXjnnXcAOOecc9w5//WZ6+zvbEU/jjzySHdur732AuD7778H4OSTT3bnfv755yz1MDr8WfNjjz0WCIpe2OcUwJo1a0r8mH5BjM8//xwIIpNXXXVV+p3NAePGjQPg/PPPd232mrOIf3H23HNPd2y3t9fv4YcfXuj2RxxxRKG2XJ/FveGGG9xxv379irzd6tWrAbjooosAFVaR/9axY0d3bJ9vflumn8d/7P322w9IP6ITJfb7xLJthg4d6s5ZkQ/7HPKjKdZmkVn/M23lypXl2OOSUSRHRERERERiJa8jOaeffjoAffr0cW02G2yzoWeffbY7ZzOfNrt09NFHu3NW3leRnOQZz4Is4pAv7r//fndsUYjDDjss489jM+2jR492bXPmzAFg3rx5GX++bKhTp447vu2224BgBtzP1bdS5bYu6dFHH3Xn9thjj3LvZ9Tceuut7vjUU08F4MMPPwTgl19+cefGjh1b4sf0y51vscUWQDDrHlebb745kPrzzF5zDz30UKFzFrmx++ezNm3aAEHkK5UpU6a44xdeeAGAe+65p3w7Vk7sfbLbbrsVeRs/OrV27VoAVqxY4dp+//33cupdvERhOblFdXI1krPpppu6Y3vPWXTKZ2P95ZdfAsnbg9jvmk6dOgFw9dVXu3NnnHFGhntceorkiIiIiIhIrOgiRyuHefkAACAASURBVEREREREYiUv09W6desGwJVXXgkkL5QvuFBq6tSp7nj58uVJ5+688053bCG6F198EYAFCxZkrsOSs/zS4ieddBIA5513Xqkew0LKF198MQC9e/cu8rYNGjRwxxZKz4V0NVsYD3DhhRcCQalYCErKplqQbO9ZC6Vvt9127pyNh5XajrOePXsCQbEBCF4zjz/+OBCk45aWv5jUSujfe++9aT1WrhgwYMB/3iZVkYCSsDS3H374wbVZepv/mMWl/kbZgw8+CARbMDRt2tSd++effwCYNm0aAOeee647t2zZsmx1sVzY1gj169cv8jb+v6kt2n799ddd27fffpt0+3fffdcdv/LKK0U+7hdffAHEO92tLCWhLaWsuDG02/gFWsqjiEHYKlb896e//1q0NDX7zXLNNde4c/b7NlVp9ypVqgDw8MMPA8kl4e157H5hUCRHRERERERiJW8iORtvvLE7HjlyJAA1a9YEkjettCt4m21PVQLPZjBPPPFE12ZXwWeddRYAgwYNyljfc4WNcXElpPOZzbCVdqbNyiDbgvriIjl//vmnO7ZSv1FUrVo1AC655BIgKAICQcEBv2iDFQDxF+gWpVatWu7YZpnywZgxYwD4+++/XZuNoR8xKA2LIvoLx222uiT/FrnGLxZQmihKqgIE06dPL9RmEY7i+OXRS3L7KLKNsi2CM3fuXHfOCqLYd2Wc2CLsm266ybXZe8gKdqTSrl27lMcAxx13nDtOVcLXWHlyK4JkWSWQuwvj01XWkut+9Ka4SE6ujqsVR0kVrb7sssuAoHT+f7HIjxW3GTx4cKHnmT17dvqdLSNFckREREREJFZ0kSMiIiIiIrGSN+lq/t4Zu+66KxCEzf19Syz0dsIJJ/znY/oLfD/77DMg2JfD3+l+/fr16XY7p3Tv3h1IvUeAjfWPP/6Y1T7FgS3Kv+WWW/7ztv4O7DNnziy3PpXVzTffDATFGGzRIgQpVo888kiJHqtly5ZAkBbi3y/uBQf8vbq23357IHlvgnTT1Izts+MvpI5yGmRZ2WL4VPy9gsozjSxXU9T8ggn+/huQXKTn9ttvz1qfsu2DDz4AoEuXLq7Ndou3IihWjAGgQ4cOAOy9996ubauttkrruS01yP4cMmSIO/fJJ58AQQql/12S68UeUrF0vhEjRri2khQtsNQ0v/BAcXI1Xc3fm8pYcRr7bi6p1q1bA8kp58b2rlO6moiIiIiISIbEPpKz7777ArDPPvu4NisFbeVo/TK/pfHrr7+641mzZgHBAt18ieTYAnKACy64oMjbWSQnjouVy4M/szd06FAgeawLstewv9twlL399ttAMHtU0qhNKsOGDQOCghdWNhmgbt26Sbf1CzMsXbo07ecMmxX5sMINPvssygQrHW1lfyH9MtRRZov9/QILxooK5GqEJVssggBB9GLy5MlA8RGy4vgRRHt/r1q1Cii8pUOU+MVl7NgKyPgl/SdOnAgUXyylf//+7tje96kKD5x88slAMGZWvhdgxx13TPrT38bAiiRYQaYo86Mx9tuuuMIAfkTGji364kd5ShPB8e+XS/zfFP5WE8aKEPjfkSXRqlUrIPk1bJ555plSPVZ5UCRHRERERERipUIi1ZRAyMpa/q9hw4bu2MooWt4+BLmDfp5wWc2YMQMIyvv6kaM33ngjrcdM55+mrGNXWv7snb+hGSTPtNkMqW1YVt5yYexSsfH0N7u0nNdU1q5dC8A555wDwN13313mPuTC2PlltC2/2GZ6/ddY8+bNk+7nRxJt5spKwmci4lrasUt33KzUsR9V+eabb4BgzSGUfbbbol221gCgU6dOZXrMVMJ4zfmfXRal8UtIl4ZfQtoi2mVdD1VSYb9fbcz8tTb2GvS/i0vCZoPt/X3mmWe6c7bW09Ya9uvXz51Ld61n2GNXnvwIx4EHHggEEZxKlSoVur2ffVISURk7P3Jdnht3WgTItgspizDGzt/Q2SKrzz33nGvr0aMHUPqNO//v//4PCDbx/uqrr9w5W3+WaiuWdJV27BTJERERERGRWNFFjoiIiIiIxEosCw/4IXJLU7NUDshsmlpRrJQ0pJ+ulguK28XZ/3tnK00tVx1zzDFAkKZWo0aNIm87f/58d3z99dcDmUlTywUbbbQRAGPGjHFtlqZmpVBtYTLANddcAwTpLP4u9nfddRcQLI5ctGhReXU7K2677TYgMwuyLZ3FUlhsN2ufFcKwRfsQlBH2ywmvWbOmzP0pD34f001TS/VYdmwlp+NYsMBPd7L3kZ/G4+96/l/8tLNu3boB0KtXryJv37VrVyBYRA/amiAVv7yxHbdp0waAAw44IIQelQ//dWeFCUpSlKAszxMX/tYqJUlT22WXXQAYOHCga7My0cbKlUNm09TSpUiOiIiIiIjESqwiOTbraCV3faNHjy7X57aFp/6C6Hxw/PHHh92FnGMlPm+88UbXZhupFhfBsdk4i/pAchnzfGAb5W233Xau7emnnwaCRbX+wseC/Fmm559/Pukxcz2Skyraki6bUbdSwL/88os7ZyVnbTGzvyD2uOOOA6IbvfFZFBSCkuZ+BN7arLiDv9GuH70CGDt2rDu2qJAt7vU3xrzuuusy0vew+eWJU81w23urOBbxGjdunGuzEslSdpUrV3bHFvm2mXjfd999l7U+lbeCG376/1/SDT4hOQoWxwiO8SOmVjjEfkf7kdJLL70UCL4XCpY5jzJFckREREREJFZiFcmxcpOWCw3BxkbPPvtsuT53wVng0047zR3HZfautCZNmhR2FyKlXr16AEydOhWA/fffv0T3sxKZxx57LJB/0RufzTrutNNOrs3We5VkU9/tt9++UJu/xilXpIqgvvTSS2V6zA033NAdW3TLWJlQCNY8WXTcXs+QWxuF+iWe7bik62cK3s7/f1uLaGXzr732Wndu+vTphZ47X1nURtGb8jFhwgR3fMIJJySd89co2yx9nJRmc09fJstER827777rjr/99lsgWN8GwVYMm222GZD8fWBRHfv8st8iUPrS49kW7d6JiIiIiIiUki5yREREREQkVmKVrmblZX22GNlfOFse/LAfBDuFx1WLFi0AaN++faFztmDX3003X1kJcwgKDZQkFO4vfCxJmlrjxo2B8n+dh83ST+fNm5fW/f3Sl6tXrwZg/fr1Ze9YBNgu835awj///POf97OF8SNGjHBtp556atJtHnjgAXdsJfhffPHF9DsbAvu3t3QMv/BAJtPHrEz5m2++WeicpbDlW7qaXybayhjb94Rv4cKFQPBv5b9f99xzz/LsYmz0798fgBNPPNG12S7xlu7rp6j57+1c5JeJtvS0TJaOjosFCxa4Y/v3nzx5smsrWEzFt3btWiBI9456ipovd3oqIiIiIiJSArGK5Fhp3a+//tq1ZWuWolmzZkn///DDD2flecOy++67A1CzZs1C56zUai6Ukc2kunXrumNbbOxvElhc2UUbK5uJuv/++905i+DYDPSWW27pzp1zzjkA7LDDDkDyInpbiH/SSSeV8m8SP1aau06dOq7NFubaLFUusRm4K6+80rXZ+84v32vRKiurve222xZ6LHs93XHHHa7toIMOAoINP/2Z3+JKdEeZXwAAkjeQzGRxGL/UdL6yEu8Wtb7qqqvcOcu4sLL5H3zwgTtnC+Rts1F7/frs89Df9Ddf+WWi7fPMj+CYjz/+GICePXsCybP6ucrKQ5e2uEBxLALkR4L8rIq4eOKJJ4Dk8u2W9eSXhzdWHMSyBT777DN3rk+fPgAceuihABx22GHuXK1atQBYsWJFxvpeWorkiIiIiIhIrMQqkmObLPqzP+U523PyySe74x49egDBjPxtt91Wbs8bBYccckiR5+IexSrIZsJPOeUU11awZGcqy5Ytc8e2PuCRRx4Bktfy2GzLvvvuCyRvLliQrZUC+Ouvv4DkGfrZs2f/Z7/iyN6rtrkl5HZpd1u7YJ95AIMGDQKCKIzPIs32moBgdt0iP/5sW+fOnQHo0KEDEKyFymW2YbNFV8urtLNtBprPSpJBYesHbdNUCEqjDxkypNDtLYJz+umnA/n7WQZQtWpVINiYF4LvHFt/Y9EbgNatW2exd+Un1fqbkrI1h/Y9Wty6nbhHcuw38uDBg13bjBkzgCDCmirqb7/t7HcKBGtae/fuDSRvP2DZVWH+HlYkR0REREREYkUXOSIiIiIiEiuxSlfLliZNmgBw6623ujbbHfbqq68G4Pfff89+xyQUtggvVQnz4tiuwxCE0v0yvunwyyHb4tJ8TuswVor2yy+/dG25XObdSkP7f4eLLroo6c+ysIWplpbaqFEjdy5Xyx9bSWe/GIixlNDiyqgWx09R89OvpGhWvMb+/C+WDvzss8+WW59yRfPmzYHkFGljqZepUv5yXWlLQ/vbNRRMO7O0PvmXlXRPVdo9XZtssknGHitdiuSIiIiIiEisKJJTCnZV+tRTTwHJ5RsnTZoEwNixY7PfsSyyhe3dunUrdM5Kp77zzjtZ7VOu2mWXXdK6n19Mw19IDsmzyFZeOp9tvfXWQBCJsEXLEJTYlsJyOcpVFCs0YZEc25jTb/NLql5wwQVFPpZtTGn386NDBQsPWMEDgAcffDCtvuerL774wh2/8cYbIfYkGqyE77Bhw4q8zciRI4F4lIlOl0Vw4lg0QEpHkRwREREREYmVWEZybCMxCEotlnaNjK27sU33ANq3bw8E62/8DZFshthfExFHe++9N5AcxTK77bYbEEQotCle6dlaC3+Dyp9++gkINnt7+umn3blc3ZgxW2wTS4t42VoTKd73338PBON29NFHu3PvvvtuKH3KlKOOOgqA119/3bVZ9OX88893bXZskRjbjBeSo0BFsfsVFxHKVX///bc7tuixjWsmzJ07F0hec7Jy5cqMPX6usvehberp69SpEwCffvppVvuUTbYBKBRfQnrWrFlA6jU5pV3XIyXz3nvvFWqzsuaZ3LC1tBTJERERERGRWNFFjoiIiIiIxEqs0tUsFeXss892bS+88AIAZ511lmv74IMPgKCQgL8bsC1Qth3oa9WqVeh5bAHkaaed5trinqZmDjjggCLPWenBOXPmZKs7kWDpY7169SrR7S3Vwy9haaWNR48eDagMbVlcdtll7vikk04CgrLKK1asCKVPucbS1aw4g6X9xoGVwL7wwgtdmxWMKVg0AFKXnC7qMSEocGB/xpGfrnbuuecCcMcdd7i222+/HQgKf/hjYWW658+fD8A111xT6PEtRdcvPCBBqlWFChUAePTRR925fFtkX5L0M0tbS/expeReffVVILloTbVq1cLqjqNIjoiIiIiIxEqFRAR3RLJZitKqV68ekHz13qpVKwCWLVvm2uxKs3r16gA0bty4RI9/1113AUFU6M8//0yrnyWVzj9NumNXUhb9evHFF4FgDAH2339/IBqL4cMYu8GDB7vjdu3aAbDpppu6Npt1u+2224DolumN4uuuJLbccksAPvroI9f2/vvvA0HJc7+gQ3ko7dhFYdyK89xzzwHJi+5tMe+iRYsy9jxhv+YsguOXkDYWyfFLQVvkxtrCLLIS9tjlslwYu4MOOsgdW7aKFVKyYgOQ/ddgVMbO/71X1qIC2So9HZWxKw9+YSQree5vJl1WpR07RXJERERERCRWdJEjIiIiIiKxEqt0tbiJc0izvGns0pdrY2e7zz/11FMA1K1b153baqutgOzt/h23dLWGDRsCQdofwPTp0wG44YYbAFi4cKE7Z4UKSivXXnNRorFLXy6MnZ+GtsceewDBnkH+uf79+wPwzTffZKVfURw7S1ezP4vbn8VPSRsxYkShtvIUxbHLlEmTJrljS7VUupqIiIiIiEiGxKqEtIjEW4sWLYCgDC0EO9PbDM+hhx7qzn333XdZ7F38/Prrr0ByeXRbbL/jjjsC0Lt3b3cu3UiOiKQ2fvx4d9y2bVsgKPjjf779/PPP2e1YBFkkxv4cPnx4aH3JVxbhB/h/9u480Krp///4MxEiQ0plKqQy9WlCkimRKaESJVPITMjQJJQxTWYyRCiRKZIhGaK+SMgsJBSSMZHS7w+/99pr33vu6Zx9z7jP6/FP217nnrPuss85d6/3e71X7dq189iT/yiSIyIiIiIisaI1OQUsznmb2aaxi05jF13c1uTkiq656DR20RXb2FkE2yKstjE5wPLly3Pal2Ibu0KisYtOa3JERERERKSk6SZHRERERERiRelqBUwhzeg0dtFp7KJTulo0uuai09hFp7GLTmMXncYuOqWriYiIiIhISSvISI6IiIiIiEhUiuSIiIiIiEis6CZHRERERERiRTc5IiIiIiISK7rJERERERGRWNFNjoiIiIiIxIpuckREREREJFZ0kyMiIiIiIrGimxwREREREYkV3eSIiIiIiEis6CZHRERERERiRTc5IiIiIiISK7rJERERERGRWFkz3x1IpEqVKvnuQkFYtWpV2j+jsfuPxi46jV106Y6dxu0/uuai09hFp7GLTmMXncYuunTHTpEcERERERGJFd3kiIiIiIhIrOgmR0REREREYkU3OSIiIiIiEisFWXhAREREStMtt9wCwGmnnebO9e3bF4AbbrghL30SkeKjSI6IiIiIiMSKIjnA3nvv7Y4vv/zy0L++xx9/HIDDDjsMgJdffjkHvZNituWWW7rjM844A4D1118fgKZNm7q2N998E4A777wTgPnz57u2v/76K+v9FBHJty5dugBBBOeHH35wbY8++mhe+iQixUuRHBERERERiZUqq6LsSpRlud706IUXXnDH++67L5B8wyHLDR4xYkRW+6UNo6LL99g1aNAAgLffftud23jjjVP++VmzZrnjU045BYC5c+dmpnOrkY+xO+aYY9yxH/0qy/LxV65cWanXyxZtBhpNvt+vxazYx659+/bu+LHHHgPgl19+AaBJkyaubenSpRl/7WIfu3zS2EWnsYtOm4GKiIiIiEhJ002OiIiIiIjEigoPAL///ntaj589e3aWelJ8atSo4Y6nTp0KQOvWrQG45pprXFu/fv1y27E8s+IC6aSo+XbbbTd3bON6wQUXADB+/PhK9i4/xo4d644PPvjgUNuGG27ojtdcs+KPJUsV9X366acAjBkzplybpbXdd9996XVWJKK6desCUL16dQDWXXdd12app4lssskmAPTo0cOdO+mkkwC49957M93NgtGnTx93bGM2aNAgIDspaiIAG220kTtu27YtEBS+6Nq1q2uz9++CBQsAuOeee1zb4MGDs91NqSRFckREREREJFZUeAA45JBD3PGTTz4JJF/c9NBDDwHQs2fPrParGBan2cwHwIQJE0JtK1ascMeHHnooAM8//3xO+pXvsdtpp50AeO+998q1vfjiiwDMnDnTnfvxxx+BYKZ3m222cW02k7Ro0SIgHAWZM2dOxvpssjV2/vPm6mPHXmfy5MkAHHXUUa7t77//ztrrpSpX79datWoBMGXKFHeuRYsWQBApLBtd8/n97NatGwCdOnUCwkUjosr3+zWqrbbaCoBevXq5c/YerlOnTqWf/5VXXgGCgjiJFOvY2XfHww8/7M5Z5MbPEMimQh47+9z3o9y23UXLli2BoMANQOfOnQF46623AGjXrp1rK5WiDWus8d+8vb1fLEIDsM8++4Qe628dYr799lsgnK0zb948ALbeemsAOnTo4NoOOOAAAF577bW0+lkoY3fEEUe440mTJgHw9ddfA+HfyYpz+VGsfFHhARERERERKWlakwM0atTIHdtMwL///lvh420G058lSPdOPi46duxYYdtaa61V7nG5iuTk22+//QbA4sWL3TmbTd9vv/2A8GagZ599NgA777wzEF7PdP755wNBrv8zzzzj2iziUwwbhlqePQRRAxsDf93CZ599BsB3333nzg0cOHC1z3/qqacCcNBBB7lzts7B/j/40cVS8uWXXwKwbNkyd+7KK68EUssr99eS3HbbbQBMnz49cx0sMmeddRYAl156KQD16tVzbVGjlD///DMQzKhCUDI9jvr371/u3JAhQ/LQk9ywWfPDDz/cnfvzzz+B4D0FQUTGIg9t2rRxbTabn+gas3MW5bGsFAi+c+Lo+OOPd8e2btUyKT755BPX9n//939AEH3xI/m33HILAAMGDADCn5Omfv36QPBZCvDHH39U/hfIA/tcsQ3KIbh+bAsHP0JvGRCNGzcG4JJLLslJPzNBkRwREREREYkV3eSIiIiIiEisqPAAwaIqCBasJSorbWWBrX8//fSTa7PUNStnmwmFsjgtGb8s8LHHHlthXywVY/vttwfghx9+yGq/CmXs/FLQlh60++67A+FSyZbelmixsoWNrQxy1apVXZuNv6UTZSIdK5djt//++wPBewuCtAJbBJqqtddeG4CJEye6c1bwwviLeNMtHZ+KQis8YKl7lrpy0UUXubYRI0ak/DzLly93x/b/pXfv3gA899xzle5nobxfk/FT9iy9xdKb/b788ssvQJAOY2mqAOPGjQOC74k777zTtdni8ESpMskUw9j52rdvDwTXjRVigSCVK1elo3M5do8//jgQ/kxKln5m5s+f744tldce/9VXX7m2jz/+GICrr74aCK5DgM022yxSn5PJ93VnqaL2vQrwwAMPADB69GgA3n//fddmn2H2ftx2221d26xZs1b7ehdeeCEQTtWyv2escFCq8jF2Vp4dYO7cuUC4cIV9755zzjlAeMmGpQRaSvh5553n2m699dZK9StdKjwgIiIiIiIlraQLD9hiPH9hny0EPPHEE4Fg5gTg5ptvBoKoTc2aNV2bbYCZyUhOMbj88svd8d133w3AtGnTyj3ONt5KttFjHPkzRLbg0fhlov2NycqykuVWVtVfpGozLDZT/9hjj1Wyx7kVtRCFX9TCihbYzF7Z6A0EkbICDFxnnL/IeOTIkUDwez/yyCNpPdfFF18MhKOHw4YNAzITwSkmu+66qzu2CI7xZ9ttawErRuMXZ3nqqaey2cWCVbt2bXdsEUC7JocPH+7a4rz5p/1NMXToUHfuzTffDP2biJ8xsnDhwtW+jr1n11tvPXduhx12AODDDz9Mo8eFxwoKQBDB8cfzsssu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cMIJQLA/i+13BkE6RSJvvvkmEN7J+8477wSCtDWRbEm0X5ilTsWxGE2qbIG7pckDdO/eHQjenyeffLJrmzVrVoXPVb16dQC6dOkChIuHZDJNrdTccMMNAAwdOhQIUv8AGjZsCMDnn3+e1T4okiMiIiIiIrGiwgMFrFQWp2WDxi46jV10hV544Oijj3bHDz74IACdOnUCgmhrPuiai05jF10xjJ1futyiiWPGjAGgd+/eOe2LrxjGrlCVythZee9PP/0UCP8OzZs3B1Iv5W9UeEBEREREREqaIjkFrFTu9rNBYxedxi66Qo/kFCpdc9Fp7KLT2EWnsYtOYxedIjkiIiIiIlLSdJMjIiIiIiKxopscERERERGJFd3kiIiIiIhIrBRk4QEREREREZGoFMkREREREZFY0U2OiIiIiIjEim5yREREREQkVnSTIyIiIiIisaKbHBERERERiRXd5IiIiIiISKzoJkdERERERGJFNzkiIiIiIhIruskREREREZFY0U2OiIiIiIjEim5yREREREQkVnSTIyIiIiIisbJmvjuQSJUqVfLdhYKwatWqtH9GY/cfjV10Grvo0h07jdt/dM1Fp7GLTmMXncYuOo1ddOmOnSI5IiIiIiISK7rJERERERGRWNFNjoiIiIiIxIpuckREREREJFZ0kyMiIiIiIrFSkNXVREREJL4aNWoEwMEHH+zOde7cGYDRo0cDMHHixNx3TERiQ5EcERERERGJlSqrohTszrJc1QP/+OOPAWjcuLE7d+uttwJw9dVXA7BgwYKc9CWRYqilPmPGDHc8e/ZsAM4+++yc9iGRYhi7QlXIY9elSxcgPMNr/X3vvfeA8DU5fPhwAObNm5eT/sVtn5waNWoA8Prrr7tz6623HgB77LEHAAsXLqz06xTyNVfoim3sdtttNwAmT54MQM2aNcs95p9//gFgyy23dOd+/PHHjPel2MaukGjsotPYRad9ckREREREpKTpJkdERERERGKlJNPVdtxxRwCmTZsGQK1atco9ZsWKFQBceeWV7tyQIUOy2q+yiiGk6acGNW3aFIDtt98egG+++SanffEV4titvfbaQJCC0b1793KPWWuttQDo0aNHSs9pzzFz5sxMdBEozLEzhx56KABPPvlkSo//+uuvAWjQoEG2uhQSt3S1unXrAonfy82aNQNg7ty5lX6dQr7mErGUvTp16gDwxRdf5K0vxTB29p0L8MorrwDBGD7//POuzb6Tt956awDOP/9812bfyZlUDGNXqIpt7GrXrg3AscceC4SXKZx66qlA8DtVrVo1q33J5dhVr14dgP3339+dGzhwIADNmzcv9/gxY8YA0Lt3b3duk002CT3mp59+itSXTFC6moiIiIiIlLSSKSF93HHHuePbbrsNCGbWE1lzzf+GZtCgQe5c3759AXjggQeA8Oz5fffdl7nOFqn1118fgH322QeAcePG5bE3+bXTTjsBwTUDQYSrVatWkZ7TZosffPBBd+6dd96J2sWiZLO+J554oju38cYbA7DzzjsD4fe6zbTbrPHSpUtz0k+Jt5EjRwJw0kknAeH35L333ht67BlnnOGO7XslVfZcjz32WIReFo4LL7zQHdv7dc6cOQB07NgxL32S+Ovfv787PvnkkwHYaqutgHBEwIpaXHXVVTnsXW7Y721FeCCICiWKivTq1QsI3qcQFAuxx8+aNcu1devWLcM9zixFckREREREJFZKZk2O3Z0C3HHHHRl5zpUrV7rjESNGAHDxxRdn5LmhOHJe/TU5bdq0AaBnz55AfiM5+Ri7ww8/3B0/8sgjAKyxRubmEex38n+3hx56KPR6TzzxRMZeJx2FlKe+ZMkSd7zBBhsAsPfeewPh6zUbtCYnmmK45vy1m3YdNWzYsFxfMvmVatGOli1bVviYQh47u0ZsewEI3p/bbbcdAD///HNO+pJIIY6drcusX78+EI5MWwR7iy22AMLvPYv2WXT/8ccfd23Z+DOvEMfO3ifPPPMMEKzDgSBaY5k4fknyoUOHAjBp0iQg2KogW3IxdraO5rXXXgOC9xvAsmXLgHCU2Z7fNuft2rVrSv1NNzpdWVqTIyIiIiIiJU03OSIiIiIiEislU3jAwuarY2kZl1xyCQDHHHOMazvkkENCj/XLDPbp0yfUlsm0NSkO/u7cydLUFi9eDCQuibruuusCsM4667hzViDDwsl+2NpKTdu/L7/8smuzRZR+idY4a926NRCMIcDy5cuB7KepSXxttNFGAEyePNmd22abbXLy2i+88EJOXidb2rdvD4RTTN566y0gv2lqhcI+q44++mh3rl+/fkDya+zff/8FYIcddnDn/GMIp+iPHTsWyE7aWr4dccQR7tiKSlmqlpUrh6AcuaVOWtloCMbF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+nd1bAZ5zz33BIp348/x48e7Y8u9j5q/65dVrVq1KhCsAygmNgvuR4yNlTwt5TLF6brxxhuBYDw/+OAD12bXTLol7m3jYyt5DME6nfvuuw8IbxSaydngQmbrZizS4Edyttlmm9X+vF37/voy+6zLZCSnWNnakbLRGwjWHyfbMLlU1K5d2x3XqlULCNZG+98vFiWxLB9bowPQpEkTILgmJbHvvvsOCK+DsvLStk6nV69ers0+j7NNkRwREREREYkV3eSIiIiIiEisxCpdzXZXPeqoo8q1+WHdjTfeGAgWI/upQbag99hjjwXgoIMOcm1169ZdbR/uueceAF588UV37uOPP07tFyhyljrzyiuvAOG0IQuv2067/uK0YnL00Uen9Di7jtZcM9pbzC91bOO60UYbAeHrtVOnTgAcdthhQLh8ty2i9Bc+F2q6jJ/OYikGb7/9tjuXTqqAhcghSEOw4h9xkas0teHDhwNwwQUXlGvr27cvkNnCG9l05plnAsHC1V122cW1WYpFonRa2wIg0eJtS8u666673DlLsbL3vv8d4pf1LQWLFi0CwoumbRd6+1xK9P3Ytm1bILzIOG7v4cqwFKpEXnvtNSD91Ms4sr/jIFgEb0WQvv7663KPt9LRVm4aggIZO+64Y7a6GSsvvfSSO7bPR/v7+4wzznBtVjrets3IFkVyREREREQkVmIVybHN2hJFcvwZeCshOG3aNAC6du3q2mrWrJny661cudId2yI2W0xVKtEbn80cJZohqVatGgAHHnggULyRHH+BcceOHSt8nEWukvFLJM+aNQsIZnr98qo202GRRCsyANC5c2cguIb9Esk2u+xvVmqbGBaaa6+91h1buXd/rC2SYOPjF/YwNlPvL1b++++/ARg8eHBmO1xiEpXtLMDdB5L69NNPgfAmvMbKxNq/PovkvPXWW+7cF198AcDkyZMBuPrqqyt8Xdssz3+OUtkg1DIbjjzySHeudevWQFBg5OGHH3ZtVpTAri0/YmlbRJQyK9N78sknV/iYiRMn5qo7Bc//7kvl88r+bvMfG3Wz71I1c+ZMd2yZJfb963/22t/pt956a1b7o0iOiIiIiIjESqwiObYO5vfff3fnatSoUe5xdjeZaEYvGds07+abbwaCNT2QfGOzUmPj4+fDGst5LVa2BgaCkuJdunQBgnU4AP/88w8QlJIGeOONN4CgBOojjzyS1mtbfrs/U2fHlm9ss6Q+60shGzdunDu22Vt/hsdmhG1WfMqUKa6tXr16QFDa3cpGQxDd9TdXldK03377AXDaaacBQflnCNbNJNpIeo01/psL3HXXXd05O7YMAX/NSdkZY3+z1e+//z76L1CELAp9yCGHuHMWobXNbhOt97IIrB+xsEhcKbP1bw0aNCjXZmsRFy5cmMsuFQ17j/plpSti73kIIrmlwqKFVhK6Muya9LfeMLZ5vCI5IiIiIiIiadBNjoiIiIiIxEqs0tWs5OzAgQPduWHDhgHpl/K1FB+/NOj1118PFO+u8rlSbAuSo7IQrF1jVpocghB3JkK+qbCFvbbLOgSlpseMGZOTPmSKlYKfPn26O3fTTTcBcMwxxwDJF976JSz9oiJS2qwsu//9YFq1agUEKY8QlDreYostVvvcidLVLJ3ZT9Uo1cXzfmq3FZ+55ZZbgCDdF4KCDpdeeimQfkpv3NlWAYlYym8xpCfnin2XQPCdYcV3/DQ0KxzSv3//cm1Dhw7Nej8LgaW6P//88wA888wzKf3ciBEjKmyzIl/2mZiPvw0VyRERERERkVipsqoAp939WbHKsjtJ2xgR4PDDDwdg7ty5QLiEpRk7diwA33zzTcb6kq4o/2syOXZR2aJIf0GpzYqeffbZQLD5XrYU69gVgkIcO3t+e+/6RS1soaTNZNoMMeR+5ijd1yv0a84Kt4waNcodNjTTAAAgAElEQVSdswWjNus5fvz4Sr9OIV5zVo7dCoq0b9/etZXdGNAfH2MLwLO9nUAhjl2xKIaxs/cbwNSpU4GgQMYTTzzh2qwkr781QTYVw9j5rFiNRRL9aI0VGrBz/qbZtmlwJt/HhTh2FsmxwlHNmjVzbcn6a/1K5TGLFy925yx6NnLkyLT6me7YKZIjIiIiIiKxopscERERERGJldinqxWzQgxpFguNXXQau+jilq6WK7rmotPYRVcMYzdgwAB3bEUs5s2bB8ABBxzg2nJdEKkYxs5Xq1YtIEh1ttRngO233x6ASZMmAcFeThAuXpAphTx2lqLsF7moW7duqA8HH3ywa7N0ymS/k6Wk+WM5Y8aMSP1TupqIiIiIiJQ0RXIKWCHf7Rc6jV10GrvoFMmJRtdcdBq76Ap57Nq2bQuES+LbAnmbEffLb+daIY9dodPYRadIjoiIiIiIlLRYbQYqIiIiUuyqVq0KBNEbn5XfFZHkFMkREREREZFY0U2OiIiIiIjEigoPFDAtTotOYxedxi46FR6IRtdcdBq76DR20WnsotPYRafCAyIiIiIiUtIKMpIjIiIiIiISlSI5IiIiIiISK7rJERERERGRWNFNjoiIiIiIxIpuckREREREJFZ0kyMiIiIiIrGimxwREREREYkV3eSIiIiIiEis6CZHRERERERiRTc5IiIiIiISK7rJERERERGRWNFNjoiIiIiIxIpuckREREREJFbWzHcHEqlSpUq+u1AQVq1alfbPaOz+o7GLTmMXXbpjp3H7j6656DR20WnsotPYRaexiy7dsVMkR0REREREYkU3OSIiIiIiEiu6yRERERERkVjRTY6IiIiIiMSKbnJERERERCRWdJMjIiIiIiKxUpAlpEVEMunMM88E4MYbb3Tn7r77bgB69+4NwMqVK3PfMRGpULNmzdzxUUcdBcDFF18MwBprBHO0V111FQATJ0505+bMmZOLLopIAVMkR0REREREYkWRnEqoW7euOz7wwANDbc8++6w7XrRoUc76VOhmzpwJwODBg905f6xKQePGjQGoXbu2O3fiiSeG/vXZJmDjx48HYOjQoa5t7ty5Wetnsdpqq63c8UknnQRAnz59gPBGYjbWjzzyCFB616FEt9566wHQvn17AA4//HDXduSRRwLw2WefAdCqVasc9654rb/++gBcf/31ABxwwAGuzd7X//77b7mfu+iiiwD4+++/3TlFckREkRwREREREYkV3eSIiIiIiEisKF0tgo022giA++67z53bd999gWAxpB8qt8fdfPPN7tyKFSuy3s9CsuuuuwLQvHnzPPck/yyNxb9+LKXlm2++AWD58uWubeuttwaChbedO3d2bWPHjgXglFNOyWKPC1vVqlUBOP300wEYMmSIa6tRo8Zqf37QoEEAPPfcc+5copSYYuGnzk6ZMgWA4447DoD7778/L30qZi+88AIALVq0cOfsmrP0x1dffdW1nXfeeQC89NJLuepibNhnYseOHfPcE4mDJk2auOOzzz4bgB133BGAOnXqlHvchAkTgHDa+LJly7LeT8keRXJERERERCRWFMlJg0VwHn30UQD22muvCh/btGlTdzxs2DAgvCDaZlRLZXGkLQy1CMXnn3+ez+7kjD+TZDPsVnjAnxk2Dz30EAB33nmnO2eRhh49egDBLDIEUR0rjfzee+9lrO/Fwmbak70fP/zwQwCmT5/uzp1xxhkA7LbbbgBss802ri0u16dFpCySqkhO+mwm1z7/Abp37w4E197ixYtz37EiZ+M5evRod65Tp05A4kjqL7/8AsA555wDwOuvv17hY4rF2muvDUDfvn3dOfu8X3PN8n+eWRGacePGAdC/f3/X9vXXX2etn8WiYcOGAAwcOBCAI444wrVZsRAbQz9bwgpWWLaEv53A8ccfD5Re9k1cKJIjIiIiIiKxokjOavhloi1fONmMcTI2AwXw119/AaUTydl5550BePzxx4H4zJRXxGZ/rrjiCnduiy22qPDxX3zxBQDffvstEJ5lsjLIb775JgAjR450bRtuuCEATz75JAANGjSobNeLjv3uFj397rvvXJv9f7BIjv//wCI5Nq5ffvll9jubA/Xq1St3zt53qdp4440B2G677QB45513XNs///xTid4Vn/fffx+AQw45xJ2zcu4SnUVwjjnmmAof88QTT7jjO+64AwivnSt29tl++eWXl2vzy92XPWeRxF122cW1devWDQii+Yl+Po6qVavmju+55x4A2rRpU+5xb7/9NgAjRowAgs9935gxYwA4+uij3bnHHnsMCLYaKBXVq1d3x/Y9ahvx+n8X2xYpw4cPB+Dee+91bb/99lu2u7laiuSIiIiIiEis6CZHRERERERiRelqFTj55JOBoPQqwO67756x57eF+P7CwbjxF91vttlmQFC0IY4spAtB2NtKiq+OLXq3UPonn3zi2mxBadu2bSv8+c033xwIrlu/D3FnYzZ16lQA5s+f79qWLl0aeqy973yWfuUvNi1ma621VrlzLVu2BODFF1+s8OcsRQ3ggQceAKBDhw4AjBo1yrVdeOGFQHGX2U6Hpfpdcskl7pylhX711Vd56FFxa9asGRAUYvE/Iy1t11KEEr1fi91BBx3kjocOHVqu3dJtLXXZZ4vnd9hhByBIJwWYPXs2AA8//DAA1157rWuLc1r8AQcc4I732GMPIPgs91PMbIuBZMUp7P285557unNWjCDu6Wq23YKlPfbr18+11a9fP/RY/3vVvm/se9gvzV0If98qkiMiIiIiIrGiSE4ZAwYMAIKFgP4sk81c2qzIu+++W+7nbSGhvwB83rx55R732muvZabDBezKK68sd+6jjz7KQ09yw1+olyyCYxt++jNKNnNpi/a+//77cj9nC0q7dOlSrs1ez+9DqbHiAonYZr1du3Yt1xaXTRs32WQTILxo1my55Zar/fnrr7/eHVsE5+mnnwZgwYIFrq1UFjSbP//8s9w5m0FXJCc1NssLcNhhhwFB5NCPCB566KFAOJIdNxYJhaBwjM+KEtn72L/G7PEWXTjrrLNc248//ggEn3FWjhuCGXUrIR+nkud+IR77bLLv0VQ3ybbvT8uI8P3f//0fAOussw4A7du3d21WBMMvFFSobBNUgA8++ACADTbYwJ0bPHgwEGxmbH+nQLClxYwZMwCYNm2aa/vjjz+AINrvX5OK5IiIiIiIiGSYbnJERERERCRWSjpdzXYU9sNrVgc80aJaWxA4duxYILxTc1m1atVyx4me66677orQ4+JgO1n7Yd27774biHcawkMPPeSOLRXNTxmwELGNhe2yDKmlD1g6kb83QseOHSvR4/hr1KgRANdccw0QDs8/++yzQOLFv8Vo2223BWDvvfcu13brrbdW+HM1a9YEwteVsYXfH3/8cSa6WJS23377cudKLWUvKisyYClqAFtttVWFj4/z94Px9xHZZ599yrVbEZrDDz8cCKdj/frrr0CQPuQX8vn999+BYL8wKx4CMGzYMCB4j9s+O3Hg/5623MAKEKSqXbt2oX/9dHHbX8fG2gpmQLDXTuvWrdPtds41bNjQHVsxI38fKtv/0fYwtLGAcOpaReya7NmzZ+U7m0GK5IiIiIiISKyUdCTHIjj+gtuy/NKLRx55JBAuUVuR6667rpK9K15W0tGfNb/xxhvz1Z2c8QsJWFTHj+5Uli1utBk7SczKlQNMnDgRgJ122gkIojcQLN71I2rFLNEu3+PHjweSR2Js92obI5HKsu0DbGG2X57c2OflOeeck7uOFQD7TIIgUmoloSHI/EhU2KisJUuWlDtnkYfnn3/enbNCGZtuummEHhc2K9Tgs0wa+2wDWLRoUegxfmTDykNbRsXMmTNd2wsvvABAlSpVgHAUd8KECZXqey75f7fadih+1N8iYlEjMUOGDAGCcSoUiuSIiIiIiEislGQk58svvwTC62bKsplP21gLUovg2MxBotKQtpYHYNKkSal1tgjddNNNAKxYscKd848l82xDy1Su0bipV68eEJTWtvLvELwPbUbZ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yUDppKu1atUKgHr16gHhQhgtWrQAoHfv3kCQYglw+OGHA/Daa6/lpJ+Fwi9GcNlllwFBunebNm1cm6XxWcEgn5WmnTFjRtb6WWz8VN0BAwYAUKtWrQof76czlxpLcU6UfmbXYO3atYFwoYynn346B70rfs2aNQOCEt0A06dPB8Lp6IVEkRwREREREYmVKqv829kCEXURmJXkrVOnTkrPb2V6C7UkZZT/NdlYQGelswFOOOEEINiMrFBndfMxdi1btnTHVu7UZnUhKCbwxhtvAIVbcrFQrrtilO7YFdK4rbvuuu7YiqxYxMJKlQNccMEFGX/tQrzmbGbygw8+AGDnnXcu99qjR48O/QtB1DFXCnHsikUxjJ0f2X7//fcrfNyDDz4IBJuyZvtPu0IcOysEcuuttwLJt2lYtmyZO7aS537RmmwqxLFLxZgxYwDo1auXO2fFRR599NGc9CHdsVMkR0REREREYkU3OSIiIiIiEiuxSleLm2INaRYCjV10GrvoijldzWcLTJ977jkgvJi5Xbt2QLC3UybomotOYxddMYxdqulqllKajXTSRAp57Jo0aQJAnz593DlLSZs6dSoA48aNc225Th0v5LFLxooM7LLLLu6c7ZmTq/3AlK4mIiIiIiIlTZGcAlasd/uFQGMXncYuurhEcnJN11x0GrvoimHs/EJKtsVAjx49AJg7d65r22+//YBwqf5sKoaxK1TFNnYbbLABAPPnzwfCBacsapYriuSIiIiIiEhJUySngBXb3X4h0dhFp7GLTpGcaHTNRaexi05jF53GLrpiGzuL1rzzzjsAXHTRRa7txhtvzGlfFMkREREREZGSppscERERERGJFaWrFbBiC2kWEo1ddBq76JSuFo2uueg0dtFp7KLT2EWnsYtO6WoiIiIiIlLSCjKSIyIiIiIiEpUiOSIiIiIiEiu6yRERERERkVjRTY6IiIiIiMSKbnJERERERCRWdJMjIiIiIiKxopscERERERGJFd3kiIiIiIhIrOgmR0REREREYkU3OSIiIiIiEiu6yRERERERkVjRTY6IiIiIiMSKbnJERERERCRW1sx3BxKpUqVKvrtQEFatWpX2z2js/qOxi05jF126Y6dx+4+uueg0dtFp7KLT2EWnsYsu3bFTJEdERERERGJFNzkiIiIiIhIruskREREREZFY0U2OiIiIiIjEim5yREREREQkVnSTIyIiIiIisaKbHBERERERiRXd5IiIiIiISKwU5Gag2bbmmv/92s2aNQOgU6dOrm2XXXYBYP/99wdgjTWC+8CpU6cC8MILLwBwyy23uLY///wziz0uLccee6w7vvfee8u1t2rVCoA5c+bkqksiIpJB9evXB2DIkCHunH32v//++wDsscceru3333/PYe9EJA4UyRERERERkVjRTY6IiIiIiMRKlVWrVq3KdyfKqlKlSsafs1q1au74hhtuAOD0009Pqy9lh+rHH390x/vuuy8AH3/8caX66YvyvyYbY5dte+21FwBNmjQBwmmA//77b7nH77rrrkDydLVSGbtsKIax23zzzd1x06ZNAejSpUvoX4AaNWqEfu7WW291x2eeeWbG+5Xu2Oma+08xXHOFqtjGztLFJ0yYAMDhhx9e4WM322wzd/z9999nvC/5HjtLz2vevLk799BDDwFw6KGHAuU/wwC22247AA455BB3bunSpQB899135R7/9NNPA/DHH38AcO2117q2qKn2+R67Yqaxiy7dsVMkR0REREREYqVkIjlXX321O+7bt2+o7bfffnPHNuPx4osvVvhcVqigY8eO7pwVI7jssssAmDVrViV7HO+7fYveANx8880ANG7cGAgXe7BIzgMPPODO2f+/xYsXV/j8+Ri7HXfc0R0fcMABAGy66abu3HPPPQcEEcC5c+dW6vWypRCvu549ewLQo0cPANq0aePa1l9/fSC1fq9YscId2/+vzz//PGP9zFUkZ5tttgFgrbXWcucWLFgA5K4ISv/+/YHgWgd4+OGHgeA9napCvOZSYVGJddddt9LPtXz5cgD+/vvvtH6uGMbO/0wfNGgQAAMHDqzw8Q8++CAAp5xyijv3119/Zbxf+R67lStXrrYfybJJEj0ulcfcd9997tyJJ56YWmfLyPfYVZb/nrXjJUuW5OS1i33sGjVq5I4tmnjccccBQWYFQLt27QB4+eWXM/baiuSIiIiIiEhJi30kp0GDBgDMmDHDnatTp07oMX4JaYvkJGMzx35+v93NWunL448/3rV99dVX6XX6/yv2u/1kTj31VHd80003hdoSRXJslhqCsU62/ikXY3fEEUcAcOmllwLBmiJIHl2w3Gk/x3zy5MkAPProo0B4vZHlUedKvq+7unXrAuG1NcOGDQPCkYuyrz1v3jwgPHYWpbnooovK9fPOO+8EoHfv3hnre7YjOccccwwAd911FwDrrLOOa7M1Dv7MbDZmv4191vkmTZoEBBHtVOX7mjNVq1Z1xxtttFGFj7O1FBaRts+CdPn/f6655hoArrjiirSeo1DGLhn/s/GDDz4Itf3yyy/u2CL29plqn5XZku+xy1ckxy/H3bZtWyD9zIJ8j126bF12165dAejTp49r23bbbYGgZPmHH34Y+XXsc8O2KJk+fXq5xxTD2FWvXt0d299ctp7d/96pWbNmhc9h71/L0lm0aFGl+6VIjoiIiIiIlDTd5IiIiIiISKzEMl3NLy9r5Rj9hco///wzABdccAEAjzzyiGuLumjXSjLaQkk/zL7bbrsBiUs7JlMMIc10WfpZotLQxk8ZsZSu0047zZ17/PHHV/s6uRg7ew37Xfw0qWeeeQaAH374wZ3bf//9geB6qFWrVoXPbT8PQanzb775Jq3+RZWP6+7cc891x7agfZNNNnHnLK3j119/LfezZ511FhCk+vnFBSxF4dVXXwVgl112cW0zZ84Ewp8NlZWNdDU/HeD1118HoGHDhgC8++67rs0WfPoLui0FKpO22morICiuct1117m2ESNGRHrOfFxzG264oTuuV68eEB47Sw2sLH+rARsfu47fe+891+anVKejkL8nbIHy888/785tscUWocf4v7dfkCYX8j12VkhlwIAB7txbb70FBKk99tkFMHv27JSf27+We/XqBQR99/8WsXS1+fPnp9X3fI9dKuxzEoKCF927dy/3OFt6MGrUKCD1YjT2/P369XPn9t57bwDq168PBMVJfIU4dvZ9a3+n2N/HEJQ4TyUl0mePv/766wG45JJLKt1Ppen/ZV4AACAASURBVKuJiIiIiEhJK3+LGQM77bSTO040S2szuH4Zxcq6+OKLAahduzYQlNPz+5NuJCdObGFuKpEcvzS0LQ5MJXqTazfeeCMQLPj2Z9mWLVtW4eNtZt7foNbKkdts08EHH+zabBbUFkwWaunpyvDLEFuEy5+xsXFJNzJhG+m1atWqsl3MG/tMgWBm3MqRH3jgga7Niqz4kYNsOPnkk4HyBVyKxdFHHw0E1xSEF8Znml8+1SK0tm2BX1AljmxWuGz0BoKMikzM7hYrK7Tgb5FQWWuvvTYQnvm3Yyvq8/XXX7u2dCM4xcAisy+99JI7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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# takes 5-10 seconds to execute this\n", + "show_MNIST(train_lbl, train_img)" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAKoCAYAAABUXzFLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJzs3Xm8VdP/x/FXSkql0uimMlVKE5FEA6mIKGT2jVQaDEWI0CCUhEx9UaZSqMzJnAiF0jeVmcjcoDlDOr8//D57r3Pvuadzz73nnn32eT8fj+/D/u517rnrrvYZ9vp81meViEQiEUREREREREJil3R3QEREREREpCjpJkdEREREREJFNzkiIiIiIhIquskREREREZFQ0U2OiIiIiIiEim5yREREREQkVHSTIyIiIiIioaKbHBERERERCRXd5IiIiIiISKjoJsexefNmBg0aRE5ODmXKlKF58+Y88cQT6e5W4G3atImrrrqKTp06Ua1aNUqUKMGIESPS3a2M8Oabb9KrVy8OPPBAypUrR61atTj55JNZtGhRursWaEuWLOGEE06gTp06lC1blj333JMjjjiCqVOnprtrGWnSpEmUKFGC8uXLp7srgfbWW29RokSJmP9bsGBBuruXEebPn0+XLl2oXLkyZcuWpV69etx4443p7lagnX/++fled7r24vv444/p1q0bOTk57L777hx44IGMGjWKrVu3prtrgffBBx/QuXNnKlSoQPny5Tn66KN59913092tAimV7g4EySmnnMKHH37ImDFjqF+/PtOmTeOss85ix44dnH322enuXmCtXbuWBx54gGbNmtGtWzcmTZqU7i5ljIkTJ7J27Vouu+wyGjVqxOrVqxk/fjytWrXilVde4Zhjjkl3FwNp/fr11K5dm7POOotatWqxZcsWHn/8cc477zxWrlzJddddl+4uZowff/yRIUOGkJOTw4YNG9LdnYxw8803c/TRR0eda9y4cZp6kzmmTZvGeeedx+mnn85jjz1G+fLl+frrr/npp5/S3bVAu/766+nXr1+e8127dmW33XbjsMMOS0Ovgm/FihW0bt2aBg0acOedd1K1alXefvttRo0axaJFi3juuefS3cXA+vDDD2nbti0tW7ZkypQpRCIRbr31Vjp06MDcuXM54ogj0t3FxEQkEolEIrNnz44AkWnTpkWd79ixYyQnJyeyffv2NPUs+Hbs2BHZsWNHJBKJRFavXh0BIsOHD09vpzLEr7/+mufcpk2bIjVq1Ih06NAhDT3KbIcffnikdu3a6e5GRjnxxBMjXbt2jfTs2TNSrly5dHcn0ObOnRsBIjNmzEh3VzLODz/8EClXrlykf//+6e5KKLz11lsRIHLdddeluyuBNWzYsAgQ+eqrr6LO9+3bNwJE1q1bl6aeBV/nzp0jNWrUiGzZssU7t3HjxkjVqlUjrVu3TmPPCkbpav/vmWeeoXz58vTo0SPq/AUXXMBPP/3EwoUL09Sz4LOQuRRc9erV85wrX748jRo1YtWqVWnoUWarWrUqpUopQJ2oqVOnMm/ePO677750d0VCbtKkSWzZsoWrr7463V0JhcmTJ1OiRAl69eqV7q4E1q677gpAxYoVo85XqlSJXXbZhdKlS6ejWxnh3XffpX379uy+++7euQoVKtC2bVvee+89fv755zT2LnG6yfl/y5Yto2HDhnm+IDVt2tRrFykOGzZsYPHixRx00EHp7krg7dixg+3bt7N69Wruu+8+XnnlFX2JStBvv/3GoEGDGDNmDHvvvXe6u5NRBg4cSKlSpdhjjz3o3Lkz8+fPT3eXAu/tt99mzz335LPPPqN58+aUKlWK6tWr069fPzZu3Jju7mWUDRs2MHPmTDp06MC+++6b7u4EVs+ePalUqRL9+/fnm2++YdOmTbz44ovcf//9DBw4kHLlyqW7i4H1119/sdtuu+U5b+c++eST4u5SUnST8//Wrl3Lnnvumee8nVu7dm1xd0my1MCBA9myZQvDhg1Ld1cCb8CAAey6665Ur16dwYMHc9ddd3HRRRelu1sZYcCAATRo0ID+/funuysZo2LFilx22WXcf//9zJ07lwkTJrBq1Srat2/PK6+8ku7uBdqPP/7I1q1b6dGjB2eccQavv/46V155JY899hhdunQhEomku4sZY/r06Wzbto0LL7ww3V0JtH322Yf333+fZcuWsf/++7PHHnvQtWtXevbsyYQJE9LdvUBr1KgRCxYsYMeOHd657du3e1lNmfKdWHkdjngpV0rHkuJw/fXX8/jjj3P33XfTokWLdHcn8K699lp69+7Nb7/9xgsvvMDFF1/Mli1bGDJkSLq7FmizZs3ihRde4OOPP9Z7WwEcfPDBHHzwwd7/b9OmDd27d6dJkyZcddVVdO7cOY29C7YdO3bwxx9/MHz4cIYOHQpA+/btKV26NIMGDeKNN97g2GOPTXMvM8PkyZOpUqUK3bt3T3dXAm3lypV07dqVGjVqMHPmTKpVq8bChQsZPXo0mzdvZvLkyenuYmBdcsklXHjhhVx88cUMGzaMHTt2MHLkSL777jsAdtklM2IkmdHLYlClSpWYd6br1q0DiBnlESlKI0eOZPTo0dx0001cfPHF6e5ORqhTpw6HHnooXbp0YeLEifTt25drrrmG1atXp7trgbV582YGDhzIJZdcQk5ODuvXr2f9+vX89ddfwL+V67Zs2ZLmXmaOSpUqceKJJ7J06VK2bduW7u4EVpUqVQDy3Agef/zxACxevLjY+5SJli5dykcffcS5554bM51IfEOHDmXjxo288sornHrqqbRt25Yrr7ySO++8k4ceeoh58+alu4uB1atXL8aMGcOUKVPYe++9qVOnDitWrPAmEGvVqpXmHiZGNzn/r0mTJnz66ads37496rzlHao8qKTSyJEjGTFiBCNGjODaa69Nd3cyVsuWLdm+fTvffPNNursSWGvWrOHXX39l/PjxVK5c2fvf9OnT2bJlC5UrV+acc85JdzcziqVaKSqWP1vfmpuNXabMDKebRR969+6d5p4E35IlS2jUqFGetTdWcltrreO7+uqrWbNmDZ988gkrV67kvffe4/fff6dcuXIZk2mid5X/1717dzZv3sysWbOizj/66KPk5ORw+OGHp6lnEnY33ngjI0aM4LrrrmP48OHp7k5Gmzt3Lrvssgv77bdfursSWDVr1mTu3Ll5/te5c2fKlCnD3LlzGT16dLq7mTF+//13XnzxRZo3b06ZMmXS3Z3AOvXUUwGYM2dO1PmXXnoJgFatWhV7nzLNn3/+ydSpU2nZsqUmXhOQk5PD8uXL2bx5c9T5999/H0AFVxKw22670bhxY+rWrcv333/Pk08+SZ8+fShbtmy6u5YQrcn5f8cffzwdO3akf//+bNy4kQMOOIDp06fz8ssvM3XqVEqWLJnuLgbanDlz2LJlC5s2bQL+3YRr5syZAHTp0iWqDKH4xo8fzw033MBxxx3HCSeckGfnan3wx9a3b1/22GMPWrZsSY0aNVizZg0zZszgySef5Morr6RatWrp7mJglSlThvbt2+c5/8gjj1CyZMmYbfKvs88+20uRrFq1Kl9++SXjx4/n119/5ZFHHkl39wKtU6dOdO3alVGjRrFjxw5atWrFRx99xMiRIznxxBM56qij0t3FwHv22WdZt26dojgJGjRoEN26daNjx44MHjyYqlWrsmDBAm655RYaNWrkpUpKXsuWLWPWrFkceuih7Lbbbvzvf/9jzJgx1KtXjxtvvDHd3UtcmvfpCZRNmzZFLr300kjNmjUjpUuXjjRt2jQyffr0dHcrI9StWzcCxPzft99+m+7uBVa7du3yHTe9PPP30EMPRdq0aROpWrVqpFSpUpFKlSpF2rVrF5kyZUq6u5axtBnozt1yyy2R5s2bRypWrBgpWbJkpFq1apHu3btHPvjgg3R3LSNs3bo1cvXVV0dq164dKVWqVKROnTqRa665JvLHH3+ku2sZoWPHjpFy5cpFNm7cmO6uZIw333wz0qlTp0jNmjUjZcuWjdSvXz9yxRVXRNasWZPurgXa559/Hmnbtm1kzz33jJQuXTpywAEHRK677rrI5s2b0921AikRiahuo4iIiIiIhIfW5IiIiIiISKjoJkdEREREREJFNzkiIiIiIhIquskREREREZFQ0U2OiIiIiIiEim5yREREREQkVHSTIyIiIiIioVIq3R2IpUSJEunuQiAks4WRxu5fGrvkaeySV9Cx07j9S9dc8jR2ydPYJU9jlzyNXfIKOnaK5IiIiIiISKjoJkdEREREREJFNzkiIiIiIhIquskREREREZFQCWThAREREQmvsmXLAtCxY0fv3IwZMwB48803AejTp4/X9sMPPxRj70QkDBTJERERERGRUCkRSaaWXYqpVN6/Mq3M4HXXXQdA7dq1AZgyZYrXNn/+/GLtSyaMXZUqVbzjI444AoDjjz8egH79+nlt3333XdR/J06c6LU99dRTRd6vTBi7oFIJ6eTomktepo3drrvuCsCkSZMAOO+88/J97CGHHOIdL1mypMj7kmljFyQau+Rp7JKnEtIiIiIiIpLVsnpNzh577AHAiBEjvHOnnXYa4EcjbrnlFq/t9ttvB2DNmjXF1MPMcsEFFwBQp04dwJ+xg+KP5ATZ0UcfDfj55wCVKlUC/Nkad7bCxtP+26ZNG6/trbfeAuC3335LXYdFJOVat24NwJdffumdW716dbq6kzKXXnopED+Cc//99wOwYsWKYumTiISTIjkiIiIiIhIquskREREREZFQyep0tV69egEwaNCgPG2WLjR06FDv3LHHHgtAly5dAKWtSXKuvvpqwE9RK4znnnsO8AsXZItmzZp5x23btgWgatWqgF8AA+Cdd94B4OSTTwZgw4YNxdXFrHHAAQd4x2+88Qbgp14B/Pjjj8Xep6Jk11Ws9/tYr+EGDRpE/f999tnHO165ciUAhx56KAAtWrTw2nJycoDoUsm9e/dOrtMB07lzZ+/4xhtvjGpzX5NWrMZS2kRSxZYrADRp0gTwU8Ld733W9u677wLR6eISfIrkiIiIiIhIqGRlJMdm1saMGZPvYz766KM852z2bfbs2YAf0QFYu3ZtEfYwMz300EOAX8jh888/T2NvgssW01pksDC2bNlS6OfIBFZa+8477wSiZ9DdUtwQXbTBZt3s56w4hhRe+fLlAZg6dap3rmbNmunqTpG7/vrrAejfvz8AmzZtyvOY3XffPc85i8jEEquwSG7u78n0SI5F+Z588knvXJkyZaIeM2HCBO/YLQKU7apVqwbADTfc4J0bMGAAALvs8u/8tG0rAHDTTTcBfkGb9evXF0s/g8SKHVWsWBHwIzMA3bt3B+DUU08Fol+7VmgqlgDuspISRx11FABXXnklAF27ds33sW4569zj416vn3zyCeBnnKSDIjkiIiIiIhIqWRnJsTU4pUuXBuDvv//22qxktP3XvWN95plnAD+/2F3LY7N+2aZbt27esc0AmLFjxxZ3dzLCNddcA0DHjh29cw0bNgRg69atAIwaNcprsyiGrT1xvfzyyynrZ7rZ3w3w8MMPA/76iHgzSbHUrVsXgHLlynnnsiUKlir22rexBZg1axaQuetw7DMB4NxzzwWgRo0aUf91JRKZicWuvT///NM7Z69lG8NMZuNy+eWXA9HrH4xtBnrzzTcXX8cyQMmSJQE/M8J9H7TrbMeOHQDsvffeXpttEt23b18ATjzxRK/t119/TWGPg8PGIJGIvfsZ8sUXXwBw9913A/42D+BHgMLEXo9nn322d27cuHGAH+GK955mW1eAH3Fs1KgRACNHjvTa7L1MkRwREREREZEiopscEREREREJlaxJV7NFsgDHHXdcVNu3337rHcdb+HjfffcBSlcDfzz/85//eOcqVKiQru5kFEtRsdKUO2MFCtzwuglLypX7t91xxx1A9I7otpA0WZbqZ4srAV555ZVCPWcmqFy5MgCnnHIK4Jd4Br+ccUHZYvJbb70ViC4CYeXRM9WFF17oHbulsSH6c2LffffNt80W28YqVDBt2jQAPv30UyB64XiYnHTSSQD069cvT9svv/wC+Onff/31V/F1LAPY+5+bpmaWL18OwMKFC4Ho4jW2yP7ggw8GolOE7N/jt99+S0GP08vGC/xtQWKlWtl1Zu/7VhIa/OIpmzdvBqBHjx6p6WwanXnmmd6xlXHfb7/98n28m3L88ccfA36K6bx587y2smXLAv5nzYcffpjnuaw40AMPPOCds+JetjQkVRTJERERERGRUMmaSI4bZahfvz7gl31OtJSvzZ5s3LgR8BcIZiNbAO6WGbTFkFJ4bulLW9AXa3bKFlpmOnc27uKLL07qOZ5//nkgenY896aCVkoa/GIPYXb++ecDfnnZBx980Gu77LLLEn6e3XbbzTu+5JJLAP89YPjw4V7bqlWrku5rELibmFp00RbSugtqJT6LHMRi0b45c+YUV3cCr2fPnt6xlSw3L730knd82mmnAX5Uwn1dWqGfgQMHAv6WF+BHOCz6GqbPavfvzG3y5MnesWXiLFmyJN/HDxkyBIi94adtHZIprCCAFaC4/fbbvbZYhUBsE2Ir926FLwA+++yzfH+PfR+24hbuxr/r1q0D/IIObqGqI488ElAkR0REREREpEB0kyMiIiIiIqGSNelqrVq1ynPu8ccfB/ww3c7Yor2LLroI8BeRuufuv//+QvUzDNJZEz3TWfqBW8hir732Sld3itQ+++zjHVuqmL1uYqUHxOOmpM2cORPwi4a4vyd3uprbZoUNpkyZUqDfHVS28NNdYHrbbbcB/oLa8ePHJ/Xc7r+PpatZ0QvbwyhssmWn86LSvHlz79jSqoy7F53S1HxWCMX2KAE/TXLbtm2AX6QC8hZpcPdZskJIOTk5gF9sBGD06NGAn761evXqovkDAmD+/PnesY2nfbcbOnSo12apU7Hsv//+gJ8q7RbCeeeddwAYM2ZMEfW4eFjaWbt27fK02evxyy+/9M5ZsYV4qWmJeO+997zjE044AYBOnToV6jkLQ5EcEREREREJldBHcnbddVcArr322jxtTz/9dFLPaWUc3bt9W8RrpQjDUto3GW6ZXikYm42Pt2PzihUriqs7RcrKOIMf8bTX58789NNPgP86HjVqlNd20EEHATBjxgwADjzwwHyfx53BfPvttxP63Zmidu3aQHRxBXuPsr/Viq0kysooP/bYY3narBCLlQQOg1gL5m12t0WLFt65xYsXRz3Grj3w3/uTLdGdqdyFzbm3E3ALpKxZs2anz2XXnZWeBTj33HMBuPfee4HCzziny5577ukd23cQ95y5+eabgYIvzP78888L0bvMM2vWLO/YvoedffbZQHQUw6LasYr1PPHEE4D/HuqWT7bIdSZwC1Hk3qLin3/+8Y4tcliUW5/Y77YCGOAXvrH3A7fghUWaUk2RHBERERERCZXQR3IOO+wwIHoWzsTatCgRVjLv999/987Z7LHdsWZzJMedfZOiZxt5ZRo3GjBs2DDAz4Xemd133x2Ayy+/HIC6det6bW657USfx32OTN6Q0d2I02a43ejY9OnTAX/TXnc2LxEdO3YEoGbNmt657du3A9HRtLBwNwi0Uqg2y2755e6xRcrcGdE//vgD8NdBuOugwrjxpb2GY33GfvHFF0D8GWN3Q8LBgwcDcNZZZwGxIxx2LdtaPIA+ffoAmVEaecCAAd5xrL9v69atALz22mvF1qdMZptKAnTv3h3woza2/QL42xRYhOPNN9/02g455JCo57RS0gBLly4t4h6njrstQO5ryx2noozg2HvhFVdcAcReA2Tc6GtBtjAoDEVyREREREQkVHSTIyIiIiIioRL6dDUr+7xhwwbvXMWKFQE/nOcuMkvE999/D8DXX3/tnbNdd1u2bAn4u6+H3S676D65KNl14xa1MFbUwl3knKmsLKctMHb973//A6ILA9iC8GbNmgHR1128FBV7nD3GCjtA3nKh8+bNK+BfkX62ezz4O0i//PLL3rlk09QsBSlWWoOVrU209H4mueaaa7xjK3ZRunTpfB9v146bqtWgQQPAT1dzd/k+/PDDi66zaVSyZEnv2Eqw5y42AH6hj02bNuVpq1+/PhB9vbol3vNjv8ctzmJpbpZKHmQ7S2W39zo3vaggjj322Dzn7PtPQd8HMs0rr7wCwAcffABEfw9r3bo1AL169QLgnHPOyfPzVoAgUz9jFy1a5B1b2myZMmWA6JRjKwjwyCOP5PtcF154oXdcvnx5wN/OwsqVg/9eENTvgsHslYiIiIiISJJCH8mpXr064EdvipI7S2CRHFvoli2RnExY6JkJjj/+eMCfEY61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92HFihXesW1e5s54GtvwK4yRHLfohy0WfeihhwA/ChOLuxGwRR4bNWoEFGxzzEzlRlltNtiik08//bTXFpb3sVtuucU7tteIzcy6G6ka97VsY/DHH3/kedzcuXMB+Pnnn4Ho9wWLBtkmoO57gEU27Fqz4g+Zxv3ssyIKsV53iURwbAzBn223Ag0//fRTofoZNhbBtmvYfc+yqGU2vI/lxwohWZaFu9GsW3Ic/KIY4H922PVqG0wDfPXVV6npbAazrKadueGGG1Lck51TJEdEREREREIlayI5bq61sfKdbv6m5V/bLF+sGXLjlvKdM2e/92S6AAAgAElEQVROkfQzk1kef48ePQDIyclJZ3dS7tNPP823LVZ5cduQ9t577wWiN47dtm1bUn2wNSOxrlPbANI23yvM70k3W1Nn626sRC34671sU1b7L/glteOtCYv1mDCWPweoXLmyd2zvf7kjYWHibupnkSpbJ+dunmobA5YvX947Z5F++6/LysDbzG+is+f2eFtf9/777yf0c0HjvlZsM133NWnrvf773//m+VnbcNGiNm4p6F9++aXoO5vhbJ0IwD333BPVZptBgzYiB387B3s9uuuTLPrYunVrIDpaY4+38vK2vYBEs60b4m2Q6l6TX3/9dcr7tDPh/CQXEREREZGspZscEREREREJlaxJV3MXStqC0nLlygHw4IMPFui5LAQXL2SXjZYsWQL4C/zefvttr23MmDEAPPfcc8XfsRQZP348AG+++Sbgp7CAX57YdksGuOuuuwBYt25dkfXBSq1a6N3tg4WWg74jcSLs7+zYsSPgL/wGaNiwYb4/Z2k18dKJYj3G0onCxl0cbmlYkyZNAmKX2Q8TW+Rv/x05cqTX1qBBAwAqVqzonTvttNMAv/hMs2bNvDY37W9n3G0FrBCCld4vyveCdLEUn0QXI0vBXHXVVd5x7nQsK6sNsHXr1uLtWABZCWnjpqRamtoxxxyT788PGTIE8LdfkGhWROXII4/M9zFLly71jv/+++9Ud2mnFMkREREREZFQKREJYL3BVG+UdtJJJwH+AmUr9RmLRScABg0aBPibSbkbzaVCMv80Yd9kLlEau+Rlwti5hR2mTp0KQNOmTQE/Quv2K97fZI/59ttvvXPnn38+4C/KT1RBx664xs1KZ7vFMizCYBEKdybYjXwXh0y45tyNgOPNBufmbrJqEcmilAljF1RBHjvbcHHBggXeOSuNbFs3HHDAAcXSl1iCOHYWlXY3i86P+35n3wWfffZZAP76668U9M4XxLFLhBUXcd/TcrMMEoiOYheVgo6dIjkiIiIiIhIquskREREREZFQycp0tUyRqSHNINDYJS9Tx2727NlA9E72iaSrWRrqlClTvHPJLsIParpa/fr1geg0gzVr1gD+vk2DBw/22g466CAA1q9fXyz9y9RrLgg0dskL8tg1btwYiE6ZN1bsoUOHDt45N62tOARx7Cx977XXXgOgTp06Xpu9l9l+WRMmTPDali1bltJ+5RbEsUuE0tVERERERETSTJGcAMvUu/0g0NglT2OXvKBGcoJO11zyNHbJC/LYWZGB999/3zu39957AzB69GgAbr31Vq+tuEtIB3nsgi5Tx06RHBERERERkTRTJCfAMvVuPwg0dsnT2CVPkZzk6JpLnsYueRq75GnskqexS54iOSIiIiIiktV0kyMiIiIiIqGimxwREREREQkV3eSIiIiIiEioBLLwgIiIiIiISLIUyRERERERkVDRTY6IiIiIiISKbnJERERERCRUdJMjIiIiIiKhopscEREREREJFd3kiIiIiIhIqOgmR0REREREQkU3OSIiIiIiEiq6yRERERERkVDRTY6IiIiIiISKbnJERERERCRUdJMjIiIiIiKhUirdHYilRIkS6e5CIEQikQL/jMbuXxq75GnsklfQsdO4/UvXXPI0dsnT2CVPY5c8jV3yCjp2iuSIiIiIiEio6CZHRERERERCRTc5IiIiIiISKrrJERERERGRUNFNjoiIiIiIhEogq6uJiIhIdqlfvz4AAwYMAOC8887z2jp27AjA4sWLi79jIpKRFMkREREREZFQKRFJpmB3igWhHvguu/x7/9ewYUPv3Ouvvw5AzZo1AVi6dKnXdswxxwCwdu3aIutDttRSr1ChAgBjx44FoH///l7be++9B8CRRx5ZoOfMlrFLBY1d8sK2T87EiRMB6NevX562vfbaC4Bffvml0L8n2665PffcE4CjjjrKOzd58mQAtm/f7p3r0KEDACtWrMj3uTJ97O6++27v+IwzzgD88XFt2LABgCpVqhTZ7870sUsnjV3yNHbJ0z45IiIiIiKS1XSTIyIiIiIioaLCA7n06dMH8NMEevTokecxO3bsAKBx48beuREjRgBwySWXpLiH4XDWWWd5x0cffTQAF154IeCPb+7jbHXAAQcAUK9ePQDat2/vteXk5AB+qstbb71VrH2TcCpV6t+PhrZt2wJ6HSajatWqALRr1847d+qppwJ+enONGjXiPsf06dMBaNasWSq6WGwsJRn8tGT7m1q1auW15U5F+eKLL7zjokwFl+xTpkwZAI499lgAhg0b5rXZNThq1Kio/wL8888/xdVFSQFFckREREREJFSyuvCAlat0F7p3794dgNq1a+f7c3/99Rfgz0gB3H777QBs3LixyPoXxsVpNr4PPPCAdy7WIlNjhQfatGlToN8TlLGrVKmSd9ygQQMATjzxRMD/2wDWrVsH+JGZU045xWvr1q0bALvvvnue57c+//333wC8/PLLXttll10GwMqVKwvU56CMXVGySGs8bhQs2YhYWAoPWKR16tSpedr+/PNPAPbZZx8Afvvtt0L/vky/5mzBPMBpp50G+BGcatWqJf283377LQD77bdfvo8J8tjZe9Zdd93lnTv//PPz7Uvuv+XMM8/0jmfOnFnk/Uv32NnngxvpMpbZEKstlhYtWgCwaNEiAB5++GGvbdmyZYXqZyzpHrtEHHjggd6xZTu4kcP8uMUt1q9fn+9zWlaFfUaD/x3Qsnp+/fXXPM+fCWNXUBdddBHgF6sB/7uHfVacffbZXtsTTzyR1O9R4QEREREREclqWbkmxyI4L730EgD77rtvgX7++++/B2DGjBneOSs5LbGVL18e8HPR40VvXAsXLkxZn4qDWx7VncXIj83WxJqtsBlzd9bYrt0xY8YAfpQI/DG39WVhECsiYzPm7lqlZAwfPtw7HjlyZL6/L+j2339/7/jyyy8H/PeqBQsWeG1//PFH1M+5awzvvPPOfJ/fojtFEcHJJIcffrh33Lt3bwDq1q0L+BtVJspmh91/j2effRaASy+91Dt3zz33JNfZgDjhhBOAvNEb17x587zj559/PqptyZIlKelXOtm6X4ArrrgC8NddxhIv0hXrcVaW3NZ/Adxwww0APPbYY0n0OHPYGAwdOhSAIUOGeG1uVkV+Yq09POmkkwDo2rUr4EdqAfbYY498n8u2GBk9evROf2+msagh+BFWy0JZvXq112ZRrFq1agHRn6fJRnIKSt/MRUREREQkVHSTIyIiIiIioZI1hQds0TfA7Nmzgfhp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HnjgAe/cV199VbwdExGRpNl7O8Ctt94KQIcOHQA4/fTTvbYvvviieDuWJebNmwfA8ccfD8DMmTO9tg0bNqSlTyISbIrkiIiIiIhIqOgmR0REREREQkXpaoVQuXJl77hOnTqAvzjylFNOyfP4EiVKeMdXXnllinsXbJ988ol3nJOTA/hlQBcuXJiWPqVTuXLlAOjevTsARx11VJ7H3HnnnQB89tlnxdexgKlWrRoALVq08M5dc801ALRp0waASCTitd18880ATJo0CYDvvvuuWPoZFDVr1gT8lNn33nvPa/vzzz/T0qdMVaFCBe948ODBUW2LFi3yjkeOHAnA7bffDsCOHTuKoXfhYu+Hf/31l3euefPmgH/drlixovg7FjBnnnmmd3zCCScAsM8++wBw8cUXe23/+9//irVfmaZ27dqAP2aHHXaY19asWTMAHn74YQCGDBlSzDeznt4AACAASURBVL2TwlAkR0REREREQqVExJ32DAg34hEkjRs3BuCSSy4BoFWrVl7bQQcdBPh9d4d18eLFgL9IFWDTpk07/X3J/NMEdexM9erVAfjwww+9c3vvvTcAM2bMAKJnp5IV5LGzaMRjjz3mnbNIoJVHdftif4uVR3VnmVIR1Qni2FmEy2bHbbzA72+s156ds6If/fv3T2k/Czp2qR43i5haJOe8887z2qZPn57S310QQbzmcitTpox3PGfOHMCPLrh92WOPPYD45Yxnz54NwKpVq/K03XLLLd7xmjVrdtqvTBi7RNl7/5FHHglA+/btvTb7jL3tttsAuOqqqwr9+zJh7A455BDvePjw4YA/PpUqVcq3X+PGjfOOhw4dWuT9yoSxi6Vnz54AjBgxwju31157AbDbbrvl+3NW6KJHjx6F7kOmjp058cQTvWPLXrLvIu51l4poa0HHTpEcEREREREJFa3JyYfNGlmeP8Do0aOB6NmTRNhGZaVKZe9wW471iy++CECtWrXyPCaMG7nVrVvXO7bITay1IzZL8+mnnwLw/fffe21Vq1YF/HUos2bN8trsOg0jN9pnf3vuqI0r3rm+ffsC8Oqrr3ptzzzzTNF1NqAaNWoE+OPmlrbPHcmxMQL/Gh00aBAAa9euTWk/M8Eff/zhHduaS4tM29on8PP67THudblly5aoc25E0mTb50SNGjW8Y4u42oz62LFjvTabQU8kupXJ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5Zdf9o5t13QrQPD66697bW7qWrazIkbua8/StzIp/TZV7PuX+zlhKWnGLWtuyw2sPLl7rR199NFAYim/mczGYO7cuUD0tiv23nfCCScAsb/3jRs3DoguWBAEiuSIiIiIiEiohKKEtM2k28Z1sTbIcxeU2oxHItzSqbYBnLsA1dhml1YW02awCiOIZQatjKdFadyIlS0utbv9bdu2pbQv8RTH2NmMpF1/PXv29NpibfZqhRksghCv1HGsiEwiJZJjLR6Pda5kyZL5/l1BvO6SZQtQLepqm8SBH80qilK0JtUlpHNzS2nbdei+13300Uc7fQ6btaxcubJ3zv6Op59+Gogu5Wtj6o5lIl577bV824JyzbkbGHfp0gWAO++8E4C9997ba4v3+rFZTlukm2rpGDt3Ftxmbi16A372gpU8t8/HWBo3buwd26JwK+zw5JNPFqqfOxOU6y6W0qVLA9HfN2wbAStA4vb/77//BvzCS24hCNs2oygFcezs+9q8efOA6KJP9957705/3qLOViwF4MwzzwSK9loM4tgl4osvvgDggAMO8M59/fXXgF+u2y3NnQoqIS0iIiIiIlktFGtyrASlRXDcjdps06yCRG/Anw2dPXu2d87d/BNg6NCh3vF///tfIPV3senWr18/wI/grF+/3mu74IILgPRGcIqTXVt2jbhrveJFZGKtu7HyxzfffDMQXfqyQYMGgD9LZf/fZc8Va12Fey7RtSyZbMqUKd6xRXBsLNyoTVFGcNIl1nqieBGTWP766688537//XcARo0aBcCyZcuS6F3msdlwgOeeey7qv+3bt/fa7LOga9eugP++CP4MfLZxIzm2/ipeBMfG010fOHjwYCB4ZWhTzd2E9vzzzwf8z9imTZsm9BwWhbRNfd1sgjfeeKMIehl8NWvWLNTPP//88wBMmDDBO2fRs1RHFYPMMpTsOnUj+7Y+LKjffRXJERERERGRUNFNjoiIiIiIhEoo0tVsgail5Xz55Zdemy2KisXCu1byE/wd6u053UXb9ry2K+7KlSsL2/WM4BZfcNMyILpMtJsmmA0WLVoE+DsFX3bZZV6bLQJ3F2fnTo+y1DSAZ555JqotVrnabGHlZt0SlnadxSvoYSUwzz77bO9c7sWazz77bJH1M8xWrVoFZE+aWiLeeuutPOdiFaHJBm7698KFC4HorRts8XvLli2B6HQsK7d96623AtFp4H369AHgiSeeSEGvg8u2FwC/JPemTZuA6M9YG0d3awIzevRowC8kEoZ03OJWo0YNIPp7X506ddLVncCw4itWcMXSmAGWLl2alj4lSpEcEREREREJlVCUkP7nn38Af3Hx+++/77W5JQTNFVdcAfiFCixqE4uVoIbij+AEpczgtGnTvOMzzjgD8GfjjjzyyCL/fUUhHWPnzvhYFCJeJCeo0n3d/frrr4AfDQM/spC7QINr+fLlQOwS21ZwwUqvpkpxl5AuClbu1120a/8GNqv84IMPprQP6b7mrDz0Dz/8kNDjrdy2ldhu166d12YlZ5966qki61886R67+vXrA4m/v1kE3MZ62LBhXlu8QgWpkO6xi8WKHuXk5AD+Ynjwo9xWXMXtv/07fPXVVyntnwni2NkWF7adhcs2xXYLZORm2zvY6xr8AlNW0KEoBHHscnMLa1mWxC+//AJEb0LtFmspDiohLSIiIiIiWS0Ua3JmzpwJ+KXsjjjiCK/NZo1csTZVNBYFmjt3LgAPPfSQ15Yta3CM5VVb9Ab8vPTjjjsuHV0KtO+//z7msRRM9erVgejS13Xr1gX8aJnNaELsct3GopB33XVXajobAraprY07+JHI2rVrp6VPxc027vzpp5+8c5MnTwb8aKx7fVmZX4vguGuX3Jn3bGBrVd31SRZpaNKkCQAzZszw2iwK++233wJ+Job8K/cGvqVK+V/TevXqBcTehkD80sYWRR03bpzXZpk4L/wfe3ceaNX0/3/8GUrKkIRSIWPSh1SIQuYhRUJSmTIUmTJPSZnHr6JMKRQlUyIhczKUKSWEQolUiApFvz/83muvfe+5t3v2PcM++7we/9j2OvecdVf7nHP3er/Xe40bV+rn7L3dtWtXIJyBYVkrxcKi1P76a1uLc/HFFwO5j95UhiI5IiIiIiKSKLrJERERERGRRElEupotXGzevDkAjRo1qtDPvf3220CQmgbB4rLly5dnsosFo3Hjxu7Y0tX81CtL6yikcKUUFiuFetlll5VqS5WeUfKc///33nsvAAsXLsxkFxPFUotSLawtlrTLX375BQjvdH7jjTcCQTp0tWrVXJul8Nq19tBDD7k2fzfwYmBj4G/XsOeee+arO4mzwQYbuGO/wIWU7b777gOCv2Eg2FpgwoQJQDhNsnv37kCw5OG5555zbW+88UZ2Oxsz9p3ppyrb38p+2mmhUCRHREREREQSJRGRHCuZaKUX/UXJqTZyuv322wH47bffgOKbeSvPNddc445t8fHZZ5/tzhVKGWQpXFa22N8Ez0qCWlnpVEUGbLHoCSec4M5NmjQpa/1MCiu9nUqxbPBrERx/I+kDDzwQgEMPPRQIil9AUHbboo5DhgzJST9FfP57t6Llz4uBbVbrbyprm3VbgYy///7bte26665AUCo/21sNxFHVqlWBcDaPsQyelStX5rRPmaBIjoiIiIiIJIpuckREREREJFGqrIphofU47AIeB7ncFbd27dpAePGoparst99+7tzixYsjPX+uFcKOwnEVx7GztFNLNbWdqSHo7zHHHAPA008/ndW+lCfdsYvTNXfssce6Y0vNsjQuP7UjG+J4zRUKjV10hTB26667rjseP348AG3atAHCe5nYYvtcKYSxs79rAHr16gUE+4FttdVWrs1SpD/++OOc9CuOY7f++usDQSruF1984dratm0LBGmA+ZTu2CmSIyIiIiIiiaJITozF8W6/UGjsotPYRVfIkZx80jUXncYuOo1ddBq76OI4djVr1gTgm2++AWDw4MGuzS9IlW+K5IiIiIiISFFTJCfG4ni3Xyg0dtFp7KJTJCcaXXPRaeyi09hFp7GLTmMXnSI5IiIiIiJS1HSTIyIiIiIiiaKbHBERERERSRTd5IiIiIiISKLEsvCAiIiIiIhIVIrkiIiIiIhIougmR0REREREEkU3OSIiIiIikii6yRERERERkUTRTY6IiIiIiCSKbnJERERERCRRdJMjIiIiIiKJopscERERERFJFN3kiIiIiIhIougmR0REREREEkU3OSIiIiIikii6yRERERERkURZK98dSKVKlSr57kIsrFq1Ku2f0dj9R2MXncYuunTHTuP2H11z0WnsotPYRaexi05jF126Y6dIjoiIiIiIJIpuckREREREJFF0kyMiIiIiIomimxwREREREUkU3eSIiIiIiEii6CZHREREREQSJZYlpEVERCR5DjroIAAmTJgAwFlnneXahgwZkpc+iUgyKZIjIiIiIiKJokiO5NQGG2wAwE8//eTOvfPOOwDsu+++eelTNm2yySbuuFGjRgBceeWVpR732WefAXDJJZfkpmMFwL8eDjvsMAAuvPBCABYuXOjafv31VwBGjhwJwK233ura/vjjj6z3U0QqrkWLFqH/r1WrVp56IsVkww03BOD0008HoE6dOmU+9ttvv3XHjzzyCAC//fZbFnsn2aJIjoiIiIiIJIpuckREREREJFGqrFq1alW+O1FSlSpV8vbaG220EQBnn302APvss49rs/CmpRZdccUVru2rr77KeF+i/NPkc+wqYsCAAUA4ZWvYsGEAnHLKKRl7nXyP3SGHHALAnXfe6c4tWbIEgB133BGA6tWrl/q5xYsXA9CsWTN3bu7cuRnrV0Xke+zMxx9/7I4bNGgABOPjpwGut956oZ+bNWuWOz744IOBcPpBNqU7dtl+v/bs2RMIPseeffZZ1/bYY49l/PXWWOO/ebNHH33UnTvmmGOA4N+zZLoSxOeai2q77bar0OOOO+44AKpWrQrA/vvv79rWXnttAMaPH+/OffLJJwA88cQTZT5noY3d1KlTgeA92a1bN9e2fPnynPal0MYuTuI8dvb+8v9+e+ihhwCoW7dumf1K9TuNGDECgBNPPDFj/Yvz2MVdumOnSI6IiIiIiCRKUUdyNt98cyAcQejRowcA9erVK9WXkkM1e/Zsd9yyZUsgs4vTknS3v/vuuwPw1ltvAcFMC8BRRx0FwNNPP52x18vH2Fn0BoLfxWZnfTNnzgRgzpw57tz2228PwFZbbQUE1yEEka5cict1t/XWW7tjG0eLotr7DeDcc88FoEOHDgCsu+66rs0irBY9W7lyZcb76YtbJKdVq1YATJo0CYClS5e6NisCUln+zOjAgQOB4D3tGzx4MADnnHNOqba4XHOprLXWf/V59tprL3fu6KOPBmCXXXYBgnHOhBUrVrjjH374AQiKlqQS57EzBxxwgDt+6aWXAGjevDkQjtjmWr7Hzj7Xzj//fHeuffv2AOy5554A3HDDDa5t+PDhZT7XzjvvDMB7770HwHfffZexfqaS77Erj/29MXny5DIf88Ybb7jjl19+GQi+f/2/CRXJge7duwPBuLRt29a1WebE8ccfD8CXX37p2ix7JZMUyRERERERkaJW1CWkbVbXNiWDYEbd8sjL48+uWV7x3XffnckuFrQtt9zSHVuOvh/BSQqL4PiRKJuh82eLbLbOohF///23azvwwAMBeOqppwC4+eabXZuVSM5kpKsQfP3112W2WV4/BLNMxx57LAAPPPCAa9tmm22AYP3c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G+aOLcXAfk+/qELNmjWBYDdsv5Sllae18ubFxlKCrAz8d99959rikKYWF1aK2y/zb4VsDj74YCD83rISyVbC3R4LQfEQSxH0CxYU6+elX0Dg/vvvB+CAAw4A4Pbbb0/5uGKy1VZbuePy0vceeughIL9pavlg5d7PPfdcd84Wxtt15LP3sRUZAOjfvz9QvO/BdKX6+zgV+3zLdppaVIrkiIiIiIhIoiSq8IDxIyw241GrVq1Sz1/er26FA/zNA63wgC3Y/eSTT1ybzQ74i8krK9+FB2yRtl9G24o6TJgwoVKvk21xWdhns8AAzzzzDBBs6GYbNEL0SI7p1asXEJ65st/HL59ekc0I4zJ2PovWvPrqqwC0bNmy1GNGjhwJBFFGCEcuciFuhQdsTGxTRSsXDeWXhs+1OF5zhUJjF12+x84+j++44w53rmfPnmU+3hbUl1c8KVfyMXYbbLCBO7btE/ztOoz93RfXMtH5vu4q4tJLL3XHlhGQihXnOvHEE7PeJ1DhARERERERKXK6yRERERERkURJZLqazxbS+zW/bY+NBQsWAPDwww+7NkuDsVQiewwEKTNWqz7bCiGkGVcau+g0dtHFPV3thhtucG353J+kJF1z0Wnsosv32FlqvZ/6Xh7bm2/o0KEZ60NU+R67QlYIY+entvsFWQCWLVvmjvfaay8g2Dsx25SuJiIiIiIiRS3xkZxCVgh3+3GlsYtOYxdd3CI5hULXXHQau+jyPXZW6Oe+++5z5yzTZOHChQBce+21rm3w4MFAUPI8n/I9doWsEMbOfz0rUnPkkUcC4QI2Y8aMyWm/FMkREREREZGipkhOjBXC3X5caeyi09hFp0hONLrmotPYRaexi05jF53GLjpFckREREREpKjpJkdERERERBJFNzkiIiIiIpIouskREREREZFEiWXhARERERERkagUyRERERERkUTRTY6IiIiIiCSKbnJERERERCRRdJMjIiIiIiKJopscERERERFJFN3kiIiIiIhIougmR0REREREEkU3OSIiIiIikii6yRERERERkUTRTY6IiIiIiCSKbnJERERERCRRdJMjIiIiIiKJsla+O5BKlSpV8t2FWFi1alXaP6Ox+4/GLjqNXXTpjp3G7T+65qLT2EWnsYtOYxedxi66dMdOkRwREREREUkU3eSIiIiIiEii6CZHREREREQSJZZrckRERCR56tWrB8C8efOA8FqDAw44AIBXXnkl9x0TkcRRJEdERERERBJFkRwREZEybLHFFgCsueaapdp23nlnAPbZZ59SbYMHDwZg5cqVZT73999/746rV68OwO+//x69szFVt25dd/zcc88BQZUkv1pSkyZNAEVyRCQzFMkREREREZFEUSRHssbyqwGuvPJKAI477jgAfvzxx7z0Ke522203IJjVBXjzzTfz1Z2ss3z8atWquXOHH344AH379nXn/ve//4UeP3fuXNfWv39/AIYOHQrAv//+m8UeS7HYe++9AXjxxReB8DVakr+uxCITdh03atSozJ979dVX3XGdOnUA+O6779y5jz76CIB+/fql0/XYsc81gGbNmoXalixZ4o4nT56csz5JcWratCkA119/vTtnEcStttoKgDPPPNO1TZgwAYA5c+bkqIeSSYrkiIiIiIhIougmR0REREREEqXKKn/VX0z4of9caN++vTu2UKZ555133PHrr7+eqy4B4QWZFZXrsUvF0jqefPJJd65du3YA7L///gC89tprWe1DIYzdpptu6o5vuukmAI4//vhSffn777/LfA5LpXn66acBeOSRRyrdr1yO3bHHHgvAY4895s799ddfADzxxBPu3Keffhr6ua5du7pjS2UbMGAAkN/UnnTHLg7v1ziIy/t1k002cce2QL5FixZp9SUbX6mpih5U5vVydd3ttNNOQPA5BeExBjjqqKPc8dixY3PSLxPnsYu7Qhs7ex+/9NJLANSqVavMx/7xxx/u2NLVTjrpJACWL19e6b4U2tjFSbpjp0iOiIiIiIgkSlEWHrAZ9F69egFw2WWXuba11goPiV/+0xaSvvzyy9nuYkEbOHAgEERvJOzCCy8E4Pzzz3fnbIO8VNZZZ50y24488kgAOnToAECDBg1c25AhQwD49ddfo3c2g9Zbbz13bMUC9tprLwBGjBjh2m6++WYAZsyYUeZz+ZHASZMmAcFC8apVq7q2FStWVLbbiWfXYcOGDd25Pn36lPl4u+YsenjQQQe5tkL/bPTHwMpDm3/++ccd33///UC4AIaxstBPPfUUAK1bt3ZtLVu2DD12+PDh7tgyCvzF936WQSE6+uijgdLRGwh+z+effz6nfZLiUbNmTXd81113AVCjRg0giPxD8F41fiTHLwIEcMwxx7jjzz77DCj/u6pQNW7c2B3fc889QPAd+80337g2K9ZgY2DZOgALFizIej9XR5EcERERERFJlKJck9OzZ08A7r777rT6Ynf3tjbHXzNgJZEt3zMTCjVv888//wTCJVfHjRsHwAknnADAb7/9ltU+xGXsNtpoI3ds625OPPFEIJxnv3TpUiAoU/n444+X+Zz+bHB5M71WqnXatGlp9TlbYzdlyhR3vO222wLBWhyLqkZh7zmbQdpzzz1d23vvvRf5eaMopDU5hx56KBCsPfE1b94cgE8++QQIr1W0aM2GG24IQKtWrVzbxx9/HKkvcXm/brPNNu7Y1mPWrl0bCM9e2nsw259jFRGXsfN17NgRgIcffhgIZs8hiIjZWPsls3MtjmNXKAph7PzPprfffhsIMncsYyBdtkYHYOTIkUD6a2HjOHb294h9j44ePdq1bbDBBqHHzpo1yx1b9pOVyvcjsxb196PglaU1OSIiIiIiUtR0kyMiIiIiIolSNIUH/PQfS9NIly1iswX1/sL6ZcuWAUGKgx8KnThxYqTXSxJbgBaH9I5cqlu3rju2687Cu35Z5AsuuACo2MLttdde2x1ffPHFAFxzzTWlHnfaaacBcPbZZ6fb7azw0+ys37fcckuk5/LHoOTCUCmbn8pnqRaWBuGX77XF83Xq1AFg6NChrs0Kt9hC06gpanHk73RuaWrGTz21Ah/F9nlWnjXWCOZMTz31VCBIU/PTVaxQSD7T1IqBXyK5ZOEHS4uG8rcoKHRt2rRxx/Y5FzVNrXfv3kC40Mqtt95aid7ln32+Q1AMyJZz+NeFFVoZNmwYAO+++65rs8JIlpraqVMn19a9e3cgXGAl1xTJERERERGRRElkJMcvx2vl/g455BB3zl8EmSn2nLZoyy8bagvdrLRyUm2xxRZAMGPiLxCzmeFi45eWtJK0FhFctGiRa/NLVq6ObZYJsGTJkjIf58/ax8GXX37pjn/55ZdKPdcBBxzgju299uabbwLw9ddfV+q5k2jrrbcGwkUGbKZ33rx5AJx++umubfHixUBQAnjXXXd1bTbD17dv3yz2OLc23nhjIJjF9Fm0xi9KYOMjAf971/++hWDRN8B+++2Xsz7lmi1092fIK8IKsQAcfPDBGelL/fr13XGTJk1CbX42SyaLJcWNH1WYOnVqpOewCLdd0507d3ZthZqlY9H4Sy+91J2zz74xY8YAcPnll7u28r5TbXNUe67NNtvMtZ1zzjmAIjkiIiIiIiIZo5scERERERFJlESmqx144IHu+I477shLH/zF0DfeeCMQXnxZkT16Co2lr9iO87ZfDgSL2orZwoULQ//NhPJ2Wn7mmWcy9jqZsP3221f6OXbffXcg2IHZ99VXXwHhYg92bClHflqgpXNY6tEPP/xQ6f7FjaWk2YJRfzGyjcUpp5wCwM8//+zaLH0r1eenpR0+/fTTWehxftjv6xe0sGumQ4cOgFLUVufJJ58sdc6+A6699tpIz+nvDWIpXVbExn8vr1y5MtLzZ4qlNFkabdyLoYwaNcodlyywkST+v8Pvv/9e4Z+77bbb3LGlqVmhFts7rJBZSuS5557rztk1cdJJJwHpF6SwlDY/VdUvRpIv+e+BiIiIiIhIBiUyktOtW7fIP/vvv/8CMGjQICA8k7TeeusBqXcIt4W9/fr1A8LFDWx28Pbbb3fnfvzxRyD17JdIefxFrX5pX4CffvrJHdvsfaHyZ+GuuOIKIJhl8hc3GotI2H99trh+5syZ7tzee+8dajvxxBNdm5XIzOROzfnwf//3f0C4lKqxgihWttz/zLryyiuBINrll0q2UqpJYmVQfVbgw7YHkNQsErDddtuVarvzzjsBeOWVV9J6zsaNGwPhxc9du3YNPcaPvFoJfn+riFyyaJ/9/ZBJ/ntv7ty5Ff45v1CGH6EE6NGjR+U7lkBWfOX888935+69914g2PYgnYhQnGywwQbu2L4XvvjiC3fOronKlhT3x8e2y9htt92A8PdvrsZRkRwREREREUmUREVyrFzgHnvsUe7jvvnmGwAGDx4MhNeLWGnbPn36ROqD5W3fdddd7pzNSNtdLUDz5s2Bwo/k2IwbpJ4tlszZcMMNARgwYIA717BhQyCYQfTLZBb6DLT/PvZndCGcjz99+vRQm63NgSBKY/zZX1srZyUzrQQ1BJur+msJsjFLmw1++fqSUW2/lKfNUJpddtnFHZfcQNbfKNQfp6RIVfLXNlDs2LEjAB9++GFO+xRn/qzwU089VeqcRRxsg8CKsoiMre9cd911y3ysH821TQfzFcmxMbCy/X5pf3/TZwhvJmnlm2fNmlXqnJk/f747/vzzz1fbF1tf5n8X2Oa1tp7pvffeW+3zFDLbrsH+67Prxv8usJLattbTv2579eqVtX7mUpcuXdyxrc28+uqr3TkrBZ1J7dq1A4L1m/61vNVWWwHhLTGyQZEcERERERFJFN3kiIiIiIhIolRZ5W9LHxP+Yv902AK98kLcAG+99RYAbdu2BcK7p1tBgJIpMOk6+eST3fEDDzxQ5uPWXHPNMtui/NNEHbuo9t9/f3dsC5iNX0LaX9ScC4UwdumyHYWvv/76Um1Lly4FguIYlRGXsfN3/h4/fjwQpKmNGDHCtZ111lmVeh1Ls9xnn33cOSsgYgtRoWKFHNIdu0yOm6UL+aXD/d8J4I033nDHU6ZMCbX56YGW8mZlpXfYYQfXlo1SynG55myhPAT/9iUXbUOQnmRpWf64WtEKPzUjm/IxdraQGOCdd94p1f7oo48CQRpZqte2wh9WVATC3ycAK1ascMeW0mWpL/7PGb94iP8ZUZZMj91GG20EhEta+4UDINhtHsKFYjLFUq38Qg32e3bu3BnITJp8XN6zqVjK3quvvurONWnSBAiK11j6N8Att9wCBOWh/c/NbKRx5WPsevbs6Y5tqYafppupz3X/PWzfRTVr1gRgwoQJrs2KdaRb/j3dsVMkR0REREREEiVRhQcqyhYVm4kTJ2b8NfxFmMXCZhpiGByMBVv8abPk/gLxkj777DN3bBtgnnDCCaUeZxsz+jPQSeEvcLeZNYvkfPzxxxl7nUmTJoX+C3DJJZcAsOWWW2bsdbLNrpOS0Ruf31be44zNiJ533nnunC0KTyJ/czzbyM42t7OFshAUIzDnnHOOO7bolxVveP/9913bnDlzMtvhHLP3w1VXXVXu46yYRyoWFfRn2Uv67rvvgKA4CASFMuyaTBXJyXc0ftGiRat9TDaiNxBEJvbaa69Sbd9++y1Q+IWOKsreg1YqGYJIxplnngkEm8tCUOzB2rIRvYkjf1P6Dz74AIDRo0eX+XgrmlGvXj13zgp+WbaOvxm3RXDsu/yYY45xbbnawFeRHBERERERSZRERHKsbKPlTqeazfE3I3vttdey1hcrT3jaaae5czYj6Hv99dez1od8KfYIzk477eSOrSTlYYcd5s5ZuW2biYzKL7lo63T8dRhJ4c+m+VGWbPFLu5a3Vi6urHT2/fff787Zurh9990XgMmTJ7s2i0akuh4tN/2FF14Awpu4FQuLxNhnuh8tPeqoo8r8Octzf+yxx4Bw9MY2GXz22Wcz2tdcsXU0/uea8dfA2HVjGQ1+hMyipKnYWh5bE+dvCHzDDTcA0LJlyyhdTzybNd98881LtT3xxBO57k4s+OtubNsO+/vQzwZo3759bjuWB/aehODvYVuj5R/ffPPNGXtNW+djG6na2uFcUiRHREREREQSRTc5IiIiIiKSKIlIV7MFtFWrVgVSp01Z+DvbLFXEUpMg2Cn977//dudSlQEuRP4u4FYe0BbqJpFfntzKo1oahV9qNlXZ2UwZNWqUO7aS55I+u06tAISfRvPNN98A4dSvuLM+n3HGGWU+xt8l3tLzLF1t2rRprs3KSftl4IuV7Yzup3ZYOVpbgL/ffvu5ttq1a4d+3i9eYQt9rUiEfXZAUFQjzsorxHHBBRe4Yyv9bIUD/FLsJflFLawU92WXXQaES1CvtVbZf6588cUXQPnFDJKuV69eZbaNGzcuhz2JD3+BvKWp2fYg3bp1y0uf8sWKTwAceOCBQPizKp2UvbffftsdW5q0XX9+MQPbpsAvvpJriuSIiIiIiEiiJGIz0IpsAmozZwBTp06N1rES/AV+PXr0AIIFVtWqVSv1eL/kpV8asyxx3mwrlaFDhwLBRqhJ2gx05513BoJNwyC8iWxJNnPhb87pb6iYKbaA0GZmMqHQrruoZs+eDaReqLv11lsD6Zf8zedmoBXhz/bajJuVW7VF5RDMjOdKLq45i0LY98Uvv/yS9muWxf/uOfzww0Nt/iayJb8X/JnmBQtDX2hWAAAgAElEQVQWRHrtXL5fbQa3VatWpdosugVBmXErGZuqkIddd37ZZXsvlvd9sWzZMiDYFBSC79bvv/++Ar9FIEmfdb///jsQjJ0f1bKNlS2rJBPiPHYWAfQ3YrdIoG3Wa5vBQ3jT2VyI89hFZSXeu3Tp4s5ZgZJMFg7SZqAiIiIiIlLUErEmx2bLcxWUsvLA1113nTtnM/2pWETDyrJK4bFZ7vKiNz7baKy8PHKfXSOW42/lZ322PsSuPwg2frv66quB0hvdyn8sx98iNBBenwLhNQW2GWFS2Mz7HXfc4c7ZGkErQ57r6E2u2doPm9X+9ddfSz3GX+NmG+1WduPZjz76yB37GQUQXuczaNCgSr1OvvkbGFeErQUrr6S+nw0wcuRIINjgMd3XKzazZs1yx5mM4MSZrYW1yJ7/3rPIYbt27QBo06aNa8vmtiJJZ2tbjzvuOAAmTpzo2nKx9cPqKJIjIiIiIiKJopscERERERFJlESkq3399dcAbLXVVmU+xl/8aWkZ99xzz2qf28pTQ1By1VLTrGR1KrY4EoKds/0dZ6Ww2ALaivJ3Wi7JFu9a2gUEC9w/+OCDMn/O0iP3339/d84WMvsFDgqBvXcsxP3II49U+jnr168PhNP5zJlnngmEU9Tsc8B2svcXqSYlvcPGxEoV+wvf33nnHSD82ZhkzZo1A2C77bbL+HP7i4LTSZv2ixQUQrraypUrc/I6VubX0nABnnnmmZy8diFp0aKFO05V7KjYWLq2pYvfeuutrs2uKUtXa968uWtTulp6mjZt6o7HjBkDBKm+55xzTl76VBZFckREREREJFESEcmxGW5bmL3GGqXv3fzylnbcsWPH1T53ujN0S5cuBWD8+PHu3PDhw1f7cxJvVgjAX6xYkl+G0hZ1+4uW7Tq10p7+5rAVMXnyZABuv/12d+6oo44Cgk30CoWVlLXCCTfddJNre/TRRwG48MILK/RcFp2x8u29e/cu87H+4npbMJm0IgO+Dh06AEFE2i9TbGXvi4VFsxo2bAiUv7FluqJGcvyIfyE45ZRTgPB3p//eLck2i67oprIW5bYCGT/99FOkfhaLnXbayR1XtMhNklnGhb0fbSNfgB133DHUJtFZdhJAzZo1AXj55ZcBmDt3bl76VBZFckREREREJFEScetvkZKzzjoLyOzsRnkzdLb5FsC8efOAIAe0WPLci8Xo0aOBIFKXih+1mTZtWtb6cvnll6c8LiQ2s3v++ecDsOuuu7q2c889F4CTTjqp1M/Nnz8fCP87NG7cGCh/XZJtHOg/5x9//BGh5/Fna08gXDIagmg3wOeff56zPsXBiy++CATXml/239Zs+ZtcliwxXp50ty+wjZMrsil0nNj6V3/9kL3/bBNoCDbltHLdixcvzlUXpcj4618bNWoEBO/H66+/3rVVr1491CbR7bLLLu7YNlW+77778tWdcimSIyIiIiIiiaKbHBERERERSZQqq2IYu6vswjBb4A3QvXt3ADbZZBN3rrzSzyX5RQxsQZUtpvRTQV5//fVIfS1PlH+afC6qq1GjBhCkJvglLceOHQvAqaeeCsCiRYuy2pdCG7s4ycfYHXnkke7YSrWfccYZkZ5rxIgR7thKg44aNQqo+ALoqNIdu2xccwMGDHDHtvO3pU/66VjZHot0xOX96n9PWEqGFagoz5IlS9yxfbZZUQH/s852YLe0y7/++quSPY7P2BWiQh87//1sxWcsRd8vUHPRRRdl/LXjMnZ+upq918rrm6WV+985ll6ZK3EZu3RZsRp/u4XBgwcDwXKRbEt37BTJERERERGRRElkJCcVfzNHm6GzxaY+m1nr379/qTZbtJtu6d+oCvVu34ov9OnTp1SbbWSZ7c23CnXs4iDfY2fPdfTRR7tz++67LwBHHHEEEC4aYBFD2wju4Ycfdm253tQzn5Ec29zONvmEoFR3p06dgCCiGjf5vuYKmcYuukIfO7/Ygz+7DuECTDNmzMj4a8dx7Cz6v99++wFBsQEIoqj9+vXLah8qIo5jV561114bgEmTJgHhTWhz9TedUSRHRERERESKmm5yREREREQkUYomXa0QFVpIM040dtFp7KLLZ7qaFWyYMGGCO2dFGE444YSMvU426JqLTmMXXaGPnb/o3gojWYpWMaarFYpCG7tXXnkFCNLG/T1xevfuDcDKlStz0helq4mIiIiISFFbK98dEBGRzFmwYIE7vvHGG/PYExHJJtttHnJfZEWKR4MGDQD46quvgKDID+QughOVIjkiIiIiIpIoiuSIiCTASy+9BEDdunXz3BMRyTXb0HL58uVAuMy+SGVsv/32+e5CZIrkiIiIiIhIougmR0REREREEkUlpGOs0MoMxonGLjqNXXT5LCFdyHTNRaexiy5JY1enTh0A/vzzTyD76WpJGrtc09hFpxLSIiIiIiJS1GIZyREREREREYlKkRwREREREUkU3eSIiIiIiEii6CZHREREREQSRTc5IiIiIiKSKLrJERERERGRRNFNjoiIiIiIJIpuckREREREJFF0kyMiIiIiIomimxwREREREUkU3eSIiIiIiEii6CZHREREREQSRTc5IiIiIiKSKGvluwOpVKlSJd9diIVVq1al/TMau/9o7KLT2EWX7thp3P6jay46jV10GrvoNHbRaeyiS3fsFMkREREREZFE0U2OiIiIiIgkim5yREREREQkUXSTIyIiIiIiiaKbHBERERERSRTd5IiIiIiISKLEsoS0iIiIJMNxxx3njrt06QLA4YcfDsAaawRzrX379gVgwIABOeydiCSVIjkiIiIiIpIoVVZF2ZUoy/K56dF9990HQN26dQHo37+/a/vpp58A+OeffwD44YcfstoXbRgVXaGN3amnngrAnnvuCcDJJ5/s2uw6u+eee0r93K+//grAoEGDMtaXfIxd1apV3fEhhxwCwJZbbunO7bDDDqHHb7LJJu64U6dOQNDvZ5991rU988wzALz++usAzJkzp1L9XJ1C3Ay0Zs2aAMyePdudO+eccwAYNWpUTvpQaO/XOInz2G2zzTYAvPzyy+5cw4YNQ4/p2LGjO37//feB4Ls22+I8dnFX6GNn37kAJ554IgCtW7cGoF27dq7thRdeyPhrF/rY5ZM2AxURERERkaKmmxwREREREUkUpasBQ4YMccenn346UH5IbPny5QAMHTrUnbvqqqsA+P333zPWrySGNA8++GAAjjnmGHfOQsMWFj7llFMq/TpxHLuePXsCQUpa586dXdtaa61VqT48+OCDQDgEH1U+xq5+/fru+LvvvqvUc6Xy0UcfAbD33nu7c8uWLcv46xRiutoFF1wAwC233OLOHXjggQC88sorOelDHN+v9p601GX7bqiMs846C4DatWsDMHXqVNdm16Z9v1RUHMfOzJgxA4Dtt9++zLadd945J31JJc5jF3eFNna77747AI899hgQ/s6xv9vuuusuIEhvLnmcKYU2dnGidDURERERESlqRV1C2mbWbdFZRdWoUQOA3r17u3P77rsvEMyALliwIBNdTIx69eoB8PjjjwOw7rrrujZbPP/oo4/mvmNZYpGVo48+2p2zBd7lzcjMnTu31LkGDRqs9vVs8f3AgQPduWnTplWsszGwcuVKd7x06VIgGK9M2GWXXYBwIZELL7wwY89fyDbffHMgfF1+++23+epOXvnv16uvvhqAJk2aZPx1/v33XwCaN2/uzq2zzjpA+pGcOGrVqhVQusiA79prr81Vd0RcgYstttgCCH/XXnLJJQA8//zzQDLeg/IfRXJERERERCRRijKSs9lmmwFw8cUXA1CtWrVKP+eOO+4IBDNYfhlbgeHDhwPhCI5p3749AJMnT85ll7KqQ4cOQPj3HTlyJBD8nhbV8q1YsaLUOSuvbGWU33zzzVKPsfUD66+/fmW6nTd+ydiuXbsC4cje9OnTgSDCcP/991foedu2bQvA5ZdfDkCPHj1c2x133AHAvHnzIvY6WWK4PDNnbENKe99C6QiObR0AQbQx1fvN3sN//fVXqZ/zjwH+7//+zx1nY41Y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hf+dYIVp/E17pWLse9c+z1JFrfMZwYmzbt26hf7fLwltpcefeOKJnPapWNjWLX502qK2VtjF/n7MJUVyREREREQkUXSTIyIiIiIiiVI0hQcKUb4XpzVu3BiAGTNmuHOTJ08GgoWPcZXvsStkGrvoCrnwQD7l45o79NBD3XGrVq2A8OfaW2+9BQSLZv203QceeACAzTffHAjvum5FQ/r16wfA3LlzK9XP1dH7Nbo4jt35558PwK233lqqrXfv3kCw6D6f4jh2hSLJY+dfm1YUYo899gDgvffeq/Tzq/CAiIiIiIgUNUVyYizJd/vZprGLTmMXnSI50eiai05jF10cx+7nn38Ggsih/T8Epbk///zzrPahIuI4doUiyWN3wAEHuOOXXnoJCLZd8YsCRaVIjoiIiIiIFLWiKSEtIiIiEmevvfYaADNnzgTgjjvucG2//vprXvokUlFTp051xxMnTgSgSZMm+eqOIjkiIiIiIpIsuskREREREZFEUeGBGEvy4rRs09hFp7GLToUHotE1F53GLjqNXXQau+g0dtGp8ICIiIiIiBS1WEZyREREREREolIkR0REREREEkU3OSIiIiIikii6yRERERERkUTRTY6IiIiIiCSKbnJERERERCRRdJMjIiIiIiKJopscERERERFJFN3kiIiIiIhIougmR0REREREEkU3OSIiIiIikii6yRERERERkUTRTY6IiIiIiCTKWvnuQCpVqlTJdxdiYdWqVWn/jMbuPxq76DR20aU7dhq3/+iai05jF53GLjqNXXQau+jSHTtFckREREREJFF0kyMiIiIiIomimxwREREREUkU3eSIiIiIiEii6CZHREREREQSRTc5IiIiIiKSKInSH0kAACAASURBVLrJERERERGRRNFNjoiIiIiIJEosNwPNtfbt27vjFi1aANC3b18A3nzzTdf24osvAnDXXXcB8Pvvv+eqi0XFxt7Xv3//PPQkuoYNGwKw5557unNt2rQB4MgjjwSgdu3arm3evHkATJkyBYCzzz7btS1evDi7nRVJw/vvvw8En5VHHXWUaxs7dmxe+pRN9erVA2CdddZx5xYtWgTAb7/9VurxW265JQCHHnooANtvv71rO+2000LP9dRTT7m2kSNHAvD0009nqusFZ7311gPgtttuA6BHjx5lPrZWrVruWN/FUlH2PdyxY0d3brfddgOCa2rOnDmu7eOPPwZgwIABAPz999+56KZkiCI5IiIiIiKSKLrJERERERGRRKmyatWqVfnuRElVqlTJyetYutD48ePduV133bXMvthQLV26FIDevXu7tocffjjj/YvyT5OrscskS1HYaKONAHjnnXdcm4WGt9hii7SeM5djd+qppwJw/PHHu3NNmjQBoE6dOqWevyJ9u/POO93xBRdcEKlfUcXxurP0nsMPPxwIpzTuuOOOZfbFfpdJkyYB8MILL7i2G264IeP9THfsCuX9aqlXAM899xwQ/K59+vRxbQMHDoz0/HG85uwzx1JI7fMJ4LPPPgPgm2++KfVzNlZrrrlmWq83e/ZsALbZZpu0fi6OY5cOP23oqquuAmDnnXcGyv/dLrroInd8xx13RHrtQh+7fCqEsatRo4Y7ts9+S02zFHGA+fPnA7D11lsDULdu3VLPNX36dACuu+46d2706NGR+lUIY+dr164dELxX/RTcUaNGAXD33XfnpC/pjp0iOSIiIiIikihFWXjgoIMOAuDSSy8FSkdvVqdmzZoADBkyxJ2z2bsuXbpkoouJ58+wDBs2DIDdd98dgI033ti1vfbaa7ntWBq6d+8OwKBBgwCoWrWqa1uyZAkAP//8sztnMzGzZs0C4OWXX3Ztm222GQAnn3wyAF27dnVt9vz+YsikqV+/vju2qE3Pnj3duX322QeA5s2bl/rZkjM7qWZ6WrduDUCzZs3cuQ8++ACAl156KWq3i0bJaJlv5syZOexJ7pSMMPtsPCxim65ffvkFgOXLl7tzjRo1ivRchcY++wcPHgwEhVgA1l133dBj/ai+zR5bBoZ9jibBhRde6I5vueUWAP79918Avv32W9dmi9/9iLSxIhj+NVXsjjjiCHdsBUEOPPBAIIju++z6s+8LgG7dugHQqVMnAB555BHXFjWSE0drrfXf7YAV4vKLPTVt2hSAlStXAuHiC61atQJgp512AuCMM87IfmfToEiOiIiIiIgkStGsybF1ExDk7/rRhJIs59rPM7R1EnbH61u2bBkA5513HgBDhw6tZI8LL28zHVZiGYJc9Oeffx4I/w4W0Ui3RGguxs7Kv+6xxx4APPTQQ67NyozPnTs3ref89NNPgfAM8Y033gjAFVdckdZzRZWLsatevToQ5OD7709/HVM22UywRRBTlQNOV1LX5Eyb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AEyZMiXsUwSnlipSZmb14kiOmZmZmZkViiM5ZjmwwQYbAHDooYcCMHz48HBMs6H675gxY8KxyZMnA/DYY4814zRzY4011gjbyu0fOHAgkJ49fvrppwHYbLPNAHjxxRebdYpmHUGfR4suumi3j3nnnXfC9uuvv97oU7IOt8ACCwDJ+i+A1VZbDYDddtsNgJVXXrmi5zrrrLMAOP744wF47bXXwjFHKvPPkRwzMzMzMysU3+SYmZmZmVmhOF3NmuLHP/4xAEcffTQA5557bji2zz77tOScmu2zn/0sAF/60pcAOOqoo8KxzTffHIAFF1wQgH/961/h2NVXXw3ALrvsAsAiiywSji288MINPOP8+PKXvwwk6XwjRowIxzQeWeVqv/CFLwDwxz/+EYBvfOMb4dg///nPxpxsh5kxY0bYVhrh1ltvHfbdfffdTT8na6yFFloobFfyGXTYYYeF7b/85S8NOSfrTP/3f8lcvb4jjzzySAC++MUvdvvnsr4vsuy7776p/+61117h2MUXXwwUK21t2WWXBZL0PEj/nXsSp85fddVVAPz85z8H4M033wzHPvroo16dZ6UcyTEzMzMzs0KZ75Mc3oLON998rT6Fiqy77rphO55NAHjiiSfCdv/+/QF45plnwr558+b1+Py1/NPkdexmzpwJwNprrw2kZ9FXWGGFur9eHsfuySefBGDAgAElxzT7ceaZZwJw/vnnlzxGs0a77rpr2HfMMccA8JOf/KRu59nqsfvUp/4XYP7ud78b9p1zzjmpY7FHH30UgLfffhtI3m9Qem1ts802YftPf/pTnc44Ue3YNev9uvjiiwPpmbR6PWdczOHTn/40kERsobJrs9XXnGy66aZhWxGHYcOGAUmDS4CJEycC8KMf/aiq59dz/PWvf009T2+0Yuzif1NFV7Oe//777wfgW9/6Vjj28ssv9+q16ykv1107avXYLb300gCccMIJYd8ee+xR03PNnTsXSL5f4ihPuYIaffv2BeCNN96o6vVaPXZZlltuOQBuvvlmoHwUrFYPPPBA2FZ2jzJVoLJxqXbsHMkxMzMzM7NC8ZocYPXVVw/bgwcPBmDPPfcEkhK0AE899RSQND8bOnRoONb1Lvuaa64pef5LL7007ItnHzrBaaedBsAll1wCdF7JY4D//Oc/qf+PZ9U33nhjIJlRyvLss8+W7Js+fXqdzq614ujLr3/9ayBZp5RFUTFI1tm89NJLAHzmM58Jxw4//HAgydHuNFr39f3vfx+AnXfeORy76667anrO+eefP/Xc8ZqMV199FYCLLrqopudutazITD3XkChSFEeMpB5RnUYbOXIkABMmTCj7OGU26PszT9GbIlE0A+Dggw8G4IADDgDg3nvvDcd22mknoP3XIcZ/3xtvvBFI1rj25MMPPwSSrJLf/OY34ZjWjiy//PIAvPXWW+GYIv5ZJaf1W/Cyyy6r7C+QM0sssUTYvummm4DGRHBk0KBBYXvatGkAfO5znwv73n333bq/piM5ZmZmZmZWKL7JMTMzMzOzQunodLXTTz8dSMoOAiy55JKpx2SlFaioQFx4oNxiKC2gjsvwaXFXrSkj7SBe9H377bcDScqfFtF3EhVdGD58OJCUNwbYYIMNgCQEn0UpkUVa+Pr5z38egBtuuCHsi9NHRSkDd955J5BOh1Kamrz33nth+/nnn6/bubajcePGAUmX7nqkA2ix7YEHHlhyTO/zdk1Pyvq8r1bX4gKVPLZdrLnmmkDPi39nzZoFJOmiVru4ZcC2224LwI477gikf4Pos1TppOutt1441qdPH6B909WWWmopIP39WEmaWtyK4ac//SmQ/O7LMmfOnJJ9P/vZzwA444wzSo6ts846QPulq+kaGT16dNiX9b3blQosPP7442HfPffcA8CoUaNKHt+vX7/U62XRtQyNKcntSI6ZmZmZmRVKx0Ry4gW3WjA7cOBAoPKZcc1ObbXVVkDlsyIPPvggANtvv33Yp5lpzbC0mwUWWADILv984oknAknkApIFyauuuiqQXvDWaeKSidXYbrvtgGI1HlNTQUUHIIn2xQu+taj23//+d4/PudFGG4XtepbWbhdaQArphboAjzzySK+fXyWDsyKLapjXbqqN4CgCc9xxx6X+v+h23333ih6nYiCdHkmthQqn6L209957h2NqPzB79mwArr322nDsyiuvBJLGx9ddV2MHRgAAEtVJREFUd104pjL77UYlnZX5ombDPdFvs/i7QGNWCTXuhuyItfzhD3+o+DnzRFlFKgiV5YMPPgjbitbod3TWb9+DDjqoZJ8KXvzyl78Esn/3qdAQJBExFYmoB0dyzMzMzMysUHyTY2ZmZmZmhVKodDWlTSiVCpL+GEpRg6SGv0Ji8eM//vhj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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# takes 5-10 seconds to execute this\n", + "show_MNIST(test_lbl, test_img)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's have a look at the average of all the images of training and testing data." + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average of all images in training dataset.\n", + "Digit 0 : 5923 images.\n", + "Digit 1 : 6742 images.\n", + "Digit 2 : 5958 images.\n", + "Digit 3 : 6131 images.\n", + "Digit 4 : 5842 images.\n", + "Digit 5 : 5421 images.\n", + "Digit 6 : 5918 images.\n", + "Digit 7 : 6265 images.\n", + "Digit 8 : 5851 images.\n", + "Digit 9 : 5949 images.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAACBCAYAAADjY3ScAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJztnXmsVdXZxh8EGQSZp4LgBIoXUYyKQypiLVJRUqmxjbbGVq0V0Yqp81BB/SppQqJtorFRYxWlRk3baGtJW1HjgIgjiDOiOBQBleHiUHrP94c+Zz933dfj5Ubu3mf7/BJyD3ufYe13v2utvd5pdahUKhUYY4wxxhhjTEnYJu8GGGOMMcYYY8zXiRc5xhhjjDHGmFLhRY4xxhhjjDGmVHiRY4wxxhhjjCkVXuQYY4wxxhhjSoUXOcYYY4wxxphS4UWOMcYYY4wxplR4kWOMMcYYY4wpFV7kGGOMMcYYY0qFFznCxo0bMWPGDAwZMgRdu3bF2LFj8ac//SnvZhWeDRs24Pzzz8cRRxyBAQMGoEOHDpg5c2bezaoLHnjgAZx88skYNWoUunfvjqFDh+L73/8+nnrqqbybVnieffZZHHXUURg+fDi6deuGvn374qCDDsLcuXPzblrdceONN6JDhw7o0aNH3k0pNA8++CA6dOgQ/lu4cGHezasLHnnkEUyePBl9+vRBt27dMHLkSFx55ZV5N6vQ/PSnP/1SvbPu1eaZZ57BMcccgyFDhmC77bbDqFGjcMUVV2DTpk15N63wLFq0CJMmTcL222+PHj164LDDDsOjjz6ad7O2iE55N6BI/OAHP8CTTz6J2bNnY7fddsMdd9yB448/Hk1NTTjhhBPybl5hWbt2Lf7whz9g7733xjHHHIMbb7wx7ybVDddffz3Wrl2Ls88+Gw0NDVi9ejXmzJmDAw88EPPnz8d3vvOdvJtYWD766CMMGzYMxx9/PIYOHYrGxkbcfvvtOPHEE7FixQpceumleTexLnjnnXdw7rnnYsiQIVi3bl3ezakLfvOb3+Cwww5rdmzPPffMqTX1wx133IETTzwRP/zhD3HrrbeiR48eeP311/Huu+/m3bRCc9lll+H0009vcXzKlCno0qUL9t9//xxaVXyWLVuGgw8+GLvvvjuuueYa9O/fHw8//DCuuOIKPPXUU/jrX/+adxMLy5NPPonx48dj3LhxuO2221CpVPDb3/4Whx9+OBYsWICDDjoo7ya2joqpVCqVyt/+9rcKgModd9zR7PjEiRMrQ4YMqWzevDmnlhWfpqamSlNTU6VSqVRWr15dAVC5/PLL821UnbBq1aoWxzZs2FAZNGhQ5fDDD8+hRfXPAQccUBk2bFjezagbjj766MqUKVMqJ510UqV79+55N6fQLFiwoAKgctddd+XdlLrj7bffrnTv3r0ybdq0vJtSCh588MEKgMqll16ad1MKyyWXXFIBUHnttdeaHT/ttNMqACoffPBBTi0rPpMmTaoMGjSo0tjYWD22fv36Sv/+/SsHH3xwji3bMhyu9gV//vOf0aNHDxx33HHNjv/sZz/Du+++iyeeeCKnlhUfuszNljNw4MAWx3r06IGGhgasXLkyhxbVP/3790enTnZSt4a5c+fioYcewnXXXZd3U0zJufHGG9HY2IgLLrgg76aUgptuugkdOnTAySefnHdTCsu2224LAOjVq1ez471798Y222yDzp0759GsuuDRRx/FhAkTsN1221WPbb/99hg/fjwee+wxvPfeezm2rvV4kfMFS5cuxR577NHi4WivvfaqnjemPVi3bh2efvppjB49Ou+m1AVNTU3YvHkzVq9ejeuuuw7z58/3g1QreP/99zFjxgzMnj0bO+ywQ97NqSumT5+OTp06oWfPnpg0aRIeeeSRvJtUeB5++GH07dsXL730EsaOHYtOnTph4MCBOP3007F+/fq8m1dXrFu3DnfffTcOP/xw7Lzzznk3p7CcdNJJ6N27N6ZNm4bly5djw4YNuO+++3DDDTdg+vTp6N69e95NLCyfffYZunTp0uI4jy1ZsqS9m9QmvMj5grVr16Jv374tjvPY2rVr27tJ5hvK9OnT0djYiEsuuSTvptQFZ5xxBrbddlsMHDgQ55xzDn73u9/hF7/4Rd7NKjxnnHEGdt99d0ybNi3vptQNvXr1wtlnn40bbrgBCxYswLXXXouVK1diwoQJmD9/ft7NKzTvvPMONm3ahOOOOw4/+tGP8K9//QvnnXcebr31VkyePBmVSiXvJtYN8+bNw8cff4xTTjkl76YUmp122gmPP/44li5dil133RU9e/bElClTcNJJJ+Haa6/Nu3mFpqGhAQsXLkRTU1P12ObNm6tRTfXyTOyYDqFWyJXDsUx7cNlll+H222/H73//e+y77755N6cuuPjii3Hqqafi/fffx7333oszzzwTjY2NOPfcc/NuWmG55557cO+99+KZZ57x2LYF7LPPPthnn32q/z/kkEMwdepUjBkzBueffz4mTZqUY+uKTVNTEz755BNcfvnluPDCCwEAEyZMQOfOnTFjxgz8+9//xne/+92cW1kf3HTTTejXrx+mTp2ad1MKzYoVKzBlyhQMGjQId999NwYMGIAnnngCV111FTZu3Iibbrop7yYWlrPOOgunnHIKzjzzTFxyySVoamrCrFmz8OabbwIAttmmPnwk9dHKdqBfv37hyvSDDz4AgNDLY8zXyaxZs3DVVVfh//7v/3DmmWfm3Zy6Yfjw4dhvv/0wefJkXH/99TjttNNw0UUXYfXq1Xk3rZBs3LgR06dPx1lnnYUhQ4bgo48+wkcffYTPPvsMwOdV6xobG3NuZf3Qu3dvHH300Xj++efx8ccf592cwtKvXz8AaLEQPPLIIwEATz/9dLu3qR55/vnnsXjxYvzkJz8Jw4lMxoUXXoj169dj/vz5OPbYYzF+/Hicd955uOaaa3DzzTfjoYceyruJheXkk0/G7Nmzcdttt2GHHXbA8OHDsWzZsqrxcOjQoTm3sHV4kfMFY8aMwYsvvojNmzc3O864Q5cHNVuTWbNmYebMmZg5cyYuvvjivJtT14wbNw6bN2/G8uXL825KIVmzZg1WrVqFOXPmoE+fPtV/8+bNQ2NjI/r06YMf//jHeTezrmColb1iXw7zW1Mou3qxDOcNvQ+nnnpqzi0pPs8++ywaGhpa5N6w5LZzrWtzwQUXYM2aNViyZAlWrFiBxx57DB9++CG6d+9eN5EmHlW+YOrUqdi4cSPuueeeZsf/+Mc/YsiQITjggANyapkpO1deeSVmzpyJSy+9FJdffnnezal7FixYgG222Qa77LJL3k0pJIMHD8aCBQta/Js0aRK6du2KBQsW4Kqrrsq7mXXDhx9+iPvuuw9jx45F165d825OYTn22GMBAPfff3+z43//+98BAAceeGC7t6ne+PTTTzF37lyMGzfOhtdWMGTIELzwwgvYuHFjs+OPP/44ALjgSivo0qUL9txzT+y444546623cOedd+LnP/85unXrlnfTWoVzcr7gyCOPxMSJEzFt2jSsX78eI0aMwLx58/CPf/wDc+fORceO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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average of all images in testing dataset.\n", + "Digit 0 : 980 images.\n", + "Digit 1 : 1135 images.\n", + "Digit 2 : 1032 images.\n", + "Digit 3 : 1010 images.\n", + "Digit 4 : 982 images.\n", + "Digit 5 : 892 images.\n", + "Digit 6 : 958 images.\n", + "Digit 7 : 1028 images.\n", + "Digit 8 : 974 images.\n", + "Digit 9 : 1009 images.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAACBCAYAAADjY3ScAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvqOYd8AAAIABJREFUeJztnXmMVtX9xh8WWRxk3zoIuLCJiLghmp+ItUhFSaXGNtoaW7VWRCum7mIFtZU0IdE20diosYpSo6ZttLUkrahRBBVBAcENUVzKKssMLqVzf3/o895nzhxehikz977X55OQebn3Xc793u85557vdlolSZLAGGOMMcYYYwpC66wbYIwxxhhjjDF7Ey9yjDHGGGOMMYXCixxjjDHGGGNMofAixxhjjDHGGFMovMgxxhhjjDHGFAovcowxxhhjjDGFwoscY4wxxhhjTKHwIscYY4wxxhhTKLzIMcYYY4wxxhQKL3KEmpoaTJs2DdXV1ejQoQNGjRqFP/3pT1k3K/ds374dV199NU455RT06tULrVq1wowZM7JuVkXw9NNP4/zzz8ewYcNQVVWFfv364Xvf+x4WL16cddNyz9KlS3HaaadhwIAB6NixI7p3747jjjsOc+bMybppFcc999yDVq1aoVOnTlk3Jdc888wzaNWqVfTfwoULs25eRfD8889j4sSJ6NatGzp27IjBgwfjlltuybpZueYnP/nJLvXOuleeJUuW4IwzzkB1dTX23XdfDBs2DDfffDN27NiRddNyz0svvYQJEyZgv/32Q6dOnXDSSSfhhRdeyLpZe0TbrBuQJ77//e/j5ZdfxqxZszBkyBA8/PDDOPvss1FXV4dzzjkn6+bllk2bNuEPf/gDDj/8cJxxxhm45557sm5SxXDXXXdh06ZNuPzyyzF8+HBs2LABs2fPxpgxYzBv3jx8+9vfzrqJuWXLli3o378/zj77bPTr1w+1tbV46KGHcO6552LNmjWYPn161k2sCD766CNceeWVqK6uxtatW7NuTkXwm9/8BieddFK9YyNGjMioNZXDww8/jHPPPRc/+MEP8MADD6BTp05499138fHHH2fdtFxz44034uKLL25wfNKkSWjfvj2OOeaYDFqVf9544w0cf/zxGDp0KG6//Xb07NkTzz33HG6++WYsXrwYf/3rX7NuYm55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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(\"Average of all images in training dataset.\")\n", + "show_ave_MNIST(train_lbl, train_img)\n", + "\n", + "print(\"Average of all images in testing dataset.\")\n", + "show_ave_MNIST(test_lbl, test_img)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing\n", + "\n", + "Now, let us convert this raw data into `DataSet.examples` to run our algorithms defined in `learning.py`. Every image is represented by 784 numbers (28x28 pixels) and we append them with its label or class to make them work with our implementations in learning module." + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(60000, 784) (60000,)\n", + "(60000, 785)\n" + ] + } + ], + "source": [ + "print(train_img.shape, train_lbl.shape)\n", + "temp_train_lbl = train_lbl.reshape((60000,1))\n", + "training_examples = np.hstack((train_img, temp_train_lbl))\n", + "print(training_examples.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we will initialize a DataSet with our training examples, so we can use it in our algorithms." + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [], + "source": [ + "# takes ~10 seconds to execute this\n", + "MNIST_DataSet = DataSet(examples=training_examples, distance=manhattan_distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Moving forward we can use `MNIST_DataSet` to test our algorithms." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plurality Learner\n", + "\n", + "The Plurality Learner always returns the class with the most training samples. In this case, `1`." + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n" + ] + } + ], + "source": [ + "pL = PluralityLearner(MNIST_DataSet)\n", + "print(pL(177))" + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 8\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 103, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[177])\n", + "plt.imshow(test_img[177].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is obvious that this Learner is not very efficient. In fact, it will guess correctly in only 1135/10000 of the samples, roughly 10%. It is very fast though, so it might have its use as a quick first guess." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Naive-Bayes\n", + "\n", + "The Naive-Bayes classifier is an improvement over the Plurality Learner. It is much more accurate, but a lot slower." + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7\n" + ] + } + ], + "source": [ + "# takes ~45 Secs. to execute this\n", + "\n", + "nBD = NaiveBayesLearner(MNIST_DataSet, continuous = False)\n", + "print(nBD(test_img[0]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make sure that the output we got is correct, let's plot that image along with its label." + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 7\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 105, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[0])\n", + "plt.imshow(test_img[0].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### k-Nearest Neighbors\n", + "\n", + "We will now try to classify a random image from the dataset using the kNN classifier." + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n" + ] + } + ], + "source": [ + "# takes ~20 Secs. to execute this\n", + "kNN = NearestNeighborLearner(MNIST_DataSet, k=3)\n", + "print(kNN(test_img[211]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make sure that the output we got is correct, let's plot that image along with its label." + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 5\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 107, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[211])\n", + "plt.imshow(test_img[211].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hurray! We've got it correct. Don't worry if our algorithm predicted a wrong class. With this techinique we have only ~97% accuracy on this dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MNIST FASHION\n", + "\n", + "Another dataset in the same format is [MNIST Fashion](https://github.com/zalandoresearch/fashion-mnist/blob/master/README.md). This dataset, instead of digits contains types of apparel (t-shirts, trousers and others). As with the Digits dataset, it is split into training and testing images, with labels from 0 to 9 for each of the ten types of apparel present in the dataset. The below table shows what each label means:\n", + "\n", + "| Label | Description |\n", + "| ----- | ----------- |\n", + "| 0 | T-shirt/top |\n", + "| 1 | Trouser |\n", + "| 2 | Pullover |\n", + "| 3 | Dress |\n", + "| 4 | Coat |\n", + "| 5 | Sandal |\n", + "| 6 | Shirt |\n", + "| 7 | Sneaker |\n", + "| 8 | Bag |\n", + "| 9 | Ankle boot |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since both the MNIST datasets follow the same format, the code we wrote for loading and visualizing the Digits dataset will work for Fashion too! The only difference is that we have to let the functions know which dataset we're using, with the `fashion` argument. Let's start by loading the training and testing images:" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "metadata": {}, + "outputs": [], + "source": [ + "train_img, train_lbl, test_img, test_lbl = load_MNIST(fashion=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualizing Data\n", + "\n", + "Let's visualize some random images for each class, both for the training and testing sections:" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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fhgzZgyOHz+OoUOHol69eti6dSty5sxplT1w4AC6d++OwYMHo2zZsq4Tkz/Ys2cPgOtWtdSsDfLGL7/8gjp16iB37tx49913kTNnTsycORPdunXD0aNHMWDAAMTGxqJVq1aYPn06hg4daptsp06diixZslgWqiNHjqBGjRrIlCkThgwZguLFiyMhIQEjRozA/v37MXXqVNv13333XZQqVQpjxoxBdHQ0SpYs6df7SwktWrRA5syZ8e2331pply5dQps2bdC7d28MGjTIejGYOXMmunTpgrZt22L69OkICQnBBx98gKZNm2LJkiXWx9Jjjz2GzZs349VXX0WpUqVw8uRJbN68GSdOnAAAnD17Fo0bN0ZcXBzee+895MmTB0eOHMGqVatw+vTpW98IPnL16lWsXLkSVatWtY1DyfHkk09i0qRJ6Nu3L1q1aoX9+/fj5ZdfxurVq7F582bkypULAPDbb7+hdu3a6NWrF2JiYrB//368+eabqFevHn788UeEhISgV69e+OuvvzBu3DjMnTsX+fLlAxAYLra1a9fG5MmT8cwzz+CRRx5BlSpVbC9FZPfu3WjRogWee+45ZM2aFT///DNGjRqFjRs3JnEdvnz5Mtq0aYOePXuif//++PbbbzF8+HDExMRgyJAhAK5rxu+//34cOnQII0eORKlSpfD111+jY8eOSa69f/9+lC5dGp06dUKOHDlw+PBhvP/++6hevTp27dplPYtAw9e2Ba4r0Pr3749BgwYhT548mDx5Mnr27IkSJUrg3nvv9Xodtz5fsGBBPPHEE/jtt9/SxL3TV/zdBikZr/fv34/evXujcOHCAID169fj3//+N/73v/9ZcujEGIMXXngB7777LiZPnmy90F+7dg1t27bF2rVrMWDAANSpUwcHDhxAfHw8GjRogE2bNtksPZs3b8ZPP/2E//u//0NcXFxAuIXTc8EYg6NHj+KNN97A2bNn0blzZ6uMr23WvXt3zJkzBwMGDMB9992HXbt24YEHHsCpU6fS9B7Onz+PTz/9FNWrV0f58uXRo0cP9OrVC59//jm6du2apPx7772HMmXKWOtfXn75ZbRo0QL79u2z3i+dfPbZZ+jSpQt69OiBcePGJevlk1KZcOPy5cto0aKF1XfXrVuHESNG4MCBA9ZaeWMM2rVrhxUrVmDw4MG45557sH37dsTHxyMhIQEJCQmWUs+XeWvChAmpHxvMbaRr164ma9asKTqmT58+JleuXF7LfPjhhwaAeeaZZ2zpr732mgFgjh07ZqXVrVvXNGrUyPp79+7dBoApVaqUuXz5su34hIQEA8B8/PHHrtdt1aqVeeCBB2xpoaGhpmfPnknKdujQwYSFhZmDBw/a0ps0aWIiIyPNqVOnjDHGLFu2zAAwNWrUMNeuXbPK/fbbbyY4ONj06dPHW1MYY7y386JFiwwA06RJkyR57dq1MxEREebw4cO29Pvuu89ER0ebM2fOGGOMef/99w2AJOV47oSEBGOMMd99950BYBYvXpxsXVetWmUAmPfee8+WvnfvXpMlSxYzZMgQK61mzZoGgPn++++93H3KmDp1qgFg1q9fby5fvmxOnz5tFi5caGJjY01UVJQ5cuSIiY+PN25dh8fu27fPSqtfv76pX7++rRwAEx8fb/3dqVMnExoaan7//XdbuebNm5uIiAhz8uRJY4wx8+fPNwDM0qVLrTJXrlwx+fPnN+3bt7fSevfubSIjI82BAwds5xszZowBYHbu3GmMMWbfvn0GgClevLi5dOlSitoptbCNEhMTky2TJ08eU7ZsWWPMddkFYD766CNbmbNnz5ocOXKY1q1b29KvXr1q7r77blOjRg0rLTIy0jz33HPJXm/Tpk0GgPnqq69Sc0u3jSNHjhgAplOnTjcs+9NPPxkA5qmnnrKlb9iwwQAwL774outx165dM5cvXzYHDhwwAMx///tfK++NN95IIu+BwJ9//mnq1atnABgAJiQkxNSpU8eMHDnSnD592vUY3ueaNWsMALNt2zYrjzL42Wef2Y5p0aKFKV26tPU3x0HZRsYY8/jjjxsAZurUqcnW+cqVK+bMmTMma9as5p133rHSOR6uWrUqBS2QdvjatkWKFDFhYWG2Mej8+fMmR44cpnfv3laa2/0l1+eNMaZly5amSJEiaXJvvuLvNvB1vHZy9epVc/nyZTNs2DCTM2dO2/tBkSJFTMuWLc25c+dM+/btTUxMjFm+fLnt+E8//dQAMF9++aUtPTEx0QAwEyZMsJ0vc+bM5pdffklBS6UdnEec/0JDQ231dpJcm+3cudMAMAMHDrSVZxt17do1ze5lxowZBoCZOHGiMcaY06dPm8jISHPPPffYynG+rlChgrly5YqVvnHjRgPAfPrpp1aafOd7/fXXTebMmc2oUaOSXNv5fpISmXCDfVeOYcYY8+qrrxoA5rvvvjPGGLN48WIDwIwePdpWbs6cOQaAmTRpkjEmZfNWaseGgHWH4xc+/5n/b7qsUaMG/vzzTzzyyCOYP3++pc11wxmMgMEMfv/99xtev23btimyKpw+fRrLli1D+/btfSq/cuVKNGnSBAUKFLCld+3aFWfOnMGGDRts6Z07d7aZ94oVK4aaNWv6zXXJrd4rV65Es2bNkDdv3iR1PHXqFBITE1N0jTJlyiA6Ohr9+/fHhx9+iJ9//jlJmYULFyJz5szo3Lmz7fkXKlQId911VxJ3m3z58qFOnTopqocv1KpVCyEhIYiKikKrVq2QN29eLFq0KNl1KjfDypUr0ahRoyTa/G7duuHcuXNWoIvmzZsjb968Ns3gkiVLcOjQIfTo0cNKW7hwIRo2bIj8+fPb2rB58+YAgDVr1tiu06ZNm2Q1mbcD4+La4ZTPdevW4a+//kLXrl1t93jt2jU0a9YMiYmJlnWxRo0amDZtGkaMGIH169fj8uXLtnOVKFEC2bNnx8CBAzFx4kTs2rUr7W7uNsFxwunWUaNGDZQtW9bmQnjs2DH06dMHhQoVQnBwMEJCQixr808//XTL6pxacubMibVr1yIxMRGvv/462rZti19//RWDBw9GhQoVLLe+vXv3onPnzsibNy8yZ86MkJAQ1K9fH0DS+wwKCkqyxqBixYo297VVq1YhKioqybwjtdLkzJkzGDhwIEqUKIHg4GAEBwcjMjISZ8+eDeg29rVtAaBSpUqW9h247qpUqlQpn13+fJ1LbzX+boOUjNcrV67E/fffj5iYGEtmhwwZghMnTiSJrHnixAncd9992LhxI7777jubGzGvmy1bNrRu3dp23UqVKiFv3rxJ5tqKFSuiVKlSN91+/mTGjBlITExEYmIiFi1ahK5du+Lpp5/G+PHjrTK+tBnb+F//+pft/B06dEgz7xIyZcoUhIeHo1OnTgCAyMhIPPTQQ1i7di12796dpHzLli1tlhy+1zr7lTEGvXv3Rnx8PGbNmoUBAwbcsC4plYnkcK6b5hjIeYiWdud89NBDDyFr1qzWfJSSeSu1BOxHUJEiRRASEmL9e/XVVwFcb4zJkydj7969ePDBB5E7d27UqlXLtTGk2xQAy7x2/vz5G16f7h2+smDBAhhjfA5L+ffff7teI3/+/ACQ5OPO+SHCNG8fgSnBWZerV6/i1KlTKarjjciZMyfWrFmDsmXL4oUXXkDZsmVRsGBBDB8+HFevXgUAHD16FFevXkX27Nltzz8kJARbt261TTBu9fYXHFy3bNmCQ4cOYfv27ahbt26aXOvEiRM+tXNwcDAee+wxzJs3z/KbnTZtGvLly2e5ZwLX23DBggVJ2q9cuXIAcMvaMDWcPXsWJ06csO4dACIiIhAdHW0rd/ToUQDXJynnfY4aNQrGGCs09Jw5c9C1a1dMnjwZtWvXRo4cOdClSxdrrUZMTAzWrFmDSpUq4cUXX0S5cuWQP39+xMfHJ/lgCiRy5cqFiIgI7Nu374ZlKUPJyRnzr127hiZNmmDu3LkYMGAAVqxYgY0bN1o+576MnYFCtWrVMHDgQHz++ec4dOgQ+vXrh/3792P06NE4c+YM7rnnHmzYsAEjRozA6tWrkZiYiLlz5wJIep8RERFJgt2EhobiwoUL1t8nTpxwVZK4jd2dO3fG+PHj0atXLyxZsgQbN25EYmIiYmNj00Ube2tb4px/gett5sv9ufX5QMNfbeDreL1x40Y0adIEwPWIkN9//z0SExPx0ksvAUgqs7/++is2bNiA5s2bu4ZdPnr0KE6ePGmtoZH/jhw5EtDzBClbtiyqVauGatWqoVmzZvjggw/QpEkTDBgwACdPnvS5zTj+OftvcHCw6zP0F3v27MG3336Lli1bwhiDkydP4uTJk+jQoQMAuK4f8/W99tKlS5gzZw7KlStnfVDfiJTKhBtubcYxkO184sQJBAcHIzY21lYuKCjI9l7r67x1MwTsmqBvvvkGly5dsv6mxSQoKAg9e/ZEz549cebMGaxZswbx8fFo1aoVdu/ejYIFC/rl+ildcPnll19a2gZfyJ49Ow4fPpwk/dChQwCQxCfcbXHtkSNH/NZBnfebOXNmREdH+1RHvhw4Fwm7dZhKlSrh888/x7Vr17Bt2zZMmTIFQ4YMQVRUFJ577jlrwel3333n6rfq9EdNq4WxHFzdkPcrF6P7MkC4kTNnTp9loXv37njjjTcwe/ZsdOzYEfPnz8dzzz1na6tcuXKhYsWKluLAifzAANKuDVPD119/jatXr9r2LnCrH9tk3LhxyUaX4oSWK1cuvP3223j77bfx+++/Y/78+Rg0aBCOHTtmRU6rUKECZs+eDWMMtm/fjmnTpmHYsGEIDw/HoEGD/HyX/iFz5sxo1KgRFi1ahIMHD3od+zhOHD58OEm5Q4cOWe25Y8cObNu2DdOmTbP5o3NNXHolJCQE8fHxeOutt7Bjxw6sXLkShw4dwurVqy3rD4Cb2vMoZ86c2LhxY5J059j9zz//YOHChYiPj7fJ1sWLF9NsT6e0xNm2/iCQxiRfuJk28HW8nj17NkJCQrBw4ULbB/lXX33lelzt2rXx0EMPoWfPngCA999/37aWNFeuXMiZM2ey0SOjoqJsf6eXZ1KxYkUsWbIEv/76q89txvHx6NGjNu+cK1eu+E3R7MZHH30EYwy++OILfPHFF0nyp0+fjhEjRqQqUi+DHDVt2hT3338/Fi9ejOzZs3s9JqUy4QbbTL6bcgxkWs6cOXHlyhUcP37c9iFk/n+o8+rVq9vK32jeuhkC1hJUsWJF6wu/WrVqrl+CkZGRaNmyJQYPHowLFy6kuRtLcl/c586dw+LFi13N98lpvho1aoTly5dbGm0yY8YMREZGokaNGrZ0Z0S2vXv3YsOGDX7d6MqtjkuWLEkSFWTGjBmIjo62PhKKFi0KAEk2kfW2SWymTJlQuXJljB8/HuHh4di8eTMAoFWrVrhy5QqOHj1qe/78R+3Y7SS5++Wiv5TSqFEj66VMMmPGDERERNhe8suWLYuaNWti6tSpmDVrFi5evIju3bvbjmvVqhV27NiB4sWLu7ah8yMoUPj999/xn//8BzExMejdu7fXsnXr1kW2bNmwa9cu13usVq0asmTJkuS4woULo2/fvmjcuLElc5KgoCDcfffdeOutt5AtWzbXMoHE4MGDYYzB448/blMakcuXL2PBggW47777AFwPJiFJTEzETz/9ZLnK8EXHGWmO0fokKbGs30rcFAqAx8Utf/78KbpPX2nYsCFOnz6dZNxzjt1BQUEwxiS59uTJky2LeKDiS9umJb5aktISf7eBr+M1N++WL8Tnz59PElFW0rVrV8yePRtTp05Fly5dbPLVqlUrnDhxAlevXnW9bunSpVN0H4HC1q1bAVwPYuRrmzFIxZw5c2zpX3zxhc/bo6SUq1evYvr06ShevDhWrVqV5F///v1x+PBhLFq0KNXXqFy5MtasWYODBw+iQYMGN9yM3F8y8cknn9j+5hjI91XON8756Msvv8TZs2etfF/nLSD1Y0PAWoKSo3v37oiOjkbdunWRN29eHD58GK+99hqyZ8+OqlWrpum1S5YsibCwMHz88ccoVaoUsmbNigIFCuD777/HpUuX0LZt2yTHVKhQAStXrsTChQuRN29eREdHo1SpUnjllVewaNEiNGjQAC+//DKyZcuGjz/+GEuWLMHYsWOTfHEfPnwYDz74IHr27ImTJ09iyJAhiIiIwMCBA9PsfocOHYqlS5eiQYMGeOmll5AtWzZMnz4dK1aswDvvvGNFh6lbty7i4uLw7LPP4vz584iKisLnn3+OTZs22c735ZdfYtq0aWjbti3i4uJw9epVfPbZZzh//ry1uWyjRo3QpUsXPPLII+jbty/q1auHiIgIHDp0CGvXrkX16tUtzdbtokWLFsiRIwd69uyJYcOGITg4GNOmTbPCiqeU+Ph4yy98yJAhyJEjBz755BN8/fXXGD16dBLrYo8ePdC7d28cOnQIderUSTIwDRs2DMuWLUOdOnXwzDPPoHTp0rhw4QL279+Pb775BhMnTvSbxTS17Nixw/I3PnbsGNauXYupU6cic+bMmDdvXhIzuZPIyEiMGzcOXbt2xV9//YUOHTogd+7cOH78OLZt24bjx4/j/fffxz///IOGDRuic+fOKFOmDKKiopCYmIjFixfjwQcfBHDdD3rChAlo164dihUrBmMM5s6di5MnT1pyGajUrl0b77//Pp566ilUrVoVTz75JMqVK4fLly9jy5YtmDRpEsqXL4958+bhiSeewLhx45ApUyY0b97cirJTqFAh9OvXD8D1dXvFixfHoEGDYIxBjhw5sGDBAixbtizJtStUqAAAeOedd9C1a1eEhISgdOnSPmkL05KmTZuiYMGCaN26NcqUKYNr165h69atGDt2LCIjI/Hss88if/78yJ49O/r06YP4+HiEhITgk08+ualtB7p06YK33noLXbp0wauvvoqSJUvim2++wZIlS2zloqOjce+99+KNN95Arly5ULRoUaxZswZTpkwJqE1n3fClbdOSChUqYO7cuXj//fdRtWpVZMqUKVmLfVrh7zbwdbxu2bIl3nzzTXTu3BlPPPEETpw4gTFjxtxwT7cOHTogIiICHTp0sCKRZcmSBZ06dcInn3yCFi1a4Nlnn0WNGjUQEhKCgwcPYtWqVWjbti0eeOCBm2mqNIfzCHDddWru3LlYtmwZHnjgAcTFxfnc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GjWq2vb2229Lkj72sY9Jkv79739XZUsuuaQk6Vvf+lafY+XnPCHtJadb2lwbGc66m2aaaSRJq6yySrXtxRdflFQf64aCj370o5JSe11++eWrsj/84Q+SpLvvvntAx4x2N+FE3U04UXcTzmDMN054goIgCIIgCIIg6Ck+9N/Bfq0aBDp54/3whz9cfXYLpiStuOKK1efRo0dLSpZQSXrrrbckJY/Q5z//+Qk6z/fff7/6/NJLL0mSLr/88tr/knTuuef2+S7X2FT9bbAW4LmQUl1z7Z/4xCeqMqyEiy222LCc10jX3aOPPiqp3rbuu+8+SclaiddRkv70pz9Jkl599VVJ0lNPPVWVbbHFFpKknXfeWZK00EILVWXPPvusJGmJJZYYtHMf6borMd1000mS9thjD0nSDDPMUJV9+tOfliR96UtfklSvO+rnC1/4giRpzJgxVRketsFkuDxBTV4fh3Zx/PHHS5Ief/zxqmyKKaaQJH32s5+VJM0000xV2d///ndJ0gYbbCCp3lZzfCzmfAZaD93Y5trCcNQdcyVz51xzzVWV/f73v5dU73cDjc7oBH5zyimnlJTGSilFbDz99NN9vtfUV6LdTThRdxNO1N2EE56gIAiCIAiCIAiCiSBegoIgCIIgCIIg6ClaFw7XFEb285//XJK04YYbVtsI63ARg3fffVdScqe7W32jjTaSJP31r3+VVBdGeOedd2rf5/ckabfddqvt87e//a0qIyxsqaWW6ve6SrTBZUqIlySdddZZkqRTTz1VUl00YeONN5ZUT8weSka67i677DJJ0vrrr19to0198pOflCSdd955VRmCEbPOOqskaZ999qnKCDOkLbswwgUXXCBJ2nbbbfucQychlyVGuu5grbXWqj4fccQRktK5XXPNNVUZYTgIlyyzzDJVGfX52muvSaqHaL733nuSpAMOOGDQznmow+GaQnvmm28+SSlsUpKWXnppSUlUw8+PuiC8iMRzSTrppJMkpfGQupKkK664ova3dD1tbXNtZDjqjpBJBAjWXHPNPvvcdNNN1WeESDoN28wpfS/fxjlJSbDBKYXG5US7m3Ci7iacqLsJJ8LhgiAIgiAIgiAIJoKulMhuomRp/NrXviZJ2mSTTSTVRQnYHxljSZp88sklJS/PVFNNVZWdccYZklKStSfy4zF6/vnna78rSbfccouklFzsViqSsn/yk59U27bZZpsOrrb7QWZXSpKleII8Wf+BBx6QlBLcscy3jU5lm9nvL3/5S7UN8QOknL/+9a9XZVjsOSaJyFJqd5S5BPSMM84oKVnxXQqa/Ts9525jjjnmqD5zfY888oik+nXg7cHb+MYbb1RliFBQ997vXn755aE47UHH719uUcf7KiVrOB5DSRo3bpykJN/OuCalRPOZZ55ZUl3AZZZZZpGUxrrPfe5zVRneIbzfBx54YFV2zz339DnnNrW5oD6+AB4g8OiJ5557TlJZhr7kASodP9+/9L18my8jQKSHeywn1AsVBEHvEJ6gIAiCIAiCIAh6itblBJVgsTasleTsSEm+1S+T42Mp8jwBvEhYtTzunUUusSo//PDDVRl5Gkgiu/wuv+NeJZf47Y82xI3uuuuu1Wcsy3h9WNROSpbDe++9V1LdYjcUDHbdlRZEBfJ/Fl988WobnkDPRaMdcG5+LGSeOQe3vFKPlLkkPG0Xa6fHwe+5556SpLFjx/Z7XSW6sd1de+21kqTbbrtNUr3P4oG88cYbJUmrrrpqVfbjH/9YUvIkffvb367KyMN64oknBu08h0si+/TTT5dU9+zglfVj5mMdcsZS8lDjlf3d735XlU077bSSUh7bpz71qT7f4x54LtF2220nqe4B7YRubHNtYSTqzvNuiYLwtjUceLRF7qnqlGh3E85g1R3jNe3HPYW5N8+9eqV5lLmxFAVRehbMf4d5vmkJFv8+XvfSNiKOeObp7xidEu3u/4icoCAIgiAIgiAIgokgXoKCIAiCIAiCIOgpWieMAIQP+WdC2Dw8A1epu1hxp+G2dHcn4SXsj9iClOSgkb8mZERKicQPPvhgn9+bffbZJUlf/OIXq23IZQ80VKnbWGCBBarPSy65pCRpueWWk5RCaaQkHX7dddcN49kNHqUwOGSaV199dUn1hPymcCDahrcR5J1xx7sMtie65xAGQpiAS5Aj8jH//PP3+/22QN+mflxkhHCK2WabTVJdqpd+ucEGG0iS5plnnqpsMMPghguSwRlLPOyWscvDBQg7JVTurbfeqspoY3PPPXef3yH0gtA3D22lL9BmPSRvvfXWkyRdeOGFA7yyoE14OOqyyy4rqT4nM0cSslYKL2Ls8vZDSBRzOKIn/psXXXSRpLpwCmHoCKf47xAi7MtWBCOLLz2y2mqrSUptxOdaxp9SKFhJJIvvlsLh8jKHuZg24+fAMUrnwHzkYXqMsYyZzzzzTFU23CGjwwHPgI8++ugEfX+kRXTCExQEQRAEQRAEQU/RWk/QpptuWn3G2vT+++9LSotRSn0t5U7+9u/7kQjsFn0sn1ic33zzzaoMLxHeEM7FcWsqggJt9wS52APWEywhU0wxRVWGhbjtuKdlpZVWkpTENNy6RZtyaXYoWTty0QPamm9rknylzC39yLX74qFIRreNSy65RJK09dZbS6rfB4RK8HjhjZWkY445RpJ06KGH1o7TVqaeempJyXvjnlW8re4dok9SRz42sq3kLXdBD6k+RuLxXGihhfp8jzYXtA/uIwswu9AL4gf0MZfIRqbdPat4cOiLvpgpHiPaGDLuUvLo4LXxcRC5duZRpNql1D7d47TiiitKSn3kpz/9af8XHwwrpUgHxiO/hz6fSc1zp9RXxMCPVfIO5d9jbvXjMIeXvp+XSal95sdsM/kSHP5cw9iAsA6LdkvlCJqc0j1l2Qaf532B9MEkPEFBEARBEARBEPQUrfUEbb755v2WebwyFiV/I22KO8wtCe69Qdaat2CXueYzCzB6vtCUU04pKS2SKSVP1ujRo/s9l24Gi7RbgYl9RRKSt3kpvdFjwX7qqaeG4zQHnW233bb6TJtya1OOt7t8v1IsLFajkrWpE6uKW2g45sorr1xta6snCC/bZJNNJqneL5GxP+ywwyTVF5odP368pJQL1JRf1QbwUBMr71ZGFq0sSa/jAWIskpLHCO+Ne39YQJrxzGPZ6cNYc11am/MK2gdtCTn6m2++uSrDI0PUhefXYK33bbQDPEa+SPHjjz8uKS1t4REDwPc8X4g+j5eJ85RSHqovfE6Owq9+9at+rjgYKUreGOZHz+lm3GJMK0lkl+Y88GPlz3b+7FLKBeoErsPHTq6DOcsjRCYV8NhK0kMPPSQpeXvvuOOOqoz6IUrBpeyJpGJJFSlFUq277rqSkvdXCk9QEARBEARBEATBoBAvQUEQBEEQBEEQ9BStDYdbY401qs/IwELJneruylwq0d2kuO8QRHD5WNz+HMslkZGgxR3vbj9cuCSVSfWEzzaC23LOOeestt15552SkvvXQ2ioD77X1nA4T/ymHZXED0ohck0rT3/Qvv3BMfg9/x6fEXCQpO9///sdHbfbePjhhyUl97jfh8997nOSpKuuukpSkqSXpCuvvFJSam8HH3zw0J/sEELoEKG1nmD85JNPSkorlUtJph4xF8Yph/HSRWDycfD++++vytZff31J0mOPPSapHppImGzQXn7+859Lkvbdd99q25e//GVJaX775S9/WZXRRjxEJp+TCZGWUvgk4XOEeErSu+++KymFtCMJL6W+T6jcwgsvXJUxJ3v4DKI9TeHKbSVPVC9BiKCUQpZ47vBQsJFI3C+ddy4o4J8Zj/y88328vGlOzkPPvQyaBBi8jG0+BubXUzrntpGHLyJrLqUQa+qzFN5aCpPm2XyrrbaqtnnKiFQPlRsq2n93giAIgiAIgiAIBkDrPEEkPWMxkpK1s2Qd4W2808WveMvnr1u0sLCSUOzenCWWWEJSssaSCCZJSy+9dJ9t0003nSTp/PPPl1RPuG8DJMi6JRoPEFYVf/vHOuBW6jbiSfdYeLBolLx7TVagwbTAcSy3aHE+eErazAorrCApCY74wsMs1njCCSdIkvbff/+qDKszlkFkWNsKXi68rN6fSosBMibSPjx5nbaJFd0t8niMSEJ37xpiCXzPrXeeyN4L0LdcJIf5B6GKJq9uaRHRkWauueaSVPcaHn/88ZL6LgsgJS+/CxJxXSxWfPjhh1dlRx99tKQkn73DDjtUZXgckdt2CzseJxKwve3TPn1/IhPa4Alq8syUBHRKnhSs8xtuuKGkuiAOdXzXXXf1OWbJwwEsuH3QQQdV23xpkAmlFH1D+3HxmnxZiFJdeN15FJBUf+6jbZTmXY5bkrxuqh9+z3+H86HdlZ4z20a+ZILL03PtzB9evzynU/deF6X5g2d55pHhEDUJT1AQBEEQBEEQBD1FvAQFQRAEQRAEQdBTtC4c7rjjjpNUT74i5AUXsbstCdHyMJjcren/N+nM46Yl+ddd76xHQgK2J3sRMub7k8DpychtAkEEd2USkkCYAiFzUrpHJNgSXtE2PPyPe01oo5dxX0tu/xJNIglN388TRj30g/7gK783ufa7GRKkCZPxcBASpRFPuO2226oywlTpex7G00ZYe4vr9/AztpVCaygrJR3TT1955ZWqjCT3kpAC4yChiR6anIfE5r85qbDQQgtJkl577TVJ0rhx46oyQuQIK6PNlijVjc8ThJM9/fTTE3nGncO1ISwgpf7H+H366adXZYQx+XlzvvS3q6++uiojTI125CIL7E+4p4e9wvvvvy9JuvTSS6tttMFFFlmk2rbiiitKSm3xpz/9aX+XPOJ4f8nDtUpj9fbbby9JWmCBBaptW265paQkWOJ9F4EnwuE+qE+yTgt1yHpjg4VfYx7y1t9+UuehjRzT22Qn3yUczudaxs7S99nPnxPZjzCxXCSkLZRCD0kHYByQ0rMvbcTXjaNe+Ov3g7nC53JC6piTQhghCIIgCIIgCIJgkGmdJwiJX6y/krTZZptJqq+GDqWVgJus4XlZaR8sWJ5wzkr2eEOwSkvJEvDDH/6w2nbuuecWrq49zDfffJLq14lVuiSMgLXArQRtAi+PWzSvv/56SdLyyy8vqS4pi+fRrfJN3pdOkif5vu9L+8Y7yTlJ0q9//WtJyZospTbr8uVtgGRcLJ++CveMM84oKclfr7nmmlUZq9Kzv9cd1iaXs+926GNYw30MKsm0YkklCbU0DiKWwDH9d7DMuTWTcZYkVk+I53f8/rTV290E1nb636KLLlqV7brrrpLSMg4umpCz3nrrVZ85xhVXXFFtW2eddSQNr7Q9Xh+/53h2uPcuAkM/cjlr2gRjj3ujL7nkEklpfvBlFmh3iA8hgiBJr7/+uqQUabD22mtXZQh4uOWYZwTkobuFJk9tiVVXXbX6vN9++0mSVlllFUn1CJd77rlHUrpHLnRyxBFHSEr3qhSJ4d6M3XffXVIS99hzzz2rsj322KPfc+0U92AjLMT44/LW9C/qyedQxjsffxjLctEiKbXZJkGikteHY3AuPraxP2OhlMZVrtGl41lmpQ2Unldof+79pw5oi6X6Zd4peZd8TqJueU4cjrk5PEFBEARBEARBEPQUrfMEEU/MX0nacccda/sQiy2lvA23LmCtLMV4TmiuBLH6xEW65LXHi08qlGJhuXYsAi51idWvlGPQBrDq+PWSD0XMv7cdrCFu9ZtQqcwmyxXHxBqDlVVKVpXLL7+82ta2XCBAZp4+e9FFF1VlWKzXWmut2j5SsrzR/z22HQtimzxBgMXTF43lGl0GGw81bdXbIO2DNuFjZL6oYlPMu1ti2Ua+kJSsySMN593JQsa+rbQQMZ4NrOJ4I6XklaVduWcS7yy5Lz6PYdn2+jrqqKM6uLLBBelq9zLgCaf/jB07tiqjj3k/wpNDZASeMyl5aKhPlpWQpEceeaT2O245x3uNV9jHVrwl/K6Uxgw8VCeeeGLjdXdK0xIb0JTzUhrPR40aVX0mnxhZa/cyPPPMM5KkAw88UJJ02WWXVWXkBO29996S6gvHcg5bb721pLrEMR4j99hiiccT99WvfrUqGwxPkHsScy+9z1H0Pcaf0pIT7kXjWNSZ5yzz3dIx2Max/Nkl/x2Xi2abe4cYR+eZZ55+j9WNNEVIsfAuEvnetsjPpz/7fEBdMC943VPm94/9aHclL99gE56gIAiCIAiCIAh6ingJCoIgCIIgCIKgp2hdOFwnEr8fJCeKi7W0CnF/v1f6zXwVXSm5/0vn4CElecJf28D9665e5GJdEAHYr63XS7jFLrvsUm3baaedJCUXr6+k3bTy9GDCb+P232677aoyQpLmmGOOatsBBxwgSTryyCMH/VyGEpJ9EeJAqlNKydelFboJHyHEx6U9PVSimyGkTUrhAYQNeBlS7S5wkAsieHgBfbFJ/pbv++/AVFNNJake4sq45nLx3QLnNhiS3YSDIWLgCdg33HCDpBTq5knEtFES+F20hPHjhRde6HPOpZC8oWLzzTev/aaUQlZWX311SdKNN95YlSF6Q8iMJN13332SkuiBH4vwK9qPC3oQZkiIjbcjQnA4F59naIP3339/te26666TNPh11iSaVCKf81ZeeeXq89FHHy1Jevnll6tthAKNGTNGUn2ORfwAyWsXzFhhhRUkpf7vbZJnD0QlSiFhfh8YXwlb8+T+TmWqm0BYSUrjdWmMoYw68DA6ztfHL+Zdrn2gzxulZ0JCV6lXP0/Ckb2uad+MuZtssklVduaZZw7ofEr4ueXPFJ22Sb7n9ZN/1yXyTzrpJEmpr3toGmGzyNl7WDX3iD7roW/0SxfwoI4Jx1x//fWrMg+BH0zCExQEQRAEQRAEQU/ROk9Q6U23abEt8Dfm/G2/ZK1vWpiySa6xSfLSpQDbmqAO/x977xlvSVGt/z+YcwQVlSAZhpxhyEEEZpAo0QACV7iAksMHwYsJDKgXEIUP6ACK45WcZYBBcpA4RCVIEFEw5/h/8ft/q56uU9Ozz5l9ztn77PV9M3u6+vTurl5V3Xs9a63i/D0ZFnWIwhR4DXw/T1ztJ7i/7vXGa+GLwpa0LYLXDbgPeOwp1y1lZc490aOVXDja4G1iXJF8LuWy2XiPrrvuutS23XbbScqeRPcsU5LYk9N7ES/0whxCP7jqg9fdxyRFO2pqT7l8gCf3YqtlYqs0dHFM9yrz2Y812nRa6ACb4V9XLujHs88+e8hx25QEinG4F52EfFTZyy67bMg5kJhOMrsknXvuubP9nrFccBY1xdWJF198UVK+916KmqgHT6xnG0qXzzvYFnbq9so4pziER0+4CiDlokdStu+tt946bXvuueckZYVt5syZs7niuYf7j/ruHuxFF11UkrTGGmtIaqp/LOz8wAMPpG2o16glXgZ/p512kpTHuvcrz6bagsUUCsCOXEVjLvHnE/eE4/v7kJe3Hinf+9730ufvfOc7kvJcg1olZZUAu/NnLfYwbdq0tI3nIPv5exht9EGtVDl/58/Mgw8+WFK+Hyy74OfgxUzoa+zTC4y4ujJS/N1xpO+RbX+3++67S5L222+/tA2bpD9dveE9gz50VcwjD6Tmc6QWDcUcwn3wOTqUoCAIgiAIgiAIgi7Qd0pQjU5+DbcpO904Fr9qa3Gttb/rdyUIz5uX78Q7ddZZZ0mSttlmm9TGQne9mCvQCXi/iHuVshcID1atFKZ7cEcjJ6g8pucl8d1ua+5V7HU8trj0KPkCi3h48WCzeLKUPViU2vWS0n4vexlfbLNtYTruM7lBUlY4KC/s3k+87Hh7a4sUcsyaoumLaZZ/1428gU5pU0ncK7n++utLynkVvmC1z2OdHBfIUWNRU/+7tddeW5K06aabpraPfvSjkrJS0Kb+jBfYg3vdV1xxRUm5RK6rp6gGviAqygiKQ23BUvJM3FYoKww+7pnbsDtXN5kPbrjhhrQNJatmp3MDXvBrr702bWN+qS00y9hDEfRxRv6Pl1hnXmKu9jmLvuZ56gok96uWf8s5M55dQWJOcSWPdo7heWrkco0ElFDPZeI+clyuTcrjl/cqf96hTrJItpTzPOnDWvls5j3/HlccpWZfeESL1Czbjo35/vQ198bnBldCxhPGniuW5PvxruMKG9EIPD/c7mij9L2/B7E/Kpz/HZ/dFstcc3/OjxahBAVBEARBEARBMFDEj6AgCIIgCIIgCAaKCREOVxY4QJKWshRdW8G57VhtoUt8T22fTo49EUBKJtxIyqEMrGLtMjDJ6p7Q3k/UEsuxKe65h3Ug/440LKittHZtRe22cu+eaNpWxKHX8FAmpHbCYyiZLUkLL7ywpBw2duaZZ6Y2wneQ7D28hhCfCy+8sMtn3l28yADhFNiX9wMhBB6aQegNoQceqkAICH3ifVMWBfD5sxbaUDKWc10txIJwQQ9zoyjLxhtvLCnbjZTthCRoSbr99ttn+538LUU5SHD3z3z3xRdfnNoIieI8a8ssjBeUpSZ52xPAOU/GoYd7sY1SwlKz2IHUnD+XW265RpvPkYRdlYn5Up5neaZ72Wag+ICUnzXsf/PNNw/ZfyTwXJs0aVLaRhjW008/PdvzLgu4+N/5HE1SPvejVnyoDA+W8nUSAubvInxP7XnE/h6mx/1i3C+yyCKprRY62imETG622WZpGyXlOUcfz5wb/enh3tgdy5JI+fruv/9+Sc0+wIbpO5+jmOe4brdvinzw97UCR7X3A+ZhiqBIzXDNkXLAAQekz/RnWTTEz5P76uMF2/IwwFmzZknKNlwrxU1opocI0neEdPrfleFzbmM8y/wcsG/aKCoymoQSFARBEARBEATBQDEhlKCyRPZKK62U2khSc+9ouX83CyS00e/FEBw8JV4aEi/zQw89JKnptaGv/D70E65KQNv9b1MEuwlemJqCiTfFvf/9VBjBFzPF43XppZdKks4///zURmnYyZMnS5KmTJmS2lgwkSR1L1HqZX57Gbc9PKN4z9xryvxCmV4pqx+0uReTz9iHz09l0QSH4iacl49pbK5NJeoWFF7B6ytlLzseYfew47knAdv74pZbbpHUPO/amAeSo1F53BuNIvmjH/1IUlP9ps2fUeW5+/6cAwouifR+zt0C26LogS/4zfhjEc1a8rqrzHjNsT9XiRiveOQ90Z7+Ry1yxYPvYdFUV0h45ngCf6kqdYuf/OQnkqQrr7wybcMLTlGS2vzPOHNVhfnbPeR4w+kLt0lslnHmfc6Y5XvcfnkuoAbUytr7s6MsloBaI815Mfo2UFB9zuCZxPm60sQ10Se1QkNuixSCQRnxwgjlIrfeRn9yfv7uUha78uIGNUWXuYd/fT7GdkYC6o2X1UcNQ7X1JUiwqbIghJTnL1cGFpoPnwAAIABJREFUuQ+oWl6Qg/7k+K7mlu90bq/YFuPYz4G2WgEatvl4Hi1CCQqCIAiCIAiCYKCYEEpQiS8EVuZMSEPLITpt6lB5TP/134nncyLlBOEZcC8eHje8Mb6gFvu19X0vU7u/pa10en/bFMG2BXxrf8854GlxjyK456qfcoIc+h9PtC8SSiwznvepU6emNrzr/L3nsLWVs+8lPJa7XITZ+4E2V9C43yi3rgQyhimJijddymoP47Vmc7TVbG4s+pZFSQ866KC0Dc8h95uF/KTswUYdJIdCynPX+973vrQNW2O+975jwUsWRPT8kK222kpS9sh7vH5ZTtbh/vn3UIIX77gvMeD3vhugZmyxxRaSpKWWWiq1rbzyypLytXieGl5h9xzTt/SLKzrkjqIy+txaqjeea1Uur+Dnh8LmHmpKKJO/0K2cIMqi77HHHmkb95Wxuvjii6c2VEIWZ/Y8B1f2oMz78/vMOGafWiQGY9UVFeZBPPm+6Dc2jMImZbvrdvQKc4V/P+8JKEGuUmE3NWUNBcJVRhY0pV9c6cBmsU1XielPzs/VCZScWi4a98ZVN46FcsPzSZKOP/54SdLRRx895HrmBIvK+ljC7rAxVz3p4zIXSqqPPcZXqURK7WoY79s1RYd3l1oOWy0CgX7kvvm7/GgRSlAQBEEQBEEQBANF/AgKgiAIgiAIgmCgmJDhcF76tC1BnTaX4zpJaG9bFb0W5gATKRyO0A0v0VuG6ngIItJqrbxkP1C71+U2D/VrWxm6rWw2NuJ2WJbidvieMmHeP/t96adwRA+npLQoCeV33nlnatt1110lSZdccokk6fTTT09thKzUwmtKe+1VPBSBUDdswfuI6/HwGeY27nutfDYhCJ7AWxbc8NDfsvRqzdY9XGq0OOywwyTlkBAph3RttNFGkqTVV189tbH6+Ec+8hFJOYldyvOT911ZqODBBx9MbYSsEeLkoUcULKAPPeymLMDg/cp98BLKfE9tfx8D3YBwH4qO3H333amNZH5CCm+99dbUVivPS8gYdurPReY//s6XtHjuueck5fA7DzOcd955JUnXXHONpBz+KuXQH8r7Sjnkh7C4buNhpxTnYLx5AjwhbIwJwqR8fx8vnC/32vuXkKXac6Isqe1FTcpS+W7nhBL6faD/sT9/Zs1NQZmvfOUrkqRrr702bcM2KHDg8x3fW3v2Ma48rJXrIvyK8Dgph7DyPX5M7JpwwVoBBvDQLi86BMy1/F1ZEn6knHLKKZKkQw45JG0jlJlz8pBR7mutKE5tG8+P2vsJc3wtZBGbwn58jiL8nLLv/nf0ne9P+Dbn5c+3zTfffMh5dYNQgoIgCIIgCIIgGCgmhBJUqgt4jKT8i9d/gZbJ57VFodoWRGVbbXFMPDpeBpOEw35VQWrw698THH2BMam+yGMtea4fqCVMlgszuo3hMXFPUak8+rFKe3N1sizt2bYYmXtOah7asSrd3Q3cQ7TeeutJymPJxx52d/jhh0tqlkwlmRR1eK+99kpt06dPH4Wz7j6u3lG+FI+nl1MmURyvm5RtBQ8htiplW6C/fH7Cy4p3sBzbUj3ZtVyAdSzwkr189pK+E53vfve7XTkOi8iiblEYQspl50lCX3fddVMbYxH1TcoeeErSuwLLcVEA/HnNNjzcnnCO2saxvcgL3vZaJAbH6ha1ObpUBDxZH5ijXUFifNVK3ZcFA6Q8jtuWmqidH33FuPbFPdnmx0TZYi7xOaUbuK0ce+yxkppFloBr4fnr6hZ94c8JijxQpMOVQZTN8phSVpPoa1cgH3jgAUm5D/25Sj/6OaCe+VzbDYh04F9JWn/99SVJ22+//ZDzZi5mfPmczLX7nI+91SJPsCUKTtDPknTfffdJyv1EUQ1JmjZtmqQ85t32Ob5/T1nIrG2R5W7RP29EQRAEQRAEQRAEXWBCKkG+cB0KRG2RwOFSLrJaKx/Jr20/B7zXEyknCM+Ve1PKUrke29vvOUF4VVy94V7jAfF4XOKPPVegtBvPSSkXrOu0NCmeEmzMS7PWShf3kw26F4jyzZQn9X4ljru22CTg6fP+cQ90L+PjiOvHG+kKDTbqaiCeU+zLbQL7RbGteXv5bs9rKRdQ9Zwg7pmXkQ36A1QUbMrnOvKhvve97zX2lXIejysW7E9+ipcJJueFOcvHK4tUss29yuClsQHb9cWQ8cS7MtwNRlo22tWCEp/PoLZQcVt+E/ehhueZjSflO5SUc7v2228/Sc37xfMK5bvWhz4/Mqdji7VctFokBs8FIng83wwFCIXHlSDukT/7ubYjjzxyyLl2GxRWV1pLyH300uwoM/6M5R2N9xJXN8llHG4e4ic/+UlJ0gknnCCpqdrT/64Oce9RUl1NIw+RY3WLUIKCIAiCIAiCIBgo4kdQEARBEARBEAQDRd+Gw3lYTylPT548OX1GPvV9hhMS1FYYoZbQhaTooQEj+d5eh5XYKUkrDV392mXj1VZbTVJOuOw3CCNyOyIMiFAGDwGqJa6W5T49Gba0jVo5S+Rjl6mR6ElU9ORHEk39WG3luXuNWjgIq757f22yySaSchKmy/5PPvmkpGynHh5RC0HpRXwuwQb4txaa4QVJyhW3a2Xc2cfDA7ET+sjLZ2OPhIl4OArn5WEWQX9AiVvmcZ+faKNownXXXZfasBuf/7AN5iAP4+JZwP6ezF0mtnuI1/PPPy9JuvfeeyU1w5kIPyc0WcqhNP0agj0Rqd0LbImwrQ033DC1MQ/xr4fsMt/585CwubXXXluStNVWW6U2/rYsPOHHuPHGGyU151DCLwkT83A9zsuLdFC6v225lLmhVhipLUWDdzX+lZrhfiPB5waeI5yLh0fTn/5O3muEEhQEQRAEQRAEwUDRt0qQqzBl2WVPpsKLOlwPOL+228pnO+U5eGEEGGlCZS9C//t1urdYygnr0tDyi/0GHnG3LdQhEonxVErSbrvtJqm5kBs2Qh/UbKumFpZJ7a4MYNckRrLIne/vikc/lSh3bxyeJBZN9YUc8UpRLtTVDryLJ554oqRmYqYrGL2M2wS2g135vIb66B4/PPFlOXentpAdXs+y8IaUvfVrrLGGpHpxFPcUBv0BqiLzGIvL+jbuvauGRFv4vIQdYG9ewINjUW7XbQWVB0XbvfWoUezvKujyyy8vqZlkXT6PfIHXYHxoe4c655xzJDXL26+wwgqSsi16gZbawt9su/TSSyU17YdiG8yZbisU8MDuXF0qow+ILpDyori+rbxWpxvvgH6M8Xqe1wpU9Ou7XShBQRAEQRAEQRAMFPEjKAiCIAiCIAiCgWJChcOxMrQnh9Pm29qSyNr2YVstSR5I1tt2223Tti9/+cuNc/Hz71cJkT7wkIPyWrx/CEti/Yh+oxbOh/SOzE44liTddtttkqS99torbWMtgfnmm09SM9yrrWgGNkWYk0vve+65p6SchPm///u/qc3lfkD27wc8vGb69OmScp/76tGEZBKa4GOPpOujjjpKkrTAAguktlo4RS/iYT8UfSABmGuW6mtFkCjO+PMkdBJY6WcPcXrrW98qSTrrrLMkNedbEoMJUfR5jdDLfgk1DDI8I2uh44RVUojFQ9gorOFrcAH25s/fDTbYQJK09NJLS8q2JklLLrmkpDxXeqEDwucIi/OQHOZE/l7KdlpbaygYHzoJB/Ow8h/+8IeNf/uJiZT+MJEJJSgIgiAIgiAIgoGib5WgmuecpDn3UpEs7N7KTpLJ2hQaLwEIeErxTnli3XCP3w/Qr65mlCWH3YPNfl4ooJ/Apvy+ca9RdpyTTz658a+Dxx3vqpS9qXhAPZEYL6d7yGaHJ77jifLE9V5ZObwTFl544fSZEtckyLpKRBnO1VdfXVLzGjfffHNJuTCCl4y+5557RuGsu8/tt9+ePq+11lqSskfevdxf+9rXJGVFXMr9xrW6Okg/UQTB5zXG93333TfkfLBHPP877LDDkGN6GdmgP2C+rkVPoDQzv7hnftlll5XUVJCwEVZ+97FGcRPmQVcg+R6eJTVViqR3n4tPOeUUSdKMGTPSNuZQ1Kh+VBOCIBhdQgkKgiAIgiAIgmCgmFBKEJ51vOlS9iTVFi/tJig/KB7rrbde6/5tpSL7AcqNHnLIIWlbuTjYCy+8kD6zMN73v//9MTi77sP5e8lNFJbhLopGv3j/dAtfYJTyxv1qY77AIiovCoMvzMgYJzfIvc6XXXaZpOwNfv/735/a2HbCCSd0/dy7iedx3XTTTZJy+XlfPBC8NL1/lrpbJphzYfFLKStBjz76aNe+JxgbUBDLsupSVr0pRe3MmjWr8e9Y4XMASxK4akp0Rm3R5SAIAimUoCAIgiAIgiAIBoz4ERQEQRAEQRAEwUDRt+FwteIElIilJLWUJX2X9pHJOYaHCxFaU5PSy/094ZxwOPafUxJmv4YoAaEwDz30UNr2rW99q7HPzJkz02cS2n/84x+P/smNApRoroWb+XW2gU1hYx7SWRbrcNsiAbhmryV33nln+rzyyis3/l7qrLhCr0BJXEmaNm2apFyE4umnn05tlF0nQX+TTTZJbRRNINHfQ+xqq3z3Itddd136TGER7OWJJ55o/VtsDNvzUtflHOo2x9/V5lnsie/2Fd5JrvfSxkF/cNVVV0nKZdi9hDxjsVaGHdy2Oin84/sPB47thXeYgz00mXYv8BAEQeCEEhQEQRAEQRAEwUAxz3/6XZIIgiAIgiAIgiAYBqEEBUEQBEEQBEEwUMSPoCAIgiAIgiAIBor4ERQEQRAEQRAEwUARP4KCIAiCIAiCIBgo4kdQEARBEARBEAQDRfwICoIgCIIgCIJgoIgfQUEQBEEQBEEQDBTxIygIgiAIgiAIgoEifgQFQRAEQRAEQTBQxI+gIAiCIAiCIAgGivgRFARBEARBEATBQBE/goIgCIIgCIIgGCheNt4nUGOeeeYZ1j7/+c9/JEkvf/nLJUn/+Mc/Ovqej3zkI5KkhRZaKG2bMWOGJOlPf/qTJOllL8tdtPbaaze+76STTuroe+ClL31p+vyvf/1rjvvzPcOhk74bLs8991z6/LrXvU6S9Ic//EGS9Pe//33Id3Pe//znP4cca9FFF5UkfelLX0rbDj300C6fce/0XY2pU6dKki655JKO9l9wwQUlSZMmTZIkXXHFFR39XXk/OqUX+45xuMsuu0iSVl111dT22te+VlK2RWxUymPu5ptvliSdeeaZqe3Pf/5z189zuH033H57yUv+n9/q3//+d0f7v+Md75AkrbTSSpKkxRdfPLX97Gc/kyS95S1vkdQcy9/5zneGdV5zSy/aXL/Qrb7DtmrzBttqz63XvOY1kqSPf/zjadtOO+0kSfr2t78tKY8/SXrxxRclSb///e8lSW94wxtS2yqrrCIpP5v/8pe/pLYvfOELkqRbb711ttflz1hgrPg1sy3sbuRE342c6LuRM5K+a2Oe/3T7iF2gmy8Gr3rVqyRJe+21V9q2//77S8oPf59o3/nOd0qSfv7zn0tqTtB8fvzxxyU1b8a3vvUtSdIZZ5whSfrFL34xrGuoMd4DZf7555eU+0KSfvnLX0rK/Vr7ocP9+Nvf/pa2cS30+a9//esh39NNxqLveODWXgz4wYKtSdKKK67YaONlQMov5H/84x8lSQsvvHBqox/5IeAv76eddpok6Ytf/KKk+hioOQzaGG+7g7PPPjt95kfPq1/9aknSG9/4xtTGfbjjjjskSRtttFFqo49f+cpXSmo6SK699lpJ0vbbb9+1cx7tH0GdcPHFF6fP/OAG/6HDvEffMKal3E8nnHCCJOmTn/zksM6hX22uH+lW37GN8eT7YA/MQW5jyy67rKTsOJSkV7ziFZKkt73tbZKyo8Jh3nRHI9ueeOIJSXk+lKQ3velNkqQnn3xSkrTPPvuktocfflhSfvaUn/3YUu6zsLuRE303cqLvRk63f7JEOFwQBEEQBEEQBANF/AgKgiAIgiAIgmCg6LtwONpq8b3I8h7PzuV5nkAZD+whIoR3ka+x1lprpTZC5fh7YqH9fPj7v/71r6ntrLPOktTMg+mE8ZZMjz76aEnSpz/96bSNPALCGzzEgM/kZvn50y+EKJCr0O1zhtHqu7a8Lg/fmjJlypB9CBehLzwX7frrr5eUw9tOPfXU1EY4HHlYhIRJ0utf/3pJOUSOsDhJ+spXvjLk/DvJExpvu1tzzTUlSZdeemnaRl4aY4/QGN9GKCr5BFIO9QK/NkJ1ttxyS0nSNddcM9fnPtbhcD4HYV/PP/982vb0009Lkq688kpJ0gsvvJDaCNmlbz0EkxDBddZZR1IznKns01roW4TDjR3dDoertTHGTjnlFEnS5ptvntqeffbZIedBqGWZr+ufsSkPoyPcGhvzuQ6webd9wjbPPffctK0WAlteT9jdyIm+GznRdyMnwuGCIAiCIAiCIAjmgr5Tgkii9IT8ZZZZRlJWgA455JDUhmfcPZl4gcp/paxU4EXy7kHdqXmY8PiTsL3vvvumtkUWWURSThaVpC9/+cuS2pPrx9tbgIL1wQ9+MG175plnJGUPnd+H0gtcqy5En7373e9ObVQEuuuuu7p27qPVd34PURD/53/+R5J05JFHprbHHntMUjPpl3vdVrwARceVy1JFc+hPktopPCFlL/59992XtnVSXWy87Y4qUB/60IfStl/96leS6uePooYqhvoh5f7n71yhRU3Ce+yJ1iNlrJQg7vfVV1+dtqGgzZo1K23jM5W4XEF761vf2jiW2wl2zj5bbLFFavva174mSTrqqKOGXEMoQWPPaFWHqz2TbrvtNkl1Rdz3L+d7H6/lNj8Wdse49WOyP/OuFy1CTfKiKOX1+DlEYYS5Z7z7rlYNmHctom6InpDycxSb8ucphTg++9nPzvF7hzu31RjvvutnQgkKgiAIgiAIgiCYC3pynaA2aiWZ8eRuuOGGkprll3/7299Kkuadd960DSWHXIvf/e53qQ2vAm3upcJzwD7uSWA/lCDK70rSZZddJkl66KGH0ja8qbXr6RWWWmopSc0S4lDzEpaeiprHpPZ3rL/UTSVotPD8McD76LkYeDRrHtOamkFfYT/u3cLjxb+uRuHp4th+TNbt+OhHP5q2dbq+zHiCMugen1IxdTsiT61Ucf1zzfsHlC7vVWrjiJwx1D4plxN29RFboc3npa233lpS9oKSNyRlW1t55ZUl5XxLSdp1110lZSWopvj2YIBBMAe4ZzUVZoEFFmi0+f2tLYnQpuiUinhtTHKsWslrju1zsUcWAMfHlvth7gvaqZVtdxthXiS/0ZcqwW5qOcurrbaapPwuePLJJw/57uGu0TaRYb1HSZpvvvkk1fPEeb8lgsiXW6m9S40HoQQFQRAEQRAEQTBQxI+gIAiCIAiCIAgGir4Lh4Ptttsufabsq4fBwU9+8hNJ0pJLLpm2fepTn5IkbbrpppJyYQUH2bUm1dcKKiD7eyJeyUUXXZQ+77HHHpJySeReBJnTwxzoDyTPWrggbbUwmRpePrWf4NqxLe8nqIW81ZLHy3A27zsSgG+++WZJzVLOFGV48cUXh3zfEkssMfyL6gEodFDrO8aZJ0Ujq3M/PPnfy+/6PlKW6hdbbLGunftoUAst23333SVJBx54YNrGdd96661p29vf/nZJ0t133y2p2aeUIyZM18fhggsuKEnab7/9JEk33XRTaqNQyqGHHiqpWZad/u3lMN+gndozjzFCQRwfV+W8L2U7q4Xp1ua/2Z1DLRSU8U5BDyk/dwnhlqSHH354jscK+ovaffN3KOa0qVOnSsrLmkh5TqLokIdNr7HGGpKkz3zmM419JOn4449vfJ+HG9fedQYBihBJuZgTc4SXrmc/wrY9tYJx7PMNx6IQkj+TZs6c2bXzd0IJCoIgCIIgCIJgoOhbJcgXMZ0xY0ajzT0+qET+y37dddeVJP3mN7+R1PQI8Eu1rZxxrbQnXgZ+3bonlPO58MIL07aDDjpIUm8rQSQeen/yqx3Ph/+KJ/GQ/b0cca1UKvh+/cQGG2wgKXtHvS9qib1tC/S1FZXAO7L88stLaio8JLxji/59JIfWzqGXwXuHuiUN7Z9aOV761RcExZNULvApZZv0hXv7DQq/SNJuu+0mKRdd8fall15aUlbGpTxXcf1+LOyKsu9PPfVUakMdKlU2qa4ARbGE/qAsa+2gduMFd294rfhB2eb3fjjec1fXOS+eMz6XMc6xcykrQRD21zu0ldWv3SdUP39XmDRpkiRp1VVXTdtQC5jbnnzyyY7O595775Uk7b///pJycR5paEEEP782m+r3ea9NOfUiUMstt5ykXJzE5waUIJ471113XWrjuePL1zC2+T6PrOJ9vduEEhQEQRAEQRAEwUDRt0qQ5/gQk84vdldoiBHlXyn/cuUXq3uc2/JaSo+Fez35Trz2/nfESN5zzz1pmy9q2avg6fX+5Lq49vnnnz+1XXzxxZKy132zzTZLbZRG9GOBly/vJ4hzrZXOrC2oC8Nd9KzMv3JVk+PjCXXPKfuxgJwk/fSnPx3Wd48VPh5Q1mqqVdsihzVPXZvCVqoWnmfE4qK9CvHXhx12WNqGt83LkHIdzHVe2pTyscyN7mUt+9I97HhJL730Ukm5RHnQ37SVAEZ9Zsz4GCMXzZVEbKlWBrccw23zoY9R5kGeF/59HMNzgqAW0dCv3vl+p1beHNruSW3/TTbZZMg2lpgAVyXKvFLPT2E+xZY9l4j3mCuuuGK251ezrYloYzwjPSeId44bb7xRUl5gW8rjlxwfX8y4jByS8nsMuaz8K0l33HFHl66iSShBQRAEQRAEQRAMFPEjKAiCIAiCIAiCgaLvwuGQy1yOu+WWWyTlECSX80no9VAPQtZqSbxlSF1biWM/5pvf/GZJORHPj00bkqCUpVhk2F4MUyI5um1Fb4fCDyTIeThcLYwCaqXN+wFCMmvhI7U+K4tKtIVneFtpd7VStNi+2yThcITtSb1pZ5K0wgorDNnWSdKp79cWItcJngzrCZzjBdfo18r17LjjjpKaY4d7631JyBt24aFyb3vb2yTlpGMvLc73MM7979hvvfXWkySdc845qa12DyZiWMhEpK2sOXMdoWV+T2slh2n/3e9+JymHvkhDx2RtriRkyc+JUDdCZ90mmRMpHuP0m/2V4YF+/oQKEwp2++23pzb69cQTT5Qkff7zn09tDz744LDOgWIDzCkeeli7X51CWJvbQ2l3tfLLtbDyyZMnS2qWUZ42bVpjn5pN147FdXJtvLNJ0rbbbisph8PNyZ7K53y/2R/UwsoJr2YJBSmnk7C0hReq4Bi8+/JOKeVCBxRwkvJ78Xe/+11J0sknn5za/DnTTUIJCoIgCIIgCIJgoOg7JejDH/6wpGahA37Z49F0zwC/QH0BJzwmeEd9/7ZSzqUH3/+ObZyXezNqpUNZ+BKPTi966Ck17IvS4XmuJbOed955krKX4LjjjkttZXlTxz16/QQqXq1ceM2LXyYe19Se2v/5jB15G4plmfTp3+dFRHqVOS3sWvZPbfHFtr/rJAnbk6rHUwkqF2quzR+rr766JOmGG25I25jPavaBB65WGAJlx8ch4xR78r/DG1grvPHYY491dI1B79FW0pfxSbEc95Tfd999ktrnv7Z5sAZtnmSNGoHdeiETFMu2ua4XCyPU+rzt3FjYnT5AEZby4u944nfaaafU1rYg+Q477CBJOuqoo9I2lu5g/PsCzMMt7FPDFZpSMUE9nBPYhqtUd95551yd13PPPSepWWDBVY/Z0en96ye8iBXPIOaBbbbZJrWdccYZkvK77LLLLpvaZs2aJSnf06effjq18YxhIW8pRycwtr1E9hFHHCFJmjJlysgvqkIoQUEQBEEQBEEQDBTxIygIgiAIgiAIgoGi78Lh1l13XUnSAw88MKSNpEEPCSJ0w1ebJYETic/3h7bwGdpqK1azFpCvgsv3uMR6/fXXS5KmTp065Pi9wqOPPiqpGebAtXgSLNDXDz300JC2trVfWK2532DFY6T9WvKvhzKVSZoumw8n2bQmt5fJmM6cQs16AfpSqocL1rbNjrZ9aivXgydo9gK1sbLiiitKyuNvvvnmS22E/rqdYZOENjAOpaFJqw4JzIS6+XzLMTim21e/hsOx3pmvo0RIMyEyntTrBUik5vgl5KPfwmJ4DtbW1eFe15LKH3nkEUnZNqUcnlZLDi/t2v9PP5Ig7eu1UASAUBlCw6Qcpu3rjZWhZnOT0N9N/H2jrTgT573BBhukNq7vgAMOkNR8DvPOQWi9r+HXxs477yypOQcTGudhT9CNcLja3Ma88rGPfSxtI+med6f3vve9qY3+8XH5gx/8QFKet3ycMs/Vns033XSTpPye6PeFcGH64oILLujoGvudWjgc/brbbrulNp4xpJxcdNFFqY20DwqWeLEsjknxC//Mdz/77LOprfbO3w16Y1YIgiAIgiAIgiAYI/pOCTr22GMlNT1SpbfTE/lJ2vQV4FmFtpZwXCuNXYIHwj3/eG9Ytd0Tuljh2r0LlPXu5dXWa7+86Z+aEgS1fm3zwj3++OMjOLvxh/uK98L7BE96LVm4kyR9p/Rkum2WaqZ7b7A3Cjj0Mn6O9J17TPnMtQ/Xy87feYIwn/m+XlHM2q5t4403lpSTSd3zjZLlHmBK9qNqUDJbapbElpp2RdEDnzcB9Qn79aIzjAEvod9JIvxoUSt6wxihpL+UC034yvSU+sc7jOIh5YIUzN9eVGLSpEmS8nVffvnlqa1t7LeV1Qfvw273Zzlvcx1StoNacRbmv5pyW/t/mz2U29z+UIV4fvs7QG1eeM973iMpP1+6oWDMDo7t8y+Uak9N/fFCE8stt5yknCTu9nP00UdLyhExN954Y2pDPVt66aUl5eIGknTKKadIqkddLL744pKatk856FqhgbZmtyU0AAAgAElEQVRS6iNh+vTpkvK8RallKReaYs7xogm8a3nxDO4/47Et2sJVdIpDcHwfCxSIOvTQQyXleyBlpcLtFvvkHLbbbrvqdfcq2LLbA6y55pqSmgrbjBkzJGVF8eGHH05tFJWg790mgXEqDS3kw1gYTUIJCoIgCIIgCIJgoOg7JQjPZg1it2t43DsepU4WraxRLqgqZa8CXidKiZafSzzmsddoi8Hk2muLprbRidLWLxDDjLeyllvm8a4olDVvalt52rZ9SpXIz4E+9hLGvYp784jvd08vnifPaYEy76Dm8UW1xVvq++P9w/Pay+Dlff755yU15yBUD+Lopaz20H81pZBt3jfYKkqTK9vYb22JAe6PzwujPdZr5Zc5R7ch1C2eE5R0laTbbrutsY+UcyT23ntvSdJaa62V2vB6MvZ9jj/77LMbf/fpT386taHkeY4q1BT02rbRohw3fr2MH7zDPgfRT25bbapLJ/bAfXSPM32Bqun2yjF9G+WyeSZ3K0erbR7uRCXxBV3J9/E82rvuuktSLkvtCjWedObIL37xi6mNqJcnnnhCUlaLJGnPPfdstLmqic1/4hOfSNuuuuoqSfnZ0e1oDVdleB/j2eVqNeOKXGufo1Fc/L2PY2CnrrAxT3GPvA+wb+YLL/PM3Md5uTrJc8ujQNiPubpWMrpXqL37Mo7dlsn3Y373kuko5tjIGmuskdoYv6hpXsK+Nndy/+jPsXhPDCUoCIIgCIIgCIKBIn4EBUEQBEEQBEEwUPRdOFyZIF1+nh2euIocXa7M7sdqC5WryerIr52WiC0TKbudbNgNKJHt0Ff82+nqzmXYFvLoRMWLZkBpW7Uk2raQrtq2tkIB7O8hPr2KJ/gT9uJhB4wvQmF8vJSr0vt4Lvvaw2s4JhK/h06MJ2XiuJfuZrwReuAhS4TDeMlYwuEIE/JwIfYj9M1DOvgewojd9lihnfvkCcYUC/FV3EebtrLnDmGC+++/v6QcAud431Fq+LDDDpvtMSnosdpqqw05BmV3PTH6xBNPbBzTk8DB54Wy4IknKxPa1C3K56gXK6Ffa0UNsE8PgWT/thC0trYyrFHKfVBbTb4WYsxzniT/boXDtR2H++VLX2y00UaSclK5hyeTRD558uS0jdCjgw8+WFIzBOz444+XJJ1++umSmqFg55xzjqQcKufPZkK/KDXtoXLnnnuupGyvUg5Ru+SSSyRJ06ZNG3IOcwMJ8w7X4iHPzC3M+z5H8dmfBcw7lAsnVEuS7r//fkk5LK629AfzoxeQeNe73tXYx8ONsUV/n2Fs0Ofed6usssqQ6x4PaiGd9Af96YVzKH5DqK+XyKaP6Xsv9sX3kBbgRXQYKz5meRYRluih1qNV2CSUoCAIgiAIgiAIBoq+U4LaFJO25HJfcIxfmbUFJktvWE0JqnmpSHTFM+ulK2ves+EkUo4X7mmD8tc4CdqdgveGxMt+w8uvl/j9rS0O24knss3b0VZmnL9zT1k/LdboKgylUvFoStlrRB90qpSVc4J7lvD61Uru9hIrrLBC+oxXmPnF5w88lq4QkFRN8mlNuaVv3QOLooN339VE7gtJwa5ccA6oKKMJ3mRXXVH88Tz6Yo8knNfKtIJfC/2Bzfh4Kj2cLOQp5T5n4UZPXqd/3v/+90vKHu5OwfMsdX8OLeeLlVZaKX3GzugLHyvYj9tiJ0pQJ8tQ1CI++D4/dq00uydoz+n7hgN9cMwxx6Rt66yzjqQ8Vn28UMwJVRXFVsrq0Oabb562UX4ddfG6665LbTx/eJ+hVL5vw+a9KE95Dg5FGVyNovz0N7/5TUlNW+gGFA2Rho4zv0+8g/hC84ANulJWzvOoY5L01FNPScpRB88880xqW2+99SRlBcz7DrAxV0iwN54h/rcUSPBzP+KII4Ycd7QpIyWk+rgsx4fPNZRR5+823XTT1IaiXhalkJrKupRLnkt5jPg+/C3zpNvraBUuCiUoCIIgCIIgCIKBou+UoDZqCg3wC1/Ksae1X8ht3nao5QZxDBZ+ciVoNBdpG2vK2PCaWlQDzwze94svvngUzm70oRysU1MU8bx3WjYW2nLRagt+4g0ry8dK9XtT8770Aq7UojD4mL3sssskZY+m9yv9UssJKhd+83j8zTbbTFLOfastDjcelF4694BxbSjPHovOtXneD7ZCfpX3G3HzbHPvPsdfdNFFJTVzD1CJsCH/PvcejjYohuRcSDkuvVZ+9fDDD5eUPeU+jvDy0idSVjbKPMg5gVKGPeHtl7LnmLwCzy/gPrTlNXlMPZ7t0YL7LGXbYGx6iW+8u75/Jyp0J4tG19qwO/dcM+/5/cZ73W14drHArpT7gJxgv3485IwNH4PYyiGHHJK2kcPENlceUDqZG5dZZpnUhi2hdLqyi32juPtSCqg8HkVAntfVV18tKecgdQvPSeJ+cr4+BsnhxO7mVHqfxTWxA9+HZwzbPD+0jDDwiA+2lUqklPNhPCKGuYfxzxwqZdv5/Oc/P+Tc54baOwi05bLXQNV0ZZf8tB//+MeSmguinnfeeZKyfbt6g70y1/rzl3cRn0uwAc7V77fPL90klKAgCIIgCIIgCAaK+BEUBEEQBEEQBMFAMaHC4dqore5cowyRq8nxbX9fK7PbTwnqc6KUjWsJhOChOkjIhM4MNyG4V/AESEA69/CzBx54QFJztW8k+jLJXxoa0tVmd25PSMok077vfe9LbRzDk+GXWmopSdKdd94522scDzzcis+e4F8mQ9fKYNfC4YBwHg/duuWWWyR1HtI5VpTzBWFfUh5vhIx4GftJkyZJyqEjUg6RoU88YbtMIvZ+o0/4+1p4Dwm/HlpJSI2HjIxW8RfK2HqIKmFCtbA8+qxWqIbQFQ+JJCzNw4rAr6+Esr4c3/uH766Vx+f7vNBDOVd4aNRohYeAhyUR4sNc5yEszGtud9zzWgn/cm5zu+skBJEx7PePc/Wy+rVlCroBoU8eTsVzgXB4fy5yX9nfbYxrYexKubw2xQm8wENp394HlFsnlM3DW7lH3DcvYc95uZ1yXtyrWmnjkUA4lIcqPvjgg5LyPORzFN9LiJXfU8ZQrWgL/eP9Sr9zP3xeog+wt1tvvTW1lWWtF1hggfSZfqo9QzgH7zuudSTU5i3opOiHj0+uyc+HeXS//faT1Hy2zJgxQ5I0c+ZMSc3CN9gnY9znBu4ptuml0TmG9w/7YZ8eZujlzrtJKEFBEARBEARBEAwUA6MEOaVH3T1TZRJcW2K7J7Didal5DScSZXIgZWdrkOAp5QUD6U8UiX7Dk/XxOnHPXXHBM1RbgBLvSM22ysIT0lBvqntV+W68eF6Csqa29aoS5B4+PHZelrVctLPmIa55w+jjWhveMLxbfq88IXO8oE9cXSYZHrXh1FNPTW177LGHJGnjjTdO2yjHS7+595M+4bq9UACfy8RWqaluSnXPn6szXoq2m+C9veiii9I2vMKoJO55ZKygZtSKj/g2vLyoYe7Bx1PJeHOvO8egoIUvJoti4d7PNpgPuEcUBik/jwbu8S/7x+cW9nMloVS6fPy1JXGX3+dwPzi2zxm0eUI730lJZC+5Pzfsu+++kprPMBSO9ddfX5K09tprpzbGL3bgChuLHrv9fOELX5Ak7bnnnpKa6hb2wLX4NfF84dnjY718dvj8yZj1Mc444vuGuzj97KAvavaDjfvC9qguFBfwuYTr876jDxizNTWQv/P+KRfjXnrppYfsj+17iXPKi/t7Af3IubtiODfU+p37iN37M3OLLbaQlOc971dUIRaOlqR77rlHUo6Q8LmTggiMPf8eIjeYj3zhcxQ/+pcFa6W8rICX21522WUl5SgQjxBxFbybhBIUBEEQBEEQBMFAMaGUoNov5XIhLodftbV8oVpMchkL6/kFUFvQqV9zgmr5TWV8Px6CGq42bL/99pJyH/qv/37C7QGPB6qBe1pqMf9Qy/spFUiHbTWbxLbw3LkSxP1zdbJXc7FqZYHd81N6j9vK2rctCudt5QJ8baWJxwNi+30cEiPNfOYx015+FLhertW9yvQNbV5emG3kNngfU6582223ldRUffDcLbbYYmnbaClBNVAj+He01ZI2UMgeeeSRcTuHuaE2xvBqu7eXvmZxSCl74Dt59tXmvLbFHMuSylK2a39Gsx8e+W4pQYwJX6iUiAgWyG3D1SqeEz72aGeudhvGo15Tdtnfc2SAfqktKM/zy/OY2I+/8/enucm1Qhnwe857FHOOK67kpaBKzKlMfa1UOpTvb94HXBN/5+9xZdlmn+/IYfEoAqIWaiWgu4HniC6//PKS8oK6ft4o9uTe+N9dfvnlkpp5P4xtlMozzzwztfEMYukXf79hrt9nn30kNcuf807E2POcap63X//619M2xg/5VJ7H5Ip6NwklKAiCIAiCIAiCgSJ+BAVBEARBEARBMFBMqHA4JDqXiJFRfRXbUn6vhSUhu9bKQdbKF3PMd73rXUPa+jUcrlZmlmtBymwrL0yJT/87+snLTPYTtVAArs3DI5GPPcG+DMmshbW1ldAt95WGruTupU9rduehGL2O91db6fByH6dttey5KfU6FmBDnljL/SXUx21oxRVXlNQMS+IaCUfwa2YMe/gJlKFHq666amojhIIwHcL2JOmOO+6QNLR4QtA/ULTBbYuxhS36WMNG3LbKZ6yPv1rZ7HK/slCMlEOOeJYvueSSqY2wG39mcf6jFQJcO3/GrM+z7Mdc7XM7bX6OZRn8Wnlx+sLvA+89PHM8RKsM3fd7xZwyZcqUIec1ffp0Sc37MDfzJtdUu69s85Auwn1ZbsNDIMtlN2rfU5vbau92fObeeJEPwtroX3/HI3zOz4Hr4XtqoXlzg5dMJzSPYg0XXHBBavPPJRSOueaaa9I2zpdjevGW9773vZJyWoM/Y+gP+vBrX/taaiMlgnvrfc7z3ct0c3/5d4UVVkhtHsrdTUIJCoIgCIIgCIJgoOhtV2gLcypvCiRTeVu5MGXNIw9tC1rWSojW1JO2c+5lKDNZoyyVXcPVN7woXHe3ykaONbX7W1tQDu+Re6LK0thtCqS3talCHB8PpCtVtYTjNvvsNWpjpJYY28lYalOQehWSbv0+4mWkAIGXZq2p3njPUGw90RTvJYm7XoCh9KAus8wy6fPDDz8sKXv5vCQ3du9lgJmD/byC3qVcRFfK80qt5DzKhc9TZTGX2mK7Iy21jC37YrHYZG2u7LQceTfoZhL8aHm+Z8c555zT0X5eknq4YA9uW7wbcL21Qj6dFs8oF+ltK6TgxywjXPz8UIVQ2HwR+Fq5fbbx3d1eLNq/n2tYZ511JDVtnc8UzHn22WdTG0UP/PlBkQXKg/N/PxbLSXiREYoZoMa6CkoBFd47XPGknPmWW26ZtmFbvB96EYrRKnITSlAQBEEQBEEQBANF3ypBNS9ArcRtWQZ3uMevHZNf3+4tYJuXIex3arG2QL+0qUXuMSo9M72wGOVIqJVaB4/Bbivxiie0Vpq9jdpCd/Qj5+Wla+d0jr1OzeNYU3RqZV87aeu2h67b4JHzOYj7TNlTFnyV8nitLdqLR8095fQvba7UsB9e2euvvz614c3D++7nx/6eKxhKUH/APceOfFFI8r7YB/uT8hirefdrURplnl6n8yBzF7blHmfGwdNPP522oWyyCGQw/tQWEeeZxTaPEmGuqSmQzDttC9T7M4TnLs/PmrrEd7uigl2jeLpK3rZwL3/nisqcns9t7L333pJyXo4k/ehHP5KUbdyXyKB/GKteuvq4446TVM9Fu+GGGyTlsu/S0GUOJk+enD5zPiygShl0h7HozwqiBmr3iPnDy72P1lILoQQFQRAEQRAEQTBQxI+gIAiCIAiCIAgGir4Nh6tRC5WplQQu5dBOixR0UjShTZrtt8IItVBArpk2Dz8oIVFOypIn1z03q073Gtx7Xw2Z0J9a8YO2ghxl8Q3/TJ97SBOfCYvzUChPgodeLQtdG7udFs/opGx2bby1hXv2Au9+97slNccYNkBiKmWxpVy21MMG6EPCgv2aCddgbHoxA5KB2ccTvt3OJemxxx5Ln9/0pjdJaiZ1s7+XQg16D2yL0DcPa8HeCGt5/PHHU9ukSZMkNcdrGQZXK5oAnZa955icwx577JHaTjrpJEn1kDwPEQrGF2zskUceSduYKwgV87m6DEHzMDqeZbUS1ISzua2VxV5qpcexYX82867CsdyWOR8Prea7Oa9bbrkltTE/jgTOd4MNNkjbNtxwQ0l5fv7Zz36W2ngekLLgy2cQIuehaFwzJa99WYRll11WUp4bPKyP+0D/+rOCdxC+x98/2Fa7R2wbi2d0KEFBEARBEARBEAwUvekWHiF4gVzBqCkObeWIS6+U/0ot29wjwHeOZTnO8aD0HtcWI6tRlotsKzDQy9RKbnLvvQjHU089JanpuSpVNLfTclvNA1rzRJXFFtxLXys+0evFABw/17Lf24og1Kglw/Z6cQ7uM95QKScW0zeLLbZYakP1dvUbZZp50BNhWYgQD6F7CrFlvsfLslLogLnOE2jXXnttSdLNN9+ctnkZ1qD3+cpXviKpOT9hB3jK3RuNV9m9vOzH86FW/IBj1iIqygVGpWzD2LeXdMfb7WOa+eDEE0+UJB100EFtlx2MAZQ1X2qppdK2MmrCn5nMMSgoXngKW/HSz9hbbUHUMhrFny/lgue+ADRzIep4TdH2Y3H+KFxeRGBu3g9PP/10SdKFF16YtmHTKDXerywmTHEBH4O19wz6k3Hm73ZcC8VwLrvsstTGoqwsmeB/R1/xnOKZIw1dVNbPsVbqvoxA6BahBAVBEARBEARBMFDEj6AgCIIgCIIgCAaKCRUOVwuRqdVlRxathWSVyesuy7GtFlKEDO8Jo220JWz3Gp7oVibBeahOiScssh993mnSe6+x4IILps9laJLLwNidr2oObatYt1GGxUlZ1mbV5qlTp6Y2ZHBfs6VXCyPUxoGHtpSJ1i7jl2GtPj7bwlt7tTAC6/YQ9uNzEOeMrXmoGW1+/dhfLayNsUhBBG8jNI6/89AjIGSEtSokaf3115fUtHHmAULzerXfBx1CahZddFFJTTvCBrmXPq7Y3yE8rRYOB23PPuzHbZ9QKMbwXnvtldoI1/H9mfdWW201SXmdFUk67bTTZvvdwehx6qmnSmquM0UC/nPPPScph1xJOUmf564Xb8EW/T2jLUy/DB13WyEEjL/3kDzmSWze33kYI/6sYn7jmGeddVZqmzVr1pDzGi6+3tqRRx452/2Y+5dbbjlJzfG2wAILSMpjXpLuv/9+SXnseXg0a4Y9+eSTwzrXKVOmSMph0tdcc01qo2jCG97whrSNvqXvvF85v1rBrrkhlKAgCIIgCIIgCAaK3nQLj5DaCsCUYvQCCSS68SvTPV6lwuFtpQfBf5GSLNymjHS6MnavcdNNN6XP6623nqRcdtRVhhJXe/Cw4N3uhkdkPPByxdgb/7qXam5Whh4JO++8s6R64qF7vDyJvZeoFRmpjT221Yog8HeuQpQFJ3weKBU5P4fxLCCBukOJU4fkXAoc+JzH/n6NfCYh1z2cKLz0g3vd8BTiZcVLK+WxzwrhnijM/l6cgb4nsXW43sRgbKCYC6qyl19n/JGg7snoF198sSRp4403HnIs5n0vcNDmyWUM4n13jz62fPvtt0uS1lxzzdS21VZbSWoqlhwD1WFuyhMH3eXqq6+ufg66B8r+ddddN27nQGRULULKnynjSShBQRAEQRAEQRAMFBNKCXJvE8yYMUOStNNOO6Vtxx57rKQcK+xeS46BR7OWN4S3yvMrrr/++iHfA3ha+0n9cU444YT0mRjPTsoLu5eaONZFFllEUv96f9zrjR2gStTybVwNw/NeK/9IX2ErbpN4QFkE0735eOyJq/UFavk7V01q6kIvUBsbLNAo5UVgiXP2vka1IQcKtUQa2tfeP+UiijUleTxgjJSx6A7x1EsssUTahvfd1Wj6Aq+b5/exsB7ljj0Wnzb61JVN7A/128FG/bzwxJPrFEpQb0JezXHHHTfbfXbYYQdJzVyCc889t/HvWOGq96abbipJ2n333dM25oVHH310TM8rCIL+IZSgIAiCIAiCIAgGivgRFARBEARBEATBQDGhwuHAw0cIs7ntttvSti222KKxPyUapZyUTJJ7LUSGpNAbb7yxo3PolTCbkeLXeeaZZ0rKqwN3yvTp0yXlsLjvfOc7XTq7scXD+AizIPzIr+kb3/jG2J7Y/4+Hg7CaNCFN0vDv21hRC2X1ZErC/gjHWX311VMbYVYk/3uo5mOPPSYpl+acOXPmsM5hPCDkknBdL7hBOCMhZbvuumtqW3nllYfsT5gk/3oIEeF2FDfxVdIpj0pYsJe1fuKJJyQ1Vy4H7GvHHXdM2x544AFJOcQu6E0IHa2FbxNaxr2vlUx3yhDO2jO5tkxEJ6XwORcvysOq9R6u3HZO/RqaHgRBdwklKAiCIAiCIAiCgWKe/4RLJAiCIAiCIAiCASKUoCAIgiAIgiAIBor4ERQEQRAEQRAEwUARP4KCIAiCIAiCIBgo4kdQEARBEARBEAQDRfwICoIgCIIgCIJgoIgfQUEQBEEQBEEQDBTxIygIgiAIgiAIgoEifgQFQRAEQRAEQTBQxI+gIAiCIAiCIAgGivgRFARBEARBEATBQBE/goIgCIIgCIIgGCjiR1AQBEEQBEEQBAPFy8b7BGrMM888o3r8DTfcUJK0/PLLS5L+/Oc/p7Y//OEPkqQ3vOENkqR//OMfqe23v/2tJOl1r3udJOnd7353anv44YclSRdccEHXzvM///nPsP9mtPvumGOOkSTdd999kqS//vWvqe0Xv/hFY9tCCy2U2t75zndKyv37pz/9KbXNmDFDUrOv55bx6Dv/+9r3L7XUUpKkD3zgA5KkFVZYIbX9/e9/lyS96lWvkiQ988wzqe3ee++VJM2cOVOS9NOf/nS23z2S6y4Zy77j7+bUd+W2ww8/PH3+4x//KCmP2dtvvz21XXPNNbP97pe85CWNY49H381tv83puzfYYANJ0tZbby1J2nTTTVPbfPPNJ0l6/etfP+Tvf/jDH0qSHn30UUnSCSeckNpefPHFYZ1fJ33Si3NdvzDec12Nboyl8nsYr//+979n+31hd2NH9N3Iib4bOd2aWyCUoCAIgiAIgiAIBop5/tPtn1VdYG5/8b7xjW9Mn4877jhJ0g477JC2velNb5IkvfKVr5SUPUxS/pWJ5/TNb35zattzzz0lSeuss44k6YUXXkht8847ryTpV7/6lSRp2rRpqe2II46QJP3rX/8a1nX0ireAa5OkW2+9VVK+TlfDUIDwIuOFlrJ6dvfdd0uSXvrSl6a2gw8+WFL2OneD8ei7l7/85ekzqtZOO+2Utn3961+XlFWwX//610OO8YpXvEKS9NrXvnbIebHtW9/6Vmqj7+jP4dpYjV6xOxQKSVpmmWUkSWuuuaYkaZtttklt66+/viTpd7/7nSTp//7v/1Lb5ZdfLikrl4899tiQ7+lUVemEsVKCoGZzX/3qV9O2vffeW1JWb5jzpDzvoaT5vIkiyfFRKqWsoKNIvuxlOaDgn//854iuo1dsrh8Zi74rleaaQtM29xB9IUlHHnmkJOnJJ5+UJK244oqp7S9/+YukPKbbzsWf223qUBthdyMn+m7kRN+NnFCCgiAIgiAIgiAI5oL4ERQEQRAEQRAEwUDRk4URRsr+++8vSdpvv/3SNsKLfv/736dthLHV5MW3vvWtknK4zTve8Y7Uhmz/0EMPSWqGflA0gfCRj370o6mNRPhDDjkkbfOQnV5n2WWXTZ8JuSE8xsOLCEkgDM5DdUhWp5+872qhDP1ILRRo0qRJ6TPhRoQSuk3SV4sssogk6fHHH09tv/nNbyTlcJNXv/rVQ76nG2FwvcLGG28sSdptt93SNuyN0K0f/OAHqe3ss8+WlMMFPYxuvfXWkyS9733vk9RM6v/yl7/c2FYLi+1V2gpheIERbA2b8TDU17zmNZLynPe3v/0ttdHP2BXhcZJ0wAEHNP71Y7J/r/dfMDJqdsc9f9vb3pa2veUtb5Ekfe5zn5Mk3XPPPantpptukiQtueSSQ9oeeeQRSdJZZ50lSbriiitS27nnntv47tqcV7PFIAjmTFl4hLQRSVp44YUl5Welv7M9++yzI/o+5gvmCkn64Ac/KCkXg/r+978/omMPh1CCgiAIgiAIgiAYKCaEEjR58mRJ0ic+8QlJWW2QslfUVR+87rWETn7VbrLJJpKaJaBnzZolKSf5u3cULz8loCkXLWUP9Yknnpi23XHHHZJycmgv8573vCd95tp/+ctfSmoWjkBhe/rppyU17wNeBUple1EJkmH7nZr3e4kllkifSSRH5fGEcvqKUsT0s5S9+JQ0XmCBBWZ7DnNKFu4HPvzhD0tqjiHsjbHrag/ji6R/V0I4BkoS3mcpFzqhz/tJveBc3fONEulFNVAbaXOboJ/YRvEIKRc8QR2i/yXpJz/5SeNc/Jj91IdzA2O3kzHmY5L7Vfa9g/1Luf+/973vjfxku0jN7rbddltJzegHbITnKQq3JN1///2S8hg+/fTTUxtRB/TLoosumtouvvhiSdL06dMlST/72c9S24033iip+Szv5rIBQTBo+Hj+0pe+JCkvBePvJ0Sq8H68yy67DDkWc9oZZ5yRtjGHPP/882nb29/+dknS+eefL6mpBPk82k1CCQqCIAiCIAiCYKDoWyXIvZ0stkmujkNOkHuH+cwv0fnnnz+1lYtVukrRVoaYWEn291+tfJ+fM7+IyX/oZcgdkLJigYfZS+dyzXjg6HspewvwbHqeESV3Rxpb2sugfEnZ004MLLHxkrTuuutKysoRC39Kuf/pV1cnS/pV/fFS6yhe7ullzOGB9/wrtqES+eLHwBj0sscgxgAAACAASURBVOSLLbaYpP72GPv4A881oxw785MraNghKo8rt3j6nnjiCUlNjzx5G9CP/Ta3DKcUuI/Jtr+jjDRRCFJWMvGIjtf45h7vs88+kqTFF188tVGa/pZbbhmyDRUbD6+UF+y99tprJUl77bVXauMZcNttt0lqzgvkCSy33HKSpAUXXDC1vf/975ckHXrooUPOOQiCOVPmBLnaw3OAZ4tH8pB3uvPOO0tqLlZ+5ZVXSpK+/e1vS2o+f3lO+/OKd8ZahNRojedQgoIgCIIgCIIgGCjiR1AQBEEQBEEQBANF34bDeaLoo48+2mjzMCykPQ8boYgByfqXXHLJkL9973vfK6kpwZXhXk899VRqQ8Yn8drD4QgBc9mPELx11llHUk7s7EUooSvlktjPPfecpGbCKyFHhNnUwkAIi0NelZqlGPsZSgZL0pQpUyQ1y4QjBRPO5v1DgQlKE3ufEBJDH7pMfdddd0mSTjvtNEnSN77xjW5cypjjYS+MVR/HQFhcrbw94aqetE14KgVLvO8I0cG+XeLvVcrQPZ+fPvShD0mSlllmmbSNEFyKlfz85z9PbYReMlfRR1IOVSJs1dtIcuVYJMTWzm+i42WhARudd955JTWXWSCEi5KzXjiFUMUf//jHadvSSy8tSZo2bZqkXEJ2rOGZSWiu31+ev4Tz+f7MWR6GedFFF0nKdurLRRAaTZ/5cgCEbTJvelg68+yOO+6YtlFAIQiCOVOmefjcBMzv/hzlM8VQFlpoodR29913S8rPVl+igtQTf5bzLGLudCIcLgiCIAiCIAiCoAv0nRKEt4mFS6Xs5cRLRTKwlJOm3TuMB/iYY46RJJ133nmpDYWD79l7771TG0oOJYpdQSKZlV+rrvrUPNT8+j3llFMkSSussELrdY8n7o0jmY1f9ng0pexJ4Ne+/x0eQfqXeyY1SyH3I3gvp06dmrah9ngRA7zG9KGrhWVyuv+d243UTK5GjWQx0DvvvDO1+edexxOnGcc1NYyCET7GS4+Se5bwSKNS1sqLk/TfD0pQWdbfy5gedNBBkpr2gtd85ZVXltS0K9RrktjdS8f8ipLmRUtITD/11FMlNRe1pZjFRFq0skwYlvJ8v9lmm0mS3vWud6U2lH/+zheh9QI9knTZZZelzyQUH3jggWkbCcIkHY8X3HPutatVKDpeMIPiJr6EAqC8Mnf5UgHsT9lcxq+U+45njj83+D6WIQiCYM74s7JUWlZdddX0mTmsnNuk/IxA7a2pt3yPFwerRbaw35Zbbjmi6xkJoQQFQRAEQRAEQTBQ9J0SdN1110lqxhHjgedXpJe1xguJp0jKcYonn3yyJGn//fdPbRyDBS1rHji81quvvnraxq9mPNTu/WQRR4+L5tcyCzb2Mq5qkbOCYkGOj5SvmX+9fyhvigrinnw80f0KuRiu0OC1rJVYB/eY4FkhDwabkYaWh/a/w2PK31EqVupfJQhlx/OpgH5xRYNtZV6VlPuKY/qin+xP3gVleXuZUlUhd1HKc5aXtOf6GcP+99gmHkDP/UOBIHfR5y7y+bDZo446KrX913/9l6T+LdXe6WLDqIbEwXtZWGwNpcNVNO5Rp/1DWX3USo+AuOeeezo6RjdA6SJ31edvlJkHHnggbSOHjDHsHmdsEnXJF0ssnyGei8b8yXhnaQwpK+H9/iyZ6JAz7XkjLEeCXbhyynsGY8qfp7zTMaakPM+xvz8nmO+JJvAlGLBPbMyf5XwP3+3PF8aFP5PZjzHiC3T3SrQB81xbno3n5JKnS7/6Eh6luu1jkL7g+e59V3uWc/88R3i0CSUoCIIgCIIgCIKBIn4EBUEQBEEQBEEwUPRdOByQBCxJZ511lqRcNtsTLZHvSOKVcvnXpZZaSpJ00kknpTZKbG633XaS8oq3Uk4ALctoS9JNN90kSVprrbUkNcOZCH14z3vek7YRtuSJsb2Kr95LyVySqL30OKELP/zhDyVJ2267bWojjBE51GXYfg9hWGONNSQ1QwOxO5f2yxK4tfA5ZGpvQ3rGjggR8O/BJgmb6Tc8gZpwLg8ZKsPAPLSQzxRS8DAH2uhfl/jpVw/F6xcIi/IwDGyBf6Vsc1yjjzvC2ghN8cIIDz30kCTpqquuktRMkiVsjjAmD7+jv/u9GIJTC13j+fDJT36y69/nyw5QAp85ePLkyaltLMPhmOcJV1lvvfVSG89TLw7BXMiz2MMMsRHmMZZUkLLdME49kZqwGYolePgq57XDDjukbZ///OeHdY3jgfcLYG+1ghy1bSWEb/3qV7/q6BzGsqw94bKeDE/IFLZCCXUphyrzruX9xXuDhzgTIomNvfOd70xtFDPBxljuQ8phnjyvCdX0c6CfvBgU4/LWW29N2zbeeGNJeW72ED4/1/Gkk3BctwfCWpkHSGuQ8rsKx2QZACk/f2oFFbjf/rwidJhwOH/H5r2y24QSFARBEARBEATBQNG3SpCDR4xf+v6LkV+geKuknMBJ2Vj3QrPf9ddfL0lae+21U9tKK60kKXsqvKACJa5RnBZffPHURtKsb+snPLEXrx2L2blnHQ8dqpgnU5cJdd7nLCLab2A/eCtdocEL5El/eE+wIy/XjHpBP3mfUK4dW/aFglnQDBXOvar9RG3xNVc58DLVys2XqoMrwXym79zrxPFdte0XttpqK0nNReVQctzmsEnsyfsNbz3ju7a4MaXsXZ3AM0o5YtRhSVp33XUlSTNnzhzZhY0zbR5S92LS19hqreDESPHlA1DZKMDgJaPHks0331xSHj++SCz24yXt6RfO1+dGngvYj49zvO38nT8n+B6eIf78BV+wtR+UoE7szfcp999pp53SZxYxpkiER554NIcfe3bnsM0220jK7zWf+tSnZnuew4FiISwrIWWVgeco84uUI0cYU64MYmM+B/JMxU7dfrhO/vXk/vI56n9XK0gEnI+PB9QS3pH8+YLC3g/UlnRB5fEIF+a+mvpWqpJeRIFjegEk+hNl96tf/Wpq80XAu0koQUEQBEEQBEEQDBTxIygIgiAIgiAIgoGib8PhanIu4UYuubHf9OnT07b1119fUpaIP/OZz6S2I488UlKWN72uOWv6sM6QJ4exVgwSrYfk+EraQBiZFxboVX7+85+nz5w3crMnwxLeQOiNS6ZI1iRjeyiYFwHoJ7jXSPV+L+kfTy4kkZxV5j0RnXARQkU8zPDyyy+XlG3Rw06Q2lmTwKV3Qkp6ufAE48yT6/ns4ZRQrrUk5b6if3zsMf7pA8IepCzjI8F3ukZML0ByrtsXduGhR/QlxQxq14XdevgMc9tnP/tZSc3QEaCggq8vRKhnv4bDteFrulEYgLXQ/FnAPEiI66OPPpraeDaxdsgFF1ww5Hv8+cUxFltsMUnNUMexhO9nTveQP+75ww8/nLZhS8ztfk1sKwu+SHlOZbz6eGd8Eipz4IEHpjZC5TzZvVfxNZNq2+iP2nORMDXeZ7wIzIMPPigph9/vs88+qe3www9vHKc2D3jxk+OPP15Svh/f//73h3zPSLjjjjskSVOmTEnbGB88C7zgB+FXtBFqL+XwKA9dw85qfYxNYT/+DoJt1Yrk8DxhPSJf44hCBx5uWK5/5YUCvHhSr8Kc5KHjXDtj9vTTT09tFGPadNNNJTVD27FhnrU+f3GvfIzzPKPAhRfX8jC7bhJKUBAEQRAEQRAEA0XfKUG1kpKAAuGeUH6VuqcZrxHeDhLYpJwYh8fFPUuHHXZY41juDcMjWPNU+4rYUK6y28s899xz6TO/1Ll2T2r7xS9+0fg7T+7neuk7LxXpals/gQcKT7rfe0o8kggq5b665JJLhuxfltN0NQPoe/em4P13TzTMP//8knpbCcIePOEbb7N70Cn4gGfJ7Q6PFeO+VoYTT5R7/7BP+t49WHgLew08nJRl90Rb+si3cW1lgQgpq4cUPbj33nuHtOGV9aRglHSgjLMkTZo0afgX1Se4151CBcxdnhiNXZGgvvPOO6c2VHWUIC95zRzpnnbUUJ4hXrp3lVVWmavrmROuxKIEUTDIn5nMWb4/dlqbxxhn7OOREtgZc6vPXfQx+3tCPON1tLzFc6KmPJQwf9dKUde2YTdHH330kDaiUfw5yhxH4ac99tgjtaG8nH/++ZKaz2YKSvnYxRZ5pndabntOoCR4FATfhTpFGW0pLyfBEhvYoZSfg65So1iglHv/zJo1S1J+1vh7B8ou852rPZwXfV57r9loo43SNp79tYIh/twaT9qWMsAeUO2kPB4pyuRjfeutt5aUn7+8Q0u5P4nWcKWNdxa3fZ47ZSEPKb+Td5tQgoIgCIIgCIIgGCj6TgnqJFbff/3zq/yaa64Zsh+LpTr8KqUMtnub+KVblm2Ucrlk4qLdM18rAY23uh8WFXTVCk8yv+y9rVS83NsDePI9z2gsFmkbDfA64pHy8p3gHkLuOZ5Qv24+Yw81z2Kpavh+tLlK5Lbbq9AX7s3DS+WlgsscA++Dss9qOUF4rty7RSlWbLkflCByvph7KEEqZQ8nqrSU83XwsLtd4TF+4YUXJDVLkGLL2Ljnu2FX2JqrS3gR+xW3HWwNL6jnHpQ5FjVOO+20IdtYZBVVyfOwWEKBeHgpKy70uecUuL2OBuuss076jNrKs9U98j/4wQ8a5yrlMVXm5ElZwbrllluGfGe5+HOt3Dg5CH4OLG1Rm4O7Ta109UifYfTZxz/+8bSN8vfYmysWKCPMT7U8VMa8LwuCcoyq5Mekr5kHpJy7y5zA30vSpZdeOqxrdLBZV06ZY1Ba3FZQPlEQfF7GfjzKB8WK/cmPkrK6QL96/izPH/bxXGf6/AMf+ICkZr4uOUoe1UHfsmiqq7cnnniieoE2e6XPPJeJZyyLl/risFwvfejvdsyh3FNX2MDvKdEMrqyB35NuEkpQEARBEARBEAQDRfwICoIgCIIgCIJgoOi7cLhO8ER+pMuTTjppyH5IszfccEPaRvliJOkLL7wwtRFuctVVV0lqSu+EmxC24JIp0nK/4pI7oS+E1XgSOklt4PeBY/D3/RAGOCeQ4ZGWPdQISdllfy+S4H8n5X4kHKct7NOTK++77z5JOeHfQ5M8TKBXQUL35NFnnnlGUrNELKEetcIRhGXRh7XkU0IgbrvttrSN5HTGrofkuaTfS6y44oqSct946C/zk5eYLcep9w3hqoTUeXgSbSQY+71Ya621JOUQQ28rQ06k/igLW0vgh5122kmSdN55583195TH4HkjSd/4xjckNYtQEPLF/fAxPdpz6LLLLps+U/abogQeisfc7n1Yzo1uI1wfITIeOo5NkVztcyohR7VQbOZL77vRYrjl88uwqF133TW1UZba5yXm8Msuu0xSM9G8LCTjz5SyaIwnoRMGzBj3+8d7jN8j5gSeX17Sem7C4bhnbrvYDeGXXmodu2Nu9ucb85AX1sCmKKTgYX+EYdKH/n5CCCppDR6iRZgqc62HmRMu6HbHeXHf/P2vV5ZeqIV0ch8Ip6bIhJQLZBBqWQu5Jh3CQyfLAgy1OcttkedGrcDYaIX3hxIUBEEQBEEQBMFAMSGVIPeW4YnyBDnKIZJEd/bZZ6e2D3/4w5KkfffdV1Iz0ZxSgHij/RfsKaecIknafvvtJTUVkn5N/K+B2oNn3j0tZSGEWlEA7k2tBGK/4Wqf1PRo4i075JBD0jYS/fFyuOetLIRQ8xixj7fh8aJ8u3udxsIrOrfg+fWSm4wv9xqRxIrXuab20D/uRWJ/yoXfeeedqQ0PKF5GXxyuV2G84al1myORFW+mlK+NhFO3HUpic8ypU6emtrLfvERuufCq2y7qgZd+nhvP8VjBtXj/LL/88pJyku66666b2lheASW2U0oP7Oc+97nU9qMf/UhSMwG4XADcx7QXDhkNXJ2l7Drf7+Xr8Qr73FPap9sI3l6KQpAQL+Uxz7OEIghSVj2wZVdIKEfsHmo826O1GPfBBx+cPqMyoAz4uwGqDcqDjyUUIC8zT79TNIHrlYYuWkl5eynPm8x/XpCJcYyS4tEdNe88fcfcsPbaaw/ZZyRwba5SYVv0j5fIphT9t7/9bUlZCZfy9bptkbiPbbhShlLB2PPIFRb6ZG7z5T5QgLiP/nxBKffzIoqAUvcbbLBBauuV5Spq93yvvfaSNHTBYilfZ+3ZjE1iWzWV2ItQADbsx2L/2rIpo9V3oQQFQRAEQRAEQTBQTEglyH914nXxhZaOOeYYSdIFF1wgSfrgBz+Y2vAI4J1w7x8eBBaMOv3001Pb/vvvLynH/9YWjpsIeEyu1IzL9hhbqekxwcvOr/nR9mKOBdzj2kKUxCm7Ksl+9Ettwdya2gOoH65O8j210rClUtWL4Bmv5dc59Fn5r0P/uvKKZwlvKh5jKZeExSPVDzlUlBglz8bzbfjsahfXVNqelL30eGc9l4jxSa6Ze+ZWWmklSTlfwD1/HKOcC3qd2nhbYYUVJOVIAfdQs+DxqaeeKikrQ8P9Hs9nwGvt94j7x1IP/ve+eOto4PMZNkWOkntlUXQ8lwN7wcvrygPedo7vig7jFPvzc6BfUE18nKMY+NzBfNltJQi1ye85OTdEOPgzn/Nkvq+pMCg1vh/zvL/PoPaiTlDmX8rPWFQMj0ZhbqDvfXHqWsloVBIUuTLfd6RwPz0ShDwlru3qq69ObeRM8R7m9k+ZZo8K4DNlt7/0pS+lNq6TOc2jg3bYYQdJ+X74vEq/cN9XW2211MZ85/kzKHDYnStyteVSxoqaTTpEPzGWfH+elWxzJQ/li2eT/x33i7HrNsYc4uOT/uHvfPxTXtyja7pBKEFBEARBEARBEAwU8SMoCIIgCIIgCIKBYkKGw7mUSUjQAQcckLZRam+LLbaQ1AzFQa5Doq+tII6Uu+eee6Y2ZPgzzjhDknTkkUemtlqSV7+GyCHVIzt7SFcZduCrfSMzI4t6SFe/wj3HZvw+c30kFEpZ9ufvauFw4CExSNf864nvhEXQ5uEgHvLQq5TJvFIOYa2Vx60l40OtVHn5994/hB+Vq9T3MmWBES/mQHiaJ5MTFkKiuq8KTxgDoS5LLbVUasN22OYlw8tV3z38jmTuTTbZJG1jxfXxoDZ/g4cLYVeExUg50Z8QrRtvvDG18QzYZZddJOUQG0n67//+7zmeF33oK6+T5O7PI76H0MX7779/jsfuFp6wTJgK4VTYk5Tvr583461cGsG3EUrkYahlaKo/J7hf2LA/XyhiUQsl9P26AQVFCIuXpC233FJSHo8+zsrQHg8JIqzIw83YrzbX8e7CPOY2TJgWNu/vQZwPx/bvI/zJl3PA5ikm0K3yxB46C5SUxsZ9HiIUddKkSZLyfCZlG7zpppvStptvvrlxLApVSbloAiF+FLOQhi5f4d9DQSzsm/6SpN/+9reScj/5cdnfbXK0inSMFIqESbmozcyZMyU17xXPVuzHw/oIY+S5TUlxPwb250VBymNL2dY5pn/PaIVYhxIUBEEQBEEQBMFAMSGVIE+e49e4e5T4dYl3w3/Fo3DUkir5TCKxe0c/9rGPScoJkl66ttd+/c8NJLPhNfe+xisCXhyC/fBclfv2I3gtyoU8pWxvngSLTeGpa1ug0Sn7zhOJ8TLVFq+tlaXsNWolsq+55hpJzRK9eKXxGvl1Qq0P6B/+zr8HjyCeK7zcvQw2Q3/4on4oke5ZRznAw+7ePa4f+/Jkdzx3lID2e8Hx+Xv3INPmi7iOJ7W5t7ZQIAoWhXEkae+99278nXv3P/CBD0jK5b8poiDl5RJQhFx9ZKFM/t69pigM7mnm+YXdjuXSAu6FpTAD48iLMuCJdw8++/EcdFUaO2WxYn+OYtc8kz0JHcWJMeC2zP2bNWtW2kZ7uXTD3MKc4gWVtttuO0nSZz7zGUnNMufYG+qLRwwwPr3UNcUnmL9ZPFrKfYwduW3tvvvukoaqIVKOOqCIhY/Pk08+ecg1cs7YnUcf+HgfLly7K0uoC0RKsOyDlPuC/qHstJTHgtsdyhqFrXxOowQ9+PMRu0OV8sR/iunwbrfEEkukNkpj0yblku/Yn88NcwPPt5pywv1qK35QK4t9wgknpM+M6ZrKyDzKvfJCHjyDGLN+P7hH9IGfA8VP/H2GdlQ+3gVGk1CCgiAIgiAIgiAYKCakElRTb9zrjue+9IQ6/CL1v8Orzy9Y/6WNF6+2iGitLGLNk90PoAChanhOQhlX6zHiKCN4ivv1+p0yz8Q9dtiUq0OlKuG2VSodtYVmy32l7KnDm+I22Q8lnxlT7tXFm8vCg9LQRRf9Ost8IR/PfMYT6gsPkuMxZcqUxnf0MngvGXeu0NTGFPlBlIf1vKdy/qsdi9h495oy9ms5F3i7PReuH8AGUCeknBP08Y9/XJJ08cUXpza87eUC2lJWFI899lhJzXGIRxv1hDyF2XH++edLys8QL0M92rhCQ84L9uNLHBCrT96Gw1zl445tqEu+8GqZM+A2yRjG03zRRReltvXXX19SU3EarcWimSdcEbjwwgslSeedd56kpv3Tdx/60IckSddee21qO+6444Ycn4VJyWP2MfvAAw9Iyp54FluVsqJDH7i3njE7Y8YMSc0+51xd4SkXrfRnDiXyR0JNsWMeIocJFUfKSgvKgPcF6pQ/K1EjiDR57LHHUhtjj3LWrtAw1slFcdtHFeJcXFn86U9/2jh3Kee68fxyO5ybvNNaWWu21VSeNqZNmyapqfBhE8z9/jzEDhiPPi7p81r0FMe/4oorJDXnUHLqXRUsj0V+0mjS/2+iQRAEQRAEQRAEwyB+BAVBEARBEARBMFBMiHC4MkTGw4wIlfHkK0JvCBFxeRNZkXATlyAJCUEudGmzXMnZz6EWllSTNvsBwq84f08ALctquuSOfIpE7yVlxzLEo5uURTRcEqfcpBdGIKmQ/dtKQDvsVyuxSqIr4TLe1g9l2K+66ipJzXAZxt65556bthGe0FYQAXyfMszQQ1gOP/xwSdJpp5028gsYY5hXCBvyJHpszsMRSObHDn11dcI0SGwlAVjK4Q+sPu9stNFGkrKtergHx6+VQu0VOG9f4oCwmU996lNp2wYbbCAph12dc845qY2keMKg+FeS9tlnH0k5HMbDjAhZuv766zs6VxKLeVaNZZEdDyVifNbCdT0kGmivLQdAuBZzlttk+X1+vdw3Qt9YQV7Kz2IPS+xWWefZ4c83nvFLL720pOazgEIZ/OuUz1Mph1ry76qrrpraKKRBn3uJY8Ycz9+77rortTEPcF88/JiwJw9D4/iEdvpzxQtJDRfOw0PlmWOY4714DWFt/OvvXMyFhLdJeZ7j+ITfSXmeY3+3LZ4LhPN6KBh/R9igF01gfy/fvPrqq0tqFg+B8UgDoEz4N7/5zbQN2/U5jWUNeFf2svaMWfrV7x/vzwsttJCk5nxHSOdJJ5005LwY/9OnTx9yLLjyyivndHlzTShBQRAEQRAEQRAMFBNCCSrxX6ngngxUCbxT7rXBQ4LHwQsc4EXxxa+AxP9SHZDqv/77VQkCSu566fHyV7x7ErgneBS6XbZ0PCgVCL9evMC1cta1vy8XRHXKstD+PSQX4wX0xO5+sLFauWLGnid7MlbxUHpf0lYbl4xDV+T6GeYx7nttIUhfCJH5jPHqnlQ8uiQdu0ef/sLDjZdPygVQ8Hr7fUIJr3n3e4VPfOITkpo2R1nez33uc2kbCez77ruvJOmoo45Kbag9p556qqSczC5Je+yxh6SsrG277baprSx379RUYO4fhXfGEvewYyPcXy89jErlRQnw8mIbbnfYRm2hZOyNY7mCxOeal5/vc+++J2+PBj5/oxxQxtvtn3vI3Oyl+EmidyUcxQGV1xUalBTGuCuuKCgoFf7sIWIAdcwLWxCd4feP8c8xvDS1K8bDBfvxSBD6g3cJL1jAeaMEeZ9jW67M0O/Ymyf+U26ZBZHdtnheYzOulGHr2Kkv70HhmMmTJ6dtjzzyiKR8v/3edmPZCsrrS1mR5jy8GAbjElvxUuiohAceeGDaRt/xfuHjkntUewehzyhnP3Xq1NTGEgI1vJAUlHOgq62jRShBQRAEQRAEQRAMFBNSCfJf+Hg0/Jcrv2bxfLpXAu8Ux3APVrlglHtOiBvlV7d7eyaKF1rK/YMXzpWgskxjzROHd6FWNrzfKBcqc0URb5bHFneySCrqjdswYEfuWcKzxne7B7IfFumtqVVcp3vcuC48Rd6H9H8nZULb9mlbaK5X4FopzeqlXLE5vw7GILkZ7onE646doBb59zDXuW3jYawtQEubbxstsAH3HpYKi48BvLvErPu8D672oArhmf/ud7+b2nbZZRdJ2TN/xBFHpDaeBZTPrp1zbWzW2uh39/COFT53Md+jDrnKwLbll18+beNaGMv+HMU+6X9XM8oIDP8ejlVbSJXIAp8XVlllFUnt3ui5oW2OqClYqEQ1fPHSthxZV3lL6CvvM/BFlUtquSujBTmgrkQxLnlnuv3221Mb9xNbrD07XQ3HDnhe+HyHgsg2/zv6p7Y0ShnxUVuk11V0xi+2fMkll6S2NhuYEzvuuKOk5jxEqX2eA55ryLhEbVxjjTVS2/HHHy+p+V7MPWEO9zb6E1v25yi5RDx/WGDb4b3P/w5b9gWg6TO+pxbp0G1CCQqCIAiCIAiCYKCIH0FBEARBEARBEAwUEzIcjhAFKct4HvqAXEnohIcfIGsiy3lIDvsjF7rUigSNRO4hcDUJtx/KF9cgKZJSnV4us0xCd1meUBva2mT9fqOW0EwIjRfWKO3NwymwEeRil43LsAsPJfRCCH6c2Z1XP0D/1JLrsR8Pe2IslaXypaFFE4a7snavQQguiep+rYS8edI6Y5CwOJ93CAEhLM7LYRNCwTGXW2651MYxuBc+13HMsSiRXQspa7N5xuL5/1975xkgS1Vu7eVVrznngJKDR5GckXBAsqiAIhIURVEEP1NnDAAAIABJREFUEQRFwSsmQFCCInBJJkSUoASRnFFyzgIGQMw5p+/H/Z7aq2r2aWbm9Mzpnl7Pn9Ondk911a69d1W9Yb2nnipJ2nrrrXvun9A41nsEFaQiib3eeutJaoeOrLXWWq39+FidaIgq0rteimC6WHjhhZvPt9xyi6T2egaMEQ9543ts8yRr+pO1ykVyGM+Mcx+vhFjyfZeOZsz72ujHHwYLF1oI42PWrFmSpFtvvbXZ1n1ePeWUU5q2pZZaSpL0zne+U1J7DvLc5s+3zL1aGRPSHniOc1ESBHJ6hXHWQke53/jayflMZ7pEPEEhhBBCCCGEkWJGeIK6idGeZIgF1D0PvSzrvP3WrNFdq7In/2INI5HLk0pr1r9h9QRhmcSK59aFrgCEWxnwjHCtXFBhWMFqwbX2ImFcXx8HWGto83FBv/Qqpkaf+5jEil8bT/OiMNtEqVmI6Bf32tB3tXnZtf7XvD2M05qM9jDBHGPN8rWLfsBLJBUPDmPVE7aRH2UfCy20UNOGpQ/Lnxd8JgmXa4FlXyoWfJfUniqYb/77RAH0kuAnQdoTnHuBOIEn8FKkEk/s7Nmzx/wdkQJu6ZwojHO/p00XyBNL5VrTZ75+M6d83OEJYrzWRITwBriIEL+D7LELf3CP5be9GCrH1cuDHsIwc/rpp0sqAglSmUvMIST7pSJQQEkDX4eYJx7NxLMv88yjfHiO5r6AV0oaX7H7WikM1g2/J3E8vtZONYP/lBRCCCGEEEIIfSQvQSGEEEIIIYSRYkaEw3VDgdwlXksWxuWOO8413nGh868ne+LKw9XvGua48QhZ8roOtXo5w5q0juuSMI1eYX0eDkc/4hadCbWTuuOHUEGpjDv+lYrrulbfpBfd8DAXPyAEhQR4T0gfhnC4mpgB+Pwi3KVW74a/rdUU6Ia/jadG0zDA2PFQIsIfvCYIAgeEQvhaB/Tlvffe22yjmjfXx3+H6uTs0+c5Y9RDKaYK6mV5PR6O8yUveUnreKRSyZ2Eea+3dcwxx0hq9x1/S7idV4Vn/Vt88cUltUNbGWMTDYPj2rj4ALU+VlpppQntqx/4eka/cK19Xt12222S2oJE3VosvtbRd4S8eVhit2p9rb4fa95ZZ53VtL385S9v/b00s8R3QmAt9/CzjTbaSFJZ+100gXWI9cufuVjX/X7A93mW8BBWQmOpBTRRYYvaswjPkh5qzbPy5ZdfPqH9zw2D/5QUQgghhBBCCH1kRniCuhb1k046qfm8yy67SCqWQakkzfJm7NZh3k6x7HkCaM0S323DSkolX0n6/ve//4jHPCzQZ5xvL8lXT27DQkeS8dwkCw8KWEXpA0/wI8nQrZxYdWueID7XvBH0Nft36w3SuV/5ylcktRO0e1UJHwbcUs88ZPy5tbybYOljEqsW+/Jr1MW9moPqFcLCTn+4RwzLup8/6xGWOJ93jA/GnnuvsbYzVtm3VKz7/Lb3aS+PU7/BIkr18xo+/1784hdLKufplk48Ou7xYv3Cg3TggQc2bWyDqZKmZ37vsMMOkqQ99tijb/t+JBDOkIo1GYswnhdprJy6VK4/nmm3BLNf5qR7kIjYYP65NDhiGxdccIGk4oGSilCFj8VLLrlkfCcawhDAur7ppps22xZccEFJ0rbbbitJWnnllZs2hG5YA/1eUXuGZVtt7V566aUlFY9TrURFr2eY2prI9+6///5mG56g6bz/xhMUQgghhBBCGClmpCfIZWOXWWYZSdJFF13UbFtxxRXnuC8sSd0ig1LxZtTkoeHKK6+UJK2zzjoTOuZhgWKpyJP28gR5/2BZ7mWJHza6RQJ93K255pqS2sVMybOoWWHwcNT6k21dmVqpWGG32WYbSW0LzTB4grD41LwwhxxySLNtv/32k1TyOTzHD+9DV4Jcku655x5JxWt39NFHjzmGXnlJgway/Hg13CNGn3ixzk022URS8Ux6ngTjEEu8e3vID2I8ea4ZXhCsgu6xY074tnmJ9w+eo/FIuk6UiRZBrVErRHrYYYe1/p1OPIdg5513llSsxO7lX2211SS1JasZn3gNKa7tcE/oJVvvax1r6e677y6pXYybbT6H3VMUwkyE6JKPfvSjc/wO90WiAqTiBfeyHl2ZeY9g6kr0155fuVf0imZxuO880rPyVBNPUAghhBBCCGGkyEtQCCGEEEIIYaSYEeFwvcAd7xKjuP0222wzSSWpUiruwZqkH+EmSIfedNNNTduZZ54pqR0SNRPBRXrOOedIKkIHNU499dTmM+ERLuE47HRDYDzUiJChbgL1VEEoSi1hcdjgHK666qpm29prry2phHUttdRSTRtVrAkn8jlIuGBNsrkbBud9N6jhqh/5yEcklfBeP85aIvi3v/3tKTuWI488UlJJmpdK2MNnPvOZKfvdMP08/PDDc2xDznb11Vdvtm255ZaSypxk3vr3uJ/ecMMNTRvh0sxhl7y+5ZZbJJUQIGeFFVYY76mEMFIQ1uohrFNBr3vmIIeaxxMUQgghhBBCGCke9Z9BfkULIYQQQgghhD4TT1AIIYQQQghhpMhLUAghhBBCCGGkyEtQCCGEEEIIYaTIS1AIIYQQQghhpMhLUAghhBBCCGGkyEtQCCGEEEIIYaTIS1AIIYQQQghhpMhLUAghhBBCCGGkyEtQCCGEEEIIYaTIS1AIIYQQQghhpMhLUAghhBBCCGGkyEtQCCGEEEIIYaR4zLw+gBqPetSjJvSd//znP3P83mMf+1hJ0nbbbddsu//++yVJF1xwwYSOa+GFF5YkbbXVVpKk3/3ud03b8ccfL0n6/e9/P6F99qLXec2J8fTd3HD++edLkv785z9Lkp7+9Kc3bf/6179a3/3nP//ZfH70ox8tSXr84x/f+leSlllmmb4f56D0XW2cLrjggpKkvfbaq2mjP/nO3//+96Ztzz33bO2TvpTG9nk/GJS+22GHHZrPK664oiTpV7/6lSTpjDPOaNp+8IMfSJL+9re/SZKe85znNG3/9V//Z+ehP5/61Kc2bb/5zW8kST/60Y/6dswT7bup6LeFFlqo+bzhhhtKkpZffnlJ0n//9383bcxBxtC///3vpu3ee++VJB155JGSpB/+8Id9P05nUMbcMDIv+s7/njlWW4s+9alPSZKe+MQnNtue9rSnSSrjj/9L0qGHHipJOuecc8bsi9/hfCdz3l0y7iZP+m7ypO8mTz/mvRNPUAghhBBCCGGkeNR/+v1a1Qcm+8b72te+dszfY51y6/msWbMkSX/4wx8kFQuyVKyhfN8tp095ylMkFQ/Qww8/3LQ961nPkiT97Gc/kyT94he/aNouv/zySZ3PoFgLsMBJ0u233y6pnLv/3l/+8hdJ0vOe9zxJ7eOnj//xj39Ialurl156aUnSz3/+874d86D0Xc0TdNNNN0lqe8NuueUWSdL8888vqd0/u+22m6TibcS7KZX+7Cfzuu8OO+wwSdJzn/vcZtuDDz4oqczBBRZYoGlj3OGt+PGPf9y04dH47W9/K0l69rOf3bQx7i699FJJ0ne/+925Pvbp8gQxBvz6b7DBBpKko446qtn217/+VZL0mMc8Zszx/elPf5JU1jgfj3jMnvCEJ0gq3m9JOu200+Z4DJNlXo+5YWYQ+2655ZaTJF1zzTWSpDvuuKNpwyv0uMc9TlI7YoDIAuZ5L8YbDdKLQey7YSF9N3nSd5MnnqAQQgghhBBCmAvyEhRCCCGEEEIYKQZSGGGi7L777pJKKNGXvvSlpo2EaE80JwyEEDZCRaQS/oHAgYsf4L4nkdpd9vfcc48k6QUveIGkEsIkSfPNN58k6cQTT5zE2c17Nt100+YzLlnEJTxckLAGQnDcbUl4IX3urLrqqpJKmM2wwLihTx7JTUv/nH766ZKkvffeu2n76U9/2tqXC05cf/31rf14eKKPXamMUamEIHq4yTDAfHHBAsYP89H7hP544IEHJLXDVJnr9AsCFFIJS3zRi17U3xOYBmohaO9///sltQUOGAOc95Of/OSmjRBBhCQ8sZ31j5C3jTbaqGljnk6FKEcYPpivH/rQh5pthKbfddddkkpYqlRCLZ/0pCdJaoejMyZ/+ctfSpIOP/zwpu3ggw9ufWcAI/lDCENGPEEhhBBCCCGEkWJoPUHrr79+8xlr5W233SZJeuYzn9m0YdHEwyMVa/L3vve9Md9/8YtfLKlYrjxBHevUC1/4Qkltaz1WLZK0r7766qZtpZVWktRODr3xxhvHd6IDgEuY/uQnP5FULOvuDcMCTV+7RRqrMdZ6T8JGenzYmKyHZZ999mn9K0lf/OIXJRWxjmWXXXaOf++W034d07wGKV2peH1czhqYX8xFqcx/Eq5dUAEPCHLYPiaB+e9S7V3v26CA14vzeNnLXta0LbLIIpLKufr3+fehhx5q2p7xjGdIKl5y93ozXxGkWHTRRcccS1dExv8uFMYrZFIToeD+416Wecniiy8uSfrCF77QbOMeQCSGVNZ5xsMrXvGKpu3Xv/61pDJ+XOQE7y8iJ9tuu23T9qY3vUlSET7xNh/XIdRgncJL6fcJxuKpp54qqS1s1YvxRoGEwSWeoBBCCCGEEMJIMbSeoBVWWKH5jOUTD41bhbAsYSWVSp7AGmusIaltPcfSvNhii0kqniHn+c9/vqR2PgZWVKRB3YrN72288cbNtmHyBHkeARYPrJbed1iba9ZOrM183y0nvv9hYrXVVpMkvec975HU9h5gWapZxjl3PJFSsW7Sr+xbKoVCa32HR46+Z/xJ0kEHHSRJuuqqqyZ4ZtMPcf5SyRtzSVDmGp5I5pRUPGM1T9Af//hHSSU3gf9LxROCJ6ifhY77ifdD15PF2JPK+Xtf4jFjzHjOGP1MDpXPW6z1SIp73tTJJ58sSXrve98rqXiL5nTMo24lra2Hm2yyiSRp5513brZxrTynDS88eTeM/+mGSIejjz5aUjt6grno+XZ4h5ibvg4yPokA8EiD7n3F/47PfP+YY45p2igKHILjUSaMlwMOOEBS8fpI0pJLLilJOvbYYyW1n88+8pGPzHH/w762uRefewvn1EuSu3bePHu84x3vaLYR/VTL/yPqxcFrfvbZZ0sqhdCnkniCQgghhBBCCCNFXoJCCCGEEEIII8XQhsO5Cx0XG9tWXnnlMd/zsC3CYFwsoUtN5hmXKbLQLg9N+BuhS4STSCVUjpCGYcNDaOhHXKV+HQhXICzC3amE6hAa4n3nks/DBJLESIi7jDBuZh8/uJvHE/7Hvh1CNWvQ97V9b7HFFo/4e/OaT3/6083nb33rW5La8ri4yTk/d+N74rnUDhl7+ctfLqmMsbPOOmvM31155ZWSpsf1PhG6IghSmTdf+9rXJLXDgklGR/JaKmvdfffdJ6m9DhJiSJvPSRLZmcNIuEslRPjyyy+X1L5OhGD6nB51sQRCQaQiL07ItosC8D0XRlhrrbUklTIQu+6669Qe7BzgGjP/vOQEaxxrvFRC+371q1+1/k4q4eTcF32NZBvjx/uHfTJuXbRol112kSQddthhkzm9oaVX2Omee+7ZfPb1VWrPz5pYzDBR6wPW9q985StN2xvf+EZJJfTcOe+881r/+t997nOfk9QOXZ0p9FqbJxrq94Y3vEGStMMOOzTbeBZECMtDg3nG9vBfnqNf/epXSyphw1PJcD59hhBCCCGEEMIkGVpPUE0mGIuGe2F423RJZqxYWJbc04GFufaGjJWKpE+3RncTv9z6R+KoW8qGCe8LrHC80bsVBqsd27xAHn1A/7gXblAT0h8JvAxYdd3KXpPO5DPWUbemuhXe/97/rleiImPfkw2XXnrp8Z7KQHH33XdLaif4v/SlL5VU5rMnYWNlriVh8xmJ5+uuu65pYx94QgaNmoUWaWKSUF12n/O55ZZbmm0kBjNv3UtE3zD2GM9Smd94yWoeRjyTSBdL0nHHHSepvQb3WlNnIlih3/72t0uSVl999aaNtQIP+le/+tWm7YgjjhizL0R/sJBusMEGTRvJw9PB2muvLanMSb+XXXPNNZLa5RIQIOFfH8uUspg1a5aktjQ79wLWRi+uzXhDAMnFUWbPni1p9DxBNdZcc83Wv1Lx/Oy///6Sxu/9qXmjB42ax2LzzTeXJL3rXe9qtuEBGo+s9TbbbNN8/tjHPiapPCf2KlExzPRap4866ihJ5f7h9xHmI/eRO++8s2njPuJzHFhDvD9Z73zeTzXxBIUQQgghhBBGiqHzBCF/62+rWCuIf8daLEn33HPPmH1gFeXNvpYbhOXDZbDZhsXfvT1Y9mpWqptuuklSiYWWhss66v1Jf9QKJVI8lkKxbi1kH3zfY5LdIzLouOTm8573PEnFOurextp15Zy7ccu1NrdSeR/PCf7erSoveMELxhzzoOW99MLjh7sWyVqeDOfuf8f32Fabz70KWA4CXhCVgpRY091DwxrkY48+4VwpQimVtZQ1y/N+8Cji3XVvOR4n9u2y43vvvbckabfddmu2DcMa14telmMkxD2Ph/IKt956qyRpxx13bNrwdIy3yCLWfGTIp7OwtK9PHC8WXS8dwThy6203WsI93dwHyZGln6SypiLB7XmQ/Da/5wUtufePCr3Gzwc+8AFJ7WefVVddVVLJJbz00kubtiOPPHKOvzMdHqBuvkiv3/TzZXz6+r3UUktJKl5VL2jf3cd4ZfxZY8lVdW9sjWEqoOp90OuZFE8rz8oeicH147rVIqvoC48QYI3wexjrI/vwZyp/Du0n8QSFEEIIIYQQRoq8BIUQQgghhBBGiqELh3vJS14yZhuuuUUWWURSCe+QpKuuukpSXagAV2kt0Y3wGw/bIgwOV723UVEdt7wnifLb7s4jzGQYwpM8JAE3JW5UD9WiP0j29crB9AcuUw+18HCaQQe3sDRWmtndyDVXeK+wq4lUa65Vea6FifG9ddddt9k2DOMNauOi5nInnJL+9+vC97siAFKZj4MuzLHddts1n0lIJcm3dj4eXtor5I8wBPZFUqpUwhAI9XTRD8YV3/EQw9VWW20CZza8EJK13377SSohwFIJBeyVQM0897lcC0Phnnb77bdLas9vQn+migUXXHDMNs7J7wkesgKcH/dmv1cy7hAmqoW5cD8lDFAq9236zNfI6UykHgToTx8zCJQwj718BfffxRdfXFIRVpHKPa1WSoF91BLb+8Vkw5xqaxoh4Oecc46k3qHgvcLVPHVh+eWXlzT+55RhCIODWgkP8HsLfc0c9DbuEWxDxEUq6wX3WASOHA/RZk6zRhDaKUn77rvvuM5posQTFEIIIYQQQhgphs4TxJvkz3/+82YbFive3g844ICmjSRVt2Tyfba5Vav7tu/WFBL9eVv1N168PYgguPUd661bw5CjHQbLvEsVcw5YBjxBDs/aFVdcIan9Fk9CnF8HcBGJQWellVZqPne9N26Vwxvh1uCud6eWmFnzBHUL1PrvYH2pSZaDW/2GiVqCf62tK0/vFkIsUHiQfQ5iUR50YQRPQmfNIYH8Zz/7WdPG+dQ8WySRYymVyvjFIu99jJRzzRO07LLLto6lJmziXoobb7yx1+kNPDXLLvcfinTW1rBa4nZXGKY29txaSr9jLXUBALdWTwV4oaRyrUmMdu8fghrunWUdwivh9xAk6ZGtdwEd+gUPkHt1ub9wT/akd0QW/BhmqpSxVPcaIsn+k5/8RFLxVkpl3PGs454dhFfOPffcZhvCKxTA9ELv/RBL8Ou63nrrSZLOPPNMSW3PYjfp3q9p7foybk4++WRJ7TIRE3nWYmxK0ne+8x1JZQ66PLSLc8yJ8QowzGu6x4aYhlTmHOucXyPuHzyL1O7bRE/5OsD18N9lTeP+44Iz8QSFEEIIIYQQQh/IS1AIIYQQQghhpBi6cDjclO5OJemPmg3XX39900ayYK0+CK5hD0nge7jo/e9wtSO84OIHhEfgMt16663H/J5rpBOmNwx4uGB3m7uGcX3SB36NCNEhdMkT64YpHM4TvwkDqolEjCccrkbtO4SI1MK32NYVAJDKNVpllVUe8XcHkVq9Ac7Pw1YJF/NQN6CvmM9+jXz/g4yLupAgT9iAz6NagjrhVITP+fiiLwlHqNW5Yt6SHCyVRGbmvie2sl5uttlmzbZhD4frBWuXr3X0QXd9kEq/8m9NGIH7mFRCRn71q19Jqgv1TBUeOtkNNfVxBy5OwHHSBzUhHM7Tw+HuvvtuSaV2kIec+71Gat832OesWbOabf4cMIyMp97MNtts03zmHkCYoF8/rhv3BO9zxp2LThEKeeedd7b23S8IgZNK3R1CaD3MsVuX0EN9CenzbYyp2jMFv8Pzm9eHpA94jvPxfeGFF0oqIV2bb7550+bhXcD9iH342nnZZZeN+f6g0A2x9GcdxmK3VqZUzpO1zMOjuafce++9ktrPLrU0AkJr+T0PZ9xkk00mflLjIJ6gEEIIIYQQwkgxdJ6gW265RZK01lprNduw/iD1eu211zZtyDSTaCWNtVK5NQ5pvlpl3K48qFtHsILxe/vss0/T9spXvrJ1fFI72XnQcUsLFj36x/ugm5Dt0pf8Xc1r4n086HjS8h/+8IdWG9Z2qYwxT0CteSomAtYXtwxi+cKa4onyfB9Z1GHDrU1di2DN61GzVmJ5qkneMyZrScaDAInNngCP5ZE549bMmjeQ+eYCL8B6Rt/6nFxooYUkFSl//3u+h2fbrXt8HkTvYy/L+kSrvPP9msABn7siCNLYsVa7Vssss0yzDREBLKRuGa3JzfYT90CScI7QgXu38NB45feu4IN7Xbn3MV59/Wcssi+PtgDuKz/60Y+abVjp3Zsx7J6gXmPxoosuktTuc7wYCB34/ff++++XVK5VTRrZv8+6Mp7ohcmwzjrrNJ932mknSUVIyaX6uX+yVuMRlYoAhHtOSaRnHJx11llNG94Ixph7ixjrrLnu1dx1111bv4NQgiRdeeWVktreK9ZovLgu816LqpmX+NrUvX+uscYaY9oYI35vZh9sq3mJmcf+HM7zt/cJ96T555+/9X9J2nDDDcd1ThMlnqAQQgghhBDCSDF0niBkFPlXKpZut4oAFii3yPNWylumvw3zmbfT2ltt14IqjX2LPvXUU5vPeICQbZSkhx9+uHZ6AwkWF2lsTHgvGWPP2+A68Pf+d27RG3Q8th1rGpZNv6b33HOPpCInLBUpScZNzQrclcr238Ri6p6BSy+9VFLxhnqhYH5vWHFPW83LAXiFalZjrFNIC/c7tn0qIW7frehYaLFc+jljWfNzxNpJX9Y8R1j3fD2jn1lTvd/pS/btHlCOYTzSsYPERKVra/HsXSbqYaRf3ZuB9fOBBx6Q1M7l8IiHqcA9TRwbVnofY1i+fb1h3rHNowTIIVtiiSUkta3DWPrxFBD5IRVrO2Pec144PoqsDiI1b+NEPZBnnHGGpDL/3YtGX5NH5d5bttVynRmnnsvB9WLt8by+fljka7kxrE1+j+XZgOPwdYXx4J6EN7/5zZLKvdiLm/PMUpPKpo1x6yVYeNZkm3tq8ax5bhDlTzhmPz7f73TRa4zVCq/TFyuuuGLTxhzt5if79xlH/jzNPYLnb5/rPB+6F22FFVZo7QsPpjR10VPxBIUQQgghhBBGirwEhRBCCCGEEEaKoQuHq4GMYw3cop5IjdsPt12toi/uYncbAy5TDwnohre98Y1vHP8JDDie2EfIG/3k4YLd8I9a8iV9598lYXGQqVVn70rUenVjwuE8lLAWRjkR6EMPiUECnnA4r/pdq0bPeQxDOKaHV3ZFJWry1oR1+Xdxw3OtPBwMcO1PteTwROE610L4CF1hnEklJMPD07p94vOuu9Z5W1fYw8NQCMEhDNnHEsfgIV2DwkRD3iYSqlS7h9SSymkjlOs973lP04YErI9R+pNwJJ8T3/ve98ZxFpPHxxHHxH3U1xbGmB8bIU2MLQ/bZMzSP4SySUUAAoEXv/ewL8L0vJ+YD57QPigwl2rzmH7yUDTYfffdJUlbbrlls42wSMLEPOSN64CMvkszd9c9DznjM+ISUpnvHJc/P3HPmQyEPtVCRTkOX9v5Xnf8eZsLeBx//PGSShjvBRdc0LSdf/75korAgfcB16gmef3FL35RUglr+8Y3vtG0EdLlx8W+Vl11VUnSzTff3LQh/T6d1NYjtnlfM6cJKXQRCsYB3/fxxPky/vzadsWH/NkFGXaXvmf+kkLi13aqnlniCQohhBBCCCGMFEPnCapZ57qFFB3eYP373e/5GzJvui4XCbyVYtFxS4LLX8/pmJ2aZXYY6IpJeB90hRE8IRAre9dqIE3cQjsvQObc6VrvPOHSJdJhshLZjDdPRoQPf/jDkqQDDzxwTFttbOExGgZPkNO1orolmvNkLLrVuSsq4ZZTtjHnu+N3XoPF2z2HJCxjUXVLGVZJP3/mVi9pVry5te8w5rzfsObxfbeeMic82XVQmGgS+kT+rtZW27bRRhtJkvbYYw9J0nzzzde04TU+55xzmm3dsgO+hniS8VTgnlE8EPyLdLpUvH4+RrDEM8d8nK677rqt36l5INiX/w7zAG+RC6ewrSaONK/pyvr7va/mAXrd614nSVp77bUlSWeffXbTRgFLxopfIzwc9JlLQDPH6cNalIavscxxvudegbnxZnCMXmAZGFtLLrlksw1BB7ySfk6ciz97HXbYYa2/8++zLrLe17wgzC/3fB900EGSyhj2Y+e4vKgv0uw8D9Q8pPOC2nrk4lWwxRZbSGqvPfwt461bHsTxNarrcUI0Qiprgwuq0O+MRRc6qQla9IN4gkIIIYQQQggjxdB5gmpvs71kb2v5F93vu4Wdt3asNp7zwm/z9245rcnz9jrmYfMAARKpWD68f7pWLe8f+hOr6rBJ6Lr0NHQ9fG5ZIm7arRy1ArNzouY9ZP9eEJXfOeKIIx5xn9Jg5mqMByxdefoOAAAgAElEQVRo/Ot9iNWYvva2rrfHY5LJd6l52AYB8rf8mJFYpc0t33fccYekdvFACpqyrrnFkrWRNreaMl+x/Pn6Rn9hGfU1AAuhS/d25ZVnEr28RBT8o9iiVIrQEvv+7W9/u2n72te+Jqlt/VxuueUklWvj16/mRegn7s3rSuP6fbVbOFYqaz/jwddGrM+MO8+3ZL94S3xsMSZr8vD8tucXzAtqecbc62vRJeSBeRFc5s51110nqX2eeBnoA5cNJreWsVWTrge/foxd91h0r5t/3z2VkwXPnYOHxctKMN5rOUHcC5hnUul/ztc9ZXis8XB4/hjrKL/nXhCuH2PaPZd8zyMTOFbaBrFURW3dwsuIPL1fZ9YC5n8tn6omcU7f0dee48c+XYqbewTeRr83e25VP4knKIQQQgghhDBS5CUohBBCCCGEMFIMXTjceKiFfLi7suvar8nGgrfhmufv3X3cq5rtZBNyBxHczPSn90E3rMhd8Ljv6c9aQt4gU5PI7rrcDzjggKat60KXxlZWdnrJ6hLehbvZkxIJV/jYxz7WOpY5/U7tPAaV2vEzxmrzmbC4WvIp4Q4elsO+JitYMdUQgukhQcw3QmYIl5JK2IWHayy22GKSShhdLQyVPvG/4zN96WF3JOSzziJ1KpXQPQ/9WWKJJSRJV155ZY+z7Q/d+eNr7kTX317fr81TeO5znyuphKr6/YhQN5KPvbzDDjvsIKktMMAaQzicj9+anG8/8XAqQixZX/z8SdJ32VzWPcakz8lrrrlGkrTgggtKap8TIaq1xHnOl/HmY5l9TKdEtt/v6I9eMvtIJu+8887NNkJFkWGWSsgXSeR+HZj/SKYjdCNJSy21lCTpvvvuk9QOE+uWS/Cxw/2ktqZyTf35ZrIlHqQyjmqJ9bXQSWA81ULJPVyYcCquja+dfGb/vZ4J/Ri4pvSZj+Xa8fC3jFPCGqV6GGA/qYkSjFe0Za+99pJUQtF8zSd0jT6srauMC+8T7uH09ezZs5s2JMvXW2+9ZhtrwjbbbCOpLchx9913jznmfjCYd/8QQgghhBBCmCJmpCfILZq8xbtXwi0HXbpSgE634JN/hzdkrIBYXmcavNFj+XJrfdc66t4erklXZnxYIKHZ6VpTNtxww+YzFlC3FnLONc9gLwt2t6/duoVHYP3115fUtgTVJJ/dOjjo+HliZcLKhsVYKudJArH3Af3DuHVrNfN4XsqW9oK1xI9vhRVWkFSKbXqhYcaMW8iRQq95vekn+gYRBanIztJfXtCSfbBv71O8GO4Jcs/GVEMfcL6+zkyFJ559upgB1wgLvt9vKGBZ83JiJfd7B5+RPe5KZk8l3nesZy6eAXgu/L7bLartVuWVVlpJUpnTjGVp7Hz1Y8BTgLXfxTfop15S8P2ml7iRF0vnmnPvc48ocwgLuFTKMXDuFEaVpJe97GWSpFVWWUVSO+kezwPesPvvv79pY/7WirNi5fd1hns3a4QLEsyNkAxruq81wPrt/cpxdKNw5nQceF1r6z1jkH362GLcsH+fs+yj1k+Mb5eA747hmldpMownoqjXc1WtWCprlVSEES699FJJ9WeFWsQKn7tCO348jOk999yzaauV9aC4LaUE3Avqhef7STxBIYQQQgghhJFiRniCum/GbpHi7bRX3H/NosDfeRufsaJ4bCxx8lhjZqonqOuxcMtGN5bX+6ebm+UWmmHAZYehW4jSrZA1K1CvArnd8dkrT83HJFYnPCNukapZjOa1hOxE8L7rxnPXLF5Y+rzv+NwtWCiVPh/UnCA8Om79v+eeeyQVq7hbkDk3l1DHuso+3ALM+MCy6XkC9DOWVZfp7Y539/pgtabQq9Tb894PfD5wvWtzjGNijb7ooovG/N14oe9YF9Zaa62mDcs/12PLLbcc17Fj6fTij7SzXk5nQV9fz/jMOPLjxvPga3rXou73AvISiff3+cf3a2slY5F9+Vhmfk/nXPZxvfHGG0sqXi5fZ8jRIafB12jyeHweMx/x3rg1nPsvlnXPneLv2JcXxMaDxHc814I1pfasw7rr0sb9GIPdHKU5tdGP3OfcC1PzHE0kz9PvIZwT49yPobtP3zf7cA8p+2Cc+7yYm1zoiXqyu97/2t8fe+yxzWfWHeZZr3XV+4AxyXz0CA7GLpEqNXn1WpkVnmf8Gk1VPtVg3v1DCCGEEEIIYYrIS1AIIYQQQghhpJgR4XBdXvKSlzSfuwlv/hlXm7t3u23uquu63D0UDBf3dEp0zgsItZlvvvnGtHmiqtQOp+gm9w9buCDXtebG77qDpRIiVHOd1/7fTTisua4Zp/47fCZswF3Y3bHs5zEM1KpSkxTt/cr3aqFyHpYitUNYCDeZ6nCtycJ5eQgqaw7n6KG/hN34+fO3tWr1jDHWOO8r9kH4hofD8Xf0n49H5ndN5naqqIVGkmzvoUfIhbPNx1AtHG48iciEM62++urNNgQtXve61435PteUdcT79dWvfrUk6Ywzzmi2saYy910QZKpxaW/6jOP1PiG0ymWPCZsjjMrFMZAFZ+z6dSBhviuEIpVrRNgr/ezHMx3hgjxfvOENb2i2IYzB8bs4CfdBwiSXWWaZpo3+8XAqxgjX2uflDTfcIKn0i4e8sS/uAX4MXEtCijbZZJOm7d3vfrekdmidP/f4780tPD8wFyXprrvuklSXgec4fJ0Dxlhtvavd+9hvbf4D48fXy67Ms6937MPnCqkR3FcoESBJ3/ve98b85tzAPOkl5AUIO0jS2WefLakdHo/IDts8hJV9MbZ8/tMvXCt//kZs4fvf//4cz8H7ExiLK6+88hz/rl/EExRCCCGEEEIYKWakJ8iTcrFS+Vtt9222ZlXm7dbbsCRgJXCvAFZRChw6M6FIKnS9Z25d6ApM1CQZsRZg0R8Wat4/zpexVSuY6xalbvGyXnKW403w7Xox3BJak6zsVeRx0HBPEJZkzsX7jj6oFXIDLOo+Rnsl5w4CXCv3sHbHnFv76BuXI2a8YmXtJVbisrUk83INasXx+Nf7kd9zK7TLR08FteuNtXfHHXdstu2xxx6Syljy466VNui1biOIgEfcLeu9zrc75vx6YPn3OdotSTDVBVId9+xwX8NqW/Ni16zRrGN+nng2GGM1QQV+x+drVyik5kHqen6nAjwuBx100By/49eQOcS51O4J041HcnA8fk27BVH9Oiy77LKT/l281V4gE0EQrqt7ErinMg+uv/76po1134Ujusft9z7WQLbV5LYZP+7Zga4XXqoLBdx+++2SipfOz2du5MVrXp9ektvM2Q984AOSSgFSqXgEEcWQxgpNuIeGZ99uYVT/3qKLLipJev3rX9+09fIAQS8BhpqUer+JJyiEEEIIIYQwUgytJ6j29ogkoXtjiM/0N3XenrHWePwrVptuDKRU3n6xnLi1B4+Tx/uO55iHDSwyXYu0NNazUSuaRZ/NjUVkXjCe/IBLLrmk+Xz44YdLkmbNmtVs6+bjuBcH6z37d+s/Fixiy936c+ONN0oqFu8VV1yxaWPM+zgdpn6vxSTXck6Yj8Qyu6R0V1rVvSTzQlZ3IlAg04+ZtQpvg68jjC/+TirnhnfcCy8CfeN/h3W1lu/WLWDo3g08JF5cdckll+x9olPAD37wA0lFDluS1lxzTUlFGhvLpVSstW657MrZcn+RpJe//OWSpOOOO06StMYaazRtvfL7urK1tTHu6wTWZDxU0+HpgHPPPbf5zFrFfPJ7LF40z2/Cks4c8/MkH4R7pq+D7MMjDID7CePNxz5rwFQVVHRqeZVc6/F4lz1PjfnpY4R5xbysRarUogm6XhC/T9DHN998s6T2fYLrV8ulZB+9CntPBPrnrLPOaratvfbarePn3ikVjwXnTQ6eJG211VaS2vcCvKk17zZjivuony+fOc9e8uv+vEj/eN+zdt52221jjsE95BOl5vXhPoD3hbxCaexcuvXWW5s2rqH3XVfi2ucl8wsvukusk3+51157SZJOO+20McfZq0B8LSJmOj2kg3n3DyGEEEIIIYQpIi9BIYQQQgghhJFiaMPhahCu4K5JXHq1xN6aNGw3RK7m+mRbLckY16AnHuKi75UIPyyQmItbtCZ+AC772U3SH/Sk9C61MLJuMqVXQ/7mN7/Z+neqmX/++SW1K9fXktoHNfSrhs9Lxg3hER5SwnnWEt5pI4zHXfx8zwUYBolauBDhC6xF3keETnWl6qV6uA4J6cxNl18mFKwbOiwVyVeuhYfrEVJRk0ueDrrhZt/61reaNkLXXvGKV0hqh3RxnosvvnizjXOhn/ycjj/+eEnS3nvvLUm69NJLx3V83XuAXyukgu+///4xx+BSyNOFJ/ATKonEsY8jxk9NNpd9+BxjXDOnPVyI+wtjykUTGIOEMxF66fg9Z6ogNNHnRDdUyp8NumvuI4X/dMUnat/vhm/58dREfLqhcl/96lebz1wrQuV8/+zDQ0OvvfbaMcczUfy4L7jgAknSrrvuKknabbfdmrabbrqpdWw+zz74wQ9KasvTE2rJmvPLX/6yaWNNYCz6mKR/mOO1575ezyy+BvKZOeN972vIROH59ogjjmi2LbTQQq1j82NkHNEH/gzDZ7+ubKuNN9rYl4cSn3/++ZKk/ffff8zf9UojYFstTWQ84lH9YnieiEIIIYQQQgihDwytJ6gmLEBya01S1hPAehVU60p61go58Z2atCeWMk+exBP0SMX5hoGudHhNKhVqcs01Oc5hgHPxsdVNEK0l5bpFqTaWJgK/5xYs9llLuORa+XWYjmKC/cKT6zkXrMieOM05Mcdrnh2sebWif17Ud5DAQu6eFI6Vda0m6eznzzqDJH3NUlizunUlUb3fGIe1BOluAq3UlmHtJ29605skteX2OQf64O67727aPvShD0kqkrEPPPBA04aV1fuAxOJawUaSgN2iPhm84CdywO4JYg4w3imWOR1goZdKQjrJ1VigpSL+4t4q5hvjszYmGT9uHef+iTXdPTvsk+u23377jTm+fhej7IXf7zinQV1Luus+wiGS9MlPfnK6D6caQYL0thdt5blho402ktT2EvG9k046qdmGhDZr5rbbbtu04WXkHuLHwO8wx3sJCHW9al26hZqvueaapq1WtHq8MOdcvptnDtYJ90hxLhyvS3V3xVv8c02woOtldA97rSh093dqESg10ZSuJ2hu+mu8xBMUQgghhBBCGCnyEhRCCCGEEEIYKYY2HK4GIRg1V6aHn9VCYqCbjFjT4Web77ObEFmr5zBsIWA1cLsSSuPhNZ6sLrVDILru1GEKy5JKGIjXKQCqYNdCY/opAIGLuLbPbqK2VBevoJr1MOAhpSRwEo6IEIRUhAMILSLB3GG8ei2TrmjCoOKhBMwxwgRcNIG5WQvT4fx9jrKttmZ164H5MSCgQNiHhwCxf5/7U1Xb5vLLL5fUrm/EuCCMykPYuiFafty1enJsY90+5ZRTmrbxiD2Mp9aFf2eTTTaRVGoQSeVeRT2QXXbZpWm78847H3H/c4Mnym+33XatNq+ZdO+990pqhxfSZ3zPxwChfbX7IWORce1rHeObcMEzzjijafPPYfCpzY0zzzxTUgl9k0p45B133CGpHVrL3Nhggw2abYhlnHzyyZLaIWCrrLKKpFILzOcwzyOsdz7uuiG2vubyfV/vOEbWGxdMmhuuuOIKSdLs2bPn+B1/9mV9Zg76vY++IwRXKms3c9BDUVnzEaqoPUeMRwRhvN/nfl+ra9dv4gkKIYQQQgghjBQzyhOEUIG/4fOW6W+1WPt4M/a3526FZE9m71bsrVmyeJv2BLWZRNfb5kltLgLg35HGVl0eJqlmSfrEJz4hqZ0EyLX2BN0uU1H5uNc+P/3pT4/57J6Oj3/8430/nqnCrXGcQ9cqJ5UxiHXLxyQW6FqV+UEfi4hdeOI/x89a5AIE9JEnk/J9T6YF+gvLpvcN/cvv+HpGgu7VV1895hhqye5uOe0nJAW7IMl4parnFd25ixdFkpZccsnpPpxx05UedxEErO5bb711s+3BBx+U1BYPgu592ucf4xRvkc9lxus73vGOMfvsjldpeqvOh/HRy/qPwIELHSBjj0cXCWypjCP39j7nOc+RVOaVlyoZBXwNxzPrHtqppNd869VWE4w69NBD+3JM42Ew7/4hhBBCCCGEMEXMKE8QOQFYA6SxBQGlsZZftx7RVssroq0bL+9gefZjAD+GYSsWCpwzMbQuVdyV/XaJ4658sVtvhgHyA9yqjSeRYmFOTUJ8Kuha1i6++OKmDauqHwMFLocB72v3hkhtTxDWPrwV7oXoFpHr5q1526DBeua5Ucw7vK6eh0YRUM+XQkaWtcc9OuSyEX/tuWN4lbDSeX8fdthhkqQDDjhAUtvLftVVV7V+Tyox7PNCijf0h14lHcgXYo2UpNe+9rWS6hERrJvkb7glmHnO+P7xj3/ctB100EGSSi6YM6hzOLSZqHeOMcW/5OaG0C/iCQohhBBCCCGMFHkJCiGEEEIIIYwUMyocjhAMd6/Xkp8JzSJMqPb9Of1fKiF2LrVNmArfJ6HPmQkue2RTkWz1PvBwCEm67LLLms/Pe97zJJUwh2GSanZOPPHE5jPjwCtcz2tcHvrLX/6ypMGXgJ4TnvDOfGK8ebInIX7Iabp874ILLihpbHiXQzjYoLHvvvtKqlfUJuzs7LPPnv4D67D77rs3n5G3RdRBkj760Y9O9yGFecBee+015jOhmS7M0Q0rr8H8Rpr3keiVcB9CCHMinqAQQgghhBDCSPGo/8R0EkIIIYQQQhgh4gkKIYQQQgghjBR5CQohhBBCCCGMFHkJCiGEEEIIIYwUeQkKIYQQQgghjBR5CQohhBBCCCGMFHkJCiGEEEIIIYwUeQkKIYQQQgghjBR5CQohhBBCCCGMFHkJCiGEEEIIIYwUeQkKIYQQQgghjBR5CQohhBBCCCGMFHkJCiGEEEIIIYwUj5nXB1DjUY96VN/3+fKXv7z5/NnPflaSdPDBB0uSzj777HHtY5111pEk7b333pKknXbaqWm77bbb+nKczn/+858J/81U9N3Tn/705vNTnvIUSdLPf/5zSdK//vWvMd//97//Pcd9cXxPfOITm21PetKTJEkPP/zw3B/s/2de990TnvAESdJhhx3WbHvlK18pSXr3u98tSbr//vubtl/96letv/f+WXLJJSVJSy21lCTpNa95TdN2yCGHSJLOPffcOR7Lf/1XsXX0ujYwr/uuF+94xzskSbfeemuz7corr2x9x+f6fPPNJ2n8c3xumWjfTVe/DTqDPOYGnUHpu8c//vHN57///e+SxrfeTBWcY6/+GZS+G0aGoe8WXHDB5vP73vc+SeV55vvf/37Tdvjhh0/rcQ1D3w0qk+m7XjzqP/3eYx+YiofRxRZbrNn2tre9TZK0/vrrS5J+/OMfN21MEBb0P/3pT03b8ssvL0n6/Oc/L0k68sgjm7Yf/OAHkqS//e1vfTv2QZkonJskLbTQQpKkP/zhD5LKS9F44e/8OJ/85CdLKg/r/RiS/e47f5EAbvArrbSSJOl73/te08aYuv3225tt//3f/y1JuvfeeyVJL37xi5u2Cy+8sNX25je/uWljDP/617+W1F7YeRnlBf3iiy9u2l772tdKar+oPvrRjx6zrcugjLsa9JP33fXXXy+p9K8/jPFyuc0220zL8U3XS1CvB7xll122+Yyh54UvfOGY3+NF+/nPf74k6Y9//GPTxvhgfP35z39u2vbaay9J5cXysY99bNP2j3/8Y1LnM8hjbtCZqr7z73R/Y80112w+88Kz+eabN9ue+cxnSpJ++9vfSmo/cP7zn/9s7euXv/xl83nWrFmSpPnnn1+StMoqqzRtz3ve8yRJ+++/vyTpuOOOa9pqL1vd+0mtnzLuJs+87juub+3a/+///q8kae211262XXvttZLKM5qPLe6L22+/vaT2fXQqmNd9N8z0+5Ul4XAhhBBCCCGEkSIvQSGEEEIIIYSRYsaHw+HmJJ9Ckh588EFJxa3u4SOE0jzmMf+XLvWb3/ymabvhhhskSR/60IckSS94wQuathtvvLFvxwyD4jK95ZZbms/kWBD2UgsT63UMnJOHY9HXhDh56M1k6Xff9QojO+mkkyRJV199dbONcyCEQ5Ke9axnSSqhJH/5y1+aNlz0Ndc+Y5Jz8vwWQkt+97vfSSp5R5J0wQUXSJK+/vWvj+s8YFDGncNxk/v0ox/9qGnbeOONJZVQQkLmJOmpT32qJGnnnXee0uOD6QqHIwTNw88+/vGPSyrrkyT99a9/bf2dz1f6lDHkYYSMD8JXyduTpLvvvluS9IpXvGLMcY1nfNUYxDE3LExn3x111FGSpJVXXrnZRsg467hU1ijCKVmfJGnxxReXJG211VaSpG9+85tN2y9+8QtJZdwSsi6VEDnCOH/60582bW9961slSXfdddeEzifjbvLMi75jfZHqawz3B+61/mzXiz322ENSWTt32WWXpu0rX/nK5A62B4M47roh1p5by32GbYsuumjTdt5550kqz8q+NnCf4tnoGc94RtO2wQYbSCrPlFJJMfnZz34mqdzT/fj6nWcYT1AIIYQQQghhpBhIdbipwJN3ET/grd8TOnmLxWLqHp5vfOMbkqRnP/vZkkpC/0zHLQJY9nq9ldeSttnGv54c+5znPEeStMYaa0iaPiWvicB4cOvTBz/4QUnFAvLqV7+6aePzl7/85WYbScIXXXSRpJLILxWPEZYu94Y99NBDkkqfuVofCchbbrmlpJIQKhVxhZ/85CfNtiuuuEJSsdp2k5QHldmzZ0sqHgn3WqAOxzh1Zb1um6vKDTOME/cEYVlzgQO83vQXnjFJetzjHiepzGES3H2/bHP1wpe97GWSpHe9612SpCOOOKJpi5V8eOkltrHqqqtKKpbcSy65pGlDdMjH1q677ipJuuOOOyRJV111VdN23XXXSZIOOOAASUX4RZKe+9zntv5l7ZOkk08+WVIRfHGBmKOPPlqS9KpXvWrMsfdKoA/DRW1s7rnnns1n1nmEYJxe97wDDzxQUrk/nHHGGU3bTTfdJEm6+eabJ3vYQ4mr1xL1hIeWZxnfxjOIi5DhAWY+e78iiOLP5jwH+TPLVBNPUAghhBBCCGGkmPGeoJp1C4sAlqtTTjmlaTv99NMlFeuRS2Tj+cFyNSxW9MlSk7/GmoYluubtqf2/a8GpxfMSKz6InqCa9C8eHepGudQrstnbbbdds43+pO9cTh1J7dqYwrOB58jzM7DQEzu/7bbbNm3EzHtOFwzb2MWSRF888MADTRvzePXVV5ckbbTRRk0b3z/nnHOm5Tini26uj1Qsd0sssUSzjXnG/CPHRyo5acxpn69Y/MnR8PHy+9//XlK9NtUAppiGcdK9V7qHBg8y3m/PO9ttt90kSW9/+9ubbe9973slFU/1Pvvs07ThyTnrrLMklbILUrEik6+7wgorNG3kDjDO3ePL2krekCT98Ic/bJ1XGF56efM+8IEPNJ8/8YlPtNo8T2089zyePfx+Qc6vr6vjOa5hozv//fmPeyu5PXfeeWfThgeI5zeXrsfLs++++475vcsuu0xS+1mQXCN/7u4eX7+JJyiEEEIIIYQwUuQlKIQQQgghhDBSzPhwOEKPPPkKdx/hRcgpSsWtievUXe7dxDp3tc5EXLoQOPdaOBxMNiRm4YUXntTfzSsQISCZ8r777ptjm1RC1whl8jGJW51t7l7n+7iI3X1MiCYhnd/+9rebtpe85CWS2hK066233oTOcVCg2jfhXLUEaOQ0fT6vttpqkqRf//rXU32I85wttthCUltUg/ACtnlIAesf/7p8NmMV8YQXvehFTRvhh4wvlzGdqDR2GBxY01l7tt9++6YNOXTWIA/HRDLdBVsWWWQRSSV08uGHH27aENchvOj5z39+04aQCeE2XoaC0GLCgQ855JCmDYlsD4U9/PDDJY1OiCbzmNAlFzOhpICX/BgmaqFQjAcXyTn44IMntf+utD+iHVIRAdlmm20kTY1k9iDQnScucMB85/mPciZSCeXn+4w/hzXC5zPP1h52y32GcGy/b0/VPI4nKIQQQgghhDBSzGxXhopl3ZPieOuvFfpkW9cyL421cmIllYoVxmVmhx33ggFv491/pbEy2N5W+z5geazJWg4yyLIuvfTSkqTll1++abv00ksltS1vJPcjA+l9wbih73xsYuliDGN58b9DJhTPhyTdc889kkoC8jCD1CZeHpfORZACD4UXZrztttum6xDnCW9729uaz1/4whckFeu7VOSyWatclh3RA9Y4PDsOlju3KvMZ76OLhmAhrAmJzGR8vvK5lojN+P385z8vqV1+AKune/LwoBx77LGSpMsvv7yfh92iuzZ7CYi11lpLUvHEsuZJRZQFIQJJ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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# takes 5-10 seconds to execute this\n", + "show_MNIST(train_lbl, train_img, fashion=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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d2JKD7Zu3QqctaqlJTYoJX788UcaN25M4cKFef755ylQoABvvPEGffr0Yf/+/QwaNIhChQrRsWNHXnvtNYYPH+6ZbKdOnUpkZKRjodq3bx/169fnkksuYejQoZQrV46VK1cyYsQIduzYwdSpUz3nf/7556lYsSJjxowhISGBChUqpOv1pYb27duTK1cuPvvsM2fbiRMn6Ny5M/379+ehhx5yXgzeeOMNevXqRZcuXXjttdeIiIjg5Zdf5uqrr2bhwoXOx1LPnj1Zt24dTzzxBBUrVuTQoUOsW7eOgwcPAnD06FFat25NYmIi48ePp0iRIuzbt4+lS5dy+PDhzO+EIDl9+jRLliyhTp06nnHoXNx555288sor3H333XTs2JEdO3bw6KOPsmzZMtatW0fBggUB2LZtG40aNeLWW28lT5487Nixg2eeeYamTZvy7bffEhERwa233soff/zBCy+8wKxZsyhWrBiQNVxsGzVqxKRJk7jnnnu4+eabqV27tuelSNiyZQvt27dnwIAB5M6dmx9++IFRo0axevXqZK7DJ0+epHPnzvTr14+BAwfy2Wef8fjjj5MnTx6GDh0KnNWMX3XVVezZs4eRI0dSsWJF5s2bx4033pjs3Dt27KBSpUp0796d/Pnzs3fvXiZMmEC9evXYvHmzcy+yGsH2LZxVoA0cOJCHHnqIIkWKMGnSJPr160f58uW58sorUzyP3zNfsmRJbr/9drZt25Yh7p3Bkt59kJrxeseOHfTv35/SpUsD8NVXX/F///d//Prrr44cBmKM4cEHH+T5559n0qRJzgv9mTNn6NKlCytWrGDQoEE0btyYnTt3kpSURPPmzVm7dq3H0rNu3Tq+//57/ve//5GYmJgl3MLFc8EYw/79+3n66ac5evQoPXr0cOoE22d9+/Zl5syZDBo0iJYtW7J582auvfZa/v777wy9hmPHjvH2229Tr149qlWrxi233MKtt97Ku+++S+/evZPVHz9+PJUrV3biXx599FHat2/Pzz//7LxfBvLOO+/Qq1cvbrnlFl544YVzevmkVib8OHnyJO3bt3ee3S+//JIRI0awc+dOJ1beGMM111zDp59+ypAhQ7jiiivYuHEjSUlJrFy5kpUrVzpKvWDmrZdeeintY4O5iPTu3dvkzp07VfvccccdpmDBginWefXVVw1g7rnnHs/2J5980gDmt99+c7Y1adLEtGrVyvn/li1bDGAqVqxoTp486dl/5cqVBjCvv/6673k7duxorr32Ws+2qKgo069fv2R1u3XrZqKjo83u3bs929u0aWPi4uLM33//bYwxZtGiRQYw9evXN2fOnHHqbdu2zYSHh5s77rgjpa4wxqTcz/PnzzeAadOmTbKya665xsTGxpq9e/d6trds2dIkJCSYI0eOGGOMmTBhggGS1ZNjr1y50hhjzOeff24As2DBgnO2denSpQYw48eP92zfvn27iYyMNEOHDnW2NWjQwADmiy++SOHqU8fUqVMNYL766itz8uRJc/jwYTN37lxTqFAhEx8fb/bt22eSkpKM36Mj+/7888/OtmbNmplmzZp56gEmKSnJ+X/37t1NVFSU2bVrl6deu3btTGxsrDl06JAxxpiPPvrIAOaTTz5x6pw6dcoUL17cdO3a1dnWv39/ExcXZ3bu3Ok53pgxYwxgvvvuO2OMMT///LMBTLly5cyJEydS1U9pRfpozZo156xTpEgRU6VKFWPMWdkFzJQpUzx1jh49avLnz286derk2X769Glz+eWXm/r16zvb4uLizIABA855vrVr1xrAfPDBB2m5pIvGvn37DGC6d+9+3rrff/+9Acxdd93l2b5q1SoDmIcffth3vzNnzpiTJ0+anTt3GsB8+OGHTtnTTz+dTN6zAr///rtp2rSpAQxgIiIiTOPGjc3IkSPN4cOHffeR61y+fLkBzIYNG5wykcF33nnHs0/79u1NpUqVnP/LOGj3kTHG3HbbbQYwU6dOPWebT506ZY4cOWJy585tnnvuOWe7jIdLly5NRQ9kHMH2bZkyZUx0dLRnDDp27JjJnz+/6d+/v7PN7/rO9cwbY0yHDh1MmTJlMuTagiW9+yDY8TqQ06dPm5MnT5rHHnvMFChQwPN+UKZMGdOhQwfzzz//mK5du5o8efKYxYsXe/Z/++23DWDef/99z/Y1a9YYwLz00kue4+XKlcv8+OOPqeipjEPmkcB/UVFRnnYHcq4+++677wxgBg8e7KkvfdS7d+8Mu5bp06cbwEycONEYY8zhw4dNXFycueKKKzz1ZL6uXr26OXXqlLN99erVBjBvv/22s81+53vqqadMrly5zKhRo5KdO/D9JDUy4Yc8u/YYZowxTzzxhAHM559/bowxZsGCBQYwo0eP9tSbOXOmAcwrr7xijEndvJXWsSHLusPJF778M//fdFm/fn1+//13br75Zj766CNHm+tHYDICSWawa9eu856/S5cuqbIqHD58mEWLFtG1a9eg6i9ZsoQ2bdpQokQJz/bevXtz5MgRVq1a5dneo0cPj3nv0ksvpUGDBunmuuTX7iVLltC2bVuKFi2arI1///03a9asSdU5KleuTEJCAgMHDuTVV1/lhx9+SFZn7ty55MqVix49enjuf6lSpbjsssuSudsUK1aMxo0bp6odwdCwYUMiIiKIj4+nY8eOFC1alPnz558zTuVCWLJkCa1atUqmze/Tpw///POPk+iiXbt2FC1a1KMZXLhwIXv27OGWW25xts2dO5cWLVpQvHhxTx+2a9cOgOXLl3vO07lz53NqMi8Gxse1I1A+v/zyS/744w969+7tucYzZ87Qtm1b1qxZ41gX69evz7Rp0xgxYgRfffUVJ0+e9ByrfPny5MuXj8GDBzNx4kQ2b96ccRd3kZBxItCto379+lSpUsXjQvjbb79xxx13UKpUKcLDw4mIiHCszd9//32mtTmtFChQgBUrVrBmzRqeeuopunTpwk8//cSQIUOoXr2649a3fft2evToQdGiRcmVKxcRERE0a9YMSH6dYWFhyWIMatSo4XFfW7p0KfHx8cnmHVsrLRw5coTBgwdTvnx5wsPDCQ8PJy4ujqNHj2bpPg62bwFq1qzpaN/hrKtSxYoVg3b5C3YuzWzSuw9SM14vWbKEq666ijx58jgyO3ToUA4ePJgss+bBgwdp2bIlq1ev5vPPP/e4Ect58+bNS6dOnTznrVmzJkWLFk0219aoUYOKFStecP+lJ9OnT2fNmjWsWbOG+fPn07t3b/773//y4osvOnWC6TPp4xtuuMFz/G7dumWYd4kwefJkYmJi6N69OwBxcXFcf/31rFixgi1btiSr36FDB48lR95rA58rYwz9+/cnKSmJt956i0GDBp23LamViXMRGDctY6DMQ2JpD5yPrr/+enLnzu3MR6mZt9JKlv0IKlOmDBEREc6/J554AjjbGZMmTWL79u1cd911FC5cmIYNG/p2hu02BTjmtWPHjp33/OLeESxz5szBGBN0Wso///zT9xzFixcHSPZxF/ghIttS+ghMDYFtOX36NH///Xeq2ng+ChQowPLly6lSpQoPPvggVapUoWTJkjz++OOcPn0agP3793P69Gny5cvnuf8RERF88803ngnGr93phQyu69evZ8+ePWzcuJEmTZpkyLkOHjwYVD+Hh4fTs2dPZs+e7fjNTps2jWLFijnumXC2D+fMmZOs/6pWrQqQaX2YFo4ePcrBgwedaweIjY0lISHBU2///v3A2Ukq8DpHjRqFMcZJDT1z5kx69+7NpEmTaNSoEfnz56dXr15OrEaePHlYvnw5NWvW5OGHH6Zq1aoUL16cpKSkZB9MWYmCBQsSGxvLzz//fN66IkPnkjMpP3PmDG3atGHWrFkMGjSITz/9lNWrVzs+58GMnVmFunXrMnjwYN5991327NnDfffdx44dOxg9ejRHjhzhiiuuYNWqVYwYMYJly5axZs0aZs2aBSS/ztjY2GTJbqKiojh+/Ljz/4MHD/oqSfzG7h49evDiiy9y6623snDhQlavXs2aNWsoVKhQSPRxSn0rBM6/cLbPgrk+v2c+q5FefRDseL169WratGkDnM0I+cUXX7BmzRoeeeQRILnM/vTTT6xatYp27dr5pl3ev38/hw4dcmJo7H/79u3L0vOEUKVKFerWrUvdunVp27YtL7/8Mm3atGHQoEEcOnQo6D6T8S/w+Q0PD/e9h+nF1q1b+eyzz+jQoQPGGA4dOsShQ4fo1q0bgG/8WLDvtSdOnGDmzJlUrVrV+aA+H6mVCT/8+kzGQOnngwcPEh4eTqFChTz1wsLCPO+1wc5bF0KWjQn6+OOPOXHihPN/sZiEhYXRr18/+vXrx5EjR1i+fDlJSUl07NiRLVu2ULJkyXQ5f2oDLt9//31H2xAM+fLlY+/evcm279mzByCZT7hfcO2+ffvS7QENvN5cuXKRkJAQVBvl5SAwSNjvgalZsybvvvsuZ86cYcOGDUyePJmhQ4cSHx/PgAEDnIDTzz//3NdvNdAfNaMCY2Vw9cO+XjsYPZgBwo8CBQoELQt9+/bl6aefZsaMGdx444189NFHDBgwwNNXBQsWpEaNGo7iIBD7AwMyrg/Twrx58zh9+rRn7QK/9kmfvPDCC+fMLiUTWsGCBXn22Wd59tln2bVrFx999BEPPfQQv/32m5M5rXr16syYMQNjDBs3bmTatGk89thjxMTE8NBDD6XzVaYPuXLlolWrVsyfP5/du3enOPbJOLF3795k9fbs2eP056ZNm9iwYQPTpk3z+KNLTFyoEhERQVJSEuPGjWPTpk0sWbKEPXv2sGzZMsf6A1zQmkcFChRg9erVybYHjt1//fUXc+fOJSkpySNb//77b4at6ZSRBPZtepCVxqRguJA+CHa8njFjBhEREcydO9fzQf7BBx/47teoUSOuv/56+vXrB8CECRM8saQFCxakQIEC58weGR8f7/l/qNyTGjVqsHDhQn766aeg+0zGx/3793u8c06dOpVuimY/pkyZgjGG9957j/feey9Z+WuvvcaIESPSlKlXkhxdffXVXHXVVSxYsIB8+fKluE9qZcIP6TP73VTGQNlWoEABTp06xYEDBzwfQub/pzqvV6+ep/755q0LIctagmrUqOF84detW9f3SzAuLo4OHTowZMgQjh8/nuFuLOf64v7nn39YsGCBr/n+XJqvVq1asXjxYkejLUyfPp24uDjq16/v2R6YkW379u2sWrUqXRe68mvjwoULk2UFmT59OgkJCc5HQtmyZQGSLSKb0iKxl1xyCbVq1eLFF18kJiaGdevWAdCxY0dOnTrF/v37Pfdf/ol27GJyruuVoL/U0qpVK+elzGb69OnExsZ6XvKrVKlCgwYNmDp1Km+99Rb//vsvffv29ezXsWNHNm3aRLly5Xz7MPAjKKuwa9cuHnjgAfLkyUP//v1TrNukSRPy5s3L5s2bfa+xbt26REZGJtuvdOnS3H333bRu3dqROZuwsDAuv/xyxo0bR968eX3rZCWGDBmCMYbbbrvNozQSTp48yZw5c2jZsiVwNpmEzZo1a/j+++8dVxl50QnMNCfZ+mxSY1nPTPwUCuC6uBUvXjxV1xksLVq04PDhw8nGvcCxOywsDGNMsnNPmjTJsYhnVYLp24wkWEtSRpLefRDseC2Ld9svxMeOHUuWUdamd+/ezJgxg6lTp9KrVy+PfHXs2JGDBw9y+vRp3/NWqlQpVdeRVfjmm2+As0mMgu0zSVIxc+ZMz/b33nsv6OVRUsvp06d57bXXKFeuHEuXLk32b+DAgezdu5f58+en+Ry1atVi+fLl7N69m+bNm593MfL0kok333zT838ZA+V9VeabwPno/fff5+jRo055sPMWpH1syLKWoHPRt29fEhISaNKkCUWLFmXv3r08+eST5MuXjzp16mTouStUqEB0dDSvv/46FStWJHfu3JQoUYIvvviCEydO0KVLl2T7VK9enSVLljB37lyKFi1KQkICFStWZNiwYcyfP5/mzZvz6KOPkjdvXl5//XUWLlzI2LFjk31x7927l+uuu45+/fpx6NAhhg4dSmxsLIMHD86w6x0+fDiffPIJzZs355FHHiFv3ry89tprfPrppzz33HNOdpgmTZqQmJjIvffey7Fjx4iPj+fdd99l7dq1nuO9//77TJs2jS5dupCYmMjp06d55513OHbsmLO4bKtWrejVqxc333wzd999N02bNiU2NpY9e/awYsUK6tWr52i2Lhbt27cnf/789OvXj8cee4zw8HCmTZvmpBVPLUlJSY5f+NChQ8mfPz9vvvkm8+bNY/To0cmsi7fccgv9+/dnz549NG7cONnA9Nhjj7Fo0SIaN27MPffcQ6VKlTh+/Dg7duzg448/ZuLEielmMU0rmzZtcvyNf/vtN1asWMHUqVPJlSsXs2fPTmYmDyQuLo4XXniB3r1788cff9CtWzcKFy7MgQMH2LBhAwcOHGDChAn89ddftGjRgh49elC5cmXi4+NZs2YNCxYs4LrrrgPO+kG/9NJLXHPNNVx66aUYY5g1axaHDh1y5DKr0qhRIyZMmMBdd91FnTp1uPPOO6latSonT55k/fr1vPLKK1SrVo3Zs2dz++2388ILL3DJJZfQrl07J8tOqVKluO+++4CzcXvlypXjoYcewhhD/vz5mTNnDosWLUp27urVqwPw3HPP0bt3byIiIqhUqVJQ2sKM5Oqrr6ZkyZJ06tSJypUrc+bMGb755hvGjh1LXFwc9957L8WLFydfvnzccccdJCUlERERwZtvvnlByw706tWLcePG0atXL5544gkqVKjAxx9/zMKFCz31EhISuPLKK3n66acpWLAgZcuWZfny5UyePDlLLTrrRzB9m5FUr16dWbNmMWHCBOrUqcMll1xyTot9RpHefRDseN2hQweeeeYZevTowe23387BgwcZM2bMedd069atG7GxsXTr1s3JRBYZGUn37t158803ad++Pffeey/169cnIiKC3bt3s3TpUrp06cK11157IV2V4cg8Amddp2bNmsWiRYu49tprSUxMDLrPqlatyk033cTYsWPJlSsXLVu25LvvvmPs2LHkyZPHN/37hTJ//nz27NnDqFGjfJXZ1apV48UXX2Ty5MlBh1n4UaVKFVasWMFVV13FlVdeyeLFi885/6eHTERGRjJ2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7uspc5+7ieMSSL94IpHabdoTGdXlFbw8Aym42d/XedZYd5MHUpW7e+vnPve5CfsNEzxHu8wwVpkbGN/SxDI0fq8o0TDbxBIUQgghhBBCGCtG0hLUwot+1Wk451e71krFOy6az7vuukvSxOKeUulPil/5W38dNIeGcNhwjTgJILi2q6++umtbddVVJfVqQOkftFktLXBdeEwqfV1rSaWinSJo01OZIsut+zAMiTw23njj7jNp19GSttKO0nceSEyfodnzYxKc20oaMOiQet1T5NI3aCClorlraU35jLXMNZz0G9o6T7vN9+644w5JpYigbwv9aVmC0Bhvttlm3TYC4bmPfv+mO/WuzzOcb6t4ZqvAM3PiVe6wAAAgAElEQVQP1kX3xEAr3JqD2IZMuhUFy0bLg4O2mbQEOVhKOG8/D9qwknoaZ6we1113XbeNscaY9XTNWHQZn56ApO4fvx/0ayuFPd/zeZPrYL1fY401urYTTjih0QPzh89bFL/l3Pw8kMG6rIlULGX89XaSJXz1q1/t2lh36V8vussxWE+9lAC/jRXd13S+5/KNRY4+/MxnPtO1tTyShh3krbX+0nf0hT8vurVtNoklKIQQQgghhDBWjJQlCM2Qa5hWWmklSb0afLQgrdgM3mr7pTXle67FIwYEbaqnCRylmCC0S/RhyzcU/3WP26B/BsUPdEFxDR/XhxxcdtllXRs+wn699EE/CxBtLpv42vJ971fkmpTj3taSO/x1+6WZnW3Q1LU0mS0tcq35dG15HdeA9U4qBZSH0RIExN/55y233LLbdswxx0hqWx5q7ar7cnMP0H66z/t73vMeScM/lmcD5LEVJ/K+971PUrHqSmWccq88NmO6U2S3YoKQleWWW67bRsyha+nRBjM/eSxH7Ynhc1adVtxlkmPWabSlyaefnkrcAop1gD7A6iOVMcS85JadW265RVJvbB/HasXdehyL1DsPYmUgxsZjUUgtzVpC0VSpjHViA6WSBptU2h6f+YlPfEILixf6paA91iZfY1nzuOc+f5Mm3AuEExNEH/gYQQb5HX9G4zfpCzwPpGK5I4223w+s7x7jjBcI3i5+Di996Us1qmCl9HXbLWpS73ONr12zSSxBIYQQQgghhLEiL0EhhBBCCCGEsWKk3OFagZZrrrmmpF6TO64ILfe52pzups9+FcAx+xHU+q1v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hRBCCCGEEMaIWIJCCCGEEEIIY0VegkIIIYQQQghjRV6CQgghhBBCCGNFXoJCCCGEEEIIY0VegkIIIYQQQghjRV6CQgghhBBCCGNFXoJCCCGEEEIIY0VegkIIIYQQQghjRV6CQgghhBBCCGNFXoJCCCGEEEIIY0VegkIIIYQQQghjRV6CQgghhBBCCGPFo2b7BFo84hGPmPJj7r333t3n5ZZbTpL0wx/+UJJ09tlnd21/+9vfJEmPfOQjJUkrrrhi17bVVltJkn73u99Jkr7zne90bQ8++GDP9zjOwvCPf/xjvr8zHX3nbLfddpKkV7/61ZKkyy+/vGtbdNFFJUl33XWXJOkvf/lL17bGGmtIki6++GJJ0pOe9KSu7bjjjpMk/fnPf56y85yJvmN/fuuxj31s17bMMstIklZdddVu2y9/+UtJ0jnnnDPf5yZJH//4xyVJJ598crft4YcfliTddttt8zy/yTKIcrfyyitLknbaaSdJpS+k/uf7kY98RJJ0wgknSJKuvfba6TrFeZ5Li+not4997GPd51/96leSpK9//euSpBtvvLFrW2KJJSSVPnVZXWyxxSRJp59+uiTpyCOPnPLzdAZR5oaFYei7pZdeuvv8+9//XpL00EMPzeg5tBiGvhtUhqHv/umfip7/Fa94hSTpKU95iiTp85///IyeizMMfTeoLEjf9eMR/5jqI04BU3mzn/jEJ0qSdt55527bc5/7XEnlZYYXF4cHch4iJGmRRRbp+d4tt9zStV1wwQVTds4wiAPllFNOkSRtu+22kqRLLrmka+PBn/782c9+1rUtv/zykqSTTjpJkvTAAw90bd/4xjckST/60Y+m7Dynq+98H34DuVhrrbW6Nl7yrrrqqm7boYceKqnIzfvf//5JndcVV1whSTrkkEMk9craKqusIkn661//Kkk677zzurYFfREfFLk79thju88rrbSSJOnRj360pPIAL5Ux/ve//11S78s3L+Qshv6yuNtuu035Oc/mSxBznL8kMz433HDDeX7fz/373/++pKL4me55ZVBkbhgZ5L7j3P70pz9121AGPfnJT5bUO14Z5z/96U9n9Pzmh8jd/zGIfVcr/nzeu/POOyVJBx54oCTp3e9+d9fG8x7rBGvJdDGIfTcsTPUrS9zhQgghhBBCCGNFXoJCCCGEEEIIY8VAxgRNJbhmebzG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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# takes 5-10 seconds to execute this\n", + "show_MNIST(test_lbl, test_img, fashion=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now see how many times each class appears in the training and testing data:" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average of all images in training dataset.\n", + "Apparel 0 : 6000 images.\n", + "Apparel 1 : 6000 images.\n", + "Apparel 2 : 6000 images.\n", + "Apparel 3 : 6000 images.\n", + "Apparel 4 : 6000 images.\n", + "Apparel 5 : 6000 images.\n", + "Apparel 6 : 6000 images.\n", + "Apparel 7 : 6000 images.\n", + "Apparel 8 : 6000 images.\n", + "Apparel 9 : 6000 images.\n" + ] + }, + { + "data": { + "image/png": "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o3v37s45ixcvRvfu3ZGQkIDY2Fh06tQJX375pd919+/fj9tvvx01atRAVFQUkpOT0alTJyxevNg55ocffkBaWhoqVqyIqKgoVK1aFX379sXu3buLpvL55LHHHkNYWBhee+018eM1MjLSsUZfuHABkydPRmpqKqKiolCxYkUMGTIkoI6LFi1C//79Ub16dURHR6Nu3boYOXIkDhw44Bwzfvx4PPDAAwCAlJSUkHK3PXjwICpUqOD3kkqUKuWb8mbPno2ePXuiSpUqiImJQcOGDfHggw/ixIkTfudQG9y2bRv69OmDuLg41KhRA6NGjcKZM2f8js3MzMRf/vIXxMfHIzExETfddFPAuAhcfPEZNGgQateujZiYGNSuXRt//etfA8a4UMOrbIkFCxagZcuWiImJQWpqqqP1JiTXuJz6fNeuXfH5559j586dfi5RRY1XGZB7Wm4yALyN1wAwYcIEtGvXDklJSUhISEDLli0xffp0GGNyLfdLL72E8PBwpKenO9vOnj2LiRMnOmNCcnIyhg8fjv379/udS3WZM2cOWrRogejoaEyYMCHXexY10dHRiIyMREREhLPNq8zOnDmDUaNGoXLlyoiNjcWVV16J77//HrVr18awYcMKtdzTp08HADzxxBPo2LEj3n//fZw8edLvGJqvp0yZgqeeegopKSmIi4tDhw4d/N6xcuLbb79FhQoVkJaWFjDGcby2CTc2b96M7t27o0yZMkhOTsbdd98dUJ/Tp09j7NixSElJQWRkJKpVq4Z//OMfAa7WXuatgo4NRWoRCgZvv/027r77btx7773o27cvwsLCsG3bNvz8888Bx956663o168f3nvvPezcuROjR4/GkCFD8MUXX+R6n6effhqpqal46qmnEB8fj/r162PatGm47bbbMH78eFxzzTUAgBo1ajjnzJ07F+Hh4ejduzfCw8OxcuVKdOnSBb169cLYsWMBAImJiQCALVu2oGPHjqhcuTJeeOEFlCtXDrNmzcKQIUOwf/9+/POf//Qrz5gxY9C6dWu88cYbOHz4MNLT09GlSxesX78etWrVyrc8iVGjRuHKK6/EtGnTcOHCBZQrVw4bN25Ep06dUK1aNbz44osoW7YsZs6ciZtvvhkHDhzAPffck6d7HDlyBD179kRqaipeeeUVJCcnY8+ePViyZIlfxxw2bBhmz56N+++/H1OmTMH+/fsxYcIEdO7cGevWrUP58uWdY3fu3Inhw4dj7NixaNiwoTg5BYNt27YBuGhdy0+skBs///wzOnbsiIoVK+K5555D+fLl8fbbb2PYsGHYu3cvRo8ejeTkZKSlpeHNN9/EhAkT/CbcGTNmIDIy0rFUZWVloW3btihVqhTGjRuHOnXqYOXKlZg4cSJ27NiBGTNm+N3/ueeeQ/369TFlyhQkJCSgXr16Qa1fXujTpw9Kly6Nr7/+2tl29uxZXHvttRg5ciQefPBB5+Xg7bffxpAhQ9C/f3+8+eabiIiIwKuvvoprrrkGCxcudD6YbrnlFqxduxaPPvoo6tevjyNHjmDt2rU4ePAgAODEiRPo0aMHUlJS8OKLL6JSpUrIysrC0qVLcezYsaIXgkeys7OxZMkStGrVym8cyok777wTr732Gu6++26kpaVhx44deOihh/DVV19h7dq1qFChAgDg119/RYcOHXDbbbchMTERO3bswFNPPYXOnTtj48aNiIiIwG233YZDhw7h+eefx5w5c1ClShUAoeFu26FDB0ybNg333HMPbr75ZrRs2dLvxYjYunUr+vTpg/vuuw9lypTBTz/9hEmTJmH16tUBbsTnzp3DtddeixEjRmDUqFH4+uuv8cgjjyAxMRHjxo0DcFFDfvXVVyMzMxOPP/446tevj88//xw33XRTwL137NiBBg0aYNCgQUhKSsKePXvw8ssvo02bNtiyZYvzLEINr7IFLirRRo0ahQcffBCVKlXCtGnTMGLECNStWxdXXnml632kPl+9enXcfvvt+PXXXwvF1dMrwZZBXsbrHTt2YOTIkahZsyYA4LvvvsN//dd/4Y8//nDaoY0xBg888ACee+45TJs2zXmpv3DhAvr374/ly5dj9OjR6NixI3bu3In09HR07doVa9as8bP4rF27Fj/++CP+93//FykpKSHhIk4eDMYY7N27F08++SROnDiBwYMHO8d4ldnw4cMxe/ZsjB49GldddRW2bNmC66+/HkePHi3UOpw6dQrvvfce2rRpg8aNG+PWW2/Fbbfdhg8//BBDhw4NOP7FF19EamqqEw/z0EMPoU+fPti+fbvzfmnzwQcfYMiQIbj11lvx/PPP5+jtk9c2IXHu3Dn06dPH6bsrVqzAxIkTsXPnTid23hiD6667Dl9++SXGjh2LK664Ahs2bEB6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vw9jDDZs2ICZM2fi4YcfRkxMDB588MEg1zI4lC5dGt27d8f8+fOxe/du17GPxok9e/YEHJeZmenIc9OmTVi/fj1mzpzp559OMXKXKhEREUhPT8fTTz+NTZs2YcmSJcjMzMRXX33lWIEAFGhNpPLly2P16tUB2+2x+88//8S8efOQnp7u17bOnDlTaGs+FSa2bINBKI1JXiiIDLyO1++//z4iIiIwb948v4/yTz/9VDyvQ4cOuPHGGzFixAgAwMsvv+wXW1qhQgWUL18+x6yS8fHxfv+/VJ5J06ZNsXDhQvzyyy+eZUbj4969e/28dM6fPx+0l22JN954A8YYfPTRR/joo48C9r/55puYOHFivjL4UuKja665BldffTUWLFiAcuXKuZ6T1zYhQTLj76Y0BtK28uXL4/z589i/f7/fx5D5/2nQ27Rp43d8bvNWQQlZi1DTpk2dL/3WrVuLX4RxcXHo27cvxo4di9OnTxe6S0tOX94nT57EggULRFN+Thqw7t27Y/HixY5mm5g1axbi4uLQtm1bv+12prbffvsNq1atCupiWFIZFy5cGJAtZNasWUhISHA+FGrXrg0AAQvNui0kW6pUKbRo0QIvvPACYmJisHbtWgBAWloazp8/j7179/o9f/pHWrLiJKf6UiBgXunevbvzYsaZNWsWYmNj/V70GzZsiHbt2mHGjBl49913cebMGQwfPtzvvLS0NGzatAl16tQRZWh/CIUKu3btwn//938jMTERI0eOdD22U6dOKFu2LLZs2SLWsXXr1oiMjAw4r2bNmrj77rvRo0cPp81xwsLC0KxZMzz99NMoW7aseEwoMXbsWBhj8Pe//91PcUScO3cOn332Ga666ioAFxNMcDIyMvDjjz86bjP0smNnoKMsfpy8WNiLEkmpAPjc3apWrZqnenqlW7duOHbsWMC4Z4/dYWFhMMYE3HvatGmOZTxU8SLbwsSrRakwCbYMvI7XtMA3fyk+depUQKZZztChQ/H+++9jxowZGDJkiF/7SktLw8GDB5GdnS3et0GDBnmqR6iwbt06ABcTG3mVGSWumD17tt/2jz76yPPSKXklOzsbb775JurUqYOlS5cG/Bs1ahT27NmD+fPn5/seLVq0wLJly7B792507do11wXLg9Um3nnnHb//0xhI76s039jz0ccff4wTJ044+73OW0DBxoaQtQjlxPDhw5GQkIBOnTqhcuXK2LNnDx577DGUK1cOrVq1KtR716tXD9HR0XjrrbdQv359lClTBtWqVcO3336Ls2fPon///gHnNGnSBEuWLMG8efNQuXJlJCQkoH79+hg/fjzmz5+Prl274qGHHkLZsmXx1ltvYeHChZg6dWrAl/eePXtwww03YMSIEThy5AjGjRuH2NhYjBkzptDqO2HCBHzxxRfo2rUr/vWvf6Fs2bJ488038eWXX+LZZ591ssZ06tQJKSkpuPfee3Hq1CnEx8fjww8/xJo1a/yu9/HHH2PmzJno378/UlJSkJ2djQ8++ACnTp1yFqDt3r07hgwZgptvvhl33303OnfujNjYWGRmZmL58uVo06aNo+EqLvr06YOkpCSMGDECDz/8MMLDwzFz5kwn5XheSU9Pd/zEx40bh6SkJLzzzjv4/PPPMXny5AAr46233oqRI0ciMzMTHTt2DBicHn74YSxatAgdO3bEPffcgwYNGuD06dPYsWMH/v3vf+OVV14JmuU0v2zatMnxP963bx+WL1+OGTNmoHTp0vjkk08CTOY2cXFxeP755zF06FAcOnQIAwcORMWKFbF//36sX78e+/fvx8svv4w///wT3bp1w+DBg5Gamor4+HhkZGRgwYIFuOGGGwBc9It+6aWXcN111+Gyyy6DMQZz5szBkSNHnHYZqnTo0AEvv/wy7rrrLrRq1Qp33nknGjVqhHPnzuGHH37Aa6+9hsaNG+OTTz7B7bffjueffx6lSpVC7969new7NWrUwP333w/gYhxfnTp18OCDD8IYg6SkJHz22WdYtGhRwL2bNGkCAHj22WcxdOhQREREoEGDBp60hoXJNddcg+rVq6Nfv35ITU3FhQsXsG7dOkydOhVxcXG49957UbVqVZQrVw533HEH0tPTERERgXfeeadASxIMGTIETz/9NIYMGYJHH30U9erVw7///W8sXLjQ77iEhARceeWVePLJJ1GhQgXUrl0by5Ytw/Tp00NqYVoJL7ItTJo0aYI5c+bg5ZdfRqtWrVCqVKkcLfeFRbBl4HW87tu3L5566ikMHjwYt99+Ow4ePIgpU6bkuubbwIEDERsbi4EDBzoZyiIjIzFo0CC888476NOnD+699160bdsWERER2L17N5YuXYr+/fvj+uuvL4ioCh2aR4CLblRz5szBokWLcP31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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average of all images in testing dataset.\n", + "Apparel 0 : 1000 images.\n", + "Apparel 1 : 1000 images.\n", + "Apparel 2 : 1000 images.\n", + "Apparel 3 : 1000 images.\n", + "Apparel 4 : 1000 images.\n", + "Apparel 5 : 1000 images.\n", + "Apparel 6 : 1000 images.\n", + "Apparel 7 : 1000 images.\n", + "Apparel 8 : 1000 images.\n", + "Apparel 9 : 1000 images.\n" + ] + }, + { + "data": { + "image/png": "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BVasWOEo9nyZt15//fWCjQ2mGBk4cKApU6ZMnn5zzz33mISEBNdr3nnnHQPAPPDAA17Hn376aQPA7N+/3znWrl0707lzZ+ff27dvNwBMnTp1THZ2ttfvV6xYYQCY999/X3xuamqquemmm7yOhYWFmSFDhuS6tm/fviY8PNzs3r3b63jXrl1NVFSUOXr0qDHGmPnz5xsApmXLlub8+fPOdb/99psJDg4299xzj5sojDHucp4zZ44BYLp27Zrr3I033mgiIyPNnj17vI5fd911JiYmxhw/ftwYY8wbb7xhAOS6ju69YsUKY4wxy5YtMwDM3LlzL1rWRYsWGQDmtdde8zq+Y8cOExoaakaNGuUca9WqlQFgfvjhB5fa540pU6YYAObHH3802dnZ5tixY2bWrFkmMTHRREdHm71795q0tDQjdR367e+//+4c69Chg+nQoYPXdQBMWlqa8+9+/fqZsLAws2vXLq/revToYSIjI01WVpYxxpiZM2caAObbb791rjl37pypXLmy6dOnj3Ns6NChJioqymRkZHjdb8KECQaA2bx5szHGmN9//90AMLVq1TJnz57Nk5zyC8koPT39otdUqFDB1KtXzxhzoe0CMO+++67XNSdOnDDx8fGmV69eXsdzcnLM1VdfbVq2bOkci4qKMg899NBFn7d69WoDwHz11Vf5qVKxsXfvXgPA9OvX75LXbt261QAw9913n9fxlStXGgDmX//6l/i78+fPm+zsbJORkWEAmP/85z/Oueeeey5Xew8EDh48aNq3b28AGAAmJCTEtG3b1jzzzDPm2LFj4m+onkuWLDEAzPr1651z1AY//fRTr9/07NnT1K1b1/k3jYNcRsYY849//MMAMFOmTLlomc+dO2eOHz9uypQpY1566SXnOI2HixYtyoMECg9fZVujRg0THh7uNQadOnXKxMfHm6FDhzrHpPpdrM8bY0xKSoqpUaNGodTNV/wtA1/Ha5ucnByTnZ1tnnjiCVOuXDmv74MaNWqYlJQUc/LkSdOnTx8TGxtrFixY4PX7jz/+2AAwX3zxhdfx9PR0A8C8/vrrXvcrXbq0+fnnn/MgqcKD5hH7v7CwMK9y21xMZps3bzYAzIgRI7yuJxkNHDiw0Ooybdo0A8C8+eabxhhjjh07ZqKiosw111zjdR3N1w0bNjTnzp1zjq9atcoAMB9//LFz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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(\"Average of all images in training dataset.\")\n", + "show_ave_MNIST(train_lbl, train_img, fashion=True)\n", + "\n", + "print(\"Average of all images in testing dataset.\")\n", + "show_ave_MNIST(test_lbl, test_img, fashion=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "Unlike Digits, in Fashion all items appear the same number of times." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing\n", + "\n", + "We will now begin testing our algorithms on Fashion.\n", + "\n", + "First, we need to convert the dataset into the `learning`-compatible `Dataset` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "metadata": {}, + "outputs": [], + "source": [ + "temp_train_lbl = train_lbl.reshape((60000,1))\n", + "training_examples = np.hstack((train_img, temp_train_lbl))" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": {}, + "outputs": [], + "source": [ + "# takes ~10 seconds to execute this\n", + "MNIST_DataSet = DataSet(examples=training_examples, distance=manhattan_distance)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plurality Learner\n", + "\n", + "The Plurality Learner always returns the class with the most training samples. In this case, `9`." + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9\n" + ] + } + ], + "source": [ + "pL = PluralityLearner(MNIST_DataSet)\n", + "print(pL(177))" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 115, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[177])\n", + "plt.imshow(test_img[177].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Naive-Bayes\n", + "\n", + "The Naive-Bayes classifier is an improvement over the Plurality Learner. It is much more accurate, but a lot slower." + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], + "source": [ + "# takes ~45 Secs. to execute this\n", + "\n", + "nBD = NaiveBayesLearner(MNIST_DataSet, continuous = False)\n", + "print(nBD(test_img[24]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's check if we got the right output." + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 121, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[24])\n", + "plt.imshow(test_img[24].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### K-Nearest Neighbors\n", + "\n", + "With the dataset in hand, we will first test how the kNN algorithm performs:" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n" + ] + } + ], + "source": [ + "# takes ~20 Secs. to execute this\n", + "kNN = NearestNeighborLearner(MNIST_DataSet, k=3)\n", + "print(kNN(test_img[211]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output is 1, which means the item at index 211 is a trouser. Let's see if the prediction is correct:" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual class of test image: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 123, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "print(\"Actual class of test image:\", test_lbl[211])\n", + "plt.imshow(test_img[211].reshape((28,28)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Indeed, the item was a trouser! The algorithm classified the item correctly." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.0" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "source": [], + "metadata": { + "collapsed": false + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/logic.ipynb b/logic.ipynb index e498dc7d6..062ffede2 100644 --- a/logic.ipynb +++ b/logic.ipynb @@ -6,18 +6,16 @@ "collapsed": true }, "source": [ - "# Logic: `logic.py`; Chapters 6-8" + "# Logic" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This notebook describes the [logic.py](https://github.com/aimacode/aima-python/blob/master/logic.py) module, which covers Chapters 6 (Logical Agents), 7 (First-Order Logic) and 8 (Inference in First-Order Logic) of *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. See the [intro notebook](https://github.com/aimacode/aima-python/blob/master/intro.ipynb) for instructions.\n", + "This Jupyter notebook acts as supporting material for topics covered in __Chapter 6 Logical Agents__, __Chapter 7 First-Order Logic__ and __Chapter 8 Inference in First-Order Logic__ of the book *[Artificial Intelligence: A Modern Approach](http://aima.cs.berkeley.edu)*. We make use of the implementations in the [logic.py](https://github.com/aimacode/aima-python/blob/master/logic.py) module. See the [intro notebook](https://github.com/aimacode/aima-python/blob/master/intro.ipynb) for instructions.\n", "\n", - "We'll start by looking at `Expr`, the data type for logical sentences, and the convenience function `expr`. Then we'll cover `KB` and `ProbKB`, the classes for Knowledge Bases. Then, we will construct a knowledge base of a specific situation in the Wumpus World. We will next go through the `tt_entails` function and experiment with it a bit. The `pl_resolution` and `pl_fc_entails` functions will come next. \n", - "\n", - "But the first step is to load the code:" + "Let's first import everything from the `logic` module." ] }, { @@ -29,7 +27,31 @@ "outputs": [], "source": [ "from utils import *\n", - "from logic import *" + "from logic import *\n", + "from notebook import psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "- Logical sentences\n", + " - Expr\n", + " - PropKB\n", + " - Knowledge-based agents\n", + " - Inference in propositional knowledge base\n", + " - Truth table enumeration\n", + " - Proof by resolution\n", + " - Forward and backward chaining\n", + " - DPLL\n", + " - WalkSAT\n", + " - SATPlan\n", + " - FolKB\n", + " - Inference in first order knowledge base\n", + " - Unification\n", + " - Forward chaining algorithm\n", + " - Backward chaining algorithm" ] }, { @@ -51,9 +73,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -81,7 +101,7 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ @@ -98,9 +118,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -132,9 +150,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -156,9 +172,7 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -178,9 +192,7 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -200,9 +212,7 @@ { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -222,9 +232,7 @@ { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -246,9 +254,7 @@ { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -275,9 +281,7 @@ { "cell_type": "code", "execution_count": 11, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -306,11 +310,11 @@ "|--------------------------|----------------------|-------------------------|---|---|\n", "| Negation | ¬ P | `~P` | `~P` | `Expr('~', P)`\n", "| And | P ∧ Q | `P & Q` | `P & Q` | `Expr('&', P, Q)`\n", - "| Or | P ∨ Q | `P` | `Q`| `P` | `Q` | `Expr('`|`', P, Q)\n", + "| Or | P ∨ Q | `P` | `Q`| `P` | `Q` | `Expr('`|`', P, Q)`\n", "| Inequality (Xor) | P ≠ Q | `P ^ Q` | `P ^ Q` | `Expr('^', P, Q)`\n", "| Implication | P → Q | `P` |`'==>'`| `Q` | `P ==> Q` | `Expr('==>', P, Q)`\n", "| Reverse Implication | Q ← P | `Q` |`'<=='`| `P` |`Q <== P` | `Expr('<==', Q, P)`\n", - "| Equivalence | P ↔ Q | `P` |`'<=>'`| `Q` |`P ==> Q` | `Expr('==>', P, Q)`\n", + "| Equivalence | P ↔ Q | `P` |`'<=>'`| `Q` |`P <=> Q` | `Expr('<=>', P, Q)`\n", "\n", "Here's an example of defining a sentence with an implication arrow:" ] @@ -318,9 +322,7 @@ { "cell_type": "code", "execution_count": 12, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -349,9 +351,7 @@ { "cell_type": "code", "execution_count": 13, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -378,9 +378,7 @@ { "cell_type": "code", "execution_count": 14, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -401,7 +399,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For now that's all you need to know about `expr`. Later we will explain the messy details of how `expr` is implemented and how `|'==>'|` is handled." + "For now that's all you need to know about `expr`. If you are interested, we explain the messy details of how `expr` is implemented and how `|'==>'|` is handled in the appendix." ] }, { @@ -412,12 +410,12 @@ "\n", "The class `PropKB` can be used to represent a knowledge base of propositional logic sentences.\n", "\n", - "We see that the class `KB` has four methods, apart from `__init__`. A point to note here: the `ask` method simply calls the `ask_generator` method. Thus, this one has already been implemented and what you'll have to actually implement when you create your own knowledge base class (if you want to, though I doubt you'll ever need to; just use the ones we've created for you), will be the `ask_generator` function and not the `ask` function itself.\n", + "We see that the class `KB` has four methods, apart from `__init__`. A point to note here: the `ask` method simply calls the `ask_generator` method. Thus, this one has already been implemented, and what you'll have to actually implement when you create your own knowledge base class (though you'll probably never need to, considering the ones we've created for you) will be the `ask_generator` function and not the `ask` function itself.\n", "\n", "The class `PropKB` now.\n", "* `__init__(self, sentence=None)` : The constructor `__init__` creates a single field `clauses` which will be a list of all the sentences of the knowledge base. Note that each one of these sentences will be a 'clause' i.e. a sentence which is made up of only literals and `or`s.\n", "* `tell(self, sentence)` : When you want to add a sentence to the KB, you use the `tell` method. This method takes a sentence, converts it to its CNF, extracts all the clauses, and adds all these clauses to the `clauses` field. So, you need not worry about `tell`ing only clauses to the knowledge base. You can `tell` the knowledge base a sentence in any form that you wish; converting it to CNF and adding the resulting clauses will be handled by the `tell` method.\n", - "* `ask_generator(self, query)` : The `ask_generator` function is used by the `ask` function. It calls the `tt_entails` function, which in turn returns `True` if the knowledge base entails query and `False` otherwise. The `ask_generator` itself returns an empty dict `{}` if the knowledge base entails query and `None` otherwise. This might seem a little bit weird to you. After all, it makes more sense just to return a `True` or a `False` instead of the `{}` or `None` But this is done to maintain consistency with the way things are in First-Order Logic, where, an `ask_generator` function, is supposed to return all the substitutions that make the query true. Hence the dict, to return all these substitutions. I will be mostly be using the `ask` function which returns a `{}` or a `False`, but if you don't like this, you can always use the `ask_if_true` function which returns a `True` or a `False`.\n", + "* `ask_generator(self, query)` : The `ask_generator` function is used by the `ask` function. It calls the `tt_entails` function, which in turn returns `True` if the knowledge base entails query and `False` otherwise. The `ask_generator` itself returns an empty dict `{}` if the knowledge base entails query and `None` otherwise. This might seem a little bit weird to you. After all, it makes more sense just to return a `True` or a `False` instead of the `{}` or `None` But this is done to maintain consistency with the way things are in First-Order Logic, where an `ask_generator` function is supposed to return all the substitutions that make the query true. Hence the dict, to return all these substitutions. I will be mostly be using the `ask` function which returns a `{}` or a `False`, but if you don't like this, you can always use the `ask_if_true` function which returns a `True` or a `False`.\n", "* `retract(self, sentence)` : This function removes all the clauses of the sentence given, from the knowledge base. Like the `tell` function, you don't have to pass clauses to remove them from the knowledge base; any sentence will do fine. The function will take care of converting that sentence to clauses and then remove those." ] }, @@ -425,258 +423,4548 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# TODO: More on KBs, plus what was promised in Intro Section\n", - "\n", - "TODO: fill in here ..." + "## Wumpus World KB\n", + "Let us create a `PropKB` for the wumpus world with the sentences mentioned in `section 7.4.3`." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "wumpus_kb = PropKB()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Appendix: The Implementation of `|'==>'|`\n", - "\n", - "Consider the `Expr` formed by this syntax:" + "We define the symbols we use in our clauses.
    \n", + "$P_{x, y}$ is true if there is a pit in `[x, y]`.
    \n", + "$B_{x, y}$ is true if the agent senses breeze in `[x, y]`.
    " ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we tell sentences based on `section 7.4.3`.
    \n", + "There is no pit in `[1,1]`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "wumpus_kb.tell(~P11)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A square is breezy if and only if there is a pit in a neighboring square. This has to be stated for each square but for now, we include just the relevant squares." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "wumpus_kb.tell(B11 | '<=>' | ((P12 | P21)))\n", + "wumpus_kb.tell(B21 | '<=>' | ((P11 | P22 | P31)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we include the breeze percepts for the first two squares leading up to the situation in `Figure 7.3(b)`" + ] + }, + { + "cell_type": "code", + "execution_count": 19, "metadata": { - "collapsed": false + "collapsed": true }, + "outputs": [], + "source": [ + "wumpus_kb.tell(~B11)\n", + "wumpus_kb.tell(B21)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can check the clauses stored in a `KB` by accessing its `clauses` variable" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(P ==> ~Q)" + "[~P11,\n", + " (~P12 | B11),\n", + " (~P21 | B11),\n", + " (P12 | P21 | ~B11),\n", + " (~P11 | B21),\n", + " (~P22 | B21),\n", + " (~P31 | B21),\n", + " (P11 | P22 | P31 | ~B21),\n", + " ~B11,\n", + " B21]" ] }, - "execution_count": 15, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "P |'==>'| ~Q" + "wumpus_kb.clauses" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "What is the funny `|'==>'|` syntax? The trick is that \"`|`\" is just the regular Python or-operator, and so is exactly equivalent to this: " + "We see that the equivalence $B_{1, 1} \\iff (P_{1, 2} \\lor P_{2, 1})$ was automatically converted to two implications which were inturn converted to CNF which is stored in the `KB`.
    \n", + "$B_{1, 1} \\iff (P_{1, 2} \\lor P_{2, 1})$ was split into $B_{1, 1} \\implies (P_{1, 2} \\lor P_{2, 1})$ and $B_{1, 1} \\Longleftarrow (P_{1, 2} \\lor P_{2, 1})$.
    \n", + "$B_{1, 1} \\implies (P_{1, 2} \\lor P_{2, 1})$ was converted to $P_{1, 2} \\lor P_{2, 1} \\lor \\neg B_{1, 1}$.
    \n", + "$B_{1, 1} \\Longleftarrow (P_{1, 2} \\lor P_{2, 1})$ was converted to $\\neg (P_{1, 2} \\lor P_{2, 1}) \\lor B_{1, 1}$ which becomes $(\\neg P_{1, 2} \\lor B_{1, 1}) \\land (\\neg P_{2, 1} \\lor B_{1, 1})$ after applying De Morgan's laws and distributing the disjunction.
    \n", + "$B_{2, 1} \\iff (P_{1, 1} \\lor P_{2, 2} \\lor P_{3, 2})$ is converted in similar manner." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Knowledge based agents" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A knowledge-based agent is a simple generic agent that maintains and handles a knowledge base.\n", + "The knowledge base may initially contain some background knowledge.\n", + "
    \n", + "The purpose of a KB agent is to provide a level of abstraction over knowledge-base manipulation and is to be used as a base class for agents that work on a knowledge base.\n", + "
    \n", + "Given a percept, the KB agent adds the percept to its knowledge base, asks the knowledge base for the best action, and tells the knowledge base that it has in fact taken that action.\n", + "
    \n", + "Our implementation of `KB-Agent` is encapsulated in a class `KB_AgentProgram` which inherits from the `KB` class.\n", + "
    \n", + "Let's have a look." ] }, { "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, + "execution_count": 21, + "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def KB_AgentProgram(KB):\n",
    +       "    """A generic logical knowledge-based agent program. [Figure 7.1]"""\n",
    +       "    steps = itertools.count()\n",
    +       "\n",
    +       "    def program(percept):\n",
    +       "        t = next(steps)\n",
    +       "        KB.tell(make_percept_sentence(percept, t))\n",
    +       "        action = KB.ask(make_action_query(t))\n",
    +       "        KB.tell(make_action_sentence(action, t))\n",
    +       "        return action\n",
    +       "\n",
    +       "    def make_percept_sentence(percept, t):\n",
    +       "        return Expr("Percept")(percept, t)\n",
    +       "\n",
    +       "    def make_action_query(t):\n",
    +       "        return expr("ShouldDo(action, {})".format(t))\n",
    +       "\n",
    +       "    def make_action_sentence(action, t):\n",
    +       "        return Expr("Did")(action[expr('action')], t)\n",
    +       "\n",
    +       "    return program\n",
    +       "
    \n", + "\n", + "\n" + ], "text/plain": [ - "(P ==> ~Q)" + "" ] }, - "execution_count": 16, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "(P | '==>') | ~Q" + "psource(KB_AgentProgram)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In other words, there are two applications of or-operators. Here's the first one:" + "The helper functions `make_percept_sentence`, `make_action_query` and `make_action_sentence` are all aptly named and as expected,\n", + "`make_percept_sentence` makes first-order logic sentences about percepts we want our agent to receive,\n", + "`make_action_query` asks the underlying `KB` about the action that should be taken and\n", + "`make_action_sentence` tells the underlying `KB` about the action it has just taken." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference in Propositional Knowledge Base\n", + "In this section we will look at two algorithms to check if a sentence is entailed by the `KB`. Our goal is to decide whether $\\text{KB} \\vDash \\alpha$ for some sentence $\\alpha$.\n", + "### Truth Table Enumeration\n", + "It is a model-checking approach which, as the name suggests, enumerates all possible models in which the `KB` is true and checks if $\\alpha$ is also true in these models. We list the $n$ symbols in the `KB` and enumerate the $2^{n}$ models in a depth-first manner and check the truth of `KB` and $\\alpha$." ] }, { "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, + "execution_count": 22, + "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def tt_check_all(kb, alpha, symbols, model):\n",
    +       "    """Auxiliary routine to implement tt_entails."""\n",
    +       "    if not symbols:\n",
    +       "        if pl_true(kb, model):\n",
    +       "            result = pl_true(alpha, model)\n",
    +       "            assert result in (True, False)\n",
    +       "            return result\n",
    +       "        else:\n",
    +       "            return True\n",
    +       "    else:\n",
    +       "        P, rest = symbols[0], symbols[1:]\n",
    +       "        return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and\n",
    +       "                tt_check_all(kb, alpha, rest, extend(model, P, False)))\n",
    +       "
    \n", + "\n", + "\n" + ], "text/plain": [ - "PartialExpr('==>', P)" + "" ] }, - "execution_count": 17, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "P | '==>'" + "psource(tt_check_all)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "What is going on here is that the `__or__` method of `Expr` serves a dual purpose. If the right-hand-side is another `Expr` (or a number), then the result is an `Expr`, as in `(P | Q)`. But if the right-hand-side is a string, then the string is taken to be an operator, and we create a node in the abstract syntax tree corresponding to a partially-filled `Expr`, one where we know the left-hand-side is `P` and the operator is `==>`, but we don't yet know the right-hand-side.\n", - "\n", - "The `PartialExpr` class has an `__or__` method that says to create an `Expr` node with the right-hand-side filled in. Here we can see the combination of the `PartialExpr` with `Q` to create a complete `Expr`:" + "The algorithm basically computes every line of the truth table $KB\\implies \\alpha$ and checks if it is true everywhere.\n", + "
    \n", + "If symbols are defined, the routine recursively constructs every combination of truth values for the symbols and then, \n", + "it checks whether `model` is consistent with `kb`.\n", + "The given models correspond to the lines in the truth table,\n", + "which have a `true` in the KB column, \n", + "and for these lines it checks whether the query evaluates to true\n", + "
    \n", + "`result = pl_true(alpha, model)`.\n", + "
    \n", + "
    \n", + "In short, `tt_check_all` evaluates this logical expression for each `model`\n", + "
    \n", + "`pl_true(kb, model) => pl_true(alpha, model)`\n", + "
    \n", + "which is logically equivalent to\n", + "
    \n", + "`pl_true(kb, model) & ~pl_true(alpha, model)` \n", + "
    \n", + "that is, the knowledge base and the negation of the query are logically inconsistent.\n", + "
    \n", + "
    \n", + "`tt_entails()` just extracts the symbols from the query and calls `tt_check_all()` with the proper parameters.\n" ] }, { "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": false - }, + "execution_count": 23, + "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def tt_entails(kb, alpha):\n",
    +       "    """Does kb entail the sentence alpha? Use truth tables. For propositional\n",
    +       "    kb's and sentences. [Figure 7.10]. Note that the 'kb' should be an\n",
    +       "    Expr which is a conjunction of clauses.\n",
    +       "    >>> tt_entails(expr('P & Q'), expr('Q'))\n",
    +       "    True\n",
    +       "    """\n",
    +       "    assert not variables(alpha)\n",
    +       "    symbols = list(prop_symbols(kb & alpha))\n",
    +       "    return tt_check_all(kb, alpha, symbols, {})\n",
    +       "
    \n", + "\n", + "\n" + ], "text/plain": [ - "(P ==> ~Q)" + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(tt_entails)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Keep in mind that for two symbols P and Q, P => Q is false only when P is `True` and Q is `False`.\n", + "Example usage of `tt_entails()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" ] }, - "execution_count": 18, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "partial = PartialExpr('==>', P) \n", - "partial | ~Q" + "tt_entails(P & Q, Q)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This [trick](http://code.activestate.com/recipes/384122-infix-operators/) is due to [Ferdinand Jamitzky](http://code.activestate.com/recipes/users/98863/), with a modification by [C. G. Vedant](https://github.com/Chipe1),\n", - "who suggested using a string inside the or-bars.\n", - "\n", - "## Appendix: The Implementation of `expr`\n", - "\n", - "How does `expr` parse a string into an `Expr`? It turns out there are two tricks (besides the Jamitzky/Vedant trick):\n", - "\n", - "1. We do a string substitution, replacing \"`==>`\" with \"`|'==>'|`\" (and likewise for other operators).\n", - "2. We `eval` the resulting string in an environment in which every identifier\n", - "is bound to a symbol with that identifier as the `op`.\n", - "\n", - "In other words," + "P & Q is True only when both P and Q are True. Hence, (P & Q) => Q is True" ] }, { "cell_type": "code", - "execution_count": 19, - "metadata": { - "collapsed": false - }, + "execution_count": 25, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(~(P & Q) ==> (~P | ~Q))" + "False" ] }, - "execution_count": 19, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "expr('~(P & Q) ==> (~P | ~Q)')" + "tt_entails(P | Q, Q)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tt_entails(P | Q, P)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "is equivalent to doing:" + "If we know that P | Q is true, we cannot infer the truth values of P and Q. \n", + "Hence (P | Q) => Q is False and so is (P | Q) => P." ] }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, + "execution_count": 27, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(~(P & Q) ==> (~P | ~Q))" + "True" ] }, - "execution_count": 20, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "P, Q = symbols('P, Q')\n", - "~(P & Q) |'==>'| (~P | ~Q)" + "(A, B, C, D, E, F, G) = symbols('A, B, C, D, E, F, G')\n", + "tt_entails(A & (B | C) & D & E & ~(F | G), A & D & E & ~F & ~G)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "One thing to beware of: this puts `==>` at the same precedence level as `\"|\"`, which is not quite right. For example, we get this:" + "We can see that for the KB to be true, A, D, E have to be True and F and G have to be False.\n", + "Nothing can be said about B or C." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Coming back to our problem, note that `tt_entails()` takes an `Expr` which is a conjunction of clauses as the input instead of the `KB` itself. \n", + "You can use the `ask_if_true()` method of `PropKB` which does all the required conversions. \n", + "Let's check what `wumpus_kb` tells us about $P_{1, 1}$." ] }, { "cell_type": "code", - "execution_count": 21, - "metadata": { - "collapsed": false - }, + "execution_count": 28, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(((P & Q) ==> P) | Q)" + "(True, False)" ] }, - "execution_count": 21, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "P & Q |'==>'| P | Q" + "wumpus_kb.ask_if_true(~P11), wumpus_kb.ask_if_true(P11)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "which is probably not what we meant; when in doubt, put in extra parens:" + "Looking at Figure 7.9 we see that in all models in which the knowledge base is `True`, $P_{1, 1}$ is `False`. It makes sense that `ask_if_true()` returns `True` for $\\alpha = \\neg P_{1, 1}$ and `False` for $\\alpha = P_{1, 1}$. This begs the question, what if $\\alpha$ is `True` in only a portion of all models. Do we return `True` or `False`? This doesn't rule out the possibility of $\\alpha$ being `True` but it is not entailed by the `KB` so we return `False` in such cases. We can see this is the case for $P_{2, 2}$ and $P_{3, 1}$." ] }, { "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false - }, + "execution_count": 29, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "((P & Q) ==> (P | Q))" + "(False, False)" ] }, - "execution_count": 22, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "(P & Q) |'==>'| (P | Q)" + "wumpus_kb.ask_if_true(~P22), wumpus_kb.ask_if_true(P22)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Proof by Resolution\n", + "Recall that our goal is to check whether $\\text{KB} \\vDash \\alpha$ i.e. is $\\text{KB} \\implies \\alpha$ true in every model. Suppose we wanted to check if $P \\implies Q$ is valid. We check the satisfiability of $\\neg (P \\implies Q)$, which can be rewritten as $P \\land \\neg Q$. If $P \\land \\neg Q$ is unsatisfiable, then $P \\implies Q$ must be true in all models. This gives us the result \"$\\text{KB} \\vDash \\alpha$ if and only if $\\text{KB} \\land \\neg \\alpha$ is unsatisfiable\".
    \n", + "This technique corresponds to proof by contradiction, a standard mathematical proof technique. We assume $\\alpha$ to be false and show that this leads to a contradiction with known axioms in $\\text{KB}$. We obtain a contradiction by making valid inferences using inference rules. In this proof we use a single inference rule, resolution which states $(l_1 \\lor \\dots \\lor l_k) \\land (m_1 \\lor \\dots \\lor m_n) \\land (l_i \\iff \\neg m_j) \\implies l_1 \\lor \\dots \\lor l_{i - 1} \\lor l_{i + 1} \\lor \\dots \\lor l_k \\lor m_1 \\lor \\dots \\lor m_{j - 1} \\lor m_{j + 1} \\lor \\dots \\lor m_n$. Applying the resolution yields us a clause which we add to the KB. We keep doing this until:\n", + "\n", + "* There are no new clauses that can be added, in which case $\\text{KB} \\nvDash \\alpha$.\n", + "* Two clauses resolve to yield the empty clause, in which case $\\text{KB} \\vDash \\alpha$.\n", + "\n", + "The empty clause is equivalent to False because it arises only from resolving two complementary\n", + "unit clauses such as $P$ and $\\neg P$ which is a contradiction as both $P$ and $\\neg P$ can't be True at the same time." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is one catch however, the algorithm that implements proof by resolution cannot handle complex sentences. \n", + "Implications and bi-implications have to be simplified into simpler clauses. \n", + "We already know that *every sentence of a propositional logic is logically equivalent to a conjunction of clauses*.\n", + "We will use this fact to our advantage and simplify the input sentence into the **conjunctive normal form** (CNF) which is a conjunction of disjunctions of literals.\n", + "For eg:\n", + "
    \n", + "$$(A\\lor B)\\land (\\neg B\\lor C\\lor\\neg D)\\land (D\\lor\\neg E)$$\n", + "This is equivalent to the POS (Product of sums) form in digital electronics.\n", + "
    \n", + "Here's an outline of how the conversion is done:\n", + "1. Convert bi-implications to implications\n", + "
    \n", + "$\\alpha\\iff\\beta$ can be written as $(\\alpha\\implies\\beta)\\land(\\beta\\implies\\alpha)$\n", + "
    \n", + "This also applies to compound sentences\n", + "
    \n", + "$\\alpha\\iff(\\beta\\lor\\gamma)$ can be written as $(\\alpha\\implies(\\beta\\lor\\gamma))\\land((\\beta\\lor\\gamma)\\implies\\alpha)$\n", + "
    \n", + "2. Convert implications to their logical equivalents\n", + "
    \n", + "$\\alpha\\implies\\beta$ can be written as $\\neg\\alpha\\lor\\beta$\n", + "
    \n", + "3. Move negation inwards\n", + "
    \n", + "CNF requires atomic literals. Hence, negation cannot appear on a compound statement.\n", + "De Morgan's laws will be helpful here.\n", + "
    \n", + "$\\neg(\\alpha\\land\\beta)\\equiv(\\neg\\alpha\\lor\\neg\\beta)$\n", + "
    \n", + "$\\neg(\\alpha\\lor\\beta)\\equiv(\\neg\\alpha\\land\\neg\\beta)$\n", + "
    \n", + "4. Distribute disjunction over conjunction\n", + "
    \n", + "Disjunction and conjunction are distributive over each other.\n", + "Now that we only have conjunctions, disjunctions and negations in our expression, \n", + "we will distribute disjunctions over conjunctions wherever possible as this will give us a sentence which is a conjunction of simpler clauses, \n", + "which is what we wanted in the first place.\n", + "
    \n", + "We need a term of the form\n", + "
    \n", + "$(\\alpha_{1}\\lor\\alpha_{2}\\lor\\alpha_{3}...)\\land(\\beta_{1}\\lor\\beta_{2}\\lor\\beta_{3}...)\\land(\\gamma_{1}\\lor\\gamma_{2}\\lor\\gamma_{3}...)\\land...$\n", + "
    \n", + "
    \n", + "The `to_cnf` function executes this conversion using helper subroutines." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def to_cnf(s):\n",
    +       "    """Convert a propositional logical sentence to conjunctive normal form.\n",
    +       "    That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 253]\n",
    +       "    >>> to_cnf('~(B | C)')\n",
    +       "    (~B & ~C)\n",
    +       "    """\n",
    +       "    s = expr(s)\n",
    +       "    if isinstance(s, str):\n",
    +       "        s = expr(s)\n",
    +       "    s = eliminate_implications(s)  # Steps 1, 2 from p. 253\n",
    +       "    s = move_not_inwards(s)  # Step 3\n",
    +       "    return distribute_and_over_or(s)  # Step 4\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(to_cnf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`to_cnf` calls three subroutines.\n", + "
    \n", + "`eliminate_implications` converts bi-implications and implications to their logical equivalents.\n", + "
    \n", + "`move_not_inwards` removes negations from compound statements and moves them inwards using De Morgan's laws.\n", + "
    \n", + "`distribute_and_over_or` distributes disjunctions over conjunctions.\n", + "
    \n", + "Run the cell below for implementation details." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def eliminate_implications(s):\n",
    +       "    """Change implications into equivalent form with only &, |, and ~ as logical operators."""\n",
    +       "    s = expr(s)\n",
    +       "    if not s.args or is_symbol(s.op):\n",
    +       "        return s  # Atoms are unchanged.\n",
    +       "    args = list(map(eliminate_implications, s.args))\n",
    +       "    a, b = args[0], args[-1]\n",
    +       "    if s.op == '==>':\n",
    +       "        return b | ~a\n",
    +       "    elif s.op == '<==':\n",
    +       "        return a | ~b\n",
    +       "    elif s.op == '<=>':\n",
    +       "        return (a | ~b) & (b | ~a)\n",
    +       "    elif s.op == '^':\n",
    +       "        assert len(args) == 2  # TODO: relax this restriction\n",
    +       "        return (a & ~b) | (~a & b)\n",
    +       "    else:\n",
    +       "        assert s.op in ('&', '|', '~')\n",
    +       "        return Expr(s.op, *args)\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def move_not_inwards(s):\n",
    +       "    """Rewrite sentence s by moving negation sign inward.\n",
    +       "    >>> move_not_inwards(~(A | B))\n",
    +       "    (~A & ~B)"""\n",
    +       "    s = expr(s)\n",
    +       "    if s.op == '~':\n",
    +       "        def NOT(b):\n",
    +       "            return move_not_inwards(~b)\n",
    +       "        a = s.args[0]\n",
    +       "        if a.op == '~':\n",
    +       "            return move_not_inwards(a.args[0])  # ~~A ==> A\n",
    +       "        if a.op == '&':\n",
    +       "            return associate('|', list(map(NOT, a.args)))\n",
    +       "        if a.op == '|':\n",
    +       "            return associate('&', list(map(NOT, a.args)))\n",
    +       "        return s\n",
    +       "    elif is_symbol(s.op) or not s.args:\n",
    +       "        return s\n",
    +       "    else:\n",
    +       "        return Expr(s.op, *list(map(move_not_inwards, s.args)))\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def distribute_and_over_or(s):\n",
    +       "    """Given a sentence s consisting of conjunctions and disjunctions\n",
    +       "    of literals, return an equivalent sentence in CNF.\n",
    +       "    >>> distribute_and_over_or((A & B) | C)\n",
    +       "    ((A | C) & (B | C))\n",
    +       "    """\n",
    +       "    s = expr(s)\n",
    +       "    if s.op == '|':\n",
    +       "        s = associate('|', s.args)\n",
    +       "        if s.op != '|':\n",
    +       "            return distribute_and_over_or(s)\n",
    +       "        if len(s.args) == 0:\n",
    +       "            return False\n",
    +       "        if len(s.args) == 1:\n",
    +       "            return distribute_and_over_or(s.args[0])\n",
    +       "        conj = first(arg for arg in s.args if arg.op == '&')\n",
    +       "        if not conj:\n",
    +       "            return s\n",
    +       "        others = [a for a in s.args if a is not conj]\n",
    +       "        rest = associate('|', others)\n",
    +       "        return associate('&', [distribute_and_over_or(c | rest)\n",
    +       "                               for c in conj.args])\n",
    +       "    elif s.op == '&':\n",
    +       "        return associate('&', list(map(distribute_and_over_or, s.args)))\n",
    +       "    else:\n",
    +       "        return s\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(eliminate_implications)\n", + "psource(move_not_inwards)\n", + "psource(distribute_and_over_or)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's convert some sentences to see how it works\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((A | ~B) & (B | ~A))" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A, B, C, D = expr('A, B, C, D')\n", + "to_cnf(A |'<=>'| B)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((A | ~B | ~C) & (B | ~A) & (C | ~A))" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_cnf(A |'<=>'| (B & C))" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(A & (C | B) & (D | B))" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_cnf(A & (B | (C & D)))" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((B | ~A | C | ~D) & (A | ~A | C | ~D) & (B | ~B | C | ~D) & (A | ~B | C | ~D))" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_cnf((A |'<=>'| ~B) |'==>'| (C | ~D))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Coming back to our resolution problem, we can see how the `to_cnf` function is utilized here" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def pl_resolution(KB, alpha):\n",
    +       "    """Propositional-logic resolution: say if alpha follows from KB. [Figure 7.12]"""\n",
    +       "    clauses = KB.clauses + conjuncts(to_cnf(~alpha))\n",
    +       "    new = set()\n",
    +       "    while True:\n",
    +       "        n = len(clauses)\n",
    +       "        pairs = [(clauses[i], clauses[j])\n",
    +       "                 for i in range(n) for j in range(i+1, n)]\n",
    +       "        for (ci, cj) in pairs:\n",
    +       "            resolvents = pl_resolve(ci, cj)\n",
    +       "            if False in resolvents:\n",
    +       "                return True\n",
    +       "            new = new.union(set(resolvents))\n",
    +       "        if new.issubset(set(clauses)):\n",
    +       "            return False\n",
    +       "        for c in new:\n",
    +       "            if c not in clauses:\n",
    +       "                clauses.append(c)\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(pl_resolution)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(True, False)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_resolution(wumpus_kb, ~P11), pl_resolution(wumpus_kb, P11)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(False, False)" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_resolution(wumpus_kb, ~P22), pl_resolution(wumpus_kb, P22)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward and backward chaining\n", + "Previously, we said we will look at two algorithms to check if a sentence is entailed by the `KB`. Here's a third one. \n", + "The difference here is that our goal now is to determine if a knowledge base of definite clauses entails a single proposition symbol *q* - the query.\n", + "There is a catch however - the knowledge base can only contain **Horn clauses**.\n", + "
    \n", + "#### Horn Clauses\n", + "Horn clauses can be defined as a *disjunction* of *literals* with **at most** one positive literal. \n", + "
    \n", + "A Horn clause with exactly one positive literal is called a *definite clause*.\n", + "
    \n", + "A Horn clause might look like \n", + "
    \n", + "$\\neg a\\lor\\neg b\\lor\\neg c\\lor\\neg d... \\lor z$\n", + "
    \n", + "This, coincidentally, is also a definite clause.\n", + "
    \n", + "Using De Morgan's laws, the example above can be simplified to \n", + "
    \n", + "$a\\land b\\land c\\land d ... \\implies z$\n", + "
    \n", + "This seems like a logical representation of how humans process known data and facts. \n", + "Assuming percepts `a`, `b`, `c`, `d` ... to be true simultaneously, we can infer `z` to also be true at that point in time. \n", + "There are some interesting aspects of Horn clauses that make algorithmic inference or *resolution* easier.\n", + "- Definite clauses can be written as implications:\n", + "
    \n", + "The most important simplification a definite clause provides is that it can be written as an implication.\n", + "The premise (or the knowledge that leads to the implication) is a conjunction of positive literals.\n", + "The conclusion (the implied statement) is also a positive literal.\n", + "The sentence thus becomes easier to understand.\n", + "The premise and the conclusion are conventionally called the *body* and the *head* respectively.\n", + "A single positive literal is called a *fact*.\n", + "- Forward chaining and backward chaining can be used for inference from Horn clauses:\n", + "
    \n", + "Forward chaining is semantically identical to `AND-OR-Graph-Search` from the chapter on search algorithms.\n", + "Implementational details will be explained shortly.\n", + "- Deciding entailment with Horn clauses is linear in size of the knowledge base:\n", + "
    \n", + "Surprisingly, the forward and backward chaining algorithms traverse each element of the knowledge base at most once, greatly simplifying the problem.\n", + "
    \n", + "
    \n", + "The function `pl_fc_entails` implements forward chaining to see if a knowledge base `KB` entails a symbol `q`.\n", + "
    \n", + "Before we proceed further, note that `pl_fc_entails` doesn't use an ordinary `KB` instance. \n", + "The knowledge base here is an instance of the `PropDefiniteKB` class, derived from the `PropKB` class, \n", + "but modified to store definite clauses.\n", + "
    \n", + "The main point of difference arises in the inclusion of a helper method to `PropDefiniteKB` that returns a list of clauses in KB that have a given symbol `p` in their premise." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
        def clauses_with_premise(self, p):\n",
    +       "        """Return a list of the clauses in KB that have p in their premise.\n",
    +       "        This could be cached away for O(1) speed, but we'll recompute it."""\n",
    +       "        return [c for c in self.clauses\n",
    +       "                if c.op == '==>' and p in conjuncts(c.args[0])]\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(PropDefiniteKB.clauses_with_premise)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now have a look at the `pl_fc_entails` algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def pl_fc_entails(KB, q):\n",
    +       "    """Use forward chaining to see if a PropDefiniteKB entails symbol q.\n",
    +       "    [Figure 7.15]\n",
    +       "    >>> pl_fc_entails(horn_clauses_KB, expr('Q'))\n",
    +       "    True\n",
    +       "    """\n",
    +       "    count = {c: len(conjuncts(c.args[0]))\n",
    +       "             for c in KB.clauses\n",
    +       "             if c.op == '==>'}\n",
    +       "    inferred = defaultdict(bool)\n",
    +       "    agenda = [s for s in KB.clauses if is_prop_symbol(s.op)]\n",
    +       "    while agenda:\n",
    +       "        p = agenda.pop()\n",
    +       "        if p == q:\n",
    +       "            return True\n",
    +       "        if not inferred[p]:\n",
    +       "            inferred[p] = True\n",
    +       "            for c in KB.clauses_with_premise(p):\n",
    +       "                count[c] -= 1\n",
    +       "                if count[c] == 0:\n",
    +       "                    agenda.append(c.args[1])\n",
    +       "    return False\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(pl_fc_entails)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function accepts a knowledge base `KB` (an instance of `PropDefiniteKB`) and a query `q` as inputs.\n", + "
    \n", + "
    \n", + "`count` initially stores the number of symbols in the premise of each sentence in the knowledge base.\n", + "
    \n", + "The `conjuncts` helper function separates a given sentence at conjunctions.\n", + "
    \n", + "`inferred` is initialized as a *boolean* defaultdict. \n", + "This will be used later to check if we have inferred all premises of each clause of the agenda.\n", + "
    \n", + "`agenda` initially stores a list of clauses that the knowledge base knows to be true.\n", + "The `is_prop_symbol` helper function checks if the given symbol is a valid propositional logic symbol.\n", + "
    \n", + "
    \n", + "We now iterate through `agenda`, popping a symbol `p` on each iteration.\n", + "If the query `q` is the same as `p`, we know that entailment holds.\n", + "
    \n", + "The agenda is processed, reducing `count` by one for each implication with a premise `p`.\n", + "A conclusion is added to the agenda when `count` reaches zero. This means we know all the premises of that particular implication to be true.\n", + "
    \n", + "`clauses_with_premise` is a helpful method of the `PropKB` class.\n", + "It returns a list of clauses in the knowledge base that have `p` in their premise.\n", + "
    \n", + "
    \n", + "Now that we have an idea of how this function works, let's see a few examples of its usage, but we first need to define our knowledge base. We assume we know the following clauses to be true." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses = ['(B & F)==>E', \n", + " '(A & E & F)==>G', \n", + " '(B & C)==>F', \n", + " '(A & B)==>D', \n", + " '(E & F)==>H', \n", + " '(H & I)==>J',\n", + " 'A', \n", + " 'B', \n", + " 'C']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now `tell` this information to our knowledge base." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "definite_clauses_KB = PropDefiniteKB()\n", + "for clause in clauses:\n", + " definite_clauses_KB.tell(expr(clause))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now check if our knowledge base entails the following queries." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_fc_entails(definite_clauses_KB, expr('G'))" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_fc_entails(definite_clauses_KB, expr('H'))" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_fc_entails(definite_clauses_KB, expr('I'))" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_fc_entails(definite_clauses_KB, expr('J'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Effective Propositional Model Checking\n", + "\n", + "The previous segments elucidate the algorithmic procedure for model checking. \n", + "In this segment, we look at ways of making them computationally efficient.\n", + "
    \n", + "The problem we are trying to solve is conventionally called the _propositional satisfiability problem_, abbreviated as the _SAT_ problem.\n", + "In layman terms, if there exists a model that satisfies a given Boolean formula, the formula is called satisfiable.\n", + "
    \n", + "The SAT problem was the first problem to be proven _NP-complete_.\n", + "The main characteristics of an NP-complete problem are:\n", + "- Given a solution to such a problem, it is easy to verify if the solution solves the problem.\n", + "- The time required to actually solve the problem using any known algorithm increases exponentially with respect to the size of the problem.\n", + "
    \n", + "
    \n", + "Due to these properties, heuristic and approximational methods are often applied to find solutions to these problems.\n", + "
    \n", + "It is extremely important to be able to solve large scale SAT problems efficiently because \n", + "many combinatorial problems in computer science can be conveniently reduced to checking the satisfiability of a propositional sentence under some constraints.\n", + "
    \n", + "We will introduce two new algorithms that perform propositional model checking in a computationally effective way.\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. DPLL (Davis-Putnam-Logeman-Loveland) algorithm\n", + "This algorithm is very similar to Backtracking-Search.\n", + "It recursively enumerates possible models in a depth-first fashion with the following improvements over algorithms like `tt_entails`:\n", + "1. Early termination:\n", + "
    \n", + "In certain cases, the algorithm can detect the truth value of a statement using just a partially completed model.\n", + "For example, $(P\\lor Q)\\land(P\\lor R)$ is true if P is true, regardless of other variables.\n", + "This reduces the search space significantly.\n", + "2. Pure symbol heuristic:\n", + "
    \n", + "A symbol that has the same sign (positive or negative) in all clauses is called a _pure symbol_.\n", + "It isn't difficult to see that any satisfiable model will have the pure symbols assigned such that its parent clause becomes _true_.\n", + "For example, $(P\\lor\\neg Q)\\land(\\neg Q\\lor\\neg R)\\land(R\\lor P)$ has P and Q as pure symbols\n", + "and for the sentence to be true, P _has_ to be true and Q _has_ to be false.\n", + "The pure symbol heuristic thus simplifies the problem a bit.\n", + "3. Unit clause heuristic:\n", + "
    \n", + "In the context of DPLL, clauses with just one literal and clauses with all but one _false_ literals are called unit clauses.\n", + "If a clause is a unit clause, it can only be satisfied by assigning the necessary value to make the last literal true.\n", + "We have no other choice.\n", + "
    \n", + "Assigning one unit clause can create another unit clause.\n", + "For example, when P is false, $(P\\lor Q)$ becomes a unit clause, causing _true_ to be assigned to Q.\n", + "A series of forced assignments derived from previous unit clauses is called _unit propagation_.\n", + "In this way, this heuristic simplifies the problem further.\n", + "
    \n", + "The algorithm often employs other tricks to scale up to large problems.\n", + "However, these tricks are currently out of the scope of this notebook. Refer to section 7.6 of the book for more details.\n", + "
    \n", + "
    \n", + "Let's have a look at the algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def dpll(clauses, symbols, model):\n",
    +       "    """See if the clauses are true in a partial model."""\n",
    +       "    unknown_clauses = []  # clauses with an unknown truth value\n",
    +       "    for c in clauses:\n",
    +       "        val = pl_true(c, model)\n",
    +       "        if val is False:\n",
    +       "            return False\n",
    +       "        if val is not True:\n",
    +       "            unknown_clauses.append(c)\n",
    +       "    if not unknown_clauses:\n",
    +       "        return model\n",
    +       "    P, value = find_pure_symbol(symbols, unknown_clauses)\n",
    +       "    if P:\n",
    +       "        return dpll(clauses, removeall(P, symbols), extend(model, P, value))\n",
    +       "    P, value = find_unit_clause(clauses, model)\n",
    +       "    if P:\n",
    +       "        return dpll(clauses, removeall(P, symbols), extend(model, P, value))\n",
    +       "    if not symbols:\n",
    +       "        raise TypeError("Argument should be of the type Expr.")\n",
    +       "    P, symbols = symbols[0], symbols[1:]\n",
    +       "    return (dpll(clauses, symbols, extend(model, P, True)) or\n",
    +       "            dpll(clauses, symbols, extend(model, P, False)))\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(dpll)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The algorithm uses the ideas described above to check satisfiability of a sentence in propositional logic.\n", + "It recursively calls itself, simplifying the problem at each step. It also uses helper functions `find_pure_symbol` and `find_unit_clause` to carry out steps 2 and 3 above.\n", + "
    \n", + "The `dpll_satisfiable` helper function converts the input clauses to _conjunctive normal form_ and calls the `dpll` function with the correct parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def dpll_satisfiable(s):\n",
    +       "    """Check satisfiability of a propositional sentence.\n",
    +       "    This differs from the book code in two ways: (1) it returns a model\n",
    +       "    rather than True when it succeeds; this is more useful. (2) The\n",
    +       "    function find_pure_symbol is passed a list of unknown clauses, rather\n",
    +       "    than a list of all clauses and the model; this is more efficient."""\n",
    +       "    clauses = conjuncts(to_cnf(s))\n",
    +       "    symbols = list(prop_symbols(s))\n",
    +       "    return dpll(clauses, symbols, {})\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(dpll_satisfiable)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see a few examples of usage." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "A, B, C, D = expr('A, B, C, D')" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True, C: False, D: True}" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dpll_satisfiable(A & B & ~C & D)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a simple case to highlight that the algorithm actually works." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{B: True, C: True, D: False}" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dpll_satisfiable((A & B) | (C & ~A) | (B & ~D))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If a particular symbol isn't present in the solution, \n", + "it means that the solution is independent of the value of that symbol.\n", + "In this case, the solution is independent of A." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True}" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dpll_satisfiable(A |'<=>'| B)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: False, B: True, C: True}" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dpll_satisfiable((A |'<=>'| B) |'==>'| (C & ~A))" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{B: True, C: True}" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dpll_satisfiable((A | (B & C)) |'<=>'| ((A | B) & (A | C)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. WalkSAT algorithm\n", + "This algorithm is very similar to Hill climbing.\n", + "On every iteration, the algorithm picks an unsatisfied clause and flips a symbol in the clause.\n", + "This is similar to finding a neighboring state in the `hill_climbing` algorithm.\n", + "
    \n", + "The symbol to be flipped is decided by an evaluation function that counts the number of unsatisfied clauses.\n", + "Sometimes, symbols are also flipped randomly to avoid local optima. A subtle balance between greediness and randomness is required. Alternatively, some versions of the algorithm restart with a completely new random assignment if no solution has been found for too long as a way of getting out of local minima of numbers of unsatisfied clauses.\n", + "
    \n", + "
    \n", + "Let's have a look at the algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def WalkSAT(clauses, p=0.5, max_flips=10000):\n",
    +       "    """Checks for satisfiability of all clauses by randomly flipping values of variables\n",
    +       "    """\n",
    +       "    # Set of all symbols in all clauses\n",
    +       "    symbols = {sym for clause in clauses for sym in prop_symbols(clause)}\n",
    +       "    # model is a random assignment of true/false to the symbols in clauses\n",
    +       "    model = {s: random.choice([True, False]) for s in symbols}\n",
    +       "    for i in range(max_flips):\n",
    +       "        satisfied, unsatisfied = [], []\n",
    +       "        for clause in clauses:\n",
    +       "            (satisfied if pl_true(clause, model) else unsatisfied).append(clause)\n",
    +       "        if not unsatisfied:  # if model satisfies all the clauses\n",
    +       "            return model\n",
    +       "        clause = random.choice(unsatisfied)\n",
    +       "        if probability(p):\n",
    +       "            sym = random.choice(list(prop_symbols(clause)))\n",
    +       "        else:\n",
    +       "            # Flip the symbol in clause that maximizes number of sat. clauses\n",
    +       "            def sat_count(sym):\n",
    +       "                # Return the the number of clauses satisfied after flipping the symbol.\n",
    +       "                model[sym] = not model[sym]\n",
    +       "                count = len([clause for clause in clauses if pl_true(clause, model)])\n",
    +       "                model[sym] = not model[sym]\n",
    +       "                return count\n",
    +       "            sym = argmax(prop_symbols(clause), key=sat_count)\n",
    +       "        model[sym] = not model[sym]\n",
    +       "    # If no solution is found within the flip limit, we return failure\n",
    +       "    return None\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(WalkSAT)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function takes three arguments:\n", + "
    \n", + "1. The `clauses` we want to satisfy.\n", + "
    \n", + "2. The probability `p` of randomly changing a symbol.\n", + "
    \n", + "3. The maximum number of flips (`max_flips`) the algorithm will run for. If the clauses are still unsatisfied, the algorithm returns `None` to denote failure.\n", + "
    \n", + "The algorithm is identical in concept to Hill climbing and the code isn't difficult to understand.\n", + "
    \n", + "
    \n", + "Let's see a few examples of usage." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "A, B, C, D = expr('A, B, C, D')" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True, C: False, D: True}" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "WalkSAT([A, B, ~C, D], 0.5, 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a simple case to show that the algorithm converges." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True, C: True}" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "WalkSAT([A & B, A & C], 0.5, 100)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True, C: True, D: True}" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "WalkSAT([A & B, C & D, C & B], 0.5, 100)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "WalkSAT([A & B, C | D, ~(D | B)], 0.5, 1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This one doesn't give any output because WalkSAT did not find any model where these clauses hold. We can solve these clauses to see that they together form a contradiction and hence, it isn't supposed to have a solution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One point of difference between this algorithm and the `dpll_satisfiable` algorithms is that both these algorithms take inputs differently. \n", + "For WalkSAT to take complete sentences as input, \n", + "we can write a helper function that converts the input sentence into conjunctive normal form and then calls WalkSAT with the list of conjuncts of the CNF form of the sentence." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def WalkSAT_CNF(sentence, p=0.5, max_flips=10000):\n", + " return WalkSAT(conjuncts(to_cnf(sentence)), 0, max_flips)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can call `WalkSAT_CNF` and `DPLL_Satisfiable` with the same arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{A: True, B: True, C: False, D: True}" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "WalkSAT_CNF((A & B) | (C & ~A) | (B & ~D), 0.5, 1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It works!\n", + "
    \n", + "Notice that the solution generated by WalkSAT doesn't omit variables that the sentence doesn't depend upon. \n", + "If the sentence is independent of a particular variable, the solution contains a random value for that variable because of the stochastic nature of the algorithm.\n", + "
    \n", + "
    \n", + "Let's compare the runtime of WalkSAT and DPLL for a few cases. We will use the `%%timeit` magic to do this." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sentence_1 = A |'<=>'| B\n", + "sentence_2 = (A & B) | (C & ~A) | (B & ~D)\n", + "sentence_3 = (A | (B & C)) |'<=>'| ((A | B) & (A | C))" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.55 ms ± 64.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "dpll_satisfiable(sentence_1)\n", + "dpll_satisfiable(sentence_2)\n", + "dpll_satisfiable(sentence_3)" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.02 ms ± 6.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "WalkSAT_CNF(sentence_1)\n", + "WalkSAT_CNF(sentence_2)\n", + "WalkSAT_CNF(sentence_3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On an average, for solvable cases, `WalkSAT` is quite faster than `dpll` because, for a small number of variables, \n", + "`WalkSAT` can reduce the search space significantly. \n", + "Results can be different for sentences with more symbols though.\n", + "Feel free to play around with this to understand the trade-offs of these algorithms better." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SATPlan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section we show how to make plans by logical inference. The basic idea is very simple. It includes the following three steps:\n", + "1. Constuct a sentence that includes:\n", + " 1. A colection of assertions about the initial state.\n", + " 2. The successor-state axioms for all the possible actions at each time up to some maximum time t.\n", + " 3. The assertion that the goal is achieved at time t.\n", + "2. Present the whole sentence to a SAT solver.\n", + "3. Assuming a model is found, extract from the model those variables that represent actions and are assigned true. Together they represent a plan to achieve the goals.\n", + "\n", + "\n", + "Lets have a look at the algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable):\n",
    +       "    """Converts a planning problem to Satisfaction problem by translating it to a cnf sentence.\n",
    +       "    [Figure 7.22]"""\n",
    +       "\n",
    +       "    # Functions used by SAT_plan\n",
    +       "    def translate_to_SAT(init, transition, goal, time):\n",
    +       "        clauses = []\n",
    +       "        states = [state for state in transition]\n",
    +       "\n",
    +       "        # Symbol claiming state s at time t\n",
    +       "        state_counter = itertools.count()\n",
    +       "        for s in states:\n",
    +       "            for t in range(time+1):\n",
    +       "                state_sym[s, t] = Expr("State_{}".format(next(state_counter)))\n",
    +       "\n",
    +       "        # Add initial state axiom\n",
    +       "        clauses.append(state_sym[init, 0])\n",
    +       "\n",
    +       "        # Add goal state axiom\n",
    +       "        clauses.append(state_sym[goal, time])\n",
    +       "\n",
    +       "        # All possible transitions\n",
    +       "        transition_counter = itertools.count()\n",
    +       "        for s in states:\n",
    +       "            for action in transition[s]:\n",
    +       "                s_ = transition[s][action]\n",
    +       "                for t in range(time):\n",
    +       "                    # Action 'action' taken from state 's' at time 't' to reach 's_'\n",
    +       "                    action_sym[s, action, t] = Expr(\n",
    +       "                        "Transition_{}".format(next(transition_counter)))\n",
    +       "\n",
    +       "                    # Change the state from s to s_\n",
    +       "                    clauses.append(action_sym[s, action, t] |'==>'| state_sym[s, t])\n",
    +       "                    clauses.append(action_sym[s, action, t] |'==>'| state_sym[s_, t + 1])\n",
    +       "\n",
    +       "        # Allow only one state at any time\n",
    +       "        for t in range(time+1):\n",
    +       "            # must be a state at any time\n",
    +       "            clauses.append(associate('|', [state_sym[s, t] for s in states]))\n",
    +       "\n",
    +       "            for s in states:\n",
    +       "                for s_ in states[states.index(s) + 1:]:\n",
    +       "                    # for each pair of states s, s_ only one is possible at time t\n",
    +       "                    clauses.append((~state_sym[s, t]) | (~state_sym[s_, t]))\n",
    +       "\n",
    +       "        # Restrict to one transition per timestep\n",
    +       "        for t in range(time):\n",
    +       "            # list of possible transitions at time t\n",
    +       "            transitions_t = [tr for tr in action_sym if tr[2] == t]\n",
    +       "\n",
    +       "            # make sure at least one of the transitions happens\n",
    +       "            clauses.append(associate('|', [action_sym[tr] for tr in transitions_t]))\n",
    +       "\n",
    +       "            for tr in transitions_t:\n",
    +       "                for tr_ in transitions_t[transitions_t.index(tr) + 1:]:\n",
    +       "                    # there cannot be two transitions tr and tr_ at time t\n",
    +       "                    clauses.append(~action_sym[tr] | ~action_sym[tr_])\n",
    +       "\n",
    +       "        # Combine the clauses to form the cnf\n",
    +       "        return associate('&', clauses)\n",
    +       "\n",
    +       "    def extract_solution(model):\n",
    +       "        true_transitions = [t for t in action_sym if model[action_sym[t]]]\n",
    +       "        # Sort transitions based on time, which is the 3rd element of the tuple\n",
    +       "        true_transitions.sort(key=lambda x: x[2])\n",
    +       "        return [action for s, action, time in true_transitions]\n",
    +       "\n",
    +       "    # Body of SAT_plan algorithm\n",
    +       "    for t in range(t_max):\n",
    +       "        # dictionaries to help extract the solution from model\n",
    +       "        state_sym = {}\n",
    +       "        action_sym = {}\n",
    +       "\n",
    +       "        cnf = translate_to_SAT(init, transition, goal, t)\n",
    +       "        model = SAT_solver(cnf)\n",
    +       "        if model is not False:\n",
    +       "            return extract_solution(model)\n",
    +       "    return None\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(SAT_plan)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see few examples of its usage. First we define a transition and then call `SAT_plan`." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n", + "['Right']\n", + "['Left', 'Left']\n" + ] + } + ], + "source": [ + "transition = {'A': {'Left': 'A', 'Right': 'B'},\n", + " 'B': {'Left': 'A', 'Right': 'C'},\n", + " 'C': {'Left': 'B', 'Right': 'C'}}\n", + "\n", + "\n", + "print(SAT_plan('A', transition, 'C', 2)) \n", + "print(SAT_plan('A', transition, 'B', 3))\n", + "print(SAT_plan('C', transition, 'A', 3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us do the same for another transition." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Right', 'Down']\n" + ] + } + ], + "source": [ + "transition = {(0, 0): {'Right': (0, 1), 'Down': (1, 0)},\n", + " (0, 1): {'Left': (1, 0), 'Down': (1, 1)},\n", + " (1, 0): {'Right': (1, 0), 'Up': (1, 0), 'Left': (1, 0), 'Down': (1, 0)},\n", + " (1, 1): {'Left': (1, 0), 'Up': (0, 1)}}\n", + "\n", + "\n", + "print(SAT_plan((0, 0), transition, (1, 1), 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## First-Order Logic Knowledge Bases: `FolKB`\n", + "\n", + "The class `FolKB` can be used to represent a knowledge base of First-order logic sentences. You would initialize and use it the same way as you would for `PropKB` except that the clauses are first-order definite clauses. We will see how to write such clauses to create a database and query them in the following sections." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Criminal KB\n", + "In this section we create a `FolKB` based on the following paragraph.
    \n", + "The law says that it is a crime for an American to sell weapons to hostile nations. The country Nono, an enemy of America, has some missiles, and all of its missiles were sold to it by Colonel West, who is American.
    \n", + "The first step is to extract the facts and convert them into first-order definite clauses. Extracting the facts from data alone is a challenging task. Fortunately, we have a small paragraph and can do extraction and conversion manually. We'll store the clauses in list aptly named `clauses`." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses = []" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "“... it is a crime for an American to sell weapons to hostile nations”
    \n", + "The keywords to look for here are 'crime', 'American', 'sell', 'weapon' and 'hostile'. We use predicate symbols to make meaning of them.\n", + "\n", + "* `Criminal(x)`: `x` is a criminal\n", + "* `American(x)`: `x` is an American\n", + "* `Sells(x ,y, z)`: `x` sells `y` to `z`\n", + "* `Weapon(x)`: `x` is a weapon\n", + "* `Hostile(x)`: `x` is a hostile nation\n", + "\n", + "Let us now combine them with appropriate variable naming to depict the meaning of the sentence. The criminal `x` is also the American `x` who sells weapon `y` to `z`, which is a hostile nation.\n", + "\n", + "$\\text{American}(x) \\land \\text{Weapon}(y) \\land \\text{Sells}(x, y, z) \\land \\text{Hostile}(z) \\implies \\text{Criminal} (x)$" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"The country Nono, an enemy of America\"
    \n", + "We now know that Nono is an enemy of America. We represent these nations using the constant symbols `Nono` and `America`. the enemy relation is show using the predicate symbol `Enemy`.\n", + "\n", + "$\\text{Enemy}(\\text{Nono}, \\text{America})$" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"Enemy(Nono, America)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"Nono ... has some missiles\"
    \n", + "This states the existence of some missile which is owned by Nono. $\\exists x \\text{Owns}(\\text{Nono}, x) \\land \\text{Missile}(x)$. We invoke existential instantiation to introduce a new constant `M1` which is the missile owned by Nono.\n", + "\n", + "$\\text{Owns}(\\text{Nono}, \\text{M1}), \\text{Missile}(\\text{M1})$" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"Owns(Nono, M1)\"))\n", + "clauses.append(expr(\"Missile(M1)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "\"All of its missiles were sold to it by Colonel West\"
    \n", + "If Nono owns something and it classifies as a missile, then it was sold to Nono by West.\n", + "\n", + "$\\text{Missile}(x) \\land \\text{Owns}(\\text{Nono}, x) \\implies \\text{Sells}(\\text{West}, x, \\text{Nono})$" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"West, who is American\"
    \n", + "West is an American.\n", + "\n", + "$\\text{American}(\\text{West})$" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"American(West)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also know, from our understanding of language, that missiles are weapons and that an enemy of America counts as “hostile”.\n", + "\n", + "$\\text{Missile}(x) \\implies \\text{Weapon}(x), \\text{Enemy}(x, \\text{America}) \\implies \\text{Hostile}(x)$" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses.append(expr(\"Missile(x) ==> Weapon(x)\"))\n", + "clauses.append(expr(\"Enemy(x, America) ==> Hostile(x)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have converted the information into first-order definite clauses we can create our first-order logic knowledge base." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "crime_kb = FolKB(clauses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `subst` helper function substitutes variables with given values in first-order logic statements.\n", + "This will be useful in later algorithms.\n", + "It's implementation is quite simple and self-explanatory." + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def subst(s, x):\n",
    +       "    """Substitute the substitution s into the expression x.\n",
    +       "    >>> subst({x: 42, y:0}, F(x) + y)\n",
    +       "    (F(42) + 0)\n",
    +       "    """\n",
    +       "    if isinstance(x, list):\n",
    +       "        return [subst(s, xi) for xi in x]\n",
    +       "    elif isinstance(x, tuple):\n",
    +       "        return tuple([subst(s, xi) for xi in x])\n",
    +       "    elif not isinstance(x, Expr):\n",
    +       "        return x\n",
    +       "    elif is_var_symbol(x.op):\n",
    +       "        return s.get(x, x)\n",
    +       "    else:\n",
    +       "        return Expr(x.op, *[subst(s, arg) for arg in x.args])\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(subst)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's an example of how `subst` can be used." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Owns(Nono, M1)" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "subst({x: expr('Nono'), y: expr('M1')}, expr('Owns(x, y)'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference in First-Order Logic\n", + "In this section we look at a forward chaining and a backward chaining algorithm for `FolKB`. Both aforementioned algorithms rely on a process called unification, a key component of all first-order inference algorithms." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Unification\n", + "We sometimes require finding substitutions that make different logical expressions look identical. This process, called unification, is done by the `unify` algorithm. It takes as input two sentences and returns a unifier for them if one exists. A unifier is a dictionary which stores the substitutions required to make the two sentences identical. It does so by recursively unifying the components of a sentence, where the unification of a variable symbol `var` with a constant symbol `Const` is the mapping `{var: Const}`. Let's look at a few examples." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{x: 3}" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unify(expr('x'), 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{x: B}" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unify(expr('A(x)'), expr('A(B)'))" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{x: Bella, y: Dobby}" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unify(expr('Cat(x) & Dog(Dobby)'), expr('Cat(Bella) & Dog(y)'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In cases where there is no possible substitution that unifies the two sentences the function return `None`." + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] + } + ], + "source": [ + "print(unify(expr('Cat(x)'), expr('Dog(Dobby)')))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also need to take care we do not unintentionally use the same variable name. Unify treats them as a single variable which prevents it from taking multiple value." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] + } + ], + "source": [ + "print(unify(expr('Cat(x) & Dog(Dobby)'), expr('Cat(Bella) & Dog(x)')))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward Chaining Algorithm\n", + "We consider the simple forward-chaining algorithm presented in Figure 9.3. We look at each rule in the knowledge base and see if the premises can be satisfied. This is done by finding a substitution which unifies each of the premise with a clause in the `KB`. If we are able to unify the premises, the conclusion (with the corresponding substitution) is added to the `KB`. This inferencing process is repeated until either the query can be answered or till no new sentences can be added. We test if the newly added clause unifies with the query in which case the substitution yielded by `unify` is an answer to the query. If we run out of sentences to infer, this means the query was a failure.\n", + "\n", + "The function `fol_fc_ask` is a generator which yields all substitutions which validate the query." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def fol_fc_ask(KB, alpha):\n",
    +       "    """A simple forward-chaining algorithm. [Figure 9.3]"""\n",
    +       "    # TODO: Improve efficiency\n",
    +       "    kb_consts = list({c for clause in KB.clauses for c in constant_symbols(clause)})\n",
    +       "    def enum_subst(p):\n",
    +       "        query_vars = list({v for clause in p for v in variables(clause)})\n",
    +       "        for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)):\n",
    +       "            theta = {x: y for x, y in zip(query_vars, assignment_list)}\n",
    +       "            yield theta\n",
    +       "\n",
    +       "    # check if we can answer without new inferences\n",
    +       "    for q in KB.clauses:\n",
    +       "        phi = unify(q, alpha, {})\n",
    +       "        if phi is not None:\n",
    +       "            yield phi\n",
    +       "\n",
    +       "    while True:\n",
    +       "        new = []\n",
    +       "        for rule in KB.clauses:\n",
    +       "            p, q = parse_definite_clause(rule)\n",
    +       "            for theta in enum_subst(p):\n",
    +       "                if set(subst(theta, p)).issubset(set(KB.clauses)):\n",
    +       "                    q_ = subst(theta, q)\n",
    +       "                    if all([unify(x, q_, {}) is None for x in KB.clauses + new]):\n",
    +       "                        new.append(q_)\n",
    +       "                        phi = unify(q_, alpha, {})\n",
    +       "                        if phi is not None:\n",
    +       "                            yield phi\n",
    +       "        if not new:\n",
    +       "            break\n",
    +       "        for clause in new:\n",
    +       "            KB.tell(clause)\n",
    +       "    return None\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(fol_fc_ask)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's find out all the hostile nations. Note that we only told the `KB` that Nono was an enemy of America, not that it was hostile." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{x: Nono}]\n" + ] + } + ], + "source": [ + "answer = fol_fc_ask(crime_kb, expr('Hostile(x)'))\n", + "print(list(answer))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The generator returned a single substitution which says that Nono is a hostile nation. See how after adding another enemy nation the generator returns two substitutions." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{x: Nono}, {x: JaJa}]\n" + ] + } + ], + "source": [ + "crime_kb.tell(expr('Enemy(JaJa, America)'))\n", + "answer = fol_fc_ask(crime_kb, expr('Hostile(x)'))\n", + "print(list(answer))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: `fol_fc_ask` makes changes to the `KB` by adding sentences to it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Backward Chaining Algorithm\n", + "This algorithm works backward from the goal, chaining through rules to find known facts that support the proof. Suppose `goal` is the query we want to find the substitution for. We find rules of the form $\\text{lhs} \\implies \\text{goal}$ in the `KB` and try to prove `lhs`. There may be multiple clauses in the `KB` which give multiple `lhs`. It is sufficient to prove only one of these. But to prove a `lhs` all the conjuncts in the `lhs` of the clause must be proved. This makes it similar to And/Or search." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### OR\n", + "The OR part of the algorithm comes from our choice to select any clause of the form $\\text{lhs} \\implies \\text{goal}$. Looking at all rules's `lhs` whose `rhs` unify with the `goal`, we yield a substitution which proves all the conjuncts in the `lhs`. We use `parse_definite_clause` to attain `lhs` and `rhs` from a clause of the form $\\text{lhs} \\implies \\text{rhs}$. For atomic facts the `lhs` is an empty list." + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def fol_bc_or(KB, goal, theta):\n",
    +       "    for rule in KB.fetch_rules_for_goal(goal):\n",
    +       "        lhs, rhs = parse_definite_clause(standardize_variables(rule))\n",
    +       "        for theta1 in fol_bc_and(KB, lhs, unify(rhs, goal, theta)):\n",
    +       "            yield theta1\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(fol_bc_or)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### AND\n", + "The AND corresponds to proving all the conjuncts in the `lhs`. We need to find a substitution which proves each and every clause in the list of conjuncts." + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def fol_bc_and(KB, goals, theta):\n",
    +       "    if theta is None:\n",
    +       "        pass\n",
    +       "    elif not goals:\n",
    +       "        yield theta\n",
    +       "    else:\n",
    +       "        first, rest = goals[0], goals[1:]\n",
    +       "        for theta1 in fol_bc_or(KB, subst(theta, first), theta):\n",
    +       "            for theta2 in fol_bc_and(KB, rest, theta1):\n",
    +       "                yield theta2\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(fol_bc_and)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the main function `fl_bc_ask` calls `fol_bc_or` with substitution initialized as empty. The `ask` method of `FolKB` uses `fol_bc_ask` and fetches the first substitution returned by the generator to answer query. Let's query the knowledge base we created from `clauses` to find hostile nations." + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Rebuild KB because running fol_fc_ask would add new facts to the KB\n", + "crime_kb = FolKB(clauses)" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{v_5: x, x: Nono}" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "crime_kb.ask(expr('Hostile(x)'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may notice some new variables in the substitution. They are introduced to standardize the variable names to prevent naming problems as discussed in the [Unification section](#Unification)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Appendix: The Implementation of `|'==>'|`\n", + "\n", + "Consider the `Expr` formed by this syntax:" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(P ==> ~Q)" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P |'==>'| ~Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What is the funny `|'==>'|` syntax? The trick is that \"`|`\" is just the regular Python or-operator, and so is exactly equivalent to this: " + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(P ==> ~Q)" + ] + }, + "execution_count": 92, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(P | '==>') | ~Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In other words, there are two applications of or-operators. Here's the first one:" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PartialExpr('==>', P)" + ] + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P | '==>'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What is going on here is that the `__or__` method of `Expr` serves a dual purpose. If the right-hand-side is another `Expr` (or a number), then the result is an `Expr`, as in `(P | Q)`. But if the right-hand-side is a string, then the string is taken to be an operator, and we create a node in the abstract syntax tree corresponding to a partially-filled `Expr`, one where we know the left-hand-side is `P` and the operator is `==>`, but we don't yet know the right-hand-side.\n", + "\n", + "The `PartialExpr` class has an `__or__` method that says to create an `Expr` node with the right-hand-side filled in. Here we can see the combination of the `PartialExpr` with `Q` to create a complete `Expr`:" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(P ==> ~Q)" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "partial = PartialExpr('==>', P) \n", + "partial | ~Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This [trick](http://code.activestate.com/recipes/384122-infix-operators/) is due to [Ferdinand Jamitzky](http://code.activestate.com/recipes/users/98863/), with a modification by [C. G. Vedant](https://github.com/Chipe1),\n", + "who suggested using a string inside the or-bars.\n", + "\n", + "## Appendix: The Implementation of `expr`\n", + "\n", + "How does `expr` parse a string into an `Expr`? It turns out there are two tricks (besides the Jamitzky/Vedant trick):\n", + "\n", + "1. We do a string substitution, replacing \"`==>`\" with \"`|'==>'|`\" (and likewise for other operators).\n", + "2. We `eval` the resulting string in an environment in which every identifier\n", + "is bound to a symbol with that identifier as the `op`.\n", + "\n", + "In other words," + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(~(P & Q) ==> (~P | ~Q))" + ] + }, + "execution_count": 95, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr('~(P & Q) ==> (~P | ~Q)')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "is equivalent to doing:" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(~(P & Q) ==> (~P | ~Q))" + ] + }, + "execution_count": 96, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P, Q = symbols('P, Q')\n", + "~(P & Q) |'==>'| (~P | ~Q)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One thing to beware of: this puts `==>` at the same precedence level as `\"|\"`, which is not quite right. For example, we get this:" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(((P & Q) ==> P) | Q)" + ] + }, + "execution_count": 97, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P & Q |'==>'| P | Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which is probably not what we meant; when in doubt, put in extra parens:" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((P & Q) ==> (P | Q))" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(P & Q) |'==>'| (P | Q)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "
    \n", + "\n", + "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from notebook import Canvas_fol_bc_ask\n", + "canvas_bc_ask = Canvas_fol_bc_ask('canvas_bc_ask', crime_kb, expr('Criminal(x)'))" ] }, { @@ -687,7 +4975,7 @@ "source": [ "# Authors\n", "\n", - "This notebook by [Chirag Vertak](https://github.com/chiragvartak) and [Peter Norvig](https://github.com/norvig).\n", + "This notebook by [Chirag Vartak](https://github.com/chiragvartak) and [Peter Norvig](https://github.com/norvig).\n", "\n" ] } @@ -708,9 +4996,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.6.1" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/logic.py b/logic.py index 8b5e8bf8e..1624d55a5 100644 --- a/logic.py +++ b/logic.py @@ -1,4 +1,5 @@ -"""Representations and Inference for Logic (Chapters 7-9, 12) +""" +Representations and Inference for Logic. (Chapters 7-9, 12) Covers both Propositional and First-Order Logic. First we have four important data types: @@ -13,7 +14,7 @@ Logical expressions can be created with Expr or expr, imported from utils, TODO or with expr, which adds the capability to write a string that uses the connectives ==>, <==, <=>, or <=/=>. But be careful: these have the -opertor precedence of commas; you may need to add parens to make precendence work. +operator precedence of commas; you may need to add parens to make precedence work. See logic.ipynb for examples. Then we implement various functions for doing logical inference: @@ -31,24 +32,23 @@ diff, simp Symbolic differentiation and simplification """ -from utils import ( - removeall, unique, first, argmax, probability, - isnumber, issequence, Symbol, Expr, expr, subexpressions -) -import agents - +import heapq import itertools import random -from collections import defaultdict +from collections import defaultdict, Counter -# ______________________________________________________________________________ +import networkx as nx +from agents import Agent, Glitter, Bump, Stench, Breeze, Scream +from csp import parse_neighbors, UniversalDict +from search import astar_search, PlanRoute +from utils import remove_all, unique, first, probability, isnumber, issequence, Expr, expr, subexpressions, extend -class KB: +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? + tell, ask_generator, and retract. Why ask_generator instead of ask? The book is a bit vague on what ask means -- For a Propositional Logic KB, ask(P & Q) returns True or False, but for an FOL KB, something like ask(Brother(x, y)) might return many substitutions @@ -57,10 +57,11 @@ class KB: first one or returns False.""" def __init__(self, sentence=None): - raise NotImplementedError + if sentence: + self.tell(sentence) def tell(self, sentence): - "Add the sentence to the KB." + """Add the sentence to the KB.""" raise NotImplementedError def ask(self, query): @@ -68,82 +69,93 @@ def ask(self, query): return first(self.ask_generator(query), default=False) def ask_generator(self, query): - "Yield all the substitutions that make query true." + """Yield all the substitutions that make query true.""" raise NotImplementedError def retract(self, sentence): - "Remove sentence from the KB." + """Remove sentence from the KB.""" raise NotImplementedError class PropKB(KB): - - "A KB for propositional logic. Inefficient, with no indexing." + """A KB for propositional logic. Inefficient, with no indexing.""" def __init__(self, sentence=None): + super().__init__(sentence) self.clauses = [] - if sentence: - self.tell(sentence) def tell(self, sentence): - "Add the sentence's clauses to the KB." + """Add the sentence's clauses to the KB.""" self.clauses.extend(conjuncts(to_cnf(sentence))) def ask_generator(self, query): - "Yield the empty substitution {} if KB entails query; else no results." + """Yield the empty substitution {} if KB entails query; else no results.""" if tt_entails(Expr('&', *self.clauses), query): yield {} def ask_if_true(self, query): - "Return True if the KB entails query, else return False." + """Return True if the KB entails query, else return False.""" for _ in self.ask_generator(query): return True return False def retract(self, sentence): - "Remove the sentence's clauses from the KB." + """Remove the sentence's clauses from the KB.""" for c in conjuncts(to_cnf(sentence)): if c in self.clauses: self.clauses.remove(c) + # ______________________________________________________________________________ -def KB_AgentProgram(KB): - """A generic logical knowledge-based agent program. [Figure 7.1]""" +def KBAgentProgram(kb): + """ + [Figure 7.1] + A generic logical knowledge-based agent program. + """ steps = itertools.count() def program(percept): t = next(steps) - KB.tell(make_percept_sentence(percept, t)) - action = KB.ask(make_action_query(t)) - KB.tell(make_action_sentence(action, t)) + kb.tell(make_percept_sentence(percept, t)) + action = kb.ask(make_action_query(t)) + kb.tell(make_action_sentence(action, t)) return action - def make_percept_sentence(self, percept, t): - return Expr("Percept")(percept, t) + def make_percept_sentence(percept, t): + return Expr('Percept')(percept, t) - def make_action_query(self, t): - return expr("ShouldDo(action, {})".format(t)) + def make_action_query(t): + return expr('ShouldDo(action, {})'.format(t)) - def make_action_sentence(self, action, t): - return Expr("Did")(action[expr('action')], t) + def make_action_sentence(action, t): + return Expr('Did')(action[expr('action')], t) return program def is_symbol(s): - "A string s is a symbol if it starts with an alphabetic char." + """A string s is a symbol if it starts with an alphabetic char. + >>> is_symbol('R2D2') + True + """ return isinstance(s, str) and s[:1].isalpha() def is_var_symbol(s): - "A logic variable symbol is an initial-lowercase string." + """A logic variable symbol is an initial-lowercase string. + >>> is_var_symbol('EXE') + False + """ return is_symbol(s) and s[0].islower() def is_prop_symbol(s): - """A proposition logic symbol is an initial-uppercase string.""" + """A proposition logic symbol is an initial-uppercase string. + >>> is_prop_symbol('exe') + False + """ return is_symbol(s) and s[0].isupper() @@ -156,8 +168,8 @@ def variables(s): 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 + """Returns True for exprs s of the form A & B & ... & C ==> D, + where all literals are positive. In clause form, this is ~A | ~B | ... | ~C | D, where exactly one clause is positive. >>> is_definite_clause(expr('Farmer(Mac)')) True @@ -166,14 +178,13 @@ def is_definite_clause(s): return True elif s.op == '==>': antecedent, consequent = s.args - return (is_symbol(consequent.op) and - all(is_symbol(arg.op) for arg in conjuncts(antecedent))) + return is_symbol(consequent.op) and all(is_symbol(arg.op) for arg in conjuncts(antecedent)) else: return False def parse_definite_clause(s): - "Return the antecedents and the consequent of a definite clause." + """Return the antecedents and the consequent of a definite clause.""" assert is_definite_clause(s) if is_symbol(s.op): return [], s @@ -181,26 +192,30 @@ def parse_definite_clause(s): antecedent, consequent = s.args return conjuncts(antecedent), consequent + # Useful constant Exprs used in examples and code: -A, B, C, D, E, F, G, P, Q, x, y, z = map(Expr, 'ABCDEFGPQxyz') +A, B, C, D, E, F, G, P, Q, a, x, y, z, u = map(Expr, 'ABCDEFGPQaxyzu') # ______________________________________________________________________________ def tt_entails(kb, alpha): - """Does kb entail the sentence alpha? Use truth tables. For propositional - kb's and sentences. [Figure 7.10]. Note that the 'kb' should be an - Expr which is a conjunction of clauses. + """ + [Figure 7.10] + Does kb entail the sentence alpha? Use truth tables. For propositional + kb's and sentences. Note that the 'kb' should be an Expr which is a + conjunction of clauses. >>> tt_entails(expr('P & Q'), expr('Q')) True """ assert not variables(alpha) - return tt_check_all(kb, alpha, prop_symbols(kb & alpha), {}) + symbols = list(prop_symbols(kb & alpha)) + return tt_check_all(kb, alpha, symbols, {}) def tt_check_all(kb, alpha, symbols, model): - "Auxiliary routine to implement tt_entails." + """Auxiliary routine to implement tt_entails.""" if not symbols: if pl_true(kb, model): result = pl_true(alpha, model) @@ -215,13 +230,33 @@ def tt_check_all(kb, alpha, symbols, model): def prop_symbols(x): - "Return a list of all propositional symbols in x." + """Return the set of all propositional symbols in x.""" if not isinstance(x, Expr): - return [] + return set() elif is_prop_symbol(x.op): - return [x] + return {x} else: - return list(set(symbol for arg in x.args for symbol in prop_symbols(arg))) + return {symbol for arg in x.args for symbol in prop_symbols(arg)} + + +def constant_symbols(x): + """Return the set of all constant symbols in x.""" + if not isinstance(x, Expr): + return set() + elif is_prop_symbol(x.op) and not x.args: + return {x} + else: + return {symbol for arg in x.args for symbol in constant_symbols(arg)} + + +def predicate_symbols(x): + """Return a set of (symbol_name, arity) in x. + All symbols (even functional) with arity > 0 are considered.""" + if not isinstance(x, Expr) or not x.args: + return set() + pred_set = {(x.op, len(x.args))} if is_prop_symbol(x.op) else set() + pred_set.update({symbol for arg in x.args for symbol in predicate_symbols(arg)}) + return pred_set def tt_true(s): @@ -237,7 +272,10 @@ 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 every proposition, this may return None to indicate 'not obvious'; - this may happen even when the expression is tautological.""" + this may happen even when the expression is tautological. + >>> pl_true(P, {}) is None + True + """ if exp in (True, False): return exp op, args = exp.op, exp.args @@ -283,7 +321,8 @@ def pl_true(exp, model={}): elif op == '^': # xor or 'not equivalent' return pt != qt else: - raise ValueError("illegal operator in logic expression" + str(exp)) + raise ValueError('Illegal operator in logic expression' + str(exp)) + # ______________________________________________________________________________ @@ -291,8 +330,10 @@ def pl_true(exp, model={}): def to_cnf(s): - """Convert a propositional logical sentence to conjunctive normal form. - That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 253] + """ + [Page 253] + Convert a propositional logical sentence to conjunctive normal form. + That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) >>> to_cnf('~(B | C)') (~B & ~C) """ @@ -305,7 +346,7 @@ def to_cnf(s): def eliminate_implications(s): - "Change implications into equivalent form with only &, |, and ~ as logical operators." + """Change implications into equivalent form with only &, |, and ~ as logical operators.""" s = expr(s) if not s.args or is_symbol(s.op): return s # Atoms are unchanged. @@ -328,11 +369,13 @@ def eliminate_implications(s): def move_not_inwards(s): """Rewrite sentence s by moving negation sign inward. >>> move_not_inwards(~(A | B)) - (~A & ~B)""" + (~A & ~B) + """ s = expr(s) if s.op == '~': def NOT(b): return move_not_inwards(~b) + a = s.args[0] if a.op == '~': return move_not_inwards(a.args[0]) # ~~A ==> A @@ -392,12 +435,16 @@ def associate(op, args): else: return Expr(op, *args) + _op_identity = {'&': True, '|': False, '+': 0, '*': 1} 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).""" + that Expr(op, *result) means the same as Expr(op, *args). + >>> dissociate('&', [A & B]) + [A, B] + """ result = [] def collect(subargs): @@ -406,6 +453,7 @@ def collect(subargs): collect(arg.args) else: result.append(arg) + collect(args) return result @@ -429,17 +477,23 @@ def disjuncts(s): """ return dissociate('|', [s]) + # ______________________________________________________________________________ -def pl_resolution(KB, alpha): - "Propositional-logic resolution: say if alpha follows from KB. [Figure 7.12]" - clauses = KB.clauses + conjuncts(to_cnf(~alpha)) +def pl_resolution(kb, alpha): + """ + [Figure 7.12] + Propositional-logic resolution: say if alpha follows from KB. + >>> pl_resolution(horn_clauses_KB, A) + True + """ + clauses = kb.clauses + conjuncts(to_cnf(~alpha)) new = set() while True: n = len(clauses) pairs = [(clauses[i], clauses[j]) - for i in range(n) for j in range(i+1, n)] + 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: @@ -458,25 +512,23 @@ def pl_resolve(ci, cj): for di in disjuncts(ci): for dj in disjuncts(cj): if di == ~dj or ~di == dj: - dnew = unique(removeall(di, disjuncts(ci)) + - removeall(dj, disjuncts(cj))) - clauses.append(associate('|', dnew)) + clauses.append(associate('|', unique(remove_all(di, disjuncts(ci)) + remove_all(dj, disjuncts(cj))))) return clauses + # ______________________________________________________________________________ class PropDefiniteKB(PropKB): - - "A KB of propositional definite clauses." + """A KB of propositional definite clauses.""" def tell(self, sentence): - "Add a definite clause to this KB." + """Add a definite clause to this KB.""" assert is_definite_clause(sentence), "Must be definite clause" self.clauses.append(sentence) def ask_generator(self, query): - "Yield the empty substitution if KB implies query; else nothing." + """Yield the empty substitution if KB implies query; else nothing.""" if pl_fc_entails(self.clauses, query): yield {} @@ -486,83 +538,207 @@ def retract(self, sentence): def clauses_with_premise(self, p): """Return a list of the clauses in KB that have p in their premise. This could be cached away for O(1) speed, but we'll recompute it.""" - return [c for c in self.clauses - if c.op == '==>' and p in conjuncts(c.args[0])] + 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. +def pl_fc_entails(kb, q): + """ [Figure 7.15] + Use forward chaining to see if a PropDefiniteKB entails symbol q. >>> pl_fc_entails(horn_clauses_KB, expr('Q')) True """ - count = {c: len(conjuncts(c.args[0])) - for c in KB.clauses - if c.op == '==>'} + count = {c: len(conjuncts(c.args[0])) for c in kb.clauses if c.op == '==>'} inferred = defaultdict(bool) - agenda = [s for s in KB.clauses if is_prop_symbol(s.op)] + agenda = [s for s in kb.clauses if is_prop_symbol(s.op)] while agenda: p = agenda.pop() if p == q: return True if not inferred[p]: inferred[p] = True - for c in KB.clauses_with_premise(p): + for c in kb.clauses_with_premise(p): count[c] -= 1 if count[c] == 0: agenda.append(c.args[1]) return False -""" [Figure 7.13] + +""" +[Figure 7.13] Simple inference in a wumpus world example """ -wumpus_world_inference = expr("(B11 <=> (P12 | P21)) & ~B11") - +wumpus_world_inference = expr('(B11 <=> (P12 | P21)) & ~B11') -""" [Figure 7.16] +""" +[Figure 7.16] Propositional Logic Forward Chaining example """ horn_clauses_KB = PropDefiniteKB() -for s in "P==>Q; (L&M)==>P; (B&L)==>M; (A&P)==>L; (A&B)==>L; A;B".split(';'): - horn_clauses_KB.tell(expr(s)) +for clause in ['P ==> Q', + '(L & M) ==> P', + '(B & L) ==> M', + '(A & P) ==> L', + '(A & B) ==> L', + 'A', 'B']: + horn_clauses_KB.tell(expr(clause)) + +""" +Definite clauses KB example +""" +definite_clauses_KB = PropDefiniteKB() +for clause in ['(B & F) ==> E', + '(A & E & F) ==> G', + '(B & C) ==> F', + '(A & B) ==> D', + '(E & F) ==> H', + '(H & I) ==>J', + 'A', 'B', 'C']: + definite_clauses_KB.tell(expr(clause)) + + +# ______________________________________________________________________________ +# Heuristics for SAT Solvers + + +def no_branching_heuristic(symbols, clauses): + return first(symbols), True + + +def min_clauses(clauses): + min_len = min(map(lambda c: len(c.args), clauses), default=2) + return filter(lambda c: len(c.args) == (min_len if min_len > 1 else 2), clauses) + + +def moms(symbols, clauses): + """ + MOMS (Maximum Occurrence in clauses of Minimum Size) heuristic + Returns the literal with the most occurrences in all clauses of minimum size + """ + scores = Counter(l for c in min_clauses(clauses) for l in prop_symbols(c)) + return max(symbols, key=lambda symbol: scores[symbol]), True + + +def momsf(symbols, clauses, k=0): + """ + MOMS alternative heuristic + If f(x) the number of occurrences of the variable x in clauses with minimum size, + we choose the variable maximizing [f(x) + f(-x)] * 2^k + f(x) * f(-x) + Returns x if f(x) >= f(-x) otherwise -x + """ + scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c)) + P = max(symbols, + key=lambda symbol: (scores[symbol] + scores[~symbol]) * pow(2, k) + scores[symbol] * scores[~symbol]) + return P, True if scores[P] >= scores[~P] else False + + +def posit(symbols, clauses): + """ + Freeman's POSIT version of MOMs + Counts the positive x and negative x for each variable x in clauses with minimum size + Returns x if f(x) >= f(-x) otherwise -x + """ + scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c)) + P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol]) + return P, True if scores[P] >= scores[~P] else False + + +def zm(symbols, clauses): + """ + Zabih and McAllester's version of MOMs + Counts the negative occurrences only of each variable x in clauses with minimum size + """ + scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c) if l.op == '~') + return max(symbols, key=lambda symbol: scores[~symbol]), True + + +def dlis(symbols, clauses): + """ + DLIS (Dynamic Largest Individual Sum) heuristic + Choose the variable and value that satisfies the maximum number of unsatisfied clauses + Like DLCS but we only consider the literal (thus Cp and Cn are individual) + """ + scores = Counter(l for c in clauses for l in disjuncts(c)) + P = max(symbols, key=lambda symbol: scores[symbol]) + return P, True if scores[P] >= scores[~P] else False + + +def dlcs(symbols, clauses): + """ + DLCS (Dynamic Largest Combined Sum) heuristic + Cp the number of clauses containing literal x + Cn the number of clauses containing literal -x + Here we select the variable maximizing Cp + Cn + Returns x if Cp >= Cn otherwise -x + """ + scores = Counter(l for c in clauses for l in disjuncts(c)) + P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol]) + return P, True if scores[P] >= scores[~P] else False + + +def jw(symbols, clauses): + """ + Jeroslow-Wang heuristic + For each literal compute J(l) = \sum{l in clause c} 2^{-|c|} + Return the literal maximizing J + """ + scores = Counter() + for c in clauses: + for l in prop_symbols(c): + scores[l] += pow(2, -len(c.args)) + return max(symbols, key=lambda symbol: scores[symbol]), True + + +def jw2(symbols, clauses): + """ + Two Sided Jeroslow-Wang heuristic + Compute J(l) also counts the negation of l = J(x) + J(-x) + Returns x if J(x) >= J(-x) otherwise -x + """ + scores = Counter() + for c in clauses: + for l in disjuncts(c): + scores[l] += pow(2, -len(c.args)) + P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol]) + return P, True if scores[P] >= scores[~P] else False + # ______________________________________________________________________________ # DPLL-Satisfiable [Figure 7.17] -def dpll_satisfiable(s): +def dpll_satisfiable(s, branching_heuristic=no_branching_heuristic): """Check satisfiability of a propositional sentence. This differs from the book code in two ways: (1) it returns a model rather than True when it succeeds; this is more useful. (2) The function find_pure_symbol is passed a list of unknown clauses, rather - than a list of all clauses and the model; this is more efficient.""" - clauses = conjuncts(to_cnf(s)) - symbols = prop_symbols(s) - return dpll(clauses, symbols, {}) + than a list of all clauses and the model; this is more efficient. + >>> dpll_satisfiable(A |'<=>'| B) == {A: True, B: True} + True + """ + return dpll(conjuncts(to_cnf(s)), prop_symbols(s), {}, branching_heuristic) -def dpll(clauses, symbols, model): - "See if the clauses are true in a partial model." +def dpll(clauses, symbols, model, branching_heuristic=no_branching_heuristic): + """See if the clauses are true in a partial model.""" unknown_clauses = [] # clauses with an unknown truth value for c in clauses: val = pl_true(c, model) if val is False: return False - if val is not True: + if val is None: unknown_clauses.append(c) if not unknown_clauses: return model P, value = find_pure_symbol(symbols, unknown_clauses) if P: - return dpll(clauses, removeall(P, symbols), extend(model, P, value)) + return dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic) P, value = find_unit_clause(clauses, model) if P: - return dpll(clauses, removeall(P, symbols), extend(model, P, value)) - if not symbols: - raise TypeError("Argument should be of the type Expr.") - P, symbols = symbols[0], symbols[1:] - return (dpll(clauses, symbols, extend(model, P, True)) or - dpll(clauses, symbols, extend(model, P, False))) + return dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic) + P, value = branching_heuristic(symbols, unknown_clauses) + return (dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic) or + dpll(clauses, remove_all(P, symbols), extend(model, P, not value), branching_heuristic)) def find_pure_symbol(symbols, clauses): @@ -613,7 +789,7 @@ def unit_clause_assign(clause, model): if model[sym] == positive: return None, None # clause already True elif P: - return None, None # more than 1 unbound variable + return None, None # more than 1 unbound variable else: P, value = sym, positive return P, value @@ -632,15 +808,285 @@ def inspect_literal(literal): else: return literal, True + +# ______________________________________________________________________________ +# CDCL - Conflict-Driven Clause Learning with 1UIP Learning Scheme, +# 2WL Lazy Data Structure, VSIDS Branching Heuristic & Restarts + + +def no_restart(conflicts, restarts, queue_lbd, sum_lbd): + return False + + +def luby(conflicts, restarts, queue_lbd, sum_lbd, unit=512): + # in the state-of-art tested with unit value 1, 2, 4, 6, 8, 12, 16, 32, 64, 128, 256 and 512 + def _luby(i): + k = 1 + while True: + if i == (1 << k) - 1: + return 1 << (k - 1) + elif (1 << (k - 1)) <= i < (1 << k) - 1: + return _luby(i - (1 << (k - 1)) + 1) + k += 1 + + return unit * _luby(restarts) == len(queue_lbd) + + +def glucose(conflicts, restarts, queue_lbd, sum_lbd, x=100, k=0.7): + # in the state-of-art tested with (x, k) as (50, 0.8) and (100, 0.7) + # if there were at least x conflicts since the last restart, and then the average LBD of the last + # x learnt clauses was at least k times higher than the average LBD of all learnt clauses + return len(queue_lbd) >= x and sum(queue_lbd) / len(queue_lbd) * k > sum_lbd / conflicts + + +def cdcl_satisfiable(s, vsids_decay=0.95, restart_strategy=no_restart): + """ + >>> cdcl_satisfiable(A |'<=>'| B) == {A: True, B: True} + True + """ + clauses = TwoWLClauseDatabase(conjuncts(to_cnf(s))) + symbols = prop_symbols(s) + scores = Counter() + G = nx.DiGraph() + model = {} + dl = 0 + conflicts = 0 + restarts = 1 + sum_lbd = 0 + queue_lbd = [] + while True: + conflict = unit_propagation(clauses, symbols, model, G, dl) + if conflict: + if dl == 0: + return False + conflicts += 1 + dl, learn, lbd = conflict_analysis(G, dl) + queue_lbd.append(lbd) + sum_lbd += lbd + backjump(symbols, model, G, dl) + clauses.add(learn, model) + scores.update(l for l in disjuncts(learn)) + for symbol in scores: + scores[symbol] *= vsids_decay + if restart_strategy(conflicts, restarts, queue_lbd, sum_lbd): + backjump(symbols, model, G) + queue_lbd.clear() + restarts += 1 + else: + if not symbols: + return model + dl += 1 + assign_decision_literal(symbols, model, scores, G, dl) + + +def assign_decision_literal(symbols, model, scores, G, dl): + P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol]) + value = True if scores[P] >= scores[~P] else False + symbols.remove(P) + model[P] = value + G.add_node(P, val=value, dl=dl) + + +def unit_propagation(clauses, symbols, model, G, dl): + def check(c): + if not model or clauses.get_first_watched(c) == clauses.get_second_watched(c): + return True + w1, _ = inspect_literal(clauses.get_first_watched(c)) + if w1 in model: + return c in (clauses.get_neg_watched(w1) if model[w1] else clauses.get_pos_watched(w1)) + w2, _ = inspect_literal(clauses.get_second_watched(c)) + if w2 in model: + return c in (clauses.get_neg_watched(w2) if model[w2] else clauses.get_pos_watched(w2)) + + def unit_clause(watching): + w, p = inspect_literal(watching) + G.add_node(w, val=p, dl=dl) + G.add_edges_from(zip(prop_symbols(c) - {w}, itertools.cycle([w])), antecedent=c) + symbols.remove(w) + model[w] = p + + def conflict_clause(c): + G.add_edges_from(zip(prop_symbols(c), itertools.cycle('K')), antecedent=c) + + while True: + bcp = False + for c in filter(check, clauses.get_clauses()): + # we need only visit each clause when one of its two watched literals is assigned to 0 because, until + # this happens, we can guarantee that there cannot be more than n-2 literals in the clause assigned to 0 + first_watched = pl_true(clauses.get_first_watched(c), model) + second_watched = pl_true(clauses.get_second_watched(c), model) + if first_watched is None and clauses.get_first_watched(c) == clauses.get_second_watched(c): + unit_clause(clauses.get_first_watched(c)) + bcp = True + break + elif first_watched is False and second_watched is not True: + if clauses.update_second_watched(c, model): + bcp = True + else: + # if the only literal with a non-zero value is the other watched literal then + if second_watched is None: # if it is free, then the clause is a unit clause + unit_clause(clauses.get_second_watched(c)) + bcp = True + break + else: # else (it is False) the clause is a conflict clause + conflict_clause(c) + return True + elif second_watched is False and first_watched is not True: + if clauses.update_first_watched(c, model): + bcp = True + else: + # if the only literal with a non-zero value is the other watched literal then + if first_watched is None: # if it is free, then the clause is a unit clause + unit_clause(clauses.get_first_watched(c)) + bcp = True + break + else: # else (it is False) the clause is a conflict clause + conflict_clause(c) + return True + if not bcp: + return False + + +def conflict_analysis(G, dl): + conflict_clause = next(G[p]['K']['antecedent'] for p in G.pred['K']) + P = next(node for node in G.nodes() - 'K' if G.nodes[node]['dl'] == dl and G.in_degree(node) == 0) + first_uip = nx.immediate_dominators(G, P)['K'] + G.remove_node('K') + conflict_side = nx.descendants(G, first_uip) + while True: + for l in prop_symbols(conflict_clause).intersection(conflict_side): + antecedent = next(G[p][l]['antecedent'] for p in G.pred[l]) + conflict_clause = pl_binary_resolution(conflict_clause, antecedent) + # the literal block distance is calculated by taking the decision levels from variables of all + # literals in the clause, and counting how many different decision levels were in this set + lbd = [G.nodes[l]['dl'] for l in prop_symbols(conflict_clause)] + if lbd.count(dl) == 1 and first_uip in prop_symbols(conflict_clause): + return 0 if len(lbd) == 1 else heapq.nlargest(2, lbd)[-1], conflict_clause, len(set(lbd)) + + +def pl_binary_resolution(ci, cj): + for di in disjuncts(ci): + for dj in disjuncts(cj): + if di == ~dj or ~di == dj: + return pl_binary_resolution(associate('|', remove_all(di, disjuncts(ci))), + associate('|', remove_all(dj, disjuncts(cj)))) + return associate('|', unique(disjuncts(ci) + disjuncts(cj))) + + +def backjump(symbols, model, G, dl=0): + delete = {node for node in G.nodes() if G.nodes[node]['dl'] > dl} + G.remove_nodes_from(delete) + for node in delete: + del model[node] + symbols |= delete + + +class TwoWLClauseDatabase: + + def __init__(self, clauses): + self.__twl = {} + self.__watch_list = defaultdict(lambda: [set(), set()]) + for c in clauses: + self.add(c, None) + + def get_clauses(self): + return self.__twl.keys() + + def set_first_watched(self, clause, new_watching): + if len(clause.args) > 2: + self.__twl[clause][0] = new_watching + + def set_second_watched(self, clause, new_watching): + if len(clause.args) > 2: + self.__twl[clause][1] = new_watching + + def get_first_watched(self, clause): + if len(clause.args) == 2: + return clause.args[0] + if len(clause.args) > 2: + return self.__twl[clause][0] + return clause + + def get_second_watched(self, clause): + if len(clause.args) == 2: + return clause.args[-1] + if len(clause.args) > 2: + return self.__twl[clause][1] + return clause + + def get_pos_watched(self, l): + return self.__watch_list[l][0] + + def get_neg_watched(self, l): + return self.__watch_list[l][1] + + def add(self, clause, model): + self.__twl[clause] = self.__assign_watching_literals(clause, model) + w1, p1 = inspect_literal(self.get_first_watched(clause)) + w2, p2 = inspect_literal(self.get_second_watched(clause)) + self.__watch_list[w1][0].add(clause) if p1 else self.__watch_list[w1][1].add(clause) + if w1 != w2: + self.__watch_list[w2][0].add(clause) if p2 else self.__watch_list[w2][1].add(clause) + + def remove(self, clause): + w1, p1 = inspect_literal(self.get_first_watched(clause)) + w2, p2 = inspect_literal(self.get_second_watched(clause)) + del self.__twl[clause] + self.__watch_list[w1][0].discard(clause) if p1 else self.__watch_list[w1][1].discard(clause) + if w1 != w2: + self.__watch_list[w2][0].discard(clause) if p2 else self.__watch_list[w2][1].discard(clause) + + def update_first_watched(self, clause, model): + # if a non-zero literal different from the other watched literal is found + found, new_watching = self.__find_new_watching_literal(clause, self.get_first_watched(clause), model) + if found: # then it will replace the watched literal + w, p = inspect_literal(self.get_second_watched(clause)) + self.__watch_list[w][0].remove(clause) if p else self.__watch_list[w][1].remove(clause) + self.set_second_watched(clause, new_watching) + w, p = inspect_literal(new_watching) + self.__watch_list[w][0].add(clause) if p else self.__watch_list[w][1].add(clause) + return True + + def update_second_watched(self, clause, model): + # if a non-zero literal different from the other watched literal is found + found, new_watching = self.__find_new_watching_literal(clause, self.get_second_watched(clause), model) + if found: # then it will replace the watched literal + w, p = inspect_literal(self.get_first_watched(clause)) + self.__watch_list[w][0].remove(clause) if p else self.__watch_list[w][1].remove(clause) + self.set_first_watched(clause, new_watching) + w, p = inspect_literal(new_watching) + self.__watch_list[w][0].add(clause) if p else self.__watch_list[w][1].add(clause) + return True + + def __find_new_watching_literal(self, clause, other_watched, model): + # if a non-zero literal different from the other watched literal is found + if len(clause.args) > 2: + for l in disjuncts(clause): + if l != other_watched and pl_true(l, model) is not False: + # then it is returned + return True, l + return False, None + + def __assign_watching_literals(self, clause, model=None): + if len(clause.args) > 2: + if model is None or not model: + return [clause.args[0], clause.args[-1]] + else: + return [next(l for l in disjuncts(clause) if pl_true(l, model) is None), + next(l for l in disjuncts(clause) if pl_true(l, model) is False)] + + # ______________________________________________________________________________ # Walk-SAT [Figure 7.18] def WalkSAT(clauses, p=0.5, max_flips=10000): """Checks for satisfiability of all clauses by randomly flipping values of variables + >>> WalkSAT([A & ~A], 0.5, 100) is None + True """ # Set of all symbols in all clauses - symbols = set(sym for clause in clauses for sym in prop_symbols(clause)) + symbols = {sym for clause in clauses for sym in prop_symbols(clause)} # model is a random assignment of true/false to the symbols in clauses model = {s: random.choice([True, False]) for s in symbols} for i in range(max_flips): @@ -651,7 +1097,7 @@ def WalkSAT(clauses, p=0.5, max_flips=10000): return model clause = random.choice(unsatisfied) if probability(p): - sym = random.choice(prop_symbols(clause)) + sym = random.choice(list(prop_symbols(clause))) else: # Flip the symbol in clause that maximizes number of sat. clauses def sat_count(sym): @@ -660,31 +1106,527 @@ def sat_count(sym): count = len([clause for clause in clauses if pl_true(clause, model)]) model[sym] = not model[sym] return count - sym = argmax(prop_symbols(clause), key=sat_count) + + sym = max(prop_symbols(clause), key=sat_count) model[sym] = not model[sym] # If no solution is found within the flip limit, we return failure return None + # ______________________________________________________________________________ +# Map Coloring SAT Problems + +def MapColoringSAT(colors, neighbors): + """Make a SAT for the problem of coloring a map with different colors + for any two adjacent regions. Arguments are a list of colors, and a + dict of {region: [neighbor,...]} entries. This dict may also be + specified as a string of the form defined by parse_neighbors.""" + if isinstance(neighbors, str): + neighbors = parse_neighbors(neighbors) + colors = UniversalDict(colors) + clauses = [] + for state in neighbors.keys(): + clause = [expr(state + '_' + c) for c in colors[state]] + clauses.append(clause) + for t in itertools.combinations(clause, 2): + clauses.append([~t[0], ~t[1]]) + visited = set() + adj = set(neighbors[state]) - visited + visited.add(state) + for n_state in adj: + for col in colors[n_state]: + clauses.append([expr('~' + state + '_' + col), expr('~' + n_state + '_' + col)]) + return associate('&', map(lambda c: associate('|', c), clauses)) + + +australia_sat = MapColoringSAT(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """) + +france_sat = MapColoringSAT(list('RGBY'), + """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: + PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA: + AU BO FC PA LR""") + +usa_sat = MapColoringSAT(list('RGBY'), + """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 AZ; + 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; + LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL; + MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL; + PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ; + NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH; + HI: ; AK: """) -class HybridWumpusAgent(agents.Agent): - "An agent for the wumpus world that does logical inference. [Figure 7.20]""" +# ______________________________________________________________________________ + + +# Expr functions for WumpusKB and HybridWumpusAgent + +def facing_east(time): + return Expr('FacingEast', time) + + +def facing_west(time): + return Expr('FacingWest', time) + + +def facing_north(time): + return Expr('FacingNorth', time) + + +def facing_south(time): + return Expr('FacingSouth', time) + + +def wumpus(x, y): + return Expr('W', x, y) + + +def pit(x, y): + return Expr('P', x, y) + + +def breeze(x, y): + return Expr('B', x, y) + + +def stench(x, y): + return Expr('S', x, y) + + +def wumpus_alive(time): + return Expr('WumpusAlive', time) + + +def have_arrow(time): + return Expr('HaveArrow', time) + + +def percept_stench(time): + return Expr('Stench', time) + + +def percept_breeze(time): + return Expr('Breeze', time) + + +def percept_glitter(time): + return Expr('Glitter', time) + + +def percept_bump(time): + return Expr('Bump', time) - def __init__(self): - raise NotImplementedError +def percept_scream(time): + return Expr('Scream', time) + + +def move_forward(time): + return Expr('Forward', time) + + +def shoot(time): + return Expr('Shoot', time) + + +def turn_left(time): + return Expr('TurnLeft', time) + + +def turn_right(time): + return Expr('TurnRight', time) + + +def ok_to_move(x, y, time): + return Expr('OK', x, y, time) + + +def location(x, y, time=None): + if time is None: + return Expr('L', x, y) + else: + return Expr('L', x, y, time) + + +# Symbols + +def implies(lhs, rhs): + return Expr('==>', lhs, rhs) + + +def equiv(lhs, rhs): + return Expr('<=>', lhs, rhs) + + +# Helper Function + +def new_disjunction(sentences): + t = sentences[0] + for i in range(1, len(sentences)): + t |= sentences[i] + return t + + +# ______________________________________________________________________________ + + +class WumpusKB(PropKB): + """ + Create a Knowledge Base that contains the a temporal "Wumpus physics" and temporal rules with time zero. + """ + + def __init__(self, dimrow): + super().__init__() + self.dimrow = dimrow + self.tell(~wumpus(1, 1)) + self.tell(~pit(1, 1)) + + for y in range(1, dimrow + 1): + for x in range(1, dimrow + 1): + + pits_in = list() + wumpus_in = list() + + if x > 1: # West room exists + pits_in.append(pit(x - 1, y)) + wumpus_in.append(wumpus(x - 1, y)) + + if y < dimrow: # North room exists + pits_in.append(pit(x, y + 1)) + wumpus_in.append(wumpus(x, y + 1)) + + if x < dimrow: # East room exists + pits_in.append(pit(x + 1, y)) + wumpus_in.append(wumpus(x + 1, y)) + + if y > 1: # South room exists + pits_in.append(pit(x, y - 1)) + wumpus_in.append(wumpus(x, y - 1)) + + self.tell(equiv(breeze(x, y), new_disjunction(pits_in))) + self.tell(equiv(stench(x, y), new_disjunction(wumpus_in))) + + # Rule that describes existence of at least one Wumpus + wumpus_at_least = list() + for x in range(1, dimrow + 1): + for y in range(1, dimrow + 1): + wumpus_at_least.append(wumpus(x, y)) + + self.tell(new_disjunction(wumpus_at_least)) + + # Rule that describes existence of at most one Wumpus + for i in range(1, dimrow + 1): + for j in range(1, dimrow + 1): + for u in range(1, dimrow + 1): + for v in range(1, dimrow + 1): + if i != u or j != v: + self.tell(~wumpus(i, j) | ~wumpus(u, v)) + + # Temporal rules at time zero + self.tell(location(1, 1, 0)) + for i in range(1, dimrow + 1): + for j in range(1, dimrow + 1): + self.tell(implies(location(i, j, 0), equiv(percept_breeze(0), breeze(i, j)))) + self.tell(implies(location(i, j, 0), equiv(percept_stench(0), stench(i, j)))) + if i != 1 or j != 1: + self.tell(~location(i, j, 0)) + + self.tell(wumpus_alive(0)) + self.tell(have_arrow(0)) + self.tell(facing_east(0)) + self.tell(~facing_north(0)) + self.tell(~facing_south(0)) + self.tell(~facing_west(0)) + + def make_action_sentence(self, action, time): + actions = [move_forward(time), shoot(time), turn_left(time), turn_right(time)] + + for a in actions: + if action is a: + self.tell(action) + else: + self.tell(~a) + + def make_percept_sentence(self, percept, time): + # Glitter, Bump, Stench, Breeze, Scream + flags = [0, 0, 0, 0, 0] + + # Things perceived + if isinstance(percept, Glitter): + flags[0] = 1 + self.tell(percept_glitter(time)) + elif isinstance(percept, Bump): + flags[1] = 1 + self.tell(percept_bump(time)) + elif isinstance(percept, Stench): + flags[2] = 1 + self.tell(percept_stench(time)) + elif isinstance(percept, Breeze): + flags[3] = 1 + self.tell(percept_breeze(time)) + elif isinstance(percept, Scream): + flags[4] = 1 + self.tell(percept_scream(time)) + + # Things not perceived + for i in range(len(flags)): + if flags[i] == 0: + if i == 0: + self.tell(~percept_glitter(time)) + elif i == 1: + self.tell(~percept_bump(time)) + elif i == 2: + self.tell(~percept_stench(time)) + elif i == 3: + self.tell(~percept_breeze(time)) + elif i == 4: + self.tell(~percept_scream(time)) + + def add_temporal_sentences(self, time): + if time == 0: + return + t = time - 1 + + # current location rules + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + self.tell(implies(location(i, j, time), equiv(percept_breeze(time), breeze(i, j)))) + self.tell(implies(location(i, j, time), equiv(percept_stench(time), stench(i, j)))) + s = list() + s.append(equiv(location(i, j, time), location(i, j, time) & ~move_forward(time) | percept_bump(time))) + if i != 1: + s.append(location(i - 1, j, t) & facing_east(t) & move_forward(t)) + if i != self.dimrow: + s.append(location(i + 1, j, t) & facing_west(t) & move_forward(t)) + if j != 1: + s.append(location(i, j - 1, t) & facing_north(t) & move_forward(t)) + if j != self.dimrow: + s.append(location(i, j + 1, t) & facing_south(t) & move_forward(t)) + + # add sentence about location i,j + self.tell(new_disjunction(s)) + + # add sentence about safety of location i,j + self.tell(equiv(ok_to_move(i, j, time), ~pit(i, j) & ~wumpus(i, j) & wumpus_alive(time))) + + # Rules about current orientation + + a = facing_north(t) & turn_right(t) + b = facing_south(t) & turn_left(t) + c = facing_east(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_east(time), a | b | c) + self.tell(s) + + a = facing_north(t) & turn_left(t) + b = facing_south(t) & turn_right(t) + c = facing_west(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_west(time), a | b | c) + self.tell(s) + + a = facing_east(t) & turn_left(t) + b = facing_west(t) & turn_right(t) + c = facing_north(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_north(time), a | b | c) + self.tell(s) + + a = facing_west(t) & turn_left(t) + b = facing_east(t) & turn_right(t) + c = facing_south(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_south(time), a | b | c) + self.tell(s) + + # Rules about last action + self.tell(equiv(move_forward(t), ~turn_right(t) & ~turn_left(t))) + + # Rule about the arrow + self.tell(equiv(have_arrow(time), have_arrow(t) & ~shoot(t))) + + # Rule about Wumpus (dead or alive) + self.tell(equiv(wumpus_alive(time), wumpus_alive(t) & ~percept_scream(time))) + + def ask_if_true(self, query): + return pl_resolution(self, query) -def plan_route(current, goals, allowed): - raise NotImplementedError # ______________________________________________________________________________ -def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable): - """Converts a planning problem to Satisfaction problem by translating it to a cnf sentence. - [Figure 7.22]""" +class WumpusPosition: + def __init__(self, x, y, orientation): + self.X = x + self.Y = y + self.orientation = orientation + + def get_location(self): + return self.X, self.Y + + def set_location(self, x, y): + self.X = x + self.Y = y + + def get_orientation(self): + return self.orientation + + def set_orientation(self, orientation): + self.orientation = orientation + + def __eq__(self, other): + if other.get_location() == self.get_location() and other.get_orientation() == self.get_orientation(): + return True + else: + return False + + +# ______________________________________________________________________________ + + +class HybridWumpusAgent(Agent): + """ + [Figure 7.20] + An agent for the wumpus world that does logical inference. + """ + + def __init__(self, dimentions): + self.dimrow = dimentions + self.kb = WumpusKB(self.dimrow) + self.t = 0 + self.plan = list() + self.current_position = WumpusPosition(1, 1, 'UP') + super().__init__(self.execute) + + def execute(self, percept): + self.kb.make_percept_sentence(percept, self.t) + self.kb.add_temporal_sentences(self.t) + + temp = list() + + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if self.kb.ask_if_true(location(i, j, self.t)): + temp.append(i) + temp.append(j) + + if self.kb.ask_if_true(facing_north(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'UP') + elif self.kb.ask_if_true(facing_south(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN') + elif self.kb.ask_if_true(facing_west(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT') + elif self.kb.ask_if_true(facing_east(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT') + + safe_points = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if self.kb.ask_if_true(ok_to_move(i, j, self.t)): + safe_points.append([i, j]) + + if self.kb.ask_if_true(percept_glitter(self.t)): + goals = list() + goals.append([1, 1]) + self.plan.append('Grab') + actions = self.plan_route(self.current_position, goals, safe_points) + self.plan.extend(actions) + self.plan.append('Climb') + + if len(self.plan) == 0: + unvisited = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + for k in range(self.t): + if self.kb.ask_if_true(location(i, j, k)): + unvisited.append([i, j]) + unvisited_and_safe = list() + for u in unvisited: + for s in safe_points: + if u not in unvisited_and_safe and s == u: + unvisited_and_safe.append(u) + + temp = self.plan_route(self.current_position, unvisited_and_safe, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0 and self.kb.ask_if_true(have_arrow(self.t)): + possible_wumpus = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if not self.kb.ask_if_true(wumpus(i, j)): + possible_wumpus.append([i, j]) + + temp = self.plan_shot(self.current_position, possible_wumpus, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0: + not_unsafe = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if not self.kb.ask_if_true(ok_to_move(i, j, self.t)): + not_unsafe.append([i, j]) + temp = self.plan_route(self.current_position, not_unsafe, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0: + start = list() + start.append([1, 1]) + temp = self.plan_route(self.current_position, start, safe_points) + self.plan.extend(temp) + self.plan.append('Climb') + + action = self.plan[0] + self.plan = self.plan[1:] + self.kb.make_action_sentence(action, self.t) + self.t += 1 + + return action + + def plan_route(self, current, goals, allowed): + problem = PlanRoute(current, goals, allowed, self.dimrow) + return astar_search(problem).solution() + + def plan_shot(self, current, goals, allowed): + shooting_positions = set() + + for loc in goals: + x = loc[0] + y = loc[1] + for i in range(1, self.dimrow + 1): + if i < x: + shooting_positions.add(WumpusPosition(i, y, 'EAST')) + if i > x: + shooting_positions.add(WumpusPosition(i, y, 'WEST')) + if i < y: + shooting_positions.add(WumpusPosition(x, i, 'NORTH')) + if i > y: + shooting_positions.add(WumpusPosition(x, i, 'SOUTH')) + + # Can't have a shooting position from any of the rooms the Wumpus could reside + orientations = ['EAST', 'WEST', 'NORTH', 'SOUTH'] + for loc in goals: + for orientation in orientations: + shooting_positions.remove(WumpusPosition(loc[0], loc[1], orientation)) + + actions = list() + actions.extend(self.plan_route(current, shooting_positions, allowed)) + actions.append('Shoot') + return actions + + +# ______________________________________________________________________________ + + +def SAT_plan(init, transition, goal, t_max, SAT_solver=cdcl_satisfiable): + """ + [Figure 7.22] + Converts a planning problem to Satisfaction problem by translating it to a cnf sentence. + >>> transition = {'A': {'Left': 'A', 'Right': 'B'}, 'B': {'Left': 'A', 'Right': 'C'}, 'C': {'Left': 'B', 'Right': 'C'}} + >>> SAT_plan('A', transition, 'C', 1) is None + True + """ # Functions used by SAT_plan def translate_to_SAT(init, transition, goal, time): @@ -694,14 +1636,16 @@ def translate_to_SAT(init, transition, goal, time): # Symbol claiming state s at time t state_counter = itertools.count() for s in states: - for t in range(time+1): - state_sym[s, t] = Expr("State_{}".format(next(state_counter))) + for t in range(time + 1): + state_sym[s, t] = Expr('S_{}'.format(next(state_counter))) # Add initial state axiom clauses.append(state_sym[init, 0]) # Add goal state axiom - clauses.append(state_sym[goal, time]) + clauses.append(state_sym[first(clause[0] for clause in state_sym + if set(conjuncts(clause[0])).issuperset(conjuncts(goal))), time]) \ + if isinstance(goal, Expr) else clauses.append(state_sym[goal, time]) # All possible transitions transition_counter = itertools.count() @@ -710,14 +1654,14 @@ def translate_to_SAT(init, transition, goal, time): s_ = transition[s][action] for t in range(time): # Action 'action' taken from state 's' at time 't' to reach 's_' - action_sym[s, action, t] = Expr("Transition_{}".format(next(transition_counter))) + action_sym[s, action, t] = Expr('T_{}'.format(next(transition_counter))) # Change the state from s to s_ - clauses.append(action_sym[s, action, t] |'==>'| state_sym[s, t]) - clauses.append(action_sym[s, action, t] |'==>'| state_sym[s_, t + 1]) + clauses.append(action_sym[s, action, t] | '==>' | state_sym[s, t]) + clauses.append(action_sym[s, action, t] | '==>' | state_sym[s_, t + 1]) # Allow only one state at any time - for t in range(time+1): + for t in range(time + 1): # must be a state at any time clauses.append(associate('|', [state_sym[s, t] for s in states])) @@ -735,7 +1679,7 @@ def translate_to_SAT(init, transition, goal, time): clauses.append(associate('|', [action_sym[tr] for tr in transitions_t])) for tr in transitions_t: - for tr_ in transitions_t[transitions_t.index(tr) + 1 :]: + for tr_ in transitions_t[transitions_t.index(tr) + 1:]: # there cannot be two transitions tr and tr_ at time t clauses.append(~action_sym[tr] | ~action_sym[tr_]) @@ -749,7 +1693,7 @@ def extract_solution(model): return [action for s, action, time in true_transitions] # Body of SAT_plan algorithm - for t in range(t_max): + for t in range(t_max + 1): # dictionaries to help extract the solution from model state_sym = {} action_sym = {} @@ -764,10 +1708,15 @@ def extract_solution(model): # ______________________________________________________________________________ -def unify(x, y, s): - """Unify expressions x,y with substitution s; return a substitution that +def unify(x, y, s={}): + """ + [Figure 9.1] + 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 - variables (e.g. Expr('x')), constants, lists, or Exprs. [Figure 9.1]""" + variables (e.g. Expr('x')), constants, lists, or Exprs. + >>> unify(x, 3, {}) + {x: 3} + """ if s is None: return None elif x == y: @@ -789,17 +1738,21 @@ def unify(x, y, s): def is_variable(x): - "A variable is an Expr with no args and a lowercase symbol as the op." + """A variable is an Expr with no args and a lowercase symbol as the op.""" return isinstance(x, Expr) and not x.args and x.op[0].islower() def unify_var(var, x, s): if var in s: return unify(s[var], x, s) + elif x in s: + return unify(var, s[x], s) elif occur_check(var, x, s): return None else: - return extend(s, var, x) + new_s = extend(s, var, x) + cascade_substitution(new_s) + return new_s def occur_check(var, x, s): @@ -818,13 +1771,6 @@ def occur_check(var, x, s): return False -def extend(s, var, val): - "Copy the substitution s and extend it by setting var to val; return copy." - s2 = s.copy() - 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) @@ -842,8 +1788,97 @@ def subst(s, x): return Expr(x.op, *[subst(s, arg) for arg in x.args]) -def fol_fc_ask(KB, alpha): - raise NotImplementedError +def cascade_substitution(s): + """This method allows to return a correct unifier in normal form + and perform a cascade substitution to s. + For every mapping in s perform a cascade substitution on s.get(x) + and if it is replaced with a function ensure that all the function + terms are correct updates by passing over them again. + >>> s = {x: y, y: G(z)} + >>> cascade_substitution(s) + >>> s == {x: G(z), y: G(z)} + True + """ + + for x in s: + s[x] = subst(s, s.get(x)) + if isinstance(s.get(x), Expr) and not is_variable(s.get(x)): + # Ensure Function Terms are correct updates by passing over them again + s[x] = subst(s, s.get(x)) + + +def unify_mm(x, y, s={}): + """Unify expressions x,y with substitution s using an efficient rule-based + unification algorithm by Martelli & Montanari; return a substitution that + would make x,y equal, or None if x,y can not unify. x and y can be + variables (e.g. Expr('x')), constants, lists, or Exprs. + >>> unify_mm(x, 3, {}) + {x: 3} + """ + + set_eq = extend(s, x, y) + s = set_eq.copy() + while True: + trans = 0 + for x, y in set_eq.items(): + if x == y: + # if x = y this mapping is deleted (rule b) + del s[x] + elif not is_variable(x) and is_variable(y): + # if x is not a variable and y is a variable, rewrite it as y = x in s (rule a) + if s.get(y, None) is None: + s[y] = x + del s[x] + else: + # if a mapping already exist for variable y then apply + # variable elimination (there is a chance to apply rule d) + s[x] = vars_elimination(y, s) + elif not is_variable(x) and not is_variable(y): + # in which case x and y are not variables, if the two root function symbols + # are different, stop with failure, else apply term reduction (rule c) + if x.op is y.op and len(x.args) == len(y.args): + term_reduction(x, y, s) + del s[x] + else: + return None + elif isinstance(y, Expr): + # in which case x is a variable and y is a function or a variable (e.g. F(z) or y), + # if y is a function, we must check if x occurs in y, then stop with failure, else + # try to apply variable elimination to y (rule d) + if occur_check(x, y, s): + return None + s[x] = vars_elimination(y, s) + if y == s.get(x): + trans += 1 + else: + trans += 1 + if trans == len(set_eq): + # if no transformation has been applied, stop with success + return s + set_eq = s.copy() + + +def term_reduction(x, y, s): + """Apply term reduction to x and y if both are functions and the two root function + symbols are equals (e.g. F(x1, x2, ..., xn) and F(x1', x2', ..., xn')) by returning + a new mapping obtained by replacing x: y with {x1: x1', x2: x2', ..., xn: xn'} + """ + for i in range(len(x.args)): + if x.args[i] in s: + s[s.get(x.args[i])] = y.args[i] + else: + s[x.args[i]] = y.args[i] + + +def vars_elimination(x, s): + """Apply variable elimination to x: if x is a variable and occurs in s, return + the term mapped by x, else if x is a function recursively applies variable + elimination to each term of the function.""" + if not isinstance(x, Expr): + return x + if is_variable(x): + return s.get(x, x) + return Expr(x.op, *[vars_elimination(arg, s) for arg in x.args]) def standardize_variables(sentence, dic=None): @@ -860,16 +1895,29 @@ def standardize_variables(sentence, dic=None): dic[sentence] = v return v else: - return Expr(sentence.op, - *[standardize_variables(a, dic) for a in sentence.args]) + return Expr(sentence.op, *[standardize_variables(a, dic) for a in sentence.args]) + standardize_variables.counter = itertools.count() + # ______________________________________________________________________________ -class FolKB(KB): +def parse_clauses_from_dimacs(dimacs_cnf): + """Converts a string into CNF clauses according to the DIMACS format used in SAT competitions""" + return map(lambda c: associate('|', c), + map(lambda c: [expr('~X' + str(abs(l))) if l < 0 else expr('X' + str(l)) for l in c], + map(lambda line: map(int, line.split()), + filter(None, ' '.join( + filter(lambda line: line[0] not in ('c', 'p'), + filter(None, dimacs_cnf.strip().replace('\t', ' ').split('\n')))).split(' 0'))))) + + +# ______________________________________________________________________________ + +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)')]) @@ -881,16 +1929,18 @@ class FolKB(KB): False """ - def __init__(self, initial_clauses=[]): + def __init__(self, clauses=None): + super().__init__() self.clauses = [] # inefficient: no indexing - for clause in initial_clauses: - self.tell(clause) + if clauses: + for clause in clauses: + self.tell(clause) def tell(self, sentence): if is_definite_clause(sentence): self.clauses.append(sentence) else: - raise Exception("Not a definite clause: {}".format(sentence)) + raise Exception('Not a definite clause: {}'.format(sentence)) def ask_generator(self, query): return fol_bc_ask(self, query) @@ -902,62 +1952,111 @@ def fetch_rules_for_goal(self, goal): return self.clauses -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)' - ])) - -crime_kb = FolKB( - map(expr, - ['(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)', # noqa - '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. [Figure 9.6] - KB should be an instance of FolKB, and query an atomic sentence. """ - return fol_bc_or(KB, query, {}) - - -def fol_bc_or(KB, goal, theta): - for rule in KB.fetch_rules_for_goal(goal): +def fol_fc_ask(kb, alpha): + """ + [Figure 9.3] + A simple forward-chaining algorithm. + """ + # TODO: improve efficiency + kb_consts = list({c for clause in kb.clauses for c in constant_symbols(clause)}) + + def enum_subst(p): + query_vars = list({v for clause in p for v in variables(clause)}) + for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)): + theta = {x: y for x, y in zip(query_vars, assignment_list)} + yield theta + + # check if we can answer without new inferences + for q in kb.clauses: + phi = unify_mm(q, alpha) + if phi is not None: + yield phi + + while True: + new = [] + for rule in kb.clauses: + p, q = parse_definite_clause(rule) + for theta in enum_subst(p): + if set(subst(theta, p)).issubset(set(kb.clauses)): + q_ = subst(theta, q) + if all([unify_mm(x, q_) is None for x in kb.clauses + new]): + new.append(q_) + phi = unify_mm(q_, alpha) + if phi is not None: + yield phi + if not new: + break + for clause in new: + kb.tell(clause) + return None + + +def fol_bc_ask(kb, query): + """ + [Figure 9.6] + A simple backward-chaining algorithm for first-order logic. + KB should be an instance of FolKB, and query an atomic sentence. + """ + 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)): + for theta1 in fol_bc_and(kb, lhs, unify_mm(rhs, goal, theta)): yield theta1 -def fol_bc_and(KB, goals, theta): +def fol_bc_and(kb, goals, theta): if theta is None: pass elif not goals: yield theta else: first, rest = goals[0], goals[1:] - for theta1 in fol_bc_or(KB, subst(theta, first), theta): - for theta2 in fol_bc_and(KB, rest, theta1): + for theta1 in fol_bc_or(kb, subst(theta, first), theta): + for theta2 in fol_bc_and(kb, rest, theta1): yield theta2 + +# A simple KB that defines the relevant conditions of the Wumpus World as in Figure 7.4. +# See Sec. 7.4.3 +wumpus_kb = PropKB() + +P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21') +wumpus_kb.tell(~P11) +wumpus_kb.tell(B11 | '<=>' | (P12 | P21)) +wumpus_kb.tell(B21 | '<=>' | (P11 | P22 | P31)) +wumpus_kb.tell(~B11) +wumpus_kb.tell(B21) + +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)'])) + +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)'])) + + # ______________________________________________________________________________ # Example application (not in the book). -# You can use the Expr class to do symbolic differentiation. This used to be +# 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. @@ -984,18 +2083,18 @@ def diff(y, 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)) + 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: {} in diff({}, {})".format(op, y, x)) + raise ValueError('Unknown op: {} in diff({}, {})'.format(op, y, x)) def simp(x): - "Simplify the expression x." + """Simplify the expression x.""" if isnumber(x) or not x.args: return x args = list(map(simp, x.args)) @@ -1052,11 +2151,14 @@ def simp(x): if u == 1: return 0 else: - raise ValueError("Unknown op: " + op) + 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." + """Differentiate and then simplify. + >>> d(x * x - x, x) + ((2 * x) - 1) + """ return simp(diff(y, x)) diff --git a/logic4e.py b/logic4e.py new file mode 100644 index 000000000..75608ad74 --- /dev/null +++ b/logic4e.py @@ -0,0 +1,1665 @@ +"""Representations and Inference for Logic (Chapters 7-10) + +Covers both Propositional and First-Order Logic. First we have four +important data types: + + KB Abstract class holds a knowledge base of logical expressions + KB_Agent Abstract class subclasses agents.Agent + Expr A logical expression, imported from utils.py + substitution Implemented as a dictionary of var:value pairs, {x:1, y:x} + +Be careful: some functions take an Expr as argument, and some take a KB. + +Logical expressions can be created with Expr or expr, imported from utils, TODO +or with expr, which adds the capability to write a string that uses +the connectives ==>, <==, <=>, or <=/=>. But be careful: these have the +operator precedence of commas; you may need to add parents to make precedence work. +See logic.ipynb for examples. + +Then we implement various functions for doing logical inference: + + pl_true Evaluate a propositional logical sentence in a model + tt_entails Say if a statement is entailed by a KB + pl_resolution Do resolution on propositional sentences + dpll_satisfiable See if a propositional sentence is satisfiable + WalkSAT Try to find a solution for a set of clauses + +And a few other functions: + + to_cnf Convert to conjunctive normal form + unify Do unification of two FOL sentences + diff, simp Symbolic differentiation and simplification +""" +import itertools +import random +from collections import defaultdict + +from agents import Agent, Glitter, Bump, Stench, Breeze, Scream +from search import astar_search, PlanRoute +from utils4e import remove_all, unique, first, probability, isnumber, issequence, Expr, expr, subexpressions + + +# ______________________________________________________________________________ +# Chapter 7 Logical Agents +# 7.1 Knowledge Based Agents + + +class KB: + """ + A knowledge base to which you can tell and ask sentences. + To create a KB, subclass this class and implement tell, ask_generator, and retract. + Ask_generator: + For a Propositional Logic KB, ask(P & Q) returns True or False, but for an + FOL KB, something like ask(Brother(x, y)) might return many substitutions + such as {x: Cain, y: Abel}, {x: Abel, y: Cain}, {x: George, y: Jeb}, etc. + So ask_generator generates these one at a time, and ask either returns the + first one or returns False. + """ + + def __init__(self, sentence=None): + raise NotImplementedError + + def tell(self, sentence): + """Add the sentence to the KB.""" + raise NotImplementedError + + def ask(self, query): + """Return a substitution that makes the query true, or, failing that, return False.""" + return first(self.ask_generator(query), default=False) + + def ask_generator(self, query): + """Yield all the substitutions that make query true.""" + raise NotImplementedError + + def retract(self, sentence): + """Remove sentence from the KB.""" + raise NotImplementedError + + +class PropKB(KB): + """A KB for propositional logic. Inefficient, with no indexing.""" + + def __init__(self, sentence=None): + self.clauses = [] + if sentence: + self.tell(sentence) + + def tell(self, sentence): + """Add the sentence's clauses to the KB.""" + self.clauses.extend(conjuncts(to_cnf(sentence))) + + def ask_generator(self, query): + """Yield the empty substitution {} if KB entails query; else no results.""" + if tt_entails(Expr('&', *self.clauses), query): + yield {} + + def ask_if_true(self, query): + """Return True if the KB entails query, else return False.""" + for _ in self.ask_generator(query): + return True + return False + + def retract(self, sentence): + """Remove the sentence's clauses from the KB.""" + for c in conjuncts(to_cnf(sentence)): + if c in self.clauses: + self.clauses.remove(c) + + +def KB_AgentProgram(KB): + """A generic logical knowledge-based agent program. [Figure 7.1]""" + steps = itertools.count() + + def program(percept): + t = next(steps) + KB.tell(make_percept_sentence(percept, t)) + action = KB.ask(make_action_query(t)) + KB.tell(make_action_sentence(action, t)) + return action + + def make_percept_sentence(percept, t): + return Expr("Percept")(percept, t) + + def make_action_query(t): + return expr("ShouldDo(action, {})".format(t)) + + def make_action_sentence(action, t): + return Expr("Did")(action[expr('action')], t) + + return program + + +# _____________________________________________________________________________ +# 7.2 The Wumpus World + + +# Expr functions for WumpusKB and HybridWumpusAgent + + +def facing_east(time): + return Expr('FacingEast', time) + + +def facing_west(time): + return Expr('FacingWest', time) + + +def facing_north(time): + return Expr('FacingNorth', time) + + +def facing_south(time): + return Expr('FacingSouth', time) + + +def wumpus(x, y): + return Expr('W', x, y) + + +def pit(x, y): + return Expr('P', x, y) + + +def breeze(x, y): + return Expr('B', x, y) + + +def stench(x, y): + return Expr('S', x, y) + + +def wumpus_alive(time): + return Expr('WumpusAlive', time) + + +def have_arrow(time): + return Expr('HaveArrow', time) + + +def percept_stench(time): + return Expr('Stench', time) + + +def percept_breeze(time): + return Expr('Breeze', time) + + +def percept_glitter(time): + return Expr('Glitter', time) + + +def percept_bump(time): + return Expr('Bump', time) + + +def percept_scream(time): + return Expr('Scream', time) + + +def move_forward(time): + return Expr('Forward', time) + + +def shoot(time): + return Expr('Shoot', time) + + +def turn_left(time): + return Expr('TurnLeft', time) + + +def turn_right(time): + return Expr('TurnRight', time) + + +def ok_to_move(x, y, time): + return Expr('OK', x, y, time) + + +def location(x, y, time=None): + if time is None: + return Expr('L', x, y) + else: + return Expr('L', x, y, time) + + +# Symbols + + +def implies(lhs, rhs): + return Expr('==>', lhs, rhs) + + +def equiv(lhs, rhs): + return Expr('<=>', lhs, rhs) + + +# Helper Function + + +def new_disjunction(sentences): + t = sentences[0] + for i in range(1, len(sentences)): + t |= sentences[i] + return t + + +# ______________________________________________________________________________ +# 7.4 Propositional Logic + + +def is_symbol(s): + """A string s is a symbol if it starts with an alphabetic char. + >>> is_symbol('R2D2') + True + """ + return isinstance(s, str) and s[:1].isalpha() + + +def is_var_symbol(s): + """A logic variable symbol is an initial-lowercase string. + >>> is_var_symbol('EXE') + False + """ + return is_symbol(s) and s[0].islower() + + +def is_prop_symbol(s): + """A proposition logic symbol is an initial-uppercase string. + >>> is_prop_symbol('exe') + False + """ + return is_symbol(s) and s[0].isupper() + + +def variables(s): + """Return a set of the variables in expression s. + >>> variables(expr('F(x, x) & G(x, y) & H(y, z) & R(A, z, 2)')) == {x, y, z} + True + """ + return {x for x in subexpressions(s) if is_variable(x)} + + +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 + ~A | ~B | ... | ~C | D, where exactly one clause is positive. + >>> is_definite_clause(expr('Farmer(Mac)')) + True + """ + if is_symbol(s.op): + return True + elif s.op == '==>': + antecedent, consequent = s.args + return (is_symbol(consequent.op) and + all(is_symbol(arg.op) for arg in conjuncts(antecedent))) + else: + return False + + +def parse_definite_clause(s): + """Return the antecedents and the consequent of a definite clause.""" + assert is_definite_clause(s) + if is_symbol(s.op): + return [], s + else: + antecedent, consequent = s.args + return conjuncts(antecedent), consequent + + +# Useful constant Exprs used in examples and code: +A, B, C, D, E, F, G, P, Q, x, y, z = map(Expr, 'ABCDEFGPQxyz') + + +# ______________________________________________________________________________ +# 7.4.4 A simple inference procedure + + +def tt_entails(kb, alpha): + """ + Does kb entail the sentence alpha? Use truth tables. For propositional + kb's and sentences. [Figure 7.10]. Note that the 'kb' should be an + Expr which is a conjunction of clauses. + >>> tt_entails(expr('P & Q'), expr('Q')) + True + """ + assert not variables(alpha) + symbols = list(prop_symbols(kb & alpha)) + return tt_check_all(kb, alpha, symbols, {}) + + +def tt_check_all(kb, alpha, symbols, model): + """Auxiliary routine to implement tt_entails.""" + if not symbols: + if pl_true(kb, model): + result = pl_true(alpha, model) + assert result in (True, False) + return result + else: + return True + else: + P, rest = symbols[0], symbols[1:] + 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 the set of all propositional symbols in x.""" + if not isinstance(x, Expr): + return set() + elif is_prop_symbol(x.op): + return {x} + else: + return {symbol for arg in x.args for symbol in prop_symbols(arg)} + + +def constant_symbols(x): + """Return the set of all constant symbols in x.""" + if not isinstance(x, Expr): + return set() + elif is_prop_symbol(x.op) and not x.args: + return {x} + else: + return {symbol for arg in x.args for symbol in constant_symbols(arg)} + + +def predicate_symbols(x): + """ + Return a set of (symbol_name, arity) in x. + All symbols (even functional) with arity > 0 are considered. + """ + if not isinstance(x, Expr) or not x.args: + return set() + pred_set = {(x.op, len(x.args))} if is_prop_symbol(x.op) else set() + pred_set.update({symbol for arg in x.args for symbol in predicate_symbols(arg)}) + return pred_set + + +def tt_true(s): + """Is a propositional sentence a tautology? + >>> tt_true('P | ~P') + True + """ + s = expr(s) + return tt_entails(True, s) + + +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 + every proposition, this may return None to indicate 'not obvious'; + this may happen even when the expression is tautological. + >>> pl_true(P, {}) is None + True + """ + if exp in (True, False): + return exp + op, args = exp.op, exp.args + if is_prop_symbol(op): + return model.get(exp) + elif op == '~': + p = pl_true(args[0], model) + 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 + 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 + return result + p, q = args + if op == '==>': + return pl_true(~p | q, model) + elif op == '<==': + return pl_true(p | ~q, model) + pt = pl_true(p, model) + if pt is None: + return None + qt = pl_true(q, model) + if qt is None: + return None + if op == '<=>': + return pt == qt + elif op == '^': # xor or 'not equivalent' + return pt != qt + else: + raise ValueError("illegal operator in logic expression" + str(exp)) + + +# ______________________________________________________________________________ +# 7.5 Propositional Theorem Proving + + +def to_cnf(s): + """Convert a propositional logical sentence to conjunctive normal form. + That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 253] + >>> to_cnf('~(B | C)') + (~B & ~C) + """ + s = expr(s) + 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 implications into equivalent form with only &, |, and ~ as logical operators.""" + s = expr(s) + 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 + elif s.op == '<==': + return a | ~b + elif s.op == '<=>': + return (a | ~b) & (b | ~a) + elif s.op == '^': + 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)) + (~A & ~B) + """ + s = expr(s) + if s.op == '~': + def NOT(b): + return 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('|', 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, *list(map(move_not_inwards, s.args))) + + +def distribute_and_over_or(s): + """Given a sentence s consisting of conjunctions and disjunctions + of literals, return an equivalent sentence in CNF. + >>> distribute_and_over_or((A & B) | C) + ((A | C) & (B | C)) + """ + s = expr(s) + if s.op == '|': + s = associate('|', s.args) + if s.op != '|': + return distribute_and_over_or(s) + if len(s.args) == 0: + return False + if len(s.args) == 1: + return distribute_and_over_or(s.args[0]) + conj = first(arg for arg in s.args if arg.op == '&') + if not conj: + 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) + for c in conj.args]) + elif s.op == '&': + 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 + instances of the same op promoted to the top level. + >>> associate('&', [(A&B),(B|C),(B&C)]) + (A & B & (B | C) & B & C) + >>> associate('|', [A|(B|(C|(A&B)))]) + (A | B | C | (A & B)) + """ + args = dissociate(op, args) + if len(args) == 0: + return _op_identity[op] + elif len(args) == 1: + return args[0] + else: + return Expr(op, *args) + + +_op_identity = {'&': True, '|': False, '+': 0, '*': 1} + + +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). + >>> dissociate('&', [A & B]) + [A, B] + """ + result = [] + + def collect(subargs): + for arg in subargs: + 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) + [A, B] + >>> conjuncts(A | B) + [(A | B)] + """ + return dissociate('&', [s]) + + +def disjuncts(s): + """Return a list of the disjuncts in the sentence s. + >>> disjuncts(A | B) + [A, B] + >>> disjuncts(A & B) + [(A & B)] + """ + return dissociate('|', [s]) + + +# ______________________________________________________________________________ + + +def pl_resolution(KB, alpha): + """ + Propositional-logic resolution: say if alpha follows from KB. [Figure 7.12] + >>> pl_resolution(horn_clauses_KB, A) + True + """ + clauses = KB.clauses + conjuncts(to_cnf(~alpha)) + new = set() + while True: + n = len(clauses) + pairs = [(clauses[i], clauses[j]) + 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 + new = new.union(set(resolvents)) + if new.issubset(set(clauses)): + return False + for c in new: + 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.""" + clauses = [] + for di in disjuncts(ci): + for dj in disjuncts(cj): + if di == ~dj or ~di == dj: + dnew = unique(remove_all(di, disjuncts(ci)) + + remove_all(dj, disjuncts(cj))) + clauses.append(associate('|', dnew)) + return clauses + + +# ______________________________________________________________________________ +# 7.5.4 Forward and backward chaining + + +class PropDefiniteKB(PropKB): + """A KB of propositional definite clauses.""" + + def tell(self, sentence): + """Add a definite clause to this KB.""" + assert is_definite_clause(sentence), "Must be definite clause" + self.clauses.append(sentence) + + def ask_generator(self, query): + """Yield the empty substitution if KB implies query; else nothing.""" + if pl_fc_entails(self.clauses, query): + yield {} + + def retract(self, sentence): + self.clauses.remove(sentence) + + def clauses_with_premise(self, p): + """Return a list of the clauses in KB that have p in their premise. + This could be cached away for O(1) speed, but we'll recompute it.""" + 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. + [Figure 7.15] + >>> pl_fc_entails(horn_clauses_KB, expr('Q')) + True + """ + count = {c: len(conjuncts(c.args[0])) + for c in KB.clauses + 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 not inferred[p]: + inferred[p] = True + for c in KB.clauses_with_premise(p): + count[c] -= 1 + if count[c] == 0: + agenda.append(c.args[1]) + return False + + +""" [Figure 7.13] +Simple inference in a wumpus world example +""" +wumpus_world_inference = expr("(B11 <=> (P12 | P21)) & ~B11") + +""" [Figure 7.16] +Propositional Logic Forward Chaining example +""" +horn_clauses_KB = PropDefiniteKB() +for s in "P==>Q; (L&M)==>P; (B&L)==>M; (A&P)==>L; (A&B)==>L; A;B".split(';'): + horn_clauses_KB.tell(expr(s)) + +""" +Definite clauses KB example +""" +definite_clauses_KB = PropDefiniteKB() +for clause in ['(B & F)==>E', '(A & E & F)==>G', '(B & C)==>F', '(A & B)==>D', '(E & F)==>H', '(H & I)==>J', 'A', 'B', + 'C']: + definite_clauses_KB.tell(expr(clause)) + + +# ______________________________________________________________________________ +# 7.6 Effective Propositional Model Checking +# DPLL-Satisfiable [Figure 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 + rather than True when it succeeds; this is more useful. (2) The + function find_pure_symbol is passed a list of unknown clauses, rather + than a list of all clauses and the model; this is more efficient. + >>> dpll_satisfiable(A |'<=>'| B) == {A: True, B: True} + True + """ + clauses = conjuncts(to_cnf(s)) + symbols = list(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 + for c in clauses: + val = pl_true(c, model) + if val is False: + return False + if val is not True: + unknown_clauses.append(c) + if not unknown_clauses: + return model + P, value = find_pure_symbol(symbols, unknown_clauses) + if P: + return dpll(clauses, remove_all(P, symbols), extend(model, P, value)) + P, value = find_unit_clause(clauses, model) + if P: + return dpll(clauses, remove_all(P, symbols), extend(model, P, value)) + if not symbols: + raise TypeError("Argument should be of the type Expr.") + P, symbols = symbols[0], symbols[1:] + 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. + >>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A]) + (A, True) + """ + 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 + 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. + >>> find_unit_clause([A|B|C, B|~C, ~A|~B], {A:True}) + (B, False) + """ + for clause in clauses: + P, value = unit_clause_assign(clause, model) + 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. + >>> unit_clause_assign(A|B|C, {A:True}) + (None, None) + >>> unit_clause_assign(B|~C, {A:True}) + (None, None) + >>> unit_clause_assign(~A|~B, {A:True}) + (B, False) + """ + P, value = None, None + for literal in disjuncts(clause): + sym, positive = inspect_literal(literal) + if sym in model: + if model[sym] == positive: + return None, None # clause already True + elif P: + return None, None # more than 1 unbound variable + else: + 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. + >>> inspect_literal(P) + (P, True) + >>> inspect_literal(~P) + (P, False) + """ + if literal.op == '~': + return literal.args[0], False + else: + return literal, True + + +# ______________________________________________________________________________ +# 7.6.2 Local search algorithms +# Walk-SAT [Figure 7.18] + + +def WalkSAT(clauses, p=0.5, max_flips=10000): + """ + Checks for satisfiability of all clauses by randomly flipping values of variables + >>> WalkSAT([A & ~A], 0.5, 100) is None + True + """ + # Set of all symbols in all clauses + symbols = {sym for clause in clauses for sym in prop_symbols(clause)} + # model is a random assignment of true/false to the symbols in clauses + model = {s: random.choice([True, False]) for s in symbols} + 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 + return model + clause = random.choice(unsatisfied) + if probability(p): + sym = random.choice(list(prop_symbols(clause))) + else: + # Flip the symbol in clause that maximizes number of sat. clauses + def sat_count(sym): + # Return the the number of clauses satisfied after flipping the symbol. + model[sym] = not model[sym] + count = len([clause for clause in clauses if pl_true(clause, model)]) + model[sym] = not model[sym] + return count + + sym = max(prop_symbols(clause), key=sat_count) + model[sym] = not model[sym] + # If no solution is found within the flip limit, we return failure + return None + + +# ______________________________________________________________________________ +# 7.7 Agents Based on Propositional Logic +# 7.7.1 The current state of the world + + +class WumpusKB(PropKB): + """ + Create a Knowledge Base that contains the atemporal "Wumpus physics" and temporal rules with time zero. + """ + + def __init__(self, dimrow): + super().__init__() + self.dimrow = dimrow + self.tell(~wumpus(1, 1)) + self.tell(~pit(1, 1)) + + for y in range(1, dimrow + 1): + for x in range(1, dimrow + 1): + + pits_in = list() + wumpus_in = list() + + if x > 1: # West room exists + pits_in.append(pit(x - 1, y)) + wumpus_in.append(wumpus(x - 1, y)) + + if y < dimrow: # North room exists + pits_in.append(pit(x, y + 1)) + wumpus_in.append(wumpus(x, y + 1)) + + if x < dimrow: # East room exists + pits_in.append(pit(x + 1, y)) + wumpus_in.append(wumpus(x + 1, y)) + + if y > 1: # South room exists + pits_in.append(pit(x, y - 1)) + wumpus_in.append(wumpus(x, y - 1)) + + self.tell(equiv(breeze(x, y), new_disjunction(pits_in))) + self.tell(equiv(stench(x, y), new_disjunction(wumpus_in))) + + # Rule that describes existence of at least one Wumpus + wumpus_at_least = list() + for x in range(1, dimrow + 1): + for y in range(1, dimrow + 1): + wumpus_at_least.append(wumpus(x, y)) + + self.tell(new_disjunction(wumpus_at_least)) + + # Rule that describes existence of at most one Wumpus + for i in range(1, dimrow + 1): + for j in range(1, dimrow + 1): + for u in range(1, dimrow + 1): + for v in range(1, dimrow + 1): + if i != u or j != v: + self.tell(~wumpus(i, j) | ~wumpus(u, v)) + + # Temporal rules at time zero + self.tell(location(1, 1, 0)) + for i in range(1, dimrow + 1): + for j in range(1, dimrow + 1): + self.tell(implies(location(i, j, 0), equiv(percept_breeze(0), breeze(i, j)))) + self.tell(implies(location(i, j, 0), equiv(percept_stench(0), stench(i, j)))) + if i != 1 or j != 1: + self.tell(~location(i, j, 0)) + + self.tell(wumpus_alive(0)) + self.tell(have_arrow(0)) + self.tell(facing_east(0)) + self.tell(~facing_north(0)) + self.tell(~facing_south(0)) + self.tell(~facing_west(0)) + + def make_action_sentence(self, action, time): + actions = [move_forward(time), shoot(time), turn_left(time), turn_right(time)] + + for a in actions: + if action is a: + self.tell(action) + else: + self.tell(~a) + + def make_percept_sentence(self, percept, time): + # Glitter, Bump, Stench, Breeze, Scream + flags = [0, 0, 0, 0, 0] + + # Things perceived + if isinstance(percept, Glitter): + flags[0] = 1 + self.tell(percept_glitter(time)) + elif isinstance(percept, Bump): + flags[1] = 1 + self.tell(percept_bump(time)) + elif isinstance(percept, Stench): + flags[2] = 1 + self.tell(percept_stench(time)) + elif isinstance(percept, Breeze): + flags[3] = 1 + self.tell(percept_breeze(time)) + elif isinstance(percept, Scream): + flags[4] = 1 + self.tell(percept_scream(time)) + + # Things not perceived + for i in range(len(flags)): + if flags[i] == 0: + if i == 0: + self.tell(~percept_glitter(time)) + elif i == 1: + self.tell(~percept_bump(time)) + elif i == 2: + self.tell(~percept_stench(time)) + elif i == 3: + self.tell(~percept_breeze(time)) + elif i == 4: + self.tell(~percept_scream(time)) + + def add_temporal_sentences(self, time): + if time == 0: + return + t = time - 1 + + # current location rules + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + self.tell(implies(location(i, j, time), equiv(percept_breeze(time), breeze(i, j)))) + self.tell(implies(location(i, j, time), equiv(percept_stench(time), stench(i, j)))) + + s = list() + + s.append( + equiv( + location(i, j, time), location(i, j, time) & ~move_forward(time) | percept_bump(time))) + + if i != 1: + s.append(location(i - 1, j, t) & facing_east(t) & move_forward(t)) + + if i != self.dimrow: + s.append(location(i + 1, j, t) & facing_west(t) & move_forward(t)) + + if j != 1: + s.append(location(i, j - 1, t) & facing_north(t) & move_forward(t)) + + if j != self.dimrow: + s.append(location(i, j + 1, t) & facing_south(t) & move_forward(t)) + + # add sentence about location i,j + self.tell(new_disjunction(s)) + + # add sentence about safety of location i,j + self.tell( + equiv(ok_to_move(i, j, time), ~pit(i, j) & ~wumpus(i, j) & wumpus_alive(time)) + ) + + # Rules about current orientation + + a = facing_north(t) & turn_right(t) + b = facing_south(t) & turn_left(t) + c = facing_east(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_east(time), a | b | c) + self.tell(s) + + a = facing_north(t) & turn_left(t) + b = facing_south(t) & turn_right(t) + c = facing_west(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_west(time), a | b | c) + self.tell(s) + + a = facing_east(t) & turn_left(t) + b = facing_west(t) & turn_right(t) + c = facing_north(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_north(time), a | b | c) + self.tell(s) + + a = facing_west(t) & turn_left(t) + b = facing_east(t) & turn_right(t) + c = facing_south(t) & ~turn_left(t) & ~turn_right(t) + s = equiv(facing_south(time), a | b | c) + self.tell(s) + + # Rules about last action + self.tell(equiv(move_forward(t), ~turn_right(t) & ~turn_left(t))) + + # Rule about the arrow + self.tell(equiv(have_arrow(time), have_arrow(t) & ~shoot(t))) + + # Rule about Wumpus (dead or alive) + self.tell(equiv(wumpus_alive(time), wumpus_alive(t) & ~percept_scream(time))) + + def ask_if_true(self, query): + return pl_resolution(self, query) + + +# ______________________________________________________________________________ + + +class WumpusPosition: + def __init__(self, x, y, orientation): + self.X = x + self.Y = y + self.orientation = orientation + + def get_location(self): + return self.X, self.Y + + def set_location(self, x, y): + self.X = x + self.Y = y + + def get_orientation(self): + return self.orientation + + def set_orientation(self, orientation): + self.orientation = orientation + + def __eq__(self, other): + if (other.get_location() == self.get_location() and + other.get_orientation() == self.get_orientation()): + return True + else: + return False + + +# ______________________________________________________________________________ +# 7.7.2 A hybrid agent + + +class HybridWumpusAgent(Agent): + """An agent for the wumpus world that does logical inference. [Figure 7.20]""" + + def __init__(self, dimentions): + self.dimrow = dimentions + self.kb = WumpusKB(self.dimrow) + self.t = 0 + self.plan = list() + self.current_position = WumpusPosition(1, 1, 'UP') + super().__init__(self.execute) + + def execute(self, percept): + self.kb.make_percept_sentence(percept, self.t) + self.kb.add_temporal_sentences(self.t) + + temp = list() + + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if self.kb.ask_if_true(location(i, j, self.t)): + temp.append(i) + temp.append(j) + + if self.kb.ask_if_true(facing_north(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'UP') + elif self.kb.ask_if_true(facing_south(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN') + elif self.kb.ask_if_true(facing_west(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT') + elif self.kb.ask_if_true(facing_east(self.t)): + self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT') + + safe_points = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if self.kb.ask_if_true(ok_to_move(i, j, self.t)): + safe_points.append([i, j]) + + if self.kb.ask_if_true(percept_glitter(self.t)): + goals = list() + goals.append([1, 1]) + self.plan.append('Grab') + actions = self.plan_route(self.current_position, goals, safe_points) + self.plan.extend(actions) + self.plan.append('Climb') + + if len(self.plan) == 0: + unvisited = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + for k in range(self.t): + if self.kb.ask_if_true(location(i, j, k)): + unvisited.append([i, j]) + unvisited_and_safe = list() + for u in unvisited: + for s in safe_points: + if u not in unvisited_and_safe and s == u: + unvisited_and_safe.append(u) + + temp = self.plan_route(self.current_position, unvisited_and_safe, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0 and self.kb.ask_if_true(have_arrow(self.t)): + possible_wumpus = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if not self.kb.ask_if_true(wumpus(i, j)): + possible_wumpus.append([i, j]) + + temp = self.plan_shot(self.current_position, possible_wumpus, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0: + not_unsafe = list() + for i in range(1, self.dimrow + 1): + for j in range(1, self.dimrow + 1): + if not self.kb.ask_if_true(ok_to_move(i, j, self.t)): + not_unsafe.append([i, j]) + temp = self.plan_route(self.current_position, not_unsafe, safe_points) + self.plan.extend(temp) + + if len(self.plan) == 0: + start = list() + start.append([1, 1]) + temp = self.plan_route(self.current_position, start, safe_points) + self.plan.extend(temp) + self.plan.append('Climb') + + action = self.plan[0] + self.plan = self.plan[1:] + self.kb.make_action_sentence(action, self.t) + self.t += 1 + + return action + + def plan_route(self, current, goals, allowed): + problem = PlanRoute(current, goals, allowed, self.dimrow) + return astar_search(problem).solution() + + def plan_shot(self, current, goals, allowed): + shooting_positions = set() + + for loc in goals: + x = loc[0] + y = loc[1] + for i in range(1, self.dimrow + 1): + if i < x: + shooting_positions.add(WumpusPosition(i, y, 'EAST')) + if i > x: + shooting_positions.add(WumpusPosition(i, y, 'WEST')) + if i < y: + shooting_positions.add(WumpusPosition(x, i, 'NORTH')) + if i > y: + shooting_positions.add(WumpusPosition(x, i, 'SOUTH')) + + # Can't have a shooting position from any of the rooms the Wumpus could reside + orientations = ['EAST', 'WEST', 'NORTH', 'SOUTH'] + for loc in goals: + for orientation in orientations: + shooting_positions.remove(WumpusPosition(loc[0], loc[1], orientation)) + + actions = list() + actions.extend(self.plan_route(current, shooting_positions, allowed)) + actions.append('Shoot') + return actions + + +# ______________________________________________________________________________ +# 7.7.4 Making plans by propositional inference + + +def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable): + """Converts a planning problem to Satisfaction problem by translating it to a cnf sentence. + [Figure 7.22] + >>> transition = {'A': {'Left': 'A', 'Right': 'B'}, 'B': {'Left': 'A', 'Right': 'C'}, 'C': {'Left': 'B', 'Right': 'C'}} + >>> SAT_plan('A', transition, 'C', 2) is None + True + """ + + # Functions used by SAT_plan + def translate_to_SAT(init, transition, goal, time): + clauses = [] + states = [state for state in transition] + + # Symbol claiming state s at time t + state_counter = itertools.count() + for s in states: + for t in range(time + 1): + state_sym[s, t] = Expr("State_{}".format(next(state_counter))) + + # Add initial state axiom + clauses.append(state_sym[init, 0]) + + # Add goal state axiom + clauses.append(state_sym[goal, time]) + + # All possible transitions + transition_counter = itertools.count() + for s in states: + for action in transition[s]: + s_ = transition[s][action] + for t in range(time): + # Action 'action' taken from state 's' at time 't' to reach 's_' + action_sym[s, action, t] = Expr( + "Transition_{}".format(next(transition_counter))) + + # Change the state from s to s_ + clauses.append(action_sym[s, action, t] | '==>' | state_sym[s, t]) + clauses.append(action_sym[s, action, t] | '==>' | state_sym[s_, t + 1]) + + # Allow only one state at any time + for t in range(time + 1): + # must be a state at any time + clauses.append(associate('|', [state_sym[s, t] for s in states])) + + for s in states: + for s_ in states[states.index(s) + 1:]: + # for each pair of states s, s_ only one is possible at time t + clauses.append((~state_sym[s, t]) | (~state_sym[s_, t])) + + # Restrict to one transition per timestep + for t in range(time): + # list of possible transitions at time t + transitions_t = [tr for tr in action_sym if tr[2] == t] + + # make sure at least one of the transitions happens + clauses.append(associate('|', [action_sym[tr] for tr in transitions_t])) + + for tr in transitions_t: + for tr_ in transitions_t[transitions_t.index(tr) + 1:]: + # there cannot be two transitions tr and tr_ at time t + clauses.append(~action_sym[tr] | ~action_sym[tr_]) + + # Combine the clauses to form the cnf + return associate('&', clauses) + + def extract_solution(model): + true_transitions = [t for t in action_sym if model[action_sym[t]]] + # Sort transitions based on time, which is the 3rd element of the tuple + true_transitions.sort(key=lambda x: x[2]) + return [action for s, action, time in true_transitions] + + # Body of SAT_plan algorithm + for t in range(t_max): + # dictionaries to help extract the solution from model + state_sym = {} + action_sym = {} + + cnf = translate_to_SAT(init, transition, goal, t) + model = SAT_solver(cnf) + if model is not False: + return extract_solution(model) + return None + + +# ______________________________________________________________________________ +# Chapter 9 Inference in First Order Logic +# 9.2 Unification and First Order Inference +# 9.2.1 Unification + + +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 + variables (e.g. Expr('x')), constants, lists, or Exprs. [Figure 9.1] + >>> unify(x, 3, {}) + {x: 3} + """ + if s is None: + return None + elif x == y: + return s + elif is_variable(x): + return unify_var(x, y, s) + elif is_variable(y): + return unify_var(y, x, s) + elif isinstance(x, Expr) and isinstance(y, Expr): + return unify(x.args, y.args, unify(x.op, y.op, 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 + 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 x.op[0].islower() + + +def unify_var(var, x, s): + if var in s: + return unify(s[var], x, s) + elif x in s: + return unify(var, s[x], s) + elif occur_check(var, x, s): + return None + 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).""" + if var == x: + return True + elif is_variable(x) and x in s: + return occur_check(var, s[x], s) + elif isinstance(x, Expr): + return (occur_check(var, x.op, s) or + occur_check(var, x.args, s)) + elif isinstance(x, (list, tuple)): + return first(e for e in x if occur_check(var, e, s)) + else: + return False + + +def extend(s, var, val): + """Copy the substitution s and extend it by setting var to val; return copy. + >>> extend({x: 1}, y, 2) == {x: 1, y: 2} + True + """ + s2 = s.copy() + s2[var] = val + return s2 + + +# 9.2.2 Storage and retrieval + + +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)')]) + >>> kb0.tell(expr('Rabbit(Flopsie)')) + >>> kb0.retract(expr('Rabbit(Pete)')) + >>> kb0.ask(expr('Hates(Mac, x)'))[x] + Flopsie + >>> kb0.ask(expr('Wife(Pete, x)')) + False + """ + + def __init__(self, initial_clauses=None): + self.clauses = [] # inefficient: no indexing + if initial_clauses: + for clause in initial_clauses: + self.tell(clause) + + def tell(self, sentence): + if is_definite_clause(sentence): + self.clauses.append(sentence) + else: + raise Exception("Not a definite clause: {}".format(sentence)) + + def ask_generator(self, query): + return fol_bc_ask(self, query) + + def retract(self, sentence): + self.clauses.remove(sentence) + + def fetch_rules_for_goal(self, goal): + return self.clauses + + +# ______________________________________________________________________________ +# 9.3 Forward Chaining +# 9.3.2 A simple forward-chaining algorithm + + +def fol_fc_ask(KB, alpha): + """A simple forward-chaining algorithm. [Figure 9.3]""" + kb_consts = list({c for clause in KB.clauses for c in constant_symbols(clause)}) + + def enum_subst(p): + query_vars = list({v for clause in p for v in variables(clause)}) + for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)): + theta = {x: y for x, y in zip(query_vars, assignment_list)} + yield theta + + # check if we can answer without new inferences + for q in KB.clauses: + phi = unify(q, alpha, {}) + if phi is not None: + yield phi + + while True: + new = [] + for rule in KB.clauses: + p, q = parse_definite_clause(rule) + for theta in enum_subst(p): + if set(subst(theta, p)).issubset(set(KB.clauses)): + q_ = subst(theta, q) + if all([unify(x, q_, {}) is None for x in KB.clauses + new]): + new.append(q_) + phi = unify(q_, alpha, {}) + if phi is not None: + yield phi + if not new: + break + for clause in new: + KB.tell(clause) + return None + + +def subst(s, x): + """Substitute the substitution s into the expression x. + >>> subst({x: 42, y:0}, F(x) + y) + (F(42) + 0) + """ + if isinstance(x, list): + return [subst(s, xi) for xi in x] + elif isinstance(x, tuple): + return tuple([subst(s, xi) for xi in x]) + elif not isinstance(x, Expr): + return x + elif is_var_symbol(x.op): + return s.get(x, x) + else: + return Expr(x.op, *[subst(s, arg) for arg in x.args]) + + +def standardize_variables(sentence, dic=None): + """Replace all the variables in sentence with new variables.""" + 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_{}'.format(next(standardize_variables.counter))) + dic[sentence] = v + return v + else: + return Expr(sentence.op, + *[standardize_variables(a, dic) for a in sentence.args]) + + +standardize_variables.counter = itertools.count() + + +# __________________________________________________________________ +# 9.4 Backward Chaining + + +def fol_bc_ask(KB, query): + """A simple backward-chaining algorithm for first-order logic. [Figure 9.6] + KB should be an instance of FolKB, and query an atomic sentence.""" + 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 + elif not goals: + yield theta + else: + first, rest = goals[0], goals[1:] + for theta1 in fol_bc_or(KB, subst(theta, first), theta): + for theta2 in fol_bc_and(KB, rest, theta1): + yield theta2 + + +# ______________________________________________________________________________ +# A simple KB that defines the relevant conditions of the Wumpus World as in Fig 7.4. +# See Sec. 7.4.3 +wumpus_kb = PropKB() + +P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21') +wumpus_kb.tell(~P11) +wumpus_kb.tell(B11 | '<=>' | (P12 | P21)) +wumpus_kb.tell(B21 | '<=>' | (P11 | P22 | P31)) +wumpus_kb.tell(~B11) +wumpus_kb.tell(B21) + +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)'])) + +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)'])) + + +# ______________________________________________________________________________ + +# Example application (not in the book). +# 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. + >>> diff(x * x, x) + ((x * 1) + (x * 1)) + """ + if y == x: + return 1 + elif not y.args: + return 0 + 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(y.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: {} in diff({}, {})".format(op, y, x)) + + +def simp(x): + """Simplify the expression x.""" + if isnumber(x) or not x.args: + return x + args = list(map(simp, x.args)) + u, op, v = args[0], x.op, args[-1] + if op == '+': + if v == 0: + return u + if u == 0: + return v + if u == v: + return 2 * u + if u == -v or v == -u: + return 0 + elif op == '-' and len(args) == 1: + if u.op == '-' and len(u.args) == 1: + return u.args[0] # --y ==> y + elif op == '-': + if v == 0: + return u + if u == 0: + return -v + if u == v: + return 0 + if u == -v or v == -u: + return 0 + elif op == '*': + if u == 0 or v == 0: + return 0 + if u == 1: + return v + if v == 1: + return u + if u == v: + return u ** 2 + elif op == '/': + if u == 0: + return 0 + if v == 0: + return Expr('Undefined') + if u == v: + return 1 + if u == -v or v == -u: + return 0 + elif op == '**': + if u == 0: + return 0 + if v == 0: + return 1 + if u == 1: + return 1 + if v == 1: + return u + elif op == 'log': + if u == 1: + return 0 + 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. + >>> d(x * x - x, x) + ((2 * x) - 1) + """ + return simp(diff(y, x)) diff --git a/making_simple_decision4e.py b/making_simple_decision4e.py new file mode 100644 index 000000000..4a35f94bd --- /dev/null +++ b/making_simple_decision4e.py @@ -0,0 +1,168 @@ +"""Making Simple Decisions (Chapter 15)""" + +import random + +from agents import Agent +from probability import BayesNet +from utils4e import vector_add, weighted_sample_with_replacement + + +class DecisionNetwork(BayesNet): + """An abstract class for a decision network as a wrapper for a BayesNet. + Represents an agent's current state, its possible actions, reachable states + and utilities of those states.""" + + def __init__(self, action, infer): + """action: a single action node + infer: the preferred method to carry out inference on the given BayesNet""" + super().__init__() + self.action = action + self.infer = infer + + def best_action(self): + """Return the best action in the network""" + return self.action + + def get_utility(self, action, state): + """Return the utility for a particular action and state in the network""" + raise NotImplementedError + + def get_expected_utility(self, action, evidence): + """Compute the expected utility given an action and evidence""" + u = 0.0 + prob_dist = self.infer(action, evidence, self).prob + for item, _ in prob_dist.items(): + u += prob_dist[item] * self.get_utility(action, item) + + return u + + +class InformationGatheringAgent(Agent): + """A simple information gathering agent. The agent works by repeatedly selecting + the observation with the highest information value, until the cost of the next + observation is greater than its expected benefit. [Figure 16.9]""" + + def __init__(self, decnet, infer, initial_evidence=None): + """decnet: a decision network + infer: the preferred method to carry out inference on the given decision network + initial_evidence: initial evidence""" + super().__init__() + self.decnet = decnet + self.infer = infer + self.observation = initial_evidence or [] + self.variables = self.decnet.nodes + + def integrate_percept(self, percept): + """Integrate the given percept into the decision network""" + raise NotImplementedError + + def execute(self, percept): + """Execute the information gathering algorithm""" + self.observation = self.integrate_percept(percept) + vpis = self.vpi_cost_ratio(self.variables) + j = max(vpis) + variable = self.variables[j] + + if self.vpi(variable) > self.cost(variable): + return self.request(variable) + + return self.decnet.best_action() + + def request(self, variable): + """Return the value of the given random variable as the next percept""" + raise NotImplementedError + + def cost(self, var): + """Return the cost of obtaining evidence through tests, consultants or questions""" + raise NotImplementedError + + def vpi_cost_ratio(self, variables): + """Return the VPI to cost ratio for the given variables""" + v_by_c = [] + for var in variables: + v_by_c.append(self.vpi(var) / self.cost(var)) + return v_by_c + + def vpi(self, variable): + """Return VPI for a given variable""" + vpi = 0.0 + prob_dist = self.infer(variable, self.observation, self.decnet).prob + for item, _ in prob_dist.items(): + post_prob = prob_dist[item] + new_observation = list(self.observation) + new_observation.append(item) + expected_utility = self.decnet.get_expected_utility(variable, new_observation) + vpi += post_prob * expected_utility + + vpi -= self.decnet.get_expected_utility(variable, self.observation) + return vpi + + +# _________________________________________________________________________ +# chapter 25 Robotics +# TODO: Implement continuous map for MonteCarlo similar to Fig25.10 from the book + + +class MCLmap: + """Map which provides probability distributions and sensor readings. + Consists of discrete cells which are either an obstacle or empty""" + + def __init__(self, m): + self.m = m + self.nrows = len(m) + self.ncols = len(m[0]) + # list of empty spaces in the map + self.empty = [(i, j) for i in range(self.nrows) for j in range(self.ncols) if not m[i][j]] + + def sample(self): + """Returns a random kinematic state possible in the map""" + pos = random.choice(self.empty) + # 0N 1E 2S 3W + orient = random.choice(range(4)) + kin_state = pos + (orient,) + return kin_state + + def ray_cast(self, sensor_num, kin_state): + """Returns distace to nearest obstacle or map boundary in the direction of sensor""" + pos = kin_state[:2] + orient = kin_state[2] + # sensor layout when orientation is 0 (towards North) + # 0 + # 3R1 + # 2 + delta = ((sensor_num % 2 == 0) * (sensor_num - 1), (sensor_num % 2 == 1) * (2 - sensor_num)) + # sensor direction changes based on orientation + for _ in range(orient): + delta = (delta[1], -delta[0]) + range_count = 0 + while (0 <= pos[0] < self.nrows) and (0 <= pos[1] < self.nrows) and (not self.m[pos[0]][pos[1]]): + pos = vector_add(pos, delta) + range_count += 1 + return range_count + + +def monte_carlo_localization(a, z, N, P_motion_sample, P_sensor, m, S=None): + """Monte Carlo localization algorithm from Fig 25.9""" + + def ray_cast(sensor_num, kin_state, m): + return m.ray_cast(sensor_num, kin_state) + + M = len(z) + W = [0] * N + S_ = [0] * N + W_ = [0] * N + v = a['v'] + w = a['w'] + + if S is None: + S = [m.sample() for _ in range(N)] + + for i in range(N): + S_[i] = P_motion_sample(S[i], v, w) + W_[i] = 1 + for j in range(M): + z_ = ray_cast(j, S_[i], m) + W_[i] = W_[i] * P_sensor(z[j], z_) + + S = weighted_sample_with_replacement(N, S_, W_) + return S diff --git a/mdp.ipynb b/mdp.ipynb index 909b874ca..b9952f528 100644 --- a/mdp.ipynb +++ b/mdp.ipynb @@ -4,27 +4,44 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Markov decision processes (MDPs)\n", + "# Making Complex Decisions\n", + "---\n", "\n", - "This IPy notebook acts as supporting material for topics covered in **Chapter 17 Making Complex Decisions** of the book* Artificial Intelligence: A Modern Approach*. We makes use of the implementations in mdp.py module. This notebook also includes a brief summary of the main topics as a review. Let us import everything from the mdp module to get started." + "This Jupyter notebook acts as supporting material for topics covered in **Chapter 17 Making Complex Decisions** of the book* Artificial Intelligence: A Modern Approach*. We make use of the implementations in mdp.py module. This notebook also includes a brief summary of the main topics as a review. Let us import everything from the mdp module to get started." ] }, { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "from mdp import MDP, GridMDP, sequential_decision_environment, value_iteration" + "from mdp import *\n", + "from notebook import psource, pseudocode, plot_pomdp_utility" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* MDP\n", + "* Grid MDP\n", + "* Value Iteration\n", + " * Value Iteration Visualization\n", + "* Policy Iteration\n", + "* POMDPs\n", + "* POMDP Value Iteration\n", + " - Value Iteration Visualization" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Review\n", + "## OVERVIEW\n", "\n", "Before we start playing with the actual implementations let us review a couple of things about MDPs.\n", "\n", @@ -46,18 +63,206 @@ "source": [ "## MDP\n", "\n", - "To begin with let us look at the implementation of MDP class defined in mdp.py The docstring tells us what all is required to define a MDP namely - set of states,actions, initial state, transition model, and a reward function. Each of these are implemented as methods. Do not close the popup so that you can follow along the description of code below." + "To begin with let us look at the implementation of MDP class defined in mdp.py The docstring tells us what all is required to define a MDP namely - set of states, actions, initial state, transition model, and a reward function. Each of these are implemented as methods. Do not close the popup so that you can follow along the description of code below." ] }, { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    class MDP:\n",
    +       "\n",
    +       "    """A Markov Decision Process, defined by an initial state, transition model,\n",
    +       "    and reward function. We also keep track of a gamma value, for use by\n",
    +       "    algorithms. The transition model is represented somewhat differently from\n",
    +       "    the text. Instead of P(s' | s, a) being a probability number for each\n",
    +       "    state/state/action triplet, we instead have T(s, a) return a\n",
    +       "    list of (p, s') pairs. We also keep track of the possible states,\n",
    +       "    terminal states, and actions for each state. [page 646]"""\n",
    +       "\n",
    +       "    def __init__(self, init, actlist, terminals, transitions = {}, reward = None, states=None, gamma=.9):\n",
    +       "        if not (0 < gamma <= 1):\n",
    +       "            raise ValueError("An MDP must have 0 < gamma <= 1")\n",
    +       "\n",
    +       "        if states:\n",
    +       "            self.states = states\n",
    +       "        else:\n",
    +       "            ## collect states from transitions table\n",
    +       "            self.states = self.get_states_from_transitions(transitions)\n",
    +       "            \n",
    +       "        \n",
    +       "        self.init = init\n",
    +       "        \n",
    +       "        if isinstance(actlist, list):\n",
    +       "            ## if actlist is a list, all states have the same actions\n",
    +       "            self.actlist = actlist\n",
    +       "        elif isinstance(actlist, dict):\n",
    +       "            ## if actlist is a dict, different actions for each state\n",
    +       "            self.actlist = actlist\n",
    +       "        \n",
    +       "        self.terminals = terminals\n",
    +       "        self.transitions = transitions\n",
    +       "        if self.transitions == {}:\n",
    +       "            print("Warning: Transition table is empty.")\n",
    +       "        self.gamma = gamma\n",
    +       "        if reward:\n",
    +       "            self.reward = reward\n",
    +       "        else:\n",
    +       "            self.reward = {s : 0 for s in self.states}\n",
    +       "        #self.check_consistency()\n",
    +       "\n",
    +       "    def R(self, state):\n",
    +       "        """Return a numeric reward for this state."""\n",
    +       "        return self.reward[state]\n",
    +       "\n",
    +       "    def T(self, state, action):\n",
    +       "        """Transition model. From a state and an action, return a list\n",
    +       "        of (probability, result-state) pairs."""\n",
    +       "        if(self.transitions == {}):\n",
    +       "            raise ValueError("Transition model is missing")\n",
    +       "        else:\n",
    +       "            return self.transitions[state][action]\n",
    +       "\n",
    +       "    def actions(self, state):\n",
    +       "        """Set of actions that can be performed in this state. By default, a\n",
    +       "        fixed list of actions, except for terminal states. Override this\n",
    +       "        method if you need to specialize by state."""\n",
    +       "        if state in self.terminals:\n",
    +       "            return [None]\n",
    +       "        else:\n",
    +       "            return self.actlist\n",
    +       "\n",
    +       "    def get_states_from_transitions(self, transitions):\n",
    +       "        if isinstance(transitions, dict):\n",
    +       "            s1 = set(transitions.keys())\n",
    +       "            s2 = set([tr[1] for actions in transitions.values() \n",
    +       "                              for effects in actions.values() for tr in effects])\n",
    +       "            return s1.union(s2)\n",
    +       "        else:\n",
    +       "            print('Could not retrieve states from transitions')\n",
    +       "            return None\n",
    +       "\n",
    +       "    def check_consistency(self):\n",
    +       "        # check that all states in transitions are valid\n",
    +       "        assert set(self.states) == self.get_states_from_transitions(self.transitions)\n",
    +       "        # check that init is a valid state\n",
    +       "        assert self.init in self.states\n",
    +       "        # check reward for each state\n",
    +       "        #assert set(self.reward.keys()) == set(self.states)\n",
    +       "        assert set(self.reward.keys()) == set(self.states)\n",
    +       "        # check that all terminals are valid states\n",
    +       "        assert all([t in self.states for t in self.terminals])\n",
    +       "        # check that probability distributions for all actions sum to 1\n",
    +       "        for s1, actions in self.transitions.items():\n",
    +       "            for a in actions.keys():\n",
    +       "                s = 0\n",
    +       "                for o in actions[a]:\n",
    +       "                    s += o[0]\n",
    +       "                assert abs(s - 1) < 0.001\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "%psource MDP" + "psource(MDP)" ] }, { @@ -94,15 +299,15 @@ }, "outputs": [], "source": [ - "# Transition Matrix as nested dict. State -> Actions in state -> States by each action -> Probabilty\n", + "# Transition Matrix as nested dict. State -> Actions in state -> List of (Probability, State) tuples\n", "t = {\n", " \"A\": {\n", - " \"X\": {\"A\":0.3, \"B\":0.7},\n", - " \"Y\": {\"A\":1.0}\n", + " \"X\": [(0.3, \"A\"), (0.7, \"B\")],\n", + " \"Y\": [(1.0, \"A\")]\n", " },\n", " \"B\": {\n", - " \"X\": {\"End\":0.8, \"B\":0.2},\n", - " \"Y\": {\"A\":1.0}\n", + " \"X\": {(0.8, \"End\"), (0.2, \"B\")},\n", + " \"Y\": {(1.0, \"A\")}\n", " },\n", " \"End\": {}\n", "}\n", @@ -122,27 +327,24 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ "class CustomMDP(MDP):\n", - "\n", - " def __init__(self, transition_matrix, rewards, terminals, init, gamma=.9):\n", + " def __init__(self, init, terminals, transition_matrix, reward = None, gamma=.9):\n", " # All possible actions.\n", " actlist = []\n", " for state in transition_matrix.keys():\n", - " actlist.extend(transition_matrix.keys())\n", + " actlist.extend(transition_matrix[state])\n", " actlist = list(set(actlist))\n", - "\n", - " MDP.__init__(self, init, actlist, terminals=terminals, gamma=gamma)\n", - " self.t = transition_matrix\n", - " self.reward = rewards\n", - " for state in self.t:\n", - " self.states.add(state)\n", + " MDP.__init__(self, init, actlist, terminals, transition_matrix, reward, gamma=gamma)\n", "\n", " def T(self, state, action):\n", - " return [(new_state, prob) for new_state, prob in self.t[state][action].items()]" + " if action is None:\n", + " return [(0.0, state)]\n", + " else: \n", + " return self.t[state][action]" ] }, { @@ -156,39 +358,196 @@ "cell_type": "code", "execution_count": 5, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "our_mdp = CustomMDP(t, rewards, terminals, init, gamma=.9)" + "our_mdp = CustomMDP(init, terminals, t, rewards, gamma=.9)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "With this we have sucessfully represented our MDP. Later we will look at ways to solve this MDP." + "With this we have successfully represented our MDP. Later we will look at ways to solve this MDP." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Grid MDP\n", + "## GRID MDP\n", "\n", - "Now we look at a concrete implementation that makes use of the MDP as base class. The GridMDP class in the mdp module is used to represent a grid world MDP like the one shown in in **Fig 17.1** of the AIMA Book. The code should be easy to understand if you have gone through the CustomMDP example.\n", - "\n" + "Now we look at a concrete implementation that makes use of the MDP as base class. The GridMDP class in the mdp module is used to represent a grid world MDP like the one shown in in **Fig 17.1** of the AIMA Book. We assume for now that the environment is _fully observable_, so that the agent always knows where it is. The code should be easy to understand if you have gone through the CustomMDP example." ] }, { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": true - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    class GridMDP(MDP):\n",
    +       "\n",
    +       "    """A two-dimensional grid MDP, as in [Figure 17.1]. All you have to do is\n",
    +       "    specify the grid as a list of lists of rewards; use None for an obstacle\n",
    +       "    (unreachable state). Also, you should specify the terminal states.\n",
    +       "    An action is an (x, y) unit vector; e.g. (1, 0) means move east."""\n",
    +       "\n",
    +       "    def __init__(self, grid, terminals, init=(0, 0), gamma=.9):\n",
    +       "        grid.reverse()  # because we want row 0 on bottom, not on top\n",
    +       "        reward = {}\n",
    +       "        states = set()\n",
    +       "        self.rows = len(grid)\n",
    +       "        self.cols = len(grid[0])\n",
    +       "        self.grid = grid\n",
    +       "        for x in range(self.cols):\n",
    +       "            for y in range(self.rows):\n",
    +       "                if grid[y][x] is not None:\n",
    +       "                    states.add((x, y))\n",
    +       "                    reward[(x, y)] = grid[y][x]\n",
    +       "        self.states = states\n",
    +       "        actlist = orientations\n",
    +       "        transitions = {}\n",
    +       "        for s in states:\n",
    +       "            transitions[s] = {}\n",
    +       "            for a in actlist:\n",
    +       "                transitions[s][a] = self.calculate_T(s, a)\n",
    +       "        MDP.__init__(self, init, actlist=actlist,\n",
    +       "                     terminals=terminals, transitions = transitions, \n",
    +       "                     reward = reward, states = states, gamma=gamma)\n",
    +       "\n",
    +       "    def calculate_T(self, state, action):\n",
    +       "        if action is None:\n",
    +       "            return [(0.0, state)]\n",
    +       "        else:\n",
    +       "            return [(0.8, self.go(state, action)),\n",
    +       "                    (0.1, self.go(state, turn_right(action))),\n",
    +       "                    (0.1, self.go(state, turn_left(action)))]\n",
    +       "    \n",
    +       "    def T(self, state, action):\n",
    +       "        if action is None:\n",
    +       "            return [(0.0, state)]\n",
    +       "        else:\n",
    +       "            return self.transitions[state][action]\n",
    +       " \n",
    +       "    def go(self, state, direction):\n",
    +       "        """Return the state that results from going in this direction."""\n",
    +       "        state1 = vector_add(state, direction)\n",
    +       "        return state1 if state1 in self.states else state\n",
    +       "\n",
    +       "    def to_grid(self, mapping):\n",
    +       "        """Convert a mapping from (x, y) to v into a [[..., v, ...]] grid."""\n",
    +       "        return list(reversed([[mapping.get((x, y), None)\n",
    +       "                               for x in range(self.cols)]\n",
    +       "                              for y in range(self.rows)]))\n",
    +       "\n",
    +       "    def to_arrows(self, policy):\n",
    +       "        chars = {\n",
    +       "            (1, 0): '>', (0, 1): '^', (-1, 0): '<', (0, -1): 'v', None: '.'}\n",
    +       "        return self.to_grid({s: chars[a] for (s, a) in policy.items()})\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "%psource GridMDP" + "psource(GridMDP)" ] }, { @@ -223,14 +582,12 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 7, @@ -248,37 +605,174 @@ "collapsed": true }, "source": [ - "# Value Iteration\n", + "# VALUE ITERATION\n", "\n", "Now that we have looked how to represent MDPs. Let's aim at solving them. Our ultimate goal is to obtain an optimal policy. We start with looking at Value Iteration and a visualisation that should help us understanding it better.\n", "\n", - "We start by calculating Value/Utility for each of the states. The Value of each state is the expected sum of discounted future rewards given we start in that state and follow a particular policy pi.The algorithm Value Iteration (**Fig. 17.4** in the book) relies on finding solutions of the Bellman's Equation. The intuition Value Iteration works is because values propagate. This point will we more clear after we encounter the visualisation. For more information you can refer to **Section 17.2** of the book. \n" + "We start by calculating Value/Utility for each of the states. The Value of each state is the expected sum of discounted future rewards given we start in that state and follow a particular policy $\\pi$. The value or the utility of a state is given by\n", + "\n", + "$$U(s)=R(s)+\\gamma\\max_{a\\epsilon A(s)}\\sum_{s'} P(s'\\ |\\ s,a)U(s')$$\n", + "\n", + "This is called the Bellman equation. The algorithm Value Iteration (**Fig. 17.4** in the book) relies on finding solutions of this Equation. The intuition Value Iteration works is because values propagate through the state space by means of local updates. This point will we more clear after we encounter the visualisation. For more information you can refer to **Section 17.2** of the book. \n" ] }, { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def value_iteration(mdp, epsilon=0.001):\n",
    +       "    """Solving an MDP by value iteration. [Figure 17.4]"""\n",
    +       "    U1 = {s: 0 for s in mdp.states}\n",
    +       "    R, T, gamma = mdp.R, mdp.T, mdp.gamma\n",
    +       "    while True:\n",
    +       "        U = U1.copy()\n",
    +       "        delta = 0\n",
    +       "        for s in mdp.states:\n",
    +       "            U1[s] = R(s) + gamma * max([sum([p * U[s1] for (p, s1) in T(s, a)])\n",
    +       "                                        for a in mdp.actions(s)])\n",
    +       "            delta = max(delta, abs(U1[s] - U[s]))\n",
    +       "        if delta < epsilon * (1 - gamma) / gamma:\n",
    +       "            return U\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "%psource value_iteration" + "psource(value_iteration)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It takes as inputs two parameters an MDP to solve and epsilon the maximum error allowed in the utility of any state. It returns a dictionary containing utilities where the keys are the states and values represent utilities. Let us solve the **sequencial_decision_enviornment** GridMDP.\n" + "It takes as inputs two parameters, an MDP to solve and epsilon, the maximum error allowed in the utility of any state. It returns a dictionary containing utilities where the keys are the states and values represent utilities.
    Value Iteration starts with arbitrary initial values for the utilities, calculates the right side of the Bellman equation and plugs it into the left hand side, thereby updating the utility of each state from the utilities of its neighbors. \n", + "This is repeated until equilibrium is reached. \n", + "It works on the principle of _Dynamic Programming_ - using precomputed information to simplify the subsequent computation. \n", + "If $U_i(s)$ is the utility value for state $s$ at the $i$ th iteration, the iteration step, called Bellman update, looks like this:\n", + "\n", + "$$ U_{i+1}(s) \\leftarrow R(s) + \\gamma \\max_{a \\epsilon A(s)} \\sum_{s'} P(s'\\ |\\ s,a)U_{i}(s') $$\n", + "\n", + "As you might have noticed, `value_iteration` has an infinite loop. How do we decide when to stop iterating? \n", + "The concept of _contraction_ successfully explains the convergence of value iteration. \n", + "Refer to **Section 17.2.3** of the book for a detailed explanation. \n", + "In the algorithm, we calculate a value $delta$ that measures the difference in the utilities of the current time step and the previous time step. \n", + "\n", + "$$\\delta = \\max{(\\delta, \\begin{vmatrix}U_{i + 1}(s) - U_i(s)\\end{vmatrix})}$$\n", + "\n", + "This value of delta decreases as the values of $U_i$ converge.\n", + "We terminate the algorithm if the $\\delta$ value is less than a threshold value determined by the hyperparameter _epsilon_.\n", + "\n", + "$$\\delta \\lt \\epsilon \\frac{(1 - \\gamma)}{\\gamma}$$\n", + "\n", + "To summarize, the Bellman update is a _contraction_ by a factor of $gamma$ on the space of utility vectors. \n", + "Hence, from the properties of contractions in general, it follows that `value_iteration` always converges to a unique solution of the Bellman equations whenever $gamma$ is less than 1.\n", + "We then terminate the algorithm when a reasonable approximation is achieved.\n", + "In practice, it often occurs that the policy $pi$ becomes optimal long before the utility function converges. For the given 4 x 3 environment with $gamma = 0.9$, the policy $pi$ is optimal when $i = 4$ (at the 4th iteration), even though the maximum error in the utility function is stil 0.46. This can be clarified from **figure 17.6** in the book. Hence, to increase computational efficiency, we often use another method to solve MDPs called Policy Iteration which we will see in the later part of this notebook. \n", + "
    For now, let us solve the **sequential_decision_environment** GridMDP using `value_iteration`." ] }, { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -309,14 +803,85 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Visualization for Value Iteration\n", + "The pseudocode for the algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ VALUE-ITERATION(_mdp_, _ε_) __returns__ a utility function \n", + " __inputs__: _mdp_, an MDP with states _S_, actions _A_(_s_), transition model _P_(_s′_ | _s_, _a_), \n", + "      rewards _R_(_s_), discount _γ_ \n", + "   _ε_, the maximum error allowed in the utility of any state \n", + " __local variables__: _U_, _U′_, vectors of utilities for states in _S_, initially zero \n", + "        _δ_, the maximum change in the utility of any state in an iteration \n", + "\n", + " __repeat__ \n", + "   _U_ ← _U′_; _δ_ ← 0 \n", + "   __for each__ state _s_ in _S_ __do__ \n", + "     _U′_\\[_s_\\] ← _R_(_s_) + _γ_ max_a_ ∈ _A_(_s_) Σ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] \n", + "     __if__ | _U′_\\[_s_\\] − _U_\\[_s_\\] | > _δ_ __then__ _δ_ ← | _U′_\\[_s_\\] − _U_\\[_s_\\] | \n", + " __until__ _δ_ < _ε_(1 − _γ_)/_γ_ \n", + " __return__ _U_ \n", + "\n", + "---\n", + "__Figure ??__ The value iteration algorithm for calculating utilities of states. The termination condition is from Equation (__??__)." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode(\"Value-Iteration\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### AIMA3e\n", + "__function__ VALUE-ITERATION(_mdp_, _ε_) __returns__ a utility function \n", + " __inputs__: _mdp_, an MDP with states _S_, actions _A_(_s_), transition model _P_(_s′_ | _s_, _a_), \n", + "      rewards _R_(_s_), discount _γ_ \n", + "   _ε_, the maximum error allowed in the utility of any state \n", + " __local variables__: _U_, _U′_, vectors of utilities for states in _S_, initially zero \n", + "        _δ_, the maximum change in the utility of any state in an iteration \n", + "\n", + " __repeat__ \n", + "   _U_ ← _U′_; _δ_ ← 0 \n", + "   __for each__ state _s_ in _S_ __do__ \n", + "     _U′_\\[_s_\\] ← _R_(_s_) + _γ_ max_a_ ∈ _A_(_s_) Σ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] \n", + "     __if__ | _U′_\\[_s_\\] − _U_\\[_s_\\] | > _δ_ __then__ _δ_ ← | _U′_\\[_s_\\] − _U_\\[_s_\\] | \n", + " __until__ _δ_ < _ε_(1 − _γ_)/_γ_ \n", + " __return__ _U_ \n", + "\n", + "---\n", + "__Figure ??__ The value iteration algorithm for calculating utilities of states. The termination condition is from Equation (__??__)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## VALUE ITERATION VISUALIZATION\n", "\n", "To illustrate that values propagate out of states let us create a simple visualisation. We will be using a modified version of the value_iteration function which will store U over time. We will also remove the parameter epsilon and instead add the number of iterations we want." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": { "collapsed": true }, @@ -342,87 +907,30 @@ "Next, we define a function to create the visualisation from the utilities returned by **value_iteration_instru**. The reader need not concern himself with the code that immediately follows as it is the usage of Matplotib with IPython Widgets. If you are interested in reading more about these visit [ipywidgets.readthedocs.io](http://ipywidgets.readthedocs.io)" ] }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", - "from collections import defaultdict\n", - "import time\n", - "\n", - "def make_plot_grid_step_function(columns, row, U_over_time):\n", - " '''ipywidgets interactive function supports\n", - " single parameter as input. This function\n", - " creates and return such a function by taking\n", - " in input other parameters\n", - " '''\n", - " def plot_grid_step(iteration):\n", - " data = U_over_time[iteration]\n", - " data = defaultdict(lambda: 0, data)\n", - " grid = []\n", - " for row in range(rows):\n", - " current_row = []\n", - " for column in range(columns):\n", - " current_row.append(data[(column, row)])\n", - " grid.append(current_row)\n", - " grid.reverse() # output like book\n", - " fig = plt.imshow(grid, cmap=plt.cm.bwr, interpolation='nearest')\n", - "\n", - " plt.axis('off')\n", - " fig.axes.get_xaxis().set_visible(False)\n", - " fig.axes.get_yaxis().set_visible(False)\n", - "\n", - " for col in range(len(grid)):\n", - " for row in range(len(grid[0])):\n", - " magic = grid[col][row]\n", - " fig.axes.text(row, col, \"{0:.2f}\".format(magic), va='center', ha='center')\n", - "\n", - " plt.show()\n", - " \n", - " return plot_grid_step\n", - "\n", - "def make_visualize(slider):\n", - " ''' Takes an input a slider and returns \n", - " callback function for timer and animation\n", - " '''\n", - " \n", - " def visualize_callback(Visualize, time_step):\n", - " if Visualize is True:\n", - " for i in range(slider.min, slider.max + 1):\n", - " slider.value = i\n", - " time.sleep(float(time_step))\n", - " \n", - " return visualize_callback\n", - " " - ] - }, { "cell_type": "code", "execution_count": 12, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ "columns = 4\n", "rows = 3\n", - "U_over_time = value_iteration_instru(sequential_decision_environment)\n", - " " + "U_over_time = value_iteration_instru(sequential_decision_environment)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ + "%matplotlib inline\n", + "from notebook import make_plot_grid_step_function\n", + "\n", "plot_grid_step = make_plot_grid_step_function(columns, rows, U_over_time)" ] }, @@ -430,24 +938,40 @@ "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": false, "scrolled": true }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAATgAAADtCAYAAAAr+2lCAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAADM5JREFUeJzt2lFolGe+gPFn0ggH1pKEHPWQ0a2Cya7scpz1ECxyEETY\ngANGUKgNbEqoopbdhFKkXikKB9obRXSDVsqxWch2KdQG9cRVKAgKktYajAtdrWldndhIUxs3vRGZ\nOReJaULSONvqzPjv87txJu/7hTd/Ph8+JyZyuRySFFFZsQ8gSU+KgZMUloGTFJaBkxSWgZMUloGT\nFFb5TIsjI/h/SKQimf1sothHeHrkctMOyyc4SWEZOElhGThJYRk4SWEZOElhGThJYRk4SWEZOElh\nGThJYRk4SWEZOElhGThJYRk4SWEZOElhGThJYRk4SWEZOElhGThJYRk4SWEZOElhGThJYRk4SWEZ\nOElhGThJYRk4SWEZOElhGThJYRk4SWEZOElhGThJYRk4SWGVXOC2b28llaplxYoUly/3Trvnxo0v\nWLXqeVKpOlpaXuTBgweT1i9e/Iiqqll0db1fiCMXhXPKn7N6tJeBecB/zrCnFagFUsDEKZ4CfgnU\nAW8+qQP+QCUVuNOnu+nvv05v7zX27z9MW9vWafft3Pk6ra2v0dt7lYqKSjo63h5fy2az7Nq1g9Wr\nGwp17IJzTvlzVvlpAf46w3o3cB24BhwGHk4xC/x+7Nq/AX8GPn1yx/yXlVTgTp7soqmpGYD6+uXc\nuzfMnTuDU/adPfshjY3rAWhqeonjx4+Nrx06dIB16zYwZ87cwhy6CJxT/pxVfv4bqJphvQtoHnu9\nHBgGBoEeRp/qngNmARvH9paKkgrcwECGZHLB+PuamiQDA5lJe4aGhqisrKKsbPToyeR8bt8eGL/+\nxIkP2LRpG7lcrnAHLzDnlD9n9XhkgAUT3s8f+9r3fb1UlFTgfqwdO15lz56JnwL8dG/ImTin/Dmr\n6T0tUygv9gGOHGnn6NEjJBIJli2rJ5O5Ob6WydyipiY5aX91dTXDw9+QzWYpKyubtOfSpY9padlI\nLpdjaOgrzpzpprx8Fun02oL+TE+Cc8qfs3r8ksDNCe9vjX3tPvCPab5eKor+BLd58yucP3+Jc+c+\nIZ1upLOzA4CengtUVFQyd+68KdesXLmKY8feA6Cz8x3S6UYA+vr66evr58qVz2ls3MDeve1hbkTn\nlD9n9cPk+P4ns7VAx9jrC0Alo791rQc+A24wGrt3x/aWiqIHbqKGhjUsXLiIpUsX09a2hX372sfX\n1q9PMzj4JQC7d7/BwYN7SaXquHv3a5qbX57yvRKJRMHOXWjOKX/OKj9NwArgKvBz4H8Z/W3pW2Pr\na4BFwGJgC/Bwis8AB4HfAr9i9JcMSwp26kdLzPTB6cjIU/NPbSmc2c/GDepjl8tNO6ySeoKTpMfJ\nwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJCsvA\nSQrLwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJCsvASQrLwEkKy8BJ\nCsvASQrLwEkKy8BJCqu82AeIYvbPcsU+wlNh5NtEsY/w1EjgPZWv75uUT3CSwjJwksIycJLCMnCS\nwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLC\nMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIq\nucBt395KKlXLihUpLl/unXbPjRtfsGrV86RSdbS0vMiDBw8mrV+8+BFVVbPo6nq/EEcuuFOnTvHL\nJUuo+8UvePPNN6fd09raSm1dHanf/Ibe3t5/6dpovKfy8XdgBfBvwN4Z9n0BPA/UAS8CE+fUCtQC\nKWD6ORdaSQXu9Olu+vuv09t7jf37D9PWtnXafTt3vk5r62v09l6loqKSjo63x9ey2Sy7du1g9eqG\nQh27oLLZLL//wx/466lT/O3KFf787rt8+umnk/Z0d3dzvb+fa1evcvjQIbZu25b3tdF4T+WrGjgA\nbH/EvteB14CrQCXwcE7dwHXgGnAYmH7OhVZSgTt5soumpmYA6uuXc+/eMHfuDE7Zd/bshzQ2rgeg\nqekljh8/Nr526NAB1q3bwJw5cwtz6ALr6emhtraW5557jlmzZrHxhRfo6uqatKerq4vm3/0OgOXL\nlzM8PMzg4GBe10bjPZWvfwf+Cyh/xL4PgfVjr18CPhh73QU0j71eDgwDU+dcaCUVuIGBDMnkgvH3\nNTVJBgYyk/YMDQ1RWVlFWdno0ZPJ+dy+PTB+/YkTH7Bp0zZyuVzhDl5AmUyGBfPnj7+fP38+mczk\nGWUGBliwYMGUPflcG4331OM0BFTxXTbmAw9nmQEWTNibnLBWPCUVuB9rx45X2bNn4udKP/UbcpR/\nMX8476mn26OeR5+4I0faOXr0CIlEgmXL6slkbo6vZTK3qKlJTtpfXV3N8PA3ZLNZysrKJu25dOlj\nWlo2ksvlGBr6ijNnuikvn0U6vbagP9OTlEwm+cfN72Z069YtksnJM0rW1HBzmj33799/5LUReE/l\nqx04AiSA/wP+4xH7q4FvgCyjz0a3GH1SY+zPmxP2TlwrnqI/wW3e/Arnz1/i3LlPSKcb6ezsAKCn\n5wIVFZXMnTtvyjUrV67i2LH3AOjsfId0uhGAvr5++vr6uXLlcxobN7B3b3uQG/E79fX1fPbZZ9y4\ncYP79+/z7l/+wtq1k3/GtWvX0vGnPwFw4cIFKisrmTdvXl7XRuA9la9XgEvAJ0yO20xPqauA98Ze\nvwM0jr1eC3SMvb7A6C8gps650IoeuIkaGtawcOEili5dTFvbFvbtax9fW78+zeDglwDs3v0GBw/u\nJZWq4+7dr2lufnnK90okEgU7dyE988wzHDxwgN82NPCrX/+ajS+8wJIlSzh8+DBvvfUWAGvWrGHR\nwoUsrq1ly9attP/xjzNeG5n3VL4GGf0MbR/wP8DPgZGxtTTw5djrNxj9byR1wNfAwzmtARYBi4Et\njD4dFl9ips9nRkb8wCFfs3/mqPIx8m3kSDxezz5b7BM8PXI5pr2xSuoJTpIeJwMnKSwDJyksAycp\nLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKSwDJyks\nAycpLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKSwDJyksAycpLAMnKazy\nYh8gipFvE8U+goL55z+LfYKnn09wksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJw\nksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCMnCS\nwjJwksIycJLCMnCSwjJwksIycJLCMnCSwjJwksIycJLCKrnAbd/eSipVy4oVKS5f7p12z40bX7Bq\n1fOkUnW0tLzIgwcPJq1fvPgRVVWz6Op6vxBHLgrnlD9nlZ+IcyqpwJ0+3U1//3V6e6+xf/9h2tq2\nTrtv587XaW19jd7eq1RUVNLR8fb4WjabZdeuHaxe3VCoYxecc8qfs8pP1DmVVOBOnuyiqakZgPr6\n5dy7N8ydO4NT9p09+yGNjesBaGp6iePHj42vHTp0gHXrNjBnztzCHLoInFP+nFV+os6ppAI3MJAh\nmVww/r6mJsnAQGbSnqGhISorqygrGz16Mjmf27cHxq8/ceIDNm3aRi6XK9zBC8w55c9Z5SfqnEoq\ncD/Wjh2vsmfPmxO+UjqDLiXOKX/OKj+lOqfyYh/gyJF2jh49QiKRYNmyejKZm+NrmcwtamqSk/ZX\nV1czPPwN2WyWsrKySXsuXfqYlpaN5HI5hoa+4syZbsrLZ5FOry3oz/QkOKf8Oav8/BTmVPQnuM2b\nX+H8+UucO/cJ6XQjnZ0dAPT0XKCiopK5c+dNuWblylUcO/YeAJ2d75BONwLQ19dPX18/V658TmPj\nBvbubS/6gB8X55Q/Z5Wfn8Kcih64iRoa1rBw4SKWLl1MW9sW9u1rH19bvz7N4OCXAOze/QYHD+4l\nlarj7t2vaW5+ecr3SiQSBTt3oTmn/Dmr/ESdU2KmDwRHRkrkH9KSNIPZs5m2qiX1BCdJj5OBkxSW\ngZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGTFJaB\nkxSWgZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGTFJaBkxSWgZMUloGT\nFJaBkxSWgZMUViKXyxX7DJL0RPgEJyksAycpLAMnKSwDJyksAycpLAMnKaz/B9v3wubCyTXSAAAA\nAElFTkSuQmCC\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAATcAAADuCAYAAABcZEBhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAADYxJREFUeJzt211oW2eex/Hf2Xpb0onWrVkm1otL\nW2SmrNaVtzS2K8jCFhJPXsbtRWcTX4zbmUBINkMYw5jmYrYwhNJuMWTjaTCYDSW5cQK9iEOcpDad\nLAREVtBEF+OwoDEyWEdxirvjelw36cScubCi1PWLvK0lnfnP9wMGHz2P4dEf8fWRnDie5wkArPmb\nah8AAMqBuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMKnm/7N5bk78dwagjDYHnGofwf88\nb11D4s4NgEnEDYBJxA2AScQNgEnEDYBJxA2AScQNgEnEDYBJxA2AScQNgEnEDYBJxA2AScQNgEnE\nDYBJxA2AScQNgEnEDYBJxA2AScQNgEnEDYBJxA2AScQNgEnEDYBJxA2AScQNgEnEDYBJxA2AScQN\ngEnEDYBJxA2AScQNgEm+jZvneerpOaJ4PKq2tueVTt9Ycd/Nm5+otbVJ8XhUPT1H5HnekvUTJ3oV\nCDianp6uxLErhvmUxoxW9zNJ35f0j6use5KOSIpKel7S1yd3WlJj4et0Gc/4Xfk2biMjlzU+nlE6\nnVFf34C6uw+tuK+7+5D6+gaUTmc0Pp7R6OiV4louN6mrV0fV0PBUpY5dMcynNGa0ujckXVlj/bKk\nTOFrQNKDyf2fpF9L+h9JqcL3fyjbKb8b38ZteHhInZ1dchxHLS1tmpmZ0dTU7SV7pqZua3Z2Vq2t\nL8lxHHV2dunixfPF9aNHu3Xs2HtyHKfSxy875lMaM1rdP0uqW2N9SFKXJEdSm6QZSbclfSRpe+Fn\nnyx8v1Ykq8m3ccvnXYXDDcXrcDiifN5dYU+keB0KPdwzPHxBoVBYTU3xyhy4wphPaczo23MlNXzt\nOlJ4bLXH/aim2gdYzTc/95C07Lfnanvm5+fV2/u2zp8fKdv5qo35lMaMvr3lU1m8i1vtcT/y1Z3b\nwMBJJRLNSiSaFQyG5LqTxTXXzSkYDC3ZHw5H5Lq54nU+v7gnmx3XxERWiURcsdjTct2ctm17QXfu\nTFXsuZQD8ymNGW2MiKTJr13nJIXWeNyPfBW3AwcOK5lMK5lMa8+eVzU4eEae5ymVuq7a2lrV1weX\n7K+vDyoQCCiVui7P8zQ4eEa7d7+iWKxJ2eynGhub0NjYhMLhiK5du6EtW+qr9Mw2BvMpjRltjA5J\nZ7R4p3ZdUq2koKR2SSNa/CPCHwrft1fpjKX49m1pe/sujYxcUjwe1aZNj6u//4PiWiLRrGQyLUk6\nfrxfBw++obt3v9T27Tu1Y8fOah25ophPacxodZ2S/lvStBbvxn4t6U+FtYOSdkm6pMV/CvK4pAeT\nq5P075K2Fq7f0tp/mKgmZ6XPHFYzN7fiW24AG2RzwK+fYPmI561rSL56WwoAG4W4ATCJuAEwibgB\nMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEw\nibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMIm4ATCJuAEwibgBMKmm2gew\nZPP3vGofwffmvnCqfQRfc8RrqJT1Tog7NwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3\nACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcA\nJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAm+TZunuepp+eI4vGo2tqeVzp9Y8V9N29+\notbWJsXjUfX0HJHneUvWT5zoVSDgaHp6uhLHrpgrV67oB889p2hjo959991l6/fu3dPeffsUbWxU\na1ubJiYmimvvvPOOoo2N+sFzz+mjjz6q4Kkri9dQKf8r6SVJj0nqXWNfVlKrpEZJeyV9VXj8XuE6\nWlifKNdBvxXfxm1k5LLGxzNKpzPq6xtQd/ehFfd1dx9SX9+A0umMxsczGh29UlzL5SZ19eqoGhqe\nqtSxK2JhYUGHf/5zXb50SbfGxjR49qxu3bq1ZM+pU6f05BNP6PeZjLp/8Qu9efSoJOnWrVs6e+6c\nxn73O125fFn/dviwFhYWqvE0yo7XUCl1kvok/bLEvjcldUvKSHpS0qnC46cK178vrL9ZnmN+S76N\n2/DwkDo7u+Q4jlpa2jQzM6OpqdtL9kxN3dbs7KxaW1+S4zjq7OzSxYvni+tHj3br2LH35DhOpY9f\nVqlUStFoVM8++6weffRR7du7V0NDQ0v2DF24oNdff12S9Nprr+njjz+W53kaGhrSvr179dhjj+mZ\nZ55RNBpVKpWqxtMoO15DpXxf0lZJf7vGHk/SbyW9Vrh+XdKD+QwVrlVY/7iw3x98G7d83lU43FC8\nDocjyufdFfZEiteh0MM9w8MXFAqF1dQUr8yBK8h1XTVEHj7vSCQi13WX72lYnF9NTY1qa2v12Wef\nLXlckiLh8LKftYLX0Eb4TNITkmoK1xFJD2boSnow3xpJtYX9/lBTekt1fPNzD0nLfnuutmd+fl69\nvW/r/PmRsp2vmr7LbNbzs1bwGtoIK92JOetYqz5f3bkNDJxUItGsRKJZwWBIrjtZXHPdnILB0JL9\n4XBErpsrXufzi3uy2XFNTGSVSMQViz0t181p27YXdOfOVMWeSzlFIhFN5h4+71wup1AotHzP5OL8\n7t+/r88//1x1dXVLHpeknOsu+9m/ZLyGSjkpqbnwlV/H/r+XNCPpfuE6J+nBDCOSHsz3vqTPtfg5\nnj/4Km4HDhxWMplWMpnWnj2vanDwjDzPUyp1XbW1taqvDy7ZX18fVCAQUCp1XZ7naXDwjHbvfkWx\nWJOy2U81NjahsbEJhcMRXbt2Q1u21FfpmW2srVu3KpPJKJvN6quvvtLZc+fU0dGxZE/Hj36k06dP\nS5I+/PBDvfzyy3IcRx0dHTp77pzu3bunbDarTCajlpaWajyNsuA1VMphSenC13p+qTmS/kXSh4Xr\n05JeKXzfUbhWYf1l+enOzbdvS9vbd2lk5JLi8ag2bXpc/f0fFNcSiWYlk2lJ0vHj/Tp48A3dvful\ntm/fqR07dlbryBVTU1Oj93/zG7X/8IdaWFjQz376U8ViMb311lt68cUX1dHRof379+snXV2KNjaq\nrq5OZwcHJUmxWEz/+uMf6x9iMdXU1Ojk++/rkUceqfIzKg9eQ6VMSXpR0qwW73P+U9ItSX8naZek\n/9JiAP9D0j5Jv5L0T5L2F35+v6SfaPGfgtRJOlvBs5fmrPSZw2rm5nz0pxAf2vw9xlPK3Bf++c3u\nR4FAtU/gf563vttDX70tBYCNQtwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwA\nmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYRNwAmETcAJhE3ACY\nRNwAmETcAJhE3ACYRNwAmETcAJhE3ACYVFPtA1gy94VT7SPgL9wf/1jtE9jBnRsAk4gbAJOIGwCT\niBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOI\nGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gb\nAJN8GzfP89TTc0TxeFRtbc8rnb6x4r6bNz9Ra2uT4vGoenqOyPO8JesnTvQqEHA0PT1diWNXDPMp\njRmtzfp8fBu3kZHLGh/PKJ3OqK9vQN3dh1bc1919SH19A0qnMxofz2h09EpxLZeb1NWro2poeKpS\nx64Y5lMaM1qb9fn4Nm7Dw0Pq7OyS4zhqaWnTzMyMpqZuL9kzNXVbs7Ozam19SY7jqLOzSxcvni+u\nHz3arWPH3pPjOJU+ftkxn9KY0dqsz8e3ccvnXYXDDcXrcDiifN5dYU+keB0KPdwzPHxBoVBYTU3x\nyhy4wphPacxobdbnU1PtA6zmm+/rJS377bDanvn5efX2vq3z50fKdr5qYz6lMaO1WZ+Pr+7cBgZO\nKpFoViLRrGAwJNedLK65bk7BYGjJ/nA4ItfNFa/z+cU92ey4JiaySiTiisWeluvmtG3bC7pzZ6pi\nz6UcmE9pzGhtf03z8VXcDhw4rGQyrWQyrT17XtXg4Bl5nqdU6rpqa2tVXx9csr++PqhAIKBU6ro8\nz9Pg4Bnt3v2KYrEmZbOfamxsQmNjEwqHI7p27Ya2bKmv0jPbGMynNGa0tr+m+fj2bWl7+y6NjFxS\nPB7Vpk2Pq7//g+JaItGsZDItSTp+vF8HD76hu3e/1PbtO7Vjx85qHbmimE9pzGht1ufjrPSeejVz\nc1r/ZgAog82bta4/zfrqbSkAbBTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTi\nBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIG\nwCTiBsAk4gbAJOIGwCTiBsAk4gbAJOIGwCTH87xqnwEANhx3bgBMIm4ATCJuAEwibgBMIm4ATCJu\nAEwibgBMIm4ATCJuAEwibgBM+jPdN0cNjYpeKAAAAABJRU5ErkJggg==\n", "text/plain": [ - "" + "" ] }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "The installed widget Javascript is the wrong version. It must satisfy the semver range ~2.1.4.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "77e9849e074841e49d8b0ebc8191507c" + } + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ "import ipywidgets as widgets\n", "from IPython.display import display\n", + "from notebook import make_visualize\n", "\n", "iteration_slider = widgets.IntSlider(min=1, max=15, step=1, value=0)\n", "w=widgets.interactive(plot_grid_step,iteration=iteration_slider)\n", @@ -455,7 +979,7 @@ "\n", "visualize_callback = make_visualize(iteration_slider)\n", "\n", - "visualize_button = widgets.ToggleButton(desctiption = \"Visualize\", value = False)\n", + "visualize_button = widgets.ToggleButton(description = \"Visualize\", value = False)\n", "time_select = widgets.ToggleButtons(description='Extra Delay:',options=['0', '0.1', '0.2', '0.5', '0.7', '1.0'])\n", "a = widgets.interactive(visualize_callback, Visualize = visualize_button, time_step=time_select)\n", "display(a)" @@ -465,7 +989,1986 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Move the slider above to observe how the utility changes across iterations. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click. The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds upto one second for each time step." + "Move the slider above to observe how the utility changes across iterations. It is also possible to move the slider using arrow keys or to jump to the value by directly editing the number with a double click. The **Visualize Button** will automatically animate the slider for you. The **Extra Delay Box** allows you to set time delay in seconds upto one second for each time step. There is also an interactive editor for grid-world problems `grid_mdp.py` in the gui folder for you to play around with." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "# POLICY ITERATION\n", + "\n", + "We have already seen that value iteration converges to the optimal policy long before it accurately estimates the utility function. \n", + "If one action is clearly better than all the others, then the exact magnitude of the utilities in the states involved need not be precise. \n", + "The policy iteration algorithm works on this insight. \n", + "The algorithm executes two fundamental steps:\n", + "* **Policy evaluation**: Given a policy _πᵢ_, calculate _Uᵢ = U(πᵢ)_, the utility of each state if _πᵢ_ were to be executed.\n", + "* **Policy improvement**: Calculate a new policy _πᵢ₊₁_ using one-step look-ahead based on the utility values calculated.\n", + "\n", + "The algorithm terminates when the policy improvement step yields no change in the utilities. \n", + "Refer to **Figure 17.6** in the book to see how this is an improvement over value iteration.\n", + "We now have a simplified version of the Bellman equation\n", + "\n", + "$$U_i(s) = R(s) + \\gamma \\sum_{s'}P(s'\\ |\\ s, \\pi_i(s))U_i(s')$$\n", + "\n", + "An important observation in this equation is that this equation doesn't have the `max` operator, which makes it linear.\n", + "For _n_ states, we have _n_ linear equations with _n_ unknowns, which can be solved exactly in time _**O(n³)**_.\n", + "For more implementational details, have a look at **Section 17.3**.\n", + "Let us now look at how the expected utility is found and how `policy_iteration` is implemented." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def expected_utility(a, s, U, mdp):\n",
    +       "    """The expected utility of doing a in state s, according to the MDP and U."""\n",
    +       "    return sum([p * U[s1] for (p, s1) in mdp.T(s, a)])\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(expected_utility)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def policy_iteration(mdp):\n",
    +       "    """Solve an MDP by policy iteration [Figure 17.7]"""\n",
    +       "    U = {s: 0 for s in mdp.states}\n",
    +       "    pi = {s: random.choice(mdp.actions(s)) for s in mdp.states}\n",
    +       "    while True:\n",
    +       "        U = policy_evaluation(pi, U, mdp)\n",
    +       "        unchanged = True\n",
    +       "        for s in mdp.states:\n",
    +       "            a = argmax(mdp.actions(s), key=lambda a: expected_utility(a, s, U, mdp))\n",
    +       "            if a != pi[s]:\n",
    +       "                pi[s] = a\n",
    +       "                unchanged = False\n",
    +       "        if unchanged:\n",
    +       "            return pi\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(policy_iteration)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
    Fortunately, it is not necessary to do _exact_ policy evaluation. \n", + "The utilities can instead be reasonably approximated by performing some number of simplified value iteration steps.\n", + "The simplified Bellman update equation for the process is\n", + "\n", + "$$U_{i+1}(s) \\leftarrow R(s) + \\gamma\\sum_{s'}P(s'\\ |\\ s,\\pi_i(s))U_{i}(s')$$\n", + "\n", + "and this is repeated _k_ times to produce the next utility estimate. This is called _modified policy iteration_." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def policy_evaluation(pi, U, mdp, k=20):\n",
    +       "    """Return an updated utility mapping U from each state in the MDP to its\n",
    +       "    utility, using an approximation (modified policy iteration)."""\n",
    +       "    R, T, gamma = mdp.R, mdp.T, mdp.gamma\n",
    +       "    for i in range(k):\n",
    +       "        for s in mdp.states:\n",
    +       "            U[s] = R(s) + gamma * sum([p * U[s1] for (p, s1) in T(s, pi[s])])\n",
    +       "    return U\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(policy_evaluation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us now solve **`sequential_decision_environment`** using `policy_iteration`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{(0, 0): (0, 1),\n", + " (0, 1): (0, 1),\n", + " (0, 2): (1, 0),\n", + " (1, 0): (1, 0),\n", + " (1, 2): (1, 0),\n", + " (2, 0): (0, 1),\n", + " (2, 1): (0, 1),\n", + " (2, 2): (1, 0),\n", + " (3, 0): (-1, 0),\n", + " (3, 1): None,\n", + " (3, 2): None}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "policy_iteration(sequential_decision_environment)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ POLICY-ITERATION(_mdp_) __returns__ a policy \n", + " __inputs__: _mdp_, an MDP with states _S_, actions _A_(_s_), transition model _P_(_s′_ | _s_, _a_) \n", + " __local variables__: _U_, a vector of utilities for states in _S_, initially zero \n", + "        _π_, a policy vector indexed by state, initially random \n", + "\n", + " __repeat__ \n", + "   _U_ ← POLICY\\-EVALUATION(_π_, _U_, _mdp_) \n", + "   _unchanged?_ ← true \n", + "   __for each__ state _s_ __in__ _S_ __do__ \n", + "     __if__ max_a_ ∈ _A_(_s_) Σ_s′_ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] > Σ_s′_ _P_(_s′_ | _s_, _π_\\[_s_\\]) _U_\\[_s′_\\] __then do__ \n", + "       _π_\\[_s_\\] ← argmax_a_ ∈ _A_(_s_) Σ_s′_ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] \n", + "       _unchanged?_ ← false \n", + " __until__ _unchanged?_ \n", + " __return__ _π_ \n", + "\n", + "---\n", + "__Figure ??__ The policy iteration algorithm for calculating an optimal policy." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('Policy-Iteration')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### AIMA3e\n", + "__function__ POLICY-ITERATION(_mdp_) __returns__ a policy \n", + " __inputs__: _mdp_, an MDP with states _S_, actions _A_(_s_), transition model _P_(_s′_ | _s_, _a_) \n", + " __local variables__: _U_, a vector of utilities for states in _S_, initially zero \n", + "        _π_, a policy vector indexed by state, initially random \n", + "\n", + " __repeat__ \n", + "   _U_ ← POLICY\\-EVALUATION(_π_, _U_, _mdp_) \n", + "   _unchanged?_ ← true \n", + "   __for each__ state _s_ __in__ _S_ __do__ \n", + "     __if__ max_a_ ∈ _A_(_s_) Σ_s′_ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] > Σ_s′_ _P_(_s′_ | _s_, _π_\\[_s_\\]) _U_\\[_s′_\\] __then do__ \n", + "       _π_\\[_s_\\] ← argmax_a_ ∈ _A_(_s_) Σ_s′_ _P_(_s′_ | _s_, _a_) _U_\\[_s′_\\] \n", + "       _unchanged?_ ← false \n", + " __until__ _unchanged?_ \n", + " __return__ _π_ \n", + "\n", + "---\n", + "__Figure ??__ The policy iteration algorithm for calculating an optimal policy." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## Sequential Decision Problems\n", + "\n", + "Now that we have the tools required to solve MDPs, let us see how Sequential Decision Problems can be solved step by step and how a few built-in tools in the GridMDP class help us better analyse the problem at hand. \n", + "As always, we will work with the grid world from **Figure 17.1** from the book.\n", + "![title](images/grid_mdp.jpg)\n", + "
    This is the environment for our agent.\n", + "We assume for now that the environment is _fully observable_, so that the agent always knows where it is.\n", + "We also assume that the transitions are **Markovian**, that is, the probability of reaching state $s'$ from state $s$ depends only on $s$ and not on the history of earlier states.\n", + "Almost all stochastic decision problems can be reframed as a Markov Decision Process just by tweaking the definition of a _state_ for that particular problem.\n", + "
    \n", + "However, the actions of our agent in this environment are unreliable. In other words, the motion of our agent is stochastic. \n", + "

    \n", + "More specifically, the agent may - \n", + "* move correctly in the intended direction with a probability of _0.8_, \n", + "* move $90^\\circ$ to the right of the intended direction with a probability 0.1\n", + "* move $90^\\circ$ to the left of the intended direction with a probability 0.1\n", + "

    \n", + "The agent stays put if it bumps into a wall.\n", + "![title](images/grid_mdp_agent.jpg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These properties of the agent are called the transition properties and are hardcoded into the GridMDP class as you can see below." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
        def T(self, state, action):\n",
    +       "        if action is None:\n",
    +       "            return [(0.0, state)]\n",
    +       "        else:\n",
    +       "            return self.transitions[state][action]\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(GridMDP.T)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To completely define our task environment, we need to specify the utility function for the agent. \n", + "This is the function that gives the agent a rough estimate of how good being in a particular state is, or how much _reward_ an agent receives by being in that state.\n", + "The agent then tries to maximize the reward it gets.\n", + "As the decision problem is sequential, the utility function will depend on a sequence of states rather than on a single state.\n", + "For now, we simply stipulate that in each state $s$, the agent receives a finite reward $R(s)$.\n", + "\n", + "For any given state, the actions the agent can take are encoded as given below:\n", + "- Move Up: (0, 1)\n", + "- Move Down: (0, -1)\n", + "- Move Left: (-1, 0)\n", + "- Move Right: (1, 0)\n", + "- Do nothing: `None`\n", + "\n", + "We now wonder what a valid solution to the problem might look like. \n", + "We cannot have fixed action sequences as the environment is stochastic and we can eventually end up in an undesirable state.\n", + "Therefore, a solution must specify what the agent shoulddo for _any_ state the agent might reach.\n", + "
    \n", + "Such a solution is known as a **policy** and is usually denoted by $\\pi$.\n", + "
    \n", + "The **optimal policy** is the policy that yields the highest expected utility an is usually denoted by $\\pi^*$.\n", + "
    \n", + "The `GridMDP` class has a useful method `to_arrows` that outputs a grid showing the direction the agent should move, given a policy.\n", + "We will use this later to better understand the properties of the environment." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
        def to_arrows(self, policy):\n",
    +       "        chars = {\n",
    +       "            (1, 0): '>', (0, 1): '^', (-1, 0): '<', (0, -1): 'v', None: '.'}\n",
    +       "        return self.to_grid({s: chars[a] for (s, a) in policy.items()})\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(GridMDP.to_arrows)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This method directly encodes the actions that the agent can take (described above) to characters representing arrows and shows it in a grid format for human visalization purposes. \n", + "It converts the received policy from a `dictionary` to a grid using the `to_grid` method." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
        def to_grid(self, mapping):\n",
    +       "        """Convert a mapping from (x, y) to v into a [[..., v, ...]] grid."""\n",
    +       "        return list(reversed([[mapping.get((x, y), None)\n",
    +       "                               for x in range(self.cols)]\n",
    +       "                              for y in range(self.rows)]))\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(GridMDP.to_grid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have all the tools required and a good understanding of the agent and the environment, we consider some cases and see how the agent should behave for each case." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Case 1\n", + "---\n", + "R(s) = -0.04 in all states except terminal states" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Note that this environment is also initialized in mdp.py by default\n", + "sequential_decision_environment = GridMDP([[-0.04, -0.04, -0.04, +1],\n", + " [-0.04, None, -0.04, -1],\n", + " [-0.04, -0.04, -0.04, -0.04]],\n", + " terminals=[(3, 2), (3, 1)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will use the `best_policy` function to find the best policy for this environment.\n", + "But, as you can see, `best_policy` requires a utility function as well.\n", + "We already know that the utility function can be found by `value_iteration`.\n", + "Hence, our best policy is:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .001))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now use the `to_arrows` method to see how our agent should pick its actions in the environment." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> > > .\n", + "^ None ^ .\n", + "^ > ^ <\n" + ] + } + ], + "source": [ + "from utils import print_table\n", + "print_table(sequential_decision_environment.to_arrows(pi))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is exactly the output we expected\n", + "
    \n", + "![title](images/-0.04.jpg)\n", + "
    \n", + "Notice that, because the cost of taking a step is fairly small compared with the penalty for ending up in `(4, 2)` by accident, the optimal policy is conservative. \n", + "In state `(3, 1)` it recommends taking the long way round, rather than taking the shorter way and risking getting a large negative reward of -1 in `(4, 2)`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Case 2\n", + "---\n", + "R(s) = -0.4 in all states except in terminal states" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sequential_decision_environment = GridMDP([[-0.4, -0.4, -0.4, +1],\n", + " [-0.4, None, -0.4, -1],\n", + " [-0.4, -0.4, -0.4, -0.4]],\n", + " terminals=[(3, 2), (3, 1)])" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> > > .\n", + "^ None ^ .\n", + "^ > ^ <\n" + ] + } + ], + "source": [ + "pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .001))\n", + "from utils import print_table\n", + "print_table(sequential_decision_environment.to_arrows(pi))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is exactly the output we expected\n", + "![title](images/-0.4.jpg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the reward for each state is now more negative, life is certainly more unpleasant.\n", + "The agent takes the shortest route to the +1 state and is willing to risk falling into the -1 state by accident." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Case 3\n", + "---\n", + "R(s) = -4 in all states except terminal states" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sequential_decision_environment = GridMDP([[-4, -4, -4, +1],\n", + " [-4, None, -4, -1],\n", + " [-4, -4, -4, -4]],\n", + " terminals=[(3, 2), (3, 1)])" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> > > .\n", + "^ None > .\n", + "> > > ^\n" + ] + } + ], + "source": [ + "pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .001))\n", + "from utils import print_table\n", + "print_table(sequential_decision_environment.to_arrows(pi))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is exactly the output we expected\n", + "![title](images/-4.jpg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The living reward for each state is now lower than the least rewarding terminal. Life is so _painful_ that the agent heads for the nearest exit as even the worst exit is less painful than any living state." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Case 4\n", + "---\n", + "R(s) = 4 in all states except terminal states" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sequential_decision_environment = GridMDP([[4, 4, 4, +1],\n", + " [4, None, 4, -1],\n", + " [4, 4, 4, 4]],\n", + " terminals=[(3, 2), (3, 1)])" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> > < .\n", + "> None < .\n", + "> > > v\n" + ] + } + ], + "source": [ + "pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .001))\n", + "from utils import print_table\n", + "print_table(sequential_decision_environment.to_arrows(pi))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case, the output we expect is\n", + "![title](images/4.jpg)\n", + "
    \n", + "As life is positively enjoyable and the agent avoids _both_ exits.\n", + "Even though the output we get is not exactly what we want, it is definitely not wrong.\n", + "The scenario here requires the agent to anything but reach a terminal state, as this is the only way the agent can maximize its reward (total reward tends to infinity), and the program does just that.\n", + "
    \n", + "Currently, the GridMDP class doesn't support an explicit marker for a \"do whatever you like\" action or a \"don't care\" condition.\n", + "You can however, extend the class to do so.\n", + "
    \n", + "For in-depth knowledge about sequential decision problems, refer **Section 17.1** in the AIMA book." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## POMDP\n", + "---\n", + "Partially Observable Markov Decision Problems\n", + "\n", + "In retrospect, a Markov decision process or MDP is defined as:\n", + "- a sequential decision problem for a fully observable, stochastic environment with a Markovian transition model and additive rewards.\n", + "\n", + "An MDP consists of a set of states (with an initial state $s_0$); a set $A(s)$ of actions\n", + "in each state; a transition model $P(s' | s, a)$; and a reward function $R(s)$.\n", + "\n", + "The MDP seeks to make sequential decisions to occupy states so as to maximise some combination of the reward function $R(s)$.\n", + "\n", + "The characteristic problem of the MDP is hence to identify the optimal policy function $\\pi^*(s)$ that provides the _utility-maximising_ action $a$ to be taken when the current state is $s$.\n", + "\n", + "### Belief vector\n", + "\n", + "**Note**: The book refers to the _belief vector_ as the _belief state_. We use the latter terminology here to retain our ability to refer to the belief vector as a _probability distribution over states_.\n", + "\n", + "The solution of an MDP is subject to certain properties of the problem which are assumed and justified in [Section 17.1]. One critical assumption is that the agent is **fully aware of its current state at all times**.\n", + "\n", + "A tedious (but rewarding, as we will see) way of expressing this is in terms of the **belief vector** $b$ of the agent. The belief vector is a function mapping states to probabilities or certainties of being in those states.\n", + "\n", + "Consider an agent that is fully aware that it is in state $s_i$ in the statespace $(s_1, s_2, ... s_n)$ at the current time.\n", + "\n", + "Its belief vector is the vector $(b(s_1), b(s_2), ... b(s_n))$ given by the function $b(s)$:\n", + "\\begin{align*}\n", + "b(s) &= 0 \\quad \\text{if }s \\neq s_i \\\\ &= 1 \\quad \\text{if } s = s_i\n", + "\\end{align*}\n", + "\n", + "Note that $b(s)$ is a probability distribution that necessarily sums to $1$ over all $s$.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### POMDPs - a conceptual outline\n", + "\n", + "The POMDP really has only two modifications to the **problem formulation** compared to the MDP.\n", + "\n", + "- **Belief state** - In the real world, the current state of an agent is often not known with complete certainty. This makes the concept of a belief vector extremely relevant. It allows the agent to represent different degrees of certainty with which it _believes_ it is in each state.\n", + "\n", + "- **Evidence percepts** - In the real world, agents often have certain kinds of evidence, collected from sensors. They can use the probability distribution of observed evidence, conditional on state, to consolidate their information. This is a known distribution $P(e\\ |\\ s)$ - $e$ being an evidence, and $s$ being the state it is conditional on.\n", + "\n", + "Consider the world we used for the MDP. \n", + "\n", + "![title](images/grid_mdp.jpg)\n", + "\n", + "#### Using the belief vector\n", + "An agent beginning at $(1, 1)$ may not be certain that it is indeed in $(1, 1)$. Consider a belief vector $b$ such that:\n", + "\\begin{align*}\n", + " b((1,1)) &= 0.8 \\\\\n", + " b((2,1)) &= 0.1 \\\\\n", + " b((1,2)) &= 0.1 \\\\\n", + " b(s) &= 0 \\quad \\quad \\forall \\text{ other } s\n", + "\\end{align*}\n", + "\n", + "By horizontally catenating each row, we can represent this as an 11-dimensional vector (omitting $(2, 2)$).\n", + "\n", + "Thus, taking $s_1 = (1, 1)$, $s_2 = (1, 2)$, ... $s_{11} = (4,3)$, we have $b$:\n", + "\n", + "$b = (0.8, 0.1, 0, 0, 0.1, 0, 0, 0, 0, 0, 0)$ \n", + "\n", + "This fully represents the certainty to which the agent is aware of its state.\n", + "\n", + "#### Using evidence\n", + "The evidence observed here could be the number of adjacent 'walls' or 'dead ends' observed by the agent. We assume that the agent cannot 'orient' the walls - only count them.\n", + "\n", + "In this case, $e$ can take only two values, 1 and 2. This gives $P(e\\ |\\ s)$ as:\n", + "\\begin{align*}\n", + " P(e=2\\ |\\ s) &= \\frac{1}{7} \\quad \\forall \\quad s \\in \\{s_1, s_2, s_4, s_5, s_8, s_9, s_{11}\\}\\\\\n", + " P(e=1\\ |\\ s) &= \\frac{1}{4} \\quad \\forall \\quad s \\in \\{s_3, s_6, s_7, s_{10}\\} \\\\\n", + " P(e\\ |\\ s) &= 0 \\quad \\forall \\quad \\text{ other } s, e\n", + "\\end{align*}\n", + "\n", + "Note that the implications of the evidence on the state must be known **a priori** to the agent. Ways of reliably learning this distribution from percepts are beyond the scope of this notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### POMDPs - a rigorous outline\n", + "\n", + "A POMDP is thus a sequential decision problem for for a *partially* observable, stochastic environment with a Markovian transition model, a known 'sensor model' for inferring state from observation, and additive rewards. \n", + "\n", + "Practically, a POMDP has the following, which an MDP also has:\n", + "- a set of states, each denoted by $s$\n", + "- a set of actions available in each state, $A(s)$\n", + "- a reward accrued on attaining some state, $R(s)$\n", + "- a transition probability $P(s'\\ |\\ s, a)$ of action $a$ changing the state from $s$ to $s'$\n", + "\n", + "And the following, which an MDP does not:\n", + "- a sensor model $P(e\\ |\\ s)$ on evidence conditional on states\n", + "\n", + "Additionally, the POMDP is now uncertain of its current state hence has:\n", + "- a belief vector $b$ representing the certainty of being in each state (as a probability distribution)\n", + "\n", + "\n", + "#### New uncertainties\n", + "\n", + "It is useful to intuitively appreciate the new uncertainties that have arisen in the agent's awareness of its own state.\n", + "\n", + "- At any point, the agent has belief vector $b$, the distribution of its believed likelihood of being in each state $s$.\n", + "- For each of these states $s$ that the agent may **actually** be in, it has some set of actions given by $A(s)$.\n", + "- Each of these actions may transport it to some other state $s'$, assuming an initial state $s$, with probability $P(s'\\ |\\ s, a)$\n", + "- Once the action is performed, the agent receives a percept $e$. $P(e\\ |\\ s)$ now tells it the chances of having perceived $e$ for each state $s$. The agent must use this information to update its new belief state appropriately.\n", + "\n", + "#### Evolution of the belief vector - the `FORWARD` function\n", + "\n", + "The new belief vector $b'(s')$ after an action $a$ on the belief vector $b(s)$ and the noting of evidence $e$ is:\n", + "$$ b'(s') = \\alpha P(e\\ |\\ s') \\sum_s P(s'\\ | s, a) b(s)$$ \n", + "\n", + "where $\\alpha$ is a normalising constant (to retain the interpretation of $b$ as a probability distribution.\n", + "\n", + "This equation is just counts the sum of likelihoods of going to a state $s'$ from every possible state $s$, times the initial likelihood of being in each $s$. This is multiplied by the likelihood that the known evidence actually implies the new state $s'$. \n", + "\n", + "This function is represented as `b' = FORWARD(b, a, e)`\n", + "\n", + "#### Probability distribution of the evolving belief vector\n", + "\n", + "The goal here is to find $P(b'\\ |\\ b, a)$ - the probability that action $a$ transforms belief vector $b$ into belief vector $b'$. The following steps illustrate this -\n", + "\n", + "The probability of observing evidence $e$ when action $a$ is enacted on belief vector $b$ can be distributed over each possible new state $s'$ resulting from it:\n", + "\\begin{align*}\n", + " P(e\\ |\\ b, a) &= \\sum_{s'} P(e\\ |\\ b, a, s') P(s'\\ |\\ b, a) \\\\\n", + " &= \\sum_{s'} P(e\\ |\\ s') P(s'\\ |\\ b, a) \\\\\n", + " &= \\sum_{s'} P(e\\ |\\ s') \\sum_s P(s'\\ |\\ s, a) b(s)\n", + "\\end{align*}\n", + "\n", + "The probability of getting belief vector $b'$ from $b$ by application of action $a$ can thus be summed over all possible evidences $e$:\n", + "\\begin{align*}\n", + " P(b'\\ |\\ b, a) &= \\sum_{e} P(b'\\ |\\ b, a, e) P(e\\ |\\ b, a) \\\\\n", + " &= \\sum_{e} P(b'\\ |\\ b, a, e) \\sum_{s'} P(e\\ |\\ s') \\sum_s P(s'\\ |\\ s, a) b(s)\n", + "\\end{align*}\n", + "\n", + "where $P(b'\\ |\\ b, a, e) = 1$ if $b' = $ `FORWARD(b, a, e)` and $= 0$ otherwise.\n", + "\n", + "Given initial and final belief states $b$ and $b'$, the transition probabilities still depend on the action $a$ and observed evidence $e$. Some belief states may be achievable by certain actions, but have non-zero probabilities for states prohibited by the evidence $e$. Thus, the above condition thus ensures that only valid combinations of $(b', b, a, e)$ are considered.\n", + "\n", + "#### A modified rewardspace\n", + "\n", + "For MDPs, the reward space was simple - one reward per available state. However, for a belief vector $b(s)$, the expected reward is now:\n", + "$$\\rho(b) = \\sum_s b(s) R(s)$$\n", + "\n", + "Thus, as the belief vector can take infinite values of the distribution over states, so can the reward for each belief vector vary over a hyperplane in the belief space, or space of states (planes in an $N$-dimensional space are formed by a linear combination of the axes)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we know the basics, let's have a look at the `POMDP` class." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    class POMDP(MDP):\n",
    +       "\n",
    +       "    """A Partially Observable Markov Decision Process, defined by\n",
    +       "    a transition model P(s'|s,a), actions A(s), a reward function R(s),\n",
    +       "    and a sensor model P(e|s). We also keep track of a gamma value,\n",
    +       "    for use by algorithms. The transition and the sensor models\n",
    +       "    are defined as matrices. We also keep track of the possible states\n",
    +       "    and actions for each state. [page 659]."""\n",
    +       "\n",
    +       "    def __init__(self, actions, transitions=None, evidences=None, rewards=None, states=None, gamma=0.95):\n",
    +       "        """Initialize variables of the pomdp"""\n",
    +       "\n",
    +       "        if not (0 < gamma <= 1):\n",
    +       "            raise ValueError('A POMDP must have 0 < gamma <= 1')\n",
    +       "\n",
    +       "        self.states = states\n",
    +       "        self.actions = actions\n",
    +       "\n",
    +       "        # transition model cannot be undefined\n",
    +       "        self.t_prob = transitions or {}\n",
    +       "        if not self.t_prob:\n",
    +       "            print('Warning: Transition model is undefined')\n",
    +       "        \n",
    +       "        # sensor model cannot be undefined\n",
    +       "        self.e_prob = evidences or {}\n",
    +       "        if not self.e_prob:\n",
    +       "            print('Warning: Sensor model is undefined')\n",
    +       "        \n",
    +       "        self.gamma = gamma\n",
    +       "        self.rewards = rewards\n",
    +       "\n",
    +       "    def remove_dominated_plans(self, input_values):\n",
    +       "        """\n",
    +       "        Remove dominated plans.\n",
    +       "        This method finds all the lines contributing to the\n",
    +       "        upper surface and removes those which don't.\n",
    +       "        """\n",
    +       "\n",
    +       "        values = [val for action in input_values for val in input_values[action]]\n",
    +       "        values.sort(key=lambda x: x[0], reverse=True)\n",
    +       "\n",
    +       "        best = [values[0]]\n",
    +       "        y1_max = max(val[1] for val in values)\n",
    +       "        tgt = values[0]\n",
    +       "        prev_b = 0\n",
    +       "        prev_ix = 0\n",
    +       "        while tgt[1] != y1_max:\n",
    +       "            min_b = 1\n",
    +       "            min_ix = 0\n",
    +       "            for i in range(prev_ix + 1, len(values)):\n",
    +       "                if values[i][0] - tgt[0] + tgt[1] - values[i][1] != 0:\n",
    +       "                    trans_b = (values[i][0] - tgt[0]) / (values[i][0] - tgt[0] + tgt[1] - values[i][1])\n",
    +       "                    if 0 <= trans_b <= 1 and trans_b > prev_b and trans_b < min_b:\n",
    +       "                        min_b = trans_b\n",
    +       "                        min_ix = i\n",
    +       "            prev_b = min_b\n",
    +       "            prev_ix = min_ix\n",
    +       "            tgt = values[min_ix]\n",
    +       "            best.append(tgt)\n",
    +       "\n",
    +       "        return self.generate_mapping(best, input_values)\n",
    +       "\n",
    +       "    def remove_dominated_plans_fast(self, input_values):\n",
    +       "        """\n",
    +       "        Remove dominated plans using approximations.\n",
    +       "        Resamples the upper boundary at intervals of 100 and\n",
    +       "        finds the maximum values at these points.\n",
    +       "        """\n",
    +       "\n",
    +       "        values = [val for action in input_values for val in input_values[action]]\n",
    +       "        values.sort(key=lambda x: x[0], reverse=True)\n",
    +       "\n",
    +       "        best = []\n",
    +       "        sr = 100\n",
    +       "        for i in range(sr + 1):\n",
    +       "            x = i / float(sr)\n",
    +       "            maximum = (values[0][1] - values[0][0]) * x + values[0][0]\n",
    +       "            tgt = values[0]\n",
    +       "            for value in values:\n",
    +       "                val = (value[1] - value[0]) * x + value[0]\n",
    +       "                if val > maximum:\n",
    +       "                    maximum = val\n",
    +       "                    tgt = value\n",
    +       "\n",
    +       "            if all(any(tgt != v) for v in best):\n",
    +       "                best.append(tgt)\n",
    +       "\n",
    +       "        return self.generate_mapping(best, input_values)\n",
    +       "\n",
    +       "    def generate_mapping(self, best, input_values):\n",
    +       "        """Generate mappings after removing dominated plans"""\n",
    +       "\n",
    +       "        mapping = defaultdict(list)\n",
    +       "        for value in best:\n",
    +       "            for action in input_values:\n",
    +       "                if any(all(value == v) for v in input_values[action]):\n",
    +       "                    mapping[action].append(value)\n",
    +       "\n",
    +       "        return mapping\n",
    +       "\n",
    +       "    def max_difference(self, U1, U2):\n",
    +       "        """Find maximum difference between two utility mappings"""\n",
    +       "\n",
    +       "        for k, v in U1.items():\n",
    +       "            sum1 = 0\n",
    +       "            for element in U1[k]:\n",
    +       "                sum1 += sum(element)\n",
    +       "            sum2 = 0\n",
    +       "            for element in U2[k]:\n",
    +       "                sum2 += sum(element)\n",
    +       "        return abs(sum1 - sum2)\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(POMDP)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `POMDP` class includes all variables of the `MDP` class and additionally also stores the sensor model in `e_prob`.\n", + "
    \n", + "
    \n", + "`remove_dominated_plans`, `remove_dominated_plans_fast`, `generate_mapping` and `max_difference` are helper methods for `pomdp_value_iteration` which will be explained shortly." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To understand how we can model a partially observable MDP, let's take a simple example.\n", + "Let's consider a simple two state world.\n", + "The states are labelled 0 and 1, with the reward at state 0 being 0 and at state 1 being 1.\n", + "
    \n", + "There are two actions:\n", + "
    \n", + "`Stay`: stays put with probability 0.9 and\n", + "`Go`: switches to the other state with probability 0.9.\n", + "
    \n", + "For now, let's assume the discount factor `gamma` to be 1.\n", + "
    \n", + "The sensor reports the correct state with probability 0.6.\n", + "
    \n", + "This is a simple problem with a trivial solution.\n", + "Obviously the agent should `Stay` when it thinks it is in state 1 and `Go` when it thinks it is in state 0.\n", + "
    \n", + "The belief space can be viewed as one-dimensional because the two probabilities must sum to 1." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's model this POMDP using the `POMDP` class." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "# transition probability P(s'|s,a)\n", + "t_prob = [[[0.9, 0.1], [0.1, 0.9]], [[0.1, 0.9], [0.9, 0.1]]]\n", + "# evidence function P(e|s)\n", + "e_prob = [[[0.6, 0.4], [0.4, 0.6]], [[0.6, 0.4], [0.4, 0.6]]]\n", + "# reward function\n", + "rewards = [[0.0, 0.0], [1.0, 1.0]]\n", + "# discount factor\n", + "gamma = 0.95\n", + "# actions\n", + "actions = ('0', '1')\n", + "# states\n", + "states = ('0', '1')" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "pomdp = POMDP(actions, t_prob, e_prob, rewards, states, gamma)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have defined our `POMDP` object." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## POMDP VALUE ITERATION\n", + "Defining a POMDP is useless unless we can find a way to solve it. As POMDPs can have infinitely many belief states, we cannot calculate one utility value for each state as we did in `value_iteration` for MDPs.\n", + "
    \n", + "Instead of thinking about policies, we should think about conditional plans and how the expected utility of executing a fixed conditional plan varies with the initial belief state.\n", + "
    \n", + "If we bound the depth of the conditional plans, then there are only finitely many such plans and the continuous space of belief states will generally be divided inte _regions_, each corresponding to a particular conditional plan that is optimal in that region. The utility function, being the maximum of a collection of hyperplanes, will be piecewise linear and convex.\n", + "
    \n", + "For the one-step plans `Stay` and `Go`, the utility values are as follows\n", + "
    \n", + "
    \n", + "$$\\alpha_{|Stay|}(0) = R(0) + \\gamma(0.9R(0) + 0.1R(1)) = 0.1$$\n", + "$$\\alpha_{|Stay|}(1) = R(1) + \\gamma(0.9R(1) + 0.1R(0)) = 1.9$$\n", + "$$\\alpha_{|Go|}(0) = R(0) + \\gamma(0.9R(1) + 0.1R(0)) = 0.9$$\n", + "$$\\alpha_{|Go|}(1) = R(1) + \\gamma(0.9R(0) + 0.1R(1)) = 1.1$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The utility function can be found by `pomdp_value_iteration`.\n", + "
    \n", + "To summarize, it generates a set of all plans consisting of an action and, for each possible next percept, a plan in U with computed utility vectors.\n", + "The dominated plans are then removed from this set and the process is repeated till the maximum difference between the utility functions of two consecutive iterations reaches a value less than a threshold value." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ POMDP-VALUE-ITERATION(_pomdp_, _ε_) __returns__ a utility function \n", + " __inputs__: _pomdp_, a POMDP with states _S_, actions _A_(_s_), transition model _P_(_s′_ | _s_, _a_), \n", + "      sensor model _P_(_e_ | _s_), rewards _R_(_s_), discount _γ_ \n", + "     _ε_, the maximum error allowed in the utility of any state \n", + " __local variables__: _U_, _U′_, sets of plans _p_ with associated utility vectors _αp_ \n", + "\n", + " _U′_ ← a set containing just the empty plan \\[\\], with _α\\[\\]_(_s_) = _R_(_s_) \n", + " __repeat__ \n", + "   _U_ ← _U′_ \n", + "   _U′_ ← the set of all plans consisting of an action and, for each possible next percept, \n", + "     a plan in _U_ with utility vectors computed according to Equation(__??__) \n", + "   _U′_ ← REMOVE\\-DOMINATED\\-PLANS(_U′_) \n", + " __until__ MAX\\-DIFFERENCE(_U_, _U′_) < _ε_(1 − _γ_) ⁄ _γ_ \n", + " __return__ _U_ \n", + "\n", + "---\n", + "__Figure ??__ A high\\-level sketch of the value iteration algorithm for POMDPs. The REMOVE\\-DOMINATED\\-PLANS step and MAX\\-DIFFERENCE test are typically implemented as linear programs." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('POMDP-Value-Iteration')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's have a look at the `pomdp_value_iteration` function." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def pomdp_value_iteration(pomdp, epsilon=0.1):\n",
    +       "    """Solving a POMDP by value iteration."""\n",
    +       "\n",
    +       "    U = {'':[[0]* len(pomdp.states)]}\n",
    +       "    count = 0\n",
    +       "    while True:\n",
    +       "        count += 1\n",
    +       "        prev_U = U\n",
    +       "        values = [val for action in U for val in U[action]]\n",
    +       "        value_matxs = []\n",
    +       "        for i in values:\n",
    +       "            for j in values:\n",
    +       "                value_matxs.append([i, j])\n",
    +       "\n",
    +       "        U1 = defaultdict(list)\n",
    +       "        for action in pomdp.actions:\n",
    +       "            for u in value_matxs:\n",
    +       "                u1 = Matrix.matmul(Matrix.matmul(pomdp.t_prob[int(action)], Matrix.multiply(pomdp.e_prob[int(action)], Matrix.transpose(u))), [[1], [1]])\n",
    +       "                u1 = Matrix.add(Matrix.scalar_multiply(pomdp.gamma, Matrix.transpose(u1)), [pomdp.rewards[int(action)]])\n",
    +       "                U1[action].append(u1[0])\n",
    +       "\n",
    +       "        U = pomdp.remove_dominated_plans_fast(U1)\n",
    +       "        # replace with U = pomdp.remove_dominated_plans(U1) for accurate calculations\n",
    +       "        \n",
    +       "        if count > 10:\n",
    +       "            if pomdp.max_difference(U, prev_U) < epsilon * (1 - pomdp.gamma) / pomdp.gamma:\n",
    +       "                return U\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(pomdp_value_iteration)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This function uses two aptly named helper methods from the `POMDP` class, `remove_dominated_plans` and `max_difference`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try solving a simple one-dimensional POMDP using value-iteration.\n", + "
    \n", + "Consider the problem of a user listening to voicemails.\n", + "At the end of each message, they can either _save_ or _delete_ a message.\n", + "This forms the unobservable state _S = {save, delete}_.\n", + "It is the task of the POMDP solver to guess which goal the user has.\n", + "
    \n", + "The belief space has two elements, _b(s = save)_ and _b(s = delete)_.\n", + "For example, for the belief state _b = (1, 0)_, the left end of the line segment indicates _b(s = save) = 1_ and _b(s = delete) = 0_.\n", + "The intermediate points represent varying degrees of certainty in the user's goal.\n", + "
    \n", + "The machine has three available actions: it can _ask_ what the user wishes to do in order to infer his or her current goal, or it can _doSave_ or _doDelete_ and move to the next message.\n", + "If the user says _save_, then an error may occur with probability 0.2, whereas if the user says _delete_, an error may occur with a probability 0.3.\n", + "
    \n", + "The machine receives a large positive reward (+5) for getting the user's goal correct, a very large negative reward (-20) for taking the action _doDelete_ when the user wanted _save_, and a smaller but still significant negative reward (-10) for taking the action _doSave_ when the user wanted _delete_. \n", + "There is also a small negative reward for taking the _ask_ action (-1).\n", + "The discount factor is set to 0.95 for this example.\n", + "
    \n", + "Let's define the POMDP." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "# transition function P(s'|s,a)\n", + "t_prob = [[[0.65, 0.35], [0.65, 0.35]], [[0.65, 0.35], [0.65, 0.35]], [[1.0, 0.0], [0.0, 1.0]]]\n", + "# evidence function P(e|s)\n", + "e_prob = [[[0.5, 0.5], [0.5, 0.5]], [[0.5, 0.5], [0.5, 0.5]], [[0.8, 0.2], [0.3, 0.7]]]\n", + "# reward function\n", + "rewards = [[5, -10], [-20, 5], [-1, -1]]\n", + "\n", + "gamma = 0.95\n", + "actions = ('0', '1', '2')\n", + "states = ('0', '1')\n", + "\n", + "pomdp = POMDP(actions, t_prob, e_prob, rewards, states, gamma)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have defined the `POMDP` object.\n", + "Let's run `pomdp_value_iteration` to find the utility function." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "utility = pomdp_value_iteration(pomdp, epsilon=0.1)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "plot_pomdp_utility(utility)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Appendix\n", + "\n", + "Surprisingly, it turns out that there are six other optimal policies for various ranges of R(s). \n", + "You can try to find them out for yourself.\n", + "See **Exercise 17.5**.\n", + "To help you with this, we have a GridMDP editor in `grid_mdp.py` in the GUI folder. \n", + "
    \n", + "Here's a brief tutorial about how to use it\n", + "
    \n", + "Let us use it to solve `Case 2` above\n", + "1. Run `python gui/grid_mdp.py` from the master directory.\n", + "2. Enter the dimensions of the grid (3 x 4 in this case), and click on `'Build a GridMDP'`\n", + "3. Click on `Initialize` in the `Edit` menu.\n", + "4. Set the reward as -0.4 and click `Apply`. Exit the dialog. \n", + "![title](images/ge0.jpg)\n", + "
    \n", + "5. Select cell (1, 1) and check the `Wall` radio button. `Apply` and exit the dialog.\n", + "![title](images/ge1.jpg)\n", + "
    \n", + "6. Select cells (4, 1) and (4, 2) and check the `Terminal` radio button for both. Set the rewards appropriately and click on `Apply`. Exit the dialog. Your window should look something like this.\n", + "![title](images/ge2.jpg)\n", + "
    \n", + "7. You are all set up now. Click on `Build and Run` in the `Build` menu and watch the heatmap calculate the utility function.\n", + "![title](images/ge4.jpg)\n", + "
    \n", + "Green shades indicate positive utilities and brown shades indicate negative utilities. \n", + "The values of the utility function and arrow diagram will pop up in separate dialogs after the algorithm converges." ] } ], @@ -485,7 +2988,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.4.3" + "version": "3.6.4" }, "widgets": { "state": { @@ -2976,5 +5479,5 @@ } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/mdp.py b/mdp.py index 8b0714da9..1003e26b5 100644 --- a/mdp.py +++ b/mdp.py @@ -1,154 +1,250 @@ -"""Markov Decision Processes (Chapter 17) +""" +Markov Decision Processes (Chapter 17) First we define an MDP, and the special case of a GridMDP, in which -states are laid out in a 2-dimensional grid. We also represent a policy -as a dictionary of {state:action} pairs, and a Utility function as a -dictionary of {state:number} pairs. We then define the value_iteration -and policy_iteration algorithms.""" - -from utils import argmax, vector_add, print_table -from grid import orientations, turn_right, turn_left +states are laid out in a 2-dimensional grid. We also represent a policy +as a dictionary of {state: action} pairs, and a Utility function as a +dictionary of {state: number} pairs. We then define the value_iteration +and policy_iteration algorithms. +""" import random +from collections import defaultdict +import numpy as np + +from utils import vector_add, orientations, turn_right, turn_left -class MDP: +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 - the text. Instead of P(s' | s, a) being a probability number for each + the text. Instead of P(s' | s, a) being a probability number for each state/state/action triplet, we instead have T(s, a) return a - list of (p, s') pairs. We also keep track of the possible states, - terminal states, and actions for each state. [page 646]""" + list of (p, s') pairs. We also keep track of the possible states, + terminal states, and actions for each state. [Page 646]""" + + def __init__(self, init, actlist, terminals, transitions=None, reward=None, states=None, gamma=0.9): + if not (0 < gamma <= 1): + raise ValueError("An MDP must have 0 < gamma <= 1") + + # collect states from transitions table if not passed. + self.states = states or self.get_states_from_transitions(transitions) - def __init__(self, init, actlist, terminals, gamma=.9): self.init = init - self.actlist = actlist + + if isinstance(actlist, list): + # if actlist is a list, all states have the same actions + self.actlist = actlist + + elif isinstance(actlist, dict): + # if actlist is a dict, different actions for each state + self.actlist = actlist + self.terminals = terminals - if not (0 <= gamma < 1): - raise ValueError("An MDP must have 0 <= gamma < 1") + self.transitions = transitions or {} + if not self.transitions: + print("Warning: Transition table is empty.") + self.gamma = gamma - self.states = set() - self.reward = {} + + self.reward = reward or {s: 0 for s in self.states} + + # self.check_consistency() def R(self, state): - "Return a numeric reward for this state." + """Return a numeric reward for this state.""" + return self.reward[state] def T(self, state, action): - """Transition model. From a state and an action, return a list + """Transition model. From a state and an action, return a list of (probability, result-state) pairs.""" - raise NotImplementedError + + if not self.transitions: + raise ValueError("Transition model is missing") + else: + return self.transitions[state][action] def actions(self, state): - """Set of actions that can be performed in this state. By default, a + """Return a list of actions that can be performed in this state. By default, a fixed list of actions, except for terminal states. Override this method if you need to specialize by state.""" + if state in self.terminals: return [None] else: return self.actlist + def get_states_from_transitions(self, transitions): + if isinstance(transitions, dict): + s1 = set(transitions.keys()) + s2 = set(tr[1] for actions in transitions.values() + for effects in actions.values() + for tr in effects) + return s1.union(s2) + else: + print('Could not retrieve states from transitions') + return None -class GridMDP(MDP): + def check_consistency(self): + + # check that all states in transitions are valid + assert set(self.states) == self.get_states_from_transitions(self.transitions) + + # check that init is a valid state + assert self.init in self.states + + # check reward for each state + assert set(self.reward.keys()) == set(self.states) + + # check that all terminals are valid states + assert all(t in self.states for t in self.terminals) + + # check that probability distributions for all actions sum to 1 + for s1, actions in self.transitions.items(): + for a in actions.keys(): + s = 0 + for o in actions[a]: + s += o[0] + assert abs(s - 1) < 0.001 + + +class MDP2(MDP): + """ + Inherits from MDP. Handles terminal states, and transitions to and from terminal states better. + """ + + def __init__(self, init, actlist, terminals, transitions, reward=None, gamma=0.9): + MDP.__init__(self, init, actlist, terminals, transitions, reward, gamma=gamma) - """A two-dimensional grid MDP, as in [Figure 17.1]. All you have to do is + def T(self, state, action): + if action is None: + return [(0.0, state)] + else: + return self.transitions[state][action] + + +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. + (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 - MDP.__init__(self, init, actlist=orientations, - terminals=terminals, gamma=gamma) - self.grid = grid + reward = {} + states = set() self.rows = len(grid) self.cols = len(grid[0]) + self.grid = grid for x in range(self.cols): for y in range(self.rows): - self.reward[x, y] = grid[y][x] - if grid[y][x] is not None: - self.states.add((x, y)) - - def T(self, state, action): - if action is None: - return [(0.0, state)] - else: + if grid[y][x]: + states.add((x, y)) + reward[(x, y)] = grid[y][x] + self.states = states + actlist = orientations + transitions = {} + for s in states: + transitions[s] = {} + for a in actlist: + transitions[s][a] = self.calculate_T(s, a) + MDP.__init__(self, init, actlist=actlist, + terminals=terminals, transitions=transitions, + reward=reward, states=states, gamma=gamma) + + def calculate_T(self, state, action): + if action: return [(0.8, self.go(state, action)), (0.1, self.go(state, turn_right(action))), (0.1, self.go(state, turn_left(action)))] + else: + return [(0.0, state)] + + def T(self, state, action): + return self.transitions[state][action] if action else [(0.0, state)] def go(self, state, direction): - "Return the state that results from going in this direction." + """Return the state that results from going in this direction.""" + state1 = vector_add(state, direction) return state1 if state1 in self.states else state 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) 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: '.'} + chars = {(1, 0): '>', (0, 1): '^', (-1, 0): '<', (0, -1): 'v', None: '.'} return self.to_grid({s: chars[a] for (s, a) in policy.items()}) + # ______________________________________________________________________________ + """ [Figure 17.1] A 4x3 grid environment that presents the agent with a sequential decision problem. """ sequential_decision_environment = GridMDP([[-0.04, -0.04, -0.04, +1], - [-0.04, None, -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. [Figure 17.4]" + """Solving an MDP by value iteration. [Figure 17.4]""" + U1 = {s: 0 for s in mdp.states} R, T, gamma = mdp.R, mdp.T, mdp.gamma while True: U = U1.copy() delta = 0 for s in mdp.states: - U1[s] = R(s) + gamma * max([sum([p * U[s1] for (p, s1) in T(s, a)]) - for a in mdp.actions(s)]) + U1[s] = R(s) + gamma * max(sum(p * U[s1] for (p, s1) in T(s, a)) + for a in mdp.actions(s)) delta = max(delta, abs(U1[s] - U[s])) - if delta < epsilon * (1 - gamma) / gamma: + if delta <= epsilon * (1 - gamma) / gamma: 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)""" + as a mapping from state to action. [Equation 17.4]""" + pi = {} for s in mdp.states: - pi[s] = argmax(mdp.actions(s), key=lambda a: expected_utility(a, s, U, mdp)) + pi[s] = max(mdp.actions(s), key=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)]) + """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 [Figure 17.7]" + """Solve an MDP by policy iteration [Figure 17.7]""" + U = {s: 0 for s in mdp.states} pi = {s: random.choice(mdp.actions(s)) for s in mdp.states} while True: U = policy_evaluation(pi, U, mdp) unchanged = True for s in mdp.states: - a = argmax(mdp.actions(s), key=lambda a: expected_utility(a, s, U, mdp)) + a = max(mdp.actions(s), key=lambda a: expected_utility(a, s, U, mdp)) if a != pi[s]: pi[s] = a unchanged = False @@ -159,18 +255,215 @@ def policy_iteration(mdp): 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).""" + R, T, gamma = mdp.R, mdp.T, mdp.gamma for i in range(k): for s in mdp.states: - U[s] = R(s) + gamma * sum([p * U[s1] for (p, s1) in T(s, pi[s])]) + U[s] = R(s) + gamma * sum(p * U[s1] for (p, s1) in T(s, pi[s])) return U + +class POMDP(MDP): + """A Partially Observable Markov Decision Process, defined by + a transition model P(s'|s,a), actions A(s), a reward function R(s), + and a sensor model P(e|s). We also keep track of a gamma value, + for use by algorithms. The transition and the sensor models + are defined as matrices. We also keep track of the possible states + and actions for each state. [Page 659].""" + + def __init__(self, actions, transitions=None, evidences=None, rewards=None, states=None, gamma=0.95): + """Initialize variables of the pomdp""" + + if not (0 < gamma <= 1): + raise ValueError('A POMDP must have 0 < gamma <= 1') + + self.states = states + self.actions = actions + + # transition model cannot be undefined + self.t_prob = transitions or {} + if not self.t_prob: + print('Warning: Transition model is undefined') + + # sensor model cannot be undefined + self.e_prob = evidences or {} + if not self.e_prob: + print('Warning: Sensor model is undefined') + + self.gamma = gamma + self.rewards = rewards + + def remove_dominated_plans(self, input_values): + """ + Remove dominated plans. + This method finds all the lines contributing to the + upper surface and removes those which don't. + """ + + values = [val for action in input_values for val in input_values[action]] + values.sort(key=lambda x: x[0], reverse=True) + + best = [values[0]] + y1_max = max(val[1] for val in values) + tgt = values[0] + prev_b = 0 + prev_ix = 0 + while tgt[1] != y1_max: + min_b = 1 + min_ix = 0 + for i in range(prev_ix + 1, len(values)): + if values[i][0] - tgt[0] + tgt[1] - values[i][1] != 0: + trans_b = (values[i][0] - tgt[0]) / (values[i][0] - tgt[0] + tgt[1] - values[i][1]) + if 0 <= trans_b <= 1 and trans_b > prev_b and trans_b < min_b: + min_b = trans_b + min_ix = i + prev_b = min_b + prev_ix = min_ix + tgt = values[min_ix] + best.append(tgt) + + return self.generate_mapping(best, input_values) + + def remove_dominated_plans_fast(self, input_values): + """ + Remove dominated plans using approximations. + Resamples the upper boundary at intervals of 100 and + finds the maximum values at these points. + """ + + values = [val for action in input_values for val in input_values[action]] + values.sort(key=lambda x: x[0], reverse=True) + + best = [] + sr = 100 + for i in range(sr + 1): + x = i / float(sr) + maximum = (values[0][1] - values[0][0]) * x + values[0][0] + tgt = values[0] + for value in values: + val = (value[1] - value[0]) * x + value[0] + if val > maximum: + maximum = val + tgt = value + + if all(any(tgt != v) for v in best): + best.append(np.array(tgt)) + + return self.generate_mapping(best, input_values) + + def generate_mapping(self, best, input_values): + """Generate mappings after removing dominated plans""" + + mapping = defaultdict(list) + for value in best: + for action in input_values: + if any(all(value == v) for v in input_values[action]): + mapping[action].append(value) + + return mapping + + def max_difference(self, U1, U2): + """Find maximum difference between two utility mappings""" + + for k, v in U1.items(): + sum1 = 0 + for element in U1[k]: + sum1 += sum(element) + sum2 = 0 + for element in U2[k]: + sum2 += sum(element) + return abs(sum1 - sum2) + + +class Matrix: + """Matrix operations class""" + + @staticmethod + def add(A, B): + """Add two matrices A and B""" + + res = [] + for i in range(len(A)): + row = [] + for j in range(len(A[0])): + row.append(A[i][j] + B[i][j]) + res.append(row) + return res + + @staticmethod + def scalar_multiply(a, B): + """Multiply scalar a to matrix B""" + + for i in range(len(B)): + for j in range(len(B[0])): + B[i][j] = a * B[i][j] + return B + + @staticmethod + def multiply(A, B): + """Multiply two matrices A and B element-wise""" + + matrix = [] + for i in range(len(B)): + row = [] + for j in range(len(B[0])): + row.append(B[i][j] * A[j][i]) + matrix.append(row) + + return matrix + + @staticmethod + def matmul(A, B): + """Inner-product of two matrices""" + + return [[sum(ele_a * ele_b for ele_a, ele_b in zip(row_a, col_b)) for col_b in list(zip(*B))] for row_a in A] + + @staticmethod + def transpose(A): + """Transpose a matrix""" + + return [list(i) for i in zip(*A)] + + +def pomdp_value_iteration(pomdp, epsilon=0.1): + """Solving a POMDP by value iteration.""" + + U = {'': [[0] * len(pomdp.states)]} + count = 0 + while True: + count += 1 + prev_U = U + values = [val for action in U for val in U[action]] + value_matxs = [] + for i in values: + for j in values: + value_matxs.append([i, j]) + + U1 = defaultdict(list) + for action in pomdp.actions: + for u in value_matxs: + u1 = Matrix.matmul(Matrix.matmul(pomdp.t_prob[int(action)], + Matrix.multiply(pomdp.e_prob[int(action)], Matrix.transpose(u))), + [[1], [1]]) + u1 = Matrix.add(Matrix.scalar_multiply(pomdp.gamma, Matrix.transpose(u1)), [pomdp.rewards[int(action)]]) + U1[action].append(u1[0]) + + U = pomdp.remove_dominated_plans_fast(U1) + # replace with U = pomdp.remove_dominated_plans(U1) for accurate calculations + + if count > 10: + if pomdp.max_difference(U, prev_U) < epsilon * (1 - pomdp.gamma) / pomdp.gamma: + return U + + __doc__ += """ >>> pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .01)) >>> sequential_decision_environment.to_arrows(pi) [['>', '>', '>', '.'], ['^', None, '^', '.'], ['^', '>', '^', '<']] +>>> from utils import print_table + >>> print_table(sequential_decision_environment.to_arrows(pi)) > > > . ^ None ^ . @@ -180,4 +473,20 @@ def policy_evaluation(pi, U, mdp, k=20): > > > . ^ None ^ . ^ > ^ < +""" # noqa + +""" +s = { 'a' : { 'plan1' : [(0.2, 'a'), (0.3, 'b'), (0.3, 'c'), (0.2, 'd')], + 'plan2' : [(0.4, 'a'), (0.15, 'b'), (0.45, 'c')], + 'plan3' : [(0.2, 'a'), (0.5, 'b'), (0.3, 'c')], + }, + 'b' : { 'plan1' : [(0.2, 'a'), (0.6, 'b'), (0.2, 'c'), (0.1, 'd')], + 'plan2' : [(0.6, 'a'), (0.2, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan3' : [(0.3, 'a'), (0.3, 'b'), (0.4, 'c')], + }, + 'c' : { 'plan1' : [(0.3, 'a'), (0.5, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan2' : [(0.5, 'a'), (0.3, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan3' : [(0.1, 'a'), (0.3, 'b'), (0.1, 'c'), (0.5, 'd')], + }, + } """ diff --git a/mdp4e.py b/mdp4e.py new file mode 100644 index 000000000..f8871bdc9 --- /dev/null +++ b/mdp4e.py @@ -0,0 +1,516 @@ +""" +Markov Decision Processes (Chapter 16) + +First we define an MDP, and the special case of a GridMDP, in which +states are laid out in a 2-dimensional grid. We also represent a policy +as a dictionary of {state: action} pairs, and a Utility function as a +dictionary of {state: number} pairs. We then define the value_iteration +and policy_iteration algorithms. +""" + +import random +from collections import defaultdict + +import numpy as np + +from utils4e import vector_add, orientations, turn_right, turn_left + + +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 + the text. Instead of P(s' | s, a) being a probability number for each + state/state/action triplet, we instead have T(s, a) return a + list of (p, s') pairs. We also keep track of the possible states, + terminal states, and actions for each state. [Page 646]""" + + def __init__(self, init, actlist, terminals, transitions=None, reward=None, states=None, gamma=0.9): + if not (0 < gamma <= 1): + raise ValueError("An MDP must have 0 < gamma <= 1") + + # collect states from transitions table if not passed. + self.states = states or self.get_states_from_transitions(transitions) + + self.init = init + + if isinstance(actlist, list): + # if actlist is a list, all states have the same actions + self.actlist = actlist + + elif isinstance(actlist, dict): + # if actlist is a dict, different actions for each state + self.actlist = actlist + + self.terminals = terminals + self.transitions = transitions or {} + if not self.transitions: + print("Warning: Transition table is empty.") + + self.gamma = gamma + + self.reward = reward or {s: 0 for s in self.states} + + # self.check_consistency() + + def R(self, state): + """Return a numeric reward for this state.""" + + return self.reward[state] + + def T(self, state, action): + """Transition model. From a state and an action, return a list + of (probability, result-state) pairs.""" + + if not self.transitions: + raise ValueError("Transition model is missing") + else: + return self.transitions[state][action] + + def actions(self, state): + """Return a list of actions that can be performed in this state. By default, a + fixed list of actions, except for terminal states. Override this + method if you need to specialize by state.""" + + if state in self.terminals: + return [None] + else: + return self.actlist + + def get_states_from_transitions(self, transitions): + if isinstance(transitions, dict): + s1 = set(transitions.keys()) + s2 = set(tr[1] for actions in transitions.values() + for effects in actions.values() + for tr in effects) + return s1.union(s2) + else: + print('Could not retrieve states from transitions') + return None + + def check_consistency(self): + + # check that all states in transitions are valid + assert set(self.states) == self.get_states_from_transitions(self.transitions) + + # check that init is a valid state + assert self.init in self.states + + # check reward for each state + assert set(self.reward.keys()) == set(self.states) + + # check that all terminals are valid states + assert all(t in self.states for t in self.terminals) + + # check that probability distributions for all actions sum to 1 + for s1, actions in self.transitions.items(): + for a in actions.keys(): + s = 0 + for o in actions[a]: + s += o[0] + assert abs(s - 1) < 0.001 + + +class MDP2(MDP): + """ + Inherits from MDP. Handles terminal states, and transitions to and from terminal states better. + """ + + def __init__(self, init, actlist, terminals, transitions, reward=None, gamma=0.9): + MDP.__init__(self, init, actlist, terminals, transitions, reward, gamma=gamma) + + def T(self, state, action): + if action is None: + return [(0.0, state)] + else: + return self.transitions[state][action] + + +class GridMDP(MDP): + """A two-dimensional grid MDP, as in [Figure 16.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 + reward = {} + states = set() + self.rows = len(grid) + self.cols = len(grid[0]) + self.grid = grid + for x in range(self.cols): + for y in range(self.rows): + if grid[y][x]: + states.add((x, y)) + reward[(x, y)] = grid[y][x] + self.states = states + actlist = orientations + transitions = {} + for s in states: + transitions[s] = {} + for a in actlist: + transitions[s][a] = self.calculate_T(s, a) + MDP.__init__(self, init, actlist=actlist, + terminals=terminals, transitions=transitions, + reward=reward, states=states, gamma=gamma) + + def calculate_T(self, state, action): + if action: + return [(0.8, self.go(state, action)), + (0.1, self.go(state, turn_right(action))), + (0.1, self.go(state, turn_left(action)))] + else: + return [(0.0, state)] + + def T(self, state, action): + return self.transitions[state][action] if action else [(0.0, state)] + + def go(self, state, direction): + """Return the state that results from going in this direction.""" + + state1 = tuple(vector_add(state, direction)) + return state1 if state1 in self.states else state + + 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) + 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({s: chars[a] for (s, a) in policy.items()}) + + +# ______________________________________________________________________________ + + +""" [Figure 16.1] +A 4x3 grid environment that presents the agent with a sequential decision problem. +""" + +sequential_decision_environment = 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)]) + + +# ______________________________________________________________________________ +# 16.1.3 The Bellman equation for utilities + + +def q_value(mdp, s, a, U): + if not a: + return mdp.R(s) + res = 0 + for p, s_prime in mdp.T(s, a): + res += p * (mdp.R(s) + mdp.gamma * U[s_prime]) + return res + + +# TODO: DDN in figure 16.4 and 16.5 + +# ______________________________________________________________________________ +# 16.2 Algorithms for MDPs +# 16.2.1 Value Iteration + + +def value_iteration(mdp, epsilon=0.001): + """Solving an MDP by value iteration. [Figure 16.6]""" + + U1 = {s: 0 for s in mdp.states} + R, T, gamma = mdp.R, mdp.T, mdp.gamma + while True: + U = U1.copy() + delta = 0 + for s in mdp.states: + # U1[s] = R(s) + gamma * max(sum(p * U[s1] for (p, s1) in T(s, a)) + # for a in mdp.actions(s)) + U1[s] = max(q_value(mdp, s, a, U) for a in mdp.actions(s)) + delta = max(delta, abs(U1[s] - U[s])) + if delta <= epsilon * (1 - gamma) / gamma: + return U + + +# ______________________________________________________________________________ +# 16.2.2 Policy Iteration + + +def best_policy(mdp, U): + """Given an MDP and a utility function U, determine the best policy, + as a mapping from state to action.""" + + pi = {} + for s in mdp.states: + pi[s] = max(mdp.actions(s), key=lambda a: q_value(mdp, s, a, U)) + 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 [Figure 17.7]""" + + U = {s: 0 for s in mdp.states} + pi = {s: random.choice(mdp.actions(s)) for s in mdp.states} + while True: + U = policy_evaluation(pi, U, mdp) + unchanged = True + for s in mdp.states: + a_star = max(mdp.actions(s), key=lambda a: q_value(mdp, s, a, U)) + # a = max(mdp.actions(s), key=lambda a: expected_utility(a, s, U, mdp)) + if q_value(mdp, s, a_star, U) > q_value(mdp, s, pi[s], U): + pi[s] = a_star + 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).""" + + R, T, gamma = mdp.R, mdp.T, mdp.gamma + for i in range(k): + for s in mdp.states: + U[s] = R(s) + gamma * sum(p * U[s1] for (p, s1) in T(s, pi[s])) + return U + + +# ___________________________________________________________________ +# 16.4 Partially Observed MDPs + + +class POMDP(MDP): + """A Partially Observable Markov Decision Process, defined by + a transition model P(s'|s,a), actions A(s), a reward function R(s), + and a sensor model P(e|s). We also keep track of a gamma value, + for use by algorithms. The transition and the sensor models + are defined as matrices. We also keep track of the possible states + and actions for each state. [Page 659].""" + + def __init__(self, actions, transitions=None, evidences=None, rewards=None, states=None, gamma=0.95): + """Initialize variables of the pomdp""" + + if not (0 < gamma <= 1): + raise ValueError('A POMDP must have 0 < gamma <= 1') + + self.states = states + self.actions = actions + + # transition model cannot be undefined + self.t_prob = transitions or {} + if not self.t_prob: + print('Warning: Transition model is undefined') + + # sensor model cannot be undefined + self.e_prob = evidences or {} + if not self.e_prob: + print('Warning: Sensor model is undefined') + + self.gamma = gamma + self.rewards = rewards + + def remove_dominated_plans(self, input_values): + """ + Remove dominated plans. + This method finds all the lines contributing to the + upper surface and removes those which don't. + """ + + values = [val for action in input_values for val in input_values[action]] + values.sort(key=lambda x: x[0], reverse=True) + + best = [values[0]] + y1_max = max(val[1] for val in values) + tgt = values[0] + prev_b = 0 + prev_ix = 0 + while tgt[1] != y1_max: + min_b = 1 + min_ix = 0 + for i in range(prev_ix + 1, len(values)): + if values[i][0] - tgt[0] + tgt[1] - values[i][1] != 0: + trans_b = (values[i][0] - tgt[0]) / (values[i][0] - tgt[0] + tgt[1] - values[i][1]) + if 0 <= trans_b <= 1 and trans_b > prev_b and trans_b < min_b: + min_b = trans_b + min_ix = i + prev_b = min_b + prev_ix = min_ix + tgt = values[min_ix] + best.append(tgt) + + return self.generate_mapping(best, input_values) + + def remove_dominated_plans_fast(self, input_values): + """ + Remove dominated plans using approximations. + Resamples the upper boundary at intervals of 100 and + finds the maximum values at these points. + """ + + values = [val for action in input_values for val in input_values[action]] + values.sort(key=lambda x: x[0], reverse=True) + + best = [] + sr = 100 + for i in range(sr + 1): + x = i / float(sr) + maximum = (values[0][1] - values[0][0]) * x + values[0][0] + tgt = values[0] + for value in values: + val = (value[1] - value[0]) * x + value[0] + if val > maximum: + maximum = val + tgt = value + + if all(any(tgt != v) for v in best): + best.append(np.array(tgt)) + + return self.generate_mapping(best, input_values) + + def generate_mapping(self, best, input_values): + """Generate mappings after removing dominated plans""" + + mapping = defaultdict(list) + for value in best: + for action in input_values: + if any(all(value == v) for v in input_values[action]): + mapping[action].append(value) + + return mapping + + def max_difference(self, U1, U2): + """Find maximum difference between two utility mappings""" + + for k, v in U1.items(): + sum1 = 0 + for element in U1[k]: + sum1 += sum(element) + sum2 = 0 + for element in U2[k]: + sum2 += sum(element) + return abs(sum1 - sum2) + + +class Matrix: + """Matrix operations class""" + + @staticmethod + def add(A, B): + """Add two matrices A and B""" + + res = [] + for i in range(len(A)): + row = [] + for j in range(len(A[0])): + row.append(A[i][j] + B[i][j]) + res.append(row) + return res + + @staticmethod + def scalar_multiply(a, B): + """Multiply scalar a to matrix B""" + + for i in range(len(B)): + for j in range(len(B[0])): + B[i][j] = a * B[i][j] + return B + + @staticmethod + def multiply(A, B): + """Multiply two matrices A and B element-wise""" + + matrix = [] + for i in range(len(B)): + row = [] + for j in range(len(B[0])): + row.append(B[i][j] * A[j][i]) + matrix.append(row) + + return matrix + + @staticmethod + def matmul(A, B): + """Inner-product of two matrices""" + + return [[sum(ele_a * ele_b for ele_a, ele_b in zip(row_a, col_b)) for col_b in list(zip(*B))] for row_a in A] + + @staticmethod + def transpose(A): + """Transpose a matrix""" + + return [list(i) for i in zip(*A)] + + +def pomdp_value_iteration(pomdp, epsilon=0.1): + """Solving a POMDP by value iteration.""" + + U = {'': [[0] * len(pomdp.states)]} + count = 0 + while True: + count += 1 + prev_U = U + values = [val for action in U for val in U[action]] + value_matxs = [] + for i in values: + for j in values: + value_matxs.append([i, j]) + + U1 = defaultdict(list) + for action in pomdp.actions: + for u in value_matxs: + u1 = Matrix.matmul(Matrix.matmul(pomdp.t_prob[int(action)], + Matrix.multiply(pomdp.e_prob[int(action)], Matrix.transpose(u))), + [[1], [1]]) + u1 = Matrix.add(Matrix.scalar_multiply(pomdp.gamma, Matrix.transpose(u1)), [pomdp.rewards[int(action)]]) + U1[action].append(u1[0]) + + U = pomdp.remove_dominated_plans_fast(U1) + # replace with U = pomdp.remove_dominated_plans(U1) for accurate calculations + + if count > 10: + if pomdp.max_difference(U, prev_U) < epsilon * (1 - pomdp.gamma) / pomdp.gamma: + return U + + +__doc__ += """ +>>> pi = best_policy(sequential_decision_environment, value_iteration(sequential_decision_environment, .01)) + +>>> sequential_decision_environment.to_arrows(pi) +[['>', '>', '>', '.'], ['^', None, '^', '.'], ['^', '>', '^', '<']] + +>>> from utils import print_table + +>>> print_table(sequential_decision_environment.to_arrows(pi)) +> > > . +^ None ^ . +^ > ^ < + +>>> print_table(sequential_decision_environment.to_arrows(policy_iteration(sequential_decision_environment))) +> > > . +^ None ^ . +^ > ^ < +""" # noqa + +""" +s = { 'a' : { 'plan1' : [(0.2, 'a'), (0.3, 'b'), (0.3, 'c'), (0.2, 'd')], + 'plan2' : [(0.4, 'a'), (0.15, 'b'), (0.45, 'c')], + 'plan3' : [(0.2, 'a'), (0.5, 'b'), (0.3, 'c')], + }, + 'b' : { 'plan1' : [(0.2, 'a'), (0.6, 'b'), (0.2, 'c'), (0.1, 'd')], + 'plan2' : [(0.6, 'a'), (0.2, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan3' : [(0.3, 'a'), (0.3, 'b'), (0.4, 'c')], + }, + 'c' : { 'plan1' : [(0.3, 'a'), (0.5, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan2' : [(0.5, 'a'), (0.3, 'b'), (0.1, 'c'), (0.1, 'd')], + 'plan3' : [(0.1, 'a'), (0.3, 'b'), (0.1, 'c'), (0.5, 'd')], + }, + } +""" diff --git a/mdp_apps.ipynb b/mdp_apps.ipynb new file mode 100644 index 000000000..da3ae7b06 --- /dev/null +++ b/mdp_apps.ipynb @@ -0,0 +1,1825 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# APPLICATIONS OF MARKOV DECISION PROCESSES\n", + "---\n", + "In this notebook we will take a look at some indicative applications of markov decision processes. \n", + "We will cover content from [`mdp.py`](https://github.com/aimacode/aima-python/blob/master/mdp.py), for **Chapter 17 Making Complex Decisions** of Stuart Russel's and Peter Norvig's book [*Artificial Intelligence: A Modern Approach*](http://aima.cs.berkeley.edu/).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from mdp import *\n", + "from notebook import psource, pseudocode" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "- Simple MDP\n", + " - State dependent reward function\n", + " - State and action dependent reward function\n", + " - State, action and next state dependent reward function\n", + "- Grid MDP\n", + " - Pathfinding problem\n", + "- POMDP\n", + " - Two state POMDP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SIMPLE MDP\n", + "---\n", + "### State dependent reward function\n", + "\n", + "Markov Decision Processes are formally described as processes that follow the Markov property which states that \"The future is independent of the past given the present\". \n", + "MDPs formally describe environments for reinforcement learning and we assume that the environment is *fully observable*. \n", + "Let us take a toy example MDP and solve it using the functions in `mdp.py`.\n", + "This is a simple example adapted from a [similar problem](http://www0.cs.ucl.ac.uk/staff/D.Silver/web/Teaching_files/MDP.pdf) by Dr. David Silver, tweaked to fit the limitations of the current functions.\n", + "![title](images/mdp-b.png)\n", + "\n", + "Let's say you're a student attending lectures in a university.\n", + "There are three lectures you need to attend on a given day.\n", + "
    \n", + "Attending the first lecture gives you 4 points of reward.\n", + "After the first lecture, you have a 0.6 probability to continue into the second one, yielding 6 more points of reward.\n", + "
    \n", + "But, with a probability of 0.4, you get distracted and start using Facebook instead and get a reward of -1.\n", + "From then onwards, you really can't let go of Facebook and there's just a 0.1 probability that you will concentrate back on the lecture.\n", + "
    \n", + "After the second lecture, you have an equal chance of attending the next lecture or just falling asleep.\n", + "Falling asleep is the terminal state and yields you no reward, but continuing on to the final lecture gives you a big reward of 10 points.\n", + "
    \n", + "From there on, you have a 40% chance of going to study and reach the terminal state, \n", + "but a 60% chance of going to the pub with your friends instead. \n", + "You end up drunk and don't know which lecture to attend, so you go to one of the lectures according to the probabilities given above.\n", + "
    \n", + "We now have an outline of our stochastic environment and we need to maximize our reward by solving this MDP.\n", + "
    \n", + "
    \n", + "We first have to define our Transition Matrix as a nested dictionary to fit the requirements of the MDP class." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t = {\n", + " 'leisure': {\n", + " 'facebook': {'leisure':0.9, 'class1':0.1},\n", + " 'quit': {'leisure':0.1, 'class1':0.9},\n", + " 'study': {},\n", + " 'sleep': {},\n", + " 'pub': {}\n", + " },\n", + " 'class1': {\n", + " 'study': {'class2':0.6, 'leisure':0.4},\n", + " 'facebook': {'class2':0.4, 'leisure':0.6},\n", + " 'quit': {},\n", + " 'sleep': {},\n", + " 'pub': {}\n", + " },\n", + " 'class2': {\n", + " 'study': {'class3':0.5, 'end':0.5},\n", + " 'sleep': {'end':0.5, 'class3':0.5},\n", + " 'facebook': {},\n", + " 'quit': {},\n", + " 'pub': {},\n", + " },\n", + " 'class3': {\n", + " 'study': {'end':0.6, 'class1':0.08, 'class2':0.16, 'class3':0.16},\n", + " 'pub': {'end':0.4, 'class1':0.12, 'class2':0.24, 'class3':0.24},\n", + " 'facebook': {},\n", + " 'quit': {},\n", + " 'sleep': {}\n", + " },\n", + " 'end': {}\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to define the reward for each state." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "rewards = {\n", + " 'class1': 4,\n", + " 'class2': 6,\n", + " 'class3': 10,\n", + " 'leisure': -1,\n", + " 'end': 0\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This MDP has only one terminal state." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "terminals = ['end']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now set the initial state to Class 1." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "init = 'class1'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will write a CustomMDP class to extend the MDP class for the problem at hand. \n", + "This class will implement the `T` method to implement the transition model. This is the exact same class as given in [`mdp.ipynb`](https://github.com/aimacode/aima-python/blob/master/mdp.ipynb#MDP)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class CustomMDP(MDP):\n", + "\n", + " def __init__(self, transition_matrix, rewards, terminals, init, gamma=.9):\n", + " # All possible actions.\n", + " actlist = []\n", + " for state in transition_matrix.keys():\n", + " actlist.extend(transition_matrix[state])\n", + " actlist = list(set(actlist))\n", + " print(actlist)\n", + "\n", + " MDP.__init__(self, init, actlist, terminals=terminals, gamma=gamma)\n", + " self.t = transition_matrix\n", + " self.reward = rewards\n", + " for state in self.t:\n", + " self.states.add(state)\n", + "\n", + " def T(self, state, action):\n", + " if action is None:\n", + " return [(0.0, state)]\n", + " else: \n", + " return [(prob, new_state) for new_state, prob in self.t[state][action].items()]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need an instance of this class." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['quit', 'sleep', 'study', 'pub', 'facebook']\n" + ] + } + ], + "source": [ + "mdp = CustomMDP(t, rewards, terminals, init, gamma=.9)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The utility of each state can be found by `value_iteration`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'class1': 16.90340650279542,\n", + " 'class2': 14.597383430869879,\n", + " 'class3': 19.10533144728953,\n", + " 'end': 0.0,\n", + " 'leisure': 13.946891353066082}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "value_iteration(mdp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we can compute the utility values, we can find the best policy." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "pi = best_policy(mdp, value_iteration(mdp, .01))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`pi` stores the best action for each state." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'class2': 'sleep', 'class3': 'pub', 'end': None, 'class1': 'study', 'leisure': 'quit'}\n" + ] + } + ], + "source": [ + "print(pi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can confirm that this is the best policy by verifying this result against `policy_iteration`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'class1': 'study',\n", + " 'class2': 'sleep',\n", + " 'class3': 'pub',\n", + " 'end': None,\n", + " 'leisure': 'quit'}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "policy_iteration(mdp)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "Everything looks perfect, but let us look at another possibility for an MDP.\n", + "
    \n", + "Till now we have only dealt with rewards that the agent gets while it is **on** a particular state.\n", + "What if we want to have different rewards for a state depending on the action that the agent takes next. \n", + "The agent gets the reward _during its transition_ to the next state.\n", + "
    \n", + "For the sake of clarity, we will call this the _transition reward_ and we will call this kind of MDP a _dynamic_ MDP. \n", + "This is not a conventional term, we just use it to minimize confusion between the two.\n", + "
    \n", + "This next section deals with how to create and solve a dynamic MDP." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### State and action dependent reward function\n", + "Let us consider a very similar problem, but this time, we do not have rewards _on_ states, \n", + "instead, we have rewards on the transitions between states. \n", + "This state diagram will make it clearer.\n", + "![title](images/mdp-c.png)\n", + "\n", + "A very similar scenario as the previous problem, but we have different rewards for the same state depending on the action taken.\n", + "
    \n", + "To deal with this, we just need to change the `R` method of the `MDP` class, but to prevent confusion, we will write a new similar class `DMDP`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class DMDP:\n", + "\n", + " \"\"\"A Markov Decision Process, defined by an initial state, transition model,\n", + " and reward model. We also keep track of a gamma value, for use by\n", + " algorithms. The transition model is represented somewhat differently from\n", + " the text. Instead of P(s' | s, a) being a probability number for each\n", + " state/state/action triplet, we instead have T(s, a) return a\n", + " list of (p, s') pairs. The reward function is very similar.\n", + " We also keep track of the possible states,\n", + " terminal states, and actions for each state.\"\"\"\n", + "\n", + " def __init__(self, init, actlist, terminals, transitions={}, rewards={}, states=None, gamma=.9):\n", + " if not (0 < gamma <= 1):\n", + " raise ValueError(\"An MDP must have 0 < gamma <= 1\")\n", + "\n", + " if states:\n", + " self.states = states\n", + " else:\n", + " self.states = set()\n", + " self.init = init\n", + " self.actlist = actlist\n", + " self.terminals = terminals\n", + " self.transitions = transitions\n", + " self.rewards = rewards\n", + " self.gamma = gamma\n", + "\n", + " def R(self, state, action):\n", + " \"\"\"Return a numeric reward for this state and this action.\"\"\"\n", + " if (self.rewards == {}):\n", + " raise ValueError('Reward model is missing')\n", + " else:\n", + " return self.rewards[state][action]\n", + "\n", + " def T(self, state, action):\n", + " \"\"\"Transition model. From a state and an action, return a list\n", + " of (probability, result-state) pairs.\"\"\"\n", + " if(self.transitions == {}):\n", + " raise ValueError(\"Transition model is missing\")\n", + " else:\n", + " return self.transitions[state][action]\n", + "\n", + " def actions(self, state):\n", + " \"\"\"Set of actions that can be performed in this state. By default, a\n", + " fixed list of actions, except for terminal states. Override this\n", + " method if you need to specialize by state.\"\"\"\n", + " if state in self.terminals:\n", + " return [None]\n", + " else:\n", + " return self.actlist" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The transition model will be the same" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t = {\n", + " 'leisure': {\n", + " 'facebook': {'leisure':0.9, 'class1':0.1},\n", + " 'quit': {'leisure':0.1, 'class1':0.9},\n", + " 'study': {},\n", + " 'sleep': {},\n", + " 'pub': {}\n", + " },\n", + " 'class1': {\n", + " 'study': {'class2':0.6, 'leisure':0.4},\n", + " 'facebook': {'class2':0.4, 'leisure':0.6},\n", + " 'quit': {},\n", + " 'sleep': {},\n", + " 'pub': {}\n", + " },\n", + " 'class2': {\n", + " 'study': {'class3':0.5, 'end':0.5},\n", + " 'sleep': {'end':0.5, 'class3':0.5},\n", + " 'facebook': {},\n", + " 'quit': {},\n", + " 'pub': {},\n", + " },\n", + " 'class3': {\n", + " 'study': {'end':0.6, 'class1':0.08, 'class2':0.16, 'class3':0.16},\n", + " 'pub': {'end':0.4, 'class1':0.12, 'class2':0.24, 'class3':0.24},\n", + " 'facebook': {},\n", + " 'quit': {},\n", + " 'sleep': {}\n", + " },\n", + " 'end': {}\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The reward model will be a dictionary very similar to the transition dictionary with a reward for every action for every state." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "r = {\n", + " 'leisure': {\n", + " 'facebook':-1,\n", + " 'quit':0,\n", + " 'study':0,\n", + " 'sleep':0,\n", + " 'pub':0\n", + " },\n", + " 'class1': {\n", + " 'study':-2,\n", + " 'facebook':-1,\n", + " 'quit':0,\n", + " 'sleep':0,\n", + " 'pub':0\n", + " },\n", + " 'class2': {\n", + " 'study':-2,\n", + " 'sleep':0,\n", + " 'facebook':0,\n", + " 'quit':0,\n", + " 'pub':0\n", + " },\n", + " 'class3': {\n", + " 'study':10,\n", + " 'pub':1,\n", + " 'facebook':0,\n", + " 'quit':0,\n", + " 'sleep':0\n", + " },\n", + " 'end': {\n", + " 'study':0,\n", + " 'pub':0,\n", + " 'facebook':0,\n", + " 'quit':0,\n", + " 'sleep':0\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The MDP has only one terminal state" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "terminals = ['end']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now set the initial state to Class 1." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "init = 'class1'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will write a CustomDMDP class to extend the DMDP class for the problem at hand.\n", + "This class will implement everything that the previous CustomMDP class implements along with a new reward model." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class CustomDMDP(DMDP):\n", + " \n", + " def __init__(self, transition_matrix, rewards, terminals, init, gamma=.9):\n", + " actlist = []\n", + " for state in transition_matrix.keys():\n", + " actlist.extend(transition_matrix[state])\n", + " actlist = list(set(actlist))\n", + " print(actlist)\n", + " \n", + " DMDP.__init__(self, init, actlist, terminals=terminals, gamma=gamma)\n", + " self.t = transition_matrix\n", + " self.rewards = rewards\n", + " for state in self.t:\n", + " self.states.add(state)\n", + " \n", + " \n", + " def T(self, state, action):\n", + " if action is None:\n", + " return [(0.0, state)]\n", + " else:\n", + " return [(prob, new_state) for new_state, prob in self.t[state][action].items()]\n", + " \n", + " def R(self, state, action):\n", + " if action is None:\n", + " return 0\n", + " else:\n", + " return self.rewards[state][action]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One thing we haven't thought about yet is that the `value_iteration` algorithm won't work now that the reward model is changed.\n", + "It will be quite similar to the one we currently have nonetheless." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Bellman update equation now is defined as follows\n", + "\n", + "$$U(s)=\\max_{a\\epsilon A(s)}\\bigg[R(s, a) + \\gamma\\sum_{s'}P(s'\\ |\\ s,a)U(s')\\bigg]$$\n", + "\n", + "It is not difficult to see that the update equation we have been using till now is just a special case of this more generalized equation. \n", + "We also need to max over the reward function now as the reward function is action dependent as well.\n", + "
    \n", + "We will use this to write a function to carry out value iteration, very similar to the one we are familiar with." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def value_iteration_dmdp(dmdp, epsilon=0.001):\n", + " U1 = {s: 0 for s in dmdp.states}\n", + " R, T, gamma = dmdp.R, dmdp.T, dmdp.gamma\n", + " while True:\n", + " U = U1.copy()\n", + " delta = 0\n", + " for s in dmdp.states:\n", + " U1[s] = max([(R(s, a) + gamma*sum([(p*U[s1]) for (p, s1) in T(s, a)])) for a in dmdp.actions(s)])\n", + " delta = max(delta, abs(U1[s] - U[s]))\n", + " if delta < epsilon * (1 - gamma) / gamma:\n", + " return U" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're all set.\n", + "Let's instantiate our class." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['quit', 'sleep', 'study', 'pub', 'facebook']\n" + ] + } + ], + "source": [ + "dmdp = CustomDMDP(t, r, terminals, init, gamma=.9)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculate utility values by calling `value_iteration_dmdp`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'class1': 2.0756895004431364,\n", + " 'class2': 5.772550326127298,\n", + " 'class3': 12.827904448229472,\n", + " 'end': 0.0,\n", + " 'leisure': 1.8474896554396596}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "value_iteration_dmdp(dmdp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are the expected utility values for our new MDP.\n", + "
    \n", + "As you might have guessed, we cannot use the old `best_policy` function to find the best policy.\n", + "So we will write our own.\n", + "But, before that we need a helper function to calculate the expected utility value given a state and an action." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def expected_utility_dmdp(a, s, U, dmdp):\n", + " return dmdp.R(s, a) + dmdp.gamma*sum([(p*U[s1]) for (p, s1) in dmdp.T(s, a)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we write our modified `best_policy` function." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from utils import argmax\n", + "def best_policy_dmdp(dmdp, U):\n", + " pi = {}\n", + " for s in dmdp.states:\n", + " pi[s] = argmax(dmdp.actions(s), key=lambda a: expected_utility_dmdp(a, s, U, dmdp))\n", + " return pi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Find the best policy." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'class2': 'sleep', 'class3': 'study', 'end': None, 'class1': 'facebook', 'leisure': 'quit'}\n" + ] + } + ], + "source": [ + "pi = best_policy_dmdp(dmdp, value_iteration_dmdp(dmdp, .01))\n", + "print(pi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From this, we can infer that `value_iteration_dmdp` tries to minimize the negative reward. \n", + "Since we don't have rewards for states now, the algorithm takes the action that would try to avoid getting negative rewards and take the lesser of two evils if all rewards are negative.\n", + "You might also want to have state rewards alongside transition rewards. \n", + "Perhaps you can do that yourself now that the difficult part has been done.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### State, action and next-state dependent reward function\n", + "\n", + "For truly stochastic environments, \n", + "we have noticed that taking an action from a particular state doesn't always do what we want it to. \n", + "Instead, for every action taken from a particular state, \n", + "it might be possible to reach a different state each time depending on the transition probabilities. \n", + "What if we want different rewards for each state, action and next-state triplet? \n", + "Mathematically, we now want a reward function of the form R(s, a, s') for our MDP. \n", + "This section shows how we can tweak the MDP class to achieve this.\n", + "
    \n", + "\n", + "Let's now take a different problem statement. \n", + "The one we are working with is a bit too simple.\n", + "Consider a taxi that serves three adjacent towns A, B, and C.\n", + "Each time the taxi discharges a passenger, the driver must choose from three possible actions:\n", + "1. Cruise the streets looking for a passenger.\n", + "2. Go to the nearest taxi stand.\n", + "3. Wait for a radio call from the dispatcher with instructions.\n", + "
    \n", + "Subject to the constraint that the taxi driver cannot do the third action in town B because of distance and poor reception.\n", + "\n", + "Let's model our MDP.\n", + "
    \n", + "The MDP has three states, namely A, B and C.\n", + "
    \n", + "It has three actions, namely 1, 2 and 3.\n", + "
    \n", + "Action sets:\n", + "
    \n", + "$K_{a}$ = {1, 2, 3}\n", + "
    \n", + "$K_{b}$ = {1, 2}\n", + "
    \n", + "$K_{c}$ = {1, 2, 3}\n", + "
    \n", + "\n", + "We have the following transition probability matrices:\n", + "
    \n", + "
    \n", + "Action 1: Cruising streets \n", + "
    \n", + "$\\\\\n", + " P^{1} = \n", + " \\left[ {\\begin{array}{ccc}\n", + " \\frac{1}{2} & \\frac{1}{4} & \\frac{1}{4} \\\\\n", + " \\frac{1}{2} & 0 & \\frac{1}{2} \\\\\n", + " \\frac{1}{4} & \\frac{1}{4} & \\frac{1}{2} \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "Action 2: Waiting at the taxi stand \n", + "
    \n", + "$\\\\\n", + " P^{2} = \n", + " \\left[ {\\begin{array}{ccc}\n", + " \\frac{1}{16} & \\frac{3}{4} & \\frac{3}{16} \\\\\n", + " \\frac{1}{16} & \\frac{7}{8} & \\frac{1}{16} \\\\\n", + " \\frac{1}{8} & \\frac{3}{4} & \\frac{1}{8} \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "Action 3: Waiting for dispatch \n", + "
    \n", + "$\\\\\n", + " P^{3} =\n", + " \\left[ {\\begin{array}{ccc}\n", + " \\frac{1}{4} & \\frac{1}{8} & \\frac{5}{8} \\\\\n", + " 0 & 1 & 0 \\\\\n", + " \\frac{3}{4} & \\frac{1}{16} & \\frac{3}{16} \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "For the sake of readability, we will call the states A, B and C and the actions 'cruise', 'stand' and 'dispatch'.\n", + "We will now build the transition model as a dictionary using these matrices." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t = {\n", + " 'A': {\n", + " 'cruise': {'A':0.5, 'B':0.25, 'C':0.25},\n", + " 'stand': {'A':0.0625, 'B':0.75, 'C':0.1875},\n", + " 'dispatch': {'A':0.25, 'B':0.125, 'C':0.625}\n", + " },\n", + " 'B': {\n", + " 'cruise': {'A':0.5, 'B':0, 'C':0.5},\n", + " 'stand': {'A':0.0625, 'B':0.875, 'C':0.0625},\n", + " 'dispatch': {'A':0, 'B':1, 'C':0}\n", + " },\n", + " 'C': {\n", + " 'cruise': {'A':0.25, 'B':0.25, 'C':0.5},\n", + " 'stand': {'A':0.125, 'B':0.75, 'C':0.125},\n", + " 'dispatch': {'A':0.75, 'B':0.0625, 'C':0.1875}\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The reward matrices for the problem are as follows:\n", + "
    \n", + "
    \n", + "Action 1: Cruising streets \n", + "
    \n", + "$\\\\\n", + " R^{1} = \n", + " \\left[ {\\begin{array}{ccc}\n", + " 10 & 4 & 8 \\\\\n", + " 14 & 0 & 18 \\\\\n", + " 10 & 2 & 8 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "Action 2: Waiting at the taxi stand \n", + "
    \n", + "$\\\\\n", + " R^{2} = \n", + " \\left[ {\\begin{array}{ccc}\n", + " 8 & 2 & 4 \\\\\n", + " 8 & 16 & 8 \\\\\n", + " 6 & 4 & 2\\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "Action 3: Waiting for dispatch \n", + "
    \n", + "$\\\\\n", + " R^{3} = \n", + " \\left[ {\\begin{array}{ccc}\n", + " 4 & 6 & 4 \\\\\n", + " 0 & 0 & 0 \\\\\n", + " 4 & 0 & 8\\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "
    \n", + "
    \n", + "We now build the reward model as a dictionary using these matrices." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "r = {\n", + " 'A': {\n", + " 'cruise': {'A':10, 'B':4, 'C':8},\n", + " 'stand': {'A':8, 'B':2, 'C':4},\n", + " 'dispatch': {'A':4, 'B':6, 'C':4}\n", + " },\n", + " 'B': {\n", + " 'cruise': {'A':14, 'B':0, 'C':18},\n", + " 'stand': {'A':8, 'B':16, 'C':8},\n", + " 'dispatch': {'A':0, 'B':0, 'C':0}\n", + " },\n", + " 'C': {\n", + " 'cruise': {'A':10, 'B':2, 'C':18},\n", + " 'stand': {'A':6, 'B':4, 'C':2},\n", + " 'dispatch': {'A':4, 'B':0, 'C':8}\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "The Bellman update equation now is defined as follows\n", + "\n", + "$$U(s)=\\max_{a\\epsilon A(s)}\\sum_{s'}P(s'\\ |\\ s,a)(R(s'\\ |\\ s,a) + \\gamma U(s'))$$\n", + "\n", + "It is not difficult to see that all the update equations we have used till now is just a special case of this more generalized equation. \n", + "If we did not have next-state-dependent rewards, the first term inside the summation exactly sums up to R(s, a) or the state-reward for a particular action and we would get the update equation used in the previous problem.\n", + "If we did not have action dependent rewards, the first term inside the summation sums up to R(s) or the state-reward and we would get the first update equation used in `mdp.ipynb`.\n", + "
    \n", + "For example, as we have the same reward regardless of the action, let's consider a reward of **r** units for a particular state and let's assume the transition probabilities to be 0.1, 0.2, 0.3 and 0.4 for 4 possible actions for that state.\n", + "We will further assume that a particular action in a state leads to the same state every time we take that action.\n", + "The first term inside the summation for this case will evaluate to (0.1 + 0.2 + 0.3 + 0.4)r = r which is equal to R(s) in the first update equation.\n", + "
    \n", + "There are many ways to write value iteration for this situation, but we will go with the most intuitive method.\n", + "One that can be implemented with minor alterations to the existing `value_iteration` algorithm.\n", + "
    \n", + "Our `DMDP` class will be slightly different.\n", + "More specifically, the `R` method will have one more index to go through now that we have three levels of nesting in the reward model.\n", + "We will call the new class `DMDP2` as I have run out of creative names." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class DMDP2:\n", + "\n", + " \"\"\"A Markov Decision Process, defined by an initial state, transition model,\n", + " and reward model. We also keep track of a gamma value, for use by\n", + " algorithms. The transition model is represented somewhat differently from\n", + " the text. Instead of P(s' | s, a) being a probability number for each\n", + " state/state/action triplet, we instead have T(s, a) return a\n", + " list of (p, s') pairs. The reward function is very similar.\n", + " We also keep track of the possible states,\n", + " terminal states, and actions for each state.\"\"\"\n", + "\n", + " def __init__(self, init, actlist, terminals, transitions={}, rewards={}, states=None, gamma=.9):\n", + " if not (0 < gamma <= 1):\n", + " raise ValueError(\"An MDP must have 0 < gamma <= 1\")\n", + "\n", + " if states:\n", + " self.states = states\n", + " else:\n", + " self.states = set()\n", + " self.init = init\n", + " self.actlist = actlist\n", + " self.terminals = terminals\n", + " self.transitions = transitions\n", + " self.rewards = rewards\n", + " self.gamma = gamma\n", + "\n", + " def R(self, state, action, state_):\n", + " \"\"\"Return a numeric reward for this state, this action and the next state_\"\"\"\n", + " if (self.rewards == {}):\n", + " raise ValueError('Reward model is missing')\n", + " else:\n", + " return self.rewards[state][action][state_]\n", + "\n", + " def T(self, state, action):\n", + " \"\"\"Transition model. From a state and an action, return a list\n", + " of (probability, result-state) pairs.\"\"\"\n", + " if(self.transitions == {}):\n", + " raise ValueError(\"Transition model is missing\")\n", + " else:\n", + " return self.transitions[state][action]\n", + "\n", + " def actions(self, state):\n", + " \"\"\"Set of actions that can be performed in this state. By default, a\n", + " fixed list of actions, except for terminal states. Override this\n", + " method if you need to specialize by state.\"\"\"\n", + " if state in self.terminals:\n", + " return [None]\n", + " else:\n", + " return self.actlist\n", + " \n", + " def actions(self, state):\n", + " \"\"\"Set of actions that can be performed in this state. By default, a\n", + " fixed list of actions, except for terminal states. Override this\n", + " method if you need to specialize by state.\"\"\"\n", + " if state in self.terminals:\n", + " return [None]\n", + " else:\n", + " return self.actlist" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Only the `R` method is different from the previous `DMDP` class.\n", + "
    \n", + "Our traditional custom class will be required to implement the transition model and the reward model.\n", + "
    \n", + "We call this class `CustomDMDP2`." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class CustomDMDP2(DMDP2):\n", + " \n", + " def __init__(self, transition_matrix, rewards, terminals, init, gamma=.9):\n", + " actlist = []\n", + " for state in transition_matrix.keys():\n", + " actlist.extend(transition_matrix[state])\n", + " actlist = list(set(actlist))\n", + " print(actlist)\n", + " \n", + " DMDP2.__init__(self, init, actlist, terminals=terminals, gamma=gamma)\n", + " self.t = transition_matrix\n", + " self.rewards = rewards\n", + " for state in self.t:\n", + " self.states.add(state)\n", + " \n", + " def T(self, state, action):\n", + " if action is None:\n", + " return [(0.0, state)]\n", + " else:\n", + " return [(prob, new_state) for new_state, prob in self.t[state][action].items()]\n", + " \n", + " def R(self, state, action, state_):\n", + " if action is None:\n", + " return 0\n", + " else:\n", + " return self.rewards[state][action][state_]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can finally write value iteration for this problem.\n", + "The latest update equation will be used." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def value_iteration_taxi_mdp(dmdp2, epsilon=0.001):\n", + " U1 = {s: 0 for s in dmdp2.states}\n", + " R, T, gamma = dmdp2.R, dmdp2.T, dmdp2.gamma\n", + " while True:\n", + " U = U1.copy()\n", + " delta = 0\n", + " for s in dmdp2.states:\n", + " U1[s] = max([sum([(p*(R(s, a, s1) + gamma*U[s1])) for (p, s1) in T(s, a)]) for a in dmdp2.actions(s)])\n", + " delta = max(delta, abs(U1[s] - U[s]))\n", + " if delta < epsilon * (1 - gamma) / gamma:\n", + " return U" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These algorithms can be made more pythonic by using cleverer list comprehensions.\n", + "We can also write the variants of value iteration in such a way that all problems are solved using the same base class, regardless of the reward function and the number of arguments it takes.\n", + "Quite a few things can be done to refactor the code and reduce repetition, but we have done it this way for the sake of clarity.\n", + "Perhaps you can try this as an exercise.\n", + "
    \n", + "We now need to define terminals and initial state." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "terminals = ['end']\n", + "init = 'A'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's instantiate our class." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['stand', 'dispatch', 'cruise']\n" + ] + } + ], + "source": [ + "dmdp2 = CustomDMDP2(t, r, terminals, init, gamma=.9)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'A': 124.4881543573768, 'B': 137.70885410461636, 'C': 129.08041190693115}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "value_iteration_taxi_mdp(dmdp2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are the expected utility values for the states of our MDP.\n", + "Let's proceed to write a helper function to find the expected utility and another to find the best policy." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def expected_utility_dmdp2(a, s, U, dmdp2):\n", + " return sum([(p*(dmdp2.R(s, a, s1) + dmdp2.gamma*U[s1])) for (p, s1) in dmdp2.T(s, a)])" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from utils import argmax\n", + "def best_policy_dmdp2(dmdp2, U):\n", + " pi = {}\n", + " for s in dmdp2.states:\n", + " pi[s] = argmax(dmdp2.actions(s), key=lambda a: expected_utility_dmdp2(a, s, U, dmdp2))\n", + " return pi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Find the best policy." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'C': 'cruise', 'A': 'stand', 'B': 'stand'}\n" + ] + } + ], + "source": [ + "pi = best_policy_dmdp2(dmdp2, value_iteration_taxi_mdp(dmdp2, .01))\n", + "print(pi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have successfully adapted the existing code to a different scenario yet again.\n", + "The takeaway from this section is that you can convert the vast majority of reinforcement learning problems into MDPs and solve for the best policy using simple yet efficient tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GRID MDP\n", + "---\n", + "### Pathfinding Problem\n", + "Markov Decision Processes can be used to find the best path through a maze. Let us consider this simple maze.\n", + "![title](images/maze.png)\n", + "\n", + "This environment can be formulated as a GridMDP.\n", + "
    \n", + "To make the grid matrix, we will consider the state-reward to be -0.1 for every state.\n", + "
    \n", + "State (1, 1) will have a reward of -5 to signify that this state is to be prohibited.\n", + "
    \n", + "State (9, 9) will have a reward of +5.\n", + "This will be the terminal state.\n", + "
    \n", + "The matrix can be generated using the GridMDP editor or we can write it ourselves." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "grid = [\n", + " [None, None, None, None, None, None, None, None, None, None, None], \n", + " [None, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, None, +5.0, None], \n", + " [None, -0.1, None, None, None, None, None, None, None, -0.1, None], \n", + " [None, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, None], \n", + " [None, -0.1, None, None, None, None, None, None, None, None, None], \n", + " [None, -0.1, None, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, None], \n", + " [None, -0.1, None, None, None, None, None, -0.1, None, -0.1, None], \n", + " [None, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, -0.1, None, -0.1, None], \n", + " [None, None, None, None, None, -0.1, None, -0.1, None, -0.1, None], \n", + " [None, -5.0, -0.1, -0.1, -0.1, -0.1, None, -0.1, None, -0.1, None], \n", + " [None, None, None, None, None, None, None, None, None, None, None]\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have only one terminal state, (9, 9)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "terminals = [(9, 9)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define our maze environment below" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "maze = GridMDP(grid, terminals)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To solve the maze, we can use the `best_policy` function along with `value_iteration`." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "pi = best_policy(maze, value_iteration(maze))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the heatmap generated by the GridMDP editor using `value_iteration` on this environment\n", + "
    \n", + "![title](images/mdp-d.png)\n", + "
    \n", + "Let's print out the best policy" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None None None None None None None None None None None\n", + "None v < < < < < < None . None\n", + "None v None None None None None None None ^ None\n", + "None > > > > > > > > ^ None\n", + "None ^ None None None None None None None None None\n", + "None ^ None > > > > v < < None\n", + "None ^ None None None None None v None ^ None\n", + "None ^ < < < < < < None ^ None\n", + "None None None None None ^ None ^ None ^ None\n", + "None > > > > ^ None ^ None ^ None\n", + "None None None None None None None None None None None\n" + ] + } + ], + "source": [ + "from utils import print_table\n", + "print_table(maze.to_arrows(pi))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can infer, we can find the path to the terminal state starting from any given state using this policy.\n", + "All maze problems can be solved by formulating it as a MDP." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## POMDP\n", + "### Two state POMDP\n", + "Let's consider a problem where we have two doors, one to our left and one to our right.\n", + "One of these doors opens to a room with a tiger in it, and the other one opens to an empty hall.\n", + "
    \n", + "We will call our two states `0` and `1` for `left` and `right` respectively.\n", + "
    \n", + "The possible actions we can take are as follows:\n", + "
    \n", + "1. __Open-left__: Open the left door.\n", + "Represented by `0`.\n", + "2. __Open-right__: Open the right door.\n", + "Represented by `1`.\n", + "3. __Listen__: Listen carefully to one side and possibly hear the tiger breathing.\n", + "Represented by `2`.\n", + "\n", + "
    \n", + "The possible observations we can get are as follows:\n", + "
    \n", + "1. __TL__: Tiger seems to be at the left door.\n", + "2. __TR__: Tiger seems to be at the right door.\n", + "\n", + "
    \n", + "The reward function is as follows:\n", + "
    \n", + "We get +10 reward for opening the door to the empty hall and we get -100 reward for opening the other door and setting the tiger free.\n", + "
    \n", + "Listening costs us -1 reward.\n", + "
    \n", + "We want to minimize our chances of setting the tiger free.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our transition probabilities can be defined as:\n", + "
    \n", + "
    \n", + "Action `0` (Open left door)\n", + "$\\\\\n", + " P(0) = \n", + " \\left[ {\\begin{array}{cc}\n", + " 0.5 & 0.5 \\\\\n", + " 0.5 & 0.5 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "Action `1` (Open right door)\n", + "$\\\\\n", + " P(1) = \n", + " \\left[ {\\begin{array}{cc}\n", + " 0.5 & 0.5 \\\\\n", + " 0.5 & 0.5 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "Action `2` (Listen)\n", + "$\\\\\n", + " P(2) = \n", + " \\left[ {\\begin{array}{cc}\n", + " 1.0 & 0.0 \\\\\n", + " 0.0 & 1.0 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "
    \n", + "
    \n", + "Our observation probabilities can be defined as:\n", + "
    \n", + "
    \n", + "$\\\\\n", + " O(0) = \n", + " \\left[ {\\begin{array}{ccc}\n", + " Open left & TL & TR \\\\\n", + " Tiger: left & 0.5 & 0.5 \\\\\n", + " Tiger: right & 0.5 & 0.5 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "\n", + "$\\\\\n", + " O(1) = \n", + " \\left[ {\\begin{array}{ccc}\n", + " Open right & TL & TR \\\\\n", + " Tiger: left & 0.5 & 0.5 \\\\\n", + " Tiger: right & 0.5 & 0.5 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "\n", + "$\\\\\n", + " O(2) = \n", + " \\left[ {\\begin{array}{ccc}\n", + " Listen & TL & TR \\\\\n", + " Tiger: left & 0.85 & 0.15 \\\\\n", + " Tiger: right & 0.15 & 0.85 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + "\n", + "
    \n", + "
    \n", + "The rewards of this POMDP are defined as:\n", + "
    \n", + "
    \n", + "$\\\\\n", + " R(0) = \n", + " \\left[ {\\begin{array}{cc}\n", + " Openleft & Reward \\\\\n", + " Tiger: left & -100 \\\\\n", + " Tiger: right & +10 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "$\\\\\n", + " R(1) = \n", + " \\left[ {\\begin{array}{cc}\n", + " Openright & Reward \\\\\n", + " Tiger: left & +10 \\\\\n", + " Tiger: right & -100 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "$\\\\\n", + " R(2) = \n", + " \\left[ {\\begin{array}{cc}\n", + " Listen & Reward \\\\\n", + " Tiger: left & -1 \\\\\n", + " Tiger: right & -1 \\\\\n", + " \\end{array}}\\right] \\\\\n", + " \\\\\n", + " $\n", + " \n", + "
    \n", + "Based on these matrices, we will initialize our variables." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first define our transition state." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "t_prob = [[[0.5, 0.5], \n", + " [0.5, 0.5]], \n", + " \n", + " [[0.5, 0.5], \n", + " [0.5, 0.5]], \n", + " \n", + " [[1.0, 0.0], \n", + " [0.0, 1.0]]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Followed by the observation model." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "e_prob = [[[0.5, 0.5], \n", + " [0.5, 0.5]], \n", + " \n", + " [[0.5, 0.5], \n", + " [0.5, 0.5]], \n", + " \n", + " [[0.85, 0.15], \n", + " [0.15, 0.85]]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And the reward model." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "rewards = [[-100, 10], \n", + " [10, -100], \n", + " [-1, -1]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now define our states, observations and actions.\n", + "
    \n", + "We will use `gamma` = 0.95 for this example.\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "# 0: open-left, 1: open-right, 2: listen\n", + "actions = ('0', '1', '2')\n", + "# 0: left, 1: right\n", + "states = ('0', '1')\n", + "\n", + "gamma = 0.95" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have all the required variables to instantiate an object of the `POMDP` class." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "pomdp = POMDP(actions, t_prob, e_prob, rewards, states, gamma)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now find the utility function by running `pomdp_value_iteration` on our `pomdp` object." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "defaultdict(list,\n", + " {'0': [array([-83.05169196, 26.94830804])],\n", + " '1': [array([ 26.94830804, -83.05169196])],\n", + " '2': [array([23.55049363, -0.76359097]),\n", + " array([23.55049363, -0.76359097]),\n", + " array([23.55049363, -0.76359097]),\n", + " array([23.55049363, -0.76359097]),\n", + " array([23.24120177, 1.56028929]),\n", + " array([23.24120177, 1.56028929]),\n", + " array([23.24120177, 1.56028929]),\n", + " array([20.0874279 , 15.03900771]),\n", + " array([20.0874279 , 15.03900771]),\n", + " array([20.0874279 , 15.03900771]),\n", + " array([20.0874279 , 15.03900771]),\n", + " array([17.91696135, 17.91696135]),\n", + " array([17.91696135, 17.91696135]),\n", + " array([17.91696135, 17.91696135]),\n", + " array([17.91696135, 17.91696135]),\n", + " array([17.91696135, 17.91696135]),\n", + " array([15.03900771, 20.0874279 ]),\n", + " array([15.03900771, 20.0874279 ]),\n", + " array([15.03900771, 20.0874279 ]),\n", + " array([15.03900771, 20.0874279 ]),\n", + " array([ 1.56028929, 23.24120177]),\n", + " array([ 1.56028929, 23.24120177]),\n", + " array([ 1.56028929, 23.24120177]),\n", + " array([-0.76359097, 23.55049363]),\n", + " array([-0.76359097, 23.55049363]),\n", + " array([-0.76359097, 23.55049363]),\n", + " array([-0.76359097, 23.55049363])]})" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "utility = pomdp_value_iteration(pomdp, epsilon=3)\n", + "utility" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "%matplotlib inline\n", + "\n", + "def plot_utility(utility):\n", + " open_left = utility['0'][0]\n", + " open_right = utility['1'][0]\n", + " listen_left = utility['2'][0]\n", + " listen_right = utility['2'][-1]\n", + " left = (open_left[0] - listen_left[0]) / (open_left[0] - listen_left[0] + listen_left[1] - open_left[1])\n", + " right = (open_right[0] - listen_right[0]) / (open_right[0] - listen_right[0] + listen_right[1] - open_right[1])\n", + " \n", + " colors = ['g', 'b', 'k']\n", + " for action in utility:\n", + " for value in utility[action]:\n", + " plt.plot(value, color=colors[int(action)])\n", + " plt.vlines([left, right], -10, 35, linestyles='dashed', colors='c')\n", + " plt.ylim(-10, 35)\n", + " plt.xlim(0, 1)\n", + " plt.text(left/2 - 0.35, 30, 'open-left')\n", + " plt.text((right + left)/2 - 0.04, 30, 'listen')\n", + " plt.text((right + 1)/2 + 0.22, 30, 'open-right')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_utility(utility)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hence, we get a piecewise-continuous utility function consistent with the given POMDP." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/neural_nets.ipynb b/neural_nets.ipynb new file mode 100644 index 000000000..1291da547 --- /dev/null +++ b/neural_nets.ipynb @@ -0,0 +1,570 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NEURAL NETWORKS\n", + "\n", + "This notebook covers the neural network algorithms from chapter 18 of the book *Artificial Intelligence: A Modern Approach*, by Stuart Russel and Peter Norvig. The code in the notebook can be found in [learning.py](https://github.com/aimacode/aima-python/blob/master/learning.py).\n", + "\n", + "Execute the below cell to get started:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import *\n", + "\n", + "from notebook import psource, pseudocode" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NEURAL NETWORK ALGORITHM\n", + "\n", + "### Overview\n", + "\n", + "Although the Perceptron may seem like a good way to make classifications, it is a linear classifier (which, roughly, means it can only draw straight lines to divide spaces) and therefore it can be stumped by more complex problems. To solve this issue we can extend Perceptron by employing multiple layers of its functionality. The construct we are left with is called a Neural Network, or a Multi-Layer Perceptron, and it is a non-linear classifier. It achieves that by combining the results of linear functions on each layer of the network.\n", + "\n", + "Similar to the Perceptron, this network also has an input and output layer; however, it can also have a number of hidden layers. These hidden layers are responsible for the non-linearity of the network. The layers are comprised of nodes. Each node in a layer (excluding the input one), holds some values, called *weights*, and takes as input the output values of the previous layer. The node then calculates the dot product of its inputs and its weights and then activates it with an *activation function* (e.g. sigmoid activation function). Its output is then fed to the nodes of the next layer. Note that sometimes the output layer does not use an activation function, or uses a different one from the rest of the network. The process of passing the outputs down the layer is called *feed-forward*.\n", + "\n", + "After the input values are fed-forward into the network, the resulting output can be used for classification. The problem at hand now is how to train the network (i.e. adjust the weights in the nodes). To accomplish that we utilize the *Backpropagation* algorithm. In short, it does the opposite of what we were doing up to this point. Instead of feeding the input forward, it will track the error backwards. So, after we make a classification, we check whether it is correct or not, and how far off we were. We then take this error and propagate it backwards in the network, adjusting the weights of the nodes accordingly. We will run the algorithm on the given input/dataset for a fixed amount of time, or until we are satisfied with the results. The number of times we will iterate over the dataset is called *epochs*. In a later section we take a detailed look at how this algorithm works.\n", + "\n", + "NOTE: Sometimes we add another node to the input of each layer, called *bias*. This is a constant value that will be fed to the next layer, usually set to 1. The bias generally helps us \"shift\" the computed function to the left or right." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![neural_net](images/neural_net.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "The `NeuralNetLearner` function takes as input a dataset to train upon, the learning rate (in (0, 1]), the number of epochs and finally the size of the hidden layers. This last argument is a list, with each element corresponding to one hidden layer.\n", + "\n", + "After that we will create our neural network in the `network` function. This function will make the necessary connections between the input layer, hidden layer and output layer. With the network ready, we will use the `BackPropagationLearner` to train the weights of our network for the examples provided in the dataset.\n", + "\n", + "The NeuralNetLearner returns the `predict` function which, in short, can receive an example and feed-forward it into our network to generate a prediction.\n", + "\n", + "In more detail, the example values are first passed to the input layer and then they are passed through the rest of the layers. Each node calculates the dot product of its inputs and its weights, activates it and pushes it to the next layer. The final prediction is the node in the output layer with the maximum value." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def NeuralNetLearner(dataset, hidden_layer_sizes=None,\n",
    +       "                     learning_rate=0.01, epochs=100, activation = sigmoid):\n",
    +       "    """Layered feed-forward network.\n",
    +       "    hidden_layer_sizes: List of number of hidden units per hidden layer\n",
    +       "    learning_rate: Learning rate of gradient descent\n",
    +       "    epochs: Number of passes over the dataset\n",
    +       "    """\n",
    +       "\n",
    +       "    hidden_layer_sizes = hidden_layer_sizes or [3]  # default value\n",
    +       "    i_units = len(dataset.inputs)\n",
    +       "    o_units = len(dataset.values[dataset.target])\n",
    +       "\n",
    +       "    # construct a network\n",
    +       "    raw_net = network(i_units, hidden_layer_sizes, o_units, activation)\n",
    +       "    learned_net = BackPropagationLearner(dataset, raw_net,\n",
    +       "                                         learning_rate, epochs, activation)\n",
    +       "\n",
    +       "    def predict(example):\n",
    +       "        # Input nodes\n",
    +       "        i_nodes = learned_net[0]\n",
    +       "\n",
    +       "        # Activate input layer\n",
    +       "        for v, n in zip(example, i_nodes):\n",
    +       "            n.value = v\n",
    +       "\n",
    +       "        # Forward pass\n",
    +       "        for layer in learned_net[1:]:\n",
    +       "            for node in layer:\n",
    +       "                inc = [n.value for n in node.inputs]\n",
    +       "                in_val = dotproduct(inc, node.weights)\n",
    +       "                node.value = node.activation(in_val)\n",
    +       "\n",
    +       "        # Hypothesis\n",
    +       "        o_nodes = learned_net[-1]\n",
    +       "        prediction = find_max_node(o_nodes)\n",
    +       "        return prediction\n",
    +       "\n",
    +       "    return predict\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(NeuralNetLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## BACKPROPAGATION\n", + "\n", + "### Overview\n", + "\n", + "In both the Perceptron and the Neural Network, we are using the Backpropagation algorithm to train our model by updating the weights. This is achieved by propagating the errors from our last layer (output layer) back to our first layer (input layer), this is why it is called Backpropagation. In order to use Backpropagation, we need a cost function. This function is responsible for indicating how good our neural network is for a given example. One common cost function is the *Mean Squared Error* (MSE). This cost function has the following format:\n", + "\n", + "$$MSE=\\frac{1}{n} \\sum_{i=1}^{n}(y - \\hat{y})^{2}$$\n", + "\n", + "Where `n` is the number of training examples, $\\hat{y}$ is our prediction and $y$ is the correct prediction for the example.\n", + "\n", + "The algorithm combines the concept of partial derivatives and the chain rule to generate the gradient for each weight in the network based on the cost function.\n", + "\n", + "For example, if we are using a Neural Network with three layers, the sigmoid function as our activation function and the MSE cost function, we want to find the gradient for the a given weight $w_{j}$, we can compute it like this:\n", + "\n", + "$$\\frac{\\partial MSE(\\hat{y}, y)}{\\partial w_{j}} = \\frac{\\partial MSE(\\hat{y}, y)}{\\partial \\hat{y}}\\times\\frac{\\partial\\hat{y}(in_{j})}{\\partial in_{j}}\\times\\frac{\\partial in_{j}}{\\partial w_{j}}$$\n", + "\n", + "Solving this equation, we have:\n", + "\n", + "$$\\frac{\\partial MSE(\\hat{y}, y)}{\\partial w_{j}} = (\\hat{y} - y)\\times{\\hat{y}}'(in_{j})\\times a_{j}$$\n", + "\n", + "Remember that $\\hat{y}$ is the activation function applied to a neuron in our hidden layer, therefore $$\\hat{y} = sigmoid(\\sum_{i=1}^{num\\_neurons}w_{i}\\times a_{i})$$\n", + "\n", + "Also $a$ is the input generated by feeding the input layer variables into the hidden layer.\n", + "\n", + "We can use the same technique for the weights in the input layer as well. After we have the gradients for both weights, we use gradient descent to update the weights of the network." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pseudocode" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### AIMA3e\n", + "__function__ BACK-PROP-LEARNING(_examples_, _network_) __returns__ a neural network \n", + " __inputs__ _examples_, a set of examples, each with input vector __x__ and output vector __y__ \n", + "    _network_, a multilayer network with _L_ layers, weights _wi,j_, activation function _g_ \n", + " __local variables__: Δ, a vector of errors, indexed by network node \n", + "\n", + " __repeat__ \n", + "   __for each__ weight _wi,j_ in _network_ __do__ \n", + "     _wi,j_ ← a small random number \n", + "   __for each__ example (__x__, __y__) __in__ _examples_ __do__ \n", + "     /\\* _Propagate the inputs forward to compute the outputs_ \\*/ \n", + "     __for each__ node _i_ in the input layer __do__ \n", + "       _ai_ ← _xi_ \n", + "     __for__ _l_ = 2 __to__ _L_ __do__ \n", + "       __for each__ node _j_ in layer _l_ __do__ \n", + "         _inj_ ← Σ_i_ _wi,j_ _ai_ \n", + "         _aj_ ← _g_(_inj_) \n", + "     /\\* _Propagate deltas backward from output layer to input layer_ \\*/ \n", + "     __for each__ node _j_ in the output layer __do__ \n", + "       Δ\\[_j_\\] ← _g_′(_inj_) × (_yi_ − _aj_) \n", + "     __for__ _l_ = _L_ − 1 __to__ 1 __do__ \n", + "       __for each__ node _i_ in layer _l_ __do__ \n", + "         Δ\\[_i_\\] ← _g_′(_ini_) Σ_j_ _wi,j_ Δ\\[_j_\\] \n", + "     /\\* _Update every weight in network using deltas_ \\*/ \n", + "     __for each__ weight _wi,j_ in _network_ __do__ \n", + "       _wi,j_ ← _wi,j_ + _α_ × _ai_ × Δ\\[_j_\\] \n", + "  __until__ some stopping criterion is satisfied \n", + "  __return__ _network_ \n", + "\n", + "---\n", + "__Figure ??__ The back\\-propagation algorithm for learning in multilayer networks." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pseudocode('Back-Prop-Learning')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "First, we feed-forward the examples in our neural network. After that, we calculate the gradient for each layers' weights by using the chain rule. Once that is complete, we update all the weights using gradient descent. After running these for a given number of epochs, the function returns the trained Neural Network." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def BackPropagationLearner(dataset, net, learning_rate, epochs, activation=sigmoid):\n",
    +       "    """[Figure 18.23] The back-propagation algorithm for multilayer networks"""\n",
    +       "    # Initialise weights\n",
    +       "    for layer in net:\n",
    +       "        for node in layer:\n",
    +       "            node.weights = random_weights(min_value=-0.5, max_value=0.5,\n",
    +       "                                          num_weights=len(node.weights))\n",
    +       "\n",
    +       "    examples = dataset.examples\n",
    +       "    '''\n",
    +       "    As of now dataset.target gives an int instead of list,\n",
    +       "    Changing dataset class will have effect on all the learners.\n",
    +       "    Will be taken care of later.\n",
    +       "    '''\n",
    +       "    o_nodes = net[-1]\n",
    +       "    i_nodes = net[0]\n",
    +       "    o_units = len(o_nodes)\n",
    +       "    idx_t = dataset.target\n",
    +       "    idx_i = dataset.inputs\n",
    +       "    n_layers = len(net)\n",
    +       "\n",
    +       "    inputs, targets = init_examples(examples, idx_i, idx_t, o_units)\n",
    +       "\n",
    +       "    for epoch in range(epochs):\n",
    +       "        # Iterate over each example\n",
    +       "        for e in range(len(examples)):\n",
    +       "            i_val = inputs[e]\n",
    +       "            t_val = targets[e]\n",
    +       "\n",
    +       "            # Activate input layer\n",
    +       "            for v, n in zip(i_val, i_nodes):\n",
    +       "                n.value = v\n",
    +       "\n",
    +       "            # Forward pass\n",
    +       "            for layer in net[1:]:\n",
    +       "                for node in layer:\n",
    +       "                    inc = [n.value for n in node.inputs]\n",
    +       "                    in_val = dotproduct(inc, node.weights)\n",
    +       "                    node.value = node.activation(in_val)\n",
    +       "\n",
    +       "            # Initialize delta\n",
    +       "            delta = [[] for _ in range(n_layers)]\n",
    +       "\n",
    +       "            # Compute outer layer delta\n",
    +       "\n",
    +       "            # Error for the MSE cost function\n",
    +       "            err = [t_val[i] - o_nodes[i].value for i in range(o_units)]\n",
    +       "\n",
    +       "            # The activation function used is relu or sigmoid function\n",
    +       "            if node.activation == sigmoid:\n",
    +       "                delta[-1] = [sigmoid_derivative(o_nodes[i].value) * err[i] for i in range(o_units)]\n",
    +       "            else:\n",
    +       "                delta[-1] = [relu_derivative(o_nodes[i].value) * err[i] for i in range(o_units)]\n",
    +       "\n",
    +       "            # Backward pass\n",
    +       "            h_layers = n_layers - 2\n",
    +       "            for i in range(h_layers, 0, -1):\n",
    +       "                layer = net[i]\n",
    +       "                h_units = len(layer)\n",
    +       "                nx_layer = net[i+1]\n",
    +       "\n",
    +       "                # weights from each ith layer node to each i + 1th layer node\n",
    +       "                w = [[node.weights[k] for node in nx_layer] for k in range(h_units)]\n",
    +       "\n",
    +       "                if activation == sigmoid:\n",
    +       "                    delta[i] = [sigmoid_derivative(layer[j].value) * dotproduct(w[j], delta[i+1])\n",
    +       "                            for j in range(h_units)]\n",
    +       "                else:\n",
    +       "                    delta[i] = [relu_derivative(layer[j].value) * dotproduct(w[j], delta[i+1])\n",
    +       "                            for j in range(h_units)]\n",
    +       "\n",
    +       "            #  Update weights\n",
    +       "            for i in range(1, n_layers):\n",
    +       "                layer = net[i]\n",
    +       "                inc = [node.value for node in net[i-1]]\n",
    +       "                units = len(layer)\n",
    +       "                for j in range(units):\n",
    +       "                    layer[j].weights = vector_add(layer[j].weights,\n",
    +       "                                                  scalar_vector_product(\n",
    +       "                                                  learning_rate * delta[i][j], inc))\n",
    +       "\n",
    +       "    return net\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(BackPropagationLearner)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "iris.classes_to_numbers()\n", + "\n", + "nNL = NeuralNetLearner(iris)\n", + "print(nNL([5, 3, 1, 0.1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "The output should be 0, which means the item should get classified in the first class, \"setosa\". Note that since the algorithm is non-deterministic (because of the random initial weights) the classification might be wrong. Usually though, it should be correct.\n", + "\n", + "To increase accuracy, you can (most of the time) add more layers and nodes. Unfortunately, increasing the number of layers or nodes also increases the computation cost and might result in overfitting.\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "source": [], + "metadata": { + "collapsed": false + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/nlp.ipynb b/nlp.ipynb index 1a2da9488..9656c1ea0 100644 --- a/nlp.ipynb +++ b/nlp.ipynb @@ -1,24 +1,1021 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NATURAL LANGUAGE PROCESSING\n", + "\n", + "This notebook covers chapters 22 and 23 from the book *Artificial Intelligence: A Modern Approach*, 3rd Edition. The implementations of the algorithms can be found in [nlp.py](https://github.com/aimacode/aima-python/blob/master/nlp.py).\n", + "\n", + "Run the below cell to import the code from the module and get started!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import nlp\n", + "from nlp import Page, HITS\n", + "from nlp import Lexicon, Rules, Grammar, ProbLexicon, ProbRules, ProbGrammar\n", + "from nlp import CYK_parse, Chart\n", + "\n", + "from notebook import psource" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## CONTENTS\n", + "\n", + "* Overview\n", + "* Languages\n", + "* HITS\n", + "* Question Answering\n", + "* CYK Parse\n", + "* Chart Parsing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OVERVIEW\n", + "\n", + "**Natural Language Processing (NLP)** is a field of AI concerned with understanding, analyzing and using natural languages. This field is considered a difficult yet intriguing field of study, since it is connected to how humans and their languages work.\n", + "\n", + "Applications of the field include translation, speech recognition, topic segmentation, information extraction and retrieval, and a lot more.\n", + "\n", + "Below we take a look at some algorithms in the field. Before we get right into it though, we will take a look at a very useful form of language, **context-free** languages. Even though they are a bit restrictive, they have been used a lot in research in natural language processing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LANGUAGES\n", + "\n", + "Languages can be represented by a set of grammar rules over a lexicon of words. Different languages can be represented by different types of grammar, but in Natural Language Processing we are mainly interested in context-free grammars.\n", + "\n", + "### Context-Free Grammars\n", + "\n", + "A lot of natural and programming languages can be represented by a **Context-Free Grammar (CFG)**. A CFG is a grammar that has a single non-terminal symbol on the left-hand side. That means a non-terminal can be replaced by the right-hand side of the rule regardless of context. An example of a CFG:\n", + "\n", + "```\n", + "S -> aSb | ε\n", + "```\n", + "\n", + "That means `S` can be replaced by either `aSb` or `ε` (with `ε` we denote the empty string). The lexicon of the language is comprised of the terminals `a` and `b`, while with `S` we denote the non-terminal symbol. In general, non-terminals are capitalized while terminals are not, and we usually name the starting non-terminal `S`. The language generated by the above grammar is the language anbn for n greater or equal than 1." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Probabilistic Context-Free Grammar\n", + "\n", + "While a simple CFG can be very useful, we might want to know the chance of each rule occurring. Above, we do not know if `S` is more likely to be replaced by `aSb` or `ε`. **Probabilistic Context-Free Grammars (PCFG)** are built to fill exactly that need. Each rule has a probability, given in brackets, and the probabilities of a rule sum up to 1:\n", + "\n", + "```\n", + "S -> aSb [0.7] | ε [0.3]\n", + "```\n", + "\n", + "Now we know it is more likely for `S` to be replaced by `aSb` than by `ε`.\n", + "\n", + "An issue with *PCFGs* is how we will assign the various probabilities to the rules. We could use our knowledge as humans to assign the probabilities, but that is a laborious and prone to error task. Instead, we can *learn* the probabilities from data. Data is categorized as labeled (with correctly parsed sentences, usually called a **treebank**) or unlabeled (given only lexical and syntactic category names).\n", + "\n", + "With labeled data, we can simply count the occurrences. For the above grammar, if we have 100 `S` rules and 30 of them are of the form `S -> ε`, we assign a probability of 0.3 to the transformation.\n", + "\n", + "With unlabeled data we have to learn both the grammar rules and the probability of each rule. We can go with many approaches, one of them the **inside-outside** algorithm. It uses a dynamic programming approach, that first finds the probability of a substring being generated by each rule, and then estimates the probability of each rule." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Chomsky Normal Form\n", + "\n", + "A grammar is in Chomsky Normal Form (or **CNF**, not to be confused with *Conjunctive Normal Form*) if its rules are one of the three:\n", + "\n", + "* `X -> Y Z`\n", + "* `A -> a`\n", + "* `S -> ε`\n", + "\n", + "Where *X*, *Y*, *Z*, *A* are non-terminals, *a* is a terminal, *ε* is the empty string and *S* is the start symbol (the start symbol should not be appearing on the right hand side of rules). Note that there can be multiple rules for each left hand side non-terminal, as long they follow the above. For example, a rule for *X* might be: `X -> Y Z | A B | a | b`.\n", + "\n", + "Of course, we can also have a *CNF* with probabilities.\n", + "\n", + "This type of grammar may seem restrictive, but it can be proven that any context-free grammar can be converted to CNF." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Lexicon\n", + "\n", + "The lexicon of a language is defined as a list of allowable words. These words are grouped into the usual classes: `verbs`, `nouns`, `adjectives`, `adverbs`, `pronouns`, `names`, `articles`, `prepositions` and `conjunctions`. For the first five classes it is impossible to list all words, since words are continuously being added in the classes. Recently \"google\" was added to the list of verbs, and words like that will continue to pop up and get added to the lists. For that reason, these first five categories are called **open classes**. The rest of the categories have much fewer words and much less development. While words like \"thou\" were commonly used in the past but have declined almost completely in usage, most changes take many decades or centuries to manifest, so we can safely assume the categories will remain static for the foreseeable future. Thus, these categories are called **closed classes**.\n", + "\n", + "An example lexicon for a PCFG (note that other classes can also be used according to the language, like `digits`, or `RelPro` for relative pronoun):\n", + "\n", + "```\n", + "Verb -> is [0.3] | say [0.1] | are [0.1] | ...\n", + "Noun -> robot [0.1] | sheep [0.05] | fence [0.05] | ...\n", + "Adjective -> good [0.1] | new [0.1] | sad [0.05] | ...\n", + "Adverb -> here [0.1] | lightly [0.05] | now [0.05] | ...\n", + "Pronoun -> me [0.1] | you [0.1] | he [0.05] | ...\n", + "RelPro -> that [0.4] | who [0.2] | which [0.2] | ...\n", + "Name -> john [0.05] | mary [0.05] | peter [0.01] | ...\n", + "Article -> the [0.35] | a [0.25] | an [0.025] | ...\n", + "Preposition -> to [0.25] | in [0.2] | at [0.1] | ...\n", + "Conjunction -> and [0.5] | or [0.2] | but [0.2] | ...\n", + "Digit -> 1 [0.3] | 2 [0.2] | 0 [0.2] | ...\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Grammar\n", + "\n", + "With grammars we combine words from the lexicon into valid phrases. A grammar is comprised of **grammar rules**. Each rule transforms the left-hand side of the rule into the right-hand side. For example, `A -> B` means that `A` transforms into `B`. Let's build a grammar for the language we started building with the lexicon. We will use a PCFG.\n", + "\n", + "```\n", + "S -> NP VP [0.9] | S Conjunction S [0.1]\n", + "\n", + "NP -> Pronoun [0.3] | Name [0.1] | Noun [0.1] | Article Noun [0.25] |\n", + " Article Adjs Noun [0.05] | Digit [0.05] | NP PP [0.1] |\n", + " NP RelClause [0.05]\n", + "\n", + "VP -> Verb [0.4] | VP NP [0.35] | VP Adjective [0.05] | VP PP [0.1]\n", + " VP Adverb [0.1]\n", + "\n", + "Adjs -> Adjective [0.8] | Adjective Adjs [0.2]\n", + "\n", + "PP -> Preposition NP [1.0]\n", + "\n", + "RelClause -> RelPro VP [1.0]\n", + "```\n", + "\n", + "Some valid phrases the grammar produces: \"`mary is sad`\", \"`you are a robot`\" and \"`she likes mary and a good fence`\".\n", + "\n", + "What if we wanted to check if the phrase \"`mary is sad`\" is actually a valid sentence? We can use a **parse tree** to constructively prove that a string of words is a valid phrase in the given language and even calculate the probability of the generation of the sentence.\n", + "\n", + "![parse_tree](images/parse_tree.png)\n", + "\n", + "The probability of the whole tree can be calculated by multiplying the probabilities of each individual rule transormation: `0.9 * 0.1 * 0.05 * 0.05 * 0.4 * 0.05 * 0.3 = 0.00000135`.\n", + "\n", + "To conserve space, we can also write the tree in linear form:\n", + "\n", + "[S [NP [Name **mary**]] [VP [VP [Verb **is**]] [Adjective **sad**]]]\n", + "\n", + "Unfortunately, the current grammar **overgenerates**, that is, it creates sentences that are not grammatically correct (according to the English language), like \"`the fence are john which say`\". It also **undergenerates**, which means there are valid sentences it does not generate, like \"`he believes mary is sad`\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "In the module we have implementation both for probabilistic and non-probabilistic grammars. Both these implementation follow the same format. There are functions for the lexicon and the rules which can be combined to create a grammar object.\n", + "\n", + "#### Non-Probabilistic\n", + "\n", + "Execute the cell below to view the implemenations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(Lexicon, Rules, Grammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's build a lexicon and a grammar for the above language:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lexicon {'Adverb': ['here', 'lightly', 'now'], 'Verb': ['is', 'say', 'are'], 'Digit': ['1', '2', '0'], 'RelPro': ['that', 'who', 'which'], 'Conjunction': ['and', 'or', 'but'], 'Name': ['john', 'mary', 'peter'], 'Pronoun': ['me', 'you', 'he'], 'Article': ['the', 'a', 'an'], 'Noun': ['robot', 'sheep', 'fence'], 'Adjective': ['good', 'new', 'sad'], 'Preposition': ['to', 'in', 'at']}\n", + "\n", + "Rules: {'RelClause': [['RelPro', 'VP']], 'Adjs': [['Adjective'], ['Adjective', 'Adjs']], 'NP': [['Pronoun'], ['Name'], ['Noun'], ['Article', 'Noun'], ['Article', 'Adjs', 'Noun'], ['Digit'], ['NP', 'PP'], ['NP', 'RelClause']], 'S': [['NP', 'VP'], ['S', 'Conjunction', 'S']], 'VP': [['Verb'], ['VP', 'NP'], ['VP', 'Adjective'], ['VP', 'PP'], ['VP', 'Adverb']], 'PP': [['Preposition', 'NP']]}\n" + ] + } + ], + "source": [ + "lexicon = Lexicon(\n", + " Verb = \"is | say | are\",\n", + " Noun = \"robot | sheep | fence\",\n", + " Adjective = \"good | new | sad\",\n", + " Adverb = \"here | lightly | now\",\n", + " Pronoun = \"me | you | he\",\n", + " RelPro = \"that | who | which\",\n", + " Name = \"john | mary | peter\",\n", + " Article = \"the | a | an\",\n", + " Preposition = \"to | in | at\",\n", + " Conjunction = \"and | or | but\",\n", + " Digit = \"1 | 2 | 0\"\n", + ")\n", + "\n", + "print(\"Lexicon\", lexicon)\n", + "\n", + "rules = Rules(\n", + " S = \"NP VP | S Conjunction S\",\n", + " NP = \"Pronoun | Name | Noun | Article Noun \\\n", + " | Article Adjs Noun | Digit | NP PP | NP RelClause\",\n", + " VP = \"Verb | VP NP | VP Adjective | VP PP | VP Adverb\",\n", + " Adjs = \"Adjective | Adjective Adjs\",\n", + " PP = \"Preposition NP\",\n", + " RelClause = \"RelPro VP\"\n", + ")\n", + "\n", + "print(\"\\nRules:\", rules)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both the functions return a dictionary with keys the left-hand side of the rules. For the lexicon, the values are the terminals for each left-hand side non-terminal, while for the rules the values are the right-hand sides as lists.\n", + "\n", + "We can now use the variables `lexicon` and `rules` to build a grammar. After we've done so, we can find the transformations of a non-terminal (the `Noun`, `Verb` and the other basic classes do **not** count as proper non-terminals in the implementation). We can also check if a word is in a particular class." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "How can we rewrite 'VP'? [['Verb'], ['VP', 'NP'], ['VP', 'Adjective'], ['VP', 'PP'], ['VP', 'Adverb']]\n", + "Is 'the' an article? True\n", + "Is 'here' a noun? False\n" + ] + } + ], + "source": [ + "grammar = Grammar(\"A Simple Grammar\", rules, lexicon)\n", + "\n", + "print(\"How can we rewrite 'VP'?\", grammar.rewrites_for('VP'))\n", + "print(\"Is 'the' an article?\", grammar.isa('the', 'Article'))\n", + "print(\"Is 'here' a noun?\", grammar.isa('here', 'Noun'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the grammar is in Chomsky Normal Form, we can call the class function `cnf_rules` to get all the rules in the form of `(X, Y, Z)` for each `X -> Y Z` rule. Since the above grammar is not in *CNF* though, we have to create a new one." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "E_Chomsky = Grammar(\"E_Prob_Chomsky\", # A Grammar in Chomsky Normal Form\n", + " Rules(\n", + " S = \"NP VP\",\n", + " NP = \"Article Noun | Adjective Noun\",\n", + " VP = \"Verb NP | Verb Adjective\",\n", + " ),\n", + " Lexicon(\n", + " Article = \"the | a | an\",\n", + " Noun = \"robot | sheep | fence\",\n", + " Adjective = \"good | new | sad\",\n", + " Verb = \"is | say | are\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('S', 'NP', 'VP'), ('VP', 'Verb', 'NP'), ('VP', 'Verb', 'Adjective'), ('NP', 'Article', 'Noun'), ('NP', 'Adjective', 'Noun')]\n" + ] + } + ], + "source": [ + "print(E_Chomsky.cnf_rules())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can generate random phrases using our grammar. Most of them will be complete gibberish, falling under the overgenerated phrases of the grammar. That goes to show that in the grammar the valid phrases are much fewer than the overgenerated ones." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'sheep that say here mary are the sheep at 2'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grammar.generate_random('S')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Probabilistic\n", + "\n", + "The probabilistic grammars follow the same approach. They take as input a string, are assembled from a grammar and a lexicon and can generate random sentences (giving the probability of the sentence). The main difference is that in the lexicon we have tuples (terminal, probability) instead of strings and for the rules we have a list of tuples (list of non-terminals, probability) instead of list of lists of non-terminals.\n", + "\n", + "Execute the cells to read the code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(ProbLexicon, ProbRules, ProbGrammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's build a lexicon and rules for the probabilistic grammar:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lexicon {'Noun': [('robot', 0.4), ('sheep', 0.4), ('fence', 0.2)], 'Name': [('john', 0.4), ('mary', 0.4), ('peter', 0.2)], 'Adverb': [('here', 0.6), ('lightly', 0.1), ('now', 0.3)], 'Digit': [('0', 0.35), ('1', 0.35), ('2', 0.3)], 'Adjective': [('good', 0.5), ('new', 0.2), ('sad', 0.3)], 'Pronoun': [('me', 0.3), ('you', 0.4), ('he', 0.3)], 'Article': [('the', 0.5), ('a', 0.25), ('an', 0.25)], 'Preposition': [('to', 0.4), ('in', 0.3), ('at', 0.3)], 'Verb': [('is', 0.5), ('say', 0.3), ('are', 0.2)], 'Conjunction': [('and', 0.5), ('or', 0.2), ('but', 0.3)], 'RelPro': [('that', 0.5), ('who', 0.3), ('which', 0.2)]}\n", + "\n", + "Rules: {'S': [(['NP', 'VP'], 0.6), (['S', 'Conjunction', 'S'], 0.4)], 'RelClause': [(['RelPro', 'VP'], 1.0)], 'VP': [(['Verb'], 0.3), (['VP', 'NP'], 0.2), (['VP', 'Adjective'], 0.25), (['VP', 'PP'], 0.15), (['VP', 'Adverb'], 0.1)], 'Adjs': [(['Adjective'], 0.5), (['Adjective', 'Adjs'], 0.5)], 'PP': [(['Preposition', 'NP'], 1.0)], 'NP': [(['Pronoun'], 0.2), (['Name'], 0.05), (['Noun'], 0.2), (['Article', 'Noun'], 0.15), (['Article', 'Adjs', 'Noun'], 0.1), (['Digit'], 0.05), (['NP', 'PP'], 0.15), (['NP', 'RelClause'], 0.1)]}\n" + ] + } + ], + "source": [ + "lexicon = ProbLexicon(\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Adverb = \"here [0.6] | lightly [0.1] | now [0.3]\",\n", + " Pronoun = \"me [0.3] | you [0.4] | he [0.3]\",\n", + " RelPro = \"that [0.5] | who [0.3] | which [0.2]\",\n", + " Name = \"john [0.4] | mary [0.4] | peter [0.2]\",\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Preposition = \"to [0.4] | in [0.3] | at [0.3]\",\n", + " Conjunction = \"and [0.5] | or [0.2] | but [0.3]\",\n", + " Digit = \"0 [0.35] | 1 [0.35] | 2 [0.3]\"\n", + ")\n", + "\n", + "print(\"Lexicon\", lexicon)\n", + "\n", + "rules = ProbRules(\n", + " S = \"NP VP [0.6] | S Conjunction S [0.4]\",\n", + " NP = \"Pronoun [0.2] | Name [0.05] | Noun [0.2] | Article Noun [0.15] \\\n", + " | Article Adjs Noun [0.1] | Digit [0.05] | NP PP [0.15] | NP RelClause [0.1]\",\n", + " VP = \"Verb [0.3] | VP NP [0.2] | VP Adjective [0.25] | VP PP [0.15] | VP Adverb [0.1]\",\n", + " Adjs = \"Adjective [0.5] | Adjective Adjs [0.5]\",\n", + " PP = \"Preposition NP [1]\",\n", + " RelClause = \"RelPro VP [1]\"\n", + ")\n", + "\n", + "print(\"\\nRules:\", rules)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use the above to assemble our probabilistic grammar and run some simple queries:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "How can we rewrite 'VP'? [(['Verb'], 0.3), (['VP', 'NP'], 0.2), (['VP', 'Adjective'], 0.25), (['VP', 'PP'], 0.15), (['VP', 'Adverb'], 0.1)]\n", + "Is 'the' an article? True\n", + "Is 'here' a noun? False\n" + ] + } + ], + "source": [ + "grammar = ProbGrammar(\"A Simple Probabilistic Grammar\", rules, lexicon)\n", + "\n", + "print(\"How can we rewrite 'VP'?\", grammar.rewrites_for('VP'))\n", + "print(\"Is 'the' an article?\", grammar.isa('the', 'Article'))\n", + "print(\"Is 'here' a noun?\", grammar.isa('here', 'Noun'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we have a grammar in *CNF*, we can get a list of all the rules. Let's create a grammar in the form and print the *CNF* rules:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "E_Prob_Chomsky = ProbGrammar(\"E_Prob_Chomsky\", # A Probabilistic Grammar in CNF\n", + " ProbRules(\n", + " S = \"NP VP [1]\",\n", + " NP = \"Article Noun [0.6] | Adjective Noun [0.4]\",\n", + " VP = \"Verb NP [0.5] | Verb Adjective [0.5]\",\n", + " ),\n", + " ProbLexicon(\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('S', 'NP', 'VP', 1.0), ('VP', 'Verb', 'NP', 0.5), ('VP', 'Verb', 'Adjective', 0.5), ('NP', 'Article', 'Noun', 0.6), ('NP', 'Adjective', 'Noun', 0.4)]\n" + ] + } + ], + "source": [ + "print(E_Prob_Chomsky.cnf_rules())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, we can generate random sentences from this grammar. The function `prob_generation` returns a tuple (sentence, probability)." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "an good sad sheep to 1 is\n", + "3.54375e-08\n" + ] + } + ], + "source": [ + "sentence, prob = grammar.generate_random('S')\n", + "print(sentence)\n", + "print(prob)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with the non-probabilistic grammars, this one mostly overgenerates. You can also see that the probability is very, very low, which means there are a ton of generateable sentences (in this case infinite, since we have recursion; notice how `VP` can produce another `VP`, for example)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HITS\n", + "\n", + "### Overview\n", + "\n", + "**Hyperlink-Induced Topic Search** (or HITS for short) is an algorithm for information retrieval and page ranking. You can read more on information retrieval in the [text notebook](https://github.com/aimacode/aima-python/blob/master/text.ipynb). Essentially, given a collection of documents and a user's query, such systems return to the user the documents most relevant to what the user needs. The HITS algorithm differs from a lot of other similar ranking algorithms (like Google's *Pagerank*) as the page ratings in this algorithm are dependent on the given query. This means that for each new query the result pages must be computed anew. This cost might be prohibitive for many modern search engines, so a lot steer away from this approach.\n", + "\n", + "HITS first finds a list of relevant pages to the query and then adds pages that link to or are linked from these pages. Once the set is built, we define two values for each page. **Authority** on the query, the degree of pages from the relevant set linking to it and **hub** of the query, the degree that it points to authoritative pages in the set. Since we do not want to simply count the number of links from a page to other pages, but we also want to take into account the quality of the linked pages, we update the hub and authority values of a page in the following manner, until convergence:\n", + "\n", + "* Hub score = The sum of the authority scores of the pages it links to.\n", + "\n", + "* Authority score = The sum of hub scores of the pages it is linked from.\n", + "\n", + "So the higher quality the pages a page is linked to and from, the higher its scores.\n", + "\n", + "We then normalize the scores by dividing each score by the sum of the squares of the respective scores of all pages. When the values converge, we return the top-valued pages. Note that because we normalize the values, the algorithm is guaranteed to converge." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### Implementation\n", + "\n", + "The source code for the algorithm is given below:" + ] + }, { "cell_type": "code", "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(HITS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we compile the collection of pages as mentioned above. Then, we initialize the authority and hub scores for each page and finally we update and normalize the values until convergence.\n", + "\n", + "A quick overview of the helper functions functions we use:\n", + "\n", + "* `relevant_pages`: Returns relevant pages from `pagesIndex` given a query.\n", + "\n", + "* `expand_pages`: Adds to the collection pages linked to and from the given `pages`.\n", + "\n", + "* `normalize`: Normalizes authority and hub scores.\n", + "\n", + "* `ConvergenceDetector`: A class that checks for convergence, by keeping a history of the pages' scores and checking if they change or not.\n", + "\n", + "* `Page`: The template for pages. Stores the address, authority/hub scores and in-links/out-links." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### Example\n", + "\n", + "Before we begin we need to define a list of sample pages to work on. The pages are `pA`, `pB` and so on and their text is given by `testHTML` and `testHTML2`. The `Page` class takes as arguments the in-links and out-links as lists. For page \"A\", the in-links are \"B\", \"C\" and \"E\" while the sole out-link is \"D\".\n", + "\n", + "We also need to set the `nlp` global variables `pageDict`, `pagesIndex` and `pagesContent`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "testHTML = \"\"\"Like most other male mammals, a man inherits an\n", + " X from his mom and a Y from his dad.\"\"\"\n", + "testHTML2 = \"a mom and a dad\"\n", + "\n", + "pA = Page('A', ['B', 'C', 'E'], ['D'])\n", + "pB = Page('B', ['E'], ['A', 'C', 'D'])\n", + "pC = Page('C', ['B', 'E'], ['A', 'D'])\n", + "pD = Page('D', ['A', 'B', 'C', 'E'], [])\n", + "pE = Page('E', [], ['A', 'B', 'C', 'D', 'F'])\n", + "pF = Page('F', ['E'], [])\n", + "\n", + "nlp.pageDict = {pA.address: pA, pB.address: pB, pC.address: pC,\n", + " pD.address: pD, pE.address: pE, pF.address: pF}\n", + "\n", + "nlp.pagesIndex = nlp.pageDict\n", + "\n", + "nlp.pagesContent ={pA.address: testHTML, pB.address: testHTML2,\n", + " pC.address: testHTML, pD.address: testHTML2,\n", + " pE.address: testHTML, pF.address: testHTML2}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now run the HITS algorithm. Our query will be 'mammals' (note that while the content of the HTML doesn't matter, it should include the query words or else no page will be picked at the first step)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, "metadata": { - "collapsed": false + "collapsed": true }, "outputs": [], "source": [ - "import nlp" + "HITS('mammals')\n", + "page_list = ['A', 'B', 'C', 'D', 'E', 'F']\n", + "auth_list = [pA.authority, pB.authority, pC.authority, pD.authority, pE.authority, pF.authority]\n", + "hub_list = [pA.hub, pB.hub, pC.hub, pD.hub, pE.hub, pF.hub]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how the pages were scored:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "A: total=0.7696163397038682, auth=0.5583254178509696, hub=0.2112909218528986\n", + "B: total=0.7795962360479536, auth=0.23657856688600404, hub=0.5430176691619495\n", + "C: total=0.8204496913590655, auth=0.4211098490570872, hub=0.3993398423019784\n", + "D: total=0.6316647735856309, auth=0.6316647735856309, hub=0.0\n", + "E: total=0.7078245882072104, auth=0.0, hub=0.7078245882072104\n", + "F: total=0.23657856688600404, auth=0.23657856688600404, hub=0.0\n" + ] + } + ], + "source": [ + "for i in range(6):\n", + " p = page_list[i]\n", + " a = auth_list[i]\n", + " h = hub_list[i]\n", + " \n", + " print(\"{}: total={}, auth={}, hub={}\".format(p, a + h, a, h))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "The top score is 0.82 by \"C\". This is the most relevant page according to the algorithm. You can see that the pages it links to, \"A\" and \"D\", have the two highest authority scores (therefore \"C\" has a high hub score) and the pages it is linked from, \"B\" and \"E\", have the highest hub scores (so \"C\" has a high authority score). By combining these two facts, we get that \"C\" is the most relevant page. It is worth noting that it does not matter if the given page contains the query words, just that it links and is linked from high-quality pages." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## QUESTION ANSWERING\n", + "\n", + "**Question Answering** is a type of Information Retrieval system, where we have a question instead of a query and instead of relevant documents we want the computer to return a short sentence, phrase or word that answers our question. To better understand the concept of question answering systems, you can first read the \"Text Models\" and \"Information Retrieval\" section from the [text notebook](https://github.com/aimacode/aima-python/blob/master/text.ipynb).\n", + "\n", + "A typical example of such a system is `AskMSR` (Banko *et al.*, 2002), a system for question answering that performed admirably against more sophisticated algorithms. The basic idea behind it is that a lot of questions have already been answered in the web numerous times. The system doesn't know a lot about verbs, or concepts or even what a noun is. It knows about 15 different types of questions and how they can be written as queries. It can rewrite [Who was George Washington's second in command?] as the query [\\* was George Washington's second in command] or [George Washington's second in command was \\*].\n", + "\n", + "After rewriting the questions, it issues these queries and retrieves the short text around the query terms. It then breaks the result into 1, 2 or 3-grams. Filters are also applied to increase the chances of a correct answer. If the query starts with \"who\", we filter for names, if it starts with \"how many\" we filter for numbers and so on. We can also filter out the words appearing in the query. For the above query, the answer \"George Washington\" is wrong, even though it is quite possible the 2-gram would appear a lot around the query terms.\n", + "\n", + "Finally, the different results are weighted by the generality of the queries. The result from the general boolean query [George Washington OR second in command] weighs less that the more specific query [George Washington's second in command was \\*]. As an answer we return the most highly-ranked n-gram." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CYK PARSE\n", + "\n", + "### Overview\n", + "\n", + "Syntactic analysis (or **parsing**) of a sentence is the process of uncovering the phrase structure of the sentence according to the rules of a grammar. There are two main approaches to parsing. *Top-down*, start with the starting symbol and build a parse tree with the given words as its leaves, and *bottom-up*, where we start from the given words and build a tree that has the starting symbol as its root. Both approaches involve \"guessing\" ahead, so it is very possible it will take long to parse a sentence (wrong guess mean a lot of backtracking). Thankfully, a lot of effort is spent in analyzing already analyzed substrings, so we can follow a dynamic programming approach to store and reuse these parses instead of recomputing them. The *CYK Parsing Algorithm* (named after its inventors, Cocke, Younger and Kasami) utilizes this technique to parse sentences of a grammar in *Chomsky Normal Form*.\n", + "\n", + "The CYK algorithm returns an *M x N x N* array (named *P*), where *N* is the number of words in the sentence and *M* the number of non-terminal symbols in the grammar. Each element in this array shows the probability of a substring being transformed from a particular non-terminal. To find the most probable parse of the sentence, a search in the resulting array is required. Search heuristic algorithms work well in this space, and we can derive the heuristics from the properties of the grammar.\n", + "\n", + "The algorithm in short works like this: There is an external loop that determines the length of the substring. Then the algorithm loops through the words in the sentence. For each word, it again loops through all the words to its right up to the first-loop length. The substring it will work on in this iteration is the words from the second-loop word with first-loop length. Finally, it loops through all the rules in the grammar and updates the substring's probability for each right-hand side non-terminal." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "The implementation takes as input a list of words and a probabilistic grammar (from the `ProbGrammar` class detailed above) in CNF and returns the table/dictionary *P*. An item's key in *P* is a tuple in the form `(Non-terminal, start of substring, length of substring)`, and the value is a probability. For example, for the sentence \"the monkey is dancing\" and the substring \"the monkey\" an item can be `('NP', 0, 2): 0.5`, which means the first two words (the substring from index 0 and length 2) have a 0.5 probablity of coming from the `NP` terminal.\n", + "\n", + "Before we continue, you can take a look at the source code by running the cell below:" ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(CYK_parse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When updating the probability of a substring, we pick the max of its current one and the probability of the substring broken into two parts: one from the second-loop word with third-loop length, and the other from the first part's end to the remainer of the first-loop length." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Let's build a probabilistic grammar in CNF:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, "metadata": { "collapsed": true }, "outputs": [], - "source": [] + "source": [ + "E_Prob_Chomsky = ProbGrammar(\"E_Prob_Chomsky\", # A Probabilistic Grammar in CNF\n", + " ProbRules(\n", + " S = \"NP VP [1]\",\n", + " NP = \"Article Noun [0.6] | Adjective Noun [0.4]\",\n", + " VP = \"Verb NP [0.5] | Verb Adjective [0.5]\",\n", + " ),\n", + " ProbLexicon(\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\"\n", + " ))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see the probabilities table for the sentence \"the robot is good\":" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "defaultdict(, {('Adjective', 1, 1): 0.0, ('NP', 0, 3): 0.0, ('Verb', 1, 1): 0.0, ('NP', 0, 2): 0.12, ('S', 1, 2): 0.0, ('Article', 2, 1): 0.0, ('NP', 3, 1): 0.0, ('S', 1, 3): 0.0, ('Adjective', 1, 3): 0.0, ('VP', 0, 4): 0.0, ('Article', 0, 3): 0.0, ('Adjective', 1, 2): 0.0, ('Verb', 1, 2): 0.0, ('Adjective', 0, 2): 0.0, ('Article', 0, 1): 0.5, ('VP', 1, 1): 0.0, ('Verb', 0, 2): 0.0, ('Adjective', 0, 3): 0.0, ('VP', 1, 2): 0.0, ('Verb', 0, 3): 0.0, ('NP', 2, 2): 0.0, ('S', 2, 2): 0.0, ('NP', 1, 3): 0.0, ('VP', 1, 3): 0.0, ('Adjective', 3, 1): 0.5, ('Adjective', 0, 1): 0.0, ('NP', 1, 2): 0.0, ('Verb', 0, 1): 0.0, ('S', 0, 3): 0.0, ('NP', 1, 1): 0.0, ('NP', 2, 1): 0.0, ('S', 0, 2): 0.0, ('Noun', 1, 2): 0.0, ('S', 0, 4): 0.015, ('Noun', 1, 3): 0.0, ('Noun', 3, 1): 0.0, ('Noun', 2, 2): 0.0, ('NP', 0, 4): 0.0, ('VP', 2, 2): 0.125, ('Noun', 2, 1): 0.0, ('Noun', 1, 1): 0.4, ('VP', 0, 3): 0.0, ('Article', 1, 2): 0.0, ('Article', 1, 1): 0.0, ('VP', 2, 1): 0.0, ('Adjective', 2, 1): 0.0, ('Verb', 2, 1): 0.5, ('Adjective', 2, 2): 0.0, ('VP', 3, 1): 0.0, ('NP', 0, 1): 0.0, ('VP', 0, 2): 0.0, ('Article', 0, 2): 0.0})\n" + ] + } + ], + "source": [ + "words = ['the', 'robot', 'is', 'good']\n", + "grammar = E_Prob_Chomsky\n", + "\n", + "P = CYK_parse(words, grammar)\n", + "print(P)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A `defaultdict` object is returned (`defaultdict` is basically a dictionary but with a default value/type). Keys are tuples in the form mentioned above and the values are the corresponding probabilities. Most of the items/parses have a probability of 0. Let's filter those out to take a better look at the parses that matter." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{('Noun', 1, 1): 0.4, ('VP', 2, 2): 0.125, ('Adjective', 3, 1): 0.5, ('S', 0, 4): 0.015, ('Article', 0, 1): 0.5, ('NP', 0, 2): 0.12, ('Verb', 2, 1): 0.5}\n" + ] + } + ], + "source": [ + "parses = {k: p for k, p in P.items() if p >0}\n", + "\n", + "print(parses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The item `('Article', 0, 1): 0.5` means that the first item came from the `Article` non-terminal with a chance of 0.5. A more complicated item, one with two words, is `('NP', 0, 2): 0.12` which covers the first two words. The probability of the substring \"the robot\" coming from the `NP` non-terminal is 0.12. Let's try and follow the transformations from `NP` to the given words (top-down) to make sure this is indeed the case:\n", + "\n", + "1. The probability of `NP` transforming to `Article Noun` is 0.6.\n", + "\n", + "2. The probability of `Article` transforming to \"the\" is 0.5 (total probability = 0.6*0.5 = 0.3).\n", + "\n", + "3. The probability of `Noun` transforming to \"robot\" is 0.4 (total = 0.3*0.4 = 0.12).\n", + "\n", + "Thus, the total probability of the transformation is 0.12.\n", + "\n", + "Notice how the probability for the whole string (given by the key `('S', 0, 4)`) is 0.015. This means the most probable parsing of the sentence has a probability of 0.015." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CHART PARSING\n", + "\n", + "### Overview\n", + "\n", + "Let's now take a look at a more general chart parsing algorithm. Given a non-probabilistic grammar and a sentence, this algorithm builds a parse tree in a top-down manner, with the words of the sentence as the leaves. It works with a dynamic programming approach, building a chart to store parses for substrings so that it doesn't have to analyze them again (just like the CYK algorithm). Each non-terminal, starting from S, gets replaced by its right-hand side rules in the chart, until we end up with the correct parses.\n", + "\n", + "### Implementation\n", + "\n", + "A parse is in the form `[start, end, non-terminal, sub-tree, expected-transformation]`, where `sub-tree` is a tree with the corresponding `non-terminal` as its root and `expected-transformation` is a right-hand side rule of the `non-terminal`.\n", + "\n", + "The chart parsing is implemented in a class, `Chart`. It is initialized with a grammar and can return the list of all the parses of a sentence with the `parses` function.\n", + "\n", + "The chart is a list of lists. The lists correspond to the lengths of substrings (including the empty string), from start to finish. When we say 'a point in the chart', we refer to a list of a certain length.\n", + "\n", + "A quick rundown of the class functions:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "* `parses`: Returns a list of parses for a given sentence. If the sentence can't be parsed, it will return an empty list. Initializes the process by calling `parse` from the starting symbol.\n", + "\n", + "\n", + "* `parse`: Parses the list of words and builds the chart.\n", + "\n", + "\n", + "* `add_edge`: Adds another edge to the chart at a given point. Also, examines whether the edge extends or predicts another edge. If the edge itself is not expecting a transformation, it will extend other edges and it will predict edges otherwise.\n", + "\n", + "\n", + "* `scanner`: Given a word and a point in the chart, it extends edges that were expecting a transformation that can result in the given word. For example, if the word 'the' is an 'Article' and we are examining two edges at a chart's point, with one expecting an 'Article' and the other a 'Verb', the first one will be extended while the second one will not.\n", + "\n", + "\n", + "* `predictor`: If an edge can't extend other edges (because it is expecting a transformation itself), we will add to the chart rules/transformations that can help extend the edge. The new edges come from the right-hand side of the expected transformation's rules. For example, if an edge is expecting the transformation 'Adjective Noun', we will add to the chart an edge for each right-hand side rule of the non-terminal 'Adjective'.\n", + "\n", + "\n", + "* `extender`: Extends edges given an edge (called `E`). If `E`'s non-terminal is the same as the expected transformation of another edge (let's call it `A`), add to the chart a new edge with the non-terminal of `A` and the transformations of `A` minus the non-terminal that matched with `E`'s non-terminal. For example, if an edge `E` has 'Article' as its non-terminal and is expecting no transformation, we need to see what edges it can extend. Let's examine the edge `N`. This expects a transformation of 'Noun Verb'. 'Noun' does not match with 'Article', so we move on. Another edge, `A`, expects a transformation of 'Article Noun' and has a non-terminal of 'NP'. We have a match! A new edge will be added with 'NP' as its non-terminal (the non-terminal of `A`) and 'Noun' as the expected transformation (the rest of the expected transformation of `A`)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can view the source code by running the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(Chart)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "We will use the grammar `E0` to parse the sentence \"the stench is in 2 2\".\n", + "\n", + "First we need to build a `Chart` object:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "chart = Chart(nlp.E0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then we simply call the `parses` function:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[0, 6, 'S', [[0, 2, 'NP', [('Article', 'the'), ('Noun', 'stench')], []], [2, 6, 'VP', [[2, 3, 'VP', [('Verb', 'is')], []], [3, 6, 'PP', [('Preposition', 'in'), [4, 6, 'NP', [('Digit', '2'), ('Digit', '2')], []]], []]], []]], []]]\n" + ] + } + ], + "source": [ + "print(chart.parses('the stench is in 2 2'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see which edges get added by setting the optional initialization argument `trace` to true." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chart_trace = Chart(nlp.E0, trace=True)\n", + "chart_trace.parses('the stench is in 2 2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try and parse a sentence that is not recognized by the grammar:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n" + ] + } + ], + "source": [ + "print(chart.parses('the stench 2 2'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An empty list was returned." + ] } ], "metadata": { @@ -37,9 +1034,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.5.2" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/nlp.py b/nlp.py index 7273b98da..03aabf54b 100644 --- a/nlp.py +++ b/nlp.py @@ -1,12 +1,11 @@ -"""A chart parser and some grammars. (Chapter 22)""" - -# (Written for the second edition of AIMA; expect some discrepanciecs -# from the third edition until this gets reviewed.) +"""Natural Language Processing; Chart Parsing and PageRanking (Chapter 22-23)""" from collections import defaultdict +from utils import weighted_choice import urllib.request import re + # ______________________________________________________________________________ # Grammars and Lexicons @@ -23,8 +22,8 @@ def Rules(**rules): def Lexicon(**rules): """Create a dictionary mapping symbols to alternative words. - >>> Lexicon(Art = "the | a | an") - {'Art': ['the', 'a', 'an']} + >>> Lexicon(Article = "the | a | an") + {'Article': ['the', 'a', 'an']} """ for (lhs, rhs) in rules.items(): rules[lhs] = [word.strip() for word in rhs.split('|')] @@ -34,7 +33,7 @@ def Lexicon(**rules): class Grammar: def __init__(self, name, rules, lexicon): - "A grammar has a set of rules and a lexicon." + """A grammar has a set of rules and a lexicon.""" self.name = name self.rules = rules self.lexicon = lexicon @@ -44,29 +43,149 @@ def __init__(self, name, rules, lexicon): self.categories[word].append(lhs) def rewrites_for(self, cat): - "Return a sequence of possible rhs's that cat can be rewritten as." + """Return a sequence of possible rhs's that cat can be rewritten as.""" return self.rules.get(cat, ()) def isa(self, word, cat): - "Return True iff word is of category cat" + """Return True iff word is of category cat""" return cat in self.categories[word] + def cnf_rules(self): + """Returns the tuple (X, Y, Z) for rules in the form: + X -> Y Z""" + cnf = [] + for X, rules in self.rules.items(): + for (Y, Z) in rules: + cnf.append((X, Y, Z)) + + return cnf + + def generate_random(self, S='S'): + """Replace each token in S by a random entry in grammar (recursively).""" + import random + + def rewrite(tokens, into): + for token in tokens: + if token in self.rules: + rewrite(random.choice(self.rules[token]), into) + elif token in self.lexicon: + into.append(random.choice(self.lexicon[token])) + else: + into.append(token) + return into + + return ' '.join(rewrite(S.split(), [])) + def __repr__(self): - return '' % self.name + return ''.format(self.name) + + +def ProbRules(**rules): + """Create a dictionary mapping symbols to alternative sequences, + with probabilities. + >>> ProbRules(A = "B C [0.3] | D E [0.7]") + {'A': [(['B', 'C'], 0.3), (['D', 'E'], 0.7)]} + """ + for (lhs, rhs) in rules.items(): + rules[lhs] = [] + rhs_separate = [alt.strip().split() for alt in rhs.split('|')] + for r in rhs_separate: + prob = float(r[-1][1:-1]) # remove brackets, convert to float + rhs_rule = (r[:-1], prob) + rules[lhs].append(rhs_rule) + + return rules + + +def ProbLexicon(**rules): + """Create a dictionary mapping symbols to alternative words, + with probabilities. + >>> ProbLexicon(Article = "the [0.5] | a [0.25] | an [0.25]") + {'Article': [('the', 0.5), ('a', 0.25), ('an', 0.25)]} + """ + for (lhs, rhs) in rules.items(): + rules[lhs] = [] + rhs_separate = [word.strip().split() for word in rhs.split('|')] + for r in rhs_separate: + prob = float(r[-1][1:-1]) # remove brackets, convert to float + word = r[:-1][0] + rhs_rule = (word, prob) + rules[lhs].append(rhs_rule) + + return rules + + +class ProbGrammar: + + def __init__(self, name, rules, lexicon): + """A grammar has a set of rules and a lexicon. + Each rule has a probability.""" + self.name = name + self.rules = rules + self.lexicon = lexicon + self.categories = defaultdict(list) + + for lhs in lexicon: + for word, prob in lexicon[lhs]: + self.categories[word].append((lhs, prob)) + + def rewrites_for(self, cat): + """Return a sequence of possible rhs's that cat can be rewritten as.""" + return self.rules.get(cat, ()) + + def isa(self, word, cat): + """Return True iff word is of category cat""" + return cat in [c for c, _ in self.categories[word]] + + def cnf_rules(self): + """Returns the tuple (X, Y, Z, p) for rules in the form: + X -> Y Z [p]""" + cnf = [] + for X, rules in self.rules.items(): + for (Y, Z), p in rules: + cnf.append((X, Y, Z, p)) + + return cnf + + def generate_random(self, S='S'): + """Replace each token in S by a random entry in grammar (recursively). + Returns a tuple of (sentence, probability).""" + import random + + def rewrite(tokens, into): + for token in tokens: + if token in self.rules: + non_terminal, prob = weighted_choice(self.rules[token]) + into[1] *= prob + rewrite(non_terminal, into) + elif token in self.lexicon: + terminal, prob = weighted_choice(self.lexicon[token]) + into[0].append(terminal) + into[1] *= prob + else: + into[0].append(token) + return into + + rewritten_as, prob = rewrite(S.split(), [[], 1]) + return (' '.join(rewritten_as), prob) + + def __repr__(self): + return ''.format(self.name) + E0 = Grammar('E0', Rules( # Grammar for E_0 [Figure 22.4] S='NP VP | S Conjunction S', - NP='Pronoun | Name | Noun | Article Noun | Digit Digit | NP PP | NP RelClause', # noqa + 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 [Figure 22.3] - Noun="stench | breeze | glitter | nothing | wumpus | pit | pits | gold | east", # noqa + Noun="stench | breeze | glitter | nothing | wumpus | pit | pits | gold | east", Verb="is | see | smell | shoot | fell | stinks | go | grab | carry | kill | turn | feel", # noqa Adjective="right | left | east | south | back | smelly", - Adverb="here | there | nearby | ahead | right | left | east | south | back", # noqa + Adverb="here | there | nearby | ahead | right | left | east | south | back", Pronoun="me | you | I | it", Name="John | Mary | Boston | Aristotle", Article="the | a | an", @@ -89,37 +208,81 @@ def __repr__(self): V='saw | liked | feel' )) -E_NP_ = Grammar('E_NP_', # another trivial grammar for testing +E_NP_ = Grammar('E_NP_', # Another Trivial Grammar for testing Rules(NP='Adj NP | N'), Lexicon(Adj='happy | handsome | hairy', N='man')) +E_Prob = ProbGrammar('E_Prob', # The Probabilistic Grammar from the notebook + ProbRules( + S="NP VP [0.6] | S Conjunction S [0.4]", + NP="Pronoun [0.2] | Name [0.05] | Noun [0.2] | Article Noun [0.15] \ + | Article Adjs Noun [0.1] | Digit [0.05] | NP PP [0.15] | NP RelClause [0.1]", + VP="Verb [0.3] | VP NP [0.2] | VP Adjective [0.25] | VP PP [0.15] | VP Adverb [0.1]", + Adjs="Adjective [0.5] | Adjective Adjs [0.5]", + PP="Preposition NP [1]", + RelClause="RelPro VP [1]" + ), + ProbLexicon( + Verb="is [0.5] | say [0.3] | are [0.2]", + Noun="robot [0.4] | sheep [0.4] | fence [0.2]", + Adjective="good [0.5] | new [0.2] | sad [0.3]", + Adverb="here [0.6] | lightly [0.1] | now [0.3]", + Pronoun="me [0.3] | you [0.4] | he [0.3]", + RelPro="that [0.5] | who [0.3] | which [0.2]", + Name="john [0.4] | mary [0.4] | peter [0.2]", + Article="the [0.5] | a [0.25] | an [0.25]", + Preposition="to [0.4] | in [0.3] | at [0.3]", + Conjunction="and [0.5] | or [0.2] | but [0.3]", + Digit="0 [0.35] | 1 [0.35] | 2 [0.3]" + )) + +E_Chomsky = Grammar('E_Prob_Chomsky', # A Grammar in Chomsky Normal Form + Rules( + S='NP VP', + NP='Article Noun | Adjective Noun', + VP='Verb NP | Verb Adjective', + ), + Lexicon( + Article='the | a | an', + Noun='robot | sheep | fence', + Adjective='good | new | sad', + Verb='is | say | are' + )) + +E_Prob_Chomsky = ProbGrammar('E_Prob_Chomsky', # A Probabilistic Grammar in CNF + ProbRules( + S='NP VP [1]', + NP='Article Noun [0.6] | Adjective Noun [0.4]', + VP='Verb NP [0.5] | Verb Adjective [0.5]', + ), + ProbLexicon( + Article='the [0.5] | a [0.25] | an [0.25]', + Noun='robot [0.4] | sheep [0.4] | fence [0.2]', + Adjective='good [0.5] | new [0.2] | sad [0.3]', + Verb='is [0.5] | say [0.3] | are [0.2]' + )) +E_Prob_Chomsky_ = ProbGrammar('E_Prob_Chomsky_', + ProbRules( + S='NP VP [1]', + NP='NP PP [0.4] | Noun Verb [0.6]', + PP='Preposition NP [1]', + VP='Verb NP [0.7] | VP PP [0.3]', + ), + ProbLexicon( + Noun='astronomers [0.18] | eyes [0.32] | stars [0.32] | telescopes [0.18]', + Verb='saw [0.5] | \'\' [0.5]', + Preposition='with [1]' + )) -def generate_random(grammar=E_, s='S'): - """Replace each token in s by a random entry in grammar (recursively). - This is useful for testing a grammar, e.g. generate_random(E_)""" - import random - - def rewrite(tokens, into): - for token in tokens: - if token in grammar.rules: - rewrite(random.choice(grammar.rules[token]), into) - elif token in grammar.lexicon: - into.append(random.choice(grammar.lexicon[token])) - else: - into.append(token) - return into - - return ' '.join(rewrite(s.split(), [])) # ______________________________________________________________________________ # Chart Parsing class Chart: - - """Class for parsing sentences using a chart data structure. [Figure 22.7] - >>> chart = Chart(E0); + """Class for parsing sentences using a chart data structure. + >>> chart = Chart(E0) >>> len(chart.parses('the stench is in 2 2')) 1 """ @@ -146,32 +309,32 @@ def parses(self, words, S='S'): def parse(self, words, S='S'): """Parse a list of words; according to the grammar. Leave results in the chart.""" - self.chart = [[] for i in range(len(words)+1)] + self.chart = [[] for i in range(len(words) + 1)] self.add_edge([0, 0, 'S_', [], [S]]) for i in range(len(words)): self.scanner(i, words[i]) return self.chart def add_edge(self, edge): - "Add edge to chart, and see if it extends or predicts another edge." + """Add edge to chart, and see if it extends or predicts another edge.""" start, end, lhs, found, expects = edge if edge not in self.chart[end]: self.chart[end].append(edge) if self.trace: - print('Chart: added %s' % (edge,)) + print('Chart: added {}'.format(edge)) if not expects: self.extender(edge) else: self.predictor(edge) def scanner(self, j, word): - "For each edge expecting a word of this category here, extend the edge." # noqa + """For each edge expecting a word of this category here, extend the edge.""" for (i, j, A, alpha, Bb) in self.chart[j]: if Bb and self.grammar.isa(word, Bb[0]): - self.add_edge([i, j+1, A, alpha + [(Bb[0], word)], Bb[1:]]) + self.add_edge([i, j + 1, A, alpha + [(Bb[0], word)], Bb[1:]]) def predictor(self, edge): - "Add to chart any rules for B that could help extend this edge." + """Add to chart any rules for B that could help extend this edge.""" (i, j, A, alpha, Bb) = edge B = Bb[0] if B in self.grammar.rules: @@ -179,7 +342,7 @@ def predictor(self, edge): self.add_edge([j, j, B, [], rhs]) def extender(self, edge): - "See what edges can be extended by this edge." + """See what edges can be extended by this edge.""" (j, k, B, _, _) = edge for (i, j, A, alpha, B1b) in self.chart[j]: if B1b and B == B1b[0]: @@ -190,23 +353,26 @@ def extender(self, edge): # CYK Parsing def CYK_parse(words, grammar): - "[Figure 23.5]" + """ [Figure 23.5] """ # We use 0-based indexing instead of the book's 1-based. N = len(words) P = defaultdict(float) + # Insert lexical rules for each word. for (i, word) in enumerate(words): - for (X, p) in grammar.categories[word]: # XXX grammar.categories needs changing, above + for (X, p) in grammar.categories[word]: P[X, i, 1] = p + # Combine first and second parts of right-hand sides of rules, # from short to long. - for length in range(2, N+1): - for start in range(N-length+1): - for len1 in range(1, length): # N.B. the book incorrectly has N instead of length + for length in range(2, N + 1): + for start in range(N - length + 1): + for len1 in range(1, length): # N.B. the book incorrectly has N instead of length len2 = length - len1 - for (X, Y, Z, p) in grammar.cnf_rules(): # XXX grammar needs this method + for (X, Y, Z, p) in grammar.cnf_rules(): P[X, start, length] = max(P[X, start, length], - P[Y, start, len1] * P[Z, start+len1, len2] * p) + P[Y, start, len1] * P[Z, start + len1, len2] * p) + return P @@ -215,17 +381,18 @@ def CYK_parse(words, grammar): # First entry in list is the base URL, and then following are relative URL pages examplePagesSet = ["https://en.wikipedia.org/wiki/", "Aesthetics", "Analytic_philosophy", - "Ancient_Greek", "Aristotle", "Astrology","Atheism", "Baruch_Spinoza", + "Ancient_Greek", "Aristotle", "Astrology", "Atheism", "Baruch_Spinoza", "Belief", "Betrand Russell", "Confucius", "Consciousness", "Continental Philosophy", "Dialectic", "Eastern_Philosophy", "Epistemology", "Ethics", "Existentialism", "Friedrich_Nietzsche", "Idealism", "Immanuel_Kant", "List_of_political_philosophers", "Logic", "Metaphysics", "Philosophers", "Philosophy", "Philosophy_of_mind", "Physics", - "Plato", "Political_philosophy", "Pythagoras", "Rationalism","Social_philosophy", - "Socrates", "Subjectivity", "Theology", "Truth", "Western_philosophy"] + "Plato", "Political_philosophy", "Pythagoras", "Rationalism", + "Social_philosophy", "Socrates", "Subjectivity", "Theology", + "Truth", "Western_philosophy"] -def loadPageHTML( addressList ): +def loadPageHTML(addressList): """Download HTML page content for every URL address passed as argument""" contentDict = {} for addr in addressList: @@ -236,20 +403,23 @@ def loadPageHTML( addressList ): contentDict[addr] = html return contentDict -def initPages( addressList ): + +def initPages(addressList): """Create a dictionary of pages from a list of URL addresses""" pages = {} for addr in addressList: pages[addr] = Page(addr) return pages -def stripRawHTML( raw_html ): + +def stripRawHTML(raw_html): """Remove the section of the HTML which contains links to stylesheets etc., and remove all other unnessecary HTML""" # TODO: Strip more out of the raw html - return re.sub(".*?", "", raw_html, flags=re.DOTALL) # remove section + return re.sub(".*?", "", raw_html, flags=re.DOTALL) # remove section -def determineInlinks( page ): + +def determineInlinks(page): """Given a set of pages that have their outlinks determined, we can fill out a page's inlinks by looking through all other page's outlinks""" inlinks = [] @@ -260,28 +430,30 @@ def determineInlinks( page ): inlinks.append(addr) return inlinks -def findOutlinks( page, handleURLs=None ): + +def findOutlinks(page, handleURLs=None): """Search a page's HTML content for URL links to other pages""" urls = re.findall(r'href=[\'"]?([^\'" >]+)', pagesContent[page.address]) if handleURLs: urls = handleURLs(urls) return urls -def onlyWikipediaURLS( urls ): + +def onlyWikipediaURLS(urls): """Some example HTML page data is from wikipedia. This function converts relative wikipedia links to full wikipedia URLs""" wikiURLs = [url for url in urls if url.startswith('/wiki/')] - return ["https://en.wikipedia.org"+url for url in wikiURLs] + return ["https://en.wikipedia.org" + url for url in wikiURLs] # ______________________________________________________________________________ # HITS Helper Functions -def expand_pages( pages ): - """From Textbook: adds in every page that links to or is linked from one of +def expand_pages(pages): + """Adds in every page that links to or is linked from one of the relevant pages.""" expanded = {} - for addr,page in pages.items(): + for addr, page in pages.items(): if addr not in expanded: expanded[addr] = page for inlink in page.inlinks: @@ -292,30 +464,37 @@ def expand_pages( pages ): expanded[outlink] = pagesIndex[outlink] return expanded + def relevant_pages(query): - """relevant pages are pages that contain the query in its entireity. - If a page's content contains the query it is returned by the function""" - relevant = {} - print("pagesContent in function: ", pagesContent) - for addr, page in pagesIndex.items(): - if query.lower() in pagesContent[addr].lower(): - relevant[addr] = page - return relevant - -def normalize( pages ): - """From the pseudocode: Normalize divides each page's score by the sum of - the squares of all pages' scores (separately for both the authority and hubs scores). + """Relevant pages are pages that contain all of the query words. They are obtained by + intersecting the hit lists of the query words.""" + hit_intersection = {addr for addr in pagesIndex} + query_words = query.split() + for query_word in query_words: + hit_list = set() + for addr in pagesIndex: + if query_word.lower() in pagesContent[addr].lower(): + hit_list.add(addr) + hit_intersection = hit_intersection.intersection(hit_list) + return {addr: pagesIndex[addr] for addr in hit_intersection} + + +def normalize(pages): + """Normalize divides each page's score by the sum of the squares of all + pages' scores (separately for both the authority and hub scores). """ - summed_hub = sum(page.hub**2 for _,page in pages.items()) - summed_auth = sum(page.authority**2 for _,page in pages.items()) + summed_hub = sum(page.hub ** 2 for _, page in pages.items()) + summed_auth = sum(page.authority ** 2 for _, page in pages.items()) for _, page in pages.items(): - page.hub /= summed_hub - page.authority /= summed_auth + page.hub /= summed_hub ** 0.5 + page.authority /= summed_auth ** 0.5 + class ConvergenceDetector(object): """If the hub and authority values of the pages are no longer changing, we have reached a convergence and further iterations will have no effect. This detects convergence so that we can stop the HITS algorithm as early as possible.""" + def __init__(self): self.hub_history = None self.auth_history = None @@ -326,16 +505,16 @@ def __call__(self): def detect(self): curr_hubs = [page.hub for addr, page in pagesIndex.items()] curr_auths = [page.authority for addr, page in pagesIndex.items()] - if self.hub_history == None: - self.hub_history, self.auth_history = [],[] + if self.hub_history is None: + self.hub_history, self.auth_history = [], [] else: - diffsHub = [abs(x-y) for x, y in zip(curr_hubs,self.hub_history[-1])] - diffsAuth = [abs(x-y) for x, y in zip(curr_auths,self.auth_history[-1])] - aveDeltaHub = sum(diffsHub)/float(len(pagesIndex)) - aveDeltaAuth = sum(diffsAuth)/float(len(pagesIndex)) - if aveDeltaHub < 0.01 and aveDeltaAuth < 0.01: # may need tweaking + diffsHub = [abs(x - y) for x, y in zip(curr_hubs, self.hub_history[-1])] + diffsAuth = [abs(x - y) for x, y in zip(curr_auths, self.auth_history[-1])] + aveDeltaHub = sum(diffsHub) / float(len(pagesIndex)) + aveDeltaAuth = sum(diffsAuth) / float(len(pagesIndex)) + if aveDeltaHub < 0.01 and aveDeltaAuth < 0.01: # may need tweaking return True - if len(self.hub_history) > 2: # prevent list from getting long + if len(self.hub_history) > 2: # prevent list from getting long del self.hub_history[0] del self.auth_history[0] self.hub_history.append([x for x in curr_hubs]) @@ -343,43 +522,48 @@ def detect(self): return False -def getInlinks( page ): +def getInLinks(page): if not page.inlinks: page.inlinks = determineInlinks(page) - return [p for addr, p in pagesIndex.items() if addr in page.inlinks ] + return [addr for addr, p in pagesIndex.items() if addr in page.inlinks] -def getOutlinks( page ): + +def getOutLinks(page): if not page.outlinks: page.outlinks = findOutlinks(page) - return [p for addr, p in pagesIndex.items() if addr in page.outlinks] + return [addr for addr, p in pagesIndex.items() if addr in page.outlinks] # ______________________________________________________________________________ # HITS Algorithm class Page(object): - def __init__(self, address, hub=0, authority=0, inlinks=None, outlinks=None): + def __init__(self, address, inLinks=None, outLinks=None, hub=0, authority=0): self.address = address self.hub = hub self.authority = authority - self.inlinks = inlinks - self.outlinks = outlinks + self.inlinks = inLinks + self.outlinks = outLinks + -pagesContent = {} # maps Page relative or absolute URL/location to page's HTML content +pagesContent = {} # maps Page relative or absolute URL/location to page's HTML content pagesIndex = {} -convergence = ConvergenceDetector() # assign function to variable to mimic pseudocode's syntax +convergence = ConvergenceDetector() # assign function to variable to mimic pseudocode's syntax + def HITS(query): """The HITS algorithm for computing hubs and authorities with respect to a query.""" - pages = expand_pages(relevant_pages(query)) # in order to 'map' faithfully to pseudocode we - for p in pages: # won't pass the list of pages as an argument + pages = expand_pages(relevant_pages(query)) + for p in pages.values(): p.authority = 1 p.hub = 1 - while True: # repeat until... convergence + while not convergence(): + authority = {p: pages[p].authority for p in pages} + hub = {p: pages[p].hub for p in pages} for p in pages: - p.authority = sum(x.hub for x in getInlinks(p)) # p.authority ← ∑i Inlinki(p).Hub - p.hub = sum(x.authority for x in getOutlinks(p)) # p.hub ← ∑i Outlinki(p).Authority + # p.authority ← ∑i Inlinki(p).Hub + pages[p].authority = sum(hub[x] for x in getInLinks(pages[p])) + # p.hub ← ∑i Outlinki(p).Authority + pages[p].hub = sum(authority[x] for x in getOutLinks(pages[p])) normalize(pages) - if convergence(): - break return pages diff --git a/nlp4e.py b/nlp4e.py new file mode 100644 index 000000000..095f54357 --- /dev/null +++ b/nlp4e.py @@ -0,0 +1,523 @@ +"""Natural Language Processing (Chapter 22)""" + +from collections import defaultdict +from utils4e import weighted_choice +import copy +import operator +import heapq +from search import Problem + + +# ______________________________________________________________________________ +# 22.2 Grammars + + +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(): + rules[lhs] = [alt.strip().split() for alt in rhs.split('|')] + return rules + + +def Lexicon(**rules): + """Create a dictionary mapping symbols to alternative words. + >>> Lexicon(Article = "the | a | an") + {'Article': ['the', 'a', 'an']} + """ + for (lhs, rhs) in 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.""" + self.name = name + self.rules = rules + self.lexicon = lexicon + self.categories = defaultdict(list) + for lhs in lexicon: + for word in lexicon[lhs]: + self.categories[word].append(lhs) + + def rewrites_for(self, cat): + """Return a sequence of possible rhs's that cat can be rewritten as.""" + return self.rules.get(cat, ()) + + def isa(self, word, cat): + """Return True iff word is of category cat""" + return cat in self.categories[word] + + def cnf_rules(self): + """Returns the tuple (X, Y, Z) for rules in the form: + X -> Y Z""" + cnf = [] + for X, rules in self.rules.items(): + for (Y, Z) in rules: + cnf.append((X, Y, Z)) + + return cnf + + def generate_random(self, S='S'): + """Replace each token in S by a random entry in grammar (recursively).""" + import random + + def rewrite(tokens, into): + for token in tokens: + if token in self.rules: + rewrite(random.choice(self.rules[token]), into) + elif token in self.lexicon: + into.append(random.choice(self.lexicon[token])) + else: + into.append(token) + return into + + return ' '.join(rewrite(S.split(), [])) + + def __repr__(self): + return ''.format(self.name) + + +def ProbRules(**rules): + """Create a dictionary mapping symbols to alternative sequences, + with probabilities. + >>> ProbRules(A = "B C [0.3] | D E [0.7]") + {'A': [(['B', 'C'], 0.3), (['D', 'E'], 0.7)]} + """ + for (lhs, rhs) in rules.items(): + rules[lhs] = [] + rhs_separate = [alt.strip().split() for alt in rhs.split('|')] + for r in rhs_separate: + prob = float(r[-1][1:-1]) # remove brackets, convert to float + rhs_rule = (r[:-1], prob) + rules[lhs].append(rhs_rule) + + return rules + + +def ProbLexicon(**rules): + """Create a dictionary mapping symbols to alternative words, + with probabilities. + >>> ProbLexicon(Article = "the [0.5] | a [0.25] | an [0.25]") + {'Article': [('the', 0.5), ('a', 0.25), ('an', 0.25)]} + """ + for (lhs, rhs) in rules.items(): + rules[lhs] = [] + rhs_separate = [word.strip().split() for word in rhs.split('|')] + for r in rhs_separate: + prob = float(r[-1][1:-1]) # remove brackets, convert to float + word = r[:-1][0] + rhs_rule = (word, prob) + rules[lhs].append(rhs_rule) + + return rules + + +class ProbGrammar: + + def __init__(self, name, rules, lexicon): + """A grammar has a set of rules and a lexicon. + Each rule has a probability.""" + self.name = name + self.rules = rules + self.lexicon = lexicon + self.categories = defaultdict(list) + + for lhs in lexicon: + for word, prob in lexicon[lhs]: + self.categories[word].append((lhs, prob)) + + def rewrites_for(self, cat): + """Return a sequence of possible rhs's that cat can be rewritten as.""" + return self.rules.get(cat, ()) + + def isa(self, word, cat): + """Return True iff word is of category cat""" + return cat in [c for c, _ in self.categories[word]] + + def cnf_rules(self): + """Returns the tuple (X, Y, Z, p) for rules in the form: + X -> Y Z [p]""" + cnf = [] + for X, rules in self.rules.items(): + for (Y, Z), p in rules: + cnf.append((X, Y, Z, p)) + + return cnf + + def generate_random(self, S='S'): + """Replace each token in S by a random entry in grammar (recursively). + Returns a tuple of (sentence, probability).""" + + def rewrite(tokens, into): + for token in tokens: + if token in self.rules: + non_terminal, prob = weighted_choice(self.rules[token]) + into[1] *= prob + rewrite(non_terminal, into) + elif token in self.lexicon: + terminal, prob = weighted_choice(self.lexicon[token]) + into[0].append(terminal) + into[1] *= prob + else: + into[0].append(token) + return into + + rewritten_as, prob = rewrite(S.split(), [[], 1]) + return (' '.join(rewritten_as), prob) + + def __repr__(self): + return ''.format(self.name) + + +E0 = Grammar('E0', + Rules( # Grammar for E_0 [Figure 22.2] + 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 [Figure 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", # noqa + Adjective="right | left | east | south | back | smelly | dead", + 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')) + +E_Prob = ProbGrammar('E_Prob', # The Probabilistic Grammar from the notebook + ProbRules( + S="NP VP [0.6] | S Conjunction S [0.4]", + NP="Pronoun [0.2] | Name [0.05] | Noun [0.2] | Article Noun [0.15] \ + | Article Adjs Noun [0.1] | Digit [0.05] | NP PP [0.15] | NP RelClause [0.1]", + VP="Verb [0.3] | VP NP [0.2] | VP Adjective [0.25] | VP PP [0.15] | VP Adverb [0.1]", + Adjs="Adjective [0.5] | Adjective Adjs [0.5]", + PP="Preposition NP [1]", + RelClause="RelPro VP [1]" + ), + ProbLexicon( + Verb="is [0.5] | say [0.3] | are [0.2]", + Noun="robot [0.4] | sheep [0.4] | fence [0.2]", + Adjective="good [0.5] | new [0.2] | sad [0.3]", + Adverb="here [0.6] | lightly [0.1] | now [0.3]", + Pronoun="me [0.3] | you [0.4] | he [0.3]", + RelPro="that [0.5] | who [0.3] | which [0.2]", + Name="john [0.4] | mary [0.4] | peter [0.2]", + Article="the [0.5] | a [0.25] | an [0.25]", + Preposition="to [0.4] | in [0.3] | at [0.3]", + Conjunction="and [0.5] | or [0.2] | but [0.3]", + Digit="0 [0.35] | 1 [0.35] | 2 [0.3]" + )) + +E_Chomsky = Grammar('E_Prob_Chomsky', # A Grammar in Chomsky Normal Form + Rules( + S='NP VP', + NP='Article Noun | Adjective Noun', + VP='Verb NP | Verb Adjective', + ), + Lexicon( + Article='the | a | an', + Noun='robot | sheep | fence', + Adjective='good | new | sad', + Verb='is | say | are' + )) + +E_Prob_Chomsky = ProbGrammar('E_Prob_Chomsky', # A Probabilistic Grammar in CNF + ProbRules( + S='NP VP [1]', + NP='Article Noun [0.6] | Adjective Noun [0.4]', + VP='Verb NP [0.5] | Verb Adjective [0.5]', + ), + ProbLexicon( + Article='the [0.5] | a [0.25] | an [0.25]', + Noun='robot [0.4] | sheep [0.4] | fence [0.2]', + Adjective='good [0.5] | new [0.2] | sad [0.3]', + Verb='is [0.5] | say [0.3] | are [0.2]' + )) +E_Prob_Chomsky_ = ProbGrammar('E_Prob_Chomsky_', + ProbRules( + S='NP VP [1]', + NP='NP PP [0.4] | Noun Verb [0.6]', + PP='Preposition NP [1]', + VP='Verb NP [0.7] | VP PP [0.3]', + ), + ProbLexicon( + Noun='astronomers [0.18] | eyes [0.32] | stars [0.32] | telescopes [0.18]', + Verb='saw [0.5] | \'\' [0.5]', + Preposition='with [1]' + )) + + +# ______________________________________________________________________________ +# 22.3 Parsing + + +class Chart: + """Class for parsing sentences using a chart data structure. + >>> chart = Chart(E0) + >>> len(chart.parses('the stench is in 2 2')) + 1 + """ + + def __init__(self, grammar, trace=False): + """A datastructure for parsing a string; and methods to do the parse. + self.chart[i] holds the edges that end just before the i'th word. + Edges are 5-element lists of [start, end, lhs, [found], [expects]].""" + self.grammar = grammar + self.trace = trace + + def parses(self, words, S='S'): + """Return a list of parses; words can be a list or string.""" + if isinstance(words, str): + words = words.split() + self.parse(words, S) + # Return all the parses that span the whole input + # 'span the whole input' => begin at 0, end at len(words) + return [[i, j, S, found, []] + for (i, j, lhs, found, expects) in self.chart[len(words)] + # assert j == len(words) + if i == 0 and lhs == S and expects == []] + + def parse(self, words, S='S'): + """Parse a list of words; according to the grammar. + Leave results in the chart.""" + self.chart = [[] for i in range(len(words) + 1)] + self.add_edge([0, 0, 'S_', [], [S]]) + for i in range(len(words)): + self.scanner(i, words[i]) + return self.chart + + def add_edge(self, edge): + """Add edge to chart, and see if it extends or predicts another edge.""" + start, end, lhs, found, expects = edge + if edge not in self.chart[end]: + self.chart[end].append(edge) + if self.trace: + print('Chart: added {}'.format(edge)) + if not expects: + self.extender(edge) + else: + self.predictor(edge) + + def scanner(self, j, word): + """For each edge expecting a word of this category here, extend the edge.""" + for (i, j, A, alpha, Bb) in self.chart[j]: + 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, edge): + """Add to chart any rules for B that could help extend this edge.""" + (i, j, A, alpha, Bb) = edge + B = Bb[0] + if B in self.grammar.rules: + for rhs in self.grammar.rewrites_for(B): + self.add_edge([j, j, B, [], rhs]) + + def extender(self, edge): + """See what edges can be extended by this edge.""" + (j, k, B, _, _) = edge + for (i, j, A, alpha, B1b) in self.chart[j]: + if B1b and B == B1b[0]: + self.add_edge([i, k, A, alpha + [edge], B1b[1:]]) + + +# ______________________________________________________________________________ +# CYK Parsing + + +class Tree: + def __init__(self, root, *args): + self.root = root + self.leaves = [leaf for leaf in args] + + +def CYK_parse(words, grammar): + """ [Figure 22.6] """ + # We use 0-based indexing instead of the book's 1-based. + P = defaultdict(float) + T = defaultdict(Tree) + + # Insert lexical categories for each word. + for (i, word) in enumerate(words): + for (X, p) in grammar.categories[word]: + P[X, i, i] = p + T[X, i, i] = Tree(X, word) + + # Construct X(i:k) from Y(i:j) and Z(j+1:k), shortest span first + for i, j, k in subspan(len(words)): + for (X, Y, Z, p) in grammar.cnf_rules(): + PYZ = P[Y, i, j] * P[Z, j + 1, k] * p + if PYZ > P[X, i, k]: + P[X, i, k] = PYZ + T[X, i, k] = Tree(X, T[Y, i, j], T[Z, j + 1, k]) + + return T + + +def subspan(N): + """returns all tuple(i, j, k) covering a span (i, k) with i <= j < k""" + for length in range(2, N + 1): + for i in range(1, N + 2 - length): + k = i + length - 1 + for j in range(i, k): + yield (i, j, k) + + +# using search algorithms in the searching part + + +class TextParsingProblem(Problem): + def __init__(self, initial, grammar, goal='S'): + """ + :param initial: the initial state of words in a list. + :param grammar: a grammar object + :param goal: the goal state, usually S + """ + super(TextParsingProblem, self).__init__(initial, goal) + self.grammar = grammar + self.combinations = defaultdict(list) # article combinations + # backward lookup of rules + for rule in grammar.rules: + for comb in grammar.rules[rule]: + self.combinations[' '.join(comb)].append(rule) + + def actions(self, state): + actions = [] + categories = self.grammar.categories + # first change each word to the article of its category + for i in range(len(state)): + word = state[i] + if word in categories: + for X in categories[word]: + state[i] = X + actions.append(copy.copy(state)) + state[i] = word + # if all words are replaced by articles, replace combinations of articles by inferring rules. + if not actions: + for start in range(len(state)): + for end in range(start, len(state) + 1): + # try combinations between (start, end) + articles = ' '.join(state[start:end]) + for c in self.combinations[articles]: + actions.append(state[:start] + [c] + state[end:]) + return actions + + def result(self, state, action): + return action + + def h(self, state): + # heuristic function + return len(state) + + +def astar_search_parsing(words, gramma): + """bottom-up parsing using A* search to find whether a list of words is a sentence""" + # init the problem + problem = TextParsingProblem(words, gramma, 'S') + state = problem.initial + # init the searching frontier + frontier = [(len(state) + problem.h(state), state)] + heapq.heapify(frontier) + + while frontier: + # search the frontier node with lowest cost first + cost, state = heapq.heappop(frontier) + actions = problem.actions(state) + for action in actions: + new_state = problem.result(state, action) + # update the new frontier node to the frontier + if new_state == [problem.goal]: + return problem.goal + if new_state != state: + heapq.heappush(frontier, (len(new_state) + problem.h(new_state), new_state)) + return False + + +def beam_search_parsing(words, gramma, b=3): + """bottom-up text parsing using beam search""" + # init problem + problem = TextParsingProblem(words, gramma, 'S') + # init frontier + frontier = [(len(problem.initial), problem.initial)] + heapq.heapify(frontier) + + # explore the current frontier and keep b new states with lowest cost + def explore(frontier): + new_frontier = [] + for cost, state in frontier: + # expand the possible children states of current state + if not problem.goal_test(' '.join(state)): + actions = problem.actions(state) + for action in actions: + new_state = problem.result(state, action) + if [len(new_state), new_state] not in new_frontier and new_state != state: + new_frontier.append([len(new_state), new_state]) + else: + return problem.goal + heapq.heapify(new_frontier) + # only keep b states + return heapq.nsmallest(b, new_frontier) + + while frontier: + frontier = explore(frontier) + if frontier == problem.goal: + return frontier + return False + + +# ______________________________________________________________________________ +# 22.4 Augmented Grammar + + +g = Grammar("arithmetic_expression", # A Grammar of Arithmetic Expression + rules={ + 'Number_0': 'Digit_0', 'Number_1': 'Digit_1', 'Number_2': 'Digit_2', + 'Number_10': 'Number_1 Digit_0', 'Number_11': 'Number_1 Digit_1', + 'Number_100': 'Number_10 Digit_0', + 'Exp_5': ['Number_5', '( Exp_5 )', 'Exp_1, Operator_+ Exp_4', 'Exp_2, Operator_+ Exp_3', + 'Exp_0, Operator_+ Exp_5', 'Exp_3, Operator_+ Exp_2', 'Exp_4, Operator_+ Exp_1', + 'Exp_5, Operator_+ Exp_0', 'Exp_1, Operator_* Exp_5'], # more possible combinations + 'Operator_+': operator.add, 'Operator_-': operator.sub, 'Operator_*': operator.mul, + 'Operator_/': operator.truediv, + 'Digit_0': 0, 'Digit_1': 1, 'Digit_2': 2, 'Digit_3': 3, 'Digit_4': 4 + }, + lexicon={}) + +g = Grammar("Ali loves Bob", # A example grammer of Ali loves Bob example + rules={ + "S_loves_ali_bob": "NP_ali, VP_x_loves_x_bob", "S_loves_bob_ali": "NP_bob, VP_x_loves_x_ali", + "VP_x_loves_x_bob": "Verb_xy_loves_xy NP_bob", "VP_x_loves_x_ali": "Verb_xy_loves_xy NP_ali", + "NP_bob": "Name_bob", "NP_ali": "Name_ali" + }, + lexicon={ + "Name_ali": "Ali", "Name_bob": "Bob", "Verb_xy_loves_xy": "loves" + }) diff --git a/nlp_apps.ipynb b/nlp_apps.ipynb new file mode 100644 index 000000000..2f4796b7a --- /dev/null +++ b/nlp_apps.ipynb @@ -0,0 +1,1038 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NATURAL LANGUAGE PROCESSING APPLICATIONS\n", + "\n", + "In this notebook we will take a look at some indicative applications of natural language processing. We will cover content from [`nlp.py`](https://github.com/aimacode/aima-python/blob/master/nlp.py) and [`text.py`](https://github.com/aimacode/aima-python/blob/master/text.py), for chapters 22 and 23 of Stuart Russel's and Peter Norvig's book [*Artificial Intelligence: A Modern Approach*](http://aima.cs.berkeley.edu/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Language Recognition\n", + "* Author Recognition\n", + "* The Federalist Papers\n", + "* Text Classification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LANGUAGE RECOGNITION\n", + "\n", + "A very useful application of text models (you can read more on them on the [`text notebook`](https://github.com/aimacode/aima-python/blob/master/text.ipynb)) is categorizing text into a language. In fact, with enough data we can categorize correctly mostly any text. That is because different languages have certain characteristics that set them apart. For example, in German it is very usual for 'c' to be followed by 'h' while in English we see 't' followed by 'h' a lot.\n", + "\n", + "Here we will build an application to categorize sentences in either English or German.\n", + "\n", + "First we need to build our dataset. We will take as input text in English and in German and we will extract n-gram character models (in this case, *bigrams* for n=2). For English, we will use *Flatland* by Edwin Abbott and for German *Faust* by Goethe.\n", + "\n", + "Let's build our text models for each language, which will hold the probability of each bigram occuring in the text." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "flatland = open_data(\"EN-text/flatland.txt\").read()\n", + "wordseq = words(flatland)\n", + "\n", + "P_flatland = NgramCharModel(2, wordseq)\n", + "\n", + "faust = open_data(\"GE-text/faust.txt\").read()\n", + "wordseq = words(faust)\n", + "\n", + "P_faust = NgramCharModel(2, wordseq)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use this information to build a *Naive Bayes Classifier* that will be used to categorize sentences (you can read more on Naive Bayes on the [`learning notebook`](https://github.com/aimacode/aima-python/blob/master/learning.ipynb)). The classifier will take as input the probability distribution of bigrams and given a list of bigrams (extracted from the sentence to be classified), it will calculate the probability of the example/sentence coming from each language and pick the maximum.\n", + "\n", + "Let's build our classifier, with the assumption that English is as probable as German (the input is a dictionary with values the text models and keys the tuple `language, probability`):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('English', 1): P_flatland, ('German', 1): P_faust}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we need to write a function that takes as input a sentence, breaks it into a list of bigrams and classifies it with the naive bayes classifier from above.\n", + "\n", + "Once we get the text model for the sentence, we need to unravel it. The text models show the probability of each bigram, but the classifier can't handle that extra data. It requires a simple *list* of bigrams. So, if the text model shows that a bigram appears three times, we need to add it three times in the list. Since the text model stores the n-gram information in a dictionary (with the key being the n-gram and the value the number of times the n-gram appears) we need to iterate through the items of the dictionary and manually add them to the list of n-grams." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS, n):\n", + " sentence = sentence.lower()\n", + " wordseq = words(sentence)\n", + " \n", + " P_sentence = NgramCharModel(n, wordseq)\n", + " \n", + " ngrams = []\n", + " for b, p in P_sentence.dictionary.items():\n", + " ngrams += [b]*p\n", + " \n", + " print(ngrams)\n", + " \n", + " return nBS(ngrams)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can start categorizing sentences." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'i'), ('i', 'c'), ('c', 'h'), (' ', 'b'), ('b', 'i'), ('i', 'n'), ('i', 'n'), (' ', 'e'), ('e', 'i'), (' ', 'p'), ('p', 'l'), ('l', 'a'), ('a', 't'), ('t', 'z')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'German'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Ich bin ein platz\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 't'), ('t', 'u'), ('u', 'r'), ('r', 't'), ('t', 'l'), ('l', 'e'), ('e', 's'), (' ', 'f'), ('f', 'l'), ('l', 'y'), (' ', 'h'), ('h', 'i'), ('i', 'g'), ('g', 'h')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'English'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Turtles fly high\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'd'), ('d', 'e'), ('e', 'r'), ('e', 'r'), (' ', 'p'), ('p', 'e'), ('e', 'l'), ('l', 'i'), ('i', 'k'), ('k', 'a'), ('a', 'n'), (' ', 'i'), ('i', 's'), ('s', 't'), (' ', 'h'), ('h', 'i'), ('i', 'e')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'German'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Der pelikan ist hier\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'a'), ('a', 'n'), ('n', 'd'), (' ', 't'), (' ', 't'), ('t', 'h'), ('t', 'h'), ('h', 'u'), ('u', 's'), ('h', 'e'), (' ', 'w'), ('w', 'i'), ('i', 'z'), ('z', 'a'), ('a', 'r'), ('r', 'd'), (' ', 's'), ('s', 'p'), ('p', 'o'), ('o', 'k'), ('k', 'e')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'English'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"And thus the wizard spoke\", nBS, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can add more languages if you want, the algorithm works for as many as you like! Also, you can play around with *n*. Here we used 2, but other numbers work too (even though 2 suffices). The algorithm is not perfect, but it has high accuracy even for small samples like the ones we used. That is because English and German are very different languages. The closer together languages are (for example, Norwegian and Swedish share a lot of common ground) the lower the accuracy of the classifier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AUTHOR RECOGNITION\n", + "\n", + "Another similar application to language recognition is recognizing who is more likely to have written a sentence, given text written by them. Here we will try and predict text from Edwin Abbott and Jane Austen. They wrote *Flatland* and *Pride and Prejudice* respectively.\n", + "\n", + "We are optimistic we can determine who wrote what based on the fact that Abbott wrote his novella on much later date than Austen, which means there will be linguistic differences between the two works. Indeed, *Flatland* uses more modern and direct language while *Pride and Prejudice* is written in a more archaic tone containing more sophisticated wording.\n", + "\n", + "Similarly with Language Recognition, we will first import the two datasets. This time though we are not looking for connections between characters, since that wouldn't give that great results. Why? Because both authors use English and English follows a set of patterns, as we show earlier. Trying to determine authorship based on this patterns would not be very efficient.\n", + "\n", + "Instead, we will abstract our querying to a higher level. We will use words instead of characters. That way we can more accurately pick at the differences between their writing style and thus have a better chance at guessing the correct author.\n", + "\n", + "Let's go right ahead and import our data:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "flatland = open_data(\"EN-text/flatland.txt\").read()\n", + "wordseq = words(flatland)\n", + "\n", + "P_Abbott = UnigramWordModel(wordseq, 5)\n", + "\n", + "pride = open_data(\"EN-text/pride.txt\").read()\n", + "wordseq = words(pride)\n", + "\n", + "P_Austen = UnigramWordModel(wordseq, 5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This time we set the `default` parameter of the model to 5, instead of 0. If we leave it at 0, then when we get a sentence containing a word we have not seen from that particular author, the chance of that sentence coming from that author is exactly 0 (since to get the probability, we multiply all the separate probabilities; if one is 0 then the result is also 0). To avoid that, we tell the model to add 5 to the count of all the words that appear.\n", + "\n", + "Next we will build the Naive Bayes Classifier:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('Abbott', 1): P_Abbott, ('Austen', 1): P_Austen}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have build our classifier, we will start classifying. First, we need to convert the given sentence to the format the classifier needs. That is, a list of words." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " sentence = sentence.lower()\n", + " sentence_words = words(sentence)\n", + " \n", + " return nBS(sentence_words)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we will input a sentence that is something Abbott would write. Note the use of square and the simpler language." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Abbott'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"the square is mad\", nBS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The classifier correctly guessed Abbott.\n", + "\n", + "Next we will input a more sophisticated sentence, similar to the style of Austen." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Austen'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"a most peculiar acquaintance\", nBS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The classifier guessed correctly again.\n", + "\n", + "You can try more sentences on your own. Unfortunately though, since the datasets are pretty small, chances are the guesses will not always be correct." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## THE FEDERALIST PAPERS\n", + "\n", + "Let's now take a look at a harder problem, classifying the authors of the [Federalist Papers](https://en.wikipedia.org/wiki/The_Federalist_Papers). The *Federalist Papers* are a series of papers written by Alexander Hamilton, James Madison and John Jay towards establishing the United States Constitution.\n", + "\n", + "What is interesting about these papers is that they were all written under a pseudonym, \"Publius\", to keep the identity of the authors a secret. Only after Hamilton's death, when a list was found written by him detailing the authorship of the papers, did the rest of the world learn what papers each of the authors wrote. After the list was published, Madison chimed in to make a couple of corrections: Hamilton, Madison said, hastily wrote down the list and assigned some papers to the wrong author!\n", + "\n", + "Here we will try and find out who really wrote these mysterious papers.\n", + "\n", + "To solve this we will learn from the undisputed papers to predict the disputed ones. First, let's read the texts from the file:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "federalist = open_data(\"EN-text/federalist.txt\").read()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how the text looks. We will print the first 500 characters:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The Project Gutenberg EBook of The Federalist Papers, by \\nAlexander Hamilton and John Jay and James Madison\\n\\nThis eBook is for the use of anyone anywhere at no cost and with\\nalmost no restrictions whatsoever. You may copy it, give it away or\\nre-use it under the terms of the Project Gutenberg License included\\nwith this eBook or online at www.gutenberg.net\\n\\n\\nTitle: The Federalist Papers\\n\\nAuthor: Alexander Hamilton\\n John Jay\\n James Madison\\n\\nPosting Date: December 12, 2011 [EBook #18]'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "federalist[:500]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems that the text file opens with a license agreement, hardly useful in our case. In fact, the license spans 113 words, while there is also a licensing agreement at the end of the file, which spans 3098 words. We need to remove them. To do so, we will first convert the text into words, to make our lives easier." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "wordseq = words(federalist)\n", + "wordseq = wordseq[114:-3098]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now take a look at the first 100 words:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'federalist no 1 general introduction for the independent journal hamilton to the people of the state of new york after an unequivocal experience of the inefficacy of the subsisting federal government you are called upon to deliberate on a new constitution for the united states of america the subject speaks its own importance comprehending in its consequences nothing less than the existence of the union the safety and welfare of the parts of which it is composed the fate of an empire in many respects the most interesting in the world it has been frequently remarked that it seems to'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "' '.join(wordseq[:100])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Much better.\n", + "\n", + "As with any Natural Language Processing problem, it is prudent to do some text pre-processing and clean our data before we start building our model. Remember that all the papers are signed as 'Publius', so we can safely remove that word, since it doesn't give us any information as to the real author.\n", + "\n", + "NOTE: Since we are only removing a single word from each paper, this step can be skipped. We add it here to show that processing the data in our hands is something we should always be considering. Oftentimes pre-processing the data in just the right way is the difference between a robust model and a flimsy one." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "wordseq = [w for w in wordseq if w != 'publius']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have to separate the text from a block of words into papers and assign them to their authors. We can see that each paper starts with the word 'federalist', so we will split the text on that word.\n", + "\n", + "The disputed papers are the papers from 49 to 58, from 18 to 20 and paper 64. We want to leave these papers unassigned. Also, note that there are two versions of paper 70; both from Hamilton.\n", + "\n", + "Finally, to keep the implementation intuitive, we add a `None` object at the start of the `papers` list to make the list index match up with the paper numbering (for example, `papers[5]` now corresponds to paper no. 5 instead of the paper no.6 in the 0-indexed Python)." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(4, 16, 52)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import re\n", + "\n", + "papers = re.split(r'federalist\\s', ' '.join(wordseq))\n", + "papers = [p for p in papers if p not in ['', ' ']]\n", + "papers = [None] + papers\n", + "\n", + "disputed = list(range(49, 58+1)) + [18, 19, 20, 64]\n", + "jay, madison, hamilton = [], [], []\n", + "for i, p in enumerate(papers):\n", + " if i in disputed or i == 0:\n", + " continue\n", + " \n", + " if 'jay' in p:\n", + " jay.append(p)\n", + " elif 'madison' in p:\n", + " madison.append(p)\n", + " else:\n", + " hamilton.append(p)\n", + "\n", + "len(jay), len(madison), len(hamilton)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, from the undisputed papers Jay wrote 4, Madison 17 and Hamilton 51 (+1 duplicate). Let's now build our word models. The Unigram Word Model again will come in handy." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "hamilton = ''.join(hamilton)\n", + "hamilton_words = words(hamilton)\n", + "P_hamilton = UnigramWordModel(hamilton_words, default=1)\n", + "\n", + "madison = ''.join(madison)\n", + "madison_words = words(madison)\n", + "P_madison = UnigramWordModel(madison_words, default=1)\n", + "\n", + "jay = ''.join(jay)\n", + "jay_words = words(jay)\n", + "P_jay = UnigramWordModel(jay_words, default=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now it is time to build our new Naive Bayes Learner. It is very similar to the one found in `learning.py`, but with an important difference: it doesn't classify an example, but instead returns the probability of the example belonging to each class. This will allow us to not only see to whom a paper belongs to, but also the probability of authorship as well. \n", + "We will build two versions of Learners, one will multiply probabilities as is and other will add the logarithms of them.\n", + "\n", + "Finally, since we are dealing with long text and the string of probability multiplications is long, we will end up with the results being rounded to 0 due to floating point underflow. To work around this problem we will use the built-in Python library `decimal`, which allows as to set decimal precision to much larger than normal.\n", + "\n", + "Note that the logarithmic learner will compute a negative likelihood since the logarithm of values less than 1 will be negative.\n", + "Thus, the author with the lesser magnitude of proportion is more likely to have written that paper.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import decimal\n", + "import math\n", + "from decimal import Decimal\n", + "\n", + "decimal.getcontext().prec = 100\n", + "\n", + "def precise_product(numbers):\n", + " result = 1\n", + " for x in numbers:\n", + " result *= Decimal(x)\n", + " return result\n", + "\n", + "def log_product(numbers):\n", + " result = 0.0\n", + " for x in numbers:\n", + " result += math.log(x)\n", + " return result\n", + "\n", + "def NaiveBayesLearner(dist):\n", + " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", + " Counter distributions and can then be used to find the probability\n", + " of a given item belonging to each class.\n", + " The input dictionary is in the following form:\n", + " ClassName: Counter\"\"\"\n", + " attr_dist = {c_name: count_prob for c_name, count_prob in dist.items()}\n", + "\n", + " def predict(example):\n", + " \"\"\"Predict the probabilities for each class.\"\"\"\n", + " def class_prob(target, e):\n", + " attr = attr_dist[target]\n", + " return precise_product([attr[a] for a in e])\n", + "\n", + " pred = {t: class_prob(t, example) for t in dist.keys()}\n", + "\n", + " total = sum(pred.values())\n", + " for k, v in pred.items():\n", + " pred[k] = v / total\n", + "\n", + " return pred\n", + "\n", + " return predict\n", + "\n", + "def NaiveBayesLearnerLog(dist):\n", + " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", + " Counter distributions and can then be used to find the probability\n", + " of a given item belonging to each class. It will compute the likelihood by adding the logarithms of probabilities.\n", + " The input dictionary is in the following form:\n", + " ClassName: Counter\"\"\"\n", + " attr_dist = {c_name: count_prob for c_name, count_prob in dist.items()}\n", + "\n", + " def predict(example):\n", + " \"\"\"Predict the probabilities for each class.\"\"\"\n", + " def class_prob(target, e):\n", + " attr = attr_dist[target]\n", + " return log_product([attr[a] for a in e])\n", + "\n", + " pred = {t: class_prob(t, example) for t in dist.keys()}\n", + "\n", + " total = -sum(pred.values())\n", + " for k, v in pred.items():\n", + " pred[k] = v/total\n", + "\n", + " return pred\n", + "\n", + " return predict\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we will build our Learner. Note that even though Hamilton wrote the most papers, that doesn't make it more probable that he wrote the rest, so all the class probabilities will be equal. We can change them if we have some external knowledge, which for this tutorial we do not have." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "dist = {('Madison', 1): P_madison, ('Hamilton', 1): P_hamilton, ('Jay', 1): P_jay}\n", + "nBS = NaiveBayesLearner(dist)\n", + "nBSL = NaiveBayesLearnerLog(dist)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As usual, the `recognize` function will take as input a string and after removing capitalization and splitting it into words, will feed it into the Naive Bayes Classifier." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " return nBS(words(sentence.lower()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can start predicting the disputed papers:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Straightforward Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 50: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 51: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 52: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 53: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 54: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 55: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 56: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 57: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 58: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 18: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 19: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 20: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 64: Hamilton: 1.0000 Madison: 0.0000 Jay: 0.0000\n", + "\n", + "Logarithmic Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: -0.330591 Madison: -0.327717 Jay: -0.341692\n", + "Paper No. 50: Hamilton: -0.333119 Madison: -0.328454 Jay: -0.338427\n", + "Paper No. 51: Hamilton: -0.330246 Madison: -0.325758 Jay: -0.343996\n", + "Paper No. 52: Hamilton: -0.331094 Madison: -0.327491 Jay: -0.341415\n", + "Paper No. 53: Hamilton: -0.330942 Madison: -0.328364 Jay: -0.340693\n", + "Paper No. 54: Hamilton: -0.329566 Madison: -0.327157 Jay: -0.343277\n", + "Paper No. 55: Hamilton: -0.330821 Madison: -0.328143 Jay: -0.341036\n", + "Paper No. 56: Hamilton: -0.330333 Madison: -0.327496 Jay: -0.342171\n", + "Paper No. 57: Hamilton: -0.330625 Madison: -0.328602 Jay: -0.340772\n", + "Paper No. 58: Hamilton: -0.330271 Madison: -0.327215 Jay: -0.342515\n", + "Paper No. 18: Hamilton: -0.337781 Madison: -0.330932 Jay: -0.331287\n", + "Paper No. 19: Hamilton: -0.335635 Madison: -0.331774 Jay: -0.332590\n", + "Paper No. 20: Hamilton: -0.334911 Madison: -0.331866 Jay: -0.333223\n", + "Paper No. 64: Hamilton: -0.331004 Madison: -0.332968 Jay: -0.336028\n" + ] + } + ], + "source": [ + "print('\\nStraightforward Naive Bayes Learner\\n')\n", + "for d in disputed:\n", + " probs = recognize(papers[d], nBS)\n", + " results = ['{}: {:.4f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n", + "print('\\nLogarithmic Naive Bayes Learner\\n')\n", + "for d in disputed:\n", + " probs = recognize(papers[d], nBSL)\n", + " results = ['{}: {:.6f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that both learners classify the papers identically. Because of underflow in the straightforward learner, only one author remains with a positive value. The log learner is more accurate with marginal differences between all the authors. \n", + "\n", + "This is a simple approach to the problem and thankfully researchers are fairly certain that papers 49-58 were all written by Madison, while 18-20 were written in collaboration between Hamilton and Madison, with Madison being credited for most of the work. Our classifier is not that far off. It correctly identifies the papers written by Madison, even the ones in collaboration with Hamilton.\n", + "\n", + "Unfortunately, it misses paper 64. Consensus is that the paper was written by John Jay, while our classifier believes it was written by Hamilton. The classifier is wrong there because it does not have much information on Jay's writing; only 4 papers. This is one of the problems with using unbalanced datasets such as this one, where information on some classes is sparser than information on the rest. To avoid this, we can add more writings for Jay and Madison to end up with an equal amount of data for each author." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## Text Classification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Text Classification** is assigning a category to a document based on the content of the document. Text Classification is one of the most popular and fundamental tasks of Natural Language Processing. Text classification can be applied on a variety of texts like *Short Documents* (like tweets, customer reviews, etc.) and *Long Document* (like emails, media articles, etc.).\n", + "\n", + "We already have seen an example of Text Classification in the above tasks like Language Identification, Author Recognition and Federalist Paper Identification.\n", + "\n", + "### Applications\n", + "Some of the broad applications of Text Classification are:-\n", + "- Language Identification\n", + "- Author Recognition\n", + "- Sentiment Analysis\n", + "- Spam Mail Detection\n", + "- Topic Labelling \n", + "- Word Sense Disambiguation\n", + "\n", + "### Use Cases\n", + "Some of the use cases of Text classification are:-\n", + "- Social Media Monitoring\n", + "- Brand Monitoring\n", + "- Auto-tagging of user queries\n", + "\n", + "For Text Classification, we would be using the Naive Bayes Classifier. The reasons for using Naive Bayes Classifier are:-\n", + "- Being a probabilistic classifier, therefore, will calculate the probability of each category\n", + "- It is fast, reliable and accurate \n", + "- Naive Bayes Classifiers have already been used to solve many Natural Language Processing (NLP) applications.\n", + "\n", + "Here we would here be covering an example of **Word Sense Disambiguation** as an application of Text Classification. It is used to remove the ambiguity of a given word if the word has two different meanings.\n", + "\n", + "As we know that we would be working on determining whether the word *apple* in a sentence refers to `fruit` or to a `company`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 1:- Defining the dataset** \n", + "\n", + "The dataset has been defined here so that everything is clear and can be tested with other things as well." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = [\n", + " \"Apple targets big business with new iOS 7 features. Finally... A corp iTunes account!\",\n", + " \"apple inc is searching for people to help and try out all their upcoming tablet within our own net page No.\",\n", + " \"Microsoft to bring Xbox and PC games to Apple, Android phones: Report: Microsoft Corp\",\n", + " \"When did green skittles change from lime to green apple?\",\n", + " \"Myra Oltman is the best. I told her I wanted to learn how to make apple pie, so she made me a kit!\",\n", + " \"Surreal Sat in a sewing room, surrounded by crap, listening to beautiful music eating apple pie.\"\n", + "]\n", + "\n", + "train_target = [\n", + " \"company\",\n", + " \"company\",\n", + " \"company\",\n", + " \"fruit\",\n", + " \"fruit\",\n", + " \"fruit\",\n", + "]\n", + "\n", + "class_0 = \"company\"\n", + "class_1 = \"fruit\"\n", + "\n", + "test_data = [\n", + " \"Apple Inc. supplier Foxconn demos its own iPhone-compatible smartwatch\",\n", + " \"I now know how to make a delicious apple pie thanks to the best teachers ever\"\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 2:- Preprocessing the dataset**\n", + "\n", + "In this step, we would be doing some preprocessing on the dataset like breaking the sentence into words and converting to lower case.\n", + "\n", + "We already have a `words(sent)` function defined in `text.py` which does the task of splitting the sentence into words." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "train_data_processed = [words(i) for i in train_data]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 3:- Feature Extraction from the text**\n", + "\n", + "Now we would be extracting features from the text like extracting the set of words used in both the categories i.e. `company` and `fruit`.\n", + "\n", + "The frequency of a word would help in calculating the probability of that word being in a particular class. " + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of words in `company` class: 49\n", + "Number of words in `fruit` class: 49\n" + ] + } + ], + "source": [ + "words_0 = []\n", + "words_1 = []\n", + "\n", + "for sent, tag in zip(train_data_processed, train_target):\n", + " if(tag == class_0):\n", + " words_0 += sent\n", + " elif(tag == class_1):\n", + " words_1 += sent\n", + " \n", + "print(\"Number of words in `{}` class: {}\".format(class_0, len(words_0)))\n", + "print(\"Number of words in `{}` class: {}\".format(class_1, len(words_1)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you might have observed, that our dataset is equally balanced, i.e. we have an equal number of words in both the classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 4:- Building the Naive Bayes Model**\n", + "\n", + "Using the Naive Bayes classifier we can calculate the probability of a word in `company` and `fruit` class and then multiplying all of them to get the probability of that sentence belonging each of the given classes. But if a word is not in our dictionary then this leads to the probability of that word belonging to that class becoming zero. For example:- the word *Foxconn* is not in the dictionary of any of the classes. Due to this, the probability of word *Foxconn* being in any of these classes becomes zero, and since all the probabilities are multiplied, this leads to the probability of that sentence belonging to any of the classes becoming zero. \n", + "\n", + "To solve the problem we need to use **smoothing**, i.e. providing a minimum non-zero threshold probability to every word that we come across.\n", + "\n", + "The `UnigramWordModel` class has implemented smoothing by taking an additional argument from the user, i.e. the minimum frequency that we would be giving to every word even if it is new to the dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "model_words_0 = UnigramWordModel(words_0, 1)\n", + "model_words_1 = UnigramWordModel(words_1, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we would be building the Naive Bayes model. For that, we would be making `dist` as we had done earlier in the Authorship Recognition Task." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('company', 1): model_words_0, ('fruit', 1): model_words_1}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 5:- Predict the class of a sentence**\n", + "\n", + "Now we will be writing a function that does pre-process of the sentences which we have taken for testing. And then predicting the class of every sentence in the document." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " sentence_words = words(sentence)\n", + " return nBS(sentence_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Apple Inc. supplier Foxconn demos its own iPhone-compatible smartwatch\t-company\n", + "I now know how to make a delicious apple pie thanks to the best teachers ever\t-fruit\n" + ] + } + ], + "source": [ + "# predicting the class of sentences in the test set\n", + "for i in test_data:\n", + " print(i + \"\\t-\" + recognize(i, nBS))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You might have observed that the predictions made by the model are correct and we are able to differentiate between sentences of different classes. You can try more sentences on your own. Unfortunately though, since the datasets are pretty small, chances are the guesses will not always be correct.\n", + "\n", + "As you might have observed, the above method is very much similar to the Author Recognition, which is also a type of Text Classification. Like this most of Text Classification have the same underlying structure and follow a similar procedure." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook.py b/notebook.py new file mode 100644 index 000000000..7f0306335 --- /dev/null +++ b/notebook.py @@ -0,0 +1,1122 @@ +import time +from collections import defaultdict +from inspect import getsource + +import ipywidgets as widgets +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np +from IPython.display import HTML +from IPython.display import display +from PIL import Image +from matplotlib import lines + +from games import TicTacToe, alpha_beta_player, random_player, Fig52Extended +from learning import DataSet +from logic import parse_definite_clause, standardize_variables, unify_mm, subst +from search import GraphProblem, romania_map + + +# ______________________________________________________________________________ +# Magic Words + + +def pseudocode(algorithm): + """Print the pseudocode for the given algorithm.""" + from urllib.request import urlopen + from IPython.display import Markdown + + algorithm = algorithm.replace(' ', '-') + url = "https://raw.githubusercontent.com/aimacode/aima-pseudocode/master/md/{}.md".format(algorithm) + f = urlopen(url) + md = f.read().decode('utf-8') + md = md.split('\n', 1)[-1].strip() + md = '#' + md + return Markdown(md) + + +def psource(*functions): + """Print the source code for the given function(s).""" + source_code = '\n\n'.join(getsource(fn) for fn in functions) + try: + from pygments.formatters import HtmlFormatter + from pygments.lexers import PythonLexer + from pygments import highlight + + display(HTML(highlight(source_code, PythonLexer(), HtmlFormatter(full=True)))) + + except ImportError: + print(source_code) + + +# ______________________________________________________________________________ +# Iris Visualization + + +def show_iris(i=0, j=1, k=2): + """Plots the iris dataset in a 3D plot. + The three axes are given by i, j and k, + which correspond to three of the four iris features.""" + + plt.rcParams.update(plt.rcParamsDefault) + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + iris = DataSet(name="iris") + buckets = iris.split_values_by_classes() + + features = ["Sepal Length", "Sepal Width", "Petal Length", "Petal Width"] + f1, f2, f3 = features[i], features[j], features[k] + + a_setosa = [v[i] for v in buckets["setosa"]] + b_setosa = [v[j] for v in buckets["setosa"]] + c_setosa = [v[k] for v in buckets["setosa"]] + + a_virginica = [v[i] for v in buckets["virginica"]] + b_virginica = [v[j] for v in buckets["virginica"]] + c_virginica = [v[k] for v in buckets["virginica"]] + + a_versicolor = [v[i] for v in buckets["versicolor"]] + b_versicolor = [v[j] for v in buckets["versicolor"]] + c_versicolor = [v[k] for v in buckets["versicolor"]] + + for c, m, sl, sw, pl in [('b', 's', a_setosa, b_setosa, c_setosa), + ('g', '^', a_virginica, b_virginica, c_virginica), + ('r', 'o', a_versicolor, b_versicolor, c_versicolor)]: + ax.scatter(sl, sw, pl, c=c, marker=m) + + ax.set_xlabel(f1) + ax.set_ylabel(f2) + ax.set_zlabel(f3) + + plt.show() + + +# ______________________________________________________________________________ +# MNIST + + +def load_MNIST(path="aima-data/MNIST/Digits", fashion=False): + import os, struct + import array + import numpy as np + + if fashion: + path = "aima-data/MNIST/Fashion" + + plt.rcParams.update(plt.rcParamsDefault) + plt.rcParams['figure.figsize'] = (10.0, 8.0) + plt.rcParams['image.interpolation'] = 'nearest' + plt.rcParams['image.cmap'] = 'gray' + + train_img_file = open(os.path.join(path, "train-images-idx3-ubyte"), "rb") + train_lbl_file = open(os.path.join(path, "train-labels-idx1-ubyte"), "rb") + test_img_file = open(os.path.join(path, "t10k-images-idx3-ubyte"), "rb") + test_lbl_file = open(os.path.join(path, 't10k-labels-idx1-ubyte'), "rb") + + magic_nr, tr_size, tr_rows, tr_cols = struct.unpack(">IIII", train_img_file.read(16)) + tr_img = array.array("B", train_img_file.read()) + train_img_file.close() + magic_nr, tr_size = struct.unpack(">II", train_lbl_file.read(8)) + tr_lbl = array.array("b", train_lbl_file.read()) + train_lbl_file.close() + + magic_nr, te_size, te_rows, te_cols = struct.unpack(">IIII", test_img_file.read(16)) + te_img = array.array("B", test_img_file.read()) + test_img_file.close() + magic_nr, te_size = struct.unpack(">II", test_lbl_file.read(8)) + te_lbl = array.array("b", test_lbl_file.read()) + test_lbl_file.close() + + # print(len(tr_img), len(tr_lbl), tr_size) + # print(len(te_img), len(te_lbl), te_size) + + train_img = np.zeros((tr_size, tr_rows * tr_cols), dtype=np.int16) + train_lbl = np.zeros((tr_size,), dtype=np.int8) + for i in range(tr_size): + train_img[i] = np.array(tr_img[i * tr_rows * tr_cols: (i + 1) * tr_rows * tr_cols]).reshape((tr_rows * te_cols)) + train_lbl[i] = tr_lbl[i] + + test_img = np.zeros((te_size, te_rows * te_cols), dtype=np.int16) + test_lbl = np.zeros((te_size,), dtype=np.int8) + for i in range(te_size): + test_img[i] = np.array(te_img[i * te_rows * te_cols: (i + 1) * te_rows * te_cols]).reshape((te_rows * te_cols)) + test_lbl[i] = te_lbl[i] + + return (train_img, train_lbl, test_img, test_lbl) + + +digit_classes = [str(i) for i in range(10)] +fashion_classes = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", + "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"] + + +def show_MNIST(labels, images, samples=8, fashion=False): + if not fashion: + classes = digit_classes + else: + classes = fashion_classes + + num_classes = len(classes) + + for y, cls in enumerate(classes): + idxs = np.nonzero([i == y for i in labels]) + idxs = np.random.choice(idxs[0], samples, replace=False) + for i, idx in enumerate(idxs): + plt_idx = i * num_classes + y + 1 + plt.subplot(samples, num_classes, plt_idx) + plt.imshow(images[idx].reshape((28, 28))) + plt.axis("off") + if i == 0: + plt.title(cls) + + plt.show() + + +def show_ave_MNIST(labels, images, fashion=False): + if not fashion: + item_type = "Digit" + classes = digit_classes + else: + item_type = "Apparel" + classes = fashion_classes + + num_classes = len(classes) + + for y, cls in enumerate(classes): + idxs = np.nonzero([i == y for i in labels]) + print(item_type, y, ":", len(idxs[0]), "images.") + + ave_img = np.mean(np.vstack([images[i] for i in idxs[0]]), axis=0) + # print(ave_img.shape) + + plt.subplot(1, num_classes, y + 1) + plt.imshow(ave_img.reshape((28, 28))) + plt.axis("off") + plt.title(cls) + + plt.show() + + +# ______________________________________________________________________________ +# MDP + + +def make_plot_grid_step_function(columns, rows, U_over_time): + """ipywidgets interactive function supports single parameter as input. + This function creates and return such a function by taking as input + other parameters.""" + + def plot_grid_step(iteration): + data = U_over_time[iteration] + data = defaultdict(lambda: 0, data) + grid = [] + for row in range(rows): + current_row = [] + for column in range(columns): + current_row.append(data[(column, row)]) + grid.append(current_row) + grid.reverse() # output like book + fig = plt.imshow(grid, cmap=plt.cm.bwr, interpolation='nearest') + + plt.axis('off') + fig.axes.get_xaxis().set_visible(False) + fig.axes.get_yaxis().set_visible(False) + + for col in range(len(grid)): + for row in range(len(grid[0])): + magic = grid[col][row] + fig.axes.text(row, col, "{0:.2f}".format(magic), va='center', ha='center') + + plt.show() + + return plot_grid_step + + +def make_visualize(slider): + """Takes an input a sliderand returns callback function + for timer and animation.""" + + def visualize_callback(visualize, time_step): + if visualize is True: + for i in range(slider.min, slider.max + 1): + slider.value = i + time.sleep(float(time_step)) + + return visualize_callback + + +# ______________________________________________________________________________ + + +_canvas = """ + +
    + +
    + + +""" # noqa + + +class Canvas: + """Inherit from this class to manage the HTML canvas element in jupyter notebooks. + To create an object of this class any_name_xyz = Canvas("any_name_xyz") + The first argument given must be the name of the object being created. + IPython must be able to reference the variable name that is being passed.""" + + def __init__(self, varname, width=800, height=600, cid=None): + self.name = varname + self.cid = cid or varname + self.width = width + self.height = height + self.html = _canvas.format(self.cid, self.width, self.height, self.name) + self.exec_list = [] + display_html(self.html) + + def mouse_click(self, x, y): + """Override this method to handle mouse click at position (x, y)""" + raise NotImplementedError + + def mouse_move(self, x, y): + raise NotImplementedError + + def execute(self, exec_str): + """Stores the command to be executed to a list which is used later during update()""" + if not isinstance(exec_str, str): + print("Invalid execution argument:", exec_str) + self.alert("Received invalid execution command format") + prefix = "{0}_canvas_object.".format(self.cid) + self.exec_list.append(prefix + exec_str + ';') + + def fill(self, r, g, b): + """Changes the fill color to a color in rgb format""" + self.execute("fill({0}, {1}, {2})".format(r, g, b)) + + def stroke(self, r, g, b): + """Changes the colors of line/strokes to rgb""" + self.execute("stroke({0}, {1}, {2})".format(r, g, b)) + + def strokeWidth(self, w): + """Changes the width of lines/strokes to 'w' pixels""" + self.execute("strokeWidth({0})".format(w)) + + def rect(self, x, y, w, h): + """Draw a rectangle with 'w' width, 'h' height and (x, y) as the top-left corner""" + self.execute("rect({0}, {1}, {2}, {3})".format(x, y, w, h)) + + def rect_n(self, xn, yn, wn, hn): + """Similar to rect(), but the dimensions are normalized to fall between 0 and 1""" + x = round(xn * self.width) + y = round(yn * self.height) + w = round(wn * self.width) + h = round(hn * self.height) + self.rect(x, y, w, h) + + def line(self, x1, y1, x2, y2): + """Draw a line from (x1, y1) to (x2, y2)""" + self.execute("line({0}, {1}, {2}, {3})".format(x1, y1, x2, y2)) + + def line_n(self, x1n, y1n, x2n, y2n): + """Similar to line(), but the dimensions are normalized to fall between 0 and 1""" + x1 = round(x1n * self.width) + y1 = round(y1n * self.height) + x2 = round(x2n * self.width) + y2 = round(y2n * self.height) + self.line(x1, y1, x2, y2) + + def arc(self, x, y, r, start, stop): + """Draw an arc with (x, y) as centre, 'r' as radius from angles 'start' to 'stop'""" + self.execute("arc({0}, {1}, {2}, {3}, {4})".format(x, y, r, start, stop)) + + def arc_n(self, xn, yn, rn, start, stop): + """Similar to arc(), but the dimensions are normalized to fall between 0 and 1 + The normalizing factor for radius is selected between width and height by + seeing which is smaller.""" + x = round(xn * self.width) + y = round(yn * self.height) + r = round(rn * min(self.width, self.height)) + self.arc(x, y, r, start, stop) + + def clear(self): + """Clear the HTML canvas""" + self.execute("clear()") + + def font(self, font): + """Changes the font of text""" + self.execute('font("{0}")'.format(font)) + + def text(self, txt, x, y, fill=True): + """Display a text at (x, y)""" + if fill: + self.execute('fill_text("{0}", {1}, {2})'.format(txt, x, y)) + else: + self.execute('stroke_text("{0}", {1}, {2})'.format(txt, x, y)) + + def text_n(self, txt, xn, yn, fill=True): + """Similar to text(), but with normalized coordinates""" + x = round(xn * self.width) + y = round(yn * self.height) + self.text(txt, x, y, fill) + + def alert(self, message): + """Immediately display an alert""" + display_html(''.format(message)) + + def update(self): + """Execute the JS code to execute the commands queued by execute()""" + exec_code = "" + self.exec_list = [] + display_html(exec_code) + + +def display_html(html_string): + display(HTML(html_string)) + + +################################################################################ + + +class Canvas_TicTacToe(Canvas): + """Play a 3x3 TicTacToe game on HTML canvas""" + + def __init__(self, varname, player_1='human', player_2='random', + width=300, height=350, cid=None): + valid_players = ('human', 'random', 'alpha_beta') + if player_1 not in valid_players or player_2 not in valid_players: + raise TypeError("Players must be one of {}".format(valid_players)) + super().__init__(varname, width, height, cid) + self.ttt = TicTacToe() + self.state = self.ttt.initial + self.turn = 0 + self.strokeWidth(5) + self.players = (player_1, player_2) + self.font("20px Arial") + self.draw_board() + + def mouse_click(self, x, y): + player = self.players[self.turn] + if self.ttt.terminal_test(self.state): + if 0.55 <= x / self.width <= 0.95 and 6 / 7 <= y / self.height <= 6 / 7 + 1 / 8: + self.state = self.ttt.initial + self.turn = 0 + self.draw_board() + return + + if player == 'human': + x, y = int(3 * x / self.width) + 1, int(3 * y / (self.height * 6 / 7)) + 1 + if (x, y) not in self.ttt.actions(self.state): + # Invalid move + return + move = (x, y) + elif player == 'alpha_beta': + move = alpha_beta_player(self.ttt, self.state) + else: + move = random_player(self.ttt, self.state) + self.state = self.ttt.result(self.state, move) + self.turn ^= 1 + self.draw_board() + + def draw_board(self): + self.clear() + self.stroke(0, 0, 0) + offset = 1 / 20 + self.line_n(0 + offset, (1 / 3) * 6 / 7, 1 - offset, (1 / 3) * 6 / 7) + self.line_n(0 + offset, (2 / 3) * 6 / 7, 1 - offset, (2 / 3) * 6 / 7) + self.line_n(1 / 3, (0 + offset) * 6 / 7, 1 / 3, (1 - offset) * 6 / 7) + self.line_n(2 / 3, (0 + offset) * 6 / 7, 2 / 3, (1 - offset) * 6 / 7) + + board = self.state.board + for mark in board: + if board[mark] == 'X': + self.draw_x(mark) + elif board[mark] == 'O': + self.draw_o(mark) + if self.ttt.terminal_test(self.state): + # End game message + utility = self.ttt.utility(self.state, self.ttt.to_move(self.ttt.initial)) + if utility == 0: + self.text_n('Game Draw!', offset, 6 / 7 + offset) + else: + self.text_n('Player {} wins!'.format("XO"[utility < 0]), offset, 6 / 7 + offset) + # Find the 3 and draw a line + self.stroke([255, 0][self.turn], [0, 255][self.turn], 0) + for i in range(3): + if all([(i + 1, j + 1) in self.state.board for j in range(3)]) and \ + len({self.state.board[(i + 1, j + 1)] for j in range(3)}) == 1: + self.line_n(i / 3 + 1 / 6, offset * 6 / 7, i / 3 + 1 / 6, (1 - offset) * 6 / 7) + if all([(j + 1, i + 1) in self.state.board for j in range(3)]) and \ + len({self.state.board[(j + 1, i + 1)] for j in range(3)}) == 1: + self.line_n(offset, (i / 3 + 1 / 6) * 6 / 7, 1 - offset, (i / 3 + 1 / 6) * 6 / 7) + if all([(i + 1, i + 1) in self.state.board for i in range(3)]) and \ + len({self.state.board[(i + 1, i + 1)] for i in range(3)}) == 1: + self.line_n(offset, offset * 6 / 7, 1 - offset, (1 - offset) * 6 / 7) + if all([(i + 1, 3 - i) in self.state.board for i in range(3)]) and \ + len({self.state.board[(i + 1, 3 - i)] for i in range(3)}) == 1: + self.line_n(offset, (1 - offset) * 6 / 7, 1 - offset, offset * 6 / 7) + # restart button + self.fill(0, 0, 255) + self.rect_n(0.5 + offset, 6 / 7, 0.4, 1 / 8) + self.fill(0, 0, 0) + self.text_n('Restart', 0.5 + 2 * offset, 13 / 14) + else: # Print which player's turn it is + self.text_n("Player {}'s move({})".format("XO"[self.turn], self.players[self.turn]), + offset, 6 / 7 + offset) + + self.update() + + def draw_x(self, position): + self.stroke(0, 255, 0) + x, y = [i - 1 for i in position] + offset = 1 / 15 + self.line_n(x / 3 + offset, (y / 3 + offset) * 6 / 7, x / 3 + 1 / 3 - offset, (y / 3 + 1 / 3 - offset) * 6 / 7) + self.line_n(x / 3 + 1 / 3 - offset, (y / 3 + offset) * 6 / 7, x / 3 + offset, (y / 3 + 1 / 3 - offset) * 6 / 7) + + def draw_o(self, position): + self.stroke(255, 0, 0) + x, y = [i - 1 for i in position] + self.arc_n(x / 3 + 1 / 6, (y / 3 + 1 / 6) * 6 / 7, 1 / 9, 0, 360) + + +class Canvas_min_max(Canvas): + """MinMax for Fig52Extended on HTML canvas""" + + def __init__(self, varname, util_list, width=800, height=600, cid=None): + super.__init__(varname, width, height, cid) + self.utils = {node: util for node, util in zip(range(13, 40), util_list)} + self.game = Fig52Extended() + self.game.utils = self.utils + self.nodes = list(range(40)) + self.l = 1 / 40 + self.node_pos = {} + for i in range(4): + base = len(self.node_pos) + row_size = 3 ** i + for node in [base + j for j in range(row_size)]: + self.node_pos[node] = ((node - base) / row_size + 1 / (2 * row_size) - self.l / 2, + self.l / 2 + (self.l + (1 - 5 * self.l) / 3) * i) + self.font("12px Arial") + self.node_stack = [] + self.explored = {node for node in self.utils} + self.thick_lines = set() + self.change_list = [] + self.draw_graph() + self.stack_manager = self.stack_manager_gen() + + def min_max(self, node): + game = self.game + player = game.to_move(node) + + def max_value(node): + if game.terminal_test(node): + return game.utility(node, player) + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + max_a = max(game.actions(node), key=lambda x: min_value(game.result(node, x))) + max_node = game.result(node, max_a) + self.utils[node] = self.utils[max_node] + x1, y1 = self.node_pos[node] + x2, y2 = self.node_pos[max_node] + self.change_list.append(('l', (node, max_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return self.utils[node] + + def min_value(node): + if game.terminal_test(node): + return game.utility(node, player) + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + min_a = min(game.actions(node), key=lambda x: max_value(game.result(node, x))) + min_node = game.result(node, min_a) + self.utils[node] = self.utils[min_node] + x1, y1 = self.node_pos[node] + x2, y2 = self.node_pos[min_node] + self.change_list.append(('l', (node, min_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return self.utils[node] + + return max_value(node) + + def stack_manager_gen(self): + self.min_max(0) + for change in self.change_list: + if change[0] == 'a': + self.node_stack.append(change[1]) + elif change[0] == 'e': + self.explored.add(change[1]) + elif change[0] == 'h': + yield + elif change[0] == 'l': + self.thick_lines.add(change[1]) + elif change[0] == 'p': + self.node_stack.pop() + + def mouse_click(self, x, y): + try: + self.stack_manager.send(None) + except StopIteration: + pass + self.draw_graph() + + def draw_graph(self): + self.clear() + # draw nodes + self.stroke(0, 0, 0) + self.strokeWidth(1) + # highlight for nodes in stack + for node in self.node_stack: + x, y = self.node_pos[node] + self.fill(200, 200, 0) + self.rect_n(x - self.l / 5, y - self.l / 5, self.l * 7 / 5, self.l * 7 / 5) + for node in self.nodes: + x, y = self.node_pos[node] + if node in self.explored: + self.fill(255, 255, 255) + else: + self.fill(200, 200, 200) + self.rect_n(x, y, self.l, self.l) + self.line_n(x, y, x + self.l, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.l, y + self.l, x + self.l, y) + self.line_n(x + self.l, y + self.l, x, y + self.l) + self.fill(0, 0, 0) + if node in self.explored: + self.text_n(self.utils[node], x + self.l / 10, y + self.l * 9 / 10) + # draw edges + for i in range(13): + x1, y1 = self.node_pos[i][0] + self.l / 2, self.node_pos[i][1] + self.l + for j in range(3): + x2, y2 = self.node_pos[i * 3 + j + 1][0] + self.l / 2, self.node_pos[i * 3 + j + 1][1] + if i in [1, 2, 3]: + self.stroke(200, 0, 0) + else: + self.stroke(0, 200, 0) + if (i, j) in self.thick_lines: + self.strokeWidth(3) + else: + self.strokeWidth(1) + self.line_n(x1, y1, x2, y2) + self.update() + + +class Canvas_alpha_beta(Canvas): + """Alpha-beta pruning for Fig52Extended on HTML canvas""" + + def __init__(self, varname, util_list, width=800, height=600, cid=None): + super().__init__(varname, width, height, cid) + self.utils = {node: util for node, util in zip(range(13, 40), util_list)} + self.game = Fig52Extended() + self.game.utils = self.utils + self.nodes = list(range(40)) + self.l = 1 / 40 + self.node_pos = {} + for i in range(4): + base = len(self.node_pos) + row_size = 3 ** i + for node in [base + j for j in range(row_size)]: + self.node_pos[node] = ((node - base) / row_size + 1 / (2 * row_size) - self.l / 2, + 3 * self.l / 2 + (self.l + (1 - 6 * self.l) / 3) * i) + self.font("12px Arial") + self.node_stack = [] + self.explored = {node for node in self.utils} + self.pruned = set() + self.ab = {} + self.thick_lines = set() + self.change_list = [] + self.draw_graph() + self.stack_manager = self.stack_manager_gen() + + def alpha_beta_search(self, node): + game = self.game + player = game.to_move(node) + + # Functions used by alpha_beta + def max_value(node, alpha, beta): + if game.terminal_test(node): + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + self.change_list.append(('p',)) + return game.utility(node, player) + v = -np.inf + self.change_list.append(('a', node)) + self.change_list.append(('ab', node, v, beta)) + self.change_list.append(('h',)) + for a in game.actions(node): + min_val = min_value(game.result(node, a), alpha, beta) + if v < min_val: + v = min_val + max_node = game.result(node, a) + self.change_list.append(('ab', node, v, beta)) + if v >= beta: + self.change_list.append(('h',)) + self.pruned.add(node) + break + alpha = max(alpha, v) + self.utils[node] = v + if node not in self.pruned: + self.change_list.append(('l', (node, max_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return v + + def min_value(node, alpha, beta): + if game.terminal_test(node): + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + self.change_list.append(('p',)) + return game.utility(node, player) + v = np.inf + self.change_list.append(('a', node)) + self.change_list.append(('ab', node, alpha, v)) + self.change_list.append(('h',)) + for a in game.actions(node): + max_val = max_value(game.result(node, a), alpha, beta) + if v > max_val: + v = max_val + min_node = game.result(node, a) + self.change_list.append(('ab', node, alpha, v)) + if v <= alpha: + self.change_list.append(('h',)) + self.pruned.add(node) + break + beta = min(beta, v) + self.utils[node] = v + if node not in self.pruned: + self.change_list.append(('l', (node, min_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return v + + return max_value(node, -np.inf, np.inf) + + def stack_manager_gen(self): + self.alpha_beta_search(0) + for change in self.change_list: + if change[0] == 'a': + self.node_stack.append(change[1]) + elif change[0] == 'ab': + self.ab[change[1]] = change[2:] + elif change[0] == 'e': + self.explored.add(change[1]) + elif change[0] == 'h': + yield + elif change[0] == 'l': + self.thick_lines.add(change[1]) + elif change[0] == 'p': + self.node_stack.pop() + + def mouse_click(self, x, y): + try: + self.stack_manager.send(None) + except StopIteration: + pass + self.draw_graph() + + def draw_graph(self): + self.clear() + # draw nodes + self.stroke(0, 0, 0) + self.strokeWidth(1) + # highlight for nodes in stack + for node in self.node_stack: + x, y = self.node_pos[node] + # alpha > beta + if node not in self.explored and self.ab[node][0] > self.ab[node][1]: + self.fill(200, 100, 100) + else: + self.fill(200, 200, 0) + self.rect_n(x - self.l / 5, y - self.l / 5, self.l * 7 / 5, self.l * 7 / 5) + for node in self.nodes: + x, y = self.node_pos[node] + if node in self.explored: + if node in self.pruned: + self.fill(50, 50, 50) + else: + self.fill(255, 255, 255) + else: + self.fill(200, 200, 200) + self.rect_n(x, y, self.l, self.l) + self.line_n(x, y, x + self.l, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.l, y + self.l, x + self.l, y) + self.line_n(x + self.l, y + self.l, x, y + self.l) + self.fill(0, 0, 0) + if node in self.explored and node not in self.pruned: + self.text_n(self.utils[node], x + self.l / 10, y + self.l * 9 / 10) + # draw edges + for i in range(13): + x1, y1 = self.node_pos[i][0] + self.l / 2, self.node_pos[i][1] + self.l + for j in range(3): + x2, y2 = self.node_pos[i * 3 + j + 1][0] + self.l / 2, self.node_pos[i * 3 + j + 1][1] + if i in [1, 2, 3]: + self.stroke(200, 0, 0) + else: + self.stroke(0, 200, 0) + if (i, j) in self.thick_lines: + self.strokeWidth(3) + else: + self.strokeWidth(1) + self.line_n(x1, y1, x2, y2) + # display alpha and beta + for node in self.node_stack: + if node not in self.explored: + x, y = self.node_pos[node] + alpha, beta = self.ab[node] + self.text_n(alpha, x - self.l / 2, y - self.l / 10) + self.text_n(beta, x + self.l, y - self.l / 10) + self.update() + + +class Canvas_fol_bc_ask(Canvas): + """fol_bc_ask() on HTML canvas""" + + def __init__(self, varname, kb, query, width=800, height=600, cid=None): + super().__init__(varname, width, height, cid) + self.kb = kb + self.query = query + self.l = 1 / 20 + self.b = 3 * self.l + bc_out = list(self.fol_bc_ask()) + if len(bc_out) == 0: + self.valid = False + else: + self.valid = True + graph = bc_out[0][0][0] + s = bc_out[0][1] + while True: + new_graph = subst(s, graph) + if graph == new_graph: + break + graph = new_graph + self.make_table(graph) + self.context = None + self.draw_table() + + def fol_bc_ask(self): + KB = self.kb + query = self.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_mm(rhs, goal, theta)): + yield ([(goal, theta1[0])], theta1[1]) + + def fol_bc_and(KB, goals, theta): + if theta is None: + pass + elif not goals: + yield ([], theta) + else: + first, rest = goals[0], goals[1:] + for theta1 in fol_bc_or(KB, subst(theta, first), theta): + for theta2 in fol_bc_and(KB, rest, theta1[1]): + yield (theta1[0] + theta2[0], theta2[1]) + + return fol_bc_or(KB, query, {}) + + def make_table(self, graph): + table = [] + pos = {} + links = set() + edges = set() + + def dfs(node, depth): + if len(table) <= depth: + table.append([]) + pos = len(table[depth]) + table[depth].append(node[0]) + for child in node[1]: + child_id = dfs(child, depth + 1) + links.add(((depth, pos), child_id)) + return (depth, pos) + + dfs(graph, 0) + y_off = 0.85 / len(table) + for i, row in enumerate(table): + x_off = 0.95 / len(row) + for j, node in enumerate(row): + pos[(i, j)] = (0.025 + j * x_off + (x_off - self.b) / 2, 0.025 + i * y_off + (y_off - self.l) / 2) + for p, c in links: + x1, y1 = pos[p] + x2, y2 = pos[c] + edges.add((x1 + self.b / 2, y1 + self.l, x2 + self.b / 2, y2)) + + self.table = table + self.pos = pos + self.edges = edges + + def mouse_click(self, x, y): + x, y = x / self.width, y / self.height + for node in self.pos: + xs, ys = self.pos[node] + xe, ye = xs + self.b, ys + self.l + if xs <= x <= xe and ys <= y <= ye: + self.context = node + break + self.draw_table() + + def draw_table(self): + self.clear() + self.strokeWidth(3) + self.stroke(0, 0, 0) + self.font("12px Arial") + if self.valid: + # draw nodes + for i, j in self.pos: + x, y = self.pos[(i, j)] + self.fill(200, 200, 200) + self.rect_n(x, y, self.b, self.l) + self.line_n(x, y, x + self.b, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.b, y, x + self.b, y + self.l) + self.line_n(x, y + self.l, x + self.b, y + self.l) + self.fill(0, 0, 0) + self.text_n(self.table[i][j], x + 0.01, y + self.l - 0.01) + # draw edges + for x1, y1, x2, y2 in self.edges: + self.line_n(x1, y1, x2, y2) + else: + self.fill(255, 0, 0) + self.rect_n(0, 0, 1, 1) + # text area + self.fill(255, 255, 255) + self.rect_n(0, 0.9, 1, 0.1) + self.strokeWidth(5) + self.stroke(0, 0, 0) + self.line_n(0, 0.9, 1, 0.9) + self.font("22px Arial") + self.fill(0, 0, 0) + self.text_n(self.table[self.context[0]][self.context[1]] if self.context else "Click for text", 0.025, 0.975) + self.update() + + +############################################################################################################ + +##################### Functions to assist plotting in search.ipynb #################### + +############################################################################################################ + + +def show_map(graph_data, node_colors=None): + G = nx.Graph(graph_data['graph_dict']) + node_colors = node_colors or graph_data['node_colors'] + node_positions = graph_data['node_positions'] + node_label_pos = graph_data['node_label_positions'] + edge_weights = graph_data['edge_weights'] + + # set the size of the plot + plt.figure(figsize=(18, 13)) + # draw the graph (both nodes and edges) with locations from romania_locations + nx.draw(G, pos={k: node_positions[k] for k in G.nodes()}, + node_color=[node_colors[node] for node in G.nodes()], linewidths=0.3, edgecolors='k') + + # draw labels for nodes + node_label_handles = nx.draw_networkx_labels(G, pos=node_label_pos, font_size=14) + + # add a white bounding box behind the node labels + [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()] + + # add edge lables to the graph + nx.draw_networkx_edge_labels(G, pos=node_positions, edge_labels=edge_weights, font_size=14) + + # add a legend + white_circle = lines.Line2D([], [], color="white", marker='o', markersize=15, markerfacecolor="white") + orange_circle = lines.Line2D([], [], color="orange", marker='o', markersize=15, markerfacecolor="orange") + red_circle = lines.Line2D([], [], color="red", marker='o', markersize=15, markerfacecolor="red") + gray_circle = lines.Line2D([], [], color="gray", marker='o', markersize=15, markerfacecolor="gray") + green_circle = lines.Line2D([], [], color="green", marker='o', markersize=15, markerfacecolor="green") + plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle), + ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'), + numpoints=1, prop={'size': 16}, loc=(.8, .75)) + + # show the plot. No need to use in notebooks. nx.draw will show the graph itself. + plt.show() + + +# helper functions for visualisations + +def final_path_colors(initial_node_colors, problem, solution): + "Return a node_colors dict of the final path provided the problem and solution." + + # get initial node colors + final_colors = dict(initial_node_colors) + # color all the nodes in solution and starting node to green + final_colors[problem.initial] = "green" + for node in solution: + final_colors[node] = "green" + return final_colors + + +def display_visual(graph_data, user_input, algorithm=None, problem=None): + initial_node_colors = graph_data['node_colors'] + if user_input is False: + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors=all_node_colors[iteration]) + except: + pass + + def visualize_callback(visualize): + if visualize is True: + button.value = False + + global all_node_colors + + iterations, all_node_colors, node = algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + # time.sleep(.5) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration=slider) + display(slider_visual) + + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, visualize=button) + display(button_visual) + + if user_input is True: + node_colors = dict(initial_node_colors) + if isinstance(algorithm, dict): + assert set(algorithm.keys()).issubset({"Breadth First Tree Search", + "Depth First Tree Search", + "Breadth First Search", + "Depth First Graph Search", + "Best First Graph Search", + "Uniform Cost Search", + "Depth Limited Search", + "Iterative Deepening Search", + "Greedy Best First Search", + "A-star Search", + "Recursive Best First Search"}) + + algo_dropdown = widgets.Dropdown(description="Search algorithm: ", + options=sorted(list(algorithm.keys())), + value="Breadth First Tree Search") + display(algo_dropdown) + elif algorithm is None: + print("No algorithm to run.") + return 0 + + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors=all_node_colors[iteration]) + except: + pass + + def visualize_callback(visualize): + if visualize is True: + button.value = False + + problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map) + global all_node_colors + + user_algorithm = algorithm[algo_dropdown.value] + + iterations, all_node_colors, node = user_algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + # time.sleep(.5) + + start_dropdown = widgets.Dropdown(description="Start city: ", + options=sorted(list(node_colors.keys())), value="Arad") + display(start_dropdown) + + end_dropdown = widgets.Dropdown(description="Goal city: ", + options=sorted(list(node_colors.keys())), value="Fagaras") + display(end_dropdown) + + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, visualize=button) + display(button_visual) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration=slider) + display(slider_visual) + + +# Function to plot NQueensCSP in csp.py and NQueensProblem in search.py +def plot_NQueens(solution): + n = len(solution) + board = np.array([2 * int((i + j) % 2) for j in range(n) for i in range(n)]).reshape((n, n)) + im = Image.open('images/queen_s.png') + height = im.size[1] + im = np.array(im).astype(np.float) / 255 + fig = plt.figure(figsize=(7, 7)) + ax = fig.add_subplot(111) + ax.set_title('{} Queens'.format(n)) + plt.imshow(board, cmap='binary', interpolation='nearest') + # NQueensCSP gives a solution as a dictionary + if isinstance(solution, dict): + for (k, v) in solution.items(): + newax = fig.add_axes([0.064 + (k * 0.112), 0.062 + ((7 - v) * 0.112), 0.1, 0.1], zorder=1) + newax.imshow(im) + newax.axis('off') + # NQueensProblem gives a solution as a list + elif isinstance(solution, list): + for (k, v) in enumerate(solution): + newax = fig.add_axes([0.064 + (k * 0.112), 0.062 + ((7 - v) * 0.112), 0.1, 0.1], zorder=1) + newax.imshow(im) + newax.axis('off') + fig.tight_layout() + plt.show() + + +# Function to plot a heatmap, given a grid +def heatmap(grid, cmap='binary', interpolation='nearest'): + fig = plt.figure(figsize=(7, 7)) + ax = fig.add_subplot(111) + ax.set_title('Heatmap') + plt.imshow(grid, cmap=cmap, interpolation=interpolation) + fig.tight_layout() + plt.show() + + +# Generates a gaussian kernel +def gaussian_kernel(l=5, sig=1.0): + ax = np.arange(-l // 2 + 1., l // 2 + 1.) + xx, yy = np.meshgrid(ax, ax) + kernel = np.exp(-(xx ** 2 + yy ** 2) / (2. * sig ** 2)) + return kernel + + +# Plots utility function for a POMDP +def plot_pomdp_utility(utility): + save = utility['0'][0] + delete = utility['1'][0] + ask_save = utility['2'][0] + ask_delete = utility['2'][-1] + left = (save[0] - ask_save[0]) / (save[0] - ask_save[0] + ask_save[1] - save[1]) + right = (delete[0] - ask_delete[0]) / (delete[0] - ask_delete[0] + ask_delete[1] - delete[1]) + + colors = ['g', 'b', 'k'] + for action in utility: + for value in utility[action]: + plt.plot(value, color=colors[int(action)]) + plt.vlines([left, right], -20, 10, linestyles='dashed', colors='c') + plt.ylim(-20, 13) + plt.xlim(0, 1) + plt.text(left / 2 - 0.05, 10, 'Save') + plt.text((right + left) / 2 - 0.02, 10, 'Ask') + plt.text((right + 1) / 2 - 0.07, 10, 'Delete') + plt.show() diff --git a/notebook4e.py b/notebook4e.py new file mode 100644 index 000000000..5b03081c6 --- /dev/null +++ b/notebook4e.py @@ -0,0 +1,1158 @@ +import time +from collections import defaultdict +from inspect import getsource + +import ipywidgets as widgets +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np +from IPython.display import HTML +from IPython.display import display +from PIL import Image +from matplotlib import lines +from matplotlib.colors import ListedColormap + +from games import TicTacToe, alpha_beta_player, random_player, Fig52Extended +from learning import DataSet +from logic import parse_definite_clause, standardize_variables, unify_mm, subst +from search import GraphProblem, romania_map + + +# ______________________________________________________________________________ +# Magic Words + + +def pseudocode(algorithm): + """Print the pseudocode for the given algorithm.""" + from urllib.request import urlopen + from IPython.display import Markdown + + algorithm = algorithm.replace(' ', '-') + url = "https://raw.githubusercontent.com/aimacode/aima-pseudocode/master/md/{}.md".format(algorithm) + f = urlopen(url) + md = f.read().decode('utf-8') + md = md.split('\n', 1)[-1].strip() + md = '#' + md + return Markdown(md) + + +def psource(*functions): + """Print the source code for the given function(s).""" + source_code = '\n\n'.join(getsource(fn) for fn in functions) + try: + from pygments.formatters import HtmlFormatter + from pygments.lexers import PythonLexer + from pygments import highlight + + display(HTML(highlight(source_code, PythonLexer(), HtmlFormatter(full=True)))) + + except ImportError: + print(source_code) + + +def plot_model_boundary(dataset, attr1, attr2, model=None): + # prepare data + examples = np.asarray(dataset.examples) + X = np.asarray([examples[:, attr1], examples[:, attr2]]) + y = examples[:, dataset.target] + h = 0.1 + + # create color maps + cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#00AAFF']) + cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#00AAFF']) + + # calculate min, max and limits + x_min, x_max = X[0].min() - 1, X[0].max() + 1 + y_min, y_max = X[1].min() - 1, X[1].max() + 1 + # mesh the grid + xx, yy = np.meshgrid(np.arange(x_min, x_max, h), + np.arange(y_min, y_max, h)) + Z = [] + for grid in zip(xx.ravel(), yy.ravel()): + # put them back to the example + grid = np.round(grid, decimals=1).tolist() + Z.append(model(grid)) + # Put the result into a color plot + Z = np.asarray(Z) + Z = Z.reshape(xx.shape) + plt.figure() + plt.pcolormesh(xx, yy, Z, cmap=cmap_light) + + # Plot also the training points + plt.scatter(X[0], X[1], c=y, cmap=cmap_bold) + plt.xlim(xx.min(), xx.max()) + plt.ylim(yy.min(), yy.max()) + plt.show() + + +# ______________________________________________________________________________ +# Iris Visualization + + +def show_iris(i=0, j=1, k=2): + """Plots the iris dataset in a 3D plot. + The three axes are given by i, j and k, + which correspond to three of the four iris features.""" + + plt.rcParams.update(plt.rcParamsDefault) + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + iris = DataSet(name="iris") + buckets = iris.split_values_by_classes() + + features = ["Sepal Length", "Sepal Width", "Petal Length", "Petal Width"] + f1, f2, f3 = features[i], features[j], features[k] + + a_setosa = [v[i] for v in buckets["setosa"]] + b_setosa = [v[j] for v in buckets["setosa"]] + c_setosa = [v[k] for v in buckets["setosa"]] + + a_virginica = [v[i] for v in buckets["virginica"]] + b_virginica = [v[j] for v in buckets["virginica"]] + c_virginica = [v[k] for v in buckets["virginica"]] + + a_versicolor = [v[i] for v in buckets["versicolor"]] + b_versicolor = [v[j] for v in buckets["versicolor"]] + c_versicolor = [v[k] for v in buckets["versicolor"]] + + for c, m, sl, sw, pl in [('b', 's', a_setosa, b_setosa, c_setosa), + ('g', '^', a_virginica, b_virginica, c_virginica), + ('r', 'o', a_versicolor, b_versicolor, c_versicolor)]: + ax.scatter(sl, sw, pl, c=c, marker=m) + + ax.set_xlabel(f1) + ax.set_ylabel(f2) + ax.set_zlabel(f3) + + plt.show() + + +# ______________________________________________________________________________ +# MNIST + + +def load_MNIST(path="aima-data/MNIST/Digits", fashion=False): + import os, struct + import array + import numpy as np + + if fashion: + path = "aima-data/MNIST/Fashion" + + plt.rcParams.update(plt.rcParamsDefault) + plt.rcParams['figure.figsize'] = (10.0, 8.0) + plt.rcParams['image.interpolation'] = 'nearest' + plt.rcParams['image.cmap'] = 'gray' + + train_img_file = open(os.path.join(path, "train-images-idx3-ubyte"), "rb") + train_lbl_file = open(os.path.join(path, "train-labels-idx1-ubyte"), "rb") + test_img_file = open(os.path.join(path, "t10k-images-idx3-ubyte"), "rb") + test_lbl_file = open(os.path.join(path, 't10k-labels-idx1-ubyte'), "rb") + + magic_nr, tr_size, tr_rows, tr_cols = struct.unpack(">IIII", train_img_file.read(16)) + tr_img = array.array("B", train_img_file.read()) + train_img_file.close() + magic_nr, tr_size = struct.unpack(">II", train_lbl_file.read(8)) + tr_lbl = array.array("b", train_lbl_file.read()) + train_lbl_file.close() + + magic_nr, te_size, te_rows, te_cols = struct.unpack(">IIII", test_img_file.read(16)) + te_img = array.array("B", test_img_file.read()) + test_img_file.close() + magic_nr, te_size = struct.unpack(">II", test_lbl_file.read(8)) + te_lbl = array.array("b", test_lbl_file.read()) + test_lbl_file.close() + + # print(len(tr_img), len(tr_lbl), tr_size) + # print(len(te_img), len(te_lbl), te_size) + + train_img = np.zeros((tr_size, tr_rows * tr_cols), dtype=np.int16) + train_lbl = np.zeros((tr_size,), dtype=np.int8) + for i in range(tr_size): + train_img[i] = np.array(tr_img[i * tr_rows * tr_cols: (i + 1) * tr_rows * tr_cols]).reshape((tr_rows * te_cols)) + train_lbl[i] = tr_lbl[i] + + test_img = np.zeros((te_size, te_rows * te_cols), dtype=np.int16) + test_lbl = np.zeros((te_size,), dtype=np.int8) + for i in range(te_size): + test_img[i] = np.array(te_img[i * te_rows * te_cols: (i + 1) * te_rows * te_cols]).reshape((te_rows * te_cols)) + test_lbl[i] = te_lbl[i] + + return (train_img, train_lbl, test_img, test_lbl) + + +digit_classes = [str(i) for i in range(10)] +fashion_classes = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", + "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"] + + +def show_MNIST(labels, images, samples=8, fashion=False): + if not fashion: + classes = digit_classes + else: + classes = fashion_classes + + num_classes = len(classes) + + for y, cls in enumerate(classes): + idxs = np.nonzero([i == y for i in labels]) + idxs = np.random.choice(idxs[0], samples, replace=False) + for i, idx in enumerate(idxs): + plt_idx = i * num_classes + y + 1 + plt.subplot(samples, num_classes, plt_idx) + plt.imshow(images[idx].reshape((28, 28))) + plt.axis("off") + if i == 0: + plt.title(cls) + + plt.show() + + +def show_ave_MNIST(labels, images, fashion=False): + if not fashion: + item_type = "Digit" + classes = digit_classes + else: + item_type = "Apparel" + classes = fashion_classes + + num_classes = len(classes) + + for y, cls in enumerate(classes): + idxs = np.nonzero([i == y for i in labels]) + print(item_type, y, ":", len(idxs[0]), "images.") + + ave_img = np.mean(np.vstack([images[i] for i in idxs[0]]), axis=0) + # print(ave_img.shape) + + plt.subplot(1, num_classes, y + 1) + plt.imshow(ave_img.reshape((28, 28))) + plt.axis("off") + plt.title(cls) + + plt.show() + + +# ______________________________________________________________________________ +# MDP + + +def make_plot_grid_step_function(columns, rows, U_over_time): + """ipywidgets interactive function supports single parameter as input. + This function creates and return such a function by taking as input + other parameters.""" + + def plot_grid_step(iteration): + data = U_over_time[iteration] + data = defaultdict(lambda: 0, data) + grid = [] + for row in range(rows): + current_row = [] + for column in range(columns): + current_row.append(data[(column, row)]) + grid.append(current_row) + grid.reverse() # output like book + fig = plt.imshow(grid, cmap=plt.cm.bwr, interpolation='nearest') + + plt.axis('off') + fig.axes.get_xaxis().set_visible(False) + fig.axes.get_yaxis().set_visible(False) + + for col in range(len(grid)): + for row in range(len(grid[0])): + magic = grid[col][row] + fig.axes.text(row, col, "{0:.2f}".format(magic), va='center', ha='center') + + plt.show() + + return plot_grid_step + + +def make_visualize(slider): + """Takes an input a sliderand returns callback function + for timer and animation.""" + + def visualize_callback(visualize, time_step): + if visualize is True: + for i in range(slider.min, slider.max + 1): + slider.value = i + time.sleep(float(time_step)) + + return visualize_callback + + +# ______________________________________________________________________________ + + +_canvas = """ + +
    + +
    + + +""" # noqa + + +class Canvas: + """Inherit from this class to manage the HTML canvas element in jupyter notebooks. + To create an object of this class any_name_xyz = Canvas("any_name_xyz") + The first argument given must be the name of the object being created. + IPython must be able to reference the variable name that is being passed.""" + + def __init__(self, varname, width=800, height=600, cid=None): + self.name = varname + self.cid = cid or varname + self.width = width + self.height = height + self.html = _canvas.format(self.cid, self.width, self.height, self.name) + self.exec_list = [] + display_html(self.html) + + def mouse_click(self, x, y): + """Override this method to handle mouse click at position (x, y)""" + raise NotImplementedError + + def mouse_move(self, x, y): + raise NotImplementedError + + def execute(self, exec_str): + """Stores the command to be executed to a list which is used later during update()""" + if not isinstance(exec_str, str): + print("Invalid execution argument:", exec_str) + self.alert("Received invalid execution command format") + prefix = "{0}_canvas_object.".format(self.cid) + self.exec_list.append(prefix + exec_str + ';') + + def fill(self, r, g, b): + """Changes the fill color to a color in rgb format""" + self.execute("fill({0}, {1}, {2})".format(r, g, b)) + + def stroke(self, r, g, b): + """Changes the colors of line/strokes to rgb""" + self.execute("stroke({0}, {1}, {2})".format(r, g, b)) + + def strokeWidth(self, w): + """Changes the width of lines/strokes to 'w' pixels""" + self.execute("strokeWidth({0})".format(w)) + + def rect(self, x, y, w, h): + """Draw a rectangle with 'w' width, 'h' height and (x, y) as the top-left corner""" + self.execute("rect({0}, {1}, {2}, {3})".format(x, y, w, h)) + + def rect_n(self, xn, yn, wn, hn): + """Similar to rect(), but the dimensions are normalized to fall between 0 and 1""" + x = round(xn * self.width) + y = round(yn * self.height) + w = round(wn * self.width) + h = round(hn * self.height) + self.rect(x, y, w, h) + + def line(self, x1, y1, x2, y2): + """Draw a line from (x1, y1) to (x2, y2)""" + self.execute("line({0}, {1}, {2}, {3})".format(x1, y1, x2, y2)) + + def line_n(self, x1n, y1n, x2n, y2n): + """Similar to line(), but the dimensions are normalized to fall between 0 and 1""" + x1 = round(x1n * self.width) + y1 = round(y1n * self.height) + x2 = round(x2n * self.width) + y2 = round(y2n * self.height) + self.line(x1, y1, x2, y2) + + def arc(self, x, y, r, start, stop): + """Draw an arc with (x, y) as centre, 'r' as radius from angles 'start' to 'stop'""" + self.execute("arc({0}, {1}, {2}, {3}, {4})".format(x, y, r, start, stop)) + + def arc_n(self, xn, yn, rn, start, stop): + """Similar to arc(), but the dimensions are normalized to fall between 0 and 1 + The normalizing factor for radius is selected between width and height by + seeing which is smaller.""" + x = round(xn * self.width) + y = round(yn * self.height) + r = round(rn * min(self.width, self.height)) + self.arc(x, y, r, start, stop) + + def clear(self): + """Clear the HTML canvas""" + self.execute("clear()") + + def font(self, font): + """Changes the font of text""" + self.execute('font("{0}")'.format(font)) + + def text(self, txt, x, y, fill=True): + """Display a text at (x, y)""" + if fill: + self.execute('fill_text("{0}", {1}, {2})'.format(txt, x, y)) + else: + self.execute('stroke_text("{0}", {1}, {2})'.format(txt, x, y)) + + def text_n(self, txt, xn, yn, fill=True): + """Similar to text(), but with normalized coordinates""" + x = round(xn * self.width) + y = round(yn * self.height) + self.text(txt, x, y, fill) + + def alert(self, message): + """Immediately display an alert""" + display_html(''.format(message)) + + def update(self): + """Execute the JS code to execute the commands queued by execute()""" + exec_code = "" + self.exec_list = [] + display_html(exec_code) + + +def display_html(html_string): + display(HTML(html_string)) + + +################################################################################ + + +class Canvas_TicTacToe(Canvas): + """Play a 3x3 TicTacToe game on HTML canvas""" + + def __init__(self, varname, player_1='human', player_2='random', + width=300, height=350, cid=None): + valid_players = ('human', 'random', 'alpha_beta') + if player_1 not in valid_players or player_2 not in valid_players: + raise TypeError("Players must be one of {}".format(valid_players)) + super().__init__(varname, width, height, cid) + self.ttt = TicTacToe() + self.state = self.ttt.initial + self.turn = 0 + self.strokeWidth(5) + self.players = (player_1, player_2) + self.font("20px Arial") + self.draw_board() + + def mouse_click(self, x, y): + player = self.players[self.turn] + if self.ttt.terminal_test(self.state): + if 0.55 <= x / self.width <= 0.95 and 6 / 7 <= y / self.height <= 6 / 7 + 1 / 8: + self.state = self.ttt.initial + self.turn = 0 + self.draw_board() + return + + if player == 'human': + x, y = int(3 * x / self.width) + 1, int(3 * y / (self.height * 6 / 7)) + 1 + if (x, y) not in self.ttt.actions(self.state): + # Invalid move + return + move = (x, y) + elif player == 'alpha_beta': + move = alpha_beta_player(self.ttt, self.state) + else: + move = random_player(self.ttt, self.state) + self.state = self.ttt.result(self.state, move) + self.turn ^= 1 + self.draw_board() + + def draw_board(self): + self.clear() + self.stroke(0, 0, 0) + offset = 1 / 20 + self.line_n(0 + offset, (1 / 3) * 6 / 7, 1 - offset, (1 / 3) * 6 / 7) + self.line_n(0 + offset, (2 / 3) * 6 / 7, 1 - offset, (2 / 3) * 6 / 7) + self.line_n(1 / 3, (0 + offset) * 6 / 7, 1 / 3, (1 - offset) * 6 / 7) + self.line_n(2 / 3, (0 + offset) * 6 / 7, 2 / 3, (1 - offset) * 6 / 7) + + board = self.state.board + for mark in board: + if board[mark] == 'X': + self.draw_x(mark) + elif board[mark] == 'O': + self.draw_o(mark) + if self.ttt.terminal_test(self.state): + # End game message + utility = self.ttt.utility(self.state, self.ttt.to_move(self.ttt.initial)) + if utility == 0: + self.text_n('Game Draw!', offset, 6 / 7 + offset) + else: + self.text_n('Player {} wins!'.format("XO"[utility < 0]), offset, 6 / 7 + offset) + # Find the 3 and draw a line + self.stroke([255, 0][self.turn], [0, 255][self.turn], 0) + for i in range(3): + if all([(i + 1, j + 1) in self.state.board for j in range(3)]) and \ + len({self.state.board[(i + 1, j + 1)] for j in range(3)}) == 1: + self.line_n(i / 3 + 1 / 6, offset * 6 / 7, i / 3 + 1 / 6, (1 - offset) * 6 / 7) + if all([(j + 1, i + 1) in self.state.board for j in range(3)]) and \ + len({self.state.board[(j + 1, i + 1)] for j in range(3)}) == 1: + self.line_n(offset, (i / 3 + 1 / 6) * 6 / 7, 1 - offset, (i / 3 + 1 / 6) * 6 / 7) + if all([(i + 1, i + 1) in self.state.board for i in range(3)]) and \ + len({self.state.board[(i + 1, i + 1)] for i in range(3)}) == 1: + self.line_n(offset, offset * 6 / 7, 1 - offset, (1 - offset) * 6 / 7) + if all([(i + 1, 3 - i) in self.state.board for i in range(3)]) and \ + len({self.state.board[(i + 1, 3 - i)] for i in range(3)}) == 1: + self.line_n(offset, (1 - offset) * 6 / 7, 1 - offset, offset * 6 / 7) + # restart button + self.fill(0, 0, 255) + self.rect_n(0.5 + offset, 6 / 7, 0.4, 1 / 8) + self.fill(0, 0, 0) + self.text_n('Restart', 0.5 + 2 * offset, 13 / 14) + else: # Print which player's turn it is + self.text_n("Player {}'s move({})".format("XO"[self.turn], self.players[self.turn]), + offset, 6 / 7 + offset) + + self.update() + + def draw_x(self, position): + self.stroke(0, 255, 0) + x, y = [i - 1 for i in position] + offset = 1 / 15 + self.line_n(x / 3 + offset, (y / 3 + offset) * 6 / 7, x / 3 + 1 / 3 - offset, (y / 3 + 1 / 3 - offset) * 6 / 7) + self.line_n(x / 3 + 1 / 3 - offset, (y / 3 + offset) * 6 / 7, x / 3 + offset, (y / 3 + 1 / 3 - offset) * 6 / 7) + + def draw_o(self, position): + self.stroke(255, 0, 0) + x, y = [i - 1 for i in position] + self.arc_n(x / 3 + 1 / 6, (y / 3 + 1 / 6) * 6 / 7, 1 / 9, 0, 360) + + +class Canvas_min_max(Canvas): + """MinMax for Fig52Extended on HTML canvas""" + + def __init__(self, varname, util_list, width=800, height=600, cid=None): + super().__init__(varname, width, height, cid) + self.utils = {node: util for node, util in zip(range(13, 40), util_list)} + self.game = Fig52Extended() + self.game.utils = self.utils + self.nodes = list(range(40)) + self.l = 1 / 40 + self.node_pos = {} + for i in range(4): + base = len(self.node_pos) + row_size = 3 ** i + for node in [base + j for j in range(row_size)]: + self.node_pos[node] = ((node - base) / row_size + 1 / (2 * row_size) - self.l / 2, + self.l / 2 + (self.l + (1 - 5 * self.l) / 3) * i) + self.font("12px Arial") + self.node_stack = [] + self.explored = {node for node in self.utils} + self.thick_lines = set() + self.change_list = [] + self.draw_graph() + self.stack_manager = self.stack_manager_gen() + + def min_max(self, node): + game = self.game + player = game.to_move(node) + + def max_value(node): + if game.terminal_test(node): + return game.utility(node, player) + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + max_a = max(game.actions(node), key=lambda x: min_value(game.result(node, x))) + max_node = game.result(node, max_a) + self.utils[node] = self.utils[max_node] + x1, y1 = self.node_pos[node] + x2, y2 = self.node_pos[max_node] + self.change_list.append(('l', (node, max_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return self.utils[node] + + def min_value(node): + if game.terminal_test(node): + return game.utility(node, player) + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + min_a = min(game.actions(node), key=lambda x: max_value(game.result(node, x))) + min_node = game.result(node, min_a) + self.utils[node] = self.utils[min_node] + x1, y1 = self.node_pos[node] + x2, y2 = self.node_pos[min_node] + self.change_list.append(('l', (node, min_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return self.utils[node] + + return max_value(node) + + def stack_manager_gen(self): + self.min_max(0) + for change in self.change_list: + if change[0] == 'a': + self.node_stack.append(change[1]) + elif change[0] == 'e': + self.explored.add(change[1]) + elif change[0] == 'h': + yield + elif change[0] == 'l': + self.thick_lines.add(change[1]) + elif change[0] == 'p': + self.node_stack.pop() + + def mouse_click(self, x, y): + try: + self.stack_manager.send(None) + except StopIteration: + pass + self.draw_graph() + + def draw_graph(self): + self.clear() + # draw nodes + self.stroke(0, 0, 0) + self.strokeWidth(1) + # highlight for nodes in stack + for node in self.node_stack: + x, y = self.node_pos[node] + self.fill(200, 200, 0) + self.rect_n(x - self.l / 5, y - self.l / 5, self.l * 7 / 5, self.l * 7 / 5) + for node in self.nodes: + x, y = self.node_pos[node] + if node in self.explored: + self.fill(255, 255, 255) + else: + self.fill(200, 200, 200) + self.rect_n(x, y, self.l, self.l) + self.line_n(x, y, x + self.l, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.l, y + self.l, x + self.l, y) + self.line_n(x + self.l, y + self.l, x, y + self.l) + self.fill(0, 0, 0) + if node in self.explored: + self.text_n(self.utils[node], x + self.l / 10, y + self.l * 9 / 10) + # draw edges + for i in range(13): + x1, y1 = self.node_pos[i][0] + self.l / 2, self.node_pos[i][1] + self.l + for j in range(3): + x2, y2 = self.node_pos[i * 3 + j + 1][0] + self.l / 2, self.node_pos[i * 3 + j + 1][1] + if i in [1, 2, 3]: + self.stroke(200, 0, 0) + else: + self.stroke(0, 200, 0) + if (i, j) in self.thick_lines: + self.strokeWidth(3) + else: + self.strokeWidth(1) + self.line_n(x1, y1, x2, y2) + self.update() + + +class Canvas_alpha_beta(Canvas): + """Alpha-beta pruning for Fig52Extended on HTML canvas""" + + def __init__(self, varname, util_list, width=800, height=600, cid=None): + super().__init__(varname, width, height, cid) + self.utils = {node: util for node, util in zip(range(13, 40), util_list)} + self.game = Fig52Extended() + self.game.utils = self.utils + self.nodes = list(range(40)) + self.l = 1 / 40 + self.node_pos = {} + for i in range(4): + base = len(self.node_pos) + row_size = 3 ** i + for node in [base + j for j in range(row_size)]: + self.node_pos[node] = ((node - base) / row_size + 1 / (2 * row_size) - self.l / 2, + 3 * self.l / 2 + (self.l + (1 - 6 * self.l) / 3) * i) + self.font("12px Arial") + self.node_stack = [] + self.explored = {node for node in self.utils} + self.pruned = set() + self.ab = {} + self.thick_lines = set() + self.change_list = [] + self.draw_graph() + self.stack_manager = self.stack_manager_gen() + + def alpha_beta_search(self, node): + game = self.game + player = game.to_move(node) + + # Functions used by alpha_beta + def max_value(node, alpha, beta): + if game.terminal_test(node): + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + self.change_list.append(('p',)) + return game.utility(node, player) + v = -np.inf + self.change_list.append(('a', node)) + self.change_list.append(('ab', node, v, beta)) + self.change_list.append(('h',)) + for a in game.actions(node): + min_val = min_value(game.result(node, a), alpha, beta) + if v < min_val: + v = min_val + max_node = game.result(node, a) + self.change_list.append(('ab', node, v, beta)) + if v >= beta: + self.change_list.append(('h',)) + self.pruned.add(node) + break + alpha = max(alpha, v) + self.utils[node] = v + if node not in self.pruned: + self.change_list.append(('l', (node, max_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return v + + def min_value(node, alpha, beta): + if game.terminal_test(node): + self.change_list.append(('a', node)) + self.change_list.append(('h',)) + self.change_list.append(('p',)) + return game.utility(node, player) + v = np.inf + self.change_list.append(('a', node)) + self.change_list.append(('ab', node, alpha, v)) + self.change_list.append(('h',)) + for a in game.actions(node): + max_val = max_value(game.result(node, a), alpha, beta) + if v > max_val: + v = max_val + min_node = game.result(node, a) + self.change_list.append(('ab', node, alpha, v)) + if v <= alpha: + self.change_list.append(('h',)) + self.pruned.add(node) + break + beta = min(beta, v) + self.utils[node] = v + if node not in self.pruned: + self.change_list.append(('l', (node, min_node - 3 * node - 1))) + self.change_list.append(('e', node)) + self.change_list.append(('p',)) + self.change_list.append(('h',)) + return v + + return max_value(node, -np.inf, np.inf) + + def stack_manager_gen(self): + self.alpha_beta_search(0) + for change in self.change_list: + if change[0] == 'a': + self.node_stack.append(change[1]) + elif change[0] == 'ab': + self.ab[change[1]] = change[2:] + elif change[0] == 'e': + self.explored.add(change[1]) + elif change[0] == 'h': + yield + elif change[0] == 'l': + self.thick_lines.add(change[1]) + elif change[0] == 'p': + self.node_stack.pop() + + def mouse_click(self, x, y): + try: + self.stack_manager.send(None) + except StopIteration: + pass + self.draw_graph() + + def draw_graph(self): + self.clear() + # draw nodes + self.stroke(0, 0, 0) + self.strokeWidth(1) + # highlight for nodes in stack + for node in self.node_stack: + x, y = self.node_pos[node] + # alpha > beta + if node not in self.explored and self.ab[node][0] > self.ab[node][1]: + self.fill(200, 100, 100) + else: + self.fill(200, 200, 0) + self.rect_n(x - self.l / 5, y - self.l / 5, self.l * 7 / 5, self.l * 7 / 5) + for node in self.nodes: + x, y = self.node_pos[node] + if node in self.explored: + if node in self.pruned: + self.fill(50, 50, 50) + else: + self.fill(255, 255, 255) + else: + self.fill(200, 200, 200) + self.rect_n(x, y, self.l, self.l) + self.line_n(x, y, x + self.l, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.l, y + self.l, x + self.l, y) + self.line_n(x + self.l, y + self.l, x, y + self.l) + self.fill(0, 0, 0) + if node in self.explored and node not in self.pruned: + self.text_n(self.utils[node], x + self.l / 10, y + self.l * 9 / 10) + # draw edges + for i in range(13): + x1, y1 = self.node_pos[i][0] + self.l / 2, self.node_pos[i][1] + self.l + for j in range(3): + x2, y2 = self.node_pos[i * 3 + j + 1][0] + self.l / 2, self.node_pos[i * 3 + j + 1][1] + if i in [1, 2, 3]: + self.stroke(200, 0, 0) + else: + self.stroke(0, 200, 0) + if (i, j) in self.thick_lines: + self.strokeWidth(3) + else: + self.strokeWidth(1) + self.line_n(x1, y1, x2, y2) + # display alpha and beta + for node in self.node_stack: + if node not in self.explored: + x, y = self.node_pos[node] + alpha, beta = self.ab[node] + self.text_n(alpha, x - self.l / 2, y - self.l / 10) + self.text_n(beta, x + self.l, y - self.l / 10) + self.update() + + +class Canvas_fol_bc_ask(Canvas): + """fol_bc_ask() on HTML canvas""" + + def __init__(self, varname, kb, query, width=800, height=600, cid=None): + super().__init__(varname, width, height, cid) + self.kb = kb + self.query = query + self.l = 1 / 20 + self.b = 3 * self.l + bc_out = list(self.fol_bc_ask()) + if len(bc_out) == 0: + self.valid = False + else: + self.valid = True + graph = bc_out[0][0][0] + s = bc_out[0][1] + while True: + new_graph = subst(s, graph) + if graph == new_graph: + break + graph = new_graph + self.make_table(graph) + self.context = None + self.draw_table() + + def fol_bc_ask(self): + KB = self.kb + query = self.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_mm(rhs, goal, theta)): + yield ([(goal, theta1[0])], theta1[1]) + + def fol_bc_and(KB, goals, theta): + if theta is None: + pass + elif not goals: + yield ([], theta) + else: + first, rest = goals[0], goals[1:] + for theta1 in fol_bc_or(KB, subst(theta, first), theta): + for theta2 in fol_bc_and(KB, rest, theta1[1]): + yield (theta1[0] + theta2[0], theta2[1]) + + return fol_bc_or(KB, query, {}) + + def make_table(self, graph): + table = [] + pos = {} + links = set() + edges = set() + + def dfs(node, depth): + if len(table) <= depth: + table.append([]) + pos = len(table[depth]) + table[depth].append(node[0]) + for child in node[1]: + child_id = dfs(child, depth + 1) + links.add(((depth, pos), child_id)) + return (depth, pos) + + dfs(graph, 0) + y_off = 0.85 / len(table) + for i, row in enumerate(table): + x_off = 0.95 / len(row) + for j, node in enumerate(row): + pos[(i, j)] = (0.025 + j * x_off + (x_off - self.b) / 2, 0.025 + i * y_off + (y_off - self.l) / 2) + for p, c in links: + x1, y1 = pos[p] + x2, y2 = pos[c] + edges.add((x1 + self.b / 2, y1 + self.l, x2 + self.b / 2, y2)) + + self.table = table + self.pos = pos + self.edges = edges + + def mouse_click(self, x, y): + x, y = x / self.width, y / self.height + for node in self.pos: + xs, ys = self.pos[node] + xe, ye = xs + self.b, ys + self.l + if xs <= x <= xe and ys <= y <= ye: + self.context = node + break + self.draw_table() + + def draw_table(self): + self.clear() + self.strokeWidth(3) + self.stroke(0, 0, 0) + self.font("12px Arial") + if self.valid: + # draw nodes + for i, j in self.pos: + x, y = self.pos[(i, j)] + self.fill(200, 200, 200) + self.rect_n(x, y, self.b, self.l) + self.line_n(x, y, x + self.b, y) + self.line_n(x, y, x, y + self.l) + self.line_n(x + self.b, y, x + self.b, y + self.l) + self.line_n(x, y + self.l, x + self.b, y + self.l) + self.fill(0, 0, 0) + self.text_n(self.table[i][j], x + 0.01, y + self.l - 0.01) + # draw edges + for x1, y1, x2, y2 in self.edges: + self.line_n(x1, y1, x2, y2) + else: + self.fill(255, 0, 0) + self.rect_n(0, 0, 1, 1) + # text area + self.fill(255, 255, 255) + self.rect_n(0, 0.9, 1, 0.1) + self.strokeWidth(5) + self.stroke(0, 0, 0) + self.line_n(0, 0.9, 1, 0.9) + self.font("22px Arial") + self.fill(0, 0, 0) + self.text_n(self.table[self.context[0]][self.context[1]] if self.context else "Click for text", 0.025, 0.975) + self.update() + + +############################################################################################################ + +##################### Functions to assist plotting in search.ipynb #################### + +############################################################################################################ + + +def show_map(graph_data, node_colors=None): + G = nx.Graph(graph_data['graph_dict']) + node_colors = node_colors or graph_data['node_colors'] + node_positions = graph_data['node_positions'] + node_label_pos = graph_data['node_label_positions'] + edge_weights = graph_data['edge_weights'] + + # set the size of the plot + plt.figure(figsize=(18, 13)) + # draw the graph (both nodes and edges) with locations from romania_locations + nx.draw(G, pos={k: node_positions[k] for k in G.nodes()}, + node_color=[node_colors[node] for node in G.nodes()], linewidths=0.3, edgecolors='k') + + # draw labels for nodes + node_label_handles = nx.draw_networkx_labels(G, pos=node_label_pos, font_size=14) + + # add a white bounding box behind the node labels + [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()] + + # add edge lables to the graph + nx.draw_networkx_edge_labels(G, pos=node_positions, edge_labels=edge_weights, font_size=14) + + # add a legend + white_circle = lines.Line2D([], [], color="white", marker='o', markersize=15, markerfacecolor="white") + orange_circle = lines.Line2D([], [], color="orange", marker='o', markersize=15, markerfacecolor="orange") + red_circle = lines.Line2D([], [], color="red", marker='o', markersize=15, markerfacecolor="red") + gray_circle = lines.Line2D([], [], color="gray", marker='o', markersize=15, markerfacecolor="gray") + green_circle = lines.Line2D([], [], color="green", marker='o', markersize=15, markerfacecolor="green") + plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle), + ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'), + numpoints=1, prop={'size': 16}, loc=(.8, .75)) + + # show the plot. No need to use in notebooks. nx.draw will show the graph itself. + plt.show() + + +# helper functions for visualisations + +def final_path_colors(initial_node_colors, problem, solution): + """Return a node_colors dict of the final path provided the problem and solution.""" + + # get initial node colors + final_colors = dict(initial_node_colors) + # color all the nodes in solution and starting node to green + final_colors[problem.initial] = "green" + for node in solution: + final_colors[node] = "green" + return final_colors + + +def display_visual(graph_data, user_input, algorithm=None, problem=None): + initial_node_colors = graph_data['node_colors'] + if user_input is False: + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors=all_node_colors[iteration]) + except: + pass + + def visualize_callback(visualize): + if visualize is True: + button.value = False + + global all_node_colors + + iterations, all_node_colors, node = algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + # time.sleep(.5) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration=slider) + display(slider_visual) + + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, visualize=button) + display(button_visual) + + if user_input is True: + node_colors = dict(initial_node_colors) + if isinstance(algorithm, dict): + assert set(algorithm.keys()).issubset({"Breadth First Tree Search", + "Depth First Tree Search", + "Breadth First Search", + "Depth First Graph Search", + "Best First Graph Search", + "Uniform Cost Search", + "Depth Limited Search", + "Iterative Deepening Search", + "Greedy Best First Search", + "A-star Search", + "Recursive Best First Search"}) + + algo_dropdown = widgets.Dropdown(description="Search algorithm: ", + options=sorted(list(algorithm.keys())), + value="Breadth First Tree Search") + display(algo_dropdown) + elif algorithm is None: + print("No algorithm to run.") + return 0 + + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors=all_node_colors[iteration]) + except: + pass + + def visualize_callback(visualize): + if visualize is True: + button.value = False + + problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map) + global all_node_colors + + user_algorithm = algorithm[algo_dropdown.value] + + iterations, all_node_colors, node = user_algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + # time.sleep(.5) + + start_dropdown = widgets.Dropdown(description="Start city: ", + options=sorted(list(node_colors.keys())), value="Arad") + display(start_dropdown) + + end_dropdown = widgets.Dropdown(description="Goal city: ", + options=sorted(list(node_colors.keys())), value="Fagaras") + display(end_dropdown) + + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, visualize=button) + display(button_visual) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration=slider) + display(slider_visual) + + +# Function to plot NQueensCSP in csp.py and NQueensProblem in search.py +def plot_NQueens(solution): + n = len(solution) + board = np.array([2 * int((i + j) % 2) for j in range(n) for i in range(n)]).reshape((n, n)) + im = Image.open('images/queen_s.png') + height = im.size[1] + im = np.array(im).astype(np.float) / 255 + fig = plt.figure(figsize=(7, 7)) + ax = fig.add_subplot(111) + ax.set_title('{} Queens'.format(n)) + plt.imshow(board, cmap='binary', interpolation='nearest') + # NQueensCSP gives a solution as a dictionary + if isinstance(solution, dict): + for (k, v) in solution.items(): + newax = fig.add_axes([0.064 + (k * 0.112), 0.062 + ((7 - v) * 0.112), 0.1, 0.1], zorder=1) + newax.imshow(im) + newax.axis('off') + # NQueensProblem gives a solution as a list + elif isinstance(solution, list): + for (k, v) in enumerate(solution): + newax = fig.add_axes([0.064 + (k * 0.112), 0.062 + ((7 - v) * 0.112), 0.1, 0.1], zorder=1) + newax.imshow(im) + newax.axis('off') + fig.tight_layout() + plt.show() + + +# Function to plot a heatmap, given a grid +def heatmap(grid, cmap='binary', interpolation='nearest'): + fig = plt.figure(figsize=(7, 7)) + ax = fig.add_subplot(111) + ax.set_title('Heatmap') + plt.imshow(grid, cmap=cmap, interpolation=interpolation) + fig.tight_layout() + plt.show() + + +# Generates a gaussian kernel +def gaussian_kernel(l=5, sig=1.0): + ax = np.arange(-l // 2 + 1., l // 2 + 1.) + xx, yy = np.meshgrid(ax, ax) + kernel = np.exp(-(xx ** 2 + yy ** 2) / (2. * sig ** 2)) + return kernel + + +# Plots utility function for a POMDP +def plot_pomdp_utility(utility): + save = utility['0'][0] + delete = utility['1'][0] + ask_save = utility['2'][0] + ask_delete = utility['2'][-1] + left = (save[0] - ask_save[0]) / (save[0] - ask_save[0] + ask_save[1] - save[1]) + right = (delete[0] - ask_delete[0]) / (delete[0] - ask_delete[0] + ask_delete[1] - delete[1]) + + colors = ['g', 'b', 'k'] + for action in utility: + for value in utility[action]: + plt.plot(value, color=colors[int(action)]) + plt.vlines([left, right], -20, 10, linestyles='dashed', colors='c') + plt.ylim(-20, 13) + plt.xlim(0, 1) + plt.text(left / 2 - 0.05, 10, 'Save') + plt.text((right + left) / 2 - 0.02, 10, 'Ask') + plt.text((right + 1) / 2 - 0.07, 10, 'Delete') + plt.show() diff --git a/notebooks/chapter19/Learners.ipynb b/notebooks/chapter19/Learners.ipynb new file mode 100644 index 000000000..c6f3d1e4f --- /dev/null +++ b/notebooks/chapter19/Learners.ipynb @@ -0,0 +1,508 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Learners\n", + "\n", + "In this section, we will introduce several pre-defined learners to learning the datasets by updating their weights to minimize the loss function. when using a learner to deal with machine learning problems, there are several standard steps:\n", + "\n", + "- **Learner initialization**: Before training the network, it usually should be initialized first. There are several choices when initializing the weights: random initialization, initializing weights are zeros or use Gaussian distribution to init the weights.\n", + "\n", + "- **Optimizer specification**: Which means specifying the updating rules of learnable parameters of the network. Usually, we can choose Adam optimizer as default.\n", + "\n", + "- **Applying back-propagation**: In neural networks, we commonly use back-propagation to pass and calculate gradient information of each layer. Back-propagation needs to be integrated with the chosen optimizer in order to update the weights of NN properly in each epoch.\n", + "\n", + "- **Iterations**: Iterating over the forward and back-propagation process of given epochs. Sometimes the iterating process will have to be stopped by triggering early access in case of overfitting.\n", + "\n", + "We will introduce several learners with different structures. We will import all necessary packages before that:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using TensorFlow backend.\n" + ] + } + ], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from deep_learning4e import *\n", + "from notebook4e import *\n", + "from learning4e import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Perceptron Learner\n", + "\n", + "### Overview\n", + "\n", + "The Perceptron is a linear classifier. It works the same way as a neural network with no hidden layers (just input and output). First, it trains its weights given a dataset and then it can classify a new item by running it through the network.\n", + "\n", + "Its input layer consists of the item features, while the output layer consists of nodes (also called neurons). Each node in the output layer has *n* synapses (for every item feature), each with its own weight. Then, the nodes find the dot product of the item features and the synapse weights. These values then pass through an activation function (usually a sigmoid). Finally, we pick the largest of the values and we return its index.\n", + "\n", + "Note that in classification problems each node represents a class. The final classification is the class/node with the max output value.\n", + "\n", + "Below you can see a single node/neuron in the outer layer. With *f* we denote the item features, with *w* the synapse weights, then inside the node we have the dot product and the activation function, *g*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![perceptron](images/perceptron.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "Perceptron learner is actually a neural network learner with only one hidden layer which is pre-defined in the algorithm of `perceptron_learner`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "raw_net = [InputLayer(input_size), DenseLayer(input_size, output_size)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Where `input_size` and `output_size` are calculated from dataset examples. In the perceptron learner, the gradient descent optimizer is used to update the weights of the network. we return a function `predict` which we will use in the future to classify a new item. The function computes the (algebraic) dot product of the item with the calculated weights for each node in the outer layer. Then it picks the greatest value and classifies the item in the corresponding class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Let's try the perceptron learner with the `iris` dataset examples, first let's regulate the dataset classes:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "iris = DataSet(name=\"iris\")\n", + "classes = [\"setosa\", \"versicolor\", \"virginica\"]\n", + "iris.classes_to_numbers(classes)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch:50, total_loss:14.089098023560856\n", + "epoch:100, total_loss:12.439240091345326\n", + "epoch:150, total_loss:11.848151059704785\n", + "epoch:200, total_loss:11.283665595671044\n", + "epoch:250, total_loss:11.153290841913241\n", + "epoch:300, total_loss:11.00747536734494\n", + "epoch:350, total_loss:10.871093050365419\n", + "epoch:400, total_loss:10.838400319844233\n", + "epoch:450, total_loss:10.687417928867456\n", + "epoch:500, total_loss:10.650371951865573\n" + ] + } + ], + "source": [ + "pl = perceptron_learner(iris, epochs=500, learning_rate=0.01, verbose=50)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see from the printed lines that the final total loss is converged to around 10.50. If we check the error ratio of perceptron learner on the dataset after training, we will see it is much higher than randomly guess:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.046666666666666634\n" + ] + } + ], + "source": [ + "print(err_ratio(pl, iris))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we test the trained learner with some test cases:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n" + ] + } + ], + "source": [ + "tests = [([5.0, 3.1, 0.9, 0.1], 0),\n", + " ([5.1, 3.5, 1.0, 0.0], 0),\n", + " ([4.9, 3.3, 1.1, 0.1], 0),\n", + " ([6.0, 3.0, 4.0, 1.1], 1),\n", + " ([6.1, 2.2, 3.5, 1.0], 1),\n", + " ([5.9, 2.5, 3.3, 1.1], 1),\n", + " ([7.5, 4.1, 6.2, 2.3], 2),\n", + " ([7.3, 4.0, 6.1, 2.4], 2),\n", + " ([7.0, 3.3, 6.1, 2.5], 2)]\n", + "print(grade_learner(pl, tests))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems the learner is correct on all the test examples.\n", + "\n", + "Now let's try perceptron learner on a more complicated dataset: the MNIST dataset, to see what the result will be. First, we import the dataset to make the examples a `Dataset` object:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "length of training dataset: 60000\n", + "length of test dataset: 10000\n" + ] + } + ], + "source": [ + "train_img, train_lbl, test_img, test_lbl = load_MNIST(path=\"../../aima-data/MNIST/Digits\")\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "train_examples = [np.append(train_img[i], train_lbl[i]) for i in range(len(train_img))]\n", + "test_examples = [np.append(test_img[i], test_lbl[i]) for i in range(len(test_img))]\n", + "print(\"length of training dataset:\", len(train_examples))\n", + "print(\"length of test dataset:\", len(test_examples))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's train the perceptron learner on the first 1000 examples of the dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch:1, total_loss:423.8627535296463\n", + "epoch:2, total_loss:341.31697581698995\n", + "epoch:3, total_loss:328.98647291325443\n", + "epoch:4, total_loss:327.8999700915627\n", + "epoch:5, total_loss:310.081065570072\n", + "epoch:6, total_loss:268.5474616202945\n", + "epoch:7, total_loss:259.0999998773958\n", + "epoch:8, total_loss:259.09999987481393\n", + "epoch:9, total_loss:259.09999987211944\n", + "epoch:10, total_loss:259.0999998693056\n" + ] + } + ], + "source": [ + "mnist = DataSet(examples=train_examples[:1000])\n", + "pl = perceptron_learner(mnist, epochs=10, verbose=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.893\n" + ] + } + ], + "source": [ + "print(err_ratio(pl, mnist))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It looks like we have a near 90% error ratio on training data after the network is trained on it. Then we can investigate the model's performance on the test dataset which it never has seen before:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.92\n" + ] + } + ], + "source": [ + "test_mnist = DataSet(examples=test_examples[:100])\n", + "print(err_ratio(pl, test_mnist))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems a single layer perceptron learner cannot simulate the structure of the MNIST dataset. To improve accuracy, we may not only increase training epochs but also consider changing to a more complicated network structure." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Neural Network Learner\n", + "\n", + "Although there are many different types of neural networks, the dense neural network we implemented can be treated as a stacked perceptron learner. Adding more layers to the perceptron network could add to the non-linearity to the network thus model will be more flexible when fitting complex data-target relations. Whereas it also adds to the risk of overfitting as the side effect of flexibility.\n", + "\n", + "By default we use dense networks with two hidden layers, which has the architecture as the following:\n", + "\n", + "\n", + "\n", + "In our code, we implemented it as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# initialize the network\n", + "raw_net = [InputLayer(input_size)]\n", + "# add hidden layers\n", + "hidden_input_size = input_size\n", + "for h_size in hidden_layer_sizes:\n", + " raw_net.append(DenseLayer(hidden_input_size, h_size))\n", + " hidden_input_size = h_size\n", + "raw_net.append(DenseLayer(hidden_input_size, output_size))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Where hidden_layer_sizes are the sizes of each hidden layer in a list which can be specified by user. Neural network learner uses gradient descent as default optimizer but user can specify any optimizer when calling `neural_net_learner`. The other special attribute that can be changed in `neural_net_learner` is `batch_size` which controls the number of examples used in each round of update. `neural_net_learner` also returns a `predict` function which calculates prediction by multiplying weight to inputs and applying activation functions.\n", + "\n", + "### Example\n", + "\n", + "Let's also try `neural_net_learner` on the `iris` dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch:10, total_loss:15.931817841643683\n", + "epoch:20, total_loss:8.248422285412149\n", + "epoch:30, total_loss:6.102968668275\n", + "epoch:40, total_loss:5.463915043272969\n", + "epoch:50, total_loss:5.298986288420822\n", + "epoch:60, total_loss:4.032928400456889\n", + "epoch:70, total_loss:3.2628899927346855\n", + "epoch:80, total_loss:6.01336701367312\n", + "epoch:90, total_loss:5.412020420311795\n", + "epoch:100, total_loss:3.1044027319850773\n" + ] + } + ], + "source": [ + "nn = neural_net_learner(iris, epochs=100, learning_rate=0.15, optimizer=gradient_descent, verbose=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly we check the model's accuracy on both training and test dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "error ration on training set: 0.033333333333333326\n" + ] + } + ], + "source": [ + "print(\"error ration on training set:\",err_ratio(nn, iris))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "accuracy on test set: 1\n" + ] + } + ], + "source": [ + "tests = [([5.0, 3.1, 0.9, 0.1], 0),\n", + " ([5.1, 3.5, 1.0, 0.0], 0),\n", + " ([4.9, 3.3, 1.1, 0.1], 0),\n", + " ([6.0, 3.0, 4.0, 1.1], 1),\n", + " ([6.1, 2.2, 3.5, 1.0], 1),\n", + " ([5.9, 2.5, 3.3, 1.1], 1),\n", + " ([7.5, 4.1, 6.2, 2.3], 2),\n", + " ([7.3, 4.0, 6.1, 2.4], 2),\n", + " ([7.0, 3.3, 6.1, 2.5], 2)]\n", + "print(\"accuracy on test set:\",grade_learner(nn, tests))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the error ratio on the training set is smaller than the perceptron learner. As the error ratio is relatively small, let's try the model on the MNIST dataset to see whether there will be a larger difference. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch:10, total_loss:89.0002153455983\n", + "epoch:20, total_loss:87.29675663038348\n", + "epoch:30, total_loss:86.29591779319225\n", + "epoch:40, total_loss:83.78091780128402\n", + "epoch:50, total_loss:82.17091581738829\n", + "epoch:60, total_loss:83.8434277386084\n", + "epoch:70, total_loss:83.55209905561495\n", + "epoch:80, total_loss:83.106898191118\n", + "epoch:90, total_loss:83.37041170165992\n", + "epoch:100, total_loss:82.57013813500876\n" + ] + } + ], + "source": [ + "nn = neural_net_learner(mnist, epochs=100, verbose=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.784\n" + ] + } + ], + "source": [ + "print(err_ratio(nn, mnist))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the model converging, the model's error ratio on the training set is still high. We will introduce the convolutional network in the following chapters to see how it helps improve accuracy on learning this dataset." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter19/Loss Functions and Layers.ipynb b/notebooks/chapter19/Loss Functions and Layers.ipynb new file mode 100644 index 000000000..25676e899 --- /dev/null +++ b/notebooks/chapter19/Loss Functions and Layers.ipynb @@ -0,0 +1,398 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Loss Function\n", + "\n", + "Loss functions evaluate how well specific algorithm models the given data. Commonly loss functions are used to compare the target data and model's prediction. If predictions deviate too much from actual targets, loss function would output a large value. Usually, loss functions can help other optimization functions to improve the accuracy of the model.\n", + "\n", + "However, there’s no one-size-fits-all loss function to algorithms in machine learning. For each algorithm and machine learning projects, specifying certain loss functions could assist the user in getting better model performance. Here we will demonstrate two loss functions: `mse_loss` and `cross_entropy_loss`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Min Square Error\n", + "\n", + "Min square error(MSE) is the most commonly used loss function in machine learning. The intuition of MSE is straight forward: the distance between two points represents the difference between them. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$MSE = -\\sum_i{(y_i-t_i)^2/n}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Where $y_i$ is the prediction of the ith example and $t_i$ is the target of the ith example. And n is the total number of examples.\n", + "\n", + "Below is a plot of an MSE function where the true target value is 100, and the predicted values range between -10,000 to 10,000. The MSE loss (Y-axis) reaches its minimum value at prediction (X-axis) = 100." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cross-Entropy\n", + "\n", + "For most deep learning applications, we can get away with just one loss function: cross-entropy loss function. We can think of most deep learning algorithms as learning probability distributions and what we are learning is a distribution of predictions $P(y|x)$ given a series of inputs. \n", + "\n", + "To associate input examples x with output examples y, the parameters that maximize the likelihood of the training set should be:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\\theta^* = argmax_\\theta \\prod_{i=0}^n p(y^{(i)}/x^{(i)})$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Maxmizing the above formula equals to minimizing the negative log form of it:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\\theta^* = argmin_\\theta -\\sum_{i=0}^n logp(y^{(i)}/x^{(i)})$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It can be proven that the above formula equals to minimizing MSE loss.\n", + "\n", + "The majority of deep learning algorithms use cross-entropy in some way. Classifiers that use deep learning calculate the cross-entropy between categorical distributions over the output class. For a given class, its contribution to the loss is dependent on its probability in the following trend:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examples\n", + "\n", + "First let's import necessary packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using TensorFlow backend.\n" + ] + } + ], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from deep_learning4e import *\n", + "from notebook4e import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Neural Network Layers\n", + "\n", + "Neural networks may be conveniently described using data structures of computational graphs. A computational graph is a directed graph describing how many variables should be computed, with each variable by computed by applying a specific operation to a set of other variables. \n", + "\n", + "In our code, we provide class `NNUnit` as the basic structure of a neural network. The structure of `NNUnit` is simple, it only stores the following information:\n", + "\n", + "- **val**: the value of the current node.\n", + "- **parent**: parents of the current node.\n", + "- **weights**: weights between parent nodes and current node. It should be in the same size as parents.\n", + "\n", + "There is another class `Layer` inheriting from `NNUnit`. A `Layer` object holds a list of nodes that represents all the nodes in a layer. It also has a method `forward` to pass a value through the current layer. Here we will demonstrate several pre-defined types of layers in a Neural Network." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Output Layers\n", + "\n", + "Neural networks need specialized output layers for each type of data we might ask them to produce. For many problems, we need to model discrete variables that have k distinct values instead of just binary variables. For example, models of natural language may predict a single word from among of vocabulary of tens of thousands or even more choices. To represent these distributions, we use a softmax layer:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$P(y=i|x)=softmax(h(x)^TW+b)_i$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "where $W$ is matrix of learned weights of output layer $b$ is a vector of learned biases, and the softmax function is:\n", + "\n", + "$$softmax(z_i)=exp(z_i)/\\sum_i exp(z_i)$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is simple to create a output layer and feed an example into it:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.03205860328008499, 0.08714431874203257, 0.23688281808991013, 0.6439142598879722]\n" + ] + } + ], + "source": [ + "layer = OutputLayer(size=4)\n", + "example = [1,2,3,4]\n", + "print(layer.forward(example))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output can be treated like normalized probability when the input of output layer is calculated by probability." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Input Layers\n", + "\n", + "Input layers can be treated like a mapping layer that maps each element of the input vector to each input layer node. The input layer acts as a storage of input vector information which can be used when doing forward propagation.\n", + "\n", + "In our realization of input layers, the size of the input vector and input layer should match." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 2, 3]\n" + ] + } + ], + "source": [ + "layer = InputLayer(size=3)\n", + "example = [1,2,3]\n", + "print(layer.forward(example))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hidden Layers\n", + "\n", + "While processing an input vector x of the neural network, it performs several intermediate computations before producing the output y. We can think of these intermediate computations as the state of memory during the execution of a multi-step program. We call the intermediate computations hidden because the data does not specify the values of these variables.\n", + "\n", + "Most neural network hidden layers are based on a linear transformation followed by the application of an elementwise nonlinear function called the activation function g:\n", + "\n", + "$$h=g(W+b)$$\n", + "\n", + "where W is a learned matrix of weights and b is a learned set of bias parameters.\n", + "\n", + "Here we pre-defined several activation functions in `utils.py`: `sigmoid`, `relu`, `elu`, `tanh` and `leaky_relu`. They are all inherited from the `Activation` class. You can get the value of the function or its derivative at a certain point of x:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sigmoid at 0: 0.5\n", + "Deriavation of sigmoid at 0: 0\n" + ] + } + ], + "source": [ + "s = sigmoid()\n", + "print(\"Sigmoid at 0:\", s.f(0))\n", + "print(\"Deriavation of sigmoid at 0:\", s.derivative(0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To create a hidden layer object, there are several attributes need to be specified:\n", + "\n", + "- **in_size**: the input vector size of each hidden layer node.\n", + "- **out_size**: the size of the output vector of the hidden layer. Thus each node will hide the weight of the size of (in_size). The weights will be initialized randomly.\n", + "- **activation**: the activation function used for this layer.\n", + "\n", + "Now let's demonstrate how a dense hidden layer works briefly:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.21990266877137224, 0.2038864498984756, 0.5543443697256466]\n" + ] + } + ], + "source": [ + "layer = DenseLayer(in_size=4, out_size=3, activation=sigmoid())\n", + "example = [1,2,3,4]\n", + "print(layer.forward(example))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This layer mapped input of size 4 to output of size 3. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Convolutional Layers\n", + "\n", + "The convolutional layer is similar to the hidden layer except they use a different forward strategy. The convolutional layer takes an input of multiple channels and does convolution on each channel with a pre-defined kernel function. Thus the output of the convolutional layer will still be with the same number of channels. If we image each input as an image, then channels represent its color model such as RGB. The output will still have the same color model as the input.\n", + "\n", + "Now let's try the one-dimensional convolution layer:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[array([3.9894228, 3.9894228, 3.9894228]), array([3.9894228, 3.9894228, 3.9894228]), array([3.9894228, 3.9894228, 3.9894228])]\n" + ] + } + ], + "source": [ + "layer = ConvLayer1D(size=3, kernel_size=3)\n", + "example = [[1]*3 for _ in range(3)]\n", + "print(layer.forward(example))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which can be deemed as a one-dimensional image with three channels." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pooling Layers\n", + "\n", + "Pooling layers can be treated as a special kind of convolutional layer that uses a special kind of kernel to extract a certain value in the kernel region. Here we use max-pooling to report the maximum value in each group." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[3, 4], [4, 4], [4, 4]]\n" + ] + } + ], + "source": [ + "layer = MaxPoolingLayer1D(size=3, kernel_size=3)\n", + "example = [[1,2,3,4], [2,3,4,1],[3,4,1,2]]\n", + "print(layer.forward(example))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that each time kernel picks up the maximum value in its region." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter19/Optimizer and Backpropagation.ipynb b/notebooks/chapter19/Optimizer and Backpropagation.ipynb new file mode 100644 index 000000000..5194adc7a --- /dev/null +++ b/notebooks/chapter19/Optimizer and Backpropagation.ipynb @@ -0,0 +1,311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Optimization Algorithms\n", + "\n", + "Training a neural network consists of modifying the network’s parameters to minimize the cost function on the training set. In principle, any kind of optimization algorithm could be used. In practice, modern neural networks are almost always trained with some variant of stochastic gradient descent(SGD). Here we will provide two optimization algorithms: SGD and Adam optimizer.\n", + "\n", + "## Stochastic Gradient Descent\n", + "\n", + "The goal of an optimization algorithm is to find the value of the parameter to make loss function very low. For some types of models, an optimization algorithm might find the global minimum value of loss function, but for neural network, the most efficient way to converge loss function to a local minimum is to minimize loss function according to each example.\n", + "\n", + "Gradient descent uses the following update rule to minimize loss function:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\\theta^{(t+1)} = \\theta^{(t)}-\\alpha\\nabla_\\theta L(\\theta^{(t)})$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "where t is the time step of the algorithm and $\\alpha$ is the learning rate. But this rule could be very costly when $L(\\theta)$ is defined as a sum across the entire training set. Using SGD can accelerate the learning process as we can use only a batch of examples to update the parameters. \n", + "\n", + "We implemented the gradient descent algorithm, which can be viewed with the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using TensorFlow backend.\n" + ] + } + ], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from deep_learning4e import *\n", + "from notebook4e import *" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def gradient_descent(dataset, net, loss, epochs=1000, l_rate=0.01,  batch_size=1):\n",
    +       "    """\n",
    +       "    gradient descent algorithm to update the learnable parameters of a network.\n",
    +       "    :return: the updated network.\n",
    +       "    """\n",
    +       "    # init data\n",
    +       "    examples = dataset.examples\n",
    +       "\n",
    +       "    for e in range(epochs):\n",
    +       "        total_loss = 0\n",
    +       "        random.shuffle(examples)\n",
    +       "        weights = [[node.weights for node in layer.nodes] for layer in net]\n",
    +       "\n",
    +       "        for batch in get_batch(examples, batch_size):\n",
    +       "\n",
    +       "            inputs, targets = init_examples(batch, dataset.inputs, dataset.target, len(net[-1].nodes))\n",
    +       "            # compute gradients of weights\n",
    +       "            gs, batch_loss = BackPropagation(inputs, targets, weights, net, loss)\n",
    +       "            # update weights with gradient descent\n",
    +       "            weights = vector_add(weights, scalar_vector_product(-l_rate, gs))\n",
    +       "            total_loss += batch_loss\n",
    +       "            # update the weights of network each batch\n",
    +       "            for i in range(len(net)):\n",
    +       "                if weights[i]:\n",
    +       "                    for j in range(len(weights[i])):\n",
    +       "                        net[i].nodes[j].weights = weights[i][j]\n",
    +       "\n",
    +       "        if (e+1) % 10 == 0:\n",
    +       "            print("epoch:{}, total_loss:{}".format(e+1,total_loss))\n",
    +       "    return net\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(gradient_descent)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There several key elements need to specify when using a `gradient_descent` optimizer:\n", + "\n", + "- **dataset**: A dataset object we used in the previous chapter, such as `iris` and `orings`.\n", + "- **net**: A neural network object which we will cover in the next chapter.\n", + "- **loss**: The loss function used in representing accuracy.\n", + "- **epochs**: How many rounds the training set is used.\n", + "- **l_rate**: learning rate.\n", + "- **batch_size**: The number of examples is used in each update. When very small batch size is used, gradient descent and be treated as SGD." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adam Optimizer\n", + "\n", + "To mitigate some of the problems caused by the fact that the gradient ignores the second derivatives, some optimization algorithms incorporate the idea of momentum which keeps a running average of the gradients of past mini-batches. Thus Adam optimizer maintains a table saving the previous gradient result.\n", + "\n", + "To view the pseudocode and the implementation, you can use the following codes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pseudocode(adam_optimizer)\n", + "psource(adam_optimizer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are several attributes to specify when using Adam optimizer that is different from gradient descent: rho and delta. These parameters determine the percentage of the last iteration is memorized. For more details of how this algorithm work, please refer to the article [here](https://arxiv.org/abs/1412.6980).\n", + "\n", + "In the Stanford course on deep learning for computer vision, the Adam algorithm is suggested as the default optimization method for deep learning applications: \n", + ">In practice Adam is currently recommended as the default algorithm to use, and often works slightly better than RMSProp. However, it is often also worth trying SGD+Nesterov Momentum as an alternative." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Backpropagation\n", + "\n", + "The above algorithms are optimization algorithms: they update parameters like $\\theta$ to get smaller loss values. And back-propagation is the method to calculate the gradient for each layer. For complicated models like deep neural networks, the gradients can not be calculated directly as there are enormous array-valued variables.\n", + "\n", + "Fortunately, back-propagation can calculate the gradients briefly which we can interpret as calculating gradients from the last layer to the first which is the inverse process to the forwarding procedure. The derivation of the loss function is passed to previous layers to make them changing toward the direction of minimizing the loss function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Applying optimizers and back-propagation algorithm together, we can update the weights of a neural network to minimize the loss function with alternatively doing forward and back-propagation process. Here is a figure form [here](https://medium.com/datathings/neural-networks-and-backpropagation-explained-in-a-simple-way-f540a3611f5e) describing how a neural network updates its weights:\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In our implementation, all the steps are integrated into the optimizer objects. The forward-backward process of passing information through the whole neural network is put into the method `BackPropagation`. You can view the code with:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(BackPropagation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The demonstration of optimizers and back-propagation algorithm will be made together with neural network learners." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter19/RNN.ipynb b/notebooks/chapter19/RNN.ipynb new file mode 100644 index 000000000..b6971b36a --- /dev/null +++ b/notebooks/chapter19/RNN.ipynb @@ -0,0 +1,487 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# RNN\n", + "\n", + "## Overview\n", + "\n", + "When human is thinking, they are thinking based on the understanding of previous time steps but not from scratch. Traditional neural networks can’t do this, and it seems like a major shortcoming. For example, imagine you want to do sentimental analysis of some texts. It will be unclear if the traditional network cannot recognize the short phrase and sentences.\n", + "\n", + "Recurrent neural networks address this issue. They are networks with loops in them, allowing information to persist.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A recurrent neural network can be thought of as multiple copies of the same network, each passing a message to a successor. Consider what happens if we unroll the above loop:\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As demonstrated in the book, recurrent neural networks may be connected in many different ways: sequences in the input, the output, or in the most general case both.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implementation\n", + "\n", + "In our case, we implemented rnn with modules offered by the package of `keras`. To use `keras` and our module, you must have both `tensorflow` and `keras` installed as a prerequisite. `keras` offered very well defined high-level neural networks API which allows for easy and fast prototyping. `keras` supports many different types of networks such as convolutional and recurrent neural networks as well as user-defined networks. About how to get started with `keras`, please read the [tutorial](https://keras.io/).\n", + "\n", + "To view our implementation of a simple rnn, please use the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using TensorFlow backend.\n" + ] + } + ], + "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n", + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from deep_learning4e import *\n", + "from notebook4e import *" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def SimpleRNNLearner(train_data, val_data, epochs=2):\n",
    +       "    """\n",
    +       "    RNN example for text sentimental analysis.\n",
    +       "    :param train_data: a tuple of (training data, targets)\n",
    +       "            Training data: ndarray taking training examples, while each example is coded by embedding\n",
    +       "            Targets: ndarray taking targets of each example. Each target is mapped to an integer.\n",
    +       "    :param val_data: a tuple of (validation data, targets)\n",
    +       "    :param epochs: number of epochs\n",
    +       "    :return: a keras model\n",
    +       "    """\n",
    +       "\n",
    +       "    total_inputs = 5000\n",
    +       "    input_length = 500\n",
    +       "\n",
    +       "    # init data\n",
    +       "    X_train, y_train = train_data\n",
    +       "    X_val, y_val = val_data\n",
    +       "\n",
    +       "    # init a the sequential network (embedding layer, rnn layer, dense layer)\n",
    +       "    model = Sequential()\n",
    +       "    model.add(Embedding(total_inputs, 32, input_length=input_length))\n",
    +       "    model.add(SimpleRNN(units=128))\n",
    +       "    model.add(Dense(1, activation='sigmoid'))\n",
    +       "    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n",
    +       "\n",
    +       "    # train the model\n",
    +       "    model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=epochs, batch_size=128, verbose=2)\n",
    +       "\n",
    +       "    return model\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(SimpleRNNLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`train_data` and `val_data` are needed when creating a simple rnn learner. Both attributes take lists of examples and the targets in a tuple. Please note that we build the network by adding layers to a `Sequential()` model which means data are passed through the network one by one. `SimpleRNN` layer is the key layer of rnn which acts the recursive role. Both `Embedding` and `Dense` layers before and after the rnn layer are used to map inputs and outputs to data in rnn form. And the optimizer used in this case is the Adam optimizer." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example\n", + "\n", + "Here is an example of how we train the rnn network made with `keras`. In this case, we used the IMDB dataset which can be viewed [here](https://keras.io/datasets/#imdb-movie-reviews-sentiment-classification) in detail. In short, the dataset is consist of movie reviews in text and their labels of sentiment (positive/negative). After loading the dataset we use `keras_dataset_loader` to split it into training, validation and test datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from keras.datasets import imdb\n", + "data = imdb.load_data(num_words=5000)\n", + "train, val, test = keras_dataset_loader(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we build and train the rnn model for 10 epochs:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: Logging before flag parsing goes to stderr.\n", + "W1018 22:51:23.614058 140557804885824 deprecation.py:323] From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/nn_impl.py:180: add_dispatch_support..wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "Use tf.where in 2.0, which has the same broadcast rule as np.where\n", + "W1018 22:51:24.267649 140557804885824 deprecation_wrapper.py:119] From /usr/local/lib/python3.6/dist-packages/keras/backend/tensorflow_backend.py:422: The name tf.global_variables is deprecated. Please use tf.compat.v1.global_variables instead.\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train on 24990 samples, validate on 25000 samples\n", + "Epoch 1/10\n", + " - 59s - loss: 0.6540 - accuracy: 0.5959 - val_loss: 0.6234 - val_accuracy: 0.6488\n", + "Epoch 2/10\n", + " - 61s - loss: 0.5977 - accuracy: 0.6766 - val_loss: 0.6202 - val_accuracy: 0.6326\n", + "Epoch 3/10\n", + " - 61s - loss: 0.5269 - accuracy: 0.7356 - val_loss: 0.4803 - val_accuracy: 0.7789\n", + "Epoch 4/10\n", + " - 61s - loss: 0.4159 - accuracy: 0.8130 - val_loss: 0.5640 - val_accuracy: 0.7046\n", + "Epoch 5/10\n", + " - 61s - loss: 0.3931 - accuracy: 0.8294 - val_loss: 0.4707 - val_accuracy: 0.8090\n", + "Epoch 6/10\n", + " - 61s - loss: 0.3357 - accuracy: 0.8637 - val_loss: 0.4177 - val_accuracy: 0.8122\n", + "Epoch 7/10\n", + " - 61s - loss: 0.3552 - accuracy: 0.8594 - val_loss: 0.4652 - val_accuracy: 0.7889\n", + "Epoch 8/10\n", + " - 61s - loss: 0.3286 - accuracy: 0.8686 - val_loss: 0.4708 - val_accuracy: 0.7785\n", + "Epoch 9/10\n", + " - 61s - loss: 0.3428 - accuracy: 0.8635 - val_loss: 0.4332 - val_accuracy: 0.8137\n", + "Epoch 10/10\n", + " - 61s - loss: 0.3650 - accuracy: 0.8471 - val_loss: 0.4673 - val_accuracy: 0.7914\n" + ] + } + ], + "source": [ + "model = SimpleRNNLearner(train, val, epochs=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The accuracy of the training dataset and validation dataset are both over 80% which is very promising. Now let's try on some random examples in the test set:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Autoencoder\n", + "\n", + "Autoencoders are an unsupervised learning technique in which we leverage neural networks for the task of representation learning. It works by compressing the input into a latent-space representation, to do transformations on the data. \n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Autoencoders are learned automatically from data examples. It means that it is easy to train specialized instances of the algorithm that will perform well on a specific type of input and that it does not require any new engineering, only the appropriate training data.\n", + "\n", + "Autoencoders have different architectures for different kinds of data. Here we only provide a simple example of a vanilla encoder, which means they're only one hidden layer in the network:\n", + "\n", + "\n", + "\n", + "You can view the source code by:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

    \n", + "\n", + "
    def AutoencoderLearner(inputs, encoding_size, epochs=200):\n",
    +       "    """\n",
    +       "    Simple example of linear auto encoder learning producing the input itself.\n",
    +       "    :param inputs: a batch of input data in np.ndarray type\n",
    +       "    :param encoding_size: int, the size of encoding layer\n",
    +       "    :param epochs: number of epochs\n",
    +       "    :return: a keras model\n",
    +       "    """\n",
    +       "\n",
    +       "    # init data\n",
    +       "    input_size = len(inputs[0])\n",
    +       "\n",
    +       "    # init model\n",
    +       "    model = Sequential()\n",
    +       "    model.add(Dense(encoding_size, input_dim=input_size, activation='relu', kernel_initializer='random_uniform',\n",
    +       "                    bias_initializer='ones'))\n",
    +       "    model.add(Dense(input_size, activation='relu', kernel_initializer='random_uniform', bias_initializer='ones'))\n",
    +       "\n",
    +       "    # update model with sgd\n",
    +       "    sgd = optimizers.SGD(lr=0.01)\n",
    +       "    model.compile(loss='mean_squared_error', optimizer=sgd, metrics=['accuracy'])\n",
    +       "\n",
    +       "    # train the model\n",
    +       "    model.fit(inputs, inputs, epochs=epochs, batch_size=10, verbose=2)\n",
    +       "\n",
    +       "    return model\n",
    +       "
    \n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(AutoencoderLearner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It shows we added two dense layers to the network structures." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter19/images/autoencoder.png b/notebooks/chapter19/images/autoencoder.png new file mode 100644 index 000000000..cd216e9f7 Binary files /dev/null and b/notebooks/chapter19/images/autoencoder.png differ diff --git a/notebooks/chapter19/images/backprop.png b/notebooks/chapter19/images/backprop.png new file mode 100644 index 000000000..8d53530e6 Binary files /dev/null and b/notebooks/chapter19/images/backprop.png differ diff --git a/notebooks/chapter19/images/corss_entropy_plot.png b/notebooks/chapter19/images/corss_entropy_plot.png new file mode 100644 index 000000000..8212405e7 Binary files /dev/null and b/notebooks/chapter19/images/corss_entropy_plot.png differ diff --git a/notebooks/chapter19/images/mse_plot.png b/notebooks/chapter19/images/mse_plot.png new file mode 100644 index 000000000..fd58f9db9 Binary files /dev/null and b/notebooks/chapter19/images/mse_plot.png differ diff --git a/notebooks/chapter19/images/nn.png b/notebooks/chapter19/images/nn.png new file mode 100644 index 000000000..673b9338b Binary files /dev/null and b/notebooks/chapter19/images/nn.png differ diff --git a/notebooks/chapter19/images/nn_steps.png b/notebooks/chapter19/images/nn_steps.png new file mode 100644 index 000000000..4a596133b Binary files /dev/null and b/notebooks/chapter19/images/nn_steps.png differ diff --git a/notebooks/chapter19/images/perceptron.png b/notebooks/chapter19/images/perceptron.png new file mode 100644 index 000000000..68d2a258a Binary files /dev/null and b/notebooks/chapter19/images/perceptron.png differ diff --git a/notebooks/chapter19/images/rnn_connections.png b/notebooks/chapter19/images/rnn_connections.png new file mode 100644 index 000000000..c72d459b8 Binary files /dev/null and b/notebooks/chapter19/images/rnn_connections.png differ diff --git a/notebooks/chapter19/images/rnn_unit.png b/notebooks/chapter19/images/rnn_unit.png new file mode 100644 index 000000000..e4ebabf2b Binary files /dev/null and b/notebooks/chapter19/images/rnn_unit.png differ diff --git a/notebooks/chapter19/images/rnn_units.png b/notebooks/chapter19/images/rnn_units.png new file mode 100644 index 000000000..5724f5d46 Binary files /dev/null and b/notebooks/chapter19/images/rnn_units.png differ diff --git a/notebooks/chapter19/images/vanilla.png b/notebooks/chapter19/images/vanilla.png new file mode 100644 index 000000000..db7a45f9a Binary files /dev/null and b/notebooks/chapter19/images/vanilla.png differ diff --git a/notebooks/chapter21/Active Reinforcement Learning.ipynb b/notebooks/chapter21/Active Reinforcement Learning.ipynb new file mode 100644 index 000000000..1ce3c79e0 --- /dev/null +++ b/notebooks/chapter21/Active Reinforcement Learning.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ACTIVE REINFORCEMENT LEARNING\n", + "\n", + "This notebook mainly focuses on active reinforce learning algorithms. For a general introduction to reinforcement learning and passive algorithms, please refer to the notebook of **[Passive Reinforcement Learning](./Passive%20Reinforcement%20Learning.ipynb)**.\n", + "\n", + "Unlike Passive Reinforcement Learning in Active Reinforcement Learning, we are not bound by a policy pi and we need to select our actions. In other words, the agent needs to learn an optimal policy. The fundamental tradeoff the agent needs to face is that of exploration vs. exploitation. \n", + "\n", + "## QLearning Agent\n", + "\n", + "The QLearningAgent class in the rl module implements the Agent Program described in **Fig 21.8** of the AIMA Book. In Q-Learning the agent learns an action-value function Q which gives the utility of taking a given action in a particular state. Q-Learning does not require a transition model and hence is a model-free method. Let us look into the source before we see some usage examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%psource QLearningAgent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Agent Program can be obtained by creating the instance of the class by passing the appropriate parameters. Because of the __ call __ method the object that is created behaves like a callable and returns an appropriate action as most Agent Programs do. To instantiate the object we need a `mdp` object similar to the `PassiveTDAgent`.\n", + "\n", + " Let us use the same `GridMDP` object we used above. **Figure 17.1 (sequential_decision_environment)** is similar to **Figure 21.1** but has some discounting parameter as **gamma = 0.9**. The enviroment also implements an exploration function **f** which returns fixed **Rplus** until agent has visited state, action **Ne** number of times. The method **actions_in_state** returns actions possible in given state. It is useful when applying max and argmax operations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us create our object now. We also use the **same alpha** as given in the footnote of the book on **page 769**: $\\alpha(n)=60/(59+n)$ We use **Rplus = 2** and **Ne = 5** as defined in the book. The pseudocode can be referred from **Fig 21.7** in the book." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from rl4e import *\n", + "from mdp import sequential_decision_environment, value_iteration" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "q_agent = QLearningAgent(sequential_decision_environment, Ne=5, Rplus=2, \n", + " alpha=lambda n: 60./(59+n))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now to try out the q_agent we make use of the **run_single_trial** function in rl.py (which was also used above). Let us use **200** iterations." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(200):\n", + " run_single_trial(q_agent,sequential_decision_environment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let us see the Q Values. The keys are state-action pairs. Where different actions correspond according to:\n", + "\n", + "north = (0, 1) \n", + "south = (0,-1) \n", + "west = (-1, 0) \n", + "east = (1, 0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "q_agent.Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Utility U of each state is related to Q by the following equation.\n", + "\n", + "$$U (s) = max_a Q(s, a)$$\n", + "\n", + "Let us convert the Q Values above into U estimates.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "U = defaultdict(lambda: -1000.) # Very Large Negative Value for Comparison see below.\n", + "for state_action, value in q_agent.Q.items():\n", + " state, action = state_action\n", + " if U[state] < value:\n", + " U[state] = value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can output the estimated utility values at each state:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "defaultdict(()>,\n", + " {(0, 0): -0.0036556430391564178,\n", + " (1, 0): -0.04862675963288682,\n", + " (2, 0): 0.03384490363100474,\n", + " (3, 0): -0.16618771401113092,\n", + " (3, 1): -0.6015323978614368,\n", + " (0, 1): 0.09161077177913537,\n", + " (0, 2): 0.1834607974581678,\n", + " (1, 2): 0.26393277962204903,\n", + " (2, 2): 0.32369726495311274,\n", + " (3, 2): 0.38898341569576245,\n", + " (2, 1): -0.044858154562400485})" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "U" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us finally compare these estimates to value_iteration results." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{(0, 1): 0.3984432178350045, (1, 2): 0.649585681261095, (3, 2): 1.0, (0, 0): 0.2962883154554812, (3, 0): 0.12987274656746342, (3, 1): -1.0, (2, 1): 0.48644001739269643, (2, 0): 0.3447542300124158, (2, 2): 0.7953620878466678, (1, 0): 0.25386699846479516, (0, 2): 0.5093943765842497}\n" + ] + } + ], + "source": [ + "print(value_iteration(sequential_decision_environment))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter21/Passive Reinforcement Learning.ipynb b/notebooks/chapter21/Passive Reinforcement Learning.ipynb new file mode 100644 index 000000000..cbb5ae9e3 --- /dev/null +++ b/notebooks/chapter21/Passive Reinforcement Learning.ipynb @@ -0,0 +1,424 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to Reinforcement Learning\n", + "\n", + "This Jupyter notebook and the others in the same folder act as supporting materials for **Chapter 21 Reinforcement Learning** of the book* Artificial Intelligence: A Modern Approach*. The notebooks make use of the implementations in `rl.py` module. We also make use of the implementation of MDPs in the `mdp.py` module to test our agents. It might be helpful if you have already gone through the Jupyter notebook dealing with the Markov decision process. Let us import everything from the `rl` module. It might be helpful to view the source of some of our implementations." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from rl4e import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we start playing with the actual implementations let us review a couple of things about RL.\n", + "\n", + "1. Reinforcement Learning is concerned with how software agents ought to take actions in an environment so as to maximize some notion of cumulative reward. \n", + "\n", + "2. Reinforcement learning differs from standard supervised learning in that correct input/output pairs are never presented, nor sub-optimal actions explicitly corrected. Further, there is a focus on on-line performance, which involves finding a balance between exploration (of uncharted territory) and exploitation (of current knowledge).\n", + "\n", + "-- Source: [Wikipedia](https://en.wikipedia.org/wiki/Reinforcement_learning)\n", + "\n", + "In summary, we have a sequence of state action transitions with rewards associated with some states. Our goal is to find the optimal policy $\\pi$ which tells us what action to take in each state." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Passive Reinforcement Learning\n", + "\n", + "In passive Reinforcement Learning the agent follows a fixed policy $\\pi$. Passive learning attempts to evaluate the given policy $pi$ - without any knowledge of the Reward function $R(s)$ and the Transition model $P(s'\\ |\\ s, a)$.\n", + "\n", + "This is usually done by some method of **utility estimation**. The agent attempts to directly learn the utility of each state that would result from following the policy. Note that at each step, it has to *perceive* the reward and the state - it has no global knowledge of these. Thus, if a certain the entire set of actions offers a very low probability of attaining some state $s_+$ - the agent may never perceive the reward $R(s_+)$.\n", + "\n", + "Consider a situation where an agent is given the policy to follow. Thus, at any point, it knows only its current state and current reward, and the action it must take next. This action may lead it to more than one state, with different probabilities.\n", + "\n", + "For a series of actions given by $\\pi$, the estimated utility $U$:\n", + "$$U^{\\pi}(s) = E(\\sum_{t=0}^\\inf \\gamma^t R^t(s'))$$\n", + "Or the expected value of summed discounted rewards until termination.\n", + "\n", + "Based on this concept, we discuss three methods of estimating utility: direct utility estimation, adaptive dynamic programming, and temporal-difference learning.\n", + "\n", + "### Implementation\n", + "\n", + "Passive agents are implemented in `rl4e.py` as various `Agent-Class`es.\n", + "\n", + "To demonstrate these agents, we make use of the `GridMDP` object from the `MDP` module. `sequential_decision_environment` is similar to that used for the `MDP` notebook but has discounting with $\\gamma = 0.9$.\n", + "\n", + "The `Agent-Program` can be obtained by creating an instance of the relevant `Agent-Class`. The `__call__` method allows the `Agent-Class` to be called as a function. The class needs to be instantiated with a policy ($\\pi$) and an `MDP` whose utility of states will be estimated.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from mdp import sequential_decision_environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `sequential_decision_environment` is a GridMDP object as shown below. The rewards are **+1** and **-1** in the terminal states, and **-0.04** in the rest. Now we define actions and a policy similar to **Fig 21.1** in the book." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Action Directions\n", + "north = (0, 1)\n", + "south = (0,-1)\n", + "west = (-1, 0)\n", + "east = (1, 0)\n", + "\n", + "policy = {\n", + " (0, 2): east, (1, 2): east, (2, 2): east, (3, 2): None,\n", + " (0, 1): north, (2, 1): north, (3, 1): None,\n", + " (0, 0): north, (1, 0): west, (2, 0): west, (3, 0): west, \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This enviroment will be extensively used in the following demonstrations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Direct Utility Estimation (DUE)\n", + " \n", + " The first, most naive method of estimating utility comes from the simplest interpretation of the above definition. We construct an agent that follows the policy until it reaches the terminal state. At each step, it logs its current state, reward. Once it reaches the terminal state, it can estimate the utility for each state for *that* iteration, by simply summing the discounted rewards from that state to the terminal one.\n", + "\n", + " It can now run this 'simulation' $n$ times and calculate the average utility of each state. If a state occurs more than once in a simulation, both its utility values are counted separately.\n", + " \n", + " Note that this method may be prohibitively slow for very large state-spaces. Besides, **it pays no attention to the transition probability $P(s'\\ |\\ s, a)$.** It misses out on information that it is capable of collecting (say, by recording the number of times an action from one state led to another state). The next method addresses this issue.\n", + " \n", + "### Examples\n", + "\n", + "The `PassiveDEUAgent` class in the `rl` module implements the Agent Program described in **Fig 21.2** of the AIMA Book. `PassiveDEUAgent` sums over rewards to find the estimated utility for each state. It thus requires the running of several iterations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%psource PassiveDUEAgent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's try the `PassiveDEUAgent` on the newly defined `sequential_decision_environment`:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "DUEagent = PassiveDUEAgent(policy, sequential_decision_environment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can try passing information through the markove model for 200 times in order to get the converged utility value:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(200):\n", + " run_single_trial(DUEagent, sequential_decision_environment)\n", + " DUEagent.estimate_U()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's print our estimated utility for each position:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, 1):0.7956939931414414\n", + "(1, 2):0.9162054322837863\n", + "(3, 2):1.0\n", + "(0, 0):0.734717308253083\n", + "(2, 2):0.9595117143816332\n", + "(0, 2):0.8481387156375687\n", + "(1, 0):0.4355860415209706\n", + "(2, 1):-0.550079982553143\n", + "(3, 1):-1.0\n" + ] + } + ], + "source": [ + "print('\\n'.join([str(k)+':'+str(v) for k, v in DUEagent.U.items()]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adaptive Dynamic Programming (ADP)\n", + " \n", + " This method makes use of knowledge of the past state $s$, the action $a$, and the new perceived state $s'$ to estimate the transition probability $P(s'\\ |\\ s,a)$. It does this by the simple counting of new states resulting from previous states and actions.
    \n", + " The program runs through the policy a number of times, keeping track of:\n", + " - each occurrence of state $s$ and the policy-recommended action $a$ in $N_{sa}$\n", + " - each occurrence of $s'$ resulting from $a$ on $s$ in $N_{s'|sa}$.\n", + " \n", + " It can thus estimate $P(s'\\ |\\ s,a)$ as $N_{s'|sa}/N_{sa}$, which in the limit of infinite trials, will converge to the true value.
    \n", + " Using the transition probabilities thus estimated, it can apply `POLICY-EVALUATION` to estimate the utilities $U(s)$ using properties of convergence of the Bellman functions.\n", + " \n", + "### Examples\n", + "\n", + "The `PassiveADPAgent` class in the `rl` module implements the Agent Program described in **Fig 21.2** of the AIMA Book. `PassiveADPAgent` uses state transition and occurrence counts to estimate $P$, and then $U$. Go through the source below to understand the agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We instantiate a `PassiveADPAgent` below with the `GridMDP` shown and train it for 200 steps. The `rl` module has a simple implementation to simulate a single step of the iteration. The function is called `run_single_trial`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: Transition table is empty.\n" + ] + } + ], + "source": [ + "ADPagent = PassiveADPAgent(policy, sequential_decision_environment)\n", + "for i in range(200):\n", + " run_single_trial(ADPagent, sequential_decision_environment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The utilities are calculated as :" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, 0):0.3014408531958584\n", + "(0, 1):0.40583863351329275\n", + "(1, 2):0.6581480346627065\n", + "(3, 2):1.0\n", + "(3, 0):0.0\n", + "(3, 1):-1.0\n", + "(2, 1):0.5341859348580892\n", + "(2, 0):0.0\n", + "(2, 2):0.810403779650285\n", + "(1, 0):0.23129676787627254\n", + "(0, 2):0.5214746706094832\n" + ] + } + ], + "source": [ + "print('\\n'.join([str(k)+':'+str(v) for k, v in ADPagent.U.items()]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When comparing to the result of `PassiveDUEAgent`, they both have -1.0 for utility at (3,1) and 1.0 at (3,2). Another point to notice is that the spot with the highest utility for both agents is (2,2) beside the terminal states, which is easy to understand when referring to the map." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Temporal-difference learning (TD)\n", + " \n", + " Instead of explicitly building the transition model $P$, the temporal-difference model makes use of the expected closeness between the utilities of two consecutive states $s$ and $s'$.\n", + " For the transition $s$ to $s'$, the update is written as:\n", + "$$U^{\\pi}(s) \\leftarrow U^{\\pi}(s) + \\alpha \\left( R(s) + \\gamma U^{\\pi}(s') - U^{\\pi}(s) \\right)$$\n", + " This model implicitly incorporates the transition probabilities by being weighed for each state by the number of times it is achieved from the current state. Thus, over a number of iterations, it converges similarly to the Bellman equations.\n", + " The advantage of the TD learning model is its relatively simple computation at each step, rather than having to keep track of various counts.\n", + " For $n_s$ states and $n_a$ actions the ADP model would have $n_s \\times n_a$ numbers $N_{sa}$ and $n_s^2 \\times n_a$ numbers $N_{s'|sa}$ to keep track of. The TD model must only keep track of a utility $U(s)$ for each state.\n", + " \n", + "### Examples\n", + "\n", + "`PassiveTDAgent` uses temporal differences to learn utility estimates. We learn the difference between the states and back up the values to previous states. Let us look into the source before we see some usage examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%psource PassiveTDAgent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In creating the `TDAgent`, we use the **same learning rate** $\\alpha$ as given in the footnote of the book: $\\alpha(n)=60/(59+n)$" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "TDagent = PassiveTDAgent(policy, sequential_decision_environment, alpha = lambda n: 60./(59+n))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we run **200 trials** for the agent to estimate Utilities." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(200):\n", + " run_single_trial(TDagent,sequential_decision_environment)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The calculated utilities are:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, 1):0.36652562797696076\n", + "(1, 2):0.6584162739552614\n", + "(3, 2):1\n", + "(0, 0):0.27775491505339645\n", + "(3, 0):0.0\n", + "(3, 1):-1\n", + "(2, 1):0.6097040420148784\n", + "(2, 0):0.0\n", + "(2, 2):0.7936759402770092\n", + "(1, 0):0.19085842384266813\n", + "(0, 2):0.5258782999305713\n" + ] + } + ], + "source": [ + "print('\\n'.join([str(k)+':'+str(v) for k, v in TDagent.U.items()]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When comparing to previous agents, the result of `PassiveTDAgent` is closer to `PassiveADPAgent`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter21/images/mdp.png b/notebooks/chapter21/images/mdp.png new file mode 100644 index 000000000..e874130ee Binary files /dev/null and b/notebooks/chapter21/images/mdp.png differ diff --git a/notebooks/chapter22/Grammar.ipynb b/notebooks/chapter22/Grammar.ipynb new file mode 100644 index 000000000..3c1a2a005 --- /dev/null +++ b/notebooks/chapter22/Grammar.ipynb @@ -0,0 +1,526 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Grammar\n", + "\n", + "Languages can be represented by a set of grammar rules over a lexicon of words. Different languages can be represented by different types of grammar, but in Natural Language Processing we are mainly interested in context-free grammars.\n", + "\n", + "## Context-Free Grammar\n", + "\n", + "A lot of natural and programming languages can be represented by a **Context-Free Grammar (CFG)**. A CFG is a grammar that has a single non-terminal symbol on the left-hand side. That means a non-terminal can be replaced by the right-hand side of the rule regardless of context. An example of a CFG:\n", + "\n", + "```\n", + "S -> aSb | ε\n", + "```\n", + "\n", + "That means `S` can be replaced by either `aSb` or `ε` (with `ε` we denote the empty string). The lexicon of the language is comprised of the terminals `a` and `b`, while with `S` we denote the non-terminal symbol. In general, non-terminals are capitalized while terminals are not, and we usually name the starting non-terminal `S`. The language generated by the above grammar is the language anbn for n greater or equal than 1." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Probabilistic Context-Free Grammar\n", + "\n", + "While a simple CFG can be very useful, we might want to know the chance of each rule occurring. Above, we do not know if `S` is more likely to be replaced by `aSb` or `ε`. **Probabilistic Context-Free Grammars (PCFG)** are built to fill exactly that need. Each rule has a probability, given in brackets, and the probabilities of a rule sum up to 1:\n", + "\n", + "```\n", + "S -> aSb [0.7] | ε [0.3]\n", + "```\n", + "\n", + "Now we know it is more likely for `S` to be replaced by `aSb` than by `ε`.\n", + "\n", + "An issue with *PCFGs* is how we will assign the various probabilities to the rules. We could use our knowledge as humans to assign the probabilities, but that is laborious and prone to error task. Instead, we can *learn* the probabilities from data. Data is categorized as labeled (with correctly parsed sentences, usually called a **treebank**) or unlabeled (given only lexical and syntactic category names).\n", + "\n", + "With labeled data, we can simply count the occurrences. For the above grammar, if we have 100 `S` rules and 30 of them are of the form `S -> ε`, we assign a probability of 0.3 to the transformation.\n", + "\n", + "With unlabeled data, we have to learn both the grammar rules and the probability of each rule. We can go with many approaches, one of them the **inside-outside** algorithm. It uses a dynamic programming approach, that first finds the probability of a substring being generated by each rule and then estimates the probability of each rule." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chomsky Normal Form\n", + "\n", + "Grammar is in Chomsky Normal Form (or **CNF**, not to be confused with *Conjunctive Normal Form*) if its rules are one of the three:\n", + "\n", + "* `X -> Y Z`\n", + "* `A -> a`\n", + "* `S -> ε`\n", + "\n", + "Where *X*, *Y*, *Z*, *A* are non-terminals, *a* is a terminal, *ε* is the empty string and *S* is the start symbol (the start symbol should not be appearing on the right-hand side of rules). Note that there can be multiple rules for each left-hand side non-terminal, as long they follow the above. For example, a rule for *X* might be: `X -> Y Z | A B | a | b`.\n", + "\n", + "Of course, we can also have a *CNF* with probabilities.\n", + "\n", + "This type of grammar may seem restrictive, but it can be proven that any context-free grammar can be converted to CNF." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lexicon\n", + "\n", + "The lexicon of a language is defined as a list of allowable words. These words are grouped into the usual classes: `verbs`, `nouns`, `adjectives`, `adverbs`, `pronouns`, `names`, `articles`, `prepositions` and `conjunctions`. For the first five classes, it is impossible to list all words since words are continuously being added in the classes. Recently \"google\" was added to the list of verbs, and words like that will continue to pop up and get added to the lists. For that reason, these first five categories are called **open classes**. The rest of the categories have much fewer words and much less development. While words like \"thou\" were commonly used in the past but have declined almost completely in usage, most changes take many decades or centuries to manifest, so we can safely assume the categories will remain static for the foreseeable future. Thus, these categories are called **closed classes**.\n", + "\n", + "An example lexicon for a PCFG (note that other classes can also be used according to the language, like `digits`, or `RelPro` for relative pronoun):\n", + "\n", + "```\n", + "Verb -> is [0.3] | say [0.1] | are [0.1] | ...\n", + "Noun -> robot [0.1] | sheep [0.05] | fence [0.05] | ...\n", + "Adjective -> good [0.1] | new [0.1] | sad [0.05] | ...\n", + "Adverb -> here [0.1] | lightly [0.05] | now [0.05] | ...\n", + "Pronoun -> me [0.1] | you [0.1] | he [0.05] | ...\n", + "RelPro -> that [0.4] | who [0.2] | which [0.2] | ...\n", + "Name -> john [0.05] | mary [0.05] | peter [0.01] | ...\n", + "Article -> the [0.35] | a [0.25] | an [0.025] | ...\n", + "Preposition -> to [0.25] | in [0.2] | at [0.1] | ...\n", + "Conjunction -> and [0.5] | or [0.2] | but [0.2] | ...\n", + "Digit -> 1 [0.3] | 2 [0.2] | 0 [0.2] | ...\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grammer Rules\n", + "\n", + "With grammars we combine words from the lexicon into valid phrases. A grammar is comprised of **grammar rules**. Each rule transforms the left-hand side of the rule into the right-hand side. For example, `A -> B` means that `A` transforms into `B`. Let's build a grammar for the language we started building with the lexicon. We will use a PCFG.\n", + "\n", + "```\n", + "S -> NP VP [0.9] | S Conjunction S [0.1]\n", + "\n", + "NP -> Pronoun [0.3] | Name [0.1] | Noun [0.1] | Article Noun [0.25] |\n", + " Article Adjs Noun [0.05] | Digit [0.05] | NP PP [0.1] |\n", + " NP RelClause [0.05]\n", + "\n", + "VP -> Verb [0.4] | VP NP [0.35] | VP Adjective [0.05] | VP PP [0.1]\n", + " VP Adverb [0.1]\n", + "\n", + "Adjs -> Adjective [0.8] | Adjective Adjs [0.2]\n", + "\n", + "PP -> Preposition NP [1.0]\n", + "\n", + "RelClause -> RelPro VP [1.0]\n", + "```\n", + "\n", + "Some valid phrases the grammar produces: \"`mary is sad`\", \"`you are a robot`\" and \"`she likes mary and a good fence`\".\n", + "\n", + "What if we wanted to check if the phrase \"`mary is sad`\" is actually a valid sentence? We can use a **parse tree** to constructively prove that a string of words is a valid phrase in the given language and even calculate the probability of the generation of the sentence.\n", + "\n", + "![parse_tree](images/parse_tree.png)\n", + "\n", + "The probability of the whole tree can be calculated by multiplying the probabilities of each individual rule transormation: `0.9 * 0.1 * 0.05 * 0.05 * 0.4 * 0.05 * 0.3 = 0.00000135`.\n", + "\n", + "To conserve space, we can also write the tree in linear form:\n", + "\n", + "[S [NP [Name **mary**]] [VP [VP [Verb **is**]] [Adjective **sad**]]]\n", + "\n", + "Unfortunately, the current grammar **overgenerates**, that is, it creates sentences that are not grammatically correct (according to the English language), like \"`the fence are john which say`\". It also **undergenerates**, which means there are valid sentences it does not generate, like \"`he believes mary is sad`\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implementation\n", + "\n", + "In the module, we have implemented both probabilistic and non-probabilistic grammars. Both of these implementations follow the same format. There are functions for the lexicon and the rules which can be combined to create a grammar object.\n", + "\n", + "### Non-Probabilistic\n", + "\n", + "Execute the cell below to view the implementations:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from nlp4e import *\n", + "from notebook4e import psource" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(Lexicon, Rules, Grammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's build a lexicon and a grammar for the above language:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lexicon {'Verb': ['is', 'say', 'are'], 'Noun': ['robot', 'sheep', 'fence'], 'Adjective': ['good', 'new', 'sad'], 'Adverb': ['here', 'lightly', 'now'], 'Pronoun': ['me', 'you', 'he'], 'RelPro': ['that', 'who', 'which'], 'Name': ['john', 'mary', 'peter'], 'Article': ['the', 'a', 'an'], 'Preposition': ['to', 'in', 'at'], 'Conjunction': ['and', 'or', 'but'], 'Digit': ['1', '2', '0']}\n", + "\n", + "Rules: {'S': [['NP', 'VP'], ['S', 'Conjunction', 'S']], 'NP': [['Pronoun'], ['Name'], ['Noun'], ['Article', 'Noun'], ['Article', 'Adjs', 'Noun'], ['Digit'], ['NP', 'PP'], ['NP', 'RelClause']], 'VP': [['Verb'], ['VP', 'NP'], ['VP', 'Adjective'], ['VP', 'PP'], ['VP', 'Adverb']], 'Adjs': [['Adjective'], ['Adjective', 'Adjs']], 'PP': [['Preposition', 'NP']], 'RelClause': [['RelPro', 'VP']]}\n" + ] + } + ], + "source": [ + "lexicon = Lexicon(\n", + " Verb = \"is | say | are\",\n", + " Noun = \"robot | sheep | fence\",\n", + " Adjective = \"good | new | sad\",\n", + " Adverb = \"here | lightly | now\",\n", + " Pronoun = \"me | you | he\",\n", + " RelPro = \"that | who | which\",\n", + " Name = \"john | mary | peter\",\n", + " Article = \"the | a | an\",\n", + " Preposition = \"to | in | at\",\n", + " Conjunction = \"and | or | but\",\n", + " Digit = \"1 | 2 | 0\"\n", + ")\n", + "\n", + "print(\"Lexicon\", lexicon)\n", + "\n", + "rules = Rules(\n", + " S = \"NP VP | S Conjunction S\",\n", + " NP = \"Pronoun | Name | Noun | Article Noun \\\n", + " | Article Adjs Noun | Digit | NP PP | NP RelClause\",\n", + " VP = \"Verb | VP NP | VP Adjective | VP PP | VP Adverb\",\n", + " Adjs = \"Adjective | Adjective Adjs\",\n", + " PP = \"Preposition NP\",\n", + " RelClause = \"RelPro VP\"\n", + ")\n", + "\n", + "print(\"\\nRules:\", rules)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both the functions return a dictionary with keys to the left-hand side of the rules. For the lexicon, the values are the terminals for each left-hand side non-terminal, while for the rules the values are the right-hand sides as lists.\n", + "\n", + "We can now use the variables `lexicon` and `rules` to build a grammar. After we've done so, we can find the transformations of a non-terminal (the `Noun`, `Verb` and the other basic classes do **not** count as proper non-terminals in the implementation). We can also check if a word is in a particular class." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "How can we rewrite 'VP'? [['Verb'], ['VP', 'NP'], ['VP', 'Adjective'], ['VP', 'PP'], ['VP', 'Adverb']]\n", + "Is 'the' an article? True\n", + "Is 'here' a noun? False\n" + ] + } + ], + "source": [ + "grammar = Grammar(\"A Simple Grammar\", rules, lexicon)\n", + "\n", + "print(\"How can we rewrite 'VP'?\", grammar.rewrites_for('VP'))\n", + "print(\"Is 'the' an article?\", grammar.isa('the', 'Article'))\n", + "print(\"Is 'here' a noun?\", grammar.isa('here', 'Noun'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Chomsky Normal Form\n", + "If the grammar is in **Chomsky Normal Form**, we can call the class function `cnf_rules` to get all the rules in the form of `(X, Y, Z)` for each `X -> Y Z` rule. Since the above grammar is not in *CNF* though, we have to create a new one." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "E_Chomsky = Grammar(\"E_Prob_Chomsky\", # A Grammar in Chomsky Normal Form\n", + " Rules(\n", + " S = \"NP VP\",\n", + " NP = \"Article Noun | Adjective Noun\",\n", + " VP = \"Verb NP | Verb Adjective\",\n", + " ),\n", + " Lexicon(\n", + " Article = \"the | a | an\",\n", + " Noun = \"robot | sheep | fence\",\n", + " Adjective = \"good | new | sad\",\n", + " Verb = \"is | say | are\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('S', 'NP', 'VP'), ('NP', 'Article', 'Noun'), ('NP', 'Adjective', 'Noun'), ('VP', 'Verb', 'NP'), ('VP', 'Verb', 'Adjective')]\n" + ] + } + ], + "source": [ + "print(E_Chomsky.cnf_rules())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can generate random phrases using our grammar. Most of them will be complete gibberish, falling under the overgenerated phrases of the grammar. That goes to show that in the grammar the valid phrases are much fewer than the overgenerated ones." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'a fence is 2 at 0 at he at john the fence at a good new sheep in the new sad robot which is who is a good robot which are good sad new now lightly sad at 2 and me are'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grammar.generate_random('S')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Probabilistic\n", + "\n", + "The probabilistic grammars follow the same approach. They take as input a string, are assembled from grammar and a lexicon and can generate random sentences (giving the probability of the sentence). The main difference is that in the lexicon we have tuples (terminal, probability) instead of strings and for the rules, we have a list of tuples (list of non-terminals, probability) instead of the list of lists of non-terminals.\n", + "\n", + "Execute the cells to read the code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(ProbLexicon, ProbRules, ProbGrammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's build a lexicon and rules for the probabilistic grammar:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lexicon {'Verb': [('is', 0.5), ('say', 0.3), ('are', 0.2)], 'Noun': [('robot', 0.4), ('sheep', 0.4), ('fence', 0.2)], 'Adjective': [('good', 0.5), ('new', 0.2), ('sad', 0.3)], 'Adverb': [('here', 0.6), ('lightly', 0.1), ('now', 0.3)], 'Pronoun': [('me', 0.3), ('you', 0.4), ('he', 0.3)], 'RelPro': [('that', 0.5), ('who', 0.3), ('which', 0.2)], 'Name': [('john', 0.4), ('mary', 0.4), ('peter', 0.2)], 'Article': [('the', 0.5), ('a', 0.25), ('an', 0.25)], 'Preposition': [('to', 0.4), ('in', 0.3), ('at', 0.3)], 'Conjunction': [('and', 0.5), ('or', 0.2), ('but', 0.3)], 'Digit': [('0', 0.35), ('1', 0.35), ('2', 0.3)]}\n", + "\n", + "Rules: {'S': [(['NP', 'VP'], 0.6), (['S', 'Conjunction', 'S'], 0.4)], 'NP': [(['Pronoun'], 0.2), (['Name'], 0.05), (['Noun'], 0.2), (['Article', 'Noun'], 0.15), (['Article', 'Adjs', 'Noun'], 0.1), (['Digit'], 0.05), (['NP', 'PP'], 0.15), (['NP', 'RelClause'], 0.1)], 'VP': [(['Verb'], 0.3), (['VP', 'NP'], 0.2), (['VP', 'Adjective'], 0.25), (['VP', 'PP'], 0.15), (['VP', 'Adverb'], 0.1)], 'Adjs': [(['Adjective'], 0.5), (['Adjective', 'Adjs'], 0.5)], 'PP': [(['Preposition', 'NP'], 1.0)], 'RelClause': [(['RelPro', 'VP'], 1.0)]}\n" + ] + } + ], + "source": [ + "lexicon = ProbLexicon(\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Adverb = \"here [0.6] | lightly [0.1] | now [0.3]\",\n", + " Pronoun = \"me [0.3] | you [0.4] | he [0.3]\",\n", + " RelPro = \"that [0.5] | who [0.3] | which [0.2]\",\n", + " Name = \"john [0.4] | mary [0.4] | peter [0.2]\",\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Preposition = \"to [0.4] | in [0.3] | at [0.3]\",\n", + " Conjunction = \"and [0.5] | or [0.2] | but [0.3]\",\n", + " Digit = \"0 [0.35] | 1 [0.35] | 2 [0.3]\"\n", + ")\n", + "\n", + "print(\"Lexicon\", lexicon)\n", + "\n", + "rules = ProbRules(\n", + " S = \"NP VP [0.6] | S Conjunction S [0.4]\",\n", + " NP = \"Pronoun [0.2] | Name [0.05] | Noun [0.2] | Article Noun [0.15] \\\n", + " | Article Adjs Noun [0.1] | Digit [0.05] | NP PP [0.15] | NP RelClause [0.1]\",\n", + " VP = \"Verb [0.3] | VP NP [0.2] | VP Adjective [0.25] | VP PP [0.15] | VP Adverb [0.1]\",\n", + " Adjs = \"Adjective [0.5] | Adjective Adjs [0.5]\",\n", + " PP = \"Preposition NP [1]\",\n", + " RelClause = \"RelPro VP [1]\"\n", + ")\n", + "\n", + "print(\"\\nRules:\", rules)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use the above to assemble our probabilistic grammar and run some simple queries:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "How can we rewrite 'VP'? [(['Verb'], 0.3), (['VP', 'NP'], 0.2), (['VP', 'Adjective'], 0.25), (['VP', 'PP'], 0.15), (['VP', 'Adverb'], 0.1)]\n", + "Is 'the' an article? True\n", + "Is 'here' a noun? False\n" + ] + } + ], + "source": [ + "grammar = ProbGrammar(\"A Simple Probabilistic Grammar\", rules, lexicon)\n", + "\n", + "print(\"How can we rewrite 'VP'?\", grammar.rewrites_for('VP'))\n", + "print(\"Is 'the' an article?\", grammar.isa('the', 'Article'))\n", + "print(\"Is 'here' a noun?\", grammar.isa('here', 'Noun'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we have a grammar in *CNF*, we can get a list of all the rules. Let's create a grammar in the form and print the *CNF* rules:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "E_Prob_Chomsky = ProbGrammar(\"E_Prob_Chomsky\", # A Probabilistic Grammar in CNF\n", + " ProbRules(\n", + " S = \"NP VP [1]\",\n", + " NP = \"Article Noun [0.6] | Adjective Noun [0.4]\",\n", + " VP = \"Verb NP [0.5] | Verb Adjective [0.5]\",\n", + " ),\n", + " ProbLexicon(\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('S', 'NP', 'VP', 1.0), ('NP', 'Article', 'Noun', 0.6), ('NP', 'Adjective', 'Noun', 0.4), ('VP', 'Verb', 'NP', 0.5), ('VP', 'Verb', 'Adjective', 0.5)]\n" + ] + } + ], + "source": [ + "print(E_Prob_Chomsky.cnf_rules())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, we can generate random sentences from this grammar. The function `prob_generation` returns a tuple (sentence, probability)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "a good good new good sheep that say a good good robot the sad robot to 1 to me you to sheep are\n", + "5.511240000000004e-26\n" + ] + } + ], + "source": [ + "sentence, prob = grammar.generate_random('S')\n", + "print(sentence)\n", + "print(prob)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with the non-probabilistic grammars, this one mostly overgenerates. You can also see that the probability is very, very low, which means there are a ton of generate able sentences (in this case infinite, since we have recursion; notice how `VP` can produce another `VP`, for example)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter22/Introduction.ipynb b/notebooks/chapter22/Introduction.ipynb new file mode 100644 index 000000000..0905b91a9 --- /dev/null +++ b/notebooks/chapter22/Introduction.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NATURAL LANGUAGE PROCESSING\n", + "\n", + "The notebooks in this folder cover chapters 23 of the book *Artificial Intelligence: A Modern Approach*, 4th Edition. The implementations of the algorithms can be found in [nlp.py](https://github.com/aimacode/aima-python/blob/master/nlp4e.py).\n", + "\n", + "Run the below cell to import the code from the module and get started!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from nlp4e import *\n", + "from notebook4e import psource" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OVERVIEW\n", + "\n", + "**Natural Language Processing (NLP)** is a field of AI concerned with understanding, analyzing and using natural languages. This field is considered a difficult yet intriguing field of study since it is connected to how humans and their languages work.\n", + "\n", + "Applications of the field include translation, speech recognition, topic segmentation, information extraction and retrieval, and a lot more.\n", + "\n", + "Below we take a look at some algorithms in the field. Before we get right into it though, we will take a look at a very useful form of language, **context-free** languages. Even though they are a bit restrictive, they have been used a lot in research in natural language processing.\n", + "\n", + "Below is a summary of the demonstration files in this chapter." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "- Introduction: Introduction to the field of nlp and the table of contents.\n", + "- Grammars: Introduction to grammar rules and lexicon of words of a language.\n", + " - Context-free Grammar\n", + " - Probabilistic Context-Free Grammar\n", + " - Chomsky Normal Form\n", + " - Lexicon\n", + " - Grammar Rules\n", + " - Implementation of Different Grammars\n", + "- Parsing: The algorithms parsing sentences according to a certain kind of grammar.\n", + " - Chart Parsing\n", + " - CYK Parsing\n", + " - A-star Parsing\n", + " - Beam Search Parsing\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter22/Parsing.ipynb b/notebooks/chapter22/Parsing.ipynb new file mode 100644 index 000000000..50a4264fb --- /dev/null +++ b/notebooks/chapter22/Parsing.ipynb @@ -0,0 +1,522 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parsing\n", + "\n", + "## Overview\n", + "\n", + "Syntactic analysis (or **parsing**) of a sentence is the process of uncovering the phrase structure of the sentence according to the rules of grammar. \n", + "\n", + "There are two main approaches to parsing. *Top-down*, start with the starting symbol and build a parse tree with the given words as its leaves, and *bottom-up*, where we start from the given words and build a tree that has the starting symbol as its root. Both approaches involve \"guessing\" ahead, so it may take longer to parse a sentence (the wrong guess mean a lot of backtracking). Thankfully, a lot of effort is spent in analyzing already analyzed substrings, so we can follow a dynamic programming approach to store and reuse these parses instead of recomputing them. \n", + "\n", + "In dynamic programming, we use a data structure known as a chart, thus the algorithms parsing a chart is called **chart parsing**. We will cover several different chart parsing algorithms." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chart Parsing\n", + "\n", + "### Overview\n", + "\n", + "The chart parsing algorithm is a general form of the following algorithms. Given a non-probabilistic grammar and a sentence, this algorithm builds a parse tree in a top-down manner, with the words of the sentence as the leaves. It works with a dynamic programming approach, building a chart to store parses for substrings so that it doesn't have to analyze them again (just like the CYK algorithm). Each non-terminal, starting from S, gets replaced by its right-hand side rules in the chart until we end up with the correct parses.\n", + "\n", + "### Implementation\n", + "\n", + "A parse is in the form `[start, end, non-terminal, sub-tree, expected-transformation]`, where `sub-tree` is a tree with the corresponding `non-terminal` as its root and `expected-transformation` is a right-hand side rule of the `non-terminal`.\n", + "\n", + "The chart parsing is implemented in a class, `Chart`. It is initialized with grammar and can return the list of all the parses of a sentence with the `parses` function.\n", + "\n", + "The chart is a list of lists. The lists correspond to the lengths of substrings (including the empty string), from start to finish. When we say 'a point in the chart', we refer to a list of a certain length.\n", + "\n", + "A quick rundown of the class functions:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* `parses`: Returns a list of parses for a given sentence. If the sentence can't be parsed, it will return an empty list. Initializes the process by calling `parse` from the starting symbol.\n", + "\n", + "\n", + "* `parse`: Parses the list of words and builds the chart.\n", + "\n", + "\n", + "* `add_edge`: Adds another edge to the chart at a given point. Also, examines whether the edge extends or predicts another edge. If the edge itself is not expecting a transformation, it will extend other edges and it will predict edges otherwise.\n", + "\n", + "\n", + "* `scanner`: Given a word and a point in the chart, it extends edges that were expecting a transformation that can result in the given word. For example, if the word 'the' is an 'Article' and we are examining two edges at a chart's point, with one expecting an 'Article' and the other a 'Verb', the first one will be extended while the second one will not.\n", + "\n", + "\n", + "* `predictor`: If an edge can't extend other edges (because it is expecting a transformation itself), we will add to the chart rules/transformations that can help extend the edge. The new edges come from the right-hand side of the expected transformation's rules. For example, if an edge is expecting the transformation 'Adjective Noun', we will add to the chart an edge for each right-hand side rule of the non-terminal 'Adjective'.\n", + "\n", + "\n", + "* `extender`: Extends edges given an edge (called `E`). If `E`'s non-terminal is the same as the expected transformation of another edge (let's call it `A`), add to the chart a new edge with the non-terminal of `A` and the transformations of `A` minus the non-terminal that matched with `E`'s non-terminal. For example, if an edge `E` has 'Article' as its non-terminal and is expecting no transformation, we need to see what edges it can extend. Let's examine the edge `N`. This expects a transformation of 'Noun Verb'. 'Noun' does not match with 'Article', so we move on. Another edge, `A`, expects a transformation of 'Article Noun' and has a non-terminal of 'NP'. We have a match! A new edge will be added with 'NP' as its non-terminal (the non-terminal of `A`) and 'Noun' as the expected transformation (the rest of the expected transformation of `A`).\n", + "\n", + "You can view the source code by running the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(Chart)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "We will use the grammar `E0` to parse the sentence \"the stench is in 2 2\".\n", + "\n", + "First, we need to build a `Chart` object:" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "chart = Chart(E0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then we simply call the `parses` function:" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[0, 6, 'S', [[0, 2, 'NP', [('Article', 'the'), ('Noun', 'stench')], []], [2, 6, 'VP', [[2, 3, 'VP', [('Verb', 'is')], []], [3, 6, 'PP', [('Preposition', 'in'), [4, 6, 'NP', [('Digit', '2'), ('Digit', '2')], []]], []]], []]], []]]\n" + ] + } + ], + "source": [ + "print(chart.parses('the stench is in 2 2'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see which edges get added by setting the optional initialization argument `trace` to true." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chart_trace = Chart(nlp.E0, trace=True)\n", + "chart_trace.parses('the stench is in 2 2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try and parse a sentence that is not recognized by the grammar:" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n" + ] + } + ], + "source": [ + "print(chart.parses('the stench 2 2'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An empty list was returned." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CYK Parse\n", + "\n", + "The *CYK Parsing Algorithm* (named after its inventors, Cocke, Younger, and Kasami) utilizes dynamic programming to parse sentences of grammar in *Chomsky Normal Form*.\n", + "\n", + "The CYK algorithm returns an *M x N x N* array (named *P*), where *N* is the number of words in the sentence and *M* the number of non-terminal symbols in the grammar. Each element in this array shows the probability of a substring being transformed from a particular non-terminal. To find the most probable parse of the sentence, a search in the resulting array is required. Search heuristic algorithms work well in this space, and we can derive the heuristics from the properties of the grammar.\n", + "\n", + "The algorithm in short works like this: There is an external loop that determines the length of the substring. Then the algorithm loops through the words in the sentence. For each word, it again loops through all the words to its right up to the first-loop length. The substring will work on in this iteration is the words from the second-loop word with the first-loop length. Finally, it loops through all the rules in the grammar and updates the substring's probability for each right-hand side non-terminal." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "The implementation takes as input a list of words and a probabilistic grammar (from the `ProbGrammar` class detailed above) in CNF and returns the table/dictionary *P*. An item's key in *P* is a tuple in the form `(Non-terminal, the start of a substring, length of substring)`, and the value is a `Tree` object. The `Tree` data structure has two attributes: `root` and `leaves`. `root` stores the value of current tree node and `leaves` is a list of children nodes which may be terminal states(words in the sentence) or a sub tree.\n", + "\n", + "For example, for the sentence \"the monkey is dancing\" and the substring \"the monkey\" an item can be `('NP', 0, 2): `, which means the first two words (the substring from index 0 and length 2) can be parse to a `NP` and the detailed operations are recorded by a `Tree` object.\n", + "\n", + "Before we continue, you can take a look at the source code by running the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from nlp4e import *\n", + "from notebook4e import psource" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(CYK_parse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When updating the probability of a substring, we pick the max of its current one and the probability of the substring broken into two parts: one from the second-loop word with third-loop length, and the other from the first part's end to the remainder of the first-loop length.\n", + "\n", + "### Example\n", + "\n", + "Let's build a probabilistic grammar in CNF:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "E_Prob_Chomsky = ProbGrammar(\"E_Prob_Chomsky\", # A Probabilistic Grammar in CNF\n", + " ProbRules(\n", + " S = \"NP VP [1]\",\n", + " NP = \"Article Noun [0.6] | Adjective Noun [0.4]\",\n", + " VP = \"Verb NP [0.5] | Verb Adjective [0.5]\",\n", + " ),\n", + " ProbLexicon(\n", + " Article = \"the [0.5] | a [0.25] | an [0.25]\",\n", + " Noun = \"robot [0.4] | sheep [0.4] | fence [0.2]\",\n", + " Adjective = \"good [0.5] | new [0.2] | sad [0.3]\",\n", + " Verb = \"is [0.5] | say [0.3] | are [0.2]\"\n", + " ))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see the probabilities table for the sentence \"the robot is good\":" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "defaultdict(, {('Article', 0, 0): , ('Noun', 1, 1): , ('Verb', 2, 2): , ('Adjective', 3, 3): , ('VP', 2, 3): })\n" + ] + } + ], + "source": [ + "words = ['the', 'robot', 'is', 'good']\n", + "grammar = E_Prob_Chomsky\n", + "\n", + "P = CYK_parse(words, grammar)\n", + "print(P)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A `defaultdict` object is returned (`defaultdict` is basically a dictionary but with a default value/type). Keys are tuples in the form mentioned above and the values are the corresponding parse trees which demonstrates how the sentence will be parsed. Let's check the details of each parsing:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{('Article', 0, 0): ['the'], ('Noun', 1, 1): ['robot'], ('Verb', 2, 2): ['is'], ('Adjective', 3, 3): ['good'], ('VP', 2, 3): [, ]}\n" + ] + } + ], + "source": [ + "parses = {k: p.leaves for k, p in P.items()}\n", + "\n", + "print(parses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Please note that each item in the returned dict represents a parsing strategy. For instance, `('Article', 0, 0): ['the']` means parsing the article at position 0 from the word `the`. For the key `'VP', 2, 3`, it is mapped to another `Tree` which means this is a nested parsing step. If we print this item in detail: " + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['is']\n", + "['good']\n" + ] + } + ], + "source": [ + "for subtree in P['VP', 2, 3].leaves:\n", + " print(subtree.leaves)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So we can interpret this step as parsing the word at index 2 and 3 together('is' and 'good') as a verh phrase." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A-star Parsing\n", + "\n", + "The CYK algorithm uses space of $O(n^2m)$ for the P and T tables, where n is the number of words in the sentence, and m is the number of nonterminal symbols in the grammar and takes time $O(n^3m)$. This is the best algorithm if we want to find the best parse and works for all possible context-free grammars. But actually, we only want to parse natural languages, not all possible grammars, which allows us to apply more efficient algorithms.\n", + "\n", + "By applying a-start search, we are using the state-space search and we can get $O(n)$ running time. In this situation, each state is a list of items (words or categories), the start state is a list of words, and a goal state is the single item S. \n", + "\n", + "In our code, we implemented a demonstration of `astar_search_parsing` which deals with the text parsing problem. By specifying different `words` and `gramma`, we can use this searching strategy to deal with different text parsing problems. The algorithm returns a boolean telling whether the input words is a sentence under the given grammar.\n", + "\n", + "For detailed implementation, please execute the following block:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(astar_search_parsing)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Now let's try \"the wumpus is dead\" example. First we need to define the grammer and words in the sentence." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [], + "source": [ + "grammar = E0\n", + "words = ['the', 'wumpus', 'is', 'dead']" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'S'" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "astar_search_parsing(words, grammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The algorithm returns a 'S' which means it treats the inputs as a sentence. If we change the order of words to make it unreadable:" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "words_swaped = [\"the\", \"is\", \"wupus\", \"dead\"]\n", + "astar_search_parsing(words_swaped, grammar)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then the algorithm asserts that out words cannot be a sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Beam Search Parsing\n", + "\n", + "In the beam searching algorithm, we still treat the text parsing problem as a state-space searching algorithm. when using beam search, we consider only the b most probable alternative parses. This means we are not guaranteed to find the parse with the highest probability, but (with a careful implementation) the parser can operate in $O(n)$ time and still finds the best parse most of the time. A beam search parser with b = 1 is called a **deterministic parser**.\n", + "\n", + "### Implementation\n", + "\n", + "In the beam search, we maintain a `frontier` which is a priority queue keep tracking of the current frontier of searching. In each step, we explore all the examples in `frontier` and saves the best n examples as the frontier of the exploration of the next step.\n", + "\n", + "For detailed implementation, please view with the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(beam_search_parsing)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Let's try both the positive and negative wumpus example on this algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'S'" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beam_search_parsing(words, grammar)" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beam_search_parsing(words_swaped, grammar)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter22/images/parse_tree.png b/notebooks/chapter22/images/parse_tree.png new file mode 100644 index 000000000..f6ca87b2f Binary files /dev/null and b/notebooks/chapter22/images/parse_tree.png differ diff --git a/notebooks/chapter22/nlp_apps.ipynb b/notebooks/chapter22/nlp_apps.ipynb new file mode 100644 index 000000000..bd38efadf --- /dev/null +++ b/notebooks/chapter22/nlp_apps.ipynb @@ -0,0 +1,1038 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NATURAL LANGUAGE PROCESSING APPLICATIONS\n", + "\n", + "In this notebook we will take a look at some indicative applications of natural language processing. We will cover content from [`nlp.py`](https://github.com/aimacode/aima-python/blob/master/nlp.py) and [`text.py`](https://github.com/aimacode/aima-python/blob/master/text.py), for chapters 22 and 23 of Stuart Russel's and Peter Norvig's book [*Artificial Intelligence: A Modern Approach*](http://aima.cs.berkeley.edu/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CONTENTS\n", + "\n", + "* Language Recognition\n", + "* Author Recognition\n", + "* The Federalist Papers\n", + "* Text Classification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LANGUAGE RECOGNITION\n", + "\n", + "A very useful application of text models (you can read more on them on the [`text notebook`](https://github.com/aimacode/aima-python/blob/master/text.ipynb)) is categorizing text into a language. In fact, with enough data we can categorize correctly mostly any text. That is because different languages have certain characteristics that set them apart. For example, in German it is very usual for 'c' to be followed by 'h' while in English we see 't' followed by 'h' a lot.\n", + "\n", + "Here we will build an application to categorize sentences in either English or German.\n", + "\n", + "First we need to build our dataset. We will take as input text in English and in German and we will extract n-gram character models (in this case, *bigrams* for n=2). For English, we will use *Flatland* by Edwin Abbott and for German *Faust* by Goethe.\n", + "\n", + "Let's build our text models for each language, which will hold the probability of each bigram occuring in the text." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "flatland = open_data(\"EN-text/flatland.txt\").read()\n", + "wordseq = words(flatland)\n", + "\n", + "P_flatland = NgramCharModel(2, wordseq)\n", + "\n", + "faust = open_data(\"GE-text/faust.txt\").read()\n", + "wordseq = words(faust)\n", + "\n", + "P_faust = NgramCharModel(2, wordseq)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use this information to build a *Naive Bayes Classifier* that will be used to categorize sentences (you can read more on Naive Bayes on the [`learning notebook`](https://github.com/aimacode/aima-python/blob/master/learning.ipynb)). The classifier will take as input the probability distribution of bigrams and given a list of bigrams (extracted from the sentence to be classified), it will calculate the probability of the example/sentence coming from each language and pick the maximum.\n", + "\n", + "Let's build our classifier, with the assumption that English is as probable as German (the input is a dictionary with values the text models and keys the tuple `language, probability`):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('English', 1): P_flatland, ('German', 1): P_faust}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we need to write a function that takes as input a sentence, breaks it into a list of bigrams and classifies it with the naive bayes classifier from above.\n", + "\n", + "Once we get the text model for the sentence, we need to unravel it. The text models show the probability of each bigram, but the classifier can't handle that extra data. It requires a simple *list* of bigrams. So, if the text model shows that a bigram appears three times, we need to add it three times in the list. Since the text model stores the n-gram information in a dictionary (with the key being the n-gram and the value the number of times the n-gram appears) we need to iterate through the items of the dictionary and manually add them to the list of n-grams." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS, n):\n", + " sentence = sentence.lower()\n", + " wordseq = words(sentence)\n", + " \n", + " P_sentence = NgramCharModel(n, wordseq)\n", + " \n", + " ngrams = []\n", + " for b, p in P_sentence.dictionary.items():\n", + " ngrams += [b]*p\n", + " \n", + " print(ngrams)\n", + " \n", + " return nBS(ngrams)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can start categorizing sentences." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'i'), ('i', 'c'), ('c', 'h'), (' ', 'b'), ('b', 'i'), ('i', 'n'), ('i', 'n'), (' ', 'e'), ('e', 'i'), (' ', 'p'), ('p', 'l'), ('l', 'a'), ('a', 't'), ('t', 'z')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'German'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Ich bin ein platz\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 't'), ('t', 'u'), ('u', 'r'), ('r', 't'), ('t', 'l'), ('l', 'e'), ('e', 's'), (' ', 'f'), ('f', 'l'), ('l', 'y'), (' ', 'h'), ('h', 'i'), ('i', 'g'), ('g', 'h')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'English'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Turtles fly high\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'd'), ('d', 'e'), ('e', 'r'), ('e', 'r'), (' ', 'p'), ('p', 'e'), ('e', 'l'), ('l', 'i'), ('i', 'k'), ('k', 'a'), ('a', 'n'), (' ', 'i'), ('i', 's'), ('s', 't'), (' ', 'h'), ('h', 'i'), ('i', 'e')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'German'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"Der pelikan ist hier\", nBS, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(' ', 'a'), ('a', 'n'), ('n', 'd'), (' ', 't'), (' ', 't'), ('t', 'h'), ('t', 'h'), ('h', 'u'), ('u', 's'), ('h', 'e'), (' ', 'w'), ('w', 'i'), ('i', 'z'), ('z', 'a'), ('a', 'r'), ('r', 'd'), (' ', 's'), ('s', 'p'), ('p', 'o'), ('o', 'k'), ('k', 'e')]\n" + ] + }, + { + "data": { + "text/plain": [ + "'English'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"And thus the wizard spoke\", nBS, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can add more languages if you want, the algorithm works for as many as you like! Also, you can play around with *n*. Here we used 2, but other numbers work too (even though 2 suffices). The algorithm is not perfect, but it has high accuracy even for small samples like the ones we used. That is because English and German are very different languages. The closer together languages are (for example, Norwegian and Swedish share a lot of common ground) the lower the accuracy of the classifier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AUTHOR RECOGNITION\n", + "\n", + "Another similar application to language recognition is recognizing who is more likely to have written a sentence, given text written by them. Here we will try and predict text from Edwin Abbott and Jane Austen. They wrote *Flatland* and *Pride and Prejudice* respectively.\n", + "\n", + "We are optimistic we can determine who wrote what based on the fact that Abbott wrote his novella on much later date than Austen, which means there will be linguistic differences between the two works. Indeed, *Flatland* uses more modern and direct language while *Pride and Prejudice* is written in a more archaic tone containing more sophisticated wording.\n", + "\n", + "Similarly with Language Recognition, we will first import the two datasets. This time though we are not looking for connections between characters, since that wouldn't give that great results. Why? Because both authors use English and English follows a set of patterns, as we show earlier. Trying to determine authorship based on this patterns would not be very efficient.\n", + "\n", + "Instead, we will abstract our querying to a higher level. We will use words instead of characters. That way we can more accurately pick at the differences between their writing style and thus have a better chance at guessing the correct author.\n", + "\n", + "Let's go right ahead and import our data:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "flatland = open_data(\"EN-text/flatland.txt\").read()\n", + "wordseq = words(flatland)\n", + "\n", + "P_Abbott = UnigramWordModel(wordseq, 5)\n", + "\n", + "pride = open_data(\"EN-text/pride.txt\").read()\n", + "wordseq = words(pride)\n", + "\n", + "P_Austen = UnigramWordModel(wordseq, 5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This time we set the `default` parameter of the model to 5, instead of 0. If we leave it at 0, then when we get a sentence containing a word we have not seen from that particular author, the chance of that sentence coming from that author is exactly 0 (since to get the probability, we multiply all the separate probabilities; if one is 0 then the result is also 0). To avoid that, we tell the model to add 5 to the count of all the words that appear.\n", + "\n", + "Next we will build the Naive Bayes Classifier:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('Abbott', 1): P_Abbott, ('Austen', 1): P_Austen}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have build our classifier, we will start classifying. First, we need to convert the given sentence to the format the classifier needs. That is, a list of words." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " sentence = sentence.lower()\n", + " sentence_words = words(sentence)\n", + " \n", + " return nBS(sentence_words)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we will input a sentence that is something Abbott would write. Note the use of square and the simpler language." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Abbott'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"the square is mad\", nBS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The classifier correctly guessed Abbott.\n", + "\n", + "Next we will input a more sophisticated sentence, similar to the style of Austen." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Austen'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recognize(\"a most peculiar acquaintance\", nBS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The classifier guessed correctly again.\n", + "\n", + "You can try more sentences on your own. Unfortunately though, since the datasets are pretty small, chances are the guesses will not always be correct." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## THE FEDERALIST PAPERS\n", + "\n", + "Let's now take a look at a harder problem, classifying the authors of the [Federalist Papers](https://en.wikipedia.org/wiki/The_Federalist_Papers). The *Federalist Papers* are a series of papers written by Alexander Hamilton, James Madison and John Jay towards establishing the United States Constitution.\n", + "\n", + "What is interesting about these papers is that they were all written under a pseudonym, \"Publius\", to keep the identity of the authors a secret. Only after Hamilton's death, when a list was found written by him detailing the authorship of the papers, did the rest of the world learn what papers each of the authors wrote. After the list was published, Madison chimed in to make a couple of corrections: Hamilton, Madison said, hastily wrote down the list and assigned some papers to the wrong author!\n", + "\n", + "Here we will try and find out who really wrote these mysterious papers.\n", + "\n", + "To solve this we will learn from the undisputed papers to predict the disputed ones. First, let's read the texts from the file:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import open_data\n", + "from text import *\n", + "\n", + "federalist = open_data(\"EN-text/federalist.txt\").read()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how the text looks. We will print the first 500 characters:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The Project Gutenberg EBook of The Federalist Papers, by \\nAlexander Hamilton and John Jay and James Madison\\n\\nThis eBook is for the use of anyone anywhere at no cost and with\\nalmost no restrictions whatsoever. You may copy it, give it away or\\nre-use it under the terms of the Project Gutenberg License included\\nwith this eBook or online at www.gutenberg.net\\n\\n\\nTitle: The Federalist Papers\\n\\nAuthor: Alexander Hamilton\\n John Jay\\n James Madison\\n\\nPosting Date: December 12, 2011 [EBook #18]'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "federalist[:500]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems that the text file opens with a license agreement, hardly useful in our case. In fact, the license spans 113 words, while there is also a licensing agreement at the end of the file, which spans 3098 words. We need to remove them. To do so, we will first convert the text into words, to make our lives easier." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "wordseq = words(federalist)\n", + "wordseq = wordseq[114:-3098]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now take a look at the first 100 words:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'federalist no 1 general introduction for the independent journal hamilton to the people of the state of new york after an unequivocal experience of the inefficacy of the subsisting federal government you are called upon to deliberate on a new constitution for the united states of america the subject speaks its own importance comprehending in its consequences nothing less than the existence of the union the safety and welfare of the parts of which it is composed the fate of an empire in many respects the most interesting in the world it has been frequently remarked that it seems to'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "' '.join(wordseq[:100])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Much better.\n", + "\n", + "As with any Natural Language Processing problem, it is prudent to do some text pre-processing and clean our data before we start building our model. Remember that all the papers are signed as 'Publius', so we can safely remove that word, since it doesn't give us any information as to the real author.\n", + "\n", + "NOTE: Since we are only removing a single word from each paper, this step can be skipped. We add it here to show that processing the data in our hands is something we should always be considering. Oftentimes pre-processing the data in just the right way is the difference between a robust model and a flimsy one." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "wordseq = [w for w in wordseq if w != 'publius']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have to separate the text from a block of words into papers and assign them to their authors. We can see that each paper starts with the word 'federalist', so we will split the text on that word.\n", + "\n", + "The disputed papers are the papers from 49 to 58, from 18 to 20 and paper 64. We want to leave these papers unassigned. Also, note that there are two versions of paper 70; both from Hamilton.\n", + "\n", + "Finally, to keep the implementation intuitive, we add a `None` object at the start of the `papers` list to make the list index match up with the paper numbering (for example, `papers[5]` now corresponds to paper no. 5 instead of the paper no.6 in the 0-indexed Python)." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(4, 16, 52)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import re\n", + "\n", + "papers = re.split(r'federalist\\s', ' '.join(wordseq))\n", + "papers = [p for p in papers if p not in ['', ' ']]\n", + "papers = [None] + papers\n", + "\n", + "disputed = list(range(49, 58+1)) + [18, 19, 20, 64]\n", + "jay, madison, hamilton = [], [], []\n", + "for i, p in enumerate(papers):\n", + " if i in disputed or i == 0:\n", + " continue\n", + " \n", + " if 'jay' in p:\n", + " jay.append(p)\n", + " elif 'madison' in p:\n", + " madison.append(p)\n", + " else:\n", + " hamilton.append(p)\n", + "\n", + "len(jay), len(madison), len(hamilton)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, from the undisputed papers Jay wrote 4, Madison 17 and Hamilton 51 (+1 duplicate). Let's now build our word models. The Unigram Word Model again will come in handy." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "hamilton = ''.join(hamilton)\n", + "hamilton_words = words(hamilton)\n", + "P_hamilton = UnigramWordModel(hamilton_words, default=1)\n", + "\n", + "madison = ''.join(madison)\n", + "madison_words = words(madison)\n", + "P_madison = UnigramWordModel(madison_words, default=1)\n", + "\n", + "jay = ''.join(jay)\n", + "jay_words = words(jay)\n", + "P_jay = UnigramWordModel(jay_words, default=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now it is time to build our new Naive Bayes Learner. It is very similar to the one found in `learning.py`, but with an important difference: it doesn't classify an example, but instead returns the probability of the example belonging to each class. This will allow us to not only see to whom a paper belongs to, but also the probability of authorship as well. \n", + "We will build two versions of Learners, one will multiply probabilities as is and other will add the logarithms of them.\n", + "\n", + "Finally, since we are dealing with long text and the string of probability multiplications is long, we will end up with the results being rounded to 0 due to floating point underflow. To work around this problem we will use the built-in Python library `decimal`, which allows as to set decimal precision to much larger than normal.\n", + "\n", + "Note that the logarithmic learner will compute a negative likelihood since the logarithm of values less than 1 will be negative.\n", + "Thus, the author with the lesser magnitude of proportion is more likely to have written that paper.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import decimal\n", + "import math\n", + "from decimal import Decimal\n", + "\n", + "decimal.getcontext().prec = 100\n", + "\n", + "def precise_product(numbers):\n", + " result = 1\n", + " for x in numbers:\n", + " result *= Decimal(x)\n", + " return result\n", + "\n", + "def log_product(numbers):\n", + " result = 0.0\n", + " for x in numbers:\n", + " result += math.log(x)\n", + " return result\n", + "\n", + "def NaiveBayesLearner(dist):\n", + " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", + " Counter distributions and can then be used to find the probability\n", + " of a given item belonging to each class.\n", + " The input dictionary is in the following form:\n", + " ClassName: Counter\"\"\"\n", + " attr_dist = {c_name: count_prob for c_name, count_prob in dist.items()}\n", + "\n", + " def predict(example):\n", + " \"\"\"Predict the probabilities for each class.\"\"\"\n", + " def class_prob(target, e):\n", + " attr = attr_dist[target]\n", + " return precise_product([attr[a] for a in e])\n", + "\n", + " pred = {t: class_prob(t, example) for t in dist.keys()}\n", + "\n", + " total = sum(pred.values())\n", + " for k, v in pred.items():\n", + " pred[k] = v / total\n", + "\n", + " return pred\n", + "\n", + " return predict\n", + "\n", + "def NaiveBayesLearnerLog(dist):\n", + " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", + " Counter distributions and can then be used to find the probability\n", + " of a given item belonging to each class. It will compute the likelihood by adding the logarithms of probabilities.\n", + " The input dictionary is in the following form:\n", + " ClassName: Counter\"\"\"\n", + " attr_dist = {c_name: count_prob for c_name, count_prob in dist.items()}\n", + "\n", + " def predict(example):\n", + " \"\"\"Predict the probabilities for each class.\"\"\"\n", + " def class_prob(target, e):\n", + " attr = attr_dist[target]\n", + " return log_product([attr[a] for a in e])\n", + "\n", + " pred = {t: class_prob(t, example) for t in dist.keys()}\n", + "\n", + " total = -sum(pred.values())\n", + " for k, v in pred.items():\n", + " pred[k] = v/total\n", + "\n", + " return pred\n", + "\n", + " return predict\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we will build our Learner. Note that even though Hamilton wrote the most papers, that doesn't make it more probable that he wrote the rest, so all the class probabilities will be equal. We can change them if we have some external knowledge, which for this tutorial we do not have." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "dist = {('Madison', 1): P_madison, ('Hamilton', 1): P_hamilton, ('Jay', 1): P_jay}\n", + "nBS = NaiveBayesLearner(dist)\n", + "nBSL = NaiveBayesLearnerLog(dist)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As usual, the `recognize` function will take as input a string and after removing capitalization and splitting it into words, will feed it into the Naive Bayes Classifier." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " return nBS(words(sentence.lower()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can start predicting the disputed papers:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Straightforward Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 50: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 51: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 52: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 53: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 54: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 55: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 56: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 57: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 58: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 18: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 19: Hamilton: 0.0000 Madison: 0.0000 Jay: 1.0000\n", + "Paper No. 20: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 64: Hamilton: 1.0000 Madison: 0.0000 Jay: 0.0000\n", + "\n", + "Logarithmic Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: -0.330591 Madison: -0.327717 Jay: -0.341692\n", + "Paper No. 50: Hamilton: -0.333119 Madison: -0.328454 Jay: -0.338427\n", + "Paper No. 51: Hamilton: -0.330246 Madison: -0.325758 Jay: -0.343996\n", + "Paper No. 52: Hamilton: -0.331094 Madison: -0.327491 Jay: -0.341415\n", + "Paper No. 53: Hamilton: -0.330942 Madison: -0.328364 Jay: -0.340693\n", + "Paper No. 54: Hamilton: -0.329566 Madison: -0.327157 Jay: -0.343277\n", + "Paper No. 55: Hamilton: -0.330821 Madison: -0.328143 Jay: -0.341036\n", + "Paper No. 56: Hamilton: -0.330333 Madison: -0.327496 Jay: -0.342171\n", + "Paper No. 57: Hamilton: -0.330625 Madison: -0.328602 Jay: -0.340772\n", + "Paper No. 58: Hamilton: -0.330271 Madison: -0.327215 Jay: -0.342515\n", + "Paper No. 18: Hamilton: -0.337781 Madison: -0.330932 Jay: -0.331287\n", + "Paper No. 19: Hamilton: -0.335635 Madison: -0.331774 Jay: -0.332590\n", + "Paper No. 20: Hamilton: -0.334911 Madison: -0.331866 Jay: -0.333223\n", + "Paper No. 64: Hamilton: -0.331004 Madison: -0.332968 Jay: -0.336028\n" + ] + } + ], + "source": [ + "print('\\nStraightforward Naive Bayes Learner\\n')\n", + "for d in disputed:\n", + " probs = recognize(papers[d], nBS)\n", + " results = ['{}: {:.4f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n", + "print('\\nLogarithmic Naive Bayes Learner\\n')\n", + "for d in disputed:\n", + " probs = recognize(papers[d], nBSL)\n", + " results = ['{}: {:.6f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that both learners classify the papers identically. Because of underflow in the straightforward learner, only one author remains with a positive value. The log learner is more accurate with marginal differences between all the authors. \n", + "\n", + "This is a simple approach to the problem and thankfully researchers are fairly certain that papers 49-58 were all written by Madison, while 18-20 were written in collaboration between Hamilton and Madison, with Madison being credited for most of the work. Our classifier is not that far off. It correctly identifies the papers written by Madison, even the ones in collaboration with Hamilton.\n", + "\n", + "Unfortunately, it misses paper 64. Consensus is that the paper was written by John Jay, while our classifier believes it was written by Hamilton. The classifier is wrong there because it does not have much information on Jay's writing; only 4 papers. This is one of the problems with using unbalanced datasets such as this one, where information on some classes is sparser than information on the rest. To avoid this, we can add more writings for Jay and Madison to end up with an equal amount of data for each author." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## Text Classification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Text Classification** is assigning a category to a document based on the content of the document. Text Classification is one of the most popular and fundamental tasks of Natural Language Processing. Text classification can be applied on a variety of texts like *Short Documents* (like tweets, customer reviews, etc.) and *Long Document* (like emails, media articles, etc.).\n", + "\n", + "We already have seen an example of Text Classification in the above tasks like Language Identification, Author Recognition and Federalist Paper Identification.\n", + "\n", + "### Applications\n", + "Some of the broad applications of Text Classification are:-\n", + "- Language Identification\n", + "- Author Recognition\n", + "- Sentiment Analysis\n", + "- Spam Mail Detection\n", + "- Topic Labelling \n", + "- Word Sense Disambiguation\n", + "\n", + "### Use Cases\n", + "Some of the use cases of Text classification are:-\n", + "- Social Media Monitoring\n", + "- Brand Monitoring\n", + "- Auto-tagging of user queries\n", + "\n", + "For Text Classification, we would be using the Naive Bayes Classifier. The reasons for using Naive Bayes Classifier are:-\n", + "- Being a probabilistic classifier, therefore, will calculate the probability of each category\n", + "- It is fast, reliable and accurate \n", + "- Naive Bayes Classifiers have already been used to solve many Natural Language Processing (NLP) applications.\n", + "\n", + "Here we would here be covering an example of **Word Sense Disambiguation** as an application of Text Classification. It is used to remove the ambiguity of a given word if the word has two different meanings.\n", + "\n", + "As we know that we would be working on determining whether the word *apple* in a sentence refers to `fruit` or to a `company`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 1:- Defining the dataset** \n", + "\n", + "The dataset has been defined here so that everything is clear and can be tested with other things as well." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = [\n", + " \"Apple targets big business with new iOS 7 features. Finally... A corp iTunes account!\",\n", + " \"apple inc is searching for people to help and try out all their upcoming tablet within our own net page No.\",\n", + " \"Microsoft to bring Xbox and PC games to Apple, Android phones: Report: Microsoft Corp\",\n", + " \"When did green skittles change from lime to green apple?\",\n", + " \"Myra Oltman is the best. I told her I wanted to learn how to make apple pie, so she made me a kit!\",\n", + " \"Surreal Sat in a sewing room, surrounded by crap, listening to beautiful music eating apple pie.\"\n", + "]\n", + "\n", + "train_target = [\n", + " \"company\",\n", + " \"company\",\n", + " \"company\",\n", + " \"fruit\",\n", + " \"fruit\",\n", + " \"fruit\",\n", + "]\n", + "\n", + "class_0 = \"company\"\n", + "class_1 = \"fruit\"\n", + "\n", + "test_data = [\n", + " \"Apple Inc. supplier Foxconn demos its own iPhone-compatible smartwatch\",\n", + " \"I now know how to make a delicious apple pie thanks to the best teachers ever\"\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 2:- Preprocessing the dataset**\n", + "\n", + "In this step, we would be doing some preprocessing on the dataset like breaking the sentence into words and converting to lower case.\n", + "\n", + "We already have a `words(sent)` function defined in `text.py` which does the task of splitting the sentence into words." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "train_data_processed = [words(i) for i in train_data]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 3:- Feature Extraction from the text**\n", + "\n", + "Now we would be extracting features from the text like extracting the set of words used in both the categories i.e. `company` and `fruit`.\n", + "\n", + "The frequency of a word would help in calculating the probability of that word being in a particular class. " + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of words in `company` class: 49\n", + "Number of words in `fruit` class: 49\n" + ] + } + ], + "source": [ + "words_0 = []\n", + "words_1 = []\n", + "\n", + "for sent, tag in zip(train_data_processed, train_target):\n", + " if(tag == class_0):\n", + " words_0 += sent\n", + " elif(tag == class_1):\n", + " words_1 += sent\n", + " \n", + "print(\"Number of words in `{}` class: {}\".format(class_0, len(words_0)))\n", + "print(\"Number of words in `{}` class: {}\".format(class_1, len(words_1)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you might have observed, that our dataset is equally balanced, i.e. we have an equal number of words in both the classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 4:- Building the Naive Bayes Model**\n", + "\n", + "Using the Naive Bayes classifier we can calculate the probability of a word in `company` and `fruit` class and then multiplying all of them to get the probability of that sentence belonging each of the given classes. But if a word is not in our dictionary then this leads to the probability of that word belonging to that class becoming zero. For example:- the word *Foxconn* is not in the dictionary of any of the classes. Due to this, the probability of word *Foxconn* being in any of these classes becomes zero, and since all the probabilities are multiplied, this leads to the probability of that sentence belonging to any of the classes becoming zero. \n", + "\n", + "To solve the problem we need to use **smoothing**, i.e. providing a minimum non-zero threshold probability to every word that we come across.\n", + "\n", + "The `UnigramWordModel` class has implemented smoothing by taking an additional argument from the user, i.e. the minimum frequency that we would be giving to every word even if it is new to the dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "model_words_0 = UnigramWordModel(words_0, 1)\n", + "model_words_1 = UnigramWordModel(words_1, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we would be building the Naive Bayes model. For that, we would be making `dist` as we had done earlier in the Authorship Recognition Task." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "from learning import NaiveBayesLearner\n", + "\n", + "dist = {('company', 1): model_words_0, ('fruit', 1): model_words_1}\n", + "\n", + "nBS = NaiveBayesLearner(dist, simple=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Step 5:- Predict the class of a sentence**\n", + "\n", + "Now we will be writing a function that does pre-process of the sentences which we have taken for testing. And then predicting the class of every sentence in the document." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "def recognize(sentence, nBS):\n", + " sentence_words = words(sentence)\n", + " return nBS(sentence_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Apple Inc. supplier Foxconn demos its own iPhone-compatible smartwatch\t-company\n", + "I now know how to make a delicious apple pie thanks to the best teachers ever\t-fruit\n" + ] + } + ], + "source": [ + "# predicting the class of sentences in the test set\n", + "for i in test_data:\n", + " print(i + \"\\t-\" + recognize(i, nBS))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You might have observed that the predictions made by the model are correct and we are able to differentiate between sentences of different classes. You can try more sentences on your own. Unfortunately though, since the datasets are pretty small, chances are the guesses will not always be correct.\n", + "\n", + "As you might have observed, the above method is very much similar to the Author Recognition, which is also a type of Text Classification. Like this most of Text Classification have the same underlying structure and follow a similar procedure." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/chapter24/Image Edge Detection.ipynb b/notebooks/chapter24/Image Edge Detection.ipynb new file mode 100644 index 000000000..6429943a1 --- /dev/null +++ b/notebooks/chapter24/Image Edge Detection.ipynb @@ -0,0 +1,408 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Edge Detection\n", + "\n", + "Edge detection is one of the earliest and popular image processing tasks. Edges are straight lines or curves in the image plane across which there is a “significant” change in image brightness. The goal of edge detection is to abstract away from the messy, multi-megabyte image and towards a more compact, abstract representation.\n", + "\n", + "There are multiple ways to detect an edge in an image but the most may be grouped into two categories, gradient, and Laplacian. Here we will introduce some algorithms among them and their intuitions. First, let's import the necessary packages.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using TensorFlow backend.\n" + ] + } + ], + "source": [ + "import os, sys\n", + "sys.path = [os.path.abspath(\"../../\")] + sys.path\n", + "from perception4e import *\n", + "from notebook4e import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Gradient Edge Detection\n", + "\n", + "Because edges correspond to locations in images where the brightness undergoes a sharp change, a naive idea would be to differentiate the image and look for places where the magnitude of the derivative is large. For many simple cases with regular geometry topologies, this simple method could work. \n", + "\n", + "Here we introduce a 2D function $f(x,y)$ to represent the pixel values on a 2D image plane. Thus this method follows the math intuition below:\n", + "\n", + "$$\\frac{\\partial f(x,y)}{\\partial x} = \\lim_{\\epsilon \\rightarrow 0} \\frac{f(x+\\epsilon,y)-\\partial f(x,y)}{\\epsilon}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above is exactly the definition of the edges in an image. In real cases, $\\epsilon$ cannot be 0. We can only investigate the pixels in the neighborhood of the current one to get the derivation of a pixel. Thus the previous formula becomes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\\frac{\\partial f(x,y)}{\\partial x} = \\lim_{\\epsilon \\rightarrow 0} \\frac{f(x+1,y)-\\partial f(x,y)}{1}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To implement the above formula, we can simply apply a filter $[1,-1]$ to extract the differentiated image. For the case of derivation in the y-direction, we can transpose the above filter and apply it to the original image. The relation of partial deviation of the direction of edges are summarized in the following picture:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "We implemented an edge detector using a gradient method as `gradient_edge_detector` in `perceptron.py`. There are two filters defined as $[[1, -1]], [[1], [-1]]$ to extract edges in x and y directions respectively. The filters are applied to an image using `convolve2d` method in `scipy.single` package. The image passed into the function needs to be in the form of `numpy.ndarray` or an iterable object that can be transformed into a `ndarray`.\n", + "\n", + "To view the detailed implementation, please execute the following block" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(gradient_edge_detector)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Now let's try the detector for real case pictures. First, we will show the original picture before edge detection:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "We will use `matplotlib` to read the image as a numpy ndarray:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "image height: 590\n", + "image width: 787\n" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import matplotlib.image as mpimg\n", + "\n", + "im =mpimg.imread('images/stapler.png')\n", + "print(\"image height:\", len(im))\n", + "print(\"image width:\", len(im[0]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code shows we get an image with a size of $787*590$. `gaussian_derivative_edge_detector` can extract images in both x and y direction and then put them together in a ndarray:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "image height: 590\n", + "image width: 787\n" + ] + } + ], + "source": [ + "edges = gradient_edge_detector(im)\n", + "print(\"image height:\", len(edges))\n", + "print(\"image width:\", len(edges[0]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The edges are in the same shape of the original image. Now we will try print out the image, we implemented a `show_edges` function to do this:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "show_edges(edges)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the edges are extracted well. We can use the result of this simple algorithm as a baseline and compare the results of other algorithms to it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Derivative of Gaussian \n", + "\n", + "When considering the situation when there is strong noise in an image, the ups and downs of the noise will induce strong peaks in the gradient profile. In order to be more noise-robust, an algorithm introduced a Gaussian filter before applying the gradient filer. In another way, convolving a gradient filter after a Gaussian filter equals to convolving a derivative of Gaussian filter directly to the image.\n", + "\n", + "Here is how this intuition is represented in math:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$(I\\bigotimes g)\\bigotimes h = I\\bigotimes (g\\bigotimes h) $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Where $I$ is the image, $g$ is the gradient filter and $h$ is the Gaussian filter. A two dimensional derivative of Gaussian kernel is dipicted in the following figure:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "In our implementation, we initialize Gaussian filters by applying the 2D Gaussian function on a given size of the grid which is the same as the kernel size. Then the x and y direction image filters are calculated as the convolution of the Gaussian filter and the gradient filter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_filter = scipy.signal.convolve2d(gaussian_filter, np.asarray([[1, -1]]), 'same')\n", + "y_filter = scipy.signal.convolve2d(gaussian_filter, np.asarray([[1], [-1]]), 'same')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then both of the filters are applied to the input image to extract the x and y direction edges. For detailed implementation, please view by:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psource(gaussian_derivative_edge_detector)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Now let's try again on the stapler image and plot the extracted edges:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "e = gaussian_derivative_edge_detector(im)\n", + "show_edges(e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the extracted edges are more similar to the original one. The resulting edges are depending on the initial Gaussian kernel size and how it is initialized." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Laplacian Edge Detector\n", + "\n", + "Laplacian is somewhat different from the methods we have discussed so far. Unlike the above kernels which are only using the first-order derivatives of the original image, the Laplacian edge detector uses the second-order derivatives of the image. Using the second derivatives also makes the detector very sensitive to noise. Thus the image is often Gaussian smoothed before applying the Laplacian filter.\n", + "\n", + "Here are how the Laplacian detector looks like:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "There are two commonly used small Laplacian kernels:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In our implementation, we used the first one as the default kernel and convolve it with the original image using packages provided by `scipy`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "Now let's use the Laplacian edge detector to extract edges of the staple example:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAATAAAADnCAYAAACZtwrQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOy9SY9cR3a+f3KunGsixaGpHiC50W4b6I3hjT+CF4YX/speeGXYbrRa3S3JIsUia8qxcs78Ler/RD43mEzB8uIPA7wAQbIq896IE2d4z3tOxC3tdrv4dH26Pl2fr