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 e6563f0fe..e465e8e4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +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 six - - 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 c6cf16d19..17f1d6085 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,172 @@ -
------------------ + # `aima-python` [](https://travis-ci.org/aimacode/aima-python) [](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/), but also usable on their own. - -# 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. The [aima-pseudocode](https://github.com/aimacode/aima-pseudocode) project describes all the algorithms from the book. - - -| **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 7976b12b2..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: | \n", + "Feel Food | \n", + "Feel Water | \n", + "Feel Nothing | \n", + "
Action: | \n", + "eat | \n", + "drink | \n", + "move down | \n", + "
Percept: | \n", @@ -226,117 +461,254 @@ "Action: | \n", "eat | \n", "drink | \n", - "move up | \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",
+ "
def different_values_constraint(A, a, B, b):\n",
+ " """A constraint saying two neighboring variables must differ in value."""\n",
+ " return a != b\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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
def T(self, state, action):\n",
+ " if action is None:\n",
+ " return [(0.0, state)]\n",
+ " else:\n",
+ " return self.transitions[state][action]\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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "
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",
+ "