<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Raihan</title>
    <description>The latest articles on Forem by Raihan (@raihan-js).</description>
    <link>https://forem.com/raihan-js</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F254365%2Fb923a1bc-0077-4742-bafd-fff5c5d9b555.png</url>
      <title>Forem: Raihan</title>
      <link>https://forem.com/raihan-js</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/raihan-js"/>
    <language>en</language>
    <item>
      <title>I built the first open benchmark for federal contracting AI. Here's what it shows about frontier LLMs.</title>
      <dc:creator>Raihan</dc:creator>
      <pubDate>Tue, 12 May 2026 12:16:46 +0000</pubDate>
      <link>https://forem.com/raihan-js/i-built-the-first-open-benchmark-for-federal-contracting-ai-heres-what-it-shows-about-frontier-5a03</link>
      <guid>https://forem.com/raihan-js/i-built-the-first-open-benchmark-for-federal-contracting-ai-heres-what-it-shows-about-frontier-5a03</guid>
      <description>&lt;p&gt;If you ask GPT-4o or Claude to extract Federal Acquisition Regulation clause numbers from a federal solicitation, a non-trivial fraction of the time they will hand you a number that does not exist. There is no &lt;code&gt;FAR 52.999-99&lt;/code&gt;. The model just made it up. For a federal contractor staffing a proposal, that is the difference between a clean compliance matrix and a rejected bid.&lt;/p&gt;

&lt;p&gt;I went looking for a benchmark that measured this. There isn't one. Commercial tools in the space — Capture2Proposal, GovTribe, GovWin, OrangeSlices — all do natural-language processing on federal solicitations, but none publish benchmarks. Academic work on RFP processing is narrow and one-off. GSA's own &lt;code&gt;srt-fbo-scraper&lt;/code&gt; covers only Section 508 compliance.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  FedProc-Bench
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/raihan-js/fedproc-bench" rel="noopener noreferrer"&gt;FedProc-Bench&lt;/a&gt; is a multi-task benchmark for federal procurement NLP. Four tasks, drawn from real federal contracting sources:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Notice type classification&lt;/td&gt;
&lt;td&gt;Eight SAM.gov notice-type buckets — Solicitation, Combined Synopsis/Solicitation, Sources Sought, and so on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;NAICS sector prediction&lt;/td&gt;
&lt;td&gt;Twenty top-level NAICS sectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Set-aside identification&lt;/td&gt;
&lt;td&gt;Multi-label across SBA, SDVOSB, WOSB, EDWOSB, 8(a), HUBZone, and SDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;FAR / DFARS clause extraction&lt;/td&gt;
&lt;td&gt;Token-level entity recognition on canonical clause numbers like &lt;code&gt;52.219-9&lt;/code&gt; or &lt;code&gt;252.225-7042&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Task 4 is the headline. It is the task where frontier LLMs visibly fail.&lt;/p&gt;

&lt;p&gt;The data sources are public and free. SAM.gov provides the solicitations themselves through its Opportunities API. The Electronic Code of Federal Regulations gives me Title 48 — the full FAR and DFARS — as structured XML, which I parse down to 1,032 individual clause records. Claude Haiku fills in a small amount of synthetic augmentation for rare set-aside types like HUBZone and EDWOSB that real SAM data barely contains. Every record carries a &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;label_origin&lt;/code&gt; field, so anyone can audit the provenance line by line.&lt;/p&gt;

&lt;p&gt;The v0 release ships 1,615 records split 1,129 / 243 / 243 train / val / test. That is small. I had originally targeted 10,000. We will come back to why I have less in the section on what bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;The companion model — &lt;a href="https://huggingface.co/raihan-js/fedproc-180m-v0" rel="noopener noreferrer"&gt;&lt;code&gt;raihan-js/fedproc-180m-v0&lt;/code&gt;&lt;/a&gt; — is a 149-million-parameter ModernBERT-base with one shared encoder and four task heads. Sequence classification heads for tasks 1 and 2 (softmax over the label set), seven sigmoid heads for the multi-label set-aside task, and a per-token BIO head for the FAR-clause extractor.&lt;/p&gt;

&lt;p&gt;The interesting design choice is the &lt;strong&gt;task mask&lt;/strong&gt;. Records from different sources contribute different supervision: SAM metadata contributes tasks 1, 2, and 3; raw FAR clause text contributes task 4; synthetic excerpts contribute all four. Inside the model's forward pass, a per-record four-boolean mask says which heads get gradient for each example. That is how a single model trains jointly on heterogeneous sources without diluting any head's signal.&lt;/p&gt;

&lt;p&gt;Training takes 4.3 minutes on a single RTX 3060 for six epochs. The whole training run cost me zero dollars and the electricity to keep my desk lamp on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I compared against
&lt;/h2&gt;

&lt;p&gt;I ran the same four tasks through three frontier systems and the trained model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Claude Sonnet 4.6 (Anthropic)&lt;/li&gt;
&lt;li&gt;GPT-4o (OpenAI)&lt;/li&gt;
&lt;li&gt;Claude Haiku 4.5 (Anthropic)&lt;/li&gt;
&lt;li&gt;FedProc-180M v0 (the model I trained)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each system gets the same prompt and the same test split. For task 4, a clean canonical metric: entity F1 with exact match on clause-number strings, plus a &lt;strong&gt;hallucination rate&lt;/strong&gt;, which I define as the share of predicted clause numbers that do not appear anywhere in the cached real FAR + DFARS corpus. Inventing a number that does not exist is the failure mode that matters here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline results
&lt;/h2&gt;

&lt;p&gt;Aggregate scores across all four tasks (mean of per-task macro-F1; task 4 is entity F1):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Aggregate&lt;/th&gt;
&lt;th&gt;T4 entity F1&lt;/th&gt;
&lt;th&gt;T4 hallucination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;0.911&lt;/td&gt;
&lt;td&gt;0.991&lt;/td&gt;
&lt;td&gt;0.0% (0 / 493)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;0.896&lt;/td&gt;
&lt;td&gt;0.970&lt;/td&gt;
&lt;td&gt;4.4% (23 / 517)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;0.851&lt;/td&gt;
&lt;td&gt;0.916&lt;/td&gt;
&lt;td&gt;15.0% (88 / 587)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;FedProc-180M v0&lt;/td&gt;
&lt;td&gt;0.497&lt;/td&gt;
&lt;td&gt;0.921&lt;/td&gt;
&lt;td&gt;5.5% (26 / 473)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude Sonnet 4.6 is genuinely impressive — zero invented clauses across 493 predicted spans. GPT-4o is close behind. The compact model places fourth on the aggregate because tasks 2 and 3 are weak in v0 (more on this in a moment), but on task 4 — the headline task — it is right behind GPT-4o and roughly matches Claude Haiku on F1 while inventing about a third as many fake clauses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest read
&lt;/h2&gt;

&lt;p&gt;Before anyone runs away with that table: 65 of the 220 task-4 test records are Claude-generated synthetic excerpts that cite specific pinned clauses. Frontier models from the Claude family are being graded on text their own family wrote. That is a real bias.&lt;/p&gt;

&lt;p&gt;The way I disclose this in the benchmark is to break out task 4 by record source. The real-FAR slice is the honest read because no system in this comparison helped author it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Real FAR text — F1&lt;/th&gt;
&lt;th&gt;Real FAR text — hallucination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;0.984&lt;/td&gt;
&lt;td&gt;0.0% (0 / 182)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;0.937&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;11.0%&lt;/strong&gt; (23 / 209)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;0.804&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;32.1%&lt;/strong&gt; (88 / 274)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FedProc-180M v0&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;13.8%&lt;/strong&gt; (22 / 159)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So on the cleanest available slice, Claude Sonnet 4.6 still wins outright. GPT-4o is solid but invents a clause number more than one in ten times. Claude Haiku 4.5 invents a clause number almost a third of the time. And FedProc-180M, the compact specialized model, matches Haiku on F1 with &lt;strong&gt;less than half&lt;/strong&gt; the hallucination rate.&lt;/p&gt;

&lt;p&gt;That last bullet is the v0 takeaway: a 150M-parameter model trained in four minutes on a consumer GPU produces task-specific extraction that is competitive with Claude Haiku and demonstrably more reliable on the failure mode that matters for the use case. At roughly fifty times lower latency and three orders of magnitude lower per-call cost, that is a real Pareto point for federal contractors who want on-prem, predictable, auditable FAR-clause extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is actually weak
&lt;/h2&gt;

&lt;p&gt;I am not going to oversell the rest of the table. Tasks 1, 2, 3 are limited in v0 because the SAM.gov daily quota on a non-federal API key ran out during the data pull before I could fetch description text for the cached solicitations. The model sees only titles like &lt;code&gt;53--O-RING&lt;/code&gt; for task-2 NAICS prediction. That is essentially impossible. v0.1, once the quota window cycles, will retrain on the full description text and these numbers should move substantially.&lt;/p&gt;

&lt;p&gt;The other honest caveat: 1,129 training records is tiny by NLP standards. The fact that ModernBERT-base lifts task 4 to 0.921 F1 on this little data is partly attributable to ModernBERT being a genuinely strong base model, and partly to the fact that the FAR-clause-number pattern is fundamentally structural — it is easier to learn &lt;code&gt;52.&amp;lt;digits&amp;gt;-&amp;lt;digits&amp;gt;&lt;/code&gt; than to learn what makes a notice an RFI versus a Sources Sought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I am a co-founder at &lt;a href="https://vetrproposal.com" rel="noopener noreferrer"&gt;VETR Proposal&lt;/a&gt;, which builds AI-assisted federal proposal management for SDVOSB, WOSB, and 8(a) contractors. Reliable FAR clause handling is core to that product. Before I shipped anything to a customer that touches clause citation, I wanted to know how often current AI systems make things up. There was no public answer. So I made the measurement public.&lt;/p&gt;

&lt;p&gt;That is the other reason the benchmark and dataset are open: anyone working in this space — competitors, GSA, academic groups, internal teams at large contractors — can now use the same yardstick. The benchmark is the contribution. The model is just one entry on the leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The model: &lt;a href="https://huggingface.co/raihan-js/fedproc-180m-v0" rel="noopener noreferrer"&gt;&lt;code&gt;raihan-js/fedproc-180m-v0&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The dataset: &lt;a href="https://huggingface.co/datasets/raihan-js/fedproc-bench" rel="noopener noreferrer"&gt;&lt;code&gt;raihan-js/fedproc-bench&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both are Apache 2.0. The source code that built them lives in the repo (link in the model card). To reproduce from scratch you need a SAM.gov developer key (free), an Anthropic key for the synthetic step, and a couple of hours on a GPU you already have.&lt;/p&gt;

&lt;p&gt;If you find a clause number my model misses, an obvious bug, or a hallucination my regex did not catch — open an issue. v0.1 lands tomorrow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build in federal contracting tech or care about the reliability of LLMs on regulated text, let me know what you find when you run it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>nlp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Three small models for healthcare intake — and what shipping all three taught me</title>
      <dc:creator>Raihan</dc:creator>
      <pubDate>Tue, 12 May 2026 01:20:37 +0000</pubDate>
      <link>https://forem.com/raihan-js/three-small-models-for-healthcare-intake-and-what-shipping-all-three-taught-me-71l</link>
      <guid>https://forem.com/raihan-js/three-small-models-for-healthcare-intake-and-what-shipping-all-three-taught-me-71l</guid>
      <description>&lt;p&gt;Two months ago I started a portfolio project: build three small specialized language models for healthcare practice intake, benchmark each one honestly against frontier APIs, and write about what I learned. The goal was to build the case that small specialized models still have a place in the 2026 toolkit alongside frontier LLMs — not as replacements, but as the first stage of a hybrid pipeline.&lt;/p&gt;

&lt;p&gt;This is the post about the third model. It's also the post about the suite — what worked across all three, what didn't, and the pattern that emerged.&lt;/p&gt;

&lt;p&gt;The three models, all on Hugging Face:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huggingface.co/raihan-js/clarioscope-intent-deberta-v1" rel="noopener noreferrer"&gt;&lt;code&gt;clarioscope-intent-deberta-v1&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — 184M DeBERTa-v3-base, 7-class intent classification. Within 4 pp of Claude Haiku 4.5, 22× faster on CPU. &lt;a href="https://dev.to/ryandevv/matching-frontier-llms-at-22x-lower-latency-a-184m-parameter-intent-classifier-for-healthcare-text-5ec2"&gt;methodology post →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huggingface.co/raihan-js/clarioscope-phi-deberta-v1" rel="noopener noreferrer"&gt;&lt;code&gt;clarioscope-phi-deberta-v1&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — 125M RoBERTa-base, 18-category PHI span detection (HIPAA Safe Harbor). Loses on aggregate but &lt;strong&gt;triples frontier F1 on geographic locations&lt;/strong&gt;. &lt;a href="https://dev.to/raihan-js/where-small-models-beat-frontier-llms-and-where-they-dont-a-125m-phi-detector-4edb"&gt;methodology post →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huggingface.co/raihan-js/clarioscope-insurance-v1" rel="noopener noreferrer"&gt;&lt;code&gt;clarioscope-insurance-v1&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — 125M RoBERTa-base, 12-field insurance / billing extraction. This post.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the third model does
&lt;/h2&gt;

&lt;p&gt;A 125M-parameter RoBERTa fine-tune that extracts twelve insurance/billing fields from patient text: carrier name, plan type, member ID, group number, policy number, subscriber name, relationship, claim ID, prior-auth number, copay, deductible, and billed amount. Output is BIO-tagged token spans, which downstream code converts into a JSON object a billing system can ingest directly.&lt;/p&gt;

&lt;p&gt;The benchmark, on a 672-example held-out test set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Macro F1&lt;/th&gt;
&lt;th&gt;Weighted F1&lt;/th&gt;
&lt;th&gt;Latency / example&lt;/th&gt;
&lt;th&gt;Cost / 1K inferences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;clarioscope-insurance-v1&lt;/code&gt; (CPU)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.7882&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.8202&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-2024-11-20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.9562&lt;/td&gt;
&lt;td&gt;0.9572&lt;/td&gt;
&lt;td&gt;1202 ms&lt;/td&gt;
&lt;td&gt;$1.90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same speed/cost shape as the other two models in the suite: ~26× faster than GPT-4o, $0 marginal cost. The accuracy gap is concentrated in a small number of low-frequency fields.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-insurance-v1%2Fresolve%2Fmain%2Fper_entity_f1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-insurance-v1%2Fresolve%2Fmain%2Fper_entity_f1.png" alt="Per-entity F1: insurance extractor vs GPT-4o" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-tune is competitive on the high-volume fields.&lt;/strong&gt; &lt;code&gt;CLAIM_ID&lt;/code&gt; (0.95 vs 1.00), &lt;code&gt;MEMBER_ID&lt;/code&gt; (0.91 vs 0.99), &lt;code&gt;CARRIER&lt;/code&gt; (0.91 vs 0.96), &lt;code&gt;SUBSCRIBER_NAME&lt;/code&gt; (0.89 vs 0.91 — essentially tied). These four fields collectively cover ~70% of the test entities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap is concentrated in a few low-volume fields.&lt;/strong&gt; &lt;code&gt;AUTH_NUMBER&lt;/code&gt; is the standout weakness: 0.30 vs 0.99. The training set has only 770 AUTH_NUMBER spans and the format space is wide (&lt;code&gt;PA-4421&lt;/code&gt;, &lt;code&gt;auth #998-2210&lt;/code&gt;, &lt;code&gt;AUTH998212&lt;/code&gt;, etc.). Same structured-ID problem as the PHI detector had with &lt;code&gt;MRN&lt;/code&gt;. &lt;code&gt;PLAN_TYPE&lt;/code&gt; is similar: short strings like "PPO", "HMO" with overloaded surface forms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three patterns that repeated across all three models
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Synthetic data is fast and noisy, and the noise is systematic
&lt;/h3&gt;

&lt;p&gt;In all three models, &lt;code&gt;gpt-4o-mini&lt;/code&gt; produced label-noise patterns I had to discover and fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intent classifier (Model 1):&lt;/strong&gt; the LLM over-fitted to "ChatGPT-polite" message style on first attempts. Fixed by adding a mandatory realism mix (40/40/20 polished / casual / messy) to the generation prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHI detector (Model 2):&lt;/strong&gt; the LLM included cue words in entity spans — &lt;code&gt;"MRN 8472301"&lt;/code&gt; annotated as the MRN span instead of &lt;code&gt;"8472301"&lt;/code&gt;. About 8.6% of training spans had this contamination. Fixed by &lt;code&gt;clean_data.py&lt;/code&gt; (cue-word stripping + re-locating spans).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insurance extractor (Model 3):&lt;/strong&gt; same cue-word noise pattern as PHI — &lt;code&gt;"member ID AET-998-2210"&lt;/code&gt; instead of &lt;code&gt;"AET-998-2210"&lt;/code&gt;, &lt;code&gt;"copay $35"&lt;/code&gt; instead of &lt;code&gt;"$35"&lt;/code&gt;. 7.4% of spans needed cleanup. Same fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: when synthetic data is the input, label QA is part of the pipeline. The LLM that generates the annotations does not produce ground truth, it produces a draft that humans (or scripts) need to validate. The version of &lt;code&gt;clean_data.py&lt;/code&gt; that I shipped for Models 2 and 3 is now part of every future synthetic NER project I'll build.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cross-generator test sets are not optional — and val numbers lie
&lt;/h3&gt;

&lt;p&gt;In all three models, val macro F1 was 5–17 percentage points higher than test macro F1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Val macro F1&lt;/th&gt;
&lt;th&gt;Test macro F1&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent classifier&lt;/td&gt;
&lt;td&gt;0.886&lt;/td&gt;
&lt;td&gt;0.911&lt;/td&gt;
&lt;td&gt;-0.025 (test actually higher)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHI detector&lt;/td&gt;
&lt;td&gt;0.863&lt;/td&gt;
&lt;td&gt;0.630&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.233&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insurance extractor&lt;/td&gt;
&lt;td&gt;0.957&lt;/td&gt;
&lt;td&gt;0.788&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.169&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The intent classifier was the exception — classification with 7 categories is more robust than span extraction with 12+ categories. For both span-extraction models, the val numbers from a same-generator split would have produced overconfident model cards.&lt;/p&gt;

&lt;p&gt;Lesson: same-generator val splits are useful for early development feedback, but the headline number that goes on a model card should be from a held-out set generated by a different model with a different prompt style. Otherwise the benchmark inflates and you'll be surprised in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Small models beat frontier on linguistic entities, lose on structured-ID memorization
&lt;/h3&gt;

&lt;p&gt;This pattern showed up clearest in the PHI detector and was the central observation in that model's writeup. The insurance extractor repeats it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linguistic + bounded vocabulary fields&lt;/strong&gt; (CARRIER from a short list of insurance companies, CLAIM_ID with predictable claim patterns, SUBSCRIBER_NAME using ordinary names): fine-tune is competitive or tied with GPT-4o.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured-ID fields with high format variance&lt;/strong&gt; (AUTH_NUMBER, PLAN_TYPE token boundaries, GROUP_NUMBER formats that vary widely): frontier wins because they've seen far more format variance during pretraining.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For both Model 2 and Model 3, the production recommendation is the same: hybrid pipeline. Fine-tuned model first, regex for highly-structured patterns, frontier API as the fallback for the long tail. Most of the cost and latency comes from the fine-tune; the frontier API runs on a small fraction of traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each model cost to build, total
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;OpenAI (data gen)&lt;/th&gt;
&lt;th&gt;RunPod (train)&lt;/th&gt;
&lt;th&gt;Benchmark APIs&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent classifier&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;$1.78&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHI detector&lt;/td&gt;
&lt;td&gt;$1.40&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$5.20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$8.10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insurance extractor&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;td&gt;$1.10 (no Anthropic)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.40&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Suite total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$8.08&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$15.70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three published models with benchmark-grade write-ups for under sixteen dollars. The Anthropic credit gap in the insurance extractor benchmark is the only thing that prevents a clean head-to-head across all three, and that's just a "buy more credit" problem.&lt;/p&gt;

&lt;p&gt;Hugging Face hosting: $0. Total infrastructure cost beyond the line items above: $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;To educate why SLM matters! The pivot story has three components: (1) I can train transformers from scratch on consumer hardware (the &lt;a href="https://huggingface.co/raihan-js" rel="noopener noreferrer"&gt;ORCH series&lt;/a&gt;), (2) I can fine-tune larger base models with QLoRA (&lt;a href="https://huggingface.co/raihan-js/orch-7b" rel="noopener noreferrer"&gt;ORCH-7B&lt;/a&gt;), and (3) I can ship benchmark-grade specialized SLMs with rigorous, transparent evaluation against frontier APIs.&lt;/p&gt;

&lt;p&gt;The ClarioScope SLM Suite is the third leg. Three months of work, three published models, three dev.to write-ups, full transparency on synthetic-data limitations and where frontier still beats us. If you're hiring for AI engineering roles where the candidate needs to understand both training-from-scratch AND production benchmarking AND honest model-limitations communication, my LinkedIn is in my &lt;a href="https://github.com/raihan-js" rel="noopener noreferrer"&gt;GitHub profile&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A v1.1 of the insurance extractor with Anthropic benchmarks once credit is restored, and a v2 of all three models trained on real (de-identified) patient text from a partner practice — which moves the project into HIPAA-eligible infrastructure (AWS SageMaker / Azure ML with a BAA) and out of the "synthetic-data v1" phase.&lt;/p&gt;

&lt;p&gt;If you've shipped a small specialized model that has the inverse story — beats frontier on aggregate but loses on a specific axis — I'd love to hear about it. The interesting trade-offs in 2026 aren't "should I use a frontier API" but "what's the right hybrid architecture for this task." This three-model suite was the project that taught me that lesson, three times in a row.&lt;/p&gt;

&lt;p&gt;Follow along on &lt;a href="https://huggingface.co/raihan-js" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; or &lt;a href="https://github.com/raihan-js" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>healthcare</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Where small models beat frontier LLMs (and where they don't): a 125M PHI detector</title>
      <dc:creator>Raihan</dc:creator>
      <pubDate>Tue, 12 May 2026 00:10:50 +0000</pubDate>
      <link>https://forem.com/raihan-js/where-small-models-beat-frontier-llms-and-where-they-dont-a-125m-phi-detector-4edb</link>
      <guid>https://forem.com/raihan-js/where-small-models-beat-frontier-llms-and-where-they-dont-a-125m-phi-detector-4edb</guid>
      <description>&lt;p&gt;Last month I published a 184M-parameter intent classifier that matches frontier LLMs at 22× lower latency. The story was clean: small specialized model, narrow task, comparable accuracy, much faster, almost free per inference. People liked it.&lt;/p&gt;

&lt;p&gt;The second model in the ClarioScope SLM Suite tells a more complicated story. It's a PHI detector — a token classifier that tags spans of protected health information in inbound patient text across all 18 HIPAA Safe Harbor identifier categories. On the macro-F1 headline number, it loses to Claude Sonnet 4.6: &lt;strong&gt;0.63 vs 0.89&lt;/strong&gt;. On Claude Haiku 4.5: 0.63 vs 0.85. On GPT-4o: 0.63 vs 0.81.&lt;/p&gt;

&lt;p&gt;So the click-through headline isn't "matches frontier." It's: &lt;strong&gt;on aggregate, frontier wins&lt;/strong&gt;. But the macro number hides what's actually happening, and the per-entity breakdown reveals something more interesting than either "small model wins" or "small model loses."&lt;/p&gt;

&lt;p&gt;Model on Hugging Face: &lt;a href="https://huggingface.co/raihan-js/clarioscope-phi-deberta-v1" rel="noopener noreferrer"&gt;&lt;code&gt;raihan-js/clarioscope-phi-deberta-v1&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-phi-deberta-v1%2Fresolve%2Fmain%2Fper_entity_f1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-phi-deberta-v1%2Fresolve%2Fmain%2Fper_entity_f1.png" alt="Per-entity F1 across all four models" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 125M-parameter fine-tune &lt;strong&gt;beats or matches&lt;/strong&gt; every frontier model on linguistic entities — geographic locations, ages, person names, dates, phone numbers, fax numbers, IP addresses. It &lt;strong&gt;loses badly&lt;/strong&gt; on structured-ID entities — MRNs, license numbers, health-plan IDs, device serial numbers. The right production architecture is not one or the other. It's hybrid. This post is the methodology, the benchmark, and the honest interpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task: span detection across all 18 HIPAA Safe Harbor categories
&lt;/h2&gt;

&lt;p&gt;Patient text comes in messy — "Hi Dr. Okafor, this is Iniko Adeleke, DOB 11/03/1985, MRN OMK-44291, phone 312.555.7820, my partner's email is &lt;a href="mailto:jordan.holloway@workmail.io"&gt;jordan.holloway@workmail.io&lt;/a&gt;. We live in Brookline." A PHI detector has to locate every individually-identifying span: two names, a date, an MRN, a phone, an email, a location. Six spans, six different entity types, in a single 25-token message.&lt;/p&gt;

&lt;p&gt;The HIPAA Safe Harbor rule defines 18 categories of PHI. The Clarioscope model tags all of them: &lt;code&gt;NAME&lt;/code&gt;, &lt;code&gt;LOC&lt;/code&gt;, &lt;code&gt;DATE&lt;/code&gt;, &lt;code&gt;PHONE&lt;/code&gt;, &lt;code&gt;FAX&lt;/code&gt;, &lt;code&gt;EMAIL&lt;/code&gt;, &lt;code&gt;SSN&lt;/code&gt;, &lt;code&gt;MRN&lt;/code&gt;, &lt;code&gt;HEALTH_PLAN&lt;/code&gt;, &lt;code&gt;ACCOUNT&lt;/code&gt;, &lt;code&gt;LICENSE&lt;/code&gt;, &lt;code&gt;VEHICLE&lt;/code&gt;, &lt;code&gt;DEVICE&lt;/code&gt;, &lt;code&gt;URL&lt;/code&gt;, &lt;code&gt;IP&lt;/code&gt;, &lt;code&gt;BIOMETRIC&lt;/code&gt;, &lt;code&gt;PHOTO_REF&lt;/code&gt;, &lt;code&gt;AGE_OVER_89&lt;/code&gt;. The architecture is standard: RoBERTa-base encoder, a token-classification head outputting BIO tags across 37 labels (one &lt;code&gt;O&lt;/code&gt; plus 18 entity types times &lt;code&gt;{B-, I-}&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A side note before going further: the repo is named &lt;code&gt;clarioscope-phi-deberta-v1&lt;/code&gt; because the original plan was DeBERTa-v3-base. During training, DeBERTa-v3 reproduced a NaN-gradient bug specific to this 37-label token classification setup — forward pass loss healthy, backward pass NaN on the first step, across fp16, bf16, and fp32, with explicit classifier head re-init and gradient clipping. After three afternoons of trying to keep DeBERTa alive, I switched to RoBERTa-base, which trained stably with the same training script. The repo name is kept for URL stability and the model card calls it out at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just call the API
&lt;/h2&gt;

&lt;p&gt;The same three reasons as last time, with slightly different weights:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy.&lt;/strong&gt; PHI is the canonical "you'd want this to never leave your infrastructure" data class. A frontier API with a Business Associate Agreement is one option, but BAAs aren't free, aren't available at every tier, and add legal complexity. A self-hosted model never sends the patient's address or DOB to a third party.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; The fine-tuned model runs in 28.6 ms on a CPU. Frontier API calls from my Bangladesh ISP run 1,000–2,000 ms. For redaction-before-routing where every inbound message has to be processed before it can be displayed in an inbox, that wall-clock floor matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Claude Sonnet 4.6 with the PHI-extraction prompt costs $2.53 per 1,000 inferences. Haiku is $1.00 per 1K. GPT-4o is $1.64 per 1K. The fine-tuned model is $0 per inference after training. For a practice receiving 10K messages per day, the math is the same as last time: $3,650–$9,234 per year on frontier vs roughly free on the local model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark
&lt;/h2&gt;

&lt;p&gt;The headline numbers, on a 548-example held-out test set, with entity-level F1 measured by &lt;a href="https://github.com/chakki-works/seqeval" rel="noopener noreferrer"&gt;seqeval&lt;/a&gt; (which requires both entity type AND exact span boundary to match for a true positive):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Macro F1&lt;/th&gt;
&lt;th&gt;Weighted F1&lt;/th&gt;
&lt;th&gt;Latency / example&lt;/th&gt;
&lt;th&gt;Cost / 1K inferences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;raihan-js/clarioscope-phi-deberta-v1&lt;/code&gt; (CPU)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6301&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.7639&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.6 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8492&lt;/td&gt;
&lt;td&gt;0.9213&lt;/td&gt;
&lt;td&gt;1294 ms&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8946&lt;/td&gt;
&lt;td&gt;0.9396&lt;/td&gt;
&lt;td&gt;1980 ms&lt;/td&gt;
&lt;td&gt;$2.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-2024-11-20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8094&lt;/td&gt;
&lt;td&gt;0.8912&lt;/td&gt;
&lt;td&gt;1111 ms&lt;/td&gt;
&lt;td&gt;$1.64&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-phi-deberta-v1%2Fresolve%2Fmain%2Ff1_vs_latency.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-phi-deberta-v1%2Fresolve%2Fmain%2Ff1_vs_latency.png" alt="Macro F1 vs latency" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you stop reading here, the takeaway is: "frontier wins, small model is 45× faster but trails 20–25 points of F1." That's true and it's the honest aggregate. But it's not the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: per-entity F1
&lt;/h2&gt;

&lt;p&gt;Same benchmark, broken out by entity type, sorted by the fine-tuned model's F1 (best to worst):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linguistic entities — small model matches or beats frontier:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;This model&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;th&gt;Sonnet&lt;/th&gt;
&lt;th&gt;GPT-4o&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.983&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.994&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AGE_OVER_89&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.976&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.967&lt;/td&gt;
&lt;td&gt;0.967&lt;/td&gt;
&lt;td&gt;0.836&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.961&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.996&lt;/td&gt;
&lt;td&gt;0.994&lt;/td&gt;
&lt;td&gt;0.980&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.949&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.967&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FAX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.949&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.984&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.945&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;0.970&lt;/td&gt;
&lt;td&gt;0.909&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.818&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.328&lt;/td&gt;
&lt;td&gt;0.289&lt;/td&gt;
&lt;td&gt;0.301&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;LOC&lt;/code&gt; is the standout. The fine-tuned model nearly &lt;strong&gt;triples&lt;/strong&gt; the frontier APIs' F1 on geographic locations. Frontier models systematically under-flag informal location mentions like "she lives in Allston" or "at the Roxbury location" — their pretraining seems to have left them uncertain about whether informal context cues count as PHI. A specialized model trained explicitly to tag these does not hesitate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGE_OVER_89&lt;/code&gt; is another quiet win. Frontier models occasionally tag ages 89-and-under as PHI (they aren't, under Safe Harbor) or miss the "over 89" qualifier ("she's 96") that determines whether the age is reportable. The fine-tuned model learned the rule directly from the training distribution.&lt;/p&gt;

&lt;p&gt;For names, dates, phone numbers, fax numbers, and IPs, the gap between this model and frontier is 1–5 percentage points. Within margin-of-noise for production use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured-ID entities — frontier wins, often dominantly:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;This model&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;th&gt;Sonnet&lt;/th&gt;
&lt;th&gt;GPT-4o&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MRN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.276&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.997&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LICENSE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.170&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HEALTH_PLAN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.264&lt;/td&gt;
&lt;td&gt;0.855&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.983&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.717&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BIOMETRIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.095&lt;/td&gt;
&lt;td&gt;0.410&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.314&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEVICE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.341&lt;/td&gt;
&lt;td&gt;0.732&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VEHICLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.640&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.583&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.983&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;0.915&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ACCOUNT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.759&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.985&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.969&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMAIL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.815&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.738&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.967&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.967&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.931&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Frontier wins these by enormous margins. The fine-tuned model gets &lt;code&gt;MRN&lt;/code&gt; right 28% of the time. Haiku and Sonnet get it right 100% of the time.&lt;/p&gt;

&lt;p&gt;The reason is straightforward once you stare at the data. Structured-ID entities follow surface conventions that &lt;strong&gt;vary wildly between institutions and generators&lt;/strong&gt;. An MRN might look like &lt;code&gt;OMK-44291&lt;/code&gt;, &lt;code&gt;RMR-882034&lt;/code&gt;, &lt;code&gt;DENT-12345-A&lt;/code&gt;, or just &lt;code&gt;8472301&lt;/code&gt;. The training set generates one distribution of ID formats; the test set was generated by a different model and uses a different distribution. The fine-tuned model can only recognize what it saw during training, and when the test-set MRN doesn't match the training conventions, the model either misses it entirely or produces a span boundary that's off by one token (which under seqeval's strict matching is a miss).&lt;/p&gt;

&lt;p&gt;Frontier models win these categories because they've seen a much wider distribution of ID formats during pretraining, and because their attention mechanism is strong enough to anchor an ID span to its context cue — "MRN" or "member ID" or "license #" — regardless of the specific token pattern that follows it.&lt;/p&gt;

&lt;p&gt;This is a real limitation. It's also a reasonable one to live with, because there's a much cheaper way to catch structured-ID PHI than running a frontier API on every message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that wasn't (and the bug that was)
&lt;/h2&gt;

&lt;p&gt;A first version of the model trained on the raw generated annotations and scored 0.57 macro F1 on test — worse than what's shipping now. The expected explanation was distribution shift between train and test. The actual explanation was simpler and more embarrassing.&lt;/p&gt;

&lt;p&gt;The training data had &lt;strong&gt;systematic label noise&lt;/strong&gt;: the data-generation LLM was returning entity texts that included the cue word that introduced the entity. The annotation for "MRN 8472301" came back as &lt;code&gt;{"text": "MRN 8472301", "label": "MRN"}&lt;/code&gt; instead of &lt;code&gt;{"text": "8472301"}&lt;/code&gt;. The literal word "SSN" was annotated as an SSN entity in six different training examples. About 8.6% of all training spans (1,676 of them) had this kind of cue-word contamination. The Claude-generated test set, with a stricter prompt, had two such cases out of 1,632 spans.&lt;/p&gt;

&lt;p&gt;So the model wasn't being graded against the same distribution it learned. It learned "MRN" was part of an MRN span; the test set told it it wasn't.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;clean_data.py&lt;/code&gt; script in the repo strips known cue-word prefixes ("MRN ", "SSN ", "phone ", "account #") from entity texts, re-locates the cleaned text in the inquiry, and drops entities that no longer have a valid span. Importantly, it preserves natural prefix characters like the opening &lt;code&gt;(&lt;/code&gt; in &lt;code&gt;(617) 555-1234&lt;/code&gt; — a first version of the cleanup stripped parens too aggressively and tanked PHONE F1 from 0.94 to 0.51 in one run. The fix was to apply punctuation stripping only after a cue word had been detected and removed, not as a generic "trim leading punctuation" pass.&lt;/p&gt;

&lt;p&gt;The cleanup recovered about 4 percentage points of test macro F1 and (more interestingly) flipped two entity types from "loses badly" to "competitive": EMAIL went from 0.55 to 0.82, ACCOUNT from 0.21 to 0.76.&lt;/p&gt;

&lt;p&gt;The deeper lesson is one that's well-known but easy to forget: &lt;strong&gt;synthetic data is fast and cheap and the cost shows up as systematic noise that nobody else will catch for you&lt;/strong&gt;. The annotations themselves need a QA pass before training. Real-world data has its own noise problems, but it tends not to label cue words as entities, because human annotators don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing benchmark leakage
&lt;/h2&gt;

&lt;p&gt;Same trick as model 1: training set was generated by &lt;code&gt;gpt-4o-mini-2024-07-18&lt;/code&gt;; the held-out test set was generated by Claude with a deliberately different prompt style. This cross-generator split mitigates the failure mode where a fine-tuned model just learns one generator's style and the benchmark inflates.&lt;/p&gt;

&lt;p&gt;A side effect on this model specifically: the Claude-generated test set uses tighter, more uniform structured-ID formats than the training set. That's part of why the test F1 on structured-ID entities is harsher than the val F1 (val: 0.86 macro; test: 0.63 macro). It's also fair, because real-world MRN formats are at least as varied as the gap between the two generators — and probably more so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommended production architecture: hybrid
&lt;/h2&gt;

&lt;p&gt;Given the per-entity breakdown, the right architecture is not "use this model alone" or "use a frontier API alone." It's a three-stage pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run this model first&lt;/strong&gt; on every inbound message. ~30 ms on a CPU, $0, never sends text off-host. Captures &lt;code&gt;NAME&lt;/code&gt;, &lt;code&gt;LOC&lt;/code&gt;, &lt;code&gt;DATE&lt;/code&gt;, &lt;code&gt;PHONE&lt;/code&gt;, &lt;code&gt;FAX&lt;/code&gt;, &lt;code&gt;IP&lt;/code&gt;, &lt;code&gt;AGE_OVER_89&lt;/code&gt; reliably — that's most of the volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add regex matchers&lt;/strong&gt; for highly structured patterns the model misses: SSN (&lt;code&gt;\d{3}-\d{2}-\d{4}&lt;/code&gt;), credit-card numbers, basic MRN/account patterns specific to your practice's conventions. Regex is fast, free, and brittle — but correct when it matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back to a frontier API only when&lt;/strong&gt; the message contains likely structured-ID content the local pipeline didn't resolve, or when downstream confidence is needed. This pays the latency and dollar cost only on a small fraction of traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a practice receiving 10,000 messages per day where roughly 10% have unresolved structured-ID content after stages 1 and 2, the hybrid sends 1,000 calls per day to Haiku ($0.30/day) instead of 10,000 ($3.00/day). Most messages never leave the host. Latency is bounded by the local model. The frontier model becomes a "structured-ID specialist" rather than a "PHI redaction generalist."&lt;/p&gt;

&lt;p&gt;This is the actual cost-effective answer in 2026. The small model is not a frontier replacement; it's a frontier accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost ledger
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9,500 synthetic training examples via OpenAI (&lt;code&gt;gpt-4o-mini&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~$1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RunPod RTX A4000 pod (two training runs, ~25 min total)&lt;/td&gt;
&lt;td&gt;~$1.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark API calls (Haiku + Sonnet + GPT-4o on 548 examples × 2 runs)&lt;/td&gt;
&lt;td&gt;~$5.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugging Face hosting&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$8.10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A bit more expensive than model 1 because the benchmark ran twice — once before the data cleanup, once after. The cleanup story was a four-percentage-point F1 improvement, which seems small but matters for the "where does this model lose" interpretation of the per-entity numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoModelForTokenClassification&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raihan-js/clarioscope-phi-deberta-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForTokenClassification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi Dr. Okafor, this is Iniko Adeleke, DOB 11/03/1985. phone 312.555.7820, email jordan@workmail.io. I live in Brookline.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_offsets_mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;offsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offset_mapping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;pred_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;id2label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id2label&lt;/span&gt;
&lt;span class="n"&gt;spans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id2label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pred_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ent_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;id2label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pred_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ent_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ent_type&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'text': 'Dr. Okafor', 'label': 'NAME'}
# {'text': 'Iniko Adeleke', 'label': 'NAME'}
# {'text': '11/03/1985', 'label': 'DATE'}
# {'text': '312.555.7820', 'label': 'PHONE'}
# {'text': 'jordan@workmail.io', 'label': 'EMAIL'}
# {'text': 'Brookline', 'label': 'LOC'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The model card has the full list. The ones worth surfacing here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All training and evaluation data is synthetic.&lt;/strong&gt; No real production validation yet. A real-world calibration pass is required before deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured-ID entities are weak.&lt;/strong&gt; Per the benchmark, this model is materially worse than frontier APIs on &lt;code&gt;MRN&lt;/code&gt;, &lt;code&gt;LICENSE&lt;/code&gt;, &lt;code&gt;HEALTH_PLAN&lt;/code&gt;, &lt;code&gt;BIOMETRIC&lt;/code&gt;, and several others. Pair with regex and/or a frontier fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a HIPAA compliance verdict.&lt;/strong&gt; This model tags entity types as defined in the Safe Harbor rule. HIPAA compliance is a regulatory determination that a model can't make on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;English only, healthcare practice domain only.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is model 2 of three. The third is &lt;code&gt;clarioscope-insurance-v1&lt;/code&gt; — structured JSON extraction of insurance- and billing-relevant fields from inbound text. Probably a small encoder-decoder with constrained decoding. When all three are published, they'll go up as a Hugging Face collection with a single longer post tying the suite together.&lt;/p&gt;

&lt;p&gt;The honest takeaway from this one: small specialized models don't always beat frontier on aggregate, but the per-entity breakdown is where the actual production decision lives. Frontier-or-nothing is the wrong frame. Frontier-as-fallback-to-a-cheap-local-model is the right one.&lt;/p&gt;

&lt;p&gt;Follow along on &lt;a href="https://huggingface.co/raihan-js" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; or &lt;a href="https://github.com/raihan-js" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>healthcare</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Matching frontier LLMs at 22 lower latency: a 184M-parameter intent classifier for healthcare text</title>
      <dc:creator>Raihan</dc:creator>
      <pubDate>Mon, 11 May 2026 17:43:38 +0000</pubDate>
      <link>https://forem.com/raihan-js/matching-frontier-llms-at-22x-lower-latency-a-184m-parameter-intent-classifier-for-healthcare-text-5ec2</link>
      <guid>https://forem.com/raihan-js/matching-frontier-llms-at-22x-lower-latency-a-184m-parameter-intent-classifier-for-healthcare-text-5ec2</guid>
      <description>&lt;p&gt;Healthcare practices drown in inbound patient text. Email, contact forms, live chat, SMS, voicemail transcripts — every channel sends messages that need to be routed: to scheduling, to billing, to clinical, to the front desk. It's a high-volume, deterministic, latency-sensitive task.&lt;/p&gt;

&lt;p&gt;The obvious answer in 2026 is to throw a frontier LLM at it. Claude Haiku 4.5 will give you 95% accuracy on this kind of classification. GPT-4o will too. But every call costs real money, adds about a second of network round-trip, and sends patient text to a third party that doesn't have a BAA with you.&lt;/p&gt;

&lt;p&gt;I built a small alternative — a 184M-parameter DeBERTa-v3-base fine-tune — and benchmarked it against Claude Haiku 4.5, Claude Sonnet 4.6, and GPT-4o on a 1,154-example test set. &lt;strong&gt;The fine-tuned model lands within 4 percentage points of accuracy&lt;/strong&gt; of the best frontier model, &lt;strong&gt;runs 22× faster&lt;/strong&gt; on a CPU, and costs &lt;strong&gt;effectively $0 per inference&lt;/strong&gt; after training. Total cost to build it: under $3.&lt;/p&gt;

&lt;p&gt;Model on Hugging Face: &lt;a href="https://huggingface.co/raihan-js/clarioscope-intent-deberta-v1" rel="noopener noreferrer"&gt;&lt;code&gt;raihan-js/clarioscope-intent-deberta-v1&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-intent-deberta-v1%2Fresolve%2Fmain%2Faccuracy_vs_latency.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fraihan-js%2Fclarioscope-intent-deberta-v1%2Fresolve%2Fmain%2Faccuracy_vs_latency.png%3Fv%3D2" alt="Accuracy vs latency" width="1691" height="967"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is model 1 of three I'm building for the ClarioScope SLM Suite — a healthcare intake intelligence pipeline. The other two are a PHI detector and an insurance extractor; they're in development. This post is the methodology and the benchmark for the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task
&lt;/h2&gt;

&lt;p&gt;Seven intent labels, designed for production routing at a healthcare practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;What it captures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new_patient_inquiry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A prospective patient asking about becoming a patient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;existing_patient_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An existing patient with a non-urgent question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appointment_request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scheduling, rescheduling, or cancellation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;billing_inquiry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Questions about bills or pricing of services already received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clinical_concern&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An active medical concern requiring clinical attention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;complaint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dissatisfaction with service, staff, communication, or outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price_shopper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pricing-only inquiry, no commitment signals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These categories are opinionated and they have real ambiguity at the edges. A new patient asking for their first appointment is both new-patient and appointment-request. A frustrated patient describing a medical concern is both clinical and complaint. The data-generation prompt encodes explicit disambiguation rules (complaint dominates when both signals are present; pre-commitment pricing questions are &lt;code&gt;price_shopper&lt;/code&gt; even if they mention insurance), but the boundary cases are where every model — fine-tuned or frontier — gives up F1 points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use the API
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Frontier API calls from my Bangladesh ISP run 1,000–1,600 ms. For routing, that's the difference between an inbox that updates instantly and one that lags noticeably. The fine-tuned model on a CPU runs in 48 ms. On a GPU it would be another 5–10× faster. Either way, the wall-clock floor for a hosted API call is in the hundreds of milliseconds even before the model processes anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Claude Sonnet 4.6 costs $0.76 per 1,000 inferences on this task. Haiku is $0.25 per 1K. GPT-4o is $0.53 per 1K. For a single practice receiving 10,000 inbound messages per day across all channels (not unrealistic for a multi-location dental or dermatology group), that's $912 to $2,774 per practice per year — a hard line item on the SaaS economics. The fine-tuned model has a one-time training cost and approximately zero marginal per-inference cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy.&lt;/strong&gt; Frontier APIs are great, and they're also a third-party data path. For protected health information you'd want a BAA, and not every API provider offers one at every tier. A self-hosted classifier never sends patient text anywhere.&lt;/p&gt;

&lt;p&gt;The accuracy gap versus frontier is real but small enough that for production routing, the speed/cost/privacy wins dominate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;Standard &lt;a href="https://huggingface.co/microsoft/deberta-v3-base" rel="noopener noreferrer"&gt;DeBERTa-v3-base&lt;/a&gt; with a sequence classification head: a single linear layer over the pooled &lt;code&gt;[CLS]&lt;/code&gt; representation producing 7 logits. All 184M parameters fine-tuned. No LoRA — at this dataset size, full fine-tuning beats parameter-efficient methods without much overhead. Training was 5 epochs of 8,099 examples on a single RTX 4090 (rented on RunPod), batch size 32, max sequence length 256 tokens, learning rate 2e-5 with cosine schedule and 10% warmup, fp16 mixed precision. Total training wall time: about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training data — synthetic and transparent about it
&lt;/h2&gt;

&lt;p&gt;This is the most important section of the post for anyone considering similar work. &lt;strong&gt;All training and test data is synthetic.&lt;/strong&gt; There is no real patient data anywhere in the pipeline. This is a deliberate choice — using synthetic data for v1 sidesteps HIPAA constraints entirely and lets the model ship fast. A v2 trained on real PHI would need HIPAA-eligible training infrastructure (AWS SageMaker or Azure ML with a BAA), and that's a separate, more careful project.&lt;/p&gt;

&lt;p&gt;But "synthetic" is doing a lot of work in that sentence. The naïve approach — ask an LLM for 1,000 example patient inquiries per intent — produces what I'll call &lt;strong&gt;ChatGPT-polite text&lt;/strong&gt;: every message opens with "Hi!", ends with "Thanks!", uses correct grammar and punctuation, and reads nothing like a real SMS message that an actual frustrated parent sends at 2 AM.&lt;/p&gt;

&lt;p&gt;A model trained on ChatGPT-polite text will overfit to the politeness markers and degrade badly on real production text. So the generation prompt forces a &lt;strong&gt;mandatory realism mix per batch&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~40% polished&lt;/strong&gt; (full sentences, correct grammar, proper punctuation, formal or neutral)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~40% casual&lt;/strong&gt; (lowercase starts, contractions, fragments, missing terminal punctuation, conversational)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~20% messy&lt;/strong&gt; (typos, autocorrect mistakes, abbreviations like &lt;code&gt;u&lt;/code&gt;/&lt;code&gt;appt&lt;/code&gt;/&lt;code&gt;tmrw&lt;/code&gt;, ALL CAPS for urgency, run-on phrasing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus channel-conditional scaling: SMS is the messiest, voicemail transcripts second messiest, email and web forms more polished. The prompt also includes about 20 lines of &lt;strong&gt;style anchors&lt;/strong&gt; — concrete patterns the LLM should reproduce. Stuff like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Abbreviations: &lt;code&gt;u&lt;/code&gt;/&lt;code&gt;ur&lt;/code&gt;, &lt;code&gt;appt&lt;/code&gt;, &lt;code&gt;tmrw&lt;/code&gt;, &lt;code&gt;yr&lt;/code&gt;, &lt;code&gt;pls&lt;/code&gt;, &lt;code&gt;thx&lt;/code&gt;, &lt;code&gt;rx&lt;/code&gt;, &lt;code&gt;ins&lt;/code&gt; (insurance)&lt;/p&gt;

&lt;p&gt;Fragment phrases: "billing question call me back", "need to reschedule thursday", "kid has fever 102", "still no answer about my x-ray"&lt;/p&gt;

&lt;p&gt;Run-on voicemail: "uh hi yeah this is calling about that thing you mentioned last week i think it was a follow up or something can you call me back"&lt;/p&gt;

&lt;p&gt;Conversational starts (no greeting): "Quick question —", "So I got this bill...", "Need to cancel —"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two dry runs before the full 9,000-example generation: the first one without the realism mix produced very polite, very clean output (82% of messages opened with "Hi!", 0% had ALL CAPS, almost nothing was a fragment); the second one with the mix landed at 18% greetingless openers, 22% abbreviations, 21% no terminal punctuation, 6% ALL CAPS urgency. The shape of the distribution actually moved when the prompt told it to move.&lt;/p&gt;

&lt;p&gt;Costs: the 9,000 training examples cost about $1.20 of OpenAI credit (via &lt;code&gt;gpt-4o-mini-2024-07-18&lt;/code&gt;, JSON-object response format, temperature 1.0, 8-worker parallel generation).&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing benchmark leakage
&lt;/h2&gt;

&lt;p&gt;The naive failure mode here is generating both train and test with the same model. The fine-tuned model would learn the generator's style, and the benchmark would inflate.&lt;/p&gt;

&lt;p&gt;So train and test come from different generators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Train (9,000 examples)&lt;/strong&gt; — generated by &lt;code&gt;gpt-4o-mini-2024-07-18&lt;/code&gt; with the prompt above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test (1,154 examples)&lt;/strong&gt; — generated by Claude with a &lt;strong&gt;deliberately different prompt style&lt;/strong&gt; and a &lt;strong&gt;different abbreviation set&lt;/strong&gt; (&lt;code&gt;w/&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;hrs&lt;/code&gt;, &lt;code&gt;BTW&lt;/code&gt;, &lt;code&gt;IDK&lt;/code&gt;, &lt;code&gt;plz&lt;/code&gt; versus the train prompt's &lt;code&gt;u&lt;/code&gt;, &lt;code&gt;tmrw&lt;/code&gt;, &lt;code&gt;appt&lt;/code&gt;). The test set leans into more medically specific content (real conditions, real procedure names) and longer rambling messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A side effect of this split: when I benchmark against Claude Haiku 4.5 and Claude Sonnet 4.6 below, those models are from the same family as the test-set generator. If anything, they should get a small style-familiarity advantage. The benchmark numbers below are with that caveat in mind. (Spoiler: they don't visibly benefit.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark
&lt;/h2&gt;

&lt;p&gt;Evaluated on 1,154 held-out test examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Macro F1&lt;/th&gt;
&lt;th&gt;Latency / example&lt;/th&gt;
&lt;th&gt;Cost / 1K inferences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;raihan-js/clarioscope-intent-deberta-v1&lt;/code&gt; (CPU)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.16%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.07%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;95.32%&lt;/td&gt;
&lt;td&gt;95.28%&lt;/td&gt;
&lt;td&gt;1064 ms&lt;/td&gt;
&lt;td&gt;$0.252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;93.59%&lt;/td&gt;
&lt;td&gt;93.53%&lt;/td&gt;
&lt;td&gt;1566 ms&lt;/td&gt;
&lt;td&gt;$0.759&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-2024-11-20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;95.23%&lt;/td&gt;
&lt;td&gt;95.17%&lt;/td&gt;
&lt;td&gt;1036 ms&lt;/td&gt;
&lt;td&gt;$0.527&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Latency is wall-clock single-example latency through each provider's chat completions API, measured from a Bangladesh ISP. The fine-tuned model number is on a CPU (no GPU acceleration). Cost is the actual API spend per 1,000 calls based on token counts from the run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqp4f5dg6rjgb3hv02jo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqp4f5dg6rjgb3hv02jo2.png" alt="Accuracy and cost comparison" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things in this table are interesting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sonnet 4.6 is worse than Haiku 4.5.&lt;/strong&gt; A bigger, slower, more expensive frontier model produces lower accuracy on this task. This isn't an artifact of one run — I've seen it consistently. My take: for narrow, well-structured classification with short prompts, more reasoning capacity sometimes second-guesses the correct intuition. The first thought is often right, and a smaller model that doesn't have the option to deliberate just commits to it. The right tool for this kind of job is small and specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The latency advantage is on CPU.&lt;/strong&gt; The 48ms number is on CPU. A modest GPU would drop it to ~5–10 ms. The frontier API numbers are network-bound — the model itself processes the request in tens of milliseconds, but the wall-clock floor for a hosted API call from a non-US-East ISP is in the hundreds of milliseconds before the model has even started. Adding a GPU at the API side does nothing for that floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost gap doesn't shrink at scale.&lt;/strong&gt; API cost scales linearly with call volume. The fine-tuned model has a one-time training cost (about $2.40 of OpenAI plus RunPod compute together) and approximately zero marginal cost. For 10K daily inferences over a year, the dollar swing is between zero and roughly $2,800.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-class F1 and where the errors live
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3kfcrkif2emfrtqppag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3kfcrkif2emfrtqppag.png" alt="Confusion matrix on val set" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model's per-class F1 on the val set, ranked best to worst:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price_shopper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;complaint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.929&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;billing_inquiry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.908&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appointment_request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.881&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clinical_concern&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.874&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;existing_patient_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.834&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new_patient_inquiry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.819&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hardest pairs to disambiguate are exactly the pairs you'd expect to be hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;new_patient_inquiry&lt;/code&gt; ↔ &lt;code&gt;appointment_request&lt;/code&gt;&lt;/strong&gt; — a new patient asking to schedule their first visit fits both labels. The data-gen prompt resolves toward &lt;code&gt;new_patient_inquiry&lt;/code&gt; for messages that lead with the becoming-a-patient signal, but the model lands on &lt;code&gt;appointment_request&lt;/code&gt; more often than the label intends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;existing_patient_question&lt;/code&gt; ↔ &lt;code&gt;clinical_concern&lt;/code&gt;&lt;/strong&gt; — medical questions from established patients read as low-grade concerns to the model, because at the lexical level they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clinical_concern&lt;/code&gt; ↔ &lt;code&gt;complaint&lt;/code&gt;&lt;/strong&gt; — frustrated medical concerns combine both signals; the prompt's tie-breaker says complaint dominates, but the model occasionally goes the other way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These same pairs gave Claude Haiku 4.5 trouble too when I ran the benchmark by hand on a sample. They're real ambiguity in the task, not classifier weakness. Useful production move: have the model emit confidence (max softmax) alongside the label, and route low-confidence predictions to a human reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost ledger
&lt;/h2&gt;

&lt;p&gt;Full breakdown of what it cost to ship this model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9,000 synthetic training examples via OpenAI (&lt;code&gt;gpt-4o-mini&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RunPod RTX 4090 pod (about 50 minutes including iteration)&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark API calls (Haiku + Sonnet + GPT-4o, 1,154 examples each)&lt;/td&gt;
&lt;td&gt;$1.78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugging Face hosting&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's it. End-to-end, from empty repo to published model + reproducible benchmark, for less than the price of lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSequenceClassification&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raihan-js/clarioscope-intent-deberta-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSequenceClassification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi, I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m new to the area and looking for a dermatologist. Are you accepting new patients?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;got a bill for $382 for my visit on 4/12 but my copay should only be $35 — what&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the rest?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my kid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s fever is 103.2 and not coming down with tylenol. need advice now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;
&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id2label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ['new_patient_inquiry', 'billing_inquiry', 'clinical_concern']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;I've put a full Limitations section in the &lt;a href="https://huggingface.co/raihan-js/clarioscope-intent-deberta-v1" rel="noopener noreferrer"&gt;model card&lt;/a&gt;, but the highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All training and test data is synthetic.&lt;/strong&gt; No real production validation yet. A real-world calibration pass is a prerequisite for production deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;English only.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare practice domain only.&lt;/strong&gt; Routes messages within a practice — does not generalize to other industries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven categories, not exhaustive.&lt;/strong&gt; Messages that don't fit get the closest available label rather than an "unknown" bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PHI redaction is performed by this model.&lt;/strong&gt; PHI detection is a separate model in the suite (in development), and HIPAA compliance is a regulatory determination that no model can make.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is model 1 of three. The other two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clarioscope-phi-deberta-v1&lt;/code&gt;&lt;/strong&gt; — a token-classification model (BIO tagging) for detecting PHI spans in patient text. Same DeBERTa base, different head, different training data (synthetic PHI-annotated text). Goal: redact before routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clarioscope-insurance-v1&lt;/code&gt;&lt;/strong&gt; — structured JSON extraction of insurance- and billing-relevant fields from inbound text. Probably a small encoder-decoder or constrained-decoding setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When all three are published, they'll go up as a Hugging Face collection and the master writeup will be a single longer post tying the suite together. Follow along on &lt;a href="https://huggingface.co/raihan-js" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; or &lt;a href="https://github.com/raihan-js" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've shipped a small specialized model that beats — sorry, &lt;strong&gt;matches&lt;/strong&gt; — frontier APIs on a narrow task, I'd love to hear about it. The pattern works.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>huggingface</category>
    </item>
  </channel>
</rss>
