Source code for Arms.Exponential

# -*- 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)