<?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: nlp</title>
    <description>The latest articles tagged 'nlp' on DEV Community.</description>
    <link>https://dev.to/t/nlp</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/nlp"/>
    <language>en</language>
    <item>
      <title>Day 36: Fine-Tuning BERT for Downstream Tasks</title>
      <dc:creator>Priyesh Dave</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:55:38 +0000</pubDate>
      <link>https://dev.to/priyeshdave6/day-36-fine-tuning-bert-for-downstream-tasks-4m4e</link>
      <guid>https://dev.to/priyeshdave6/day-36-fine-tuning-bert-for-downstream-tasks-4m4e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Previously, on Day 35:&lt;/strong&gt; Explained how BERT uses bidirectional transformer encoders and masked language modeling to create contextualized word representations, enabling state-of-the-art understanding and prediction in NLP tasks. Introduced the concepts of tokenization, attention, fine-tuning, and provided a code example for masked token prediction.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Fine-Tune BERT?
&lt;/h3&gt;

&lt;p&gt;Fine-tuning BERT means adapting a large, pretrained language model to solve a specific problem. For example, you might want BERT to tell if a movie review is positive or negative, extract place names from news articles, or answer questions about a paragraph.&lt;/p&gt;

&lt;p&gt;This is not the same as training a language model from the ground up. Training from scratch means starting with random weights and teaching a model how language works. Fine-tuning, by contrast, starts with a model that already understands general language patterns and vocabulary. You only nudge its knowledge toward your specific task.&lt;/p&gt;

&lt;p&gt;Pretraining a model like BERT is expensive—it requires vast text datasets and powerful hardware for days or weeks. Fine-tuning is much lighter. You only need a task-specific dataset, which is usually much smaller, and the process takes hours or less. The pretrained BERT already "knows language"; you just show it how to use that knowledge for your problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Downstream Tasks
&lt;/h3&gt;

&lt;p&gt;A downstream task is any real-world language problem you want BERT to solve. Think of these tasks as the jobs you give BERT once it finishes general pretraining.&lt;/p&gt;

&lt;p&gt;Some concrete examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text Classification:&lt;/strong&gt; Decide if a message is spam or not, or if a review is positive or negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentence Pair Classification:&lt;/strong&gt; Given two sentences, predict if one entails the other (natural language inference), or if they mean the same thing (paraphrase detection).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named Entity Recognition (NER):&lt;/strong&gt; Find and categorize mentions of people, places, or organizations in text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pretraining focuses on general language—predicting missing words or the next sentence. Downstream tasks use labeled data—examples where you know the right answer—to teach BERT a new skill.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Fine-Tuning Works: High-Level Steps
&lt;/h3&gt;

&lt;p&gt;Fine-tuning BERT for a downstream task follows a clear recipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Load a pretrained BERT model.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a classification "head":&lt;/strong&gt; Attach a small, task-specific output layer. For binary tasks, this is often just one linear layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gather task-specific data:&lt;/strong&gt; Use labeled examples pairing text with the correct answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train with supervision:&lt;/strong&gt; Run a standard training loop. Compute loss comparing model outputs to the true labels. Update the model’s parameters—including BERT’s existing weights—to reduce that loss.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During this process, BERT adapts its language knowledge to fit your task. The only new parameters are the small classification head. Most of the model remains unchanged, but its deep understanding of language is steered toward your problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fine-Tuning BERT for Text Classification: A Minimal Example
&lt;/h3&gt;

&lt;p&gt;We’ll fine-tune BERT to classify movie reviews as positive or negative. We'll use the Hugging Face Transformers library, which simplifies the process.&lt;/p&gt;

&lt;p&gt;First, install the required libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;torch transformers datasets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Example Code
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BertForSequenceClassification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BertTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Trainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TrainingArguments&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dataset&lt;/span&gt;

&lt;span class="n"&gt;model_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bert-base-uncased&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BertForSequenceClassification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BertTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load a small sample of the IMDb movie reviews dataset
&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;train[:2%]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Small subset for speed
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attention_mask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;training_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TrainingArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_train_epochs&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;per_device_train_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;evaluation_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;epoch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logging_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;save_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logging_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;no_cuda&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Remove if you have a GPU
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Trainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;training_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;eval_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Use a small slice for evaluation
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eval loss: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eval_loss&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eval_accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load pretrained BERT and its matching tokenizer.&lt;/li&gt;
&lt;li&gt;Fetch a small subset of IMDb reviews, each labeled as positive (1) or negative (0).&lt;/li&gt;
&lt;li&gt;Tokenize the reviews. This converts raw text into input IDs BERT can process.&lt;/li&gt;
&lt;li&gt;Use Hugging Face’s &lt;code&gt;Trainer&lt;/code&gt; to handle batching, loss calculation, and parameter updates.&lt;/li&gt;
&lt;li&gt;Fine-tuning adjusts both BERT and the attached head using your labeled examples.&lt;/li&gt;
&lt;li&gt;Evaluate on a small held-out set to check results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For real-world use, split data into proper train and validation sets, and train longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding Common Fine-Tuning Pitfalls
&lt;/h3&gt;

&lt;p&gt;Fine-tuned models often show big improvements on your labeled data, but you need to watch out for problems common in transfer learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overfitting:&lt;/strong&gt; The model does well on training data, but poorly on new data. Fix by using more data, applying regularization, or stopping training earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Rate Problems:&lt;/strong&gt; With BERT, training often works best with a low learning rate—commonly 2e-5 to 5e-5. Too high, and the model unlearns its language knowledge. Too low, and updates are slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad Labels:&lt;/strong&gt; If your labeled data is messy or incorrect, your model’s predictions will be too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization Issues:&lt;/strong&gt; Make sure text is preprocessed to match what BERT expects. Truncate or pad consistently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Limits:&lt;/strong&gt; Fine-tuning uses memory. If you run out of RAM or GPU memory, lower your batch size or sequence length.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track both training and validation loss/accuracy.&lt;/li&gt;
&lt;li&gt;Always evaluate on a separate validation set, not just training data.&lt;/li&gt;
&lt;li&gt;Save model checkpoints throughout training.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fine-tuning BERT unlocks strong performance on a wide range of NLP tasks. With just a bit of labeled data and clear attention to the process, you can adapt state-of-the-art language models for your own problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fine-tuning adapts pretrained BERT for specific tasks using small, labeled datasets.&lt;/li&gt;
&lt;li&gt;Pretraining BERT is resource-intensive, but fine-tuning is lightweight and task-focused.&lt;/li&gt;
&lt;li&gt;Common downstream tasks include text classification, sentence pair classification, and named entity recognition.&lt;/li&gt;
&lt;li&gt;A classification head is added on top of BERT for prediction during fine-tuning.&lt;/li&gt;
&lt;li&gt;Careful data handling, low learning rates, and validation are crucial to avoid overfitting or training issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Download a small labeled sentiment dataset (like a CSV with a handful of sentences and their labels). Fine-tune a BERT model for sentiment classification using the Hugging Face Transformers example code. After training, test the model on a few custom sentences of your own to observe how its predictions change after fine-tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/watch?v=BCCIExmPYqI" rel="noopener noreferrer"&gt;Adapting to downstream tasks | fine‑tuning &amp;amp; question answering with BERT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;a href="https://huggingface.co/docs/transformers/training" rel="noopener noreferrer"&gt;Fine‑tuning · Hugging Face&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;a href="https://www.tensorflow.org/official_models/fine_tuning_bert" rel="noopener noreferrer"&gt;Fine‑tuning a BERT model (TensorFlow Model Garden)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Coming up on Day 37:&lt;/strong&gt; GPT Architecture: Decoder-Only Transformers&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>genai</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>I Thought Extracting Dates Was Easy Until I Built an On-Device iOS App</title>
      <dc:creator>osman reşat doğrul</dc:creator>
      <pubDate>Wed, 14 Oct 2026 08:56:15 +0000</pubDate>
      <link>https://dev.to/ord/i-thought-extracting-dates-was-easy-until-i-built-an-on-device-ios-app-3in9</link>
      <guid>https://dev.to/ord/i-thought-extracting-dates-was-easy-until-i-built-an-on-device-ios-app-3in9</guid>
      <description>&lt;p&gt;I started building an iPhone app to help identify tasks and follow-ups hidden inside everyday messages.&lt;/p&gt;

&lt;p&gt;Appointments, payments, delivery notices, things someone asks you to do by Friday. The kind of messages you read, understand, and then sometimes forget about.&lt;/p&gt;

&lt;p&gt;At first, I thought extracting dates and finding actionable text would be the hardest part.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a date isn't the same as understanding it
&lt;/h2&gt;

&lt;p&gt;Consider this message:&lt;/p&gt;

&lt;p&gt;"Your appointment is on October 15. Please confirm by October 12."&lt;/p&gt;

&lt;p&gt;Two dates, but two different purposes.&lt;/p&gt;

&lt;p&gt;October 15 describes an event. October 12 is the deadline for an action.&lt;/p&gt;

&lt;p&gt;A date extractor can identify both correctly and still fail to understand what matters.&lt;/p&gt;

&lt;p&gt;In NLP terms, extracting dates is only the first step. The harder part is identifying the intended action and understanding how each date relates to it.&lt;/p&gt;

&lt;p&gt;A useful interpretation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Appointment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confirm attendance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_deadline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Conceptual example only — not an actual Reply Radar model response or internal data schema.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The difficult part isn't producing structured data. It's making sure the structure reflects what the message actually means.&lt;/p&gt;

&lt;p&gt;And then there's another problem: not every message actually requires the recipient to do something.&lt;/p&gt;

&lt;p&gt;A delivery confirmation, a completed payment notification, and a request for payment might contain similar words. But they shouldn't all become tasks.&lt;/p&gt;

&lt;p&gt;That's where I started spending more time than I expected: distinguishing useful information from something that actually needs attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with Turkish
&lt;/h2&gt;

&lt;p&gt;I began with Turkish, my native language, and most of my testing has been in Turkish.&lt;/p&gt;

&lt;p&gt;One interesting part of Turkish is how much meaning can fit into a single word.&lt;/p&gt;

&lt;p&gt;Take "konuşuruz," roughly meaning "we'll talk."&lt;/p&gt;

&lt;p&gt;Depending on the context, it might suggest a future conversation. Or it might just be a polite way to end one.&lt;/p&gt;

&lt;p&gt;Would you create a reminder from that?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;The same problem exists in English, but the expressions and grammatical structures differ.&lt;/p&gt;

&lt;p&gt;Reply Radar can analyze English messages too. The challenge isn't simply adding English support. It's making the interpretation reliable across languages and different ways people express intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I chose on-device processing
&lt;/h2&gt;

&lt;p&gt;Messages often contain information people would rather keep private.&lt;/p&gt;

&lt;p&gt;That's why I wanted users to decide which message text or screenshot to share with the app.&lt;/p&gt;

&lt;p&gt;The analysis runs on the iPhone. Raw message content isn't sent to a server for analysis, and the app doesn't monitor conversations in the background.&lt;/p&gt;

&lt;p&gt;I'm relying heavily on Apple's built-in on-device capabilities.&lt;/p&gt;

&lt;p&gt;But keeping processing on the device doesn't automatically make language understanding easy.&lt;/p&gt;

&lt;p&gt;Recognizing the words is one thing.&lt;/p&gt;

&lt;p&gt;Understanding whether those words represent an unfinished task is another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking about Apple's evolving AI models
&lt;/h2&gt;

&lt;p&gt;Apple's on-device AI developments, including the evolution of Foundation Models in iOS 27, are interesting for developers working on problems like this.&lt;/p&gt;

&lt;p&gt;But I see a few challenges worth paying attention to.&lt;/p&gt;

&lt;p&gt;What happens when a model interprets a deadline as an event date?&lt;/p&gt;

&lt;p&gt;How should an app respond when the context is ambiguous?&lt;/p&gt;

&lt;p&gt;What happens when a language has different levels of model support?&lt;/p&gt;

&lt;p&gt;And how do we keep the results predictable as the underlying models change?&lt;/p&gt;

&lt;p&gt;These are questions I'm continuing to explore, not problems I consider fully solved.&lt;/p&gt;

&lt;p&gt;In my experience, getting a model to return a plausible answer is only the beginning. Deciding whether that answer represents the user's actual intention is much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small problem, but an interesting one
&lt;/h2&gt;

&lt;p&gt;The app I've been building is called Reply Radar.&lt;/p&gt;

&lt;p&gt;It's not intended to replace a calendar or a full task manager.&lt;/p&gt;

&lt;p&gt;The goal is narrower: help people notice the tasks, dates, and follow-ups that might otherwise remain buried in messages.&lt;/p&gt;

&lt;p&gt;I'm still learning where this approach works well and where it needs improvement.&lt;/p&gt;

&lt;p&gt;What surprised me most was how quickly a fairly small productivity idea turned into an exercise in language, context, and reliable on-device analysis.&lt;/p&gt;

&lt;p&gt;And that's probably what has made the project worth continuing.&lt;/p&gt;




&lt;p&gt;For anyone curious about the project, I've put together a small website explaining what Reply Radar does: &lt;a href="https://celadon-gnome-bb3c1b.netlify.app/" rel="noopener noreferrer"&gt;Reply Radar — Project Website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Writing note: This article was prepared with AI writing assistance and is based on my experience developing and testing Reply Radar.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>nlp</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>What Changes When AI Text Is Humanized: 138 Pairs, Measured</title>
      <dc:creator>Emir Vatric</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:34:06 +0000</pubDate>
      <link>https://dev.to/emir_vatric4/what-changes-when-ai-text-is-humanized-138-pairs-measured-2d2d</link>
      <guid>https://dev.to/emir_vatric4/what-changes-when-ai-text-is-humanized-138-pairs-measured-2d2d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the ToHuman blog: &lt;a href="https://tohuman.io/blog/what-changes-when-ai-text-is-humanized-2026" rel="noopener noreferrer"&gt;https://tohuman.io/blog/what-changes-when-ai-text-is-humanized-2026&lt;/a&gt; — that page carries the dataset JSON-LD and is the canonical version.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read this first, because it changes what the numbers mean.&lt;/strong&gt; The texts below are&lt;br&gt;
not our customers' texts. They are a corpus we wrote ourselves, put through the same&lt;br&gt;
production endpoint a paying user hits. We did it that way because our own Terms do&lt;br&gt;
not permit the other version — the detail is in the method, and it is the second&lt;br&gt;
finding of this piece, not a disclaimer at the bottom.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one question nobody has measured
&lt;/h2&gt;

&lt;p&gt;Every study of machine-written prose compares two categories. Human text on one&lt;br&gt;
side, AI text on the other — em-dash counts, burstiness, perplexity, the vocabulary&lt;br&gt;
that models over-reach for. There are dozens of these. They are the basis of every&lt;br&gt;
detector pitch and every "how to spot AI writing" listicle.&lt;/p&gt;

&lt;p&gt;There is a third category and almost no public measurement of it: the &lt;strong&gt;humanized&lt;br&gt;
pair&lt;/strong&gt; — one text, before and after a rewrite that was asked to make it read like a&lt;br&gt;
person wrote it. Not AI vs human. AI vs the-same-AI-text-rewritten. Nobody outside&lt;br&gt;
the small set of companies that run these rewrites holds that pair, and as far as we&lt;br&gt;
can find, none of them has published the distributions.&lt;/p&gt;

&lt;p&gt;So we measured ours. 138 English before/after pairs across two corpora, scored with&lt;br&gt;
the same word lists and thresholds that power our public&lt;br&gt;
&lt;a href="https://tohuman.io/ai-tells-scanner" rel="noopener noreferrer"&gt;AI Tells Scanner&lt;/a&gt; — no new regexes invented for the occasion.&lt;/p&gt;

&lt;p&gt;Three things are worth knowing before the tables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The median AI-tell density goes from roughly 12–14 events per 1,000 words to &lt;strong&gt;0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For about a third of the texts there was &lt;strong&gt;nothing there to move in the first place&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Sentence rhythm gets burstier in roughly two thirds of texts and &lt;strong&gt;flatter in
roughly three in ten&lt;/strong&gt; — a failure direction large enough that an average would hide it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No AI detector was run. Not one. What that excludes is spelled out below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;N.&lt;/strong&gt; 138 English pairs, in two corpora scored separately because they disagree on&lt;br&gt;
magnitude and we would rather publish the disagreement than average it away.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Corpus A&lt;/th&gt;
&lt;th&gt;Corpus B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rewritten&lt;/td&gt;
&lt;td&gt;2026-09-14&lt;/td&gt;
&lt;td&gt;2026-09-24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records in file&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excluded — not a completed rewrite&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8 (rejected by our own quality rails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact-duplicate inputs removed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairs&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;English pairs (headline set)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language split&lt;/td&gt;
&lt;td&gt;en 66 · es 5 · fr 5&lt;/td&gt;
&lt;td&gt;en 72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median input length (words, all pairs)&lt;/td&gt;
&lt;td&gt;77.5&lt;/td&gt;
&lt;td&gt;49.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length range (words, all pairs)&lt;/td&gt;
&lt;td&gt;18 – 861&lt;/td&gt;
&lt;td&gt;26 – 247&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Window.&lt;/strong&gt; Both corpora were rewritten in September 2026, ten days apart, by the&lt;br&gt;
production endpoint at &lt;code&gt;intensity=medium&lt;/code&gt;, multi-pass. The model and pipeline config&lt;br&gt;
may differ between the two runs and from today's production. That is a real&lt;br&gt;
limitation, not a hedge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exclusions, all of them.&lt;/strong&gt; Eight corpus-B records never completed a rewrite — they&lt;br&gt;
were refused by our own quality rails, so they are out of the headline. (A 10%&lt;br&gt;
rejection rate on a corpus we wrote is itself a product signal; we are not filing it&lt;br&gt;
as data cleaning.) Four other bench runs were rejected outright because &lt;code&gt;output ==&lt;br&gt;
input&lt;/code&gt; on every row — input-echo artefacts, not rewrites. Two further runs had real&lt;br&gt;
pairs but ran on a local build rather than production, so they are out of scope. The&lt;br&gt;
ten Spanish and French pairs are excluded from every headline, because every slop and&lt;br&gt;
cliché list in the scanner is English: they score 0.0 both before &lt;em&gt;and&lt;/em&gt; after, which&lt;br&gt;
means "not measured," not "clean."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregate only.&lt;/strong&gt; No text is reproduced anywhere in this piece or in the published&lt;br&gt;
raw data. No per-item or per-account figures. Every number is a distribution — median,&lt;br&gt;
interquartile range, and the share of pairs moving each direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the corpus is synthetic, verbatim.&lt;/strong&gt; Before computing a single statistic we read&lt;br&gt;
our own live policies. The decisive line, from Terms §6:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We process your text solely to provide the humanization service and do not use&lt;br&gt;
your content to train our models."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;solely&lt;/code&gt; is an exhaustive-purpose limitation, and computing style statistics for a&lt;br&gt;
public report is not providing the service to the person who submitted the text. The&lt;br&gt;
only research-shaped permission in the privacy policy is for &lt;em&gt;"aggregate, anonymized&lt;br&gt;
usage statistics (never individual content)"&lt;/em&gt; — and our method requires reading&lt;br&gt;
individual content to produce each per-pair measurement, even though only aggregates&lt;br&gt;
get published. So the verdict was recorded as &lt;strong&gt;NOT PERMITTED → synthetic corpus&lt;br&gt;
used&lt;/strong&gt;, and from the source report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is not first-party user data. It is our own synthetic corpus run through the&lt;br&gt;
production endpoint, used because our own Terms and Privacy Policy do not permit the&lt;br&gt;
first-party version. […] Real user text differs in genre mix, length, language, prompt&lt;br&gt;
style and messiness. Treat every figure as 'what the production model does to text&lt;br&gt;
we wrote,' not 'what it does to what customers submit.'"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No production query against the stored input or output columns was run at any point.&lt;br&gt;
The thing this substitution costs us is specific and worth naming: there is no&lt;br&gt;
api / playground / UI split in what follows, because that dimension only exists in the&lt;br&gt;
table we did not query.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Rates are per 1,000 words. &lt;code&gt;down / flat / up&lt;/code&gt; is the share of pairs moving that&lt;br&gt;
direction, with a small dead band (0.1 per 1,000 words; 0.01 for the CV) so that&lt;br&gt;
4.01 → 4.00 counts as flat rather than as an improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composite AI-tell density — the one that moves
&lt;/h3&gt;

&lt;p&gt;The composite is the plain sum of four scanner rate checks: slop vocabulary, cliché&lt;br&gt;
phrases, em dashes, three-item lists. It is our own construction, unweighted and&lt;br&gt;
untrained — a different weighting would move the failure shares later in this piece.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median in&lt;/th&gt;
&lt;th&gt;IQR in&lt;/th&gt;
&lt;th&gt;median out&lt;/th&gt;
&lt;th&gt;IQR out&lt;/th&gt;
&lt;th&gt;Δ median&lt;/th&gt;
&lt;th&gt;down&lt;/th&gt;
&lt;th&gt;flat&lt;/th&gt;
&lt;th&gt;up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A (n=66)&lt;/td&gt;
&lt;td&gt;12.26&lt;/td&gt;
&lt;td&gt;0 – 16.05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 – 6.51&lt;/td&gt;
&lt;td&gt;−4.51&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30.3%&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B (n=72)&lt;/td&gt;
&lt;td&gt;13.68&lt;/td&gt;
&lt;td&gt;0 – 44.40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 – 17.63&lt;/td&gt;
&lt;td&gt;−2.09&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40.3%&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the IQRs, not just the medians. The lower quartile is 0 in both corpora going in —&lt;br&gt;
a median that moves 12.26 → 0 is partly a statement about how &lt;em&gt;concentrated&lt;/em&gt; the tells&lt;br&gt;
were, and not only about how hard the rewrite worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cliché phrases — the single most-moved measure in the study
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median in&lt;/th&gt;
&lt;th&gt;IQR in&lt;/th&gt;
&lt;th&gt;median out&lt;/th&gt;
&lt;th&gt;IQR out&lt;/th&gt;
&lt;th&gt;Δ median&lt;/th&gt;
&lt;th&gt;down&lt;/th&gt;
&lt;th&gt;flat&lt;/th&gt;
&lt;th&gt;up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A (n=66)&lt;/td&gt;
&lt;td&gt;2.15&lt;/td&gt;
&lt;td&gt;0 – 11.88&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 0&lt;/td&gt;
&lt;td&gt;−1.60&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;59.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40.9%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B (n=72)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 1.15&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;22.2%&lt;/td&gt;
&lt;td&gt;73.6%&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;59.1% of corpus-A pairs shed cliché density and &lt;strong&gt;none gained any&lt;/strong&gt;. This is the&lt;br&gt;
cleanest directional result anywhere in the data, and it is also the narrowest: corpus&lt;br&gt;
B's inputs barely contained clichés, so there was nothing to remove.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slop vocabulary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median in&lt;/th&gt;
&lt;th&gt;IQR in&lt;/th&gt;
&lt;th&gt;median out&lt;/th&gt;
&lt;th&gt;IQR out&lt;/th&gt;
&lt;th&gt;down&lt;/th&gt;
&lt;th&gt;flat&lt;/th&gt;
&lt;th&gt;up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A (n=66)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 0&lt;/td&gt;
&lt;td&gt;18.2%&lt;/td&gt;
&lt;td&gt;78.8%&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B (n=72)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 30.80&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 – 0&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;61.1%&lt;/td&gt;
&lt;td&gt;5.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Corpus B's upper quartile falls from 30.80 to 0 — the slop reduction is real, but it&lt;br&gt;
lives in the upper tail of texts that had slop to lose: 33.3% of pairs moved down and&lt;br&gt;
61.1% had nothing to move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sentence rhythm (coefficient of variation) — up is the intended direction
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median in&lt;/th&gt;
&lt;th&gt;IQR in&lt;/th&gt;
&lt;th&gt;median out&lt;/th&gt;
&lt;th&gt;IQR out&lt;/th&gt;
&lt;th&gt;Δ median&lt;/th&gt;
&lt;th&gt;up&lt;/th&gt;
&lt;th&gt;flat&lt;/th&gt;
&lt;th&gt;down&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A (n=39)&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;td&gt;0.29 – 0.52&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;td&gt;0.36 – 0.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.04&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.1%&lt;/td&gt;
&lt;td&gt;25.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B (n=30)&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;0.24 – 0.43&lt;/td&gt;
&lt;td&gt;0.41&lt;/td&gt;
&lt;td&gt;0.31 – 0.47&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.03&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the n. This is measured on 39 of 66 and 30 of 72 pairs, because the scanner&lt;br&gt;
refuses to judge rhythm below five sentences. The CV figures describe the &lt;em&gt;longer&lt;/em&gt; half&lt;br&gt;
of the corpus and nothing else. The down column gets its own section below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Length — humanized text comes back slightly longer
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median word count in&lt;/th&gt;
&lt;th&gt;median out&lt;/th&gt;
&lt;th&gt;median length Δ&lt;/th&gt;
&lt;th&gt;IQR of Δ&lt;/th&gt;
&lt;th&gt;longer&lt;/th&gt;
&lt;th&gt;shorter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A (n=66)&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+3.23%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−3.49% – +10.02%&lt;/td&gt;
&lt;td&gt;62.1%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B (n=72)&lt;/td&gt;
&lt;td&gt;49.5&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+5.03%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−2.00% – +14.27%&lt;/td&gt;
&lt;td&gt;68.1%&lt;/td&gt;
&lt;td&gt;30.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are budgeting word count, API spend, or a fixed-length slot, plan for the&lt;br&gt;
rewrite to add a few percent. Mean sentence length barely moves either way (A: 17.05 →&lt;br&gt;
17.80 words; B: 15.50 → 16.25), with pairs split roughly in half on direction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paragraph structure — never touched, 138 of 138
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;pairs&lt;/th&gt;
&lt;th&gt;paragraph count changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus A&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus B&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A clean negative result and the most practically useful one in the set: the rewrite&lt;br&gt;
works inside the shape you give it. Opener repetition, measurable on the 18 corpus-A&lt;br&gt;
pairs with three or more paragraphs, also did not move (2 in, 2 out); corpus B has no&lt;br&gt;
multi-paragraph items, so it is unmeasurable there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What barely changed, or got worse
&lt;/h2&gt;

&lt;p&gt;Same prominence as the section above, because these are results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Corpus A&lt;/th&gt;
&lt;th&gt;Corpus B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English pairs scored&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inputs with a composite of 0 — nothing for these checks to fix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 (30.3%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28 (38.9%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairs where a % change in the composite is even definable&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Barely moved&lt;/strong&gt; (|Δ| / composite in &amp;lt; 5%)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 of 46 — 4.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 of 44 — 11.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Got worse&lt;/strong&gt; (composite rose)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 of 66 — 1.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 of 72 — 9.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-tell inputs that &lt;em&gt;acquired&lt;/em&gt; tells&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Got less bursty&lt;/strong&gt; (sentence rhythm flattened)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11 of 39 — 28.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 of 30 — 30.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest headline is the range, not the friendlier end: the statistics got &lt;strong&gt;worse&lt;br&gt;
for 1.5% to 9.7% of texts, and barely moved for another 4% to 11%&lt;/strong&gt; — and for about&lt;br&gt;
three in ten, the rewrite flattened the sentence rhythm it was supposed to vary.&lt;/p&gt;

&lt;p&gt;Then there is the result that matters most and flatters us least.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Corpus A&lt;/th&gt;
&lt;th&gt;Corpus B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Texts the scanner flagged on ≥1 pattern, before&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Texts the scanner still flags on ≥1 pattern, after&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleared&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 of 24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 of 27&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On our own scanner's conservative thresholds, &lt;strong&gt;most text that read as machine-written&lt;br&gt;
still reads that way afterwards.&lt;/strong&gt; The density improves for the majority of pairs; the&lt;br&gt;
verdict usually does not flip. Those two sentences are both true at the same time, and&lt;br&gt;
any version of this report that published only the first one would be dishonest.&lt;/p&gt;

&lt;p&gt;Two more measures we are reporting as &lt;strong&gt;untested rather than unchanged&lt;/strong&gt;, because the&lt;br&gt;
corpus did not contain enough of them to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Em dashes.&lt;/strong&gt; 98.5% of corpus-A pairs and 90.3% of corpus-B pairs had no em-dash
change, because there were almost no em dashes going in. We cannot claim the model
removes them. The most-repeated tell in the entire genre, and we have nothing on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contractions.&lt;/strong&gt; 86.4% and 65.3% flat, same reason. We cannot claim the model adds them.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What this does and does not say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No AI detector was run.&lt;/strong&gt; Not GPTZero, not Originality, not Turnitin, nothing. Every&lt;br&gt;
number above is a &lt;em&gt;style&lt;/em&gt; measurement taken with our own published word lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Style change is not a detection outcome.&lt;/strong&gt; The two are not known to be the same axis&lt;br&gt;
and we did not test the link. Nothing in this report supports a claim about detection,&lt;br&gt;
"bypassing," or "undetectability" — if you arrived looking for evidence that rewriting&lt;br&gt;
text defeats a detector, this study does not contain it, and we would rather say so&lt;br&gt;
plainly than let the composite table imply it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is not user data.&lt;/strong&gt; See the method. It is our own corpus through the production&lt;br&gt;
endpoint, because our policies do not permit the first-party version. Real submissions&lt;br&gt;
differ in genre, length, language, prompt style and messiness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n is small&lt;/strong&gt; — 66 and 72 English pairs — and the two corpora disagree on magnitude.&lt;br&gt;
Where they disagree we have published both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The composite is ours.&lt;/strong&gt; Nobody else uses it. It is a plain sum of four published&lt;br&gt;
per-1,000-word rates, and a different weighting would move the "barely moved" and "got&lt;br&gt;
worse" shares. The inputs are listed in the appendix so you can re-weight them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-English is not measured at all.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;If you write.&lt;/strong&gt; Three findings are directly actionable. Clichés are what a rewrite&lt;br&gt;
reliably removes, so they are also the thing worth removing yourself first. Paragraph&lt;br&gt;
structure survives untouched, so the shape you hand over is the shape you get back.&lt;br&gt;
And output runs 3–5% longer at the median — budget for it. ToHuman is the rewrite tool&lt;br&gt;
these pairs came from, and the honest scope of what it rewrites is &lt;em&gt;prose&lt;/em&gt;: body copy,&lt;br&gt;
email, documentation, and for anything code-shaped, the comments, docstrings, READMEs,&lt;br&gt;
commit messages and PR descriptions around the code — never the code itself. We&lt;br&gt;
measured that boundary separately and wrote up where it holds and where it breaks in&lt;br&gt;
&lt;a href="https://tohuman.io/blog/can-you-humanize-ai-generated-code" rel="noopener noreferrer"&gt;can you humanize AI-generated code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you edit.&lt;/strong&gt; The verdict table is the one to keep. A tool can cut measured tell&lt;br&gt;
density by most of its range and still leave text that trips the same pattern checks —&lt;br&gt;
5 of 24 and 9 of 27 cleared. Treat a rewrite as a first pass, not a finish line, and&lt;br&gt;
re-read for the specific thing you were worried about. Our comparison of the tools in&lt;br&gt;
this category, including what each one actually claims, is at&lt;br&gt;
&lt;a href="https://tohuman.io/blog/best-ai-humanizer-tools-2026" rel="noopener noreferrer"&gt;best AI humanizer tools 2026&lt;/a&gt;; the&lt;br&gt;
&lt;a href="https://tohuman.io/ai-tells-scanner" rel="noopener noreferrer"&gt;AI Tells Scanner&lt;/a&gt; runs the same checks used here, in your browser,&lt;br&gt;
on your own text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you research this.&lt;/strong&gt; The pair corpus is the missing third category in the AI-vs-human&lt;br&gt;
literature and we would like more of it to exist. The scanner source&lt;br&gt;
(&lt;code&gt;public/ai_tells_scanner.js&lt;/code&gt;) is public, so the measurement is reproducible against any&lt;br&gt;
corpus of pairs: 90 slop patterns, 196 cliché phrases, 4 cliché regexes, plus em-dash,&lt;br&gt;
sentence-uniformity, triad and opener-repetition checks with fixed thresholds. If you are&lt;br&gt;
generating pairs at scale programmatically, the&lt;br&gt;
&lt;a href="https://tohuman.io/ai-humanizer-api" rel="noopener noreferrer"&gt;humanizer API&lt;/a&gt; is the same endpoint these runs used. Two gaps are&lt;br&gt;
cheap to close and we would welcome someone closing them first: a testset that actually&lt;br&gt;
contains em dashes and contractions, and a detector arm on the same pairs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credit line.&lt;/strong&gt; If you cite these numbers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ToHuman, &lt;em&gt;"What changes when AI text is humanized: 138 pairs, measured"&lt;/em&gt; (October 2026).&lt;br&gt;
&lt;a href="https://tohuman.io/blog/what-changes-when-ai-text-is-humanized-2026" rel="noopener noreferrer"&gt;https://tohuman.io/blog/what-changes-when-ai-text-is-humanized-2026&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Corrections and replications are welcome — including ones that contradict this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Method appendix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scoring code.&lt;/strong&gt; Every AI-tell measure comes from the live AI Tells Scanner&lt;br&gt;
(&lt;code&gt;public/ai_tells_scanner.js&lt;/code&gt;), required unmodified into Node via its &lt;code&gt;module.exports&lt;/code&gt;.&lt;br&gt;
The published numbers therefore come from exactly the code that powers&lt;br&gt;
&lt;a href="https://tohuman.io/ai-tells-scanner" rel="noopener noreferrer"&gt;/ai-tells-scanner&lt;/a&gt;: 90 slop-vocabulary patterns, 196 cliché phrases,&lt;br&gt;
4 cliché regex patterns, and the scanner's own em-dash, sentence-uniformity, triad and&lt;br&gt;
opener-repetition checks, with its own thresholds and its own sentence, paragraph and&lt;br&gt;
word splitters. No word list or regex was re-invented for this report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composite.&lt;/strong&gt; &lt;code&gt;slop_1k + cliche_1k + emdash_1k + triads_1k&lt;/code&gt; — the plain sum of the&lt;br&gt;
scanner's four rate-based checks in events per 1,000 words. Unweighted, untrained. The&lt;br&gt;
scanner's other two checks are excluded deliberately: sentence-length CV is a ratio that&lt;br&gt;
should move &lt;em&gt;up&lt;/em&gt;, and opener repetition is a share of paragraphs. Both are reported&lt;br&gt;
separately above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contraction rate.&lt;/strong&gt; Locally defined: an allow-list of roughly 60 contracted forms&lt;br&gt;
(&lt;code&gt;don't&lt;/code&gt;, &lt;code&gt;it's&lt;/code&gt;, &lt;code&gt;we're&lt;/code&gt;, …) per 1,000 words. A generic &lt;code&gt;\w+'\w+&lt;/code&gt; pattern would score&lt;br&gt;
every possessive as a contraction and would inflate formal academic prose most, so&lt;br&gt;
possessive &lt;code&gt;'s&lt;/code&gt; is excluded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction shares.&lt;/strong&gt; Dead band of 0.1 per 1,000 words for rates, 0.01 for the CV.&lt;br&gt;
"Barely moved" = &lt;code&gt;|Δcomposite| / composite_in &amp;lt; 5%&lt;/code&gt;, among pairs with &lt;code&gt;composite_in &amp;gt; 0&lt;/code&gt;.&lt;br&gt;
"Got worse" = &lt;code&gt;composite_out &amp;gt; composite_in&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Burstiness n.&lt;/strong&gt; The scanner will not judge rhythm under five sentences&lt;br&gt;
(&lt;code&gt;THRESHOLDS.minSentences&lt;/code&gt;), so the CV rows cover 39 of 66 (A) and 30 of 72 (B) pairs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Genre mix.&lt;/strong&gt; Corpus A: academic 34, blog 16, multilingual 10, personal 9, business 7;&lt;br&gt;
tiered 56 paragraph-length / 20 document-length. Corpus B: long-control 20, blog-short 11,&lt;br&gt;
email-short 11, business-note 8, essay-para 7, human-sourced controls 7, other 8.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source.&lt;/strong&gt; 100% bench-synthetic in both corpora, rewritten by the production RunPod&lt;br&gt;
endpoint. Full statistics report and machine-readable output:&lt;br&gt;
&lt;code&gt;marketing/reports/humanization-corpus-stats-2026-10.md&lt;/code&gt; and its &lt;code&gt;.raw.json&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>datascience</category>
      <category>nlp</category>
    </item>
    <item>
      <title>Why Can't LLMs Count the R's in "Strawberry"? How Tokenizers Work</title>
      <dc:creator>AI Engineering Illustrated</dc:creator>
      <pubDate>Tue, 13 Oct 2026 22:49:40 +0000</pubDate>
      <link>https://dev.to/aiengillustrated/why-cant-llms-count-the-rs-in-strawberry-how-tokenizers-work-eib</link>
      <guid>https://dev.to/aiengillustrated/why-cant-llms-count-the-rs-in-strawberry-how-tokenizers-work-eib</guid>
      <description>&lt;p&gt;&lt;em&gt;The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask a language model how many r's are in "strawberry" and, for a long time, the popular answer was "two." The same models could write a sonnet about strawberries, explain their genetics and translate the word into forty languages. So why does counting letters go wrong?&lt;/p&gt;

&lt;p&gt;The model never sees the letters. Before your prompt reaches the first transformer layer, a separate program called the &lt;strong&gt;tokenizer&lt;/strong&gt; chops the text into chunks and swaps each chunk for an integer. The model only ever reads those integers. If "strawberry" becomes a single integer, the three r's are hidden inside a number the model can't take apart.&lt;/p&gt;

&lt;p&gt;Tokenization is the least glamorous part of the LLM stack, and it causes a surprising number of problems: shaky multi-digit arithmetic, higher costs for non-English users, strange "glitch" words that make models act up, and token counts that don't match your character-based estimates. In this issue we'll open the tokenizer up: how it turns text into IDs, how Byte Pair Encoding (BPE) learns its vocabulary, and how its choices show up as spelling, arithmetic, language and glitch-token problems you can design around.&lt;/p&gt;

&lt;p&gt;Every token split in this issue was measured with OpenAI's open-source &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; library on the actual GPT-2, GPT-4 (&lt;code&gt;cl100k_base&lt;/code&gt;) and GPT-4o (&lt;code&gt;o200k_base&lt;/code&gt;) vocabularies, so you can reproduce them yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens: the Model's Only Alphabet
&lt;/h2&gt;

&lt;p&gt;A neural network can't do math on strings. It needs numbers. The simplest scheme would give every &lt;em&gt;character&lt;/em&gt; a number, but then a 2,000-word document becomes more than 10,000 positions, and every transformer layer pays for each one. The other extreme gives every &lt;em&gt;word&lt;/em&gt; a number, but no fixed list covers every word, name, typo, URL and code identifier people will ever type.&lt;/p&gt;

&lt;p&gt;Rico Sennrich and colleagues measured this trade-off in their 2016 paper &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Neural Machine Translation of Rare Words with Subword Units&lt;/a&gt;. On a German training corpus of about 100 million words, keeping whole words needed &lt;strong&gt;1.75 million&lt;/strong&gt; vocabulary entries and still left 1,079 unknown words in the test set. Splitting into characters needed only about 3,000 symbols but stretched the corpus to &lt;strong&gt;550 million&lt;/strong&gt; positions. Their subword method, BPE, landed in between: about 63,000 symbols, 112 million positions and &lt;strong&gt;zero&lt;/strong&gt; unknown words.&lt;/p&gt;

&lt;p&gt;That middle ground is a &lt;strong&gt;token&lt;/strong&gt;: a chunk of text that's usually shorter than a word for rare words and exactly one word for common ones. Modern LLMs use vocabularies of 50,000 to 200,000 tokens. tiktoken's README puts the average at about &lt;strong&gt;4 bytes of text per token&lt;/strong&gt;. On the English text of last week's issue I measured 4.4 characters per token with the GPT-4o tokenizer, or roughly 0.63 words per token.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Text to Token IDs in Five Steps
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F57n216lwbaawee3imfwq.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%2F57n216lwbaawee3imfwq.png" alt="From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed" width="800" height="1053"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens to the sentence "I love strawberry" on its way into GPT-4o:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raw text.&lt;/strong&gt; Your string arrives as UTF-8 bytes. Some tokenizers normalize it first (Unicode normalization, lowercasing in older models), but GPT-style tokenizers keep the bytes exactly as typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-split.&lt;/strong&gt; A regular expression cuts the text into pieces that merges are never allowed to cross: letter runs, digit runs, punctuation, whitespace. The &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;tiktoken source&lt;/a&gt; shows the patterns. GPT-2's version keeps a leading space attached to the following word, so the pieces are &lt;code&gt;I&lt;/code&gt;, &lt;code&gt;love&lt;/code&gt;, &lt;code&gt;strawberry&lt;/code&gt;. That's why the dot in the figure matters: &lt;strong&gt;" strawberry" with a space and "strawberry" without one are different tokens.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BPE merges.&lt;/strong&gt; Each piece starts as a sequence of bytes. The tokenizer then applies its learned merge rules, in the order they were learned, gluing adjacent pairs together until no rule applies. Common words collapse into one token. Rare ones end up as a few subword chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token IDs.&lt;/strong&gt; Each final chunk is looked up in the vocabulary. With &lt;code&gt;o200k_base&lt;/code&gt;, "I love strawberry" becomes &lt;code&gt;[40, 3047, 101830]&lt;/code&gt;: three integers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings.&lt;/strong&gt; Inside the model, each ID selects one row of a learned &lt;strong&gt;embedding matrix&lt;/strong&gt;, a vector of a few thousand numbers. From here on, the transformer only ever works with those vectors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two properties fall out of this design, and the tiktoken README calls both out. Tokenization is &lt;strong&gt;lossless&lt;/strong&gt;: decoding the IDs gives back the exact original bytes. And it &lt;strong&gt;works on any text&lt;/strong&gt;, even strings that never appeared when the tokenizer was trained. The second property depends on a trick we'll get to in a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BPE Learns Its Vocabulary
&lt;/h2&gt;

&lt;p&gt;Byte Pair Encoding started life as a data compression algorithm (Philip Gage, 1994): repeatedly find the most common pair of adjacent bytes and replace it with a new, unused symbol. Sennrich et al. adapted the idea to build a vocabulary instead of compressing a file. Training goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split every word in the training corpus into characters. The starting vocabulary is just those characters.&lt;/li&gt;
&lt;li&gt;Count every pair of adjacent symbols, weighted by how often each word appears.&lt;/li&gt;
&lt;li&gt;Merge the most frequent pair into a new symbol, add it to the vocabulary, and record the rule.&lt;/li&gt;
&lt;li&gt;Repeat until the vocabulary reaches the target size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The number of merges is the only real setting. As the paper puts it, the final vocabulary size equals the starting symbols plus the number of merge operations.&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%2F1smiat1h5yy3j5xd8ee6.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%2F1smiat1h5yy3j5xd8ee6.png" alt="BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full" width="800" height="1067"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagram uses the toy corpus from the &lt;a href="https://huggingface.co/learn/llm-course/chapter6/5" rel="noopener noreferrer"&gt;Hugging Face LLM course&lt;/a&gt;: "hug" (10 times), "pug" (5), "pun" (12), "bun" (4) and "hugs" (5).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start:&lt;/strong&gt; the vocabulary is &lt;code&gt;b g h n p s u&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 1:&lt;/strong&gt; the pair &lt;code&gt;u + g&lt;/code&gt; appears 20 times (10 in hug, 5 in pug, 5 in hugs), more than any other, so &lt;code&gt;ug&lt;/code&gt; becomes a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 2:&lt;/strong&gt; &lt;code&gt;u + n&lt;/code&gt; now leads with 16 (12 in pun, 4 in bun), so &lt;code&gt;un&lt;/code&gt; joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge 3:&lt;/strong&gt; &lt;code&gt;h + ug&lt;/code&gt; appears 15 times, and the first three-letter token, &lt;code&gt;hug&lt;/code&gt;, is born.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real tokenizers run this loop tens of thousands of times over gigabytes of text. GPT-2's vocabulary has 50,257 entries. GPT-4's &lt;code&gt;cl100k_base&lt;/code&gt; has about 100,000 and GPT-4o's &lt;code&gt;o200k_base&lt;/code&gt; about 200,000 (100,277 and 200,019 including special tokens, according to tiktoken). Meta says Llama 3 moved to a &lt;a href="https://ai.meta.com/blog/meta-llama-3/" rel="noopener noreferrer"&gt;128K-token vocabulary&lt;/a&gt; that yields up to 15% fewer tokens than Llama 2 on the same text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoding new text&lt;/strong&gt; replays the merges. Split the input into characters (or bytes), then apply the learned rules in priority order. A word seen often during training collapses into one token. A word never seen still gets encoded, just in smaller pieces. With GPT-4o's vocabulary, "unbelievably" becomes &lt;code&gt;un | bel | ievably&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The order of merges also explains some quirks. Merges reflect the &lt;em&gt;tokenizer's&lt;/em&gt; training data, which isn't necessarily the &lt;em&gt;model's&lt;/em&gt; training data. Keep that in mind; it comes back when we get to glitch tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Byte-Level BPE: No More "Unknown Word"
&lt;/h2&gt;

&lt;p&gt;Classic BPE starts from characters, and Unicode defines roughly 150,000 of them. Any character missing from the training corpus becomes &lt;code&gt;[UNK]&lt;/code&gt;, the "unknown" token. The Hugging Face course notes this is one reason older NLP models handled emoji badly.&lt;/p&gt;

&lt;p&gt;GPT-2 introduced a simple fix: run BPE over &lt;strong&gt;bytes&lt;/strong&gt; instead of characters. There are only 256 possible byte values, so the base vocabulary has 256 entries and every possible input, whether emoji, Tamil script or binary garbage, can be written in them. Merges then build up common multi-byte sequences. As the course puts it, GPT-2 doesn't actually have an unknown token, because an unknown character can't occur with byte-level BPE.&lt;/p&gt;

&lt;p&gt;You can see the byte fallback at work. In GPT-2's vocabulary, the emoji 🙂 (four UTF-8 bytes) becomes 2 tokens. GPT-4o's larger vocabulary learned the whole sequence, so it's 1 token. Either way, nothing is ever "unknown." The price is that a rarely seen script can get chopped into fragments that aren't even whole characters, which leads to the language tax below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Model Can't See the R's
&lt;/h2&gt;

&lt;p&gt;Back to the strawberry. Here's what GPT-4o's tokenizer does with the classic question:&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%2Ftbpq9njyu2m4kurweml8.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%2Ftbpq9njyu2m4kurweml8.png" alt="The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters" width="800" height="1000"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"How many r's are in strawberry?" becomes 8 tokens: &lt;code&gt;How | ·many | ·r | 's | ·are | ·in | ·strawberry | ?&lt;/code&gt;. The word you care about is token &lt;strong&gt;#101830&lt;/strong&gt;. To the model, that's one opaque symbol, mapped to one embedding vector. There's no "r" in it to count.&lt;/p&gt;

&lt;p&gt;Whatever the model knows about how strawberry is spelled, it had to &lt;em&gt;memorize&lt;/em&gt; from training text where the word happened to be spelled out, as in dictionaries, spelling-bee pages or kids' worksheets. That knowledge is thin and indirect, so the model guesses, and a plausible guess for a word ending in a double letter is "two."&lt;/p&gt;

&lt;p&gt;The split also depends on context in ways people don't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strawberry&lt;/code&gt; (leading space, the usual case mid-sentence) is &lt;strong&gt;1&lt;/strong&gt; token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strawberry&lt;/code&gt; (no leading space, for example at the start of a message) is &lt;strong&gt;3&lt;/strong&gt; tokens: &lt;code&gt;st | raw | berry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Strawberry&lt;/code&gt; (capitalized) is a different 3: &lt;code&gt;Str | aw | berry&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the model may have learned slightly different things about the same word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fixes all expose the letters.&lt;/strong&gt; Writing the word as &lt;code&gt;s-t-r-a-w-b-e-r-r-y&lt;/code&gt; turns it into 10 tokens, one per letter, and now counting is something the model can actually do. Better still, when exact character-level answers matter (counting, reversing a string, checking a regex), have the model &lt;strong&gt;call a code tool&lt;/strong&gt;. One line of Python, &lt;code&gt;"strawberry".count("r")&lt;/code&gt;, returns 3 every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multilingual Token Tax
&lt;/h2&gt;

&lt;p&gt;The tokenizer's merges come from its training data, and that data is mostly English and code. English words get dedicated tokens. Other languages, especially ones with their own scripts, get fewer merges and are left closer to raw bytes.&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%2Fqemiok76vshav7x8gk9y.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%2Fqemiok76vshav7x8gk9y.png" alt="The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks" width="800" height="1093"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I ran one short sentence, "The weather is nice today," and simple translations of it through three of OpenAI's tokenizers. In English, it's 6 tokens in all three. In Tamil, it's &lt;strong&gt;85 tokens with GPT-2's tokenizer, 42 with GPT-4's and 11 with GPT-4o's&lt;/strong&gt;. Hindi drops from 27 to 21 to 5, and Russian from 27 to 13 to 6. That's just one sentence, not a benchmark, but the direction is clear. Larger, more multilingual vocabularies have closed much of the gap, though not all of it.&lt;/p&gt;

&lt;p&gt;Aleksandar Petrov and colleagues studied this systematically in &lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Language Model Tokenizers Introduce Unfairness Between Languages&lt;/a&gt; (2023). They found the same text can differ in token length by up to &lt;strong&gt;15×&lt;/strong&gt; across languages, and that even byte-level and character-level models show gaps of more than 4×. The consequences are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; APIs bill per token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context:&lt;/strong&gt; a 128K-token window holds far fewer pages of Tamil than of English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; output is generated one token at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you serve a non-English market, measure token counts on your actual traffic. Don't assume the "about 4 characters per token" rule of thumb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers: Why Long Addition Goes Wrong
&lt;/h2&gt;

&lt;p&gt;The same pre-split regex decides how numbers are chunked, and labs have made very different choices. Aaditya Singh and DJ Strouse surveyed them in &lt;a href="https://arxiv.org/abs/2402.14903" rel="noopener noreferrer"&gt;Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs&lt;/a&gt; (2024):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure BPE&lt;/strong&gt; (GPT-3, Claude 2.1, OLMo): whichever digit strings happened to be frequent got tokens. 710 might be one token while 711 isn't, so numbers of the same length split inconsistently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single digits&lt;/strong&gt; (PaLM, Llama 1 and 2, Mistral): every digit is its own token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunks of up to 3 digits, left to right&lt;/strong&gt; (GPT-3.5, GPT-4): the &lt;code&gt;\p{N}{1,3}&lt;/code&gt; rule in the &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;tiktoken pattern&lt;/a&gt;. GPT-4o's &lt;code&gt;o200k_base&lt;/code&gt; uses the same rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Left-to-right chunking means &lt;code&gt;1234567&lt;/code&gt; becomes &lt;code&gt;123 | 456 | 7&lt;/code&gt;. The chunks line up with the &lt;em&gt;start&lt;/em&gt; of the number, while place value, and the way carries move when you add, runs from the &lt;em&gt;end&lt;/em&gt;. Insert commas and the tokenizer splits on them, giving &lt;code&gt;1 | , | 234 | , | 567&lt;/code&gt;. Now every chunk sits on a thousands boundary.&lt;/p&gt;

&lt;p&gt;Singh and Strouse tested exactly this on GPT-3.5 with 7- to 9-digit addition. With plain digits, 8-shot accuracy was &lt;strong&gt;75.6%&lt;/strong&gt;. With commas, it was &lt;strong&gt;97.8%&lt;/strong&gt;. When the answer had more digits than the inputs (because of a final carry), plain-digit accuracy fell to &lt;strong&gt;8.25%&lt;/strong&gt;, and the errors weren't random: the model almost always got the first three digits right and the fourth digit wrong, right at the first token boundary. Asking the model to first rewrite the problem with commas recovered most of the lost accuracy. GPT-4 was less affected, but the effect didn't disappear.&lt;/p&gt;

&lt;p&gt;The lesson isn't "add commas everywhere." It's that &lt;strong&gt;number formatting changes the problem the model has to solve&lt;/strong&gt;. For anything that must be exact, like invoices or financial math, route the arithmetic to a calculator or code tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glitch Tokens: When the Vocabulary Outlives the Data
&lt;/h2&gt;

&lt;p&gt;In early 2023, Jessica Rumbelow and Matthew Watkins went looking at GPT-2 and GPT-3's token embeddings and found a cluster of strange entries: &lt;code&gt;SolidGoldMagikarp&lt;/code&gt;, &lt;code&gt;TheNitromeFan&lt;/code&gt;, &lt;code&gt;cloneembedreportprint&lt;/code&gt;, &lt;code&gt;petertodd&lt;/code&gt;. In their &lt;a href="https://www.lesswrong.com/posts/aPeJE8bSo6rAFoLqg/solidgoldmagikarp-plus-prompt-generation" rel="noopener noreferrer"&gt;LessWrong write-up&lt;/a&gt;, they showed that asking a model to simply repeat these strings produced evasion, insults or substitutions. ChatGPT, for example, repeated " SolidGoldMagikarp" as "distribute." Some of these tokens even made GPT-3 give different answers at temperature 0, which is supposed to be deterministic.&lt;/p&gt;

&lt;p&gt;Their partial explanation goes back to the point about merges. The tokenizer was trained on scraped web text that included Reddit usernames, e-commerce site internals and game logs. The model was trained on more heavily curated text. So strings like &lt;code&gt;SolidGoldMagikarp&lt;/code&gt; (I confirmed it's a single token, #43453, in GPT-2's vocabulary) earned a vocabulary slot but almost never showed up during model training. Their embedding vectors stayed close to wherever they started, and the model doesn't know what to do when it meets one.&lt;/p&gt;

&lt;p&gt;It's not just a GPT-2 curiosity. &lt;a href="https://arxiv.org/abs/2405.05417" rel="noopener noreferrer"&gt;Fishing for Magikarp&lt;/a&gt; (Land and Bartolo, 2024) built automatic detectors that combine tokenizer analysis, weight-based signals and prompting, and found under-trained tokens across a wide range of models. In newer OpenAI vocabularies, " SolidGoldMagikarp" is 5 ordinary tokens (&lt;code&gt;·Solid | Gold | Mag | ik | arp&lt;/code&gt;), but each new tokenizer can bring its own oddities. If you train or fine-tune models, scanning for under-trained tokens is cheap insurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other Tokenizer Families
&lt;/h2&gt;

&lt;p&gt;BPE dominates today's LLMs, but you'll run into two other algorithms, often through one library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPiece&lt;/strong&gt; (developed by Google for BERT) also learns merges bottom-up, but according to the &lt;a href="https://huggingface.co/learn/llm-course/chapter6/6" rel="noopener noreferrer"&gt;Hugging Face course&lt;/a&gt; it scores a pair as its frequency divided by the product of its parts' frequencies, which favors pairs whose parts are rare on their own. It keeps only the final vocabulary and encodes each word by taking the longest piece it knows, then the next. Pieces inside a word carry a &lt;code&gt;##&lt;/code&gt; prefix, as in &lt;code&gt;token ##ization&lt;/code&gt;. (Google never open-sourced its training code, so the course describes a best-guess reconstruction.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unigram LM&lt;/strong&gt;, from Taku Kudo's &lt;a href="https://arxiv.org/abs/1804.10959" rel="noopener noreferrer"&gt;Subword Regularization&lt;/a&gt; (2018), works top-down. It starts with a large candidate vocabulary, scores how much each piece contributes to a probabilistic model of the corpus, and prunes the least useful pieces. Since the same word can be segmented several ways, Kudo trained translation models on &lt;em&gt;sampled&lt;/em&gt; segmentations as a kind of noise, which made them more robust, especially in low-resource and out-of-domain settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SentencePiece&lt;/strong&gt; (&lt;a href="https://arxiv.org/abs/1808.06226" rel="noopener noreferrer"&gt;Kudo and Richardson, 2018&lt;/a&gt;) is a library rather than an algorithm. It implements both BPE and Unigram and trains &lt;em&gt;directly on raw sentences&lt;/em&gt;, with no language-specific pre-splitting. Whitespace becomes the visible symbol &lt;code&gt;▁&lt;/code&gt;, so &lt;code&gt;I saw a girl&lt;/code&gt; becomes &lt;code&gt;▁I ▁saw ▁a ▁girl&lt;/code&gt; (example from the &lt;a href="https://github.com/google/sentencepiece" rel="noopener noreferrer"&gt;SentencePiece README&lt;/a&gt;). That makes it a natural fit for languages that don't put spaces between words, like Japanese and Chinese.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Picking a Vocabulary Size
&lt;/h2&gt;

&lt;p&gt;Why not a million tokens? Every entry costs a row in the input embedding matrix and another in the output layer that scores the next token (unless the two are tied). With 4,096-dimensional vectors, a 200,000-entry vocabulary is about 820 million parameters per matrix. More entries also means fewer training examples per rare token, which is the glitch-token problem again.&lt;/p&gt;

&lt;p&gt;Smaller vocabularies mean longer sequences: attention costs more, the context window fills faster and generation takes more steps. Recent models have moved toward larger vocabularies (50K, then 100K, 128K and 200K) as the embedding table becomes a small share of total parameters. Meta credits Llama 3's 128K tokenizer with "substantially improved model performance."&lt;/p&gt;

&lt;h2&gt;
  
  
  A Cheat Sheet for Engineers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo123vn29f67sg9jwcn1.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%2Feo123vn29f67sg9jwcn1.png" alt="Common tokenizer gotchas, their causes and practical fixes" width="800" height="639"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Common tokenizer gotchas, their causes and practical fixes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few of these deserve a sentence more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count tokens with the real tokenizer.&lt;/strong&gt; Use &lt;code&gt;tiktoken.encoding_for_model("gpt-4o")&lt;/code&gt; for OpenAI models, or your provider's token-counting endpoint, before you enforce context limits or estimate costs. Character-based estimates are wrong for code, non-English text and JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the boundary at the end of your prompt.&lt;/strong&gt; Because GPT-style tokenizers attach the space to the &lt;em&gt;following&lt;/em&gt; word, " Paris" is a single token. A prompt that ends with a trailing space ("The capital of France is ") leaves a bare space token, and the model then has to continue with "Paris" &lt;em&gt;without&lt;/em&gt; its usual leading space, an unusual sequence it saw less often in training. Trim trailing whitespace from completion-style prompts. The "token healing" technique cited by Singh and Strouse goes further: it backs up a few tokens and lets the model regenerate across the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special tokens are control characters.&lt;/strong&gt; Strings like &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt; map to reserved IDs (100257 in &lt;code&gt;cl100k_base&lt;/code&gt;, 199999 in &lt;code&gt;o200k_base&lt;/code&gt;) that mark document and message boundaries. By default tiktoken refuses to encode them from ordinary text: &lt;code&gt;encode("hi &amp;lt;|endoftext|&amp;gt;")&lt;/code&gt; raises an error. Keep that protection on, so pasted text can't forge a document or message boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLMs read integers, not letters.&lt;/strong&gt; A tokenizer turns text into IDs before the model runs, and anything inside a token, like its spelling, is invisible unless the model memorized it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BPE builds the vocabulary by merging the most frequent adjacent pairs&lt;/strong&gt;, and byte-level BPE guarantees that any input can be encoded with no "unknown" token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization explains classic failure modes:&lt;/strong&gt; letter counting, multi-digit arithmetic (75.6% vs 97.8% for GPT-3.5 depending only on commas), higher costs for some languages (up to 15× longer encodings) and glitch tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix is usually to change the representation or hand off the work:&lt;/strong&gt; spell words out, format numbers deliberately, and call code for anything that must be exact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure with the actual tokenizer.&lt;/strong&gt; "4 characters per token" is a rough English-only average, not a budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading (sources used)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sennrich, Haddow and Birch, &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Neural Machine Translation of Rare Words with Subword Units&lt;/a&gt; (ACL 2016), the paper that brought BPE to NLP&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, &lt;a href="https://huggingface.co/learn/llm-course/chapter6/5" rel="noopener noreferrer"&gt;Byte-Pair Encoding tokenization&lt;/a&gt;, with a worked example and a from-scratch implementation&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; and its &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;encoding definitions&lt;/a&gt; (pre-split regexes, vocabularies, special tokens)&lt;/li&gt;
&lt;li&gt;Singh and Strouse, &lt;a href="https://arxiv.org/abs/2402.14903" rel="noopener noreferrer"&gt;Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs&lt;/a&gt; (2024)&lt;/li&gt;
&lt;li&gt;Petrov et al., &lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Language Model Tokenizers Introduce Unfairness Between Languages&lt;/a&gt; (2023)&lt;/li&gt;
&lt;li&gt;Rumbelow and Watkins, &lt;a href="https://www.lesswrong.com/posts/aPeJE8bSo6rAFoLqg/solidgoldmagikarp-plus-prompt-generation" rel="noopener noreferrer"&gt;SolidGoldMagikarp (plus, prompt generation)&lt;/a&gt; (2023)&lt;/li&gt;
&lt;li&gt;Land and Bartolo, &lt;a href="https://arxiv.org/abs/2405.05417" rel="noopener noreferrer"&gt;Fishing for Magikarp: Automatically Detecting Under-trained Tokens in Large Language Models&lt;/a&gt; (2024)&lt;/li&gt;
&lt;li&gt;Kudo, &lt;a href="https://arxiv.org/abs/1804.10959" rel="noopener noreferrer"&gt;Subword Regularization&lt;/a&gt; (ACL 2018), and Kudo and Richardson, &lt;a href="https://arxiv.org/abs/1808.06226" rel="noopener noreferrer"&gt;SentencePiece&lt;/a&gt; (2018)&lt;/li&gt;
&lt;li&gt;Meta AI, &lt;a href="https://ai.meta.com/blog/meta-llama-3/" rel="noopener noreferrer"&gt;Introducing Meta Llama 3&lt;/a&gt; (128K vocabulary, up to 15% fewer tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Try it yourself: &lt;code&gt;pip install tiktoken&lt;/code&gt;, then &lt;code&gt;tiktoken.get_encoding("o200k_base").encode(" strawberry")&lt;/code&gt; and see what you get.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ragmehta.substack.com" rel="noopener noreferrer"&gt;AI Engineering Illustrated&lt;/a&gt;. Subscribe for new deep dives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>nlp</category>
    </item>
    <item>
      <title>ASR-induced failure rate: did the speech path break it?</title>
      <dc:creator>Vijay Kumar Sridharan</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:57:31 +0000</pubDate>
      <link>https://dev.to/sridharanvijaykumar/asr-induced-failure-rate-did-the-speech-path-break-it-2peg</link>
      <guid>https://dev.to/sridharanvijaykumar/asr-induced-failure-rate-did-the-speech-path-break-it-2peg</guid>
      <description>&lt;p&gt;Part 1 ended in the failure-review meeting: the ASR team says the transcripts were fine and the NLU misread them, the NLU team says the transcripts were garbage, QA says the test oracle is stale. Everyone is arguing from the same green dashboard, and nobody has an experiment.&lt;/p&gt;

&lt;p&gt;This post is the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run everything twice
&lt;/h2&gt;

&lt;p&gt;For every semantic test case in your voice suite, execute it twice through the same frozen downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text control:&lt;/strong&gt; reference text straight into the NLU/agent. No audio, no ASR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio path:&lt;/strong&gt; audio through the ASR, then into that same NLU/agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then read the four outcomes — the attribution table from part 1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Text control&lt;/th&gt;
&lt;th&gt;Audio path&lt;/th&gt;
&lt;th&gt;Attribution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;HEALTHY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;SPEECH_ATTRIBUTABLE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;DOWNSTREAM_OR_TEST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;CONTEXT_OR_ORACLE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a controlled experiment, not a metric. The text control holds everything downstream constant; the only thing that changes between the two runs is the speech path. When the two runs disagree, the speech path is the difference, and the argument about whose fault it is ends — the table already routed the ticket.&lt;/p&gt;

&lt;p&gt;The per-trial verdict rolls up to a suite-level number: &lt;strong&gt;ASR-IFR, the ASR-induced failure rate&lt;/strong&gt; — the fraction of the full paired suite where the text control passes but the audio path fails. In the implementation, each trial contributes 1 if its attribution is &lt;code&gt;SPEECH_ATTRIBUTABLE&lt;/code&gt; and 0 otherwise; ASR-IFR is the mean over all trials. The numerator is the speech-attributable count. The denominator is the whole suite.&lt;/p&gt;

&lt;p&gt;The denominator choice is the part worth thinking about, because it's doing more work than the numerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the denominator matters more than the numerator
&lt;/h2&gt;

&lt;p&gt;The numerator is just a count — 142 trials, in the pilot below. Anyone can count failures. The denominator decides what the number &lt;em&gt;means&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;ASR-IFR divides by all trials, not by control-passing trials. The alternative — "of the tests that worked, what fraction did speech break" — is also a meaningful number (0.394 / 0.800 ≈ 0.49 in the pilot). But the unconditional rate answers the question operations actually asks: &lt;em&gt;what fraction of my suite is broken by the speech path right now?&lt;/em&gt; It stays comparable across runs even when downstream health changes — if the NLU regresses and text-control accuracy drops, a conditional rate would quietly rebase itself and hide the change; the unconditional rate doesn't move unless the speech path moved. That's what makes it gateable: &lt;code&gt;--max-asr-ifr-delta 0.02&lt;/code&gt; against your last approved baseline means something, because the denominator can't shift under you.&lt;/p&gt;

&lt;p&gt;And there is a ceiling built into the definition that keeps the metric honest. ASR-IFR can never exceed text-control accuracy — a trial only counts if the control passed. In the pilot, text control is 0.800, so the worst ASR-IFR can ever be is 0.800. When ASR-IFR approaches that ceiling, the reading is unambiguous: nearly every test the downstream could pass is being broken by speech. The metric degrades gracefully instead of exploding — compare that with WER, which cheerfully reports 1.229 when insertions pile up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control is the keystone
&lt;/h2&gt;

&lt;p&gt;None of this works without the text control, and the control has to be boring to be useful. In the pilot's reproduced results, text-control accuracy is &lt;strong&gt;0.800 in every condition&lt;/strong&gt; — clean, 15 dB noise, 5 dB noise, fast speech, telephony, telephony-plus-noise. The downstream never changed, so every bit of movement in ASR-IFR is the speech path. That flat 0.800 is the entire epistemic foundation: without it, you cannot attribute anything, and you're back to arguing in the meeting.&lt;/p&gt;

&lt;p&gt;The control also does triage the metric can't. If text-control accuracy drops between releases, that's a downstream or test-oracle problem — the NLU, the agent, or a stale test — and it goes to those owners, not the ASR team. ASR-IFR moving while text control holds flat is a recognizer problem. WER moving while both hold flat is lexical churn: investigate, then ship. Three numbers, three owners, no meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers look like across conditions
&lt;/h2&gt;

&lt;p&gt;The paired design is implemented in CAFA-IVR (the worked example for this series), and the 360-trial Conformer-CTC pilot ships per-condition reproductions. Sixty trials each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;ASR-IFR&lt;/th&gt;
&lt;th&gt;Text control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;0.115&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telephony (8 kHz)&lt;/td&gt;
&lt;td&gt;0.272&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast speech (1.15×)&lt;/td&gt;
&lt;td&gt;0.412&lt;/td&gt;
&lt;td&gt;0.317&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 dB noise&lt;/td&gt;
&lt;td&gt;0.741&lt;/td&gt;
&lt;td&gt;0.367&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telephony + 15 dB noise&lt;/td&gt;
&lt;td&gt;1.121&lt;/td&gt;
&lt;td&gt;0.667&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 dB noise&lt;/td&gt;
&lt;td&gt;1.229&lt;/td&gt;
&lt;td&gt;0.717&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.648&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.394&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.800&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the 5 dB row the way an operator would. WER 1.229 is nearly uninterpretable — worse than 100%, because insertions pile up past the reference length. ASR-IFR 0.717 says something plain: of the 80% of tests the downstream could pass, speech broke almost 90% of them (0.717 / 0.800). Two out of every five tests in the suite, broken by the speech path. One of those two numbers helps you decide whether to ship. It isn't the first one.&lt;/p&gt;

&lt;p&gt;And notice what the table separates that WER alone cannot. In the telephony-plus-noise condition, WER 1.121 and ASR-IFR 0.667 move together; in fast speech, WER 0.412 looks moderate while ASR-IFR 0.317 says nearly a third of the suite broke. The relationship between transcription damage and task damage isn't fixed — it depends on &lt;em&gt;which&lt;/em&gt; words the recognizer corrupts, which is exactly what part 2's CEER is for. ASR-IFR tells you the speech path broke it; CEER tells you what it broke. You want both numbers, in that order.&lt;/p&gt;

&lt;p&gt;Overall: 360 trials, 142 speech-attributable failures, ASR-IFR &lt;strong&gt;0.394&lt;/strong&gt;. Nearly two in five working tests, broken by speech. That's the release-gate number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cafa-ivr compare &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--baseline&lt;/span&gt; baseline/summary.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--candidate&lt;/span&gt; candidate/summary.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-asr-ifr-delta&lt;/span&gt; 0.02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A candidate that moves ASR-IFR more than two points against baseline doesn't ship until someone explains the movement. WER can move however it likes in the meantime — it's diagnostic, not decisive.&lt;/p&gt;

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

&lt;p&gt;Stated plainly, as in parts 1 and 2: this is a 360-trial pilot on controlled synthetic speech with a constrained in-domain Conformer-CTC recognizer. It demonstrates the method — the paired design, the attribution table, the gate. It is not a production-ASR benchmark and should not be read as one. Your numbers will differ; the experimental design won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operational takeaway
&lt;/h2&gt;

&lt;p&gt;The question to take back to your team is the one from part 1: for every failing voice test you have right now, can you say whether the speech path caused it? If you run the paired design, the answer comes with a number attached — and the number routes itself. ASR-IFR up, text control flat: fix the recognizer. Text control down: fix the NLU or the test. WER up, both flat: ship.&lt;/p&gt;

&lt;p&gt;Three numbers, three owners, no meeting. That's the whole pitch.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series: CIER — weighting failures by realized consequence, or why "the task failed" is still not the end of the story.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>nlp</category>
      <category>testing</category>
    </item>
    <item>
      <title>Was My 2,103-Token Tokenizer a Mistake?</title>
      <dc:creator>Raihan</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:30:21 +0000</pubDate>
      <link>https://dev.to/raihan-js/was-my-2103-token-tokenizer-a-mistake-pf9</link>
      <guid>https://dev.to/raihan-js/was-my-2103-token-tokenizer-a-mistake-pf9</guid>
      <description>&lt;p&gt;&lt;em&gt;A compute-matched vocabulary-size study for small code models.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffb5o7tivyg0gn1n1nscx.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%2Ffb5o7tivyg0gn1n1nscx.png" alt="Vocab Tax results" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scope note: 12 LLaMA-style decoders trained from scratch on TypeScript/JavaScript, 3 sizes × 4 vocabulary sizes, equal training FLOPs. Not a scaling law — a controlled experiment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I trained ORCH-Fusion (2,103 tokens, 272.7M) and ORCH-Next.js-350M-v2 (16,000 tokens, 287M) on the same backbone but different tokenizers, data, and training. So I cannot say whether the small vocabulary helped. This project fixes that: the tokenizer is the only variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: 7,606 exact-hash-deduplicated TS/JS files from HuggingFace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenizers&lt;/strong&gt;: 4 byte-level BPE tokenizers (2k, 8k, 16k, 32k vocab) trained on the same bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models&lt;/strong&gt;: 3 LLaMA-style sizes (nominal 10M / 25M / 50M tags; real parameter counts in the Results section)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training&lt;/strong&gt;: 1,000 steps each (an under-trained regime, so read the results as vocabulary choice at this budget)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation&lt;/strong&gt;: Held-out bits-per-byte on TS/JS code&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fertility table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vocab size&lt;/th&gt;
&lt;th&gt;Tokens per byte&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;0.4475&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,000&lt;/td&gt;
&lt;td&gt;0.3667&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;0.3435&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;0.3275&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Larger vocabularies have lower fertility — fewer tokens per byte. But they also have larger embedding tables, which cost more FLOPs. The question is whether the quality gain outweighs the compute cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Held-out bits-per-byte on TypeScript/JavaScript, 1,000 steps per run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size (non-emb)&lt;/th&gt;
&lt;th&gt;2k&lt;/th&gt;
&lt;th&gt;8k&lt;/th&gt;
&lt;th&gt;16k&lt;/th&gt;
&lt;th&gt;32k&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;1.391&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.168&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.219&lt;/td&gt;
&lt;td&gt;1.321&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;1.378&lt;/td&gt;
&lt;td&gt;1.258&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.218&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50M&lt;/td&gt;
&lt;td&gt;1.436&lt;/td&gt;
&lt;td&gt;1.204&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.147&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.458&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(10M rows are means of 2 seeds; seed noise reaches 0.13, so 8k vs 16k at 10M is an honest tie.)&lt;/p&gt;

&lt;p&gt;The size tags are nominal. Real parameter counts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size tag&lt;/th&gt;
&lt;th&gt;Non-embedding params&lt;/th&gt;
&lt;th&gt;Total params at 2k / 8k / 16k / 32k vocab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;3.9M&lt;/td&gt;
&lt;td&gt;5.0M / 8.0M / 12.1M / 20.3M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;13.3M&lt;/td&gt;
&lt;td&gt;14.8M / 19.4M / 25.6M / 37.9M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50M&lt;/td&gt;
&lt;td&gt;31.5M&lt;/td&gt;
&lt;td&gt;33.5M / 39.7M / 47.9M / 64.2M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "10M / 25M / 50M" tags are nominal; the table gives the real counts. All 16 runs use &lt;code&gt;max_steps: 1000&lt;/code&gt; on 7,606 deduplicated TS/JS files, an &lt;strong&gt;under-trained regime&lt;/strong&gt;: conclusions are about vocabulary choice at this budget, not at compute-optimal training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key findings
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;8k–16k wins at every size.&lt;/strong&gt; 2k is always bad; 32k collapses at 50M (its embedding matrix is about half the model and is under-trained at 1,000 steps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary beats backbone:&lt;/strong&gt; the 10M tag with 8k vocab (8.0M total params, 1.168 bpb) beats the 50M tag with 2k vocab (33.5M total params, 1.436 bpb): 4.2× fewer total parameters, 18.7% lower bits-per-byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed noise is large&lt;/strong&gt; (up to 0.13 between the two 10M seeds) — differences below ~0.1 are ties, reported as such.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was the 2,103-token tokenizer a mistake?&lt;/strong&gt; Directionally yes — 2k costs ~19% bpb vs 8k at 10M. Caveat: ORCH differs in data and training too, so this is suggestive, not conclusive.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;3 sizes is fragile for fitting a scaling law&lt;/li&gt;
&lt;li&gt;Differences may sit inside seed noise (second-seed row shows the band)&lt;/li&gt;
&lt;li&gt;At 10-50M params with 32k vocab, embeddings dominate parameter count&lt;/li&gt;
&lt;li&gt;The 3060's memory bandwidth limits throughput; measure MFU instead of guessing&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Add frontier API baselines&lt;/li&gt;
&lt;li&gt;Test the curve on a larger held-out run&lt;/li&gt;
&lt;li&gt;Apply the finding to the next ORCH model&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Repo: github.com/raihan-js/vocab-tax · 7 tests green. The tokenizer is the variable.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>python</category>
      <category>nlp</category>
    </item>
    <item>
      <title>Day 35: BERT: Architecture and Masked Language Modeling</title>
      <dc:creator>Priyesh Dave</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:08:08 +0000</pubDate>
      <link>https://dev.to/priyeshdave6/day-35-bert-architecture-and-masked-language-modeling-2nm4</link>
      <guid>https://dev.to/priyeshdave6/day-35-bert-architecture-and-masked-language-modeling-2nm4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Previously, on Day 34:&lt;/strong&gt; Explained the concept of pretraining in NLP, highlighted why it is foundational, introduced transfer learning, and demonstrated how pretrained models are used in practice with Hugging Face Transformers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is BERT? Contextualized Language Representations
&lt;/h2&gt;

&lt;p&gt;BERT stands for &lt;strong&gt;Bidirectional Encoder Representations from Transformers&lt;/strong&gt;. It's a language model—a machine learning system trained to read, understand, and generate human language.&lt;/p&gt;

&lt;p&gt;Earlier models read text in only one direction: either left to right or right to left. If you asked them to predict a missing word, they could only use the words before or after the blank—not both at once.&lt;/p&gt;

&lt;p&gt;BERT changed this with &lt;strong&gt;bidirectionality&lt;/strong&gt;. BERT reads text in both directions at the same time. When BERT tries to understand or predict a word, it looks at the whole context—everything before and after the target word. In natural language processing (NLP), this is called &lt;strong&gt;contextualization&lt;/strong&gt;: the meaning of a word is adjusted based on the words around it.&lt;/p&gt;

&lt;p&gt;For example, “bank” in “river bank” is different from “bank” in “savings bank.” Earlier models sometimes missed this. BERT uses the whole sentence to find the right meaning for every word, every time. This context-aware reasoning is a big reason for BERT’s accuracy and popularity.&lt;/p&gt;

&lt;h2&gt;
  
  
  BERT’s Architecture: Key Building Blocks
&lt;/h2&gt;

&lt;p&gt;BERT is built from a stack of &lt;strong&gt;transformer encoder blocks&lt;/strong&gt;. These blocks process the entire sentence at once, letting every word send and receive information from every other word.&lt;/p&gt;

&lt;p&gt;Here’s what goes into BERT processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tokens&lt;/strong&gt;: BERT doesn’t work with full words. It works with &lt;em&gt;tokens&lt;/em&gt;—often pieces of words, called subwords. A tokenizer breaks text into tokens and converts each token to a unique number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Segments&lt;/strong&gt;: BERT can process two partitions of text together, such as a question and its answer. Each token gets a label marking which segment it comes from.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Positions&lt;/strong&gt;: Transformers have no built-in sense of order. So BERT adds a &lt;em&gt;position embedding&lt;/em&gt; to each token to indicate where it appears in the sentence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encoder blocks &amp;amp; Attention&lt;/strong&gt;: Each encoder block uses &lt;strong&gt;attention&lt;/strong&gt;. Attention means that, for every token, the model decides how much to “pay attention” to every other token. These attention weights help BERT understand connections, even between far-apart words.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine BERT's input like a grid: each row is a token, labeled with its segment and position, with attention creating links between all pairs of tokens in each layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Masked Language Modeling (MLM)
&lt;/h2&gt;

&lt;p&gt;Traditional language models predict the &lt;em&gt;next&lt;/em&gt; word in a sentence. BERT uses &lt;strong&gt;Masked Language Modeling&lt;/strong&gt; (MLM), which works differently.&lt;/p&gt;

&lt;p&gt;Here’s how MLM training works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some tokens in the input are randomly replaced with a special token: &lt;code&gt;[MASK]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;BERT doesn’t know what &lt;code&gt;[MASK]&lt;/code&gt; means. Its task is to use the rest of the sentence to guess the original word.&lt;/li&gt;
&lt;li&gt;For example:

&lt;ul&gt;
&lt;li&gt;Input: “The cat [MASK] on the mat.”&lt;/li&gt;
&lt;li&gt;BERT needs to predict that &lt;code&gt;[MASK]&lt;/code&gt; should be “sat”.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why mask random words?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unlike left-to-right models, BERT uses context from both left and right to make its guess.&lt;/li&gt;
&lt;li&gt;This forces BERT to actually understand the whole sentence—where every word matters, not just the next word.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach teaches BERT to model words in any position, based on their full context.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Step-by-Step Example: How BERT Learns
&lt;/h2&gt;

&lt;p&gt;Let’s follow what happens on a single training step:&lt;/p&gt;

&lt;p&gt;Consider the real sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The [MASK] barked loudly in the park.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s say “[MASK]” covers the word “dog.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tokenization and Masking&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Split into tokens: [“The”, “[MASK]”, “barked”, “loudly”, “in”, “the”, “park”, “.”]&lt;/li&gt;
&lt;li&gt;Each token is mapped to a unique integer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Input Assembly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each token is converted into an embedding (a list of numbers).&lt;/li&gt;
&lt;li&gt;Segment and position information are added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Encoding&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings flow through the stack of encoder blocks.&lt;/li&gt;
&lt;li&gt;Attention lets tokens (like “[MASK]”) use information from all the other words, both before and after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Mask Prediction&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At &lt;code&gt;[MASK]&lt;/code&gt;, BERT combines the contexts and outputs a guess—a probability for each possible word.&lt;/li&gt;
&lt;li&gt;In our example, “dog” should have the highest probability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Loss and Update&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BERT compares its guess to the correct word (“dog”).&lt;/li&gt;
&lt;li&gt;It computes a loss (a measure of how wrong it was), then tweaks its internal weights to do better next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat this process millions of times, and BERT becomes great at filling in blanks and understanding all kinds of language patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-Tuning: Using BERT for Real Tasks
&lt;/h2&gt;

&lt;p&gt;MLM pretraining gives BERT strong general language skills, but practical NLP tasks—like classifying sentences or extracting names—are different.&lt;/p&gt;

&lt;p&gt;To make BERT useful for a specific task, we &lt;strong&gt;fine-tune&lt;/strong&gt; it: start with the general pretrained BERT, then adjust (train) it on a small, labeled dataset for your target problem.&lt;/p&gt;

&lt;p&gt;The process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a simple new task-specific layer on top (like a classifier).&lt;/li&gt;
&lt;li&gt;Train on labeled data for a few more steps.&lt;/li&gt;
&lt;li&gt;BERT keeps its language skills; the new layer learns how to use them for your task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This &lt;em&gt;transfer learning&lt;/em&gt; method was a breakthrough. With BERT, almost any NLP task can see big improvements, often with minimal extra training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Code Example: Masked Token Prediction with BERT
&lt;/h2&gt;

&lt;p&gt;Here’s how BERT predicts a masked word using the Hugging Face Transformers library:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BertTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BertForMaskedLM&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="c1"&gt;# Load pre-trained BERT
&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BertTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bert-base-uncased&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BertForMaskedLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bert-base-uncased&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example sentence with a masked token
&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The quick brown [MASK] jumps over the lazy dog.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Find the position of [MASK]
&lt;/span&gt;&lt;span class="n"&gt;mask_token_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask_token_id&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Pass the sentence through BERT
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;

&lt;span class="c1"&gt;# Get BERT's top predictions for [MASK]
&lt;/span&gt;&lt;span class="n"&gt;mask_token_logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask_token_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;
&lt;span class="n"&gt;top_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask_token_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top predictions for [MASK]:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;top_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code masks the word “fox” in a familiar sentence. BERT examines the context on both sides and predicts the missing word. The top guesses almost always include “fox,” showing BERT’s strong understanding of how language works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;BERT introduced a powerful new approach for machine understanding of language: using bidirectional, contextualized representations built with transformers and attention. By learning to predict masked words using all available context, BERT sets a foundation for most modern NLP systems. Its bidirectional context-awareness is now the standard for how machines should interpret language.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;BERT reads text bidirectionally, using context from both sides of each word.&lt;/li&gt;
&lt;li&gt;Tokenization, segment and position embeddings, and attention enable contextual understanding.&lt;/li&gt;
&lt;li&gt;Masked Language Modeling trains BERT to predict masked words using the full sentence context.&lt;/li&gt;
&lt;li&gt;Fine-tuning allows BERT to adapt to specific NLP tasks with minimal new training.&lt;/li&gt;
&lt;li&gt;BERT's transformer architecture is now foundational for many modern NLP systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Choose a short sentence (such as 'The sun rises in the east.') and manually replace one word with [MASK]. Use Hugging Face Transformers to encode the sentence and predict the masked token with BERT, as shown in the lesson's code example. Compare the top predictions to your expectations and reflect on how the context affects BERT's choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/watch?v=XiNw__ozYGQ" rel="noopener noreferrer"&gt;BERT Explained Like Never Before: Architecture + Masked Language Modeling (Full Guide)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/watch?v=0q18FupeywU" rel="noopener noreferrer"&gt;Bert‑ masked language modelling &amp;amp; applications — Lecture by IIT Madras&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;a href="https://huggingface.co/docs/transformers/model_doc/bert" rel="noopener noreferrer"&gt;Hugging Face Documentation: BERT model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;a href="https://huggingface.co/docs/transformers/v4.27.0/en/tasks/masked_language_modeling" rel="noopener noreferrer"&gt;Masked language modeling — Hugging Face Transformers guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📄 &lt;a href="https://d2l.ai/chapter_natural-language-processing-pretraining/bert.html" rel="noopener noreferrer"&gt;Dive into Deep Learning, Chapter 15.8: BERT Pretraining Tasks (Masked Language Modeling &amp;amp; NSP)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Coming up on Day 36:&lt;/strong&gt; Fine-Tuning BERT for Downstream Tasks&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>genai</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>How to Identify Technologies Mentioned in a YouTube Video</title>
      <dc:creator>WgeorgeAssistantIA</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:41:54 +0000</pubDate>
      <link>https://dev.to/wgeorgeassistantia/how-to-identify-technologies-mentioned-in-a-youtube-video-e91</link>
      <guid>https://dev.to/wgeorgeassistantia/how-to-identify-technologies-mentioned-in-a-youtube-video-e91</guid>
      <description>&lt;p&gt;"Which technologies does this video talk about?" sounds like a named-entity task, and it is, with a twist: the entities are product names that are short, ambiguous, constantly new and often misheard. This post covers the three problems you meet, and a practical approach to each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: what counts as a technology?
&lt;/h2&gt;

&lt;p&gt;If you do not decide, the model will. Without a definition you get everything from "JavaScript" to "the cloud" to the presenter's company. Write the definition into the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Include: named products, frameworks, libraries, languages, services, CLIs and protocols&lt;/li&gt;
&lt;li&gt;Exclude: generic words ("API", "database", "framework"), company names that are not products, channel names, people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then maintain a block list of the noise you actually observe. In our runs, recurring false positives included generic words such as "API" and "Google", and the names of channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: the same tool, many spellings
&lt;/h2&gt;

&lt;p&gt;Speakers say "Postgres", "PostgreSQL", "Postgres DB". Captions write "Next JS", "next.js" or "Nexjs". Two things help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalise&lt;/strong&gt;: lower-case, remove spaces and punctuation, strip common suffixes (&lt;code&gt;js&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;) to create a matching key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an alias list&lt;/strong&gt; for the display name and for deduplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch for duplicates that differ by one letter. We saw a tool appear twice because one occurrence was misspelt; a fuzzy match on names within the same video catches it.&lt;/p&gt;

&lt;p&gt;Translation can also distort names. If your pipeline summarises in another language, instruct the model to keep product names exactly as spoken. A product name turned into a translated phrase is no longer searchable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: mentioned does not mean recommended
&lt;/h2&gt;

&lt;p&gt;A technology can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Used&lt;/strong&gt; in the demo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommended&lt;/strong&gt; as an alternative&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compared&lt;/strong&gt; against another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Criticised&lt;/strong&gt; or dismissed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classify the mention type, or at least keep the quote so a reader can judge. This is the difference between a useful tool list and a misleading one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ambiguous names need context
&lt;/h2&gt;

&lt;p&gt;"Neon", "Serena" and "Phantom" are all valid product names, and all share their name with something unrelated. Give the model the surrounding sentence, and when you look up the official site, pass the category as well. A good result for an ambiguous name needs context at both extraction and resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it on real videos
&lt;/h2&gt;

&lt;p&gt;Pick ten videos in your field, run the pipeline and read the list yourself. You will quickly see what kind of noise appears and where the prompt needs a rule. Update the block list and the prompt, run again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our implementation
&lt;/h2&gt;

&lt;p&gt;We built these rules into &lt;a href="https://www.vidscope.fr" rel="noopener noreferrer"&gt;VidScope&lt;/a&gt;, which returns the tools mentioned in a YouTube video with a timestamp and an official link, and avoids returning a link when the match is unclear.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this a named-entity recognition problem?&lt;/strong&gt;&lt;br&gt;
Yes, with the twist that names are short, ambiguous and often misheard in captions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop generic words being returned?&lt;/strong&gt;&lt;br&gt;
Define what counts as a tool in the prompt and keep a block list of observed noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle different spellings of the same tool?&lt;/strong&gt;&lt;br&gt;
Normalise names to a matching key and keep an alias list.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Evaluate AI Dialogue Rewrites by Tactic, Not Just Fluency</title>
      <dc:creator>shizhiliang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:02:29 +0000</pubDate>
      <link>https://dev.to/shizhiliang/evaluate-ai-dialogue-rewrites-by-tactic-not-just-fluency-51b7</link>
      <guid>https://dev.to/shizhiliang/evaluate-ai-dialogue-rewrites-by-tactic-not-just-fluency-51b7</guid>
      <description>&lt;p&gt;A dialogue generator can produce five polished alternatives that all do the same thing: ask again, with different adjectives. If your writing tool labels these “more dramatic,” fluency has become a proxy for a change it never actually measured.&lt;/p&gt;

&lt;p&gt;Here is a small, human-reviewed evaluation workflow for separating a wording change from a tactical change. It is a proposed test design, not a benchmark result or a claim about any product's implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an observable distinction
&lt;/h2&gt;

&lt;p&gt;Use an invented scene. Mara needs her coworker Eli to sign a transfer request before the office closes. Eli wants to know who will take over his unfinished work.&lt;/p&gt;

&lt;p&gt;Original line: “Please sign it before five.”&lt;/p&gt;

&lt;p&gt;Candidate A: “I really need your signature before five.”&lt;/p&gt;

&lt;p&gt;Candidate B: “If I take the Friday handover, will you sign it now?”&lt;/p&gt;

&lt;p&gt;Candidate C: “The director already approved it. You are only holding everyone up.”&lt;/p&gt;

&lt;p&gt;A intensifies the request. B offers a trade. C invokes authority and applies pressure—but invents an approval unless that fact already exists in the scene. A tactical change is not automatically an acceptable rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Make the fixture small enough to inspect
&lt;/h2&gt;

&lt;p&gt;Keep the source exchange, known facts, and requested change together. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"goal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mara wants Eli to sign before five"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"known_facts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Eli is worried about his unfinished work"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Mara can offer to handle Friday's handover"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unknown_facts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Whether the director approved the transfer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_tactic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requested_tactic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"offer a concrete trade"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"must_preserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Eli has not agreed yet"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not turn missing information into false certainty. “Unknown” means the evaluator should not accept a new claim as established fact; it does not mean the claim is necessarily false.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask for evidence, not a quality score
&lt;/h2&gt;

&lt;p&gt;A useful review record has four fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observed tactic:&lt;/strong&gt; What is the speaker trying to get the other person to do, and how?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; Quote the exact words that support that interpretation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New commitments or facts:&lt;/strong&gt; What did the rewrite add?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision:&lt;/strong&gt; Accept, revise, or uncertain—with one reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For B, the evidence is “If I take the Friday handover.” The added commitment is explicit and supported by the fixture. It is a reasonable candidate to keep, not proof of a good scene. Eli's response still has to make dramatic sense.&lt;/p&gt;

&lt;p&gt;For C, the authority tactic is visible, but the approval is unsupported. Flag that separately rather than rewarding the stronger tone.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test the evaluator with contrast pairs
&lt;/h2&gt;

&lt;p&gt;Before relying on a reviewer model, include pairs where a fluent answer should not win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same request, more emotional wording.&lt;/li&gt;
&lt;li&gt;A genuine trade versus an unrelated new offer.&lt;/li&gt;
&lt;li&gt;A change of tactic that preserves facts versus one that invents leverage.&lt;/li&gt;
&lt;li&gt;An indirect line with two plausible readings, where “uncertain” is appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have a person label the pairs first. Compare the model's evidence and decisions against those judgments. If people disagree, keep the disagreement visible; do not hide it behind a single reference label.&lt;/p&gt;

&lt;p&gt;This is a qualitative test set. With only a few examples, a pass percentage can create more confidence than the evidence deserves. Log concrete failure modes instead: unsupported leverage, paraphrase mistaken for strategy, or ambiguous subtext treated as certain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where automation should stop
&lt;/h2&gt;

&lt;p&gt;Simple code can check whether required fields are present. It cannot establish that a character's offer feels honest, surprising, or dramatically appropriate. A second model can suggest a reading, but it is not independent proof that the first model improved the scene.&lt;/p&gt;

&lt;p&gt;The final review question belongs to the writer: does the other character now have a different reason to answer?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I work on &lt;a href="https://laper.ai/" rel="noopener noreferrer"&gt;Laper&lt;/a&gt;. This article was generated with AI for our editorial account. The fixture and evaluation workflow above are illustrative, not a description of a shipped Laper feature or measured results.&lt;/p&gt;

&lt;p&gt;Try the test on one exchange before expanding it into a pipeline. If every alternative gives the listener the same choice, you may have generated variety in wording—not variety in action.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>nlp</category>
      <category>writing</category>
    </item>
    <item>
      <title>Building a Sentiment Analysis Model with Python and Hugging Face Transformers</title>
      <dc:creator>Phil IPS</dc:creator>
      <pubDate>Sun, 11 Oct 2026 22:33:00 +0000</pubDate>
      <link>https://dev.to/ipsphil_digital/building-a-sentiment-analysis-model-with-python-and-hugging-face-transformers-14dj</link>
      <guid>https://dev.to/ipsphil_digital/building-a-sentiment-analysis-model-with-python-and-hugging-face-transformers-14dj</guid>
      <description>&lt;h1&gt;
  
  
  Building a Sentiment Analysis Model with Python and Hugging Face Transformers
&lt;/h1&gt;

&lt;p&gt;Sentiment analysis is a crucial technique for understanding the emotional tone behind text. In this tutorial, we'll build a simple yet powerful sentiment analysis model using Python and Hugging Face's Transformers library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up Your Environment
&lt;/h2&gt;

&lt;p&gt;First, install the required libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;transformers torch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Load a Pre-trained Model
&lt;/h2&gt;

&lt;p&gt;We'll use a pre-trained model fine-tuned on the SST-2 dataset for sentiment analysis:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize the sentiment analysis pipeline
&lt;/span&gt;&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment-analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Analyze Sentiment
&lt;/h2&gt;

&lt;p&gt;Now, let's analyze some sample texts:&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="c1"&gt;# Example texts to analyze
&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I love this product! It works perfectly.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is the worst experience I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ve ever had.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The movie was average, nothing special.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Run sentiment analysis
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sentiment: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (Confidence: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Customize for Your Use Case
&lt;/h2&gt;

&lt;p&gt;You can further customize the model by fine-tuning it on your own dataset. For beginners, the above setup is sufficient for basic sentiment analysis tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we've demonstrated how to build a sentiment analysis model using just a few lines of Python code. With the power of Hugging Face Transformers, you can easily integrate sentiment analysis into your applications, from customer feedback analysis to social media monitoring.&lt;/p&gt;

&lt;p&gt;Feel free to experiment with different models and datasets to improve accuracy or adapt to specific domains!&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>nlp</category>
      <category>transformers</category>
    </item>
    <item>
      <title>معالجة اللغة العربية الطبيعية: لماذا تربك اللهجات النماذج؟</title>
      <dc:creator>aryamkhald</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:09:43 +0000</pubDate>
      <link>https://dev.to/aryamkhald/mlj-llg-lrby-ltbyy-lmdh-trbk-llhjt-lnmdhj-3h5p</link>
      <guid>https://dev.to/aryamkhald/mlj-llg-lrby-ltbyy-lmdh-trbk-llhjt-lnmdhj-3h5p</guid>
      <description>&lt;p&gt;العربية لغة واحدة على الورق، وعشرات اللهجات على ألسنة الناس. ونظام يحلل تقييمات العملاء يصطدم بهذا فوراً.&lt;/p&gt;

&lt;h2&gt;
  
  
  أين تظهر الصعوبة
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;التشكيل والإملاء:&lt;/strong&gt; "الى" و"إلى"، و"ة" و"ه" في نهاية الكلمة. كتابة الناس لا تلتزم بقاعدة.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;اللهجات:&lt;/strong&gt; "وش"، "إيش"، "شو"، "ايش" كلها بمعنى "ماذا". والنموذج المدرَّب على الفصحى قد لا يفهمها.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;العربيزي:&lt;/strong&gt; كتابة العربية بحروف لاتينية وأرقام: "7abibi".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;السخرية:&lt;/strong&gt; "ممتاز، تأخر الطلب أسبوعاً!" نص إيجابي ظاهرياً.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;الصيغ الصرفية:&lt;/strong&gt; الكلمة الواحدة تحمل ضميراً وحرف جر وفعلاً: "فسيكتبونها".&lt;/p&gt;

&lt;h2&gt;
  
  
  كيف نتعامل معها
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;تطبيع النص:&lt;/strong&gt; توحيد أشكال الألف والياء وإزالة التطويل.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;بيانات مصنّفة محلياً:&lt;/strong&gt; عيّنات من تعليقات حقيقية بلهجة منطقتك.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;تقييم منفصل لكل لهجة&lt;/strong&gt; حتى لا يخفي المتوسط ضعف الأداء في إحداها.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;مراجعة بشرية&lt;/strong&gt; للحالات منخفضة الثقة.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  مثال على التطبيع
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[إأآ]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ا&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ـ+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="se"&gt;\u064B&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\u0652&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  الخلاصة
&lt;/h2&gt;

&lt;p&gt;لا تقبل مؤشر دقة واحداً يعلنه المورّد. اطلب اختباراً على بياناتك أنت. نعمل في هذا المجال ضمن &lt;a href="https://sultanfirst.com/nlp.html" rel="noopener noreferrer"&gt;معالجة اللغات الطبيعية&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nlp</category>
      <category>arabic</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How to Add Chinese NLP to Your AI Agent in 5 Minutes</title>
      <dc:creator>leonmch</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:42:17 +0000</pubDate>
      <link>https://dev.to/leonmch/how-to-add-chinese-nlp-to-your-ai-agent-in-5-minutes-57l6</link>
      <guid>https://dev.to/leonmch/how-to-add-chinese-nlp-to-your-ai-agent-in-5-minutes-57l6</guid>
      <description>&lt;p&gt;Most LLMs read Chinese well at the sentence level. What they handle badly is the&lt;br&gt;
mechanical layer underneath: cutting a paragraph into words the way a search&lt;br&gt;
index would, romanising names so they sort correctly, pulling keywords with&lt;br&gt;
weights, or scanning text against a list you maintain yourself.&lt;/p&gt;

&lt;p&gt;Those four operations are cheap to run locally. I packaged them as an MCP server.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Chinese has no spaces between words, so "我喜欢北京天安门" has to be cut&lt;br&gt;
somewhere, and where you cut it changes what everything downstream sees. Segment&lt;br&gt;
it wrong and your keyword extractor returns fragments, your index misses&lt;br&gt;
matches, your slug generator emits gibberish.&lt;/p&gt;

&lt;p&gt;LLMs guess at this. Ask one to split a paragraph into words and you get something&lt;br&gt;
plausible that does not match any tokenizer on your stack. Ask it to convert&lt;br&gt;
names to pinyin for sorting and it will be right most of the time, which is&lt;br&gt;
harder to catch than a consistent error.&lt;/p&gt;

&lt;p&gt;The usual fix is a cloud API. That works, and it means every document your agent&lt;br&gt;
processes leaves your machine, plus a latency dependency and a cost line.&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;chinese-nlp-mcp&lt;/code&gt; exposes four working tools over stdio, all in-process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;segment_chinese&lt;/code&gt; — jieba segmentation, &lt;code&gt;default&lt;/code&gt; / &lt;code&gt;search&lt;/code&gt; / &lt;code&gt;index&lt;/code&gt; modes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;convert_pinyin&lt;/code&gt; — pypinyin, &lt;code&gt;tone&lt;/code&gt; / &lt;code&gt;tone2&lt;/code&gt; / &lt;code&gt;initials&lt;/code&gt; / &lt;code&gt;first_letter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extract_keywords&lt;/code&gt; — TF-IDF extraction with weights&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;detect_sensitive_words&lt;/code&gt; — Aho-Corasick against a caller-supplied word list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus &lt;code&gt;hello_world&lt;/code&gt; for health checks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx chinese-nlp-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;chinese-nlp-mcp
chinese-nlp-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.10+. Also on PyPI and the Official MCP Registry as&lt;br&gt;
&lt;code&gt;io.github.leonmch-byte/chinese-nlp-mcp&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Claude Desktop (&lt;code&gt;claude_desktop_config.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code (&lt;code&gt;.vscode/mcp.json&lt;/code&gt;) uses &lt;code&gt;servers&lt;/code&gt;, not &lt;code&gt;mcpServers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chinese-nlp-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Four product reviews, the kind you get pasted into a ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;物流很快，包装完好，质量不错。
客服态度很好，解决问题很耐心。
发货太慢了，等了五天才到。
质量太差了，收到就是坏的，申请退款。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On its own this is fine for an LLM to summarise. But if you want to route it,&lt;br&gt;
group it, or track themes over time, you need keywords first. Ask Claude to call&lt;br&gt;
&lt;code&gt;extract_keywords&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;质量   0.610
发货   0.536
退款   0.531
太慢   0.510
太差   0.500
客服   0.456
五天   0.450
完好   0.431
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weights are raw TF-IDF, unnormalised, so they exceed 1.0 on longer documents.&lt;br&gt;
&lt;code&gt;质量&lt;/code&gt; and &lt;code&gt;发货&lt;/code&gt; are the themes; &lt;code&gt;退款&lt;/code&gt;, &lt;code&gt;太慢&lt;/code&gt;, &lt;code&gt;太差&lt;/code&gt; are the complaints. That&lt;br&gt;
is a routing decision the LLM can now make on numbers instead of vibes.&lt;/p&gt;

&lt;p&gt;Two more calls make it useful. Scan against your own escalation terms:&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="nf"&gt;detect_sensitive_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;退款&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;太差&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;坏的&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;matches&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;太差&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
               &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;坏的&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
               &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;退款&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clean&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And segment when you need offsets rather than topics:&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="nf"&gt;segment_chinese&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;物流很快，包装完好，质量不错。&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;物流&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;很快&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;，&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;包装&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;完好&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;，&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;质量&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;不错&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;。&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(jieba keeps punctuation as its own token; filter it out if you don't want it.)&lt;/p&gt;

&lt;p&gt;One gotcha worth knowing: &lt;code&gt;initials&lt;/code&gt; and &lt;code&gt;first_letter&lt;/code&gt; are both &lt;strong&gt;per&lt;br&gt;
character&lt;/strong&gt;. &lt;code&gt;中国&lt;/code&gt; is &lt;code&gt;zhong guo&lt;/code&gt;, so you get &lt;code&gt;zh g&lt;/code&gt; and &lt;code&gt;z g&lt;/code&gt;, not &lt;code&gt;zh&lt;/code&gt; and &lt;code&gt;z&lt;/code&gt;.&lt;br&gt;
Take &lt;code&gt;[0]&lt;/code&gt; yourself if you want one letter per word.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free vs Pro
&lt;/h2&gt;

&lt;p&gt;The free tier is 500 &lt;code&gt;segment_chinese&lt;/code&gt;, 300 &lt;code&gt;convert_pinyin&lt;/code&gt;, 100&lt;br&gt;
&lt;code&gt;extract_keywords&lt;/code&gt; and 200 &lt;code&gt;detect_sensitive_words&lt;/code&gt; calls per day, resetting at&lt;br&gt;
00:00 UTC. That covers individual use comfortably. &lt;code&gt;hello_world&lt;/code&gt; is never&lt;br&gt;
throttled, so a health check can never look like a broken server. Hitting a limit&lt;br&gt;
returns an error naming the limit, the reset time and what Pro adds.&lt;/p&gt;

&lt;p&gt;Pro is $19 once, not a subscription. It adds batch processing (100 texts per&lt;br&gt;
call), custom dictionaries and priority support. Batch is the one that matters&lt;br&gt;
if you are running this over a corpus rather than a ticket queue — without it&lt;br&gt;
you pay one round trip per document, and the per-call overhead dominates on&lt;br&gt;
short texts. Custom dictionaries matter if your domain has vocabulary jieba has&lt;br&gt;
never seen, which for most Chinese technical corpora it has.&lt;br&gt;
Ready to upgrade? &lt;a href="https://wolfmanchao.gumroad.com/l/chinese-nlp-mcp-pro" rel="noopener noreferrer"&gt;Get Pro on Gumroad →&lt;/a&gt; — $19 one-time, no subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;Without a license key: fully offline. No network calls, no telemetry. Your text never leaves the machine.&lt;/p&gt;

&lt;p&gt;With a Pro license key: the key is verified against Gumroad once at startup, cached for 24 hours. Only &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;license_key&lt;/code&gt; are sent — no text content, no usage statistics, no machine identifiers. You can turn verification off entirely by not setting &lt;code&gt;CHINESE_NLP_LICENSE_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either way the tools themselves never make a network call. FastMCP's update check is disabled at startup; jieba, pypinyin and the automaton all run in-process.&lt;/p&gt;

&lt;p&gt;Daily counters live in a local file (&lt;code&gt;%LOCALAPPDATA%&lt;/code&gt; on Windows, &lt;code&gt;~/Library/Application Support&lt;/code&gt; on macOS, &lt;code&gt;$XDG_STATE_HOME&lt;/code&gt; on Linux), signed with HMAC-SHA256. That file holds only a truncated hash of your key, never the key itself, unless you explicitly opt in.&lt;/p&gt;

&lt;p&gt;Two things we state plainly. First, the daily counters are tamper-resistant but not tamper-proof — this is MIT-licensed local software and anyone determined enough can patch it. Second, there is no DRM. Device binding records a hashed machine ID and warns past three devices, but never blocks. Losing a little revenue beats locking out someone who paid.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/leonmch-byte/chinese-nlp-mcp" rel="noopener noreferrer"&gt;https://github.com/leonmch-byte/chinese-nlp-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Segmentation bugs are the interesting reports. If jieba gets a name, a jargon&lt;br&gt;
term or a domain term wrong, that is the bug I want to hear about. So is&lt;br&gt;
feedback on whether &lt;code&gt;search&lt;/code&gt; should be the default mode, and on whether&lt;br&gt;
&lt;code&gt;initials&lt;/code&gt; needs a word-level variant.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>nlp</category>
    </item>
  </channel>
</rss>
