ContentsFoundations to FrontierBezhig

integration_code/solutions/dual.py

All lab code. Download this file.

"""W04: forward-mode automatic differentiation; each operation carries its tangent."""
from dataclasses import dataclass
import math


@dataclass(frozen=True)
class Dual:
    value: float
    tangent: float=0.

    def __post_init__(self):
        if not all(type(x) in (int,float) and math.isfinite(x)
                   for x in (self.value,self.tangent)):
            raise ValueError("finite real value and tangent required")

    @staticmethod
    def wrap(other):
        return other if isinstance(other,Dual) else Dual(other)

    def __add__(self,other):
        other=self.wrap(other)
        return Dual(self.value+other.value,self.tangent+other.tangent)
    __radd__=__add__

    def __neg__(self):return Dual(-self.value,-self.tangent)
    def __sub__(self,other):return self+(-self.wrap(other))
    def __rsub__(self,other):return self.wrap(other)+(-self)

    def __mul__(self,other):
        other=self.wrap(other)
        return Dual(self.value*other.value,self.tangent*other.value+self.value*other.tangent)
    __rmul__=__mul__

    def __truediv__(self,other):
        other=self.wrap(other)
        if other.value==0:raise ZeroDivisionError("zero primal denominator")
        value=self.value/other.value
        tangent=(self.tangent-value*other.tangent)/other.value
        return Dual(value,tangent)
    def __rtruediv__(self,other):return self.wrap(other)/self


def sin(x):
    x=Dual.wrap(x)
    return Dual(math.sin(x.value),math.cos(x.value)*x.tangent)


def exp(x):
    x=Dual.wrap(x);value=math.exp(x.value)
    return Dual(value,value*x.tangent)


def log(x):
    x=Dual.wrap(x)
    if x.value<=0:raise ValueError("log requires positive primal input")
    return Dual(math.log(x.value),x.tangent/x.value)


if __name__=='__main__':
    w=Dual(2,1);x=2;b=1;y=3
    loss=(w*x+b-y)*(w*x+b-y)/2
    print(loss)  # value 2; derivative with respect to w is 4