<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Yuanpeng QU</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://qyp9909.github.io/</id>
  <link href="https://qyp9909.github.io/" rel="alternate"/>
  <link href="https://qyp9909.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Yuanpeng QU</rights>
  <subtitle>Ph.D. in Engineering</subtitle>
  <title>Kyoku's Blog</title>
  <updated>2025-08-20T02:14:52.660Z</updated>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h1>LeetCode Hot100-Day 2</h1><p>Created by: Yuanpeng QU<br>Created time: 2025年8月15日 14:37</p><h2 id="2-两数相加-mid">2. 两数相加[mid]</h2><p>给定两个链表l1，l2，表示逆向储存的数字，比如1→2→3表示321，求两个链表表示的数字相加的和的链表，两个链表的长度可以不一样，每个节点的值不超过10。</p><p>实际上逆向储存的意思就是刚好按照个十百千这个顺序来的。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Definition for singly-linked list.</span></span><br><span class="line"><span class="comment"># class ListNode(object):</span></span><br><span class="line"><span class="comment">#     def __init__(self, val=0, next=None):</span></span><br><span class="line"><span class="comment">#         self.val = val</span></span><br><span class="line"><span class="comment">#         self.next = next</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">addTwoNumbers</span>(<span class="params">self, l1, l2</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type l1: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :type l2: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        dummy_head = ListNode()</span><br><span class="line">        current = dummy_head</span><br><span class="line">        carry = <span class="number">0</span> <span class="comment"># 进位</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> l1 <span class="keyword">or</span> l2 <span class="keyword">or</span> carry:</span><br><span class="line">          l1_val = l1.val <span class="keyword">if</span> l1 <span class="keyword">else</span> <span class="number">0</span></span><br><span class="line">          l2_val = l2.val <span class="keyword">if</span> l2 <span class="keyword">else</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># 个位开始相加(包含上一位的进位)</span></span><br><span class="line">          sum_ = l1_val + l2_val + carry</span><br><span class="line">          d = sum_ % <span class="number">10</span></span><br><span class="line">          carry = sum_ // <span class="number">10</span></span><br><span class="line"></span><br><span class="line">          current.<span class="built_in">next</span> = ListNode(d) <span class="comment">#第一次创建的话就是在dummy.next</span></span><br><span class="line">          current = current.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> l1:</span><br><span class="line">            l1 = l1.<span class="built_in">next</span></span><br><span class="line">          <span class="keyword">if</span> l2:</span><br><span class="line">            l2 = l2.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> dummy_head.<span class="built_in">next</span></span><br></pre></td></tr></table></figure><ul><li><p>关键思路：明白逆向的意义，以及知道储存相加导致的<strong>进位carry</strong></p></li><li><p>不知道的话就会想到将链表读取值后转成数组，再转成数字，相加后再转换回链表，这样复杂度爆炸，因为如果链表长度特别长的话运行时间肯定超</p></li><li><p>知道逆向存储是按照个十百千这个顺序的话，还需要再知道一个关键：</p><p><strong>num % 10 ⇒对任意一个数取余数</strong>，比如7%10=7, 12%10=2，就能得到其个位数是什么</p><p><strong>num // 10 ⇒对任意一个数取10位数的值</strong>，可以表示要进几位，比如7//10=0,17//10=1</p><p>这样任意一个数就能通过num%10 num//10-拼在一起得到</p></li><li><p>开始的时候先定一个哑巴指针dummy让curr指向它，最后dummy.next指向的链表作为输出。</p></li><li><p><code>while l1 or l2 or carry：</code>只要其中有不为0或none的话就还需要继续算，先取出l1和l2第一个节点的值，也就是个位数将他们和carry相加，得到结果，因为是个位所以carry=0不会造成进位，然后求出<strong>现在这个数的值(%10)和是否需要进位(//10)</strong>，将这个值存到<code>curr.next</code>上，而<code>cur.next</code>还没被创建，也就是得先创建新节点用来存值，再将cur指向新节点的next，最后再把l1和l2向后移一位(要判断下一位是否为空)</p></li></ul><h2 id="19-删除链表的倒数第-N-个结点-mid"><strong>19. 删除链表的倒数第 N 个结点[mid]</strong></h2><p>1→2→3→4→5   n=2 ⇒ 1→2→3→5</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">removeNthFromEnd</span>(<span class="params">self, head, n</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :type n: int</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        dummy_head = ListNode(<span class="number">0</span>,head)</span><br><span class="line">        current = head</span><br><span class="line">        size = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> current:</span><br><span class="line">          current = current.<span class="built_in">next</span></span><br><span class="line">          size += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        p = size - n</span><br><span class="line">        current = head</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> p == <span class="number">0</span>:</span><br><span class="line">          head = head.<span class="built_in">next</span></span><br><span class="line">          <span class="keyword">return</span> head</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(p-<span class="number">1</span>):</span><br><span class="line">          current = current.<span class="built_in">next</span></span><br><span class="line">        current.<span class="built_in">next</span> = current.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line">        <span class="keyword">return</span> dummy_head.<span class="built_in">next</span></span><br></pre></td></tr></table></figure><ul><li>最直观的想法：先计算链表的长度size，再通过<code>size-n</code>得到需要删除的节点所在的正向顺序，然后再遍历一遍链表到这个位置的前一个节点，将这个节点的nextnext指向删掉的节点的next，这样就删掉了。</li><li>注意如果<code>size-n==0</code>，也就是说是head节点被删了，就需要head.next赋给现在的head，这样原来的head就删了。</li></ul><h2 id="21-合并两个有序链表-easy">21. 合并两个有序链表[easy]</h2><p>两个值从小到大排列的链表合并成一个，合并完的链表仍然是从小到大排序的</p><p>1→2→3→4→5</p><p>1→3→4→4→8</p><p>1→1→2→3→3→4→4→4→5→8</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">mergeTwoLists</span>(<span class="params">self, list1, list2</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type list1: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :type list2: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        dummy_head = ListNode(<span class="number">0</span>)</span><br><span class="line">        current = dummy_head</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> list1 <span class="keyword">and</span> list2:</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> list1.val &lt;= list2.val:</span><br><span class="line">            current.<span class="built_in">next</span> = list1</span><br><span class="line">            list1 = list1.<span class="built_in">next</span></span><br><span class="line">          <span class="keyword">else</span>:</span><br><span class="line">            current.<span class="built_in">next</span> = list2</span><br><span class="line">            list2 = list2.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">          current = current.<span class="built_in">next</span></span><br><span class="line">        <span class="keyword">if</span> list1:</span><br><span class="line">          current.<span class="built_in">next</span> = list1</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">          current.<span class="built_in">next</span> = list2</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> dummy_head.<span class="built_in">next</span>  </span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>关键点：有序链表，可以把两个链表的节点比较大小后像拉链一样一个一个塞到新链表里</li><li>一般需要新建链表的时候都会提前初始化一个<code>dummy_head=ListNode(0)</code>，然后让它指向我们最后要返回的链表。</li><li>只要list1和list2都不为空才需要比较大小，如果有一个为空的话另一个不为空的显然要比所有的值都要大，因为是递增链表。这样的话直接将剩下的值拼接到合并的链表最后面就完成了。</li><li>比大小则是current指向新链表，然后将值小的节点放到current指的位置，current往后移，如此重复。</li></ul><h2 id="141-环形链表-easy"><strong>141. 环形链表[easy]</strong></h2><p>判断一个链表有没有环，有就返回true，其中pos只是告诉你例题中环的所指位置</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">hasCycle</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: ListNode</span></span><br><span class="line"><span class="string">        :rtype: bool</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        fast, slow = head, head</span><br><span class="line">        <span class="keyword">while</span> fast <span class="keyword">and</span> fast.<span class="built_in">next</span>:</span><br><span class="line">            fast = fast.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line">            slow = slow.<span class="built_in">next</span></span><br><span class="line">            <span class="keyword">if</span> fast == slow:</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure><ul><li>核心思想：运动场跑道上跑得快和跑得慢的人同时从起点出发，<strong>跑得快的人肯定会在某圈后和跑的慢的人相遇</strong></li><li>直接创建一个fast和slow指针指向head，fast每次遍历两个节点，slow遍历一个，这样如果没有环的话fast肯定先到末尾，fast.next会指向none，以及fast自己也可能是none，这样就能作为while的停止条件了，而如果有环的话肯定会有一个时间点有<code>fast == slow</code>，这个时候返回true</li></ul><h2 id="142-环形链表-II-mid"><strong>142. 环形链表 II[mid]</strong></h2><p>和141一样，但是要求第一次遇到环的时候环的起点位置。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">detectCycle</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: ListNode</span></span><br><span class="line"><span class="string">        :rtype: ListNode</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        hashmap = <span class="built_in">set</span>()</span><br><span class="line">        current = head</span><br><span class="line">        <span class="keyword">while</span> current:</span><br><span class="line">            <span class="keyword">if</span> current <span class="keyword">in</span> hashmap:</span><br><span class="line">                <span class="keyword">return</span> current</span><br><span class="line">            hashmap.add(current)</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><ul><li>解法一还是弗洛伊德法，也就是141的方法，最高效O（1）但是难懂，我选择解法二，也就是哈希表查表秒杀</li><li>关键：哈希表不能用带默认值int的，因为链表和数组不一样没法确定节点index，所以set()才是最好的，也就是说循环遍历链表的同时将节点整个加入set中（add），然后同时判断set里面有没有重复的节点，有的话直接返回，没有的话循环到最后肯定会跳出，也就是current.next最后肯定是none，返回false即可。</li></ul><h2 id="160-相交链表-easy"><strong>160. 相交链表[easy]</strong></h2><p>给定两个链表，判断他们到那个节点会相交，相交后成为一个链表，也就是【Y】逆时针90度的样子</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">getIntersectionNode</span>(<span class="params">self, headA, headB</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head1, head1: ListNode</span></span><br><span class="line"><span class="string">        :rtype: ListNode</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> headA <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> headB <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">        pA = headA</span><br><span class="line">        pB = headB</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> pA != pB:</span><br><span class="line">            <span class="keyword">if</span> pA <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">                pA = headB</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                pA = pA.<span class="built_in">next</span></span><br><span class="line">            <span class="keyword">if</span> pB <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">                pB = headA</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                pB = pB.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 简洁版：</span></span><br><span class="line">            <span class="comment"># pA = pA.next if pA else headB</span></span><br><span class="line">            <span class="comment"># pB = pB.next if pB else headA</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> pA</span><br></pre></td></tr></table></figure><ul><li>核心思路：一个指针从走完A链表再走B链表 == 另一个指针走完B链表再走A链表，如果他们有交点，那到最后两指针走的路肯定相等，并且最后的交汇处就是交点：假如AB链表相交后的链表为C，那就有指针1的路径A+C+B，指针2的路径B+C+A,显然这两个路径是相等的，并且都走到底后就该走C了，他们会相交。如果没有交点的话他们不相交，最后两个指针的值肯定都是none。<strong>注意while的条件就是他们不相交就一直遍历，如果不相交最后也会因为都是none而跳出循环。</strong></li><li>或者<strong>用哈希表存A链表的每一个节点也可以，再遍历B</strong>，遍历的时候去哈希表里查有没有和A重复，重复的第一个节点就是相交节点，直接返回</li></ul><h2 id="206-反转链表-easy"><strong>206. 反转链表[easy]</strong></h2><p>将给定链表反转</p><p>两种方法都常考，分别是指针法和递归法</p><ul><li>指针法可以把链表想象成火车，A→B→C→D，现在对B施工（current）想将它的车头和车尾调过来，第一步就是先把B前头的牵引B→C系上安全绳（tmp = current.next）防止前面的车厢丢了，第二步就可以将牵引接到A上了（current.next = prev），完事后需要到下个车厢C去继续调转车头车尾，需要从B“过去”，B车厢则是之前的工作地(current = prev)此时current还是B车厢，最后我们到了C车厢，C车厢则成为现在的工作地（current = tmp）</li><li>原链表:1→2→3→4(pre=null)<br>1: null←1 2→3→4(pre=1)<br>2: null←1←2 3→4(pre=2)<br>3: null←1←2←3 4(pre=3)<br>4: null←1←2←3←4(pre=4)</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">prev = <span class="literal">None</span></span><br><span class="line">current = head</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> current:</span><br><span class="line">    tmp = current.<span class="built_in">next</span></span><br><span class="line">    current.<span class="built_in">next</span> = prev</span><br><span class="line">    prev = current</span><br><span class="line">    current = tmp</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> prev <span class="comment"># 返回的是prev！！！！</span></span><br></pre></td></tr></table></figure><ul><li>递归法：利用head和head.next==None的条件来递归调用自己</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">reverseList</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> head <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> head.<span class="built_in">next</span> == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">return</span> head <span class="comment"># 链表为空或者只有一个节点直接返回本身,同时作为递归停止条件</span></span><br><span class="line">        </span><br><span class="line">        newHead = <span class="variable language_">self</span>.reverseList(head.<span class="built_in">next</span>)</span><br><span class="line"></span><br><span class="line">        head.<span class="built_in">next</span>.<span class="built_in">next</span> = head</span><br><span class="line">        head.<span class="built_in">next</span> = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> newHead</span><br></pre></td></tr></table></figure><ul><li>假如要反转1→2→3，那么递归法首先会执行到<code>newHead = self.reverseList(head.next)</code>, 其中<code>head==1</code>，所以<code>head.next==2</code>，把2作为head再调用自己一次，此时的<code>newHead = self.reverseList(1)</code>暂停，等待递归结果返回，现在进入<code>newHead = self.reverseList(2)</code>，</li><li>2里面同样不满足if的递归停止判断，来到<code>newHead = self.reverseList(2.next)</code> 也就是 <code>self.reverseList(3)</code>，到了 <code>self.reverseList(3)</code>，满足了if条件，<code>return head==3</code>，这下 <code>self.reverseList(2)</code>等到了返回的结果也就是<code>newHead = 3</code>，但是在 <code>self.reverseList(2)</code>中head还是2，<code>head.next == 3</code>,</li><li>我们直接反转链表，也就是将<code>head.next.next = 2</code>，也就是将3的next直接设为2（这个时候head还是2，而newhead才是3），就是当前节点这样就出现了<strong>1 -&gt; 2 &lt;-&gt; 3</strong>，2和3双头都链接了，需要断掉<strong>2→3</strong>这个链子，所以下一步就是<code>head.next = None</code>也就是<code>2.next = none</code>，结果就是<strong>1 -&gt; 2 &lt;- 3</strong>，<code>return newHead</code>回去<code>self.reverseList(1)</code>，相同操作就能得到null←1←2←3了,最后返回的头节点就是<code>newHead==3</code></li></ul><h2 id="24-两两交换链表中的节点-mid"><strong>24. 两两交换链表中的节点[mid]</strong></h2><p>将一个链表里的元素两两分别交换：1-2-3-4  ⇒ 2-1-4-3</p><ul><li>方法一，迭代法，基本就是三个指针两两交换，但是这里最关键也是最方便理解的是设定一个prev指针指向head，并且以prev的视角来交换节点。</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">swapPairs</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        dummy_head = ListNode(<span class="number">0</span>,head)</span><br><span class="line">        prev = dummy_head</span><br><span class="line">        <span class="keyword">while</span> prev.<span class="built_in">next</span> <span class="keyword">and</span> prev.<span class="built_in">next</span>.<span class="built_in">next</span>:</span><br><span class="line">            node1 = prev.<span class="built_in">next</span></span><br><span class="line">            node2 = prev.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># prev-&gt;1-&gt;2-&gt;3-&gt;4  前后两个链不能断，也就是prev.next和node2.next</span></span><br><span class="line">            prev.<span class="built_in">next</span> = node2</span><br><span class="line">            node1.<span class="built_in">next</span> = node2.<span class="built_in">next</span></span><br><span class="line">            node2.<span class="built_in">next</span> = node1</span><br><span class="line">            prev = node1</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> dummy_head.<span class="built_in">next</span></span><br></pre></td></tr></table></figure><ul><li>首先需要设置一个dummy_head用来输出变化完后的整个数组，然后再设置一个prev指向head</li><li>条件是<code>while prev.next</code>和<code>prev.next.next</code> 都不为none，当然可以设值current，但是这里以prev的视角来会更简单。并且设<code>node1=prev.next, node2=prev.next.next</code></li><li>接下来开始迭代的经典操作，<code>prev.next = node2</code> , <code>node1.next = node2.next</code>,  <code>node2.next = node1</code>注意，这三个顺序是可变的，关键在于有没有错误的把后面的链接断掉。最后将prev移动到node1：<code>prev = node1</code>进行下一轮while</li><li>递归法暂时先不看，并且递归法空间复杂度高一点</li></ul><h2 id="25-K一个组翻转链表-hard">25. K一个组翻转链表[hard]</h2><p>给定一个链表和一个k，将链表按长度为k的子链表进行划分，再将每个子链表内部进行翻转，如果划分到最后长度不够k则这部分不用翻转。 1→2→3→4→5, k=2 ⇒ 2→1→4→3→5</p><p>这题主要分3步，1.按k划分 2.子链表内部翻转 3.翻转后的结果拼接到原来链表上</p><ul><li>所以先考虑怎么按k来划分。把长度为k的子链表看成一个整体节点的话，对于这个整体我们需要它前面的指针（group_prev）和后面的指针（group_next）,然后还有指向它自己的指针kth</li><li>所以使用dummy_head方便表示边界和输出，并且将group_prev和他一样指向头部节点。接下来就是遍历链表到k的位置，如果链表的长度不够k则没法翻转任何地方，所以直接跳出，返回。这里的关键是<code>kth = group_prev</code>，kth也从头节点的前一个开始，这样for循环遍历完后kth就会在这一子链表的最后一个节点。同样第二个关键则是还需要判断一次 <code>if kth is None</code>:</li><li>最关键的就是kth到位后，需要把现在的子链表和链表的后续节点链接起来，这样才不会丢失后续节点，也就是 <code>group_next = kth.next</code>。同样也需要保存最开始的节点这样能够在翻转完后能够将开始节点链接到后续节点<code>head_of_group = group_prev.next</code>，因为翻转后它就成子链表的最后节点了。此时group_next指向后续节点，group_prev指向子链表第一个节点前面的节点。</li><li>接下来开始翻转子链表，关键在于需要用到prev和curr专门来对翻转子链表进行操作。（和206题一样）此时<code>prev = group_next</code>(<strong>不同于206，因为206反转完后最后一个节点前面是null，而这题前面则是下一组的第一个节点</strong>)，<code>curr = group_prev.next</code></li><li>翻转子链表内容就和206完全一样，想象一个火车的车厢对调，先系安全绳：<code>next_tmp = curr.next</code>, 然后翻转当前车厢: <code>curr.next = prev</code>, 翻转完成后需要移动到下一个车厢继续，所以现在的上一个车厢则成了刚才施工的车厢：<code>prev = curr</code>, 现在的车厢在哪？在安全绳的后面:  <code>curr = next_tmp</code>。</li><li>完成后的关键就是<strong>拼接回后续链表</strong>（和前面的链表）上，也就是将kth所指位置给到group_prev的next上，这样就能把上一组的末尾节点通过group_prev链接到刚刚翻转完成的这一组的头节点上：<code>group_prev.next = kth</code>（kth在翻转前是这一组的最后面，翻转后就到最前面了，要将前面一组的头部节点和它链接）, 然后就是链接后面节点，head_of_group本来是这组的头节点，翻转完后就到最后一个节点了（下一组的前面一个节点），通过这个公式将group_prev 链接到下一组的前面一个节点：<code>group_prev = head_of_group</code>，注意不能把上面这两个公式一块看，要分开看。至此结束，循环结束后返回dummy_head.next就能得到整个链表</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">reverseKGroup</span>(<span class="params">self, head, k</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :type k: int</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> head <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> k == <span class="number">1</span>:</span><br><span class="line">            <span class="keyword">return</span> head</span><br><span class="line">        </span><br><span class="line">        dummy_head = ListNode(<span class="number">0</span>, head)</span><br><span class="line">        group_prev = dummy_head</span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>: <span class="comment">#</span></span><br><span class="line">            kth = group_prev <span class="comment">#</span></span><br><span class="line">            <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(k): <span class="comment">#</span></span><br><span class="line">                <span class="keyword">if</span> kth <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">                    <span class="keyword">break</span></span><br><span class="line">                kth = kth.<span class="built_in">next</span></span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> kth <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            </span><br><span class="line">            group_next = kth.<span class="built_in">next</span></span><br><span class="line">            head_of_group= group_prev.<span class="built_in">next</span> <span class="comment">#</span></span><br><span class="line"></span><br><span class="line">            prev = group_next</span><br><span class="line">            curr = group_prev.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(k):</span><br><span class="line">                next_tmp = curr.<span class="built_in">next</span></span><br><span class="line">                curr.<span class="built_in">next</span> = prev <span class="comment"># </span></span><br><span class="line">                prev = curr</span><br><span class="line">                curr = next_tmp</span><br><span class="line"></span><br><span class="line">            group_prev.<span class="built_in">next</span> = kth <span class="comment">#</span></span><br><span class="line">            group_prev = head_of_group <span class="comment">#</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> dummy_head.<span class="built_in">next</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/15/leetcode/LeetCode%20Hot100-Day%202/</id>
    <link href="https://qyp9909.github.io/2025/08/15/leetcode/LeetCode%20Hot100-Day%202/"/>
    <published>2025-08-15T05:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>LeetCode Hot100-Day 2</h1>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月15日 14:37</p>
<h2 id="2-两数相加-mid">2. 两数相加[mid]</h2>
<p>给定]]>
    </summary>
    <title>LeetCode Hot100-Day 2</title>
    <updated>2025-08-20T02:14:52.660Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h1>LeetCode Hot100-Day 1</h1><p>Created by: Yuanpeng QU<br>Created time: 2025年8月14日 11:27</p><h2 id="15-三数之和-mid">15. 三数之和[mid]</h2><p>给一个数组nums，求 满足nums[i]+nums[j]+nums[k]==0的所有[nums[i],nums[j],nums[k]]组合，顺序没有要求，但是不能有重复的组合出现</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">threeSum</span>(<span class="params">self, nums</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :rtype: List[List[int]]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        nums.sort()</span><br><span class="line">        </span><br><span class="line">        n = <span class="built_in">len</span>(nums)</span><br><span class="line">        <span class="comment"># left, right = 1, n-1 关键2</span></span><br><span class="line">        res = []</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> k <span class="keyword">in</span> <span class="built_in">range</span>(n):</span><br><span class="line">            <span class="keyword">if</span> nums[k] &gt; <span class="number">0</span>:</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> k &gt; <span class="number">0</span> <span class="keyword">and</span> nums[k] == nums[k-<span class="number">1</span>]:</span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">            left, right = k+<span class="number">1</span>, n-<span class="number">1</span> <span class="comment"># 关键2</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> left &lt; right:</span><br><span class="line">                <span class="keyword">if</span> nums[left] + nums[right] + nums[k] &lt; <span class="number">0</span>: <span class="comment"># 关键2</span></span><br><span class="line">                    left += <span class="number">1</span></span><br><span class="line">                <span class="keyword">elif</span> nums[left] + nums[right] + nums[k] &gt; <span class="number">0</span>:</span><br><span class="line">                    right -= <span class="number">1</span></span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    res.append([nums[k],nums[left],nums[right]])</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">while</span> left &lt; right <span class="keyword">and</span> nums[left] == nums[left+<span class="number">1</span>]:<span class="comment"># 关键1</span></span><br><span class="line">                        left += <span class="number">1</span></span><br><span class="line">                    <span class="keyword">while</span> left &lt; right <span class="keyword">and</span> nums[right] == nums[right-<span class="number">1</span>]:</span><br><span class="line">                        right -= <span class="number">1</span></span><br><span class="line">                    </span><br><span class="line">                    left += <span class="number">1</span> <span class="comment">#关键2</span></span><br><span class="line">                    right -= <span class="number">1</span></span><br><span class="line">        <span class="keyword">return</span> res</span><br></pre></td></tr></table></figure><ul><li>我想到的地方：先把nums排序，这样就能用<strong>双指针</strong>做，也就是先固定一个k，再求<code>nums[i]+nums[j]</code>判断其是否等于<code>-nums[k]</code>从而得到一组结果。但是错误的是我想着同时挪动两个指针往中间靠了，所以没写出来。</li><li>关键1：去重，k和left和right所对应的nums的值可能和上一个或下一个重复，需要跳过，并且需要一个list保存结果</li><li>关键2：<code>left, right = k+1, n-1</code>要在for 里面这样才能确保left能够更新，并且<code>nums[left] + nums[right] + nums[k] &lt; 0</code> 的时候是<code>left+=1</code>而不是<code>&gt;0</code>的时候，仔细想想就知道如果<code>nums[left] + nums[right]</code>的值要小过结果的话，只有把<code>nums[left]</code>往大的值挪才有可能让结果<code>==0</code>。对于<code>nums[right]</code>也同理。 只有最后<code>等于0</code>的结果出现了才需要<code>left和right都往中间挪</code>，这是因为它们现在所在位置的值都被用上了，避免重复使用。</li></ul><h2 id="283-移动零-easy">283. 移动零[easy]</h2><p>给一个数组nums，将所有0移动到最后面</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">moveZeroes</span>(<span class="params">self, nums</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :rtype: None Do not return anything, modify nums in-place instead.</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        slow, fast = <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">while</span> fast &lt; <span class="built_in">len</span>(nums):</span><br><span class="line">            <span class="keyword">if</span> nums[fast] != <span class="number">0</span>:</span><br><span class="line">                nums[slow] = nums[fast]</span><br><span class="line">                slow += <span class="number">1</span></span><br><span class="line">            fast += <span class="number">1</span></span><br><span class="line">        <span class="keyword">while</span> slow &lt; <span class="built_in">len</span>(nums):</span><br><span class="line">            nums[slow] = <span class="number">0</span></span><br><span class="line">            slow += <span class="number">1</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> nums</span><br></pre></td></tr></table></figure><ul><li>关键思路：<strong>双指针（快慢指针版）</strong></li><li>一开始就想错了不能用朝中间靠拢版的双指针，问题的关键在于使用fast遍历整个数组，slow则用于储存非0的数后+1，并且两个指针开始都指向0，这样就能保证slow和其左边的值都是非0的，最后等fast遍历完后将slow的右边全部赋0就可以了。</li><li>改进：用for循环替代<code>while fast&lt;len(nums):  fast+=1</code>和<code>while slow &lt; len(nums): slow += 1</code>。这会更加python！</li></ul><h2 id="75-颜色分类-mid">75. 颜色分类[mid]</h2><p>给一个数组nums里面元素只有0，1，2，按顺序表示红白蓝，现在要求按照这个顺序把nums内所有的元素原地排序，不能用sort，进阶方法是只遍历一次</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">sortColors</span>(<span class="params">self, nums</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :rtype: None Do not return anything, modify nums in-place instead.</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        dict_num = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">            <span class="keyword">if</span> nums[i] == <span class="number">0</span>:</span><br><span class="line">                dict_num[<span class="number">0</span>] += <span class="number">1</span></span><br><span class="line">            <span class="keyword">elif</span> nums[i] == <span class="number">1</span>:</span><br><span class="line">                dict_num[<span class="number">1</span>] += <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                dict_num[<span class="number">2</span>] += <span class="number">1</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">            <span class="keyword">if</span> j &lt; dict_num[<span class="number">0</span>]:</span><br><span class="line">                nums[j] = <span class="number">0</span></span><br><span class="line">            <span class="keyword">elif</span> j &lt; dict_num[<span class="number">0</span>]+dict_num[<span class="number">1</span>]:</span><br><span class="line">                nums[j] = <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                nums[j] = <span class="number">2</span></span><br><span class="line">            </span><br><span class="line">        <span class="keyword">return</span> nums  </span><br></pre></td></tr></table></figure><ul><li>我的做法：<strong>哈希储存+遍历2次</strong></li><li>简单直观的想法就是遍历第一次找一个地方储存0，1，2出现的次数，再遍历一次将出现0，1，2出现的次数分别填上去</li><li>缺点是空间复杂度高</li><li>最优解是3指针，</li><li><code>p0</code>：指向 <code>0</code> 区的<strong>末尾</strong>。初始时 <code>p0 = 0</code>。<code>[0, p0-1]</code> 区间内的都是<code>0</code>。</li><li><code>p2</code>：指向 <code>2</code> 区的<strong>开头</strong>。初始时 <code>p2 = len(nums) - 1</code>。<code>[p2+1, n-1]</code> 区间内的都是<code>2</code>。</li><li><code>curr</code>：当前遍历到的元素的指针。初始时 <code>curr = 0</code>。</li></ul><p>我们让 <code>curr</code> 指针从头到尾遍历数组，在 <code>curr &lt;= p2</code> 的条件下循环，根据 <code>nums[curr]</code> 的值来执行不同的操作：</p><ul><li><strong>如果 <code>nums[curr] == 0</code></strong>:<ul><li>说明这个 <code>0</code> 应该被放到 <code>0</code> 区。</li><li>我们将 <code>nums[curr]</code> 和 <code>nums[p0]</code> 进行交换。</li><li>交换后，<code>p0</code> 指向的元素已经处理完毕，所以 <code>p0</code> 向右移动一位 (<code>p0 += 1</code>)。</li><li><code>curr</code> 指针也向右移动一位 (<code>curr += 1</code>)，继续检查下一个元素。</li></ul></li><li><strong>如果 <code>nums[curr] == 2</code></strong>:<ul><li>说明这个 <code>2</code> 应该被放到 <code>2</code> 区。</li><li>我们将 <code>nums[curr]</code> 和 <code>nums[p2]</code> 进行交换。</li><li>交换后，<code>p2</code> 指向的元素已经处理完毕，所以 <code>p2</code> 向左移动一位 (<code>p2 -= 1</code>)。</li><li><strong>注意：</strong> 此时 <code>curr</code> 指针<strong>不能移动</strong>！因为从 <code>p2</code> 交换过来的那个新 <code>nums[curr]</code> 还没有被检查过，它可能是 <code>0</code>、<code>1</code> 或 <code>2</code>，需要下一轮循环来处理它。</li></ul></li><li><strong>如果 <code>nums[curr] == 1</code></strong>:<ul><li>说明这个 <code>1</code> 就在它应该在的位置（在 <code>0</code> 区和 <code>2</code> 区之间）。</li><li>我们不需要做任何交换，只需要移动 <code>curr</code> 指针，继续检查下一个元素 (<code>curr += 1</code>)。</li></ul></li></ul><p>循环在 <code>curr</code> 指针“追上”或超过 <code>p2</code> 指针时结束。此时，整个数组的排序就完成了。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">List</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">sortColors</span>(<span class="params">self, nums: <span class="type">List</span>[<span class="built_in">int</span>]</span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        Do not return anything, modify nums in-place instead.</span></span><br><span class="line"><span class="string">        荷兰国旗问题解法 (三指针)</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        n = <span class="built_in">len</span>(nums)</span><br><span class="line">        <span class="comment"># p0 指向 0 区的末尾边界</span></span><br><span class="line">        <span class="comment"># p2 指向 2 区的开头边界</span></span><br><span class="line">        p0, p2 = <span class="number">0</span>, n - <span class="number">1</span></span><br><span class="line">        <span class="comment"># curr 用于遍历数组</span></span><br><span class="line">        curr = <span class="number">0</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 当 curr 还没有“追上” p2 时</span></span><br><span class="line">        <span class="keyword">while</span> curr &lt;= p2:</span><br><span class="line">            <span class="keyword">if</span> nums[curr] == <span class="number">0</span>:</span><br><span class="line">                <span class="comment"># 发现 0，与 p0 指向的元素交换</span></span><br><span class="line">                nums[curr], nums[p0] = nums[p0], nums[curr]</span><br><span class="line">                <span class="comment"># 交换后，p0 和 curr 都向前进一步</span></span><br><span class="line">                p0 += <span class="number">1</span></span><br><span class="line">                curr += <span class="number">1</span></span><br><span class="line">            <span class="keyword">elif</span> nums[curr] == <span class="number">2</span>:</span><br><span class="line">                <span class="comment"># 发现 2，与 p2 指向的元素交换</span></span><br><span class="line">                nums[curr], nums[p2] = nums[p2], nums[curr]</span><br><span class="line">                <span class="comment"># 交换后，p2 向前一步</span></span><br><span class="line">                <span class="comment"># curr 不动，因为换过来的新 nums[curr] 需要被重新判断</span></span><br><span class="line">                p2 -= <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>: <span class="comment"># nums[curr] == 1</span></span><br><span class="line">                <span class="comment"># 发现 1，位置正确，curr 直接向前一步</span></span><br><span class="line">                curr += <span class="number">1</span></span><br></pre></td></tr></table></figure><h2 id="42-接雨水-hard">42.接雨水[hard]</h2><p>典中典</p><p><img src="image.png" alt="image.png"></p><p>给一个数组height表示各个位置上的方块个数（高度），求整个数组表示的图形能接多少个水方块</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">trap</span>(<span class="params">self, height</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type height: List[int]</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        left, right = <span class="number">0</span>, <span class="built_in">len</span>(height)-<span class="number">1</span></span><br><span class="line">        left_max, right_max = <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">        total = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(height) &lt;= <span class="number">2</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> left &lt; right:</span><br><span class="line">            <span class="keyword">if</span> height[left] &lt; height[right]:</span><br><span class="line">                <span class="keyword">if</span> height[left] &gt;= left_max:</span><br><span class="line">                    left_max = height[left]</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    total += left_max - height[left]</span><br><span class="line">                left += <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="keyword">if</span> height[right] &gt;= right_max:</span><br><span class="line">                    right_max = height[right]</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    total += right_max - height[right]</span><br><span class="line">                right -= <span class="number">1</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> total</span><br></pre></td></tr></table></figure><ul><li>核心就是<strong>双指针（往中间靠拢版）</strong></li><li>关键1：这题和11. 盛最多水的容器类似，都是双指针，不同的地方在于11可以通过算left和right围起来的面积来求最大值，而这题可能有多个水坑可以接水，所以需要<strong>设置左边和右边都要有个最大高度来确保装水的量</strong>。而最大高度则通过每次遍历指针的时候与其对比不断更新。</li><li>关键2：确保有最大高度后，只需要计算在遍历的时候<strong>当前的位置头顶上能够顶几个水方块</strong>即可，注意不需要知道当前水坑能够装多少，只需要计算每一个位置的水方块最后加起来就可以。而这个方块数则可以通过最大高度-当前高度来得到。</li><li>关键3：总数小于等于2的时候没法围成水坑，所以直接返回0</li></ul><h2 id="3-无重复字符的最长子串-mid">3. 无重复字符的最长子串[mid]</h2><p>给一个字符串s，求它里面最长的无重复字符的子串长度，其中子串和子序列的区别要搞清楚。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">left = <span class="number">0</span></span><br><span class="line">max_len = <span class="number">0</span></span><br><span class="line">char_set = <span class="built_in">set</span>() </span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> right <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(s)):</span><br><span class="line"><span class="keyword">while</span> s[right] <span class="keyword">in</span> char_set:</span><br><span class="line">char_set.remove(s[left])</span><br><span class="line">left += <span class="number">1</span></span><br><span class="line">char_set.add(s[right])</span><br><span class="line"></span><br><span class="line">max_len = <span class="built_in">max</span>(max_len, right-left+<span class="number">1</span>)</span><br><span class="line"><span class="keyword">return</span> max_len</span><br></pre></td></tr></table></figure><ul><li>模式：<strong>滑动窗口+哈希储存</strong></li><li>想象一个窗口，从左开始，一直把不重复的元素放入set中，直到right遍历到set里重复的元素，这个时候就需要按顺序从左开始删除元素，<strong>直到删除到set里没有重复元素为止，所以要用while而不是if</strong>，最后right走到终点后<code>right-left+1</code>就是最大长度</li><li>关键：使用set储存窗口元素，为什么不用defaultdict（int）？是因为我们<strong>只需要判断x字符在不在哈希表</strong>里，而<strong>不需要判断有多少个x字符</strong>在不在。set集合可以用add和remove来添加和删除</li><li>需要不断更新max_len是因为right到最后，但是最长子串可能在中间，时刻更新max_len确保一直是最大的</li></ul><h2 id="239-滑动窗口最大值-hard">239. 滑动窗口最大值[hard]</h2><p>给定一个数组nums，再给定一个滑动窗口大小k，输出滑动窗口从头到尾中每个窗口的最大值组成的list</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">输入：nums = [1,3,-1,-3,5,3,6,7], k = 3</span><br><span class="line">输出：[3,3,5,5,6,7]</span><br><span class="line">解释：</span><br><span class="line">滑动窗口的位置                最大值</span><br><span class="line">---------------               -----</span><br><span class="line">[1  3  -1] -3  5  3  6  7      3</span><br><span class="line"> 1 [3  -1  -3] 5  3  6  7      3</span><br><span class="line"> 1  3 [-1  -3  5] 3  6  7      5</span><br><span class="line"> 1  3  -1 [-3  5  3] 6  7      5</span><br><span class="line"> 1  3  -1  -3 [5  3  6] 7      6</span><br><span class="line"> 1  3  -1  -3  5 [3  6  7]     7</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">maxSlidingWindow</span>(<span class="params">self, nums, k</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :type k: int</span></span><br><span class="line"><span class="string">        :rtype: List[int]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        left = <span class="number">0</span></span><br><span class="line">        right = left + k - <span class="number">1</span> </span><br><span class="line">        res = []</span><br><span class="line">        max_i = <span class="built_in">float</span>(<span class="string">&#x27;-inf&#x27;</span>)</span><br><span class="line">        <span class="keyword">while</span> right &lt; <span class="built_in">len</span>(nums):</span><br><span class="line">            <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(left, right+<span class="number">1</span>):</span><br><span class="line">                max_i = <span class="built_in">max</span>(max_i, nums[i])</span><br><span class="line">            res.append(max_i)</span><br><span class="line">            right += <span class="number">1</span></span><br><span class="line">            left += <span class="number">1</span></span><br><span class="line">            max_i = <span class="built_in">float</span>(<span class="string">&#x27;-inf&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> res</span><br></pre></td></tr></table></figure><ul><li>我直接暴力解决，但是超时了，因为时间复杂度为O(N*k), N和k都很大时运算量爆炸</li><li>最佳解法是用双向队列deque解决，时间复杂度为O(N)</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">   deque = collections.deque() <span class="comment"># 创建小队</span></span><br><span class="line">   res = []</span><br><span class="line"></span><br><span class="line">   <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">       </span><br><span class="line">       <span class="comment"># 规则3， 队头离开了滑动窗口需要请走</span></span><br><span class="line">       <span class="keyword">if</span> deque <span class="keyword">and</span> deque[<span class="number">0</span>] &lt;= i - k:</span><br><span class="line">           deque.popleft()</span><br><span class="line">       <span class="comment"># 规则2， 每个入队的元素需要和队里所有成员“比武”，比不过的老队员全部踢出去，这样就能保证每一个循环的队头都是最大的</span></span><br><span class="line">       <span class="keyword">while</span> deque <span class="keyword">and</span> nums[deque[-<span class="number">1</span>]] &lt; nums[i]:</span><br><span class="line">           deque.pop()</span><br><span class="line">       <span class="comment"># 规则1， 新人入队， 队里只存nums的index，而不存值</span></span><br><span class="line">       deque.append(i)</span><br><span class="line"></span><br><span class="line">       <span class="comment"># 窗口形成后，将每次队里的最大值（队头）取出</span></span><br><span class="line">       <span class="keyword">if</span> i &gt;= k - <span class="number">1</span>:</span><br><span class="line">           res.append(nums[deque[<span class="number">0</span>]])</span><br><span class="line"><span class="keyword">return</span> res</span><br></pre></td></tr></table></figure><ul><li><p>为什么要用双向队列？</p><ul><li>用暴力法 <code>O(N*k)</code> 之所以慢，是因为每当窗口向右滑动一格时，我们都<strong>重新</strong>在 <code>k</code> 个元素里找了一遍最大值。这里面有大量的重复劳动。</li><li>比如窗口从 <code>[1, 3, -1]</code> 滑动到 <code>[3, -1, -3]</code>。在 <code>[1, 3, -1]</code> 中，<code>3</code> 是最大值。当 <code>1</code> 离开，<code>-3</code> 加入时，我们其实<strong>不需要</strong>重新比较 <code>3</code> 和 <code>-1</code>。我们只需要让新来的 <code>-3</code> 和之前的最大值 <code>3</code> 比较一下就行。</li><li>但问题是，如果离开的元素恰好是最大值怎么办？比如从 <code>[3, -1, -3]</code> 滑动到 <code>[-1, -3, 5]</code>，此时最大值 <code>3</code> 离开了，我们就需要知道剩下元素里的“第二大值”是谁。</li><li>所以，核心难点在于：<strong>我们需要一个数据结构，它能帮我们快速知道“当旧的最大值离开窗口后，新的最大值是谁？”</strong></li></ul></li><li><p><strong>用一个“精英小队”来解决问题 (双端队列 Deque 的比喻)</strong></p><p>想象一下，我们不让窗口里的所有 <code>k</code> 个成员都参与“比武招亲”，而是组织一个“精英小队”（这就是我们的双端队列 Deque）。</p><p>这个小队有几条非常严格的规矩：</p><ol><li><strong>队员身份</strong>：小队里只记录队员的<strong>编号（即数组索引 <code>index</code>）</strong>。</li><li><strong>入队规矩（从队尾进）</strong>：新人 <code>A</code> 想要入队时，要从队尾开始，跟前面的老队员 <code>B</code>、<code>C</code>、<code>D</code> … 挨个比武。如果老队员打不过新人 <code>A</code> (<code>nums[老队员] &lt; nums[A]</code>)，那么这个老队员就直接被<strong>踢出队伍</strong>（从队尾踢出）。<code>A</code> 会一直踢到遇见一个比他强的，或者队伍里没人了，他才从队尾站进去。</li><li><strong>出队规矩（从队头出）</strong>：我们只关心队头的那个人。如果发现他的编号<strong>太老了</strong>（已经滑出窗口范围了），就把他从队头请出去。</li></ol><p><strong>这个规矩带来了什么神奇效果？</strong></p><ul><li>因为规矩2的存在，这个“精英小队”从队头到队尾，永远是<strong>按“武力值”（即 <code>nums</code> 的值）从大到小排列的</strong>。</li><li>因此，<strong>队头的那个队员，永远是当前小队里最能打的</strong>，也就是当前窗口的最大值！</li></ul></li><li><p>这样顺序就是，先创建一个deque，用来储存index，nums里面的第一个值进来后先判断规则3和规则2（最前面有判断deque是否为空）保证第一个元素能直接进来。然后第二个元素进来后需要和第一个元素所对应的nums值进行比较，如果赢了那就把第一个踢掉，值最大的index当队长！保证队头的index对应的值一定是这次for里最大的。</p></li><li><p>以此类推，先等到遍历到窗口长度的时候（<code>i≥k-1</code>,也就是nums的下标到长度k的时候）取出当前窗口最大的队头记录，然后到下一轮循环的时候根据规则3判断窗口的左边界i-k是否已经比队头的index大了将小的index对应的队头移出去就完成移动了。</p></li><li><p>注意最后的<code>if i≥k-1</code>表示是否已经形成窗口，形成窗口后窗口就不会消失了也就是说之后的for循环代表的窗口头边界往前移一位那就会产生一个新的窗口最大值。</p></li></ul><h2 id="438-找到字符串中所有字母异位词-mid">438. 找到字符串中所有字母异位词[mid]</h2><p>给两个字符串s和p，找出s中满足p的所有异位词，异位词就是不管字母顺序只管字符串长度是否相等，字母是否相同。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">findAnagrams</span>(<span class="params">self, s, p</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type s: str</span></span><br><span class="line"><span class="string">        :type p: str</span></span><br><span class="line"><span class="string">        :rtype: List[int]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        s_len, p_len = <span class="built_in">len</span>(s),<span class="built_in">len</span>(p)</span><br><span class="line">        <span class="keyword">if</span> s_len &lt; p_len:</span><br><span class="line">            <span class="keyword">return</span> []</span><br><span class="line">        </span><br><span class="line">        p_dict = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        window = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        res = []</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(p_len):</span><br><span class="line">            p_dict[p[i]] += <span class="number">1</span></span><br><span class="line">            window[s[i]] += <span class="number">1</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> window == p_dict:</span><br><span class="line">            res.append(<span class="number">0</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(p_len, s_len):</span><br><span class="line">            s_in = s[j]</span><br><span class="line">            s_out = s[j-p_len]</span><br><span class="line"></span><br><span class="line">            window[s_in] += <span class="number">1</span></span><br><span class="line">            window[s_out] -= <span class="number">1</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> window[s_out] == <span class="number">0</span>:</span><br><span class="line">                <span class="keyword">del</span> window[s_out]</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> window == p_dict:</span><br><span class="line">                res.append(j - p_len + <span class="number">1</span>) <span class="comment"># 这是子串的头字符的index</span></span><br><span class="line">        <span class="keyword">return</span> res</span><br></pre></td></tr></table></figure><ul><li>这题用滑动窗口+哈希，看到固定字符串长度就想到滑动窗口</li><li>问题的关键在于我们创建的两个带默认值的哈希表，和239题不一样，这次的滑动窗口需要我们先填充元素，也就是说我们创建p的哈希表和window哈希表，p_dict初始化就是将p的值全部放进去变成{’a’:1, ‘c’:2, …}。 而window哈希表也需要初始化，内容就是把它放到s里的第一组值。</li><li>这样我们在初始化之后就可以开始挪动窗口了，指向窗口头部的for循环直接从p_len位置开始直到s结束位置，窗口头部指针指向的值加入window，并且将窗口尾部的指针指向的值从window里扔掉，这样就是<code>window[s[j]]+=1</code>, <code>window[s[j-p_len]] -=1</code></li><li>不出错的关键在于如果window里面被删除的元素没了的话，要使得<code>if window == p_dict</code>生效，一定需要把归零的元素del 掉</li><li>最后通过<code>if window == p_dict</code>判断窗口里的子串是否是p的异位，将是的子串的开头字符的下标<code>j-p_len+1</code>存入res即可。</li></ul><h2 id="506-和为-K-的子数组-mid">506. 和为 K 的子数组[mid]</h2><p>给定一个数组nums，求能够满足和为k的所有子数组，其中的元素是连续的</p><p>这题是经典的前缀和的应用题，什么是前缀和？给定一个长度为j+1的数组nums，有</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">prefix_map[i] = nums[<span class="number">0</span>]+…+nums[i]</span><br><span class="line">prefix_map[j] = nums[<span class="number">0</span>]+…+nums[i] + nums[i+<span class="number">1</span>]+…+nums[j]</span><br></pre></td></tr></table></figure><p>想要求一个连续子数组的和<code>sum(nums[i,..,j])</code>可以通过<code>prefix_map[j]-prefix[i-1]</code>得到</p><p>**最关键：**所以这题能够转化为想要求子数组和为<code>k(nums[i~j])</code>的数目，有：</p><p><code>prefix_map[j] = prefix_map[i-1]+k</code>，也就是<code>prefix_map[i-1] = prefix_map[j] - k</code></p><p>这个时候<code>prefix_map[j]</code>就是for循环遍历的指针指到的位置之前的和，可以记为curr_sum,而k是满足条件的子数组的和，<code>prefix_map[i-1]</code>则是要通过上面公式来在数组中比对的和</p><ul><li>所以这个时候就明确了：我们需要将cur_sum每走一步就记录一次，把记录存在hashmap里，记录完了后再和k相减，看看hashmap里有没有满足相减结果的值，有的话就说明现在有一个前缀和满足了，计数器+1</li><li>关键1：在<strong>hashmap初始化的时候不要忘了0</strong>这个数，因为有可能会出现nums的两个值和刚好被k整减，最后为0的结果，或者是nums里面刚好有等于k的数。所以初始化一定要prefix_map[0] += 1</li><li>关键2：<code>count += prefix_map[target_sum]</code> 而不是<code>count += 1</code></li><li>关键3：<code>prefix_map[curr_sum] += 1</code> 这个一定放在if后面，因为if里面涉及到了prefix_map的更新，也就是如果curr_num等于target_sum的情况（k=0时）如果先把 <code>current_sum</code> 的计数值加了1，然后又用 <code>current_sum</code> 本身作为 <code>target</code> 去查找，你就会把自己刚刚放进去的那个计数值也算进去，导致重复计算。</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">subarraySum</span>(<span class="params">self, nums, k</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :type k: int</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        prefix_map = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        prefix_map[<span class="number">0</span>] = <span class="number">1</span></span><br><span class="line">        curr_sum, count = <span class="number">0</span>, <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">            curr_sum += nums[j]</span><br><span class="line"></span><br><span class="line">            target_sum = curr_sum - k</span><br><span class="line">            <span class="keyword">if</span> target_sum <span class="keyword">in</span> prefix_map:</span><br><span class="line">                <span class="comment"># count += 1 不对，要存的是target_sum的个数，也就是找到多少组</span></span><br><span class="line">                <span class="comment"># 并且count += 1 会导致某个prefix_map[x] = 2的情况时漏掉一次，如nums = [1, -1, 1, -1, 1], k = 1</span></span><br><span class="line">                count += prefix_map[target_sum]</span><br><span class="line"></span><br><span class="line">            prefix_map[curr_sum] += <span class="number">1</span> <span class="comment">#这个一定放在if后面</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> count</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/14/leetcode/LeetCode%20Hot100-Day%201/</id>
    <link href="https://qyp9909.github.io/2025/08/14/leetcode/LeetCode%20Hot100-Day%201/"/>
    <published>2025-08-14T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>LeetCode Hot100-Day 1</h1>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月14日 11:27</p>
<h2 id="15-三数之和-mid">15. 三数之和[mid]</h2>
<p>]]>
    </summary>
    <title>LeetCode Hot100-Day 1</title>
    <updated>2025-08-19T07:52:42.574Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h1>LeetCode Hot100-Day 0</h1><p>Created by: Yuanpeng QU<br>Created time: 2025年8月13日 21:29</p><p><strong>HOT100 抱佛脚计划</strong></p><table><thead><tr><th>日期</th><th>主题焦点</th><th>核心模式</th><th>目标Hot 100题号</th><th>每日定性目标</th></tr></thead><tbody><tr><td><strong>Day 1</strong></td><td><strong>数组与指针基础</strong></td><td>双指针, 滑动窗口</td><td>1, 11, 15, 26, 283, 75, 42, 3, 76, 239, 438, 560</td><td>掌握线性扫描、原地修改以及寻找配对/区间的核心技巧。</td></tr><tr><td><strong>Day 2</strong></td><td><strong>链表操作</strong></td><td>快慢指针, 链表原地反转</td><td>2, 19, 21, 23, 141, 142, 160, 206, 24, 25, 92</td><td>精通链表的节点操作、环检测和反转。K路合并(23)作为堆的预热。</td></tr><tr><td><strong>Day 3</strong></td><td><strong>树与图遍历基础</strong></td><td>树的BFS, 树的DFS</td><td>102, 103, 199, 104, 94, 144, 145, 101, 100, 226, 114, 543, 200</td><td>固化树的递归与迭代遍历范式，解决对称性、深度、路径等基础问题。</td></tr><tr><td><strong>Day 4</strong></td><td><strong>递归、回溯与搜索</strong></td><td>子集/回溯, 改进的二分查找</td><td>17, 22, 39, 46, 78, 90, 79, 33, 34, 153, 162, 69</td><td>学习如何系统性地探索解空间，并掌握在有序结构中高效搜索的技巧。</td></tr><tr><td><strong>Day 5</strong></td><td><strong>堆与优先队列应用</strong></td><td>前K个元素, 双堆, K路合并</td><td>215, 347, 295, 23 (复习), 56, 57 (区间合并)</td><td>掌握利用堆解决排序、中位数和合并问题。穿插区间合并作为调剂。</td></tr><tr><td><strong>Day 6</strong></td><td><strong>动态规划 (Part 1)</strong></td><td>1D DP, 基础DP</td><td>70, 198, 53, 121, 5, 300, 139, 322, 55, 45, 338</td><td>建立DP思维，从最基础的爬楼梯、打家劫舍开始，理解状态定义和转移。</td></tr><tr><td><strong>Day 7</strong></td><td><strong>动态规划 (Part 2) 与图</strong></td><td>2D DP, 背包, 拓扑排序</td><td>62, 64, 221, 152, 32, 72, 96, 416, 207, 210, 128</td><td>挑战更复杂的DP问题（矩阵、字符串编辑），并掌握基础图算法。</td></tr><tr><td><strong>Day 8</strong></td><td><strong>复盘、综合与模拟</strong></td><td>所有模式, 杂项</td><td>146, 208, 48, 136, 191, 复习Day1-7标记的难题</td><td>解决LRU等设计题，复习所有模式，进行2-3次完整的模拟面试。</td></tr></tbody></table><h2 id="1-两数之和-Two-Sum-easy">1.两数之和(Two Sum)[easy]</h2><p>一句话概括题意：在一个数组里找两个数，让他们的和等于指定目标值</p><p>思路：</p><ul><li>我自己的思路：暴力求解O(N^2) 不是最优</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line"><span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(i+<span class="number">1</span>,<span class="built_in">len</span>(nums)):</span><br><span class="line"><span class="keyword">if</span> nums[i] + nums[j] == target:</span><br><span class="line"><span class="keyword">return</span> [i, j]</span><br></pre></td></tr></table></figure><ul><li>核心：用hashmap (python里面的字典)，时间为O(N), <code>i+j == target</code>可以转化为 <code>j == target - i</code></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">hashmap = &#123;&#125;</span><br><span class="line"><span class="keyword">for</span> idx, i <span class="keyword">in</span> <span class="built_in">enumerate</span>(nums):</span><br><span class="line">j = target - i</span><br><span class="line"><span class="keyword">if</span> j <span class="keyword">in</span> hashmap:</span><br><span class="line"><span class="keyword">return</span> [idx, hashmap[j]]</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">hashmap[i] = idx <span class="comment"># 把&#123;idx:i&#125;存入哈希表</span></span><br></pre></td></tr></table></figure><ul><li>注意：<code>hashmap[value] = key</code>，或者<code>hashmap[key] = value</code> 语法上都对，但是取决于<strong>查询目标</strong>。在“两数之和”里，我们的目标是查询<strong>值</strong>，所以必须用<strong>值</strong>来做 key。</li></ul><h2 id="76-最小覆盖子串-hard"><strong>76. 最小覆盖子串[hard]</strong></h2><p>给定两个字符串s和t，求s里包含t中所有字符的最小字符串，没有就返回“”</p><p>根本想不到（</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">minWindow</span>(<span class="params">self, s, t</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type s: str</span></span><br><span class="line"><span class="string">        :type t: str</span></span><br><span class="line"><span class="string">        :rtype: str</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        need = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        window = collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        left, right = <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">        start, valid = <span class="number">0</span>, <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        min_len = <span class="built_in">float</span>(<span class="string">&#x27;inf&#x27;</span>) <span class="comment"># 初始化最小字串的长度为无穷</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> char <span class="keyword">in</span> t:</span><br><span class="line">          need[char] += <span class="number">1</span> <span class="comment"># need字典初始化，将需要比对的t转换为字符后储存在字典里，这里的+=1 则能计算每个字符数</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">while</span> right &lt; <span class="built_in">len</span>(s):</span><br><span class="line">          c = s[right]</span><br><span class="line">          right += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> c <span class="keyword">in</span> need:</span><br><span class="line">            window[c] += <span class="number">1</span></span><br><span class="line">            </span><br><span class="line">            <span class="comment"># 关键1</span></span><br><span class="line">            <span class="keyword">if</span> window[c] == need[c]: <span class="comment"># 这个if是在上一个的里面</span></span><br><span class="line">              valid += <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 关键2</span></span><br><span class="line">          <span class="keyword">while</span> valid == <span class="built_in">len</span>(need):</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> right - left &lt; min_len:</span><br><span class="line">              start = left <span class="comment"># 这个也在if里</span></span><br><span class="line">              min_len = right - left</span><br><span class="line">            </span><br><span class="line">            <span class="comment"># 这两句在if后面</span></span><br><span class="line">            d = s[left]</span><br><span class="line">            left += <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 关键3</span></span><br><span class="line">            <span class="keyword">if</span> d <span class="keyword">in</span> need:</span><br><span class="line">              <span class="keyword">if</span> window[d] == need[d]:</span><br><span class="line">                valid -= <span class="number">1</span></span><br><span class="line">              window[d] -= <span class="number">1</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span> <span class="keyword">if</span> min_len == <span class="built_in">float</span>(<span class="string">&#x27;inf&#x27;</span>) <span class="keyword">else</span> s[start: start + min_len]</span><br></pre></td></tr></table></figure><ul><li>最主要的记法：<strong>滑动窗口+2x哈希表</strong></li><li>两个哈希表，一个need将t字符串里的所有字符按照{字符：出现次数}保存，另一个哈希表window用于记录s字符串中滑动窗口两个指针中间的窗口部分的字符数量，同样按照上面的格式储存，need显然只需要执行一次，也就是将t储存后剩下的都是起判断作用</li><li>哈希表初始化： <code>import collections → need = collections.defaultdict(int)</code> 因为value是字符次数</li><li>**关键1：**使用一个valid变量来判断读取到某个属于need的字符c的时候window[c]是否和need[c]次数相等，相等就说明这个字符已经达到输出要求了，<code>valid+=1</code>，这样最后<code>valid ==len(need)</code>的时候window里面就已经有所有need数量的字符了。</li><li>**关键2：**关键1过后，左边的指针left就可以往右边移动了，当然移动之前得先确认好现在的最小字符串长度是多少（因为是第一次达到关键1结果，所以直接<code>min_len = right-left</code>）注意min_len初始化的时候是<code>min_len = float(’inf’)</code>无穷大，因为要用作判断<code>if right-left &lt; min_len</code>，这表示最小字符串有更小的值了，需要更新。更新完min_len后就可以将left往前挪1了，挪的同时需要保存原来的left指向的地方（<code>d = s[left]</code>），这里还需要在开始部分定义一个start=0用于保存最小字符串的起始位置。</li><li>**关键3：**对d进行判断，如果被删掉的这个d是属于need的话，那就还需要再判断一下如果窗口里面d的数量已经和need 相等了，那被删掉后就不满足<code>valid == len(need)</code>条件了，也就是说这个时候valid要-1，跳出内循环while，for循环重新判断新的right和c。如果窗口里d的数量还是比need里的多，就说明还能继续删。</li></ul><h2 id="11-盛最多水的容器-mid">11. 盛最多水的容器[mid]</h2><p>给一个数组height，元素表示高度，求这些统计图里的柱子围起来的水的最大体积，显然水的多少由最矮那边决定。</p><ul><li>模板：典中典<strong>双指针</strong>（两侧靠拢版）</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">maxArea</span>(<span class="params">self, height</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type height: List[int]</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(height) &lt;= <span class="number">1</span>: <span class="comment"># 其实可以直接max_area = 0就行了</span></span><br><span class="line">            <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        left, right = <span class="number">0</span>, <span class="built_in">len</span>(height)-<span class="number">1</span></span><br><span class="line">        max_area = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> left &lt; right:</span><br><span class="line">            tall = <span class="built_in">min</span>(height[left], height[right])</span><br><span class="line">            area = tall * (right - left)</span><br><span class="line">            <span class="keyword">if</span> area &gt; max_area:</span><br><span class="line">                max_area = area</span><br><span class="line">            <span class="keyword">if</span> height[left] &lt; height[right]: <span class="comment"># 高度如果相等的话向左向右移动都可以！！</span></span><br><span class="line">                left += <span class="number">1</span></span><br><span class="line">            <span class="keyword">else</span>: <span class="comment"># 这里是左移了</span></span><br><span class="line">                right -= <span class="number">1</span></span><br><span class="line">        <span class="keyword">return</span> max_area</span><br><span class="line">        </span><br></pre></td></tr></table></figure><ul><li>这题想到了<code>tall = min(height[left], height[right])</code>以及使用<code>max_area = area</code>来储存最大体积，但是唯一的错误就是把两个指针都设在左边了，最开始写的是<code>left, right = 0, 1,</code> while的条件成了：<code>while right &lt; len(height):</code> 这样其实到最后还是变成暴力求解，复杂度为N平方</li><li><strong>关键就是左右两边向中间靠拢的双指针，而不是同向</strong>。并且应该是<strong>哪边矮则哪边向中间靠拢</strong>，因为高的那边靠拢的话得到的结果可能还是矮的那边更矮，并且向中间靠拢底长也变短了。</li></ul>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/13/leetcode/LeetCode%20Hot100-Day%200/</id>
    <link href="https://qyp9909.github.io/2025/08/13/leetcode/LeetCode%20Hot100-Day%200/"/>
    <published>2025-08-13T13:00:00.000Z</published>
    <summary>
      <![CDATA[<h1>LeetCode Hot100-Day 0</h1>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月13日 21:29</p>
<p><strong>HOT100 抱佛脚计划</strong></p>
<table]]>
    </summary>
    <title>LeetCode Hot100-Day 0</title>
    <updated>2025-08-19T07:52:36.211Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h2 id="机器学习小记">机器学习小记</h2><p>Created by: Yuanpeng QU<br>Created time: 2025年8月13日 10:43</p><h2 id="第一部分：神经网络的核心组件-Core-Components-of-Neural-Networks"><strong>第一部分：神经网络的核心组件 (Core Components of Neural Networks)</strong></h2><h3 id="1-1-激活函数：从经典到现代"><strong>1.1 激活函数：从经典到现代</strong></h3><p>激活函数是神经网络的灵魂，它负责向网络中引入<strong>非线性</strong>，使得网络有能力学习和拟合现实世界中复杂的非线性关系。如果没有激活函数，多层神经网络本质上等同于一个单层的线性模型。</p><p><strong>1.1.1 Sigmoid &amp; Tanh：经典饱和函数的特性与局限性</strong></p><p><strong>1. Sigmoid 函数</strong></p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo>+</mo><msup><mi>e</mi><mrow><mo>−</mo><mi>x</mi></mrow></msup></mrow></mfrac></mrow><annotation encoding="application/x-tex">f(x) = \frac{1}{1+e^{-x}}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.1076em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0908em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">e</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6973em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">−</span><span class="mord mathnormal mtight">x</span></span></span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>核心特性：</strong></p><ul><li>将任意实数输入压缩到 <code>(0, 1)</code> 区间内。</li><li>这个特性使其输出可以被直观地解释为<strong>概率</strong>，因此在逻辑回归以及各类分类模型的<strong>输出层</strong>中，当需要预测一个概率时，Sigmoid 仍然是标准选择。特别是在<strong>CTR/CVR预估</strong>中，它的地位不可动摇。</li></ul></li><li><p><strong>主要局限性 (面试重点):</strong></p><ol><li><strong>梯度消失 (Vanishing Gradient):</strong> 这是 Sigmoid 最致命的弱点。当输入值的绝对值很大时（例如 <code>x &gt; 5</code> 或 <code>x &lt; -5</code>），Sigmoid 函数的曲线非常平坦，其导数（梯度）趋近于0。在深度网络中，根据链式法则，梯度在反向传播时会逐层相乘。多个接近0的小数相乘，会导致传到浅层网络的梯度变得无限小，使得浅层网络的参数几乎无法更新，模型也就学不动了。</li><li><strong>输出非零中心 (Non-Zero-Centered):</strong> Sigmoid 的输出恒为正数。这会导致后一层神经元的输入全是正的，在反向传播计算梯度时，会导致对权重的梯度方向要么全为正，要么全为负。这种更新方式效率低下，收敛过程会呈现“Z”字形，拖慢训练速度。</li></ol></li></ul><p><strong>2. Tanh (双曲正切) 函数</strong></p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>tanh</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mrow><msup><mi>e</mi><mi>x</mi></msup><mo>−</mo><msup><mi>e</mi><mrow><mo>−</mo><mi>x</mi></mrow></msup></mrow><mrow><msup><mi>e</mi><mi>x</mi></msup><mo>+</mo><msup><mi>e</mi><mrow><mo>−</mo><mi>x</mi></mrow></msup></mrow></mfrac></mrow><annotation encoding="application/x-tex">\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mop">tanh</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.2177em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.4483em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">e</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">x</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">e</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6973em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">−</span><span class="mord mathnormal mtight">x</span></span></span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">e</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6644em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">x</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">e</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7713em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">−</span><span class="mord mathnormal mtight">x</span></span></span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>核心特性：</strong></p><ul><li>可以看作是“加强版”的 Sigmoid，它将输入压缩到 <code>(-1, 1)</code> 区间。</li><li><strong>优点：</strong> 它的输出是<strong>零中心</strong>的，解决了 Sigmoid 的非零中心问题，因此在实践中通常比 Sigmoid 收敛更快。</li><li><strong>局限性：</strong> 它依然是一个饱和函数，在输入值绝对值很大时，两端同样非常平坦，所以<strong>仍然存在梯度消失</strong>的问题，只是相比 Sigmoid 有所缓解。</li></ul></li></ul><p><strong>总结：</strong> 由于梯度消失的致命缺陷，Sigmoid 和 Tanh 目前已<strong>很少被用于深度神经网络的隐藏层</strong>，它们的位置被下面要讲的 ReLU 所取代。</p><hr><p><strong>1.1.2 ReLU：现代网络的默认选择、优点与“死亡”问题</strong></p><p><strong>ReLU (Rectified Linear Unit, 修正线性单元)</strong> 是深度学习领域最重要的突破之一，它极大地促进了深度网络的成功训练。</p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>=</mo><mi>max</mi><mo>⁡</mo><mo stretchy="false">(</mo><mn>0</mn><mo separator="true">,</mo><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">f(x) = \max(0, x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.1076em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mop">max</span><span class="mopen">(</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></span></p></li><li><p><strong>核心特性与优点 (面试重点):</strong></p><ol><li><strong>有效解决梯度消失：</strong> 在输入为正数时 (<code>x &gt; 0</code>)，ReLU 的导数恒为1。这意味着梯度在反向传播过程中可以完整地通过激活函数，不会像 Sigmoid/Tanh 那样逐层衰减。这是它能够支持训练非常深的网络的最主要原因。</li><li><strong>计算效率极高：</strong> 实现上只是一个简单的取最大值操作，相比 Sigmoid/Tanh 的指数运算，计算成本极低，无论在前向传播还是反向传播中都非常快。</li><li><strong>引入稀疏性：</strong> 当输入为负数时，ReLU 的输出为0。这会使得网络中的一部分神经元“关闭”，形成稀疏的激活状态。这种稀疏性一方面降低了计算量，另一方面也被认为有助于降低过拟合。</li></ol></li><li><p><strong>主要局限性：</strong></p><ol><li><strong>“死亡 ReLU” 问题 (Dying ReLU):</strong> 这是 ReLU 最著名的缺点。在训练过程中，如果一个神经元接收到的输入，由于某次不恰当的权重更新（比如学习率过大），导致其之后对于整个训练集的所有输入都恒为负数，那么这个神经元的输出将永远是0。由于 <code>x&lt;0</code> 时 ReLU 的梯度也为0，这个神经元的权重将永远无法再得到更新。它就像一个“坏死”的神经元，不再对任何输入有响应，在网络中失去了作用。</li><li><strong>输出非零中心：</strong> 和 Sigmoid 一样，ReLU 的输出也非零中心（恒大于等于0），这会带来一些优化上的低效，但相比其解决梯度消失的巨大优点，这个问题通常被认为是次要的。</li></ol></li></ul><p><strong>1.1.3 Swish &amp; GELU：更平滑、更先进的激活函数探索</strong></p><p>在 ReLU 成为主流之后，研究者们并未停止探索的脚步。他们希望找到一种激活函数，既能拥有 ReLU 解决梯度消失的优点，又能克服其“硬拐点”（在0处不可导）和“死亡”等问题。Swish 和 GELU 就是其中的佼佼者。</p><p><strong>1. Swish 函数</strong></p><ul><li><strong>公式：</strong> <code>f(x)=x⋅σ(x)</code>，其中 σ(x) 是 Sigmoid 函数。</li><li><strong>核心思想：自门控 (Self-gating)。</strong> 函数的输出是输入 <code>x</code> 本身与一个由 <code>x</code> 生成的“门” <code>σ(x)</code> 的乘积。这个门的取值在 <code>(0, 1)</code> 之间，可以被看作是一个软性的、连续的开关，决定了原始输入 <code>x</code> 有多大比例可以通过。</li><li><strong>特性与优点：</strong><ul><li><strong>平滑连续：</strong> Swish 在所有点上都是可导的，解决了 ReLU 在0点的硬拐点问题，这通常有利于优化过程。</li><li><strong>非单调性：</strong> 在负值区域，Swish 的函数图像会先轻微下降再上升。这种非单调的特性被认为增加了模型的表达能力，使其能拟合更复杂的关系。</li><li><strong>经验性成功：</strong> Swish 最初是由 Google 通过自动化架构搜索发现的，在许多深度模型上的实验表明，它的性能通常略优于 ReLU。</li></ul></li></ul><p><strong>2. GELU (Gaussian Error Linear Unit) 函数</strong></p><ul><li><strong>公式：</strong> <code>f(x)=x⋅Φ(x)</code>，其中 <code>Φ(x)</code> 是标准正态分布的累积分布函数 (CDF)。</li><li><strong>核心思想：概率化的 ReLU。</strong> GELU 的设计思想更具理论性。它可以被理解为对输入 <code>x</code> 进行的一种随机正则化。<code>Φ(x)</code> 代表了“从标准正态分布中采样的值小于x”的概率。因此，GELU 的输出可以看作是输入 <code>x</code> 在其可能被“激活”（即其值在统计上足够显著）的概率下的期望值。</li><li><strong>特性与应用 (面试重点)：</strong><ul><li><strong>理论驱动：</strong> 其设计与 Dropout 和 Zoneout 等正则化方法有深刻的理论联系。</li><li><strong>性能卓越：</strong> GELU 是 <strong>Transformer 架构（如 BERT, GPT）中前馈网络层（FFN）的标准激活函数</strong>。提到这一点可以充分展现你对前沿模型的了解。</li></ul></li></ul><p><strong>Swish 与 GELU 的关系：</strong><br>它们的函数形状和性能表现都非常相似。在实践中，<strong>Swish 可以被看作是 GELU 的一个非常有效且形式更简洁的近似</strong>。它们的核心区别在于设计思想：<code>GELU 源于概率理论，而 Swish 源于实验搜索</code>。</p><hr><p><strong>1.1.4 GLU (门控线性单元)：一种智能的信息流控制机制</strong></p><p>GLU 不是一个简单的、作用于单个神经元的激活函数，而是一种更复杂的、控制信息流的<strong>网络结构单元</strong>。</p><ul><li><strong>公式：</strong> <code>GLU(x,v)=x⊙σ(v)</code>，其中 <code>⊙</code> 代表逐元素相乘。</li><li><strong>工作机制：</strong><ol><li><strong>输入分裂：</strong> 将上一层的输出 Tensor 在特征维度上分成两半，得到 <code>x</code> 和 <code>v</code>。</li><li><strong>信息流 (<code>x</code>)：</strong> 作为主要的待处理信息。</li><li><strong>控制门 (<code>v</code>)：</strong> 作为“阀门”的控制信号。它会经过一个 Sigmoid 函数 <code>σ(v)</code>，生成一个值在 <code>(0, 1)</code> 之间的门控向量。</li><li><strong>信息过滤：</strong> 将信息流 <code>x</code> 与其对应的门控向量 <code>σ(v)</code> 进行逐元素相乘。这样，门控向量中的每个值就决定了信息流 <code>x</code> 中对应维度的信息有多大比例可以通过。如果门控值为0，则信息被完全阻断；如果为1，则信息无损通过。</li></ol></li><li><strong>核心价值：</strong> GLU 赋予了网络一种<strong>动态的、依赖于数据自身来控制信息流</strong>的能力。这比静态的、一成不变的激活（如ReLU）要灵活得多。</li><li><strong>应用与联系：</strong><ul><li>这个“门控”思想是 <strong>LSTM 和 GRU</strong> 等循环神经网络的核心。</li><li><strong>Swish</strong> 函数 <code>x * σ(x)</code> 可以被视为 GLU 的一种特例，其中信息流和控制门都来自同一个输入 <code>x</code>。</li><li>在现代的 <strong>Transformer</strong> 模型中，使用 GLU 的变体（如 <strong>SwiGLU</strong>, <strong>GeGLU</strong>）来构建前馈网络层，已经成为提升模型性能的有效手段。</li></ul></li></ul><hr><p><strong>1.1.5 基础辨析：为什么不用 sin/cos 作为激活函数？</strong></p><p>这是一个很好的“第一性原理”问题，考察你对激活函数根本作用的理解。在标准的分类、回归等任务中，sin/cos 不被用作激活函数，主要有以下三个原因：</p><ol><li><strong>周期性导致的“多对一”歧义：</strong><br>这是最根本的问题。由于 <code>sin(x) = sin(x + 2π)</code>，多个不同的输入会映射到完全相同的输出。这给模型的学习带来了巨大的歧义。在反向传播时，如果输出层出现某个误差，模型将无法判断这个误差是由哪个输入周期引起的，导致梯度更新方向不明确，模型难以收敛。</li><li><strong>不稳定的周期性梯度：</strong><code>sin(x)</code> 的导数是 <code>cos(x)</code>，其值在 <code>[-1, 1]</code> 之间周期性地来回震荡，并且会周期性地取到0。对于深度网络，我们希望梯度能够稳定地、一致地传播。一个来回变换正负、大小不一且会周期性消失的梯度，会使得网络的训练过程非常不稳定，权重更新摇摆不定，难以走向最优。</li><li><strong>缺乏理想的函数特性：</strong><br>主流的激活函数通常具备一些理想的特性来帮助学习。例如，ReLU 提供了单侧抑制和稀疏性，并且其激活值没有上限；Sigmoid 提供了 <code>(0, 1)</code> 的平滑门控。而 <code>sin/cos</code> 的输出被严格限制在 <code>[-1, 1]</code>，并且其非单调的波动特性，对于深度网络逐层提取和抽象特征这一通用目标来说，通常不具备优势。</li></ol><h3 id="1-2-权重初始化：让训练稳定起步"><strong>1.2 权重初始化：让训练稳定起步</strong></h3><p>在开始训练神经网络之前，我们需要为模型的权重（Weight）和偏置（Bias）赋一个初始值。这个看似简单的步骤，却对整个训练过程的成败至关重要。一个好的初始化方案能让训练事半功倍，而一个糟糕的初始化则可能让网络从一开始就无法学习。</p><p><strong>为什么权重初始化如此重要？<strong>如果初始化不当，主要会导致</strong>梯度消失</strong>或<strong>梯度爆炸</strong>问题。</p><ul><li><strong>初始化太小（比如都接近0）：</strong><br>输入信号 <code>x</code> 在逐层前向传播时，每经过一层，其方差（可以理解为信号的“能量”）都会缩小。传到深层时，信号可能已经变得非常微弱。在反向传播时，梯度也会逐层衰减，最终导致<strong>梯度消失</strong>。</li><li><strong>初始化太大：</strong><br>信号在逐层传播时，其方差会不断被放大。传到深层时，激活值可能变得极大。这会导致两个问题：<ol><li>对于 Sigmoid/Tanh 等饱和激活函数，巨大的输入值会使其落入“饱和区”，导致梯度为0，从而引发<strong>梯度消失</strong>。</li><li>在反向传播时，梯度也可能被逐层放大，最终变得极其巨大，引发<strong>梯度爆炸</strong>，这会导致参数更新过猛，使训练过程完全发散。</li></ol></li></ul><p><strong>核心原则：</strong><br>一个好的初始化方法，其核心目标是<strong>让网络各层的激活值和梯度的方差在传播过程中都能保持稳定</strong>，既不放大也不缩小。</p><hr><p><strong>1.2.1 Xavier 与 He 初始化：激活函数的“最佳拍档”</strong></p><p>Xavier 和 He 是目前最主流的两种初始化方法，它们的选择与你所使用的<strong>激活函数</strong>紧密相关。</p><p><strong>1. Xavier 初始化 (又名 Glorot 初始化)</strong></p><ul><li><strong>设计目标/适用场景：</strong><br>专为<strong>对称的、以零为中心</strong>的激活函数设计，最典型的就是 <strong>Tanh</strong> 和 <strong>Sigmoid</strong>（在输入标准化后）。</li><li><strong>核心思想：</strong><br>Xavier 的推导基于一个前提：激活函数在线性区工作，且输入和输出的均值为0。它试图让每一层输出的方差等于输入的方差，同时让反向传播的梯度方差也保持不变。</li><li><strong>公式（思想）：</strong><br>它会根据一层的<strong>输入神经元数量 <code>fan_in</code></strong> 和<strong>输出神经元数量 <code>fan_out</code></strong> 来决定权重的初始方差。一个常见的实现是从一个均值为0，方差为 Var(W)=fanin+fanout2 的分布中进行采样。<ul><li><strong>直观理解：</strong> 输入连接 <code>fan_in</code> 越多，输出的方差越容易被放大。为了抵消这种影响，权重的方差就需要相应地调小，所以 <code>fan_in</code> 和 <code>fan_out</code> 会出现在分母上。</li></ul></li><li><strong>不适用于 ReLU 的原因：</strong><br>ReLU 函数会将所有负数输入都置为0，这破坏了 Xavier 所需的“零中心”假设。更重要的是，这个操作会使得输出的方差大约变为输入方差的<strong>一半</strong>。因此，如果将 Xavier 初始化用于 ReLU 网络，信号的方差依然会逐层递减，当网络很深时，最终还是会导致梯度消失。</li></ul><p><strong>2. He 初始化 (又名 Kaiming 初始化)</strong></p><ul><li><strong>设计目标/适用场景：</strong><br>专为 <strong>ReLU 及其变种（Leaky ReLU, PReLU 等）</strong> 量身打造。这是目前<strong>深度学习模型的标准选择</strong>。</li><li><strong>核心思想：</strong><br>He 初始化精准地解决了 Xavier 初始化在 ReLU 网络中的问题。它考虑到了 ReLU 会使方差减半这一特性。</li><li><strong>公式（思想）：</strong><br>它的实现是从一个均值为0，方差为 Var(W)=fanin2 的分布中进行采样。<ul><li><strong>关键洞察：</strong> 与 Xavier 的一个常用变种（方差为 <code>1/fan_in</code>）相比，He 初始化的方差正好是其<strong>两倍</strong>。这个“乘以2”的操作，就是为了<strong>补偿</strong> ReLU 激活函数带来的方差减半的损失，从而保证了信号方差在网络中能够保持稳定。</li></ul></li></ul><p><strong>总结：</strong></p><table><thead><tr><th>初始化方法</th><th>最佳搭配激活函数</th><th>核心思想</th></tr></thead><tbody><tr><td><strong>Xavier 初始化</strong></td><td>Tanh, Sigmoid</td><td>保持输入输出方差一致，适用于对称激活函数</td></tr><tr><td><strong>He 初始化</strong></td><td><strong>ReLU</strong> 及其变种</td><td><strong>补偿</strong>因ReLU导致的方差损失，适用于非对称的ReLU类函数</td></tr></tbody></table><h2 id="第二部分：模型的“导航系统”：损失函数-The-Model’s-“Navigation-System”-Loss-Functions"><strong>第二部分：模型的“导航系统”：损失函数 (The Model’s “Navigation System”: Loss Functions)</strong></h2><p>损失函数（Loss Function）的核心作用是<strong>衡量模型预测结果与真实标签之间的差距</strong>。模型训练的过程，本质上就是通过优化算法（如梯度下降）来不断调整参数，以最小化这个“差距”（即损失函数的值）。</p><h3 id="2-1-分类问题的基石：交叉熵-Cross-Entropy"><strong>2.1 分类问题的基石：交叉熵 (Cross-Entropy)</strong></h3><p>在所有分类任务中，无论是二分类还是多分类，交叉熵都是当之无愧的“标准”损失函数。要深刻理解交叉熵，我们必须先从它的“理论源头”——KL散度开始。</p><p><strong>2.1.1 KL 散度与交叉熵的深刻联系</strong></p><p><strong>1. KL 散度 (Kullback-Leibler Divergence)：衡量分布的“距离”</strong></p><ul><li><strong>核心思想：</strong><br>KL散度源于信息论，它用于衡量两个概率分布之间的<strong>差异性</strong>或**“距离”**。这里的“距离”不是几何距离，而是信息论意义上的距离。<br>假设我们有两个分布：<ul><li><code>P</code>：真实世界的数据分布（<strong>真相</strong>）。</li><li><code>Q</code>：我们模型预测出的概率分布（<strong>模型的认知</strong>）。<br>KL散度 DKL(P∣∣Q) 度量了当我们用“模型的认知Q”来描述“真相P”时，相比于用“真相P”本身来描述，会额外产生多少信息损失或“意外”。</li></ul></li><li><strong>目标：</strong><br>在机器学习中，我们的目标就是让我们模型的认知 <code>Q</code> 无限地逼近真相 <code>P</code>，也就是最小化 DKL(P∣∣Q)。当两个分布完全相同时，KL散度为0。</li><li><strong>关键特性：</strong> KL散度是非对称的，即 <code>D_KL(P||Q)!=D_KL(Q||P)</code>。我们始终使用前者，因为它惩罚的是“模型未能预测出真实情况”，这正是我们想要的。</li></ul><p><strong>2. 交叉熵 (Cross-Entropy)：KL散度的“实用替身”</strong></p><p>既然我们的目标是最小化KL散度，为什么在实践中几乎所有的代码里写的都是交叉熵损失呢？答案就藏在下面这个关键的恒等式中：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>H</mi><mo stretchy="false">(</mo><mi>P</mi><mo separator="true">,</mo><mi>Q</mi><mo stretchy="false">)</mo><mo>=</mo><mi>H</mi><mo stretchy="false">(</mo><mi>P</mi><mo stretchy="false">)</mo><mo>+</mo><msub><mi>D</mi><mrow><mi>K</mi><mi>L</mi></mrow></msub><mo stretchy="false">(</mo><mi>P</mi><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi><mi>Q</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">H(P,Q) = H(P) + D_{KL}(P||Q)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">Q</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">K</span><span class="mord mathnormal mtight">L</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord">∣∣</span><span class="mord mathnormal">Q</span><span class="mclose">)</span></span></span></span></span></p><ul><li><strong>公式解读：</strong><ul><li>H(P,Q)：分布 <code>P</code> 和 <code>Q</code> 的<strong>交叉熵</strong>。</li><li>H(P)：真实分布 <code>P</code> 的<strong>信息熵</strong>。它代表了真实数据本身所固有的不确定性或信息量。对于一个给定的、不变的数据集来说，<code>H(P)</code> 是一个<strong>固定的常数</strong>。</li><li>DKL(P∣∣Q)：<code>P</code> 和 <code>Q</code> 之间的 <strong>KL散度</strong>。</li></ul></li><li><strong>“Aha!”时刻：</strong><br>我们的优化目标是<strong>最小化 DKL(P∣∣Q)</strong>。观察上面的公式，由于 H(P) 是一个无法被模型改变的常数，那么<strong>最小化等式右边的 DKL(P∣∣Q)，就完全等价于最小化等式左边的交叉熵 H(P,Q)！</strong></li><li><strong>为什么选择交叉熵？</strong><br>因为交叉熵的计算<strong>更简单、更方便</strong>。<ul><li><p>KL散度的公式为：计算时需要用到 logP(x)。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>D</mi><mrow><mi>K</mi><mi>L</mi></mrow></msub><mo stretchy="false">(</mo><mi>P</mi><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi><mi>Q</mi><mo stretchy="false">)</mo><mo>=</mo><mo>∑</mo><mi>P</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mi>log</mi><mo>⁡</mo><mfrac><mrow><mi>P</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo></mrow><mrow><mi>Q</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo></mrow></mfrac></mrow><annotation encoding="application/x-tex">D_{KL}(P||Q) = \sum P(x) \log \frac{P(x)}{Q(x)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">K</span><span class="mord mathnormal mtight">L</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord">∣∣</span><span class="mord mathnormal">Q</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.363em;vertical-align:-0.936em;"></span><span class="mop op-symbol large-op" style="position:relative;top:0em;">∑</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">Q</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p>交叉熵的公式为：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>H</mi><mo stretchy="false">(</mo><mi>P</mi><mo separator="true">,</mo><mi>Q</mi><mo stretchy="false">)</mo><mo>=</mo><mo>−</mo><mo>∑</mo><mi>P</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mi>log</mi><mo>⁡</mo><mi>Q</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">H(P,Q) = -\sum P(x) \log Q(x)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">Q</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.6em;vertical-align:-0.55em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-symbol large-op" style="position:relative;top:0em;">∑</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">Q</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span></span></span></span></span></p><p>交叉熵的计算只需要真实标签（<code>P(x)</code>，在分类中通常是0或1）和模型的预测概率（<code>Q(x)</code>），完全避开了对 logP(x) 的计算，在编程实现上更加直接。</p></li></ul></li></ul><p><strong>总结：</strong></p><p>“KL散度和交叉熵是衡量两个概率分布差异的两个高度相关的指标。KL散度是理论上的差异度量，也是我们模型优化的根本目标。而交叉熵可以被分解为‘真实数据的信息熵’与‘KL散度’之和。</p><p>在机器学习任务中，由于真实数据的信息熵是一个固定的常数，因此<strong>最小化交叉熵就等价于最小化KL散度</strong>。我们之所以在实践中都使用交叉熵作为损失函数，是因为它的计算形式比KL散度更简洁，不涉及对真实分布取对数，从而在实现上更方便，但达成的优化目标是完全一致的：即使模型的预测分布 <code>Q</code> 尽可能地接近真实的数据分布 <code>P</code>。”</p><hr><p><strong>2.1.2 二元交叉熵 (BCE) vs. 多元交叉熵 (CE)</strong></p><p>虽然都叫交叉熵，但这两个术语是交叉熵原理在两种不同分类场景下的具体应用。其根本区别在于<strong>问题的性质</strong>和<strong>模型的输出层设计</strong>。</p><p><strong>1. 二元交叉熵 (Binary Cross-Entropy, BCE)</strong></p><ul><li><p><strong>应用场景：二分类问题 (Binary Classification)</strong></p><ul><li>任务是“是/否”的判断，类别之间是“二选一”。</li><li><strong>“搜广推”典型应用：</strong> 点击率(CTR)预估（点击/不点击）、转化率(CVR)预估（转化/不转化）、垃圾邮件检测（是/否）等。</li></ul></li><li><p><strong>模型输出层设计：</strong></p><ul><li>最后一层网络通常只有 <strong>1个输出神经元</strong>。</li><li>激活函数使用 <strong>Sigmoid</strong>，将输出值 <code>z</code> 映射为一个 <code>(0, 1)</code> 之间的概率 <code>p</code>，这个 <code>p</code> 代表了样本属于正类（类别为1）的概率。负类的概率则隐含为 <code>1-p</code>。</li></ul></li><li><p><strong>损失函数公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>B</mi><mi>C</mi><mi>E</mi></mrow></msub><mo>=</mo><mo>−</mo><mo stretchy="false">[</mo><mi>y</mi><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>p</mi><mo stretchy="false">)</mo><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>y</mi><mo stretchy="false">)</mo><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>p</mi><mo stretchy="false">)</mo><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">L_{BCE} = -[y \log(p) + (1 - y) \log(1 - p)]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0502em;">B</span><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.0576em;">E</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">−</span><span class="mopen">[</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord mathnormal">p</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">p</span><span class="mclose">)]</span></span></span></span></span></p><ul><li><code>y</code> 是真实标签（取值为0或1）。</li><li><code>p</code> 是模型预测为正类的概率。</li><li><strong>工作原理：</strong> 当真实标签 <code>y=1</code> 时，损失为 <code>log(p)</code>，为了最小化损失，模型必须让 <code>p</code> 趋近于1。当 <code>y=0</code> 时，损失为 <code>log(1-p)</code>，模型必须让 <code>p</code> 趋近于0。这个公式巧妙地将两种情况统一了起来。</li></ul></li></ul><p><strong>2. 多元交叉熵 (Categorical Cross-Entropy, CE)</strong></p><ul><li><p><strong>应用场景：多分类问题 (Multi-class Classification)</strong></p><ul><li>任务是“N选一”，类别之间是<strong>互斥</strong>的。</li><li><strong>“搜广推”典型应用：</strong> 预测一个商品属于哪个品类（电子/图书/服装/…），预测用户年龄分段，识别图片中的物体属于哪个类别。</li></ul></li><li><p><strong>模型输出层设计：</strong></p><ul><li>最后一层网络有 <strong>K个输出神经元</strong>（K为类别总数）。</li><li>激活函数使用 <strong>Softmax</strong>，它将K个神经元的输出 <code>z_1, ..., z_K</code> 转化为一个K维的概率向量，向量中每个元素代表样本属于该类的概率，且所有元素之和为1。</li></ul></li><li><p><strong>损失函数公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>C</mi><mi>E</mi></mrow></msub><mo>=</mo><mo>−</mo><munderover><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><mi>K</mi></munderover><msub><mi>y</mi><mi>k</mi></msub><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><msub><mi>p</mi><mi>k</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">L_{CE} = -\sum_{k=1}^{K} y_k \log(p_k)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.0576em;">E</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:3.1304em;vertical-align:-1.3021em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8283em;"><span style="top:-1.8479em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0315em;">k</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">K</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.3021em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.0315em;">k</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.0315em;">k</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></p><ul><li><code>y</code> 是 one-hot 编码的真实标签向量（如 <code>[0, 1, 0]</code> 表示真实类别是第2类）。<code>y_k</code> 是该向量的第k个元素。</li><li><code>p_k</code> 是模型预测样本属于第k类的概率。</li><li><strong>工作原理：</strong> 由于 <code>y</code> 是 one-hot 向量，只有一个 <code>y_k</code> 为1，其余都为0。因此，上面的求和公式会简化为：LCE=−log(pc)，其中 <code>p_c</code> 是模型对<strong>那个唯一的正确类别</strong>所预测的概率。其目标就是最大化模型对正确答案预测的置信度。</li></ul></li></ul><table><thead><tr><th>特性</th><th><strong>二元交叉熵 (BCE)</strong></th><th><strong>多元交叉熵 (CE)</strong></th></tr></thead><tbody><tr><td><strong>任务场景</strong></td><td>二分类 (Yes/No)</td><td>多分类 (Choose one from N)</td></tr><tr><td><strong>输出层激活</strong></td><td>Sigmoid</td><td>Softmax</td></tr><tr><td><strong>输出维度</strong></td><td>1个概率值 <code>p</code></td><td>K个概率值 (和为1)</td></tr><tr><td><strong>标签形式</strong></td><td>标量 (0或1)</td><td>One-hot 向量</td></tr><tr><td><strong>本质</strong></td><td>交叉熵在<strong>伯努利分布</strong>下的特例</td><td>交叉熵在**类别分布(多项分布)**下的应用</td></tr></tbody></table><hr><p><strong>2.1.3 核心辨析：为什么分类任务用交叉熵，而不是均方误差(MSE)？</strong></p><p>这是一个在面试中极高频出现的问题，能清晰回答代表你对模型训练的原理有深刻理解。MSE 在回归任务中是王者，但用于分类任务则水土不服，主要有两大原因：</p><p><strong>1. 梯度优化问题：MSE 在分类任务中会“偷懒”</strong></p><ul><li><strong>MSE的梯度陷阱：</strong><br>在分类任务中，我们通常用 Sigmoid/Softmax 将输出映射为概率。以 Sigmoid 为例，其函数曲线在两端（输出接近0或1）非常平坦，这意味着它的<strong>导数在这些区域非常小</strong>。<br>MSE 损失的梯度，根据链式法则，正比于 <code>(预测值 - 真实值) * Sigmoid的导数</code>。<br><strong>问题来了：</strong> 当模型犯下弥天大错时（比如真实标签是1，模型预测概率接近0），“预测值-真实值”这一项很大，但“Sigmoid的导数”这一项却<strong>趋近于0</strong>！导致最终的梯度非常小。<br><strong>结论就是：模型在错得最离谱的时候，梯度反而消失了，学习得最慢。</strong> 这完全违背了我们希望模型“知错能改，大错快改”的初衷。</li><li><strong>交叉熵的梯度优势：</strong><br>交叉熵损失函数经过数学上的巧妙设计，其梯度（相对于激活前的输出<code>z</code>）可以被推导和化简为一个非常简洁优美的形式：<strong><code>预测概率 - 真实值</code></strong>（即 <code>p - y</code>）。<br><strong>这就完美了！</strong> 梯度的大小直接正比于预测误差的大小。模型错得越多，梯度就越大，参数更新的步长就越大，学习得就越快。这提供了一个非常稳定且高效的学习信号。</li></ul><p><strong>2. 损失函数的目标不匹配：MSE“关心错了重点”</strong></p><ul><li><strong>MSE的目标：</strong> 它衡量的是预测值与真实值（0或1）在<strong>数值上</strong>的距离。它希望预测的概率值 <code>p</code> 在数值上离 <code>y</code> 越近越好。</li><li><strong>交叉熵的目标：</strong> 它衡量的是模型对<strong>正确答案预测的置信度</strong>。</li><li><strong>举例说明：</strong> 假设真实标签为1。<ul><li>预测A：<code>p=0.6</code>，模型“有点把握”。</li><li>预测B：<code>p=0.1</code>，模型“错得离谱”。</li><li><strong>MSE的视角：</strong> 它觉得A的误差是0.4，B的误差是0.9。惩罚的差异是有限的。</li><li><strong>交叉熵的视角：</strong> 损失分别是 <code>log(0.6)</code> 和 <code>log(0.1)</code>。由于log函数的特性，对低概率的惩罚是<strong>指数级增长</strong>的。<code>log(0.1)</code> 比 <code>log(0.6)</code> 要大得多得多。它会给那个“错得离谱”的预测B一个极其巨大的惩罚，迫使模型去修正这个严重的错误。</li></ul></li></ul><p><strong>总结：</strong><br>“在分类问题中，我们选择交叉熵而不是MSE，主要有两个原因：</p><ol><li><strong>从梯度来看：</strong> MSE与Sigmoid等激活函数结合时，在模型预测错误的情况下会出现梯度消失问题，导致学习缓慢。而交叉熵的梯度形式非常优雅，梯度大小与误差成正比，能提供更稳定高效的优化路径。</li><li><strong>从优化目标来看：</strong> MSE关心的是数值上的差异，而交叉熵关心的是模型对正确类别的预测置信度。交叉熵的对数形式能极大地惩罚那些“自信的错误预测”，这与分类任务的目标——最大化正确类别的概率——是完全一致的。”</li></ol><h3 id="2-2-回归问题的选择：MSE-MAE-与-Huber-Loss"><strong>2.2 回归问题的选择：MSE, MAE 与 Huber Loss</strong></h3><p>当模型的预测目标是一个连续值，如预测用户观看视频的时长、预测商品的价格时，我们就需要回归损失函数来衡量预测值 <code>ŷ</code> 和真实值 <code>y</code> 之间的数值差距。</p><p><strong>2.2.1 MSE vs. MAE：对离群点的敏感度与收敛点（均值vs.中位数）的差异</strong></p><p><strong>1. MSE (Mean Squared Error, 均方误差)</strong></p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>M</mi><mi>S</mi><mi>E</mi><mo>=</mo><mfrac><mn>1</mn><mi>n</mi></mfrac><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><mo stretchy="false">(</mo><msub><mover accent="true"><mi>y</mi><mo>^</mo></mover><mi>i</mi></msub><mo>−</mo><msub><mi>y</mi><mi>i</mi></msub><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">MSE = \frac{1}{n} \sum_{i=1}^{n} (\hat{y}_i - y_i)^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.9291em;vertical-align:-1.2777em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">n</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.6514em;"><span style="top:-1.8723em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em;"><span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.1944em;"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em;"><span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1141em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></p></li><li><p><strong>核心思想：</strong> 惩罚误差的<strong>平方</strong>。这是一种“严厉”的惩罚机制，误差越大，损失会呈指数级增长。</p></li><li><p><strong>对离群点的敏感度：非常敏感。</strong></p><ul><li><strong>原因：</strong> 由于损失是误差的平方，一个离群点（即 <code>y</code> 与 <code>ŷ</code> 差距极大的点）会产生一个巨大的损失值。为了最小化总损失，模型会被迫投入过多精力去“迁就”这一个离群点，可能会导致其对其他正常数据的拟合效果变差。整个模型的预测线会被离群点“拽”过去。</li></ul></li><li><p><strong>收敛点特性：</strong></p><ul><li>最小化 MSE 的模型，其预测结果会收敛于目标值的<strong>条件均值 (Conditional Mean)</strong>。</li><li><strong>理解：</strong> 均值本身就是一个对离群点非常敏感的统计量（比如一个亿万富翁可以显著拉高整个小区的平均收入）。因此，一个以均值为优化目标的模型，自然也会对离群点敏感。</li></ul></li><li><p><strong>优点：</strong> 数学上，MSE 的损失函数处处可导且平滑，优化过程非常稳定高效。</p></li></ul><p><strong>2. MAE (Mean Absolute Error, 平均绝对误差)</strong></p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>M</mi><mi>A</mi><mi>E</mi><mo>=</mo><mfrac><mn>1</mn><mi>N</mi></mfrac><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>N</mi></munderover><mi mathvariant="normal">∣</mi><msub><mi>y</mi><mi>i</mi></msub><mo>−</mo><msub><mover accent="true"><mi>y</mi><mo>^</mo></mover><mi>i</mi></msub><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">MAE = \frac{1}{N} \sum_{i=1}^{N} |y_i - \hat{y}_i|</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:3.106em;vertical-align:-1.2777em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.109em;">N</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8283em;"><span style="top:-1.8723em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.109em;">N</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">∣</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.1944em;"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em;"><span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord">∣</span></span></span></span></span></p></li><li><p><strong>核心思想：</strong> 惩罚误差的<strong>绝对值</strong>。这是一种“温和”的惩罚机制，损失与误差呈线性关系。</p></li><li><p><strong>对离群点的敏感度：非常鲁棒 (Robust)。</strong></p><ul><li><strong>原因：</strong> 即使一个离群点产生了巨大的误差，它对总损失的贡献也是线性的，不会被不成比例地放大。因此，模型有能力“忽视”这些极端值，而专注于拟合数据的主体部分，不会轻易被带偏。</li></ul></li><li><p><strong>收敛点特性：</strong></p><ul><li>最小化 MAE 的模型，其预测结果会收敛于目标值的<strong>条件中位数 (Conditional Median)</strong>。</li><li><strong>理解：</strong> 中位数是一个对离群点非常鲁棒的统计量（无论那个亿万富翁多有钱，小区的收入中位数都不会有太大变化）。因此，以中位数为优化目标的模型，自然也继承了这种鲁棒性。</li></ul></li><li><p><strong>缺点：</strong> 在误差为0的点，MAE 的导数不连续（有一个“拐点”），这可能导致在接近最优解时，梯度仍在-1和1之间跳跃，不利于模型进行精细的收敛。</p></li></ul><hr><p><strong>2.2.2 Huber Loss：集大成者的稳健选择</strong></p><p>既然 MSE 和 MAE 各有优劣，Huber Loss 便应运而生，它试图集二者之长。</p><ul><li><strong>核心思想：</strong> 它是一个<strong>分段函数</strong>，兼具了智慧与灵活性。<ul><li><strong>当误差较小时 (<code>|y - ŷ| ≤ δ</code>)：</strong> 此时损失函数等同于 <strong>MSE</strong>。这保证了在接近最优解时，损失函数平滑可导，梯度会平稳地减小，利于模型进行精细的收敛。</li><li><strong>当误差较大时 (<code>|y - ŷ| &gt; δ</code>)：</strong> 此时损失函数等同于 <strong>MAE</strong>（但形式上有所调整以保证连接处平滑）。这保证了当遇到离群点时，损失不会被过度放大，模型依然保持鲁棒。</li></ul></li><li><strong><code>δ</code> (delta)</strong> 是一个超参数，它定义了“小误差”和“大误差”之间的边界。</li></ul><p><strong>总结：</strong></p><p>“在回归任务中，MSE、MAE和Huber Loss是最核心的三种损失函数。</p><ul><li><strong>MSE</strong> 对大误差惩罚最重，对离群点最敏感，它的优化目标是预测数据的<strong>均值</strong>。优点是优化过程平滑稳定。</li><li><strong>MAE</strong> 对所有误差的惩罚是线性的，对离群点非常鲁棒，它的优化目标是预测数据的<strong>中位数</strong>。缺点是在最优解附近梯度不稳定。</li><li><strong>Huber Loss</strong> 是两者的结合。它在误差小时使用MSE的特性，保证收敛精度；在误差大时使用MAE的特性，保证模型鲁棒性。</li></ul><p>在选择时，如果数据比较干净，我会优先考虑 <strong>MSE</strong>。如果数据中含有较多噪音或离群点，比如在搜广推场景中预测用户的观看时长或消费金额这类长尾分布的数据，为了保证模型的稳健性，<strong>Huber Loss</strong> 通常是比 MAE 更好的实践选择。”</p><h3 id="2-3-解决特定难题的进阶损失函数"><strong>2.3 解决特定难题的进阶损失函数</strong></h3><p><strong>2.3.1 Focal Loss：应对“搜广推”场景中的样本极度不均衡问题</strong></p><p><strong>1. 问题背景：样本不均衡的“诅咒”</strong></p><p>在“搜广推”的核心任务——<strong>点击率（CTR）和转化率（CVR）预估</strong>中，一个普遍且棘手的问题就是样本极度不均衡。</p><ul><li><strong>现象：</strong> 在广告或推荐场景中，用户点击（正样本）的行为是极其稀疏的，绝大部分的曝光（负样本）并不会产生点击。正负样本的比例可能达到 <code>1:1000</code> 甚至更悬殊。</li><li><strong>使用标准交叉熵的后果：</strong><ul><li><strong>损失被“多数派”主导：</strong> 模型的总损失会被海量的负样本淹没。即使这些负样本都“很容易”被正确分类（比如模型预测的点击概率是0.001，非常接近真实值0），它们的总损失加起来也远远超过了少数正样本贡献的损失。</li><li><strong>模型“走捷径”：</strong> 为了快速降低总损失，模型会学到一个最简单的策略：对所有样本都预测为“不点击”。这样虽然整体准确率很高（比如99.9%），但模型完全没有学到如何识别我们真正关心的、有价值的正样本，因此这个模型是毫无用处的。</li></ul></li></ul><p><strong>2. 核心思想：让模型“聚焦”于难点</strong></p><p>Focal Loss 的设计哲学非常直观：<strong>我们不应该在已经学得很好的“简单题”上浪费时间，而应该把精力“聚焦”到那些模型搞不懂的“难题”上。</strong></p><p>它通过修改标准的交叉熵损失，动态地降低了那些“容易分类、置信度高”的样本对总损失的贡献权重，从而让模型能够更专注于学习那些“难以分类、置信度低”的样本。</p><p><strong>3. 公式与机制解析</strong></p><p>我们从标准的二元交叉熵（BCE）出发，一步步演进到 Focal Loss。</p><ul><li><p><strong>BCE Loss:</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>B</mi><mi>C</mi><mi>E</mi></mrow></msub><mo>=</mo><mo>−</mo><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><msub><mi>p</mi><mi>t</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">L_{BCE} = -\log(p_t)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0502em;">B</span><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.0576em;">E</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></p><p>（其中 <code>p_t</code> 是模型对<strong>正确类别</strong>预测的概率）</p></li><li><p><strong>加权 BCE Loss (α-balanced):</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>α</mi><mi>B</mi><mi>C</mi><mi>E</mi></mrow></msub><mo>=</mo><mo>−</mo><msub><mi>α</mi><mi>t</mi></msub><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><msub><mi>p</mi><mi>t</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">L_{\alpha BCE} = -\alpha_t \log(p_t)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0037em;">α</span><span class="mord mathnormal mtight" style="margin-right:0.0502em;">B</span><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.0576em;">E</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">−</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0037em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></p><p>这是处理不均衡的传统方法，即为数量少的正类分配一个更高的权重 <code>α</code>，为数量多的负类分配一个较小的权重 <code>1-α</code>。这在一定程度上能缓解问题，但它无法区分样本的“难易”程度。</p></li><li><p><strong>Focal Loss:</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>F</mi><mi>L</mi></mrow></msub><mo>=</mo><mo>−</mo><msub><mi>α</mi><mi>t</mi></msub><msup><mrow><mo fence="true">(</mo><mn>1</mn><mo>−</mo><msub><mi>p</mi><mi>t</mi></msub><mo fence="true">)</mo></mrow><mi>γ</mi></msup><mi>log</mi><mo>⁡</mo><mrow><mo fence="true">(</mo><msub><mi>p</mi><mi>t</mi></msub><mo fence="true">)</mo></mrow></mrow><annotation encoding="application/x-tex">L_{F L}=-\alpha_{t}\left(1-p_{t}\right)^{\gamma} \log \left(p_{t}\right)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.1389em;">F</span><span class="mord mathnormal mtight">L</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.0543em;vertical-align:-0.25em;"></span><span class="mord">−</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0037em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner"><span class="minner"><span class="mopen delimcenter" style="top:0em;">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;">)</span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8043em;"><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0556em;">γ</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;">(</span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;">)</span></span></span></span></span></span></p><ul><li><strong>关键创新：调制因子 (1−pt)γ</strong></li><li><code>γ</code> (gamma) 被称为<strong>聚焦参数</strong>，是一个可调的超参数，通常 <code>γ ≥ 0</code>。</li><li><strong>工作机制：</strong><ul><li><strong>对于一个“简单”样本：</strong> 比如一个真实不点击的样本，模型很自信地预测其点击概率 <code>p=0.01</code>，那么 <code>p_t = 1-p = 0.99</code>。此时调制因子为 <code>(1-0.99)^γ = 0.01^γ</code>。如果我们取 <code>γ=2</code>，这个因子就是 <code>0.0001</code>。这意味着这个简单样本对总损失的贡献被<strong>急剧地降低了1万倍</strong>！模型被告知“你已经做得很好了，不用再管这个了”。</li><li><strong>对于一个“困难”样本：</strong> 比如一个真实点击的样本，模型却不自信地只预测其点击概率 <code>p=0.1</code>，那么 <code>p_t = p = 0.1</code>。此时调制因子为 <code>(1-0.1)^γ = 0.9^γ</code>。如果我们取 <code>γ=2</code>，这个因子是 <code>0.81</code>。可以看到，这个困难样本的损失<strong>基本没有被降低</strong>。模型被告知“你在这道题上还错得很，要继续学习！”。</li></ul></li></ul></li></ul><p><strong>总结：</strong></p><p><strong>问：“在CTR预估任务中，正负样本比例非常悬殊，你会如何处理？”</strong></p><blockquote><p>答： “这是一个在工业界非常普遍且关键的问题。传统的解决方法如下采样或上采样，但可能会丢失信息或引入噪音。在模型层面，一个更先进和有效的方法是使用 Focal Loss 作为损失函数。</p><p>Focal Loss 是对标准交叉熵损失的一种改进，它专门用于解决样本不均衡问题。其核心思想是引入了一个<strong>调制因子 <code>(1-pt)^γ</code></strong>。这个因子可以动态地、大幅地降低海量‘易分类’样本（大部分是负样本）在损失计算中的权重，从而让模型能够<strong>聚焦</strong>于学习我们更关心的、数量稀少的正样本，以及那些难以区分的样本。</p><p>通过调整聚焦参数 <code>γ</code>，我们可以灵活地控制对简单样本的抑制程度。Focal Loss 几乎是处理CTR/CVR预估这类任务时，解决样本不均衡问题的标准和首选方案之一。”</p></blockquote><hr><p><strong>2.3.2 对比学习损失：学习优质 Embedding 的核心</strong></p><p><strong>1. 为什么需要对比学习？——表征学习的目标</strong></p><p>在“搜广推”系统中，我们需要处理各种各样的实体，如用户、商品、搜索词、广告等。为了让模型能够理解它们，我们首先需要将这些实体转化为计算机能够处理的低维稠密向量——<strong>Embedding</strong>。</p><p>一个“好”的 Embedding 应该具备<strong>语义上的几何结构</strong>，即：<strong>相似的样本在向量空间中距离近，不相似的样本距离远</strong>。</p><p>然而，像交叉熵这样的标准分类损失，其目标是“分对类别”，它并不会直接对 Embedding 在空间中的相对位置进行优化。对比学习（Contrastive Learning）就是专门为了解决这个问题而生的，它的目标就是学习出具有优良空间结构的 Embedding。</p><p><strong>Contrastive Loss &amp; Triplet Loss：基于“对”和“三元组”的度量学习</strong></p><p>这是对比学习的经典方法，也称为度量学习（Metric Learning）。其核心思想是，学习信号不再来自于样本的固定标签，而是来自于样本之间的两两比较。</p><p><strong>1. 对比损失 (Contrastive Loss)</strong></p><ul><li><strong>学习单元：</strong> <strong>样本对 (Pair)</strong>，即 <code>(样本A, 样本B)</code>。</li><li><strong>数据构造：</strong> 需要构造两种样本对：<ul><li><strong>正样本对 (Positive Pair):</strong> A和B是相似的。例如，在推荐中，是同一个用户点击过的两件不同商品。</li><li><strong>负样本对 (Negative Pair):</strong> A和B是不相似的。例如，一件用户点击过的商品和一件他曝光未点击的商品。</li></ul></li><li><strong>优化目标：</strong><ul><li>对正样本对，<strong>拉近</strong>它们在向量空间中的距离。</li><li>对负样本对，<strong>推远</strong>它们的距离，但并不要求无限远，只要它们的距离大于一个预设的<strong>间隔 (margin) <code>m</code></strong> 即可。这相当于在不同类的样本之间划出一条“安全边界”。</li></ul></li></ul><p><strong>2. 三元组损失 (Triplet Loss)</strong></p><ul><li><strong>学习单元：</strong> <strong>三元组 (Triplet)</strong>，即 <code>(Anchor, Positive, Negative)</code>。</li><li><strong>数据构造：</strong><ul><li><code>Anchor</code> (锚点)：一个基准样本，比如一个特定的用户 <code>u</code>。</li><li><code>Positive</code> (正例)：与 <code>Anchor</code> 相似的样本，比如该用户 <code>u</code> 点击过的商品 <code>i</code>。</li><li><code>Negative</code> (负例)：与 <code>Anchor</code> 不相似的样本，比如用户 <code>u</code> 未点击的商品 <code>j</code>。</li></ul></li><li><strong>优化目标：</strong><br>它进行的是一种<strong>相对比较</strong>。目标是让“锚点到正例的距离” <strong>显著小于</strong> “锚点到负例的距离”。<ul><li>即：<code>D(Anchor,Positive) + margin &lt; D(Anchor,Negative)</code></li><li>损失函数会惩罚那些不满足这个条件的“坏”三元组，直到负例被推得比正例足够远（远出一个 margin 的距离）。Triplet Loss 通常比 Contrastive Loss 更常用，效果也更稳定。</li></ul></li></ul><hr><p><strong>N-pair Loss &amp; InfoNCE Loss：更高效的多负例对比方法</strong></p><p>Triplet Loss 一次只用一个负例，学习效率不高。如果我们能一次性让一个正例与<strong>多个</strong>负例进行对比，模型就会面临一个更难的挑战，从而学习到更具区分度的 Embedding。</p><p><strong>核心思想转变：</strong> 从“度量距离远近”转变为“<strong>在众多候选中进行分类</strong>”。</p><p><strong>1. N-pair Loss</strong></p><ul><li><strong>核心思想：</strong> 将对比学习任务转化为一个 N 分类问题。它在一个 batch 中构造 N 个正样本对，对于其中一个 <code>Anchor</code>，它的 <code>Positive</code> 是它的正确答案，而 batch 中其他 N-1 个 <code>Positive</code> 样本则都成为它的负例。模型的目标就是为这个 <code>Anchor</code> 在 N 个选项中选出正确的那个 <code>Positive</code>。</li></ul><p><strong>2. InfoNCE Loss (现代对比学习的标准)</strong></p><ul><li><strong>核心思想：</strong> InfoNCE 将“多负例对比”的思想发扬光大并系统化。它的任务可以被生动地描述为**“大海捞针”<strong>。给你一个嫌疑人（Anchor）的照片，然后给你一个有100人的广场，里面混着他的孪生兄弟（Positive）和99个路人（Negatives）。警察的要求是：“请你准确地从这100人里</strong>把他兄弟给我找出来**”。这是一个<strong>多选一的分类</strong>任务，难度显然大得多，对模型学习区分性特征的要求也更高。</li></ul><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mtext>InfoNCE</mtext></msub><mo>=</mo><mo>−</mo><mi>log</mi><mo>⁡</mo><mfrac><mrow><mi>exp</mi><mo>⁡</mo><mo stretchy="false">(</mo><mtext>sim</mtext><mo stretchy="false">(</mo><mi>q</mi><mo separator="true">,</mo><msub><mi>k</mi><mo>+</mo></msub><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><mi>τ</mi><mo stretchy="false">)</mo></mrow><mrow><mi>exp</mi><mo>⁡</mo><mo stretchy="false">(</mo><mtext>sim</mtext><mo stretchy="false">(</mo><mi>q</mi><mo separator="true">,</mo><msub><mi>k</mi><mo>+</mo></msub><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><mi>τ</mi><mo stretchy="false">)</mo><mo>+</mo><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>N</mi></munderover><mi>exp</mi><mo>⁡</mo><mo stretchy="false">(</mo><mtext>sim</mtext><mo stretchy="false">(</mo><mi>q</mi><mo separator="true">,</mo><msub><mi>k</mi><mrow><mi>i</mi><mo>−</mo></mrow></msub><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><mi>τ</mi><mo stretchy="false">)</mo></mrow></mfrac></mrow><annotation encoding="application/x-tex">L_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(q, k_+)/\tau)}{\exp(\text{sim}(q, k_+)/\tau) + \sum_{i=1}^N \exp(\text{sim}(q, k_{i-})/\tau)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">InfoNCE</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.5979em;vertical-align:-1.1709em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em;"><span style="top:-2.1288em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mop">exp</span><span class="mopen">(</span><span class="mord text"><span class="mord">sim</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0359em;">q</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2583em;"><span style="top:-2.55em;margin-left:-0.0315em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">+</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mord">/</span><span class="mord mathnormal" style="margin-right:0.1132em;">τ</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:0em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9812em;"><span style="top:-2.4003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.109em;">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2997em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">exp</span><span class="mopen">(</span><span class="mord text"><span class="mord">sim</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0359em;">q</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0315em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mord mtight">−</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mord">/</span><span class="mord mathnormal" style="margin-right:0.1132em;">τ</span><span class="mclose">)</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mop">exp</span><span class="mopen">(</span><span class="mord text"><span class="mord">sim</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0359em;">q</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2583em;"><span style="top:-2.55em;margin-left:-0.0315em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">+</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mord">/</span><span class="mord mathnormal" style="margin-right:0.1132em;">τ</span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.1709em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><ul><li><code>q</code> 是 anchor 的 embedding，<code>k+</code> 是 positive 的 embedding，<code>k-</code> 是 negative 的 embedding。</li><li><code>sim()</code> 是余弦相似度。<code>τ</code> 是温度超参，用于调节 Softmax 的锐利度。</li><li><strong>优化目标：</strong> 在一个包含 <strong>1个正例</strong> 和 <strong>N个负例</strong> 的集合中，InfoNCE 的目标是让模型能够以最高的概率<strong>识别</strong>出那个唯一的正例。</li><li><strong>公式本质：</strong> 其损失函数的形式，本质上就是一个在<strong>相似度（通常是点积或余弦相似度）上计算的多分类交叉熵损失(一个标准的 Softmax + 交叉熵损失)</strong>。它通过一个 Softmax 函数，将识别问题转化为了一个概率最大化问题。</li><li><strong>优势：</strong> InfoNCE 可以非常高效地利用大量的负样本（比如一个 batch 内的所有其他样本），这提供了一个非常强的学习信号，使得模型必须学习到高度精细和鲁棒的特征，才能在众多干扰项中找出正确的正例。</li></ul><p><strong>在“搜广推”中的核心应用：双塔模型召回</strong></p><p>对比学习损失，特别是 InfoNCE，是驱动现代<strong>推荐/搜索系统召回阶段</strong>的引擎。</p><ol><li><strong>模型架构：</strong> 构建<strong>双塔模型</strong>。一个“用户塔”负责将用户信息和上下文处理成 User Embedding；一个“物品塔”负责将物品信息处理成 Item Embedding。</li><li><strong>训练：</strong> 使用海量的用户行为数据（如 <code>(用户, 点击物品, 曝光未点物品)</code>），通过 <strong>InfoNCE Loss</strong> 来端到端地训练双塔模型。</li><li><strong>线上服务（召回）：</strong><ul><li><strong>离线：</strong> 用训练好的“物品塔”计算好全量百万、千万甚至上亿个物品的 Item Embedding，存入<strong>向量数据库</strong>（如 FAISS）。</li><li><strong>在线：</strong> 当用户请求到来时，用“用户塔”实时计算出该用户的 User Embedding。</li><li><strong>匹配：</strong> 使用<strong>近似最近邻（ANN）搜索</strong>技术，在毫秒级别内，从海量物品库中，找出与用户 Embedding 最相似（点积最大）的几百或几千个候选物品。</li></ul></li><li><strong>完成召回：</strong> 这几百上千个物品就构成了召回集，它们将被送入下一步更复杂的**排序（Ranking）**模型中进行精排。</li></ol><hr><p><strong>2.3.3 Center Loss：提升特征的类内紧凑性</strong></p><p><strong>1. 问题背景：分得开，但不够“好”</strong></p><p>标准的分类损失函数，如<strong>交叉熵 (Cross-Entropy)</strong>，其主要目标是让模型学习到<strong>可分性 (Separability)</strong>。也就是说，它会努力地将不同类别的样本特征在向量空间中推开，使得分类器能够画出一条清晰的决策边界。</p><p>然而，交叉熵并<strong>不关心同一类别内部的样本特征是否紧凑</strong>。这可能导致一个问题：虽然不同类别分开了，但每个类别内部的特征分布可能非常“松散”，像一盘散沙。这种高“<strong>类内方差 (Intra-class Variation)</strong>”会使得模型的决策边界非常脆弱，抗干扰能力差，学习到的特征也不够有代表性。</p><p><strong>2. 核心思想：在“推开”的同时，也要“拉近”</strong></p><p>Center Loss 的设计思想非常直观：它作为一种<strong>正则化项</strong>，与主要的分类损失（如交叉熵）<strong>协同工作</strong>，以实现两个目标：</p><ol><li><strong>交叉熵损失 (主任务):</strong> 负责将<strong>不同</strong>类别的特征簇（Inter-class）<strong>推开</strong>。</li><li><strong>Center Loss (辅助任务):</strong> 负责将<strong>相同</strong>类别内的样本特征（Intra-class）向该类的中心点<strong>拉近</strong>。</li></ol><p><strong>总损失函数公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mtext>Total</mtext></msub><mo>=</mo><msub><mi>L</mi><mtext>Softmax</mtext></msub><mo>+</mo><mi>λ</mi><mo>⋅</mo><msub><mi>L</mi><mtext>Center</mtext></msub><mspace linebreak="newline"></mspace><msub><mi>L</mi><mtext>Center</mtext></msub><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>N</mi></munderover><mi mathvariant="normal">∥</mi><msub><mi mathvariant="bold">x</mi><mi>i</mi></msub><mo>−</mo><msub><mi mathvariant="bold">c</mi><msub><mi>y</mi><mi>i</mi></msub></msub><msubsup><mi mathvariant="normal">∥</mi><mn>2</mn><mn>2</mn></msubsup></mrow><annotation encoding="application/x-tex">L_{\text{Total}} = L_{\text{Softmax}} + \lambda \cdot L_{\text{Center}}\\L_{\text{Center}} = \frac{1}{2} \sum_{i=1}^{N} \| \mathbf{x}_i - \mathbf{c}_{y_i} \|_2^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">Total</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">Softmax</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">Center</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span><span class="mspace newline"></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">Center</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:3.106em;vertical-align:-1.2777em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">2</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8283em;"><span style="top:-1.8723em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.109em;">N</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord">∥</span><span class="mord"><span class="mord mathbf">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1502em;vertical-align:-0.2861em;"></span><span class="mord"><span class="mord mathbf">c</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0359em;">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3281em;"><span style="top:-2.357em;margin-left:-0.0359em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.143em;"><span></span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em;"><span></span></span></span></span></span></span><span class="mord"><span class="mord">∥</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.247em;"><span></span></span></span></span></span></span></span></span></span></span></p><ul><li><code>$L_&#123;Softmax&#125;$</code> 就是我们熟悉的交叉熵损失。</li><li><code>$L_&#123;Center&#125;$</code> 计算了每个样本的特征向量 <code>x_i</code> 到其对应类别（由标签 <code>y_i</code> 决定）的中心 <code>c_yi</code> 的距离的平方和。</li><li><code>λ</code> 是一个平衡超参数，用于控制“拉近”这个辅助任务的重要性。</li></ul><p><strong>3. 工作机制：双重更新的动态过程</strong></p><p>为了实现“拉近”这个目标，Center Loss 在训练过程中引入了一个双重更新的机制：</p><ol><li><strong>更新网络参数 <code>θ</code>：</strong> 在反向传播时，模型的权重不仅接收来自交叉熵损失的梯度，也接收来自 Center Loss 的梯度。Center Loss 的梯度会“指示”网络权重进行调整，使得下一次该样本输出的特征 <code>x_i</code> 会<strong>更靠近</strong>它的类别中心 <code>c_yi</code>。</li><li><strong>更新类别中心 <code>c</code>：</strong> 类别中心 <code>c</code> 本身也不是固定不变的。在每个 mini-batch 训练后，每个类别中心都会根据该 batch 中属于它的那些样本的特征，向它们的<strong>均值方向</strong>进行移动。</li></ol><p>这个“<strong>模型权重更新</strong>”和“<strong>类别中心更新</strong>”交替进行的过程，就像一个动态的“捏面团”过程：网络努力地把“面”（特征）往“中心”送，而“中心”也在不断地移动到“面”最密集的地方。经过多轮迭代，最终每个类别的特征就会被“捏”成一个非常紧凑、密实的特征簇。</p><p><strong>4. 对比其它方法</strong></p><ul><li><strong>与 Contrastive/Triplet Loss 的区别：</strong> 对比学习关注的是样本<strong>之间</strong>的相对距离（比如A离B比离C更近）。而 Center Loss 关注的是每个样本与它所属的那个<strong>全局类别中心</strong>的绝对距离。Center Loss 的实现相对更简单，因为它不需要构造复杂的样本对或三元组。</li></ul><p><strong>5. 在“搜广推”中的应用</strong><br>Center Loss 在“搜广推”中属于一个更精细化的“工具”，适用于对特征判别性要求高的场景。</p><ul><li><strong>细粒度分类：</strong> 当需要对商品或广告进行非常精细的类目划分时（比如“红色iPhone手机壳” vs &quot;粉色iPhone手机壳”），使用 Center Loss 可以让学习到的 Embedding 更具区分度，边界更清晰。</li><li><strong>用户/广告风格聚类：</strong> 对广告创意素材按风格进行分类时，Center Loss 可以让同种风格的素材在向量空间中聚合得更紧，有利于后续进行风格化的推荐或分析。</li><li><strong>作为辅助损失：</strong> 在多任务学习模型中，可以引入 Center Loss 作为一个辅助任务，来提升模型中间共享层 Embedding 的质量，从而惠及所有下游任务。</li></ul><p><strong>总结：</strong><br>“Center Loss 不是一个独立的损失函数，而是一个与交叉熵等主分类损失配合使用的<strong>正则化项</strong>。它的核心目标是通过最小化<strong>类内方差</strong>，来提升模型学习到的特征的<strong>判别性</strong>和<strong>鲁棒性</strong>。</p><p>它的工作机制是，在交叉熵负责推开不同类别特征的同时，Center Loss 会额外惩罚每个样本特征与其所属类别中心之间的距离，从而在训练中将同一类别的样本向其中心‘拉近’。这最终会使得学习到的特征不仅类别间可分，而且类别内高度紧凑，在一些细粒度分类或对特征质量要求高的任务中非常有价值。”</p><h2 id="第三部分：理论深度：统计与优化原理-Theoretical-Depth-Statistical-Optimization-Principles"><strong>第三部分：理论深度：统计与优化原理 (Theoretical Depth: Statistical &amp; Optimization Principles)</strong></h2><h3 id="3-1-参数估计的两种视角：最大似然-MLE-与最大后验-MAP"><strong>3.1 参数估计的两种视角：最大似然(MLE)与最大后验(MAP)</strong></h3><p>当我们训练一个模型时，本质上是在“估计”一组最能描述数据的参数 <code>θ</code>。对于如何找到“最好”的参数，统计学界主要有两种观点。</p><p><strong>1. 最大似然估计 (Maximum Likelihood Estimation, MLE)</strong></p><ul><li><strong>核心思想 (频率派观点)：</strong><br>MLE 认为模型的真实参数 <code>θ</code> 是一个未知的<strong>固定常量</strong>。它的目标是：找到这样一组参数 <code>θ</code>，使得我们已经观测到的这批训练数据 <code>D</code> 出现的<strong>概率最大</strong>。<ul><li><strong>直观比喻：</strong> 你捡到一枚硬币，抛了10次，出现了7次正面。MLE会问：“这枚硬币的正面朝上概率 <code>p</code> 是多少时，‘10次抛出7次正面’这个结果出现的可能性最大？” 答案是 <code>p=0.7</code>。MLE完全相信数据，数据告诉我们什么，它就认为真相是什么。</li></ul></li><li><strong>数学表示：</strong> 目标是最大化似然函数 L(θ)=P(D∣θ)。</li></ul><p><strong>3.1.1 损失函数与 MLE 的血缘关系</strong></p><p>我们日常使用的损失函数，正是 <strong>“最小化损失函数” 等价于 “最大化对数似然”</strong> 这一原则的体现。</p><ul><li><strong>核心转换：</strong><code>最大化 Likelihood</code> ⇔ <code>最大化 log(Likelihood)</code> (取对数不改变极值点，但将连乘变为连加，便于计算) ⇔ <strong><code>最小化 -log(Likelihood)</code></strong><br>这个 <strong>负对数似然 <code>log(Likelihood)</code></strong>，就是我们各种损失函数的“真身”。</li><li><strong>交叉熵的推导 (分类问题):</strong><ul><li><p><strong>假设：</strong> 我们假设二分类问题的标签 <code>y</code> 服从<strong>伯努利分布</strong>。</p></li><li><p><strong>推导：</strong> 单个样本的似然是</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>P</mi><mo stretchy="false">(</mo><mi>y</mi><mi mathvariant="normal">∣</mi><mi>x</mi><mo stretchy="false">)</mo><mo>=</mo><msup><mi>p</mi><mi>y</mi></msup><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>p</mi><msup><mo stretchy="false">)</mo><mrow><mn>1</mn><mo>−</mo><mi>y</mi></mrow></msup></mrow><annotation encoding="application/x-tex">P(y|x) = p^y(1-p)^{1-y}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mord">∣</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">p</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7144em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.0359em;">y</span></span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1141em;vertical-align:-0.25em;"></span><span class="mord mathnormal">p</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span><span class="mbin mtight">−</span><span class="mord mathnormal mtight" style="margin-right:0.0359em;">y</span></span></span></span></span></span></span></span></span></span></span></span></span></p><p>取负对数似然，得到:</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mo>−</mo><mo stretchy="false">[</mo><mi>y</mi><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>p</mi><mo stretchy="false">)</mo><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>y</mi><mo stretchy="false">)</mo><mi>log</mi><mo>⁡</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>p</mi><mo stretchy="false">)</mo><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">-[y\log(p)+(1 - y) \log(1 - p)]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">−</span><span class="mopen">[</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord mathnormal">p</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">p</span><span class="mclose">)]</span></span></span></span></span></p></li><li><p><strong>结论：</strong> 这正是我们熟悉的<strong>二元交叉熵 (BCE) 损失</strong>！因此，当我们在分类任务中使用交叉熵损失时，我们实际上是在对一个假设输出服从伯努利/类别分布的模型进行最大似然估计。</p></li></ul></li><li><strong>MSE的推导 (回归问题):</strong><ul><li><strong>假设：</strong> 我们假设真实值 <code>y</code> 等于模型预测值 <code>f(x)</code> 加上一个服从**正态分布（高斯分布）**的零均值噪声 <code>ε</code>。</li><li><strong>推导：</strong> 在这个假设下，<code>y</code> 的概率密度函数的负对数似然，其需要最小化的部分正比于 <code>(y−f(x))^2</code>。</li><li><strong>结论：</strong> 这正是<strong>均方误差 (MSE) 损失</strong>！因此，使用MSE等于在假设误差服从高斯分布的前提下进行最大似然估计。</li></ul></li></ul><hr><p><strong>2. 最大后验概率估计 (Maximum A Posteriori, MAP)</strong></p><ul><li><strong>核心思想 (贝叶斯派观点):</strong><br>MAP 认为参数 <code>θ</code> 不是固定的，它本身也是一个随机变量，在看到数据之前，我们可能对它有一个<strong>先验的认知</strong>，记为 <strong><code>P(θ)</code></strong>。<br>MAP的目标是：综合考虑“<strong>数据 <code>D</code></strong>”和我们的“<strong>先验认知 <code>P(θ)</code></strong>”，找到一个最合理的参数 <code>θ</code>。</li><li><strong>数学表示（贝叶斯定理）：</strong> 目标是最大化后验概率 <code>P(θ∣D)</code>。</li></ul><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi><munder><mo><mtext>argmax</mtext></mo><mi>θ</mi></munder></mi><mi>P</mi><mo stretchy="false">(</mo><mi>θ</mi><mi mathvariant="normal">∣</mi><mi>D</mi><mo stretchy="false">)</mo><mo>∝</mo><mi><munder><mo><mtext>argmax</mtext></mo><mi>θ</mi></munder></mi><mrow><mo fence="true">[</mo><mi>P</mi><mo stretchy="false">(</mo><mi>D</mi><mi mathvariant="normal">∣</mi><mi>θ</mi><mo stretchy="false">)</mo><mo>⋅</mo><mi>P</mi><mo stretchy="false">(</mo><mi>θ</mi><mo stretchy="false">)</mo><mo fence="true">]</mo></mrow></mrow><annotation encoding="application/x-tex">\underset{\theta}{\text{argmax}} P(\theta|D) \propto \underset{\theta}{\text{argmax}} \left[ P(D|\theta) \cdot P(\theta) \right]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.6965em;vertical-align:-0.9465em;"></span><span class="mord"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.4306em;"><span style="top:-2.1535em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0278em;">θ</span></span></span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop"><span class="mord text"><span class="mord">argmax</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9465em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0278em;">θ</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">∝</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.6965em;vertical-align:-0.9465em;"></span><span class="mord"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.4306em;"><span style="top:-2.1535em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0278em;">θ</span></span></span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop"><span class="mord text"><span class="mord">argmax</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9465em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;">[</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.0278em;">θ</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0278em;">θ</span><span class="mclose">)</span><span class="mclose delimcenter" style="top:0em;">]</span></span></span></span></span></span></p><p><code>MAP = 似然 × 先验, P(D∣θ)=似然, P(θ)=先验</code></p><p><strong>3.1.2 正则化的贝叶斯解释：L1/L2 分别对应何种先验？</strong></p><p>这揭示了正则化项的深刻本质：<strong>正则化 = 在损失函数中引入先验信念</strong>。</p><ul><li><p><strong>推导：</strong><code>最大化 Likelihood × Prior</code> ⇔ <code>最大化 log(Likelihood) + log(Prior)</code> ⇔ <strong><code>最小化 [-log(Likelihood)] + [-log(Prior)]</code></strong></p><p>我们发现，MAP的目标函数变成了： <strong><code>最小化 损失函数 + 参数的先验证罚项</code></strong></p><ul><li><strong><code>log(Likelihood)</code>：</strong> 就是我们从MLE推导出的损失函数（如交叉熵或MSE）。</li><li><strong><code>log(Prior)</code>：</strong> 就是我们要添加的<strong>正则化项</strong>！</li></ul></li><li><p><strong>L1/L2 正则化与先验的对应关系 :</strong></p><ul><li><strong>L2 正则化 (Ridge Regression):</strong> 如果我们对参数的先验信念是“<strong>参数 <code>θ</code> 应该比较小，并且集中在0附近</strong>”，这种信念可以用一个<strong>高斯先验 (Gaussian Prior)</strong> 来数学化描述。高斯分布的负对数正好是 <code>θ</code> 的二次方项。因此，<strong>MAP + 高斯先验 = 损失函数 + L2正则化</strong>。</li><li><strong>L1 正则化 (Lasso Regression):</strong> 如果我们的先验信念是“<strong>参数 <code>θ</code> 应该非常稀疏，即很多参数值应该正好是0</strong>”，这种信念可以用<strong>拉普拉斯先验 (Laplace Prior)</strong> 来描述。拉普拉斯分布的负对数正好是 <code>θ</code> 的绝对值项。因此，<strong>MAP + 拉普拉斯先验 = 损失函数 + L1正则化</strong>。</li></ul></li><li><p><strong>MLE 与 MAP 的统一：</strong><br>如果对参数没有任何先验信念，即认为所有参数取值的可能性都一样，这就是<strong>均匀分布先验</strong>。此时 <code>P(θ)</code> 是一个常数，<code>log(P(θ))</code> 也是常数，在优化中可以忽略。MAP 就退化为了 MLE。所以，<strong>MLE 是 MAP 在“无信息先验”下的一种特例</strong>。</p></li></ul><h3 id="3-2-“分布”的核心概念解读"><strong>3.2 “分布”的核心概念解读</strong></h3><p>“分布”是用来描述<strong>数据整体面貌和内在规律</strong>的语言。理解了数据的分布，就等于掌握了它的“性格”。</p><p><strong>3.2.1 什么是分布？（经验分布 vs. 理论分布）</strong></p><ul><li><strong>直观定义：</strong> 分布描述了一组数据中，各个数值的<strong>疏密程度</strong>和<strong>排列模式</strong>。它告诉我们哪些值更常见，哪些值更稀有。</li><li><strong>两种视角看分布：</strong><ol><li><strong>经验分布 (Empirical Distribution):</strong><ul><li><strong>是什么：</strong> 这是我们通过<strong>直接观察</strong>手中有限的样本数据而得到的分布。它是对我们<strong>已有数据</strong>的最真实写照。</li><li><strong>怎么看：</strong> 最常用的方法就是画<strong>直方图 (Histogram)</strong>。直方图的形状，就是这批数据的经验分布。它很直观，但可能因为数据的随机性而不够平滑，且不具备很好的泛化性。</li></ul></li><li><strong>理论分布 (Theoretical Distribution):</strong><ul><li><strong>是什么：</strong> 这是一个用<strong>数学函数</strong>来描述的、理想化的、平滑的分布。</li><li><strong>为什么用：</strong> 当我们观察到经验分布的形状（比如直方图的“山包”）与某个经典的数学函数很像时，我们就可以用这个简洁的数学函数来**近似地“建模”**我们的数据。这样做的好处是，模型变得非常简洁，并且具有很好的统计性质和泛化能力。</li><li><strong>常见例子：</strong><ul><li><strong>正态分布（高斯分布）：</strong> 对称的“钟形”曲线，由均值<code>μ</code>和标准差<code>σ</code>决定。在自然界和工业界中极为常见。</li><li><strong>伯努利分布：</strong> 单次“是/否”试验的分布，由单次成功的概率<code>p</code>决定。是CTR预估等二分类问题的基础。</li><li><strong>均匀分布：</strong> 所有可能结果的发生概率都相等，如掷一枚均匀的骰子。</li></ul></li></ul></li></ol></li></ul><p><strong>3.2.2 “求XX数据的分布”具体指什么？</strong></p><p>这句话在实践中通常指一个两步走的过程：</p><ol><li><strong>探索性分析：</strong> 首先，通过<strong>可视化</strong>（如画直方图）等手段，观察你手中这批数据的<strong>经验分布</strong>，了解它的实际形状、中心位置、离散程度等。</li><li><strong>模型拟合：</strong> 然后，根据观察到的形状，选择一个合适的<strong>理论分布</strong>去近似它。并利用手中的数据去<strong>估计</strong>这个理论分布的参数。<ul><li><strong>例如：</strong> “求用户年龄的分布” -&gt; 画出年龄的直方图，发现它呈现钟形 -&gt; 假设它服从正态分布 -&gt; 计算样本的平均年龄作为<code>μ</code>的估计值，计算样本的标准差作为<code>σ</code>的估计值 -&gt; 得出结论：“用户年龄近似服从均值为32，标准差为8的正态分布”。</li></ul></li></ol><h3 id="3-3-优化的“地形”：凸函数与非凸函数"><strong>3.3 优化的“地形”：凸函数与非凸函数</strong></h3><p>机器学习的训练过程，就是在一个由损失函数定义的、高低不平的“地形”上，寻找最低点的过程。这个“地形”的形状，决定了寻找最低点（最优解）的难度。</p><p><strong>3.3.1 为何凸优化是“理想型”？</strong></p><ul><li><strong>“地形”特征：</strong> 凸函数的“地形”像一个完美的<strong>碗</strong>。</li><li><strong>核心优点：</strong><ol><li><strong>全局唯一最优解：</strong> 在这个“碗”里，只存在<strong>一个</strong>最低点，任何局部最低点都必然是全局最低点。</li><li><strong>保证收敛：</strong> 使用梯度下降等优化算法，无论从“碗”的哪个位置开始下滑，最终都<strong>保证</strong>能到达那个唯一的全局最优点。</li></ol></li><li><strong>结论：</strong> 拥有凸损失函数的模型（如逻辑回归、线性回归）进行优化是<strong>稳定、可靠且结果可复现的</strong>。这使得凸优化成为我们最希望遇到的“理想情况”。</li></ul><p><strong>3.3.2 深度学习中的非凸优化意味着什么？</strong></p><ul><li><strong>“地形”特征：</strong> 深度神经网络的损失函数是高度<strong>非凸</strong>的，其“地形”像一个极其复杂的<strong>山脉</strong>，充满了无数高低不同、大小不一的“山谷”（局部最优点）。</li><li><strong>对训练的深刻影响：</strong><ol><li><strong>无法保证全局最优：</strong> 我们通过梯度下降找到的解，几乎可以肯定只是众多局部最优点中的一个，无法保证它是全局最好的解。我们的目标是找到一个“足够好”的局部最优解。</li><li><strong>对初始化高度敏感：</strong> 初始权重的位置，决定了你从哪个“山峰”开始往下走，最终可能落入完全不同的“山谷”，导致模型性能差异巨大。这就是 <strong>He 初始化</strong> 等方法至关重要的原因。</li><li><strong>优化器至关重要：</strong> 简单的梯度下降法很容易在“山谷”间被困住。而 <strong>Adam</strong> 等更先进的自适应优化器，通过引入动量等机制，像一辆性能更强的越野车，能帮助我们更好地“探索”这个复杂的地形，有机会越过一些小的障碍，找到更深的“山谷”。</li><li><strong>超参数决定路径：</strong> 学习率、batch size等超参数的选择，直接影响了下降的路径和速度，是深度学习“炼丹”的关键所在。</li></ol></li></ul><p><strong>总结：</strong><br>“优化是机器学习的核心。凸优化问题，如逻辑回归，因其拥有唯一的全局最优解而非常稳定。相比之下，深度学习是一个高度非凸的优化问题，这意味着我们无法保证找到全局最优解，且训练结果对<strong>参数初始化</strong>、<strong>优化器选择</strong>和<strong>超参数设定</strong>都高度敏感。因此，在深度学习中，我们的目标是找到一个泛化能力强的、足够好的局部最优解。”</p><h2 id="第四部分：经典模型对比与数据预处理-Classic-Model-Comparison-Data-Preprocessing"><strong>第四部分：经典模型对比与数据预处理 (Classic Model Comparison &amp; Data Preprocessing)</strong></h2><h3 id="4-1-两大线性分类器：SVM-vs-逻辑回归-LR"><strong>4.1 两大线性分类器：SVM vs. 逻辑回归(LR)</strong></h3><p>SVM 和 LR 都是监督学习中用于分类的强大算法，当面试官要求对比它们时，他想考察的是你是否理解两者在<strong>核心思想、损失函数和应用场景</strong>上的本质区别。</p><p><strong>4.1.1 核心思想对比：最大间隔 vs. 最大似然</strong></p><ol><li><strong>支持向量机 (SVM) 的哲学：最大间隔 (Maximum Margin)</strong><ul><li><strong>核心思想：</strong> SVM 是一个“<strong>边境防御</strong>”模型。它只关心那些离决策边界最近的、最难区分的样本点，这些点被称为<strong>支持向量 (Support Vectors)</strong>。SVM 的目标是在这些“边境哨兵”之间，找到一个能使“军事缓冲区”（即<strong>间隔，Margin</strong>）最宽的决策边界。</li><li><strong>损失函数：Hinge Loss</strong>。这个损失函数的设计非常巧妙，它只对那些落在间隔内或者被分错的样本点进行惩罚。对于那些远离边界、被轻松正确分类的点，其损失为0。这完美体现了 SVM “只关心支持向量”的极简主义思想。</li><li><strong>动机：</strong> 几何驱动。它追求的是在特征空间中最“安全”、最鲁棒的划分。</li></ul></li><li><strong>逻辑回归 (LR) 的哲学：最大似然 (Maximum Likelihood)</strong><ul><li><strong>核心思想：</strong> LR 是一个“<strong>民主投票</strong>”模型。它关心<strong>每一个样本点</strong>。它的目标是找到一个决策边界，使得在现有数据分布下，所有样本被正确分类的<strong>总概率</strong>最大。</li><li><strong>损失函数：Log Loss (交叉熵)</strong>。我们之前已经深入探讨过，这个损失函数源于最大似然估计。任何一个样本，只要其预测概率不为1，都会对总损失产生贡献。样本离决策边界越远，其贡献越小，但永远不会为0。</li><li><strong>动机：</strong> 概率驱动。它追求的是对数据整体分布的最佳概率拟合。</li></ul></li></ol><p><strong>主要技术差异总结：</strong></p><table><thead><tr><th>特性</th><th><strong>支持向量机 (SVM)</strong></th><th><strong>逻辑回归 (LR)</strong></th></tr></thead><tbody><tr><td><strong>核心思想</strong></td><td><strong>最大间隔</strong></td><td><strong>最大似然</strong></td></tr><tr><td><strong>损失函数</strong></td><td>Hinge Loss</td><td>Log Loss (交叉熵)</td></tr><tr><td><strong>关注点</strong></td><td>只关心<strong>支持向量</strong></td><td>关心<strong>所有</strong>数据点</td></tr><tr><td><strong>输出</strong></td><td>类别 / 到超平面的距离</td><td><strong>概率</strong></td></tr><tr><td><strong>非线性处理</strong></td><td><strong>核技巧 (Kernel Trick)</strong></td><td><strong>特征工程</strong></td></tr><tr><td><strong>数据敏感度</strong></td><td>对非支持向量鲁棒</td><td>对所有数据点敏感</td></tr></tbody></table><p><strong>4.1.2 在“搜广推”场景下的选择与原因</strong></p><p><strong>结论：</strong> 在几乎所有的“搜广推”核心场景，如 <strong>CTR/CVR 预估</strong>中，<strong>逻辑回归（LR）及其深度学习的演进版本是绝对的主流选择</strong>。</p><p><strong>核心原因：</strong></p><ol><li><strong>需要概率输出 (The Killer Feature):</strong> 这是最根本、最决定性的原因。<ul><li><strong>排序 (Ranking):</strong> 无论是搜索结果、推荐列表还是广告排序，我们都需要一个<strong>连续的分数</strong>来对成百上千的候选项进行排序。LR 天然输出的<strong>点击概率 (CTR)</strong>，是完成这个任务最理想、最符合逻辑的度量。</li><li><strong>竞价 (Bidding):</strong> 在计算广告中，广告主的出价（e.g., eCPM = pCTR * pCVR * Bid）直接依赖于模型预测的概率值。</li><li>SVM 的输出是类别，无法直接用于排序和竞价，需要额外的校准处理，效果和解释性都不如 LR。</li></ul></li><li><strong>可扩展性与训练效率：</strong><ul><li>LR 的损失函数光滑可导，非常适合使用<strong>随机梯度下降 (SGD)</strong> 及其变种进行优化。这使得 LR 能够非常方便地进行<strong>在线学习 (Online Learning)</strong> 和<strong>大规模并行化训练</strong>，完美契合了工业界海量数据、实时更新的需求。</li><li>SVM 的优化问题（求解一个二次规划问题）在数据量巨大时，训练复杂度通常高于 LR。</li></ul></li><li><strong>模型演进的基石：</strong><ul><li>现代“搜广推”领域主流的深度模型，如 <strong>DeepFM</strong>, <strong>DIN</strong> 等，都可以被看作是 LR 的扩展。它们通常包含一个负责学习特征交叉的深度部分（DNN），和一个负责记忆的浅层部分（如FM或LR），最后通过一个 Sigmoid 函数输出概率。理解 LR，是理解整个现代推荐广告体系的起点。</li></ul></li></ol><p><strong>总结：</strong><br>“对于 CTR 预估这类任务，我会毫不犹豫地选择逻辑回归或其深度学习变种。最核心的理由是，<strong>LR能直接输出一个校准良好的概率</strong>，这对于排序、竞价等下游商业任务至关重要，而 SVM 无法做到这一点。此外，LR 易于大规模并行化训练和在线学习的特性，也使它更适合处理工业界的海量数据流。”</p><h3 id="4-2-特征缩放：归一化-Normalization-vs-标准化-Standardization"><strong>4.2 特征缩放：归一化(Normalization) vs. 标准化(Standardization)</strong></h3><p>特征缩放是数据预处理中至关重要的一步。如果不同特征之间的数值范围（量纲）差距过大（例如，年龄在10-90之间，而年收入在30万-1000万之间），很多机器学习模型将无法正常工作。</p><p><strong>为什么需要特征缩放？</strong></p><ol><li><strong>加速梯度下降：</strong> 对于使用梯度下降优化的模型（如逻辑回归、SVM、神经网络），特征缩放可以使损失函数的等高线图更接近于“圆形”，从而让梯度下降的过程更平顺、更快速地找到最优解。否则，瘦长的等高线会导致优化路径来回震荡，收敛缓慢。</li><li><strong>消除量纲影响：</strong> 对于依赖距离计算的模型（如K-Means、KNN），特征缩放可以防止数值范围大的特征在距离计算中占据主导地位，使得所有特征能够被公平地对待。</li></ol><p><strong>注意：</strong> 决策树、随机森林、GBDT等<strong>树模型</strong>通常<strong>不需要</strong>进行特征缩放，因为树的分裂是基于单个特征的阈值，不受其他特征尺度的影响。</p><p><strong>4.2.1 对离群点的敏感度分析与选择策略</strong></p><p>这是归一化和标准化最核心的区别所在。</p><p><strong>1. 归一化 (Normalization)，又称 Min-Max Scaling</strong></p><ul><li><p><strong>公式：</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>X</mi><mtext>norm</mtext></msub><mo>=</mo><mfrac><mrow><mi>X</mi><mo>−</mo><msub><mi>X</mi><mtext>min</mtext></msub></mrow><mrow><msub><mi>X</mi><mtext>max</mtext></msub><mo>−</mo><msub><mi>X</mi><mtext>min</mtext></msub></mrow></mfrac></mrow><annotation encoding="application/x-tex">X_{\text{norm}} = \frac{X - X_{\text{min}}}{X_{\text{max}} - X_{\text{min}}}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.0785em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">norm</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1963em;vertical-align:-0.836em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:-0.0785em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">max</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3175em;"><span style="top:-2.55em;margin-left:-0.0785em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">min</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3175em;"><span style="top:-2.55em;margin-left:-0.0785em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">min</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.836em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>作用：</strong> 将数据的数值范围严格地“压缩”到 <code>[0, 1]</code> 区间内。</p></li><li><p><strong>对离群点的敏感度：非常敏感！</strong></p><ul><li><strong>例子：</strong> 假设我们有一组商品价格数据 <code>[100, 200, 300, 500]</code>。归一化后，它们的值会均匀地分布在 <code>[0, 1]</code> 之间。但如果此时进来一个<strong>离群点</strong>，比如一个奢侈品价格为 <code>50000</code>，那么 <code>X_max</code> 就会变成 <code>50000</code>。</li><li><strong>后果：</strong> 此时，原来的 <code>[100, 200, 300, 500]</code> 这四个“正常”数据点，在归一化后会全部被挤压到 <code>[0, 0.01]</code> 这个极其狭小的区间内，彼此之间几乎失去了区分度。模型将很难从这些被“压扁”的数据中学到有用的信息。</li></ul></li></ul><p><strong>2. 标准化 (Standardization)，又称 Z-score Scaling</strong></p><ul><li><p><strong>公式：</strong> （<code>μ</code> 是均值，<code>σ</code> 是标准差）</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>X</mi><mtext>std</mtext></msub><mo>=</mo><mfrac><mrow><mi>X</mi><mo>−</mo><mi>μ</mi></mrow><mi>σ</mi></mfrac></mrow><annotation encoding="application/x-tex">X_{\text{std}} = \frac{X-\mu}{\sigma}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.0785em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">std</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">σ</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal">μ</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>作用：</strong> 将数据变换为<strong>均值为0，标准差为1</strong>的分布。变换后的数据<strong>没有固定的数值范围</strong>。</p></li><li><p><strong>对离群点的敏感度：非常鲁棒 (Robust)！</strong></p><ul><li><strong>例子：</strong> 同样面对 <code>[100, 200, 300, 500, 50000]</code> 这组数据。那个 <code>50000</code> 的离群点虽然会影响 <code>μ</code> 和 <code>σ</code> 的计算，但它的影响是有限的。</li><li><strong>后果：</strong> 变换后，那四个“正常”的数据点依然会以0为中心，保持一个合理的相对分布，而那个 <code>50000</code> 的离群点则会变成一个绝对值很大的Z-score（比如 <code>z=4</code> 或 <code>z=5</code>），它本身成为了一个“异常”的数值，但<strong>不会破坏其他正常数据的结构</strong>。</li></ul></li></ul><p><strong>选择策略总结：</strong></p><p>“归一化和标准化是两种最常用的特征缩放方法。</p><ul><li><strong>归一化</strong>的优点是能将数据严格限定在 <code>[0, 1]</code>，在某些对数据范围有严格要求的场景（如图像处理中的像素值）非常有用。但它的<strong>致命缺点是对离群点极其敏感</strong>。</li><li><strong>标准化</strong>将数据转换为均值为0、标准差为1的分布，它<strong>对离群点的鲁棒性要好得多</strong>。</li></ul><p>因此，在<strong>绝大多数机器学习场景中，特别是当数据分布未知或我们怀疑可能存在离群点时，标准化是更安全、更通用、更被推荐的选择。</strong> 在‘搜广推’的深度学习模型中，处理像‘商品价格’、‘用户年龄’、‘历史点击次数’这类稠密数值特征时，我们通常都会采用<strong>标准化</strong>，以保证模型训练的稳定性和鲁棒性。”</p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/12/ML_1/</id>
    <link href="https://qyp9909.github.io/2025/08/12/ML_1/"/>
    <published>2025-08-12T09:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="机器学习小记">机器学习小记</h2>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月13日 10:43</p>
<h2 id="第一部分：神经网络的核心组件-Core-Components-of-Neur]]>
    </summary>
    <title>机器学习小记</title>
    <updated>2025-08-13T05:23:07.876Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h2 id="深度学习小记">深度学习小记</h2><p>Created by: Yuanpeng QU<br>Created time: 2025年8月12日 17:41</p><h2 id="I-神经网络的核心组件与训练技巧"><strong>I. 神经网络的核心组件与训练技巧</strong></h2><h3 id="1-归一化层：BN-与-LN-的原理、区别与应用场景"><strong>1. 归一化层：BN 与 LN 的原理、区别与应用场景</strong></h3><ul><li><strong>核心目的</strong>：解决“内部协变量偏移 (Internal Covariate Shift)”问题。即在训练中，由于前层网络参数不断变化，导致后层网络接收到的数据分布一直在变，拖慢收敛速度。归一化层通过将每层网络的输入强制拉回到一个稳定的分布（如均值为0，方差为1），从而加速训练。</li><li><strong>Batch Normalization (BN)</strong><ul><li><strong>原理</strong>：“纵向”或“按特征”归一化。它在一个批次（mini-batch）内，对<strong>每一个特征维度</strong>计算均值和方差，并进行归一化。</li><li><strong>关键机制</strong>：引入了两个可学习的参数 γ (缩放) 和 β (平移)，让网络可以自主决定是否以及在多大程度上<strong>恢复原始的分布</strong>，以保证模型的表达能力。</li><li><strong>训练 vs 推理</strong>：训练时使用当前批次的统计量，同时用滑动平均记录全局统计量；推理时则使用保存下来的全局统计量，以保证输出的确定性。</li><li><strong>应用场景</strong>：在<strong>卷积神经网络 (CNN)</strong> 和处理表格数据的<strong>多层感知机 (MLP)</strong> 中效果显著，前提是批次大小（Batch Size）不能过小。</li></ul></li><li><strong>Layer Normalization (LN)</strong><ul><li><strong>原理</strong>：“横向”或“按样本”归一化。它在<strong>单个样本内部</strong>，对<strong>该样本的所有特征维度</strong>计算均值和方差，并进行归一化。</li><li><strong>关键优势</strong>：其计算完全<strong>独立于批次大小</strong>，与批次内的其他样本无关。</li><li><strong>应用场景</strong>：完美适用于<strong>自然语言处理 (NLP)</strong> 领域，尤其是 <strong>RNN 和 Transformer</strong>。因为 NLP 任务的序列长度经常变化，需要 padding，BN 的效果会受到严重干扰，而 LN 则完全不受影响。在 Transformer 中，Pre-LN 的设计比 Post-LN 训练更稳定，是当前的主流。</li></ul></li></ul><h3 id="2-梯度问题：梯度消失与梯度爆炸"><strong>2. 梯度问题：梯度消失与梯度爆炸</strong></h3><ul><li><p><strong>本质成因</strong>：两者都是由于<strong>深层网络的链式求导法则</strong>所引发的累积效应。梯度的计算是一个长链条的连乘，如果连乘因子持续大于1，则梯度爆炸；如果持续小于1，则梯度消失。当我们想计算第一层网络参数W1的梯度时，链式求导法则如下：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mrow><mi mathvariant="normal">∂</mi><mi>L</mi></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>W</mi><mn>1</mn></msub></mrow></mfrac><mo>=</mo><mfrac><mrow><mi mathvariant="normal">∂</mi><mi>L</mi></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mi>n</mi></msub></mrow></mfrac><mo>⋅</mo><mfrac><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mi>n</mi></msub></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mrow><mi>n</mi><mo>−</mo><mn>1</mn></mrow></msub></mrow></mfrac><mo>⋅</mo><mfrac><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mrow><mi>n</mi><mo>−</mo><mn>1</mn></mrow></msub></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mrow><mi>n</mi><mo>−</mo><mn>2</mn></mrow></msub></mrow></mfrac><mo>⋅</mo><mo>⋯</mo><mo>⋅</mo><mfrac><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mn>2</mn></msub></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mn>1</mn></msub></mrow></mfrac><mo>⋅</mo><mfrac><mrow><mi mathvariant="normal">∂</mi><msub><mi>a</mi><mn>1</mn></msub></mrow><mrow><mi mathvariant="normal">∂</mi><msub><mi>W</mi><mn>1</mn></msub></mrow></mfrac></mrow><annotation encoding="application/x-tex">\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial a_n} \cdot \frac{\partial a_n}{\partial a_{n-1}} \cdot \frac{\partial a_{n-1}}{\partial a_{n-2}} \cdot \dots \cdot \frac{\partial a_2}{\partial a_1} \cdot \frac{\partial a_1}{\partial W_1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.2074em;vertical-align:-0.836em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">W</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord mathnormal">L</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.836em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.2074em;vertical-align:-0.836em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord mathnormal">L</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.836em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.2658em;vertical-align:-0.8943em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.8943em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.2658em;vertical-align:-0.8943em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span><span class="mbin mtight">−</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.8943em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4445em;"></span><span class="minner">⋯</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.2074em;vertical-align:-0.836em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.836em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.2074em;vertical-align:-0.836em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">W</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord" style="margin-right:0.0556em;">∂</span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.836em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>这里的每一项 ∂ai/∂ai−1 都与第 i 层的<strong>权重矩阵</strong>和<strong>激活函数的导数</strong>有关。整个梯度就变成了一个<strong>非常长的乘法链</strong>。</p><p>现在，问题就出在这个“连乘”上：</p><ul><li>如果连乘的因子大部分都<strong>小于 1</strong>，那么最终的乘积就会<strong>指数级地趋近于 0</strong>。这就是<strong>梯度消失</strong>。</li><li>如果连乘的因子大部分都<strong>大于 1</strong>，那么最终的乘积就会<strong>指数级地变得巨大</strong>。这就是<strong>梯度爆炸</strong>。</li></ul></li><li><p><strong>梯度消失 (Gradient Vanishing)</strong></p><ul><li><strong>现象</strong>：靠近输入层的网络无法得到有效的梯度信号，参数更新缓慢，模型学不动。</li><li><strong>主要原因</strong>：不合适的激活函数（如 Sigmoid 在其饱和区的导数接近0）或过小的权重。</li><li><strong>解决方案“工具箱”</strong>：<ol><li><strong>更换激活函数</strong>：使用 <strong>ReLU</strong> 及其变体（如 GELU, SiLU），它们在正区间的导数为1，不会造成梯度衰减。</li><li><strong>使用残差连接 (Residual Connections)</strong>：ResNet 的核心。通过“快捷方式”让梯度可以直接流向前层，将“连乘”变为“连加”，从根本上解决问题。</li><li><strong>使用归一化层 (BN/LN)</strong>：将数据拉回到激活函数的非饱和区，保证梯度的活性。</li></ol></li></ul></li><li><p><strong>梯度爆炸 (Gradient Explosion)</strong></p><ul><li><strong>现象</strong>：梯度变得极大，导致参数更新步子过大，损失函数剧烈震荡甚至变为 <code>NaN</code>，训练崩溃。</li><li><strong>主要原因</strong>：过大的权重初始化。</li><li><strong>解决方案“工具箱”</strong>：<ol><li><strong>梯度裁剪 (Gradient Clipping)</strong>：最直接有效的方法。给梯度设置一个范数上限，超过则按比例缩放，从而限制单次更新的最大步长。</li><li><strong>使用归一化层 (BN/LN)</strong>：同样能起到稳定激活值，间接稳定梯度的作用。</li><li><strong>合适的权重初始化</strong>：如 Xavier 或 He 初始化，从源头上避免权重过大。</li></ol></li></ul></li></ul><h3 id="3-初始化策略"><strong>3. 初始化策略</strong></h3><ul><li><strong>核心目的：打破对称性 (Break the Symmetry)</strong><ul><li><strong>问题</strong>：如果将同一层的所有权重初始化为相同的值（如全0或全1），那么在正向和反向传播中，这些神经元的行为将<strong>永远保持一致</strong>，它们会学到完全相同的特征。这等效于该层只有一个神经元在工作，完全浪费了网络的表达能力。</li><li><strong>解决</strong>：必须通过<strong>随机初始化</strong>来赋予每个神经元独特的初始状态，让它们有机会在训练中学习到不同的特征。</li></ul></li><li><strong>Transformer 为何不常用 He 初始化？</strong><ul><li><strong>He 初始化的专长</strong>：专为 <strong>ReLU</strong> 网络设计，目的是维持激活值的方差稳定。</li><li><strong>Transformer 的特殊性</strong>：<ol><li><strong>无处不在的 LayerNorm</strong>：Transformer 中大量使用 LN，它本身就是一个强大的稳定器，在每层之后都对数据分布进行“重置”，这使得网络对权重的初始尺度不那么敏感。</li><li><strong>投影层的需求</strong>：Transformer 中的许多线性层用于“投影”（如生成 QKV），其目标是稳定地传递信息。<strong>Xavier/Glorot 初始化</strong>同时考虑了输入和输出维度，旨在维持信号方差在流经该层时保持不变，理论上更适合这种场景。</li></ol></li></ul></li></ul><h2 id="II-优化算法的演进之路"><strong>II. 优化算法的演进之路</strong></h2><h3 id="1-核心思想：一阶动量与二阶动量"><strong>1. 核心思想：一阶动量与二阶动量</strong></h3><p>现代优化器的设计，都围绕着这两个核心概念，它们源于统计学中的“矩”：</p><ul><li><strong>一阶动量 (First-Order Momentum, mt)</strong><ul><li><strong>统计意义</strong>：梯度的<strong>指数移动平均值</strong>，是对梯度分布<strong>均值</strong>的估计。</li><li><strong>物理意义</strong>：“惯性”或“速度”。</li><li><strong>作用</strong>：决定参数更新的<strong>主要方向</strong>。通过累积历史梯度，它可以平滑更新路径，抑制在“峡谷”地带的震荡，并在梯度方向一致时加速前进。</li></ul></li><li><strong>二阶动量 (Second-Order Momentum, Vt)</strong><ul><li><strong>统计意义</strong>：梯度<strong>平方</strong>的指数移动平均值，是对梯度分布<strong>方差</strong>的估计。</li><li><strong>物理意义</strong>：“摩擦力”或“路况感知器”。</li><li><strong>作用</strong>：实现<strong>自适应学习率</strong>。它被放在学习率计算公式的分母位置。对于梯度变化剧烈（方差大）的参数，其有效学习率会变小，步子更稳；对于梯度一直很小（方差小）的参数，其有效学习率会变大，鼓励其更新。</li></ul></li></ul><p><strong>优化算法的统一框架</strong></p><ol><li><p><strong>计算梯度</strong>：计算当前参数 wt 的梯度 <code>g_t=∇f(w_t)</code>。</p></li><li><p><strong>更新动量</strong>：根据历史梯度计算<code>一阶动量 m_t（梯度的均值）</code>和<code>二阶动量 V_t（梯度平方的均值）</code>。</p></li><li><p><strong>计算下降步长</strong>：<code>η_t=α⋅m_t/V_t</code>。</p></li><li><p><strong>更新参数</strong>：<code>w_&#123;t+1&#125;=w_t−η_t</code>。</p></li></ol><p>这个框架的核心在于：</p><p><strong>参数的更新方向由一阶动量 mt 决定，而更新的步长（学习率）由二阶动量 Vt 进行动态调整</strong></p><p>。不同的优化算法，就是在这四步，特别是第 2 步上做了不同的设计。</p><h3 id="2-SGD-家族：从“盲人摸象”到“带惯性下坡”"><strong>2. SGD 家族：从“盲人摸象”到“带惯性下坡”</strong></h3><p>这个家族的特点是使用<strong>全局统一的学习率</strong>。</p><ul><li><strong>SGD (随机梯度下降)</strong><ul><li><strong>做法</strong>：只看当前一步的梯度方向，然后走一小步。</li><li>SGD <strong>没有动量 (Momentum)</strong> 的概念。可以理解为：一阶动量mt就是当前梯度gt，二阶动量Vt不使用，或者说恒为1。</li><li>所以按照上面的公式3代入SGD的下降步长为<code>η_t=α·gt</code>，更新参数则为<code>w_&#123;t+1&#125;=w_t−α·gt</code></li><li><strong>问题</strong>：非常“短视”，容易在曲折的路径上震荡，收敛缓慢。</li></ul></li><li><strong>SGDM (SGD with Momentum)</strong><ul><li><p><strong>改进</strong>：引入了<strong>一阶动量</strong>，像一个有惯性的小球，累积过去的速度。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>m</mi><mi>t</mi></msub><mo>=</mo><msub><mi>β</mi><mn>1</mn></msub><mo>⋅</mo><msub><mi>m</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><msub><mi>β</mi><mn>1</mn></msub><mo stretchy="false">)</mo><mo>⋅</mo><msub><mi>g</mi><mi>t</mi></msub></mrow><annotation encoding="application/x-tex">m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5806em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7917em;vertical-align:-0.2083em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span></p></li><li><p>mt：t时刻的动量，代表了历史梯度的累积方向。</p></li><li><p>beta1: 动量系数，通常取 0.9。这个值意味着，当前的更新方向<strong>主要由历史方向决定 (90%)</strong>，只受当前梯度的一点点影响 (10%)。</p></li><li><p>二阶动量Vt仍然不使用。</p></li><li><p>代入公式3，得到SGDM下降步长为<code>η_t=α·mt</code>,更新参数则为<code>w_&#123;t+1&#125;=w_t−α·mt</code></p></li><li><p><strong>效果</strong>：极大地缓解了震荡，加速了收敛。</p></li><li><p><strong>新问题</strong>：惯性是“盲目”的，可能会因为速度太快而冲过最优解。</p></li></ul></li><li><strong>NAG (Nesterov Accelerated Gradient)</strong><ul><li><strong>改进</strong>：引入了“预判”机制。它会先“探头”看一下按照当前动量走一步后会到达什么位置，然后用那个“未来”位置的梯度来修正当前的方向。</li><li><strong>效果</strong>：更“聪明”，能有效防止冲过头，收敛通常更稳定。</li></ul></li></ul><h3 id="3-自适应家族：为每个参数定制学习率"><strong>3. 自适应家族：为每个参数定制学习率</strong></h3><p>这个家族解决了 SGD 家族“一刀切”学习率的问题。</p><ul><li><p><strong>AdaGrad</strong></p><p>AdaGrad 第一个提出了这个革命性的想法。</p><ul><li><p><strong>核心动机</strong>：<br>一个参数过去的梯度历史，可以指导它未来的更新步伐。</p><ul><li>如果一个参数的梯度一直很大，说明它可能处于一个“陡峭”的区域，我们应该小心翼翼，用一个<strong>较小的学习率</strong>。</li><li>如果一个参数的梯度一直很小（甚至是0，例如稀疏特征），说明它很少被更新。一旦它有机会更新，我们应该给它一个<strong>较大的学习率</strong>，让它能“迎头赶上”。</li></ul></li><li><p><strong>实现机制</strong>：<br>AdaGrad 为每一个参数 w(i) 维护一个历史梯度平方的<strong>累加器</strong> Vt(i)。</p><ol><li><p><strong>累积梯度平方和</strong>：在每一步 t，计算当前梯度 gt(i)，并将其平方累加到 Vt(i) 中。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>V</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msubsup><mi>V</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mo stretchy="false">(</mo><msubsup><mi>g</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">V_t^{(i)} = V_{t-1}^{(i)} + (g_t^{(i)})^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2948em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></p></li><li><p><strong>更新参数</strong>：在更新时，将全局学习率 α 除以 根号Vt(i)。ϵ 是一个极小值防止分母为零</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>w</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msubsup><mi>w</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>−</mo><mfrac><mi>α</mi><msqrt><mrow><msubsup><mi>V</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mi>ϵ</mi></mrow></msqrt></mfrac><mo>⋅</mo><msubsup><mi>g</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup></mrow><annotation encoding="application/x-tex">w_{t+1}^{(i)} = w_t^{(i)} - \frac{\alpha}{\sqrt{V_t^{(i)} + \epsilon}} \cdot g_t^{(i)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.8376em;vertical-align:-1.73em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.11em;"><span class="pstrut" style="height:3.3845em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3845em;"><span class="svg-align" style="top:-3.8em;"><span class="pstrut" style="height:3.8em;"></span><span class="mord" style="padding-left:1em;"><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal">ϵ</span></span></span><span style="top:-3.3445em;"><span class="pstrut" style="height:3.8em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.88em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.88em" viewbox="0 0 400000 1944" preserveaspectratio="xMinYMin slice"><path d="M983 90l0 -0c4,-6.7,10,-10,18,-10 H400000v40H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5c53.7,-170.3,84.5,-266.8,92.5,-289.5zM1001 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.4555em;"><span></span></span></span></span></span></span></span><span style="top:-3.6145em;"><span class="pstrut" style="height:3.3845em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-4.0615em;"><span class="pstrut" style="height:3.3845em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.73em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span></span></span></span></span></p></li></ol><ul><li><strong>优点</strong>：<ul><li>在处理<strong>稀疏数据</strong>（如搜广推中的海量ID类特征）时表现极其出色。对于那些不常出现的特征，其 Vt 累积很慢，因此能保持较大的学习率，保证了有效的学习。</li></ul></li><li><strong>致命缺陷</strong>：<ul><li><p>由于 (gt(i))2 永远是正数，累加器 Vt(i) 是<strong>严格单调递增</strong>的。</p></li><li><p>随着训练的进行，Vt 会变得越来越大，导致分母 Vt(i) 也无限增大。</p></li><li><p>这使得有效学习率 α/根号Vt(i) 会<strong>持续衰减，并最终趋近于零</strong>。这会导致模型在训练后期“学习乏力”，过早地停止更新，即使它还没有达到最优状态。</p></li></ul></li></ul></li></ul></li><li><p><strong>RMSProp</strong></p><ul><li><p><strong>改进</strong>：将 AdaGrad 的“累加和”改为了<strong>指数移动平均（即二阶动量）</strong>。</p><ol><li><p><strong>更新二阶动量</strong>：不再是简单相加，而是加权平均。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>V</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msub><mi>β</mi><mn>2</mn></msub><mo>⋅</mo><msubsup><mi>V</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><msub><mi>β</mi><mn>2</mn></msub><mo stretchy="false">)</mo><mo>⋅</mo><mo stretchy="false">(</mo><msubsup><mi>g</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">V_t^{(i)} = \beta_2 \cdot V_{t-1}^{(i)} + (1 - \beta_2) \cdot (g_t^{(i)})^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2948em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></p><p>其中，衰减率 β2 是一个接近 1 的值（如0.999），它决定了历史信息的“遗忘速度”。</p></li><li><p><strong>更新参数</strong>：更新规则的形式与 AdaGrad 完全相同。</p></li></ol></li><li><p><strong>优点</strong>：</p><ul><li>由于 Vt 是一个移动平均值，它不再会无限增长。如果最近的梯度变小了， Vt 也会随之减小，从而让学习率有机会“回升”。这使得 RMSProp 非常鲁棒，在各种任务上都表现良好。</li></ul></li><li><p><strong>潜在不足</strong>：</p><ul><li>它只解决了自适应学习率的问题（二阶动量），但没有集成 SGD with Momentum 那样的“惯性”（一阶动量）。</li></ul></li></ul></li><li><p><strong>Adam (Adaptive Moment Estimation)</strong></p><ul><li><p><strong>改进</strong>：<strong>集大成者</strong>。既然一阶动量（如 Momentum）能帮助我们确定更好的更新方向，而二阶动量（如 RMSProp）能为每个参数找到合适的学习率，为什么不把它们都用上呢？它将 <strong>SGDM 的一阶动量</strong>（控制方向）和 <strong>RMSProp 的二阶动量</strong>（控制自适应学习率）完美结合。</p></li><li><p><strong>实现机制</strong>：<br>Adam 为每个参数维护了<strong>两个</strong>移动平均值：</p><ol><li><p><strong>一阶动量 mt (梯度的均值)</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>m</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msub><mi>β</mi><mn>1</mn></msub><mo>⋅</mo><msubsup><mi>m</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><msub><mi>β</mi><mn>1</mn></msub><mo stretchy="false">)</mo><mo>⋅</mo><msubsup><mi>g</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup></mrow><annotation encoding="application/x-tex">m_t^{(i)} = \beta_1 \cdot m_{t-1}^{(i)} + (1 - \beta_1) \cdot g_t^{(i)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span></span></span></span></span></p></li><li><p><strong>二阶动量 Vt (梯度平方的均值)</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>V</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msub><mi>β</mi><mn>2</mn></msub><mo>⋅</mo><msubsup><mi>V</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><msub><mi>β</mi><mn>2</mn></msub><mo stretchy="false">)</mo><mo>⋅</mo><mo stretchy="false">(</mo><msubsup><mi>g</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">V_t^{(i)} = \beta_2 \cdot V_{t-1}^{(i)} + (1 - \beta_2) \cdot (g_t^{(i)})^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2948em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></p></li></ol></li><li><p><strong>独有设计：偏差修正 (Bias Correction)</strong></p><ul><li><p><strong>问题</strong>：mt 和 Vt 都是从 0 开始初始化的。在训练初期，由于 β1 和 β2 都很接近 1，会导致 mt 和 Vt 的值系统性地<strong>偏向于 0</strong>。</p></li><li><p><strong>修正</strong>：为了解决这个冷启动问题，Adam 在使用它们之前，先对其进行修正：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mover accent="true"><mi>m</mi><mo>^</mo></mover><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><mfrac><msubsup><mi>m</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mrow><mn>1</mn><mo>−</mo><msubsup><mi>β</mi><mn>1</mn><mi>t</mi></msubsup></mrow></mfrac></mrow><annotation encoding="application/x-tex">\hat{m}_t^{(i)} = \frac{m_t^{(i)}}{1 - \beta_1^t}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal">m</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.6741em;vertical-align:-0.9523em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.7218em;"><span style="top:-2.3588em;"><span class="pstrut" style="height:3.0448em;"></span><span class="mord"><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7754em;"><span style="top:-2.4337em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span><span style="top:-3.0448em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2663em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.2748em;"><span class="pstrut" style="height:3.0448em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.7218em;"><span class="pstrut" style="height:3.0448em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9523em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mover accent="true"><mi>V</mi><mo>^</mo></mover><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><mfrac><msubsup><mi>V</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mrow><mn>1</mn><mo>−</mo><msubsup><mi>β</mi><mn>2</mn><mi>t</mi></msubsup></mrow></mfrac></mrow><annotation encoding="application/x-tex">\hat{V}_t^{(i)} = \frac{V_t^{(i)}}{1 - \beta_2^t}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9468em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.2222em;">V</span></span><span style="top:-3.2523em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.6741em;vertical-align:-0.9523em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.7218em;"><span style="top:-2.3588em;"><span class="pstrut" style="height:3.0448em;"></span><span class="mord"><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7754em;"><span style="top:-2.4337em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span><span style="top:-3.0448em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2663em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.2748em;"><span class="pstrut" style="height:3.0448em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.7218em;"><span class="pstrut" style="height:3.0448em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9523em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>在训练初期（t 很小），分母会显著地放大 mt 和 Vt 的值，以纠正偏差。随着训练的进行（t 变大），分母会趋近于 1，修正作用随之消失。</p></li></ul></li><li><p><strong>最终更新规则</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>w</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>=</mo><msubsup><mi>w</mi><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>−</mo><mfrac><mi>α</mi><msqrt><mrow><msubsup><mover accent="true"><mi>V</mi><mo>^</mo></mover><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup><mo>+</mo><mi>ϵ</mi></mrow></msqrt></mfrac><mo>⋅</mo><msubsup><mover accent="true"><mi>m</mi><mo>^</mo></mover><mi>t</mi><mrow><mo stretchy="false">(</mo><mi>i</mi><mo stretchy="false">)</mo></mrow></msubsup></mrow><annotation encoding="application/x-tex">w_{t+1}^{(i)} = w_t^{(i)} - \frac{\alpha}{\sqrt{\hat{V}_t^{(i)} + \epsilon}} \cdot \hat{m}_t^{(i)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3694em;vertical-align:-0.3246em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4337em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3246em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.8376em;vertical-align:-1.73em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.11em;"><span class="pstrut" style="height:3.3845em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3845em;"><span class="svg-align" style="top:-3.8em;"><span class="pstrut" style="height:3.8em;"></span><span class="mord" style="padding-left:1em;"><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9468em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.2222em;">V</span></span><span style="top:-3.2523em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal">ϵ</span></span></span><span style="top:-3.3445em;"><span class="pstrut" style="height:3.8em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.88em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.88em" viewbox="0 0 400000 1944" preserveaspectratio="xMinYMin slice"><path d="M983 90l0 -0c4,-6.7,10,-10,18,-10 H400000v40H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5c53.7,-170.3,84.5,-266.8,92.5,-289.5zM1001 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.4555em;"><span></span></span></span></span></span></span></span><span style="top:-3.6145em;"><span class="pstrut" style="height:3.3845em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-4.0615em;"><span class="pstrut" style="height:3.3845em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.73em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.2906em;vertical-align:-0.2458em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal">m</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0448em;"><span style="top:-2.4542em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span><span style="top:-3.2198em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">i</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2458em;"><span></span></span></span></span></span></span></span></span></span></span></p><p>这里，参数更新的<strong>方向</strong>由修正后的一阶动量 m^t 提供，而每一步的<strong>学习率</strong>由修正后的二阶动量 V^t 进行<strong>自适应地、逐参数地</strong>调节。</p></li></ul><p><strong>总结</strong>：这条演进之路清晰地展示了研究者们如何不断发现问题并解决问题：<br><strong>AdaGrad</strong> 提出了自适应思想但有硬伤 -&gt; <strong>RMSProp</strong> 修复了硬伤但不够全面 -&gt; <strong>Adam</strong> 融合了 Momentum 和 RMSProp 的优点并加入了偏差修正，最终成为了一个极其强大和通用的优化器。</p></li></ul><h3 id="4-高级优化器：面向工业界的实战选择"><strong>4. 高级优化器：面向工业界的实战选择</strong></h3><ul><li><p><strong>AdamW (Adam with Decoupled Weight Decay)</strong></p><p>要深刻理解 AdamW，我们必须先厘清一个经常被混淆的概念：<strong>L2 正则化 (L2 Regularization)</strong> 和 <strong>权重衰减 (Weight Decay)</strong>。</p><ul><li>在 SGD 这样的简单优化器中，这两者在数学上是等价的。</li><li>但在 Adam 这样的自适应优化器中，它们的效果截然不同，而这个差异正是 AdamW 诞生的原因。</li></ul><p><strong>1. 问题：Adam + L2 正则化错在哪里？</strong></p><ul><li><p><strong>标准 L2 正则化的做法</strong>：将 L2 惩罚项 λ/2 ∥w∥2 加入到损失函数中。这会导致反向传播时，梯度中增加了一项 λw。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>g</mi><mrow><mi>t</mi><mi>o</mi><mi>t</mi><mi>a</mi><mi>l</mi></mrow></msub><mo>=</mo><msub><mi>g</mi><mrow><mi>l</mi><mi>o</mi><mi>s</mi><mi>s</mi></mrow></msub><mo>+</mo><mi>λ</mi><mi>w</mi></mrow><annotation encoding="application/x-tex">g_{total}=g_{loss}+λw</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight">o</span><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight" style="margin-right:0.0197em;">l</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0197em;">l</span><span class="mord mathnormal mtight">oss</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span></span></span></span></span></p></li><li><p><strong>Adam 的处理方式</strong>：Adam 会将这个<strong>包含了正则项的完整梯度</strong> gtotal，送入它的一阶和二阶动量计算中。我们重点看二阶动量 Vt：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>V</mi><mi>t</mi></msub><mo>=</mo><msub><mi>β</mi><mn>2</mn></msub><msub><mi>V</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><msub><mi>β</mi><mn>2</mn></msub><mo stretchy="false">)</mo><mo stretchy="false">(</mo><msub><mi>g</mi><mrow><mi>l</mi><mi>o</mi><mi>s</mi><mi>s</mi></mrow></msub><mo>+</mo><mi>λ</mi><mi>w</mi><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">V_t = \beta_2 V_{t-1} + (1 - \beta_2)(g_{loss} + \lambda w)^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.9028em;vertical-align:-0.2083em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0528em;">β</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0528em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0197em;">l</span><span class="mord mathnormal mtight">oss</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1141em;vertical-align:-0.25em;"></span><span class="mord mathnormal">λ</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></p><p>最终的参数更新步长大致为：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi mathvariant="normal">Δ</mi><mi>w</mi><mo>∝</mo><mfrac><mi>α</mi><msqrt><msub><mi>V</mi><mi>t</mi></msub></msqrt></mfrac><mo>⋅</mo><msub><mi>m</mi><mi>t</mi></msub></mrow><annotation encoding="application/x-tex">\Delta w \propto \frac{\alpha}{\sqrt{V_t}} \cdot m_t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord">Δ</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">∝</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0376em;vertical-align:-0.93em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.2583em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8517em;"><span class="svg-align" style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord" style="padding-left:0.833em;"><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-2.8117em;"><span class="pstrut" style="height:3em;"></span><span class="hide-tail" style="min-width:0.853em;height:1.08em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewbox="0 0 400000 1080" preserveaspectratio="xMinYMin slice"><path d="M95,702c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429c69,-144,104.5,-217.7,106.5,-221l0 -0c5.3,-9.3,12,-14,20,-14H400000v40H845.2724s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47zM834 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1883em;"><span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.93em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.5806em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span></p></li><li><p><strong>矛盾出现</strong>：</p><ul><li><p>L2 正则化的初衷是：<strong>权重 w 越大，惩罚（衰减）就应该越厉害</strong>，以防止过拟合。</p></li><li><p>但在 Adam 中，对于那些本身梯度很大（gloss 很大）或者权重很大（λw 很大）的参数，它们的二阶动量 Vt 也会变得很大。</p></li><li><p>这导致<strong>分母 Vt 变大</strong>，反而<strong>削弱了</strong>对这些重要参数的学习率和正则化效果。</p></li><li><p><strong>结论</strong>：Adam 的自适应学习率机制与 L2 正则项发生了“耦合”，扭曲了 L2 正则化原本的意图，使其效果大打折扣。</p></li></ul></li></ul><p><strong>2. AdamW 的解决方案：解耦 (Decouple)</strong></p><p>AdamW 的思想是返璞归真，让正则化和梯度优化“分道扬镳”。</p><ul><li><p><strong>第一步：纯粹的梯度优化</strong></p><ul><li><p>在计算一阶动量 mt 和二阶动量 Vt 时，<strong>只使用原始的损失梯度 gloss</strong>。这样，mt 和 Vt 就纯粹地反映了损失函数本身的几何特性。</p></li><li><p>计算出不含正则项的“Adam 更新量”：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>Adam_update</mtext><mo>=</mo><mfrac><mi>α</mi><mrow><msqrt><msub><mover accent="true"><mi>V</mi><mo>^</mo></mover><mi>t</mi></msub></msqrt><mo>+</mo><mi>ϵ</mi></mrow></mfrac><mo>⋅</mo><msub><mover accent="true"><mi>m</mi><mo>^</mo></mover><mi>t</mi></msub></mrow><annotation encoding="application/x-tex">\text{Adam\_update} = \frac{\alpha}{\sqrt{\hat{V}_t} + \epsilon} \cdot \hat{m}_t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0044em;vertical-align:-0.31em;"></span><span class="mord text"><span class="mord">Adam_update</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.2376em;vertical-align:-1.13em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.11em;"><span class="pstrut" style="height:3.0834em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.0834em;"><span class="svg-align" style="top:-3.2em;"><span class="pstrut" style="height:3.2em;"></span><span class="mord" style="padding-left:1em;"><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9468em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal" style="margin-right:0.2222em;">V</span></span><span style="top:-3.2523em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.0434em;"><span class="pstrut" style="height:3.2em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.28em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.28em" viewbox="0 0 400000 1296" preserveaspectratio="xMinYMin slice"><path d="M263,681c0.7,0,18,39.7,52,119c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120c340,-704.7,510.7,-1060.3,512,-1067l0 -0c4.7,-7.3,11,-11,19,-11H40000v40H1012.3s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60zM1001 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1566em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal">ϵ</span></span></span><span style="top:-3.3134em;"><span class="pstrut" style="height:3.0834em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.7604em;"><span class="pstrut" style="height:3.0834em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0037em;">α</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.13em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8444em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6944em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord mathnormal">m</span></span><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="accent-body" style="left:-0.25em;"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span></p></li></ul></li><li><p><strong>第二步：独立的权重衰减</strong></p><ul><li><p>在参数更新的最后，<strong>直接减去一个与权重大小成正比的衰减项</strong>。</p></li><li><p>最终更新规则：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>w</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub><mo>=</mo><msub><mi>w</mi><mi>t</mi></msub><mo>−</mo><mtext>Adam_update</mtext><mo>−</mo><mi>α</mi><mo>⋅</mo><msup><mi>λ</mi><mo mathvariant="normal" lspace="0em" rspace="0em">′</mo></msup><mo>⋅</mo><msub><mi>w</mi><mi>t</mi></msub></mrow><annotation encoding="application/x-tex">w_{t+1} = w_t - \text{Adam\_update} - \alpha \cdot \lambda&#x27; \cdot w_t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6389em;vertical-align:-0.2083em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.7333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.0044em;vertical-align:-0.31em;"></span><span class="mord text"><span class="mord">Adam_update</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4445em;"></span><span class="mord mathnormal" style="margin-right:0.0037em;">α</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8019em;"></span><span class="mord"><span class="mord mathnormal">λ</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8019em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.5806em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.2806em;"><span style="top:-2.55em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span></p><p>(这里的 λ′ 是权重衰减系数，注意这个衰减项<strong>没有被 Vt 除</strong>！)</p></li></ul></li><li><p><strong>效果与应用</strong>：</p><ul><li>AdamW 的权重衰减效果更稳定、更符合直觉。它能带来更好的泛化性能和更低的训练损失。</li><li>由于其出色的表现，AdamW 已成为训练 <strong>Transformer</strong>、<strong>BERT</strong>、<strong>GPT</strong> 等大型语言模型的<strong>事实标准</strong>，是现代深度学习工具箱中的必备选项。</li></ul></li></ul></li><li><p><strong>FTRL (Follow-The-Regularized-Leader)</strong></p><p>FTRL 的设计哲学和应用场景与 Adam 完全不同，它是<strong>在线学习 (Online Learning)</strong> 和<strong>广告点击率 (CTR) 预估</strong>领域的王者。</p><p><strong>1. 核心动机与场景</strong></p><ul><li><strong>场景</strong>：在推荐和广告系统中，特征空间是<strong>亿级别、甚至百亿级别的</strong>，并且是<strong>极度稀疏</strong>的。例如，一个用户的特征由他的ID、他正在看的商品ID、广告ID等组成，这些ID绝大部分都不会在下一个样本中出现。</li><li><strong>需求</strong>：<ol><li>模型必须非常小，以便于快速部署和线上<strong>低延迟</strong>推理。</li><li>模型必须能高效地处理稀疏特征。</li></ol></li><li><strong>核心目标</strong>：训练出一个<strong>高度稀疏</strong>的模型，即模型的大部分权重都<strong>等于零</strong>。</li></ul><p><strong>2. 实现机制：带 L1 正则化的“懒惰”更新</strong></p><p>FTRL 的核心在于它独特的、逐坐标的更新法则，其中巧妙地融入了 L1 正则化，以强制产生稀疏性。</p><p>我们可以这样理解它的更新步骤：</p><ol><li><strong>累积信息</strong>：和 AdaGrad 类似，FTRL 为每个参数 w(i) 维护两个累加器：<ul><li>zt(i): 历史梯度之和。</li><li>Vt(i): 历史梯度平方之和（用于自适应学习率）。</li></ul></li><li><strong>“懒惰”的更新决策（稀疏性的来源）</strong>：<br>在决定是否更新 w(i) 之前，FTRL会先做一个判断：<ul><li><strong>如果累积的梯度信号 ∣zt(i)∣ 还不足以“战胜”L1 正则化的惩罚力度 λ1</strong>，即 ∣zt(i)∣&lt;λ1：<ul><li><strong>决策</strong>：FTRL 认为这个特征的信号太弱，不值得为其分配非零权重。于是，<strong>直接将该参数设为 0</strong>。</li></ul></li><li><strong>如果累积的梯度信号足够强</strong>，即 ∣zt(i)∣≥λ1：<ul><li><strong>决策</strong>：此时才认为该特征是有效的，并根据“超出” L1 惩罚的那部分梯度，以及自适应学习率，计算出一个<strong>非零</strong>的权重值。</li></ul></li></ul></li></ol><ul><li><strong>效果与应用</strong>：<ul><li><strong>极致稀疏</strong>：通过这种“阈值过滤”机制，FTRL 能将大量不重要、信号不强的特征权重直接剪枝为零，产出的模型非常小巧。</li><li><strong>工业标准</strong>：在处理大规模类别特征的 LR、FM 这类模型上，FTRL 是当之无愧的工业标准优化器，广泛应用于各大互联网公司的广告和推荐系统的排序阶段，尤其是 <strong>Wide &amp; Deep 模型中的 Wide 部分</strong>。</li></ul></li></ul><p><strong>AdamW vs. FTRL 面试总结</strong></p><table><thead><tr><th>对比维度</th><th>AdamW</th><th>FTRL</th></tr></thead><tbody><tr><td><strong>核心优势</strong></td><td>优秀的泛化能力，稳定的正则化效果</td><td>产出<strong>高度稀疏</strong>的模型，内存占用小，推理速度快</td></tr><tr><td><strong>处理对象</strong></td><td><strong>稠密模型</strong>，如深度神经网络、Transformer</td><td><strong>稀疏模型</strong>，如带海量ID特征的线性模型（LR/FM）</td></tr><tr><td><strong>应用场景</strong></td><td>训练大语言模型、计算机视觉、推荐系统的<strong>Deep部分</strong></td><td>广告/推荐系统的CTR预估、在线学习、推荐系统的<strong>Wide部分</strong></td></tr><tr><td><strong>一句话总结</strong></td><td>训练复杂<strong>深度模型</strong>的“瑞士军刀”</td><td>处理海量<strong>稀疏特征</strong>、追求极致效率的“手术刀”</td></tr></tbody></table></li></ul><h3 id="5-学习率调度：掌握训练的“节奏”"><strong>5. 学习率调度：掌握训练的“节奏”</strong></h3><ul><li><strong>问题</strong>：单一的、固定的学习率无法适应整个训练过程的需求。</li><li><strong>常用策略</strong>：<strong>Warmup + Decay</strong><ul><li><strong>Warmup (热身)</strong>：在训练初期，使用一个很小的学习率，并逐步<strong>线性增加</strong>到预设的目标值。这可以防止模型在初始权重混乱时因步子太大而“崩溃”。</li><li><strong>Decay (衰减)</strong>：在 Warmup 结束后，让学习率随着训练的进行而<strong>逐渐降低</strong>。这有助于模型在接近最优解时，能以更小的步长进行精细探索，从而收敛得更好。</li><li><strong>组合推荐</strong>：<strong>“线性 Warmup + 余弦退火 (Cosine Annealing) Decay”</strong> 是一个非常鲁棒且流行的现代训练策略。</li></ul></li></ul><h2 id="III-文本表示的进化"><strong>III. 文本表示的进化</strong></h2><h3 id="1-统计时代：TF-IDF-—-“以数取胜”的关键词大师"><strong>1. 统计时代：TF-IDF — “以数取胜”的关键词大师</strong></h3><p>在深度学习普及之前，我们用巧妙的统计学方法来衡量词的重要性。其中，TF-IDF 是最经典、最有效的代表。</p><ul><li><strong>核心思想</strong>：一个词的重要性，由它在**本文档内的频率（TF）<strong>和在</strong>整个语料库中的稀有度（IDF）**共同决定。</li><li><strong>TF (Term Frequency, 词频)</strong><ul><li><strong>是什么</strong>：一个词在本篇文章中出现的频率。</li><li><strong>计算</strong>：<code>TF = (某词在文章中出现的次数) / (文章总词数)</code></li><li><strong>直觉</strong>：在一篇关于“自动驾驶”的论文中，“传感器”这个词的 TF 值会很高。但这有个问题：像“的”、“是”这种常用词的 TF 值也会很高。</li></ul></li><li><strong>IDF (Inverse Document Frequency, 逆文档频率)</strong><ul><li><strong>是什么</strong>：对 TF 值进行修正的“惩罚”项，用来衡量一个词的“信息量”或“稀有度”。</li><li><strong>计算</strong>：<code>IDF = log(总文档数 / (包含该词的文档数 + 1))</code></li><li><strong>直觉</strong>：像“的”、“是”这种词，几乎所有文档都包含，分母会很大，因此它们的 IDF 值会极其接近 0。而“自动驾驶”这种专业术语，只在少数文档中出现，其 IDF 值就会很高。</li></ul></li><li><strong>最终形态：TF-IDF</strong><ul><li><strong>计算</strong>：<code>TF-IDF = TF × IDF</code></li><li><strong>效果</strong>：只有那些<strong>在本文档中频繁出现（高TF），但在别的文档中很少见（高IDF）的词，才能获得很高的 TF-IDF 分数。这使得它成为一个极其出色的关键词提取</strong>算法。</li><li><strong>局限性</strong>：<ul><li><strong>语义缺失</strong>：它完全不理解词语的含义。在 TF-IDF 的世界里，“国王”和“女王”是两个毫无关系的词。</li><li><strong>稀疏性</strong>：它为每个文档生成一个维度等于整个词典大小的向量，其中绝大部分都是零，非常稀疏。</li></ul></li></ul></li></ul><h3 id="2-静态向量时代：Word2Vec-—-“物以类聚”的语义先锋"><strong>2. 静态向量时代：Word2Vec — “物以类聚”的语义先锋</strong></h3><p>为了解决 TF-IDF 无法理解语义的问题，神经网络方法应运而生，其核心思想是<strong>分布式假设 (Distributional Hypothesis)</strong>：“一个词的含义，由其上下文决定”。</p><ul><li><strong>核心思想</strong>：<br>不再统计词频，而是通过一个神经网络，为词典中的每一个词学习一个<strong>稠密的、低维的向量（即 Embedding）</strong>。在这个向量空间中，意思相近的词，其向量距离也相近。</li><li><strong>两种经典训练范式</strong>：<ol><li><strong>CBOW (Continuous Bag-of-Words)</strong>：<strong>上下文 -&gt; 中心词</strong>。<ul><li><strong>任务</strong>：“完形填空”。模型看到 <code>[深度, __, 网络]</code>，然后去预测中间的词是“学习”。</li><li><strong>特点</strong>：训练速度快，对高频词效果好。</li></ul></li><li><strong>Skip-gram</strong>：<strong>中心词 -&gt; 上下文</strong>。<ul><li><strong>任务</strong>：“头脑风暴”。模型看到“学习”，然后去预测它周围可能出现的词是 <code>[深度, __, 网络]</code>。</li><li><strong>特点</strong>：训练速度慢一些，但对低频词和生僻词的学习效果更好。</li></ul></li></ol></li><li><strong>在推荐系统中的应用：Item2Vec</strong><ul><li>这是一个绝妙的类比：将<strong>用户的行为序列（如点击、购买）看作一个“句子”</strong>，将<strong>商品ID看作“词语”</strong>。</li><li>通过 Skip-gram 模型训练这些“句子”，我们就能得到每个商品的 Embedding。</li><li><strong>效果</strong>：经常被一同购买或浏览的商品（如“手机壳”和“手机膜”），它们的 Embedding 就会非常接近。这是构建“相关推荐”、“猜你喜欢”等模块的强大基石。</li></ul></li><li><strong>致命缺陷：无法解决多义词 (Polysemy)</strong><ul><li>Word2Vec 为每个词生成一个<strong>全局唯一、固定不变</strong>的静态向量。</li><li><strong>经典例子</strong>：<ol><li>我把钱存进了<strong>银行 (bank)</strong>。 (金融机构)</li><li>我坐在<strong>河岸 (bank)</strong> 上。 (地理位置)</li></ol></li><li>对于 Word2Vec，这两个 <code>bank</code> 的向量是<strong>一模一样的</strong>。它无法根据上下文动态地调整词的表示，这是其最大的局限性。</li></ul></li></ul><h3 id="3-动态向量时代：BERT-—-“千人千面”的语境大师"><strong>3. 动态向量时代：BERT — “千人千面”的语境大师</strong></h3><p>BERT 的出现，标志着 NLP 进入了一个新纪元，其核心是提供<strong>上下文相关的动态词向量 (Contextualized Word Embeddings)</strong>。</p><ul><li><strong>核心思想</strong>：一个词没有固定的含义，它的意义完全由其所在的上下文语境所定义。</li><li><strong>如何实现？</strong><ol><li><strong>强大的骨架：Transformer Encoder</strong>。我们已经深入讨论过，其核心的<strong>自注意力机制</strong>，允许输入序列中的每一个词都能“看到”并与所有其他词进行信息交互。</li><li><strong>动态计算</strong>：BERT <strong>不会预先存好每个词的向量</strong>。当一个句子输入后，句子中的每个词的初始 Embedding 会流经 BERT 的多层（如12层）Transformer。在<strong>每一层</strong>，每个词的向量都会根据<strong>整个句子</strong>的信息被重新计算和优化。</li><li><strong>最终输出</strong>：从 BERT 的最后一层输出的，才是这个词在<strong>当前特定语境下</strong>的最终向量表示。</li></ol></li><li><strong>解决多义词问题</strong>：<ul><li>当处理 “I went to the <strong>bank</strong>” 时，<code>bank</code> 的向量会因为它看到了 <code>went to</code> 等信息，而被塑造成带有“金融”含义的向量。</li><li>当处理 “The river <strong>bank</strong>” 时，<code>bank</code> 的向量会因为它看到了 <code>river</code>，而被塑造成带有“地理”含义的向量。</li><li>这两个 <code>bank</code> 的最终输出向量在向量空间中会相距很远，从而完美解决了多义词问题。</li></ul></li></ul><p><strong>总结</strong>：从 TF-IDF 的词频统计，到 Word2Vec 的静态语义向量，再到 BERT 的动态上下文向量，我们看到机器对文本的理解，从<strong>表层统计</strong>，发展到<strong>固定语义</strong>，最终达到了<strong>深度、动态的语境理解</strong>。这条进化路径是所有现代搜广推应用的基础。</p><h2 id="IV-Transformer-与-BERT-模型深度剖析"><strong>IV. Transformer 与 BERT 模型深度剖析</strong></h2><h3 id="1-Transformer-基石：自注意力机制-Self-Attention"><strong>1. Transformer 基石：自注意力机制 (Self-Attention)</strong></h3><p>自注意力机制是 Transformer 的心脏。它的核心思想是：一个序列中的每个元素，其新的表示，应该由序列中<strong>所有元素</strong>根据相关性进行加权求和得到。</p><p><strong>1.1 QKV 矩阵的角色分工与生成方式</strong></p><p>为了实现上述思想，自注意力机制为每个输入向量 <code>x</code> 创造了三个不同的身份，这个过程是通过三个独立、可学习的线性变换矩阵 WQ,WK,WV 完成的：</p><ul><li><strong>Query (Q, 查询)</strong>: Q=X⋅WQ<ul><li><strong>角色</strong>：代表当前词为了更好地理解自己，主动发出的一个“<strong>查询</strong>”或“<strong>问题</strong>”。它在问：“为了理解我，我应该关注序列中的哪些信息？”</li></ul></li><li><strong>Key (K, 键)</strong>: K=X⋅WK<ul><li><strong>角色</strong>：代表序列中每个词所拥有的一个“<strong>索引</strong>”或“<strong>标签</strong>”，用来被别人查询。它在说：“这是我的身份标签，你们可以根据这个标签来判断我是否与你们相关。”</li></ul></li><li><strong>Value (V, 值)</strong>: V=X⋅WV<ul><li><strong>角色</strong>：代表序列中每个词实际蕴含的**“内容”或“信息”**。它在说：“如果你们关注我，这就是我能提供给你们的丰富信息。”</li></ul></li></ul><p><strong>计算流程：</strong></p><ol><li><strong>匹配打分</strong>：用每个词的 Query 向量，去和所有词的 Key 向量进行点积运算 (QKT)，得到一个注意力分数矩阵。这个分数代表了“查询”与“索引”之间的匹配程度。</li><li><strong>加权求和</strong>：将分数通过 Softmax 归一化为权重，然后用这个权重去对所有词的 Value 向量进行加权求和。</li></ol><p><strong>Encoder vs Decoder 中的区别</strong>:</p><ul><li><strong>Encoder 中 (Self-Attention)</strong>：Q, K, V 全部来自于<strong>上一层 Encoder 的输出</strong>。它是在对输入序列自身进行信息重组，目的是为了深刻理解输入。</li><li><strong>Decoder 中 (包含两种 Attention)</strong>：<ol><li><strong>Masked Self-Attention</strong>: Q, K, V 全部来自于<strong>上一层 Decoder 的输出</strong>。目的是让已生成的部分序列理解自身，为生成下一个词做准备。</li><li><strong>Cross-Attention</strong>: <strong>Q 来自于 Decoder</strong> (上一步 Masked Self-Attention 的输出)，而 <strong>K 和 V 来自于 Encoder 的最终输出</strong>。这是让 Decoder 在生成新内容时，能够查询和参考完整的原始输入信息的关键桥梁。</li></ol></li></ul><p><strong>1.2 位置编码 (Positional Encoding): 弥补顺序信息的缺失</strong></p><ul><li><strong>问题</strong>：自注意力机制本身是“无序”的，它平等地看待序列中的每一个词，无法感知它们的先后顺序。</li><li><strong>解决方案</strong>：在模型的输入层，将每个词的 Embedding 与一个代表其<strong>绝对位置</strong>的“位置向量”相加，强行把顺序信息注入模型。</li><li><strong>两种主流方式</strong>：<ol><li><strong>固定式 (Sinusoidal PE)</strong>：原始 Transformer 使用的方式。它通过不同频率的 <code>sin</code> 和 <code>cos</code> 函数生成一个固定的、不参与训练的位置向量。其优点是能够处理比训练时更长的序列，并且其周期性使得模型能轻易学习到相对位置关系。</li><li><strong>可学习式 (Learnable PE)</strong>：BERT 等模型使用的方式。它创建一个位置 Embedding 矩阵（就像一个词表一样），让模型在训练过程中自己去学习每个位置最合适的向量表示。优点是更灵活、更能适应特定任务，但通常有最大长度限制。</li></ol></li></ul><p><strong>1.3 注意力掩码 (Attention Mask): 控制信息的流动</strong></p><ul><li><strong>原理</strong>：在计算好的注意力分数矩阵上，在送入 Softmax 之前，将需要被屏蔽的位置的分数<strong>设置为一个巨大的负数</strong>（如 -1e9）。这样，经过 Softmax 后，这些位置的注意力权重就会变成 0。</li><li><strong>两种核心掩码</strong>：<ol><li><strong>Padding Mask (填充掩码)</strong>：<ul><li><strong>应用场景</strong>：Encoder 和 Decoder。</li><li><strong>目的</strong>：在处理一个批次的变长序列时，我们需要用 <code>&lt;pad&gt;</code> 标记将短序列补齐。此掩码的作用就是告诉模型，在计算注意力时，<strong>完全忽略这些无意义的 <code>&lt;pad&gt;</code> 标记</strong>。</li></ul></li><li><strong>Look-ahead Mask (前瞻/因果掩码)</strong>：<ul><li><strong>应用场景</strong>：<strong>仅用于 Decoder</strong>。</li><li><strong>目的</strong>：在生成任务中，为了保证模型的自回归特性，在预测第 <code>i</code> 个词时，<strong>绝对不能让它“偷看”到第 <code>i</code> 个词之后的信息</strong>。</li><li><strong>实现</strong>：通过一个上三角矩阵，将所有“未来”位置的注意力分数全部屏蔽掉。</li></ul></li></ol></li></ul><p><strong>1.4 注意力缩放因子根号 dk 的作用</strong></p><ul><li><p><strong>问题</strong>：在计算 Q 和 K 的点积时，如果向量维度 dk 很大（例如 512），点积的结果的方差也会变得很大。</p></li><li><p><strong>危害</strong>：过大的点积结果会把 Softmax 函数推向其<strong>饱和区</strong>，导致梯度变得极小，从而引发<strong>梯度消失</strong>，使训练过程不稳定甚至失败。</p></li><li><p><strong>解决方案</strong>：将计算出的点积分数<strong>除以根号 dk</strong>。</p></li><li><p><strong>数学原理</strong>：原论文作者证明，假设 Q 和 K 的元素是均值为 0、方差为 1 的独立随机变量，那么它们的点积结果的方差就是 dk。因此，除以 <strong>根号</strong>dk 这一步，恰好能将点积结果的方差重新<strong>归一化为 1</strong>，从而保证了送入 Softmax 的数值在一个合理的、梯度稳定的范围内。这是一个至关重要的<strong>稳定化技巧</strong>。</p></li></ul><h3 id="2-BERT-模型解析">2. BERT 模型解析</h3><p>BERT (Bidirectional Encoder Representations from Transformers) 的发布是 NLP 领域的里程碑事件。它的核心贡献在于，通过巧妙的预训练任务，第一次实现了<strong>深度的、无监督的双向语言表示</strong>学习。</p><p><strong>2.1 预训练任务：MLM 与 NSP 的原理、缺陷及改进</strong></p><p>BERT 的强大能力，源于它在海量文本上进行的两项“自我修炼”任务。</p><ul><li><strong>MLM (Masked Language Model / 掩码语言模型)</strong><ul><li><strong>原理</strong>：这是 BERT 的<strong>核心创新</strong>。它采用“完形填空”的方式进行训练。<ol><li>随机选取输入序列中 15% 的 Token。</li><li>在这 15% 的 Token 中，80% 的情况用一个特殊的 <code>[MASK]</code> 标记替换掉，10% 的情况用一个随机的词替换，10% 的情况保持原样。</li><li>模型的目标，是<strong>仅根据未被遮盖的上下文，准确预测出这些被遮盖位置的原始 Token 是什么</strong>。</li></ol></li><li><strong>意义（为什么是双向的？）</strong>：与只能看到左侧文本的 GPT 不同，MLM 任务天然地强迫模型必须<strong>同时利用被遮盖位置的左侧和右侧的全部上下文信息</strong>才能做出正确的预测。这使得 BERT 能够学习到真正意义上的<strong>深层双向语境表示</strong>。</li><li><strong>缺陷与改进</strong>：<ul><li><strong>独立性假设问题</strong>：BERT 在预测多个 <code>[MASK]</code> 时是相互独立的，无法利用被预测词之间的关系。</li><li><strong>改进 1: WWM (Whole Word Masking)</strong>：对策是，如果一个词被 WordPiece 分割成多个子词（如 <code>playing</code> -&gt; <code>['play', '##ing']</code>），那么就将这个完整单词对应的<strong>所有子词一同进行 Mask</strong>。这增加了任务难度，也更符合语言逻辑。</li><li><strong>改进 2: SpanBERT</strong>：更进一步，不再 Mask 零散的词，而是 <strong>Mask 一个连续的文本片段 (Span)</strong>。模型需要利用片段<strong>两端边界</strong>的信息来预测整个片段的内容，这促使模型学习更强的短语级语义关系。</li></ul></li></ul></li><li><strong>NSP (Next Sentence Prediction / 下一句预测)</strong><ul><li><strong>原理</strong>：这是一个二分类任务，用于学习<strong>句子间的关系</strong>。<ol><li>模型接收一对句子 (A, B)。</li><li>50% 的情况下，B 是 A 在原文中真实的下一句（正样本）。</li><li>50% 的情况下，B 是从其他文档中随机抽取的一个句子（负样本）。</li><li>模型需要预测 B 是否是 A 的下一句。</li></ol></li><li><strong>缺陷</strong>：后来的研究发现，这个任务存在“捷径”。由于负样本来自完全不同的文档，模型很可能仅通过判断<strong>两个句子的主题是否相关</strong>就能做出正确判断，而没有真正学到更深层次的<strong>逻辑连贯性</strong>。</li><li><strong>改进</strong>：<ul><li><strong>RoBERTa</strong> 发现此任务弊大于利，<strong>直接将其废除</strong>，仅使用 MLM 在更长的连续文本上训练，效果反而更好。</li><li><strong>ALBERT</strong> 提出了 <strong>SOP (Sentence Order Prediction)</strong> 任务。它取同一文档中的两个连续句子，50% 保持原序，50% 调换顺序。这排除了主题相关性的干扰，强迫模型去学习更细致的<strong>语篇连贯性</strong>。</li></ul></li></ul></li></ul><p><strong>2.2 BERT 的输入构成：三位一体的 Embedding</strong></p><p>为了让模型同时理解“词义”、“位置”和“句间关系”，BERT 的每个输入 Token 的最终向量表示，是由<strong>三种 Embedding 按元素相加</strong>而成的。</p><ul><li><strong>Token Embeddings (词元嵌入)</strong>：最基础的部分，代表了每个词或子词（WordPiece）本身的语义。</li><li><strong>Segment Embeddings (分段嵌入)</strong>：用于区分两个不同的输入句子。所有属于第一句话的 Token，其 Segment Embedding 都是同一个向量（通常是 EA）；所有属于第二句话的 Token，其 Segment Embedding 都是另一个向量（EB）。这是为 NSP 任务量身设计的。</li><li><strong>Position Embeddings (位置嵌入)</strong>：我们讨论过的<strong>可学习的</strong>位置向量。每个位置（0, 1, 2, …, 511）都有一个独立的、在训练中不断优化的向量，来为模型提供顺序信息。</li></ul><p><code>最终输入 = Token Embedding + Segment Embedding + Position Embedding</code></p><p><strong>2.3 [CLS] 标记：序列表示的“代言人”</strong></p><ul><li><strong>是什么</strong>：一个特殊的标记，永远被放在输入序列的<strong>最开头</strong>。</li><li><strong>工作原理</strong>：<br><code>[CLS]</code> 本身没有语义，但因为它和其他所有词一样，参与了 BERT 所有 Encoder 层的<strong>全局自注意力计算</strong>，所以在经过 12 层的信息充分“搅拌”和“融合”后，<code>[CLS]</code> 标记在最后一层对应的<strong>输出向量</strong>，就成了一个<strong>包含了整个输入序列信息的“聚合表示”</strong>。它像一个指定的“会议总结人”，最终产出了代表会议精神的总结报告。</li><li><strong>意义与应用</strong>：<ul><li><strong>提供固定维度的句子表示</strong>：无论输入句子多长，我们都可以从 <code>[CLS]</code> 位置得到一个固定维度（如 768 维）的向量来代表整个句子。</li><li><strong>简化下游任务微调</strong>：在进行句子级别的分类任务时（如情感分析、文本分类），我们只需要：<ol><li>取出 BERT 最后一层的 <code>[CLS]</code> 输出向量。</li><li>将这个向量接入一个简单的、小型的分类器（如一个全连接层）。</li><li>进行微调。此时，<strong>分类任务的梯度会通过 <code>[CLS]</code> 向量反向传播到整个 BERT 模型</strong>，等于在告诉模型：“请调整你所有的参数，把对我的任务最有用的信息，都浓缩到 <code>[CLS]</code> 这个向量里来！”</li></ol></li></ul></li></ul><p>通过这种方式，<code>[CLS]</code> 成为了连接强大的预训练模型和各种具体下游任务的标准、高效的桥梁。</p><h3 id="3-Transformer-BERT-的局限与优化"><strong>3. Transformer &amp; BERT 的局限与优化</strong></h3><p><strong>3.1 注意力机制效率优化</strong></p><p>这部分主要关注两个场景：一是<strong>推理 (Inference)</strong> 时的显存和速度优化，二是<strong>训练 (Training)</strong> 时的计算效率优化。</p><p><strong>从 MHA 到 GQA 和 MQA 的演进：优化推理中的 KV Cache</strong></p><p>在进行自回归生成（即一个词一个词地生成文本）时，为了避免重复计算，模型需要将过去所有 token 的 Key (K) 和 Value (V) 缓存起来，这被称为 <strong>KV Cache</strong>。随着生成文本越来越长，KV Cache 会变得非常巨大，成为推理时的显存瓶颈。</p><ul><li><strong>MHA (Multi-Head Attention)</strong>：标准的多头注意力。<ul><li><strong>机制</strong>：假设有 32 个头，那么就有 32 组独立的 Q, K, V 投影矩阵。</li><li><strong>问题</strong>：在推理时，需要缓存 32 组独立的 K 和 V。这导致 KV Cache 非常庞大，不仅消耗大量显存，而且在生成每个新 token 时，从显存中读取巨大的 K 和 V 矩阵也非常耗时。</li></ul></li><li><strong>MQA (Multi-Query Attention)</strong>：一个极致的优化方案。<ul><li><strong>核心思想</strong>：“所有头真的需要各自独立的 K 和 V 吗？也许它们可以共享？”</li><li><strong>机制</strong>：仍然保留 32 个独立的 Query 头，但让所有这 32 个头<strong>共享同一套 Key 和 Value 投影</strong>。</li><li><strong>效果</strong>：KV Cache 的大小被急剧压缩为原来的 1/32！这极大地降低了推理时的显存占用，并显著提升了生成速度。</li><li><strong>代价</strong>：牺牲了一定的模型性能和容量，因为所有头被迫从同一个“知识库”中提取信息。</li></ul></li><li><strong>GQA (Grouped-Query Attention)</strong>：一个介于 MHA 和 MQA 之间的“黄金平衡点”。<ul><li><strong>机制</strong>：将 32 个 Query 头分成若干组，比如 4 组，每组 8 个头。<strong>在同一组内的 8 个头，共享同一套 Key 和 Value</strong>。这样，我们就从 MHA 的 32 组 K/V，和 MQA 的 1 组 K/V，变成了 4 组 K/V。</li><li><strong>效果</strong>：它获得了 MQA 大部分的加速和显存节省效果，同时比 MQA 保留了更好的模型质量。GQA 是当前最先进的开源大模型（如 Llama 2/3）采用的主流方案。</li></ul></li></ul><p><strong>FlashAttention 的 I/O 感知优化原理：重塑训练效率</strong></p><p>FlashAttention 是一项革命性的算法层面的优化，它在**不改变任何数学结果（即精确注意力）**的前提下，极大地提升了训练和长文本推理的速度。</p><ul><li><strong>核心洞察</strong>：标准注意力的瓶颈不在于浮点数计算（FLOPs），而在于<strong>内存的读写（I/O）</strong>。在计算过程中，需要生成一个巨大的 N×N 的中间矩阵（QKT 的结果），并反复地在 GPU 核心的高速缓存（SRAM）和 GPU 的主显存（HBM）之间进行读写。HBM 带宽远低于 SRAM 的计算速度，这造成了大量的等待时间。</li><li><strong>举例</strong>：对于一个不算太长的序列，比如 N = 16k (16384)，如果用半精度(2字节)存储，这个矩阵的大小就是 <code>16384 * 16384 * 2 bytes ≈ 512 MB</code>。</li><li><strong>瓶颈</strong>：这个 512MB 的矩阵，对于 GPU 核心旁边的高速缓存 (SRAM, 只有几 MB) 来说太巨大了，根本放不下。因此，GPU 必须将这个巨大的矩阵写入速度慢得多的主显存 (HBM)。在整个计算过程中，GPU 核心不得不频繁地暂停计算，去等待这个大矩阵从 HBM 中读出或写入。<strong>计算单元大部分时间都在“等”，而不是在“算”</strong>。这就是所谓的 I/O 瓶颈，其读写次数是 O(N2) 的。</li><li><strong>FlashAttention 的解决方案</strong>：<ol><li><strong>分块计算 (Tiling)</strong>：它不再试图一次性生成整个 N×N 的大矩阵。它将输入的 Q, K, V 矩阵切分成一个个更小的“块 (Tile)”，小到可以完全放进高速的 SRAM 中。</li><li><strong>算子融合 (Kernel Fusion)</strong>：FlashAttention 将注意力的多个计算步骤（矩阵乘法、Mask、Softmax、与V相乘等）<strong>融合成了单个 GPU 计算核 (Kernel)</strong>。数据块一旦从慢速的 HBM 载入到高速的 SRAM 后，就会在这个“小作坊”里完成所有的计算，得到最终结果，然后再写回 HBM。</li><li><strong>执行流程</strong>：<ul><li>将一小块 Q 和一小块 K 从慢速 HBM 加载进高速 SRAM。</li><li><strong>在 SRAM 内部</strong>，完成这部分的 QK^T 计算、Mask、Softmax 和与 V 相乘的全部操作。</li><li><strong>只将最终计算好的那一小块输出结果</strong>写回到慢速 HBM 中。</li><li><strong>关键</strong>：那个巨大的 N×N 中间矩阵，自始至终<strong>从未被完整地创建和写入到慢速 HBM 中</strong>。它只在高速 SRAM 的“小作坊”里昙花一现。</li></ul></li><li><strong>在线 Softmax</strong>：它使用了一种巧妙的数值稳定技巧，可以在不看到全局的情况下，分块地计算出完全正确的 Softmax 结果。</li></ol></li><li><strong>效果</strong>：通过<strong>最大化高效率的片上计算，最小化低效率的内存读写</strong>，FlashAttention 将注意力的实际复杂度从 O(N2) 降低到了接近线性的 O(N)，使得在数万长度的序列上训练 Transformer 成为可能。</li></ul><p><strong>3.2 长文本处理策略</strong></p><p>当序列长度超出了模型本身或硬件的极限时，我们需要一些工程策略来处理。</p><ul><li><strong>截断/分块 (Truncation/Segmentation)</strong><ul><li><strong>做法</strong>：最简单粗暴的方法。直接将长文本切成固定长度（如 512）的块，分别输入模型，最后再对结果进行聚合（如取平均）。</li><li><strong>缺点</strong>：完全丢失了块与块之间的上下文信息。</li></ul></li><li><strong>滑动窗口 (Sliding Window)</strong><ul><li><strong>做法</strong>：同样是分块，但在块与块之间设置<strong>重叠区域 (Overlap)</strong>。例如，第一块是 <code>[0..512]</code>，第二块可以是 <code>[384..896]</code>。</li><li><strong>优点</strong>：通过重叠部分，模型能够感知到一部分跨块的上下文，效果优于简单截断。</li></ul></li><li><strong>稀疏注意力模型 (Sparse Attention Models)</strong><ul><li><strong>做法</strong>：从模型架构层面进行修改，不再让每个 token 都关注所有其他 token。</li><li><strong>例子</strong>：<strong>Longformer</strong> 模型使用了<strong>局部窗口注意力</strong>（每个 token 只关注自己附近的一小块区域）和<strong>全局注意力</strong>（少数几个重要的 token 可以被所有 token 关注）的组合。</li><li><strong>注意</strong>：这类方法是标准注意力的<strong>一种近似</strong>，会损失部分信息，而 FlashAttention 是对标准注意力的<strong>精确加速</strong>。</li></ul></li></ul><p><strong>3.3 QLoRA 能用 FlashAttention 吗？</strong></p><p><strong>答案是：不但能，而且是“天作之合”！</strong></p><p>在实际应用中，将 QLoRA 和 FlashAttention 结合使用，是当前在有限资源下<strong>最高效地微调长文本大语言模型</strong>的<strong>黄金标准方案</strong>。</p><p>它们之所以能完美配合，是因为它们解决了两个<strong>正交的、互补的</strong>问题。</p><ul><li><strong>QLoRA 的职责：解决“静态存储”问题</strong><ul><li><strong>目标</strong>：降低模型<strong>权重参数 (Weights)</strong> 的存储体积。</li><li><strong>手段</strong>：通过 4-bit 量化，将一个原本需要 140GB 显存的 70B 模型压缩到约 35GB，使其能够**“装进”** 单张 GPU。</li><li><strong>关注点</strong>：模型文件本身的大小。</li></ul></li><li><strong>FlashAttention 的职责：解决“动态计算”问题</strong><ul><li><strong>目标</strong>：降低模型在<strong>前向/反向传播</strong>过程中，<strong>中间激活值 (Activations)</strong> 产生的内存和 I/O 开销。</li><li><strong>手段</strong>：通过 I/O 感知的算法，避免生成巨大的中间矩阵。</li><li><strong>关注点</strong>：计算过程中的效率和临时内存占用。</li></ul></li></ul><p><strong>它们如何协同工作？</strong></p><p>设想一下用 QLoRA + FlashAttention 微调一个长文本任务的完整流程：</p><ol><li><strong>模型加载</strong>：首先，<strong>QLoRA</strong> 发挥作用。一个巨大的、被量化成 4-bit 的基础模型和微小的、16-bit 的 LoRA 适配器被加载到 GPU 显存中。<strong>（解决了静态存储问题）</strong></li><li><strong>前向传播开始</strong>：当数据输入到模型的某个注意力层时：<br>a. <strong>QLoRA</strong> 将当前计算需要的<strong>一小部分</strong> 4-bit 权重<strong>即时反量化</strong>成 16-bit。<br>b. 用这些 16-bit 的权重去计算出 16-bit 的 Q, K, V 矩阵。</li><li><strong>注意力计算</strong>：此时，<strong>FlashAttention</strong> 接管工作。<br>a. 它接收 16-bit 的 Q, K, V 矩阵。<br>b. 开始它的“分块+融合”计算流程，高效地算出注意力输出，全程<strong>避免了生成那个巨大的 N x N 矩阵</strong>。<strong>（解决了动态计算问题）</strong></li><li><strong>反向传播</strong>：梯度计算的过程也同样受益于 FlashAttention 的优化。</li></ol><p><strong>总结</strong>：QLoRA 解决了**“模型太大放不进显卡”<strong>的问题，而 FlashAttention 解决了</strong>“序列太长导致计算过程爆炸”**的问题。两者联手，使得我们可以在单张消费级或专业级 GPU 上，高效地微调一个百亿级别参数的大模型来处理数万长度的超长文本，这在以前是完全无法想象的。</p><h2 id="V-大语言模型（LLM）的训练与应用实践"><strong>V. 大语言模型（LLM）的训练与应用实践</strong></h2><h3 id="1-参数高效微调-PEFT"><strong>1. 参数高效微调 (PEFT)</strong></h3><ul><li><strong>核心动机</strong>：<br>对一个拥有数十亿甚至上百亿参数的大语言模型进行全量微调（即更新所有参数），其代价是极其高昂的。不仅需要海量的计算资源，更需要巨大的 GPU 显存来存储模型参数、梯度、以及像 Adam 这样的优化器所产生的额外状态。PEFT 的目标就是，在<strong>尽可能少地改动原模型</strong>的前提下，达到<strong>接近全量微调的效果</strong>。</li><li><strong>基本思路</strong>：<br><strong>冻结</strong>大部分原始模型的参数，只向模型中注入一小部分<strong>可训练的“适配器”模块</strong>，在微调时，只更新这些新增的、极少数的参数。</li></ul><p><strong>1.1 LoRA 的核心原理：低秩适应、冻结参数与“旁路”训练</strong></p><p>LoRA (Low-Rank Adaptation) 是目前最成功、应用最广泛的 PEFT 方法之一。</p><ul><li><strong>核心洞察 (Low-Rank Hypothesis)</strong>：<br>一个预训练好的大模型在适配下游新任务时，其参数的“改变量” ΔW 是具有“低内在秩”的。通俗地说，这个巨大的改动矩阵，可以被近似地分解为两个非常“瘦长”的小矩阵 B⋅A 的乘积。</li><li><strong>实现机制：注入“旁路”</strong><ol><li><p><strong>冻结原始权重</strong>：将 Transformer 中需要微调的层（通常是 QKV 投影层和 MLP 层）的原始权重矩阵 W <strong>完全冻结</strong>，使其在训练中不被更新。</p></li><li><p><strong>创建低秩旁路</strong>：在原始权重旁边，并联一个新的、由两个低秩矩阵 A 和 B 组成的“旁路”。</p><ul><li>矩阵 A 的维度是 <code>d x r</code>（d是输入维度，r是极小的秩，如8）。</li><li>矩阵 B 的维度是 <code>r x k</code>（k是输出维度）。</li><li>这两个小矩阵 A 和 B 是<strong>可训练的</strong>。</li></ul></li><li><p><strong>合并计算</strong>：对于输入 <code>x</code>，最终的输出是“主路”和“旁路”输出的加和。</p><p>h=Wx+B(Ax)=(W+BA)x</p></li><li><p><strong>训练过程</strong>：在训练时，只有矩阵 A 和 B 的参数会被梯度更新。因为 <code>r</code> 非常小，所以需要训练的参数量只有全量微调的 <strong>0.1% ~ 1%</strong>，极大地降低了计算和存储开销。</p></li></ol></li><li><strong>优点</strong>：<ul><li><strong>高效</strong>：训练速度快，显存占用小。</li><li><strong>易于部署</strong>：对于不同的任务，只需要保存各自训练好的、非常小巧的 LoRA 权重（A和B），而共享同一个巨大的基础模型。</li></ul></li></ul><p><strong>1.2 QLoRA 的核心原理：4-bit 量化、块缩放与即时反量化</strong></p><p>QLoRA 将 LoRA 的效率推向了极致，解决了在微调时<strong>加载整个基础模型</strong>的显存瓶颈问题。</p><ul><li><strong>核心思想</strong>：<br>既然基础模型 W 在 LoRA 训练中是被冻结的，我们是否可以用一种精度更低、更省空间的方式来<strong>存储</strong>它呢？答案就是<strong>量化 (Quantization)</strong>。</li><li><strong>关键技术</strong>：<ol><li><strong>4-bit 量化 (NF4)</strong>：<ul><li><strong>做法</strong>：QLoRA 将原来用 16-bit (FP16/BF16) 存储的基础模型权重，压缩成用一种新的、信息论最优的 <strong>4-bit NormalFloat (NF4)</strong> 数据类型来存储。</li><li><strong>效果</strong>：仅此一项，就将基础模型的显存占用<strong>降低为原来的 1/4</strong>。</li></ul></li><li><strong>块自适应缩放 (Block-wise Scaling)</strong>：<ul><li><strong>做法</strong>：为了提高量化精度，模型权重被分成很多小块（如256个元素一组）。<strong>每一块都独立计算一个缩放因子</strong>。</li><li><strong>效果</strong>：这使得量化过程可以自适应地处理不同数值范围的权重，极大地减小了量化误差。</li></ul></li><li><strong>即时反量化 (On-the-fly Dequantization)</strong>：<ul><li><strong>工作方式</strong>：在模型进行前向传播时，<strong>计算需要用到哪一小块权重，就即时地将这一块 4-bit 的数据反量化回 16-bit 的计算精度</strong>。计算完成后，这个 16-bit 的临时数据立刻被丢弃，显存中只保留 4-bit 的版本。</li><li><strong>效果</strong>：这保证了计算的精度，同时维持了极低的常驻显存占用。</li></ul></li></ol></li></ul><h3 id="2-解码与生成策略：控制模型的“创造力”"><strong>2. 解码与生成策略：控制模型的“创造力”</strong></h3><p>LLM 在生成文本时，本质上是在每一步都计算出一个覆盖整个词汇表的概率分布。解码策略就是我们从这个概率分布中挑选下一个词的方法。</p><ul><li><strong>Greedy Search (贪心搜索)</strong><ul><li><strong>做法</strong>：最简单直接的方法，每一步都选择当前概率最高的那个词。</li><li><strong>问题</strong>：虽然安全，但极其容易导致生成内容<strong>枯燥、重复</strong>，甚至陷入“我我我我……”这样的死循环。它只顾眼前最优，可能会错失全局更优的句子。</li></ul></li><li><strong>Top-k Sampling (Top-k 采样)</strong><ul><li><strong>做法</strong>：为了增加多样性，我们不再只看第一名，而是划定一个范围。<ol><li>找出概率最高的 <code>k</code> 个候选词。</li><li>在这 <code>k</code> 个词内部，按照它们自身的概率分布进行<strong>随机采样</strong>。</li></ol></li><li><strong>效果</strong>：通过引入可控的随机性，生成的内容变得更生动、更多样。<code>k</code> 是一个超参数，<code>k</code> 越大，随机性越强；<code>k</code> 越小，生成结果越稳定。</li></ul></li><li><strong>Top-p (Nucleus) Sampling (Top-p / 核心采样)</strong><ul><li><strong>做法</strong>：这是目前更受青睐的一种方法，它比 Top-k 更智能、更自适应。<ol><li>它不固定候选词的数量 <code>k</code>，而是设定一个<strong>累积概率阈值 <code>p</code></strong>（例如 0.95）。</li><li>从概率最高的词开始逐个累加，直到这部分词的<strong>总概率超过 <code>p</code></strong> 为止，形成一个“核心候选集 (Nucleus)”。</li><li>在这个动态大小的核心候选集中进行随机采样。</li></ol></li><li><strong>优点</strong>：<ul><li>当模型对下一个词非常**“确定”**时（例如，“法国的首都是” -&gt; “巴”的概率极高），核心集会很小，保证了事实的准确性。</li><li>当模型非常**“不确定”**时（例如，写故事的开头），核心集会很大，允许模型进行更有创造性的探索。</li></ul></li></ul></li></ul><p><strong>2. Decoder-Only 架构成为主流的原因</strong></p><p>近年来，我们看到像 GPT、Llama、PaLM 等顶级模型，都纷纷采用了 Decoder-only 架构，而不是像 T5、BART 那样的 Encoder-Decoder 架构。</p><ul><li><strong>原因一：任务的统一与架构的简洁</strong><ul><li><strong>Encoder-Decoder</strong> 架构天然为**“源-目标”**式的转换任务（如翻译、摘要）而生，它需要一个 Encoder 先完整理解源文本。</li><li><strong>Decoder-only</strong> 架构的本质是<strong>自回归 (Autoregressive)</strong>，即“根据上文预测下文”。事实证明，几乎所有的自然语言任务都可以被统一地、巧妙地转化为这种**“提示工程/文本补全 (Prompting/Completion)”**的范式。无论是问答、分类还是生成，都可以通过设计不同的 Prompt 来让模型“续写”出答案。这种“万物皆可续写”的模式，使得更简洁的 Decoder-only 架构足以应对绝大多数场景，工程上更具优势。</li></ul></li><li><strong>原因二：预训练任务的强大与通用</strong><ul><li>Decoder-only 模型的核心预训练任务极其简单且强大：<strong>“预测下一个词 (Next Token Prediction)”</strong>。</li><li>当这个简单的任务在一个接近“无限”的、高质量的互联网语料上进行训练时，模型为了降低预测的困惑度，被迫学习到了关于语法、事实、逻辑、风格等极其丰富的世界知识。</li><li>这个单一、通用的预训练目标，被证明是创造一个强大“通才”模型的极佳路径。</li></ul></li></ul><p><strong>3. 知识蒸馏技术（以 DistilBERT 为例）</strong></p><ul><li><strong>核心目的</strong>：进行<strong>模型压缩</strong>。将一个庞大、精确但笨重的“<strong>教师模型</strong>”（如 BERT），其所学到的知识，“蒸馏”到一个更小、更快的“<strong>学生模型</strong>”（如 DistilBERT）中，以方便实际部署。</li><li><strong>核心思想：学习“软标签 (Soft Labels)”</strong><ul><li><strong>硬标签</strong>：是数据集中非黑即白的标准答案，例如一张图片，标签是 <code>[0, 0, 1, 0]</code>，代表“猫”。</li><li><strong>软标签</strong>：是“教师模型”经过思考后，输出的带有不确定性的概率分布。例如，教师模型可能认为图片是 <code>[狗:0.05, 虎:0.04, 猫:0.90, 兔:0.01]</code>。</li><li><strong>蒸馏的精髓</strong>：学生模型不仅要学习硬标签（学会做对题），更要学习教师模型的软标签（<strong>学会老师的“解题思路”</strong>）。软标签中蕴含的类别间的相似性等知识，是硬标签无法提供给学生模型的宝贵信息。</li></ul></li><li><strong>DistilBERT 的损失函数</strong><br>为了让学生全方位地模仿老师，DistilBERT 在训练时会同时优化三个目标：<ol><li><strong>监督学习损失 (L_CE)</strong>：学生模型的预测结果与<strong>真实硬标签</strong>之间的损失。确保学生能做对题。</li><li><strong>蒸馏损失 (L_Distill)</strong>：学生模型的<strong>软标签</strong>与教师模型的<strong>软标签</strong>之间的差异（通常用 KL 散度衡量）。这是模仿老师“思路”的核心。</li><li><strong>特征对齐损失 (L_Cos)</strong>：鼓励学生模型的<strong>中间层隐状态向量</strong>与教师模型的隐状态向量在方向上保持一致（用余弦相似度衡量）。这是在模仿老师“思考的中间过程”。</li></ol></li></ul><p>通过这种方式，DistilBERT 在参数量减少 40%、速度提升 60% 的情况下，依然能保留 BERT 约 97% 的性能。</p><h3 id="3-LLM-训练中的显存占用分析"><strong>3. LLM 训练中的显存占用分析</strong></h3><p>GPU 显存是训练大模型最宝贵的资源。一份完整的“显存账单”主要包括以下四个部分：</p><ol><li><strong>模型参数 (Model Parameters)</strong><ul><li><strong>是什么</strong>：模型自身的权重（weights）和偏置（biases）。</li><li><strong>特点</strong>：这是“静态”成本，一旦模型架构和精度（如 FP16/BF16）确定，这部分大小就固定了。</li><li><strong>例子</strong>：一个 7B（70亿）参数的模型，用半精度（2字节/参数）存储，需要 <code>7B × 2 = 14 GB</code> 显存。</li></ul></li><li><strong>优化器状态 (Optimizer States)</strong><ul><li><strong>是什么</strong>：优化器为每个<strong>可训练参数</strong>存储的额外信息。</li><li><strong>特点</strong>：这是最容易被忽略的“隐藏”成本。对于 <strong>AdamW</strong> 优化器，它需要为每个参数存储<strong>一阶动量</strong>和<strong>二阶动量</strong>，这相当于将参数的显存需求<strong>乘以了 2</strong>。</li><li><strong>例子</strong>：对于上述 7B 模型进行全量微调，优化器状态就需要 <code>14 GB × 2 = 28 GB</code> 显存。</li><li><strong>启示</strong>：这也是为什么 LoRA/QLoRA 如此高效的原因之一，因为它们只训练极少数的适配器参数，所以优化器状态的显存开销也极小。</li></ul></li><li><strong>梯度 (Gradients)</strong><ul><li><strong>是什么</strong>：反向传播时为每个可训练参数计算出的梯度值。</li><li><strong>特点</strong>：其大小与可训练参数完全一致。例如，全量微调 7B 模型，梯度也需要 14GB 显存。</li></ul></li><li><strong>激活值 (Activations)</strong><ul><li><strong>是什么</strong>：模型在前向传播过程中，每一层网络计算产生的中间输出。这些输出需要被存储下来，以供反向传播时使用。</li><li><strong>特点</strong>：这是“动态”成本，其大小与<strong>批次大小 (Batch Size)</strong>、<strong>序列长度 (Sequence Length)</strong> 和<strong>模型层数</strong>等因素成正比。这是我们在显存不足时，最常用来“开刀”进行调整的部分。</li></ul></li></ol><p><strong>PyTorch 中多损失函数的 <code>backward</code> 方法</strong></p><ul><li><strong>场景</strong>：在多任务学习、知识蒸馏等场景中，我们常常需要同时优化多个损失函数。</li><li><strong>错误的做法</strong>：对每个 loss 单独调用 <code>loss.backward()</code>。<ul><li><strong>原因</strong>：PyTorch 在默认情况下，执行完一次 <code>.backward()</code> 后，为了释放内存，会立即销毁计算图。当第二次调用 <code>.backward()</code> 时，就会因找不到计算图而报错。</li></ul></li><li><strong>不推荐的做法</strong>：<code>loss1.backward(retain_graph=True)</code>，<code>loss2.backward()</code>。<ul><li><strong>原因</strong>：这会强制保留计算图，不仅消耗大量额外显存，而且对于简单的多任务场景，这种做法在梯度累加的逻辑上也是不正确的。</li></ul></li><li><strong>正确且标准的方法：先求和，再反传</strong><ul><li><p><strong>做法</strong>：将所有损失函数按照一定的权重（权重也可以是1）相加，形成一个最终的总损失，然后对这个总损失调用一次 <code>.backward()</code>。Python</p><p><code>total_loss = w1 * loss1 + w2 * loss2   total_loss.backward()</code></p></li><li><p><strong>原理</strong>：基于微积分的链式法则，梯度的加法等价于加法的梯度 <code>∇(L1+L2) = ∇L1 + ∇L2</code>。这种方法在数学上完全等价，且计算效率最高。</p></li></ul></li></ul><p><strong><code>model.eval()</code> 的作用及其对现代 LLM 的影响</strong></p><ul><li><strong><code>model.eval()</code> 的两大作用</strong>：<ol><li><strong>关闭 Dropout</strong>：将模型中所有的 Dropout 层的丢弃概率设为 0。这是为了在推理时使用模型的全部能力，并得到一个确定的输出。</li><li><strong>切换 BatchNorm (BN)</strong>：将模型中所有的 BN 层切换到评估模式。此时，BN 不再使用当前批次的均值和方差，而是使用在整个训练集上学习到的、固定的全局统计量。</li></ol></li><li><strong>对现代 LLM 的影响：基本无效</strong><ul><li>这是一个非常好的面试“陷阱”题。虽然 <code>model.eval()</code> 在 ResNet 等 CV 模型中至关重要，但对于当前主流的、基于 Transformer Decoder 的大语言模型（如 Llama, GPT系列），调用它<strong>几乎没有任何作用</strong>。</li><li><strong>原因</strong>：<ol><li>这些模型<strong>不使用 BatchNorm</strong>，而是使用 <strong>LayerNorm</strong>。而 LayerNorm 在训练和推理时的行为完全一样，不受 <code>eval()</code> 模式的影响。</li><li>这些模型在训练时，也<strong>普遍不使用或很少使用 Dropout</strong>。它们更依赖于海量数据和其他正则化手段。</li></ol></li><li><strong>结论</strong>：知道这一点，能体现出你对模型架构具体实现的深入了解。</li></ul></li></ul><p><strong>神经网络在特定领域（如风控）的挑战</strong></p><ul><li><strong>1. 可解释性 (Interpretability)</strong>：神经网络是一个“黑盒”，其决策过程难以向业务方或监管机构解释。而在风控领域，决策的“可解释性”是硬性要求。</li><li><strong>2. 对稀疏噪声数据的敏感性 (Sensitivity to Sparse &amp; Noisy Data)</strong>：风控数据通常是高维稀疏的，且可能包含噪声或异常点。神经网络强大的拟合能力有时反而会学到这些噪声，导致模型鲁棒性差。</li><li><strong>3. 难以融合专家规则 (Difficulty Integrating Expert Rules)</strong>：风控领域高度依赖专家经验（Feature Engineering）。相比于能清晰体现“IF-THEN”规则的树模型（如 GBDT），神经网络更像一个“大力出奇迹”的模型，难以将专家规则直观地融入其中。</li><li><strong>业界常用解决方案：GBDT + NN</strong>：结合 GBDT 强大的特征交叉与筛选能力，和神经网络强大的非线性拟合能力。先用 GBDT 对原始特征进行处理，生成一组新的、更有信息量的特征，再将这些新特征输入神经网络进行最终的预测。</li></ul>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/12/DL_1/</id>
    <link href="https://qyp9909.github.io/2025/08/12/DL_1/"/>
    <published>2025-08-12T03:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="深度学习小记">深度学习小记</h2>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月12日 17:41</p>
<h2 id="I-神经网络的核心组件与训练技巧"><strong>I. 神经网络的核心组件]]>
    </summary>
    <title>深度学习小记</title>
    <updated>2025-08-13T05:23:31.916Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h3 id="搜广推-指标">搜广推[指标]</h3><p>Created by: Yuanpeng QU<br>Created time: 2025年8月11日 16:14</p><h3 id="一、-开篇：从“线上线下不一致”谈起——我们为何需要正确的评估指标？"><strong>一、 开篇：从“线上线下不一致”谈起——我们为何需要正确的评估指标？</strong></h3><p>整个对话的起点，就是那个在工业界极其经典的问题：<strong>为什么我离线辛辛苦苦优化的模型，AUC涨了好几个点，但一上线，核心业务指标（如CTR、收入）就是不涨，甚至还会下跌？</strong></p><p>这个问题之所以重要，是因为它点明了一个残酷的现实：<strong>离线评估环境只是真实线上环境的一个不完美的“模拟器”</strong>。如果我们不理解这个模拟器（离线评估）的局限性，就会被虚高的指标迷惑，做出错误的判断。造成这种“线上线下不一致”的原因主要有三点：</p><ol><li><strong>特征不一致</strong>：由于数据处理延迟，线上实时服务用的特征可能是几个小时前的旧特征，而离线训练时却用了最新的特征。模型在线下学会了依赖“新特征”，但在线上根本用不上，效果自然会差。</li><li><strong>数据分布不一致</strong>：线上的数据是动态变化的，比如一个新上线的推荐模型，它遇到的用户和物品分布，和它在“旧模型”产生的数据集上训练时遇到的分布是完全不同的。</li><li><strong>评估目标不一致</strong>：这是最核心的一点。比如，我们离线用AUC来评估一个排序模型，但历史数据本身就带有<strong>位置偏见</strong>（排在前面的物品更容易被点击）。离线AUC高，可能只是因为模型学会了这种偏见，而不是真正学会了判断物品的质量。</li></ol><p><strong>这引出了我们为什么需要深入理解评估指标的根本原因：</strong></p><p>因为一个单一、粗糙的离线指标（比如不加思考就使用的AUC）是<strong>不足以</strong>反映模型在线上真实价值的。我们需要一整套<strong>正确且全面</strong>的评估指标体系，来从不同维度审视我们的模型，确保离线评估的结果能最大程度上**“预测”**线上的表现。</p><p>比如，为了解决“位置偏见”问题，我们需要引入对位置更敏感的<strong>NDCG</strong>；为了评估个性化推荐的效果，我们需要用<strong>GAUC</strong>来代替AUC；为了判断模型预测的pCTR分数是否可以直接用于广告竞价，我们还需要引入<strong>校准度</strong>指标。</p><p>因此，对评估指标的深入理解，是我们连接“模型技术”和“业务价值”的唯一桥梁。</p><h3 id="二、-核心分野：理解评估指标的两大家族——分类指标-vs-排序指标"><strong>二、 核心分野：理解评估指标的两大家族——分类指标 vs. 排序指标</strong></h3><p>在我们讨论的所有指标中，它们基本可以被归入两个大的家族，理解这个区别是后续一切讨论的基础。</p><p><strong>1. 分类指标 (Classification Metrics)</strong></p><ul><li><strong>回答的问题是</strong>：“这个样本属于A类还是B类？” 它是一个“<strong>是非题</strong>”。</li><li><strong>核心目标</strong>：判断模型做出的<strong>单个预测的准确性</strong>。</li><li><strong>典型场景</strong>：<ul><li>判断一封邮件是否是“垃圾邮件”。</li><li>判断一个用户是否会“点击”某个广告。</li><li>判断一张图片里的是“猫”还是“狗”。</li></ul></li><li><strong>代表指标</strong>：<strong>准确率(Accuracy)、精确率(Precision)、召回率(Recall)、F1分数</strong>。这些指标都围绕着一个<strong>固定的分类阈值</strong>（比如，预测概率 &gt; 0.5 就判为正类）下的混淆矩阵（TP, FP, TN, FN）来计算。</li></ul><p><strong>2. 排序指标 (Ranking Metrics)</strong></p><ul><li><strong>回答的问题是</strong>：“给定一批物品，应该按什么顺序呈现给用户才是最好的？” 它是一个“<strong>排序题</strong>”。</li><li><strong>核心目标</strong>：判断模型给出的一个<strong>有序列表的整体质量</strong>。</li><li><strong>典型场景</strong>：<ul><li>搜索引擎返回的结果列表。</li><li>信息流推荐的内容顺序。</li><li>电商网站的商品推荐列表。</li></ul></li><li><strong>代表指标</strong>：<strong>AUC、GAUC、NDCG、MRR、Hit Rate</strong>。这些指标通常不关心单一物品的预测分数究竟是多少，而更关心<strong>不同物品之间的相对顺序</strong>是否正确，尤其是列表头部的顺序。</li></ul><p><strong>两者的联系与区别</strong></p><p>在“搜广推”中，这两个家族紧密相连。我们通常会先训练一个<strong>分类模型</strong>（如CTR预估模型）来为每一个候选物品打一个分数（如pCTR），然后再利用这些分数来进行<strong>排序</strong>。</p><p>这就引出了像<strong>AUC</strong>这样处于“十字路口”的特殊指标：</p><ul><li>我们用它来评估一个<strong>分类模型</strong>。</li><li>但它的内在含义（一个正样本排在负样本前面的概率）却是在衡量这个模型分数所隐含的<strong>排序能力</strong>。</li></ul><p>理解了这个核心分野，我们就能明白为什么不能用“准确率”去评估一个推荐列表的好坏，也更能理解为什么AUC会在排序任务中扮演如此重要的角色。这是我们选择正确评估工具的第一步。</p><h3 id="三、-分类任务评估：走出“准确率陷阱”，拥抱P-R权衡（精确率、召回率、F1分数）"><strong>三、 分类任务评估：走出“准确率陷阱”，拥抱P-R权衡（精确率、召回率、F1分数）</strong></h3><p>在评估一个分类模型时，最朴素的想法就是：“模型预测对了多少？”。这引出了最直观的指标——准确率（Accuracy）。然而，在“搜广推”这种样本极不均衡的场景下，准确率是一个充满欺骗性的“陷阱”。为了做出正确的评估，我们必须理解并使用一套更严谨的指标：精确率（Precision）、召回率（Recall）以及它们的平衡 F1分数（F1-Score）。</p><h3 id="1-万物的基础：混淆矩阵-Confusion-Matrix">1. 万物的基础：混淆矩阵 (Confusion Matrix)</h3><p>在介绍所有指标前，我们必须先理解一个工具——<strong>混淆矩阵</strong>。它是一个表格，用来总结我们的模型预测结果与真实情况之间的关系。</p><table><thead><tr><th></th><th><strong>真实为正 (Actual Positive)</strong></th><th><strong>真实为负 (Actual Negative)</strong></th></tr></thead><tbody><tr><td><strong>预测为正 (Predicted Positive)</strong></td><td><strong>TP (True Positive)</strong> <br> 真正例</td><td><strong>FP (False Positive)</strong> <br> 假正例</td></tr><tr><td><strong>预测为负 (Predicted Negative)</strong></td><td><strong>FN (False Negative)</strong> <br> 假负例</td><td><strong>TN (True Negative)</strong> <br> 真负例</td></tr></tbody></table><p>导出到 Google 表格</p><ul><li><strong>TP (真正例)</strong>：模型预测为正，实际也为正。（预测会点击，用户真的点了）</li><li><strong>FP (假正例)</strong>：模型预测为正，实际却为负。（预测会点击，用户没点）—— <strong>Type I Error，误报</strong></li><li><strong>FN (假负例)</strong>：模型预测为负，实际却为正。（预测不点击，用户结果点了）—— <strong>Type II Error，漏报</strong></li><li><strong>TN (真负例)</strong>：模型预测为负，实际也为负。（预测不点击，用户真的没点）</li></ul><h3 id="2-“准确率陷阱”：为什么99-的准确率可能毫无价值？">2. “准确率陷阱”：为什么99%的准确率可能毫无价值？</h3><ul><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>A</mi><mi>c</mi><mi>c</mi><mi>u</mi><mi>r</mi><mi>a</mi><mi>c</mi><mi>y</mi><mo>=</mo><mfrac><mrow><mi>T</mi><mi>P</mi><mo>+</mo><mi>T</mi><mi>N</mi></mrow><mrow><mi>T</mi><mi>P</mi><mo>+</mo><mi>T</mi><mi>N</mi><mo>+</mo><mi>F</mi><mi>P</mi><mo>+</mo><mi>F</mi><mi>N</mi></mrow></mfrac><mo>=</mo><mfrac><mtext>所有预测正确的</mtext><mtext>总样本数</mtext></mfrac></mrow><annotation encoding="application/x-tex">Accuracy = \frac{TP + TN}{TP + TN + FP + FN} = \frac{所有预测正确的}{总样本数}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">A</span><span class="mord mathnormal">cc</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0359em;">cy</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1297em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">总样本数</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">所有预测正确的</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>意义</strong>：衡量模型在所有样本上（无论正负）预测正确的比例。</p></li><li><p><strong>陷阱举例（CTR预估）</strong>：<br>假设我们有1000个广告曝光样本，其中只有1个被用户点击（1个正样本），剩下999个都未被点击（999个负样本）。<br>现在有一个“懒惰”的模型，它不对任何数据进行学习，只是无脑地预测所有曝光都<strong>不会被点击</strong>。<br>我们用混淆矩阵来看它的表现：</p><ul><li>TP = 0 (没有预测任何点击)</li><li>FP = 0 (没有把“未点击”误报为“点击”)</li><li>FN = 1 (漏掉了那个唯一的真实点击)</li><li>TN = 999 (正确预测了所有未点击的样本)</li></ul><p>它的准确率是：<code>(0 + 999) / (0 + 0 + 1 + 999) = 999 / 1000 = 99.9%</code>。<br>这个模型有着近乎完美的准确率，但它对我们的业务（找到会点击的用户）<strong>没有任何帮助</strong>，因为它一个正样本都找不到。这就是“准确率陷阱”。</p></li></ul><h3 id="3-精确率-Precision-vs-召回率-Recall-：一枚硬币的两面">3. 精确率 (Precision) vs. 召回率 (Recall)：一枚硬币的两面</h3><p>为了走出陷阱，我们需要把目光聚焦于我们真正关心的“正样本”上。精确率和召回率就是从两个不同的角度来衡量模型“寻找正样本”的能力。</p><h3 id="精确率-Precision-查准率"><strong>精确率 (Precision) / 查准率</strong></h3><ul><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>P</mi><mi>r</mi><mi>e</mi><mi>c</mi><mi>i</mi><mi>s</mi><mi>i</mi><mi>o</mi><mi>n</mi><mo>=</mo><mfrac><mrow><mi>T</mi><mi>P</mi></mrow><mrow><mi>T</mi><mi>P</mi><mo>+</mo><mi>F</mi><mi>P</mi></mrow></mfrac><mo>=</mo><mfrac><mtext>真正是正例</mtext><mtext>所有被你预测为正例的</mtext></mfrac></mrow><annotation encoding="application/x-tex">Precision = \frac{TP}{TP+FP} = \frac{真正是正例}{所有被你预测为正例的}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">i</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1297em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">所有被你预测为正例的</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">真正是正例</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>意义</strong>：衡量的是**“你预测出来的结果有多准”**。在你所有认为是“正”的预测中，有多少是真的。</p></li><li><p><strong>业务场景举例</strong>：</p><ul><li><strong>发优惠券</strong>：你希望推送的优惠券，用户领了之后真的会去用。如果推了很多别人不用的（FP高），不仅浪费了营销预算，还会骚扰用户。此时，你需要高精确率。</li><li><strong>高风险股票推荐</strong>：你推荐给用户的“会上涨”的股票，最好真的能涨。推荐错了（FP高），用户会亏钱。此时，你需要高精确率。</li></ul></li><li><p><strong>一句话理解</strong>：<strong>宁缺毋滥</strong>。我推荐的一定要好，哪怕因此少推荐了一些。</p></li></ul><h3 id="召回率-Recall-查全率"><strong>召回率 (Recall) / 查全率</strong></h3><ul><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>R</mi><mi>e</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>l</mi><mo>=</mo><mfrac><mrow><mi>T</mi><mi>P</mi></mrow><mrow><mi>T</mi><mi>P</mi><mo>+</mo><mi>F</mi><mi>N</mi></mrow></mfrac><mo>=</mo><mfrac><mtext>被你找出的正例</mtext><mtext>所有真实存在的正例</mtext></mfrac></mrow><annotation encoding="application/x-tex">Recall = \frac{TP}{TP + FN} = \frac{被你找出的正例}{所有真实存在的正例}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1297em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">所有真实存在的正例</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">被你找出的正例</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>意义</strong>：衡量的是**“<code>所有该找出来的，你找到了多少</code>”**。<strong>在所有真实为“正”的样本中，你成功识别出了多少</strong>。</p></li><li><p><strong>业务场景举例</strong>：</p><ul><li><strong>诊断癌症</strong>：在所有真正的癌症患者中（FN的代价是生命），我们希望尽可能多地把他们都找出来，哪怕因此误诊了一些健康的人（FP高），让他们再去做一次检查。此时，你需要高召回率。</li><li><strong>推荐系统召回层</strong>：在海量的物品库中，我们希望把用户<strong>所有可能</strong>感兴趣的物品都找出来，送到排序模型那里去。漏掉一个（FN高），就永远失去了一次推荐这个物品的机会。此时，你需要高召回率。</li></ul></li><li><p><strong>一句话理解</strong>：<strong>宁可错杀，不可放过</strong>。我可能推荐了一些不那么好的，但一定要把所有好的都包含进来。</p></li></ul><h3 id="4-F1分数：在“准”与“全”之间寻求平衡">4. F1分数：在“准”与“全”之间寻求平衡</h3><p>我们发现，精确率和召回率往往是相互制约的。要提高召回率，就要放宽标准，这样精确率就可能下降；反之亦然。为了综合评价一个模型，我们需要一个能平衡这两者的指标——F1分数。</p><ul><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>F</mi><mn>1</mn><mo>=</mo><mn>2</mn><mo>×</mo><mfrac><mrow><mi>P</mi><mi>r</mi><mi>e</mi><mi>c</mi><mi>i</mi><mi>s</mi><mi>i</mi><mi>o</mi><mi>n</mi><mo>×</mo><mi>R</mi><mi>e</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>l</mi></mrow><mrow><mi>P</mi><mi>r</mi><mi>e</mi><mi>c</mi><mi>i</mi><mi>s</mi><mi>i</mi><mi>o</mi><mi>n</mi><mo>+</mo><mi>R</mi><mi>e</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>l</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">F1 = 2 \times \frac{Precision \times Recall}{Precision + Recall}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">2</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.1408em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">i</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">i</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>意义</strong>：F1分数是精确率和召回率的<strong>调和平均值</strong>。</p></li><li><p><strong>为什么用调和平均？</strong> 调和平均值的一个重要特性是，它会更偏向于两个数中较小的那个。如果Precision或Recall中有一个值极低，F1分数就会被严重拉低。这强迫我们必须让两个指标都达到一个较高的水平，从而实现真正的“平衡”。</p></li></ul><p><strong>总结</strong>：在分类任务中，我们不能仅仅满足于高“准确率”。必须深入到混淆矩阵，结合业务目标，去分析模型的<strong>精确率</strong>和<strong>召回率</strong>，并在两者之间做出最合适的权衡，而<strong>F1分数</strong>为我们提供了一个量化这种权衡的有效工具。如果用算数平均简单计算则会在极端的情况下出问题，比如100个真实病人和9900个健康人都被诊断患病。</p><h3 id="四、-排序质量评估（上）：全局排序的王者——AUC的深刻内涵与个性化升级（GAUC）"><strong>四、 排序质量评估（上）：全局排序的王者——AUC的深刻内涵与个性化升级（GAUC）</strong></h3><p>在排序任务中，我们需要的不仅仅是一个能判断“是与非”的分类器，更需要一个能排出“好与坏”顺序的裁判。AUC及其升级版GAUC，就是这个裁判手中的黄金标准。</p><h3 id="一-全局排序的王者-——-AUC-Area-Under-the-Curve">(一) 全局排序的王者 —— AUC (Area Under the Curve)</h3><p>AUC的全称是“ROC曲线下的面积”，但这个几何名称远不如它的概率意义来得深刻和实用。</p><p><strong>1. AUC的双重定义</strong></p><ul><li><strong>几何意义（它是什么样）</strong>：AUC是<strong>ROC曲线</strong>下方的面积。ROC曲线的横轴是<strong>假阳率(FPR)</strong>，纵轴是<strong>真阳率(TPR)</strong>，它描绘了模型在所有分类阈值下，TPR随FPR变化的轨迹。AUC的值在0.5到1之间，一个随机猜测的模型AUC为0.5，一个完美的模型AUC为1。</li><li><strong>概率意义（它代表什么）</strong>：这是理解AUC的精髓。<strong>AUC代表了“随机抽取一个正样本和一个负样本，你的模型将该正样本的预测分数排在负样本之上的概率有多大”</strong>。这个定义将一个看似复杂的指标，转化为了一个极其直观的“排序正确率”的概念。</li></ul><p><strong>2. 为什么说AUC是“王者”？—— 它的三大优势</strong></p><ol><li><strong>不依赖阈值 (Threshold-Independent)</strong>：无论是精确率、召回率还是F1分数，它们的计算都依赖于一个固定的分类阈值（比如<code>pCTR &gt; 0.5</code>）。而阈值的选择本身就是一个难题。AUC则评估了模型在<strong>所有可能阈值下</strong>的综合排序能力，衡量的是模型排序性能的“内功”，更具全局性和稳定性。</li><li><strong>对样本不均衡不敏感 (Insensitive to Class Imbalance)</strong>：这在“搜广推”场景下是压倒性的优势。我们之前已经看到，准确率在CTR预估这种正负样本比例1:1000的场景下会完全失效。而AUC因为其基于“样本对”的排序概率定义，其计算结果与正负样本的比例无关，能够非常稳定、客观地反映模型在倾斜数据上的表现。</li><li><strong>物理意义明确 (Intuitive Meaning)</strong>：“衡量模型将正样本排在负样本前面的概率”，这个解释非常直观，便于算法工程师之间以及与业务方进行沟通。</li></ol><p><strong>3. AUC如何计算？—— 排序与计数的艺术</strong></p><p>虽然可以通过绘制ROC曲线然后计算面积，但在实践中，我们更多地使用它等价的概率定义来计算，效率更高。</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>A</mi><mi>U</mi><mi>C</mi><mo>=</mo><mfrac><mrow><mtext>正样本预测概率 &gt; 负样本预测概率的次数</mtext><mo>+</mo><mn>0.5</mn><mo>×</mo><mtext>正负样本预测概率相等的次数</mtext></mrow><mrow><mi>m</mi><mo>×</mo><mi>n</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">AUC = \frac{\text{正样本预测概率 &gt; 负样本预测概率的次数} + 0.5 \times \text{正负样本预测概率相等的次数}}{m \times n}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.109em;">U</span><span class="mord mathnormal" style="margin-right:0.0715em;">C</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1297em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal">n</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord cjk_fallback">正样本预测概率</span><span class="mord"> &gt; </span><span class="mord cjk_fallback">负样本预测概率的次数</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">0.5</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord text"><span class="mord cjk_fallback">正负样本预测概率相等的次数</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p><strong>举例说明：</strong><br>假设有2个正样本(P1, P2)和2个负样本(N1, N2)，模型打分如下：</p><ul><li>P1: 0.8</li><li>P2: 0.6</li><li>N1: 0.7</li><li>N2: 0.3</li></ul><p>总共有 <code>2 * 2 = 4</code> 个正负样本对：</p><ol><li>(P1, N1): score(0.8) &gt; score(0.7) -&gt; <strong>排序正确</strong></li><li>(P1, N2): score(0.8) &gt; score(0.3) -&gt; <strong>排序正确</strong></li><li>(P2, N1): score(0.6) &lt; score(0.7) -&gt; <strong>排序错误</strong></li><li>(P2, N2): score(0.6) &gt; score(0.3) -&gt; <strong>排序正确</strong></li></ol><p>在4个样本对中，有3个排序正确。因此，AUC = 3 / 4 = <strong>0.75</strong>。</p><h3 id="二-个性化升级-——-GAUC-Grouped-AUC">(二) 个性化升级 —— GAUC (Grouped AUC)</h3><p>AUC虽然强大，但它有一个致命的缺陷：它衡量的是<strong>全局</strong>的排序能力。</p><p><strong>1. AUC的困境：张三的苹果和李四的橘子</strong></p><p>在推荐系统中，我们关心的是<strong>为每个用户提供个性化的、精准的排序</strong>。<br>假设一个场景：</p><ul><li>小明喜欢低分段的物品，模型给他推荐的物品分数都在0.1-0.2之间，且排序完美（<strong>小明的AUC=1.0</strong>）。</li><li>小红喜欢高分段的物品，模型给她推荐的物品分数都在0.8-0.9之间，且排序也完美（<strong>小红的AUC=1.0</strong>）。</li></ul><p>如果用普通的AUC计算，它会把小明喜欢的物品（比如0.15分）和小红不喜欢的物品（比如0.85分）进行比较，得出“排序错误”的结论，从而拉低整体AUC。但这种跨用户的比较是<strong>毫无意义</strong>的，我们并不关心模型是否把小明喜欢的物品排在小红不喜欢物品的前面。</p><p><strong>2. GAUC的解决方案：“先分组，再计算，后加权平均”</strong></p><p>GAUC完美地解决了这个问题。它的计算方法如下：</p><ol><li><strong>分组 (Group)</strong>：将所有样本按照“组”进行划分，在推荐场景下，这个“组”通常就是**<code>user_id</code>*。</li><li><strong>计算 (Calculate)</strong>：在<strong>每一个组（用户）内部</strong>，独立计算该用户的AUC。这样就避免了跨用户的比较。</li><li><strong>加权平均 (Weighted Average)</strong>：将所有用户的AUC值，按照一定的权重进行加权平均，得到最终的GAUC。最常用的权重是<strong>用户的曝光量（Impression）</strong>，即该用户在测试集中有多少样本。</li></ol><p><strong>GAUC公式:</strong></p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>G</mi><mi>A</mi><mi>U</mi><mi>C</mi><mo>=</mo><mfrac><mrow><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>N</mi></munderover><mi>A</mi><mi>U</mi><msub><mi>C</mi><mi>i</mi></msub><mo>×</mo><msub><mi>w</mi><mi>i</mi></msub></mrow><mrow><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>N</mi></munderover><msub><mi>w</mi><mi>i</mi></msub></mrow></mfrac></mrow><annotation encoding="application/x-tex">GAUC = \frac{\sum_{i=1}^{N} AUC_i \times w_i}{\sum_{i=1}^{N} w_i}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal">G</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.109em;">U</span><span class="mord mathnormal" style="margin-right:0.0715em;">C</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.8419em;vertical-align:-1.1709em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.6709em;"><span style="top:-2.1288em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:0em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9812em;"><span style="top:-2.4003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.109em;">N</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2997em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.6897em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:0em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9812em;"><span style="top:-2.4003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.109em;">N</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2997em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.109em;">U</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0715em;">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.0269em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.1709em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>其中, <code>N</code>是总用户数, <code>AUC_i</code>是第i个用户的AUC, <code>w_i</code>是第i个用户的权重（如曝光量）。</p><p><strong>结论：</strong> GAUC通过强制在用户内部进行评估，真正衡量了模型的<strong>个性化排序能力</strong>，是目前业界评估个性化推荐模型时，离线最重要的核心指标，没有之一。</p><h3 id="五、-排序质量评估（下）：面向用户的Top-K列表评估（NDCG、Recall-k、Hit-Rate）"><strong>五、 排序质量评估（下）：面向用户的Top-K列表评估（NDCG、Recall@k、Hit Rate）</strong></h3><h3 id="1-命中率-Hit-Rate-k-——-“我的推荐有用吗？”">1. 命中率 (Hit Rate @k) —— “我的推荐有用吗？”</h3><p>这是最简单、最直观的Top-K指标。</p><ul><li><p><strong>核心思想</strong>：它只回答一个“是”或“否”的问题：“在给用户推荐的K个物品中，是否<strong>至少有1个</strong>是他真正喜欢的？”</p></li><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>H</mi><mi>i</mi><mi>t</mi><mi>R</mi><mi>a</mi><mi>t</mi><mi>e</mi><mi mathvariant="normal">@</mi><mi>k</mi><mo>=</mo><mfrac><mrow><mtext>至少命中</mtext><mn>1</mn><mtext>个推荐的用户数</mtext></mrow><mtext>总用户数</mtext></mfrac></mrow><annotation encoding="application/x-tex">HitRate@k = \frac{至少命中1个推荐的用户数}{总用户数}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.0077em;">tR</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord">@</span><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">总用户数</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">至少命中</span><span class="mord">1</span><span class="mord cjk_fallback">个推荐的用户数</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p></li><li><p><strong>举例说明</strong>：<br>假设我们为3位用户都推荐了Top-5的商品 (<code>k=5</code>)。</p><ul><li><strong>小明</strong>：推荐了<code>[A, B, C, D, E]</code>，他实际购买了<code>[B, F]</code>。–&gt; 因为推荐的B被购买了，所以对小明算一次<strong>命中(Hit)</strong>。</li><li><strong>小红</strong>：推荐了<code>[G, H, I, J, K]</code>，她实际购买了<code>[L]</code>。–&gt; 因为推荐的商品她都没买，所以算一次<strong>未命中(Miss)</strong>。</li><li><strong>小李</strong>：推荐了<code>[M, N, O, P, Q]</code>，他实际购买了<code>[P, R, S]</code>。–&gt; 因为推荐的P被购买了，所以也算一次<strong>命中(Hit)</strong>。</li></ul><p>最终，<code>Hit Rate@5 = 2 / 3 ≈ 66.7%</code>。</p></li><li><p><strong>评价</strong>：</p><ul><li><strong>优点</strong>：非常容易理解和计算，便于向业务方沟通，能快速了解推荐的有效覆盖率。</li><li><strong>缺点</strong>：<strong>极其粗糙</strong>。它不关心命中了几个，也不关心命中的物品排在第几位。在上面的例子中，小明和小李都被算作一次“命中”，但我们无法区分是命中1个好，还是命中多个好，也无法区分是排在第1位命中好，还是第5位命中好。</li></ul></li></ul><h3 id="2-召回率-Recall-k-——-“用户喜欢的，我找回了多少？”">2. 召回率 (Recall @k) —— “用户喜欢的，我找回了多少？”</h3><p>这个指标比命中率更进了一步，它开始关心“命中数量”的比例。</p><ul><li><p><strong>核心思想</strong>：对于一个用户，在他所有真正喜欢的物品中，我们推荐的Top-K列表成功“覆盖”了多大的比例？</p></li><li><p><strong>如何计算</strong>：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>R</mi><mi>e</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi mathvariant="normal">@</mi><mi>k</mi><mo stretchy="false">(</mo><mtext>单用户</mtext><mo stretchy="false">)</mo><mo>=</mo><mfrac><mrow><mi>T</mi><mi>o</mi><mi>p</mi><mi>K</mi><mtext>推荐中命中用户喜好的物品数量</mtext></mrow><mtext>该用户所有喜好的物品总数</mtext></mfrac></mrow><annotation encoding="application/x-tex">Recall@k(单用户) = \frac{TopK推荐中命中用户喜好的物品数量}{该用户所有喜好的物品总数}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord">@</span><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="mopen">(</span><span class="mord cjk_fallback">单用户</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.0463em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord cjk_fallback">该用户所有喜好的物品总数</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.0715em;">K</span><span class="mord cjk_fallback">推荐中命中用户喜好的物品数量</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><p>最终的指标是所有用户的<code>Recall@k</code>的平均值，即 <strong>Mean Recall@k</strong>。</p></li><li><p><strong>举例说明</strong>（沿用上面的例子）：</p><ul><li><strong>小明</strong>：推荐命中1个(<code>B</code>)，但他总共喜欢2个(<code>B, F</code>)。<code>Recall@5 = 1 / 2 = 0.5</code>。</li><li><strong>小红</strong>：推荐命中0个，她总共喜欢1个(<code>L</code>)。<code>Recall@5 = 0 / 1 = 0.0</code>。</li><li><strong>小李</strong>：推荐命中1个(<code>P</code>)，他总共喜欢3个(<code>P, R, S</code>)。<code>Recall@5 = 1 / 3 ≈ 0.33</code>。</li></ul><p>最终，<code>Mean Recall@5 = (0.5 + 0.0 + 0.33) / 3 ≈ 0.277</code>。</p></li><li><p><strong>评价</strong>：</p><ul><li><strong>优点</strong>：相比Hit Rate，它考虑了命中数量的比例，更能反映推荐的“查全”能力。</li><li><strong>缺点</strong>：它依然<strong>不关心位置</strong>。排在第1位命中和排在第k位命中，对它的贡献是完全一样的。</li></ul></li></ul><h3 id="3-NDCG-k-归一化折损累计增益-——-“排得好不好？”">3. NDCG @k (归一化折损累计增益) —— “排得好不好？”</h3><p>这是Top-K评估指标中的“王者”，因为它最全面，既考虑了命中的数量，也考虑了命中的位置，甚至能考虑命中的“程度”。</p><ul><li><p><strong>核心思想</strong>：</p><ol><li><strong>相关性（Gain）</strong>：越相关的物品，得分越高。</li><li><strong>位置折损（Discounted）</strong>：排在越前面的物品，其价值越高；排在越后面，其价值就要打折扣。</li><li><strong>归一化（Normalized）</strong>：让不同用户之间的得分可以公平比较。</li></ol></li><li><p><strong>如何计算</strong>（我们在上一个问题中详细演算过，这里回顾其精髓）：</p><ol><li><strong>计算DCG</strong>：对推荐列表中的每个物品，根据其<strong>真实相关性<code>rel_i</code>和位置<code>i</code></strong>，计算一个带折扣的得分<code>rel_i / log₂(i+1)</code>，然后全部加起来。</li><li><strong>计算IDCG</strong>：计算理论上<strong>最完美的推荐列表</strong>能拿到的DCG分数。</li><li><strong>计算NDCG</strong>：<code>NDCG@k=DCG@k / IDCG@k</code></li></ol></li><li><p><strong>举例说明</strong>：</p><ul><li><strong>场景A</strong>：Top-5推荐列表在第1位命中了用户最喜欢的物品（相关性=3分）。</li><li><strong>场景B</strong>：Top-5推荐列表在第5位命中了同一个物品（相关性=3分）。</li></ul><p>对于场景A，它的DCG贡献是<code>3 / log₂(1+1) = 3.0</code>。<br>对于场景B，它的DCG贡献是<code>3 / log₂(5+1) ≈ 1.16</code>。<br>显而易见，NDCG严厉地惩罚了场景B这种把好东西排在后面的行为。</p><p>一个例子：推荐系统经过计算，给小红的推荐列表（前5名）是这样的：</p><p>第1步：计算模型的DCG: 我们来为系统推荐的列表计算<code>DCG@5</code>：</p><table><thead><tr><th>位置 (i)</th><th>推荐电影</th><th>真实喜好度 (rel_i)</th><th>折扣 <code>log₂(i+1)</code></th><th>折扣后增益 <code>rel_i / log₂(i+1)</code></th></tr></thead><tbody><tr><td>1</td><td>《电影B》</td><td>2</td><td><code>log₂(1+1) = 1</code></td><td>2 / 1 = 2.0</td></tr><tr><td>2</td><td>《电影A》</td><td>3</td><td><code>log₂(2+1) ≈ 1.585</code></td><td>3 / 1.585 ≈ 1.893</td></tr><tr><td>3</td><td>《电影E》</td><td>1</td><td><code>log₂(3+1) = 2</code></td><td>1 / 2 = 0.5</td></tr><tr><td>4</td><td>《电影C》</td><td>3</td><td><code>log₂(4+1) ≈ 2.322</code></td><td>3 / 2.322 ≈ 1.292</td></tr><tr><td>5</td><td>《电影D》</td><td>0</td><td><code>log₂(5+1) ≈ 2.585</code></td><td>0 / 2.585 = 0.0</td></tr></tbody></table><p><strong>DCG@5</strong> = 2.0 + 1.893 + 0.5 + 1.292 + 0.0 = <strong>5.685</strong></p><p>第2步：计算理想的IDCG (Ideal DCG)</p><p>IDCG代表的是<strong>理论上最完美的推荐列表</strong>能拿到的DCG分数。完美的列表，就是把所有电影按照真实喜好度从高到低排序。我们用同样的方法计算这个“完美列表”的<code>IDCG@5</code>：</p><table><thead><tr><th>位置 (i)</th><th>完美推荐</th><th>真实喜好度 (rel_i)</th><th>折扣 <code>log₂(i+1)</code></th><th>折扣后增益 <code>rel_i / log₂(i+1)</code></th></tr></thead><tbody><tr><td>1</td><td>《电影A》</td><td>3</td><td><code>log₂(1+1) = 1</code></td><td>3 / 1 = 3.0</td></tr><tr><td>2</td><td>《电影C》</td><td>3</td><td><code>log₂(2+1) ≈ 1.585</code></td><td>3 / 1.585 ≈ 1.893</td></tr><tr><td>3</td><td>《电影B》</td><td>2</td><td><code>log₂(3+1) = 2</code></td><td>2 / 2 = 1.0</td></tr><tr><td>4</td><td>《电影E》</td><td>1</td><td><code>log₂(4+1) ≈ 2.322</code></td><td>1 / 2.322 ≈ 0.431</td></tr><tr><td>5</td><td>《电影D》</td><td>0</td><td><code>log₂(5+1) ≈ 2.585</code></td><td>0 / 2.585 = 0.0</td></tr></tbody></table><p><strong>IDCG@5</strong> = 3.0 + 1.893 + 1.0 + 0.431 + 0.0 = <strong>6.324</strong></p><p>第3步：计算最终的NDCG (Normalized DCG)</p><p><strong>NDCG@5</strong> = 5.685 / 6.324 ≈ <strong>0.899</strong></p></li><li><p><strong>评价</strong>：</p><ul><li><strong>优点</strong>：<strong>最全面、最科学</strong>的Top-K排序评估指标。它对位置敏感，且能处理多级的相关性（比如：购买 &gt; 点击 &gt; 曝光），非常适合评估搜索和推荐这种对排序极其敏感的系统。</li><li><strong>缺点</strong>：计算相对复杂，且需要定义一个明确的相关性等级。</li></ul></li></ul><p><strong>总结</strong></p><p>这三个指标形成了一个评估的“进阶三部曲”：</p><ul><li><strong>Hit Rate</strong>：最基础，看“有没有”。</li><li><strong>Recall@k</strong>：进一步，看“有多少”。</li><li><strong>NDCG@k</strong>：最全面，看“有多少”并且“排得好不好”。</li></ul><h3 id="六、-进阶辨析：如何在ROC-AUC与PR曲线-F1分数之间做出正确选择？"><strong>六、 进阶辨析：如何在ROC/AUC与PR曲线/F1分数之间做出正确选择？</strong></h3><p>在机器学习的工具箱里，并没有一个“万能”的评估指标。选择使用AUC还是F1（以及它们各自的曲线），不是一个“谁更高级”的问题，而是一个“谁更合适”的问题。这个选择主要取决于两个核心维度：<strong>1. 你的数据集样本是否均衡？</strong> <strong>2. 你的业务目标和最终应用场景是什么？</strong></p><h3 id="辨析一：样本不均衡问题-——-ROC曲线-vs-PR曲线">辨析一：样本不均衡问题 —— ROC曲线 vs. PR曲线</h3><p>这是区分使用这两条曲线最关键的第一个考量点。</p><ul><li><strong>当你的数据集相对均衡时：ROC曲线/AUC是稳定且优秀的选择。</strong><ul><li>在这种情况下，假阳率（FPR）能够真实地反映模型将负样本错判为正样本的比例，ROC曲线能清晰地展示出模型在“找到多少真病人(TPR)”和“误诊多少健康人(FPR)”之间的权衡，AUC给出的也是一个非常稳定可靠的综合评分。</li></ul></li><li><strong>当你的数据集高度不均衡时：PR曲线是更佳的诊断工具。</strong><ul><li>我们之前已经详细分析过，在正负样本比例悬殊（如1:1000）的情况下，ROC曲线会因为其横轴FPR对大量的真负例(TN)不敏感，而显得**“过于乐观”**，无法有效暴露模型的问题。</li><li>PR曲线的两个坐标轴（Precision和Recall）都聚焦于正样本的表现，对假正例(FP)的变化极为敏感。它能更“诚实”、更“严苛”地反映出模型在“大海捞针”式的任务中，其性能到底好不好。</li></ul></li></ul><p><strong>决策依据</strong>：拿到一个分类问题，你首先要问自己：“我的数据是否存在严重的类别不平衡？” 如果答案是肯定的，那么在你的评估报告中，<strong>PR曲线和它下方的面积（AUPRC）必须占有一席之地</strong>，因为它能告诉你ROC曲线可能隐藏起来的真相。</p><h3 id="辨析二：业务目标与应用-——-AUC-vs-F1分数">辨析二：业务目标与应用 —— AUC vs. F1分数</h3><p>这是第二个考量点，它关注的是“模型评估服务于什么目的”。</p><ul><li><strong>当你的目标是评估模型的“整体排序能力”或“内在潜力”时：选择AUC。</strong><ul><li>如果你想在离线阶段比较两个不同算法或特征工程的优劣，而<strong>不关心最终线上使用的具体阈值</strong>，AUC是最好的选择。</li><li>AUC衡量的是模型“是骡子是马”的根本能力，即它区分好坏样本的排序内功有多深厚。在CTR预估这类典型的排序任务中，AUC是核心的离线评估指标，因为它直接反映了模型为后续排序提供的分数质量。</li></ul></li><li><strong>当你的目标是评估模型在“某个固定决策点”上的表现时：选择F1分数。</strong><ul><li>如果你的模型最终上线后，需要根据一个<strong>固定的、明确的阈值</strong>来做出“是”或“否”的判断，那么F1分数是更具指导意义的指标。</li><li>F1分数直接告诉你，在这个阈值下，模型在“查准”和“查全”这两个具体任务上的平衡做得怎么样。</li><li><strong>典型例子</strong>：<ul><li><strong>垃圾邮件过滤</strong>：系统必须做出一个最终决定：这封邮件是“垃圾邮件”（放入垃圾箱）还是“正常邮件”（放入收件箱）。我们需要在某个阈值下评估这个决定的质量，此时F1分数（或者直接看P和R）更有意义。</li><li><strong>欺诈检测</strong>：风控系统需要决定是否“拦截”一笔交易。这个“拦截”与否的决定，就是在某个阈值下做出的，其产生的业务成本（错误拦截FP vs. 漏掉欺诈FN）非常直观，适合用F1分数来度量。</li></ul></li></ul></li></ul><h3 id="总结与决策流程">总结与决策流程</h3><p>我们可以将选择过程总结为一个简单的决策流程：</p><ol><li><strong>第一问：我的数据是否严重不平衡？</strong><ul><li><strong>是</strong> -&gt; 必须重点关注 <strong>PR曲线</strong>，用它来诊断模型在少数类上的真实表现。</li><li><strong>否</strong> -&gt; <strong>ROC曲线/AUC</strong> 是一个非常可靠的起点。</li></ul></li><li><strong>第二问：我的业务最终是需要一个排序列表，还是一个确定的分类决策？</strong><ul><li><strong>需要排序列表</strong>（如推荐、搜索） -&gt; <strong>AUC/GAUC</strong> 是核心离线指标，因为它衡量排序质量。</li><li><strong>需要确定决策</strong>（如是否拦截交易） -&gt; <strong>F1分数/精确率/召回率</strong> 是核心离线指标，因为它衡量在特定决策点上的性能。</li></ul></li></ol><p>在成熟的工业实践中，我们从不会只看一个指标。一个优秀的算法工程师会给出一份全面的评估报告：“我们的新模型，在GAUC上提升了2个百分点，证明其个性化排序的整体能力更强。同时，从PR曲线上看，它在高召回率区间的精确率有明显改善，这意味着我们能在覆盖更多用户兴趣的同时保证更高的准确性。在最终部署的阈值下，F1分数也提升了3%，这将直接转化为线上业务的收益。”</p><h3 id="七、-终极话题：当模型“说谎”时——超越排序，评估预测的“可信度”（校准度与LogLoss）"><strong>七、 终极话题：当模型“说谎”时——超越排序，评估预测的“可信度”（校准度与LogLoss）</strong></h3><p><strong>模型的“谎言”是什么？</strong><br>就是它预测出的概率分数，与其代表的真实世界中的概率不符。我们之前讨论过，AUC无法区分模型预测的0.95和0.81，只要它们都排在0.8前面即可。但0.95和0.81这两个分数本身，可能都是“谎言”。</p><p>一个模型可能对某个广告预测的pCTR是0.8（即80%），这是一个极高的分数，能让它在排序中胜出。但实际上，类似得到0.8分的所有广告，它们真实的平均点击率可能只有10%。这就意味着，模型虽然排对了序，但它对自己预测的“自信程度”撒了谎，它严重地**过分自信（Over-confident）**了。</p><p>评估和修正这种“谎言”的能力，就是**校准度（Calibration）**分析。</p><p><strong>1. 为什么我们需要“可信的”预测分数？</strong></p><p>在很多场景下，我们需要的不仅仅是一个相对的排序，更是那个<strong>绝对的、可信的概率值</strong>，因为它直接指导着真金白银的决策。</p><ul><li><strong>场景一：计算广告中的智能出价 (oCPC/oCPM)</strong><br>这是最经典的场景。广告平台需要预估一个广告的点击率<code>pCTR</code>，然后乘以广告主的出价<code>bid</code>，得到<code>eCPM (expected Cost Per Mille)</code>，并以此来进行排序和竞价。<ul><li>如果你的模型<strong>高估</strong>了pCTR（比如预测8%，实际只有4%），平台就会错误地提高出价，导致广告主成本虚高，ROI下降，最终广告主可能会停止投放。</li><li>如果你的模型<strong>低估</strong>了pCTR，平台就会出价过低，导致广告失去曝光机会，媒体收入和广告主业务量都受损。</li><li>在这个场景下，一个“诚实”的pCTR值是整个商业模式的基石。</li></ul></li><li><strong>场景二：业务预测与规划</strong><br>一个电商平台可能会利用模型预测的转化率，来预估整个平台下一季度的总销售额。如果模型的预测普遍偏高或偏低，那么整个公司的战略规划和资源分配都会基于一个错误的数据，后果不堪设想。</li></ul><p><strong>2. 识别“谎言”的工具箱：校准度评估指标</strong></p><p>为了判断模型是否“诚实”，我们需要一套超越AUC的工具。</p><ul><li><strong>工具一：对数损失 (LogLoss) —— “过分自信”的惩罚者</strong><br>我们之前提到，LogLoss不仅是优秀的损失函数，也是优秀的评估指标。因为它对“过分自信的错误”惩罚极重。一个模型如果对一个负样本给出了0.99的超高分，它的LogLoss会趋近于无穷大。因此，在两个AUC相近的模型中，<strong>LogLoss更低的那个，通常校准得更好</strong>，其预测的概率值更接近真实分布。</li><li><strong>工具二：校准曲线 (Calibration Curve) —— 可视化的“测谎仪”</strong><br>这是一个极其直观的可视化工具。<ul><li><strong>做法</strong>：我们将所有预测分数分桶（如0-0.1, 0.1-0.2…），然后计算每个桶内样本的“平均预测概率”（X轴）和“真实正样本比例”（Y轴），然后将这些点连成线。</li><li><strong>解读</strong>：<ul><li><strong>诚实的模型</strong>：校准曲线会紧贴着 <code>y=x</code> 的对角线。</li><li><strong>过分自信的模型</strong>：曲线会在对角线下方，形成一个“U”型或“S”型。这意味着，比如当模型预测平均概率为80%时，真实概率可能只有60%。<br>通过观察校准曲线的形状，我们可以清晰地诊断出模型是在哪些分数段“撒了谎”，以及“谎言”的模式是怎样的。</li></ul></li></ul></li><li><strong>工具三：期望校准误差 (ECE) —— 校准度的“量化总分”</strong><br>ECE（Expected Calibration Error）是将校准曲线的偏离程度量化成一个单一数值的指标。它计算的是每个桶的“预测概率”和“真实概率”之差的加权平均值。<strong>ECE越接近0，说明模型的校准度越好</strong>。</li></ul><p><strong>3. 如何“修正”一个说谎的模型？</strong></p><p>当我们发现模型的校准度很差时，除了调整模型结构和特征，还可以使用一些“事后”的校准技术，最常用的有：</p><ul><li><strong>普拉特缩放 (Platt Scaling)</strong>：用一个简单的逻辑回归模型来拟合原模型的输出。</li><li><strong>保序回归 (Isotonic Regression)</strong>：一种更强大的非参数方法，可以修正更复杂的校准偏差。</li></ul><p><strong>总结</strong></p><p>从评估排序（AUC/NDCG）到评估校准度（LogLoss/ECE），体现了我们对模型评估的认知深化。</p><ul><li><strong>AUC/NDCG</strong> 保证了模型能**“做对事”**（把好的排在前面）。</li><li><strong>校准度</strong> 则保证了模型能**“说实话”**（预测的概率是可信的）。</li></ul><h3 id="八、-总结：从技术指标到业务价值——如何选择最合适的指标驱动增长？"><strong>八、 总结：从技术指标到业务价值——如何选择最合适的指标驱动增长？</strong></h3><p><strong>核心原则：没有“最好”的指标，只有“最合适”的指标</strong></p><p>这是我们所有讨论的基石。在“搜广推”的复杂世界里，试图寻找一个能包打天下的“万能指标”是徒劳的。一个指标的价值，完全取决于它能在多大程度上<strong>反映特定阶段的特定业务目标</strong>。</p><p>因此，选择指标的过程，本质上是一个<strong>将业务问题翻译成数学语言</strong>的过程。</p><p><strong>实践框架：分阶段、多维度地构建指标体系</strong></p><p>一个成熟的推荐或广告系统，其评估体系也应该是分阶段、多维度的，就像一个精密的仪表盘，而不是只有一个速度表。</p><p><strong>阶段一：召回层 (Matching/Recall Stage)</strong></p><ul><li><strong>业务目标</strong>：“广而全”，确保不漏掉任何潜在的机会。我们需要从海量的物料库中，快速、宽泛地筛选出一个包含所有用户可能感兴趣物品的候选集。</li><li><strong>核心指标</strong>：<strong>Recall@k</strong> (例如 Recall@200)。我们最关心的是，在返回给排序模型的这几百个候选物料中，是否成功地覆盖了用户最终会喜欢的那些？<strong>查全率</strong>是这一阶段的生命线。</li></ul><p><strong>阶段二：排序层 (Ranking/Scoring Stage)</strong></p><ul><li><strong>业务目标</strong>：“精而准”，对召回层给出的候选集进行精准打分，为最终的排序提供高质量的依据。这是模型算法的核心价值体现。</li><li><strong>核心离线指标组合</strong>：<ol><li><strong>GAUC (分组AUC)</strong>：<strong>绝对的核心</strong>。它衡量的是模型对每个用户的<strong>个性化排序能力</strong>。GAUC的持续提升，是模型排序“内功”增强的最直接体现。</li><li><strong>LogLoss / 校准度指标 (ECE等)</strong>：当分数直接用于<strong>商业决策</strong>（如广告竞价）时，该指标与GAUC<strong>同等重要</strong>。它衡量模型预测的“可信度”，确保我们的出价和预算是基于“诚实”的概率。</li><li><strong>NDCG@k</strong>：可以作为辅助指标，提前观察模型在最终列表呈现上的表现，特别是对头部位置的排序能力。</li></ol></li></ul><p><strong>阶段三：最终呈现与用户体验 (Re-ranking &amp; Display)</strong></p><ul><li><strong>业务目标</strong>：确保用户最终看到的Top-K列表具有最佳的体验，不仅要精准，还要考虑多样性、新颖性等。</li><li><strong>核心离线指标组合</strong>：<ol><li><strong>NDCG@k</strong> (例如 NDCG@10)：<strong>核心中的核心</strong>。它最全面地评估了最终呈现给用户的短列表的综合质量，既考虑了物品的相关性，也考虑了位置。</li><li><strong>Recall@k / Precision@k / Hit Rate@k</strong>：作为辅助和诊断指标。Hit Rate简单直观，便于沟通；Precision@k关注推荐的准确性；Recall@k关注对用户兴趣的覆盖度。</li></ol></li></ul><p><strong>终极裁判：线上 A/B 测试</strong></p><p>我们必须清醒地认识到，<strong>所有离线指标都只是线上效果的“代理指标（Proxy）”</strong>。离线指标涨得再好，也只是“模拟考”成绩优异，最终能否被采纳，必须由线上的A/B测试这场“高考”来决定。</p><p>在线上A/B测试中，我们关注的指标会更加直接和业务化：</p><ul><li><strong>短期业务指标</strong>：**CTR、CVR、GMV（成交总额）、人均停留时长、eCPM（广告千次展示收入）**等。这些指标直接关系到公司的收入和核心KPI。</li><li><strong>长期健康度指标</strong>：<strong>用户留存率、DAU（日活跃用户）、推荐多样性、内容消耗分布</strong>等。这些指标衡量的是我们是否以牺牲长期用户体验为代价来换取短期指标的增长。例如，一个只推荐美女图片的模型，短期CTR可能会暴涨，但长期来看会导致用户审美疲劳和流失。</li></ul><p><strong>最终结论</strong></p><p>从技术到价值的路径，就是建立一个<strong>从离线到线上、从单一到多维、从短期到长期</strong>的立体化评估体系。一位优秀的算法工程师，他的工作流程应该是：</p><ol><li><strong>理解业务</strong>，明确当前阶段要解决的核心问题。</li><li><strong>选择</strong>与之对应的、以GAUC/NDCG等为核心的<strong>离线指标矩阵</strong>。</li><li>通过算法和策略的迭代，驱动<strong>离线指标</strong>的提升。</li><li>最终通过严谨的<strong>线上A/B测试</strong>来验证模型是否真正带来了<strong>业务价值</strong>的增长。</li></ol>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/11/Rec_AD_Metrics/</id>
    <link href="https://qyp9909.github.io/2025/08/11/Rec_AD_Metrics/"/>
    <published>2025-08-11T06:00:00.000Z</published>
    <summary>
      <![CDATA[<h3 id="搜广推-指标">搜广推[指标]</h3>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月11日 16:14</p>
<h3 id="一、-开篇：从“线上线下不一致”谈起——我们为何需要正确的评估指标？"><]]>
    </summary>
    <title>搜广推笔记 指标</title>
    <updated>2025-08-11T08:18:18.894Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h3 id="搜广推-多目标排序">搜广推[多目标排序]</h3><p>Created by: Yuanpeng QU<br>Created time: 2025年8月10日 21:12</p><h3 id="第一部分：多目标学习的核心挑战与基础"><strong>第一部分：多目标学习的核心挑战与基础</strong></h3><h3 id="1-动机：为何需要多目标学习？"><strong>1. 动机：为何需要多目标学习？</strong></h3><p>现代工业级推荐系统（如电商、短视频）的目标是综合性的，不能只关注单一指标 。例如，一个短视频推荐系统不仅要优化点击率，可能还要同时提升用户的点赞、关注、转发、评论率以及观看时长 。</p><p>如果为每个任务单独建模和优化，或者用一个简单的模型直接预测所有任务，就会遇到**<code>“跷跷板效应” (Seesaw Phenomenon)</code>** 。</p><ul><li><strong>定义</strong>：<strong>指的是当模型在优化一个目标时，会导致另一个或多个其他目标的性能下降的现象</strong> 。</li><li><strong>举例</strong>：<ul><li><strong>任务跷跷板 (Task Seesaw)</strong>：一个模型如果过度优化点击率（CTR），可能会倾向于推荐标题党或封面吸引人的内容，但这部分内容的用户实际满意度（如观看时长、点赞率）可能很低，从而损害了长期用户体验 。</li><li><strong>领域跷跷板 (Domain Seesaw)</strong>：在多场景（多领域）推荐中，由于不同场景的用户群体和行为分布不同，一个模型在优化A场景（如“发现页”）的性能时，可能会损害其在B场景（如“精选页”）的推荐效果 。</li></ul></li><li><strong>结论</strong>：单任务优化的局限性在于它忽略了任务间的关联和冲突。因此，必须采用多目标学习（Multi-Task Learning），通过设计更精巧的模型来平衡不同目标，在多个维度上共同提升，实现全局最优。</li></ul><h3 id="2-核心痛点一：样本选择偏差-Sample-Selection-Bias-SSB"><strong>2. 核心痛点一：样本选择偏差 (Sample Selection Bias, SSB)</strong></h3><p>SSB问题是多目标学习，特别是涉及用户序贯行为（sequential behaviors）时，最经典也最核心的挑战之一。</p><ul><li><strong>定义</strong>：指模型在训练时所用的数据集分布，与在真实环境中进行预测时所面对的数据集分布不一致，从而导致模型产生预测偏差。</li><li><strong>以CVR（点击后转化率）预估为例</strong>：<ol><li><strong>训练空间 (Training Space)</strong>：CVR的定义是“用户在<strong>点击</strong>商品之后发生转化的概率”，即 <code>P(conversion | click=1)</code>。因此，传统的CVR模型，其训练样本天然就是<strong>所有被点击过的商品</strong>。其中，点击后转化的为正样本，点击后未转化的为负样本。</li><li><strong>预测空间 (Inference Space)</strong>：在实际线上服务时，系统需要对<strong>所有被曝光的商品</strong>进行打分，以便用 <code>pCTR × pCVR</code> 这样的公式来预估其最终价值并排序。此时，模型需要预测的是所有曝光商品的CVR，无论它们是否被点击。</li><li><strong>偏差的产生</strong>：模型只在“点击”这个有偏的样本空间上学习了如何区分转化与否，但却被要求在“曝光”这个全量的、无偏的样本空间上做预测。模型没有见过那些“未被点击”的样本，无法准确判断它们的转化潜力，因此做出的预测是有偏的、不准确的。</li></ol></li><li><strong>结论</strong>：SSB问题会导致模型对物品的价值预估不准，是序贯多目标任务中必须解决的基础性难题。这也是ESMM这类模型被提出的直接原因。</li></ul><h3 id="3-核心痛点二：任务间冲突与负迁移-Negative-Transfer"><strong>3. 核心痛点二：任务间冲突与负迁移 (Negative Transfer)</strong></h3><p>在多任务学习中，我们期望不同任务能互相借鉴，共同进步（正迁移）。但实际情况中，常常会因为任务间的冲突导致互相“拖后腿”，即<strong>负迁移</strong>。</p><ul><li><strong>根源：梯度冲突 (Gradient Conflict)</strong><ul><li>在拥有共享参数（如Shared-Bottom的底层网络或MMoE的共享专家）的模型中，不同任务为了最小化各自的损失函数，<strong>可能会对同一个共享参数产生方向相反的更新需求</strong>。</li><li><strong>举例</strong>：假设任务A（如点击率）的优化需要增大某个参数，而任务B（如观看时长）的优化需要减小同一个参数。在更新时，这个共享参数就会陷入“拉锯战”，最终的更新结果可能对两个任务都不是最优的，从而导致性能受损。</li></ul></li><li><strong>“双重跷跷板效应” (Imperfectly Double Seesaw Phenomenon)</strong><ul><li>这是快手在PEPNet论文中提出的一个更贴近工业界真实场景的概念，它是两种“跷跷板效应”的叠加 。</li><li><strong>任务跷跷板 (Task Seesaw)</strong>：指前述的不同任务之间的冲突，尤其是在任务目标稀疏度不同时，模型很容易偏向于拟合更简单的任务，而忽略稀疏任务 。</li><li><strong>领域跷跷板 (Domain Seesaw)</strong>：指在不同推荐场景（领域）下，由于数据分布、用户意图存在差异，导致模型无法同时在所有领域都取得好效果的现象 。</li><li>当这两种冲突同时存在时，问题就变得更加复杂，形成了“双重跷跷板效应” 。这也是催生出MMoE、PLE、PEPNet等更先进模型以及PCGrad等优化算法的直接原因。</li></ul></li></ul><h3 id="4-核心痛点三：稀疏任务的“梯度消失”问题"><strong>4. 核心痛点三：稀疏任务的“梯度消失”问题</strong></h3><p>这是一个在计算广告和推荐系统中非常具体且常见的问题，尤其是在CTR（点击率）预估这类正样本极其稀疏的任务中。</p><ul><li><strong>问题描述：以CTR任务为例</strong><ul><li>CTR任务通常使用LogLoss作为损失函数，其对模型logits输出<code>z</code>的梯度为 <code>p - y</code>，其中<code>p</code>是模型预测的点击概率，<code>y</code>是真实标签（0或1）。</li><li><strong>对于负样本 (y=0)</strong>：梯度为<code>p</code>。由于点击率通常非常低（例如<code>p &lt; 0.01</code>），这意味着占数据总量99%以上的负样本，在反向传播时产生的梯度信号极其微弱。模型几乎“忽略”了这些负样本提供的信息，导致学习不充分。</li><li><strong>对于正样本 (y=1)</strong>：梯度为<code>p - 1</code>。当模型预测不准时（<code>p</code>接近0），梯度接近-1，这是一个非常强的更新信号。</li><li><strong>结论</strong>：正负样本对模型更新的贡献极度不平衡，模型很难从海量的负样本中学到有效信息。</li></ul></li><li><strong>基础解决方案：使用Rank-style或对比学习类损失函数</strong><ul><li><strong>核心思想</strong>：改变优化目标，从“精确预测每个样本的绝对概率”转向“正确预估正负样本对的相对顺序”。</li><li><strong>以RankNet Loss为例</strong>：其损失函数为 <code>L_Rank = -ln(σ(s+ - s-))</code>，其中<code>s+</code>和<code>s-</code>分别是正负样本的得分。</li><li><strong>优势</strong>：在这种损失下，对负样本得分<code>s-</code>的梯度取决于正负样本的<strong>得分差</strong>，而不是<code>s-</code>本身的大小。即使<code>s-</code>很小，只要它和<code>s+</code>的差距不够大，模型依然能收到一个很强的“把<code>s-</code>降得更低”的梯度信号，从而能更充分地从负样本中学习，缓解梯度消失问题。</li></ul></li></ul><h3 id="第二部分：经典与前沿的多目标模型架构"><strong>第二部分：经典与前沿的多目标模型架构</strong></h3><h3 id="1-ESMM-Entire-Space-Multi-task-Model-：专门解决SSB问题"><strong>1. ESMM (Entire Space Multi-task Model)</strong>：专门解决SSB问题</h3><p>ESMM是阿里巴巴在2018年提出的一个影响深远的模型，它的设计目标非常明确，就是为了解决在序贯行为（如“曝光 -&gt; 点击 -&gt; 转化”）中普遍存在的**样本选择偏差（SSB）**问题。</p><p><strong>1.1 背景：为何需要ESMM？</strong></p><p>正如我们第一部分所讨论的，SSB问题的根源在于CVR（点击后转化率）模型的<strong>训练空间</strong>（被点击过的样本）和<strong>预测空间</strong>（所有被曝光的样本）不一致。这导致传统CVR模型预估出来的转化率是有偏的，无法直接在全量商品排序中准确使用。</p><p>ESMM的提出，就是为了解决这个偏差，得到一个可以在**全样本空间（Entire Space）**上使用的、无偏的CVR预估值。</p><p><strong>1.2 ESMM的核心思想：在全空间建模</strong></p><p>ESMM的思路非常巧妙，它没有试图去“修正”有偏差的训练数据，而是通过一个数学上的恒等关系，从根本上改变了学习的目标。</p><ul><li><p><strong>核心恒等式</strong>：模型认识到，用户的“曝光后即转化”行为，可以被分解为“先点击”和“点击后转化”两个连续的事件。用概率公式表达就是 ：</p><p><code>p(转化 | 曝光) = p(点击 | 曝光) × p(转化 | 曝光, 点击)</code></p></li><li><p><strong>转化为模型术语</strong>：这个公式可以被直接翻译成我们熟悉的三个预估任务 ：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>p</mi><mi>C</mi><mi>T</mi><mi>C</mi><mi>V</mi><mi>R</mi><mo>=</mo><mi>p</mi><mi>C</mi><mi>T</mi><mi>R</mi><mo>×</mo><mi>p</mi><mi>C</mi><mi>V</mi><mi>R</mi></mrow><annotation encoding="application/x-tex">pCTCVR=pCTR×pCVR</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0715em;">pC</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.0715em;">C</span><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0715em;">pC</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0715em;">pC</span><span class="mord mathnormal" style="margin-right:0.2222em;">V</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span></span></span></span></span></p><ul><li><strong>pCTR</strong> (Click-Through Rate): 点击率，可以在<strong>全曝光样本空间</strong>上建模。</li><li><strong>pCVR</strong> (Conversion Rate): 点击后转化率，这是我们真正关心但有偏差的目标。</li><li><strong>pCTCVR</strong> (Click-Through &amp; Conversion Rate): 曝光后“既点击又转化”的概率，也可以在<strong>全曝光样本空间</strong>上建模。</li></ul><p>ESMM的洞见在于，既然pCTR和pCTCVR都可以在全空间上进行无偏的学习，我们就可以利用这个等式关系，来“约束”和“求解”出那个无偏的pCVR。</p></li></ul><p><strong>1.3 ESMM的架构与关键机制</strong></p><p>为了实现上述思想，ESMM设计了一个简洁而有效的双塔结构。</p><ul><li><p><strong>双塔结构 (Two-Tower Architecture)</strong>：</p><ul><li><strong>CTR塔</strong>：一个独立的子网络，输入特征，输出预估的<code>pCTR</code>。</li><li><strong>CVR塔</strong>：另一个独立的子网络，输入相同的特征，输出预估的<code>pCVR</code>。</li><li>这两个塔通常共享底层的Embedding层，以学习通用的特征表示。</li></ul></li><li><p><strong>损失函数与隐式训练 (Loss Function &amp; Implicit Training)</strong>：<br>这是ESMM的“魔法”所在。它的总损失函数由两部分构成：</p><p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mrow><mi>t</mi><mi>o</mi><mi>t</mi><mi>a</mi><mi>l</mi></mrow></msub><mo>=</mo><msub><mi>L</mi><mrow><mi>C</mi><mi>T</mi><mi>R</mi></mrow></msub><mo>+</mo><msub><mi>L</mi><mrow><mi>C</mi><mi>T</mi><mi>C</mi><mi>V</mi><mi>R</mi></mrow></msub></mrow><annotation encoding="application/x-tex">L_{total}=L_{CTR}+L_{CTCVR}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight">o</span><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight" style="margin-right:0.0197em;">l</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.1389em;">T</span><span class="mord mathnormal mtight" style="margin-right:0.0077em;">R</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.1389em;">T</span><span class="mord mathnormal mtight" style="margin-right:0.0715em;">C</span><span class="mord mathnormal mtight" style="margin-right:0.2222em;">V</span><span class="mord mathnormal mtight" style="margin-right:0.0077em;">R</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span></span></p><ul><li><code>L_CTR</code>：CTR任务的损失。用<strong>CTR塔</strong>的输出<code>pCTR</code>和真实的点击标签<code>y_click</code>计算。</li><li><code>L_CTCVR</code>：CTCVR任务的损失。用<strong>CTR塔和CVR塔输出的乘积</strong> <code>pCTR × pCVR</code> 和真实的“点击且转化”标签<code>y_conversion</code>计算。</li></ul><p><strong>关键机制</strong>：请注意，整个训练过程中，<strong>没有任何损失函数是直接作用于CVR塔的输出<code>pCVR</code>上的</strong>。CVR塔的参数更新，其梯度完全来自于<code>L_CTCVR</code>，并通过那个乘法节点反向传播回来。</p><p>这种**“隐式训练”**的方式，强迫CVR塔必须学习到一个能在全空间上都成立的<code>pCVR</code>。因为只有这样，它与<code>pCTR</code>相乘后才能最好地拟合在全空间上观测到的<code>pCTCVR</code>。这就从机制上保证了我们最终得到的<code>pCVR</code>是无偏的，完美地解决了SSB问题。</p></li></ul><p><strong>1.4 总结</strong></p><ul><li><strong>目标</strong>：专门解决序贯任务中的样本选择偏差（SSB）问题。</li><li><strong>思想</strong>：利用<code>pCTCVR = pCTR × pCVR</code>的恒等式，在全样本空间上进行建模。</li><li><strong>机制</strong>：通过一个共享底层的双塔结构，联合优化CTR和CTCVR两个任务的损失，从而对CVR塔进行<strong>隐式监督</strong>，最终得到无偏的CVR预估值。</li></ul><h3 id="2-MMoE-Multi-gate-Mixture-of-Experts-：经典的任务间信息选择与共享模型"><strong>2. MMoE (Multi-gate Mixture-of-Experts)</strong>：经典的任务间信息选择与共享模型</h3><p>MMoE是Google在2018年提出的一个里程碑式的多任务学习模型。它的设计目标非常明确，就是为了解决多任务学习中最常见的**“负迁移”<strong>或</strong>“跷跷板效应”**问题。</p><p><strong>2.1 背景：为何需要MMoE？</strong></p><p>在MMoE之前，最流行的多任务学习架构是<strong>Shared-Bottom</strong>，即所有任务共享一个底层的特征提取网络，然后各自连接一个独立的任务塔。这种硬参数共享的方式过于“一刀切”，它强迫所有任务（无论相关性高低）都使用同一套特征表示。当任务间差异较大时，就会产生我们之前讨论的“梯度冲突”和“跷跷-板效应”，模型性能受损。MMoE的提出，就是为了用一种更“柔软”、更智能的方式来处理任务间的信息共享。</p><p><strong>2.2 MMoE的核心思想与架构</strong></p><p>MMoE的核心思想是**“混合专家” (Mixture of Experts)**，它借鉴了MoE架构并针对多任务场景进行了关键创新。</p><ul><li><p><strong>架构组成</strong>：</p><ol><li><strong>多个共享的专家网络 (Shared Experts)</strong>：模型底层不再是单个共享网络，而是并行的、多个结构相同但参数不共享的“专家”子网络。每个专家都可以被看作一个独立的特征提取器，它们从不同角度去理解输入信息。</li><li><strong>多个独立的门控网络 (Gating Networks)</strong>：这是MMoE最关键的创新。它为<strong>每一个任务都配备了一个独立的、轻量的门控网络</strong>。</li></ol></li><li><p><strong>工作流程</strong>：</p><p><img src="image.png" alt="image.png"></p><ol><li>当一个样本输入时，它会被<strong>同时送往所有的专家网络和所有的门控网络</strong>。</li><li>每个专家网络输出一个特征向量。</li><li><strong>每个任务的专属门控网络</strong>会输出一组权重（经过Softmax函数，所以权重之和为1）。</li><li>对于某个特定任务（如CTR），模型会用其专属门控网络产生的权重，对所有专家的输出向量进行<strong>加权求和</strong>，形成一个为CTR任务量身定制的融合特征。</li><li>这个融合后的特征再被送入CTR专属的任务塔，得到最终的预测值。其他任务（如CVR）也依此类推，使用自己的门控权重进行融合。</li></ol><p><img src="image%201.png" alt="image.png"></p></li><li><p><strong>优势</strong>：通过为每个任务分配独立的门控，MMoE可以显式地学习任务间的关系 。如果两个任务相关性高，它们的门控网络就可能学习到相似的权重分布，从而共享更多的专家信息；如果任务相关性低，它们的门控网络就可以学习到差异化的权重，有效避免不相干信息的干扰，从而缓解负迁移问题。</p></li></ul><p><strong>2.3 工程实践：用Dropout解决“门控极化”问题</strong></p><p>在实际训练中，MMoE可能会出现一个问题，即“门控极化” (Gate Polarization)。</p><ul><li><strong>问题描述</strong>：某个任务的门控网络在训练中变得“过于自信”，其Softmax输出的权重变得非常极端，比如<code>[0, 0, 1, 0]</code>。这意味着这个任务几乎只从一个专家那里学习信息，而<strong>完全忽略了其他专家</strong>。</li><li><strong>后果</strong>：这使得MMoE模型退化成了一个普通的单网络模型，失去了“混合专家”的优势，可能重新引发“跷跷板效应”。</li><li><strong>解决方案</strong>：对Softmax的输出使用<strong>Dropout</strong>。在训练时，随机地将门控网络输出权重向量中的某些值置为0（例如，每个权重有10%的概率被mask）。</li><li><strong>作用</strong>：这种做法强迫模型不能过度依赖任何一个专家。即使模型想让权重极化成<code>[0, 0, 1, 0]</code>，那个唯一的<code>1</code>也有概率被dropout掉，从而迫使模型必须学会利用其他专家来进行“备份”，最终学习到更健壮、更多样化的专家组合策略。比如<code>[0.1, 0.2, 0.5, 0.2]</code> 更加平滑鲁棒。</li></ul><p><strong>2.4 总结</strong></p><ul><li><strong>目标</strong>：解决多任务学习中的负迁移（“跷跷板效应”）问题。</li><li><strong>思想</strong>：用一种软参数共享的方式，让不同任务有选择性地共享信息。</li><li><strong>机制</strong>：通过“<strong>多个共享专家网络 + 每个任务独立的门控网络</strong>”的结构，为每个任务学习一套专属的专家组合权重。</li><li><strong>实践</strong>：通过在门控输出上施加<strong>Dropout</strong>来防止“门控极化”，增强模型鲁棒性。</li></ul><h3 id="3-PLE-Progressive-Layered-Extraction-：MMoE的进阶版"><strong>3. PLE (Progressive Layered Extraction)</strong>：MMoE的进阶版</h3><p>PLE是腾讯PCG在2020年提出的，可以看作是MMoE的一个重要演进和增强版本。它针对MMoE的一些潜在局限性进行了改进，并整合了解决SSB问题的方案，使其成为一个更强大、更全面的多任务学习框架。</p><p><strong>3.1 背景：为何需要PLE？——MMoE的局限性</strong></p><p>我们在之前的讨论中提到，MMoE通过多个门控网络来选择性地组合一系列<strong>共享专家</strong>的输出。这里的关键在于，所有的专家都是<strong>共享</strong>的。</p><ul><li><strong>MMoE的局限</strong>：如果不同任务之间的差异非常大，可能需要的特征也大相径庭。在这种情况下，仅仅依靠“重新组合”共享的特征可能是不够的。用之前图片中的比喻就是，MMoE能改变“厨师”的配方，但无法改变“菜品”本身。模型缺少为某个任务“开小灶”，学习独有特征的能力。</li></ul><p>PLE正是为了解决这个问题而设计的。</p><p><strong>3.2 PLE的核心思想与架构</strong></p><p>PLE引入了两个核心的创新点，使其比MMoE更加灵活和强大。</p><ul><li><strong>核心思想：引入“任务专属专家”与“共享专家”的组合</strong><ul><li>这是PLE对MMoE最直接的改进。在PLE的网络结构中，专家被分成了两类：<ol><li><strong>共享专家 (Shared Experts)</strong>：和MMoE中的专家一样，所有任务都可以使用它们，用于学习通用的模式。</li><li><strong>任务专属专家 (Task-specific Experts)</strong>：为每一个任务都配备一组自己私有的专家。这些专家只为当前任务服务，用于学习该任务独特的、个性化的特征。</li></ol></li><li>这种设计使得模型既能捕捉任务间的共性，又能保留每个任务的个性，大大增强了模型的表达能力。</li></ul></li><li><strong>关键机制一：渐进式分层抽取结构 (Progressive Layered Extraction)</strong><ul><li>这是PLE名字的由来，也是其结构上的最大特点。PLE的网络不是单层的，而是<strong>多层堆叠的</strong>。</li><li><strong>信息流动方式</strong>：第一层网络（包含共享专家和各任务的专属专家）的输出，并不会直接用于最终的预测。相反，它们会被汇集起来，作为<strong>第二层网络的输入</strong>。</li><li><strong>逐层抽象</strong>：通过这种方式，PLE构建了一个从底层到高层、对特征进行渐进式加工和抽取的信息流。高层网络可以基于低层网络已经融合过的、更抽象的特征来做进一步的信息交互和选择，从而能学习到任务间更复杂、更深层次的关系。</li></ul></li></ul><p><strong>3.3 PLE的关键机制二：一个模型解决两大难题</strong></p><p>PLE的强大之处在于，它通过一个统一的框架，同时解决了多任务学习中的两个核心痛点。</p><ol><li><strong>解决负迁移</strong>：通过“专属专家+共享专家”的精巧结构，以及门控的选择机制，PLE可以非常灵活地处理任务间的关系，有效隔离冲突，促进共享，从而缓解“跷跷板效应”。</li><li><strong>解决样本选择偏差 (SSB)</strong>：PLE在其损失函数的设计中，明确地引入了<strong>Mask（掩码）机制</strong>。<ul><li><p><strong>损失函数</strong>：</p><p><img src="image%202.png" alt="image.png"></p></li><li><p><strong>机制解读</strong>：在计算任务<code>k</code>的损失时，只有当样本<code>i</code>属于任务<code>k</code>的有效训练样本时，其掩码<code>δ_i^k</code>才为1，否则为0。例如，在计算CVR任务的loss时，只有那些被点击过的样本才会被纳入计算。</p></li><li><p><strong>效果</strong>：这个机制从根本上解决了不同任务训练样本空间不一致的问题，使得PLE可以优雅地处理像CTR-CVR预估这类存在SSB问题的序贯任务。</p></li></ul></li></ol><p><strong>3.4 总结</strong></p><ul><li><strong>定位</strong>：MMoE的进阶版，一个更强大、更全面的多任务学习框架。</li><li><strong>核心改进</strong>：引入了“<strong>任务专属专家</strong>”，打破了MMoE中专家必须全部共享的限制。</li><li><strong>核心架构</strong>：采用<strong>渐进式的分层结构</strong>，实现对特征的逐层抽象和抽取。</li><li><strong>综合能力</strong>：通过其网络结构缓解了<strong>负迁移</strong>问题，同时通过<strong>Masked Loss</strong>解决了**样本选择偏差（SSB）**问题，是一个“一站式”的解决方案。</li></ul><h3 id="4-PEPNet-Parameter-and-Embedding-Personalized-Network-：快手提出的个性化多任务多域模型"><strong>4. PEPNet (Parameter and Embedding Personalized Network)</strong>：快手提出的个性化多任务多域模型</h3><p>PEPNet是快手在2023年KDD会议上提出的一个SOTA（State-of-the-art）模型，它为解决工业界复杂的<strong>多任务、多领域</strong>推荐问题提供了一个新颖且高效的思路。</p><p><strong>4.1 背景：为何需要PEPNet？——直面“双重跷跷板效应”</strong></p><p>正如我们在第一部分所讨论的，工业级推荐系统面临着“任务跷跷板”和“领域跷跷板”的双重挑战。PEPNet的论文将这种复杂的现象命名为**“不完美的双重跷跷板效应” (Imperfectly Double Seesaw Phenomenon)** 。现有的模型如MMoE或PLE虽然在解决任务冲突上很有效，但在直接扩展到多领域时，仍然可能因为领域间的巨大差异而表现不佳 。PEPNet的目标就是同时缓解这两种冲突。</p><p><strong>4.2 PEPNet的核心思想：注入个性化先验</strong></p><p>与MMoE、PLE等模型专注于设计更精巧的主干网络结构不同，PEPNet的核心思想是作为一个**“即插即用” (plug-and-play)** 的模块，可以被注入到任何模型中 。</p><p>它的策略是，充分利用各种<strong>个性化的先验信息</strong>（如用户画像、物品属性、领域特征等），通过门控机制，对一个基础的多任务模型进行<strong>动态的、精细化的调整</strong> 。</p><p><strong>4.3 PEPNet的关键机制：双管齐下</strong></p><p>PEPNet通过两个子网络EPNet和PPNet，分别应对“领域”和“任务”的挑战。</p><p><img src="image%203.png" alt="image.png"></p><ul><li><p><strong>机制一：EPNet (Embedding Personalized Network) —— 解决“域”问题</strong></p><ul><li><p><strong>目标</strong>：为不同领域中的不同用户，融合具有不同重要性的特征，解决“领域跷跷板” 。</p></li><li><p><strong>工作机制</strong>：EPNet使用<strong>领域相关的特征</strong>（如domain ID，用户在该域的交互次数等）作为其门控单元（Gate NU）的输入 。这个门控单元会生成一个与Embedding层维度相同的门控向量，然后通过</p><p><strong>按元素相乘</strong>的方式，作用于共享的Embedding层，从而为不同的领域生成一套专属的、个性化的Embedding表示 。</p></li></ul></li><li><p><strong>机制二：PPNet (Parameter Personalized Network) —— 解决“任务”问题</strong></p><ul><li><p><strong>目标</strong>：为不同任务中的不同用户，平衡稀疏度不同的目标，解决“任务跷跷板” 。</p></li><li><p><strong>工作机制</strong>：PPNet使用<strong>用户/物品/作者相关的通用个性化特征</strong>（如user ID, item ID, user age等）作为其门控单元的输入 。这些门控单元会为DNN任务塔的</p><p><strong>每一层</strong>都生成一个门控向量，然后通过<strong>按元素相乘</strong>的方式，作用于该层网络的隐层激活值上 。这相当于为每个用户的每次预测，都动态调整了DNN任务塔每一层的“神经元”的激活强度，实现了非常精细的个性化调控。</p></li></ul></li></ul><p><strong>4.4 灵感来源：Gate NU与LHUC算法</strong></p><p>您总结得非常准确，PEPNet中这种在DNN每一层进行参数调控的核心思想，其直接灵感来自于</p><p><strong>语音识别领域的LHUC算法 (Learning Hidden Unit Contributions)</strong> 。</p><ul><li><strong>思想迁移</strong>：LHUC算法通过学习一个说话人专属的门控，来缩放（scale）声学模型中隐藏层的单元，从而提升对不同说话人语音的识别准确率 。PEPNet巧妙地将这种思想迁移过来：PPNet通过学习一个用户/物品专属的门控，来缩放推荐模型中任务塔隐藏层的单元，从而提升对不同用户在不同任务上的预测准确率 。</li><li><strong>Gate NU</strong>：这个基础的门控单元本身，通常采用一个**“先降维、后升维”**的两层MLP瓶颈结构，以保证高效和轻量。其输出层通常使用<code>γ*Sigmoid</code>（<code>γ</code>常设为2）作为激活函数，使得门控既能抑制（输出&lt;1）又能增强（输出&gt;1）信号，比普通门控更灵活 。</li></ul><p><strong>4.5 总结</strong></p><ul><li><strong>定位</strong>：快手提出的，一个用于解决复杂<strong>多任务、多领域</strong>问题的SOTA工业级解决方案。</li><li><strong>核心问题</strong>：针对“<strong>不完美的双重跷跷板效应</strong>”。</li><li><strong>核心策略</strong>：作为一个<strong>即插即用</strong>的模块，通过<strong>注入个性化先验信息</strong>来动态调控主干模型。</li><li><strong>关键机制</strong>：<ul><li><strong>EPNet</strong>：在<strong>Embedding层</strong>进行“领域”级别的个性化。</li><li><strong>PPNet</strong>：在<strong>DNN任务塔的每一层</strong>进行“任务”级别的个性化。</li></ul></li><li><strong>灵感来源</strong>：其核心的层层调控机制源于语音识别领域的<strong>LHUC算法</strong>。</li></ul><h3 id="5-STAR-Star-Topology-Adaptive-Recommender-：阿里提出的多领域CTR模型"><strong>5. STAR (Star Topology Adaptive Recommender)</strong>：阿里提出的多领域CTR模型</h3><p>STAR是阿里巴巴在2021年CIKM会议上提出的一个专门用于解决多领域（Multi-Domain）CTR预估问题的模型。它的核心在于如何优雅地处理不同领域之间的共性和差异性，以缓解“领域跷跷板”效应。</p><p><strong>5.1 背景：为何需要STAR？——多领域推荐的挑战</strong></p><p>在像淘宝这样的超级App中，存在多个推荐场景（领域），比如“猜你喜欢”、“购买后推荐”、“搜索结果页”等。这些领域的用户和物品有重叠，但用户的行为意图、物品的分布却有很大差异。STAR模型就是为了在这种复杂的环境下，构建一个统一的模型，既能利用所有领域的数据来增强学习效果，又能适应每个领域的独特特性。</p><p><strong>5.2 STAR的核心思想：星形拓扑结构</strong></p><p>为了平衡共性与个性，STAR模型设计了一种“中心-辐射”式的网络结构，即<strong>星形拓扑结构 (Star Topology)</strong>。</p><ul><li><strong>共享中心网络 (Shared Center Network)</strong>：作为“中心”，它负责学习所有领域都通用的底层知识和特征表示。这部分网络由所有领域的数据共同训练，实现了知识的迁移和共享。</li><li><strong>领域特定网络 (Domain-specific Networks)</strong>：作为从中心“辐射”出去的分支，每个领域都拥有一个自己专属的网络。它负责学习该领域的个性化模式，捕捉其独有的特征。</li><li><strong>优势</strong>：这种结构允许模型在共享参数的同时，也为每个领域分配了专属参数，从而能够灵活地适应不同领域的数据分布，有效缓解领域间的冲突和负迁移。</li></ul><p><strong>5.3 STAR的关键机制</strong></p><p>除了宏观的星形拓扑结构，STAR还引入了两个非常精巧的关键机制来增强其领域自适应能力。</p><ul><li><strong>机制一：分区归一化 (Partitioned Normalization, PN)</strong><ul><li><strong>问题</strong>：标准的批归一化（Batch Normalization, BN）假设一个批次内的数据来自同一分布，但在多领域场景下，一个批次内会混合来自不同领域、分布差异巨大的数据，这使得BN失效甚至起到反作用。</li><li><strong>解决方案</strong>：PN在做归一化时，会先在批次内根据<strong>领域ID</strong>对数据进行分区。然后，它为每个分区（即每个领域）<strong>单独计算</strong>均值<code>μd</code>和方差<code>σd²</code>，并用这些领域专属的统计量来对该领域的数据进行归一化。</li><li><strong>公式</strong>：<code>z' = (γ · γd) * (z - μd) / sqrt(σd² + ε) + (β + βd)</code>。<ul><li>这里的<code>γ</code>和<code>β</code>是全局共享的缩放和偏移参数，而<code>γd</code>和<code>βd</code>是为每个领域<code>d</code>学习的专属缩放和偏移参数。</li></ul></li><li><strong>优势</strong>：PN能够精准地捕捉每个领域的真实数据分布，避免了不同领域统计信息的混乱，从而让模型训练更稳定，性能更好。</li></ul></li><li><strong>机制二：辅助网络 (Auxiliary Network)</strong><ul><li><strong>问题</strong>：模型很难自动、显式地学习到不同领域间的差异。如果仅把领域ID作为一个普通特征输入，其信号在经过多层网络后很容易被削弱。</li><li><strong>解决方案</strong>：STAR引入了一个独立的辅助网络，专门用来建模领域差异。</li><li><strong>机制</strong>：这个网络直接将<strong>领域指示器 (domain indicator)</strong> 作为输入，学习一个领域专属的嵌入向量。它的最终输出会<strong>直接与主网络的输出相加</strong>，共同形成最终的预测值。</li><li><strong>优势</strong>：这种设计相当于为主模型的预测结果提供了一个<strong>可学习的、领域专属的偏置修正项 (Bias Correction)</strong>。它保证了领域信息能直接、有力地影响最终的预测结果，让模型对领域差异更加敏感。</li></ul></li></ul><p><strong>5.4 总结</strong></p><ul><li><strong>定位</strong>：阿里巴巴提出的，专门解决工业级多领域CTR预估问题的SOTA模型。</li><li><strong>核心架构</strong>：<strong>星形拓扑结构</strong>，通过“共享中心网络+领域专属网络”来平衡知识共享与领域个性化。</li><li><strong>关键机制</strong>：<ol><li><strong>分区归一化 (PN)</strong>：一种领域感知的归一化方法，用于精确适配不同领域的数据分布。</li><li><strong>辅助网络 (Auxiliary Network)</strong>：一个并行的旁路网络，用于为主模型提供一个显式的、领域专属的偏置修正。</li></ol></li></ul><h3 id="第三部分：多任务的进阶优化策略（模型之外）"><strong>第三部分：多任务的进阶优化策略（模型之外）</strong></h3><p>当我们确定了多任务学习的模型架构后（如MMoE或PLE），优化的过程本身也大有可为。这部分策略不改变模型结构，而是直接作用于模型的<strong>训练过程</strong>，通过更精细的手段来平衡不同任务，因此可以被看作是“模型之外”的通用优化技巧。</p><h3 id="1-梯度层面：直接处理梯度冲突"><strong>1. 梯度层面：直接处理梯度冲突</strong></h3><ul><li><strong>背景：为何需要在梯度层面进行优化？</strong><ul><li>我们之前反复提到，多任务学习的核心挑战之一是“负迁移”，其根本原因在于<strong>梯度冲突</strong>。在拥有共享参数的模型中，不同任务为了最小化各自的损失，在反向传播时可能会对同一个共享参数产生方向相反的更新需求（一个想让参数增大，另一个想让参数减小）。</li><li>这种“拉锯战”会导致模型训练不稳定，收敛缓慢，并最终损害整体性能。梯度层面的优化策略，就是像一个“交通警察”一样，在参数更新前，主动地对这些冲突的“指令”进行协调和处理。</li></ul></li></ul><p><strong>1.1 梯度优化策略一：PCGrad (Projecting Conflicting Gradients)</strong></p><ul><li><strong>核心思想</strong>：“有冲突才调解，没冲突就放行”。它是一种相对温和的、只在必要时介入的策略。</li><li><strong>工作机制</strong>：<ol><li><strong>检测冲突</strong>：在每次参数更新前，算法会检查任意两个任务（如任务i和任务j）的梯度向量 <code>gi</code> 和 <code>gj</code>。它通过<strong>计算两个向量的余弦相似度（即点积）来判断方向</strong>。如果余弦相似度为<strong>负数</strong>，意味着两个梯度的夹角大于90度，方向存在明显的冲突。</li><li><strong>解决冲突</strong>：一旦检测到冲突，PCGrad会进行一次“投影”操作。例如，它会将梯度<code>gi</code>中与<code>gj</code>方向相反的分量剔除掉，只保留与<code>gj</code>正交（不冲突）的分量。这可以理解为，它修改了<code>gi</code>，告诉它：“你可以朝自己的目标更新，但不要干扰任务j”。</li><li><strong>更新参数</strong>：最后，使用这些经过“调解”和修正后的梯度来更新共享参数。</li></ol></li><li><strong>优势</strong>：PCGrad的逻辑清晰，实现相对简单，能有效避免任务间最直接的相互损害。</li></ul><p><strong>1.2 梯度优化策略二：GradVec</strong></p><ul><li><strong>核心思想</strong>：不满足于仅仅被动地消除冲突，而是主动地去寻找一个对所有任务都尽可能公平的“共同前进”方向。</li><li><strong>工作机制</strong>：<ul><li>GradVec的目标是找到一个新的梯度向量<code>v</code>，这个<code>v</code>与<strong>所有单个任务</strong>的梯度<code>gi</code>都尽可能方向一致（即夹角尽可能小）。</li><li>它不再是简单地对梯度进行“是/否”冲突的判断和修正，而是将寻找最优更新方向本身，重新定义为一个优化问题。根据图片中的描述，它可以根据不同任务间的关系，设定一个可容忍的“最大夹角”（或最小余弦相似度），并在这个约束下找到一个最优的折衷方向。</li></ul></li><li><strong>优势</strong>：相比PCGrad的“两两调解”，GradVec试图从一个更全局的视角来规划更新路径，理论上可能找到更稳定、更高效的收敛路径。</li></ul><h3 id="2-损失层面：动态平衡任务权重"><strong>2. 损失层面：动态平衡任务权重</strong></h3><p>在多任务学习中，一个最直接的问题就是如何组合不同任务的损失函数。简单地将它们等权重相加（<code>L_total = L_A + L_B</code>）往往效果不佳，因为不同任务的量纲、难度、收敛速度都不同。模型很容易被“简单”或“loss值大”的任务主导，而忽略了其他任务。因此，动态平衡损失权重至关重要。</p><p><strong>2.1 策略一：自适应加权 (Adaptive Weighting, 如DWA)</strong></p><ul><li><strong>核心思想</strong>：像一位“因材施教”的老师，动态地给“学得慢”（即难收敛）的任务分配更多的关注（即更高的loss权重）。</li><li><strong>工作机制</strong>：<ul><li>这种方法会实时追踪每个任务的学习进度，通常用<strong>损失函数值的下降速率</strong>来衡量。</li><li>如果一个任务的loss在最近几轮迭代中下降得很慢，说明模型在这个任务上遇到了瓶颈。此时，算法会自动调高这个任务的loss权重，迫使模型在下一轮更新中投入更多“精力”来优化它。</li><li>它的目标是让所有任务的<strong>学习速度尽可能保持一致</strong>，避免出现部分任务已经过拟合、而另一部分任务还未充分学习的情况。</li></ul></li></ul><p><strong>2.2 策略二：不确定性加权 (Uncertainty Weighting)</strong></p><ul><li><strong>核心思想</strong>：让模型不仅要学习预测任务，还要学习评估自己对这个预测的“不确定性”。一个任务的loss权重，应该和模型对这个任务的确定性成正比。</li><li><strong>工作机制</strong>：<ul><li>该方法为每个任务<code>i</code>引入一个可学习的<strong>不确定性参数<code>σi²</code></strong>。总的损失函数被构造成类似 <code>(1/2σi²) * Li + log(σi)</code> 的形式。</li><li>为了最小化这个总损失，模型有两个选择：一是努力降低任务损失<code>Li</code>；二是“承认自己能力不足”，调高不确定性<code>σi²</code>来降低第一项<code>Li</code>的影响。</li><li><code>log(σi)</code>这一项作为<strong>正则项</strong>，惩罚过大的不确定性，防止模型“躺平”，直接将所有<code>σi²</code>设为无穷大来忽略所有任务。</li><li>最终，模型会达到一个平衡：对于那些模型确实难以把握、噪声较大的任务，它会学习到一个较高的不确定性，从而自动降低该任务的权重。</li></ul></li></ul><h3 id="3-业务目标层面：约束与重构"><strong>3. 业务目标层面：约束与重构</strong></h3><p>有时，多任务优化的目标并不仅仅是几个loss的加权和，而是要满足一些复杂的、非线性的业务规则。</p><p><strong>3.1 策略一：约束优化 (Constrained Optimization)</strong></p><ul><li><strong>适用场景</strong>：当业务有<strong>硬性指标（KPI）或约束</strong>时。例如：“在提升CTR的同时，GMV（商品交易总额）<strong>绝不能低于</strong>200万”。</li><li><strong>工作机制</strong>：<ul><li>这将多任务学习问题转化为一个标准的<strong>带约束优化问题</strong>。</li><li>模型的优化过程不再是无约束地寻找loss最低点，而是在一个由业务指标限定的**“可行域”**内寻找最优解。</li><li>这个“可行域”的边界通常由<strong>KKT条件</strong>（一种在非线性规划中找到最优解的必要条件）来定义。在训练的每次迭代中，都需要校验优化结果是否满足这些硬性约束，满足了才是一次有效的更新。</li></ul></li></ul><p><strong>3.2 策略二：重构优化目标 (Reconstructing the Objective)</strong></p><ul><li><strong>适用场景</strong>：当业务逻辑非常复杂，难以用简单的约束来表达时，可以尝试从根源上重新定义模型的学习目标。</li><li><strong>工作机制（以广告回报模型为例）</strong>：<ol><li><strong>定义新指标</strong>：不直接优化CTR或CVR，而是定义一个更贴近商业目标的指标，比如“单次曝光的平均收益”。</li><li><strong>重构标签</strong>：计算出一个全局的“平均曝光收益”基准线。对于每一次广告曝光，如果它的实际收益（综合点击费用、转化服务费等）高于基准线，则其训练标签为正；如果低于基准线，则其标签为<strong>负</strong>。</li><li><strong>带来的好处</strong>：<ul><li><strong>惩罚低效广告</strong>：那些高曝光但低转化、拉低平台整体收益的广告，会因为得到负向的训练信号而逐渐减少曝光机会。</li><li><strong>扶持新广告</strong>：一个全新的、没有历史数据的广告，其初始收益可被认为是0，这比那些收益为负的存量广告要“好”，因此更容易获得冷启动的探索机会。</li></ul></li></ol></li></ul><p>这种方法非常巧妙地将复杂的商业权衡（Exploration &amp; Exploitation、平台长期收益）直接编码到了模型的训练信号中。</p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/10/Rec_AD_MTL/</id>
    <link href="https://qyp9909.github.io/2025/08/10/Rec_AD_MTL/"/>
    <published>2025-08-10T09:00:00.000Z</published>
    <summary>
      <![CDATA[<h3 id="搜广推-多目标排序">搜广推[多目标排序]</h3>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月10日 21:12</p>
<h3 id="第一部分：多目标学习的核心挑战与基础"><strong>第一部]]>
    </summary>
    <title>搜广推笔记 多目标排序</title>
    <updated>2025-08-11T08:17:48.461Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h2 id="搜广推-排序">搜广推[排序]</h2><p>Created by: Yuanpeng QU<br>Created time: 2025年8月9日 14:29</p><h3 id="推荐系统排序模型知识体系总结（大纲）"><strong>推荐系统排序模型知识体系总结（大纲）</strong></h3><h2 id="第一章：排序模型的基础-Foundations-of-Ranking-Models"><strong>第一章：排序模型的基础 (Foundations of Ranking Models)</strong></h2><p>在深入探讨各种复杂精妙的模型之前，我们必须先理解所有排序模型所立足的共同基础。这个基础包含两个层面：一是我们如何从业务逻辑上将“排序”这个抽象任务，转化为一个可以用数学模型解决的问题；二是在这个问题的解决方案中，那个最简单、最核心的基石模型——逻辑回归（LR）——是如何工作的。</p><h3 id="1-1-排序的核心任务：从“排序列表”到“点击率预估”的思维转变">1.1 排序的核心任务：从“排序列表”到“点击率预估”的思维转变</h3><p>一个推荐系统或搜索引擎，其最终呈现给用户的，是一个<strong>有序的物品列表</strong>。系统的终极目标是让这个列表的排序方式，能够最大化某个商业指标（如用户满意度、平台收入、用户总停留时长等）。</p><p>直接去优化一个列表的“顺序”本身，在数学上是一件非常困难的事情。我们很难定义一个平滑、可导的损失函数来衡量一个列表“排得有多好”。因此，工业界和学术界普遍采用了一种非常关键的<strong>思维转变</strong>，将这个复杂的“列表排序问题”进行降维和简化。这种转变的核心是：<strong>不再直接比较物品与物品之间的相对顺序，而是为每一个独立的物品进行“打分”。</strong></p><p>我们只需要找到一个能够精准衡量“物品对特定用户的吸引力”的分数。分数越高的物品，就理应排在越靠前的位置。在互联网广告、电商推荐、信息流等场景下，<strong>点击率（Click-Through Rate, CTR）</strong> 被证明是这个“分数”的最佳选择之一：</p><ol><li><strong>可衡量性</strong>：用户的点击行为可以被大规模、精确地记录下来，为模型训练提供了海量的、真实的标注数据（曝光但未点击为负样本，点击为正样本）。</li><li><strong>与兴趣的强关联性</strong>：用户的一次点击，是其对某个内容感兴趣的、最直接、最明确的信号。一个物品的预估点击率越高，通常意味着它更能吸引该用户。</li><li><strong>概率的天然契合</strong>：CTR 本身是一个概率值（点击的概率），这与许多机器学习分类模型的输出形式（如逻辑回归通过Sigmoid函数输出一个0到1的概率）完美契合。</li></ol><p>通过这个思维转变，我们成功地将一个模糊的“排序问题”，转换成了一个定义清晰的、标准的<strong>二元分类问题</strong>：</p><blockquote><p><strong>对于任意一个给定的（用户，物品，上下文）组合，预测该用户点击该物品的概率是多少？</strong></p></blockquote><p>一旦我们训练好一个能精准预测CTR的模型，排序过程就变得非常简单：</p><ol><li>对所有候选物品，逐一调用模型，预测出它们各自的pCTR（predicted CTR）分数。</li><li>将所有物品按照pCTR分数从高到低进行排序。</li><li>将排序后的列表呈现给用户。</li></ol><p>这个从“优化顺序”到“预测分数”的转变，是整个现代推荐排序系统的基石，它为我们后续讨论的所有复杂模型的应用铺平了道路。</p><h3 id="1-2-万变不离其宗的基石：逻辑回归-LR-的原理与局限性">1.2 万变不离其宗的基石：逻辑回归 (LR) 的原理与局限性</h3><p>逻辑回归（Logistic Regression, LR）是解决CTR预估问题最基础、最经典的“Hello, World”级别的模型。尽管它很简单，但其核心思想贯穿了后续许多复杂模型的始终。</p><p><strong>模型原理</strong></p><p>LR模型通过两步来完成从输入特征到最终概率的预测：</p><p>**1. 线性求和 (Linear Combination)：**首先，模型接收一组输入特征（如年龄、性别、物品品类等），并为每个特征分配一个权重 wi。然后将所有特征进行加权求和，得到一个原始的预测分数 z。</p><p><img src="image.png" alt="image.png"></p><p>这里的权重 wi是模型需要从数据中学习的参数，它代表了每个特征对最终结果的影响方向（正或负）和大小。</p><p><strong>2. Sigmoid函数</strong>：上述的线性分数 z 是一个没有边界的实数。为了将其转换为一个0到1之间的概率值，LR使用了一个关键的非线性函数——<strong>Sigmoid函数</strong>。</p><p><img src="image%201.png" alt="image.png"></p><p>无论输入的 z 多大或多小，经过Sigmoid函数处理后的输出y_hat都会被“压扁”到(0, 1)区间内，这个值就是模型预测的点击率（pCTR）。</p><p><strong>LR的优点</strong></p><ul><li><strong>简单高效</strong>：模型原理简单，计算量小，无论训练还是在线预测，速度都非常快，非常适合大规模工业应用。</li><li><strong>可解释性强</strong>：每个特征的权重 wi 都清晰地反映了该特征对结果的影响。例如，一个较大的正权重意味着这个特征是点击行为的强预测因子。这使得模型非常便于理解和调试。</li></ul><p><strong>LR的局限性 (也是后续模型演进的动力)</strong></p><p>LR最大的局限性在于它的<strong>线性假设</strong>。</p><ol><li><strong>无法学习非线性关系</strong>：模型假设最终的决策边界是线性的，无法捕捉现实世界中普遍存在的非线性模式。</li><li><strong>无法自动学习特征交叉</strong>：这是线性假设最直接的后果。LR无法自动学习到“20岁的男性”对“游戏机”这个<strong>组合</strong>的特殊偏好。它只能学习到“20岁”的独立影响和“男性”的独立影响，然后将它们简单相加。</li><li><strong>依赖大量人工特征工程</strong>：为了克服上述缺点，算法工程师必须手动地、基于业务经验地去创造交叉特征（例如，创建一个新的特征叫 <code>is_male_and_age_20</code>），然后再喂给LR模型。这个过程极其耗费人力、难以穷举，且效果高度依赖工程师的经验。</li></ol><p><strong>总结</strong>：逻辑回归是排序模型的起点。它的简单和高效使其成为一个强大的基准，但其无法自动学习特征交叉的根本性缺陷，是催生后续 GBDT+LR、FM 以及所有深度学习排序模型不断演进的核心驱动力。</p><h3 id="1-3-数据的预处理：特征工程的核心概念">1.3 数据的预处理：特征工程的核心概念</h3><p>在我们讨论的所有模型中，都有一个共同的前提：模型本身只能处理数值。然而，我们拥有的原始数据是各式各样的，有类别、有数字、有文本。<strong>特征工程 (Feature Engineering)</strong> 的核心任务，就是将这些原始数据转换成模型能够理解和利用的、高质量的数值化特征。这是决定模型性能上限的关键一步。</p><ul><li>1.3.1 类别特征与独热编码 (One-Hot Encoding)</li></ul><p><strong>类别特征 (Categorical Features)</strong> 是指那些取值为离散标签或符号的特征。这些值之间没有大小、顺序之分。在推荐系统中，这类特征无处不在，例如：</p><ul><li><code>user_id</code></li><li><code>item_id</code></li><li><code>city</code> (如：‘北京’, ‘上海’, ‘广州’)</li><li><code>gender</code> (如：‘男性’, ‘女性’)</li></ul><p><strong>面临的问题</strong>：<br>模型无法直接理解 <code>'北京'</code> 这样的文本。一个最朴素的想法可能是给它们编号，比如 <code>'北京'=1</code>, <code>'上海'=2</code>, <code>'广州'=3</code>。但这样做是<strong>完全错误</strong>的，因为它凭空引入了一种不存在的数学关系，即 <code>广州 &gt; 上海 &gt; 北京</code>。模型会基于这个错误的大小关系去学习，导致结果荒谬。</p><p><strong>解决方案：独热编码 (One-Hot Encoding)</strong><br>独热编码是处理类别特征最标准、最有效的方法。它的核心思想是<strong>用一个向量来表示一个类别，且向量中只有一个位置是 <code>1</code>，其他所有位置都是 <code>0</code></strong>。<br>通过这种方式，我们成功地将类别特征转换成了一个<strong>高维度的稀疏向量</strong>（向量中绝大部分元素都是0）。这样做的好处是，它彻底消除了类别之间的伪序关系，并使得线性模型可以为每一个独立的类别（如 <code>'北京'</code> 这个城市）学习到一个专属的、不受其他城市干扰的权重 <code>w_beijing</code>。</p><ul><li>1.3.2 连续特征与归一化、标准化</li></ul><p><strong>连续特征 (Continuous Features)</strong> 是指那些取值是连续数值的特征，这些数值有明确的大小和顺序关系。例如：</p><ul><li><code>age</code> (年龄)</li><li><code>price</code> (商品价格)</li><li><code>item_historical_ctr</code> (物品历史点击率)</li></ul><p><strong>面临的问题：</strong><br>不同连续特征之间的**数值尺度（Scale）**可能差异巨大。例如，用户的年龄 <code>age</code> 可能在 <code>18</code> 到 <code>60</code> 之间，而用户的年收入 <code>income</code> 可能在 <code>50000</code> 到 <code>1000000</code> 之间。如果直接将这些原始数值输入模型：</p><ul><li><strong>大尺度特征会“支配”模型</strong>：在计算损失和梯度时，<code>income</code> 这个特征的数值会远远大于 <code>age</code>，导致模型对 <code>income</code> 的变化极其敏感，而几乎忽略 <code>age</code> 的影响。</li><li><strong>训练过程不稳定</strong>：巨大的数值差异会导致损失函数的“等高线”变成一个又扁又窄的“峡谷”，使得梯度下降法很难稳定、高效地找到最优点。</li></ul><p><strong>解决方案：特征缩放 (Feature Scaling)</strong><br>为了消除不同特征间的尺度差异，我们需要对它们进行缩放处理，让所有特征都处于一个可比较的量级上。最常用的两种方法是归一化和标准化。</p><p><strong>1. 归一化 (Normalization)</strong></p><ul><li><strong>别名</strong>：最小-最大缩放 (Min-Max Scaling)。</li><li><strong>目标</strong>：将数据线性地缩放到一个固定的区间，通常是 <code>[0, 1]</code>。</li><li><strong>公式</strong>：<code>X_norm = (X - X_min) / (X_max - X_min)</code></li><li><strong>说明</strong>：<code>X</code> 是原始值，<code>X_min</code> 和 <code>X_max</code> 分别是该特征在整个数据集中的最小值和最大值。这种方法非常直观，但对**异常值（Outliers）**非常敏感。</li></ul><p><strong>2. 标准化 (Standardization)</strong></p><ul><li><strong>别名</strong>：Z-score 标准化。</li><li><strong>目标</strong>：将数据转换成一个<strong>均值为 <code>0</code>，标准差为 <code>1</code></strong> 的标准正态分布。</li><li><strong>公式</strong>：<code>X_std = (X - μ) / σ</code></li><li><strong>说明</strong>：<code>μ</code> 是该特征的均值，<code>σ</code> 是其标准差。这种方法比归一化更<strong>鲁棒</strong>，因为它不受异常值的影响。在大多数深度学习和线性模型中，<strong>标准化是更常用、更受推荐的选择</strong>。</li></ul><p>数据预处理是构建任何成功排序模型的<strong>必要前提</strong>。通过独热编码处理类别特征，以及通过归一化/标准化处理连续特征，我们能够为模型提供一份“干净”、“公平”且易于学习的数值化输入，这是保证模型训练稳定、高效并最终取得良好效果的基石。</p><h2 id="第二章：特征交叉的“手动”与“自动”时代-The-Era-of-Manual-vs-Automatic-Feature-Crossing"><strong>第二章：特征交叉的“手动”与“自动”时代 (The Era of Manual vs. Automatic Feature Crossing)</strong></h2><p>在第一章我们认识到，逻辑回归（LR）最大的瓶颈在于其无法自动学习<strong>特征交叉 (Feature Crossing)</strong>。为了让模型捕捉到现实世界中普遍存在的非线性关系，工程师们开始了从“手动”到“自动”探索特征交叉的漫长而关键的旅程。</p><h3 id="2-1-“半自动”的探索：GBDT-LR-组合模型">2.1 “半自动”的探索：GBDT+LR 组合模型</h3><p>在深度学习模型全面流行之前，业界找到了一个极其巧妙的“半自动”解决方案，就是由 Facebook 在2014年提出的 <strong>GBDT+LR</strong> 模型。它不是一个端到端的模型，而是一个两阶段的流程，聪明地利用了两种模型的各自优势。</p><ul><li><strong>核心思想</strong>：让强大的 <strong>GBDT (梯度提升决策树)</strong> 模型充当“<strong>特征工程大师</strong>”，自动完成最困难的特征交叉和特征离散化工作；然后让简单高效的 <strong>LR</strong> 模型充当“<strong>分类专家</strong>”，对这些加工好的高质量特征进行学习和打分。</li><li><strong>工作流程</strong>：<ol><li><strong>训练 GBDT</strong>：首先，在数据集上训练一个 GBDT 模型。GBDT由多棵决策树组成。</li><li><strong>特征变换</strong>：对于每一个输入样本，让它穿过 GBDT 模型中已经训练好的每一棵树。样本在每棵树中都会从根节点走到一个特定的叶子节点。<ul><li><strong>自动特征交叉</strong>：从根节点到叶子节点的这条<strong>路径</strong>，其本身就是一系列特征判断规则的组合（例如，<code>IF age &gt; 30 AND city = '北京'</code>），这构成了一个高阶的、非线性的组合特征。</li></ul></li><li><strong>One-Hot 编码</strong>：模型将样本在<strong>每一棵树</strong>上最终落到的那个<strong>叶子节点的编号 (index)</strong> 收集起来。然后，将这些叶子节点编号进行 <code>One-Hot</code> 编码，形成一个维度很高、但极其稀疏的新特征向量。</li><li><strong>训练 LR</strong>：最后，将这个由 GBDT 生成的、包含了丰富交叉信息的新特征向量，作为输入来训练一个 LR 模型，由它做出最终的 CTR 预估。</li></ol></li><li><strong>总结</strong>：GBDT+LR 是一个里程碑式的组合模型。它 brilliantly地利用了 GBDT 这种树模型的结构特性，来自动化地完成特征交叉这个曾经极度依赖人工经验的脏活累活。它虽然不是一个端到端的优雅模型，但其思想为后续的自动化建模探索铺平了道路，是“半自动”时代最耀眼的明星。</li></ul><h3 id="2-2-“自动交叉”的基石：因子分解机-FM-的原理与突破">2.2 “自动交叉”的基石：因子分解机 (FM) 的原理与突破</h3><p>如果说 GBDT+LR 是用工程组合的方式解决了问题，那么<strong>因子分解机 (Factorization Machine, FM)</strong> 则是从模型结构层面，为“自动特征交叉”提供了一个优雅、高效且影响深远的数学方案。</p><ul><li><strong>核心动机</strong>：FM 的设计直指传统模型在处理<strong>数据稀疏性 (Data Sparsity)</strong> 时的最大痛点。在推荐场景中，绝大多数的 <code>(用户, 物品)</code> 对在训练数据中从未出现过。对于一个从未见过的特征组合 <code>(x_i, x_j)</code>，模型是无法学习出其交叉权重 <code>w_ij</code> 的。</li><li><strong>FM 的突破性思想：因子分解 (Factorization)</strong><br>FM 不再直接学习每一个交叉项的独立权重 <code>w_ij</code>，而是做了一个巧妙的假设：这个交叉权重 <code>w_ij</code> 可以被“分解”为两个低维<strong>隐向量 (Latent Vector)</strong> 的<strong>内积 (Dot Product)</strong>。</li></ul><p><img src="image%202.png" alt="image.png"></p><p>这里，模型为<strong>每一个</strong>原始特征 <code>i</code>，都学习一个低维（例如，维度为 <code>k=16</code>）的稠密向量 <code>v_i</code>。这个 <code>v_i</code> 就是我们现在所熟知的**“Embedding”**。</p><p><strong>1. 起点：FM二阶项的暴力计算公式</strong></p><p>我们从 FM 模型中负责特征交叉的<strong>二阶项</strong>的原始定义出发：</p><p><img src="image%203.png" alt="image.png"></p><p>其中，n 是特征数量，vi是第 i 个特征的 k 维隐向量。</p><p><strong>复杂度分析</strong>：这个公式的计算需要遍历所有不重复的特征对 (i,j)，约有 2n(n−1) 对，复杂度为 O(n2)。对于每一对，都需要计算一次 k 维向量的内积，复杂度为 O(k)。因此，总的计算复杂度为 O(kn2)。对于百万级的特征量，这个计算量是无法接受的。</p><p><strong>核心推导：从 O(kn2) 到 O(kn)</strong></p><p>我们的目标是对上述公式进行化简。</p><p><strong>第一步：展开内积</strong></p><p>首先，我们将内积 ⟨vi,vj⟩ 展开为其定义形式，即对应元素乘积的和：</p><p><img src="image%204.png" alt="image.png"></p><p>其中 vif是向量 vi的第 f 个元素。将此代入原公式，得到：</p><p><img src="image%205.png" alt="image.png"></p><p><strong>第二步：利用对称性，变换求和范围（关键步骤）</strong></p><p>直接处理 ∑j=i+1n 这个求和上下限不固定的部分比较困难。我们观察所有 vif vjf 项构成的矩阵，可以发现以下关系：</p><p><img src="image%206.png" alt="image.png"></p><p>由此，我们可以得到我们想要的求和部分：</p><p><img src="image%207.png" alt="image.png"></p><p>我们将这个恒等式应用到我们的问题上。</p><p><strong>第三步：交换求和顺序，提取公因式</strong></p><p>让我们先只关注一个特定的维度 f。将公式中的 vifxi 看作上面恒等式中的 ai，我们可以得到：</p><p><img src="image%208.png" alt="image.png"></p><p>现在，我们把对所有维度 f (从1到k) 的求和重新引入。将 ∑f=1~k应用到整个表达式中：</p><p><img src="image%209.png" alt="image.png"></p><p>最后，我们将常数1/2提到最外面，就得到了最终的化简公式。经过上述推导，FM二阶项的计算可以表示为：</p><p><img src="image%2010.png" alt="image.png"></p><ul><li>ŷ = w_0 + Σ(w_i * x_i) + ΣΣ(&lt;v_i, v_j&gt; * x_i * x_j)</li><li>这个公式清晰地展示了 FM 的两大部分：<ol><li><strong>一阶部分 <code>Σ(w_i * x_i)</code></strong>：与 LR 完全一样，负责建模每个特征的独立线性效应。</li><li><strong>二阶部分 <code>ΣΣ(&lt;v_i, v_j&gt; * x_i * x_j)</code></strong>：这是 FM 的核心创新，负责自动建模所有特征之间的两两交叉效应。</li></ol></li><li><strong>FM 如何解决稀疏问题？</strong><br>这正是因子分解的魔力所在。即使特征 <code>i</code> 和特征 <code>j</code> 在训练数据中从未同时出现过，模型也依然可以估算出它们的交叉强度。只要特征 <code>i</code> 与其他特征（如 <code>a</code>, <code>b</code>, <code>c</code>）有过共现，模型就能学出它的隐向量 <code>v_i</code>；同理也能学出 <code>v_j</code>。最终，对于未见过的组合 <code>(i, j)</code>，模型可以通过计算它们各自学到的隐向量的内积 <code>&lt;v_i, v_j&gt;</code> 来<strong>泛化</strong>和预测其交叉强度。</li><li><strong>总结</strong>：FM 通过引入<strong>隐向量</strong>和<strong>因子分解</strong>，从根本上解决了稀疏场景下的二阶特征交叉问题。它不仅能自动学习所有特征的两两组合，还能泛化到从未见过的组合上。这个“为每个特征学习一个低维稠密向量来表达其潜在特性”的思想，成为了后续所有深度学习推荐模型的基石。</li></ul><p><strong>复杂度分析</strong>：</p><ol><li>我们来看括号内的计算。对于每一个维度 f，我们需要先计算 ∑i=1nvifxi。这个求和需要遍历 n 个特征，其复杂度为 O(n)。（对于稀疏特征，只需遍历非零项，复杂度更低）。</li><li>这个计算需要对 f 从 1 到 k 循环 k 次。</li><li>在循环的每一步中，计算平方、减法等都是常数时间操作。</li></ol><h3 id="2-3-核心概念：理解“阶”的含义（一阶与二阶）">2.3 核心概念：理解“阶”的含义（一阶与二阶）</h3><p>在特征交叉的讨论中，“阶” (Order) 是一个核心术语，它精确地定义了特征之间交互的复杂度。我们可以将排序模型的预测公式类比为一个大的多项式方程，其中的变量就是模型的输入特征 xi。</p><ul><li><strong>一阶 (First-Order)</strong><ul><li><strong>数学形式</strong>: wixi</li><li><strong>定义</strong>: 一个特征项中，只包含<strong>一个</strong>独立的原始特征变量。</li><li><strong>意义</strong>: 建模<strong>单个特征的独立、线性效应</strong>。它反映了该特征本身对最终结果的贡献大小，而不考虑它与其他特征的任何组合。例如，在预测商品购买概率时，价格 xprice 越高，购买概率可能越低，一阶项 wpricexprice 就负责捕捉这种直接的、线性的关系。逻辑回归以及FM的线性部分，都是由一阶项构成的。</li></ul></li><li><strong>二阶 (Second-Order)</strong><ul><li><strong>数学形式</strong>: wij xi xj</li><li><strong>定义</strong>: 一个特征项中，包含<strong>两个</strong>独立的原始特征变量的乘积。</li><li><strong>意义</strong>: 建模<strong>两个特征之间的交互效应 (Interaction Effect)</strong> 或<strong>交叉效应 (Crossing Effect)</strong>。它捕捉的是当两个特征<strong>同时出现</strong>时，所产生的 <code>1+1 ≠ 2</code> 的特殊效果。例如，特征 <code>user_gender=女性</code> (xi) 和特征 <code>item_category=美妆</code> (xj)，它们各自的一阶效应可能并不突出，但当它们同时出现时（即 xixj=1），会产生一个非常强烈的正向购买信号。这个强烈的信号就是由二阶项捕捉的。FM模型的核心创新，就是高效地对所有特征进行二阶交叉建模。</li></ul></li></ul><h3 id="2-4-核心概念：Embedding-——-将万物向量化">2.4 核心概念：Embedding —— 将万物向量化</h3><p>如果说特征交叉是排序模型的核心“战术”，那么 <strong>Embedding</strong> 则是驱动这一切的、最核心的“技术引擎”。它是连接现实世界中离散符号（如ID）与神经网络数学世界的桥梁。</p><p><strong>1. 为什么需要 Embedding？—— One-Hot编码的局限</strong></p><p>我们之前提到，处理 <code>user_id</code>, <code>item_id</code> 这类类别特征的标准方法是<strong>独热编码 (One-Hot Encoding)</strong>。这种方法虽然解决了类别间无序性的问题，但它有两个致命缺陷：</p><ul><li><strong>维度灾难与稀疏性</strong>：当类别数量巨大时（如上亿的物品ID），独热编码会产生一个维度高达上亿的、并且只有一个位置是1的极其稀疏的向量。这在计算和存储上都是巨大的挑战。</li><li><strong>无法表达语义相似性</strong>：更重要的是，任意两个不同类别的独热向量都是<strong>正交</strong>的，它们的内积为0。这意味着模型无法从向量层面得知 <code>iPhone 17</code> 和 <code>iPhone 17 Pro</code> 是相似的，也无法知道 <code>iPhone 17</code> 和 <code>牛肉干</code> 是不相似的。这严重限制了模型的泛化能力。</li></ul><p><strong>2. Embedding 的解决方案：从稀疏到稠密</strong></p><p>Embedding 技术通过一个“<strong>查询表 (Lookup Table)</strong>”的思路，将高维稀疏的独热向量，映射成一个低维稠密的浮点数向量。</p><ul><li><strong>定义</strong>：一个 Embedding 是一个<strong>可学习的</strong>、<strong>低维的</strong>、<strong>稠密的</strong>向量表示。</li><li><strong>工作原理</strong>：<ol><li>我们可以想象有一个巨大的查询矩阵，我们称之为 <strong>Embedding 矩阵</strong>。这个矩阵的行数等于该特征的词汇表大小（如总物品数），列数等于我们设定的 Embedding 维度 k（如 k=64）。</li><li>每一个物品ID，都对应这个矩阵中的<strong>一行</strong>。这一行就是一个 k 维的稠密向量。</li><li>在模型训练开始时，这个矩阵的数值是随机初始化的。在训练过程中，模型通过反向传播算法，不断地学习和调整矩阵中的数值。</li></ol></li></ul><p><strong>3. Embedding 的核心价值</strong></p><ul><li><strong>降维与信息压缩</strong>：将千万甚至亿级别的稀疏维度，压缩到几十或几百维的稠密向量中，极大地提升了计算和存储效率。</li><li><strong>捕捉语义相似性（最关键）</strong>：在训练过程中，那些经常被相似用户群体互动、或者本身属性相似的物品，它们的 Embedding 向量在向量空间中的位置会变得越来越<strong>接近</strong>。最终，“iPhone 17”和“iPhone 17 Pro”的向量会很相似，而它们与“牛肉干”的向量会相距很远。Embedding 成功地<strong>将行为和内容上的相似性，转化为了空间几何上的相近性</strong>。</li><li><strong>赋能泛化</strong>：正是因为捕捉到了语义相似性，模型才能实现泛化。模型从“iPhone 17”上学到的知识（比如购买它的用户画像），可以很自然地迁移应用到“iPhone 17 Pro”上，因为它俩的向量表示很接近。这是独热编码完全无法做到的。</li></ul><h3 id="2-5-核心概念：防止过拟合的利器-——-正则化-L1-vs-L2">2.5 核心概念：防止过拟合的利器 —— 正则化 (L1 vs L2)</h3><p>在模型训练中，我们经常会遇到一个棘手的问题，叫做<strong>过拟合 (Overfitting)</strong>。</p><ul><li><strong>什么是过拟合？</strong><br>简单来说，就是模型“太努力”了，它在训练数据上表现得近乎完美，但对于从未见过的新数据（测试数据），表现却一塌糊涂。这就像一个只会“死记硬背”的学生，他能完美回答所有练习册上的原题，但一到真正的考试，遇到新题型就完全不会了。</li><li><strong>为什么会过拟合？</strong><br>通常是因为模型过于复杂，而训练数据又不足以支撑这种复杂性。模型不仅学习到了数据中普适的“规律”，还把训练数据特有的“噪声”和“偶然性”也当作规律记了下来。这在模型参数上的体现，往往是某些特征的权重（wi）变得<strong>异常巨大</strong>。</li></ul><p><strong>正则化 (Regularization)</strong> 就是一类专门用来对抗过拟合、提升模型<strong>泛化能力</strong>的技术。其核心思想是在模型的原始损失函数（如对数损失）的基础上，增加一个<strong>惩罚项 (Penalty Term)</strong>，以此来“惩罚”或“约束”模型的权重，不让它们变得过大。</p><p><strong><code>*新的损失函数=原始损失函数+惩罚项*</code></strong></p><p>模型在优化时，就必须在“降低原始损失（拟合数据）”和“降低惩罚项（保持权重简单）”之间做出权衡。最常见的两种正则化方法就是 L1 和 L2 正则化。</p><p><strong>1. L1 正则化 (Lasso Regression)</strong></p><ul><li><p><strong>惩罚项公式</strong>：L1 正则化的惩罚项是所有模型权重 wi 的<strong>绝对值之和</strong>，再乘以一个超参数 λ。</p><p><img src="image%2011.png" alt="image.png"></p><p>这里的 λ 控制着正则化的强度，λ 越大，惩罚越重。</p></li><li><p><strong>核心效果：稀疏性 (Sparsity)</strong><br>L1 正则化最显著的特点是它能够产生<strong>稀疏解</strong>，也就是说，它会倾向于将许多<strong>不那么重要的特征的权重直接置为精准的 0</strong>。</p></li><li><p><strong>为什么能产生稀疏解？</strong><br>从优化的角度看，L1 惩罚项的“菱形”约束边界使得损失函数在优化时，最优解更容易出现在坐标轴的顶点上，而顶点处恰好意味着某些特征的权重为0。</p></li><li><p><strong>主要作用</strong>：由于 L1 正则化能将权重归零，它也常被用作一种<strong>自动的特征选择 (Feature Selection)</strong> 方法。在拥有海量特征，但怀疑其中很多特征是无关紧要的场景下，L1 正则化非常有用。</p></li></ul><p><strong>2. L2 正则化 (Ridge Regression)</strong></p><ul><li><p><strong>惩罚项公式</strong>：L2 正则化的惩罚项是所有模型权重 wi 的<strong>平方和</strong>，再乘以超参数 λ。</p><p><img src="image%2012.png" alt="image.png"></p></li><li><p><strong>核心效果：权重衰减 (Weight Decay)</strong><br>L2 正则化最显著的特点是它会使得模型的权重<strong>整体上都变得比较小，趋近于0，但很难精确等于0</strong>。</p></li><li><p><strong>为什么不会归零？</strong><br>L2 惩罚项的“圆形”约束边界比较平滑，最优解很难恰好落在坐标轴上。从梯度上看，L2 惩罚项的梯度与权重大小成正比，当权重越接近0时，它产生的惩罚梯度也越小，因此很难将权重“推到”绝对的0点。</p></li><li><p><strong>主要作用</strong>：L2 正则化是<strong>应用最广泛</strong>的正则化方法。它通过惩罚较大的权重，使得模型的决策不再过分依赖于少数几个特征，而是让所有特征都贡献一点力量，从而使得模型更加<strong>平滑</strong>和<strong>鲁棒</strong>，拥有更好的泛化能力。在深度学习中，它几乎是默认的正则化选项。</p></li></ul><p><strong>总结对比</strong></p><table><thead><tr><th>对比维度</th><th><strong>L1 正则化 (Lasso)</strong></th><th><strong>L2 正则化 (Ridge)</strong></th></tr></thead><tbody><tr><td><strong>惩罚公式</strong></td><td>λsum(w_i)</td><td>λsum(w_i^2)</td></tr><tr><td><strong>对权重的影响</strong></td><td>倾向于将部分权重<strong>精确地置为 0</strong></td><td>倾向于让所有权重都<strong>变得很小，趋近于 0</strong></td></tr><tr><td><strong>核心特性</strong></td><td><strong>稀疏性 (Sparsity)</strong></td><td><strong>权重衰减 (Weight Decay)</strong></td></tr><tr><td><strong>主要应用场景</strong></td><td><strong>特征选择</strong>，创造稀疏模型</td><td><strong>通用的过拟合防治</strong>，提升模型泛化能力</td></tr><tr><td><strong>几何约束</strong></td><td>菱形或更高维度的多面体</td><td>圆形或更高维度的超球面</td></tr></tbody></table><h2 id="第三章：深度学习排序模型的“三驾马车”-The-“Big-Three”-of-Deep-Ranking-Models"><strong>第三章：深度学习排序模型的“三驾马车” (The “Big Three” of Deep Ranking Models)</strong></h2><h3 id="3-1-Wide-Deep-“记忆”与“泛化”的首次结合">3.1 Wide &amp; Deep: “记忆”与“泛化”的首次结合</h3><p>由 Google 在2016年提出的 <strong>Wide &amp; Deep</strong> 模型，是深度学习在推荐系统领域应用的开创性工作。它并未盲目地用一个“大而全”的深度网络去解决所有问题，而是深刻地洞察到推荐系统需要同时具备两种看似矛盾的能力：<strong>记忆（Memorization）和泛化（Generalization）</strong>。</p><ul><li><strong>记忆能力 (Memorization)</strong><ul><li><strong>定义</strong>：指模型学习并利用历史数据中频繁共现的、非常具体和直接的规则的能力。它善于“记住”那些显而易见的、强相关的模式。</li><li><strong>例子</strong>：模型能够牢牢记住“<strong>用户下载过A应用，又看到了A应用的辅助工具B，那么该用户点击B的概率极高</strong>”这样的强关联规则。</li><li><strong>实现</strong>：线性模型（如逻辑回归）是实现“记忆”的绝佳工具，因为它能为每一个具体的特征组合学习一个独立的权重。</li></ul></li><li><strong>泛化能力 (Generalization)</strong><ul><li><strong>定义</strong>：指模型探索和发现数据中稀疏的、从未见过的、更“抽象”的模式的能力。它善于“泛化”知识，从而提高推荐的多样性和惊喜度。</li><li><strong>例子</strong>：模型通过学习大量的用户行为，可能会发现“<strong>喜欢看科幻电影的用户</strong>”和“<strong>喜欢购买硬核科技产品的用户</strong>”在底层兴趣上存在关联，从而为一个科幻迷推荐一款他从未见过的新款无人机。</li><li><strong>实现</strong>：深度神经网络（DNN）通过将高维稀疏特征映射到低维稠密的 <strong>Embedding</strong> 向量中，非常擅长发现这种潜在的、语义层面的关联。</li></ul></li></ul><p><strong>Wide &amp; Deep 的核心架构</strong></p><p>为了将这两种能力有机地结合起来，Wide &amp; Deep 设计了一种优雅的<strong>并行结构</strong>，由 <strong>Wide</strong> 和 <strong>Deep</strong> 两个部分组成，并进行<strong>联合训练 (Joint Training)</strong>。</p><ol><li><strong>Wide 部分 (负责记忆)</strong><ul><li><strong>结构</strong>：一个简单的广义线性模型，等价于一个逻辑回归。</li><li><strong>输入</strong>：<strong>人工设计的、高维稀疏的交叉特征</strong>。这是 Wide 部分的关键，其效果高度依赖算法工程师的经验来手动创造出有意义的特征组合，例如 <code>AND(gender='female', item_category='makeup')</code>。</li></ul></li><li><strong>Deep 部分 (负责泛化)</strong><ul><li><strong>结构</strong>：一个标准的前馈神经网络 (DNN/MLP)。</li><li><strong>输入</strong>：原始的连续特征，以及被转换成低维稠密 <strong>Embedding</strong> 向量的类别特征（如 <code>user_id</code>, <code>item_id</code>）。</li></ul></li><li><strong>输出与联合训练</strong><ul><li><p>模型的最终输出由 Wide 部分的输出与 Deep 部分的输出<strong>相加</strong>，然后共同经过一个 Sigmoid 函数得到。其简化后的公式可以理解为：</p><p><img src="image%2013.png" alt="image.png"></p></li><li><p>整个模型是<strong>端到端 (End-to-End)</strong> 训练的。最终的损失梯度会同时反向传播到 Wide 和 Deep 两个部分，并同时更新两边的参数。这比 GBDT+LR 的两阶段训练模式更为先进和统一。</p></li></ul></li></ol><p>损失函数：BCELOSS</p><p><img src="image%2014.png" alt="image.png"></p><ul><li>N 是批次大小 (batch size)。</li><li>yi 是第 i 个样本的真实标签（点击为1，未点击为0）。</li><li>y^i 是模型对第 i 个样本<strong>最终预测</strong>的点击概率。</li></ul><p>y_hat = sigmoid(z_deep+z_DNN+b), z_wide=w_wide x_wide (LR的线性部分), z_deep=DNN(x_deep), x_deep是经过embedding的稠密向量。得到预测的y_hat后在BCE中和gt作比较。</p><p><strong>打个比方</strong>：<br>整个训练过程就像一个<strong>联合项目</strong>。</p><ol><li><strong>项目交付 (前向传播)</strong>：Wide团队和Deep团队各自完成自己的部分，最后将成果汇总，交付一个最终产品（预测值 y^）。</li><li><strong>客户反馈 (计算损失)</strong>：根据产品的好坏（与真实标签 y 的差距），得到一个总的项目评分（损失 L）。</li><li><strong>责任分配 (反向传播)</strong>：项目经理（反向传播算法）根据总评分，分析出两个团队各自的贡献和不足，并给出具体的改进意见（梯度）。</li><li><strong>内部优化 (权重更新)</strong>：<ul><li>Wide团队的经理（FTRL优化器）拿到意见后，采用“末位淘汰”式的管理方法，将表现差的员工（权重）直接“开除”（置为0）。</li><li>Deep团队的经理（Adam优化器）拿到意见后，采用“个性化辅导”的管理方法，根据每个员工的历史表现，动态调整对他的培养力度（自适应学习率）。</li></ul></li></ol><h3 id="3-2-DeepFM-FM与DNN的无缝集成">3.2 DeepFM: FM与DNN的无缝集成</h3><p>DeepFM (2017) 的诞生，旨在解决其两大“前辈”——<strong>Wide &amp; Deep</strong> 和 <strong>FNN</strong>——各自的核心缺陷，从而打造一个既不需要人工特征工程，又能端到端训练，同时高效学习所有阶特征交叉的强大模型。</p><p><strong>1. DeepFM 的动机：站在巨人的肩膀上解决问题</strong></p><p>为了理解 DeepFM 的精妙之处，我们必须先看清它要解决的问题：</p><ul><li><strong>Wide &amp; Deep 的痛点</strong>：其 <strong>Wide</strong> 部分虽然能够很好地“记忆”低阶交叉特征，但这些交叉特征需要<strong>依赖算法工程师手动设计</strong>。这是一个巨大的瓶颈，费时费力且高度依赖经验，无法穷举所有有用的组合。</li><li><strong>FNN 的痛点</strong>：FNN (Factorization-machine supported Neural Network) 尝试用预训练的 FM 模型来初始化 DNN 的 Embedding，以期引入低阶交叉信息。但它的训练是<strong>两阶段的、非端到端的</strong>，这意味着 FM 学到的 Embedding 无法在 DNN 的训练中得到微调。同时，低阶特征信息在送入深层网络后，很容易被复杂的非线性变换所“稀释”，导致学习效率不高。</li></ul><p><strong>DeepFM 的核心目标</strong>：设计一个<strong>端到端</strong>的统一模型，它既能像 FM 一样自动学习低阶特征交叉，又能像 DNN 一样自动学习高阶特征交叉，并且完全<strong>不需要任何人工特征工程</strong>。</p><p><strong>2. DeepFM 的核心架构：“共享”与“并行”的艺术</strong></p><p>DeepFM 的结构设计堪称典范，它由并行的 <strong>FM 组件</strong>和 <strong>DNN 组件</strong>组成，而这两大组件的“无缝集成”，依赖于一个核心设计——<strong>共享 Embedding 层</strong>。</p><p><em><center>DeepFM 简化架构图</center></em></p><p><strong>a) 共享的基石：唯一的 Embedding 层</strong><br>这是 DeepFM 相对于 FNN 最关键的改进。模型中只有一个全局的 Embedding 矩阵。所有输入的稀疏类别特征（如 <code>user_id</code>, <code>item_id</code>）都会被映射到这个共享的 Embedding 空间中，得到各自的低维稠密向量。</p><p>这个共享的 Embedding 层，是连接 FM 和 DNN 两大组件的<strong>唯一纽带</strong>。</p><p><strong>b) FM 组件：低阶特征交叉的“自动”专家</strong><br>这个部分的作用等同于 Wide &amp; Deep 中的 “Wide” 部分，但它实现了完全的自动化。它直接在共享的 Embedding 向量上进行运算，同时负责两部分计算：</p><ul><li><strong>一阶特征 (1st-order Interactions)</strong>：对所有原始特征的线性加权求和，与逻辑回归完全相同。</li></ul><p><img src="image%2015.png" alt="image.png"></p><p><strong>二阶特征 (2nd-order Interactions)</strong>：对每一对特征的 Embedding 向量进行<strong>内积 (Dot Product)</strong> 运算，以此来建模所有特征的两两交叉。</p><p><img src="image%2016.png" alt="image.png"></p><p>这里的v_i 和v_j 正是从共享 Embedding 层中查询得到的向量。</p><p><strong>c) DNN 组件：高阶特征交叉的“黑盒”大师</strong><br>这个部分负责探索那些更复杂、更抽象的高阶非线性关系。</p><ul><li><strong>输入</strong>：将所有稀疏特征从<strong>共享 Embedding 层</strong>中查到的 Embedding 向量，与原始的稠密特征（如有）进行<strong>拼接 (Concatenate)</strong>，形成一个长长的、稠密的向量。</li><li><strong>结构</strong>：一个标准的多层感知机 (MLP)，包含若干个全连接层和非线性激活函数（如 ReLU）。</li></ul><p><img src="image%2017.png" alt="image.png"></p><ul><li><strong>输出</strong>：经过多层变换后，最终输出一个代表了所有高阶交叉信息的 Logit。</li></ul><p><strong>d) 最终的“无缝”集成</strong></p><p>DeepFM 的集成方式极其简单和高效：将 FM 组件的输出（包含一阶和二阶）和 DNN 组件的输出</p><p><strong>直接相加</strong>，然后将总和送入一个 Sigmoid 函数，得到最终的 CTR 预测值。</p><p><img src="image%2018.png" alt="image.png"></p><p>这种简单的相加操作，意味着两个组件在以一种<strong>并行、互补</strong>的方式工作，共同对最终的预测结果做出贡献。</p><p><strong>3. DeepFM 的突破性优势</strong></p><ul><li><strong>真正的端到端学习</strong>：得益于共享 Embedding，梯度可以从最终的损失函数，无阻碍地同时反向传播到 FM 和 DNN 两个部分，并最终<strong>共同更新同一份 Embedding 参数</strong>。这意味着 Embedding 的学习同时受到了来自“低阶交叉任务”和“高阶交叉任务”的双重指导，使其表示能力更强、学习更充分。</li><li><strong>完全无需人工特征工程</strong>：FM 组件完美地替代了 Wide &amp; Deep 中需要手动设计的 Wide 部分，实现了所有低阶交叉的自动化。</li><li><strong>兼具效率与性能</strong>：模型中不包含任何复杂的特殊结构，FM 部分有线性时间复杂度的解法，DNN 部分也是标准的 MLP，整个模型的训练和预测效率都非常高，非常适合工业界大规模部署。</li></ul><p><strong>总结：</strong><br>DeepFM 之所以是“重中之重”，因为它<strong>用最简洁、最优雅的工程设计，完美地解决了前代模型的关键痛点</strong>。它通过“共享 Embedding + 并行结构”这一核心思想，将 FM 对低阶特征交叉的精准刻画能力，与 DNN 对高阶特征交叉的强大泛化能力无缝地结合在了一个可以端到端训练的统一框架下，并且完全实现了自动化。这使其成为了后续非序列深度学习排序模型的一个<strong>黄金标准和核心范式</strong>。</p><h3 id="3-3-DCN-xDeepFM-对“显式特征交叉”的极致探索">3.3 DCN &amp; xDeepFM: 对“显式特征交叉”的极致探索</h3><p><strong>1. 动机：打开 DNN 的“黑箱”</strong></p><p>DeepFM 的 DNN 部分虽然强大，但它学习高阶特征交叉的过程是**“隐式”的 (Implicit)**。也就是说，特征交叉是通过多层复杂的、不可控的非线性变换自动发生的，我们很难知道模型具体学习到了哪些阶数的、哪些有效的交叉组合。这种“黑箱”式的学习可能有以下缺点：</p><ul><li><strong>效率不高</strong>：可能需要大量的数据和参数才能学习到一些相对简单的交叉关系。</li><li><strong>不可控</strong>：我们无法控制模型学习交叉的阶数。</li></ul><p>因此，一个新的研究方向应运而生：我们能否设计一种<strong>全新的网络结构</strong>，它既能<strong>自动</strong>学习高阶交叉，又能让这个过程变得<strong>显式 (Explicit)</strong> 和<strong>高效可控</strong>？DCN 和 xDeepFM 就是这个方向上最重要的两个探索者。</p><hr><p><strong>2. DCN (Deep &amp; Cross Network, 2017): 显式交叉的开创者</strong></p><p>DCN 的架构直接脱胎于 Wide &amp; Deep，它保留了并行的 Deep 部分，但用一个创新设计的 <strong>Cross Network</strong> 替换了需要人工特征工程的 Wide 部分。</p><ul><li><p><strong>核心组件：Cross Network</strong></p><ul><li><strong>目标</strong>：以一种高效、可控的方式，让特征交叉的阶数随着网络层数的增加而自动增加。</li><li><strong>核心公式</strong>：Cross Network 的每一层都遵循一个独特的更新规则：</li></ul><p><img src="image%2019.png" alt="image.png"></p></li></ul><p>我们来拆解这个公式：</p><ul><li>xl 是第 l 层的输出向量。</li><li>x0 是整个网络最开始的<strong>原始输入向量</strong>（通常是所有特征 Embedding 的拼接）。</li><li><code>x0xlTwl</code> 是最关键的<strong>交叉操作</strong>。它将上一层的输出 xl 与原始输入 x0 进行了一次显式的交叉，并由一个可学习的权重向量 wl 来控制交叉的模式。</li><li>+xl 是一个<strong>残差连接 (Residual Connection)</strong>。这是一个点睛之笔，它保证了在学习更高阶交叉的同时，所有在前面层已经学到的低阶交叉信息都能被完整地保留下来，极大地稳定了训练过程。</li></ul><p><strong>工作原理</strong>：</p><ul><li><strong>第1层</strong>：x0 与自身进行交叉，产出所有特征的<strong>二阶</strong>交叉信息。</li><li><strong>第2层</strong>：x0 与包含了二阶信息的 x1 进行交叉，产出<strong>三阶</strong>交叉信息。</li><li>以此类推，一个深度为 L 的 Cross Network 能够显式地建模所有从一阶到 L+1 阶的特征交叉。</li></ul><p><strong>DCN 的局限</strong>：<br>DCN 的交叉是所谓的 <strong>bit-wise (比特级/元素级)</strong>。交叉项 <code>x_l^T w_l</code> 的结果是一个标量（单个数值），这个标量再去乘以整个原始输入向量 x0。这种方式虽然实现了交叉，但表达能力有限，不够灵活。</p><p><strong>3. xDeepFM (eXtreme DeepFM, 2018): 从 bit-wise 到 vector-wise 的进化</strong></p><p>xDeepFM 在 DCN 的思想上更进一步，它认为特征交叉应该发生在更有意义的<strong>向量级别 (vector-wise)</strong>，而不是比特级别。为此，它设计了一个全新的核心组件——<strong>CIN (Compressed Interaction Network)</strong>。</p><ul><li><strong>核心组件：CIN (压缩交互网络)</strong><ul><li><strong>目标</strong>：在 <strong>Embedding 向量</strong>的粒度上，显式地、逐层地构建高阶特征交叉。</li><li><strong>工作原理 (概念)</strong>：<ol><li><strong>输入</strong>：CIN 的输入不再是一个拍平的长向量，而是一个矩阵 X0，其每一行都是一个特征的 Embedding 向量。</li><li><strong>逐层交叉</strong>：CIN 的第 k 层，会计算第 k−1 层的输出矩阵 Xk−1 与原始输入矩阵 X0 之间，<strong>每一对 Embedding 向量</strong>的<strong>哈达玛积 (Element-wise Product, ⊙)</strong>。这就在向量层面生成了更高一阶的交叉特征。</li><li><strong>压缩</strong>：由于上述操作会产生巨量的交叉向量，CIN 会通过类似卷积的操作（使用一组可学习的滤波器）对这些交叉向量进行加权求和，将其“压缩”成一个更紧凑的矩阵 Xk，作为下一层的输入。</li></ol></li><li><strong>最终输出</strong>：xDeepFM 的完整架构通常包含三部分：<strong>传统的线性部分</strong>（负责一阶）、<strong>CIN 部分</strong>（负责显式的、vector-wise 的高阶交叉）和<strong>传统的 DNN 部分</strong>（负责隐式的、bit-wise 的高阶交叉），将三部分的输出融合后进行最终预测。</li></ul></li></ul><p><strong>总结与对比</strong></p><table><thead><tr><th>模型</th><th><strong>高阶交叉方式</strong></th><th><strong>显式/隐式</strong></th><th><strong>交叉粒度</strong></th><th><strong>核心思想</strong></th></tr></thead><tbody><tr><td><strong>DeepFM</strong></td><td>标准 DNN (MLP)</td><td><strong>隐式</strong></td><td>Bit-wise</td><td>通过多层非线性变换“黑箱”学习高阶关系。</td></tr><tr><td><strong>DCN</strong></td><td>Cross Network</td><td><strong>显式</strong></td><td>Bit-wise</td><td>通过独特的残差交叉结构，逐层、可控地增加交叉阶数。</td></tr><tr><td><strong>xDeepFM</strong></td><td>CIN (压缩交互网络)</td><td><strong>显式</strong></td><td><strong>Vector-wise</strong></td><td>在更有意义的 Embedding 向量粒度上，进行显式交叉。</td></tr></tbody></table><h3 id="3-4-核心概念：共享-Embedding-Shared-Embedding-的原理与作用">3.4 核心概念：共享 Embedding (Shared Embedding) 的原理与作用</h3><p><strong>1. 原理：一份输入，多路更新</strong></p><p>共享 Embedding 指的是，模型中不同的部分（例如 DeepFM 中的 FM 组件和 DNN 组件）在处理同一个类别特征时，会查询和使用<strong>同一个、全局唯一的 Embedding 矩阵</strong>。</p><ul><li><p><strong>前向传播</strong>：当一个类别特征（如 <code>item_id=123</code>）输入时，模型从共享的 Embedding 矩阵中查出其对应的向量 v123。接着，这个向量的<strong>副本</strong>会同时被送往 FM 部分和 DNN 部分，参与各自的计算。</p></li><li><p><strong>反向传播（核心）</strong>：在计算完最终的损失后，梯度会从损失函数反向传播。FM 部分和 DNN 部分会各自计算出它们对向量 v123 的梯度贡献，我们称之为 ∇FM 和 ∇DNN。最终，作用于共享 Embedding 矩阵中那个唯一的 v123 向量的更新梯度，是<strong>所有部分梯度贡献的总和</strong>：</p><p>∇total=∇FM+∇DNN</p><p>这意味着，Embedding 向量的学习同时受到了“低阶交叉任务（来自FM）”和“高阶交叉任务（来自DNN）”的双重指导。</p></li></ul><p><strong>2. 作用：为何共享如此强大？</strong></p><ul><li><strong>参数高效</strong>：这是最直接的优势。模型只需要维护一份 Embedding 矩阵，而不是每个组件都维护一份，极大地减少了模型的总参数量，节省了内存，并加快了训练速度。</li><li><strong>学习更丰富的表示（最重要的优势）</strong>：这是共享 Embedding 的精髓所在。由于 Embedding 的更新同时接收来自不同模型组件的信号，它被迫学习一种能够<strong>同时满足多种需求</strong>的表示。FM 部分会“教”它学习显式的、低阶的共现关系；而 DNN 部分则会“教”它学习抽象的、高阶的非线性关系。最终学到的 Embedding 表示会比单一任务学到的更丰富、更鲁棒。</li><li><strong>加速收敛与提升泛化</strong>：更丰富的梯度信号有助于 Embedding 更快地学习到有意义的表示。同时，对于长尾的、交互稀疏的特征，即使 DNN 很难有效学习，FM 部分简单的共现信号依然可以对其进行有效的更新，从而提升了模型的泛化能力。</li><li><strong>实现端到端训练</strong>：共享 Embedding 是将多个独立模型组件“粘合”在一起，实现真正端到端联合训练的关键纽带。</li></ul><h3 id="3-5-核心概念：激活函数的作用与演进-ReLU-PReLU-Dice">3.5 核心概念：激活函数的作用与演进 (ReLU, PReLU, Dice)</h3><p><strong>1. 激活函数的核心作用：引入非线性</strong></p><p>激活函数是神经网络的“灵魂”所在。如果没有激活函数，一个无论多深的神经网络，本质上都只是一系列线性变换的叠加，其最终效果等价于一个简单的线性模型。<strong>激活函数的核心作用就是引入非线性</strong>，它对每一层线性变换后的结果进行“弯曲”或“折叠”，从而赋予了网络拟合任意复杂函数的能力。</p><p><strong>2. 激活函数的演进之路</strong></p><p>在排序模型中，激活函数的演进主要围绕着如何让训练更稳定、更高效展开。</p><ul><li><p><strong>ReLU (Rectified Linear Unit)</strong></p><ul><li><strong>公式</strong>：<code>f(x)=max(0,x)</code></li><li><strong>地位</strong>：现代神经网络的<strong>默认标准激活函数</strong>。它计算极其简单，并且在输入为正数时梯度恒为1，极大地缓解了深度网络中的“梯度消失”问题。</li><li><strong>缺陷</strong>：存在**“死亡ReLU问题” (Dying ReLU Problem)**。如果一个神经元的输入在训练中持续为负，那么它的输出和梯度将永远是0，导致该神经元无法再进行学习。</li></ul></li><li><p><strong>PReLU (Parametric ReLU)</strong></p><ul><li><strong>公式</strong>：<code>f(s)=max(αs,s)</code>，其中 α 是一个可学习的参数。</li><li><strong>改进</strong>：PReLU 是对 ReLU 的直接改进。它通过引入一个可学习的、微小的负半轴斜率 α，实现了“泄漏 (Leaky)”。当输入为负时，梯度不再是0而是 α，从而<strong>解决了“神经元死亡”的问题</strong>。因为它比固定的 Leaky ReLU 更灵活，所以通常效果更好。</li></ul></li><li><p><strong>Dice (Data Adaptive Activation Function)</strong></p><ul><li><strong>动机</strong>：PReLU 解决了神经元死亡问题，但它的状态切换点依然是<strong>固定在0</strong>。在数据分布剧烈变化的工业级场景中（即“内部协变量偏移”问题），一个固定的切换点并非最优。</li><li><strong>核心思想</strong>：让激活函数的行为能够<strong>自适应数据的分布</strong>。</li></ul><p><img src="image%2020.png" alt="image.png"></p><ul><li><strong>实现方式</strong>：Dice 可以被看作一个在 <code>f(s)=s</code> 和 <code>f(s)=αs</code> 之间的“软开关”。这个开关的位置，不再是固定的0点，而是通过一个基于<strong>批归一化 (Batch Normalization)</strong> 思想的门控函数，<strong>动态地调整到了当前这批 (mini-batch) 数据的均值 E[s] 处,而不是ReLU和PReLU的0处</strong>。</li></ul><p><img src="image%2021.png" alt="image.png"></p><ul><li><strong>意义</strong>：Dice 通过让激活函数的“激活点”主动去追随数据的分布中心，极大地提升了模型在非平稳数据分布下的训练稳定性和效率。它是为大规模、动态变化的推荐场景量身定制的、更“智能”的激活函数。</li></ul></li></ul><h2 id="第四章：用户行为序列建模时代-The-Era-of-User-Behavior-Sequence-Modeling"><strong>第四章：用户行为序列建模时代 (The Era of User Behavior Sequence Modeling)</strong></h2><h3 id="4-1-DIN-Deep-Interest-Network-开启注意力机制的应用">4.1 DIN (Deep Interest Network): 开启注意力机制的应用</h3><p>在 DIN (2018) 诞生之前，深度学习排序模型（如 Wide &amp; Deep, DeepFM）在处理用户历史行为序列时，普遍采用一种简单粗暴的方式：<strong>简单池化 (Pooling)</strong>。</p><p><strong>1. DIN 的动机：挑战“兴趣大杂烩”</strong></p><ul><li><strong>当时的主流做法</strong>：将用户历史上交互过的所有物品（比如点击过的50个商品）的 Embedding 向量取出来，然后通过一个池化操作（如 <code>Average Pooling</code> 或 <code>Max Pooling</code>），将它们压缩成一个<strong>固定长度的向量</strong>，以此作为代表该用户“兴趣”的唯一输入。</li><li><strong>这种做法的致命缺陷</strong>：它将用户丰富、多样、且在不同场景下关注点不同的兴趣，粗暴地“平均”成了一个大杂烩。</li><li><strong>一个经典的例子</strong>：一位年轻的母亲，她的购物历史里既有大量的“连衣裙”、“高跟鞋”、“手提包”，也有“婴儿纸尿裤”和“奶粉”。<ul><li>当系统要向她推荐一款新的**“手提包”<strong>时，一个经过平均池化的兴趣向量，会因为包含了大量的母婴用品信息而变得“面目模糊”。“母婴”兴趣的信号会严重</strong>稀释和干扰**她此刻对“时尚”的兴趣表达。</li></ul></li><li><strong>DIN 的核心洞察</strong>：用户的兴趣表示，不应该是一个一成不变的静态向量，而应该是<strong>动态的、自适应的</strong>。它应该<strong>根据当前要预测的候选商品（Target Item）的不同而变化</strong>。</li></ul><p><strong>2. DIN 的核心架构：目标感知的注意力机制</strong></p><p>为了实现上述目标，DIN 首次将<strong>注意力机制 (Attention Mechanism)</strong> 引入了 CTR 预估领域，设计了一套全新的用户兴趣表示方法。</p><p><strong>a) 激活单元 (Activation Unit) - 注意力网络</strong><br>DIN 的核心是一个小型的神经网络，即“激活单元”，它负责计算<strong>每一个历史行为</strong>与<strong>当前候选商品</strong>之间的“相关性得分”，也就是<strong>注意力权重</strong>。</p><ul><li><strong>输入</strong>：对于用户历史中的第 i 个商品，激活单元的输入包括：<ol><li>历史商品 i 的 Embedding 向量 ei。</li><li>候选商品（广告） a 的 Embedding 向量 ea。</li><li>两者的交互特征：为了让模型更好地学习它们的关联性，通常还会把它们的<strong>差值</strong> <code>(ea−ei)</code> 和<strong>元素积</strong> <code>(ea⊙ei)</code> 也作为输入。</li></ol></li><li><strong>结构</strong>：将上述所有向量拼接起来，送入一个浅层的 MLP (多层感知机)。</li><li><strong>输出</strong>：MLP 最终输出一个标量（单个数值）wi，这个值就代表了历史行为 i 在当前候选商品 a “在场”的情况下的注意力得分。</li></ul><p><strong>b) 加权求和：构建动态兴趣向量</strong><br>在为所有 N 个历史行为都计算出对应的注意力得分 w1,w2,…,wN 之后，模型通过<strong>加权求和</strong>的方式，来构建最终的用户兴趣向量 vU：</p><p><img src="image%2022.png" alt="image.png"></p><p>这个最终的兴趣向量 vU 是<strong>动态的</strong>。当候选商品变化时，注意力得分会重新计算，vU 也会随之改变，从而精准地反映出用户在此情此景下的特定兴趣。</p><p><strong>c) 关键创新：摒弃 Softmax</strong><br>传统的注意力机制通常会在最后使用 <code>Softmax</code> 函数将所有权重归一化，使其和为1。DIN 创新地<strong>摒弃了 Softmax</strong>。</p><ul><li><strong>原因</strong>：Softmax 会“拉平”兴趣的强度。一个在某个领域有100次点击的用户和一个只有10次点击的用户，他们的兴趣强度是截然不同的。Softmax 会将这种绝对强度的信息抹去，只保留相对大小。</li><li><strong>好处</strong>：不使用 Softmax，直接使用注意力得分进行加权，可以<strong>保留用户兴趣的绝对强度</strong>。如果用户是某领域的狂热粉丝，其加权后的兴趣向量模长就会更大，这本身就是一个非常强的信号，可以被后续的 DNN 有效捕捉。</li></ul><p><strong>3. DIN 的其他重要贡献</strong></p><p>除了注意力机制，DIN 论文还贡献了两个非常实用的工程技术，以保证模型在工业级海量数据下的训练效果和效率：</p><ul><li><strong>Dice 激活函数</strong>：一种数据自适应的激活函数，能够根据每个 mini-batch 的数据分布动态调整其激活点，以应对工业数据分布剧烈变化的挑战，使训练更稳定。</li><li><strong>小批量感知正则化 (Mini-batch Aware Regularization)</strong>：一种针对海量稀疏特征（如 <code>item_id</code>）的正则化优化方法。它只在每个批次中，对当次出现的特征的 Embedding 进行正则化计算，极大地降低了计算开销。</li></ul><p><strong>总结：</strong><br>DIN 是深度学习推荐系统发展史上的一个分水岭。它通过引入<strong>目标感知的注意力机制</strong>，将用户兴趣的建模方式从“静态的、无差别的兴趣平均”，带入了**“动态的、上下文相关的兴趣聚焦”<strong>的新时代。DIN 不仅是一个具体的模型，它所开创的“根据目标物品来动态计算用户兴趣”这一</strong>范式**，被后续几乎所有的序列推荐模型（如 DIEN, SIM）所继承和发展，影响至今。</p><h3 id="4-2-DIEN-Deep-Interest-Evolution-Network-捕捉兴趣的“进化”轨迹">4.2 DIEN (Deep Interest Evolution Network): 捕捉兴趣的“进化”轨迹</h3><p><strong>1. 动机：从“兴趣是什么”到“兴趣将去向何方”</strong></p><ul><li><strong>DIN 的成就</strong>：DIN brilliantly地回答了这个问题：“考虑到用户要看的是<strong>这个</strong>商品，他过去的<strong>哪些</strong>兴趣是相关的？” 它捕捉了兴趣的<strong>多样性</strong>。</li><li><strong>DIN 的局限</strong>：DIN 将用户的历史行为视为一个无序的集合，完全忽略了<strong>时间顺序</strong>。它无法区分一个兴趣是正在快速增长，还是早已衰退。例如，一个用户上周买了相机、昨天买了镜头，和一个用户一年前买了相机、此后再无动作，DIN 对这两种情况的理解是相似的。但前者明显表现出对“摄影”这个兴趣的<strong>演进和增强</strong>。</li><li><strong>DIEN 的目标</strong>：DIEN 的核心目标，就是要<strong>显式地对用户兴趣的演进轨迹进行建模</strong>。它不仅要捕捉兴趣的相关性，更要捕捉兴趣的动态变化过程，从而预测兴趣的“最终状态”，用这个“最终状态”来进行更精准的推荐。</li></ul><p><strong>2. DIEN 的核心架构：两阶段 RNN</strong></p><p>为了对“进化”这一时序过程进行建模，DIEN 创新地设计了一个两阶段的循环神经网络 (RNN) 架构。</p><p><strong>第一阶段：兴趣提取层 (Interest Extractor Layer)</strong></p><ul><li><strong>目标</strong>：从用户原始的行为序列（一串物品ID）中，学习并提取出抽象的、潜在的“兴趣状态”序列。</li><li><strong>实现</strong>：使用一个 <strong>GRU (Gated Recurrent Unit)</strong> 网络。<ul><li>将用户的历史行为 Embedding 按照时间顺序，依次输入到 GRU 中。</li><li>GRU 在每个时间步 <code>t</code> 都会输出一个隐藏状态 <code>h_t</code>。这个 <code>h_t</code> 融合了 <code>t</code> 时刻及之前所有的行为信息，可以被看作是用户在该时刻的“潜在兴趣状态”向量。</li></ul></li></ul><p><strong>第二阶段：兴趣进化层 (Interest Evolving Layer) - [DIEN 的核心创新]</strong></p><ul><li><strong>目标</strong>：在第一阶段生成的“兴趣状态”序列 <code>h_1, h_2, ..., h_T</code> 的基础上，根据<strong>当前候选商品（Target Item）</strong>，抽取出最相关的一条<strong>兴趣进化链</strong>，并模拟其演进过程。</li><li><strong>关键设计：AUGRU (GRU with Attention Update Gate)</strong><br>DIEN 的作者认为，并非所有的兴趣进化都与当前推荐任务相关。因此，他们设计了一种由注意力机制引导的、全新的 GRU 单元——AUGRU。<ol><li><strong>计算注意力</strong>：首先，模型会计算<strong>候选商品</strong>的 Embedding <code>e_a</code> 与<strong>每一个</strong>历史兴趣状态 <code>h_t</code> 之间的相关性，得到一个注意力得分 <code>a_t</code>。这个得分衡量了历史上的某个兴趣状态与当前目标的关联程度。</li><li><strong>改造更新门</strong>：GRU 有一个关键的“更新门” <code>u_t</code>，它决定了当前时刻的信息在多大程度上被用来更新 GRU 的记忆。AUGRU 对这个更新门做了巧妙的改造，将原始更新门与注意力得分相乘：<code>u'_t = a_t * u_t</code>。</li><li><strong>核心作用</strong>：<ul><li>如果某个历史兴趣 <code>h_t</code> 与当前候选商品<strong>高度相关</strong>，其注意力得分 <code>a_t</code> 就很高，这会“放大”更新门的作用，使得这个兴趣状态在进化网络中产生<strong>更强的影响力</strong>。</li><li>反之，如果 <code>h_t</code> 与候选商品<strong>无关</strong>，其注意力得分 <code>a_t</code> 很低，这会“抑制”更新门，使得这条无关的兴趣信息在进化网络中被<strong>有效削弱</strong>。</li></ul></li><li><strong>最终输出</strong>：AUGRU 网络的最终一个隐藏状态，就代表了这条“与目标相关的兴趣进化轨迹”的最终结果。这个向量被认为是用户兴趣的“进化终点”，并被用于最终的 CTR 预测。</li></ol></li></ul><p><strong>3. 锦上添花：辅助损失 (Auxiliary Loss)</strong></p><p>为了让第一阶段的 GRU 能够学习到更有意义的兴趣状态表示，DIEN 还引入了一个<strong>辅助损失函数</strong>。</p><ul><li><strong>动机</strong>：只靠最终的 CTR 预测任务来监督整个长序列的学习，梯度信号会非常稀疏和微弱。</li><li><strong>做法</strong>：在兴趣提取层，额外增加一个“用前一个行为预测后一个行为”的学习任务。即用 <code>h_t</code> 去预测真实的用户下一个行为 <code>e_&#123;t+1&#125;</code>。</li><li><strong>好处</strong>：这个辅助任务为兴趣提取层的每一步都提供了直接的监督信号，迫使 GRU 学会如何从行为序列中提取出真正有预测性的“兴趣状态”。</li></ul><p><strong>总结：</strong><br>DIEN 是对 DIN 的一次深刻升华。它成功地将推荐系统从对用户<strong>静态、多样</strong>兴趣的捕捉，推进到了对<strong>动态、时序</strong>兴趣的建模。其核心贡献可以概括为：</p><ol><li>首次使用 <strong>RNN 结构</strong>来显式地捕捉用户行为的<strong>序列性</strong>。</li><li>创新地设计了 <strong>AUGRU</strong> 单元，巧妙地将<strong>注意力机制</strong>与 <strong>RNN 的门控机制</strong>相结合，实现了对“与目标相关的兴趣进化”的精准模拟。</li><li>引入<strong>辅助损失</strong>，有效提升了底层序列表示的学习效果。</li></ol><h3 id="4-3-SIM-Search-based-Interest-Model-攻克工业级“超长序列”难题">4.3 SIM (Search-based Interest Model): 攻克工业级“超长序列”难题</h3><p><strong>1. 动机：当序列长度成为瓶颈</strong></p><ul><li><strong>现实的挑战</strong>：一个活跃用户的历史行为记录，可以轻易达到数千、数万甚至更长。</li><li><strong>现有模型的困境</strong>：<ul><li><strong>DIEN (RNN-based)</strong>：RNN 的计算是串行的，序列越长，计算耗时越长。处理上万长度的序列，对于要求在几十毫秒内返回结果的在线推荐服务来说，是完全不可接受的。</li><li><strong>Transformer-based</strong>：标准 Transformer 的自注意力机制，其计算复杂度是序列长度的平方，即 <code>O(n^2)</code>。这使得它在处理长序列时比 RNN 的情况更糟。</li><li><strong>DIN (Attention-based)</strong>：虽然 DIN 的注意力计算是并行的，但对上万个历史行为逐一计算与候选商品的注意力得分，依然是一笔巨大的计算开销。</li></ul></li></ul><p><strong>SIM (Search-based Interest Model, 2020)</strong> 的提出，不是对模型结构进行小修小补，而是从根本上改变了处理超长序列的<strong>思想范式</strong>。</p><p><strong>2. SIM 的核心思想：从“一次性建模”到“两阶段检索”</strong></p><p>SIM 的核心洞察是：我们不必，也不能在一次在线预测中，对用户的全部上万条历史记录进行复杂建模。对于一个特定的候选商品，用户的历史行为中，真正能提供决策信息的，往往只是其中一小部分最相关的行为。</p><p>因此，SIM 将用户兴趣建模的过程，巧妙地重构成了一个**“在用户历史中进行搜索”**的任务。它将推荐系统中经典的“召回-排序”两阶段漏斗思想，“微缩”并应用到了用户兴趣建模的内部。</p><p><strong>3. SIM 的核心架构：GSU + ESU 两阶段搜索</strong></p><p><strong>第一阶段：通用兴趣检索 (General Search Unit, GSU) - “硬搜索”</strong></p><ul><li><strong>目标</strong>：从用户上万条的完整历史中，<strong>快速、低成本地</strong>“召回”出一个几百条的、高度相关的历史行为候选子集。</li><li><strong>实现方式</strong>：<ol><li><strong>离线建索引</strong>：对于每个用户，系统会离线地将他的<strong>全部</strong>历史行为物品的 Embedding 向量，存入一个高效的<strong>近似最近邻（ANN）检索引擎</strong>（例如 Faiss）。这相当于为每个用户都建立了一个私有的、可被高速检索的“行为数据库”。</li><li><strong>在线检索</strong>：当一次推荐请求到来时，模型会先构建一个<strong>查询向量 (Query Vector)</strong>。这个查询向量代表了用户当前的“大致兴趣方向”，通常由用户<strong>最近的少数几个行为</strong>（如最近点击的5个商品）的 Embedding 平均池化得到。</li><li>模型用这个查询向量，去用户的“行为数据库”（ANN索引）中进行一次极速的向量检索，找出与当前兴趣方向最相似的 Top-K（比如200）个历史行为。这个过程因为是硬性地筛选出一部分，所以被称为“<strong>硬搜索</strong>”。</li></ol></li></ul><p><strong>第二阶段：精准兴趣检索 (Exact Search Unit, ESU) - “软搜索”</strong></p><ul><li><p><strong>目标</strong>：在 GSU 阶段筛选出的那 200 条相关历史中，根据<strong>当前候选商品（Target Item）</strong>，进行更精细的兴趣匹配和建模。</p></li><li><p><strong>实现方式</strong>：<strong>这一阶段的实现，几乎就是 DIN 的一个翻版！</strong></p><ol><li><strong>输入</strong>：当前候选商品的 Embedding <code>e_a</code>，以及 GSU 筛选出的 200 个历史行为的 Embedding。</li><li><strong>注意力计算</strong>：使用与 DIN 相同的<strong>目标感知注意力网络</strong>，计算 <code>e_a</code> 与这 200 个历史行为中每一个的相关性（注意力得分）。因为是计算连续的权重，所以被称为“<strong>软搜索</strong>”。</li><li><strong>加权求和</strong>：将这 200 个历史行为的 Embedding，根据其注意力得分进行加权求和，得到最终的、动态的、与当前候选商品强相关的用户兴趣向量。</li></ol><p><img src="image%2023.png" alt="image.png"></p></li></ul><p><strong>总结：</strong><br>SIM 是一个算法与工程完美结合的典范。它没有试图用一个单一的、复杂的模型去硬“啃”超长序列，而是创造性地提出了**“先捞后筛”**的两阶段兴趣建模范式。</p><ul><li><strong>GSU</strong> 负责从汪洋大海般的历史中，高效地“捞”出最可能相关的几百条数据。</li><li><strong>ESU</strong> 则负责在这几百条数据中，用 DIN 的思想进行精细化的“筛选”和“整合”。</li></ul><h2 id="第五章：生成式与大语言模型的前沿探索-The-Frontier-Generative-LLM-based-Models"><strong>第五章：生成式与大语言模型的前沿探索 (The Frontier: Generative &amp; LLM-based Models)</strong></h2><h3 id="5-1-范式转移：从“判别式”到“生成式”推荐">5.1 范式转移：从“判别式”到“生成式”推荐</h3><p>到目前为止，我们讨论的所有模型，从 LR 到 DIEN，都属于<strong>判别式模型 (Discriminative Models)</strong>。而最新的研究浪潮，正推动着我们走向<strong>生成式模型 (Generative Models)</strong>。</p><p><strong>1. 判别式推荐 (我们一直在做的事)</strong></p><ul><li><strong>核心目标</strong>：学习一个条件概率 <code>P(y|x)</code>。</li><li><strong>翻译成大白话</strong>：给定一个“输入” <code>x</code>（即一个<code>(用户, 物品)</code>对），模型需要“判别”出对应的“标签” <code>y</code>（即用户是否会点击）的概率。</li><li><strong>模型角色</strong>：像一个“<strong>裁判</strong>”或“<strong>打分器</strong>”。它的任务是，对于每一个候选物品，都给出一个“好”或“不好”的概率分数。它回答的问题是：“这个物品，给这个用户，匹配度有多高？”</li></ul><p><strong>2. 生成式推荐 (未来的方向)</strong></p><ul><li><strong>核心目标</strong>：学习输入数据 <code>x</code> 本身的联合概率分布 <code>P(x)</code>。</li><li><strong>翻译成大白话</strong>：模型不再是去“判断”一个已有的组合，而是去<strong>学习用户行为序列本身是如何“生成”的</strong>。它试图理解用户行为的内在逻辑和模式。</li><li><strong>模型角色</strong>：像一个“<strong>故事续写者</strong>”或“<strong>语言模型</strong>”。它的任务是，在看到用户过去的一系列行为后，去“生成”或“预测”他下一步最有可能产生的行为。它回答的问题是：“根据这个用户的历史，他下一步会做什么？”</li></ul><p><strong>为什么会发生这种转变？</strong><br>生成式范式借鉴了 GPT 等大语言模型的成功，它为推荐系统带来了几个潜在的巨大优势：</p><ol><li><strong>更全面的行为建模</strong>：它不再孤立地看待每一次推荐，而是将用户的整个行为序列视为一个连贯的整体来学习，能更好地捕捉用户的长期动态意图。</li><li><strong>更高的灵活性</strong>：一个训练好的生成式模型，不仅能用于预测下一个物品（排序），还能用于生成整个会话（session）、生成推荐理由等更多元的任务。</li><li><strong>与LLM的天然结合</strong>：将物品视为“词汇”，将用户行为视为“句子”，这使得推荐系统可以直接利用在海量文本上预训练好的、拥有强大语义理解能力的 Transformer 架构。</li></ol><h3 id="5-2-Kuaiformer-LLM4Rec-借鉴LLM架构解决推荐三大挑战">5.2 Kuaiformer (LLM4Rec): 借鉴LLM架构解决推荐三大挑战</h3><p>Kuaiformer 是快手团队提出的一个典型的生成式推荐模型，它展示了如何在工业级的超大规模场景下，借鉴并改造 LLM 架构来解决推荐的核心难题。</p><p><strong>面临的三大挑战：</strong></p><ol><li><strong>超大候选集</strong>：推荐系统的物品“词汇表”是十亿甚至百亿级别，远超LLM的几十万词汇表，无法直接使用 <code>Softmax</code>。</li><li><strong>用户兴趣多样性</strong>：用户的兴趣是发散且多样的，需要模型能同时捕捉多个兴趣点。</li><li><strong>超长用户序列</strong>：用户的历史行为序列可达上万，标准 Transformer 的 <code>O(n^2)</code> 复杂度无法承受。</li></ol><p><strong>Kuaiformer 的解决方案：</strong></p><p><strong>1. 应对超长序列 → 分段建模 (Sequence Segmentation)</strong><br>为了破解 <code>O(n^2)</code> 的魔咒，Kuaiformer 采用“分而治之”的策略：</p><ul><li>将用户的超长历史行为序列，按时间顺序切分成若干个<strong>段落 (Segment)</strong>。</li><li>对每个段落<strong>并行地</strong>使用一个 <strong>Bi-Transformer</strong> (双向Transformer) 进行内部信息编码，然后通过<strong>平均池化</strong>将每个段落压缩成一个代表性向量。</li><li>最后将所有段落的代表性向量拼接起来，形成一个对原始长序列的<strong>浓缩摘要</strong>。</li></ul><p><strong>2. 应对多重兴趣 → 查询式注意力 (Query-based Attention)</strong><br>在得到浓缩的历史摘要后，模型通过一个 <strong>Causal Transformer</strong> 来提取用户的多个兴趣：</p><ul><li>模型引入 <code>k</code> 个可学习的<strong>查询向量 (Query Vector)</strong>，并将它们与历史摘要一同输入到 Transformer 中。</li><li>在 Transformer 内部，每个 Query 向量会通过自注意力机制，像“探针”一样去扫描和聚合历史摘要中与自己相关的部分。</li><li>最终，<code>k</code> 个 Query 向量在 Transformer 输出层的位置上，就形成了代表用户 <code>k</code> 个不同兴趣中心的<strong>多兴趣向量</strong> <code>&#123;u_1, u_2, ..., u_k&#125;</code>。</li></ul><p><strong>3. 应对超大候选集 → 双塔预测 与 In-batch 训练</strong></p><ul><li><p><strong>预测方式</strong>：最终的预测是一个双塔问题。Kuaiformer 本身构成了复杂的 <strong>User Tower</strong>（输出 <code>k</code> 个兴趣向量），而物品侧则是一个简单的 <strong>Item Tower</strong>（输出物品 Embedding）。用户对一个物品的最终得分为：</p><p><img src="image%2024.png" alt="image.png"></p><p>即物品 Embedding 与用户所有兴趣向量相似度的<strong>最大值</strong>。</p></li><li><p><strong>训练方式</strong>：由于无法对全量物品计算 <code>Softmax</code>，模型采用 <strong>In-batch Softmax</strong> 的方式进行训练，即只在当前一个批次 (mini-batch) 内的样本中进行 <code>Softmax</code> 计算。</p></li><li><p><strong>偏差修正</strong>：为了解决 In-batch 采样带来的<strong>流行度偏差</strong>和<strong>标签噪声</strong>问题，Kuaiformer 在训练时采用了我们之前讨论过的 <strong>LogQ修正</strong> 和 <strong>标签平滑</strong> 等高级技巧，以保证训练的稳定和无偏。</p></li></ul><h3 id="5-3-核心概念：高级训练技巧-——-偏差修正与标签平滑">5.3 核心概念：高级训练技巧 —— 偏差修正与标签平滑</h3><p>当我们使用像 Kuaiformer 这样的大模型，并采用 <strong>In-batch Softmax</strong> 策略进行训练时，会遇到两个普遍存在且严重影响模型性能的问题：<strong>流行度偏差</strong> 和 <strong>标签噪声</strong>。为了解决这两个问题，研究者们引入了两种极其重要的高级训练技巧。</p><p><strong>1. 偏差修正：对抗“流行即原罪”的 LogQ 修正</strong></p><ul><li><p><strong>问题背景：In-batch Softmax 带来的流行度偏差</strong></p><ul><li>在拥有亿级物品池的推荐系统中，我们无法在每次训练时对所有物品计算 <code>Softmax</code>。因此，我们采用 <code>In-batch Softmax</code>，即对于一个正样本，只把它和<strong>当前批次 (mini-batch) 内</strong>的其他样本作为负样本来计算损失。</li><li>这样做的问题在于，一个 mini-batch 内的样本<strong>并非对全体物品的均匀随机采样</strong>。<strong>热门物品</strong>因为本身与用户交互多，所以它们出现在任意一个 batch 中的概率天然就更高。</li><li><strong>后果</strong>：模型在训练时，看到的负样本大多是“热门选手”。为了在竞争中胜出，模型可能会学到一种错误的偏见：“<strong>热门的物品通常不是好的推荐</strong>”，从而系统性地压低对所有热门物品的预测分数。这与“热门物品之所以热门，是因为它们质量高、受欢迎”的常识相悖，会导致模型性能严重退化。这与我们在召回阶段讨论的 SSB (Sampling-Bias-Corrected) 问题根源完全相同。</li></ul></li><li><p><strong>解决方案：LogQ 修正 (LogQ Correction)</strong></p><ul><li><strong>核心思想</strong>：在计算损失前，对每个物品的原始预测分数（logit）进行一次“纠偏”，以抵消其因流行度带来的采样概率不均。</li><li><strong>实现方式</strong>：将每个物品的原始分数 <code>s_i</code> 调整为 <code>s'_i</code>：<br><code>s'_i = s_i - log(Q_i)</code><br>其中：<ul><li><code>s_i</code> 是模型对物品 <code>i</code> 输出的原始分数。</li><li><code>Q_i</code> 是物品 <code>i</code> 被采样到 batch 中的概率，通常用它的<strong>全局流行度</strong>来近似。</li></ul></li><li><strong>损失函数</strong>：使用修正后的分数 <code>s'_i</code> 来计算 In-batch Softmax 损失：</li></ul><p><img src="cbde9e5e-09d1-49c0-84b6-5315b7f5c408.png" alt="image.png"></p><ul><li><strong>作用效果</strong>：<ul><li>对于热门物品，其 <code>Q_j</code> 很大，<code>log(Q_j)</code> 也很大。从它的分数中减去一个大数，相当于<strong>降低了它作为负样本时的“惩罚”力度</strong>，给了它一个公平竞争的机会。</li><li>对于冷门物品，其 <code>Q_j</code> 很小，<code>log(Q_j)</code> 是一个较大的负数。从分数中减去一个负数，等于<strong>增加了它的分数</strong>，给了它一个“补偿”，让它不至于被热门物品淹没。</li></ul></li><li><strong>最终目的</strong>：通过 LogQ 修正，使得 In-batch Softmax 损失在数学上成为对全局 Softmax 损失的一个<strong>无偏估计</strong>，从而让模型学习到物品的真实匹配度，而非被流行度所误导。</li></ul></li></ul><p><strong>2. 标签平滑：接受“不完美”的用户行为</strong></p><ul><li><strong>问题背景：过分自信与标签噪声</strong><ul><li><strong>标签非完美</strong>：用户的行为数据是有噪声的。一次点击（标签为<code>1</code>）不代表用户100%满意，可能只是“手滑”或“好奇”；一次未点击（标签为<code>0</code>）更不代表用户100%讨厌。</li><li><strong>模型过分自信 (Overconfidence)</strong>：标准的交叉熵损失函数会驱使模型将对正样本的预测概率无限推向 <code>1.0</code>。为了达到这个“绝对正确”的目标，模型可能会学得过于极端，对训练数据中的噪声也进行了过度拟合，从而导致<strong>泛化能力下降</strong>。</li></ul></li><li><strong>解决方案：标签平滑 (Label Smoothing)</strong><ul><li><p><strong>核心思想</strong>：对模型“温柔”一点，不要强求它做出“非黑即白”的判断。我们主动地给标签增加一点“不确定性”。</p></li><li><p><strong>实现方式</strong>：我们不再使用 <code>[0, 1]</code> 这样的硬标签，而是使用“软标签”。</p><ul><li>选定一个很小的平滑值 <code>ε</code>（例如 <code>0.1</code>）。</li><li>对于<strong>正样本</strong>，我们将其目标标签从 <code>1.0</code> 修改为 <code>1.0 - ε</code>（即 <code>0.9</code>）。</li><li>对于<strong>负样本</strong>，我们不再要求其目标为 <code>0</code>，而是将剩余的概率 <code>ε</code> 平均分配给所有 <code>K-1</code> 个负样本，每个负样本的目标变为 <code>ε / (K-1)</code>。</li></ul></li><li><p><strong>损失函数</strong>：相应的，损失函数也从只惩罚正样本，变为同时考虑所有样本：</p><p><img src="image%2025.png" alt="image.png"></p><p>（注：这里的 pj 是对负样本的预测概率，为了简化，有时会写成 log(1−pj) 的形式，但核心思想是让模型不要把负样本的概率压到绝对的0）</p></li><li><p><strong>作用效果</strong>：</p><ul><li><strong>防止过分自信</strong>：由于目标不再是绝对的1，模型不会把正样本的logit无限推高，使其权重分布更合理。</li><li><strong>提升泛化能力</strong>：强迫模型为负样本也保留一点点概率，这鼓励了不同类别之间的Embedding在向量空间中形成一个更合理的、有间隔的分布，而不是把所有正样本都挤在一个点上。</li><li><strong>作为一种正则化</strong>：标签平滑是一种简单而高效的正则化技术，能有效防止模型过拟合，使训练过程更稳定。</li></ul></li></ul></li></ul><h2 id="第六章：工业界全景与总结-The-Industrial-Landscape-Summary"><strong>第六章：工业界全景与总结 (The Industrial Landscape &amp; Summary)</strong></h2><h3 id="6-1-工业界的标准流程：多阶段排序漏斗（召回、粗排、精排）">6.1 工业界的标准流程：多阶段排序漏斗（召回、粗排、精排）</h3><p>现代大规模推荐系统面临着一个核心的工程矛盾：一方面，候选的物品池是<strong>海量</strong>的（通常是十亿甚至百亿级别）；另一方面，对用户的每一次推荐请求，系统必须在极短的时间内（通常在 <strong>50-100毫秒</strong>）返回一个高质量的、个性化的排序列表。</p><p><strong>在一个单一阶段内，用一个复杂的模型（如DeepFM）去处理十亿级的物品，是绝对不可能的。</strong> 这种矛盾催生了工业界标准的<strong>多阶段排序漏斗 (Multi-stage Ranking Funnel)</strong> 架构。</p><p>这个架构的核心思想是**“层层筛选，逐步聚焦”<strong>，通过多个阶段，不断地在</strong>计算成本<strong>和</strong>排序精度**之间做出权衡，将海量的物品集一步步精炼成最终呈现给用户的几十个物品。</p><p><strong>第一阶段：召回 (Recall)</strong></p><ul><li><strong>目标</strong>：<strong>“大海捞针”</strong>。从全量的、百亿级的物料库中，快速、低成本地找出几千个用户<strong>可能</strong>感兴趣的候选物品。</li><li><strong>核心指标</strong>：<strong>召回率 (Recall)</strong>。这一阶段的使命是“宁可错杀一千，不可放过一个”，要尽可能地保证用户未来真正会喜欢的物品，能够出现在这个候选池中。如果召回阶段就漏掉了，后续阶段再强大也无力回天。</li><li><strong>候选集规模</strong>：从 <code>百亿级</code> 筛选到 <code>千级</code>。</li><li><strong>技术与模型</strong>：追求极致的速度。<ul><li><strong>多路召回</strong>：最常见的策略，并行使用多种不同的简单召回方法，并将结果合并，以保证结果的多样性。例如：<ul><li><strong>热门召回</strong>：返回全局热门的物品，作为基础流量保障。</li><li><strong>协同过滤</strong>：经典的 User-CF, Item-CF，找到相似用户喜欢的，或相似物品。</li><li><strong>向量检索召回</strong>：目前最主流的方法。利用双塔模型等提前计算好所有用户和物品的 Embedding 向量，将物品向量存入高效的近似最近邻（ANN）索引（如 <code>Faiss</code>）。在线上，根据用户的 Embedding 向量，进行一次极速的向量检索，找出最相似的 Top-K 物品。</li></ul></li></ul></li></ul><p><strong>第二阶段：粗排 (Pre-ranking / Coarse Ranking)</strong></p><ul><li><strong>目标</strong>：<strong>“精挑细选”</strong>。对召回层送来的几千个候选物品，进行一次初步的、计算量适中的排序，淘汰掉其中大量“明显不靠谱”的物品，将候选集进一步压缩。</li><li><strong>核心指标</strong>：在<strong>极低延迟</strong>和<strong>较高精度</strong>之间做权衡。它需要比精排快得多，但比召回准得多。</li><li><strong>候选集规模</strong>：从 <code>千级</code> 筛选到 <code>百级</code>。</li><li><strong>技术与模型</strong>：使用相对简单的模型和少量核心特征。<ul><li><strong>逻辑回归 (LR)</strong> 或 <strong>浅层的 MLP</strong>。</li><li><strong>简化的深度模型</strong>：比如一个“青春版”的 DeepFM，网络层数更少、宽度更窄，使用的特征也更少。</li><li><strong>知识蒸馏</strong>：这是保证粗排效果的关键技术。让复杂的**精排模型（老师）<strong>来指导简单的</strong>粗排模型（学生）**进行学习，让学生模型能够模仿老师模型的打分行为，从而保证两个阶段的排序偏好尽可能一致，避免错杀“潜力股”。</li></ul></li></ul><p><strong>第三阶段：精排 (Fine-ranking)</strong></p><ul><li><strong>目标</strong>：<strong>“精雕细琢”</strong>。对粗排层送来的几百个高质量候选物品，不计成本地使用最强大、最复杂的模型，进行最精准的个性化打分和排序。</li><li><strong>核心指标</strong>：<strong>精准率 (Precision)</strong>。这一阶段的目标是追求排序结果的极致准确，尤其是列表顶部的准确性。</li><li><strong>候选集规模</strong>：从 <code>百级</code> 筛选到最终呈现给用户的 <code>几十个</code>。</li><li><strong>技术与模型</strong>：所有我们之前详细讨论过的**“重武器”模型**都在这一层大展身手。<ul><li><strong>特征交互模型</strong>：<code>DeepFM</code>, <code>DCN</code>, <code>xDeepFM</code> 等。</li><li><strong>序列建模</strong>：<code>DIN</code>, <code>DIEN</code>, <code>SIM</code> 等。</li><li><strong>多任务与多场景</strong>：<code>MMoE</code>, <code>CAN</code> 等。</li><li><strong>前沿模型</strong>：<code>LLM-based</code> 模型。</li><li>在这一层，模型会使用上千维的、包含大量<strong>实时特征</strong>的、最全面的特征信息来进行预测。</li></ul></li></ul><h3 id="6-2-当前工业界的主力模型：为何DeepFM架构是“万能骨架”？">6.2 当前工业界的主力模型：为何DeepFM架构是“万能骨架”？</h3><h3 id="6-3-技术演进路线图：从LR到LLM的回顾与展望">6.3 技术演进路线图：从LR到LLM的回顾与展望</h3>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/08/Rec_AD_Ranking/</id>
    <link href="https://qyp9909.github.io/2025/08/08/Rec_AD_Ranking/"/>
    <published>2025-08-08T09:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="搜广推-排序">搜广推[排序]</h2>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月9日 14:29</p>
<h3 id="推荐系统排序模型知识体系总结（大纲）"><strong>推荐系统排序模型知识]]>
    </summary>
    <title>搜广推笔记 排序</title>
    <updated>2026-06-27T10:21:43.824Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="Recsys" scheme="https://qyp9909.github.io/categories/Recsys/"/>
    <category term="Study" scheme="https://qyp9909.github.io/tags/Study/"/>
    <content>
      <![CDATA[<h2 id="搜广推-召回">搜广推[召回]</h2><p>Created by: Yuanpeng QU<br>Created time: 2025年8月5日 22:51</p><h2 id="一、-推荐系统总体架构-Overall-Recommendation-System-Architecture"><strong>一、 推荐系统总体架构 (Overall Recommendation System Architecture)</strong></h2><p><code>1. 级联漏斗范式 (Cascading Funnel Paradigm)</code> 现代大规模推荐系统的核心架构，普遍遵循一种<strong>级联漏斗范式 (Cascading Funnel Paradigm)</strong>。这个范式的诞生，是为了解决一个根本性的矛盾：一方面，我们的候选物品库是海量的（百万、千万甚至上亿级别）；另一方面，用户的屏幕空间是有限的，且要求响应速度极快（毫秒级）。</p><p>因此，我们不可能对所有物品都用最复杂的模型进行最精确的计算。漏斗范式通过设置多个层层递进的过滤环节，实现了<strong>从海量到少量，从粗糙到精准的逐级筛选，在计算效率和推荐效果之间取得了极致的平衡。</strong></p><p>这个漏斗通常包含以下四个核心层级：</p><h3 id="1-召回层-Recall-Layer"><code>1. 召回层 (Recall Layer)</code></h3><ul><li><strong>核心目标：“海选”——保证覆盖率与多样性</strong><ul><li>召回层的首要任务是从全量的、数以亿计的物品库中，快速、高效地筛选出一个几百到几千的、与用户兴趣可能相关的候选集合。它的核心目标是“宁可错杀，不可放过”<strong>，即尽可能地保证用户未来真正会感兴趣的物品，都包含在这个候选集里。它追求的是</strong>高召回率 (Recall Rate)，可以牺牲一定的精确率。</li></ul></li><li><strong>处理规模：</strong> 从全量（10^8 - 10^9+）筛选至中等规模（10^2 - 10^3）。</li><li><strong>模型/策略特点：</strong><ul><li><strong>速度第一</strong>：模型和策略必须极度高效，计算复杂度低。</li><li><strong>多路并行</strong>：通常会并行运行数十甚至上百路不同的召回策略，并将结果合并，以保证候选集的多样性。常见的召回策略包括：<ul><li><strong>协同过滤</strong>：UserCF, ItemCF, Swing 等。</li><li><strong>向量化召回</strong>：最主流的方式，如双塔模型 (DSSM)。</li><li><strong>图模型召回</strong>：DeepWalk, GNN 等。</li><li><strong>序列模型召回</strong>：SDM 等。</li><li><strong>策略性召回</strong>：热门召回、新上物品召回、复购召回、地理位置召回等。</li></ul></li></ul></li><li><strong>一个比喻：招聘中的“简历筛选”:</strong><ul><li>HR 助手面对成千上万份简历，他会用一些简单的规则（如学历、关键词、工作年限）快速过滤，筛选出几百份基本符合要求的简历进入下一轮。他不会去深究每份简历的细节，目标是快速缩小范围，别把合适的人漏掉。</li></ul></li></ul><h3 id="2-粗排层-Coarse-Ranking-Layer"><code>2. 粗排层 (Coarse Ranking Layer)</code></h3><ul><li><strong>核心目标：“初试”——初步过滤，减轻精排压力</strong><ul><li>粗排层是一个“缓冲带”或“减压阀”，位于召回和精排之间。它使用比召回更丰富但比精排更简单的特征和模型，对召回送来的几百上千个物品进行一次快速的排序，进一步筛选掉那些明显不那么相关的物品。</li></ul></li><li><strong>处理规模：</strong> 从中等规模（10^2 - 10^3）筛选至小规模（10^2 级别）。</li><li><strong>模型/策略特点：</strong><ul><li><strong>平衡效率与效果</strong>：模型比召回复杂，但远比精排简单，要求在可接受的延迟内完成计算。</li><li><strong>特征简化</strong>：会使用部分用户和物品的交叉特征，但会避免使用那些计算开销巨大的特征。</li><li><strong>常用模型</strong>：逻辑回归 (LR)、浅层的神经网络 (MLP)、或简化版的精排模型。</li></ul></li><li><strong>一个比喻：招聘中的“电话面试”</strong><ul><li>招聘专员对通过简历筛选的几百人进行一轮简短的电话沟通，通过一些标准化的、核心的问题，快速淘汰掉那些明显不合适的候选人，只把几十个最有希望的留给部门经理去面试。</li></ul></li></ul><h3 id="3-精排层-Fine-Ranking-Layer"><code>3. 精排层 (Fine-Ranking Layer)</code></h3><ul><li><strong>核心目标：“终面”——精准排序，最大化核心指标</strong><ul><li>精排层是整个推荐系统最核心、最能体现技术水平的部分。它的目标是对粗排送来的上百个高质量候选物品，使用最复杂的模型和最丰富的特征，进行最精确的个性化排序。它追求的是<strong>高精确率 (Precision)</strong>，直接对最终的业务指标（如CTR、CVR、GMV）负责。</li></ul></li><li><strong>处理规模：</strong> 从小规模（10^2 级别）排序后，输出最终呈现给用户的几十个物品的有序列表。</li><li><strong>模型/策略特点：</strong><ul><li><strong>效果第一</strong>：在延迟允许的范围内，可以不计代价地使用复杂的模型和特征。</li><li><strong>模型复杂</strong>：广泛使用各种深度学习模型，如 Wide&amp;Deep, DeepFM, DIN, DCN, MMoE 等。</li><li><strong>特征海量</strong>：会用到用户、物品、上下文、行为序列、交叉组合等成百上千维，甚至更多的特征。</li></ul></li><li><strong>一个比喻：招聘中的“专家小组面试”</strong><ul><li>部门经理、总监、核心技术专家组成面试小组，对最终的几十位候选人进行长达数小时的、全方位的深入面试。他们会动用所有能考察的信息和手段，最终给出一个非常精确的候选人能力排序，决定录用谁，以及录用优先级。</li></ul></li></ul><h3 id="4-重排层-Re-ranking-Layer"><code>4. 重排层 (Re-ranking Layer)</code></h3><ul><li><strong>核心目标：“发Offer前的最终考量”——业务调控，提升整体体验</strong><ul><li>在精排之后，列表已经是按照某个指标（如预估点击率）从高到低排好的。但一个好的推荐结果，除了“准”，还需要考虑<strong>多样性、新颖性、公平性</strong>等宏观指标。重排层的目标就是对这个已经排好的列表进行微调，以优化最终的用户体验和生态健康。</li></ul></li><li><strong>处理规模：</strong> 对精排输出的 Top-N 结果（如前50个）进行局部顺序调整。</li><li><strong>模型/策略特点：</strong><ul><li><strong>规则驱动</strong>：大量使用业务规则和启发式算法。</li><li><strong>列表级优化</strong>：它的优化目标是整个列表，而不是单个物品。</li><li><strong>常见操作</strong>：<ul><li><strong>打散</strong>：避免同类目、同作者或同品牌的商品连续出现。</li><li><strong>去重</strong>：过滤掉用户近期已经看过或消费过的内容。</li><li><strong>提权/降权</strong>：提升新品、高利润商品的权重，打压过曝内容。</li><li><strong>N-box</strong>：在固定位置（如第3位）强制插入一个运营指定的内容。</li></ul></li></ul></li><li><strong>一个比喻：招聘中的“确定录用名单”</strong><ul><li>面试结束后，管理层看着排好序的候选人名单。他们可能会说：“前两名都是搞算法的，第三名虽然分数稍低但是搞工程的，为了团队平衡，我们把第三名提上来，让他排第二。” 或者 “第一名虽然技术最强，但要求的薪资太高/无法立即入职，我们把第二名排在最前面。” 这就是基于整体考量对局部顺序的微调。</li></ul></li></ul><h2 id="二、-召回层核心技术详解-Detailed-Core-Technologies-of-the-Recall-Layer"><strong>二、 召回层核心技术详解 (Detailed Core Technologies of the Recall Layer)</strong></h2><h3 id="1-召回模型的基石：双塔模型-Foundation-Two-Tower-Model"><code>1. 召回模型的基石：双塔模型 (Foundation: Two-Tower Model)</code></h3><p>在现代推荐系统的召回阶段，双塔模型（Two-Tower Model）已经成为事实上的行业标准和基石架构。它之所以如此成功，是因为它优雅地解决了从海量候选集中进行高效语义匹配的核心难题。</p><ul><li><p><code>1.1. 向量召回基本原理 (Principle of Vector Recall)</code></p><p>向量召回的诞生，是为了解决传统召回方法（如基于协同过滤或标签匹配）的根本性缺陷：<strong>泛化能力弱</strong>和<strong>语义鸿沟</strong>问题。传统方法很难发现用户潜在的、跨领域的兴趣。</p><p>向量召回的基本原理，是将推荐问题从“匹配”问题转化为了一个“空间距离”问题。</p><ol><li><strong>万物皆可向量化 (Embedding Everything)</strong>：其核心思想是将系统中的两种核心实体——<strong>用户 (User)</strong> 和 <strong>物品 (Item)</strong>，通过深度学习模型，分别映射（或称为“嵌入”，Embedding）到一个<strong>共同的、高维的向量空间</strong>中。这个空间也被称为“语义空间”或“兴趣空间”。</li><li><strong>以“距离”度量“相关性” (Proximity as Relevance)</strong>：在这个被学习出来的空间里，向量之间的<strong>几何距离</strong>被赋予了<strong>语义上的意义</strong>。如果一个用户的向量和一个物品的向量在这个空间中的位置非常接近，我们就认为该用户对这个物品的兴趣程度很高。</li><li><strong>核心度量：内积/余弦相似度 (Core Metric: Dot Product / Cosine Similarity)</strong>：衡量“距离”最常用且最高效的方式就是计算两个向量的<strong>点积 (Dot Product)</strong>。在对向量进行 L2 归一化（即长度统一为1）之后，点积在数学上等价于<strong>余弦相似度 (Cosine Similarity)</strong>，直接度量了两个向量在方向上的对齐程度。点积越大，代表夹角越小，兴趣越一致。</li></ol><ul><li><strong>一个比喻：兴趣地图</strong><ul><li>我们可以把这个高维向量空间想象成一张“兴趣地图”。一个深度科幻迷用户的坐标，会落在地图上“硬核科幻”这个区域；而电影《流浪地球》的坐标，也会落在同一个区域。因此，通过计算地图上两个坐标点的距离，我们就能判断他们的匹配程度，即便这个用户以前从未看过《流浪地球》。</li></ul></li></ul></li><li><p><code>1.2. 模型架构：用户塔与物品塔 (Architecture: User &amp; Item Towers)</code></p><p>双塔模型就是实现上述“向量化”和“映射”过程的具体神经网络结构。它由两个相互独立、结构上非对称的子网络（塔）组成。</p><ul><li><strong>用户塔 (User Tower / Query Tower)</strong>：<ul><li><strong>输入</strong>：关于用户的<strong>一切特征</strong>。包括：<ul><li><strong>用户画像特征</strong>：年龄、性别、地域等。</li><li><strong>长期行为特征</strong>：历史点击/购买过的物品序列。</li><li><strong>实时上下文特征</strong>：当前时间、使用的设备、最近的几次操作等。</li></ul></li><li><strong>结构</strong>：一个深度神经网络（DNN），通常由多层全连接层（MLP）构成，也可以集成更复杂的结构如 Attention、RNN 来处理序列特征。</li><li><strong>输出</strong>：一个<strong>固定维度</strong>的用户向量 <code>User Embedding</code>。</li></ul></li><li><strong>物品塔 (Item Tower / Candidate Tower)</strong>：<ul><li><strong>输入</strong>：关于物品的<strong>一切特征</strong>。包括：<ul><li><strong>物品属性特征</strong>：类目、品牌、价格、标签、文本描述、图片等。</li><li><strong>物品统计特征</strong>：历史点击率、曝光数、好评率等。</li></ul></li><li><strong>结构</strong>：另一个独立的深度神经网络。</li><li><strong>输出</strong>：一个与用户向量<strong>维度相同</strong>的物品向量 <code>Item Embedding</code>。</li></ul></li><li><strong>交互方式</strong>：在训练时，两个塔的输出——<code>User Embedding</code> 和 <code>Item Embedding</code>——最终只通过一个极其简单的<strong>点积</strong>操作进行交互，并将结果送入损失函数。正是这个点积操作和后续的损失函数，作为“桥梁”和“监督者”，在反向传播时<strong>共同指导</strong>两个独立的塔去学习，迫使它们学会将用户和其喜欢的物品映射到兴趣空间中的相近位置。两个塔的参数<strong>不共享</strong>，从而可以为两种不同性质的实体分别建模。</li></ul></li><li><p><code>1.3. 训练与在线服务分离思想 (Separation of Training &amp; Serving)</code></p><p>这是双塔模型能够被广泛应用于工业界<strong>最核心的工程优势</strong>。它巧妙地将计算量巨大的部分移到线下，从而保证了线上服务的毫秒级响应。</p><ul><li><strong>离线阶段 (Offline)：训练与索引</strong><ol><li><strong>模型训练</strong>：使用海量的用户历史交互数据（如点击日志），以端到端的方式联合训练用户塔和物品塔。</li><li><strong>物品向量生成</strong>：模型训练完成后，丢弃用户塔。使用<strong>训练好的物品塔</strong>，对<strong>全量</strong>的、数以百万计的物品进行一次完整的计算，生成所有物品的 Embedding 向量。这是一个计算量巨大但可以接受的离线任务。</li><li><strong>构建ANN索引</strong>：将所有物品的 Embedding 向量存入专门的<strong>近似最近邻 (Approximate Nearest Neighbor, ANN) 检索引擎</strong>中（如 Facebook 的 Faiss、Google 的 ScaNN）。这个过程相当于为海量的物品向量建立了一个高效的“索引目录”。</li></ol></li><li><strong>在线阶段 (Online)：实时召回</strong><ol><li>当一个用户发起请求时，系统获取用户的实时特征。</li><li>将这些特征输入到<strong>训练好的用户塔</strong>中，进行一次前向计算，<strong>实时</strong>生成该用户的 <code>User Embedding</code>。这个计算非常快，因为只涉及一个样本和一个网络。</li><li>用这个新鲜出炉的 <code>User Embedding</code>，去预先构建好的 ANN 索引中进行查询，瞬间找出与它“距离最近”的 Top-K 个物品向量。</li><li>返回这 K 个物品的ID，完成召回。</li></ol></li></ul></li></ul><h3 id="2-模型训练的核心：样本构建-Core-of-Training-Sample-Construction"><code>2. 模型训练的核心：样本构建 (Core of Training: Sample Construction)</code></h3><p>这是整个模型训练环节中最重要、最能体现算法工程师“手艺”的地方。模型的理论上限可能由架构决定，但模型的实际效果，几乎完全由样本的质量决定。</p><ul><li><p><code>2.1. 正样本的选择与定义 (Selection of Positive Samples)</code></p><p>正样本，顾名思义，是那些能够明确或间接<strong>表明用户兴趣和满意度</strong>的行为。它是模型学习的“靶心”，模型的一切努力，都是为了能更准确地预测出这类行为的发生。</p><h3 id="什么是正样本？——-不仅仅是点击">什么是正样本？—— 不仅仅是点击</h3><p>一个常见的误区是认为“点击”就是唯一的正样本。在工业界，我们会根据业务目标，定义一个<strong>从弱到强的正样本信号谱系</strong>：</p><ul><li><strong>弱正样本 (Weak Positive Signals)</strong>：<ul><li><strong>点击 (Click)</strong>：这是最常用但信号也最模糊的正样本。用户可能因为标题党、好奇、误触而点击，点击后可能立刻就退出了。</li><li><strong>短时观看/停留 (Short Dwell Time)</strong>：比如观看了一个视频的15%，或者在一个商品详情页停留了10秒。</li></ul></li><li><strong>强正样本 (Strong Positive Signals)</strong>：<ul><li><strong>长时观看/停留 (Long Dwell Time)</strong>：比如一个视频被完整播放，或者用户在商品页停留了超过1分钟。这比单纯的点击更能代表用户的真实兴趣。</li><li><strong>点赞 (Like) / 收藏 (Favorite) / 分享 (Share)</strong>：这些是用户的显式表达，代表了高度的认可。</li><li><strong>加入购物车 (Add-to-Cart)</strong>：在电商场景中，这是比点击强得多、仅次于购买的信号。</li><li><strong>购买 (Purchase) / 付费 (Pay)</strong>：<strong>终极正样本</strong>，商业价值最高，代表了用户的最终决策。</li></ul></li></ul><h3 id="实践中的考量">实践中的考量</h3><ol><li><strong>与业务目标对齐</strong>：选择什么样的行为做正样本，必须和你的业务目标紧密挂钩。如果你的目标是提升用户粘性，那么“长时观看”就是比“点击”更好的正样本。如果目标是提升GMV，“购买”就是最好的正样本。</li><li><strong>数据清洗</strong>：真实的用户行为数据充满了噪声。我们需要通过规则或模型，<strong>排除掉“脏数据”</strong>，比如：<ul><li>机器人的刷量行为。</li><li>用户的快速、连续点击（可能是误触或程序bug）。</li><li>点击后“秒退”（停留时间小于1-2秒）的行为，这些甚至可以被当作负样本。</li></ul></li></ol></li><li><p><code>2.2. 负样本的选择策略 (Negative Sampling Strategies)</code></p><p>如果说正样本是“明”的，那么负样本就是“暗”的。用户与物品的交互记录构成了正样本，而<strong>海量的、用户从未交互过的物品</strong>则构成了潜在的负样本空间。如何从这片“汪洋”中，聪明地捞取能让模型有效学习的负样本，是样本构建中最具挑战性的部分。负样本的作用是为模型提供“边界”，告诉模型“用户不喜欢什么”或者“用户对什么无感”，从而让模型学会区分好坏。</p><ul><li><p><code>2.2.1. 全局随机采样 (Global Random Sampling)</code></p><p>这是最基础、最直接的负采样方法。</p><ul><li><strong>工作方式</strong>：<br>对于每一个正样本对 <code>&lt;User, Item_positive&gt;</code>，我们从<strong>整个物品库 (Item Corpus)</strong> 中，<strong>随机</strong>地抽取一个或多个该 User 从未交互过的物品 <code>Item_negative</code>，来构成训练三元组 <code>&lt;User, Item_positive, Item_negative&gt;</code>。</li><li><strong>核心目的与优势</strong>：<ol><li><strong>克服样本选择偏差 (Sample Selection Bias, SSB)</strong>：这是它最大的优点。在很多场景下，我们能观测到的只有“曝光”数据，如果只从曝光未点击的物品中选择负样本，模型就永远不知道那些从未被曝光的物品是什么样的，视野会变得越来越窄。全局随机采样，相当于让模型**“见过世面”**，给了模型一个关于“世界”是怎样的无偏估计，强迫它去学习用户的真实、泛化的兴趣，而不是只学会在一小撮热门物品里做排序。</li><li><strong>实现简单</strong>：逻辑清晰，易于实现。</li></ol></li><li><strong>缺点与挑战</strong>：<ol><li><strong>训练效率低下（Easy Negative 问题）</strong>：想象一下，为了教模型“用户喜欢科幻电影”，我们随机采样到的负样本大概率是“一口锅”、“一条毛巾”或者“一本言情小说”。这些负样本太“简单”了，模型几乎毫不费力就能将它们与科幻电影区分开。模型在这些样本上学不到太多有用的信息（梯度很小），导致训练收敛速度慢，效率低下。</li><li><strong>可能采到“假负例” (False Negative)</strong>：这是个很严重的问题。我们随机采样的物品，<strong>有可能是用户未来会喜欢的，只是他还没发现而已</strong>。我们却错误地把它作为负样本喂给了模型，让模型去学习“用户不喜欢这个物品”，这会干扰模型的正常学习，给训练引入了错误的信号。</li><li><strong>无法学习细粒度差异</strong>：由于绝大多数负样本都是“风马牛不相及”的简单负样本，模型缺乏足够的机会去学习<strong>区分那些相似物品之间的细微差别</strong>。比如，它很难学会“为什么用户喜欢《流浪地球》而不是《上海堡垒》”，因为它的“同学”大部分是“锅碗瓢盆”。</li></ol></li></ul></li><li><p><code>2.2.2. Batch内负采样 (In-batch Negative Sampling)</code><br>在全局随机采样的“低效”问题背景下，Batch内负采样应运而生。它以其惊人的工程效率和出色的效果，成为了当前双塔召回模型训练中<strong>最主流、最核心</strong>的负采样策略。</p><ul><li><p><strong>工作原理与结构 (Mechanism &amp; Structure)</strong></p><p>其核心思想是**“就地取材”**，不再需要去一个庞大的全局物品库里额外捞取负样本，而是巧妙地利用了每个训练批次（batch）内已有的数据。</p><ol><li><strong>数据准备</strong>：首先，数据加载器会准备一个批次的数据，这个批次由 <code>N</code> 个独立的<strong>正样本对</strong> <code>&lt;User, Positive_Item&gt;</code> 构成。<ul><li>例如，一个 <code>batch_size=N</code> 的批次看起来像： <code>[(U₁, I₁), (U₂, I₂), ..., (Uₙ, Iₙ)]</code></li></ul></li><li><strong>动态构建训练实例</strong>：模型在训练时，会以批次中的<strong>每一个样本对</strong>为中心，动态地构建其对应的正负样本集。我们以第一个样本 <code>(U₁, I₁)</code> 为例：<ul><li><strong>正样本 (Positive)</strong>：对于用户 <code>U₁</code>，其正样本就是 <code>I₁</code>。</li><li><strong>负样本 (Negatives)</strong>：对于用户 <code>U₁</code>，其负样本就是这个批次中<strong>所有其他的 <code>N-1</code> 个物品</strong> <code>&#123;I₂, I₃, ..., Iₙ&#125;</code>。</li></ul></li><li><strong>训练目标</strong>：对于 <code>U₁</code> 而言，模型的目标就转化成了一个<strong>多分类问题</strong>：从 <code>&#123;I₁, I₂, I₃, ..., Iₙ&#125;</code> 这 <code>N</code> 个物品中，准确地把 <code>I₁</code> 给识别出来。这天然地适配了我们之前讨论过的 <strong>Softmax 交叉熵损失函数</strong>。</li></ol><p>这个过程会对批次内的每一个样本都重复一遍。当轮到处理 <code>(U₂, I₂)</code> 时，<code>I₂</code> 就是正样本，而 <code>&#123;I₁, I₃, ..., Iₙ&#125;</code> 就成了它的负样本集。</p></li><li><p><strong>优缺点与挑战 (Pros, Cons &amp; Challenges)</strong></p><ul><li><strong>优点 (Pros)</strong>：<ol><li><strong>极致的训练效率</strong>：这是它最大的优势。因为它不需要任何额外的数据库查询或磁盘I/O来获取负样本，所有需要的样本（正例和负例）都已经一次性加载到内存/显存中，极大提升了数据吞吐量和训练速度。</li><li><strong>隐式的困难负样本挖掘</strong>：由于一个批次内的物品都是被其他用户真实点击过的，它们通常是<strong>热门或高质量的物品</strong>。让模型去区分“我喜欢的这个好东西”和“别人喜欢的那些好东西”，本身就是一种非常有效的<strong>困难负样本训练</strong>。这强迫模型去学习更细粒度的、个性化的用户偏好，而不是简单地区分“好”与“坏”。</li></ol></li><li><strong>缺点与挑战 (Cons &amp; Challenges)</strong>：<ol><li><strong>样本选择偏差 (Sample Selection Bias, SSB)</strong>：这是它最主要的理论缺陷。因为负样本全部来自于热门物品，模型在训练时<strong>永远也见不到那些冷门或长尾的物品</strong>。这导致模型的“视野”是有偏的，它只擅长在热门物品中做选择，对于挖掘长尾兴趣的能力有限。</li><li><strong>假负例问题 (False Negatives)</strong>：这个问题在这里尤为突出。用户 <code>U₁</code> 可能本身也喜欢物品 <code>I₂</code>（只是没被观测到），但在训练时 <code>I₂</code> 却被当作了 <code>U₁</code> 的负样本，这会向模型传递一个错误的、有冲突的信号。</li><li><strong>对 Batch Size 高度敏感</strong>：为了让每个正样本都能有足够多、足够丰富的负样本进行对比，该方法要求 <strong>Batch Size 非常大</strong>（通常是1024, 4096甚至更大）。小的 Batch Size 会导致负样本集既少又缺乏多样性，训练效果很不稳定。这对训练硬件（如GPU显存）提出了很高的要求。</li></ol></li></ul></li></ul></li><li><p><code>2.2.3. 高级困难负样本挖掘 (Advanced Hard Negative Mining)</code></p><p>Batch内负采样提供的是一种“隐式”的、便捷的困难负样本。但在很多场景下，我们希望更“显式”地、更有针对性地为模型提供“高质量的难题”来攻克，这就是高级困难负样本挖掘的目标。<strong>“困难负样本”的定义</strong>：那些与正样本<strong>非常相似</strong>，以至于模型在当前状态下<strong>很难将它们区分开</strong>的负样本。在向量空间中，它们通常是与用户向量或正样本物品向量距离非常近的负样本点。<strong>常见的挖掘策略</strong>：</p><ol><li><strong>基于 Embedding 空间的挖掘 (Offline)</strong>：<ul><li>这是目前工业界非常流行的一种离线挖掘方式。</li><li><strong>流程</strong>：使用一个预训练好的双塔模型，先为所有物品生成 Embedding。对于一个正样本 <code>I+</code>，我们利用 <strong>ANN 引擎（如Faiss）</strong> 在海量的物品向量中，找出与 <code>I+</code> 的 Embedding <strong>距离最近</strong>的 Top-K 个物品。排除掉其他正样本后，这些“近邻”就是非常高质量的困难负样本，因为它们在语义上与正样本高度相似。</li></ul></li><li><strong>基于模型预测的挖掘 (Online)</strong>：<ul><li>在训练的每一步，对于一个 <code>&lt;U, I+&gt;</code> 对，我们可以额外拿 <code>U</code> 的向量去和一个<strong>实时的、包含大量候选物品的向量索引</strong>进行查询，找出除了 <code>I+</code> 之外，得分最高的那些物品。这些物品就是模型当前“认知”中最容易混淆的负样本。这种方式更加动态，但实现也更复杂。</li></ul></li><li><strong>基于业务规则/知识图谱的挖掘</strong>：<ul><li>利用业务先验知识来构造困难负样本。</li><li><strong>例子</strong>：<ul><li><strong>同类目不同品牌</strong>：用户点击了“华为手机”，那么“小米手机”、“苹果手机”就是它的困难负样本。</li><li><strong>同session内曝光未点击</strong>：用户在一次会话中，浏览了A、B、C三个物品，但最终只点击了A。那么B和C对于A来说，就是非常好的困难负样本。</li></ul></li></ul></li><li><strong>从下游排序模型中挖掘（“蒸馏”思想）</strong>：<ul><li>这是一个非常强大的策略。排序模型（精排）通常比召回模型强大得多。那些<strong>能够通过召回和粗排，但最终在精排阶段被打了低分</strong>的物品，是“金牌”困难负样本。</li><li>将这些样本捞回来给召回模型做负样本，相当于在告诉召回模型：“你觉得这个东西还不错，但更聪明的精排模型认为它不行，你要好好学学为什么不行。” 这能极大地帮助召回模型理解更复杂的排序逻辑。</li></ul></li></ol><p><strong>使用困难负样本的挑战</strong>：挖掘的“度”很重要。如果负样本“太难”（甚至就是假负例），可能会让模型训练过程不稳定，甚至不收敛。因此，实践中往往是将<strong>困难负样本</strong>与**简单负样本（如全局随机采样）**按照一定比例混合起来进行训练，达到一个平衡。</p></li></ul></li></ul><h3 id="3-训练范式与损失函数-Training-Paradigms-Loss-Functions-LTR"><code>3. 训练范式与损失函数 (Training Paradigms &amp; Loss Functions - LTR)</code></h3><ul><li><p><code>3.1. Pointwise (点对) - BCELoss</code></p><p>Pointwise 是最直观、最简单的训练范式。它将复杂的“排序”问题，彻底简化为了一个基础的“二分类”问题。它在看待数据时，是“一个一个点”地看，完全独立，不考虑样本之间的任何关联。</p><ul><li><strong>处理单元</strong>：一个独立的 <code>&lt;User, Item&gt;</code> 对。</li><li><strong>任务转化</strong>：对于每一个 <code>&lt;User, Item&gt;</code> 对，模型需要回答一个问题：“这位用户会点击这个物品吗？” (Yes/No)。</li><li><strong>标签 (Label)</strong>：<code>y=1</code> (代表正样本，如点击) 或 <code>y=0</code> (代表负样本，如曝光未点击)。</li><li><strong>模型目标</strong>：模型需要输出一个概率值 <code>p</code> (介于0和1之间)，这个 <code>p</code> 代表了模型预测该用户会点击该物品的概率。Pointwise 的目标就是让预测概率 <code>p</code> 无限逼近真实标签 <code>y</code>。</li></ul><p><strong>损失函数：BCELoss (Binary Cross-Entropy Loss / 二元交叉熵损失)</strong></p><p>为了达成上述目标，Pointwise 范式最常使用的损失函数就是 BCELoss。</p><ul><li><strong>公式</strong>：</li></ul><p><img src="image.png" alt="image.png"></p><ul><li><strong>工作原理详解</strong>：<ul><li><strong>当处理正样本时 (<code>y_i=1</code>)</strong>：<ul><li>公式简化为 <code>L = -log(p_i)</code>。</li><li><code>p_i</code> 是模型对这个正样本的预测概率。为了让损失 <code>L</code> 最小，<code>log(p_i)</code> 就要最大，这意味着 <code>p_i</code> 必须无限趋近于 1。</li><li><strong>效果</strong>：强迫模型对正样本输出高分。</li></ul></li><li><strong>当处理负样本时 (<code>y_i=0</code>)</strong>：<ul><li>公式简化为 <code>L = -log(1-p_i)</code>。</li><li>为了让损失 <code>L</code> 最小，<code>log(1-p_i)</code> 就要最大，这意味着 <code>1-p_i</code> 必须无限趋近于 1，也就是 <code>p_i</code> 必须无限趋近于 0。</li><li><strong>效果</strong>：强迫模型对负样本输出低分。</li></ul></li></ul></li><li><strong>总结与评价</strong>：<ul><li>Pointwise 的优点是<strong>简单、易于理解、并行性好</strong>。</li><li>但它的<strong>核心缺陷</strong>在于，它<strong>没有直接对“排序”这个目标进行优化</strong>。它只关心分数预测的绝对准确性，而不在乎物品之间的相对顺序。一个模型的预测可能是 <code>&#123;正:0.6, 负:0.5&#125;</code>，另一个模型的预测是 <code>&#123;正:0.9, 负:0.1&#125;</code>，对于BCELoss来说，两者的损失可能差不多，但后者的排序效果显然要好得多。</li></ul></li></ul></li><li><p><code>3.2. Pairwise (对对) - BPR Loss, Triplet Loss</code></p><p>Pairwise 范式是对 Pointwise 的一次重大升级，它更贴近“排序”的本质。它不再孤立地看待每个物品，而是将重点放在**“比较”**上。它认为，一个好的推荐模型，关键在于能准确判断出一对物品中，哪一个更值得被推荐。</p><ul><li><strong>处理单元</strong>：一个三元组 <code>&lt;User, Positive_Item, Negative_Item&gt;</code>。</li><li><strong>任务转化</strong>：对于一个用户，给定一个他喜欢的物品 <code>I+</code> 和一个他不喜欢的物品 <code>I-</code>，模型需要回答：“<code>I+</code> 是否比 <code>I-</code> 更值得推荐？”。</li><li><strong>模型目标</strong>：让模型为 <code>I+</code> 打出的分数 <code>Score_pos</code>，<strong>高于</strong>为 <code>I-</code> 打出的分数 <code>Score_neg</code>。即 <code>Score_pos &gt; Score_neg</code>。</li></ul><p><strong>损失函数1：BPR Loss (Bayesian Personalized Ranking Loss)</strong></p><p>BPR Loss 是 Pairwise 范式中最具代表性的损失函数之一。</p><ul><li><p><strong>公式</strong>：</p><p><img src="image%201.png" alt="image.png"></p></li><li><p><strong>工作原理详解</strong>：</p><ol><li><strong>计算分差</strong>：首先计算正样本得分与负样本得分的差值 <code>x = Score_&#123;pos&#125; - Score_&#123;neg&#125;</code>。我们的目标是让这个差值 <code>x</code> 越大越好。</li><li><strong>Sigmoid 激活</strong>：将差值 <code>x</code> 送入 Sigmoid 函数 <code>σ(x)</code>。Sigmoid 函数会将任何实数映射到 <code>(0, 1)</code> 区间。在这里，<code>σ(x)</code> 可以被巧妙地理解为**“模型认为正样本排在负样本前面的概率”**。<code>x</code> 越大，<code>σ(x)</code> 就越接近1。</li><li><strong>负对数似然</strong>：最后，对这个概率取 <code>log</code>。这是在应用最大似然估计的原理。<strong>最小化 <code>log(概率)</code>，就等价于最大化这个概率</strong>。</li></ol><ul><li><strong>效果</strong>：整个优化过程，就是在强迫模型去最大化“正样本排在负样本前面”的概率。</li></ul></li></ul><p><strong>损失函数2：Triplet Loss (三元组损失 / Hinge Loss)</strong></p><p>Triplet Loss 是另一种非常流行的 Pairwise 损失，它比 BPR 的要求更“严格”。</p><ul><li><p><strong>公式</strong>：</p><p><img src="image%202.png" alt="image.png"></p></li><li><p><strong>工作原理详解</strong>：</p><ol><li><strong>引入安全边界 (Margin)</strong>：<code>m</code> 是一个预先设定的超参数，称为“边界”或“间隔”。</li><li><strong>更严格的目标</strong>：Triplet Loss 不仅要求 <code>Score_pos &gt; Score_neg</code>，它要求 <code>Score_pos</code> 必须比 <code>Score_neg</code> <strong>至少大 <code>m</code></strong>，即 <code>Score_pos - Score_neg &gt; m</code>。</li><li><strong>Hinge 机制</strong>：<code>max(0, ...)</code> 的作用在于：<ul><li>如果 <code>Score_pos - Score_neg</code> 已经大于 <code>m</code> 了，说明这个“PK对”已经被模型轻松、正确地分开了，那么 <code>m - (Score_pos - Score_neg)</code> 就是一个负数，<code>max(0, ...)</code> 的结果就是 0。这意味着<strong>对于简单的、区分明显的样本对，不产生任何损失</strong>。</li><li>只有当这个边界条件<strong>不被满足</strong>时 (<code>Score_pos - Score_neg &lt; m</code>)，才会产生一个正的损失值。</li></ul></li></ol><ul><li><strong>效果</strong>：这种机制强迫模型将更多的精力集中在那些**“模棱两可”的、难以区分的困难样本对**上，努力将它们的得分差距拉开至少 <code>m</code>，从而使得学出来的向量空间分布更合理，边界更清晰。</li></ul></li></ul></li><li><p><code>3.3. Listwise (列表对) - Softmax Cross-Entropy Loss</code></p><p>Listwise 是三种 LTR 范式中理论上最先进、最贴近真实排序目标的思想。Pointwise 只看“点”，Pairwise 比较“线”（一对点），而 Listwise 则着眼于整个“面”——它将<strong>一个完整的候选列表视为一个整体</strong>来进行优化。</p><ul><li><strong>处理单元</strong>：一个用户的“学习实例”，由 <code>&lt;User, Positive_Item, &#123;A_Set_of_Negative_Items&#125;&gt;</code> 构成。</li><li><strong>任务转化</strong>：Listwise 将排序问题巧妙地转化成了一个**“在候选列表中，找出哪个是正确答案”的多分类问题**。</li><li><strong>模型目标</strong>：对于给定的候选列表，模型需要为真正的那个正样本，分配到最高的概率。</li></ul><h3 id="工作原理与损失函数"><strong>工作原理与损失函数</strong></h3><p><img src="image%203.png" alt="image.png"></p><p>图示完美地展示了其工作流程：</p><ol><li><p><strong>计算原始分 (Logits)</strong>：对于一个用户，模型首先计算出他与<strong>列表中每一个物品</strong>（1个正样本 + N个负样本）的相似度得分。我们得到一个分数向量 <code>[s+, s₁⁻, s₂⁻, ..., sₙ⁻]</code>。</p></li><li><p><strong>Softmax 归一化</strong>：将这个分数向量送入 <strong>Softmax 函数</strong>。</p><p>P(itemi)=∑j∈Listexp(sj)exp(si)</p><ul><li><strong>作用</strong>：Softmax 会将原始的、任意大小的分数，转换成一个<strong>概率分布</strong>。所有物品的概率值都在 (0, 1) 之间，且它们的总和为1。<code>P(item+)</code> 就代表了模型认为“在这个列表中，正样本 item+ 是正确答案”的预测概率。</li></ul></li><li><p><strong>交叉熵损失 (Cross-Entropy Loss)</strong>：</p><ul><li><p><strong>真实标签 (Ground Truth)</strong>：在这个多分类问题中，真实的标签是一个 <strong>one-hot 向量</strong>，只有正样本对应的位置是1，其他所有负样本的位置都是0。即 <code>y = [1, 0, 0, ..., 0]</code>。</p></li><li><p><strong>损失函数</strong>：计算模型的预测概率分布和真实的 one-hot 标签分布之间的交叉熵。由于真实标签中只有正样本位置是1，公式可以简化为：</p><p>LListwise=−log(P(item+))</p></li><li><p><strong>优化目标</strong>：最小化这个损失，就等同于<strong>最大化正样本的预测概率 <code>P(item+)</code></strong>。为了让正样本的概率变大，模型必须同时做到两件事：<strong>提升正样本的分数 <code>s+</code></strong>，以及<strong>压低所有负样本的分数 <code>s⁻</code></strong>。</p></li></ul></li></ol><ul><li><strong>总结与评价</strong>：<ul><li>Listwise 通过一次性考虑整个列表，能够更好地学习到物品之间的相互关系，其优化目标也与最终的排序评估指标（如NDCG）更接近。</li><li>它天然地适用于 Batch 内负采样的场景，因为 Batch 内负采样本身就为每个正样本提供了一个“列表”来进行对比。</li></ul></li></ul></li><li><p><code>3.4. 训练高级技巧</code></p><ul><li><strong>向量归一化 (L2 Normalization)</strong><ul><li><strong>是什么</strong>：在计算完用户或物品的原始 Embedding 向量后，通过一个数学操作，使其 L2 范数（可以理解为向量的“长度”）等于1。这相当于把所有向量都投影到一个高维的“单位球”的表面上。</li><li><strong>为什么至关重要</strong>：<ol><li><strong>稳定训练，防止模型“作弊”</strong>：这是最核心的原因。如果没有归一化，模型为了让正样本的点积 <code>u·v</code> 变大，可能会走一条“捷径”：不是努力地让 <code>u</code> 和 <code>v</code> 的方向变得更接近，而是无脑地把 <code>u</code> 和 <code>v</code> 的长度（模长）变得无限大。这是一种无效的学习。归一化后，所有向量长度都为1，模型唯一的优化路径就只剩下<strong>调整向量的方向</strong>，这才能真正学到语义上的相似性。</li><li><strong>让点积等价于余弦相似度</strong>：对于长度为1的单位向量，<code>u·v = |u||v|cos(θ) = cos(θ)</code>。这让我们的优化目标变得非常明确和纯粹：就是最小化用户和正样本向量之间的夹角。</li><li><strong>提升数值稳定性</strong>：防止向量中的值在训练中爆炸或消失，导致梯度问题。</li></ol></li></ul></li><li><strong>温度系数 (Temperature τ)</strong><ul><li><strong>是什么</strong>：一个标量超参数 <code>τ</code>，在将分数（logits）送入 Softmax 函数之前，先用它来对分数进行缩放：<code>Softmax(scores / τ)</code>。</li><li><strong>作用：控制模型预测的“锐利度”</strong><ul><li><strong>低温 (τ &lt; 1, e.g., 0.1)</strong>：<code>scores</code> 的差距会被放大，Softmax 的输出会变得非常**“尖锐 (Peaky)”<strong>。模型会以极高的置信度（比如99%的概率）指向它认为最可能的那个选项。这会</strong>强迫模型更关注那些最困难的负样本**，努力将它们与正样本彻底分开。</li><li><strong>高温 (τ &gt; 1, e.g., 2.0)</strong>：<code>scores</code> 的差距会被缩小，Softmax 的输出会变得非常**“平滑 (Smooth)”**。概率会更均匀地分布在多个选项上。这会让模型变得不那么“自信”，可以防止它在有噪声的标签上过拟合。</li></ul></li><li><strong>与归一化的关系</strong>：当使用了 L2 归一化后，logits 的值被限制在 <code>[-1, 1]</code> 的小区间内。此时，如果<strong>不使用温度系数（相当于τ=1）</strong>，不同 logits 之间的差距会很小，导致 Softmax 输出的概率区分度不大，损失函数的梯度信号微弱，模型难以学习。因此，<strong>归一化之后，通常必须配合一个较小的温度系数 <code>τ</code> 来“放大”信号，让模型有效训练</strong>。</li></ul></li><li><strong>采样修正损失 (Sampling-Corrected Loss): NCE, Sampled Softmax</strong><ul><li><strong>动机</strong>：我们之前讨论过，无论是全局随机采样还是 Batch 内负采样，我们构造的负样本分布，都和数据真实的全局分布存在<strong>偏差 (Bias)</strong>。采样修正损失就是为了在数学上<strong>校正</strong>这种偏差。</li><li><strong>NCE (Noise Contrastive Estimation / 噪声对比估计)</strong>：<ul><li>它将问题从“预测是哪个物品”转化为了“判断一个样本是来自真实数据还是噪声”。</li><li>它的损失函数中，会显式地<strong>减去 <code>log(Q(item))</code> 这一项</strong>，其中 <code>Q(item)</code> 是这个物品在我们的负采样（噪声）分布中被抽到的概率。这个修正项的作用是，对于那些<strong>很容易被当做负样本采到的热门物品，对其进行“惩罚”</strong>，从而得到一个更无偏的、更能反映真实喜好的分数。</li></ul></li><li><strong>Sampled Softmax</strong>：<ul><li>当物品库极其巨大时（如数百万），计算标准 Softmax 的分母（需要对所有物品求和）是不可能的。</li><li>Sampled Softmax 通过只在**“一个正样本 + 少量随机采样的负样本”**这个小集合上计算 Softmax 来近似全局 Softmax。</li><li>为了保证这种近似的无偏性，它的损失函数同样会利用负样本的采样概率 <code>Q(item)</code> 来对结果进行校正。</li></ul></li></ul></li></ul></li></ul><h3 id="4-高级召回模型架构-Advanced-Recall-Model-Architectures-拓展"><code>4. 高级召回模型架构 (Advanced Recall Model Architectures)[拓展]</code></h3><ul><li><p><code>4.1. 基于图的召回 (Graph-based): DeepWalk</code></p><p>传统的协同过滤或双塔模型，往往关注用户和物品的直接交互，但可能会忽略物品之间更深层次的、间接的关联（比如 A-B-C，A和C之间的关系）。图（Graph）结构是描述这种复杂网络关系的天然工具，而 DeepWalk 则是将图结构与深度学习结合的开创性工作。核心思想：将所有用户的行为历史聚合成一张巨大的<strong>物品关系图</strong>，然后在这张图上“漫步”，生成物品序列，最后用自然语言处理（NLP）中的 Word2Vec 模型来学习图中每个物品的向量表示（Embedding）。</p><ul><li><p><code>建图 -&gt; 随机游走 -&gt; Word2Vec训练</code></p><p><strong>工作流程三部曲</strong></p><p><strong>第一步：建图 (Graph Construction)</strong></p><ul><li><strong>节点 (Nodes)</strong>：图中的每一个节点，就是一个独立的<strong>物品 (Item)</strong>。</li><li><strong>边 (Edges)</strong>：用户的一次<strong>连续行为</strong>，构成了物品节点之间的有向边。<ul><li><strong>过程</strong>：我们收集大量用户的行为序列，比如用户1的行为是 <code>物品A -&gt; 物品B -&gt; 物品C</code>，用户2的行为是 <code>物品D -&gt; 物品B -&gt; 物品E</code>。</li><li><strong>聚合</strong>：我们将这些序列中的相邻关系全部投影到一张图上。<code>A-&gt;B</code> 就在节点A和B之间画一条边，<code>B-&gt;C</code>就在B和C之间画一条边，<code>D-&gt;B</code>就在D和B之间画一条边……最终，所有用户的行为汇集成一张包含了所有物品及其流转关系的复杂网络。边的权重可以由转换发生的频率来隐式定义。</li></ul></li><li><strong>意义</strong>：这张图浓缩了所有用户的“群体智慧”，物品之间的连接强度和路径，反映了它们在真实世界中的关联性。</li></ul><p><strong>第二步：随机游走 (Random Walk)</strong></p><ul><li><strong>目的</strong>：我们手头最强大的序列处理工具是 Word2Vec，但它只能处理线性的句子，不认识图这种非线性结构。随机游走的任务，就是把图的结构信息，<strong>“翻译”成 Word2Vec 能读懂的线性序列格式</strong>。</li><li><strong>过程</strong>：<ol><li>在图上随机选择一个节点作为起点。</li><li>从当前节点，等概率地随机跳转到它的一个邻居节点。</li><li>不断重复上一步，直到走满一个预设的长度（比如80步）。</li><li>这样走出的一条节点序列，如 <code>[Item_A, Item_B, Item_D, Item_F, ...]</code>，就被我们视作一个**“句子”**。</li></ol></li><li><strong>效果</strong>：通过在图上进行海量的随机游走，我们就能得到数百万条这样的“物品句子”。由于游走被图的结构所约束，那些在图中连接紧密的物品（比如同一个“社区”内的物品），会更频繁地在这些“句子”中共同出现。</li></ul><p><strong>第三步：Word2Vec 训练</strong></p><ul><li><strong>类比</strong>：这是最关键的认知转换。<ul><li>图中的<strong>每个物品节点</strong> &lt;==&gt; 语言中的<strong>每个单词</strong>。</li><li>随机游走产生的<strong>物品序列</strong> &lt;==&gt; 语言中的<strong>一个句子</strong>。</li></ul></li><li><strong>模型 (Skip-gram)</strong>：我们使用 Word2Vec 中的 Skip-gram 模型来训练。它的目标是**“根据中心词预测上下文”**。</li><li><strong>应用</strong>：我们将物品“句子”喂给 Skip-gram 模型。模型在学习时，会不断地尝试根据序列中的一个中心物品（如 Item_B），去预测它前后窗口内的其他物品（如 Item_A, Item_D）。</li><li><strong>结果</strong>：为了完成这个预测任务，模型必须为每个物品学习一个向量 Embedding。那些<strong>经常在相似上下文（即相似的邻域结构）中出现的物品，最终会被赋予相似的向量表示</strong>。至此，我们就成功地将图的结构信息，编码进了每个物品的 Embedding 之中。</li></ul></li></ul></li><li><p><code>4.2. 多兴趣召回 (Multi-Interest): MIND (Capsule Network)</code></p><p><strong>核心痛点：<strong>标准双塔模型最大的局限性，在于它试图将一个用户的</strong>所有兴趣</strong>——无论是看电影、买菜还是学编程——都强行压缩到<strong>一个单一的向量</strong>中。这就像把一个人的所有性格特点平均一下，得到一个“不好不坏”的平庸结论，造成了巨大的信息损失。</p><p><strong>核心思想：<strong>一个用户有多个兴趣，我们就应该为他提取出</strong>多个兴趣向量</strong>。MIND 模型巧妙地借鉴了胶囊网络中的**“动态路由 (Dynamic Routing)”**机制，来自动地将用户的行为序列聚类成多个兴趣簇，并为每个簇生成一个代表性的兴趣向量。</p><p><strong>工作流程：</strong></p><p><strong>第一步：动态路由与多兴趣提取</strong></p><ul><li><strong>目标</strong>：输入一个用户的历史行为物品序列（的Embedding矩阵），输出 K 个代表不同兴趣的向量（称为“兴趣胶囊”）。</li><li><strong>比喻：智能分拣系统</strong><ul><li>想象一个传送带上放着用户过去点击过的50个物品（的Embedding），我们有 K 个篮子（兴趣胶囊）。我们希望能自动地把“电子产品”都分到1号篮，“服饰鞋帽”都分到2号篮……</li></ul></li><li><strong>迭代式的“投票-聚类”过程 (Routing by Agreement)</strong>：<ol><li><strong>初始化</strong>：随机生成 K 个“兴趣篮子”的初始位置。</li><li><strong>循环迭代 (通常3次)</strong>：<ul><li><strong>a. 投票 (Agreement)</strong>：传送带上的<strong>每一个物品</strong>，都会看一下当前的 K 个篮子，然后判断自己和哪个篮子的“气质”最像（向量点积最大）。</li><li><strong>b. 分配权重</strong>：根据相似度，这个物品会决定将自己“归属于”每个篮子的权重（通过Softmax计算）。比如，一个“iPhone”可能会以90%的权重归属于“数码篮”，10%的权重归属于“生活篮”。</li><li><strong>c. 更新篮子位置</strong>：<strong>每一个篮子</strong>收集所有物品分配给它的权重，然后通过对所有物品向量进行<strong>加权求和</strong>，来更新自己的位置。这个新位置更能代表所有“支持者”（高权重物品）的中心。</li></ul></li><li><strong>收敛</strong>：经过几轮迭代，K 个篮子的位置会稳定下来，每一个篮子都准确地找到了它所代表的那个“兴趣簇”的中心。这 K 个最终的篮子向量，就是我们提取出的用户多兴趣向量。</li></ol></li></ul><p><strong>第二步：目标注意力机制 (Target Attention)</strong></p><ul><li><strong>问题</strong>：现在，我们有了用户的 K 个兴趣向量（比如“数码兴趣”和“服饰兴趣”），当要预测用户是否会点击一个<strong>目标商品</strong>（比如一个“鼠标”）时，我们应该用哪个兴趣向量呢？</li><li><strong>解决方案：让目标物品自己来决定！</strong><ul><li><strong>过程</strong>：<ol><li>拿出“鼠标”的 Embedding。</li><li>让“鼠标”的 Embedding，分别去和用户的 K 个兴趣向量计算<strong>相似度</strong>。</li><li>“鼠标”会发现，它和“数码兴趣”向量的相似度极高，和“服饰兴趣”向量的相似度极低。</li><li>将这些相似度得分通过 Softmax 转换成一组<strong>注意力权重</strong>。此时，“数码兴趣”的权重会非常高（如0.98），其他兴趣的权重会非常低。</li><li>用这组权重，对 K 个兴趣向量进行<strong>加权求和</strong>。</li></ol></li></ul></li><li><strong>结果</strong>：最终得到的用户向量，几乎就等于他原始的“数码兴趣”向量。这个最终向量是<strong>为当前目标物品“量身定制”的</strong>，它动态地聚焦于用户最相关的那个兴趣侧面，极大地提升了预测的精准性。</li></ul></li><li><p><code>4.3. 生成式召回 (Generative): Deep Retrieval (Item-to-Path)[拓展]</code></p><p><strong>核心思想：从“匹配”到“生成”</strong></p><ul><li><strong>传统判别式模型 (如双塔)</strong>：它的核心是回答一个问题：“给定的 <code>&lt;User, Item&gt;</code> 对，它们的匹配分数是多少？” 它是在<strong>判断</strong>一个已存在的对。</li><li><strong>生成式召回 (DR)</strong>：它的核心是回答一个问题：“对于这个 <code>User</code>，他/她感兴趣的**‘兴趣篮子’<strong>（或称为‘路径’，Path）是哪几个？” 然后再看哪些物品属于这些篮子。它是在为用户</strong>生成**一个兴趣表示。</li></ul><p>你可以把这种方法想象成一个**“兴趣篮子”**模型。</p><ol><li><strong>定义篮子</strong>：我们不直接学习物品本身，而是在系统中预先定义好几千个全局的“兴趣篮子”（在论文里称为“路径”Path）。比如：<code>[科幻电影篮]</code>, <code>[周末烘焙篮]</code>, <code>[篮球装备篮]</code> 等等。</li><li><strong>模型任务</strong>：模型的任务有两个：<ul><li><strong>对用户</strong>：预测这个用户最喜欢哪几个“篮子”。</li><li><strong>对物品</strong>：决定每个物品应该被放入哪个“篮子”。</li></ul></li><li><strong>召回方式</strong>：当一个用户来访时，模型先预测出他最感兴趣的几个篮子（比如“科幻电影篮”和“篮球装备篮”），然后<strong>直接把这两个篮子里的所有物品都作为召回结果</strong>推荐给他。</li></ol><p>这种“先生成兴趣篮子，再取出物品”的方式，就是所谓的<strong>生成式召回</strong>。</p><p><strong>遇到的最大难题：“路径坍缩”</strong></p><p>这个想法虽好，但在实际训练中会遇到一个致命问题——<strong>路径坍缩 (Path Collapse)</strong>。</p><ul><li><strong>简单比喻</strong>：<strong>“所有鸡蛋都放进了一个篮子”</strong>。</li><li><strong>问题描述</strong>：模型在训练时会“偷懒”，它会发现一个降低损失最快的捷径：把<strong>所有物品</strong>（无论科幻电影还是篮球鞋）都拼命地塞进<strong>同一个、最热门的篮子</strong>里。</li><li><strong>后果</strong>：最终，几千个篮子里只有一个被有效使用，其他都空了。模型完全丧失了区分用户多兴趣的能力，这个精巧的设计也就失败了。</li></ul><p><strong>聪明的解决方案：“两步走”的EM算法</strong></p><p>为了解决“路径坍缩”这个“鸡生蛋还是蛋生鸡”的难题，研究者们采用了一种叫 <strong>EM</strong> 的算法，把复杂的训练过程拆成了交替进行的两步。</p><p><strong>第一步：E-Step (分篮子 - Path Assignment)</strong></p><ul><li><strong>目标</strong>：<strong>打破“所有鸡蛋放一个篮子”的局面</strong>。</li><li><strong>做法</strong>：在这一步，我们先“冻结”住神经网络，不让它动。然后，对于一个用户的历史物品，我们用一个<strong>聪明的规划算法（贪心算法）</strong>，强制性地为他选出<strong>多个</strong>最合适的篮子。</li><li><strong>通俗理解</strong>：我们不再听凭模型“偷懒”，而是像一个“整理师”，手动帮用户把他的物品（《三体》、篮球、烤箱）<strong>合理地分配到不同的篮子里</strong>（“科幻篮”、“运动篮”、“生活篮”）。这一步保证了分配结果是<strong>多样化</strong>的。</li></ul><p><strong>第二步：M-Step (学分配 - Network Update)</strong></p><ul><li><strong>目标</strong>：<strong>让模型学会我们刚才聪明的分配方法</strong>。</li><li><strong>做法</strong>：现在，我们“冻结”住上一步分配好的“篮子方案”，把它当作**“标准答案”**。然后，我们解冻神经网络，开始训练它。</li><li><strong>通俗理解</strong>：我们指着分配好的篮子告诉模型：“看，对于这个用户，这就是他兴趣的正确答案（他喜欢这3个篮子），你要好好学习，下次要能准确地预测出来！”</li></ul><p>通过**“先手动分好篮子”** -&gt; <strong>“再让模型学习这个分配结果”</strong> -&gt; <strong>“学好后再更智能地分篮子”</strong>……这样来回交替进行，模型就被引导着学会了如何利用多个篮子来刻画用户的多元兴趣，从而完美地解决了“路径坍缩”的问题。</p></li><li><p><code>4.4. 解决特定问题的模型演化</code></p><ul><li><p><code>多目标学习: MMoE (与大模型MoE的对比)</code></p><p><strong>要解决的问题：当“鱼”和“熊掌”都想要</strong></p><p>在现代推荐系统中，业务目标往往是多元的。我们不仅希望用户<strong>点击</strong>（提升CTR），还希望他们<strong>长时间观看</strong>（提升Duration）、<strong>点赞/分享</strong>（提升Interaction），甚至<strong>购买</strong>（提升CVR）。这些目标之间有时甚至是相互冲突的（比如，标题党内容CTR高，但观看时长短）。如果只用一个模型优化一个目标，必然会顾此失彼。</p><p><strong>解决方案：MMoE (Multi-gate Mixture-of-Experts)</strong></p><p>MMoE 是一种非常优雅且高效的多任务学习框架，它的核心思想是**“共享专家，任务独有门控”**。</p><ul><li><strong>架构组成</strong>：<ol><li><strong>多个专家网络 (Experts)</strong>：模型底层设置了多个共享的、结构相同但参数不共享的子网络（即“专家”）。每个专家都可以从输入特征中学习到一种特定的、有侧重的知识表示。你可以把它们想象成一个顾问团里的“财务专家”、“市场专家”、“技术专家”等。</li><li><strong>多个门控网络 (Gating Networks)</strong>：这是MMoE的精髓。它<strong>为每一个学习任务都配备一个专属的“门控网络”</strong>。这个门控网络也是一个小型神经网络，它的作用是根据输入特征，智能地决定应该以多大的“权重”去听取每一位专家的意见。</li></ol></li><li><strong>工作流程</strong>：<ol><li>输入特征同时送入所有的专家网络，得到多份并行的“专家意见”。</li><li>对于<strong>任务A（如预测点击）</strong>，其专属的<strong>门控网络A</strong>会输出一组权重，比如 <code>&#123;专家1: 0.7, 专家2: 0.1, 专家3: 0.2&#125;</code>。然后用这组权重对所有专家的意见进行加权求和，得到专门用于预测点击的最终表示。</li><li>同时，对于<strong>任务B（如预测时长）</strong>，其专属的<strong>门控网络B</strong>可能会输出另一组完全不同的权重，比如 <code>&#123;专家1: 0.2, 专家2: 0.6, 专家3: 0.2&#125;</code>，然后用这组权重去融合专家的意见，得到专门用于预测时长的最终表示。</li></ol></li><li><strong>与大模型 MoE 的对比</strong>：<ul><li><strong>核心目标不同</strong>：推荐中的 <strong>MMoE</strong> 是为了<strong>有效进行多任务学习</strong>，解决任务冲突；大模型中的 <strong>MoE</strong> 是为了用可控的计算成本<strong>极大化模型容量</strong>。</li><li><strong>工作方式不同</strong>：<strong>MMoE</strong> 中，<strong>所有专家</strong>都参与计算，每个任务只是对它们的意见进行不同的**“加权融合”<strong>。而 MoE 中，对于一个输入，路由器会</strong>选择性地激活<strong>Top-K个（通常是1或2个）专家，是一种</strong>“稀疏激活”**，绝大部分专家在“休息”。</li></ul></li></ul></li><li><p><code>信息瓶颈问题: SeNet, 并行多塔</code></p><p><strong>要解决的问题：双塔模型的“致命弱点”</strong></p><p>标准双塔模型最大的结构缺陷在于，两个塔辛辛苦苦将丰富的特征信息编码成高维向量后，最终却只通过一个极其简单的**“点积 (Dot Product)”<strong>操作来决定生死。这个点积操作是一个巨大的</strong>信息瓶颈**，它无法捕捉特征之间复杂的、高阶的交互关系，损失了大量有价值的信息。</p><p><strong>解决方案1：SeNet (Squeeze-and-Excitation Network) - “为特征划重点”</strong></p><ul><li><strong>核心思想</strong>：在进行点积之前，先用一个“注意力”模块，动态地学习 Embedding 中<strong>每个维度的重要性</strong>，然后进行加权，突出重要信息，压制噪声信息。</li><li><strong>工作流程</strong>：<ol><li><strong>Squeeze (压缩)</strong>：将原始 Embedding 的全局信息压缩成一个简短的概要。</li><li><strong>Excitation (激励)</strong>：基于这个概要，通过一个微型网络，为原始 Embedding 的<strong>每一维</strong>都生成一个 <code>0~1</code> 之间的重要性权重。</li><li><strong>Reweight (重赋权)</strong>：将原始 Embedding 与这个权重向量按位相乘，得到一个“划过重点”的、信息量更精纯的新 Embedding。</li></ol></li><li><strong>效果</strong>：让简单的点积操作，也能作用在经过“智能预处理”的、信息含量更高的向量上。</li></ul><p><strong>解决方案2：并行多塔 (Parallel Multi-Towers) - “集思广益”</strong></p><ul><li><strong>核心思想</strong>：既然一种交互方式有局限，那就让多种不同的、各有所长的交互模块并行工作，最后将它们的结果综合起来。</li><li><strong>架构</strong>：将原始的用户和物品 Embedding，同时输入到多个并行的“交互通道”中，例如：<ul><li><strong>MLP 通道</strong>：学习隐式的、高阶的非线性特征交互。</li><li><strong>DCN (Deep &amp; Cross Network) 通道</strong>：擅长学习显式的、有限阶的特征交叉。</li><li><strong>FM / CIN (Factorization Machine / Compressed Interaction Network) 通道</strong>：擅长学习显式的二阶特征交叉。</li></ul></li><li><strong>融合</strong>：将所有通道的输出结果进行拼接或加权求和，得到一个综合了各种交互模式的、更鲁棒的最终匹配分数。</li></ul></li><li><p><code>物品冷启动问题: SSL双塔</code></p><p><strong>要解决的问题：新物品的“无人问津”</strong></p><p>一个新上架的物品，没有任何用户的交互记录（点击、购买等）。传统的、依赖用户行为的召回模型（如协同过滤、标准双塔），完全无法为它生成一个有意义的 Embedding，导致它永无出头之日。这就是“物品冷启动”问题。</p><p><strong>解决方案：SSL双塔 - “让物品自己教自己”</strong></p><ul><li><strong>核心思想</strong>：既然没有用户行为数据，我们就想办法<strong>只利用物品自身的属性特征（如标题、类目、品牌、图片等）来进行学习。通过自监督学习 (SSL)</strong>，让物品塔在没有用户的情况下，也能完成“预训练”。</li><li><strong>工作流程</strong>：<ol><li><strong>构造正样本对</strong>：这是SSL的关键。我们对一个物品的原始特征进行两次独立的<strong>数据增强 (Data Augmentation)</strong>，比如随机遮盖掉一些词（Masking）或随机丢弃一些特征（Dropout），得到两个“面目全非”但“同根同源”的增强版特征。这两个增强版就被视作一个<strong>正样本对</strong>。</li><li><strong>自监督训练</strong>：<ul><li>将这两个增强版的特征，分别送入<strong>同一个物品塔</strong>，得到两个 Embedding <code>emb_aug1</code> 和 <code>emb_aug2</code>。</li><li><strong>采用对比学习损失 (InfoNCE Loss)</strong>：这个损失函数的目标是，在向量空间中，<strong>拉近</strong> <code>emb_aug1</code> 和 <code>emb_aug2</code> 的距离，同时<strong>推远</strong>它们与<strong>同一个 batch 中其他物品</strong>的增强向量的距离。</li></ul></li><li><strong>混合训练</strong>：最终，模型的总损失由两部分组成：一部分是来自用户-物品交互的<strong>监督学习主损失</strong>；另一部分就是这个<strong>自监督SSL损失</strong>。</li></ol></li><li><strong>效果</strong>：通过SSL部分的训练，物品塔学会了从物品<strong>自身属性</strong>中理解其内在的语义。当一个新物品到来时，即使没有任何用户行为，物品塔也能根据它的标题、类目等信息，直接生成一个高质量、有意义的 Embedding，使其能够被公平地推荐给可能对它感兴趣的用户，从而完美地解决了冷启动问题。</li></ul></li></ul></li></ul><h3 id="5-经典与其他召回策略-Classic-Other-Recall-Strategies"><code>5. 经典与其他召回策略 (Classic &amp; Other Recall Strategies)</code></h3><ul><li><p><code>5.1. 协同过滤 (Collaborative Filtering)</code></p><p>协同过滤（CF）是推荐系统历史上最重要、应用最广泛的算法范式之一。它的魅力在于，它<strong>完全不需要理解物品本身的内容（比如电影的情节或商品的功能）</strong>，仅通过分析用户与物品之间的交互行为数据（“用户行为矩阵”），就能给出相当精准的推荐。</p><p>其核心哲学是“物以类聚，人以群分”，即利用群体智慧来进行推荐。它主要分为两大流派：基于用户的协同过滤（UserCF）和基于物品的协同过滤（ItemCF）。</p><ul><li><p><code>UserCF, ItemCF</code></p><p><strong>UserCF (User-Based Collaborative Filtering)</strong></p><ul><li><strong>核心思想：“向与你品味相似的朋友寻求推荐”</strong><ul><li>UserCF 的逻辑非常符合人类社会的社交推荐行为。它分为两步：<ol><li>找到与你（目标用户）的<strong>兴趣品味最相似</strong>的一群用户。</li><li>将这群“品味相投”的朋友们喜欢过，而你<strong>还未接触过</strong>的物品，推荐给你。</li></ol></li></ul></li><li><strong>工作流程详解</strong>：<ol><li><strong>计算用户相似度</strong>：这是 UserCF 的核心与计算瓶颈。系统会遍历所有用户，计算目标用户与每一个其他用户之间的相似度。常用的相似度度量包括：<ul><li><strong>Jaccard 相似系数</strong>：计算两个用户交互过的物品集合的交集与并集的比例。</li><li><strong>余弦相似度</strong>：将每个用户对物品的偏好（如评分或点击）视为一个向量，计算向量之间的夹角。</li></ul></li><li><strong>生成推荐列表</strong>：<ul><li>找出与目标用户最相似的 Top-K 个用户。</li><li>汇总这 K 个用户喜欢过的所有物品，并根据“推荐强度”进行排序（例如，一个物品被越多的相似用户喜欢，其推荐强度就越高）。</li><li>过滤掉目标用户已经交互过的物品后，得到最终的推荐列表。</li></ul></li></ol></li><li><strong>优点与挑战</strong>：<ul><li><strong>优点</strong>：能够发现一些用户潜在的、跨领域的兴趣，推荐结果具有<strong>新颖性和惊喜性 (Serendipity)</strong>。比如，通过相似用户，一个科幻迷可能会被推荐一本他从未接触过的硬核历史小说。</li><li><strong>挑战</strong>：<ul><li><strong>可扩展性差 (Poor Scalability)</strong>：在用户量巨大（千万、上亿级别）的现代平台，计算所有用户两两之间的相似度，其计算量是平方级别的，几乎是不可接受的。</li><li><strong>数据稀疏性 (Sparsity)</strong>：用户交互过的物品相对于整个物品库来说极其稀少，导致很难找到有足够多共同交互物品的“相似用户”。</li><li><strong>时效性差</strong>：用户的兴趣是会变化的，但用户相似度矩阵的计算成本极高，无法频繁更新。</li></ul></li></ul></li></ul><p><strong>ItemCF (Item-Based Collaborative Filtering)</strong></p><p>为了解决 UserCF 的可扩展性等问题，ItemCF 应运而生，并迅速成为业界主流。</p><ul><li><strong>核心思想：“根据你喜欢的，推荐与它相似的”</strong><ul><li>ItemCF 的逻辑更关注物品之间的关联性。我们日常在电商网站看到的“购买此商品的顾客也购买了…”功能，就是 ItemCF 思想最经典的体现。</li><li>它的工作流程同样分为两步：<ol><li>（离线）计算出整个平台中，物品与物品之间的相似度。</li><li>（在线）根据你历史上喜欢过的物品，找到与它们最相似的一批物品，推荐给你。</li></ol></li></ul></li><li><strong>工作流程详解</strong>：<ol><li><strong>（离线）构建物品相似度矩阵</strong>：这是 ItemCF 的核心。系统会遍历所有用户的行为历史，计算物品两两之间的相似度。最基础的计算方法是：<code>Similarity(i, j) = |U(i) ∩ U(j)| / sqrt(|U(i)| * |U(j)|)</code>，其中 <code>U(i)</code> 是喜欢物品 <code>i</code> 的用户集合。这个计算量巨大，但由于物品之间的关系相对稳定，可以离线计算并定期更新。</li><li><strong>（在线）生成推荐列表</strong>：<ul><li>获取用户的历史正反馈物品列表 <code>L</code>。</li><li>对于 <code>L</code> 中的每一个物品 <code>i</code>，从预先计算好的相似度矩阵中，找出与 <code>i</code> 最相似的 Top-K 个物品。</li><li>将所有这些找出的相似物品进行汇总、加权（比如，用物品相似度分乘以用户对 <code>i</code> 的喜爱程度）、排序、去重后，得到最终的推荐列表。</li></ul></li></ol></li><li><strong>优点与挑战</strong>：<ul><li><strong>优点</strong>：<ul><li><strong>可扩展性好</strong>：物品数量通常比用户数量少且增长慢，物品相似度矩阵可以离线计算，在线推荐速度极快。</li><li><strong>推荐更稳定且可解释</strong>：推荐结果往往与用户的近期行为高度相关，更容易被用户理解和接受（“因为你喜欢A，所以推荐B”）。</li></ul></li><li><strong>挑战</strong>：<ul><li><strong>推荐多样性与新颖性不足</strong>：由于推荐结果高度依赖用户已有行为，很难跳出用户当前的兴趣圈，挖掘新的兴趣领域。</li><li><strong>热门偏见 (Popularity Bias)</strong>：<strong>这是 ItemCF 最大的缺陷</strong>。两个本身不相关的热门物品（比如《新华字典》和“可口可乐”），很可能因为都被大量用户购买过，而被算法错误地认为“非常相似”。</li></ul></li></ul></li></ul></li><li><p><code>Swing 算法[拓展]</code></p><p><strong>Swing 算法：ItemCF 的高级进化版</strong></p><p>Swing 算法的提出，就是为了<strong>精准打击 ItemCF 的热门偏见问题</strong>。</p><ul><li><strong>核心思想</strong>：两个物品 <code>i</code> 和 <code>j</code> 的相似度，不应该由那些“大众品味”的用户决定，而应该由那些“小众、专一”的用户来定义。</li><li><strong>一个直观的例子</strong>：<ul><li><strong>场景1</strong>：用户A只买了“键盘”和“鼠标”，用户B也只买了“键盘”和“鼠标”。这两个用户的行为，极强地证明了“键盘”和“鼠标”是高度相关的。</li><li><strong>场景2</strong>：用户C是个“数码达人”，买了100件商品，其中包括“键盘”和“鼠标”；用户D也是“数码达人”，也买了这100件商品。这两个用户的行为，只能微弱地证明“键盘”和“鼠标”相关，因为它们可能只是“数码产品”这个大筐子里的两个普通成员。</li></ul></li><li><strong>Swing 的工作原理</strong>：<ul><li>它考察的是所有同时喜欢了物品 <code>i</code> 和 <code>j</code> 的<strong>用户对 <code>&lt;u, v&gt;</code></strong>。</li><li>它认为，如果用户 <code>u</code> 和 <code>v</code> 的<strong>交集很小</strong>（像场景1中的A和B），那么他们共同喜欢 <code>i</code> 和 <code>j</code> 的行为，就对 <code>sim(i, j)</code> 的贡献<strong>非常大</strong>。</li><li>反之，如果用户 <code>u</code> 和 <code>v</code> 的<strong>交集很大</strong>（像场景2中的C和D），那么他们共同喜欢 <code>i</code> 和 <code>j</code> 的行为，对 <code>sim(i, j)</code> 的贡献就<strong>非常小</strong>。</li><li><strong>公式体现</strong>：<code>sim(i, j) = Σ [1 / (α + |I(u) ∩ I(v)|)]</code>，其中 <code>I(u)</code> 是用户 <code>u</code> 喜欢的物品集合。分母上的交集项，完美地实现了**“交集越大，贡献越小”**这一核心思想。</li></ul></li><li><strong>效果</strong>：Swing 能够有效地滤除热门物品带来的噪声，挖掘出物品之间更真实、更鲁棒的关联关系，是目前工业界应用非常广泛且有效的 ItemCF 升级方案。</li></ul></li></ul></li><li><p><code>5.2. 序列建模 (Sequential Modeling): SDM (长短期兴趣)</code></p><p>**核心动机：用户的兴趣是“流动的河”。**我们之前讨论的协同过滤、图模型等方法，很多时候是将用户的历史行为看作一个静态的“集合”，忽略了一个至关重要的维度——<strong>时间</strong>。</p><p>用户的兴趣并非一成不变，而是像一条流动的河。你<strong>刚刚</strong>点击过的几个物品，远比你<strong>一年前</strong>喜欢的东西，更能代表你<strong>当下</strong>的意图。序列建模的核心，就是为了<strong>捕捉用户兴趣的动态性、时序性</strong>，从而做出更即时、更精准的推荐。</p><p>SDM 模型是阿里在2019年提出的，是序列化召回领域的经典之作。它的核心创见在于，它认为一个用户的最终兴趣，是其<strong>长期稳定的泛在兴趣</strong>和<strong>短期即时的会话兴趣</strong>共同作用的结果，并设计了一套精巧的结构来分别建模并融合它们。</p><p><strong>SDM 的核心架构：长短期兴趣的“双轨制”</strong></p><p><strong>1. 长期兴趣建模 (Long-Term Interest Modeling)</strong></p><ul><li><strong>目标</strong>：刻画用户稳定、持久的“核心品味”或“用户画像”。</li><li><strong>输入</strong>：用户的<strong>较长跨度</strong>的历史行为序列（比如过去一个月的几百次点击）。</li><li><strong>建模方式</strong>：<ul><li>由于长期兴趣不强调严格的顺序，而在于“用户到底喜欢过哪些东西”的整体面貌，SDM 直接将这些物品的 Embedding 进行处理。</li><li>最简单的方式是直接<strong>平均池化 (Average Pooling)</strong>。</li><li>更高级的方式是如 <code>image_4b0f4d.png</code> 中提到的 <strong>Att Net (Attention Network)</strong>，即通过一个注意力网络，为用户的每一个长期行为物品赋予一个权重，然后进行加权求和。这可以让模型自动识别出哪些历史行为最能代表用户的核心兴趣。</li></ul></li><li><strong>输出</strong>：一个代表用户长期、泛在兴趣的向量 <code>U_long</code>。</li></ul><p><strong>2. 短期兴趣建模 (Short-Term Interest Modeling)</strong></p><ul><li><strong>目标</strong>：精准捕捉用户在<strong>当前会话 (Session)</strong> 中的即时意图。</li><li><strong>输入</strong>：用户的<strong>最近几次</strong>行为序列（比如最近1小时内的10次点击）。</li><li><strong>建模方式</strong>：<ul><li>短期兴趣对<strong>顺序</strong>极为敏感（比如，你先搜了“显示器”，再搜“显卡”，这和你反过来搜，意图可能完全不同）。</li><li>因此，必须使用能处理时序信息的模型。SDM 的论文中，综合对比了多种方法，最终采用了 <strong>RNN + 多头自注意力 (Multi-Head Self-Attention)</strong> 的组合。<ol><li><strong>RNN (或 LSTM/GRU)</strong>：先用一个循环神经网络来初步编码序列中的时序依赖关系。</li><li><strong>多头自注意力</strong>：这是借鉴了 Transformer 的思想。在 RNN 初步编码后，再用一个多头自注意力层，来捕捉序列内部<strong>不同物品之间的复杂关联</strong>。比如，在一个序列 <code>[手机壳, 充电器, 钢化膜]</code> 中，注意力机制能学会这三者是紧密关联的“手机配件”组合，从而提炼出一个更精准的“购买手机配件”的短期意图。</li></ol></li></ul></li><li><strong>输出</strong>：一个代表用户短期、即时兴趣的向量 <code>U_short</code>。</li></ul><p><strong>3. 长短期兴趣融合 (Fusion)</strong></p><ul><li><p><strong>问题</strong>：现在我们有了代表“我是谁”的 <code>U_long</code> 和代表“我现在想干嘛”的 <code>U_short</code>，该如何将它们融合成一个最终的用户向量呢？</p></li><li><p><strong>解决方案：门控网络 (Gating Network)</strong></p><ul><li><p>SDM 设计了一个巧妙的门控单元。这个门也是一个微型神经网络，它会根据输入信息，<strong>动态地生成一个权重 <code>g</code></strong>（一个0到1之间的数值）。</p></li><li><p><strong>最终的用户向量</strong>通过如下方式加权融合：</p><p>Ufinal=g⋅Ushort+(1−g)⋅Ulong</p></li></ul></li><li><p><strong>门控的意义</strong>：这个权重 <code>g</code> 就像一个**“智能调控阀”**。</p><ul><li>当用户的短期行为意图非常明确时（比如连续搜索了某个特定商品），门控网络会学会输出一个较大的 <code>g</code>，使得最终的用户向量更偏向于短期兴趣 <code>U_short</code>。</li><li>当用户只是在无目的地“闲逛”，短期行为很发散时，门控网络会学会输出一个较小的 <code>g</code>，使得最终的用户向量更偏向于稳定可靠的长期兴趣 <code>U_long</code>。</li></ul></li></ul><p>SDM 模型的核心贡献在于：</p><ol><li><strong>首次清晰地将用户兴趣拆分为长、短期两个维度</strong>，并使用不同的、更具针对性的网络结构（RNN/Attention vs. Attention/Pooling）进行建模。</li><li>设计了<strong>门控网络</strong>这一动态融合机制，让模型可以根据上下文，智能地权衡用户的即时意图和稳定偏好。</li><li>相比于之前的模型，SDM 能够<strong>更精准、更动态地捕捉用户的兴趣变化</strong>，尤其是在短视频、新闻、电商等需要对用户即时意图做出快速响应的场景下，取得了非常显著的效果，是序列化召回领域的一个里程碑。</li></ol></li><li><p><code>5.3. 内容与运营策略 (Content &amp; Operational Strategies)</code></p><p>如果说协同过滤和深度学习模型是召回系统里依靠“数据和算法”驱动的“正规军”，那么内容与运营策略召回，就是系统中不可或缺的、负责<strong>查漏补缺、处理特殊情况、注入业务理解</strong>的“特种部队”和“后勤保障部队”。一个成熟的召回系统，绝对是这两类方法协同作战的结果。</p><ul><li><p><strong>标签召回 (Tag-based Recall / u2tag2i)</strong></p><p>这是一种以“标签”为核心，连接用户与物品的、非常经典且有效的内容召回方法。</p><p>**核心思想：“以标签为媒，连接人与物”。**它的逻辑非常清晰，分为两步：</p><ol><li><strong>u2tag (User-to-Tag)</strong>：先搞清楚<strong>用户喜欢什么标签</strong>。</li><li><strong>tag2i (Tag-to-Item)</strong>：再根据用户喜欢的标签，去找<strong>带有这些标签的物品</strong>。</li></ol><p><strong>工作流程详解</strong></p><ol><li><strong>构建用户标签偏好画像 (u2tag)</strong>：<ul><li><strong>简单统计</strong>：分析用户历史交互过（点击、购买等）的所有物品，将这些物品携带的标签进行汇总、加权。比如，用户点击了10件带有“科幻”标签的物品，5件带有“悬疑”标签的物品，那么他对“科幻”标签的偏好权重就更高。</li><li><strong>模型预测</strong>：也可以训练一个模型，根据用户的行为和属性，直接预测他对各个标签的偏好得分。</li><li><strong>最终产出</strong>：一个用户的标签偏好画像，形式通常是 <code>&#123;“科幻”: 0.9, “悬疑”: 0.7, “爱情”: 0.1, ...&#125;</code>。</li></ul></li><li><strong>匹配物品 (tag2i)</strong>：<ul><li>系统中需要有一个**“物品-标签”的倒排索引**，即能快速地根据一个标签，找到所有具备该标签的物品。</li><li>将用户的标签偏好向量，与全量物品的标签进行匹配，计算一个匹配分数，召回得分最高的物品。</li></ul></li></ol><p><strong>优点与挑战</strong></p><ul><li><strong>优点</strong>：<ul><li><strong>可解释性强</strong>：推荐理由非常直观（“因为你喜欢科幻，所以为你推荐……”）。</li><li><strong>泛化能力强，善于“破圈”</strong>：可以为用户推荐他从未见过，但标签符合其偏好的<strong>新物品</strong>，是解决物品冷启动和挖掘用户新兴趣的利器。</li></ul></li><li><strong>挑战</strong>：<ul><li><strong>极度依赖标签体系的质量</strong>：标签的<strong>准确性、覆盖率、粒度</strong>直接决定了这条召回路径效果的上限。构建和维护一个高质量的标签体系，需要大量的人工、算法和业务经验投入，是典型的“脏活累活”。</li></ul></li></ul></li><li><p><strong>热销/热门召回 (Hot-Item Recall)</strong></p><p>这是所有召回策略中最简单、最鲁棒，也是<strong>不可或缺的“保底”策略</strong>。**核心思想：“相信群众的选择”。**它的逻辑是，不管用户是谁，把当前整个平台上最受欢迎、最多人消费的物品推荐给他，总归不会犯大错。这些热门物品本身已经经过了市场的“检验”，通常质量和吸引力都有保障。</p><p><strong>主要应用场景：终极“安全网”</strong></p><ol><li><strong>用户冷启动</strong>：<strong>这是热门召回最重要的使命</strong>。对于一个没有任何行为历史的新用户，我们对他一无所知，任何个性化算法都无从下手。此时，提供一个热门榜单是开启用户“探索之旅”、收集其初始偏好的最有效方式。</li><li><strong>个性化召回“失灵”的补充</strong>：当所有个性化召回路径（如双塔、协同过滤）因为数据稀疏等原因，没有为某个用户召回足够多的物品时，热门召回可以作为补充，确保推荐页面不会“开天窗”。</li></ol><p><strong>实践中的挑战</strong></p><p>实现一个好的热门召回，远不止是“按销量排个序”那么简单，需要精细化运营：</p><ul><li><strong>“热门”的定义</strong>：<ul><li><strong>时间窗口</strong>：是取最近1小时的热门，还是24小时，或是7天？不同的时间窗口反映了不同的趋势。实时热门榜能追热点，但可能不稳定；周期热门榜更稳健，但对突发事件不敏感。</li><li><strong>统计口径</strong>：是用绝对的销量/点击量，还是用点击率/转化率？</li><li><strong>分门别类</strong>：全局的热门榜很容易被少数几个大品类（如手机、服饰）霸占。更精细的做法是维护<strong>分品类、分地域、分人群</strong>的热门榜。</li></ul></li><li><strong>热门的偏见与反作弊</strong>：<ul><li>需要剔除<strong>大促、营销活动</strong>带来的“虚假”热门。</li><li>要警惕热门推荐带来的“马太效应”，即越热门的越被推荐，越被推荐的就越热门，导致多样性下降。需要加入一些<strong>时间衰减、多样性打散</strong>的机制。</li></ul></li></ul></li><li><p><strong>其他策略 (复购, 高CTR等)</strong></p><p>除了上述策略，召回系统还会配置多种满足特定业务目标的策略。</p><ul><li><strong>复购召回 (Rebuy Recall)</strong><ul><li><strong>逻辑</strong>：向用户推荐他们曾经购买过的商品。</li><li><strong>应用场景</strong>：对<strong>消耗品</strong>（如纸巾、饮料、猫粮、化妆品等）极其有效。</li><li><strong>实现</strong>：识别出消耗品类目，结合用户的平均购买周期，在用户可能即将用完时，触发复购召回。</li></ul></li><li><strong>高CTR/CVR召回 (High CTR/CVR Recall)</strong><ul><li><strong>逻辑</strong>：维护一个全站范围内，历史上平均点击率或转化率最高的物品列表。</li><li><strong>应用场景</strong>：这些物品是“转化效率”的保证。在需要提升关键指标时，或者作为探索用户兴趣的“钩子”，适量混入这些高CTR物品，能有效提升整体收益。</li></ul></li><li><strong>高质量/好评召回 (High-Quality Recall)</strong><ul><li><strong>逻辑</strong>：召回由平台编辑、专家或高分用户评选出的高质量内容/商品。</li><li><strong>应用场景</strong>：用于提升平台格调和用户信任感，平衡纯数据驱动可能带来的“劣币驱逐良币”问题。</li></ul></li></ul></li></ul></li></ul><h2 id="三、-召回系统工程与策略-Recall-System-Engineering-Strategy"><strong>三、 召回系统工程与策略 (Recall System Engineering &amp; Strategy)</strong></h2><h3 id="1-模型与数据的更新机制-Model-Data-Updating"><code>1. 模型与数据的更新机制 (Model &amp; Data Updating)</code></h3><p>推荐系统不是一个静态的系统，它是一个需要与用户实时互动的“生命体”。用户的兴趣在变，平台上的物品在变，热点趋势也在变。因此，模型和它所依赖的数据必须保持“新鲜”，才能做出精准、及时的推荐。如何保持新鲜？业界主要有两种策略：全量更新和增量更新。</p><ul><li><p><code>1.1. 全量更新 (Full Retraining)</code></p><p>这是一种最稳健、最经典的更新方式。</p><ul><li><strong>核心思想：“推倒重来，定期大扫除”</strong><ul><li>全量更新的逻辑是，我们会设定一个更新周期（比如每天一次）。在这个周期内，我们彻底<strong>丢弃掉旧的模型</strong>，然后收集这个周期内（比如过去24小时或过去7天）的<strong>全部新数据</strong>，从零开始重新训练一个全新的模型。</li></ul></li><li><strong>工作流程详解</strong>：<ol><li><strong>数据聚合</strong>：在固定的时间点（比如每天凌晨），一个ETL任务会启动，将过去一段时间（如24小时）的所有用户行为日志（点击、曝光、购买等）收集、清洗、整合成一个庞大的训练数据集。</li><li><strong>模型训练</strong>：使用这个完整的数据集，从头开始训练我们的召回模型（如双塔模型）。这个过程会持续数小时，直到模型完全收敛。</li><li><strong>离线评估</strong>：在训练好的新模型上线前，会在一个预留的测试集上进行评估，确保其效果优于旧模型。</li><li><strong>全量部署</strong>：评估通过后，用这个新模型为<strong>全量</strong>的用户和物品生成最新的 Embedding 向量，构建新的 ANN 索引，然后通过原子切换，将线上服务指向这个全新的模型和索引。</li></ol></li><li><strong>优点与缺点</strong>：<ul><li><strong>优点</strong>：<ul><li><strong>模型效果最稳定、全局最优</strong>：由于模型每次都能看到一个完整周期内的全局数据分布，它能学习到最宏观、最鲁棒的模式，不易被短期的数据噪声或趋势带偏。</li><li><strong>工程实现与维护相对简单</strong>：整个流程是周期性的、批量的，逻辑清晰，更容易调试和管理。</li></ul></li><li><strong>缺点</strong>：<ul><li><strong>时效性极差，延迟高</strong>：这是它最大的弊端。从用户产生一个行为，到这个行为被模型学到，中间的延迟非常长（通常是T+1，即延迟一天）。这导致模型<strong>无法捕捉实时热点</strong>，对用户兴趣的快速变化响应迟钝。</li><li><strong>计算资源开销巨大</strong>：每天都对海量数据进行一次从零开始的训练，是对计算资源的巨大消耗，成本高昂。</li></ul></li></ul></li><li><strong>一个比喻：年度体检</strong><ul><li>全量更新就像我们每年做一次全面的身体检查。它非常彻底，能发现深层次的、全局性的健康状况，但你无法通过它来了解自己此时此刻的瞬时心率。</li></ul></li></ul></li><li><p><code>1.2. 增量更新 (Incremental/Online Learning)</code></p><p>为了解决全量更新时效性差的问题，增量更新应运而生。</p><ul><li><strong>核心思想：“即刻反馈，持续微调”</strong><ul><li>增量更新不再“推倒重来”，而是在一个<strong>已经训练好的线上模型</strong>的基础上，利用<strong>实时产生</strong>的新数据，对模型进行持续、小步快跑式的微调。</li></ul></li><li><strong>工作流程详解</strong>：<ol><li><strong>数据流式传输</strong>：用户的行为数据不再是每天打包一次，而是通过消息队列（如 Kafka）以<strong>流的形式</strong>实时地传输到训练平台。</li><li><strong>实时训练 (Near Real-time Training)</strong>：一个持续运行的训练服务会消费这个数据流，以很小的批次（mini-batch）为单位，对线上的模型进行不间断的训练（梯度更新）。</li><li><strong>模型同步</strong>：更新后的模型参数或特定的 Embedding（尤其是 User Embedding）会以较高的频率（比如每10分钟或每小时）被同步到线上的推荐服务实例中。<ul><li><strong>最常见的增量更新</strong>：对于用户塔，可以实现分钟级的更新。当用户产生新行为后，可以立即重新计算并更新他本人的 User Embedding，这样他下一次刷新就能看到基于他刚才行为的推荐。</li></ul></li></ol></li><li><strong>优点与缺点</strong>：<ul><li><strong>优点</strong>：<ul><li><strong>时效性极佳</strong>：模型能够<strong>分钟级甚至秒级</strong>地学习到用户的最新行为和全局热点的变化，推荐的相关性和即时性非常强。</li><li><strong>资源效率高</strong>：每次只处理新产生的数据，避免了重复计算，节省了大量资源。</li></ul></li><li><strong>缺点</strong>：<ul><li><strong>有“过拟合”于近期数据的风险</strong>：模型可能会过度关注最新的数据流，而慢慢“忘记”长期、稳定的用户兴趣模式，这种现象被称为“灾难性遗忘 (Catastrophic Forgetting)”。</li><li><strong>工程实现与维护极其复杂</strong>：构建一个稳定、可靠的流式数据处理和实时训练系统，其技术挑战远高于批处理系统。</li><li><strong>训练可能不稳定</strong>：实时数据流中可能存在噪声或异常数据，容易对模型造成短期的负面冲击。</li></ul></li></ul></li><li><strong>一个比喻：可穿戴健康手环</strong><ul><li>增量更新就像一个7x24小时佩戴的智能手环。它能实时监测你的心率、步数，并立刻给你反馈，但它可能无法发现那些需要全面检查才能看出的深层健康问题。</li></ul></li></ul></li><li><p><code>1.3. 行业标准：混合模式 (Hybrid Approach)</code></p><p>在现代工业界，几乎没有任何一个大型推荐系统只采用上述两种模式中的一种。最成熟、最普遍的解决方案是**“全量+增量”的混合模式**：</p><ul><li><strong>以“全量更新”为根本</strong>：保持一个<strong>天级或周级</strong>的全量训练流程，用于产出一个高质量、鲁棒的<strong>基座模型 (Base Model)</strong>。这个基座模型保证了系统的下限和稳定性，能够纠正增量学习可能带来的长期漂移。</li><li><strong>以“增量更新”为抓手</strong>：在这个高质量的基座模型之上，进行<strong>小时级或分钟级</strong>的增量更新。这保证了系统对实时变化的<strong>敏感性</strong>和<strong>响应速度</strong>。</li></ul></li></ul><h3 id="2-多路召回的策略设计-Multi-Path-Recall-Strategy-Design"><code>2. 多路召回的策略设计 (Multi-Path Recall Strategy Design)</code></h3><ul><li><p><code>2.1. 场景化策略 (Homepage vs. Detail Page)</code></p><p>一个核心的策略思想是：<strong>“用户的意图，由场景决定；召回的策略，由意图决定。”</strong> 不同的推荐场景（用户在APP里的不同位置），暗示了用户完全不同的心理状态和需求。我们的召回策略必须具备“见人下菜碟”的能力。</p><p><strong>首页/信息流推荐 (Homepage/Feed Recommendation)</strong></p><ul><li><strong>用户意图：模糊、宽泛、探索性 (Exploratory &amp; Broad)</strong><ul><li>用户打开APP首页时，通常没有一个非常具体的目标，心态是“随便逛逛，看看有什么有意思的”。</li></ul></li><li><strong>召回目标：探索与多样性 (Exploration &amp; Diversity)</strong><ul><li>我们的首要任务是快速试探出用户当下的兴趣点，或者为他打开一扇新世界的大门。因此，召回结果必须<strong>丰富多样、覆盖面广</strong>。</li></ul></li><li><strong>策略组合：多路、宽泛策略为主</strong><ul><li><strong>个性化泛兴趣召回</strong>：基于用户的长期、泛在兴趣画像进行向量化召回（User-based）。</li><li><strong>热门召回</strong>：必须有，用于捕捉全局热点，并服务于冷启动用户。</li><li><strong>社交网络/图召回</strong>：挖掘圈层热点，实现“破圈”。</li><li><strong>标签召回 (u2tag2i)</strong>：根据用户的兴趣标签，推荐一些他可能从未见过的新品类。</li></ul></li><li><strong>一个比喻：大型百货商场的橱窗</strong><ul><li>商场入口处的橱窗，会陈列当季最新款的服装、最酷的电子产品、最香的香水……它的目标是吸引各种不同需求的顾客，让他们产生“进去逛逛”的欲望。</li></ul></li></ul><p><strong>详情页/播放页推荐 (Detail Page/Post-Interaction Recommendation)</strong></p><ul><li><strong>用户意图：明确、深入、关联性 (Focused &amp; Deep)</strong><ul><li>当用户点进一个具体的物品（比如一部手机）的详情页时，他的意图变得非常清晰：他此刻正高度关注这个物品或其同类。</li></ul></li><li><strong>召回目标：深化与精准 (Deepening &amp; Precision)</strong><ul><li>我们的任务不再是探索，而是要**“趁热打铁”**，提供与当前物品高度相关的内容，满足用户的深度挖掘需求，促成转化。</li></ul></li><li><strong>策略组合：精准、窄域策略为主</strong><ul><li><strong>Item-to-Item (I2I)</strong>：<strong>绝对的主力</strong>。包括基于向量的I2I（找Embedding最近邻）、基于协同过滤的I2I（如Swing算法）、基于图关联的I2I等。</li><li><strong>属性召回</strong>：召回同一品牌、同一细分品类、同一作者/店铺的其他物品。</li></ul></li><li><strong>一个比喻：超市里的特定货架</strong><ul><li>当你拿起一包“番茄味薯片”时，你身边的货架上一定是其他各种口味的薯片、其他品牌的薯片，以及搭配薯片的可乐。所有推荐都围绕着“薯片”这个核心。</li></ul></li></ul></li><li><p><code>2.2. 召回配额管理 (Quota Management)</code></p><p>当系统中有几十上百路召回策略并行时，我们就面临一个关键问题：最终要呈现给粗排的1000个候选中，每一路策略应该“贡献”多少个名额？这就是配额管理。</p><ul><li><strong>静态/固定配额</strong>：<ul><li><strong>做法</strong>：最简单的方式，由算法工程师根据离线实验和线上经验，为每一路召回手动配置一个固定的数量或比例。例如：向量召回出400个，ItemCF出200个，热门召回出100个……</li><li><strong>优缺点</strong>：实现简单，稳定可控。但无法做到个性化，对所有用户都“一视同仁”。</li></ul></li><li><strong>动态/个性化配额</strong>：<ul><li><strong>核心思想</strong>：不同用户、不同场景下，各路召回策略的“靠谱程度”是不同的。因此，配额也应该是<strong>因人而异、因时而异</strong>的。</li><li><strong>做法</strong>：训练一个轻量级的**“配额分配模型”**（比如 Logistic Regression 或浅层MLP）。<ul><li><strong>输入</strong>：用户的特征，如<strong>活跃度</strong>（新用户/老用户）、<strong>会话状态</strong>（刚打开APP/已浏览一段时间）、<strong>用户画像</strong>（兴趣是否收敛）等。</li><li><strong>输出</strong>：一个权重分布，代表了每一路召回策略在当前情况下的“推荐价值”。</li></ul></li><li><strong>例子</strong>：<ul><li>对于一个<strong>新用户</strong>，该模型可能会给“热门召回”分配90%的配额。</li><li>对于一个<strong>深度篮球迷</strong>，模型可能会给“篮球圈层I2I召回”分配70%的配额。</li></ul></li><li><strong>效果</strong>：通过动态配额，系统可以为每个用户“量身定制”一个最优的召回策略组合，将宝贵的粗排计算资源，集中在那些最可能产生好结果的召回源上。</li></ul></li></ul></li><li><p><code>2.3. 特殊场景优化 (如电商付费推荐)</code></p><p>除了通用的推荐场景，召回系统还必须为一些具有<strong>明确商业目标</strong>的特殊场景进行深度定制。以电商付费推广（广告）为例：</p><ul><li><strong>目标变化</strong>：此时的最终目标，不再仅仅是用户的“兴趣”，而是平台的**“收入” (RPM/eCPM)<strong>，需要同时平衡</strong>用户体验、平台收入、广告主ROI**三方利益。</li><li><strong>召回策略的适配与演化</strong>：<ol><li><strong>候选集变化</strong>：召回的候选池不再是全量商品库，而是<strong>广告商品池</strong>。</li><li><strong>关联性重定义</strong>：I2I的逻辑，从“看了A的用户也看了B”，演变为“点击了A商品详情页的用户，最终转化（购买）了B广告商品”。</li><li><strong>商业化目标融入召回</strong>：<ul><li><strong>出价 (Bid) 感知的召回</strong>：如我们之前讨论的，通过数学变换，将广告主的出价信息融入到物品向量 <code>a'</code> 中，使得召回模型在计算点积时，天然地倾向于那些**“eCPM潜力高”**的广告。</li></ul></li><li><strong>高意图场景的重点优化</strong>：<ul><li>在**“购物车页面”<strong>、</strong>“支付成功页”<strong>、</strong>“订单详情页”**等场景，用户的购买意图极强。</li><li>在这些“黄金地段”，召回策略会变得更“激进”，大力度地推荐与用户购买/加购商品高度相关的付费推广商品，转化率极高。</li></ul></li></ol></li></ul></li></ul><p><strong>总结</strong>：特殊场景的优化，要求我们将<strong>业务逻辑</strong>深度嵌入到召回算法的设计中。召回模型需要理解的，不再仅仅是用户的兴趣向量，更是复杂的<strong>场景上下文、用户意图强度和商业化目标</strong>。</p><h3 id="3-多路召回的融合方法-Merging-Methods-for-Multi-Path-Recall-拓展"><code>3. 多路召回的融合方法 (Merging Methods for Multi-Path Recall)[拓展]</code></h3><ul><li><p><code>3.1. Zigzag Merge 策略</code></p><p><strong>核心思想：“交叉轮播，雨露均沾”</strong></p><p>Zigzag Merge，又称交叉合并，它的核心思想非常直观：<strong>避免任何单一召回源霸占最终列表的头部位置，保证各路召回的优质结果都有机会得到展现，从而提升最终候选集的整体多样性和覆盖率。</strong></p><p>它就像洗牌一样，将来自不同牌堆的牌交叉地叠在一起，形成一副新的、混合均匀的牌。</p><p><strong>工作流程详解</strong></p><p>中清晰地展示了 Zigzag Merge 的完整步骤，我们来详细解读一下：</p><p><strong>第一步：召回排序 (Rank within each path)</strong></p><ul><li><strong>前提条件</strong>：在进行合并之前，每一路召回策略必须<strong>已经完成了内部的排序</strong>。</li><li><strong>例如</strong>：<ul><li><strong>向量召回A路</strong>，已经根据点积分数从高到低排出了它的结果：<code>[A1, A2, A3, A4, A5, ...]</code></li><li><strong>ItemCF召回B路</strong>，已经根据物品相似度从高到低排出了它的结果：<code>[B1, B2, B3, B4, B5, ...]</code></li><li><strong>热门召回C路</strong>，已经根据热度分数从高到低排出了它的结果：<code>[C1, C2, C3, C4, C5, ...]</code></li></ul></li></ul><p><strong>第二步：设定比例 (Set Proportions)</strong></p><ul><li><strong>策略核心</strong>：这是注入业务理解和策略调控的关键一步。我们需要根据不同召回路径的重要性和历史表现，为它们设定一个在最终候选集中的<strong>期望占比</strong>。</li><li><strong>例如，我们设定</strong>：<ul><li>路径A (主力个性化路径)：占比 <strong>50%</strong></li><li>路径B (补充关联性路径)：占比 <strong>30%</strong></li><li>路径C (保证多样性与新颖性路径)：占比 <strong>20%</strong></li></ul></li><li>这个比例 <code>5:3:2</code> 将作为后续交叉提取的指导方针。</li></ul><p><strong>第三步：交替提取 (Interleaving / The “Zigzag”)</strong></p><p>这是 Zigzag Merge 最核心的执行步骤，一个轮流从不同列表“取货”的过程。</p><ul><li><strong>过程</strong>：<ol><li>系统会为每个列表维护一个指针，初始都指向列表的第一个位置。</li><li>根据 <code>5:3:2</code> 的比例，进行一轮“取货”。为了尽可能地模拟这个比例，我们可以决定这一轮“取2个A，取1个B，取1个C”。</li><li><strong>开始取货</strong>：<ul><li>从 A 列表的头部取出 <strong>2</strong> 个物品：<code>A1, A2</code>。</li><li>从 B 列表的头部取出 <strong>1</strong> 个物品：<code>B1</code>。</li><li>从 C 列表的头部取出 <strong>1</strong> 个物品：<code>C1</code>。</li></ul></li><li><strong>更新指针</strong>：A列表的指针移动到第3位，B和C的指针移动到第2位。</li><li><strong>循环往复</strong>：不断重复这个“按比例轮流取货”的过程，直到最终的候选集数量达到预设的目标（比如1000个）。</li></ol></li></ul><p><strong>第四步：结果去重 (Deduplication)</strong></p><ul><li><strong>必要性</strong>：不同的召回路径，完全有可能召回同一个物品。比如，一个物品既是热门，又和用户的历史兴趣高度相似。</li><li><strong>做法</strong>：这是一个在<strong>交替提取过程中实时进行</strong>的步骤。每当要从某个列表（比如B列表）取出一个物品（比如<code>B2</code>）时，系统会先检查这个 <code>B2</code> 是不是<strong>已经存在</strong>于我们正在构建的最终候选集里了。<ul><li>如果<strong>已存在</strong>，就跳过 <code>B2</code>，尝试取 B 列表的下一个物品 <code>B3</code>。</li><li>如果<strong>不存在</strong>，才将 <code>B2</code> 加入最终候选集。</li></ul></li></ul><p><strong>第五步：最终候选集 (Final Candidate Set)</strong></p><ul><li>通过以上步骤，我们最终得到一个融合了多路召回结果的、多样化且内部相对有序的候选列表，例如：<code>[A1, A2, B1, C1, A3, B2, A4, C2, ...]</code>。这个列表将作为下一阶段——<strong>粗排层</strong>的输入。</li></ul><p><strong>总结：Zigzag Merge 的价值</strong></p><ul><li><strong>保证多样性与公平性</strong>：这是它最大的价值。通过交叉轮播，它确保了即使是占比很小的召回路径（如新奇特物品探索路径），它的头部优质结果也有机会出现在最终候选集的前列，从而避免了整个推荐系统只推荐“安全牌”，丧失探索能力。</li><li><strong>简单高效</strong>：它是一种启发式规则，不涉及复杂的模型计算，实现简单，运行速度极快，完全满足召回阶段的低延迟要求。</li><li><strong>灵活可控</strong>：算法工程师和策略运营人员可以非常方便地通过调整“第二步”的比例，来调控不同召- 回源在最终结果中的影响力，实现精细化的运营。</li></ul></li></ul><h2 id="四-、-评估体系-The-Evaluation-Framework">四**、 评估体系 (The Evaluation Framework)**</h2><h3 id="1-离线评估-Offline-Evaluation"><code>1. 离线评估 (Offline Evaluation)</code></h3><p><strong>1. Recall@K (召回率@K)</strong></p><ul><li><p><strong>核心问题</strong>：“在我为用户召回的 Top-K 个物品中，成功‘覆盖’了多少用户在未来<strong>真正喜欢</strong>的物品？”</p></li><li><p><strong>公式</strong> (针对单个用户)：</p><p><img src="image%204.png" alt="image.png"></p></li><li><p><strong>名词解释</strong>：</p><ul><li><code>召回的TopK结果</code>：你的新召回模型为该用户产出的前 K 个物品。</li><li><code>用户真实喜欢的物品集合 (Ground Truth)</code>：在测试集中，该用户有过明确正反馈（如点击、购买）的物品集合。</li></ul></li><li><p><strong>举例说明</strong>：</p><ul><li>在测试数据中，用户小明真实点击了5个商品 <code>&#123;A, B, C, D, E&#125;</code>。</li><li>你的召回模型为他召回了500个商品，其中排名前500的结果里，包含了 <code>&#123;A, D, F, G, ...&#125;</code>。</li><li>两个集合的交集是 <code>&#123;A, D&#125;</code>，数量为2。</li><li>那么，你的模型对小明的 <code>Recall@500</code> = 2 / 5 = <strong>40%</strong>。</li><li>最终的全局指标，是所有用户的 Recall@K 的平均值。</li></ul></li><li><p><strong>解读与局限</strong>：</p><ul><li><strong>解读</strong>：这个指标直接衡量了召回模型的“本职工作”——<strong>查全率</strong>。它反映了模型为后续排序阶段提供“炮弹”（好物料）的能力。Recall@K 越高，说明漏掉的好东西越少。</li><li><strong>局限</strong>：它<strong>完全不关心顺序</strong>。在上面的例子中，无论 A 和 D 出现在召回列表的第1、2位，还是第499、500位，Recall@500 的值都是一样的。</li></ul></li></ul><p><strong>“教科书”中的 Recall：二分类世界的视角</strong></p><p>在一个二分类问题（比如，判断一封邮件是不是垃圾邮件）中：</p><ul><li><strong>TP (True Positive)</strong>：邮件是垃圾邮件，模型也正确地预测为垃圾邮件。</li><li><strong>FN (False Negative)</strong>：邮件是垃圾邮件，但模型错误地预测为正常邮件。（<strong>漏报了</strong>）</li><li><strong>FP (False Positive)</strong>：邮件是正常邮件，但模型错误地预测为垃圾邮件。（<strong>误报了</strong>）</li><li><strong>TN (True Negative)</strong>：邮件是正常邮件，模型也正确地预测为正常邮件。</li></ul><p>这里的 <strong>Recall（召回率）</strong> 公式是：</p><p><img src="image%205.png" alt="image.png"></p><ul><li><strong>分母 (TP + FN)</strong>：代表<strong>所有实际上是正样本</strong>的数量（所有真正的垃圾邮件数量）。</li><li><strong>分子 (TP)</strong>：代表在所有正样本中，<strong>被模型成功找出来</strong>的数量。</li></ul><p>所以，它的核心问题是：“<strong>在所有真正的垃圾邮件里，我们成功揪出来了多少？</strong>” 它衡量的是模型“找全”正样本的能力。</p><p><strong>推荐召回场景的特殊性</strong></p><p>现在，我们试着把这个定义直接套用到推荐召回上，会发现两个核心困难：</p><ol><li><strong>“负样本”的定义极其困难，尤其是 TN</strong>：<ul><li><strong>TP</strong>：我们推荐了一个物品，用户点击了。(√ 好定义)</li><li><strong>FN</strong>：一个物品用户很喜欢，但我们没推荐。 (√ 好定义)</li><li><strong>TN (True Negative)</strong>：我们<strong>没有</strong>推荐一个物品，而用户<strong>也确实不喜欢</strong>它。<strong>这个集合有多大？</strong> 对于一个用户来说，可能是<strong>几百万甚至上亿</strong>的物品！我们不可能知道用户对所有未推荐物品的真实偏好。因为无法确定 TN，也就无法计算像 Accuracy 这样的指标。</li></ul></li><li><strong>推荐的输出不是“是/否”，而是一个“列表”</strong>：<ul><li>二分类模型会对<strong>每一个</strong>样本都给出一个“是”或“否”的判断。</li><li>推荐召回系统不会对全站所有物品都做判断，它的最终输出是一个<strong>Top-K 的、排好序的候选列表</strong>（比如，召回500个物品）。</li></ul></li></ol><p><strong>2. Hit Rate@K (命中率@K)</strong></p><ul><li><p><strong>核心问题</strong>：“我的召回策略，对于多大比例的用户群体是‘有效’的？”</p></li><li><p><strong>计算方式</strong>：</p><ol><li><p>对于每一个用户，判断他的 <code>Ground Truth</code> 中，是否有<strong>至少一个</strong>物品出现在了召回的 Top-K 结果里。</p></li><li><p>如果有，则记为一次**“命中 (Hit)”**，这位用户的得分是1；否则为0。</p></li><li><p>最终的指标是：</p><p><img src="image%206.png" alt="image.png"></p></li></ol></li><li><p><strong>举例说明</strong>：</p><ul><li>我们在10000个用户上进行测试。</li><li>测试结果发现，有7800个用户，他们至少有一个未来点击的商品，被我们的模型成功召回在了Top-500的列表里。</li><li>那么，<code>Hit Rate@500</code> = 7800 / 10000 = <strong>78%</strong>。</li></ul></li><li><p><strong>解读与局限</strong>：</p><ul><li><strong>解读</strong>：Hit Rate 衡量了召回策略的**“覆盖广度”**。一个高 Hit Rate 意味着你的策略对于大部分用户都能起到最基本的作用，至少没“空手而归”。</li><li><strong>局限</strong>：这是一个<strong>非常粗粒度</strong>的指标。对于一个用户，命中1个物品和命中10个物品，都被记为一次 Hit，它无法区分这种“命中质量”的差异。</li></ul></li></ul><p><strong>3. MAP@K (Mean Average Precision@K / 平均准确率均值@K)</strong></p><ul><li><strong>核心思想</strong>：这个指标开始**关心“排序位置”**了。它认为，命中一个物品，且这个物品排在召回列表的越前面，得分就应该越高。</li><li><strong>计算方式</strong>：它由两个部分构成：AP 和 MAP。<ol><li><p><strong>Precision@i (P@i)</strong>：前 i 个推荐结果中的准确率。</p></li><li><p><strong>Average Precision (AP@K)</strong> (针对单个用户)：只在<strong>命中正样本的位置</strong>计算 P@i，然后求平均。</p><p><img src="image%207.png" alt="image.png"></p><p>其中 <code>rel(i)</code> 是一个指示函数，如果位置 <code>i</code> 的物品是正样本，则为1，否则为0。</p></li><li><p><strong>Mean Average Precision (MAP@K)</strong>：将所有用户的 AP@K 值求算术平均。</p></li></ol></li><li><strong>举例说明</strong>：<ul><li>小明的 Ground Truth 是 <code>&#123;A, C&#125;</code>。</li><li>模型的召回列表是 <code>[A, B, C, D, ...]</code> (K&gt;=4)。</li><li><strong>计算小明的 AP@4</strong>：<ul><li>在位置1，命中了A：此时 P@1 = 1/1 = 1.0。</li><li>在位置2，没命中。</li><li>在位置3，命中了C：此时 P@3 = 2/3 ≈ 0.67。</li><li>AP@4 = (1.0 + 0.67) / 2 = <strong>0.835</strong>。</li></ul></li><li>如果召回列表是 <code>[B, C, D, A, ...]</code>，那么 AP@4 = (1/2 + 2/4) / 2 = <strong>0.5</strong>。分数显著降低。</li></ul></li><li><strong>解读</strong>：MAP@K 是一个对排序非常敏感的指标，它能很好地反映出模型<strong>将好东西排在前面的能力</strong>。</li></ul><p><strong>4. NDCG@K (Normalized Discounted Cumulative Gain@K / 归一化折损累计增益@K)</strong></p><ul><li><p><strong>核心思想</strong>：这是目前学术界和工业界最常用、最科学的排序评估指标之一。它在 MAP 的基础上，考虑了**“相关度的等级”**。</p></li><li><p><strong>计算方式</strong>：由四个部分层层递进。</p><ol><li><p><strong>Cumulative Gain (CG@K)</strong>：累计增益。只把 Top-K 个物品的<strong>相关度得分</strong>（不再是0/1，可以是1, 2, 3, 4, 5这样的等级）加起来，不考虑顺序。</p></li><li><p><strong>Discounted CG (DCG@K)</strong>：折损累计增益。对排在后面的物品进行“惩罚”，即越往后的物品，其相关度得分要除以一个越大的“折损”系数（通常是 <code>log₂(position+1)</code>）。</p><p><img src="image%208.png" alt="image.png"></p></li><li><p><strong>Ideal DCG (IDCG@K)</strong>：理想折损累计增益。即对于一个用户，最理想的、完美的推荐排序所能得到的 DCG 分数。</p></li><li><p><strong>Normalized DCG (NDCG@K)</strong>：将模型的实际 DCG 分数除以理想的 IDCG 分数，得到一个 <code>[0, 1]</code> 之间的归一化结果。</p><p><img src="image%209.png" alt="image.png"></p></li></ol></li><li><p><strong>解读</strong>：NDCG@K 不仅对排序位置敏感（DCG），还能处理多等级的相关度（比如“完美相关=5分”，“一般相关=3分”），并且通过归一化（NDCG），使得不同用户之间的评估结果具有可比性，是衡量排序质量的**“金标准”**。</p></li></ul><p><strong>召回离线评估的挑战与实践</strong></p><ul><li><strong>全库评估的难题</strong>：在召回阶段，我们不可能真的为每个用户都对全量百万、千万的物品进行打分和排序来计算上述指标，这在计算上是不可行的。</li><li><strong>业界常用实践</strong>：将评估问题转化为一个**“排序中的排序”<strong>问题。对于测试集中的每一个</strong>正样本**，我们再额外<strong>随机采样</strong> 999个（或其他数量）用户未交互过的负样本，构成一个<strong>1+999</strong>的候选池。然后，我们评估新召回模型，能否在这个1000个物品的集合中，将那个唯一的正样本排在尽可能靠前的位置。在这个小集合上，我们就可以高效地计算上述所有指标。</li></ul><h3 id="2-在线评估-Online-Evaluation"><code>2. 在线评估 (Online Evaluation)</code></h3><p>离线评估是在“实验室”里用历史数据进行的“模拟考试”，它快速、安全、低成本。然而，<strong>在线评估，才是在真实世界里检验模型能力的“最终高考”</strong>。</p><p>离线评估有其固有局限性（如数据偏差、无法衡量新颖性等），因此，一个离线指标提升的模型，在线上不一定能带来业务增长。在线评估通过将新策略推送给<strong>真实的、活生生的用户</strong>，直接衡量它对核心业务指标的<strong>真实影响</strong>，是所有算法和策略迭代最终的、最权威的“裁判”。</p><p>在线评估主要有两种黄金标准方法：A/B 测试和交叉实验。</p><p><strong>2.1. A/B 测试 (A/B Testing)</strong></p><p>A/B 测试是互联网产品迭代和算法评估的基石，是一种严谨的<strong>对照实验</strong>方法。</p><p><strong>核心思想：“控制变量，对比观察”</strong></p><p><strong>比喻：药物临床试验</strong></p><ul><li>为了验证一种新药是否有效，我们会招募两组病人。一组服用新药（实验组），另一组服用外观一样的安慰剂（控制组）。通过严格对比两组病人的康复情况，我们就能科学地判断新药的真实疗效。</li></ul><p><strong>如何设计一个召回策略的 A/B 测试</strong></p><p><strong>确定目标与假设 (Define Goal &amp; Hypothesis)</strong></p><ul><li><strong>假设</strong>：明确你认为新策略会带来什么改变。例如：“我假设，新的 Swing 算法召回策略，相比于老的 ItemCF 策略，能够提升用户的点击率和互动多样性。”</li><li><strong>目标</strong>：将假设转化为可量化的指标。</li></ul><p><strong>划分流量 (Split Traffic)</strong></p><ul><li>将线上用户<strong>随机、均匀</strong>地分成几个互不重叠的组。最简单的就是两组：<ul><li><strong>控制组 (Group A)</strong>：继续使用当前线上的、稳定的旧召回策略。这是我们的<strong>基线 (Baseline)</strong>。</li><li><strong>实验组 (Group B)</strong>：使用我们想要验证的<strong>新召回策略</strong>。</li></ul></li><li><strong>划分方法</strong>：通常使用用户ID或设备ID进行哈希，然后对哈希值取模来分流，以保证分组的随机性和均匀性。必须确保两个组的用户画像（如新老、活跃度等）在统计上是一致的。</li></ul><p><strong>确定实验周期 (Determine Duration)</strong></p><ul><li>实验需要运行足够长的时间，以<strong>保证收集的数据量足以得出统计显著的结论</strong>。</li><li>同时，周期应覆盖一个完整的业务循环（比如至少一周），以<strong>消除节假日、周末等周期性因素的干扰</strong>，并减弱新功能上线带来的“新奇效应”。</li></ul><p><strong>数据收集与分析 (Collect &amp; Analyze Data)</strong></p><ul><li>在实验期间，系统会记录并区分来自不同组的用户行为日志。</li><li>实验结束后，我们会对比两组的核心业务指标，并通过<strong>统计检验</strong>（如t检验、卡方检验）来判断观察到的差异是否<strong>统计显著 (Statistically Significant)</strong>。只有当p-value小于一个阈值（如0.05）时，我们才能有信心地说：“实验组的提升不是由偶然运气造成的，而是新策略的真实效果。”</li></ul><p><strong>关注哪些核心业务指标</strong></p><p>一个好的评估体系，会同时监控多维度指标，以全面判断一个策略的好坏。</p><ul><li><strong>效果指标 (Engagement Metrics)</strong> - “用户是否更喜欢？”<ul><li><strong>点击率 (CTR)</strong>、<strong>转化率 (CVR)</strong>：最核心的效率指标。</li><li><strong>人均观看/停留时长 (Duration)</strong>：衡量用户的内容消费深度。</li><li><strong>互动率</strong>：人均点赞、评论、分享、收藏等。</li></ul></li><li><strong>商业/效率指标 (Business/Efficiency Metrics)</strong> - “我们是否更赚钱？”<ul><li><strong>人均推荐贡献收入 (GMV / RPM)</strong>：在电商或广告场景下，衡量变现效率。</li><li><strong>人均推荐引导访问/购买数</strong>。</li></ul></li><li><strong>生态/长期指标 (Ecosystem/Long-term Metrics)</strong> - “推荐是否健康可持续？”<ul><li><strong>推荐多样性</strong>：推荐结果的类目、作者是否更丰富。</li><li><strong>负反馈率</strong>：用户“不感兴趣”、屏蔽、取消关注等行为的比例。</li><li><strong>用户留存率 (Retention)</strong>：新策略是否对用户的长期留存有正向或负向影响。<strong>这是一个非常重要的“护栏”指标</strong>，需要警惕那些短期提升CTR但长期损害用户留存的“标题党”式策略。</li></ul></li></ul><p><strong>2.2. 交叉实验 (Interleaving)</strong></p><p><strong>核心思想：“同台竞技，立分高下”</strong></p><p>A/B 测试虽然严谨，但当两个策略效果差异很微小时，往往需要很长的测试时间和海量用户才能得出结论。交叉实验是一种<strong>更灵敏、更高效</strong>的，专门用于<strong>对比两个排序算法优劣</strong>的在线评估方法。</p><p><strong>比喻：饮料的“盲品测试”</strong></p><ul><li>A/B测试就像让一群人连续一周只喝可口可乐，另一群人只喝百事可乐，最后看哪个群体更开心。</li><li>交叉实验则是把可口可乐和百事可乐都倒进没有标签的杯子里，让同一个人同时品尝，然后直接问他“你更喜欢哪一杯？”。这种直接的、并列的比较，能更快速、更敏感地分出高下。</li></ul><p><strong>工作原理</strong></p><p><strong>混合列表</strong>：被分到交叉实验组的用户，看到的推荐列表<strong>不是</strong>单纯来自策略A或策略B，而是一个由<strong>A和B的结果混合而成</strong>的列表。</p><p><strong>交替展示 (Interleave)</strong>：系统会从A的排序结果 <code>[A1, A2, A3, ...]</code> 和 B的排序结果 <code>[B1, B2, B3, ...]</code> 中，<strong>交替地取出物品</strong>，形成一个最终展示给用户的混合列表，比如 <code>[A1, B1, A2, B2, A3, B3, ...]</code>。</p><p><strong>点击归因 (“投票”)</strong>：当用户在这个混合列表上发生点击时，这个点击就会被归因于最初提供这个物品的那个策略。</p><ul><li>如果用户点击了 <code>A2</code>，那么策略A就**“得一分”**。</li><li>如果用户点击了 <code>B3</code>，那么策略B就**“得一分”**。</li></ul><p><strong>对比胜率</strong>：实验运行一段时间后，我们统计策略A和策略B各自的总得分。如果策略A的得分显著高于策略B，我们就能非常有信心地判断：<strong>策略A的排序结果比B更吸引用户</strong>。</p><p><strong>优点与局限</strong></p><p><strong>优点</strong>：</p><ul><li><strong>极高的灵敏度</strong>：由于是在同一个用户、同一个页面上进行直接比较，它消除了大量因“用户差异”带来的噪声，能够检测出非常微弱的排序效果差异。</li><li><strong>极高的效率</strong>：达到统计显著性所需要的<strong>用户量和时间</strong>，远少于A/B测试。非常适合算法工程师进行快速的、小步快跑式的迭代。</li></ul><p><strong>局限</strong>：</p><ul><li><strong>应用范围窄</strong>：它主要用于评估<strong>排序质量</strong>的优劣，不适用于评估那些无法以“排序列表”形式呈现的、差异巨大的产品改动（比如UI界面、商业化策略等）。</li><li><strong>实现更复杂</strong>：需要开发一套支持列表混合、点击归因的实验框架。</li></ul><h3 id="3-超越准确性的评估"><code>3. 超越准确性的评估</code></h3><p>如果我们只关注 CTR、CVR 等准确性指标，推荐系统会很快陷入一个**“越推越窄”<strong>的恶性循环，最终给用户造成</strong>“信息茧房”和审美疲劳**。多样性、新颖性和覆盖率，就是衡量系统这种“健康度”和“长远眼光”的核心指标。<br><strong>3.1. 多样性 (Diversity)</strong></p><ul><li><strong>核心问题</strong>：“我给用户推荐的这一页内容，是不是看起来都差不多？”</li><li><strong>定义</strong>：多样性衡量的是<strong>单次推荐列表内部</strong>，物品之间互不相似的程度。它的目标是<strong>避免推荐结果的同质化</strong>。</li><li><strong>一个比喻：健康的饮食</strong><ul><li>如果一个营养师给你推荐的“健康餐”是：<code>[西兰花, 芥蓝, 西芹, 菜花]</code>，虽然都是健康蔬菜，但过于单调。</li><li>一个好的推荐应该是：<code>[西兰花, 鸡胸肉, 糙米饭, 苹果]</code>，种类丰富，营养均衡。推荐系统也应如此。</li></ul></li></ul><p><strong>如何度量多样性？</strong></p><ol><li><strong>Intra-List Similarity (ILS / 列表内相似度)</strong>：这是最常用的指标。<ul><li><strong>计算方式</strong>：<ol><li>在一个推荐列表（比如Top-10）中，取出所有的物品对（比如 <code>C(10,2)=45</code> 对）。</li><li>计算每一对物品之间的相似度（可以用物品 Embedding 的余弦相似度，或者类目、标签的 Jaccard 相似度等）。</li><li>将所有这些相似度值求一个平均，就得到了 ILS。</li></ol></li><li><strong>解读</strong>：ILS 的值越高，代表列表内的物品越相似，<strong>多样性就越差</strong>。因此，我们通常用 <strong><code>Diversity = 1 - ILS</code></strong> 来表示多样性，这个值越高越好。</li></ul></li><li><strong>类目/标签多样性 (Category/Tag Diversity)</strong>：<ul><li><strong>计算方式</strong>：一个更简单、更可解释的方法。直接统计一个推荐列表（比如Top-10）中，出现了多少个<strong>独一无二的物品类目或标签</strong>。</li><li><strong>解读</strong>：不重复的类目/标签数量越多，说明推荐的内容来源越广泛，多样性就越好。</li></ul></li></ol><p><strong>3.2. 新颖性 (Novelty)</strong></p><ul><li><strong>核心问题</strong>：“我推荐的东西，对用户来说有多大的‘惊喜感’？”</li><li><strong>定义</strong>：新颖性衡量的是推荐结果对于<strong>某个特定用户</strong>来说，有多么的**“新奇”、“出乎意料”<strong>。它关注的是物品与</strong>用户历史行为**之间的差异性。</li><li><strong>与多样性的关键区别</strong>：<ul><li><strong>多样性</strong>：是列表内的物品<strong>互相之间</strong>有多不一样。</li><li><strong>新颖性</strong>：是列表内的物品与<strong>用户的过去</strong>有多不一样。</li><li><strong>例子</strong>：给一个科幻迷推荐10部他都没看过的、不同题材的冷门科幻电影，这个列表<strong>多样性</strong>很高，但<strong>新颖性</strong>可能并不高（因为还是在他熟悉的领域内）。如果此时推荐了一部高质量的历史纪录片并被他喜欢了，这才叫高新颖性。</li></ul></li></ul><p><strong>如何度量新颖性？</strong></p><ol><li><strong>推荐列表的平均流行度 (Mean Popularity)</strong>：<ul><li><strong>计算方式</strong>：计算推荐列表中所有物品的平均流行度（比如，平均被多少用户消费过）。</li><li><strong>解读</strong>：平均流行度越<strong>低</strong>，说明推荐的物品越<strong>小众</strong>，因此新颖性就越<strong>高</strong>。</li></ul></li><li><strong>平均自信息 (Mean Self-Information)</strong>：<ul><li><strong>计算方式</strong>：这是对平均流行度的一种更优雅的数学表达。一个物品 <code>i</code> 的流行度（即它被消费的概率）为 <code>p(i)</code>，那么它的自信息就是 <code>log₂(p(i))</code>。越流行的物品，<code>p(i)</code> 越高，自信息就越低。推荐列表的新颖性，就是列表中所有物品的平均自信息。</li><li><strong>解读</strong>：推荐的物品越冷门、越小众，这个值就越高，代表新颖性越好。</li></ul></li></ol><p><strong>3.3. 覆盖率 (Coverage)</strong></p><p><strong>核心问题</strong>：“我的推荐系统，有没有‘雨露均沾’？全站有多少比例的物品曾经得到过推荐的机会？”</p><p><strong>定义</strong>：覆盖率是一个<strong>系统级</strong>的宏观指标，它衡量的是在一段时间内，推荐系统能够推荐出的<strong>不重复物品的总数</strong>，占整个物品库总数的比例。</p><p><strong>为什么覆盖率很重要？</strong></p><p><strong>平台生态健康</strong>：高覆盖率意味着平台上的<strong>长尾物品、新晋内容创作者/商家</strong>有被发现的机会，而不是让所有流量都集中在少数几个头部爆款上。这对于维持一个健康、有活力的创作者/商家生态至关重要。</p><p><strong>用户长期体验</strong>：高覆盖率是实现新颖性和多样性的基础。如果系统来来回回只能推荐那一小撮东西，用户的长期体验必然会下降。</p><p><strong>如何度量覆盖率？</strong></p><p><strong>物品空间覆盖率 (Item-Space Coverage)</strong>：</p><ul><li><p><strong>计算方式</strong>：</p><ol><li>选定一个时间窗口（比如一周）。</li><li>统计在这个窗口内，<strong>至少被推荐给过任意一个用户的、所有不重复的物品</strong>总数。</li><li>用这个总数，除以平台<strong>总的物品库数量</strong>。</li></ol><p><img src="image%2010.png" alt="image.png"></p></li></ul><p><strong>基尼系数 (Gini Coefficient) / 熵 (Entropy)</strong>：</p><ul><li>这两个指标可以衡量<strong>曝光分布的均衡性</strong>。</li><li><strong>基尼系数</strong>越低，或<strong>熵</strong>越高，说明推荐系统给予每个物品的曝光机会越<strong>公平、越均匀</strong>，而不是集中在少数爆款上。</li></ul>]]>
    </content>
    <id>https://qyp9909.github.io/2025/08/06/Rec_AD_Recall/</id>
    <link href="https://qyp9909.github.io/2025/08/06/Rec_AD_Recall/"/>
    <published>2025-08-06T09:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="搜广推-召回">搜广推[召回]</h2>
<p>Created by: Yuanpeng QU<br>
Created time: 2025年8月5日 22:51</p>
<h2 id="一、-推荐系统总体架构-Overall-Recommendation-Sys]]>
    </summary>
    <title>搜广推笔记 召回</title>
    <updated>2025-08-09T10:04:57.517Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="movies" scheme="https://qyp9909.github.io/categories/movies/"/>
    <category term="movie" scheme="https://qyp9909.github.io/tags/movie/"/>
    <content>
      <![CDATA[<p>作为一个影迷，我经常被电影中的故事、情感和摄影打动。以下是我最喜欢的十部电影，它们或震撼心灵，或温柔治愈，值得每一个人静下心来欣赏。</p><h2 id="🎬-Top-10-私藏电影清单">🎬 Top 10 私藏电影清单</h2><ol><li><p><strong>《肖申克的救赎》（The Shawshank Redemption）</strong></p><blockquote><p>“希望是件好东西，也许是最好的，好东西是不会消亡的。”</p></blockquote></li><li><p><strong>《阿甘正传》（Forrest Gump）</strong></p><blockquote><p>人生就像一盒巧克力，你永远不知道你会得到什么。</p></blockquote></li><li><p><strong>《星际穿越》（Interstellar）</strong></p><blockquote><p>爱是一种我们还无法理解的力量。</p></blockquote></li><li><p><strong>《美丽人生》（La vita è bella）</strong></p><blockquote><p>即使在最黑暗的时刻，也要为孩子营造光明。</p></blockquote></li><li><p><strong>《千与千寻》</strong></p><blockquote><p>每次重温宫崎骏，都像重拾童年梦境。</p></blockquote></li><li><p><strong>《忠犬八公的故事》</strong></p><blockquote><p>忠诚和爱不需要语言。</p></blockquote></li><li><p><strong>《盗梦空间》（Inception）</strong></p><blockquote><p>一部让人深思的烧脑神作。</p></blockquote></li><li><p><strong>《海上钢琴师》（The Legend of 1900）</strong></p><blockquote><p>在海上，他弹奏出了整个世界。</p></blockquote></li><li><p><strong>《放牛班的春天》</strong></p><blockquote><p>音乐是最温柔的救赎。</p></blockquote></li><li><p><strong>《无间道》</strong></p></li></ol><blockquote><p>一念天堂，一念地狱，港片巅峰。</p></blockquote><hr><h2 id="🍿-推荐理由">🍿 推荐理由</h2><p>这十部电影风格各异，但共同之处是——<strong>它们都在某个阶段触动了我</strong>。不论是故事深度、视觉冲击还是情感共鸣，都值得收藏。</p><p>你也有私藏的 Top 10 吗？欢迎在评论区留言分享！</p><hr><blockquote><p>📌 本文同步更新于 <a href="/movies/">我的电影页面</a>，欢迎前往查看更多 🎥</p></blockquote>]]>
    </content>
    <id>https://qyp9909.github.io/2025/06/09/my-first-blog/</id>
    <link href="https://qyp9909.github.io/2025/06/09/my-first-blog/"/>
    <published>2025-06-09T08:00:00.000Z</published>
    <summary>一起看看我最爱的十部电影，每一部都是心头好。</summary>
    <title>我最喜欢的十部电影推荐</title>
    <updated>2025-06-14T08:34:01.181Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="232-Implement-Queue-using-Stacks">232. Implement Queue using Stacks</h2><p>python中list是一个常用的栈，所以可以用list来模拟队列</p><p>注意pop方法可以用deque方法更加高效。</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> collections <span class="keyword">import</span> deque</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">pop</span>(<span class="params">self</span>):</span><br><span class="line">    res = <span class="variable language_">self</span>.queue.popleft()</span><br><span class="line">    <span class="keyword">return</span> res</span><br></pre></td></tr></table></figure><p>GPT版本：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyQueue</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;初始化一个空队列&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.queue = deque()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">push</span>(<span class="params">self, x: <span class="built_in">int</span></span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">        <span class="string">&quot;&quot;&quot;入队，在队列尾部添加元素&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.queue.append(x)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">pop</span>(<span class="params">self</span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        <span class="string">&quot;&quot;&quot;出队，从队列头部删除并返回元素&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.queue.popleft() <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.empty() <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">peek</span>(<span class="params">self</span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        <span class="string">&quot;&quot;&quot;查看队列头部的元素&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.queue[<span class="number">0</span>] <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.empty() <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">empty</span>(<span class="params">self</span>) -&gt; <span class="built_in">bool</span>:</span><br><span class="line">        <span class="string">&quot;&quot;&quot;检查队列是否为空&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.queue) == <span class="number">0</span></span><br></pre></td></tr></table></figure><h2 id="225-Implement-Stack-using-Queues">225. Implement Stack using Queues</h2><p>完全做出来了</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyStack</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="variable language_">self</span>.stack = []</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">push</span>(<span class="params">self, x</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type x: int</span></span><br><span class="line"><span class="string">        :rtype: None</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.stack.append(x)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">pop</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> <span class="variable language_">self</span>.empty():</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span> </span><br><span class="line">        <span class="comment"># self.stack.reverse()</span></span><br><span class="line">        <span class="comment"># res = self.stack.pop(0)</span></span><br><span class="line">        <span class="comment"># self.stack.reverse()</span></span><br><span class="line">        res = <span class="variable language_">self</span>.stack[-<span class="number">1</span>]</span><br><span class="line">        <span class="keyword">return</span> res</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">top</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> <span class="variable language_">self</span>.empty():</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.stack[<span class="built_in">len</span>(<span class="variable language_">self</span>.stack)-<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">empty</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :rtype: bool</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.stack) == <span class="number">0</span></span><br></pre></td></tr></table></figure><h2 id="20-Valid-Parentheses">20. Valid Parentheses</h2><p>这题很经典，使用栈匹配括号，但是没做出来</p><p>核心思路是左括号和右括号的顺序是一样的，只要有左括号就一定有右括号，并且先进后出，也就是匹配到的左括号所对应的右括号放入栈底，然后逐渐放入其他各种右括号，然后关键是如果不是左括号的话，那就判断当前元素是否等于栈顶的右括号，如果不等于那就是错的，因为如果是类似[()]的话一定是对应的。最后还是满足的话就将最顶上的元素出栈，如果正确匹配的话最后栈一定是空的。</p><h2 id="1047-Remove-All-Adjacent-Duplicates-In-String">1047. Remove All Adjacent Duplicates In String</h2><p>这题也完全做出来了</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">removeDuplicates</span>(<span class="params">self, s</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type s: str</span></span><br><span class="line"><span class="string">        :rtype: str</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        stack = []</span><br><span class="line">        <span class="keyword">for</span> ss <span class="keyword">in</span> s:</span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">len</span>(stack) == <span class="number">0</span> <span class="keyword">or</span> stack[-<span class="number">1</span>] != ss: <span class="comment"># 这里写 not stack也可以</span></span><br><span class="line">                stack.append(ss)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                stack.pop(-<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span>.join(stack)</span><br><span class="line">        </span><br></pre></td></tr></table></figure><h2 id="1365-How-Many-Numbers-Are-Smaller-Than-the-Current-Number">1365. How Many Numbers Are Smaller Than the Current Number</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">k = <span class="number">0</span></span><br><span class="line">count = [<span class="number">0</span>] * <span class="built_in">len</span>(nums)</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">    k = <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">        <span class="keyword">if</span> nums[i] &gt; nums[j]:</span><br><span class="line">            k += <span class="number">1</span></span><br><span class="line">            count[i] = k</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> count</span><br><span class="line"></span><br><span class="line">         count = [<span class="number">0</span>] * <span class="built_in">len</span>(nums)</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)):</span><br><span class="line">    <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(i+<span class="number">1</span>, <span class="built_in">len</span>(nums)):</span><br><span class="line">        <span class="keyword">if</span> nums[i] &gt; nums[j]:</span><br><span class="line">            count[i] += <span class="number">1</span></span><br><span class="line">        <span class="keyword">elif</span> nums[i] &lt; nums[j]: </span><br><span class="line">            count[j] += <span class="number">1</span>            </span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> count</span><br></pre></td></tr></table></figure><p>暴力解法 时间复杂度n平方，需要优化</p><p>可以用哈希表，之后再写一遍</p><h2 id="150-Evaluate-Reverse-Polish-Notation">150. Evaluate Reverse Polish Notation</h2><p>逆波兰表达式求值，还没做</p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/04/19/leetcode/leetcode-5/</id>
    <link href="https://qyp9909.github.io/2025/04/19/leetcode/leetcode-5/"/>
    <published>2025-04-19T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="232-Implement-Queue-using-Stacks">232. Implement Queue using Stacks</h2>
<p>python中list是一个常用的栈，所以可以用list来模拟队列</p>
<p>注意pop方法可以用deque]]>
    </summary>
    <title>LeetCode 5</title>
    <updated>2025-06-25T11:06:51.386Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="344-Reverse-String">344. Reverse String</h2><p>这题自己做对了，还有更精简的方法：</p><p>使用<code>reversed，s[:] = reversed(s)</code></p><p>使用切片，<code>s[:] = s[::-1]</code></p><p>使用<code>reverse(), s.reverse()</code></p><h2 id="541-Reverse-String-II">541. Reverse String II</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">reverseStr</span>(<span class="params">self, s, k</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type s: str</span></span><br><span class="line"><span class="string">        :type k: int</span></span><br><span class="line"><span class="string">        :rtype: str</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        s_list = [ss <span class="keyword">for</span> ss <span class="keyword">in</span> s]</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> k &gt;= <span class="built_in">len</span>(s_list):</span><br><span class="line">            s_list.reverse()</span><br><span class="line">            res = <span class="string">&quot;&quot;</span>.join(s_list)</span><br><span class="line">            <span class="keyword">return</span> res </span><br><span class="line"></span><br><span class="line">        k_l, k_r = <span class="number">0</span>, k</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> k_r &lt;= <span class="built_in">len</span>(s_list):</span><br><span class="line"></span><br><span class="line">            k_reverse = s_list[k_l:k_r]</span><br><span class="line">            k_reverse.reverse()</span><br><span class="line">            s_list[k_l:k_r] = k_reverse</span><br><span class="line">            k_l += k*<span class="number">2</span></span><br><span class="line">            k_r += k*<span class="number">2</span></span><br><span class="line"></span><br><span class="line">        res = <span class="string">&quot;&quot;</span>.join(s_list)</span><br><span class="line">        <span class="keyword">return</span> res</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我写的代码有两个错误，一个是while循环的条件错了，应该是<code>k_l &lt; len(s_list)</code>，因为如果用k_r的话那么k_l到结尾距离要是小于2k但是大于k的时候不会被处理。</p><p>第二个是<code>k_r += k*2</code>可能出现越界的问题，因此需要取最小值<code>k_r = min(len(s_list),k_l + k)</code>,使用<code>k_l+k</code>就能描述 <code>k_r += k*2</code>.</p><p>另外，还可以优化代码，比如</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">s_list = [ss <span class="keyword">for</span> ss <span class="keyword">in</span> s] ---&gt; s_list = <span class="built_in">list</span>(s)</span><br><span class="line"></span><br><span class="line">k_reverse = s_list[k_l:k_r]</span><br><span class="line">k_reverse.reverse()</span><br><span class="line">s_list[k_l:k_r] = k_reverse</span><br><span class="line">可以优化成</span><br><span class="line">s_list[k_l:k_r] = <span class="built_in">reversed</span>(s_list[k_l:k_r] )</span><br><span class="line">或者更加简便的写法，思路是将2k看作一个区间，然后用<span class="keyword">for</span>跳着遍历，在每个区间内只反转前k个字符</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, <span class="built_in">len</span>(s_list), <span class="number">2</span> * k):  <span class="comment"># 每 2k 作为一个区间</span></span><br><span class="line">        s_list[i:i + k] = <span class="built_in">reversed</span>(s_list[i:i + k])  <span class="comment"># 仅反转前 k 个字符</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>自动处理边界，不会越界，最后一个区间小于 k 仍然会被反转，如果最后一个区间小于 2k 但大于 k，后 k 个字符保持不变</p><h2 id="151-Reverse-Words-in-a-String">151. Reverse Words in a String</h2><p>这题注意：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s_list = [x <span class="keyword">for</span> x <span class="keyword">in</span> s_list <span class="keyword">if</span> x!=<span class="string">&quot;&quot;</span>] <span class="comment"># 这里的x!=&quot;&quot;而不是x!=&quot; &quot;</span></span><br></pre></td></tr></table></figure><p>这是因为<code>s.split(&quot; &quot;)</code> 按单个空格拆分，多个空格会产生 <code>&quot;&quot;</code> 需要手动去除 <code>&quot;&quot;</code>，</p><p>而<code>s.split()</code> 按空格拆分，自动去掉多余空格</p><p>如果不用split，那可以按传统方法， 分三步</p><p>首先去重，然后双指针将所有字符反转，然后再按空格将每个单词反转(整体反转+局部反转)</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">reverseWords</span>(<span class="params">self, s: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">        <span class="comment"># 将字符串拆分为单词，即转换成列表类型</span></span><br><span class="line">        words = s.split()</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 反转单词</span></span><br><span class="line">        left, right = <span class="number">0</span>, <span class="built_in">len</span>(words) - <span class="number">1</span></span><br><span class="line">        <span class="keyword">while</span> left &lt; right:</span><br><span class="line">            words[left], words[right] = words[right], words[left]</span><br><span class="line">            left += <span class="number">1</span></span><br><span class="line">            right -= <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 将列表转换成字符串</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot; &quot;</span>.join(words)</span><br></pre></td></tr></table></figure><h2 id="28-Find-the-Index-of-the-First-Occurrence-in-a-String">28. Find the Index of the First Occurrence in a String</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">getNext</span>(<span class="params">self, <span class="built_in">next</span>: <span class="type">List</span>[<span class="built_in">int</span>], s: <span class="built_in">str</span></span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">        j = <span class="number">0</span></span><br><span class="line">        <span class="built_in">next</span>[<span class="number">0</span>] = <span class="number">0</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(s)):</span><br><span class="line">            <span class="keyword">while</span> j &gt; <span class="number">0</span> <span class="keyword">and</span> s[i] != s[j]:</span><br><span class="line">                j = <span class="built_in">next</span>[j - <span class="number">1</span>]</span><br><span class="line">            <span class="keyword">if</span> s[i] == s[j]:</span><br><span class="line">                j += <span class="number">1</span></span><br><span class="line">            <span class="built_in">next</span>[i] = j</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">strStr</span>(<span class="params">self, haystack: <span class="built_in">str</span>, needle: <span class="built_in">str</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(needle) == <span class="number">0</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">        <span class="built_in">next</span> = [<span class="number">0</span>] * <span class="built_in">len</span>(needle)</span><br><span class="line">        <span class="variable language_">self</span>.getNext(<span class="built_in">next</span>, needle)</span><br><span class="line">        j = <span class="number">0</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(haystack)):</span><br><span class="line">            <span class="keyword">while</span> j &gt; <span class="number">0</span> <span class="keyword">and</span> haystack[i] != needle[j]:</span><br><span class="line">                j = <span class="built_in">next</span>[j - <span class="number">1</span>]</span><br><span class="line">            <span class="keyword">if</span> haystack[i] == needle[j]:</span><br><span class="line">                j += <span class="number">1</span></span><br><span class="line">            <span class="keyword">if</span> j == <span class="built_in">len</span>(needle):</span><br><span class="line">                <span class="keyword">return</span> i - <span class="built_in">len</span>(needle) + <span class="number">1</span></span><br><span class="line">        <span class="keyword">return</span> -<span class="number">1</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>KMP算法，主要用来匹配子字符串，复杂度m+n，关键思想就是给要匹配的字符串创建一个next数组，用来告诉主指针如果指到没匹配上的位置的时候该从哪个位置开始匹配，而不是从头再来</p><p><code>B站KMP算法中印度老哥讲的最好</code>。主要思想是将要匹配的字符串和主字符串一一比对，如果匹配不上 的话，就在当前位置回头看有没有相同的子序列，比如：</p><p>主字符串：abcab x abcab c aby</p><p>要匹配的：abcab y</p><p>首先将下面和上面比对，直到y这个位置发现匹配不上了，于是就寻找y前面有没有相同的两个或以上的子序列，发现有，也就是ab，那就说明在x和y的前一个都是ab，所以下一次比对开始只要从 y 前面的ab再前面的ab的后一个位置（这里是c）开始比对就行了，</p><p>而主序列也从x开始比对，也就是说首先比对x和c，发现不相等，所以下面的序列从c开始再次往前找有没有两个或者以上的相同子序列，发现前面都是独立的元素并没有子序列，因此就需要从头开始比对，而主序列则继续从x开始比对，</p><p>从x开始后一直到主序列的c和下面序列的y再次比对不上了，这个时候下面序列再次从y往前找有没有相同的子序列，发现还是ab，于是就找到y前面ab的再前面的ab的后面的位置（c）开始继续比对，这次发现主序列也是c，比对上了，那么下面的序列就可以从c开始比对而不是从头开始比对，主序列也是从c开始，直到结束，最后比对成功，返回主序列第一次比对上的下标。</p><p>那么怎么向前找是否有相同的子序列呢，这里定义一个next数组用于存放位置，用两个指针i和j分别指向主序列的开头和第二个位置，其中i用于在前面比对，j用于与i相比较，是否相等。如果相等的话，j的位置的数组值就加1，然后分别往下移动一个位置，然后继续判断第二个位置是否相等，如果再相等的话那j这个位置的值比刚才记录的第一个相等的位置的值再加1也就是2，如果再下一个位置不相等，那就j回到其前一个值所对应的next数组里的位置，这里建议看视频7:40处容易理解。</p><p>或者用双指针，也就是两个指针分别比对是否相等，相等就都++，不相等那要配对的序列的指针就回到初始位置，最后一次会迭代到needle末尾，也就是p2==n2，如果是的话说明比对上了 ，返回p1-p2的差值为开始的下标</p><h2 id="459-Repeated-Substring-Pattern">459. Repeated Substring Pattern</h2><p>这题有几种做法，最有技巧性和直观的是字符串拼接法（最优解，O(N)）</p><p>核心思想：如果 s 可以由子串 t 重复构成，那么：s + s 去掉首尾字符后，仍然包含 s。</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">s = <span class="string">&quot;abab&quot;</span></span><br><span class="line">s + s = <span class="string">&quot;abababab&quot;</span></span><br><span class="line">去掉头尾: <span class="string">&quot;bababa&quot;</span></span><br><span class="line">发现仍然包含 <span class="string">&quot;abab&quot;</span></span><br></pre></td></tr></table></figure><p>所以只需要判断s+s去掉首尾后是否包含s即可，一句话<code>return s in (s+s)[1:-1]</code></p><p>或者KMP 前缀表算法，利用 KMP 算法的 next 数组，找到 s 的最长相同前后缀。</p><p>KMP的主要思想是当出现字符串不匹配时，可以知道一部分之前已经匹配的文本内容，可以利用这些信息避免从头再去做匹配了。</p><p>记 <code>next[-1] = L</code>（最后一个 next 值）。<code>如果 L &gt; 0 且 s 的长度 len(s) % (len(s) - L) == 0</code>，说明 s 是某个子串的重复。</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">repeatedSubstringPattern</span>(<span class="params">s</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">compute_next</span>(<span class="params">s</span>):</span><br><span class="line">            next_arr = [<span class="number">0</span>] * <span class="built_in">len</span>(s)  <span class="comment"># 初始化 next 数组</span></span><br><span class="line">            j = <span class="number">0</span>  <span class="comment"># 记录前缀匹配长度</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(s)):  <span class="comment"># 从索引 1 开始</span></span><br><span class="line">                    <span class="keyword">while</span> j &gt; <span class="number">0</span> <span class="keyword">and</span> s[i] != s[j]:  <span class="comment"># 失配时回退</span></span><br><span class="line">                            j = next_arr[j - <span class="number">1</span>]  </span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> s[i] == s[j]:  <span class="comment"># 匹配成功，增加匹配长度</span></span><br><span class="line">                            j += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">                    next_arr[i] = j  <span class="comment"># 记录前缀匹配值</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> next_arr</span><br><span class="line"></span><br><span class="line">    next_arr = compute_next(s)</span><br><span class="line">    L = next_arr[-<span class="number">1</span>]</span><br><span class="line">    <span class="keyword">return</span> L &gt; <span class="number">0</span> <span class="keyword">and</span> <span class="built_in">len</span>(s) % (<span class="built_in">len</span>(s) - L) == <span class="number">0</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里 next_arr 只用于判断 L 是否能整除 <code>len(s)</code></p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/04/09/leetcode/leetcode-4/</id>
    <link href="https://qyp9909.github.io/2025/04/09/leetcode/leetcode-4/"/>
    <published>2025-04-09T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="344-Reverse-String">344. Reverse String</h2>
<p>这题自己做对了，还有更精简的方法：</p>
<p>使用<code>reversed，s[:] = reversed(s)</code></p>
<p>使用切片，<cod]]>
    </summary>
    <title>LeetCode 4</title>
    <updated>2025-06-25T11:07:46.872Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="242-Valid-Anagram">242. Valid Anagram</h2><p>这题有好几种解法，首先我自己想的</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">s_set, t_set = <span class="built_in">set</span>(s),<span class="built_in">set</span>(t)</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(s_set) != <span class="built_in">len</span>(t_set):</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">merged_set = s_set | t_set</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(merged_set) == <span class="built_in">len</span>(s_set) <span class="keyword">and</span> <span class="built_in">len</span>(merged_set) == <span class="built_in">len</span>(t_set):</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure><p>有问题，因为<code>set（）</code>没法判断长度是否相等，比如aabb和ab最后会返回true</p><p>最简单高效的方法是引入collections包里的Counter方法，Counter方法主要统计 （）中每个字符的出现次数，所以只需要判断结果是否相等即可</p><p>还有就是<code>sorted（）</code>方法，可以将字符从a到z按顺序排列，也就是说如果排序好的s和t长度和字母都一样就说明是true</p><p>核心思路还是统计每个字符串里单词出现的次数，所以可以按照Counter的思路，设计1个dict储存s出现的字符和次数，key对应字符，value对应次数，然后再遍历t，然后找到出现的字符，将其对应的value-1，最后再判断一次dict里面的所有value是否为0，如果不是则返回false：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">isAnagram</span>(<span class="params">s: <span class="built_in">str</span>, t: <span class="built_in">str</span></span>) -&gt; <span class="built_in">bool</span>:</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(s) != <span class="built_in">len</span>(t):  <span class="comment"># 长度不同，直接返回 False</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    char_count = &#123;&#125;  <span class="comment"># 创建字典存储 s 中字符的出现次数</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 统计 s 中每个字符的频率</span></span><br><span class="line">    <span class="keyword">for</span> char <span class="keyword">in</span> s:</span><br><span class="line">        char_count[char] = char_count.get(char, <span class="number">0</span>) + <span class="number">1</span>  <span class="comment"># 若 char 不在字典中，则默认为 0，再 +1</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 遍历 t，减少对应字符的次数</span></span><br><span class="line">    <span class="keyword">for</span> char <span class="keyword">in</span> t:</span><br><span class="line">        <span class="keyword">if</span> char <span class="keyword">not</span> <span class="keyword">in</span> char_count <span class="keyword">or</span> char_count[char] == <span class="number">0</span>:  <span class="comment"># 如果 t 中的字符不在 s 中，或次数为 0</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">        char_count[char] -= <span class="number">1</span>  <span class="comment"># 减少对应字符的出现次数</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 检查字典中是否所有值都为 0（其实不检查也可以，因为长度相等 + 上面操作确保匹配）</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">all</span>(value == <span class="number">0</span> <span class="keyword">for</span> value <span class="keyword">in</span> char_count.values())</span><br></pre></td></tr></table></figure><h2 id="349-Intersection-of-Two-Arrays">349. Intersection of Two Arrays</h2><p>这里主要注意并集和交集的符号使用</p><p>两个集合set取并集<code>set(s1) | set(s2)</code>,取交集 <code>set(s1) &amp; set(s2)</code></p><p>还有注意python中的set和dict底层结构都是哈希表，最主要的区别是set没有值，dict有<code>key-value</code></p><h2 id="202-Happy-Number">202. Happy Number</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">isHappy</span>(<span class="params">self, n</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    :type n: int</span></span><br><span class="line"><span class="string">    :rtype: bool</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">if</span> n &lt;= <span class="number">9</span> <span class="keyword">and</span> n != <span class="number">1</span>: <span class="comment"># 不需要判断是否为1，循环里也能判断，如果是1的话平方还是自己，最后会返回True</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">    <span class="keyword">if</span> n == <span class="number">1</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 首先可以通过将给定的n转换为字符然后快速得到每个数字,这里用map()最快</span></span><br><span class="line">    nums = <span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">int</span>,<span class="built_in">str</span>(n)))</span><br><span class="line">    sum_ = <span class="number">0</span></span><br><span class="line">    seen = <span class="built_in">set</span>()</span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        sum_ = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> num <span class="keyword">in</span> nums:</span><br><span class="line">            sum_ += num ** <span class="number">2</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> sum_ == <span class="number">1</span>: </span><br><span class="line">            <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">        <span class="keyword">if</span> sum_ &lt; <span class="number">10</span> <span class="keyword">and</span> sum_ != <span class="number">1</span>: <span class="comment"># -----这里有问题。小于10也可以是快乐书，只是平方就是它本身，不需要加其他数，比如7</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">        <span class="keyword">if</span> sum_ <span class="keyword">in</span> seen:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">        seen.add(sum_)</span><br><span class="line">        nums = <span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">int</span>,<span class="built_in">str</span>(sum_)))</span><br></pre></td></tr></table></figure><p>又是错的，个位数也是快乐数！不能忽视！</p><p>核心思想就是将sum_放到set里，去重，同时再判断新的sum有没有在set里，有的话就是死循环，返回false，没有就继续，直到变成1或者死循环。</p><p>第二点就是会得到任意n的所有组成数字，使用<code>nums = list(map(int,str(n)))</code>，还有其他方法， 比较简单的就是将n转换为字符串，然后再通过遍历或者拆分得到每一个字符，再将字符转换为数字就可以了，上面这里就是先转换为str，然后通过map将每个str转换为int，最后再变成list。</p><p>还有一种方法就是，主要判断循环是否跳出的条件就是最后是1还是死循环，而死循环可以通过弗洛伊德算法判断是否有环，也就是定义两个快慢指针，快的要是和慢的能走到一起那就是死循环，如果快的先到1那就不是</p><h2 id="1-Two-Sum">1. Two Sum</h2><p>目前只用了暴力解法，并且可以进一步优化：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">j = idx + <span class="number">1</span></span><br><span class="line"><span class="keyword">while</span> j &lt;= <span class="built_in">len</span>(nums)-<span class="number">1</span>:</span><br></pre></td></tr></table></figure><p>这里可以合并成</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(idx+<span class="number">1</span>, <span class="built_in">len</span>(nums)):</span><br><span class="line">等于<span class="keyword">for</span>(j=i+<span class="number">1</span>;j&lt;nums.size()-<span class="number">1</span>;)</span><br></pre></td></tr></table></figure><p>第二种思路：用字典，字典是哈希表的一种，可以通过 <code>key = num_dict[value]</code>找到值对应的索引，也可以通过索引找值 <code>value = num_dict[key]</code>，所以我们只需要遍历nums，然后将target分别减去每一个数，如果得到的结果可以在字典中找到，那就返回字典中找到的值的索引和遍历到当前值的索引，如果没有找到那就往字典里添加当前的值和对应的当前的索引。</p><h2 id="454-4Sum-II">454. 4Sum II</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">fourSumCount</span>(<span class="params">self, nums1, nums2, nums3, nums4</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    :type nums1: List[int]</span></span><br><span class="line"><span class="string">    :type nums2: List[int]</span></span><br><span class="line"><span class="string">    :type nums3: List[int]</span></span><br><span class="line"><span class="string">    :type nums4: List[int]</span></span><br><span class="line"><span class="string">    :rtype: int</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    sum_12 = defaultdict(<span class="built_in">list</span>)</span><br><span class="line">    sum_34 = defaultdict(<span class="built_in">list</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums1)):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums2)):</span><br><span class="line">            num = nums1[i] + nums2[j]</span><br><span class="line">            sum_12[num] = [i, j] <span class="comment"># ++而不是赋值索引</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> k <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums1)):</span><br><span class="line">        <span class="keyword">for</span> l <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums2)):</span><br><span class="line">            num = nums3[k] + nums4[l]</span><br><span class="line">            sum_34[num] = [k, l] <span class="comment"># ++而不是赋值索引</span></span><br><span class="line"></span><br><span class="line">    counter = <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> sum_12:</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> sum_34:</span><br><span class="line">            <span class="keyword">if</span> i + j == <span class="number">0</span>:</span><br><span class="line">                counter += <span class="number">1</span> <span class="comment">#需要计算次数后相乘</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> counter</span><br></pre></td></tr></table></figure><p>这题的思路就是拆分为两个两个数之和。最后是n^2的复杂度，比暴力n^4好</p><p>但是上面的代码有几个问题，首先是我们不需要记住具体的索引是多少，只需要统计符合条件的结果有几个。因此我们只需要定义：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sum_12 = defaultdict(<span class="built_in">int</span>)</span><br><span class="line">sum_34 = defaultdict(<span class="built_in">int</span>)</span><br></pre></td></tr></table></figure><p>即可，计算出两个数之和得到答案的次数，</p><p><code>sum_12[num] = 3</code>，即 <code>A[i] + B[j] = num</code> 的情况出现了 3 次。</p><p><code>sum_34[-num] = 4</code>，即 <code>C[k] + D[l] = -num</code> 的情况出现了 4 次。</p><p>对于这 3 种 <code>(A[i], B[j]) </code>组合，每种都可以搭配 4 种 <code>(C[k], D[l]) </code>组合：总共的匹配情况=3×4=12</p><p>所以我们在最后通过乘法来算出所有情况，而不是上面的简单加1，当然最后可以优化成a+(-a)=0，从而减少一次for循环</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> num <span class="keyword">in</span> sum_12:</span><br><span class="line">        <span class="keyword">if</span> -num <span class="keyword">in</span> sum_34:  <span class="comment"># 查找是否存在 -(A+B) == (C+D)</span></span><br><span class="line">            counter += sum_12[num] * sum_34[-num]  <span class="comment"># 计算总匹配次数</span></span><br></pre></td></tr></table></figure><h2 id="383-Ransom-Note">383. Ransom Note</h2><p>这题做对了，但是还可以再优化一下代码，核心思想就是创建一个字典用于存放magazine的所有字符出现的次数，然后再遍历ransomNote，将匹配到的字符对应的次数-1，如果出现magazine没有的字符则返回false，并且如果magazine字典里的次数被用完了也就是&lt;=0的时候也返回false。</p><p>优化：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> j <span class="keyword">in</span> ransomNote:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> j <span class="keyword">not</span> <span class="keyword">in</span> m_dict: </span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> j <span class="keyword">in</span> m_dict:</span><br><span class="line">        <span class="keyword">if</span> m_dict[j] &lt;= <span class="number">0</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            m_dict[j] -= <span class="number">1</span></span><br></pre></td></tr></table></figure><p><code>if j not in m_dict </code>其实是多余的，因为 <code>defaultdict(int)</code> 默认 0，所以 <code>m_dict[j] </code>不存在时 <code>m_dict[j] == 0</code>，可以直接判断 <code>if m_dict[j] == 0</code>。</p><h2 id="15-3Sum">15. 3Sum</h2><p>这题非常经典，没做出来</p><h3 id="解法1：排序-哈希表">解法1：排序+哈希表</h3><p>首先将nums排序，这样可以使得输出的时候的元组的顺序固定，防止出现顺序不一样元素一样的结果</p><p>然后固定第一个数nums[i]，遍历第二个数nums[j]，并通过<code>c = 0-(nums[i]+nums[j])</code>来匹配第三个数</p><p>这个时候要注意，由于要求结果不能重复，如果 c 需要在整个 nums 里查找，那 nums[k] 可能比 nums[i] 还小，这样就会出现重复或者无效的解。注意题目要求 <code>i != j, i != k, and j != k</code>。</p><p>所以使用<code>seen = set()</code>来保存出现的nums[j]，而nums[j]可以表示为除了nums[i]的其他元素，我们可以将<code>seen.add(nums[j])</code>放在for j的里面，也可以放在for i的里面，两者区别主要是前者动态比较c与两个数的和，确保后面得到的结果不会重复，后者会这样所以需要额外增加去重步骤。</p><p>所以我们先判断c是不是在seen里，如果是，就说明nums里面包含了满足条件的三个数，将其添加到结果set res_set里，注意res_set不能直接添加<code>[nums[i] ,nums[j], c]</code>, 其会报list不是可哈希的错误。所以我们要添加元组<code>（nums[i] ,nums[j], c）</code>到res_set里，在最后将元组通过map转换成list。</p><p>c如果不在seen里那就把nums[j]添加进seen，这样循环到最后会把除了num[i]的其他元素都添加进seen，并且能够按顺序判断得到多个元组。</p><p>注意seen 必须使用 set()，不能使用 list()，因为set 查找补数 (complement in seen) 是 O(1)，而 list 查找是 O(N)，会导致算法变慢</p><h3 id="解法2：排序-双指针">解法2：排序+双指针</h3><p>首先也是排序，这样就能通过双指针来做了，主要是固定第一个数字nums[i]，然后将剩下的list里的最左边和最右边分别用相加，如果<code>nums[i] + nums[left] + nums[right] &gt; 0</code> 就说明 此时三数之和大了，因为数组是排序后了，所以right下标就应该向左移动，这样才能让三数之和小一些。</p><p>如果 <code>nums[i] + nums[left] + nums[right] &lt; 0</code> 说明 此时 三数之和小了，left 就向右移动，才能让三数之和大一些，直到left与right相遇为止。</p><h2 id="18-4Sum">18. 4Sum</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">fourSum</span>(<span class="params">self, nums, target</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type nums: List[int]</span></span><br><span class="line"><span class="string">        :type target: int</span></span><br><span class="line"><span class="string">        :rtype: List[List[int]]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        nums.sort() <span class="comment"># [-2, -1, 0, 0, 1, 2]</span></span><br><span class="line">        res_set = <span class="built_in">set</span>()</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)-<span class="number">4</span>):</span><br><span class="line">            <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(i+<span class="number">1</span>, <span class="built_in">len</span>(nums)-<span class="number">3</span>):</span><br><span class="line">                c = nums[i] + nums[j]</span><br><span class="line"></span><br><span class="line">                left = j + <span class="number">1</span></span><br><span class="line">                right = <span class="built_in">len</span>(nums) - <span class="number">1</span></span><br><span class="line"></span><br><span class="line">                <span class="keyword">while</span> left &lt; right:</span><br><span class="line">                    <span class="keyword">if</span> c + nums[left] + nums[right] &gt; target:</span><br><span class="line">                        right -= <span class="number">1</span></span><br><span class="line">                    <span class="keyword">elif</span> c + nums[left] + nums[right] &lt; target:</span><br><span class="line">                        left += <span class="number">1</span></span><br><span class="line">                    <span class="keyword">if</span> c + nums[left] + nums[right] == target:</span><br><span class="line">                        res_set.add((nums[i],nums[j],nums[left],nums[right]))</span><br><span class="line">                        left += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">list</span>,res_set))</span><br></pre></td></tr></table></figure><p>上面是我根据提示写出来的代码。提示是和3Sum一样用双指针法，所以我的思路是排序后先将两个数相加，然后再将剩下的数用双指针遍历。</p><p>其中有几个错误需要注意。</p><p>for循环范围错误：<code>for i in range(len(nums)-4):</code>应该是<code>len(nums)-3</code>， <code>for j in range(i+1, len(nums)-3):</code>应该是<code>len(nums)-2</code>，这是因为<code>range(len(nums) - 4)</code> 意味着 i 只能遍历到 <code>len(nums) - 5</code>，但我们需要 i 取到 <code>len(nums) - 4</code>。同理。</p><p>i 和 j没有考虑重复的元素，比如<code>nums[i] ==nums[i-1]</code>的话可能出现重复解,因此在两个for之后都需要判断当前的nums[i]是否和nums[i-1]相等，当然需要有效的i，即i&gt;0，j也需要<code>j &gt; i+1 and nums[j] == nums[j-1]</code>，这个时候需要跳过当前相同的值，比如：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">nums = [-<span class="number">2</span>, -<span class="number">2</span>, -<span class="number">1</span>, <span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">2</span>, <span class="number">2</span>]</span><br><span class="line">i = <span class="number">0</span>, nums[i] = -<span class="number">2</span></span><br><span class="line">i = <span class="number">1</span>, nums[i] = -<span class="number">2</span> <span class="comment"># 重复了，需要跳过</span></span><br><span class="line">如果不加 <span class="keyword">if</span> i &gt; <span class="number">0</span> <span class="keyword">and</span> nums[i] == nums[i - <span class="number">1</span>]: <span class="keyword">continue</span>，那么：</span><br><span class="line">i = <span class="number">0</span>, nums[i] = -<span class="number">2</span> 会找到所有符合的四元组。</span><br><span class="line">i = <span class="number">1</span>, nums[i] = -<span class="number">2</span> 会重复计算完全一样的四元组。</span><br><span class="line">最终会出现重复解：[-<span class="number">2</span>, -<span class="number">1</span>, <span class="number">0</span>, <span class="number">1</span>] ，[-<span class="number">2</span>, -<span class="number">1</span>, <span class="number">0</span>, <span class="number">1</span>] <span class="comment"># 计算两次</span></span><br></pre></td></tr></table></figure><p>j也同理。</p><p><code>if c + nums[left] + nums[right] == target:</code>判断错误：如果在前面left或者right变化后，<code>nums[left] + nums[right] </code>可能已经越界！所以应该是elif或者else而不是if。</p><p>最后还有left和right重复的情况没考虑：如果<code>nums[left] == nums[left + 1]</code>或者<code>nums[right] == nums[right - 1]</code>的时候，需要跳过重复解，为什么用while不用if是因为left或者right旁边可能有很多个相同的值，因为已经排序过了。</p><p>最后注意双指针的while条件是<code>left += 1</code>和<code>right -= 1</code>同时作用，确保 left 指向新的不同值，right 也指向新的不同值，防止重复匹配相同解。</p><p>而对于双指针法的应用，到底是都收缩还是只变一个需要具体问题具体分析。如3，4数之和就需要都变，而滑动窗口求最大子序列就只需要变left等。</p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/03/29/leetcode/leetcode-3/</id>
    <link href="https://qyp9909.github.io/2025/03/29/leetcode/leetcode-3/"/>
    <published>2025-03-29T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="242-Valid-Anagram">242. Valid Anagram</h2>
<p>这题有好几种解法，首先我自己想的</p>
<figure class="highlight py"><table><tr><td class="gutter"><pre><]]>
    </summary>
    <title>LeetCode 3</title>
    <updated>2025-06-25T09:21:44.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="203-Remove-Linked-List-Elements">203. Remove Linked List Elements</h2><p>注意：链表的定义很简单，只要创建一个ListNode类并实例化就算完成了，其中每个实例代表一个节点，因此每个实例都得有value和next，分别表示该节点的值和指向下一节点的指针：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ListNode</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, val=<span class="number">0</span>, head=<span class="literal">None</span></span>):</span><br><span class="line">            <span class="variable language_">self</span>.val = val</span><br><span class="line">                <span class="variable language_">self</span>.<span class="built_in">next</span> =  <span class="built_in">next</span></span><br></pre></td></tr></table></figure><p>而遍历链表也不同于for循环，其主要通过头节点的指针不断寻找下一个节点，也就是current = current.next</p><p>例如创建一个链表并遍历：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">head = ListNode(<span class="number">1</span>)</span><br><span class="line">head.<span class="built_in">next</span> = ListNode(<span class="number">2</span>)</span><br><span class="line">head.<span class="built_in">next</span>.<span class="built_in">next</span> = ListNode(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">current = head <span class="comment">#从头开始遍历</span></span><br><span class="line"><span class="keyword">while</span> current.<span class="built_in">next</span>:</span><br><span class="line">    <span class="built_in">print</span>(current.val)</span><br><span class="line">        current = current.<span class="built_in">next</span> <span class="comment"># 指针指向现在的节点的下一个节点</span></span><br></pre></td></tr></table></figure><p>知道这些后，删除节点这一题就好解决了：</p><p>逐步解释代码：</p><p>首先使用dummy head哨兵节点来指向head节点，防止要删除的节点是头节点</p><p><code>dummy_head = ListNode(0, head) </code><br>然后将哨兵节点和链表的头节点连接起来，然后再将指针指向现在的位置：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dummy_head.<span class="built_in">next</span> = head</span><br><span class="line">current = dummy_head</span><br></pre></td></tr></table></figure><p>然后遍历链表</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> current.<span class="built_in">next</span> : <span class="comment"># 如果next=none就说明到达链表尾部，停止循环</span></span><br><span class="line">    <span class="keyword">if</span> current.<span class="built_in">next</span>.val == val:</span><br><span class="line">            current.<span class="built_in">next</span> = current.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line">     <span class="keyword">else</span>:</span><br><span class="line">         current = current.<span class="built_in">next</span> <span class="comment"># 指针指向下一个节点</span></span><br></pre></td></tr></table></figure><p>最后再返回链表的头节点就完事了，因为你可以通过头节点这个实例来逐步得到之后的节点</p><h2 id="707-Design-Linked-List">707. Design Linked List</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ListNode</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, val=<span class="number">0</span>, <span class="built_in">next</span>=<span class="literal">None</span>, index=<span class="number">0</span></span>):</span><br><span class="line">        <span class="variable language_">self</span>.val = val</span><br><span class="line">        <span class="variable language_">self</span>.<span class="built_in">next</span> = <span class="built_in">next</span></span><br><span class="line">        <span class="variable language_">self</span>.index = index <span class="comment"># 没有必要index，直接通过for循环的次数和size就能确认位置</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyLinkedList</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="variable language_">self</span>.head = ListNode() <span class="comment"># 直接self.head = None初始化即可，一开始是空而不需要其他东西</span></span><br><span class="line">        <span class="variable language_">self</span>.dummy_head = ListNode(<span class="number">0</span>, <span class="variable language_">self</span>.head, -<span class="number">1</span>) <span class="comment"># 完全不需要dummy，因为按照索引删除的时候如果删除了头节点也只需要将头节点的next指向下一个即可</span></span><br><span class="line">        <span class="variable language_">self</span>.dummy_head.<span class="built_in">next</span> = <span class="variable language_">self</span>.head</span><br><span class="line">        <span class="variable language_">self</span>.size = <span class="number">0</span> <span class="comment"># 使用size来时刻记录链表长度</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">get</span>(<span class="params">self, index</span>): <span class="comment"># 需要考虑index的合法性，主要体现在小于0和大于等于size以及size为0的情况</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type index: int</span></span><br><span class="line"><span class="string">        :rtype: int</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        current = <span class="variable language_">self</span>.dummy_head.<span class="built_in">next</span> <span class="comment"># 直接让指针等于头节点即可</span></span><br><span class="line">        <span class="keyword">while</span> current.<span class="built_in">next</span>: <span class="comment"># 可以通过for _ in range(index)：来找到index位置的节点，并返回val</span></span><br><span class="line">            <span class="keyword">if</span> current.index == index:</span><br><span class="line">                <span class="keyword">return</span> current.val</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                current = current.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> -<span class="number">1</span> </span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">addAtHead</span>(<span class="params">self, val</span>): <span class="comment"># 在头部添加新节点同样很简单，只需要创建一个新节点对象，然后将对象的next指向头节点就行了，注意size++</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type val: int</span></span><br><span class="line"><span class="string">        :rtype: None</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        head = NodeList(val, <span class="variable language_">self</span>.head, <span class="number">0</span>)</span><br><span class="line">        head = <span class="variable language_">self</span>.dummy_head.<span class="built_in">next</span></span><br><span class="line">        head.<span class="built_in">next</span> = <span class="variable language_">self</span>.head</span><br><span class="line">        current = head</span><br><span class="line">        i = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> current.<span class="built_in">next</span>:</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line">            i += <span class="number">1</span></span><br><span class="line">            current.index = i</span><br><span class="line"></span><br><span class="line">        <span class="variable language_">self</span>.size += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">addAtTail</span>(<span class="params">self, val</span>): <span class="comment"># 在尾部添加则是通过size找到最后一个节点，然后创建一个新节点对象，让最后一个节点的next指向它，size++</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;                           </span></span><br><span class="line"><span class="string">        :type val: int   #还得注意如果size是0，那就是添加头节点</span></span><br><span class="line"><span class="string">        :rtype: None</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        current = <span class="variable language_">self</span>.dummy_head.<span class="built_in">next</span></span><br><span class="line">        i = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> current.<span class="built_in">next</span>:</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line">            i += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        tail = NodeList(val, <span class="literal">None</span>, i)</span><br><span class="line">        current.<span class="built_in">next</span> = tail</span><br><span class="line"></span><br><span class="line">        <span class="variable language_">self</span>.size += <span class="number">1</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>上面的代码时直接写的代码，错误很多，关键思路已给出，接下来是其他没写的函数：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">addAtIndex</span>(<span class="params">self, index, val</span>):</span><br><span class="line">    <span class="comment">#首先还是判断index合法，如果不合法则return ，表示什么都不返回，注意这个时候可以等于size，表示在size这个位置新创建一个节点</span></span><br><span class="line">        <span class="comment"># 如果index为0，那就是添加头节点</span></span><br><span class="line">        <span class="comment"># 否则则和查找类似，遍历到index的前一个位置，然后创建新节点，将将它的next指向后一个节点，然后再前一个节点的next指向它，size++</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">deleteAtIndex</span>(<span class="params">self, index</span>):</span><br><span class="line">    <span class="comment">#首先判断index是否合法</span></span><br><span class="line">        <span class="comment"># 如果index==0，那就是删除头节点，直接将头节点next指向的节点赋值给头节点就可以了self.head = self.head.next</span></span><br><span class="line">        <span class="comment"># 否则那就遍历到index的前一个节点，然后将后一个节点的next直接指向前一个节点的next就行了current.next = current.next.next。size--</span></span><br></pre></td></tr></table></figure><h2 id="206-Reverse-Linked-List">206. Reverse Linked List</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">reverseList</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        current = head</span><br><span class="line">        <span class="keyword">if</span> current == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="comment"># 这里不能空，而是None</span></span><br><span class="line">        <span class="keyword">elif</span> current.<span class="built_in">next</span> == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">return</span> current</span><br><span class="line"></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            dummy_head = ListNode(<span class="number">0</span>, current)</span><br><span class="line">            <span class="keyword">while</span> current.<span class="built_in">next</span>:</span><br><span class="line">                current = current.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> current != head:</span><br><span class="line">                current = current.<span class="built_in">next</span> <span class="comment"># 这里并不会反转链表</span></span><br><span class="line">            <span class="keyword">return</span> current</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>还是存在大量错误，问题的关键就是反转链表时需要使用一个中间变量tmp来储存<code>current.next</code>指向的节点，并且同时定义另一个指针prev用于新的表头。</p><p>双指针法，prev和current两个指针，并通过tmp来暂存中间值防止值丢失</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">tmp = current.<span class="built_in">next</span></span><br><span class="line">current.<span class="built_in">next</span> = prev </span><br><span class="line">prev = current <span class="comment"># 将current本身作为current.next的prev指向的节点，而current.next则被上一轮的prev覆盖</span></span><br><span class="line">current = tmp <span class="comment"># current.next的值赋给current，实现反转</span></span><br></pre></td></tr></table></figure><h2 id="24-Swap-Nodes-in-Pairs">24. Swap Nodes in Pairs</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">swapPairs</span>(<span class="params">self, head</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        :type head: Optional[ListNode]</span></span><br><span class="line"><span class="string">        :rtype: Optional[ListNode]</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        current = head</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> current == <span class="literal">None</span>: <span class="comment"># 首先不需要这样，直接判断head是否为none和head.next是否为none就可以知道链表节点数是否为2个以下</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">        <span class="keyword">if</span>  current.<span class="built_in">next</span> == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">return</span> current</span><br><span class="line">        <span class="keyword">if</span> current.<span class="built_in">next</span>.<span class="built_in">next</span> ==<span class="literal">None</span>:</span><br><span class="line">            tmp = current</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line">            current.<span class="built_in">next</span> = tmp</span><br><span class="line">            <span class="keyword">return</span> current</span><br><span class="line">        <span class="keyword">while</span> current: <span class="comment"># current 和 current.next都不为none才继续循环，保证有两个节点可以交换</span></span><br><span class="line">            a = current.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line">            prev = current</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line">            current.<span class="built_in">next</span> = prev</span><br><span class="line">            current.<span class="built_in">next</span>.<span class="built_in">next</span> = a</span><br><span class="line">            current = current.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> current</span><br></pre></td></tr></table></figure><p>还是有大量错误，节点交换的指针和顺序需要特别注意！！</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"> <span class="comment">#关键是使用dummy head来指向头节点，这样方便非常多</span></span><br><span class="line">        dummy_head = ListNode(<span class="number">0</span>, head)</span><br><span class="line">        current = head <span class="comment"># 指针指向头节点</span></span><br><span class="line">        prev = dummy_head <span class="comment"># 头节点的prev指针指向其前一个节点，也就是dummy head。</span></span><br><span class="line">        <span class="comment"># 这两个指针是关键，current用于指向接下来要操作的pair，prev指向要操作的pair的前一个节点位置</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> current <span class="keyword">and</span> current.<span class="built_in">next</span>:</span><br><span class="line">        second = current.<span class="built_in">next</span> <span class="comment">#将第二个节点储存</span></span><br><span class="line">        next_pair = current.<span class="built_in">next</span>.<span class="built_in">next</span> <span class="comment">#将第三个节点，也就是下一个pair的开头节点储存</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 开始交换，首先将第二个节点的next指向要交换的第一个节点</span></span><br><span class="line">        second.<span class="built_in">next</span> = current</span><br><span class="line">        <span class="comment"># 然后将第一个节点的next指向第三个节点</span></span><br><span class="line">      current.<span class="built_in">next</span> = next_pair</span><br><span class="line">        <span class="comment"># 最后将头节点前面的prev节点的next指向交换完成的头节点second</span></span><br><span class="line">        prev.<span class="built_in">next</span> = second</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 上面的顺序应该是可以随便的，current和second由于交换位置所以current所指向的节点是第二个节点，second是第一个节点</span></span><br><span class="line">        <span class="comment"># 最后就是处理两个指针prev和current</span></span><br><span class="line">        prev = current <span class="comment"># 先将current所指向的节点，也就是第二个节点储存到prev，这样prev对应的节点就是next_pair的前一个节点</span></span><br><span class="line">        current = next_pair <span class="comment"># 将current指向下一组开始</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> dummy.<span class="built_in">next</span> <span class="comment">#最后再返回头节点</span></span><br></pre></td></tr></table></figure><h2 id="19-Remove-Nth-Node-From-End-of-List">19. Remove Nth Node From End of List</h2><p>算是自己做对的一题，关键思路是找到链表长度size，再通过size-n来将倒方向变为正方向</p><p>其中n取值不同分不同情况讨论，如果n取到头节点了就是head=head.next，其他情况就是遍历然后删除</p><p>并且head为none或者只有一个节点的时候情况也不同</p><p>复杂度为O(2n)</p><p>另一种解法：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">removeNthFromEnd</span>(<span class="params">head, n</span>):</span><br><span class="line">    dummy = ListNode(<span class="number">0</span>, head)  <span class="comment"># 哨兵节点，防止删除头节点时出错</span></span><br><span class="line">    first = second = dummy</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 让 first 指针先前进 `n+1` 步，使 second 停在待删除节点的前一个</span></span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(n + <span class="number">1</span>):</span><br><span class="line">        first = first.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 让 first 和 second 同步前进，直到 first 到达 `None`</span></span><br><span class="line">    <span class="keyword">while</span> first:</span><br><span class="line">        first = first.<span class="built_in">next</span></span><br><span class="line">        second = second.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 删除目标节点</span></span><br><span class="line">    second.<span class="built_in">next</span> = second.<span class="built_in">next</span>.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> dummy.<span class="built_in">next</span>  <span class="comment"># 返回新的头节点</span></span><br></pre></td></tr></table></figure><h2 id="160-Intersection-of-Two-Linked-Lists">160. Intersection of Two Linked Lists</h2><p>这题非常巧妙，主要有两种解法</p><h3 id="1-双指针法：">1.双指针法：</h3><p>思路：让两个指针 走完相同的路程，确保它们在相交点相遇，或者同时到达 None</p><p><code>headA: A1 → A2 → A3 → C1 → C2 → C3</code></p><p><code>headB: B1 → B2 → C1 → C2 → C3</code></p><p>A 链表的长度 = a + c，B 链表的长度 = b + c，c 是公共部分（相交部分）的长度</p><p>指针pA 先遍历 headA（长度 a + c），然后跳到 headB 继续走（长度 b）</p><p>指针pB 先遍历 headB（长度 b + c），然后跳到 headA 继续走（长度 a）</p><p>它们最终都走了 a + c + b 的距离，所以一定会相遇在交点 C1。</p><p>pA 走的路径： <code>A1 → A2 → A3 → C1 → C2 → C3 → B1 → B2 → C1</code></p><p>pB 走的路径： <code>B1 → B2 → C1 → C2 → C3 → A1 → A2 → A3 → C1</code></p><p>它们都在 C1 处相遇，所以 C1 就是交点。如果没有相交的情况下，pA 和 pB 最终都会变成 None，返回 None。</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#核心代码</span></span><br><span class="line">pA = headA</span><br><span class="line">pB = headB</span><br><span class="line"><span class="keyword">while</span> pA != pB:</span><br><span class="line">    <span class="keyword">if</span> pA <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            pA = headB</span><br><span class="line">     <span class="keyword">else</span>:</span><br><span class="line">         pA = pA.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> pB <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">        pB = headA</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line">     pB = pB.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> pA</span><br></pre></td></tr></table></figure><h3 id="2-相同长度法">2.相同长度法</h3><p>核心思想：两个链表可能长度不相等，因此找出长的链表，将指针指向和短的链表的长度相同的位置开始一起遍历，遍历到相同的val就返回，否则就返回None</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">get_length</span>(<span class="params">head</span>):</span><br><span class="line">        length = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> head:</span><br><span class="line">            length += <span class="number">1</span></span><br><span class="line">            head = head.<span class="built_in">next</span></span><br><span class="line">        <span class="keyword">return</span> length</span><br><span class="line"></span><br><span class="line">    lenA, lenB = get_length(headA), get_length(headB)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> lenA &gt; lenB:</span><br><span class="line">        headA = headA.<span class="built_in">next</span></span><br><span class="line">        lenA -= <span class="number">1</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> lenB &gt; lenA:</span><br><span class="line">        headB = headB.<span class="built_in">next</span></span><br><span class="line">        lenB -= <span class="number">1</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> headA != headB:</span><br><span class="line">        headA = headA.<span class="built_in">next</span></span><br><span class="line">        headB = headB.<span class="built_in">next</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> headA</span><br></pre></td></tr></table></figure><h2 id="141-Linked-List-Cycle">141. Linked List Cycle</h2><p>判断是否有环，需要记住floyd算法</p><p>也就是快慢指针法，设fast指针每次走两步，slow指针每次走一步，如果快的指针还能和慢的相遇那就说明肯定有环</p><p>注意判断条件：</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> fast <span class="keyword">and</span> fast.<span class="built_in">next</span>: <span class="comment"># 不是while fast.next.next或者while fast.next</span></span><br></pre></td></tr></table></figure><p>这是因为fast 不会是 None，避免 <code>fast.next</code> 访问错误。<code>fast.next</code> 也不会是 None，避免 <code>fast.next.next</code> 访问错误。</p><h2 id="142-Linked-List-Cycle-II">142. Linked List Cycle II</h2><p>本题和141一样先通过floyd算法判断是否有环，有环的话再进一步寻找环的入口！</p><p>如果有环，此处需要数学推导</p><p>链表头到环入口的距离 a，快慢指针相遇点到环起点的距离 b，环的长度 c：</p><p>快指针走的步数 = a + b + k*c （k 表示完整绕环 k 圈）</p><p>慢指针走的步数 = a + b</p><p>慢指针先从head开始走到环口距离是a，再从环口走到相遇点的距离是b，同理，快指针先走了a后，先进入环内绕圈，绕了k圈后再到相遇点，也就是<code>a+ k*c + b </code>（此时 slow 在环中某个点，而 fast 走过的总路程比 slow 多整 k 圈。）</p><p>快指针走的步数是慢指针的两倍：<code>2(a+b)=a+k∗c+b</code>，也就是<code>a=k∗c−b</code></p><p>等式左右相等说明：从 head 走 a 步，与 slow 从 b 位置开始继续走 都会到环的起点，所以它们一定会相遇！</p><p>也就是说a = c-b，我们使k等于1也能满足等式成立，也就是说从 head 走 a 步=slow走b-c步</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> fast == slow:</span><br><span class="line">    entry = head</span><br><span class="line">    <span class="keyword">while</span> entry != slow:</span><br><span class="line">        entry = entry.<span class="built_in">next</span></span><br><span class="line">        slow = slow.<span class="built_in">next</span></span><br><span class="line">    <span class="keyword">return</span> entry</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://qyp9909.github.io/2025/03/19/leetcode/leetcode-2/</id>
    <link href="https://qyp9909.github.io/2025/03/19/leetcode/leetcode-2/"/>
    <published>2025-03-19T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="203-Remove-Linked-List-Elements">203. Remove Linked List Elements</h2>
<p>注意：链表的定义很简单，只要创建一个ListNode类并实例化就算完成了，其中每个实例代表一个节点，因此每个实例都得]]>
    </summary>
    <title>LeetCode 2</title>
    <updated>2025-06-25T11:03:37.402Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="programming" scheme="https://qyp9909.github.io/categories/programming/"/>
    <category term="leetcode" scheme="https://qyp9909.github.io/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="704-Binary-Search">704. Binary Search</h2><p>二分查找的注意点</p><p>思路：每次取list的最大最小下标，然后找到中间下标与其所对应的值，将其与目标比较大小，从而进一步缩短目标所在区间，时间复杂度logn</p><p>临界值的把握最重要</p><p>首先确定while循环的条件所在区间 如果我们假设了<code>left &lt;= right</code>，那么就存在<code>left == right</code>这种情况，也就是target 可能位于<code>left==right</code>之间。 这个时候为了避免死循环，那么就需要在比完大小后<code>right = middle-1</code> 或者 <code>left = middle+1</code>，而不是直接<code>left = middle</code> 或<code>right = middle</code> 假设 <code>nums = [2, 5]，target = 5： left = 0, right = 1 middle = (0 + 1) / 2 = 0</code>（因为 / 是浮点数除法，应该用 // 取整） <code>nums[middle] = nums[0] = 2 &lt; target</code> 执行 <code>left = middle</code>，但 <code>middle == left</code>，所以 left 没有变化 死循环（left 不增加，<code>while left &lt; right</code> 不能终止）</p><p>如果假设了while循环条件是left &lt; right的话，那就不需要考虑<code>left==right</code>了，这样就有<code>right = middle</code>而不是<code>right = middle-1</code></p><h2 id="27-Remove-Element">27. Remove Element</h2><h3 id="第一种：暴力求解">第一种：暴力求解</h3><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">i = <span class="number">0</span> </span><br><span class="line"><span class="keyword">while</span> i &lt; <span class="built_in">len</span>(nums): </span><br><span class="line">    <span class="keyword">if</span> nums[i] == val: </span><br><span class="line">        nums.pop(i) <span class="comment"># 删除元素 </span></span><br><span class="line">    <span class="keyword">else</span>: i += <span class="number">1</span> <span class="comment"># 只有不删除时才递增索引 </span></span><br><span class="line"><span class="keyword">return</span> <span class="built_in">len</span>(nums)</span><br></pre></td></tr></table></figure><p>暴力求解，使用<code>list.pop()</code>删除元素，复杂度n^2，最简单，从头遍历，遍历到要删除的元素就删除，不是要删除的元素的话下标+1，最后返回删除完毕后的list</p><h3 id="第二种：双指针方法">第二种：双指针方法</h3><p>使用一个下标变量k从0开始记录新的数组（慢指针），使用另一个下标变量i记录遍历原数组（快指针）</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">k = <span class="number">0</span> </span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(nums)): </span><br><span class="line"><span class="keyword">if</span> nums[i] != val: </span><br><span class="line">nums[k]=nums[i] </span><br><span class="line">k=k+<span class="number">1</span> </span><br><span class="line"><span class="keyword">return</span> k </span><br></pre></td></tr></table></figure><p>只保留不等于val的元素，将其作为新的数组，由于第一个元素下标0的时候就进行判断，所以可以用同一个数组存放新的数组 最后得到的就是覆盖之后的</p><p>为什么 <code>nums[k] = nums[i]</code> 不会导致新数组 k 和原数组 i 混在一起？ 换句话说，为什么不会影响 nums[i] 后面的元素？</p><p><code>nums[k] = nums[i]</code> 只会覆盖前面的部分，不会影响后面的部分 i 始终遍历原数组，它是从 0 → n-1 线性递增的。 k 仅递增存储有效元素的位置，它的值不会超过 i，它最多和 i 相等。 因此，每次 <code>nums[k] = nums[i]</code> 只会覆盖数组的前面部分，而 i 后面的元素仍然是原来的数据，不会影响后续遍历。</p><p>错误理解 正确理解 <code>nums[k] = nums[i]</code> 会破坏 nums[i] 后面的数据 <code>nums[k] = nums[i]</code> 只影响前面，不影响后面的遍历 nums[i] 被修改后，nums[i+1] 可能出错 nums[i] 仍然按顺序遍历，不会被提前覆盖 需要两层 for 处理 只需 O(n) 单层遍历</p><h2 id="977-Squares-of-a-Sorted-Array">977. Squares of a Sorted Array</h2><p>双指针处理平方和+升序排序问题</p><p>问题关键： 原序列不管是正数还是负数，都已经是升序排序好的，如[-4,-1,0,3,10] 因此在将所有数都平方后最大的数不是在最左边就是在最右边，中间的数一定不是最大的，所以我们只需要两两比较最左和最右边的数，得到的结果储存在新数组res里就完事，注意最后肯定会出现i=j的情况，此时有<code>nums[i]** 2 == nums[j]** 2 ==同一个元素</code>，因此将其直接存入res就行 在比大小完后需要将i和j分别指向下一个目标，同时k也应该往前更新位置</p><p>如果题目没说不能用新的list，那就可以创建新的list来储存排序的结果列表 <code>res = [0]* len(nums)</code> 注意 res=[]不行，空list不能调用索引k，所以报错 然后我们创建第一个指针指向新列表res的末尾 注意python 中用 <code>i, j = 0, len(nums)-1 while i &lt;= j:</code> 来代替c++里的<code>for(i=0, j=nums.size()-1, j&lt;=j; )</code> 于是第二个指针指的是i j 两个分别指向nums的首尾元素</p><p>当然也可以先全部平方然后暴力排序秒杀O(n log n) nums.sort() sorted(nums)</p><h2 id="209-Minimum-Size-Subarray-Sum">209. Minimum Size Subarray Sum</h2><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">sum_, k, i = <span class="number">0</span>, <span class="number">0</span>, <span class="built_in">len</span>(nums)-<span class="number">1</span></span><br><span class="line"></span><br><span class="line">    nums.sort()</span><br><span class="line">    <span class="keyword">while</span> i &gt;= <span class="number">0</span>:</span><br><span class="line">        <span class="keyword">if</span> nums[i] &gt; target:</span><br><span class="line">            <span class="keyword">return</span> <span class="number">1</span></span><br><span class="line">        <span class="keyword">elif</span> sum_ &gt;= target:</span><br><span class="line">            <span class="keyword">return</span> k</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            sum_ += nums[i]</span><br><span class="line">            k += <span class="number">1</span></span><br><span class="line">        i -= <span class="number">1</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>这个方法不行，虽然通过了run，但是在submit的时候出错了</p><p>偶尔 能得到正确答案，但由于它 破坏了连续性要求，在某些情况下会 返回错误结果。最优解依然是 滑动窗口</p><p>滑动窗口也是双指针的一种，主要形容两个指针中间的序列</p><p>关键思想：首先想到暴力求解，也就是通过两个for循环，一个位于开头一个位于当前位置，<code>i=0 j=i</code>,这样可以算出所有的子序列并且找出最小的，复杂度最大为n^2</p><p>然后再想到通过一个for循环来解决问题，双指针！注意这个for循环只能指向子序列的终止位置，这样才能够”先动带后动“找到子序列，如果指向起始位置的话那么第二个指针还是得一遍一遍的遍历后面所有子序列，这样和暴力求解一样了。</p><p>然后先动带后动，第一个指针走前面，并且计算到跟前位置元素的和，直到和大于target才进行下一步操作，如果遍历结束都没有满足那就直接返回0.</p><p>大于target后就说明当前序列为止有子序列满足条件，但是不知道是否最短，所以我们先需要确定当前位置的子序列长度<code>right-left+1</code>，并且让其始终处于最短，然后通过while逐步从左减小sum的值并同时判断最小子序列是否满足大于target来缩小子序列长度。最后再挪动left指针，让其往前走一步。</p><p>感觉动手画一下会很清晰</p><h2 id="59-Spiral-Matrix-II">59. Spiral Matrix II</h2><p>问题的关键就是找到要旋转多少圈，每一圈的初始点在哪，每一边的开闭区间是否一致</p><p>其中可以观察到要旋转的圈数为n//2，这是因为每次往里面旋转横竖都得少两个元素，也就是外圈是nxn，第二圈是（n-2）x（n-2）</p><p>我们首先从左上角出发，</p><figure class="highlight py"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">i, j, count = <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">        offset = <span class="number">1</span></span><br><span class="line">        matrix = [[<span class="number">0</span>] * n <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(n)]</span><br><span class="line">        <span class="keyword">while</span> offset &lt;= n//<span class="number">2</span>:</span><br><span class="line">            <span class="keyword">while</span> j &lt; n - offset:</span><br><span class="line">                count += <span class="number">1</span></span><br><span class="line">                matrix[i][j] = count</span><br><span class="line">                j += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> i &lt; n - offset:</span><br><span class="line">                count += <span class="number">1</span></span><br><span class="line">                matrix[i][j] = count</span><br><span class="line">                i += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> j &gt; <span class="number">0</span>:</span><br><span class="line">                count += <span class="number">1</span></span><br><span class="line">                matrix[i][j] = count</span><br><span class="line">                j -= <span class="number">1</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> i &gt; <span class="number">0</span>:</span><br><span class="line">                count += <span class="number">1</span></span><br><span class="line">                matrix[i][j] = count</span><br><span class="line">                i -= <span class="number">1</span></span><br><span class="line">            i += <span class="number">1</span></span><br><span class="line">            j += <span class="number">1</span></span><br><span class="line">            offset += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> n%<span class="number">2</span> == <span class="number">1</span>:</span><br><span class="line">            matrix[i][j] = n ** <span class="number">2</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> matrix</span><br></pre></td></tr></table></figure><p>这是错误的，首先原来我用的是浅拷贝<code>matrix = [[0]* n ]* n</code>，这样不会拷贝地址，所以错误，必须使用深拷贝<code>matrix = [[0] * n for _ in range(n)]</code></p><p>第二个错误是 <code>while j &gt; 0</code>和 <code>while i &gt; 0</code>，这样可能导致跳出边界</p><p>现在gpt改的代码中将<code>offset=0</code>，并且在判断语句中使用<code>j &gt;= offset</code>而不是<code>j &gt;0</code>，因为如果有两圈以上的话第二层是在内层拐弯，而不是在最外层拐弯</p><p><code>while j &gt;= offset</code>确保遍历范围正确，不会漏掉最左侧列，<code>matrix[i][j] == 0 </code>防止重复填充，确保没填充的每一项都是0，避免错误覆盖</p><p>而 <code>while i &gt; offset</code> 则是因为这一圈的第一个元素刚开始就被填充了旋转一圈后回到了起点的下面，因此不需要再填充，画个图会清晰明了。</p>]]>
    </content>
    <id>https://qyp9909.github.io/2025/03/09/leetcode/leetcode-1/</id>
    <link href="https://qyp9909.github.io/2025/03/09/leetcode/leetcode-1/"/>
    <published>2025-03-09T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="704-Binary-Search">704. Binary Search</h2>
<p>二分查找的注意点</p>
<p>思路：每次取list的最大最小下标，然后找到中间下标与其所对应的值，将其与目标比较大小，从而进一步缩短目标所在区间，时间复杂度logn</p]]>
    </summary>
    <title>LeetCode 1</title>
    <updated>2025-06-25T09:29:25.958Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="blog" scheme="https://qyp9909.github.io/categories/blog/"/>
    <category term="blog" scheme="https://qyp9909.github.io/tags/blog/"/>
    <content>
      <![CDATA[<p>从零开始搭建 Hexo 博客，并安装Butterfly 主题。</p><h2 id="✨-前置条件">✨ 前置条件</h2><ul><li>安装了 Node.js（推荐 v16.x 或以上）</li><li>安装了 Git</li><li>基本的终端操作能力</li></ul><hr><h2 id="1️⃣-安装-Hexo">1️⃣ 安装 Hexo</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">npm install -g hexo-cli</span><br><span class="line">hexo init my-blog</span><br><span class="line"><span class="built_in">cd</span> my-blog</span><br><span class="line">npm install</span><br><span class="line">hexo server</span><br><span class="line"><span class="built_in">cd</span> my-blog/themes</span><br><span class="line">git <span class="built_in">clone</span> -b master https://github.com/jerryc127/hexo-theme-butterfly.git butterfly</span><br></pre></td></tr></table></figure><p>然后编辑根目录的 _config.yml 文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">theme: butterfly</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-renderer-pug hexo-renderer-stylus --save</span><br><span class="line">vi themes/butterfly/_config.yml</span><br><span class="line">hexo new <span class="string">&quot;Hello Hexo&quot;</span></span><br></pre></td></tr></table></figure><h2 id="本地预览-生成部署">本地预览 + 生成部署</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">hexo clean</span><br><span class="line">hexo generate</span><br><span class="line">hexo server</span><br><span class="line">npm install hexo-deployer-git --save</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在 _config.yml 添加</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line">  type: git</span><br><span class="line">  repo: https://github.com/用户名/仓库.git</span><br><span class="line">  branch: gh-pages</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp; hexo d</span><br></pre></td></tr></table></figure><p>然后访问 <a href="https://xn--eqr924avxo.github.io">https://用户名.github.io</a> 即可看到博客</p>]]>
    </content>
    <id>https://qyp9909.github.io/2021/10/25/hexo-butterfly-begin/</id>
    <link href="https://qyp9909.github.io/2021/10/25/hexo-butterfly-begin/"/>
    <published>2021-10-25T12:00:00.000Z</published>
    <summary>
      <![CDATA[<p>从零开始搭建 Hexo 博客，并安装Butterfly 主题。</p>
<h2 id="✨-前置条件">✨ 前置条件</h2>
<ul>
<li>安装了 Node.js（推荐 v16.x 或以上）</li>
<li>安装了 Git</li>
<li>基本的终端操作能力</l]]>
    </summary>
    <title>搭建 Hexo 博客并配置 Butterfly 主题</title>
    <updated>2025-06-25T13:34:18.736Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="blog" scheme="https://qyp9909.github.io/categories/blog/"/>
    <category term="blog" scheme="https://qyp9909.github.io/tags/blog/"/>
    <content>
      <![CDATA[<h2 id="Quick-Start">Quick Start</h2><h3 id="Create-a-new-post">Create a new post</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p><h3 id="Run-server">Run server</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p><h3 id="Generate-static-files">Generate static files</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p><h3 id="Deploy-to-remote-sites">Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>]]>
    </content>
    <id>https://qyp9909.github.io/2021/10/14/test/</id>
    <link href="https://qyp9909.github.io/2021/10/14/test/"/>
    <published>2021-10-14T08:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Quick-Start">Quick Start</h2>
<h3 id="Create-a-new-post">Create a new post</h3>
<figure class="highlight bash"><table><tr><td class=]]>
    </summary>
    <title>Test the blog with hexo</title>
    <updated>2025-06-25T13:36:21.425Z</updated>
  </entry>
  <entry>
    <author>
      <name>Yuanpeng QU</name>
    </author>
    <category term="影评" scheme="https://qyp9909.github.io/categories/%E5%BD%B1%E8%AF%84/"/>
    <category term="movie" scheme="https://qyp9909.github.io/tags/movie/"/>
    <content>
      <![CDATA[<p>克里斯托弗·诺兰执导的《记忆碎片》（Memento）是一部打破传统叙事逻辑的电影杰作。它通过“倒叙+正叙”交错进行的方式，让观众仿佛置身于主人公失忆的混乱中。</p><p>本片以其极具创意的剪辑风格、哲学性极强的主题，以及不断反转的剧情节奏，被誉为心理惊悚片的经典之作。诺兰以独特的结构挑战观众对记忆与现实的信任——我们到底是在看真相，还是一段被不断重写的叙述？</p><blockquote><p>🎬 推荐指数：★★★★★<br>🎭 类型：悬疑 / 心理 / 犯罪<br>📅 上映时间：2000年</p></blockquote><p>如果你喜欢挑战自己大脑的电影，绝不能错过这部作品。</p>]]>
    </content>
    <id>https://qyp9909.github.io/2021/10/05/movie-test/</id>
    <link href="https://qyp9909.github.io/2021/10/05/movie-test/"/>
    <published>2021-10-05T06:31:49.000Z</published>
    <summary>
      <![CDATA[<p>克里斯托弗·诺兰执导的《记忆碎片》（Memento）是一部打破传统叙事逻辑的电影杰作。它通过“倒叙+正叙”交错进行的方式，让观众仿佛置身于主人公失忆的混乱中。</p>
<p>本片以其极具创意的剪辑风格、哲学性极强的主题，以及不断反转的剧情节奏，被誉为心理惊悚片的经典之作。诺]]>
    </summary>
    <title>《记忆碎片》：一场逆向思维的视觉实验</title>
    <updated>2025-06-14T08:35:50.264Z</updated>
  </entry>
</feed>
