diff --git a/week8/community_contributions/lisekarimi/10_part1_ensemble_model.ipynb b/week8/community_contributions/lisekarimi/10_part1_ensemble_model.ipynb new file mode 100644 index 0000000..5635a9f --- /dev/null +++ b/week8/community_contributions/lisekarimi/10_part1_ensemble_model.ipynb @@ -0,0 +1,1126 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ede0360-00f4-404e-b0d2-4a83cc385654", + "metadata": { + "id": "3ede0360-00f4-404e-b0d2-4a83cc385654" + }, + "source": [ + "๐Ÿ”— Ensemble Model\n", + "---\n", + "Weโ€™ll reuse core components built earlier:\n", + "\n", + "- A fine-tuned LLaMA model\n", + "- An XGBoost regression model, stored in Hugging Face\n", + "- A ChromaDB vector store, stored on Google Drive and also available on AWS S3\n", + "- A GPT-4o mini + RAG pipeline\n", + "\n", + "We'll run all three models on the same test data, gather their predictions, and train a Linear Regression Ensemble. The ensemble learns how to combine these predictions to output a more accurate final price.\n", + "\n", + "Once trained, we'll save the ensemble as ensemble_model.pkl, ready for later use.\n", + "\n", + "- ๐Ÿง‘โ€๐Ÿ’ป Skill Level: Advanced\n", + "- โš™๏ธ Hardware: โš ๏ธ GPU required (use Google Colab)\n", + "- ๐Ÿ› ๏ธ Requirements: \n", + "\n", + " - ๐Ÿ”‘ Hugging Face Token and OpenAI Key โ€” must be set in Google Colab secrets or .env files if you are running with your own GPU\n", + " - completion of Part 9 of [this series of notebooks](https://github.com/lisekarimi/lexo)\n", + "- ๐ŸŽฏ Task: Train and save the Ensemble Model\n", + "\n", + "---\n", + "๐Ÿ“ข Find more LLM notebooks on my [GitHub repository](https://github.com/lisekarimi/lexo)" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mzYB4XYQeWRQ", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "mzYB4XYQeWRQ", + "outputId": "f474ce9b-09fb-4a47-93d7-273fe2d2ba10" + }, + "outputs": [], + "source": [ + "# Install required packages in Google Colab\n", + "%pip install -q tqdm huggingface_hub numpy sentence-transformers datasets chromadb xgboost peft torch bitsandbytes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3caecd1-8712-4acd-80b5-e8059c16f43f", + "metadata": { + "id": "b3caecd1-8712-4acd-80b5-e8059c16f43f" + }, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import re\n", + "import zipfile\n", + "import chromadb\n", + "import joblib\n", + "import numpy as np\n", + "import pandas as pd\n", + "import requests\n", + "import torch\n", + "from datasets import load_dataset\n", + "from google.colab import userdata\n", + "from huggingface_hub import HfApi, hf_hub_download, login\n", + "from openai import OpenAI\n", + "from peft import PeftModel\n", + "from sentence_transformers import SentenceTransformer\n", + "from sklearn.linear_model import LinearRegression\n", + "from sklearn.metrics import r2_score, mean_squared_error\n", + "from sklearn.metrics import r2_score\n", + "from tqdm import tqdm\n", + "from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05d9523f-b6c9-4132-bd2b-6712772b3cd2", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "05d9523f-b6c9-4132-bd2b-6712772b3cd2", + "outputId": "7077320e-43e2-4b03-ca7d-e7ea9a3407f8" + }, + "outputs": [], + "source": [ + "# Mount Google Drive to access saved ChromaDB and XGBoost model files\n", + "\n", + "from google.colab import drive\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "z9735RD_TUHw", + "metadata": { + "id": "z9735RD_TUHw" + }, + "outputs": [], + "source": [ + "# Load from Colab's secure storage\n", + "\n", + "openai_api_key = userdata.get(\"OPENAI_API_KEY\")\n", + "openai = OpenAI(api_key=openai_api_key)\n", + "\n", + "hf_token = userdata.get(\"HF_TOKEN\")\n", + "login(hf_token, add_to_git_credential=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DtswsfBQxxJF", + "metadata": { + "id": "DtswsfBQxxJF" + }, + "outputs": [], + "source": [ + "# Configuration\n", + "\n", + "HF_USER = \"lisekarimi\"\n", + "ROOT = \"/content/drive/MyDrive/snapr\"\n", + "os.makedirs(ROOT, exist_ok=True)\n", + "\n", + "api = HfApi(token=hf_token)\n", + "REPO_NAME = \"smart-deal-finder-models\"\n", + "REPO_ID = f\"{HF_USER}/{REPO_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "id": "qByarIFiTYa1", + "metadata": { + "id": "qByarIFiTYa1" + }, + "source": [ + "### ๐Ÿ“ฅ Load Test Dataset" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9ca3e34", + "metadata": {}, + "outputs": [], + "source": [ + "# #If you face NotImplementedError: Loading a dataset cached in a LocalFileSystem is not supported run:\n", + "# %pip install -U datasets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0eKakxSFTVcA", + "metadata": { + "id": "0eKakxSFTVcA" + }, + "outputs": [], + "source": [ + "DATASET_NAME = f\"{HF_USER}/pricer-data\"\n", + "dataset = load_dataset(DATASET_NAME)\n", + "test = dataset[\"test\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cWqvs8JRTggE", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 110 + }, + "id": "cWqvs8JRTggE", + "outputId": "bf7f0113-de82-422a-aaec-54efbb2b9d16" + }, + "outputs": [], + "source": [ + "# Format description function (no price in text)\n", + "def description(item):\n", + " text = item[\"text\"].replace(\n", + " \"How much does this cost to the nearest dollar?\\n\\n\", \"\"\n", + " )\n", + " text = text.split(\"\\n\\nPrice is $\")[0]\n", + " return f\"passage: {text}\"\n", + "\n", + "\n", + "description(test[0])" + ] + }, + { + "cell_type": "markdown", + "id": "alpkYSc2UX0n", + "metadata": { + "id": "alpkYSc2UX0n" + }, + "source": [ + "### ๐Ÿ“ฅ Load Models and ChromaDB" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "pjPBEgXqmHOA", + "metadata": { + "id": "pjPBEgXqmHOA" + }, + "outputs": [], + "source": [ + "# ChromaDB\n", + "\n", + "CHROMA_PATH = f\"{ROOT}/chroma\"\n", + "COLLECTION_NAME = \"price_items\"\n", + "CHROMA_ZIP_URL = \"https://aiprojects-lise-karimi.s3.eu-west-3.amazonaws.com/smart-deal-finder/chroma.zip\"\n", + "\n", + "# Download and unzip if CHROMA_PATH doesn't exist\n", + "if not os.path.exists(CHROMA_PATH):\n", + " os.makedirs(CHROMA_PATH, exist_ok=True)\n", + " r = requests.get(CHROMA_ZIP_URL)\n", + " with open(\"/tmp/chroma.zip\", \"wb\") as f:\n", + " f.write(r.content)\n", + " with zipfile.ZipFile(\"/tmp/chroma.zip\", \"r\") as zip_ref:\n", + " zip_ref.extractall(CHROMA_PATH)\n", + "\n", + "client = chromadb.PersistentClient(path=CHROMA_PATH)\n", + "collection = client.get_or_create_collection(name=COLLECTION_NAME)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fi1BS71XCv1", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 337, + "referenced_widgets": [ + "c60f1153084a493ea31fac10bf986aef", + "6de41ac188dd48aea5d30a90bc52c38c", + "d2b4cdcaef6a4c41972f8c96af2814ab", + "2180dfb4e6e74df5bf9985c481b6e420", + "dcff84c8c3bf4f4bae334e0484207d10", + "ca1ed709ecaa4a0e8ac96ffb930e6613", + "e5297e7d36334c57aece043f62c79841", + "5fdca4e0987a4788983c418941711d7e", + "200e8e9b0df84affa177567243bf18d1", + "ef6e6dcff8b444bba62c1f76e1127d7c", + "7c94357c0d4e444489e8d47d2151437b", + "4ffdcd2ec96046ffb5121def27c95c9d", + "dd47ad1efe46496cb096a7714cf27c19", + "194ad4f8707b4d288e88cdbdfa33605d", + "c8a05ae3f5854f24998cc615a8849c88", + "b5ec411e72a946f8b4a470de5827c949", + "1a0be25b030d43858cee804da65d67a1", + "88d1f6a56f9b4a50854aee82c0945cc9", + "73e5967ae96942e080f3b05638583bc8", + "8434bfa06abf42c98e8ffb0e7b83c9f9", + "43d872e632da4d9883ea3d71dc91bdf9", + "86729f54df1b4967b2730b48f84a98aa", + "83fcebcf2b2c4213835334a998ba91e9", + "4ef06b10bfcb418d85534a8b73688eff", + "88c466cc89234d8f9f21147882fc5faf", + "f87e958c639544c0b925646fc28c4604", + "a52988b97dff4759a456398ecea1eaaf", + "c1116a13be86401bbbf6e51de0df7d12", + "d6ed27ce322748d29ed864808f619ee3", + "4a6fbedd3333496081695800cae8bdda", + "a7badc083fb34e69bd6f27bc9a805e7b", + "a78bcdbac2f74c72938d87c431f23e78", + "1d627cf1043642a3815a2902f65b4ded", + "3b8cc480ded24f66b03779fd25844670", + "0ce0073368c64339b3c1f960861e4b56", + "ffc973a4347943ebaa4ead16e04c05f6", + "aafba411ee984946a3ec0760580b60b6", + "0dd2501d917f48739b2817d598541660", + "213ca3afc47945a68e28a6ae005c3b7d", + "6355e004d7c34b969b2d2c6ccbc12620", + "9359b873cb4a4187b67a1732d78c7534", + "32f86a2b9e0547a6bc0a523ca3cfa088", + "0f446cb8ee3147438ef1e98e665a2831", + "64bb7ebae66d42f2a4d6a3039bf67d4b", + "e2b51ee511234ff2bc2cf33227fe2088", + "a76d3def06db41fab4ab2f077839d5bc", + "fa9598b858c14024aaf15d1417e9683d", + "df0bb9a9635643ebb679e115f45dde8e", + "527c4d1987334e3e9b2aa0de7d0527a7", + "0c6a889a9066484abbcb87b730d7e325", + "80a1f4c902154f2184c38ef844a1cca7", + "463c3cc65cd343108fe6049e4cde7142", + "fb015ce2fd9b48d79db67f80181964b7", + "07f46375dc594cd19ac5ab983083b2de", + "451ca5f213544cc8b24de6b7d55602fc", + "5bb2e645ea7741839e0f88ae484d94d2", + "19d1353070f643e08364500e9b1c30e6", + "fff94d7934cc4793876903d1c18efbfa", + "f0413b6310ef4510bf493e6814fa162d", + "a97c1662e64c44ee9f6e5be5617c07ec", + "da02a5ab5fe44cc297ab3048509a99e1", + "ee8c23aa2ca84b32a02a2500917559d8", + "6635ef559f72485e9453f87b3921f954", + "d9f2925a563d4d9fa332c15205f44d9b", + "73ec7891d53149e7a072a0e310716178", + "f7309076b36e4224acc42ade5d09bf37", + "cd53294ff44e4955afbfbd4660563b58", + "2ae42eb6385f43fab59f2bb56bb8a28e", + "fc0f1abaaf054d0d93a27c7ee0f6630d", + "c7a61078596c475784307480d26e3661", + "5bd26e4ff28f4639b52aab848ada03d1", + "ce710ef5ffa14cfe9842c63caebc81e5", + "63061726d47940c395a00d5d01556f4f", + "2b41598231d14f3ca6354c9543ec4351", + "0df33079f63c45d39de21439289aa4a8", + "8cc5d2eac9a64d68b72608bb5ae44c89", + "dd33a409204c4610a08e44c3e82e00da", + "b96b7ac71a6c48a9a6c888f2f34efea5", + "794d71dd5b734a3cb5607fc31aaddd18", + "0f6e7a2d9b8846178a7492e137d83bba", + "657bb839f0ea40eb9873385cecd06fd0", + "1ac174e8904943bb9a5e5483e58eef63", + "a1c9714fc4ea48af83669481e89c58c7", + "763a2d64c8e94ca1b0289264d9f868bc", + "ef78cf15ab914b3fa95ff95a86ec7a99", + "24350ebdc38a41e689f3e3b09dfc3e35", + "b8131b3c4c4c4b20809af9b0e91dd006", + "420c50ed8abe49ec9f4f2777e6cd2749", + "fc3f2d2c33ee40f8850710c2f4ce331f", + "90730ec699e84ddda2af799f8220e7a5", + "01f1c4b3b434474dbf2212a05869354d", + "912d6c1687324bb9b334bbf98a2b5b30", + "dc4106a0020b4b9fa21cd12a44967f2e", + "0084e537ffa74ab4a6d5f307b0916d2c", + "0edeb9ca771c4ca2a9a678e0e8a91614", + "b610d515ddb4405695e6972e45463194", + "4af72cb05f284d42bca73fcb88904255", + "86f34fb6325e4e878eee0be27946c88b", + "bfc26f456f1d440bb80eedac1cb14967", + "1f005c3cf7594275a37bb937a3c33db3", + "4347e7d3db4d4cc3836a4e69db032f27", + "809a4d0270dd4c05817ea224bb78ff5a", + "b403a344e84342e4b076e64e829d7354", + "6b3cbb0ac0b14e3fa193cd5cb3f8f521", + "2d06aaf8d15b456b8fadeb54dd2ea73d", + "74842e94dda648d18cd055220a3d2b39", + "5a3818bde07841fbb6077bf20b7dec4b", + "2a5224c8b3004d249a07297a2111493f", + "ff02ac7f08974426b3f70b71e59ed5bb", + "0dd550d3e39f42809fa16770231af7e5" + ] + }, + "id": "8fi1BS71XCv1", + "outputId": "9256b509-1371-4bb3-bd84-98bb75725ac3" + }, + "outputs": [], + "source": [ + "# Embedding Model\n", + "\n", + "embedding_model = SentenceTransformer(\"intfloat/e5-small-v2\", device=\"cuda\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "zmwIbufXUzMo", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 483, + "referenced_widgets": [ + "36b26207185e4e10a8c60f0f5918aa7c", + "96a0c5044c824968a701e20319c8d037", + "a567759a6e554c2eb9334559e880a56e", + "dcc68ce908ac4cfc9ffdcd0333cce14b", + "06d7ac4abde74e1496ef80b9e22cd148", + "94ba3ecd90f84291a46462a51ba001b8", + "00b94b9d1e4e4e428c7467b4f99e45af", + "02219374302849bd93d7aba7f65ee42b", + "82223db2023045e88dc3d9652bc6b183", + "6e08a796d2c440c58d5e6bb20f39be16", + "6ac04725da4f485f8d7efd738de0940a", + "230a6add3adb4dbd9f0329412e3f1455", + "db5f777e8ec143fe89a2e6296031c9c2", + "4f87c3aba240471d96349a037777732e", + "74b04b71ce0746a89d146c4044c85890", + "582e3cd91e3048639ab48492b5ca4b15", + "1d8e31a4418c49e0bfa28b399673f718", + "ec17f9e6bbbc4584baf11c0e7c504f84", + "19779fea3c8c4a5cbea953b059775110", + "3c82450a6db649f59f36668d5521d203", + "3447d41776ef406bbfbe3e6c277b82d2", + "4195859f8457499ba8e61a9f1662931c", + "6f80cc0bed9b42e89f9691675ff5a484", + "dc39eee63325479a915101649ab04273", + "99e1e2c842f845d1b2ded34736b60ad7", + "20ff78d4df09401d8aee462b57c57a48", + "1aff04abbd0b4f15bd58154b00591264", + "9cf024b488154926861d695137268da1", + "6470d6b03e3548d783ed252d128ec361", + "230a79f3d05d4c9eac73ae6962cb5d2d", + "adfe1cc7a6f94b0ab43170c75688374d", + "b6d24621c29f4352862f37ae69f2d6ad", + "303a6f79669e40d98ed2998f4f5e47cc", + "b737799377c948a99edf34c014a105c4", + "daf9616a687f406da1d9ee2bd147c850", + "cfd34b617326486498a916531bae9a87", + "201ea14e31a244a3aa2aeed2c12fb255", + "8454d5f263eb45daa0e6f7db6aa3f92d", + "f9787ee50d634421ae5f0325126dcb73", + "c2200daaf2994ed9ae16587d8d52236c", + "93a0ec9723074e9199d1f9db988c30dc", + "b1cb687d58ba40f49961d5485a466ef7", + "ccc580852d66401993d675c254832379", + "7321b89aea1c4746901ef40548bcf056", + "8fae24599e174689adbaa52a16270785", + "01605d6fb53d4e56aca9a746b2c75566", + "45cb352704fe41f1be0f01c61511323d", + "881550789f4845dda8561f6b26aff204", + "654bf64ec165449993b195209d75f4ba", + "9eaa6e09335047d5987c0a6528d5e77e", + "39331a837a644795bada1e2b034fe14d", + "a339562553394755811bb7077a81843d", + "4f58f5fa385f44e0ae09e2019294d597", + "431cc587951845d6af39f3e4ab0f2f76", + "ac879c2c923b423dabd6d0d60b12266d", + "4a3eb0fc1d2d4606a8acb382085a57af", + "f82bbcbd14ad48a78ebdfdfb43916bd1", + "a634449526034dbdb945c4905f4edeba", + "da4133a915ad4d449876a43468203842", + "a56f3a61d1ba4011ab6fce4067fa8418", + "46727d5afdf7487fb073e7e2d25cc75d", + "1480c5a1a0ad4151a12d47bc22685f04", + "5720fc31c90344908d9eeb49fe83df1b", + "47bd422d424a47e48edd304773162082", + "6a68ecb89be34255a0e0fc6db41c1f4d", + "12b1b3c7f0914030ad756b676cc97962", + "780f5b6ad91142f991a936b55219f61d", + "45723f91352b49688469a95e7f47aa9d", + "90bbf502500340a1993a957c27ad3d33", + "dcbb25b2a082476d905bcf124a849322", + "38e6bd6d64b54f9a8cdb4f40eaf41cde", + "7544a101cad94a15a2f4eb5639d22525", + "501184a0fc424a02a80aecd3f62fb9fd", + "1cbf5d28bced46ac8712a4609b5a5867", + "9ba6ecc0a422472681d8e61bdb32f87b", + "8383700148dd44538ed81ec5a261b7b5", + "740e930ff17c4f668818a8c762a5470e", + "0d995d8da0464c9ca7b1b444c22de025", + "bc8a5e6d27ba402996434f00918c8b0e", + "75f225b1a6f845148361b029878b63ea", + "c16b051d3cb44607b339770f5f8b6f2e", + "7857ed1e0b0f45bdb48269fbad68653d", + "e65ffc77bd6740c7924aa5b93297cb89", + "3ba97a41b4654dc0bb9bcaaa685b4518", + "6504931865a74cb5a80f2ac60da47430", + "ff8d5791b13c440d81312a6b96c9592f", + "c31bd8cb693b4e248a29f2ded032fb70", + "c25b1a42547a422ba7597c99ca4ce249", + "5c6338fcad9344e092f5077bf73c4910", + "dca176a7a6ea4fe9b025f851976f436a", + "611fc076771a4fcca5c46367b711d61a", + "0cfde45e26cf4c05b67755c2274f2df8", + "5d572d2f46ea484587e085c29318b616", + "8ad59c1261844a06b7abecebd7b60377", + "82d610cd077c47bd9efd609f2399c861", + "07fd58c9d07144a7a0aacab6b8252125", + "aa0932b4e66b4f33ba9f5237ea1470da", + "0d867615a23a42988bb91b4f0d0cc942", + "9c28ff7b0f5c421390ac1ccb899f093f", + "0645e7ed6593410eaf9c9c0b25158667", + "ca68b1dc60a343f9bd7298a63cadd556", + "9597ec6b495c4298b87967ed3e4044db", + "d7dad0ae58814124af1e92a078122736", + "173575b8b5254537937206759d6b6262", + "d1efdc10d36441d88cf7705e846bfbef", + "3550d450f95f40eeae0c0d559ae9f4de", + "773df79ed7b44f698cce98ca9ed802cc", + "24a2b4b88e1d46488eabb9101536beac", + "0d61c01e6cdf445b9474f9d759676edd", + "9b3505aacd164a19b45aba89eee46378", + "bd6fb8b066be46aaa7d457bf89257e54", + "f1eca4e5d600407885264d340b4f47a7", + "ef1e6a69995845e09781f76a38fced30", + "c5f50067867a40b99cb9f312e8adc49f", + "4e9cf63dbef041aba2c7f0b9c74466c8", + "07baa025b3a14bf89d6f6b438b695bbd", + "fb8609ef5b8d4653b25e52f853b7be1f", + "2f90ac58752347319d1203b5e8765c0e", + "d9815cabf324472a8eab585afabfa47e", + "120687e04065424595571941d816a134", + "a6d09159931f4d3d91a0647d9fa9d8ba", + "833c755f7bf9479abeef0041a82a92ba", + "56d848676e644c739e28730af99d69c6", + "39654eca8add4c09815cb3e6a45616ec", + "02aa70e064744a29af0a68aaca33c741", + "7fd83f95cfef4b1dbd881be7083d7455", + "cdf56625053b417fad2e64a0bed6725b", + "4199341c09bd46fab8a3b649d0c8af7a", + "334e67f38b8243aa9072f52a32e46080", + "492cdb40ffbf444d8e256875663fc598", + "655c6e0f21ff4e9db7f35522355d847c", + "926c1be6e26f4d9eba332881f975ed38", + "47641f7363be4252b9f5e53846bee057", + "887f8a2b268541eab71804a44ba1479b", + "5b6e78d1727e473ab3b66d6ff042aeca", + "acc60a1210104049983341db3010be0a", + "c028c37980e14b3ea07b1da6f558651e", + "9d1085906e3548078e5e393a86337c3e", + "259c86a51f4e4cab9648cc603fc25c7e", + "8ce05076e77643a88b062687e2b24493", + "8b3b7f947f4d4401bbca47d5720f7450", + "9bf9dccee248425da698dbb4526fcad9", + "b991477124184bf3b4397762649a6596", + "fb139bcae29f49778bf172eb503c0668", + "93ba52daa9aa4dee8da91bba6c7d0269", + "fefedf36efc94ed287bdeceae698d5b5", + "8288b87b06b34ba4b2c7a343d6cea827", + "5a85d212a6b5468fbc10e6aeb0ad8bee", + "22274b08e14c4c77a6223131779f6f48", + "edc0e436da954a33bbea8e80629eb43c", + "1d9b1c594680467f9c8a6682d8aeb2e7", + "d82f940e0a8a478e8b1ee8f169f798fc", + "ab1616f507594b27b898ede4504b4e39", + "1fed11f1c7484251a2a7400627ad5f6a" + ] + }, + "id": "zmwIbufXUzMo", + "outputId": "2acb6897-4c41-4447-e029-ffcc1b3b4da1" + }, + "outputs": [], + "source": [ + "# Fine Tuned Llama Model\n", + "\n", + "BASE_MODEL = \"meta-llama/Meta-Llama-3.1-8B\"\n", + "FINETUNED_MODEL = \"ed-donner/pricer-2024-09-13_13.04.39\"\n", + "REVISION = \"e8d637df551603dc86cd7a1598a8f44af4d7ae36\"\n", + "\n", + "# Quantization config (4-bit)\n", + "quant_config = BitsAndBytesConfig(\n", + " load_in_4bit=True,\n", + " bnb_4bit_use_double_quant=True,\n", + " bnb_4bit_compute_dtype=torch.bfloat16,\n", + " bnb_4bit_quant_type=\"nf4\",\n", + ")\n", + "\n", + "# Load tokenizer\n", + "tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)\n", + "tokenizer.pad_token = tokenizer.eos_token\n", + "tokenizer.padding_side = \"right\"\n", + "\n", + "# Load base model\n", + "base_model = AutoModelForCausalLM.from_pretrained(\n", + " BASE_MODEL, quantization_config=quant_config, device_map=\"auto\"\n", + ")\n", + "\n", + "# Load fine-tuned model\n", + "fine_tuned_model = PeftModel.from_pretrained(\n", + " base_model, FINETUNED_MODEL, revision=REVISION\n", + ")\n", + "\n", + "# Align generation config\n", + "fine_tuned_model.generation_config.pad_token_id = tokenizer.pad_token_id\n", + "\n", + "print(f\"Memory footprint: {fine_tuned_model.get_memory_footprint() / 1e6:.1f} MB\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0IHiJNU7a4XC", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 49, + "referenced_widgets": [ + "0264a3987fbf4040860ffa3fc47940d8", + "06d1db35940b469797c39c653741ea36", + "84c4d2fdaf734a559ee3eee09f1be295", + "fddde0bfed544b18ba39bfaa40eb9e1b", + "d40cc525cc28416cad4a45b3631798c9", + "e1372af176154902b1f555f30c28c007", + "5a1352c5ceb84320b14353b7aa21650d", + "522d0ed9e705457e9c72d276e2a26dbd", + "4de73aa76f044811990c379737a8e5c0", + "9305e96697ab4854ac89a6636991101d", + "b00e41d1051340fd904ba719111a907d" + ] + }, + "id": "0IHiJNU7a4XC", + "outputId": "c68bc44e-6b15-46c3-c8d9-3f256f368317" + }, + "outputs": [], + "source": [ + "# XGBoost Trained Model\n", + "\n", + "MODEL_FILENAME = \"xgboost_model.pkl\"\n", + "model_path = hf_hub_download(repo_id=REPO_ID, filename=MODEL_FILENAME, token=hf_token)\n", + "xgb_model = joblib.load(model_path)" + ] + }, + { + "cell_type": "markdown", + "id": "76BhcPjWa6C5", + "metadata": { + "id": "76BhcPjWa6C5" + }, + "source": [ + "### ๐Ÿ“Š Model prediction collection" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LgGmUKJxayZ6", + "metadata": { + "id": "LgGmUKJxayZ6" + }, + "outputs": [], + "source": [ + "def extract_tagged_price(output: str):\n", + " \"\"\"Extracts a float price from a string based on 'Price is $' keyword.\"\"\"\n", + " try:\n", + " contents = output.split(\"Price is $\")[1].replace(\",\", \"\")\n", + " match = re.search(r\"[-+]?\\d*\\.\\d+|\\d+\", contents)\n", + " return float(match.group()) if match else 0.0\n", + " except Exception:\n", + " return 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ggKf1nSQbAnv", + "metadata": { + "id": "ggKf1nSQbAnv" + }, + "outputs": [], + "source": [ + "def ft_llama_price(description: str):\n", + " prompt = (\n", + " f\"How much does this cost to the nearest dollar?\\n\\n{description}\\n\\nPrice is $\"\n", + " )\n", + " inputs = tokenizer(prompt, return_tensors=\"pt\").to(\"cuda\")\n", + "\n", + " outputs = fine_tuned_model.generate(\n", + " **inputs, max_new_tokens=5, num_return_sequences=1\n", + " )\n", + "\n", + " result = tokenizer.decode(outputs[0])\n", + " price = extract_tagged_price(result)\n", + " return price" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "_cWyYUd4Ub-K", + "metadata": { + "id": "_cWyYUd4Ub-K" + }, + "outputs": [], + "source": [ + "def xgboost_price(description: str):\n", + " vector = embedding_model.encode([description], normalize_embeddings=True)[0]\n", + " pred = xgb_model.predict([vector])[0]\n", + " return round(float(max(0, pred)), 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3Skod8juXgnN", + "metadata": { + "id": "3Skod8juXgnN" + }, + "outputs": [], + "source": [ + "def gpt4o_price(item):\n", + " def get_embedding(text):\n", + " return embedding_model.encode([text], normalize_embeddings=True)\n", + "\n", + " def find_similars(text):\n", + " results = collection.query(\n", + " query_embeddings=get_embedding(text).astype(float).tolist(), n_results=5\n", + " )\n", + " docs = results[\"documents\"][0]\n", + " prices = [m[\"price\"] for m in results[\"metadatas\"][0]]\n", + " return docs, prices\n", + "\n", + " def format_context(similars, prices):\n", + " context = (\n", + " \"To provide some context, here are similar products and their prices:\\n\\n\"\n", + " )\n", + " for sim, price in zip(similars, prices):\n", + " context += f\"Product:\\n{sim}\\nPrice is ${price:.2f}\\n\\n\"\n", + " return context\n", + "\n", + " def build_messages(description, similars, prices):\n", + " system_message = (\n", + " \"You are a pricing expert. \"\n", + " \"Given a product description and a few similar products with their prices, \"\n", + " \"estimate the most likely price. \"\n", + " \"Respond ONLY with a number, no words.\"\n", + " )\n", + " context = format_context(similars, prices)\n", + " user_prompt = (\n", + " \"Estimate the price for the following product:\\n\\n\"\n", + " + description\n", + " + \"\\n\\n\"\n", + " + context\n", + " )\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt},\n", + " {\"role\": \"assistant\", \"content\": \"Price is $\"},\n", + " ]\n", + "\n", + " docs, prices = find_similars(description(item))\n", + " messages = build_messages(description(item), docs, prices)\n", + " response = openai.chat.completions.create(\n", + " model=\"gpt-4o-mini\", messages=messages, seed=42, max_tokens=5\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return float(\n", + " re.search(r\"[-+]?\\d*\\.\\d+|\\d+\", reply.replace(\"$\", \"\").replace(\",\", \"\")).group()\n", + " or 0\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "98bf0aed", + "metadata": {}, + "source": [ + "### โœ‚๏ธ Split dataset and process" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8XQK5yrk8On4", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8XQK5yrk8On4", + "outputId": "ec379798-8b73-4e66-a517-a818845c8353" + }, + "outputs": [], + "source": [ + "print(\"Splitting entire dataset...\")\n", + "np.random.seed(42)\n", + "all_indices = list(range(len(test)))\n", + "np.random.shuffle(all_indices)\n", + "\n", + "train_split_size = int(0.8 * len(all_indices))\n", + "train_indices = all_indices[:train_split_size] # 80% of total\n", + "test_indices = all_indices[train_split_size:] # 20% of total\n", + "\n", + "train_indices = train_indices[:250] # First 250 from training split\n", + "test_indices = test_indices[:50] # First 50 from testing split" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "XN7P5fkkXfgP", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XN7P5fkkXfgP", + "outputId": "69f9d265-a402-48ab-a91e-8c6032ea4118" + }, + "outputs": [], + "source": [ + "# Process subset of TRAINING data\n", + "ft_llama_preds_train = []\n", + "gpt4omini_preds_train = []\n", + "xgboost_preds_train = []\n", + "true_prices_train = []\n", + "\n", + "for i in tqdm(train_indices):\n", + " item = test[i]\n", + " text = description(item)\n", + " true_prices_train.append(item[\"price\"])\n", + " ft_llama_preds_train.append(ft_llama_price(text))\n", + " gpt4omini_preds_train.append(gpt4o_price(item))\n", + " xgboost_preds_train.append(xgboost_price(text))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1_6_atEgHnFR", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "1_6_atEgHnFR", + "outputId": "956e4dcb-2300-44ab-a66b-9b1254216762" + }, + "outputs": [], + "source": [ + "print(\"True Prices:\", true_prices_train)\n", + "print(\"FT-LLaMA Predictions:\", ft_llama_preds_train)\n", + "print(\"GPT-4o-mini Predictions:\", gpt4omini_preds_train)\n", + "print(\"XGBoost Predictions:\", xgboost_preds_train)" + ] + }, + { + "cell_type": "markdown", + "id": "ygJsuvtLtOdR", + "metadata": { + "id": "ygJsuvtLtOdR" + }, + "source": [ + "Example :\n", + "- True Prices: [245.0, 24.99, 302.4, 737.0, ...]\n", + "- FT-LLaMA Predictions: [99.0, 53.0, 550.0, 852.0, ...]\n", + "- GPT-4o-mini Predictions: [179.99, 97.0, 348.0, 769.0, ...]\n", + "- XGBoost Predictions: [220.19, 59.85, 254.29, 335.76, 165.04, ...]" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "tYWMhTrXcA7x", + "metadata": { + "id": "tYWMhTrXcA7x" + }, + "outputs": [], + "source": [ + "# Create features for TRAINING data\n", + "maxes_train = [\n", + " max(a, b, c)\n", + " for a, b, c in zip(ft_llama_preds_train, gpt4omini_preds_train, xgboost_preds_train)\n", + "]\n", + "means_train = [\n", + " np.mean([a, b, c])\n", + " for a, b, c in zip(ft_llama_preds_train, gpt4omini_preds_train, xgboost_preds_train)\n", + "]\n", + "\n", + "# Create TRAINING dataframe\n", + "X_train = pd.DataFrame(\n", + " {\n", + " \"FT_LLaMA\": ft_llama_preds_train,\n", + " \"GPT4oMini\": gpt4omini_preds_train,\n", + " \"XGBoost\": xgboost_preds_train,\n", + " \"Max\": maxes_train,\n", + " \"Mean\": means_train,\n", + " }\n", + ")\n", + "\n", + "y_train = pd.Series(true_prices_train)" + ] + }, + { + "cell_type": "markdown", + "id": "e1682cf0", + "metadata": {}, + "source": [ + "### ๐Ÿ‹๏ธTrain the Ensemble Model" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "-WsFABEicOyo", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-WsFABEicOyo", + "outputId": "42ae6421-fb4e-4ae6-ab54-b075e311b94d" + }, + "outputs": [], + "source": [ + "np.random.seed(42)\n", + "lr = LinearRegression()\n", + "lr.fit(X_train, y_train)\n", + "\n", + "# Print feature coefficients\n", + "feature_columns = X_train.columns.tolist()\n", + "for feature, coef in zip(feature_columns, lr.coef_):\n", + " print(f\"{feature}: {coef:.2f}\")\n", + "print(f\"Intercept={lr.intercept_:.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "GnYPOslHFgGx", + "metadata": { + "id": "GnYPOslHFgGx" + }, + "source": [ + "- FT_LLaMA: 0.52\n", + "- GPT4oMini: 0.17\n", + "- XGBoost: -0.31\n", + "- Max: 0.45\n", + "- Mean: 0.13\n", + "- Intercept=-6.06\n", + "\n", + "---\n", + "FT_LLaMA is the most influential model in the ensemble.\n", + "\n", + "Max prediction also has strong positive impact.\n", + "\n", + "GPT4oMini and Mean contribute less, but still add value.\n", + "\n", + "XGBoost has a negative coefficient, acting as a counterbalance.\n", + "\n", + "\n", + "Overall: FT_LLaMA leads, max adds value, XGBoost corrects for overestimationโ€”resulting in a balanced ensemble." + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "wyx39HEL9niI", + "metadata": { + "id": "wyx39HEL9niI" + }, + "source": [ + "### ๐Ÿ”ฎ Prediction" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "W3F0nNBXlrUJ", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "W3F0nNBXlrUJ", + "outputId": "1dbd9702-50cf-4d80-b8ab-9b2000dd3b10" + }, + "outputs": [], + "source": [ + "# Process subset of TEST data\n", + "ft_llama_preds_test = []\n", + "gpt4omini_preds_test = []\n", + "xgboost_preds_test = []\n", + "true_prices_test = []\n", + "\n", + "print(\"Processing TEST data (50 items)...\")\n", + "for i in tqdm(test_indices):\n", + " item = test[i]\n", + " text = description(item)\n", + " true_prices_test.append(item[\"price\"])\n", + " ft_llama_preds_test.append(ft_llama_price(text))\n", + " gpt4omini_preds_test.append(gpt4o_price(item))\n", + " xgboost_preds_test.append(xgboost_price(text))\n", + "\n", + "# Create features for TEST data\n", + "maxes_test = [\n", + " max(a, b, c)\n", + " for a, b, c in zip(ft_llama_preds_test, gpt4omini_preds_test, xgboost_preds_test)\n", + "]\n", + "means_test = [\n", + " np.mean([a, b, c])\n", + " for a, b, c in zip(ft_llama_preds_test, gpt4omini_preds_test, xgboost_preds_test)\n", + "]\n", + "\n", + "# Create TEST dataframe\n", + "X_test = pd.DataFrame(\n", + " {\n", + " \"FT_LLaMA\": ft_llama_preds_test,\n", + " \"GPT4oMini\": gpt4omini_preds_test,\n", + " \"XGBoost\": xgboost_preds_test,\n", + " \"Max\": maxes_test,\n", + " \"Mean\": means_test,\n", + " }\n", + ")\n", + "\n", + "y_test = pd.Series(true_prices_test)" + ] + }, + { + "cell_type": "markdown", + "id": "mVn6AAGq96wm", + "metadata": { + "id": "mVn6AAGq96wm" + }, + "source": [ + "### ๐Ÿงช Evaluation" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "y25l8rR791wG", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "y25l8rR791wG", + "outputId": "0a02a620-eb0d-46a6-8f54-1046c2394ab3" + }, + "outputs": [], + "source": [ + "# Evaluate on the test set\n", + "print(\"Evaluating model...\")\n", + "y_pred = lr.predict(X_test)\n", + "r2 = r2_score(y_test, y_pred)\n", + "print(f\"Rยฒ score: {r2:.4f}\")\n", + "\n", + "# Calculate RMSE\n", + "rmse = np.sqrt(mean_squared_error(y_test, y_pred))\n", + "print(f\"RMSE: {rmse:.2f}\")\n", + "\n", + "# Calculate MAPE\n", + "mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100\n", + "print(f\"MAPE: {mape:.2f}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "vHJLe6LNEBrB", + "metadata": { + "id": "vHJLe6LNEBrB" + }, + "source": [ + "Evaluating model...\n", + "- Rยฒ score: 0.7376\n", + "- RMSE: 127.62\n", + "- MAPE: 29.70%\n", + "\n", + "---\n", + "\n", + "- Rยฒ = 0.74: This is a solid Rยฒ value, indicating our model explains about 74% of the variance in the price data\n", + "Generally, an Rยฒ above 0.7 is considered good for price prediction tasks\n", + "- RMSE = 127.6: Average error; good if prices are in the thousands.\n", + "- MAPE = 29.7%: This means our predictions are off by roughly 30% on average. Typical for price prediction, but thereโ€™s room for improvement.\n" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "C6cEJ57WApkG", + "metadata": { + "id": "C6cEJ57WApkG" + }, + "source": [ + "### ๐Ÿš€ Push to HF" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "_n7n_MnscS4r", + "metadata": { + "id": "_n7n_MnscS4r" + }, + "outputs": [], + "source": [ + "# Serialize Ensemble model locally for Hugging Face upload\n", + "\n", + "MODEL_DIR = os.path.join(ROOT, \"models\")\n", + "MODEL_FILENAME = \"ensemble_model.pkl\"\n", + "LOCAL_MODEL = os.path.join(MODEL_DIR, MODEL_FILENAME)\n", + "\n", + "os.makedirs(MODEL_DIR, exist_ok=True)\n", + "joblib.dump(lr, LOCAL_MODEL)\n", + "\n", + "# Create the model repo if it doesn't exist\n", + "api.create_repo(repo_id=REPO_ID, repo_type=\"model\", private=True, exist_ok=True)\n", + "\n", + "# Upload the saved model\n", + "api.upload_file(\n", + " path_or_fileobj=LOCAL_MODEL,\n", + " path_in_repo=MODEL_FILENAME,\n", + " repo_id=REPO_ID,\n", + " repo_type=\"model\",\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/10_part2_modal.ipynb b/week8/community_contributions/lisekarimi/10_part2_modal.ipynb new file mode 100644 index 0000000..f1525d7 --- /dev/null +++ b/week8/community_contributions/lisekarimi/10_part2_modal.ipynb @@ -0,0 +1,387 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44c6af6b-6fc3-44d5-a586-71618af7d09a", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "# Modal (Part 2)\n", + "\n", + "---\n", + "โœ… With all models and ChromaDB set up, it's time to integrate everything into a real system: **Snapr** โ€” an app that scans online product listings, predicts their value, and alerts users to great deals.\n", + "\n", + "To power SSnapr, weโ€™ll need:\n", + "- Price prediction models โ€” ready for production \n", + "- Fast, on-demand predictions \n", + "- A scalable setup that handles real-world usage\n", + "\n", + "๐Ÿ”ง Thatโ€™s where **Modal** comes in. Modal lets us deploy models and services to the cloud, with minimal setup, low latency, and clean Python APIs.\n", + "\n", + "- You can check out a [live demo](https://huggingface.co/spaces/lisekarimi/snapr) of the project\n", + "- The source code is available on [GitHub](https://github.com/lisekarimi/snapr)\n", + "\n", + "---\n", + "๐Ÿ“ข Find more LLM notebooks on my [GitHub repository](https://github.com/lisekarimi/lexo)\n" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "b8c175e7-ca0a-4664-bded-08ec131c5636", + "metadata": {}, + "source": [ + "## ๐Ÿ“š Pre-requisites\n", + "\n", + "To follow this project smoothly, it's helpful to know:\n", + "\n", + "- ๐Ÿ›ฐ๏ธ What an API is: You send a request โ†’ itโ€™s processed remotely โ†’ you receive a result\n", + "- ๐Ÿณ What a Docker image & container are:\n", + " - Image = environment with code & dependencies\n", + " - Container = running instance of that image\n", + "- ๐Ÿง‘โ€๐Ÿ’ป Local vs Remote code execution:\n", + " - Local code runs on your machine\n", + " - Remote code runs in the cloud (via Modal" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "440fffc2-9ec1-433d-9b71-e6fae3b46415", + "metadata": {}, + "source": [ + "## ๐Ÿ”ง Install & Setup Modal\n", + "- Before starting, install Modal in your environment (Run this once): `uv pip install modal`\n", + "- Create an account at modal.com (they give you $5 free to start).\n", + "- Then authenticate your environment: `modal setup`" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef286205", + "metadata": {}, + "outputs": [], + "source": [ + "!uv pip install modal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3906c01-b313-4dac-9a2e-6c7dbfdcc8fd", + "metadata": {}, + "outputs": [], + "source": [ + "import modal\n", + "import sys\n", + "sys.path.append(\".\") # Make sure your local modules are accessible" + ] + }, + { + "cell_type": "markdown", + "id": "43c59002-afe6-4dcc-a53e-b50d85857f7d", + "metadata": {}, + "source": [ + "## ๐Ÿง  Key Concepts\n", + "\n", + "Modal is a platform that lets you run Python code in the cloud. You can:\n", + "- Deploy code as APIs\n", + "- Run GPU workloads (e.g., LLMs)\n", + "- Automatically handle Docker, infra, deployment\n", + "\n", + "What is a Modal App?\n", + "An \"App\" is a containerized cloud service where you can run code remotely.\n", + "- Code runs in isolated containers (like Docker)\n", + "- These containers are created on-demand and destroyed when idle\n", + "- You define your logic in a file and deploy it to Modal\n", + "\n", + "Key Modal Concepts\n", + "- `modal.Image`: Defines the environment (like a Docker image)\n", + "- `@app.cls`: Runs classes remotely inside a container\n", + "- `modal.App`: Defines and registers the Modal app\n", + "- `.remote()`: Sends request to Modal API to execute the code remotely\n", + "- `modal deploy -m`: Deploys app permanently like a real cloud service" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "79436b01-9623-4b0e-8ffc-0ea51a5783ac", + "metadata": {}, + "source": [ + "## โš™๏ธ Minimal Example" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d62850fc-dbf4-48b0-a2f2-a1e9a200414d", + "metadata": {}, + "outputs": [], + "source": [ + "from modal_services.get_started import app, f\n", + "\n", + "with app.run(): # This spins up a container in Modal\n", + " print(f.local(1000)) # Run locally inside the notebook\n", + " print('*' * 5)\n", + " print(f.remote(1000)) # Run remotely via Modal API inside a container" + ] + }, + { + "attachments": { + "886d059a-a8ca-4552-86d2-fb87fb824441.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "d07d54f3-dfb9-46b2-be50-d6d35f81b3c0", + "metadata": {}, + "source": [ + "๐Ÿ”„ What Happens When You Call .remote()?\n", + "\n", + "some_function.remote() โ†’ Modal SDK sends API request\n", + " โ†’ Spins up a container\n", + " โ†’ Runs the code remotely\n", + " โ†’ Sends the result back to your local machine\n", + "\n", + "![image.png](attachment:886d059a-a8ca-4552-86d2-fb87fb824441.png)\n", + "\n", + "What we have here is an **ephemeral app**: the container shuts down after finishing.\n", + "\n", + "For our project, we need a persistently running app that behaves like a production API. To achieve that, we should use `modal deploy -m`, making the app suitable for serving AI services reliably." + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "9422c5b0-573e-43a4-99b5-2c6199770f5c", + "metadata": {}, + "source": [ + "## ๐Ÿ“ฆ Persistent Deployment with `modal deploy`" + ], + "outputs": [] + }, + { + "attachments": { + "b84a3557-9805-462f-a1d5-008b3aa4f4f5.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "8edb6eb4-4489-4823-9d16-a01c8d0355b6", + "metadata": {}, + "source": [ + "Click the blue \"+\" button at the top left of JupyterLab, then choose \"Terminal\" to open a new terminal tab.\n", + "\n", + "There, you can run:\n", + "\n", + "```bash\n", + "conda activate llms\n", + "modal deploy -m modal_services.get_started\n", + "```\n", + "\n", + "This builds and deploys the app (`example-hello-world`), registers `f()`, and makes it callable via `.remote()` anytime โ€” even outside the notebook.\n", + "\n", + "![image.png](attachment:b84a3557-9805-462f-a1d5-008b3aa4f4f5.png)" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656456f5-b3f9-40bf-8a2e-169da0a68fe8", + "metadata": {}, + "outputs": [], + "source": [ + "from modal_services.get_started import f\n", + "f = modal.Function.from_name(\"example-hello-world\", \"f\") # (app_name, function_name)\n", + "print(f.remote(20))" + ] + }, + { + "attachments": { + "b950fed1-8806-424c-830a-d8b99927801e.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "7d2c4b43-97a7-4a8e-a0df-1e20de053d5a", + "metadata": {}, + "source": [ + "## ๐Ÿš€ Deploy Our first Modal-powered model\n", + "\n", + "So far, weโ€™ve seen how to run simple remote functions using `@app.function()` and call them via `modal.Function.from_name(...)` in a **persistent app** โ€” good for basic tasks.\n", + "\n", + "But in our Smart Deal Finder project, we need more:\n", + "- Load and reuse a large model (like LLaMA) \n", + "- Keep the model in memory \n", + "- Expose one or more methods (like `price()`)\n", + "\n", + "Thatโ€™s why we use `@app.cls` โ€” it lets us define a class (e.g. `Pricer`) that lives in a Modal container, loads the model once in `setup()`, and handles remote requests efficiently.\n", + "\n", + "Full code : `\\modal_services.ft_pricer.py`\n", + "\n", + "---\n", + "\n", + "\n", + "๐Ÿš€ In this step, weโ€™ll deploy a class-based app using `modal.Cls.from_name`.\n", + "\n", + "Specifically, weโ€™ll deploy `Pricer`, which loads our 4-bit quantized fine-tuned LLaMA model (trained in Notebook 9), and exposes a remote `.price()` method to estimate item prices.\n", + "\n", + "โš ๏ธ Before deploying, add your HF_TOKEN in Modal\n", + "\n", + "Then open a terminal and run:\n", + "\n", + "```bash\n", + "modal deploy -m modal_services.ft_pricer\n", + "```\n", + "\n", + "This will:\n", + "- Build the image with your code and dependencies\n", + "- Deploy the app `llm-ft-pricer` and register the `Pricer` class and its methods\n", + "- Not start any container yet โ€” setup() isn't run and the model isnโ€™t loaded\n", + "- Prepare the app to handle `.remote()` calls when they come in\n", + "\n", + "![image.png](attachment:b950fed1-8806-424c-830a-d8b99927801e.png)" + ], + "outputs": [] + }, + { + "attachments": { + "1c697283-e5e2-4b09-b1f1-d1c11f18c8e4.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABdoAAACPCAIAAABia/6KAAAgAElEQVR4Ae2d/1MUR/7/84u/3A9XdRWNU1Z55i2TEFGOvI2Ib08wmAu66xcERQwSAYEIbxTxwDeuhG8G0CxKsouCgmTRFddvKNHEaDAav6GRLycLJ9RVpSqfSlmVqlT5L9znumemZ2Z3ZlhgF4F9blE60zP96p7HdPd0P+fVPa+99tprHH4gAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAITReA1yDEThRrpgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAhADkG5QAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEJpQA5JgJxY3EQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAByDMoACIAACIAACIAACIAACIAACIAACIAACEwoAcgxE4obiYEACIAACIAACIAACIAACIAACIAACIAA5BiUARAAARAAARAAARAAARAAARAAARAAARCYUAKQYyYUNxIDARAAARAAARAAARAAARAAARAAARAAAcgxKAMgAAIgAAIgAAIgAAIgAAIgAAIgAAIgMKEEIMdMKG4kBgIgAAIgAAIgAAIgAAIgAAIgAAIgAAKQY1AGQAAEQAAEQAAEQAAEQAAEQAAEQAAEQGBCCUCOmVDcSAwEQAAEQAAEQAAEQAAEQAAEQAAEQAAEIMegDIAACIAACIAACIAACIAACIAACIAACIDAhBKAHDOhuJEYCIAACIAACIAACIAACIAACIAACIAACECOQRkAARAAARAAARAAARAAARAAARAAARAAgQklADlmQnEjMRAAARAAARAAARAAARAAARAAARAAARCAHIMyAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAITSkBXjpk7d+4777wTERHxLn4gAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAL+I6Atx8ydO3f58uXh4eFvvfUWjx8IgAAIgAAIgAAIgAAIgAAIgAAIgAAIgID/CGjLMe+88054eLj/UoElEAABEAABEAABEAABEAABEAABEAABEAABkYC2HBMREQG/GJQREAABEAABEAABEAABEAABEAABEAABEAgEAW055t133w1EYrAJAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAOQZlAARAAARAAARAAARAAARAAARAAARAAAQmlADkmAnFjcRAAARA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QAAEQAAEQAAEQAAEQECLgK4c8y5+IAACIAACIAACIAACIAACIAACIAACIAACASCgK8dw+IEACIAACIAACIAACIAACIAACIAACIAACASAAOSYAECFSRAAARAAARAAARAAARAAARAAARAAARDQJwA5Rp8NjoAACIAACIAACIAACIAACIAACIAACIBAAAhAjgkAVJgEARAAARAAARAAARAAARAAARAAARAAAX0CkGP02eAICIAACIAACIAACIAACIAACIAACIAACASAAOSYAECFSRAAARAAARAAARAAARAAARAAARAAARDQJwA5Rp8NjoAACIAACIAACIAACIAACIAACIDAWAnMmTtnXujckIg3314yPzQqBH+MwNtL5odEvDkvdO6cuXPGSnfKx4McM+VvIS4ABEAABEAABEAABEAABEAABEBgUhGYM3fO/EXzmPqADQMC8xfNC05RBnLMpKqzyAwIgAAIgAAIgAAIgAAIgAAIgMDUJvBnfq6B+oBDmgT+zM+d2nd99LmHHDN6ZogBAiAAAiAAAiAAAiAAAiAAAiAAAloE5oVCixnjtKx5ocGlyECO0apACAMBEAABEAABEAABEAABEAABEACBURKAX4ym54vvgUHlIwM5ZpTVC6eDAAiAAAiAAAiAAAiAAAiAAAiAgBeBOXPnjKg7RGe9Z66INldER2e9N+LJwXlC8Kwj43c5hl+6JrOwal/Gmijeq3QiQI8Av/nQiSO5q/QOI9zfBMJizKa10WH+M6tvMGxFyp7yokxTpK8Vgo80ZxQdKtxhXuodIzxa75BeLD7SbFqr+POpYhrWYj6KGBzpcsJi9NoBQ+PyHaHcyvakxGjcJX3jcnxsgUBgCfhWEUaRB32DtHZX7k7xvckyqD780s17yrVrlk4sIWOKZkSjafK+zvDolLzK8rzUFeHexzj9BlNxsn5zxxkaZyb0WkV6Ar90jdnkU3vI7GEDBPxKwKCE+zUdGJsaBPQfAYr8j9SAj6a3qTCLTT8TMF6711wRXTSUWvIiI+9hUt7DpJIXGUVDqeaKaF80l7CiiNhLy2KqF4QljHEekC+pjPmcsKIFEflvjSb6W+EHFkR8on0t8xfN8/ONmazm/CrH8Ftqv3EPDg73ddN/vzm62Xs8OVlBvNp8FZ53Dw5eL3+1mQii1HOdT4YHB7tOJPjlmsN2Hr/bNziscQdj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" + }, + "4a22e438-6b25-4c69-9439-99d146ffd188.png": { + "image/png": "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pH06nREREWlpaU1NtDTr5MmTpaWl8ldvsrKympubhUrV3Ny8bRvtCY9fAASCK5QEkAEEURGIWbJqbaJ9kG+xqfzrDin4qiWy0Z18ldztuk/QyRdx4CeB2BWJgsDvv5YXu0I0V/EzCdqzzbACLLILZi/+xgN/IAACoSdADdywwaqTNmvUaj9Gx9Efrko07LfRGxjhghsIjEkCrH8Y9Pk+5LGEKQv5fVus3XC46BNw1qxZ5eXlHo8nJyfnv9uR/Hfr1vDw8EWLFuXk5Hg8nvLy8lmzZvkEgsMIETCrGOS+IgijfnoMMZvrESrPREpmyZIla9euXbLEbN0bsZgzZ05CQoK1H2tmy5cvX7VqFXYnsaZkfRVCiTUfXAUBPQH/hRJ9SJyDAAiAAAiAAAiAwMshIAslQ0s+NTW1rq5ObfJQV1cnvwYfWlzwDQIgAAJjhwCEkrFzr5DT0UFgx353XSl9uxk/EAABEAABEAABEBgjBHbUHHfX7AwwswsWLFjJfgsWLAgwCgQDARAAgTFFAELJmLpdyCwIgAAIgAAIgAAIgAAIgAAIgAAIgEAoCUAoCSVdxA0CIAACIAACIAACIAACIAACIAACIDCmCEAoGVO3C5kFARAAARAAARAAARAAARAAARAAARAIJQEIJaGki7hBAARAAARAAARAAARAAARAAARAAATGFAEIJWPqdiGzIAACIAACIAACIAACIAACIAACIAACoSQAoSSUdBE3CIAACIAACIAACIAACIAACIAACIDAmCIAoWRM3S5kFgRAAARAAARAAARAAARAAARAAARAIJQEIJSEki7iBgEQAAEQAAEQAAEQAAEQAAEQAAEQGFMEIJSMqduFzIIACIAACIAACIAACIAACIAACIAACISSAISSUNJF3CAAAiAAAiAAAiAAAiAAAiAAAiAAAmOKgJVQ8g5+IAACIAACIAACIAACIAACIAACIAACIDCRCFgJJRx+IAACIAACIAACIAACIAACIAACIAACIDCRCEAomUh3G2UFARAAARAAARAAARAAARAAARAAARCwJAChxBIPLoIACIAACIAACIAACIAACIAACIAACEwkAhBKJtLdRllBAARAAARAAARAAARAAARAAARAAAQsCUAoscSDiyAAAiAAAiAAAiAAAiAAAiAAAiAAAhOJAISSiXS3UVYQAAEQAAEQAAEQAAEQ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" + } + }, + "cell_type": "markdown", + "id": "afff309a-740b-443f-96f1-4b20618ada8b", + "metadata": {}, + "source": [ + "## ๐Ÿ”— Connect to Our Deployed App\n", + "\n", + "Now that our app is deployed, we can connect to it and use it like a remote service.\n", + "\n", + "We'll do this using `modal.Cls.from_name(\"llm-ft-pricer\", \"Pricer\")`, which fetches the `Pricer` class from our deployed app via the Modal API.\n", + "\n", + "Then, calling `.price.remote(...)` sends a request to Modal, spins up a container if needed, loads the model, runs the method, and returns the result.\n", + "\n", + "This is how we turn our model into a cloud API.\n", + "\n", + "What happens under the hood when calling price.remote(...): \n", + "- First run = downloads model files โ†’ stores in volume (/cache) โ†’ loads into memory โ†’ runs \n", + "- Later runs = load from volume โ†’ memory โ†’ run (no re-download)\n", + "\n", + "---\n", + "\n", + "Since we added `min_containers=1`, a container is created and kept warm as soon as the app is deployed. Models remain loaded in memory, so there are no cold starts โ€” unless the app is stopped or the container crashes. \n", + "\n", + "![image.png](attachment:1c697283-e5e2-4b09-b1f1-d1c11f18c8e4.png)\n", + "\n", + "โš ๏ธ However, this **continuously consumes credits** if you forget to stop the container or app manually.\n", + "\n", + "To save credits, you can set `min_containers=0` and `scaledown_window=300` โ€” this way, no container stays warm by default, and a new one will spin up only when `.remote()` is called (i.e., on cold start).\n", + "\n", + "![image.png](attachment:4a22e438-6b25-4c69-9439-99d146ffd188.png)\n" + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b478ae8-f636-4ac7-bf53-0f3b23c21a72", + "metadata": {}, + "outputs": [], + "source": [ + "Pricer = modal.Cls.from_name(\"llm-ft-pricer\", \"Pricer\")\n", + "pricer = Pricer()\n", + "reply = pricer.price.remote(\"SEVERIN 28L Microwave, 900W, 5 power levels, 35-min timer, turntable (31.5 cm), Silver, MW 7772\")\n", + "print(reply)" + ] + }, + { + "cell_type": "markdown", + "id": "2e63efbf-344b-4b5f-8a0d-27b6e41f8508", + "metadata": {}, + "source": [ + "Now that weโ€™ve deployed our model and learned how to call it remotely with `.remote()`,\n", + "letโ€™s go one step further โ€” wrap this logic inside a local Python class.\n", + "\n", + "In the next step, we'll build a local Agent that cleanly interacts with our deployed `Modal app`, using the same `Modal API` under the hood." + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "8fbc7696-e892-4f08-80c5-5199b03ed175", + "metadata": {}, + "source": [ + "## ๐Ÿ”Œ Connect to Your Modal App with a Local Agent\n", + "\n", + "`ft_pricer.py` is now a deployed API on Modal. \n", + "\n", + "To use it locally, weโ€™ll wrap it in a class called `FTPriceAgent` (Full code: `\\agents\\ft_price_agent.py)` that:\n", + "\n", + "- Connects to the remote app via `modal.Cls.from_name(...)` \n", + "- Calls `.price.remote(...)` to run predictions \n", + "\n", + "๐Ÿ”„ **Two API Calls:** happen\n", + "1. `modal.Cls.from_name(...)` โ†’ fetches the deployed class \n", + "2. `.price.remote(...)` โ†’ runs the remote method on Modal \n", + "\n", + "This keeps our code clean and modular." + ], + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b80cd15a-e419-4c21-97e7-4a56ed4db680", + "metadata": {}, + "outputs": [], + "source": [ + "from agents.ft_price_agent import FTPriceAgent\n", + "\n", + "agent = FTPriceAgent()\n", + "agent.price(\"Apple AirPods Max wireless over-ear headphones with active noise cancellation and spatial audio\")" + ] + }, + { + "cell_type": "markdown", + "id": "65522b93-59c9-4d15-a12d-58e078b88545", + "metadata": {}, + "source": [ + "Now that weโ€™ve seen how Modal agents work โ€” connecting to remote services and running `.remote()` โ€” weโ€™ll use the same pattern for the rest of our models.\n", + "\n", + "โœ… For each model โ€” **XGBoost**, **GPT-4o RAG**, and the **Ensemble** โ€” weโ€™ll build a dedicated Agent. " + ], + "outputs": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/agents/__init__.py b/week8/community_contributions/lisekarimi/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/week8/community_contributions/lisekarimi/agents/base_agent.py b/week8/community_contributions/lisekarimi/agents/base_agent.py new file mode 100644 index 0000000..fe09e18 --- /dev/null +++ b/week8/community_contributions/lisekarimi/agents/base_agent.py @@ -0,0 +1,33 @@ +import logging + +class Agent: + """ + An abstract superclass for Agents + Used to log messages in a way that can identify each Agent + """ + + # Foreground colors + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + + # Background color + BG_BLACK = '\033[40m' + + # Reset code to return to default color + RESET = '\033[0m' + + name: str = "" + color: str = '\033[37m' + + def log(self, message): + """ + Log this as an info message, identifying the agent + """ + color_code = self.BG_BLACK + self.color + message = f"[{self.name}] {message}" + logging.info(color_code + message + self.RESET) \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/agents/ft_price_agent.py b/week8/community_contributions/lisekarimi/agents/ft_price_agent.py new file mode 100644 index 0000000..465f1bb --- /dev/null +++ b/week8/community_contributions/lisekarimi/agents/ft_price_agent.py @@ -0,0 +1,29 @@ +import modal +from agents.base_agent import Agent + + +class FTPriceAgent(Agent): + """ + An Agent that runs the fine-tuned LLM that's running remotely on Modal + """ + + name = "FTPrice Agent" + color = Agent.RED + + def __init__(self): + """ + Set up this Agent by creating an instance of the modal class + """ + self.log("FTPrice Agent is initializing - connecting to modal") + Pricer = modal.Cls.from_name("llm-ft-pricer", "Pricer") # 1st API call: to fetch Pricer (remote class) + self.pricer = Pricer() + self.log("FTPrice Agent is ready") + + def price(self, description: str) -> float: + """ + Make a remote call to return the estimate of the price of this item + """ + self.log("FTPrice Agent is calling remote fine-tuned model") + result = self.pricer.price.remote(description) # 2nd API call: to run the price method in the remote Pricer class + self.log(f"FTPrice Agent completed - predicting ${result:.2f}") + return result \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/helpers/__init__.py b/week8/community_contributions/lisekarimi/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/week8/community_contributions/lisekarimi/helpers/items.py b/week8/community_contributions/lisekarimi/helpers/items.py new file mode 100644 index 0000000..a594e27 --- /dev/null +++ b/week8/community_contributions/lisekarimi/helpers/items.py @@ -0,0 +1,120 @@ +from typing import Optional # A variable might be a certain type or None +from transformers import AutoTokenizer +import re + +BASE_MODEL = "meta-llama/Meta-Llama-3.1-8B" + +MIN_TOKENS = 150 # Minimum tokens required to accept an item +MAX_TOKENS = 160 # We limit to 160 tokens so that after adding prompt text, the total stays around 180 tokens. + +MIN_CHARS = 300 # Reject items with less than 300 characters +CEILING_CHARS = MAX_TOKENS * 7 # Truncate long text to about 1120 characters (approx 160 tokens) + +class Item: + """ + An Item is a cleaned, curated datapoint of a Product with a Price + """ + + # Load tokenizer for the model + tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True) + + # Define PRICE_LABEL and question for the training prompt + PRICE_LABEL = "Price is $" + QUESTION = "How much does this cost to the nearest dollar?" + + # A list of useless phrases to remove to reduce noise for price prediction + REMOVALS = ['"Batteries Included?": "No"', '"Batteries Included?": "Yes"', '"Batteries Required?": "No"', '"Batteries Required?": "Yes"', "By Manufacturer", "Item", "Date First", "Package", ":", "Number of", "Best Sellers", "Number", "Product "] + + # Attributes for each item + title: str + price: float + category: str + token_count: int = 0 # How many tokens in the final prompt + + # Optional fields + details: Optional[str] # The value can be a string or can be None + prompt: Optional[str] = None + include = False # Whether to keep the item or not + + def __init__(self, data, price): + self.title = data['title'] + self.price = price + self.parse(data) + + def scrub_details(self): + """ + Removes useless phrases from details, which often has repeated specs or boilerplate text. + """ + details = self.details + for remove in self.REMOVALS: + details = details.replace(remove, "") + return details + + def scrub(self, stuff): + """ + Clean up the provided text by removing unnecessary characters and whitespace + Also remove words that are 7+ chars and contain numbers, as these are likely irrelevant product numbers + """ + stuff = re.sub(r'[:\[\]"{}ใ€ใ€‘\s]+', ' ', stuff).strip() + stuff = stuff.replace(" ,", ",").replace(",,,",",").replace(",,",",") + words = stuff.split(' ') + select = [word for word in words if len(word)<7 or not any(char.isdigit() for char in word)] + return " ".join(select) + + def parse(self, data): + """ + Prepares the text, checks length, tokenizes it, and sets include = True if itโ€™s valid. + """ + # Builds a full contents string by combining description, features, and cleaned details. + contents = '\n'.join(data['description']) + if contents: + contents += '\n' + features = '\n'.join(data['features']) + if features: + contents += features + '\n' + self.details = data['details'] + if self.details: + contents += self.scrub_details() + '\n' + + # If content is long enough, trim it to max char limit before processing. + if len(contents) > MIN_CHARS: + contents = contents[:CEILING_CHARS] + + # Clean and tokenize text, then check token count. + text = f"{self.scrub(self.title)}\n{self.scrub(contents)}" + tokens = self.tokenizer.encode(text, add_special_tokens=False) + + if len(tokens) > MIN_TOKENS: + # Truncate tokens, decode them back and create the training prompt + tokens = tokens[:MAX_TOKENS] + text = self.tokenizer.decode(tokens) + self.make_prompt(text) + + # Mark the item as valid and ready to be used in training + self.include = True # Only items with MIN_TOKENS <= tokens <= MAX_TOKENS are kept + + + def make_prompt(self, text): + """ + Builds the training prompt using the question, text, and price. Then counts the tokens. + """ + self.prompt = f"{self.QUESTION}\n\n{text}\n\n" + self.prompt += f"{self.PRICE_LABEL }{str(round(self.price))}.00" + self.token_count = len(self.tokenizer.encode(self.prompt, add_special_tokens=False)) + + def test_prompt(self): + """ + Returns the prompt without the actual price, useful for testing/inference. + """ + return self.prompt.split(self.PRICE_LABEL )[0] + self.PRICE_LABEL + + def __repr__(self): + """ + Defines how the Item object looks when printed โ€” it shows the title and price. + """ + return f"<{self.title} = ${self.price}>" + + + + + \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/helpers/loaders.py b/week8/community_contributions/lisekarimi/helpers/loaders.py new file mode 100644 index 0000000..4314c65 --- /dev/null +++ b/week8/community_contributions/lisekarimi/helpers/loaders.py @@ -0,0 +1,106 @@ +from datetime import datetime # Measure how long loading takes +from tqdm import tqdm # Shows a progress bar while processing data +from datasets import load_dataset # Load a dataset from Hugging Face Hub +from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor # For parallel processing (speed) +from items import Item + +CHUNK_SIZE = 1000 # Process the dataset in chunks of 1000 datapoints at a time (for efficiency) +MIN_PRICE = 0.5 +MAX_PRICE = 999.49 +WORKER = 4 # Set the number of workers here + +class ItemLoader: + + def __init__(self, name): + """ + Initialize the loader with a dataset name. + """ + self.name = name # Store the category name + self.dataset = None #Placeholder for the dataset (we load it later in load()) + + def process_chunk(self, chunk): + """ + Convert a chunk of datapoints into valid Item objects. + """ + batch = [] # Initialize the list to hold valid items + + # Loop through each datapoint in the chunk + for datapoint in chunk: + try: + # Extract price from datapoint + price_str = datapoint['price'] + if price_str: + price = float(price_str) + + # Check if price is within valid range + if MIN_PRICE <= price <= MAX_PRICE: + item = Item(datapoint, price) + + # Keep only valid items + if item.include: + batch.append(item) + except ValueError: + continue # Skip datapoints with invalid price format + return batch # Return the list of valid items + + + def load_in_parallel(self, workers): + """ + Split the dataset into chunks and process them in parallel. + """ + results = [] + size = len(self.dataset) + chunk_count = (size // CHUNK_SIZE) + 1 + + # Build chunks directly here (no separate function) + chunks = [ + self.dataset.select(range(i, min(i + CHUNK_SIZE, size))) + for i in range(0, size, CHUNK_SIZE) + ] + + # Process chunks in parallel using multiple CPU cores + with ProcessPoolExecutor(max_workers=workers) as pool: + for batch in tqdm(pool.map(self.process_chunk, chunks), total=chunk_count): + results.extend(batch) + + # Add the category name to each result + for result in results: + result.category = self.name + + return results + + + def load(self, workers=WORKER): + """ + Load and process the dataset, returning valid items. + """ + # Record start time + start = datetime.now() + + # Print loading message + print(f"Loading dataset {self.name}", flush=True) + + # Load dataset from Hugging Face (based on category name) + self.dataset = load_dataset( + "McAuley-Lab/Amazon-Reviews-2023", + f"raw_meta_{self.name}", + split="full", + trust_remote_code=True + ) + + # Process the dataset in parallel and collect valid items + results = self.load_in_parallel(workers) + + # Record end time and print summary + finish = datetime.now() + print( + f"Completed {self.name} with {len(results):,} datapoints in {(finish-start).total_seconds()/60:.1f} mins", + flush=True + ) + + # Return the list of valid items + return results + + + + \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/helpers/testing.py b/week8/community_contributions/lisekarimi/helpers/testing.py new file mode 100644 index 0000000..9422182 --- /dev/null +++ b/week8/community_contributions/lisekarimi/helpers/testing.py @@ -0,0 +1,84 @@ +import math +import matplotlib.pyplot as plt + +GREEN = "\033[92m" +YELLOW = "\033[93m" +RED = "\033[91m" +RESET = "\033[0m" +COLOR_MAP = {"red":RED, "orange": YELLOW, "green": GREEN} + +class Tester: + + def __init__(self, predictor, data, title=None, size=250): + self.predictor = predictor + self.data = data + self.title = title or predictor.__name__.replace("_", " ").title() + self.size = size + self.guesses = [] + self.truths = [] + self.errors = [] + self.sles = [] + self.colors = [] + + def color_for(self, error, truth): + if error<40 or error/truth < 0.2: + return "green" + elif error<80 or error/truth < 0.4: + return "orange" + else: + return "red" + + def run_datapoint(self, i): + datapoint = self.data[i] + guess = self.predictor(datapoint) + truth = datapoint["price"] + error = abs(guess - truth) + log_error = math.log(truth+1) - math.log(guess+1) + sle = log_error ** 2 + color = self.color_for(error, truth) + title = datapoint["text"][:40] + "..." if len(datapoint["text"]) > 40 else datapoint["text"] + self.guesses.append(guess) + self.truths.append(truth) + self.errors.append(error) + self.sles.append(sle) + self.colors.append(color) + # print(f"{COLOR_MAP[color]}{i+1}: Guess: ${guess:,.2f} Truth: ${truth:,.2f} Error: ${error:,.2f} SLE: {sle:,.2f} Item: {title}{RESET}") + + def chart(self, title): + max_error = max(self.errors) + plt.figure(figsize=(15, 6)) + max_val = max(max(self.truths), max(self.guesses)) + plt.plot([0, max_val], [0, max_val], color='deepskyblue', lw=2, alpha=0.6) + plt.scatter(self.truths, self.guesses, s=3, c=self.colors) + plt.xlabel('Ground Truth') + plt.ylabel('Model Estimate') + plt.xlim(0, max_val) + plt.ylim(0, max_val) + plt.title(title) + + # Add color legend + from matplotlib.lines import Line2D + legend_elements = [ + Line2D([0], [0], marker='o', color='w', label='Accurate (green)', markerfacecolor='green', markersize=8), + Line2D([0], [0], marker='o', color='w', label='Medium error (orange)', markerfacecolor='orange', markersize=8), + Line2D([0], [0], marker='o', color='w', label='High error (red)', markerfacecolor='red', markersize=8) + ] + plt.legend(handles=legend_elements, loc='upper left') + plt.show() + + def report(self): + average_error = sum(self.errors) / self.size + rmsle = math.sqrt(sum(self.sles) / self.size) + hits = sum(1 for color in self.colors if color=="green") + title = f"{self.title} Error=${average_error:,.2f} RMSLE={rmsle:,.2f} Hits={hits/self.size*100:.1f}%" + self.chart(title) + + def run(self): + self.error = 0 + for i in range(self.size): + self.run_datapoint(i) + self.report() + + @classmethod + def test(cls, function, data): + cls(function, data).run() \ No newline at end of file diff --git a/week8/community_contributions/lisekarimi/modal_services/__init__.py b/week8/community_contributions/lisekarimi/modal_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/week8/community_contributions/lisekarimi/modal_services/ft_pricer.py b/week8/community_contributions/lisekarimi/modal_services/ft_pricer.py new file mode 100644 index 0000000..974aeb8 --- /dev/null +++ b/week8/community_contributions/lisekarimi/modal_services/ft_pricer.py @@ -0,0 +1,140 @@ +import modal +from modal import App, Volume, Image + +import logging +logging.basicConfig(level=logging.INFO) + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Constants +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +GPU = "T4" # Use a T4 GPU for inference +CACHE_PATH = "/cache" # Mount point for the Modal volume + +# Hugging Face model references +BASE_MODEL = "meta-llama/Meta-Llama-3.1-8B" +FINETUNED_MODEL = "ed-donner/pricer-2024-09-13_13.04.39" +REVISION = "e8d637df551603dc86cd7a1598a8f44af4d7ae36" # Commit of the fine-tuned model + +# Local cache paths (inside the volume) +BASE_MODEL_DIR = f"{CACHE_PATH}/llama_base_model" +FINETUNED_MODEL_DIR = f"{CACHE_PATH}/llama_finetuned_model" + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Structure +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Container (App: llm-ft-pricer) +# โ”œโ”€โ”€ /app โ† Code + installed Python packages (from image) +# โ”œโ”€โ”€ /cache โ† Mounted Modal volume (`hf-hub-cache`) +# โ”‚ โ””โ”€โ”€ meta-llama/Meta-Llama-3.1-8B/... โ† HuggingFace model files downloaded via snapshot_download + + + +QUESTION = "How much does this cost to the nearest dollar?" +PREFIX = "Price is $" # Used to parse generated output + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Modal App, Image, Volume, Secrets +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +app = modal.App("llm-ft-pricer") # Define the Modal app + +image = ( + Image.debian_slim() + .pip_install("huggingface", "torch", "transformers", "bitsandbytes", "accelerate", "peft") # All needed libraries + .env({"HF_HUB_CACHE": CACHE_PATH}) # Hugging Face will store model files in /cache +) + +cache_vol = modal.Volume.from_name("hf-hub-cache", create_if_missing=True) # Persisted volume for caching models +secrets = [modal.Secret.from_name("HF_TOKEN")] # Hugging Face auth token + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Modal Class: Pricer +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# All methods in this class run inside the container with the image, volume, secrets, and GPU you configured. +@app.cls( + image=image, + secrets=secrets, + volumes={CACHE_PATH: cache_vol}, # Mount volume into /cache + gpu=GPU, + timeout=1800, # 30-minute max runtime + min_containers=0, # = 1 : Keeping one container warm uses credits continuously if you forget to stop it. + scaledown_window=300, # Shuts down the container +) +class Pricer: + @modal.enter() + def setup(self): + import os, torch + import logging + from huggingface_hub import snapshot_download + from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig + from peft import PeftModel + + # Create cache path if it doesn't exist + os.makedirs(CACHE_PATH, exist_ok=True) + + # Download base and fine-tuned models into volume + logging.info("Downloading base model...") + snapshot_download(BASE_MODEL, local_dir=BASE_MODEL_DIR) + + logging.info("Downloading fine-tuned model...") + snapshot_download(FINETUNED_MODEL, revision=REVISION, local_dir=FINETUNED_MODEL_DIR) + + # Quantization config (4-bit) + quant_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_use_double_quant=True, + bnb_4bit_compute_dtype=torch.bfloat16, + bnb_4bit_quant_type="nf4" + ) + + # Load tokenizer + self.tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_DIR) + self.tokenizer.pad_token = self.tokenizer.eos_token + self.tokenizer.padding_side = "right" + + # Load base model (quantized) + base_model = AutoModelForCausalLM.from_pretrained( + BASE_MODEL_DIR, + quantization_config=quant_config, + device_map="auto" + ) + + # Apply fine-tuned weights + self.fine_tuned_model = PeftModel.from_pretrained( + base_model, + FINETUNED_MODEL_DIR, + revision=REVISION + ) + self.fine_tuned_model.generation_config.pad_token_id = self.tokenizer.pad_token_id + + @modal.method() + def price(self, description: str) -> float: + import re, torch + from transformers import set_seed + + set_seed(42) # Deterministic output + + # Construct prompt + prompt = f"{QUESTION}\n\n{description}\n\n{PREFIX}" + inputs = self.tokenizer.encode(prompt, return_tensors="pt").to("cuda") + attention_mask = torch.ones(inputs.shape, device="cuda") + + # Generate model output (max 5 tokens) + outputs = self.fine_tuned_model.generate( + inputs, + attention_mask=attention_mask, + max_new_tokens=5, + num_return_sequences=1 + ) + result = self.tokenizer.decode(outputs[0]) + + # Extract number after "Price is $" + contents = result.split("Price is $")[1] + contents = contents.replace(',', '') + match = re.search(r"[-+]?\d*\.\d+|\d+", contents) + return float(match.group()) if match else 0 # Return parsed price or 0 if not found + + diff --git a/week8/community_contributions/lisekarimi/modal_services/get_started.py b/week8/community_contributions/lisekarimi/modal_services/get_started.py new file mode 100644 index 0000000..510d7ad --- /dev/null +++ b/week8/community_contributions/lisekarimi/modal_services/get_started.py @@ -0,0 +1,12 @@ +import sys, modal + +app = modal.App("example-hello-world") + +@app.function() +def f(i: int) -> int: + if i % 2 == 0: + print("hello", i) + else: + print("world", i, file=sys.stderr) + + return i * i