<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Softmax on KbWen Blog</title>
    <link>https://www.kbwen.com/tags/softmax/</link>
    <description>KbWen is a practical technology blog about AI systems, machine learning, Python, data engineering, and software development.</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <image>
      <url>https://www.kbwen.com/images/og-default.png</url>
      <title>KbWen Blog</title>
      <link>https://www.kbwen.com/</link>
    </image>
    
    <lastBuildDate>Thu, 06 Aug 2026 09:16:00 +0800</lastBuildDate><atom:link href="https://www.kbwen.com/tags/softmax/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>為什麼分類的損失函數幾乎都是交叉熵</title>
      <link>https://www.kbwen.com/why-classification-uses-cross-entropy/</link>
      <pubDate>Thu, 06 Aug 2026 09:16:00 +0800</pubDate><dc:creator>KbWen</dc:creator>
      <guid>https://www.kbwen.com/why-classification-uses-cross-entropy/</guid>
      <description>把 softmax 加交叉熵對 logit 的導數一路算出來，結果剛好是預測機率減去標籤。這篇從一個三類別的小例子走進這個梯度，看它為什麼乾淨、又為什麼信心錯得越離譜就修得越用力。</description>
      <content:encoded><![CDATA[<p>訓練分類模型，最後總要把模型的輸出變成一個能調整參數的訊號，而這一步幾乎清一色都用交叉熵（cross-entropy）。回歸問題可以直接拿預測值跟目標值相減、平方，分類很少這麼算。會這樣選，是因為 softmax 接上交叉熵之後，對模型輸出求導會得到一個特別乾淨的結果。下面就把導數算出來，順便看它為什麼會長成這個形狀。</p>
<p>先擺一個小例子在桌上。要分貓、雞、狗三類，手上的圖其實是貓，正確答案寫成 one-hot 向量 <code>y = (1, 0, 0)</code>。模型看完圖，對三類各給一個分數，這三個分數就是 logit，習慣寫成 <code>o = (o₁, o₂, o₃)</code>，比如 <code>o = (2.0, 1.0, 0.1)</code>。logit 本身不是機率，可以是負的，三個加起來也不保證等於 1。</p>
<p>為什麼不乾脆把三個分數當成預測，直接跟 <code>(1, 0, 0)</code> 相減再平方，像回歸一樣？<a href="https://d2l.ai/chapter_linear-classification/softmax-regression.html">Dive into Deep Learning</a> 的 softmax 回歸章節講得很白：把分類當成向量對向量的回歸問題，其實「surprisingly well」，但不理想。問題出在 logit 不具備機率該有的性質，既不保證非負，加起來也不保證是 1。硬拿它們做平方誤差，等於用沒有範圍的東西去逼近機率向量。所以中間得先加一道手續，把 logit 壓成一組合法的機率，這道手續就是 softmax：每個分數取指數，再除以全部指數的總和。套進上面的數字，<code>o = (2.0, 1.0, 0.1)</code> 過完 softmax 大約是 <code>(0.66, 0.24, 0.10)</code>，三個都落在 0 到 1 之間，加起來剛好是 1。</p>
<p>有了機率之後，交叉熵的定義其實很短：把每一類的預測機率取對數、乘上該類的標籤，全部加起來再取負號，寫成 <code>l = −Σₖ yₖ log ŷₖ</code>。標籤是 one-hot 的時候，<code>y</code> 只有正確類別是 1、其餘是 0，加總裡其他項都被乘成 0，所以整條損失只剩下一項：正確類別的機率取負對數。以這張貓的圖來說，模型給貓的機率是 0.66，損失就是 <code>−log(0.66)</code>，大約 0.42。假如模型很有把握、給貓 0.99，損失掉到 <code>−log(0.99)</code> 約 0.01；假如模型幾乎不信是貓、只給 0.01，損失衝到 <code>−log(0.01)</code> 約 4.6。交叉熵只盯著正確類別的機率，機率越低、罰得越重，其他兩類給多少並不直接管。</p>
<p>真正值得停下來的是下一步：把 softmax 接上交叉熵，然後對 logit 求導。推導之前要先把完整的加總寫回來。剛才算數字時走了 one-hot 的捷徑，只留正確類別一項；推導得從 <code>l = −Σₖ yₖ log ŷₖ</code> 出發，每一類都留著，才看得出化簡後的式子是怎麼來的。把 softmax 的定義代進去，加總裡每一項成了 <code>−yₖ log(exp(oₖ) / Σᵢ exp(oᵢ))</code>；分母那個加總跑遍所有類別，跟外層是兩件事，所以換個代號寫成 <code>i</code>。這一步靠的是對數的除法規則，<code>log(a/b)</code> 等於 <code>log a − log b</code>，前面又帶著負號，於是 <code>−log(exp(oₖ) / Σᵢ exp(oᵢ))</code> 就是 <code>log Σᵢ exp(oᵢ) − oₖ</code>，其中 <code>log exp(oₖ)</code> 直接還原成 <code>oₖ</code>。代回加總，損失拆成兩塊：<code>Σₖ yₖ log Σᵢ exp(oᵢ) − Σₖ yₖ oₖ</code>。前一塊裡的 <code>log Σᵢ exp(oᵢ)</code> 跟 <code>k</code> 無關，每一項乘的都是同一個數，可以提到加總外面，留下的 <code>Σₖ yₖ</code> 正好是 one-hot 向量各項相加，等於 1。整條損失於是化簡成 <code>l = log Σᵢ exp(oᵢ) − Σₖ yₖ oₖ</code>。</p>
<p>接下來挑其中一個 logit <code>oⱼ</code> 求偏導。後一塊 <code>Σₖ yₖ oₖ</code> 攤開來是 <code>y₁o₁ + y₂o₂ + y₃o₃</code>，其中只有 <code>yⱼoⱼ</code> 含有 <code>oⱼ</code>，其餘各項對 <code>oⱼ</code> 而言都是常數、導數為 0，所以整塊的偏導就是 <code>yⱼ</code>。前一塊 <code>log Σᵢ exp(oᵢ)</code> 要用連鎖律：外層 <code>log u</code> 的導數是 <code>1/u</code>，<code>u</code> 就是整個指數和 <code>Σᵢ exp(oᵢ)</code>；內層 <code>Σᵢ exp(oᵢ)</code> 對 <code>oⱼ</code> 求導，同樣只有 <code>exp(oⱼ)</code> 含有 <code>oⱼ</code>，其餘都是常數，所以內層留下 <code>exp(oⱼ)</code>。兩層相乘是 <code>exp(oⱼ) / Σᵢ exp(oᵢ)</code>，也就是 softmax 給第 <code>j</code> 類的機率。前後兩塊相減，<code>∂l/∂oⱼ = softmax(o)ⱼ − yⱼ</code>。整段推導沒用到什麼技巧，<code>log</code> 和 <code>exp</code> 在中間互相抵消掉大半，剩下的就是這條式子。</p>
<p>停在這個結果上。梯度白話講就是「模型給某一類的機率，減掉該類到底有沒有發生」。回到貓的圖，<code>y = (1, 0, 0)</code>、softmax 是 <code>(0.66, 0.24, 0.10)</code>，三個 logit 收到的梯度就是 <code>(0.66−1, 0.24−0, 0.10−0)</code>，即 <code>(−0.34, 0.24, 0.10)</code>。正確類別（貓）收到負梯度，梯度下降會把貓的 logit 往上推；另外兩類收到正梯度，會被往下壓。d2l 說這個形狀跟線性回歸是同一個：回歸算的是觀測值減預測值，分類算的是預測機率減實際發生，兩邊長得像並不是巧合。對實作來說，最實際的好處就是梯度好算。</p>
<p>再多看一眼式子，會發現它自己就把「錯得多離譜」量化好了。梯度既然是機率減標籤，大小就天生有界，也好讀。一個根本沒發生的類別，<code>y = 0</code>，模型不管怎麼亂給，梯度頂多接近 1；模型越是自信地把機率堆在錯的類別上，往回修的力道就越接近上限。反過來，正確類別已經給到接近 1 的時候，<code>softmax(o) − y</code> 接近 0，幾乎不再更新。信心錯得越離譜、梯度就越大，這是 <code>softmax(o) − y</code> 直接算出來的結果，沒有另外加規則去調整力道。</p>
<p>實作上不用自己動手串 softmax。PyTorch 的 <a href="https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html"><code>nn.CrossEntropyLoss</code></a> 官方文件寫的是，它吃的是 logit，「the unnormalized logits for each class」；criterion 內部等價於先做 <code>LogSoftmax</code> 再接 <code>NLLLoss</code>，softmax 那一步已經包在裡面了。所以模型最後一層直接輸出原始分數就好：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="nn">nn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">loss_fn</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">CrossEntropyLoss</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3 筆資料、5 個類別；logits，沒有先過 softmax</span>
</span></span><span class="line"><span class="cl"><span class="n">logits</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="n">requires_grad</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">target</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">tensor</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">])</span>   <span class="c1"># 每筆是類別索引</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">loss</span> <span class="o">=</span> <span class="n">loss_fn</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">loss</span><span class="o">.</span><span class="n">backward</span><span class="p">()</span>
</span></span></code></pre></div><p>先自己套一層 softmax 再丟進 <code>CrossEntropyLoss</code>，softmax 就做了兩次，梯度也不再是上面推出來的乾淨式子。</p>
<p>回到開頭那張貓的圖。假如模型不溫吞地給 0.66，而是非常有把握地認定圖裡是雞，機率壓到 <code>(0.01, 0.98, 0.01)</code>，三個 logit 拿到的梯度就是 <code>(0.01−1, 0.98−0, 0.01−0)</code>，也就是 <code>(−0.99, 0.98, 0.01)</code>。貓的 logit 被接近 1 的力道往上抬，雞的 logit 被幾乎同樣的力道壓下去。修正的方向完全由 <code>y</code> 決定。如果 <code>(1, 0, 0)</code> 本身標錯了，這條乾淨的訊號會用同樣大的力道把模型往錯的方向拉，公式並不知道標籤是不是真的。</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
