diff --git a/cohort_2/week2/1. Synthetic-Transactions-Logfire.ipynb b/cohort_2/week2/1. Synthetic-Transactions-Logfire.ipynb
new file mode 100644
index 0000000..dd1c1d9
--- /dev/null
+++ b/cohort_2/week2/1. Synthetic-Transactions-Logfire.ipynb
@@ -0,0 +1,1015 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Week 2: Fine-tuning Embeddings for RAG Applications\n",
+ "\n",
+ "> **Prerequisites**: Please complete Week 1's notebooks before starting this one. The concepts build directly on that foundation.\n",
+ "\n",
+ "Most teams avoid fine-tuning their embedding models, thinking they need tons of data and complex setups. But our experience shows that with just 100-200 thoughtfully created examples, you can significantly improve your model's performance over general-purpose embeddings.\n",
+ "\n",
+ "## Why This Matters\n",
+ "\n",
+ "Fine-tuning embedding models gives several key advantages over using general-purpose models. While it takes more upfront work to create training data and run the fine-tuning process, this investment typically leads to:\n",
+ "\n",
+ "1. Much better accuracy on your specific tasks\n",
+ "2. Lower running costs through using smaller, more efficient models\n",
+ "3. Better handling of domain-specific language and context\n",
+ "\n",
+ "## What You'll Learn\n",
+ "\n",
+ "Through this tutorial, you'll discover how to:\n",
+ "\n",
+ "1. Build Quality Training Data\n",
+ "\n",
+ "- Generate synthetic transactions systematically\n",
+ "- Review and validate examples manually\n",
+ "- Create diverse, representative samples\n",
+ "\n",
+ "2. Structure Your Dataset\n",
+ "\n",
+ "- Define clear schema for transactions\n",
+ "- Add meaningful metadata\n",
+ "- Ensure data consistency\n",
+ "\n",
+ "3. Establish Performance Baselines\n",
+ "\n",
+ "- Measure initial embedding performance\n",
+ "- Calculate retrieval metrics\n",
+ "- Set up evaluation pipelines\n",
+ "\n",
+ "By the end of this notebook, you'll know how to create high-quality synthetic data for fine-tuning embedding models. This prepares you for the hands-on fine-tuning work in notebooks 2 and 3, where we'll use both Cohere's managed service and open-source tools to improve retrieval performance.\n",
+ "\n",
+ "## Case Study: Ramp's Transaction Categorization\n",
+ "\n",
+ "> Read about Ramp's succesful case study using fine-tuned embeddings [here](https://engineering.ramp.com/transaction-embeddings)\n",
+ "\n",
+ "We'll follow Ramp's successful approach to fine-tuning embeddings for transaction categorization. Their team demonstrated that even with unique customer categories, a fine-tuned model could effectively generalize to new customers and scenarios.\n",
+ "\n",
+ "Using synthetic financial data, we'll walk through their process step by step:\n",
+ "\n",
+ "1. **Data Understanding**: Learn what transaction data looks like and how to structure it for embedding\n",
+ "2. **Synthetic Data Generation**: Create realistic, challenging test cases using large language models\n",
+ "3. **Model Fine-tuning**: Compare performance between base and fine-tuned models\n",
+ "\n",
+ "Throughout this process, we'll use Logfire to track experiments and measure improvements systematically. This workflow will give you a practical foundation for fine-tuning embeddings in your own applications.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Understanding Transactions\n",
+ "\n",
+ "To fine-tune our model effectively, we need to understand the transaction data we're working with.\n",
+ "\n",
+ "Typical Transaction Fields:\n",
+ "\n",
+ "- Merchant Name: The vendor or service provider's name.\n",
+ "- Merchant Category Code (MCC): General category of the transaction (e.g., Restaurants).\n",
+ "- Department Name: The company department responsible for the transaction.\n",
+ "- Location: Where the transaction took place.\n",
+ "- Amount: The transaction's monetary value.\n",
+ "- Spend Program Name: Specific budget or spend limit allocated.\n",
+ "- Trip Name: If the transaction occurred during travel.\n",
+ "\n",
+ "We can see an example below\n",
+ "\n",
+ "```\n",
+ "Name : Beirut Bakery\n",
+ "Category: Restaurants, Cafeteria\n",
+ "Department: Engineering\n",
+ "Location: Calgary, AB, Canada\n",
+ "Amount: 56.67 CAD\n",
+ "Card: Ramp's Physical Card\n",
+ "Trip Name: unknown\n",
+ "```\n",
+ "\n",
+ "This is a difficult task because there's very little information. Additionally since each company has unique categories that have some implicit rules, it's difficult for a general embedding model to classify these transactions without fine-tuning.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Generating Synthetic Transactions\n",
+ "\n",
+ "We'll generate a synthetic transaction dataset in 3 steps\n",
+ "\n",
+ "1. **Initial Examples** : We'll create a batch of basic transactions using `gpt-4o-mini` and manually review them to create a small dataset of good and bad examples.\n",
+ "\n",
+ "2. **Refine Data** : We'll then randomly select a subset of these initial examples and use them to generate new examples that are more challenging by adding them to the prompt as few shot examples.\n",
+ "\n",
+ "3. **Logfire Evaluation** : We'll then use `Logfire` to evaluate the recall@1,3,5 and mrr@1,3,5 of our initial and refined examples.\n",
+ "\n",
+ "We recommend using `ChatGPT` to evaluate these examples and get a sense for what makes a good or bad example during this process. We'll iterate on this until we've generated at least 300 examples. This will ensure that we have enough examples to fine-tune a cohere re-ranker ( requires min 256 examples) or a sentence transformer model while also having enough examples to create a held out evaluation set.\n",
+ "\n",
+ "There are two key advantages to iteratively generating our dataset in small batches\n",
+ "\n",
+ "1. **Practicality** : A small amount of data is easy to label\n",
+ "2. **Randomness** : By constantly sampling from our growing number of examples, we're able to have enough randomness to create a diverse dataset. This helps us to avoid potential issues with diversity and quality that doing a single pass of data generation can introduce.\n",
+ "\n",
+ "### Step 1 : Generating our initial transactions.\n",
+ "\n",
+ "We'll start by generating our initial transactions using a simple prompt. These are going to be very simple examples that will not be great but they are useful as an initial starting point.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/ivanleo/Documents/coding/systematically-improving-rag/cohort_2/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020\n",
+ " warnings.warn(\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pydantic import BaseModel, field_validator, ValidationInfo\n",
+ "from openai import AsyncOpenAI\n",
+ "import instructor\n",
+ "from typing import Optional\n",
+ "from textwrap import dedent\n",
+ "import random\n",
+ "import json\n",
+ "import asyncio\n",
+ "\n",
+ "# Load in pre-defined categories\n",
+ "categories = json.load(open(\"data/categories.json\"))\n",
+ "\n",
+ "\n",
+ "# Define a Pydantic model that can represent the same transaction data that Ramp was using\n",
+ "class Transaction(BaseModel):\n",
+ " merchant_name: str\n",
+ " merchant_category: list[str]\n",
+ " department: str\n",
+ " location: str\n",
+ " amount: float\n",
+ " spend_program_name: str\n",
+ " trip_name: Optional[str] = None\n",
+ " expense_category: str\n",
+ "\n",
+ " def format_transaction(self):\n",
+ " return dedent(\n",
+ " f\"\"\"\n",
+ " Name : {self.merchant_name}\n",
+ " Category: {\", \".join(self.merchant_category)}\n",
+ " Department: {self.department}\n",
+ " Location: {self.location}\n",
+ " Amount: {self.amount}\n",
+ " Card: {self.spend_program_name}\n",
+ " Trip Name: {self.trip_name if self.trip_name else \"unknown\"}\n",
+ " \"\"\"\n",
+ " )\n",
+ "\n",
+ " @field_validator(\"expense_category\")\n",
+ " @classmethod\n",
+ " def validate_expense_category(cls, v, info: ValidationInfo):\n",
+ " # We use this later to read in the generated transactions\n",
+ " if not info.context:\n",
+ " return v\n",
+ "\n",
+ " # Validate that we've generated the right expense category\n",
+ " assert v == info.context[\"category\"][\"category\"], (\n",
+ " f\"The transaction must have an expense category of {info.context['category']['category']} instead of {v}\"\n",
+ " )\n",
+ " return v\n",
+ "\n",
+ "\n",
+ "client = instructor.from_openai(AsyncOpenAI())\n",
+ "\n",
+ "\n",
+ "async def generate_transaction(category):\n",
+ " return await client.chat.completions.create(\n",
+ " model=\"gpt-4o-mini\",\n",
+ " messages=[\n",
+ " {\n",
+ " \"role\": \"system\",\n",
+ " \"content\": \"\"\"Generate a transaction for a tech company that could be filed under the category of {{ category }}. This should be distinct from the sample_transactions provided in the categories.json file\n",
+ "\n",
+ " - The spend program is a specific spending authority or allocation that has defined limits, rules, and permissions. It's like a virtual card or spending account set up for a specific purpose.\n",
+ " - Merchant Category Name is a label that best describes the merchant of the transaction.\n",
+ " - Merchant name should be realistic and not obviously made up.\n",
+ " \"\"\",\n",
+ " }\n",
+ " ],\n",
+ " context={\"category\": category},\n",
+ " response_model=Transaction,\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Generate 5 initial transactions and choose the category randomly\n",
+ "coros = []\n",
+ "for _ in range(5):\n",
+ " coros.append(generate_transaction(random.choice(categories)))\n",
+ "\n",
+ "transactions = await asyncio.gather(*coros)\n",
+ "with open(\"./data/generated_transactions.jsonl\", \"a\") as f:\n",
+ " for transaction in transactions:\n",
+ " f.write(transaction.model_dump_json() + \"\\n\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
Transaction ( \n",
+ " merchant_name ='TechBank USA' ,\n",
+ " merchant_category =[ 'Bank & Transaction Fees' ] ,\n",
+ " department ='Finance' ,\n",
+ " location ='San Francisco, CA' ,\n",
+ " amount =150.0 ,\n",
+ " spend_program_name ='Operational Expenses' ,\n",
+ " trip_name =None ,\n",
+ " expense_category ='Bank & Transaction Fees' \n",
+ ") \n",
+ " \n"
+ ],
+ "text/plain": [
+ "\u001b[1;35mTransaction\u001b[0m\u001b[1m(\u001b[0m\n",
+ " \u001b[33mmerchant_name\u001b[0m=\u001b[32m'TechBank USA'\u001b[0m,\n",
+ " \u001b[33mmerchant_category\u001b[0m=\u001b[1m[\u001b[0m\u001b[32m'Bank & Transaction Fees'\u001b[0m\u001b[1m]\u001b[0m,\n",
+ " \u001b[33mdepartment\u001b[0m=\u001b[32m'Finance'\u001b[0m,\n",
+ " \u001b[33mlocation\u001b[0m=\u001b[32m'San Francisco, CA'\u001b[0m,\n",
+ " \u001b[33mamount\u001b[0m=\u001b[1;36m150\u001b[0m\u001b[1;36m.0\u001b[0m,\n",
+ " \u001b[33mspend_program_name\u001b[0m=\u001b[32m'Operational Expenses'\u001b[0m,\n",
+ " \u001b[33mtrip_name\u001b[0m=\u001b[3;35mNone\u001b[0m,\n",
+ " \u001b[33mexpense_category\u001b[0m=\u001b[32m'Bank & Transaction Fees'\u001b[0m\n",
+ "\u001b[1m)\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from rich import print\n",
+ "\n",
+ "print(transactions[0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Step 2 : Labeling Transactions\n",
+ "\n",
+ "Now that we've generated a small set of initial transactions, please run `streamlit run label.py` to manually select transactions that you think might be difficult to classify.\n",
+ "\n",
+ "> You can modify and edit the transaction details before approving them. Hot keys of ctrl + e ( approve ) and ctrl + r ( reject ) make this process much faster. Only approved transactions will be saved to `generated_transactions.jsonl` below. We'll then use these examples to generate a new set of transactions that are more challenging.\n",
+ "\n",
+ "You can also manually override transaction details in the streamlit application. We recommend using `ChatGPT` or `Claude` to discuss and generate good default and examples. A prompt that I used to prompt the chat UI was\n",
+ "\n",
+ "```\n",
+ "I'd like to generate a transaction for a tech company that is challenging to classify into a specific category. Here are the details\n",
+ "\n",
+ "\n",
+ "\n",
+ "I'd like you to help rewrite some of the details to make it more realistic. Please stick to the following rules\n",
+ "\n",
+ "- MCCs should be realistic. If possible, let's try to use a MCC that will cover a superset of the given category\n",
+ "- Let's try to suggest a non-uniform number (Eg. not 1500 ) so that it seems more realistic\n",
+ "- The Spend Program name should be a specific spending authority or allocation that has defined limits, rules, and permissions. It's like a virtual card or spending account set up for a specific purpose. In our case, this spend program name should not be a name that directly mentions the category or merchant\n",
+ "```\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "async def generate_transaction_with_examples(category, examples: list[Transaction]):\n",
+ " return await client.chat.completions.create(\n",
+ " model=\"gpt-4o\",\n",
+ " messages=[\n",
+ " {\n",
+ " \"role\": \"system\",\n",
+ " \"content\": \"\"\"\n",
+ " Generate a potentially ambiguous business transaction that could reasonably be categorized as {{ category }} or another similar category. The goal is to create transactions that challenge automatic categorization systems by having characteristics that could fit multiple categories.\n",
+ "\n",
+ "\n",
+ " Available categories in the system.:\n",
+ " \n",
+ " {% for category_option in categories %}\n",
+ " {{ category_option[\"category\"] }}\n",
+ " {% endfor %}\n",
+ " \n",
+ "\n",
+ " \n",
+ " The transaction should:\n",
+ " 1. Use a realistic but non-obvious merchant name (international names welcome), don't use names that are obviously made u \n",
+ " 2. Include a plausible but non-rounded amount with decimals (e.g., $1247.83)\n",
+ " 3. Be difficult to categorize definitively (could fit in multiple categories)\n",
+ " 4. Merchant Category Name(s) should not reference the category at all and should be able to be used for other similar categories if possible.\n",
+ "\n",
+ " Here are some good examples of transactions that were previously generated for other categories.\n",
+ "\n",
+ " {% for example in examples %}\n",
+ " {{ example.model_dump_json() }}\n",
+ " {% endfor %}\n",
+ " \"\"\",\n",
+ " }\n",
+ " ],\n",
+ " context={\"category\": category, \"examples\": examples, \"categories\": categories},\n",
+ " response_model=Transaction,\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 240,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open(\"./data/cleaned.jsonl\", \"r\") as f:\n",
+ " sample_transactions = []\n",
+ " for line in f:\n",
+ " sample_transactions.append(Transaction(**json.loads(line)))\n",
+ "\n",
+ "\n",
+ "coros = []\n",
+ "for _ in range(20):\n",
+ " coros.append(\n",
+ " generate_transaction_with_examples(\n",
+ " random.choice(categories), random.sample(sample_transactions, 10)\n",
+ " )\n",
+ " )\n",
+ "\n",
+ "transactions = await asyncio.gather(*coros)\n",
+ "\n",
+ "with open(\"./data/generated_transactions.jsonl\", \"w\") as f:\n",
+ " for transaction in transactions:\n",
+ " f.write(transaction.model_dump_json() + \"\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Step 3 : Evaluating Recall and MRR Performance.\n",
+ "\n",
+ "> Make sure that you've setup Logfire before continuing with the following section. For instructions on how to do so, please refer to `week-0/3. Using Pydantic Evals`\n",
+ "\n",
+ "Remember that we're building a model that can suggest transaction categories to a user. To do so, we'll only be able to show the top 3-5 results and we want to make sure that the correct result is ranked as highly as possible.\n",
+ "\n",
+ "Therefore, we'll be using recall and mrr to evaluate our model's performance here.\n",
+ "\n",
+ "- `recall` : This measures whether the correct category is in the top k retrieved results.\n",
+ "- `mrr` : This measures how highly is the correct category ranked in the retrieved results.\n",
+ "\n",
+ "Ideally we want a model with a high recall and mrr. This means that when we display the results, they're likely to be relevant to the user. By measuring the recall and mrr, we're able to ensure that we're conssitently generating questions that the model finds challenging.\n",
+ "\n",
+ "We're using `lancedb` here since it provides an easy way to perform these evaluations with automatic batching of embeddings for queries and data along with a single api for vector search and reranking.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lancedb\n",
+ "from lancedb.pydantic import LanceModel, Vector\n",
+ "from lancedb.embeddings import get_registry\n",
+ "\n",
+ "func = get_registry().get(\"openai\").create(name=\"text-embedding-3-small\")\n",
+ "categories = json.load(open(\"data/categories.json\"))\n",
+ "\n",
+ "\n",
+ "class Category(LanceModel):\n",
+ " text: str = func.SourceField()\n",
+ " embedding: Vector(func.ndims()) = func.VectorField()\n",
+ "\n",
+ "\n",
+ "db = lancedb.connect(\"./lancedb\")\n",
+ "table = db.create_table(\"categories\", schema=Category, mode=\"overwrite\")\n",
+ "\n",
+ "\n",
+ "table.add(\n",
+ " [\n",
+ " {\n",
+ " \"text\": category[\"category\"],\n",
+ " }\n",
+ " for category in categories\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "table.create_fts_index(field_names=[\"text\"], replace=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can then read in our transactions that we've manually annotated using the `label.py` streamlit application"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Transaction ( \n",
+ " merchant_name ='Tooly' ,\n",
+ " merchant_category =[ 'Survey Software' , 'SaaS' ] ,\n",
+ " department ='Marketing' ,\n",
+ " location ='San Francisco, CA' ,\n",
+ " amount =2000.0 ,\n",
+ " spend_program_name ='Annual Marketing Technology Budget' ,\n",
+ " trip_name =None ,\n",
+ " expense_category ='Subscriptions & Memberships' \n",
+ ") \n",
+ " \n"
+ ],
+ "text/plain": [
+ "\u001b[1;35mTransaction\u001b[0m\u001b[1m(\u001b[0m\n",
+ " \u001b[33mmerchant_name\u001b[0m=\u001b[32m'Tooly'\u001b[0m,\n",
+ " \u001b[33mmerchant_category\u001b[0m=\u001b[1m[\u001b[0m\u001b[32m'Survey Software'\u001b[0m, \u001b[32m'SaaS'\u001b[0m\u001b[1m]\u001b[0m,\n",
+ " \u001b[33mdepartment\u001b[0m=\u001b[32m'Marketing'\u001b[0m,\n",
+ " \u001b[33mlocation\u001b[0m=\u001b[32m'San Francisco, CA'\u001b[0m,\n",
+ " \u001b[33mamount\u001b[0m=\u001b[1;36m2000\u001b[0m\u001b[1;36m.0\u001b[0m,\n",
+ " \u001b[33mspend_program_name\u001b[0m=\u001b[32m'Annual Marketing Technology Budget'\u001b[0m,\n",
+ " \u001b[33mtrip_name\u001b[0m=\u001b[3;35mNone\u001b[0m,\n",
+ " \u001b[33mexpense_category\u001b[0m=\u001b[32m'Subscriptions & Memberships'\u001b[0m\n",
+ "\u001b[1m)\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from rich import print\n",
+ "\n",
+ "print(transactions[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pydantic_evals import Dataset, Case\n",
+ "\n",
+ "cases = []\n",
+ "for line in open(\"./data/cleaned.jsonl\").readlines():\n",
+ " transaction = Transaction(**json.loads(line))\n",
+ " cases.append(\n",
+ " Case(\n",
+ " inputs=transaction.format_transaction(),\n",
+ " expected_output=[transaction.expense_category],\n",
+ " )\n",
+ " )\n",
+ "\n",
+ "dataset = Dataset(cases=cases)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 55,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from helpers import get_metrics_at_k, task\n",
+ "from dataclasses import dataclass\n",
+ "from pydantic_evals.evaluators import Evaluator, EvaluatorContext\n",
+ "from lancedb.table import Table\n",
+ "from concurrent.futures import ThreadPoolExecutor\n",
+ "from functools import partial\n",
+ "import logfire\n",
+ "\n",
+ "\n",
+ "@dataclass\n",
+ "class RagMetricsEvaluator(Evaluator):\n",
+ " async def evaluate(self, ctx: EvaluatorContext[str, str]) -> dict[str, float]:\n",
+ " predictions = ctx.output\n",
+ " labels = ctx.expected_output\n",
+ " metrics = get_metrics_at_k(metrics=[\"mrr\", \"recall\"], sizes=[1, 3, 5])\n",
+ " return {\n",
+ " metric: score_fn(predictions, labels)\n",
+ " for metric, score_fn in metrics.items()\n",
+ " }\n",
+ "\n",
+ "\n",
+ "async def retrieve_results(\n",
+ " question: str,\n",
+ " table: Table,\n",
+ " pool: ThreadPoolExecutor,\n",
+ " max_k=25,\n",
+ " reranker=None,\n",
+ "):\n",
+ " loop = asyncio.get_running_loop()\n",
+ " return await loop.run_in_executor(\n",
+ " pool,\n",
+ " partial(task, user_query=question, table=table, max_k=max_k, reranker=reranker),\n",
+ " )\n",
+ "\n",
+ "\n",
+ "logfire.configure(\n",
+ " send_to_logfire=True,\n",
+ " environment=\"experimentation\",\n",
+ " service_name=\"synthetic-transactions\",\n",
+ " console=False,\n",
+ ")\n",
+ "\n",
+ "dataset.add_evaluator(RagMetricsEvaluator())\n",
+ "with ThreadPoolExecutor(max_workers=10) as executor:\n",
+ " evaluation_result = await dataset.evaluate(\n",
+ " partial(retrieve_results, table=table, pool=executor)\n",
+ " )\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 60,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.3885 \n",
+ "mrr@3 0.4981 \n",
+ "mrr@5 0.5254 \n",
+ "recall@1 0.3885 \n",
+ "recall@3 0.6462 \n",
+ "recall@5 0.7692 \n",
+ " \n"
+ ],
+ "text/plain": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3885\u001b[0m\n",
+ "mrr@\u001b[1;36m3\u001b[0m \u001b[1;36m0.4981\u001b[0m\n",
+ "mrr@\u001b[1;36m5\u001b[0m \u001b[1;36m0.5254\u001b[0m\n",
+ "recall@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3885\u001b[0m\n",
+ "recall@\u001b[1;36m3\u001b[0m \u001b[1;36m0.6462\u001b[0m\n",
+ "recall@\u001b[1;36m5\u001b[0m \u001b[1;36m0.7692\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from tabulate import tabulate\n",
+ "\n",
+ "def format_results(result):\n",
+ " return tabulate(\n",
+ " [[item, round(value,4)] for item, value in result.averages().scores.items()],\n",
+ " headers=[\"Metric\", \"Score\"],\n",
+ " )\n",
+ "\n",
+ "print(format_results(evaluation_result))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "At this point, we've generated a large dataset of synthetic transactions that we can use to fine-tune a model on. However, it's important here to call out that synthetic data has its challenges.\n",
+ "\n",
+ "1. Quality and Diversity : It's difficult to ensure that the synthetic data is of high quality and diverse. We've done so by manually reviewing and selecting good examples but ultimately we need real production data to ensure that our model is able to generalise.\n",
+ "\n",
+ "2. Human Error : Manual review is great to ensure the quality of transactions but is expensive and error prone. This is not something that scales well, especially if you're trying to generate thousands of examples which you'd like humans to manually label.\n",
+ "\n",
+ "We want to treat this synthetic data as a starting point and iteratively make it better using the techniques we've discussed in this notebook. But you will need to eventually mix in production data and continue generating synthetic data in order to adequately evaluate and test the generalisation capabilities of your model.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Creating a Dataset\n",
+ "\n",
+ "We want to segregate our data into a train and evaluation set because it allows us to evaluate the performance of our model on data that it hasn't seen before. \n",
+ "\n",
+ "We use `pydantic_evals` here to segregate our dataset into two separate yml files and store them in our `data` folder. This allows us to easily share and run evaluations on our model in the subsequent notebooks later on.\n",
+ "\n",
+ "\n",
+ "If we fine-tuned our model on the same data that we evaluated it on, it would be difficult to tell if the improvements we made were due to the model generalizing better or due to overfitting. In this case, we're just going to split our data by selecting the first 80% as our training set and the remaining 20% as our evaluation set.\n",
+ "\n",
+ "In practice, you'd want to think carefully about these splits - using the category as a way to ensure that we have a diverse set of examples or generating new labels for the evaluation set based on the training labels. (Eg. Restaurants -> Dining Establishments or randomly grouping categories together )\n",
+ "\n",
+ "Before we start fine-tuning our models here, we also need to make sure that the evaluation set and training set are similar. We do so by measuring the recall and mrr and verifying that they have similar values.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_ratio = 0.8 * len(cases)\n",
+ "\n",
+ "train_cases = cases[: int(train_ratio)]\n",
+ "eval_cases = cases[int(train_ratio) :]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 50,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_dataset_path = \"./data/train_transactions.yml\"\n",
+ "eval_dataset_path = \"./data/eval_transactions.yml\"\n",
+ "\n",
+ "train_dataset = Dataset(cases=train_cases)\n",
+ "train_dataset.to_file(train_dataset_path)\n",
+ "\n",
+ "eval_dataset = Dataset(cases=eval_cases)\n",
+ "eval_dataset.to_file(eval_dataset_path)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can validate that the dataset is formatted correctly by printing out the first item.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 51,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Case ( \n",
+ " name =None ,\n",
+ " inputs ='\\nName : Tooly\\nCategory: Survey Software, SaaS\\nDepartment: Marketing\\nLocation: San Francisco, \n",
+ "CA\\nAmount: 2000.0\\nCard: Annual Marketing Technology Budget\\nTrip Name: unknown\\n' ,\n",
+ " metadata =None ,\n",
+ " expected_output =[ 'Subscriptions & Memberships' ] ,\n",
+ " evaluators =[] \n",
+ ") \n",
+ " \n"
+ ],
+ "text/plain": [
+ "\u001b[1;35mCase\u001b[0m\u001b[1m(\u001b[0m\n",
+ " \u001b[33mname\u001b[0m=\u001b[3;35mNone\u001b[0m,\n",
+ " \u001b[33minputs\u001b[0m=\u001b[32m'\\nName : Tooly\\nCategory: Survey Software, SaaS\\nDepartment: Marketing\\nLocation: San Francisco, \u001b[0m\n",
+ "\u001b[32mCA\\nAmount: 2000.0\\nCard: Annual Marketing Technology Budget\\nTrip Name: unknown\\n'\u001b[0m,\n",
+ " \u001b[33mmetadata\u001b[0m=\u001b[3;35mNone\u001b[0m,\n",
+ " \u001b[33mexpected_output\u001b[0m=\u001b[1m[\u001b[0m\u001b[32m'Subscriptions & Memberships'\u001b[0m\u001b[1m]\u001b[0m,\n",
+ " \u001b[33mevaluators\u001b[0m=\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m\n",
+ "\u001b[1m)\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from rich import print\n",
+ "\n",
+ "dataset = Dataset.from_file(train_dataset_path)\n",
+ "print(dataset.cases[0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can see that the dataset has the input formatted correctly as well as the expected output. Now let's see if our evaluation set and training set are similar.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 65,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "260 66 \n",
+ " \n"
+ ],
+ "text/plain": [
+ "\u001b[1;36m260\u001b[0m \u001b[1;36m66\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "train_dataset = Dataset.from_file(train_dataset_path)\n",
+ "eval_dataset = Dataset.from_file(eval_dataset_path)\n",
+ "\n",
+ "print(len(train_dataset.cases), len(eval_dataset.cases))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from lancedb import connect\n",
+ "import concurrent\n",
+ "\n",
+ "datasets = [[\"train\", train_dataset], [\"eval\", eval_dataset]]\n",
+ "\n",
+ "db = connect(\"./lancedb\")\n",
+ "table = db.open_table(\"categories\")\n",
+ "\n",
+ "results = []\n",
+ "for dataset_name, dataset_partition in datasets:\n",
+ " with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n",
+ " dataset_partition.add_evaluator(RagMetricsEvaluator())\n",
+ " result = await dataset_partition.evaluate(\n",
+ " partial(retrieve_results, table=table, pool=executor)\n",
+ " )\n",
+ " results.append(result)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 67,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.3885 \n",
+ "mrr@3 0.4981 \n",
+ "mrr@5 0.5254 \n",
+ "recall@1 0.3885 \n",
+ "recall@3 0.6462 \n",
+ "recall@5 0.7692 \n",
+ " \n"
+ ],
+ "text/plain": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3885\u001b[0m\n",
+ "mrr@\u001b[1;36m3\u001b[0m \u001b[1;36m0.4981\u001b[0m\n",
+ "mrr@\u001b[1;36m5\u001b[0m \u001b[1;36m0.5254\u001b[0m\n",
+ "recall@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3885\u001b[0m\n",
+ "recall@\u001b[1;36m3\u001b[0m \u001b[1;36m0.6462\u001b[0m\n",
+ "recall@\u001b[1;36m5\u001b[0m \u001b[1;36m0.7692\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "print(format_results(results[0]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 68,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.3788 \n",
+ "mrr@3 0.4798 \n",
+ "mrr@5 0.5131 \n",
+ "recall@1 0.3788 \n",
+ "recall@3 0.6061 \n",
+ "recall@5 0.7424 \n",
+ " \n"
+ ],
+ "text/plain": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3788\u001b[0m\n",
+ "mrr@\u001b[1;36m3\u001b[0m \u001b[1;36m0.4798\u001b[0m\n",
+ "mrr@\u001b[1;36m5\u001b[0m \u001b[1;36m0.5131\u001b[0m\n",
+ "recall@\u001b[1;36m1\u001b[0m \u001b[1;36m0.3788\u001b[0m\n",
+ "recall@\u001b[1;36m3\u001b[0m \u001b[1;36m0.6061\u001b[0m\n",
+ "recall@\u001b[1;36m5\u001b[0m \u001b[1;36m0.7424\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "print(format_results(results[1]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Results and Analysis\n",
+ "\n",
+ "Now let's visualise the scores for our evaluation and training datasets below"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 69,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " mrr@1 \n",
+ " mrr@3 \n",
+ " mrr@5 \n",
+ " recall@1 \n",
+ " recall@3 \n",
+ " recall@5 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " Train \n",
+ " 0.39 \n",
+ " 0.50 \n",
+ " 0.53 \n",
+ " 0.39 \n",
+ " 0.65 \n",
+ " 0.77 \n",
+ " \n",
+ " \n",
+ " Eval \n",
+ " 0.38 \n",
+ " 0.48 \n",
+ " 0.51 \n",
+ " 0.38 \n",
+ " 0.61 \n",
+ " 0.74 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " mrr@1 mrr@3 mrr@5 recall@1 recall@3 recall@5\n",
+ "Train 0.39 0.50 0.53 0.39 0.65 0.77\n",
+ "Eval 0.38 0.48 0.51 0.38 0.61 0.74"
+ ]
+ },
+ "execution_count": 69,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "scores = []\n",
+ "\n",
+ "for result in results:\n",
+ " result_scores = {}\n",
+ " for score_name, score in result.averages().scores.items():\n",
+ " result_scores[score_name] = score\n",
+ " scores.append(result_scores)\n",
+ "\n",
+ "df = pd.DataFrame(scores, index=[\"Train\", \"Eval\"])\n",
+ "df.round(2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 70,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAHqCAYAAABMTMx9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABf8klEQVR4nO3dC5hVZb04/u9wFxVRuSVxxFsJXkBBSNO0E4qXLDxZZCU4FacbZXKsoBRESzQJKSNJi7TSpIvZRcOME5VJYZBpKpiKYiq3lIt4AGX2/3nX7z/TDDMDA+6ZPWvP5/M86xnX2mvt/e69+W7X+37fS0WhUCgEAAAAAABAK9eu1AUAAAAAAABoCkkNAAAAAAAgFyQ1AAAAAACAXJDUAAAAAAAAckFSAwAAAAAAyAVJDQAAAAAAIBckNQAAAAAAgFyQ1AAAAAAAAHJBUgMAAAAAAMgFSQ0A4oILLoj+/ftHOcjLe6moqIjx48fv9vUvvfRS9OrVK2655ZZozV555ZXo169ffOMb3yh1UQAAoCj38ZdddlnN/k033ZQde+qpp0paLoC2RFIDyL3qm8i03XvvvfUeLxQKWaNqevztb397tGapMb76vaRtzz33jGHDhsV3v/vdUhet7Jxyyil1Pus99tgjjj766Jg5c2ZUVVVFa/fVr3419t5773jve99bcyxVrtJ7adeuXTzzzDP1rtmwYUP2PrdPqKQKWO3PIl2/3377xRlnnBELFy6s9zzVr1O9dezYMfu3+6lPfSrWrVtX59z02IQJE+JLX/pSbN68ueifAwAA5Vu/S1uHDh2ib9++WeelZ599ttTFA6AV6FDqAgAUS5cuXeLWW2+NE088sc7x3/3ud/HPf/4zOnfuHHkwePDg+J//+Z/sv59//vn41re+FWPHjo0tW7bEuHHjmuU1b7zxxlw05Bfb61//+pg2bVr232vXrs3+/Vx00UWxZs2arBG+NY9+SEmNVNb27dvXezz9W//BD34Qn/3sZ+scv/3223f4vOedd16ceeaZsW3btnjsscey0RVvfetb4/7774+jjjqq3vnXX3997LXXXrFp06aYP39+XHfddbFkyZJ6ycXKysqYOHFi9vl+8IMf3O33DQBA23H55ZfHQQcdlHWM+dOf/pQlO9J95t///ves7gdA22WkBlA2UmPsj370o3j11VfrHE8NqUOGDIk+ffpEHqReSB/4wAey7TOf+Ux2454ajq+99tpme83Um76YSZ/UyJ0H++yzT81n/elPfzp+//vfx4EHHpg1zqeG/dbql7/8ZZZ4ec973tNoLKSkxvZSLJx11lmNPu+xxx6bfRYpiZaSOuk5UjItJS8acu6552bnf+QjH4kf/vCHMXr06PjjH/8YixYtqnNe9+7d47TTTssqogAA0BRp1HC61/zwhz+cdfS6+OKL44knnoif//znpS4aACUmqQGUjdTL/F//+lfcc889Nce2bt0aP/7xj+N973tfg9ek0QlpuqEjjjgi6+3Tu3fvrIH2xRdfrHPez372s6wx+IADDsga/w855JC44oor6jV8pymNjjzyyHjkkUeyHu5du3bNkhRf/vKXd/t99ezZMw4//PDsBn53yp786le/ipNPPjmbrqhbt25x3HHHZQ3cja1DUT0d0fTp07NkSmroT9MWpedIPaNqS9empEsqX2pMT6/x/ve/vya5kUadpOm/0uf2xje+MXvONCXY9r7//e9nU22lz2zfffeNt7zlLfHrX/96l7+D1yJ9jumz2bhxY6xevbrm+IMPPpi9z4MPPjg7JyXI0oiD9O+toWmZHn/88ez81JifEidppMLLL7+809f/4he/mE39lJIqO3LHHXdk31f6DBqS/r0/8MADsXTp0ppjK1eujP/93/9tNBYactJJJ2V/t/+3tzvnn3rqqVmC7oUXXmjy6wMAwI7uNdP9bupok6ZOTffpQ4cObTDpkaZITaOc0z10qkukEdtjxozJRmtX1xsnT56cdYZL9+9pGuD0er/97W9b8B0C0FSSGkDZSDeoxx9/fJ0e6qkxf/369XXWHagtJQHSaIg3v/nN2XQ+qfE5Lbw8cuTIbIqfaqmHeWq4T2sDpPPSzW666U1T6mwvJRVOP/30GDRoUHzlK1/JEhKf+9znsrLsjjTyJE2flRr6d7fsKRmQGpMnTZoUV111VTbF1bx583b62mktj6997WvxiU98Irs2JTT+8z//M1atWlWvjOl108LVKWnxrne9K0tcvOMd78iSIunzmDFjRpbUSGVOn2NtU6dOjfPPPz8bMZKGmaf9lAhJjfC78x28FtUJnZSQqJYSZU8++WT2GaeEQ/r3dNttt2VJnIYSNGkERUqMpKmt0n+nsqf3tCOXXHJJ9n6++c1vxic/+ckdnnvfffdloyoakxJCqaJWO3E1d+7c7PPb0UiN7VUvdrj9v73dOT99X+mzSmUHAIBdtf295sMPPxxvetOb4tFHH83qBKnulZIRo0aNip/+9Kc117300ktZgiLdx6fRw6ku8dGPfjRLiKR6VvXac2k0SOqkdvXVV2edldLI6FTHSZ2FAGhlCgA5953vfCe1Khfuv//+wte//vXC3nvvXXj55Zezx9797ncX3vrWt2b/feCBBxbOOuusmuv+8Ic/ZNfdcsstdZ5v3rx59Y5XP19tH/nIRwpdu3YtbN68uebYySefnF373e9+t+bYli1bCn369Cm8613v2ul7SWU87bTTCmvWrMm2hx56qHD++ednz/mJT3xil8u+bt267PMYPnx44f/+7//qnFtVVVXz32PHjs1eu9ry5cuz59ljjz0K//znP2uO//nPf86OX3TRRXWuTccmTpxY5/nvuOOO7PgXv/jFOsfPPffcQkVFReHxxx/P9v/xj38U2rVrVzjnnHMK27Zta7SMTf0Otn8vjUnf1eGHH17zWS9durTwmc98Jitz7X8njb32D37wg+zc3//+9zXHpkyZkh374Ac/WOfc9N7233//Osdqf6f/8z//k30GN910007L/corr2SfX7pme9Wvn97PxRdfXDj00ENrHjvuuOMKlZWV9V679vc9derU7NqVK1dm/8bSNen4j370owZfZ9myZdn5Tz31VGHOnDnZv5eePXsWNm3aVK9szz33XHbN1VdfvdP3CABA21Vdv/vNb36T3Ws+88wzhR//+MfZfWbnzp2z/eRtb3tb4aijjqpTF0j1hxNOOKFw2GGH1RybPHly9ny33357vdeqrm+8+uqrWb2tthdffLHQu3fvevf26bnS/fD25U331AC0DCM1gLKSesX/3//9X7bmQOopn/42Nt1OWn8jDS1O0+KkYcfVW+pRnnq01x5qnKZeqpaeN52XevukKYVqT/GTpGvT3K/VOnXqlE2rlHr6N0WacilNOZW2tDjz9773vWyEwDXXXLPLZU8jDFJ5U8+l7RfTS6MRdib1ckrTZ1VL72P48OFx11131Tv3Yx/7WJ39dE5axPpTn/pUneNpOqpUF6geuZKmUkpTaaVRCmnqpcbKuCvfQVOl66o/6zSiJn3GaXTJ9ms/1H7ttFBheu3UKyxJC2NvL/X8qi2VM01VlXqA1ZY+h/Hjx2e9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tW+qe/fu3aJlzpv3v//98Ytf/CJeeumlmmN33313vPzyy3HOOedk+5s2bcqSS3/5y19i/vz50a5du+yxqipDuAAAgLrUMQAAaJNramzdujUWL14ckyZNqjmWbnRHjBgRCxcubPS6dON84IEHZjfDxx57bFx55ZVxxBFHNHjuli1bsq3ahg0bsr/p2u1vptN+oVCo2WqriJa1/es3xS9/+cvYa6+96hxLn+1nP/vZ2HPPPeP222+P888/Pzt+6623xjve8Y7s/PRa//Vf/1Xnum9/+9vRq1evePjhh+PII4+sKU9Dn82O3kPaGvqsm0P196eSBPknnimldrHr/w9m559pISqiqvT9acpLiX8j/Ua3DQ3VMT7/+c/X1DF++tOf1qtj7L333tn+u971rjrXpY5rPXv2jEceeSSrYwAAQO6SGmvXro1t27bVG2mR9pcuXdrgNW984xuzm+Gjjz461q9fH9OnT48TTjgha3x//etfX+/8adOmxdSpU+sdX7NmTWzevLnecOlUOXv11VezrbaO0bK2f/2dSeU+5ZRT4rrrrqtzfL/99sv+nnvuuXHLLbfEeeedl/WY+tnPfhbf//73a17nH//4R/Y53X///dn3Ul1JXb58eRx++OE15zX02ezoPaTn+de//hUdOzb/J5heK/2bSA2hKTkG5Jd4ppQG7CupUWwpitd1PShLbLSzkF/x7GC61pawcePGkr4+LeOtb31rXH/99fXqGB06dIj3vOc9WR0jJTWq6xi33XZbzXmpjjF58uT485//XKeOsWLFCkkNAADymdTYHccff3y2VUsJjQEDBsQ3v/nNuOKKK+qdn0YqpCHPtUdq9OvXL+shlNbmqC0lOVLlLN2gp62UdvX1U6Nf6kGVEhAN+cAHPpAlPV544YW45557Yo899oizzjqr5nXSSI00+uWGG26IAw44IKtwHHXUUVnSqfbnsSufTTovlSvNn9sSi7mnMqepydJ3qxEU8k08U0qPvtjS4zPbxkiN7h2XR88ND0lqFFOvXiV9+Za4v6P00miMQw89tNEpqE4++eRsPcTqOkZaWLza2WefndUxbrzxxpo6RkpmpBH7AACwu0racp8WkWvfvn2sWrWqzvG0n9bKaIo0AuCYY46Jxx9/vNGF7dK2vdRItn1DWdpPjWjVWynt7us3dt2b3/zmLJnzwx/+MH71q1/Fu9/97ujUqVP2WBpJsWzZsqyycdJJJ2XH7r333prnq/157MpnU31uQ591c2np14Nq/SfeWeoilF0D6H2fHiqeKYmqFp90sm2oiEKW0JDUKKIS/z76fSZ1MEt1jLlz59bUMapHaO+ojgEAALlNaqRG9SFDhmSLxo0aNSo7lnrvpP3x48c36TnSSIKHHnoozjzzzGjr0tohK1eurDdaIiWPkve9733ZQuyPPfZY/Pa3v605Z999981GU6RRGq973euy4eATJ05s8fIDAACtizoGAACtTcm7V6WpoVLvnZtvvjkeffTR+NjHPpbNx1pZWZk9PmbMmDoLiV9++eXx61//Op588slYsmRJNq3S008/HR/+8IejrZs3b15WYai9nXjiiXWGh6dF+fr27ZuN3Kjdyy7NfZsWbU/DwS+66KK45pprSvQuAACA1kIdAwCA1qbka2qMHj06W7Q7LSCXegANHjw4u3GuXjw89eipPbT9xRdfjHHjxmXnpt4/aaTHfffdFwMHDmzegl62Plqzm266Kdt2JK09khbdbciIESOyykhttc/t379/o9cCAADlVb9I1DEAAGiNSp7USNJUU41NN7VgwYI6+9dee222AQAAAAAAbUvJp58CAAAAAABoCkkNAAAAAAAgFyQ1AAAAAACAXJDUAAAAAAAAckFSowFVVVWlLkJZ8DkCAMD/4974tfMZAgCQdPAx/FunTp2iXbt28dxzz0XPnj2z/YqKilIXK3cKhUJs3bo11qxZk32e6XMEAICWMmvWrLjmmmti5cqVMWjQoLjuuuti2LBhDZ57yimnxO9+97t6x88888y48847X3NZ1DFeO/ULAABqk9SoJd0gH3TQQfH8889nlQ5em65du8Z//Md/ZJ8rAAC0hLlz58aECRNi9uzZMXz48Jg5c2aMHDkyli1bFr169ap3/u233541mFf717/+lSVC3v3udxelPOoYxaN+AQBAIqmxndTrJ90ov/rqq7Ft27ZSFye32rdvHx06dNALDQCAFjVjxowYN25cVFZWZvspuZFGXMyZMycmTpxY7/z99tuvzv5tt92WNZ4XK6mRqGO8duoXAABUk9RoQLpR7tixY7YBAAD5kEZcLF68OCZNmlRzLPXqHzFiRCxcuLBJz/Htb3873vve98aee+5Z1LKpYwAAQHFIagDQOt06OmLDg2lZ0FKXpLxc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AAAof7tVU3j55Zfjk5/8ZNx8883Z/mOPPZaNlEjH0rRQEydObNLzdOrUKYYMGZKNthg1alTNMPa0P378+CaXJ11Te4qp2jp37pxtDSVG0lYqVaHBpDlURCFrLNFgUkQljJM8EdPFJ56biZjeKfHcPMR0+cVzMe+li1W/AAAAyt9u1UTS6Ie//e1vsWDBgjo9tNIC33Pnzt2l50qjKG688casApNGXHzsYx+LTZs2RWVlZfb4mDFjsterlkZk3HPPPfHkk09m53/lK1+J733ve/GBD3xgd94KAABQYsWsXwAAAOVtt0Zq3HHHHVnl4k1velNUVPy7N+MRRxwRTzzxxC491+jRo7P1LSZPnhwrV67MppOaN29ezeLhK1asqNMLLCU8Pv7xj8c///nP2GOPPeLwww+P73//+9nzAAAA+VPM+gUAAFDediupkZIQvRqYwzclHGpXQpoqTTXV2HRTqbdWbV/84hezDQAAKA/Frl8AAADla7emnxo6dGjceeedNfvVFY1vfetbcfzxxxevdAAAQNlTvwAAAJp1pMaVV14ZZ5xxRjzyyCPx6quvxle/+tXsv++777743e9+tztPCQAAtFHqFwAAQLOO1DjxxBOzhfxSheOoo46KX//619lw8YULF8aQIUN25ykBAIA2Sv0CAABotpEar7zySnzkIx+JSy+9NG688cZdvRwAAKCG+gUAANCsIzU6duwYP/nJT3b1MgAAgHrULwAAgGaffmrUqFFxxx137M6lAAAAdahfAAAAzbpQ+GGHHRaXX355/PGPf8zmuN1zzz3rPP6pT31qd54WAABog9QvAACAZk1qfPvb347u3bvH4sWLs622iooKlQ4AAKDJ1C8AAIBmTWosX758dy4DAACoR/0CAABo1jU1aisUCtkGAADwWqlfAAAAzZLU+O53vxtHHXVU7LHHHtl29NFHx/e+973dfToAAKANK2b9YtasWdG/f//o0qVLDB8+PBYtWtSk62677bZsuqu0cDkAAFBG00/NmDEjLr300hg/fny8+c1vzo7de++98dGPfjTWrl0bF110UbHLCQAAlKli1i/mzp0bEyZMiNmzZ2cJjZkzZ8bIkSNj2bJl0atXr0ave+qpp+Liiy+Ok046qSjvCQAAaEVJjeuuuy6uv/76GDNmTM2xd7zjHXHEEUfEZZddJqkBAACUpH6REiTjxo2LysrKbD8lN+68886YM2dOTJw4scFrtm3bFu9///tj6tSp8Yc//CHWrVtXhHcFAAC0mqTG888/HyeccEK94+lYegwAAKCl6xdbt26NxYsXx6RJk2qOtWvXLkaMGBELFy5s9LrLL788G8XxoQ99KEtq7MyWLVuyrdqGDRuyv1VVVdlGeUjfZVrfxXdKKbQLaws1x2daiIqoeu3Ly1Kb38gmEdPFJZ7LN6abet+1W0mNQw89NH74wx/G5z//+XpDvQ877LDdeUoAAKCNKlb9Ik1VlUZd9O7du87xtL906dIGr0nTXH3729+OBx54oMmvM23atGxUx/bWrFkTmzdvbvLz0LqlSvX69euzxEZKjkFLGrCvBtBiS1G8rutBWUNou9AQXzSrV5e6BLkgpotLPJdvTG/cuLH5khrpBn706NHx+9//vmbO2z/+8Y8xf/78rDICAADQ2usXqdJ0/vnnx4033hg9evRo8nVpJEhat6P2SI1+/fpFz549o1u3bs1UWkqR1EgLx6fvVVKDlvboixWlLkJZ9uzu3nF59NzwkEbQYtrBelX8m5guLvFcvjHdpUuX5ktqvOtd74o///nPce2118Ydd9yRHRswYEAsWrQojjnmmN15SgAAoI0qVv0iJSbat28fq1atqnM87ffp06fe+U888US2QPjZZ59db8h7hw4dssXFDznkkHrXde7cOdu2lxq+NX6Xl5TU8L1SClWhAbQ5VEQhawDVCFpEfh+bREwXn3guz5hu6j3XbiU1kiFDhsT3v//93b0cAACgqPWLTp06Zc+TRniMGjWqJkmR9sePH1/v/MMPPzweeuihOscuueSSbATHV7/61Wz0BQAA0LrsVlLjrrvuynpAjRw5ss7xu+++O6s0nHHGGcUqHwAAUOaKWb9I00KNHTs2hg4dGsOGDYuZM2fGpk2borKyMnt8zJgx0bdv32xdjDS8/cgjj6xzfffu3bO/2x8HAABah90aTzJx4sRsAb7tpQXU0mMAAAClqF+ktTmmT58ekydPjsGDB2cLgM+bN69m8fAVK1bE888/X7SyAwAAORip8Y9//CMGDhzY4PDtxx9/vBjlAgAA2ohi1y/SVFMNTTeVLFiwYIfX3nTTTbv8eq1F/4l3lroIZbcI6X2fHlrqYgAAUIyRGvvss088+eST9Y6nCseee+65O08JAAC0UeoXAABAsyY13vnOd8anP/3peOKJJ+pUOP7nf/4n3vGOd+zOUwIAAG2U+gUAANCsSY0vf/nLWY+pNBz8oIMOyrb03/vvv382fy0AAEBTqV8AAADNuqZGGh5+3333xT333BN/+9vfYo899ohBgwbFSSedtDtPBwAAtGHqFwAAQLMkNRYuXBj/+te/4u1vf3tUVFTEaaedFs8//3xMmTIlXn755Rg1alRcd9110blz5115WgAAoA1Sv6DVu3V0xIYHI6Kq1CUpL5etL3UJAIC2Mv3U5ZdfHg8//HDN/kMPPRTjxo2LU089NSZOnBi/+MUvYtq0ac1RTgAAoMyoXwAAAM2a1HjggQfibW97W83+bbfdFsOGDYsbb7wxJkyYEF/72tfihz/84S4XAgAAaHvULwAAgGZNarz44ovRu3fvmv3f/e53ccYZZ9TsH3fccfHMM8/sciEAAIC2R/0CAABo1qRGqnAsX748+++tW7fGkiVL4k1velPN4xs3boyOHTvuciEAAIC2R/0CAABo1qTGmWeemc1t+4c//CEmTZoUXbt2jZNOOqnm8QcffDAOOeSQXS4EAADQ9qhfAAAAu6rDrpx8xRVXxH/913/FySefHHvttVfcfPPN0alTp5rH58yZE6eddtouFwIAAGh71C8AAIBmTWr06NEjfv/738f69euzSkf79u3rPP6jH/0oOw4AALAz6hcAAECzJjWq7bPPPg0e32+//Xbn6QAAgDZM/QIAAGiWNTUAAAAAAABKRVIDAAAAAADIBUkNAAAAAAAgFyQ1AAAAAACAXJDUAAAAAAAAckFSAwAAAAAAyAVJDQAAAAAAIBckNQAAAAAAgFyQ1AAAAAAAAHJBUgMAAAAAAMgFSQ0AAAAAACAXJDUAAAAAAIBckNQAAAAAAAByQVIDAAAAAADIBUkNAAAAAAAgFyQ1AAAAAACAXJDUAAAAAAAAckFSAwAAAAAAyAVJDQAAAAAAIBckNQAAAAAAgFyQ1AAAAAAAAHJBUgMAAAAAAMgFSQ0AAAAAACAXJDUAAAAAAIBckNQAAAAAAAByQVIDAAAAAADIBUkNAAAAAAAgFyQ1AAAAAACAXJDUAAAAysqsWbOif//+0aVLlxg+fHgsWrSo0XNvv/32GDp0aHTv3j323HPPGDx4cHzve99r0fICAABNJ6kBAACUjblz58aECRNiypQpsWTJkhg0aFCMHDkyVq9e3eD5++23X3zhC1+IhQsXxoMPPhiVlZXZdvfdd7d42QEAgJ2T1AAAAMrGjBkzYty4cVliYuDAgTF79uzo2rVrzJkzp8HzTznllDjnnHNiwIABccghh8SFF14YRx99dNx7770tXnYAAGDnJDUAAICysHXr1li8eHGMGDGi5li7du2y/TQSY2cKhULMnz8/li1bFm95y1uaubQAAMDu6BCtZM7ba665JlauXJkND7/uuuti2LBhDZ574403xne/+934+9//nu0PGTIkrrzyykbPBwAA2oa1a9fGtm3bonfv3nWOp/2lS5c2et369eujb9++sWXLlmjfvn184xvfiFNPPbXR89N5aau2YcOG7G9VVVW2lUq7KJTstctR+jwLURFV+gIWXwnjJC/Ec/GJ6WYinptETBeXeC7fmG7qvXSH1jLnbRoWnhbxmzlzZjbnbeod1atXr3rnL1iwIM4777w44YQTsoX/rr766jjttNPi4YcfzioiAAAAu2LvvfeOBx54IF566aVspEaqnxx88MHZ1FQNmTZtWkydOrXe8TVr1sTmzZujVAbsq8GkmFIzybquB2WNJu1Co11RNbLGDf8mnotPTDcT8dwkYrq4xHP5xvTGjRvzkdSoPedtkpIbd955Zzbn7cSJE+udf8stt9TZ/9a3vhU/+clPssrHmDFjWqzcAABA69KjR49spMWqVavqHE/7ffr0afS6NEXVoYcemv334MGD49FHH80SF40lNSZNmpQlPmqP1OjXr1/07NkzunXrFqXy6IsVJXvtcu0F2r3j8ui54SENJsXWQAdG6hLPxSemm4l4bhIxXVziuXxjOg1iaPVJjeo5b1OlYHfmvE1efvnleOWVV2K//fZrxpICAACtXadOnbLpaVOHp1GjRtUMYU/748ePb/LzpGtqTy+1vc6dO2fb9lJdJm2lUhUaTIqtIgpZY4kGkyIrYZzkhXhuHmK6GYjnJhHTxSeeyzOmm3ov3SGPc97W9rnPfS4OOOCAOosB1ma+27bDfHrNxPyYTSKmi0s8NyMxvVPiufjEdNue77alpREUY8eOjaFDh2br7qXpbTdt2lQzMjyN7k7T1qaRGEn6m8495JBDsnrDXXfdFd/73vfi+uuvL/E7AQAAWuX0U6/FVVddFbfddlu2zkZjQ1PMd9t2mE+vmZgfs0nEdHGJ52Yk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2WXZYvPVLrjgghg1alRJywTsPjEN5UM8t22SGpStf/3rX9kP3HHHHRc9e/aM//iP/4izzjorbrvttigUCvXO/9KXvhQnnHBCdO3aNbp3716SMgPFi+l3vOMd2TldunSJ173udXH++efHc889V5KyA68tnvv37x8VFRV1tquuuqokZQdoyCuvvBI33HBDjBgxIvr27Rt9+vTJ6hbTp0+Pl19+ud75t99+e5x22mmx//77Z79pDzzwQEnKDRQnptN9zeGHHx577rln7Lvvvtl1f/7zn0tSduC1xXNKjmxf9zj99NNLUnYaJ6lB7qTGjldffbXe8a1bt9b8969//et4wxveEPfff39cfPHF2X6qOLz97W+PK664IkaOHBmbNm2qd/273/3u+NjHPtYi7wNo3ph+61vfGj/84Q9j2bJl8ZOf/CSeeOKJOPfcc1vkPUFb1VzxnFx++eXx/PPP12yf/OQnm/39AK1b7d+WUnryySfj2GOPjVmzZmX3Gj/60Y+y37ZPf/rTMX/+/DjiiCPiscceq3NN+p078cQT4+qrry5ZuaG1yXNMp3ubr3/96/HQQw/Fvffem3XISInLNWvWlOx9QCnlOZ6TlMSoXff4wQ9+UJLyswMFaGEnn3xyYfz48YULL7yw0L1790KvXr0KN9xwQ+Gll14qXHDBBYW99tqrcMghhxTuuuuu7Pzf/va3qctmtn/ssccWOnbsmB1Lz/OJT3wie57999+/cMopp2Tn33///YX99tuv8POf/7zB13/llVcKlZWVhbPPPrvBx7/zne8U9tlnn2b8BKC8tPaYrvazn/2sUFFRUdi6dWszfApQHlprPB944IGFa6+9tgU+AaA1a+i35aGHHiqcfvrphT333DP7zfrABz5QWLNmTc0127ZtK1x99dXZb1enTp0K/fr1K3zxi1+sefyzn/1s4bDDDivssccehYMOOqhwySWX1LlXmDJlSmHQoEE1+2PHji28853vrNlft25d4dBDDy1ceumlhaqqqgbLnX5H0+/YCy+8UO+x5cuXZ7+jf/3rX4vyGUGelGNMV1u/fn0W27/5zW9e02cEeVFO8bz989A6GalBSdx8883Ro0ePWLRoUdbTMo2OSKMk0vCvJUuWZD0a0lQxtYeBTZw4MZtq4tFHH42jjz665nk6deoUf/zjH2P27NnZsfR8aSqps88+Ox55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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "# Create figure with two subplots side by side\n",
+ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 5))\n",
+ "\n",
+ "# Get MRR and Recall columns\n",
+ "mrr_cols = [\"mrr@1\", \"mrr@3\", \"mrr@5\"]\n",
+ "recall_cols = [\"recall@1\", \"recall@3\", \"recall@5\"]\n",
+ "x = np.arange(len(mrr_cols))\n",
+ "width = 0.35\n",
+ "\n",
+ "# Plot MRR bars\n",
+ "ax1.bar(x - width / 2, df.loc[\"Train\", mrr_cols], width, label=\"Train\")\n",
+ "ax1.bar(x + width / 2, df.loc[\"Eval\", mrr_cols], width, label=\"Eval\")\n",
+ "ax1.set_title(\"Mean Reciprocal Rank (MRR)\")\n",
+ "ax1.set_xticks(x)\n",
+ "ax1.set_xticklabels(mrr_cols)\n",
+ "ax1.set_ylabel(\"Score\")\n",
+ "ax1.legend()\n",
+ "ax1.grid(True, alpha=0.3)\n",
+ "\n",
+ "# Plot Recall bars\n",
+ "ax2.bar(x - width / 2, df.loc[\"Train\", recall_cols], width, label=\"Train\")\n",
+ "ax2.bar(x + width / 2, df.loc[\"Eval\", recall_cols], width, label=\"Eval\")\n",
+ "ax2.set_title(\"Recall\")\n",
+ "ax2.set_xticks(x)\n",
+ "ax2.set_xticklabels(recall_cols)\n",
+ "ax2.set_ylabel(\"Score\")\n",
+ "ax2.legend()\n",
+ "ax2.grid(True, alpha=0.3)\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Conclusion\n",
+ "\n",
+ "In this notebook, we've laid the foundation for fine-tuning by creating high-quality synthetic training data. Our analysis shows that our train and eval sets have similar performance metrics, suggesting they represent the same underlying patterns and will give us reliable estimates of real-world performance.\n",
+ "\n",
+ "While our approach works well for getting started, production systems need more robust testing - like generating similar but different labels, using multiple test sets, and carefully preventing data leakage. We'll explore some of these ideas in Week 4 when we look at handling different types of queries.\n",
+ "\n",
+ "In the next two notebooks, we'll put this data to work: first using Cohere's managed re-ranker service as an easy starting point, then exploring open-source fine-tuning for more control. This builds on Week 1's evaluation framework while preparing us for more advanced query handling in future weeks.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/cohort_2/week2/2. Finetune Cohere Logfire.ipynb b/cohort_2/week2/2. Finetune Cohere Logfire.ipynb
new file mode 100644
index 0000000..d96d224
--- /dev/null
+++ b/cohort_2/week2/2. Finetune Cohere Logfire.ipynb
@@ -0,0 +1,751 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Week 2: Getting Started with Re-ranker Fine-tuning\n",
+ "\n",
+ "> **Prerequisites**: Complete the `1. Synthetic Transactions.ipynb` notebook first to generate the evaluation dataset we'll use for fine-tuning. You'll also need a Cohere API Key which you can get by signing up for a free account on [Cohere](https://cohere.com/).\n",
+ "\n",
+ "When improving RAG systems, starting with a managed re-ranker service can provide quick wins with minimal engineering overhead. This notebook demonstrates how to fine-tune a Cohere re-ranker for better retrieval performance using just a few hundred examples.\n",
+ "\n",
+ "## Why This Matters\n",
+ "\n",
+ "Re-rankers offer several advantages when you're just getting started:\n",
+ "\n",
+ "1. **Data Efficiency**\n",
+ " - Work effectively with limited data (as few as 256 examples)\n",
+ " - Only need to learn ranking within small candidate sets \n",
+ " - Can achieve significant gains without massive training sets\n",
+ "\n",
+ "2. **Easy Integration** \n",
+ " - Drop-in addition to existing retrieval pipelines\n",
+ " - Progressive improvement without system overhauls\n",
+ " - No need to re-embed your entire document collection\n",
+ "\n",
+ "3. **Quick Implementation**\n",
+ " - Hosted services handle infrastructure complexity\n",
+ " - No hyperparameter tuning required \n",
+ " - Focus on experimentation rather than deployment\n",
+ "\n",
+ "## What You'll Learn\n",
+ "\n",
+ "Through hands-on examples, you'll discover how to:\n",
+ "\n",
+ "1. **Prepare Training Data**\n",
+ " - Format data for re-ranker fine-tuning\n",
+ " - Create effective training examples\n",
+ " - Generate hard negatives\n",
+ "\n",
+ "2. **Fine-tune Models**\n",
+ " - Configure training parameters\n",
+ " - Monitor training progress\n",
+ " - Validate model improvements\n",
+ "\n",
+ "3. **Evaluate Performance**\n",
+ " - Compare against baseline retrieval\n",
+ " - Measure recall and MRR improvements\n",
+ " - Analyze result quality\n",
+ "\n",
+ "By the end of this notebook, you'll have a fine-tuned re-ranker improving your retrieval results. This builds on Week 1's evaluation framework while preparing you for more advanced fine-tuning using open-source models in notebook 3.\n",
+ "\n",
+ "## Preparing Our Training Data\n",
+ "\n",
+ "In this notebook, we'll be using the synthetic transactions dataset we created in the previous notebook to fine-tune a Cohere re-ranker. [Cohere will use the most recent base model for fine-tuning](https://docs.cohere.com/v2/docs/rerank-starting-the-training#parameters) so do check their website to see which model version is the latest one that they've released.\n",
+ "\n",
+ "We'll then create hard negatives so that our re-ranker learns the difference between similar categories.\n",
+ "\n",
+ "### Hard Negatives\n",
+ "\n",
+ "A key aspect of effective re-ranker training is the selection of hard negatives - examples that are similar to the correct answer but shouldn't be ranked highly. For instance:\n",
+ "\n",
+ "Query:\n",
+ "```\n",
+ "Name: Ayden\n",
+ "Category: Financial Software\n",
+ "Department: Finance\n",
+ "Location: Berlin, DE\n",
+ "Amount: 1273.45\n",
+ "```\n",
+ "\n",
+ "Positive Example:\n",
+ "```\n",
+ "Subscription & Revenue Infrastructure\n",
+ "```\n",
+ "\n",
+ "Hard Negatives:\n",
+ "```\n",
+ "Office Equipment Maintenance\n",
+ "Office Supplies & Stationery\n",
+ "Human Resources\n",
+ "```\n",
+ "\n",
+ "Hard negatives help your model by:\n",
+ "1. Improving discriminative ability between similar categories\n",
+ "2. Building robustness against noisy results\n",
+ "3. Making efficient use of limited training data\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Creating Our Dataset\n",
+ "\n",
+ "In this section, we'll use the examples that we saved to our local data directory to create a dataset to train our Cohere re-ranker.\n",
+ "\n",
+ "We'll use the `train` examples to train our re-ranker and the `eval` examples to benchmark our model. Once we've formatted our data in the correct format, we'll save it to a file and upload it to Cohere."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/ivanleo/Documents/coding/systematically-improving-rag/cohort_2/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020\n",
+ " warnings.warn(\n",
+ "/Users/ivanleo/Documents/coding/systematically-improving-rag/cohort_2/.venv/lib/python3.9/site-packages/pydantic_evals/dataset.py:390: UserWarning: Could not determine the generic parameters for ; using `Any` for each. You should explicitly set the generic parameters via `Dataset[MyInputs, MyOutput, MyMetadata]` when serializing or deserializing.\n",
+ " warnings.warn(\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pydantic_evals import Dataset\n",
+ "\n",
+ "train_dataset: Dataset = Dataset.from_file(\"./data/train_transactions.yml\")\n",
+ "eval_dataset: Dataset = Dataset.from_file(\"./data/eval_transactions.yml\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pydantic import BaseModel\n",
+ "\n",
+ "\n",
+ "# Define Pydantic model to store our finetuning data\n",
+ "class FinetuneItem(BaseModel):\n",
+ " query: str\n",
+ " relevant_passages: list[str]\n",
+ " hard_negatives: list[str]\n",
+ "\n",
+ "\n",
+ "# Get all the labels in our dataset\n",
+ "labels = set([item.expected_output[0] for item in train_dataset.cases])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random\n",
+ "\n",
+ "finetuning_data = [\n",
+ " FinetuneItem(\n",
+ " query=transaction.inputs,\n",
+ " relevant_passages=transaction.expected_output,\n",
+ " hard_negatives=random.sample(\n",
+ " [label for label in labels if label != transaction.expected_output[0]], k=4\n",
+ " ),\n",
+ " )\n",
+ " for transaction in train_dataset.cases\n",
+ " # Generate 2 samples with 4 hard negatives each for each transaction\n",
+ " for _ in range(2)\n",
+ "]\n",
+ "\n",
+ "with open(\"./data/cohere_finetune.jsonl\", \"w\") as f:\n",
+ " for item in finetuning_data:\n",
+ " f.write(item.model_dump_json() + \"\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Finetuning Our Model\n",
+ "\n",
+ "Now that we've created our dataset, we can upload it to Cohere and kick off our fine-tuning job. \n",
+ "\n",
+ "### Uploading Our Dataset\n",
+ "\n",
+ "A reminder that the fine-tuning itself will take around 1 hour to a day so you'll need to come back to this notebook later down the line when this is done.\n",
+ "\n",
+ "Once the dataset has a status `validated`, we can kick off our fine-tuning job."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "...\n",
+ "...\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "'validated'"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import cohere\n",
+ "\n",
+ "co = cohere.ClientV2()\n",
+ "\n",
+ "reranked_dataset = co.datasets.create(\n",
+ " name=\"Synthetic Transactions Finetune\",\n",
+ " data=open(\"./data/cohere_finetune.jsonl\", \"rb\"),\n",
+ " type=\"reranker-finetune-input\",\n",
+ ")\n",
+ "\n",
+ "co.wait(reranked_dataset).dataset.validation_status"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Submitting Our Finetuning Job\n",
+ "\n",
+ "Now that we've uploaded our dataset, we can kick off our fine-tuning job. Make sure to indicate that you're doing a re-ranker finetune when creating the job. You can check the status of your job at any time by calling `co.finetuning.get_finetuned_model` or visiting the Cohere dashboard."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from cohere.finetuning import BaseModel, FinetunedModel, Settings\n",
+ "\n",
+ "finetune_request = co.finetuning.create_finetuned_model(\n",
+ " request=FinetunedModel(\n",
+ " name=\"finetuned-cohere-reranker\",\n",
+ " settings=Settings(\n",
+ " base_model=BaseModel(base_type=\"BASE_TYPE_RERANK\"),\n",
+ " dataset_id=reranked_dataset.id,\n",
+ " ),\n",
+ " )\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'STATUS_READY'"
+ ]
+ },
+ "execution_count": 29,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "response = co.finetuning.get_finetuned_model(finetune_request.finetuned_model.id)\n",
+ "response.finetuned_model.status"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Benchmarking our Model\n",
+ "\n",
+ "We want to quantify the improvement that fine-tuning a model gets us. In order to do so, we'll be using the same retrieval evals as before to benchmark our fine-tuned model. Since we're building a model here that will suggest relevant categories for a given transaction, we'll be measuring the following two metrics. \n",
+ "\n",
+ "- Recall : Whether the correct category is in the top N results\n",
+ "- MRR : The mean reciprocal rank of the correct category in the top N results\n",
+ "\n",
+ "We want to mainly measure two things\n",
+ "\n",
+ "- How much of an improvement does a fine-tuned model get us over a pure embedding based approach\n",
+ "- How does the fine-tuned model perform against the default Cohere re-ranker\n",
+ "\n",
+ "In order to do so, we'll be benchmarking our fine-tuned model against the default text-embedding-3-small model as well as the default Cohere re-ranker. \n",
+ "\n",
+ "We'll use `Logfire` here to run our evaluations and compare the results between our different configurations since it's where we've stored our evaluation data and provides an easy way to share our results with others.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lancedb\n",
+ "\n",
+ "db = lancedb.connect(\"./lancedb\")\n",
+ "table = db.open_table(\"categories\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from helpers import get_metrics_at_k, task\n",
+ "from dataclasses import dataclass\n",
+ "from pydantic_evals.evaluators import Evaluator, EvaluatorContext\n",
+ "from lancedb.table import Table\n",
+ "from concurrent.futures import ThreadPoolExecutor\n",
+ "from functools import partial\n",
+ "import logfire\n",
+ "import asyncio\n",
+ "\n",
+ "\n",
+ "@dataclass\n",
+ "class RagMetricsEvaluator(Evaluator):\n",
+ " async def evaluate(self, ctx: EvaluatorContext[str, str]) -> dict[str, float]:\n",
+ " predictions = ctx.output\n",
+ " labels = ctx.expected_output\n",
+ " metrics = get_metrics_at_k(metrics=[\"mrr\", \"recall\"], sizes=[1, 3, 5])\n",
+ " return {\n",
+ " metric: score_fn(predictions, labels)\n",
+ " for metric, score_fn in metrics.items()\n",
+ " }\n",
+ "\n",
+ "\n",
+ "async def retrieve_results(\n",
+ " question: str,\n",
+ " table: Table,\n",
+ " pool: ThreadPoolExecutor,\n",
+ " max_k=25,\n",
+ " reranker=None,\n",
+ "):\n",
+ " loop = asyncio.get_running_loop()\n",
+ " return await loop.run_in_executor(\n",
+ " pool,\n",
+ " partial(task, user_query=question, table=table, max_k=max_k, reranker=reranker),\n",
+ " )\n",
+ "\n",
+ "\n",
+ "logfire.configure(\n",
+ " send_to_logfire=True,\n",
+ " environment=\"experimentation\",\n",
+ " service_name=\"synthetic-transactions\",\n",
+ " console=False,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from lancedb.rerankers import CohereReranker\n",
+ "\n",
+ "\n",
+ "rerankers = [\n",
+ " # Remember to replace this with your fine-tuned model id\n",
+ " CohereReranker(model_name=\"0486d248-8476-40f6-a5bf-1ef6fd8a65dd-ft\"),\n",
+ " CohereReranker(model_name=\"rerank-english-v3.0\"),\n",
+ " None,\n",
+ "]\n",
+ "\n",
+ "results = []\n",
+ "\n",
+ "\n",
+ "# eval_dataset.add_evaluator(RagMetricsEvaluator())\n",
+ "\n",
+ "for reranker in rerankers:\n",
+ " with ThreadPoolExecutor(max_workers=10) as executor:\n",
+ " evaluation_result = await eval_dataset.evaluate(\n",
+ " partial(\n",
+ " retrieve_results,\n",
+ " table=table,\n",
+ " pool=executor,\n",
+ " reranker=reranker,\n",
+ " max_k=25,\n",
+ " )\n",
+ " )\n",
+ " results.append(evaluation_result)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.803\n",
+ "mrr@3 0.8788\n",
+ "mrr@5 0.8818\n",
+ "recall@1 0.803\n",
+ "recall@3 0.9697\n",
+ "recall@5 0.9848\n",
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.1515\n",
+ "mrr@3 0.2146\n",
+ "mrr@5 0.2177\n",
+ "recall@1 0.1515\n",
+ "recall@3 0.303\n",
+ "recall@5 0.3182\n",
+ "Metric Score\n",
+ "-------- -------\n",
+ "mrr@1 0.3788\n",
+ "mrr@3 0.4798\n",
+ "mrr@5 0.5131\n",
+ "recall@1 0.3788\n",
+ "recall@3 0.6061\n",
+ "recall@5 0.7424\n"
+ ]
+ }
+ ],
+ "source": [
+ "from tabulate import tabulate\n",
+ "\n",
+ "\n",
+ "def format_results(result):\n",
+ " return tabulate(\n",
+ " [[item, round(value, 4)] for item, value in result.averages().scores.items()],\n",
+ " headers=[\"Metric\", \"Score\"],\n",
+ " )\n",
+ "\n",
+ "\n",
+ "for result in results:\n",
+ " print(format_results(result))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Analysing Our Results\n",
+ "\n",
+ "Now that we've run our evaluations, let's take a closer look at the results. We want to compare the performance of our fine-tuned model against the default Cohere re-ranker as well as the default vector search baseline without a re-ranker."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " mrr@1 \n",
+ " mrr@3 \n",
+ " mrr@5 \n",
+ " recall@1 \n",
+ " recall@3 \n",
+ " recall@5 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " Fine-Tuned Reranker \n",
+ " 0.83 \n",
+ " 0.88 \n",
+ " 0.89 \n",
+ " 0.83 \n",
+ " 0.94 \n",
+ " 0.98 \n",
+ " \n",
+ " \n",
+ " Default Reranker \n",
+ " 0.24 \n",
+ " 0.29 \n",
+ " 0.30 \n",
+ " 0.24 \n",
+ " 0.35 \n",
+ " 0.41 \n",
+ " \n",
+ " \n",
+ " No Reranker \n",
+ " 0.39 \n",
+ " 0.52 \n",
+ " 0.54 \n",
+ " 0.39 \n",
+ " 0.67 \n",
+ " 0.77 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " mrr@1 mrr@3 mrr@5 recall@1 recall@3 recall@5\n",
+ "Fine-Tuned Reranker 0.83 0.88 0.89 0.83 0.94 0.98\n",
+ "Default Reranker 0.24 0.29 0.30 0.24 0.35 0.41\n",
+ "No Reranker 0.39 0.52 0.54 0.39 0.67 0.77"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "scores = []\n",
+ "\n",
+ "for result in results:\n",
+ " result_scores = {}\n",
+ " for score_name, score in result.summary.scores.items():\n",
+ " result_scores[score_name] = score.score\n",
+ " scores.append(result_scores)\n",
+ "\n",
+ "df = pd.DataFrame(\n",
+ " scores, index=[\"Fine-Tuned Reranker\", \"Default Reranker\", \"No Reranker\"]\n",
+ ")\n",
+ "df.round(2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " mrr@1 \n",
+ " mrr@3 \n",
+ " mrr@5 \n",
+ " recall@1 \n",
+ " recall@3 \n",
+ " recall@5 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " Fine-Tuned Reranker \n",
+ " 0.83 \n",
+ " 0.88 \n",
+ " 0.89 \n",
+ " 0.83 \n",
+ " 0.94 \n",
+ " 0.98 \n",
+ " \n",
+ " \n",
+ " Default Reranker \n",
+ " 0.24 \n",
+ " 0.29 \n",
+ " 0.30 \n",
+ " 0.24 \n",
+ " 0.35 \n",
+ " 0.41 \n",
+ " \n",
+ " \n",
+ " No Reranker \n",
+ " 0.39 \n",
+ " 0.52 \n",
+ " 0.54 \n",
+ " 0.39 \n",
+ " 0.67 \n",
+ " 0.77 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " mrr@1 mrr@3 mrr@5 recall@1 recall@3 recall@5\n",
+ "Fine-Tuned Reranker 0.83 0.88 0.89 0.83 0.94 0.98\n",
+ "Default Reranker 0.24 0.29 0.30 0.24 0.35 0.41\n",
+ "No Reranker 0.39 0.52 0.54 0.39 0.67 0.77"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df = pd.DataFrame(\n",
+ " results.values(), index=[\"Fine-Tuned Reranker\", \"Default Reranker\", \"No Reranker\"]\n",
+ ")\n",
+ "df.round(2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAHqCAYAAABMTMx9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABrkklEQVR4nO3dC7hUVfk4/sUdURGVmyKK19QCUbyEaVopaqZiWWR+BUlRS/KWqZSCaIq3CCvTxEwrTdJKS00r8i6GaV5KwXuoiWAqIAYonP/zrt8z538OHOCAnDNnz3w+zzMeZs/es9fsmbXde71rvatVTU1NTQIAAAAAAGjhWpe7AAAAAAAAAI0hqAEAAAAAABSCoAYAAAAAAFAIghoAAAAAAEAhCGoAAAAAAACFIKgBAAAAAAAUgqAGAAAAAABQCIIaAAAAAABAIQhqAAAAAAAAhSCoAUA66qijUp8+fVIlKMpnadWqVRo5cuRqb//uu++m7t27p+uvvz61ZO+//37q3bt3+vGPf1zuogAAwBq5jj/nnHNqn1977bV52csvv1zWcgFUE0ENoPBKF5HxeOCBB5Z5vaamJjeqxuuf+9znUksWjfGlzxKPtddeO+26667p5z//ebmLVnH23nvvesd6rbXWSv369UsTJkxIS5YsSS3dZZddltZdd9305S9/uXZZ3FzFZ2ndunV65ZVXltlm7ty5+XMuHVCJG7C6xyK232CDDdIBBxyQpkyZssz7lPZTerRr1y7/dk888cT0zjvv1Fs3Xjv11FPT+eefnxYsWLDGjwMAAJV7fxePtm3bpl69euXOS6+99lq5iwdAC9C23AUAWFM6duyYbrjhhrTHHnvUW37vvfemV199NXXo0CEVQf/+/dM3v/nN/O/XX389XX311WnYsGFp4cKFacSIEU2yz4kTJxaiIX9N22STTdK4cePyv9988838+znllFPS7NmzcyN8Sx79EEGNKGubNm2WeT1+67/61a/S6aefXm/5b3/72xW+7+GHH54++9nPpsWLF6dnn302j6741Kc+lR555JHUt2/fZda/4oor0jrrrJPmz5+fJk+enH74wx+mxx57bJng4vDhw9OZZ56Zj+9Xv/rV1f7cAABUj3PPPTdtvvnmuWPMww8/nIMdcZ35z3/+M9/7AVC9jNQAKkY0xt50003pgw8+qLc8GlIHDBiQevbsmYogeiH93//9X35861vfyhfu0XD8/e9/v8n2Gb3p12TQJxq5i2C99darPdYnn3xyuu+++9Jmm22WG+ejYb+luu2223Lg5Utf+tJy60IENZYWdeHAAw9c7vvutNNO+VhEEC2COvEeEUyL4EVDDjvssLz+cccdl37961+nIUOGpAcffDBNnTq13npdunRJgwYNyjeiAADQGDFqOK41jznmmNzR67TTTksvvPBC+v3vf1/uogFQZoIaQMWIXub//e9/05///OfaZYsWLUo333xz+spXvtLgNjE6IdINffSjH829fXr06JEbaN9+++1669166625MXjjjTfOjf9bbrllOu+885Zp+I6URh/72MfS008/nXu4d+rUKQcpLr744tX+XN26dUvbbrttvoBfnbKHP/7xj2mvvfbK6Yo6d+6cdtlll9zAvbx5KErpiC699NIcTImG/khbFO8RPaPqim0j6BLli8b02McRRxxRG9yIUSeR/iuO20c+8pH8npESbGm//OUvc6qtOGbrr79++uQnP5n+9Kc/rfJ38GHEcYxjM2/evDRr1qza5U8++WT+nFtssUVeJwJkMeIgfm8NpWV6/vnn8/rRmB+Bkxip8N577610/9/97ndz6qcIqqzILbfckr+vOAYNid/7448/nqZNm1a7bObMmemvf/3rcutCQ/bcc8/8d+nf3uqsv+++++YA3VtvvdXo/QMAwIquNeN6NzraROrUuE7feeedGwx6RIrUGOUc19BxLxEjtocOHZpHa5fuG0ePHp07w8X1e6QBjv3dfffdzfgJAWgsQQ2gYsQF6sCBA+v1UI/G/Dlz5tSbd6CuCALEaIhPfOITOZ1PND7HxMv77bdfTvFTEj3Mo+E+5gaI9eJiNy56I6XO0iKosP/++6cddtghfe9738sBiTPOOCOXZXXEyJNInxUN/atb9ggGRGPyqFGj0oUXXphTXN15550r3XfM5fGDH/wgnXDCCXnbCGh8+tOfTm+88cYyZYz9xsTVEbT4whe+kAMXBx98cA6KxPEYP358DmpEmeM41jV27Nh05JFH5hEjMcw8nkcgJBrhV+c7+DBKAZ0ISJREoOzFF1/MxzgCDvF7uvHGG3MQp6EATYygiMBIpLaKf0fZ4zOtyFlnnZU/z09+8pP0jW98Y4XrPvTQQ3lUxfJEQChu1OoGriZNmpSP34pGaiytNNnh0r+91Vk/vq84VlF2AABYVUtfa/7rX/9KH//4x9MzzzyT7wni3iuCEYMHD06/+93vard79913c4AiruNj9HDcSxx//PE5IBL3WaW552I0SHRSu+iii3JnpRgZHfc40VkIgBamBqDgfvazn0Wrcs0jjzxS86Mf/ahm3XXXrXnvvffya1/84hdrPvWpT+V/b7bZZjUHHnhg7Xb3339/3u7666+v93533nnnMstL71fXcccdV9OpU6eaBQsW1C7ba6+98rY///nPa5ctXLiwpmfPnjVf+MIXVvpZooyDBg2qmT17dn489dRTNUceeWR+zxNOOGGVy/7OO+/k47HbbrvV/O9//6u37pIlS2r/PWzYsLzvkpdeeim/z1prrVXz6quv1i7/29/+lpefcsop9baNZWeeeWa997/lllvy8u9+97v1lh922GE1rVq1qnn++efz8+eee66mdevWNYceemjN4sWLl1vGxn4HS3+W5Ynvatttt6091tOmTav51re+lctc93eyvH3/6le/yuved999tcvGjBmTl331q1+tt258tg033LDesrrf6Te/+c18DK699tqVlvv999/Pxy+2WVpp//F5TjvttJqtttqq9rVddtmlZvjw4cvsu+73PXbs2LztzJkz828stonlN910U4P7mT59el7/5Zdfrrnmmmvy76Vbt2418+fPX6Zs//nPf/I2F1100Uo/IwAA1at0f/eXv/wlX2u+8sorNTfffHO+zuzQoUN+Hj7zmc/U9O3bt969QNw/7L777jVbb7117bLRo0fn9/vtb3+7zL5K9xsffPBBvm+r6+23367p0aPHMtf28V5xPbx0eeOaGoDmYaQGUFGiV/z//ve/POdA9JSPv8tLtxPzb8TQ4kiLE8OOS4/oUR492usONY7USyXxvrFe9PaJlEJ1U/yE2DZyv5a0b98+p1WKnv6NESmXIuVUPGJy5l/84hd5hMAll1yyymWPEQZR3ui5tPRkejEaYWWil1OkzyqJz7HbbrulO+64Y5l1v/a1r9V7HuvEJNYnnnhiveWRjiruBUojVyKVUqTSilEKkXppeWVcle+gsWK70rGOETVxjGN0ydJzP9Tdd0xUGPuOXmEhJsZeWvT8qivKGamqogdYXXEcRo4cmXuLRfqtmMtiZWLETWy3stET8buPNFgxyXfp78pST40ZMyYfi0iv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1o85cg488MD0qU99Kj3++OPp5JNPTsccc0y66667qvo4wofh9w4ANCXppwAAgIpxwAEH5EdjXXnllWnzzTdP3/ve9/Lz7bbbLj3wwAPp+9//ftpvv/2asKQAAMDqENQAAKhie++9d+rfv3+aMGFCqiZHHXVUnuegMamJWqpq/e7WtClTpqR99tmn3rIIZsSIjeVZuHBhfpTMnTs3/12yZEl+1BXPYx6G0qMSxKiWGA1Tbb+94cOH5/PGr3/96/y8iN9nc313pd97Q3UCyqV0PvabhOJRf6vHkkZ+x4IaAAAfUp8zb2/W/b184YGr3IB/3XXXLbP8ueeeS7/97W9Tu3btUlOIeQqiAW1F7r777tw439IsXfauXbumXXbZJV100UWpb9++ZS0ba9bMmTNTjx496i2L5xGo+N///lc76XFd48aNS2PHjl1m+ezZs9OCBQvqLYs5BeLmLCaXjkfJ1mf/KTWn584btErrH3300ekXv/jFMsuffvrpNGnSpHzeqPt51pR777037bvvvitc589//nPaa6+9UnMrNdCX5ldaOsXS0mWP88bOO++czj///BZz3igFG5riu6sr3j+O1X//+98m+38MrKr4Tc6ZMyfXgdatZWOHIlF/q8e8efMatZ6gBgBAFdh///3Tz372s3rLunXrltq0adNk+9x9993T66+/Xvv8pJNOyg3FdcuxwQYbpJZs+vTpqXPnzuk///lP+ta3vpXnXnj++edXe7L4RYsWtdiJ5lty2VqaUaNGpVNPPbX2efyue/funetU/F7qiiBH3Jy1bds2P8plVfcdDQZx3rjmmmua9byx55575vpWEiNm4vjWLUecN8pxLOOYxCM+f0MN9aXjMm3atNrzxumnn54GDx6cg8gt4bwRgZh4rKnjt7yyxfvHsdpwww1Tx44d18i+YE00isbvP85jGkWhWNTf6tGxkdcNfgUAAFWgQ4cOqWfPnvUe0QAXoyTqptnp06dPuuCCC9JXv/rVtO6666ZNN900XXXVVfXe65VXXklf+tKXUpcuXXLj4iGHHJJefvnlZfYZDV119xc93uuW48tf/nJu8KsrGv9iZMmaLE/0qo4G6Hg9Gthin41NG9O9e/dc1p122ikfp9hXNFiWxNwL0Qgbny0atU888cQ8UXXd8p933nlp6NChuZHz2GOPzcvPOOOMtM0226ROnTql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mtFjO+YJiIngS1xXp0Ydi+UdP4rJOa5x/O5pidxj0FSkn2p66m/1mNvIewxzagCsIhcsAAAAAFAeQlsAAAAAAEAhCGoAAAAAAACFIKgBAAAAAAAUgqAGAAAAAABQCIIaAAAAAABAIQhqAAAAAAAAhSCoAQAAAAAAFIKgBgAAAAAAUAiCGgAArJKrrroq9e7dO7Vu3TpNmDBhjbznyy+/nFq1apUef/zxVGn23nvvdPLJJ5e7GFBWzhurxnkDAGD52pa7AAAARdfv6s2adX9PHvPvVVr/qKOOStddd13+d9u2bdMGG2yQ+vXrlw4//PD8WjQyNtbcuXPTyJEj0/jx49MXvvCFtN5666WmcM8996RPfepT6e23305dunRZ6XolXbt2Tbvssku66KKLUt++fZukbLBGnLNeM+9vziqt7rwBAEBLZaQGAEAV2H///dPrr7+eezb/8Y9/zA16J510Uvrc5z6XPvjgg0a/z4wZM9L777+fDjzwwLTRRhulTp06pZZg+vTp+fPdddddaeHChbl8ixYtWu33+zDbNrWWXDYqi/NG5dTNllw2AIBVJagBAFAFOnTokHr27Jl69eqVdtppp/Ttb3873Xrrrbmh8tprr61d75133knHHHNM6tatW+rcuXP69Kc/nZ544on8WqxX6sW8xRZb5LQv0dj5wgsvpEMOOST16NEjrbvuumngwIHpL3/5S739x7q33HJLvWXRk7ruvkviPUu9qNdff/28bfQMX5Hu3bvnzxefLVK2vPLKK2natGm1rz/wwANpzz33TGuttVZOgXPiiSem+fPn177ep0+fdN5556WhQ4fmz33sscfm5WeccUbaZpttciNsfOazzz47N86WnHPOOal///7pF7/4RX6P6IH+5S9/Oc2bN2+5Zb399tvzetdff31+HmX90pe+lI9H9IaPYxnHoCQ+++DBg9P555+fNt544/SRj3xkhccCinbeWGeddfJICecN5w0AgMYQ1AAAqFLR8LjDDjuk3/72t7XLvvjFL6ZZs2blRstHH300N/Z95jOfSW+99VYaMmRIbaPj1KlTcw/naOh7991302c/+9k0efLk9Nhjj6VBgwalgw8+OPfOXh3xnr/5zW/q9aS+7LLLGrXtnDlz0o033pj/3b59+/w3Gk+jx3mkvXnyySfTpEmTcmNlpMOp69JLL83H4x//+Edu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ffGLX/xQ7+m8AQBQHcoe1Lj88svz0N64YN5tt93S1KlTV7h+DAH+yEc+ktZaa63Uu3fvdMopp6QFCxY0W3kBAIpon332ST179sy9rlckGhc/+tGPpg4dOuRrtO9973srfe/oaR3vvdFGG6Xdd98998COa7q5c+fWrnPrrbemnXbaKV/zbbHFFmns2LHpgw8+qPceV1xxRTr44IPT2muvnc4///y0ePHidPTRR6fNN988X/vFNeBll122TG/ywYMH55Ehsf8NN9wwnXDCCSts3Lz66qtTly5d0uTJk/Pzf/7zn+mAAw5I66yzTu4xfuSRR6Y333yzdv299947jRw5Mp188smpa9euab/99lvpMYFK0JznjajrzhsAALT4oMakSZPSqaeemsaMGZMee+yxtMMOO+SLvVmzZjW4/g033JCHPsf6zzzzTPrpT3+a3+Pb3/52s5cdAKBI2rRpky644IL0wx/+ML366qsNrvPoo4/mERZf/vKX01NPPZXOOeecdPbZZ+dUMY0V13HREBn7i0e4//7709ChQ9NJJ52Unn766fSTn/wkv2c0QNYV+4u0NrHvr371q2nJkiVpk002STfddFPebvTo0fm679e//nW97e6+++70wgsv5L+RLifee3llvvjii/P15J/+9Kf0mc98Jr3zzjvp05/+dNpxxx3T3//+93TnnXemN954Y5mRJvG+7du3Tw8++GC68sorG308oMia87zxu9/9rlDnjfjrvAEAUIVzaowfPz6NGDGiNp9qXOjdfvvt6ZprrqmXt7XkoYceSp/4xCfSV77ylfw8egEdfvjh6W9/+1uzlx0AoGii4a9///65g0h0Dmno2iwa6qJBMmyzzTa5UTDy3EfP5uWZM2dO7q0cOfbfe++9vOwb3/hG7jkdond1XNsNGzYsP48e1+edd146/fTTc1lK4hqvdF1YEtuWRM/rKVOm5MbJuo2H66+/fvrRj36UG0O33XbbdOCBB+be1HGdWdcZZ5yRfvGLX6R777039yoPsV00TEbDbUlci8aI4EijFccgbL311rlhE6pNc543TjzxxBZ/3ojy/vjHP3beAACoxqBG5BSNXj2jRo2qXda6des8xDkuOhsSw5J/+ctf5mHJu+66a3rxxRfTHXfckYf6AgCwcpEfP0YmnHbaacu8FiNhDznkkHrLokNJpP+MlC6lHtRL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9OmTOnbsmHbbbbc0derUFa7/zjvvpBNOOCFttNFGqUOHDmmbbbZJd9xxR7OVFwAAAAAAKI+2qYwmTZqUTj311HTllVfmgMaECRPSfvvtl6ZPn566d+++zPqLFi1K++67b37t5ptvTr169Ur//ve/U5cuXcpSfgAAAAAAoEqCGuPHj08jRoxIw4cPz88juHH77bena665Jp155pnLrB/L33rrrfTQQw+ldu3a5WUxygMAAAAAAKh8ZQtqxKiLRx99NI0aNap2WevWrdM+++yTpkyZ0uA2v//979PAgQNz+qlbb701devWLX3lK19JZ5xxRmrTpk0zlh4AAAAAVq7vdX1TNXlq2FPlLgJQ4coW1HjzzTfT4sWLU48ePeotj+fTpk1rcJsXX3wx/fWvf01HHHFEnkfj+eefT1//+tfT+++/n8aMGdPgNgsXLsyPkrlz5+a/S5YsyY9yaZ1qUjUpx7GOfdbU1JT1e6Yyqb/Ns0/1l6ag/jbPPtXf6uA7BgAAqi791OrcOMV8GldddVUemTFgwID02muvpUsuuWS5QY1x48alsWPHLrN89uzZacGCBalctlu/uhpVZs2aVZbfy5w5c3LDSowCgjVF/W166i9NRf1teupv9Zg3b165iwAAAFShsgU1unbtmgMTb7zxRr3l8bxnz54NbrPRRhvluTTqpprabrvt0syZM3M6q/bt2y+zTaS3isnI647U6N27d05d1blz51Quz7zdKlWThiZ+b45GlVatWuXvWqMKa5L62/TUX5qK+tv01N/q0bFjx3IXAQAAqEJlC2pEACJGWkyePDkNHjy49iY4no8cObLBbT7xiU+kG264Ia9Xukl+9tlnc7CjoYBG6NChQ34sLbYv5432klRdjSrlOtbRqFLu75rKo/42D/WXpqD+Ng/1tzr4fgEAgHIo651IjKCYOHFiuu6669IzzzyTvva1r6X58+en4cOH59eHDh1abyLxeP2tt95KJ510Ug5m3H777emCCy7IE4cDAAAAAACVraxzagwZMiTPbTF69OicQqp///7pzjvvrJ08fMaMGfV6gEXaqLvuuiudcsopqV+/fqlXr145wHHGGWeU8VMAAAAAAABVMVF4pJpaXrqpe+65Z5llAwcOTA8//HAzlAwAAAAAAGhJJMIFAAAAAAAKQVADAAAAAAAoBEENAAAAAACgEAQ1AAAAAACAQhDUAAAAAAAACkFQAwAAAAAAKARBDQAAAAAAoBAENQAAAAAAgEIQ1AAAAAAAAApBUAMAAAAAACgEQQ0AAAAAAKAQBDUAAAAAAIBCENQAAAAAAAAKQVADAAAAAAAoBEENAAAAAACgEAQ1AACAinL55ZenPn36pI4dO6bddtstTZ06dYXrv/POO+mEE05IG220UerQoUPaZptt0h133NFs5QUAABqv7SqsCwAA0KJNmjQpnXrqqenKK6/MAY0JEyak/fbbL02fPj117959mfUXLVqU9t133/zazTffnHr16pX+/e9/py5dupSl/AAAwIoJagAAABVj/PjxacSIEWn48OH5eQQ3br/99nTNNdekM888c5n1Y/lbb72VHnroodSuXbu8LEZ5AAAALZOgBgAAUBFi1MWjjz6aRo0aVbusdevWaZ999klTpkxpcJvf//73aeDAgTn91K233pq6deuWvvKVr6QzzjgjtWnTpsFtFi5cmB8lc+fOzX+XLFmSH+XSOtWkalKOYx37rKmpKev3TGVSfyu7/rausuzv1XaOVH+bZ5/qb3XU3yWN3L+gBgAAUBHefPPNtHjx4tSjR496y+P5tGnTGtzmxRdfTH/961/TEUcckefReP7559PXv/719P7776cxY8Y0uM24cePS2LFjl1k+e/bstGDBglQu261fXY0qs2bNKsuN9pw5c3LDSgTMYE1Rfyu7/m7ddutUTcrx/ZaT+tv01N/qqb/z5s1r1HqCGgAAVIS+1/VN1eSpYU+VuwgVIW6SYz6Nq666Ko/MGDBgQHrttdfSJZdcstygRowEiXk76o7U6N27dx7l0blz51Quz7zdKlWThuZIaY7fS6tWrfJ3LajBmqT+Vnb9fe6D51I1Kcf3W07qb9NTf6un/nbs2LFR6wlqAAAAFaFr1645MPHGG2/UWx7Pe/bs2eA2G220UZ5Lo26qqe222y7NnDkzp7Nq3779Mtt06NAhP5YWN9nlbOhekqqrUaVcxzoaVcr9XVN51N/Krr9LUpWlY6qy86P62zzU3+qov60buf/qOssAAAAVKwIQMdJi8uTJ9Xr2xfOYN6Mhn/jEJ3LKqbr5e5999tkc7GgooAEAAJSXoAYAAFAxIi3UxIkT03XXXZeeeeaZ9LWvfS3Nnz8/DR8+PL8+dOjQehOJx+tvvfVWOumkk3Iw4/bbb08XXHBBnjgcAABoeaSfAgAAKsaQIUPyhN2jR4/OKaT69++f7rzzztrJw2fMmFFvWHvMhXHXXXelU045JfXr1y/16tUrBzjOOOOMMn4KAABgeQQ1AACAijJy5Mj8aMg999yzzLJITfXwww83Q8kAAIAPS/opAAAAAACgEAQ1AAAAAACAQhDUAAAAAAAACkFQAwAAAAAAKARBDQAAAAAAoBDalrsAANCS9L2ub6omTw17qtxFAAAAAGg0IzUAAAAAAIBCENQAAAAAAAAKQVADAAAAAAAoBEENAAAAAACg8oMaixYtStOnT08ffPDBmisRAABQldxfAAAATRLUeO+999LRRx+dOnXqlD760Y+mGTNm5OXf+MY30oUXXrg6bwkAAFQp9xcAAECTBjVGjRqVnnjiiXTPPfekjh071i7fZ5990qRJk1bnLQEAgCrl/gIAAGistmk13HLLLfnm4uMf/3hq1apV7fLoVfXCCy+szlsCAABVyv0FAADQpCM1Zs+enbp3777M8vnz59e7CQEAAFgZ9xcAAECTBjV23nnndPvtt9c+L91oXH311WngwIGr85YAAECVcn8BAAA0afqpCy64IB1wwAHp6aefTh988EG67LLL8r8feuihdO+9967OWwIAAFXK/QUAANCkIzX22GOPPJFf3HD07ds3/elPf8rDxadMmZIGDBiwOm8JAABUKfcXAABAk43UeP/999Nxxx2Xzj777DRx4sRV3RwAAKCW+wsAAKBJR2q0a9cu/eY3v1nVzQAAAJbh/gIAAGjy9FODBw9Ot9xyy+psCgAAUI/7CwAAoEknCt96663Tueeemx588MGc43bttdeu9/qJJ564Om8LAABUIfcXAABAkwY1fvrTn6YuXbqkRx99ND/qatWqlZsOAACg0dxfAAAATRrUeOmll1ZnMwAAgGW4vwAAAJp0To26ampq8gMAAODDcn8BAAA0SVDj5z//eerbt29aa6218qNfv37pF7/4xeq+HQAAUMXcXwAAAE2Wfmr8+PHp7LPPTiNHjkyf+MQn8rIHHnggHX/88enNN99Mp5xyyuq8LQAAUIXcXwAAAE0a1PjhD3+YrrjiijR06NDaZQcffHD66Ec/ms455xw3HQAAQKO5vwAAAJo0/dTrr7+edt9992WWx7J4DQAAoLHcXwAAAE0a1Nhqq63Sr3/962WWT5o0KW299dar85YAAECVcn8BAAA0afqpsWPHpiFDhqT77ruvNuftgw8+mCZPntzgzQgAAMDyuL8AAACadKTGF77whfS3v/0tde3aNd1yyy35Ef+eOnVqOvTQQ1fnLQEAgCrl/gIAAGjSkRphwIAB6Ze//OXqbg4AAFDL/QUAANBkIzXuuOOOdNdddy2zPJb98Y9/XJ23BAAAqpT7CwAAoEmDGmeeeWZavHjxMstramryawAAAI3l/gIAAGjSoMZzzz2Xtt9++2WWb7vttun5559fnbcEAACqlPsLAACgSYMa6623XnrxxReXWR43HGuvvfbqvCUAAFCl3F8AAABNGtQ45JBD0sknn5xeeOGFejcc3/zmN9PBBx+8Om8JAABUKfcXAABAkwY1Lr744txjKoaDb7755vkR/95www3TpZdeujpvCQAAVCn3FwAAQGO1Tas5PPyhhx5Kf/7zn9MTTzyR1lprrbTDDjukPffcc3XeDgAAqGLuLwAAgCYZqTFlypR022235X+3atUqDRo0KHXv3j33nvrCF76Qjj322LRw4cJVeUsAAKBKub8AAACaNKhx7rnnpn/961+1z5966qk0YsSItO+++6Yzzzwz/eEPf0jjxo1b5UIAAADVx/0FAADQpEGNxx9/PH3mM5+pfX7jjTemXXfdNU2cODGdeuqp6Qc/+EH69a9/vcqFAAAAqo/7CwAAoEmDGm+//Xbq0aNH7fN77703HXDAAbXPd9lll/TKK6+sciEAAIDq4/4CAABo0qBG3HC89NJL+d+LFi1Kjz32WPr4xz9e+/q8efNSu3btVrkQAABA9XF/AQAANGlQ47Of/WzObXv//fenUaNGpU6dOqU999yz9vUnn3wybbnllqtcCAAAoPq4vwAAAFZV21VZ+bzzzkuf//zn01577ZXWWWeddN1116X27dvXvn7NNdekQYMGrXIhAACA6uP+AgAAaNKgRteuXdN9992X5syZk2862rRpU+/1m266KS8HAABYGfcXAABAkwY1StZbb70Gl2+wwQar83YAAEAVc38BAAA0yZwaAAAAAAAA5SKoAQAAAAAAFIKgBgAAAAAAUAiCGgAAAAAAQCEIagAAAAAAAIXQIoIal19+eerTp0/q2LFj2m233dLUqVMbtd2NN96YWrVqlQYPHtzkZQQAAAAAAKo8qDFp0qR06qmnpjFjxqTHHnss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TwMGDEiTJ0+u18spng8cOLDBbWJ53fXDn//85+WuDwAAVAf3FwAAUPnKnn4qhm0PGzYs7bzzzmnXXXdNEyZMSPPnz0/Dhw/Prw8dOjT16tUr560NJ510Utprr73S9773vXTggQemG2+8Mf39739PV111VZk/CQAAUG7uLwAAoLKVPagxZMiQNHv27DR69Og8GV///v3TnXfeWTtZ34wZM1Lr1v//gJLdd9893XDDDemss85K3/72t9PWW2+dbrnllvSxj32sjJ+ClihSAowZM2aZ1ABAy6f+QnGpv5Sb+wuaknMcFJf6C8Wl/rK0VjUx+wYAAAAAAEALV9Y5NQAAAAAAABpLUAMAAAAAACgEQQ0AAAAAAKAQBDUAAACgkc4555w8AX3JUUcdlQYPHlzWMgGNo/5Ccam/1CWoQVX573//m0+Cu+yyS+rWrVvadNNN04EHHphuvPHGVFNTs8z6559/ftp9991Tp06dUpcuXcpSZmD16u/BBx+c1+nYsWPaaKON0pFHHpn+85//lKXsUO1Wtf726dMntWrVqt7jwgsvLEvZAVbX+++/n6666qq0zz77pF69eqWePXvme4tLL700vffee8us/9vf/jYNGjQobbjhhvm89/jjj5el3MCq19+4ztl2223T2muvndZff/283d/+9reylB2q3arW3wiOLH3vsf/++5el7DSeoAYVIRpEPvjgg2WWL1q0qPbff/rTn9I222yTHnnkkXTaaafl53Hj8LnPfS6dd955ab/99kvz589fZvsvfvGL6Wtf+1qzfA6oRk1Vfz/1qU+lX//612n69OnpN7/5TXrhhRfSYYcd1iyfCapFU9XfcO6556bXX3+99vGNb3yjyT8PUHx1zz/l9OKLL6addtopXX755fn646abbsrnv5NPPjlNnjw5ffSjH03PPvtsvW3iXLjHHnukiy66qGzlhnIqcv2Na50f/ehH6amnnkoPPPBA7qARQcrZs2eX7XNAcypy/Q0RxKh77/GrX/2qLOVnFdRAC7DXXnvVjBw5suakk06q6dKlS0337t1rrrrqqpp333235qijjqpZZ511arbccsuaO+64I69/9913R7fO/HynnXaqadeuXV4W73PCCSfk99lwww1r9t5777z+I488UrPBBhvU/P73v29w/++//37N8OHDaw466KAGX//Zz35Ws9566zXhEYDiaun1t+TWW2+tadWqVc2iRYua4ChAMbXU+rvZZpvVfP/732+GIwAUXUPnn6eeeqpm//33r1l77bXzee3//u//ambPnl27zeLFi2suuuiifH5r3759Te/evWu++93v1r5++umn12y99dY1a621Vs3mm29ec9ZZZ9W7fhgzZkzNDjvsUPt82LBhNYccckjt83feeadmq622qjn77LNrlixZ0mC541wb57q33nprmddeeumlfK79xz/+sUaOEbRUlVh/S+bMmZPr8V/+8pcPdYygpaqk+rv0+1AMRmrQYlx33XWpa9euaerUqbk3ZoyOiFESMUTssccey70cIn1M3aFiZ555Zk5H8cwzz6R+/frVvk/79u3Tgw8+mK688sq8LN4vUkkddNBB6emnn0577bVXTn/xpS99KZ166qnp4osvzuvGa3fffXfZjgEUVUuvv2+99Va6/vrrc3natWvXTEcFiqGl1t94/0jBsuOOO6ZLLrmkwREhAEuff+Lc8elPfzqfO/7+97+nO++8M73xxhv5vFMyatSovN7ZZ5+dzz833HBD6tGjR+3r6667brr22mvza5dddlmaOHFi+v73v9/o8sR7DxgwII84mzNnTjriiCNqU1/84Ac/SAcccEAaMWJE2nPPPdOECRPW+PGAIqnE+hs91iP1zXrrrZd22GGHD3mEoOWqpPp7zz33pO7du6ePfOQj+X4o0ufSwpU7qgKlCO8ee+xR+/yDDz7Ikd0jjzyydtnrr7+eezpMmTKltqfoLbfcssz77LjjjvWWPfvsszU9e/bMvUHjfbfZZpuaY489Nvd8+sEPflDTtm3bHO0NEQU+44wzlimfkRpQzPobPT06deqU9/fxj3+85s0332yiowDF1FLr7/e+9728ryeeeKLmiiuuyKNITjnllCY8EkBRLX3+Oe+882oGDRpUb51XXnkln7umT59eM3fu3JoOHTrUTJw4sdH7uOSSS2oGDBjQ6J6ivXr1yr1Vw1e/+tWagQMH1jz88MN51FqcF6PMIXpw77bbbsvsz0gNqkWl1d8//OEP+ToqRodvvPHGNVOnTl2l4wFFUkn191e/+lXO7PDkk0/W/O53v6vZbrvtanbZZZd8D0PL1bbcQRUoKfX0DG3atMm9M/v27Vu7rBS9nTVrVurcuXP+984777zM+0RUtq7IaRkTk7Zt2zZHe1977bWc6zJ6a/fv3z/9/ve/r103JhN+4oknmuTzQSVrqfX3W9/6Vjr66KPTv//97zR27Ng0dOjQdNttt+WJv4CWW39jFEfd8kUPsOOOOy6NGzcudejQYY19dqAy1D3/xLkkRn6ts846y6wX82u98847aeHChekzn/nMct9v0qRJuUdnrP/uu+/mkWKl89/KxOjQefPmpY997GP5+R/+8Id0yy23pN122y0/HzlyZPrzn/9ce+57++23V/nzQiWppPobc/o9/vjj6c0338w9zKOHekwWHr2/oRJVSv398pe/XPvvuA+K+48tt9wyj95YUXkpL+mnaDGWTgkTjY51l5UaIZcsWVK7bO21117mfZZeFifBtdZaq3YYaLxn3fete8KNNBtbbbXVGvk8UE1aav2NlDoxad++++6bbrzxxnTHHXekhx9++EN8Uqg8LbX+1hU3I/F+L7/88ip+OqAa1D3/RCNIpLyLhsW6j+eeey598pOfrD0vLc+UKVNyuorPfvazuSPEP/7xj/Sd73yn0ROgxrmqY8eOtc9ju7rlc+8BlVt/Y91Y9vGPfzz99Kc/zR074i9Uqkqqv3VtscUWuS3h+eefb9S+KQ9BDSpenKiit2iI3HjRoBI9RRcvXpwbN++66670/vvv51yAf/zjH9NRRx1V7iIDTVB/Sw2y0TsEKFb9jRui1q1b6+kIrNROO+2U/vWvf6U+ffrk81DdRzRubL311rlhZfLkyQ1u/9BDD6XNNtssN6TEqLRYP0Z8NlY0gkRDSuQRD3vssUeeP+h///tfHrEWvbdL+4l91B2ZBtWu0upv3H+496BaVFL9ffXVV/OcGjGig5ZLUIOKF5MUxUkshsHFCTQmHRo9enROXzF8+PA0ePDgdNFFF6Wf/exn6U9/+lOewLRkxowZuSEl/kYjTCnSHBFooOXW3xjmHY2nUV/jQuivf/1rOvzww/MQ0oEDB5b7Y0FVWN36G720YuK+GML+4osvpuuvvz6dcsop6f/+7//S+uuvX+6PBbRwJ5xwQk5BEf/ff+SRR3IKiwiixnknruejF+cZZ5yRTj/99PTzn/88vx6B1lJv6mhEiWv/GOEZr0UajN/97neN3n8EYA8++OD04x//OD+PiU6jt2n0EI2UFjF69N57701f/epX82t101pEuePaJVL2henTp+fnM2fOXOPHCVqiotbf+fPnp29/+9u5LHHv8eijj+Z1oiH1i1/8YhMdLWhZilp/o30v0lZHWWJUeARdDjnkkByM2W+//ZroaLEmmFODihdpM6LRZNiwYem+++7LQ9lmz56dbw423njjNGfOnHTVVVc1mPcvGl+iB2ndBpoQDTR77713s34OqEarW387deqUfvvb36YxY8bkm4zoYbH//vuns846Sz5+aOH1N+po3Mycc845uXfj5ptvnoMaejMDjRHnlwcffDA3nAwaNCifR6LnZ1wHRINHOPvss3NamLjW/89//pOvE44//vj8WjSIxDkncm/HtgceeGBeP85JjRXvu+uuu+YUNAcccEAOUsS5LwKz0XM7eohGj9KlxVxD0fizdI7vuJ5Zlf1DURW1/sacZNOmTcttBzGfRsxPFvOK3X///emjH/3oGj5K0DIVuf4++eSTuf7GvB/xOaL85513nraDFq5VzBZe7kJAc7jgggvS+PHj06hRo9KQIUPSJptskk+UEamNk1U0lhx66KHlLibQAPUXikv9BapRjECLoESMMhsxYkRtw2ak5bv00kvz6LQ4NwItj/oLxaX+Vg9BDapK9JT47ne/m+65554UP/2YSGj77bdPJ554YjrmmGNqo8dAy6P+QnGpv0A1eumll9K5556b02eU0tfG3EAxgi0CvZ07dy53EYHlUH+huNTf6iCoQVWKHqKzZs1K6667burSpUu5iwOsAvUXikv9BapRpLyIiUsjgNujR49yFwdYBeovFJf6W9kENQAAAAAAgEIw1h8AAAAAACgEQQ0AAAAAAKAQBDUAAAAAAIBCENQAAAAAAAAKQVADAAAAAAAoBEENAAAAAACgEAQ1AAAAAACAQhDUAAAAAAAACkFQAwAAAAAASEXw/wHtJEt1kjFN9QAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "# Create figure with two subplots side by side\n",
+ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 5))\n",
+ "\n",
+ "# Plot MRR scores\n",
+ "mrr_cols = [\"mrr@1\", \"mrr@3\", \"mrr@5\"]\n",
+ "x = np.arange(len(mrr_cols))\n",
+ "width = 0.25\n",
+ "\n",
+ "for i, model in enumerate(df.index):\n",
+ " offset = (i - 1) * width\n",
+ " ax1.bar(x + offset, df.loc[model, mrr_cols], width, label=model)\n",
+ "\n",
+ "ax1.set_title(\"Mean Reciprocal Rank (MRR)\")\n",
+ "ax1.set_xticks(x)\n",
+ "ax1.set_xticklabels(mrr_cols)\n",
+ "ax1.set_ylabel(\"Score\")\n",
+ "ax1.legend()\n",
+ "ax1.grid(True, alpha=0.3)\n",
+ "\n",
+ "# Plot Recall scores\n",
+ "recall_cols = [\"recall@1\", \"recall@3\", \"recall@5\"]\n",
+ "x = np.arange(len(recall_cols))\n",
+ "\n",
+ "for i, model in enumerate(df.index):\n",
+ " offset = (i - 1) * width\n",
+ " ax2.bar(x + offset, df.loc[model, recall_cols], width, label=model)\n",
+ "\n",
+ "ax2.set_title(\"Recall\")\n",
+ "ax2.set_xticks(x)\n",
+ "ax2.set_xticklabels(recall_cols)\n",
+ "ax2.set_ylabel(\"Score\")\n",
+ "ax2.legend()\n",
+ "ax2.grid(True, alpha=0.3)\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Conclusion\n",
+ "\n",
+ "In this notebook, we demonstrated how fine-tuning a Cohere re-ranker can significantly improve retrieval performance. With just 256 synthetic examples, our fine-tuned model showed a 60% increase in recall and a 49% increase in mean reciprocal rank (MRR) compared to the text-embedding-3-small model.\n",
+ "\n",
+ "This substantial improvement highlights two key considerations for future fine-tuning efforts:\n",
+ "\n",
+ "1. **Model Selection**: Explore various re-ranker options, including English and multi-lingual models from providers like Cohere and Jina. Experiment to find the best fit for your specific use case.\n",
+ "\n",
+ "2. **Dataset Quality**: Move beyond simple random negative selection. Consider more sophisticated approaches like using cosine similarity or leveraging language models to identify hard negatives.\n",
+ "\n",
+ "The fast, objective metrics we established in Week 1 were crucial in our analysis. They allowed us to quickly benchmark our fine-tuned model against both the default Cohere re-ranker and the text-embedding-3-small model. Surprisingly, we discovered that the default Cohere re-ranker actually degraded performance while increasing latency.\n",
+ "\n",
+ "In Week 4, we'll use similar approaches to discover query patterns with BERTopic, and in Week 5, we'll apply these techniques to structured data and metadata. These are applications where a fine-tuned re-ranker can provide a significant improvement for, especially when at the start when we have a limited amount of user data.As you accumulate more user data, that's when you might want to start looking at fine-tuning an open-source model using the Sentence Transformers library which introduces new challenges such as managing hyper-parameters, loss functions and training loops which we'll explore in the next notebook."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/cohort_2/week2/3. Open Source Models_logfire.ipynb b/cohort_2/week2/3. Open Source Models_logfire.ipynb
new file mode 100644
index 0000000..7da14f8
--- /dev/null
+++ b/cohort_2/week2/3. Open Source Models_logfire.ipynb
@@ -0,0 +1,873 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The autoreload extension is already loaded. To reload it, use:\n",
+ " %reload_ext autoreload\n"
+ ]
+ }
+ ],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you're running the notebook in colab, make sure that you run the following cell. \n",
+ "\n",
+ "**There is no need to do so if you're cloning the repository locally**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " % Total % Received % Xferd Average Speed Time Time Time Current\n",
+ " Dload Upload Total Spent Left Speed\n",
+ "100 12543 100 12543 0 0 406k 0 --:--:-- --:--:-- --:--:-- 408k\n",
+ " % Total % Received % Xferd Average Speed Time Time Time Current\n",
+ " Dload Upload Total Spent Left Speed\n",
+ "100 97327 100 97327 0 0 1214k 0 --:--:-- --:--:-- --:--:-- 1218k\n",
+ " % Total % Received % Xferd Average Speed Time Time Time Current\n",
+ " Dload Upload Total Spent Left Speed\n",
+ "100 18441 100 18441 0 0 590k 0 --:--:-- --:--:-- --:--:-- 600k\n",
+ " % Total % Received % Xferd Average Speed Time Time Time Current\n",
+ " Dload Upload Total Spent Left Speed\n",
+ "100 68646 100 68646 0 0 107k 0 --:--:-- --:--:-- --:--:-- 107k\n",
+ " % Total % Received % Xferd Average Speed Time Time Time Current\n",
+ " Dload Upload Total Spent Left Speed\n",
+ "100 1450 100 1450 0 0 43896 0 --:--:-- --:--:-- --:--:-- 45312\n"
+ ]
+ }
+ ],
+ "source": [
+ "!mkdir -p data\n",
+ "!curl -L -o ./data/categories.json https://raw.githubusercontent.com/567-labs/systematically-improving-rag/main/cohort_2/week2/data/categories.json\n",
+ "!curl -L -o ./data/cleaned.jsonl https://raw.githubusercontent.com/567-labs/systematically-improving-rag/main/cohort_2/week2/data/cleaned.jsonl\n",
+ "!curl -L -o ./data/eval_transactions.jsonl https://raw.githubusercontent.com/567-labs/systematically-improving-rag/main/cohort_2/week2/data/eval_transactions.jsonl\n",
+ "!curl -L -o ./data/train_transactions.jsonl https://raw.githubusercontent.com/567-labs/systematically-improving-rag/main/cohort_2/week2/data/train_transactions.jsonl\n",
+ "!curl -L -o ./helpers.py https://raw.githubusercontent.com/567-labs/systematically-improving-rag/main/cohort_2/week2/helpers.py"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "ModuleNotFoundError",
+ "evalue": "No module named 'google.colab'",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[6], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mgoogle\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcolab\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m userdata\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mos\u001b[39;00m\n\u001b[1;32m 4\u001b[0m os\u001b[38;5;241m.\u001b[39menviron[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mHF_TOKEN\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m userdata\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mHF_TOKEN\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
+ "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'google.colab'"
+ ]
+ }
+ ],
+ "source": [
+ "from google.colab import userdata\n",
+ "import os\n",
+ "\n",
+ "os.environ[\"HF_TOKEN\"] = userdata.get(\"HF_TOKEN\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!pip install sentence-transformers===3.1.1 transformers==4.45.2 pydantic-evals lancedb datasets\n",
+ "!pip uninstall wandb -y"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Week 2 : Fine-tuning Open Source Embedding Models\n",
+ "\n",
+ "> **Prerequisites** : Before running this notebook, make sure that you've completed the previous two notebooks in this week - [1. Synthetic Transactions.ipynb](1. Synthetic Transactions.ipynb) and [2. Finetune Cohere.ipynb](2. Finetune Cohere.ipynb). This notebook will build on top of the previous two notebooks.\n",
+ "> \n",
+ "> You must have a hugging face token with write access set as an environment variable. If you don't have one, you can create one by following the instructions [here](https://huggingface.co/docs/hub/en/security-tokens).\n",
+ "\n",
+ "[](https://colab.research.google.com/github/567-labs/systematically-improving-rag/blob/main/cohort_2/week2/3.%20Open%20Source%20Models.ipynb)\n",
+ "\n",
+ "\n",
+ "After exploring managed services, let's dive into fine-tuning open source embedding models. While this approach requires more setup, it offers greater control and potential cost savings.\n",
+ "\n",
+ "## Why This Matters\n",
+ "\n",
+ "Fine-tuning open source models gives you full control over how your model learns and behaves. While we'll use a small synthetic dataset of 256 examples for this tutorial, real-world applications benefit greatly from larger private datasets - the larger the better. This is because larger datasets allow you to capture specific domain knowledge and relationships that general models might miss.\n",
+ "\n",
+ "The main advantage of open source fine-tuning is the cost savings at inference time. Once trained, you can run these models on your own infrastructure without paying per-query fees. This makes them especially attractive for high-volume applications. We'll use sentence-transformers since it offers robust training options, works seamlessly with popular model hubs, and has strong community support.\n",
+ "\n",
+ "## What You'll Learn\n",
+ "\n",
+ "Though this hands on tutorial, you learn how to\n",
+ "\n",
+ "This notebook walks through:\n",
+ "\n",
+ "1. **Dataset Preparation**\n",
+ " - Creating train/test/eval splits\n",
+ " - Formatting data for triplet loss\n",
+ " - Setting up evaluation metrics\n",
+ "\n",
+ "2. **Model Fine-tuning**\n",
+ " - Configuring training arguments\n",
+ " - Setting up loss functions\n",
+ " - Training and monitoring progress\n",
+ " \n",
+ "3. **Performance Evaluation**\n",
+ " - Comparing against base model\n",
+ " - Measuring recall and MRR improvements\n",
+ " - Analyzing trade-offs\n",
+ "\n",
+ "\n",
+ "By the end of this notebook, you'll have a better understanding of when you might want to consider fine-tuning an open source embedding model, how you can do so using the `sentence-transformers` library and how to evaluate the performance of your fine-tuned model.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "In this section, we'll be fine-tuning the `BAAI/bge-base-en` model. We'll be using the `BatchSemiHardTripletLoss` loss function to train our model. Let's first understand how this loss function works before we dive into the code\n",
+ "\n",
+ "### Understanding Semi-Hard Triplet Loss\n",
+ "\n",
+ "\n",
+ "A Batch Semi Hard Triplet Loss works by taking a batch of sentence pairs and computing the loss for all possible valid triplets, then identifying semi-hard positives and negatives. A semi-hard negative is an example that is not as close to the anchor as the positive example, but is still close to the anchor than the negative example. \n",
+ "It works with three pieces:\n",
+ "\n",
+ "1. An anchor (your main example) - in our case, a transaction description\n",
+ "2. A positive match (something similar) - the correct category\n",
+ "3. A negative match (something different) - incorrect categories that are close, but not quite right\n",
+ "\n",
+ "We can see an example of this in the image below where we have an achor, a positive match and a negative match. The negative match is not as close to the anchor as the positive match, but is still close to the anchor than the negative match.\n",
+ "\n",
+ " \n",
+ "\n",
+ "For our transaction data, this translates to:\n",
+ "- Anchor: Transaction description\n",
+ "- Positive: Correct category\n",
+ "- Negative: Similar but incorrect categories\n",
+ "\n",
+ "It's important here to note that the reason why we want an example that's **semi-hard** is because we want to find negative examples that are tricky. They need to be different as compared to the anchor, but not too different. This ultimately helps the model learn to distinguish between similar and dissimilar examples.\n",
+ "\n",
+ "\n",
+ " \n",
+ "\n",
+ "Using our Cohere dataset format:\n",
+ "\n",
+ "1. The transaction description becomes our anchor (query)\n",
+ "2. The correct category is our positive match (relevant_passages)\n",
+ "3. Similar but wrong categories are our negative matches (hard_negatives)\n",
+ "\n",
+ "While a single training run can provide a baseline, exploring hyperparameter optimization—through techniques like grid or random search—can significantly enhance model performance, especially when executed on a larger scale with appropriate computational resources. Here's a quick example of [how we can do this using `modal` to run a grid search over all possible parameters](https://modal.com/blog/fine-tuning-embeddings)\n",
+ "\n",
+ "\n",
+ "### Declaring Constants\n",
+ "\n",
+ "When writing fine-tuning code, we want to declare our constants up front. This ensures that we have a consistent set of parameters to use when training our model and makes it easy for us to change them later."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# To resolve the warning from huggingface/tokenizers about parallelism:\n",
+ "# 1. Avoid using `tokenizers` before the fork if possible.\n",
+ "# 2. Explicitly set the environment variable TOKENIZERS_PARALLELISM to either 'true' or 'false'.\n",
+ "import os\n",
+ "\n",
+ "# Set the environment variable to disable parallelism warnings\n",
+ "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n",
+ "\n",
+ "TEST_SIZE = 0.2\n",
+ "BASE_MODEL_NAME = \"BAAI/bge-base-en\"\n",
+ "FINETUNED_MODEL_NAME = \"ivanleomk/finetuned-bge-base-en\"\n",
+ "\n",
+ "MODEL_OUTPUT_DIR = \"./models/bge-base-en\"\n",
+ "CATEGORIES_PATH = \"data/categories.json\"\n",
+ "TRAIN_EVALUATOR_NAME = \"bge-base-en-train\"\n",
+ "EVAL_EVALUATOR_NAME = \"bge-base-en-eval\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Preparing the Dataset\n",
+ "\n",
+ "While we use the same dataset as before, we need to handle a lot more configuration when fine-tuning open source models in order for us to evaluate our model's performance while training as well as to use the `BatchSemiHardTripletLoss` loss function.\n",
+ "\n",
+ "To do so, we'll format our original train and eval split into a train, test and eval split. \n",
+ "\n",
+ "- Train : This is only used to train the model\n",
+ "- Test : This is used to evaluate the model during training\n",
+ "- Eval : This is used to evaluate the model after training\n",
+ "\n",
+ "We want to have a separate set of data points set aside for testing our model during training or to use when evaluating different versions of our model. This ensures that our model does not overfit to the evaluation dataset."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/ivanleo/Documents/coding/systematically-improving-rag/cohort_2/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020\n",
+ " warnings.warn(\n",
+ "/Users/ivanleo/Documents/coding/systematically-improving-rag/cohort_2/.venv/lib/python3.9/site-packages/pydantic_evals/dataset.py:390: UserWarning: Could not determine the generic parameters for ; using `Any` for each. You should explicitly set the generic parameters via `Dataset[MyInputs, MyOutput, MyMetadata]` when serializing or deserializing.\n",
+ " warnings.warn(\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "(52, 208, 66)"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import json\n",
+ "from pydantic_evals import Dataset as EvalsDataset\n",
+ "\n",
+ "categories = json.load(open(CATEGORIES_PATH))\n",
+ "train_data_path = \"./data/train_transactions.yml\"\n",
+ "eval_data_path = \"./data/eval_transactions.yml\"\n",
+ "\n",
+ "train_data = EvalsDataset.from_file(train_data_path)\n",
+ "eval_data = EvalsDataset.from_file(eval_data_path)\n",
+ "\n",
+ "test_data = EvalsDataset(\n",
+ " cases=train_data.cases[: int(len(train_data.cases) * TEST_SIZE)]\n",
+ ")\n",
+ "train_data = EvalsDataset(\n",
+ " cases=train_data.cases[int(len(train_data.cases) * TEST_SIZE) :]\n",
+ ")\n",
+ "len(test_data.cases), len(train_data.cases), len(eval_data.cases)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from collections import defaultdict\n",
+ "import random\n",
+ "from datasets import Dataset\n",
+ "\n",
+ "\n",
+ "def create_labels(data: EvalsDataset):\n",
+ " label_to_example = defaultdict(list)\n",
+ "\n",
+ " for item in data.cases:\n",
+ " label_to_example[item.expected_output[0]].append(item)\n",
+ "\n",
+ " return {label: idx for idx, label in enumerate(label_to_example.keys())}\n",
+ "\n",
+ "\n",
+ "def create_sentence_to_label_dataset(data: EvalsDataset, label_to_idx):\n",
+ " return Dataset.from_dict(\n",
+ " {\n",
+ " \"sentence\": [item.inputs for item in data.cases],\n",
+ " \"label\": [label_to_idx[item.expected_output[0]] for item in data.cases],\n",
+ " }\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def create_triplet_dataset(data: EvalsDataset):\n",
+ " label_to_example = defaultdict(list)\n",
+ "\n",
+ " for item in data.cases:\n",
+ " label_to_example[item.expected_output[0]].append(item)\n",
+ "\n",
+ " labels = set(label_to_example.keys())\n",
+ "\n",
+ " anchors = []\n",
+ " positives = []\n",
+ " negatives = []\n",
+ "\n",
+ " for item in data.cases:\n",
+ " label = item.expected_output[0]\n",
+ " anchor = item.inputs\n",
+ " positive = label\n",
+ " negative = random.choice([item for item in labels if item != label])\n",
+ " anchors.append(anchor)\n",
+ " positives.append(positive)\n",
+ " negatives.append(negative)\n",
+ "\n",
+ " return {\"anchor\": anchors, \"positive\": positives, \"negative\": negatives}\n",
+ "\n",
+ "\n",
+ "labels_to_idx = create_labels(train_data)\n",
+ "\n",
+ "train_triplets = create_triplet_dataset(train_data)\n",
+ "test_triplets = create_triplet_dataset(test_data)\n",
+ "eval_triplets = create_triplet_dataset(eval_data)\n",
+ "\n",
+ "sentence_to_label_train_dataset = create_sentence_to_label_dataset(\n",
+ " train_data, labels_to_idx\n",
+ ")\n",
+ "sentence_to_label_test_dataset = create_sentence_to_label_dataset(\n",
+ " test_data, labels_to_idx\n",
+ ")\n",
+ "sentence_to_label_eval_dataset = create_sentence_to_label_dataset(\n",
+ " eval_data, labels_to_idx\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Fine-Tuning\n",
+ "\n",
+ "Now that we have our training data formatted in the right format, we can start training our model. We'll do so in 3 steps\n",
+ "\n",
+ "1. First we'll declare training arguments - we're using the default arguments provided in their documentation but ideally you'd want to experiment and tinkker with different configurations\n",
+ "\n",
+ "2. Next we'll start a training run with `trainer.train()`\n",
+ "\n",
+ "3. Finally, we'll train our model before uploading it to the Hugging Face model hub."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'bge-base-en-train_cosine_accuracy': 0.8269230769230769,\n",
+ " 'bge-base-en-train_dot_accuracy': 0.17307692307692307,\n",
+ " 'bge-base-en-train_manhattan_accuracy': 0.8269230769230769,\n",
+ " 'bge-base-en-train_euclidean_accuracy': 0.8269230769230769,\n",
+ " 'bge-base-en-train_max_accuracy': 0.8269230769230769}"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from sentence_transformers import (\n",
+ " SentenceTransformer,\n",
+ " SentenceTransformerTrainer,\n",
+ " SentenceTransformerTrainingArguments,\n",
+ ")\n",
+ "from sentence_transformers.losses import BatchSemiHardTripletLoss\n",
+ "from sentence_transformers.training_args import BatchSamplers\n",
+ "from sentence_transformers.evaluation import TripletEvaluator\n",
+ "\n",
+ "model = SentenceTransformer(BASE_MODEL_NAME)\n",
+ "loss = BatchSemiHardTripletLoss(model)\n",
+ "args = SentenceTransformerTrainingArguments(\n",
+ " # Required parameter:\n",
+ " output_dir=MODEL_OUTPUT_DIR,\n",
+ " num_train_epochs=5,\n",
+ " per_device_train_batch_size=16,\n",
+ " per_device_eval_batch_size=16,\n",
+ " learning_rate=2e-5,\n",
+ " warmup_ratio=0.1,\n",
+ " fp16=False, # Set to False if you get an error that your GPU can't run on FP16\n",
+ " bf16=False, # Set to True if you have a GPU that supports BF16\n",
+ " batch_sampler=BatchSamplers.NO_DUPLICATES, # MultipleNegativesRankingLoss benefits from no duplicate samples in a batch\n",
+ " eval_strategy=\"steps\",\n",
+ " eval_steps=100,\n",
+ " save_strategy=\"steps\",\n",
+ " save_steps=100,\n",
+ " save_total_limit=2,\n",
+ " logging_steps=100,\n",
+ ")\n",
+ "\n",
+ "train_evaluator = TripletEvaluator(\n",
+ " anchors=train_triplets[\"anchor\"],\n",
+ " positives=train_triplets[\"positive\"],\n",
+ " negatives=train_triplets[\"negative\"],\n",
+ " name=TRAIN_EVALUATOR_NAME,\n",
+ ")\n",
+ "\n",
+ "train_evaluator(model)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "fe44fd0d72f74d9f99ac65c31e9cc1dd",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ " 0%| | 0/65 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b3fb40ef22404e11b2d56f9ed600030c",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Computing widget examples: 0%| | 0/1 [00:00, ?example/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'train_runtime': 10.2381, 'train_samples_per_second': 101.581, 'train_steps_per_second': 6.349, 'train_loss': 4.90222402719351, 'epoch': 5.0}\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "TrainOutput(global_step=65, training_loss=4.90222402719351, metrics={'train_runtime': 10.2381, 'train_samples_per_second': 101.581, 'train_steps_per_second': 6.349, 'total_flos': 0.0, 'train_loss': 4.90222402719351, 'epoch': 5.0})"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "trainer = SentenceTransformerTrainer(\n",
+ " model=model,\n",
+ " args=args,\n",
+ " train_dataset=sentence_to_label_train_dataset,\n",
+ " eval_dataset=sentence_to_label_test_dataset,\n",
+ " loss=loss,\n",
+ " evaluator=train_evaluator,\n",
+ ")\n",
+ "\n",
+ "trainer.train()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'bge-base-en-eval_cosine_accuracy': 0.9696969696969697,\n",
+ " 'bge-base-en-eval_dot_accuracy': 0.030303030303030304,\n",
+ " 'bge-base-en-eval_manhattan_accuracy': 0.9696969696969697,\n",
+ " 'bge-base-en-eval_euclidean_accuracy': 0.9696969696969697,\n",
+ " 'bge-base-en-eval_max_accuracy': 0.9696969696969697}"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "test_evaluator = TripletEvaluator(\n",
+ " anchors=eval_triplets[\"anchor\"],\n",
+ " positives=eval_triplets[\"positive\"],\n",
+ " negatives=eval_triplets[\"negative\"],\n",
+ " name=EVAL_EVALUATOR_NAME,\n",
+ ")\n",
+ "test_evaluator(model)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "a47bc8065f934923bd56f93717d3e7f1",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "model.safetensors: 0%| | 0.00/438M [00:00, ?B/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "'https://huggingface.co/ivanleomk/finetuned-bge-base-en/commit/e377421e7ddc5908c3a18758c5b9d7681dce5850'"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "model.save_pretrained(f\"models/finetuned-{BASE_MODEL_NAME}\")\n",
+ "model.push_to_hub(FINETUNED_MODEL_NAME, exist_ok=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Evaluation\n",
+ "\n",
+ "Here we use `lancedb` again to evaluate our model. It comes with out of the box support for hugging face models, which makkes it incredibly easy for us to evaluate our fine-tuned model vs the base model. \n",
+ "\n",
+ "We'll similarly use `pydantic-evals` to evaluate our model and compare it against the base model by looking at recall and mrr @1,3,5."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lancedb\n",
+ "from lancedb.pydantic import LanceModel, Vector\n",
+ "from lancedb.embeddings import get_registry\n",
+ "\n",
+ "\n",
+ "def create_lancedb_table(model_name: str, categories: list[str]):\n",
+ " model = get_registry().get(\"huggingface\").create(name=model_name)\n",
+ "\n",
+ " class Category(LanceModel):\n",
+ " text: str = model.SourceField()\n",
+ " embedding: Vector(model.ndims()) = model.VectorField()\n",
+ "\n",
+ " db = lancedb.connect(\"./lancedb\")\n",
+ " table_name = f\"categories-{model_name.replace('/', '-')}\"\n",
+ " if table_name in db.table_names():\n",
+ " table = db.open_table(table_name)\n",
+ " else:\n",
+ " table = db.create_table(table_name, schema=Category, mode=\"overwrite\")\n",
+ " table.add(\n",
+ " [\n",
+ " {\n",
+ " \"text\": category[\"category\"],\n",
+ " }\n",
+ " for category in categories\n",
+ " ]\n",
+ " )\n",
+ "\n",
+ " return table\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from helpers import get_metrics_at_k, task\n",
+ "from dataclasses import dataclass\n",
+ "from pydantic_evals.evaluators import Evaluator, EvaluatorContext\n",
+ "from lancedb.table import Table\n",
+ "from concurrent.futures import ThreadPoolExecutor\n",
+ "from functools import partial\n",
+ "import logfire\n",
+ "import asyncio\n",
+ "\n",
+ "\n",
+ "@dataclass\n",
+ "class RagMetricsEvaluator(Evaluator):\n",
+ " async def evaluate(self, ctx: EvaluatorContext[str, str]) -> dict[str, float]:\n",
+ " predictions = ctx.output\n",
+ " labels = ctx.expected_output\n",
+ " metrics = get_metrics_at_k(metrics=[\"mrr\", \"recall\"], sizes=[1, 3, 5])\n",
+ " return {\n",
+ " metric: score_fn(predictions, labels)\n",
+ " for metric, score_fn in metrics.items()\n",
+ " }\n",
+ "\n",
+ "\n",
+ "async def retrieve_results(\n",
+ " question: str,\n",
+ " table: Table,\n",
+ " pool: ThreadPoolExecutor,\n",
+ " max_k=25,\n",
+ " reranker=None,\n",
+ "):\n",
+ " loop = asyncio.get_running_loop()\n",
+ " return await loop.run_in_executor(\n",
+ " pool,\n",
+ " partial(task, user_query=question, table=table, max_k=max_k, reranker=reranker),\n",
+ " )\n",
+ "\n",
+ "\n",
+ "logfire.configure(\n",
+ " send_to_logfire=True,\n",
+ " environment=\"experimentation\",\n",
+ " service_name=\"synthetic-transactions\",\n",
+ " console=False,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import concurrent\n",
+ "from helpers import get_metrics_at_k, task\n",
+ "import json\n",
+ "\n",
+ "categories = json.load(open(CATEGORIES_PATH))\n",
+ "base_table = create_lancedb_table(BASE_MODEL_NAME, categories)\n",
+ "finetuned_table = create_lancedb_table(FINETUNED_MODEL_NAME, categories)\n",
+ "\n",
+ "db = lancedb.connect(\"./lancedb\")\n",
+ "\n",
+ "eval_data.evaluators = []\n",
+ "eval_data.add_evaluator(RagMetricsEvaluator())\n",
+ "\n",
+ "results = []\n",
+ "for query_table in [base_table, finetuned_table]:\n",
+ " with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:\n",
+ " result = await eval_data.evaluate(\n",
+ " partial(retrieve_results, table=query_table, pool=executor)\n",
+ " )\n",
+ " results.append(result)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " mrr@1 \n",
+ " mrr@3 \n",
+ " mrr@5 \n",
+ " recall@1 \n",
+ " recall@3 \n",
+ " recall@5 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " Base Model \n",
+ " 0.30 \n",
+ " 0.48 \n",
+ " 0.50 \n",
+ " 0.30 \n",
+ " 0.70 \n",
+ " 0.80 \n",
+ " \n",
+ " \n",
+ " Fine-Tuned Model \n",
+ " 0.55 \n",
+ " 0.69 \n",
+ " 0.71 \n",
+ " 0.55 \n",
+ " 0.86 \n",
+ " 0.94 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " mrr@1 mrr@3 mrr@5 recall@1 recall@3 recall@5\n",
+ "Base Model 0.30 0.48 0.50 0.30 0.70 0.80\n",
+ "Fine-Tuned Model 0.55 0.69 0.71 0.55 0.86 0.94"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "scores = []\n",
+ "\n",
+ "for result in results:\n",
+ " result_scores = {}\n",
+ " for score_name, score in result.averages().scores.items():\n",
+ " result_scores[score_name] = score\n",
+ " scores.append(result_scores)\n",
+ "\n",
+ "df = pd.DataFrame(scores, index=[\"Base Model\", \"Fine-Tuned Model\"])\n",
+ "df.round(2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAHqCAYAAABMTMx9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABhr0lEQVR4nO3dCZiVVd0A8DPsouLGpqTiviWiqIhLtqC4pNJKZoKktChpkqmUsmiJW4R9mX4upJUmaWZ9aViR5oZimEsl7ogbAqmAGIsw3/M/ONMMM4MDAve+M7/f87zPcN/73vueux3e//mfpaKysrIyAQAAAAAAlLkWpS4AAAAAAABAY0hqAAAAAAAAhSCpAQAAAAAAFIKkBgAAAAAAUAiSGgAAAAAAQCFIagAAAAAAAIUgqQEAAAAAABSCpAYAAAAAAFAIkhoAAAAAAEAhSGoAkE444YTUvXv31BQU5bVUVFSkoUOHrvbj33777dS5c+d0ww03pHK2ZMmStOWWW6af/OQnpS4KAACskev4UaNGVd++7rrr8r7p06eXtFwAzYmkBlB4VReRsd1333117q+srMyNqnH/Jz/5yVTOojG+6rXEtv7666d99903/exnPyt10Zqcj370o7Xe6/XWWy/16NEjjRs3Li1btiyVu8suuyxtuOGG6Qtf+EL1vgiu4rW0aNEivfTSS3UeM2/evPw6V0yoRABW872Ix2+66abp8MMPT5MnT67zPFXnqdpat26dv7unnnpqeuutt2odG/cNGzYsff/7308LFy5c4+8DAABNN76LrVWrVqlbt26589Irr7xS6uIBUAZalboAAGtKu3bt0o033pgOPPDAWvv/+te/ppdffjm1bds2FUHPnj3Tt771rfzv1157LV1zzTVp0KBBadGiRWnIkCFr5ZxXX311IRry17QPfehDacyYMfnfc+bMyd+f008/Pc2ePTs3wpfz6IdIakRZW7ZsWef++K7/8pe/TGeeeWat/bfeeutKn/fYY49NRxxxRFq6dGl6+umn8+iKj33sY+nhhx9Ou+++e53jr7jiirTBBhukBQsWpEmTJqX/+Z//SY888kid5OLgwYPT2Wefnd/fL3/5y6v9ugEAaD7OO++8tM022+SOMQ8++GBOdsR15j/+8Y8c+wHQfBmpATQZ0Rh78803p3fffbfW/mhI7dWrV+ratWsqguiF9KUvfSlv3/72t/OFezQc//CHP1xr54ze9Gsy6RON3EWw0UYbVb/X3/zmN9M999yTtt5669w4Hw375er3v/99Trx8/vOfb/C3EEmNFcVv4cgjj2zweffaa6/8XkQSLZI68RyRTIvkRX0++9nP5uO/+tWvpl/96ldpwIAB6f77709TpkypddzGG2+cDj300ByIAgBAY8So4bjWPOmkk3JHrzPOOCM999xz6Xe/+12piwZAiUlqAE1G9DL/97//nf70pz9V71u8eHG65ZZb0he/+MV6HxOjE2K6od122y339unSpUtuoH3zzTdrHffb3/42NwZvscUWufF/u+22S+eff36dhu+Y0ujDH/5w+te//pV7uLdv3z4nKS6++OLVfl2dOnVKO++8c76AX52yhz/84Q/p4IMPztMVdejQIe2zzz65gbuhdSiqpiO69NJLczIlGvpj2qJ4jugZVVM8NpIuUb5oTI9zHHfccdXJjRh1EtN/xfu200475eeMKcFW9Itf/CJPtRXv2SabbJI+8pGPpD/+8Y+r/Bl8EPE+xnszf/78NGvWrOr9jz/+eH6d2267bT4mEmQx4iC+b/VNy/Tss8/m46MxPxInMVLhnXfeed/zf+9738tTP0VSZWVuu+22/HnFe1Cf+L4/+uijadq0adX7Zs6cmf7yl780+Fuoz0EHHZT/rvjdW53jDznkkJyge+ONNxp9fgAAWNm1ZlzvRkebmDo1rtP33nvvepMeMUVqjHKOa+iIJWLE9sCBA/No7aq4ccSIEbkzXFy/xzTAcb677rprHb5CABpLUgNoMuICtU+fPrV6qEdj/ty5c2utO1BTJAFiNMQBBxyQp/OJxudYeLlfv355ip8q0cM8Gu5jbYA4Li5246I3ptRZUSQVDjvssLTHHnukH/zgBzkhcdZZZ+WyrI4YeRLTZ0VD/+qWPZIB0Zg8fPjwdOGFF+YpriZOnPi+5461PH70ox+lU045JT82Ehof//jH0+uvv16njHHeWLg6khaf+cxncuLi6KOPzkmReD/Gjh2bkxpR5ngfaxo9enQ6/vjj84iRGGYetyMREo3wq/MZfBBVCZ1ISFSJRNnzzz+f3+NIOMT36aabbspJnPoSNDGCIhIjMbVV/DvKHq9pZc4555z8ev73f/83feMb31jpsQ888EAeVdGQSAhFoFYzcTVhwoT8/q1spMaKqhY7XPG7tzrHx+cV71WUHQAAVtWK15r//Oc/03777ZeefPLJHBNE7BXJiP79+6ff/OY31Y97++23c4IiruNj9HDEEl/72tdyQiTirKq152I0SHRSu+iii3JnpRgZHTFOdBYCoMxUAhTcT3/602hVrnz44Ycrf/zjH1duuOGGle+8806+73Of+1zlxz72sfzvrbfeuvLII4+sfty9996bH3fDDTfUer6JEyfW2V/1fDV99atfrWzfvn3lwoULq/cdfPDB+bE/+9nPqvctWrSosmvXrpWf+cxn3ve1RBkPPfTQytmzZ+ftiSeeqDz++OPzc55yyimrXPa33norvx+9e/eu/M9//lPr2GXLllX/e9CgQfncVV544YX8POutt17lyy+/XL3/oYceyvtPP/30Wo+NfWeffXat57/tttvy/u9973u19n/2s5+trKioqHz22Wfz7WeeeaayRYsWlZ/61Kcqly5d2mAZG/sZrPhaGhKf1c4771z9Xk+bNq3y29/+di5zze9JQ+f+5S9/mY+95557qveNHDky7/vyl79c69h4bZtttlmtfTU/029961v5Pbjuuuvet9xLlizJ7188ZkVV54/Xc8YZZ1Ruv/321ffts88+lYMHD65z7pqf9+jRo/NjZ86cmb9j8ZjYf/PNN9d7nqeeeiofP3369Mrx48fn70unTp0qFyxYUKdsr776an7MRRdd9L6vEQCA5qsqvvvzn/+crzVfeumlyltuuSVfZ7Zt2zbfDp/4xCcqd99991qxQMQP+++/f+UOO+xQvW/EiBH5+W699dY656qKN959990ct9X05ptvVnbp0qXOtX08V1wPr1jeuKYGYN0wUgNoUqJX/H/+85+85kD0lI+/DU23E+tvxNDimBYnhh1XbdGjPHq01xxqHFMvVYnnjeOit09MKVRzip8Qj425X6u0adMmT6sUPf0bI6ZciimnYovFmX/+85/nEQKXXHLJKpc9RhhEeaPn0oqL6cVohPcTvZxi+qwq8Tp69+6d7rjjjjrHfv3rX691O46JRaxPPfXUWvtjOqqIBapGrsRUSjGVVoxSiKmXGirjqnwGjRWPq3qvY0RNvMcxumTFtR9qnjsWKoxzR6+wEAtjryh6ftUU5YypqqIHWE3xPgwdOjT3Fovpt2Iti/cTI27ice83eiK+9zENVizyXfX3/aaeGjlyZH4v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gPJlofBG6n727ev0fNMvPHKVjo+L6Ouvv7769qabbpr22WefdPHFF6cePXqkUrnuuuvS4MGD084775yefPLJWvfdfPPN6fOf/3zaeuut0/Tp00tWRgAAWNfEF6tHfAEAgJEaTchhhx2WXnvttbxNmjQptWrVKn3yk58sdbHS+uuvn2bNmpUmT55ca/+1116bttpqq5KVCwAAaJj4AgCAciSp0YS0bds2de3aNW89e/ZMZ599dnrppZfS7Nmzq48566yz0o477pjat2+ftt1223TuueemJUuWVN//2GOPpY997GNpww03TB06dEi9evVKf/vb36rvv++++9JBBx2U1ltvvbTlllumU089NS1YsGCl5Yrg54tf/GIaP3589b6XX3453X333Xn/iq644oq03XbbpTZt2qSddtop/fznP691/zPPPJM+8pGPpHbt2qVdd901/elPf6rzHPG6o5fWxhtvnHuVHXPMMXprAQDAKhBf/Jf4AgCgfEhqNFFvv/12+sUvfpG23377tNlmm1Xvj2Aihmz/61//Spdddlm6+uqr0w9/+MPq+4877rj0oQ99KD388MNp6tSpOXBp3bp1vu+5557LvbU+85nPpMcffzxNmDAhByFDhw593/J8+ctfTr/61a/SO++8k29HGeK5unTpUuu43/zmN+m0005L3/rWt9I//vGP9NWvfjUPL7/rrrvy/cuWLUuf/vSnc0Dy0EMPpSuvvDIHUjVFENWvX7/8Wu+99950//33pw022CCfb/HixR/wnQUAgOZHfCG+AAAoF9bUaEJ+//vf54vrEL2bNt9887yvRYv/5q7OOeec6n937949nXHGGemmm25KZ555Zt43Y8aM9O1vfzvPURt22GGH6uPHjBmTg5JvfvOb1ff96Ec/SgcffHDu/RQ9mxqy55575p5bt9xySzr++ONz0DF27Nj0/PPP1zru0ksvzfP3nnzyyfn2sGHD0oMPPpj3Rw+vP//5z2natGnpzjvvTFtssUU+5oILLkiHH3549XNEMBTByTXXXJMX+As//elPc6+q6L116KGHfqD3GQAAmgPxxXLiCwCA8mKkRhMSF+WPPvpo3qZMmZJ7E8XF+IsvvljrgvyAAw7IQ8gjQIkgJAKNKnGRf9JJJ6W+ffumCy+8MPeeqjl0PIKFeFzVFueIC/wXXnihUb2p4uL/r3/9aw6K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nDlz0umnn746TwsAADRD4gsAAGCtJjX+53/+J11xxRVp4MCB1fuOPvrotNtuu6VRo0YJOgAAgEYTXwAAAGt1+qnXXnst7b///nX2x764DwAAoLHEFwAAwFpNamy//fbpV7/6VZ39EyZMSDvssMPqPCUAANBMiS8AAIC1Ov3U6NGj04ABA9I999xTPeft/fffnyZNmlRvMAIAANAQ8QUAALBWR2p85jOfSQ899FDq2LFjuu222/IW/54yZUr61Kc+tTpPCQAANFPiCwAAYK2O1Ai9evVKv/jFL1b34QAAANXEFwAAwFobqXHHHXekO++8s87+2PeHP/xhdZ4SAABopsQXAADAWk1qnH322Wnp0qV19ldWVub7AAAAGkt8AQAArNWkxjPPPJN23XXXOvt33nnn9Oyzz67OUwIAAM2U+AIAAFirSY2NNtooPf/883X2R8Cx/vrrr85TAgAAzZT4AgAAWKtJjWOOOSZ985vfTM8991ytgONb3/pWOvroo1fnKQEAgGZKfAEAAKzVpMbFF1+ce0zFcPBtttkmb/HvzTbbLF166aWr85QAAEAzJb4AAAAaq1VazeHhDzzwQPrTn/6UHnvssbTeeuulPfbYIx100EGr83QAAEAzJr4AAADWykiNyZMnp9///vf53xUVFenQQw9NnTt3zr2nPvOZz6SvfOUradGiRavylAAAQDMlvgAAANZqUuO8885L//znP6tvP/HEE2nIkCHpkEMOSWeffXb6v//7vzRmzJhVLgQAAND8iC8AAIC1mtR49NFH0yc+8Ynq2zfddFPad99909VXX52GDRuWfvSjH6Vf/epXq1wIAACg+RFfAAAAazWp8eabb6YuXbpU3/7rX/+aDj/88Orb++yzT3rppZdWuRAAAEDzI74AAADWalIjAo4XXngh/3vx4sXpkUceSfvtt1/1/fPnz0+tW7de5UIAAADNj/gCAABYq0mNI444Is9te++996bhw4en9u3bp4MOOqj6/scffzxtt912q1wIAACg+RFfAAAAq6rVqhx8/vnnp09/+tPp4IMPThtssEG6/vrrU5s2barvHz9+fDr00ENXuRAAAEDzI74AAADWalKjY8eO6Z577klz587NQUfLli1r3X/zzTfn/QAAAO9HfAEAAKzVpEaVjTbaqN79m2666eo8HQAA0IyJLwAAgLWypgYAAAAA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iGuRH//4x+mJJ55I9913X+5AEUnQ2bNnl+x1QFNS5PohRBKjZuzxy1/+MhVaJRTEwQcfXDl06NDK0047rXLjjTeu7Ny5c+VVV11V+fbbb1eecMIJlRtssEHldtttV3nHHXfk4++6667oFplv77XXXpWtW7fO++J5TjnllPw8m222WeVHP/rRfPzDDz9cuemmm1b+7ne/q/f8S5YsqRw8eHDlUUcdVe/9P/3pTys32mijtfgOAEWtH6r89re/rayoqKhcvHjxWngXgCLVD1tvvXXlD3/4w3XwDgD1/X6feOKJysMOO6xy/fXXz/XCl770pcrZs2dXP2bp0qWVF110Ua4f2rRpU7nllltWfu9736u+/8wzz6zcYYcdKtdbb73KbbbZpvKcc86p9f/7yJEjK/fYY4/q24MGDao85phjqm+/9dZbldtvv33lueeeW7ls2bJ6yx11VdQVb7zxRp37XnjhhVxX/f3vf18j7xE0V02xfqgyd+7cXE/8+c9//kDvETRXTal+GLTC8zQFRmpQKNdff33q2LFjmjJlSu7NGKMjYpREDLF65JFHci+EmN6l5lCrs88+O0/n8OSTT6YePXpUP0+bNm3S/fffn6688sq8L54vppI66qij0r/+9a908MEH5+kjPv/5z6dhw4aliy++OB8b9911110lew+AYtYPb7zxRrrhhhtyeVq3br2O3hWgnOuHeP6YQmbPPfdMl1xySb0jQoA1o+bvN357H//4x/Nv729/+1uaOHFiev311/Pvtsrw4cPzceeee27+/d54442pS5cu1fdvuOGG6brrrsv3XXbZZenqq69OP/zhDxtdnnjuXr165RFbc+fOTccdd1z11BE/+tGP0uGHH56GDBmSDjrooDRu3Lg1/n4ATbt+iB7lMTXNRhttlPbYY48P+A5B89WU6oe77747de7cOe200045Horpcwut1FkVWJUM6YEHHlh9+913382Z0eOPP75632uvvZZ7IkyePLm6p+Vtt91W53n23HPPWvuefvrpyq5du+belPG8O+64Y+VXvvKV3PPpRz/6UWWrVq1ytjREFvWss86qUz4jNaB0yrl+iJ4Y7du3z+fbb7/9KufMmbOW3gWgSPXDD37wg3yuxx57rPKKK67Io0hOP/30tfhOQPO14u/3/PPPrzz00ENrHfPSSy/l3/5TTz1VOW/evMq2bdtWXn311Y0+xyWXXFLZq1evRve07NatW+7tGb785S9X9unTp/LBBx/Mo76iXokyh+hh3bt37zrnM1ID1oymVj/83//9X77OidHhW2yxReWUKVNW6f0Ammb98Mtf/jLPHPH4449X/uY3v6ncZZddKvfZZ58cwxRVq1InVWBVVPWUDC1btsy9G3fffffqfVXZz1mzZqUOHTrkf++99951nieymjXFnJOxsGerVq1ytvSVV17Jc1FGb+qePXum3/3ud9XHxmK/jz322Fp5fUDTqx++/e1vpxNPPDG9+OKLafTo0WngwIHp97//fV6YC2i+9UOM4qhZvugB9tWvfjWNGTMmtW3bdo29dqDu7zd+izFyaoMNNqhzXKx/9dZbb6VFixalT3ziEw0+34QJE3KPyDj+7bffziOtquqP9xOjN+fPn58+/OEP59v/93//l2677bbUu3fvfHvo0KHpT3/6U3Xd8eabb67y6wWaZ/0Qa/o9+uijac6cObkHePQgj8XCo3c20Hzrhy984QvV/444KOKP7bbbLo/eWFl5y5nppyiUFadsiUbBmvuqGgmXLVtWvW/99dev8zwr7otKZL311qsephnPWfN5a1ZYMU3F9ttvv0ZeD9D064eY8iYW7TvkkEPSTTfdlO6444704IMPfoBXCjSV+qGmCEbi+aZPn76Krw5ojJq/32hEiCnjouGv5vbMM8+kj3zkI9W/64ZMnjw5T/dwxBFH5I4Kf//739N3v/vdRi8gGr/1du3aVd+Ox9Usn9gD1q2mVD/EsbFvv/32S9dee23ueBF/gdXTlOqHmrbddtvcVvHss8+mopLUgJTyDz16W4aYWy4aJKKn5dKlS3Pj45133pmWLFmS59L7wx/+kE444YRSFxkoYP1Q1WAavTeA4luT9UMERC1atNCTEtaBvfbaK/3zn/9M3bt3z7/jmls0Duywww65YWLSpEn1Pv6BBx5IW2+9dW6IiFFdcXyMyGysaESIhoiYhzsceOCBef2d//znP3nEV/SurjpPnKPmyC5g7Wpq9UPEH2IPWDOaUv3w8ssv5zU1YkRHUUlqQEp5kZ+oBGIYWVRAsWjPiBEj8vQPgwcPTv37908XXXRR+ulPf5r++Mc/5gVAq8yYMSM3RMTfaMSoytRGBhdovvVDDPOOxs2oD+JC5S9/+Us69thj8xDPPn36lPplASWsH6KXVizcF0PYn3/++XTDDTek008/PX3pS19Km2yySalfFjR5p5xySp7CIf5ffvjhh/MUEJGEjN9tXM9HL8izzjornXnmmelnP/tZvj8SlVW9naMRIq79YwRm3BfTSPzmN79p9PkjgXn00Uenn/zkJ/l2LBQavTWjh2VMCRGjO//617+mL3/5y/m+mtNCRLnj2iKmvAtPPfVUvj1z5sw1/j5Bc1TU+mHBggXpO9/5Ti5LxB5Tp07Nx0RD5+c+97m19G5B81LU+uHtt9/O02JHWWJUeCRdjjnmmJyM6devXyoqa2rAe9NORKPDoEGD0j333JOHgs2ePTsHB1tssUWaO3duuuqqq+qdNy8aL6IHZs0GjhANHB/96EfX6esAyqd+aN++fbr11lvTyJEjc5ARPSAOO+ywdM4555gvH5p5/RB1QAQzo0aNyr0nt9lmm5zU0Bsb1o34fd5///254eHQQw/Nv8PoORn/T0eDQTj33HPztC1xrf/qq6/m/8e/9rWv5fuiQSF+szF3dTz2yCOPzMfHb7qx4nn33XffPEXM4YcfnpMUUXdEYjN6VkcPy+iRuaJYqycaT1acIzuuN1bl/EDTqh9izbBp06bltolYTyPWD4t1v+6999602267reF3CZqnItcPjz/+eK4fYt2PeB1R/vPPP7/QbRMVsVp4qQsB5eKCCy5IY8eOTcOHD08DBgxIH/rQh3JFE5nO+LFHY8OnPvWpUhcTKAH1A9AQ9QOwOmIEVyQlYpTWkCFDqhseY1q7Sy+9NI/uiroFaH7UD0BD1A/LSWrACqInw/e+97109913p/h5xEI8u+66azr11FPTSSedVJ19BZof9QPQEPUDsDpeeOGFdN555+XpJ6qmr421dWIEWCRKO3ToUOoiAiWifgAa8oL6QVIDGhI9LGfNmpU23HDDtPHGG5e6OEAZUT8ADVE/AKsjpoyIhT8jAdqlS5dSFwcoI+oHoCHLmnH9IKkBAAAAAAAUgnHwAAAAAABAIUhqAAAAAAAAhSCpAQAAAAAAFIKkBgAAAAAAUAiSGgAAAAAAQCFIagAAAAAAAIUgqQEAAAAAABSCpAYAAAAAAFAIkhoAAAAAAEAqgv8HWEuewWUgDTwAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "# Create figure with two subplots side by side\n",
+ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 5))\n",
+ "\n",
+ "# Plot MRR scores\n",
+ "mrr_cols = [\"mrr@1\", \"mrr@3\", \"mrr@5\"]\n",
+ "x = np.arange(len(mrr_cols))\n",
+ "width = 0.25\n",
+ "\n",
+ "for i, model in enumerate(df.index):\n",
+ " offset = (i - 1) * width\n",
+ " ax1.bar(x + offset, df.loc[model, mrr_cols], width, label=model)\n",
+ "\n",
+ "ax1.set_title(\"Mean Reciprocal Rank (MRR)\")\n",
+ "ax1.set_xticks(x)\n",
+ "ax1.set_xticklabels(mrr_cols)\n",
+ "ax1.set_ylabel(\"Score\")\n",
+ "ax1.legend()\n",
+ "ax1.grid(True, alpha=0.3)\n",
+ "\n",
+ "# Plot Recall scores\n",
+ "recall_cols = [\"recall@1\", \"recall@3\", \"recall@5\"]\n",
+ "x = np.arange(len(recall_cols))\n",
+ "\n",
+ "for i, model in enumerate(df.index):\n",
+ " offset = (i - 1) * width\n",
+ " ax2.bar(x + offset, df.loc[model, recall_cols], width, label=model)\n",
+ "\n",
+ "ax2.set_title(\"Recall\")\n",
+ "ax2.set_xticks(x)\n",
+ "ax2.set_xticklabels(recall_cols)\n",
+ "ax2.set_ylabel(\"Score\")\n",
+ "ax2.legend()\n",
+ "ax2.grid(True, alpha=0.3)\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Conclusion\n",
+ "\n",
+ "In this notebook, we successfully fine-tuned the BAAI/bge-base-en model using sentence-transformers, achieving a 20% improvement in MRR@1 and a 12% increase in recall@5 compared to the base model. More importantly, we learned how to balance performance gains against implementation complexity.\n",
+ "\n",
+ "This concludes our exploration of fine-tuning approaches, where we:\n",
+ "\n",
+ "1. Created synthetic training data thoughtfully using iterative generation and manual review to ensure quality\n",
+ "2. Explored managed re-rankers through Cohere, showing how they offer quick wins with minimal setup\n",
+ "3. Implemented open-source fine-tuning using sentence-transformers, trading simplicity for greater control and lower inference costs\n",
+ "\n",
+ "While managed solutions like Cohere offer faster time-to-production and simplified deployment, open-source models provide greater control and customization potential. The choice between these approaches should be guided by your specific requirements, resources, and the level of performance improvement needed for your use case. \n",
+ "\n",
+ "These retrieval improvements complement the techniques we'll explore in later weeks - like discovering query patterns (Week 4) and handling structured data (Week 5). By combining effective retrieval with these methods, we can build more robust and capable RAG systems."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}