Skip to content

Commit 2ec29a6

Browse files
author
Emily Strong
committed
parameterized empty neighborhoods feature
Signed-off-by: Emily Strong <emily.strong@fmr.com>
1 parent 9ee088d commit 2ec29a6

19 files changed

+217
-67
lines changed

CHANGELOG.txt

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
MABWiser CHANGELOG
33
=====================
44

5+
6+
-------------------------------------------------------------------------------
7+
Aug, 13, 2019 1.6.0
8+
-------------------------------------------------------------------------------
9+
major:
10+
- Configurable empty neighborhood operation for Radius policy
11+
- Empty neighborhood operation changed to use numpy.random.choice instead of numpy.random.randint. Observed predictions for empty neighborhoods may differ from versions 1.5 and prior.
12+
513
-------------------------------------------------------------------------------
614
August, 12, 2019 1.5.10
715
-------------------------------------------------------------------------------

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ To confirm that cloning the repo was successful, run the tests and all should pa
9696
```bash
9797
git clone https://github.com/fmr-llc/mabwiser.git
9898
cd mabwiser
99-
python -m unittest discover tests
99+
python -m unittest discover -v tests
100100
```
101101

102102
To confirm that installation was successful, import the library in Python shell or notebook.

dist/mabwiser-1.5.10-py3-none-any.whl

-41.4 KB
Binary file not shown.

dist/mabwiser-1.6.0-py3-none-any.whl

41.8 KB
Binary file not shown.

docs/_build/doctrees/api.doctree

3.05 KB
Binary file not shown.
536 Bytes
Binary file not shown.

docs/_build/html/api.html

+24-2
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@
271271
</tr>
272272
<tr class="field-even field"><th class="field-name">Email:</th><td class="field-body"><a class="reference external" href="mailto:mabwiser&#37;&#52;&#48;fmr&#46;com">mabwiser<span>&#64;</span>fmr<span>&#46;</span>com</a></td>
273273
</tr>
274-
<tr class="field-odd field"><th class="field-name">Version:</th><td class="field-body">1.5.9 of July 1, 2019</td>
274+
<tr class="field-odd field"><th class="field-name">Version:</th><td class="field-body">1.6.0 of August 13, 2019</td>
275275
</tr>
276276
</tbody>
277277
</table>
@@ -1091,6 +1091,22 @@
10911091
</table>
10921092
</dd></dl>
10931093

1094+
<dl class="attribute">
1095+
<dt id="mabwiser.mab.NeighborhoodPolicy.Radius.no_nhood_prob_of_arm">
1096+
<code class="descname">no_nhood_prob_of_arm</code><a class="headerlink" href="#mabwiser.mab.NeighborhoodPolicy.Radius.no_nhood_prob_of_arm" title="Permalink to this definition"></a></dt>
1097+
<dd><p>The probabilities associated with each arm.
1098+
If not given, a uniform random distribution over all arms is assumed.
1099+
The probabilities should sum up to 1.</p>
1100+
<table class="docutils field-list" frame="void" rules="none">
1101+
<col class="field-name" />
1102+
<col class="field-body" />
1103+
<tbody valign="top">
1104+
<tr class="field-odd field"><th class="field-name">Type:</th><td class="field-body">None or List</td>
1105+
</tr>
1106+
</tbody>
1107+
</table>
1108+
</dd></dl>
1109+
10941110
<p class="rubric">Example</p>
10951111
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">mabwiser.mab</span> <span class="k">import</span> <span class="n">MAB</span><span class="p">,</span> <span class="n">LearningPolicy</span><span class="p">,</span> <span class="n">NeighborhoodPolicy</span>
10961112
<span class="gp">&gt;&gt;&gt; </span><span class="n">list_of_arms</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
@@ -1109,6 +1125,12 @@
11091125
<dd><p>Alias for field number 1</p>
11101126
</dd></dl>
11111127

1128+
<dl class="attribute">
1129+
<dt>
1130+
<code class="descname">no_nhood_prob_of_arm</code></dt>
1131+
<dd><p>Alias for field number 2</p>
1132+
</dd></dl>
1133+
11121134
<dl class="attribute">
11131135
<dt>
11141136
<code class="descname">radius</code></dt>
@@ -1130,7 +1152,7 @@
11301152
</tr>
11311153
<tr class="field-even field"><th class="field-name">Email:</th><td class="field-body"><a class="reference external" href="mailto:mabwiser&#37;&#52;&#48;fmr&#46;com">mabwiser<span>&#64;</span>fmr<span>&#46;</span>com</a></td>
11321154
</tr>
1133-
<tr class="field-odd field"><th class="field-name">Version:</th><td class="field-body">1.5.9 of July 1, 2019</td>
1155+
<tr class="field-odd field"><th class="field-name">Version:</th><td class="field-body">1.6.0 of August 13, 2019</td>
11341156
</tr>
11351157
</tbody>
11361158
</table>

docs/_build/html/genindex.html

+4-2
Original file line numberDiff line numberDiff line change
@@ -403,15 +403,17 @@ <h2 id="N">N</h2>
403403
</ul></li>
404404
<li><a href="api.html#mabwiser.mab.MAB.neighborhood_policy">neighborhood_policy (mabwiser.mab.MAB attribute)</a>
405405
</li>
406-
</ul></td>
407-
<td style="width: 33%; vertical-align: top;"><ul>
408406
<li><a href="api.html#mabwiser.mab.NeighborhoodPolicy">NeighborhoodPolicy (class in mabwiser.mab)</a>
409407
</li>
408+
</ul></td>
409+
<td style="width: 33%; vertical-align: top;"><ul>
410410
<li><a href="api.html#mabwiser.mab.NeighborhoodPolicy.Clusters">NeighborhoodPolicy.Clusters (class in mabwiser.mab)</a>
411411
</li>
412412
<li><a href="api.html#mabwiser.mab.NeighborhoodPolicy.KNearest">NeighborhoodPolicy.KNearest (class in mabwiser.mab)</a>
413413
</li>
414414
<li><a href="api.html#mabwiser.mab.NeighborhoodPolicy.Radius">NeighborhoodPolicy.Radius (class in mabwiser.mab)</a>
415+
</li>
416+
<li><a href="api.html#mabwiser.mab.NeighborhoodPolicy.Radius.no_nhood_prob_of_arm">no_nhood_prob_of_arm (mabwiser.mab.NeighborhoodPolicy.Radius attribute)</a>, <a href="api.html#mabwiser.mab.NeighborhoodPolicy.Radius.no_nhood_prob_of_arm">[1]</a>
415417
</li>
416418
<li><a href="api.html#mabwiser.utils.Num">Num (in module mabwiser.utils)</a>
417419
</li>

docs/_build/html/objects.inv

12 Bytes
Binary file not shown.

docs/_build/html/searchindex.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/simulator.py

-5
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def binarize(decision, reward):
5858
sim = Simulator(contextual_mabs, decisions, rewards, contexts,
5959
scaler=StandardScaler(), test_size=0.5, is_ordered=False, batch_size=0, seed=123456)
6060
sim.run()
61-
sim.save_results()
6261
end = time()
6362

6463
runtime = (end - start) / 60
@@ -84,7 +83,6 @@ def binarize(decision, reward):
8483
sim = Simulator(context_free_mabs, decisions, rewards, contexts=None,
8584
scaler=None, test_size=0.5, is_ordered=False, batch_size=100, seed=123456)
8685
sim.run()
87-
sim.save_results()
8886
end = time()
8987

9088
runtime = (end - start) / 60
@@ -110,7 +108,6 @@ def binarize(decision, reward):
110108
sim = Simulator(mixed, decisions, rewards, contexts,
111109
scaler=StandardScaler(), test_size=0.5, is_ordered=False, batch_size=0, seed=123456)
112110
sim.run()
113-
sim.save_results()
114111
end = time()
115112

116113
runtime = (end - start) / 60
@@ -138,7 +135,6 @@ def binarize(decision, reward):
138135
sim = Simulator(hyper_parameter_tuning, decisions, rewards, contexts,
139136
scaler=StandardScaler(), test_size=0.5, is_ordered=False, batch_size=10, seed=123456)
140137
sim.run()
141-
sim.save_results()
142138
end = time()
143139

144140
runtime = (end - start) / 60
@@ -165,7 +161,6 @@ def binarize(decision, reward):
165161
sim = Simulator(contextual_mabs, decisions, rewards, contexts,
166162
scaler=StandardScaler(), test_size=0.5, is_ordered=False, batch_size=0, seed=123456, is_quick=True)
167163
sim.run()
168-
sim.save_results()
169164
end = time()
170165

171166
runtime = (end - start) / 60

mabwiser/mab.py

+19-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66
:Author: FMR LLC
77
:Email: mabwiser@fmr.com
8-
:Version: 1.5.10 of August 12, 2019
8+
:Version: 1.6.0 of August 13, 2019
99
1010
This module defines the public interface of the **MABWiser Library** providing access to the following modules:
1111
@@ -14,7 +14,7 @@
1414
- ``NeighborhoodPolicy``
1515
"""
1616

17-
from typing import List, Union, Dict, NamedTuple, NoReturn, Callable
17+
from typing import List, Union, Dict, NamedTuple, NoReturn, Callable, Optional
1818

1919
import numpy as np
2020
import pandas as pd
@@ -36,7 +36,6 @@
3636

3737

3838
class LearningPolicy(NamedTuple):
39-
4039
class EpsilonGreedy(NamedTuple):
4140
"""Epsilon Greedy Learning Policy.
4241
@@ -49,7 +48,7 @@ class EpsilonGreedy(NamedTuple):
4948
The probability of selecting a random arm for exploration.
5049
Integer or float. Must be between 0 and 1.
5150
Default value is 0.05.
52-
51+
5352
Example
5453
-------
5554
>>> from mabwiser.mab import MAB, LearningPolicy
@@ -64,7 +63,6 @@ class EpsilonGreedy(NamedTuple):
6463
epsilon: Num = 0.05
6564

6665
def _validate(self):
67-
6866
check_true(isinstance(self.epsilon, (int, float)), TypeError("Epsilon must be an integer or float."))
6967
check_true(0 <= self.epsilon <= 1, ValueError("The value of epsilon must be between 0 and 1."))
7068

@@ -286,7 +284,6 @@ def _validate(self):
286284

287285

288286
class NeighborhoodPolicy(NamedTuple):
289-
290287
class Clusters(NamedTuple):
291288
"""Clusters Neighborhood Policy.
292289
@@ -378,6 +375,10 @@ class Radius(NamedTuple):
378375
The metric used to calculate distance.
379376
Accepts any of the metrics supported by scipy.spatial.distance.cdist.
380377
Default value is Euclidean distance.
378+
no_nhood_prob_of_arm: None or List
379+
The probabilities associated with each arm.
380+
If not given, a uniform random distribution over all arms is assumed.
381+
The probabilities should sum up to 1.
381382
382383
Example
383384
-------
@@ -395,12 +396,18 @@ class Radius(NamedTuple):
395396
"""
396397
radius: Num = 0.05
397398
metric: str = "euclidean"
399+
no_nhood_prob_of_arm: Optional[List] = None
398400

399401
def _validate(self):
400402
check_true(isinstance(self.radius, (int, float)), TypeError("Radius must be an integer or a float."))
401403
check_true((self.metric in Constants.distance_metrics),
402404
ValueError("Metric must be supported by scipy.spatial.distance.cdist"))
403405
check_true(self.radius > 0, ValueError("Radius must be greater than zero."))
406+
check_true((self.no_nhood_prob_of_arm == None) or isinstance(self.no_nhood_prob_of_arm, List),
407+
TypeError("no_nhood_prob_of_arm must be None or List."))
408+
if isinstance(self.no_nhood_prob_of_arm, List):
409+
check_true(np.isclose(sum(self.no_nhood_prob_of_arm), 1.0),
410+
ValueError("no_nhood_prob_of_arm should sum up to 1.0"))
404411

405412

406413
class MAB:
@@ -494,7 +501,6 @@ def __init__(self,
494501
If set to -1, all CPUs are used.
495502
If set to -2, all CPUs but one are used, and so on.
496503
497-
498504
Raises
499505
------
500506
TypeError: Arms were not provided in a list.
@@ -511,6 +517,7 @@ def __init__(self,
511517
TypeError: For Clusters, n_clusters must be an integer.
512518
TypeError: For Clusters, is_minibatch must be a boolean.
513519
TypeError: For Radius, radius must be an integer or float.
520+
TypeError: For Radius, no_nhood_prob_of_arm must be None or List that sums up to 1.0.
514521
TypeError: For KNearest, k must be an integer or float.
515522
516523
ValueError: Invalid number of arms.
@@ -525,6 +532,7 @@ def __init__(self,
525532
ValueError: For Clusters, n_clusters cannot be less than 2.
526533
ValueError: For Radius and KNearest, metric is not supported by scipy.spatial.distance.cdist.
527534
ValueError: For Radius, radius must be greater than zero.
535+
ValueError: For Radius, if given, no_nhood_prob_of_arm list should sum up to 1.0.
528536
ValueError: For KNearest, k must be greater than zero.
529537
"""
530538

@@ -564,15 +572,16 @@ def __init__(self,
564572
if neighborhood_policy:
565573
self.is_contextual = True
566574

567-
# Do not use parallel fit or predict for Learning Policy when co
575+
# Do not use parallel fit or predict for Learning Policy when contextual
568576
lp.n_jobs = 1
569577

570578
if isinstance(neighborhood_policy, NeighborhoodPolicy.Clusters):
571579
self._imp = _Clusters(self._rng, self.arms, self.n_jobs, lp, self.neighborhood_policy.n_clusters,
572580
self.neighborhood_policy.is_minibatch)
573581
elif isinstance(neighborhood_policy, NeighborhoodPolicy.Radius):
574582
self._imp = _Radius(self._rng, self.arms, self.n_jobs, lp,
575-
self.neighborhood_policy.radius, self.neighborhood_policy.metric)
583+
self.neighborhood_policy.radius, self.neighborhood_policy.metric,
584+
self.neighborhood_policy.no_nhood_prob_of_arm)
576585
elif isinstance(neighborhood_policy, NeighborhoodPolicy.KNearest):
577586
self._imp = _KNearest(self._rng, self.arms, self.n_jobs, lp,
578587
self.neighborhood_policy.k, self.neighborhood_policy.metric)
@@ -881,7 +890,7 @@ def _validate_fit_args(self, decisions, rewards, contexts) -> NoReturn:
881890
# Sync contexts data with contextual policy
882891
check_true(self.is_contextual,
883892
TypeError("Fitting contexts data requires context policy or parametric learning policy."))
884-
check_true((len(decisions) == len(contexts)) or (len(decisions)==1 and isinstance(contexts, pd.Series)),
893+
check_true((len(decisions) == len(contexts)) or (len(decisions) == 1 and isinstance(contexts, pd.Series)),
885894
ValueError("Decisions and contexts should be same length: len(decision) = " +
886895
str(len(decisions)) + " vs. len(contexts) = " + str(len(contexts))))
887896

mabwiser/neighbors.py

+29-25
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
class _Neighbors(BaseMAB):
21+
2122
def __init__(self, rng: np.random.RandomState, arms: List[Arm], n_jobs: int,
2223
lp: Union[_EpsilonGreedy, _Linear, _Random, _Softmax, _ThompsonSampling, _UCB1], metric: str):
2324
super().__init__(rng, arms, n_jobs)
@@ -79,6 +80,17 @@ def _predict_contexts(self, contexts: np.ndarray, is_predict: bool,
7980
"""Abstract method to be implemented by child classes."""
8081
pass
8182

83+
def _get_nhood_predictions(self, lp, indices, row_2d, is_predict):
84+
85+
# Fit the decisions and rewards of the neighbors
86+
lp.fit(self.decisions[indices], self.rewards[indices], self.contexts[indices])
87+
88+
# Predict based on the neighbors
89+
if is_predict:
90+
return lp.predict(row_2d)
91+
else:
92+
return lp.predict_expectations(row_2d)
93+
8294
def _uptake_new_arm(self, arm: Arm, binarizer: Callable = None, scaler: Callable = None):
8395
self.lp.add_arm(arm, binarizer)
8496

@@ -87,10 +99,11 @@ class _Radius(_Neighbors):
8799

88100
def __init__(self, rng: np.random.RandomState, arms: List[Arm], n_jobs: int,
89101
lp: Union[_EpsilonGreedy, _Softmax, _ThompsonSampling, _UCB1, _Linear],
90-
radius: Num, metric: str):
102+
radius: Num, metric: str, no_nhood_prob_of_arm=Optional[List]):
91103
super().__init__(rng, arms, n_jobs, lp, metric)
92104

93105
self.radius = radius
106+
self.no_nhood_prob_of_arm = no_nhood_prob_of_arm
94107

95108
def _predict_contexts(self, contexts: np.ndarray, is_predict: bool,
96109
seeds: Optional[np.ndarray] = None, start_index: Optional[int] = None) -> List:
@@ -119,26 +132,25 @@ def _predict_contexts(self, contexts: np.ndarray, is_predict: bool,
119132

120133
# If neighbors exist
121134
if indices[0].size > 0:
122-
123-
# Fit the decisions and rewards of the neighbors
124-
lp.fit(self.decisions[indices], self.rewards[indices], self.contexts[indices])
125-
126-
# Predict based on the neighbors
127-
if is_predict:
128-
predictions[index] = lp.predict(row_2d)
129-
else:
130-
predictions[index] = lp.predict_expectations(row_2d)
131-
135+
predictions[index] = self._get_nhood_predictions(lp, indices, row_2d, is_predict)
132136
else: # When there are no neighbors
133-
# Random arm (or nan expectations)
134-
if is_predict:
135-
predictions[index] = self.arms[lp.rng.randint(0, len(self.arms))]
136-
else:
137-
predictions[index] = self.arm_to_expectation.copy()
137+
predictions[index] = self._get_no_nhood_predictions(lp, is_predict)
138138

139139
# Return the list of predictions
140140
return predictions
141141

142+
def _get_no_nhood_predictions(self, lp, is_predict):
143+
144+
if is_predict:
145+
# if no_nhood_prob_of_arm is None, select a random int
146+
# else, select a non-uniform random arm
147+
# choice returns an array, hence get zero index
148+
rand_int = lp.rng.choice(len(self.arms), 1, p=self.no_nhood_prob_of_arm)[0]
149+
return self.arms[rand_int]
150+
else:
151+
# Expectations will be nan when there are no neighbors
152+
return self.arm_to_expectation.copy()
153+
142154

143155
class _KNearest(_Neighbors):
144156

@@ -173,15 +185,7 @@ def _predict_contexts(self, contexts: np.ndarray, is_predict: bool,
173185
# Find the k nearest neighbor indices
174186
indices = np.argpartition(distances_to_row, self.k - 1)[:self.k]
175187

176-
# Fit the decisions and rewards of the neighbors learning from the contexts
177-
lp.fit(self.decisions[indices], self.rewards[indices], self.contexts[indices])
178-
179-
# Predict (or predict_expectations) based on the neighbors
180-
# The row is used only for parametric learning policies, and it has to be 2D
181-
if is_predict:
182-
predictions[index] = lp.predict(row_2d)
183-
else:
184-
predictions[index] = lp.predict_expectations(row_2d)
188+
predictions[index] = self._get_nhood_predictions(lp, indices, row_2d, is_predict)
185189

186190
# Return the list of predictions
187191
return predictions

0 commit comments

Comments
 (0)