packages feed

horde-ad-0.3.0.0: src/HordeAd/External/OptimizerTools.hs

-- | Tools for implementing (and debugging the use of) gradient descent schemes.
module HordeAd.External.OptimizerTools
  ( updateWithGradient
--  , gradientIsNil, minimumGradient, maximumGradient
  , ArgsAdam(..), defaultArgsAdam
  , StateAdam(..), initialStateAdam
  , updateWithGradientAdam
  ) where

import Prelude

import Data.Array.Nested qualified as Nested

import HordeAd.Core.CarriersConcrete
import HordeAd.Core.Ops
import HordeAd.Core.OpsConcrete ()
import HordeAd.Core.TensorKind
import HordeAd.Core.Types

updateWithGradient :: forall y.
                      Double -> SingletonTK y
                   -> Concrete y -> Concrete (ADTensorKind y)
                   -> Concrete y
updateWithGradient gamma stk p@(Concrete params)
                             g@(Concrete gradient) = case stk of
  STKScalar @r -> Concrete $
    ifDifferentiable @r
      (params - realToFrac gamma * gradient)
      params
  STKR _ (STKScalar @r) -> Concrete $
    ifDifferentiable @r
      (params - Nested.rreplicatePrim (Nested.rshape params)
                                      (realToFrac gamma)
                * gradient)
      params
  STKS _ (STKScalar @r) -> Concrete $
    ifDifferentiable @r
      (params - Nested.sreplicatePrim (Nested.sshape params)
                                      (realToFrac gamma)
                * gradient)
      params
  STKX _ (STKScalar @r) -> Concrete $
    ifDifferentiable @r
      (params - Nested.mreplicatePrim (Nested.mshape params)
                                      (realToFrac gamma)
                * gradient)
      params
  STKProduct stk1 stk2 ->
    tpair (updateWithGradient gamma stk1 (tproject1 p) (tproject1 g))
          (updateWithGradient gamma stk2 (tproject2 p) (tproject2 g))
  _ -> error "updateWithGradient: only non-nested arrays supported"
         -- we could support nested arrays, but some new unwinding code
         -- would be needed here and for Adam, with no existing use case

{- there are mostly for debugging
gradientIsNil :: (Eq r) => HVector Concrete -> Bool
gradientIsNil (HVector Concrete gradient0 gradientR) =
  V.all (== 0) gradient0
  && V.all isTensorDummyD gradientR

minimumGradient :: (Ord r) => HVector Concrete -> r
minimumGradient (HVector Concrete gradient0 gradientR) =
  min (if V.null gradient0 then 0 else LA.minElement gradient0)
      (if V.null gradientR then 0
       else V.minimum (V.map OR.minimumA gradientR))

maximumGradient :: (Ord r) => HVector Concrete -> r
maximumGradient (HVector Concrete gradient0 gradientR) =
  max (if V.null gradient0 then 0 else LA.maxElement gradient0)
      (if V.null gradientR then 0
       else V.maximum (V.map OR.maximumA gradientR))
-}

data ArgsAdam = ArgsAdam
  { alpha   :: Double
  , betaOne :: Double
  , betaTwo :: Double
  , epsilon :: Double
  }

-- The defaults taken from
-- https://www.tensorflow.org/api_docs/python/tf/keras/optimizers/Adam
defaultArgsAdam :: ArgsAdam
defaultArgsAdam = ArgsAdam
  { alpha = 0.001
  , betaOne = 0.9
  , betaTwo = 0.999
  , epsilon = 1e-7
  }

type family Triplify y where
  Triplify (TKScalar r) =
    TKProduct (TKProduct (TKScalar r) (TKScalar r)) (TKScalar r)
  Triplify (TKR n r) = TKProduct (TKProduct (TKR n r) (TKR n r)) (TKR n r)
  Triplify (TKS sh r) = TKProduct (TKProduct (TKS sh r) (TKS sh r)) (TKS sh r)
  Triplify (TKX sh r) = TKProduct (TKProduct (TKX sh r) (TKX sh r)) (TKX sh r)
  Triplify (TKProduct x z) = TKProduct (Triplify x) (Triplify z)

unzip3Rep
  :: SingletonTK y -> Concrete (Triplify y)
  -> (Concrete y, Concrete y, Concrete y)
unzip3Rep stk (Concrete t) = case stk of
  STKScalar ->
    (Concrete $ fst $ fst t, Concrete $ snd $ fst t, Concrete $ snd t)
  STKR _ STKScalar ->
    (Concrete $ fst $ fst t, Concrete $ snd $ fst t, Concrete $ snd t)
  STKS _ STKScalar ->
    (Concrete $ fst $ fst t, Concrete $ snd $ fst t, Concrete $ snd t)
  STKX _ STKScalar ->
    (Concrete $ fst $ fst t, Concrete $ snd $ fst t, Concrete $ snd t)
  STKProduct stk1 stk2 ->
    let !(!a1, !b1, !c1) = unzip3Rep stk1 $ Concrete $ fst t
        !(!a2, !b2, !c2) = unzip3Rep stk2 $ Concrete $ snd t
    in ( Concrete (unConcrete a1, unConcrete a2)
       , Concrete (unConcrete b1, unConcrete b2)
       , Concrete (unConcrete c1, unConcrete c2))
  _ -> error "unzip3Rep: only non-nested arrays supported"

type role StateAdam nominal
data StateAdam y = StateAdam
  { tAdam :: Int  -- iteration count
  , mAdam :: Concrete y
  , vAdam :: Concrete y
  }

-- TODO: introduce and use something like TensorOrZero
initialStateAdam :: FullShapeTK y -> StateAdam y
initialStateAdam ftk =
  StateAdam { tAdam = 0
            , mAdam = tdefTarget ftk
            , vAdam = tdefTarget ftk
            }

updateWithGradientAdam
  :: ArgsAdam -> StateAdam y -> SingletonTK y
  -> Concrete y -> Concrete (ADTensorKind y)
  -> (Concrete y, StateAdam y)
updateWithGradientAdam ArgsAdam{..} StateAdam{..} stk0 paramsR gradientR =
  let mAdamR = mAdam
      vAdamR = vAdam
      tAdamNew = tAdam + 1
      oneMinusBeta1 = 1 - betaOne
      oneMinusBeta2 = 1 - betaTwo
      updateR :: (Fractional r, Nested.FloatElt r, Nested.PrimElt r)
              => Nested.Ranked n r -> Nested.Ranked n r
              -> Nested.Ranked n r -> Nested.Ranked n r
              -> (Nested.Ranked n r, Nested.Ranked n r, Nested.Ranked n r)
      updateR mA vA p g =
        let sh = Nested.rshape g
            mANew = Nested.rreplicatePrim sh (realToFrac betaOne) * mA
                    + Nested.rreplicatePrim sh (realToFrac oneMinusBeta1) * g
            vANew = Nested.rreplicatePrim sh (realToFrac betaTwo) * vA
                    + Nested.rreplicatePrim sh (realToFrac oneMinusBeta2)
                      * (g * g)
            alphat = alpha * sqrt (1 - betaTwo ^ tAdamNew)
                             / (1 - betaOne ^ tAdamNew)
        in ( mANew
           , vANew
           , p - (Nested.rreplicatePrim sh (realToFrac alphat) * mANew)
                 / (sqrt vANew
                    + Nested.rreplicatePrim sh (realToFrac epsilon)) )
      updateProd :: forall y2.
                    SingletonTK y2
                 -> Concrete y2 -> Concrete y2
                 -> Concrete y2 -> Concrete (ADTensorKind y2)
                 -> Concrete (Triplify y2)
      updateProd stk (Concrete mA) (Concrete vA)
                     (Concrete p) (Concrete g) = case stk of
        STKScalar @r ->
          ifDifferentiable @r
            (let !(!mAN, !vAN, !pN) =
                   updateR (Nested.rscalar mA)
                           (Nested.rscalar vA)
                           (Nested.rscalar p)
                           (Nested.rscalar g)
             in Concrete
                (( Nested.runScalar mAN
                 , Nested.runScalar vAN )
                , Nested.runScalar pN ))
            (Concrete ((mA, vA), p))
        STKR SNat (STKScalar @r) ->
          ifDifferentiable @r
            (let !(!mAN, !vAN, !pN) = updateR mA vA p g
             in Concrete ((mAN, vAN), pN))
            (Concrete ((mA, vA), p))
        STKS sh (STKScalar @r) ->
          ifDifferentiable @r
            (let !(!mAN, !vAN, !pN) =
                   updateR (Nested.stoRanked mA)
                           (Nested.stoRanked vA)
                           (Nested.stoRanked p)
                           (Nested.stoRanked g)
             in Concrete
                ( ( Nested.rcastToShaped mAN sh
                  , Nested.rcastToShaped vAN sh )
                , Nested.rcastToShaped pN sh ))
            (Concrete ((mA, vA), p))
        STKX _ (STKScalar @r) ->
          ifDifferentiable @r
            (let !(!mAN, !vAN, !pN) =
                   updateR (Nested.mtoRanked mA)
                           (Nested.mtoRanked vA)
                           (Nested.mtoRanked p)
                           (Nested.mtoRanked g)
             in Concrete
                ( ( Nested.mreshape (Nested.mshape mA)
                    $ Nested.rtoMixed mAN
                  , Nested.mreshape (Nested.mshape vA)
                    $ Nested.rtoMixed vAN )
                , Nested.mreshape (Nested.mshape p)
                  $ Nested.rtoMixed pN ))
            (Concrete ((mA, vA), p))
        STKProduct stk1 stk2 ->
          let !a1 = unConcrete $ updateProd stk1
                      (Concrete $ fst mA) (Concrete $ fst vA)
                      (Concrete $ fst p) (Concrete $ fst g)
              !a2 = unConcrete $ updateProd stk2
                      (Concrete $ snd mA) (Concrete $ snd vA)
                      (Concrete $ snd p) (Concrete $ snd g)
          in Concrete (a1, a2)
        _ -> error "updateWithGradientAdam: only non-nested arrays supported"
               -- we could support nested arrays, but some new unwinding code
               -- woudl be needed, with no existing use case
      (!mAdamRNew, !vAdamRNew, !paramsRNew) =
        unzip3Rep stk0 $ updateProd stk0 mAdamR vAdamR paramsR gradientR
  in ( paramsRNew
     , StateAdam
         { tAdam = tAdamNew
         , mAdam = mAdamRNew
         , vAdam = vAdamRNew
         }
     )