Manipulating BVars
The most important aspect of the usability of this library is allowing you to
seamlessly manipulate BVar s as as if they were just as, without requiring
you as the user to be able to recognize or acknowledge the difference. Here
are some techniques to that end.
Remember, a BVar s a is a BVar containing an a --- it's an a that, when
used, keeps track of and propagates your gradient.
Typeclass Interface
BVars have Num, Fractional, Floating, Eq, and Ord instances. These
instances are basically "lifted" to the BVar itself, so if you have a BVar s
Double, you can use (*), sqrt, (>), etc. on it exactly as if it were
just a Double.
Constant Values
If we don't care about a value's gradient, we can use auto:
auto :: a -> BVar s a
auto x basically gives you a BVar that contains just x alone. Useful for
using with functions that expect BVars, but you just have a specific value
you want to use.
Coercible
If a and b are Coercible, then so are BVar s a and BVar s b, using
the coerceVar function. This is useful for "unwrapping" and "wrapping"
BVars of newtypes:
newtype MyInt = MyInt Int
getMyInt :: BVar s MyInt -> BVar s Int
getMyInt = coerceVar
Accessing Contents
The following techniques can be used to access values inside BVars:
Traversable Containers
One that we saw earlier was sequenceVar, which we used to turn a BVar
containing a list into a list of BVars:
sequenceVar :: (Backprop a, Reifies s W)
=> BVar s [a]
-> [BVar s a]
If you have a BVar containing a list, you can get a list of BVars of all of
that list's elements. (sequenceVar actually works on all Traversable
instances, not just lists) This is very useful when combined with
-XViewPatterns, as seen earlier.
Records and Fields
In practice, a lot of usage involves functions involving contents of records or data types containing fields. The previous example, involving a simple ANN, demonstrates this:
data Net = N { _nWeights1 :: L 20 100
, _nBias1 :: R 20
, _nWeights2 :: L 5 20
, _nBias2 :: R 5
}
deriving (Show, Generic)
instance Backprop Net -- can be automatically defined
To compute the result of this network (ran on an R 100, a 100-vector) and get
the output R 5, we need do a matrix multiplication by the _nWeights1 field,
add the result to the _nBias1 field...basically, the result is a function of
linear algebra and related operations on the input and all of the contents of
the Net data type. However, you can't directly use _nWeights, since it
takes a Net, not BVar s Net. And you also can't directly pattern match on
the N constructor.
There are two main options for this: the lens interface, and the higher-kinded data interface.
Lens Interface
The most straightforward way to do this is the lens-based interface, using
viewVar or ^^..
If we make lenses for Net using the lens or microlens-th packages:
-- requires -XTemplateHaskell
makeLenses ''Net
or make them manually:
nBias1' :: Functor f => (R 20 -> f (R 20)) -> Net -> f Net
nBias1' f n = (\b -> n { _nBias1 = b }) <$> f (_nBias1 n)
then ^. from the lens or microlens packages lets you retrieve a field
from a Net:
(^. nWeights1) :: Net -> L 20 100
(^. nBias1 ) :: Net -> R 20
(^. nWeights2) :: Net -> L 5 20
(^. nBias2 ) :: Net -> R 5
And, ^^. from backprop (also aliased as viewVar) lets you do the same
thing from a BVar s Net (a BVar containing your Net):
(^^. nWeights1) :: BVar s Net -> BVar s (L 20 100)
(^^. nBias1 ) :: BVar s Net -> BVar s (R 20)
(^^. nWeights2) :: BVar s Net -> BVar s (L 5 20)
(^^. nBias2 ) :: BVar s Net -> BVar s (R 5)
With our lenses and ^^., we can write our network running function. This
time, I'll include the type!
runNet :: Reifies s W
=> BVar s Net
-> BVar s (R 100)
-> BVar s (R 5)
runNet net x = z
where
-- run first layer
y = logistic $ (net ^^. nWeights1) #> x + (net ^^. nBias1)
-- run second layer
z = logistic $ (net ^^. nWeights2) #> y + (net ^^. nBias2)
logistic :: Floating a => a -> a
logistic x = 1 / (1 + exp (-x))
Note that we are using versions of #> lifted for BVars, from the
hmatrix-backprop library:
(#>) :: BVar s (L m n) -> BVar s (R n) -> BVar s (R m)
Higher-Kinded Data Interface
Using the lens based interface, you can't directly pattern match and construct fields. To allow for directly pattern matching, there's another interface option involving the "Higher-Kinded Data" techniques described in this article.
If we had a type-family (that can be re-used for all of your data types):
type family HKD f a where
HKD Identity a = a
HKD f a = f a
We can define Net instead as:
data Met' f = M { _mWeights1 :: HKD f (L 20 100)
, _mBias1 :: HKD f (R 20)
, _mWeights2 :: HKD f (L 5 20)
, _mBias2 :: HKD f (R 5)
}
deriving Generic
Then our original type is:
type Met = Met' Identity
deriving instance Show Met
instance Backprop Met
Met is the same as Net in every way -- it can be pattern matched on to get
the L 20 100, etc. (the Identity disappears):
getMetBias1 :: Met -> R 20
getMetBias1 (M _ b _ _) = b
The benefit of this is that we can now directly pattern match on a BVar s Met
to get the internal fields as BVars using splitBV as a view pattern (or the
BV pattern synonym):
runMet :: Reifies s W
=> BVar s Met
-> BVar s (R 100)
-> BVar s (R 5)
runMet (splitBV -> M w1 b1 w2 b2) x = z
where
-- run first layer
y = logistic $ w1 #> x + b1
-- run second layer
z = logistic $ w2 #> y + b2
runMetPS :: Reifies s W
=> BVar s Met
-> BVar s (R 100)
-> BVar s (R 5)
runMetPS (BV (M w1 b1 w2 b2)) x = z
where
-- run first layer
y = logistic $ w1 #> x + b1
-- run second layer
z = logistic $ w2 #> y + b2
Now, the M w1 b1 w2 b2 pattern can be used to deconstruct both "normal"
Mets, as well as a BVar s Met (with splitBV or BV).
Note that this HKD access method is potentially less performant than lens access (by about 10-20%).
Potential or Many Fields
Some values "may" or "may not" have values of a given field. An example would
include the nth item in a list or vector, or the Just of a Maybe.
For these, the lens-based (prism-based/traversal-based) interface is the main way to access
partial fields. You can use (^^?) or previewVar with any Traversal:
(^?) :: a -> Traversal' a b -> Maybe b
(^^?) :: BVar s a -> Traversal' a b -> Maybe (BVar s b)
If the value in the BVar "has" that field, then you'll get a Just with the
BVar of that field's contents. If it doesn't, you'll get a Nothing.
You can use this with any prism or traversal, like using _head to get the
first item in a list if it exists.
If you have a type that might contain many values of a field (like a tree or
list), you can use (^^..) or toListOfVar, which works on any Traversal:
(^..) :: a -> Traversal' a b -> [ b]
(^^..) :: BVar s a -> Traversal' a b -> [BVar s b]
This can be used to implement sequenceVar, actually:
sequenceVar :: BVar s [a] -> [BVar s a]
sequenceVar xs = xs ^^.. traverse
Tuples
The T2 pattern synonym is provided, which allow you to pattern match on a
BVar s (a, b) to get a BVar s a and BVar s b. The T3 pattern is also
provided, which does the same thing for three-tuples.
Note that T2 and T3 are bidirectional pattern synonyms, and can be used to
construct as well as deconstruct.
Combining BVars
The following techniques can be used to "combine" BVars:
Foldable Containers
The "opposite" of sequenceVar is collectVar, which takes a foldable
container of BVars and returns a BVar containing that foldable container of
contents:
collectVar :: (Backprop a, Foldable t, Functor t, Reifies s W)
=> t (BVar s a)
-> BVar s (t a)
Constructors
Sometimes you would like to combine a bunch of BVars into a BVar of
specific container or data type.
isoVar
The simplest way to do this is using the isoVar, isoVar2, etc. family of
functions:
isoVar2
:: (Backprop a, Backprop b, Backprop c, Reifies s W)
=> (a -> b -> c)
-> (c -> (a, b))
-> BVar s a
-> BVar s b
-> BVar s c
So if we had a type like:
data DoubleInt = DI Double Int
We can combine a Double and Int into a DoubleInt using isoVar2:
isoVar2 DI (\(DI x y) -> (x,y))
:: Reifies s W
=> BVar s Double
-> BVar s Int
-> BVar s DoubleInt
Higher-Kinded Data Interface
You can also use the "Higher Kinded Data" interface, as well. For our
Met type above, you can use joinBV, or the BV pattern synonym:
makeMet :: Reifies s W
=> BVar s (L 20 100)
-> BVar s (R 20)
-> BVar s (L 5 20)
-> BVar s (R 5)
-> BVar s Met
makeMet w1 b1 w2 b2 = joinBV (M w1 b1 w2 b2)
Modifying fields
If you just want to "set" a specific field, you can use the lens-based
interface with (.~~) or setVar. For example, if we wanted to set the
_nWeights2 field of a Net to a new matrix, we can do:
myNet & nWeights2 .~~ newMatrix
or
setVar nWeights2
:: Reifies s W
=> BVar s (L 20 5)
-> BVar s Net
-> BVar s Net
You can also use (%~~) or overVar to apply a function to a specific
inside your value.
Prelude Modules
Finally, the Prelude.Backprop module has a lot of your normal Prelude
functions "lifted" to work on BVars of values. For many situations, these
aren't necessary, and normal Prelude functions will work just fine on BVars
of values (like (.)). However, it does have some convenient functions, like
minimum, foldl', fmap, toList, fromIntegral, realToFrac, etc.
lifted to work on BVars. This module is meant to be imported qualified.
Moving On
Now that you know all about BVars, you really can just jump into the
haddocks and start writing programs. The next section of this
documentation is more details about the Backprop typeclass.
