<?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>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%2Fl2rce2guh72x18cry8ob.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%2Fl2rce2guh72x18cry8ob.png" alt="The dataset workspace: review corpus health, lexical metrics, distributions, entropy, and word-cloud signals before benchmarking." width="800" height="975"&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%2Fv3u9ltr8nu27cmrp9vtx.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%2Fv3u9ltr8nu27cmrp9vtx.png" alt="Cross-benchmark comparison dashboard: two tokenizers over the same dataset, across twelve metrics." width="800" height="1066"&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%2Fvgswpbbmkmji2wl92lsn.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%2Fvgswpbbmkmji2wl92lsn.png" alt="Tokenizer workspace: model metadata, vocabulary statistics, and a paginated token preview." width="800" height="802"&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;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%2Fqw69hhfapse31cnkiung.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%2Fqw69hhfapse31cnkiung.png" alt="Settings: local runtime, port, logging, and integration controls." width="799" height="544"&gt;&lt;/a&gt;&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%2Fat5jr4xqo4sdnosdl7j1.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%2Fat5jr4xqo4sdnosdl7j1.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%2Fkstlb2rku75zijbsbl0y.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%2Fkstlb2rku75zijbsbl0y.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%2F2mi6j9uv1n6xujstivf6.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%2F2mi6j9uv1n6xujstivf6.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%2Fnou51zhwn0pad46ltqzg.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%2Fnou51zhwn0pad46ltqzg.png" alt="Training workspace" width="800" height="835"&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>
