Last active
December 24, 2025 16:44
-
-
Save Mrutyunjay01/f0688f727f95e7ac7deba6ff0fb39dd3 to your computer and use it in GitHub Desktop.
Neural Networks in JAX from scratch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "code", | |
| "execution_count": 1, | |
| "id": "69b9d38d", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "import jax\n", | |
| "from math import ceil\n", | |
| "from functools import partial\n", | |
| "from typing import NamedTuple, Any\n", | |
| "from jax import numpy as jnp\n", | |
| "from sklearn.datasets import fetch_openml\n", | |
| "from sklearn.model_selection import StratifiedKFold, train_test_split\n", | |
| "\n", | |
| "import matplotlib.pyplot as plt\n", | |
| "%matplotlib inline" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 2, | |
| "id": "c94a8159", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "fashion_mnist = fetch_openml(\"Fashion-MNIST\", cache=True)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 3, | |
| "id": "eb5b23f1", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "X = jnp.array(fashion_mnist.data)\n", | |
| "y = jnp.array(fashion_mnist.target.astype(\"int\"))" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 4, | |
| "id": "49821634", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "class DataLoader:\n", | |
| " def __init__(self, X: jnp.array, y:jnp.array, batch_size: int):\n", | |
| " self.X = X\n", | |
| " self.y = y\n", | |
| " self.len_data = X.shape[0]\n", | |
| " assert self.len_data == y.shape[0], f\"mismatching samples between X {X.shape[0]} and y {y.shape[0]}\"\n", | |
| " self.batch_size = batch_size\n", | |
| " pass\n", | |
| "\n", | |
| " def get_data(self, is_train=True):\n", | |
| " # yield data in batches as per self.batch_size\n", | |
| " for idx in range(0, self.len_data, self.batch_size):\n", | |
| " features = self.X[idx: idx+self.batch_size]\n", | |
| " if features.shape[0] < self.batch_size:\n", | |
| " # pass on the last incomplete batch\n", | |
| " continue\n", | |
| " targets = self.y[idx: idx+self.batch_size]\n", | |
| " yield (features, targets) if is_train else features\n", | |
| " pass\n", | |
| " pass" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 5, | |
| "id": "1510ed45", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "data_loader = DataLoader(X, y, 8)\n", | |
| "train_data = data_loader.get_data(is_train=True)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 6, | |
| "id": "34187ab9", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "def show_data(x: jnp.array, y: jnp.array):\n", | |
| " n_cols = 4\n", | |
| " n_rows = ceil(x.shape[0] / n_cols)\n", | |
| " fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols)\n", | |
| "\n", | |
| " for row in range(n_rows):\n", | |
| " for col in range(n_cols):\n", | |
| " axes[row, col].imshow(x[row * col + col].reshape(28, 28))\n", | |
| " axes[row, col].set_xlabel(f\"class: {y[row * col + col]}\")\n", | |
| " plt.show()\n", | |
| " pass" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 7, | |
| "id": "e64786a3", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "image/png": "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dy0qe6J3gGL3t7/9Dddffz2eeOIJXHrppfjiiy9wyy23ID09HXPnzk307pFJmpqacOmll2L69OlYuHAhNm/ejJtuugk5OTmYM2dOonePTMAcoJ///Oe49dZbxXXTpk3DhAkTErRHJyihzd2TSDAYVI888og69dRTldPpVKWlpeqhhx5SSim1e/duBUB9+umnSimlAoGAuummm9Tw4cNVWlqaGjlypFqwYIF4vA8//FBNmDBBud1u5fF41JQpU9SePXuUUkpVVlaqCy+8UGVmZqqsrCw1btw4tX79+hPe1x/+8IfqmmuuEdc9/vjjqqSkRIVCoV68CpRKefD000+r3Nxc5fP5wtfdc8896owzzujlqzCwMQcolXJAV1lZqQCojz766KQfwww88vG1efPm4bnnnsNjjz2GqVOn4tChQ9i+fXuXtw2FQigpKcGSJUuQn5+PVatWYc6cORgyZAh+8IMfIBAIYObMmbjlllvwyiuvoKOjA+vWrYPFYgEAXH/99Tj33HPxzDPPwGazobKyEg6HI/z4FosFixYtwuzZs7t8fp/PB7fbLa5LT0/H/v37sXfvXgwfPjwur8lAlEp5sHr1alxwwQVwOp3h62bMmIFHHnkEx44dQ25ubvxemAGEOUCplAO6P/7xjxg5ciTOP//8Xr8OfSrRo59k0NTUpFwul3ruuee63K6PdLtSUVGhZs2apZRS6siRIwqAWrFiRZe3zcrKUosXL+72sc444wz1xhtvdLv92WefVW63W33wwQcqGAyqqqoqNWrUKAVArVq1qtv7UXSplgeXXHKJmjNnjrhu69atCoDatm1bt/ej7jEHKNVyIJLX61W5ubnqkUceOaHbJxIHH0qptWvXKgBq165dXW7vKtmefPJJNW7cOFVQUKAyMjKUw+FQEyZMCG+fPXu2crlc6oorrlALFixQBw8eDG974IEHlN1uV9OmTVPz589XO3fujGl/Q6GQuvvuu1VaWpqy2WwqNzdXPfjggwoAl5ruhVTLA/7hiT/mAKVaDkR6+eWXld1uVzU1NSf9GGbh2S44PmURi1dffRU///nPcfPNN+P9999HZWUlfvKTn6CjoyN8m0WLFmH16tWYMmUKXnvtNYwcORJr1qwBcLw6eevWrfiXf/kXLF++HGPGjMGbb755ws9vsVjwyCOPoKWlBXv37kVNTQ0mTpwIADjllFNi+l3IkGp5UFRUhNraWnHdN3FRUVFMvwsdxxygVMuBSH/84x9xxRVXoLCw8KTub6pEj36SgdfrVenp6Sd8mG3u3Lnq4osvFreZNm2aOuecc7p9jsmTJ6vbb7+9y23XXXeduvLKK09q37/x4x//WJWXl/fqMQa6VMuDb4oNOzo6wtfNmzePxYa9wBygVMuBb+zatUtZLBb1zjvvxHzfROCRDwBpaWm45557cPfdd+PFF1/EV199hTVr1uD555/v8vann346NmzYgL///e/48ssvcd9992H9+vXh7bt378a8efOwevVq7N27F++//z527NiB0aNHw+v1Yu7cuVixYgX27t2LTz75BOvXr8fo0aPD9x81alTUkW99fT0WLlyI7du3o7KyEv/2b/+GJUuWYMGCBXF7TQaiVMuDH/3oR3A6nbj55puxdetWvPbaa/jDH/6Au+66K34vygDDHKBUy4FvvPDCCxgyZAguu+yy3r8IZkj06CdZBINB9dBDD6lhw4Yph8OhysrK1MMPP6yU6jzSbW9vV7Nnz1Yej0fl5OSo2267Tf3iF78Ij3RramrUzJkz1ZAhQ5TT6VTDhg1T999/vwoGg8rn86nrrrtOlZaWKqfTqYqLi9XcuXOV1+sN7wsAtWjRom73ta6uTk2ePFllZGQot9utpk2bxlqPOEmlPFBKqc8++0xNnTpVuVwuNXToUPWb3/ymL16WAYU5QKmWA8FgUJWUlKhf/vKXffFy9AmLUkolbuhDREREAw2nXYiIiMhUHHwQERGRqTj4ICIiIlNx8EFERESm4uCDiIiITMXBBxEREZmKgw8iIiIyFQcfREREZCoOPoiIiMhUHHwQERGRqTj4ICIiIlNx8EFERESm+v8BG1uFbSYq3hwAAAAASUVORK5CYII=", | |
| "text/plain": [ | |
| "<Figure size 640x480 with 8 Axes>" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "output_type": "display_data" | |
| } | |
| ], | |
| "source": [ | |
| "x_sample, y_sample = next(iter(train_data))\n", | |
| "show_data(x_sample, y_sample)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 8, | |
| "id": "2affd4be", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "# how long does it take to get the complete dataset w.r.t. a batch size\n", | |
| "# import time\n", | |
| "\n", | |
| "# data_loader = DataLoader(X, y, batch_size=2048)\n", | |
| "# train_data = data_loader.get_data(is_train=True)\n", | |
| "# tic = time.time()\n", | |
| "# for x_train, y_train in train_data:\n", | |
| "# continue\n", | |
| "# print(f'{time.time() - tic:.2f}')" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 9, | |
| "id": "091f25b7", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "------------------ Fold 0 ------------------\n", | |
| "Iteration 0 Accuracy: 0.78, Loss 105.10041809082031\n", | |
| "Iteration 1 Accuracy: 0.84, Loss 50.06229782104492\n", | |
| "Iteration 2 Accuracy: 0.86, Loss 41.21639633178711\n", | |
| "Iteration 3 Accuracy: 0.87, Loss 36.25566864013672\n", | |
| "Iteration 4 Accuracy: 0.87, Loss 33.13346862792969\n", | |
| "Iteration 5 Accuracy: 0.86, Loss 31.37713050842285\n", | |
| "Iteration 6 Accuracy: 0.87, Loss 29.916488647460938\n", | |
| "Iteration 7 Accuracy: 0.86, Loss 28.327608108520508\n", | |
| "Iteration 8 Accuracy: 0.87, Loss 26.640146255493164\n", | |
| "Iteration 9 Accuracy: 0.86, Loss 25.484012603759766\n", | |
| "Iteration 10 Accuracy: 0.87, Loss 25.4431209564209\n", | |
| "Iteration 11 Accuracy: 0.88, Loss 25.435298919677734\n", | |
| "Iteration 12 Accuracy: 0.89, Loss 24.539390563964844\n", | |
| "Iteration 13 Accuracy: 0.89, Loss 23.322032928466797\n", | |
| "Iteration 14 Accuracy: 0.89, Loss 21.963422775268555\n", | |
| "Fold 0 Accuracy: 0.89\n", | |
| "------------------ Fold 1 ------------------\n", | |
| "Iteration 0 Accuracy: 0.77, Loss 111.32299041748047\n", | |
| "Iteration 1 Accuracy: 0.83, Loss 50.82963943481445\n", | |
| "Iteration 2 Accuracy: 0.84, Loss 39.62657165527344\n", | |
| "Iteration 3 Accuracy: 0.86, Loss 35.53031921386719\n", | |
| "Iteration 4 Accuracy: 0.87, Loss 32.69033432006836\n", | |
| "Iteration 5 Accuracy: 0.87, Loss 32.02552032470703\n", | |
| "Iteration 6 Accuracy: 0.87, Loss 29.951984405517578\n", | |
| "Iteration 7 Accuracy: 0.88, Loss 28.434228897094727\n", | |
| "Iteration 8 Accuracy: 0.88, Loss 27.313901901245117\n", | |
| "Iteration 9 Accuracy: 0.88, Loss 26.59328842163086\n", | |
| "Iteration 10 Accuracy: 0.88, Loss 26.086761474609375\n", | |
| "Iteration 11 Accuracy: 0.88, Loss 25.072847366333008\n", | |
| "Iteration 12 Accuracy: 0.89, Loss 23.68248748779297\n", | |
| "Iteration 13 Accuracy: 0.88, Loss 22.690715789794922\n", | |
| "Iteration 14 Accuracy: 0.89, Loss 22.063501358032227\n", | |
| "Fold 1 Accuracy: 0.89\n", | |
| "------------------ Fold 2 ------------------\n", | |
| "Iteration 0 Accuracy: 0.75, Loss 107.47624969482422\n", | |
| "Iteration 1 Accuracy: 0.84, Loss 51.53094482421875\n", | |
| "Iteration 2 Accuracy: 0.85, Loss 41.4818115234375\n", | |
| "Iteration 3 Accuracy: 0.84, Loss 36.80481719970703\n", | |
| "Iteration 4 Accuracy: 0.86, Loss 34.52952194213867\n", | |
| "Iteration 5 Accuracy: 0.87, Loss 31.991575241088867\n", | |
| "Iteration 6 Accuracy: 0.87, Loss 29.796144485473633\n", | |
| "Iteration 7 Accuracy: 0.87, Loss 28.153940200805664\n", | |
| "Iteration 8 Accuracy: 0.88, Loss 26.822463989257812\n", | |
| "Iteration 9 Accuracy: 0.88, Loss 26.207122802734375\n", | |
| "Iteration 10 Accuracy: 0.88, Loss 26.46046257019043\n", | |
| "Iteration 11 Accuracy: 0.88, Loss 24.957033157348633\n", | |
| "Iteration 12 Accuracy: 0.86, Loss 24.07745361328125\n", | |
| "Iteration 13 Accuracy: 0.87, Loss 23.486385345458984\n", | |
| "Iteration 14 Accuracy: 0.88, Loss 22.554162979125977\n", | |
| "Fold 2 Accuracy: 0.88\n", | |
| "------------------ Fold 3 ------------------\n", | |
| "Iteration 0 Accuracy: 0.76, Loss 110.51836395263672\n", | |
| "Iteration 1 Accuracy: 0.82, Loss 50.895751953125\n", | |
| "Iteration 2 Accuracy: 0.85, Loss 40.91993713378906\n", | |
| "Iteration 3 Accuracy: 0.87, Loss 36.93730926513672\n", | |
| "Iteration 4 Accuracy: 0.87, Loss 34.084014892578125\n", | |
| "Iteration 5 Accuracy: 0.85, Loss 32.147178649902344\n", | |
| "Iteration 6 Accuracy: 0.87, Loss 30.568044662475586\n", | |
| "Iteration 7 Accuracy: 0.87, Loss 30.234619140625\n", | |
| "Iteration 8 Accuracy: 0.87, Loss 28.12507438659668\n", | |
| "Iteration 9 Accuracy: 0.87, Loss 27.100788116455078\n", | |
| "Iteration 10 Accuracy: 0.88, Loss 26.548999786376953\n", | |
| "Iteration 11 Accuracy: 0.87, Loss 25.430347442626953\n", | |
| "Iteration 12 Accuracy: 0.87, Loss 25.04415512084961\n", | |
| "Iteration 13 Accuracy: 0.87, Loss 23.559974670410156\n", | |
| "Iteration 14 Accuracy: 0.87, Loss 23.09308433532715\n", | |
| "Fold 3 Accuracy: 0.87\n", | |
| "------------------ Fold 4 ------------------\n", | |
| "Iteration 0 Accuracy: 0.78, Loss 110.41956329345703\n", | |
| "Iteration 1 Accuracy: 0.82, Loss 48.65843200683594\n", | |
| "Iteration 2 Accuracy: 0.83, Loss 40.263362884521484\n", | |
| "Iteration 3 Accuracy: 0.86, Loss 36.066993713378906\n", | |
| "Iteration 4 Accuracy: 0.86, Loss 32.979530334472656\n", | |
| "Iteration 5 Accuracy: 0.86, Loss 30.87268829345703\n", | |
| "Iteration 6 Accuracy: 0.87, Loss 29.401487350463867\n", | |
| "Iteration 7 Accuracy: 0.87, Loss 28.30144500732422\n", | |
| "Iteration 8 Accuracy: 0.87, Loss 26.695728302001953\n", | |
| "Iteration 9 Accuracy: 0.87, Loss 25.73813819885254\n", | |
| "Iteration 10 Accuracy: 0.88, Loss 25.964645385742188\n", | |
| "Iteration 11 Accuracy: 0.88, Loss 25.051902770996094\n", | |
| "Iteration 12 Accuracy: 0.87, Loss 23.67770004272461\n", | |
| "Iteration 13 Accuracy: 0.89, Loss 23.780059814453125\n", | |
| "Iteration 14 Accuracy: 0.89, Loss 22.97209358215332\n", | |
| "Fold 4 Accuracy: 0.89\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "class Softmax:\n", | |
| " def __init__(self):\n", | |
| " pass\n", | |
| "\n", | |
| " def __call__(self, x):\n", | |
| " x = x - jnp.max(x, axis=-1, keepdims=True) # for stability\n", | |
| " num = jnp.exp(x)\n", | |
| " denum = jnp.sum(num, axis=-1, keepdims=True)\n", | |
| " return num / denum\n", | |
| " pass\n", | |
| "\n", | |
| "class RELU:\n", | |
| " def __init__(self):\n", | |
| " pass\n", | |
| "\n", | |
| " def __call__(self, x: jnp.array):\n", | |
| " return jnp.maximum(x, 0)\n", | |
| "\n", | |
| "class LayerNorm:\n", | |
| " def __init__(self):\n", | |
| " pass\n", | |
| "\n", | |
| " def __call__(self, x, eps=1e-9):\n", | |
| " mean = jnp.mean(x, axis=-1, keepdims=True)\n", | |
| " var = jnp.var(x, axis=-1, keepdims=True)\n", | |
| " return (x - mean)/jnp.sqrt(var + eps)\n", | |
| " pass\n", | |
| "\n", | |
| "class LinearLayer:\n", | |
| " def __init__(self, in_features: int, out_features: int):\n", | |
| " self.in_features = in_features\n", | |
| " self.out_features = out_features\n", | |
| " pass\n", | |
| "\n", | |
| " def init(self, prng_key):\n", | |
| " keys = jax.random.split(prng_key, 2)\n", | |
| " scale = jnp.sqrt(2/self.in_features) # > fan-in initialization to prevent dying nerurons\n", | |
| "\n", | |
| " return {\n", | |
| " \"w\": scale * jax.random.normal(keys[0], shape=(self.in_features, self.out_features)),\n", | |
| " \"b\": jnp.zeros((self.out_features, ))\n", | |
| " }\n", | |
| "\n", | |
| " def __call__(self, params: dict, x: jnp.array):\n", | |
| " out = x @ params[\"w\"] + params[\"b\"]\n", | |
| " return out\n", | |
| " pass\n", | |
| "\n", | |
| "# define a classifier model\n", | |
| "class FashionMNISTClassifier:\n", | |
| " def __init__(self, in_features: int):\n", | |
| " self.linear_0 = LinearLayer(in_features=in_features, out_features=784)\n", | |
| " self.linear_1 = LinearLayer(in_features=784, out_features=512)\n", | |
| " self.linear_2 = LinearLayer(in_features=512, out_features=128)\n", | |
| " self.linear_3 = LinearLayer(in_features=128, out_features=64)\n", | |
| " self.linear_4 = LinearLayer(in_features=64, out_features=10)\n", | |
| " self.softmax = Softmax()\n", | |
| " self.relu = RELU()\n", | |
| " self.layer_norm = LayerNorm()\n", | |
| "\n", | |
| " def init(self, prng_key):\n", | |
| " # initialize params tree\n", | |
| " keys = jax.random.split(prng_key, 5)\n", | |
| " return {\n", | |
| " \"linear_0\": self.linear_0.init(prng_key=keys[0]),\n", | |
| " \"linear_1\": self.linear_1.init(prng_key=keys[1]),\n", | |
| " \"linear_2\": self.linear_2.init(prng_key=keys[2]),\n", | |
| " \"linear_3\": self.linear_3.init(prng_key=keys[3]),\n", | |
| " \"linear_4\": self.linear_4.init(prng_key=keys[4]),\n", | |
| " }\n", | |
| " pass\n", | |
| "\n", | |
| " def forward(self, params: dict, x: jnp.array):\n", | |
| " x = self.linear_0(params=params[\"linear_0\"], x=x)\n", | |
| " x = self.layer_norm(x)\n", | |
| " x = self.relu(x)\n", | |
| " x = self.linear_1(params=params[\"linear_1\"], x=x)\n", | |
| " x = self.layer_norm(x)\n", | |
| " x = self.relu(x)\n", | |
| " x = self.linear_2(params=params[\"linear_2\"], x=x)\n", | |
| " x = self.layer_norm(x)\n", | |
| " x = self.relu(x)\n", | |
| " x = self.linear_3(params=params[\"linear_3\"], x=x)\n", | |
| " x = self.relu(x)\n", | |
| " out = self.linear_4(params=params[\"linear_4\"], x=x)\n", | |
| " return out\n", | |
| "\n", | |
| " def validate(self, params, x, y):\n", | |
| " logits = self.forward(params=params, x=x)\n", | |
| " softmax_probs = self.softmax(logits)\n", | |
| " predicted_labels = jnp.argmax(softmax_probs, axis=-1)\n", | |
| " return predicted_labels == y\n", | |
| "\n", | |
| " def predict(self, params, x, proba=True):\n", | |
| " logits = self.forward(params, x)\n", | |
| " return logits if not proba else self.softmax(logits)\n", | |
| " pass\n", | |
| "\n", | |
| "class AdamState(NamedTuple):\n", | |
| " m: Any # 1st momentum\n", | |
| " v: Any # 2nd momentum\n", | |
| " t: int # step\n", | |
| " pass\n", | |
| "\n", | |
| "class AdamOptimizer:\n", | |
| " def __init__(self, learning_rate, beta_1, beta_2, eps):\n", | |
| " self.learning_rate = learning_rate\n", | |
| " self.beta_1 = beta_1\n", | |
| " self.beta_2 = beta_2\n", | |
| " self.epsilon = eps\n", | |
| " pass\n", | |
| "\n", | |
| " def init(self, params) -> AdamState:\n", | |
| " # for each param, initiate 1st and 2nd order momentum\n", | |
| " m = jax.tree.map(lambda p: jnp.zeros_like(p), params)\n", | |
| " v = jax.tree.map(lambda p: jnp.zeros_like(p), params)\n", | |
| " return AdamState(m=m, v=v, t=0)\n", | |
| "\n", | |
| " def update(self, grad, state: AdamState, params: dict=None):\n", | |
| " m, v, t = state.m, state.v, state.t\n", | |
| " updated_m = jax.tree.map(\n", | |
| " lambda m_val, g_val: self.beta_1 * m_val + (1 - self.beta_1) * g_val,\n", | |
| " m,\n", | |
| " grad\n", | |
| " )\n", | |
| " updated_v = jax.tree.map(\n", | |
| " lambda v_val, g_val: self.beta_2 * v_val + (1 - self.beta_2) * (g_val ** 2),\n", | |
| " v,\n", | |
| " grad\n", | |
| " )\n", | |
| "\n", | |
| " # apply bias correction\n", | |
| " t_next = t + 1\n", | |
| " bias_correction_2 = 1 - self.beta_2 ** t_next\n", | |
| " bias_correction_1 = 1 - self.beta_1 ** t_next\n", | |
| "\n", | |
| " updated_grad = jax.tree.map(\n", | |
| " lambda m_, v_: - self.learning_rate * (m_/bias_correction_1) / (jnp.sqrt(v_/bias_correction_2) + self.epsilon),\n", | |
| " updated_m,\n", | |
| " updated_v\n", | |
| " )\n", | |
| "\n", | |
| " # update the states\n", | |
| " updated_state = AdamState(\n", | |
| " m = updated_m,\n", | |
| " v = updated_v,\n", | |
| " t = t_next\n", | |
| " )\n", | |
| "\n", | |
| " return updated_grad, updated_state\n", | |
| " pass\n", | |
| " pass\n", | |
| "\n", | |
| "class SGDOptimizer:\n", | |
| " def __init__(self, learning_rate):\n", | |
| " self.learning_rate = learning_rate\n", | |
| " pass\n", | |
| "\n", | |
| " def init(self):\n", | |
| " return None\n", | |
| " pass\n", | |
| "\n", | |
| " def update(self, grad, state, params: dict=None):\n", | |
| " updated_grad = jax.tree.map(\n", | |
| " lambda g: - self.learning_rate * g, grad\n", | |
| " )\n", | |
| "\n", | |
| " return updated_grad, state\n", | |
| " pass\n", | |
| "\n", | |
| "def weight_decay_l2(params):\n", | |
| " return jax.tree.reduce(\n", | |
| " lambda x, y: x + y,\n", | |
| " jax.tree.map(\n", | |
| " lambda p: jnp.sum(p**2), params\n", | |
| " ))\n", | |
| "\n", | |
| "def loss_fn(params: dict, model: FashionMNISTClassifier, x: jnp.array, y_true: jnp.array):\n", | |
| " # calculate cross entropy loss\n", | |
| " # sum(y_t * log(y_pred))\n", | |
| " logits = model.forward(params=params, x=x)\n", | |
| " assert y_true.shape[0] == logits.shape[0], f\"mismatching shape between ground truth {y_true.shape} and logits {logits.shape}\"\n", | |
| " log_probs = logits - jax.nn.logsumexp(logits, axis=-1, keepdims=True)\n", | |
| " return -jnp.mean(jnp.sum(y_true * log_probs, axis=-1))\n", | |
| "\n", | |
| "def apply_updates(grad, params, weight_decay=1e-5):\n", | |
| " \"\"\" apply updated grads to the params \"\"\"\n", | |
| " # apply grad of same keys to params of same key\n", | |
| " return jax.tree.map(\n", | |
| " lambda p, g: p + g - weight_decay * p, params, grad # > decoupled weight decay\n", | |
| " )\n", | |
| "\n", | |
| "@partial(jax.jit, static_argnums=[0, 2])\n", | |
| "def training_step(model, params, optim, optim_state, x_sample, y_sample):\n", | |
| " y_ohe = jax.nn.one_hot(y_sample, num_classes=10)\n", | |
| " loss, grad = jax.value_and_grad(loss_fn)(params, model, x_sample, y_ohe)\n", | |
| " updated_grad, optim_state = optim.update(grad=grad, state=optim_state)\n", | |
| " params = apply_updates(updated_grad, params)\n", | |
| " return params, optim_state, loss\n", | |
| "\n", | |
| "features = jnp.array(fashion_mnist.data)\n", | |
| "targets = jnp.array(fashion_mnist.target.astype(\"int\"))\n", | |
| "X, X_test, y, y_test = train_test_split(features, targets, test_size=0.1, shuffle=True, random_state=42)\n", | |
| "X = X/255. # remove the normalization and see what happens if weights are initialized to normal distribution to 0 mean and 1 var\n", | |
| "\n", | |
| "prng_key = jax.random.key(seed=42)\n", | |
| "\n", | |
| "skf = StratifiedKFold(5, shuffle=True, random_state=42)\n", | |
| "folds = skf.split(X, y)\n", | |
| "for fold, (train_idx, val_idx) in enumerate(folds):\n", | |
| " print(f\"------------------ Fold {fold} ------------------\")\n", | |
| "\n", | |
| " X_train, y_train = X[train_idx], y[train_idx]\n", | |
| " X_val, y_val = X[val_idx], y[val_idx]\n", | |
| "\n", | |
| " train_data_loader = DataLoader(X_train, y_train, 512)\n", | |
| "\n", | |
| " model = FashionMNISTClassifier(in_features=x_sample.shape[1])\n", | |
| " params = model.init(prng_key=prng_key)\n", | |
| " # optim = SGDOptimizer(learning_rate=0.01)\n", | |
| " optim = AdamOptimizer(learning_rate=0.01, beta_1=0.9, beta_2=0.999, eps=1e-9)\n", | |
| " optim_state = optim.init(params=params)\n", | |
| " n_iterations = 15\n", | |
| "\n", | |
| " for iteration in range(n_iterations):\n", | |
| " train_data = train_data_loader.get_data(is_train=True)\n", | |
| " iteration_loss = 0\n", | |
| " for step, (x_sample, y_sample) in enumerate(train_data):\n", | |
| " params, optim_state, loss = training_step(model, params, optim, optim_state, x_sample, y_sample)\n", | |
| " iteration_loss += loss\n", | |
| "\n", | |
| " result = model.validate(params, X_val, y_val)\n", | |
| " print(f\"Iteration {iteration} Accuracy: {result.sum()/result.shape[0]:.2f}, Loss {iteration_loss}\")\n", | |
| "\n", | |
| " fold_result = model.validate(params, X_val, y_val)\n", | |
| " print(f\"Fold {fold} Accuracy: {result.sum()/result.shape[0]:.2f}\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 10, | |
| "id": "3010bd64", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "11.0 {'x': Array(3., dtype=float32, weak_type=True), 'y': Array(2., dtype=float32, weak_type=True)}\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "def f(params: dict): return params['x']**3 + params['y']**2 + 9\n", | |
| "\n", | |
| "loss, grad = jax.value_and_grad(f)({'x': 1., 'y': 1.}) # tuple[0] -> evaluates the function, tuple[1] -> evaluates the grad\n", | |
| "print(loss, grad)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 11, | |
| "id": "41f8f065", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/plain": [ | |
| "6" | |
| ] | |
| }, | |
| "execution_count": 11, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "sample_data = {\n", | |
| " \"x\": {\n", | |
| " \"a\": 1,\n", | |
| " \"b\": 2,\n", | |
| " },\n", | |
| " \"y\": {\n", | |
| " \"c\": 1,\n", | |
| " \"d\": 2\n", | |
| " }\n", | |
| "}\n", | |
| "\n", | |
| "jax.tree.reduce(lambda x, y: x + y, tree=sample_data) # reduce operates between two leaves and cumulates over" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 15, | |
| "id": "a54a95ff", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "Accuracy: 0.87\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "# predict\n", | |
| "probs = model.predict(params, X_test)\n", | |
| "y_preds = jnp.argmax(probs, axis=-1)\n", | |
| "result = y_preds == y_test\n", | |
| "print(f\"Accuracy: {result.sum()/result.shape[0]:.2f}\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "2e050283", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [] | |
| } | |
| ], | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": "cml", | |
| "language": "python", | |
| "name": "python3" | |
| }, | |
| "language_info": { | |
| "codemirror_mode": { | |
| "name": "ipython", | |
| "version": 3 | |
| }, | |
| "file_extension": ".py", | |
| "mimetype": "text/x-python", | |
| "name": "python", | |
| "nbconvert_exporter": "python", | |
| "pygments_lexer": "ipython3", | |
| "version": "3.11.10" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 5 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment