# Imports
from PythonInterface import Python
= Python.import_module("pathlib") # Python standard library
let pathlib = Python.import_module("gzip") # Python standard library
let gzip = Python.import_module("pickle") # Python standard library
let pickle = Python.import_module("numpy")
let np
# Get the data
= pathlib.Path('./lost+found/data/mnist.pkl.gz')
path_gz = gzip.open(path_gz, 'rb')
f = pickle._Unpickler(f)
u = 'latin1'
u.encoding = u.load()
data
= data[0]
data_train = data[1]
data_valid
= data_train[0]
x_train = data_train[1]
y_train = np.expand_dims(y_train, 1)
y_train
= data_valid[0]
x_valid = data_valid[1]
y_valid = np.expand_dims(y_valid, 1)
y_valid f.close()
π₯ Forward and backward pass
Let us put in the code fom the previous notebook, to do the imports an to load the data.
from DType import DType
from Memory import memset_zero
from Object import object, Attr
from Pointer import DTypePointer, Pointer
from Random import rand
from Range import range
from TargetInfo import dtype_sizeof
type: DType]:
struct Matrix[type]
var data: DTypePointer[
var rows: Int
var cols: Int
__init__(inout self, rows: Int, cols: Int):
fn self.data = DTypePointer[type].alloc(rows * cols)
self.data, rows*cols)
rand(self.rows = rows
self.cols = cols
self, other: Self):
fn __copyinit__(inout self.data = other.data
self.rows = other.rows
self.cols = other.cols
__del__(owned self):
fn self.data.free()
self):
fn zero(inout self.data, self.rows * self.cols)
memset_zero(
@always_inline
__getitem__(self, y: Int, x: Int) -> SIMD[type, 1]:
fn return self.load[1](y, x)
@always_inline
self, y: Int, x: Int) -> SIMD[type, nelts]:
fn load[nelts:Int](return self.data.simd_load[nelts](y * self.cols + x)
@always_inline
__setitem__(self, y: Int, x: Int, val: SIMD[type, 1]):
fn return self.store[1](y, x, val)
@always_inline
self, y: Int, x: Int, val: SIMD[type, nelts]):
fn store[nelts:Int](self.data.simd_store[nelts](y * self.cols + x, val)
type: DType]( a:PythonObject, o: Matrix[type], bs: Int) raises -> Matrix[type]:
fn matrix_dataloader[for i in range(bs):
for j in range(o.cols):
= a[i][j].to_float64().cast[type]()
o[i,j] return o
= 5 # batch-size
let bs: Int = 28*28
let ni: Int
= Matrix[DType.float32](bs,ni)
let xb: Matrix[DType.float32] = Matrix[DType.float32](bs,1)
let yb: Matrix[DType.float32]
xb.zero()
yb.zero()
= matrix_dataloader(x_train, xb, bs)
xb = matrix_dataloader(y_train, yb, bs) yb
= 10
let no: Int = Matrix[DType.float32](ni, no) # weights
var w: Matrix[DType.float32] = Matrix[DType.float32](no, 1) # bias
var b: Matrix[DType.float32]
b.zero()= Matrix[DType.float32](xb.rows, w.cols) # result
var res res.zero()
from TargetInfo import dtype_sizeof, dtype_simd_width
from Functional import vectorize
= dtype_simd_width[DType.float32]() # The SIMD vector width.
alias nelts
type: DType](res: Matrix[type], xb: Matrix[type], w: Matrix[type], b: Matrix[type]) raises -> Matrix[type]:
fn lin_vectorized[for i in range(xb.rows): # 50000
for j in range(xb.cols): # 784
@parameter
fn dotbias[nelts: Int](k: Int):+ xb[i,j] * w.load[nelts](j,k) + b.load[nelts](k,0))
res.store[nelts](i,k, res.load[nelts](i,k)
vectorize[nelts, dotbias](w.cols)return res
res.zero()= lin_vectorized(res, xb, w, b) res
print(res.rows)
print(res.cols)
5
10
Foundations
RELu from foundations
from Math import max
type: DType](acts: Matrix[type], out: Matrix[type]) -> Matrix[type]:
fn relu_layer[@parameter
fn relu[nelts: Int](k: Int):#let l = SIMD[type, nelts](0)
0, max[type, nelts](acts.load[nelts](k,0), 0.0))
out.store[nelts](k,
vectorize[nelts, relu](acts.rows)return out
Letβs test that with an example. Firstly a little print function for convenience.
type: DType](mat: Matrix[type]):
fn print_matrix[for row in range(mat.rows):
for col in range(mat.cols):
print(mat[row, col])
And now, some dummy data.
# These are like the activations
= Matrix[DType.float32](4, 1)
var x 3,0] = -0.333 # Intentionally have a negative value here
x[ print_matrix[DType.float32](x)
0.76618862152099609
0.21974173188209534
0.11567939817905426
-0.33300000429153442
= Matrix[DType.float32](x.rows, x.cols) # Matrix to hold the result
var res
res.zero() print_matrix[DType.float32](res)
0.0
0.0
0.0
0.0
So, the result has been initialized / zeroed out nicely. Letβs see what it shows after calling the RELu function.
= relu_layer[DType.float32](x, res)
var o print_matrix(o)
0.76618862152099609
0.21974173188209534
0.11567939817905426
0.0
Looks good. The negative values are being zeroed-out, while the positive values are the same as the input matrix.
Next up: The loss function.
Loss function from the foundations: MSE
from Math import pow
type: DType](y_pred: Matrix[type], y_true: Matrix[type], res: Matrix[type]):
fn loss[@parameter
fn mse[nelts: Int](k: Int):= pow[type, nelts](y_pred.load[nelts](k,0) - y_true.load[nelts](k,0), 2).reduce_add()
let sum_of_squares 1](0, 0, res.load[1](0, 0) + sum_of_squares[0])
res.store[
vectorize[nelts, mse](y_pred.rows)1](0, 0, res.load[1](0, 0) / y_pred.rows ) res.store[
Let us test that with a couple of examples.
First, if both inputs are zeros, then the loss should be zero.
= Matrix[DType.float32](8, 1)
var y1
y1.zero()= Matrix[DType.float32](8, 1)
var y2
y2.zero()= Matrix[DType.float32](1, 1)
var l
l.zero()print(l[0,0])
0.0
loss[DType.float32](y1, y2, l)print(l[0,0])
0.0
That looks fine.
Now if we have two random matrices, we would expect the loss to be non-zero.
= Matrix[DType.float32](8, 1)
var y1 = Matrix[DType.float32](8, 1)
var y2 = Matrix[DType.float32](1, 1)
var l
l.zero()print(l[0,0])
0.0
loss[DType.float32](y1, y2, l)print(l[0,0])
0.21140147745609283
And if both the vectors are equal, we should get zero loss again.
l.zero()
loss[DType.float32](y1, y1, l)print(l[0,0])
0.0