From b832a5ee51167590950baf2c4c4583babd5dedcf Mon Sep 17 00:00:00 2001 From: The Top Dev Date: Wed, 29 Oct 2025 05:29:54 +0300 Subject: [PATCH 1/4] Add Week 6 finetuning solution: Clean implementation with pickle data - Added Week6_Product_Pricer_Clean.ipynb with complete fine-tuning pipeline - Added enhanced_items.py and testing.py modules for Windows compatibility - Added train.pkl, test.pkl, validation.pkl data files (250 items total) - Implements OpenAI fine-tuning with enhanced prompts - Includes comprehensive evaluation and comparison framework - Ready for submission and grading --- .../Week6_Product_Pricer_Clean.ipynb | 828 ++++++++++++++++++ .../finetuning-joshua/enhanced_items.py | 149 ++++ .../finetuning-joshua/test.pkl | Bin 0 -> 37506 bytes .../finetuning-joshua/testing.py | 75 ++ .../finetuning-joshua/train.pkl | Bin 0 -> 113108 bytes .../finetuning-joshua/validation.pkl | Bin 0 -> 37066 bytes 6 files changed, 1052 insertions(+) create mode 100644 week6/community-contributions/finetuning-joshua/Week6_Product_Pricer_Clean.ipynb create mode 100644 week6/community-contributions/finetuning-joshua/enhanced_items.py create mode 100644 week6/community-contributions/finetuning-joshua/test.pkl create mode 100644 week6/community-contributions/finetuning-joshua/testing.py create mode 100644 week6/community-contributions/finetuning-joshua/train.pkl create mode 100644 week6/community-contributions/finetuning-joshua/validation.pkl diff --git a/week6/community-contributions/finetuning-joshua/Week6_Product_Pricer_Clean.ipynb b/week6/community-contributions/finetuning-joshua/Week6_Product_Pricer_Clean.ipynb new file mode 100644 index 0000000..6b4fc33 --- /dev/null +++ b/week6/community-contributions/finetuning-joshua/Week6_Product_Pricer_Clean.ipynb @@ -0,0 +1,828 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Week 6 - Product Pricer Challenge\n", + "\n", + "**A baseline established by GPT-4o and attempt to beat it with fine-tuning**\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize and Load Configuration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "import os\n", + "import re\n", + "import math\n", + "import json\n", + "import random\n", + "import pickle\n", + "from collections import Counter\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from huggingface_hub import login\n", + "from openai import OpenAI\n", + "\n", + "# SimpleItem class definition for pickle compatibility\n", + "class SimpleItem:\n", + " \"\"\"\n", + " Simple item class for pickle compatibility\n", + " This matches the structure used in the CSV conversion script\n", + " \"\"\"\n", + " def __init__(self, title, description, price, category=\"Human_Generated\", token_count=0):\n", + " self.title = title\n", + " self.description = description\n", + " self.price = price\n", + " self.category = category\n", + " self.token_count = token_count\n", + "\n", + " def test_prompt(self):\n", + " \"\"\"\n", + " Return a prompt suitable for testing, with the actual price removed\n", + " This method is needed for compatibility with the testing framework\n", + " \"\"\"\n", + " return f\"How much does this cost to the nearest dollar?\\n\\n{self.title}\\n\\n{self.description}\\n\\nPrice is $\"\n", + "\n", + " def __repr__(self):\n", + " return f\"SimpleItem(title='{self.title[:50]}...', price=${self.price})\"\n", + "\n", + "# Import our custom classes\n", + "# Use original testing class to avoid matplotlib color issues\n", + "try:\n", + " from enhanced_items import Item\n", + " # Use original Tester to avoid matplotlib color issues\n", + " import sys\n", + " import os\n", + " sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(''))))\n", + " from testing import Tester\n", + " print(\"✅ Using enhanced items and original testing from parent directory\")\n", + "except ImportError:\n", + " # Fallback to parent directory modules\n", + " import sys\n", + " import os\n", + " sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(''))))\n", + " from items import Item\n", + " from testing import Tester\n", + " print(\"✅ Using modules from parent directory\")\n", + "\n", + "print(\"✅ All imports successful!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Environment setup\n", + "try:\n", + " from google.colab import userdata\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " os.environ['HF_TOKEN'] = userdata.get('HF_TOKEN')\n", + " print(\"✅ Using Colab secrets\")\n", + "except:\n", + " from dotenv import load_dotenv\n", + " load_dotenv(override=True)\n", + " os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', 'your-key-if-not-using-env')\n", + " os.environ['HF_TOKEN'] = os.getenv('HF_TOKEN', 'your-key-if-not-using-env')\n", + " print(\"✅ Using local .env file\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Log in to HuggingFace\n", + "hf_token = os.environ['HF_TOKEN']\n", + "login(hf_token)\n", + "\n", + "# Initialize OpenAI client\n", + "openai = OpenAI()\n", + "\n", + "# Enable matplotlib inline for Colab\n", + "%matplotlib inline\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Data\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load pre-processed pickle files (our data loading hack)\n", + "def load_pickle_data():\n", + " \"\"\"\n", + " Load pre-processed pickle files with fallback to sample data\n", + " \"\"\"\n", + " print(\"📦 Loading pre-processed pickle files...\")\n", + " \n", + " # Try to load pickle files\n", + " pickle_files = ['train.pkl', 'test.pkl', 'validation.pkl', \n", + " 'data/train.pkl', 'data/test.pkl', 'data/validation.pkl',\n", + " '../train.pkl', '../test.pkl', '../validation.pkl']\n", + " \n", + " train = None\n", + " test = None\n", + " validation = None\n", + " \n", + " # Load training data\n", + " for file_path in ['train.pkl', 'data/train.pkl', '../train.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " train = pickle.load(f)\n", + " print(f\"✅ Loaded training data: {file_path} ({len(train)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + " # Try to load as dictionary and convert to SimpleItem\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " raw_data = pickle.load(f)\n", + " if isinstance(raw_data, list) and len(raw_data) > 0:\n", + " if isinstance(raw_data[0], dict):\n", + " # Convert dictionary to SimpleItem\n", + " train = []\n", + " for item_dict in raw_data:\n", + " item = SimpleItem(\n", + " title=item_dict.get('title', ''),\n", + " description=item_dict.get('description', ''),\n", + " price=item_dict.get('price', 0.0),\n", + " category=item_dict.get('category', 'Human_Generated'),\n", + " token_count=item_dict.get('token_count', 0)\n", + " )\n", + " train.append(item)\n", + " print(f\" Converted {len(train)} training items from dictionary format\")\n", + " break\n", + " except Exception as e2:\n", + " print(f\" ❌ Failed to convert {file_path}: {e2}\")\n", + " \n", + " # Load test data\n", + " for file_path in ['test.pkl', 'data/test.pkl', '../test.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " test = pickle.load(f)\n", + " print(f\"✅ Loaded test data: {file_path} ({len(test)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + " # Try to load as dictionary and convert to SimpleItem\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " raw_data = pickle.load(f)\n", + " if isinstance(raw_data, list) and len(raw_data) > 0:\n", + " if isinstance(raw_data[0], dict):\n", + " # Convert dictionary to SimpleItem\n", + " test = []\n", + " for item_dict in raw_data:\n", + " item = SimpleItem(\n", + " title=item_dict.get('title', ''),\n", + " description=item_dict.get('description', ''),\n", + " price=item_dict.get('price', 0.0),\n", + " category=item_dict.get('category', 'Human_Generated'),\n", + " token_count=item_dict.get('token_count', 0)\n", + " )\n", + " test.append(item)\n", + " print(f\" Converted {len(test)} test items from dictionary format\")\n", + " break\n", + " except Exception as e2:\n", + " print(f\" ❌ Failed to convert {file_path}: {e2}\")\n", + " \n", + " # Load validation data\n", + " for file_path in ['validation.pkl', 'data/validation.pkl', '../validation.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " validation = pickle.load(f)\n", + " print(f\"✅ Loaded validation data: {file_path} ({len(validation)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + " # Try to load as dictionary and convert to SimpleItem\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " raw_data = pickle.load(f)\n", + " if isinstance(raw_data, list) and len(raw_data) > 0:\n", + " if isinstance(raw_data[0], dict):\n", + " # Convert dictionary to SimpleItem\n", + " validation = []\n", + " for item_dict in raw_data:\n", + " item = SimpleItem(\n", + " title=item_dict.get('title', ''),\n", + " description=item_dict.get('description', ''),\n", + " price=item_dict.get('price', 0.0),\n", + " category=item_dict.get('category', 'Human_Generated'),\n", + " token_count=item_dict.get('token_count', 0)\n", + " )\n", + " validation.append(item)\n", + " print(f\" Converted {len(validation)} validation items from dictionary format\")\n", + " break\n", + " except Exception as e2:\n", + " print(f\" ❌ Failed to convert {file_path}: {e2}\")\n", + " \n", + " # If no pickle files found, create sample data\n", + " if not train or not test:\n", + " print(\"🔄 No pickle files found, creating sample data...\")\n", + " train, test, validation = create_sample_data()\n", + " \n", + " # Debug: Check what we actually loaded\n", + " print(f\"\\n🔍 Debug - Data loaded:\")\n", + " print(f\" train: {len(train) if train else 0} items\")\n", + " print(f\" test: {len(test) if test else 0} items\") \n", + " print(f\" validation: {len(validation) if validation else 0} items\")\n", + " \n", + " # Additional safety check\n", + " if not test or len(test) == 0:\n", + " print(\"⚠️ WARNING: Test dataset is empty! Creating emergency sample data...\")\n", + " # Create emergency test data\n", + " emergency_test = [\n", + " SimpleItem(\"Test Product 1\", \"A test product for evaluation\", 25.99, \"Test\", 10),\n", + " SimpleItem(\"Test Product 2\", \"Another test product\", 45.50, \"Test\", 12),\n", + " SimpleItem(\"Test Product 3\", \"Third test product\", 15.75, \"Test\", 8)\n", + " ]\n", + " test = emergency_test\n", + " print(f\" Emergency test data created: {len(test)} items\")\n", + " \n", + " return train, test, validation\n", + "\n", + "def create_sample_data():\n", + " \"\"\"\n", + " Create sample data for demonstration\n", + " \"\"\"\n", + " # Sample product data (expanded for better testing)\n", + " sample_products = [\n", + " {\"title\": \"Wireless Bluetooth Headphones\", \"price\": 89.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Stainless Steel Water Bottle\", \"price\": 24.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Organic Cotton T-Shirt\", \"price\": 19.99, \"category\": \"Clothing\"},\n", + " {\"title\": \"Ceramic Coffee Mug\", \"price\": 12.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"LED Desk Lamp\", \"price\": 45.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Yoga Mat\", \"price\": 29.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Leather Wallet\", \"price\": 39.99, \"category\": \"Accessories\"},\n", + " {\"title\": \"Bluetooth Speaker\", \"price\": 79.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Kitchen Knife Set\", \"price\": 129.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Running Shoes\", \"price\": 89.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Smartphone Case\", \"price\": 15.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Coffee Maker\", \"price\": 89.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Backpack\", \"price\": 49.99, \"category\": \"Accessories\"},\n", + " {\"title\": \"Tennis Racket\", \"price\": 79.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Laptop Stand\", \"price\": 34.99, \"category\": \"Electronics\"}\n", + " ]\n", + " \n", + " # Create SimpleItem objects\n", + " items = []\n", + " for product in sample_products:\n", + " item = SimpleItem(\n", + " title=product['title'],\n", + " description=f\"High-quality {product['title'].lower()}\",\n", + " price=product['price'],\n", + " category=product['category'],\n", + " token_count=len(product['title'] + f\"High-quality {product['title'].lower()}\") // 4\n", + " )\n", + " items.append(item)\n", + " \n", + " # Split into train/test/validation (more balanced split)\n", + " train = items[:10] # 10 items\n", + " test = items[10:13] # 3 items \n", + " validation = items[13:] # 2 items\n", + " \n", + " print(f\"✅ Created sample data: {len(train)} train, {len(test)} test, {len(validation)} validation\")\n", + " return train, test, validation\n", + "\n", + "# Load the data\n", + "train, test, validation = load_pickle_data()\n", + "\n", + "print(f\"\\n📊 Dataset Statistics:\")\n", + "print(f\" Training: {len(train)} items\")\n", + "print(f\" Test: {len(test)} items\")\n", + "print(f\" Validation: {len(validation)} items\")\n", + "\n", + "if train:\n", + " print(f\"\\n🔍 Sample Training Item:\")\n", + " print(f\" Title: {train[0].title}\")\n", + " print(f\" Price: ${train[0].price}\")\n", + " print(f\" Category: {train[0].category}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prepare Fine-tuning Data\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# OpenAI recommends fine-tuning with 50-100 examples\n", + "# Use our actual train/validation split from the pickle files\n", + "fine_tune_train = train # Use all training data (150 items)\n", + "fine_tune_validation = validation # Use validation data (50 items)\n", + "\n", + "print(f\"📊 Fine-tuning data prepared:\")\n", + "print(f\" Training: {len(fine_tune_train)} items\")\n", + "print(f\" Validation: {len(fine_tune_validation)} items\")\n", + "\n", + "# Weight and Biases integration (optional)\n", + "wandb_integration = {\"type\": \"wandb\", \"wandb\": {\"project\": \"gpt-pricer-ft\"}}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper Functions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Utility function to extract price from a string\n", + "def get_price(s):\n", + " s = s.replace('$', '').replace(',', '')\n", + " match = re.search(r\"[-+]?\\d*\\.\\d+|\\d+\", s)\n", + " return float(match.group()) if match else 0\n", + "\n", + "# Prompt generation functions\n", + "def messages_for(item):\n", + " system_message = \"You estimate prices of items. Reply only with the price, no explanation\"\n", + " user_prompt = item.test_prompt().replace(\" to the nearest dollar\", \"\").replace(\"\\n\\nPrice is $\", \"\")\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt},\n", + " {\"role\": \"assistant\", \"content\": \"Price is $\"}\n", + " ]\n", + "\n", + "def messages_with_price(item):\n", + " system_message = \"You estimate prices of items. Reply only with the price, no explanation\"\n", + " user_prompt = item.test_prompt().replace(\" to the nearest dollar\", \"\").replace(\"\\n\\nPrice is $\", \"\")\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt},\n", + " {\"role\": \"assistant\", \"content\": f\"Price is ${item.price:.2f}\"}\n", + " ]\n", + "\n", + "print(\"✅ Helper functions defined!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Baseline GPT-4o Model\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def gpt_4o_frontier(item):\n", + " response = openai.chat.completions.create(\n", + " model=\"gpt-4o\",\n", + " messages=messages_for(item),\n", + " seed=42,\n", + " max_tokens=5\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return get_price(reply)\n", + "\n", + "print(\"🧪 Testing baseline GPT-4o model...\")\n", + "\n", + "# Safety check: Make sure we have test data\n", + "if not test or len(test) == 0:\n", + " print(\"❌ No test data available! Cannot run baseline test.\")\n", + " print(\"💡 Please check the data loading section above.\")\n", + " print(\"🔍 Debug info:\")\n", + " print(f\" test variable exists: {test is not None}\")\n", + " print(f\" test length: {len(test) if test else 'N/A'}\")\n", + " print(f\" test type: {type(test)}\")\n", + "else:\n", + " print(f\"📊 Testing on {len(test)} items...\")\n", + " print(f\"🔍 Test data preview:\")\n", + " for i, item in enumerate(test[:3]): # Show first 3 items\n", + " print(f\" Item {i}: {item.title} - ${item.price}\")\n", + " \n", + " try:\n", + " # Create Tester with correct size parameter\n", + " tester = Tester(gpt_4o_frontier, test, size=len(test))\n", + " tester.run()\n", + " except IndexError as e:\n", + " print(f\"❌ IndexError in Tester.test: {e}\")\n", + " print(f\"🔍 Test data length: {len(test)}\")\n", + " print(\"💡 This suggests the Tester is trying to access more items than available.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fine-tuning Implementation\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if fine_tuned_model_name:\n", + " def gpt_fine_tuned(item):\n", + " response = openai.chat.completions.create(\n", + " model=fine_tuned_model_name,\n", + " messages=messages_for(item),\n", + " seed=42,\n", + " max_tokens=7\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return get_price(reply)\n", + " \n", + " print(\"🧪 Testing fine-tuned model...\")\n", + " # Create Tester with correct size parameter to avoid IndexError\n", + " tester = Tester(gpt_fine_tuned, test, size=len(test))\n", + " tester.run()\n", + "else:\n", + " print(\"⏳ Fine-tuned model not ready yet. Please wait and re-run the previous cell.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Convert items to JSONL format for fine-tuning\n", + "def make_jsonl(items):\n", + " result = \"\"\n", + " for item in items:\n", + " messages = messages_with_price(item)\n", + " messages_str = json.dumps(messages)\n", + " result += '{\"messages\": ' + messages_str + '}\\n'\n", + " return result.strip()\n", + "\n", + "def write_jsonl(items, filename):\n", + " with open(filename, \"w\") as f:\n", + " jsonl = make_jsonl(items)\n", + " f.write(jsonl)\n", + "\n", + "# Create fine-tuning files\n", + "write_jsonl(fine_tune_train, \"fine_tune_train.jsonl\")\n", + "write_jsonl(fine_tune_validation, \"fine_tune_validation.jsonl\")\n", + "\n", + "print(\"✅ Fine-tuning files created:\")\n", + "print(\" - fine_tune_train.jsonl\")\n", + "print(\" - fine_tune_validation.jsonl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload files to OpenAI\n", + "with open(\"fine_tune_train.jsonl\", \"rb\") as f:\n", + " train_file = openai.files.create(file=f, purpose=\"fine-tune\")\n", + "\n", + "with open(\"fine_tune_validation.jsonl\", \"rb\") as f:\n", + " validation_file = openai.files.create(file=f, purpose=\"fine-tune\")\n", + "\n", + "print(f\"✅ Files uploaded to OpenAI:\")\n", + "print(f\" Training file ID: {train_file.id}\")\n", + "print(f\" Validation file ID: {validation_file.id}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create fine-tuning job\n", + "fine_tuning_job = openai.fine_tuning.jobs.create(\n", + " training_file=train_file.id,\n", + " validation_file=validation_file.id,\n", + " model=\"gpt-4o-mini\",\n", + " seed=42,\n", + " hyperparameters={\"n_epochs\": 1},\n", + " integrations=[wandb_integration],\n", + " suffix=\"pricer\"\n", + ")\n", + "\n", + "print(f\"🚀 Fine-tuning job created: {fine_tuning_job.id}\")\n", + "print(\"⏳ This will take some time to complete...\")\n", + "print(\"💡 You can monitor progress in the OpenAI dashboard or Weights & Biases\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# FIXED: Test enhanced model (if ready) - with correct Tester size\n", + "try:\n", + " enhanced_model_name = openai.fine_tuning.jobs.retrieve(fine_tuning_job_v2.id).fine_tuned_model\n", + " \n", + " def gpt_enhanced_fine_tuned(item):\n", + " response = openai.chat.completions.create(\n", + " model=enhanced_model_name,\n", + " messages=messages_v2(item, with_price=False),\n", + " seed=42,\n", + " temperature=1.0,\n", + " max_tokens=7\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return get_price(reply)\n", + " \n", + " print(\"🧪 Testing enhanced fine-tuned model...\")\n", + " # Create Tester with correct size parameter to avoid IndexError\n", + " tester = Tester(gpt_enhanced_fine_tuned, test, size=len(test))\n", + " tester.run()\n", + " \n", + "except:\n", + " print(\"⏳ Enhanced fine-tuned model not ready yet.\")\n", + " print(\"💡 Please wait for completion and re-run this cell.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check job status\n", + "job_id = fine_tuning_job.id\n", + "job_status = openai.fine_tuning.jobs.retrieve(job_id)\n", + "\n", + "print(f\"📊 Job Status: {job_status.status}\")\n", + "print(f\"📈 Training File: {job_status.training_file}\")\n", + "print(f\"📈 Validation File: {job_status.validation_file}\")\n", + "print(f\"🤖 Model: {job_status.model}\")\n", + "\n", + "# Get recent events\n", + "events = openai.fine_tuning.jobs.list_events(fine_tuning_job_id=job_id, limit=10)\n", + "print(f\"\\n📋 Recent Events:\")\n", + "for event in events.data:\n", + " print(f\" {event.created_at}: {event.message}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test Fine-tuned Model\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait for fine-tuning to complete and get the model name\n", + "# Note: In practice, you would wait for the job to complete\n", + "try:\n", + " fine_tuned_model_name = openai.fine_tuning.jobs.retrieve(job_id).fine_tuned_model\n", + " print(f\"✅ Fine-tuned model ready: {fine_tuned_model_name}\")\n", + "except:\n", + " print(\"⏳ Fine-tuning still in progress...\")\n", + " print(\"💡 Please wait for completion and re-run this cell\")\n", + " fine_tuned_model_name = None\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test the fine-tuned model (if ready)\n", + "if fine_tuned_model_name:\n", + " def gpt_fine_tuned(item):\n", + " response = openai.chat.completions.create(\n", + " model=fine_tuned_model_name,\n", + " messages=messages_for(item),\n", + " seed=42,\n", + " max_tokens=7\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return get_price(reply)\n", + " \n", + " print(\"🧪 Testing fine-tuned model...\")\n", + " Tester.test(gpt_fine_tuned, test)\n", + "else:\n", + " print(\"⏳ Fine-tuned model not ready yet. Please wait and re-run the previous cell.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Fine-tuning with Enhanced Prompts\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Enhanced prompt function (based on gold standard)\n", + "def messages_v2(item, with_price=True):\n", + " system_message = (\n", + " \"Role: You are a retail price estimator.\\n\"\n", + " \"Market: United States; Currency: USD.\\n\"\n", + " \"Scope: Predict the most likely new retail price. Ignore taxes, shipping, coupons, bundles, used/renewed.\\n\"\n", + " \"Output: Only a number with two decimals (e.g., 129.99). No $ sign. No words.\\n\"\n", + " \"Think silently; do not reveal reasoning.\"\n", + " )\n", + " \n", + " user_prompt = item.test_prompt().replace(\" to the nearest dollar\", \"\").replace(\"\\n\\nPrice is $\", \"\")\n", + " \n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": str({\n", + " \"query\": \"price_estimate\",\n", + " \"locale\": \"en_US\",\n", + " \"currency\": \"USD\",\n", + " \"category\": item.category,\n", + " \"description\": user_prompt,\n", + " \"brand\": json.loads(item.details).get(\"Brand\", \"Unknown\") if item.details else \"Unknown\"\n", + " })},\n", + " {\"role\": \"assistant\", \"content\": f\"Price is ${item.price:.2f}\" if with_price else \"Price is $\"}\n", + " ]\n", + "\n", + "print(\"✅ Enhanced prompt function created!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create enhanced fine-tuning data\n", + "def make_jsonl_v2(items):\n", + " result = \"\"\n", + " for item in items:\n", + " messages = messages_v2(item)\n", + " messages_str = json.dumps(messages)\n", + " result += '{\"messages\": ' + messages_str + '}\\n'\n", + " return result.strip()\n", + "\n", + "def write_jsonl_v2(items, filename):\n", + " with open(filename, \"w\") as f:\n", + " jsonl = make_jsonl_v2(items)\n", + " f.write(jsonl)\n", + "\n", + "# Create enhanced fine-tuning files\n", + "write_jsonl_v2(fine_tune_train, \"fine_tune_train_v2.jsonl\")\n", + "write_jsonl_v2(fine_tune_validation, \"fine_tune_validation_v2.jsonl\")\n", + "\n", + "print(\"✅ Enhanced fine-tuning files created:\")\n", + "print(\" - fine_tune_train_v2.jsonl\")\n", + "print(\" - fine_tune_validation_v2.jsonl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload enhanced files and create second fine-tuning job\n", + "with open(\"fine_tune_train_v2.jsonl\", \"rb\") as f:\n", + " train_file_v2 = openai.files.create(file=f, purpose=\"fine-tune\")\n", + "\n", + "with open(\"fine_tune_validation_v2.jsonl\", \"rb\") as f:\n", + " validation_file_v2 = openai.files.create(file=f, purpose=\"fine-tune\")\n", + "\n", + "# Create second fine-tuning job with enhanced prompts\n", + "fine_tuning_job_v2 = openai.fine_tuning.jobs.create(\n", + " training_file=train_file_v2.id,\n", + " validation_file=validation_file_v2.id,\n", + " model=\"gpt-4o-mini\",\n", + " seed=42,\n", + " hyperparameters={\"n_epochs\": 1},\n", + " integrations=[wandb_integration],\n", + " suffix=\"pricer-v2\"\n", + ")\n", + "\n", + "print(f\"🚀 Enhanced fine-tuning job created: {fine_tuning_job_v2.id}\")\n", + "print(\"⏳ This will take some time to complete...\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Comparison and Results\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test enhanced model (if ready)\n", + "try:\n", + " enhanced_model_name = openai.fine_tuning.jobs.retrieve(fine_tuning_job_v2.id).fine_tuned_model\n", + " \n", + " def gpt_enhanced_fine_tuned(item):\n", + " response = openai.chat.completions.create(\n", + " model=enhanced_model_name,\n", + " messages=messages_v2(item, with_price=False),\n", + " seed=42,\n", + " temperature=1.0,\n", + " max_tokens=7\n", + " )\n", + " reply = response.choices[0].message.content\n", + " return get_price(reply)\n", + " \n", + " print(\"🧪 Testing enhanced fine-tuned model...\")\n", + " Tester.test(gpt_enhanced_fine_tuned, test)\n", + " \n", + "except:\n", + " print(\"⏳ Enhanced fine-tuned model not ready yet.\")\n", + " print(\"💡 Please wait for completion and re-run this cell.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary and Next Steps\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"🎉 Week 6 Product Pricer Challenge Complete!\")\n", + "print(\"=\" * 50)\n", + "\n", + "print(\"\\n📊 What We Accomplished:\")\n", + "print(\"✅ Loaded data using pickle files (our data loading hack)\")\n", + "print(\"✅ Established baseline with GPT-4o\")\n", + "print(\"✅ Implemented fine-tuning with OpenAI API\")\n", + "print(\"✅ Created enhanced prompts for better performance\")\n", + "print(\"✅ Set up comprehensive evaluation framework\")\n", + "\n", + "print(\"\\n🚀 Next Steps:\")\n", + "print(\"1. Wait for fine-tuning jobs to complete\")\n", + "print(\"2. Compare performance of all models\")\n", + "print(\"3. Experiment with different hyperparameters\")\n", + "print(\"4. Try different base models (GPT-4.1, etc.)\")\n", + "print(\"5. Implement ensemble methods\")\n", + "\n", + "print(\"\\n💡 Key Learnings:\")\n", + "print(\"• Fine-tuning can significantly improve model performance\")\n", + "print(\"• Prompt engineering is crucial for good results\")\n", + "print(\"• Data quality and quantity matter for fine-tuning\")\n", + "print(\"• Evaluation metrics help track progress\")\n", + "\n", + "print(\"\\n🎯 This implementation follows the gold standard approach\")\n", + "print(\" while incorporating our data loading improvements!\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/week6/community-contributions/finetuning-joshua/enhanced_items.py b/week6/community-contributions/finetuning-joshua/enhanced_items.py new file mode 100644 index 0000000..e727573 --- /dev/null +++ b/week6/community-contributions/finetuning-joshua/enhanced_items.py @@ -0,0 +1,149 @@ +from typing import Optional +from transformers import AutoTokenizer +import re +import os + +# Try multiple model sources in order of preference +BASE_MODEL_OPTIONS = [ + "/root/.llama/checkpoints/Llama3.1-8B", # Local llama-stack download + "microsoft/DialoGPT-medium", # Accessible alternative + "gpt2" # Fallback +] + +BASE_MODEL = None + +MIN_TOKENS = 150 # Any less than this, and we don't have enough useful content +MAX_TOKENS = 160 # Truncate after this many tokens. Then after adding in prompt text, we will get to around 180 tokens + +MIN_CHARS = 300 +CEILING_CHARS = MAX_TOKENS * 7 + +class Item: + """ + An Item is a cleaned, curated datapoint of a Product with a Price + Enhanced version with better error handling and alternative tokenizer + """ + + # Initialize tokenizer with fallback options + tokenizer = None + for model_path in BASE_MODEL_OPTIONS: + try: + if model_path.startswith("/") and not os.path.exists(model_path): + continue # Skip local paths that don't exist + tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) + BASE_MODEL = model_path + print(f"✅ Successfully loaded tokenizer from: {model_path}") + break + except Exception as e: + print(f"⚠️ Failed to load {model_path}: {e}") + continue + + if tokenizer is None: + print("❌ All tokenizer options failed. Using character-based fallback.") + # Create a dummy tokenizer for fallback + class DummyTokenizer: + def encode(self, text, add_special_tokens=False): + # Rough approximation: 1 token ≈ 4 characters + return list(range(len(text) // 4)) + def decode(self, tokens): + return "dummy text" + tokenizer = DummyTokenizer() + BASE_MODEL = "fallback" + + PREFIX = "Price is $" + QUESTION = "How much does this cost to the nearest dollar?" + 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 " + ] + + title: str + price: float + category: str + token_count: int = 0 + details: Optional[str] + prompt: Optional[str] = None + include = False + + def __init__(self, data, price): + self.title = data['title'] + self.price = price + self.parse(data) + + def scrub_details(self): + """ + Clean up the details string by removing common text that doesn't add value + """ + 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): + """ + Parse this datapoint and if it fits within the allowed Token range, + then set include to True + """ + 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 len(contents) > MIN_CHARS: + contents = contents[:CEILING_CHARS] + text = f"{self.scrub(self.title)}\n{self.scrub(contents)}" + tokens = self.tokenizer.encode(text, add_special_tokens=False) + if len(tokens) > MIN_TOKENS: + tokens = tokens[:MAX_TOKENS] + text = self.tokenizer.decode(tokens) + self.make_prompt(text) + self.include = True + + def make_prompt(self, text): + """ + Set the prompt instance variable to be a prompt appropriate for training + """ + self.prompt = f"{self.QUESTION}\n\n{text}\n\n" + self.prompt += f"{self.PREFIX}{str(round(self.price))}.00" + self.token_count = len(self.tokenizer.encode(self.prompt, add_special_tokens=False)) + + def test_prompt(self): + """ + Return a prompt suitable for testing, with the actual price removed + """ + return self.prompt.split(self.PREFIX)[0] + self.PREFIX + + def __repr__(self): + """ + Return a String version of this Item + """ + return f"<{self.title} = ${self.price}>" + + + diff --git a/week6/community-contributions/finetuning-joshua/test.pkl b/week6/community-contributions/finetuning-joshua/test.pkl new file mode 100644 index 0000000000000000000000000000000000000000..bdbfb3294b574e3a6fa229ef6602ce20d29600d4 GIT binary patch literal 37506 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 EpQKyRvj6}9 literal 0 HcmV?d00001 diff --git a/week6/community-contributions/finetuning-joshua/validation.pkl b/week6/community-contributions/finetuning-joshua/validation.pkl new file mode 100644 index 0000000000000000000000000000000000000000..eb626e4d9ed9119f415cf2acde67d77e0d8f7626 GIT binary patch literal 37066 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

Up(I z`a%q(3QuggUYzMM_a_QGAXv`y6NrERcSS8ed1SIxWyoF0u3;j&E(}c5RJZ}xr{<{ z1D>G3l{$$<(~FcQ_E!0#H%+M3ZXP#Web6%-Us%D(NPbE5j|jm=5XDHzwqQ3RtSE@2 z`tF=iqJlySSE@Ec`&wY!m?Ysn7^HWLi^Nr|Yz}4+iuH#ObF4hMMMyk?xxQTycNza7 zK?K{e{$Ql@aCY7%FtJaFH|)KCb{^v#ceq1Qi3Bd0hW8aJXo@;qlXu^Wn1_-mnS}@2 zOcFEX9S}WW;7t@2CZ>_6PADuBr|iXHfSaq2n2*OIqH@|?t3Fz67%9R0*T&`!MTvp$ zL`;s$do--@Zn;epmq|(2|9%PTd~bshGcA%pJ5K}k;LU?}Xsk&jb`c49$MmhK;i$QN zk`1!8Dq~u5z1EppI3XnVh*%Jf+0P>!2H$;hUhFl^B5c9Ym_a7#@Dt~D+A6;upmRb z6?f!()-s!SXcb^q-)nYM&w>Wb8_ssvia6BkPEyno;I zR#EzG#Z!Nz4Q+nHi0;Lro=+bN^{~rRBJHpiwr;vUnND)?#_`{I$>{L;Us@XHx6_-E z+j_I5Ef-3a!w}yGO6;oG8Et4Sjj=Nhks{7UD@IjTHq-L};Qd>A1muPg45>(yRmk3c}m; zFsdjQ4Z>W0n+W*MN&o~8ECfFD;1qT>1}eO|#c&6r6&H%vc!PpQ1v_{aH_yzWyN>y` zA9TEKQf>k@aOC@4?5VH|-D|Mkx$S*5BSeJ2ef-O~v(;|VD5m&) z-_eb4-&urSv@!~{2yZxx*i;LWiOfJSf?vjBXx)OB?7`Pa0k9(6D_ljFrR_ZeWP)9w z$0B`K>pu6o!%23*2H{T7exR|HLM&!S-G+tZtR6!v;rWThyQO-`%}a#EOL-+nEv z+KX~*|Nl>b>t%TgKvrp+-GW#Ea2M5|3 zr#Y9*+tR}W9+v;GESHh?1gCJQ*)4=FP=3ZAC6ctfJlf39| zYG^Qq+qPAUBg7S^1l^Dm+v`K~9zp|NvKa-w!%|(ysWe(9us_!+IgnF>({7FtyiJ9M zbEM>jDiGh6H&p#G5nE=C;|3Bo3ebnFjwD_@0G>v`i|20Sxz%CMQF{k78s6 z)g{Js82i&Grtg{?qTr>CNBjR`pkc8RqQ10!K}{Qf5h$TH`uT0e65++#i1l-n@-8_b zaEx{2>`)9y%i_-H5;xceYsF+l8>4DsG_|_u#ftbvV!O5;@5aS7K`F;Zey=1b7bLs@ z#VX%Qa1jQVSP}PYP*@6}S!G8v#jguH?}U5n4^Qta0~y-b1<3tal5Sm;$>HiPW zOL2F8UL_s(=kqegIlji;3QDmus+-X3Vg(j~GY8y7%Vl2 z$1^o)L7m_~Czo-bZ$f705?pqVoz}zq>eD=Ar(-d>I21GGDb=rw0Tc?v+};F+L#o7g z))@*O0}FL@w+lSQxH5c&$jzWo3$D5wis*9l+Gfm14DNu}R9e;Jji)QTFR3k-iJb}x zu@DF~tpEd=eY=odQ{U*YTNDz`a1?U|4AY4gg1|gSARMF8x*o?Tc!Sp0h-f<*aGRJk zZiguf29&Sm-MpX*m3`y(bmYHT*NQW+sgW)4rTSJlYsmsd6XjLyTcyCsuar1@hrpde z?jSAp`#?jhpy*N}5P5UOc93mXl_k_!1Mb+$6np@;ilA1b%fT1gI;~kk5#bS|eeSxe z{<{LlRFHd8o$$2nebZlbg$iX+v?&F$qrSCO1|Icjg7~Z}BuRiN5?fvp3b+UzkxsMC zvV?{$3m^rd+$*~a3TH^w2q-$#D;k1fdLRKZRxh*<{ZL)k=-oU+1s zVrnTEBTiTXNzRq%AU_d+0CvX}BwH;MoYUhoMU9B1P>yXmT_K*Ux#g$;ju|jJt+OSVk$iSB^JXRHsCHH!%0YU$?5g`|7Uh*J+}5q?!*CA7Qt@QT zuv0{L)^47k9oJjz`8%su@3^VvLTUjGMN)IBC8c`d0Xs#H-w>%-c#r4gvUK#sCQr({ z!6!8wv`!?!;s>Uf%h~LIQexk~dy-+mG0ow<2c#>DS``v!EcJWot?v~SX=7ChJ>{`} zmQGPP(`G7Wq4UdIEl4w#5;L9wDwpjj(&CyT9Io)Gz0#mAQODfW(_{rW< zNljiMq9AMTwNK1HuleJXyS6w2a&>}r=4>I!W55}_^q5sQqQwzVV-x*9Qt_5ca> zD|K*zt6)T1s*K1RV$cIylzb#P@oiri~DI zvvMrZd{QTj(m(?aNcM+V{XmOU!-bz!h*KNjb<(}vnB+*uO##6-JqPC?RQZcg4Ckuq;oE`^w^hJHy5?0 zZrUz2EE6y(7A6c6N^0F(n)b*g$Oi=$s9yqJ=9{ZEV{erx*obA-6=r|wb}Zl418ev+ zB21=1WhZbjeRl|MtIng*K;nKRcC{)S&UR?*OR+yC%1Qx&t?c)by=S-!Y}NH?aki7T z+-Wj|?j$Ww`vgR-^`iF9vj95YM%2{m*nBmkeJziJ6}AyUQ*bzjnY7prRcu$um=Y28 z9H?US^qdVQAQNu{;gJl>@>^ou%M2WV{(+xFwj4^}E+`!*l9s%0x!e&4 zko9#8D4wDy#onJn<$Tv`9w9P6T^uKZ7*18ku8iH)Qgwd@{3s8P$AKY>2+Kv9%OB{# zN++hi!Iz^kF3ZCNr>pkfM=MJL8);DnAi4Axh4Q8J4T%q|W>X=e5rDh;fR{n%Flowwq=nB+kS$Rf&r+&@X ze30U{5Q@6>YSQOgK}1U3Ac65zpsE+lEfejC27oWVb)j~JVkB`64^kc@q?TQSi}pov zr!LOmQpro(!>ldg#;;|LU|(3~=N zp}V_|gap>Sh1xh9-zpMtK$SGT!u%+{AXq^@J2ec1qk}NTz8^AAf{JtM zY{3>I$q_i{UqRYwY@ECxD&hw@0dEq%`O#03I>stIK{@73caq$AsfV6pYr*h>VqCZe z??PqOc3RNAjg`$E3UT|))a$pu6Nz&xVyDYp_qzt1W|`s)c5nlKqh7U-R8@tERS)mh z+MG_nu0n-#u+CU5lpZ4kd1VY4t^qZOi#r)Lk@BgveFK{@4F%1tjWW%+Esg^fy(LFU zQ93DQGcx!Z3U|lTD@3d-p*sTLh?-SzB3?c)n*#Y@OY!t4Y*W*20ksnsC+=fhjK{nX zGaTItO;_@b6E<>L5dlwyz)Thu4YMq^XkTD1MvD)!7$XzLre@D=?eT@OF~%4`2qIJ{ zfpa_L15G^&1R$fJO0!BZ<2Rh&fU}NY?*lWtWV_9!u5Qe`!X{dz*IvrUYrc&QTIj?6 zy5SX@LBzyX;uRz1m_5J+brCbqs{X|iv;SI&*{|M{GFMy}%Ykd#MAr~RH>J1bV83oL z5`Z$kD+l3e)5Af+zWC^QvY7*;r9|6_d5xy(Ful7I!X~KmGbt=Pl=08Pl_(|4(JVW< z`N>IE>&~QZGDE-!3!6W$ogP&TO`oE?7_eb9QdKJ3-Uug+(^%U|f!&RzaFpU=*9FDM zFg)wxTT_-+jspNR6$+~ej(O2(+)NApje8}6<9U%;K-N+Ol-z;k7I8DiRpnGX4UKS$ zmAatHs+7ksE^+oB`^E0g0KOxzRjzbZX}x6%qRG)#2Cv$M?X6^|NGgEO%N<+5M-n5# zU_8xCg2!}*Ws3zJab%5F0yC(@%$u(I`&ml1m;rU>Xjw zVg?3}NmZ&a$dnxTT3hE}<|v*$jszM)Y_~rs1Q|zEBDk)eQCP(}UA2#vA^b0vxb!zD zow*KAlua?uFK!Xq*x9a}v{>NEQ@Kr>{E%Bb?;>Zob4a2cPQ>{bVJ{x`u8H4%Y|)i}Trupdb~y=#419$EX9Lc72#6fNXC5$hzR;;s}ks`9Xo0dx$w!a@d|7=z)Kst-*yVu|94Q=@_$=L#CKFiVc0o+QBtc|DgJ(DpZk3sBXF^2oPV$M=*MyC4@q0TR0=yW8p>`5TW#;E|Ak&b-mFOodmf074H-gc| ze;y9LivF`GMHs6=>!^ZQ8(?d?E0wpruwvJIIu>8*a*zxZ0P5T9DSI@(#Rz8$Xfb$H zm2D63lZ)pg*>}I1cCk~Mtv8Jx8@`#=D6tSo0kdk+L@&!INwM?>pOR!HlNW7VehXX z6gt<4gLY%@)Id);;M0%C6kGRT>u>(tt8e|r-?JJ7%=9@ZSjD(3AK1du;_i+JsT_~u znZmF{!`w3l{+8{YehO=WW)yOaaa7FZWBJ-@&Gpjb>06o_x(1Dv__*%bhDLVO$ycyp zzs(JXX#2eEs?UO$l}-We-VbrWd(`}ip<1qneBu*drcWE|KG-k*%|tr#=O z(^*Y#O(YfWKTYO^pK>8g52r+ zn+HxC#B015sV+ARS;j==2b?hmR0Cm{73I^oho0tAy{DtQOy=9$tJj+w@e0or%1MUh z&mb>%M)?1xbMV$@(xDnY|Il71#%EJ%nK*a5{_N2%8?G*qV?4O&ta5+>$-|ElgFF<} z;Y3iaQH;u&7r~kzSkN)pfou=AtpNT`1hPyL42;4yd3@-N!66-^FXwGXF`|vdq56tz z-TelE_itQXPVD~El8^AWTD8aRCoMxx1zsu;iz()oFdUi;;NwoM8Cc431a%bW6dnaF zCKJTDcsR7eVLLQwIJ8cp<%Oxe296lGfO@p!vioA;Uy#bbqr2bKF735Wn%?%kcu=}+ zLeE{Ng>EAQLyn8(=q^&koO3>yUtvIy4+K}P1bES*at^17Y79t-T1AhhCxlxB?t3|| zG_jQXDm`PVh6pr>I(Uj(VGnD`u#MC;15q#$;AfE!6&>Y<0Qa?UR24H4Y?~2`Cqrf7 z%h-4-Zu_+f<=ao4Sfd-H~b+&QaD zP7EkmDI@aB`n*A2b5a}!t1=$Ejf9c;2Nh^l!vF^<%pq?mKadlzizv%od(rjNQ}gmE zht=}S4C&$7TF{2^EY?}@SO?sjIF-*c`S&Gp+T$cL)h@xt7a&idH_k=N33N!FQ5y&y zsVx8!A0!~Lw&aaS7Jd_1$VV12C_BZ;IQ4PAS?f19ZDzuolUqY)qKtbztvTBseFWz6 z)O6yBXhocr#c%Ub1_fH}M6E?2_$~Yd`>@RM2syw8E6isPkz}xKQwqTD=rb+-k&cZf zh6do%rXJNYDV*cr9yjQWP3~WN_@KJ}m7FB4oM#BO;Atx+j}d2j{F@^zAvC(uWGrXs zF4B_BLO6snCW!*v32yM6A&8x-o}!c>E+lApMskyJ44ugio37F5 zcJNsB9N`6#)|zlB!&pgs@pJse|Y=cl# z$`3iSQxbj+0rF0)3(V7q?EJUa?YI9N?hyf^EA6k*KnQkHX0H_5MSn8u`LCvQxr7I0 z>!U>*DyOeAF2CKbhJ{CAy0S~ zpSYnn#SWo|+K-dg>cP9QtWcQ+E5Pigs*H4f+rgz}vdmSg5CaGSRf~aLpq(j3A$Qyv z%ffz}&}^2n4*#YkH0!NSZ=g#36GTEL1|sO2D_xa*b0(3Un8|mvsM5FO+hP!*bBq$0+mS6E;gT zWUJ@}6$pIhjYtE(FbvOZqo{2%Y7#?_EGbaNX%?)dJD60k?Aciw%pmFV-6c*rfUMG% z3s8oh7lYvrD<%z4Ao{9gb~CM9dvv}qv*sP$)$MI>z_hn z3$Be`-h&W;rLx|jxqrIEgdesQ(eH!kCDvZZ21cN2Opz@~huwimy<}5tY6Yu)(-`c? zE5Vmeeb9S4*3u~GT09%)@zQdS^i`Q<6>m<*bZd!XXMWLOfJwMR7=ZX|_k(COJx9mbEb>QQRxMjOcJ9K48{5!l7upq@xvN0aCj3t5#0 zJQUe8 zJ4qF2Wxj;(J8=oTG-Rl9Ab}W2rIWPTBjl)vdd-ebEo65jHTcV(s~h$tqtf5X}N6 z3I)JseEo_9lsmZYzgkrR>tj?{2o!z$3LDKI)G=!ka4>C}~T8@&oMXtld4fLa~iN?4!ePi#l@=r^JLsI+QjP05F9EB02}% z7SZl&cE&QqL6nn%Q7jEon*hrTp@4NaxHW|7KYO#81Tscg5CDb=?aOF!1QA$^B- zQJ&Zima4Sb&ZSRr>55Du&_Lh#i|;k-n+Guqu5YgS30M-t|EcpFKTq4A=cP{@OdA3> z>lKD+W@(cIvZ68hSPg1v5e|1N>uVs1rv2D!=;&RM(eZn`)~a*9DKTgxfHNM!DeXyf zY0}w*(lOyf(Q8bUEM~mbxbQD4Z?{-PE&=Xhn+SrdLZn72r7$U0d#yfJ#|dCiF`dlL zIz-O+SQkn_5U!=O|LG!c<{be?l}(M53YScJr4z$!#7dZTs_alYN=~7F80{rE6#HP) zh0XW|r|_&@wYo-tJmP}Ag(Yntt~8qPv%fA~B*^FF$b(N@&nr?X~tlM9ePj2R6n91r+m3sVT*Z`JU zmx~-~p>4F11VRWxa$2WoE!Y06hvn;G5}itJY2%hugA|J2j6HQRP$LUh1hX6J3kbj<~W*m8OrjhwN#yI zzvjN}gztCw2p{f+9JzJQcGvGL6R8E_J%BIDFwF}<#yE2M0#c%&F{%n~Qs}8bzh-RD z)Kf4xe?%6uxb}gC=|Zi-`8=khS&o>KkMxA%#@F57TjuL>Uf}R+oA|Eijy*s8N(Fje zFVQA$jgH}5y^&J8_|GWrH2)pY=vOa<-27WkzXZ!yDAc{fF3u&`2dhr7ojPE0&p&bcu4*d)1Dsd|asgv7@=l<6DM*#)F$m=LBwk(PJ;IeVY|d|tic%wcL=pp*=OSCE;`;m9M8 zJiK`XCK2Vm>AzXh^c(p$ul)*AyM5Xy00-r4prUZI8n#}9bB+?!p(6Uufb{*>VgB4` zNs8fNi=l5*f;lWU3dQ|F3QWNEoS3~$9VU8e_^rYWxmR<*voCyvmwV|2G%qo=nj ziJ*5KWT@U>s`yM%^9Zu31Gk+xQj}KI!$C~pbk~ojJe5zN0#3f?0)P~Ii|bkoa( z(>WC<$q)Fh%VS>JplRn@dL!Z#(@e)UFm^fF!<`w&F5&H%y{Xw@msLbobxiLFew*c} zke^~9ZTA}0mU2w;FDUrtDg=}PVHj0r&-QVz+K<$6)uEG09fV7mmsjk}Ax><`^C*ho z*uE@dqxh|xJR{{>l`8JeFe<3KpsH+?#}#wS=t{yyh)m;HtM+e5eJBYV^d*(dLQh^Y zPNA{1wwpp{%4qYAkp%MOn3Gaw{}IdEE|WHSD5I7$c$MEwFG4!NlZC71Fal6|=s%vSDas9GnY@E!9g=xWTn=3oKx$%GoN^fpISlNE zuVd56kt~+f&h4}BD#uijaoe#mTfPsZeD3)#zh65$JbGL^!w3O_wRwY`wzwSVB0}uU z0pSH~qHGS8D{Qk5*uwd?c|*Na+Cufa`qs2PQmN_WZi0*QJbmBOo{`&Ch5QX{Vw**x zFjc*kyR}F&A1b8RazE0}`Vv~MooTet&iS=74RJBp7W}DbgSgiY#tG|o3i_G_WVbsy zlx^vWc;W?TdR;B`O=!^0_9GwUQFnsj%Wo+bP+W=bPHbtNqIn|jc=0&-Rt<#`;WtvA z=I47L_bvr60(M5Oh2XUJzf2%BDYXuGP#O*!WLgJL72o#2vyh%`-det-NETn&TfVf% zAsPx(FTMD9r1*Mqfv?X4Rsm9GnkDFRC=HUR62kWS$lb%fE83yb3({yZ^IYSId zM%`&dAQDPg`cnu&xi<*Q%N4j7J%^bYQXK5iH$JUx?MvNI&l`_xHB7#hf7+e5CEF+g z)SWS_0yb};pAe5WC!|u}BeC1hsN&V8%qK9A^A?C8Y7I$C?s*{P?Ka znVpgR3^7^y$}kB6GA`7d!FImn%eOfV0?Sg7J}JHjh25t~cBnx}?Y6RSjhq|i9P#HurWANI@R=)Z0|&$Ipn~1NH(z3 zplbPNLTlRMiY+O<&EI|?0SYG8gw%cY?i~V%?VN^>Vetddxj|3PrQXw7!SsQ8f^6tx z&=gCZ2c(Pdk^+Nt(gk=?IYa0y6=9V#RWf*(g?#Ta0RU5vn4ZO_ViW>HYUvenDK_e# zS%_7n;s`q}BVtxIP-#OfPNMGE$N!`gjSjE---cY}EQvkAS4C+&t8X>oyRkYAT zTuHTxiHNJ|v#+o-pN)qyH8?DJO-%e9!XkfMr|HsR=d7kzoq6%lu5I_${))nk77XI+ zb&p-o_oHt0qr=1qtN8q@tv;XDn5^oMhBFu=Q zS`XjP_w4a4+5J`Cjb0X!FW1<-!m&T0eOwP)>uWW>e;^d4GK881`6ctx-rw^@N@e21 zyyB5uNQ7yySPA+dT5$xMc#|zzXMo`0!J@A`PLlg-_UX@f9o|!v0gk_|Oyi2ckeR$A z7FXmHMmlk#thps{0d##Sj^`IiLa{MuR;ZYl;dNj?kcxVwRF%z@U#aJyn*G*Jl znjLlwW118k1~FN(Yq^p9B>nXjPS$;5lsKR>Umk$MVr4v^qvQfDFPT_jQ|v*Wnl?vV zW3A+~S)7tF6#|ks3~NrK7VzY#f?78!_Aon~U%Lc0!at(1K8;+tz_%i@lz-brXGFj% cI7ZNE`+>*7P!v#W=yCsYq33_#H2LcP0#Gr9jQ{`u literal 0 HcmV?d00001 From 05d81f078ea229cac8513eb993cdd96942d647e2 Mon Sep 17 00:00:00 2001 From: The Top Dev Date: Wed, 29 Oct 2025 05:35:08 +0300 Subject: [PATCH 2/4] Add Week 7 finetuning solution: Complete QLoRA fine-tuning notebook - Added Week7_Complete_FineTuning.ipynb with comprehensive QLoRA implementation - Optimized for Colab Pro with enhanced GPU utilization and memory efficiency - Implements 4-bit QLoRA fine-tuning of open-source LLMs for product price prediction - Includes advanced training techniques (gradient checkpointing, bf16, xformers) - Features memory optimizations and comprehensive evaluation framework - Ready for submission and grading --- .../Week7_Complete_FineTuning.ipynb | 1984 +++++++++++++++++ 1 file changed, 1984 insertions(+) create mode 100644 week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb diff --git a/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb b/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb new file mode 100644 index 0000000..67ede7e --- /dev/null +++ b/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb @@ -0,0 +1,1984 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c88d0ea8", + "metadata": { + "id": "c88d0ea8" + }, + "source": [ + "# Week 7 - Complete Fine-tuning with Open Source LLMs\n", + "\n", + "This notebook implements QLoRA fine-tuning of open-source LLMs for product price prediction.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "721835a5", + "metadata": { + "id": "721835a5" + }, + "outputs": [], + "source": [ + "%pip install -q -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n", + "%pip install -q -U transformers>=4.45.0 accelerate>=0.33.0 peft>=0.11.1 trl>=0.8.0\n", + "%pip install -q -U datasets \"huggingface_hub>=0.23.2,<1.0\" sentencepiece einops safetensors\n", + "%pip install -q -U bitsandbytes>=0.43.2 xformers\n", + "%pip install -q -U wandb tensorboard" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8a8017b0", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8a8017b0", + "outputId": "6c5288b6-3d15-4439-de01-ad2ff7b2b262" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "PyTorch version: 2.8.0+cu126\n", + "CUDA available: True\n", + "GPU: NVIDIA A100-SXM4-40GB\n", + "GPU Memory: 42.5 GB\n", + "CUDA version: 12.6\n" + ] + } + ], + "source": [ + "# Core imports\n", + "import os\n", + "import torch\n", + "import pickle\n", + "import numpy as np\n", + "import json\n", + "import re\n", + "from datetime import datetime\n", + "from datasets import Dataset\n", + "from transformers import (\n", + " AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig,\n", + " TrainingArguments, Trainer, DataCollatorForLanguageModeling\n", + ")\n", + "from peft import LoraConfig, TaskType, get_peft_model, PeftModel\n", + "from trl import SFTTrainer\n", + "import transformers\n", + "import wandb\n", + "\n", + "# Enable optimizations for Colab Pro\n", + "torch.backends.cudnn.benchmark = True\n", + "torch.backends.cuda.matmul.allow_tf32 = True\n", + "torch.backends.cudnn.allow_tf32 = True\n", + "\n", + "print(f\"PyTorch version: {torch.__version__}\")\n", + "print(f\"CUDA available: {torch.cuda.is_available()}\")\n", + "if torch.cuda.is_available():\n", + " print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n", + " print(f\"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB\")\n", + " print(f\"CUDA version: {torch.version.cuda}\")\n", + "else:\n", + " raise SystemExit(\"❌ No GPU detected.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "0b4d0cd3", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 280 + }, + "id": "0b4d0cd3", + "outputId": "65ab54e5-4fec-4db8-e6e9-3fb86d2a13f3" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Using Colab secrets\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "Note: Environment variable`HF_TOKEN` is set and is the current active token independently from the token you've just configured.\n", + "WARNING:huggingface_hub._login:Note: Environment variable`HF_TOKEN` is set and is the current active token independently from the token you've just configured.\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Finishing previous runs because reinit is set to 'default'." + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + " View run wobbly-resonance-1 at: https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning/runs/fwkqveds
View project at: https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning
Synced 5 W&B file(s), 0 media file(s), 0 artifact file(s) and 0 other file(s)" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Find logs at: ./wandb/run-20251028_115212-fwkqveds/logs" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Tracking run with wandb version 0.22.2" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Run data is saved locally in /content/wandb/run-20251028_115650-rd1q63l3" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Syncing run easy-cloud-2 to Weights & Biases (docs)
" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + " View project at https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + " View run at https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning/runs/rd1q63l3" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ W&B initialized\n" + ] + } + ], + "source": [ + "# Environment setup for Colab Pro\n", + "try:\n", + " from google.colab import userdata\n", + " os.environ['HF_TOKEN'] = userdata.get('HF_TOKEN')\n", + " os.environ['WANDB_API_KEY'] = userdata.get('WANDB_API_KEY')\n", + " print(\"✅ Using Colab secrets\")\n", + "except:\n", + " from dotenv import load_dotenv\n", + " load_dotenv(override=True)\n", + " os.environ['HF_TOKEN'] = os.getenv('HF_TOKEN', 'your-hf-token')\n", + " os.environ['WANDB_API_KEY'] = os.getenv('WANDB_API_KEY', 'your-wandb-key')\n", + " print(\"✅ Using local environment\")\n", + "\n", + "# Login to HuggingFace\n", + "from huggingface_hub import login\n", + "login(os.environ['HF_TOKEN'])\n", + "\n", + "# Initialize Weights & Biases (optional)\n", + "try:\n", + " wandb.init(project=\"colab-pro-finetuning\", mode=\"online\")\n", + " print(\"✅ W&B initialized\")\n", + "except:\n", + " print(\"⚠️ W&B not available, continuing without logging\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "809d2271", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "809d2271", + "outputId": "2afd08ed-5da7-4a93-99cd-4d8f881bd0af" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "📦 Loading pre-processed pickle files...\n", + "✅ Loaded training data: train.pkl (150 items)\n", + "✅ Loaded test data: test.pkl (50 items)\n", + "✅ Loaded validation data: validation.pkl (50 items)\n", + "\n", + "📊 Dataset Statistics:\n", + " Training: 150 items\n", + " Test: 50 items\n", + " Validation: 50 items\n" + ] + } + ], + "source": [ + "# Load pre-processed pickle files (optimized for Colab Pro)\n", + "def load_pickle_data():\n", + " \"\"\"Load pre-processed pickle files with robust error handling\"\"\"\n", + " print(\"📦 Loading pre-processed pickle files...\")\n", + "\n", + " # Try multiple locations for pickle files\n", + " pickle_files = [\n", + " 'train.pkl', 'test.pkl', 'validation.pkl'\n", + " ]\n", + "\n", + " train = None\n", + " test = None\n", + " validation = None\n", + "\n", + " # Load training data\n", + " for file_path in ['train.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " train = pickle.load(f)\n", + " print(f\"✅ Loaded training data: {file_path} ({len(train)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + "\n", + " # Load test data\n", + " for file_path in ['test.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " test = pickle.load(f)\n", + " print(f\"✅ Loaded test data: {file_path} ({len(test)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + "\n", + " # Load validation data\n", + " for file_path in ['validation.pkl']:\n", + " if os.path.exists(file_path):\n", + " try:\n", + " with open(file_path, 'rb') as f:\n", + " validation = pickle.load(f)\n", + " print(f\"✅ Loaded validation data: {file_path} ({len(validation)} items)\")\n", + " break\n", + " except Exception as e:\n", + " print(f\"❌ Error loading {file_path}: {e}\")\n", + "\n", + " # If no pickle files found, create sample data\n", + " if not train or not test or not validation:\n", + " print(\"🔄 No pickle files found, creating sample data...\")\n", + " train, test, validation = create_sample_data()\n", + "\n", + " return train, test, validation\n", + "\n", + "def create_sample_data():\n", + " \"\"\"Create sample data for demonstration\"\"\"\n", + " # Sample product data (expanded for better training)\n", + " sample_products = [\n", + " {\"title\": \"Wireless Bluetooth Headphones\", \"price\": 89.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Stainless Steel Water Bottle\", \"price\": 24.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Organic Cotton T-Shirt\", \"price\": 19.99, \"category\": \"Clothing\"},\n", + " {\"title\": \"Ceramic Coffee Mug\", \"price\": 12.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"LED Desk Lamp\", \"price\": 45.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Yoga Mat\", \"price\": 29.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Leather Wallet\", \"price\": 39.99, \"category\": \"Accessories\"},\n", + " {\"title\": \"Bluetooth Speaker\", \"price\": 79.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Kitchen Knife Set\", \"price\": 129.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Running Shoes\", \"price\": 89.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Smartphone Case\", \"price\": 15.99, \"category\": \"Electronics\"},\n", + " {\"title\": \"Coffee Maker\", \"price\": 89.99, \"category\": \"Home & Kitchen\"},\n", + " {\"title\": \"Backpack\", \"price\": 49.99, \"category\": \"Accessories\"},\n", + " {\"title\": \"Tennis Racket\", \"price\": 79.99, \"category\": \"Sports & Outdoors\"},\n", + " {\"title\": \"Laptop Stand\", \"price\": 34.99, \"category\": \"Electronics\"}\n", + " ]\n", + "\n", + " # Create SimpleItem objects\n", + " items = []\n", + " for product in sample_products:\n", + " item = SimpleItem(\n", + " title=product['title'],\n", + " description=f\"High-quality {product['title'].lower()}\",\n", + " price=product['price'],\n", + " category=product['category'],\n", + " token_count=len(product['title'] + f\"High-quality {product['title'].lower()}\") // 4\n", + " )\n", + " items.append(item)\n", + "\n", + " # Split into train/test/validation\n", + " train = items[:10] # 10 items\n", + " test = items[10:13] # 3 items\n", + " validation = items[13:] # 2 items\n", + "\n", + " print(f\"✅ Created sample data: {len(train)} train, {len(test)} test, {len(validation)} validation\")\n", + " return train, test, validation\n", + "\n", + "# SimpleItem class definition for pickle compatibility\n", + "class SimpleItem:\n", + " \"\"\"Simple item class for pickle compatibility\"\"\"\n", + " def __init__(self, title, description, price, category=\"Human_Generated\", token_count=0):\n", + " self.title = title\n", + " self.description = description\n", + " self.price = price\n", + " self.category = category\n", + " self.token_count = token_count\n", + "\n", + " def test_prompt(self):\n", + " \"\"\"Return a prompt suitable for testing\"\"\"\n", + " return f\"How much does this cost to the nearest dollar?\\n\\n{self.title}\\n\\n{self.description}\\n\\nPrice is $\"\n", + "\n", + " def __repr__(self):\n", + " return f\"SimpleItem(title='{self.title[:50]}...', price=${self.price})\"\n", + "\n", + "# Load the data\n", + "train, test, validation = load_pickle_data()\n", + "\n", + "print(f\"\\n📊 Dataset Statistics:\")\n", + "print(f\" Training: {len(train)} items\")\n", + "print(f\" Test: {len(test)} items\")\n", + "print(f\" Validation: {len(validation)} items\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "946a3a05", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "946a3a05", + "outputId": "41936ca5-d092-43a2-ed29-d66607af7d89" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Datasets prepared:\n", + " Training: 150 examples\n", + " Validation: 50 examples\n", + " Sample training text: <|system|>\n", + "You are a retail price estimator. Predict the most likely new retail price in USD.\n", + "<|user...\n" + ] + } + ], + "source": [ + "# Prepare datasets for training (optimized for Colab Pro)\n", + "def prepare_training_data(items):\n", + " \"\"\"Convert items to training format\"\"\"\n", + " data = []\n", + " for item in items:\n", + " # Create training prompt\n", + " prompt = f\"<|system|>\\nYou are a retail price estimator. Predict the most likely new retail price in USD.\\n<|user|>\\n{item.title}\\n{item.description}\\n<|assistant|>\\n${item.price:.2f}\"\n", + " data.append({\"text\": prompt})\n", + " return data\n", + "\n", + "# Prepare training and validation datasets\n", + "train_data = prepare_training_data(train)\n", + "val_data = prepare_training_data(validation)\n", + "\n", + "# Convert to HuggingFace datasets\n", + "train_ds = Dataset.from_list(train_data)\n", + "val_ds = Dataset.from_list(val_data)\n", + "\n", + "print(f\"✅ Datasets prepared:\")\n", + "print(f\" Training: {len(train_ds)} examples\")\n", + "print(f\" Validation: {len(val_ds)} examples\")\n", + "print(f\" Sample training text: {train_ds[0]['text'][:100]}...\")\n" + ] + }, + { + "cell_type": "code", + "source": [ + "# Tokenize datasets for causal LM (creates input_ids, attention_mask, labels)\n", + "MAX_LEN = 256 # Further reduced for stability\n", + "\n", + "def tokenize_function(examples):\n", + " # Tokenize with padding and truncation\n", + " outputs = tokenizer(\n", + " examples[\"text\"],\n", + " truncation=True,\n", + " max_length=MAX_LEN,\n", + " padding=\"max_length\", # Pad to max_length\n", + " return_tensors=None, # Return lists, not tensors\n", + " )\n", + " # Labels are the shifted inputs for causal LM\n", + " outputs[\"labels\"] = outputs[\"input_ids\"].copy()\n", + " return outputs\n", + "\n", + "def ensure_consistent_lengths(dataset, max_len):\n", + " \"\"\"Ensure all sequences in dataset have consistent length\"\"\"\n", + " def pad_sequences(examples):\n", + " # Convert to lists if they're tensors\n", + " input_ids = []\n", + " attention_masks = []\n", + " labels = []\n", + "\n", + " for i in range(len(examples[\"input_ids\"])):\n", + " # Get the sequence and convert to list if tensor\n", + " seq = examples[\"input_ids\"][i]\n", + " attn = examples[\"attention_mask\"][i]\n", + " lbl = examples[\"labels\"][i]\n", + "\n", + " # Convert tensors to lists\n", + " if hasattr(seq, 'tolist'):\n", + " seq = seq.tolist()\n", + " if hasattr(attn, 'tolist'):\n", + " attn = attn.tolist()\n", + " if hasattr(lbl, 'tolist'):\n", + " lbl = lbl.tolist()\n", + "\n", + " # Truncate if too long\n", + " if len(seq) > max_len:\n", + " seq = seq[:max_len]\n", + " attn = attn[:max_len]\n", + " lbl = lbl[:max_len]\n", + "\n", + " # Pad if too short\n", + " while len(seq) < max_len:\n", + " seq.append(tokenizer.pad_token_id)\n", + " attn.append(0) # 0 for padding\n", + " lbl.append(-100) # -100 for padding in labels (ignored in loss)\n", + "\n", + " input_ids.append(seq)\n", + " attention_masks.append(attn)\n", + " labels.append(lbl)\n", + "\n", + " return {\n", + " \"input_ids\": input_ids,\n", + " \"attention_mask\": attention_masks,\n", + " \"labels\": labels\n", + " }\n", + "\n", + " return dataset.map(pad_sequences, batched=True)\n", + "\n", + "print(\"🔄 Checking dataset status...\")\n", + "print(f\"Training dataset columns: {train_ds.column_names}\")\n", + "print(f\"Validation dataset columns: {val_ds.column_names}\")\n", + "\n", + "# Check if we need to tokenize or just ensure consistent lengths\n", + "if \"text\" in train_ds.column_names:\n", + " print(\"🔄 Tokenizing datasets...\")\n", + " train_ds = train_ds.map(tokenize_function, batched=True, remove_columns=[\"text\"])\n", + " val_ds = val_ds.map(tokenize_function, batched=True, remove_columns=[\"text\"])\n", + " print(\"✅ Tokenization complete\")\n", + "else:\n", + " print(\"✅ Datasets already tokenized\")\n", + "\n", + "# Ensure consistent lengths\n", + "print(\"🔄 Ensuring consistent sequence lengths...\")\n", + "train_ds = ensure_consistent_lengths(train_ds, MAX_LEN)\n", + "val_ds = ensure_consistent_lengths(val_ds, MAX_LEN)\n", + "\n", + "# Verify all sequences are the same length\n", + "print(\"🔍 Verifying sequence lengths...\")\n", + "train_lengths = [len(seq) for seq in train_ds[\"input_ids\"]]\n", + "val_lengths = [len(seq) for seq in val_ds[\"input_ids\"]]\n", + "\n", + "print(f\"Training sequence lengths - Min: {min(train_lengths)}, Max: {max(train_lengths)}\")\n", + "print(f\"Validation sequence lengths - Min: {min(val_lengths)}, Max: {max(val_lengths)}\")\n", + "\n", + "if len(set(train_lengths)) == 1 and len(set(val_lengths)) == 1:\n", + " print(\"✅ All sequences have consistent length\")\n", + "else:\n", + " print(\"⚠️ Inconsistent sequence lengths detected - this will cause training errors\")\n", + "\n", + "# Set format for PyTorch\n", + "train_ds.set_format(type=\"torch\", columns=[\"input_ids\", \"attention_mask\", \"labels\"])\n", + "val_ds.set_format(type=\"torch\", columns=[\"input_ids\", \"attention_mask\", \"labels\"])\n", + "\n", + "print(f\"Sample input_ids shape: {train_ds[0]['input_ids'].shape}\")\n", + "print(f\"Sample attention_mask shape: {train_ds[0]['attention_mask'].shape}\")\n", + "print(f\"Sample labels shape: {train_ds[0]['labels'].shape}\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 289, + "referenced_widgets": [ + "245570c62c3844728d7125a706fbbc9b", + "8d95da3803e542f8b855175013d497ba", + "34a89db126a64690bc2f6c8656ba2210", + "4c47ce21b5a14328aa22403782e4da9b", + "7dff366d9e71427dbae40b1dce7a9bfa", + "0614c35b3690494ca3b8f9ab71d71a08", + "42315e83fbac49c2bc7f2faf1abcc22e", + "4cdff5bdf7574795802e821aa42f3c4e", + "754aa440f45c4a878d99572368d659c8", + "8b5f0c156a9641cfa5413668a0b97b9c", + "aff498bd632f4036958f59cfc6587ea3", + "d6825cc926a24f2482ce72c15242081e", + "24b2b5f5d92049a79014b8278e97451b", + "a6001d34e58a47cab0d8bff2451afb6e", + "b42e8d8b61d7431a814a03c5e07a1166", + "9ce1659c776140bcaf3c16eae6f70967", + "cceae79c145d4b73a64e80ad3fc8866c", + "56bc56071ff04223935dc2d98d2703ab", + "67cacc87afe14250baaa073289fb4a8f", + "227eea7074544adbb2c34b9dde340fa5", + "8bd9aebb2cc5420094b2b441a5183523", + "1c3eb3793b6e4291b4fa57ce8419ef1f" + ] + }, + "id": "zWgL4fhku_XN", + "outputId": "8d375a13-59cf-4eea-f16f-fde5dbf7f0e8" + }, + "id": "zWgL4fhku_XN", + "execution_count": 40, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "🔄 Checking dataset status...\n", + "Training dataset columns: ['input_ids', 'attention_mask', 'labels']\n", + "Validation dataset columns: ['input_ids', 'attention_mask', 'labels']\n", + "✅ Datasets already tokenized\n", + "🔄 Ensuring consistent sequence lengths...\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Map: 0%| | 0/150 [00:00" + ], + "text/html": [ + "\n", + "

\n", + " \n", + " \n", + " [30/30 00:40, Epoch 3/3]\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
StepTraining LossValidation Loss

" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Training completed!\n", + "Model saved to: ./outputs\n" + ] + } + ], + "source": [ + "# Start training\n", + "print(\"🚀 Starting training...\")\n", + "print(f\"Training on: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}\")\n", + "print(f\"Batch size: {training_args.per_device_train_batch_size}\")\n", + "print(f\"Gradient accumulation: {training_args.gradient_accumulation_steps}\")\n", + "print(f\"Effective batch size: {training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps}\")\n", + "\n", + "# Train the model\n", + "trainer.train()\n", + "\n", + "print(\"✅ Training completed!\")\n", + "print(f\"Model saved to: {training_args.output_dir}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "a4df3b21", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "a4df3b21", + "outputId": "3cb4ac09-9b61-4ab8-bc11-cf480cb764be" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Model and tokenizer saved\n", + "Saved to: ./outputs\n", + "Mounted at /content/drive\n", + "✅ Model also saved to Google Drive: /content/drive/MyDrive/Colab Notebooks/finetuned_model_20251028_123003\n" + ] + } + ], + "source": [ + "# Save the final model\n", + "trainer.save_model()\n", + "tokenizer.save_pretrained(training_args.output_dir)\n", + "\n", + "print(\"✅ Model and tokenizer saved\")\n", + "print(f\"Saved to: {training_args.output_dir}\")\n", + "\n", + "# Save to Google Drive (optional)\n", + "try:\n", + " from google.colab import drive\n", + " drive.mount('/content/drive')\n", + "\n", + " # Copy to Drive\n", + " import shutil\n", + " drive_path = f\"/content/drive/MyDrive/Colab Notebooks/finetuned_model_{datetime.now().strftime('%Y%m%d_%H%M%S')}\"\n", + " shutil.copytree(training_args.output_dir, drive_path)\n", + " print(f\"✅ Model also saved to Google Drive: {drive_path}\")\n", + "except:\n", + " print(\"⚠️ Google Drive not available, model saved locally only\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "e2507760", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 228 + }, + "id": "e2507760", + "outputId": "03dda8b6-711c-4bee-b4ed-b00aedde5ab4" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "📊 Evaluating model...\n", + "⚠️ Best checkpoint not found, using final model\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "

\n", + " \n", + " \n", + " [25/25 00:01]\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "📈 Evaluation Results:\n", + " eval_loss: 5.8997\n", + " eval_runtime: 1.5263\n", + " eval_samples_per_second: 32.7600\n", + " eval_steps_per_second: 16.3800\n", + " epoch: 3.0000\n", + "\n", + "✅ Evaluation completed!\n" + ] + } + ], + "source": [ + "# Evaluate the model\n", + "print(\"📊 Evaluating model...\")\n", + "\n", + "# Load the best model\n", + "best_model_path = f\"{training_args.output_dir}/checkpoint-best\"\n", + "if os.path.exists(best_model_path):\n", + " model = PeftModel.from_pretrained(model, best_model_path)\n", + " print(\"✅ Loaded best checkpoint\")\n", + "else:\n", + " print(\"⚠️ Best checkpoint not found, using final model\")\n", + "\n", + "# Run evaluation\n", + "eval_results = trainer.evaluate()\n", + "print(f\"\\n📈 Evaluation Results:\")\n", + "for key, value in eval_results.items():\n", + " print(f\" {key}: {value:.4f}\")\n", + "\n", + "print(\"\\n✅ Evaluation completed!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "c80bebe1", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "c80bebe1", + "outputId": "75b9cf98-cae9-48c0-de41-eb245f574e0c" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "🧪 Testing inference...\n", + "\n", + "--- Test 1 ---\n", + "Item: MyCableMart 3.5mm Plug/Jack, 4 Conductor TRRS, Self Solder, Male\n", + "Actual Price: $25.00\n", + "Model Response: <|system|>\n", + "You are a retail price estimator. Predict the most likely new retail price in USD.\n", + "<|user|>\n", + "MyCableMart 3.5mm Plug/Jack, 4 Conductor TRRS, Self Solder, Male\n", + "Connects stereo audio & microphone devices requiring 4 conductors (left and right audio and microphone plus ground). This connector MAY also be suitable for left/right audio 1 video (composite) and ground. Great for making your own 3.5mm 4 conductor Cables or for repairing existing cables. Wire terminals are attached using solder (not included).Features 3.5mm 4 conductor (3 band) plug 3.5mm 4 conductor (3 band) plug Nickel Plated Nickel Plated Strain relief Strain relief Outer Dimensions (at PVC outer molding) Outer Dimensions (at PVC outer molding) Outer Dimensions (with PVC outer molding\n", + "<|assistant|>\n", + "input.5, 3.00,,5,2,2,2,2,2\n", + "\n", + "--- Test 2 ---\n", + "Item: OtterBox + Pop Symmetry Series Case for iPhone 11 Pro (ONLY) - Retail Packaging - White Marble\n", + "Actual Price: $20.00\n", + "Model Response: <|system|>\n", + "You are a retail price estimator. Predict the most likely new retail price in USD.\n", + "<|user|>\n", + "OtterBox + Pop Symmetry Series Case for iPhone 11 Pro (ONLY) - Retail Packaging - White Marble\n", + "OtterBox + Pop Symmetry Series Case for iPhone 11 Pro (ONLY) - Retail Packaging - White Marble Compatible with iPhone 11 Pro Thin one-piece case with durable protection against drops, bumps and fumbles that is also compatible with Qi wireless charging PopSockets PopGrip is integrated into case to help with holding, texting, snapping better pictures and hand-free viewing PopTop designs are easy to switch out — just close flat, press down and turn to swap the PopTop. Includes OtterBox limited lifetime warranty (see website for details) and 100% authentic Dimensions 7.8 x 4.29 x 1.06 inches, Weight 3\n", + "<|assistant|>\n", + "Type.html,.html.html,, Material, width, material, material, material,\n", + "\n", + "--- Test 3 ---\n", + "Item: Dell XPS Desktop ( Intel Core i7 4790 (3.6 GHz), 8GB, 1TB HDD,Windows 10 Home Black\n", + "Actual Price: $500.00\n", + "Model Response: <|system|>\n", + "You are a retail price estimator. Predict the most likely new retail price in USD.\n", + "<|user|>\n", + "Dell XPS Desktop ( Intel Core i7 4790 (3.6 GHz), 8GB, 1TB HDD,Windows 10 Home Black\n", + "Product description Bring your multimedia to life with Dell XPS desktop PCs offering powerful processors, superb graphics performance and lots of storage space. Amazon.com Processor 4th Generation Intel Core processor (8M Cache, up to 4.00 GHz) OS Windows 7 Professional, English Graphics Card NVIDIA GeForce GTX 750Ti 2GB DDR5 Memory 32GB Dual Channel DDR3 - 4 DIMMs Hard Drive 1TB 7200 RPM SATA Hard Drive 6.0 Gb/s + 256GB SSD Processor 3.6 GHz RAM 8 GB DDR5, Memory Speed 1600 MHz,\n", + "<|assistant|>\n", + "USB HDD,RAM, HDD.8GB2,USB HDD,USB HDD,USB HDD,\n", + "\n", + "✅ Inference testing completed!\n" + ] + } + ], + "source": [ + "# Test inference on sample data\n", + "print(\"🧪 Testing inference...\")\n", + "\n", + "def test_inference(model, tokenizer, test_item):\n", + " \"\"\"Test inference on a single item\"\"\"\n", + " prompt = f\"<|system|>\\nYou are a retail price estimator. Predict the most likely new retail price in USD.\\n<|user|>\\n{test_item.title}\\n{test_item.description}\\n<|assistant|>\\n\"\n", + "\n", + " inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n", + "\n", + " with torch.no_grad():\n", + " outputs = model.generate(\n", + " **inputs,\n", + " max_new_tokens=20,\n", + " temperature=0.7,\n", + " do_sample=True,\n", + " pad_token_id=tokenizer.eos_token_id\n", + " )\n", + "\n", + " response = tokenizer.decode(outputs[0], skip_special_tokens=True)\n", + " return response\n", + "\n", + "# Test on a few examples\n", + "for i, item in enumerate(test[:3]):\n", + " print(f\"\\n--- Test {i+1} ---\")\n", + " print(f\"Item: {item.title}\")\n", + " print(f\"Actual Price: ${item.price:.2f}\")\n", + "\n", + " try:\n", + " response = test_inference(model, tokenizer, item)\n", + " print(f\"Model Response: {response}\")\n", + " except Exception as e:\n", + " print(f\"Error: {e}\")\n", + "\n", + "print(\"\\n✅ Inference testing completed!\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + }, + "colab": { + "provenance": [], + "gpuType": "A100" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "accelerator": "GPU", + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "245570c62c3844728d7125a706fbbc9b": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_8d95da3803e542f8b855175013d497ba", + "IPY_MODEL_34a89db126a64690bc2f6c8656ba2210", + "IPY_MODEL_4c47ce21b5a14328aa22403782e4da9b" + ], + "layout": "IPY_MODEL_7dff366d9e71427dbae40b1dce7a9bfa" + } + }, + "8d95da3803e542f8b855175013d497ba": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0614c35b3690494ca3b8f9ab71d71a08", + "placeholder": "​", + "style": "IPY_MODEL_42315e83fbac49c2bc7f2faf1abcc22e", + "value": "Map: 100%" + } + }, + "34a89db126a64690bc2f6c8656ba2210": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4cdff5bdf7574795802e821aa42f3c4e", + "max": 150, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_754aa440f45c4a878d99572368d659c8", + "value": 150 + } + }, + "4c47ce21b5a14328aa22403782e4da9b": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8b5f0c156a9641cfa5413668a0b97b9c", + "placeholder": "​", + "style": "IPY_MODEL_aff498bd632f4036958f59cfc6587ea3", + "value": " 150/150 [00:00<00:00, 1904.80 examples/s]" + } + }, + "7dff366d9e71427dbae40b1dce7a9bfa": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0614c35b3690494ca3b8f9ab71d71a08": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "42315e83fbac49c2bc7f2faf1abcc22e": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "4cdff5bdf7574795802e821aa42f3c4e": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "754aa440f45c4a878d99572368d659c8": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "8b5f0c156a9641cfa5413668a0b97b9c": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "aff498bd632f4036958f59cfc6587ea3": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "d6825cc926a24f2482ce72c15242081e": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_24b2b5f5d92049a79014b8278e97451b", + "IPY_MODEL_a6001d34e58a47cab0d8bff2451afb6e", + "IPY_MODEL_b42e8d8b61d7431a814a03c5e07a1166" + ], + "layout": "IPY_MODEL_9ce1659c776140bcaf3c16eae6f70967" + } + }, + "24b2b5f5d92049a79014b8278e97451b": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_cceae79c145d4b73a64e80ad3fc8866c", + "placeholder": "​", + "style": "IPY_MODEL_56bc56071ff04223935dc2d98d2703ab", + "value": "Map: 100%" + } + }, + "a6001d34e58a47cab0d8bff2451afb6e": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_67cacc87afe14250baaa073289fb4a8f", + "max": 50, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_227eea7074544adbb2c34b9dde340fa5", + "value": 50 + } + }, + "b42e8d8b61d7431a814a03c5e07a1166": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8bd9aebb2cc5420094b2b441a5183523", + "placeholder": "​", + "style": "IPY_MODEL_1c3eb3793b6e4291b4fa57ce8419ef1f", + "value": " 50/50 [00:00<00:00, 1509.88 examples/s]" + } + }, + "9ce1659c776140bcaf3c16eae6f70967": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cceae79c145d4b73a64e80ad3fc8866c": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "56bc56071ff04223935dc2d98d2703ab": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "67cacc87afe14250baaa073289fb4a8f": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "227eea7074544adbb2c34b9dde340fa5": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "8bd9aebb2cc5420094b2b441a5183523": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1c3eb3793b6e4291b4fa57ce8419ef1f": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 9d8fca6eb4c8ecf085ed732614174f172f4cf49b Mon Sep 17 00:00:00 2001 From: The Top Dev Date: Thu, 30 Oct 2025 00:38:42 +0300 Subject: [PATCH 3/4] feat(week8): add ensemble-joshua assignment (API, Modal, agents); omit large RF model --- .../ensemble-joshua/agents/agent.py | 35 ++++ .../ensemble-joshua/agents/deals.py | 111 +++++++++++++ .../ensemble-joshua/agents/ensemble_agent.py | 57 +++++++ .../ensemble-joshua/agents/messaging_agent.py | 80 ++++++++++ .../ensemble-joshua/agents/planning_agent.py | 58 +++++++ .../agents/random_forest_agent.py | 46 ++++++ .../ensemble-joshua/agents/scanner_agent.py | 95 +++++++++++ .../ensemble-joshua/api.py | 75 +++++++++ .../ensemble-joshua/ensemble_model.pkl | Bin 0 -> 929 bytes .../ensemble-joshua/frontier_agent.py | 150 ++++++++++++++++++ .../ensemble-joshua/pricer_service2.py | 98 ++++++++++++ 11 files changed, 805 insertions(+) create mode 100644 week8/community_contributions/ensemble-joshua/agents/agent.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/deals.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/ensemble_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/messaging_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/planning_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/random_forest_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/agents/scanner_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/api.py create mode 100644 week8/community_contributions/ensemble-joshua/ensemble_model.pkl create mode 100644 week8/community_contributions/ensemble-joshua/frontier_agent.py create mode 100644 week8/community_contributions/ensemble-joshua/pricer_service2.py diff --git a/week8/community_contributions/ensemble-joshua/agents/agent.py b/week8/community_contributions/ensemble-joshua/agents/agent.py new file mode 100644 index 0000000..8f376fa --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/agent.py @@ -0,0 +1,35 @@ +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) + + diff --git a/week8/community_contributions/ensemble-joshua/agents/deals.py b/week8/community_contributions/ensemble-joshua/agents/deals.py new file mode 100644 index 0000000..acfcb74 --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/deals.py @@ -0,0 +1,111 @@ +from pydantic import BaseModel +from typing import List, Dict, Self +from bs4 import BeautifulSoup +import re +import feedparser +from tqdm import tqdm +import requests +import time + +feeds = [ + "https://www.dealnews.com/c142/Electronics/?rss=1", + "https://www.dealnews.com/c39/Computers/?rss=1", + "https://www.dealnews.com/c238/Automotive/?rss=1", + "https://www.dealnews.com/f1912/Smart-Home/?rss=1", + "https://www.dealnews.com/c196/Home-Garden/?rss=1", + ] + +def extract(html_snippet: str) -> str: + """ + Use Beautiful Soup to clean up this HTML snippet and extract useful text + """ + soup = BeautifulSoup(html_snippet, 'html.parser') + snippet_div = soup.find('div', class_='snippet summary') + + if snippet_div: + description = snippet_div.get_text(strip=True) + description = BeautifulSoup(description, 'html.parser').get_text() + description = re.sub('<[^<]+?>', '', description) + result = description.strip() + else: + result = html_snippet + return result.replace('\n', ' ') + +class ScrapedDeal: + """ + A class to represent a Deal retrieved from an RSS feed + """ + category: str + title: str + summary: str + url: str + details: str + features: str + + def __init__(self, entry: Dict[str, str]): + """ + Populate this instance based on the provided dict + """ + self.title = entry['title'] + self.summary = extract(entry['summary']) + self.url = entry['links'][0]['href'] + stuff = requests.get(self.url).content + soup = BeautifulSoup(stuff, 'html.parser') + content = soup.find('div', class_='content-section').get_text() + content = content.replace('\nmore', '').replace('\n', ' ') + if "Features" in content: + self.details, self.features = content.split("Features") + else: + self.details = content + self.features = "" + + def __repr__(self): + """ + Return a string to describe this deal + """ + return f"<{self.title}>" + + def describe(self): + """ + Return a longer string to describe this deal for use in calling a model + """ + return f"Title: {self.title}\nDetails: {self.details.strip()}\nFeatures: {self.features.strip()}\nURL: {self.url}" + + @classmethod + def fetch(cls, show_progress : bool = False) -> List[Self]: + """ + Retrieve all deals from the selected RSS feeds + """ + deals = [] + feed_iter = tqdm(feeds) if show_progress else feeds + for feed_url in feed_iter: + feed = feedparser.parse(feed_url) + for entry in feed.entries[:10]: + deals.append(cls(entry)) + time.sleep(0.5) + return deals + +class Deal(BaseModel): + """ + A class to Represent a Deal with a summary description + """ + product_description: str + price: float + url: str + +class DealSelection(BaseModel): + """ + A class to Represent a list of Deals + """ + deals: List[Deal] + +class Opportunity(BaseModel): + """ + A class to represent a possible opportunity: a Deal where we estimate + it should cost more than it's being offered + """ + deal: Deal + estimate: float + discount: float + + diff --git a/week8/community_contributions/ensemble-joshua/agents/ensemble_agent.py b/week8/community_contributions/ensemble-joshua/agents/ensemble_agent.py new file mode 100644 index 0000000..84add6b --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/ensemble_agent.py @@ -0,0 +1,57 @@ +import pandas as pd +from sklearn.linear_model import LinearRegression +import joblib +import os + +from agents.agent import Agent +from agents.specialist_agent import SpecialistAgent +from agents.frontier_agent import FrontierAgent +from agents.random_forest_agent import RandomForestAgent + +class EnsembleAgent(Agent): + + name = "Ensemble Agent" + color = Agent.YELLOW + + def __init__(self, collection): + """ + Create an instance of Ensemble, by creating each of the models + And loading the weights of the Ensemble + """ + self.log("Initializing Ensemble Agent") + self.specialist = SpecialistAgent() + self.frontier = FrontierAgent(collection) + self.random_forest = RandomForestAgent() + # Resolve model path: prefer local contribution folder copy, fallback to week8 root + candidate_paths = [ + os.path.join(os.path.dirname(os.path.dirname(__file__)), 'ensemble_model.pkl'), # ../../ensemble_model.pkl + os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'ensemble_model.pkl'), # ../../../ensemble_model.pkl (week8 root) + 'ensemble_model.pkl', + ] + model_path = next((p for p in candidate_paths if os.path.exists(p)), candidate_paths[-1]) + self.model = joblib.load(model_path) + self.log("Ensemble Agent is ready") + + def price(self, description: str) -> float: + """ + Run this ensemble model + Ask each of the models to price the product + Then use the Linear Regression model to return the weighted price + :param description: the description of a product + :return: an estimate of its price + """ + self.log("Running Ensemble Agent - collaborating with specialist, frontier and random forest agents") + specialist = self.specialist.price(description) + frontier = self.frontier.price(description) + random_forest = self.random_forest.price(description) + X = pd.DataFrame({ + 'Specialist': [specialist], + 'Frontier': [frontier], + 'RandomForest': [random_forest], + 'Min': [min(specialist, frontier, random_forest)], + 'Max': [max(specialist, frontier, random_forest)], + }) + y = max(0, self.model.predict(X)[0]) + self.log(f"Ensemble Agent complete - returning ${y:.2f}") + return y + diff --git a/week8/community_contributions/ensemble-joshua/agents/messaging_agent.py b/week8/community_contributions/ensemble-joshua/agents/messaging_agent.py new file mode 100644 index 0000000..2c51d8d --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/messaging_agent.py @@ -0,0 +1,80 @@ +import os +# from twilio.rest import Client +from agents.deals import Opportunity +import http.client +import urllib +from agents.agent import Agent + +# Uncomment the Twilio lines if you wish to use Twilio + +DO_TEXT = False +DO_PUSH = True + +class MessagingAgent(Agent): + + name = "Messaging Agent" + color = Agent.WHITE + + def __init__(self): + """ + Set up this object to either do push notifications via Pushover, + or SMS via Twilio, + whichever is specified in the constants + """ + self.log(f"Messaging Agent is initializing") + if DO_TEXT: + account_sid = os.getenv('TWILIO_ACCOUNT_SID', 'your-sid-if-not-using-env') + auth_token = os.getenv('TWILIO_AUTH_TOKEN', 'your-auth-if-not-using-env') + self.me_from = os.getenv('TWILIO_FROM', 'your-phone-number-if-not-using-env') + self.me_to = os.getenv('MY_PHONE_NUMBER', 'your-phone-number-if-not-using-env') + # self.client = Client(account_sid, auth_token) + self.log("Messaging Agent has initialized Twilio") + if DO_PUSH: + self.pushover_user = os.getenv('PUSHOVER_USER', 'your-pushover-user-if-not-using-env') + self.pushover_token = os.getenv('PUSHOVER_TOKEN', 'your-pushover-user-if-not-using-env') + self.log("Messaging Agent has initialized Pushover") + + def message(self, text): + """ + Send an SMS message using the Twilio API + """ + self.log("Messaging Agent is sending a text message") + message = self.client.messages.create( + from_=self.me_from, + body=text, + to=self.me_to + ) + + def push(self, text): + """ + Send a Push Notification using the Pushover API + """ + self.log("Messaging Agent is sending a push notification") + conn = http.client.HTTPSConnection("api.pushover.net:443") + conn.request("POST", "/1/messages.json", + urllib.parse.urlencode({ + "token": self.pushover_token, + "user": self.pushover_user, + "message": text, + "sound": "cashregister" + }), { "Content-type": "application/x-www-form-urlencoded" }) + conn.getresponse() + + def alert(self, opportunity: Opportunity): + """ + Make an alert about the specified Opportunity + """ + text = f"Deal Alert! Price=${opportunity.deal.price:.2f}, " + text += f"Estimate=${opportunity.estimate:.2f}, " + text += f"Discount=${opportunity.discount:.2f} :" + text += opportunity.deal.product_description[:10]+'... ' + text += opportunity.deal.url + if DO_TEXT: + self.message(text) + if DO_PUSH: + self.push(text) + self.log("Messaging Agent has completed") + + + + diff --git a/week8/community_contributions/ensemble-joshua/agents/planning_agent.py b/week8/community_contributions/ensemble-joshua/agents/planning_agent.py new file mode 100644 index 0000000..891a06d --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/planning_agent.py @@ -0,0 +1,58 @@ +from typing import Optional, List +from agents.agent import Agent +from agents.deals import ScrapedDeal, DealSelection, Deal, Opportunity +from agents.scanner_agent import ScannerAgent +from agents.ensemble_agent import EnsembleAgent +from agents.messaging_agent import MessagingAgent + + +class PlanningAgent(Agent): + + name = "Planning Agent" + color = Agent.GREEN + DEAL_THRESHOLD = 50 + + def __init__(self, collection): + """ + Create instances of the 3 Agents that this planner coordinates across + """ + self.log("Planning Agent is initializing") + self.scanner = ScannerAgent() + self.ensemble = EnsembleAgent(collection) + self.messenger = MessagingAgent() + self.log("Planning Agent is ready") + + def run(self, deal: Deal) -> Opportunity: + """ + Run the workflow for a particular deal + :param deal: the deal, summarized from an RSS scrape + :returns: an opportunity including the discount + """ + self.log("Planning Agent is pricing up a potential deal") + estimate = self.ensemble.price(deal.product_description) + discount = estimate - deal.price + self.log(f"Planning Agent has processed a deal with discount ${discount:.2f}") + return Opportunity(deal=deal, estimate=estimate, discount=discount) + + def plan(self, memory: List[str] = []) -> Optional[Opportunity]: + """ + Run the full workflow: + 1. Use the ScannerAgent to find deals from RSS feeds + 2. Use the EnsembleAgent to estimate them + 3. Use the MessagingAgent to send a notification of deals + :param memory: a list of URLs that have been surfaced in the past + :return: an Opportunity if one was surfaced, otherwise None + """ + self.log("Planning Agent is kicking off a run") + selection = self.scanner.scan(memory=memory) + if selection: + opportunities = [self.run(deal) for deal in selection.deals[:5]] + opportunities.sort(key=lambda opp: opp.discount, reverse=True) + best = opportunities[0] + self.log(f"Planning Agent has identified the best deal has discount ${best.discount:.2f}") + if best.discount > self.DEAL_THRESHOLD: + self.messenger.alert(best) + self.log("Planning Agent has completed a run") + return best if best.discount > self.DEAL_THRESHOLD else None + return None + diff --git a/week8/community_contributions/ensemble-joshua/agents/random_forest_agent.py b/week8/community_contributions/ensemble-joshua/agents/random_forest_agent.py new file mode 100644 index 0000000..a114f3a --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/random_forest_agent.py @@ -0,0 +1,46 @@ +# imports + +import os +import re +from typing import List +from sentence_transformers import SentenceTransformer +import joblib +import os +from agents.agent import Agent + + + +class RandomForestAgent(Agent): + + name = "Random Forest Agent" + color = Agent.MAGENTA + + def __init__(self): + """ + Initialize this object by loading in the saved model weights + and the SentenceTransformer vector encoding model + """ + self.log("Random Forest Agent is initializing") + self.vectorizer = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') + # Resolve model path: prefer local contribution folder copy, fallback to week8 root + candidate_paths = [ + os.path.join(os.path.dirname(os.path.dirname(__file__)), 'random_forest_model.pkl'), # ../../random_forest_model.pkl + os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'random_forest_model.pkl'), # ../../../random_forest_model.pkl (week8 root) + 'random_forest_model.pkl', + ] + model_path = next((p for p in candidate_paths if os.path.exists(p)), candidate_paths[-1]) + self.model = joblib.load(model_path) + self.log("Random Forest Agent is ready") + + def price(self, description: str) -> float: + """ + Use a Random Forest model to estimate the price of the described item + :param description: the product to be estimated + :return: the price as a float + """ + self.log("Random Forest Agent is starting a prediction") + vector = self.vectorizer.encode([description]) + result = max(0, self.model.predict(vector)[0]) + self.log(f"Random Forest Agent completed - predicting ${result:.2f}") + return result + diff --git a/week8/community_contributions/ensemble-joshua/agents/scanner_agent.py b/week8/community_contributions/ensemble-joshua/agents/scanner_agent.py new file mode 100644 index 0000000..2b34207 --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/agents/scanner_agent.py @@ -0,0 +1,95 @@ +import os +import json +from typing import Optional, List +from openai import OpenAI +from agents.deals import ScrapedDeal, DealSelection +from agents.agent import Agent + + +class ScannerAgent(Agent): + + MODEL = "gpt-4o-mini" + + SYSTEM_PROMPT = """You identify and summarize the 5 most detailed deals from a list, by selecting deals that have the most detailed, high quality description and the most clear price. + Respond strictly in JSON with no explanation, using this format. You should provide the price as a number derived from the description. If the price of a deal isn't clear, do not include that deal in your response. + Most important is that you respond with the 5 deals that have the most detailed product description with price. It's not important to mention the terms of the deal; most important is a thorough description of the product. + Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price. + + {"deals": [ + { + "product_description": "Your clearly expressed summary of the product in 4-5 sentences. Details of the item are much more important than why it's a good deal. Avoid mentioning discounts and coupons; focus on the item itself. There should be a paragpraph of text for each item you choose.", + "price": 99.99, + "url": "the url as provided" + }, + ... + ]}""" + + USER_PROMPT_PREFIX = """Respond with the most promising 5 deals from this list, selecting those which have the most detailed, high quality product description and a clear price that is greater than 0. + Respond strictly in JSON, and only JSON. You should rephrase the description to be a summary of the product itself, not the terms of the deal. + Remember to respond with a paragraph of text in the product_description field for each of the 5 items that you select. + Be careful with products that are described as "$XXX off" or "reduced by $XXX" - this isn't the actual price of the product. Only respond with products when you are highly confident about the price. + + Deals: + + """ + + USER_PROMPT_SUFFIX = "\n\nStrictly respond in JSON and include exactly 5 deals, no more." + + name = "Scanner Agent" + color = Agent.CYAN + + def __init__(self): + """ + Set up this instance by initializing OpenAI + """ + self.log("Scanner Agent is initializing") + self.openai = OpenAI() + self.log("Scanner Agent is ready") + + def fetch_deals(self, memory) -> List[ScrapedDeal]: + """ + Look up deals published on RSS feeds + Return any new deals that are not already in the memory provided + """ + self.log("Scanner Agent is about to fetch deals from RSS feed") + urls = [opp.deal.url for opp in memory] + scraped = ScrapedDeal.fetch() + result = [scrape for scrape in scraped if scrape.url not in urls] + self.log(f"Scanner Agent received {len(result)} deals not already scraped") + return result + + def make_user_prompt(self, scraped) -> str: + """ + Create a user prompt for OpenAI based on the scraped deals provided + """ + user_prompt = self.USER_PROMPT_PREFIX + user_prompt += '\n\n'.join([scrape.describe() for scrape in scraped]) + user_prompt += self.USER_PROMPT_SUFFIX + return user_prompt + + def scan(self, memory: List[str]=[]) -> Optional[DealSelection]: + """ + Call OpenAI to provide a high potential list of deals with good descriptions and prices + Use StructuredOutputs to ensure it conforms to our specifications + :param memory: a list of URLs representing deals already raised + :return: a selection of good deals, or None if there aren't any + """ + scraped = self.fetch_deals(memory) + if scraped: + user_prompt = self.make_user_prompt(scraped) + self.log("Scanner Agent is calling OpenAI using Structured Output") + result = self.openai.beta.chat.completions.parse( + model=self.MODEL, + messages=[ + {"role": "system", "content": self.SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt} + ], + response_format=DealSelection + ) + result = result.choices[0].message.parsed + result.deals = [deal for deal in result.deals if deal.price>0] + self.log(f"Scanner Agent received {len(result.deals)} selected deals with price>0 from OpenAI") + return result + return None + + diff --git a/week8/community_contributions/ensemble-joshua/api.py b/week8/community_contributions/ensemble-joshua/api.py new file mode 100644 index 0000000..0b604ea --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/api.py @@ -0,0 +1,75 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +import os +import chromadb + +from agents.specialist_agent import SpecialistAgent +from agents.frontier_agent import FrontierAgent +from agents.random_forest_agent import RandomForestAgent +from agents.ensemble_agent import EnsembleAgent +from deal_agent_framework import DealAgentFramework + + +class PriceRequest(BaseModel): + description: str + + +class DealScanResponse(BaseModel): + opportunities: list + + +DB_PATH = os.path.join(os.path.dirname(__file__), "../../products_vectorstore") +client = chromadb.PersistentClient(path=DB_PATH) +collection = client.get_or_create_collection("products") + +app = FastAPI(title="Week8 Pricer API", version="1.0.0") + + +@app.get("/healthz") +def healthz(): + return {"ok": True} + + +@app.post("/price/specialist") +def price_specialist(body: PriceRequest): + if not body.description: + raise HTTPException(400, "description is required") + agent = SpecialistAgent() + price = float(agent.price(body.description)) + return {"price": price, "agent": "specialist"} + + +@app.post("/price/frontier") +def price_frontier(body: PriceRequest): + if not body.description: + raise HTTPException(400, "description is required") + agent = FrontierAgent(collection) + price = float(agent.price(body.description)) + return {"price": price, "agent": "frontier"} + + +@app.post("/price/random_forest") +def price_random_forest(body: PriceRequest): + if not body.description: + raise HTTPException(400, "description is required") + agent = RandomForestAgent() + price = float(agent.price(body.description)) + return {"price": price, "agent": "random_forest"} + + +@app.post("/price/ensemble") +def price_ensemble(body: PriceRequest): + if not body.description: + raise HTTPException(400, "description is required") + agent = EnsembleAgent(collection) + price = float(agent.price(body.description)) + return {"price": price, "agent": "ensemble"} + + +@app.post("/deals/scan") +def deals_scan(): + framework = DealAgentFramework() + opportunities = framework.run() + return {"count": len(opportunities), "opportunities": [o.dict() for o in opportunities]} + + diff --git a/week8/community_contributions/ensemble-joshua/ensemble_model.pkl b/week8/community_contributions/ensemble-joshua/ensemble_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..94efeec05be715d7a2e3b1bd6c99b470123638ee GIT binary patch literal 929 zcmb7@F=!M)6o&6|+2o8lxeya2Hqk2AEGS4K1aBo0#nTg_U@XEoyEm6zvpc)a%qBTd ziAXfjvGN^hVz9N4q_MD&Sct?z5F67)5K#+JK}Z>A_i_=H6izYwX7&`eC`NmoWQJXD-$e2Mi9jw;p!#? zvR(PPj;YhoQ&$C!rO-gO6{F=b#&G2V!$CmUZ;CVzP0PicgVf?vAe(6kN50BQYTU*F zfr2)zY@e0XQJEZ2c1cv!zTH;0lyAe?KA4@k4MWgVNXg_?C>D#SHq?lxLSBF=Td?c; z6Ug&$pi^h^=BUB)>A_#{a&{63E3Gt>&WsM(Y??WPK{EwB1SBq za=}7N#L)jHQbPI?1(B_%ejD9jzZ-EWO|VKR5+0)twWfWd~Z;( zMSoIf92(7d5{OD$ubTBNc2Trhw_3>xn$#?xlo8mOT4UH*yWQR$6L#8r?7eoEY$*Ss zZ2w=%CR7N!d&b@$N!A!CF4t1o^oP&+mBTw1U(LhvkCl$a{*O?zefV+rw;~una}SO$ ze7S4foGf3NxPHgz>ora`caIs>OZ9jyTb-}kcH7c6aSJOFRa84SkbXb+e9q`-H{h1) YF_}f14-btK7C$Yi9n^dELlBz!FX)JRc>n+a literal 0 HcmV?d00001 diff --git a/week8/community_contributions/ensemble-joshua/frontier_agent.py b/week8/community_contributions/ensemble-joshua/frontier_agent.py new file mode 100644 index 0000000..e1e9858 --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/frontier_agent.py @@ -0,0 +1,150 @@ +# imports + +import os +import re +import math +import json +from typing import List, Dict +from openai import OpenAI +try: + from openai import APIStatusError + APIStatusError = Exception +import statistics +from sentence_transformers import SentenceTransformer +from datasets import load_dataset +import chromadb +from items import Item +from testing import Tester +from agents.agent import Agent + + +class FrontierAgent(Agent): + + name = "Frontier Agent" + color = Agent.BLUE + + MODEL = "gpt-4o-mini" + + def __init__(self, collection): + """ + Set up this instance by connecting to OpenAI or DeepSeek, to the Chroma Datastore, + And setting up the vector encoding model + """ + self.log("Initializing Frontier Agent") + deepseek_api_key = os.getenv("DEEPSEEK_API_KEY") + if deepseek_api_key: + self.client = OpenAI(api_key=deepseek_api_key, base_url="https://api.deepseek.com") + self.MODEL = "deepseek-chat" + self.log("Frontier Agent is set up with DeepSeek") + else: + self.client = OpenAI() + self.MODEL = "gpt-4o-mini" + self.log("Frontier Agent is setting up with OpenAI") + self.collection = collection + self.model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') + self.log("Frontier Agent is ready") + + def make_context(self, similars: List[str], prices: List[float]) -> str: + """ + Create context that can be inserted into the prompt + :param similars: similar products to the one being estimated + :param prices: prices of the similar products + :return: text to insert in the prompt that provides context + """ + message = "To provide some context, here are some other items that might be similar to the item you need to estimate.\n\n" + for similar, price in zip(similars, prices): + message += f"Potentially related product:\n{similar}\nPrice is ${price:.2f}\n\n" + return message + + def messages_for(self, description: str, similars: List[str], prices: List[float]) -> List[Dict[str, str]]: + """ + Create the message list to be included in a call to OpenAI + With the system and user prompt + :param description: a description of the product + :param similars: similar products to this one + :param prices: prices of similar products + :return: the list of messages in the format expected by OpenAI + """ + system_message = "You estimate prices of items. Reply only with the price, no explanation" + user_prompt = self.make_context(similars, prices) + user_prompt += "And now the question for you:\n\n" + user_prompt += "How much does this cost?\n\n" + description + return [ + {"role": "system", "content": system_message}, + {"role": "user", "content": user_prompt}, + {"role": "assistant", "content": "Price is $"} + ] + + def find_similars(self, description: str): + """ + Return a list of items similar to the given one by looking in the Chroma datastore + """ + self.log("Frontier Agent is performing a RAG search of the Chroma datastore to find 5 similar products") + vector = self.model.encode([description]) + results = self.collection.query(query_embeddings=vector.astype(float).tolist(), n_results=5) + documents = results['documents'][0][:] + prices = [m['price'] for m in results['metadatas'][0][:]] + self.log("Frontier Agent has found similar products") + return documents, prices + + def get_price(self, s) -> float: + """ + A utility that plucks a floating point number out of a string + """ + s = s.replace('$','').replace(',','') + match = re.search(r"[-+]?\d*\.\d+|\d+", s) + return float(match.group()) if match else 0.0 + + def price(self, description: str) -> float: + """ + Make a call to OpenAI or DeepSeek to estimate the price of the described product, + by looking up 5 similar products and including them in the prompt to give context + :param description: a description of the product + :return: an estimate of the price + """ + documents, prices = self.find_similars(description) + + # If external calls are disabled, or similar pricing is empty, use heuristic + allow_external = os.getenv("FRONTIER_ALLOW_EXTERNAL", "true").lower() in {"1", "true", "yes"} + + def heuristic_price() -> float: + if prices: + # Robust central tendency fallback + try: + return float(statistics.median(prices)) + except Exception: + return float(sum(prices) / max(len(prices), 1)) + # As a last resort, return 0.0 + return 0.0 + + if not allow_external: + self.log("External LLM calls disabled via FRONTIER_ALLOW_EXTERNAL; using heuristic fallback") + result = heuristic_price() + self.log(f"Frontier Agent (fallback) - predicting ${result:.2f}") + return result + + self.log(f"Frontier Agent is about to call {self.MODEL} with context including 5 similar products") + try: + response = self.client.chat.completions.create( + model=self.MODEL, + messages=self.messages_for(description, documents, prices), + seed=42, + max_tokens=5, + ) + reply = response.choices[0].message.content + result = self.get_price(reply) + self.log(f"Frontier Agent completed - predicting ${result:.2f}") + return result + except APIStatusError as e: # Insufficient balance or other HTTP errors + msg = getattr(e, "message", str(e)) + self.log(f"Frontier Agent API error: {msg}. Falling back to heuristic price.") + result = heuristic_price() + self.log(f"Frontier Agent (fallback) - predicting ${result:.2f}") + return result + except Exception as e: + self.log(f"Frontier Agent unexpected error: {e}. Falling back to heuristic price.") + result = heuristic_price() + self.log(f"Frontier Agent (fallback) - predicting ${result:.2f}") + return result + + diff --git a/week8/community_contributions/ensemble-joshua/pricer_service2.py b/week8/community_contributions/ensemble-joshua/pricer_service2.py new file mode 100644 index 0000000..8bdd854 --- /dev/null +++ b/week8/community_contributions/ensemble-joshua/pricer_service2.py @@ -0,0 +1,98 @@ +import modal +from modal import App, Volume, Image + + +app = modal.App("pricer-service") +image = Image.debian_slim().pip_install("huggingface", "torch", "transformers", "bitsandbytes", "accelerate", "peft") + +secrets = [modal.Secret.from_name("hf-secret")] + +# Constants +GPU = "T4" +BASE_MODEL = "meta-llama/Meta-Llama-3.1-8B" +PROJECT_NAME = "pricer" +HF_USER = "ed-donner" +RUN_NAME = "2024-09-13_13.04.39" +PROJECT_RUN_NAME = f"{PROJECT_NAME}-{RUN_NAME}" +REVISION = "e8d637df551603dc86cd7a1598a8f44af4d7ae36" +FINETUNED_MODEL = f"{HF_USER}/{PROJECT_RUN_NAME}" +CACHE_DIR = "/cache" + + +MIN_CONTAINERS = 0 + +QUESTION = "How much does this cost to the nearest dollar?" +PREFIX = "Price is $" + +hf_cache_volume = Volume.from_name("hf-hub-cache", create_if_missing=True) + +@app.cls( + image=image.env({"HF_HUB_CACHE": CACHE_DIR}), + secrets=secrets, + gpu=GPU, + timeout=1800, + min_containers=MIN_CONTAINERS, + volumes={CACHE_DIR: hf_cache_volume} +) +class Pricer: + + @modal.enter() + def setup(self): + import torch + from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, set_seed + from peft import PeftModel + + # Quant Config + 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 model and tokenizer + self.tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL) + self.tokenizer.pad_token = self.tokenizer.eos_token + self.tokenizer.padding_side = "right" + self.base_model = AutoModelForCausalLM.from_pretrained( + BASE_MODEL, + quantization_config=quant_config, + device_map="auto" + ) + self.fine_tuned_model = PeftModel.from_pretrained(self.base_model, FINETUNED_MODEL, revision=REVISION) + + @modal.method() + def price(self, description: str) -> float: + import os + import re + import torch + from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, set_seed + from peft import PeftModel + + set_seed(42) + 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") + 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]) + + 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 + + +# Simple HTTP endpoint so external apps can call this on Modal +@app.function(image=image, secrets=secrets, gpu=GPU, timeout=1800) +@modal.web_endpoint(method="POST") +def price_http(body: dict): + """HTTP endpoint: {"description": str} -> {"price": float}""" + description = body.get("description", '').strip() + if not description: + return {"error": "Missing 'description'"} + + pricer = Pricer() + value = pricer.price.remote(description) + return {"price": float(value)} + + From 91c568cf634b0f967003a3548b160a1b64a0c464 Mon Sep 17 00:00:00 2001 From: The Top Dev Date: Thu, 30 Oct 2025 13:52:52 +0300 Subject: [PATCH 4/4] Finetune using Llama3.1 --- .../Week7_Complete_FineTuning.ipynb | 2010 ++++++++++------- 1 file changed, 1148 insertions(+), 862 deletions(-) diff --git a/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb b/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb index 67ede7e..bb1e960 100644 --- a/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb +++ b/week7/community_contributions/finetuning-joshua/Week7_Complete_FineTuning.ipynb @@ -1,5 +1,107 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d2afa3e9", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluation utilities for the fine-tuned open-source model (Week 7)\n", + "import re\n", + "import math\n", + "import numpy as np\n", + "import torch\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Extract numeric price from model output\n", + "def extract_price(text: str) -> float:\n", + " text = (text or \"\").replace(\"$\", \"\").replace(\",\", \"\")\n", + " m = re.search(r\"[-+]?\\d*\\.\\d+|\\d+\", text)\n", + " return float(m.group(0)) if m else 0.0\n", + "\n", + "# Build prompt consistent with Week 7 training template\n", + "def build_pricing_prompt(item) -> str:\n", + " # Matches the training format used in Week 7\n", + " return (\n", + " \"<|system|>\\nYou are a retail price estimator. Predict the most likely new retail price in USD.\\n\"\n", + " \"<|user|>\\n\"\n", + " f\"{item.title}\\n{item.description}\\n\"\n", + " \"<|assistant|>\\n\"\n", + " )\n", + "\n", + "# Single-item prediction using the fine-tuned causal LM\n", + "@torch.no_grad()\n", + "def predict_price(model, tokenizer, item, max_new_tokens: int = 20) -> float:\n", + " prompt = build_pricing_prompt(item)\n", + " inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n", + " outputs = model.generate(\n", + " **inputs,\n", + " max_new_tokens=max_new_tokens,\n", + " temperature=0.7,\n", + " do_sample=True,\n", + " pad_token_id=tokenizer.eos_token_id,\n", + " )\n", + " decoded = tokenizer.decode(outputs[0], skip_special_tokens=True)\n", + " # Take only the newly generated continuation beyond the prompt\n", + " continuation = decoded[len(tokenizer.decode(inputs[\"input_ids\"][0], skip_special_tokens=True)) :]\n", + " return extract_price(continuation)\n", + "\n", + "# Batch evaluation (MAE, RMSE, MAPE) with quick scatter plot\n", + "def evaluate_model(model, tokenizer, test_items, limit: int = None, title: str = \"Fine-tuned Model Evaluation\"):\n", + " if not test_items:\n", + " print(\"⚠️ No test items available.\")\n", + " return {\"mae\": None, \"rmse\": None, \"mape\": None}\n", + "\n", + " items = test_items[:limit] if limit else test_items\n", + "\n", + " y_true, y_pred = [], []\n", + " for i, item in enumerate(items):\n", + " try:\n", + " pred = predict_price(model, tokenizer, item)\n", + " except Exception as e:\n", + " print(f\"Error on item {i}: {e}\")\n", + " pred = 0.0\n", + " y_true.append(float(getattr(item, \"price\", 0.0)))\n", + " y_pred.append(float(pred))\n", + "\n", + " y_true_np = np.array(y_true, dtype=float)\n", + " y_pred_np = np.array(y_pred, dtype=float)\n", + "\n", + " mae = float(np.mean(np.abs(y_pred_np - y_true_np)))\n", + " rmse = float(np.sqrt(np.mean((y_pred_np - y_true_np) ** 2)))\n", + " with np.errstate(divide='ignore', invalid='ignore'):\n", + " mape_arr = np.where(y_true_np != 0, np.abs((y_pred_np - y_true_np) / y_true_np), np.nan)\n", + " mape = float(np.nanmean(mape_arr)) * 100.0\n", + "\n", + " print(f\"\\n📈 {title}\")\n", + " print(f\"MAE : {mae:.2f}\")\n", + " print(f\"RMSE: {rmse:.2f}\")\n", + " print(f\"MAPE: {mape:.2f}%\")\n", + "\n", + " # Scatter plot\n", + " try:\n", + " plt.figure(figsize=(6, 6))\n", + " plt.scatter(y_true_np, y_pred_np, alpha=0.6)\n", + " mx = max(y_true_np.max() if y_true_np.size else 0, y_pred_np.max() if y_pred_np.size else 0)\n", + " plt.plot([0, mx], [0, mx], 'r--', label='Ideal')\n", + " plt.xlabel('Actual Price')\n", + " plt.ylabel('Predicted Price')\n", + " plt.title(title)\n", + " plt.legend()\n", + " plt.tight_layout()\n", + " plt.show()\n", + " except Exception as e:\n", + " print(f\"Plotting error: {e}\")\n", + "\n", + " return {\"mae\": mae, \"rmse\": rmse, \"mape\": mape}\n", + "\n", + "# Convenience wrapper mirroring Week 6's Tester usage pattern\n", + "# Usage:\n", + "# results = evaluate_model(model, tokenizer, test, limit=len(test))\n", + "print(\"✅ Evaluation utilities for Week 7 added. Use evaluate_model(model, tokenizer, test, limit=len(test)).\")\n" + ] + }, { "cell_type": "markdown", "id": "c88d0ea8", @@ -41,8 +143,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "PyTorch version: 2.8.0+cu126\n", "CUDA available: True\n", @@ -100,139 +202,139 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "✅ Using Colab secrets\n" ] }, { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "Note: Environment variable`HF_TOKEN` is set and is the current active token independently from the token you've just configured.\n", "WARNING:huggingface_hub._login:Note: Environment variable`HF_TOKEN` is set and is the current active token independently from the token you've just configured.\n" ] }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Finishing previous runs because reinit is set to 'default'." + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { + "text/html": [], "text/plain": [ "" - ], - "text/html": [] + ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ " View run wobbly-resonance-1 at: https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning/runs/fwkqveds
View project at: https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning
Synced 5 W&B file(s), 0 media file(s), 0 artifact file(s) and 0 other file(s)" - ] - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { + ], "text/plain": [ "" - ], + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { "text/html": [ "Find logs at: ./wandb/run-20251028_115212-fwkqveds/logs" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { + "text/html": [], "text/plain": [ "" - ], - "text/html": [] + ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Tracking run with wandb version 0.22.2" - ] - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { + ], "text/plain": [ "" - ], + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { "text/html": [ "Run data is saved locally in /content/wandb/run-20251028_115650-rd1q63l3" - ] - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { + ], "text/plain": [ "" - ], + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { "text/html": [ "Syncing run easy-cloud-2 to Weights & Biases (docs)
" - ] - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { + ], "text/plain": [ "" - ], + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { "text/html": [ " View project at https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning" - ] - }, - "metadata": {} - }, - { - "output_type": "display_data", - "data": { + ], "text/plain": [ "" - ], - "text/html": [ - " View run at https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning/runs/rd1q63l3" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + " View run at https://wandb.ai/oluoch-joshua-udemy/colab-pro-finetuning/runs/rd1q63l3" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "✅ W&B initialized\n" ] @@ -277,8 +379,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "📦 Loading pre-processed pickle files...\n", "✅ Loaded training data: train.pkl (150 items)\n", @@ -427,8 +529,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "✅ Datasets prepared:\n", " Training: 150 examples\n", @@ -466,6 +568,94 @@ }, { "cell_type": "code", + "execution_count": 40, + "id": "zWgL4fhku_XN", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 289, + "referenced_widgets": [ + "245570c62c3844728d7125a706fbbc9b", + "8d95da3803e542f8b855175013d497ba", + "34a89db126a64690bc2f6c8656ba2210", + "4c47ce21b5a14328aa22403782e4da9b", + "7dff366d9e71427dbae40b1dce7a9bfa", + "0614c35b3690494ca3b8f9ab71d71a08", + "42315e83fbac49c2bc7f2faf1abcc22e", + "4cdff5bdf7574795802e821aa42f3c4e", + "754aa440f45c4a878d99572368d659c8", + "8b5f0c156a9641cfa5413668a0b97b9c", + "aff498bd632f4036958f59cfc6587ea3", + "d6825cc926a24f2482ce72c15242081e", + "24b2b5f5d92049a79014b8278e97451b", + "a6001d34e58a47cab0d8bff2451afb6e", + "b42e8d8b61d7431a814a03c5e07a1166", + "9ce1659c776140bcaf3c16eae6f70967", + "cceae79c145d4b73a64e80ad3fc8866c", + "56bc56071ff04223935dc2d98d2703ab", + "67cacc87afe14250baaa073289fb4a8f", + "227eea7074544adbb2c34b9dde340fa5", + "8bd9aebb2cc5420094b2b441a5183523", + "1c3eb3793b6e4291b4fa57ce8419ef1f" + ] + }, + "id": "zWgL4fhku_XN", + "outputId": "8d375a13-59cf-4eea-f16f-fde5dbf7f0e8" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔄 Checking dataset status...\n", + "Training dataset columns: ['input_ids', 'attention_mask', 'labels']\n", + "Validation dataset columns: ['input_ids', 'attention_mask', 'labels']\n", + "✅ Datasets already tokenized\n", + "🔄 Ensuring consistent sequence lengths...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "245570c62c3844728d7125a706fbbc9b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map: 0%| | 0/150 [00:00" - ], "text/html": [ "\n", "
\n", @@ -1035,13 +1133,17 @@ " \n", " \n", "

" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "✅ Training completed!\n", "Model saved to: ./outputs\n" @@ -1076,8 +1178,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "✅ Model and tokenizer saved\n", "Saved to: ./outputs\n", @@ -1122,19 +1224,15 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "📊 Evaluating model...\n", "⚠️ Best checkpoint not found, using final model\n" ] }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "\n", "

\n", @@ -1143,13 +1241,17 @@ " [25/25 00:01]\n", "
\n", " " + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "\n", "📈 Evaluation Results:\n", @@ -1197,8 +1299,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "🧪 Testing inference...\n", "\n", @@ -1275,644 +1377,211 @@ "\n", "print(\"\\n✅ Inference testing completed!\")\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e716982", + "metadata": {}, + "outputs": [], + "source": [ + "# Fixed evaluation with price range constraints and better post-processing\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import re\n", + "import torch\n", + "\n", + "def extract_price_safe(text: str) -> float:\n", + " \"\"\"Extract price with safety constraints\"\"\"\n", + " if not text:\n", + " return 0.0\n", + " \n", + " # Clean the text\n", + " text = str(text).replace(\"$\", \"\").replace(\",\", \"\").strip()\n", + " \n", + " # Look for price patterns\n", + " patterns = [\n", + " r'\\$?(\\d+\\.?\\d*)\\s*(?:dollars?|USD|usd)?', # $123.45 or 123.45 dollars\n", + " r'(\\d+\\.?\\d*)', # Just numbers\n", + " ]\n", + " \n", + " for pattern in patterns:\n", + " matches = re.findall(pattern, text, re.IGNORECASE)\n", + " if matches:\n", + " try:\n", + " price = float(matches[0])\n", + " # Apply reasonable price constraints\n", + " if 0.01 <= price <= 100000: # Between 1 cent and $100k\n", + " return price\n", + " except ValueError:\n", + " continue\n", + " \n", + " return 0.0\n", + "\n", + "def build_pricing_prompt_fixed(item) -> str:\n", + " \"\"\"Build prompt with explicit price range guidance\"\"\"\n", + " return (\n", + " \"<|system|>\\n\"\n", + " \"You are a retail price estimator. Predict the most likely new retail price in USD. \"\n", + " \"Typical prices range from $1 to $10,000. Be realistic and conservative.\\n\"\n", + " \"<|user|>\\n\"\n", + " f\"Product: {item.title}\\n\"\n", + " f\"Description: {item.description}\\n\"\n", + " f\"Category: {getattr(item, 'category', 'Unknown')}\\n\"\n", + " \"What is the retail price?\\n\"\n", + " \"<|assistant|>\\n\"\n", + " \"The retail price is $\"\n", + " )\n", + "\n", + "@torch.no_grad()\n", + "def predict_price_fixed(model, tokenizer, item, max_new_tokens=15) -> float:\n", + " \"\"\"Predict price with better constraints\"\"\"\n", + " prompt = build_pricing_prompt_fixed(item)\n", + " inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n", + " \n", + " # Generate with more conservative settings\n", + " outputs = model.generate(\n", + " **inputs,\n", + " max_new_tokens=max_new_tokens,\n", + " temperature=0.3, # Lower temperature for more conservative predictions\n", + " do_sample=True,\n", + " pad_token_id=tokenizer.eos_token_id,\n", + " repetition_penalty=1.1, # Reduce repetition\n", + " no_repeat_ngram_size=2,\n", + " )\n", + " \n", + " # Decode only the new tokens\n", + " prompt_length = len(tokenizer.decode(inputs[\"input_ids\"][0], skip_special_tokens=True))\n", + " full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)\n", + " new_text = full_response[prompt_length:]\n", + " \n", + " # Extract price with constraints\n", + " price = extract_price_safe(new_text)\n", + " \n", + " # Additional safety: if price is still unreasonable, use a fallback\n", + " if price > 50000: # If over $50k, it's probably wrong\n", + " # Try to extract a more reasonable number\n", + " numbers = re.findall(r'\\d+\\.?\\d*', new_text)\n", + " if numbers:\n", + " try:\n", + " # Take the first reasonable number\n", + " for num in numbers:\n", + " candidate = float(num)\n", + " if 1 <= candidate <= 10000:\n", + " return candidate\n", + " except ValueError:\n", + " pass\n", + " return 0.0\n", + " \n", + " return price\n", + "\n", + "def evaluate_model_fixed(model, tokenizer, test_items, limit=None, title=\"Fixed Fine-tuned Model\"):\n", + " \"\"\"Evaluate with fixed price extraction\"\"\"\n", + " if not test_items:\n", + " print(\"⚠️ No test items available.\")\n", + " return {\"mae\": None, \"rmse\": None, \"mape\": None}\n", + " \n", + " items = test_items[:limit] if limit else test_items\n", + " print(f\"🔍 Evaluating on {len(items)} items...\")\n", + " \n", + " y_true, y_pred = [], []\n", + " errors = []\n", + " \n", + " for i, item in enumerate(items):\n", + " try:\n", + " pred = predict_price_fixed(model, tokenizer, item)\n", + " true_price = float(getattr(item, \"price\", 0.0))\n", + " \n", + " y_true.append(true_price)\n", + " y_pred.append(pred)\n", + " \n", + " # Track individual errors for debugging\n", + " error = abs(pred - true_price)\n", + " errors.append({\n", + " 'item': i,\n", + " 'title': getattr(item, 'title', 'Unknown')[:50],\n", + " 'true': true_price,\n", + " 'pred': pred,\n", + " 'error': error\n", + " })\n", + " \n", + " except Exception as e:\n", + " print(f\"Error on item {i}: {e}\")\n", + " y_true.append(0.0)\n", + " y_pred.append(0.0)\n", + " \n", + " y_true = np.array(y_true, dtype=float)\n", + " y_pred = np.array(y_pred, dtype=float)\n", + " \n", + " # Calculate metrics\n", + " mae = float(np.mean(np.abs(y_pred - y_true)))\n", + " rmse = float(np.sqrt(np.mean((y_pred - y_true) ** 2)))\n", + " \n", + " # MAPE (avoid division by zero)\n", + " mape = float(np.mean(np.abs((y_true - y_pred) / np.maximum(y_true, 1.0)))) * 100\n", + " \n", + " # Hits within 15% tolerance\n", + " tolerance = 0.15\n", + " hits = float(np.mean(np.abs(y_pred - y_true) <= (tolerance * np.maximum(y_true, 1.0)))) * 100\n", + " \n", + " # Create scatter plot\n", + " plt.figure(figsize=(8, 6))\n", + " plt.scatter(y_true, y_pred, alpha=0.7, s=30, c='blue')\n", + " \n", + " # Add diagonal line\n", + " max_val = max(y_true.max() if y_true.size else 0, y_pred.max() if y_pred.size else 0, 1)\n", + " plt.plot([0, max_val], [0, max_val], 'r--', alpha=0.8, label='Perfect Prediction')\n", + " \n", + " plt.xlabel('True Price ($)')\n", + " plt.ylabel('Predicted Price ($)')\n", + " plt.title(f'{title}\\nMAE=${mae:.2f} RMSE=${rmse:.2f} MAPE={mape:.1f}% Hits={hits:.1f}%')\n", + " plt.legend()\n", + " plt.grid(True, alpha=0.3)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + " # Show worst predictions\n", + " errors.sort(key=lambda x: x['error'], reverse=True)\n", + " print(f\"\\n🔍 Top 5 Worst Predictions:\")\n", + " for i, err in enumerate(errors[:5]):\n", + " print(f\" {i+1}. {err['title']}...\")\n", + " print(f\" True: ${err['true']:.2f}, Pred: ${err['pred']:.2f}, Error: ${err['error']:.2f}\")\n", + " \n", + " return {\n", + " \"mae\": mae,\n", + " \"rmse\": rmse, \n", + " \"mape\": mape,\n", + " \"hits_pct\": hits,\n", + " \"y_true\": y_true,\n", + " \"y_pred\": y_pred,\n", + " \"errors\": errors\n", + " }\n", + "\n", + "# Test the fixed evaluation\n", + "print(\"🧪 Testing fixed price prediction...\")\n", + "results = evaluate_model_fixed(model, tokenizer, test, limit=20, title=\"Fixed Fine-tuned Model\")\n" + ] } ], "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "A100", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, "language_info": { "name": "python" }, - "colab": { - "provenance": [], - "gpuType": "A100" - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "accelerator": "GPU", "widgets": { "application/vnd.jupyter.widget-state+json": { - "245570c62c3844728d7125a706fbbc9b": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HBoxModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_8d95da3803e542f8b855175013d497ba", - "IPY_MODEL_34a89db126a64690bc2f6c8656ba2210", - "IPY_MODEL_4c47ce21b5a14328aa22403782e4da9b" - ], - "layout": "IPY_MODEL_7dff366d9e71427dbae40b1dce7a9bfa" - } - }, - "8d95da3803e542f8b855175013d497ba": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_0614c35b3690494ca3b8f9ab71d71a08", - "placeholder": "​", - "style": "IPY_MODEL_42315e83fbac49c2bc7f2faf1abcc22e", - "value": "Map: 100%" - } - }, - "34a89db126a64690bc2f6c8656ba2210": { - "model_module": "@jupyter-widgets/controls", - "model_name": "FloatProgressModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_4cdff5bdf7574795802e821aa42f3c4e", - "max": 150, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_754aa440f45c4a878d99572368d659c8", - "value": 150 - } - }, - "4c47ce21b5a14328aa22403782e4da9b": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8b5f0c156a9641cfa5413668a0b97b9c", - "placeholder": "​", - "style": "IPY_MODEL_aff498bd632f4036958f59cfc6587ea3", - "value": " 150/150 [00:00<00:00, 1904.80 examples/s]" - } - }, - "7dff366d9e71427dbae40b1dce7a9bfa": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, "0614c35b3690494ca3b8f9ab71d71a08": { "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "42315e83fbac49c2bc7f2faf1abcc22e": { - "model_module": "@jupyter-widgets/controls", - "model_name": "DescriptionStyleModel", - "model_module_version": "1.5.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "4cdff5bdf7574795802e821aa42f3c4e": { - "model_module": "@jupyter-widgets/base", "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "754aa440f45c4a878d99572368d659c8": { - "model_module": "@jupyter-widgets/controls", - "model_name": "ProgressStyleModel", - "model_module_version": "1.5.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "8b5f0c156a9641cfa5413668a0b97b9c": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "aff498bd632f4036958f59cfc6587ea3": { - "model_module": "@jupyter-widgets/controls", - "model_name": "DescriptionStyleModel", - "model_module_version": "1.5.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "d6825cc926a24f2482ce72c15242081e": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HBoxModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_24b2b5f5d92049a79014b8278e97451b", - "IPY_MODEL_a6001d34e58a47cab0d8bff2451afb6e", - "IPY_MODEL_b42e8d8b61d7431a814a03c5e07a1166" - ], - "layout": "IPY_MODEL_9ce1659c776140bcaf3c16eae6f70967" - } - }, - "24b2b5f5d92049a79014b8278e97451b": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_cceae79c145d4b73a64e80ad3fc8866c", - "placeholder": "​", - "style": "IPY_MODEL_56bc56071ff04223935dc2d98d2703ab", - "value": "Map: 100%" - } - }, - "a6001d34e58a47cab0d8bff2451afb6e": { - "model_module": "@jupyter-widgets/controls", - "model_name": "FloatProgressModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_67cacc87afe14250baaa073289fb4a8f", - "max": 50, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_227eea7074544adbb2c34b9dde340fa5", - "value": 50 - } - }, - "b42e8d8b61d7431a814a03c5e07a1166": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_8bd9aebb2cc5420094b2b441a5183523", - "placeholder": "​", - "style": "IPY_MODEL_1c3eb3793b6e4291b4fa57ce8419ef1f", - "value": " 50/50 [00:00<00:00, 1509.88 examples/s]" - } - }, - "9ce1659c776140bcaf3c16eae6f70967": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "cceae79c145d4b73a64e80ad3fc8866c": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "56bc56071ff04223935dc2d98d2703ab": { - "model_module": "@jupyter-widgets/controls", - "model_name": "DescriptionStyleModel", - "model_module_version": "1.5.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "67cacc87afe14250baaa073289fb4a8f": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "227eea7074544adbb2c34b9dde340fa5": { - "model_module": "@jupyter-widgets/controls", - "model_name": "ProgressStyleModel", - "model_module_version": "1.5.0", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "8bd9aebb2cc5420094b2b441a5183523": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", "state": { "_model_module": "@jupyter-widgets/base", "_model_module_version": "1.2.0", @@ -1963,8 +1632,8 @@ }, "1c3eb3793b6e4291b4fa57ce8419ef1f": { "model_module": "@jupyter-widgets/controls", - "model_name": "DescriptionStyleModel", "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", @@ -1975,10 +1644,627 @@ "_view_name": "StyleView", "description_width": "" } + }, + "227eea7074544adbb2c34b9dde340fa5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "245570c62c3844728d7125a706fbbc9b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_8d95da3803e542f8b855175013d497ba", + "IPY_MODEL_34a89db126a64690bc2f6c8656ba2210", + "IPY_MODEL_4c47ce21b5a14328aa22403782e4da9b" + ], + "layout": "IPY_MODEL_7dff366d9e71427dbae40b1dce7a9bfa" + } + }, + "24b2b5f5d92049a79014b8278e97451b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_cceae79c145d4b73a64e80ad3fc8866c", + "placeholder": "​", + "style": "IPY_MODEL_56bc56071ff04223935dc2d98d2703ab", + "value": "Map: 100%" + } + }, + "34a89db126a64690bc2f6c8656ba2210": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4cdff5bdf7574795802e821aa42f3c4e", + "max": 150, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_754aa440f45c4a878d99572368d659c8", + "value": 150 + } + }, + "42315e83fbac49c2bc7f2faf1abcc22e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "4c47ce21b5a14328aa22403782e4da9b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8b5f0c156a9641cfa5413668a0b97b9c", + "placeholder": "​", + "style": "IPY_MODEL_aff498bd632f4036958f59cfc6587ea3", + "value": " 150/150 [00:00<00:00, 1904.80 examples/s]" + } + }, + "4cdff5bdf7574795802e821aa42f3c4e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "56bc56071ff04223935dc2d98d2703ab": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "67cacc87afe14250baaa073289fb4a8f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "754aa440f45c4a878d99572368d659c8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "7dff366d9e71427dbae40b1dce7a9bfa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8b5f0c156a9641cfa5413668a0b97b9c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8bd9aebb2cc5420094b2b441a5183523": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8d95da3803e542f8b855175013d497ba": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0614c35b3690494ca3b8f9ab71d71a08", + "placeholder": "​", + "style": "IPY_MODEL_42315e83fbac49c2bc7f2faf1abcc22e", + "value": "Map: 100%" + } + }, + "9ce1659c776140bcaf3c16eae6f70967": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a6001d34e58a47cab0d8bff2451afb6e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_67cacc87afe14250baaa073289fb4a8f", + "max": 50, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_227eea7074544adbb2c34b9dde340fa5", + "value": 50 + } + }, + "aff498bd632f4036958f59cfc6587ea3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b42e8d8b61d7431a814a03c5e07a1166": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_8bd9aebb2cc5420094b2b441a5183523", + "placeholder": "​", + "style": "IPY_MODEL_1c3eb3793b6e4291b4fa57ce8419ef1f", + "value": " 50/50 [00:00<00:00, 1509.88 examples/s]" + } + }, + "cceae79c145d4b73a64e80ad3fc8866c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d6825cc926a24f2482ce72c15242081e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_24b2b5f5d92049a79014b8278e97451b", + "IPY_MODEL_a6001d34e58a47cab0d8bff2451afb6e", + "IPY_MODEL_b42e8d8b61d7431a814a03c5e07a1166" + ], + "layout": "IPY_MODEL_9ce1659c776140bcaf3c16eae6f70967" + } } } } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +}