# -*- coding: utf-8 -*-
""" Exponentially distributed arm.
Example of creating an arm:
>>> import random; import numpy as np
>>> random.seed(0); np.random.seed(0)
>>> Exp03 = ExponentialFromMean(0.3)
>>> Exp03
\mathrm{Exp}(3.2, 1)
>>> Exp03.mean # doctest: +ELLIPSIS
0.3000...
Examples of sampling from an arm:
>>> Exp03.draw() # doctest: +ELLIPSIS
0.052...
>>> Exp03.draw_nparray(20) # doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE
array([0.18..., 0.10..., 0.15..., 0.18..., 0.26...,
0.13..., 0.25..., 0.03..., 0.01..., 0.29... ,
0.07..., 0.19..., 0.17..., 0.02... , 0.82... ,
0.76..., 1. , 0.05..., 0.07..., 0.04...])
"""
from __future__ import division, print_function # Python 2 compatibility
__author__ = "Olivier Cappé, Aurélien Garivier, Lilian Besson"
__version__ = "0.5"
from math import isinf, exp, log
from random import random
import numpy as np
from numpy.random import random as nprandom
from scipy.optimize import minimize
# Local imports
try:
from .Arm import Arm
from .kullback import klExp
except ImportError:
from Arm import Arm
from kullback import klExp
def p_of_expectation(expectation, trunc=1):
"""Use a numerical solver (:func:`scipy.optimize.minimize`) to find the value p giving an arm Exp(p) of a given expectation."""
if isinf(trunc):
def expp(p):
""" mean = expectation(p)."""
return 1. / p
else:
def expp(p):
""" mean = expectation(p)."""
return (1. - exp(-p * trunc)) / p
def objective(p):
""" Objective function to minimize."""
return abs(expectation - expp(p))
return minimize(objective, 1).x[0]
[docs]class Exponential(Arm):
""" Exponentially distributed arm, possibly truncated.
- Default is to truncate to 1 (so Exponential.draw() is in [0, 1]).
"""
# def __init__(self, p, trunc=float('+inf')):
[docs] def __init__(self, p, trunc=1):
"""New arm."""
self.p = p #: Parameter p for Exponential arm
assert p > 0, "Error, the parameter 'p' for Exponential arm has to be > 0."
self.trunc = trunc #: Max value of reward
assert trunc > 0, "Error, the parameter 'trunc' for Exponential arm has to be > 0."
if isinf(trunc):
self.mean = 1. / p #: Mean of Exponential arm
else:
self.mean = (1. - exp(-p * trunc)) / p
# --- Random samples
[docs] def draw(self, t=None):
""" Draw one random sample. The parameter t is ignored in this Arm."""
return min((-1. / self.p) * log(random()), self.trunc)
[docs] def draw_nparray(self, shape=(1,)):
""" Draw one random sample. The parameter t is ignored in this Arm."""
return np.minimum((-1. / self.p) * np.log(nprandom(shape)), self.trunc)
[docs] def set_mean_param(self, p_inv):
self.p = 1 / p_inv
if isinf(self.trunc):
self.mean = 1. / self.p #: Mean of Exponential arm
else:
self.mean = (1. - exp(-self.p * self.trunc)) / self.p
# --- Printing
# This decorator @property makes this method an attribute, cf. https://docs.python.org/3/library/functions.html#property
@property
def lower_amplitude(self):
"""(lower, amplitude)"""
return 0., self.trunc
[docs] def __str__(self):
return "Exponential"
[docs] def __repr__(self):
return r"{}({:.3g}{})".format(r'\mathrm{Exp}', self.p, '' if isinf(self.trunc) else ', {:.3g}'.format(self.trunc))
# --- Lower bound
[docs] @staticmethod
def kl(x, y):
""" The kl(x, y) to use for this arm."""
return klExp(x, y)
[docs] @staticmethod
def oneLR(mumax, mu):
""" One term of the Lai & Robbins lower bound for Exponential arms: (mumax - mu) / KL(mu, mumax). """
return (mumax - mu) / klExp(mu, mumax)
[docs] def oneHOI(self, mumax, mu):
""" One term for the HOI factor for this arm."""
return 1 - (mumax - mu) / self.trunc
[docs]class ExponentialFromMean(Exponential):
""" Exponentially distributed arm, possibly truncated, defined by its mean and not its parameter.
- Default is to truncate to 1 (so Exponential.draw() is in [0, 1]).
"""
[docs] def __init__(self, mean, trunc=1):
"""New arm."""
p = p_of_expectation(mean)
super(ExponentialFromMean, self).__init__(p, trunc=trunc)
[docs]class UnboundedExponential(Exponential):
""" Exponential distributed arm, not truncated, ie. trunc = oo."""
[docs] def __init__(self, mu):
"""New arm."""
super(UnboundedExponential, self).__init__(mu, trunc=float('+inf'))
# Only export and expose the class defined here
__all__ = ["Exponential", "ExponentialFromMean", "UnboundedExponential"]
# --- Debugging
if __name__ == "__main__":
# Code for debugging purposes.
from doctest import testmod
print("\nTesting automatically all the docstring written in each functions of this module :")
testmod(verbose=True)