<?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: Muhammad Abdullah</title>
    <description>The latest articles on DEV Community by Muhammad Abdullah (@muhammad_abdullah_958c011).</description>
    <link>https://dev.to/muhammad_abdullah_958c011</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4045926%2Fc9f0148f-eab7-47f0-b70e-ac8af5534cbc.jpeg</url>
      <title>DEV Community: Muhammad Abdullah</title>
      <link>https://dev.to/muhammad_abdullah_958c011</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammad_abdullah_958c011"/>
    <language>en</language>
    <item>
      <title>Emotion blog post</title>
      <dc:creator>Muhammad Abdullah</dc:creator>
      <pubDate>Fri, 24 Jul 2026 17:31:36 +0000</pubDate>
      <link>https://dev.to/muhammad_abdullah_958c011/emotion-blog-post-3hk8</link>
      <guid>https://dev.to/muhammad_abdullah_958c011/emotion-blog-post-3hk8</guid>
      <description>&lt;h1&gt;
  
  
  The first thing my emotion model did was completely ignore grief
&lt;/h1&gt;

&lt;p&gt;I trained a model to detect emotions in text last week, and the first version had a strange habit: it never once predicted "grief." Not "predicted it badly"  literally never. Zero. Same story for "relief." I'd feed it a sentence dripping with relief and the model would shrug and pick something else.&lt;/p&gt;

&lt;p&gt;At first I assumed I'd broken something. I hadn't. The model was behaving completely rationally. It had just quietly figured out that ignoring those emotions was the smartest thing it could do  and fixing that turned into the whole project.&lt;/p&gt;

&lt;p&gt;Here's the story, including the parts that didn't work and the two hours I lost to a deployment error that had nothing to do with machine learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dataset that sets you up to fail
&lt;/h2&gt;

&lt;p&gt;I used GoEmotions, a Google dataset of about 54,000 Reddit comments, each tagged with some combination of 28 emotions things like joy, anger, gratitude, curiosity, grief. A single comment can carry several at once ("I'm so grateful but honestly terrified"), which makes this a &lt;em&gt;multi-label&lt;/em&gt; problem: for every comment, the model makes 28 separate yes/no calls.&lt;/p&gt;

&lt;p&gt;The catch is buried in the numbers. In the test set, "neutral" shows up 1,787 times. "Grief" shows up &lt;strong&gt;six&lt;/strong&gt; times. "Relief" eleven. "Pride" sixteen. That's not a small imbalance, that's a cliff.&lt;/p&gt;

&lt;p&gt;And this is where my model's laziness came from. When you train with the standard loss function for this kind of task (binary cross-entropy), every example is weighted the same. So think about what the model learns: if it just always guesses "no grief," it'll be right in something like 99.98% of cases, because grief almost never appears. The penalty for ignoring grief entirely is basically a rounding error. The model isn't broken  it's doing exactly what you asked, which is to minimize its loss. Ignoring rare emotions &lt;em&gt;is&lt;/em&gt; the optimal strategy under that loss.&lt;/p&gt;

&lt;p&gt;That's the trap: a model can look fine on paper while silently failing on a third of the categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that hides the problem, and the one that exposes it
&lt;/h2&gt;

&lt;p&gt;When I first measured performance, the overall score looked respectable. That's because I was looking at the wrong number.&lt;/p&gt;

&lt;p&gt;There are two ways to average performance across 28 classes. Micro-F1 pools every single prediction together  which means the handful of super-common emotions dominate the score, and the rare ones barely register. A model can post a healthy Micro-F1 while being useless on grief, and you'd never know.&lt;/p&gt;

&lt;p&gt;Macro-F1 does the opposite: it scores each emotion separately and then averages those scores, so grief counts exactly as much as neutral. The moment I switched to reporting Macro-F1, the real picture showed up  and it was ugly. A big chunk of my 28 classes were sitting near zero.&lt;/p&gt;

&lt;p&gt;If you take one thing from this: pick the metric that measures the thing you actually care about. Micro-F1 would have let me lie to myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: make the model care about rare things
&lt;/h2&gt;

&lt;p&gt;The solution wasn't a bigger model or more training. It was changing the loss function to one that stops treating every mistake as equal  specifically, Asymmetric Loss.&lt;/p&gt;

&lt;p&gt;The intuition is simple. It does two things: it turns down the volume on the easy, confident "obviously not this emotion" predictions the model already gets right, so the training signal shifts toward the hard cases. And it punishes &lt;em&gt;missing&lt;/em&gt; a real emotion harder than it punishes a false alarm which directly attacks the "just say no to everything" shortcut.&lt;/p&gt;

&lt;p&gt;The effect was immediate and it landed exactly where the problem was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grief: 0.00 → 0.35&lt;/li&gt;
&lt;li&gt;Relief: 0.00 → 0.31&lt;/li&gt;
&lt;li&gt;Pride: 0.11 → 0.39&lt;/li&gt;
&lt;li&gt;Nervousness: 0.18 → 0.39&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall Macro-F1 went from 0.48 to 0.53. And crucially, the common emotions didn't suffer  gratitude stayed up around 0.92. I lifted the floor without lowering the ceiling. That's the result I was after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea I was sure would work, and didn't
&lt;/h2&gt;

&lt;p&gt;Here's the part most write-ups leave out.&lt;/p&gt;

&lt;p&gt;Emotions don't appear randomly they travel in packs. Gratitude shows up with admiration. Grief shows up with sadness. So I figured: instead of treating all 28 emotions as independent decisions, what if I made the model &lt;em&gt;learn&lt;/em&gt; those relationships? I added a second training objective (supervised contrastive learning) that pushes the model to represent comments sharing emotions similarly, and comments sharing none differently.&lt;/p&gt;

&lt;p&gt;It made things slightly worse.&lt;/p&gt;

&lt;p&gt;Not dramatically about a point of Macro-F1  but it lost. I tried tuning its strength; still lost. After digging in, the reason is structural: this technique needs lots of examples-that-share-labels in each training batch to get a useful signal, and I was capped at 32 examples per batch on a free GPU. With 28 sparse emotions, most batches just didn't have enough overlap. The idea wasn't wrong; my hardware couldn't give it a fair test.&lt;/p&gt;

&lt;p&gt;I kept it in the final writeup anyway, clearly marked as "tried, didn't beat the simpler approach." A negative result you understand is worth more than pretending you only ever had good ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two hours that had nothing to do with machine learning
&lt;/h2&gt;

&lt;p&gt;The modeling took an afternoon. Getting the thing &lt;em&gt;deployed&lt;/em&gt; is what nearly broke me.&lt;/p&gt;

&lt;p&gt;DeBERTa-v3, the model I used, silently produces garbage (NaN values) under half-precision training a known quirk I didn't know about, which cost me a wasted training run that came out as all zeros. Then when I tried to put the demo online, the hosting platform quietly ran it on a brand-new version of Python that PyTorch doesn't support yet, so it kept insisting "torch isn't installed" when torch was right there in my requirements. That one took embarrassingly long to diagnose.&lt;/p&gt;

&lt;p&gt;None of this is in the papers. It's the tax you pay for shipping something real instead of leaving it in a notebook. Worth it, but nobody warns you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself starting over
&lt;/h2&gt;

&lt;p&gt;Grief is still basically unlearnable at six examples. No loss function fixes that  the honest answer is that some problems are data problems, not modeling problems, and cleverness has limits.&lt;/p&gt;

&lt;p&gt;But the core lesson held up: before reaching for a fancier model, look at &lt;em&gt;how&lt;/em&gt; your current one is failing and fix the specific thing. The imbalance was the disease; the fancy contrastive idea was a cure for a different illness. The boring loss-function swap was the one that mattered.&lt;/p&gt;

&lt;p&gt;The model's live if you want to poke at it, and the code and full results are on GitHub. Type in something complicated and mixed — that's where emotion detection gets interesting.&lt;/p&gt;




&lt;p&gt;*Model and demo links in the repo. Built on DeBERTa-v3 + GoEmotions, trained on a free Kaggle GPU.&lt;br&gt;
&lt;a href="https://github.com/Dev-Abdullah-17/goemotions-emotion-demo" rel="noopener noreferrer"&gt;https://github.com/Dev-Abdullah-17/goemotions-emotion-demo&lt;/a&gt;&lt;br&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%2Fy5ul6bs3kf835niu2wcr.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%2Fy5ul6bs3kf835niu2wcr.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://goemotions-emotion-demo-cksccxfpdawgisu4esdrzu.streamlit.app/" rel="noopener noreferrer"&gt;https://goemotions-emotion-demo-cksccxfpdawgisu4esdrzu.streamlit.app/&lt;/a&gt;&lt;/p&gt;

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