diff --git a/doc-sources/changelog.rst b/doc-sources/changelog.rst
index 7cf43e09..9de05d67 100644
--- a/doc-sources/changelog.rst
+++ b/doc-sources/changelog.rst
@@ -4,6 +4,23 @@
Changelog
=========
+Version 1.1.0a6 (2024-02-06)
+============================
+
+- **Breaking** Rename ``max_iteration_index`` to ``max_iterations_count`` in ``...TerminateAfterIterations.__init__``
+- Make the Python API guide Jupyter Notebook downloadable
+- Expose all attributes of ``WPB.LearningData``; start to document them in our Python API guide
+- Expose parameters of EvalMaxSAT in our API and command-line interface (see ``lincs learn classification-model --help``):
+
+ - ``--ucncs.max-sat-by-separation.solver`` (for consistency, always ``"eval-max-sat"`` for now)
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads``
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize``
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time``
+ - ``--ucncs.max-sat-by-coalitions.solver`` (for consistency, always ``"eval-max-sat"`` for now)
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.nb-minimize-threads``
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.timeout-fast-minimize``
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.coef-minimize-time``
+
Version 1.1.0a5 (2024-02-01)
============================
diff --git a/doc-sources/conceptual-overview/conceptual-overview.ipynb b/doc-sources/conceptual-overview/conceptual-overview.ipynb
index 1fca336f..d865266d 100644
--- a/doc-sources/conceptual-overview/conceptual-overview.ipynb
+++ b/doc-sources/conceptual-overview/conceptual-overview.ipynb
@@ -21,7 +21,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs classify problem.yml model.yml alternatives.csv\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs classify problem.yml model.yml alternatives.csv\n",
"name,Math,Physics,Literature,History,category\n",
"A,1,1,1,1,\"Full scholarship\"\n",
"B,1,1,1,0,\"Full scholarship\"\n",
diff --git a/doc-sources/get-started.rst b/doc-sources/get-started.rst
index 54e9be05..56f31a55 100644
--- a/doc-sources/get-started.rst
+++ b/doc-sources/get-started.rst
@@ -75,7 +75,7 @@ The generated ``problem.yml`` should look like:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-problem 4 3 --random-seed 40
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classification-problem 4 3 --random-seed 40
kind: classification-problem
format_version: 1
criteria:
@@ -146,7 +146,7 @@ It should look like:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort
kind: ncs-classification-model
format_version: 1
accepted_values:
@@ -219,7 +219,7 @@ It should start with something like this, and contain 1000 alternatives:
.. code:: text
- # Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0
name,"Criterion 1","Criterion 2","Criterion 3","Criterion 4",category
"Alternative 1",0.37454012,0.796543002,0.95071429,0.183434784,"Best category"
"Alternative 2",0.731993914,0.779690981,0.598658502,0.596850157,"Intermediate category 1"
@@ -255,7 +255,7 @@ so it is numerically different:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0
+ # Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0
kind: ncs-classification-model
format_version: 1
accepted_values:
diff --git a/doc-sources/get-started/get-started.ipynb b/doc-sources/get-started/get-started.ipynb
index 52e50d4c..3f4f32b9 100644
--- a/doc-sources/get-started/get-started.ipynb
+++ b/doc-sources/get-started/get-started.ipynb
@@ -48,7 +48,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-problem 4 3 --random-seed 40\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classification-problem 4 3 --random-seed 40\n",
"kind: classification-problem\n",
"format_version: 1\n",
"criteria:\n",
@@ -126,7 +126,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
@@ -211,7 +211,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0\n",
"name,\"Criterion 1\",\"Criterion 2\",\"Criterion 3\",\"Criterion 4\",category\n",
"\"Alternative 1\",0.37454012,0.796543002,0.95071429,0.183434784,\"Best category\"\n",
"\"Alternative 2\",0.731993914,0.779690981,0.598658502,0.596850157,\"Intermediate category 1\"\n",
@@ -260,7 +260,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
diff --git a/doc-sources/user-guide.rst b/doc-sources/user-guide.rst
index 3ffd5e50..c044281f 100644
--- a/doc-sources/user-guide.rst
+++ b/doc-sources/user-guide.rst
@@ -512,7 +512,7 @@ They produce a different kind of model, with the sufficient coalitions specified
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions
+ # Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions
kind: ncs-classification-model
format_version: 1
accepted_values:
diff --git a/doc-sources/user-guide/alglib-learning/alglib-learning.ipynb b/doc-sources/user-guide/alglib-learning/alglib-learning.ipynb
index 6b561860..313bfc7a 100644
--- a/doc-sources/user-guide/alglib-learning/alglib-learning.ipynb
+++ b/doc-sources/user-guide/alglib-learning/alglib-learning.ipynb
@@ -27,7 +27,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver alglib --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver alglib --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
diff --git a/doc-sources/user-guide/gpu-learning/gpu-learning.ipynb b/doc-sources/user-guide/gpu-learning/gpu-learning.ipynb
index 97977930..c9c6c1cc 100644
--- a/doc-sources/user-guide/gpu-learning/gpu-learning.ipynb
+++ b/doc-sources/user-guide/gpu-learning/gpu-learning.ipynb
@@ -27,7 +27,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor gpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor gpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
diff --git a/doc-sources/user-guide/sat-learning/sat-learning.ipynb b/doc-sources/user-guide/sat-learning/sat-learning.ipynb
index a0879a35..a3851b2c 100644
--- a/doc-sources/user-guide/sat-learning/sat-learning.ipynb
+++ b/doc-sources/user-guide/sat-learning/sat-learning.ipynb
@@ -22,7 +22,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
@@ -58,7 +58,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy max-sat-by-separation --ucncs.max-sat-by-separation.solver eval-max-sat --ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads 0 --ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize 60 --ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time 2\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy max-sat-by-separation --ucncs.max-sat-by-separation.solver eval-max-sat --ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads 0 --ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize 60 --ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time 2\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
diff --git a/doc-sources/user-guide/synthetic-data/synthetic-data.ipynb b/doc-sources/user-guide/synthetic-data/synthetic-data.ipynb
index f8c8631a..61afeb39 100644
--- a/doc-sources/user-guide/synthetic-data/synthetic-data.ipynb
+++ b/doc-sources/user-guide/synthetic-data/synthetic-data.ipynb
@@ -14,7 +14,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-problem 4 3 --random-seed 57\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classification-problem 4 3 --random-seed 57\n",
"kind: classification-problem\n",
"format_version: 1\n",
"criteria:\n",
@@ -63,7 +63,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classification-model problem.yml --random-seed 58 --model-type mrsort\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classification-model problem.yml --random-seed 58 --model-type mrsort\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
@@ -101,7 +101,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs generate classified-alternatives problem.yml model.yml 100 --random-seed 59 --misclassified-count 0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs generate classified-alternatives problem.yml model.yml 100 --random-seed 59 --misclassified-count 0\n",
"name,\"Criterion 1\",\"Criterion 2\",\"Criterion 3\",\"Criterion 4\",category\n",
"\"Alternative 1\",0.924035132,0.804616809,0.157870576,0.637420833,\"Best category\"\n",
"\"Alternative 2\",0.866915047,0.979161799,0.0841569453,0.397855282,\"Best category\"\n",
@@ -224,7 +224,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "# Reproduction command (with lincs version 1.1.0a6.dev0): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 60 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
+ "# Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 60 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0\n",
"kind: ncs-classification-model\n",
"format_version: 1\n",
"accepted_values:\n",
diff --git a/docs/.buildinfo b/docs/.buildinfo
index 0f7fa6c9..46c17ba1 100644
--- a/docs/.buildinfo
+++ b/docs/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: c3efb7cd4e9892bca8f9a10959dff7ff
+config: bf6a2a9ac0817ff6464d100acb2befb2
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/.doctrees/changelog.doctree b/docs/.doctrees/changelog.doctree
index 139dfba3..34869da9 100644
Binary files a/docs/.doctrees/changelog.doctree and b/docs/.doctrees/changelog.doctree differ
diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle
index dac910f4..73ffd531 100644
Binary files a/docs/.doctrees/environment.pickle and b/docs/.doctrees/environment.pickle differ
diff --git a/docs/.doctrees/get-started.doctree b/docs/.doctrees/get-started.doctree
index d6d01506..85d982e5 100644
Binary files a/docs/.doctrees/get-started.doctree and b/docs/.doctrees/get-started.doctree differ
diff --git a/docs/.doctrees/python-api.doctree b/docs/.doctrees/python-api.doctree
index ebd84073..539e9c69 100644
Binary files a/docs/.doctrees/python-api.doctree and b/docs/.doctrees/python-api.doctree differ
diff --git a/docs/.doctrees/reference.doctree b/docs/.doctrees/reference.doctree
index a68b3e02..f1884d44 100644
Binary files a/docs/.doctrees/reference.doctree and b/docs/.doctrees/reference.doctree differ
diff --git a/docs/.doctrees/reference/lincs.doctree b/docs/.doctrees/reference/lincs.doctree
index 4f1ef614..843d41e0 100644
Binary files a/docs/.doctrees/reference/lincs.doctree and b/docs/.doctrees/reference/lincs.doctree differ
diff --git a/docs/.doctrees/user-guide.doctree b/docs/.doctrees/user-guide.doctree
index cb134dcf..4527df3d 100644
Binary files a/docs/.doctrees/user-guide.doctree and b/docs/.doctrees/user-guide.doctree differ
diff --git a/docs/README.html b/docs/README.html
index baf45229..323625cb 100644
--- a/docs/README.html
+++ b/docs/README.html
@@ -5,11 +5,11 @@
-
Contributors — lincs 1.1.0a5 documentation
+ Contributors — lincs 1.1.0a6 documentation
-
+
diff --git a/docs/_downloads/019271a03d05a599bcfb6d68c868c86a/python-api.ipynb b/docs/_downloads/019271a03d05a599bcfb6d68c868c86a/python-api.ipynb
new file mode 100644
index 00000000..81a889b6
--- /dev/null
+++ b/docs/_downloads/019271a03d05a599bcfb6d68c868c86a/python-api.ipynb
@@ -0,0 +1,2264 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "03918549-cc28-44e3-9d3c-627c2a4e1683",
+ "metadata": {},
+ "source": [
+ "# The Python API"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ef1a1243-f769-4cf1-9016-960e656e2e3e",
+ "metadata": {},
+ "source": [
+ "This document builds up on [our \"Get Started\" guide](https://mics-lab.github.io/lincs/get-started.html) and our [user guide](https://mics-lab.github.io/lincs/user-guide.html), and introduces *lincs*' Python API.\n",
+ "This API is more flexible, albeit more complex, than the command-line interface you've been using so far.\n",
+ "\n",
+ "If you're a Jupyter user, you can [download the notebook](python-api/python-api.ipynb) this document is based on."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "110a27c5-0f21-4c03-84d7-1947a86bb8fd",
+ "metadata": {},
+ "source": [
+ "## Do it again, in Python"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a567f03d-64aa-4cbb-9ec5-d12a6ab4bec7",
+ "metadata": {},
+ "source": [
+ "First, lets do exactly the same thing as in our \"Get started\" guide, but using the Python API."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "77963196-9956-460c-989f-e7772f800a01",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from lincs import classification as lc"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d85a8506-401f-4a3d-893c-996b11e99fe2",
+ "metadata": {},
+ "source": [
+ "Generate a synthetic classification problem:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "08948c5a-22e5-498a-96a7-9826d47e2f5d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "problem = lc.generate_problem(criteria_count=4, categories_count=3, random_seed=40)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0a9103e8-8884-4606-88ca-ed41b2b0972f",
+ "metadata": {},
+ "source": [
+ "The first difference with the command-line interface is the third argument to the call to `generate_problem`: it's the pseudo-random seed optionally passed by the `--random-seed` option on the command line. All pseudo-random seeds are mandatory in the Python API, so that you have full control of reproducibility. If you don't care about it, you can use `random.randrange(2**30)` to use pseudo-random pseudo-random seeds. (No typo here: the pseudo-random seeds are pseudo-random.)\n",
+ "\n",
+ "Generated problems are returned as Python objects of class `lincs.Problem`. You can print them:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "65d792e1-0720-45d8-b756-fe74537865b0",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: classification-problem\n",
+ "format_version: 1\n",
+ "criteria:\n",
+ " - name: Criterion 1\n",
+ " value_type: real\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 1\n",
+ " - name: Criterion 2\n",
+ " value_type: real\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 1\n",
+ " - name: Criterion 3\n",
+ " value_type: real\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 1\n",
+ " - name: Criterion 4\n",
+ " value_type: real\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 1\n",
+ "ordered_categories:\n",
+ " - name: Worst category\n",
+ " - name: Intermediate category 1\n",
+ " - name: Best category\n"
+ ]
+ }
+ ],
+ "source": [
+ "import sys\n",
+ "problem.dump(sys.stdout)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "6875cdf0-9e37-4733-9b1d-11b474a09380",
+ "metadata": {},
+ "source": [
+ "Description functions generate a list of strings:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "4e37c394-a2e5-46ec-8813-5475f0a5c6f3",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "This a classification problem into 3 ordered categories named \"Worst category\", \"Intermediate category 1\" and \"Best category\".\n",
+ "The best category is \"Best category\" and the worst category is \"Worst category\".\n",
+ "There are 4 classification criteria (in no particular order).\n",
+ "Criterion \"Criterion 1\" takes real values between 0.0 and 1.0 included.\n",
+ "Higher values of \"Criterion 1\" are known to be better.\n",
+ "Criterion \"Criterion 2\" takes real values between 0.0 and 1.0 included.\n",
+ "Higher values of \"Criterion 2\" are known to be better.\n",
+ "Criterion \"Criterion 3\" takes real values between 0.0 and 1.0 included.\n",
+ "Higher values of \"Criterion 3\" are known to be better.\n",
+ "Criterion \"Criterion 4\" takes real values between 0.0 and 1.0 included.\n",
+ "Higher values of \"Criterion 4\" are known to be better.\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"\\n\".join(lc.describe_problem(problem)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b170efd-eef5-4d4b-b435-b3c246a5fda6",
+ "metadata": {},
+ "source": [
+ "Generate a synthetic MR-Sort classification model, again with an explicit pseudo-random seed:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "ba5ba906-1457-4544-a754-fa33f30c19d5",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.255905151, 0.676961303]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.0551739037, 0.324553937]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.162252158, 0.673279881]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.0526000932, 0.598555863]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [0.147771254, 0.618687689, 0.406786472, 0.0960085914]\n",
+ " - *coalitions\n"
+ ]
+ }
+ ],
+ "source": [
+ "model = lc.generate_mrsort_model(problem, random_seed=41)\n",
+ "\n",
+ "model.dump(problem, sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b836036d-1da3-427b-8c78-c9b457fca7a9",
+ "metadata": {},
+ "source": [
+ "Visualization functions interface with [Matplotlib](https://matplotlib.org/):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "4ef64c1f-1f27-4864-b527-a21b0b8b9596",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "3c2c0bb7-e738-4cd9-83cf-bd60e4774d58",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "axes = plt.subplots(1, 1, figsize=(6, 4), layout=\"constrained\")[1]\n",
+ "lc.visualize_model(problem, model, [], axes)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3bee34aa-beec-41ba-813a-3cd582aee9b0",
+ "metadata": {},
+ "source": [
+ "Get the model's description:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "d2eadcf7-4565-45fe-bbc3-9e5cd7c9b73b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "This is a MR-Sort (a.k.a. 1-Uc-NCS) model: an NCS model where the sufficient coalitions are specified using the same criterion weights for all boundaries.\n",
+ "The weights associated to each criterion are:\n",
+ " - Criterion \"Criterion 1\": 0.15\n",
+ " - Criterion \"Criterion 2\": 0.62\n",
+ " - Criterion \"Criterion 3\": 0.41\n",
+ " - Criterion \"Criterion 4\": 0.10\n",
+ "To get into an upper category, an alternative must be better than the following profiles on a set of criteria whose weights add up to at least 1:\n",
+ " - For category \"Intermediate category 1\": at least 0.26 on criterion \"Criterion 1\", at least 0.06 on criterion \"Criterion 2\", at least 0.16 on criterion \"Criterion 3\", and at least 0.05 on criterion \"Criterion 4\"\n",
+ " - For category \"Best category\": at least 0.68 on criterion \"Criterion 1\", at least 0.32 on criterion \"Criterion 2\", at least 0.67 on criterion \"Criterion 3\", and at least 0.60 on criterion \"Criterion 4\"\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"\\n\".join(lc.describe_model(problem, model)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "00fab94b-22a7-41ae-8dfa-48263d3d8dfe",
+ "metadata": {},
+ "source": [
+ "Generate a synthetic learning set (with an explicit pseudo-random seed):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "dac523f0-1340-4465-b487-7eca3bcd7c64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "learning_set = lc.generate_alternatives(problem, model, alternatives_count=1000, random_seed=42)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "76b3a98d-e69c-4164-8ba0-557ff039bef9",
+ "metadata": {},
+ "source": [
+ "Dump it (in memory instead of on `sys.stdout` to print only the first few lines):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "0d3fc508-480b-40dc-b7c9-1f7eb5637d8c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "name,\"Criterion 1\",\"Criterion 2\",\"Criterion 3\",\"Criterion 4\",category\n",
+ "\"Alternative 1\",0.37454012,0.796543002,0.95071429,0.183434784,\"Best category\"\n",
+ "\"Alternative 2\",0.731993914,0.779690981,0.598658502,0.596850157,\"Intermediate category 1\"\n",
+ "\"Alternative 3\",0.156018645,0.445832759,0.15599452,0.0999749228,\"Worst category\"\n",
+ "\"Alternative 4\",0.0580836125,0.4592489,0.866176128,0.333708614,\"Best category\"\n",
+ "\"Alternative 5\",0.601114988,0.14286682,0.708072603,0.650888503,\"Intermediate category 1\"\n",
+ "...\n"
+ ]
+ }
+ ],
+ "source": [
+ "import io\n",
+ "f = io.StringIO()\n",
+ "learning_set.dump(problem, f)\n",
+ "print(\"\\n\".join(f.getvalue().splitlines()[:6] + ['...']))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6e864665-f7b1-454e-ae4e-8b397e211266",
+ "metadata": {},
+ "source": [
+ "Visualize it:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "3c2d9304-eeb3-4b56-8dcf-368e46117048",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "axes = plt.subplots(1, 1, figsize=(6, 4), layout=\"constrained\")[1]\n",
+ "lc.visualize_model(problem, model, learning_set.alternatives[:5], axes)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b625f1a5-ef14-4533-912c-cc281e269641",
+ "metadata": {},
+ "source": [
+ "Let's now train a new model from this synthetic learning set.\n",
+ "The command-line interface of `lincs learn classification-model` accepts quite a few options.\n",
+ "Most of them set up the strategies used for the learning, as described further in our [user guide](https://mics-lab.github.io/lincs/user-guide.html).\n",
+ "When using the Python API, you have to create these strategies yourself:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "c552db53-b77d-4011-9298-6e2504e4093c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43)\n",
+ "profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)\n",
+ "weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)\n",
+ "profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)\n",
+ "breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)\n",
+ "termination_strategy = lc.TerminateAtAccuracy(learning_data, target_accuracy=len(learning_set.alternatives))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e9eb59f7-3c47-429f-857a-2c88b1d2deb6",
+ "metadata": {},
+ "source": [
+ "Then create the learning itself:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "df2de7ed-4a91-4dc9-b5b2-598a277c9a5a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "learning = lc.LearnMrsortByWeightsProfilesBreed(\n",
+ " learning_data,\n",
+ " profiles_initialization_strategy,\n",
+ " weights_optimization_strategy,\n",
+ " profiles_improvement_strategy,\n",
+ " breeding_strategy,\n",
+ " termination_strategy,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8848f232-beda-41d0-9ba7-0889f15e6216",
+ "metadata": {},
+ "source": [
+ "And `.perform` it to create the learned `Model` object:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "4f752442-f9ee-43ce-bd51-341b6e6a5081",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.339874953, 0.421424538]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.0556534864, 0.326433569]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.162616938, 0.67343241]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0.0878681168, 0.252649099]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [0, 1.01327896e-06, 0.999998987, 0]\n",
+ " - *coalitions\n"
+ ]
+ }
+ ],
+ "source": [
+ "learned_model = learning.perform()\n",
+ "learned_model.dump(problem, sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a6f694fa-f820-4869-bde6-d46ff90cf9b6",
+ "metadata": {},
+ "source": [
+ "Create a testing set and classify it, taking notes of the accuracy of the new model on that testing set:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "1fdb4fa6-f54e-4981-832c-f0175fc1c54b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(4, 2996)"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "testing_set = lc.generate_alternatives(problem, model, alternatives_count=3000, random_seed=44)\n",
+ "classification_result = lc.classify_alternatives(problem, learned_model, testing_set)\n",
+ "classification_result.changed, classification_result.unchanged"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "44f38224-0b66-42c3-becb-7b44e3924305",
+ "metadata": {},
+ "source": [
+ "This covers what was done in our \"Get started\" guide.\n",
+ "As you can see the Python API is more verbose, but for good reasons: it's more powerful as you'll see in the next section."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e04a3a27-824d-41c6-9d0f-3ded09b205a8",
+ "metadata": {},
+ "source": [
+ "## Do more, with the Python API"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3dd98560-201c-4634-9e9c-2c54d73ca0a8",
+ "metadata": {},
+ "source": [
+ "### Create classification objects"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8f4aa99e-35a7-449c-bd00-4bea102390fa",
+ "metadata": {},
+ "source": [
+ "You don't have to use our pseudo-random generation functions; you can create `Problem`, `Model`, *etc.* instances yourself."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e95efaf-8714-4868-b7e1-aaecf4c21885",
+ "metadata": {},
+ "source": [
+ "#### Create a `Problem`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "ed9ed7fd-6dcd-4b0f-95de-5118a266056d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: classification-problem\n",
+ "format_version: 1\n",
+ "criteria:\n",
+ " - name: Physics grade\n",
+ " value_type: integer\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 100\n",
+ " - name: Literature grade\n",
+ " value_type: enumerated\n",
+ " ordered_values: [f, e, d, c, b, a]\n",
+ "ordered_categories:\n",
+ " - name: Failed\n",
+ " - name: Passed\n",
+ " - name: Congratulations\n"
+ ]
+ }
+ ],
+ "source": [
+ "problem = lc.Problem(\n",
+ " criteria=[\n",
+ " lc.Criterion(\"Physics grade\", lc.Criterion.IntegerValues(lc.Criterion.PreferenceDirection.increasing, 0, 100)),\n",
+ " lc.Criterion(\"Literature grade\", lc.Criterion.EnumeratedValues([\"f\", \"e\", \"d\", \"c\", \"b\", \"a\"])),\n",
+ " ],\n",
+ " ordered_categories=[lc.Category(\"Failed\"), lc.Category(\"Passed\"), lc.Category(\"Congratulations\")],\n",
+ ")\n",
+ "\n",
+ "problem.dump(sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "60d24fd0-67ef-42ec-b76a-fa5729658e4d",
+ "metadata": {},
+ "source": [
+ "You can access all their attributes in code as well:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "a0dc62c9-9f5a-4ffb-98aa-cae20a56492e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "criterion = problem.criteria[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "0ae00d4a-4890-436b-8e81-b0f113e37f9b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Physics grade'"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "criterion.name"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "6f7e7b92-d111-47aa-bd3d-02837b1392e0",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.ValueType.integer, False, True, False)"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "criterion.value_type, criterion.is_real, criterion.is_integer, criterion.is_enumerated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "35d2c1c0-9836-479c-a440-2f68ab35d050",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "values = criterion.integer_values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "fb85c12f-5cb3-49cc-9778-c7f702cbc11a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.PreferenceDirection.isotone, True, False)"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "values.preference_direction, values.is_increasing, values.is_decreasing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "9c39cda6-5fa8-41c9-83fb-324148628d09",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0, 100)"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "values.min_value, values.max_value"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "706551fd-25aa-4cfd-8778-a556ce99fc36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "criterion = problem.criteria[1]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "13843008-4dcc-4da7-8a4d-73a3b449a8f1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Literature grade'"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "criterion.name"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "2b3f21b9-39d8-41ef-bfaa-76106f31834b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.ValueType.enumerated, False, False, True)"
+ ]
+ },
+ "execution_count": 25,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "criterion.value_type, criterion.is_real, criterion.is_integer, criterion.is_enumerated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "17b53592-8a71-4286-91da-0c92b941ad93",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "values = criterion.enumerated_values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "c71ea01f-0f6d-4a55-b462-9fafbf1013fa",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['f', 'e', 'd', 'c', 'b', 'a']"
+ ]
+ },
+ "execution_count": 27,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(values.ordered_values)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "3d2c5f72-04c4-4a2e-8df0-23551beef9f1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "5"
+ ]
+ },
+ "execution_count": 28,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "values.get_value_rank(value=\"a\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e74adcad-1a37-4628-8435-077c7587c9dd",
+ "metadata": {},
+ "source": [
+ "#### Create a `Model`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "8142b595-1213-42ad-bfbe-c38f5f9113d7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [50, 80]\n",
+ " - kind: thresholds\n",
+ " thresholds: [c, a]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [0.5, 0.5]\n",
+ " - *coalitions\n"
+ ]
+ }
+ ],
+ "source": [
+ "model = lc.Model(\n",
+ " problem,\n",
+ " accepted_values=[\n",
+ " lc.AcceptedValues(lc.AcceptedValues.IntegerThresholds([50, 80])),\n",
+ " lc.AcceptedValues(lc.AcceptedValues.EnumeratedThresholds([\"c\", \"a\"])),\n",
+ " ],\n",
+ " sufficient_coalitions=[\n",
+ " lc.SufficientCoalitions(lc.SufficientCoalitions.Weights([0.5, 0.5])),\n",
+ " lc.SufficientCoalitions(lc.SufficientCoalitions.Weights([0.5, 0.5])),\n",
+ " ],\n",
+ ")\n",
+ "\n",
+ "model.dump(problem, sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "id": "4d9286ed-1fde-42ea-854b-3ef3cd532f82",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "accepted = model.accepted_values[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "161f926c-f9e5-4bc7-9382-c069d10458d8",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.ValueType.integer, False, True, False)"
+ ]
+ },
+ "execution_count": 31,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "accepted.value_type, accepted.is_real, accepted.is_integer, accepted.is_enumerated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "c1298109-a0e9-4660-bd49-c325c0222d72",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.Kind.thresholds, True)"
+ ]
+ },
+ "execution_count": 32,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "accepted.kind, accepted.is_thresholds"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "id": "767b2049-aad2-45a1-9bfc-1e3f7273be46",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[50, 80]"
+ ]
+ },
+ "execution_count": 33,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(accepted.integer_thresholds.thresholds)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "id": "36256e92-7b2a-4131-b088-35f52dae6dd3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "accepted = model.accepted_values[1]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "id": "62c1aa27-cbc8-4032-9282-e1acf76ffa01",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.ValueType.enumerated, False, False, True)"
+ ]
+ },
+ "execution_count": 35,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "accepted.value_type, accepted.is_real, accepted.is_integer, accepted.is_enumerated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "id": "ffc03b33-b0cc-4211-b8f3-d0dbedc8131a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.Kind.thresholds, True)"
+ ]
+ },
+ "execution_count": 36,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "accepted.kind, accepted.is_thresholds"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "id": "26a4d2eb-5d2b-42a3-8348-da1d16b5303f",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['c', 'a']"
+ ]
+ },
+ "execution_count": 37,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(accepted.enumerated_thresholds.thresholds)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "id": "4690357c-2082-420e-b294-0010ef46f662",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sufficient = model.sufficient_coalitions[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "id": "bc0e16b6-ac03-4f81-9c03-79f6c6c91a8b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.Kind.weights, True, False)"
+ ]
+ },
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "sufficient.kind, sufficient.is_weights, sufficient.is_roots"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "id": "6fb81ac9-3f62-4f90-874f-de827d8d761a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0.5, 0.5]"
+ ]
+ },
+ "execution_count": 40,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(sufficient.weights.criterion_weights)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "34adef06-2a98-4278-976f-0bfbcdae399b",
+ "metadata": {},
+ "source": [
+ "#### Create (classified) `Alternatives`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "id": "44429b29-e039-46ab-b5ad-e2939a729406",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "alternatives = lc.Alternatives(problem, [\n",
+ " lc.Alternative(\n",
+ " \"Unclassified alternative\",\n",
+ " [\n",
+ " lc.Performance(lc.Performance.Integer(50)),\n",
+ " lc.Performance(lc.Performance.Enumerated(\"c\")),\n",
+ " ],\n",
+ " None\n",
+ " ),\n",
+ " lc.Alternative(\n",
+ " \"Classified alternative\",\n",
+ " [\n",
+ " lc.Performance(lc.Performance.Integer(90)),\n",
+ " lc.Performance(lc.Performance.Enumerated(\"a\")),\n",
+ " ],\n",
+ " 2\n",
+ " ),\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "id": "55a42ccb-27e8-46ea-89c5-3b2828cb34e8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "alternative = alternatives.alternatives[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "id": "2dfdf211-2120-4098-a84b-c3b71ac7b8c7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "True"
+ ]
+ },
+ "execution_count": 43,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "alternative.category_index is None"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "id": "3ddd8143-0374-4e5d-9d97-8673907062cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "performance = alternative.profile[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 45,
+ "id": "168d930a-f8e9-4777-b489-7e3214b2fc54",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(liblincs.ValueType.integer, False, True, False)"
+ ]
+ },
+ "execution_count": 45,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "performance.value_type, performance.is_real, performance.is_integer, performance.is_enumerated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 46,
+ "id": "77e19ca9-1fa5-4a4c-99b9-18be4b660596",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "50"
+ ]
+ },
+ "execution_count": 46,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "performance.integer.value"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "id": "95ee2b32-0210-4b77-a0b6-66812bcb1e45",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Congratulations'"
+ ]
+ },
+ "execution_count": 47,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "problem.ordered_categories[alternatives.alternatives[1].category_index].name"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "14c1121f-8cfc-4df8-ae73-1574970a382f",
+ "metadata": {},
+ "source": [
+ "### Clone classification objects"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "269d4ce1-75de-4107-b89d-3d86787fa6aa",
+ "metadata": {},
+ "source": [
+ "Just use [`copy.deepcopy`](https://docs.python.org/3/library/copy.html#copy.deepcopy):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "id": "b3927282-a8d3-4f91-959a-9c4d23e5f473",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import copy\n",
+ "\n",
+ "copied_problem = copy.deepcopy(problem)\n",
+ "copied_model = copy.deepcopy(model)\n",
+ "copied_alternatives = copy.deepcopy(alternatives)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b97e1f1-00b2-4274-9eb3-145cab40542d",
+ "metadata": {},
+ "source": [
+ "This is especially useful *e.g.* if you want to identify alternatives that are classified differently by two models, because `lc.classify_alternatives` mutates the alternatives: clone the `Alternatives`, classify the copy and iterate over the [`zip`](https://docs.python.org/3/library/functions.html#zip) of both `Alternatives`, comparing their `.category_index`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "97c52075-e6df-4763-ac72-7ea8e3adebe5",
+ "metadata": {},
+ "source": [
+ "### Serialize classification objects"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f04191af-4bce-4328-91e8-eb5292d26ebd",
+ "metadata": {},
+ "source": [
+ "#### In YAML and CSV like the command-line\n",
+ "\n",
+ "(and the upcomming C++ API)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "395bbfef-ce1a-4d56-81d5-fa9bcbbbd410",
+ "metadata": {},
+ "source": [
+ "Classification objects have a `.dump` method, and their classes have a static `.load` method that accept file-like objects.\n",
+ "\n",
+ "We've used them above to print classification objects to `sys.stdout`. Here is an example of how to use them with actual files:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "id": "b76a3046-5e1c-4ad8-9b00-4dc24486a4d9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open(\"problem.yml\", \"w\") as f:\n",
+ " problem.dump(f)\n",
+ "\n",
+ "with open(\"model.yml\", \"w\") as f:\n",
+ " model.dump(problem, f)\n",
+ "\n",
+ "with open(\"alternatives.csv\", \"w\") as f:\n",
+ " alternatives.dump(problem, f)\n",
+ "\n",
+ "with open(\"problem.yml\") as f:\n",
+ " problem = lc.Problem.load(f)\n",
+ "\n",
+ "with open(\"model.yml\") as f:\n",
+ " model = lc.Model.load(problem, f)\n",
+ "\n",
+ "with open(\"alternatives.csv\") as f:\n",
+ " alternatives = lc.Alternatives.load(problem, f)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "87932b00-021c-4389-87ed-690d1f30effb",
+ "metadata": {},
+ "source": [
+ "And here with in-memory [io](https://docs.python.org/3/library/io.html) objects:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 50,
+ "id": "8844542e-b8bf-4a6e-b370-828d6e05c3ab",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: classification-problem\n",
+ "format_version: 1\n",
+ "criteria:\n",
+ " - name: Physics grade\n",
+ " value_type: integer\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 100\n",
+ " - name: Literature grade\n",
+ " value_type: enumerated\n",
+ " ordered_values: [f, e, d, c, b, a]\n",
+ "ordered_categories:\n",
+ " - name: Failed\n",
+ " - name: Passed\n",
+ " - name: Congratulations\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "f = io.StringIO()\n",
+ "problem.dump(f)\n",
+ "s = f.getvalue()\n",
+ "print(s)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 51,
+ "id": "b5fe484c-7d80-40ea-9098-0b2776833510",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "f = io.StringIO(s)\n",
+ "problem = lc.Problem.load(f)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "784d7f86-0b02-4bd6-a4a9-280232b05fde",
+ "metadata": {},
+ "source": [
+ "#### Using the Python-specific `pickle` module"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "57f2a0dc-608d-4db8-b3b6-ac0b4d198a6d",
+ "metadata": {},
+ "source": [
+ "Classification objects simply support [pickling](https://docs.python.org/3/library/pickle.html) and unpickling. We recommend using the YAML and CSV formats whenever possible because they are not tied to the Python language (or the *lincs* library for that matter)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 52,
+ "id": "1e1b8f9d-c82d-4525-b094-13a55c445ff0",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: classification-problem\n",
+ "format_version: 1\n",
+ "criteria:\n",
+ " - name: Physics grade\n",
+ " value_type: integer\n",
+ " preference_direction: increasing\n",
+ " min_value: 0\n",
+ " max_value: 100\n",
+ " - name: Literature grade\n",
+ " value_type: enumerated\n",
+ " ordered_values: [f, e, d, c, b, a]\n",
+ "ordered_categories:\n",
+ " - name: Failed\n",
+ " - name: Passed\n",
+ " - name: Congratulations\n"
+ ]
+ }
+ ],
+ "source": [
+ "import pickle\n",
+ "\n",
+ "pickle.loads(pickle.dumps(problem)).dump(sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eba4a737-e844-44d3-9158-a63a7fdefc3d",
+ "metadata": {},
+ "source": [
+ "Note however that learning objects (*e.g.* instances of `LearnMrsortByWeightsProfilesBreed`) are *not* picklable."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c33628cc-d611-4ed3-954d-0b7bbc04c698",
+ "metadata": {},
+ "source": [
+ "### Customize the model visualization"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "132da858-692d-46b3-b7ad-f10c6bb744fd",
+ "metadata": {},
+ "source": [
+ "@todo(Documentation, v1.1) Write this section. Explain that `visualize_model` is written in terms of the public Python API and can be used as a base (*i.e.* copy-pasted)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e8556878-23a0-45d2-964f-f82f1a217b1d",
+ "metadata": {},
+ "source": [
+ "### Create your own learning strategies"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e5326968-1069-4c3f-b1a1-8823c7956e3b",
+ "metadata": {},
+ "source": [
+ "@todo(Documentation, v1.1) Introduce this section."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 53,
+ "id": "de3701e6-784a-4fb8-84cf-aa65ecc26d71",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "problem = lc.Problem(\n",
+ " [\n",
+ " lc.Criterion(\"Criterion 1\", lc.Criterion.RealValues(lc.Criterion.PreferenceDirection.decreasing, 0, 10)),\n",
+ " lc.Criterion(\"Criterion 2\", lc.Criterion.IntegerValues(lc.Criterion.PreferenceDirection.increasing, 0, 100)),\n",
+ " lc.Criterion(\"Criterion 3\", lc.Criterion.EnumeratedValues([\"F\", \"E\", \"D\", \"C\", \"B\", \"A\"])),\n",
+ " ],\n",
+ " [lc.Category(\"Bad\"), lc.Category(\"Medium\"), lc.Category(\"Good\")],\n",
+ ")\n",
+ "model = lc.generate_mrsort_model(problem, random_seed=42)\n",
+ "learning_set = lc.generate_alternatives(problem, model, alternatives_count=1000, random_seed=43)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "89022cc0-86d2-41ae-9492-60ce1623425f",
+ "metadata": {},
+ "source": [
+ "#### `LearningData`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6b3b861f-f16a-4574-9956-c6087482e302",
+ "metadata": {},
+ "source": [
+ "First, let's get more familiar with the `LearningData`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 54,
+ "id": "dec0c847-98ac-42f2-a2ee-1fd0a4c9ac0e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dc4244fd-f862-4ba9-8d6f-6e669c273530",
+ "metadata": {},
+ "source": [
+ "It contains two families of attributes."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d611a8bc-b4d7-4003-8d4d-f1d7835d3a4f",
+ "metadata": {},
+ "source": [
+ "##### Input data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6e40ad12-e93b-40a9-96a9-3b3e7c717152",
+ "metadata": {},
+ "source": [
+ "The first one is about the problem and learning set. These attributes never change. First, the counts:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 55,
+ "id": "5e6d7c88-af1e-474f-b631-8ff6d84ab60e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(3, 3, 2, 1000)"
+ ]
+ },
+ "execution_count": 55,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(learning_data.criteria_count, learning_data.categories_count, learning_data.boundaries_count, learning_data.alternatives_count)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a718bf27-98dc-4d60-a824-7baac5f607cb",
+ "metadata": {},
+ "source": [
+ "The learning set is pre-processed in the `LearningData` so that learning algorithms don't have to manipulate the different type of criterion values. In the `LearningData`, we keep only the ranks of the performances of each alternative in the learning set. The learning set is also destructured into a few arrays. Here are the attributes that describe this pre-processed learning set:\n",
+ "\n",
+ "The number of distinct values actually seen for each criterion (including the min and max values for numerical criteria):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 56,
+ "id": "c2531e9f-a957-4903-9e77-ad7ee513ca63",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1002, 101, 6]"
+ ]
+ },
+ "execution_count": 56,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(learning_data.values_counts) # Indexed by [criterion_index]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "id": "b5f7c642-475b-4986-b0f1-0168fa169476",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(1002, 6)"
+ ]
+ },
+ "execution_count": 57,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(learning_data.values_counts[0], learning_data.values_counts[learning_data.criteria_count - 1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0eb4ea9-f857-479e-b1c9-a87c44e151bf",
+ "metadata": {},
+ "source": [
+ "We see that the learning data knows 1002 values for the real-valued criterion. This is usual as it's rare that two floating point values are exactly equal, so the 1000 alternatives have distinct values, and the min and max are two more values. The learning data contains 101 values for the integer-valued criterion, meaning that the alternatives in the learning set do actually cover the whole set of possible values. And similarly, 6 values for the enumerated criterion.\n",
+ "\n",
+ "For each criterion, the ranks of the performance of each alternative:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
+ "id": "085a36fa-7c82-43da-aa29-1687b693dcb2",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[[883, 900, 753, 216, 365, 410, 302, 852, 738, 45, '...'],\n",
+ " [50, 13, 17, 86, 4, 2, 25, 81, 47, 87, '...'],\n",
+ " [3, 0, 1, 0, 3, 3, 2, 0, 0, 3, '...']]"
+ ]
+ },
+ "execution_count": 58,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[list(v)[:10] + ['...'] for v in learning_data.performance_ranks] # Indexed by [criterion_index][alternative_index]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 59,
+ "id": "e416f0d9-8077-4215-852b-1e01308f9c5d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(883, 3)"
+ ]
+ },
+ "execution_count": 59,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(learning_data.performance_ranks[0][0], learning_data.performance_ranks[learning_data.criteria_count - 1][learning_data.alternatives_count - 1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "544df6c0-be0c-41b7-ac0e-11a31c5b7be8",
+ "metadata": {},
+ "source": [
+ "The assignment of each alternative, *i.e.* the index of its category:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 60,
+ "id": "26d14839-e77c-4c6b-9957-86d8c8f2d4b4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[2, 2, 2, 0, 1, 1, 1, 2, 2, 0, '...']"
+ ]
+ },
+ "execution_count": 60,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(learning_data.assignments)[:10] + ['...'] # Indexed by [alternative_index]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 61,
+ "id": "16d76309-1578-45ed-bf65-1559545e1908",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(2, 2)"
+ ]
+ },
+ "execution_count": 61,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(learning_data.assignments[0], learning_data.assignments[learning_data.alternatives_count - 1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bf4de4df-f675-44e1-a908-a3554dd08083",
+ "metadata": {},
+ "source": [
+ "##### In-progress data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bbeccc8f-4b25-4029-a5fb-76d6257c4df7",
+ "metadata": {},
+ "source": [
+ "The second family of attributes is about the WeightsProfilesBreed algorithm itself.\n",
+ "\n",
+ "The `LearningData` contains several \"in progress\" models. Their number is constant:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 62,
+ "id": "e4ca254f-3629-4b3d-9811-745d054fc344",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "9"
+ ]
+ },
+ "execution_count": 62,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "learning_data.models_count"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9807dc89-c81b-4e87-8671-07bce5a79edb",
+ "metadata": {},
+ "source": [
+ "Each model comes with a uniform random bits generator (URBG for short):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 63,
+ "id": "cbd965fe-19b5-41b5-8428-17b59b5c9d3b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['',\n",
+ " '',\n",
+ " '',\n",
+ " '',\n",
+ " '',\n",
+ " '',\n",
+ " '',\n",
+ " '',\n",
+ " '']"
+ ]
+ },
+ "execution_count": 63,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[str(urbg)[:43] + '>' for urbg in learning_data.urbgs] # Indexed by [model_index]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b6e0e786-1a6f-4b1f-b173-c91398d0e3f4",
+ "metadata": {},
+ "source": [
+ "This lets heuristic strategies operate in parallel on models and still produce deterministic results. URBGs are callable to get the next pseudo-random integer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 64,
+ "id": "79f1d0ca-68b0-4d2b-aabb-5191cac23c74",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[494155588,\n",
+ " 870190228,\n",
+ " 2450414687,\n",
+ " 641676058,\n",
+ " 661088198,\n",
+ " 363471047,\n",
+ " 1448606581,\n",
+ " 1348129397,\n",
+ " 2542538607]"
+ ]
+ },
+ "execution_count": 64,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[r() for r in learning_data.urbgs]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "485e3ba4-f926-402a-b4ae-32363861a132",
+ "metadata": {},
+ "source": [
+ "WPB learning is iterative, and the `iteration_index` is stored in the learning data. It starts at zero and tells you the current iteration:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 65,
+ "id": "aa17bb83-d47f-42bc-a0d1-da6d201d4ad2",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0"
+ ]
+ },
+ "execution_count": 65,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "learning_data.iteration_index"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "acaa99a2-e744-4063-8d2e-90aa24bc92b9",
+ "metadata": {},
+ "source": [
+ "The remaining attributes are modified at each iteration, and start uninitialized. For this presentation, we'll first run one iteration of WPB so that their values make sense."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "id": "72a72efe-2404-4f92-8ffc-cee6fbbeced1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43)\n",
+ "profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)\n",
+ "weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)\n",
+ "profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)\n",
+ "breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)\n",
+ "termination_strategy = lc.TerminateAfterIterations(learning_data, max_iterations_count=1)\n",
+ "\n",
+ "lc.LearnMrsortByWeightsProfilesBreed(\n",
+ " learning_data,\n",
+ " profiles_initialization_strategy,\n",
+ " weights_optimization_strategy,\n",
+ " profiles_improvement_strategy,\n",
+ " breeding_strategy,\n",
+ " termination_strategy,\n",
+ ").perform()\n",
+ "\n",
+ "assert(learning_data.iteration_index == 0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b3a8e44f-ced5-4e34-bdbe-0cd33a854348",
+ "metadata": {},
+ "source": [
+ "Its `model_indexes` contains indexes of models in increasing order of accuracy."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 67,
+ "id": "e9f9a2e9-dbc3-444a-a0f3-4b090dd46ca5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1, 5, 4, 8, 3, 6, 2, 0, 7]"
+ ]
+ },
+ "execution_count": 67,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(learning_data.model_indexes)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b7f78a52-a965-4872-b6cf-a6fb45dd597d",
+ "metadata": {},
+ "source": [
+ "Its `accuracies` attribute holds the accuracy of each model. They are stored as the count of correctly-classified alternatives, between 0 and `alternatives_count` included:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 68,
+ "id": "504796aa-7aa1-424f-a458-e9e1df7c550f",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[728, 593, 725, 671, 637, 609, 676, 833, 640]"
+ ]
+ },
+ "execution_count": 68,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "list(learning_data.accuracies) # Indexed by model_index"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b5b5d24e-d8f0-45e0-adda-c4978a943872",
+ "metadata": {},
+ "source": [
+ "If you iterate the `accuracies` in the order of `model_indexes`, they are sorted:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 69,
+ "id": "dccb02f0-4eff-4452-b7f4-15d71b5db16a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "True"
+ ]
+ },
+ "execution_count": 69,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[learning_data.accuracies[index] for index in learning_data.model_indexes] == sorted(learning_data.accuracies)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d80a063d-1456-4ae6-a57a-96a28bfda3a1",
+ "metadata": {},
+ "source": [
+ "@todo(Documentation, v1.1) Introduce profiles"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 70,
+ "id": "85b36a2f-39c0-45df-8648-083d0a85071c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[[[124, 9, 0], [633, 9, 3]],\n",
+ " [[492, 8, 1], [965, 8, 4]],\n",
+ " [[268, 15, 0], [506, 54, 3]],\n",
+ " [[230, 8, 1], [272, 26, 4]],\n",
+ " [[201, 1, 1], [201, 2, 3]],\n",
+ " [[86, 64, 0], [86, 99, 0]],\n",
+ " [[223, 60, 2], [310, 80, 5]],\n",
+ " [[235, 20, 2], [595, 20, 3]],\n",
+ " [[261, 52, 0], [262, 52, 3]]]"
+ ]
+ },
+ "execution_count": 70,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[[list(vv) for vv in v] for v in learning_data.profile_ranks] # Indexed by [model_index][boundary_index][criterion_index]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fc0441fa-244f-4ed3-ad57-cf8cbea3ecf4",
+ "metadata": {},
+ "source": [
+ "@todo(Documentation, v1.1) Introduce weights"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 71,
+ "id": "382c323a-f9ac-437a-8a28-3472ca341efa",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[[1.0132789611816406e-06, 0.9999989867210388, 1.0132789611816406e-06],\n",
+ " [0.9999989867210388, 0.9999989867210388, 1.0132789611816406e-06],\n",
+ " [1.0, 0.0, 0.9999989867210388],\n",
+ " [0.5000004768371582, 0.4999994933605194, 0.4999994933605194],\n",
+ " [1.0132789611816406e-06, 0.9999989867210388, 0.0],\n",
+ " [0.9999989867210388, 0.0, 1.0132789611816406e-06],\n",
+ " [1.0, 0.0, 0.0],\n",
+ " [1.0, 0.9999989867210388, 1.0132789611816406e-06],\n",
+ " [0.9999989867210388, 0.0, 0.9999989867210388]]"
+ ]
+ },
+ "execution_count": 71,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "[list(v) for v in learning_data.weights] # Indexed by [model_index][criterion_index]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6b4697a0-1ef4-4702-a261-fd909c29f08a",
+ "metadata": {},
+ "source": [
+ "#### `Observer`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "99070e74-7640-4e05-a0e0-8066f304eb83",
+ "metadata": {},
+ "source": [
+ "With this better understanding of `LearningData`, let's write our own `Observer` strategy. It's arguably the simplest to starts with, because it's not expected to *change* the `LearningData`.\n",
+ "\n",
+ "To start as simple as possible, lets reproduce the behavior of the `--...-verbose` flag on the command line, by creating an observer that just prints the best accuracy at each step.\n",
+ "\n",
+ "`Observer` strategies must define two methods to be called by the learning algorithm: `after_iteration`, to be called at the end of each iteration, after the breeding of models is done, and `before_return`, to be called just before the final model is returned."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "id": "11b11239-c0fb-4755-a61f-83e4a9a57494",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class VerboseObserver(lc.LearnMrsortByWeightsProfilesBreed.Observer):\n",
+ " def __init__(self, learning_data):\n",
+ " super().__init__()\n",
+ " self.learning_data = learning_data\n",
+ "\n",
+ " def after_iteration(self):\n",
+ " print(f\"Best accuracy (after {self.learning_data.iteration_index + 1} iterations): {self.learning_data.get_best_accuracy()}\")\n",
+ "\n",
+ " def before_return(self):\n",
+ " print(f\"Final accuracy (after {self.learning_data.iteration_index + 1} iterations): {self.learning_data.get_best_accuracy()}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f69b75d0-3a80-4c80-b7aa-c2ffe3b58973",
+ "metadata": {},
+ "source": [
+ "We can now pass it to a learning and perform that learning to observe its effects:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 73,
+ "id": "8925d4ec-5716-428b-b895-9a34be3216ce",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Best accuracy (after 1 iterations): 938\n",
+ "Best accuracy (after 2 iterations): 992\n",
+ "Best accuracy (after 3 iterations): 997\n",
+ "Best accuracy (after 4 iterations): 997\n",
+ "Best accuracy (after 5 iterations): 997\n",
+ "Best accuracy (after 6 iterations): 997\n",
+ "Best accuracy (after 7 iterations): 997\n",
+ "Best accuracy (after 8 iterations): 999\n",
+ "Final accuracy (after 9 iterations): 1000\n"
+ ]
+ }
+ ],
+ "source": [
+ "profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)\n",
+ "weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)\n",
+ "profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)\n",
+ "breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)\n",
+ "termination_strategy = lc.TerminateAtAccuracy(learning_data, target_accuracy=len(learning_set.alternatives))\n",
+ "observer = VerboseObserver(learning_data)\n",
+ "\n",
+ "learned_model = lc.LearnMrsortByWeightsProfilesBreed(\n",
+ " learning_data,\n",
+ " profiles_initialization_strategy,\n",
+ " weights_optimization_strategy,\n",
+ " profiles_improvement_strategy,\n",
+ " breeding_strategy,\n",
+ " termination_strategy,\n",
+ " [observer],\n",
+ ").perform()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a558c178-0ba7-49fe-b7c0-95a89e13ff0b",
+ "metadata": {},
+ "source": [
+ "Now let's do something slightly more complicated: our goal for `IntermediatesObserver` is to keep track of the best model so far at different times during the learning. Specifically, we want to keep the models at iterations 1, 2, 4, 8, *etc.*."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 74,
+ "id": "b953eeba-6185-4432-b6ae-a262d88a635c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [7.7909708, 4.06594753]\n",
+ " - kind: thresholds\n",
+ " thresholds: [20, 20]\n",
+ " - kind: thresholds\n",
+ " thresholds: [D, C]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [1, 0.999998987, 1.01327896e-06]\n",
+ " - *coalitions\n",
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [7.95116329, 3.89878368]\n",
+ " - kind: thresholds\n",
+ " thresholds: [0, 21]\n",
+ " - kind: thresholds\n",
+ " thresholds: [C, B]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [1, 0, 1]\n",
+ " - *coalitions\n",
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [7.96338844, 3.82566905]\n",
+ " - kind: thresholds\n",
+ " thresholds: [73, 84]\n",
+ " - kind: thresholds\n",
+ " thresholds: [B, B]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [1, 0, 1]\n",
+ " - *coalitions\n",
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [7.96338844, 3.74707603]\n",
+ " - kind: thresholds\n",
+ " thresholds: [94, 99]\n",
+ " - kind: thresholds\n",
+ " thresholds: [B, B]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [1, 0, 1]\n",
+ " - *coalitions\n",
+ "kind: ncs-classification-model\n",
+ "format_version: 1\n",
+ "accepted_values:\n",
+ " - kind: thresholds\n",
+ " thresholds: [7.95116329, 3.74707603]\n",
+ " - kind: thresholds\n",
+ " thresholds: [94, 99]\n",
+ " - kind: thresholds\n",
+ " thresholds: [B, B]\n",
+ "sufficient_coalitions:\n",
+ " - &coalitions\n",
+ " kind: weights\n",
+ " criterion_weights: [1, 0, 1]\n",
+ " - *coalitions\n"
+ ]
+ }
+ ],
+ "source": [
+ "import math\n",
+ "\n",
+ "class IntermediatesObserver(lc.LearnMrsortByWeightsProfilesBreed.Observer):\n",
+ " def __init__(self, problem, learning_data):\n",
+ " super().__init__()\n",
+ " self.problem = problem\n",
+ " self.learning_data = learning_data\n",
+ " self.intermediate_models = []\n",
+ "\n",
+ " def after_iteration(self):\n",
+ " if math.log2(self.learning_data.iteration_index + 1).is_integer():\n",
+ " self.intermediate_models.append(self.learning_data.get_best_model())\n",
+ "\n",
+ " def before_return(self):\n",
+ " pass\n",
+ "\n",
+ "learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43) # Do *not* reuse the same `LearningData` for several learnings\n",
+ "profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)\n",
+ "weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)\n",
+ "profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)\n",
+ "breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)\n",
+ "termination_strategy = lc.TerminateAtAccuracy(learning_data, target_accuracy=len(learning_set.alternatives))\n",
+ "observer = IntermediatesObserver(problem, learning_data)\n",
+ "\n",
+ "final_model = lc.LearnMrsortByWeightsProfilesBreed(\n",
+ " learning_data,\n",
+ " profiles_initialization_strategy,\n",
+ " weights_optimization_strategy,\n",
+ " profiles_improvement_strategy,\n",
+ " breeding_strategy,\n",
+ " termination_strategy,\n",
+ " [observer],\n",
+ ").perform()\n",
+ "\n",
+ "for model in observer.intermediate_models:\n",
+ " model.dump(problem, sys.stdout)\n",
+ "final_model.dump(problem, sys.stdout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "76bbaf16-0934-4cb2-b969-8a4f3f8cdf3e",
+ "metadata": {},
+ "source": [
+ "#### Other strategies"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d3754888-d110-494f-9bb5-846cd749eb5a",
+ "metadata": {},
+ "source": [
+ "@todo(Documentation, v1.1) Write this section"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 75,
+ "id": "2182489b-a451-4e97-9045-0d65af27e0f1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class SillyWeightsStrategy(lc.LearnMrsortByWeightsProfilesBreed.WeightsOptimizationStrategy):\n",
+ " pass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 76,
+ "id": "418ed6c6-2a36-438f-be7e-e47368523bf4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class SillyProfilesStrategy(lc.LearnMrsortByWeightsProfilesBreed.ProfilesImprovementStrategy):\n",
+ " pass"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/_sources/changelog.rst.txt b/docs/_sources/changelog.rst.txt
index 7cf43e09..9de05d67 100644
--- a/docs/_sources/changelog.rst.txt
+++ b/docs/_sources/changelog.rst.txt
@@ -4,6 +4,23 @@
Changelog
=========
+Version 1.1.0a6 (2024-02-06)
+============================
+
+- **Breaking** Rename ``max_iteration_index`` to ``max_iterations_count`` in ``...TerminateAfterIterations.__init__``
+- Make the Python API guide Jupyter Notebook downloadable
+- Expose all attributes of ``WPB.LearningData``; start to document them in our Python API guide
+- Expose parameters of EvalMaxSAT in our API and command-line interface (see ``lincs learn classification-model --help``):
+
+ - ``--ucncs.max-sat-by-separation.solver`` (for consistency, always ``"eval-max-sat"`` for now)
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads``
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize``
+ - ``--ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time``
+ - ``--ucncs.max-sat-by-coalitions.solver`` (for consistency, always ``"eval-max-sat"`` for now)
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.nb-minimize-threads``
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.timeout-fast-minimize``
+ - ``--ucncs.max-sat-by-coalitions.eval-max-sat.coef-minimize-time``
+
Version 1.1.0a5 (2024-02-01)
============================
diff --git a/docs/_sources/get-started.rst.txt b/docs/_sources/get-started.rst.txt
index 39c40b47..56f31a55 100644
--- a/docs/_sources/get-started.rst.txt
+++ b/docs/_sources/get-started.rst.txt
@@ -75,7 +75,7 @@ The generated ``problem.yml`` should look like:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a5): lincs generate classification-problem 4 3 --random-seed 40
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classification-problem 4 3 --random-seed 40
kind: classification-problem
format_version: 1
criteria:
@@ -146,7 +146,7 @@ It should look like:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a5): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classification-model problem.yml --random-seed 41 --model-type mrsort
kind: ncs-classification-model
format_version: 1
accepted_values:
@@ -219,7 +219,7 @@ It should start with something like this, and contain 1000 alternatives:
.. code:: text
- # Reproduction command (with lincs version 1.1.0a5): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0
+ # Reproduction command (with lincs version 1.1.0a6): lincs generate classified-alternatives problem.yml model.yml 1000 --random-seed 42 --misclassified-count 0
name,"Criterion 1","Criterion 2","Criterion 3","Criterion 4",category
"Alternative 1",0.37454012,0.796543002,0.95071429,0.183434784,"Best category"
"Alternative 2",0.731993914,0.779690981,0.598658502,0.596850157,"Intermediate category 1"
@@ -255,7 +255,7 @@ so it is numerically different:
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a5): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0
+ # Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type mrsort --mrsort.strategy weights-profiles-breed --mrsort.weights-profiles-breed.models-count 9 --mrsort.weights-profiles-breed.accuracy-heuristic.random-seed 43 --mrsort.weights-profiles-breed.initialization-strategy maximize-discrimination-per-criterion --mrsort.weights-profiles-breed.weights-strategy linear-program --mrsort.weights-profiles-breed.linear-program.solver glop --mrsort.weights-profiles-breed.profiles-strategy accuracy-heuristic --mrsort.weights-profiles-breed.accuracy-heuristic.processor cpu --mrsort.weights-profiles-breed.breed-strategy reinitialize-least-accurate --mrsort.weights-profiles-breed.reinitialize-least-accurate.portion 0.5 --mrsort.weights-profiles-breed.target-accuracy 1.0
kind: ncs-classification-model
format_version: 1
accepted_values:
diff --git a/docs/_sources/python-api.md.txt b/docs/_sources/python-api.md.txt
index 279d8633..fa1d042e 100644
--- a/docs/_sources/python-api.md.txt
+++ b/docs/_sources/python-api.md.txt
@@ -5,6 +5,8 @@
This document builds up on {doc}`our "Get Started" guide ` and our {doc}`user guide `, and introduces *lincs*' Python API.
This API is more flexible, albeit more complex, than the command-line interface you've been using so far.
+If you're a Jupyter user, you can [download the notebook](python-api/python-api.ipynb) this document is based on.
+
## Do it again, in Python
First, lets do exactly the same thing as in our "Get started" guide, but using the Python API.
@@ -166,7 +168,7 @@ Dump it (in memory instead of on `sys.stdout` to print only the first few lines)
import io
f = io.StringIO()
learning_set.dump(problem, f)
-print("\n".join(f.getvalue().splitlines()[:6]))
+print("\n".join(f.getvalue().splitlines()[:6] + ['...']))
```
```text
@@ -176,6 +178,7 @@ name,"Criterion 1","Criterion 2","Criterion 3","Criterion 4",category
"Alternative 3",0.156018645,0.445832759,0.15599452,0.0999749228,"Worst category"
"Alternative 4",0.0580836125,0.4592489,0.866176128,0.333708614,"Best category"
"Alternative 5",0.601114988,0.14286682,0.708072603,0.650888503,"Intermediate category 1"
+...
```
@@ -817,4 +820,536 @@ Note however that learning objects (*e.g.* instances of `LearnMrsortByWeightsPro
### Create your own learning strategies
-@todo(Documentation, v1.1) Write this section. Include a demo of how to create a custom `Observer` that keeps the best model at iterations 1, 2, 4, *etc.*
+@todo(Documentation, v1.1) Introduce this section.
+
+
+```python
+problem = lc.Problem(
+ [
+ lc.Criterion("Criterion 1", lc.Criterion.RealValues(lc.Criterion.PreferenceDirection.decreasing, 0, 10)),
+ lc.Criterion("Criterion 2", lc.Criterion.IntegerValues(lc.Criterion.PreferenceDirection.increasing, 0, 100)),
+ lc.Criterion("Criterion 3", lc.Criterion.EnumeratedValues(["F", "E", "D", "C", "B", "A"])),
+ ],
+ [lc.Category("Bad"), lc.Category("Medium"), lc.Category("Good")],
+)
+model = lc.generate_mrsort_model(problem, random_seed=42)
+learning_set = lc.generate_alternatives(problem, model, alternatives_count=1000, random_seed=43)
+```
+
+#### `LearningData`
+
+First, let's get more familiar with the `LearningData`:
+
+
+```python
+learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43)
+```
+
+It contains two families of attributes.
+
+##### Input data
+
+The first one is about the problem and learning set. These attributes never change. First, the counts:
+
+
+```python
+(learning_data.criteria_count, learning_data.categories_count, learning_data.boundaries_count, learning_data.alternatives_count)
+```
+
+
+
+
+```text
+(3, 3, 2, 1000)
+```
+
+
+
+The learning set is pre-processed in the `LearningData` so that learning algorithms don't have to manipulate the different type of criterion values. In the `LearningData`, we keep only the ranks of the performances of each alternative in the learning set. The learning set is also destructured into a few arrays. Here are the attributes that describe this pre-processed learning set:
+
+The number of distinct values actually seen for each criterion (including the min and max values for numerical criteria):
+
+
+```python
+list(learning_data.values_counts) # Indexed by [criterion_index]
+```
+
+
+
+
+```text
+[1002, 101, 6]
+```
+
+
+
+
+```python
+(learning_data.values_counts[0], learning_data.values_counts[learning_data.criteria_count - 1])
+```
+
+
+
+
+```text
+(1002, 6)
+```
+
+
+
+We see that the learning data knows 1002 values for the real-valued criterion. This is usual as it's rare that two floating point values are exactly equal, so the 1000 alternatives have distinct values, and the min and max are two more values. The learning data contains 101 values for the integer-valued criterion, meaning that the alternatives in the learning set do actually cover the whole set of possible values. And similarly, 6 values for the enumerated criterion.
+
+For each criterion, the ranks of the performance of each alternative:
+
+
+```python
+[list(v)[:10] + ['...'] for v in learning_data.performance_ranks] # Indexed by [criterion_index][alternative_index]
+```
+
+
+
+
+```text
+[[883, 900, 753, 216, 365, 410, 302, 852, 738, 45, '...'],
+ [50, 13, 17, 86, 4, 2, 25, 81, 47, 87, '...'],
+ [3, 0, 1, 0, 3, 3, 2, 0, 0, 3, '...']]
+```
+
+
+
+
+```python
+(learning_data.performance_ranks[0][0], learning_data.performance_ranks[learning_data.criteria_count - 1][learning_data.alternatives_count - 1])
+```
+
+
+
+
+```text
+(883, 3)
+```
+
+
+
+The assignment of each alternative, *i.e.* the index of its category:
+
+
+```python
+list(learning_data.assignments)[:10] + ['...'] # Indexed by [alternative_index]
+```
+
+
+
+
+```text
+[2, 2, 2, 0, 1, 1, 1, 2, 2, 0, '...']
+```
+
+
+
+
+```python
+(learning_data.assignments[0], learning_data.assignments[learning_data.alternatives_count - 1])
+```
+
+
+
+
+```text
+(2, 2)
+```
+
+
+
+##### In-progress data
+
+The second family of attributes is about the WeightsProfilesBreed algorithm itself.
+
+The `LearningData` contains several "in progress" models. Their number is constant:
+
+
+```python
+learning_data.models_count
+```
+
+
+
+
+```text
+9
+```
+
+
+
+Each model comes with a uniform random bits generator (URBG for short):
+
+
+```python
+[str(urbg)[:43] + '>' for urbg in learning_data.urbgs] # Indexed by [model_index]
+```
+
+
+
+
+```text
+['',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '']
+```
+
+
+
+This lets heuristic strategies operate in parallel on models and still produce deterministic results. URBGs are callable to get the next pseudo-random integer:
+
+
+```python
+[r() for r in learning_data.urbgs]
+```
+
+
+
+
+```text
+[494155588,
+ 870190228,
+ 2450414687,
+ 641676058,
+ 661088198,
+ 363471047,
+ 1448606581,
+ 1348129397,
+ 2542538607]
+```
+
+
+
+WPB learning is iterative, and the `iteration_index` is stored in the learning data. It starts at zero and tells you the current iteration:
+
+
+```python
+learning_data.iteration_index
+```
+
+
+
+
+```text
+0
+```
+
+
+
+The remaining attributes are modified at each iteration, and start uninitialized. For this presentation, we'll first run one iteration of WPB so that their values make sense.
+
+
+```python
+learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43)
+profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)
+weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)
+profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)
+breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)
+termination_strategy = lc.TerminateAfterIterations(learning_data, max_iterations_count=1)
+
+lc.LearnMrsortByWeightsProfilesBreed(
+ learning_data,
+ profiles_initialization_strategy,
+ weights_optimization_strategy,
+ profiles_improvement_strategy,
+ breeding_strategy,
+ termination_strategy,
+).perform()
+
+assert(learning_data.iteration_index == 0)
+```
+
+Its `model_indexes` contains indexes of models in increasing order of accuracy.
+
+
+```python
+list(learning_data.model_indexes)
+```
+
+
+
+
+```text
+[1, 5, 4, 8, 3, 6, 2, 0, 7]
+```
+
+
+
+Its `accuracies` attribute holds the accuracy of each model. They are stored as the count of correctly-classified alternatives, between 0 and `alternatives_count` included:
+
+
+```python
+list(learning_data.accuracies) # Indexed by model_index
+```
+
+
+
+
+```text
+[728, 593, 725, 671, 637, 609, 676, 833, 640]
+```
+
+
+
+If you iterate the `accuracies` in the order of `model_indexes`, they are sorted:
+
+
+```python
+[learning_data.accuracies[index] for index in learning_data.model_indexes] == sorted(learning_data.accuracies)
+```
+
+
+
+
+```text
+True
+```
+
+
+
+@todo(Documentation, v1.1) Introduce profiles
+
+
+```python
+[[list(vv) for vv in v] for v in learning_data.profile_ranks] # Indexed by [model_index][boundary_index][criterion_index]
+```
+
+
+
+
+```text
+[[[124, 9, 0], [633, 9, 3]],
+ [[492, 8, 1], [965, 8, 4]],
+ [[268, 15, 0], [506, 54, 3]],
+ [[230, 8, 1], [272, 26, 4]],
+ [[201, 1, 1], [201, 2, 3]],
+ [[86, 64, 0], [86, 99, 0]],
+ [[223, 60, 2], [310, 80, 5]],
+ [[235, 20, 2], [595, 20, 3]],
+ [[261, 52, 0], [262, 52, 3]]]
+```
+
+
+
+@todo(Documentation, v1.1) Introduce weights
+
+
+```python
+[list(v) for v in learning_data.weights] # Indexed by [model_index][criterion_index]
+```
+
+
+
+
+```text
+[[1.0132789611816406e-06, 0.9999989867210388, 1.0132789611816406e-06],
+ [0.9999989867210388, 0.9999989867210388, 1.0132789611816406e-06],
+ [1.0, 0.0, 0.9999989867210388],
+ [0.5000004768371582, 0.4999994933605194, 0.4999994933605194],
+ [1.0132789611816406e-06, 0.9999989867210388, 0.0],
+ [0.9999989867210388, 0.0, 1.0132789611816406e-06],
+ [1.0, 0.0, 0.0],
+ [1.0, 0.9999989867210388, 1.0132789611816406e-06],
+ [0.9999989867210388, 0.0, 0.9999989867210388]]
+```
+
+
+
+#### `Observer`
+
+With this better understanding of `LearningData`, let's write our own `Observer` strategy. It's arguably the simplest to starts with, because it's not expected to *change* the `LearningData`.
+
+To start as simple as possible, lets reproduce the behavior of the `--...-verbose` flag on the command line, by creating an observer that just prints the best accuracy at each step.
+
+`Observer` strategies must define two methods to be called by the learning algorithm: `after_iteration`, to be called at the end of each iteration, after the breeding of models is done, and `before_return`, to be called just before the final model is returned.
+
+
+```python
+class VerboseObserver(lc.LearnMrsortByWeightsProfilesBreed.Observer):
+ def __init__(self, learning_data):
+ super().__init__()
+ self.learning_data = learning_data
+
+ def after_iteration(self):
+ print(f"Best accuracy (after {self.learning_data.iteration_index + 1} iterations): {self.learning_data.get_best_accuracy()}")
+
+ def before_return(self):
+ print(f"Final accuracy (after {self.learning_data.iteration_index + 1} iterations): {self.learning_data.get_best_accuracy()}")
+```
+
+We can now pass it to a learning and perform that learning to observe its effects:
+
+
+```python
+profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)
+weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)
+profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)
+breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)
+termination_strategy = lc.TerminateAtAccuracy(learning_data, target_accuracy=len(learning_set.alternatives))
+observer = VerboseObserver(learning_data)
+
+learned_model = lc.LearnMrsortByWeightsProfilesBreed(
+ learning_data,
+ profiles_initialization_strategy,
+ weights_optimization_strategy,
+ profiles_improvement_strategy,
+ breeding_strategy,
+ termination_strategy,
+ [observer],
+).perform()
+```
+
+```text
+Best accuracy (after 1 iterations): 938
+Best accuracy (after 2 iterations): 992
+Best accuracy (after 3 iterations): 997
+Best accuracy (after 4 iterations): 997
+Best accuracy (after 5 iterations): 997
+Best accuracy (after 6 iterations): 997
+Best accuracy (after 7 iterations): 997
+Best accuracy (after 8 iterations): 999
+Final accuracy (after 9 iterations): 1000
+```
+
+
+Now let's do something slightly more complicated: our goal for `IntermediatesObserver` is to keep track of the best model so far at different times during the learning. Specifically, we want to keep the models at iterations 1, 2, 4, 8, *etc.*.
+
+
+```python
+import math
+
+class IntermediatesObserver(lc.LearnMrsortByWeightsProfilesBreed.Observer):
+ def __init__(self, problem, learning_data):
+ super().__init__()
+ self.problem = problem
+ self.learning_data = learning_data
+ self.intermediate_models = []
+
+ def after_iteration(self):
+ if math.log2(self.learning_data.iteration_index + 1).is_integer():
+ self.intermediate_models.append(self.learning_data.get_best_model())
+
+ def before_return(self):
+ pass
+
+learning_data = lc.LearnMrsortByWeightsProfilesBreed.LearningData(problem, learning_set, models_count=9, random_seed=43) # Do *not* reuse the same `LearningData` for several learnings
+profiles_initialization_strategy = lc.InitializeProfilesForProbabilisticMaximalDiscriminationPowerPerCriterion(learning_data)
+weights_optimization_strategy = lc.OptimizeWeightsUsingGlop(learning_data)
+profiles_improvement_strategy = lc.ImproveProfilesWithAccuracyHeuristicOnCpu(learning_data)
+breeding_strategy = lc.ReinitializeLeastAccurate(learning_data, profiles_initialization_strategy=profiles_initialization_strategy, count=4)
+termination_strategy = lc.TerminateAtAccuracy(learning_data, target_accuracy=len(learning_set.alternatives))
+observer = IntermediatesObserver(problem, learning_data)
+
+final_model = lc.LearnMrsortByWeightsProfilesBreed(
+ learning_data,
+ profiles_initialization_strategy,
+ weights_optimization_strategy,
+ profiles_improvement_strategy,
+ breeding_strategy,
+ termination_strategy,
+ [observer],
+).perform()
+
+for model in observer.intermediate_models:
+ model.dump(problem, sys.stdout)
+final_model.dump(problem, sys.stdout)
+```
+
+```yaml
+kind: ncs-classification-model
+format_version: 1
+accepted_values:
+ - kind: thresholds
+ thresholds: [7.7909708, 4.06594753]
+ - kind: thresholds
+ thresholds: [20, 20]
+ - kind: thresholds
+ thresholds: [D, C]
+sufficient_coalitions:
+ - &coalitions
+ kind: weights
+ criterion_weights: [1, 0.999998987, 1.01327896e-06]
+ - *coalitions
+kind: ncs-classification-model
+format_version: 1
+accepted_values:
+ - kind: thresholds
+ thresholds: [7.95116329, 3.89878368]
+ - kind: thresholds
+ thresholds: [0, 21]
+ - kind: thresholds
+ thresholds: [C, B]
+sufficient_coalitions:
+ - &coalitions
+ kind: weights
+ criterion_weights: [1, 0, 1]
+ - *coalitions
+kind: ncs-classification-model
+format_version: 1
+accepted_values:
+ - kind: thresholds
+ thresholds: [7.96338844, 3.82566905]
+ - kind: thresholds
+ thresholds: [73, 84]
+ - kind: thresholds
+ thresholds: [B, B]
+sufficient_coalitions:
+ - &coalitions
+ kind: weights
+ criterion_weights: [1, 0, 1]
+ - *coalitions
+kind: ncs-classification-model
+format_version: 1
+accepted_values:
+ - kind: thresholds
+ thresholds: [7.96338844, 3.74707603]
+ - kind: thresholds
+ thresholds: [94, 99]
+ - kind: thresholds
+ thresholds: [B, B]
+sufficient_coalitions:
+ - &coalitions
+ kind: weights
+ criterion_weights: [1, 0, 1]
+ - *coalitions
+kind: ncs-classification-model
+format_version: 1
+accepted_values:
+ - kind: thresholds
+ thresholds: [7.95116329, 3.74707603]
+ - kind: thresholds
+ thresholds: [94, 99]
+ - kind: thresholds
+ thresholds: [B, B]
+sufficient_coalitions:
+ - &coalitions
+ kind: weights
+ criterion_weights: [1, 0, 1]
+ - *coalitions
+```
+
+
+#### Other strategies
+
+@todo(Documentation, v1.1) Write this section
+
+
+```python
+class SillyWeightsStrategy(lc.LearnMrsortByWeightsProfilesBreed.WeightsOptimizationStrategy):
+ pass
+```
+
+
+```python
+class SillyProfilesStrategy(lc.LearnMrsortByWeightsProfilesBreed.ProfilesImprovementStrategy):
+ pass
+```
diff --git a/docs/_sources/reference/lincs.rst.txt b/docs/_sources/reference/lincs.rst.txt
index 8647d341..e17ce245 100644
--- a/docs/_sources/reference/lincs.rst.txt
+++ b/docs/_sources/reference/lincs.rst.txt
@@ -26,6 +26,14 @@
Raised by learning algorithms when they can't reach their objective.
+ .. class:: UniformRandomBitsGenerator
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. method:: __call__(arg: UniformRandomBitsGenerator1) -> int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
.. module:: lincs.classification
The ``lincs.classification`` module
@@ -654,7 +662,7 @@
@todo(Documentation, v1.1) Add a docstring.
- .. method:: __init__(problem: Problem, learning_set: Alternatives)
+ .. method:: __init__(problem: Problem, learning_set: Alternatives [, nb_minimize_threads: int=0 [, timeout_fast_minimize: int=60 [, coef_minimize_time: int=2]]])
@todo(Documentation, v1.1) Add a docstring.
@@ -666,7 +674,7 @@
@todo(Documentation, v1.1) Add a docstring.
- .. method:: __init__(problem: Problem, learning_set: Alternatives)
+ .. method:: __init__(problem: Problem, learning_set: Alternatives [, nb_minimize_threads: int=0 [, timeout_fast_minimize: int=60 [, coef_minimize_time: int=2]]])
@todo(Documentation, v1.1) Add a docstring.
@@ -714,7 +722,48 @@
@todo(Documentation, v1.1) Add a docstring.
- .. method:: get_best_accuracy() -> int
+ .. property:: criteria_count
+ :type: int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: categories_count
+ :type: int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: boundaries_count
+ :type: int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: alternatives_count
+ :type: int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: values_counts
+ :type: Iterable[int]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: performance_ranks
+ :type: Iterable[Iterable[int]]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: assignments
+ :type: Iterable[int]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: models_count
+ :type: int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: urbgs
+ :type: Iterable[UniformRandomBitsGenerator]
@todo(Documentation, v1.1) Add a docstring.
@@ -723,6 +772,34 @@
@todo(Documentation, v1.1) Add a docstring.
+ .. property:: model_indexes
+ :type: Iterable[int]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: weights
+ :type: Iterable[Iterable[int]]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: profile_ranks
+ :type: Iterable[Iterable[Iterable[int]]]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. property:: accuracies
+ :type: Iterable[int]
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. method:: get_best_accuracy() -> int
+
+ @todo(Documentation, v1.1) Add a docstring.
+
+ .. method:: get_best_model() -> Model
+
+ @todo(Documentation, v1.1) Add a docstring.
+
.. class:: ProfilesInitializationStrategy
@todo(Documentation, v1.1) Add a docstring.
@@ -855,7 +932,7 @@
@todo(Documentation, v1.1) Add a docstring.
- .. method:: __init__(learning_data: LearningData, max_iteration_index: int)
+ .. method:: __init__(learning_data: LearningData, max_iterations_count: int)
@todo(Documentation, v1.1) Add a docstring.
diff --git a/docs/_sources/user-guide.rst.txt b/docs/_sources/user-guide.rst.txt
index ff43dbb6..c044281f 100644
--- a/docs/_sources/user-guide.rst.txt
+++ b/docs/_sources/user-guide.rst.txt
@@ -512,7 +512,7 @@ They produce a different kind of model, with the sufficient coalitions specified
.. code:: yaml
- # Reproduction command (with lincs version 1.1.0a5): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions
+ # Reproduction command (with lincs version 1.1.0a6): lincs learn classification-model problem.yml learning-set.csv --model-type ucncs --ucncs.strategy sat-by-coalitions
kind: ncs-classification-model
format_version: 1
accepted_values:
diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js
index 236873b3..e705bc86 100644
--- a/docs/_static/documentation_options.js
+++ b/docs/_static/documentation_options.js
@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
- VERSION: '1.1.0a5',
+ VERSION: '1.1.0a6',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
diff --git a/docs/changelog.html b/docs/changelog.html
index 15847c16..029c458d 100644
--- a/docs/changelog.html
+++ b/docs/changelog.html
@@ -5,11 +5,11 @@
- Changelog — lincs 1.1.0a5 documentation
+ Changelog — lincs 1.1.0a6 documentation
-
+
@@ -34,6 +34,28 @@
Changelog
+
+Version 1.1.0a6 (2024-02-06)
+
+Breaking Rename max_iteration_index
to max_iterations_count
in ...TerminateAfterIterations.__init__
+Make the Python API guide Jupyter Notebook downloadable
+Expose all attributes of WPB.LearningData
; start to document them in our Python API guide
+Expose parameters of EvalMaxSAT in our API and command-line interface (see lincs learn classification-model --help
):
+
+
+--ucncs.max-sat-by-separation.solver
(for consistency, always "eval-max-sat"
for now)
+--ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads
+--ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize
+--ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time
+--ucncs.max-sat-by-coalitions.solver
(for consistency, always "eval-max-sat"
for now)
+--ucncs.max-sat-by-coalitions.eval-max-sat.nb-minimize-threads
+--ucncs.max-sat-by-coalitions.eval-max-sat.timeout-fast-minimize
+--ucncs.max-sat-by-coalitions.eval-max-sat.coef-minimize-time
+
+
+
+
+
Version 1.1.0a5 (2024-02-01)
@@ -346,6 +368,7 @@ Navigation
Contributor guide
Roadmap
Changelog
+Version 1.1.0a6 (2024-02-06)
Version 1.1.0a5 (2024-02-01)
Versions 1.1.0a2 to 1.1.0a4 (2024-01-29)
Versions 1.1.0a0 (2024-01-10), 1.1.0a1 (2024-01-11)
diff --git a/docs/conceptual-overview.html b/docs/conceptual-overview.html
index 8f4f34cb..e7717edf 100644
--- a/docs/conceptual-overview.html
+++ b/docs/conceptual-overview.html
@@ -5,11 +5,11 @@
- Conceptual overview — lincs 1.1.0a5 documentation
+ Conceptual overview — lincs 1.1.0a6 documentation
-
+
diff --git a/docs/contributor-guide.html b/docs/contributor-guide.html
index 6a3586ce..597e0b64 100644
--- a/docs/contributor-guide.html
+++ b/docs/contributor-guide.html
@@ -5,11 +5,11 @@
- Contributor guide — lincs 1.1.0a5 documentation
+ Contributor guide — lincs 1.1.0a6 documentation
-
+
diff --git a/docs/genindex.html b/docs/genindex.html
index 9a91d44e..a5d61393 100644
--- a/docs/genindex.html
+++ b/docs/genindex.html
@@ -4,11 +4,11 @@
- Index — lincs 1.1.0a5 documentation
+ Index — lincs 1.1.0a6 documentation
-
+
@@ -292,8 +292,6 @@ Symbols
lincs-learn-classification-model command line option
-
-
+
+
--output-model
@@ -407,6 +407,62 @@ Symbols
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.coef-minimize-time
+
+
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.nb-minimize-threads
+
+
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.timeout-fast-minimize
+
+
+
+ --ucncs.max-sat-by-coalitions.solver
+
+
+
+ --ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time
+
+
+
+ --ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads
+
+
+
+ --ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize
+
+
+
+ --ucncs.max-sat-by-separation.solver
+
+
@@ -508,6 +564,8 @@ Symbols
_
@@ -646,6 +710,8 @@ B
+ criteria_count (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData property) , [1]
+
Criterion (class in lincs.classification) , [1]
Criterion.EnumeratedValues (class in lincs.classification) , [1]
@@ -752,11 +822,13 @@ G
generate_mrsort_model() (in module lincs.classification) , [1]
-
-
+
get_best_accuracy() (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData method) , [1]
+
+ get_best_model() (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData method) , [1]
get_value_rank() (lincs.classification.Criterion.EnumeratedValues method) , [1]
@@ -1081,6 +1153,22 @@ L
--mrsort.weights-profiles-breed.weights-strategy
--output-model
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.coef-minimize-time
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.nb-minimize-threads
+
+ --ucncs.max-sat-by-coalitions.eval-max-sat.timeout-fast-minimize
+
+ --ucncs.max-sat-by-coalitions.solver
+
+ --ucncs.max-sat-by-separation.eval-max-sat.coef-minimize-time
+
+ --ucncs.max-sat-by-separation.eval-max-sat.nb-minimize-threads
+
+ --ucncs.max-sat-by-separation.eval-max-sat.timeout-fast-minimize
+
+ --ucncs.max-sat-by-separation.solver
--ucncs.strategy
@@ -1159,6 +1247,10 @@ M
Model (class in lincs.classification) , [1]
Model.JSON_SCHEMA (in module lincs.classification) , [1]
+
+ model_indexes (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData property) , [1]
+
+ models_count (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData property) , [1]
module
@@ -1239,6 +1331,8 @@ P
Performance.Integer (class in lincs.classification) , [1]
Performance.Real (class in lincs.classification) , [1]
+
+ performance_ranks (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData property) , [1]
PRE
@@ -1281,6 +1375,8 @@ P
Problem.JSON_SCHEMA (in module lincs.classification) , [1]
profile (lincs.classification.Alternative property) , [1]
+
+ profile_ranks (lincs.classification.LearnMrsortByWeightsProfilesBreed.LearningData property) , [1]
@@ -1386,10 +1482,14 @@ U
@@ -1405,8 +1505,6 @@ V
(lincs.classification.Performance.Real property) , [1]
-
-
+
@@ -1423,9 +1525,11 @@ V
W