from abc import ABC, abstractmethod
from functools import partial
from typing import Callable
import numpy as np
[docs]class LossFunction(ABC):
"""An abstract class for loss function.
Users can define their loss functions by inheriting from this class and override the method :meth:`~ pynol.environment.loss_function.LossFunction.__getitem__`.
"""
def __init__(self) -> None:
pass
@abstractmethod
def __getitem__(self, t: int) -> Callable[[np.ndarray], float]:
pass
[docs]class InnerLoss(LossFunction):
"""This class defines the inner loss function.
Args:
feature (numpy.ndarray): Features of the environment.
scale (float): Scale coefficient of the loss function.
Example:
::
import numpy as np
func = InnerLoss(feature=np.random.rand(1000, 5)) # 1000 rounds, 5 dimension
Then, call ``func[t]`` will return the inner loss function :math:`f_t(x) =
\langle \\varphi_t, x \\rangle`, where :math:`\\varphi_t` is the feature at
round :math:`t`.
"""
def __init__(self, feature: np.ndarray = None, scale: float = 1.) -> None:
self.feature = feature
self.scale = scale
def __getitem__(self, t: int):
return lambda x: self.scale * np.dot(x, self.feature[t])
[docs]class SquareLoss(LossFunction):
"""This class defines the logistic loss function.
Args:
feature (numpy.ndarray): Features of the environment.
label (numpy.ndarray): Labels of the environment.
scale (float): Scale coefficient of the loss function.
Example:
::
import numpy as np
feature, label = np.random.rand(1000, 5), np.random.randint(2, size=1000)
func = SquareLoss(feature, label) # 1000 rounds, 5 dimension
Then, call ``func[t]`` will return the square loss function :math:`f_t(x) =
\\frac{1}{2} (y_t - \langle \\varphi_t, x \\rangle)^2`, where
:math:`\\varphi_t` and :math:`y_t` are the feature and label at
round :math:`t`.
"""
def __init__(self,
feature: np.ndarray = None,
label: np.ndarray = None,
scale: float = 1.) -> None:
self.feature = feature
self.label = label
self.scale = scale
def __getitem__(self, t: int) -> Callable[[np.ndarray], float]:
return lambda x: self.scale * 1 / 2 * (
(np.dot(x, self.feature[t]) - self.label[t])**2)
[docs]class LogisticLoss(LossFunction):
"""This class defines the logistic loss function.
Args:
Feature (numpy.ndarray): Features of the environment.
label (numpy.ndarray): Labels of the environment.
scale (float): Scale coefficient of the loss function.
Example:
::
import numpy as np
feature, label = np.random.rand(1000, 5), np.random.randint(2, size=1000)
func = LogisticLoss(feature, label) # 1000 rounds, 5 dimension
Then, call ``func[t]`` will return the loss function :math:`f_t(x) = y \log (\\frac{1}{1+e^{-\\varphi_t^\\top x}})+(1-y) \log (1-\\frac{1}{1+e^{-\\varphi_t^\\top x}})` where
:math:`\\varphi_t` and :math:`y_t` are the feature and label at
round :math:`t`.
"""
def __init__(self,
feature: np.ndarray = None,
label: np.ndarray = None,
scale: float = 1.) -> None:
self.feature = feature
self.label = label
self.scale = scale
def __getitem__(self, t: int) -> Callable[[np.ndarray], float]:
return partial(self.func, t=t)
def func(self, x, t):
prediction = 1 / (1 + np.e**(-np.dot(x, self.feature[t])))
loss = prediction * np.log(
self.label[t]) + (1 - prediction) * np.log(1 - self.y[t])
return self.scale * loss
[docs]class FuncWithSwitch:
"""This class defines the loss function with switching cost.
Args:
f (LossFunction): Origin loss function.
penalty (float): Penalty coefficient of the switching cost.
norm (non-zero int, numpy.inf): Order of the norm. The default is 2 norm.
order (int): Order the the switching cost. The default is 2.
Example:
::
import numpy as np
feature, label = np.random.rand(1000, 5), np.random.randint(2, size=1000)
f = SquareLoss(feature, label)
func = FuncWithSwitch(f, penalty=1, norm=2, order=2)
Then, call ``func[t]`` will return the square loss function with switching
cost :math:`f_t(x) = \\frac{1}{2} (y_t - \langle \\varphi_t, x \\rangle)^2 +
\lVert x - x_{t-1}\\rVert_2^2` where :math:`\\varphi_t` and :math:`y_t` are
the feature and label at round :math:`t`.
"""
def __init__(self,
f: LossFunction = None,
penalty: float = 1.,
norm: int = 2,
order: int = 2) -> None:
self.f = f
self.penalty = penalty
self.norm = norm
self.order = order
self.x_last = None
def __getitem__(self, t: int) -> Callable[[np.ndarray], float]:
return partial(self.func, f=self.f[t])
def func(self, x: np.ndarray, f: Callable[[np.ndarray], float]):
assert x.ndim == 1 or x.ndim == 2
if self.x_last is None:
self.x_last = x
if x.ndim == 1:
loss = f(x) + self.penalty * np.linalg.norm(
x - self.x_last, ord=self.norm)**self.order
else:
loss = f(x) + self.penalty * np.linalg.norm(
x - self.x_last, ord=self.norm, axis=1)**self.order
self.x_last = x
return loss
class HuberLoss(LossFunction):
"""This class defines the huber loss function.
Args:
Feature (numpy.ndarray): Features of the environment.
label (numpy.ndarray): Labels of the environment.
scale (float): Scale coefficient of the loss function.
Example:
::
import numpy as np
feature, label = np.random.rand(1000, 5), np.random.randint(2, size=1000)
func = HuberLoss(feature, label) # 1000 rounds, 5 dimension
"""
def __init__(self,
feature: np.ndarray = None,
label: np.ndarray = None,
threshold: float = 1.,
scale: float = 1.) -> None:
self.feature = feature
self.label = label
self.threshold = threshold
self.scale = scale
def __getitem__(self, t: int) -> Callable[[np.ndarray], float]:
return partial(self.func, t=t)
def func(self, x, t):
prediction = np.dot(self.feature[t], x)
if abs(prediction - self.label[t]) < self.threshold:
return self.scale * 1 / 2 * (prediction - self.label[t])**2
else:
return self.scale * (self.threshold * abs(prediction - self.label[t]) - self.threshold**2 / 2)