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 |
|---|---|
| `TWM_KerasIntro.ipynb` | The submitted notebook — the deliverable. |
| `TWM_KerasIntro.py` | An `nbconvert` export of that notebook. Derived, not edited by hand. |
| `twm.py` | Helper code used by the notebook. |
| `TWM_KerasIntro.py` | An `nbconvert` export of the notebook's **CIFAR-10 section only**. |
| `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
reviewable and diffable; edit the notebook, then re-export.
The notebook is the source of truth. The `.py` is kept because it makes that
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": [
"import itertools\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",
"import matplotlib.pyplot as plt # MATLAB like plotting routines\n",
"import numpy as np # advanced math library\n",
"import seaborn as sns\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.layers import (\n",
" Activation,\n",
@ -101,9 +107,14 @@
") # Types of layers to be used in our model\n",
"from keras.models import Sequential # Model type to be used\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.preprocessing import OneHotEncoder\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": [],
"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",
"\n",
"print(\"X_train shape\", X_train.shape)\n",
"print(\"y_train shape\", y_train.shape)\n",
"print(\"X_test shape\", X_test.shape)\n",
"print(\"y_test shape\", y_test.shape)"
"sys.stdout.write(f\"X_train shape {X_train.shape}\\n\")\n",
"sys.stdout.write(f\"y_train shape {y_train.shape}\\n\")\n",
"sys.stdout.write(f\"X_test shape {X_test.shape}\\n\")\n",
"sys.stdout.write(f\"y_test shape {y_test.shape}\\n\")"
]
},
{
@ -162,20 +174,28 @@
"source": [
"plt.rcParams[\"figure.figsize\"] = (9, 9) # Make the figures a bit bigger\n",
"\n",
"CLASS_COUNT = 10\n",
"\n",
"def visualize_classes(X, y):\n",
" for i in range(10):\n",
" img_batch = X[y == i][0:10]\n",
" img_batch = np.reshape(\n",
" img_batch, (img_batch.shape[0] * img_batch.shape[1], img_batch.shape[2])\n",
"\n",
"def visualize_classes(images: np.ndarray, labels: np.ndarray) -> None:\n",
" \"\"\"Show ten samples of each class as one wide strip.\n",
"\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",
" 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.axis(\"off\")\n",
" plt.imshow(img, cmap=\"gray\")\n",
" plt.imshow(np.concatenate(strips, axis=1), cmap=\"gray\")\n",
"\n",
"\n",
"visualize_classes(X_train, y_train)"
@ -203,12 +223,14 @@
"outputs": [],
"source": [
"# 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",
" for x in mat:\n",
" for i, y in enumerate(x):\n",
" print((\"{:\" + str(col_maxes[i]) + fmt + \"}\").format(y), end=\" \")\n",
" print()\n",
" for row in mat:\n",
" for i, value in enumerate(row):\n",
" # Two trailing spaces and no newline: this builds one line per row.\n",
" sys.stdout.write((\"{:\" + str(col_maxes[i]) + fmt + \"} \").format(value))\n",
" sys.stdout.write(\"\\n\")\n",
"\n",
"\n",
"# now print!\n",
@ -257,44 +279,42 @@
},
"outputs": [],
"source": [
"import matplotlib.patheffects as PathEffects\n",
"import seaborn as sns\n",
"from sklearn.manifold import TSNE\n",
"\n",
"RS = 19238\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",
"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",
" palette = np.array(sns.color_palette(\"hls\", 10))\n",
" palette = np.array(sns.color_palette(\"hls\", CLASS_COUNT))\n",
"\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",
" 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.ylim(-25, 25)\n",
" ax.axis(\"off\")\n",
" ax.axis(\"tight\")\n",
"\n",
" # We add the labels for each digit.\n",
" txts = []\n",
" for i in range(10):\n",
" for i in range(CLASS_COUNT):\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.set_path_effects(\n",
" [PathEffects.Stroke(linewidth=5, foreground=\"w\"), PathEffects.Normal()]\n",
" [patheffects.Stroke(linewidth=5, foreground=\"w\"), patheffects.Normal()]\n",
" )\n",
" txts.append(txt)\n",
"\n",
"\n",
"def plot_tsne(X, y):\n",
" print(\"calculating tsne ...\")\n",
" proj = TSNE(random_state=RS, learning_rate=200, init=\"random\").fit_transform(X)\n",
" scatter(proj, y)"
"def plot_tsne(features: np.ndarray, labels: np.ndarray) -> None:\n",
" \"\"\"Project `features` to two dimensions with t-SNE and scatter them.\"\"\"\n",
" sys.stdout.write(\"calculating tsne ...\\n\")\n",
" proj = TSNE(random_state=RS, learning_rate=200, init=\"random\").fit_transform(\n",
" features\n",
" )\n",
" scatter(proj, labels)"
]
},
{
@ -364,8 +384,8 @@
")\n",
"X_test /= 255\n",
"\n",
"print(\"Training matrix shape\", X_train.shape)\n",
"print(\"Testing matrix shape\", X_test.shape)"
"sys.stdout.write(f\"Training matrix shape {X_train.shape}\\n\")\n",
"sys.stdout.write(f\"Testing matrix shape {X_test.shape}\\n\")"
]
},
{
@ -447,7 +467,8 @@
"outputs": [],
"source": [
"# 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",
"model.add(\n",
" Dense(512, input_shape=(784,))\n",
@ -462,10 +483,11 @@
},
"outputs": [],
"source": [
"# An \"activation\" is a non-linear function applied to the output of the layer above.\n",
"# It checks the new value of the node, and decides whether that artifical neuron has fired.\n",
"# The Rectified Linear Unit (ReLU) converts all negative inputs to nodes in the next layer to be zero.\n",
"# Those inputs are then not considered to be fired.\n",
"# An \"activation\" is a non-linear function applied to the output of the layer\n",
"# above. It checks the new value of the node, and decides whether that\n",
"# artifical neuron has 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",
"\n",
"model.add(Activation(\"relu\"))"
@ -512,8 +534,9 @@
"outputs": [],
"source": [
"# 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",
"# they receive 512 inputs from the output of the first 512-node layer.\n",
"# However, instead of each of the 512-node receiving 784-inputs from the input\n",
"# image data, they receive 512 inputs from the output of the first 512-node\n",
"# layer.\n",
"\n",
"model.add(Dense(512))\n",
"model.add(Activation(\"relu\"))\n",
@ -537,9 +560,10 @@
},
"outputs": [],
"source": [
"# The final layer of 10 neurons in fully-connected to the previous 512-node layer.\n",
"# The final layer of a FCN should be equal to the number of desired classes (10 in this case).\n",
"model.add(Dense(10))"
"# The final layer of 10 neurons in fully-connected to the previous 512-node\n",
"# layer. The final layer of a FCN should be equal to the number of desired\n",
"# classes (10 in this case).\n",
"model.add(Dense(CLASS_COUNT))"
]
},
{
@ -550,8 +574,8 @@
},
"outputs": [],
"source": [
"# The \"softmax\" activation represents a probability distribution over K different possible outcomes.\n",
"# Its values are all non-negative and sum to 1.\n",
"# The \"softmax\" activation represents a probability distribution over K\n",
"# different possible outcomes. Its values are all non-negative and sum to 1.\n",
"\n",
"model.add(Activation(\"softmax\"))"
]
@ -710,8 +734,8 @@
"outputs": [],
"source": [
"score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n",
"print(\"Test accuracy:\", score[1])"
"sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
]
},
{
@ -723,18 +747,24 @@
"outputs": [],
"source": [
"def plot_confusion_matrix(\n",
" cm, classes, normalize=False, title=\"Confusion matrix\", cmap=plt.cm.Blues\n",
"):\n",
" \"\"\"This function prints and plots the confusion matrix.\n",
" cm: np.ndarray,\n",
" classes: list[str],\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",
" \"\"\"\n",
" if normalize:\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",
" print(\"Confusion matrix, without normalization\")\n",
" sys.stdout.write(\"Confusion matrix, without normalization\\n\")\n",
"\n",
" print(cm)\n",
" sys.stdout.write(f\"{cm}\\n\")\n",
"\n",
" plt.imshow(cm, interpolation=\"nearest\", cmap=cmap)\n",
" plt.title(title)\n",
@ -756,7 +786,17 @@
"\n",
" plt.ylabel(\"True 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": [],
"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",
" for i, sample in enumerate(indices[: count**2]):\n",
" pred_id = int(np.argmax(preds[sample]))\n",
" real_id = int(labels[sample])\n",
" pred_score = preds[sample][pred_id]\n",
" real_score = preds[sample][real_id]\n",
" pred_id = int(np.argmax(samples.scores[sample]))\n",
" real_id = int(samples.labels[sample])\n",
" pred_score = samples.scores[sample][pred_id]\n",
" real_score = samples.scores[sample][real_id]\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",
" if len(names) > 0:\n",
" plt.title(\n",
" f\"P: {names[pred_id]} ({pred_score:.2f})\\nE: {names[real_id]} ({real_score:.2f})\"\n",
" )\n",
" if samples.names:\n",
" predicted_label: str | int = samples.names[pred_id]\n",
" real_label: str | int = samples.names[real_id]\n",
" else:\n",
" plt.title(\n",
" f\"P: {pred_id} ({pred_score:.2f})\\nE: {real_id} ({real_score:.2f})\"\n",
" )\n",
" predicted_label = pred_id\n",
" real_label = real_id\n",
" plt.title(\n",
" f\"P: {predicted_label} ({pred_score:.2f})\\n\"\n",
" f\"E: {real_label} ({real_score:.2f})\"\n",
" )\n",
"\n",
" plt.tight_layout()\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": [],
"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": [],
"source": [
"# 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",
"X_train = X_train.reshape(\n",
" 60000, 28, 28, 1\n",
@ -1054,8 +1100,8 @@
")\n",
"X_test /= 255\n",
"\n",
"print(\"Training matrix shape\", X_train.shape)\n",
"print(\"Testing matrix shape\", X_test.shape)"
"sys.stdout.write(f\"Training matrix shape {X_train.shape}\\n\")\n",
"sys.stdout.write(f\"Testing matrix shape {X_test.shape}\\n\")"
]
},
{
@ -1167,8 +1213,9 @@
"outputs": [],
"source": [
"# 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",
"# because we are actually LOADING the data into the network in batches before processing each batch\n",
"# Besides loss function considerations as before, this method actually results\n",
"# in significant memory savings because we are actually LOADING the data into\n",
"# the network in batches before processing each batch\n",
"\n",
"# Before the data was all loaded into memory, but then processed in batches.\n",
"\n",
@ -1217,8 +1264,8 @@
"outputs": [],
"source": [
"score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n",
"print(\"Test accuracy:\", score[1])"
"sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
]
},
{
@ -1271,7 +1318,7 @@
},
"outputs": [],
"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": [],
"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": [],
"source": [
"def visualize2(model, layer, img, r, c):\n",
" from keras import Model\n",
"\n",
"def visualize2(\n",
" model: Sequential, layer: object, img: np.ndarray, r: int, c: int\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",
" img = np.expand_dims(img, axis=0)\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",
" feature_maps = model.predict(img)\n",
" feature_maps = feature_model.predict(img)\n",
" for fmap in feature_maps:\n",
" # plot all 64 maps in an 8x8 squares\n",
" ix = 1\n",
@ -1409,31 +1457,34 @@
"outputs": [],
"source": [
"# 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",
" (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",
" train_ids = np.in1d(y_train, digits)\n",
" test_ids = np.in1d(y_test, digits)\n",
" X_train = X_train[train_ids]\n",
" X_test = X_test[test_ids]\n",
" train_ids = np.isin(y_train, digits).ravel()\n",
" test_ids = np.isin(y_test, digits).ravel()\n",
" x_train = x_train[train_ids]\n",
" x_test = x_test[test_ids]\n",
" y_train = y_train[train_ids]\n",
" y_test = y_test[test_ids]\n",
"\n",
" X_train = np.expand_dims(X_train, axis=-1)\n",
" X_test = np.expand_dims(X_test, axis=-1)\n",
" x_train = np.expand_dims(x_train, axis=-1)\n",
" x_test = np.expand_dims(x_test, axis=-1)\n",
"\n",
" X_train = X_train.astype(\n",
" x_train = x_train.astype(\n",
" \"float32\"\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",
" X_train /= (\n",
" x_train /= (\n",
" 255 # normalize each value for each pixel for the entire vector for each input\n",
" )\n",
" X_test /= 255\n",
" x_test /= 255\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": [],
"source": [
"# 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",
"\n",
" # Convolution Layer 1\n",
@ -1485,12 +1537,11 @@
"outputs": [],
"source": [
"# One-hot encoding of non-consecutive labels\n",
"def one_hot(labels):\n",
" from sklearn.preprocessing import OneHotEncoder\n",
"\n",
"def one_hot(labels: np.ndarray) -> np.ndarray:\n",
" \"\"\"One-hot encode `labels`, which need not be 0..n-1.\"\"\"\n",
" encoder = OneHotEncoder(categories=\"auto\")\n",
" l = labels.reshape(-1, 1)\n",
" output = encoder.fit_transform(l)\n",
" column = labels.reshape(-1, 1)\n",
" output = encoder.fit_transform(column)\n",
" return output.toarray()"
]
},
@ -1502,23 +1553,25 @@
},
"outputs": [],
"source": [
"def test_model(model, X_test, Y_test, y_test, digits):\n",
" score = model.evaluate(X_test, Y_test)\n",
" print(\"Test score:\", score[0])\n",
" print(\"Test accuracy:\", score[1])\n",
"def test_model(\n",
" model: Sequential,\n",
" x_test: np.ndarray,\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",
" # The predict_classes function outputs the highest probability class\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",
"\n",
" actual_classes = [digits[x] for x in predicted_classes]\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",
"\n",
" class_names = [str(i) for i in digits]\n",
@ -1648,7 +1701,10 @@
"outputs": [],
"source": [
"# 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",
" new_model = Sequential()\n",
" for ix in range(starting_layer_ix, ending_layer_ix + 1):\n",
@ -1892,46 +1948,50 @@
},
"outputs": [],
"source": [
"import os\n",
"import urllib\n",
"# The scheme is spelled out at each use site rather than folded into this\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",
"def download_and_load(class_names, test_split=0.2, max_items_per_class=10000):\n",
" root = \"data\"\n",
" if not os.path.exists(root):\n",
" os.makedirs(root)\n",
"def download_and_load(\n",
" class_names: list[str],\n",
" test_split: float = 0.2,\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",
" print(\"downloading ...\")\n",
" base = \"https://storage.googleapis.com/quickdraw_dataset/full/numpy_bitmap/\"\n",
" sys.stdout.write(\"downloading ...\\n\")\n",
" for c in class_names:\n",
" cc = c.replace(\"_\", \"%20\")\n",
" path = base + cc + \".npy\"\n",
" print(path)\n",
" if not os.path.exists(f\"{root}/{cc}.npy\"):\n",
" urllib.request.urlretrieve(path, f\"{root}/{cc}.npy\")\n",
" path = f\"https://{QUICKDRAW_HOST}/{cc}.npy\"\n",
" sys.stdout.write(f\"{path}\\n\")\n",
" destination = root / f\"{cc}.npy\"\n",
" if not destination.exists():\n",
" with urllib.request.urlopen(path) as response:\n",
" destination.write_bytes(response.read())\n",
" else:\n",
" print(\"Already downloaded\")\n",
" print(\"loading ...\")\n",
" sys.stdout.write(\"Already downloaded\\n\")\n",
" sys.stdout.write(\"loading ...\\n\")\n",
"\n",
" # initialize variables\n",
" x = np.empty([0, 784])\n",
" y = np.empty([0])\n",
"\n",
" # load each data file\n",
" for idx, file in enumerate(class_names):\n",
" file = file.replace(\"_\", \"%20\")\n",
" data = np.load(f\"{root}/{file}.npy\")\n",
" for idx, name in enumerate(class_names):\n",
" quoted = name.replace(\"_\", \"%20\")\n",
" data = np.load(root / f\"{quoted}.npy\")\n",
" data = data[0:max_items_per_class, :]\n",
" labels = np.full(data.shape[0], idx)\n",
"\n",
" x = np.concatenate((x, data), axis=0)\n",
" y = np.append(y, labels)\n",
"\n",
" data = None\n",
" labels = None\n",
"\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",
" y = y[permutation]\n",
"\n",
@ -1947,8 +2007,8 @@
" x_train = x[test_size : x.shape[0], :]\n",
" y_train = y[test_size : y.shape[0]]\n",
"\n",
" print(\"Training Data : \", x_train.shape[0])\n",
" print(\"Testing Data : \", x_test.shape[0])\n",
" sys.stdout.write(f\"Training Data : {x_train.shape[0]}\\n\")\n",
" sys.stdout.write(f\"Testing Data : {x_test.shape[0]}\\n\")\n",
" return x_train, y_train, x_test, y_test"
]
},
@ -1977,9 +2037,7 @@
},
"outputs": [],
"source": [
"with open(\"categories.txt\") as f:\n",
" all_class_names = f.readlines()\n",
"\n",
"all_class_names = Path(\"categories.txt\").read_text().splitlines()\n",
"\n",
"random.seed(RANDOM_SEED)\n",
"\n",
@ -1990,10 +2048,11 @@
"# select random 10 classes\n",
"class_names = random.sample(all_class_names, nb_classes)\n",
"\n",
"# you can also try those \"hard\" classes instead\n",
"# class_names = ['wheel', 'pizza', 'smiley_face', 'apple', 'potato', 'basketball', 'soccer_ball', 'brain', 'clock', 'circle']\n",
"# The lab also suggests a deliberately hard fixed set instead of a random one:\n",
"# wheel, pizza, smiley_face, apple, potato, basketball, soccer_ball, brain,\n",
"# clock and circle.\n",
"\n",
"print(class_names)\n",
"sys.stdout.write(f\"{class_names}\\n\")\n",
"\n",
"X_train, y_train, X_test, y_test = download_and_load(class_names)\n",
"\n",
@ -2125,8 +2184,8 @@
"outputs": [],
"source": [
"score = model.evaluate(X_test, Y_test)\n",
"print(\"Test score:\", score[0])\n",
"print(\"Test accuracy:\", score[1])"
"sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
"sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")"
]
},
{
@ -2166,7 +2225,7 @@
},
"outputs": [],
"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": [],
"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_test = y_test.reshape((1, -1))[0]\n",
"\n",
"print(\"Training matrix shape\", X_train.shape, y_train.shape)\n",
"print(\"Testing matrix shape\", X_test.shape, y_test.shape)\n",
"sys.stdout.write(f\"Training matrix shape {X_train.shape} {y_train.shape}\\n\")\n",
"sys.stdout.write(f\"Testing matrix shape {X_test.shape} {y_test.shape}\\n\")\n",
"\n",
"# one-hot format classes\n",
"\n",
@ -2271,23 +2332,22 @@
},
"outputs": [],
"source": [
"for i in range(10):\n",
" img_batch = X_train[y_train == i][0:10]\n",
" img_batch = np.reshape(\n",
" img_batch,\n",
" (\n",
" img_batch.shape[0] * img_batch.shape[1],\n",
" img_batch.shape[2],\n",
" img_batch.shape[3],\n",
" ),\n",
"strips = []\n",
"for class_id in range(CLASS_COUNT):\n",
" img_batch = X_train[y_train == class_id][0:10]\n",
" strips.append(\n",
" np.reshape(\n",
" img_batch,\n",
" (\n",
" img_batch.shape[0] * img_batch.shape[1],\n",
" img_batch.shape[2],\n",
" img_batch.shape[3],\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.axis(\"off\")\n",
"plt.imshow(img, cmap=\"gray\")"
"plt.imshow(np.concatenate(strips, axis=1), cmap=\"gray\")"
]
},
{
@ -2310,7 +2370,8 @@
},
"outputs": [],
"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",
"\n",
" # Convolution Layer 1\n",
@ -2335,7 +2396,8 @@
" return model\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",
"\n",
" # Convolution Layer 1\n",
@ -2360,7 +2422,8 @@
" return model\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",
"\n",
" # Convolutional Layers with Max Pooling\n",
@ -2386,7 +2449,8 @@
" return model\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",
"\n",
" # Convolution Layer 1\n",
@ -2530,14 +2594,14 @@
},
"outputs": [],
"source": [
"for model in models:\n",
" score = model.evaluate(X_test, Y_test)\n",
" print(\"Test score:\", score[0])\n",
" print(\"Test accuracy:\", score[1])\n",
"for trained in models:\n",
" score = trained.evaluate(X_test, Y_test)\n",
" sys.stdout.write(f\"Test score: {score[0]}\\n\")\n",
" sys.stdout.write(f\"Test accuracy: {score[1]}\\n\")\n",
"\n",
" # The predict_classes function outputs the highest probability class\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",
"\n",
" # Check which items we got right / wrong\n",
@ -2547,7 +2611,7 @@
"\n",
" cnf_matrix = confusion_matrix(y_test, predicted_classes)\n",
"\n",
" class_names = [str(i) for i in range(10)]\n",
" class_names = [str(i) for i in range(CLASS_COUNT)]\n",
"\n",
" # Plot non-normalized confusion matrix\n",
" plt.figure()\n",
@ -2569,24 +2633,27 @@
},
"outputs": [],
"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",
" for i, sample in enumerate(indices[: count**2]):\n",
" pred_id = int(np.argmax(preds[sample]))\n",
" real_id = int(labels[sample])\n",
" pred_score = preds[sample][pred_id]\n",
" real_score = preds[sample][real_id]\n",
" pred_id = int(np.argmax(samples.scores[sample]))\n",
" real_id = int(samples.labels[sample])\n",
" pred_score = samples.scores[sample][pred_id]\n",
" real_score = samples.scores[sample][real_id]\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",
" if len(names) > 0:\n",
" plt.title(\n",
" f\"P: {names[pred_id]} ({pred_score:.2f})\\nE: {names[real_id]} ({real_score:.2f})\"\n",
" )\n",
" if samples.names:\n",
" predicted_label: str | int = samples.names[pred_id]\n",
" real_label: str | int = samples.names[real_id]\n",
" else:\n",
" plt.title(\n",
" f\"P: {pred_id} ({pred_score:.2f})\\nE: {real_id} ({real_score:.2f})\"\n",
" )\n",
" predicted_label = pred_id\n",
" real_label = real_id\n",
" plt.title(\n",
" f\"P: {predicted_label} ({pred_score:.2f})\\n\"\n",
" f\"E: {real_label} ({real_score:.2f})\"\n",
" )\n",
"\n",
" plt.tight_layout()"
]
@ -2612,7 +2679,9 @@
},
"outputs": [],
"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": [],
"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[ ]:
import sys
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()
Needs a GPU: the script exits early without one, exactly as the notebook does.
`print` is `sys.stdout.write` here so the output is identical to the notebook's
without a logging handler having to be configured first.
"""
from __future__ import annotations
import itertools
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from keras.datasets import cifar10
from keras.layers import (
Activation,
@ -36,20 +35,59 @@ from sklearn.metrics import confusion_matrix
from tensorflow.keras.optimizers import Adam
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(
cm, classes, normalize=False, title="Confusion matrix", cmap=plt.cm.Blues
):
"""This function prints and plots the confusion matrix.
cm: np.ndarray,
classes: Sequence[str],
*,
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`.
"""
if normalize:
cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
sys.stdout.write("Normalized confusion matrix\n")
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.title(title)
@ -87,15 +125,13 @@ X_test /= 255
y_train = y_train.reshape((1, -1))[0]
y_test = y_test.reshape((1, -1))[0]
print("Training matrix shape", X_train.shape, y_train.shape)
print("Testing matrix shape", X_test.shape, y_test.shape)
sys.stdout.write(f"Training matrix shape {X_train.shape} {y_train.shape}\n")
sys.stdout.write(f"Testing matrix shape {X_test.shape} {y_test.shape}\n")
# one-hot format classes
nb_classes = 10
Y_train = to_categorical(y_train, nb_classes)
Y_test = to_categorical(y_test, nb_classes)
Y_train = to_categorical(y_train, NUM_CLASSES)
Y_test = to_categorical(y_test, NUM_CLASSES)
cifar_names = [
"airplane",
@ -113,23 +149,23 @@ cifar_names = [
# ## Podgląd zbioru treningowego
# In[ ]:
for i in range(10):
img_batch = X_train[y_train == i][0:10]
img_batch = np.reshape(
img_batch,
(
img_batch.shape[0] * img_batch.shape[1],
img_batch.shape[2],
img_batch.shape[3],
),
# 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.
strips = []
for class_id in range(NUM_CLASSES):
img_batch = X_train[y_train == class_id][0:10]
strips.append(
np.reshape(
img_batch,
(
img_batch.shape[0] * img_batch.shape[1],
img_batch.shape[2],
img_batch.shape[3],
),
)
)
if i > 0:
img = np.concatenate([img, img_batch], axis=1)
else:
img = img_batch
img = np.concatenate(strips, axis=1)
plt.figure(figsize=(10, 20))
plt.axis("off")
plt.imshow(img, cmap="gray")
@ -137,10 +173,9 @@ plt.imshow(img, cmap="gray")
# ## Przygotowanie modelu
# In[ ]:
def generate_model():
def generate_model() -> Sequential:
"""Build the minimal variant: one conv layer straight into the classifier."""
model = Sequential() # Linear stacking of layers
# Convolution Layer 1
@ -154,7 +189,7 @@ def generate_model():
# ...
# 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.summary()
@ -165,7 +200,8 @@ def generate_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
# Convolution Layer 1
@ -179,7 +215,7 @@ def generate_model_default():
# ...
# 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.summary()
@ -190,7 +226,8 @@ def generate_model_default():
return model
def generate_model_gemini():
def generate_model_gemini() -> Sequential:
"""Build the variant Gemini proposed: two conv blocks and heavy dropout."""
model = Sequential()
# Convolutional Layers with Max Pooling
@ -204,7 +241,7 @@ def generate_model_gemini():
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dropout(0.5)) # Regularization
model.add(Dense(10, activation="softmax"))
model.add(Dense(NUM_CLASSES, activation="softmax"))
# Model Compilation
model.compile(
@ -216,7 +253,11 @@ def generate_model_gemini():
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
# Convolution Layer 1
@ -251,7 +292,7 @@ def generate_model_chat():
model.add(Dropout(0.6))
# 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.summary()
@ -263,9 +304,6 @@ def generate_model_chat():
return model
# In[ ]:
model = generate_model()
model_default = generate_model_default()
model_gemini = generate_model_gemini()
@ -275,9 +313,6 @@ models = [model_chat]
# ## Trening
# In[ ]:
gen = ImageDataGenerator(
rotation_range=8,
width_shift_range=0.08,
@ -287,38 +322,35 @@ gen = ImageDataGenerator(
validation_split=0.2,
)
train_generator = gen.flow(X_train, Y_train, batch_size=128, subset="training")
valid_generator = gen.flow(X_train, Y_train, batch_size=128, subset="validation")
# In[ ]:
train_generator = gen.flow(X_train, Y_train, batch_size=BATCH_SIZE, subset="training")
valid_generator = gen.flow(X_train, Y_train, batch_size=BATCH_SIZE, subset="validation")
# Max 20 epoch
for model in models:
model.fit(
for trained in models:
trained.fit(
train_generator,
steps_per_epoch=40000 // 128,
epochs=20,
steps_per_epoch=TRAIN_IMAGES // BATCH_SIZE,
epochs=EPOCHS,
verbose=1,
validation_data=valid_generator,
validation_steps=10000 // 128,
validation_steps=VALID_IMAGES // BATCH_SIZE,
)
# ## Test
# In[ ]:
for model in models:
score = model.evaluate(X_test, Y_test)
print("Test score:", score[0])
print("Test accuracy:", score[1])
for evaluated in models:
score = evaluated.evaluate(X_test, Y_test)
sys.stdout.write(f"Test score: {score[0]}\n")
sys.stdout.write(f"Test accuracy: {score[1]}\n")
# The predict_classes function outputs the highest probability class
# 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)
results = Predictions(
scores=predicted, images=X_test, labels=y_test, names=cifar_names
)
# Check which items we got right / wrong
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)
class_names = [str(i) for i in range(10)]
class_names = [str(i) for i in range(NUM_CLASSES)]
# Plot non-normalized confusion matrix
plt.figure()
@ -338,42 +370,38 @@ for model in models:
plt.show()
# In[ ]:
def show_samples_rgb(indices, preds, images, labels, count=3, names=[]):
def show_samples_rgb(
indices: np.ndarray, results: Predictions, *, count: int = 3
) -> None:
"""Plot a `count` x `count` grid of samples, titled predicted vs expected."""
plt.figure()
for i, sample in enumerate(indices[: count**2]):
pred_id = int(np.argmax(preds[sample]))
real_id = int(labels[sample])
pred_score = preds[sample][pred_id]
real_score = preds[sample][real_id]
pred_id = int(np.argmax(results.scores[sample]))
real_id = int(results.labels[sample])
pred_score = results.scores[sample][pred_id]
real_score = results.scores[sample][real_id]
plt.subplot(count, count, i + 1)
plt.imshow(images[sample], interpolation="none")
plt.imshow(results.images[sample], interpolation="none")
plt.axis("off")
if len(names) > 0:
plt.title(
f"P: {names[pred_id]} ({pred_score:.2f})\nE: {names[real_id]} ({real_score:.2f})"
)
if results.names:
predicted_label: str | int = results.names[pred_id]
real_label: str | int = results.names[real_id]
else:
plt.title(
f"P: {pred_id} ({pred_score:.2f})\nE: {real_id} ({real_score:.2f})"
)
predicted_label = pred_id
real_label = real_id
plt.title(
f"P: {predicted_label} ({pred_score:.2f})\n"
f"E: {real_label} ({real_score:.2f})"
)
plt.tight_layout()
# ## Poprawne klasyfikacje
# In[ ]:
show_samples_rgb(correct_indices, predicted, X_test, y_test, 5, cifar_names)
show_samples_rgb(correct_indices, results, count=5)
# ## Błędne klasyfikacje
# In[ ]:
show_samples_rgb(incorrect_indices, predicted, X_test, y_test, 5, cifar_names)
show_samples_rgb(incorrect_indices, results, count=5)

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
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 |
| 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`,
`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? |
|---|---|---|
| `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 |
| `PSD` | 152 | No: `run.sh` predates the harness and installs packages with sudo |
| `PBAD` | 115 | No: same dead jupyter shim |
**Before running the notebook courses, fix `scripts/course_run.sh` first.**
Its notebook branch is `jupyter nbconvert --execute --inplace`, and all four
notebooks are tracked WITH their outputs -- that would overwrite a committed
deliverable. This exact bug has already been found twice (TRAK's
`outputs/output.png`, SPD's `results_*.png`); execute to a temp path instead.
`course_run.sh`'s notebook branch is fixed: it executes to a temp
`--output-dir`, probes jupyter by running it rather than by `command -v`, and
`course_main` now fails if a run changed anything git can see under the course
root. `PBAD` is still blocked on jupyter itself -- the binary on PATH is a
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
@ -64,6 +77,7 @@ RUFF findings unless it says otherwise**; only `TRAK` has been type-checked.
| `NLP` | 29 |
| `EARIN/lab1` alone | 46 |
| `TRAK` | **0** -- cleared 2026-08-29 |
| `twm_4` | **0** -- cleared 2026-08-29 |
| `PSD/zin1` | 5 |
Two structural notes for whoever runs it next:
@ -90,7 +104,7 @@ Two structural notes for whoever runs it next:
| Type | Count | Note |
|---|---|---|
| `.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 | |
| `.c` | 4 | |
| `.java` | 2 | |

View File

@ -77,7 +77,16 @@ exclude = [
# 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.
[[tool.mypy.overrides]]
module = ["Imath.*", "OpenEXR.*", "OpenGL.*", "matplotlib.*"]
module = [
"Imath.*",
"OpenEXR.*",
"OpenGL.*",
"matplotlib.*",
"keras.*",
"tensorflow.*",
"sklearn.*",
"seaborn.*",
]
ignore_missing_imports = true
[tool.coverage.run]