{ "cells": [ { "cell_type": "markdown", "id": "03df8edb", "metadata": {}, "source": [ "# ๐Ÿท Machine Learning Homework: Find the Hidden Groups\n", "\n", "Welcome to your worksheet on **unsupervised learning**.\n", "\n", "In the first Supervised homework you had the answers, each penguin came with its species. Here the situation is different. We have measurements, but **nobody tells\n", "us the groups**. The job of the model is to find the groups on its own. That is **unsupervised**\n", "learning.\n", "\n", "We use a small set of data about **wines**. Each wine has 13 chemical measurements. We do not tell\n", "the model anything about the type of wine. We ask it to sort the wines into groups by itself, and\n", "then we check how well it did.\n", "\n", "### How to use this worksheet\n", "* Some cells have blank spaces marked `___` and `# TODO`. Fill them in, then run the cell.\n", "* Every task ends with a short footer:\n", " * โœ… **Check yourself:** what your result should look like.\n", " * ๐Ÿ”ฌ **Why it matters:** what this idea is used for in normal life.\n", " * ๐ŸŽ‰ **Fun fact:** how this idea is used in particle physics.\n", " * ๐Ÿ’ก **Go further:** an extra challenge, if you want to push yourself.\n", "\n", "You do not need any physics or any earlier machine learning set up to do this (suggested to be done on goole collab)." ] }, { "cell_type": "markdown", "id": "cb2c4868", "metadata": {}, "source": [ "## Setup\n", "Run this one time. Everything here is already available in Google Colab." ] }, { "cell_type": "code", "execution_count": null, "id": "af285c68", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "from sklearn.datasets import load_wine\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.cluster import KMeans\n", "from sklearn.decomposition import PCA\n", "from sklearn.metrics import adjusted_rand_score\n", "\n", "print(\"Setup complete\")" ] }, { "cell_type": "markdown", "id": "8bfe3c1a", "metadata": {}, "source": [ "## Part 0. Meet the data ๐Ÿ” (about 5 min)\n", "\n", "We load the wine measurements. Notice that we take only the measurements (`X`). We keep the true\n", "wine types (`y_true`) hidden in a drawer, and we will not show them to the model. We only use them\n", "at the very end, to check the work.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "f0f1aa99", "metadata": {}, "outputs": [], "source": [ "wine = load_wine()\n", "X = wine.data # 13 measurements for each wine\n", "y_true = wine.target # the real wine type: we hide this from the model\n", "\n", "# TODO: print the shape of X to see how many wines and how many measurements we have\n", "print(\"Shape of X:\", X.___)\n", "print(\"Number of measurements:\", len(wine.feature_names))" ] }, { "cell_type": "markdown", "id": "314b3a45", "metadata": {}, "source": [ "โœ… **Check yourself:** you should see **178 wines** and **13 measurements** each.\n", "\n", "๐Ÿ”ฌ **Why it matters:** most data in the real world has no labels. Nobody has gone through and\n", "marked every shopper, every photo, or every collision. Unsupervised learning is how we find\n", "structure when there are no answers to copy.\n", "\n", "๐ŸŽ‰ **Fun fact:** when particles fly out of a collision, physicists use grouping methods like the\n", "one in this homework to gather them into sprays called jets. Studying jets is how they learn about\n", "quarks and gluons, tiny particles that are never seen on their own.\n", "\n", "๐Ÿ’ก **Go further:** print `wine.feature_names`. These are real chemical measurements, such as\n", "alcohol and colour intensity. Which ones do you think might separate the wines?\n" ] }, { "cell_type": "markdown", "id": "1ca9c73b", "metadata": {}, "source": [ "## Part 1. Prepare the data ๐Ÿงน (about 5 min)\n", "\n", "Clustering measures the distance between wines. If one measurement uses much bigger numbers than\n", "another, it will control all the distances. So, just like with the SVM in the first homework, we\n", "put every measurement on a fair scale first.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "e6997e89", "metadata": {}, "outputs": [], "source": [ "# TODO: fit a StandardScaler on X and use it to transform X into X_scaled\n", "scaler = StandardScaler()\n", "X_scaled = scaler.___(X)\n", "\n", "print(\"Before scaling, first wine:\", X[0][:3].round(2))\n", "print(\"After scaling, first wine: \", X_scaled[0][:3].round(2))" ] }, { "cell_type": "markdown", "id": "9886259e", "metadata": {}, "source": [ "โœ… **Check yourself:** after scaling, the numbers should be small and centred around 0\n", "(some negative, some positive), instead of the large raw values.\n", "\n", "๐Ÿ”ฌ **Why it matters:** putting features on a fair scale is one of the most important steps in any\n", "project that measures distance between points. Forgetting it is a very common mistake.\n", "\n", "๐ŸŽ‰ **Fun fact:** a single particle detector can record hundreds of numbers for one collision, on\n", "many different scales. Physicists must prepare and balance these numbers with great care before\n", "any model can use them.\n", "\n", "๐Ÿ’ก **Go further:** try the clustering later without scaling and compare. You will usually get worse\n", "groups when the measurements are not balanced.\n" ] }, { "cell_type": "markdown", "id": "0184ded0", "metadata": {}, "source": [ "## Part 2. K means clustering ๐ŸŽฏ (about 7 min)\n", "\n", "Now the main idea. **K means** places a number of centre points in the data, then moves them\n", "around until each one sits in the middle of a group of nearby wines. We ask it for 3 groups,\n", "because we believe there are 3 kinds of wine. The model does not know this. It only sees numbers.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "207ab3c9", "metadata": {}, "outputs": [], "source": [ "# TODO: create a KMeans model that looks for 3 clusters, then fit it on X_scaled\n", "kmeans = KMeans(n_clusters=___, random_state=42, n_init=10)\n", "kmeans.fit(X_scaled)\n", "\n", "clusters = kmeans.labels_ # the group number that the model gave to each wine\n", "print(\"First 20 group labels:\", clusters[:20])\n", "print(\"Wines in each group:\", np.bincount(clusters))" ] }, { "cell_type": "markdown", "id": "2fc10194", "metadata": {}, "source": [ "โœ… **Check yourself:** every wine now has a group label of 0, 1 or 2. The three groups should\n", "have roughly **65, 51 and 62** wines in them. The model built these groups without ever seeing the\n", "true wine types.\n", "\n", "๐Ÿ”ฌ **Why it matters:** grouping unlabelled data is used everywhere. Shops use it to place customers\n", "into shopping types. Streaming apps use it to group films that feel similar. It is one of the most\n", "common tools in real machine learning.\n", "\n", "๐ŸŽ‰ **Fun fact:** grouping particles into jets uses the very same idea of gathering nearby points\n", "together. It is one of the most used tools in all of particle physics.\n", "\n", "๐Ÿ’ก **Go further:** the value `kmeans.inertia_` measures how tight the groups are (smaller is\n", "tighter). Print it. We will use it in the next part.\n" ] }, { "cell_type": "markdown", "id": "e06d015d", "metadata": {}, "source": [ "## Part 3. How many groups? ๐Ÿ“ (about 7 min)\n", "\n", "We asked for 3 groups, but what if we did not know the answer? A common trick is to try many\n", "values and look at how tight the groups are. As we add more groups, the tightness score (called\n", "inertia) always drops. We look for the point where it stops dropping quickly. That bend is a good\n", "choice for the number of groups.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "54f0f26d", "metadata": {}, "outputs": [], "source": [ "# TODO: for each k from 1 to 6, fit KMeans and store its inertia (tightness score)\n", "inertias = []\n", "for k in range(1, 7):\n", " model = KMeans(n_clusters=k, random_state=42, n_init=10).fit(X_scaled)\n", " inertias.append(model.___)\n", "\n", "plt.plot(range(1, 7), inertias, marker=\"o\")\n", "plt.xlabel(\"number of groups (k)\"); plt.ylabel(\"inertia (lower is tighter)\")\n", "plt.title(\"Look for the bend\"); plt.show()" ] }, { "cell_type": "markdown", "id": "65977a85", "metadata": {}, "source": [ "โœ… **Check yourself:** the line should fall fast from 1 to 3 groups, then flatten out. The\n", "clearest bend is at **3 groups**, which matches the real number of wine types. Nice.\n", "\n", "๐Ÿ”ฌ **Why it matters:** in real problems nobody tells you how many groups exist. Simple checks like\n", "this one help you make a sensible choice instead of just guessing.\n", "\n", "๐ŸŽ‰ **Fun fact:** choosing the right number of groups is a real question in physics too. Pick too\n", "few and you blur different things together. Pick too many and you split one real thing into pieces\n", "that do not mean anything.\n", "\n", "๐Ÿ’ก **Go further:** the bend is not always sharp. Try the same plot without scaling the data and see\n", "how much harder the bend is to find.\n" ] }, { "cell_type": "markdown", "id": "8e690c35", "metadata": {}, "source": [ "## Part 4. See the groups ๐Ÿ‘€ (about 7 min)\n", "\n", "Our wines have 13 measurements, but we can only draw in 2 dimensions. **PCA** is a method that\n", "squeezes many measurements down into a few, while keeping as much of the shape as possible. We\n", "squeeze 13 numbers into 2 so that we can make a picture of the groups.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "5dd634b8", "metadata": {}, "outputs": [], "source": [ "# TODO: use PCA to turn the 13 measurements into just 2, so we can plot them\n", "pca = PCA(n_components=___)\n", "X_2d = pca.fit_transform(X_scaled)\n", "\n", "plt.scatter(X_2d[:, 0], X_2d[:, 1], c=clusters, cmap=\"viridis\")\n", "plt.xlabel(\"PCA direction 1\"); plt.ylabel(\"PCA direction 2\")\n", "plt.title(\"The groups found by K means\"); plt.show()\n", "\n", "print(\"Part of the shape kept by 2 directions:\", round(pca.explained_variance_ratio_.sum(), 3))" ] }, { "cell_type": "markdown", "id": "b4c92c40", "metadata": {}, "source": [ "โœ… **Check yourself:** you should see three fairly separate clouds of points, one colour per\n", "group. The two PCA directions keep about **0.55** (about 55 percent) of the full shape, which is\n", "enough to see clear groups.\n", "\n", "๐Ÿ”ฌ **Why it matters:** humans cannot picture 13 dimensions. Squeezing data down to 2 or 3 so we\n", "can actually look at it is a everyday tool for data scientists.\n", "\n", "๐ŸŽ‰ **Fun fact:** one collision in a big experiment can have hundreds of numbers. Physicists use\n", "methods like this to squeeze them down to a few important directions that they can plot and\n", "understand.\n", "\n", "๐Ÿ’ก **Go further:** colour the same plot by the true wine types instead of the clusters\n", "(`c=y_true`). Do the real groups sit in the same places as the ones the model found?\n" ] }, { "cell_type": "markdown", "id": "ddb18a3a", "metadata": {}, "source": [ "## Part 5. Did we find the real groups? โš–๏ธ (about 6 min)\n", "\n", "Now we open the drawer and take out the true wine types that we hid at the start. Remember, the\n", "model never saw them. Let us check how close the groups it found are to the real ones.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "ef3744ff", "metadata": {}, "outputs": [], "source": [ "# A table: each row is a group the model found, each column is a true wine type.\n", "table = pd.crosstab(clusters, y_true, rownames=[\"group found\"], colnames=[\"true type\"])\n", "print(table)\n", "\n", "# TODO: compute the agreement score between the found groups and the true types.\n", "# adjusted_rand_score gives 1.0 for a perfect match and near 0.0 for random guessing.\n", "score = adjusted_rand_score(y_true, ___)\n", "print(\"\\nAgreement score (1.0 is perfect):\", round(score, 3))" ] }, { "cell_type": "markdown", "id": "c86108f6", "metadata": {}, "source": [ "โœ… **Check yourself:** in the table, almost every wine of one true type should fall into a\n", "single group (each row has one big number and the rest small). The agreement score should be about\n", "**0.90**, which is very high. The model rediscovered the wine types on its own, using only the\n", "chemical numbers.\n", "\n", "๐Ÿ”ฌ **Why it matters:** this is the surprising power of unsupervised learning. With no answer key at\n", "all, it found groups that closely match the truth. That is how we discover structure in data that\n", "no one has ever labelled.\n", "\n", "๐ŸŽ‰ **Fun fact:** unsupervised learning helps physicists search for brand new physics. By learning\n", "what normal collisions look like, they can point to the strange ones that do not fit, which might\n", "hold a particle that nobody has discovered yet.\n", "\n", "๐Ÿ’ก **Go further:** try `KMeans` with 2 groups and with 4 groups, and look at the agreement score\n", "each time. Why does 3 give the best match?\n" ] }, { "cell_type": "markdown", "id": "963d3f81", "metadata": {}, "source": [ "## ๐ŸŽ“ Very well done\n", "\n", "You just did a full unsupervised project. You took data with no labels, prepared it, grouped it\n", "with K means, chose a sensible number of groups, drew a picture of the groups with PCA, and\n", "checked your work against the hidden truth.\n", "\n", "The big lesson: even with no answer key, machine learning can find real structure in data. This is\n", "one of the most powerful ideas in the whole field, and it is how scientists explore data that has\n", "never been sorted before.\n", "\n", "The best way to learn more is to change things and see what happens. Every small experiment turns\n", "a fact that you memorised into an understanding that you own.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }