{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Final Assignment: Green Patent Detection\n",
"## Advanced Agentic Workflow with QLoRA\n",
"\n",
"This notebook builds on Assignments 2 and 3 to construct a data labeling pipeline that:\n",
"1. Fine-tunes an LLM with **QLoRA** to understand patent language\n",
"2. Integrates that model into a **Multi-Agent System** (Advocate, Skeptic, Judge)\n",
"3. Uses **Exception-Based HITL** only reviewing disagreements between the Skeptic and Advocator\n",
"4. Produces a final **fine-tuned PatentSBERTa** model"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Part A & B: Setup (Same as Assignment 2 & 3)\n",
"Load the dataset, reproduce splits, and export the same top 100 high-risk claims."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Using device: cpu\n",
"Note: Install torch-directml for AMD GPU acceleration: pip install torch-directml\n"
]
}
],
"source": [
"import os\n",
"import json\n",
"import re\n",
"import requests\n",
"import numpy as np\n",
"import pandas as pd\n",
"import torch\n",
"from transformers import AutoTokenizer, AutoModel, logging as hf_logging\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import classification_report, f1_score\n",
"from tqdm.auto import tqdm\n",
"\n",
"hf_logging.set_verbosity_error()\n",
"\n",
"# Device setup\n",
"try:\n",
" import torch_directml\n",
" DEVICE = torch_directml.device()\n",
" print(f\"Using device: DirectML (AMD GPU)\")\n",
"except ImportError:\n",
" if torch.cuda.is_available():\n",
" DEVICE = torch.device(\"cuda\")\n",
" elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():\n",
" DEVICE = torch.device(\"mps\")\n",
" else:\n",
" DEVICE = torch.device(\"cpu\")\n",
" print(f\"Using device: {DEVICE}\")\n",
" print(\"Note: Install torch-directml for AMD GPU acceleration: pip install torch-directml\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Data already exists in 'patents_data_raw'. Skipping download.\n"
]
}
],
"source": [
"# Download and prepare the dataset (if not already done)\n",
"from huggingface_hub import snapshot_download\n",
"\n",
"folder_name = \"patents_data_raw\"\n",
"if os.path.exists(folder_name) and any(os.scandir(folder_name)):\n",
" print(f\"Data already exists in '{folder_name}'. Skipping download.\")\n",
"else:\n",
" print(f\"Downloading dataset files to '{folder_name}'\")\n",
" try:\n",
" snapshot_download(\n",
" repo_id=\"AI-Growth-Lab/patents_claims_1.5m_traim_test\",\n",
" repo_type=\"dataset\",\n",
" local_dir=folder_name,\n",
" ignore_patterns=[\"*.git*\"]\n",
" )\n",
" print(\"Download complete.\")\n",
" except Exception as e:\n",
" print(f\"Download failed: {e}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found existing file: patents_50k_green.parquet. Skipping.\n"
]
}
],
"source": [
"# Create the balanced 50k parquet (if not already done)\n",
"from datasets import load_dataset, concatenate_datasets, disable_progress_bar\n",
"import datasets\n",
"\n",
"disable_progress_bar()\n",
"datasets.utils.logging.set_verbosity_error()\n",
"\n",
"output_filename = \"patents_50k_green.parquet\"\n",
"\n",
"if os.path.exists(output_filename):\n",
" print(f\"Found existing file: {output_filename}. Skipping.\")\n",
"else:\n",
" print(\"Loading and filtering dataset...\")\n",
" dataset_full = load_dataset(\"./patents_data_raw\", split=\"train\")\n",
" y02_cols = [c for c in dataset_full.column_names if c.startswith(\"Y02\")]\n",
"\n",
" dataset_green = dataset_full.filter(\n",
" lambda x: any(x[col] == 1 for col in y02_cols), num_proc=1\n",
" ).shuffle(seed=42).select(range(25000))\n",
"\n",
" dataset_not_green = dataset_full.filter(\n",
" lambda x: all(x[col] == 0 for col in y02_cols), num_proc=1\n",
" ).shuffle(seed=42).select(range(25000))\n",
"\n",
" dataset_green = dataset_green.map(lambda x: {\"is_green_silver\": 1})\n",
" dataset_not_green = dataset_not_green.map(lambda x: {\"is_green_silver\": 0})\n",
"\n",
" final_dataset = concatenate_datasets([dataset_green, dataset_not_green]).shuffle(seed=42)\n",
" final_dataset.to_parquet(output_filename)\n",
" print(f\"Saved: {output_filename} ({len(final_dataset):,} rows)\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading patents_50k_green.parquet...\n",
" train_silver: 2000 rows\n",
" eval_silver: 5000 rows\n",
" pool_unlabeled: 43000 rows\n"
]
}
],
"source": [
"# Creating data splits identical to previous assignment\n",
"print(\"Loading patents_50k_green.parquet...\")\n",
"df = pd.read_parquet(\"patents_50k_green.parquet\")\n",
"\n",
"df_eval = df.sample(n=5000, random_state=42)\n",
"df_remaining = df.drop(df_eval.index)\n",
"df_train_silver = df_remaining.sample(n=2000, random_state=42)\n",
"df_pool = df_remaining.drop(df_train_silver.index)\n",
"\n",
"print(f\" train_silver: {len(df_train_silver)} rows\")\n",
"print(f\" eval_silver: {len(df_eval)} rows\")\n",
"print(f\" pool_unlabeled: {len(df_pool)} rows\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading PatentSBERTa for baseline...\n",
"Generating training embeddings...\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "d25cc8b82956458e84b21a45097af344",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Encoding: 0%| | 0/63 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating evaluation embeddings...\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "516080b2eccb496186c64d457073fcfb",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Encoding: 0%| | 0/157 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"PART A: BASELINE MODEL (Frozen Embeddings)\n",
"==================================================\n",
" precision recall f1-score support\n",
"\n",
" Not Green 0.74 0.76 0.75 2493\n",
" Green 0.75 0.74 0.75 2507\n",
"\n",
" accuracy 0.75 5000\n",
" macro avg 0.75 0.75 0.75 5000\n",
"weighted avg 0.75 0.75 0.75 5000\n",
"\n",
"Baseline Macro F1: 0.7494\n",
"==================================================\n"
]
}
],
"source": [
"# Baseline model + Uncertainty Sampling\n",
"print(\"Loading PatentSBERTa for baseline...\")\n",
"model_name = \"AI-Growth-Lab/PatentSBERTa\"\n",
"tokenizer = AutoTokenizer.from_pretrained(model_name)\n",
"base_model = AutoModel.from_pretrained(model_name)\n",
"base_model.to(DEVICE)\n",
"base_model.eval()\n",
"\n",
"def get_embeddings(text_list, batch_size=32):\n",
" all_embeddings = []\n",
" for i in tqdm(range(0, len(text_list), batch_size), desc=\"Encoding\", leave=False):\n",
" batch_texts = text_list[i:i+batch_size]\n",
" inputs = tokenizer(batch_texts, padding=True, truncation=True, max_length=128, return_tensors=\"pt\").to(DEVICE)\n",
" with torch.no_grad():\n",
" outputs = base_model(**inputs)\n",
" embeddings = outputs.last_hidden_state[:, 0, :].cpu().numpy()\n",
" all_embeddings.append(embeddings)\n",
" return np.vstack(all_embeddings)\n",
"\n",
"# Train baseline classifier\n",
"print(\"Generating training embeddings...\")\n",
"X_train = get_embeddings(df_train_silver['text'].tolist())\n",
"y_train = df_train_silver['is_green_silver'].values\n",
"\n",
"print(\"Generating evaluation embeddings...\")\n",
"X_eval = get_embeddings(df_eval['text'].tolist())\n",
"y_eval = df_eval['is_green_silver'].values\n",
"\n",
"clf_baseline = LogisticRegression(max_iter=1000, random_state=42)\n",
"clf_baseline.fit(X_train, y_train)\n",
"\n",
"y_pred_baseline = clf_baseline.predict(X_eval)\n",
"f1_baseline = f1_score(y_eval, y_pred_baseline, average='macro')\n",
"\n",
"print(\"\\n\" + \"=\"*50)\n",
"print(\"PART A: BASELINE MODEL (Frozen Embeddings)\")\n",
"print(\"=\"*50)\n",
"print(classification_report(y_eval, y_pred_baseline, target_names=['Not Green', 'Green']))\n",
"print(f\"Baseline Macro F1: {f1_baseline:.4f}\")\n",
"print(\"=\"*50)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating embeddings for the unlabeled pool...\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "070afc87fe144d7c85b972eb36782f02",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Encoding: 0%| | 0/1344 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Saved 100 high-risk claims to hitl_green_100_final.csv\n",
"High-risk claims: 100\n",
"Uncertainty range: [0.9965, 1.0000]\n"
]
}
],
"source": [
"# Export the same top 100 high-risk claims\n",
"safe_filename = \"hitl_green_100_final.csv\"\n",
"\n",
"if os.path.exists(safe_filename):\n",
" print(f\"Found existing file: '{safe_filename}'. Loading...\")\n",
" df_high_risk = pd.read_csv(safe_filename)\n",
"else:\n",
" print(\"Generating embeddings for the unlabeled pool...\")\n",
" X_pool = get_embeddings(df_pool['text'].tolist())\n",
" \n",
" probs = clf_baseline.predict_proba(X_pool)[:, 1]\n",
" uncertainty = 1 - 2 * np.abs(probs - 0.5)\n",
" \n",
" df_pool = df_pool.copy()\n",
" df_pool['p_green'] = probs\n",
" df_pool['u'] = uncertainty\n",
" \n",
" df_high_risk = df_pool.sort_values(by='u', ascending=False).head(100).copy()\n",
" \n",
" if 'id' in df_high_risk.columns:\n",
" df_high_risk = df_high_risk.rename(columns={'id': 'doc_id'})\n",
" else:\n",
" df_high_risk['doc_id'] = df_high_risk.index\n",
" \n",
" # Initialize columns for the MAS debate\n",
" for col in ['advocate_argument', 'skeptic_argument', 'judge_label', 'judge_confidence',\n",
" 'judge_rationale', 'agents_agree', 'is_green_gold']:\n",
" df_high_risk[col] = \"\"\n",
" \n",
" df_high_risk.to_csv(safe_filename, index=False)\n",
" print(f\"Saved {len(df_high_risk)} high-risk claims to {safe_filename}\")\n",
"\n",
"print(f\"High-risk claims: {len(df_high_risk)}\")\n",
"print(f\"Uncertainty range: [{df_high_risk['u'].min():.4f}, {df_high_risk['u'].max():.4f}]\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Part C, Step 1: Domain Adaptation with QLoRA\n",
"\n",
"Fine-tune a generative LLM using QLoRA on `train_silver` so it learns the dense linguistic style of patent claims and the logic of Y02 classifications.\n",
"\n",
"I have used **Unsloth** for efficient QLoRA fine-tuning, due to QLoRA only is compatible with NVIDIA, the fine-tuning have been made in Colab utilizing their free 15gb VRAM T4/A100 GPU. This seperate pipeline is in the file called \"Part_C_Step_1.ipny\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Part C, Step 2: Multi-Agent System (MAS) with CrewAI\n",
"\n",
"The fine-tuned QLoRA model is served via LM Studio and used as the judge for the agentates.\n",
"We use **CrewAI** as our multi-agent framework to orchestrate the debate.\n",
"\n",
"**Architecture:**\n",
"- **Agent 1 (The Advocate):** Uses the QLoRA fine-tuned model. Argues *for* the green classification.\n",
"- **Agent 2 (The Skeptic):** Uses the QLoRA fine-tuned model. Argues *against* (identifies greenwashing).\n",
"- **Agent 3 (The Judge):** Uses a larger model (e.g. Qwen3-8B). Weighs arguments and produces final label + rationale.\n",
"\n",
"**Setup:** Load the GGUF custom model based on Qwen3-8B model in LM Studio and note the model name."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Debate function ready.\n"
]
}
],
"source": [
"import json, re, os, time\n",
"from openai import OpenAI\n",
"\n",
"# Direct LM Studio clients\n",
"_client = OpenAI(base_url=\"http://127.0.0.1:1234/v1\", api_key=\"lm-studio\")\n",
"_NO_THINK = {\"enable_thinking\": False}\n",
"\n",
"def _llm(model, system, user, max_tokens=512, retries=3):\n",
" \"\"\"Call LM Studio directly with thinking mode disabled.\"\"\"\n",
" for attempt in range(retries + 1):\n",
" try:\n",
" resp = _client.chat.completions.create(\n",
" model=model,\n",
" messages=[{\"role\": \"system\", \"content\": system},\n",
" {\"role\": \"user\", \"content\": user}],\n",
" temperature=0.1,\n",
" max_tokens=max_tokens,\n",
" extra_body=_NO_THINK,\n",
" )\n",
" content = (resp.choices[0].message.content or \"\").strip()\n",
" if content:\n",
" return content\n",
" raise ValueError(\"LLM returned empty content\")\n",
" except Exception as e:\n",
" if attempt < retries:\n",
" wait = (attempt + 1) * 5\n",
" print(f\" [LLM retry {attempt+1}/{retries}] {e} — waiting {wait}s...\")\n",
" time.sleep(wait)\n",
" else:\n",
" raise\n",
"\n",
"def strip_think(text):\n",
" text = str(text)\n",
" cleaned = re.sub(r'.*?', '', text, flags=re.DOTALL).strip()\n",
" if not cleaned:\n",
" m = re.search(r'(.*?)', text, re.DOTALL)\n",
" if m:\n",
" cleaned = m.group(1).strip()\n",
" return cleaned or text\n",
"\n",
"def parse_judge_json(response_text):\n",
" text = strip_think(str(response_text))\n",
" clean = text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n",
" try:\n",
" return json.loads(clean)\n",
" except json.JSONDecodeError:\n",
" pass\n",
" match = re.search(r'\\{[^{}]*\\}', clean, re.DOTALL)\n",
" if match:\n",
" try:\n",
" return json.loads(match.group())\n",
" except json.JSONDecodeError:\n",
" pass\n",
" return None\n",
"\n",
"def run_crewai_debate(claim_text, idx):\n",
" \"\"\"Multi-agent debate: Advocate → Skeptic → Advocate → Skeptic → Judge.\"\"\"\n",
" s = claim_text[:1500] # claim snippet\n",
"\n",
" \n",
" ADV_SYS = (\"You are a Green Patent Advocate. Argue FOR green technology (Y02) \"\n",
" \"classification using specific language from the claim.\")\n",
" SKP_SYS = (\"You are a Greenwashing Skeptic. Argue AGAINST green classification. \"\n",
" \"Expose generic technology with no specific climate benefit.\")\n",
" JDG_SYS = (\"You are an Impartial Patent Judge. Output ONLY a valid JSON object — \"\n",
" \"no prose, no markdown, nothing else.\")\n",
"\n",
" \n",
" adv_r1 = strip_think(_llm(MODEL_ADVOCATE, ADV_SYS,\n",
" f\"Patent Claim:\\n{s}\\n\\nProvide a 2-3 sentence argument FOR green classification.\"))\n",
"\n",
" skp_r1 = strip_think(_llm(MODEL_SKEPTIC, SKP_SYS,\n",
" f\"Advocate argued: {adv_r1[:400]}\\n\\nPatent Claim:\\n{s}\\n\\n\"\n",
" f\"Provide a 2-3 sentence counter-argument AGAINST green classification.\"))\n",
"\n",
" \n",
" adv_r2 = strip_think(_llm(MODEL_ADVOCATE, ADV_SYS,\n",
" f\"Skeptic countered: {skp_r1[:400]}\\n\\n\"\n",
" f\"Provide a 2-sentence rebuttal defending green classification.\"))\n",
"\n",
" skp_r2 = strip_think(_llm(MODEL_SKEPTIC, SKP_SYS,\n",
" f\"Advocate rebutted: {adv_r2[:400]}\\n\\n\"\n",
" f\"Provide a 2-sentence final argument against green classification.\"))\n",
"\n",
" judge_prompt = (\n",
" f\"Patent Claim:\\n{s}\\n\\n\"\n",
" f\"Advocate (R1): {adv_r1[:300]}\\n\"\n",
" f\"Skeptic (R1): {skp_r1[:300]}\\n\"\n",
" f\"Advocate (R2): {adv_r2[:300]}\\n\"\n",
" f\"Skeptic (R2): {skp_r2[:300]}\\n\\n\"\n",
" f'Output ONLY this JSON (no other text):\\n'\n",
" f'{{\"suggestion\": 0, \"confidence\": \"Low\", \"rationale\": \"...\", \"agreement\": \"agree\"}}'\n",
" )\n",
" judge_raw = strip_think(_llm(MODEL_JUDGE, JDG_SYS, judge_prompt, max_tokens=200))\n",
"\n",
" transcript = (\n",
" f\"--- PATENT CLAIM ---\\n{claim_text}\\n\\n--- THE DEBATE ---\\n\"\n",
" f\"Advocate (R1): {adv_r1}\\nSkeptic (R1): {skp_r1}\\n\"\n",
" f\"Advocate (R2): {adv_r2}\\nSkeptic (R2): {skp_r2}\\n\\n\"\n",
" f\"--- JUDGE'S VERDICT ---\\n{judge_raw}\"\n",
" )\n",
" os.makedirs(TRANSCRIPT_DIR, exist_ok=True)\n",
" with open(os.path.join(TRANSCRIPT_DIR, f\"debate_row_{idx}.txt\"), \"w\", encoding=\"utf-8\") as f:\n",
" f.write(transcript)\n",
"\n",
" return {\n",
" \"advocate_summary\": f\"R1: {adv_r1[:200]} | R2: {adv_r2[:200]}\",\n",
" \"skeptic_summary\": f\"R1: {skp_r1[:200]} | R2: {skp_r2[:200]}\",\n",
" \"judge_raw\": judge_raw,\n",
" \"parsed\": parse_judge_json(judge_raw),\n",
" }\n",
"\n",
"print(\"Debate function ready.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All agents ready\n",
" 1. Green Patent Advocate → qwen/qwen3-4b\n",
" 2. Greenwashing Skeptic → qwen/qwen3-4b:2\n",
" 3. Impartial Patent Classification Judge → qlora-green-patent\n"
]
}
],
"source": [
"# Agents and LLM setup for MAS debate\n",
"import os\n",
"\n",
"LM_STUDIO_URL = \"http://127.0.0.1:1234/v1\"\n",
"MODEL_ADVOCATE = \"qwen/qwen3-4b\" # Advocate: Qwen3-4B instance 1\n",
"MODEL_SKEPTIC = \"qwen/qwen3-4b:2\" # Skeptic: Qwen3-4B instance 2\n",
"MODEL_JUDGE = \"qlora-green-patent\" # Judge: QLoRA fine-tuned ~8B model (Qwen3-8B))\n",
"# Temperatures have been set to 0.1 in LM studio, adjustment from Assignment 3, where the advocator and skeptic had a temperature of 0.3.\n",
"# enable_thinking=False disables this and forces a direct response.\n",
"_no_think = {\"enable_thinking\": False}\n",
"\n",
"llm_advocate = LLM(model=f\"openai/{MODEL_ADVOCATE}\", base_url=LM_STUDIO_URL, api_key=\"lm-studio\", temperature=0.1, max_tokens=512, extra_body=_no_think)\n",
"llm_skeptic = LLM(model=f\"openai/{MODEL_SKEPTIC}\", base_url=LM_STUDIO_URL, api_key=\"lm-studio\", temperature=0.1, max_tokens=512, extra_body=_no_think)\n",
"llm_judge = LLM(model=f\"openai/{MODEL_JUDGE}\", base_url=LM_STUDIO_URL, api_key=\"lm-studio\", temperature=0.1, max_tokens=256, extra_body=_no_think)\n",
"\n",
"FILENAME = \"hitl_green_100_final.csv\"\n",
"TRANSCRIPT_DIR = \"debate_transcripts_final\"\n",
"os.makedirs(TRANSCRIPT_DIR, exist_ok=True)\n",
"\n",
"advocate_agent = Agent(\n",
" role=\"Green Patent Advocate\",\n",
" goal=\"Argue logically FOR the classification of a patent claim as Green Technology (Y02). Identify genuine environmental benefits, energy savings, or climate change mitigation. Use specific language from the claim.\",\n",
" backstory=\"You are a domain-expert patent analyst fine-tuned on Y02 green technology classifications. You specialize in identifying legitimate innovations that contribute to climate change mitigation, renewable energy, and environmental sustainability.\",\n",
" llm=llm_advocate, verbose=False, allow_delegation=False,\n",
")\n",
"skeptic_agent = Agent(\n",
" role=\"Greenwashing Skeptic\",\n",
" goal=\"Argue logically AGAINST the green classification. Expose generic technology with no specific climate benefit. Identify greenwashing: superficial Y02 language without genuine innovation.\",\n",
" backstory=\"You are a rigorous patent examiner fine-tuned on Y02 classification data. You specialize in distinguishing genuine green technology from standard industrial processes superficially framed as environmental.\",\n",
" llm=llm_skeptic, verbose=False, allow_delegation=False,\n",
")\n",
"judge_agent = Agent(\n",
" role=\"Impartial Patent Classification Judge\",\n",
" goal=\"Weigh both sides of the debate using the CrewAI framework. Output a valid JSON verdict: suggestion (0/1), confidence (Low/Medium/High), rationale (one sentence), agreement (agree/disagree).\",\n",
" backstory=\"You are a senior patent classification judge fine-tuned on Y02 data. You make impartial evidence-based decisions via CrewAI. You ALWAYS respond in valid JSON only — no text outside the JSON object.\",\n",
" llm=llm_judge, verbose=False, allow_delegation=False,\n",
")\n",
"\n",
"print(f\"All agents ready\")\n",
"print(f\" 1. {advocate_agent.role} → {MODEL_ADVOCATE}\")\n",
"print(f\" 2. {skeptic_agent.role} → {MODEL_SKEPTIC}\")\n",
"print(f\" 3. {judge_agent.role} → {MODEL_JUDGE}\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- MAS Debate: 0 claims remaining ---\n",
"\n",
"\n",
"All debates completed! Errors: 0\n"
]
}
],
"source": [
"import pandas as pd\n",
"\n",
"FILENAME = \"hitl_green_100_final.csv\"\n",
"TRANSCRIPT_DIR = \"debate_transcripts_final\"\n",
"\n",
"df_debate = pd.read_csv(FILENAME)\n",
"\n",
"str_cols = ['advocate_argument', 'skeptic_argument', 'judge_confidence',\n",
" 'judge_rationale', 'agents_agree', 'is_green_gold']\n",
"for col in str_cols:\n",
" if col in df_debate.columns:\n",
" df_debate[col] = df_debate[col].astype(object)\n",
"\n",
"for col in ['debate_completed', 'deadlock', 'needs_hitl']:\n",
" if col not in df_debate.columns:\n",
" df_debate[col] = False\n",
"\n",
"remaining = df_debate[df_debate['debate_completed'] == False].index.tolist()\n",
"print(f\"--- MAS Debate: {len(remaining)} claims remaining ---\\n\")\n",
"\n",
"errors = []\n",
"\n",
"for count, idx in enumerate(remaining):\n",
" row = df_debate.loc[idx]\n",
" claim_text = str(row['text'])\n",
" uncertainty = row.get('u', 0.0)\n",
"\n",
" print(f\"\\n[{count+1}/{len(remaining)}] Row {idx} (Uncertainty: {uncertainty:.4f})\")\n",
" print(f\"Claim: {claim_text[:300]}...\\n\")\n",
"\n",
" try:\n",
" result = run_crewai_debate(claim_text, idx)\n",
"\n",
" if result['parsed']:\n",
" suggestion = result['parsed'].get('suggestion', 0)\n",
" confidence = str(result['parsed'].get('confidence', 'Low'))\n",
" rationale = str(result['parsed'].get('rationale', 'No rationale'))\n",
" agreement = str(result['parsed'].get('agreement', 'unknown'))\n",
" else:\n",
" suggestion, confidence, rationale, agreement = 0, \"Low\", \"Failed to parse\", \"unknown\"\n",
" print(\"Warning: Could not parse Judge response.\")\n",
"\n",
" is_deadlock = (agreement.lower() == 'disagree')\n",
" is_needs_hitl = is_deadlock or (confidence.lower() == 'low')\n",
"\n",
" print(f\"Judge: {suggestion} ({confidence}) | Agreement: {agreement} | Deadlock: {is_deadlock}\")\n",
"\n",
" df_debate.at[idx, 'advocate_argument'] = result['advocate_summary'][:500]\n",
" df_debate.at[idx, 'skeptic_argument'] = result['skeptic_summary'][:500]\n",
" df_debate.at[idx, 'judge_label'] = suggestion\n",
" df_debate.at[idx, 'judge_confidence'] = confidence\n",
" df_debate.at[idx, 'judge_rationale'] = rationale\n",
" df_debate.at[idx, 'agents_agree'] = agreement\n",
" df_debate.at[idx, 'deadlock'] = is_deadlock\n",
" df_debate.at[idx, 'needs_hitl'] = is_needs_hitl\n",
" df_debate.at[idx, 'debate_completed'] = True\n",
"\n",
" except Exception as e:\n",
" print(f\"ERROR on row {idx}: {e} — skipping and continuing.\")\n",
" errors.append((idx, str(e)))\n",
" df_debate.at[idx, 'judge_label'] = 0\n",
" df_debate.at[idx, 'judge_confidence'] = \"Low\"\n",
" df_debate.at[idx, 'judge_rationale'] = f\"Error: {str(e)[:100]}\"\n",
" df_debate.at[idx, 'debate_completed'] = True\n",
"\n",
" df_debate.to_csv(FILENAME, index=False)\n",
" print(\"-\" * 60)\n",
"\n",
"print(f\"\\nAll debates completed! Errors: {len(errors)}\")\n",
"if errors:\n",
" print(\"Failed rows:\", [e[0] for e in errors])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Part D: Targeted Human Review & Final Integration\n",
"\n",
"**Exception-Based HITL:** Only deadloack situations where reviewed:\n",
"- The Advocate and Skeptic **disagreed**\n",
"- OR the Judge's confidence is **Low** essentially not able to answer\n",
"\n",
"For all other claims, accept the Judge's decision automatically."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================================\n",
"EXCEPTION-BASED HITL TRIAGE\n",
"==================================================\n",
"Total claims: 100\n",
"Auto-accepted (Judge): 74\n",
"Needs human review: 26\n",
"==================================================\n"
]
}
],
"source": [
"# Identify claims needing human review\n",
"df_debate = pd.read_csv(FILENAME)\n",
"\n",
"# Ensure judge_label is numeric\n",
"df_debate['judge_label'] = pd.to_numeric(df_debate['judge_label'], errors='coerce').fillna(0).astype(int)\n",
"\n",
"# Flag claims that need human review\n",
"needs_review = (\n",
" (df_debate['agents_agree'].str.lower() == 'disagree') |\n",
" (df_debate['judge_confidence'].str.lower() == 'low')\n",
")\n",
"\n",
"# For claims the agents agree on with Medium/High confidence: auto-accept Judge's label\n",
"auto_accepted = ~needs_review\n",
"df_debate.loc[auto_accepted, 'is_green_gold'] = df_debate.loc[auto_accepted, 'judge_label']\n",
"\n",
"n_auto = auto_accepted.sum()\n",
"n_review = needs_review.sum()\n",
"\n",
"print(f\"=\" * 50)\n",
"print(f\"EXCEPTION-BASED HITL TRIAGE\")\n",
"print(f\"=\" * 50)\n",
"print(f\"Total claims: 100\")\n",
"print(f\"Auto-accepted (Judge): {n_auto}\")\n",
"print(f\"Needs human review: {n_review}\")\n",
"print(f\"=\" * 50)\n",
"\n",
"# Save progress\n",
"df_debate.to_csv(FILENAME, index=False)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All claims requiring review have been labeled!\n"
]
}
],
"source": [
"# Human-in-the-Loop review (only disagreements with deadlocks)\n",
"\n",
"def exception_hitl_review():\n",
" df = pd.read_csv(FILENAME)\n",
" df['judge_label'] = pd.to_numeric(df['judge_label'], errors='coerce').fillna(0).astype(int)\n",
" \n",
" # Find rows flagged for review that haven't been labeled yet\n",
" needs_review = (\n",
" (df['agents_agree'].str.lower() == 'disagree') |\n",
" (df['judge_confidence'].str.lower() == 'low')\n",
" )\n",
" \n",
" # Only review rows where is_green_gold is still empty/NaN\n",
" unlabeled = df['is_green_gold'].isna() | (df['is_green_gold'] == \"\")\n",
" review_indices = df[needs_review & unlabeled].index.tolist()\n",
" \n",
" if not review_indices:\n",
" print(\"All claims requiring review have been labeled!\")\n",
" return\n",
" \n",
" print(f\"--- Exception-Based HITL Review ---\")\n",
" print(f\"Claims to review: {len(review_indices)}\")\n",
" print(f\"(Tip: Check debate transcripts in '{TRANSCRIPT_DIR}/' for full context)\")\n",
" print(\"-\" * 50 + \"\\n\")\n",
" \n",
" for count, idx in enumerate(review_indices):\n",
" row = df.loc[idx]\n",
" claim_text = str(row['text'])\n",
" uncertainty = row.get('u', 0.0)\n",
" suggestion = int(row['judge_label'])\n",
" confidence = row.get('judge_confidence', 'Unknown')\n",
" rationale = row.get('judge_rationale', 'No rationale')\n",
" agreement = row.get('agents_agree', 'unknown')\n",
" \n",
" print(f\"\\n[{count+1}/{len(review_indices)}] Row {idx} (Uncertainty: {uncertainty:.4f})\")\n",
" print(f\"REASON FOR REVIEW: Agreement={agreement}, Confidence={confidence}\")\n",
" print(f\"\\nCLAIM: {claim_text[:800]}...\\n\")\n",
" print(f\"JUDGE SAYS: {suggestion} (Confidence: {confidence})\")\n",
" print(f\"RATIONALE: {rationale}\")\n",
" print(f\"(Full debate: {TRANSCRIPT_DIR}/debate_row_{idx}.txt)\\n\")\n",
" \n",
" while True:\n",
" user_input = input(f\"Your Final Label (0/1) [Enter to accept Judge's {suggestion}]: \")\n",
" if user_input.strip() == \"\":\n",
" final_label = suggestion\n",
" break\n",
" if user_input.strip() in ['0', '1']:\n",
" final_label = int(user_input)\n",
" break\n",
" print(\"Please enter 0 or 1, or press Enter to accept.\")\n",
" \n",
" df.at[idx, 'is_green_gold'] = final_label\n",
" df.to_csv(FILENAME, index=False)\n",
" \n",
" override = \"(Override!)\" if final_label != suggestion else \"(Accepted)\"\n",
" print(f\"Saved: {final_label} {override}\")\n",
" print(\"-\" * 50)\n",
" \n",
" print(\"\\nAll exception reviews complete!\")\n",
"\n",
"# Run the HITL review\n",
"exception_hitl_review()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================================\n",
"DISAGREEMENT REPORT\n",
"==================================================\n",
"Total claims: 100\n",
"Auto-accepted (Judge agreed, High/Med): 74\n",
"Required human intervention: 26\n",
"Human overrides of Judge: 3\n",
"Human accepted Judge label: 23\n",
"==================================================\n",
"\n",
"For your report: 'The agents disagreed on 26 out of 100 claims.'\n"
]
}
],
"source": [
"# Disagreement Report\n",
"import pandas as pd\n",
"\n",
"FILENAME = \"hitl_green_100_final.csv\"\n",
"\n",
"df_final = pd.read_csv(FILENAME)\n",
"df_final['judge_label'] = pd.to_numeric(df_final['judge_label'], errors='coerce').fillna(0).astype(int)\n",
"df_final['is_green_gold'] = pd.to_numeric(df_final['is_green_gold'], errors='coerce').fillna(0).astype(int)\n",
"\n",
"needs_review = (\n",
" (df_final['agents_agree'].str.lower() == 'disagree') |\n",
" (df_final['judge_confidence'].str.lower() == 'low')\n",
")\n",
"n_hitl = needs_review.sum()\n",
"\n",
"reviewed_rows = df_final[needs_review]\n",
"overrides = (reviewed_rows['judge_label'] != reviewed_rows['is_green_gold']).sum()\n",
"\n",
"print(\"=\" * 50)\n",
"print(\"DISAGREEMENT REPORT\")\n",
"print(\"=\" * 50)\n",
"print(f\"Total claims: 100\")\n",
"print(f\"Auto-accepted (Judge agreed, High/Med): {100 - n_hitl}\")\n",
"print(f\"Required human intervention: {n_hitl}\")\n",
"print(f\"Human overrides of Judge: {overrides}\")\n",
"print(f\"Human accepted Judge label: {n_hitl - overrides}\")\n",
"print(\"=\" * 50)\n",
"print(f\"\\nFor your report: 'The agents disagreed on {n_hitl} out of 100 claims.'\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Part D (continued): Fine-Tune PatentSBERTa\n",
"\n",
"Using the newly minted Gold dataset (100 human-verified labels), perform a final fine-tuning of PatentSBERTa.\n",
"\n",
"This goes beyond the previous assignments where only used frozen embeddings where used, this assignment actually fine-tunes the transformer weights using contrastive learning."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Reloading df_train_silver from parquet...\n",
"Gold dataset: 100 claims | Green: 62 | Not Green: 38\n",
"Saved 2000 training pairs to sbert_train_pairs.pkl\n"
]
}
],
"source": [
"# Prepare training data (no sentence-transformers import here)\n",
"import pandas as pd\n",
"import numpy as np\n",
"import pickle\n",
"\n",
"if 'df_train_silver' not in dir() or df_train_silver is None:\n",
" print(\"Reloading df_train_silver from parquet...\")\n",
" _df_all = pd.read_parquet(\"patents_50k_green.parquet\")\n",
" _df_eval_tmp = _df_all.sample(n=5000, random_state=42)\n",
" _df_remaining = _df_all.drop(_df_eval_tmp.index)\n",
" df_train_silver = _df_remaining.sample(n=2000, random_state=42)\n",
"\n",
"FILENAME = \"hitl_green_100_final.csv\"\n",
"df_gold = pd.read_csv(FILENAME)\n",
"df_gold['is_green_gold'] = pd.to_numeric(df_gold['is_green_gold'], errors='coerce').fillna(0).astype(int)\n",
"\n",
"print(f\"Gold dataset: {len(df_gold)} claims | Green: {(df_gold['is_green_gold']==1).sum()} | Not Green: {(df_gold['is_green_gold']==0).sum()}\")\n",
"\n",
"green_texts = df_gold[df_gold['is_green_gold'] == 1]['text'].tolist()\n",
"not_green_texts = df_gold[df_gold['is_green_gold'] == 0]['text'].tolist()\n",
"silver_green = df_train_silver[df_train_silver['is_green_silver'] == 1]['text'].tolist()\n",
"silver_not_green = df_train_silver[df_train_silver['is_green_silver'] == 0]['text'].tolist()\n",
"\n",
"all_green = green_texts + silver_green\n",
"all_not_green = not_green_texts + silver_not_green\n",
"\n",
"np.random.seed(42)\n",
"pairs = []\n",
"\n",
"for _ in range(500):\n",
" i, j = np.random.choice(len(all_green), 2, replace=False)\n",
" pairs.append((all_green[i][:512], all_green[j][:512], 1.0))\n",
"\n",
"for _ in range(500):\n",
" i, j = np.random.choice(len(all_not_green), 2, replace=False)\n",
" pairs.append((all_not_green[i][:512], all_not_green[j][:512], 1.0))\n",
"\n",
"for _ in range(1000):\n",
" i = np.random.randint(len(all_green))\n",
" j = np.random.randint(len(all_not_green))\n",
" pairs.append((all_green[i][:512], all_not_green[j][:512], 0.0))\n",
"\n",
"np.random.shuffle(pairs)\n",
"\n",
"# Save as plain tuples (no sentence_transformers dependency)\n",
"with open(\"sbert_train_pairs.pkl\", \"wb\") as f:\n",
" pickle.dump(pairs, f)\n",
"\n",
"print(f\"Saved {len(pairs)} training pairs to sbert_train_pairs.pkl\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"PyTorch version: 2.4.1+cpu\n",
"✓ DirectML available\n",
"✓ AMD GPU device: privateuseone:0\n",
"✓ Test tensor on privateuseone:0: shape torch.Size([10, 10])\n"
]
}
],
"source": [
"import torch\n",
"\n",
"print(\"PyTorch version:\", torch.__version__)\n",
"\n",
"try:\n",
" import torch_directml\n",
" device = torch_directml.device()\n",
" print(f\"✓ DirectML available\")\n",
" print(f\"✓ AMD GPU device: {device}\")\n",
"except ImportError:\n",
" print(\"✗ torch-directml not installed\")\n",
" print(\" Installing now...\")\n",
" import subprocess, sys\n",
" subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"torch-directml\"], check=True)\n",
" import torch_directml\n",
" device = torch_directml.device()\n",
" print(f\"✓ Now ready: {device}\")\n",
"\n",
"# Test it works\n",
"test = torch.randn(10, 10).to(device)\n",
"print(f\"✓ Test tensor on {device}: shape {test.shape}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Starting PatentSBERTa fine-tuning (progress streams live)...\n",
"Success! Model saved to: patentsberta_finetuned_final\n"
]
}
],
"source": [
"# Fine-tune PatentSBERTa via an isolated subprocess\n",
"# finetune_sbert.py handles DirectML (AMD GPU) with CPU fallback.\n",
"import sys, subprocess, os\n",
"\n",
"OUTPUT_DIR = \"patentsberta_finetuned_final\"\n",
"SCRIPT_PATH = \"finetune_sbert.py\"\n",
"\n",
"if os.path.isdir(OUTPUT_DIR):\n",
" print(f\"Fine-tuned model already exists at {OUTPUT_DIR!r}. Skipping.\")\n",
"else:\n",
" print(\"Starting PatentSBERTa fine-tuning (progress streams live)...\")\n",
" result = subprocess.run([sys.executable, \"-u\", SCRIPT_PATH])\n",
" if result.returncode == 0:\n",
" print(\"Success! Model saved to:\", OUTPUT_DIR)\n",
" else:\n",
" print(\"Failed with exit code\", result.returncode)\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Model directory 'patentsberta_finetuned_final/' contains 11 files:\n",
" 1_Pooling 0.0 MB\n",
" config.json 0.0 MB\n",
" config_sentence_transformers.json 0.0 MB\n",
" model.safetensors 417.7 MB\n",
" modules.json 0.0 MB\n",
" README.md 0.0 MB\n",
" sentence_bert_config.json 0.0 MB\n",
" special_tokens_map.json 0.0 MB\n",
" tokenizer.json 0.7 MB\n",
" tokenizer_config.json 0.0 MB\n",
" vocab.txt 0.2 MB\n",
"Fine-tuned PatentSBERTa is ready for evaluation.\n"
]
}
],
"source": [
"# Verify the fine-tuned model was saved correctly\n",
"import os\n",
"\n",
"OUTPUT_DIR = \"patentsberta_finetuned_final\"\n",
"\n",
"if os.path.isdir(OUTPUT_DIR):\n",
" files = os.listdir(OUTPUT_DIR)\n",
" print(f\"Model directory '{OUTPUT_DIR}/' contains {len(files)} files:\")\n",
" for f in files:\n",
" size = os.path.getsize(os.path.join(OUTPUT_DIR, f))\n",
" print(f\" {f:40s} {size/1024/1024:.1f} MB\")\n",
" print(\"Fine-tuned PatentSBERTa is ready for evaluation.\")\n",
"else:\n",
" print(f\"ERROR: Model directory '{OUTPUT_DIR}/' not found.\")\n",
" print(\"Please run cell D5 (above) to fine-tune the model first.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"WARNING:tensorflow:From c:\\Users\\cbnsp\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tf_keras\\src\\losses.py:2976: The name tf.losses.sparse_softmax_cross_entropy is deprecated. Please use tf.compat.v1.losses.sparse_softmax_cross_entropy instead.\n",
"\n",
"Device: privateuseone:0\n",
"Reloading df_eval...\n",
"Generating embeddings with fine-tuned PatentSBERTa...\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "0c032c0e3da641d18c4035172f798be0",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Batches: 0%| | 0/63 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "1e9d11d7d95846a69508f5669e4c1879",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Batches: 0%| | 0/4 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "b782188398354d1d8d34be976a85df16",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Batches: 0%| | 0/157 [00:00, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"FINAL MODEL: Fine-tuned PatentSBERTa + Silver + Gold\n",
"==================================================\n",
" precision recall f1-score support\n",
"\n",
" Not Green 0.75 0.76 0.75 2493\n",
" Green 0.76 0.75 0.75 2507\n",
"\n",
" accuracy 0.75 5000\n",
" macro avg 0.75 0.75 0.75 5000\n",
"weighted avg 0.75 0.75 0.75 5000\n",
"\n",
"Final Macro F1: 0.7530\n",
"==================================================\n"
]
}
],
"source": [
"# Evaluate the fine-tuned PatentSBERTa\n",
"\n",
"import torch\n",
"import pandas as pd\n",
"import numpy as np\n",
"from sentence_transformers import SentenceTransformer\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import classification_report, f1_score\n",
"\n",
"# Reload DEVICE if kernel was restarted\n",
"if 'DEVICE' not in dir():\n",
" try:\n",
" import torch_directml\n",
" DEVICE = torch_directml.device()\n",
" except ImportError:\n",
" DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
" print(f\"Device: {DEVICE}\")\n",
"\n",
"# Reload df_train_silver if kernel was restarted\n",
"if 'df_train_silver' not in dir() or df_train_silver is None:\n",
" print(\"Reloading df_train_silver...\")\n",
" _df_all = pd.read_parquet(\"patents_50k_green.parquet\")\n",
" _df_eval_tmp = _df_all.sample(n=5000, random_state=42)\n",
" _df_remaining = _df_all.drop(_df_eval_tmp.index)\n",
" df_train_silver = _df_remaining.sample(n=2000, random_state=42)\n",
"\n",
"if 'df_eval' not in dir() or df_eval is None:\n",
" print(\"Reloading df_eval...\")\n",
" _df_all = pd.read_parquet(\"patents_50k_green.parquet\")\n",
" df_eval = _df_all.sample(n=5000, random_state=42)\n",
"\n",
"y_eval = df_eval['is_green_silver'].values\n",
"FILENAME = \"hitl_green_100_final.csv\"\n",
"\n",
"# Load the fine-tuned model\n",
"st_finetuned = SentenceTransformer(\"patentsberta_finetuned_final\", device=str(DEVICE))\n",
"\n",
"print(\"Generating embeddings with fine-tuned PatentSBERTa...\")\n",
"\n",
"X_train_ft = st_finetuned.encode(df_train_silver['text'].tolist(), batch_size=32, show_progress_bar=True)\n",
"y_train_ft = df_train_silver['is_green_silver'].values\n",
"\n",
"df_gold = pd.read_csv(FILENAME)\n",
"df_gold['is_green_gold'] = pd.to_numeric(df_gold['is_green_gold'], errors='coerce').fillna(0).astype(int)\n",
"X_gold_ft = st_finetuned.encode(df_gold['text'].tolist(), batch_size=32, show_progress_bar=True)\n",
"y_gold_ft = df_gold['is_green_gold'].values\n",
"\n",
"X_combined = np.vstack([X_train_ft, X_gold_ft])\n",
"y_combined = np.concatenate([y_train_ft, y_gold_ft])\n",
"\n",
"X_eval_ft = st_finetuned.encode(df_eval['text'].tolist(), batch_size=32, show_progress_bar=True)\n",
"\n",
"clf_final = LogisticRegression(max_iter=1000, random_state=42)\n",
"clf_final.fit(X_combined, y_combined)\n",
"\n",
"y_pred_final = clf_final.predict(X_eval_ft)\n",
"f1_final = f1_score(y_eval, y_pred_final, average='macro')\n",
"\n",
"print(\"\\n\" + \"=\" * 50)\n",
"print(\"FINAL MODEL: Fine-tuned PatentSBERTa + Silver + Gold\")\n",
"print(\"=\" * 50)\n",
"print(classification_report(y_eval, y_pred_final, target_names=['Not Green', 'Green']))\n",
"print(f\"Final Macro F1: {f1_final:.4f}\")\n",
"print(\"=\" * 50)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Part E: Performance Comparison & Summary\n",
"\n",
"Compare all four model versions across all assignments."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"FINAL PERFORMANCE COMPARISON TABLE\n",
"======================================================================\n",
"Model Version Training Data Source F1 Score\n",
"----------------------------------------------------------------------\n",
"1. Baseline Frozen Embeddings (No Fine-tuning) 0.7494\n",
"2. Assignment 2 Silver + Gold (GPT-OSS 20B) 0.7465\n",
"3. Assignment 3 Silver + Gold (MAS + HITL) 0.7467\n",
"4. Final Model Silver + Gold (QLoRA MAS + HITL) 0.7530\n",
"======================================================================\n"
]
}
],
"source": [
"# Model comparison table\n",
"\n",
"# Values already computed in previous cells\n",
"f1_baseline = 0.7494 # from part A/Baseline with frozen embeddings and silver data only\n",
"f1_final = 0.7530 # from final cell part D in this assignment\n",
"f1_assignment_2 = 0.7465 # Assignment 2 F1 (Single LLM GPT-oss 20B approach and HITL review)\n",
"f1_assignment_3 = 0.7467 # Assignment 3 F1 (Multiple LLM approach and HITL review)\n",
"\n",
"print(\"=\" * 70)\n",
"print(\"FINAL PERFORMANCE COMPARISON TABLE\")\n",
"print(\"=\" * 70)\n",
"print(f\"{'Model Version':<25} {'Training Data Source':<45} {'F1 Score':>8}\")\n",
"print(\"-\" * 70)\n",
"print(f\"{'1. Baseline':<25} {'Frozen Embeddings (No Fine-tuning)':<45} {f1_baseline:>8.4f}\")\n",
"print(f\"{'2. Assignment 2':<25} {'Silver + Gold (GPT-OSS 20B)':<45} {f1_assignment_2:>8.4f}\")\n",
"print(f\"{'3. Assignment 3':<25} {'Silver + Gold (MAS + HITL)':<45} {f1_assignment_3:>8.4f}\")\n",
"print(f\"{'4. Final Model':<25} {'Silver + Gold (QLoRA MAS + HITL)':<45} {f1_final:>8.4f}\")\n",
"print(\"=\" * 70)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAJOCAYAAACqS2TfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZu5JREFUeJzt3QeYE1X/9vGz9Ca9I4r03osoggVFRYrtQUSagI8FRbEASlcEC00EQSmKqCAINhBFpCko1YJioQmiVOkg4DLvdZ/nnfyTbLKbXXbY7Ob7ua6BzcwkOZnMmczv1DjHcRwDAAAAAABSXabUf0kAAAAAAEDQDQAAAACAh6jpBgAAAADAIwTdAAAAAAB4hKAbAAAAAACPEHQDAAAAAOARgm4AAAAAADxC0A0AAAAAgEcIugEAAAAA8AhBN4B0Iy4uzresWrUq7H7vvvuub78yZcp4ni69h97rXC1dutS+TpcuXSJ+zpVXXhlwXLTkyZPH1KxZ0wwYMMAcOXLEnA9btmwxN998sylcuLDJlCmTTYc+D6LT6tWrfefL0KFD0zo5Ue3++++3x0nn9e+//x52v+3bt9v9lCfPNV/7O3XqlClQoIB9jauvvtqkBfc6o88IAEg+gm4A6dJbb70VdtuMGTNMrGnRooXp3LmzXRo3bmyD4GeeecY0atTIHDx40NP3Pnv2rLntttvM+++/bypUqGDuuusum47ixYt7+r5IuTfffDOivBTrTp8+bWbNmmX/dhwnTY7Vxx9/bA4dOmT/XrZsmfnjjz/OexqQMq+//rotrBg8eDCHEIhxBN0A0pXMmTObGjVq2Bvhf//9N8H2AwcOmIULF5q6deuaWNK3b197g6dl0aJFZuPGjbYG/ueffzbDhg3z9L1V+/Xtt9+aK664wrZAmD59uk1H5cqVPX1fpMyZM2fMzJkz7d8qGPn111/NN998w+EMYcGCBebvv/82JUqUSFBYcb6476k0qIArLQJ/5elNmzaZUqVKnff3BoCMgKAbQLrToUMHs3//fvPpp58m2KZgXEGFaltj2SWXXGKGDBli/1YNtJfcmreyZct6+j5IHSqUUv65/PLLbdPptAom0wP3uCgvlStXzhZirV279ry9vwJ+Bf45c+Y0r732WkCazqeLLrrIFqJlzZr1vL83AGQEBN0A0p0777zTNtkL1Yxc69SnuU2bNom+hm5kr732WttXMkeOHKZSpUq2tthtxhns5MmT5qmnnrLBrPbXDfigQYNs89PEqHZIfTlLly5tsmfPbooVK2buuOMO8+OPPxqv1alTx/6/c+fOgPV63LNnT/sZ9FkKFixobrrpJrNy5coEr+HfH3X37t2me/fu5sILLzRZsmQxY8aMsduaNWtm933jjTd8/YSD+7Um53irKaZeQ7Xl6nustBUqVMiuU426f5r27t1runXrZmtsc+fObZo0aRLwOSZOnGj7tyto0Xeg11ZtYbAVK1bYY6J9lUbtryAjXBr906DA6L777rM1kfqOq1evbqZOnRr2e9Hxf+ihh0zFihXt++j4169f3wZ2wX3w1aT5nXfesX153WNXpUoV+zlOnDhhUsLNNyqYcgun3MKqxM5jHWe1ntBnLFq0qA3aX3zxxYAWJ/59f99++21z6aWXmgsuuMDkz5/ft4/S/fTTT9vjpM+fL18+07RpU1/te7B9+/bZ76Fq1ao2b2t/HbtOnTrZ88Of+lzru9D2XLly2WNbrVo189///tf88ssvyTpO+t7nz59vP+9//vMfW9h3voNe93tp3bq1ufHGG+3x17Vjw4YNIffX9WjChAmmQYMGNs/oGOg5ykPBx/fYsWNm+PDhplatWvaY6tjqmnD77bcnKNBMrE+3mrzr/NT3rHNU6VTBRLim1f6vpQJBnSPKu/qu2rdvH7L5vPKZO07E559/bs8XvZ/Owx49epjDhw/b/XQ90HetGnnllYYNGyY6toRaeOjzKu9my5bNXtt0jduxY0ei16UffvjBfif6vEq7roHB1099zq5du9q/lbf9x93Qa7j0vLZt25qLL77Ynmu6lindOuf1HQHIIBwASCd0ycqcObP9u1mzZk6uXLmco0eP+rZv2bLF7tOxY0fnr7/+sn9ffPHFCV7n2WeftduyZMniXHPNNU67du2cCy+80K6rWLGis3v37oD9T5065VxxxRV2e4ECBZxbbrnFadmypZMzZ06nVatWzkUXXWS3BZs3b56TPXt2u6127drObbfd5jRq1MiJi4uzaV+2bFnA/kuWLLH7du7cOeJjouOg5+i5wb766iu77YILLvCtW7lypf0MWl+pUiX7WfTZdCx0bGfOnBkyTTfeeKM9RsWLF7ef46abbnImTZpk09qiRQu7T7ly5exjLcOHD0/x8R40aJDd1rVr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xowxx48f941m3qlTJ1OqVCnbN1t69eplmjVrZkaOHGlatmxpZs6cadauXWteffXVNP4kAAAAAIBYF3VBd7t27cy+ffvMwIED7WBotWvXNgsXLvQNlrZjxw6TKdP/VdBfdtll5u233zb9+/c3Tz75pKlQoYIdubx69epp+ClwPqibgEaqD+4uACBtkTeB6ETeBKITeTPji6opwwAAAAAAyEiiqk83AAAAAAAZCUE3AAAAAAAeIegGAAAAAMAjBN2ISUuXLjVxcXHm0KFD9vHrr7/OfO0AAACIinvT1KLX1CDTSFsE3YjY8uXLTatWrUzJkiXPKQPrue6SJUsWc9FFF9mp4k6dOpWmo+b/+uuvafb+gFc0vWKDBg3MBRdcYIoWLWratm1rfvnllxS/XosWLUzmzJnNmjVrTFrfnLRp08aUKFHC5M6d28508dZbb6VpmoDkeOWVV0zNmjVN3rx57dK4cWPzySefpPu8qevLVVddZWedyZEjhylbtqydYebMmTNpmi4gNY0YMcLexz788MPJet727dsD7oPd5a677rIzMv31118mX758fFkZEEE3Iqb50mvVqmXGjx9/zkdt2rRp9sKybds2M2HCBPPmm2+aZ555Js2+jZw5c9qABMholi1bZh544AHz9ddfm0WLFtkb3+uuu87m5+TSlI0rV640PXv2NFOnTjVpSelQwPLee++Z77//3nTt2tV06tTJfPzxx2maLiBSF154ob1xX7dunVm7dq25+uqrbUHSjz/+mK7zZtasWW1e/Oyzz2wAPmbMGPPaa6/ZKT6BjEAFW5MmTbK/QSn1+eef2/tgd9G9dbZs2Uzx4sVtEI4MSFOGAcmlU2fevHmp9txu3bo5N954o+/x5s2bndatWztFixZ1cufO7dSvX99ZtGhRwHPGjx/vlC9f3smePbvd79Zbb/Vti4+Pd5599lmnTJkyTo4cOZyaNWs6s2fP9m1fsmSJTcfBgwft42nTpjn58uXzbR80aJBTq1YtZ/r06c7FF1/s5M2b12nXrp1z5MiRiN8DiEZ79+615/6yZcuS/dzBgwc7d9xxh7Np0yabX06cOBGwXed/9erVbX4oWLCgc8011zjHjh3z5bkGDRo4uXLlss+97LLLnO3bt0ec3yKha0jXrl2T/bmAaFGgQAFn8uTJGS5vPvLII06TJk2S/bmAaHP06FGnQoUK9p60WbNmTq9evZL1/G3bttnf4A0bNiTYFu7edOHChU7lypXt/XCLFi2cP//80/ec1atXO82bN3cKFSpk82fTpk2ddevWpdo9O1IPNd1Ic2rW/cUXX5hGjRr51h07dszceOONZvHixWbDhg3m+uuvt03bVZovqhV46KGHzNChQ21J+sKFC03Tpk0DmtROnz7dTJw40dYaPPLII7bpjmr9IrVlyxbbhF41Z1r0XNVKpOZ7AOfb4cOH7f8FCxZM1vP0u60WKjrHK1eubMqXL2/mzJnj266S+vbt25u7777bbNq0yTb/vuWWW+zz/v33X9usvVmzZrZWetWqVeaee+4JKM1PKr9F+tmS+7mAaBAfH29mzpxpW6ComXlGypubN2+2v9F6DyC9U8uxli1bmubNm5+X9ztx4oR58cUXbYtQdfPUffBjjz3m23706FHTuXNn8+WXX9oWbRUqVLD3z1qPKJOKATxiyLnWdKu0XSV2qqXW45tuusk5ffp0os+rVq2aM27cOPv3e++9Z0v0QpW2//PPP7bEfuXKlQlq09u3bx9xTbdew//1H3/8cadRo0YRvwcQbdQ6o2XLls7ll1+e7Od+9tlnTpEiRZwzZ87Yx6NHj7al/C6VrCtPuTVk/g4cOGC3LV26NORrJ5XfIjFr1iwnW7ZszsaNG5P5yYC08/3339vfwsyZM9vfoPnz52eYvNm4cWPfb/w999xjrz9AevbOO+/YFiMnT560j8+lpjtnzpw277vL+vXrQ96b6rFaf/q38ixWrFjY11c+u+CCC5yPPvrIt46a7uhATTfSxOjRo823335rvvvuO1tyrtrujh07BtR0qySvSpUqdlTxPHny2BJ6t6b72muvNRdffLEdoEXP0wBKKg10S9X1t/bR89xFtdIqsY9UmTJl7OBTLg3YtHfv3lR9D+B8l9Bv3LjR1qgll/qJasBBDX4oqjn76quvfOe7xnu45pprTI0aNcztt99u+3AePHjQblPtc5cuXexAT2qxMnbsWFv7Fml+S8qSJUtsn269Z7Vq1ZL92YC0UqlSJftb+M0335j77rvP1lj99NNPGSJvzpo1y6xfv968/fbbZv78+ba2Dkivdu7caXr16mXvNzVA4LlS/lDed5eqVauG3C9XrlymXLlyYfPfnj17TI8ePWwNtwZg06CMuod275cRPQi6kSY0UISa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",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Chart saved as 'final_comparison_chart.png'\n"
]
}
],
"source": [
"# VISUAL COMPARISON CHART\n",
"import matplotlib.pyplot as plt\n",
"\n",
"models = ['1. Baseline', '2. Assn 2\\n(GPT-OSS 20B)', '3. Assn 3\\n(MAS+HITL)', '4. Final\\n(QLoRA + MAS+HITL)']\n",
"scores = [f1_baseline, f1_assignment_2, f1_assignment_3, f1_final]\n",
"colors = ['#95a5a6', '#e67e22', '#27ae60', '#2980b9']\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 6))\n",
"bars = ax.bar(models, scores, color=colors, width=0.6, edgecolor='white', linewidth=1.5)\n",
"\n",
"# Add value labels on bars\n",
"for bar, score in zip(bars, scores):\n",
" ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,\n",
" f'{score:.4f}', ha='center', va='bottom', fontsize=12, fontweight='bold')\n",
"\n",
"ax.set_ylabel('Macro F1 Score', fontsize=13)\n",
"ax.set_title('Model Performance Across All Assignments', fontsize=15, pad=15)\n",
"ax.set_ylim(0, 1.0)\n",
"ax.spines['top'].set_visible(False)\n",
"ax.spines['right'].set_visible(False)\n",
"\n",
"fig.tight_layout()\n",
"fig.savefig('final_comparison_chart.png', dpi=300, bbox_inches='tight')\n",
"plt.show()\n",
"print(\"Chart saved as 'final_comparison_chart.png'\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# SAVE FINAL MODEL FOR HUGGING FACE\n",
"import joblib\n",
"\n",
"# Save the classifier\n",
"joblib.dump(clf_final, \"final_classifier.joblib\")\n",
"print(\"Classifier saved: final_classifier.joblib\")\n"
]
},
{
"attachments": {
"image.png": {
"image/png": "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"
}
},
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"# Summary\n",
"\n",
"This notebook implements the full pipeline required for the Final Assignment:\n",
"\n",
"| Step | What | How | Key Details |\n",
"|------|------|-----|-------------|\n",
"| Part A&B | Setup & baseline | Same splits as Assignments 2&3 | train_silver=2000, eval=5000, top-100 high-risk claims (u ≥ 0.9965) |\n",
"| Part C Step 1 | QLoRA fine-tuning | **Qwen3-8B** (bnb-4bit) via Unsloth on Google Colab T4 | r=16, α=16, 3 epochs, 2000 Alpaca-format examples, loss=0.8899, ~105 min |\n",
"| Part C Step 2 | Multi-Agent System (MAS) | Advocate + Skeptic (Qwen3-4B) + Judge (QLoRA model) via LM Studio | 2-round debate per claim; Judge uses fine-tuned QLoRA model as brain |\n",
"| Part D | Exception-Based HITL | Only reviewed deadlocks (agents_agree==disagree) or low confidence | 74 auto-accepted, 26 human-reviewed, 3 human overrides of Judge |\n",
"| Part D | Fine-tune PatentSBERTa | Contrastive learning on Silver+Gold pairs | 2000 pairs, 3 epochs, CosineSimilarityLoss, ~30 min on CPU |\n",
"| Part E | Model comparison | F1 across all 4 model versions | Baseline: 0.7494 → Final: 0.7530 (+0.0036 improvement) |\n",
"\n",
"## QLoRA Model Details (Part C Step 1)\n",
"- **Base model:** (8.2B parameters, 4-bit quantized)\n",
"- **Trainable parameters:** 43,646,976 / 8,234,382,336 (0.53% — LoRA adapters only)\n",
"- **Training data:** 2,000 patent claims in Alpaca instruction format, labelled with Y02 silver labels\n",
"- **Export:** Saved as LoRA adapter + GGUF Q4_K_M (4.682 GB) for local inference via LM Studio\n",
"\n",
"## MAS Architecture (Part C Step 2)\n",
"- **Advocate** (): Argues FOR Y02 green classification\n",
"- **Skeptic** (): Argues AGAINST (greenwashing detection)\n",
"- **Judge** ( — the fine-tuned Qwen3-8B): Weighs 2 rounds of debate, outputs JSON \n",
"- All agents served locally via **LM Studio** (Network API)\n",
"\n",
"## Final results\n",
"\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.12.10"
}
},
"nbformat": 4,
"nbformat_minor": 4
}