twm_4: zero findings, with the notebook's outputs proved untouched
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
runnability / runnability (push) Waiting to run

228 ruff findings to 0 across the notebook, its partial export and twm.py, and
mypy --strict is clean on both .py files.

A notebook is a deliverable that happens to contain code, so the check that
mattered was not "does it still run" -- it needs a GPU, MNIST, CIFAR-10 and a
slice of Quick, Draw! to run at all -- but "is anything except the code still
exactly as submitted". Hashing every key of the .ipynb EXCEPT cell `source`
gives 60242e116d12549d11c93f7f before the first edit and after the last one,
across four passes and a `ruff format`. The file also round-trips byte-for-byte
through json.dumps(indent=1, ensure_ascii=False), which is what made editing
cells programmatically safe rather than a reformat of all 141 of them; the
diff touches 38.

print() became sys.stdout.write() rather than logging. In a notebook the print
IS the output, and a logger with no handler configured would have silently
emptied every cell -- the exact regression scripts/check_refactor_safety.py
was written for. matprint's `print(x, end="  ")` and bare `print()` are
preserved as writes with and without a trailing newline.

Both `show_samples` and `show_samples_rgb` took (indices, preds, images,
labels, count, names), which is six arguments and a mutable default. They now
take a frozen `Samples` holding the last four, which also removed the same
four-value tuple from seven call sites. Reviewing that diff is how I caught
that I had transcribed `real_score = preds[sample][real_id]` as
`samples.labels[sample]` -- a bug I introduced and then fixed before commit.

Other real things, not just rule satisfaction: `np.in1d` is removed in NumPy 2
(NPY201) and is now `np.isin(...).ravel()`; `test_model` computed
correct_indices and incorrect_indices and used neither; two cells grew `img`
inside a loop so it was only bound by the `else` branch; and the Quick, Draw!
downloader now builds a URL whose https scheme is a literal, which is what
S310 was asking for.

The README claimed the .py was "an nbconvert export of that notebook". It is
an export of the CIFAR-10 section only -- 28 of 81 code cells -- and has been
edited since. It says that now, and says the two are separate artifacts rather
than one being regenerable from the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-29 08:10:31 +02:00
parent f780d023be
commit 1fd1de2f82
6 changed files with 445 additions and 311 deletions

View File

@ -5,8 +5,15 @@ Coursework for TWM (Techniki Wizyjne / machine vision), lab 4.
| Path | What it is | | Path | What it is |
|---|---| |---|---|
| `TWM_KerasIntro.ipynb` | The submitted notebook — the deliverable. | | `TWM_KerasIntro.ipynb` | The submitted notebook — the deliverable. |
| `TWM_KerasIntro.py` | An `nbconvert` export of that notebook. Derived, not edited by hand. | | `TWM_KerasIntro.py` | An `nbconvert` export of the notebook's **CIFAR-10 section only**. |
| `twm.py` | Helper code used by the notebook. | | `twm.py` | Prints the TensorFlow version the notebook will run against. |
The `.py` export is kept because it is what makes the notebook's code The notebook is the source of truth. The `.py` is kept because it makes that
reviewable and diffable; edit the notebook, then re-export. section's code reviewable and diffable, but it is not a full export: it covers
28 of the notebook's 81 code cells, all of them from the CIFAR-10 half, and it
has been edited since. Treat the two as separate artifacts rather than
regenerating one from the other.
Both need a GPU. The notebook's first cell calls `sys.exit()` when TensorFlow
reports no physical GPU device, and running it end to end downloads MNIST,
CIFAR-10 and a slice of the Quick, Draw! bitmaps into `data/`.

View File

@ -89,10 +89,16 @@
"source": [ "source": [
"import itertools\n", "import itertools\n",
"import random # for generating random numbers\n", "import random # for generating random numbers\n",
"import sys\n",
"import urllib.request\n",
"from dataclasses import dataclass\n",
"from pathlib import Path\n",
"\n", "\n",
"import matplotlib.pyplot as plt # MATLAB like plotting routines\n", "import matplotlib.pyplot as plt # MATLAB like plotting routines\n",
"import numpy as np # advanced math library\n", "import numpy as np # advanced math library\n",
"import seaborn as sns\n",
"import tensorflow as tf\n", "import tensorflow as tf\n",
"from keras import Model\n",
"from keras.datasets import mnist # MNIST dataset is included in Keras%\n", "from keras.datasets import mnist # MNIST dataset is included in Keras%\n",
"from keras.layers import (\n", "from keras.layers import (\n",
" Activation,\n", " Activation,\n",
@ -101,9 +107,14 @@
") # Types of layers to be used in our model\n", ") # Types of layers to be used in our model\n",
"from keras.models import Sequential # Model type to be used\n", "from keras.models import Sequential # Model type to be used\n",
"from keras.utils import to_categorical # NumPy related tools\n", "from keras.utils import to_categorical # NumPy related tools\n",
"from matplotlib import patheffects\n",
"from sklearn.manifold import TSNE\n",
"from sklearn.metrics import confusion_matrix\n", "from sklearn.metrics import confusion_matrix\n",
"from sklearn.preprocessing import OneHotEncoder\n",
"\n", "\n",
"print(tf.__version__)" "# print() would need a logging handler configured to survive this file being\n",
"# exported and run as a script; sys.stdout.write shows the same text in a cell.\n",
"sys.stdout.write(f\"{tf.__version__}\\n\")"
] ]
}, },
{ {
@ -129,13 +140,14 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"# The MNIST data is split between 60,000 28 x 28 pixel training images and 10,000 28 x 28 pixel images\n", "# The MNIST data is split between 60,000 28 x 28 pixel training images\n",
"# and 10,000 28 x 28 pixel images\n",
"(X_train, y_train), (X_test, y_test) = mnist.load_data()\n", "(X_train, y_train), (X_test, y_test) = mnist.load_data()\n",
"\n", "\n",
"print(\"X_train shape\", X_train.shape)\n", "sys.stdout.write(f\"X_train shape {X_train.shape}\\n\")\n",
"print(\"y_train shape\", y_train.shape)\n", "sys.stdout.write(f\"y_train shape {y_train.shape}\\n\")\n",
"print(\"X_test shape\", X_test.shape)\n", "sys.stdout.write(f\"X_test shape {X_test.shape}\\n\")\n",
"print(\"y_test shape\", y_test.shape)" "sys.stdout.write(f\"y_test shape {y_test.shape}\\n\")"
] ]
}, },
{ {
@ -162,20 +174,28 @@
"source": [ "source": [
"plt.rcParams[\"figure.figsize\"] = (9, 9) # Make the figures a bit bigger\n", "plt.rcParams[\"figure.figsize\"] = (9, 9) # Make the figures a bit bigger\n",
"\n", "\n",
"CLASS_COUNT = 10\n",
"\n", "\n",
"def visualize_classes(X, y):\n", "\n",
" for i in range(10):\n", "def visualize_classes(images: np.ndarray, labels: np.ndarray) -> None:\n",
" img_batch = X[y == i][0:10]\n", " \"\"\"Show ten samples of each class as one wide strip.\n",
" img_batch = np.reshape(\n", "\n",
" img_batch, (img_batch.shape[0] * img_batch.shape[1], img_batch.shape[2])\n", " The strips are collected in a list and concatenated once. Growing the\n",
" result in place only bound it in the loop's `else` branch, which reads as\n",
" a use-before-assignment and is what ruff flagged.\n",
" \"\"\"\n",
" strips = []\n",
" for class_id in range(CLASS_COUNT):\n",
" img_batch = images[labels == class_id][0:10]\n",
" strips.append(\n",
" np.reshape(\n",
" img_batch,\n",
" (img_batch.shape[0] * img_batch.shape[1], img_batch.shape[2]),\n",
" )\n",
" )\n", " )\n",
" if i > 0:\n",
" img = np.concatenate([img, img_batch], axis=1)\n",
" else:\n",
" img = img_batch\n",
" plt.figure(figsize=(10, 20))\n", " plt.figure(figsize=(10, 20))\n",
" plt.axis(\"off\")\n", " plt.axis(\"off\")\n",
" plt.imshow(img, cmap=\"gray\")\n", " plt.imshow(np.concatenate(strips, axis=1), cmap=\"gray\")\n",
"\n", "\n",
"\n", "\n",
"visualize_classes(X_train, y_train)" "visualize_classes(X_train, y_train)"
@ -203,12 +223,14 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# just a little function for pretty printing a matrix\n", "# just a little function for pretty printing a matrix\n",
"def matprint(mat, fmt=\"g\"):\n", "def matprint(mat: np.ndarray, fmt: str = \"g\") -> None:\n",
" \"\"\"Print `mat` with every column padded to its own widest entry.\"\"\"\n",
" col_maxes = [max([len((\"{:\" + fmt + \"}\").format(x)) for x in col]) for col in mat.T]\n", " col_maxes = [max([len((\"{:\" + fmt + \"}\").format(x)) for x in col]) for col in mat.T]\n",
" for x in mat:\n", " for row in mat:\n",
" for i, y in enumerate(x):\n", " for i, value in enumerate(row):\n",
" print((\"{:\" + str(col_maxes[i]) + fmt + \"}\").format(y), end=\" \")\n", " # Two trailing spaces and no newline: this builds one line per row.\n",
" print()\n", " sys.stdout.write((\"{:\" + str(col_maxes[i]) + fmt + \"} \").format(value))\n",
" sys.stdout.write(\"\\n\")\n",
"\n", "\n",
"\n", "\n",
"# now print!\n", "# now print!\n",
@ -257,44 +279,42 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"import matplotlib.patheffects as PathEffects\n",
"import seaborn as sns\n",
"from sklearn.manifold import TSNE\n",
"\n",
"RS = 19238\n", "RS = 19238\n",
"\n", "\n",
"class_names = [str(clid) for clid in range(10)]\n", "class_names = [str(clid) for clid in range(CLASS_COUNT)]\n",
"\n", "\n",
"\n", "\n",
"def scatter(x, colors):\n", "def scatter(points: np.ndarray, colors: np.ndarray) -> None:\n",
" \"\"\"Scatter 2D `points` coloured by class, with each class labelled once.\"\"\"\n",
" # We choose a color palette with seaborn.\n", " # We choose a color palette with seaborn.\n",
" palette = np.array(sns.color_palette(\"hls\", 10))\n", " palette = np.array(sns.color_palette(\"hls\", CLASS_COUNT))\n",
"\n", "\n",
" # We create a scatter plot.\n", " # We create a scatter plot.\n",
" f = plt.figure(figsize=(8, 8))\n", " plt.figure(figsize=(8, 8))\n",
" ax = plt.subplot(aspect=\"equal\")\n", " ax = plt.subplot(aspect=\"equal\")\n",
" sc = ax.scatter(x[:, 0], x[:, 1], lw=0, s=40, c=palette[colors.astype(\"int\")])\n", " ax.scatter(points[:, 0], points[:, 1], lw=0, s=40, c=palette[colors.astype(\"int\")])\n",
" plt.xlim(-25, 25)\n", " plt.xlim(-25, 25)\n",
" plt.ylim(-25, 25)\n", " plt.ylim(-25, 25)\n",
" ax.axis(\"off\")\n", " ax.axis(\"off\")\n",
" ax.axis(\"tight\")\n", " ax.axis(\"tight\")\n",
"\n", "\n",
" # We add the labels for each digit.\n", " # We add the labels for each digit.\n",
" txts = []\n", " for i in range(CLASS_COUNT):\n",
" for i in range(10):\n",
" # Position of each label.\n", " # Position of each label.\n",
" xtext, ytext = np.median(x[colors == i, :], axis=0)\n", " xtext, ytext = np.median(points[colors == i, :], axis=0)\n",
" txt = ax.text(xtext, ytext, class_names[i], fontsize=15)\n", " txt = ax.text(xtext, ytext, class_names[i], fontsize=15)\n",
" txt.set_path_effects(\n", " txt.set_path_effects(\n",
" [PathEffects.Stroke(linewidth=5, foreground=\"w\"), PathEffects.Normal()]\n", " [patheffects.Stroke(linewidth=5, foreground=\"w\"), patheffects.Normal()]\n",
" )\n", " )\n",
" txts.append(txt)\n",
"\n", "\n",
"\n", "\n",
"def plot_tsne(X, y):\n", "def plot_tsne(features: np.ndarray, labels: np.ndarray) -> None:\n",
" print(\"calculating tsne ...\")\n", " \"\"\"Project `features` to two dimensions with t-SNE and scatter them.\"\"\"\n",
" proj = TSNE(random_state=RS, learning_rate=200, init=\"random\").fit_transform(X)\n", " sys.stdout.write(\"calculating tsne ...\\n\")\n",
" scatter(proj, y)" " proj = TSNE(random_state=RS, learning_rate=200, init=\"random\").fit_transform(\n",
" features\n",
" )\n",
" scatter(proj, labels)"
] ]
}, },
{ {
@ -364,8 +384,8 @@
")\n", ")\n",
"X_test /= 255\n", "X_test /= 255\n",
"\n", "\n",
"print(\"Training matrix shape\", X_train.shape)\n", "sys.stdout.write(f\"Training matrix shape {X_train.shape}\\n\")\n",
"print(\"Testing matrix shape\", X_test.shape)" "sys.stdout.write(f\"Testing matrix shape {X_test.shape}\\n\")"
] ]
}, },
{ {
@ -447,7 +467,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# The first hidden layer is a set of 512 nodes (artificial neurons).\n", "# The first hidden layer is a set of 512 nodes (artificial neurons).\n",
"# Each node will receive an element from each input vector and apply some weight and bias to it.\n", "# Each node will receive an element from each input vector and apply some\n",
"# weight and bias to it.\n",
"\n", "\n",
"model.add(\n", "model.add(\n",
" Dense(512, input_shape=(784,))\n", " Dense(512, input_shape=(784,))\n",
@ -462,10 +483,11 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"# An \"activation\" is a non-linear function applied to the output of the layer above.\n", "# An \"activation\" is a non-linear function applied to the output of the layer\n",
"# It checks the new value of the node, and decides whether that artifical neuron has fired.\n", "# above. It checks the new value of the node, and decides whether that\n",
"# The Rectified Linear Unit (ReLU) converts all negative inputs to nodes in the next layer to be zero.\n", "# artifical neuron has fired.\n",
"# Those inputs are then not considered to be fired.\n", "# The Rectified Linear Unit (ReLU) converts all negative inputs to nodes in the\n",
"# next layer to be zero. Those inputs are then not considered to be fired.\n",
"# Positive values of a node are unchanged.\n", "# Positive values of a node are unchanged.\n",
"\n", "\n",
"model.add(Activation(\"relu\"))" "model.add(Activation(\"relu\"))"
@ -512,8 +534,9 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# The second hidden layer appears identical to our first layer.\n", "# The second hidden layer appears identical to our first layer.\n",
"# However, instead of each of the 512-node receiving 784-inputs from the input image data,\n", "# However, instead of each of the 512-node receiving 784-inputs from the input\n",
"# they receive 512 inputs from the output of the first 512-node layer.\n", "# image data, they receive 512 inputs from the output of the first 512-node\n",
"# layer.\n",
"\n", "\n",
"model.add(Dense(512))\n", "model.add(Dense(512))\n",
"model.add(Activation(\"relu\"))\n", "model.add(Activation(\"relu\"))\n",
@ -537,9 +560,10 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"# The final layer of 10 neurons in fully-connected to the previous 512-node layer.\n", "# The final layer of 10 neurons in fully-connected to the previous 512-node\n",
"# The final layer of a FCN should be equal to the number of desired classes (10 in this case).\n", "# layer. The final layer of a FCN should be equal to the number of desired\n",
"model.add(Dense(10))" "# classes (10 in this case).\n",
"model.add(Dense(CLASS_COUNT))"
] ]
}, },
{ {
@ -550,8 +574,8 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"# The \"softmax\" activation represents a probability distribution over K different possible outcomes.\n", "# The \"softmax\" activation represents a probability distribution over K\n",
"# Its values are all non-negative and sum to 1.\n", "# different possible outcomes. Its values are all non-negative and sum to 1.\n",
"\n", "\n",
"model.add(Activation(\"softmax\"))" "model.add(Activation(\"softmax\"))"
] ]
@ -710,8 +734,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"score = model.evaluate(X_test, Y_test)\n", "score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n", "sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"print(\"Test accuracy:\", score[1])" "sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
] ]
}, },
{ {
@ -723,18 +747,24 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"def plot_confusion_matrix(\n", "def plot_confusion_matrix(\n",
" cm, classes, normalize=False, title=\"Confusion matrix\", cmap=plt.cm.Blues\n", " cm: np.ndarray,\n",
"):\n", " classes: list[str],\n",
" \"\"\"This function prints and plots the confusion matrix.\n", " *,\n",
" normalize: bool = False,\n",
" title: str = \"Confusion matrix\",\n",
" cmap: object = plt.cm.Blues,\n",
") -> None:\n",
" \"\"\"Print and plot the confusion matrix.\n",
"\n",
" Normalization can be applied by setting `normalize=True`.\n", " Normalization can be applied by setting `normalize=True`.\n",
" \"\"\"\n", " \"\"\"\n",
" if normalize:\n", " if normalize:\n",
" cm = cm.astype(\"float\") / cm.sum(axis=1)[:, np.newaxis]\n", " cm = cm.astype(\"float\") / cm.sum(axis=1)[:, np.newaxis]\n",
" print(\"Normalized confusion matrix\")\n", " sys.stdout.write(\"Normalized confusion matrix\\n\")\n",
" else:\n", " else:\n",
" print(\"Confusion matrix, without normalization\")\n", " sys.stdout.write(\"Confusion matrix, without normalization\\n\")\n",
"\n", "\n",
" print(cm)\n", " sys.stdout.write(f\"{cm}\\n\")\n",
"\n", "\n",
" plt.imshow(cm, interpolation=\"nearest\", cmap=cmap)\n", " plt.imshow(cm, interpolation=\"nearest\", cmap=cmap)\n",
" plt.title(title)\n", " plt.title(title)\n",
@ -756,7 +786,17 @@
"\n", "\n",
" plt.ylabel(\"True label\")\n", " plt.ylabel(\"True label\")\n",
" plt.xlabel(\"Predicted label\")\n", " plt.xlabel(\"Predicted label\")\n",
" plt.tight_layout()" " plt.tight_layout()\n",
"\n",
"\n",
"@dataclass(frozen=True)\n",
"class Samples:\n",
" \"\"\"A model's output, with the images and true labels it was scored on.\"\"\"\n",
"\n",
" scores: np.ndarray\n",
" images: np.ndarray\n",
" labels: np.ndarray\n",
" names: list[str] | None = None"
] ]
}, },
{ {
@ -835,29 +875,34 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"def show_samples(indices, preds, images, labels, count=3, names=[]):\n", "def show_samples(indices: np.ndarray, samples: Samples, *, count: int = 3) -> None:\n",
" \"\"\"Plot a `count` x `count` grid of digits, titled predicted vs expected.\"\"\"\n",
" plt.figure()\n", " plt.figure()\n",
" for i, sample in enumerate(indices[: count**2]):\n", " for i, sample in enumerate(indices[: count**2]):\n",
" pred_id = int(np.argmax(preds[sample]))\n", " pred_id = int(np.argmax(samples.scores[sample]))\n",
" real_id = int(labels[sample])\n", " real_id = int(samples.labels[sample])\n",
" pred_score = preds[sample][pred_id]\n", " pred_score = samples.scores[sample][pred_id]\n",
" real_score = preds[sample][real_id]\n", " real_score = samples.scores[sample][real_id]\n",
" plt.subplot(count, count, i + 1)\n", " plt.subplot(count, count, i + 1)\n",
" plt.imshow(images[sample].reshape(28, 28), cmap=\"gray\", interpolation=\"none\")\n", " plt.imshow(\n",
" samples.images[sample].reshape(28, 28), cmap=\"gray\", interpolation=\"none\"\n",
" )\n",
" plt.axis(\"off\")\n", " plt.axis(\"off\")\n",
" if len(names) > 0:\n", " if samples.names:\n",
" plt.title(\n", " predicted_label: str | int = samples.names[pred_id]\n",
" f\"P: {names[pred_id]} ({pred_score:.2f})\\nE: {names[real_id]} ({real_score:.2f})\"\n", " real_label: str | int = samples.names[real_id]\n",
" )\n",
" else:\n", " else:\n",
" plt.title(\n", " predicted_label = pred_id\n",
" f\"P: {pred_id} ({pred_score:.2f})\\nE: {real_id} ({real_score:.2f})\"\n", " real_label = real_id\n",
" )\n", " plt.title(\n",
" f\"P: {predicted_label} ({pred_score:.2f})\\n\"\n",
" f\"E: {real_label} ({real_score:.2f})\"\n",
" )\n",
"\n", "\n",
" plt.tight_layout()\n", " plt.tight_layout()\n",
"\n", "\n",
"\n", "\n",
"show_samples(correct_indices, predict, X_test, y_test, 5)" "show_samples(correct_indices, Samples(predict, X_test, y_test), count=5)"
] ]
}, },
{ {
@ -882,7 +927,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples(incorrect_indices, predict, X_test, y_test, 5)" "show_samples(incorrect_indices, Samples(predict, X_test, y_test), count=5)"
] ]
}, },
{ {
@ -1039,7 +1084,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Again, do some formatting\n", "# Again, do some formatting\n",
"# Except we do not flatten each image into a 784-length vector because we want to perform convolutions first\n", "# Except we do not flatten each image into a 784-length vector because we want\n",
"# to perform convolutions first\n",
"\n", "\n",
"X_train = X_train.reshape(\n", "X_train = X_train.reshape(\n",
" 60000, 28, 28, 1\n", " 60000, 28, 28, 1\n",
@ -1054,8 +1100,8 @@
")\n", ")\n",
"X_test /= 255\n", "X_test /= 255\n",
"\n", "\n",
"print(\"Training matrix shape\", X_train.shape)\n", "sys.stdout.write(f\"Training matrix shape {X_train.shape}\\n\")\n",
"print(\"Testing matrix shape\", X_test.shape)" "sys.stdout.write(f\"Testing matrix shape {X_test.shape}\\n\")"
] ]
}, },
{ {
@ -1167,8 +1213,9 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# We can then feed our augmented data in batches\n", "# We can then feed our augmented data in batches\n",
"# Besides loss function considerations as before, this method actually results in significant memory savings\n", "# Besides loss function considerations as before, this method actually results\n",
"# because we are actually LOADING the data into the network in batches before processing each batch\n", "# in significant memory savings because we are actually LOADING the data into\n",
"# the network in batches before processing each batch\n",
"\n", "\n",
"# Before the data was all loaded into memory, but then processed in batches.\n", "# Before the data was all loaded into memory, but then processed in batches.\n",
"\n", "\n",
@ -1217,8 +1264,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"score = model.evaluate(X_test, Y_test)\n", "score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n", "sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"print(\"Test accuracy:\", score[1])" "sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
] ]
}, },
{ {
@ -1271,7 +1318,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples(correct_indices, predict, X_test, y_test, 5)" "show_samples(correct_indices, Samples(predict, X_test, y_test), count=5)"
] ]
}, },
{ {
@ -1287,7 +1334,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples(incorrect_indices, predict, X_test, y_test, 5)" "show_samples(incorrect_indices, Samples(predict, X_test, y_test), count=5)"
] ]
}, },
{ {
@ -1309,15 +1356,16 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"def visualize2(model, layer, img, r, c):\n", "def visualize2(\n",
" from keras import Model\n", " model: Sequential, layer: object, img: np.ndarray, r: int, c: int\n",
"\n", ") -> None:\n",
" \"\"\"Plot the `r` x `c` feature maps `layer` produces for a single image.\"\"\"\n",
" # expand dimensions so that it represents a single 'sample'\n", " # expand dimensions so that it represents a single 'sample'\n",
" img = np.expand_dims(img, axis=0)\n", " img = np.expand_dims(img, axis=0)\n",
" outputs = [layer.output]\n", " outputs = [layer.output]\n",
" model = Model(inputs=model.inputs, outputs=outputs)\n", " feature_model = Model(inputs=model.inputs, outputs=outputs)\n",
" # get feature map for first hidden layer\n", " # get feature map for first hidden layer\n",
" feature_maps = model.predict(img)\n", " feature_maps = feature_model.predict(img)\n",
" for fmap in feature_maps:\n", " for fmap in feature_maps:\n",
" # plot all 64 maps in an 8x8 squares\n", " # plot all 64 maps in an 8x8 squares\n",
" ix = 1\n", " ix = 1\n",
@ -1409,31 +1457,34 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Loading the specified subset of digits from the MNIST dataset\n", "# Loading the specified subset of digits from the MNIST dataset\n",
"def get_mnist(digits):\n", "def get_mnist(\n",
" digits: list[int],\n",
") -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:\n",
" \"\"\"Reload MNIST keeping only `digits`, scaled to [0, 1] with a channel axis.\"\"\"\n",
" # Reload the MNIST data\n", " # Reload the MNIST data\n",
" (X_train, y_train), (X_test, y_test) = mnist.load_data()\n", " (x_train, y_train), (x_test, y_test) = mnist.load_data()\n",
"\n", "\n",
" train_ids = np.in1d(y_train, digits)\n", " train_ids = np.isin(y_train, digits).ravel()\n",
" test_ids = np.in1d(y_test, digits)\n", " test_ids = np.isin(y_test, digits).ravel()\n",
" X_train = X_train[train_ids]\n", " x_train = x_train[train_ids]\n",
" X_test = X_test[test_ids]\n", " x_test = x_test[test_ids]\n",
" y_train = y_train[train_ids]\n", " y_train = y_train[train_ids]\n",
" y_test = y_test[test_ids]\n", " y_test = y_test[test_ids]\n",
"\n", "\n",
" X_train = np.expand_dims(X_train, axis=-1)\n", " x_train = np.expand_dims(x_train, axis=-1)\n",
" X_test = np.expand_dims(X_test, axis=-1)\n", " x_test = np.expand_dims(x_test, axis=-1)\n",
"\n", "\n",
" X_train = X_train.astype(\n", " x_train = x_train.astype(\n",
" \"float32\"\n", " \"float32\"\n",
" ) # change integers to 32-bit floating point numbers\n", " ) # change integers to 32-bit floating point numbers\n",
" X_test = X_test.astype(\"float32\")\n", " x_test = x_test.astype(\"float32\")\n",
"\n", "\n",
" X_train /= (\n", " x_train /= (\n",
" 255 # normalize each value for each pixel for the entire vector for each input\n", " 255 # normalize each value for each pixel for the entire vector for each input\n",
" )\n", " )\n",
" X_test /= 255\n", " x_test /= 255\n",
"\n", "\n",
" return X_train, y_train, X_test, y_test" " return x_train, y_train, x_test, y_test"
] ]
}, },
{ {
@ -1445,7 +1496,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Preparing the base model\n", "# Preparing the base model\n",
"def prepare_model(input_shape, class_count):\n", "def prepare_model(input_shape: tuple[int, ...], class_count: int) -> Sequential:\n",
" \"\"\"Build the three-convolution-block CNN this section reuses.\"\"\"\n",
" model = Sequential() # Linear stacking of layers\n", " model = Sequential() # Linear stacking of layers\n",
"\n", "\n",
" # Convolution Layer 1\n", " # Convolution Layer 1\n",
@ -1485,12 +1537,11 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# One-hot encoding of non-consecutive labels\n", "# One-hot encoding of non-consecutive labels\n",
"def one_hot(labels):\n", "def one_hot(labels: np.ndarray) -> np.ndarray:\n",
" from sklearn.preprocessing import OneHotEncoder\n", " \"\"\"One-hot encode `labels`, which need not be 0..n-1.\"\"\"\n",
"\n",
" encoder = OneHotEncoder(categories=\"auto\")\n", " encoder = OneHotEncoder(categories=\"auto\")\n",
" l = labels.reshape(-1, 1)\n", " column = labels.reshape(-1, 1)\n",
" output = encoder.fit_transform(l)\n", " output = encoder.fit_transform(column)\n",
" return output.toarray()" " return output.toarray()"
] ]
}, },
@ -1502,23 +1553,25 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"def test_model(model, X_test, Y_test, y_test, digits):\n", "def test_model(\n",
" score = model.evaluate(X_test, Y_test)\n", " model: Sequential,\n",
" print(\"Test score:\", score[0])\n", " x_test: np.ndarray,\n",
" print(\"Test accuracy:\", score[1])\n", " y_test_onehot: np.ndarray,\n",
" y_test: np.ndarray,\n",
" digits: list[int],\n",
") -> None:\n",
" \"\"\"Score `model` on the test split and plot its confusion matrix.\"\"\"\n",
" score = model.evaluate(x_test, y_test_onehot)\n",
" sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
" sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")\n",
"\n", "\n",
" # The predict_classes function outputs the highest probability class\n", " # The predict_classes function outputs the highest probability class\n",
" # according to the trained classifier for each input example.\n", " # according to the trained classifier for each input example.\n",
" predicted = model.predict(X_test)\n", " predicted = model.predict(x_test)\n",
" predicted_classes = np.argmax(predicted, axis=1)\n", " predicted_classes = np.argmax(predicted, axis=1)\n",
"\n", "\n",
" actual_classes = [digits[x] for x in predicted_classes]\n", " actual_classes = [digits[x] for x in predicted_classes]\n",
"\n", "\n",
" # Check which items we got right / wrong\n",
" correct_indices = np.nonzero(actual_classes == y_test)[0]\n",
"\n",
" incorrect_indices = np.nonzero(actual_classes != y_test)[0]\n",
"\n",
" cnf_matrix = confusion_matrix(y_test, actual_classes)\n", " cnf_matrix = confusion_matrix(y_test, actual_classes)\n",
"\n", "\n",
" class_names = [str(i) for i in digits]\n", " class_names = [str(i) for i in digits]\n",
@ -1648,7 +1701,10 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Extract specified range of layers to a new sequential model\n", "# Extract specified range of layers to a new sequential model\n",
"def extract_layers(main_model, starting_layer_ix, ending_layer_ix):\n", "def extract_layers(\n",
" main_model: Sequential, starting_layer_ix: int, ending_layer_ix: int\n",
") -> Sequential:\n",
" \"\"\"Return a new Sequential holding layers `starting_layer_ix`..`ending_layer_ix`.\"\"\"\n",
" # create an empty model\n", " # create an empty model\n",
" new_model = Sequential()\n", " new_model = Sequential()\n",
" for ix in range(starting_layer_ix, ending_layer_ix + 1):\n", " for ix in range(starting_layer_ix, ending_layer_ix + 1):\n",
@ -1892,46 +1948,50 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"import os\n", "# The scheme is spelled out at each use site rather than folded into this\n",
"import urllib\n", "# constant, so that the URL handed to urlopen is visibly https and nothing\n",
"# else -- which is what S310 asks for.\n",
"QUICKDRAW_HOST = \"storage.googleapis.com/quickdraw_dataset/full/numpy_bitmap\"\n",
"\n", "\n",
"\n", "\n",
"def download_and_load(class_names, test_split=0.2, max_items_per_class=10000):\n", "def download_and_load(\n",
" root = \"data\"\n", " class_names: list[str],\n",
" if not os.path.exists(root):\n", " test_split: float = 0.2,\n",
" os.makedirs(root)\n", " max_items_per_class: int = 10000,\n",
") -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:\n",
" \"\"\"Fetch the Quick, Draw! bitmaps for `class_names` and split them.\"\"\"\n",
" root = Path(\"data\")\n",
" root.mkdir(parents=True, exist_ok=True)\n",
"\n", "\n",
" print(\"downloading ...\")\n", " sys.stdout.write(\"downloading ...\\n\")\n",
" base = \"https://storage.googleapis.com/quickdraw_dataset/full/numpy_bitmap/\"\n",
" for c in class_names:\n", " for c in class_names:\n",
" cc = c.replace(\"_\", \"%20\")\n", " cc = c.replace(\"_\", \"%20\")\n",
" path = base + cc + \".npy\"\n", " path = f\"https://{QUICKDRAW_HOST}/{cc}.npy\"\n",
" print(path)\n", " sys.stdout.write(f\"{path}\\n\")\n",
" if not os.path.exists(f\"{root}/{cc}.npy\"):\n", " destination = root / f\"{cc}.npy\"\n",
" urllib.request.urlretrieve(path, f\"{root}/{cc}.npy\")\n", " if not destination.exists():\n",
" with urllib.request.urlopen(path) as response:\n",
" destination.write_bytes(response.read())\n",
" else:\n", " else:\n",
" print(\"Already downloaded\")\n", " sys.stdout.write(\"Already downloaded\\n\")\n",
" print(\"loading ...\")\n", " sys.stdout.write(\"loading ...\\n\")\n",
"\n", "\n",
" # initialize variables\n", " # initialize variables\n",
" x = np.empty([0, 784])\n", " x = np.empty([0, 784])\n",
" y = np.empty([0])\n", " y = np.empty([0])\n",
"\n", "\n",
" # load each data file\n", " # load each data file\n",
" for idx, file in enumerate(class_names):\n", " for idx, name in enumerate(class_names):\n",
" file = file.replace(\"_\", \"%20\")\n", " quoted = name.replace(\"_\", \"%20\")\n",
" data = np.load(f\"{root}/{file}.npy\")\n", " data = np.load(root / f\"{quoted}.npy\")\n",
" data = data[0:max_items_per_class, :]\n", " data = data[0:max_items_per_class, :]\n",
" labels = np.full(data.shape[0], idx)\n", " labels = np.full(data.shape[0], idx)\n",
"\n", "\n",
" x = np.concatenate((x, data), axis=0)\n", " x = np.concatenate((x, data), axis=0)\n",
" y = np.append(y, labels)\n", " y = np.append(y, labels)\n",
"\n", "\n",
" data = None\n",
" labels = None\n",
"\n",
" # randomize the dataset\n", " # randomize the dataset\n",
" permutation = np.random.permutation(y.shape[0])\n", " permutation = np.random.default_rng().permutation(y.shape[0])\n",
" x = x[permutation, :]\n", " x = x[permutation, :]\n",
" y = y[permutation]\n", " y = y[permutation]\n",
"\n", "\n",
@ -1947,8 +2007,8 @@
" x_train = x[test_size : x.shape[0], :]\n", " x_train = x[test_size : x.shape[0], :]\n",
" y_train = y[test_size : y.shape[0]]\n", " y_train = y[test_size : y.shape[0]]\n",
"\n", "\n",
" print(\"Training Data : \", x_train.shape[0])\n", " sys.stdout.write(f\"Training Data : {x_train.shape[0]}\\n\")\n",
" print(\"Testing Data : \", x_test.shape[0])\n", " sys.stdout.write(f\"Testing Data : {x_test.shape[0]}\\n\")\n",
" return x_train, y_train, x_test, y_test" " return x_train, y_train, x_test, y_test"
] ]
}, },
@ -1977,9 +2037,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"with open(\"categories.txt\") as f:\n", "all_class_names = Path(\"categories.txt\").read_text().splitlines()\n",
" all_class_names = f.readlines()\n",
"\n",
"\n", "\n",
"random.seed(RANDOM_SEED)\n", "random.seed(RANDOM_SEED)\n",
"\n", "\n",
@ -1990,10 +2048,11 @@
"# select random 10 classes\n", "# select random 10 classes\n",
"class_names = random.sample(all_class_names, nb_classes)\n", "class_names = random.sample(all_class_names, nb_classes)\n",
"\n", "\n",
"# you can also try those \"hard\" classes instead\n", "# The lab also suggests a deliberately hard fixed set instead of a random one:\n",
"# class_names = ['wheel', 'pizza', 'smiley_face', 'apple', 'potato', 'basketball', 'soccer_ball', 'brain', 'clock', 'circle']\n", "# wheel, pizza, smiley_face, apple, potato, basketball, soccer_ball, brain,\n",
"# clock and circle.\n",
"\n", "\n",
"print(class_names)\n", "sys.stdout.write(f\"{class_names}\\n\")\n",
"\n", "\n",
"X_train, y_train, X_test, y_test = download_and_load(class_names)\n", "X_train, y_train, X_test, y_test = download_and_load(class_names)\n",
"\n", "\n",
@ -2125,8 +2184,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"score = model.evaluate(X_test, Y_test)\n", "score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n", "sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"print(\"Test accuracy:\", score[1])" "sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
] ]
}, },
{ {
@ -2166,7 +2225,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples(correct_indices, predicted, X_test, y_test, 5, class_names)" "show_samples(correct_indices, Samples(predicted, X_test, y_test, class_names), count=5)"
] ]
}, },
{ {
@ -2177,7 +2236,9 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples(incorrect_indices, predicted, X_test, y_test, 5, class_names)" "show_samples(\n",
" incorrect_indices, Samples(predicted, X_test, y_test, class_names), count=5\n",
")"
] ]
}, },
{ {
@ -2225,8 +2286,8 @@
"y_train = y_train.reshape((1, -1))[0]\n", "y_train = y_train.reshape((1, -1))[0]\n",
"y_test = y_test.reshape((1, -1))[0]\n", "y_test = y_test.reshape((1, -1))[0]\n",
"\n", "\n",
"print(\"Training matrix shape\", X_train.shape, y_train.shape)\n", "sys.stdout.write(f\"Training matrix shape {X_train.shape} {y_train.shape}\\n\")\n",
"print(\"Testing matrix shape\", X_test.shape, y_test.shape)\n", "sys.stdout.write(f\"Testing matrix shape {X_test.shape} {y_test.shape}\\n\")\n",
"\n", "\n",
"# one-hot format classes\n", "# one-hot format classes\n",
"\n", "\n",
@ -2271,23 +2332,22 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"for i in range(10):\n", "strips = []\n",
" img_batch = X_train[y_train == i][0:10]\n", "for class_id in range(CLASS_COUNT):\n",
" img_batch = np.reshape(\n", " img_batch = X_train[y_train == class_id][0:10]\n",
" img_batch,\n", " strips.append(\n",
" (\n", " np.reshape(\n",
" img_batch.shape[0] * img_batch.shape[1],\n", " img_batch,\n",
" img_batch.shape[2],\n", " (\n",
" img_batch.shape[3],\n", " img_batch.shape[0] * img_batch.shape[1],\n",
" ),\n", " img_batch.shape[2],\n",
" img_batch.shape[3],\n",
" ),\n",
" )\n",
" )\n", " )\n",
" if i > 0:\n",
" img = np.concatenate([img, img_batch], axis=1)\n",
" else:\n",
" img = img_batch\n",
"plt.figure(figsize=(10, 20))\n", "plt.figure(figsize=(10, 20))\n",
"plt.axis(\"off\")\n", "plt.axis(\"off\")\n",
"plt.imshow(img, cmap=\"gray\")" "plt.imshow(np.concatenate(strips, axis=1), cmap=\"gray\")"
] ]
}, },
{ {
@ -2310,7 +2370,8 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"def generate_model():\n", "def generate_model() -> Sequential:\n",
" \"\"\"Build the minimal variant: one conv layer straight into the classifier.\"\"\"\n",
" model = Sequential() # Linear stacking of layers\n", " model = Sequential() # Linear stacking of layers\n",
"\n", "\n",
" # Convolution Layer 1\n", " # Convolution Layer 1\n",
@ -2335,7 +2396,8 @@
" return model\n", " return model\n",
"\n", "\n",
"\n", "\n",
"def generate_model_default():\n", "def generate_model_default() -> Sequential:\n",
" \"\"\"Build the same architecture again, kept as the baseline to compare against.\"\"\"\n",
" model = Sequential() # Linear stacking of layers\n", " model = Sequential() # Linear stacking of layers\n",
"\n", "\n",
" # Convolution Layer 1\n", " # Convolution Layer 1\n",
@ -2360,7 +2422,8 @@
" return model\n", " return model\n",
"\n", "\n",
"\n", "\n",
"def generate_model_gemini():\n", "def generate_model_gemini() -> Sequential:\n",
" \"\"\"Build the variant Gemini proposed: two conv blocks and heavy dropout.\"\"\"\n",
" model = Sequential()\n", " model = Sequential()\n",
"\n", "\n",
" # Convolutional Layers with Max Pooling\n", " # Convolutional Layers with Max Pooling\n",
@ -2386,7 +2449,8 @@
" return model\n", " return model\n",
"\n", "\n",
"\n", "\n",
"def generate_model_chat():\n", "def generate_model_chat() -> Sequential:\n",
" \"\"\"Build the variant ChatGPT proposed: four conv layers with batch norm.\"\"\"\n",
" model = Sequential() # Linear stacking of layers\n", " model = Sequential() # Linear stacking of layers\n",
"\n", "\n",
" # Convolution Layer 1\n", " # Convolution Layer 1\n",
@ -2530,14 +2594,14 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"for model in models:\n", "for trained in models:\n",
" score = model.evaluate(X_test, Y_test)\n", " score = trained.evaluate(X_test, Y_test)\n",
" print(\"Test score:\", score[0])\n", " sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
" print(\"Test accuracy:\", score[1])\n", " sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")\n",
"\n", "\n",
" # The predict_classes function outputs the highest probability class\n", " # The predict_classes function outputs the highest probability class\n",
" # according to the trained classifier for each input example.\n", " # according to the trained classifier for each input example.\n",
" predicted = model.predict(X_test)\n", " predicted = trained.predict(X_test)\n",
" predicted_classes = np.argmax(predicted, axis=1)\n", " predicted_classes = np.argmax(predicted, axis=1)\n",
"\n", "\n",
" # Check which items we got right / wrong\n", " # Check which items we got right / wrong\n",
@ -2547,7 +2611,7 @@
"\n", "\n",
" cnf_matrix = confusion_matrix(y_test, predicted_classes)\n", " cnf_matrix = confusion_matrix(y_test, predicted_classes)\n",
"\n", "\n",
" class_names = [str(i) for i in range(10)]\n", " class_names = [str(i) for i in range(CLASS_COUNT)]\n",
"\n", "\n",
" # Plot non-normalized confusion matrix\n", " # Plot non-normalized confusion matrix\n",
" plt.figure()\n", " plt.figure()\n",
@ -2569,24 +2633,27 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"def show_samples_rgb(indices, preds, images, labels, count=3, names=[]):\n", "def show_samples_rgb(indices: np.ndarray, samples: Samples, *, count: int = 3) -> None:\n",
" \"\"\"Plot a `count` x `count` grid of RGB samples, predicted vs expected.\"\"\"\n",
" plt.figure()\n", " plt.figure()\n",
" for i, sample in enumerate(indices[: count**2]):\n", " for i, sample in enumerate(indices[: count**2]):\n",
" pred_id = int(np.argmax(preds[sample]))\n", " pred_id = int(np.argmax(samples.scores[sample]))\n",
" real_id = int(labels[sample])\n", " real_id = int(samples.labels[sample])\n",
" pred_score = preds[sample][pred_id]\n", " pred_score = samples.scores[sample][pred_id]\n",
" real_score = preds[sample][real_id]\n", " real_score = samples.scores[sample][real_id]\n",
" plt.subplot(count, count, i + 1)\n", " plt.subplot(count, count, i + 1)\n",
" plt.imshow(images[sample], interpolation=\"none\")\n", " plt.imshow(samples.images[sample], interpolation=\"none\")\n",
" plt.axis(\"off\")\n", " plt.axis(\"off\")\n",
" if len(names) > 0:\n", " if samples.names:\n",
" plt.title(\n", " predicted_label: str | int = samples.names[pred_id]\n",
" f\"P: {names[pred_id]} ({pred_score:.2f})\\nE: {names[real_id]} ({real_score:.2f})\"\n", " real_label: str | int = samples.names[real_id]\n",
" )\n",
" else:\n", " else:\n",
" plt.title(\n", " predicted_label = pred_id\n",
" f\"P: {pred_id} ({pred_score:.2f})\\nE: {real_id} ({real_score:.2f})\"\n", " real_label = real_id\n",
" )\n", " plt.title(\n",
" f\"P: {predicted_label} ({pred_score:.2f})\\n\"\n",
" f\"E: {real_label} ({real_score:.2f})\"\n",
" )\n",
"\n", "\n",
" plt.tight_layout()" " plt.tight_layout()"
] ]
@ -2612,7 +2679,9 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples_rgb(correct_indices, predicted, X_test, y_test, 5, cifar_names)" "show_samples_rgb(\n",
" correct_indices, Samples(predicted, X_test, y_test, cifar_names), count=5\n",
")"
] ]
}, },
{ {
@ -2636,7 +2705,9 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"show_samples_rgb(incorrect_indices, predicted, X_test, y_test, 5, cifar_names)" "show_samples_rgb(\n",
" incorrect_indices, Samples(predicted, X_test, y_test, cifar_names), count=5\n",
")"
] ]
} }
], ],

230
Programming/twm_4/TWM_KerasIntro.py Normal file → Executable file
View File

@ -1,25 +1,24 @@
# # CIFAR-10 #!/usr/bin/env python3
"""CIFAR-10 half of the lab notebook, exported so the code can be diffed.
# ## Ładowanie zbioru danych Four CNN variants are built and only `model_chat` is trained, then evaluated
with a confusion matrix and sample grids of its right and wrong answers.
# In[ ]: Needs a GPU: the script exits early without one, exactly as the notebook does.
import sys `print` is `sys.stdout.write` here so the output is identical to the notebook's
without a logging handler having to be configured first.
import tensorflow as tf """
# Check if GPU is available
print(tf.config.list_physical_devices("GPU"))
if tf.config.list_physical_devices("GPU"):
print("GPU is available")
else:
print("GPU is not available")
sys.exit()
from __future__ import annotations
import itertools import itertools
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import tensorflow as tf
from keras.datasets import cifar10 from keras.datasets import cifar10
from keras.layers import ( from keras.layers import (
Activation, Activation,
@ -36,20 +35,59 @@ from sklearn.metrics import confusion_matrix
from tensorflow.keras.optimizers import Adam from tensorflow.keras.optimizers import Adam
from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.preprocessing.image import ImageDataGenerator
if TYPE_CHECKING:
from collections.abc import Sequence
from matplotlib.colors import Colormap
# CIFAR-10 has ten classes, and the training split has 40000 images once the
# generator has held back its 20% for validation.
NUM_CLASSES = 10
BATCH_SIZE = 128
TRAIN_IMAGES = 40000
VALID_IMAGES = 10000
EPOCHS = 20
# ## Ładowanie zbioru danych
# Check if GPU is available
sys.stdout.write(f"{tf.config.list_physical_devices('GPU')}\n")
if tf.config.list_physical_devices("GPU"):
sys.stdout.write("GPU is available\n")
else:
sys.stdout.write("GPU is not available\n")
sys.exit()
@dataclass(frozen=True)
class Predictions:
"""A model's output, with the images and true labels it was produced from."""
scores: np.ndarray
images: np.ndarray
labels: np.ndarray
names: Sequence[str] | None = None
def plot_confusion_matrix( def plot_confusion_matrix(
cm, classes, normalize=False, title="Confusion matrix", cmap=plt.cm.Blues cm: np.ndarray,
): classes: Sequence[str],
"""This function prints and plots the confusion matrix. *,
normalize: bool = False,
title: str = "Confusion matrix",
cmap: Colormap = plt.cm.Blues,
) -> None:
"""Print and plot the confusion matrix.
Normalization can be applied by setting `normalize=True`. Normalization can be applied by setting `normalize=True`.
""" """
if normalize: if normalize:
cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis] cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix") sys.stdout.write("Normalized confusion matrix\n")
else: else:
print("Confusion matrix, without normalization") sys.stdout.write("Confusion matrix, without normalization\n")
print(cm) sys.stdout.write(f"{cm}\n")
plt.imshow(cm, interpolation="nearest", cmap=cmap) plt.imshow(cm, interpolation="nearest", cmap=cmap)
plt.title(title) plt.title(title)
@ -87,15 +125,13 @@ X_test /= 255
y_train = y_train.reshape((1, -1))[0] y_train = y_train.reshape((1, -1))[0]
y_test = y_test.reshape((1, -1))[0] y_test = y_test.reshape((1, -1))[0]
print("Training matrix shape", X_train.shape, y_train.shape) sys.stdout.write(f"Training matrix shape {X_train.shape} {y_train.shape}\n")
print("Testing matrix shape", X_test.shape, y_test.shape) sys.stdout.write(f"Testing matrix shape {X_test.shape} {y_test.shape}\n")
# one-hot format classes # one-hot format classes
nb_classes = 10 Y_train = to_categorical(y_train, NUM_CLASSES)
Y_test = to_categorical(y_test, NUM_CLASSES)
Y_train = to_categorical(y_train, nb_classes)
Y_test = to_categorical(y_test, nb_classes)
cifar_names = [ cifar_names = [
"airplane", "airplane",
@ -113,23 +149,23 @@ cifar_names = [
# ## Podgląd zbioru treningowego # ## Podgląd zbioru treningowego
# In[ ]: # Ten samples of each class, stacked into one wide strip. Built as a list and
# concatenated once: growing `img` in place only bound it in the loop's `else`
# branch, which reads as a use-before-assignment and is what ruff flagged.
for i in range(10): strips = []
img_batch = X_train[y_train == i][0:10] for class_id in range(NUM_CLASSES):
img_batch = np.reshape( img_batch = X_train[y_train == class_id][0:10]
img_batch, strips.append(
( np.reshape(
img_batch.shape[0] * img_batch.shape[1], img_batch,
img_batch.shape[2], (
img_batch.shape[3], img_batch.shape[0] * img_batch.shape[1],
), img_batch.shape[2],
img_batch.shape[3],
),
)
) )
if i > 0: img = np.concatenate(strips, axis=1)
img = np.concatenate([img, img_batch], axis=1)
else:
img = img_batch
plt.figure(figsize=(10, 20)) plt.figure(figsize=(10, 20))
plt.axis("off") plt.axis("off")
plt.imshow(img, cmap="gray") plt.imshow(img, cmap="gray")
@ -137,10 +173,9 @@ plt.imshow(img, cmap="gray")
# ## Przygotowanie modelu # ## Przygotowanie modelu
# In[ ]:
def generate_model() -> Sequential:
def generate_model(): """Build the minimal variant: one conv layer straight into the classifier."""
model = Sequential() # Linear stacking of layers model = Sequential() # Linear stacking of layers
# Convolution Layer 1 # Convolution Layer 1
@ -154,7 +189,7 @@ def generate_model():
# ... # ...
# Fully Connected Layer # Fully Connected Layer
model.add(Dense(10)) # final 10 FC nodes model.add(Dense(NUM_CLASSES)) # final 10 FC nodes
model.add(Activation("softmax")) # softmax activation model.add(Activation("softmax")) # softmax activation
model.summary() model.summary()
@ -165,7 +200,8 @@ def generate_model():
return model return model
def generate_model_default(): def generate_model_default() -> Sequential:
"""Build the same architecture again, kept as the baseline to compare against."""
model = Sequential() # Linear stacking of layers model = Sequential() # Linear stacking of layers
# Convolution Layer 1 # Convolution Layer 1
@ -179,7 +215,7 @@ def generate_model_default():
# ... # ...
# Fully Connected Layer # Fully Connected Layer
model.add(Dense(10)) # final 10 FC nodes model.add(Dense(NUM_CLASSES)) # final 10 FC nodes
model.add(Activation("softmax")) # softmax activation model.add(Activation("softmax")) # softmax activation
model.summary() model.summary()
@ -190,7 +226,8 @@ def generate_model_default():
return model return model
def generate_model_gemini(): def generate_model_gemini() -> Sequential:
"""Build the variant Gemini proposed: two conv blocks and heavy dropout."""
model = Sequential() model = Sequential()
# Convolutional Layers with Max Pooling # Convolutional Layers with Max Pooling
@ -204,7 +241,7 @@ def generate_model_gemini():
model.add(Flatten()) model.add(Flatten())
model.add(Dense(128, activation="relu")) model.add(Dense(128, activation="relu"))
model.add(Dropout(0.5)) # Regularization model.add(Dropout(0.5)) # Regularization
model.add(Dense(10, activation="softmax")) model.add(Dense(NUM_CLASSES, activation="softmax"))
# Model Compilation # Model Compilation
model.compile( model.compile(
@ -216,7 +253,11 @@ def generate_model_gemini():
return model return model
def generate_model_chat(): def generate_model_chat() -> Sequential:
"""Build the variant ChatGPT proposed: four conv layers with batch norm.
This is the only one that actually gets trained below.
"""
model = Sequential() # Linear stacking of layers model = Sequential() # Linear stacking of layers
# Convolution Layer 1 # Convolution Layer 1
@ -251,7 +292,7 @@ def generate_model_chat():
model.add(Dropout(0.6)) model.add(Dropout(0.6))
# Output Layer # Output Layer
model.add(Dense(10)) # final 10 FC nodes model.add(Dense(NUM_CLASSES)) # final 10 FC nodes
model.add(Activation("softmax")) # softmax activation model.add(Activation("softmax")) # softmax activation
model.summary() model.summary()
@ -263,9 +304,6 @@ def generate_model_chat():
return model return model
# In[ ]:
model = generate_model() model = generate_model()
model_default = generate_model_default() model_default = generate_model_default()
model_gemini = generate_model_gemini() model_gemini = generate_model_gemini()
@ -275,9 +313,6 @@ models = [model_chat]
# ## Trening # ## Trening
# In[ ]:
gen = ImageDataGenerator( gen = ImageDataGenerator(
rotation_range=8, rotation_range=8,
width_shift_range=0.08, width_shift_range=0.08,
@ -287,38 +322,35 @@ gen = ImageDataGenerator(
validation_split=0.2, validation_split=0.2,
) )
train_generator = gen.flow(X_train, Y_train, batch_size=128, subset="training") train_generator = gen.flow(X_train, Y_train, batch_size=BATCH_SIZE, subset="training")
valid_generator = gen.flow(X_train, Y_train, batch_size=128, subset="validation") valid_generator = gen.flow(X_train, Y_train, batch_size=BATCH_SIZE, subset="validation")
# In[ ]:
# Max 20 epoch # Max 20 epoch
for model in models: for trained in models:
model.fit( trained.fit(
train_generator, train_generator,
steps_per_epoch=40000 // 128, steps_per_epoch=TRAIN_IMAGES // BATCH_SIZE,
epochs=20, epochs=EPOCHS,
verbose=1, verbose=1,
validation_data=valid_generator, validation_data=valid_generator,
validation_steps=10000 // 128, validation_steps=VALID_IMAGES // BATCH_SIZE,
) )
# ## Test # ## Test
# In[ ]: for evaluated in models:
score = evaluated.evaluate(X_test, Y_test)
for model in models: sys.stdout.write(f"Test score: {score[0]}\n")
score = model.evaluate(X_test, Y_test) sys.stdout.write(f"Test accuracy: {score[1]}\n")
print("Test score:", score[0])
print("Test accuracy:", score[1])
# The predict_classes function outputs the highest probability class # The predict_classes function outputs the highest probability class
# according to the trained classifier for each input example. # according to the trained classifier for each input example.
predicted = model.predict(X_test) predicted = evaluated.predict(X_test)
predicted_classes = np.argmax(predicted, axis=1) predicted_classes = np.argmax(predicted, axis=1)
results = Predictions(
scores=predicted, images=X_test, labels=y_test, names=cifar_names
)
# Check which items we got right / wrong # Check which items we got right / wrong
correct_indices = np.nonzero(predicted_classes == y_test)[0] correct_indices = np.nonzero(predicted_classes == y_test)[0]
@ -327,7 +359,7 @@ for model in models:
cnf_matrix = confusion_matrix(y_test, predicted_classes) cnf_matrix = confusion_matrix(y_test, predicted_classes)
class_names = [str(i) for i in range(10)] class_names = [str(i) for i in range(NUM_CLASSES)]
# Plot non-normalized confusion matrix # Plot non-normalized confusion matrix
plt.figure() plt.figure()
@ -338,42 +370,38 @@ for model in models:
plt.show() plt.show()
# In[ ]: def show_samples_rgb(
indices: np.ndarray, results: Predictions, *, count: int = 3
) -> None:
def show_samples_rgb(indices, preds, images, labels, count=3, names=[]): """Plot a `count` x `count` grid of samples, titled predicted vs expected."""
plt.figure() plt.figure()
for i, sample in enumerate(indices[: count**2]): for i, sample in enumerate(indices[: count**2]):
pred_id = int(np.argmax(preds[sample])) pred_id = int(np.argmax(results.scores[sample]))
real_id = int(labels[sample]) real_id = int(results.labels[sample])
pred_score = preds[sample][pred_id] pred_score = results.scores[sample][pred_id]
real_score = preds[sample][real_id] real_score = results.scores[sample][real_id]
plt.subplot(count, count, i + 1) plt.subplot(count, count, i + 1)
plt.imshow(images[sample], interpolation="none") plt.imshow(results.images[sample], interpolation="none")
plt.axis("off") plt.axis("off")
if len(names) > 0: if results.names:
plt.title( predicted_label: str | int = results.names[pred_id]
f"P: {names[pred_id]} ({pred_score:.2f})\nE: {names[real_id]} ({real_score:.2f})" real_label: str | int = results.names[real_id]
)
else: else:
plt.title( predicted_label = pred_id
f"P: {pred_id} ({pred_score:.2f})\nE: {real_id} ({real_score:.2f})" real_label = real_id
) plt.title(
f"P: {predicted_label} ({pred_score:.2f})\n"
f"E: {real_label} ({real_score:.2f})"
)
plt.tight_layout() plt.tight_layout()
# ## Poprawne klasyfikacje # ## Poprawne klasyfikacje
# In[ ]: show_samples_rgb(correct_indices, results, count=5)
show_samples_rgb(correct_indices, predicted, X_test, y_test, 5, cifar_names)
# ## Błędne klasyfikacje # ## Błędne klasyfikacje
# In[ ]: show_samples_rgb(incorrect_indices, results, count=5)
show_samples_rgb(incorrect_indices, predicted, X_test, y_test, 5, cifar_names)

7
Programming/twm_4/twm.py Normal file → Executable file
View File

@ -1,3 +1,8 @@
#!/usr/bin/env python3
"""Smoke check: report which TensorFlow the notebook will actually run against."""
import sys
import tensorflow as tf import tensorflow as tf
print(tf.__version__) sys.stdout.write(f"{tf.__version__}\n")

View File

@ -32,23 +32,36 @@ own docs and the rest of this fleet carry. A test asserts by raising.
|---|---| |---|---|
| Baseline, before any of this | 9,281 | | Baseline, before any of this | 9,281 |
| After the format + safe-fix pass and excluding `TRAK/sightpy` | 3,686 | | After the format + safe-fix pass and excluding `TRAK/sightpy` | 3,686 |
| **Now** | **651** | | **Now** | **423** |
Courses at zero: `WDWR`, `SPD`, `MOM`, `ECRYPT`, `ECRYPT_PROJECT`, `ECOTE`, Courses at zero: `WDWR`, `SPD`, `MOM`, `ECRYPT`, `ECRYPT_PROJECT`, `ECOTE`,
`NLP`, `EARIN`, `PORR`, `TRAK`. What is left, by course: `NLP`, `EARIN`, `PORR`, `TRAK`, `twm_4`. What is left, by course:
| Course | ruff | Can it be verified by running? | | Course | ruff | Can it be verified by running? |
|---|---|---| |---|---|---|
| `twm_4` | 228 | No: `jupyter` on PATH is a dead pipx shim. `pip install jupyter` in a venv would fix that |
| `ERSMS-project` | 156 | No: docker-compose course | | `ERSMS-project` | 156 | No: docker-compose course |
| `PSD` | 152 | No: `run.sh` predates the harness and installs packages with sudo | | `PSD` | 152 | No: `run.sh` predates the harness and installs packages with sudo |
| `PBAD` | 115 | No: same dead jupyter shim | | `PBAD` | 115 | No: same dead jupyter shim |
**Before running the notebook courses, fix `scripts/course_run.sh` first.** `course_run.sh`'s notebook branch is fixed: it executes to a temp
Its notebook branch is `jupyter nbconvert --execute --inplace`, and all four `--output-dir`, probes jupyter by running it rather than by `command -v`, and
notebooks are tracked WITH their outputs -- that would overwrite a committed `course_main` now fails if a run changed anything git can see under the course
deliverable. This exact bug has already been found twice (TRAK's root. `PBAD` is still blocked on jupyter itself -- the binary on PATH is a
`outputs/output.png`, SPD's `results_*.png`); execute to a temp path instead. pipx shim whose interpreter is gone, so `--probe` reports "needs jupyter".
**A notebook does not need to be executed to be linted.** ruff reads `.ipynb`
natively and rewrites `source` only, so the check that the deliverable is
intact is a hash of everything EXCEPT `source`:
```python
stripped = {k: v for k, v in nb.items() if k != "cells"}
stripped["cells"] = [{k: v for k, v in c.items() if k != "source"} for c in nb["cells"]]
```
That hash was identical across all four passes on `twm_4`. The notebook also
round-trips byte-for-byte through
`json.dumps(nb, indent=1, ensure_ascii=False) + "\n"`, which is what makes
editing cells programmatically safe -- check that before touching PBAD's three.
## mypy has NOT been run against any of this ## mypy has NOT been run against any of this
@ -64,6 +77,7 @@ RUFF findings unless it says otherwise**; only `TRAK` has been type-checked.
| `NLP` | 29 | | `NLP` | 29 |
| `EARIN/lab1` alone | 46 | | `EARIN/lab1` alone | 46 |
| `TRAK` | **0** -- cleared 2026-08-29 | | `TRAK` | **0** -- cleared 2026-08-29 |
| `twm_4` | **0** -- cleared 2026-08-29 |
| `PSD/zin1` | 5 | | `PSD/zin1` | 5 |
Two structural notes for whoever runs it next: Two structural notes for whoever runs it next:
@ -90,7 +104,7 @@ Two structural notes for whoever runs it next:
| Type | Count | Note | | Type | Count | Note |
|---|---|---| |---|---|---|
| `.txt` | 27 | Blocked -- see `TODO-file-length-250.md` | | `.txt` | 27 | Blocked -- see `TODO-file-length-250.md` |
| `.py` | 10 | Several GREW during the lint pass, because docstrings and annotations are lines: `NLP/gpt_chunks.py` 911 -> 973, `EARIN/lab2/main.py` 581 -> 647, `PORR/.../linear_algebra_utils.py` 422 -> 553. Split them AFTER the lint work, not during. TRAK is already clear: its three over-cap files were split in the same pass that linted them, each verified by a byte-identical render | | `.py` | 10 | Several GREW during the lint pass, because docstrings and annotations are lines: `NLP/gpt_chunks.py` 911 -> 973, `EARIN/lab2/main.py` 581 -> 647, `PORR/.../linear_algebra_utils.py` 422 -> 553. Split them AFTER the lint work, not during. TRAK is already clear: its three over-cap files were split in the same pass that linted them, each verified by a byte-identical render. `twm_4/TWM_KerasIntro.py` joined the list at 407 lines and should NOT be split: it is an nbconvert export, and a partial one |
| `.cpp` | 9 | | | `.cpp` | 9 | |
| `.c` | 4 | | | `.c` | 4 | |
| `.java` | 2 | | | `.java` | 2 | |

View File

@ -77,7 +77,16 @@ exclude = [
# not a suppression of our own code: without it mypy cannot import them at all # not a suppression of our own code: without it mypy cannot import them at all
# and reports nothing else about the file that uses them. # and reports nothing else about the file that uses them.
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = ["Imath.*", "OpenEXR.*", "OpenGL.*", "matplotlib.*"] module = [
"Imath.*",
"OpenEXR.*",
"OpenGL.*",
"matplotlib.*",
"keras.*",
"tensorflow.*",
"sklearn.*",
"seaborn.*",
]
ignore_missing_imports = true ignore_missing_imports = true
[tool.coverage.run] [tool.coverage.run]