<?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>DEV Community: Thomas Virdis</title>
    <description>The latest articles on DEV Community by Thomas Virdis (@ctcycle).</description>
    <link>https://dev.to/ctcycle</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4033906%2F94785381-d76a-4a06-ba89-2e5062d4db48.png</url>
      <title>DEV Community: Thomas Virdis</title>
      <link>https://dev.to/ctcycle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ctcycle"/>
    <language>en</language>
    <item>
      <title>DILIGENT: A Local Workspace for Structured Drug-Induced Liver Injury Review</title>
      <dc:creator>Thomas Virdis</dc:creator>
      <pubDate>Sun, 18 Oct 2026 06:31:26 +0000</pubDate>
      <link>https://dev.to/ctcycle/diligent-a-local-workspace-for-structured-drug-induced-liver-injury-review-4c8n</link>
      <guid>https://dev.to/ctcycle/diligent-a-local-workspace-for-structured-drug-induced-liver-injury-review-4c8n</guid>
      <description>&lt;p&gt;Drug-induced liver injury is a hard call, and the difficulty is rarely about spotting a signal. A clinician looking at a rising ALT already knows something is happening. The hard part is the reasoning that has to follow: which of the patient's medications is a plausible cause, how the injury pattern reads on the lab values and the timeline, whether a competing explanation fits better, and whether the label of "causality" is defensible enough to act on.&lt;/p&gt;

&lt;p&gt;That reasoning is structured. Causality assessment frameworks such as RUCAM and the DILIN expert-consensus categories, the Hy's Law thresholds, and the LiverTox monographs all exist precisely because the argument has to be laid out in a consistent, inspectable way. A lot of the work of a DILI review is therefore assembly and consistency rather than insight: collecting the right inputs, resolving the liver chemistry pattern, checking the same boxes every time, and keeping the evidence visible next to whatever conclusion gets written.&lt;/p&gt;

&lt;p&gt;DILIGENT is an application for that part of the job. It is a local, single-user decision-support tool for structured DILI review: it collects the case, runs a deterministic pipeline over it, assembles an evidence bundle, and produces a reviewable draft assessment that a clinician can edit, approve, or reject. The draft is not an answer. It is the assembled case, written down in a form that can be checked.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn53iqhvyu3df7l3ef9ur.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn53iqhvyu3df7l3ef9ur.png" alt="DILI Agent with a structured fictional case ready for review" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a review looks like
&lt;/h2&gt;

&lt;p&gt;The workflow is one screen at a time, and each step has a defined output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure.&lt;/strong&gt; The application needs several models, and it treats them as roles rather than as a single chatbot: a clinical model for the consultation step, a text extraction model for structured extraction, a revision model for editing passes, and a timeline model for chronological reconstruction. Each role can point at a local Ollama model or at a cloud provider. Where a provider needs a key, the key is stored by the backend and shown back only as a fingerprint, and it stays inactive until it is explicitly activated, so a rejected key cannot replace the one that works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter the case.&lt;/strong&gt; The DILI Agent form asks for the things the frameworks need: patient and case identifiers, demographics, the suspected drug with dose and timing, symptom onset and history, laboratory values with units and reference limits, concomitant medications, competing causes, dechallenge and rechallenge status, and the clinical question being asked. Where the deployment enables it, files can be attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-flight checks.&lt;/strong&gt; Before any model is called, rule-based checks run over the input. Blocking issues have to be fixed. Warnings can be acknowledged and carried forward. Section extraction confidence is enforced here too: an aggregate below 0.65 blocks the run, and between 0.65 and 0.85 the operator has to acknowledge the weakness. This is the first place where the design chooses to stop rather than guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline.&lt;/strong&gt; The assessment runs as a background job, so a long consultation does not hold the browser. The stages are concrete: normalization, deterministic section extraction, structured LLM extraction of drugs, diseases and labs with bounded retries and a deterministic fallback, presentation hepatic pattern resolution, case qualification kept deliberately separate from drug attribution, conservative drug identity resolution, per-drug RUCAM evidence, an internal structured causality synthesis, Hy's Law status, an explicit severity grade, a mandatory competing-cause checklist, optional retrieval, a bounded clinical consultation, and finally the evidence bundle and the report with a faithfulness audit.&lt;/p&gt;

&lt;p&gt;The R ratio is computed from paired ALT and ALP values at the presentation episode, each divided by its own laboratory upper limit of normal rather than by a shared reference: R at or above 5 reads hepatocellular, at or below 2 cholestatic, between 2 and 5 mixed, and the pattern stays indeterminate when the inputs cannot support the calculation. Case qualification is its own answer, too. Whether the labs meet the application's typical liver-injury detection criteria is recorded separately from which drug, if any, is implicated, and a qualifying episode is not treated as a diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review.&lt;/strong&gt; The result lands in Clinical Sessions as two things side by side: the narrative report and the structured dossier behind it, including twelve acceptance questions answered with quotes and a fourteen-section dossier in the shape clinicians already expect from an FDA-style review. Drug matching is exposed rather than hidden, with per-drug flags distinguishing accepted matches from ambiguous ones that need a human decision and from missing ones. From there the report can be edited by hand, put through an LLM revision that becomes a new official version, compared against the previous version, saved, copied, or exported. A separate tab reconstructs the patient timeline, and Data Inspection shows the local catalogs the matching ran against.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2fosgsd8anxfqnl2jql.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2fosgsd8anxfqnl2jql.png" alt="Saved report showing detected drugs and the hepatotoxicity summary" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The report is built from the evidence, not from the model
&lt;/h2&gt;

&lt;p&gt;The design decision that shapes everything else is the order of operations. The final report is generated from a persisted evidence bundle that exists before any LLM summary is written. The narrative is then checked against that bundle by a faithfulness audit, and blocking faithfulness issues are finalization blockers: a session with an unresolved one is forced into "requires human review" and can never be marked successful.&lt;/p&gt;

&lt;p&gt;Revisions get the same treatment rather than a lighter one. A revised report is audited again against the structured evidence before it can be persisted as a successful revision, so an editing pass cannot talk its way past a competing-cause, Hy's Law, causality or rechallenge safeguard that the deterministic layer already enforced.&lt;/p&gt;

&lt;p&gt;The same instinct shows up in the extraction layer. When a model-based extraction fails, the pipeline falls back to deterministic rule-based parsers and records a fallback warning instead of failing the run or inventing content. Degradation is visible in the session rather than silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Causality stays conservative
&lt;/h2&gt;

&lt;p&gt;Scoring a drug as a cause is where clinical software is most likely to overreach, so the causal semantics here are deliberately narrow. If the patient's own record already documents a RUCAM score, that score is preserved and mapped to its category. If it does not, the pipeline does not manufacture a numeric pseudo-score: it keeps a criterion-level evidence checklist, with its limitations stated, instead of a number nobody computed. RUCAM output is supportive rather than dispositive in either case.&lt;/p&gt;

&lt;p&gt;The same line is drawn around drug-level knowledge. A LiverTox likelihood and an FDA DILIrank concern class are prior opinions about a drug, not patient-level causality, and neither is converted into a weight on this patient's case. A rechallenge is never recommended. A missing follow-up laboratory value is recorded as missing, not as negative, and a general laboratory improvement is not attributed to every medication in a polypharmacy case. Competing causes are checked against a mandatory list and carry four explicit states, excluded, not excluded, unknown and missing data, so an unresolved differential stays unresolved in writing.&lt;/p&gt;

&lt;p&gt;None of this makes the tool cautious in a vague way. It makes each statement in the report traceable to either a computed value or an explicit absence, which is what an audit of the case would need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval that runs on the machine
&lt;/h2&gt;

&lt;p&gt;The RAG layer is optional per assessment, and its embedding stack is local by construction: the pinned IBM Granite multilingual embedding model, 384 dimensions, run through ONNX Runtime on CPU with a quantized artifact whose SHA-256 is checked against a pinned value. Retrieval itself is hybrid over LanceDB, combining vector similarity with text search, weighted so that vector evidence dominates while exact terms still matter.&lt;/p&gt;

&lt;p&gt;The corpora come from updaters built into the application: LiverTox monographs retrieved through NCBI's machine services, the RxNorm-backed RxNav drug catalog, the FDA DILIrank workbook, and a documents folder you point the app at, which the desktop build selects through the native operating system picker. DILIrank is deliberately read-only and row-level: a record that cannot be linked deterministically to the canonical drug catalog stays inspectable but never feeds the clinical consultation. A retrieval run does not need a cloud embedding service, and it does not need to send the case anywhere to find the relevant monograph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models are a configuration choice
&lt;/h2&gt;

&lt;p&gt;The four roles can run entirely on Ollama, drawing from a local catalog that includes general and medical models, or they can point at cloud providers. Switching is configuration rather than architecture. That matters for two audiences at once: a hospital-adjacent user who needs the case to stay on the workstation, and a researcher who wants to compare how different models handle the structured extraction and consultation steps over the same evidence bundle.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6v6ffex2pwhjsspkwzp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6v6ffex2pwhjsspkwzp.png" alt="Patient timeline overview with review controls and chronology" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it, and where the data lives
&lt;/h2&gt;

&lt;p&gt;On Windows the application ships as a packaged desktop build: a portable executable and an MSI installer, a Tauri 2 shell using the system WebView2 runtime, and the Python backend frozen with PyInstaller into its own binary. Release artifacts carry SHA-256 manifests, and the published binaries are unsigned under a policy the release notes state plainly rather than leaving you to discover it at launch. Application data lives under the user's local application data directory, beside a hash-addressed runtime directory, so installing a new version does not touch the data the previous one wrote. A source mode is also available for anyone who would rather build from the tree; it pins the exact Python, Node, uv and Rust versions it expects.&lt;/p&gt;

&lt;p&gt;macOS and Linux are supported in source mode only, with no one-step launcher, and containers are explicitly not supported: there is no Dockerfile in the repository and no official container path. Storage is a relational database, either embedded SQLite or an external PostgreSQL instance, and the API surface is described by a checked-in OpenAPI document so the client and backend cannot drift apart silently.&lt;/p&gt;

&lt;p&gt;One caveat belongs in the first paragraph of any deployment discussion rather than the last: if the model roles are pointed at a cloud provider, the clinical text of the case leaves the machine. The local configuration exists, and the choice is the user's.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvt62b1vt7ze6ja45bmoe.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvt62b1vt7ze6ja45bmoe.png" alt="Timeline event inspector for a selected laboratory value" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately is not
&lt;/h2&gt;

&lt;p&gt;DILIGENT is not a diagnostic device and does not replace clinical judgment. Every input, generated statement, conclusion and piece of exported text is the responsibility of the qualified clinician using it. It is a single-user tool: there is no account system, no multi-user deployment, no mobile client and no hosted service, and network deployment behind a reverse proxy is explicitly unsupported. The packaged build does add a loopback guard, a bootstrap token exchanged for a session cookie plus host and origin checks, but that is protection for one user on one machine, not a login system. It does not train or fine-tune models, and it does not publish accuracy numbers, because none have been measured. The repository ships user documentation, architecture documentation and runtime documentation instead, which is the honest artifact for software at this stage.&lt;/p&gt;

&lt;p&gt;The licence is the GNU General Public License, version 3 or any later version, so it is free software in the full sense: inspectable, modifiable, and redistributable under the same terms.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feovgy2t81qefhbz2qgx5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feovgy2t81qefhbz2qgx5.png" alt="Data Inspection view filtered to public drug catalog records" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;If you work with drug-induced liver injury and you have ever wanted the case assembled the way the frameworks actually specify it, with the scoring visible and the report traceable line by line to the evidence behind it, this is worth an afternoon. It runs on a workstation, it takes a fictional case in minutes, and it will show you exactly how it reached whatever it says.&lt;/p&gt;

&lt;p&gt;The repository, documentation and packaged Windows builds are at &lt;a href="https://github.com/CTCycle/DILIGENT-Clinical-Copilot" rel="noopener noreferrer"&gt;github.com/CTCycle/DILIGENT-Clinical-Copilot&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>healthcare</category>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Eight green rows and an empty string</title>
      <dc:creator>Thomas Virdis</dc:creator>
      <pubDate>Tue, 15 Sep 2026 20:04:01 +0000</pubDate>
      <link>https://dev.to/ctcycle/llmeter-measuring-the-model-you-actually-run-394n</link>
      <guid>https://dev.to/ctcycle/llmeter-measuring-the-model-you-actually-run-394n</guid>
      <description>&lt;p&gt;I wanted a narrow answer this morning: does the 2B model I keep on this laptop return a function call when I ask for one, or does it wander off mid-sentence?&lt;/p&gt;

&lt;p&gt;I got a report with eight green rows, zero errors, and eight empty response strings. I noticed the first two facts and missed the third for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  The laptop
&lt;/h2&gt;

&lt;p&gt;6 GB RTX 3060, laptop version. Ollama loads &lt;code&gt;qwen3.5:2b&lt;/code&gt; at 5.0 GB of VRAM with a 131072-token context window, and reports the split as 25 percent CPU, 75 percent GPU. It does not fit, so it spills, and that is a lot of cache to be holding next to weights that already overflow.&lt;/p&gt;

&lt;p&gt;Your machine will differ in every one of those numbers, which is the whole reason for measuring at home.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run
&lt;/h2&gt;

&lt;p&gt;The tool is LLMeter, one Rust binary that benchmarks whatever OpenAI-compatible endpoint you point it at. Standard suite, temperature 0.0, sixteen tokens per answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llmeter &lt;span class="nt"&gt;--provider&lt;/span&gt; ollama bench run &lt;span class="nt"&gt;--suite&lt;/span&gt; llm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--models&lt;/span&gt; qwen3.5:2b &lt;span class="nt"&gt;--benchmarks&lt;/span&gt; all &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runs&lt;/span&gt; 1 &lt;span class="nt"&gt;--max-tokens&lt;/span&gt; 16 &lt;span class="nt"&gt;--temperature&lt;/span&gt; 0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--export&lt;/span&gt; both &lt;span class="nt"&gt;--report&lt;/span&gt; both
&lt;/code&gt;&lt;/pre&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvr8qt4m1ddldsf0n6gh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvr8qt4m1ddldsf0n6gh.png" alt="Wall time and output throughput for the eight requests in one run of the standard suite" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;Valid output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;chat generation&lt;/td&gt;
&lt;td&gt;short&lt;/td&gt;
&lt;td&gt;103,621 ms&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;responses API&lt;/td&gt;
&lt;td&gt;responses&lt;/td&gt;
&lt;td&gt;834 ms&lt;/td&gt;
&lt;td&gt;19.2&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;consistency&lt;/td&gt;
&lt;td&gt;x2&lt;/td&gt;
&lt;td&gt;844 ms&lt;/td&gt;
&lt;td&gt;38.5&lt;/td&gt;
&lt;td&gt;similarity 1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt sizes&lt;/td&gt;
&lt;td&gt;short (22 tok in)&lt;/td&gt;
&lt;td&gt;389 ms&lt;/td&gt;
&lt;td&gt;41.1&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt sizes&lt;/td&gt;
&lt;td&gt;medium (49 tok in)&lt;/td&gt;
&lt;td&gt;723 ms&lt;/td&gt;
&lt;td&gt;22.1&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt sizes&lt;/td&gt;
&lt;td&gt;long (124 tok in)&lt;/td&gt;
&lt;td&gt;2,144 ms&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;structured JSON&lt;/td&gt;
&lt;td&gt;schema required&lt;/td&gt;
&lt;td&gt;812 ms&lt;/td&gt;
&lt;td&gt;19.7&lt;/td&gt;
&lt;td&gt;0% schema valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool call&lt;/td&gt;
&lt;td&gt;function required&lt;/td&gt;
&lt;td&gt;522 ms&lt;/td&gt;
&lt;td&gt;30.7&lt;/td&gt;
&lt;td&gt;0% tool call valid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row: 103.6 seconds for sixteen tokens. Everything after it ran between 389 ms and 2.1 s. Short prompts pushed 41 tokens per second, the long one managed 7.5 because the prefill had 124 input tokens to chew before generation could start.&lt;/p&gt;

&lt;p&gt;Two of the eight requests came back flagged: structured JSON invalid, tool call invalid. At sixteen output tokens this model had no room to do either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I stopped believing the first row
&lt;/h2&gt;

&lt;p&gt;My first reading was that 103 seconds told me something about the model. To check that, I unloaded the weights and measured the load itself.&lt;/p&gt;

&lt;p&gt;I unloaded the weights (&lt;code&gt;ollama stop qwen3.5:2b&lt;/code&gt;, then &lt;code&gt;ollama ps&lt;/code&gt; to confirm nothing was resident) and ran the same single request again: twelve point six seconds, then eight point seven, then nine point seven.&lt;/p&gt;

&lt;p&gt;Same card, same model, same command, twenty minutes apart. The first measurement of the session was twelve times slower than the fastest reload, and nothing about the model changed in between. The first load pulled 2.7 GB of weights off a cold disk. By the third run the operating system had those files in the page cache and the card had less work to do.&lt;/p&gt;

&lt;p&gt;So the biggest number in that chart mostly measures how fast this disk handed those files over. The model's own contribution to it was small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The empty string
&lt;/h2&gt;

&lt;p&gt;I turned on response previews to see what the model actually said, and the preview column came back empty. I re-ran the same request with the token budget raised to 32, 64 and 200: still empty, at every budget. The provider returned usage numbers the whole time, 22 tokens in and 16 out, and the CLI computed throughput from them correctly, 19.7 tokens per second.&lt;/p&gt;

&lt;p&gt;Then the consistency row, the one with 1.0000 next to it. Two identical prompts, both answered with an empty string, compared pairwise and scored as a perfect match. That row reads like the healthiest number in the report, and it is the emptiest.&lt;/p&gt;

&lt;p&gt;Nothing failed. Eight records, zero errors, successful records: eight. Had I stopped at the executive summary I would have written down that this model produces about twenty tokens per second and I would have been describing the speed of an empty response.&lt;/p&gt;

&lt;p&gt;I still do not know why the content field was empty. Something about how this model's output arrives through the OpenAI-compatible endpoint, or the token budget. I did not chase it down. The part that mattered was already in the report, in a column I could read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool got right
&lt;/h2&gt;

&lt;p&gt;The capability flags stayed visible. &lt;code&gt;schema_valid: false&lt;/code&gt;, &lt;code&gt;tool_call_valid: false&lt;/code&gt;, &lt;code&gt;json_parse_success: false&lt;/code&gt;, each in its own column instead of folded into a pass rate. That is the difference between a report I can act on and a green tick I have to trust.&lt;/p&gt;

&lt;p&gt;It attaches to the endpoint and measures what comes back. It does not start your servers or load your models. That sounds like a missing feature until you have had a load generator restart the thing it was supposed to be measuring, mid-run, and then report the result as a number.&lt;/p&gt;

&lt;p&gt;Twelve provider presets, three of them first-class (&lt;code&gt;ollama&lt;/code&gt;, &lt;code&gt;lmstudio&lt;/code&gt;, &lt;code&gt;llama-cpp&lt;/code&gt;), then vLLM, SGLang, LocalAI, LiteLLM, TGI, text-generation-webui, Jan, MLX-LM and a generic OpenAI-compatible target. Subcommands are scriptable and the exit codes are meaningful, so this can run in CI rather than in a screenshot.&lt;/p&gt;

&lt;p&gt;Two smaller decisions that mattered more than they sound. Results are written as schema &lt;code&gt;3.0&lt;/code&gt; with a mandatory run kind, and a result file without that version is refused on load rather than silently upgraded. That refusal is what keeps a series of results a series. Previews are off by default and credential-shaped values are redacted before anything reaches disk, so turning previews on was a deliberate act on my part rather than something I found later in a JSON file.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhehiudz9rnqkcozpsg25.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhehiudz9rnqkcozpsg25.png" alt="The self-contained HTML report generated from the same run, including the two failed capability checks" width="800" height="1471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The export behind the chart near the top is in a scratch folder on my laptop. The repository ships the code and the manual, not the artifacts, and its QA directory is gitignored on purpose. The run ID and the command are in this post, so you can repeat the measurement on hardware you own, which is the only comparison worth making.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do differently now
&lt;/h2&gt;

&lt;p&gt;I read the content field before any average. OK in the status column means the request completed, not that the answer was usable, and this run is the cleanest example of that gap I have produced.&lt;/p&gt;

&lt;p&gt;The capability flags are the result, the tokens-per-second column is the footnote. My question was answered by two zeros, not by 41 tokens per second.&lt;/p&gt;

&lt;p&gt;One run is a sample of one. The same request on the same laptop covered a twelvefold range inside twenty minutes, so three runs with the model unloaded in between, median reported, is the minimum I would put in front of another person.&lt;/p&gt;

&lt;p&gt;And I stopped comparing across machines. The 25/75 CPU-GPU split is the number that explains everything above, and it exists only on this machine.&lt;/p&gt;

&lt;p&gt;The model still is not good enough for the agent I had in mind. I found that out in one command, and I have a report I can keep and diff against the next run instead of a leaderboard entry for somebody else's hardware.&lt;/p&gt;




&lt;p&gt;LLMeter is MIT licensed: &lt;a href="https://github.com/CTCycle/LLMeter-local-benchmarks" rel="noopener noreferrer"&gt;github.com/CTCycle/LLMeter-local-benchmarks&lt;/a&gt;. Release archives cover Windows, Linux and both macOS architectures with checksums. There is no crates.io package yet, so the archives or a checkout are the two routes.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rust</category>
      <category>benchmark</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Benchmarking Tokenizers Without the Guesswork</title>
      <dc:creator>Thomas Virdis</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:41:05 +0000</pubDate>
      <link>https://dev.to/ctcycle/benchmarking-tokenizers-without-the-guesswork-27pj</link>
      <guid>https://dev.to/ctcycle/benchmarking-tokenizers-without-the-guesswork-27pj</guid>
      <description>&lt;h1&gt;
  
  
  Benchmarking Tokenizers Without the Guesswork
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;What TKBEN measures, why tokenizer evaluation is deceptively hard, and how reproducible evidence beats a one-off script.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The quiet bottleneck under every model
&lt;/h2&gt;

&lt;p&gt;Every language model has a tokenizer bolted to its front door. Before the model sees a single word, the tokenizer chops the text into units, maps them to numbers, and decides, in effect, what the model is allowed to think about. That choice is not cosmetic. Two tokenizers over the same corpus can differ in vocabulary size, in how many pieces a word gets split into, in how fast they run, in how much memory they use, and in whether arbitrary text can even be reconstructed at the end of the round trip. Each of those differences changes real-world behavior: inference cost, latency, how gracefully a model handles a language it was never trained on, and how trustworthy the decoded output is.&lt;/p&gt;

&lt;p&gt;Yet tokenizers are usually the least-examined part of a model pipeline. Most benchmarks of language models treat the tokenizer as a fixed input, something to be accepted rather than evaluated, and most teams pick one the way they pick a default: it came with the model, so it will do.&lt;/p&gt;

&lt;p&gt;TKBEN (&lt;a href="https://github.com/CTCycle/TKBEN-tokenizers-benchmarker" rel="noopener noreferrer"&gt;TKBEN-tokenizers-benchmarker&lt;/a&gt;) is built on the opposite assumption. A tokenizer is a choice, and it should be examined the way you would examine any performance-critical component: on repeatable, inspectable data, not on a gut feeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the naive benchmark is not enough
&lt;/h2&gt;

&lt;p&gt;The obvious way to compare tokenizers is to run a quick script: load one, feed it some text, print a number, move on. That approach has a weakness that shows up only later. The number is gone. The person who ran it cannot reopen the evidence, cannot re-run the comparison against a different dataset, cannot show how the figure was produced, and cannot tell you whether it reflects the test set that actually matters.&lt;/p&gt;

&lt;p&gt;Tokenizers are sensitive to what you feed them. A tokenizer that looks efficient on English news text may fragment a multilingual dataset badly. A vocabulary that seems rich can still leave you with a high unknown-token rate on a domain you cared about. A fast encode time tells you nothing about latency at the tail, or memory under load. None of these come through in a single printed stat.&lt;/p&gt;

&lt;p&gt;TKBEN treats this properly. It is a local web application that keeps downloaded tokenizer assets, validation reports, and benchmark results in its own workspace, so a later session can reopen and inspect the same evidence. A benchmark becomes a saved report with provenance, re-openable weeks later, rather than a printed number that is gone the moment the script exits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept: validation before comparison
&lt;/h2&gt;

&lt;p&gt;The heart of TKBEN is a separation that most ad-hoc benchmarking collapses: you cannot compare tokenizers until you first understand the text you are going to run over them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset validation comes first.&lt;/strong&gt; You load a Hugging Face dataset, a custom identifier, or a local CSV, XLS, or XLSX file, and run it through the validation pipeline. The result is a saved report covering corpus scale and structure (document counts, lengths, and how spread out they are), lexical diversity and frequency (type-token ratio, moving-average TTR, word distributions), word and character signals, document quality signals, structural regularity, and compression and redundancy. In plain terms, TKBEN tells you what kind of text you are about to benchmark, so you are not comparing tokenizers on data you do not actually understand.&lt;/p&gt;

&lt;p&gt;Then, and only then, do you bring in the tokenizers. You scan or add tokenizer identifiers, download the assets you need, upload a custom &lt;code&gt;tokenizer.json&lt;/code&gt;, and inspect each report's vocabulary statistics and token preview before trusting it in a comparison. Once you have a validated dataset and a set of tokenizers you have actually looked at, you can build a cross-benchmark 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feovkxklhr7pgioy2icd0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feovkxklhr7pgioy2icd0.png" alt="The dataset workspace: review corpus health, lexical metrics, distributions, entropy, and word-cloud signals before benchmarking." width="800" height="1001"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison actually measures
&lt;/h2&gt;

&lt;p&gt;The benchmark result is a dashboard of widgets, each one a separate view of a tokenizer's behavior on the validated dataset. The metrics fall into six groups, and each answers a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata.&lt;/strong&gt; Vocabulary size, added-token count, special-token share. The basics of what a tokenizer carries with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency.&lt;/strong&gt; Tokens per second, characters per second, bytes per second, and the time split across encoding, dataset streaming, post-processing, and the complete run from start to finish. This is the throughput story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Median, p95, and p99 encode latency, plus the raw latency distribution. The tail, where interactive models actually feel slow, is not hidden behind an average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fidelity.&lt;/strong&gt; Whether text survives the trip. Exact token-ID round trip, normalized text round trip, unknown-token rate, byte-fallback rate, and how much character coverage the vocabulary provides. A fast tokenizer that cannot faithfully round-trip its input is not fast, it is lossy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragmentation.&lt;/strong&gt; Tokens per character, characters per token, pieces per word. A measure of how aggressively text is split, and how that shifts with word length.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources.&lt;/strong&gt; Peak RSS and memory delta. What the tokenizer costs in resident memory while it works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can switch between bar, horizontal-bar, box-plot, histogram, dot-whisker, and grouped-bar views, reorder the widgets to match how you think about the problem, and export the whole comparison as a PDF. The dashboard below compares a GPT-2 style tokenizer against RoBERTa-base over the same C4 sample, side by side, with the numbers that actually differ made visible.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvw2vuxaem9w6sl2fjtn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvw2vuxaem9w6sl2fjtn.png" alt="Cross-benchmark comparison dashboard: two tokenizers over the same dataset, across twelve metrics." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The value of a dashboard like this is not the top-of-the-fold number. It is that a memory-hungry tokenizer, a slow tail, and a fracture-prone vocabulary can all live in the same view, so a team sees the whole trade-off instead of a single ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  A report you can trust a month later
&lt;/h2&gt;

&lt;p&gt;TKBEN's core design decision is that a benchmark is a saved report, not an event. Each validation and each cross-benchmark run persists in the workspace, with a page to reopen it. Combined with the PDF export, that turns a fragile one-off script into an auditable workflow: you can show exactly which dataset, which tokenizer versions, and which metrics produced a given comparison, and you can reopen that evidence days or weeks later without re-running anything.&lt;/p&gt;

&lt;p&gt;That matters whether you are choosing a tokenizer for a new model, auditing the efficiency of an existing pipeline, or documenting a decision for a team. The report becomes the artifact you argue from, and the artifact still exists the next time somebody asks how the decision was made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokenizer assets you can really inspect
&lt;/h2&gt;

&lt;p&gt;Under the hood, a tokenizer report shows what the download actually gave you: model metadata, a vocabulary count, a token-length distribution, and a paginated preview of the tokens themselves. This is a small thing that saves real confusion. Reported and actual vocabulary sizes can drift, especially with added tokens, and downloading a model you think is one thing and inspecting another is how wrong comparisons start. TKBEN surfaces the actual token list and lets you confirm that what was downloaded is what you intended to evaluate.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9qituaml0r5cqdkj4dt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9qituaml0r5cqdkj4dt.png" alt="Tokenizer workspace: model metadata, vocabulary statistics, and a paginated token preview." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Local, and honest about it
&lt;/h2&gt;

&lt;p&gt;Like the rest of the CTCycle portfolio, TKBEN runs locally. The browser is the interface; a local FastAPI service does the processing and stores the reports. Config stays in a machine-specific, never-committed settings file, and the embedded SQLite database is managed through explicit migrations. This keeps your datasets and analysis on your machine, which matters when the data you are benchmarking is proprietary or simply not something you want to send to a cloud service. The local setting is a completeness of control, not a limitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence stays
&lt;/h2&gt;

&lt;p&gt;Most tooling treats a tokenizer as a fixed input and a benchmark as a number you print and forget. TKBEN does the opposite. It validates the text before it benchmarks, measures the trade-offs that actually differ between tokenizers, and persists the whole thing as evidence you can reopen later.&lt;/p&gt;

&lt;p&gt;If a team wants to pick a tokenizer with something better than intuition, TKBEN is the workspace to do it in. It runs on Windows through a single launcher, or on macOS and Linux with a short manual setup, and the source is open under an MIT licence. The repository is the place to start: &lt;a href="https://github.com/CTCycle/TKBEN-tokenizers-benchmarker" rel="noopener noreferrer"&gt;github.com/CTCycle/TKBEN-tokenizers-benchmarker&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of the CTCycle open-source portfolio. See the full open-source portfolio at &lt;a href="https://ctcycle.github.io/CTCycle/" rel="noopener noreferrer"&gt;ctcycle.github.io/CTCycle&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>nlp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>XREPORT: A Local-First Lab for Draft Radiology Reports</title>
      <dc:creator>Thomas Virdis</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:41:03 +0000</pubDate>
      <link>https://dev.to/ctcycle/xreport-a-local-first-lab-for-draft-radiology-reports-14fh</link>
      <guid>https://dev.to/ctcycle/xreport-a-local-first-lab-for-draft-radiology-reports-14fh</guid>
      <description>&lt;h1&gt;
  
  
  XREPORT: A Local-First Lab for Draft Radiology Reports
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;What it is, why it exists, and the design choices that make it a cautious tool instead of an ambitious one.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The job that hides inside the scan
&lt;/h2&gt;

&lt;p&gt;If you have ever watched a radiologist work a stack of chest X-rays, the first thing you notice is how much of the job is not diagnosis. A lot of it is dictation. The lungs, the cardiac silhouette, the mediastinum, the bones. Normal, unremarkable, no acute findings. The kind of report that makes up most of a working day and takes very little of a radiologist's brain to produce correctly, but still has to be written, in the right format, with the right words, every single time.&lt;/p&gt;

&lt;p&gt;That is the problem &lt;a href="https://github.com/CTCycle/XREPORT-radiological-reports-generator" rel="noopener noreferrer"&gt;XREPORT&lt;/a&gt; is aimed at, and the honest way to state it is also the narrow way. Doing radiology reporting well is exhausting, and fatigue is the last thing you want anywhere near a diagnostic decision. Automation cannot fix that fatigue, but it can carry some of the routine load so the human review happens on a less tired brain.&lt;/p&gt;

&lt;p&gt;This is not a tool that reads a scan and tells you what is wrong. It is a tool that produces a structured first draft of the report, which a qualified clinician then reads, corrects, and signs. The difference between those two jobs is the whole design. XREPORT is built to be a laboratory for making drafts, with the review step kept firmly human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local first, because the data decides
&lt;/h2&gt;

&lt;p&gt;The most important sentence in the whole project is in the repository's opening note: generated drafts are not clinically approved and require qualified independent review. The second most important is the privacy boundary. XREPORT is local-first, and for medical imaging that is not a packaging preference, it is the entire point.&lt;/p&gt;

&lt;p&gt;X-ray images are patient data under legal and ethical constraints. A tool that sends chest scans to a cloud service is a non-starter for most hospitals regardless of how accurate the model is, because the institution cannot control where the data goes or who can look at it. XREPORT never phones home. Models are downloaded once, cached in a project-local resource directory, verified, and reused offline. Training, validation, and generation all run on the machine the user controls. For clinical settings this is not a feature that needs to be argued for; it is the price of entry.&lt;/p&gt;

&lt;p&gt;That single decision shapes everything downstream. Because there is no cloud backend, there is also no silent model update, no surprise behavior change between Tuesday and Wednesday. What the user validated last month is what runs next month, unless they choose to change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not one report engine, but a choice with provenance
&lt;/h2&gt;

&lt;p&gt;XREPORT avoids locking the user into a single black box. It ships with a fixed catalogue of five pinned public Hugging Face report-generation models, and it also accepts locally trained checkpoints through its own training workflow. The pinned models are chosen because each is a known, verifiable starting point. The custom checkpoints exist because a research lab or a hospital with its own data should not be forced onto a generic model when it has spent months building one on a domain-specific set.&lt;/p&gt;

&lt;p&gt;Every generated report carries its provenance with it. The output comes back as editable Findings and Impression fields with the model, provider, and revision metadata attached, so anyone reading the draft can see where the text came from. That traceability matters in a domain where the reader needs to know whether the suggestion was produced by the model the team validated or by something else. It is the difference between a reproducible artifact and an output you cannot explain a month later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the custom model actually is
&lt;/h2&gt;

&lt;p&gt;The in-house training path treats report generation as an image-to-text problem, the same framing you would use for image captioning, but with medical language and a strict output shape. It is not a chat model asked nicely to describe an X-ray. It is a purpose-built encoder-decoder that maps an image to a token-by-token sequence of report language.&lt;/p&gt;

&lt;p&gt;The image side starts with a BEiT image encoder, a vision transformer pretrained on image patches, which turns a 224 by 224 chest X-ray into a set of visual feature vectors. Those features are projected into the shared embedding space. On the text side, a positional embedding encodes the report tokens as they are produced, and a stack of transformer encoder and decoder layers learns to attend from the image to that sequence. The default shape is four encoder layers and four decoder layers with eight attention heads and a 256-dimensional embedding, though the sizes are configuration options rather than hard constants. A softmax classifier over the vocabulary produces the next token at each step.&lt;/p&gt;

&lt;p&gt;Training uses a masked cross-entropy loss that only scores the report tokens, with a masked-accuracy metric tracking how often the model predicts the right next token at the right position. The optimizer is AdamW with an optional warmup learning-rate schedule. The important practical detail is the checkpoint lifecycle. A run can be paused, resumed from any saved state, and evaluated against held-out examples, so a team is never forced to finish a model in one sitting or trust a single untested run. The evaluation view shows loss and accuracy for a saved checkpoint before that model is allowed anywhere near real generation.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfdutm0yqje3mdd5r436.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfdutm0yqje3mdd5r436.png" alt="Checkpoint evaluation in the training view: loss, accuracy, and resume controls for a saved model state." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The five public models, and what they trade off
&lt;/h2&gt;

&lt;p&gt;Because not everyone wants to train their own model, XREPORT also ships a pinned catalogue of five public chest imaging models, each with its capabilities spelled out in the config rather than assumed. The catalogue is deliberately small and explicitly pinned to exact revisions, so a model that works today still works six months from now.&lt;/p&gt;

&lt;p&gt;At the lightweight end is &lt;strong&gt;CXRMate Multi TF&lt;/strong&gt;, a 0.1B parameter model that is the recommended default. It is designed for multi-view chest X-rays and its output is structured, with separate Findings and Impression sections. On modest hardware it is usable locally, which makes it the sensible first choice for most runs. Its sibling &lt;strong&gt;CXRMate-ED&lt;/strong&gt; is a similar small model that additionally accepts a clinical indication or history from the user and maps it into the model's context input. Same family, one richer input.&lt;/p&gt;

&lt;p&gt;Further up the scale sit two larger, more demanding models. &lt;strong&gt;CheXOne&lt;/strong&gt;, a 4B parameter vision-language model from Stanford, is capable not only of drafting reports but of reasoning and of output that can be localized back to regions of the image, so it is the entry point if grounding matters. &lt;strong&gt;CXRMate-2&lt;/strong&gt; is a 3B parameter specialist with structured output and clinical-context support, at the cost of much heavier local storage and memory. Both are far more capable than the lightweight pair, and both are far more demanding on hardware, which is exactly the trade-off a local-first tool has to surface rather than hide.&lt;/p&gt;

&lt;p&gt;Rounding out the set is &lt;strong&gt;MedGemma 1.5 4B&lt;/strong&gt;, a broader medical-imaging model that, unlike the chest specialists, is not validated only on radiographs. It produces a single raw report rather than separate structured sections, and it is the one model that also supports prior images as context. It is also gated: downloading it requires accepting Google's model terms and configuring a local token, which XREPORT notes up front rather than pretending the download is automatic.&lt;/p&gt;

&lt;p&gt;The picture that emerges is not "here is the one best model." It is a ladder of choices from a small fast default up to heavyweight research models, with their parameter sizes, licence restrictions, hardware demands, and structured-versus-raw output all visible in one place. A small radiology group on a workstation and a research lab with a GPU server look at the same screen and pick different entries on the same list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow is the concept
&lt;/h2&gt;

&lt;p&gt;XREPORT is a loop, not a single inference button. The loop is the point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dataset preparation.&lt;/strong&gt; Import an image folder plus CSV or XLSX report metadata, review how images and reports match, inspect paired records, then clean, tokenize, split, and build training-ready datasets. MIMIC-CXR is supported as a starting validation dataset, alongside custom image-report pair formats. The review step matters, because a training set built without checking that each scan is paired with the right report is quietly teaching the model wrong associations.&lt;/li&gt;
&lt;/ol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rbbsqkwiodaunj0g9z9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rbbsqkwiodaunj0g9z9.png" alt="Dataset review: a lateral chest X-ray shown next to its matched report before the dataset is built." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Training.&lt;/strong&gt; Configure CPU or GPU training, watch live metrics and logs, save checkpoints, resume, and evaluate. Model readiness and provenance warnings stay visible throughout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation.&lt;/strong&gt; Add up to sixteen study images, pick a generation profile, and submit a background job against the chosen model or checkpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; The result returns as editable Findings and Impression text with its metadata attached, ready to be copied or exported for qualified review. The screen is built around the edit, not the acceptance. Nothing is published straight from the model; the draft is handed to a person.&lt;/li&gt;
&lt;/ol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69vovz0ez7nfcefirmgg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69vovz0ez7nfcefirmgg.png" alt="The generated draft in editable Findings and Impression fields, with the generating model, provider, and revision named below." width="799" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two parts of this deserve emphasis. One is that the user can generate from up to sixteen images in a single case, which matters because a real radiological study is rarely a single view. The other is that validation is a first-class step, not an afterthought. A tool that shows a confident report with no indication of whether the model behind it was ever validated is a tool that has quietly handed the judgment to the machine. XREPORT keeps the validation state in front of the user instead of hiding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tool deliberately does less
&lt;/h2&gt;

&lt;p&gt;The temptation in a project like this is to keep pushing toward the interesting part: the idea that a model might one day make a clinical call. XREPORT is explicitly built to stop well short of that line. The value it claims is a coherent first draft that flags what a reader should look for, which is most useful on the routine cases where the findings are unremarkable. For rare or critical findings, an automated draft is only as useful as the clinician's judgment in catching what the model missed. That is why the human-in-the-loop is not a concession but the designed center of the system, and why the output is labeled a research-use draft rather than a diagnosis.&lt;/p&gt;

&lt;p&gt;Getting from local promise to usable software took real work on the delivery side too. On Windows the app runs through a single launcher that manages the portable runtimes and dependencies. A Tauri 2 desktop shell packages it as CPU and CUDA installers, so a user who needs the version built for their graphics setup has a normal installer rather than a pile of manual steps. The binaries are currently unsigned, and MSI installs are per-machine, so this is a tool that assumes a technically literate operator, which is honest about the audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The restless part under the surface
&lt;/h2&gt;

&lt;p&gt;Underneath, the application is a FastAPI backend with an Angular 22 frontend, with settings kept local and machine-specific, never committed, and the SQLite database managed through explicit migrations. None of that is the story a reader needs to remember. The reason to mention the stack at all is that it was chosen to stay boring and auditable: a report you can trace, a schema you can reason about, dependencies you can pin. In a tool that produces medical text, boring and auditable is a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design stance
&lt;/h2&gt;

&lt;p&gt;Most report-generation tools are pitched on how much they can automate. XREPORT is more honest than that, and I think that is its best quality. Its automation effort is aimed squarely at the routine load, it keeps the data local, it attaches provenance to every output, and it never pretends to own the diagnostic step. Whether you train a checkpoint or pick a pinned public model, you can see what produced the draft and how validated it was, and the final sign-off stays human.&lt;/p&gt;

&lt;p&gt;If a radiologist wants to spend less of their day on unremarkable dictations and more on the cases that actually need them, that is the version of XREPORT that exists. The first run is a research-use tool, not a clinical product, and the source is open under an MIT licence, so any team that disagrees with a design decision can take the code and change it.&lt;/p&gt;

&lt;p&gt;If that sounds like your problem, the repository is the place to start: &lt;a href="https://github.com/CTCycle/XREPORT-radiological-reports-generator" rel="noopener noreferrer"&gt;github.com/CTCycle/XREPORT-radiological-reports-generator&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of the CTCycle open-source portfolio. Models and generated drafts are not clinically approved and require qualified independent review.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>medical</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Deep Q-Network That Plays Roulette: What Happens When You Train RL on Pure Chance</title>
      <dc:creator>Thomas Virdis</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:43:52 +0000</pubDate>
      <link>https://dev.to/ctcycle/a-deep-q-network-that-plays-roulette-what-happens-when-you-train-rl-on-pure-chance-3b0a</link>
      <guid>https://dev.to/ctcycle/a-deep-q-network-that-plays-roulette-what-happens-when-you-train-rl-on-pure-chance-3b0a</guid>
      <description>&lt;h1&gt;
  
  
  A Deep Q-Network That Plays Roulette: What Happens When You Train RL on Pure Chance
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why train a DQN on a mathematically hostile environment
&lt;/h2&gt;

&lt;p&gt;European roulette carries a house edge of 2.7% on every bet. No amount of pattern recognition, trend following, or "system" playing changes the fact that the expected value of every spin is negative. It is, in the most literal sense, a stochastic process designed to drain capital over time.&lt;/p&gt;

&lt;p&gt;So why build a deep Q-network to play it?&lt;/p&gt;

&lt;p&gt;Because this is exactly what makes it a revealing reinforcement learning problem. When you train an agent in an environment where the optimal long-term strategy is simply "don't play," you get to watch a learning algorithm that desperately wants to find signal confront a data-generating process that contains none. The training dynamics reveal how DQNs behave at the boundary of detectability. That has implications far beyond the casino.&lt;/p&gt;

&lt;p&gt;This article walks through &lt;strong&gt;FAIRS&lt;/strong&gt; (Fabulous Automated Intelligent Roulette System), an open-source research application built to explore that tension. The full implementation is MIT-licensed on &lt;a href="https://github.com/CTCycle/FAIRS-Roulette-Player" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: RL in a zero-signal environment
&lt;/h2&gt;

&lt;p&gt;The classic DQN breakthrough (Mnih et al., 2015) demonstrated human-level performance on 49 Atari 2600 games. Those environments share a critical property: they contain genuine signal. Pixels correlate with game state, actions produce predictable outcomes, and a consistent policy can achieve superhuman scores.&lt;/p&gt;

&lt;p&gt;Roulette shares none of these properties. Each spin is independent and identically distributed. The wheel has no memory. Past outcomes do not inform future ones. The state transition function is:&lt;/p&gt;

&lt;p&gt;$$P(s_{t+1} \mid s_t, a_t) = P(s_{t+1})$$&lt;/p&gt;

&lt;p&gt;That is, the next state is completely independent of both the current state and the agent's action. The Q-function, which in standard RL represents the expected return of taking action $a$ in state $s$, degenerates to its immediate-reward term plus an action-independent constant:&lt;/p&gt;

&lt;p&gt;$$Q(s, a) = \mathbb{E}[R \mid a] + C$$&lt;/p&gt;

&lt;p&gt;where $C$ is the discounted expected future reward, identical for every action because future spins are independent of both the current state and the current action. The constant does not change how actions rank, so $\arg\max_a Q(s, a) = \arg\max_a \mathbb{E}[R \mid a]$. The state $s$ still carries no predictive information; the only thing that distinguishes actions is the immediate expected value, which for any roulette bet on a European wheel is $-0.027$ per unit wagered.&lt;/p&gt;

&lt;p&gt;This makes roulette a useful null-case benchmark. If an RL algorithm learns something, that something is either an artifact of variance, overfitting to noise, or a bug. Observing &lt;em&gt;how&lt;/em&gt; it fails tells us about the algorithm's inductive biases.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DQN algorithm: a refresher
&lt;/h2&gt;

&lt;p&gt;The DQN (Deep Q-Network) algorithm approximates the optimal action-value function $Q^*(s, a)$ using a neural network. The standard temporal-difference update is:&lt;/p&gt;

&lt;p&gt;$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma \max_{a'} Q(s', a') - Q(s, a) \right]$$&lt;/p&gt;

&lt;p&gt;In practice, the network is trained to minimize the mean squared Bellman error:&lt;/p&gt;

&lt;p&gt;$$\mathcal{L}(\theta) = \mathbb{E}&lt;em&gt;{(s, a, r, s') \sim \mathcal{D}} \left[ \left( r + \gamma \max&lt;/em&gt;{a'} Q(s', a'; \theta^-) - Q(s, a; \theta) \right)^2 \right]$$&lt;/p&gt;

&lt;p&gt;Where $\theta$ are the online network parameters and $\theta^-$ are target network parameters that are periodically copied from $\theta$ to stabilise training. The transitions $(s, a, r, s')$ are sampled uniformly from a replay buffer $\mathcal{D}$, breaking temporal correlations (Mnih et al., 2015).&lt;/p&gt;

&lt;p&gt;FAIRS extends this with the &lt;strong&gt;Double DQN&lt;/strong&gt; modification (Van Hasselt et al., 2016), which decouples action selection from action evaluation to reduce overestimation bias. The target becomes:&lt;/p&gt;

&lt;p&gt;$$y = r + \gamma Q(s', \arg\max_{a'} Q(s', a'; \theta); \theta^-)$$&lt;/p&gt;

&lt;p&gt;This is visible in the source code at &lt;code&gt;app/server/learning/training/agents.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;next_action_selection&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="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;best_next_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&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;next_action_selection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="n"&gt;q_futures_target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;q_future_selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q_futures_target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;best_next_actions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The online model selects the best action; the target model evaluates it.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAIRS architecture: a dual-input Q-network
&lt;/h2&gt;

&lt;p&gt;The FAIRS implementation diverges from the vanilla DQN in several design choices worth spelling out.&lt;/p&gt;

&lt;h3&gt;
  
  
  47-action space
&lt;/h3&gt;

&lt;p&gt;Unlike the common 11-action mapping found in simplified roulette simulations, FAIRS exposes 47 actions: 37 straight number bets (0-36), 9 outside bets (Red, Black, Odd, Even, Low, High, and three Dozens), and a Pass action, so 37 + 9 + 1 = 47. This is a deliberately high-dimensional action space, and most of those 47 are strictly dominated by even-money bets. Watching which actions the agent settles on is therefore more informative than it would be with a small action set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dual-input network with learned embeddings
&lt;/h3&gt;

&lt;p&gt;The Q-network (&lt;code&gt;app/server/learning/models/qnet.py&lt;/code&gt;) takes two inputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timeseries&lt;/strong&gt;: the last 64 roulette outcomes as integer tokens, fed through a &lt;code&gt;RouletteEmbedding&lt;/code&gt; layer that learns distributed representations of each number&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gain&lt;/strong&gt;: the current capital-to-initial-capital ratio as a scalar context signal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These pathways are merged via an &lt;code&gt;AddNorm&lt;/code&gt; layer (a residual connection with layer normalisation), then passed through a &lt;code&gt;QScoreNet&lt;/code&gt; head that produces Q-values for all 47 actions. The network uses &lt;code&gt;BatchNormDense&lt;/code&gt; layers with ReLU activations, 30% dropout for regularisation, and the AdamW optimiser (Loshchilov &amp;amp; Hutter, 2019).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwuu77w60ojslwkkci2o7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwuu77w60ojslwkkci2o7.png" alt="Training workspace" width="800" height="821"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The FAIRS training workspace. Dataset management on the left, checkpoint panel on the right, and the live training monitor charting rewards and loss in real time.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Reward scaling
&lt;/h3&gt;

&lt;p&gt;Raw roulette rewards range from -10 (loss) to +350 (straight-up win). The environment scales these to $[-1, 1]$ using an asymmetric transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;negative rewards: (reward + max_bet) / max_bet - 1   → maps to [-1, 0)
positive rewards: reward / (max_bet * 35)             → maps to (0, 1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents the rare +350 reward from dominating gradient updates while preserving the relative ranking of outcomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experience replay with periodic target updates
&lt;/h3&gt;

&lt;p&gt;The agent stores transitions in a deque buffer of configurable size (default 10,000). Training begins once the buffer exceeds the replay batch size (default 1,000). Every &lt;code&gt;update_frequency&lt;/code&gt; steps (default 10), the target network weights are synced to the online network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;target_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_weights&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;get_weights&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Training dynamics: what the agent actually learns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Uniform exploration
&lt;/h3&gt;

&lt;p&gt;At the start of training, epsilon is 0.75. The agent selects actions nearly at random, exploring all 47 actions roughly uniformly. The replay buffer fills with transitions from all bet types. Loss values are high and unstable because the network has no predictive structure to latch onto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Convergence to even-money bets
&lt;/h3&gt;

&lt;p&gt;As epsilon decays (default rate: 0.995 per step), the agent begins to follow its learned policy. The key finding: the Q-network converges to a strong preference for even-money bets (Red, Black, Odd, Even) regardless of the state input. This is purely statistical. The network has discovered that these actions minimise variance.&lt;/p&gt;

&lt;p&gt;The action distribution shift is the clearest signal that learning has occurred. Early in training, the 47 actions are roughly equiprobable. By the time epsilon reaches 0.1, the even-money bets account for 60-70% of action selections, while straight number bets are nearly abandoned.&lt;/p&gt;

&lt;p&gt;This is not the network "understanding" roulette. It is the network learning that the expected reward of a straight bet is -0.027 per unit but with catastrophic variance, while even-money bets deliver the same expected value with much smaller variance. The Q-network has independently discovered the variance-minimising policy within the constraints of its function approximation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: The loss never converges
&lt;/h3&gt;

&lt;p&gt;Unlike supervised learning, where loss typically decreases toward zero, DQN loss in a stochastic environment oscillates persistently. The root-mean-square error hovers around 0.5-1.0 without trending downward.&lt;/p&gt;

&lt;p&gt;This is the "moving target" problem in a purely stochastic setting. The target Q-values depend on the network's own predictions. As the network updates, the targets shift. In a non-stationary environment where the optimal Q-values are themselves noisy estimates of a negative expectation, the network never settles.&lt;/p&gt;

&lt;p&gt;Persistent non-convergence of the TD error, together with well-behaved action distributions, is a diagnostic signature. It tells you the environment has no exploitable structure. Any apparent convergence in loss on a real-world RL problem should be examined critically: are you learning genuine structure, or just memorising variance?&lt;/p&gt;

&lt;h3&gt;
  
  
  Capital evolution
&lt;/h3&gt;

&lt;p&gt;Over repeated episodes, the agent's capital follows a random walk with negative drift. Even with the "optimal" policy of exclusively playing even-money bets, expected loss is approximately 2.7% of total wager per spin. The variance can mask this edge for hundreds or even thousands of spins, creating the illusion of a winning strategy before the mathematics reasserts itself.&lt;/p&gt;

&lt;p&gt;Here is the core point: in a zero-signal environment, the best any agent can do is minimise the rate at which it loses money. The agent that converges to even-money betting has "solved" roulette in the only way that is possible for it, by losing the least.&lt;/p&gt;







&lt;h2&gt;
  
  
  Implementation nuances: where the code diverges from the textbook
&lt;/h2&gt;

&lt;p&gt;A few design choices in the FAIRS codebase illustrate the gap between textbook RL and a working implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gamma = 0.5 (myopic discounting).&lt;/strong&gt; Standard DQN implementations use Gamma = 0.95–0.99, allowing the agent to reason about long-term consequences. FAIRS uses 0.5 by default, which gives the standard effective horizon of $1/(1-\gamma)=2$ steps (the discount weight $\gamma^t$ only falls below 1% after about 7 steps, a more generous and nonstandard reading). For roulette, where the data-generating process is i.i.d., this is defensible; there are no long-term consequences to reason about because each spin is independent. A higher gamma would simply accumulate more noise. But it also means the agent cannot learn temporal patterns even if they existed, which is a deliberate constraint that simplifies interpretation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reward scaling design.&lt;/strong&gt; The asymmetric scaling function maps losses to $[-1, 0)$ and wins to $(0, 1]$, with straight-up wins (35:1) scaled to 1.0 and even-money wins scaled near 0.03. This creates an inherent bias: the network sees straight-up bets as having higher scaled expected value than their true mathematics warrants, because the rare +350 reward is compressed into the same $[0,1]$ range as smaller wins. This is a deliberate tradeoff to prevent the extreme variance of straight-up bets from dominating gradient updates, but it subtly warps the action-value landscape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frequent target network updates (every 10 steps).&lt;/strong&gt; Vanilla DQN synchronises the target network every 5,000–10,000 steps (Mnih et al., 2015). FAIRS syncs every 10 steps. This is so frequent that the stabilising benefit of a separate target network is largely negated. The motivation is practical: the agent reaches its training horizon in ~2,000 steps per episode, so a 5,000-step sync interval would never fire during a single training run. The net effect is that FAIRS behaves more like standard Q-learning with a slowly-following target than true Double DQN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchical dual-agent architecture.&lt;/strong&gt; The dynamic betting mode introduces a second DQN agent (the &lt;code&gt;StrategyAgent&lt;/code&gt;) that selects between 5 betting strategies (Martingale, Reverse Martingale, D'Alembert, Fibonacci, Keep). Both agents receive the same reward signal simultaneously, creating a non-stationary multi-agent learning problem: each agent's policy shifts the reward landscape the other observes. This is an advanced design pattern that goes beyond most DQN implementations, but its effectiveness in a zero-signal environment is questionable. Both agents are chasing noise.&lt;/p&gt;

&lt;p&gt;These observations are not criticisms of FAIRS. It is a research tool, not a production trading system. They are documented here because understanding how an implementation diverges from the textbook is often where the real learning happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implications for applied RL
&lt;/h2&gt;

&lt;p&gt;The FAIRS experiment produces takeaways that transfer to real-world RL applications:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Low-signal environments require careful validation.&lt;/strong&gt; If your agent's reward curve looks like the FAIRS training trace, with high variance, no trend, and action distributions converging to something that looks reasonable, you may be learning noise. Run a permutation test: shuffle the transitions and retrain. If the "policy" survives, it is an artifact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Action distribution analysis is more informative than reward curves.&lt;/strong&gt; In FAIRS, the reward curve is nearly unreadable. It bounces between positive and negative values for hundreds of episodes. The action distribution, by contrast, tells a clear story. When evaluating RL agents, watch &lt;em&gt;what they choose&lt;/em&gt;, not just what they earn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Double DQN modification matters in stochastic environments.&lt;/strong&gt; Van Hasselt et al. (2016) showed that DQN overestimates Q-values, particularly in noisy environments. FAIRS uses action selection from the online network with evaluation from the target network, which mitigates this. The difference is modest in a purely random environment (overestimation is bounded by the action space size) but becomes critical in environments with a real signal buried in noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the experiment yourself
&lt;/h2&gt;

&lt;p&gt;FAIRS is designed to be accessible. The project runs as a local web application with a FastAPI backend and React frontend. The launcher script handles dependency installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\start_on_windows.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The training page lets you upload or generate roulette datasets, configure hyperparameters (episodes, learning rate, epsilon schedule, network architecture), and watch the training metrics live. The inference workspace lets you load trained checkpoints and evaluate policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  What FAIRS is and is not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FAIRS is a research tool.&lt;/strong&gt; It provides a clean, instrumented environment for studying how deep Q-networks behave in purely stochastic settings. The training monitors, checkpoint system, and inference workspace are designed to make it easy to run experiments, vary hyperparameters, and observe the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAIRS is not a winning roulette system.&lt;/strong&gt; The house edge is real. No deep learning architecture, no amount of training data, and no exploration strategy can overcome the mathematics of a negative-expectation game. The project's value lies in what the training process reveals about the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAIRS is open source (MIT).&lt;/strong&gt; The entire codebase is at &lt;a href="https://github.com/CTCycle/FAIRS-Roulette-Player" rel="noopener noreferrer"&gt;github.com/CTCycle/FAIRS-Roulette-Player&lt;/a&gt;. You can fork it, extend it with new agent architectures (PPO, SAC, curiosity-driven exploration), add roulette variants, or adapt the environment to other stochastic dynamics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The roulette wheel is a useful adversary for RL research precisely because it is not trying to be beaten. It offers no pattern, adapts no strategy, and reveals no structure. Training a DQN against it is a reminder that not every problem is solvable by adding more layers. Recognising the absence of signal is itself a form of learning.&lt;/p&gt;

&lt;p&gt;The full source code, including unit tests, end-to-end tests, and a pre-configured development environment, is available on GitHub. Contributions, experiments, and open issues are welcome.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mnih, V., et al. (2015). Human-level control through deep reinforcement learning. &lt;em&gt;Nature&lt;/em&gt;, 518(7540), 529-533.&lt;/li&gt;
&lt;li&gt;Van Hasselt, H. (2010). Double Q-learning. &lt;em&gt;Advances in Neural Information Processing Systems 23 (NeurIPS)&lt;/em&gt;, 2613-2621. The original Double Q-learning paper, which used a roulette game as an evaluation environment.&lt;/li&gt;
&lt;li&gt;Van Hasselt, H., Guez, A., &amp;amp; Silver, D. (2016). Deep reinforcement learning with double Q-learning. &lt;em&gt;Proceedings of the AAAI Conference on Artificial Intelligence&lt;/em&gt;, 30(1).&lt;/li&gt;
&lt;li&gt;Loshchilov, I., &amp;amp; Hutter, F. (2019). Decoupled weight decay regularization. &lt;em&gt;International Conference on Learning Representations (ICLR)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;FAIRS source code: &lt;a href="https://github.com/CTCycle/FAIRS-Roulette-Player/tree/main/app/server/learning" rel="noopener noreferrer"&gt;&lt;code&gt;app/server/learning/&lt;/code&gt;&lt;/a&gt; (MIT License).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built by CTCycle. ML researcher and data scientist working on clinical AI, RL agents, and scientific computing tools. Find me at &lt;a href="https://github.com/CTCycle" rel="noopener noreferrer"&gt;github.com/CTCycle&lt;/a&gt; and on Dev.to at &lt;a href="https://dev.to/@ctcycle"&gt;@ctcycle&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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