horde-ad-0.3.0.0: src/HordeAd/External/CommonRankedOps.hs
{-# LANGUAGE OverloadedLists #-}
-- | Commonly used ranked operations on tensors.
module HordeAd.External.CommonRankedOps
( module HordeAd.External.CommonRankedOps
) where
import Prelude
import Control.Exception.Assert.Sugar
import Data.Type.Equality (gcastWith, (:~:))
import Data.Vector.Generic qualified as V
import GHC.TypeLits (KnownNat)
import Data.Array.Nested.Convert (withShsFromShR)
import Data.Array.Nested.Ranked.Shape
import Data.Array.Nested.Shaped.Shape
import Data.Array.Nested.Types (unsafeCoerceRefl)
import HordeAd.Core.ConvertTensor
import HordeAd.Core.Ops (tD)
import HordeAd.Core.TensorKind
import HordeAd.Core.Types
import HordeAd.OpsTensor
-- This is not only ranked, so move it once we have CommonAnyOps.hs.
assumeEquality :: forall a b r. (a ~ b => r) -> r
assumeEquality = gcastWith (unsafeCoerceRefl :: a :~: b)
rminimum :: forall target n r.
( BaseTensor target, ConvertTensor target, LetTensor target
, NumScalar r )
=> target (TKR n r) -> target (TKScalar r)
rminimum t = tlet (rflatten t) $ \tf ->
rindex0 tf (tplainPart (kfromR (rargMin tf)) :.: ZIR)
rmaximum :: forall target n r.
( BaseTensor target, ConvertTensor target, LetTensor target
, NumScalar r )
=> target (TKR n r) -> target (TKScalar r)
rmaximum t = tlet (rflatten t) $ \tf ->
rindex0 tf (tplainPart (kfromR (rargMax tf)) :.: ZIR)
relu, reluLeaky
:: forall target n r.
(ADReady target, NumScalar r, KnownNat n, Differentiable r)
=> target (TKR n r) -> target (TKR n r)
relu v0 = tlet v0 $ \v ->
let oneIfGtZero = rmap0N (\x -> ifH (x <=. 0) 0.0 1.0) v
in oneIfGtZero * v
reluLeaky v0 = tlet v0 $ \v ->
let oneIfGtZero = rmap0N (\x -> ifH (x <=. 0) 0.01 1.0) v
in oneIfGtZero * v
logistic :: forall target r n.
( BaseTensor target, LetTensor target, BaseTensor (PrimalOf target)
, KnownNat n, NumScalar r, Differentiable r )
=> target (TKR n r) -> target (TKR n r)
logistic d0 = tlet d0 $ \d -> -- used in rprimalPart and in tdualPart
let one = rrepl (rshape d) 1
y0 = recip (one + exp (- rprimalPart d))
in tletPrimal y0 $ \y ->
rfromPrimal y + rfromDual (rScale @target (y * (one - y)) $ rdualPart d)
rsquare :: (NumScalar a, ADReady target)
=> target (TKR n a) -> target (TKR n a)
rsquare x' = tlet x' $ \x -> x * x
-- slower even symbolically: rsquare x = x ** rrepl (rshape x) 2
-- Optimized and more clearly written @u ** 2@. It's not clear if this is
-- currently faster than @u ** 2@ and in which pipelines, but it's different,
-- so useful as a test.
squareR :: forall target r n.
( BaseTensor target, LetTensor target
, KnownNat n, Num (PrimalOf target (TKR n r)), NumScalar r )
=> target (TKR n r) -> target (TKR n r)
squareR d' = tlet d' $ \d ->
let u = rprimalPart @target d
u' = rdualPart @target d
in tD knownSTK (u * u) (rScale @target (2 * u) u')
squaredDifference
:: forall target n r.
( BaseTensor target, LetTensor target
, KnownNat n, Num (PrimalOf target (TKR n r)), NumScalar r )
=> PrimalOf target (TKR n r) -> target (TKR n r) -> target (TKR n r)
squaredDifference targ res = squareR @target $ res - rfromPrimal @target targ
lossCrossEntropyV
:: (BaseTensor target, KnownNat n, NumScalar r, Differentiable r)
=> target (TKR n r) -> target (TKR n r) -> target (TKScalar r)
lossCrossEntropyV targ res = negate $ log res `rdot0` targ
-- | Note that this is equivalent to a composition of softMax and cross entropy
-- only when @expected@ is one-hot. Otherwise, results vary wildly. In our
-- rendering of the MNIST data all labels are one-hot.
lossSoftMaxCrossEntropyR
:: forall target n r.
( BaseTensor target, LetTensor target
, BaseTensor (PrimalOf target), ConvertTensor (PrimalOf target)
, LetTensor (PrimalOf target), KnownNat n, NumScalar r, Differentiable r )
=> PrimalOf target (TKR n r) -> target (TKR n r) -> target (TKScalar r)
lossSoftMaxCrossEntropyR expected d' = tlet d' $ \d ->
-- The following protects from underflows, overflows and exploding gradients
-- and is required by QuickCheck tests to avoid NaNs, etc., for argument
-- values we don't fully control.
-- See https://github.com/tensorflow/tensorflow/blob/5a566a7701381a5cf7f70fce397759483764e482/tensorflow/core/kernels/sparse_softmax_op.cc#L106
-- and https://github.com/tensorflow/tensorflow/blob/5a566a7701381a5cf7f70fce397759483764e482/tensorflow/core/kernels/xent_op.h
let u = rprimalPart d
expU' = exp (u - rreplicate0N (rshape u) (rminimum u))
in tletPrimal expU' $ \expU ->
let softMaxU0 =
let sumExpU = rsum0 expU
recipSum = recip sumExpU
in rreplicate0N (rshape u) recipSum * expU
in tletPrimal softMaxU0 $ \softMaxU ->
tD STKScalar
(negate $ log softMaxU `rdot0` expected)
-- TODO: avoid: log . exp
(kdualPart $ rfromPrimal (softMaxU - expected) `rdot0` d)
-- Fails for empty x'.
rlogsumexp :: forall n r target. (NumScalar r, Differentiable r, ADReady target)
=> target (TKR n r) -> target (TKScalar r)
rlogsumexp x' = tlet x' $ \x -> tlet (rmaximum x) $ \maxx ->
let shiftedx = x - rreplicate0N (rshape x) maxx
logged = log (rsum0 (exp shiftedx))
in logged + maxx
-- | No padding; remaining areas ignored.
maxPool1 :: ( BaseTensor target, ConvertTensor target, LetTensor target
, NumScalar r )
=> Int -> Int -> target (TKR 1 r) -> target (TKR 1 r)
maxPool1 ksize stride v =
let slices = [rslice i ksize v | i <- [0, stride .. rwidth v - ksize]]
k = (rwidth v - ksize) `div` stride
in rfromVectorLinear (k :$: ZSR) $ V.fromList $ map rmaximum slices
softMax1 :: ( BaseTensor target, LetTensor target
, NumScalar r, Differentiable r )
=> target (TKR n r) -> target (TKR n r)
softMax1 d =
let expU0 = exp d
in tlet expU0 $ \expU ->
rreplicate0N (rshape d) (recip $ rsum0 expU) * expU
-- | Unpadded full convolution, where the output image size is the same
-- as the input size.
--
-- BTW, the indexing lower bounds in the code are spurious,
-- so they get simplified away in the resulting AST program.
conv2dSame
:: (ADReady target, NumScalar r)
=> target (TKR 4 r) -> target (TKR 4 r) -> target (TKR 4 r)
conv2dSame arrK arrA =
let [nImgs, nCinpA, nAh, nAw] = rshape arrA
[nCout, nCinpK, nKh, nKw] = rshape arrK
nCinp = assert (nCinpA == nCinpK `blame` (nCinpA, nCinpK)) nCinpA
shB = [nImgs, nCout, nAh, nAw]
shK1 = [1, nCinp, nKh, nKw]
in rbuild shB $ \case
[iImg, iCout, iBh, iBw] ->
let arrAt = slicez shK1 arrA [iImg, 0, iBh, iBw]
arrKt = slicez shK1 arrK [iCout, 0, 0, 0]
in rfromK $ rdot0 arrAt arrKt
_ -> error "conv2dSame: impossible pattern needlessly required"
-- | Full convolution with only enough padding to ensure all output points
-- are affected by the same number of input points,
-- where the output size shrinks depending on the input size and kernel size.
-- Also no input points are ever ignored, though some are read less often.
--
-- This corresponds to
-- https://hackage.haskell.org/package/hmatrix-0.20.2/docs/Numeric-LinearAlgebra.html#v:corr2
conv2dShrinking
:: (ADReady target, NumScalar r)
=> target (TKR 4 r) -> target (TKR 4 r) -> target (TKR 4 r)
conv2dShrinking arrK arrA =
let [nImgs, nCinpA, nAh, nAw] = rshape arrA
[nCout, nCinpK, nKh, nKw] = rshape arrK
nCinp = assert (nCinpA == nCinpK `blame` (nCinpA, nCinpK)) nCinpA
shB = [nImgs, nCout, nAh - nKh + 1, nAw - nKw + 1]
shK1 = [1, nCinp, nKh, nKw]
in rbuild shB $ \case
[iImg, iCout, iBh, iBw] ->
let arrAt = slicez shK1 arrA [iImg, 0, iBh, iBw]
arrKt = slicez shK1 arrK [iCout, 0, 0, 0]
in rfromK $ rdot0 arrAt arrKt
_ -> error "conv2dShrinking: impossible pattern needlessly required"
-- | Full convolution with enough padding to apply kernels at all
-- positions that give non-zero results. This corresponds to
-- https://hackage.haskell.org/package/hmatrix-0.20.2/docs/Numeric-LinearAlgebra.html#v:conv2
-- though it doesn't do the kernel flipping.
conv2dPadded
:: (ADReady target, NumScalar r)
=> target (TKR 4 r) -> target (TKR 4 r) -> target (TKR 4 r)
conv2dPadded arrK arrA =
let [nImgs, nCinpA, nAh, nAw] = rshape arrA
[nCout, nCinpK, nKh, nKw] = rshape arrK
nCinp = assert (nCinpA == nCinpK `blame` (nCinpA, nCinpK)) nCinpA
shB = [nImgs, nCout, nAh + nKh - 1, nAw + nKw - 1]
shK1 = [1, nCinp, nKh, nKw]
in rbuild shB $ \case
[iImg, iCout, iBh, iBw] ->
let arrAt = slicez shK1 arrA [ iImg, 0
, iBh - fromIntegral nKh + 1
, iBw - fromIntegral nKw + 1 ]
arrKt = slicez shK1 arrK [iCout, 0, 0, 0]
in rfromK $ rdot0 arrAt arrKt
_ -> error "conv2dPadded: impossible pattern needlessly required"
-- | Full convolution with custom padding, where the output image size
-- depends on the input size, kernel size and padding.
conv2dCustomPadded
:: (ADReady target, NumScalar r)
=> (Int, Int) -> target (TKR 4 r) -> target (TKR 4 r) -> target (TKR 4 r)
conv2dCustomPadded (nPh, nPw) arrK arrA =
let [nImgs, nCinpA, nAh, nAw] = rshape arrA
[nCout, nCinpK, nKh, nKw] = rshape arrK
nCinp = assert (nCinpA == nCinpK `blame` (nCinpA, nCinpK)) nCinpA
nBh = nAh + 2 * nPh - nKh + 1
nBw = nAw + 2 * nPw - nKw + 1
shB = [nImgs, nCout, nBh, nBw]
shK1 = [1, nCinp, nKh, nKw]
in rbuild shB $ \case
[iImg, iCout, iBh, iBw] ->
let iFh = iBh - fromIntegral nPh
iFw = iBw - fromIntegral nPw
arrAt = slicez shK1 arrA [iImg, 0, iFh, iFw]
arrKt = slicez shK1 arrK [iCout, 0, 0, 0]
in rfromK $ rdot0 arrAt arrKt
_ -> error "conv2dCustomPadded: impossible pattern needlessly required"
-- | Slice a section out of a tensor,
-- given a base offset and shape of the section.
--
-- If the slice extends out side the source array then the corresponding
-- elements are set to zero.
slicez
:: (ADReady target, NumScalar r, KnownNat n)
=> IShR n -> target (TKR n r) -> IxROf target n -> target (TKR n r)
slicez shOut d ixBase =
rbuild shOut $ \ixResult -> rindex @_ @0 d (ixrZipWith (+) ixBase ixResult)
maxPool2dUnpadded
:: (ADReady target, NumScalar r)
=> Int -> Int -> target (TKR 4 r) -> target (TKR 4 r)
maxPool2dUnpadded ksize stride arr =
let [batch_size, channels, h, w] = rshape arr
shOutR :: IShR 4
shOutR = [batch_size, channels, h `div` stride, w `div` stride]
shK1 :: IShR 4
shK1 = [1, 1, ksize, ksize]
in
withShsFromShR shOutR $ \(sh :: ShS shOut) ->
withKnownShS sh $
rfromS $ kbuild @shOut $ \case
[iImg, iChan, iBh, iBw] ->
rmaximum $ slicez shK1 arr [ iImg, iChan
, fromIntegral stride * iBh
, fromIntegral stride * iBw ]
_ -> error "maxPool2dUnpadded: impossible pattern needlessly required"
xfromIndex0 :: forall r target.
(BaseTensor target, ConvertTensor target, NumScalar r)
=> IntOf target -> target (TKX '[] r)
xfromIndex0 = xfromIntegral . xfromK . tfromPlain STKScalar