├── readme_pics
├── jax_logo.png
├── going_deeper.jpg
├── tpu_jit_speed.PNG
└── api_onion_structure.jpg
├── .gitignore
├── LICENCE
├── README.md
├── Tutorial_3_JAX_Neural_Network_from_Scratch_Colab.ipynb
└── Tutorial_4_Flax_Zero2Hero_Colab.ipynb
/readme_pics/jax_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gordicaleksa/get-started-with-JAX/HEAD/readme_pics/jax_logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # PyCharm IDE
2 | .idea
3 | __pycache__
4 |
5 | # Jupyter notebook checkpoints
6 | .ipynb_checkpoints
7 |
8 |
--------------------------------------------------------------------------------
/readme_pics/going_deeper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gordicaleksa/get-started-with-JAX/HEAD/readme_pics/going_deeper.jpg
--------------------------------------------------------------------------------
/readme_pics/tpu_jit_speed.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gordicaleksa/get-started-with-JAX/HEAD/readme_pics/tpu_jit_speed.PNG
--------------------------------------------------------------------------------
/readme_pics/api_onion_structure.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gordicaleksa/get-started-with-JAX/HEAD/readme_pics/api_onion_structure.jpg
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Aleksa Gordić
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Get started with JAX! :computer: :zap:
2 |
3 | The goal of this repo is to make it easier to get started with [JAX](https://github.com/google/jax), [Flax](https://github.com/google/flax), and [Haiku](https://github.com/deepmind/dm-haiku)!
4 |
5 | `JAX` ecosystem is becoming an increasingly popular alternative to `PyTorch` and `TensorFlow`. :sunglasses:
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | *Note: I'm only going to recommend content that I've personally analyzed and found useful here.
18 | If you want a comprehensive list check out the [awesome-jax repo](https://github.com/n2cholas/awesome-jax).*
19 |
20 | ## Table of Contents
21 | * [Machine Learning with JAX](#my-machine-learning-with-jax-tutorials)
22 | + [Tutorial #1: From Zero to Hero](#tutorial-1-from-zero-to-hero)
23 | + [Tutorial #2: From Hero to Hero Pro+](#tutorial-2-from-hero-to-heropro)
24 | + [Tutorial #3: Coding a Neural Network from Scratch in Pure JAX](#tutorial-3-building-a-neural-network-from-scratch)
25 | + [Tutorial #4: Flax From Zero to Hero](#tutorial-4-machine-learning-with-flax---from-zero-to-hero)
26 | + [Tutorial #5: Haiku From Zero to Hero (coming soon)](#tutorial-5-coming-up-machine-learning-with-haiku---from-zero-to-hero)
27 | * [Other useful JAX resources](#other-useful-content)
28 |
29 | ## My Machine Learning with JAX Tutorials
30 |
31 | *Tip on how to use notebooks: just open the notebook directly in Google Colab
32 | (you'll see a button on top of the Jupyter file which will direct you to Colab).
33 | This way you can avoid having to setup the Python env! (This was especially convenient for me since I'm on Windows which is still not supported)*
34 |
35 | ### Tutorial #1: From Zero to Hero
36 |
37 | In this video, we start from the basics and then gradually dig into the nitty-gritty details
38 | of `jit`, `grad`, `vmap`, and various other idiosyncrasies of JAX.
39 |
40 | [YouTube Video (Tutorial #1)](https://youtu.be/SstuvS-tVc0)
41 | [Accompanying Jupyter Notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_1_JAX_Zero2Hero_Colab.ipynb)
42 |
43 |
44 |
46 |
47 |
48 | ### Tutorial #2: From Hero to HeroPro+
49 |
50 | In this video, we learn all additional components needed to train ML models (such as NNs) on multiple machines!
51 | We'll train a simple MLP model and we'll even train an ML model on 8 TPU cores!
52 |
53 | [YouTube Video (Tutorial #2)](https://www.youtube.com/watch?v=CQQaifxuFcs)
54 | [Accompanying Jupyter Notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_2_JAX_HeroPro%2B_Colab.ipynb)
55 |
56 |
57 |
59 |
60 |
61 | ### Tutorial #3: Building a Neural Network from Scratch
62 |
63 | Watch me code a Neural Network from scratch! :partying_face: In this 3rd video of the JAX tutorials series.
64 |
65 | In this video, I build an [MLP](https://en.wikipedia.org/wiki/Multilayer_perceptron) and train it as a classifier on MNIST
66 | using PyTorch's data loader (although it's trivial to use a more complex dataset) - all this in "pure" JAX (no Flax/Haiku/Optax).
67 |
68 | I then do an additional analysis:
69 | * Visualize MLP's learned weights
70 | * Visualize embeddings of a batch of images using t-SNE
71 | * Finally, I analyze whether we have too many dead ReLU neurons in our network
72 |
73 | [YouTube Video (Tutorial #3)](https://www.youtube.com/watch?v=6_PqUPxRmjY)
74 | [Accompanying Jupyter Notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_3_JAX_Neural_Network_from_Scratch_Colab.ipynb) (Note: I'll soon refactor it but I'll link the original)
75 |
76 |
77 |
79 |
80 |
81 | ---
82 |
83 | ### Tutorial #4: Machine Learning with Flax - From Zero to Hero
84 |
85 | In this video, I cover everything you need to know to get started with [Flax](https://github.com/google/flax)!
86 |
87 | We cover `init`, `apply`, `TrainState`, etc. and other idiosyncrasies like the usage of `mutable` and `rngs` keywords.
88 |
89 | [YouTube Video (Tutorial #4)](https://www.youtube.com/watch?v=5eUSmJvK8WA)
90 | [Accompanying Jupyter Notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_4_Flax_Zero2Hero_Colab.ipynb)
91 |
92 |
93 |
95 |
96 |
97 | ---
98 |
99 | ### Tutorial #5 (coming up): Machine Learning with Haiku - From Zero to Hero
100 |
101 | todo
102 |
103 | ## Other useful content
104 |
105 | Aside from the [official docs](https://jax.readthedocs.io/) here are some resources that helped me.
106 |
107 | ### Videos
108 |
109 | * [Introduction to JAX](https://www.youtube.com/watch?v=0mVmRHMaOJ4&ab_channel=GoogleCloudTech) (gives a very high-level overview)
110 | * [JAX: Accelerated Machine Learning Research | SciPy 2020 | VanderPlas](https://www.youtube.com/watch?v=z-WSrQDXkuM&ab_channel=Enthought) (many more details)
111 | * [NeurIPS 2020: JAX Ecosystem Meetup](https://www.youtube.com/watch?v=iDxJxIyzSiM&t=1s&ab_channel=DeepMind) (DeepMind team about the ecosystem of libs around JAX)
112 | * [Introduction to JAX for Machine Learning and More](https://www.youtube.com/watch?v=QkmKfzxbCLQ&ab_channel=UWaterlooDataScience) (nice, hands-on workshop)
113 | * [Day 1 Talks: JAX, Flax & Transformers | HuggingFace](https://www.youtube.com/watch?v=fuAyUQcVzTY&ab_channel=HuggingFace) (all 4 talks are good)
114 | * [Day 2 Talks: JAX, Flax & Transformers | HuggingFace](https://www.youtube.com/watch?v=__eG63ZP_5g&ab_channel=HuggingFace) (only the first 2 talks are relevant)
115 |
116 | ### Blogs
117 |
118 | * [Using JAX to accelerate our research | DeepMind](https://deepmind.com/blog/article/using-jax-to-accelerate-our-research) (similar info as the NeuroIPS 2020 video)
119 | * [You don't know JAX | Colin Raffel](https://colinraffel.com/blog/you-don-t-know-jax.html)
120 |
121 | ## Acknowledgements
122 |
123 | * The notebooks were heavily inspired by the official [JAX](https://jax.readthedocs.io/), [Flax](https://flax.readthedocs.io/en/latest/), and [Haiku](https://dm-haiku.readthedocs.io/en/latest/) docs.
124 |
125 | ## Citation
126 |
127 | If you find this content useful, please cite the following:
128 |
129 | ```
130 | @misc{Gordic2021GetStartedWithJAX,
131 | author = {Gordić, Aleksa},
132 | title = {Get started with JAX},
133 | year = {2021},
134 | publisher = {GitHub},
135 | journal = {GitHub repository},
136 | howpublished = {\url{https://github.com/gordicaleksa/get-started-with-JAX}},
137 | }
138 | ```
139 |
140 | ## Connect With Me
141 |
142 | If you'd love to have some more AI-related content in your life :nerd_face:, consider:
143 | * Subscribing to my YouTube channel [The AI Epiphany](https://www.youtube.com/c/TheAiEpiphany) :bell:
144 | * Follow me on [LinkedIn](https://www.linkedin.com/in/aleksagordic/) and [Twitter](https://twitter.com/gordic_aleksa) :bulb:
145 | * Follow me on [Medium](https://gordicaleksa.medium.com/) :books: :heart:
146 | * Join the [Discord](https://discord.gg/peBrCpheKE) community! :family:
147 |
148 | ## Licence
149 |
150 | [](https://github.com/gordicaleksa/get-started-with-JAX/blob/master/LICENCE)
--------------------------------------------------------------------------------
/Tutorial_3_JAX_Neural_Network_from_Scratch_Colab.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Tutorial 3: JAX - Building a Neural Network from Scratch.ipynb",
7 | "provenance": [],
8 | "collapsed_sections": [],
9 | "authorship_tag": "ABX9TyMpTL6XC+tcxSqZ2FePUhlZ",
10 | "include_colab_link": true
11 | },
12 | "kernelspec": {
13 | "name": "python3",
14 | "display_name": "Python 3"
15 | },
16 | "language_info": {
17 | "name": "python"
18 | }
19 | },
20 | "cells": [
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {
24 | "id": "view-in-github",
25 | "colab_type": "text"
26 | },
27 | "source": [
28 | ""
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {
34 | "id": "XZuyP-M3KPUR"
35 | },
36 | "source": [
37 | "# MLP training on MNIST"
38 | ]
39 | },
40 | {
41 | "cell_type": "code",
42 | "metadata": {
43 | "id": "8-SzJ0NTKRP1"
44 | },
45 | "source": [
46 | "import numpy as np\n",
47 | "import jax.numpy as jnp\n",
48 | "from jax.scipy.special import logsumexp\n",
49 | "import jax\n",
50 | "from jax import jit, vmap, pmap, grad, value_and_grad\n",
51 | "\n",
52 | "from torchvision.datasets import MNIST\n",
53 | "from torch.utils.data import DataLoader"
54 | ],
55 | "execution_count": 1,
56 | "outputs": []
57 | },
58 | {
59 | "cell_type": "code",
60 | "metadata": {
61 | "colab": {
62 | "base_uri": "https://localhost:8080/"
63 | },
64 | "id": "G4NrxSVjKt8f",
65 | "outputId": "6bb8bef6-3098-4fd5-8ffe-62f4b0b1aa79"
66 | },
67 | "source": [
68 | "seed = 0\n",
69 | "mnist_img_size = (28, 28)\n",
70 | "\n",
71 | "def init_MLP(layer_widths, parent_key, scale=0.01):\n",
72 | "\n",
73 | " params = []\n",
74 | " keys = jax.random.split(parent_key, num=len(layer_widths)-1)\n",
75 | "\n",
76 | " for in_width, out_width, key in zip(layer_widths[:-1], layer_widths[1:], keys):\n",
77 | " weight_key, bias_key = jax.random.split(key)\n",
78 | " params.append([\n",
79 | " scale*jax.random.normal(weight_key, shape=(out_width, in_width)),\n",
80 | " scale*jax.random.normal(bias_key, shape=(out_width,))\n",
81 | " ]\n",
82 | " )\n",
83 | "\n",
84 | " return params\n",
85 | "\n",
86 | "# test\n",
87 | "key = jax.random.PRNGKey(seed)\n",
88 | "MLP_params = init_MLP([784, 512, 256, 10], key)\n",
89 | "print(jax.tree_map(lambda x: x.shape, MLP_params))"
90 | ],
91 | "execution_count": 4,
92 | "outputs": [
93 | {
94 | "output_type": "stream",
95 | "name": "stdout",
96 | "text": [
97 | "[[(512, 784), (512,)], [(256, 512), (256,)], [(10, 256), (10,)]]\n"
98 | ]
99 | }
100 | ]
101 | },
102 | {
103 | "cell_type": "code",
104 | "metadata": {
105 | "colab": {
106 | "base_uri": "https://localhost:8080/"
107 | },
108 | "id": "U_z7eLxINv9x",
109 | "outputId": "e9909f9f-6778-4977-91f1-f5b14dd9ecd4"
110 | },
111 | "source": [
112 | "def MLP_predict(params, x):\n",
113 | " hidden_layers = params[:-1]\n",
114 | "\n",
115 | " activation = x\n",
116 | " for w, b in hidden_layers:\n",
117 | " activation = jax.nn.relu(jnp.dot(w, activation) + b)\n",
118 | "\n",
119 | " w_last, b_last = params[-1]\n",
120 | " logits = jnp.dot(w_last, activation) + b_last\n",
121 | "\n",
122 | " # log(exp(o1)) - log(sum(exp(o1), exp(o2), ..., exp(o10)))\n",
123 | " # log( exp(o1) / sum(...) )\n",
124 | " return logits - logsumexp(logits)\n",
125 | "\n",
126 | "# tests\n",
127 | "\n",
128 | "# test single example\n",
129 | "\n",
130 | "dummy_img_flat = np.random.randn(np.prod(mnist_img_size))\n",
131 | "print(dummy_img_flat.shape)\n",
132 | "\n",
133 | "prediction = MLP_predict(MLP_params, dummy_img_flat)\n",
134 | "print(prediction.shape)\n",
135 | "\n",
136 | "# test batched function\n",
137 | "batched_MLP_predict = vmap(MLP_predict, in_axes=(None, 0))\n",
138 | "\n",
139 | "dummy_imgs_flat = np.random.randn(16, np.prod(mnist_img_size))\n",
140 | "print(dummy_imgs_flat.shape)\n",
141 | "predictions = batched_MLP_predict(MLP_params, dummy_imgs_flat)\n",
142 | "print(predictions.shape)"
143 | ],
144 | "execution_count": 5,
145 | "outputs": [
146 | {
147 | "output_type": "stream",
148 | "name": "stdout",
149 | "text": [
150 | "(784,)\n",
151 | "(10,)\n",
152 | "(16, 784)\n",
153 | "(16, 10)\n"
154 | ]
155 | }
156 | ]
157 | },
158 | {
159 | "cell_type": "code",
160 | "metadata": {
161 | "colab": {
162 | "base_uri": "https://localhost:8080/"
163 | },
164 | "id": "5pPM1dZ4QyYe",
165 | "outputId": "3317666b-e167-46b7-8cf4-b8592adc065a"
166 | },
167 | "source": [
168 | "def custom_transform(x):\n",
169 | " return np.ravel(np.array(x, dtype=np.float32))\n",
170 | "\n",
171 | "def custom_collate_fn(batch):\n",
172 | " transposed_data = list(zip(*batch))\n",
173 | "\n",
174 | " labels = np.array(transposed_data[1])\n",
175 | " imgs = np.stack(transposed_data[0])\n",
176 | "\n",
177 | " return imgs, labels\n",
178 | "\n",
179 | "batch_size = 128\n",
180 | "train_dataset = MNIST(root='train_mnist', train=True, download=True, transform=custom_transform)\n",
181 | "test_dataset = MNIST(root='test_mnist', train=False, download=True, transform=custom_transform)\n",
182 | "\n",
183 | "train_loader = DataLoader(train_dataset, batch_size, shuffle=True, collate_fn=custom_collate_fn, drop_last=True)\n",
184 | "test_loader = DataLoader(test_dataset, batch_size, shuffle=False, collate_fn=custom_collate_fn, drop_last=True)\n",
185 | "\n",
186 | "# test\n",
187 | "batch_data = next(iter(train_loader))\n",
188 | "imgs = batch_data[0]\n",
189 | "lbls = batch_data[1]\n",
190 | "print(imgs.shape, imgs[0].dtype, lbls.shape, lbls[0].dtype)\n",
191 | "\n",
192 | "# optimization - loading the whole dataset into memory\n",
193 | "train_images = jnp.array(train_dataset.data).reshape(len(train_dataset), -1)\n",
194 | "train_lbls = jnp.array(train_dataset.targets)\n",
195 | "\n",
196 | "test_images = jnp.array(test_dataset.data).reshape(len(test_dataset), -1)\n",
197 | "test_lbls = jnp.array(test_dataset.targets)"
198 | ],
199 | "execution_count": null,
200 | "outputs": [
201 | {
202 | "output_type": "stream",
203 | "name": "stdout",
204 | "text": [
205 | "(128, 784) float32 (128,) int64\n"
206 | ]
207 | }
208 | ]
209 | },
210 | {
211 | "cell_type": "code",
212 | "metadata": {
213 | "id": "YQEYcSNzVeim"
214 | },
215 | "source": [
216 | "num_epochs = 5\n",
217 | "\n",
218 | "def loss_fn(params, imgs, gt_lbls):\n",
219 | " predictions = batched_MLP_predict(params, imgs)\n",
220 | "\n",
221 | " return -jnp.mean(predictions * gt_lbls)\n",
222 | "\n",
223 | "def accuracy(params, dataset_imgs, dataset_lbls):\n",
224 | " pred_classes = jnp.argmax(batched_MLP_predict(params, dataset_imgs), axis=1)\n",
225 | " return jnp.mean(dataset_lbls == pred_classes)\n",
226 | "\n",
227 | "@jit\n",
228 | "def update(params, imgs, gt_lbls, lr=0.01):\n",
229 | " loss, grads = value_and_grad(loss_fn)(params, imgs, gt_lbls)\n",
230 | "\n",
231 | " return loss, jax.tree_multimap(lambda p, g: p - lr*g, params, grads)\n",
232 | "\n",
233 | "# Create a MLP\n",
234 | "MLP_params = init_MLP([np.prod(mnist_img_size), 512, 256, len(MNIST.classes)], key)\n",
235 | "\n",
236 | "for epoch in range(num_epochs):\n",
237 | "\n",
238 | " for cnt, (imgs, lbls) in enumerate(train_loader):\n",
239 | "\n",
240 | " gt_labels = jax.nn.one_hot(lbls, len(MNIST.classes))\n",
241 | " \n",
242 | " loss, MLP_params = update(MLP_params, imgs, gt_labels)\n",
243 | " \n",
244 | " if cnt % 50 == 0:\n",
245 | " print(loss)\n",
246 | "\n",
247 | " print(f'Epoch {epoch}, train acc = {accuracy(MLP_params, train_images, train_lbls)} test acc = {accuracy(MLP_params, test_images, test_lbls)}')\n"
248 | ],
249 | "execution_count": null,
250 | "outputs": []
251 | },
252 | {
253 | "cell_type": "code",
254 | "metadata": {
255 | "colab": {
256 | "base_uri": "https://localhost:8080/",
257 | "height": 316
258 | },
259 | "id": "YmdBRBvU1wuA",
260 | "outputId": "efcfa75e-d0bb-4f16-9fb2-e85e82a53bcf"
261 | },
262 | "source": [
263 | "imgs, lbls = next(iter(test_loader))\n",
264 | "img = imgs[0].reshape(mnist_img_size)\n",
265 | "gt_lbl = lbls[0]\n",
266 | "print(img.shape)\n",
267 | "\n",
268 | "import matplotlib.pyplot as plt\n",
269 | "\n",
270 | "pred = jnp.argmax(MLP_predict(MLP_params, np.ravel(img)))\n",
271 | "print('pred', pred)\n",
272 | "print('gt', gt_lbl)\n",
273 | "\n",
274 | "plt.imshow(img); plt.show()"
275 | ],
276 | "execution_count": null,
277 | "outputs": [
278 | {
279 | "output_type": "stream",
280 | "name": "stdout",
281 | "text": [
282 | "(28, 28)\n",
283 | "pred 7\n",
284 | "gt 7\n"
285 | ]
286 | },
287 | {
288 | "output_type": "display_data",
289 | "data": {
290 | "image/png": "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\n",
291 | "text/plain": [
292 | ""
293 | ]
294 | },
295 | "metadata": {
296 | "needs_background": "light"
297 | }
298 | }
299 | ]
300 | },
301 | {
302 | "cell_type": "markdown",
303 | "metadata": {
304 | "id": "TwgI3fZbKRqM"
305 | },
306 | "source": [
307 | "# Visualizations"
308 | ]
309 | },
310 | {
311 | "cell_type": "code",
312 | "metadata": {
313 | "colab": {
314 | "base_uri": "https://localhost:8080/",
315 | "height": 299
316 | },
317 | "id": "jddJj8zo4D1e",
318 | "outputId": "fb157d1c-4fbe-45a5-c84d-6abe38355a5e"
319 | },
320 | "source": [
321 | "w = MLP_params[0][0]\n",
322 | "print(w.shape)\n",
323 | "\n",
324 | "w_single = w[500, :].reshape(mnist_img_size)\n",
325 | "print(w_single.shape)\n",
326 | "plt.imshow(w_single); plt.show()"
327 | ],
328 | "execution_count": null,
329 | "outputs": [
330 | {
331 | "output_type": "stream",
332 | "name": "stdout",
333 | "text": [
334 | "(512, 784)\n",
335 | "(28, 28)\n"
336 | ]
337 | },
338 | {
339 | "output_type": "display_data",
340 | "data": {
341 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAPsAAAD4CAYAAAAq5pAIAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAa60lEQVR4nO2deZBddZXHv6f3NZ3u7EtnT4BAWJslLIIgVEAFBEUWKRzR6BSO4qijpVXKTJU1ztSAS80UY1jGaCGog0BURBjAYRXTYUtIAp2EbJ3eknTS3en1vT7zRx5WxP59b9udfq/H3/dT1dWv73m/+/vd373fd1/f8zvnmLtDCPHXT16uByCEyA4SuxCRILELEQkSuxCRILELEQkFWe2spNyLK2qCdqsZoO1ThwrD++7lfaeLuN0TPvaK2/rD+67gO7cpCcfVET4uABgsSvCYuJHGvCmK+BuK2xK6ziN9A0iVhe2D/LCRx6cNBd18XryA9J1w5Q9W8HmZVtpB7a1tE6ndyNB9Qpq2ZQy0HkCqo3vIAx+V2M1sBYDvAcgHcLe7f5u9v7iiBkvf/4Ww/WPNtL+WtdODtkkb+InvmMfVnCqlZiy8a0fQdmD5bNq2aCU/rtanZlF799wUtef1hI8tr4+L0Wv5p+T8/6RmDFTwS2jvSWFFd8/ggipv5Ods0nr+adBflR/uewrfd+95ndR+6wlPUfudP7iC2vPIKU297wBt6+TD/e0vrQr3SfdKMLN8AP8B4FIASwFcZ2ZLR7o/IcTYMpr/2c8AsMXdt7l7P4AHAPCPMyFEzhiN2GcB2HXE37sz2/4EM1tpZvVmVp/qPTSK7oQQo2HMn8a7+yp3r3P3uoKS8rHuTggRYDRibwRQe8TfszPbhBDjkNGIfS2AxWY238yKAFwLYM3RGZYQ4mhjo4l6M7PLAHwXh11v97r7t9j7SxfN9IV3fDJo793EfZNTXgmPdfBvuEN436tTqb3oIHdRlbaE+2Y+UwDoWMDt1Zv4DvIHuL3x4rALq3xKN21rL1ZR+6SL91B7y3Mzqf3qq54N2l7cO5+2rSrqofb3TGqg9u89e0nQdsXpL9O2T/70DGovbeXnpKyFu0t3fDB8vdX+ljZF6efC5+Slz/wEHW+2HH0/u7s/CuDR0exDCJEdtFxWiEiQ2IWIBIldiEiQ2IWIBIldiEiQ2IWIhKzGs3tvPvreCPvSFzzEwwpbzpoQtBX+lPvRS2q4H73ggn3UvndfRbjvZh7PXtLG+z54NT/uno4Sai9qDIeRTn+gmLbdsYL7i8+csp3aHyqbQe331Z8ZNg7yeTl96TZqn1LA561iS/jy/t/13I/eVddH7YfmculMquf2wurw+oeW08to2/Kf1wZtqfbwtag7uxCRILELEQkSuxCRILELEQkSuxCRILELEQlZdb1ZSRqFx4VT8O46NmEH9WFTqiQhpXHYcwYASA/wqZg27WDQNnsRzwb6+vOLqX2gnae2PWnJTmrfsC8cQ9tZy11v837Fs8v+4ak6ardP8FRjSz/fErQdPGcebXtwIZ+Xb6y9nNorifesq5a7HJPcqemErLz24S5qz39pStCWKuNjm7QhHPpb0BMOd9adXYhIkNiFiASJXYhIkNiFiASJXYhIkNiFiASJXYhIyKqffXAgD4dawlVhCg+Eq24CgC8LhwVOeYSHgfZWcz98xxJeP3j2v4Q/F9d9hqfArjyW++HzX6mm9h2vLaT2hR8K++G7lnF/8daGsL8XACZu5PeDKT/n52zLZ8NrAIrb+TlpWRcO5QSA8mbePk3c9GVDZ1v+Iz1Tua97+hq+fmHPhfx6soVhP33hTr7vgq5w+XBLh8etO7sQkSCxCxEJErsQkSCxCxEJErsQkSCxCxEJErsQkZBVPzvyAJSkg+aBydy3uWTa3qBtT+0c2jZ9VjiOHgBm/pin7337qrA/ubCVNkVXVzgFNgAUc5cvOhaEY5QBYODx8LF3L+Vx10Xt/PM+b4Casee8hMFXhX3CeceEbQBQ8btKas9LKGVdelU4lr55/TTatqyJH9dp/0CSKwCYcPMiam8+L7y2onIXL/fcfE54XcfA7vB1Oiqxm9l2AJ0A0gBS7s4zHQghcsbRuLO/193Dt1whxLhA/7MLEQmjFbsDeNzM1pnZyqHeYGYrzazezOrTnTxfmRBi7Bjt1/hz3b3RzKYCeMLMNrv7M0e+wd1XAVgFAMXzZ/MnKkKIMWNUd3Z3b8z8bgXwEABeLU8IkTNGLHYzKzezyndeA7gEwIajNTAhxNFlNF/jpwF4yMze2c9P3P0x2iJtsC7SZUIJ320kvnnhZTto2537ecx481k8Lnv6Kc3htuum07aVW/m+B7gbHvkzwnnCAaC7MBzLb6SELwBUbud9J1HYxc9Z+eZw/0UdPOa7h4fa48CJ3B896edhX/rEhOUB++r4vh97nHuZCy7nHfQfGz6nqYt4uWh7Nuxnd3L7HrHY3X0bgJNG2l4IkV3kehMiEiR2ISJBYhciEiR2ISJBYhciErIb4moOLwgvorN+7q5g4ZY7n55L25566UZqx0xufvGlcD1pr+QhqIPt3PVWsZMvLCx/D09F3fbarKBtxvPcbbfzEp6C2xLcof013EVVvSl8P+mcy+81cx/m8VWeN5nay/aFx9a0nJ+TRffz2N6dl/D2l175e2p/dNvxQZsZvx4q9oSvt3wybN3ZhYgEiV2ISJDYhYgEiV2ISJDYhYgEiV2ISJDYhYiErPrZ8/oN5dvDXc65k4fDN/2YOMOfqKFtu1M8nPKVzfOovWZT2N/s+dwXveiGN6l9y31LqP3Aw2E/OgD0kwRAe84ldYsBpMv4GoHShLLIBd38EqpZG86z3V/JY1i3fZT70Y0PHXvOC9/LPJ/7snefz+dt/i8OUvsjxTyPS8GccIq24jW8BHjzinAK7oEXVbJZiOiR2IWIBIldiEiQ2IWIBIldiEiQ2IWIBIldiEgw9+wVaSmeU+szvnxr0F65nX/2dCwL+xeLd3M/emEH9xdXN/C47IPzw/7kgXN5OWhbx3NFT9rE+953HPdl9xxDUg938rZlpMQvACz9IF8jsOMHfI1A79XhWPz833J/ckE3NSf62buuDJ+Xikf4OWk9O1xaHACQx3VjA/xaLm4Lz3v/Qp6DYOGd4QP/w6t3oqOrcciLXXd2ISJBYhciEiR2ISJBYhciEiR2ISJBYhciEiR2ISIhq/Hs+b1A9cawv7v97F7a/pjalqCt4WC4nDMADC7mTtvWynJqf/8HwnnAH3maxy4X8qrJaEooF52ayP3wee1kjUHCMopDi8NrFwCgKI/7m1PX7KP2jtawP7uigq99aD+Nj614D19b4ZvDfe9bRpuiYiuXRsn5PKf9wU4eD1/wdvh6q/llMW3bOS9sS5M8/Yl3djO718xazWzDEdtqzOwJM2vI/ObFz4UQOWc4X+N/CGDFu7Z9FcCT7r4YwJOZv4UQ45hEsbv7MwD2v2vzFQBWZ16vBnDlUR6XEOIoM9IHdNPcvSnzuhnAtNAbzWylmdWbWX2qJ5x3Swgxtoz6abwfjqQJPgZy91XuXufudQWl/CGYEGLsGKnYW8xsBgBkfodTiAohxgUjFfsaADdlXt8E4JGjMxwhxFiR6Gc3s/sBXABgspntBvBNAN8G8DMzuxnADgDXDKs3A9LFYd/qtovvpc0X3f+ZoM0n8Xrak37J/Z7lTdzH//LZYT9+5WJeP73kAR63nT/AneGNF1IzrXk/c3Ebbdv16HRqb5tfQe1F9/N8/bY8PLb0mTwPwJwfllF7/y38C2XrvrCf3VP8PtfrfHHE4As8p31hwvqG/onhNxzq42Pre09n0JZ+MRzrnih2d78uYLooqa0QYvyg5bJCRILELkQkSOxCRILELkQkSOxCREJWQ1wLO1KY+WQ4JPKUi66l7Vl54Vm/5IdiCSmzp39rG7W/efdxQVv3AtoUZWne9/5r+TJia+TurwUPhUNBd366krYt4VGieKuBlMkG8Nt/vp3a33//l4K23ia+orJ/Ag+BTf90KrXb8WHbrOd56G7bDV3UPriez2thwsrwM64Ilyd/484T+L4fDV8P+QdHEeIqhPjrQGIXIhIkdiEiQWIXIhIkdiEiQWIXIhIkdiEiIbslm2trfdatXwjaFzzMS9Xai+uDti23n07bTn+Rj61sDw9x3frRcHrfqjd5KuhDs/kcD9byvvPfLqH2dGl4/4PFvG9PKD18zD3cYXxwCfc3t9WFbTOe430fWMzntfotnmL7xK+/FrS9cM+pvO9lvB501Rt8bGl+yjBI1jccms+Pi6W53rb6DvQ071LJZiFiRmIXIhIkdiEiQWIXIhIkdiEiQWIXIhIkdiEiIavx7JYGig6EY5QLG99dUu5P2Ux86dNe4n23nsZjowuO5WmLQUoXF3Vwf3He8bys8f6mKmpf8GtebnrnpeGxFzXyz3NLWGbRfA4fW9IagnyyhKBnMj8nJW0JKbYv4Me2a91JQVvhBQk5BPbw66FzIffD5w3wY5t5alPQdmj7FNq2a3E4bfpgSXjOdGcXIhIkdiEiQWIXIhIkdiEiQWIXIhIkdiEiQWIXIhKy62d3IJ9UVm47fxZtP1ga9nV3T+WHUtXAfbbt5/NY+pmPhEv4dnO3KCZ+j8d8H/g4j1/unM/LTePYcI7z3i0853xxe4Kv+6K91J7ex/dvxeFjm31uC227836ekH+whPu6J24IXxMHTuElmcvaEsomn8Cvl7nT+bxtXzs7aJv+Gr9WUzeE16PsLQxrJPHObmb3mlmrmW04YtttZtZoZq9mfi5L2o8QIrcM52v8DwGsGGL7d9z95MzPo0d3WEKIo02i2N39GQB8HasQYtwzmgd0nzWz1zNf86tDbzKzlWZWb2b16e6EAlhCiDFjpGK/E8BCACcDaAIQrO7n7qvcvc7d6/LLeCE/IcTYMSKxu3uLu6fdfRDAXQDOOLrDEkIcbUYkdjObccSfHwIQrj8rhBgXJPrZzex+ABcAmGxmuwF8E8AFZnYyAAewHcCnh9uhE7dub0J8M4hb1S9sp007Nk2k9qoqHjO+5wPhz8Xj/onHq7efPp3aF97J88bvO4HPS2F92I9/qJbXIS9r5vnP/eFJ1I5zw7XhAaCudlfQtu7pY2nb/EkJMeHzuC+7a3N43if9nhem37ecH9dpc8LHBQBbfraE2qtXtAVt+1OTaduCF8L2wa6wpBPF7u7XDbH5nqR2QojxhZbLChEJErsQkSCxCxEJErsQkSCxCxEJWQ1xLdo/gLk/awzaq+87SNuv/+nS8L7Xctda7U3bqf3N38+j9tKDYTfQthtn0rY3XP0Utd/79AXUXryfu6DOvvT1oO2V1cto24s+xWtZP3vHmdSeX8Rde688flzQVtPAQ1RbVvRRe1s7Dx3GWeHQX3ueh+YiIcV2yx0LefM5vP3+jWH3GbvWAODQ7PC8sVLQurMLEQkSuxCRILELEQkSuxCRILELEQkSuxCRILELEQnmnuBQPIqUTan1Y6/8QtB+8BjenpX/7avlIYkY4J9reV081LOsOdy+IOzOBQD01XB7WRM/B11zuN+1b3b42Ceu4ymTu+bwvit2877z+3j7faeHU0kXVXE/+uB2ntlo8omt1D6hKLz/ho08bXnhAX69zF4eXi8CANubE0KDCdN+WUztrSRVTOMd30Xfrl1DnjTd2YWIBIldiEiQ2IWIBIldiEiQ2IWIBIldiEiQ2IWIhKzGs6dLuC89VcVLFxcvCqd7Htw8gfddmrCeICGLNZYfCJp6X0lIU30W9wf3/WoqtVdu52P3/HAQc/cs3raogx94T0I56lQZt1dsCY8tVcbTOZfyDN3o3D+N2puPCa8/yK/h6zL6C/nYSm7m81pyPS+zXdoWbj/zsw20bfsT4TTVRtIL6M4uRCRI7EJEgsQuRCRI7EJEgsQuRCRI7EJEgsQuRCRk1c8+sfIQLr/opaD96bt4jvIZv+kI2rZfX0XbVi7jTtveF3mZ3L4Z4alKLeBx2akHubO6/CPN1J7/73xs/RPDY+vjSwBQsYv7iw/wysNIVw9Qe3dheGxL/ovXCdhyAx98xU6+RmDJJ+qDtvabltO2ztMbYPO3+DnxQV6Gu6C3JGh7cw2f9NQp4fUmXhLOKZ94ZzezWjN72sw2mtkbZvb5zPYaM3vCzBoyv6uT9iWEyB3D+RqfAvBFd18K4CwAt5jZUgBfBfCkuy8G8GTmbyHEOCVR7O7e5O4vZ153AtgEYBaAKwCszrxtNYArx2qQQojR8xc9oDOzeQBOAfASgGnu3pQxNQMYcqGyma00s3ozq+9p5//bCiHGjmGL3cwqADwI4FZ3/5MnZX44a+WQT3rcfZW717l7XWk1T6QnhBg7hiV2MyvEYaHf5+6/yGxuMbMZGfsMADy0SwiRUxJdb2ZmAO4BsMnd7zjCtAbATQC+nfn9SNK+ercVY9N184P20u+30PZ9m8MurKkX8tS+fgcPI+04n7ugil8Jl/hdvHorbdtw6wJqx4M8VLOgio+tc17Y3TJxM3dPFXbzssn9U3jfcx7m94tdl4TbF39/P21bczd3vbWez8NUDz1wYrjv+oR56eLHPevnPAR27zIurdS53O3IKHg97Ga2XpLyfBj7PgfAjQDWm9mrmW1fw2GR/8zMbgawA8A1wx2sECL7JIrd3Z9DOLXDRUd3OEKIsULLZYWIBIldiEiQ2IWIBIldiEiQ2IWIhKyGuPZPLMCuy8M+5bzf8vZ7/zZcG7ngKV6CdyDBj17WlJBSeXm47+YD3I9eeJDve/8J3Nc95zGSHxhAaXPY55sU4to5l3/el08NhxUDwIFFPLS4bE/Y9noVP2cnfPJtak//OLxmAwD2nxee17Ou3kDbPvfycdTeOT8hBtb49Zb/Rjj1+d99+Fe07Xf7wk6wUYW4CiH+OpDYhYgEiV2ISJDYhYgEiV2ISJDYhYgEiV2ISMiqnx0GDBaFzc7dzThpVjhmvXIuT3n19jeOpfams8nAAFQ9Xh60pcq4Hz0/IRuXJRx3xVd2U/vutfOCthPP3ELb9l3Pj3vHx+ZSe8cSXma7qD3sj654LZxOGQD2PMP96NVv9lC7pcNlk7f+einfdzU/p7XXb6P2zn+cTe07Lg3P++1PXUbbljaG5zSvJ3z/1p1diEiQ2IWIBIldiEiQ2IWIBIldiEiQ2IWIBIldiEjIrp8d3Kdc1sxjgOtfXhS0zfod77esg/tkJ2zjecD3n0CMs8MldAGg+vGwvxcAihLi3Qe+yOtvlH8u7I9uWLOYtu2/PRynDwBTH+Cx9NVv8bG3fCQ8N+mu8NoFAOicR82JfZftDY+98QJ+nytt5vve+ALPYZD6OF9ckUdKJJS08Fj5hFD5cJ8jayaE+P+GxC5EJEjsQkSCxC5EJEjsQkSCxC5EJEjsQkTCcOqz1wL4EYBpABzAKnf/npndBuBTANoyb/2auz/K9jWh+hDe95E/BO2/eex0OpbivWH/Y2kL93W3nhaurw4AxQcTgso97HeteK6MNs0b4Pu+8cu/ofbvnM+L5RYUHgraau/mp7ipmM8LjPvZ2xdzn/Dkh8NrDPorucM4Xcx93Ttu4WNL7ykO2ioW8Proh6bzWHs0hfcNAMVbefuZ54VzFGzbMZW2LWoKrwlxcvsezqKaFIAvuvvLZlYJYJ2ZPZGxfcfd/20Y+xBC5Jjh1GdvAtCUed1pZpsA8FIeQohxx1/0P7uZzQNwCoCXMps+a2avm9m9ZlYdaLPSzOrNrL6nPSE/kxBizBi22M2sAsCDAG519w4AdwJYCOBkHL7z3z5UO3df5e517l5XWs3/zxFCjB3DEruZFeKw0O9z918AgLu3uHva3QcB3AXgjLEbphBitCSK3cwMwD0ANrn7HUdsn3HE2z4EgJfFFELklOE8jT8HwI0A1pvZq5ltXwNwnZmdjMPuuO0APp20o4795XjygfAXgIHj+f/01ZM6g7bOhhre90Lu/iqbz0sToyec+nf5hZtp0423LaP2uxvOpvayN7gbh4U87riUH3f1Ru7+ajkj4X4wN+z2A4DyS/YHbU1vzQjaAKBsasK+n+blogu6w8eW2jLkI6Y/UpXm81LRyN1+O6/iKbbf3hQ+9qIDfM6nvBY+p00kkns4T+OfAzCUw5P61IUQ4wutoBMiEiR2ISJBYhciEiR2ISJBYhciEiR2ISIhq6mk81JAaVvYf+mb+HLa0q3h4badwsMhZz/F/aK7L5xA7fPX9Adtz59yKm3b9T7u6656bCK1Y0U7NU+8uzJo25fHT3F+Px9bxQ4+r+01vORz0d8PBG21CWsuO+ZwP/qUl7kf/u3Lw6HHZU2875IP8vTdPYP8Ppm/aRK1L/zvcEj27vfysOPe6nDfgyTiWHd2ISJBYhciEiR2ISJBYhciEiR2ISJBYhciEiR2ISLB3EdY/3UknZm1AdhxxKbJAPZmbQB/GeN1bON1XIDGNlKO5tjmuvuUoQxZFfufdW5W7+51ORsAYbyObbyOC9DYRkq2xqav8UJEgsQuRCTkWuyrctw/Y7yObbyOC9DYRkpWxpbT/9mFENkj13d2IUSWkNiFiISciN3MVpjZm2a2xcy+mosxhDCz7Wa23sxeNbP6HI/lXjNrNbMNR2yrMbMnzKwh85snQM/u2G4zs8bM3L1qZpflaGy1Zva0mW00szfM7POZ7TmdOzKurMxb1v9nN7N8AG8BuBjAbgBrAVzn7huzOpAAZrYdQJ2753wBhpm9B0AXgB+5+wmZbf8KYL+7fzvzQVnt7l8ZJ2O7DUBXrst4Z6oVzTiyzDiAKwF8HDmcOzKua5CFecvFnf0MAFvcfZu79wN4AMAVORjHuMfdnwHw7pIqVwBYnXm9GocvlqwTGNu4wN2b3P3lzOtOAO+UGc/p3JFxZYVciH0WgF1H/L0b46veuwN43MzWmdnKXA9mCKa5+ztJlZoBTMvlYIYgsYx3NnlXmfFxM3cjKX8+WvSA7s85191PBXApgFsyX1fHJX74f7Dx5DsdVhnvbDFEmfE/ksu5G2n589GSC7E3Aqg94u/ZmW3jAndvzPxuBfAQxl8p6pZ3KuhmfvPMiFlkPJXxHqrMOMbB3OWy/HkuxL4WwGIzm29mRQCuBbAmB+P4M8ysPPPgBGZWDuASjL9S1GsA3JR5fROAR3I4lj9hvJTxDpUZR47nLuflz9096z8ALsPhJ/JbAXw9F2MIjGsBgNcyP2/kemwA7sfhr3UDOPxs42YAkwA8CaABwP8AqBlHY/sxgPUAXsdhYc3I0djOxeGv6K8DeDXzc1mu546MKyvzpuWyQkSCHtAJEQkSuxCRILELEQkSuxCRILELEQkSuxCRILELEQn/ByEtCrqpk9g4AAAAAElFTkSuQmCC\n",
342 | "text/plain": [
343 | ""
344 | ]
345 | },
346 | "metadata": {
347 | "needs_background": "light"
348 | }
349 | }
350 | ]
351 | },
352 | {
353 | "cell_type": "code",
354 | "metadata": {
355 | "colab": {
356 | "base_uri": "https://localhost:8080/",
357 | "height": 484
358 | },
359 | "id": "AZxm7G3j4iOS",
360 | "outputId": "521c3ad2-147d-4076-eea0-6537f32dafa0"
361 | },
362 | "source": [
363 | "# todo: visualize embeddings using t-SNE\n",
364 | "\n",
365 | "from sklearn.manifold import TSNE\n",
366 | "\n",
367 | "def fetch_activations(params, x):\n",
368 | " hidden_layers = params[:-1]\n",
369 | "\n",
370 | " activation = x\n",
371 | " for w, b in hidden_layers:\n",
372 | " activation = jax.nn.relu(jnp.dot(w, activation) + b)\n",
373 | "\n",
374 | " return activation\n",
375 | "\n",
376 | "batched_fetch_activations = vmap(fetch_activations, in_axes=(None, 0))\n",
377 | "imgs, lbls = next(iter(test_loader))\n",
378 | "\n",
379 | "batch_activations = batched_fetch_activations(MLP_params, imgs)\n",
380 | "print(batch_activations.shape) # (128, 2)\n",
381 | "\n",
382 | "t_sne_embeddings = TSNE(n_components=2, perplexity=30,).fit_transform(batch_activations)\n",
383 | "cora_label_to_color_map = {0: \"red\", 1: \"blue\", 2: \"green\", 3: \"orange\", 4: \"yellow\", 5: \"pink\", 6: \"gray\"}\n",
384 | "\n",
385 | "for class_id in range(10):\n",
386 | " plt.scatter(t_sne_embeddings[lbls == class_id, 0], t_sne_embeddings[lbls == class_id, 1], s=20, color=cora_label_to_color_map[class_id])\n",
387 | "plt.show()"
388 | ],
389 | "execution_count": null,
390 | "outputs": [
391 | {
392 | "output_type": "stream",
393 | "name": "stdout",
394 | "text": [
395 | "(128, 256)\n"
396 | ]
397 | },
398 | {
399 | "output_type": "error",
400 | "ename": "KeyError",
401 | "evalue": "ignored",
402 | "traceback": [
403 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
404 | "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)",
405 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mclass_id\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0mplt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mscatter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mt_sne_embeddings\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mlbls\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mclass_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt_sne_embeddings\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mlbls\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mclass_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m20\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcolor\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcora_label_to_color_map\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mclass_id\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
406 | "\u001b[0;31mKeyError\u001b[0m: 7"
407 | ]
408 | },
409 | {
410 | "output_type": "display_data",
411 | "data": {
412 | "image/png": "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\n",
413 | "text/plain": [
414 | ""
415 | ]
416 | },
417 | "metadata": {
418 | "needs_background": "light"
419 | }
420 | }
421 | ]
422 | },
423 | {
424 | "cell_type": "code",
425 | "metadata": {
426 | "colab": {
427 | "base_uri": "https://localhost:8080/"
428 | },
429 | "id": "MHL27HumNgwf",
430 | "outputId": "d44b1e9c-33d6-4dd3-cf05-b3a1e19f0194"
431 | },
432 | "source": [
433 | "# todo: dead neurons\n",
434 | "\n",
435 | "def fetch_activations2(params, x):\n",
436 | " hidden_layers = params[:-1]\n",
437 | " collector = []\n",
438 | "\n",
439 | " activation = x\n",
440 | " for w, b in hidden_layers:\n",
441 | " activation = jax.nn.relu(jnp.dot(w, activation) + b)\n",
442 | " collector.append(activation)\n",
443 | "\n",
444 | " return collector\n",
445 | "\n",
446 | "batched_fetch_activations2 = vmap(fetch_activations2, in_axes=(None, 0))\n",
447 | "\n",
448 | "imgs, lbls = next(iter(test_loader))\n",
449 | "\n",
450 | "MLP_params2 = init_MLP([np.prod(mnist_img_size), 512, 256, len(MNIST.classes)], key)\n",
451 | "\n",
452 | "batch_activations = batched_fetch_activations2(MLP_params2, imgs)\n",
453 | "print(batch_activations[1].shape) # (128, 512/256)\n",
454 | "\n",
455 | "dead_neurons = [np.ones(act.shape[1:]) for act in batch_activations]\n",
456 | "\n",
457 | "for layer_id, activations in enumerate(batch_activations):\n",
458 | " dead_neurons[layer_id] = np.logical_and(dead_neurons[layer_id], (activations == 0).all(axis=0))\n",
459 | "\n",
460 | "for layers in dead_neurons:\n",
461 | " print(np.sum(layers))"
462 | ],
463 | "execution_count": null,
464 | "outputs": [
465 | {
466 | "output_type": "stream",
467 | "name": "stdout",
468 | "text": [
469 | "(128, 256)\n",
470 | "0\n",
471 | "7\n"
472 | ]
473 | }
474 | ]
475 | },
476 | {
477 | "cell_type": "markdown",
478 | "metadata": {
479 | "id": "jMmOX-VSKTjQ"
480 | },
481 | "source": [
482 | "# Parallelization"
483 | ]
484 | },
485 | {
486 | "cell_type": "code",
487 | "metadata": {
488 | "id": "1aCkdHuhKUqV"
489 | },
490 | "source": [
491 | ""
492 | ],
493 | "execution_count": null,
494 | "outputs": []
495 | }
496 | ]
497 | }
--------------------------------------------------------------------------------
/Tutorial_4_Flax_Zero2Hero_Colab.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "attachments": {},
5 | "cell_type": "markdown",
6 | "metadata": {},
7 | "source": [
8 | "[](https://deepnote.com/launch?url=https%3A%2F%2Fgithub.com%2Fgordicaleksa%2Fget-started-with-JAX%2Fblob%2Fmain%2FTutorial_4_Flax_Zero2Hero_Colab.ipynb)"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "metadata": {},
14 | "source": [
15 | ""
16 | ]
17 | },
18 | {
19 | "cell_type": "markdown",
20 | "metadata": {
21 | "id": "TbMr3-5oun69"
22 | },
23 | "source": [
24 | "# Flax: From Zero to Hero!\n",
25 | "\n",
26 | "This notebook heavily relies on the [official Flax docs](https://flax.readthedocs.io/en/latest/) and [examples](https://github.com/google/flax/blob/main/examples/) + some additional code/modifications, comments/notes, etc."
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {
32 | "id": "C1qve53yeof5"
33 | },
34 | "source": [
35 | "### Enter Flax - the basics ❤️\n",
36 | "\n",
37 | "Before you jump into the Flax world I strongly recommend you check out my JAX tutorials, as I won't be covering the details of JAX here.\n",
38 | "\n",
39 | "* (Tutorial 1) ML with JAX: From Zero to Hero ([video](https://www.youtube.com/watch?v=SstuvS-tVc0), [notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_1_JAX_Zero2Hero_Colab.ipynb))\n",
40 | "* (Tutorial 2) ML with JAX: from Hero to Hero Pro+ ([video](https://www.youtube.com/watch?v=CQQaifxuFcs), [notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_2_JAX_HeroPro%2B_Colab.ipynb))\n",
41 | "* (Tutorial 3) ML with JAX: Coding a Neural Network from Scratch in Pure JAX ([video](https://www.youtube.com/watch?v=6_PqUPxRmjY), [notebook](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_3_JAX_Neural_Network_from_Scratch_Colab.ipynb))\n",
42 | "\n",
43 | "That out of the way - let's start with the basics!"
44 | ]
45 | },
46 | {
47 | "cell_type": "code",
48 | "execution_count": 1,
49 | "metadata": {
50 | "id": "GHcasJkggdZN"
51 | },
52 | "outputs": [],
53 | "source": [
54 | "# Install Flax and JAX\n",
55 | "!pip install --upgrade -q \"jax[cuda11_cudnn805]\" -f https://storage.googleapis.com/jax-releases/jax_releases.html\n",
56 | "!pip install --upgrade -q git+https://github.com/google/flax.git\n",
57 | "!pip install --upgrade -q git+https://github.com/deepmind/dm-haiku # Haiku is here just for comparison purposes"
58 | ]
59 | },
60 | {
61 | "cell_type": "code",
62 | "execution_count": 2,
63 | "metadata": {
64 | "id": "HmVx7EjigrEZ"
65 | },
66 | "outputs": [],
67 | "source": [
68 | "import jax\n",
69 | "from jax import lax, random, numpy as jnp\n",
70 | "\n",
71 | "# NN lib built on top of JAX developed by Google Research (Brain team)\n",
72 | "# Flax was \"designed for flexibility\" hence the name (Flexibility + JAX -> Flax)\n",
73 | "import flax\n",
74 | "from flax.core import freeze, unfreeze\n",
75 | "from flax import linen as nn # nn notation also used in PyTorch and in Flax's older API\n",
76 | "from flax.training import train_state # a useful dataclass to keep train state\n",
77 | "\n",
78 | "# DeepMind's NN JAX lib - just for comparison purposes, we're not learning Haiku here\n",
79 | "import haiku as hk \n",
80 | "\n",
81 | "# JAX optimizers - a separate lib developed by DeepMind\n",
82 | "import optax\n",
83 | "\n",
84 | "# Flax doesn't have its own data loading functions - we'll be using PyTorch dataloaders\n",
85 | "from torchvision.datasets import MNIST\n",
86 | "from torch.utils.data import DataLoader\n",
87 | "\n",
88 | "# Python libs\n",
89 | "import functools # useful utilities for functional programs\n",
90 | "from typing import Any, Callable, Sequence, Optional\n",
91 | "\n",
92 | "# Other important 3rd party libs\n",
93 | "import numpy as np\n",
94 | "import matplotlib.pyplot as plt"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {
100 | "id": "aSDyQLgOesZp"
101 | },
102 | "source": [
103 | "The goal of this notebook is to get you started with Flax!\n",
104 | "\n",
105 | "I'll only cover the most essential parts of Flax (and Optax) - just as much as needed to get you started with training NNs!"
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": null,
111 | "metadata": {
112 | "id": "y1kdq0P_g7LU"
113 | },
114 | "outputs": [],
115 | "source": [
116 | "# Let's start with the simplest model possible: a single feed-forward layer (linear regression)\n",
117 | "model = nn.Dense(features=5)\n",
118 | "\n",
119 | "# All of the Flax NN layers inherit from the Module class (similarly to PyTorch)\n",
120 | "print(nn.Dense.__bases__)"
121 | ]
122 | },
123 | {
124 | "cell_type": "markdown",
125 | "metadata": {
126 | "id": "ux9Okie5PWpw"
127 | },
128 | "source": [
129 | "So how can we do inference with this simple model? 2 steps: init and apply!"
130 | ]
131 | },
132 | {
133 | "cell_type": "code",
134 | "execution_count": 4,
135 | "metadata": {
136 | "id": "QViTvJhFite2"
137 | },
138 | "outputs": [],
139 | "source": [
140 | "# Step 1: init\n",
141 | "seed = 23\n",
142 | "key1, key2 = random.split(random.PRNGKey(seed))\n",
143 | "x = random.normal(key1, (10,)) # create a dummy input, a 10-dimensional random vector\n",
144 | "\n",
145 | "# Initialization call - this gives us the actual model weights \n",
146 | "# (remember JAX handles state externally!)\n",
147 | "y, params = model.init_with_output(key2, x) \n",
148 | "print(y)\n",
149 | "print(jax.tree_map(lambda x: x.shape, params))\n",
150 | "\n",
151 | "# Note1: automatic shape inference\n",
152 | "# Note2: immutable structure (hence FrozenDict)\n",
153 | "# Note3: init_with_output if you care, for whatever reason, about the output here"
154 | ]
155 | },
156 | {
157 | "cell_type": "code",
158 | "execution_count": null,
159 | "metadata": {
160 | "id": "b3yFAqeTjdLj"
161 | },
162 | "outputs": [],
163 | "source": [
164 | "# Step 2: apply\n",
165 | "y = model.apply(params, x) # this is how you run prediction in Flax, state is external!\n",
166 | "print(y)"
167 | ]
168 | },
169 | {
170 | "cell_type": "code",
171 | "execution_count": null,
172 | "metadata": {
173 | "id": "31O_mx-Smalq"
174 | },
175 | "outputs": [],
176 | "source": [
177 | "try:\n",
178 | " y = model(x) # this doesn't work anymore (bye bye PyTorch syntax)\n",
179 | "except Exception as e:\n",
180 | " print(e)"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": null,
186 | "metadata": {
187 | "id": "fQYyv76sCJ25"
188 | },
189 | "outputs": [],
190 | "source": [
191 | "# todo: a small coding exercise - let's contrast Flax with Haiku"
192 | ]
193 | },
194 | {
195 | "cell_type": "code",
196 | "execution_count": null,
197 | "metadata": {
198 | "cellView": "form",
199 | "id": "UWr3hdpmFBng"
200 | },
201 | "outputs": [],
202 | "source": [
203 | "#@title Haiku vs Flax solution\n",
204 | "model = hk.transform(lambda x: hk.Linear(output_size=5)(x))\n",
205 | "\n",
206 | "seed = 23\n",
207 | "key1, key2 = random.split(random.PRNGKey(seed))\n",
208 | "x = random.normal(key1, (10,)) # create a dummy input, a 10-dimensional random vector\n",
209 | "\n",
210 | "params = model.init(key2, x)\n",
211 | "out = model.apply(params, None, x)\n",
212 | "print(out)\n",
213 | "\n",
214 | "print(hk.Linear.__bases__)"
215 | ]
216 | },
217 | {
218 | "cell_type": "markdown",
219 | "metadata": {
220 | "id": "wWBxTShUiLzW"
221 | },
222 | "source": [
223 | "All of this might (initially!) be overwhelming if you're used to stateful, object-oriented paradigm.\n",
224 | "\n",
225 | "What Flax offers is high performance and flexibility (similarly to JAX).\n",
226 | "\n",
227 | "Here are some [benchmark numbers](https://github.com/huggingface/transformers/tree/master/examples/flax/text-classification) from the HuggingFace team.\n",
228 | "\n",
229 | ""
230 | ]
231 | },
232 | {
233 | "cell_type": "markdown",
234 | "metadata": {
235 | "id": "eUBYtd40krx1"
236 | },
237 | "source": [
238 | "Now that we have a an answer to \"why should I learn Flax?\" - let's start our descent into Flaxlandia!\n",
239 | "\n",
240 | "### A toy example 🚚 - training a linear regression model\n",
241 | "\n",
242 | "We'll first implement a pure-JAX appoach and then we'll do it the Flax-way."
243 | ]
244 | },
245 | {
246 | "cell_type": "code",
247 | "execution_count": null,
248 | "metadata": {
249 | "id": "53-TXcbYkt9D"
250 | },
251 | "outputs": [],
252 | "source": [
253 | "# Defining a toy dataset\n",
254 | "n_samples = 150\n",
255 | "x_dim = 2 # putting small numbers here so that we can visualize the data easily\n",
256 | "y_dim = 1\n",
257 | "noise_amplitude = 0.1\n",
258 | "\n",
259 | "# Generate (random) ground truth W and b\n",
260 | "# Note: we could get W, b from a randomely initialized nn.Dense here, being closer to JAX for now \n",
261 | "key, w_key, b_key = random.split(random.PRNGKey(seed), num=3)\n",
262 | "W = random.normal(w_key, (x_dim, y_dim)) # weight\n",
263 | "b = random.normal(b_key, (y_dim,)) # bias\n",
264 | "\n",
265 | "# This is the structure that Flax expects (recall from the previous section!)\n",
266 | "true_params = freeze({'params': {'bias': b, 'kernel': W}})\n",
267 | "\n",
268 | "# Generate samples with additional noise\n",
269 | "key, x_key, noise_key = random.split(key, num=3)\n",
270 | "xs = random.normal(x_key, (n_samples, x_dim))\n",
271 | "ys = jnp.dot(xs, W) + b\n",
272 | "ys += noise_amplitude * random.normal(noise_key, (n_samples, y_dim))\n",
273 | "print(f'xs shape = {xs.shape} ; ys shape = {ys.shape}')"
274 | ]
275 | },
276 | {
277 | "cell_type": "code",
278 | "execution_count": 9,
279 | "metadata": {
280 | "colab": {
281 | "base_uri": "https://localhost:8080/",
282 | "height": 266
283 | },
284 | "id": "lc4-xoIapKCs",
285 | "outputId": "52656571-0aa5-4c6f-f522-83b0158c1b97"
286 | },
287 | "outputs": [
288 | {
289 | "data": {
290 | "text/plain": [
291 | ""
292 | ]
293 | },
294 | "execution_count": 9,
295 | "metadata": {},
296 | "output_type": "execute_result"
297 | },
298 | {
299 | "data": {
300 | "image/png": "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\n",
301 | "text/plain": [
302 | ""
303 | ]
304 | },
305 | "metadata": {
306 | "needs_background": "light"
307 | },
308 | "output_type": "display_data"
309 | }
310 | ],
311 | "source": [
312 | "# Let's visualize our data (becoming one with the data paradigm <3)\n",
313 | "fig = plt.figure()\n",
314 | "ax = fig.add_subplot(111, projection='3d')\n",
315 | "assert xs.shape[-1] == 2 and ys.shape[-1] == 1 # low dimensional data so that we can plot it\n",
316 | "ax.scatter(xs[:, 0], xs[:, 1], zs=ys)\n",
317 | "\n",
318 | "# todo: exercise - let's show that our data lies on the 2D plane embedded in 3D\n",
319 | "# option 1: analytic approach\n",
320 | "# option 2: data-driven approach"
321 | ]
322 | },
323 | {
324 | "cell_type": "code",
325 | "execution_count": 22,
326 | "metadata": {
327 | "id": "mKiCOyoikxcM"
328 | },
329 | "outputs": [],
330 | "source": [
331 | "def make_mse_loss(xs, ys):\n",
332 | " \n",
333 | " def mse_loss(params):\n",
334 | " \"\"\"Gives the value of the loss on the (xs, ys) dataset for the given model (params).\"\"\"\n",
335 | " \n",
336 | " # Define the squared loss for a single pair (x,y)\n",
337 | " def squared_error(x, y):\n",
338 | " pred = model.apply(params, x)\n",
339 | " # Inner because 'y' could have in general more than 1 dims\n",
340 | " return jnp.inner(y-pred, y-pred) / 2.0\n",
341 | "\n",
342 | " # Batched version via vmap\n",
343 | " return jnp.mean(jax.vmap(squared_error)(xs, ys), axis=0)\n",
344 | "\n",
345 | " return jax.jit(mse_loss) # and finally we jit the result (mse_loss is a pure function)\n",
346 | "\n",
347 | "mse_loss = make_mse_loss(xs, ys)\n",
348 | "value_and_grad_fn = jax.value_and_grad(mse_loss)"
349 | ]
350 | },
351 | {
352 | "cell_type": "code",
353 | "execution_count": null,
354 | "metadata": {
355 | "id": "phLYjH5ZkzLn"
356 | },
357 | "outputs": [],
358 | "source": [
359 | "# Let's reuse the simple feed-forward layer since it trivially implements linear regression\n",
360 | "model = nn.Dense(features=y_dim)\n",
361 | "params = model.init(key, xs)\n",
362 | "print(f'Initial params = {params}')\n",
363 | "\n",
364 | "# Let's set some reasonable hyperparams\n",
365 | "lr = 0.3\n",
366 | "epochs = 20\n",
367 | "log_period_epoch = 5\n",
368 | "\n",
369 | "print('-' * 50)\n",
370 | "for epoch in range(epochs):\n",
371 | " loss, grads = value_and_grad_fn(params)\n",
372 | " # SGD (closer to JAX again, but we'll progressively go towards how stuff is done in Flax)\n",
373 | " params = jax.tree_multimap(lambda p, g: p - lr * g, params, grads)\n",
374 | "\n",
375 | " if epoch % log_period_epoch == 0:\n",
376 | " print(f'epoch {epoch}, loss = {loss}')\n",
377 | "\n",
378 | "print('-' * 50)\n",
379 | "print(f'Learned params = {params}')\n",
380 | "print(f'Gt params = {true_params}')"
381 | ]
382 | },
383 | {
384 | "cell_type": "markdown",
385 | "metadata": {
386 | "id": "rvy6Oow2lLHu"
387 | },
388 | "source": [
389 | "Now let's do the same thing but this time with dedicated optimizers!\n",
390 | "\n",
391 | "Enter DeepMind's optax! ❤️🔥"
392 | ]
393 | },
394 | {
395 | "cell_type": "code",
396 | "execution_count": null,
397 | "metadata": {
398 | "id": "5hhcFZ7UlCov"
399 | },
400 | "outputs": [],
401 | "source": [
402 | "opt_sgd = optax.sgd(learning_rate=lr)\n",
403 | "opt_state = opt_sgd.init(params) # always the same pattern - handling state externally\n",
404 | "print(opt_state)\n",
405 | "# todo: exercise - compare Adam's and SGD's states"
406 | ]
407 | },
408 | {
409 | "cell_type": "code",
410 | "execution_count": null,
411 | "metadata": {
412 | "id": "t_EHHjy_lFGN"
413 | },
414 | "outputs": [],
415 | "source": [
416 | "params = model.init(key, xs) # let's start with fresh params again\n",
417 | "\n",
418 | "for epoch in range(epochs):\n",
419 | " loss, grads = value_and_grad_fn(params)\n",
420 | " updates, opt_state = opt_sgd.update(grads, opt_state) # arbitrary optim logic!\n",
421 | " params = optax.apply_updates(params, updates)\n",
422 | "\n",
423 | " if epoch % log_period_epoch == 0:\n",
424 | " print(f'epoch {epoch}, loss = {loss}')\n",
425 | "\n",
426 | "# Note 1: as expected we get the same loss values\n",
427 | "# Note 2: we'll later see more concise ways to handle all of these state components (hint: TrainState)"
428 | ]
429 | },
430 | {
431 | "cell_type": "markdown",
432 | "metadata": {
433 | "id": "QF1gAYSzxQ1R"
434 | },
435 | "source": [
436 | "In this toy SGD example Optax may not seem that useful but it's very powerful.\n",
437 | "\n",
438 | "You can build arbitrary optimizers with arbitrary hyperparam schedules, chaining, param freezing, etc. You can check the [official docs here](https://optax.readthedocs.io/en/latest/)."
439 | ]
440 | },
441 | {
442 | "cell_type": "code",
443 | "execution_count": 8,
444 | "metadata": {
445 | "cellView": "form",
446 | "id": "rKbis5O0KQYH"
447 | },
448 | "outputs": [],
449 | "source": [
450 | "#@title Optax Advanced Examples\n",
451 | "# This cell won't \"compile\" (no ml_collections package) and serves just as an example\n",
452 | "\n",
453 | "# Example from Flax (ImageNet example)\n",
454 | "# https://github.com/google/flax/blob/main/examples/imagenet/train.py#L88\n",
455 | "def create_learning_rate_fn(\n",
456 | " config: ml_collections.ConfigDict,\n",
457 | " base_learning_rate: float,\n",
458 | " steps_per_epoch: int):\n",
459 | " \"\"\"Create learning rate schedule.\"\"\"\n",
460 | " warmup_fn = optax.linear_schedule(\n",
461 | " init_value=0., end_value=base_learning_rate,\n",
462 | " transition_steps=config.warmup_epochs * steps_per_epoch)\n",
463 | " cosine_epochs = max(config.num_epochs - config.warmup_epochs, 1)\n",
464 | " cosine_fn = optax.cosine_decay_schedule(\n",
465 | " init_value=base_learning_rate,\n",
466 | " decay_steps=cosine_epochs * steps_per_epoch)\n",
467 | " schedule_fn = optax.join_schedules(\n",
468 | " schedules=[warmup_fn, cosine_fn],\n",
469 | " boundaries=[config.warmup_epochs * steps_per_epoch])\n",
470 | " return schedule_fn\n",
471 | "\n",
472 | "tx = optax.sgd(\n",
473 | " learning_rate=learning_rate_fn,\n",
474 | " momentum=config.momentum,\n",
475 | " nesterov=True,\n",
476 | ")\n",
477 | "\n",
478 | "# Example from Haiku (ImageNet example)\n",
479 | "# https://github.com/deepmind/dm-haiku/blob/main/examples/imagenet/train.py#L116\n",
480 | "def make_optimizer() -> optax.GradientTransformation:\n",
481 | " \"\"\"SGD with nesterov momentum and a custom lr schedule.\"\"\"\n",
482 | " return optax.chain(\n",
483 | " optax.trace(\n",
484 | " decay=FLAGS.optimizer_momentum,\n",
485 | " nesterov=FLAGS.optimizer_use_nesterov),\n",
486 | " optax.scale_by_schedule(lr_schedule), optax.scale(-1))"
487 | ]
488 | },
489 | {
490 | "cell_type": "markdown",
491 | "metadata": {
492 | "id": "WFAeHIEwL0ZH"
493 | },
494 | "source": [
495 | "Now let's go beyond these extremely simple models!"
496 | ]
497 | },
498 | {
499 | "cell_type": "markdown",
500 | "metadata": {
501 | "id": "7_33y-bTl6bd"
502 | },
503 | "source": [
504 | "### Creating custom models ⭐"
505 | ]
506 | },
507 | {
508 | "cell_type": "code",
509 | "execution_count": null,
510 | "metadata": {
511 | "id": "JOrJHqTSl75M"
512 | },
513 | "outputs": [],
514 | "source": [
515 | "class MLP(nn.Module):\n",
516 | " num_neurons_per_layer: Sequence[int] # data field (nn.Module is Python's dataclass)\n",
517 | "\n",
518 | " def setup(self): # because dataclass is implicitly using the __init__ function... :')\n",
519 | " self.layers = [nn.Dense(n) for n in self.num_neurons_per_layer]\n",
520 | "\n",
521 | " def __call__(self, x):\n",
522 | " activation = x\n",
523 | " for i, layer in enumerate(self.layers):\n",
524 | " activation = layer(activation)\n",
525 | " if i != len(self.layers) - 1:\n",
526 | " activation = nn.relu(activation)\n",
527 | " return activation\n",
528 | "\n",
529 | "x_key, init_key = random.split(random.PRNGKey(seed))\n",
530 | "\n",
531 | "model = MLP(num_neurons_per_layer=[16, 8, 1]) # define an MLP model\n",
532 | "x = random.uniform(x_key, (4,4)) # dummy input\n",
533 | "params = model.init(init_key, x) # initialize via init\n",
534 | "y = model.apply(params, x) # do a forward pass via apply\n",
535 | "\n",
536 | "print(jax.tree_map(jnp.shape, params))\n",
537 | "print(f'Output: {y}')\n",
538 | "\n",
539 | "# todo: exercise - use @nn.compact pattern instead\n",
540 | "# todo: check out https://realpython.com/python-data-classes/"
541 | ]
542 | },
543 | {
544 | "cell_type": "markdown",
545 | "metadata": {
546 | "id": "TEhC-WdPnAYp"
547 | },
548 | "source": [
549 | "Great! \n",
550 | "\n",
551 | "Now that we know how to build more complex models let's dive deeper and understand how the 'nn.Dense' module is designed itself.\n",
552 | "\n",
553 | "#### Introducing \"param\""
554 | ]
555 | },
556 | {
557 | "cell_type": "code",
558 | "execution_count": null,
559 | "metadata": {
560 | "id": "Z9YhSgxjnBQg"
561 | },
562 | "outputs": [],
563 | "source": [
564 | "class MyDenseImp(nn.Module):\n",
565 | " num_neurons: int\n",
566 | " weight_init: Callable = nn.initializers.lecun_normal()\n",
567 | " bias_init: Callable = nn.initializers.zeros\n",
568 | "\n",
569 | " @nn.compact\n",
570 | " def __call__(self, x):\n",
571 | " weight = self.param('weight', # parametar name (as it will appear in the FrozenDict)\n",
572 | " self.weight_init, # initialization function, RNG passed implicitly through init fn\n",
573 | " (x.shape[-1], self.num_neurons)) # shape info\n",
574 | " bias = self.param('bias', self.bias_init, (self.num_neurons,))\n",
575 | "\n",
576 | " return jnp.dot(x, weight) + bias\n",
577 | "\n",
578 | "x_key, init_key = random.split(random.PRNGKey(seed))\n",
579 | "\n",
580 | "model = MyDenseImp(num_neurons=3) # initialize the model\n",
581 | "x = random.uniform(x_key, (4,4)) # dummy input\n",
582 | "params = model.init(init_key, x) # initialize via init\n",
583 | "y = model.apply(params, x) # do a forward pass via apply\n",
584 | "\n",
585 | "print(jax.tree_map(jnp.shape, params))\n",
586 | "print(f'Output: {y}')\n",
587 | "\n",
588 | "# todo: exercise - check out the source code:\n",
589 | "# https://github.com/google/flax/blob/main/flax/linen/linear.py\n",
590 | "# https://github.com/google/jax/blob/main/jax/_src/nn/initializers.py#L150 <- to see why lecun_normal() vs zeros (no brackets)"
591 | ]
592 | },
593 | {
594 | "cell_type": "code",
595 | "execution_count": null,
596 | "metadata": {
597 | "id": "AqCPhl9fBI_Z"
598 | },
599 | "outputs": [],
600 | "source": [
601 | "from inspect import signature\n",
602 | "\n",
603 | "# You can see it expects a PRNG key and it is passed implicitly through the init fn (same for zeros)\n",
604 | "print(signature(nn.initializers.lecun_normal()))"
605 | ]
606 | },
607 | {
608 | "cell_type": "markdown",
609 | "metadata": {
610 | "id": "MWB8HvLHn6g0"
611 | },
612 | "source": [
613 | "So far we've only seen **trainable** params. \n",
614 | "\n",
615 | "ML models often times have variables which are part of the state but are not optimized via gradient descent.\n",
616 | "\n",
617 | "Let's see how we can handle them using a simple (and contrived) example!\n",
618 | "\n",
619 | "#### Introducing \"variable\"\n",
620 | "\n",
621 | "*Note on terminology: variable is a broader term and it includes both params (trainable variables) as well as non-trainable vars.*"
622 | ]
623 | },
624 | {
625 | "cell_type": "code",
626 | "execution_count": null,
627 | "metadata": {
628 | "id": "oGE6qTHHngYh"
629 | },
630 | "outputs": [],
631 | "source": [
632 | "class BiasAdderWithRunningMean(nn.Module):\n",
633 | " decay: float = 0.99\n",
634 | "\n",
635 | " @nn.compact\n",
636 | " def __call__(self, x):\n",
637 | " is_initialized = self.has_variable('batch_stats', 'ema')\n",
638 | "\n",
639 | " # 'batch_stats' is not an arbitrary name!\n",
640 | " # Flax uses that name in its implementation of BatchNorm (hard-coded, probably not the best of designs?)\n",
641 | " ema = self.variable('batch_stats', 'ema', lambda shape: jnp.zeros(shape), x.shape[1:])\n",
642 | "\n",
643 | " # self.param will by default add this variable to 'params' collection (vs 'batch_stats' above)\n",
644 | " # Again some idiosyncrasies here we need to pass a key even though we don't actually use it...\n",
645 | " bias = self.param('bias', lambda key, shape: jnp.zeros(shape), x.shape[1:])\n",
646 | "\n",
647 | " if is_initialized:\n",
648 | " # self.variable returns a reference hence .value\n",
649 | " ema.value = self.decay * ema.value + (1.0 - self.decay) * jnp.mean(x, axis=0, keepdims=True)\n",
650 | "\n",
651 | " return x - ema.value + bias\n",
652 | "\n",
653 | "x_key, init_key = random.split(random.PRNGKey(seed))\n",
654 | "\n",
655 | "model = BiasAdderWithRunningMean()\n",
656 | "x = random.uniform(x_key, (10,4)) # dummy input\n",
657 | "variables = model.init(init_key, x)\n",
658 | "print(f'Multiple collections = {variables}') # we can now see a new collection 'batch_stats'\n",
659 | "\n",
660 | "# We have to use mutable since regular params are not modified during the forward\n",
661 | "# pass, but these variables are. We can't keep state internally (because JAX) so we have to return it.\n",
662 | "y, updated_non_trainable_params = model.apply(variables, x, mutable=['batch_stats'])\n",
663 | "print(updated_non_trainable_params)"
664 | ]
665 | },
666 | {
667 | "cell_type": "code",
668 | "execution_count": null,
669 | "metadata": {
670 | "id": "PuzwVt8RoHvY"
671 | },
672 | "outputs": [],
673 | "source": [
674 | "# Let's see how we could train such model!\n",
675 | "def update_step(opt, apply_fn, x, opt_state, params, non_trainable_params):\n",
676 | "\n",
677 | " def loss_fn(params):\n",
678 | " y, updated_non_trainable_params = apply_fn(\n",
679 | " {'params': params, **non_trainable_params}, \n",
680 | " x, mutable=list(non_trainable_params.keys()))\n",
681 | " \n",
682 | " loss = ((x - y) ** 2).sum() # not doing anything really, just for the demo purpose\n",
683 | "\n",
684 | " return loss, updated_non_trainable_params\n",
685 | "\n",
686 | " (loss, non_trainable_params), grads = jax.value_and_grad(loss_fn, has_aux=True)(params)\n",
687 | " updates, opt_state = opt.update(grads, opt_state)\n",
688 | " params = optax.apply_updates(params, updates)\n",
689 | " \n",
690 | " return opt_state, params, non_trainable_params # all of these represent the state - ugly, for now\n",
691 | "\n",
692 | "model = BiasAdderWithRunningMean()\n",
693 | "x = jnp.ones((10,4)) # dummy input, using ones because it's easier to see what's going on\n",
694 | "\n",
695 | "variables = model.init(random.PRNGKey(seed), x)\n",
696 | "non_trainable_params, params = variables.pop('params')\n",
697 | "del variables # delete variables to avoid wasting resources (this pattern is used in the official code)\n",
698 | "\n",
699 | "sgd_opt = optax.sgd(learning_rate=0.1) # originally you'll see them use the 'tx' naming (from opTaX)\n",
700 | "opt_state = sgd_opt.init(params)\n",
701 | "\n",
702 | "for _ in range(3):\n",
703 | " # We'll later see how TrainState abstraction will make this step much more elegant!\n",
704 | " opt_state, params, non_trainable_params = update_step(sgd_opt, model.apply, x, opt_state, params, non_trainable_params)\n",
705 | " print(non_trainable_params)"
706 | ]
707 | },
708 | {
709 | "cell_type": "markdown",
710 | "metadata": {
711 | "id": "gzWUq5vBrWMe"
712 | },
713 | "source": [
714 | "Let's go a level up in abstraction again now that we understand params and variables!\n",
715 | "\n",
716 | "Certain layers like BatchNorm will use variables in the background.\n",
717 | "\n",
718 | "Let's see a last example that is conceptually as complicated as it gets when it comes to Flax's idiosyncrasies, and high-level at the same time."
719 | ]
720 | },
721 | {
722 | "cell_type": "code",
723 | "execution_count": null,
724 | "metadata": {
725 | "id": "rDw2986orY0a"
726 | },
727 | "outputs": [],
728 | "source": [
729 | "class DDNBlock(nn.Module):\n",
730 | " \"\"\"Dense, dropout + batchnorm combo.\n",
731 | "\n",
732 | " Contains trainable variables (params), non-trainable variables (batch stats),\n",
733 | " and stochasticity in the forward pass (because of dropout).\n",
734 | " \"\"\"\n",
735 | " num_neurons: int\n",
736 | " training: bool\n",
737 | "\n",
738 | " @nn.compact\n",
739 | " def __call__(self, x):\n",
740 | " x = nn.Dense(self.num_neurons)(x)\n",
741 | " x = nn.Dropout(rate=0.5, deterministic=not self.training)(x)\n",
742 | " x = nn.BatchNorm(use_running_average=not self.training)(x)\n",
743 | " return x\n",
744 | "\n",
745 | "key1, key2, key3, key4 = random.split(random.PRNGKey(seed), 4)\n",
746 | "\n",
747 | "model = DDNBlock(num_neurons=3, training=True)\n",
748 | "x = random.uniform(key1, (3,4,4))\n",
749 | "\n",
750 | "# New: because of Dropout we now have to include its unique key - kinda weird, but you get used to it\n",
751 | "variables = model.init({'params': key2, 'dropout': key3}, x)\n",
752 | "print(variables)\n",
753 | "\n",
754 | "# And same here, everything else remains the same as the previous example\n",
755 | "y, non_trainable_params = model.apply(variables, x, rngs={'dropout': key4}, mutable=['batch_stats'])\n",
756 | "\n",
757 | "# Let's run these model variables during \"evaluation\":\n",
758 | "eval_model = DDNBlock(num_neurons=3, training=False)\n",
759 | "# Because training=False we don't have stochasticity in the forward pass neither do we update the stats\n",
760 | "y = eval_model.apply(variables, x)"
761 | ]
762 | },
763 | {
764 | "cell_type": "markdown",
765 | "metadata": {
766 | "id": "Ys1y-yM8vzT8"
767 | },
768 | "source": [
769 | "### A fully-fledged CNN on MNIST example in Flax! 💥\n",
770 | "\n",
771 | "Modified the official MNIST example here: https://github.com/google/flax/tree/main/examples/mnist\n",
772 | "\n",
773 | "We'll be using PyTorch dataloading instead of TFDS.\n",
774 | "\n",
775 | "Let's start by defining a model:"
776 | ]
777 | },
778 | {
779 | "cell_type": "code",
780 | "execution_count": 3,
781 | "metadata": {
782 | "id": "MD8t9K2Nv0yC"
783 | },
784 | "outputs": [],
785 | "source": [
786 | "class CNN(nn.Module): # lots of hardcoding, but it serves a purpose for a simple demo\n",
787 | " @nn.compact\n",
788 | " def __call__(self, x):\n",
789 | " x = nn.Conv(features=32, kernel_size=(3, 3))(x)\n",
790 | " x = nn.relu(x)\n",
791 | " x = nn.avg_pool(x, window_shape=(2, 2), strides=(2, 2))\n",
792 | " x = nn.Conv(features=64, kernel_size=(3, 3))(x)\n",
793 | " x = nn.relu(x)\n",
794 | " x = nn.avg_pool(x, window_shape=(2, 2), strides=(2, 2))\n",
795 | " x = x.reshape((x.shape[0], -1)) # flatten\n",
796 | " x = nn.Dense(features=256)(x)\n",
797 | " x = nn.relu(x)\n",
798 | " x = nn.Dense(features=10)(x)\n",
799 | " x = nn.log_softmax(x)\n",
800 | " return x"
801 | ]
802 | },
803 | {
804 | "cell_type": "markdown",
805 | "metadata": {
806 | "id": "rVgWLMhiSAYv"
807 | },
808 | "source": [
809 | "Let's add the data loading support in PyTorch!\n",
810 | "\n",
811 | "I'll be reusing code from [tutorial #3](https://github.com/gordicaleksa/get-started-with-JAX/blob/main/Tutorial_3_JAX_Neural_Network_from_Scratch_Colab.ipynb):"
812 | ]
813 | },
814 | {
815 | "cell_type": "code",
816 | "execution_count": 4,
817 | "metadata": {
818 | "id": "UZ-og2UOUUWD"
819 | },
820 | "outputs": [],
821 | "source": [
822 | "def custom_transform(x):\n",
823 | " # A couple of modifications here compared to tutorial #3 since we're using a CNN\n",
824 | " # Input: (28, 28) uint8 [0, 255] torch.Tensor, Output: (28, 28, 1) float32 [0, 1] np array\n",
825 | " return np.expand_dims(np.array(x, dtype=np.float32), axis=2) / 255.\n",
826 | "\n",
827 | "def custom_collate_fn(batch):\n",
828 | " \"\"\"Provides us with batches of numpy arrays and not PyTorch's tensors.\"\"\"\n",
829 | " transposed_data = list(zip(*batch))\n",
830 | "\n",
831 | " labels = np.array(transposed_data[1])\n",
832 | " imgs = np.stack(transposed_data[0])\n",
833 | "\n",
834 | " return imgs, labels\n",
835 | "\n",
836 | "mnist_img_size = (28, 28, 1)\n",
837 | "batch_size = 128\n",
838 | "\n",
839 | "train_dataset = MNIST(root='train_mnist', train=True, download=True, transform=custom_transform)\n",
840 | "test_dataset = MNIST(root='test_mnist', train=False, download=True, transform=custom_transform)\n",
841 | "\n",
842 | "train_loader = DataLoader(train_dataset, batch_size, shuffle=True, collate_fn=custom_collate_fn, drop_last=True)\n",
843 | "test_loader = DataLoader(test_dataset, batch_size, shuffle=False, collate_fn=custom_collate_fn, drop_last=True)\n",
844 | "\n",
845 | "# optimization - loading the whole dataset into memory\n",
846 | "train_images = jnp.array(train_dataset.data)\n",
847 | "train_lbls = jnp.array(train_dataset.targets)\n",
848 | "\n",
849 | "# np.expand_dims is to convert shape from (10000, 28, 28) -> (10000, 28, 28, 1)\n",
850 | "# We don't have to do this for training images because custom_transform does it for us.\n",
851 | "test_images = np.expand_dims(jnp.array(test_dataset.data), axis=3)\n",
852 | "test_lbls = jnp.array(test_dataset.targets)"
853 | ]
854 | },
855 | {
856 | "cell_type": "code",
857 | "execution_count": 5,
858 | "metadata": {
859 | "colab": {
860 | "base_uri": "https://localhost:8080/",
861 | "height": 282
862 | },
863 | "id": "2HeXX51NU0k6",
864 | "outputId": "43dad5bf-20c2-4c5a-9705-12b2e422f915"
865 | },
866 | "outputs": [
867 | {
868 | "name": "stdout",
869 | "output_type": "stream",
870 | "text": [
871 | "7\n"
872 | ]
873 | },
874 | {
875 | "data": {
876 | "image/png": "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\n",
877 | "text/plain": [
878 | ""
879 | ]
880 | },
881 | "metadata": {
882 | "needs_background": "light"
883 | },
884 | "output_type": "display_data"
885 | }
886 | ],
887 | "source": [
888 | "# Visualize a single image\n",
889 | "imgs, lbls = next(iter(test_loader))\n",
890 | "img = imgs[0].reshape(mnist_img_size)[:, :, 0]\n",
891 | "gt_lbl = lbls[0]\n",
892 | "\n",
893 | "print(gt_lbl)\n",
894 | "plt.imshow(img); plt.show()"
895 | ]
896 | },
897 | {
898 | "cell_type": "markdown",
899 | "metadata": {
900 | "id": "TsGPQKx0SPL-"
901 | },
902 | "source": [
903 | "Great - we have our data pipeline ready and the model architecture defined.\n",
904 | "\n",
905 | "Now let's define core training functions:"
906 | ]
907 | },
908 | {
909 | "cell_type": "code",
910 | "execution_count": 6,
911 | "metadata": {
912 | "id": "qD8ztbEsVM43"
913 | },
914 | "outputs": [],
915 | "source": [
916 | "@jax.jit\n",
917 | "def train_step(state, imgs, gt_labels):\n",
918 | " def loss_fn(params):\n",
919 | " logits = CNN().apply({'params': params}, imgs)\n",
920 | " one_hot_gt_labels = jax.nn.one_hot(gt_labels, num_classes=10)\n",
921 | " loss = -jnp.mean(jnp.sum(one_hot_gt_labels * logits, axis=-1))\n",
922 | " return loss, logits\n",
923 | " \n",
924 | " (_, logits), grads = jax.value_and_grad(loss_fn, has_aux=True)(state.params)\n",
925 | " state = state.apply_gradients(grads=grads) # this is the whole update now! concise!\n",
926 | " metrics = compute_metrics(logits=logits, gt_labels=gt_labels) # duplicating loss calculation but it's a bit cleaner\n",
927 | " return state, metrics\n",
928 | "\n",
929 | "@jax.jit\n",
930 | "def eval_step(state, imgs, gt_labels):\n",
931 | " logits = CNN().apply({'params': state.params}, imgs)\n",
932 | " return compute_metrics(logits=logits, gt_labels=gt_labels)"
933 | ]
934 | },
935 | {
936 | "cell_type": "code",
937 | "execution_count": 7,
938 | "metadata": {
939 | "id": "v5VblVs2VWxo"
940 | },
941 | "outputs": [],
942 | "source": [
943 | "def train_one_epoch(state, dataloader, epoch):\n",
944 | " \"\"\"Train for 1 epoch on the training set.\"\"\"\n",
945 | " batch_metrics = []\n",
946 | " for cnt, (imgs, labels) in enumerate(dataloader):\n",
947 | " state, metrics = train_step(state, imgs, labels)\n",
948 | " batch_metrics.append(metrics)\n",
949 | "\n",
950 | " # Aggregate the metrics\n",
951 | " batch_metrics_np = jax.device_get(batch_metrics) # pull from the accelerator onto host (CPU)\n",
952 | " epoch_metrics_np = {\n",
953 | " k: np.mean([metrics[k] for metrics in batch_metrics_np])\n",
954 | " for k in batch_metrics_np[0]\n",
955 | " }\n",
956 | "\n",
957 | " return state, epoch_metrics_np\n",
958 | "\n",
959 | "def evaluate_model(state, test_imgs, test_lbls):\n",
960 | " \"\"\"Evaluate on the validation set.\"\"\"\n",
961 | " metrics = eval_step(state, test_imgs, test_lbls)\n",
962 | " metrics = jax.device_get(metrics) # pull from the accelerator onto host (CPU)\n",
963 | " metrics = jax.tree_map(lambda x: x.item(), metrics) # np.ndarray -> scalar\n",
964 | " return metrics"
965 | ]
966 | },
967 | {
968 | "cell_type": "code",
969 | "execution_count": 8,
970 | "metadata": {
971 | "id": "xiV5yiA4BKEk"
972 | },
973 | "outputs": [],
974 | "source": [
975 | "# This one will keep things nice and tidy compared to our previous examples\n",
976 | "def create_train_state(key, learning_rate, momentum):\n",
977 | " cnn = CNN()\n",
978 | " params = cnn.init(key, jnp.ones([1, *mnist_img_size]))['params']\n",
979 | " sgd_opt = optax.sgd(learning_rate, momentum)\n",
980 | " # TrainState is a simple built-in wrapper class that makes things a bit cleaner\n",
981 | " return train_state.TrainState.create(apply_fn=cnn.apply, params=params, tx=sgd_opt)\n",
982 | "\n",
983 | "def compute_metrics(*, logits, gt_labels):\n",
984 | " one_hot_gt_labels = jax.nn.one_hot(gt_labels, num_classes=10)\n",
985 | "\n",
986 | " loss = -jnp.mean(jnp.sum(one_hot_gt_labels * logits, axis=-1))\n",
987 | " accuracy = jnp.mean(jnp.argmax(logits, -1) == gt_labels)\n",
988 | "\n",
989 | " metrics = {\n",
990 | " 'loss': loss,\n",
991 | " 'accuracy': accuracy,\n",
992 | " }\n",
993 | " return metrics"
994 | ]
995 | },
996 | {
997 | "cell_type": "code",
998 | "execution_count": 9,
999 | "metadata": {
1000 | "colab": {
1001 | "base_uri": "https://localhost:8080/"
1002 | },
1003 | "id": "s8EFriHnVcJO",
1004 | "outputId": "cb40714f-6150-44d6-e1e0-290b72a23eda"
1005 | },
1006 | "outputs": [
1007 | {
1008 | "name": "stdout",
1009 | "output_type": "stream",
1010 | "text": [
1011 | "Train epoch: 1, loss: 0.2903152406215668, accuracy: 91.86198115348816\n",
1012 | "Test epoch: 1, loss: 44.35035705566406, accuracy: 94.77999806404114\n",
1013 | "Train epoch: 2, loss: 0.058339256793260574, accuracy: 98.23551177978516\n",
1014 | "Test epoch: 2, loss: 17.13631820678711, accuracy: 97.33999967575073\n"
1015 | ]
1016 | }
1017 | ],
1018 | "source": [
1019 | "# Finally let's define the high-level training/val loops\n",
1020 | "seed = 0 # needless to say these should be in a config or defined like flags\n",
1021 | "learning_rate = 0.1\n",
1022 | "momentum = 0.9\n",
1023 | "num_epochs = 2\n",
1024 | "batch_size = 32\n",
1025 | "\n",
1026 | "train_state = create_train_state(jax.random.PRNGKey(seed), learning_rate, momentum)\n",
1027 | "\n",
1028 | "for epoch in range(1, num_epochs + 1):\n",
1029 | " train_state, train_metrics = train_one_epoch(train_state, train_loader, epoch)\n",
1030 | " print(f\"Train epoch: {epoch}, loss: {train_metrics['loss']}, accuracy: {train_metrics['accuracy'] * 100}\")\n",
1031 | "\n",
1032 | " test_metrics = evaluate_model(train_state, test_images, test_lbls)\n",
1033 | " print(f\"Test epoch: {epoch}, loss: {test_metrics['loss']}, accuracy: {test_metrics['accuracy'] * 100}\")\n",
1034 | "\n",
1035 | "# todo: exercise - how would we go about adding dropout? What about BatchNorm? What would have to change?"
1036 | ]
1037 | },
1038 | {
1039 | "cell_type": "markdown",
1040 | "metadata": {
1041 | "id": "6U-BIjQ1v4ff"
1042 | },
1043 | "source": [
1044 | "Bonus point: a walk-through the \"non-toy\", distributed ImageNet CNN training example.\n",
1045 | "\n",
1046 | "Head over to https://github.com/google/flax/tree/main/examples/imagenet\n",
1047 | "\n",
1048 | "You'll keep seeing the same pattern/structure in all official Flax examples."
1049 | ]
1050 | },
1051 | {
1052 | "cell_type": "markdown",
1053 | "metadata": {
1054 | "id": "6Q4C2M2tv_0J"
1055 | },
1056 | "source": [
1057 | "### Further learning resources 📚\n",
1058 | "\n",
1059 | "Aside from the [official docs](https://flax.readthedocs.io/en/latest/) and [examples](https://github.com/google/flax/tree/main/examples) I found [HuggingFace's Flax examples](https://github.com/huggingface/transformers/tree/master/examples/flax) and the resources from their [\"community week\"](https://github.com/huggingface/transformers/tree/master/examples/research_projects/jax-projects) useful as well.\n",
1060 | "\n",
1061 | "Finally, [source code](https://github.com/google/flax) is also your friend, as the library is still evolving."
1062 | ]
1063 | },
1064 | {
1065 | "cell_type": "markdown",
1066 | "metadata": {
1067 | "id": "T5DqxlZ-SD3e"
1068 | },
1069 | "source": [
1070 | "### Connect with me ❤️\n",
1071 | "\n",
1072 | "Last but not least I regularly post AI-related stuff (paper summaries, AI news, etc.) on my Twitter/LinkedIn. We also have an ever increasing Discord community (1600+ members at the time of writing this). If you care about any of these I encourage you to connect! \n",
1073 | "\n",
1074 | "Social: \n",
1075 | "💼 LinkedIn - https://www.linkedin.com/in/aleksagordic/ \n",
1076 | "🐦 Twitter - https://twitter.com/gordic_aleksa \n",
1077 | "👨👩👧👦 Discord - https://discord.gg/peBrCpheKE \n",
1078 | "🙏 Patreon - https://www.patreon.com/theaiepiphany \n",
1079 | "\n",
1080 | "Content: \n",
1081 | "📺 YouTube - https://www.youtube.com/c/TheAIEpiphany/ \n",
1082 | "📚 Medium - https://gordicaleksa.medium.com/ \n",
1083 | "💻 GitHub - https://github.com/gordicaleksa \n",
1084 | "📢 AI Newsletter - https://aiepiphany.substack.com/ "
1085 | ]
1086 | }
1087 | ],
1088 | "metadata": {
1089 | "accelerator": "GPU",
1090 | "colab": {
1091 | "collapsed_sections": [],
1092 | "name": "Tutorial 4: Flax Zero2Hero.ipynb",
1093 | "provenance": []
1094 | },
1095 | "kernelspec": {
1096 | "display_name": "Python 3 (ipykernel)",
1097 | "language": "python",
1098 | "name": "python3"
1099 | },
1100 | "language_info": {
1101 | "codemirror_mode": {
1102 | "name": "ipython",
1103 | "version": 3
1104 | },
1105 | "file_extension": ".py",
1106 | "mimetype": "text/x-python",
1107 | "name": "python",
1108 | "nbconvert_exporter": "python",
1109 | "pygments_lexer": "ipython3",
1110 | "version": "3.9.0"
1111 | }
1112 | },
1113 | "nbformat": 4,
1114 | "nbformat_minor": 1
1115 | }
1116 |
--------------------------------------------------------------------------------