Note for Github users: development is happening on Gitlab please go there to submit any issue or merge request.
A small lib allowing to manipulate matrices.
This is not intended to exhaustively implements all possible mathematical operations on matrices. Instead, it's just what I needed to build a neural network. That being said, it provides build blocks to easily extend it (see Extending
section). If you feel some operation should be in there, feel free to send a pull request.
go get github.com/oelmekki/matrix
You can generate an initialized zero matrix providing its number of rows and cols:
matrix := GenerateMatrix(3, 2)
Or, you can use the builder to provide matrix values in an human readable way.
Two types are provided to build Matrix
: Builder
and Row
.
myMatrix, err := matrix.Build(
matrix.Builder{
matrix.Row{10.0, -5.3, 22.0},
matrix.Row{-2.0, -25.0, 12.0},
matrix.Row{7.0, 5.3, -12.5},
},
)
if err != nil {
fmt.Println("You passed a 0x0 or 1x0 matrix.")
}
All rows will have the same amount of columns than the first one. If subsequent has less columns, the remaining ones will be filled with 0.
Documentation details are provided after this section, but here is what the lib allows you to do in a glance:
package main
import (
"fmt"
"gitlab.com/oelmekki/matrix"
)
func main() {
firstMatrix, _ := matrix.Build(
matrix.Builder{
matrix.Row{10, -5.3, 22},
matrix.Row{-2, -25, 12},
matrix.Row{7, 5, -12.5},
},
)
// tests
fmt.Println(firstMatrix.Valid()) // true
fmt.Printf("%v\n", firstMatrix.At(1, 1)) // -25
fmt.Printf("%v\n", firstMatrix.Rows()) // 3
fmt.Printf("%v\n", firstMatrix.Cols()) // 3
secondMatrix, _ := matrix.Build(
matrix.Builder{
matrix.Row{12, -15.5},
matrix.Row{-4, -5},
matrix.Row{3, 2.5},
},
)
fmt.Println(firstMatrix.SameDimensions(secondMatrix)) // false
thirdMatrix, _ := matrix.Build(
matrix.Builder{
matrix.Row{1, 7.2, 2},
matrix.Row{2, 5, 1},
matrix.Row{2, -25.3, -2.5},
},
)
fmt.Println(firstMatrix.SameDimensions(thirdMatrix)) // true
fmt.Println(firstMatrix.EqualTo(thirdMatrix)) // false
// operations
newMatrix, _ := firstMatrix.ScalarMultiply(10.0)
fmt.Println(newMatrix.String())
/*
* { 100 -53 220 }
* { -20 -250 120 }
* { 70 50 -125 }
*/
newMatrix, _ = firstMatrix.DotProduct(secondMatrix)
fmt.Println(newMatrix.String())
/*
* { 207.2 -73.5 }
* { 112 186 }
* { 26.5 -164.75 }
*/
newMatrix, _ = secondMatrix.Transpose()
fmt.Println(newMatrix.String())
/*
* { 12 -4 3 }
* { -15.5 -5 2.5 }
*/
// and more! See doc below
}
A few helper methods are provided to generate matrices and test them.
Generate an zero matrix with rows
rows and cols
cols
Generate a Matrix having the same dimensions than origin matrix, but filled with 0.0.
Generate a new matrix by passing a Builder
.
This allows to have a human friendly looking way of initializing matrices:
myMatrix, _ := matrix.Build(
matrix.Builder {
matrix.Row{ 10, -5.3, 22 },
matrix.Row{ -2, -25, 12 },
matrix.Row{ 7, 5, -12.5 },
},
)
It returns an error if you try to provide a builder with no rows or rows with no cols (you can safely ignore error if you're confident your builder is valid).
Generate a matrix of rows
rows and cols
cols with randomized
values.
Values are randomized with standard normal distribution, using
math.NormFloat64()
(don't forget to seed randomizer if you
want it to be truly random).
Returns the number of rows
Returns the number of columns
Returns a human readable representation of matrix, ready to print
Returns the value at position row
, col
.
Just like an array, you're responsible to make sure you don't ask for an out of range value.
Return a whole row, as []float64
.
As usual, index starts from 0.
Returns error if requested index is out of matrix.
Set the given value in matrix at position (row, col)
(zero-indexed).
You can loop on a matrix this way:
myMatrix := matrix.RandomMatrix(5, 5)
for i := 0; i < myMatrix.Rows(); i++ {
for j := 0; j < myMatrix.Cols(); i++ {
fmt.Printf("%v\n", myMatrix.At(i, j))
}
}
If it's too costly for you performance wise, see the Low level implementation
section at the end of this doc.
A matrix is valid if it has rows, columns and if all its rows have the same amount of columns.
True if both matrices are valid and have the same dimensions.
Tells if two valid matrices have the same dimensions and same values in each cell.
Those are the operations currently implemented. Note that all operations produce a new matrix and return it, current matrix and argument matrix are never modified.
Multiply the matrix with a scalar.
Error is returned if matrix is not valid.
Perform a mathematical standard multiplication between matrix and otherMatrix, and return the resulting resultMatrix.
Error is returned if resultMatrix is undefined (that is, if matrix columns count is not the same than otherMatrix rows count).
Switch matrix dimensions, so that, eg, a 2x3 matrix returns a 3x2 one.
Error is returned if matrix is not valid.
Multiply each cell from matrix with each cell at the same coordinate in otherMatrix.
Note that this is not standard mathematical matrix multiplication. For that one,
use DotProduct()
.
Error is returned if matrices do not have the same dimensions.
Add otherMatrix to matrix and return the resulting resultMatrix.
Error is returned if matrices are not valid or do not have the same dimensions.
Substract otherMatrix from matrix and return the resulting resultMatrix.
Error is returned if matrices are not valid or do not have the same dimensions.
Apply sigmoid function on each cell of matrix and return resulting Matrix.
Error is returned if matrix is not valid.
Compute derivative for sigmoid function on each cell of matrix and return resulting Matrix.
Error is returned if matrix is not valid.
Two generic operations are provided that should allow you to perform any cell
by cell operation by taking one or two matrices as an input: UnaryOperation
and BinaryOperation
.
func (matrix Matrix) UnaryOperation(operation func(float64) float64, operationName string) (resultMatrix Matrix, err error)
Produce a new matrix by applying operation
cell by cell on matrix, so that:
operation(cell) -> resultCell
operationName
is a name you provide for your operation so that it's easier to
know where the error comes from (error will contain that operation name).
operation
is a function you provide, that will receive the float64
value of
each cell, and should return the new float64
value for that cell.
So, for example, if you want to multiply each value by 2 and add 1:
operation := func(value float64) float64 {
return value*2 + 1
}
resultMatrix, err := myMatrix.UnaryOperation(operation, "x * 2 + 1")
Error is returned is the matrix is not valid.
func (matrix Matrix) BinaryOperation(otherMatrix Matrix, operation func(float64, float64) float64, operationName string) (resultMatrix Matrix, err error)
Produce a new matrix by applying operation
cell by cell on two matrices, so that:
operation(cell1, cell2) -> resultCell
operationName
is used in error message, so that it's easier to know which
operation error'd.
So, for example, if you want to divide each cell of matrix by corresponding cell in otherMatrix:
operation := func(value float64, otherValue float64) float64 {
return value / otherValue
}
resultMatrix, err := myMatrix.BinaryOperation(otherMatrix, operation, "x / y")
Returns error if any matrix is invalid, or both matrices aren't of same dimensions.
Sometime, having the lib panic'ing instead of returning error is more useful, so that you can see the stacktrace.
If you want that, use once:
matrix.SetDebug(true)
Under the hood, a Matrix is a []float64
. First entry is the number of rows,
second entry is the number of cols.
To retrieve the index of a value for a matrix position in that array, you can
use IndexFor(row, col int) float64
:
myMatrix, _ := matrix.Build(
matrix.Builder{
matrix.Row{10.0, -5.3, 22.0},
matrix.Row{-2.0, -25.0, 12.0},
matrix.Row{7.0, 5.3, -12.5},
},
)
fmt.Println(myMatrix.IndexFor(1, 2)) // 7
fmt.Printf("%v\n", myMatrix[7]) // 12
This implementation was chosen because it's 50% faster than using a
[][]float64
to map the matrix. It's also slightly faster than using a
struct{ Rows int, Cols int, Values []float64 }
and prevent having to pass the
Matrix by reference everywhere: since it's a slice, it's always a reference.
if you need to iterate directly on matrix values for some performance critical operation, you can do it this way:
for i := 2 ; i < len(myMatrix); i++ {
// ...
}
Or, to iterate on rows:
for i := 2 ; i < len(myMatrix); i += myMatrix[1] {
// `i` is the start of a row
}