synapse-0.1.0.0: src/Synapse/Tensors/Mat.hs
{- | Implementation of matrix.
Matrices are a backbone of any machine learning library,
since most of the operations are implemented by the matrices
combinations (matrix multiplication, elementwise operations).
'Mat' datatype provides interface for all of those operations.
-}
-- 'TypeFamilies' are needed to instantiate 'Indexable', 'ElementwiseScalarOps', 'SingletonOps', 'MatOps' typeclasses.
{-# LANGUAGE TypeFamilies #-}
module Synapse.Tensors.Mat
( -- * 'Mat' datatype and simple getters.
Mat (nRows, nCols)
, nElements
, size
, isTransposed
, isSubmatrix
-- * Utility
, force
, toLists
-- * Constructors
, empty
, singleton
, fromList
, fromLists
, generate
, replicate
-- * Vec operations
, rowVec
, colVec
, fromVec
, indexRow
, indexCol
, safeIndexRow
, safeIndexCol
, diagonal
, flatten
-- * Combining
, map
, mapRow
, mapCol
, for
, imap
, elementwise
-- * Operations with matrices
, setSize
, extend
, shrink
, swapRows
, swapCols
, transpose
-- * Submatrices
, minor
, submatrix
, split
, join
, (<|>)
, (<->)
-- * Mathematics
, zeroes
, ones
, identity
, adamarMul
, matMul
, det
, rref
, inverse
, orthogonalized
) where
import Synapse.Tensors (DType, Indexable(..), (!), ElementwiseScalarOps(..), SingletonOps(..), MatOps(..))
import Synapse.Tensors.Vec (Vec(Vec))
import qualified Synapse.Tensors.Vec as SV
import Prelude hiding (map, replicate, zip)
import Data.Foldable (Foldable(..))
import Data.List (find)
import Data.Tuple (swap)
import qualified Data.Vector as V
{- | Mathematical matrix (collection of elements).
This implementation focuses on sharing parts of matrices and clever indexation
to reduce overhead of several essential operations.
Those include splitting matrices into submatrices, transposing - their asymptotical complexity becomes O(1).
However there are few downsides:
the first is that severely splitted matrix is hard to garbage collect and is not cache-friendly
and the second is that mass traversal operations on those sparse matrices might not fuse and combine well.
'force' function and some functions that by their nature are forced address those issues,
but most of the time those problems are not significant enough and you are just better using convenient functions instead of workarounds.
-}
data Mat a = Mat
{ nRows :: {-# UNPACK #-} !Int -- ^ Number of rows.
, nCols :: {-# UNPACK #-} !Int -- ^ Number of columns.
, rowStride :: {-# UNPACK #-} !Int -- ^ How much increasing row index affects true indexing.
, colStride :: {-# UNPACK #-} !Int -- ^ How much increasing column index affects true indexing.
, rowOffset :: {-# UNPACK #-} !Int -- ^ Row offset (from which row index does the matrix actually start).
, colOffset :: {-# UNPACK #-} !Int -- ^ Column offset (from which column index does the matrix actually start).
, storage :: V.Vector a -- ^ Internal storage (elements are stored in a vector using row-major ordering).
}
-- | Number of elements in a matrix.
nElements :: Mat a -> Int
nElements mat = nRows mat * nCols mat
-- | Size of matrix.
size :: Mat a -> (Int, Int)
size mat = (nRows mat, nCols mat)
-- | Returns whether the matrix is transposed. If the matrix consists of only one element, it is considered never transposed.
isTransposed :: Mat a -> Bool
isTransposed mat = colStride mat /= 1 && rowStride mat == 1
-- | Returns whether the matrix is a submatrix from another matrix.
isSubmatrix :: Mat a -> Bool
isSubmatrix mat = any (0 /=) [rowOffset mat, colOffset mat]
-- | Converts two dimensional matrix index to one dimensional vector index.
indexMatToVec :: Mat a -> (Int, Int) -> Int
indexMatToVec (Mat _ _ rk ck r0 c0 _) (r, c) = (r0 + r) * rk + (c0 + c) * ck
-- | Converts one dimensional vector index to two dimensional matrix index.
indexVecToMat :: Mat a -> Int -> (Int, Int)
indexVecToMat mat@(Mat _ _ rk ck r0 c0 _) i = let t = isTransposed mat
(r', c') = (if t then swap else id) $ quotRem i (if t then ck else rk)
in (r' - r0, c' - c0)
-- | Copies matrix data dropping any extra memory that may be held if given matrix is a submatrix.
force :: Mat a -> Mat a
force x = Mat (nRows x) (nCols x) (nCols x) 1 0 0 (V.fromList [unsafeIndex x (r, c) | r <- [0 .. nRows x - 1], c <- [0 .. nCols x - 1]])
-- | Converts matrix to list of lists.
toLists :: Mat a -> [[a]]
toLists x = [[unsafeIndex x (r, c) | c <- [0 .. nCols x - 1] ] | r <- [0 .. nRows x - 1]]
-- Typeclasses
instance Show a => Show (Mat a) where
show mat = "(" ++ show (size mat) ++ "): " ++ show (toLists mat)
type instance DType (Mat a) = a
instance Indexable (Mat a) where
type Index (Mat a) = (Int, Int)
unsafeIndex x (r, c) = V.unsafeIndex (storage x) (indexMatToVec x (r, c))
(!) x (r, c)
| r < 0 || r >= nRows x || c < 0 || c >= nCols x = error $ "Index " ++ show (r, c) ++ " is out of bounds for matrix with size " ++ show (size x)
| otherwise = unsafeIndex x (r, c)
(!?) x (r, c)
| r < 0 || r >= nRows x || c < 0 || c >= nCols x = Nothing
| otherwise = Just $ unsafeIndex x (r, c)
instance Num a => Num (Mat a) where
(+) = elementwise (+)
(-) = elementwise (-)
negate = fmap (0 -)
(*) = adamarMul
abs = fmap abs
signum = fmap signum
fromInteger = singleton . fromInteger
instance Fractional a => Fractional (Mat a) where
(/) = elementwise (/)
recip = fmap (1/)
fromRational = singleton . fromRational
instance Floating a => Floating (Mat a) where
pi = singleton pi
(**) = elementwise (**)
sqrt = fmap sqrt
exp = fmap exp
log = fmap log
sin = fmap sin
cos = fmap cos
asin = fmap asin
acos = fmap acos
atan = fmap atan
sinh = fmap sinh
cosh = fmap cosh
asinh = fmap asinh
acosh = fmap acosh
atanh = fmap atanh
instance ElementwiseScalarOps (Mat a) where
(+.) x n = fmap (+ n) x
(-.) x n = fmap (subtract n) x
(*.) x n = fmap (* n) x
(/.) x n = fmap (/ n) x
(**.) x n = fmap (** n) x
elementsMin x n = fmap (min n) x
elementsMax x n = fmap (max n) x
instance SingletonOps (Mat a) where
singleton x = Mat 1 1 1 1 0 0 (V.singleton x)
isSingleton mat = nElements mat == 1
unSingleton mat
| not $ isSingleton mat = error "Matrix is not a singleton"
| otherwise = unsafeIndex mat (0, 0)
extendSingleton mat reference = extend mat (unSingleton mat) (size reference)
elementsSum mat@(Mat rows cols _ _ _ _ _) = singleton $ sum [unsafeIndex mat (r, c) | r <- [0 .. rows - 1], c <- [0 .. cols - 1]]
elementsProduct mat@(Mat rows cols _ _ _ _ _) = singleton $ product [unsafeIndex mat (r, c) | r <- [0 .. rows - 1], c <- [0 .. cols - 1]]
mean x = elementsSum x /. fromIntegral (nElements x)
norm x = sqrt $ elementsSum $ x * x
instance Eq a => Eq (Mat a) where
(==) a@(Mat rows1 cols1 _ _ _ _ _) b@(Mat rows2 cols2 _ _ _ _ _)
| rows1 /= rows2 || cols1 /= cols2 = False
| otherwise = and [unsafeIndex a (r, c) == unsafeIndex b (r, c) | r <- [0 .. rows1 - 1], c <- [0 .. cols1 - 1]]
instance Functor Mat where
fmap f (Mat rows cols rk ck r0 c0 x) = Mat rows cols rk ck r0 c0 (fmap f x)
(<$) = fmap . const
instance Applicative Mat where
pure x = Mat 1 1 1 1 0 0 (V.singleton x)
(<*>) = elementwise (\f x -> f x)
instance Foldable Mat where
foldr f x = V.foldr f x . storage
foldl f x = V.foldl f x . storage
foldr' f x = V.foldr' f x . storage
foldl' f x = V.foldl' f x . storage
foldr1 f = V.foldr1 f . storage
foldl1 f = V.foldl1 f . storage
toList = V.toList . storage
null x = nElements x == 0
length = nElements
instance Traversable Mat where
sequenceA mat@(Mat rows cols _ _ _ _ _) = fmap (Mat rows cols cols 1 0 0) . sequenceA . storage $ force mat
-- Constructors
-- | Creates empty 'Mat'.
empty :: Mat a
empty = Mat 0 0 0 0 0 0 V.empty
-- | Creates 'Mat' from list (will throw an error, if elements of that list do not form a matrix of given size).
fromList :: (Int, Int) -> [a] -> Mat a
fromList (rows, cols) xs
| V.length m /= rows * cols = error "Given dimensions do not match with list length"
| otherwise = Mat rows cols cols 1 0 0 m
where
m = V.fromList xs
-- | Creates 'Mat' from list of lists (alias for @fromLists (rows, cols) (concat xs)@).
fromLists :: (Int, Int) -> [[a]] -> Mat a
fromLists (rows, cols) xs = fromList (rows, cols) (concat xs)
-- | Creates 'Mat' of given size using generating function.
generate :: (Int, Int) -> ((Int, Int) -> a) -> Mat a
generate (rows, cols) f = Mat rows cols cols 1 0 0 (V.generate (rows * cols) (f . flip quotRem cols))
-- | Creates 'Mat' of given size filled with given element.
replicate :: (Int, Int) -> a -> Mat a
replicate (rows, cols) x = Mat rows cols cols 1 0 0 (V.replicate (rows * cols) x)
-- Vec operations
-- | Converts 'Synapse.Tensors.Vec.Vec' to a one row 'Mat'.
rowVec :: Vec a -> Mat a
rowVec (Vec x) = Mat 1 (V.length x) (V.length x) 1 0 0 x
-- | Converts 'Synapse.Tensors.Vec.Vec' to a one column 'Mat'.
colVec :: Vec a -> Mat a
colVec (Vec x) = Mat (V.length x) 1 1 1 0 0 x
-- | Initializes 'Mat' from given 'Synapse.Tensors.Vec.Vec'.
fromVec :: (Int, Int) -> Vec a -> Mat a
fromVec (rows, cols) (Vec x)
| V.length x /= rows * cols = error "Given dimensions do not match with vector length"
| otherwise = Mat rows cols cols 1 0 0 x
-- | Extracts row from 'Mat'. If row is not present, an error is thrown.
indexRow :: Mat a -> Int -> Vec a
indexRow mat@(Mat rows cols _ _ _ _ x) r
| r < 0 || r >= rows = error "Given row is not present in the matrix"
| isTransposed mat = Vec $ V.fromList [unsafeIndex mat (r, c) | c <- [0 .. cols - 1]]
| otherwise = Vec $ V.slice (indexMatToVec mat (r, 0)) cols x
-- | Extracts column from 'Mat'. If column is not present, an error is thrown.
indexCol :: Mat a -> Int -> Vec a
indexCol mat@(Mat rows cols _ _ _ _ x) c
| c < 0 || c >= cols = error "Given column is not present in the matrix"
| isTransposed mat = Vec $ V.slice (indexMatToVec mat (0, c)) rows x
| otherwise = Vec $ V.fromList [unsafeIndex mat (r, c) | r <- [0 .. rows - 1]]
-- | Extracts row from 'Mat'.
safeIndexRow :: Mat a -> Int -> Maybe (Vec a)
safeIndexRow mat@(Mat rows cols _ _ _ _ x) r
| r < 0 || r >= rows = Nothing
| isTransposed mat = Just $ Vec $ V.fromList [unsafeIndex mat (r, c) | c <- [0 .. cols - 1]]
| otherwise = Just $ Vec $ V.slice (indexMatToVec mat (r, 0)) cols x
-- | Extracts column from 'Mat'.
safeIndexCol :: Mat a -> Int -> Maybe (Vec a)
safeIndexCol mat@(Mat rows cols _ _ _ _ x) c
| c < 0 || c >= cols = Nothing
| isTransposed mat = Just $ Vec $ V.slice (indexMatToVec mat (0, c)) rows x
| otherwise = Just $ Vec $ V.fromList [unsafeIndex mat (r, c) | r <- [0 .. rows - 1]]
-- | Extracts diagonal from 'Mat'.
diagonal :: Mat a -> Vec a
diagonal x = Vec $ V.fromList [unsafeIndex x (n, n) | n <- [0 .. min (nRows x) (nCols x) - 1]]
-- | Flattens 'Mat' to a 'Vec'.
flatten :: Mat a -> Vec a
flatten = Vec . storage . force
-- Combining
-- | Applies function to every element of 'Mat'.
map :: (a -> b) -> Mat a -> Mat b
map = fmap
-- | Applies function to a given row. If new 'Synapse.Tensors.Vec.Vec' is longer then кщц, it is truncated.
mapRow :: Int -> (Vec a -> Vec a) -> Mat a -> Mat a
mapRow row f mat = let newRow = f $ indexRow mat row
in imap (\(r, c) x -> if r == row then unsafeIndex newRow c else x) mat
-- | Applies function to a given column. If new 'Synapse.Tensors.Vec.Vec' is longer then column, it is truncated.
mapCol :: Int -> (Vec a -> Vec a) -> Mat a -> Mat a
mapCol col f mat = let newCol = f $ indexCol mat col
in imap (\(r, c) x -> if c == col then unsafeIndex newCol r else x) mat
-- | Flipped 'map'.
for :: Mat a -> (a -> b) -> Mat b
for = flip map
-- | Applies function to every element and its position of 'Mat'.
imap :: ((Int, Int) -> a -> b) -> Mat a -> Mat b
imap f mat@(Mat rows cols rk ck r0 c0 x) = Mat rows cols rk ck r0 c0 (V.imap (f . indexVecToMat mat) x)
-- | Zips two 'Mat's together using given function.
elementwise :: (a -> b -> c) -> Mat a -> Mat b -> Mat c
elementwise f a b
| size a /= size b = error "Two matrices have different sizes"
| otherwise = let (rows, cols) = size a
in Mat rows cols cols 1 0 0 $
V.fromList [f (unsafeIndex a (r, c)) (unsafeIndex b (r, c)) | r <- [0 .. rows - 1], c <- [0 .. cols - 1]]
-- Operations with matrices
-- | Sets new size for a matrix relative to top left corner and uses given element for new entries if the matrix is extended.
setSize :: Mat a -> a -> (Int, Int) -> Mat a
setSize mat x = flip generate $ \(r, c) -> if r < nRows mat && c < nCols mat then unsafeIndex mat (r, c) else x
-- | Extends matrix size relative to top left corner using given element for new entries. The matrix is never reduced in size.
extend :: Mat a -> a -> (Int, Int) -> Mat a
extend mat x (rows, cols) = setSize mat x (max (nRows mat) rows, max (nCols mat) cols)
-- | Shrinks matrix size relative to top left corner. The matrix is never extended in size.
shrink :: Mat a -> (Int, Int) -> Mat a
shrink mat (rows, cols) = setSize mat undefined (min (nRows mat) rows, min (nCols mat) cols)
-- | Swaps two rows.
swapRows :: Mat a -> Int -> Int -> Mat a
swapRows mat@(Mat rows cols _ _ _ _ _) row1 row2
| row1 < 0 || row2 < 0 || row1 >= rows || row2 >= rows = error "Given row indices are out of bounds"
| otherwise = generate (rows, cols) $ \(r, c) -> if r == row1 then unsafeIndex mat (row2, c)
else if r == row2 then unsafeIndex mat (row1, c)
else unsafeIndex mat (r, c)
-- | Swaps two columns.
swapCols :: Mat a -> Int -> Int -> Mat a
swapCols mat@(Mat rows cols _ _ _ _ _) col1 col2
| col1 < 0 || col2 < 0 || col1 >= cols || col2 >= cols = error "Given column indices are out of bounds"
| otherwise = generate (rows, cols) $ \(r, c) -> if c == col1 then unsafeIndex mat (r, col2)
else if c == col2 then unsafeIndex mat (r, col1)
else unsafeIndex mat (r, c)
-- Submatrices
-- | Extacts minor matrix, skipping given row and column.
minor :: Mat a -> (Int, Int) -> Mat a
minor mat@(Mat rows cols _ _ _ _ _) (r', c') = Mat (rows - 1) (cols - 1) (cols - 1) 1 0 0 $
V.fromList [unsafeIndex mat (r, c) | r <- [0 .. rows - 1], c <- [0 .. cols - 1], r /= r' && c /= c']
-- | Extracts submatrix, that is located between given two positions.
submatrix :: Mat a -> ((Int, Int), (Int, Int)) -> Mat a
submatrix (Mat rows cols rk ck r0 c0 x) ((r1, c1), (r2, c2))
| r1 < 0 || c1 < 0 || r2 < 0 || c2 < 0 ||
r2 < r1 || r2 > rows || c2 < c2 || c2 > cols = error "Given row and column limits are incorrect"
| otherwise = Mat (r2 - r1) (c2 - c1) rk ck (r0 + r1) (c0 + c1) x
-- | Splits matrix into 4 parts, given position is a pivot, that corresponds to first element of bottom-right subpart.
split :: Mat a -> (Int, Int) -> (Mat a, Mat a, Mat a, Mat a)
split x (r, c) = (submatrix x ((0, 0), (r, c)), submatrix x ((0, c), (r, nCols x)),
submatrix x ((r, 0), (nRows x, c)), submatrix x ((r, c), (nRows x, nCols x)))
-- | Joins 4 blocks of matrices.
join :: (Mat a, Mat a, Mat a, Mat a) -> Mat a
join (tl@(Mat rowsTL colsTL _ _ _ _ _), tr@(Mat rowsTR colsTR _ _ _ _ _),
bl@(Mat rowsBL colsBL _ _ _ _ _), br@(Mat rowsBR colsBR _ _ _ _ _))
| rowsTL /= rowsTR || rowsBL /= rowsBR ||
colsTL /= colsBL || colsTR /= colsBR = error "Matrices dimensions do not match"
| otherwise = generate (rowsTL + rowsBL, colsTL + colsTR) $
\(r, c) -> uncurry unsafeIndex $
if r >= rowsTL && c >= colsTL then (br, (r - rowsTL, c - colsTL))
else if r >= rowsTL then (bl, (r - rowsTL, c))
else if c >= colsTL then (tr, (r, c - colsTL))
else (tl, (r, c))
infixl 9 <|>
-- | Joins two matrices horizontally.
(<|>) :: Mat a -> Mat a -> Mat a
(<|>) a@(Mat rows1 cols1 _ _ _ _ _) b@(Mat rows2 cols2 _ _ _ _ _)
| rows1 /= rows2 = error "Given matrices must have the same number of rows"
| otherwise = generate (rows1, cols1 + cols2) $ \(r, c) -> if c < cols1
then unsafeIndex a (r, c)
else unsafeIndex b (r, c - cols1)
infixl 9 <->
-- | Joins two matrices vertically.
(<->) :: Mat a -> Mat a -> Mat a
(<->) a@(Mat rows1 cols1 _ _ _ _ _) b@(Mat rows2 cols2 _ _ _ _ _)
| cols1 /= cols2 = error "Given matrices must have the same number of columns"
| otherwise = generate (rows1 + rows2, cols1) $ \(r, c) -> if r < rows1
then unsafeIndex a (r, c)
else unsafeIndex b (r - rows1, c)
-- Functions that work on mathematical matrix (type constraint refers to a number)
-- | Creates 'Mat' that is filled with zeroes.
zeroes :: Num a => (Int, Int) -> Mat a
zeroes = flip replicate 0
-- | Creates 'Mat' that is filled with ones.
ones :: Num a => (Int, Int) -> Mat a
ones = flip replicate 1
-- | Creates identity matrix.
identity :: Num a => Int -> Mat a
identity n = generate (n, n) $ \(r, c) -> if r == c then 1 else 0
-- | Adamar multiplication (elementwise multiplication).
adamarMul :: Num a => Mat a -> Mat a -> Mat a
adamarMul = elementwise (*)
instance Num a => MatOps (Mat a) where
transpose (Mat rows cols rk ck r0 c0 x) = Mat cols rows ck rk c0 r0 x
addMatRow mat row
| nRows row /= 1 = error "Given row matrix is not a row"
| nCols row /= nCols mat = error "Number of columns does not match"
| otherwise = imap (\(_, c) -> (+ unsafeIndex row (0, c))) mat
matMul a@(Mat rows1 cols1 _ _ _ _ _) b@(Mat rows2 cols2 _ _ _ _ _)
| cols1 /= rows2 = error "Matrices dimensions do not match"
| otherwise = generate (rows1, cols2) $ \(r, c) -> unSingleton $ indexRow a r `SV.dot` indexCol b c
-- | Determinant of a square matrix. If matrix is empty, zero is returned.
det :: Num a => Mat a -> a
det mat@(Mat rows cols _ _ _ _ _)
| rows /= cols = error "Matrix is not square"
| nElements mat == 0 = 0
| nElements mat == 1 = unsafeIndex mat (0, 0)
| otherwise = let mat' = if isTransposed mat then transpose mat else mat
determinant :: Num a => Mat a -> a
determinant x@(Mat 2 _ _ _ _ _ _) = unsafeIndex x (0, 0) * unsafeIndex x (1, 1) -
unsafeIndex x (1, 0) * unsafeIndex x (0, 1)
determinant x = sum [((-1) ^ i) * (indexRow x 0 ! i) * determinant (minor x (0, i))
| i <- [0 .. nRows x - 1]]
in determinant mat'
-- | Row reduced echelon form of matrix.
rref :: (Eq a, Fractional a) => Mat a -> Mat a
rref mat@(Mat rows cols _ _ _ _ _) = go mat 0 [0 .. rows - 1]
where
go m _ [] = m
go m lead (r:rs) = case find ((0 /=) . unsafeIndex m) [(i, j) | j <- [lead .. cols - 1], i <- [r .. rows - 1]] of
Nothing -> m
Just (pivotRow, lead') -> let newRow = SV.map (/ unsafeIndex m (pivotRow, lead')) (indexRow m pivotRow)
m' = swapRows m pivotRow r
m'' = mapRow r (const newRow) m'
m''' = imap (\(row, c) -> if row == r
then id
else subtract (newRow ! c * unsafeIndex m'' (row, lead'))
) m''
in go m''' (lead' + 1) rs
-- | Inverse of a square matrix. If given matrix is empty, empty matrix is returned.
inverse :: (Eq a, Fractional a) => Mat a -> Maybe (Mat a)
inverse mat@(Mat rows cols _ _ _ _ _)
| rows /= cols = error "Matrix is not square"
| nElements mat == 0 = Just empty
| otherwise = let mat' = mat <|> identity rows
reduced = rref mat'
(left, right, _, _) = split reduced (rows, cols)
in case V.find (== 0) (SV.unVec $ diagonal left) of
Nothing -> Just right
Just _ -> Nothing
-- | Orthogonalizes matrix by rows using Gram-Schmidt algorithm.
orthogonalized :: Floating a => Mat a -> Mat a
orthogonalized mat = foldl' (\mat' row -> mapRow row SV.normalized $ iterationGramSchmidt mat' row) mat [0 .. nRows mat]
where
iterationGramSchmidt mat' row = foldl' (\mat'' r ->
mapRow row (subtract $ indexRow mat'' r *. unSingleton (indexRow mat'' r `SV.dot` indexRow mat'' row)) mat''
) mat' [0 .. row]