Skip to content

Commit

Permalink
Minor things before next release (#154)
Browse files Browse the repository at this point in the history
* Update docs, readme

* Intersphinx

* Fix MABWiser link

* Notes and warnings

* Bump version to v5.2.0
  • Loading branch information
N-Wouda committed Jun 10, 2023
1 parent 963080d commit d5372dd
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 126 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ operator selection scheme is updated based on the evaluation outcome.

`alns` depends only on `numpy` and `matplotlib`. It may be installed in the
usual way as

```
pip install alns
```
Additionally, to enable more advanced operator selection schemes using
multi-armed bandit algorithms, `alns` may be installed with the optional
[MABWiser][12] dependency:
```
pip install alns[mabwiser]
```

The documentation is available [here][1].

Expand Down Expand Up @@ -122,3 +127,5 @@ Or, using the following BibTeX entry:
[10]: https://alns.readthedocs.io/en/latest/setup/template.html

[11]: https://alns.readthedocs.io/en/latest/setup/introduction_to_alns.html

[12]: https://github.com/fidelity/mabwiser
12 changes: 3 additions & 9 deletions alns/State.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,13 @@ def objective(self) -> float:
"""


class ContextualState(Protocol):
class ContextualState(State, Protocol):
"""
Protocol for a solution state that also provides context. Solutions should
define an ``objective()`` function as well as a ``get_context()``
function.
define ``objective()`` and ``get_context()`` methods.
"""

def objective(self) -> float:
"""
Computes the state's associated objective value.
"""

def get_context(self) -> np.ndarray:
"""
Computes a context vector for the current state
Computes a context vector for the current state.
"""
2 changes: 1 addition & 1 deletion alns/select/AlphaUCB.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class AlphaUCB(OperatorSelectionScheme):
----------
.. [1] Hendel, G. 2022. Adaptive large neighborhood search for mixed
integer programming. *Mathematical Programming Computation* 14:
185 221.
185 - 221.
"""

def __init__(
Expand Down
74 changes: 40 additions & 34 deletions alns/select/MABSelector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from alns.State import ContextualState
from alns.select.OperatorSelectionScheme import OperatorSelectionScheme

MABWISER_AVAILABLE = True
try:
from mabwiser.mab import MAB, LearningPolicy, NeighborhoodPolicy

MABWISER_AVAILABLE = True
except ModuleNotFoundError:
MABWISER_AVAILABLE = False

Expand All @@ -18,6 +19,11 @@ class MABSelector(OperatorSelectionScheme):
"""
A selector that uses any multi-armed-bandit algorithm from MABWiser.
.. warning::
ALNS does not install MABWiser by default. You can install it as an
extra dependency via ``pip install alns[mabwiser]``.
This selector is a wrapper around the many multi-armed bandit algorithms
available in the `MABWiser <https://github.com/fidelity/mabwiser>`_
library. Since ALNS operator selection can be framed as a
Expand All @@ -26,9 +32,12 @@ class MABSelector(OperatorSelectionScheme):
multi-armed-bandit algorithms as operator selectors instead of
having to reimplement them.
Note that if the provided learning policy is a contextual bandit
algorithm, your state class must provide a `get_context` function that
returns a context vector for the current state.
.. note::
If the provided learning policy is a contextual bandit algorithm, your
state class must implement a ``get_context`` method that returns a
context vector for the current state. See the
:class:`~alns.State.ContextualState` protocol for details.
Parameters
----------
Expand All @@ -54,12 +63,15 @@ class MABSelector(OperatorSelectionScheme):
Optional boolean matrix that indicates coupling between destroy and
repair operators. Entry (i, j) is True if destroy operator i can be
used together with repair operator j, and False otherwise.
kwargs
Any additional arguments. These will be passed to the underlying MAB
object.
References
----------
.. [1] Emily Strong, Bernard Kleynhans, & Serdar Kadioglu (2021).
MABWiser: Parallelizable Contextual Multi-armed Bandits.
Int. J. Artif. Intell. Tools, 30(4), 2150021:1–2150021:19.
Int. J. Artif. Intell. Tools, 30(4), 2150021: 1 - 19.
"""

def __init__(
Expand All @@ -74,7 +86,11 @@ def __init__(
**kwargs,
):
if not MABWISER_AVAILABLE:
raise ImportError("MABSelector requires the MABWiser library. ")
msg = """
The MABSelector requires the MABWiser dependency to be installed.
You can install it using `pip install alns[mabwiser]`.
"""
raise ModuleNotFoundError(msg)

super().__init__(num_destroy, num_repair, op_coupling)

Expand All @@ -85,26 +101,19 @@ def __init__(
# More than four is OK because we only use the first four.
raise ValueError(f"Expected four scores, found {len(scores)}")

# forward the seed argument if not null
self._scores = scores

if seed is not None:
kwargs["seed"] = seed

# the set of valid operator pairs (arms) is equal to the cartesian
# product of destroy and repair operators, except we leave out any
# pairs disallowed by op_coupling
arms = [
f"{d_idx}_{r_idx}"
for d_idx in range(num_destroy)
for r_idx in range(num_repair)
if self._op_coupling[d_idx, r_idx]
]
self._mab = MAB(
arms,
learning_policy,
neighborhood_policy,
**kwargs,
)
self._scores = scores

self._mab = MAB(arms, learning_policy, neighborhood_policy, **kwargs)

@property
def scores(self) -> List[float]:
Expand All @@ -125,19 +134,17 @@ def __call__( # type: ignore[override]
strategy
"""
if self._mab._is_initial_fit:
has_context = self._mab.is_contextual
context = (
np.atleast_2d(curr.get_context()) if has_context else None
)
prediction = self._mab.predict(contexts=context)
has_ctx = self._mab.is_contextual
ctx = np.atleast_2d(curr.get_context()) if has_ctx else None
prediction = self._mab.predict(contexts=ctx)
return arm2ops(prediction)
else:
# This can happen when the MAB object has not yet been fit on any
# observations. In that case we return any feasible operator index
# pair as a first observation.
allowed = np.argwhere(self._op_coupling)
idx = rnd_state.randint(len(allowed))
return (allowed[idx][0], allowed[idx][1])
return allowed[idx][0], allowed[idx][1]

def update( # type: ignore[override]
self,
Expand All @@ -150,20 +157,19 @@ def update( # type: ignore[override]
Updates the underlying MAB algorithm given the reward of the chosen
destroy and repair operator combination ``(d_idx, r_idx)``.
"""
has_context = self._mab.is_contextual
context = np.atleast_2d(cand.get_context()) if has_context else None

has_ctx = self._mab.is_contextual
ctx = np.atleast_2d(cand.get_context()) if has_ctx else None
self._mab.partial_fit(
[ops2arm(d_idx, r_idx)],
[self._scores[outcome]],
contexts=context,
contexts=ctx,
)


def ops2arm(destroy_idx: int, repair_idx: int) -> str:
def ops2arm(d_idx: int, r_idx: int) -> str:
"""
Converts a tuple of destroy and repair operator indices to an arm
string that can be passed to self._mab.
Converts the given destroy and repair operator indices to an arm string
that can be passed to the MAB instance.
Examples
--------
Expand All @@ -172,12 +178,12 @@ def ops2arm(destroy_idx: int, repair_idx: int) -> str:
>>> ops2arm(12, 3)
"12_3"
"""
return f"{destroy_idx}_{repair_idx}"
return f"{d_idx}_{r_idx}"


def arm2ops(arm: str) -> Tuple[int, int]:
"""
Converts an arm string returned from self._mab to a tuple of destroy
Converts an arm string returned by the MAB instance into a tuple of destroy
and repair operator indices.
Examples
Expand All @@ -187,5 +193,5 @@ def arm2ops(arm: str) -> Tuple[int, int]:
>>> arm2ops("12_3")
(12, 3)
"""
[destroy, repair] = arm.split("_")
return int(destroy), int(repair)
d_idx, r_idx = map(int, arm.split("_"))
return d_idx, r_idx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy.random as rnd
from mabwiser.mab import LearningPolicy, NeighborhoodPolicy
from numpy.testing import assert_equal, assert_raises
from numpy.testing import assert_, assert_equal, assert_raises
from pytest import mark

from alns.Outcome import Outcome
Expand All @@ -29,25 +29,16 @@ def test_arm_conversion(destroy_idx, repair_idx):


def test_does_not_raise_on_valid_mab():
MABSelector([0, 0, 0, 0], 2, 1, LearningPolicy.EpsilonGreedy(0.15))
MABSelector(
[0, 0, 0, 0],
2,
1,
LearningPolicy.EpsilonGreedy(0.15),
NeighborhoodPolicy.Radius(5),
)
MABSelector(
[0, 0, 0, 0],
2,
1,
LearningPolicy.EpsilonGreedy(0.15),
NeighborhoodPolicy.Radius(5),
1234567,
)
policy = LearningPolicy.EpsilonGreedy(0.15)
select = MABSelector([5, 0, 3, 0], 2, 1, policy)
assert_equal(select.scores, [5, 0, 3, 0])
assert_(len(select.mab.arms), 2)

MABSelector([0, 0, 0, 0], 2, 1, policy, NeighborhoodPolicy.Radius(5))
MABSelector(
[0, 0, 0, 0], 2, 1, LearningPolicy.EpsilonGreedy(0.15), seed=1234567
[1, 0, 0, 0], 2, 1, policy, NeighborhoodPolicy.Radius(5), 1234567
)
MABSelector([2, 1, 0, 0], 2, 1, policy, seed=1234567)


@mark.parametrize(
Expand Down
4 changes: 3 additions & 1 deletion docs/source/api/select.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ All operator selection schemes inherit from :class:`~alns.select.OperatorSelecti
:members:

.. automodule:: alns.select.MABSelector
:members:

.. autoclass:: MABSelector
:members:

.. automodule:: alns.select.RandomSelect
:members:
Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
"mabwiser": ("https://fidelity.github.io/mabwiser/", None),
}
intersphinx_disabled_domains = ["std"]

Expand Down
Loading

0 comments on commit d5372dd

Please sign in to comment.