diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..cd7f405 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.isort", + "eamodio.gitlens", + "tamasfe.even-better-toml", + "ms-toolsai.jupyter", + "ms-python.black-formatter", + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a470c0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "editor.formatOnSave": true, + "git.autofetch": true, + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-python.black-formatter", + }, + "python.analysis.typeCheckingMode": "basic", + "isort.args": [ + "--profile", + "black" + ], + "python.analysis.autoImportCompletions": true, + "python.analysis.autoFormatStrings": true, + "python.analysis.inlayHints.callArgumentNames": true, + "python.analysis.inlayHints.functionReturnTypes": true, + "python.analysis.inlayHints.pytestParameters": true, + "python.analysis.inlayHints.variableTypes": true, + "editor.rulers": [ + 79, + 88 + ], +} \ No newline at end of file diff --git a/app/Home.py b/app/Home.py index cf9f7f7..2089304 100644 --- a/app/Home.py +++ b/app/Home.py @@ -1,16 +1,50 @@ import streamlit as st +from PIL import Image -st.title("Welcome to the Demo Club!") +from python_intro import app_functions, dpaths + +# Set variables + +image = Image.open(dpaths.STATIC / "gzip-performance.png") +paper_url = "https://aclanthology.org/2023.findings-acl.426.pdf" + +st.title("Basic ML Model Prototype") st.markdown( - "Congrats on making it this far! Here's a small reward for all your hard work " - "getting this far - a bit of playtime before we get back to learning." + "This demo is a machine learning system live on your machine, being " + "recalculated in response to your inputs." ) +# Instantiate components +app_functions.cycle_state() + +st.subheader("Gzip Is All You Need") st.markdown( - "This demo is a machine learning system live on your machine, being " - "recalculated in response to your inputs. Have a play around with it, and " - "later we'll explore how to build this site (on branch feature/simple-app), " - "as well as give you a more complicated demo to play with (on branch " - "feature/complex-app)." + f"Recently a paper was [published]({paper_url}) which caused quite a stir. With a " + 'the rather pedestrian title, \'"Low-Resource" Text Classification: A Parameter-' + "Free Classification Method with Compressors', it showed a novel method of " + "attaining competitive text classification scores on many benchmarks. It often " + "outperformed transformer-based approaches, which are a class of significantly " + "more complicated text models." +) +st.markdown( + "The approach used was novel, but also kind of crazy. It was the sort of thing " + "that probably shouldn't work. But, the theory behind it was sound, and gives " + "us insights as to how LLMs achieve the things they do. It's also a handy reminder " + "that you don't always need the most complicated approach, when something simple " + "will do." +) +st.markdown("Tonight, we're going to play with this crazy little idea!") + +st.image( + image, caption="Red is where (bigger, more complex) models lose to a silly approach" +) + +st.markdown( + "As you read this, the model has already been trained, and we're " + "generating some test data now. (This model is fast to train but the current) " + "implementation is slow at test time.) Take a look and have a play, and we'll " + "explain what's going on, and how this works, in the next page. For now, it's " + "conceptually enough to know that the model is mapping the test input to the most " + "'similar' training input, and assuming the label must be the same." ) diff --git a/app/pages/1_Prototype.py b/app/pages/1_Prototype.py index 78574a1..a4f45e4 100644 --- a/app/pages/1_Prototype.py +++ b/app/pages/1_Prototype.py @@ -1,101 +1,12 @@ -import numpy as np -import pandas as pd import streamlit as st -from datasets import load_dataset -from npc_gzip.compressors.base import BaseCompressor -from npc_gzip.compressors.gzip_compressor import GZipCompressor -from npc_gzip.knn_classifier import KnnClassifier -from PIL import Image from sklearn.metrics import classification_report -from python_intro import dpaths +from python_intro import app_functions -# Set variables -image = Image.open(dpaths.STATIC / "gzip-performance.png") -not_so_rng = np.random.default_rng(42) -paper_url = "https://aclanthology.org/2023.findings-acl.426.pdf" +model, label_map, test_cases, test_labels = app_functions.cycle_state() -# Function definitions -@st.cache_data -def create_data(dataset="ag_news"): - # Dataset arg is used to cache properly - dataset = load_dataset(dataset) - - label_map = { - idx: name - for idx, name in enumerate( - dataset["train"].features["label"].names # type:ignore - ) - } - - X_train = np.array(dataset["train"]["text"]) # type: ignore - X_test = np.array(dataset["test"]["text"]) # type: ignore - y_train = np.array(dataset["train"]["label"]) # type: ignore - y_test = np.array(dataset["test"]["label"]) # type: ignore - return X_train, X_test, y_train, y_test, label_map - - -@st.cache_resource -def create_model(x_train, y_train): - compressor = GZipCompressor() - model = KnnClassifier( - compressor=compressor, - training_inputs=x_train.tolist(), - training_labels=y_train.tolist(), - distance_metric="ncd", - ) - return model - - -@st.cache_data -def generate_predictions(_model, list_of_test_items, sampling_percentage=0.2): - (distances, labels, similar_samples) = model.predict( - list_of_test_items, 1, sampling_percentage=sampling_percentage - ) - labels = labels.reshape(-1) - return (distances, labels, similar_samples) - - -# Instantiate components -X_train, X_test, y_train, y_test, label_map = create_data() - -# Train model -model = create_model(X_train, y_train) - - -st.title("Basic ML Model Prototype") - -st.subheader("Gzip Is All You Need") -st.markdown( - f"Recently a paper was [published]({paper_url}) which caused quite a stir. With a " - 'the rather pedestrian title, \'"Low-Resource" Text Classification: A Parameter-' - "Free Classification Method with Compressors', it showed a novel method of " - "attaining competitive text classification scores on many benchmarks. It often " - "outperformed transformer-based approaches, which are a class of significantly " - "more complicated text models." -) -st.markdown( - "The approach used was novel, but also kind of crazy. It was the sort of thing " - "that probably shouldn't work. But, the theory behind it was sound, and gives " - "us insights as to how LLMs achieve the things they do. It's also a handy reminder " - "that you don't always need the most complicated approach, when something simple " - "will do." -) -st.markdown("Tonight, we're going to play with this crazy little idea!") - -st.image( - image, caption="Red is where (bigger, more complex) models lose to a silly approach" -) - -st.markdown( - "As you read this, the model has already been trained, and we're " - "generating some test data now. (This model is fast to train but the current) " - "implementation is slow at test time.) Take a look and have a play, and we'll " - "explain what's going on, and how this works, in the next page. For now, it's " - "conceptually enough to know that the model is mapping the test input to the most " - "'similar' training input, and assuming the label must be the same." -) +st.title("Playing with the Prototype") st.subheader("Dataset Details") st.markdown( @@ -107,15 +18,13 @@ def generate_predictions(_model, list_of_test_items, sampling_percentage=0.2): "(i.e. it will probably do badly with TikTok video titles!). But you can also have " "some fun with this, as the model struggles to classify some obscure input!" ) +intro = f"Here are the dataset labels and the human-friendly categories:" st.markdown( - f"Here are the dataset labels and the human-friendly categories: {label_map}" + "\n".join([intro] + [f"- {key} = {value}" for key, value in label_map.items()]) ) # Prep some test cases -random_indicies = not_so_rng.choice(X_test.shape[0], 100, replace=False) -test_cases = X_test[random_indicies].tolist() -test_labels = y_test[random_indicies].tolist() -(distances, predictions, similar_samples) = generate_predictions( +(distances, predictions, similar_samples) = app_functions.generate_predictions( model, test_cases, 0.025 ) report = classification_report(test_labels, predictions) @@ -131,15 +40,19 @@ def generate_predictions(_model, list_of_test_items, sampling_percentage=0.2): # Add text input st.subheader("Try it yourself!") -st.markdown("Now you can have an idea of") +st.markdown( + "Now you can have an idea of what is likely to work, try inputting a few made-up " + "headlines below. See what words and phrases can confuse the model! (I've found " + "random line-breaks and symbols can often produce strange results.)" +) default_text = ( "Socialites unite - dolphin groups Dolphin groups, or 'pods', rely on socialites " "to keep them from collapsing, scientists claim." ) test_sample = st.text_area("Input a headline here:", value=default_text) -(_, input_prediction_list, similar_samples) = generate_predictions( - model, [test_sample], 0.1 +(_, input_prediction_list, similar_samples) = app_functions.generate_predictions( + model, [test_sample], 0.25 ) user_prediction = input_prediction_list[0] diff --git a/app/pages/2_Compressor_Model_Details.py b/app/pages/2_Compressor_Model_Details.py index e69de29..c45c1cc 100644 --- a/app/pages/2_Compressor_Model_Details.py +++ b/app/pages/2_Compressor_Model_Details.py @@ -0,0 +1,31 @@ +import pathlib + +import streamlit as st +from PIL import Image + +from python_intro import dpaths + +TEXTFILES = pathlib.Path(__file__).parent / "textfiles" + +image_gzip_explained = Image.open(dpaths.STATIC / "gzip-explained.png") +image_nlp_intuition = Image.open(dpaths.STATIC / "nlp-intuition.png") + + +def load_text(filename): + with open(TEXTFILES / filename, "r") as handle: + return handle.read() + + +st.title("A Peek Inside The Black Box") +st.markdown(load_text("2_intro.md")) + +st.subheader("What Compressor Models Do") +st.image(image_gzip_explained) +st.markdown(load_text("2_compressor_explained.md")) + +st.subheader("How We Think Of NLP's Mechanics") +st.image(image_nlp_intuition) +st.markdown(load_text("2_nlp_intuition.md")) + +st.subheader("So What's Going On Here?") +st.markdown(load_text("2_explained.md")) diff --git a/app/pages/textfiles/2_compressor_explained.md b/app/pages/textfiles/2_compressor_explained.md new file mode 100644 index 0000000..1f75118 --- /dev/null +++ b/app/pages/textfiles/2_compressor_explained.md @@ -0,0 +1,18 @@ +As this presentation has been alluding to, GZip is at the core of the compressor model, though it's reasonable to assume that other algorithms could work too. Compression algorithms are used to "shrink" files down. The details of how compression algorithms do this aren't important. We just need to note a single thing - that compression algorithms use repeated patterns in their input to find a more space-efficient 'coding' of the input. That is, compression can be used to make data (including text) shorter, and the more repeating patterns are in the text, the better this works. + +So how does the model use Gzip to make predictions? It's actually quite simple. Alarmingly simple. The model takes in a list of strings - or bits of text - as input. It then compresses the strings and measures the length of the compressed examples. This is just some number $n >= 0$. These values are stored as a set of `(example, length_examples, label)` pairs. With that, the training step is done. No, seriously. + +At inference time, it's a little more complicated, but only a little. Given a new string `input`, the model does the following: + +1. The `input` is compressed and its length (`length_input`) recorded +1. The model randomly samples from the `examples`. +1. The model concatenates (joins) the `input` string to each of the sampled `examples`, compresses the resulting long string, and measures the length `length_joined`. + + - Think about it for a moment and convince yourself that `length_joined` will be at least as long as one of the other two lengths, and should be no longer than both of their lengths added. Let's call these the `lower` and `upper` bounds respectively + +1. We then measure how "far" the `input` is from any other datapoint by considering how "far" from the `lower` bound `length_joined` is. We call this the distance +1. Finally, we check the `labels` where the distance is lowest, corresponding to the most compressible points. We then take the max or use a simple voting algorithm to pick a label. + +So, in essence, what we do is check how much of a compression saving we get between any two strings, and consider this as a measure of similarity. Compared to a transformer or an RNN, this is trivial to reason about. It's technically not even machine learning, because there aren't really parameters here. + +**This is wild.** diff --git a/app/pages/textfiles/2_explained.md b/app/pages/textfiles/2_explained.md new file mode 100644 index 0000000..6f5381e --- /dev/null +++ b/app/pages/textfiles/2_explained.md @@ -0,0 +1,11 @@ +The most important similarity is that we're looking for points that are close to each other in some sense. The most important difference is how we measure what we mean by similar. In NLP, we like to think of it as learning something about the semantics of words with relation to the task. There definitely seem to be cases that suggest this is true - at least some of the time. With our compressor model however, we instead concentrate purely on how much overlap there is between our two inputs. This is purely information theoretic, no learning whatsoever. + +One thing we can immediately notice is the two approaches are both doing the same thing - compression. "Old hands" won't be surprised by this conclusion, but newcomers may be confused into thinking the machine is literally learning. With enough time, it becomes pretty clear to most practitioners that this GZip model is just a distillation of what our models are doing during the training process. Really, our models are just doing a slightly more sophisticated (and nuanced) form of data compression. + +A second point emerges when we consider that the GZip algorithm is purely looking at occurrences of word- and sub-word combinations. This is similar to older NLP methods which use n-grams and word counts as input features, but using shared information to select which elements are relevant to a task and to normalise for sequence length. Using information directly without involving parameters means this method is relatively data efficient. It's also worth noting that byte-pair encoding has become increasingly common for large language models. + +Contrast this zero-training, data efficient approach to the typical NLP method. Normally, when we have a text classification problem we reach straight for Huggingface and do some transfer learning. After all, "all NLP problems are hard" and that justifies jumping up in complexity. Even though transfer learning improves our data efficiency, we still require a lot of data to get even moderately successful results. + +Finally, there's a lesson in humility. We often motivate our explanation of these models through the lens of black boxes and learning latent meanings, rather than information theory and basic encoding. The fact that compression-based techniques perform well on a number of tasks thought to be difficult suggests the tasks may not be as hard as we expected. It definitely calls into question how we interpret what a lot of our heavyweight models are actually doing, and if the only result of this paper is less wooly thinking about ML, our field will benefit. At the very least, we should think twice before reaching for the largest, coolest, most complicated SOTA model. + +Now, nobody is saying this will replace ChatGPT anytime soon, but the fact that this can do so well on benchmarks is suggestive. It confirms certain feature engineering practices (BPE, n-gram and lemmatisation) even deep into the era of transformers. It also suggests that the issue with NLP tasks may not be the inherent difficulty of language, but rather one of finding an efficient representation. It also reminds us that our models do have interpretable analogues in traditional computing, and that we should be hasty in jumping to intuitive-but-complex answers. This GZip model is a handy reminder that Occam's Razor and the KISS principle apply to the study of artificial intelligence, too. diff --git a/app/pages/textfiles/2_intro.md b/app/pages/textfiles/2_intro.md new file mode 100644 index 0000000..e6eac4f --- /dev/null +++ b/app/pages/textfiles/2_intro.md @@ -0,0 +1,5 @@ +This is a quick little explainer as to what's going on here. After all, in the Home page we asserted that this probably shouldn't work. Let's go through the following: + +- explain what this compressor model does +- explain briefly how we think about modern NLP, to get an idea of why this result is so surprising +- dig into why the result works, and some quick lessons we can draw about model ML/AI research diff --git a/app/pages/textfiles/2_nlp_intuition.md b/app/pages/textfiles/2_nlp_intuition.md new file mode 100644 index 0000000..e5dfaba --- /dev/null +++ b/app/pages/textfiles/2_nlp_intuition.md @@ -0,0 +1,10 @@ +We don't have time for a full rundown of how deep learning-based NLP works (it's a huge subject!), but let's concentrate on the essential details. Here's a simplified explanation of most NLP models: + +1. First, we map our words to a maths-friendly representation (like `chocolate = [0.1, 0.333, -2]`) so we can do maths with them. We call this "vectorisation", because we're mapping from our input (text) space into a fixed "vector" space. Sometimes we'll do this at the sub-word level. +1. Next, we use the flexible sequence modelling of deep learning to combine the numbers together according to a flexible, parameterised model. +1. In the output layer of the model, our vectors now live in the "latent space" of our task, and as we train our model, this representation will be more and more useful for whatever task we're doing. We tend to think of this as the model knowing/learning the meanings of these words and how they relate to our output. +1. Finally, we apply some decision function to achieve our task, often some kind of classification. For the sake of similarity with our GZip example, assume we're doing a KNN lookup and just finding the closest points by some distance function. Here, because the model has done a lot of heavy lifting in shaping the meaning of the output space, we often just use the Euclidean (Pythagoras) distance you learned in high school. + +Even though we've assumed we're doing KNN for the sake of discussion, it's worth noting that you can think of tasks like e.g. logistic regression as KNN. The neighbor in question though isn't another datapoint, but with respect to a hypothetical "average" datapoint which is encoded by the parameters of the model. + +Regardless of which way we go, we understand NLP as learning the regularities that encode meaning in language, and understanding the relevance of these meanings as they relate to our task. diff --git a/pdm.lock b/pdm.lock index 14f9aa4..8313401 100644 --- a/pdm.lock +++ b/pdm.lock @@ -6,7 +6,7 @@ groups = ["default", "dev"] cross_platform = true static_urls = false lock_version = "4.3" -content_hash = "sha256:dee6238a9daae6ef533ce368314fac1b0c7b1bf5d4ef1956026c861c3e639bde" +content_hash = "sha256:c163b141191ae328261937f9c1f357a3c5545b5fa8b52987ddc61d9ab2037dbc" [[package]] name = "aiohttp" @@ -216,7 +216,7 @@ files = [ [[package]] name = "autoflake" -version = "2.2.0" +version = "2.2.1" requires_python = ">=3.8" summary = "Removes unused imports and unused variables" dependencies = [ @@ -224,8 +224,8 @@ dependencies = [ "tomli>=2.0.1; python_version < \"3.11\"", ] files = [ - {file = "autoflake-2.2.0-py3-none-any.whl", hash = "sha256:de409b009a34c1c2a7cc2aae84c4c05047f9773594317c6a6968bd497600d4a0"}, - {file = "autoflake-2.2.0.tar.gz", hash = "sha256:62e1f74a0fdad898a96fee6f99fe8241af90ad99c7110c884b35855778412251"}, + {file = "autoflake-2.2.1-py3-none-any.whl", hash = "sha256:265cde0a43c1f44ecfb4f30d95b0437796759d07be7706a2f70e4719234c0f79"}, + {file = "autoflake-2.2.1.tar.gz", hash = "sha256:62b7b6449a692c3c9b0c916919bbc21648da7281e8506bcf8d3f8280e431ebc1"}, ] [[package]] @@ -487,13 +487,13 @@ files = [ [[package]] name = "datasets" -version = "2.14.4" +version = "2.14.5" requires_python = ">=3.8.0" summary = "HuggingFace community-driven open-source library of datasets" dependencies = [ "aiohttp", "dill<0.3.8,>=0.3.0", - "fsspec[http]>=2021.11.1", + "fsspec[http]<2023.9.0,>=2023.1.0", "huggingface-hub<1.0.0,>=0.14.0", "multiprocess", "numpy>=1.17", @@ -506,8 +506,8 @@ dependencies = [ "xxhash", ] files = [ - {file = "datasets-2.14.4-py3-none-any.whl", hash = "sha256:29336bd316a7d827ccd4da2236596279b20ca2ac78f64c04c9483da7cbc2459b"}, - {file = "datasets-2.14.4.tar.gz", hash = "sha256:ef29c2b5841de488cd343cfc26ab979bff77efa4d2285af51f1ad7db5c46a83b"}, + {file = "datasets-2.14.5-py3-none-any.whl", hash = "sha256:dd4155091034cba04d5a28711f2ed3944275ed15c5d0c5a2d0b6b9ea34a2bdfe"}, + {file = "datasets-2.14.5.tar.gz", hash = "sha256:b738a86540ab8e1a7806c8a3790b67be0056318d0c5d5a58a1b0dbdd76c0f568"}, ] [[package]] @@ -1313,46 +1313,43 @@ files = [ [[package]] name = "matplotlib" -version = "3.7.2" -requires_python = ">=3.8" +version = "3.8.0" +requires_python = ">=3.9" summary = "Python plotting package" dependencies = [ "contourpy>=1.0.1", "cycler>=0.10", "fonttools>=4.22.0", "kiwisolver>=1.0.1", - "numpy>=1.20", + "numpy<2,>=1.21", "packaging>=20.0", "pillow>=6.2.0", - "pyparsing<3.1,>=2.3.1", + "pyparsing>=2.3.1", "python-dateutil>=2.7", ] files = [ - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"}, - {file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"}, - {file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"}, - {file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"}, - {file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"}, - {file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"}, + {file = "matplotlib-3.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c4940bad88a932ddc69734274f6fb047207e008389489f2b6f77d9ca485f0e7a"}, + {file = "matplotlib-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a33bd3045c7452ca1fa65676d88ba940867880e13e2546abb143035fa9072a9d"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea6886e93401c22e534bbfd39201ce8931b75502895cfb115cbdbbe2d31f287"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d670b9348e712ec176de225d425f150dc8e37b13010d85233c539b547da0be39"}, + {file = "matplotlib-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b37b74f00c4cb6af908cb9a00779d97d294e89fd2145ad43f0cdc23f635760c"}, + {file = "matplotlib-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:0e723f5b96f3cd4aad99103dc93e9e3cdc4f18afdcc76951f4857b46f8e39d2d"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5dc945a9cb2deb7d197ba23eb4c210e591d52d77bf0ba27c35fc82dec9fa78d4"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b5a1bf27d078453aa7b5b27f52580e16360d02df6d3dc9504f3d2ce11f6309"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f25ffb6ad972cdffa7df8e5be4b1e3cadd2f8d43fc72085feb1518006178394"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732"}, + {file = "matplotlib-3.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36eafe2128772195b373e1242df28d1b7ec6c04c15b090b8d9e335d55a323900"}, + {file = "matplotlib-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:061ee58facb3580cd2d046a6d227fb77e9295599c5ec6ad069f06b5821ad1cfc"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3cc3776836d0f4f22654a7f2d2ec2004618d5cf86b7185318381f73b80fd8a2d"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c49a2bd6981264bddcb8c317b6bd25febcece9e2ebfcbc34e7f4c0c867c09dc"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ed11654fc83cd6cfdf6170b453e437674a050a452133a064d47f2f1371f8d3"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196"}, + {file = "matplotlib-3.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87df75f528020a6299f76a1d986c0ed4406e3b2bd44bc5e306e46bca7d45e53e"}, + {file = "matplotlib-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d74a95fe055f73a6cd737beecc1b81c26f2893b7a3751d52b53ff06ca53f36"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0b11f354aae62a2aa53ec5bb09946f5f06fc41793e351a04ff60223ea9162955"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f54b9fb87ca5acbcdd0f286021bedc162e1425fa5555ebf3b3dfc167b955ad9"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:60a6e04dfd77c0d3bcfee61c3cd335fff1b917c2f303b32524cd1235e194ef99"}, + {file = "matplotlib-3.8.0.tar.gz", hash = "sha256:df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69"}, ] [[package]] @@ -1662,30 +1659,30 @@ files = [ [[package]] name = "pandas" -version = "2.0.3" -requires_python = ">=3.8" +version = "2.1.0" +requires_python = ">=3.9" summary = "Powerful data structures for data analysis, time series, and statistics" dependencies = [ - "numpy>=1.21.0; python_version >= \"3.10\"", + "numpy>=1.22.4; python_version < \"3.11\"", "numpy>=1.23.2; python_version >= \"3.11\"", "python-dateutil>=2.8.2", "pytz>=2020.1", "tzdata>=2022.1", ] files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, + {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, + {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, + {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, + {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, + {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, ] [[package]] @@ -1972,7 +1969,7 @@ files = [ [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" requires_python = ">=3.7" summary = "pytest: simple powerful testing with Python" dependencies = [ @@ -1984,8 +1981,8 @@ dependencies = [ "tomli>=1.0.0; python_version < \"3.11\"", ] files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [[package]] @@ -2441,7 +2438,7 @@ files = [ [[package]] name = "streamlit" -version = "1.25.0" +version = "1.26.0" requires_python = ">=3.8, !=3.9.7" summary = "A faster way to build and share data apps" dependencies = [ @@ -2471,8 +2468,8 @@ dependencies = [ "watchdog>=2.1.5; platform_system != \"Darwin\"", ] files = [ - {file = "streamlit-1.25.0-py2.py3-none-any.whl", hash = "sha256:3c561dca1b5430e73b7f2d66bff1d26103936bb4223912ab563ffee881fccc30"}, - {file = "streamlit-1.25.0.tar.gz", hash = "sha256:8a7c93bee8703869045804afe22e9373c4e974fdb2a3e9abe3b027df3de03119"}, + {file = "streamlit-1.26.0-py2.py3-none-any.whl", hash = "sha256:2bfdac041816e2e1ba27f061d40112afe61e0d4e72d25f354b38ba81107b4cb3"}, + {file = "streamlit-1.26.0.tar.gz", hash = "sha256:25475fb15a3cc9fb184945f3fc936f011998bd8386e0c892febe14c9625bf47a"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 8860481..e5fd8e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,10 +20,10 @@ dev = [ "flake8>=6.1.0", "black>=23.9.1", "isort>=5.12.0", - "mypy>=1.5.1", "autoflake>=2.0.1", "pytest>=7.2.1", "jupyter>=1.0.0", + "mypy>=1.5.1", ] [tool.pdm.scripts] diff --git a/src/python_intro/app_functions.py b/src/python_intro/app_functions.py new file mode 100644 index 0000000..e785574 --- /dev/null +++ b/src/python_intro/app_functions.py @@ -0,0 +1,72 @@ +import numpy as np +import streamlit as st +from datasets import load_dataset +from npc_gzip.compressors.gzip_compressor import GZipCompressor +from npc_gzip.knn_classifier import KnnClassifier + +not_so_rng = np.random.default_rng(42) + + +@st.cache_data +def create_data(dataset="ag_news"): + # Dataset arg is used to cache properly + dataset = load_dataset(dataset) + + label_map = { + idx: name + for idx, name in enumerate( + dataset["train"].features["label"].names # type:ignore + ) + } + + X_train = np.array(dataset["train"]["text"]) # type: ignore + X_test = np.array(dataset["test"]["text"]) # type: ignore + y_train = np.array(dataset["train"]["label"]) # type: ignore + y_test = np.array(dataset["test"]["label"]) # type: ignore + return X_train, X_test, y_train, y_test, label_map + + +@st.cache_data +def generate_predictions(_model, list_of_test_items, sampling_percentage=0.2): + (distances, labels, similar_samples) = _model.predict( + list_of_test_items, 1, sampling_percentage=sampling_percentage + ) + labels = labels.reshape(-1) + return (distances, labels, similar_samples) + + +@st.cache_resource +def create_model(x_train, y_train): + compressor = GZipCompressor() + model = KnnClassifier( + compressor=compressor, + training_inputs=x_train.tolist(), + training_labels=y_train.tolist(), + distance_metric="ncd", + ) + return model + + +@st.cache_resource +def cycle_state(): + checks = ["model", "label_map", "test_cases", "test_labels"] + if all(item in st.session_state for item in checks): + print("Session cache hits!") + model, label_map = st.session_state.model, st.session_state.label_map + test_cases = st.session_state.test_cases + test_labels = st.session_state.test_labels + return model, label_map, test_cases, test_labels + + X_train, X_test, y_train, y_test, label_map = create_data() + model = create_model(X_train, y_train) + + random_indicies = not_so_rng.choice(X_test.shape[0], 100, replace=False) + test_cases = X_test[random_indicies].tolist() + test_labels = y_test[random_indicies].tolist() + + st.session_state.model = model + st.session_state.test_cases = test_cases + st.session_state.test_labels = test_labels + st.session_state.label_map = label_map + + return model, label_map, test_cases, test_labels diff --git a/static/gzip-explained.png b/static/gzip-explained.png new file mode 100644 index 0000000..508be0a Binary files /dev/null and b/static/gzip-explained.png differ diff --git a/static/nlp-intuition.png b/static/nlp-intuition.png new file mode 100644 index 0000000..6ed46db Binary files /dev/null and b/static/nlp-intuition.png differ