<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Big-Yellow-J</title><link>https://www.big-yellow-j.top</link><description>个人技术博客系统记录各类算法原理、实现细节与代码实战，便于总结与复盘技术成长过程</description><language>zh-CN</language><pubDate>Wed, 22 Jul 2026 16:44:51 +0000</pubDate><lastBuildDate>Wed, 22 Jul 2026 16:44:51 +0000</lastBuildDate><generator>Jekyll v3.9.5</generator><atom:link href="https://www.big-yellow-j.top/static/xml/rss.xml" rel="self" type="application/rss+xml"/><item><title>音频型号处理————VAD基本原理以及常用模型</title><link>https://www.big-yellow-j.top/posts/2026/07/18/AudioSignalProcessing-1.html</link><description><![CDATA[VAD核心任务是在连续音频中精准区分语音与非语音段。理解其工作原理需掌握采样率（16kHz黄金平衡）、位深（16bit标配）、分帧（帧长20-40ms、帧移10ms）、特征（257维STFT功率谱或80维Fbank）以及对数分贝压缩动态范围。所有VAD均依赖滞后机制（最小语音/静音时长、首尾填充、双阈值）平滑抖动，并通过端点检测（回溯/提前）优化边界。主流模型包括：WebRTC VAD（6子带GMM似然比，极轻量）、Silero VAD（CNN+双向GRU，2MB，6000+语种预训练）、FSMN-VAD（前馈序列记忆网络，0.4M参数，并行推理高效）、pyannote SAD（Sinc...]]></description><content:encoded><![CDATA[<h2 id="vad模型简要概述">VAD模型简要概述</h2><p>VAD 的核心任务是 <strong>判断一段音频中哪些时间段有人说话哪些时间段没有人说话</strong>（也就是他就是一个音频是不是有静音的检测器），也就是区分语音和非语音（或静音）部分。想象一下，在一个有背景噪音的环境中，VAD 就像一个智能“守门人”，它能准确识别什么时候有人在说话，什么时候是纯粹的环境噪音或沉默。</p><h3 id="vad模型工作原理">VAD模型工作原理</h3><p><strong>音频数字化过程</strong>：声音通过震动转化为电压的模拟信号，但是电脑只能处理数字信号（01等），因此在数字化信号处理过程中通过ADC（模数转换器）进行处理，其主要是如下几件事情：<strong>1、采样</strong>每隔固定时间测量一次声音幅度，常用指标 <em>Hz</em>表示每秒采样多少声音副本（常用的8Hz、16Hz）；<strong>2、量化</strong>采样后数值结果是一批连续点，但是计算机存储数据范围有限如8bit就只有 <code>2^8=256</code>因此需要将数值进行量化处理如 <code>0.123456V--&gt;37</code>，常用指标为 <em>位深</em>如8bit、16bit等；实际代码去理解音频处理过程（<em>以模型训练中数据加载进行理解</em>），在<a href="https://datasets-server.huggingface.co/cached-assets/edinburghcstr/ami/--/46f28f2503e2ec48f8867a84eef356c70476beab/--/ihm/train/50/audio/audio.wav?Expires=1784359125&amp;Signature=svrspzw2YN9rWfF4o6DYYXf96iKc7qB~zVVwZ6XpODe4leVgGVb9tNW8wooOguZVR-mPeGVnWiUhOGfkmwCZTJFYHV4q98U4hGJasBxkN1jA-wXpvUp0WrQoN173pIrjhHvwnRmPmhlqvNOhGu22HYCsOId1PJDw2-1Tr8vWvkg8d4CGtTkL1k9nax7UHqFjkv9Qml5CxW-raSAAwtV4H9vmUX0WZVkDPsYkRcfP54SpUL4XPxTGI1S2DOTDerhw-ikcEwJDV-E8r8frX5HdjyvnJqqX7Xx1wtVTkDmDwjCbMvxURtjGnfxuFZ5QsJDaguoy0qeN8hogl0A~NqOyKA__&amp;Key-Pair-Id=KII6SEJ68IEHF">测试音频</a>中通过绘制出波形图、Mel频谱图：<br/><img src="https://files.seeusercontent.com/2026/07/18/nfX5/20260718145407476.png" alt=""/><br/> 其中在<strong>波形图</strong>中，X 轴 = 时间，Y 轴 = 振幅（空气振动强度），密集振荡的区域 → 有声音，平坦区域 → 静音。在<strong>Mel频谱图</strong>中：X轴 = 时间，Y 轴 = Mel 频率（人耳感知的频率刻度，低频分辨更精细），颜色 = 能量 (dB)。它将一维时序信号变成二维热力图，同时展示什么频率在什么时候出现。</p><blockquote><p><strong>Mel 频谱图计算过程：</strong><br/><strong>Step 1 — 预加重 (Pre-emphasis)</strong>：$y_t’ = y_t - \alpha \cdot y_{t-1}$（$\alpha \approx 0.97$），增强高频分量，补偿发声时声门和嘴唇对高频的衰减。<br/><strong>Step 2 — 分帧 (Framing)</strong>：将一维信号切成重叠的小段。每帧长度通常 20~40ms（如 16kHz 下 512 点 = 32ms），帧移通常 10ms（如 160 点）。帧与帧之间有重叠，保证时间上的连续性。<br/><strong>Step 3 — 加窗 (Windowing)</strong>：对每帧乘以汉明窗或汉宁窗，减少分帧带来的频谱泄漏（边缘截断效应）：$x_n^{windowed} = x_n \cdot w_n$。<br/><strong>Step 4 — STFT / FFT</strong>：对每帧做短时傅里叶变换，得到该帧的<strong>功率谱</strong>：$P = |\text{FFT}(x)|^2$。此时得到的是线性频率刻度上的能量分布。<br/><strong>Step 5 — Mel 滤波器组 (Mel Filterbank)</strong>：人耳对低频的分辨能力远高于高频。将线性频率 $f$（Hz）映射到 Mel 刻度：<br/> \(m = 2595 \cdot \log_{10}\left(1 + \frac{f}{700}\right)\)<br/> 用 $N$ 个三角形滤波器（如 80 个）在 Mel 刻度上等距排列，对功率谱加权求和，得到 $N$ 维的 Mel 能量向量。<br/><strong>Step 6 — 转 dB (Log Compression)</strong>：$\text{Mel}<em>{\text{dB}} = 10 \cdot \log</em>{10}\left(\frac{\text{Mel}}{\text{ref}}\right)$，取对数压缩动态范围，也更贴近人耳对响度的对数感知特性。最终得到一个 <code>(n_mels × n_frames)</code> 的二维矩阵——即 Mel 频谱图。</p></blockquote><p>回到VAD模型处理过程中，理想的VAD模型效果就是在上述波形图中找到静音区域（尽量不受到背景噪音的干扰）然后切分出来（亦或者从Mel图中剔除掉db小的内容）</p><h3 id="前置知识">前置知识</h3><p>理解了上面的 Mel 频谱图计算过程后，我们还需要掌握几个贯穿所有 VAD 模型的核心概念，才能理解后面每个模型的参数为什么这样设计、该怎样调。</p><h4 id="一采样率sample-rate">一、采样率（Sample Rate）</h4><p>采样率 = 每秒采集多少个声音振幅的”快照”，单位 Hz。16kHz 意味着每秒在波形上取 16,000 个点。</p><p>为什么 VAD 几乎全是 16kHz？</p><ul><li>人类语音主要能量集中在 <strong>300Hz ~ 3.4kHz</strong>（电话频带）。根据奈奎斯特定理，采样率 ≥ 语音最高频率 × 2 即可完整保留信息，所以 8kHz 就够了。</li><li>但 16kHz 能保留到 8kHz 的高频泛音（齿音、摩擦音等），这些对语音清晰度和 VAD 判断有帮助，而 44.1kHz/48kHz 的超声波段对 VAD 无意义只会增加计算量。</li><li>16kHz 是语音处理领域的黄金平衡点——信息完整、计算友好。</li></ul><h4 id="二位深bit-depth">二、位深（Bit Depth）</h4><p>位深 = 每个采样点用多少 bit 存储振幅值。8bit → $2^8=256$ 个可能值，16bit → $2^{16}=65,!536$ 个。16bit 是音频处理的标配——能表达从耳语到呐喊的动态范围而不过载或淹没在量化误差里。</p><h4 id="三分帧帧长帧移与重叠">三、分帧：帧长、帧移与重叠</h4><p><strong>为什么分帧？</strong> 语音是非平稳信号，频率成分随时间剧烈变化。直接对整段 10 秒音频做傅里叶变换会丢失所有时间信息——你不知道哪个频率发生在第几秒。所以在 Mel 频谱图计算的 Step 2 中，我们把长音频切成几十毫秒的小片段（帧），假设每帧内信号近似平稳，对每帧分别分析。</p><p>两个关键参数：</p><table><thead><tr><th>概念</th><th>典型值</th><th>含义</th></tr></thead><tbody><tr><td><strong>帧长 (Frame Length / Window Size)</strong></td><td>20~40ms（16kHz 下 320~640 点）</td><td>每次分析截取多长的音频片段。太长 → 时间分辨率差（变化捕捉迟钝），太短 → 频率分辨率差</td></tr></tbody></table><p>例如 FSMN-VAD 的 <code>frame_out_ms=25</code>（帧长 25ms）、<code>frame_in_ms=10</code>（帧移 10ms），WebRTC VAD 固定 30ms 帧长。</p><table><thead><tr><th>概念</th><th>典型值</th><th>含义</th></tr></thead><tbody><tr><td><strong>帧移 (Hop Length / Frame Shift)</strong></td><td>10ms（16kHz 下 160 点）</td><td>相邻两帧起点间隔多少毫秒。帧移 &lt; 帧长 → 帧之间有<strong>重叠</strong>，保证时间连续性，避免遗漏帧边界处的语音变化</td></tr><tr><td><strong>重叠 (Overlap)</strong></td><td>帧长 − 帧移</td><td>重叠越大 → 时间分辨率越高，但计算量也越大</td></tr></tbody></table><pre><code>帧长 25ms, 帧移 10ms:
|========帧0========|
          |========帧1========|
                    |========帧2========|
                              ...
|&lt;-- 10ms --&gt;|
</code></pre><h4 id="四特征stft-功率谱-vs-fbank梅尔滤波器组能量">四、特征：STFT 功率谱 vs Fbank（梅尔滤波器组能量）</h4><p>不同 VAD 模型使用的输入特征不同，理解它们的区别很重要：</p><table><thead><tr><th>特征</th><th>维度</th><th>谁在用</th><th>特点</th></tr></thead><tbody><tr><td><strong>STFT 功率谱</strong></td><td>257 维/frame（16kHz, 512 点 FFT）</td><td>Silero VAD</td><td>线性频率刻度，保留全部频域细节</td></tr><tr><td><strong>Fbank (Filterbank)</strong></td><td>80 维/frame（80 个 Mel 滤波器）</td><td>FSMN-VAD、FireRedVAD</td><td>Mel 刻度 + 三角滤波压缩，维度低、去冗余、更贴合人耳</td></tr></tbody></table><p>Fbank 就是 Mel 频谱图计算过程的 Step 1~5（跳过 Step 6 转 dB），得到的是 80 维能量值而非 dB。两者没有绝对的优劣：功率谱信息完整但维度高、冗余多；Fbank 压缩了维度，语音信息密度更高、模型更容易训练。</p><h4 id="五分贝db为什么用对数尺度">五、分贝（dB）：为什么用对数尺度</h4><p>音频处理中几乎处处用 dB 而不是线性能量值，原因有两条：</p><ol><li><strong>人耳对数感知</strong>：声压 10 倍 → 听感约 2 倍响。dB 就是对能量取对数，匹配人耳特性。</li><li><strong>压缩动态范围</strong>：耳语和呐喊的能量差可达 $10^6$ 倍，线性值下小信号完全被淹没。转 dB 后变成 60dB 差距，算法更好处理。</li></ol><p>两个常用 dB 公式：</p> \[\text{功率比 dB} = 10 \cdot \log_{10}\left(\frac{P}{P_{\text{ref}}}\right) \quad \text{振幅比 dB} = 20 \cdot \log_{10}\left(\frac{A}{A_{\text{ref}}}\right)\] <p>能量 VAD 中的 <code>threshold_db=-30</code> 意思是：帧 RMS 能量比最大 RMS 低 30dB 以下算静音。−30dB 约等于最大能量的 1/32，是一个比较保守的静音阈值。</p><h4 id="六vad-后处理滞后机制hysteresis">六、VAD 后处理：滞后机制（Hysteresis）</h4><p>这是理解 VAD 参数最核心的概念。模型输出的逐帧概率（0 或接近 1 = 语音，0 或 接近 0 = 静音）往往伴随频繁跳变——一帧语音、一帧静音、一帧语音……这是因为：</p><ul><li>词语之间的短暂停顿（”今天——天气——很好”）</li><li>爆破音/塞音的瞬时低能量段</li><li>模型自身的抖动</li></ul><p>直接按帧级概率切分会得到大量碎片。所有 VAD 都会用一套<strong>滞后（Hysteresis）机制</strong>来平滑：</p><pre><code class="language-mermaid">flowchart TD
    A["逐帧概率"] --&gt; B{"概率 &gt; 阈值 ?"}
    B --&gt;|是| C["触发计数 +1"]
    B --&gt;|否| D["静音计数 +1"]
    C --&gt; E{"连续触发 ≥ min_speech ?"}
    E --&gt;|是| F["语音段开始"]
    F --&gt; G["持续记录..."]
    G --&gt; H{"连续静音 ≥ min_silence ?"}
    H --&gt;|否| G
    H --&gt;|是| I["语音段结束"]
    I --&gt; J{"本段时长 ≥ min_dur ?"}
    J --&gt;|是| K["保留此语音段"]
    J --&gt;|否| L["丢弃（太短）"]
</code></pre><p>对应的常见参数（不同模型叫法略有不同）：</p><table><thead><tr><th>参数</th><th>含义</th><th>调大效果</th><th>调小效果</th></tr></thead><tbody><tr><td><code>threshold</code> / <code>speech_threshold</code></td><td>单帧判为语音的概率/能量门槛</td><td>更保守（更少判语音）</td><td>更激进（更多判语音）</td></tr><tr><td><code>min_speech_duration_ms</code></td><td>连续多少毫秒判语音才确认”语音段开始”</td><td>过滤短噪音导致的误触发</td><td>更快响应，但可能误触发</td></tr><tr><td><code>min_silence_duration_ms</code></td><td>连续多少毫秒静音才确认”语音段结束”</td><td>语音段更长（容忍更长的句中停顿）</td><td>语音段更短，句中停顿即切分</td></tr><tr><td><code>speech_pad_ms</code></td><td>在语音段首尾额外多保留多少毫秒</td><td>避免切掉开头/结尾，语音段更完整</td><td>边界更精确，但可能截断尾音</td></tr></tbody></table><p>这四个参数的组合决定了 VAD 的<strong>激进程度</strong>——是宁可多切（保守，漏语音少但杂音多）还是宁可不切（激进，杂音少但可能漏语音）。</p><h4 id="七端点检测endpoint-detection">七、端点检测（Endpoint Detection）</h4><p>FSMN-VAD 参数中大量的 <code>start_point</code>、<code>end_point</code>、<code>lookback</code>、<code>lookahead</code> 参数，就是端点检测的具体实现。核心思想与滞后机制一致，但更精细：</p><ul><li><strong>起点检测</strong>：连续 N 帧判语音后才标记起点，可以回溯（<code>lookback_time_start_point</code>）补回触发前的部分，避免切掉辅音开头</li><li><strong>终点检测</strong>：连续 M 帧静音后才标记终点，可以提前（<code>lookahead_time_end_point</code>）减少延迟</li><li><strong>双阈值 (Dual Threshold)</strong>：某些实现（如 FSMN-VAD 的 <code>speech_noise_thresh_low/high</code>）用高阈值确认”有语音”，用低阈值确认”语音结束”，防止能量波动导致反复切换</li></ul><h4 id="八其他参数速查">八、其他参数速查</h4><table><thead><tr><th>概念</th><th>出现在</th><th>含义</th></tr></thead><tbody><tr><td><strong>SNR (信噪比)</strong></td><td>FSMN-VAD</td><td>语音能量 / 噪声能量，&lt; 阈值则判噪声，用于筛除能量低但噪音大的帧</td></tr><tr><td><strong>chunk / 分块推理</strong></td><td>FireRedVAD、FSMN-VAD</td><td>将长音频切成固定时长（如 30,000 帧）逐块推理，控制显存/内存占用</td></tr><tr><td><strong>平滑窗口 (Smooth Window)</strong></td><td>FireRedVAD</td><td>对连续 N 帧的概率取平均，消除单帧抖动</td></tr><tr><td><strong>CMVN (倒谱均值方差归一化)</strong></td><td>FireRedVAD</td><td>对 Fbank 特征做零均值单位方差归一化，消除信道/录音设备差异</td></tr></tbody></table><p>有了这些前置知识，再看下面每个模型的参数表，就能理解每个参数控制的是哪一步、调大会产生什么效果。</p><h2 id="常用vad模型架构">常用VAD模型架构</h2><h3 id="webrtc-vad--高斯混合模型-gmm">WebRTC VAD — 高斯混合模型 (GMM)</h3><p>Google 在 WebRTC 项目中开源的经典 VAD <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>，浏览器内置，极轻量零依赖，是实时通信场景的事实标准。采用纯信号处理方法，无需神经网络。</p><h4 id="模型结构">模型结构</h4><p><strong>特征提取</strong><sup id="fnref:1:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>：每帧提取 6 个子带（80Hz~4kHz 按对数刻度划分）的能量，得到一个 6 维特征向量。<br/><strong>模型结构</strong>：每个子带用 <strong>2 个高斯分布</strong> 分别建模语音和噪声的能量分布，共 12 个高斯分量。对每帧的 6 维特征向量，计算其在语音 GMM 和噪声 GMM 下的似然比：</p> \[\text{LLR} = \log \frac{P(\mathbf{x} \mid \text{语音})}{P(\mathbf{x} \mid \text{噪声})} = \sum_{k=1}^{6} \log \frac{\mathcal{N}(x_k; \mu_k^s, \sigma_k^s)}{\mathcal{N}(x_k; \mu_k^n, \sigma_k^n)}\] <p>LLR &gt; 阈值则判为语音帧。GMM 参数通过 EM 算法离线训练，推理仅需查表 + 指数运算。</p><pre><code class="language-mermaid">flowchart LR
    A[16kHz PCM] --&gt; B[30ms 分帧]
    B --&gt; C[6 子带能量]
    C --&gt; D[语音 GMM 似然]
    C --&gt; E[噪声 GMM 似然]
    D --&gt; F[似然比 &gt; 阈值?]
    E --&gt; F
    F --&gt; G[语音 / 静音]
</code></pre><h4 id="模型使用">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>aggressiveness</code></td><td><code>2</code></td><td>激进程度（0~3），越大越容易判为语音</td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("webrtc", aggressiveness=2)
segments = vad.detect("audio.wav")
</code></pre><p><img src="https://files.seeusercontent.com/2026/07/22/fM3u/webrtc_mel.png" alt=""/></p><h3 id="silero-vad--crnncnn--gru">Silero VAD — CRNN（CNN + GRU）</h3><p>Silero 团队开源的企业级 VAD <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>，训练数据覆盖 6000+ 语种，模型仅 2MB，单 CPU 线程 &lt;1ms/帧，社区最活跃的开源 VAD（GitHub 9.6k Stars）。</p><h4 id="模型结构-1">模型结构</h4><p><strong>特征提取</strong><sup id="fnref:2:1" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>：输入 16kHz 音频 → 512 点 STFT（32ms 窗，10ms 帧移）→ 257 维功率谱。<br/><strong>模型结构</strong>（前向计算流）：</p><pre><code>STFT (257, T)
  ↓
Conv1d(k=3, 257→128) + ReLU
  ↓
Conv1d(k=3, 128→64) + ReLU
  ↓
2× GRU(hidden=64, bidirectional)
  ↓
Linear(128→1) + Sigmoid
  ↓
每帧语音概率 ∈ [0, 1]
</code></pre><p>CNN 负责提取局部时频模式（如共振峰、谐波结构），双向 GRU 捕捉前后时间上下文（如语音段边界过渡），最后 Sigmoid 输出逐帧概率。总参数约 <strong>2MB</strong>，纯 PyTorch JIT，可导出 ONNX。</p><h4 id="模型使用-1">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>use_onnx</code></td><td><code>False</code></td><td>是否使用 ONNX 推理（需 <code>onnxruntime</code>）</td></tr><tr><td><code>threshold</code></td><td><code>0.5</code></td><td>语音概率阈值（0~1），高于此值判为语音</td></tr><tr><td><code>min_speech_duration_ms</code></td><td><code>250</code></td><td>最短语音持续（毫秒），短于此的片段被过滤</td></tr><tr><td><code>min_silence_duration_ms</code></td><td><code>100</code></td><td>最短静音间隔（毫秒），小于此的间隔被合并</td></tr><tr><td><code>speech_pad_ms</code></td><td><code>30</code></td><td>语音段首尾填充（毫秒），避免切掉开头结尾</td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("silero", threshold=0.6, min_speech_duration_ms=300)
segments = vad.detect("audio.wav")
</code></pre><p><img src="https://files.seeusercontent.com/2026/07/22/Cbx5/silero_mel.png" alt=""/></p><h3 id="fsmn-vad--前馈序列记忆网络">FSMN-VAD — 前馈序列记忆网络</h3><blockquote><p><a href="https://modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch/summary">https://modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch/summary</a></p></blockquote><h4 id="模型结构-2">模型结构</h4><p>阿里达摩院在 FunASR 中开源的 VAD 模块 <sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>，基于前馈序列记忆网络（FSMN），专为中英文语音场景优化，仅 0.4M 参数。其具体模型结构：<br/><img src="https://modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch/resolve/master/fig/struct.png" alt=""/></p><p><strong>特征提取</strong><sup id="fnref:3:1" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>：80 维 Fbank（梅尔滤波器组能量），帧长 25ms，帧移 10ms。<br/><strong>模型结构</strong>：在传统的序列建模模型RNN/LSTM等在第 t 帧的结果依赖第 t−1 帧的隐藏状态，所以无法并行化，训练和推理都很慢，FSMN 的核心洞察是：序列建模不一定需要递归。如果你能让每一帧直接”看到”前后若干帧的信息，就能用纯前馈网络（可并行）达到类似的上下文建模效果，因此FSMN 的核心思想是在标准前馈网络（FFN）的隐藏层中插入 <strong>可学习的记忆模块 (Memory Block)</strong>：</p> \[\tilde{h}_t = h_t + \sum_{i=0}^{N} a_i \odot h_{t-i} + \sum_{j=1}^{M} c_j \odot h_{t+j}\] <p>其中 $h_t$ 是当前帧的隐藏向量，$a_i, c_j$ 是前向/后向记忆权重。相比 LSTM/GRU，FSMN 无需序列化递归计算，所有帧可并行前向传播，训练和推理都更快。对于上述个公式在具体的模型结构设计中对于输入特征维度 $(B,T,D)$ 直接可以通过循环去对时间维度 $T$ 进行读取然后进行编码即可，实际上更加高效的计算直接通过卷积进行实现：</p><pre><code class="language-python">class MemoryBlock(nn.Module):
  """FSMN 核心：可学习的固定窗口上下文加权模块。
  
  公式: h̃_t = h_t + Σ a_i ⊙ h_{t-i} + Σ c_j ⊙ h_{t+j}
  
  关键设计：a_i, c_j 是 d 维向量（不是矩阵），参数量仅 O(d × (N+M))，
  远小于自注意力的 O(d²)。
  """
  
  def __init__(self, dim: int, forward_order: int, backward_order: int):
      """
      Args:
          dim: 隐藏层维度 d
          forward_order: 前向阶数 N（看过去多少帧）
          backward_order: 后向阶数 M（看未来多少帧）
      """
      super().__init__()
      self.N = forward_order   # 前向记忆阶数
      self.M = backward_order  # 后向记忆阶数
      
      # 可学习的记忆权重向量，形状 (N+1, dim) 和 (M, dim)
      # +1 是因为 i=0 也包含在内（当前帧自身的 weight）
      self.forward_weights = nn.Parameter(torch.randn(forward_order + 1, dim) * 0.02)
      self.backward_weights = nn.Parameter(torch.randn(backward_order, dim) * 0.02)
  
  def forward(self, x: torch.Tensor) -&gt; torch.Tensor:
      """
      Args:
          x: (B, T, dim) — 批量 × 时间帧 × 隐藏维度
      Returns:
          (B, T, dim) — 融合上下文后的增强表示
      """
      B, T, d = x.shape
      out = torch.zeros_like(x)
      
      # ---------- 逐帧计算（可优化为 conv1d，这里保持公式的直观性）----------
      for t in range(T):
          # --- 前向部分：Σ a_i ⊙ h_{t-i}，i = 0..N ---
          forward_sum = torch.zeros(B, d, device=x.device)
          for i in range(self.N + 1):
              idx = t - i
              if idx &gt;= 0:
                  forward_sum = forward_sum + self.forward_weights[i] * x[:, idx, :]
          
          # --- 后向部分：Σ c_j ⊙ h_{t+j}，j = 1..M ---
          backward_sum = torch.zeros(B, d, device=x.device)
          for j in range(1, self.M + 1):
              idx = t + j
              if idx &lt; T:
                  backward_sum = backward_sum + self.backward_weights[j - 1] * x[:, idx, :]
          
          out[:, t, :] = x[:, t, :] + forward_sum + backward_sum
      
      return out


class FastMemoryBlock(nn.Module):
  """MemoryBlock 的高效卷积实现版本。
  
  将逐元素加权求和等价为 depthwise 1D 卷积：
  - 前向部分等价于 causal conv1d (groups=dim)
  - 后向部分等价于 anti-causal conv1d (groups=dim)
  """
  
  def __init__(self, dim: int, forward_order: int, backward_order: int):
      super().__init__()
      self.N = forward_order
      self.M = backward_order
      
      # 前向权重：shape (dim, 1, N+1)，作为 depthwise conv 的 kernel
      # 卷积核排列：[w_0, w_1, ..., w_N]（从左到右 = 从当前到过去）
      fwd_kernel = torch.randn(dim, 1, forward_order + 1) * 0.02
      self.register_buffer('fwd_kernel', fwd_kernel)
      
      # 后向权重：shape (dim, 1, M)，右侧填充 0 作为 anti-causal
      bwd_kernel = torch.randn(dim, 1, backward_order) * 0.02
      self.register_buffer('bwd_kernel', bwd_kernel)
      
      # 用 Parameter 方式（支持训练）：
      self.fwd_weight = nn.Parameter(torch.randn(dim, 1, forward_order + 1) * 0.02)
      self.bwd_weight = nn.Parameter(torch.randn(dim, 1, backward_order) * 0.02)
  
  def forward(self, x: torch.Tensor) -&gt; torch.Tensor:
      """
      x: (B, T, dim) → permute → (B, dim, T) 作为 depthwise conv 的输入
      """
      B, T, d = x.shape
      x_t = x.permute(0, 2, 1)  # (B, dim, T)
      
      # 前向 causal conv：padding 在左侧，只看过去
      fwd_out = F.conv1d(
          x_t, self.fwd_weight, groups=d,
          padding=self.N  # 左侧 pad N 个零（保证 causal）
      )[:, :, :T]  # 裁掉右侧多余的
      
      # 后向 anti-causal conv：padding 在右侧，只看未来
      bwd_out = F.conv1d(
          torch.flip(x_t, [2]),  # 时间反转
          self.bwd_weight, groups=d,
          padding=self.M
      )[:, :, :T]
      bwd_out = torch.flip(bwd_out, [2])  # 再反转回来
      
      out = x_t + fwd_out + bwd_out
      return out.permute(0, 2, 1)  # (B, T, dim)
</code></pre><p>不过在具体的“时序”数据中因为未来时间信息是未知的所以<strong>训练/推理过程中比较有效的策略</strong>：1、<strong>离线推理过程</strong>：训练过程中双向+推理使用双向，不过训练过程中直接使用 <strong>Dropout</strong> 随机不让 <em>未来</em> 信息融入；2、<strong>实时推理</strong>：此时需要将模型退化为因果FSMN（$tilde{h}<em>t = h_t + \sum</em>{i=0}^{N} a_i \odot h_{t-i}$），而为了保证实时推理的有效一般策略是使用 <em>状态累计策略</em>，通过固定采集音频进行输入而后对采集的音频中进行 “静音片段检测”当片段超过累计则进行放回，具体流程如下：<br/><img src="https://files.seeusercontent.com/2026/07/22/0Fdc/20260722233630743.png" alt=""/></p><p>FSMN-VAD 仅 <strong>0.4M 参数</strong>，是当前最小的高精度 VAD。<br/><img src="https://files.seeusercontent.com/2026/07/22/yp9I/fsmn_mel.png" alt=""/></p><h4 id="模型使用-2">模型使用</h4><p><strong>音频特征参数：</strong></p><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>sample_rate</code></td><td><code>16000</code></td><td>音频采样率（Hz）</td></tr><tr><td><code>frame_in_ms</code></td><td><code>10</code></td><td>帧移（ms），每帧间隔</td></tr><tr><td><code>frame_out_ms</code></td><td><code>25</code></td><td>帧长（ms），每帧包含的音频时长</td></tr><tr><td><code>window_size_ms</code></td><td><code>200</code></td><td>推理滑动窗口长度（ms）</td></tr></tbody></table><p><strong>端点检测参数：</strong></p><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>detect_mode</code></td><td><code>0</code></td><td>检测模式，<code>1</code> 更严格，<code>0</code> 更宽松</td></tr><tr><td><code>do_start_point_detection</code></td><td><code>True</code></td><td>是否启用语音起点检测</td></tr><tr><td><code>do_end_point_detection</code></td><td><code>True</code></td><td>是否启用语音终点检测</td></tr><tr><td><code>max_start_silence_time</code></td><td><code>0</code></td><td>起点前最大静音（ms），<code>0</code> 不限制</td></tr><tr><td><code>max_end_silence_time</code></td><td><code>0</code></td><td>终点后最大静音（ms），<code>0</code> 不限制</td></tr><tr><td><code>lookback_time_start_point</code></td><td><code>0</code></td><td>起点回溯时间（ms），防止切掉开头</td></tr><tr><td><code>lookahead_time_end_point</code></td><td><code>0</code></td><td>终点提前时间（ms），减少延迟</td></tr><tr><td><code>sil_to_speech_time_thres</code></td><td><code>150</code></td><td>静音→语音切换所需连续触发时长（ms）</td></tr><tr><td><code>speech_to_sil_time_thres</code></td><td><code>150</code></td><td>语音→静音切换所需连续静音时长（ms）</td></tr><tr><td><code>do_extend</code></td><td><code>1</code></td><td>是否延长语音段尾部，避免截断尾音</td></tr></tbody></table><p><strong>分段控制：</strong></p><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>max_single_segment_time</code></td><td><code>15000</code></td><td>单段最大时长（ms），超长自动切分</td></tr></tbody></table><p><strong>噪声与能量门限：</strong></p><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>snr_mode</code></td><td><code>0</code></td><td>信噪比模式，<code>0</code> 关闭，<code>1</code> 启用</td></tr><tr><td><code>snr_thres</code></td><td><code>−100</code></td><td>信噪比阈值（dB），低于此值判为噪声</td></tr><tr><td><code>noise_frame_num_used_for_snr</code></td><td><code>100</code></td><td>SNR 估计所用噪声帧数</td></tr><tr><td><code>decibel_thres</code></td><td><code>−100</code></td><td>分贝阈值，低于此值的帧判为静音</td></tr><tr><td><code>speech_noise_thres</code></td><td><code>0.8</code></td><td>语音/噪声能量比阈值，控制噪声过滤灵敏度</td></tr><tr><td><code>speech_noise_thresh_low</code></td><td><code>−1</code></td><td>双阈值下限</td></tr><tr><td><code>speech_noise_thresh_high</code></td><td><code>1</code></td><td>双阈值上限</td></tr><tr><td><code>speech_2_noise_ratio</code></td><td><code>0.5</code></td><td>语音/噪声比例，越大越严格</td></tr><tr><td><code>fe_prior_thres</code></td><td><code>1e-4</code></td><td>特征能量先验阈值，小于此的帧直接判静音</td></tr></tbody></table><p><strong>推理环境：</strong></p><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>device</code></td><td><code>"cpu"</code></td><td>推理设备，<code>"cuda"</code> 或 <code>"cpu"</code></td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("fsmn", speech_noise_thres=0.9, sil_to_speech_time_thres=100,
                        max_single_segment_time=10000, device="cuda")
segments = vad.detect("audio.wav")
</code></pre><h3 id="pyannoteaudio--sincnet--lstm--self-attention">pyannote.audio — SincNet + LSTM + Self-Attention</h3><p>法国 CNRS 开源的说话人日志工具包 <sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>，学术基准长期 SOTA，其语音活动检测（SAD）模块端到端从原始波形学习，无需手工特征。</p><h4 id="模型结构-3">模型结构</h4><p><strong>特征提取</strong>：直接用 <strong>SincNet</strong> 从原始波形学习滤波器，无需 STFT：</p> \[g[n, f_l, f_h] = 2f_h \cdot \text{sinc}(2\pi f_h n) - 2f_l \cdot \text{sinc}(2\pi f_l n)\] <p>$f_l, f_h$ 是可学习的低/高频截止频率，相当于在频域学习一组带通滤波器。<br/><strong>模型结构</strong>：</p><pre><code>原始波形 (1, T)
  ↓
SincNet (学习 64 个带通滤波器)
  ↓
2× LSTM (hidden=128, bidirectional)
  ↓
Multi-Head Self-Attention (4 heads)
  ↓
Linear(128→1) + Sigmoid
  ↓
逐帧语音概率
</code></pre><p>SincNet 替代手工 Fbank/STFT，端到端从波形学习；LSTM 建模长程时序依赖；自注意力聚合全局上下文，捕捉整段音频的语音分布模式。</p><h4 id="模型使用-3">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>token</code></td><td><code>HF_TOKEN</code> 环境变量</td><td>HuggingFace 访问令牌，需先接受<a href="https://hf.co/pyannote/voice-activity-detection">用户协议</a></td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("pyannote", token="hf_xxx")
segments = vad.detect("audio.wav")
</code></pre><h3 id="fireredvad">FireRedVAD</h3><h4 id="模型结构-4">模型结构</h4><p>小红书开源VAD模型<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>模型参数上只有0.6M基于DFSMN同时支持流式和非流式的使用，<a href="#fsmn-vad--前馈序列记忆网络">FSMN模型结构介绍</a></p><h4 id="模型使用-4">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>model_dir</code></td><td><code>pretrained_models/FireRedVAD/VAD</code></td><td>模型路径，首次自动下载</td></tr><tr><td><code>speech_threshold</code></td><td><code>0.4</code></td><td>语音概率阈值（0~1），低于此值判为静音</td></tr><tr><td><code>use_gpu</code></td><td><code>False</code></td><td>是否启用 GPU 推理</td></tr><tr><td><code>smooth_window_size</code></td><td><code>5</code></td><td>概率平滑窗口大小（帧数），减少抖动</td></tr><tr><td><code>min_speech_frame</code></td><td><code>20</code></td><td>最短语音持续帧数，短于此的片段被过滤</td></tr><tr><td><code>max_speech_frame</code></td><td><code>2000</code></td><td>最长语音持续帧数，超长自动切分</td></tr><tr><td><code>min_silence_frame</code></td><td><code>20</code></td><td>最短静音间隔帧数，小于此的间隔被合并</td></tr><tr><td><code>merge_silence_frame</code></td><td><code>0</code></td><td>合并间距帧数，相邻段间隔小于此值则合并</td></tr><tr><td><code>extend_speech_frame</code></td><td><code>0</code></td><td>向外扩展帧数，在语音段首尾各延伸</td></tr><tr><td><code>chunk_max_frame</code></td><td><code>30000</code></td><td>分块推理的最大帧数，控制显存/内存占用</td></tr></tbody></table><pre><code class="language-python">from fireredvad import FireRedVad, FireRedVadConfig

config = FireRedVadConfig(
    use_gpu=use_gpu, speech_threshold=speech_threshold,
    smooth_window_size=smooth_window_size,
    min_speech_frame=min_speech_frame, max_speech_frame=max_speech_frame,
    min_silence_frame=min_silence_frame, merge_silence_frame=merge_silence_frame,
    extend_speech_frame=extend_speech_frame, chunk_max_frame=chunk_max_frame,
)
vad = FireRedVad.from_pretrained(model_dir, config)
result, _ = vad.detect(audio)
</code></pre><p><img src="https://files.seeusercontent.com/2026/07/22/qPt2/firered_mel.png" alt=""/></p><h3 id="能量-vad--rms-阈值">能量 VAD — RMS 阈值</h3><p>最简单的 VAD 实现，仅依赖信号处理计算帧能量，零模型依赖、零网络请求、零额外存储，适合快速原型和噪声可控的环境。</p><h4 id="模型结构-5">模型结构</h4><p>最朴素的方法：计算每帧 RMS → 转 dB → 与固定阈值比较。</p> \[\text{RMS}_t = \sqrt{\frac{1}{N} \sum_{n=0}^{N-1} x_t[n]^2}, \quad \text{dB}_t = 20 \log_{10} \left( \frac{\text{RMS}_t}{\text{RMS}_{\max}} \right)\] <p>$\text{dB}_t &gt; \theta$（如 −30dB）判为语音。零模型依赖，但阈值固定，无法应对动态噪声环境。</p><h4 id="模型使用-5">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>threshold_db</code></td><td><code>−30</code></td><td>能量阈值（dB），高于此值判为语音</td></tr><tr><td><code>frame_ms</code></td><td><code>25</code></td><td>帧长（毫秒）</td></tr><tr><td><code>hop_ms</code></td><td><code>10</code></td><td>帧移（毫秒）</td></tr><tr><td><code>min_speech_ms</code></td><td><code>200</code></td><td>最短语音持续（毫秒），短于此的片段被过滤</td></tr><tr><td><code>min_silence_ms</code></td><td><code>300</code></td><td>最短静音间隔（毫秒），小于此的间隔被合并</td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("energy", threshold_db=-25, min_speech_ms=100)
segments = vad.detect("audio.wav")
</code></pre><h3 id="auditok--能量--频谱">auditok — 能量 + 频谱</h3><p>基于能量和频谱特性的纯算法 VAD，Python 生态中轻量级音频切分工具，无需训练，开箱即用。</p><h4 id="模型结构-6">模型结构</h4><p>基于能量和频谱特性的传统信号处理方法。对音频流进行实时分帧，计算每帧短时能量，当能量连续超过阈值且持续足够时长时标记为语音段。核心逻辑为状态机：静音 → 检测到能量 → 语音段开始 → 能量持续低于阈值 + 静音超时 → 语音段结束。无需训练数据，纯算法实现。</p><h4 id="模型使用-6">模型使用</h4><table><thead><tr><th>参数</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td><code>energy_threshold</code></td><td><code>50</code></td><td>能量阈值，高于此值视为有声音</td></tr><tr><td><code>min_dur</code></td><td><code>0.2</code></td><td>最短语音持续（秒），短于此的片段被过滤</td></tr><tr><td><code>max_dur</code></td><td><code>15</code></td><td>最长语音持续（秒），超长自动切分</td></tr><tr><td><code>max_silence</code></td><td><code>0.3</code></td><td>最大静音容忍（秒），静音超过此值则切分</td></tr></tbody></table><pre><code class="language-python">from AudioVADProcess import VADProcess

vad = VADProcess.create("auditok", energy_threshold=45, min_dur=0.5)
segments = vad.detect("audio.wav")
</code></pre><p><img src="https://files.seeusercontent.com/2026/07/22/9nYf/auditok_mel.png" alt=""/></p><h2 id="vad模型补充介绍">VAD模型补充介绍</h2><h3 id="训练推理过程">训练/推理过程</h3><ul><li><strong>训练数据构建</strong></li></ul><p><strong>音频数据结构</strong>一般对于输入的音频读取过程中，假设直接通过<a href="https://docs.pytorch.org/audio/stable/generated/torchaudio.load.html#torchaudio.load">torchaudio</a>去读取 <code>waveform, sample_rate = torchaudio.load("test.wav")</code>对于两部分返回内容分别表示：1、<code>sample_rate</code>：采样率；2、<code>waveform</code>：<code>[channel, time]</code>表示通道数量以及音频采样点数量。比如说16Hz，3秒，单声道返回结果就是 <code>torch.Size([1, 48000])</code>。一般而言模型输入数据主要是两种：1、直接输入waveform数据；2、直接输入输入Mel/Fbank特征（会将音频的 waveform 数据进行转换）：对于输入为 <code>[1, 48000]</code>会直接转换为 <code>[T,F]</code>比如说 <code>[300, 80]</code>那么这样一来音频就变成 <code>[bs, 300, 80]</code>。</p><blockquote><p>如果音频长度不一致一般就直接用padding策略即可</p></blockquote><p><strong>标签构建过程</strong>对于输入音频只需要标记出来中间那些位置有音频，比如说10s的音频设置10ms（每一秒100帧），那么0.80s ~ 3.50s对对应帧 80 到帧 349，全部标1依次类推这样一来<strong>标签就变成了</strong>：<code>[0,0,...,1,...1,0...0]</code>，但是对于训练数据如何去对音频截取窗口就需要考虑：因为很可能一段音频里面大部分都是静音的，这样就会导致模型效果不佳。一般的处理策略很多比如说有些ASR模型会返回每个字所对应的时间戳直接去根据这个时间戳截取即可。</p><ul><li><strong>损失函数设计</strong></li></ul><p><strong>损失函数构建</strong>可以直接使用二元交叉熵损失函数处理即可</p><ul><li><strong>推理过程</strong></li></ul><p>对于输入音频进行读取获取1维的数组（如果双通道直接取平均即可）假设得到数据维度为 <code>[1, 3s x 16000hz]</code>，而后将1维的波形特征转化为2维矩阵特征假设得到维度信息为：<code>[1, 300帧, 80]</code>而后直接将这个音频输入到模型这哦你进行推理输出每一帧的概率 <code>[1,300,1]</code>。但是输出结果中可能出现在 t帧位置判断为静音（可能是错误判断），但是不能根据这个又把音频切分成两段，因此需要一个后处理过程最简单的就是直接静音帧累计策略（静音帧数累计&gt;30就认为应该切分了）</p><blockquote><p>除此之外有些时候还会使用 <code>speech_pad</code> 策略就是将预测的时间范围向前向后补充部分帧避免截断为题出现</p></blockquote><p><strong>除此之外</strong>还需要考虑超长音频吹过程（不可能直接全部输入到模型中，显存处理不下）具体过程如下（假设模型结构为FSMN）：<br/><img src="https://files.seeusercontent.com/2026/07/22/mjS8/20260723004146063.png" alt=""/></p><h1 id="参考">参考</h1><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>WebRTC VAD — <a href="https://webrtc.googlesource.com/src/+/refs/heads/main/common_audio/vad/">https://webrtc.googlesource.com/src/+/refs/heads/main/common_audio/vad/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:1:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://github.com/snakers4/silero-vad">Silero VAD: pre-trained enterprise-grade Voice Activity Detector.</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/abs/2305.11013">FunASR: A Fundamental End-to-End Speech Recognition Toolkit.</a>” <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:3:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://github.com/pyannote/pyannote-audio">Powerset multi-class cross entropy loss for neural speaker diarization.</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2603.10420">FireRedASR2S: A State-of-the-Art Industrial-Grade All-in-One Automatic Speech Recognition System.” arXiv:2603.10420, 2026.</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>音频信号处理</category><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/07/18/AudioSignalProcessing-1.html</guid></item><item><title>🔥Pytorch使用-5：基于Ray服务部署-1</title><link>https://www.big-yellow-j.top/posts/2026/06/05/RayArchUse.html</link><description><![CDATA[Ray是支持以本地Python写法实现分布式/并行计算的开源框架，核心计算仍由PyTorch等执行，覆盖模型部署、服务器调度等场景。其架构包含五大核心组件：Head与Worker两类节点、两层分布式调度器、基于Apache Arrow/Plasma的内存分布式对象存储、GCS全局控制存储、节点级守护进程Raylet，可实现低延迟调度、零拷贝数据访问、节点自治等特性。上层基于Ray Core的Task、Actor、Object基础分布式原语，封装Ray Data分布式数据预处理、Ray Train多框架分布式训练、Ray Tune分布式超参数搜索能力，大幅降低分布式开发门槛。]]></description><content:encoded><![CDATA[<p>前面介绍了在<a href="https://www.big-yellow-j.top/posts/2026/04/20/torch-basic-distribute-1.html">pytorch中不同的分布式训练实现方式</a>，这里简单介绍分布式框架（更加多的设计到了模型部署、服务器调度之间内容，非严格的pytorch内容）Ray以及Docker等内容。</p><h2 id="前置知识">前置知识</h2><p>所有内容只去介绍基本概念与使用，更加丰富的细节建议去看官方文档（或者直接AI）。docker文档<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">1</a></sup>、FastAPI<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">2</a></sup></p><h3 id="并发控制">并发控制</h3><p>对于高并发问题最简单理解方式：100个请求同时发送过来你如何进行处理这100个请求。可以串行解决（做完A做B）也可以并行处理（处理A的同时去处理B）</p><blockquote><p>高并发是一个复杂工程设计问题：比如说你的请求优先级高那么如何进行“插队”处理、你的视频内容很长直接下载放到内存中会导致内存问题、其他请求超时如何处理（比如处理1个请求10s超时设置时间是30s那么如果请求很多就会出现超时问题）等等</p></blockquote><p>下面主要去介绍常用的处理 <strong>高并发处理方式</strong>如通过异步/多线程/多进程进行处理、通过Redis队列进行控制、基于Ray原生的处理方式进行控制等等。</p><h4 id="异步多线程多进程">异步/多线程/多进程</h4><p><strong>首先程序任务主要为两类</strong>：<em>1、CPU 密集</em>：一直在”<strong>算</strong>“，CPU 满载（图片处理、模型推理、加密、大量计算）；<em>2、IO 密集</em>：大量时间在”<strong>等</strong>“（网络请求/读写文件）。而对于异步/多线程/多进程可以简单理解为（以餐厅服务多人为例）：一个服务员等菜的时候去别的桌子（<em>异步</em>）、多个服务员同时共用一个厨师（<em>多线程</em>）、直接开多家餐厅（<em>多进程</em>）</p><blockquote><p>进程好理解，在python有 GIL（全局解释器锁），同一时刻只有一个线程能执行 Python 代码，因此对于多线程和异步（两个都是处理IO密集的）使用“差异不大”，如果并发大（比如1w请求）可以考虑异步（不可能开1w个进程）、如果库是同步的就多线程（如request）</p></blockquote><p><strong>异步核心语法</strong><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">3</a></sup>，一般而言异步核心（一般而言涉及到高IO的可以用异步）就是如下几组语法：</p><pre><code class="language-python">import asyncio
# 声明协程函数
async def main():
    # 等待可以去执行其他任务
    await asyncio.sleep(3)
    # 创建 task，提交给事件循环调度执行（await 让出后才真正跑）
    task = asyncio.create_task(fetch())
# 创建 event loop 将main放进去进行循环调度
asyncio.run(main())
</code></pre><p>写异步操作过程中需要去注意：1、先去分析任务类型（如果是IO密集的），比如说使用fastapi中构建请求，就可以直接大胆的用异步比如说：</p><pre><code class="language-python">@app.get("/health")
async def health():
    ...
</code></pre><p>2、对于异步中其他语法可以简单理解：</p><ul><li><code>await</code>：等待一个异步任务执行完成，并在等待期间主动让出 CPU 控制权，让事件循环去执行其他协程，不过await后只能跟协程/task/future</li><li><code>asyncio.run</code>：启动整个异步程序，并创建事件循环（Event Loop）去执行指定协程</li><li><code>asyncio.create_task</code>：去创建多个task任务，比如说一般会有health任务去检查所有服务，那么可以直接通过此方法直接去创建多个task然后异步执行</li><li><code>asyncio.gather</code>：并发执行多个协程，并等待所有协程全部完成后统一返回结果</li></ul><h4 id="ray原始并发控制">Ray原始并发控制</h4><p>Ray 在 actor/task 层面提供了原生的并发与容错控制，核心参数都写在 <code>@ray.remote</code> 装饰器（或调用时的 <code>.options()</code>）里：</p><pre><code class="language-python">@ray.remote(
    num_gpus=0.25,          # GPU 配额:分数=多个 actor 分时共用一张卡(逻辑调度配额,非物理隔离)
    max_concurrency=4,      # actor 内并发上限:一个 actor 同时最多处理 4 个调用(线程池)
    max_restarts=3,         # actor 进程崩溃后最多自动重建 3 次
    max_task_retries=2,     # 单次调用失败后最多自动重试 2 次
)
class Model:
    def infer(self, x): ...
</code></pre><p><strong>并发控制原理</strong>：Ray 对 Actor 的并发控制分为两层——<code>调度层</code> 和 <code>执行层</code>。调度层由 Raylet + GCS 负责：当多个调用同时发往同一个 Actor 时，Raylet 先把它们放进该 Actor 的<code>内部任务队列</code>；执行层由 Actor 进程内的<code>线程池</code>（大小 = <code>max_concurrency</code>）消费这个队列——每个线程从队列取一个调用执行，执行完再取下一个，直到队列为空。</p><pre><code class="language-mermaid">sequenceDiagram
    participant C1 as 调用方 A
    participant C2 as 调用方 B
    participant C3 as 调用方 C
    participant RL as Raylet (本地调度)
    participant TQ as Actor 内部任务队列
    participant TP as Actor 线程池&lt;br/&gt;(max_concurrency=2)
    participant GPU as GPU

    C1-&gt;&gt;RL: actor.infer.remote(img1)
    C2-&gt;&gt;RL: actor.infer.remote(img2)
    C3-&gt;&gt;RL: actor.infer.remote(img3)

    RL-&gt;&gt;TQ: 三个调用依次入队

    Note over TP: 线程-1 取任务1&lt;br/&gt;线程-2 取任务2&lt;br/&gt;任务3 排队等待

    TP--&gt;&gt;GPU: 线程-1 执行推理
    TP--&gt;&gt;GPU: 线程-2 执行推理

    Note over GPU: GPU kernel 实际串行&lt;br/&gt;但预处理/后处理可重叠

    TP-&gt;&gt;C1: 返回结果1
    Note over TP: 线程-1 空闲 → 取任务3
    TP-&gt;&gt;C2: 返回结果2
    TP--&gt;&gt;GPU: 线程-1 执行推理 (任务3)
    TP-&gt;&gt;C3: 返回结果3
</code></pre><blockquote><p>关键理解：<code>max_concurrency</code> 只是决定了<strong>同时有几个线程在”伺候”这个 Actor</strong>，但 GPU 计算本身是串行的——多个线程的 GPU kernel 在 CUDA stream 上排队执行。所以 <code>max_concurrency</code> 的实际收益来自<strong>让 IO/预处理与 GPU 计算重叠</strong>（线程 A 等在 GPU 上时，线程 B 可以做图像解码），而不是让 GPU “同时算两件事”。</p></blockquote><p>几个要点：</p><ul><li><code>max_concurrency</code>：控制 <code>单个 actor 内部</code> 的并发。注意 Python GIL + GPU kernel 本身是串行的，设 <code>&gt;1</code> 主要让预处理/IO 与计算重叠，真正的 GPU 计算还是排队；对 <code>非线程安全</code> 的模型（比如 vLLM 引擎）必须设成 <code>1</code> 强制串行，否则并发进同一模型会崩。</li><li><code>num_gpus</code>：填分数（<code>&lt;1</code>）时多个 actor 会被打包到同一张卡<code>分时复用</code>，它是 Ray 的<code>调度配额</code>不是显存物理隔离，所以同卡上几个 actor 的显存要自己算好别超（比如 <code>0.2+0.5+0.3=1.0</code> 刚好一张卡）。</li><li><code>max_restarts</code> + <code>max_task_retries</code>：<code>容错</code>，actor 崩了 Ray 自动拉起、调用失败自动重试。</li><li>超时取消：<code>ray.get(ref, timeout=...)</code> 超时后可 <code>ray.cancel(ref)</code>，但 <code>同步方法一旦开跑就无法中断</code>，cancel 只能撤掉还没起跑的排队任务（所以超时了 GPU 其实还在跑，要靠限流从源头控制）。</li></ul><blockquote><p>Ray 原生并发是”actor 内”的并发，实际工程里常在它之上再加一层 <code>API 入口的信号量/限流做背压</code>：给每个模型建一个大小 = 它 <code>max_concurrency</code> 的信号量，满额直接拒绝（快速失败返回 <code>503</code>），而不是让请求堆在 Ray 队列里干等到超时——这样慢模型的积压不会白白空耗 GPU，超时也不会误触发<code>熔断</code>。</p></blockquote><h4 id="redis队列控制">Redis队列控制</h4><p>用 Redis 做队列本质是<code>削峰填谷</code>：请求先进队列缓冲，后端 worker 按自己的处理能力慢慢消费，避免瞬时高并发直接压垮推理服务。典型三个角色：</p><ul><li><code>生产者</code>（API 层）：收到请求把任务塞进 Redis 队列（<code>LPUSH</code>），立刻返回一个任务 id（异步任务模式，不阻塞等结果）。</li><li><code>消费者</code>（worker）：循环从队列阻塞取任务（<code>BRPOP</code>）处理，结果写回 Redis（<code>SET result:&lt;id&gt;</code>）。</li><li><code>客户端</code>：拿 id 轮询结果，或用 WebSocket 等服务端推送。</li></ul><p><strong>并发控制原理</strong>：Redis 队列实现的是<code>消费端拉取</code>模式——不是服务端主动推送，而是多个 worker 自己去抢任务，天然形成<code>竞争消费</code>。核心依赖 Redis List 的以下特性：</p><table><thead><tr><th style="text-align: center">命令</th><th style="text-align: left">行为</th><th style="text-align: left">并发中的作用</th></tr></thead><tbody><tr><td style="text-align: center"><code>LPUSH</code></td><td style="text-align: left">从左侧入队（生产者写）</td><td style="text-align: left">O(1)，瞬时写入不阻塞，抗住突发流量</td></tr><tr><td style="text-align: center"><code>BRPOP</code></td><td style="text-align: left">从右侧<strong>阻塞</strong>弹出（消费者取）</td><td style="text-align: left">队列空时阻塞等待，不空转 CPU；多个消费者同时 <code>BRPOP</code> 同一队列时，Redis <strong>单线程</strong>保证同一任务只会被一个消费者取走，不会有”抢到同一个任务”的问题</td></tr><tr><td style="text-align: center"><code>LLEN</code></td><td style="text-align: left">查看队列长度</td><td style="text-align: left">监控积压：队列持续增长说明消费跟不上，需要加 worker 或优化推理</td></tr></tbody></table><pre><code class="language-mermaid">sequenceDiagram
    participant P as 生产者 (FastAPI)
    participant R as Redis List&lt;br/&gt;"infer_queue"
    participant W1 as Worker 1
    participant W2 as Worker 2
    participant M as GPU (模型推理)
    participant S as Redis KV&lt;br/&gt;result:(id)

    Note over P,S: ═══ 正常消费（消费 &gt; 生产） ═══

    P-&gt;&gt;R: LPUSH task_1
    W1-&gt;&gt;R: BRPOP (阻塞等待)
    R-&gt;&gt;W1: 弹出 task_1
    W1-&gt;&gt;M: model.infer(task_1)
    M-&gt;&gt;W1: 推理结果
    W1-&gt;&gt;S: SET result:1

    Note over P,S: ═══ 多个 Worker 竞争消费 ═══

    P-&gt;&gt;R: LPUSH task_2
    P-&gt;&gt;R: LPUSH task_3

    par 竞争弹出
        W1-&gt;&gt;R: BRPOP → 拿到 task_2
        W2-&gt;&gt;R: BRPOP → 拿到 task_3
    end

    Note over W1,W2: Redis 单线程保证&lt;br/&gt;task_2 和 task_3 不会&lt;br/&gt;被同一个 Worker 取走

    par 并行推理
        W1-&gt;&gt;M: infer(task_2)
        W2-&gt;&gt;M: infer(task_3)
    end

    W1-&gt;&gt;S: SET result:2
    W2-&gt;&gt;S: SET result:3

    Note over P,S: ═══ 削峰场景（生产 &gt; 消费） ═══

    P-&gt;&gt;R: 瞬时 LPUSH 1000 个任务
    Note over R: 队列堆积 1000 条&lt;br/&gt;生产者已返回 task_id&lt;br/&gt;不阻塞、不丢请求

    loop 逐条消费
        W1-&gt;&gt;R: BRPOP → 取一条 → 推理 → 写结果
        W2-&gt;&gt;R: BRPOP → 取一条 → 推理 → 写结果
    end

    Note over R: 队列逐渐消化，最终归零
</code></pre><blockquote><p>关键理解：Redis 队列的并发能力来自 <strong>“单队列 + 多消费者竞争”</strong>——队列本身只是一条 List，没有复杂的锁或分片，但多个 Worker 同时 <code>BRPOP</code> 时，Redis 的单线程模型天然保证每条任务只被一个 Worker 取走，不需要额外的分布式锁。横向扩展只需要加 Worker 进程数即可，Worker 之间完全无状态、无协调开销。</p></blockquote><pre><code class="language-python">import redis
r = redis.Redis()

# 生产者:入队,立即返回 task_id
r.lpush("infer_queue", task_json)

# 消费者:阻塞取任务 --&gt; 推理 --&gt; 结果回写(存1小时)
_, task = r.brpop("infer_queue")
result = model.infer(task)
r.set(f"result:{task_id}", result, ex=3600)
</code></pre><p>它的价值：</p><ul><li><code>削峰</code>：1w 请求瞬时进来，队列缓冲，worker 不会被打爆；</li><li><code>限流/优先级</code>：用多个队列（高优先级队列先消费）就能实现前面说的“插队”；</li><li><code>解耦+横向扩展</code>：worker 可以多开几个进程/机器一起消费同一个队列。</li></ul><blockquote><p>生产上一般不自己撸队列，直接用 Celery / RQ（底层就是拿 Redis 当 broker）。这套模式最适合<code>耗时任务异步化</code>——比如前面提到的“长视频处理”，把同步 HTTP 变成“提交任务–&gt;轮询/回调拿结果”，请求端秒回不占连接。</p></blockquote><h4 id="增加节点副本数量">增加节点副本数量</h4><p>在使用 <a href="#ray原始并发控制">Ray进行并发控制</a>中单 actor 进程内推理串行（一个模型实例同时只跑一个请求），如果去提高并发数量可以直接 “扩充节点副本”（比如将开始的同一个功能的一个节点直接拓展到n个节点，通过这种方式同时也<strong>会带来显存的额外占用</strong>）其本质和多进程是相似的比如说下面代码：</p><pre><code class="language-python">import ray
from itertools import cycle

@ray.remote
class ModelA:
    def __init__(self): pass
    def output(self): return "A"

@ray.remote
class ModelB:
    def __init__(self): pass
    def output(self): return "B"

# 1) 配置里加副本数：(类, 副本数)
config = {
    "ModelA": (ModelA, 2),   # 起 2 副本
    "ModelB": (ModelB, 1),   # 单副本
}

ray.shutdown()
ray.init()

# 2) 命名展开 + 创建：N 副本注册为 name#0..name#N-1；1 副本沿用原名
def replica_names(name, n):
    return [name] if n &lt;= 1 else [f"{name}#{i}" for i in range(n)]

actors = {}                      # {注册名: handle}
replicas = {}                    # {逻辑名: [注册名...]}
for name, (cls, n) in config.items():
    names = replica_names(name, n)
    replicas[name] = names
    for ray_name in names:
        actors[ray_name] = cls.options(name=ray_name).remote()

# 3) 轮询分流：每个逻辑名一个游标，round-robin 选副本
cursors = {name: cycle(names) for name, names in replicas.items()}

def pick(name):
    return actors[next(cursors[name])]

# ---- 使用 ----
print(actors)                    # {'ModelA#0':.., 'ModelA#1':.., 'ModelB':..}

for _ in range(4):               # 连续 4 次调用 ModelA，轮流落到 #0/#1
    ref = pick("ModelA").output.remote()
    print(ray.get(ref))
</code></pre><h4 id="批处理">批处理</h4><p><strong>不去拓展额外的进程数量</strong>，可以通过批处理的方式提高并发，主要分为两种：1、<strong>动态批处理</strong>；2、<strong>连续批处理</strong>对于两种批处理方式解释如下：</p><ul><li><strong>1、动态批处理</strong></li></ul><p>对于同时输入的请求，可以尝试将这些请求进行合并组合到一起交给模型处理，比如说又yolo节点，同时输入3张图像（ABC）需要处理，如果是普通处理过程可能是：A-&gt;B-&gt;C，通过动态批处理则是直接将ABC三组图像组合交给模型处理（ <strong>简单理解为模型训练过程中batch_size</strong> ），唯一需要注意的是需要设定批处理上线防止OOM出现，比如说以Yolo节点补充动态批处理为例：</p><pre><code class="language-python">import asyncio
import ray
from ultralytics import YOLO


@ray.remote(num_gpus=0.25)
class YOLOActor:
    def __init__(self, model_path="yolov8n.pt", max_batch_size=8, max_wait_ms=10):
        self.model = YOLO(model_path)
        self.max_batch_size = max_batch_size
        self.max_wait = max_wait_ms / 1000.0
        self._queue = None
        self._worker = None

    def _ensure_worker(self):
        if self._queue is None:
            self._queue = asyncio.Queue()
            self._worker = asyncio.create_task(self._batch_loop())
            
    async def _batch_loop(self):
        while True:
            first = await self._queue.get()                    # 阻塞等第一个请求
            batch = [first]
            start = asyncio.get_running_loop().time()
            while len(batch) &lt; self.max_batch_size:
                remaining = self.max_wait - (asyncio.get_running_loop().time() - start)
                if remaining &lt;= 0:
                    break
                try:
                    batch.append(await asyncio.wait_for(self._queue.get(), remaining))
                except asyncio.TimeoutError:
                    break                                      # 窗口到,不再等
            await self._run_batch(batch)

    async def _run_batch(self, batch):
        paths = [b[0] for b in batch]
        confs = [b[1] for b in batch]
        futs = [b[2] for b in batch]
        batch_conf = min(confs)
        try:
            loop = asyncio.get_running_loop()
            # 前向是同步阻塞(但 torch 计算段释放 GIL),丢到线程池避免卡住攒批循环
            results = await loop.run_in_executor(
                None, lambda: self.model(paths, conf=batch_conf, verbose=False)
            )
            for res, conf, fut in zip(results, confs, futs):
                fut.set_result(self._postprocess(res, conf))   # 按各请求自己的 conf 过滤
        except Exception as e:
            for fut in futs:
                if not fut.done():
                    fut.set_exception(e)                       # 整批失败,逐个报错

    @staticmethod
    def _postprocess(res, conf):
        dets = []
        if res.boxes is not None:
            for box in res.boxes:
                c = float(box.conf[0])
                if c &lt; conf:                                   # 批用了最小 conf,这里按自身阈值筛
                    continue
                x1, y1, x2, y2 = box.xyxy[0].tolist()
                dets.append({
                    "bbox": [round(x1, 1), round(y1, 1), round(x2, 1), round(y2, 1)],
                    "class": res.names[int(box.cls[0])],
                    "conf": round(c, 4),
                })
        return dets
    
    async def infer(self, image_path: str, conf: float = 0.25):
        self._ensure_worker()
        fut = asyncio.get_running_loop().create_future()
        await self._queue.put((image_path, conf, fut))
        return await fut
ray.init()
actors = {"yolo": YOLOActor.options(name="yolo").remote()}
refs = [actors["yolo"].infer.remote(f"street{i}.jpg", conf=0.5) for i in range(16)]
for r in ray.get(refs):
    print(r)
</code></pre><p>对于上述批处理过程也比较简单：<strong>在每个节点内部创建队列在限制时间内的所有请求同时往队列中补充，在队满之后直接交给模型处理即可</strong></p><ul><li><strong>2、连续批处理</strong></li></ul><p>主要是用在大模型文本生成过程中，比如说一般大模型模型处理过程<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">4</a></sup>：<br/><img src="https://hfai-static.high-flyer.cn/static/28d7172c5c27e843bc31bb91aacac16c/e548f/02.png" alt=""/><br/> 同时输入4个请求只有在最长的请求处理完毕之后这个批次才算解释，那么就会造成较大浪费，比如请求3在 $T_5$ 就已经处理完毕但是必须等到 $T_8$才能完成输出。那么 <strong>连续批处理</strong>过程就是优化这点<br/><img src="https://hfai-static.high-flyer.cn/static/10738c93c0d5b194e103c4bd291b65b8/e548f/03.png" alt=""/><br/> 在 $S_3$ 在 $T_5$ 处理完毕之后下一秒直接将新的输入 $S_5$ 接着加入进来进行推理（保证时间利用最大化）。用下面列子解释过程，比如说bs=4其中ABC请求已经在生成处理，而请求E为新加入的请求其token长度为512，那么为了优化计算直接将4个请求就行拼接，那么此时输入形状为： <code>[3+512,4096]</code>，将输入拆分为不同的头（假设num_heads=32）那么：<code>[515, 32, 128]</code>，因为ABC这3个请求有KV-cache但是新的E是没有的那么：对于ABC这3组请求其KV为 <code>[L+1, 32, 128]</code>（其中L为历史长度）而E则是 <code>[512, 32, 128]</code></p><blockquote><p><strong>值得注意的是</strong>：KV维度和Q一致都是 <code>[1,32,128]</code> 但是因为attention计算需要“之前内容”，因此就会把缓存的KVcache直接拿出来就行拼接也就是得到了 <code>[L+1, 32, 128]</code></p></blockquote><h4 id="并发分析">并发分析</h4><p>除去上面介绍的几种方法，在并发控制中对于N卡还可以使用MPS<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">5</a></sup>或者MIG<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">6</a></sup>机制就行处理（需要注意不是所有的卡都支持），对于这两种机制都是都是让一张 GPU 被多个任务共享的机制。在模型部署推理过程中需要区分当前过程中的上限在哪里一般需要分析的有：<br/><img src="https://files.seeusercontent.com/2026/07/12/zI0t/20260712194241854.png" alt=""/></p><h3 id="fastapi">FastAPI</h3><p>可以简单理解为将你的程序“打包成服务”，别人可以直接通过端口去访问你的代码，一个最简单例子：</p><pre><code class="language-python">from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="fun-fastapi")

# 构建请求体
class SimpleComRequest(BaseModel):
    a: int = Field(description="实数A")
    b: int = Field(description="实数B")

@app.get("/v1/simple_com")
async def simple_com(request: SimpleComRequest):
    return request.a+ request.b
</code></pre><ul><li><code>@app.接口类型</code></li></ul><p>一般而言 <strong>接口类型</strong> 如下几种，使用比较多的也就是get和post前者一般是获取信息，后者是提交任务<br/><img src="https://files.seeusercontent.com/2026/06/11/2gXo/20260611215148357.png" alt=""/></p><ul><li><code>启动方式</code>：一帮而言启动方式有两种：<code>uvicorn main:app --port 8000 --host 127.0.0.1 --reload</code> 或者直接 <code>pyhton main.py</code>其中第一种通过 <code>--reload</code> 参数保证代码修改之后自动重启服务不用每次都去手动启动服务，一般启动之后可以直接访问 <code>127.0.0.1:8000/docs</code> 去看所有的请求以及参数</li><li><code>请求体</code>：可以简答理解为“函数参数”，别人调用你的服务用什么参数（对于参数越详细描述越好）</li></ul><p><strong>补充内容</strong>，除去上面使用的几种方式之外，实际开发过程中还会遇到如下几种场景：<br/><strong>1、实时交互</strong>，比如说我的模型都部署在服务器，但是我如果做实时视频监控识别/实时语音转录，此时就需要使用 <code>websocket</code> 功能（<strong>可以简单理解为双发不断“交换数据”协议</strong>），使用其核心作用在在于 前端、后端、算法端进行交互，以ASR（实时语音转录为例）对于这3端功能简要概述如下：后端负责不停地搬 + 给控制信号，算法负责在连续流里找句子的起止并出字，前端则是负责不断进行渲染。对于 <code>websocket</code>和前面的 <code>post</code> 请求方式类似：发送请求–&gt;模型处理–&gt;返回请求。那么大致伪代码如下（<strong>开发过程中需要注意前后算法确定彼此之间传递参数</strong>）：</p><blockquote><p><strong>音频处理过程主要流程</strong>：VAD（进行音频切分）–&gt;ASR（进行音频转译）–&gt;Punc（进行文本标点处理）</p></blockquote><pre><code class="language-python">from fastapi import APIRouter, WebSocket, WebSocketDisconnect
router = APIRouter(prefix="/voice", tags=["voice-stream"])

# 请求端处理
@router.websocket("/stream/asr")
async def stream_asr(ws: WebSocket):
    await ws.accept()
    start = await asyncio.wait_for(ws.receive(), 60) # 持续从 ws 中接收
    if ...:
        # 发送错误信号
        await ws.send_json({"type": "error"})
    session = SessionProcess(...)
    # 发送准备信号
    await ws.send_json({"type": "ready"})
    while True:
        msg = await asyncio.wait_for(ws.receive(), 60) # 接受信息
        data = msg.get("bytes")
        if data is not None:                      # 音频帧：边收边出 partial/final
            await session.feed(data)
            continue
        ...
        if t == "end":
            await _send_frames(ws, await session.finish())
            await ws.send_json({"type": "done"})
            break
# 算法端处理
class SessionProcess:
    def __init__(...):
        self._pending = bytearray()
        self.buf = bytearray() # 对于音频可能就需要去考虑进行缓存累加，比如说缓存 1s 音频再去进行ASR处理，对于图像可能使用队列更加方便。两种核心都是 消费-生产模型
        ...
    async def feed(self, pcm: bytes):
        """接受数据--&gt;缓存--&gt;处理---&gt;返回"""
        # 对于音频可能
        self.buf += pcm
        self._pending += pcm
        frames: List[dict] = []
        while len(self._pending)...:
            ...
            frames += await asr_process(...) # 主要返回字段 {"text":xxx}
        return frames
</code></pre><p>从上面代码可以发现对于 <code>websocket</code> 而言主要是如下几个功能：<strong>1、接收（信息）过程</strong>：直接通过<code>ws.accept()</code> 接收客户端消息，<code>ws.receive()</code>：接收消息，<strong>2、发送（信息）过程</strong>：发送和接受是相同的主要是发送2大类：<strong>文本帧</strong>以及 <strong>字节帧</strong>对于两者：</p><pre><code class="language-text"># 文本帧
{
    "type": "websocket.receive",
    "text": 'hello'
}
{
    "type": "websocket.receive",
    "text": '{"type":"start"}'
}
# 字节帧
{
    "type": "websocket.receive",
    "bytes": b'\x01\x02\x03\x04'
}
</code></pre><p>所以一般直接 <code>ws.receive</code> 而后后续直接 <code>msg.get("text")</code>或者 <code>msg.get("bytes")</code> 获取不同类型结果。</p><h3 id="docker">Docker</h3><blockquote><p>安装配置好docker之后运维国内监管可能需要去配置<a href="https://github.com/dongyubin/DockerHub">docker镜像</a></p></blockquote><p>一句话介绍Docker作用：<strong>避免每次开发因为不同的开发环境问题而去抓狂</strong>。在docker中核心就是3部分组成：1、镜像（image）；2、容器（container）；3、dockerfile。对于这三部分实际上你可以简单的把image理解为可执行程序，container就是运行起来的进程。那么写程序需要源代码，那么“写”image就需要dockerfile，dockerfile就是image的源代码，docker就是”编译器”。因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置，之后把dockerfile交给“编译器”docker进行“编译”，也就是docker build命令，生成的可执行程序就是image，之后就可以运行这个image了，这就是docker run命令，image运行起来后就是docker container。</p><blockquote><p><strong>简单总结起来就是</strong>：dockerfile定义如何构建、image是构建后的模板、container是模板运行后的实例。首先我们编写 Dockerfile，用来描述项目需要什么环境、依赖以及启动方式；然后根据 Dockerfile 构建出 Image（镜像）；最后通过 Image 启动 Container（容器）运行程序。Container 可以理解为一个相对独立、隔离的运行环境，因此能够避免“我电脑能跑，你电脑跑不了”的问题。</p></blockquote><p>如果要去<strong>打包一个docker</strong>需要执行的处理（直接AI分析去写所有的文件即可），<strong>1、写Dockerfile</strong>（去docker里面都需要执行哪些操作直接提前安排好运行），Dockerfile核心语法就是如下几个：<code>FROM</code>：使用的镜像比如说用到Python/Linux/cuda等版本信息、<code>WORKDIR</code>：设置工作目录、<code>COPY</code>：复制文件、<code>RUN</code>：执行命令（比如说<code>apt install</code>以及 <code>pip install</code>等命令）、<code>CMD</code>：相当于终端执行；<strong>2、docker-compose.yml文件</strong>；<strong>3、<code>.dockerignore</code></strong>。<br/> 除此之外一些<strong>docker常用的基本命令</strong>：</p><table><thead><tr><th style="text-align: center">docker命令</th><th style="text-align: center">语法</th></tr></thead><tbody><tr><td style="text-align: center">查看版本</td><td style="text-align: center"><code>docker --version</code></td></tr><tr><td style="text-align: center">镜像语法</td><td style="text-align: center">查看镜像：<code>docker images</code>、下载镜像：<code>docker pull nginx</code>、删除镜像：<code>docker rmi nginx</code> 、构建镜像：<code>docker build -t myapp .</code>（直接在又Dockerfile文件里面去构建一个镜像）</td></tr><tr><td style="text-align: center">容器语法</td><td style="text-align: center">启动容器：<code>docker run nginx</code>、查看运行的容器：<code>docker ps</code>、停止容器：<code>docker stop 容器ID</code>、删除容器：<code>docker rm my-nginx</code></td></tr><tr><td style="text-align: center">查看日志</td><td style="text-align: center"><code>docker logs my-nginx</code></td></tr><tr><td style="text-align: center">进入容器</td><td style="text-align: center"><code>docker exec -it my-nginx bash</code>（交互模式：-i、终端模式：-t）</td></tr></tbody></table><p>对于上述语法中启动容器 <code>docker run xxx</code>里面 <code>xxx</code>一般就是镜像名称，在构建镜像之后可以直接 <code>run</code>即可，一般而言<a href="https://www.runoob.com/docker/docker-run-command.html">run的参数</a>。所以一般而言Docker启动命令如下：</p><pre><code class="language-bash"># 1、首先构建docker镜像，直接基于本目录去构建
docker build -t xxx:xxx . # 其中 xxx:xxx 代表具体镜像名称 . 代表当前目录，也就是说基于当前文件夹 Dockerfile 去构建docker镜像

# 2、创建docker容器
docker run -d --name example \
  --env-file .env.example \
  -e PORT=59420 \
  -v /data_share/model:/data_share/model \
  -v /本地文件夹:/docker文件夹
  -p 59420:8080 \
  xxx:xxx
# example为具体容器名称 xxx:xxx 为镜像名称 -p 分别代表本地端口:docker端口，也就是说docker内部放行8080走本地59420去访问 -v 去挂载目录，一般就是项目代码/模型权重

# 3、容器使用
docker logs example # 查看日志
docker stop example # 停止容器
docker rm -f example # 停止容器
docker rmi xxx:xxx  # 删除镜像
docker exec -it &lt;容器名称或ID&gt; /bin/bash # 进入容器
</code></pre><p><strong>除去常用的docker语法</strong>，在使用过程中一般而言需要容器的“热重启”（本地修改–&gt;容器自动修改）因此在启动容器时候就需要将本地文件进行挂载（使用参数 “-v” 即可），对于 <code>Dockerfile</code> 中 <code>CMD</code> 一般使用过程中我的 <code>bash</code> 脚本会去使用部分参数比如在使用fastapi中去使用端口等，在启动容器时候只需要 <code>-e 脚本参数</code></p><table><tbody><tr><td><a href="# (```)">//</a>: # ( DISP ==&gt;</td><td>Ray RPC（跨机）</td><td>AC2)</td></tr></tbody></table><table><tbody><tr><td><a href="# (```)">//</a>: # ( DISP ==&gt;</td><td>Ray RPC（跨机）</td><td>AC3)</td></tr></tbody></table><table><tbody><tr><td><a href="# (```)">//</a>: # ( DISP ==&gt;</td><td>Ray RPC（跨机）</td><td>AC4)</td></tr></tbody></table><table><tbody><tr><td><a href="# (```)">//</a>: # ( DISP ==&gt;</td><td>Ray RPC</td><td>LLM)</td></tr></tbody></table><h2 id="ray">Ray</h2><p>一句话介绍Ray：<strong>主要是进行分布式计算 / 并行计算的开源框架</strong>，核心目标是：让你用“写本地 Python 的方式”，轻松把程序<strong>扩展到多台机器上运行</strong>（切记<em>如果服务不涉及到多台府服务器协同/不是高并发不一定要用Ray</em>）。<strong>不过</strong>Ray只负责管理核心计算还是Pytorch进行。 Ray 架构简单介绍，参考官方v2架构说明<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">7</a></sup>简单介绍Ray架构设计，其中Ray 的架构可以拆解为五个核心组件：<br/><img src="https://files.seeusercontent.com/2026/06/07/7Zrp/20260607214147926.png" alt=""/></p><h3 id="ray-核心组件">Ray 核心组件</h3><h4 id="1-node-组件head-node-与-worker-node">1. Node 组件：Head Node 与 Worker Node</h4><p>Ray 集群由两类节点组成：</p><ul><li><strong>Head Node（头节点）</strong>：集群的”大脑”。除了运行工作负载外，它还负责管理集群的全局状态（通过 GCS）、调度任务、运行 Dashboard 和 Autoscaler。它是集群中唯一知道所有节点信息的节点。</li><li><strong>Worker Node（工作节点）</strong>：集群的”手脚”。不参与全局决策，只负责接收 Head Node 分配的任务并执行。每个 Worker Node 上运行一个 Raylet 进程，负责本地的任务调度和资源管理。</li></ul><blockquote><p>可以把 Head Node 理解为建筑工地的总指挥，Worker Node 是各个工种的施工队——总指挥分配任务，施工队埋头干活。</p></blockquote><h4 id="2-scheduler调度器">2. Scheduler（调度器）</h4><p>Ray 采用<strong>自底向上的分布式调度</strong>策略，调度逻辑分散在两层：</p><ul><li><strong>Global Scheduler（全局调度器）</strong>：运行在 Head Node 的 GCS 中。它维护全局资源视图（哪个节点有空闲 GPU/CPU/内存），当本地调度器无法满足任务时接管调度。</li><li><strong>Local Scheduler（本地调度器，即 Raylet）</strong>：每个节点一个。优先在本地满足任务请求（减少数据传输），只有本地资源不足时才把任务”上报”给全局调度器。</li></ul><p>这种设计避免了传统中心化调度的瓶颈——大多数任务的调度决策在本地就完成了，延迟极低。</p><h4 id="3-object-store分布式对象存储">3. Object Store（分布式对象存储）</h4><p>Ray 基于 Apache Arrow / Plasma 实现了<strong>内存中的分布式对象存储</strong>，是 Ray 高性能的关键：</p><blockquote><p>举个直观的例子：Worker A 在节点 1 上产生了一个 10GB 的 tensor，Worker B 在同一节点上可以直接”看到”它，就像两个线程共享同一块内存一样。</p></blockquote><ul><li>每个节点上运行一个 Object Store 进程（<code>plasma_store</code>），通过共享内存提供零拷贝的数据访问。</li><li>同一节点上的多个 Worker 进程可以通过共享内存直接读写同一份对象，无需序列化/反序列化。</li><li>跨节点的数据传输是<strong>惰性的</strong>——只有当某个节点的 Worker 真正需要某个对象时，才会从其他节点拉取。</li></ul><h4 id="4-global-control-storegcs全局控制存储">4. Global Control Store（GCS，全局控制存储）</h4><p>GCS 是 Ray 的<strong>分布式键值存储</strong>，运行在 Head Node 上，是整个系统的”中枢神经系统”：</p><ul><li><strong>存储集群元数据</strong>：节点列表、存活状态、资源总量。</li><li><strong>存储系统状态</strong>：哪些 Actor 在哪个节点、函数定义、对象的位置信息（存储在哪个节点的 Object Store 上）。</li><li><strong>提供 Pub/Sub 机制</strong>：当某个事件发生时（如节点故障、Actor 创建/销毁），GCS 会通知订阅者。</li></ul><p>GCS 本身是一个 Redis-like 的 KV 存储，所有组件通过它与集群的”全局知识”交互。</p><h4 id="5-raylet">5. Raylet</h4><p>Raylet 是运行在<strong>每个节点</strong>上的守护进程（用 C++ 实现），是节点级的”管家”，负责三件事：</p><table><thead><tr><th>职责</th><th>说明</th></tr></thead><tbody><tr><td><strong>本地调度</strong></td><td>从 GCS 拉取任务队列，根据本地资源状态分配给 Worker</td></tr><tr><td><strong>资源管理</strong></td><td>跟踪本节点 CPU/GPU/内存的实时使用情况，向 GCS 汇报</td></tr><tr><td><strong>对象管理</strong></td><td>管理本地 Object Store 中的对象生命周期（引用计数、淘汰策略）</td></tr></tbody></table><p>Raylet 的设计理念是”让每个节点自治”——即使 Head Node 暂时不可达，Worker Node 上的 Raylet 依然能继续调度本地的任务和执行中的 Actor。<strong>简单任务介绍一下上述过程</strong>：比如说使用Ray进行<strong>贝叶斯调参过程</strong>，假设我们一块CPU假设32个核心我们并行4组参数执行，每组任务拥有5个核心，那么此时我们就有<strong>4个Worker node</strong>，<strong>Head Node内部处理过程</strong>：去控制每次贝叶斯下一次的参数（比如说RF中节点数量等）那么此时计算出4组优化参数（如n、depth等），通过<strong>GCS</strong>（比如说告诉目前迭代轮次、指标等） <strong>将任务写入队列</strong>，<strong>Scheduler</strong> 根据资源视图把任务分配到 4 个 Worker。每个<strong>Worker内部处理过程</strong> ：各 Worker 的 Raylet 分配 5 个核，启动训练 → 结果写入本地 Object Store。 <strong>Head Node</strong> 拿到 4 个准确率 → 更新 GCS 中的历史记录 → 贝叶斯优化器计算下一轮参数 → 循环。</p><blockquote><p>对于<strong>普通训练过程</strong>可能是：初始化参数–&gt;进行参数计算得到效果–&gt;贝叶斯优化器决定下一组参数–&gt;循环</p></blockquote><h3 id="ray-core">Ray Core</h3><h4 id="简单使用">简单使用</h4><p>Ray Core 是 Ray 的<strong>底层编程接口</strong>，上层的 Ray Data、Ray Train、Ray Tune 都建在它之上。它只做一件事：<strong>把”本地 Python”翻译成”集群上并行跑”</strong>。要理解它，先记住一个核心动作 <code>.remote()</code>：凡是加了它的调用都<strong>不阻塞</strong>，立刻返回一个 <code>ObjectRef</code>（可以理解为”取餐号”——任务还没好，但你先拿到了凭证），真正要结果时再用 <code>ray.get()</code> 凭号取餐。围绕这个动作，Ray 给了三个原语，解决三个递进的问题：</p><table><thead><tr><th style="text-align: center">原语</th><th style="text-align: left">一句话</th><th style="text-align: left">解决什么问题</th><th style="text-align: center">状态</th></tr></thead><tbody><tr><td style="text-align: center"><strong>Task</strong></td><td style="text-align: left">远程<strong>函数</strong></td><td style="text-align: left">把一次性计算丢到别的核/机器上并行跑</td><td style="text-align: center">无状态</td></tr><tr><td style="text-align: center"><strong>Actor</strong></td><td style="text-align: left">远程<strong>类实例</strong></td><td style="text-align: left">让”加载一次、反复调用”的东西常驻</td><td style="text-align: center">有状态</td></tr><tr><td style="text-align: center"><strong>Object</strong></td><td style="text-align: left">远程<strong>数据引用</strong></td><td style="text-align: left">让大数据在节点间零拷贝流转，不重复传</td><td style="text-align: center">——</td></tr></tbody></table><p><strong>1、Task：把函数变成可并行的任务</strong></p><p>最常见的需求：我有个函数要跑很多遍（不同参数），每遍互不相关，想并行。<code>@ray.remote</code> 就把普通函数变成”远程任务”。</p><pre><code class="language-python">import ray

@ray.remote                       # 装饰器：把函数注册为 Ray Task
def train_rf(n_estimators, max_depth):
    from sklearn.ensemble import RandomForestClassifier
    model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth)
    model.fit(X_train, y_train)
    return model.score(X_val, y_val)

# .remote() 发起任务：非阻塞，立刻返回 ObjectRef（取餐号）
refs = [train_rf.remote(n, d) for n, d in param_combinations]

# ray.get() 凭号取餐：阻塞，等所有结果回来
scores = ray.get(refs)
</code></pre><blockquote><p><strong>关键点</strong>：<code>for</code> 循环里的 4 个 <code>.remote()</code> 几乎瞬间执行完，因为它们只是”把任务提交出去”。4 个训练是在 4 个 Worker 上<strong>同时</strong>跑的，不是排队跑的。</p></blockquote><p>对应到架构：<code>@ray.remote</code> 的函数定义被 GCS 记录 → 调用 <code>.remote()</code> 时 Scheduler 挑一个空闲 Worker 执行 → 返回值放进 Object Store，<code>ray.get()</code> 时取回。<strong>何时用</strong>：无状态的批量计算——批量调参、批量数据处理、Map 阶段。</p><p><strong>2、Actor：把”加载一次”的东西常驻下来</strong></p><p>Task 的问题：<strong>它是无状态的，每次调用都从零开始</strong>。如果每个任务都要先 <code>YOLO("yolov8n.pt")</code> 加载几百 MB 模型再推理，加载开销会把你拖死。Actor 解决这个问题——它是一个<strong>有状态、长期活着的 Worker</strong>：模型在 <code>__init__</code> 里加载<strong>一次</strong>，常驻显存，之后每次 <code>infer</code> 直接用。</p><pre><code class="language-python">import ray
from ultralytics import YOLO

@ray.remote(num_gpus=0.25)          # 声明为 Actor，并分配 1/4 张 GPU
class YOLOActor:
    def __init__(self, model_path="yolov8n.pt"):
        self.model = YOLO(model_path)   # 只加载一次，常驻 GPU

    def infer(self, image_path: str, conf: float = 0.25):
        """对图片做目标检测，返回 bbox + 类别 + 置信度。"""
        results = self.model(image_path, conf=conf, verbose=False)
        dets = []
        if results[0].boxes is not None:
            for box in results[0].boxes:
                x1, y1, x2, y2 = box.xyxy[0].tolist()
                dets.append({
                    "bbox": [round(x1, 1), round(y1, 1), round(x2, 1), round(y2, 1)],
                    "class": results[0].names[int(box.cls[0])],
                    "conf": round(float(box.conf[0]), 4),
                })
        return dets
# 方式一
# 创建 Actor 实例：部署到某个 Worker，占用 0.25 张 GPU，模型在此刻加载
detector = YOLOActor.remote("yolov8n.pt")
# 调用方法：.remote() 仍然非阻塞，ray.get() 取结果
ref = detector.infer.remote("street.jpg", conf=0.5)
results = ray.get(ref)

# 方式二
ray.init()

model_class = {"yolo": YOLOActor}
actors = {}
for name, cls in model_class.items():
    # 将所有的节点加入到cluster中
    actors[name] = cls.options(name=name).remote()
model_a_ref = actors["yolo"].infer.remote("street.jpg", conf=0.5) # 传递参数给节点
print(model_a_ref, ray.get(model_a_ref)) # 获取节点结果
</code></pre><blockquote><p>对于通过ray修饰的类在获取结果上先通过</p></blockquote><p>Task vs Actor 的本质区别，看这张时序图（注意 <code>__init__</code> 只发生一次）：</p><pre><code class="language-text">脚本进程                              Ray Worker 进程 (持有 GPU)
   │                                          │
   ├─ detector = YOLOActor.remote(...) ──────→│ 创建 Actor，执行 __init__
   │                                          │   YOLO(...) 加载到 GPU（仅此一次）
   │                                          │   模型常驻，等待调用
   │                                          │
   ├─ ref_a = detector.infer.remote("a.jpg")─→│ 直接推理（不再加载模型）
   ├─ ref_b = detector.infer.remote("b.jpg")─→│ 排队（受 max_concurrency 限制）
   │                                          │
   ├─ ray.get(ref_a) ←────────────────────────┤ 从 Object Store 取结果
   └─ ray.get(ref_b) ←────────────────────────┤
</code></pre><p>对应到架构：Actor 的生命周期由 GCS 管理，所在节点的 Raylet 为它分配资源、调度它的方法调用。<strong>何时用</strong>：需要”加载一次、反复用”的有状态场景——模型推理服务、维护计数器/缓存、有内部状态的流式处理。</p><p><strong>3、Object：让数据在集群里零拷贝流转</strong></p><p>前两个原语传的是”任务”，这个原语传的是”<strong>数据</strong>“。问题场景：你有个 10GB 的数据集，要被 4 个 Task 共用。如果每次 <code>.remote(dataset)</code> 都把它序列化、复制一份传过去，4 份 40GB，内存直接爆。<code>ray.put()</code> 把数据放进 Object Store <strong>一次</strong>，返回一个 <code>ObjectRef</code>（指针），之后所有任务传这个<strong>引用</strong>就行，同节点的 Worker 直接共享内存读，零拷贝。</p><pre><code class="language-python">data_ref = ray.put(large_dataset)        # 放入 Object Store，返回引用（只存一份）

# 把"引用"传给任务，而不是数据本身
refs = [train_rf.remote(data_ref, params) for params in grid]
results = ray.get(refs)                   # 同节点零拷贝；跨节点才惰性拉取
</code></pre><blockquote><p>其实你前面用 <code>ray.get(refs)</code> 取 Task / Actor 的结果时，背后就是 Object Store 在工作——<strong>所有 <code>.remote()</code> 的返回值都自动存在 Object Store 里</strong>。<code>ray.put</code> 只是让你手动把”输入数据”也放进去。</p></blockquote><p>对应到架构：这就是前面 Object Store 组件讲到的——同节点共享内存零拷贝，跨节点惰性传输（谁用到才拉）。<strong>何时用</strong>：多个任务共享同一份大数据（数据集、大权重、配置），避免重复传输。<br/><strong>一句话总结 Ray Core</strong>：<code>@ray.remote</code> 把<strong>函数</strong>变 Task、把<strong>类</strong>变 Actor；<code>ray.put/get</code> 让<strong>数据</strong>通过 <code>ObjectRef</code> 在集群里透明流转。三者都靠 <code>.remote()</code> 异步发起、<code>ray.get()</code> 同步取回。理解了这三者，就理解了 Ray 的编程基础。除此之外，实际使用还绕不开两个配套能力：<br/><strong>1、资源分配</strong>，在 <code>@ray.remote</code> 装饰器里直接声明资源占用，如 <code>@ray.remote(num_gpus=0.5, num_cpus=2)</code>。注意 <code>num_gpus=0.5</code> 是<strong>软配额</strong>——Ray 只负责”按比例调度”，不强制隔离显存，靠你自己保证同卡上多个 Actor 显存加起来不超。<a href="https://www.aidoczh.com/ray/ray-core/api/doc/ray.remote.html">支持的参数配置</a><br/><strong>2、并发控制</strong>，Actor 默认<strong>串行</strong>执行方法（一次一个），用 <code>@ray.remote(max_concurrency=N)</code> 可让单个 Actor 同时处理 N 个请求——配合上面的软配额，同一张卡上多个服务就能真正并发吃满 GPU。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:3" role="doc-endnote"><p><a href="https://docs.docker.com/get-started/get-docker/">https://docs.docker.com/get-started/get-docker/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://fastapi.tiangolo.com/zh/python-types/">https://fastapi.tiangolo.com/zh/python-types/</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://docs.python.org/zh-cn/3/howto/a-conceptual-overview-of-asyncio.html#a-conceptual-overview-of-asyncio">https://docs.python.org/zh-cn/3/howto/a-conceptual-overview-of-asyncio.html#a-conceptual-overview-of-asyncio</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://www.anyscale.com/blog/continuous-batching-llm-inference">https://www.anyscale.com/blog/continuous-batching-llm-inference</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:8" role="doc-endnote"><p><a href="https://docs.nvidia.com/deploy/mps/latest/quick-start.html">https://docs.nvidia.com/deploy/mps/latest/quick-start.html</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/latest/index.html">https://docs.nvidia.com/datacenter/tesla/mig-user-guide/latest/index.html</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://docs.google.com/document/d/1tBw9A4j62ruI5omIJbMxly-la5w4q_TjyJgJL_jN2fI/preview?tab=t.0#heading=h.iyrm5j2gcdoq">Ray v2 Architecture</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>pytorch</category><pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/06/05/RayArchUse.html</guid></item><item><title>Claude Code安装使用及Agent相关架构问题分析</title><link>https://www.big-yellow-j.top/posts/2026/05/17/ClaudeCode.html</link><description><![CDATA[在Windows上通过WSL安装Claude Code是推荐方式，支持桌面端和终端安装，需配置环境变量并处理VPN节点或全局代理以解决安装报错。Skills分为项目目录和根目录，可自动触发（基于description字段）或手动触发（/命令联想），安装他人Skills可通过复制文件夹或执行npx命令。自定义Skills需在SKILL.md中定义name、description、when_to_use等YAML头部字段，结合prompt和脚本实现function calling机制。Agent Team模式允许任务拆分给多个子Agent并行执行，子Agent各自拥有独立上下文窗口。抓包分析...]]></description><content:encoded><![CDATA[<p>本文详细介绍Claude Code在windows上安装使用，对于一般非计算机行业用户或者对底层技术了解兴趣不深的可以只看：1、第一部分Claude Code简单使用；2、Claude Code进阶使用；3、Claude Code底层原理中的Skills开发。</p><h2 id="claude-code简单使用">Claude Code简单使用</h2><h3 id="安装环境准备">安装环境准备</h3><p>目前大部分的skill或者说Clade Code运行任务（比如说读取文件等）都会用到python，因此就需要去安装python，除此之外如果要去安装一些skills还需要权重npm等，因此：<strong>在win电脑上更加建议直接使用wsl去搭建Claude Code</strong>，<a href="https://www.runoob.com/linux/windows-wsl-linux.html">WSL安装方式</a>，安装完毕之后其他命令就和linux安装命令相同，先去介绍基于WSL安装claude过程，执行如下命令：</p><blockquote><p>对于配置DeepSeek：<a href="#配置其他api">配置DeepSeek订阅</a></p></blockquote><pre><code class="language-bash"># ========== 1. 基础系统依赖 ==========
sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install -y build-essential python3 python3-pip python3-venv curl wget unzip git-all

# ========== 2. 安装 nvm + Node.js ==========
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
# 重新加载 shell 配置
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; \. "$NVM_DIR/nvm.sh"
nvm install 26
node -v
npm -v
npm config set registry https://registry.npmmirror.com

# ========== 3. 安装 Miniconda ==========
wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86.sh
bash Miniconda3-latest-Linux-x86.sh -b   # -b 静默安装，跳过交互
~/miniconda3/bin/conda init bash
source ~/.bashrc

# ========== 4. 安装 Claude Code ==========
curl -fsSL https://claude.ai/install.sh | bash
# 确保 PATH 包含 ~/.local/bin（只添加一次）
grep -q 'export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc || \
  echo 'export PATH="$HOME/.local/bin:$PATH"' &gt;&gt; ~/.bashrc
source ~/.bashrc

# ========== 5. 配置 DeepSeek API ==========
mkdir -p ~/.claude
cat &gt; ~/.claude/settings.json &lt;&lt; 'EOF'
{
  "env": {
    "ANTHROPIC_AUTH_TOKEN": "sk-你的DeepSeek-API-Key",
    "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro",
    "ANTHROPIC_MODEL": "deepseek-v4-pro"
  },
  "autoUpdatesChannel": "latest",
  "theme": "dark"
}
EOF

# ========== 6. 安装 rtk ==========
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh
source ~/.bashrc
rtk init --global
</code></pre><p>对于WSL后续使用直接在终端里面输入 <code>wsl</code> 然后输入 <code>claude</code> 即可</p><h3 id="安装与卸载">安装与卸载</h3><blockquote><p>⭐<strong>在win电脑上更加建议直接使用wsl去搭建Claude Code</strong>，<a href="https://www.runoob.com/linux/windows-wsl-linux.html">WSL安装方式</a>，安装完毕之后其他命令就和linux安装命令相同</p></blockquote><p><strong>以win电脑为例</strong>按照官方过程直接输入命令即可，对于<strong>桌面端安装</strong>直接访问<a href="https://code.claude.com/docs/en/desktop">链接</a>然后安装即可，对于<strong>终端安装</strong>参考<a href="https://code.claude.com/docs/zh-CN/terminal-guide">链接</a>首先安装 <a href="https://git-scm.com/install/windows"><code>git</code></a>然后直接终端（win+r 然后输入 cmd）执行安装即可：<code>curl -fsSL https://claude.ai/install.cmd -o install.cmd &amp;&amp; install.cmd &amp;&amp; del install.cmd</code>或者直接：<br/><img src="https://files.seeusercontent.com/2026/05/19/vg0U/20260518221435773.webp" alt="20260518221435773"/><br/> 打开 PowerShell然后输入安装命令：<code>irm https://claude.ai/install.ps1 | iex</code><br/><strong>处理报错</strong>，如果遇到如下报错：<br/><img src="https://files.seeusercontent.com/2026/05/19/2xMq/20260518215550368.webp" alt="20260518215550368"/><br/><strong>解决措施</strong>：（理论上）直接将VPN节点换到美国即可<br/><strong>处理报错</strong>，如果遇到报错 <code>Failed to fetch version from https://downloads.claude.ai/claude-code-releases/latest: ECONNREFUSED</code><br/><img src="https://files.seeusercontent.com/2026/05/19/Nag5/20260518215850658.webp" alt="20260518215850658"/><br/><strong>解决措施</strong>：将VPN开启虚拟网卡模式或者直接将VPN开全局代理即可<br/> 最后终端中出现如下界面表示安装完成<br/><img src="https://files.seeusercontent.com/2026/05/19/dBy8/20260518220850516.webp" alt="20260518220850516"/><br/> 值得注意的是里面提到：<code>Native installation exists.....</code>，这是因为没有<strong>配置好系统环境变量</strong>，直接<code>win+r</code>然后输入 <code>sysdm.cpl</code><br/><img src="https://files.seeusercontent.com/2026/05/19/F4ug/20260518221208684.webp" alt="20260518221208684"/><br/> 点击确认即可完成环境变量配置处理。然后终端直接输入 <code>claude</code><br/><strong>卸载过程</strong>就比较简单直接去删除对应文件即可，<strong>为了节约token</strong>选择直接<a href="https://github.com/rtk-ai/rtk/blob/develop/README_zh.md">安装</a><code>rtk</code>，首先<a href="https://github.com/rtk-ai/rtk/releases/download/dev-0.41.0-rc.227/rtk-x86_64-pc-windows-msvc.zip">下载对应文件</a>并且解压（<strong>记住解压的路径</strong>），解压完成之后将对应文件添加到环境变量中（过程和上面配置claude code环境变量相同），然后 <code>rtk.exe init --global</code>（如果不行就直接输入<code>D:\ClaudeCode\rtk.exe init --global</code>，前面的路径为你解压路径）运行之后就可以开启节约token，值得注意的是如果环境变量添加了，终端输入 <code>rtk</code> 没有效果可以直接去 <code>C:\Users\hjie\.claude\settings.json</code> 里面修改配置，将里面的hook修改为<br/><img src="https://files.seeusercontent.com/2026/05/19/4bCo/20260519211105815.webp" alt="20260519211105815"/></p><h3 id="配置其他api">配置其他API</h3><p>以<a href="https://platform.deepseek.com/usage">DeepSeek</a>配置过程为例，打开目录 <code>C:\Users\hjie\.claude</code> 然后<br/><img src="https://files.seeusercontent.com/2026/05/19/bG7z/20260518222937445.webp" alt="20260518222937445"/></p><pre><code class="language-json">{
  "env": {
    "ANTHROPIC_AUTH_TOKEN": "sk-",
    "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro",
    "ANTHROPIC_MODEL": "deepseek-v4-pro"
  },
  "autoUpdatesChannel": "latest",
  "theme": "dark"
}
</code></pre><p>在配置完毕之后，打开 Powershell然后执行 <code>claude</code><br/><img src="https://files.seeusercontent.com/2026/05/19/qy1O/20260518223208225.webp" alt="20260518223208225"/></p><h3 id="基础命令">基础命令</h3><p><img src="https://files.seeusercontent.com/2026/05/19/fj9N/20260518224707197.webp" alt="20260518224707197"/><br/> 在终端中所有的命令可以直接输入 <code>/</code> 来进行执行比如说切换模型 <code>/model</code> 然后按不同箭头选择模型（enter 回车选择模型），或者直接使用 <code>@</code> 去访问文件等，简单使用过程中一般而言用的比较多的几个命令：1、<code>/compact</code>：去压缩对话，一般而言模型上下文有限去对上下文进行压缩</p><pre><code class="language-cmd">&gt; /compact
# 基本压缩：将历史对话总结为关键要点
&gt; /compact 保留认证模块的变更细节和当前测试失败信息
# 定向压缩：指定需要保留的重点内容
</code></pre><p>2、<code>clear</code>：完全重置，清空当前会话的全部对话历史和上下文，回到初始状态；3、<code>/context</code>查看上下文使用，显示当前上下文的占用情况和分类统计。除了基础命令之外有些时候还需要去控制claude 在执行一些 <strong>简单安全任务</strong>（比如说去搜索整理文件这些任务）在启动时候可以直接开启 <code>claude --permission-mode auto</code><strong>强制自动判断需不要询问用户</strong>（一定要让他执行可信任务，不然可能删除一些重要东西导致出现错误，可以先去跑通一个任务之后，后续再去实现类似任务可以直接开启强制自动判断）或者直接在对话系统里面按键 <code>shift+tab</code>进行切换（直接切换到 <code>auto mode on</code>即可）：<br/><img src="https://files.seeusercontent.com/2026/05/20/Y3oc/20260520215009426.webp" alt="20260520215009426"/><br/> 而后再去执行 <code>/sandbox</code>（建立一个 <strong>沙盒</strong>隔离环境避免直接操作自己电脑导致误删文件等）<br/><img src="https://files.seeusercontent.com/2026/05/20/2pNt/20260520225631202.webp" alt="20260520225631202"/><br/> 除此之外要<strong>监控各类task任务</strong>（比如说有些python自动脚本会到建立task）进行情况可以直接安装：<code>npx claude-task-viewer</code>然后就可以看到正在进行进程：<br/><img src="https://files.seeusercontent.com/2026/05/20/Cil9/20260520212935955.webp" alt="20260520212935955"/><br/> 除此之外有些时候可能需要<a href="https://code.claude.com/docs/zh-CN/agent-view">一次性进行多个任务</a>（比如说去搜索A相关事情、搜索B相关事情）可以执行<code>claude agents</code>，这样就可以快速进行多组对话进行切换（或者直接在对话系统里面直接按箭头左右也可以）</p><h3 id="插件安装">插件安装</h3><p>比如说安装<a href="https://github.com/jarrodwatts/claude-hud/blob/main/README.zh.md">用量检查插件</a>或者安装superpowers：<code>/plugin install superpowers@claude-plugins-official</code></p><h3 id="mcp">MCP</h3><p><em>MCP简单理解为赋予Claude Code工具调用权限，比如说想让Claude Code去收索文书等</em><br/> 比如说安装<code>playwright</code>（直接用自然语言描述你想做的事，而后调用对应的浏览器操作）：<code>claude mcp add playwright npx @playwright/mcp@latest</code>如果要卸载直接<code>claude mcp remove playwright</code>，安装完毕之后<br/><img src="https://files.seeusercontent.com/2026/05/20/7Ame/20260520163227086.webp" alt=""/><br/> 可以看到MCP已经启动了，如果要去使用这个MCP直接 <code>使用 playwright 打开浏览器访问 https://www.big-yellow-j.top/posts/2026/04/20/torch-basic-distribute-1.html 并且检查还有什么需要补充的分布式训练方式，以及内容上还有什么不足</code> 去让Claude Code去调用playwright的MCP。</p><h3 id="skills使用">Skills使用</h3><p><em>skills简单理解为一个可以复用的任务prompt（比如说创建PPT）不用每次都去重新写prompt直接通过skills进行复用即可</em>，如果要去找其他的Skills直接可以访问：<a href="https://www.skills.sh/">https://www.skills.sh/</a>或者直接去安装<a href="https://www.skills.sh/vercel-labs/skills/find-skills">find-skills</a>让其帮你自动去搜索一些skills，</p><blockquote><p>安装前简单了解一下skills有“两个目录”：<strong>1、项目目录</strong>（那么你的skills就只在这个项目起到效果），比如直接在你的文件夹里面进行打开就会访问项目目录比如说 <code>D:\ClaudeCode\.claude</code>；<strong>2、根目录</strong>（所有项目都可以用到这个skills），这个就是你的claude code安装目录比如说：<code>C:\Users\hjie\.claude</code></p></blockquote><p><strong>第一种、安装他人skills</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>有如下几种：<strong>1、复制文件夹</strong>直接将别人skills所有内容复制到skills目录下即可，<strong>2、直接下载</strong>，以获取微信文章为例直接访问<a href="https://www.skills.sh/">地址</a>然后搜索<code>wechat-article-extractor</code>在得到安装命令<code>npx skills add https://github.com/freestylefly/wechat-article-extractor-skill --skill wechat-article-extractor</code>在你的终端进行安装即可，安装完过程中可能要选择是 project 还是 global 根据提示进行选择即可</p><blockquote><p>需要安装npm，安装方式参考：<a href="https://www.cnblogs.com/liushunli/p/18663191">链接</a></p></blockquote><p>安装完毕之后skills触发有两种：<strong>1、自动进行触发</strong>，比如在上述skills中有description字段<br/><img src="https://files.seeusercontent.com/2026/05/19/Yb8y/20260519214202838.webp" alt="20260519214202838"/><br/> 当你在claude中输入 <code>提取微信文章：https://mp.weixin.qq.com/s/8axsDd-vY247Nd3oPZ9_zQ</code> 他就会自动触发这个skills进行处理<br/><img src="https://files.seeusercontent.com/2026/05/19/c7Nx/20260519214401207.webp" alt="20260519214401207"/><br/><strong>2、手动触发</strong>，可以直接在 claude中输入 ‘/we’ 会在下面触发联想，然后通过箭头选择自己需要内容直接按 <code>tab</code> 补全命令<br/><img src="https://files.seeusercontent.com/2026/05/19/9Wma/20260519214750902.webp" alt="20260519214750902"/><br/><strong>第二种、自定义skills</strong>：直接去看别人skills怎么写的然后进行修改即可，或者直接让claude code写skills</p><h4 id="搜索自动化">搜索自动化</h4><p>一般而言在工作中可能需要去调研（比如说搜集论文、收集相关数据集等），推荐直接使用 <a href="https://github.com/unclecode/crawl4ai">crawl4ai</a> 去自动搜索爬取内容，可以直接使用他的<a href="https://docs.crawl4ai.com/assets/crawl4ai-skill.zip">skills</a>然后放到自己的目录下面，然后再终端中执行<code>claude --permission-mode auto</code>，然后输入自己要求：<br/><img src="https://files.seeusercontent.com/2026/05/20/Dts8/20260520223001394.webp" alt="20260520223001394"/><br/> 最后输出结果（任务比较费时用来12min52s完成）<br/><img src="https://files.seeusercontent.com/2026/05/20/B9jx/20260520223828636.webp" alt="20260520223828636"/><br/> 通过上面一轮对话处理下来token消耗<br/><img src="https://files.seeusercontent.com/2026/05/20/kRv5/20260520224434337.webp" alt="20260520224434337"/></p><h3 id="agent-team协作">Agent Team协作</h3><p>一般而言在使用claude code进行对话时候，都是执行一个任务，在等待结果之后再去执行一个新的任务，比如说法律一个案子需要同时去收集案子以及法条然后再去对材料进行整理，这里就需要用到<strong>agent team概念</strong>，简单介绍Agent team概念参考里面对于subagents和agent teams之间对比<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">2</a></sup>：<br/><img src="https://files.seeusercontent.com/2026/05/22/V9ek/20260522220431712.png" alt="20260522220431712"/><br/> 两者之间差异点在于subagents相当于一个老师将任务分配给几位学生然后学生给老师一个反馈，agent teams相当于老师给学生分配任务，学生之间还在不断交流，那么以<a href="https://news.cctv.com/2021/09/30/ARTIUszTVNT8vTB7EhBbgvgT210930.shtml">长沙货拉拉案</a>为例，以一个法律从业者出发（本人非专业人员）去构建一个agent team来解决这个案子，<strong>首先</strong>本地创建一个文件夹然后执行claude code</p><pre><code class="language-bash">mkdir CSHuolala # 创建文件夹
mkdir .claude   # 创建文件夹
echo '{
  "env": {
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
  }
}' &gt; /home/hjie/ClaudeCode/CShuolala/.claude/settings.json # 相当于写入文件
 claude --permission-mode auto --teammate-mode in-process  # teammate-mode 控制显示模式

# 测试的提示词是
为货拉拉案法庭辩护材料准备材料辩护词
1、任务 A：证据审查与现场环境重构（由 Evidence-Agent 认领）
2、任务 B：刑法因果关系与无罪/罪轻辩护词起草（由 Criminal-Law-Agent 认领）
3、任务 C：被害人过错分析及同行案例检索（由 Case-Search-Agent 认领）
4、任务 D：最后对辩护词进行审查（有Lawer-Agent认领）
执行顺序上首先执行任务A而后并行执行B和C，最后执行D
辩护词合适标准：1、辩护词合乎法律文书书写；2、所有收集材料都在本地文件夹保存；3、法律必需都有自己出处
</code></pre><blockquote><p>值得注意的是上面任务都是基于网络进行搜索，实际情况可能需要对自己具体材料（比如说收集的材料）进行处理，可能就需要用到其他skills/mcp工具，<strong>在规划任务时候越详细越好</strong>（更加好控制模型）</p></blockquote><p><img src="https://files.seeusercontent.com/2026/05/22/r7Ak/20260522223624142.png" alt=""/><br/> 可以看到目前之后一个 <code>Evidence-Agent</code> 在执行工作，可以直接通过键盘↓去进入 <code>@main @Evidence-Agent</code>然后键盘←→箭头进行跳装查看工作如何，或者直接通过<code>npx claude-task-viewer</code>然后进入对应网页查看如下：<br/><img src="https://files.seeusercontent.com/2026/05/22/9Ttd/20260522224446512.png" alt=""/><br/> 那么在<code>Evidence-Agent</code>中执行效果如下：<br/><img src="https://files.seeusercontent.com/2026/05/22/cG0g/20260522224004375.png" alt=""/><br/> 很明显看到执行 <em>争取审查+任务重构</em>符合我的任务A要求，A执行完毕就会进行BC比如说得到如下：<br/><img src="https://files.seeusercontent.com/2026/05/22/Na8o/20260522224706671.png" alt=""/><br/> 可以看到另外两个agent也开始执行了，<strong>最后</strong>所有的整理的材料以及最后的书写得到的辩护词在<a href="https://pan.baidu.com/s/1wzkkb8n_HqIcmLZqeynuYA?pwd=ve8w">百度网盘</a>总共是花费了大概4元（DeepSeek-4-pro）</p><h2 id="claude-code工具开发">Claude Code工具开发</h2><h3 id="skills开发">Skills开发</h3><blockquote><p><strong>最简单方法直接看别人怎么写然后进行仿写即可</strong></p></blockquote><p>以crawl4ai提供的skills为例，参考这个例子<strong>规范文书写作skills</strong>（<strong>大部分内容基于AI出发，不一定符合实际法律工作内容</strong>），首先介绍如何去定义自己的skills而后去介绍在claude code中如何去定义自己的skills，比如说在<a href="https://docs.crawl4ai.com/assets/crawl4ai-skill.zip">crawl4ai</a>文件中<strong>主要是如下几个文件</strong><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">3</a></sup>（不同skills可能差异，比如crawl4ai可能还有references）：</p><pre><code class="language-bash">my-skill/
├── SKILL.md           # 主要说明（必需）
├── template.md        # Claude 要填写的模板
├── reference.md       # 详细介绍API使用文档，比如说在scripts里面会提前告诉我的模型运行scripts需要哪些参数
├── examples/
│   └── sample.md      # 显示预期格式的示例输出
└── scripts/
    └── validate.sh    # Claude 可以执行的脚本
</code></pre><p>首先核心内容就集中在 <code>SKILL.md</code>文件中，简单介绍一下他的 <strong>模板</strong>描写方式：</p><pre><code class="language-markdown">---
name: api-conventions
description: API design patterns for this codebase
---

When writing API endpoints:
- Use RESTful naming conventions
- Return consistent error formats
- Include request validation
</code></pre><p><strong>首先</strong>对于上述模板中 <code>---</code> 之间内容表示提前告诉我的模型这个 <code>skills</code>的一些基本配置，他一般有如下字段：</p><table><thead><tr><th style="text-align: center">字段</th><th style="text-align: center">必需</th><th style="text-align: left">描述</th></tr></thead><tbody><tr><td style="text-align: center">name</td><td style="text-align: center">否</td><td style="text-align: left">Skill 的显示名称。<strong>如果省略，使用目录名称</strong>。仅小写字母、数字和连字符（最多 64 个字符）。</td></tr><tr><td style="text-align: center">description</td><td style="text-align: center">推荐</td><td style="text-align: left">Skill 的功能以及何时使用它。Claude 使用它来决定何时应用该 skill。如果省略，使用 markdown 内容的第一段。前置关键用例：组合的 description 和 when_to_use 文本在技能列表中被截断为 1,536 个字符以减少上下文使用。</td></tr><tr><td style="text-align: center">when_to_use</td><td style="text-align: center">否</td><td style="text-align: left">关于 Claude 何时应该调用该 skill 的额外上下文，例如触发短语或示例请求。附加到技能列表中的 description，并计入 1,536 个字符的上限。</td></tr><tr><td style="text-align: center">argument-hint</td><td style="text-align: center">否</td><td style="text-align: left">自动完成期间显示的提示，指示预期的参数。示例：[issue-number] 或 [filename] [format]。</td></tr><tr><td style="text-align: center">arguments</td><td style="text-align: center">否</td><td style="text-align: left">用于 skill 内容中 $name 替换的命名位置参数。接受空格分隔的字符串或 YAML 列表。名称按顺序映射到参数位置。</td></tr><tr><td style="text-align: center">disable-model-invocation</td><td style="text-align: center">否</td><td style="text-align: left"><strong>设置为 true 以防止 Claude 自动加载此 skill</strong>。用于你想使用 /name 手动触发的工作流。也防止该 skill 被预加载到 subagents 中。默认值：false。</td></tr><tr><td style="text-align: center">user-invocable</td><td style="text-align: center">否</td><td style="text-align: left">设置为 false 以从 / 菜单中隐藏。用于用户不应直接调用的背景知识。默认值：true。</td></tr><tr><td style="text-align: center">allowed-tools</td><td style="text-align: center">否</td><td style="text-align: left">当此 skill 处于活动状态时，Claude 可以使用而无需请求权限的工具。接受空格分隔的字符串或 YAML 列表。</td></tr><tr><td style="text-align: center">model</td><td style="text-align: center">否</td><td style="text-align: left">当此 skill 处于活动状态时要使用的模型。覆盖适用于当前轮的其余部分，不保存到设置；会话模型在你的下一个提示时恢复。接受与 /model 相同的值，或 inherit 以保持活动模型。</td></tr><tr><td style="text-align: center">effort</td><td style="text-align: center">否</td><td style="text-align: left">当此 skill 处于活动状态时的工作量级别。覆盖会话工作量级别。默认值：继承自会话。选项：low、medium、high、xhigh、max；可用级别取决于模型。</td></tr><tr><td style="text-align: center">context</td><td style="text-align: center">否</td><td style="text-align: left">设置为 fork 以在分叉的 subagent 上下文中运行。</td></tr><tr><td style="text-align: center">agent</td><td style="text-align: center">否</td><td style="text-align: left">当设置 context: fork 时要使用的 subagent 类型。</td></tr><tr><td style="text-align: center">hooks</td><td style="text-align: center">否</td><td style="text-align: left">限定于此 skill 生命周期的 hooks。有关配置格式，请参阅 Skills 和代理中的 Hooks。</td></tr><tr><td style="text-align: center">paths</td><td style="text-align: center">否</td><td style="text-align: left">Glob 模式，限制何时激活此 skill。接受逗号分隔的字符串或 YAML 列表。设置后，Claude 仅在处理与模式匹配的文件时自动加载该 skill。使用与路径特定规则相同的格式。</td></tr><tr><td style="text-align: center">shell</td><td style="text-align: center">否</td><td style="text-align: left">用于此 skill 中 !<code>command</code> 和 ```! 块的 shell。接受 bash（默认）或 powershell。设置 powershell 在 Windows 上通过 PowerShell 运行内联 shell 命令。需要 CLAUDE_CODE_USE_POWERSHELL_TOOL=1。</td></tr></tbody></table><p>其实对于上述字段中最核心的就是：1、description：告诉模型这个skills都是干什么的；2、name：告诉模型你的skills叫什么（如果没有他就会用上层的文件夹名称），除此之外其他的也可以去注意一下使用（具体描述看上面的表格里面描述）：disable-model-invocation，在定义完skills中yaml之后就是关于第二部分，prompt书写（这部分其实就是相当于你将你一贯的prompt直接机械的告诉大模型，让他按照你这个内容去机械的进行操作），回到最上面去实现一个<strong>规范文书写作skills</strong>（很粗糙具体内容可能需要具体修改），首先明确我的skills都需要哪些内容：1、定义名称（<code>legaldocnorm</code>）；2、定义一些小的脚本工具，比如说对于文档（假设为docx文件）需要让模型去打开文件就需要一些脚本；3、其他。按照上述内容开始一个skills创作（全部基于wsl上，有些命令都是linux命令可以自然的转化到win上，比如创建文件夹）。<br/><strong>第一步</strong>、去构建一个所有文件“系统”以及大致SKILL.md文件（<strong>头部信息推荐英文</strong>，后续内容中英都行）最后所有文件见<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/tree/master/code/Python/skills/legaldocnorm">Github链接</a><br/><strong>第二步</strong>、去构建我的脚本 <code>script</code>（不会写直接让AI帮你写即可），最后的script见<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/tree/master/code/Python/skills/legaldocnorm">Github链接</a><br/><strong>第三步</strong>、去构建一个reference，因为法律文书在书写上比较规划，模型可能不知道具体如何书写可以简单给一个参考让模型规范输出（规范文本可以直接用最高法院提供模板），这里只提供两种规范文本供参考：1、<a href="https://www.gzhzcourt.gov.cn/news/45007004.cshtml">广州市海珠区人民法院——民事答辩状</a>；2、<a href="https://www.gzhzcourt.gov.cn/news/45007009.cshtml">广州市海珠区人民法院——民事起诉状)</a>，最后所有的reference见<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/tree/master/code/Python/skills/legaldocnorm">Github链接</a><br/><strong>skills底层原理</strong>：还是一个function calling，所谓 <strong>function calling</strong>比如说：“北京今天天气如何？”输入模型模型（大模型本身只能输出文本不能去搜索网页等功能）通过分析用户文本输出结构化信息：<code>{"name": "get_weather", "arguments": {"date":xxx, ....}}</code> 而后通过结构化信息进行工具调用（比如说调用搜索天气相关的API进行天气检索）。因此虽然claude code中skills都是文本prompt，大模型在检索到要使用的skills之后通过分析skills中内容自动解析处需要进行操作，因此claude code中skills底层就是：<code>Prompt+Tool Description+ Few-shot examples+ Execution</code></p><h3 id="mcp-开发">MCP 开发</h3><h2 id="claude-code抓包">Claude Code抓包</h2><h3 id="简单网络抓包">简单网络抓包</h3><p>为了分析Claude Code中每一步系统层都在发生什么就需要最Claude Code进行抓包，具体过程如下，首先对环境进行配置（<strong>基于wsl</strong>）：</p><pre><code class="language-bash"># 基于wsl
conda create -n mitm python=3.11
conda activate mitm
pip install mitmproxy
mitmweb --listen-host 0.0.0.0 --listen-port 9870 # 8080 为代理端口 cc走这里 8081 web ui看流量

# 新建窗口
export http_proxy=http://0.0.0.0:9870
export https_proxy=http://0.0.0.0:9870
export HTTP_PROXY=http://0.0.0.0:9870
export HTTPS_PROXY=http://0.0.0.0:9870
export ALL_PROXY=http://0.0.0.0:9870
export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem
export SSL_CERT_FILE=~/.mitmproxy/mitmproxy-ca-cert.pem
claude --permission-mode auto
</code></pre><p>在启动完毕之后，可以看到终端<br/><img src="https://files.seeusercontent.com/2026/05/27/4hnK/20260527214050041.png" alt=""/><br/> 而后直接去Claude code随便测试：1、你好；2、<code>/crawl4ai 搜索一下Yolo系列论文</code>，<strong>直接去终端里面提供的url地址</strong>然后可以直接<code>~c 200</code>（因为访问 https://api.anthropic.com/api/event_logging/v2/batch <strong>可能会</strong>有很多失败会显示400，因此重点看一下链接成功的），比如说通过解析：<br/><img src="https://files.seeusercontent.com/2026/05/27/5dcD/20260528001036233.png" alt=""/><br/> 在输入第一段对话 <code>你好</code> 模型会输入很多内容如tools/skills描述等，但是这些内容绝大部分会“命中缓存”（观察deepseek中就有这种内容）<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">4</a></sup></p><blockquote><p>所谓命中缓存意思就是说：<strong>这部分内容不用模型去重新计算可以直接复用</strong>，这是因为大模型生成过程是prefill+decode，prefill阶段就是对我的prompt进行编码，比如说上面输入模型是一个结构化文档，在<a href="https://www.big-yellow-j.top/posts/2026/03/15/vllm-3.html">vllm的cache逻辑中</a>会将输入token进行block处理（比如每个block存储n个token）当出现block相同时候就会复用结果（必须保证如：输入1：QWERASDF和输入2：QWERASDF此时可以命中缓存，但是如果输入1改成WQERASDF就不行，因此<strong>为了更加大的命中缓存会直接将容易命中缓存输入放到模型输入前面如tools、system等</strong>），而decode就是对prefill后内容开始解码一个token一个token进行输出，<strong>即使如此还是建议如果skills不用就不要开启</strong></p></blockquote><p>而后可以看模型输出部分think+输出：<br/><img src="https://files.seeusercontent.com/2026/05/27/Iro1/20260528001620963.png" alt=""/><br/> 而后在第二部分对话中 <code>/crawl4ai 搜索一下Yolo系列论文</code> 首先可以看模型终端输出：<br/><img src="https://files.seeusercontent.com/2026/05/27/y4Wa/20260528002707498.png" alt=""/><br/> 模型进行如下4次对话处理：<br/><strong>1、任务重写</strong>：从模糊到精确（第 1-3 轮）<br/> 第 1 轮：输入“搜索 Yolo 论文”，模型携带了 crawl4ai 的技能定义。<br/> 关键转折（你发现的 1 &amp; 2 点）：<strong>crawl4ai生成了两个极其精确的搜索关键词</strong>：<code>YOLO series papers object detection survey 2024 2025</code>（针对综述）；<code>YOLOv1 to YOLOv12 paper list arxiv</code>（针对列表）<br/> 第 2-3 轮： 框架将这两个“进阶关键词”重新喂回给模型。这时，模型不再是处理你那个模糊的原始需求，而是拿到了已经被优化过的检索指令。目的： 这就是 Query Expansion（查询扩展）。Agent 知道直接搜“Yolo 论文”结果太乱，所以先把它拆解成“搜综述”和“搜最新版本列表”两个子任务。<br/><strong>2、执行与回传</strong>（第 4 轮）<br/> 动作： 携带了这两个精确关键词的对话被送入 web_search 工具。<br/> 结果： 搜索引擎返回了你看到的那一串包含 YOLOv1 到 YOLOv26 的 Arxiv 链接和详细摘要。<br/> 总结： 最终 DeepSeek 拿到这些被精准定位后的素材，进行最后的整合输出。</p><blockquote><p><em>值得注意的是</em>,web_search返回内容还是通过模型/工具进行总结的，比如说一个搜索得到5-6结果模型进行总结即可</p></blockquote><h3 id="抓包分析功能">抓包分析功能</h3><p>通过上面抓包方式可以进一步去分析claude code中其中功能比如说 <code>/compact</code>，<code>/resume</code> 等，<strong>抓包分析resume</strong>：终端执行 <code>/resume</code> 之后而后直接输入新的内容在claude code直接加载历史对话而后补充到上下文中比如所大致结构如下：</p><pre><code class="language-json">{
    "model": "deepseek-v4-pro",
    "messages": [
      {
        "role": "user",
        "content": [
          {
            历史用户输入对话信息
          },
          {
            新的对话信息
          }
        ],
        .....
      }
</code></pre><p>在新的对话信息里面也会直接加载启用的skills等以及后续模型的tools也会加载（和第一次对话加载信息相同只是多了一组用户历史信息），<strong>但是</strong>这里简单介绍一些claude code中是如何去缓存对话信息的在路径 <code>~/.claude</code> 中文件结构如下，对于每一组对话都会存储在 history.jsonl 中每组对话结构为 <code>{"display":"你是什么模型","pastedContents":{},"timestamp":1780404540849,"project":"/home/huangjie/MdiriCode/Claude","sessionId":"5186f729-1c6e-4698-99e6-7ad2cd667cde"}</code> 最后的sessionid对应每次对话的id信息，都会在 <strong>sessions文件夹</strong>（<em>主要是记录上下文对话信息</em>）中记录<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">5</a></sup><br/><img src="https://files.seeusercontent.com/2026/06/02/5Ekt/20260602214220864.png" alt=""/><br/> 除此之外在<strong>projects文件夹</strong>中还会记录更加多内容，其大致结构如下：</p><pre><code class="language-bash">projects/
 ├── sessionid.jsonl   # 区别于上面 session 里面只记录对话信息，此sessionid.jsonl则是记录所有 “日志记录”，比如说  {"type":"permission-mode","permissionMode":"auto","sessionId":"b69647ef-a95b-428d-a0ac-530440c7853f"}
 ├── memory/
 ├── subagents/
 ├──── xxxx.meta.jsonl # 主要记录 subagets 具体信息比如说：{"agentType":"claude-code-guide","description":"Explain Claude Code compaction","toolUseId":"call_00_0UXP3yM5PsH4VZ2ZbA1c5929"}
 ├──── xxxx.jsonl      # 子agents产生对话信息
</code></pre><p><strong>主要介绍一些subagents</strong>，在claude code中一般是 <strong>主agent收到信息去分发给不同子agent，而后将子agent信息接收进行总结分析等</strong>（具体构建等<a href="#sub-agents服务">见下面分析</a>）。简单总结一些子agents：1、子代理只是向Anthropic发出的独立请求，每个请求都有自己的系统消息。 2、每个子代理都有自己的上下文窗口。它不会与其他子代理或主代理共享内存或聊天记录。 3、“主”Claude（委托代理）为子代理编写提示词，该提示词最终将成为子代理收到的第一条用户消息。 子代理完成任务后，会将其发现的摘要发送回主Claude，随后主Claude会尝试整合所有报告并对其进行解读。 在不断积累上下文之后就需要考虑对内容进行压缩，在claude中<strong>压缩过程</strong>有两种：1、直接 <code>/compact</code>；2、输入对应文本 <code>/compact xxxx</code>对于两种压缩方式通过抓包分析发现使用的提示词都是大致相同的（更加具体的压缩过程<a href="#context-engine">见后续分析</a>）具体的<strong>压缩提示词</strong>如下：</p><pre><code class="language-json">CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.

- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an &lt;analysis&gt; block followed by a &lt;summary&gt; block.

Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.

Before providing your final summary, wrap your analysis in &lt;analysis&gt; tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:

1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
   - The user's explicit requests and intents
   - Your approach to addressing the user's requests
   - Key decisions, technical concepts and code patterns
   - Specific details like:
     - file names
     - full code snippets
     - function signatures
     - file edits
   - Errors that you ran into and how you fixed them
   - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
   - Note any security-relevant instructions or constraints the user stated (e.g., sensitive files or data to avoid, operations that must not be performed, credential or secret handling rules). These MUST be preserved verbatim in the summary so they continue to apply after compaction.
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.

Your summary should include the following sections:

1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent. Preserve any security-relevant instructions or constraints verbatim so they remain in effect after compaction.
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.

Here's an example of how your output should be structured:

&lt;example&gt;
&lt;analysis&gt;
[Your thought process, ensuring all points are covered thoroughly and accurately]
&lt;/analysis&gt;

&lt;summary&gt;1. Primary Request and Intent:
   [Detailed description]

2. Key Technical Concepts:
   - [Concept 1]
   - [Concept 2]
   - [...]

3. Files and Code Sections:
   - [File Name 1]
      - [Summary of why this file is important]
      - [Summary of the changes made to this file, if any]
      - [Important Code Snippet]
   - [File Name 2]
      - [Important Code Snippet]
   - [...]

4. Errors and fixes:
    - [Detailed description of error 1]:
      - [How you fixed the error]
      - [User feedback on the error if any]
    - [...]

5. Problem Solving:
   [Description of solved problems and ongoing troubleshooting]

6. All user messages: 
    - [Detailed non tool use user message]
    - [...]

7. Pending Tasks:
   - [Task 1]
   - [Task 2]
   - [...]

8. Current Work:
   [Precise description of current work]

9. Optional Next Step:
   [Optional Next step to take]

&lt;/summary&gt;
&lt;/example&gt;

Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response. 

There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
&lt;example&gt;
## Compact Instructions
When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
&lt;/example&gt;

&lt;example&gt;
# Summary instructions
When you are using compact - please focus on test output and code changes. Include file reads verbatim.
&lt;/example&gt;


REMINDER: Do NOT call any tools. Respond with plain text only — an &lt;analysis&gt; block followed by a &lt;summary&gt; block. Tool calls will be rejected and you will fail the task.
</code></pre><h2 id="agent架构问题">Agent架构问题</h2><p>上面无论式skills涉及还是tools使用其实都会带来很多Agent底层设计问题比如说：<strong>1、上下文工程（Context Engine）</strong> 随着用户不断对话那么对话历史就会不断变长，如果将对话历史全部塞到对话窗口里面就会导致上下文过长问题（导致模型可能丢失/处理不好）；2、tools如果没有命中怎么办？ 等等诸如此类问题，因此下面对Agent设计过程中会遇到问题以及架构设计上内容进行介绍</p><h3 id="cache机制">Cache机制</h3><p>最上面简单介绍一下缓存机制，通过相同cache缓存命中去选择复用，不过值得注意的是虽然在对话过程中（进行抓包分析）每次用户输入都是在最上面，但是模型会将这些信息进行编排 <code>tools + system+ messages</code>这种方式，保存缓存命中。 在不同模型中缓存命中方式存在些许差异，比如说：<strong>1、DeepSeek缓存命中机制</strong>：；<strong>2、Claude缓存命中机制</strong><br/> https://grok.com/c/b083cb5c-cf7a-40dd-8890-6998438f3d2a?rid=7f9573b4-b0ac-44f8-baa5-015f9f9caf5e<br/> https://zhuanlan.zhihu.com/p/17239625983</p><h3 id="context-engine">Context Engine</h3><p>所谓上下文工程指的是，随着用户之间对话那么窗口token就肯定会超出模型限制（比如DeepSeek的1M），那么就需要对历史对话进行处理如压缩等（不过值得注意的是假设模型上下文窗口是1M在claude code中不会将所有上下文都用完才会去压缩，会设定阈值进行压缩，比如说达到80%自动进行压缩），以如下代码为例：</p><pre><code class="language-python">from openai import OpenAI
client = OpenAI(api_key="xxx",base_url="https://api.deepseek.com")
messages = [
    {
        "role": "system",
        "content": "你是一个专业AI助手"
    }
]

while True:
    user_input = input("User: ")
    if user_input.lower() == "exit":
        break

    messages.append({
        "role": "user",
        "content": user_input
    })
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        temperature=0.7
    )
    assistant_reply = response.choices[0].message.content
    messages.append({
        "role": "assistant",
        "content": assistant_reply
    })
    print("User:", user_input)
    print("\nAssistant:", assistant_reply)
</code></pre><p>模拟进行对话最后得到输出文本:</p><pre><code class="language-markdown">[{'content': '你是一个专业AI助手', 'role': 'system'},
 {'content': '我是小红来自美国，我现在问你一些问题请不多于100个字进行输出，请简单介绍一下北京和上海', 'role': 'user'},
 {'content': '北京是中国的首都，历史悠久，拥有故宫、长城等著名景点；上海是国际大都市，以现代金融和繁华商业闻名，外滩、东方明珠是标志性地标。',
  'role': 'assistant'},
 {'content': '北京和上海两个城市哪一个更加推荐我去？', 'role': 'user'},
 {'content': '这取决于你的旅行偏好。如果你喜欢历史文化，推荐北京，可以探索故宫、长城和胡同。如果你偏爱现代都市与商业氛围，上海更合适，有外滩、陆家嘴和时尚购物区。',
  'role': 'assistant'},
 {'content': '我想去看无人机表演推荐去哪个城市？深圳怎么样？', 'role': 'user'},
 {'content': '深圳是中国无人机表演最出色的城市之一，常有大疆等企业举办大型灯光秀。若想看科技感强的表演，深圳是首选。北京和上海也有相关活动，但深圳更具创新氛围。',
  'role': 'assistant'},
 {'content': '我是谁来自哪里？', 'role': 'user'},
 {'content': '你叫小红，来自美国。', 'role': 'assistant'}]
</code></pre><p>下面简单介绍几种常见的上下文压缩方式<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">6</a></sup>:<br/><strong>1、Claude Code上下文压缩方案</strong>：比如说在上面抓包分析Claude Code的压缩过程就可以看到他所使用的压缩提示词，但是实际过程中不可能全部只通过压缩提示词进行上下文压缩，参考对Claude Code的逆向分析<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">7</a></sup>简单总结其压缩方案：<strong>优先使用廉价规则清理，再到轻量摘要，最后才进行昂贵的 LLM 总结</strong>。<em>廉价处理过程</em>（<strong>对话持续进行持续压缩避免占用上下文</strong>）：将部分特定工具（比如说文件读取，那么就会获取很长的上下文）输出进行压缩，直接简化将文件内容缓存本地，并且只生成简单预览以及本地路径（通过预览如果需要复用直接通过路径再去获取文件信息）。 <em>模型压缩过程</em>（<strong>手动/自动执行压缩命令执行</strong>）：直接通过提示词去对所有的对话历史进行压缩（具体提示词见上面抓包得到的）。最后输出<code>&lt;analysis&gt;</code> + <code>&lt;summary&gt;</code> 两层<sup id="fnref:12" role="doc-noteref"><a href="#fn:12" class="footnote" rel="footnote">8</a></sup>，其中<code>&lt;analysis&gt;</code>：允许模型在内部把对话按时间线梳理一遍，减少遗漏，<code>&lt;summary&gt;</code>：对外提供结构化的最终压缩结果，便于机器再加工（比如客户端替换历史、写入本地存档）。压缩完毕之后就会涉及到信息复用，claude code中直接把总结内容复用到上下文中即可比如说：<br/><img src="https://files.seeusercontent.com/2026/06/03/n0hN/20260604000325877.png" alt=""/></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://www.bilibili.com/video/BV1BFouBYERu/?spm_id_from=333.337.search-card.all.click&amp;vd_source=881c4826193cfb648b5cdd0bad9f19f0">https://www.bilibili.com/video/BV1BFouBYERu/?spm_id_from=333.337.search-card.all.click&amp;vd_source=881c4826193cfb648b5cdd0bad9f19f0</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://code.claude.com/docs/zh-CN/agent-teams">https://code.claude.com/docs/zh-CN/agent-teams</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://code.claude.com/docs/zh-CN/skills">https://code.claude.com/docs/zh-CN/skills</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:8" role="doc-endnote"><p><a href="https://code.claude.com/docs/zh-CN/prompt-caching">https://code.claude.com/docs/zh-CN/prompt-caching</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="https://code.claude.com/docs/zh-CN/claude-directory#ce-global-projects">https://code.claude.com/docs/zh-CN/claude-directory#ce-global-projects</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2507.13334">https://arxiv.org/pdf/2507.13334</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:10" role="doc-endnote"><p><a href="https://github.com/win4r/cc-notebook/blob/main/Claude_Code%E4%B8%8A%E4%B8%8B%E6%96%87%E5%8E%8B%E7%BC%A9%E7%AE%97%E6%B3%95%E6%B7%B1%E5%BA%A6%E5%88%86%E6%9E%90.md">Claude Code 上下文压缩算法深度分析</a> <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:12" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/2004602569171935364">https://zhuanlan.zhihu.com/p/2004602569171935364</a> <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>agent</category><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/05/17/ClaudeCode.html</guid></item><item><title>🔥Pytorch使用-3：常见的分布式训练及其实现</title><link>https://www.big-yellow-j.top/posts/2026/04/20/torch-basic-distribute-1.html</link><description><![CDATA[模型微调多卡训练依赖分布式策略。数据并行中DDP采用独立运行机制，每个进程拥有独立优化器，通过all-reduce同步梯度，避免DP的主GPU瓶颈与模型冗余拷贝。张量并行按行或列拆分权重矩阵，列并行需all-reduce聚合完整输入梯度，行并行则通过all-gather汇总分片梯度。流水线并行将模型按顺序分配到不同设备，面临气泡问题。GPipe引入micro-batch与梯度累积，结合梯度检查点降低显存，按1F1B策略交替前反向；PipeDream通过Weight stashing确保同一micro-batch前后向使用相同权重版本，Vertical Sync虽保证全局一致但通常弃用以避...]]></description><content:encoded><![CDATA[<p>模型微调过程中当有多卡时候就离不开进行分布式训练，本文主要介绍几种常见的分布式训练方式以及其基本原理：</p><h2 id="分布式训练实现">分布式训练实现</h2><p>各类分布式训练方式<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/Pytorch-Learning/learning_distribute/distirbute_training.ipynb">实现</a>。</p><h2 id="常见的分布式训练">常见的分布式训练</h2><h3 id="1数据并行dp">1、数据并行（DP）</h3><p><code>DP流程</code><img src="https://s2.loli.net/2025/06/21/BpalDmSEKdA5Y9s.webp" alt="image" width="1157" height="689" loading="lazy" decoding="async"/><br/> 缺点也是显而易见：</p><ul><li>1、数据副本会冗余（因为要把数据先复制，然后进行分布）；</li><li>2、前向传递前在 GPU 上复制模型（由于模型参数是在主 GPU 上更新的，因此必须在每次前向传递开始时重新同步模型）；</li><li>3、GPU 利用率不均衡（损失计算在主 GPU 上进行，在主 GPU 上进行梯度降低和参数更新</li></ul><p><code>DDP流程</code><br/><img src="https://s2.loli.net/2025/06/21/xTAvw7Sm9GFKc5X.webp" alt="image" width="1269" height="674" loading="lazy" decoding="async"/></p><blockquote><p>对比 <code>DP</code>和 <code>DDP</code><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup><br/> 1、<code>DP</code>是一种 <code>集中-分发</code>机制（优化器/梯度计算都是再 <code>master</code>进程上处理好之后，然后分发到不同的进程中）<br/> 2、<code>DDP</code>是一种 <code>独立-运行</code>机制（每个进程都有自己的优化器，并且在计算梯度过程中：各进程需要将梯度进行汇总规约到主进程，主进程用梯度来更新模型权重，然后其 <code>broadcast</code> 模型到所有进程（其他GPU）进行下一步训练）</p></blockquote><p>整体流程：<br/><strong>1、加载模型阶段</strong>。每个GPU都拥有模型的一个副本，所以不需要拷贝模型。rank为0的进程会将网络初始化参数broadcast到其它每个进程中，确保每个进程中的模型都拥有一样的初始化值。<br/><strong>2、加载数据阶段</strong>。DDP 不需要广播数据，而是使用多进程并行加载数据。在 <code>host</code> 之上，每个 <code>worker</code>进程都会把自己负责的数据从硬盘加载到 ` page-locked memory<code>。</code>DistributedSampler<code> 保证每个进程加载到的数据是彼此不重叠的。 **3、前向传播阶段**。在每个GPU之上运行前向传播，计算输出。每个GPU都执行同样的训练，所以不需要有主 GPU。 **4、计算损失**。在每个GPU之上计算损失。 **5、反向传播阶段**。运行后向传播来计算梯度，在计算梯度同时也对梯度执行 </code>all-reduce`操作。</p><blockquote><p>由于数据实在不同设备上，但是是一个模型，对于梯度的计算可以：直接将不同设备之间梯度相互传播（每个设备的数据是不一样的，但是模型是相同的，这样计算梯度会不同），然后计算平均（<code>alll-reduce</code>计算方法）</p></blockquote><p><strong>6、更新模型参数阶段</strong>。因为每个GPU都从完全相同的模型开始训练，并且梯度被 <code>all-reduced</code>，因此每个GPU在反向传播结束时最终得到平均梯度的相同副本，所有GPU上的权重更新都相同，也就不需要模型同步了。注意，在每次迭代中，模型中的 <code>Buffers</code> 需要从rank为0的进程广播到进程组的其它进程上</p><h3 id="2张量并行tp">2、张量并行（TP）</h3><p><strong>张量并行目的是模型参数矩阵太大，需要将他们拆分到不同设备</strong>。张量切分方式分为按行进行切分和按列进行切分，分别对应行并行（Row Parallelism）(权重矩阵按行分割)与列并行（Column Parallelism）(权重矩阵按列分割)。假设计算过程为：$y=Ax$ 其中 $A$ 为权重</p><p><img src="https://s2.loli.net/2025/06/22/neZUk6FpYKy2EOz.webp" alt="image" width="1054" height="639" loading="lazy" decoding="async"/></p><p><strong>列并行操作</strong>：对我的权重矩阵按列进行切分而后分配到不同设备上<br/> 观察上面图像上半部分，forward部分容易理解，对于backward部分理解：第一部分计算（反向传播-1）：得到两个新的Y1和Y2然后将他们进行<strong>拼接</strong>，计算梯度可以直接 $\frac{\partial L}{\partial Y_1} \frac{\partial L}{\partial Y_2}$ 得到梯度，第二部分计算（反向传播-2）：由于x是完整的因此可以直接：<br/> $\frac{\partial L}{\partial X}=\frac{\partial L}{\partial X}|<em>{A_1}+\frac{\partial L}{\partial X}|</em>{A_2}$</p><p><strong>行并行操作</strong>：将输入x以及权重矩阵都按行进行切分分配到不同设备上<br/> 观察上面图像上半部分，forward分别对输入x以及参数A进行才分然后计算，对于backward理解：第一部分（反向传播-2）：因为得到的Y是由两部分Y1和Y2直接相加得到结果，因此：$\frac{\partial L}{\partial Y_1}= \frac{\partial L}{\partial Y}$，第二部分（反向传播-1）：$\frac{\partial L}{\partial X}=[\frac{\partial L}{\partial X_1}+\frac{\partial L}{\partial X_2}]$</p><p>对于 <strong>列并行操作</strong>由于x是完整的只需要通过 <strong>all-reduce</strong>操作（将不同设别的梯度信息“汇总”起来）。<strong>行并行操作</strong>：由于x都被拆分了，因此需要通过 <strong>all-gather</strong>（将不同GPU梯度聚合而后广播）</p><blockquote><p>all-reduce、all-gather等见：<a href="https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html#:~:text=%E5%AF%B9%E4%BA%8E%E4%B8%8A%E8%BF%B0%E8%BF%87%E7%A8%8B%E4%B8%AD%E6%8F%90%E5%88%B0%E7%9A%84%E5%87%A0%E4%B8%AA%E6%A6%82%E5%BF%B5%EF%BC%9AAll%2DGather%2C%20All%2DReduce%2C%20reduce%2Dscatter%E5%85%B6%E5%85%B7%E4%BD%93%E7%9A%84%E5%8E%9F%E7%90%86%E5%A6%82%E4%B8%8B">All-Gather, All-Reduce, reduce-scatter什么意思？</a></p></blockquote><h3 id="3流水线并行pp">3、流水线并行（PP）</h3><p>当模型变得过大以至于单个设备无法容纳其任何一层，或者需要以不同方式重叠计算和通信时，流水线并行提供了一种替代的扩展策略。不同于复制整个模型或拆分单个层的方法，<strong>流水线并行将模型本身按顺序分配到多个设备上</strong>。每个设备或设备组都成为流水线中的一个“阶段”，负责运行模型层的一个子集。其内部并行的机制，如一个在四个GPU上运行的四层模型：<br/> GPU 0 (阶段 0): 运行第 1 层。<br/> GPU 1 (阶段 1): 运行第 2 层。<br/> GPU 2 (阶段 2): 运行第 3 层。<br/> GPU 3 (阶段 3): 运行第 4 层并计算损失。<br/> 输入数据进入第一阶段（GPU 0）。处理后，输出激活被发送到第二阶段（GPU 1）。这会一直持续，直到最后阶段计算出输出和损失。随后，梯度以相反的顺序反向流经流水线。GPU 3 计算第 4 层的梯度，并将第 3 层输出的梯度发回给 GPU 2，然后 GPU 2 计算第 3 层的梯度并将其发回给 GPU 1，依此类推，直到梯度到达第一阶段。在这个过程中会存在 “<strong>流水线气泡问题</strong>”：当阶段 1 处理第一个数据批次时，阶段 0 处于空闲状态，等待下一个批次。类似地，当阶段 2 处理时，阶段 0 和 1 处于空闲状态（假设只有一个批次流过）（backwards同理），常见两种处理方式：</p><h4 id="31-gpipe实现流水线并行">3.1 GPipe实现流水线并行</h4><blockquote><p><a href="https://torchgpipe.readthedocs.io">https://torchgpipe.readthedocs.io</a></p></blockquote><p><code>GPipe</code><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> 将一个小批量（<code>mini-batch</code>）分割成多个微批量（<code>micro-batch</code>），使设备尽可能并行工作。其核心原理就是<strong>当每个分区处理完一个微型批次后，可以将输出扔给下一个分区，并立即开始处理下一个微型批次</strong>。<br/><img src="https://s2.loli.net/2025/06/21/65roh7zdVCAqIQE.webp" alt="image" width="729" height="442" loading="lazy" decoding="async"/></p><p>上图中b、c分别表示为 <code>模型并行</code>和 <code>流水线并行</code>都会有一个“拆分”的处理（模型并行和流水线并行<strong>都会对模型进行拆分</strong>，但<strong>模型并行</strong>主要关注模型的计算任务如何分布到不同设备，而<strong>流水线并行</strong>还结合了微批次化的数据处理，用于提升并行效率。），对比b-c很容易发现使用模型并行会有一个问题：设备闲置问题，只用第一层处理完之后才会进入到下一层，因此 <strong>流水线</strong>并行做的就是：<strong>在模型并行的基础上，进一步引入数据并行的办法，即把原先的数据再划分成若干个batch，送入GPU进行训练</strong>。</p><blockquote><p><strong>对比数据并行</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup><br/> micro-batch 跟数据并行有高度的相似性：<br/> 1、数据并行是空间上的，数据被拆分成多个 tensor，同时喂给多个设备并行计算，然后将梯度累加在一起更新。<br/> 2、micro-batch 是时间上的数据并行，数据被拆分成多个 tensor，这些 tensor 按照时序依次进入同一个设备串行计算，然后将梯度累加在一起更新。</p></blockquote><p>那么就会有存在一个问题：因为数据/模型被拆分，那么如何去处理梯度？对于上述两种方式，我们假设数据数量：10，然后设备个数：5，同时假设我们也将模型分布到这5个设备上，那么在 <code>forawrd</code> 阶段处理过程为：<br/><strong>数据并行</strong>：每个设备会处理2个数据（10/5）</p><p><strong>流水线并行</strong>：因为模型分布在不同设备上（假设：$ld_1, ld_2, ld_3, ld_4, ld_5$），会有一个操作：将数据在拆分为不同 <code>micro-batch</code>（这里假设为5，得到：$md_1,md_2,md_3,md_4,md_5 $），这样一来随着前向传播：$t_0$ 时：$ld_1$ 处理 $md_1$；$t_1$ 时：$(ld_1, md_2), (ld_2, md_1)$（ 值得注意的是 此处的md_1是由 ld_1处理完md_1得到结果，更加准确描述是：$(ld_2, ld_1(md_1))$）。</p><p>同理在 <code>backward</code> 阶段处理过程（对于 <strong>数据并行</strong>直接依次计算梯度即可 ）为：在 <code>forward</code> 结束之后此时 <strong>流水线并行</strong> 中对于设备 <code>ld_5</code> 在 $t_9$: $ld_5$ 接收 Loss 传回的 $md_5$ 的梯度。它利用之前存的 $md_5$ 前向输入，重算一遍，算出 $W$ 的梯度。$t_{10}$: $ld_5$ 接收 Loss 传回的 $md_4$ 的梯度，重复上述动作。</p><p>如下图Gpipe中的forward 以及 backward 过程：<br/><img src="https://files.seeusercontent.com/2026/05/07/x6xO/20260420213604.webp" alt="20260420213604"/></p><blockquote><p>数据被切分成小的micro-batch，forward过程中一次将数据进行输入处理，在backward过程中则是依次将计算得到的梯度返回给上一层设备</p></blockquote><p><strong>不过值得注意的是</strong>，在Gpipe中存在 <strong>激活缓存机制</strong>：如果将每一个 micro-batch 计算结果都进行缓存会导致显存随 micro-batch 进行线性增长，因此在 <code>forward</code> 阶段丢弃中间保留边界，显存中只会保留 $md_i$ 的输入，在 <code>backward</code>阶段：即时计算，因为需要计算梯度那么直接将初始输入拿出来在计算 forward过程（也就是所谓的 <code>gradient-checkpoint</code>）。 以及 <strong>梯度累计机制</strong>：因为梯度更新必须是完整的一个batc处理之后才会进行（而我的数据都被拆分成小的micro-batch），因此在数据 $md_5 \rightarrow md_1$ 的过程中，<strong>激活缓存机制</strong>会不断的将梯度进行累积直到所有的数据处理完毕再进行参数更新。</p><blockquote><p><code>gradient-checkpoint</code> 过程：<br/> 比如说：$x \xrightarrow{x_1} a_1 \xrightarrow{x_2} a_2 \xrightarrow{x_3} a_3 \xrightarrow{x_4} a_4$ 那么计算loss： $loss=(a_4- y)^2$</p><p><strong>朴素方法</strong>：$\frac{dloss}{dw_1}=\frac{dloss}{da_4} \times \frac{da_4}{da_3} \times \frac{da_3}{da_2} \times \frac{da_2}{da_1} \times \frac{da_1}{dw_1}$。前向过程需要计算并存储 $a_1, a_2, a_3, a_4$，反向时再调用这些激活值计算梯度。</p><p><strong>梯度检查点</strong>：只存少量检查点，反向时若需要某个激活值但没有存储，则从最近的检查点重新计算前向。例如只保存 $a_1$ 和 $a_4$：</p><p><strong>前向</strong>：正常计算 $a_1 \to a_2 \to a_3 \to a_4$，但只保存 $a_1$ 和 $a_4$，丢弃 $a_2, a_3$。</p><p><strong>反向</strong>： <br/> 1、从 $a_4$ 开始反向，需要 $a_3$：从最近的检查点 $a_1$ 重新计算 $a_1 \to a_2 \to a_3$，得到 $a_3$ 后计算 $\frac{dloss}{dw_4}$ 和 $\frac{dloss}{da_3}$，然后丢弃 $a_2, a_3$。</p><p>2、继续反向，需要 $a_2$：再次从 $a_1$ 重新计算 $a_1 \to a_2$，得到 $a_2$ 后计算 $\frac{dloss}{dw_3}$ 和 $\frac{dloss}{da_2}$，丢弃 $a_2$。</p><p>3、反向到 $a_1$：$a_1$ 已保存，直接使用计算 $\frac{dloss}{dw_2}$。</p><p>4、最后计算 $\frac{dloss}{dw_1}$ 使用输入 $x$。</p><p><strong>效果</strong>：内存从存储 4 个激活值减少到 2 个，代价是增加了额外的重计算（本例中 $a_1 \to a_2 \to a_3$ 被计算了多次）。</p><p><strong>梯度检查点（gradient checkpointing）</strong> 的工作原理是从计算图中省略一些激活值（由前向传播产生，其中这里的”一些“是指可以只省略模型中的部分激活值，折中时间和空间，陈天奇在它的<a href="https://arxiv.org/pdf/1604.06174">论文</a>使用了如下动图的方法，<strong>即前向传播的时候存一个节点释放一个节点，空的那个等需要用的时候再backword的时候重新计算）。这减少了计算图使用的内存，降低了总体内存压力（并允许在处理过程中使用更大的批次大小）</strong>。</p></blockquote><p>简单使用:</p><pre><code class="language-python">from torchgpipe import GPipe

model = nn.Sequential(a, b, c, d)
model = GPipe(model, balance=[2, 2], chunks=8)

# 1st partition: nn.Sequential(a, b) on cuda:0
# 2nd partition: nn.Sequential(c, d) on cuda:1

for input in data_loader:
    output = model(input)
</code></pre><p>对于 Gpipe 后续一个简单优化：因为梯度必须等所有 <code>forward</code> 处理完才会进行 <code>backward</code> 处理效率太低除此之外因为需要缓存 m 份 activation导致内存增加。原因是每个microbatch前向计算的中间结果activation都要被其后向计算所使用，所以需要在内存中缓存。，可以让模型交替执行 forward 和 backward（也就是 1F1B策略）</p><h4 id="32-pipedream-实现流水线并行">3.2 PipeDream 实现流水线并行</h4><p><img src="https://files.seeusercontent.com/2026/05/07/c7Cg/20260420215304.webp" alt="20260420215304"/></p><p>在论文<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>中提出在<strong>权重更新过程中存在问题</strong>：<br/><strong>1、同一个minibatch的前向传播和后向传播使用的参数不一致</strong>：比如在 Machine1上输入数据5时，用的是数据1的更新后参数，依次类推到数据5梯度更新时用的是1、2、3、4这4组数据梯度，这就导致 minibatch 5 的前向计算和后向计算时候，使用的参数不一致。即，第一行 Machine 1，蓝色 5 号 和 绿色 5 号 计算时候，必须都使用 绿色 1 号之后更新的参数。<br/> 2、<strong>同一个数据在不同Machine上做同样操作（同样做前向操作，或者同样做后向传播）使用的参数版本不一致</strong>。如对于 数据 5 在 machine 1 上的前向计算部分（蓝色5），他的前向逻辑在 数据1 的后向计算以后执行。但是 数据 5 在 machine 2 上的前向计算部分（蓝色5），是在 “数据 1, 数据 2” 的后向计算结束后才执行。这就导致了 数据 5 在两个stage上前向计算使用的参数版本不一致。</p><p>对于上述问题，其提出Weight stashing、Vertical Sync策略解决这个问题。<strong>Weight stashing</strong>过程如下：</p><blockquote><p>PipeDream核心在于：<strong>同一个 micro-batch 的 forward 和 backward 使用同一份权重</strong></p></blockquote><p><img src="https://files.seeusercontent.com/2026/05/07/Bid7/20260420222207.webp" alt="20260420222207"/></p><p>以 数据5 为例：在forward中对于 Worker1 使用的是 数据1 更新后的权重，那么在 backward 中就需要去对 数据1 更新后权重进行 backward，因此在Worker1中为了保证上述原理，就需要去缓存1-4的数据的权重（因为数据5 backward 之前还进行其他3组数据）也就是 $W_1^{(1)},…,W_1^{(4)}$，那么类似的对于Worker2就需要去缓存2-4的数据的权重依次类推。数据5 的 backward 必须使用它 forward 时对应的权重版本也就是将 $W_1^{(1)}$ 拿出来（如 数据5 在worker1 forward中计算是：$o = W_1\times \text{Data}_5$ 那么 backward 时需要将 $W_1^{(1)}$ 拿出来）</p><p><strong>Vertical Sync</strong>过程如下<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>：<br/><img src="https://files.seeusercontent.com/2026/05/07/s3Rj/20260420234611.webp" alt="20260420234611"/></p><p>每个进入管道的 数据 都与其进入流水线输入阶段时候的最新权重版本相联系。当小批次在流水线前向传播阶段前进时候，这个版本信息随着激活值和梯度一起流动。比如说上图中，强制所有worker在计算 minibatch 5 的时候都用本worker做 minibatch 1 反向传播之后的参数，具体来说就是：对于 worker 2，使用本阶段绿色1（1反向传播之后，更新的本阶段权重）来做 5 的前向传播。但是，这样同步会导致很多计算浪费无用。比如5更新时用的1的权重，但2/3/4后向传播的权重都白白计算了，所以<strong>默认不使用Vertical Sync</strong>。这样虽然每层不完全一致，但是由于weight stashing的存在，所有的参数都是有效的。</p><h3 id="4专家并行">4、专家并行</h3><h2 id="状态切分优化显存">状态切分优化显存</h2><p>以上述DDP训练过程为例，为了更大限度的榨干设备显存（让其可以接受更加大的数据输入，而在模型训练过程中以Adam优化器为例在显存占用上：模型参数、优化器状态、梯度这三个是显存占用大头，那么为了优化就需要对其进行切分，每台设备只保存部分信息），就可以考虑使用 <code>DeepSpeed</code> 以及 toch原生的 <code>FSDP2</code>方式去对<strong>模型/梯度/优化器状态</strong>的分布式分片，除此之外对于更加大的模型可能就会考虑直接使用 混合并行（多种分布式训练叠加）</p><h3 id="deepspeed">DeepSpeed</h3><p>简单回顾一下<a href="https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html">DeepSpeed中基本原理</a>，在 <code>DeepSpeed</code>中指出模型训练过程中显存占用上主要分为如下3块（假设模型的参数为 $\Phi$，使用混合精度训练，对于参数以及梯度会用fp16而对于adam优化器则是fp32）：1、模型参数（ $2\Phi$）；2、梯度（ $2\Phi$）；3、优化器状态（ $4+4+4\Phi$）</p><p><img src="https://s2.loli.net/2025/06/21/4OUkVeJpjsF8zvc.webp" alt="image" width="889" height="381" loading="lazy" decoding="async"/></p><p>其中有3中不同的切分处理方式（上图从左到右）， <code>Zero-1</code>直接去对优化器进行切分每个设备只保留部分；<code>Zero-2</code>：额外去对梯度进行切分；<code>Zero-3</code>：额外去对模型参数进行切分。除此之外还会涉及到<strong>不同设备之间的同步方式</strong>：All-reduce（聚合所有结果，然后计算平均比如DDP中就是使用All-reduce）、reduce-scatter、All-Gather（收集每张卡状态然后广播所有设备）。<strong>简单使用DeepSpeed进行分布式训练</strong>：</p><pre><code class="language-python">import deepspeed
...
deepspeed_config = {
    "train_batch_size": 128,
    "optimizer": {
        "type": "Adam",
        "params": {"lr": 1e-3}
    },
    "fp16": {
        "enabled": True
    },
    "zero_optimization": {
        "stage": 2               
    },
    "gradient_accumulation_steps": 1,
    "steps_per_print": 50
}

model = ...
model, optimizer, _, _ = deepspeed.initialize(
    model=model,
    config=deepspeed_config,
    model_parameters=filter(lambda p: p.requires_grad, model.parameters())
)

for epoch in range(5):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(model.device), target.to(model.device)
        outputs = model(data)
        loss = ...
        model.backward(loss)
        model.step()
</code></pre><p>使用deepspeed十分简单，只需要提前配置好deepspeed的参数，而后将其通过deepspeed初始化处理（模型、优化器等），而后后续训练就和普通训练过程差异不大，model.backward(loss) 和 model.step() 已经自动管理了梯度清零，所以传统训练里必须用的 optimizer.zero_grad() 不需要显式调用。</p><h3 id="fsdp">FSDP</h3><p>其在基本思想上和DeepSpeed相似都是选择去对3部分进行切分，可以简单理解为：<strong>PyTorch 原生的 ZeRO-3 实现，通过在前反向计算时临时 All-Gather 完整参数、计算后立即释放的方式，将参数、梯度、优化器状态均匀分片到所有 GPU 上，使显存占用从 O(N) 降至 O(1/N)。</strong> 简单使用FSDP进行分布式训练：</p><pre><code class="language-python">from torch.distributed.fsdp import (
    FullyShardedDataParallel as FSDP,
    ShardingStrategy,
    StateDictType,
)
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy
from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
    apply_activation_checkpointing,
)
import torch.distributed as dist

dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)

model = ...

auto_wrap_policy = size_based_auto_wrap_policy   # 基于参数数量的自动包装
model = FSDP(
    model,
    sharding_strategy=ShardingStrategy.FULL_SHARD,  # ZeRO-3 行为
    auto_wrap_policy=auto_wrap_policy,
    use_orig_params=True,            # 允许直接使用原始参数名
)
for epoch in range(5):
    sampler.set_epoch(epoch)
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.cuda(), target.cuda()
        optimizer.zero_grad()
        with torch.cuda.amp.autocast():
            outputs = model(data)
            loss = criterion(outputs, target)
        loss.backward()
        optimizer.step()
</code></pre><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/">https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/1811.06965">https://arxiv.org/pdf/1811.06965</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/617087561">https://zhuanlan.zhihu.com/p/617087561</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://www.cnblogs.com/rossiXYZ/p/15172816.html">https://www.cnblogs.com/rossiXYZ/p/15172816.html</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://people.eecs.berkeley.edu/~matei/papers/2019/sosp_pipedream.pdf">https://people.eecs.berkeley.edu/~matei/papers/2019/sosp_pipedream.pdf</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://www.cnblogs.com/rossiXYZ/p/15272831.html#12-gpipe%E6%B5%81%E6%B0%B4%E7%BA%BF">https://www.cnblogs.com/rossiXYZ/p/15272831.html#12-gpipe%E6%B5%81%E6%B0%B4%E7%BA%BF</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>pytorch</category><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/04/20/torch-basic-distribute-1.html</guid></item><item><title>🔥Pytorch使用-1：Pytorch计算图等概念</title><link>https://www.big-yellow-j.top/posts/2026/04/10/torch_basic_torch_compile.html</link><description><![CDATA[PyTorch计算图是有向无环图，节点代表操作，边代表数据流，动态图在每次前向传播时即时构建并默认释放，支持灵活调试。反向传播时框架沿图计算梯度并累加到叶子张量。静态图则提前构建完整计算图以优化执行。torch.compile引入三个核心组件：TorchDynamo通过捕获Python字节码将Tensor操作记录为FX Graph，遇复杂控制流产生图断开；AOTAutograd提前从前向图生成反向图，避免运行时动态建图；TorchInductor作为默认后端，融合算子、优化内存并生成Triton或C++代码。使用torch.compile可显著减少训练时间，模型效果差异不大。关键参数包括...]]></description><content:encoded><![CDATA[<h2 id="torch计算图概念">torch计算图概念</h2><p>在深度学习框架中，计算图（英文简称Graph）是一个有向无环图（DAG），它的节点代表操作（例如加法、乘法或者更复杂的函数），边则代表数据（例如张量或者标量）。计算图为深度学习中的前向传播（forward propagation）和反向传播（backward propagation）提供了一个可视化的框架，它能清楚地展示数据是如何流动和操作的。比如说以下面例子为例解释</p><h3 id="简单例子">简单例子</h3><blockquote><p>PyTorch 在 forward 时构建一张由 Node 组成的反向执行图，每个 Node 记录局部梯度计算规则和必要上下文，在 backward 时由 Engine 按拓扑顺序逐节点执行并完成梯度传播与累加。</p></blockquote><p><img src="https://s2.loli.net/2025/08/14/gIftdlM7KTw2Yak.webp" alt="image" width="889" height="754" loading="lazy" decoding="async"/></p><p>参考上图中给出的例子<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>，PyTorch 的计算图在底层负责记录张量之间的运算与依赖关系。要点：<strong>节点（Node）</strong> 为张量或运算；<strong>边（Edge）</strong>表示数据流，指明某张量由哪个运算生成或某运算的输入来自哪个张量；叶子节点为用户创建且需梯度的张量（<code>requires_grad=True</code>）。PyTorch 是动态图框架：每次 forward 即时构建计算图，执行后默认释放（除非 <code>retain_graph=True</code>）。调用 <code>.backward()</code> 时，框架沿计算图从输出向输入反向传播，逐节点累加梯度并回传到叶子张量（例如 $z=w=y_1\times y_2= \log(a) \times \sin(x_2)=\log(x_1\times x_2)\times \sin(x_2)$ 的梯度由对应运算节点按链式法则逐步计算）：</p><pre><code class="language-python">import torch
# 叶子节点
X1 = torch.tensor(2.0, requires_grad=True)
X2 = torch.tensor(3.0, requires_grad=True)

a = X1 * X2                  # a = X1 * X2
y1 = torch.log(a)            # y1 = log(a)
y2 = torch.sin(X2)           # y2 = sin(X2)
w = y1 * y2                  # w = y1 * y2
z = w                        # z = w

a.retain_grad()
y1.retain_grad()
y2.retain_grad()
w.retain_grad()
z.retain_grad()

print("Forward: z =", z.item())
z.backward()
print(f"dz/dz = 1 (输出对自己的梯度永远是1) -&gt; {z.grad.item()}")
print(f"dz/dw = dz/dz * ∂z/∂w = 1 * 1 = {w.grad.item()}")
print(f"dz/dy1 = dz/dw * ∂w/∂y1 = {w.grad.item()} * y2 = {w.grad.item()} * {y2.item()} = {y1.grad.item()}")
print(f"dz/dy2 = dz/dw * ∂w/∂y2 = {w.grad.item()} * y1 = {w.grad.item()} * {y1.item()} = {y2.grad.item()}")
print(f"dz/da  = dz/dy1 * ∂y1/∂a = {y1.grad.item()} * (1/a) = {y1.grad.item()} * (1/{a.item()}) = {a.grad.item()}")
print(f"dz/dX1 = dz/da * ∂a/∂X1 = {a.grad.item()} * X2 = {a.grad.item()} * {X2.item()} = {X1.grad.item()}")
print(f"dz/dX2 = dz/da * ∂a/∂X2 + dz/dy2 * ∂y2/∂X2\n"
      f"       = {a.grad.item()} * X1 + {y2.grad.item()} * cos(X2)\n"
      f"       = {a.grad.item()} * {X1.item()} + {y2.grad.item()} * {torch.cos(X2).item()}\n"
      f"       = {X2.grad.item()}")
</code></pre><p>输出结果为：</p><pre><code class="language-python">Forward: z = 0.2528530955314636
dz/dz = 1 (输出对自己的梯度永远是1) -&gt; 1.0
dz/dw = dz/dz * ∂z/∂w = 1 * 1 = 1.0
dz/dy1 = dz/dw * ∂w/∂y1 = 1.0 * y2 = 1.0 * 0.14112000167369843 = 0.14112000167369843
dz/dy2 = dz/dw * ∂w/∂y2 = 1.0 * y1 = 1.0 * 1.7917594909667969 = 1.7917594909667969
dz/da  = dz/dy1 * ∂y1/∂a = 0.14112000167369843 * (1/a) = 0.14112000167369843 * (1/6.0) = 0.023520000278949738
dz/dX1 = dz/da * ∂a/∂X1 = 0.023520000278949738 * X2 = 0.023520000278949738 * 3.0 = 0.07056000083684921
dz/dX2 = dz/da * ∂a/∂X2 + dz/dy2 * ∂y2/∂X2
       = 0.023520000278949738 * X1 + 1.7917594909667969 * cos(X2)
       = 0.023520000278949738 * 2.0 + 1.7917594909667969 * -0.9899924993515015
       = -1.7267885208129883
</code></pre><p>对于计算图就是对于你的输入数据进行了那种计算方式进行记录，后续梯度反向传播时候通过上面计算图（<strong>计算图保存了所有中间变量和梯度信息</strong>）来计算梯度更新参数。<strong>更加底层</strong>的了解一下上述整个过程，通过下面代码访问具体计算图：</p><pre><code class="language-python">def show_graph(fn, indent=0):
    if fn is None:
        return
    print("  " * indent, fn)
    for next_fn, _ in fn.next_functions:
        show_graph(next_fn, indent + 1)
show_graph(z.grad_fn)
</code></pre><p>最后输出内容就是：</p><pre><code> &lt;MulBackward0 object at 0x7f3aac04af20&gt;         z=y2* y3
   &lt;LogBackward0 object at 0x7f3aac0494b0&gt;       y2=log(y1)
     &lt;MulBackward0 object at 0x7f3aac049690&gt;     y1=x1*x2
       &lt;AccumulateGrad object at 0x7f3aac049600&gt; x1
       &lt;AccumulateGrad object at 0x7f3aac049510&gt; x2
   &lt;SinBackward0 object at 0x7f3aac0495d0&gt;       y3=sin(x2)
     &lt;AccumulateGrad object at 0x7f3aac049690&gt;   x2
</code></pre><p>那么在 <code>forward</code> 过程中，Step-1计算 $y1= x1\times x2$，那么此时创建Node：MulBackward0并且保存<code>x1 x2</code>，并且建立关系：</p><pre><code class="language-markdown">y1.grad_fn → MulBackward0
MulBackward0.next_functions → [AccumulateGrad(x1), AccumulateGrad(x2)]
</code></pre><p>那么在 <code>backward</code> 过程中，Step-1计算 MulBackward0（z=y2* y3）计算：</p><pre><code class="language-markdown">dz/dy2 = y3
dz/dy3 = y2
</code></pre><p>传递：</p><pre><code class="language-markdown">→ LogBackward0（带 y3）
→ SinBackward0（带 y2）
</code></pre><p>如果要得到模型具体的计算图下面两种方法：</p><pre><code class="language-python">import torch
from torchviz import make_dot
from torchvision.models import resnet18
"""
apt-get install -y graphviz
pip install torchviz
pip install onnxscript
"""
model = resnet18()
model.eval()
x = torch.randn(1, 3, 224, 224)
y = model(x)

dot = make_dot(y, params=dict(model.named_parameters()))
dot.format = 'png'
dot.render("model_graph")

onnx_file_path = "resnet18_model.onnx"
torch.onnx.export(
    model,                      # 要导出的模型
    x,                          # 虚拟输入
    onnx_file_path,             # 导出文件路径
    export_params=True,         # 是否导出权重参数
    opset_version=17,           # ONNX 算子版本，建议 12 及以上
    do_constant_folding=True,   # 是否执行常量折叠优化
    input_names=['input'],      # 输入节点的名称
    output_names=['output'],    # 输出节点的名称
    dynamic_axes={              # 可选：支持动态 Batch Size
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)
print(f"模型已成功导出至: {onnx_file_path}")
</code></pre><p>对于第二种可以直接将到处的模型通过网站：<a href="https://netron.app/">https://netron.app/</a> 去分析每个节点的具体参数以及输入和输出。在torch计算图概念中主要是两种计算方式：1、动态计算图；2、静态计算图，两者之间的核心差异在于前者计算图在 forward 执行过程中边执行边生成（执行每一个运算过程就会生成一个新的节点），而后者计算图在执行前已经完整确定，并作为“可优化程序”执行（提前解析表达式得到计算图）</p><h2 id="torchcompile">torch.compile</h2><p><img src="https://files.seeusercontent.com/2026/05/07/o8lL/image20260410161253319.webp" alt=""/></p><p>对于上述图像简单了解几个基本过程<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">2</a></sup>：<br/><strong>第一步：首先通过TorchDynamo —— “动态录音机”（抓图）</strong><br/> 当你第一次运行被 torch.compile 装饰的函数时，Dynamo 会“偷偷”接管 Python 的执行。 它不是静态看代码，而是一边模拟运行，一边录音： 把所<strong>有 PyTorch 操作（加、乘、卷积、ReLU 等）记录下来，画成一张 FX Graph（一张计算流程图）</strong>（对于普通的python操作不会被记录）。 python 的普通代码（if 判断、for 循环、打印等）如果太复杂，就产生 Graph Break（图断开），这部分还是用原来的慢方式运行。 它还会记录“假设”：比如输入 tensor 的形状是 [32, 3, 224, 224]、类型是 float32 等。这些假设叫 Guards（守卫）。 <br/><img src="https://files.seeusercontent.com/2026/05/07/6uLd/image20260410161614945.webp" alt=""/></p><p><strong>第二步：AOTAutograd —— “提前准备反向传播”</strong><br/> 如果是训练（需要 backward），Dynamo 只抓了前向（forward）。 AOTAutograd 会提前从前向图生成反向图（不用等到真正做 backward 时才临时建图）。 它还会把复杂操作分解成更基础的操作（PrimTorch），让后续优化更容易。 好处：前向+反向可以一起优化，节省内存（不用保存所有中间结果）。<br/><img src="https://files.seeusercontent.com/2026/05/07/lhE7/image20260410161628959.webp" alt=""/></p><p><strong>第三步：TorchInductor（默认后端）—— “优化工厂 + 代码生成器”</strong><br/> 拿到干净的计算图后，Inductor 开始大改造： 融合操作：把能合并的算子合成一个内核（例如 conv + batchnorm + relu 变成一个 GPU 内核，减少内存读写）。 布局优化、内存复用、循环优化等。 生成代码： GPU 上主要生成 Triton 代码（一种简单却高效的语言，比手写 CUDA 容易，性能接近官方）。 CPU 上生成 C++ 代码。</p><h3 id="简单使用">简单使用</h3><p><strong>值得注意的是</strong>：在torch&gt;2.0之后引入一个新的概念 <code>torch.compile</code><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> 在传统的计算过程中，如 <code>x+y</code>那么pytorch就会执行Python 解释器调用函数、检查类型、分配内存、调用 GPU/CPU 操作等操作，这样以来过程就会比较慢，比如说简单的计算：</p><pre><code class="language-python">import torch
def fun1(a, b):
    return a+b
fun_compile = torch.compile(fun1)

@torch.compile
def fun2(a, b):
    return a+b
</code></pre><p>基本上只需要对涉及到计算的函数用 <code>torch.compile</code>处理即可（第一次编译速度比较慢，后续计算就快了），直接测试使用compile再模型训练过程中的表现，值得注意的是在使用Trl框架进行强化学习过程中，训练参数直接支持使用 <code>compile</code>（具体位置为：transformers/training_args.py，直接在DPOConfig中进行指定即可，直接使用 <code>torch_compile= True</code> 即可启动），如果是其它训练过程（假设使用accelerator框架进行）：</p><pre><code class="language-python">if compile:
    s_compile_time = time.time()
    model = torch.compile(model, mode="reduce-overhead")
    accelerator.print(f"Compile Time: {time.time() - s_compile_time:.2f}s")
...
model, optimizer, train_loader, test_loader = accelerator.prepare(model, optimizer, train_loader, test_loader)
</code></pre><p>在测试resnet50在CIFAR10数据集上表现如下（<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/Pytorch-Learning/torch_compile.ipynb">代码</a>）：</p><pre><code class="language-python"># 使用compile
Compile Time: 0.89s
Epoch 00 | Train Time: 12.17s | Batch Time: 0.14764126466245067Train ACC: 11.19% | Test ACC: 11.89%
Epoch 05 | Train Time: 5.72s | Batch Time: 0.046339944917328506Train ACC: 15.49% | Test ACC: 15.54%
Epoch 10 | Train Time: 5.71s | Batch Time: 0.04499245176509935Train ACC: 18.26% | Test ACC: 19.50%
...
Epoch 85 | Train Time: 5.60s | Batch Time: 0.04556511859504544Train ACC: 37.01% | Test ACC: 39.24%
Epoch 90 | Train Time: 5.70s | Batch Time: 0.04652732245776118Train ACC: 37.74% | Test ACC: 40.30%
Epoch 95 | Train Time: 5.66s | Batch Time: 0.04582754933104223Train ACC: 38.30% | Test ACC: 41.03%

# 不使用compile
Epoch 00 | Train Time: 5.71s | Batch Time: 0.05050786174073511Train ACC: 11.29% | Test ACC: 11.41%
Epoch 05 | Train Time: 5.25s | Batch Time: 0.047670155155415436Train ACC: 15.84% | Test ACC: 16.45%
Epoch 10 | Train Time: 6.11s | Batch Time: 0.04854408575564015Train ACC: 18.56% | Test ACC: 19.64%
...
Epoch 85 | Train Time: 6.00s | Batch Time: 0.048004476391539284Train ACC: 36.92% | Test ACC: 39.01%
Epoch 90 | Train Time: 5.05s | Batch Time: 0.04837274064823073Train ACC: 37.71% | Test ACC: 39.21%
Epoch 95 | Train Time: 5.30s | Batch Time: 0.04843420398478605Train ACC: 38.82% | Test ACC: 40.20%
</code></pre><p>从上述结果上看，最后ACC差异不大，但是在每个epoch以及batch_time上还是有差异的，于此同时对于在GRPOTrainer上表现如下（只看loss和奖励值，测试的数据以及模型为trl-lib/DeepMath-103K和Qwen2-0.5B-Instruct，其中只使用1%数据）：<br/><img src="https://files.seeusercontent.com/2026/05/07/Bgr7/image20260408142215222.webp" alt="20260408142215222"/><br/><img src="https://files.seeusercontent.com/2026/05/07/2Yug/image20260408142251113.webp" alt="20260408142251113"/><br/> 通过在Resnet以及GRPO两种训练中发现，时间上都会减少并且在模型最后效果是差异不大。下面进一步解释上面没有解释的几个概念。</p><h4 id="torchcompile-参数"><code>torch.compile</code> 参数</h4><p>官方<a href="https://docs.pytorch.org/docs/stable/generated/torch.compile.html">参数</a>中提供的核心参数如下：</p><ol><li><code>backend</code> (后端)：决定了计算图最终被转化为何种形式。<code>inductor</code> (默认值)：这是最推荐的选择。它使用 TorchInductor 后端，将代码编译为 Triton (针对 GPU) 或 C++ (针对 CPU)。它能提供最深的算子融合和内存优化。<code>cudagraphs</code>：利用 NVIDIA 的 CUDA Graphs 技术，通过减少 CPU 启动 Kernel 的开销来加速小模型。<code>其他</code>：如 onnxrt (ONNX Runtime) 或 tvm，通常用于特定的硬件部署场景。</li><li><code>mode</code> (预设模式)：1、 <code>default</code>：默认模式；2、 <code>reduce-overhead</code>：减少开销模式（使用 CUDA Graphs 减少 CPU 启动开销，最适合小 batch 或推理场景，但会增加显存占用）；3、 <code>max-autotune</code>：最大自动调优模式（使用 Triton 优化算子，如 ReLU、Softmax 等，编译时间较长）</li><li><code>fullgraph</code>（全图捕捉）：<code>False</code> (默认)：如果编译器遇到无法处理的 Python 代码（如使用了复杂的第三方库或特殊的 print 语句），它会将图<strong>拆分</strong>成几个小图，中间夹杂着 Python 解释器执行。<code>True</code>：强制要求整个模型被捕捉为一张完整的计算图。如果模型中存在无法编译的代码，会直接报错。这通常用于追求极致性能的导出场景。</li><li><code>options</code>可以直接向底层后端传递特定的优化指令。可以直接通过 <code>torch._inductor.list_options()</code>查看支持哪些操作，除此之外按照官方文档中介绍的几种处理方式：<br/><img src="https://files.seeusercontent.com/2026/05/07/8aYn/20260420172724.webp" alt="20260420172724"/></li></ol><h3 id="基础概念">基础概念</h3><p>对于最开始提到的3组合概念：1、dynamo；2、AOTAutograd；3、TorchInductor下面逐一进行介绍</p><h4 id="dynamo">dynamo</h4><p>最上面提到在使用 compile 之前会去获取计算图，之所以要提前获取计算图是因为：pytorch中有些计算如 <code>ReLU(Add(A, B))</code>等，执行逻辑就是先add而后计算relu，但是如果提前获取计算图可以直接通过triton将两部合并为一段代码进而减少计算提高速度（这就是动态计算图和静态计算图之间一个较大的差异，动态计算图便计算边获取图，静态提前获取整个计算图），参考<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">4</a></sup>这些内容理解。比如说在官方示例中对于compile获取计算图过程为：</p><pre><code class="language-python">from typing import List
import torch
from torch import _dynamo as torchdynamo
def my_compiler(gm: torch.fx.GraphModule, example_inputs: List[torch.Tensor]):
    print("my_compiler() called with FX graph:")
    gm.graph.print_tabular()
    return gm.forward  # return a python callable

@torchdynamo.optimize(my_compiler)
def toy_example(a, b):
    x = a / (torch.abs(a) + 1)
    if b.sum() &lt; 0:
        b = b * -1
    return x * b
for _ in range(100):
    toy_example(torch.randn(10), torch.randn(10))
</code></pre><p>得到的输出是：<br/><img src="https://files.seeusercontent.com/2026/05/07/gI9c/image20260410163138961.webp" alt=""/></p><p>对于上述参数解释如下：1、<code>opcode</code>（操作码），<code>placeholder</code>: 函数的输入参数（入口）。 <code>call_function</code>: 调用一个 Python 函数（如 add, mul）。 <code>call_method</code>: 调用一个对象的方法（如 tensor.sum()）。 <code>output</code>: 整个图的返回值（出口）；2、<code>name</code>: 这个节点在图中的唯一名称（可以理解为变量名）。 3、<code>target</code>: 实际执行的具体函数或目标。 4、<code>args / kwargs</code>: 该操作需要的输入参数。如果参数是 abs_1，表示它引用了前面名为 abs_1 节点的输出。</p><p><strong>第一个图：逻辑分支的“上半部分”</strong>（对应我的 <code>x = a / (torch.abs(a) + 1)</code>） ，这个图展示了一段包含条件判断的计算逻辑：<br/><em>输入</em>: 接收两个输入 l_a_ 和 l_b_。<br/><em>计算路径A</em>: 计算 abs(l_a_) + 1，然后计算 l_a_ / (abs(l_a_) + 1)，结果存为 x。<br/><em>计算路径B</em>: 计算 l_b_.sum()，结果存为 sum_1。<br/><em>条件判定</em>: 判断 sum_1 &lt; 0，结果存为 lt（此时其为一个bool类型数据）。<br/><strong>输出</strong>: 返回了一个元组 (lt, x)。 之所以要返回一个bool类型数据是因为 TorchDynamo 遇到了图中断（Graph Break）。Python 的 if 分支通常无法直接被编译进同一个静态图中。它先编译到 if 判定的地方，根据 lt 的真假，再决定后面走哪个子图。</p><p><strong>后两个图</strong>：分支后的执行路径，由于代码中可能存在类似 <code>if sum(b) &lt; 0: return x * (-b) else: return x * b</code> 的判断语法，编译器生成了两个不同的子图：<strong>子图 2</strong>：执行的是 x * (b * -1)。这对应 sum(b) &lt; 0 成立时的逻辑。<strong>子图 3</strong> ：执行的是 x * b。这对应 sum(b) &lt; 0 不成立时的逻辑。那么最后通过上述过程将图处理为：</p><pre><code class="language-python">def forward(a, b):
    # 对应第一个图
    abs_1 = abs(a)
    add = abs_1 + 1
    x = a / add
    sum_1 = b.sum()
    
    # 此时发生了 Graph Break (图中断)
    # 因为后端需要知道 lt 是 True 还是 False 才能继续
    if sum_1 &lt; 0:
        # 对应第二个图
        return x * (b * -1)
    else:
        # 对应第三个图
        return x * b
</code></pre><p>compile中具体获取计算图过程<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup><sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">6</a></sup>是直接通过<strong>捕获计算图是在翻译 Python 字节码<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">7</a></sup>的过程中实现的</strong>，还是用最上面的例子直接通过 <code>dis.dis(toy_example)</code> 输出得到字节码：</p><blockquote><p>对于 <strong>字节码</strong> 简单理解为：在执行python代码（源码）之前会将代码进行编译得到所谓的字节码，然后直接将字节码交给虚拟机执行，那么在 <code>dynamo</code> 过程中则是 <strong>逐条解释字节码，遇到tensor操作将其记录为计算图节点</strong>（可以简单将 计算图 理解为一个栈，遇到和pytorch相关计算就放到这个 栈 中）</p></blockquote><pre><code class="language-markdown"> 11           0 RESUME                   0

 12           2 LOAD_FAST                0 (a)
              4 LOAD_GLOBAL              1 (NULL + torch)
             14 LOAD_ATTR                2 (abs)
             34 LOAD_FAST                0 (a)
             36 CALL                     1
             44 LOAD_CONST               1 (1)
             46 BINARY_OP                0 (+)
             50 BINARY_OP               11 (/)
             54 STORE_FAST               2 (x)

 13          56 LOAD_FAST                1 (b)
             58 LOAD_ATTR                5 (NULL|self + sum)
             78 CALL                     0
             86 LOAD_CONST               2 (0)
             88 COMPARE_OP               2 (&lt;)
             92 POP_JUMP_IF_FALSE        5 (to 104)

 14          94 LOAD_FAST                1 (b)
             96 LOAD_CONST               3 (-1)
             98 BINARY_OP                5 (*)
            102 STORE_FAST               1 (b)

 15     &gt;&gt;  104 LOAD_FAST                2 (x)
            106 LOAD_FAST                1 (b)
            108 BINARY_OP                5 (*)
            112 RETURN_VALUE
</code></pre><p>那么dynamo利用这个字节码的逻辑就是，<strong>输入</strong>：Python 字节码。 <strong>处理</strong>：逐条扫描字节码，若是 Tensor 运算就记在 FX Graph 账本上，若是普通 Python 逻辑就正常模拟。 <strong>结果</strong>：产生一个高效的 FX Graph + 一组确保安全的 Guards。</p><h4 id="aotautograd">AOTAutograd</h4><p>dynamo只是获取了forward过程，模型优化（backward过程）在compile中则是通过AOTAutograd进行处理<strong>为推理出来的计算图，自动生成配套的反向传播（Backward）计算图</strong>。在pytorch中还有一中计算梯度方式也是最常见的计算方式autograd对于这两种之间差异在于：<br/> 1、<code>Autograd</code>：你只写前向传播（forward），PyTorch 会在运行时<strong>动态记录每一步操作</strong>，自动构建一个计算图（computational graph）。当你对 loss 调用 <code>.backward()</code> 时，它就沿着这个图反向走一遍，用链式法则自动算出所有参数的梯度。 整个过程中这个图是动态、临时的：<strong>每次 forward 都会重新建图，用完就销毁</strong>（除非手动 <code>retain_graph=True</code>）。优点是超级灵活——支持 if、for 循环、任意 Python 代码，调试也方便。但缺点是每次都要重新建图，Python 开销大，不容易做全局优化。<br/> 2、<code>AOTAutograd</code>：它和普通 Autograd 的最大区别是：不是在运行时动态建图，而是<strong>提前就把前向和反向的整个计算图一次性捕获</strong>。第一次运行时，用“假张量”（FakeTensor）模拟一遍 forward，记录下所有操作，生成两个静态的 FX Graph（一个 forward，一个 backward）。这两个图是可分析、可复用、可优化的 Python 对象。</p><blockquote><p><strong>简单总结就是</strong>：<code>Autograd</code>每次计算都会重新去构建图，<code>AOTAutograd</code>提前将图创建好，下次用直接按照图去运行即可（省去创建费时）</p></blockquote><h4 id="torchinductor">TorchInductor</h4><p>主要是对算子进行融合，比如说$y = \text{ReLU}(Ax + b)$，传统方式要读写三次显存，Inductor 会将其合并为一个 Kernel，数据读入显卡后一次性算完再写回。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://pytorch.org/blog/computational-graphs-constructed-in-pytorch/ <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/680288200?utm_psn=2025772286163067927">https://zhuanlan.zhihu.com/p/680288200?utm_psn=2025772286163067927</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://docs.pytorch.org/tutorials/intermediate/torch_compile_tutorial.html">https://docs.pytorch.org/tutorials/intermediate/torch_compile_tutorial.html</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:8" role="doc-endnote"><p><a href="https://docs.pytorch.org/docs/stable/user_guide/torch_compiler/torch.compiler_dynamo_overview.html">https://docs.pytorch.org/docs/stable/user_guide/torch_compiler/torch.compiler_dynamo_overview.html</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/644590863">https://zhuanlan.zhihu.com/p/644590863</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/630933479">https://zhuanlan.zhihu.com/p/630933479</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="https://docs.python.org/zh-cn/3/library/dis.html">https://docs.python.org/zh-cn/3/library/dis.html</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>pytorch</category><pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/04/10/torch_basic_torch_compile.html</guid></item><item><title>🔥Pytorch使用-2：dataloader处理过程及模型训练性能分析</title><link>https://www.big-yellow-j.top/posts/2026/03/31/ModelTraining2.html</link><description><![CDATA[PyTorch训练推理场景性能瓶颈分为CPU、GPU计算、I/O、多卡通信、框架开销五类，对应不同判别特征：GPU利用率波动跳变对应CPU瓶颈，训练起步慢后续提速对应I/O瓶颈，NCCL AllReduce耗时占比超30%对应多卡通信瓶颈。宏观可通过bpytop、nvidia-smi、iotop快速排查CPU、GPU、磁盘占用，微观借助torch.profiler的概览、算子、追踪等视图精确定位耗时节点。可调整Dataloader的num_workers、pin_memory等参数优化数据加载，超大数据用IterableDataset、WebDataset或Hugging Face流式加...]]></description><content:encoded><![CDATA[<p>在模型训练过程中，通过分析模型损失、准确率这些基础指标去判别模型优化效果，通过flash-attn、混合精度训练等去优化模型训练速度，但是训练过程中对于设备性能瓶颈分析似乎做的比较少，比如说CPU、GPU使用率等，下面内容系统分析一下如何去分析训练/推理过程中的性能瓶颈。在介绍工具使用之前首先了解在使用pytroch进行训练过程中设备之间处理顺序是什么：<code>磁盘 → 内存 → CPU → GPU（前向）→ GPU（反向）→ GPU（参数更新）→ 内存 → 磁盘（可选）</code>，一般而言对于数据处理（<strong>主要是通过CPU进行数据处理，如数据增强等</strong>），这个过程主要是 <code>磁盘 → 内存 → CPU</code>，而后就是将处理后的数据交给GPU进行计算。<strong>训练过程中瓶颈分析</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>：</p><p><strong>CPU 瓶颈</strong>比较好认。GPU 利用率像心电图一样上下跳动，高的时候在算，低的时候在等数据。htop 一看，CPU 某几个核打满了，其他的闲着，DataLoader 的 worker 数量没配对。<strong>GPU 计算瓶颈</strong>的表现是利用率高，但实际吞吐量低。这时候得看 MFU（Model FLOPs Utilization），如果 MFU 很低，说明 GPU 算力没被喂饱。可能是算子实现效率差，也可能是 kernel 太碎，调度开销太大。<strong>I/O 瓶颈</strong>有个很典型的症状：训练刚开始特别慢，跑几个 step 之后速度才上来。因为第一批数据要从磁盘读，后面的数据可能已经缓存到内存里了。iotop 一看，磁盘读写爆高，CPU 反而不怎么忙。<strong>多卡训练的通信瓶颈</strong>也好判断。看 nvidia-smi，某几张卡利用率明显比其他的低，它们在等梯度同步。在 profiler 里看 NCCL 相关操作，如果 AllReduce 的时间占到 30% 以上，就是通信在拖。还有个容易被忽略的：框架开销。Python 解释器本身、GIL 锁、过多的 Python 层函数调用，这些都会吃掉时间。在 <code>torch.profiler </code>的 CPU trace 里，如果看到大量时间花在 Python 调度上而不是实际计算上，就是这个问题。</p><blockquote><p>绝大部分时间 kernel算子一般都是优化比较好的（除非你自己去写算子），绝大多数情况下优化dataloader过程基本可以满足需求</p></blockquote><h2 id="dataloader过程">dataloader过程</h2><p>平时写代码过程中对于 <code>dataloader</code>过程处理比较简单：</p><pre><code class="language-python">from torch.utils.data import DataLoader, Dataset
class CustomDataset(Dataset):
    def __init__(self, ...):
        ...
    def __len__(self):
        # 一般就是直接返回数据数量
        ...
    def __getitem__(self, idx):
        # 一般就是对数据进行处理如标准化等
        ...
        # 如果处理报错就可以直接去下一个数据处理
        # next_index = (index + 1) % len(self)
        # return self.__getitem__(next_index)
    def collate_fn(self, batches):
        batch_size = len(batches)
        # 解包 batches 在 __getitem__ 中返回什么就解包得到什么
        _, _ = zip(*batches)
train_dataset = CustomDataset(xxx)
train_loader = DataLoader(
    train_dataset,
    batch_size=64,                  # 根据你的 GPU 显存调整，越大越好
    shuffle=True,
    num_workers=8,                  # 根据 CPU 核心数和实验调整（起始建议）
    pin_memory=True,                # 强烈推荐
    prefetch_factor=4,              # 可选，加速预取
    persistent_workers=True,        # 可选，推荐
    drop_last=True                  # 可选，避免小 batch
)
</code></pre><p>介绍dataloader原理之前先去看里面<strong>参数含义</strong>：1、<strong>batch_size</strong>（int）：一次处理多少数据；2、<strong>shuffle</strong>（bool）：是否对数据进行打乱（一般对val数据不打乱）；3、<strong>sampler</strong>：从样本中的采样策略；4、<strong>num_workers</strong>（int）：数据加载进程数量；5、<strong>collate_fn</strong>：对batch数据进行处理操作；6、<strong>pin_memory_device</strong>：指定 pin memory 操作的目标<a href="https://docs.pytorch.org/docs/stable/torch.html#accelerators">设备</a>（通常配合 pin_memory=True 使用）；7、<strong>pin_memory</strong>（bool）：<em>数据提前复制到cuda显存中</em>。一般而言除去常用几个参数即使去调整 <code>pin_memory</code> 去加快数据加载速度，一般而言对于 <code>num_workers</code> 并非越大越好（习惯用8），可以通过实则调节而后通过 <code>torch.profile</code>去分析数据加载费时。</p><p><strong>这里再去介绍一些Dataloader原理</strong>（图像来自<a href="https://zhuanlan.zhihu.com/p/1936349147797120821">知乎</a>）：</p><p><img src="https://files.seeusercontent.com/2026/05/07/cj9R/20260417195427.webp" alt="20260417195427"/></p><p><code>Dataset</code> 是整个数据加载流程的基础。它提供了<strong>一个索引访问接口</strong>，主要定义了两个方法：<code>__len__()</code>：返回数据集总样本数，<code>__getitem__(index)</code>：<strong>根据索引返回第 $i$ 个样本</strong>（样本可能来自磁盘读取、内存加载，或实时生成）。Sampler 与 BatchSampler 的作用如下：如果没有手动指定 sampler，且 shuffle=True，DataLoader 会自动使用 RandomSampler，在每个 epoch 开始时对所有样本的索引进行一次性随机打乱。BatchSampler 则负责将打乱后的索引列表，按照 batch_size 分组，形成一个个 batch 的索引列表（例如 [ [45, 7, 23, 12], … ]）。<strong>当 num_workers &gt; 0 时</strong>，DataLoader 会启用多进程数据加载机制，具体流程如下：</p><p>主进程通过 BatchSampler 生成所有已打乱的 batch 索引列表。主进程将每个 batch 的索引（batch_indices）通过 index_queue 分发给不同的 worker 进程。<strong>每个 worker 进程 独立完成</strong>以下工作：接收一个 batch_indices（例如 [45, 7, 23, 12]）多次调用 dataset[i] 获取对应样本在 worker 进程内执行 collate_fn，<strong>将多个样本组装成一个完整的 batch</strong>将组装好的 batch 通过 worker_result_queue 发送回主进程，主进程接收到 batch 后，若设置了 pin_memory=True，则通过后台线程将其转换为 pinned memory（页锁定内存），最后将 batch 返回给训练循环使用。</p><p>对于 <code>Dataloader</code> 中可以做的修改内容不多，比如有比较常见的几种情况：1、我的数据（dataset）中我的数据有1T，设备处不了如何做？2、如何取定义自己的采样器？<br/><strong>1、处理大批量数据</strong>：一般在定义 dataset（pytorch中提供两种数据类型 <code>Map-Style Datasets</code> 以及 <code>Iterable-Style Datasets</code>） 过程中是直接通过 <code>__len__</code> 以及 <code>__getiem__</code> 去获取数据信息，如果数据有1T直接打开然后 <code>len</code> 势必导致问题，比较简单方式直接通过pytorch原生的 <code>IterableDataset</code> 进行处理将数据转化为数据流，除此之外还可以通过使用huggingface datasets中流式模式 <code>dataset = load_dataset("json", data_files="data/1T.jsonl", streaming=True)</code>，如果对于图像等可以直接使用 <code>WebDataset</code> 方式，以使用 <code>IterableDataset</code> 为例：</p><pre><code class="language-python">import json
import torch
from torch.utils.data import IterableDataset, DataLoader

class IterableDatasetJsonl(IterableDataset):
  def __init__(self, file_path, file_type: str='jsonl', shard_rank=0, num_shards=1):
    self.file_path = file_path
    self.file_type = file_type
    self.shard_rank = shard_rank
    self.num_shards = num_shards

  def __iter__(self):
    worker_info = torch.utils.data.get_worker_info()
    if worker_info is not None:
      shard_rank = self.shard_rank * worker_info.num_workers + worker_info.id
      num_shards = self.num_shards * worker_info.num_workers
    else:
      shard_rank, num_shards = self.shard_rank, self.num_shards

    with open(self.file_path, 'r', encoding='utf-8') as f:
      for i, line in enumerate(f):
        if i % num_shards == shard_rank:
          sample = json.loads(line)
          '''
          继续后处理
          '''
          yield sample
</code></pre><p>对于 <code>IterableDataset</code> 使用区别 datatset 最大差异就是直接在 iter 中对数据进行处理</p><p><strong>2、使用不同的采样器</strong>，最常见的情况就是，1、对于图像进行分桶输入（比如有1024x1024也有1024x768），3、控制数据分布，如数据自身具有一定特征需要控制这些特征在一个batch中分布相对一致。</p><blockquote><p>对于文本情况，如果需要输入 bs&gt;1 一般而言直接定义 <code>collate_fn</code> 提前去对文本进行 padding 并且返回 mask（一般模型都可以接受mask作为输入），padding方式： <code>padded_input_ids = pad_sequence(truncated, batch_first=True, padding_value=pad_token_id)</code>（<code>from torch.nn.utils.rnn import pad_sequence </code>）</p></blockquote><pre><code class="language-python">import torch
import random
from collections import defaultdict
from torch.utils.data import DataLoader, Sampler, Dataset
class BucketBatchSampler(Sampler):
  def __init__(self, dataset, batch_size, drop_last=True, shuffle=True):
    self.dataset = dataset
    self.batch_size = batch_size
    self.drop_last = drop_last
    self.shuffle = shuffle

    self.buckets = defaultdict(list)
    # 直接去便利所有数据 在效率上存在一定欠缺
    for idx in range(len(dataset)):
      h, w = dataset.get_image_size(idx)
      self.buckets[(h, w)].append(idx)
    self.bucket_keys = list(self.buckets.keys())

  def __iter__(self):
    batches = []
    for key in self.bucket_keys:
      indices = self.buckets[key][:]
      if self.shuffle:
        random.shuffle(indices)

      for i in range(0, len(indices), self.batch_size):
        batch = indices[i:i + self.batch_size]
        if self.drop_last and len(batch) &lt; self.batch_size:
          continue
        if batch:
          batches.append(batch)

    if self.shuffle:
      random.shuffle(batches)

    for batch in batches:
      yield batch

  def __len__(self):
    total = 0
    for indices in self.buckets.values():
      total += len(indices) // self.batch_size
      if not self.drop_last and len(indices) % self.batch_size != 0:
        total += 1
    return total
</code></pre><p>对于上述代码中在sampler中提前遍历了所有的数据并且根据分辨率进行分组（实际可能需要对图像进行分辨率计算–&gt;根据计算预估分组） ，<code>__iter__</code>：定义如何生成采样顺序（在使用 <code>for batch in dataloader</code> 中就会使用这个方法），最后通过 <code>yield</code> 进行返回（因为 <code>__iter__</code> 必须返回一个迭代器也可以使用 <code>return</code>），<code>__len__</code>：返回 该采样器在当前配置下会产生多少个 batch（或多少个样本）</p><h2 id="宏观指标分析">宏观指标分析</h2><p>最简单分析方法直接在模型运行过程中使用 <code>time</code> 去记录时间就可以快速了解每一个阶段耗时统计，除此之外还可以直接基于linux（假设服务器为linux Ubuntu系统）的基础命令进行分析，主要是分析CPU内存使用情况、GPU使用情况、磁盘io使用情况。<strong>CPU性能分析</strong>，一般而言可以直接使用htop、top、bytop等工具直接去看，这里直接使用<strong>bytop</strong>工具进行性能分析，首先安装bytop<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>（<code>pip3 install bpytop --upgrade</code> 或者直接使用 <code>sudo apt install bpytop</code>），而后就可以直接终端使用 <code>bpytop</code>就可以看到各项性能分析<br/><img src="https://files.seeusercontent.com/2026/05/07/1uGv/image20260401153039800.webp" alt=""/><br/><img src="https://files.seeusercontent.com/2026/05/07/xUi7/image20260401153015796.webp" alt=""/><br/> 使用方法比较简单直接通过数字选择（直接键盘输入数字）需要看到的面板：</p><pre><code>1：显示/关闭 CPU性能分析
2：显示/关闭 内存/存储性能分析
3：显示/关闭 网络分析
4：显示/关闭 各项进程进行分析
</code></pre><p>首先通过上述 <code>bpytop</code>就可以简单了解各项进程上在内存上使用情况如何、CPU使用情况如何。<strong>GPU性能分析</strong>，对于GPU性能分析最简单工具直接使用 <code>watch -n 0.1 nvidia-smi</code> 每0.1s刷新nvidia-smi情况，主要是去看GPU利用率、显存占用情况<br/><img src="https://files.seeusercontent.com/2026/05/07/7njW/image20260401153751296.webp" alt=""/><br/><strong>值得注意的是</strong>，<strong>有些时候即使将所有的在跑的程序都关闭但是发现显存还是被占用（利用率是0）</strong><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>使用<code>ps -ef</code>命令<br/><img src="https://files.seeusercontent.com/2026/05/07/Px8l/image-20220713171325500.webp" alt=""/><br/> 可以看到PID、PPID、CMD 3列重要信息，分别是进程ID、父进程ID、进程的启动命令。通过命令可以判断哪些进程是自己程序启动的进程，比如上方的python train.py就是我启动的进程，其他的均为系统进程或无关显存占用的进程。接下来杀死进程：从截图中看到python train.py程序的进程ID是594 和797，那么可以使用<code>kill -9 594 797</code>命令来结束进程。</p><p>但是常常占用显存的进程会很多，特别是在多卡并行时，按此方法会比较繁琐，以下介绍一种更强大的方式结束进程：通过<code>ps -ef</code>能看出，我自己的进程都包含了train关键字（并且其他无关的系统进程没有包含，防止误杀），那么使用grep命令可以过滤出我自己的进程，例如：<br/><img src="https://files.seeusercontent.com/2026/05/07/paU5/image-20220713172143285.webp" alt=""/><br/> 接下来是获取进程的ID，此时可以使用awk命令，awk命令用法复杂，这里简单记住以下命令即可：<br/><img src="https://files.seeusercontent.com/2026/05/07/Kl9h/image-20220713172301267.webp" alt=""/><br/> 最后再通过kill命令，即可完整的结束进程。完整命令为<code>ps -ef | grep train | awk '{print $2}' | xargs kill -9</code><br/><img src="https://files.seeusercontent.com/2026/05/07/uOw7/image-20220713172428298.webp" alt=""/><br/> 以上输出中会多出来一个No such process的错误，可以忽略，出现原因是grep train也会产生一个进程，被自己过滤出来。</p><h2 id="微观指标分析">微观指标分析</h2><p>上面介绍了宏观指标去看CPU/GPU/磁盘/内存之间的使用情况，最好的情况就是这几项的指标都要上去保证在一个较好的情况下，下面进一步介绍更加微观的指标</p><h3 id="基于torch-profiler分析">基于torch profiler分析</h3><p>直接使用torch原生工具<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>进行性能分析可以帮助我们分析和优化模型的执行时间、GPU 利用率、内存带宽等性能指标。通过 torch.profiler，你可以了解每一层模型在设备上的执行情况，分析 GPU 资源的利用率（<strong>再了解每一块的耗时之后就可以直接再去争对耗时较长的内容进一步分析优化了</strong>），具体代码测试过程中使用方法比较简单：</p><pre><code class="language-python">from torch.profiler import profile, ProfilerActivity, tensorboard_trace_handler
# 首先初始化 profile 
# 如果要使用tensorboard需要额外安装 pip install torch-tb-profiler
...

self.accelerator = Accelerator(...,log_with='tensorboard',project_dir=args.log_dir)
...
log_root = self.args.log_dir
if self.accelerator.is_main_process:
    profiler = profile(
        activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
        schedule=torch.profiler.schedule(
            wait=1,      # 等待步数
            warmup=1,    # 预热步数
            active=3,    # 活跃步数
            repeat=2     # 重复次数
        ),
        on_trace_ready=tensorboard_trace_handler(log_dir),
        record_shapes=True,            # 记录张量形状
        profile_memory=True,           # 记录内存使用
        with_stack=True,               # 记录调用栈
        with_flops=True                # 计算 FLOPs
    )
    profiler.start()
...
for epoch in range(1, self.num_epochs + 1):
    for batch_idx, (images, labels) in enumerate(self.train_loader):
        ...# 模型训练过程
        if profiler and self.accelerator.is_main_process:
            profiler.step()
if prof:
    # profiler.export_chrome_trace("trace.json")
    prof.stop()
</code></pre><p>torch profile使用比较简单就是先初始化而后<code>start()</code>启动记录器、<code>step()</code>记录结果、<code>stop()</code>停止记录，而后直接通过 <code>tensorboard --logdir logs/</code> 即可（ <strong>值得注意的是</strong>，上面代码只会记录少数步，当 <code>repeat=0</code>时候就会一直记录，不需要频繁记录那么多），上述过程中需要注意tensorboard和profile的存储的最终的文件夹要保持一致，对于启动后的在tensorboard中视图中各项结果分析如下<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>：<br/><strong>Overview（概览）</strong>：这个页面能帮你快速判断性能瓶颈在哪。<br/><img src="https://files.seeusercontent.com/2026/05/07/Cta3/image20260401215011996.webp" alt=""/><br/> 主要关注红框中内容，它会将每个Step（迭代）的时间拆分成 <strong>Kernel</strong>（计算）、<strong>Memcpy</strong>（数据传输）、<strong>Memset</strong>（GPU内存设置时间）、<strong>DataLoader</strong>（数据加载） 和 <strong>CPU Exec</strong>（CPU计算） 等几部分。如果”Kernel”占比低而”DataLoader”很高，说明数据加载是瓶颈；如果”CPU Exec”很高，则说明CPU侧的算子或逻辑存在优化空间。<br/><strong>Operator（算子）</strong>：这个表格是所有PyTorch操作（如aten::convolution）的性能数据。<br/><img src="https://files.seeusercontent.com/2026/05/07/0tYa/image20260401215618602.webp" alt=""/></p><p>主要关注红框中内容，Calls（运行过程中被使用次数）、Device xxx Duration（在 GPU 上花费的累计时间）、Host xxx Duration（在主机上花费时间），分析过程中主要是去更具耗时最长的就是优化重点。如果开启了with_stack=True，点击”Call Stack”还能直接跳转到你代码中调用该算子的位置<br/><strong>Trace（追踪）</strong>：这个时间线视图最直观，能让你看到每个算子和CUDA Kernel的精确起止时间。使用方法：在Chrome浏览器打开 chrome://tracing，然后加载生成的JSON文件。或者直接在TensorBoard的Trace页面分析。你可以通过鼠标滚轮缩放，并利用右上角的 Flow Events 按钮，查看是哪个CPU算子启动了一个GPU Kernel，这对于定位CUDA Kernel的启动延迟问题非常有帮助。</p><p><strong>Memory（内存）</strong>：这个视图展示了内存随时间的分配和释放情况，帮你发现内存泄漏或不必要的显存占用。<br/><img src="https://files.seeusercontent.com/2026/05/07/m1Xn/image20260401220618523.webp" alt=""/></p><p><strong>Kernel（内核）</strong>：这是GPU上执行的底层函数视图。<br/><img src="https://files.seeusercontent.com/2026/05/07/ngD5/image20260401220329364.webp" alt=""/></p><p>主要是去查看GPU利用率（GPU Utilization）、SM效率（Est. SM Efficiency）以及Tensor Core的使用情况。如果这些指标偏低，说明GPU并没有被充分利用。</p><h2 id="调节参数优化">调节参数优化</h2><p>CPU 瓶颈（一般就去修改数据处理过程，如数据增强等操作）可以直接调 num_workers、增大 prefetch_factor、把预处理卸载到 GPU等处理操作。</p><pre><code class="language-python">DataLoader(train_dataset, batch_size=args.batch_size, shuffle=False, num_workers=4, pin_memory=True)
</code></pre><p>I/O 瓶颈就换 SSD、预加载数据到内存、用 NVIDIA DALI。GPU 计算瓶颈就做算子融合、上 torch.compile、开混合精度。通信瓶颈就上梯度压缩、通信计算重叠。框架开销就减少 Python 调用、用 TorchScript 或者 C++ 扩展</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://www.zhihu.com/question/1927112862976972744/answer/2016593596803986385?utm_psn=2022387930946126849">https://www.zhihu.com/question/1927112862976972744/answer/2016593596803986385?utm_psn=2022387930946126849</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://github.com/aristocratos/bpytop">https://github.com/aristocratos/bpytop</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://www.autodl.com/docs/qa4/">https://www.autodl.com/docs/qa4/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://pytorch-cn.com/tutorials/recipes/recipes/profiler_recipe.html">https://pytorch-cn.com/tutorials/recipes/recipes/profiler_recipe.html</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://github.com/pytorch/kineto/blob/main/tb_plugin/README.md">https://github.com/pytorch/kineto/blob/main/tb_plugin/README.md</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>pytorch</category><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/31/ModelTraining2.html</guid></item><item><title>强化学习算法-3：GSPO、SAPO及KL散度改进</title><link>https://www.big-yellow-j.top/posts/2026/03/26/RLHF-3.html</link><description><![CDATA[针对GRPO存在的token级重要性采样与序列级奖励不匹配、路由阶段不一致易导致专家训练崩溃问题，Qwen团队提出GSPO算法，取消token平均改为序列级重要性计算，在TRL中可通过GRPOConfig设置importance_sampling_level为sequence快速启用。针对GRPO硬裁剪脆性问题，Qwen团队推出SAPO算法，将硬裁剪升级为温度控制软裁剪，兼顾序列一致性与token自适应，在TRL中设置loss_type为sapo即可启用。KL散度计算主流采用K3 Bregman估计器，DeepSeek-V3.2对其进行修正，解决原始K3梯度权重无界、训练动态不稳定问题。]]></description><content:encoded><![CDATA[<p>系列文章： <br/> 1、<a href="https://www.big-yellow-j.top/posts/2026/02/26/RLHF-1.html">强化学习算法-1：GRPO、DPO与PPO解析</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2026/03/01/RLHF-2.html">强化学习算法-2：熵坍缩以及奖励坍缩问题机制分析及解决措施</a><br/> 3、<a href="https://www.big-yellow-j.top/posts/2026/03/26/RLHF-3.html">强化学习算法-3：GSPO、SAPO及KL散度改进</a></p><h2 id="gspo">GSPO</h2><blockquote><p><strong>处理长序列优化问题，将token级别处理为sequence级别</strong></p></blockquote><p>Qwen团队论文<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>里面首先分析在GRPO中存在如下几点问题：1、奖励函数一般是sequence级别的（对整个回答进行评分）但是却对每个token去计算重要性比率；2、不同专家被不同token激活，导致路由（routing）在rollout和training阶段不一致，GRPO的token级噪声会直接把某些专家“训崩”。针对上述两点问题在GSPO中损失函数为：<br/><img src="https://files.seeusercontent.com/2026/05/07/H5yk/image20260326214529171.webp" alt=""/><br/> 区别与GRPO中改进就在于：<strong>1、去掉了对于token的平均</strong>（在GRPO中对于模型输出会计算token平均：$\frac{1}{\vert o_i \vert}\sum_{t=1}^{\vert o_i \vert}$）；<strong>2、计算sequence重要性</strong>：将GRPO中计算方式由 $r_{i,t}(\theta)=\frac{\pi_{\theta}(o_{i,t} \vert q,o_{i&lt;t})}{\pi_{\theta_{old}}(o_{i,t} \vert q,o_{i&lt;t})}$改为 $s_i(\theta)=(\frac{\pi_{\theta}(o_i \vert q)}{\pi_{\theta}(o_i \vert q)})^{\frac{1}{\vert o_i \vert}}$。通过改进GSPO区别GRPO表现：<br/><img src="https://files.seeusercontent.com/2026/05/07/d7Hx/image20260326215727398.webp" alt=""/><br/> 两部分算法在<strong>代码差异点</strong>如下（<a href="https://github.com/huggingface/trl/blob/134e015aec24a3a0433992703722598eead04d08/trl/trainer/grpo_trainer.py#L2302-L2307">Github-GRPOTrainer</a>）：</p><pre><code class="language-python">log_ratio = per_token_logps - old_per_token_logps # shape: [batch*G, seq_len]
if self.importance_sampling_level == "token":
    log_importance_weights = log_ratio # 每个token都一个独立权重
elif self.importance_sampling_level == "sequence":
    log_importance_weights = (log_ratio * mask).sum(-1) / mask.sum(-1).clamp(min=1.0) # 所有token加权求和得到 sequence
    log_importance_weights = log_importance_weights.unsqueeze(-1) # # shape: [batch*G, 1] 得到sequence级别
coef_1 = torch.exp(log_importance_weights)
</code></pre><p><strong>在trl中要实现GSPO</strong>直接使用可以直接使用参数：<code>GRPOConfig(importance_sampling_level="sequence",)</code> 就可以切换到GSPO优化了，按照论文里面参数配置</p><pre><code class="language-python">training_args = GRPOConfig(
    importance_sampling_level="sequence",
    beta=0.0,# 不加KL散度
    epsilon=3e-4,# clipping 下界
    epsilon_high=4e-4,# clipping 上界（
)
</code></pre><h2 id="sapo">SAPO</h2><p>Qwen团队论文<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>核心目标是解决“硬clipping的脆性问题”（当一条序列中只要有少数几个 token 的 ratio 超出 clipping 范围，整个序列的梯度就会被全部或大量抑制）。它在保持group-based RL（去Critic、组内相对优势）的基础上，把clipping机制从硬裁剪升级为温度控制的软控制，从而实现sequence-coherent（序列一致性） + token-adaptive（token自适应）的双重优势<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>，其损失函数为：<br/><img src="https://files.seeusercontent.com/2026/05/07/fJh7/image20260326223541434.webp" alt=""/></p><p>整个损失函数还是基于GRPO进行出发，将内部的 $\min(r, clip(r,1-\epsilon, 1+\epsilon))$ 替换为 $f$ 其中 $\sigma$ 是sigmoid函数，$x = r_{i,t}$ 重要性，$\tau$ 是温度参数。而在trl中使用改方法比较简单直接在config中进行指定即可：<code>GRPOConfig(loss_type="sapo")</code>具体的代码处理逻辑为：</p><pre><code class="language-python">elif self.loss_type in ["grpo", "bnpo", "dr_grpo", "dapo", "luspo"]: # trl 中默认直接计算DAPO损失
    coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high)
    if self.args.delta is not None:
        coef_1 = torch.clamp(coef_1, max=self.args.delta)

    per_token_loss1 = coef_1 * advantages
    per_token_loss2 = coef_2 * advantages
    per_token_loss = -torch.min(per_token_loss1, per_token_loss2)
elif self.loss_type == "sapo":
    # 首先计算 温度系数 直接根据优势值 &gt;0--&gt;有利的“行为” &lt;0--&gt;不利的“行为”
    temperatures = torch.where(advantages &gt; 0, self.args.sapo_temperature_pos, self.args.sapo_temperature_neg)
    # 计算软边界
    soft_coef_1 = torch.sigmoid(temperatures * (coef_1 - 1)) * 4 / temperatures
    per_token_loss = -soft_coef_1 * advantages
...
if self.loss_type in ["grpo", "sapo"]:
    loss = ((per_token_loss * mask).sum(-1) / mask.sum(-1).clamp(min=1.0)).mean()
    normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0  # no accum in eval
    loss = loss / normalizer
</code></pre><p>最终模型论文中的表现：<br/><img src="https://files.seeusercontent.com/2026/05/07/7Gri/image20260326224041380.webp" alt=""/></p><h2 id="kl散度改进">KL散度改进</h2><p>KL 散度是用来度量两个概率分布相似度的指标，而计算过程也比较简单直接计算（假设为离散变量）：<br/> \(\mathcal{D}_{KL}(P \Vert Q)=\sum_i P(i)\ln(\frac{P(i)}{Q(i)})\)</p><p>不过对于llm中对于内部的 $\sum$ 过程不可能去计算（因为token的词表是非常大的，直接 $\sum$ 不现实）那么退而求其次，只对模型的输出的token去计算log概率，因此就可以将KL计算过程改写为计算期望过程：<br/> \(\mathcal{D}_{KL}(P \Vert Q)=E_{x～P}[\ln\frac{P(x)}{Q(x)}]\)</p><p>而对于整个期望的计算可以直接用蒙特卡洛估计方式计算：<br/> \(\mathcal{D}_{KL}(P \Vert Q)≈\frac{1}{N}\sum_{i=1}^{N}\ln \frac{P(x)}{Q(x)} \quad x～P\)<br/> 对于上述过程在代码（trl）中计算方式也比较简单直接：</p><pre><code class="language-python">per_token_logps, entropies = self._get_per_token_logps_and_entropies(model,input_ids,...)
if self.beta != 0.0:
    ref_per_token_logps = inputs["ref_per_token_logps"]
    per_token_kl = (
        torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1
    )
</code></pre><p>在具体的KL计算过程中有3种<strong>K1（朴素估计器）</strong>：$K1=\log\frac{P(x)}{Q(x)}$、<strong>K2（平方对数估计量）</strong>：$K2=\frac{1}{2}(\log r)^2$、<strong>K3（Bregman 估计器）</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>：$K3=\frac{P(x)}{Q(x)}-1-\log \frac{P(x)}{Q(x)}$，目前主流使用都是基于K3，在DeepSeek-V3.2论文<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>中对于K3做了修改：<br/><img src="https://files.seeusercontent.com/2026/05/07/3mFq/image20260330162350640.webp" alt="20260330162350640"/><br/> 其中括号内部的为原始的K3计算，在DS中则是补充了一个 $\frac{\pi_\theta}{\pi_{old}}$ 那么按照论文里面的描述添加此项的好处在于在最初的K3计算过程中当 $𝜋_𝜃 ≪ 𝜋_{ref}$ 时候<strong>梯度会赋予过大</strong>且无界的权重以最大化这些标记的似然性，导致梯度更新噪声较大，这些噪声会累积，从而在后续迭代中降低样本质量，并导致训练动态不稳定。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2507.18071">https://arxiv.org/pdf/2507.18071</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2511.20347">https://arxiv.org/pdf/2511.20347</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/1978208354783486737">https://zhuanlan.zhihu.com/p/1978208354783486737</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="http://joschu.net/blog/kl-approx.html">http://joschu.net/blog/kl-approx.html</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2512.02556">https://arxiv.org/pdf/2512.02556</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>强化学习</category><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/26/RLHF-3.html</guid></item><item><title>残差连接————Kimi注意力残差/字节混合注意力</title><link>https://www.big-yellow-j.top/posts/2026/03/21/attention-residuals.html</link><description><![CDATA[传统残差连接通过跳跃连接缓解深度模型梯度消失与退化问题，但存在各层贡献权重一致、浅层信息随层数叠加逐渐被稀释的缺陷，过往门控、加权类改进效果有限。针对该痛点，Kimi提出注意力残差连接，对前序所有block输出计算softmax注意力权重做加权融合，分别在单block计算后、MLP处理前执行两次融合。字节推出混合深度注意力方案，基于GQA的历史KV缓存，同步计算序列维度常规注意力与深度维度历史信息注意力，融合更新输出，解决大模型深度增加后的信号衰减问题。]]></description><content:encoded><![CDATA[<p>本文主要介绍最新的Kimi的注意力残差连接以及字节的“残差”连接两篇论文，在最开始的残差连接方案<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">1</a></sup>中：<br/><img src="https://files.seeusercontent.com/2026/05/07/Sp4t/image20260321105833272.webp" alt=""/><br/> 核心过程就是 $x = x+ f(x)$，随着不断的叠加卷积层数，那么就容易导致 梯度消失以及 退化问题，残差连接就是通过跳跃连接（skip connection），允许输入信息绕过若干层直接传递到后面的层。后续也有很多去对这个过程进行改进比如说使用门控残差连接、加权残差连接、修改连接位置等。不过影响都不是很大，因此对于残差连接过程就一直没有变化还是保持最开始的计算方式了。在kimi以及字节最近新发表两篇论文都是对这个过程做的改进具体解释如下。</p><h2 id="kimi注意力残差连接">Kimi注意力残差连接</h2><p>首先按照论文中逻辑出发，在标准的残差计算中：$h_l=h_{l-1}+f_{l-1}(h_{l-1})$ 对于这个计算方式在计算梯度传播过程中会直接将<strong>每一层的贡献是相同</strong>（直接计算上公式梯度）因此后续论文就提出做一个门控的残差连接方式 $h_l=\alpha_l \cdot h_{l-1}+\beta_l \cdot f_{l-1}(h_{l-1})$，对于上述两种残差注意力方式带来最大的问题就是：<strong>所有层的贡献都是一致的，除此之外后续层只能获取前层的信息导致更加前面层的信息被稀释</strong>（比如说l层只能获取l-1层信息，虽然l-2的信息会融合到l-1层但是l-2还是对l层的作用有限）。因此kimi的attention-residual出发点就是让后续层可以看到更加前面层的信息以及通过一个合适权重去控制残差连接，基于这个论文<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>里面提出方案如下图c中描述：<br/><img src="https://files.seeusercontent.com/2026/05/07/9iXr/image20260321111201650.webp" alt=""/><br/> 对于<em>第n个block我将前面的几层的输出都进加权融合作为第n层的输入</em>，具体融合方式为：<br/> \(h_l= \alpha_{0\rightarrow l}\cdot h_1+ \sum_{i=1}^{l-1}\alpha_{i\rightarrow l}\cdot f_i(h_i)\)<br/> 其中 $\sum_{i=1}^{l-1}\alpha_{i\rightarrow l}=1$那么对于权重系数 $\alpha_{i\rightarrow l}$ 的计算方式为：<br/><img src="https://files.seeusercontent.com/2026/05/07/T3mt/image20260321213723236.webp" alt=""/><br/> 其实也就是计算softmax的注意力权重，里面的 $q_l=w_l$ 通过一个学习的向量以及历史层的输出去计算softmax值去控制权重特征融合。去看代码具体过程：</p><pre><code class="language-python">def block_attn_res(blocks: list[Tensor], partial_block: Tensor, proj: Linear, norm: RMSNorm) -&gt; Tensor:
    V = torch.stack(blocks + [partial_block]) # [N+1, B, T, D]
    K = norm(V)
    logits = torch.einsum('d, n b t d -&gt; n b t', proj.weight.squeeze(), K)
    h = torch.einsum('n b t, n b t d -&gt; b t d', logits.softmax(0), V)
    return h

def forward(self, blocks: list[Tensor], hidden_states: Tensor) -&gt; tuple[list[Tensor], Tensor]:
    partial_block = hidden_states          # 进入当前层的初始 hidden_states（通常是上一层的输出）
    
    # 在 Attention 子层前，先做一次 Block AttnRes
    h = block_attn_res(blocks, partial_block, self.attn_res_proj, self.attn_res_norm)
    
    # 如果当前层是 Block 的边界层 → 把当前 partial_block 作为完整 Block 保存下来
    if self.layer_number % (self.block_size // 2) == 0:
        blocks.append(partial_block)       # blocks 列表增长，新增一个完成的 Block rep
        partial_block = None               # 重置 partial（新 Block 从零开始？代码这里有小问题，实际可能要用 h 或重置逻辑）
    
    # 自注意力子层（标准 Transformer attention）
    attn_out = self.attn(self.attn_norm(h))
    partial_block = partial_block + attn_out if partial_block is not None else attn_out
    # ↑ 标准残差：partial_block += attn_out   （Block 内部用经典 +）

    # 在 MLP 子层前，再做一次 Block AttnRes（用不同的 proj 和 norm）
    h = block_attn_res(blocks, partial_block, self.mlp_res_proj, self.mlp_res_norm)
    
    # MLP 子层
    mlp_out = self.mlp(self.mlp_norm(h))
    partial_block = partial_block + mlp_out               # 再次标准残差累加
    
    return blocks, partial_block    # 返回更新后的 blocks 列表 + 当前 Block 的 partial sum
</code></pre><p>其实通过代码很容易发现在block计算过程就是，输入前将前n层的block特征进行attention-residual方式特征融合，在计算完毕之后进行一个普通的残差连接，而后在将输出进行mlp处理之前再次通过一次attention-residual连接处理。</p><h2 id="字节混合注意力">字节混合注意力</h2><p>在字节论文<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">3</a></sup>中提出混合注意力去解决：<strong>随着 LLM 的深度增加，它们往往会遭遇信号衰减的问题</strong>：在浅层形成的有用特征会因反复的残差更新而逐渐被稀释，使得它们在更深的层中更难恢复（出发点和kimi的attention-residual相同）。<br/><img src="https://files.seeusercontent.com/2026/05/07/w8bY/image20260321220611829.webp" alt=""/><br/> 对于上图中提到的read以及write分别表示的是残差连接方式 $x=x+f(x)$里面分别对于x以及连接方式，比如说对于最开始残差连接我的read就是x（不去对x进行其他处理因此论文里面将其标记为identity）而我的连接方式是add因此将write处理为add。在上图b中选择直接将所有的信息进行拼接（比如说第i层计算输出就行和输入就行concat操作），虽然在信息传播过程是无损的，可以解决上面的信号衰减问题，但是这样会带来显存占用过高。那么论文里面提出Depth Attention处理过程为，对于输入通过相面方式处理：<br/><img src="https://files.seeusercontent.com/2026/05/07/v1xH/image20260321224003992.webp" alt=""/><br/> 其中对于 $K_i$ 以及 $V_i$ 表示的GQA过程中我的历史缓存的kv值而 $Q_{l-1}$ 则是上一层的Q结果，通过注意力融合方式得到最终的输入 $X_l^{in}$ 直接将这个结果解析attention的注意力计算得到 $X_l^{out}$，在得到结果之后通过：<br/><img src="https://files.seeusercontent.com/2026/05/07/Zyx2/image20260321224445649.webp" alt=""/><br/> 又可以得到新的一层的输出结果（相当于替代了之前的残差连接通过相加为线性层处理方式）。除此之外进一步提出升级的 Mixture-of-Depth Attention方式：<br/><img src="https://files.seeusercontent.com/2026/05/07/1uEt/image20260321232204028.webp" alt=""/><br/> 对于上述过程中depth表示所有前面层的深度 KV cache（<strong>对应深度部分</strong>），而QKV则是表示当前层的结果（<strong>对应序列部分</strong>），10-23行处理序列部分注意力（就是比较常规的注意力计算过程），24-29行处理处理深度部分注意力，在计算注意力过程中会用softmax去更新同一个（m, acc, o），相当于将cache部分信息融入到注意力中。</p><h2 id="总结">总结</h2><p>两篇论文中都是为了解决随着层数的叠加带来的“信息遗忘”问题，Kimi中选择直接将“历史block”信息通过注意力融合方式进行加权残差连接（attention-residual）也就是 $y=\alpha \cdot h_l+ \sum_{i=1}^{l-1} \alpha h_i$，具体过程为<strong>将历史所有的block结果和用一个可学习的向量之间计算softmax作为权重</strong> $\alpha$ 具体残差发生在：1、mlp处理前；2、每一个block处理之后。在字节的mixture-of-depth attention处理方式则是直接将GQA中的kv-cache中的KV值用来计算注意力去弥补信息损失，具体过程为在序列部分直接计算常规注意力，<strong>在深度部分（KV cache历史结果）部分通过历史结果去更新在序列部分计算得到的注意力结果</strong>。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/abs/1512.03385">https://arxiv.org/abs/1512.03385</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/abs/2603.15031">https://arxiv.org/abs/2603.15031</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/abs/2603.15619">https://arxiv.org/abs/2603.15619</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>paper</category><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/21/attention-residuals.html</guid></item><item><title>模型推理框架vllm-3——KVCache管理器</title><link>https://www.big-yellow-j.top/posts/2026/03/15/vllm-3.html</link><description><![CDATA[基于vllm 0.17.1版本，详解KV缓存块管理器实现逻辑。默认block_size为16，单块可存储16个token，显存按设备显存固定比率预分配，block数量由可用显存、单块单层字节数、模型层数计算得到，每层预分配形状为[2,num_blocks,block_size,num_kv_heads,head_size]的KV缓存张量。prefill阶段按token数向上取整分配block并写入全量KV，decode阶段增量写入，显存不足时触发请求换出、重排等抢占策略，支持公共前缀缓存复用。KVCacheManager核心能力包含前缀块匹配、槽位分配、块释放、前缀缓存重置，通过slot...]]></description><content:encoded><![CDATA[<p>在之前的Blog分别介绍了<a href="https://www.big-yellow-j.top/posts/2025/12/27/vllm-1.html">vllm整体框架及使用</a>以及在<a href="https://www.big-yellow-j.top/posts/2026/03/15/vllm-2.html">vllm中生成以及调度过程</a> ，本文主要介绍块管理器的原理（<strong>全部是基于</strong>：<code>Version: 0.17.1</code>）。</p><blockquote><p><code>conda create -n vllm_server python=3.12</code><br/><code>conda activate vllm_server</code><br/><code>pip install vllm==0.17.1</code></p></blockquote><p>在之前介绍vllm的调度过程在token_budge下去处理waiting以及running队列，对于里面如何对块进行管理没有介绍，比如说在running和waiting队列中，虽然在token_budege下running队列处理过程，通过理论计算比如说我需要running生成一个token，你的显存能够你去生成这一个token吗？以及在waiting队列处理过程中获取我已经计算过的token以及prefill能不能处理这么多new_tokens？这些都需要 <code>kv_cache_manager</code>过程进行管理，比如说下面代码过程：</p><pre><code class="language-python"># running 队列中获取 num_new_tokens 是不是能够被分配到block
new_blocks = self.kv_cache_manager.allocate_slots(
                        request,
                        num_new_tokens,
                        num_lookahead_tokens=self.num_lookahead_tokens,
                    )
# waiting 队列获取已经计算过的token
new_computed_blocks, num_new_local_computed_tokens = (
    self.kv_cache_manager.get_computed_blocks(request)
)
# waiting 中prefill阶段能不能将所有的 num_new_tokens去分配block
new_blocks = self.kv_cache_manager.allocate_slots(
    request,
    num_new_tokens,
    num_new_computed_tokens=num_new_local_computed_tokens,
    new_computed_blocks=new_computed_blocks,
    num_lookahead_tokens=effective_lookahead_tokens,
    num_external_computed_tokens=num_external_computed_tokens,
    delay_cache_blocks=load_kv_async,
    num_encoder_tokens=num_encoder_tokens,
)
</code></pre><p>在代码中 <code>kv_cache_manager</code>初始化过程为：</p><pre><code class="language-python"># /vllm/v1/core/sched/scheduler.py
self.kv_cache_manager = KVCacheManager(
    kv_cache_config=kv_cache_config,
    max_model_len=self.max_model_len,
    enable_caching=self.cache_config.enable_prefix_caching,
    use_eagle=self.use_eagle,
    log_stats=self.log_stats,
    enable_kv_cache_events=self.enable_kv_cache_events,
    dcp_world_size=self.dcp_world_size,
    pcp_world_size=self.pcp_world_size,
    hash_block_size=self.block_size,
    metrics_collector=self.kv_metrics_collector,
)
</code></pre><p>对于初始化过程中几个关键参数解释如下：1、kv_cache_config：主要是管理 num_blocks设置（vllm/v1/kv_cache_interface.py）；2、max_model_len：模型支持的最大序列长度；3、enable_caching：开启前缀缓存。</p><h2 id="block分配整体过程">block分配整体过程</h2><p>在最开始介绍vllm中的整体框架中介绍如下几个参数：<strong>1、block_szie</strong>：这个一般就是默认16块也就是每一块block存储16个tokens；<strong>2、预分配显存大小</strong>：一般就是直接 设备显存x分配比率，比如说24Gx0.9≈21.6G也就是vllm提前占用21.6G显存（但是实际kv cache占用不一定就有21.6G，在初始化过程中模型会进行一次forward去计算出，模型运行期间除了 KV cache 以外所消耗的内存）；<strong>3、block数量</strong>：int(available_memory // page_size // num_layers)，page_szie一个 block在单层上的字节数计算过程为(K+V)$\times$ block_size $\times$ num_kv_heads$\times$ head_size$\times$ dtype_bytes=2 $\times$ 16 $\times$ num_kv_heads $\times$ head_size$\times$ dtype_bytes。num_layers：所有kv_cache_groups中层数的最大值，除此之外vllm为模型的每一层都分配了一个独立的、形状为 [2, num_blocks, block_size, num_kv_heads, head_size] 的KV缓存张量（具体过程后面描述）。那么vllm中整体分配过程如下<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>：<br/><img src="https://files.seeusercontent.com/2026/05/07/Lu9d/image20260322170632097.webp" alt=""/><br/> 对于每一组输入 prompt，在 prefill 阶段会根据 token 数提前分配 block：⌈num_tokens / block_size⌉ 块（例如 block_size=16，5 token 只需 1 块）。 prefill 阶段一次性把 prompt 的所有 token 的 KV 写入这些 block（对于每一个block中几个参数block_id表示当前block的序号，ref_cnt表示当前block被引用次数）。 decode 阶段是增量式的：每次只生成 1 个（或少量）新 token，如果当前最后一个 block 还有空位就继续写入；满了再申请新 block。 当 GPU KV cache 内存不足时，调度器会根据策略（默认 FCFS + 优先 decode）进行 preemption：可能把等待中的请求换出（移动到CPU中）、丢弃重排，或在某些配置下丢弃部分 running 请求。<br/> 虽然输入是多组 prompt，但 vLLM 会把它们拼接成一个“序列”来统一计算，以最大化 GPU 利用率。每个序列的 position ids 从 0 独立开始，通过 attention mask保证每组 token 只能看到自己组的信息，跨组完全隔离。 在 decode 阶段，vLLM 使用 slot_mapping tensor 来记录本次 forward 中每个要生成的新 token 对应到物理 KV cache 的哪个 slot 索引，从而实现非连续 block 的高效寻址和写入。</p><h2 id="kvcachemanager处理过程">KVCacheManager处理过程</h2><blockquote><p>除了KVCache在vllm中还有一个prefix cache其作用表示当多个请求中有相同的前缀时，避免重复计算这部分内容，不过值得注意的是必须是公共前缀，中间相同并不能共享（原因很简单，decode阶段是用n-1去预测n如果两个序列中前k个都相同那么直接复用即可），比如说下面例子中<br/> eg1：你好，帮我介绍武汉？ eg2：你好，帮我介绍北京？==&gt;就可以直接复用 “你好，帮我介绍” 这部分kv cache<br/> eg1：你好，帮我介绍武汉？ eg2：你是一个旅游专家，你好，帮我介绍北京？==&gt;不能实现上面复用</p></blockquote><p>在KVCacheManager（vllm/v1/core/kv_cache_manager.py）中核心逻辑如下几个，1、<strong>get_computed_blocks</strong>：为当前的request找到他的prefix cache；2、<strong>allocate_slots</strong>：为当前的new_token去申请分配block， 3、<strong>free</strong>：释放所有的block当request被处理完之后 ；3、<strong>cache_blocks</strong>：为prefix cache去分配block；4、<strong>reset_prefix_cache</strong>：清空所有的cache block。在了解核心逻辑之前了解block池创建过程。</p><h3 id="blockpool创建">blockpool创建</h3><blockquote><p>测试模型为：Qwen/Qwen2-0.5B-Instruct（后续具体数值和显存大小（32G）以及显存初始化大小（0.9）有关）<br/> 具体代码位置：vllm/v1/core/block_pool.py<br/> kvcache block构建：vllm/v1/core/kv_cache_utils.py</p></blockquote><p>在代码中（vllm/v1/core/block_pool.py）<strong>直接创建所有的blocks</strong>： <code>self.blocks: list[KVCacheBlock] = [KVCacheBlock(idx) for idx in range(num_gpu_blocks)]</code>（其中 num_gpu_block大小为：131928） 而里面的KVCacheBlock创建过程比较简单，为每一块block都去创建如下属性;<br/><img src="https://files.seeusercontent.com/2026/05/07/Pao5/image20260323161425227.webp" alt="20260323161425227"/></p><p>值得注意的是每一个block都是一个双向队列因此prev以及next分别指向上下的block的idx，而其它熟悉含义如下：block_id当前block的序号、ref_cnt当前block被引用次数。值得注意的是上面过程只是创建了一个元数据对象还不知道具体的显存物理地址，还是在代码 <code>vllm/v1/core/kv_cache_utils.py</code> 中的<strong>显存分配过程</strong>：</p><pre><code class="language-python"># vllm/v1/core/kv_cache_utils.py
# 测试模型为 Qwen/Qwen2-0.5B-Instruct
def get_kv_cache_config_from_groups(vllm_config: VllmConfig, kv_cache_groups: list[KVCacheGroupSpec], available_memory: int):
    if len(kv_cache_groups) == 1 and isinstance(kv_cache_groups[0].kv_cache_spec, UniformTypeKVCacheSpecs):...
    else:
        group_size = max(len(group.layer_names) for group in kv_cache_groups) # 24
        page_size = get_uniform_page_size(
            [group.kv_cache_spec for group in kv_cache_groups]
        ) # 8192
        assert group_size &gt; 0, "group_size must be greater than 0"
        num_blocks = get_num_blocks(
            vllm_config, group_size, available_memory, page_size
        ) # 131928
        kv_cache_tensors = []
        for i in range(group_size):
            shared_by = []
            for j in range(len(kv_cache_groups)):
                if i &lt; len(kv_cache_groups[j].layer_names):
                    shared_by.append(kv_cache_groups[j].layer_names[i])
            kv_cache_tensors.append(
                KVCacheTensor(size=page_size * num_blocks, shared_by=shared_by)
            )
    ...
</code></pre><blockquote><p>kv_cache_groups对应的结果为（测试模型为 <code>Qwen/Qwen2-0.5B-Instruct</code> ）： <code>kv_cache_groups=[KVCacheGroupSpec(layer_names=['model.layers.0.self_attn.attn', 'model.layers.1.self_attn.attn', ..., 'model.layers.23.self_attn.attn'], kv_cache_spec=FullAttentionSpec(block_size=16, num_kv_heads=2, head_size=64, dtype=torch.bfloat16, page_size_padded=None, head_size_v=64, sliding_window=None, attention_chunk_size=None))]</code><br/> 最终的返回内容为： <code>kv_cache_config=KVCacheConfig(num_blocks=131928, kv_cache_tensors=[KVCacheTensor(size=1080754176, shared_by=['model.layers.0.self_attn.attn']), ...], kv_cache_groups=[KVCacheGroupSpec(layer_names=['model.layers.0.self_attn.attn', ...], kv_cache_spec=FullAttentionSpec(block_size=16, num_kv_heads=2, head_size=64, dtype=torch.bfloat16, page_size_padded=None, head_size_v=64, sliding_window=None, attention_chunk_size=None))])</code><br/> 1080754176= page_size* num_blocks= 8192x131928</p></blockquote><p>上述代码主要是计算需要分配的显存大小和block数量，除此之外会对模型中每一层都去计算需要分配的kv_cache_tensor大小，比如说上面代码计算得到每一层结果都是：8192* 131928，也就是我的就会对每一层分配一个这么大的内容去显存中占用，直接去看具体显存分配过程，在代码（<code>vllm/v1/worker/gpu_model_runner.py</code>）中的初始化kv_cache_tensor过程如下：</p><pre><code class="language-python"># vllm/v1/worker/gpu_model_runner.py
def initialize_kv_cache_tensors(self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int]):
    ...
    else:
        # 创建 kv_cache_tensors
        kv_cache_raw_tensors = self._allocate_kv_cache_tensors(kv_cache_config)
        # 修改 kv_cache_tensor 形状
        kv_caches = self._reshape_kv_cache_tensors(kv_cache_config, kv_cache_raw_tensors, kernel_block_sizes)
    return kv_caches
</code></pre><p>对于里面的<strong>创建 kv_cache_tensors</strong>过程则是直接通过 <code>tensor = torch.zeros(kv_cache_tensor.size, dtype=torch.int8, device=self.device)</code>去初始化一层的张量其大小为 kv_cache_tensor（page_size* num_blocks=8192x131928），而后将其分配给每一层 <code>for layer_name in kv_cache_tensor.shared_by: kv_cache_raw_tensors[layer_name] = tensor</code>而后将每层中分配得到的tensor大小通过 _reshape_kv_cache_tensors 处理，将最开始的 page_size* num_blocks的修改为 kv_cache_shape，让每个层真正需要的 KV cache 形状和 stride 布局，其中得到的kv_cache_shape为：(2, 131928, 16, 2, 64)对应 <code>(K+V, num_blocks, block_size, num_kv_heads, head_size)</code>。</p><blockquote><p>也就是将上面提到的 KVCacheTensor 中每个size大小等于K+V$\times$ num_blocks$\times$ block_size$\times$ num_kv_heads$\times$ head_size$\times$ dtype_bytes</p></blockquote><p>这样一来，整个 KV Cache 的物理显存占用和元数据管理由两大部分组成：<strong>物理 KV 数据存储部分</strong>（真正占大头显存） 在 kv_cache_tensors 创建和 reshape 阶段，vLLM 为模型的每一层 attention（或每个 KV cache group）提前分配好了对应的 KVCacheTensor。 形状为：(2, num_blocks, block_size, num_kv_heads, head_size)（比如说：(2, 131928, 16, 2, 64)） 这部分是真正存储 K 和 V 数值的显存。 每一层都有自己独立的（或通过 view 共享底层内存的）tensor。 所有层的 tensor 都使用相同的 num_blocks（131928） 作为第一维（block 维度）。 <strong>全局 Block 元数据管理部分</strong>（几乎不占显存） 在 BlockPool 中一次性创建：<code>list[KVCacheBlock] = [KVCacheBlock(idx) for idx in range(num_gpu_blocks)]</code> 这 num_gpu_blocks 个 KVCacheBlock 对象只保存元数据（block_id、ref_cnt、block_hash、last_access_time 等），不存储任何 KV 数据。<br/><strong>总结</strong>：block_pool想象成一个大池塘，当一个请求需要分配新的block时，就会从free_block_queue取出对应数量的block，如果不够，那就分配失败，当请求完成，释放所有block时，就将请求下的所有block释放回free_block_queue。</p><h3 id="block分配逻辑">block分配逻辑</h3><p>上面简单介绍了如何去分配每层中的显存占用以及创建block，下面介绍输入一个新的request如何去分配block进行占用。以上面waiting分配过程为例：KVCacheManager中初始block协调器（<code>self.coordinator = get_kv_cache_coordinator</code>）–&gt; <code>get_kv_cache_coordinator</code>去选择不同的协调器通过 <code>allocate_new_blocks</code>去分配新的block，其内部逻辑是通过 manager进行配分–&gt; manager实现逻辑在 <code>get_manager_for_kv_cache_spec</code>中通过不同的attention计算方式选择不同的分配方式（<code>vllm/v1/core/single_type_kv_cache_manager.py</code>）。</p><pre><code class="language-python"># get_kv_cache_coordinator 选择kvcache分配器 以KVCacheCooridantor为例 kv_cache_coordinator.py
def allocate_new_blocks(
    self,
    request_id: str,
    num_tokens: int,
    num_tokens_main_model: int,
    num_encoder_tokens: int = 0,
) -&gt; tuple[list[KVCacheBlock], ...]:
    return tuple(
        manager.allocate_new_blocks(
            request_id,
            num_encoder_tokens
            if isinstance(manager, CrossAttentionManager)
            else num_tokens,
            num_tokens_main_model,
        )
        for manager in self.single_type_managers
    )
# 里面 self.single_type_managers 主要是通过 get_manager_for_kv_cache_spec获取，其代码定义位置为 single_type_kv_cache_manager.py
# get_manager_for_kv_cache_spec支持的attention分配逻辑
spec_manager_map: dict[type[KVCacheSpec], type[SingleTypeKVCacheManager]] = {
    FullAttentionSpec: FullAttentionManager,
    MLAAttentionSpec: FullAttentionManager,
    SlidingWindowSpec: SlidingWindowManager,
    ChunkedLocalAttentionSpec: ChunkedLocalAttentionManager,
    MambaSpec: MambaManager,
    CrossAttentionSpec: CrossAttentionManager,
    SinkFullAttentionSpec: SinkFullAttentionManager,
}
</code></pre><p>在 <code>get_manager_for_kv_cache_spec</code> 支持多种attention都是继承 <code>SingleTypeKVCacheManger</code>，主要去看其内部的 <code>allocate_new_blocks</code> 和 <code>allocate_new_computed_blocks</code> 这两部分代码逻辑，在 new_blocks中：比较简单直接<strong>计算需要分配的blocks数量</strong>（cdiv=a//b，用新的生成的tokens数量除block_size而后取整即可），而后去我的block pool去拿出对应数量的block即可。对于 new_computed_blocks则是从prefix cache中去拿出已经被处理过程的cache，那么上述整个cache block分配逻辑如下：</p><p>比如说我的模型只有4层，而后我输入token数量是50个，block_size是16这样一来就需要4个block才能存下所有的tokens，首先去检查prefix cache（也就是<code>allocate_new_computed_blocks</code>过程）假设前 2 个逻辑 block 已经缓存在 KV cache 中，那么req_to_blocks值假如就是 <code>block_table = [10, 21, null, null]</code>前两个是真实 hit 的 block_id，后两个先占位，而后通过 <code>allocate_new_blocks</code>计算还需要2个block去存储剩下的tokens假设得到的是45，67那么最后我的tokens存储的block_table就是 <code>[10, 21, 45, 67]</code>那么去计算prefill处理，这个过程就是直接拿着这个block_table去实际物理地址中去找（上面我的每一层提前缓存了 (2, 131928, 16, 2, 64)–(2, num_blocks, block_size, num_kv_heads, head_size) 这么多tensor）那么对于每一层</p><pre><code class="language-python">layer-0: kv_cache_layer0[:, [10,21,45,67], ...]
...
layer-3: kv_cache_layer0[:, [10,21,45,67], ...]
</code></pre><p>此时我的blok没有被填满，而后持续deocde到被填满（分配了4个刚刚好64个被填满）此时需要新的block加入是89那么拿出添加到 block_table中即可，而后依次类推。</p><h3 id="prefix-cache逻辑">Prefix Cache逻辑</h3><p>Prefix Cache指的是缓存已处理请求的 kv-cache 块，并在新请求到达时重用这些块，前提是新请求与先前请求具有相同的前缀<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。在推理框架SGLang中一个比较明显特点就是Prefix Cache不过两者在逻辑上存在差异，vllm是基于block而SGLang则是基于token级别的，也就是说vLLM中只有block被填满了斌且block内都是相同的才能复用prefix cache。在此之前在vllm中的block管理结构如下，block都是一个双链表，彼此之间都知道前后的block，比如说<br/> 第 0 个 block（tokens 0~15）： <code>hash0 = hash( NONE_HASH + tuple(tokens[0:16]) + extra_keys )</code><br/> 第 1 个 block（tokens 16~31）： <code>hash1 = hash( hash0 + tuple(tokens[16:32]) + extra_keys )</code><br/> 以此类推后续都是如此。<strong>不过必须注意的是</strong>：只有在你的block被完全填满（比如说有16个token）之后才会为这个block添加上hash标志！<br/> 在介绍调度器中，<a href="https://www.big-yellow-j.top/posts/2026/03/15/vllm-2.html#:~:text=%E5%88%A4%E6%96%AD%E5%AE%8C%E6%AF%95%E4%B9%8B%E5%90%8E-,2%E3%80%81%E5%BE%97%E5%88%B0%E5%B7%B2%E7%BB%8F%E8%AE%A1%E7%AE%97%E7%9A%84tokens%E6%95%B0%E9%87%8F,-%EF%BC%8C%E9%A6%96%E5%85%88%E5%8E%BB%E5%88%A4%E6%96%AD">对于waiting队列管理中有一个逻辑就是</a><code>new_computed_blocks, num_new_local_computed_tokens = self.kv_cache_manager.get_computed_blocks(request)</code> 通过kv_manager去获取已经被计算过的blocks以及新的本地计算计算的tokens，这部分代码内容逻辑如下：</p><pre><code class="language-python"># kv_cache_manager.py
def get_computed_blocks(self, request: Request) -&gt; tuple[KVCacheBlocks, int]:
    if not self.enable_caching or request.skip_reading_prefix_cache:
        return self.empty_kv_cache_blocks, 0

    computed_blocks, num_new_computed_tokens = (
        self.coordinator.find_longest_cache_hit(
            request.block_hashes,  # 请求预先计算好的 block hash 列表（链式）
            request.num_tokens - 1
        )
    )
    ...
    return self.create_kv_cache_blocks(computed_blocks), num_new_computed_tokens
</code></pre><p>从代码中对于被计算过的blocks获取方法直接通过 <code>find_longest_cache_hit</code> 去获取，这部分代码内部逻辑是我直接去遍历所有的KVCacheBlock中去看哪些 hash 是对应的，如果找到对应的那就可以直接复用即可，<a href="https://docs.vllm.com.cn/en/latest/design/prefix_caching/#eviction-lru:~:text=%E5%9D%97%E5%93%88%E5%B8%8C%E3%80%82-,%E7%A4%BA%E4%BE%8B,-%C2%B6">直接用官方例子</a>来解释：<br/><strong>时间1</strong>：缓存为空，输入request0（prompt为A-O）总共15个token。直接从block pool中分配了 4 个块（假设每个块只能缓存4个token）。其中 3 个已满那么就会被缓存（直接使用SHA256进行编码，编码逻辑是： <code>sha256( parent_hash + 本块tokens + extra )</code> 分别表示上一个列表、本block token、额外信息），第四个块部分填充了 3 个 token，那么此时被填满token的block直接建立hash id比如A-D对应ID=0，如果下次还有A-D的token输入可以直接从cache blocks直接复用（直接拿ID=0）</p><blockquote><p>具体复用可以直接看上面显存分配过程，每层(K+V, num_blocks, block_size, num_kv_heads, head_size) 只需要去num_blocks去拿对应ID即可，除此之外虽然分了block但是计算时候还是复用的，比如说计算E时候会直接那 ABCD的cache内容</p></blockquote><p><img src="https://files.seeusercontent.com/2026/05/28/n6fC/20260528213242518.png" alt=""/><br/><strong>时间2</strong>：开始decode处理，这样就会对 request0 使块3填满，并请求新块以继续解码，这个时候有4块被填满那么就会在cache block中建立4个hash<br/><img src="https://files.seeusercontent.com/2026/05/28/U5ut/20260528213333101.png" alt=""/></p><p><strong>时间 3</strong>：request1 到达，带有 14 个提示 token（A-J，k-n），其中前 10 个 token 与请求 0 相同。我们可以看到只有前 2 个块（8 个 token）<strong>命中缓存</strong>，因为第 3 个块只匹配了 4 个 token 中的 2 个。其具体的<strong>命中缓存内部逻辑</strong>如下，比如开始 request1在预分配的4个block其中前三块都是被填满建立索引假设是 *0 *1 *2 *3 （在我的request0中建立hash是 *0 *1 *4 *5 *6）那么会去调用 <code>find_longest_cache_hit(request.block_hashes)</code> 去cache blocks进行查找发现request2中 *2没有命中，那么直接复用 *0 以及 *1。也就对应下面图像中 request1服用了ID0和ID1，比如说代码：</p><pre><code class="language-python">@classmethod  
def find_longest_cache_hit(cls, block_hashes, max_length, ...):  
    computed_blocks = tuple([] for _ in range(len(kv_cache_group_ids)))  
    max_num_blocks = max_length // block_size  
      
    for block_hash in itertools.islice(block_hashes, max_num_blocks):
        # 直接去 blocks_hashes 中查找
        if cached_block := block_pool.get_cached_block(block_hash, kv_cache_group_ids):
            # 遇到匹配的cahce
            for computed, cached in zip(computed_blocks, cached_block):  
                computed.append(cached)  
        else:  
            break  
      
    if use_eagle and computed_blocks[0]:  
        for computed in computed_blocks:  
            computed.pop()  
      
    return computed_blocks
</code></pre><p><img src="https://files.seeusercontent.com/2026/05/28/o0sD/20260528213355348.png" alt=""/></p><blockquote><p>值得注意的是，只有当前面token都相同才会命中缓存，比如说Reques2输入是：BACDEFGHxxxx就是有EFGH相同也不会命中缓存，以为计算需要复用Request2开始的token式BACD完全和Request0的ABCD不同那么就不要复用EFGH的缓存</p></blockquote><p><strong>时间 4</strong>：请求 0 完成并释放。块 2、3 和 4 按反向顺序添加到空闲队列（但块 2 和 3 仍被缓存）。块 0 和 1 未添加到空闲队列，因为它们正在被请求 1 使用<br/><img src="https://files.seeusercontent.com/2026/05/28/zXr5/20260528213423308.png" alt=""/><br/><strong>时间 5</strong>：请求 1 完成并释放。<br/><img src="https://files.seeusercontent.com/2026/05/28/Z9bo/20260528213443724.png" alt=""/><br/><strong>时间 6</strong>：请求 2 到达，带有 29 个提示 token，其中前 12 个 token 与请求 0 相同。请注意，即使空闲队列中的块顺序是 7 - 8 - 9 - 4 - 3 - 2 - 6 - 5 - 1 - 0，缓存命中的块（即 0、1、2）在分配前会被触碰并从队列中移除，因此空闲队列变为 7 - 8 - 9 - 4 - 3 - 6 - 5。结果，分配的块是 0（已缓存）、1（已缓存）、2（已缓存）、7、8、9、4、3（已驱逐）。<br/><img src="https://files.seeusercontent.com/2026/05/28/Ivz9/20260528213459725.png" alt=""/></p><h2 id="总结">总结</h2><p>KVCache中几个核心的代码文件：1、 <code>kv_cache_manager.py</code>：对block进行分配（不过核心调度代码在 <code>kv_cache_coordinator.py</code>中），包括输入新的token去计算需要多少block、prefix cache使用；2、<code>block_pool.py</code>：创建blook池核心就是根据计算得到的num_blocks去计算分配多少全局管理的block： <code>KVCacheBlock</code>；3、<code>gpu_model_runner.py</code>：直接为模型的每一层去占用物理显存，并且对于占用的物理显存通过全局的 <code>KVCacheBlock</code>进行管理。<br/> 对于block pool想象成一个池子总共有<code>num_blocks</code>个每个池子只能存储 <code>block_size</code>个token，我提前对我模型中每一层都分配了一个 <code>(K+V, num_blocks, block_size, num_kv_heads, head_size)</code>大小的张量进行占用，所有物理块共享这块连续的 GPU 内存，通过 <strong>block_id</strong> 进行分页索引，在新的prompt输入时候计算需要分配池子数量 len(tokens)//block_size，而后会将所有的token都放到池子中，对于<strong>填满了的池子会用一个hash标记</strong>放到block hash中，新的输入时候，进行hash计算如果在block hash中有重复的直接复用（prefix cache过程），而在具体计算中就直接根据block需要去分配的层中进行索引，比如说某个prompt分配了[1,2]那么计算过程就是 <code>(K+V, [2,3], block_size, num_kv_heads, head_size)</code> 去复用kv cache。当prompt被解码之后，他的block会放到block pool中，而且cache还在，新的输入过来就去检查是不是有被计算过程直接复用！</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://vllm.ai/blog/anatomy-of-vllm#run-forward-pass">https://vllm.ai/blog/anatomy-of-vllm#run-forward-pass</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://docs.vllm.com.cn/en/latest/design/prefix_caching/">https://docs.vllm.com.cn/en/latest/design/prefix_caching/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>推理部署框架vllm</category><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/15/vllm-3.html</guid></item><item><title>模型推理框架vllm-2——生成以及调度过程</title><link>https://www.big-yellow-j.top/posts/2026/03/15/vllm-2.html</link><description><![CDATA[基于vllm 0.17.1，介绍其推理中generate过程及调度器运行。generate时，prompt经处理进入_add_request，编码并关联采样参数；后进入_run_engine，涉及模型输出。调度器含waiting与running队列，waiting队列中取出request，判断状态、算已处理tokens、定新生成tokens数与分配block后加入running；running队列则定新生成tokens数、资源抢占后让模型执行，整体围绕请求处理、状态判断、资源分配等关键环节展开，把控生成与调度流程。]]></description><content:encoded><![CDATA[<p>在之前的Blog（<a href="https://www.big-yellow-j.top/posts/2025/12/27/vllm-1.html">https://www.big-yellow-j.top/posts/2025/12/27/vllm-1.html</a>）简单介绍了vllm初始化过程中对于显存的分配过程，本文主要介绍调度器的运行过程（<strong>全部是基于</strong>：<code>Version: 0.17.1</code>）。</p><blockquote><p><code>conda create -n vllm_server python=3.12</code><br/><code>conda activate vllm_server</code><br/><code>pip install vllm==0.17.1</code></p></blockquote><p>首先看vllm中直接使用模型进行推理的过程：</p><pre><code class="language-python">from vllm import LLM, SamplingParams
# model_name 可以直接用本地模型地址
llm = LLM(model='Qwen/Qwen2-0.5B-Instruct', download_dir=cache_dir,...)
sampling_params = SamplingParams(temperature=0.8,...)
prompt_list = ['Please tell me how to acceralate the llm generate!']
outputs = llm.generate(prompt_list, sampling_params)
</code></pre><p>代码中直接通过LLM去初始化一个大模型而后再去使用generate进行生成，核心去看里面生成过程vllm中是如何处理的，整个过程的运行逻辑如下<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">1</a></sup>：<br/><img src="https://files.seeusercontent.com/2026/05/07/t4lT/image20260322162644542.webp" alt=""/></p><p>具体代码中，在generate中（函数涉及到多层的嵌套）模型输入prompt、采样参数等然后会将参数直接输入 <code>_run_completion</code></p><pre><code class="language-python">def _run_completion(self, prompts, params,....):
    self._add_completion_requests(
        prompts=prompts,
        params=params,
        use_tqdm=use_tqdm,
        lora_request=lora_request,
        priority=priority,
        tokenization_kwargs=tokenization_kwargs,
    )
    return self._run_engine(use_tqdm=use_tqdm, output_type=output_type)
</code></pre><p>在上述函数中 <code>_add_completion_requests</code>以及 <code>_run_engine</code> 分别起到的作用是：将输入prompt进行processor进行处理而后向llm_engine添加一个request请求（self.llm_engine.add_request）、而后面以后就好理解就是直接进行模型输出，首先判断是不是有没有没有处理完的请求而后逐一的去对请求进行处理。<br/> 总结一下 generate 过程：prompt–&gt;处理添加请求（_add_request）–&gt;处理请求（_run_engine）。因此主要去看一下这两个过程是如何处理的</p><h2 id="_add_request-过程">_add_request 过程</h2><p>主要是将输入的prompt进行编码，然后对输入的prompt去打上id，告诉模型每一次处理的prompt的序号id，以及prompt所对应的采样参数。在代码中处理十分简单在代码中：</p><pre><code class="language-python">def add_request(self, request_id, prompt, params,...) -&gt; str:
    ...
    if isinstance(prompt, EngineCoreRequest):
        ...
    else:
        request = self.input_processor.process_inputs(
            request_id,
            prompt,
            params,
            supported_tasks=self.get_supported_tasks(),
            arrival_time=arrival_time,
            lora_request=lora_request,
            tokenization_kwargs=tokenization_kwargs,
            trace_headers=trace_headers,
            priority=priority,
        )
        prompt_text, _, _ = extract_prompt_components(self.model_config, prompt)

    self.input_processor.assign_request_id(request)
    req_id = request.request_id
    params = request.params
    n = params.n if isinstance(params, SamplingParams) else 1
    if n == 1:
        self.output_processor.add_request(request, prompt_text, None, 0)
        self.engine_core.add_request(request)
        return req_id

    parent_req = ParentRequest(request)
    for idx in range(n):
        request_id, child_params = parent_req.get_child_info(idx)
        child_request = request if idx == n - 1 else copy(request)
        child_request.request_id = request_id
        child_request.sampling_params = child_params
        self.output_processor.add_request(
            child_request, prompt_text, parent_req, idx
        )
        self.engine_core.add_request(child_request)
    return req_id
</code></pre><p>过程比较简单调用 processor 去对输入进行编码转换为对应的token，对应request的结果为：</p><pre><code class="language-python">EngineCoreRequest(request_id='1', prompt_token_ids=[4340, 525, 575, 11319], mm_features=None, sampling_params=SamplingParams(n=1, presence_penalty=0.0, frequency_penalty=0.0, repetition_penalty=1.05, temperature=0.8, top_p=0.9, top_k=40, min_p=0.0, seed=None, stop=[], stop_token_ids=[151643], bad_words=[], include_stop_str_in_output=False, ignore_eos=False, max_tokens=20, min_tokens=0, logprobs=5, prompt_logprobs=None, skip_special_tokens=True, spaces_between_special_tokens=True, structured_outputs=None, extra_args=None), pooling_params=None, arrival_time=1773562288.0634456, lora_request=None, cache_salt=None, data_parallel_rank=None, prompt_embeds=None, client_index=0, current_wave=0, priority=0, trace_headers=None, resumable=False, external_req_id=None, reasoning_ended=None)
</code></pre><p>通过EngineCoreRequest进行包裹输出里面主要是请求id、采样参数、文本编码后的token等，也就是相当于每次去请求llm都会告诉模型我问了什么、我的输出参数是什么。除此之外在request中还有几个比较重要的参数：</p><blockquote><p>vllm/v1/request.py</p></blockquote><ul><li><strong>1、status</strong></li></ul><p>表示当前request的状态，具体有如下几种<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup>：<br/><strong>WAITING</strong>：请求刚被创建，在等待队列中等待被调度器调度。这是所有请求的初始状态。<br/><strong>WAITING_FOR_FSM</strong>：当请求使用结构化输出功能时，需要等待有限状态机（FSM）编译完成。在此期间，请求处于此状态。<br/><strong>WAITING_FOR_STREAMING_REQ</strong>：对于流式输入（如分块传输的提示词），请求在等待下一个输入块时会进入此状态。<br/><strong>WAITING_FOR_REMOTE_KVS</strong>：在分离式推理（disaggregated prefill/decode）或KV缓存卸载场景中，请求需要等待远端传输KV缓存数据时，会进入此状态。<br/><strong>PREEMPTED</strong>：当KV缓存资源紧张时，调度器可能会抢占一个正在运行的请求，将其放回等待队列以腾出资源给其他请求。被抢占的请求进入此状态。<br/><strong>RUNNING</strong>：请求成功获得资源，正在GPU上执行（进行prefill或decode阶段）。这是请求被服务的核心阶段。<br/><strong>FINISHED_STOPPED</strong>：请求正常结束。可能的原因包括：生成了终止符（EOS token），或者匹配到了用户指定的stop词或stop token。<br/><strong>FINISHED_LENGTH_CAPPED</strong>：请求生成的token数量达到了用户设置的max_tokens限制，因此被停止。<br/><strong>FINISHED_ABORTED</strong>：请求被客户端主动取消或中断。<br/><strong>FINISHED_IGNORED</strong>：请求因内部错误或状态无效而终止。这通常意味着处理过程出现了异常。</p><ul><li><strong>2、数量指标</strong></li></ul><p>1、<strong>num_prompt_tokens</strong>：输入prompt的长度；2、<strong>num_output_placeholders</strong>：表示异步调度中预留但尚未生成的输出令牌占位符数量；3、<strong>num_tokens_with_spec</strong>：表示包含推测解码tokens数量计算过程：len(prompt_token_ids) + len(output_token_ids) + len(spec_token_ids)==<strong>prompt占用token数量+输出tokens数量+推测未来token数量</strong>；</p><h2 id="_run_engine过程">_run_engine过程</h2><p>先去看核心代码：</p><pre><code class="language-python">def _run_engine(...):
    ...
    outputs: list[_O] = []
    while self.llm_engine.has_unfinished_requests():
        step_outputs = self.llm_engine.step()
        for output in step_outputs:
            assert isinstance(output, output_type)
            if output.finished:
                outputs.append(output)
                ...
    return sorted(outputs, key=lambda x: int(x.request_id))
</code></pre><p>对于上面过程直接去使用 <code>step()</code>进行模型输出，在 <code>step()</code>过程中</p><h3 id="llm_enginestep-过程">llm_engine.step() 过程</h3><p>具体代码处理过程，详细解释看代码：</p><pre><code class="language-python"># /vllm/v1/engine/llm_engine.py
def step(self) -&gt; list[RequestOutput | PoolingRequestOutput]:
    if self.should_execute_dummy_batch:
        self.should_execute_dummy_batch = False
        self.engine_core.execute_dummy_batch()
        return []

    # 1、模型输出过程得到结果 从 EngineCore 获取模型输出
    with record_function_or_nullcontext("llm_engine step: get_output"):
        outputs = self.engine_core.get_output()

    # 2、将 EngineCore的原始输出进行process处理 把 EngineCore 的原始输出 → 加工成用户能看懂的输出
    with record_function_or_nullcontext("llm_engine step: process_outputs"):
        iteration_stats = IterationStats() if self.log_stats else None
        processed_outputs = self.output_processor.process_outputs(
            outputs.outputs,
            engine_core_timestamp=outputs.timestamp,
            iteration_stats=iteration_stats,
        )
        self.output_processor.update_scheduler_stats(outputs.scheduler_stats)

    # 处理因 stop strings 命中而需要立即中止的请求
    with record_function_or_nullcontext("llm_engine step: abort_requests"):
        self.engine_core.abort_requests(processed_outputs.reqs_to_abort)

    # 记录统计信息（logging / metrics / tracing）
    with record_function_or_nullcontext("llm_engine step: record_stats"):
        if (
            self.logger_manager is not None
            and outputs.scheduler_stats is not None
            and len(outputs.outputs) &gt; 0
        ):
            self.logger_manager.record(
                scheduler_stats=outputs.scheduler_stats,
                iteration_stats=iteration_stats,
                mm_cache_stats=self.renderer.stat_mm_cache(),
            )
            self.do_log_stats_with_interval()

    return processed_outputs.request_outputs
</code></pre><p>在上面过程中主要有4个过过程中不同过程模型输出内容：</p><pre><code class="language-EngineCoreOutputs">EngineCoreOutputs(engine_index=0, 
outputs=[EngineCoreOutput(request_id='1-aadc7e9e', 
new_token_ids=[99882], 
new_logprobs=LogprobsLists(logprob_token_ids=array([[ 99882,  18493,  99882,  20412, 106789, 105792]], dtype=int32), 
logprobs=array([[-1.2735124, -1.2065492, -1.2735124, -1.9096742, -2.8471742,-2.8471742]], dtype=float32), ...)
</code></pre><p><strong>1、模型输出过程得到结果</strong>：这个过程主要是去输出prompt后的第一个token，对于这部分结果中关注点在于logprob_token_ids、logprobs、new_token_ids分别表示我的模型输出的token以及对应的概率以及我的第一个输出token的id（直接从最大的概率进行解码输出即可）；</p><pre><code class="language-OutputProcessorOutput">OutputProcessorOutput(
request_outputs=[RequestOutput(request_id=1, 
prompt='你是谁？', 
prompt_token_ids=[105043, 100165, 11319], 
encoder_prompt=None, encoder_prompt_token_ids=None, prompt_logprobs=None, 
outputs=[
CompletionOutput(index=0, 
text='我叫“小鱼”，我来自海底，我是海里的小鱼，我的小家叫', 
token_ids=[35946, 99882, 2073, 30709, 100655, 33590, 35946, 101919, 112002, 3837, 104198, 55135, 102073, 30709, 100655, 3837, 97611, 30709, 45629, 99882], 
routed_experts=None, cumulative_logprob=-35.836418479681015, 
logprobs=[
{35946: Logprob(logprob=-1.362297773361206, rank=1, decoded_token='我'),...}
{99882: Logprob(logprob=-0.3063066303730011, rank=1, decoded_token='叫'), ...}]
</code></pre><p><strong>2、将 EngineCore的原始输出进行process处理</strong>：这个过程中主要是去看CompletionOutput部分内容，里面的token_ids表示的是<strong>从 prompt 之后开始的所有已生成 token id</strong>，而在logprobs中则是每一次生成词的概率，而后根据你采样的方式去选择输出。<br/> 上面过程知识模型“单步执行”的外层循环，但是有一个新的问题，比如说我的模型这一次直接输入了1w组prompt对于这些在vllm中是如何去处理呢？如何控制处理顺序呢？用下面<a href="https://audreywongkg.medium.com/understanding-vllm-scheduling-token-budgets-chunked-prefill-and-policies-2c879e3980e3">图片</a>来解释为什么要用调度器（下图过程是基于v0的，注意只是简单介绍一下调度器是工作过程的）：<br/><img src="https://ghfast.top/https://raw.githubusercontent.com/Big-Yellow-J/BlogImage/main/image1_eWGNwmYzO0Nb3q5t_ZtQ_Q.gif" alt=""/></p><h3 id="调度器处理过程">调度器处理过程</h3><p>在调度器中vllm有两块定义：1、vllm/v1/core/sched/scheduler.py；2、vllm/v1/core/sched/async_scheduler.py。这两部分调度器，后者直接继承前者并且作异步处理，具体使用何种调度器在参数定义 SchedulerConfig中可以使用 （vllm/config/scheduler.py）如果要具体使用何种采样器可以直接指定参数： <code>async_scheduling=True/False</code> 或者在启动server中使用参数 <code>--no-async-scheduling</code>。以 cheduler.py 出发分析其主要功能，在最开始的代码 <code>add_request</code> 中我将我的prompt都已经编码完毕，而后就是对编码后的内容进行处理，在代码中主要有两个队列负责request管理：1、waiting；2、running；这两部分都是python的deque()实例（双端队列，允许你从队列两侧添加或删除元素）</p><h4 id="waiting-队列处理过程">waiting 队列处理过程</h4><p>对于编码后的所有的request 会直接都存储到 self.waiting 中而后在 waiting 队列中解析遍历内容写入 running 队列中进行执行，因此会从 waiting 中去拿出存储到request中那么代码过程为（主体框架，具体每一部分都在下面其他小的内容中描述）：</p><pre><code class="language-python">while self.waiting and token_budget &gt; 0:
    if len(self.running) == self.max_num_running_reqs:
        break
    request = self.waiting.peek_request()
    request_id = request.request_id
    
    """"""""""""""""""""""""""""""
    # 第一步：对 request 状态进行检查
    """"""""""""""""""""""""""""""

    num_external_computed_tokens = 0
    load_kv_async = False
    connector_prefix_cache_queries, connector_prefix_cache_hits = 0, 0
    
    """""""""""""""""""""""""""""
    # 第二步：得到已经计算的tokens数量
    """""""""""""""""""""""""""""
    
    """""""""""""""""""""""""""""
    # 第三步：开始计算本次要为这个请求新增计算/生成的 tokens 数量
    """""""""""""""""""""""""""""
    
    """""""""""""""""""""""""""""
    # 第四步：开始计算需要缓存的 new_blocks
    """""""""""""""""""""""""""""  
      
</code></pre><p>对于上述waiting队列处理过程总结如下：首先在while大循环下确定waiting非空以及token_budget还有空余，而后从waiting中取出request，<strong>1、去判断request的状态情况</strong>，主要是判断这三类：</p><pre><code class="language-python">if request.status == RequestStatus.WAITING_FOR_REMOTE_KVS:...
if request.status == RequestStatus.WAITING_FOR_FSM:...
if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ:...
</code></pre><p>在完成对request状态判断完毕之后<strong>2、得到已经计算的tokens数量</strong>，首先去判断request是不是以已经有过计算的tokens，如果没有（request没有被处理）就需要先去计算本地缓存（num_new_local_computed_tokens，先不去管缓存是怎么处理的分配的），以及从connector获取外部缓存（num_external_computed_tokens），最后计算得到 <code>num_computed_tokens = num_new_local_computed_tokens + num_external_computed_tokens</code>。之所以这样是因为：<a href="https://www.big-yellow-j.top/posts/2025/02/27/LLM-OUT.html">大模型生成过程</a>是 prefill（编码prompt）+decode（正式生成），如果最开始我的token在我的block中已经被缓存了那么可以直接拿出进行复用（可以理解为prompt中有被处理过的token）</p><pre><code class="language-python">if request.num_computed_tokens == 0:
  # 本地缓存（本机 KV Cache Manager）
  new_computed_blocks, num_new_local_computed_tokens = self.kv_cache_manager.get_computed_blocks(request)
  # 外部缓存（通过 KVConnector，例如分布式 KV 转移、SharedStorage、LMCache、Offloading 等）
  if self.connector is not None:
    ext_tokens, load_kv_async = self.connector.get_num_new_matched_tokens(request, num_new_local_computed_tokens)
        
    request.num_external_computed_tokens = ext_tokens
    num_external_computed_tokens = ext_tokens
        
  num_computed_tokens = num_new_local_computed_tokens + num_external_computed_tokens
else:
   new_computed_blocks = self.kv_cache_manager.empty_kv_cache_blocks
   num_new_local_computed_tokens = 0
   num_computed_tokens = request.num_computed_tokens
</code></pre><p>在得到需要计算的tokens之后 <strong>3、去计算本地需要生成多少tokens</strong>，在上一步中得到这个request已经被计算的tokens数量，那么就需要计算本次还需要新计算/处理的 token 数量。在request.num_tokens表示的是当前总共拥有的 token 数量（prompt + 已经生成的 output tokens）</p><pre><code class="language-python">if load_kv_async:
  assert num_external_computed_tokens &gt; 0
  num_new_tokens = 0
else:
  num_new_tokens = request.num_tokens - num_computed_tokens
  threshold = self.scheduler_config.long_prefill_token_threshold
  if 0 &lt; threshold &lt; num_new_tokens:
    num_new_tokens = threshold
</code></pre><blockquote><p>第二步中查找前缀缓存命中（Prefix Cache Hit），确定“这个请求已经有哪一部分 KV 可以直接复用，不需要重新计算”，第三步中则是决定本次调度循环实际要处理多少新 tokens（num_new_tokens）</p></blockquote><p>在得到本地需要生成新的token之后就需要 <strong>4、计算这些新的需要计算的token需要计算的block数量</strong></p><pre><code class="language-python">new_blocks = self.kv_cache_manager.allocate_slots(
    request,
    num_new_tokens,
    num_new_computed_tokens=num_new_local_computed_tokens,
    new_computed_blocks=new_computed_blocks,
    num_lookahead_tokens=effective_lookahead_tokens,
    num_external_computed_tokens=num_external_computed_tokens,
    delay_cache_blocks=load_kv_async,
    num_encoder_tokens=num_encoder_tokens,
)
</code></pre><p>在得到需要计算的blocks之后就是将 <strong>5、waiting队列中内容加入到running队列中去进行计算了</strong></p><pre><code class="language-python">request = self.waiting.pop_request()
if load_kv_async:
    # If loading async, allocate memory and put request
    # into the WAITING_FOR_REMOTE_KV state.
    skipped_waiting_requests.prepend_request(request)
    request.status = RequestStatus.WAITING_FOR_REMOTE_KVS
    continue

self.running.append(request)
</code></pre><p><strong>总结一下 waiting 队列处理过程</strong>：1、取出一条request，去判断当前这条request的状态；2、计算当前request有多少token是被计算的（因为llm生成是 prefill+deocde），如果远程缓存中有了直接用远程缓存；3、计算需要新计算的tokens数量；4、对新的计算的tokens去分配block；5、加入到running中。对于waiting中<strong>几个关键参</strong>数设置如下：<br/> 1、token_budget：代表本次Step可处理的“新 Token 总数上限”。该参数在每次调度开始时都会根据 max_num_batched_tokens 进行初始化重置，用于约束本轮迭代的计算规模，防止单次推理耗时过长或显存溢出。</p><blockquote><p>其具体消耗过程在每次step中优先被用于running（decode）使用，当所有的在running中prompt的被decode 处理完毕之后（也就是解码完毕）还有剩余的就回去waiting队列中去取prompt进行prefill。通过这样并行就会让vllm效率高处理快</p></blockquote><p>2、long_prefill_token_threshold：如果一个 waiting 请求的 prompt 超长（&gt; threshold），就强制切成小块，防止它一次性吃掉整个 token_budget，导致其他请求饿死。对应参数 <code>long_prefill_token_threshold=1024</code><br/> 3、enable_prefix_caching：开始prefix缓存复用<br/> 4、max_num_seqs：最大并发数</p><p><strong>简单列子解释调度过程</strong><br/> 假如 request=[R1, R2] 分别都有3000和1000个tokens，我的参数配置是max_num_batched_tokens=2048（对应token_budget）、long_prefill_token_threshold=1024（最长处理1024长的prompt 对应long_prefill_token_threshold）、max_new_tokens=100（最大生成200个新的toknes假设没有eos去终止生成）</p><table><thead><tr><th style="text-align: center">时间</th><th style="text-align: center">waiting 队列</th><th style="text-align: center">running 队列</th><th style="text-align: center">token_budge</th><th style="text-align: center">R1-num_new_tokens</th><th style="text-align: center">R1-num_computed_tokens</th><th style="text-align: center">R2-num_new_tokens</th><th style="text-align: center">R2-num_computed_tokens</th></tr></thead><tbody><tr><td style="text-align: center">t0</td><td style="text-align: center">[R1, R2]</td><td style="text-align: center">[]</td><td style="text-align: center">2048</td><td style="text-align: center">0</td><td style="text-align: center">0</td><td style="text-align: center">0</td><td style="text-align: center">0</td></tr><tr><td style="text-align: center">t1</td><td style="text-align: center">[]</td><td style="text-align: center">[R1,R2]</td><td style="text-align: center">2048-&gt;24</td><td style="text-align: center">0</td><td style="text-align: center">1024</td><td style="text-align: center">0</td><td style="text-align: center">1000</td></tr><tr><td style="text-align: center">t2</td><td style="text-align: center">[]</td><td style="text-align: center">[R1,R2]</td><td style="text-align: center">2048-&gt;1023</td><td style="text-align: center">0</td><td style="text-align: center">2028</td><td style="text-align: center">1</td><td style="text-align: center">1001</td></tr><tr><td style="text-align: center">t3</td><td style="text-align: center">[]</td><td style="text-align: center">[R1,R2]</td><td style="text-align: center">2048-&gt;1095</td><td style="text-align: center">0</td><td style="text-align: center">3000</td><td style="text-align: center">2</td><td style="text-align: center">1002</td></tr><tr><td style="text-align: center">t4</td><td style="text-align: center">[]</td><td style="text-align: center">[R1,R2]</td><td style="text-align: center">2048-&gt;2046</td><td style="text-align: center">1</td><td style="text-align: center">3001</td><td style="text-align: center">3</td><td style="text-align: center">1003</td></tr></tbody></table><p>在最开始中R1是不够的因此需要被切分到最长内容（1024）而后R2去使用剩下的（1024）刚刚好满足1000token就会剩余24，那么下次调度过程中就会生成新的token_budge而后依次类推，只有当prompt的token完全被吃下之后才会去解码生成！一个更加极端情况，如果token_budge设置为1024，那么只有当先进的prompt被吃完才会去处理下一个prompt（此时先进的prompt就会在running队列中）。因此为了加速vllm过程可以适当的加大token_budge参数让其可以吃的更多。</p><h4 id="running-队列处理过程">running 队列处理过程</h4><p>区别waiting需要不断调度请求，running相对比较简单，只需要将request进行block管理即可，整体过程如下</p><pre><code class="language-python">req_index = 0
while req_index &lt; len(self.running) and token_budget &gt; 0:
    request = self.running[req_index]
    """"""""""""""""""""""""""""""
    # 第一步、计算需要生成多少新的tokens
    """"""""""""""""""""""""""""""

    """"""""""""""""""""""""""""""
    # 第二步、进行资源抢占
    """"""""""""""""""""""""""""""

    """"""""""""""""""""""""""""""
    # 第三步、“执行”模型进行处理
    """"""""""""""""""""""""""""""
</code></pre><p>在读取得到新的请求之后，<strong>1、先去计算需要生成多少新的tokens</strong>（对应计算num_new_tokens）这个过程主要是如下（具体解释看代码）：</p><pre><code class="language-python">if (request.num_output_placeholders &gt; 0 and request.num_computed_tokens + 2 - request.num_output_placeholders&gt;= request.num_prompt_tokens + request.max_tokens
):
    req_index += 1
    continue
#  总tokens+ 异步预留tokens- 已经计算的tokens 就得到需要新生成tokens
num_new_tokens = (request.num_tokens_with_spec # 对于这个值一般就是对于 prompt tokens数量
                  + request.num_output_placeholders # 一般在 prefill 阶段是0 进入decoding阶段之后就是 1
                  - request.num_computed_tokens # 被计算的tokens
                  ) 
# 避免 prompt 过长直接全部占满
if 0 &lt; self.scheduler_config.long_prefill_token_threshold &lt; num_new_tokens:
    num_new_tokens = self.scheduler_config.long_prefill_token_threshold
# 依次调度中 只能生成 token_budge 
num_new_tokens = min(num_new_tokens, token_budget)
num_new_tokens = min(num_new_tokens, self.max_model_len - 1 - request.num_computed_tokens)

# 这块主要是处理多模态模型中token它在计算这一步要处理多少文本 Token 的同时，检查是否有“多模态数据（图像/视频/音频）”挡在路中间。如果有，就先分配计算资源给 Encoder（视觉/音频编码器）去处理这些输入。
if request.has_encoder_inputs:
    (encoder_inputs_to_schedule,num_new_tokens,new_encoder_compute_budget,external_load_encoder_input,) = self._try_schedule_encoder_inputs(request,request.num_computed_tokens,num_new_tokens,encoder_compute_budget,shift_computed_tokens=1 if self.use_eagle else 0,
    )
</code></pre><p>估算需要新生成多tokens，除此之外还需要报错新生成tokens数量不能超过token_budge。在得到需要新生成tokens之后，<strong>2、进行资源的抢占</strong>，知道需要新生成的tokens数量之后，显卡不一定能够吃下这么多的tokens，就需要进行资源抢占</p><pre><code class="language-python">with record_function_or_nullcontext("schedule: allocate_slots"):
    while True:
        new_blocks = self.kv_cache_manager.allocate_slots(request,num_new_tokens,num_lookahead_tokens=self.num_lookahead_tokens,)

        if new_blocks is not None:
            break
        # 优先级策略
        if self.policy == SchedulingPolicy.PRIORITY:
            preempted_req = max(self.running, key=lambda r: (r.priority, r.arrival_time),)
            self.running.remove(preempted_req)
            if preempted_req in scheduled_running_reqs:
                preempted_req_id = preempted_req.request_id
                scheduled_running_reqs.remove(preempted_req)
                token_budget += num_scheduled_tokens.pop(preempted_req_id)
                req_to_new_blocks.pop(preempted_req_id)
                scheduled_spec_decode_tokens.pop(preempted_req_id, None)
                preempted_encoder_inputs = scheduled_encoder_inputs.pop(
                    preempted_req_id, None
                )
                if preempted_encoder_inputs:
                    num_embeds_to_restore = sum(
                        preempted_req.get_num_encoder_embeds(i)
                        for i in preempted_encoder_inputs
                    )
                    encoder_compute_budget += num_embeds_to_restore
                req_index -= 1
        else:
            # 直接末尾踢掉
            preempted_req = self.running.pop()

        self._preempt_request(preempted_req, scheduled_timestamp)
        preempted_reqs.append(preempted_req)
        if preempted_req == request:
            break
</code></pre><p>对于资源抢占过程中，首先通过kv_cache_manager去计算对于request以及nun_new_tokens需要多少block，如果显卡还有显存，那么就直接给他分配，如果显存不过就需要进行资源抢占，1、<strong>PRIORITY（优先级策略）</strong>：直接找到running中队列中 <strong>优先级最低</strong> 且 <strong>到达时间最晚</strong> 的请求。 <code>preempted_req = max(self.running, key=lambda r: (r.priority, r.arrival_time),)</code>直接将他踢掉，而后释放这个request占用的 token_budget；2、<strong>直接踢掉末尾</strong>。在获取需要踢掉的request之后<code>self._preempt_request</code>去释放kv_cache、编码显存占用、重置状态为<code>PREEMPTED</code>将其放到waiting中。通过第一、二步计算了模型的新生成的tokens数量，并且根据这些数量对资源进行抢占，那么 <strong>3、调用模型“执行”</strong>，并非真的执行，前面两步拿到了模型整个过程中所有的需要的资源，就带着这些资源给模型进行执行。</p><pre><code class="language-python"># 首先将 request 加到 需要running “名单” 并且扣除 token预算
scheduled_running_reqs.append(request)
request_id = request.request_id
req_to_new_blocks[request_id] = new_blocks
num_scheduled_tokens[request_id] = num_new_tokens
token_budget -= num_new_tokens
req_index += 1
# 计算 在这一步分配的 Token 中，有多少是真正用于“推测”
if request.spec_token_ids:
    num_scheduled_spec_tokens = (
        num_new_tokens
        + request.num_computed_tokens
        - request.num_tokens
        - request.num_output_placeholders
    )
</code></pre><p><strong>简单总结一下running过程</strong>：1、确定需要处理多少新的tokens，一般计算过程：总tokens数量（prompt的token数量+生成token数量，在prefill阶段没有生成那么就等于prompt长度，后续decoding阶段有生成就会增加这个数值）+预占用token数量（prefill阶段==0，decoding阶段==1）-已经处理的tokens数量，然后还需要将其控制在 token_budge下不能超过他；2、而后进行资源抢占，如果计算的 新生成的token可以满足显存分配，那么就为其分配，如果不满足就直接：1、踢出running队列中末尾的请求；2、或者提出优先级最低、时间最晚的请求。在确定好之后就需要将 剔除的资源进行释放；3、拿着前面两步内容去下一步让模型进行输出。</p><h2 id="总结">总结</h2><p>llm生成过程中主要是prefill（编码prompt）+decode（解码输出），在调度过程中总结就是：<strong>先进先处理</strong>（对于先进入的request会一直占用直到被prefill完毕后才会给下一个prompt进行处理），<strong>run优先并行处理</strong>（进入running优先使用token预算，剩下预算的给下一个prompt进行prefill或者decode）。里面每次处理都会使用token预算（对应参数 <code>max_num_batched_tokens</code>），本次调度token预算用完进行下一次，并且调度值有限被running处理。<br/><strong>几个核心参数</strong>：<br/> 1、<strong>token_budget</strong>：代表本次Step可处理的“新 Token 总数上限”。该参数在每次调度开始时都会根据 max_num_batched_tokens 进行初始化重置，用于约束本轮迭代的计算规模，防止单次推理耗时过长或显存溢出。<br/> 2、<strong>long_prefill_token_threshold</strong>：如果一个 waiting 请求的 prompt 超长（&gt; threshold），就强制切成小块，防止它一次性吃掉整个 token_budget，导致其他请求饿死。对应参数 <code>long_prefill_token_threshold=1024</code><br/> 3、<strong>enable_prefix_caching</strong>：开始prefix缓存复用<br/> 4、<strong>max_num_seqs</strong>：最大并发数<br/> 5、<strong>num_prompt_tokens</strong>：输入prompt的长度；<br/> 6、<strong>num_output_placeholders</strong>：表示异步调度中预留但尚未生成的输出令牌占位符数量；<br/> 7、<strong>num_tokens_with_spec</strong>：表示包含推测解码tokens数量计算过程：len(prompt_token_ids) + len(output_token_ids) + len(spec_token_ids)==<strong>prompt占用token数量+输出tokens数量+推测未来token数量</strong>；</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:4" role="doc-endnote"><p><a href="https://vllm.ai/blog/anatomy-of-vllm#llm-engine--engine-core">https://vllm.ai/blog/anatomy-of-vllm#llm-engine–engine-core</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://deepwiki.com/vllm-project/vllm/3.2-request-lifecycle-and-state-management">https://deepwiki.com/vllm-project/vllm/3.2-request-lifecycle-and-state-management</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>推理部署框架vllm</category><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/15/vllm-2.html</guid></item><item><title>开源模型技术总结-3————FireRed（小红书）开源模型</title><link>https://www.big-yellow-j.top/posts/2026/03/09/FireRedModel.html</link><description><![CDATA[FireRED OCR基于Qwen3-vl微调，数据构建环节采用聚类去重保留长尾数据、多维度分类保证分布均衡、多工具联动清洗修复三类处理，训练分三阶段推进：先预训练强化目标检测、特定区域识别、页面转Markdown三类文档识别能力，再用高质量数据做监督微调，最后通过GRPO强化学习优化输出格式，奖励函数覆盖数学公式、表格完整性、文本准确性、标签闭合四类维度。FireRED Edit采用分桶采样策略提升显存利用率，优化DPO策略区分择优避坏参数，搭配DiffusionNFT自生成训练数据，双维度奖励函数结合一致性损失规避生成图像细节崩溃。]]></description><content:encoded><![CDATA[<p>首先对于FireRED OCR模型以及FireRED Edit虽然都是模型微调，但是对于其训练过程还是很有参考意义，比如Edit模型中通过训练强化模型对于细节的感知能力（这里可以对一些对细节要求很高的生成模型训练很有启发意义，而不是直接拿着数据直接SFT看效果）</p><h2 id="firered-ocr">FireRED OCR</h2><p>OCR<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>模型主要是对Qwen3-vl进行微调的一个模型，因此主要了解一下其数据构建过程以及其训练思路即可，因此对于其模型 <strong>数据构建</strong>过程中：<br/><img src="https://files.seeusercontent.com/2026/05/07/9jzI/image20260309220245004.webp" alt=""/><br/><strong>对数据进行聚类处理</strong>（筛选去重），直接通过轻量的ResNet/Vit对图像进行编码而后通过聚类算法（KNN等）去筛选出相似度高的、保留长尾数据（如表格等）；<strong>数据分类</strong>、这部分主要是对数据的语言类型、布局、数据来源（PDF、扫描件等）进行分类保证数据类别的分布；<strong>数据清洗</strong>：这部分主要是对数据进行识别（比如说有些数据可能只有图像）就需要使用PaddleOCR-VL进行识别、合成数据（通过提前设计好html/css模板去合成表格等数据）、还有一些内容就直接通过大模型去判断最后标签是不是有效的，比如说对于识别失败的，直接用llm判断markdown中格式不是都正确是不是有缺失，并且进行修复，判断图像是不是“好的”没有质量太低的（直接丢弃）</p><p><strong>模型训练</strong>过程不是直接SFT+RL而是在此之前先去给模型赋予“文档”识别能力（可能基于Qwen3-VL非专门的OCR模型因此<strong>先去强化OCR中主要的3类任务能力，减轻后续SFT、RL难度</strong>）：<strong>第一阶段</strong>赋予模型文档识别能力：主要是对文档中进行多任务的微调包含如下3组任务：目标检测以及OCR、特定区域OCR（主要是通过提供prompt去识别对应区域内容）、页面转换（将布局转换为Markdown内容）；<strong>第二阶段</strong>监督微调：这部分数据相对去第一部分中的数学质量更加高，比如说转化公式严格满足和markdown保持一致，文档的种类更加丰富。<strong>第三阶段</strong>GRPO强化学习（主要是争对模型最后输出<strong>内容的格式</strong>），对于GRPO中奖励函数主要对4类内容进行打分：数学公式评分（直接通过Latex解析器处理数学公式如果失败处理记分-1，成功则是按照公式复杂度进行评分）、表格完整性评分（检查行/列是否一致及1或0）、文本准确性评分（计算文本之间Levenshtein distance，这个距离主要是计算从A–&gt;B字符串需要进行多少次计算）、层级闭合（主要是记录标签是否闭合，比如html中 tag 之间）。最后对上面4类进行加权即为最后奖励值。<br/><img src="https://files.seeusercontent.com/2026/05/07/G5gv/image20260309220052574.webp" alt=""/></p><blockquote><p>对于训练过程可以总结为：粗–&gt;细–&gt;强化</p></blockquote><h2 id="firered-edit">FireRED Edit</h2><p>模型<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>结构如下（<a href="https://github.com/FireRedTeam/FireRed-Image-Edit/issues/10#issuecomment-3959708960">还是对qwen-image-edit做的微调</a>，那么qwen-image-edit的lora也可以直接拿来用）<br/><img src="https://files.seeusercontent.com/2026/05/07/Ftt9/image20260309221805928.webp" alt=""/><br/> 对于训练过程中，<strong>输入数据处理过程</strong>主要是对数据进行分桶保证显卡最大利用：计算batch的总视觉token数量尽量接近某个固定值C，而后在一个桶里面找一个最合适的分辨率（$\text{argmin}\sum \vert (H_iW_i-hw \vert)$ 小的hw为桶的尺寸）保证尺寸一致。image-text打乱处理，比如说交换Fig位置对应文本中位置也发生改变、随机丢弃图像文本也对应丢弃描述。</p><blockquote><p>分桶后，训练过程一般优先从相同bucket中挑选样本组成batch；当bucket内样本不足batch_size时，会通过设置repeat参数（重复次数）来虚拟扩充样本量，从而保证训练能稳定、连续地进行，而不会频繁丢弃不完整batch。</p></blockquote><p><strong>模型训练策略</strong>：将VLM部分视觉提取特征提前处理好存储在本地避免训练过程中再计算、FSDP（梯度优化器状态切分放到节约显存）<br/><img src="https://files.seeusercontent.com/2026/05/07/c4mO/image20260310213427159.webp" alt=""/><br/> 模型训练分为5个阶段，每个阶段数据质量是越来越高的，主要看里面的DPO以及NFT处理过程。<strong>首先DPO过程</strong>，作者训练过程中发现对于Win以及Lose都会发生上升（也就是选择“好”以及选择“坏”的概率都在上升）：<br/> \(\mathcal{L}_{\text {Ours }}=-\mathbb{E}_{\left(c, x_{w}, x_{l}\right) \sim \mathcal{D}}[\log \sigma(\beta[\underbrace{\left(\mathcal{L}_{l}^{\theta}-\mathcal{L}_{l}^{\text {ref }}\right)}_{\text {Lose Diff }}-\omega \cdot \underbrace{\left(\mathcal{L}_{w}^{\theta}-\mathcal{L}_{w}^{\text {ref }}\right)}_{\text {Win Diff }}])-\lambda \mathcal{L}_{w}^{\theta}]\)<br/> 对比普通的DPO就是将择优以及避坏都用不同参数而非相同的参数进行控制（$\beta$ 以及 $w$）。使用 <em>DiffusionNFT</em>优化策略，在DPO中需要成对的数据进行训练而DiffusionNFT<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>模型自己生成图片，然后通过一个奖励模型给这张图打分，这个分数被转化为最优概率 $r \in [0, 1]$<br/> \(\mathcal{L}_{\mathrm{NFT}}=\mathbb{E}_{t, x_{0} \sim \pi^{\mathrm{old}}}[r \underbrace{\left\|v_{\theta}^{+}\left(x_{t}, t\right)-v\right\|^{2}}_{\text {Positive Match }}+(1-r) \underbrace{\left\|v_{\theta}^{-}\left(x_{t}, t\right)-v\right\|^{2}}_{\text {Negative Match }}]\)<br/> 对于里面奖励函数评分主要是两个：1、Fine-grained Logit-Weighted Ensembling Reward：一般RL过程中奖励值一般是离散的也就是1-5，论文里面做法让 reward model 先写 CoT 推理，然后只看它对[1,2,3,4,5]这几个数字 token 的 logits（<strong>具体过程</strong>：输入一张图像，先通过VLM输出COT回答最后给出评分，因为LLM生成token会在1-5每个数字都生成一个概率然后选择最大概率最为模型输出，因此我在llm输出数字时候变相的得到了1-5这几个数字的概率），用 softmax 做软概率加权平均，得到一个 [1.0～5.0] 区间内的连续软分数 → 再 ensemble 多条推理路径取平均 → 得到最终奖励 R；2、Layout-Aware OCR-based Reward：这个奖励函数主要是针对文字生成进行优化处理同时考虑文字的生成以及文字的布局是否准确；<br/><strong>一致性损失</strong>主要是为了保证训练过程中图像不“崩溃”（解噪过程中前期生成“轮廓”，后期生成“细节”，这个过程主要是保证细节不崩溃，如人脸改变等）<br/> \(\mathcal{L}_{id} = \frac{1}{N} \sum_{i=1}^{N} \left( 1 - \frac{\phi(\mathcal{T}_i(x_0)) \cdot \phi(\mathcal{T}_i(x_{gt}))}{\|\phi(\mathcal{T}_i(x_0))\|_2 \cdot \|\phi(\mathcal{T}_i(x_{gt}))\|_2} \right)\)<br/> 里面的 $x_0$表示的是直接一步生成结果（之所以不用最后结果是因为你最后生成图像整个图像信息已经被确定了，如果对中间过程进行改进最后图像也会发生改进）</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2603.01840">FireRed-OCR Technical Report</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2602.13344">FireRed-Image-Edit-1.0 Technical Report</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2509.16117">https://arxiv.org/pdf/2509.16117</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>多模态</category><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/09/FireRedModel.html</guid></item><item><title>强化学习算法-2：熵坍缩以及奖励坍缩问题机制分析及解决措施</title><link>https://www.big-yellow-j.top/posts/2026/03/01/RLHF-2.html</link><description><![CDATA[梳理强化学习DPO、PPO、GRPO三类优化算法常见的两类训练崩溃问题。熵坍缩源于动作概率与优势值协方差结构性恒正，训练初期策略熵快速趋近于0，模型失去探索性，效果陷入瓶颈，可通过DAPO调整GRPO的epsilon高低边界、动态温度调度仅对top20%高熵token计算策略梯度两种方案缓解，对应GRPO可配置epsilon_low、epsilon_high、top_entropy_quantile参数实现。奖励坍缩由GRPO组内归一化抹平多奖励信号相对差异导致，训练信号分辨率大幅下降，可采用GDPO对各奖励分量独立标准化后加权求和的方案解决。]]></description><content:encoded><![CDATA[<p>系列文章：<br/> 1、<a href="https://www.big-yellow-j.top/posts/2026/02/26/RLHF-1.html">强化学习算法-1：GRPO、DPO与PPO解析</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2026/03/01/RLHF-2.html">强化学习算法-2：熵坍缩以及奖励坍缩问题机制分析及解决措施</a><br/> 3、<a href="https://www.big-yellow-j.top/posts/2026/03/26/RLHF-3.html">强化学习算法-3：GSPO、SAPO及KL散度改进</a></p><h2 id="熵坍缩">熵坍缩</h2><p>按照论文<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>里面对于熵的定义<br/><img src="https://files.seeusercontent.com/2026/05/07/8Xpg/image20260302001310510.webp" alt=""/><br/> 上面公式中 $\pi_\theta$ 以及 $\mathbf{D}$ 分别表示policy model（一般可以直接简单理解为我们需要优化的模型）以及数据集，对于熵值高低往往也就意味着模型输出的“不确定性”，高熵意味着模型可以尝试多种可能性，熵则意味着模型倾向于选择那些确定性的动作。在论文中<sup id="fnref:1:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>在实验中经常会发现，在训练的初期，Policy Entropy会迅速下降到接近0（如下图左侧图深蓝色线所示），模型变得过于确定，不再尝试新路径，这会导致模型的效果达到瓶颈，上升停滞。毕竟，强化学习之所以能够生效，全都依赖于模型的输出具有多样性，每次的输出和reward都是不一样的；假如模型的输出每次都是一样的，那么强化学习就失去了意义，因此就会出现后期的准确率基本保持不变的情况也就是所谓的熵坍缩（entropy collapse）<br/><img src="https://files.seeusercontent.com/2026/05/07/3bEh/image20260302000155767.webp" alt=""/></p><h3 id="机制分析">机制分析</h3><p>对于之所以会发生熵坍缩问题<strong>先说结论</strong>：单步熵变化 $ΔH ≈ −η ⋅ Cov[log π(a|s), A(s,a)]$，而该协方差在实践中结构性恒正。因为SFT之后模型默认输出高概率token，reward model 倾向给当前高概率的输出打高分最后导致恒正。<br/> 对于上述问题之所以发生在论文里面对于连续两步中熵的变化为：<br/><img src="https://files.seeusercontent.com/2026/05/07/Bzr5/image20260302103944023.webp" alt=""/><br/> 上面公式中 $z_{s,a}^{k+1}- z_{s,a}^{k}$ 表示的是两步之间的输出差异，在梯度优化过程中 $z_{s,a}^{k+1}- z_{s,a}^{k}=-\eta \nabla_z J(\theta)$ 其中 $J(\theta)$ 表示优化目标（对应损失函数） $\eta$ 表示学习率，对于该等式可以证明得到 $z_{s,a}^{k+1}- z_{s,a}^{k}=-\eta \pi_{\theta}(a\vert s)A(s,a)$</p><blockquote><p><img src="https://files.seeusercontent.com/2026/05/07/6qFv/image20260302110318425.webp" alt=""/></p></blockquote><p>那么将该式代入最上面等式中就可以得到：<br/> \(\mathcal{H}\left(\pi_{\theta}^{k+1} \mid s\right)-\mathcal{H}\left(\pi_{\theta}^{k} \mid s\right) ≈-\eta \cdot \operatorname{Cov}_{a \sim \pi_{\theta}^{k}(\cdot \mid s)}(\log \pi_{\theta}^k(a|s),A(s,a))\)<br/> 对于上述公式中也就是证明了动作概率与对应的优势值之间相关性，如果正相关那么就会导致最后熵值的不断下降。</p><ul><li>当 Cov &gt; 0 时 → ΔH &lt; 0 → 熵下降（policy 变得更确定）</li><li>当 Cov &lt; 0 时 → ΔH &gt; 0 → 熵上升（增加探索性）</li><li>Cov = 0 → 熵基本不变（理想但极少出现的中性状态）</li></ul><p><strong>Cov[log π(a|s), A(s,a)] &gt; 0 的含义</strong>：当前 policy 认为高概率概率的动作（log π 较大），在当前训练 batch 中也<strong>倾向于</strong>获得更高的优势估计 A（正相关）。反之，低概率动作即使偶尔拿到高 A，其对协方差的贡献也很微弱（因为采样权重 π(a) 本身极小）。而在训练过程中协方差几乎恒正，<strong>因为</strong>SFT 后模型“默认”倾向输出高概率 token → 这些 token 在 reward 眼中也“默认”较优 → 正相关 → 每步更新都系统性地压缩分布 → 熵单向坍缩。<br/><img src="https://files.seeusercontent.com/2026/05/07/5gNl/image20260302114540358.webp" alt=""/><br/> 左图：熵变化和协方差之间变化，也就证明了协方差增加导致熵下降；右图：按 prompt 难度分组的协方差分布Easy prompt（准确率高）：协方差大；Hard prompt（准确率低）：协方差小甚至为负；这说明：对容易任务，策略强化了已有倾向（熵下降）；对困难任务，模型更不确定（熵保持或上升）</p><h3 id="解决措施">解决措施</h3><h4 id="dapo">DAPO</h4><p>按照论文<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>中介绍方法<br/><img src="https://files.seeusercontent.com/2026/05/07/v4Nv/image20260302120102013.webp" alt=""/><br/> 在GRPO中对于 $\epsilon$ 一般默认就是0.2，而DAPO中直接设置两个超参数 $\epsilon_{low}$ 以及 $\epsilon_{high}$ 论文里面选择是0.2以及0.28，之所以使用该方法可以一定程度缓解问题是因为：<strong>增大上边界让低概率的tokens能够不被“压制”鼓励其输出，但是增大下边界可能导致采样空间的崩溃</strong>。之所以增大上边界影响分析如下：在最上面分析熵坍缩原因中因为模型更加“偏爱”输出高概率token导致坍缩问题，那么在DAPO中直接鼓励模型去输出低概率token去弱化相关性，增加上边界可以鼓励模型去输出低概率值。直观解释，比如说在模型输出过程中 $\pi_{\theta}(o_i\vert q)$ 在输出若干个token之后，比如说输出两个词：好（对应模型高概率值假设0.9）、呀（对应低概率值假设0.1），在裁剪过程中低概率的token上限就是1.2那么对应就是：1.2x0.1=0.012那么在后续不断优化过程中（比如说10步）得到结果就是 $0.1\times 1.2^5=0.249$ 如果增加上边界那么对应 $0.1\times 1.8^5=1.889$之间差距还是明显随着后续步数迭代这些低概率token就会有“表现”机会。<br/> 实际<strong>代码操作</strong>中以GRPO训练为例只需要在参数中指定<code>GRPOConfig(epsilon_low=0.1, epsilon_high=0.3)</code></p><h4 id="动态温度调度">动态温度调度</h4><p>按照论文<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>里描述在输出token中平均熵最高的词元通常起到“分叉”的作用，从而决定推理方向；而平均熵最低的词元则倾向于沿着既定路径执行推理步骤。<br/><img src="https://files.seeusercontent.com/2026/05/07/Moh1/image20260302152317680.webp" alt=""/><br/><strong>高熵Token</strong>（分岔点，”forking tokens” ）主要是逻辑连接词（however, thus, because）、假设词（suppose, assume, given）、修正词（wait, unless）。它们在推理路径上充当决策点。比如，模型在说”however”时，它可能在犹豫是继续当前思路还是转向反方观点。在说这些词的时候，推理就可以有多个不同的发展方向，所以模型在生成它们时很不确定，因此这些token是High Entropy的。<strong>低熵Token</strong>主要是词缀（如ing, ed）、代码片段、数学表达式的固定部分（如括号、等号）。这些表述是机械性的、高度可预测的。<br/> 因此解决熵坍缩问题里面就可以直接对于高熵token用较高的temperature去进行采样鼓励模型去输出这些高熵token，除此之外在论文里面还做了一个额外实验：只在高熵（<strong>选择20%的高熵token</strong>）token上计算policy gradient对于低熵的token梯度直接丢弃，最后得到优化效果发现丢弃token效果比使用全部token的效果要好。<br/><img src="https://files.seeusercontent.com/2026/05/07/3Ohc/image20260302153327085.webp" alt=""/><br/> 实际<strong>代码操作</strong>中以GRPO训练为例只需要在参数中指定<code>GRPOConfig(top_entropy_quantile=0.2)</code>即可（2/8法则）实现丢弃部分token。参数具体解释：<code>top_entropy_quantile</code>（浮点数，可选，默认为 1.0）来自超越 80/20 规则的 ρ 参数。在策略损失项中只保留每个序列位置上的代币概率分布熵的 top-ρ 量化值，从而改善结果。范围： [0.0-1.0]。<strong>0.0的值会屏蔽除最高熵标记外的所有标记；1.0 会保留所有标记</strong>。如果与 mask_truncated_completions=True 一起使用，则只考虑来自非截断序列的标记。</p><h2 id="奖励坍缩">奖励坍缩</h2><p>奖励坍缩（reward collapse）：比如说在GRPO优化过程中在多奖励设置下，GRPO的group-wise normalization（组内归一化）会把不同奖励组合压缩成几乎相同的advantage（优势值），导致训练信号分辨率大幅下降。简单来说，<strong>当多个奖励信号被组合后，它们的相对差异被”抹平”了，模型无法有效区分哪些行为是真正更好的</strong>。这就像把不同颜色的颜料混合在一起，最终只剩下一种模糊的灰色，导致训练信号丢失。<br/> 比如说对于prompt产生的4组回答，我需要分别取评估回答格式是否正确、回答内容是否正确得到：A（1，1）；B（1，0）；C（0，1）；D（0，0）按照GRPO中计算有优势值方法：$A_i=\frac{r_i- \text{mean}(r_1,…,r_G)}{\text{std}(r_1,…,r_G)}$ 在对BC计算优势值发现两部分优势值是相等的，但是实际上B回答正确格式错误而C格式正确回答错误，最后优势值只会告诉模型两部分相同（在奖励曲线上一般是先去上升后续出现断崖下降）。</p><h3 id="解决措施-1">解决措施</h3><h4 id="gdpo">GDPO</h4><p>在论文<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>里面先对每个奖励分量独立组内标准化（得到每个分量的 normalized scalar），然后加权求和这些 normalized 值 → 最终 $ \hat{A}_i^{\text{GDPO}}$<br/><img src="https://files.seeusercontent.com/2026/05/07/2Ttg/image20260302170633732.webp" alt=""/><br/> 比如说在下面例子中：<br/><img src="https://files.seeusercontent.com/2026/05/07/qQm2/image20260302170103647.webp" alt=""/><br/> 比如说在上面计算中在GRPO例子AD优势幅度完全对称（只是正负相反），模型很难学到“正确性比格式更重要”的信号。在GRPO（权重2：1）中A 和 D 的差距被极大放大：+2.683 vs -2.683（幅度翻倍）。模型能更强烈地学习到“优先追求高正确性，即使牺牲一些格式”，而 GRPO 中这个倾向非常弱。</p><pre><code class="language-python"># line 1254 in NVlabs/GDPO/trl-GDPO/trl-0.18.0-gdpo/trl/trainer/grpo_trainer.py
# Gather the reward per function: this part is crucial, because the rewards are normalized per group and the
# completions may be distributed across processes
rewards_per_func = gather(rewards_per_func)
rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1)

# Compute grouped-wise rewards
mean_grouped_rewards = rewards.view(-1, self.num_generations).mean(dim=1)
std_grouped_rewards = rewards.view(-1, self.num_generations).std(dim=1)
is_std_zero = torch.isclose(std_grouped_rewards, torch.zeros_like(std_grouped_rewards))

# Normalize the rewards to compute the advantages
mean_grouped_rewards = mean_grouped_rewards.repeat_interleave(self.num_generations, dim=0)
std_grouped_rewards = std_grouped_rewards.repeat_interleave(self.num_generations, dim=0)
advantages = rewards - mean_grouped_rewards
if self.scale_rewards:
    advantages = advantages / (std_grouped_rewards + 1e-4)
</code></pre><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2505.22617v1">https://arxiv.org/pdf/2505.22617v1</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:1:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2503.14476">https://arxiv.org/pdf/2503.14476</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2506.01939">https://arxiv.org/pdf/2506.01939</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://nvlabs.github.io/GDPO/">GDPO: Group reward-Decoupled Normalization Policy Optimization for Multi-reward RL Optimization</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>强化学习</category><pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/03/01/RLHF-2.html</guid></item><item><title>强化学习算法-1：GRPO、DPO与PPO解析</title><link>https://www.big-yellow-j.top/posts/2026/02/26/RLHF-1.html</link><description><![CDATA[大语言模型训练分为预训练、监督微调（SFT）、人类反馈强化学习（RLHF）三个阶段，RLHF用于对齐人类偏好，核心优化算法包含DPO、PPO、GRPO三类。DPO直接基于人类偏好数据微调，通过对比优选、劣选回答的生成概率优化，无需独立奖励模型。PPO为策略梯度类算法，通过截断更新幅度、引入GAE广义优势估计保证训练稳定性，需搭配奖励模型、价值模型计算损失。GRPO为DPO的组级扩展，通过同prompt下多响应的组内归一化优势计算优化，可缓解输出变长、奖励方差异常问题。]]></description><content:encoded><![CDATA[<p>系列文章：<br/> 1、<a href="https://www.big-yellow-j.top/posts/2026/02/26/RLHF-1.html">强化学习算法-1：GRPO、DPO与PPO解析</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2026/03/01/RLHF-2.html">强化学习算法-2：熵坍缩以及奖励坍缩问题机制分析及解决措施</a><br/> 3、<a href="https://www.big-yellow-j.top/posts/2026/03/26/RLHF-3.html">强化学习算法-3：GSPO、SAPO及KL散度改进</a></p><h2 id="简单回顾llm训练过程">简单回顾LLM训练过程</h2><p>在<a href="https://www.big-yellow-j.top/posts/2025/02/15/LLM.html">Blog</a>里面讨论过LLM框架这里简单讨论一下LLM训练过程，一般而言在LLM中训练主要分为如下几个阶段：<strong>1、预训练阶段</strong>：这部分简单理解就是让LLM能够说“人话”，自回归模型通过前一段文本然后预测下一个文本，并且让模型能够较好的“说话”（比如说：大语言模，下一个字可以正确输出 “型”）；<strong>2、后训练阶段</strong>：在得到一个能够说人话的模型之后，就需要让模型能够“思考”，这部分主要分为两部分：1、监督微调（<strong>SFT</strong> Supervised Training）；2、人类反馈强化学习（<strong>RLHF</strong> Reinforcement Learning from Human Feedback）。前者：顾名思义，我们首先使用监督学习方法，在少量高质量的专家推理数据上对 LLM 进行微调，例如指令跟踪、问题解答和/或思维链。希望在训练阶段结束时，模型已经学会如何模仿专家演示。后者：RLHF 利用人类反馈来训练奖励模型，然后通过 RL 引导 LLM 学习。这就使模型与人类的细微偏好保持一致。对于SFT以及RL两者之间差异可以简单理解为：SFT是模型对数据的“拟合”因为SFT过程中一般就是让模型输出去贴近我的数据集，而RL更加像模型对问题探索</p><h2 id="dpo">DPO</h2><p><a href="https://github.com/hkproj/dpo-notes/blob/main/DPO_Final.pdf">DPO</a>直接根据人类偏好数据对模型进行微调，使其生成更符合人类期望的输出，<strong>损失函数</strong>为：</p> \[\mathcal{L}_{\text{DPO}}(\pi_{\theta};\pi_{\text{ref}})=-\mathbb{E}_{(x,y_c,y_r)\sim\mathcal{D}}\left[\log\sigma\left(\beta\log\frac{\pi_{\theta}(y_c|x)}{\pi_{\text{ref}}(y_c|x)}-\beta\log\frac{\pi_{\theta}(y_r|x)}{\pi_{\text{ref}}(y_r|x)}\right)\right]\] <p>其中：<br/> $\pi_{\theta}$：当前优化的语言模型（策略模型，实际应用可能就是<strong>添加Lora的模型</strong>）。<br/> $\pi_{\text{ref}}$：参考模型，通常是监督微调后的模型（或者就是<strong>原始模型</strong>，亦或者更加强大的模型），用于稳定训练。<br/> $\sigma$：Sigmoid 函数，将偏好分数映射到 (0, 1)。<br/> $\beta$：一个超参数，控制偏好强度的缩放（通常取值在 0.1 到 1 之间）。<br/> $y_c,y_r$：优选和劣选回答。<br/> $\mathcal{D}$：偏好数据集。</p><h3 id="dpo-优化过程">DPO 优化过程</h3><p>直观理解上面过程，DPO模型是一种纯粹数据驱动的训练范式，比如说下面例子：<br/><img src="https://s2.loli.net/2025/06/21/C2QjJtryViueONf.webp" alt="image" width="1273" height="578" loading="lazy" decoding="async"/><br/> 模型的优化目标就是需要让 我们的模型生成更加倾向生成<strong>Good answer</strong> 并且降低生成<strong>Bad answer</strong>的概率。假设存在数据集$\mathcal{D}=(\text{Prompt}, \text{GoodAnswer}, \text{BadAnswer})$（简化为：$D=(x, y_c, y_r)$）；模型：$\pi_{\theta}$、$\pi_{\text{ref}}$。损失函数为：</p> \[\mathcal{L}_{\text{DPO}}(\pi_\theta ; \pi_{\rm ref}) = -\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\left[ \log \sigma \left( \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\rm ref}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\rm ref}(y_l|x)} \right) \right]\] <p>那么DPO处理过程为：<br/><strong>第一步</strong>进行数据准备与拼接。将输入数据通过tokenizer处理然后进行拼接得到两个输入队列： $[x,y_w]$ 以及 $[x,y_l]$<br/><strong>第二步</strong>前向计算 log-probabilities（只计算回答部分的 token 概率）。分别将两个序列输入 $\pi_\theta$ 和 $\pi_{\rm ref}$，得到 logitsq（<code>(batch_size, seq_len, vocab_size)</code>）。比如说对于chosen 序列：模型的输入长度为：$|x| + |y_w|$，模型输出 logits直接通过 <code>logits = model(input_ids_chosen).logits</code> 形状：$(1, |x|+|y_w|, V)$，只需要<strong>回答部分</strong>的 log-prob（<code>log_probs = log_softmax(logits, dim=-1)</code>），即从第 $|x|$ 个位置开始（注意：自回归模型中，第 $|x|$ 个 token 的预测对应 $y_w$ 的第一个 token）</p><pre><code class="language-python">per_token_logps = torch.gather(
    log_probs,
    dim=-1,
    index=labels.unsqueeze(-1)
).squeeze(-1)   # shape: (bs, seq_len)

# 忽略 prompt 部分（mask 掉前 |x| 个位置的 loss）
masked_logps = per_token_logps * response_mask
logps_chosen_θ = masked_logps.sum(dim=-1) / response_mask.sum(dim=-1).clamp(min=1)
</code></pre><p>通过上面代码就可以直接得到 $\log \pi_\theta(y_w | x) \approx \text{logps_chosen_θ}$。同理计算 $\log \pi_\theta(y_l | x)$、参考模型的 $\log \pi_{\rm ref}(y_w | x)$ 和 $\log \pi_{\rm ref}(y_l | x)$。<br/><strong>第三步</strong>计算loss并且反向传播。</p><pre><code class="language-python">log_ratio_w = logps_chosen_θ - logps_chosen_ref
log_ratio_l = logps_rejected_θ - logps_rejected_ref

diff = β * (log_ratio_w - log_ratio_l)
loss = -log(sigmoid(diff)).mean()
</code></pre><h2 id="ppo">PPO</h2><p><img src="https://files.seeusercontent.com/2026/05/07/Ie4v/20260226163830.webp" alt="20260226163830"/><br/> PPO是一种基于策略梯度的强化学习算法，核心思想是通过<strong>限制策略更新的幅度</strong>来保持训练的稳定性。其目标函数（通过KL散度处理）为：</p> \[L^{CLIP}(\theta)=\hat{\mathbb{E}}_{t}\left[\min(r_{t}(\theta)\hat{A}_{t},\operatorname{clip}(r_{t}(\theta),1-\epsilon,1+\epsilon)\hat{A}_{t})\right]\] \[r_t(\theta)=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}\] <p>$\pi_\theta$: 当前策略参数化的策略函数<br/> $A_t$: 优势函数，衡量动作$a_t$相对于平均水平的优势<br/> $\epsilon$: 超参数（通常0.1-0.2），限制策略更新的最大幅度<br/> ​<strong>Clipping机制</strong>：通过截断重要性采样比率，防止策略更新过大导致训练不稳定<br/> 对于上述公式里面优势函数$A_t$（用来衡量的是某个动作相对于平均水平的优势，也就是说，这个动作比平均情况好多少）具体计算公式为：$A_t=Q(s_t, a_t)-V(s_t)$，分别表示：1、$Q(s_t, a_t)$：在状态$s_t$下执行动作$a_t$得到的期望汇报；2、$V(s_t)$：状态$s_t$的平均累计期望。对于其计算可以通过GAE（广义优势估计）来进行计算。在实际优化过程中计算的loss是</p> \[L(\theta,\phi)=\underbrace{\mathbb{E}_t\left[L^{\mathrm{CLIP}}(\theta)\right]}_{\text{policy loss}}-c_1\underbrace{\mathbb{E}_t\left[(V_\phi(s_t)-\hat{R}_t)^2\right]}_{\mathrm{value~loss}}+c_2\underbrace{\mathbb{E}_t\left[S[\pi_\theta](s_t)\right]}_{\text{entropy bonus}}\] <h3 id="ppo优化过程">PPO优化过程</h3><p>对于PPO模型优化过程：<br/><img src="https://s2.loli.net/2025/09/05/AvLeinFOo5lPV6z.webp" alt="image" width="8000" height="4500" loading="lazy" decoding="async"/><br/><strong>第一步</strong> 数据采样与 rollout（on-policy 生成）。从当前策略 $\pi_\theta$（通常是 SFT 后的模型，或者直接Lora处理的模型）采样一批 prompt $x$，然后让模型<strong>自回归生成完整的模型输出</strong> $y \sim \pi_\theta(\cdot | x)$。记录每个 token 的 log-prob（用于后续 importance ratio 计算）和 entropy（可选）。此外，通常会生成多个响应（e.g. 4~64 条）来增加多样性以及更加稳定的优势估计。<br/><strong>第二步</strong> 前向计算 reward + advantage（使用 Reward Model + Critic + GAE）。对每条生成的响应 $y$。<br/> 用 <strong>Reward Model</strong> $r_\phi(x, y)$ 计算<strong>标量奖励</strong> $r$，通常在序列末尾打分，或 process-level 每步打分，一般而言对于Reward Model会直接用一个独立的模型直接将我的prompt+模型输出丢给模型然后输出一个评分。<br/> 用<strong>参考模型</strong> $\pi_{\rm ref}$（通常是冻结的 SFT 模型）计算 KL 散度（防止偏离过远）。<br/> \(\tilde{r}_t = r_t - \beta \cdot \text{KL}\bigl(\pi_\theta(\cdot|s_t) \big\| \pi_{\rm ref}(\cdot|s_t)\bigr)\) <br/> 其中 $r_t = 0$（对于 $t &lt; T$），$r_T = r$（序列末尾），$\beta$ 是 KL 系数（常见 0.01–0.05，可动态调整）。<br/> 用<strong>价值模型（Critic</strong>）$V_\psi(s)$ 估计状态价值。一般价值模型和策略模型（一般而言就是Lora分装的模型）共享大部分参数（transformer backbone），但在最后一层额外接一个线性头，输出 scalar value $V_\psi(s_t)$。计算GAE得到每一个token的优势估计 $\hat{A}_t$：</p> \[\delta_t = r_t + \gamma V_\psi(s_{t+1}) - V_\psi(s_t)\\ \hat{A}_t = \delta_t + (\gamma\lambda)\delta_{t+1} + \cdots + (\gamma\lambda)^{T-t}\delta_{T-1}\] <p>其中（$\gamma \approx 1.0$ 或 0.99，$\lambda \approx 0.95$）。$r_t$ 在第 t 步（生成第 t 个 token 后）获得的即时奖励即 Reward Model 给整个响应的分数。对于GAE过程可以这么理解：最开始模型对于prompt生成一条（多条）输出，也就是下面一个token序列</p><pre><code>s_0 = prompt
a_0 → s_1 = prompt + token_0
a_1 → s_2 = prompt + token_0 + token_1
...
a_{T-1} → s_T = prompt + 全部生成 token（结束）
</code></pre><p>直接整个序列输入给 Critic 网络，得到每一个位置的价值估计：</p><pre><code>states = [s_0, s_1, ..., s_T]          # 实际上是累积的 input_ids
values = critic(states)                # shape: [T+1] 或 [seq_len]
</code></pre><p><strong>第三步</strong> 前向计算 PPO 损失并反向传播。分别使用当前策略 $\pi_\theta$ 和旧策略 $\pi_{\theta_{\rm old}}$（通常是 rollout 开始时的策略拷贝，或上一次更新的快照）计算每个 token 的 log-probability，得到重要性采样比率 $r_t(\theta)$：</p> \[r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\rm old}}(a_t \mid s_t)} = \exp\left( \log\pi_\theta(a_t \mid s_t) - \log\pi_{\theta_{\rm old}}(a_t \mid s_t) \right)\] <p>基于这个比率和前面计算得到的优势 $\hat{A}_t$，构造 PPO-Clip 的代理目标（surrogate objective）：</p> \[L^{\mathrm{CLIP}}(\theta) = \min\left( r_t(\theta) \hat{A}_t,\ \mathrm{clip}\bigl(r_t(\theta), 1-\epsilon, 1+\epsilon\bigr) \hat{A}_t \right)\] <p>然后计算 PPO 的完整损失函数（实际训练中最小化这个 loss）：</p> \[L(\theta,\phi) = -\mathbb{E}_t \Bigl[ L^{\mathrm{CLIP}}(\theta) \Bigr]+ c_1 \mathbb{E}_t \Bigl[ \bigl(V_\phi(s_t) - \hat{R}_t\bigr)^2 \Bigr]- c_2 \mathbb{E}_t \Bigl[ S[\pi_\theta](s_t) \Bigr]+ \beta \cdot \mathrm{KL}\bigl(\pi_{\theta_{\rm old}} \big\| \pi_\theta\bigr)\] <p>其中：</p><ul><li>第一项：<strong>policy loss</strong>（负的 clipped surrogate，最大化它等价于鼓励好动作）</li><li>第二项：<strong>value loss</strong>（Critic 的 MSE 损失，$\hat{R}_t$ 是 GAE 计算的 discounted returns 或 bootstrapped target）</li><li>第三项：<strong>entropy bonus</strong>（鼓励探索，防止过早收敛）</li><li>第四项：<strong>KL 惩罚</strong>（可选，防止策略偏离过远，常见于 adaptive KL 或 early stopping）</li></ul><p>对于上述过程中用到模型有：1、policy_model（一般而言就是SFT之后的模型，被Lora封装的，<strong>模型需要被训练</strong>）；2、value_model（一般就是在policy_model基础上额外补充一个处理头去计算每个token概率，<strong>模型一般需要训练</strong>）；3、ref_model（一般就是SFT之后的模型，<strong>模型不需要训练</strong>）；4、reward_model（<strong>模型不需要训练</strong>）。另外两个模型——reward_model 和 value_model——主要是用来一起算出“优势值”（Advantage），也就是告诉模型“这个选择到底比平均水平好多少”。 简单用开车比喻：<br/><strong>reward_model</strong> 就像路上的<strong>导航终点 + 路牌评分</strong>,它告诉你“往左拐这条路通向风景最好的山顶（高奖励）”，或者“直走这条路会掉进沟里（低/负奖励）”。<strong>它负责定义“什么叫好开、什么叫开砸了”</strong>。 <br/><strong>value_model</strong> 就像<strong>车载仪表盘上的剩余路程 + 坡度提示</strong>.它实时估算：“你现在开到半山腰了，前面还有多陡的坡、多远的路、预计还能赚多少分（剩余价值）”。它帮你知道当前位置的“价值基准”，避免每次都靠运气猜。<br/> 把两者结合起来，就能算出优势值（δ_t ≈ r_t + γ V(s_{t+1}) - V(s_t)）：<br/> 相当于仪表盘告诉你：“刚才右转这个弯，比直走平均水平多省了 20 分钟油钱 + 风景更好（正优势），应该多学学这个动作！”或者：“刚才超车这个操作，虽然过了，但前面堵得更严重，实际比平均水平亏了（负优势），下次别这么干。”<br/> 一句话总结：</p><ul><li>reward_model：告诉你“哪条路是终极好路”（目标方向）</li><li>value_model：告诉你“现在开到哪儿了，离好路还有多远、多陡”（当前基准）</li></ul><p>缺了任何一个，车就只能瞎开——要么不知道终点在哪，要么不知道自己开得好不好，全靠蒙。</p><pre><code class="language-python"># 1. 计算重要性比率（importance ratio）
ratio = torch.exp(log_probs_theta - log_probs_old)   # shape: [bs, seq_len]

# 2. PPO-Clip 代理目标
clipped_ratio = torch.clamp(ratio, 1.0 - epsilon, 1.0 + epsilon)   # epsilon 通常 0.1~0.2
surrogate1 = ratio * advantages                                     # advantages: [bs, seq_len]
surrogate2 = clipped_ratio * advantages
policy_loss = -torch.min(surrogate1, surrogate2).mean()            # 负号：最小化 loss → 最大化 surrogate

# 3. Value loss（Critic 回归到 GAE target 或 returns）
# returns 可以是 GAE discounted sum，或 advantages + values
value_loss = F.mse_loss(values, returns, reduction='mean')

# 4. Entropy bonus（鼓励动作多样性）
# probs: 当前策略的 softmax 概率分布
entropy = -(probs * log_probs_theta).sum(dim=-1).mean()

# 5. 可选：KL 惩罚（per-token 或 sequence-level，防止崩坏）
# 实际中常使用 ref model 的 log_probs_ref 计算
kl = (log_probs_ref - log_probs_theta).mean()   # 近似 KL（或用更精确的无偏估计）

# 6. 组合总损失（典型权重：c1 ≈ 0.5, c2 ≈ 0.01, β ≈ 0.01~0.05）
loss = (
    policy_loss
    + c1 * value_loss
    - c2 * entropy
    + beta * kl
)
</code></pre><h2 id="grpo">GRPO</h2><p>GRPO是DPO的扩展形式，处理<strong>组级别</strong>的偏好优化问题，其核心公式：</p> \[\mathcal{J}_{\text{GRPO}}(\theta) = \mathbb{E}_{x \sim \mathcal{D}} \left[ \frac{1}{G} \sum_{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} \min\left( r_{i,t}(\theta) \hat{A}_i,\ \text{clip}(r_{i,t}(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_i \right) - \beta \, D_{\text{KL}}(\pi_\theta || \pi_{\rm ref}) \right]\] \[D_{\text{KL}}(\pi_\theta || \pi_{\rm ref})=\frac{\pi_{ref}(o_i \vert q)}{\pi_{\theta}(o_i \vert q)}-\log\frac{\pi_{ref}(o_i \vert q)}{\pi_{\theta}(o_i \vert q)}-1, \quad r_{i,t}(\theta)=\frac{\pi_{\theta}(o_{i,t} \vert q,o_{i&lt;t})}{\pi_{\theta_{old}}(o_{i,t} \vert q,o_{i&lt;t})}\] \[A_i=\frac{r_i- \text{mean}({r_1,...,r_G})}{\text{std}({r_1,...,r_G})}\\\] <p>$r^*$: 组内最优响应<br/> $\mathcal{R}$: 包含k个响应的候选集<br/> $q$: 问题, $o_i$: 对于问题生成的第 $i$ 个回答，$r_i$: 回答对应的奖励得分<br/> KL项：防止模型过度偏离初始策略，缓解模式坍塌。对于上面的loss计算公式内部的平均（$\frac{1}{\vert o_i \vert}$）是对每条轨迹的token去计算平均值，而外部的平均是对每条轨迹去计算平均值，会带来一些小的问题以及一些小的tips：<br/><strong>1、模型的输出变长（对于错误输出格外明显）</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>主要由如下两点导致：1、$\frac{1}{\vert o_i \vert}$ 偏差，在正确回答中 $A_i&gt;0$ 那么在除token长度时候就更加倾向长度越短才能保证内部min，而错误回答 $A_i&lt;0$ 则是相反会导致输出长度更加长。<strong>2、问题难度偏差</strong>，标准差较低的问题（例如，那些过于简单或过于困难，结果奖励几乎全部为 1 或 0 的问题），就会导致计<strong>方差很小但是最后优势值极大</strong>。针对这两种问题直接：1、去掉计算 $\frac{1}{\vert o_i \vert}$ ；2、不去计算 $\text{std}$</p><blockquote><p>上述为 Dr.GRPO 论文中提出改进</p></blockquote><p><strong>2、模型奖励信号是0</strong>，这点问题其实就是如果模型回答后内容奖励函数进行打分发现结果都是相同的就会导致优势值为0进而导致模型无法更新。<strong>解决措施</strong><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>：直接添加虚拟满分样本</p><pre><code>rewards = [0.8, 0.8, 0.8, 0.8]
augmented = rewards + [1.0]  # 虚拟满分样本
mean = sum(augmented) / len(augmented)  # 0.84
std = sqrt(variance(augmented))          # 0.08
advantage = (r - mean) / std             # 非零！
</code></pre><h3 id="grpo-优化过程">GRPO 优化过程</h3><p><img src="https://files.seeusercontent.com/2026/05/07/k7tS/20260226163847.webp" alt="20260226163847"/><br/> 上图中几个比较关键词：1、<strong>Policy Model</strong>：即我们需要通过强化学习优化的模型；2、<strong>Reward Model</strong>：奖励模型，即对模型做出的决策所给出的反馈（分类打分的）；3、<strong>Value Model</strong>：估计状态的价值，帮助指导策略优化（分类打分的）；4、<strong>Reference Model</strong>：提供历史策略的参考，确保优化过程中策略变化不过度。其模型优化过程为：<br/><strong>第一步</strong>数据采样与生成。对每个 prompt $x$，用当前旧策略（亦或者没有加Lora模型） $\pi_{\theta_{\rm old}}$ 生成 G 条完整响应 ${y_1, y_2, \dots, y_G} $（通常 G=16~64，采样时带 temperature &gt;0 以增加多样性）。每条 $ y_i $ 都是自回归生成的完。同时记录每条响应的 per-token log-prob（用于后续 ratio 计算）。<br/><strong>第二步</strong>计算奖励 + 组相对优势。对每条生成的响应 $ y_i $ 计算标量奖励 $ r_i $（verifiable reward），然后在组内归一化得到相对优势：</p> \[\hat{A}_i = \frac{r_i - \text{mean}(\{r_1, \dots, r_G\})}{\text{std}(\{r_1, \dots, r_G\}) + \epsilon}\] <p>（$ \epsilon $ 小常数防除零，通常 1e-8）</p><pre><code class="language-python">import torch

rewards = torch.tensor([r1, r2, ..., rG])   # shape [G]
mean_r = rewards.mean()
std_r = rewards.std() + 1e-8
advantages = (rewards - mean_r) / std_r     # shape [G]，每个响应的相对优势
</code></pre><p>这个 $A_i$ 会广播到该响应 $y_i$ 的所有 token 上（因为 reward 是 sequence-level），和PPO区别在于PPO中会通过模型（直接在Lora封装模型外接一层直接计算每一个token的状态估计值，而后通过GAE去估计最后的优势值）<br/><strong>第三步</strong>计算 GRPO 损失并反向传播。计算当前策略 $ \pi_\theta $ 和旧策略 $ \pi_{\theta_{\rm old}} $ 的 log-prob ratio（per-token）：</p><pre><code class="language-python"># ratio shape: [bs, seq_len] 或 per-response
ratio = torch.exp(log_probs_theta - log_probs_old)

clipped_ratio = torch.clamp(ratio, 1-ε, 1+ε)   # ε 通常 0.2

# surrogate objective（最大化这个，或最小化负值）
surrogate1 = ratio * advantages_i     # advantages_i 广播到该响应的所有 token
surrogate2 = clipped_ratio * advantages_i
policy_loss = -torch.min(surrogate1, surrogate2).mean()   # 平均过所有 token 和所有组
kl = ...  # 计算 π_θ 和 π_ref 的 KL（通常 per-token 平均，或无偏估计）
loss = policy_loss + β * kl   # β 通常 0.01~0.05
</code></pre><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2503.20783">https://arxiv.org/pdf/2503.20783</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2509.18851">https://arxiv.org/pdf/2509.18851</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>强化学习</category><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/02/26/RLHF-1.html</guid></item><item><title>深入浅出了解生成模型-11：Flow Matching基本原理</title><link>https://www.big-yellow-j.top/posts/2026/02/23/FlowMatching.html</link><description><![CDATA[Latent Consistency Model（LCM）通过将随机微分SDE转化为常微分ODE实现生成加速，仅需3-5步生成图像。Flow Matching算法则通过学习连续的确定性向量场（velocity field），构造从噪声到数据的最优概率路径加速生成。传统扩散模型需前向加噪、反向去噪，路径弯曲，采样需20-100步；Flow Matching直接学习从噪声（t=0）到真实数据（t=1）的连续流动路径，路径确定。训练中传统SD模型预测噪声，Flow Matching预测移动速度（如MNIST数据集中预测速度场），采样路径固定，因预测内容与路径差异，需适配方法作用于SD1.5等模型。]]></description><content:encoded><![CDATA[<p>在前面文章中介绍过<a href="https://www.big-yellow-j.top/posts/2025/06/17/CM.html">LCM</a>（Latent Consistency Model）跳过将随机过程转化为固定过程（将随机微分SDE转化为常微分ODE）进而实现生成（只需要3-5步生成图像）加速，简单回顾<br/><img src="https://s2.loli.net/2025/06/21/cXk2KYJA78PbdIW.webp" alt="image" width="781" height="357" loading="lazy" decoding="async"/><br/><strong>将随机生成过程变成“确定”过程，这样一来生成就是确定的，那么就可以实现跳跃式生成</strong>，从 $T\rightarrow t_0$ 所有的点都在“一条线”上等式 $f(x_t,t)=f(x_{t^\prime},t^\prime)$ 其中 $t,t^\prime \in [\epsilon,T]$ 成立那么就保证了模型不需要再去不断依靠 $t+1$ 生成内容去推断 $t$时刻内容。而本文介绍的Flow Matching算法，通过<strong>直接学习一个连续的确定性向量场</strong>（velocity field），构造出从噪声到数据的最优（通常近似直线）的概率路径从而实现生成的加速。</p><h2 id="flow-matching基本原理">Flow Matching基本原理</h2><h3 id="简单介绍">简单介绍</h3><p>用基本例子进行理解，比如说给定一个全随机的橡皮泥（相当于纯噪声），将其慢慢捏成一个具体的雕像（比如一只猫图像）。<strong>传统扩散模型</strong>：先把雕像一点点加上各种小随机扰动，直到完全看不出是猫了（前向加噪）。然后训练一个神经网络学会“反过来一点点去掉扰动”，从乱泥巴慢慢还原成猫（反向去噪）。这个过程有很多随机性，每走一步都有点“抖动”，所以路径是弯弯曲曲的，采样时通常要走很多步（20–100步）。<strong>Flow Matching</strong>：直接跳过一条最平滑、最直接的路，让这团乱橡皮泥顺着这条路一直往前走，就能变成猫，其核心思想就是，学习一条从噪声到数据的连续“流动”路径（flow），并且这条路径是确定的。<br/> 对于其中所谓的“流动”路径，比如说生成过程从t=0（纯噪声数据）到t=1（真实数据），如果存在“速度场”，能够了解在每一个时刻 t、每一个位置x上，东西应该往哪个方向、以多大的速度移动，才能最终到达正确图像，也就是对于微分方程 $\frac{dx}{dt}=v_{\theta}(x,t)$相对于告诉每一个瞬时速度。<strong>模型训练过程中</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>，在传统的SD模型训练过程中是去预测每一步的噪声（而后去减掉该步的噪声），而Flow Matching则是直接让模型去预测移动速度，比如下面代码<br/><img src="https://files.seeusercontent.com/2026/05/07/0zoW/image20260223160705294.webp" alt=""/><br/> 对于上图中数据直接就是MNIST数据集（x_1，y，x_0分别对应图像、标签、噪声）而上述黄色框中过程首先去生成时间步（正如上面描述的时间从t=0–&gt;t=1）那么就对应每张图像都是$t\in [0,1]$，除此之外其中的<code>path_sampler</code>就对应上面提到的流动路径，返回的两个值：$x_t=t\times x_1+ (1-t)x_0$ 以及对应的速度 $dx_t=x_1-x_0$那么最后模型预测过程也就是去预测这个速度。在得到最后的优化模型之后就是直接去采样，<strong>采样过程</strong>：<br/><img src="https://files.seeusercontent.com/2026/05/07/8Yty/image20260223162241331.webp" alt=""/><br/> 对于上述的采样路径（$x_t=t\times x_1+ (1-t)x_0$）可以直接替换为其他的都可以，除此之外Flow Matching可能不能直接（效果不佳）去作用在SD1.5此类模型可以借鉴<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>中方法进行操作，主要是因为两者在寻览过程中预测内容就是不同的，前者是直接预测噪声，并且其采样路径是随机的，而FM路径是固定的。</p><h3 id="深度介绍">深度介绍</h3><blockquote><p>ing……….</p></blockquote><p>https://mlg.eng.cam.ac.uk/blog/2024/01/20/flow-matching.html#introduction<br/> https://peterroelants.github.io/posts/flow_matching_intro/<br/> https://federicosarrocco.com/blog/flow-matching<br/> https://arxiv.org/pdf/2210.02747</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://github.com/keishihara/flow-matching/blob/main/scripts/train_flow_matching_on_image.py#L208">https://github.com/keishihara/flow-matching/blob/main/scripts/train_flow_matching_on_image.py#L208</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://github.com/CompVis/diff2flow">https://github.com/CompVis/diff2flow</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/02/23/FlowMatching.html</guid></item><item><title>模型训练分析-1：Loss以及Grad Norm分析</title><link>https://www.big-yellow-j.top/posts/2026/02/20/ModelTraining.html</link><description><![CDATA[训练Qwen2.5VL-3B模型时出现Loss下降但Grad Norm先降后升的现象。模型采用AdamW优化器、cosine学习率warm up策略及交叉熵损失函数，通过tensorboard记录训练指标。Loss反映模型拟合效果，Grad Norm为所有参数梯度向量拼接后的L2范数，反映优化器中间状态。分析表明，Grad Norm上升可能因梯度范数与参数范数成正比，参数范数增加导致；也与权重衰减和学习率安排（尤其是warm up策略）的相互作用有关。]]></description><content:encoded><![CDATA[<p>在训练模型（Qwen2.5VL-3B）过程中出现奇怪现象：Loss下降但是Grad Norm先下降后上升的情况争对这种情况简单调研分析，首先选择模型以及训练过程中参数如下：Qwen2.5VL-3B、AdamW、cosine（学习率warm up策略）、交叉熵损失函数。而后通过tensorboard记录优化过程loss以及grad_norm，其中记录方式如下：</p><pre><code class="language-python">outputs = model(**batch_data)
loss = outputs.loss
accelerator.backward(loss)
if accelerator.sync_gradients:
    # 记录裁剪钱梯度值
    # grad_norm = torch.norm(torch.stack(
    #     [torch.norm(p.grad.detach(), p=2.0) 
    #      for p in model.parameters() if p.grad is not None])).item()
    # accelerator.clip_grad_norm_(model.parameters(), 
    #                             config.max_grad_norm)
    # 直接记录裁剪侯的梯度值
    raw_grad_norm = accelerator.clip_grad_norm_(model.parameters(), config.max_grad_norm)
    grad_norm = raw_grad_norm.item() if hasattr(raw_grad_norm, 'item') else raw_grad_norm
...
if accelerator.sync_gradients:
    progress_bar.update(1)
    global_step += 1
    if accelerator.is_main_process:
        accelerator.log(
            {'Train/Loss': loss.detach().item(), 
             'Train/lr': lr_scheduler.get_last_lr()[0],
             'Train/graid_norm': grad_norm}, 
            step=global_step)
</code></pre><p>通过上面方式去记录loss等变化情况得到最终图像如下：<br/><img src="https://files.seeusercontent.com/2026/05/07/Qrx9/image20260220153354808.webp" alt=""/></p><h2 id="loss以及grad-norm理论简单分析">loss以及Grad Norm理论简单分析</h2><p>首先loss往往直接用来表示模型的拟合效果（loss下降代表拟合效果较好，部分震荡代表数据中部分样本很难较好的进行“拟合”）。Gradient（梯度）一般而言就是对于需要优化函数的导数，而Grad Norm一般就是表示<strong>所有参数梯度向量拼接（展平）后形成的超长向量的 L2 范数</strong>。在模型训练过程总一般而言主要关注两个指标比较多：1、loss；2、评估指标（ACC等），但是对于Grad Norm这个值相对讨论较少，简单对于Grad Norm过程指标（optimization dynamic 的诊断信号），区别loss它不直接衡量模型好坏，而是反映优化器当前“还能走多远、多快”、训练是否稳定、是否接近某种奇异点等中间状态。<br/> 那么理论上而言模型优化过程中应该是loss以及Grad Norm（越往后期模型理论上越接近“最优值”那么梯度理论越小）两个指标都一起下降，但是实际情况可能相反，下面就这种情况简单分析如下：</p><h2 id="grad-norm上升原因分析">Grad Norm上升原因分析</h2><p>在Github-issue<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>中给出结论是：<strong>梯度范数大致与参数范数成正比</strong>（或者至少取决于参数范数）。作者直接给出了梯度与模型参数的变化情况分析：$\Vert \nabla f(\theta) \Vert ≈ \Vert \theta \Vert \cdot \Vert \nabla f(\theta / \Vert \theta \Vert)\Vert$，那么也就意味了如果模型 $\theta / \Vert \theta \Vert$ 大致逐渐收敛但是参数 $\Vert \theta \Vert$在增加就会导致最终的Grad Norm逐渐上升。<br/> 在论文中<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>作者给出解释是：<strong>权重衰减与学习率安排相互作用的结果</strong>，具体理论分析如下:<br/><img src="https://files.seeusercontent.com/2026/05/07/kxQ3/image20260220162749186.webp" alt=""/><br/> 这样一来梯度$g$ 与权重 $x_t$ 之间关系就只与学习率 $\gamma$ 和参数 $\lambda$ 之间有关系。因此就可以得到：$\frac{\Vert g_t \Vert}{\Vert x_t \Vert}=\sqrt{\frac{2\lambda}{\gamma_t}}$ 当使用<strong>学习率warm up策略</strong>时候就会发生下降上升的情况。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:2" role="doc-endnote"><p><a href="https://github.com/allenai/OLMo/issues/596#issuecomment-2147860609">why is the total_grad_norm increasing across training? </a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/abs/2506.02285">Why Gradients Rapidly Increase Near the End of Training</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/02/20/ModelTraining.html</guid></item><item><title>深入浅出了解生成模型-10：模型蒸馏与剪枝</title><link>https://www.big-yellow-j.top/posts/2026/02/15/ModelDistillation.html</link><description><![CDATA[大模型推理优化除量化、多attention方案、cache策略外，可通过模型蒸馏、模型剪枝两类方案实现参数优化。模型蒸馏核心是让小体量学生模型学习预训练大模型的行为逻辑，主流方案包括叠加双损失的KD知识蒸馏、带温度参数的DKD解耦知识蒸馏、基于双损失约束的DMD分配匹配蒸馏，可在降低计算量的前提下尽可能保留大模型性能。模型剪枝分为置零单权重的非结构化剪枝、移除整体结构单元的结构化剪枝两类，需通过多轮剪枝+微调迭代实现，可借助PyTorch内置prune工具快速落地。]]></description><content:encoded><![CDATA[<p>在<a href="https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html">最开始的文章</a>中介绍了模型量化、使用不同attention方式、cache策略去对模型生成进行加速，这里主要介绍几种模型蒸馏以及模型剪枝的方式去优化模型参数</p><h2 id="模型蒸馏">模型蒸馏</h2><p>模型蒸馏是一种模型压缩和优化技术。简单来说，就是让一个<strong>小模型（学生模型）去模仿一个已经训练好的大模型（教师模型）的行为，从而用更小的体积、更低的计算量，尽量接近大模型的性能</strong>。比如说Qwen0.5B模型就是直接从一个较大的模型进行蒸馏得到。而对于蒸馏过程也有很多，最常见的就是<strong>直接通过数据进行蒸馏</strong>，比如说通过CahtGPT生成高质量数据而后将这部分高质量数据进行模型训练也可以达到蒸馏的目的。亦或者直接通过模型进行蒸馏，<strong>最简单的蒸馏例子</strong>（计算students的预测loss以及students和teacher模型之间的KD）：<strong>1、KD知识蒸馏过程</strong>：直接定义一个参数较小的学生模型而后对于相同的数据分别通过小模型以及大模型处理，再去计算“叠加loss”：$L=L_{student}+ L_{KD}$ 其中第一项是小模型的loss第二项是计算小-大模型之间的KD；<strong>2、DKD解耦知识蒸馏</strong>：$L=\alpha L_{TCKD}+ \beta L_{NCFD}$ 其中 <br/> \(\mathcal{L}_{TCKD} = \mathrm{KL} ( [p^S_t,\ 1-p^S_t]\ \Vert\ [p^T_t,\ 1-p^T_t] )\) <br/> 以及 <br/> \(\mathcal{L}_{NCKD} = \mathrm{KL} ( \frac{p^S}{\sum_{j\neq t} p^S_j},\ \frac{p^T}{\sum_{j\neq t} p^T_j} )\)<br/> 在实际应用过程中有一个比较重要参数：temperature使用方式和llm中的相同都是输出概率去除温度系数，比如说softmax中：$\frac{e^{x/t}}{\sum e^{x/t}}$$</p><blockquote><p>对于两种简单的知识蒸馏代码：<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/DF_acceralate">代码</a></p></blockquote><h3 id="分配匹配蒸馏dmd">分配匹配蒸馏（DMD）</h3><p>对于DMD<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>方法原理如下（DMD1的算法流程）：</p><p><img src="https://files.seeusercontent.com/2026/05/07/Tee1/image20260219145134921.webp" alt="DMD1"/></p><p>对于上诉算法流程图简单描述在DMD蒸馏中主要是通过两个Loss实现，<strong>1、regression loss（回归损失）</strong>：对于教师模型生成过程中得到一批noise-image对（对应 $z_{ref}, y_{ref}$），对于蒸馏的学生模型生成器 $G_\theta$ 直接用初始化噪声以及noise-image中的噪声进行单步生成得到image分别得到：$x$ 以及 $x_{ref}$，而后对于noise-image中的噪声直接去计算<strong>LPIPS损失函数</strong>（主要是计算两组图像之间的相似度，<a href="https://lightning.ai/docs/torchmetrics/stable/image/learned_perceptual_image_patch_similarity.html">代码实现</a>）；<strong>2、diffusion loss</strong>：蒸馏过程中核心损失，在一般的蒸馏模型过程如LCM是去强迫学生模型模仿 teacher 的每一步去噪轨迹，而DMD则是去计算最终生成的图像分布是否和真实分布之间是否一致，具体的处理过程直接去计算distributionMatchingLoss：<br/><img src="https://files.seeusercontent.com/2026/05/07/9eqO/image20260331210034629.webp" alt=""/><br/> 从上面过程很容易知道处理过程，通过计算“真假两个扩散模型”在噪声预测上的偏差，产生一个指引梯度，推着学生模型去生成更符合真实统计规律的图像。<br/><strong>在DMDv2</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>中<strong>直接取消了回归损失</strong>（主要是在DMDv1中需要预先通过教师模型生成一个庞大的“文本/噪声-图像”对数据集，并且使用回归损失和分布匹配的核心思想相悖，限制了模型性能：回归损失强制学生模型模仿教师模型的具体采样路径，而不是匹配整体的输出分布。）但是直接取消会带来训练效果下降，因此在论文里面使用<strong>TTUR</strong>：在DMDv1中使用的loss处理思路和GAN相似，因此在v2中未来弥补取消回归损失影响对模型优化影响，对于生成器更新一次，而我的判别器会更新n次（论文中5次，在TTUR原始论文中是使用不同的学习率）。<br/><img src="https://files.seeusercontent.com/2026/05/07/2hzE/image20260401140400394.webp" alt=""/><br/> 除此之外<strong>引入GAN损失</strong>：<br/> \(L_{GAN} = \mathbb{E}_{x \sim p_{real}, t \sim [0,T]} [\log D(F(x, t))] + \mathbb{E}_{z \sim p_{noise}, t \sim [0,T]} [-\log(D(F(G_\theta(z), t)))]\)<br/> 各项参数含义，D：GAN discriminator（分类器），通常作为 fake denoiser（μ_fake）的 bottleneck 上的一个分支。F(·, t)：forward diffusion process（在图像上加噪声到 timestep t），让 discriminator 在 noisy 版本上区分 real vs fake（这与 diffusion 的 noisy nature 兼容）。$G_θ(z)$：student generator 输出的图像（z 是纯噪声）。生成器 $G_θ$ 最小化损失（让 D 把 fake 判断成 real）；D 则学习区分 real 与 fake。简单总结上述loss计算过程，对于学生模型 $G_θ(z)$ 通过对其解析拆解构成判别器模型，在生成图像之后通过判别去判别（GAN的思路），<strong>总结而言就是将DMDv1中回归损失直接更换为GAN损失其他保持不变</strong>。</p><blockquote><p><a href="https://www.big-yellow-j.top/posts/2025/06/17/CM.html">LCM蒸馏过程</a>：训练过程可以简单理解为：对于输入图像 $x$，直接添加 $n$ 步的噪声得到 $x_n$，而后我的学生模型直接去预测 $t_0$ 时候的结果 $y_1$；同时，我的教师模型（预训练好的扩散模型）从 $x_n$ 出发，通过 DDIM 采样器向前走一步（跨越 $k$ 个时间步），得到 $t_{n-k}$ 时刻在轨迹上的观察点 $x_{n-k}$；而后再去用学生模型通过 $x_{n-k}$ 预测 $t_0$ 的结果得到 $y_2$。最后计算 $y_1$ 与 $y_2$ 之间的距离损失（Consistency Loss），迫使模型无论从哪个时间步出发，预测的终点都指向同一点。</p></blockquote><h2 id="模型剪枝">模型剪枝</h2><p>在一个训练好的大模型中，数以亿计的参数（权重）里，也存在大量冗余或贡献微弱的连接。模型剪枝的核心思想，就是识别并移除这些“不重要”的权重，从而得到一个更小、更高效的模型。在剪枝方法上主要两大类，1、<strong>非结构化剪枝</strong>，将单个权重值置为零。这会产生一个“稀疏”的权重矩阵，即矩阵中包含大量零值。2、<strong>结构化剪枝</strong>，移除整个结构单元，例如整个神经元（矩阵的行/列）、注意力头，甚至是整个网络层。模型剪枝过程必须<strong>剪枝-&gt;微调-&gt;剪枝-&gt;微调</strong>不断地重复这个过程。<strong>最简单的剪枝过程</strong>可以直接使用<code>torch.nn.utils.prune</code>来进行操作，在代码中torch提供多种剪枝条方法（一般后缀中有 <code>_unstructured</code>表示的是非结构化的剪枝，而<code>_structured</code>则是表示结构化的剪枝），<strong>非结构化剪枝代码</strong></p><pre><code class="language-python">import torch.nn.utils.prune as prune
prune.global_unstructured(
    parameters_to_prune,
    pruning_method=prune.L1Unstructured,
    amount=10,
)
</code></pre><p>不过需要注意的是按照<a href="https://docs.pytorch.org/docs/stable/generated/torch.nn.utils.prune.global_unstructured.html">官方介绍</a>：对于非结构化剪枝的区域回去添加一个名为name+’_mask’的命名缓冲区，对应于修剪方法对参数名称应用的二进制掩码（mask是一个0/1张量量化过程就是：<code>weight = weight_orig * mask</code>）。将参数名称替换为修剪后的版本，而原始（未修剪）参数存储在名为name+’_orig’的新参数中。也就意味着在 <code>prune.global_unstructured</code><strong>不会改变模型大小只是添加mask让模型推理可以加速</strong>。<strong>结构化剪枝代码</strong>：</p><pre><code class="language-python">import torch.nn.utils.prune as prune
prune.ln_structured(module,...)
</code></pre><p>具体测试脚本：<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/RL-TRL/ModelPrune.py">ModelPrune.py</a></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:2" role="doc-endnote"><p><a href="https://tianweiy.github.io/dmd/">One-step Diffusion with Distribution Matching Distillation</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2405.14867">Improved Distribution Matching Distillation for Fast Image Synthesis</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/02/15/ModelDistillation.html</guid></item><item><title>深入浅出了解生成模型-9：扩散模型微调框架ai-toolkit介绍</title><link>https://www.big-yellow-j.top/posts/2026/01/28/Ai-toolkit-Training.html</link><description><![CDATA[Ai-toolkit框架用于扩散模型微调，需在autodl服务器（VGPU-32G、CUDA 13.0）安装，配置hf token后上传数据集（图像为.jpg/.jpeg/.png，文本为txt且一一对应，路径xxx/ai-toolkit/datasets）。训练界面参数建议默认模型路径，模型下载报错可重启任务。数据处理涉及dataset构建（FileItemDTO处理prompt和图像tensor转换）、DataLoaderBatchDTO（含tensor、control_tensor、caption_list），训练通过train_single_accumulation函数进行vae...]]></description><content:encoded><![CDATA[<p>本文主要介绍Ai-toolkit框架去对扩散模型进行微调操作</p><h2 id="ai-toolkit">Ai-toolkit</h2><h3 id="ai-toolkit安装介绍">Ai-toolkit安装介绍</h3><h4 id="环境准备">环境准备</h4><p>在autodl上的服务器进行的操作（GPU：VGPU-32G，CUDA Version: 13.0 ）</p><pre><code class="language-python"># 首先安装基本环境
source /etc/network_turbo  # autodl 上执行该命令进行代理
git clone https://github.com/ostris/ai-toolkit.git
cd ai-toolkit
conda create -n ai-toolkit python=3.12
conda activate ai-toolkit
pip3 install --no-cache-dir torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu126
pip3 install -r requirements.txt

# 安装 npm
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
apt update
apt install -y nodejs

# 安装完毕之后直接测试，如果显示版本那么表示安装成功
node -v # v24.13.0
npm -v # 11.6.2
npm config set registry https://registry.npmmirror.com # 换npm源

# 由于启动了代理可以先使用下面代码之后再去执行 run 程序
npm config set strict-ssl false
export NODE_TLS_REJECT_UNAUTHORIZED=0

export HF_ENDPOINT=https://hf-mirror.com
# 建议取修改hf模型下载路径
vim ~/.bashrc
export HF_HOME="/path/to/you/dir"  # 替换为你想更改的目标路径
source ~/.bashrc

cd ai-toolkit/ui
export HF_ENDPOINT=https://hf-mirror.com
npm run build_and_start
</code></pre><p>上面运行代码运行之后出现：<br/><img src="https://s2.loli.net/2026/02/03/SYvOuAUjJtMZV4x.webp" alt="image" width="827" height="185" loading="lazy" decoding="async"/><br/> 可以直接访问上面地址进入 ai-toolkit</p><h4 id="界面简单介绍">界面简单介绍</h4><p>首先取配置自己hf token<br/><img src="https://s2.loli.net/2026/01/28/es4i58XBa3K7Oxr.webp" alt="image" width="2555" height="460" loading="lazy" decoding="async"/><br/> 配置完毕之后可以直接上传数据集/直接在本地数据集，不过数据集需要在路径：<code>xxx/ai-toolkit/datasets</code>（对应上面图像中的路径） 中除此之外还需要注意数据集的格式问题，以文生图任务为例，我的数据集必须满足：1、图像必须是：.jpg, .jpeg, .png；2、文本：txt。除此之外图片文本之间必须匹配：1.png 1.txt…..<br/><img src="https://s2.loli.net/2026/02/03/marREeQHCq3bnyi.webp" alt="image" width="1190" height="358" loading="lazy" decoding="async"/><br/> 可以直接将上面文件夹上传到<code>xxx/ai-toolkit/datasets</code>中<br/><img src="https://s2.loli.net/2026/01/28/rxjsKY8hdpgV1BE.webp" alt="image" width="2559" height="375" loading="lazy" decoding="async"/><br/><img src="https://s2.loli.net/2026/01/28/VLBpKZX3JSti2IG.webp" alt="image" width="2560" height="1311" loading="lazy" decoding="async"/><br/> 对于<strong>训练界面参数</strong>介绍：<br/><img src="https://s2.loli.net/2026/02/03/os42J5FXbDHVA1a.webp" alt="image" width="2551" height="1280" loading="lazy" decoding="async"/><br/><img src="https://s2.loli.net/2026/02/03/weFAziYN1xkv3mp.webp" alt="image" width="2560" height="1306" loading="lazy" decoding="async"/><br/> 1、模型路径尽量不要去修改就用默认的，如果要去修改可以参考：reddit上的方法<br/> 2、如果报错是和模型下载相关（如CAS报错、hf_transfer报错），可以直接重启任务就行（去Training Quene找到任务然后重新启动即可）<br/><strong>模型训练</strong>处理<br/><img src="https://s2.loli.net/2026/02/03/PnH8iG7k4tmhNBu.webp" alt="image" width="2560" height="384" loading="lazy" decoding="async"/></p><h3 id="ai-toolkit模型微调">Ai-toolkit模型微调</h3><p>对于文生图/图生图微调训练很简单只需要将上面的数据进行修改即可，而后点击开始训练即可<br/><img src="https://s2.loli.net/2026/01/28/ydSs2CKgG8tD5T7.webp" alt="image" width="1546" height="594" loading="lazy" decoding="async"/><br/> 值得注意的是，如果模型还需要去继续训练，比如说我在第一批数据得到一个lora但是又有另外一批数据需要继续去训练，在前端是不支持的，需要重新去建立一个训练任务（可以直接复制第一批数据的yaml文件）而后将其中的数据以及命名都改掉，修改yaml中的<code>name: Flux-Klein-AID-Inpatient-2000-9B</code>，而后将上一批数据中训练得到的最好的lora复制到新的文件夹中，而后启动训练即可（<code>python run.py output/tmp/config.yaml</code>）这个过程会在终端出现</p><pre><code class="language-bash">#### IMPORTANT RESUMING FROM /root/autodl-tmp/xxx/ai-toolkit/output/xxx/xxx.safetensors ####
Loading from /root/autodl-tmp/xxx/ai-toolkit/output/xxx/xxx.safetensors
</code></pre><p>说明模型重新去加载了lora，具体代码在<code>jobs/process/BaseSDTrainProcess.py</code>中的函数<code>get_latest_save_path</code>就是去加载最新得到权重进行继续训练。</p><pre><code class="language-python">if latest_save_path is not None:
    print_acc(f"#### IMPORTANT RESUMING FROM {latest_save_path} ####")
    model_config_to_load.name_or_path = latest_save_path
    self.load_training_state_from_metadata(latest_save_path)
</code></pre><h3 id="ai-toolkit代码分析">Ai-toolkit代码分析</h3><p>在Ai-toolkit中模型微调整个流程如下：<a href="https://drive.google.com/file/d/1X87iDyYk2ebtdrG5-_Q4qUvu67wwvEOs/view?usp=sharing">Googledrive-Drawio</a>。对于上述流程图中只介绍了对于模型、数据都是如何处理的，对于具体如何处理没有介绍，这里简单做一些介绍数据以及模型处理过程进行初步介绍，对于数据处理过程：<br/><strong>对于dataset构建过程</strong>：</p><pre><code class="language-python">class AiToolkitDataset(LatentCachingMixin, ControlCachingMixin, CLIPCachingMixin, TextEmbeddingCachingMixin, BucketsMixin, CaptionMixin, Dataset):
  def __init__(self, ...):
    ...
    self.file_list: List['FileItemDTO'] = []
    # 其中 self.dataset_path 对应我的 yaml 文件中的 folder_path
    if os.path.isdir(self.dataset_path):
      extensions = image_extensions
      if self.is_video:
          extensions = video_extensions
      file_list = [os.path.join(root, file) for root, _, files in os.walk(self.dataset_path) for file in files if file.lower().endswith(tuple(extensions))]
    else:
        with open(self.dataset_path, 'r') as f:
            self.caption_dict = json.load(f)
            # keys are file paths
            file_list = list(self.caption_dict.keys())
    # 而后去对数据进行重复采样
    ...
    for file in tqdm(file_list):
      try:
      file_item = FileItemDTO(
          sd=self.sd,
          path=file,
          dataset_config=dataset_config,
          dataloader_transforms=self.transform,
          size_database=self.size_database,
          dataset_root=dataset_folder,
          encode_control_in_text_embeddings=self.sd.encode_control_in_text_embeddings if self.sd else False,
          text_embedding_space_version=self.sd.model_config.arch if self.sd else "sd1",
          te_padding_side=self.sd.te_padding_side if self.sd else "right",
          latent_space_version=latent_space_version,
      )
      self.file_list.append(file_item)
  def __len__(self):
    if self.dataset_config.buckets:
      return len(self.batch_indices)
    return len(self.file_list)
  def __getitem__(self, item):
    if self.dataset_config.buckets:
      if len(self.batch_indices) - 1 &lt; item:
        idx_list = self.batch_indices[item]
        return [self._get_single_item(idx) for idx in idx_list]
    else:
        return self._get_single_item(item)
</code></pre><p>对于上面的<code>FileItemDTO</code>其实就是对于上面流程图中比如说<code>LatentCachingFileItemDTOMixin</code>这些父类都是定义了一些基础数据处理，比如说prompt、图像进行tensor转换等。<strong>对于loader过程</strong>，在ai-toolkit中会将数据都封装为：<code>DataLoaderBatchDTO</code>(<code>toolkit/data_transfer_object/data_loader.py</code>)对于里面定义了很多属性值需要关注的就是如下几个（<code>batch = next(iter(data_loader))</code>）：1、<code>batch.tensor</code>：对于目标图的tensor尺寸大小；2、<code>batch.control_tensor</code>：对应条件图的tensor尺寸大小；3、<code>batch.get_caption_list()</code>：获取整个batch中所有的提示词。在得到完整的laoder数据集之后就是直接进行模型训练。对于训练过程<strong>整体代码如下</strong>：</p><pre><code class="language-python">class BaseSDTrainProcess(BaseTrainProcess):
  ...
  # 预先定义好整个训练过程就和hf中trainer中一样

class SDTrainer(BaseSDTrainProcess):
  # 具体训练过程就和DPOTrainer一样都是继承一个小的trainer
  ...
  def __init__(...):
    ...
  def hook_train_loop(self, batch: Union[DataLoaderBatchDTO, List[DataLoaderBatchDTO]]):
    # 模型计算loss/梯度更新
    for batch in batch_list:
      ...
      loss = self.train_single_accumulation(batch) # 去vae等编码而后计算loss
      ...
      if total_loss is None:
          total_loss = loss
      else:
          total_loss += loss
      ...
    if not self.is_grad_accumulation_step:
      # 梯度累计 
      ...
      self.accelerator.clip_grad_norm_(...)
      ...
      with self.timer('optimizer_step'):
        self.optimizer.step()
        self.optimizer.zero_grad(set_to_none=True)
      ...
    with self.timer('scheduler_step'):
      self.lr_scheduler.step()
    loss_dict = OrderedDict({'loss': (total_loss / len(batch_list)).item()})
    self.end_of_training_loop()
    return loss_dict
</code></pre><p><strong>对于训练过程中</strong>，主要是<code>self.train_single_accumulation(batch)</code>在这个函数中对于输入的batch会进行vae、text_encoder等进行编码然后计算loss整个过程如下：</p><pre><code class="language-python">def train_single_accumulation(self, batch: DataLoaderBatchDTO):
  with torch.no_grad():
    ....
    noisy_latents, noise, timesteps, conditioned_prompts, imgs = self.process_general_training_batch(batch)
    if self.train_config.do_cfg or self.train_config.do_random_cfg:
      # 如果要做文本的 cfg，如果有negative那么就使用否则直接用 '' 空字符串代替
      ....
    if self.adapter and isinstance(self.adapter, CustomAdapter):
      # 如果有 adapter 那么就用adapter去处理 conditioned_prompts
      conditioned_prompts = self.adapter.condition_prompt(conditioned_prompts)
    ...
    if self.train_config.short_and_long_captions_encoder_split and self.sd.is_xl:
      # 如果是 sdxl 模型会对prompt进行切断然后长/短分别交给不同编码器
      ...
    if self.train_config.single_item_batching:
      # 单样本逐个批处理模式，主要是为了处理显存不够情况，那么将数据进行按 bs进行chunk到list中
      batch_size = noisy_latents.shape[0]
      noisy_latents_list = torch.chunk(noisy_latents, batch_size, dim=0)
      ...
    else:
      noisy_latents_list = [noisy_latents]
      ...
    # 一次读取数据开始训练
    for noisy_latents, noise,... in zip(noisy_latents_list, noise_list, ...):
      with (network):
        # 专门处理“图像适配器”（adapter）的条件嵌入编码和注入，让模型在当前 batch 的去噪预测中能利用额外的图像条件（如 IP-Adapter 风格的 CLIP Vision 图像提示，或自定义的图像参考）。
        # TODO: 详细去了解一下如何使用 adapter 在ai-toolkit中
      with self.timer('encode_prompt'):
        prompt_kwargs = {}
        if self.sd.encode_control_in_text_embeddings and batch.control_tensor is not None:
          prompt_kwargs['control_images'] = batch.control_tensor.to(...)
        if self.train_config.unload_text_encoder or self.is_caching_text_embeddings:
          # 这部分参数在前端中就有选择
          # 如果卸载text_encoder 以及缓存text_embedding的时候需要处理
          ...
        elif grad_on_text_encoder:
          # 训练 text_encoder
          ...
        # TODO: 详细去了解一下如何使用 adapter 在ai-toolkit中
      if self.train_config.timestep_type == 'next_sample':
        # 获取 预测的noise
        ...
      if batch.unconditional_latents is not None or self.do_guided_loss:
        # 用差分引导损失（让 LoRA 专注“变化部分”，防过拟合/出血）
        ...
      elif self.train_config.loss_type == 'mean_flow':
        # 用 flow-matching 专属的平均流损失
        ...
      ...
        self.accelerator.backward(loss)
  return loss.detach()
</code></pre><p><strong>数据后处理过程</strong>：对于<code>process_general_training_batch</code>过程需要去看<code>BaseSDTrainProcess</code>（<code>jobs/process/BaseSDTrainProcess.py</code>）代码对于这个过程简单总结如下：<br/> 1、<code>prepare_prompt</code>：对 prompts 进行系统性扩展和条件化处理，确保模型在不同配置（短/长 caption、refiner、embedding、trigger、prompt saturation）下，得到正确且一致的输入文本<br/> 2、<code>prepare_latents</code>：直接获取<code>batch.tensor</code>进行vae编码<br/> 3、<code>prepare_scheduler</code>：直接处理好调度器，比如说调度此要采样多少步，默认是num_train_timesteps=1000<br/> 4、<code>prepare_timesteps_indices</code>：选择时间步，比如说参数<code>next_sample</code>以及<code>one_steps</code>前面代表去随机选择(0,num_train_timesteps-2)或者直接就是一步（类似flow-matching）以及<code>content_or_style</code>参数主要是3个值：<code>content</code>：选择靠前的时间步（学习图像结构特征）、<code>style</code>：选择靠后的时间步（学习图像纹理）、<code>balanced</code>（<strong>默认</strong>）：选择中段时间步，之所以这样是因为df解噪声过程直接从1-1000越往后模型越体现的是细节内容<br/> 5、<code>convert_timestep_indices_to_timesteps</code>：将时间步添加到调度器中，<code>self.sd.noise_scheduler.timesteps[timestep_indices.long()]</code><br/> 6、<code>prepare_noise</code>： 生成噪声<br/> 7、<code>make_noisy_latents</code>：将得到的noise添加到latents中。<br/> 通过上面过程最后返回：<code>noisy_latents, noise, timesteps, conditioned_prompts, imgs</code>，这样一来标准的输入数据格式就准备好了。</p><h3 id="ai-toolkit-训练参数">ai-toolkit 训练参数</h3><p>值得注意的是除去ai-toolkit中前端默认参数还可以直接自定义参数（较多参数都在<code>ai-toolkit/toolkit/config_modules.py</code>文件中<strong>给了默认参数</strong>）：</p><ul><li><code>model</code> 模块参数（直接去<code>BaseSDTrainProcess.py</code>看参数）</li></ul><p><code>assistant_lora_path:str</code>： lora模型路径（建议直接使用 hf地址）</p><ul><li><code>train</code> 模块参数（直接去<code>BaseSDTrainProcess.py</code>看参数）</li></ul><table><thead><tr><th style="text-align: center">参数名称</th><th style="text-align: center">参数描述</th><th style="text-align: center">注意事项</th></tr></thead><tbody><tr><td style="text-align: center"><code>xformers:bool</code></td><td style="text-align: center">是否启动xformer，直接去vae以及unet中启动xformer加速计算</td><td style="text-align: center">注意模型是不是支持xformer，代码位置</td></tr><tr><td style="text-align: center"><code>attention_backendattention_backend:str</code></td><td style="text-align: center">后端attention计算方式，也是对vae/unet进行，比如说<code>flash</code>等</td><td style="text-align: center"> </td></tr><tr><td style="text-align: center"><code>decorator_config</code></td><td style="text-align: center">这部分参数配置和 <code>train</code> 中配置参数写的方式是一样的</td><td style="text-align: center"> </td></tr></tbody></table><ul><li><code>decorator</code> 模块参数（暂时只支持 flux 模型）（默认没有，参数方法和 train 中使用方法相同）</li></ul><p><code>num_tokens:int</code> 文本嵌入修饰器/适配器，专门用于在扩散模型的文本条件输入上额外拼接几个可学习的 token</p><ul><li><code>adapter</code> t2i模块参数（默认没有，参数方法和 train 中使用方法相同）</li></ul><p>对于这个模块参数可以<a href="https://huggingface.co/spaces/rahul7star/ai-toolkit/blob/main/config/examples/train_flex_redux.yaml">参考配置</a>。主要作用是实现图像到图像（image-to-image）条件适配器，主要功能是，让模型“看见”参考图像，然后根据文本提示 + 参考图像 来生成变体（variation）、风格迁移、细节保持等。</p><pre><code class="language-yaml">adapter:
  train: false                # adapter是否参与训练  
  type: "ip_adapter"          # 支持："t2i", "control_net", "clip", "ip" 也支持直接自定义  
  name_or_path: "h94/IP-Adapter"      # 上面4种参数直接会从 hf 上进行加载，对于自定义的值就回去加载custom
  weight_name: "ip-adapter_sd15.safetensors"
  scale: 0.8                  # 强度缩放（可选，默认 1.0）
  test_img_path:
    - "path/to/your/image.png"
    - "path/to/your/image2.png"
</code></pre><h3 id="排错处理">排错处理</h3><ul><li>1、加载数据过程中出现数据形状不匹配问题</li></ul><p><img src="https://files.seeusercontent.com/2026/05/07/kK2f/image_17727623386283.webp" alt="企业微信截图_17727623386283"/><br/> 对于上面截图报错在测试<code>Klein9B</code>模型过程中出现，主要是在测试<code>batch_size&gt;1</code>就容易发生，可以直接将所有的数据都固定到相同形状可以解决</p><ul><li>2、进程一直被<code>killed</code></li></ul><p>在运行代码（直接使用<code>python run.py ...</code>）有些适合就会在终端直接被<code>killed</code>（ai-toolkit默认的 <code>torch=2.7</code>可能版本不太合适）切换为 <code>torch=2.10</code>的版本 <code>pip3 install --no-cache-dir torch==2.10.0 torchvision==0.25.0 torchaudio==2.10.0 --index-url https://download.pytorch.org/whl/cu126</code></p><ul><li>3、hf_transfer/CAS 错误</li></ul><blockquote><p>An error occurred while downloading using <code>hf_transfer</code>. Consider disabling HF_HUB_ENABLE_HF_TRANSFER for better error handling.</p></blockquote><p>启动命令前使用</p><pre><code class="language-cmd">export HF_HUB_ENABLE_HF_TRANSFER=0 # 处理hf_transfer报错
export HF_HUB_DISABLE_XET=1 # 处理CAS报错
</code></pre><ul><li>4、端口占用错误</li></ul><pre><code class="language-cmd">[UI] next start --port 8675 restarted
[UI]  ⨯ Failed to start server
[UI] Error: listen EADDRINUSE: address already in use :::8675
[UI]     at &lt;unknown&gt; (Error: listen EADDRINUSE: address already in use :::8675)
[UI]     at new Promise (&lt;anonymous&gt;) {
[UI]   code: 'EADDRINUSE',
[UI]   errno: -98,
[UI]   syscall: 'listen',
[UI]   address: '::',
[UI]   port: 8675
[UI] }
[UI] next start --port 8675 exited with code 1
</code></pre><p>保持npm运行另起终端</p><pre><code class="language-cmd">sudo apt install net-tools
sudo netstat -tunlp | grep ':8675'
kill -9 470457
</code></pre> ]]></content:encoded><category>生成模型</category><pubDate>Wed, 28 Jan 2026 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2026/01/28/Ai-toolkit-Training.html</guid></item><item><title>深入浅出了解生成模型-8：生成加速策略概述</title><link>https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html</link><description><![CDATA[扩散模型生成加速策略主要包括加速框架、Cache策略和量化技术。加速框架通过优化计算后端（如flash_attn）、torch.compile编译、torch.channels_last内存访问优化及xFormers加速attention计算，结合CPU卸载和设备分配等显存优化措施提升效率。Cache策略利用扩散过程时间冗余，如DeepCache缓存UNet高层特征、FORA复用DiT的Attn/MLP特征、CacheDit基于残差误差动态复用缓存，减少重复计算。量化技术通过PTQ（如Bitsandbytes即时量化、SVDQuant分解低秩与残差量化、GGUF的K-Quant优化）和Q...]]></description><content:encoded><![CDATA[<h2 id="扩散模型生成加速策略">扩散模型生成加速策略</h2><p>Diffusion推理加速的方案，主要包括Cache、量化、分布式推理、采样器优化和蒸馏等。下面内容主要是去对Cache、计算加速框架以及量化技术进行介绍</p><blockquote><p>SD模型加速方式：<a href="https://github.com/xlite-dev/Awesome-DiT-Inference?tab=readme-ov-file#Quantization">https://github.com/xlite-dev/Awesome-DiT-Inference?tab=readme-ov-file#Quantization</a></p></blockquote><p>不过值得注意的是对于下面内容，首先介绍加速框架（这部分内容主要是介绍进行加速的一些小trick，主要是直接通过api去加速）、cache以及量化一般就会涉及到一些算法的基本原理。所有的测试代码：<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelCode/DF_acceralate/df_acceralate.ipynb">df_acceralate.ipynb</a></p><h3 id="一般加速框架以及显存优化措施">一般加速框架以及显存优化措施</h3><blockquote><p>下面介绍的 <code>flash-attn</code>、<code>torch.compile</code>属于通用的加速策略（llm、扩散模型都可使用）</p></blockquote><p>这部分内容的话比较杂（直接总结<a href="https://huggingface.co/docs/diffusers/optimization/fp16#scaled-dot-product-attention">huggingface</a>内容），1、<strong>直接使用attn计算加速后端</strong>，比如说一般就是直接使用比如说<code>flash_attn</code>进行attention计算加速，比如说：</p><pre><code class="language-python">pipeline.transformer.set_attention_backend("_flash_3_hub") # 启用flash attn计算加速
pipeline.transformer.reset_attention_backend()             # 关闭flash attn计算加速
</code></pre><p>不过值得注意的是<code>_flash_3_hub</code> 只支持非hopper架构，因此可以直接就使用<code>set_attention_backend("flash")</code>。2、<strong>直接使用</strong><code>torch.compile</code>进行加速（对于 compile 原理解释: pytorch使用-1：<a href="https://www.big-yellow-j.top/posts/2025/07/01/torch_basic.html">🔥Pytorch使用-1：Pytorch计算图概念</a>），不过值得注意的是<strong>在开始使用过程中会比较慢</strong>，因为在执行时，它会将模型编译为优化的内核，所以相对会比较慢，但是如果对编译后模型进行批量测试在时间上就会有所提升比如说在代码<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelCode/DF_acceralate/df_acceralate.ipynb">df_acceralate.ipynb</a>中测试结果使用compile在z-image上生成5张图片耗时：86.49s（<strong>平均生图时间</strong>4s）不使用compile：29.92（<strong>平均生图时间</strong>5s）；3、使用<code>torch.channels_last</code>去优化数据结构（<a href="https://docs.pytorch.org/tutorials/intermediate/memory_format_tutorial.html#performance-gains">torch文档</a>）：最主要的一点是通过channel_last让 GPU 在计算卷积 / attention 时，内存访问更连续，比如说一般数据的输入是NCHW那么在内存访问中格式是：<code>N0C0H0W0, N0C0H0W1, ..., N0C0H1W0, ...</code>这个里面通道C变化最慢，使用channel_list数据格式变为NHWC在内存中访问顺序是：<code>N0H0W0C0, N0H0W0C1, N0H0W0C2, ...</code>值得注意的是两部分数据在shape上是一致的只是strid不一致。使用方式也比较简单：</p><pre><code class="language-python"># 修改模型
model = model.to(memory_format=torch.channels_last)
# 修改输入
input = input.to(memory_format=torch.channels_last)
output = model(input)
...
pipeline.unet.to(memory_format=torch.channels_last)
</code></pre><h4 id="1xformers加速">1、xFormers加速</h4><blockquote><p>项目地址：<a href="https://github.com/facebookresearch/xformers">https://github.com/facebookresearch/xformers</a></p></blockquote><p>在SD模型中对于xformers基本使用方式如下所示：</p><pre><code class="language-python">import torch
from diffusers import StableDiffusionXLPipeline

pipeline = StableDiffusionXLPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
).to("cuda")
# 使用xformer加速
pipeline.enable_xformers_memory_efficient_attention()
# 关闭xformer加速
pipeline.disable_xformers_memory_efficient_attention()
</code></pre><p>xformers作用在于<strong>加速attention计算并降低显存</strong>，除此之外还提供了多种注意力实现方式，如casual attention等。根据<a href="https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.fmha.cutlass.FwOp">官方文档</a>中的描述，对于对于<code>xformers.ops.memory_efficient_attention</code>在使用上参数主要是：1、输入数据也就是QKV的格式上必须满足为：<code>[B, M, H, K]</code>分别表示的是其中B 为batch size, N为序列长度, num_heads为多头注意力头的个数, dim_head则为每个头对应的embeding size；2、attn_bias实际上充当为在使用mask attention时的mask；3、p也就是dropout对应值；4、op为Tuple，用于指定优化self-attention计算所采用的算子。基本使用方式如下：</p><pre><code class="language-python">import xformers.ops as xops
y = xops.memory_efficient_attention(q, k, v)
y = xops.memory_efficient_attention(q, k, v, p=0.2) # 使用dropout
y = xops.memory_efficient_attention(
    q, k, v,
    attn_bias=xops.LowerTriangularMask()
)# 使用casual 注意力
</code></pre><p>值得着重了解的就是其中<code>attn_bias</code>参数，简单直观的理解：用于控制注意力可见性和结构的统一接口，<strong>既可以表示 mask，也可以表示稀疏/局部/因果等高级注意力模式</strong>，并且以高性能方式融入 attention 内核。比如说：<br/> 1、<code>xops.LowerTriangularMask()</code>：常规的causal注意力也就是下三角mask<br/> 2、<code>xops.LocalAttentionFromBottomRightMask</code>：局部注意力，每个token只能看最近的window_size个token</p><h4 id="2显存优化">2、显存优化</h4><blockquote><p>这部分内容直接总结：<a href="https://huggingface.co/docs/diffusers/en/optimization/memory?device-map=pipeline+level#reduce-memory-usage">https://huggingface.co/docs/diffusers/en/optimization/memory?device-map=pipeline+level#reduce-memory-usage</a></p></blockquote><p>对于模型的显存过大可以考虑根据自身的设备进行分配，比如说将模型卸载到CPU或者将VAE等放到其它显卡上，在diffusers就提供了这些方法（这块内容直接问AI进行总结）：<br/><strong>1、CPU卸载</strong><br/> 它启用了一种极致级别的逐层（leaf-level / sequential）CPU offloading机制，核心思路是：把模型的计算图中<strong>最底层的参数（leaf modules，即最细粒度的子模块、层或权重块）默认放在 CPU 内存里存储</strong>。在前向传播（forward pass）过程中，只在真正需要计算某个具体层的时候，才把那一小块参数临时从 CPU 拷贝（onload）到 GPU。计算完这层之后，立刻把这块参数再 offload 回 CPU，释放 GPU 显存。然后再加载下一层，以此类推，一层一层顺序执行（sequential）。<br/><img src="https://s2.loli.net/2026/01/15/UWczbBg9x41OI7G.webp" alt="image" width="807" height="497" loading="lazy" decoding="async"/><br/><strong>2、设备分配</strong><br/> 这部分主要是将生成模型中不同模型结构如VAE、CLIP去分配到其它显卡上：</p><pre><code class="language-python">import torch
from diffusers import AutoModel, StableDiffusionXLPipeline
pipeline = StableDiffusionXLPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
    device_map="balanced" # 使用balance就可以实现不同设备分配
)
print(pipeline.hf_device_map)
{'unet': 1, 'vae': 1, 'safety_checker': 0, 'text_encoder': 0}
</code></pre><p>亦或者直接自己定分配：</p><pre><code class="language-python">import torch
from diffusers import AutoModel
device_map = {
    'pos_embed': 0, 'time_text_embed': 0, 'context_embedder': 0, 'x_embedder': 0, 'transformer_blocks': 0, 'single_transformer_blocks.0': 0, 'single_transformer_blocks.1': 0, 'single_transformer_blocks.2': 0, 'single_transformer_blocks.3': 0, 'single_transformer_blocks.4': 0, 'single_transformer_blocks.5': 0, 'single_transformer_blocks.6': 0, 'single_transformer_blocks.7': 0, 'single_transformer_blocks.8': 0, 'single_transformer_blocks.9': 0, 'single_transformer_blocks.10': 1, 'single_transformer_blocks.11': 1, 'single_transformer_blocks.12': 1, 'single_transformer_blocks.13': 1, 'single_transformer_blocks.14': 1, 'single_transformer_blocks.15': 1, 'single_transformer_blocks.16': 1, 'single_transformer_blocks.17': 1, 'single_transformer_blocks.18': 1, 'single_transformer_blocks.19': 1, 'single_transformer_blocks.20': 1, 'single_transformer_blocks.21': 'cpu', 'single_transformer_blocks.22': 'cpu', 'single_transformer_blocks.23': 'cpu', 'single_transformer_blocks.24': 'cpu', 'single_transformer_blocks.25': 'cpu', 'single_transformer_blocks.26': 'cpu', 'single_transformer_blocks.27': 'cpu', 'single_transformer_blocks.28': 'cpu', 'single_transformer_blocks.29': 'cpu', 'single_transformer_blocks.30': 'cpu', 'single_transformer_blocks.31': 'cpu', 'single_transformer_blocks.32': 'cpu', 'single_transformer_blocks.33': 'cpu', 'single_transformer_blocks.34': 'cpu', 'single_transformer_blocks.35': 'cpu', 'single_transformer_blocks.36': 'cpu', 'single_transformer_blocks.37': 'cpu', 'norm_out': 'cpu', 'proj_out': 'cpu'
}
transformer = AutoModel.from_pretrained(
    "black-forest-labs/FLUX.1-dev", 
    subfolder="transformer",
    device_map=device_map,
    torch_dtype=torch.bfloat16
)
</code></pre><h3 id="cache策略概述">cache策略概述</h3><p>cache指的是：<strong>缓存通过存储和重用不同层（例如注意力层和前馈层）的中间输出来加速推理，而不是在每个推理步骤执行整个计算</strong>。它以更多内存为代价显着提高了生成速度，并且不需要额外的训练。主要详细介绍两种：1、DeepCache；2、FORA。对于更加多的cache策略可以看<a href="https://zhuanlan.zhihu.com/p/711223667">知乎</a>，<strong>推荐直接使用</strong><a href="#cachedit">CacheDit</a>来进行加速。</p><h4 id="deepcache策略">DeepCache策略</h4><blockquote><p>Paper:<a href="https://arxiv.org/pdf/2312.00858">https://arxiv.org/pdf/2312.00858</a><br/> Code:<a href="https://link.zhihu.com/?target=https%3A//github.com/horseee/DeepCache">https://link.zhihu.com/?target=https%3A//github.com/horseee/DeepCache</a></p></blockquote><p><strong>主要针对UNet架构</strong>的Diffusion模型进行推理加速。DeepCache 是一种Training-free的扩散模型加速算法，核心思想是<strong>利用扩散模型序列去噪步骤中固有的时间冗余来减少计算开销</strong>。<br/><img src="https://s2.loli.net/2026/01/14/27yEAxsmGB53rLI.webp" alt="image" width="1205" height="536" loading="lazy" decoding="async"/><br/> 基于 U-Net 结构特性，发现相邻去噪步骤的高层特征具有显著时间一致性（Adjacent steps in the denoising process exhibit significant temporal similarity in high-level features.），比如说上图中作者在测试上采用block $U_2$的特征和其它所有的采样步之间相似性计算（图b），因此缓存这些高层特征并仅以低成本更新低层特征，从而避免重复计算。具体方法为：<br/><img src="https://s2.loli.net/2026/01/14/H1TMdUPVFN7QtAa.webp" alt="image" width="597" height="479" loading="lazy" decoding="async"/><br/> 比如说在官方的使用中有参数：<code>helper.set_params(cache_interval=3,cache_branch_id=0,)</code>表示是每3个时间步进行一次完成forward然后刷新cache，而其中参数cache_branch_id值得是一般而言在UNet中会定义<code>branch 0 → early / down blocks</code>等就是选择哪些层的输出。具体过程如下：t=1进行计算缓存，t=2,3都直接使用缓存，t=4完整计算得到缓存。</p><h4 id="fora">FORA</h4><blockquote><p>Paper: <a href="https://arxiv.org/pdf/2407.01425">https://arxiv.org/pdf/2407.01425</a><br/> Code: <a href="https://github.com/prathebaselva/FORA">https://github.com/prathebaselva/FORA</a></p></blockquote><p><strong>主要是争对Dit架构</strong>的Diffusion模型进行推理加速。利用 Diffusion Transformer 扩散过程的重复特性实现了可用于DiT的Training-free的Cache加速算法。<br/><img src="https://s2.loli.net/2026/01/14/S1UFewKTnOLhDV4.webp" alt="image" width="931" height="580" loading="lazy" decoding="async"/><br/> FORA的核心在于发现Dit在去噪过程中，<strong>相邻时间步的Attn和MLP层特征存在显著重复性</strong>（如上图所示:在layer0、9、18、27这些层以及250步采样中，随后采样步约往后特征之间相似性也就越高。）。通过Caching特征，FORA 将这些重复计算的中间特征保存并在后续时间步直接复用，避免逐步重新计算。<br/><img src="https://s2.loli.net/2026/01/15/pmEKdAQchbPrnxH.webp" alt="image" width="963" height="404" loading="lazy" decoding="async"/><br/> 具体而言，模型以固定间隔 N 重新计算并缓存特征：当时间步 t 满足 t mod N=0 时，更新所有层的缓存；在后续 N-1 步中，直接检索cached的 Attn 和 MLP 特征，跳过重复计算。这种策略利用了 DiT 架构在邻近时间时间步的特征相似性，在不修改DiT模型结构的前提下实现加速。例如，在 250 步 DDIM 采样中，当 N=3 时，模型仅需在第 3、6、9… 步重新计算特征，其余步骤复用Cache，使计算量减少约 2/3。实验表明，FORA对后期去噪阶段的特征相似性利用更为高效，此时特征变化缓慢，缓存复用的性价比最高。</p><h4 id="fbcache">FBCache</h4><blockquote><p>项目地址：<a href="https://github.com/chengzeyi/ParaAttention/blob/main/doc/fastest_flux.md">https://github.com/chengzeyi/ParaAttention/blob/main/doc/fastest_flux.md</a></p></blockquote><p>通过缓存变换器模型中变换器块的输出，并在下一步推理中重新使用它们，可以降低计算成本，加快推理速度。然而，很难决定何时重新使用缓存以确保生成图像的质量。最近，TeaCache 提出，可以使用时间步嵌入来近似模型输出之间的差异。AdaCache 也表明，在多个图像和视频 DiT 基线中，<strong>缓存可以在不牺牲生成质量的情况下显著提高推理速度</strong>。不过，TeaCache 仍然有点复杂，因为它需要重新缩放策略来确保缓存的准确性。在 ParaAttention 中，<strong>发现可以直接使用第一个transformer输出的残差来近似模型输出之间的差异。当差值足够小时，我们可以重复使用之前推理步骤的残差</strong>，这意味着我们实际上跳过了去噪步骤。我们的实验证明了这一方法的有效性，我们可以在 FLUX.1-dev 推理上实现高达 1.5 倍的速度，而且质量非常好<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。<br/> 简单来说就是上面提到的DeepCache/FORA在使用上太粗糙直接通过固定时间步去cache缓存这样忽视输出差异的非均匀性，因此后续的TeaCache发现模型输入与输出的强相关性，通过Timestep Emebdding（输入）来估计输出差异。而后FBCache又做了新的改进：<br/><img src="https://s2.loli.net/2026/01/14/EJNoQIHsiRjTyW4.webp" alt="image" width="1125" height="481" loading="lazy" decoding="async"/><br/> 利用residual cache实现了一个基于First Block L1误差的Cache方案，误差小于指定阈值，就跳过当前步计算，复用residual cache，对当前步的输出进行估计。</p><h4 id="cachedit">CacheDit</h4><p><a href="https://github.com/vipshop/cache-dit">cache-dit</a>这个框架主要是适用于Dit结构的扩散模型使用，其具体<a href="https://cache-dit.readthedocs.io/en/latest/user_guide/DBCACHE_DESIGN/">模型框架</a>如下：<br/><img src="https://s2.loli.net/2026/01/14/mU9YHvENodt8Z1B.webp" alt="image" width="1149" height="559" loading="lazy" decoding="async"/><br/> 对于上述框架首先了解CacheDit中几个概念：1、<code>Fn</code>：表示需要计算前n层transformer block在时间步t计算得到结果；2、<code>Bn</code>:表示进一步的融合后n层transformer block的信息去强化预测准确性。其中n=1时候就是FBCache。<br/> 因此对于CacheDit具体过程为：<strong>在t-1步时候</strong>，前n块block去计算他们的结果得到输出结果hidden state并且写入缓存中$C_{t-1}$，而后后几层进行完整结算。<strong>在t步时候</strong>，前n块block不完整计算，而是直接复用/近似 t-1 步的缓存$C_{t-1}$得到近似的结果，计算近似结果和缓存结果中差异（L1 范数），如果差异小于阈值直接复用缓存输入到后续的块中计算，反之就重新计算这n块结果。<br/> 其中具体使用如下：<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelCode/DF_acceralate/df_acceralate.ipynb">df_acceralate.ipynb</a></p><blockquote><p>简单总结上面过程就是，通过在前几个block（或第一个block）计算一个probe残差，然后与上一步缓存的残差做L1（或相对L1）差异比较，来决定当前步是否可以复用/跳过后续block的完整计算，从而实现加速。</p></blockquote><h3 id="扩散模型生成加速总结">扩散模型生成加速总结</h3><p>本文主要是介绍一些在SD模型中加快生图的策略，1、直接使用加速框架进行优化，比如说指定attention计算后端方式、通过<code>torch.compile</code>进行编译、使用<code>torch.channels_last</code>去优化内存访问方式等；2、cache策略，发现在生成过程中在某些层/时间布之间图像的特征比较相似，因此就可以考虑将这些计算结果进行缓存在后续n步中直接加载缓存好的特征来实现生成加速，主要介绍框架是<code>cache-dit</code>；3、量化技术概述。最后简单对比一下生成加速时间</p><blockquote><p>测试模型为<code>Tongyi-MAI/Z-Image-Turbo</code>测试prompt: <code>超写实亚洲中年男性，年龄约45-55岁。面容坚毅、憔悴，带有生活阅历的痕迹（如眼角的细纹）。他穿着质感柔软的深灰色高领毛衣，外搭一件经典的卡其色风衣，站在寒风中周围是高楼大厦</code><br/> 从测试结果上图像的差异还是不大，时间的话从5.97–&gt;5.48（<strong>不一定严谨！</strong>）还是有效的</p></blockquote><table><thead><tr><th style="text-align: center">正常生图</th><th style="text-align: center">+使用channel+ flash_attn</th><th style="text-align: center">+使用cachedit</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2026/01/14/Rdyl2sOowVTaNH3.webp" alt="image" width="843" height="846" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2026/01/14/aryogiJQOuW5FIZ.webp" alt="image" width="838" height="834" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2026/01/14/8lwizF6ZVvn45sW.webp" alt="image" width="838" height="835" loading="lazy" decoding="async"/></td></tr><tr><td style="text-align: center"><code>5.97</code></td><td style="text-align: center"><code>5.67</code></td><td style="text-align: center"><code>5.48</code></td></tr></tbody></table><h2 id="通用加速策略">通用加速策略</h2><p>指的是在扩散模型、LLM中都可以使用道德加速策略，因为在扩散模型生成加速中介绍到了使用 <code>flash-attn</code> 以及 <code>torch.compile</code> 这两部分也属于通用的加速策略因为在扩散模型中介绍了下面不重复介绍。</p><h3 id="基于量化加速策略">基于量化加速策略</h3><p><a href="https://www.big-yellow-j.top/posts/2025/10/11/Quantized.html">量化技术</a>是一种模型压缩的常见方法，将模型权重从高精度（如FP16或FP32）量化为低比特位（如INT8、INT4）去实现<strong>降低显存+生成加速</strong>。量化过程的基本范式，量化过程：$Q=\frac{W}{S}$ 其中 $S$ 表示scale，反量化过程：$\hat{w}=QS$，因此对于量化只需要保存：1、量化后权重；2、scale值（不同量化模型计算方式不同）。比如说（对称量化过程）对于：<code>1.21, -1.13, 0.22, 0.83, 2.11, -1.53, 0.79, -0.54, 0.84</code>其中最大值为2.11那么可以计算出缩放系数为：$\frac{2.11}{127}=0.01661417$（127代表int8数值范围-127，127）那么可以对数据缩放（量化）得到：<code>72, -69, 13, 49, 127, -93, 47, -33, 50</code>反量化可以得到：<code>1.19622024,....</code>（直接乘scale即可）具体计算数字之间差异，都是存在误差的。</p><p>常见的量化策略可以分为PTQ和QAT两大类。<strong>量化感知训练</strong>（QAT）：在<strong>模型训练过程中进行量化</strong>，一般效果会更好一些，但需要额外训练数据和大量计算资源比如说qlora（对模型权重NF4冻结，前向传播过程将NF4权重反量化到BF16计算完毕后丢弃，而Lora则是使用BF16进行训练）。<strong>后量化（PTQ）</strong>：在<strong>模型训练完成后，对模型进行量化</strong>，无需重新训练。对于线性量化下，浮点数与定点数之间的转换公式如下：$Q=\frac{R}{S}+Z;R=(Q-Z)*S$，其中R 表示量化前的浮点数、Q 表示量化后的定点数、S（Scale）表示缩放因子的数值、Z（Zero）表示零点的数值。除此之外</p><p>比如说在LLM中常用的两种<strong>后量化技术</strong>（具体介绍：<a href="https://www.big-yellow-j.top/posts/2025/10/11/Quantized.html">模型量化操作————GPTQ和AWQ量化</a>）：1、<strong>GPTQ量化技术</strong>：通过量化——补偿——量化迭代方法，首先量化$W_{:,j}$，而后去计算误差并且补充到 $W_{:,j:(i+B)}$而后进行迭代实现所有参数的量化；2、<strong>AWQ量化技术</strong>：模型计算过程中只有关键参数起作用因此对于关键参数保持原来的精度(FP16)，对其他权重进行低比特量化，但是这样不同进度参数会导致硬件问题，因此在AWQ中<strong>对所有权重均进行低比特量化，但是，在量化时，对于显著权重乘以较大的scale，相当于降低其量化误差；同时，对于非显著权重，乘以较小的scale，相当于给予更少的关注。</strong></p><blockquote><p>补充一个小知识，一般量化看到比较多就是W4A4这个一般指的就是权重和激活的4bit量化，其中权重一般就是<strong>对应该层的模型权重</strong>，激活就是<strong>对应该层的输入</strong></p></blockquote><p>还会听到几个概念：<strong>1、非对称量化</strong>：是一种用于将浮点数转换为整数表示的量化方法。与对称量化不同的是，这种方法在数据具有偏移（即非对称分布）时更有效，因为它可以减少量化误差。非对称量化会分别找出浮点数的最小值和最大值，分别量化到目标整数范围的最小值和最大值，充分利用量化后的整数范围。这可以使用一个缩放因子（scale）和偏移量（zero-point）来实现<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup>。<br/> 对于<strong>非对称量化</strong>在<em>量化过程</em>中首先计算量化值： <strong>scale</strong> 简单计算过程为：$\text{scale}=\frac{x_{\max}- x_{\min}}{q_{\max}- q_{\min}}$ 而后去计算零点（zero_point）：$\text{zero_point}=\text{round}(q_{\min}-\frac{x_{\min}}{\text{scale}})$，在计算得到两部分量化值之后直接计算：$\text{quant_value}= \text{round}(\frac{fp16_value}{\text{scale}}+zero_point)$，<em>返量化</em> 就直接对上量化公式返计算即可。</p><p><strong>2、对称量化</strong>：对称量化的核心思想是将浮点数量化为整数，且量化后的分布是关于零对称的，对称量化过程中零点呗固定一次只需要去计算缩放因子（scale）：$\text{scale}=\frac{\max(\vert x_{\min} \vert, \vert x_{\max} \vert) }{(q_{\max}-q_{\min})/2}$</p><h4 id="bitsandbytes-量化">bitsandbytes 量化</h4><p>通过使用 <strong>bitsandbytes量化</strong> 来实现8-bit（int8）或者4-bit（int4、Qlora中一般就会使用）量化，不过区别上面提到的AWQ以及GPTQ量化，bitsandbytes不需要对模型进行训练（AWQ、GPTQ可能需要输入数据然后计算误差进行量化），前者需要通过数据来保证量化精度（量化过程是离线、一次性过程），后者量化过程是即时的可逆的。<strong>其技术原理如下</strong>（以对称量化过程为例）：$w≈s q$ 其中w表示原始的FP16权重，q代表int4/int8权重，s缩放因子，其量化过程为对每一个block权重计算：$\max(\text{abs}(w))$ 而后去计算scale：$s=\frac{amx(| w|)}{2^{b-1}-1}$ 而后代入公式就可以得到量化后权重，不过值得注意的是，在使用 Bitsandbytes 进行量化过程中对于上述提到参数计算过程：<strong>将权重 tensor 按固定块大小分割（默认 block_size=64 或 128 元素一块），每个块独立计算量化参数</strong>，在推理过程中进行：反量化 + 矩阵乘法融合在一个 CUDA kernel 中完成：$Y=X(sq)$。因此对于其使用也很简单，比如说在代码中：<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelCode/DF_acceralate/cache_acceralate.py">cache_acceralate.py</a></p><pre><code class="language-python"># 在ZImagePipeline中参数为：
class ZImagePipeline(DiffusionPipeline, ZImageLoraLoaderMixin, FromSingleFileMixin):
    def __init__(,..,vae, text_encoder, tokenizr, transformer):
        ...
# 因此可以直接对里面的text_encoder使用量化处理

from diffusers import BitsAndBytesConfig as DiffusersBitsAndBytesConfig
quantization_config = DiffusersBitsAndBytesConfig(
    load_in_4bit=True,# 在模型加载阶段，将权重以 4-bit 量化形式加载
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,# 指定 反量化后参与计算的 dtype
    bnb_4bit_use_double_quant=True,#启用 Double Quantization（双重量化），也就是对block的scale在进行一次量化
    llm_int8_skip_modules=["transformer_blocks.0.img_mod"],# 指定 不参与 bitsandbytes 量化的模块
)
transformer = AutoModel.from_pretrained(
    model_name,
    cache_dir=cache_dir,
    subfolder="transformer",
    quantization_config=quantization_config,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    mirror='https://hf-mirror.com'
)
</code></pre><p>对你的<code>model_name</code>里面的transformer进行量化处理，除此之外还有使用例子就是进行优化器量化，比如说</p><pre><code class="language-python"># 和使用adamw方式一样，使用qlora使用一般带上这个优化器
import bitsandbytes as bnb
optimizer_class = bnb.optim.AdamW8bit
</code></pre><p><strong>量化支持</strong>，在bitsandbytes量化中主要<a href="https://huggingface.co/docs/transformers/quantization/bitsandbytes">支持两种量化精度</a>：int4（主要是用来qlora训练）和int8（主要是用来推理）对于两种量化方式代码使用上：</p><pre><code class="language-python">from transformers import BitsAndBytesConfig
# 8 bit 量化
quantization_config = BitsAndBytesConfig(load_in_8bit=True)

# 4 bit 量化
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # 4bit 量化数据类型
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True
)
</code></pre><h4 id="torchao-量化">torchao 量化</h4><p>上述过程中提到的bitsandbytes量化，在pytorch中支持原生的量化过程tochao（支持llm、扩散模型，并且在使用上区别 bitsandbytes 更加精细）在使用上也比较简单，只需要对上面 Bitsandbytes 量化代码中将 <code>quantization_config</code> 改成torchao所对应的量化即可，比如说：</p><pre><code class="language-python">from transformers import TorchAoConfig
from torchao.quantization import Float8DynamicActivationFloat8WeightConfig, quantize_
quant_type = Float8DynamicActivationFloat8WeightConfig(activation_dtype=torch.float8_e4m3fn,weight_dtype=torch.float8_e4m3fn)
quantization_config = TorchAoConfig(quant_type= quant_type,)
model = AutoModel.from_pretrained(...,quantization_config=quantization_config)

# 除此之外对于自己的模型量化直接使用：
quantize_(model, Float8DynamicActivationFloat8WeightConfig(granularity=PerTensor()))
</code></pre><p>然后将 <code>from_pretrained</code> 里面的 <code>quantization_config</code> 修改即可，在 torchao 中支持的<a href="https://docs.pytorch.org/ao/stable/api_reference/api_ref_quantization.html#torchao-quantization">量化方式</a>有：</p><p><img src="https://files.seeusercontent.com/2026/05/07/yj4C/image20260414155418919.webp" alt=""/></p><p>而对于 <strong>TorchAO 的量化核心</strong>基于仿射量化和 分组/通道级粒度（值得是在量化过程中对于 “数据选择”决定“多少个权重/激活值共享同一个缩放因子”的粗细程度 比如说 bitsandbytes 量化过程中直接是选择 block_size 而在 torchao 中支持 block_size/granularity=PerTensor()/granularity=PerRow()），底层依赖自定义内核（CUDA/XPU/CPU）与 inductor 编译器融合。除去上述量化方式 torchao 支持QAT（量化感知训练），根据<a href="https://docs.pytorch.org/ao/stable/eager_tutorials/finetuning.html">官方</a>描述：</p><p><img src="https://files.seeusercontent.com/2026/05/07/Teg2/image20260414191418227.webp" alt=""/></p><p>TorchAO的QAT支持包含两个独立步骤：prepare 和 converted。准备步骤“假”是在训练过程中量化激活和/或权重，这意味着高精度值（例如bf16）会映射到对应的量化值，但实际上不会将其投射到目标的低精度d类型（例如int4）。训练后应用的转换步骤，将模型中的“假”量化操作替换为执行dtype铸造的“真实”量化。比如说下面代码：</p><pre><code class="language-python">from torchao.quantization import quantize_
from torchao.quantization.qat import QATConfig, IntxFakeQuantizeConfig

class SimpleModule(nn.Module):
    def __init__(self, input_dim, output_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.Linear(128, output_dim)
        )
    def forward(self, x):
        return self.fc(x)

activation_config = IntxFakeQuantizeConfig(torch.int8, "per_token", is_symmetric=False,)
weight_config = IntxFakeQuantizeConfig(torch.int4, group_size=32, is_symmetric=True,)
qat_config = QATConfig(activation_config=activation_config,weight_config=weight_config,step="prepare",)
quantize_(simple_model, qat_config)
...
base_config = Int4WeightOnlyConfig(group_size=32)
quantize_(compile_simple_model, QATConfig(base_config, step="convert"))
</code></pre><p>我的模型权重变化过程如下：</p><pre><code># 最开始模型层为
(0): Linear(in_features=32, out_features=1024, bias=True)
# prepare 处理得到模型为
(0): FakeQuantizedLinear(
      in_features=32, out_features=1024, bias=True
      (activation_fake_quantizer): FakeQuantizer(IntxFakeQuantizeConfig(dtype=torch.int8, granularity=PerToken(), mapping_type=&lt;MappingType.ASYMMETRIC: 3&gt;, scale_precision=torch.float32, zero_point_precision=torch.int32, zero_point_domain=&lt;ZeroPointDomain.INT: 1&gt;, is_dynamic=True, range_learning=False, eps=None))
      (weight_fake_quantizer): FakeQuantizer(IntxFakeQuantizeConfig(dtype=torch.int4, granularity=PerGroup(group_size=32), mapping_type=&lt;MappingType.SYMMETRIC: 1&gt;, scale_precision=torch.float32, zero_point_precision=torch.int32, zero_point_domain=&lt;ZeroPointDomain.INT: 1&gt;, is_dynamic=True, range_learning=False, eps=None))
    )
# convert 处理得到模型为
(0): Linear(in_features=32, out_features=1024, bias=True, weight=Int4Tensor(shape=torch.Size([1024, 32]), block_size=[1, 32], device=cuda:0, activation_dtype=torch.bfloat16))
</code></pre><p>从上面模型变化过程可以看到，在 prepare 阶段，会将模型进行fake包裹，而后通过训练得到量化的模型权重，再通过 convert 处理，将模型中的权重和激活值都进行了量化</p><h4 id="gguf">GGUF</h4><blockquote><p>HF文档：<a href="https://huggingface.co/docs/hub/en/gguf">https://huggingface.co/docs/hub/en/gguf</a><br/><a href="https://unsloth.ai/docs/basics/inference-and-deployment/saving-to-gguf">https://unsloth.ai/docs/basics/inference-and-deployment/saving-to-gguf</a></p></blockquote><p>GGUF格式是用于存储大型模型预训练结果的，相较于Hugging Face和torch的bin文件，它采用了紧凑的二进制编码格式、优化的数据结构以及内存映射等技术，提供了更高效的数据存储和访问方式。GGUF 本身支持多种量化级别（Q2_K ~ Q8_0、IQ2 ~ IQ4 等），这些量化方式属于后训练量化（PTQ），和bitsandbytes 4bit 一样，都是在预训练模型上直接执行量化（不需要重新训练）。在GGUF中可以实现量化方式有两类：</p><table><thead><tr><th style="text-align: center"><strong>传统Q系列</strong>（按照权重逐层量化）</th><th style="text-align: center"><strong>K-Quant系列</strong>（通过 block-wise + scale 优化）</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2026/01/16/rNKS8R6Z3mhidX1.webp" alt="image" width="726" height="387" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2026/01/16/dLECSrbH51Mozuh.webp" alt="image" width="732" height="264" loading="lazy" decoding="async"/></td></tr></tbody></table><p>其中<strong>传统Q系列</strong>主要是一整块权重共享一个 scale（缩放因子），每个权重用低 bit 整数表示，容易受到极端值的影响。 <strong>K-Quant系列</strong>一个 block 内，再分“子块”，每个子块有自己的 scale，其中S代表子块少、scale少；M代表子块多、scale多。</p><h4 id="svdquant量化">SVDQuant量化</h4><blockquote><p><a href="https://github.com/nunchaku-ai/nunchaku">https://github.com/nunchaku-ai/nunchaku</a></p></blockquote><p>在扩散模型中，权重（Weights）和激活（Activations）往往包含大量异常值（极端大或小的值），这些值在低位量化（如4-bit INT4）时会引起严重误差，导致生成的图像失真或噪声增多。<br/><img src="https://s2.loli.net/2026/01/15/IGADqfWUCstnc1k.webp" alt="image" width="862" height="182" loading="lazy" decoding="async"/></p><blockquote><p>a：权重和激活值中都存在异常值，b：将激活值的异常值移动到权重中，c：将权重进行分解低秩的$L_1L_2$以及残差<br/> 其中对于b过程就是常见的SmoothQuant<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">3</a></sup>量化过程</p></blockquote><p>因此对于SVDQuant过程描述如下，对于权重和激活值：$\mathbf{W}$ 以及 $\mathbf{X}$，在最初这两部分值都是存在大量异常值，因此首先通过<em>平滑操作</em>（主要是让减少异常值让量化不会造成太大误差）将激活$\mathbf{X}$中的异常值迁移到权重 $\mathbf{X}$中得到更新后权重 $\hat{\mathbf{W}}$，这部分数据表述为：$\hat{\mathbf{W}}=\mathbf{W}\odot S$其中S是平滑因子，用于转移异常（⊙表示逐元素乘法）。这部分操作主要是因为：<strong>将异常值集中到权重侧，因为权重是静态的，更容易后续处理。激活侧的异常值减少后，量化难度降低</strong>。</p><p>而后，进行SVD分解与低秩吸收，对更新后权重进行奇异值分解：$ \hat{𝑾} = 𝑼 \Sigma 𝑽^T $, 其中𝑼和𝑽是正交矩阵，$\Sigma$是奇异值对角矩阵。保留前k个最大奇异值（低秩r，通常r « min(m,n)，其中m,n是权重矩阵维度），形成低秩近似：$ 𝑳_1 𝑳_2 = 𝑼[:,:r] \cdot \Sigma[:r,:r] \cdot 𝑽^T[:r,:] $。然后，计算残差：$ 𝑹 = \hat{𝑾} - 𝑳_1 𝑳_2$，其中只对残差$𝑹$进行量化（$Q(𝑹)=\text{round}(\frac{𝑹}{S_𝑹})S_𝑹$，其中$S_𝑹$为缩放因子）处理。低秩分支$ 𝑳_1 𝑳_2 $使用高精度（16-bit float）运行，专门“吸收”异常值和主要信息，而残差𝑹中的异常值和幅度显著减少，只需量化到4-bit。量化误差界限分析（从论文中）：量化误差上界可通过F范数和奇异值控制，证明低秩吸收后残差的量化难度降低（误差 ≤ $ \frac{\sqrt{\log(\text{size}(𝑹)\pi)}}{q_{\max}} \mathbb{E}[|𝑹|_F] $，其中q_max是量化最大值），<strong>最后的整体近似计算</strong>：</p> \[\hat{\mathbf{X}}\hat{\mathbf{W}}=\hat{\mathbf{X}}(R+L_1L_2)≈\hat{\mathbf{X}}(L_1L_2)+Q(\hat{\mathbf{X}})Q(R)\] <blockquote><p>对于低秩分支 $L_1 L_2$可以无损的去拼接其它lora，因为输入 $\hat{X}$ 被平滑了因此直接量化误差不大</p></blockquote><p>第一项是16-bit低秩分解，第二项为4-bit残差分支。<strong>简而言之</strong>上述过程为：首先利用平滑因子 $S$ 将激活值的离群值转移至权重 $\hat{W}$ 中；随后对 $\hat{W}$ 执行奇异值分解（SVD），提取承载异常特征的低秩分支 $L_1 L_2$；最后计算并量化残差矩阵 $R = \hat{W} - L_1 L_2$（因为提取低秩矩阵之后权重就相对平滑，那么对残差量化误差就比较小）其中，残差矩阵为INT4类型，低秩矩阵$L_1 L_2$为FP16。在推理时，权重 $\hat{W}$ 就可以直接变成 $\hat{W} =R+ L_1 L_2$</p><p><img src="https://s2.loli.net/2026/01/15/ou7nqDyeBPlakV2.webp" alt="image" width="868" height="224" loading="lazy" decoding="async"/><br/> 这里简单介绍一下如何使用SVDQuant量化后模型，一般而言不会去主动通过SVDQuant去量化模型（比如说量化Flux）会直接去加载量化后的模型，可以去<a href="https://huggingface.co/nunchaku-ai">huggingface-nunchakus</a>里面找量化后的模型，对于comfyui使用可以直接nunchakus的comfyui去选择自己模型即可，如果<a href="https://nunchaku.tech/docs/nunchaku/usage/lora.html">纯代码</a>使用可以</p><pre><code class="language-python">import torch
from diffusers import FluxPipeline

from nunchaku import NunchakuFluxTransformer2dModel
from nunchaku.lora.flux.compose import compose_lora
from nunchaku.utils import get_precision

precision = get_precision()  # auto-detect your precision is 'int4' or 'fp4' based on your GPU
transformer = NunchakuFluxTransformer2dModel.from_pretrained(
    f"nunchaku-tech/nunchaku-flux.1-dev/svdq-{precision}_r32-flux.1-dev.safetensors"
)
pipeline = FluxPipeline.from_pretrained(
    "black-forest-labs/FLUX.1-dev", transformer=transformer, torch_dtype=torch.bfloat16
).to("cuda")

### LoRA Related Code ###
composed_lora = compose_lora(
    [
        ("aleksa-codes/flux-ghibsky-illustration/lora.safetensors", 1),
        ("alimama-creative/FLUX.1-Turbo-Alpha/diffusion_pytorch_model.safetensors", 1),
    ]
)  # set your lora strengths here when using composed lora
transformer.update_lora_params(composed_lora)
### End of LoRA Related Code ###

image = pipeline(
    "GHIBSKY style, cozy mountain cabin covered in snow, with smoke curling from the chimney and a warm, inviting light spilling through the windows",  # noqa: E501
    num_inference_steps=8,
    guidance_scale=3.5,
).images[0]
image.save(f"flux.1-dev-turbo-ghibsky-{precision}.png")
</code></pre><p>整个过程中还是比较简单的，只不过值得注意的是，假如我的模型是int4量化后的权重，对于量化后的模型权重不能进行lora训练（精度丢失严重）那么直接对fp16的模型进行加载（如果显存不够，<code>from_pretrained</code>过程中可以使用<code>BitsAndBytesConfig</code>进行量化）而后通过Qlora进行微调这样得到的 LoRA 权重是全精度（通常 float16）的，不是量化过的。<strong>在comfyui中</strong>可以直接加载这个lora权重（将lora放到<code>models/loras/</code>中然后使用<code>Nunchaku Flux LoRA Loader</code>进行加载即可）。但是对于其它权重需要<a href="https://nunchaku.tech/docs/nunchaku/python_api/nunchaku.lora.flux.convert.html">进行量化</a>将其转化为nunchakus可以使用权重：</p><pre><code class="language-python">python -m nunchaku.lora.flux.convert \
    --lora-path xx.safetensor \
    --base-model xxx.safetensors \
    --output-dir ./tmp/ \
    --lora-name xx
</code></pre><p>上面几个参数分别表示lora、模型权重。<br/> 对于上述过程<strong>简单总结</strong>如下：首先在量化过程中激活值以及权重都存在异常值，这个异常值会导致后续量化后模型效果较差，因此首先是<strong>将激活值的异常值移动到权重中</strong>，而后将得到权重$\hat{W}$进行分解低秩的$L_1L_2$进而可以计算残差$R$，因此对于模型输出过程：$Y=XW=\hat{X}\hat{W}=\hat{X}(L_1L_2+Q(\hat{W}-L_1L_2))$其中$\hat{X}$表示平滑后的激活值，$L_1L_2$表示16-bit的分解矩阵，$Q(\hat{W}-L_1L_2)$表示4-bit低精度值</p><h3 id="使用vllm">使用vllm</h3><p>在对llm进行推理加速过程中比较常见的就是直接使用vllm进行推理加速，简单介绍一下vllm使用方式，分为两种：<strong>1、在线推理</strong>（而后本地可以直接通过类似Openai调用方式使用）；<strong>2、离线推理</strong>（一般在模型训练比如说GRPOTrainer过程中就会使用去生成）。对于两种方式的使用代码如下：</p><pre><code class="language-python"># 在线推理
"""
启动服务
HF_ENDPOINT=https://hf-mirror.com HF_HUB_CACHE=/root/autodl-tmp/.cache vllm serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 8001 --gpu-memory-utilization 0.5 --max-model-len 16384 --max-num-seqs 256 --trust-remote-code --served-model-name qwen3-0.6B
1、Qwen/Qwen3-0.6B 可以直接改为自己的模型文件夹
"""
llm = OpenAI(base_url="http://127.0.0.1:8001/v1", api_key="EMPTY")
completion = llm.chat.completions.create(...) # 得到输出
# 离线推理

from vllm import LLM, SamplingParams
llm = LLM(model=model_name_path,...)
sampling_params = SamplingParams(...)
outputs = llm.generate([prompt], sampling_params)
</code></pre><p>vllm不仅仅对llm可以起作用，在diffuseion model中也可以起作用，可以直接使用 vllm-omni<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">4</a></sup>进行扩散模型加速推理其内部逻辑为：</p><p><img src="https://files.seeusercontent.com/2026/05/07/zI6b/image20260415161433343.webp" alt=""/></p><p>在对比omni和diffusers在生成图像效果如下，测试<a href="https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/examples/offline_inference/text_to_image/#example-materials">代码</a>：</p><blockquote><p>测试参数为（具体设备为 <code>vGPU-32G</code>）：<code>Klein4B</code>、<code>超写实亚洲中年男性，年龄约45-55岁。面容坚毅、憔悴，带有生活阅历的痕迹（如眼角的细纹）。他穿着质感柔软的深灰色高领毛衣，外搭一件经典的卡其色风衣，站在寒风中周围是高楼大厦</code>、<code>random_seed=416</code>，直接运行10次测试平均省图时间</p></blockquote><table><thead><tr><th style="text-align: center">生成方式</th><th style="text-align: center">正常生图</th><th style="text-align: center">时间</th><th style="text-align: center">cache_dit+ compile</th><th style="text-align: center">时间</th></tr></thead><tbody><tr><td style="text-align: center"><code>diffusers</code></td><td style="text-align: center"><img src="https://files.seeusercontent.com/2026/05/07/siV8/9.webp" alt="9"/></td><td style="text-align: center"><code>2.478</code></td><td style="text-align: center"><img src="https://files.seeusercontent.com/2026/05/07/siV8/9.webp" alt="9"/></td><td style="text-align: center"><code>2.101</code></td></tr><tr><td style="text-align: center"><code>vllm-omni</code></td><td style="text-align: center"><img src="https://files.seeusercontent.com/2026/05/07/t3Ds/omni-n-9.webp" alt="omni-n-9"/></td><td style="text-align: center"><code>2.368</code></td><td style="text-align: center"><img src="https://files.seeusercontent.com/2026/05/07/yqM3/omni-c-9.webp" alt="omni-c-9"/></td><td style="text-align: center"><code>2.143</code></td></tr></tbody></table><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://github.com/chengzeyi/ParaAttention/blob/main/doc/fastest_flux.md">https://github.com/chengzeyi/ParaAttention/blob/main/doc/fastest_flux.md</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://juejin.cn/post/7436976221068148786">https://juejin.cn/post/7436976221068148786</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2211.10438">https://arxiv.org/pdf/2211.10438</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model/">https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model/</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Mon, 29 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html</guid></item><item><title>模型推理框架vllm-1——整体框架及使用</title><link>https://www.big-yellow-j.top/posts/2025/12/27/vllm-1.html</link><description><![CDATA[Page Attention是优化KV-cache内存管理的方法，可解决预留浪费、内部及外部内存碎片化问题，通过将KV-cache划分为固定大小Block，利用Block Table维护逻辑与物理映射，有效管理非连续内存；同时处理Softmax计算中的数值溢出与全局信息问题。vllm框架支持离线与在线调用，初始化过程包括模型加载、KV Cache显存预分配（计算可用内存、num_blocks及总token数）并加载到GPU，提升内存使用效率。]]></description><content:encoded><![CDATA[<h2 id="pageattention原理分析">PageAttention原理分析</h2><p><code>Page Attention</code>也是一种优化方法（区别于<code>MLA</code>，<code>page attention</code>是对内存进行分配管理）。参考论文<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>中描述，对于<code>KV-cache</code>存在3个问题：<br/><img src="https://s2.loli.net/2025/06/21/9QpfhleHvRPxLmW.webp" alt="image" width="1014" height="190" loading="lazy" decoding="async"/><br/> 1、<strong>预留浪费 (Reserved)</strong>：为将来可能的 token 预留的空间，这些空间被保留但暂未使用，其他请求无法使用这些预留空间；<br/> 2、<strong>内部内存碎片化问题（internal memory fragmentation）</strong>：系统会为每个请求预先分配一块连续的内存空间，大小基于最大可能长度(比如2048个token)，但实际请求长度往往远小于最大长度，这导致预分配的内存有大量空间被浪费。<br/> 3、<strong>外部内存碎片化问题（external memory fragmentation）</strong>：不同内存块之间的零散空闲空间，虽然总空闲空间足够，但因不连续而难以使用。<br/><img src="https://s2.loli.net/2025/06/21/ryt7kgaGZSw32HN.webp" alt="image" width="473" height="262" loading="lazy" decoding="async"/><br/> 只有 <strong>20.4%-38.2%</strong> 的token是被使用的，大部分都被浪费掉了。<code>Page Attention</code>允许在非连续的内存空间中存储连续的 key 和 value 。具体来说，<code>Page Attention</code>将每个序列的 <code>KV-cache</code> 划分为块，每个块包含固定数量 token 的键和值。在注意力计算期间，<code>Page Attention</code>内核可以有效地识别和获取这些块。如何理解上面描述呢？还是借用论文中的描述：<br/><img src="https://s2.loli.net/2025/06/21/sZ1uOlYStP3ehDb.webp" alt="image" width="808" height="462" loading="lazy" decoding="async"/><br/> 比如说按照上面Prompt要输出（假设只输出这些内容）：“fathers brought a car”，一般的套路可能是：比如说：“Four score and seven years ago our xxxxx”（xxx代表预留空间）因为实际不知道到底要输出多少文本，因此会提前预留很长的一部分空间（但是如果只输出4个字符，这预留空间就被浪费了），因此在<code>page attention</code>里面就到用一种“分块”的思想处理，以上图为例，分为8个Block每个Block只能存储4个内容，因此就可以通过一个<code>Block Table</code>来建立一个表格告诉那些Block存储了多少，存储满了就去其他Blobk继续存储。整个过程如下：<br/><img src="https://s2.loli.net/2025/02/02/3lWpNMUQyLojhP9.webp" alt="image" width="1200" height="590" loading="lazy" decoding="async"/><br/> 上述过程描述如下：具体而言，Page Attention 首先将 Key/Value 的连续显存空间划分为固定大小的 Block（页），每个 Block 作为最小的内存分配与调度单元。随后，引入一个 Block Table（页表） 来维护逻辑序列位置与物理 Block 之间的映射关系，用于记录每个 Block 当前的存储状态与可用容量。<br/><strong>一个小问题</strong>：分块之后注意力计算过程，因为我的KV被存储在不同的block中，由于Block table存在可以直接去索引不同Blcok中KV值，这样一来对于Q、K、V三者计算不成问题，不过关键问题就是：Softmax 的分母需要全局信息，Block （不管是Flash Attn还是Page Attn都需要面对这个问题）是分开的，怎么办？<br/> 在softmax计算过程中：$\sigma= \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}}$ 由于分块可能导致值过小进而导致数值溢出问题，除此之外计算需要所有token的分数一起归一化，因此首先会对上面的公式改进为：$\sigma= \frac{e^{z_i-m}}{\sum_{j=1}^K e^{z_j-m}}$也就是将每块都去减去当前的最大值（<strong>避免溢出问题</strong>）。<strong>在处理全局问题上</strong>：只需要考虑两个值的更新：1、当前最大值；2、归一化因子（$\sum_{j=1}^K e^{z_j-m}$）因此这个过程就可以处理为：</p> \[l_{t+1}=\sum_{i\in B_{≤t+1}} e^{z_j- m_{t+1}}=\sum_{i\in B_{≤t}}e^{z_j- m_{t+1}}+ \sum_{i\in B_{t+1}}e^{z_j- m_{t+1}}\\ =\sum_{i\in B_{≤t}}e^{z_j- m_{t}} e^{m_t-m_{t+1}}+ \sum_{i\in B_{t+1}}e^{z_j- m_{t+1}}\] <p>这样一来就可以转化为：$l_{t+1}=l_t e^{m_t-m_{t+1}}+ \sum_{i\in B_{t+1}}e^{z_j- m_{t+1}}$</p><h2 id="基本使用方式">基本使用方式</h2><p>在使用vllm上有两种方式：1、离线使用；2、在线使用（直接将使用过程转化为调用API方式）：</p><pre><code class="language-python">from vllm import LLM, SamplingParams
prompts = ["Hello, my name is",
           "The president of the United States is",
           "The capital of France is",
           "The future of AI is",]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
llm = LLM(model="facebook/opt-125m")
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
</code></pre><h2 id="vllm整体框架分析">vllm整体框架分析</h2><blockquote><p>基于：<code>Version: 0.17.0</code></p></blockquote><p>在vllm中主要是两种调用方式：1、离线调用；2、在线调用（这个就类似在本地启动一个服务，而后其他及其直接访问ip端口等进行访问处理）<br/><img src="https://s2.loli.net/2026/01/15/aDyb2iSmqhuPInK.webp" alt="image" width="1166" height="626" loading="lazy" decoding="async"/><br/> 上图中在线调用方式（Asy）和离线调用（Syn）</p><blockquote><p>对于具体的<code>LLMEngine</code>的结构描述见后面的描述</p></blockquote><p>以离线调用方式进行解释，直接使用官方代码为例：</p><pre><code class="language-python">from vllm import LLM, SamplingParams
prompts = ["Hello, my name is",
           "The president of the United States is",
           "The capital of France is",
           "The future of AI is",]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
llm = LLM(model="facebook/opt-125m")
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
</code></pre><p>从上面代码分析发现感觉和平时使用Transformer框架和相似：加载模型–&gt;编码输入–&gt;输入模型–&gt;模型输出并且解码。差异在于使用vllm首先会使用一个<code>LLM</code>去处理你的模型，而后你其他的方式都是在这个<code>LLM</code>中，因此了解一下在模型接受到我的<code>prompt</code>之前模型都在做什么。</p><h3 id="vllm初始化过程">vllm初始化过程</h3><p>按照PPT<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>中对于模型加载的描述：<br/><img src="https://s2.loli.net/2026/01/15/ynDbOIQ5zJxHrtZ.webp" alt="image" width="1161" height="736" loading="lazy" decoding="async"/><br/> 在模型进行输出之前主要是进行3步：1、初始化并且加载模型；2、预分配显存过程；3、将预分配的KV Cache加载到gpu上。</p><h4 id="模型初始化过程">模型初始化过程</h4><p>在vllm中定义一个llm过程为：</p><pre><code class="language-python"># vllm/entrypoints/llm.py
class LLM:
    ...
    self.llm_engine = LLMEngine.from_engine_args(...)

# vllm/v1/engine/llm_engine.py
class LLMEngine:
    def __init__(...):
      self.engine_core = EngineCoreClient.make_client(...)
    def generate(...):
        ...
    def add_request(...):
        ...
# /vllm/v1/engine/core_client.py 中 EngineCoreClient通过多种（异步/多进程，这也就意味这在linux有些可能需要使用`multiprocessing.set_start_method('spawn', force=True)`）方式进行加载模型
</code></pre><p>在<a href="https://github.com/vllm-project/vllm/blob/8711b216766bb5d3cbe15161061c3a7d9fffe59c/vllm/v1/engine/llm_engine.py#L46">LLMEngine代码</a>中定义了基本所有函数功能，如生成等（<strong>后续解释具体过程</strong>）。</p><h4 id="预分配显存过程">预分配显存过程</h4><p>这个给过程的的话首先是去计算预分配的KV Cache大小，而后将预分配的KV Cache加载（一般就是初始化为0的向量）到gpu上</p><ul><li><strong>计算预分配的KV Cache</strong></li></ul><blockquote><p>计算预分配的KV Cache<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>：<strong>可用显存大小×预分配vllm比率- 非kv cache占用大小</strong>得到kv cache的可用（字节）大小，而后通过总共可用大小计算可用分多少个block：<strong>可分配大小//KV cache block 的字节大小//所有 kv_cache_groups中层数的最大值</strong></p></blockquote><p><img src="https://s2.loli.net/2026/01/15/qDBNMLpzWOFkVHG.webp" alt="image" width="950" height="565" loading="lazy" decoding="async"/><br/> 在调用代码<code>LLM(model="facebook/opt-125m")</code>实际过程中会使用<code>load_model</code>进行模型加载（代码：<code>vllm/v1/worker/gpu_model_runner.py</code>）在加载模型之后，模型会进行一个显存的预分配处理，这个过程（代码：<code>vllm/v1/core/kv_cache_utils.py</code>）描述如下：<br/> 1、计算需要分配多少显存给vllm：<em>*可用显存大小</em>初始化分配大小<strong>（<code>self.requested_memory=self.init_snapshot.total_memory * self.cache_config.gpu_memory_utilization</code>，比说24G（实际可能比24G要小，因为还有模型占用）显卡那么的第一项结果就是：24*1024^3，后面一下就是最开始的参数）<br/> 2、计算分配给kv cache的显存占用字节大小：</strong>可以显存大小-除去KV cache显存外其他大小<strong>（<code>self.available_kv_cache_memory_bytes = self.requested_memory - profile_result.non_kv_cache_memory</code>） 在计算完毕之后（以上面模型加载为例，得到KV cache大小为：20.44GiB）接下来就是计算GPU上 KV Cache 内</strong>总token数量<em>*：<code>num_tokens = num_blocks // len(kv_cache_groups) * min_block_size</code><br/><code>num_blocks</code>计算过程：<code>int(available_memory // page_size // num_layers)</code>，其中<code>page_size</code>代表是一个 KV cache block 的字节大小（<code>page_size</code> =(K+V)$\times$ block_size$\times$ num_kv_heads$\times$ head_size$\times$ dtype_bytes= 2 * 16 * 12 * 64</em> 2(其中fp16对应2)=49152，里面num_kv_heads对应你的模型结构使用数量）；<code>num_layers</code>：所有kv_cache_groups中层数的最大值，比如说在模型facebook/opt-125m中总共有12层decode（即 12 层进行注意力计算）并且这些attn计算方式完全相同那么就是1个group分组（如果还有其他attn那么可能就是多个group但是最后还是取最大值：<code>group_size = max(len(group.layer_names) for group in kv_cache_groups)</code>。最后计算得到结果为：num_blocks = 21946158284//49152// 12=37207。<code>min_block_size = min([group.kv_cache_spec.block_size for group in kv_cache_groups])</code> 计算得到:16。</p><blockquote><p>实际调试过程中（直接在需要调试位置使用<code>logger.info</code>），输出<code>kv_cache_groups</code>看到的比如（其实这个参数也就是记录cache需要发生位置，一般就是attn计算，不过可能对于不同的attn存在差异，有些是常规有些有可能是window-attn等）：<code>[KVCacheGroupSpec(layer_names=['model.decoder.layers.0.self_attn.attn', ..., 'model.decoder.layers.11.self_attn.attn'], kv_cache_spec=FullAttentionSpec(block_size=16, num_kv_heads=12, head_size=64, dtype=torch.float16, sliding_window=None, attention_chunk_size=None))]</code> 除此之外这部分结果会直接存入<code>KVCacheConfig</code>中。在后续代码（<code>vllm/v1/worker/gpu_model_runner.py</code>）中对于<code>initialize_kv_cache</code>（具体解释见下面）还会为每一块<code>model.decoder.layers.0.self_attn.attn</code>取分配一个初始化（具体函数：<code>initialize_kv_cache_tensors</code>）为0的向量大小为：[2, num_blocks, block_size, num_kv_heads, head_size]</p></blockquote><p>因此最后就可以直接得到：num_tokens = 37207// 1*16 = 595,312。</p><ul><li><strong>将预分配的KV Cache加载到gpu上</strong></li></ul><p><img src="https://s2.loli.net/2026/01/15/kQK5LmEcjA4VeUJ.webp" alt="image" width="1068" height="575" loading="lazy" decoding="async"/><br/> 在上述步骤中计算得到了预分配的KV cache大小以及num blocks，接下来就是直接将其先放置到gpu上，实现显存的预分配，以后这块显存就是专门用来做KV Cache。具体过程中还是使用上面得到的<code>kv_cache_groups</code>这个参数</p><pre><code class="language-python"># vllm/v1/worker/gpu_model_runner.py
def initialize_kv_cache_tensors(self, kv_cache_config: KVCacheConfig):
    # Initialize the memory buffer for KV cache
    kv_cache_raw_tensors = self._allocate_kv_cache_tensors(kv_cache_config)
    # Change the memory buffer to the desired shape
    kv_caches = self._reshape_kv_cache_tensors(kv_cache_config, kv_cache_raw_tensors)
    ...
    num_attn_module = 2 if self.model_config.hf_config.model_type == "longcat_flash" else 1
    bind_kv_cache(kv_caches,
                  self.compilation_config.static_forward_context,
                  self.kv_caches, num_attn_module)
    return kv_caches

def _allocate_kv_cache_tensors(self, kv_cache_config: KVCacheConfig):
    kv_cache_raw_tensors: dict[str, torch.Tensor] = {}
    logger.info(kv_cache_config)
    for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
        tensor = torch.zeros(kv_cache_tensor.size,
                             dtype=torch.int8,
                             device=self.device)
        for layer_name in kv_cache_tensor.shared_by:
            kv_cache_raw_tensors[layer_name] = tensor
    ...
    return kv_cache_raw_tensors

</code></pre><blockquote><p>对于参数<code>KVCacheConfig</code>就是上面的<code>kv_cache_groups</code>结果，只不过还会取计算每层的大小也就是会更新为：<code>KVCacheConfig(num_blocks=37207, kv_cache_tensors=[KVCacheTensor(size=1828798464, shared_by=['model.decoder.layers.0.self_attn.attn']), ..., KVCacheTensor(size=1828798464, shared_by=['model.decoder.layers.11.self_attn.attn'])], kv_cache_groups=[KVCacheGroupSpec(layer_names=['model.decoder.layers.0.self_attn.attn', ...,'model.decoder.layers.10.self_attn.attn', 'model.decoder.layers.11.self_attn.attn'], kv_cache_spec=FullAttentionSpec(block_size=16, num_kv_heads=12, head_size=64, dtype=torch.float16, sliding_window=None, attention_chunk_size=None))])</code><br/> 对于里面size计算：num_blocks × block_size × num_kv_heads × head_size × dtype_bytes × 2 (K + V)</p></blockquote><p>不过值得注意的是对于上面KVCacheConfig定义了一个获取LLM中所有attn 层的 KV cache 该怎么组织、分组、分配内存的全局蓝图，还是用上面例子，num_blocks表示分配得到显存切分了多少个block，kv_cache_groups表示分层了多少组（如果有sliding-window这些可能又是新的一组），得到KVCache信息之后就可以对应的去初始化一些张量去占用显存，在函数<code>self._allocate_kv_cache_tensors</code>中很容易理解直接初始化一个全部为0的张量，而后再去通过函数<code>_reshape_kv_cache_tensors</code>将张量的形状改为<code>[num_blocks, block_size, num_kv_heads, head_size]</code></p><h2 id="vllm简单使用">vllm简单使用</h2><p>vllm使用主要有两种主流方式：1、在线；2、离线两种对于这两种的代码如下：1、<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/VLLM-SGLang/vllm_base_use.py">离线推理使用</a>；2、<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/RL-TRL/model_test.py">在线推理使用</a>，除此之外有些时候模型训练过程中可能需要进行模型lora测试，如果用transformers自己的生成框架速度很慢，可以考虑使用vllm来进行生成（就像GRPOTrainer中一样），不过需要注意显存的占用（特别是单卡下，多卡就可以专门用一张卡去做推理）消耗，因为vllm初始化过程对于显存占用会比较大，下面主要是争对单卡下的vllm离线使用，最简单方式可以直接使用下面代码（可以直接使用trl中vllm代码/或者直接自己定义），具体的代码位置：trl/generation/vllm_generation.py</p><pre><code class="language-python">from trl.generation.vllm_generation import VLLMGeneration
# 初始化vllm 参数配置可以参考 GRPOTrainer 中的过程
vllm_generation = VLLMGeneration(
    model=your_hf_model,               # 模型
    accelerator=xxx,
    is_fsdp_enabled=True/False,
    processing_class=xxx, # 编码器
    
    gpu_memory_utilization=0.35,       # 单卡关键，调低留空间给训练
    tensor_parallel_size=1,
    enforce_eager=True,                # 推荐单卡防冲突
    enable_lora=True,                  # 如果你用 LoRA
    max_loras=1,
    max_lora_rank=你的rank,
    distributed_executor_backend="external_launcher",  # colocate 必备
    enable_sleep_mode=True, # 启动sleep vllm生成完毕后就直接休眠避免显存占用
    # 生成参数设置
    repetition_penalty=xxx, # 重复度
    temperature=xxx, # 采样温度
    top_p=xxx,
    top_k=xxx,
    min_p=xxx,
    max_completion_length=xxx,
    logprobs=0,  # we only need the generated token logprobs for the importance sampling correction
    generation_kwargs=xxx,
)
# 同步lora效果
vllm_generation.sync_weights()
# 模型生成
outputs = vllm_generation.generate(prompts_or_messages, sampling_params)
</code></pre><p>除此之外对于</p><h2 id="总结">总结</h2><p>本文主要是简单介绍了一些vllm的显存分配过程中，主要是记住几个关键参数：1、预分配显存：<strong>可用显存大小×预分配vllm比率- 非kv cache占用大小</strong>；2、在预分配的显存下可以分配出多少个block：<code>int(available_memory // page_size // num_layers)</code> 对于里面的page_size=(K+V) $\times$ block_size（这个参数一般是指定默认是16）$\times$ num_kv_heads$\times$ head_size$\times$ dtype_bytes，<code>num_layers</code>：所有kv_cache_groups中层数的最大值；3、而后就是直接去计算KV Cache中总token数量这部分计算过程是：<code>num_tokens = num_blocks // len(kv_cache_groups) * min_block_size</code>。<br/> 在获取预分配的显存大小之后，vllm对显存占用方式直接去获取llm结构中“所有的attention”中规则，比如说attention计算发生的层以及给该层预分配的大小最后再去初始化0的张量进行显存占用即可。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://dl.acm.org/doi/pdf/10.1145/3600006.3613165">https://dl.acm.org/doi/pdf/10.1145/3600006.3613165</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://docs.google.com/presentation/d/1QL-XPFXiFpDBh86DbEegFXBXFXjix4v032GhShbKf3s/edit?pli=1&amp;slide=id.g24ad94a0065_0_162#slide=id.g24ad94a0065_0_162">vllm-ppt</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/691045737">https://zhuanlan.zhihu.com/p/691045737</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>推理部署框架vllm</category><pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/12/27/vllm-1.html</guid></item><item><title>Stable Diffusion WebUI和Comfui基础使用</title><link>https://www.big-yellow-j.top/posts/2025/12/12/WebUI_Comfui.html</link><description><![CDATA[Stable Diffusion WebUI基础使用指南：包含Linux环境安装流程，如克隆仓库、修改patches.py避免本地存储、配置webui.sh虚拟环境路径，运行脚本完成部署；详解模型安装方法，可下载dreamshaperXL等权重至指定目录；介绍插件安装，包括汉化插件启用步骤（设置界面语言）与ControlNet插件及对应模型权重配置；支持API调用，通过bash webui.sh -f --api启用后，可用requests方式访问。涵盖目录结构说明、安装避坑技巧，助用户快速上手图像生成与扩展功能。]]></description><content:encoded><![CDATA[<h2 id="stable-diffusion-webui-基础使用">Stable Diffusion WebUI 基础使用</h2><h3 id="sd-webui-安装使用">SD WebUI 安装使用</h3><p>SD WebUI官方地址：<a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui">https://github.com/AUTOMATIC1111/stable-diffusion-webui</a>里面关于安装的介绍不多，这里直接介绍在Linux上直接安装并且基础使用。<br/><strong>首先</strong>、克隆仓库到本地</p><pre><code class="language-bash"># 直接从Github
git clone git@github.com:AUTOMATIC1111/stable-diffusion-webui.git
# 直接从 Gitee（f非官方）
git clone git@gitee.com:smallvillage/stable-diffusion-webui.git
</code></pre><p>在clone得到文件之后对Stable Diffusion WebUI文件夹基本介绍如下<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>：<br/> 1、文本到图像的目录 (outputs/txt2img-images): 存储从文本描述生成的图像。这类目录通常用于保存用户输入文本提示后，系统生成的图像。<br/> 2、图像到图像的目录 (outputs/img2img-images): 存储基于现有图像进行修改或再创作后生成的新图像。这是用于图像编辑或风格迁移任务的输出位置。<br/> 3、附加或实验性质的输出目录 (outputs/extras-images): 可能用于存储实验性或不符合主要类别的其他图像生成结果。<br/> 4、文本到图像网格的目录 (outputs/txt2img-grids): 存储以网格形式展示的多个文本到图像的生成结果，这对于一次性查看和比较多个图像特别有用。<br/> 5、图像到图像网格的目录 (outputs/img2img-grids): 存储以网格形式展示的多个图像到图像的生成结果，同样便于比较和展示。<br/> 6、图像生成日志目录 (log/images): 存储与图像生成过程相关的日志信息，这对于调试和分析生成过程非常重要。<br/> 7、初始化图像的目录 (outputs/init-images): 用于保存在图像到图像转换过程中使用的初始图像或源图像。<br/><strong>根目录</strong><br/><code>.launcher</code>：可能包含与项目启动器相关的配置文件。<br/><code>__pycache__</code>：存储 Python 编译过的字节码文件，以加快加载时间。<br/><code>config_states</code>：可能用于存储项目配置的状态或历史版本。<br/><code>configs</code>：用于存放配置文件，通常包含项目运行所需的参数设置。 <br/><code>detected_maps</code>：可能存储自动生成的映射或检测结果。<br/><code>embeddings</code>：可能包含用于机器学习的嵌入向量数据。<br/><code>extensions</code> 和 ` extensions_builtin <code>：存储项目的扩展或插件。 </code>git<code>：通常是 Git 版本控制的相关目录。 </code>html<code>、</code> javascript <code>：存储网页前端相关的 HTML 文件和 JavaScript 脚本。 </code>launcher<code>：可能包含启动项目的脚本或可执行文件。 </code>localizations<code>：包含项目的本地化文件，如翻译或语言资源。 </code>log<code>：存储日志文件，记录项目运行时的活动或错误信息。 </code>models<code>：通常用于存储机器学习模型或项目中使用的数据模型。 </code>modules<code>：包含项目的代码模块或组件。 </code>outputs<code>：存储项目运行产生的输出文件，如生成的图像或报告。 </code>py310<code>：可能指 Python 3.10 版本的特定文件或环境。 </code>repositories<code>：可能用于存储与代码仓库相关的数据。 </code>scripts<code>：包含用于项目构建、部署或其他自动化任务的脚本。 </code>tags<code>：可能用于版本标记或注释。 </code>test<code>：存储测试代码和测试数据。 </code>textual_inversion<code>：可能是一个特定的功能模块，用于文本相关的处理或转换。 </code>textual_inversion_templates<code>：存储文本逆向工程或模板化处理的文件 </code>tmp<code>：临时文件夹，用于存储临时数据或运行时产生的临时文件。 得到内容之后直接去修改</code>stable-diffusion-webui/modules/patches.py`里面的</p><pre><code class="language-python"># data_path = cmd_opts_pre.data_dir
# models_path = cmd_opts_pre.models_dir if cmd_opts_pre.models_dir else os.path.join(data_path, "models")
data_path = '/root/autodl-tmp/SDWebUIFile/data'
models_path = '/root/autodl-tmp/SDWebUIFile/models'
</code></pre><p>去避免文件直接都下载到本地环境，除此之外在<code>webui.sh</code>里面直接去第一行添加<code>venv_dir="/root/autodl-tmp/SDWebUIFile/venv"</code>（避免虚拟环境直接装在本地）准备工作做完之后就可以直接运行sh文件</p><pre><code class="language-bash">cd stable-diffusion-webui/
# source /etc/network_turbo 如果使用 autodl 服务器
bash webui.sh -f # 加 -f 这个参数如果你是 root 用户使用这个参数避免
</code></pre><blockquote><p>添加 <code>-f</code> 是因为他不支持<code>ERROR: This script must not be launched as root, aborting...</code></p></blockquote><p>安装完毕之后基本就可以直接访问本地地址<code>http://127.0.0.1:7860/</code>然后进行生成图片了。</p><h3 id="sd-webui-其他模型安装">SD WebUI 其他模型安装</h3><p>执行上面操作之后 <code>SD WebUI</code>会默认安装一个模型，不过这个模型效果不是很好，那就需要去安装其他模型，具体操作如下：比如说我需要安装这两个模型：<code>dreamshaperXL_sfwV2TurboDPMSDE.safetensors</code>和 <code>sdxl_vae.safetensors</code>那么只需要去huggingface上去找到指定权重然后下载（<strong>可以直接将huggingface改为国内镜像地址</strong>，但是autodl可以直接 <code>source /etc/network_turbo</code>可以加速Github和Huggingface）即可：</p><blockquote><p><strong>建议直接使用</strong>镜像进行下载具体操作：<a href="https://hf-mirror.com/">huggingface镜像</a>然后看方法三即可<br/><code>./hfd.sh lllyasviel/ControlNet-v1-1 --include control_v11e_sd15_ip2p.pth control_v11e_sd15_ip2p.yaml --local-dir /root/autodl-tmp/SDWebUIFile/models/ControlNet</code><br/> 值得注意的是上面代码如果 include 是一个文件夹那么会直接带着文件夹一起下载，比如说<br/><code>./hfd.sh lllyasviel/ControlNet --include annotator/ckpts/dpt_hybrid-midas-501f0c75.pt --local-dir /root/autodl-tmp/SDWebUIFile/data/extensions/sd-webui-controlnet/annotator/downloads/midas/</code><br/> 可能就需要去移动到指定目录：<code>mv /root/autodl-tmp/SDWebUIFile/data/extensions/sd-webui-controlnet/annotator/downloads/midas/annotator/ckpts/dpt_hybrid-midas-501f0c75.pt /root/autodl-tmp/SDWebUIFile/data/extensions/sd-webui-controlnet/annotator/downloads/midas/</code></p></blockquote><pre><code class="language-python"># 安装 huggingface-cli
pip install -U huggingface_hub
# 登录 下面操作可能因为 huggingface_hub版本不一致不用 可能直接使用 hug_cli 而不是 hf
hf auth login
# 下载所有权重
hf download Madespace/Checkpoint --local-dir ~/autodl-tmp/SDWebUIFile/models/Stable-diffusion/
# 下载部分权重
hf download Madespace/Checkpoint dreamshaperXL_sfwV2TurboDPMSDE.safetensors --local-dir ~/autodl-tmp/SDWebUIFile/models/Stable-diffusion/
hf download stabilityai/sdxl-vae sdxl_vae.safetensors --local-dir ~/autodl-tmp/SDWebUIFile/models/Stable-diffusion/
</code></pre><p>执行上面处理就可以在SD WebUI里面看到自己下载的权重了</p><h3 id="sd-webui-插件安装使用">SD WebUI 插件安装使用</h3><p>对于SD WebUI插件主要介绍两种：1、汉化插件；2、ControlNext插件<br/><strong>安装汉化插件</strong></p><blockquote><p>项目地址：<a href="https://github.com/hanamizuki-ai/stable-diffusion-webui-localization-zh_Hans">https://github.com/hanamizuki-ai/stable-diffusion-webui-localization-zh_Hans</a></p></blockquote><p>操作步骤：<strong>第一步：安装插件</strong><br/><img src="https://s2.loli.net/2025/12/13/mEPsjkRGoqH4UK2.webp" alt="image" width="1904" height="580" loading="lazy" decoding="async"/><br/> 当下面出现：<code>AssertionError: Extension directory already exists: /root/autodl-tmp/SDWebUIFile/data/extensions/stable-diffusion-webui-localization-zh_Hans</code>时候就代表安装完毕，然后就可以直接去进行下面步骤<br/><img src="https://s2.loli.net/2026/01/14/L46qsUEPurB8xwf.webp" alt="image" width="1881" height="821" loading="lazy" decoding="async"/><br/><strong>第二步：启用插件</strong><br/> 然后就可以正常安装了，然后就需要去<code>seeting</code>–&gt;<code>User interface</code>，然后在这个界面选择中文即可（<strong>一定要先点击Apply</strong>）<br/><img src="https://s2.loli.net/2025/12/13/rdDkIGc7jfvhp3U.webp" alt="image" width="1880" height="469" loading="lazy" decoding="async"/><br/> 最后<code>Reload UI</code>即可，这样界面就变成中文了。<br/><strong>ControlNet 插件安装</strong><br/> 基本安装步骤和上面的一样，只是不需要进行第二步：启用插件了。安装<code>ControNet</code>插件之后就只需要去安装对应的模型权重即可使用插件。如果按照上面步骤修改了地址那么：</p><pre><code class="language-bash">(base) root@xxxx:~/autodl-tmp/SDWebUIFile/models# ls
Codeformer  ControlNet  GFPGAN  Lora  Stable-diffusion  hypernetworks
</code></pre><p>然后对于<code>ControlNet</code>权重就可以直接下载然后放到<code>ControlNet</code>中即可，比如说下载<br/><img src="https://s2.loli.net/2025/12/13/ZlzmYJv192Pi57o.webp" alt="image" width="1890" height="749" loading="lazy" decoding="async"/><br/> 就只需要：</p><pre><code class="language-bash">hf download lllyasviel/sd_control_collection diffusers_xl_canny_full.safetensors --local-dir /root/autodl-tmp/SDWebUIFile/models/ControlNet
hf download lllyasviel/sd_control_collection diffusers_xl_depth_full.safetensors --local-dir /root/autodl-tmp/SDWebUIFile/models/ControlNet
</code></pre><p>具体使用可以见：<a href="https://zhuanlan.zhihu.com/p/692537570">https://zhuanlan.zhihu.com/p/692537570</a></p><h3 id="sd-webui-api调用">SD WebUI API调用</h3><p>执行完毕上面操作之后既可以直接调用API进行处理了（<code>bash webui.sh -f --api</code>启用API访问）然后可以直接使用 <code>requests</code>方式进行访问，具体例子比如说：用上面下面的<code>control_v11e_sd15_ip2p.pth</code>和 <code>control_v11f1p_sd15_depth.pth</code>进行测试实验，具体代码：<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/SDWebUI-Comfui/webui_comfui.ipynb">code</a>，值得注意的是：<br/><img src="https://s2.loli.net/2025/12/13/FjRUOs6DXqeKwJL.webp" alt="image" width="930" height="899" loading="lazy" decoding="async"/><br/> 最终得到效果如下<br/><img src="https://s2.loli.net/2025/12/13/iYSXdl91w657u2q.webp" alt="image" width="1840" height="583" loading="lazy" decoding="async"/></p><h2 id="comfyui-基础使用">ComfyUI 基础使用</h2><p>教程：<a href="https://www.bilibili.com/video/BV14w41167eZ/?vd_source=881c4826193cfb648b5cdd0bad9f19f0">https://www.bilibili.com/video/BV14w41167eZ/?vd_source=881c4826193cfb648b5cdd0bad9f19f0</a>对于ComfyUI只需要了解如下几个点：1、如何API调用；2、如何自定义节点；3、工作流搭建（这个直接多看几个别人的就会了）</p><blockquote><p>对于ComfyUI不需要看太多视频，简单总结使用就是：在ComfyUI中所有的运行过程都是在 <strong>工作流</strong> 上进行运行，而工作流中每一个“方框”就是我们的 <strong>节点</strong>（可以意见为一个函数有与输入和输出），不同节点之间输入接输出。</p></blockquote><h3 id="comfyui-自定义节点">ComfyUI 自定义节点</h3><p>自定义节点之前，先了解安装节点，直接进入ComfyUI里面的节点目录<code>/root/autodl-tmp/ComfyUI/custom_nodes</code>，然后找到你需要安装的节点 clone到这个文件夹里面，然后安装依赖，就可以完成一个节点的安装了，<strong>推荐安装节点</strong>：<a href="https://github.com/LAOGOU-666/ComfyUI-LG_HotReload">ComfyUI-LG_HotReload</a>避免每次更新节点都需要去重启服务。自定义节点过程<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。这里直接给出一个比较简洁的模板去自定义节点，首先在ComfyUI/custom_nodes文件中创建一个节点名称（名字任取）文件夹，而后再去创建一个<code>__init__.py</code>文件，以及你的节点功能文件，比如说<code>sam_node.py</code>，对于这两个文件，前者主要是用来将你的节点暴露给comfyui让他可以找到你的节点，后者则是你的节点的功能实现。比如说我要做一个onefromer+sam去<strong>图像分割的节点</strong>那么就可以按照下面过程进行。<br/><strong>首先</strong>，明确我的函数需要的输入和返回。对于图像分割那么输入就是原始图像，而输出一般就是mask图像。那么对于<code>sam_node.py</code>就可以这么定义：</p><pre><code class="language-python">class OneformerSAMNode:
def __init__(self):
  ....
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"oneformer_model": (["oneformer_large", "oneformer_tiny"], {"default": "oneformer_large"}),
"sam_model": (["vit_b", "vit_l"], {"default": "vit_l"}),
"processor_resolution": ("INT", {"default": 512, "min": 256, "max": 1024, "step": 64}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOOLEAN")
RETURN_NAMES = ("mask_image", "mask", "is_empty")
FUNCTION = "detect"
CATEGORY = "image/empty_detect"

def detect(self, image, oneformer_model, sam_model, processor_resolution):
  ...
  return (mask_image, mask, is_empty)
</code></pre><p>对于上述函数，<code>INPUT_TYPES</code>里面定义了节点的输入，<code>RETURN_TYPES</code>定义了节点的输出，<code>FUNCTION</code>定义了节点的功能，<code>CATEGORY</code>定义了节点的分类。<br/><strong>而后</strong>，在定义函数之后就只需要让comfyui去找到这个节点，那么就需要在<code>__init__.py</code>文件中进行如下定义：</p><pre><code class="language-python">from .comfyui_node import NODE_CLASS_MAPPINGS as EMPTY_DETECT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as EMPTY_DETECT_DISPLAY

# 合并所有节点映射
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(EMPTY_DETECT_MAPPINGS)

NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(EMPTY_DETECT_DISPLAY)

# ComfyUI 会自动导入这些映射
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
</code></pre><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://blog.csdn.net/weixin_47420447/article/details/135663351">https://blog.csdn.net/weixin_47420447/article/details/135663351</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://waytoagi.feishu.cn/wiki/Mhj5wl6v1i0j6ZkpevycPIlYnAg">comfyui自定义节点</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>AIGC工具使用</category><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/12/12/WebUI_Comfui.html</guid></item><item><title>从南京到北京</title><link>https://www.big-yellow-j.top/posts/2025/12/01/travel1.html</link><description><![CDATA[从南京到北京]]></description><content:encoded><![CDATA[ ]]></content:encoded><category>life</category><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/12/01/travel1.html</guid></item><item><title>模型量化操作————GPTQ和AWQ量化</title><link>https://www.big-yellow-j.top/posts/2025/10/11/Quantized.html</link><description><![CDATA[模型量化技术是将高精度（如FP16/FP32）模型权重转化为低比特（如INT8、INT4）的压缩方法，分量化感知训练（QAT）和后量化（PTQ）两大类，核心是数值精度校准与转化。GPTQ作为LLM后量化技术，通过“量化-补偿-量化-补偿”迭代流程，分块拆分权重矩阵，逐列量化并计算误差补偿，利用Hessian矩阵辅助误差传播，实现低比特（3-4位）压缩。AWQ则基于激活值分布挑选显著权重（0.1%-1%），对所有权重低比特量化，显著权重乘大scale降低误差，非显著权重乘小scale，通过网格搜索确定最佳scale，保持精度并减少内存占用。两者均属PTQ，有效平衡模型大小、内存占用与推理准确性。]]></description><content:encoded><![CDATA[<h2 id="模型量化技术">模型量化技术</h2><p><strong>量化</strong>：是一种模型压缩的常见方法，将模型权重从高精度（如FP16或FP32）量化为低比特位（如INT8、INT4）。常见的量化策略可以分为PTQ和QAT两大类。<strong>量化感知训练</strong>（Quantization-Aware Training）：在模型训练过程中进行量化，一般效果会更好一些，但需要额外训练数据和大量计算资源。<strong>后量化</strong>（Post-Training Quantization, PTQ）：在模型训练完成后，对模型进行量化，无需重新训练。<br/> 因此对于量化过程总结为：<strong>将数值精度进行“校准”</strong>（比如FP32转化到INT8，两种表述范围不同，因此就需要将前者校准到后者范围），<strong>对“校准”数据进行精度转化</strong>。对于线性量化下，浮点数与定点数之间的转换公式如下：$Q=\frac{R}{S}+Z;R=(Q-Z)*S$，其中R 表示量化前的浮点数、Q 表示量化后的定点数、S（Scale）表示缩放因子的数值、Z（Zero）表示零点的数值。</p><blockquote><p>量化浮点数格式：FP64、FP32、FP16、BF16等<br/> FP以及BF之间差异就在于尾数数量上差异，除此之外在<a href="https://www.big-yellow-j.top/posts/2025/01/01/mixed-precision.html">混合精度</a>训练中也有直接使用FP16精度进行模型训练，不过FP8一般在计算过程中进行使用，模型的存储等还是使用FP16，之所以使用FP8主要还是为了节约显存加速训练，除此之外在FP8格式设计上争对不同阶段有：E4M3（表示值±448）和E5M2（表示值±57344）前面一种更加适合前向传播后面更加适合反向传播。除此之外在训练过程中使用FP8在对最后模型质量的变化差异不大<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">1</a></sup>，在保证FP8训练过程中稳定：1、<strong>Per-tensor / per-block scaling</strong>（张量级 / 小块级缩放）：每个权重矩阵 / 激活张量都有自己独立的缩放因子（scale），让 FP8 的动态范围“对齐”当前数据的实际分布，最大限度减少量化误差。2、<strong>Delayed scaling / delayedamax</strong>：不实时计算 scale，而是累积几步历史最大值再更新，避免 scale 抖动太大导致不稳定</p></blockquote><p><strong>模型量化具体实现过程</strong>（直接使用：<a href="https://zhuanlan.zhihu.com/p/646210009">https://zhuanlan.zhihu.com/p/646210009</a>中的描述）：<br/> 对称量化中，零点 Z = 0，一般不记录，我们只需要关心如何求解 Scale。由于 weight 几乎不存在异常值，因此我们可以直接取 Scale 为一个 layer 或 block 内所有参数的最大绝对值，于是所有的参数都在 [-1, 1] 的区间内。随后，这些参数将找到最近的量化格点，并转化成定点数。<br/><img src="https://s2.loli.net/2025/10/12/Jly87qpuXLHVWPT.webp" alt="image" width="827" height="148" loading="lazy" decoding="async"/><br/><strong>推荐进一步阅读</strong>：<a href="https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html">https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html</a></p><h3 id="gptq量化技术">GPTQ量化技术</h3><p>GPTQ<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>是一种用于大型语言模型（LLM）的后训练量化技术。它通过将模型权重从高精度（如FP16或FP32）压缩到低比特（如3-4位整数）来减少模型大小和内存占用，同时保持较高的推理准确性。一般而言对于量化过程为：对于给定的权重矩阵$W\in R^{n\times m}$，<strong>量化过程</strong>就是需要找到一个低比特的矩阵$\hat{W}$使得：</p> \[\min_{\hat{w}}\Vert WX-\hat{W}X\Vert^2_F\] <p>其中$X$为输入向量，$\Vert. \Vert_F$为Frobenius范数。按照论文里面的描述GPTQ整个过程为：<br/><img src="https://s2.loli.net/2025/10/12/zTrLfJi3HXyt9jm.webp" alt="image" width="1000" height="346" loading="lazy" decoding="async"/></p><blockquote><p>实际使用LLMCompressor进行模型量化过程中，$\lambda$对应参数<code>dampening_frac</code>可能（$W8A8$）会出现：<code>Failed to invert hessian due to numerical instability. Consider increasing GPTQModifier.dampening_frac, increasing the number of calibration samples, or shuffling the calibration dataset</code>其主要原因是计算Hessian矩阵出现严重病态（ill-conditioned）或接近奇异/非正定时，Cholesky 分解就会失败，抛出数值不稳定错误。因此就可以根据里面建议：增加数据、增加$\lambda$的值</p></blockquote><p>对于具体数学原理的描述参考文章<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">3</a></sup><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">4</a></sup>（数学原理推荐直接看：<a href="https://zhuanlan.zhihu.com/p/1941146483756897225">GPTQ详细解读</a>），简单总结一下上面过程就是：1、每行独立计算二阶海森矩阵。2、每行按顺序进行逐个参数量化，从而可以并行计算。3、按block维度进行更新，对剩余参数进行延迟更新弥补。4、对逆海森矩阵使用cholesky分解，等价消除迭代中的矩阵更新计算。<strong>它的核心流程其实就是量化-补偿-量化-补偿的迭代</strong>（具体过程见流程图中<strong>内部循环</strong>：首先量化$W_{:,j}$，而后去计算误差并且补充到 $W_{:,j:(i+B)}$），具体的代码实现过程（<a href="https://github.com/IST-DASLab/gptq">官方GPTQ-Github</a>）主要是对其中LlamaAttention和LlamaMLP层中的Linear层<a href="https://github.com/IST-DASLab/gptq/blob/2d65066eeb06a5c9ff5184d8cebdf33662c67faf/llama.py#L75C1-L84C1">权重进行量化</a>。代码处理过程<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">5</a></sup>：<br/><strong>首先</strong>、计算Hessian矩阵（因为后续计算损失和补偿权重需要，因此提前计算矩阵） 这个矩阵近似：$H_F=2X_FX_F^T$（$X$是<strong>经过前面几层神经网络之后，到达被量化层的激活</strong>）。实现方式是在每一层Layer上注册hook，通过hook的方式在layer forward后使用calibration data的input来生成Hessian矩阵，这种计算方式常见于量化流程中校准数据的处理</p><pre><code class="language-python">def add_batch(name):
    def tmp(_, inp, out):
        # 假设过程为：x → Linear(W) → ReLU
        # x →inp[0].data Linear层输出→out
        gptq[name].add_batch(inp[0].data, out.data)
    return tmp
handles = []
# 添加hook
for name in subset:
    handles.append(subset[name].register_forward_hook(add_batch(name)))
# 处理样本计算数据
for j in range(args.nsamples):
    outs[j] = layer(inps[j].unsqueeze(0), attention_mask=attention_mask, position_ids=position_ids)[0]
# 去除hook
for h in handles:
    h.remove()
</code></pre><p>在<code>add_batch</code>中具体为了利用所有的校准数据，这里通过迭代的方式将每组数据计算的Hessian矩阵值进行求和然后取平均，代码实现是迭代逐渐平均叠加的过程，Hessian矩阵求解公式：$H_F=2X_FX_F^T$</p><pre><code class="language-python"># 假设过程为：x → Linear(W) → ReLU
# x →inp[0].data Linear层输出→out
#gptq[name].add_batch(inp[0].data, out.data)
def add_batch(self, inp, out):
    ...
    if len(inp.shape) == 2:
        inp = inp.unsqueeze(0)
    tmp = inp.shape[0]
    if isinstance(self.layer, nn.Linear) or isinstance(self.layer, transformers.Conv1D):
        if len(inp.shape) == 3:
            inp = inp.reshape((-1, inp.shape[-1]))
        inp = inp.t()
    if isinstance(self.layer, nn.Conv2d):
        unfold = nn.Unfold(
            self.layer.kernel_size,
            dilation=self.layer.dilation,
            padding=self.layer.padding,
            stride=self.layer.stride
        )
        inp = unfold(inp)
        inp = inp.permute([1, 0, 2])
        inp = inp.flatten(1)
    self.H *= self.nsamples / (self.nsamples + tmp)
    self.nsamples += tmp
    inp = math.sqrt(2 / self.nsamples) * inp.float()
    self.H += inp.matmul(inp.t())
</code></pre><p><strong>其次</strong>、逐层weight量化</p><pre><code class="language-python">for name in subset:
    gptq[name].fasterquant(
        percdamp=args.percdamp, groupsize=args.groupsize, actorder=args.act_order, static_groups=args.static_groups
    )
    quantizers['model.layers.%d.%s' % (i, name)] = gptq[name].quantizer
    gptq[name].free()
</code></pre><p>主要是通过逐层使用<code>fasterquant</code>方法作为入口来进行量化处理。<code>fasterquant</code> 用层的权重矩阵 W 和之前收集到的激活 Gram（或近似 Hessian）H 来做按列（按 block）贪心量化。它先把 H 经过阻尼并通过 Cholesky/逆操作得到用于投影/补偿的因子（称为 Hinv），然后按 block 内逐列量化：对第 j 列量化后计算误差 e_j，用 Hinv 的相应行/列把这个误差按 Schur 补方式投影/传播到该 block 内剩余列并在 block 外一次性传播到后续列，从而实现 GPTQ 的误差补偿策略。在<code>fasterquant</code>方法中主要进行了量化的计算过程，具体实现过程为（核心代码）：</p><pre><code class="language-python">def fasterquant(
    self, blocksize=128, percdamp=.01, groupsize=-1, actorder=False, static_groups=False
):
    W = self.layer.weight.data.clone()
    if isinstance(self.layer, nn.Conv2d):
        W = W.flatten(1)
    if isinstance(self.layer, transformers.Conv1D):
        W = W.t()
    W = W.float()

    tick = time.time()

    if not self.quantizer.ready():
        self.quantizer.find_params(W, weight=True)

    # self.H 是上一步中计算得到的Hessian矩阵
    H = self.H
    del self.H
    dead = torch.diag(H) == 0
    H[dead, dead] = 1
    W[:, dead] = 0

    ...
    # 初始化 losses 0矩阵
    Losses = torch.zeros_like(W)
    Q = torch.zeros_like(W)

    damp = percdamp * torch.mean(torch.diag(H))
    diag = torch.arange(self.columns, device=self.dev)
    H[diag, diag] += damp
    H = torch.linalg.cholesky(H)
    H = torch.cholesky_inverse(H)
    H = torch.linalg.cholesky(H, upper=True)
    Hinv = H
    # 逐Block处理
    # self.columns = W.shape[1]
    for i1 in range(0, self.columns, blocksize):
        i2 = min(i1 + blocksize, self.columns)
        count = i2 - i1

        W1 = W[:, i1:i2].clone()
        Q1 = torch.zeros_like(W1)
        Err1 = torch.zeros_like(W1)
        Losses1 = torch.zeros_like(W1)
        Hinv1 = Hinv[i1:i2, i1:i2]
        # Block内部量化
        for i in range(count):
            w = W1[:, i]
            d = Hinv1[i, i]

            if groupsize != -1:
                if not static_groups:
                    if (i1 + i) % groupsize == 0:
                        self.quantizer.find_params(W[:, (i1 + i):(i1 + i + groupsize)], weight=True)
                else:
                    idx = i1 + i
                    if actorder:
                        idx = perm[idx]
                    self.quantizer = groups[idx // groupsize]

            q = quantize(
                w.unsqueeze(1), self.quantizer.scale, 
                self.quantizer.zero, self.quantizer.maxq
            ).flatten()
            Q1[:, i] = q
            Losses1[:, i] = (w - q) ** 2 / d ** 2

            err1 = (w - q) / d
            W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))
            Err1[:, i] = err1

        Q[:, i1:i2] = Q1
        Losses[:, i1:i2] = Losses1 / 2

        W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])
    torch.cuda.synchronize()
    ...

    if actorder:
        Q = Q[:, invperm]

    if isinstance(self.layer, transformers.Conv1D):
        Q = Q.t()
    self.layer.weight.data = Q.reshape(self.layer.weight.shape).to(self.layer.weight.data.dtype)
</code></pre><p>对于上面过程主要是看两个for循环的里面内容，首先第一个for循环去根据block去将权重矩阵W进行<strong>分块拆分</strong>（<code>W1 = W[:, i1:i2].clone()</code>），接下来第二个for循环依次去对第1块中每列进行量化，第i列进行量化（<code>quantize</code>）处理（<code>q = quantize(...)</code>），而后去计算loss并且去对其他的列（<code>i:</code>）计算<code>W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))</code>，在处理完毕第1块之后再去将后面块的列进行<strong>误差补偿</strong>（<code>W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])</code>），这样整个过程就完成了。</p><pre><code class="language-python"># 量化函数
def quantize(x, scale, zero, maxq):
    if maxq &lt; 0:
        return (x &gt; scale / 2).float() * scale + (x &lt; zero / 2).float() * zero
    q = torch.clamp(torch.round(x / scale) + zero, 0, maxq)
    return scale * (q - zero)
</code></pre><p><strong>最后</strong>、量化模型保存 。之前的步骤中量化和反量化后计算lose都是浮点位数的，所以并没有生成wbit位format的数值内容，在llama_pack方法中通过model和之前得到的quantizer(scale, zero)来生成wbit位数表达格式的量化模型，其定义如下所示</p><pre><code class="language-python">def llama_pack3(model, quantizers):
    layers = find_layers(model)
    layers = {n: layers[n] for n in quantizers}
    make_quant3(model, quantizers)
    qlayers = find_layers(model, [Quant3Linear])
    for name in qlayers:
        quantizers[name] = quantizers[name].cpu()
        # 使用 Quant3Linear 进行pack处理
        qlayers[name].pack(layers[name], quantizers[name].scale, quantizers[name].zero)
    return model
# 将model中每一层都替换为 Quant3Linear
def make_quant3(module, names, name='', faster=False):
    if isinstance(module, Quant3Linear):
        return
    for attr in dir(module):
        tmp = getattr(module, attr)
        name1 = name + '.' + attr if name != '' else attr
        if name1 in names:
            setattr(module, attr, Quant3Linear(tmp.in_features, tmp.out_features, faster=faster))
    for name1, child in module.named_children():
        make_quant3(child, names, name + '.' + name1 if name != '' else name1, faster=faster)
...
if args.wbits &lt; 16 and not args.nearest:
    quantizers = llama_sequential(model, dataloader, DEV)
if args.save:
    llama_pack3(model, quantizers)
</code></pre><p>其中quantizers来自量化后的返回，它是一个dict里面保存了每一个层和它对应的quantizer、scale、zero、group_idx等信息，其中quantizer是layer-level的，zero和scale是group-level的。</p><blockquote><p>quantizers的结果为：<code>quantizers['model.layers.%d.%s' % (i, name)] = (gptq[name].quantizer.cpu(), scale.cpu(), zero.cpu(), g_idx.cpu(), args.wbits, args.groupsize)</code></p></blockquote><p>Quant3Linear具体处理过程（<a href="https://github.com/IST-DASLab/gptq/blob/2d65066eeb06a5c9ff5184d8cebdf33662c67faf/quant.py#L137C1-L200C68">代码</a>），通过qweight、zeros和scales、bias等属性来保存量化后的低比特信息。：</p><pre><code class="language-python"># qlayers[name].pack(layers[name], quantizers[name].scale, quantizers[name].zero)
class Quant3Linear(nn.Module): 
    def __init__(self, infeatures, outfeatures, faster=False):
        super().__init__()
        self.register_buffer('zeros', torch.zeros((outfeatures, 1)))
        self.register_buffer('scales', torch.zeros((outfeatures, 1)))
        self.register_buffer('bias', torch.zeros(outfeatures))
        self.register_buffer(
            'qweight', torch.zeros((infeatures // 32 * 3, outfeatures), dtype=torch.int)
        )
        self.faster = faster

    def pack(self, linear, scales, zeros):
        self.zeros = zeros * scales
        self.scales = scales.clone()
        if linear.bias is not None:
            self.bias = linear.bias.clone()

        intweight = torch.round((linear.weight.data + self.zeros) / self.scales).to(torch.int)
        intweight = intweight.t().contiguous()
        intweight = intweight.numpy().astype(np.uint32)
        qweight = np.zeros(
            (intweight.shape[0] // 32 * 3, intweight.shape[1]), dtype=np.uint32
        )

        i, row = 0, 0
        while row &lt; qweight.shape[0]:
            # 把 32 个 3-bit 整数按位连续打包到 3 个 uint32
            ...
        qweight = qweight.astype(np.int32)
        self.qweight = torch.from_numpy(qweight) 
</code></pre><p>对于上述打包（3-bit打包）处理过程为：<code>qweight = np.zeros((intweight.shape[0] // 32 * 3, intweight.shape[1]), dtype=np.uint32)</code>每 32 个 intweight 的行使用 3 个 uint32 行来存储，不过值得注意的是以 int32 的形式存储量化权重，但这 并不代表每个权重占 32 bit。这里的 int32 是一个打包容器（bit-packing container），里面塞了多个低 bit（比如 3 bit）的权重值。</p><h3 id="awq量化技术">AWQ量化技术</h3><p>AWQ量化<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">6</a></sup>（逐层量化方法，需要每层的输入激活来计算 scale 和 clip 值）是一种基于<strong>激活值分布挑选显著权重</strong>进行量化的方法，其不依赖于任何反向传播或重建，因此可以很好地保持LLM在不同领域和模式上的泛化能力，而不会过拟合到校准集，属训练后量化大类，论文里面出发点就是模型的权重并不同等重要，<strong>仅有0.1%-1%的小部分显著权重对模型输出精度影响较大</strong>。因此如果能有办法只对0.1%~1%这一小部分权重保持原来的精度(FP16)，对其他权重进行低比特量化，就可以在保持精度几乎不变的情况下，大幅降低模型内存占用，并提升推理速度。<br/><img src="https://s2.loli.net/2025/10/14/PGa2p3Ac9xCTD7I.webp" alt="image" width="1502" height="361" loading="lazy" decoding="async"/><br/> 但是如果部分用FP16而其他的用INT3这样就会导致硬件上存储困难（图b情况），因此作者使用的操作就是：<strong>对所有权重均进行低比特量化，但是，在量化时，对于显著权重乘以较大的scale，相当于降低其量化误差；同时，对于非显著权重，乘以较小的scale，相当于给予更少的关注</strong>。因此代码关注点就是找到这个scale值</p><blockquote><p><strong>基于激活值分布挑选方法</strong>：<strong>激活值指的是</strong>与权重矩阵运算的输入值，比如说：$V=W_vX$其中的 $X$就是权重 $W_v$的激活值，按激活值绝对值大小由大到小排序，绝对值越大越显著，选择前0.1%~1%的元素作为显著权重。<br/> 具体代码过程（<a href="https://github.com/mit-han-lab/llm-awq/blob/d6e797a42b9ef7778de8ee2352116e0f48a78d61/awq/quantize/pre_quant.py#L102">Github-Code</a>）</p></blockquote><p>首先是获取 模型第一层的输入激活值，供后续的逐层量化使用，代码整体流程如下（核心代码格式）：</p><pre><code class="language-python">@torch.no_grad()
def run_awq(model,enc,w_bit,q_config,n_samples=512,seqlen=512,auto_scale=True,mse_range=True,calib_data="pileval",):
    ...

    layers = get_blocks(model)
    samples = get_calib_dataset(...)
    # 得到第一层的激活值
    inps = []
    layer_kwargs = {}

    layers[0] = layers[0].cuda()
    ...
    class Catcher(nn.Module):
        def __init__(self, module):
            super().__init__()
            self.module = module

        def forward(self, inp, **kwargs):
            inps.append(inp)
            layer_kwargs.update(kwargs)
            raise ValueError
    layers[0] = Catcher(layers[0])
    try:
        if model.__class__.__name__ == "LlavaLlamaModel":
            model.llm(samples.to(next(model.parameters()).device))
        ...
    except ValueError:
        pass
    ...
    layers[0] = layers[0].module
    inps = inps[0]
    layers[0] = layers[0].cpu()
    ...
</code></pre><p><strong>而后</strong>、逐层进行量化处理，在AWQ量化过程中需要记录两部分量化值<code>scale</code>（<a href="https://github.com/mit-han-lab/llm-awq/blob/main/awq/quantize/auto_scale.py">auto_sclae.py</a>） 和 <code>clip</code>（<a href="https://github.com/mit-han-lab/llm-awq/blob/main/awq/quantize/auto_clip.py">auto_clip.py</a>）两部分具体源码处理过程都是相似的先去计算scale值而后将scale值应用，在计算两部分值之前和GPTQ处理相似去记录forward过程，具体代码为：</p><pre><code class="language-python">for i in tqdm.tqdm(range(len(layers))):
    layer = layers[i]
    layer = layer.cuda()
    named_linears = get_named_linears(layer)
    
    # AWQ量化过程中，记录输入数据
    def cache_input_hook(m, x, y, name, feat_dict):
        x = x[0]
        x = x.detach().cpu()
        feat_dict[name].append(x)
        
    input_feat = defaultdict(list)
    handles = []
    for name in named_linears:
        handles.append(
            named_linears[name].register_forward_hook(
                functools.partial(cache_input_hook, name=name, feat_dict=input_feat)
            )
        )
        
    inps = inps.to(next(layer.parameters()).device)
    # 输入数据被 线性层 处理触发上面的 hook 去记录每层的输入 x
    inps = layer(inps, **layer_kwargs)[0]
    for h in handles:
        h.remove()
    input_feat = {k: torch.cat(v, dim=0) for k, v in input_feat.items()}
</code></pre><p>其中cache_input_hook过程就是直接记录每层layer中的linear层的输入值并且将其记录到input_feat中。<strong>scale处理过程</strong>（寻找最有因子过程）代码如下：</p><pre><code class="language-python">elif isinstance(module, (LlamaDecoderLayer, Qwen2DecoderLayer)):
    # attention input
    scales_list.append(
        _auto_get_scale(
            prev_op=module.input_layernorm,
            layers=[
                module.self_attn.q_proj,
                module.self_attn.k_proj,
                module.self_attn.v_proj,
            ],
            inp=input_feat["self_attn.q_proj"],
            module2inspect=module.self_attn,
            kwargs=module_kwargs,
        )
    )

'''
_auto_get_scale 中核心逻辑是使用 search_module_scale 并且其中4个参数分别对应
block = module2inspect=module.self_attn 
linears2scale = [module.self_attn.q_proj, module.self_attn.k_proj, module.self_attn.v_proj]
x = input_feat["self_attn.q_proj"]
'''
def _search_module_scale(block, linears2scale: list, x, kwargs={}):
    # block：对应block linears2scale：对应线性层
    x = x.to(next(block.parameters()).device)
    
    # 第一步，将数据数据用没有被量化的模型进行一次计算，并且计算 平均幅度（x_max）
    with torch.no_grad():
        org_out = block(x, **kwargs)
        ...
    x_max = get_act_scale(x) # x.abs().view(-1, x.shape[-1]).mean(0)

    best_error = float("inf")
    best_ratio = -1
    best_scales = None
    
    # 第二步，直接用网格搜索方法去寻找最优的 scale
    n_grid = 20
    history = []
    org_sd = {k: v.cpu() for k, v in block.state_dict().items()}
    for ratio in range(n_grid):
        ratio = ratio * 1 / n_grid
        # 计算当前比例的缩放因子，并且进行归一化处理
        scales = x_max.pow(ratio).clamp(min=1e-4).view(-1)
        scales = scales / (scales.max() * scales.min()).sqrt()
        
        # 进行一次模拟量化操作
        for fc in linears2scale:
            # 物理缩放权重：把激活值的压力转移到权重上
            fc.weight.mul_(scales.view(1, -1).to(fc.weight.device))
            # 模拟量化：量化后再除以 scales 还原回浮点域
            fc.weight.data = w_quantize_func(fc.weight.data) / (scales.view(1, -1))
        out = block(x, **kwargs) # 计算量化后的模型输出
        ...
        loss = ((org_out - out).float().pow(2).mean().item()) # 计算损失
        is_best = loss &lt; best_error
        if is_best:
            ...
            best_scales = scales
        # 恢复到最初状态，去寻找下一个 ratio
        block.load_state_dict(org_sd)
    ...
    best_scales = best_scales.view(-1)
    ...
    return best_scales.detach()
</code></pre><blockquote><p><strong>对所有权重均进行低比特量化，但是，在量化时，对于显著权重乘以较大的scale，相当于降低其量化误差；同时，对于非显著权重，乘以较小的scale，相当于给予更少的关注</strong></p></blockquote><p>其实对于上面过程就是直接通过网格搜索策略通过得到的<code>x_max=x.abs().view(-1, x.shape[-1]).mean(0)</code>去不断尝试scales去让loss最小，从而得到scale值。对于其中的量化处理过程<a href="https://github.com/mit-han-lab/llm-awq/blob/d6e797a42b9ef7778de8ee2352116e0f48a78d61/awq/quantize/quantizer.py#L61">w_quantize_func</a>，核心是计算 $q=clip(round(\frac{w}{s}​)+z,q_{min}​,q_{max}​)$：</p><pre><code class="language-python">'''
w_quantize_func(fc.weight.data) / (scales.view(1, -1))
w 对应 fc.weight.data) / (scales.view(1, -1)
'''
def pseudo_quantize_tensor(w, n_bit=8, zero_point=True, q_group_size=-1, inplace=False, get_scale_zp=False):
    org_w_shape = w.shape
    if q_group_size &gt; 0:
        assert org_w_shape[-1] % q_group_size == 0
        w = w.reshape(-1, q_group_size)
    assert w.dim() == 2
    if zero_point: 
        max_val = w.amax(dim=1, keepdim=True)
        min_val = w.amin(dim=1, keepdim=True)
        max_int = 2**n_bit - 1
        min_int = 0
        scales = (max_val - min_val).clamp(min=1e-5) / max_int
        zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)
    else:  ... # 对称量化
    ...
    if inplace:...
    else:
        w = (
            torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) - zeros
        ) * scales
    assert torch.isnan(w).sum() == 0
    w = w.reshape(org_w_shape)
    if get_scale_zp:...
    else:
        return w
</code></pre><p>对于上面过程总结就是：把 w 线性映射到一个由 bit 位数（n_bit）决定的固定整数区间（q_min 到 q_max），其中scale 决定缩放比例，zero_point 决定映射偏移</p><h2 id="总结">总结</h2><p>GPTQ量化技术总结：核心流程其实就是<strong>量化-补偿-量化-补偿的迭代</strong>，首先通过对模型权重$W$首先去对$W$进行<strong>分块拆分</strong>得到不同的block再去到每一个block里面去按照每i列进行量化（<code>quantize</code>）处理（<code>q = quantize(...)</code>），而后去计算loss并且去对其他的列（<code>i:</code>）计算<code>W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))</code>，在处理完毕第1块之后再去将后面块的列进行误差补偿（<code>W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])</code>），这样就得到了scales, zeros这信息，在去使用这些信息去对模型权重进行转化<code>intweight = torch.round((linear.weight.data + self.zeros) / self.scales).to(torch.int)</code>，最后就是用32 个intweight的行使用 3 个 uint32 行来存储，推理过程的话：$y = Wx + b\rightarrow y≈x(s_j(q-z_j))+b$<br/> AWQ量化技术总结：核心流程就是<strong>对所有权重均进行低比特量化，但是，在量化时，对于显著权重乘以较大的scale，相当于降低其量化误差；同时，对于非显著权重，乘以较小的scale，相当于给予更少的关注</strong>，对于这个scale值的寻找直接计算每一层的输入“激活值”（<code>x.abs().view(-1, x.shape[-1]).mean(0)</code>）而后对这个激活值通过网格搜索方法（<code>scales = x_max.pow(ratio).clamp(min=1e-4).view(-1)</code>其中ratio对应网格收缩）不断去尝试不同的scale，并且将这个scale去用到最初的模型权重上进行一次<strong>模拟量化</strong>处理，而后去计算 <em>模拟量化后模型计算得到的损失和没有量化的模型之间损失</em>，找到这个最佳scale即可。</p><h2 id="代码操作">代码操作</h2><blockquote><p><a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelCode/DF_acceralate/quant_LLM.ipynb">Github-code</a><br/><a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/ONNX_TensoRT/ModelDeployment.ipynb">模型ONNX部署技术</a></p></blockquote><p>直接使用<code>llmcompressor</code>来量化模型（具体地址：<a href="https://docs.vllm.ai/projects/llm-compressor/en/latest/getting-started/install/#prerequisites">llmcompressor</a>）支持量化类型：<br/><img src="https://s2.loli.net/2025/11/11/KUrHF4IiLC7WaXf.webp" alt="image" width="659" height="741" loading="lazy" decoding="async"/><br/><strong>推荐进一步阅读</strong>：<a href="https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html">https://www.big-yellow-j.top/posts/2025/12/29/SDAcceralate.html</a></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:6" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2310.18313v2">https://arxiv.org/pdf/2310.18313v2</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://github.com/IST-DASLab/gptq">https://github.com/IST-DASLab/gptq</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/646210009">https://zhuanlan.zhihu.com/p/646210009</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/629517722">https://zhuanlan.zhihu.com/p/629517722</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/697860995">https://zhuanlan.zhihu.com/p/697860995</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2306.00978">https://arxiv.org/pdf/2306.00978</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>量化部署</category><pubDate>Sat, 11 Oct 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/10/11/Quantized.html</guid></item><item><title>多模态模型——QwenVL2.5的微调以及强化学习代码操作</title><link>https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html</link><description><![CDATA[本文详细解析QwenVL2.5模型的处理流程及微调方法，包括模板化输入（通过processor.apply_chat_template处理对话messages，含等标记模拟用户/assistant对话）、编码输入（图像处理采用smart_resize动态调整分辨率确保可被patch_size整除，经归一化后转为Vit的patch序列；文本通过tokenizer编码）、模型处理（视觉Transformer对pixel_values进行Conv3d处理生成特征，结合window-attention计算）。同时，阐述了SFT微调流程：数据层面构建对话模板生成input_ids、pixel_va...]]></description><content:encoded><![CDATA[<p>从代码角度去理解QwenVL2.5是如何处理，以及结合实际操作理解如何去对一个QwenVL2.5-3B进行SFT和强化学习处理。<br/> 简单了解一下QwenVL2.5模型的整个处理过程，模型整体过程大致为：1、首先是通过模板化处理我的模型的输入（image+text）；2、将输入转化为编码形式（比如文本tokenizer处理等）；3、出入模型处理输入然后模型输出；4、解码输出内容。整体主要是上述4个过程，因此下面逐一了解一下模型到底在做什么。<br/> 内容较多对于强化学习部分之间看最后的总结部分即可：<br/> 1、<a href="https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html#:~:text=%E4%B8%80%E8%88%AC%E5%BE%97%E5%88%B0%E7%9A%84%E6%98%AF-,RL%2DPPO%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93,-RL%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94">trl框架下PPO代码总结</a>；<br/> 2、<a href="https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html#:~:text=%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93-,%E9%A6%96%E5%85%88,-%E5%AF%B9%E4%BA%8E%E6%88%91%E4%BB%AC%E7%9A%84">trl框架下DPO代码总结</a>；<br/> 3、<a href="https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html#:~:text=%E6%9C%80%E5%90%8E%E7%9A%84%E5%80%BC%E3%80%82-,RL%2DGRPO%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93,-%E5%AF%B9%E4%BA%8E%E4%B8%8A%E9%9D%A2loss">trl框架下GRPO代码总结</a></p><h2 id="qwenvl的基本使用">QwenVL的基本使用</h2><h3 id="1模板化模型输入">1、模板化模型输入</h3><pre><code class="language-python">messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "image",
                "image": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg",
            },
            {"type": "text", "text": "Describe this image."},
        ],
    }
]

text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
</code></pre><p>所谓模板化模型的输入，很容易理解（通过<code>processor.apply_chat_template</code>把<strong>对话 messages 转成模型能理解的 prompt</strong>，不过值得注意的是不同模型可能处理的方式不同），就是将我的内容“填充”到模板中模拟对话内容，比如说上面处理得到的一个简单结果就是：</p><pre><code class="language-python">&lt;|im_start|&gt;system
You are a helpful assistant.&lt;|im_end|&gt;
&lt;|im_start|&gt;user
&lt;|vision_start|&gt;&lt;|image_pad|&gt;&lt;|vision_end|&gt;Describe this image.&lt;|im_end|&gt;
&lt;|im_start|&gt;assistant
</code></pre><p>一般在<strong>data_loader里面就会提前将我们的模型需要的输入处理好</strong>，比如说我们定义如下的模板</p><pre><code class="language-python">def format_data(self, image, text, prompt):
    # self.SYSTEM_MESSAGE = """You are a helpful assistant."""
    return [
        {
            "role": "system",
            "content": [{"type": "text", "text": self.SYSTEM_MESSAGE}],
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "image": image,
                },
                {
                    "type": "text",
                    "text": prompt
                },
            ],
        },
        {
            "role": "assistant",
            "content": [{"type": "text", "text": text}],
        },
    ]
""" 
&lt;|im_start|&gt;system
You are a helpful assistant.&lt;|im_end|&gt;
&lt;|im_start|&gt;user
&lt;|vision_start|&gt;&lt;|image_pad|&gt;&lt;|vision_end|&gt;This is a prompt&lt;|im_end|&gt;
&lt;|im_start|&gt;assistant
This is a text&lt;|im_end|&gt;
&lt;|im_start|&gt;assistant
"""

</code></pre><p>对于上面内容输出理解，首先 <code>&lt;|im_start|&gt;....&lt;|im_end|&gt;</code>一般是一组“发言”的开始和结束标记，而后里面内容就是我们的文本/图像内容，<code>user</code>/ <code>assistant</code>/ <code>system</code> 则是分别代表：用户、模型、角色（告诉模型今天扮什么角色）。<code>&lt;|vision_start|&gt;...&lt;|vision_end|&gt;</code>：表示图像输入的占位符，告诉模型这里有一段视觉信息。<code>&lt;|image_pad|&gt;</code>：图像实际的 embedding 会在这里替换（填充），不是文字，而是图像编码后的向量。值得注意的是 <code>assistant</code>后面的内容就是 <strong>模型需要输出的文本内容</strong>。上面过程很容易理解，只不过需要注意如下问题，因为QwenVL2.5对于分辨率是存在处理（一般直接通过<code>smart_resize</code>处理，后续有介绍），因此如果涉及到目标识别，可能需要提前将坐标进行转换避免分辨率不同导致bbox对应不上的问题</p><h3 id="2编码模板输入">2、编码模板输入</h3><pre><code class="language-python">image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
    text=[text],
    images=image_inputs,
    videos=video_inputs,
    padding=True,
    return_tensors="pt",
)
</code></pre><p>编码模板输入就比较简单，因为我的输入都是文本/图片，此过程就是需要将这些内容转化为编码形式（比如tokenizer处理等），处理方式如下：</p><ul><li>1、<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L352">process_vision_info</a>:返回我的图像/视频输出（都存储在list中）</li></ul><p>首先是过<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L334">extract_vision_info</a>从我上面的内容中提取出图片/视频（<code>[{'type': 'image', 'image': 'https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg'}]</code>）提取完毕之后就是交给处理图片/视频的函数进行处理<br/><strong>图片处理过程</strong>（<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L97"><code>fetch_image</code></a>）此过程也会比较简单，首先去判断类型（是<code>Image.Image</code>对象/图片链接等）然后打开图片，而后就是<strong>确定图片分辨率尺寸</strong>，有两种<code>smart_resize</code>处理方式，第一种是直接通过：<code>resized_height</code> 和 <code>resized_width</code>来确定改变，另外一种直接通过 <code>min_pixels</code> 和 <code>max_pixels</code> 来处理图像尺寸。对于<code>smart_rezie</code>函数处理过程为：</p><pre><code class="language-python">def smart_resize(
    height: int, width: int, factor: int = IMAGE_FACTOR, min_pixels: int = MIN_PIXELS, max_pixels: int = MAX_PIXELS)
    # IMAGE_FACTOR= 28
    if max(height, width) / min(height, width) &gt; MAX_RATIO:
        ...
    h_bar = max(factor, round_by_factor(height, factor)) # round(number / factor) * factor
    w_bar = max(factor, round_by_factor(width, factor))
    if h_bar * w_bar &gt; max_pixels:
        beta = math.sqrt((height * width) / max_pixels)
        h_bar = floor_by_factor(height / beta, factor) # 按比例缩小并向下取整  math.floor(number / factor) * factor
        w_bar = floor_by_factor(width / beta, factor)
    elif h_bar * w_bar &lt; min_pixels:
        beta = math.sqrt(min_pixels / (height * width))
        h_bar = ceil_by_factor(height * beta, factor) # 按比例放大并向上取整 math.ceil(number / factor) * factor
        w_bar = ceil_by_factor(width * beta, factor)
    return h_bar, w_bar
</code></pre><p>上面3个小的子函数表示：计算factor倍数、向上取整计算倍数、向下取整计算倍数，对于smart_resize（去实现动态分辨率）函数：<strong>通过四舍五入的方式，重新设置图片的 h 和 w 值，确保它们可以被28整除</strong>，这样一来就得到了图像的需要修改的尺寸了，比如说：<br/> 输入: 一张 1000x500 的图像<br/> 计算基础尺寸：round(1000/28)=36, round(500/28)=18 → 1008x504<br/> 检查像素数：1008<em>504 = 508,032 &gt; MAX_PIXELS(200,704)<br/> 计算缩放系数：beta = sqrt(1000</em>500/200704) ≈ 1.58<br/> 最终尺寸：floor(1000/1.58)=632, floor(500/1.58)=316 → 616x308（28的倍数）<br/><strong>视频处理过程</strong>（<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L277">fetch_video</a>）对于视频处理和图像处理相类似打开–&gt;改变尺寸。只不过在打开过程中QwenLV2.5处理过程为：</p><pre><code class="language-python">def fetch_video(ele: dict, image_factor: int = IMAGE_FACTOR, return_video_sample_fps: bool = False):
    if isinstance(ele["video"], str):
        video_reader_backend = get_video_reader_backend()
        try:
            video, sample_fps = VIDEO_READER_BACKENDS[video_reader_backend](ele)
        except Exception as e:
            logger.warning(f"video_reader_backend {video_reader_backend} error, use torchvision as default, msg: {e}")
            video, sample_fps = VIDEO_READER_BACKENDS["torchvision"](ele)
    ...
</code></pre><p>对于<code>VIDEO_READER_BACKENDS</code>设计了3中不同范式：1、<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L226">_read_video_decord</a>；2、<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L183">_read_video_torchvision</a>；3、_read_video_torchcodec。</p><ul><li><code>_read_video_decord</code></li></ul><pre><code class="language-python">def _read_video_decord(
    ele: dict,
) -&gt; (torch.Tensor, float):
    """read video using decord.VideoReader

    Args:
        ele (dict): a dict contains the configuration of video.
        support keys:
            - video: the path of video. support "file://", "http://", "https://" and local path.
            - video_start: the start time of video.
            - video_end: the end time of video.
    Returns:
        torch.Tensor: the video tensor with shape (T, C, H, W).
    """
    import decord
    video_path = ele["video"]
    st = time.time()
    vr = decord.VideoReader(video_path)
    total_frames, video_fps = len(vr), vr.get_avg_fps()
    start_frame, end_frame, total_frames = calculate_video_frame_range(
        ele,
        total_frames,
        video_fps,
    ) # 得到视频的开始 结束 总结多少帧
    nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
    idx = torch.linspace(start_frame, end_frame, nframes).round().long().tolist()
    video = vr.get_batch(idx).asnumpy()
    video = torch.tensor(video).permute(0, 3, 1, 2)  # Convert to TCHW format
    ...
    sample_fps = nframes / max(total_frames, 1e-6) * video_fps
    return video, sample_fps
</code></pre><p>对于其中的 <code>calculate_video_frame_range</code>函数处理过程也很简单（直接去计算视频开始、结束、总共多少帧），而后类似动态分辨率（smart_resize中成立相类似的）对于视频会通过智能视频帧数计算算法（smart_nframes），用于<strong>确定从视频中提取多少帧作为模型输入</strong>，处理过程为：第一种直接通过<code>round_by_factor(ele["nframes"], FRAME_FACTOR)</code>来得到帧数；第二种处理方式为（FPS_MIN_FRAMES = 4、FRAME_FACTOR = 2、FPS_MAX_FRAMES = 768、FPS = 2.0）：</p><pre><code class="language-python">fps = ele.get("fps", FPS)
min_frames = ceil_by_factor(ele.get("min_frames", FPS_MIN_FRAMES), FRAME_FACTOR)
max_frames = floor_by_factor(ele.get("max_frames", min(FPS_MAX_FRAMES, total_frames)), FRAME_FACTOR)
nframes = total_frames / video_fps * fps
nframes = min(min(max(nframes, min_frames), max_frames), total_frames)
nframes = floor_by_factor(nframes, FRAME_FACTOR)

"""
config = {"nframes": 24}
result = smart_nframes(config, total_frames=100, video_fps=30)
# 输出：24（直接使用配置值）

config = {"fps": 10, "min_frames": 16, "max_frames": 32}
result = smart_nframes(config, total_frames=100, video_fps=30)
# 计算：100/30*10 ≈ 33.33 → 约束到32 → 对齐到32（FRAME_FACTOR=8的倍数）
"""
</code></pre><ul><li>2、<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/processing_qwen2_5_vl.py#L48">processor</a>：去将图片/文本进行编码</li></ul><p>其中对于文本编码直接通过 <code>self.tokenizer</code> 来处理，而对于图像直接通过 <code>self.image_processor</code>来处理。首先在 <a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/processing_qwen2_5_vl.py#L48">代码</a>中很容易看到使用的图像/文本处理方式<code>image_processor_class = "AutoImageProcessor"</code> 对于文本处理方式 <code>tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")</code>。<br/> 对于<strong>图片处理方式</strong>的 <code>Qwen2VLImageProcessor</code>（<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py#L87">代码</a>）的处理思路：</p><pre><code class="language-python">class Qwen2VLImageProcessor(BaseImageProcessor):
    def __init(...):
        ...
    def _preprocess(self, images, ...):
        ...
        height, width = get_image_size(images[0], channel_dim=input_data_format)
        resized_height, resized_width = height, width
        processed_images = []
        # Step-1
        for image in images:
            if do_resize:
                resized_height, resized_width = smart_resize(
                    height,
                    width,
                    factor=self.patch_size * self.merge_size,
                    min_pixels=self.min_pixels,
                    max_pixels=self.max_pixels,
                )
                image = resize(
                    image, size=(resized_height, resized_width), resample=resample, input_data_format=input_data_format
                )
            if do_rescale:
                image = self.rescale(image,...)
            if do_normalize:
                image = self.normalize(image,...)
        # Step-2
        patches = np.array(processed_images)
        if data_format == ChannelDimension.LAST:
            patches = patches.transpose(0, 3, 1, 2)
        if patches.shape[0] % self.temporal_patch_size != 0:
            # 视频补帧处理
            repeats = np.repeat(patches[-1][np.newaxis], self.temporal_patch_size - 1, axis=0)
            patches = np.concatenate([patches, repeats], axis=0)
        # 计算不同 patch 网格大小
        channel = patches.shape[1]
        grid_t = patches.shape[0] // self.temporal_patch_size
        grid_h, grid_w = resized_height // self.patch_size, resized_width // self.patch_size

        patches = patches.reshape(
            grid_t,
            self.temporal_patch_size,
            channel,
            grid_h // self.merge_size,
            self.merge_size,
            self.patch_size,
            grid_w // self.merge_size,
            self.merge_size,
            self.patch_size,
        )
        patches = patches.transpose(0, 3, 6, 4, 7, 2, 1, 5, 8)
        flatten_patches = patches.reshape(
            grid_t * grid_h * grid_w, channel * self.temporal_patch_size * self.patch_size * self.patch_size
        )

        return flatten_patches, (grid_t, grid_h, grid_w)
</code></pre><p>对于上面处理过程中，<strong>首先</strong>对于 <code>_preprocess</code>主要是对图像进行一些预处理：1、do_resize：改变图片大小（直接通过<code>smrt_resize</code>进行处理）2、do_rescale：像素缩减到0-1之间；3、do_normalize：对图片进行归一化处理（通道维度）；<strong>而后</strong>直接对于预处理后的图像直接进行切割处理为不同的patch输入到Vit中。<br/><strong>回顾一下QwenVL2.5的图片处理过程</strong>：首先是去对图片进行改变尺寸（保证图片最后可以整除patch_size）/缩放/归一化。而后就是直接将图片处理为vit能够处理的“序列输入”得到的维度为：<code>[grid_t * grid_h * grid_w, channel * temporal_patch_size(2) * patch_size(14) * patch_size(14)]</code>。</p><blockquote><p><strong>补充一</strong>：图片输入具体例子说明<br/> 假设默认参数为：patch_size= 14, temporal_patch_size= 2, merge_size= 2<br/> 图像输入为（通过process_vision_info提前处理之后的维度）：(1092, 1568) <br/> 首先计算 <code>resized_height, resized_width = smart_resize</code>得到 812 1176<br/> 首先计算：grid_t=1，grit_h=812//14=58，grid_w=1176//14=84那么计算得到为 4872另外一项为 1176也就是最后图像处理得到的输出为：<code>(1*58*84, 14*14*2*3)=(4872,1176)</code><br/><strong>补充二</strong>：对于 smart_resize快速估算最后大小：<br/> 先 round 到 factor 的倍数<br/> 如果超出 max_pixels → 除以 sqrt(HW/max_pixels)，floor → factor 倍数<br/> 如果小于 min_pixels → 乘以 sqrt(min_pixels/HW)，ceil → factor 倍数<br/> 其实也就是：<strong>首先将图像处理到为factor倍数的分辨率，而后去判断和max_pixels和min_pixels之间大小，大于前者就缩小，小于前者就放大</strong></p></blockquote><p>最后通过一系列编码之后得到输出：</p><pre><code class="language-python">inputs = processor(
    text=[text],
    images=image_inputs,
    videos=video_inputs,
    padding=True,
    return_tensors="pt",
)
"""
input_ids: torch.Size([1, 1243])
attention_mask: torch.Size([1, 1243])
pixel_values: torch.Size([4872, 1176])
image_grid_thw: torch.Size([1, 3])
"""
</code></pre><h3 id="3模型输入处理">3、模型输入处理</h3><pre><code class="language-python">generated_ids = model.generate(**inputs, max_new_tokens=128)
</code></pre><p>整体<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1724">模型</a>输入处理，输入模型也就是上面编码模板输入几个部分，只不过主要就是如下几个处理：首先是模型处理输入 <code>input_ids</code> 以及我的图像 <code>pixel_values</code>（<code>inputs_embeds = self.model.embed_tokens(input_ids)</code><a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1790">代码</a>），而后将输入进行位置编码处理（<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1838">代码</a>），最后输出模型结果（<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1861">代码</a>），对于QwenVL2.5完整模型结构：</p><pre><code class="language-python">Qwen2_5_VLForConditionalGeneration(
  (model): Qwen2_5_VLModel(
    (visual): Qwen2_5_VisionTransformerPretrainedModel(
      (patch_embed): Qwen2_5_VisionPatchEmbed(
        (proj): Conv3d(3, 1280, kernel_size=(2, 14, 14), stride=(2, 14, 14), bias=False)
      )
      (rotary_pos_emb): Qwen2_5_VisionRotaryEmbedding()
      (blocks): ModuleList(
        (0-31): 32 x Qwen2_5_VLVisionBlock(
          (norm1): Qwen2RMSNorm((1280,), eps=1e-06)
          (norm2): Qwen2RMSNorm((1280,), eps=1e-06)
          (attn): Qwen2_5_VLVisionAttention(
            (qkv): Linear(in_features=1280, out_features=3840, bias=True)
            (proj): Linear(in_features=1280, out_features=1280, bias=True)
          )
          (mlp): Qwen2_5_VLMLP(
            (gate_proj): Linear(in_features=1280, out_features=3420, bias=True)
            (up_proj): Linear(in_features=1280, out_features=3420, bias=True)
            (down_proj): Linear(in_features=3420, out_features=1280, bias=True)
            (act_fn): SiLU()
          )
        )
      )
      (merger): Qwen2_5_VLPatchMerger(
        (ln_q): Qwen2RMSNorm((1280,), eps=1e-06)
        (mlp): Sequential(
          (0): Linear(in_features=5120, out_features=5120, bias=True)
          (1): GELU(approximate='none')
          (2): Linear(in_features=5120, out_features=2048, bias=True)
        )
      )
    )
    (language_model): Qwen2_5_VLTextModel(
      (embed_tokens): Embedding(151936, 2048)
      (layers): ModuleList(
        (0-35): 36 x Qwen2_5_VLDecoderLayer(
          (self_attn): Qwen2_5_VLAttention(
            (q_proj): Linear(in_features=2048, out_features=2048, bias=True)
            (k_proj): Linear(in_features=2048, out_features=256, bias=True)
            (v_proj): Linear(in_features=2048, out_features=256, bias=True)
            (o_proj): Linear(in_features=2048, out_features=2048, bias=False)
            (rotary_emb): Qwen2_5_VLRotaryEmbedding()
          )
          (mlp): Qwen2MLP(
            (gate_proj): Linear(in_features=2048, out_features=11008, bias=False)
            (up_proj): Linear(in_features=2048, out_features=11008, bias=False)
            (down_proj): Linear(in_features=11008, out_features=2048, bias=False)
            (act_fn): SiLU()
          )
          (input_layernorm): Qwen2RMSNorm((2048,), eps=1e-06)
          (post_attention_layernorm): Qwen2RMSNorm((2048,), eps=1e-06)
        )
      )
      (norm): Qwen2RMSNorm((2048,), eps=1e-06)
      (rotary_emb): Qwen2_5_VLRotaryEmbedding()
    )
  )
  (lm_head): Linear(in_features=2048, out_features=151936, bias=False)
)
</code></pre><ul><li><strong>首先</strong>：对于视觉部分处理（<code>Qwen2_5_VisionTransformerPretrainedModel</code>）</li></ul><blockquote><p>对于视觉模型主要需要处理的就是 <code>pixel_values</code>，假设输入的 <code>pixel_values</code>信息为：<code>[4872, 1176]</code>，image_grid_thw为： [1, 84, 58]（就是对应grid_t、grid_h、grid_w这三个数值）</p></blockquote><p>主要包括如下几个模块：<br/> 1、<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L88">Qwen2_5_VisionPatchEmbed</a>：主要进行处理通过一个 <code>Conv3d</code>处理，处理<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L105C4-L111C29">过程</a>也就是说首先将输入的维度进行修改得到：<code>view(-1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size)</code> –&gt; (4872,1176)–&gt;(4872,3,2,14,14)而后再去通过卷积处理得到 (4872,1280,1,1,1)最后得到：<strong>(4872,1280)</strong>，也就对应着：<code>(grid_t*grid_h*grid_w, hiddend_size)</code>；<br/> 2、Qwen2_5_VisionRotaryEmbedding；<br/> 3、<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L233">Qwen2_5_VLVisionAttention</a>：首先去划分<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L465">window_size</a>这一步直接根据计算得到的：<code>[grid_t, grid_h, grid_w]</code>去划分windows，比如说在上述例子中，得到的cu_seqlens = [0,64,128,…,4872]，而后再去通过如下处理：</p><pre><code class="language-python">lengths = cu_seqlens[1:] - cu_seqlens[:-1]
splits = [
    torch.split(tensor, lengths.tolist(), dim=2) for tensor in (query_states, key_states, value_states)
]
</code></pre><p>去划分q、k、v（形状都为：[1, 16, 4872, 80]）然后计算注意力，而后通过<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L146">Qwen2_5_VLPatchMerger</a>将结果合并起来。<br/><strong>具体计算过程</strong>，首先是如何得到cu_seqlens，因为我们得到的gird_thw=(1, 84, 58)也就是说总共有84*58=4872个token去计算全局注意力，那么这就会导致计算注意力的消耗过大，因此可以先去切分成小的window然后小块内部注意力计算。因此首先计算“块”的大小：<code>vit_merger_window_size = self.window_size // self.spatial_merge_size // self.patch_size</code>得到结果为: 4（112/2/14）也就是说每块大小为：4x4=16，但是不一定我的grid_h和grid_w可能整除4，因此就需要去计算填充数量 <code>vit_merger_window_size - llm_grid_h % vit_merger_window_size</code> 分别得到 4和2因此填充后的h和w为：88,60这样一来计算得到window数量为：88//4 * 60//4=330每个窗口的tokens数量：16</p><h3 id="4图像处理过程总结">4、图像处理过程总结</h3><p><strong>总结上述图像处理过程</strong>：对于任意输入图像首先通过smart_resize（首先将图像改变到 factor的倍数，然后去判断和min_pixels和max_pixels之间大小，然后进行扩大，缩小）进行处理保证都可以整除patch_size（14）然后丢到 <code>processor</code>中进行处理主要是对图像归一化、正则化、改变维度（还会通过smart_resize在处理一次），处理之后再去确定他的 <code>grid_t, grid_h, grid_w</code>（对于这3个参数确定：直接通过 第二次smart_resize处理之后的结果除 patch_size即可）也就是tokens数量，而后将图像内容通过 conv3d处理得到：<code>(grid_t* grid_h* grid_w, hidden_size)</code>，最后就是计算window_attention（首先确定widow_size索引，通过索引进行切分，最后计算注意力）</p><blockquote><p>补充：对于window-attention可以用卷积的思路去理解，比如说我得到“图像”：<code>(grid_t, grid_h, grid_w)</code> 我提前计算我的“卷积核”大小（<code>vit_merger_window_size = self.window_size // self.spatial_merge_size // self.patch_size</code>）为了保证我的 “图像”可以被卷积核处理就需要做一部分填充，而后用这个“卷积核”去划分成不同“小块”在到这个小块里面计算注意力。</p></blockquote><h2 id="qwenvl的微调过程">QwenVL的微调过程</h2><p>所有的代码：<a href="https://github.com/shangxiaaabb/Docparse-QwenVL">https://github.com/shangxiaaabb/Docparse-QwenVL</a></p><blockquote><p><strong>补充一：节约显存可以进行的操作</strong><br/> 1、使用<code>gradient_checkpointing</code>：<code>model.gradient_checkpointing_enable()</code><br/> 2、使用 <code>qlora</code>进行优化<br/> 3、使用 <code>AdamW8bit</code> 而不是 <code>AdamW</code><br/> 4、使用 <code>xformers</code> （<code>model.enable_xformers_memory_efficient_attention()</code>），不过需要注意的是 QwenVL2.5不支持使用 <code>xformers</code>（除此之外安装也比较复杂）<br/> 5、避免显存碎片（不要过度的去评估模型），可以使用 <code>gc.collect() torch.cuda.empty_cache()</code> 去适当的减小缓存压力，对于不需要的内容（中间值）直接通过 <code>del xx</code> 处理掉</p></blockquote><h3 id="sft-处理">SFT 处理</h3><p>https://www.f22labs.com/blogs/complete-guide-to-fine-tuning-qwen2-5-vl-model/</p><h4 id="sft数据处理过程">SFT数据处理过程</h4><p>首先假设数据（通过jsonl进行存储）输入格式为：</p><pre><code class="language-json">{"image": 
    "845c2f9b-0583-4127-82a6-47c4c1c3ceb7.jpg", 
"prefix": 
    "QwenVL HTML", 
"suffix": 
    "&lt;body&gt;&lt;h2 data-bbox=......"
}
</code></pre><p>构建data_loader只需要注意如下几个流程即可：<br/><strong>首先构建我的输入模板</strong>。这一步主要是将我的数据进行读取，然后去构建成QwenVL2.5（或者其他大模型的对话形式），比如说：</p><pre><code class="language-python">def format_data(self, image, entry, text, prompt):
    return [
        {
            "role": "system",
            "content": [{"type": "text", "text": self.SYSTEM_MESSAGE}],
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "image": image,
                },
                {
                    "type": "text",
                    "text": (
                        "Must output the layout of the image strictly in HTML format. "
                        "Must follow the example below:\n"
                        "&lt;h2 data-bbox='x1 y1 x2 y2'&gt;Text&lt;/h2&gt;\n"
                        "&lt;p data-bbox='x1 y1 x2 y2'&gt;Text&lt;/p&gt;")
                },
            ],
        },
        {
            "role": "assistant",
            "content": [{"type": "text", "text": text}],
        },
    ]
</code></pre><p>然后就只需要将参数丢到这个函数里面就可以自动化的将数据处理好（补充一点，对于上面几个参数，一般来说其中<code>text</code>就是我的模型需要输出的label，而后其他的内容就是模型的输入），其次就只需要将<strong>输入进行编码</strong>即可也就是说直接通过：</p><pre><code class="language-python">image_inputs, _ = process_vision_info(messages)
encoding = self.processor(
    text=[text],
    images= image_inputs,
    return_tensors="pt",
    padding= False,
    truncation=True,
    max_length= self.max_length
)
</code></pre><p>这样就会的得到模型的输入内容，一般来说得到的是：<code>input_ids</code>: 文本编码内容（一般来说会直接将 input_ids进行复制作为我们的 labels，当然也可以直接对与输入解析，只需要模型那部分作为labels），<code>attention_mask</code>，<code>pixel_values</code>: 图片像素编码结果<code>image_grid_thw</code>: 我的tokens数量（<code>grid_t*grid_h*grid_w</code>）。<br/> 不过上面处理过程只是针对一张图片进行处理去构建对话信息，如果需要<strong>处理多组图片同时进行输入</strong>（比如说3张图片进行排序，让QwenVL输出）那么处理过程只需要修改 <code>content</code>即可（在content里面指定多个图片即可）</p><pre><code class="language-python">"content": [
            {
                "type": "image",
                "image": "./tmp/7.png",
            },
            {
                "type": "image",
                "image": "./tmp/1.png",
            },
            {"type": "text", "text": "..."},
        ],
</code></pre><h4 id="sft模型处理">SFT模型处理</h4><p>一般来说如果直接使用lora去对模型进行微调，处理也比较简答：</p><pre><code class="language-python">target_modules = ['q_proj', 'v_proj']
lora_config = LoraConfig(
    task_type= config.lora_task_type,
    target_modules= target_modules,
    r= config.lora_rank,
    lora_alpha= config.lora_alpha,
    lora_dropout= config.lora_dropout,
)
model = get_peft_model(model, lora_config)
</code></pre><p>这样一来模型就会被lora“包裹”，微调过程也就是优化lora的参数，不过如果需要使用<code>qlora</code>（lora量化版本）再模型加载过程中需要使用参数 <code>quantization_config</code>：</p><pre><code class="language-python">bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16
)
...
if model_name == 'Qwen/Qwen2.5-VL-3B-Instruct':
    model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
        model_name, 
        torch_dtype= torch.bfloat16, 
        cache_dir= config.cache_dir,
        quantization_config= bnb_config if config.lora_type== 'qlora' else None,
    )
</code></pre><p>对于模型训练以及参数优化过程就比较简单：</p><pre><code class="language-python">for step, batch in enumerate(train_loader):
    outputs = model(**batch)
    loss = outputs.loss
</code></pre><p>得到的所有的内容可以直接全部丢到model里面，他会自动计算loss值，对于<code>outputs = model(**batch)</code>模型<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L1397">返回</a>得到结果为：<br/><code>loss</code>: Optional[torch.FloatTensor]：模型计算得到的loss（直接计算交叉熵损失得到），如果输入内容中没有labels（就是模型输出那段文本）那么就不会去计算loss<br/><code>logits</code>: Optional[torch.FloatTensor]：模型输出结果<br/><code>past_key_values</code>: Optional[list[torch.FloatTensor]]：Transformer 解码器的 KV 缓存（每一层的注意力 key 和 value）<br/><code>hidden_states</code>: Optional[tuple[torch.FloatTensor]]：每一层的 hidden state (batch_size, seq_len, hidden_size)<br/><code>attentions</code>: Optional[tuple[torch.FloatTensor]]：每一层注意力权重 (batch_size, num_heads, seq_len, seq_len)<br/><code>rope_deltas</code>: Optional[torch.LongTensor]：旋转位置编码 RoPE（Rotary Position Embedding）的偏移量</p><h3 id="rl-处理">RL 处理</h3><blockquote><p>强化学习框架很多，1、huggingface-trl: <a href="https://github.com/huggingface/trl">https://github.com/huggingface/trl</a>；2、字节跳动-verl: <a href="https://github.com/volcengine/verl">https://github.com/volcengine/verl</a>；3、OpenRLHF：<a href="https://github.com/OpenRLHF/OpenRLHF">https://github.com/OpenRLHF/OpenRLHF</a></p></blockquote><p>强化学习处理过程（直接使用 trl（<strong>使用版本：0.22.1</strong>）库，它里面提供了<a href="https://github.com/huggingface/trl/blob/main/examples/scripts/dpo_vlm.py">多种脚本</a>）对于多模态/大语言模型使用RL中比较常见的的数据类型：一般就是抛出问题，而后给出选项让模型进行选择。此类数据集一般格式为：</p><pre><code class="language-python">{"images": [], "prompt": [], "chosen": [], "rejected": []}
# 当然这个 images 也可以替换为文本问题 "question"
</code></pre><p>比如说数据集：<a href="https://huggingface.co/datasets/HuggingFaceH4/rlaif-v_formatted/viewer/default/train?row=0&amp;views%5B%5D=train">HuggingFaceH4/rlaif-v_formatted</a>他的数据结构如下：<br/><img src="https://s2.loli.net/2025/09/05/O8E94bqdysHGxV6.webp" alt="image.png" width="1496" height="370" loading="lazy" decoding="async"/><br/> 直接看trl中如何实现<a href="https://github.com/huggingface/trl/blob/main/examples/scripts/dpo_vlm.py">QwenVL-DPO</a>过程代码：</p><pre><code class="language-python">from trl import (
    DPOConfig,
    DPOTrainer,
    ModelConfig,
    ScriptArguments,
    TrlParser,
    get_kbit_device_map,
    get_peft_config,
    get_quantization_config,
)
...
dataset = load_dataset(
    script_args.dataset_name,
    name=script_args.dataset_config,
    streaming=script_args.dataset_streaming,
)
...
# ref_model 和 model 都是直接使用QwenVL
trainer = DPOTrainer(
    model,
    ref_model,
    args=training_args,
    train_dataset=dataset[script_args.dataset_train_split],
    eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None,
    processing_class=processor,
    peft_config=peft_config,
)
</code></pre><p>初次之外，RL就和SFT一样需要让模型去按照我的数据进行输出，因此处理也就是直接<code>logits=model(**model_inputs).logits</code>得到模型最后输出（见相当于每个词的概率）</p><h4 id="rl-dpo处理代码">RL-DPO处理代码</h4><p>首先在代码（<code>DPOTrainer</code>）主要是通过继承 <code>Trainer</code>（<a href="https://huggingface.co/docs/transformers/en/main_classes/trainer">代码</a>包裹好了各种处理过程比如数据加载模型评估等各项处理过程）直接看 <code>DPOTrainer</code>里面的 <code>get_batch_loss_metrics</code>（完整模型输入然后输出loss）：</p><pre><code class="language-python">def get_batch_loss_metrics(self, model, batch, train_eval):
    ...
    if ...:
        ...
    else:
        model_output = self.concatenated_forward(model, batch)
        if "ref_chosen_logps" in batch and "ref_rejected_logps" in batch:
            ref_chosen_logps = batch["ref_chosen_logps"]
            ref_rejected_logps = batch["ref_rejected_logps"]
        else:
            ref_chosen_logps, ref_rejected_logps = self.compute_ref_log_probs(batch)

        losses = 0
        chosen_rewards = 0
        rejected_rewards = 0

        for idx, loss_type in enumerate(self.loss_type):
            _losses, _chosen_rewards, _rejected_rewards = self.dpo_loss(
                model_output["chosen_logps"],
                model_output["rejected_logps"],
                ref_chosen_logps,
                ref_rejected_logps,
                loss_type,
                model_output,
            )

            weight = self.loss_weights[idx] if self.loss_weights else 1.0
            losses = losses + _losses * weight
            chosen_rewards = chosen_rewards + _chosen_rewards * weight
            rejected_rewards = rejected_rewards + _rejected_rewards * weight
    return losses.mean(), ...
</code></pre><p>对于DPOTrainer里面data_loader处理过程为，首先对于 <code>dataset</code>会通过 <code>processing_class</code>（一般来说也就是对于文本直接使用 tokenizer，亦或者直接使用 <code>AutoProcessor.from_pretrained(...)</code>）进行处理，也就是说会提前将数据processor处理（和SFT处理方式相同）那么就会得到 <code>self.train_dataset</code>，那么接下来就是直接去通过<a href="https://github.com/huggingface/trl/blob/8534f0edf8608ad6bcbea9beefae380fa60ded77/trl/trainer/dpo_trainer.py#L455">代码</a>（加载train_loader数据），其中处理方式为：<code>ref_chosen_logp, ref_rejected_logp = self.compute_ref_log_probs(padded_batch)</code> 对于 <a href="https://github.com/huggingface/trl/blob/8534f0edf8608ad6bcbea9beefae380fa60ded77/trl/trainer/dpo_trainer.py#L758"><code>compute_ref_log_probs</code></a>里面处理过程为：直接去通过 model/ref_model去处理：<code>self.concatenated_forward</code>（<a href="https://github.com/huggingface/trl/blob/8534f0edf8608ad6bcbea9beefae380fa60ded77/trl/trainer/dpo_trainer.py#L961">代码</a>）得到模型输出： <code>model_output</code>，而后再去使用 <code>self.dpo_loss</code>去计算损失。</p><ul><li><code>self.concatenated_forward</code>处理过程 <a href="https://github.com/huggingface/trl/blob/8534f0edf8608ad6bcbea9beefae380fa60ded77/trl/trainer/dpo_trainer.py#L961">Github-代码</a>（实际解释使用 <strong>trl:0.22.1版本代码</strong>和github有差异）</li></ul><pre><code class="language-python">def concatenated_forward(model, batch, is_ref_model):
    concatenated_batch = self.concatenated_inputs(batch, padding_value=self.padding_value)
    prompt_input_ids = concatenated_batch["prompt_input_ids"]         # 问题文本
    prompt_attention_mask = concatenated_batch["prompt_attention_mask"]
    completion_input_ids = concatenated_batch["completion_input_ids"] # 回答文本 同时拼接了chosen_input_ids 和 rejected_input_ids
    completion_attention_mask = concatenated_batch["completion_attention_mask"]
    if self.is_encoder_decoder:
        labels = completion_input_ids
        labels[completion_attention_mask == 0] = self.label_pad_token_id
        outputs = model(
                    input_ids=prompt_input_ids,
                    attention_mask=prompt_attention_mask,
                    labels=labels,  # we need the labels for the logits to be returned
                    **model_kwargs,
                )
        logits = outputs.logits
        loss_mask = completion_attention_mask.bool()
    else:
        # Process-1
        input_ids = torch.cat((prompt_input_ids, completion_input_ids), dim=1)
        ...
        outputs = model(input_ids, **model_kwargs)
        logits = outputs.logits
    # Process-2
</code></pre><p><strong>Process-1</strong>：首先是将文本和回答进行拼接，而后去判断如果指定 <code>max_length</code>那么就去根据 <code>truncation_mode</code>（掐头/去尾：保留序列末尾，移除开头多余部分）去裁减输入以及移除填充和限制计算范围来优化内存和性能最后丢到模型中进行处理。</p><blockquote><p>掐头去尾过程<br/><code>keep_start</code>：保留序列开头。先调用 flush_left（<strong>所有有效的token左移动去除中间padding</strong>）。然后截断到 max_length（[:, :self.max_length]）。<code>[0, 0, x, x, x, x] → flush_left</code> 后 <code>[x, x, x, x]</code>，若 max_length=3，则截断为 <code>[x, x, x]</code><br/> keep_end：保留序列末尾。先调用 flush_right（<strong>将所有有效token向右移动，前面填充padding</strong>）。截断到最后 max_length 个 token（[:, -self.max_length:]）。再次调用 flush_left，确保左侧无填充。<code>[0, 0, x, x, x, x] → flush_right</code> 后 <code>[0, 0, x, x]</code>，截断后 <code>[x, x]</code>，flush_left 后保持不变。</p></blockquote><p>回顾一下<code>self.concatenated_forward</code>（模型处理）整个过程：首先是将<code>chosen_input_ids</code> 和 <code>rejected_input_ids</code>两部分进行<strong>拼接</strong>（<code>self.concatenated_inputs</code>做的，于此同时对于其他内容也都会拼接成两部分）作为<strong>我们模型的回答</strong>。而后丢到<strong>模型中进行处理</strong>（对于 <code>is_encoder_decoder</code> 可以直接给模型处理，如果不是那么就通过<strong>截断裁剪等处理来节约存储在由模型处理</strong>）得到 <code>logits</code>，去通过logits, label得到每个token的对数概率：<code>all_logps</code>，而后再去判断是否进行优化策略： <code>ipo</code> 或者 <code>ld_alpha</code>（长度去敏化）去优化得到的 <code>all_logps</code>（对其直接切分就可以得到：<code>chosen_logps</code> 和 <code>rejected_logps</code>）</p><ul><li><code>self.dpo_loss</code>计算损失过程 <a href="https://github.com/huggingface/trl/blob/8534f0edf8608ad6bcbea9beefae380fa60ded77/trl/trainer/dpo_trainer.py#L844">Github-代码</a>（实际解释使用 <strong>trl:0.22.1版本代码</strong>和github有差异）</li></ul><pre><code class="language-python">model_output = self.concatenated_forward(model, batch)
if "ref_chosen_logps" in batch and "ref_rejected_logps" in batch:
    # 直接使用数据里面的的结果
    ref_chosen_logps = batch["ref_chosen_logps"]
    ref_rejected_logps = batch["ref_rejected_logps"]
else:
    # 相对于直接在用模型处理一下得到结果
    ref_chosen_logps, ref_rejected_logps = self.compute_ref_log_probs(batch)
_losses, _chosen_rewards, _rejected_rewards = self.dpo_loss(
    model_output["chosen_logps"],
    model_output["rejected_logps"],
    ref_chosen_logps,
    ref_rejected_logps,
    loss_type,
    model_output,)
</code></pre><blockquote><p><code>if "ref_chosen_logps" in batch and "ref_rejected_logps" in batch:</code> 直接使用数据里面的结果过程一样的还是通过模型 <code>self.compute_ref_log_probs(batch)</code>（这个还是调用了 <code>self.concatenated_forward</code>）去得到chosen_logps 和 rejected_logps结果。<br/> 对于 dpo_loss 里面model_ 和 ref_ 这两部分理论上是两个不同的模型的输出结果，但是如果没有指定 ref_model 那么直接就都直接使用 model 即可</p></blockquote><p>对于DPO的loss处理过程就比较简单，在trl中提供3种计算方式：<br/><strong>1、Alpha散度计算</strong><br/><img src="https://s2.loli.net/2025/09/05/sonSkV1aNPZdD9H.webp" alt="image.png" width="687" height="488" loading="lazy" decoding="async"/></p><p><strong>2、KL散度计算</strong><br/><img src="https://s2.loli.net/2025/09/05/UOpNRbKQcxa18dL.webp" alt="image.png" width="595" height="485" loading="lazy" decoding="async"/></p><p><strong>3、JS散度计算</strong><br/><img src="https://s2.loli.net/2025/09/05/OhCBN8q7y4lzGtx.webp" alt="image.png" width="956" height="507" loading="lazy" decoding="async"/></p><p>在计算得到不同方式得到的结果：logits然后再去根据不同 <code>loss_type</code>去做处理（比如说：<code>loss_type == "sigmoid"</code> 处理过程为：<code>losses = (-F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing)- F.logsigmoid(-self.beta * logits) * self.label_smoothing)</code>）</p><h4 id="rl-dpo处理过程总结">RL-DPO处理过程总结</h4><p><strong>首先</strong>对于我们的数据集（假设为3元组：[问题, 接受回答, 拒绝回答]）首先就是去通过 <code>processor</code>（比如Qwen2.5vl可以直接 load）去编码我的所有内容（这一步和SFT过程相似），<strong>而后</strong>就是去通过<code>self.concatenated_forward</code>这个函数将我们的3元组进行拼接得到：[问题,问题], [接受回答, 拒绝回答]而后得到模型的输入为：[问题+接受回答, 问题+拒绝回答]，将输入直接交给的模型（由于见内容直接拼接起来，可能会优化模型的输入/出长度过长导致爆显存，因此输入之前会由一些裁剪处理操作）去得到输出：<code>logits</code>，而后通过logits, label得到每个token的对数概率：<code>all_logps</code>，（通过对<code>all_logps</code>进行拆分）就可以得到接受回答的值（<code>chosen_logps</code>），以及拒绝回答的值（<code>rejected_logps</code>），<strong>最后</strong>在得到这两部分值之后就是直接去计算loss。<br/> 对于loss计算过程（假设为KL散度）：$\mathrm{loss}=-\frac{1}{N}\sum_{i=1}^{N}\log\sigma\left(\beta\cdot((\log\pi_{\theta}(y_{w}|x)-\log\pi_{\theta}(y_{l}|x))-(\log\pi_{\mathrm{ref}}(y_{w}|x)-\log\pi_{\mathrm{ref}}(y_{l}|x)))\right)$。对于里面两项相减过程代码：</p><pre><code class="language-python">chosen_logratios = chosen_logps.to(device) - (not self.reference_free) * ref_chosen_logps.to(device)
rejected_logratios = rejected_logps.to(device) - (not self.reference_free) * ref_rejected_logps.to(device)
</code></pre><p><strong>反思</strong>：如果需要手搓一个DPO训练过程代码（需要借鉴<code>concatenated_forward</code>代码来辅助实现）</p><h4 id="rl-grpo处理代码">RL-GRPO处理代码</h4><p>官方实现<a href="https://github.com/huggingface/trl/blob/main/examples/scripts/grpo_vlm.py">代码</a>，对于DPO过程很容易发现一点在GRPO中直接不要<code>ref_model</code> 只是用一个model不过设计了一个<code>reward_function</code>。</p><ul><li><strong>数据处理过程</strong></li></ul><p>以官方代码为例（训练一个具有思考过程的多模态模型），在数据处理层面使用类似如下数据集<br/><img src="https://s2.loli.net/2025/09/05/3xYD4jFp5VsyPeI.webp" alt="image.png" width="1380" height="347" loading="lazy" decoding="async"/><br/> 以为需要设计一个“输出”思考过程的模型因此设计设计具有“思考”过程的prompt，最后输入模型数据格式为：</p><pre><code class="language-python"># 原始文本
{'image': &lt;PIL.PngImagePlugin.PngImageFile image mode=RGB size=147x86 at 0x7FF65C5776D0&gt;,
 'original_answer': ...,
 'original_question': ...,
 'problem': ...
 'prompt': [{'content': 'system-content',
             'role': 'system'},
            {'content': 'user-content',
             'role': 'user'}],
 'solution': "&lt;think&gt;...&lt;/think&gt;'
             '&lt;answer&gt;...&lt;/answer&gt;'}
# 初步处理后文本
{'The prompt Text: '
'&lt;|im_start|&gt;system\n systen-content &lt;|im_end|&gt;\n'
'&lt;|im_start|&gt;user\n user-content &lt;|im_end|&gt;\n'
'&lt;|im_start|&gt;assistant\n'}
# 模型最后得到的输出
output = {
            "prompt_ids": prompt_ids,
            "prompt_mask": prompt_mask,
            "completion_ids": completion_ids,
            "completion_mask": completion_mask,
            "advantages": advantages,
            "num_items_in_batch": num_items_in_batch,
        }
</code></pre><p>不过在得到类似上面数据集之后，不是直接丢到模型里面进行处理，在DPOTrainer中首先会去由<code>_prepare_inputs</code>（<a href="https://github.com/huggingface/trl/blob/67991605c0e6aaf1ef3c2bf64e11da914948c4a4/trl/trainer/grpo_trainer.py#L975">代码</a>）函数进行处理，对于测试直接通过函数 ` self._generate_and_score_completions(…)`处理，对于训练数据集</p><blockquote><p><code>_generate_and_score_completions</code>：<br/><strong>第一步、格式化数据</strong>。（对于多模态/只有文本）这个过程主要是争对我上面数据中的<code>prompt</code>直接通过模板进行处理得到<code>prompts_text</code>，而后就是直接再去通过 <code>processing_claa</code>（直接调用QwenVL的processor）处理得到<code>prompt_inputs</code>，而后就是如果<code>self.max_prompt_length</code>那么就会去对多模态（文字 + 图像）输入时，对 <code>prompt_inputs["input_ids"]</code>还原文本然后去除类似<code>&lt;pad&gt;</code>和一些重复/错误的 <code>&lt;image&gt;</code>得到干净的 <code>prompts_text</code>。<br/><strong>第二步、生成回答</strong>。在<code>trl</code>中使用了3种生成方式：1、直接用模型生成；2、使用vllm方式生成；3、使用use_transformers_paged方式。对于生成（直接通过模型）过程而言就比较简单直接将<code>prompt_inputs["input_ids"]</code> 和 <code>prompt_inputs["attention_mask"]</code> 丢到模型里面得到<code>prompt_completion_ids</code>再去将 prompt内容和回答截取出来得到 <code>prompt_ids</code> 和 <code>completion_ids</code><br/><strong>第三步、计算奖励值</strong>。这个过程就比较简单，直接将模型的回答进行解码再去通过奖励函数计算回答的奖励值，而后归一化成优势函数（<code>advantages</code>），按 group（一次生成多个样本）算均值，计算每个样本的 相对优势（比如说两个回答打分为 [0.8, 0.5]那么减去 group 内均值，假设为[+0.15, -0.15]）<br/><strong>最后、返回输出</strong>。<br/><img src="https://s2.loli.net/2025/09/05/f2loj6LEVUwr7Kg.webp" alt="image.png" width="572" height="353" loading="lazy" decoding="async"/><br/> 在最后返回的输出中 <code>old_per_token_logps</code> 和 <code>ref_per_token_logps</code>处理直接通过函数<code>_get_per_token_logps_and_entropies</code>（就相当于把 第二步得到的 <code>prompt_completion_ids</code>在交给模型里面去计算每个token的概率）</p></blockquote><ul><li><strong>奖励函数设计</strong></li></ul><p>GRPO没有使用ref_model转而使用奖励函数，对于奖励函数设计：<code>think_format_reward</code>， <code>accuracy_reward</code>。对于<code>accuracy_reward</code>很容易理解代码就是<strong>直接对比模型输出和答案之间是否正确</strong>（通过<code>parse</code> [<code>from math_verify import LatexExtractionConfig, parse, verify</code>] 去解析最后输出打答案然后对比两者之间是否正确）。对于<code>think_format_reward</code>：这个更加直接，直接去判断输出是不是有 <code>&lt;think&gt;...&lt;/think&gt;</code> 包裹（有=1，无/缺失=0）<br/> 当然不一定要使用自定义的（这么粗糙的）在DPOTrainer中对于<code>self.reward_funcs</code>（<a href="https://github.com/huggingface/trl/blob/18633dbb06ff6efc5099779592ba180d8ca767ea/trl/trainer/grpo_trainer.py#L290C9-L302C41">代码</a>）也可以直接去加载训练好的模型 <code>AutoModelForSequenceClassification.from_pretrained(...)</code></p><ul><li><strong>模型处理过程</strong></li></ul><p>直接去看loss计算过程：</p><pre><code class="language-python">def compute_loss(self, model, inputs, return_outputs, num_items_in_batch):
    ...
    if self.use_liger_loss:
        unwrapped_model = self.accelerator.unwrap_model(model)
        return self._forward_redirection(model, unwrapped_model, self.compute_liger_loss, unwrapped_model, inputs)
    else:
        return self._compute_loss(model, inputs)
</code></pre><p>其中使用了两种loss处理过程：<code>_forward_redirection</code> 以及 <code>_compute_loss</code>。</p><ul><li><code>self._compute_loss</code> 处理过程（<a href="https://github.com/huggingface/trl/blob/67991605c0e6aaf1ef3c2bf64e11da914948c4a4/trl/trainer/grpo_trainer.py#L1626">Github-代码</a>）（实际解释使用 <strong>trl:0.22.1版本代码</strong>和github有差异）</li></ul><p>首先是将输入问题和回答拼接起来，然后直接丢到<code>self._get_per_token_logps_and_entropies</code>（直接将数据丢到模型中，而后去截取模型输出中“真正回答”的内容）中进行处理得到<code>per_token_logps</code>（每个token的概率），<code>entropies</code>（每个token的信息熵），而后就是通过高熵去过滤token只在<strong>高熵位置计算 loss</strong>，而后就是<strong>计算KL散度</strong>（<code>torch.exp(inputs["ref_per_token_logps"] - per_token_logps) - (inputs["ref_per_token_logps"] - per_token_logps) - 1)</code>），避免新策略漂移太远</p><blockquote><p><code>self._get_per_token_logps_and_entropies</code>处理过程（<a href="https://github.com/huggingface/trl/blob/67991605c0e6aaf1ef3c2bf64e11da914948c4a4/trl/trainer/grpo_trainer.py#L786">Github-代码</a>）（实际解释使用 <strong>trl:0.22.1版本代码</strong>和github有差异）<br/> 其处理过程比较简单，直接将所有的数据都处理成模型输入（GRPO不想DPO那样需要将3元组进行拆开拼接）如：input_ids、pixel_values等然后直接<code>logits = model(**model_inputs).logits</code>在得到模型的输出之后后续就是对输出做一些截断处理（如只需要模型回答部分的输出<code>logits[:, -logits_to_keep:, :]</code>）而后去计算 <code>logits / self.temperature</code>（通过温度系数来确定输出内容多样化）最后再去通过：<code>logps = selective_log_softmax(logits, completion_ids)</code>（selective_log_softmax只去计算completion_ids部分的log_softmax值）就可以得到最后的值。</p></blockquote><h4 id="rl-grpo处理过程总结">RL-GRPO处理过程总结</h4><p><img src="https://s2.loli.net/2025/09/21/x45DlMb6QVPuh7r.webp" alt="1.png" width="4268" height="1936" loading="lazy" decoding="async"/><br/> 对于上面loss计算公式中主要就是如下几个值需要关注：1、advantage值；2、KL散度值。<br/> 因此简单总结一些GRPO代码处理过程<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>，<strong>首先</strong>，对于数据处理，这块内容比较简单直接 <strong>模板化</strong>、<strong>编码内容即可</strong>，因为GRPO是“一个问题抛出多组回答然后评估回答”，因此在数据处理过程中通过模型生成回答 <code>prompt_completion_ids=model.generate(...)</code>而后需要做的就是将生成内容进行拆分得到<code>prompt_ids</code>和 <code>completion_ids</code>（得到这一部分值之后就只需要在去还原成text文本然后再去通过reward函数去计算reward值以及计算最后需要的 <code>advantage</code>值），除此之外还会去通过model和model_ref分别计算回答中每个token的logits值：<code>old_per_token_logps</code> 和 <code>ref_per_token_logps</code></p><blockquote><p>这个过程直接通过函数 <a href="https://github.com/huggingface/trl/blob/67991605c0e6aaf1ef3c2bf64e11da914948c4a4/trl/trainer/grpo_trainer.py#L786">_get_per_token_logps_and_entropies</a>处理，他的处理思路简单直接将 model需要的内容再丢到model里面得到每个token的logits然后再去计算softmax值</p></blockquote><p>最后得到一个完整的output如下：</p><pre><code class="language-python">output = {
    "prompt_ids": prompt_ids,    # 问题token
    "prompt_mask": prompt_mask,
    "completion_ids": completion_ids,    # 问题的回答token
    "completion_mask": completion_mask,
    "advantages": advantages,
    "num_items_in_batch": num_items_in_batch,
    "old_per_token_logps": old_per_token_logps  
    "importance_sampling_ratio": importance_sampling_ratio  
    "ref_per_token_logps": ref_per_token_logps   
    "pixel_values": prompt_inputs["pixel_values"]   
    "image_grid_thw": prompt_inputs["image_grid_thw"]   
    "pixel_attention_mask": prompt_inputs["pixel_attention_mask"]   
    "image_sizes": prompt_inputs["image_sizes"]   
}

</code></pre><p><strong>而后</strong>，对于loss计算过程首先将上面output中的 问题+回答进行组合再丢到<code>_get_per_token_logps_and_entropies</code>中得到每个token概率以及熵的值：<code>per_token_logps</code>，<code>entropies</code>，而后就是：1、<strong>选择出高熵值的token</strong>（<code>entropy_mask</code>）；2、<strong>计算KL散度</strong>（<code>torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1</code>）；3、<strong>重要性采样权重</strong>：比较当前 log 概率和旧策略（<code>per_token_logps - old_per_token_logps</code>），得到 importance weight，做 clipping 限制。构造两个候选 loss（不裁剪和裁剪），取最小值，形成 <code>per_token_loss</code>再去乘上 entropy_mask和加上 KL 惩罚项就可以得到最后的loss值。</p><h4 id="rl-ppo处理代码">RL-PPO处理代码</h4><p>借用huggingface中对于PPO过程描述图：<br/><img src="https://s2.loli.net/2025/09/05/AvLeinFOo5lPV6z.webp" alt="image.png" width="8000" height="4500" loading="lazy" decoding="async"/><br/> 对于<a href="https://github.com/huggingface/trl/blob/1d06757e57723e85048ab7b061b12aac8895ca89/trl/trainer/ppo_trainer.py#L100">代码</a>使用，相比较GRPO和DPO要简单很多（不过在使用模型上，DPO和PPO都需要加载model和ref_model而GRPO只需要加载一个model），按照上面的处理过程：<br/><strong>首先</strong>计算rollout输出，直接通过加载的模型然后模型对于“问题”去得到“回答”<code>query_responses</code>（<strong>完整的模型生成内容</strong>：prompt+模型的回答），<code>logitss</code>，接下来（<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L473C21-L490C34">代码</a>）去计算model和ref_model中每个token的log概率值（这个过程和GRPO处理是一样的，将问题+回答拼接起来而后丢到模型中计算每个token的log概率值）最后分别得到模型的输出结果：<code>logprob</code><code>response</code>（截取model回答内容） 和 <code>ref_logprob</code>。后面部分（<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L492C21-L509C22">代码</a>）就是直接根据 <code>response</code>（model的回答） 以及 <code>query</code>（就是我们的问题）去计算reward的值<code>scores</code>。<br/> 接下来处理过程：1、处理 EOS 缺失惩罚：将socres中如果生成内容不含结束标记就从<code>scores</code>中减去数值；2、计算kl以及最后的rewards值，对kl直接首先通过mask去掩盖部分logprobs（ref_logprobs）然后直接通过 <code>kl = -(ref_logprobs - logprobs) if args.kl_estimator == "k1" else ((ref_logprobs - logprobs).exp() - 1) - logr</code>得到kl值；3、计算advantage值（<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L561">代码</a>）<br/> 最后就是迭代优化模型参数（<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L576C13-L654C34">代码</a>）这个过程（对采样得到的一批序列数据做多轮（num_ppo_epochs）小批次更新，通过 ratio = πθ/π_old 和裁剪（clip）来构造策略损失，同时对价值函数做裁剪的 value loss）主要是进行如下处理流程：首先是直接将最上面得到的<code>query_responses</code>中选择部分例子丢到模型中去计算每一个token的logits（ <code>new_logprobs = selective_log_softmax(logits, mb_responses) </code>） 而后计算策略损失值（<code>pg_loss</code>）以及vf_loss</p><blockquote><p>回顾一下，对于加载的<strong>llm在使用generate</strong>时一般返回如下4个值：<br/><code>sequences</code>：生成的 token ids（跟默认返回一样）；<br/><code>scores</code>：每一步的 logits（如果 output_scores=True）<br/><code>attentions</code>：注意力矩阵（如果 output_attentions=True）<br/><code>hidden_states</code>：隐藏层表示（如果 output_hidden_states=True）<br/> 一般而言使用到的主要是上面两项，对于<strong>第一项</strong><code>sequences</code>一般得到的完整的回答（prompt+模型生成的内容），所以一般会有一个截取处理（只需要记录<code>inputs['input_ids'].shape[1]</code>然后去截取即可）；对于<strong>第二项</strong><code>scores</code>一般得到的是通常是logits（需要去通过softmax计算才能得到token概率）；因此在GRPO和PPO中为了<strong>得到每一个token的log概率值</strong>，<code>logprob = selective_log_softmax(logits, response)</code>直接通过这种方式去计算来节约显存。<br/> 除此之外也有直接通过 <code>model(**model_inputs)</code>这样处理一般得到的是</p></blockquote><h4 id="rl-ppo处理过程总结">RL-PPO处理过程总结</h4><p><strong>第一阶段</strong>：首先是对于问题（<code>query</code>）通过丢到模型<code>batch_generation</code>中处理得到<code>query_responses</code>（完整问题+模型回答） 和<code>logitss</code>（每个token对应的概率），进一步将其得到回答token的概率值<code>logprob</code>（<code>selective_log_softmax</code>）同样的处理过程通过policy_model将<code>query_response</code>（从 <code>query_responses</code>挑选的）输入到模型进行处理同样的处理得到<code>ref_logprob</code>，最后就是通过<code>reward_model</code>去计算（<code>torch.cat((query, postprocessed_response), 1)</code>）得到奖励值。<br/><strong>第二阶段</strong>：<strong>kl值</strong>：直接计算<code>ref_logprobs - logprobs</code>（也就是计算上面阶段的ref_logprob和 logprob之间差值）；<strong>rewards值</strong>：直接copy计算的kl结果然后再序列的结尾补充上scores；<strong>advantage值</strong>：根据 reward 和 value，用 GAE 算 advantage。GAE计算过程：$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$ 和$A_t = \delta_t + \gamma \lambda A_{t+1}$最后计算<code>advantages + values</code>也就是 $R_t=A_t+V(s_t)$<br/><strong>第三阶段</strong>：进行迭代优化模型参数过程，优化过程首先是直接将小批次的<code>query_responses</code> 输入到模型中计算得到<code>output, vpred_temp</code>然后就是老操纵得到每个token的logits值<code>new_logprobs</code>，然后计算去计算<code>vf_loss</code>：计算loss1（<code>torch.square(vpred - mb_return)</code>）和loss2（<code>torch.square(vpredclipped - mb_return)</code>）的最大值。<code>pg_loss</code>：计算loss1（<code>-mb_advantage * ratio</code>）和loss2（<code>-mb_advantage * torch.clamp(ratio, 1.0 - args.cliprange, 1.0 + args.cliprange)</code>）的最大值然后取mean。最后得到loss为<code>pg_loss + args.vf_coef * vf_loss</code></p><blockquote><p>vpred、vpredclipped、mb_return分别通过从vpred_temp选择回答token、对vpred进行clamp裁剪、advantages + values</p></blockquote><h3 id="rl算法对比">RL算法对比</h3><h4 id="对比一下grpo和dpo的处理过程">对比一下GRPO和DPO的处理过程</h4><p><strong>DPO纯数据驱动过程</strong>，数据驱动：训练时需要标注好的偏好对：$[q, y^+], [q, y^-]$。计算流程：1. 输入同一个问题 $q$，分别拼接上正样本回答 $y^+$ 和负样本回答 $y^-$。2. 用当前模型和参考模型分别计算 $\log \pi_\theta(y^+|q), \log \pi_\theta(y^-|q), \log \pi_{\text{ref}}(y^+|q), \log \pi_{\text{ref}}(y^-|q)$。3. 基于这 4 个 log-prob，直接计算一个 logistic 回归式的 loss，强制模型在正样本上比分数更高，在负样本上比分数更低。<br/><strong>GRPO生成驱动过程</strong>，生成驱动：训练时只给定问题 prompt，模型自己 roll-out 多个回答。计算流程：1. 对每个问题生成 $G$ 个回答。2. 通过奖励函数（或打分器）给每个回答打分 $r_i$。3. 组内归一化奖励 → 得到 advantage 值 $A_i$（比组内平均好/差多少）。4. 用参考模型计算 ref_per_token_logps（使用ref_model生成没有的话直接用model代替ref_model）。5. 用旧策略（冻结一帧的当前模型）得到 old_per_token_logps（直接通过model生成）。6. 用当前模型得到 per_token_logps。7. 计算重要性比率和 KL 散度（使用per_token_logps和ref_per_token_logps计算）近似，再套 PPO 风格的剪切目标（使用old_per_token_logps和per_token_logp） → 最终 loss。</p><h4 id="对于dpogrpoppo中kl计算差异">对于DPO、GRPO、PPO中KL计算差异</h4><blockquote><p>$KL(p||q)=\sum_x p(x)\log\frac{p(x)}{q(x)}=H(p,q)-H(q)$，交叉熵-熵<br/> 计算交叉熵的目的在于<strong>约束新策略不要偏离参考策略太多</strong>，类似的对于交叉熵损失（$H(p,q)=-\sum_x p(x)\log q(x)$）两者之间差异是交叉熵是让“q去拟合p”，而KL则是度量“q和p之间距离”</p></blockquote><p><strong>1、DPO中计算KL</strong>：在model_ref以及model分别输入“3元组”数据之后会去计算不同token的概率值，也就是model和ref都会生成 reject和choose的概率值，然后去计算：$\mathrm{loss}=-\frac{1}{N}\sum_{i=1}^{N}\log\sigma\left(\beta\cdot((\log\pi_{\theta}(y_{w}|x)-\log\pi_{\theta}(y_{l}|x))-(\log\pi_{\mathrm{ref}}(y_{w}|x)-\log\pi_{\mathrm{ref}}(y_{l}|x)))\right)$ 的sigmoid 损失优化相对偏好<br/><strong>2、GRPO中计算KL</strong>：通过model_ref对于问题Q以及模型生成的多组回答进而可以得到每组回答的token概率：<code>ref_per_token_logps</code> 而后我又通过model去生成多组回答以及token概率：<code>per_token_logps</code>接下来就是直接他们之间KL散度：<br/><img src="https://s2.loli.net/2025/09/21/UwmkqNA42lgvzWy.webp" alt="image" width="645" height="81" loading="lazy" decoding="async"/><br/><strong>3、PPO中计算KL</strong>：通过model得到回答中的每一个token的概率<code>logprobs</code>，同样的再去通过model_rf也去计算每一个token的概率<code>ref_logprobs</code>然后去计算KL<br/><img src="https://s2.loli.net/2025/09/21/EsyjUOIolMTDJHm.webp" alt="image" width="534" height="67" loading="lazy" decoding="async"/><br/> DPO：通过“偏好差值”间接引入 KL 约束，偏重于 对比学习。<br/> GRPO：显式计算 生成候选组的 token 级 KL，作为正则项，保证模型不偏离参考策略。<br/> PPO：基于当前策略与参考策略（或旧策略）的 KL，常作为 正则或 early stopping 信号</p><h4 id="对于grpo以及ppo中优势值计算过程">对于GRPO以及PPO中优势值计算过程</h4><p><strong>GRPO优势值计算过程</strong>：对于给出多组回答直接通过奖励函数去计算每组回答的奖励值而后去上计算：$A_i = \frac{r_i- mean(r)}{std(r)}$<br/><strong>PPO优势值计算过程</strong>：一般直接通过广义优势估计方法GAE来计算优势值，首先通过奖<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L503">励函数评估模型输出</a>（问题+回答），而后<a href="https://github.com/huggingface/trl/blob/9955ee7eaa7e361ef46f7ac26b5ddc79199811f8/trl/trainer/ppo_trainer.py#L561C17-L569C76">计算GAE</a></p><h4 id="对比dpogrpoppo中loss计算差异">对比DPO、GRPO、PPO中loss计算差异</h4><p>DPO的loss计算：<br/> \(\mathcal{L}_{\text{DPO}} = -\frac{1}{N} \sum_{i=1}^{N} \log \sigma\left( \beta \underbrace{\left[ \log \pi_\theta(y_w|x) - \log \pi_\theta(y_l|x) \right]}_{\text{model 之间差异}} - \underbrace{\left( \log \pi_{\text{ref}}(y_w|x) - \log \pi_{\text{ref}}(y_l|x) \right)}_{\text{隐含 KL 基准}} \right)\)<br/> GRPO的loss计算：<br/> \(\mathcal{L}_{\text{GRPO}} = -\mathbb{E}\left[ \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)} A(y) \right] + \lambda \, \mathrm{KL}\left( \pi_\theta \parallel \pi_{\text{ref}} \right)\)<br/> PPO的loss计算：<br/> \(r_t(\theta) = \exp\left( \log \pi_\theta(a_t|s_t) - \log \pi_{\text{ref}}(a_t|s_t) \right)\)</p> \[\mathcal{L}_{\text{PPO}} = -\mathbb{E}\left[ \min\left( r_t(\theta) A_t, \, \mathrm{clip}\left(r_t(\theta), \, 1 - \epsilon, \, 1 + \epsilon\right) A_t \right) \right] + \lambda \, \mathrm{KL}\left( \pi_\theta \parallel \pi_{\text{ref}} \right)\] <h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://huggingface.co/docs/trl/main/en/grpo_trainer <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>多模态</category><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html</guid></item><item><title>开源模型技术总结-1————Qwen系列模型</title><link>https://www.big-yellow-j.top/posts/2025/08/28/OpenModelQwen.html</link><description><![CDATA[通义千问多模态系列QwenVL迭代脉络清晰，初代采用ViT-bigG视觉编码器、单层交叉注意力模块配合可学习查询，将视觉特征压缩为256长度输入7B基座大模型。QwenVL2支持动态分辨率，引入2×2相邻token拼接、多模态旋转位置编码M-RoPE，新增时间维度对齐视频处理逻辑。QwenVL2.5替换RMSNorm、SwiGLU激活，新增窗口注意力机制。QwenVL3推出MRoPE-Interleave、DeepStack多层特征注入技术，提升长视频理解与图文对齐精度。QwenVL3.5采用3:1比例混合线性与全注意力，引入门控注意力机制，降低计算复杂度同时优化长序列推理效果。]]></description><content:encoded><![CDATA[<h2 id="qwen多模态系列模型">Qwen多模态系列模型</h2><h3 id="qwenvl">QwenVL</h3><p>在QwenVL<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">1</a></sup>中在论文里面作者提到的其模型的整个训练过程如下：<br/><img src="https://s2.loli.net/2025/09/21/HEhlRPFJBMKpjoZ.webp" alt="image" width="814" height="308" loading="lazy" decoding="async"/></p><blockquote><p>仅从提供的不同阶段还是很容易发现QwenVL还是是采用和BLIP相似的使用 learned-query来对齐模态信息<br/> 语言模型使用（7.7B）：Qwen-7B<br/> 视觉编码器（1.9B）：Vit-bigG<br/> 融合器（0.08B）：Learnable Query</p></blockquote><p>不过论文里面对于模型细节介绍不是很多，从代码角度出发窥其模型结构：<br/><strong>模型视觉编码器</strong>：视觉编码器使用的是ViT架构（Vision Transformer），ViT的网络设置和初始化参数使用了OpenCLIP预训练好的<strong>ViT-bigG模型</strong>。具体的代码处理过程（<a href="https://huggingface.co/Qwen/Qwen-VL/blob/main/visual.py">代码</a>），其中模型输出维度变化过程：1x3x448x448–&gt;1x1664x32x32（首先卷积处理）–&gt;1x1024x1664（拉平交换维度）<br/><strong>特征融合器</strong>：上述ViT处理后，对于$448\times 448$分辨率的图像，生成一个 <strong>[1024, 1664]</strong>的序列，也就是向量维度为1664的长度为1024的序列。为了压缩视觉token的输入长度，Qwen-VL引入了一个Adapter来压缩图像特征。这个Adaper就是一个随机初始化的单层Cross-Attention模块。该模块使用一组可学习的query向量，将来自ViT的图像特征作为Key向量。通过Cross-Attention操作后将视觉特征序列压缩到固定的256长度（也就是将视觉特征压缩到 <strong>256 1644</strong>）<br/> 此外，考虑到位置信息对于精细图像理解的重要性，Qwen-VL将二维绝对位置编码（三角位置编码）整合到Cross-Attention的 $q,k$中，以减少压缩过程中可能丢失的位置细节。随后将长度为256的压缩图像特征序列输入到大型语言模型中。</p><h3 id="qwenvl-2">QwenVL-2</h3><p>对于QwenVL-2<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup>其模型的基本结构如下：<br/><img src="https://s2.loli.net/2025/09/21/5c1jovnLVOaS62H.webp" alt="image" width="776" height="498" loading="lazy" decoding="async"/></p><p><strong>1、使用动态分辨率</strong>（也就是说输入图像不需要再去改变图像尺寸到一个固定值），于此同时为了减少 <strong>visual-token</strong>数量，将<strong>2x2的的相邻的token进行拼接</strong>到一个token而后通过MLP层进行处理。<br/><img src="https://s2.loli.net/2025/09/21/w3agENHmLVcoSdt.webp" alt="image" width="1235" height="277" loading="lazy" decoding="async"/></p><p><strong>动态分辨率</strong>处理如上，通过指定<code>[mix_pixels, max_pixels]</code>范围然后将图像保持原始的纵横比去缩减图像到上面的范围中（<a href="https://github.com/QwenLM/Qwen2.5-VL/blob/c15045f8829fee29d4b3996e068775fe6a5855db/qwen-vl-utils/src/qwen_vl_utils/vision_process.py#L59">处理过程</a>，首先计算原始图像的像素数量，而后判断和上面指标的范围，如果超出范围就去计算需要修改的比例，在将整个比例去处理到分辨率上）<br/> 在通过使用动态分辨率处理图像之后会在单一<strong>图片增加时间维度</strong>也就是将：CHW–&gt;TCHW（这点是为了和视频处理过程进行对齐），在源码中T选择数值为2也就是将图片“复制一次”，而后对帧序列进行Patchification操作</p><pre><code class="language-python">def _preprocess(): 
    ......   
    channel = patches.shape[1]
    grid_t = patches.shape[0] // self.temporal_patch_size
    grid_h, grid_w = resized_height // self.patch_size, resized_width // self.patch_size
    patches = patches.reshape(
        grid_t,                            # 0
        self.temporal_patch_size, channel, # 1 2
        grid_h // self.merge_size,         # 3
        self.merge_size, self.patch_size,  # 4 5
        grid_w // self.merge_size,         # 6
        self.merge_size, self.patch_size,  # 7 8
    ) # self.merge_size=2 self.patch_size=14 self.temporal_patch_size=2
    ### 将2x2的邻域Patch放到一起，方便后续做领域的Patch过Projector层做聚合压缩
    patches = patches.transpose(0, 3, 6, 4, 7, 2, 1, 5, 8)
    ### Patch序列化，并保留Patch位置信息（时间，高，宽）
    flatten_patches = patches.reshape(
        grid_t * grid_h * grid_w, channel * self.temporal_patch_size * self.patch_size * self.patch_size
    )
</code></pre><p>上面过程也就是进行所谓的“2x2的相邻token拼接”，最后得到<code>[grid_t * grid_h * grid_w, channel * temporal_patch_size(2) * patch_size(14) * patch_size(14)]</code>（其中<code>grid_h=resized_height // self.patch_size(14)</code>）<br/> 2、<strong>多模态的旋转位置编码（M-RoPE）</strong>,也就是将原来位置编码所携带的信息处理为：时序（temporal）、高度（height）、宽度（width）。比如下图中对于文本处理直接初始化为：$(i,i,i)$。但是对于图片而言就是：$(i,x,y)$ 其中 $i$ 是恒定的，而对于视频就会将 $i$ 换成视频中图像的顺序<br/><strong>总结处理过程</strong>：动态分辨率处理–&gt;复制时间维度–&gt;将序列切割为patch。这样一来就会直接将图像处理为：<code>[grid_t * grid_h * grid_w, channel * temporal_patch_size(2) * patch_size(14) * patch_size(14)]</code>（其中<code>grid_h=resized_height // self.patch_size(14)</code>）除此之外而后去计算 3d-RoPE最后通过一层线性层处理就得到最后的视觉token。</p><h3 id="qwenvl-25">QwenVL-2.5</h3><p>在QwenVL2.5中<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">3</a></sup>模型具体的代码处理过程参考Blog<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">4</a></sup>具体模型结构：<br/><img src="https://s2.loli.net/2025/09/21/R8yLfVqpznvkgZw.webp" alt="image" width="1159" height="762" loading="lazy" decoding="async"/><br/> 在图像处理过程上和QwenVL2差异不大都是直接：动态分辨率处理–&gt;复制时间维度–&gt;将序列切割为patch，对比两个模型差异：<br/><img src="https://s2.loli.net/2025/09/22/NvKgQqhC36WAkjU.webp" alt="image" width="2992" height="1632" loading="lazy" decoding="async"/><br/> 1、采用 RMSNorm 替换了所有 LayerNorm；2、ViT中每一个VisionBlock中的MLP换成了SwiGLU 结构。只从模型结构上差异不到，在QwenVL2.5中主要进行改动：1、在视觉编码过程中使用window-attention（对应上述结构中的<code>Qwen2_5_VLVisionAttention</code>）对于具体的划分window方法（<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L465">代码</a>）：根据输入的图像大小 (gird_t, grid_h, grid_w)去得到窗口索引 (window_index) 和 累积序列长度 (cu_window_seqlens)。具体例子如下：</p><pre><code class="language-python"># 数据数据特征
[ [ 0,  1,  2,  3,  4,  5],
  [ 6,  7,  8,  9, 10, 11],
  [12, 13, 14, 15, 16, 17],
  [18, 19, 20, 21, 22, 23],
  [24, 25, 26, 27, 28, 29],
  [30, 31, 32, 33, 34, 35] ]
# 保证可以被window_size划分需要进行填充
[ [ 0,  1,  2,  3,  4,  5, X, X],
  [ 6,  7,  8,  9, 10, 11, X, X],
  [12, 13, 14, 15, 16, 17, X, X],
  [18, 19, 20, 21, 22, 23, X, X],
  [24, 25, 26, 27, 28, 29, X, X],
  [30, 31, 32, 33, 34, 35, X, X],
  [ X,  X,  X,  X,  X,  X, X, X],
  [ X,  X,  X,  X,  X,  X, X, X] ]
# 而后直接更具window大小得到每个需要计算注意力的window
# window-0
[ 0,  1,  2,  3]
[ 6,  7,  8,  9] 
[12, 13, 14, 15]
[18, 19, 20, 21]
# 展平重新排列得到：
# window-0
[0, 1, 2, 3, 6, 7, 8, 9, 12, 13, 14, 15, 18, 19, 20, 21]
# window-1 
[4, 5, 10, 11, 16, 17, 22, 23]
# 计算累计长度
seqlens = (index_padded != -100).sum([2, 3]) # 计算有效长度：window-0：16 window-1：8.....
cu_seqlens_tmp = seqlens.cumsum(0) * 4 + cu_window_seqlens[-1]
cu_window_seqlens.extend(cu_seqlens_tmp.tolist())
# [0, 64, 96, 128, 144]
# 得到最后返回结果window_index, cu_window_seqlens
</code></pre><p>在得到window_index和cu_window_seqlens之后就是<a href="https://github.com/huggingface/transformers/blob/41925e42135257361b7f02aa20e3bbdab3f7b923/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L267C9-L275C100">计算注意力过程</a></p><pre><code class="language-python">for i in range(1, len(cu_seqlens)):
  attention_mask[..., cu_seqlens[i - 1] : cu_seqlens[i], cu_seqlens[i - 1] : cu_seqlens[i]] = 0

q = q.transpose(0, 1)
k = k.transpose(0, 1)
v = v.transpose(0, 1)
attn_weights = torch.matmul(q, k.transpose(1, 2)) / math.sqrt(self.head_dim)
attn_weights = attn_weights + attention_mask
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(q.dtype)
</code></pre><h3 id="qwenvl-3">QwenVL-3</h3><p>在官方Blog<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">5</a></sup>的介绍中<br/><img src="https://files.seeusercontent.com/2026/05/07/aEf9/20260226135106.webp" alt="20260226135106"/><br/> 对于模型架构的更新简单总结为：1、<strong>MRoPE-Interleave</strong>: 改进位置编码，采用时间(t)、高度(h)、宽度(w)交错分布形式，提升对长视频的理解能力。2、<strong>DeepStack 技术</strong>: 融合 ViT 多层次特征，将视觉特征注入 LLM 的多层中，实现更精细化的视觉理解和图文对齐精度。3、<strong>文本时间戳对齐机制 (T-RoPE 升级)</strong>: 采用“时间戳-视频帧”交错输入形式，实现帧级别时间信息与视觉内容的细粒度对齐，提升视频事件定位精度。整体模型结构在区别上一代QwenVL-2.5改进点在于：patch_embed的patch_size变大了（14-&gt;16），embed使用的三维卷积里加了bias，ViT的隐层维度hiddeen_dim从1280-&gt;1152，而后使用DeepStack、MRoPE-Interleave。</p><ul><li><strong>DeepStack 技术原理</strong></li></ul><p>从最上面的模型结构图中可以发现DeepStack就是将视觉视觉编码器特征融入到LLM Block的每一层中，参考论文中的结构图<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">6</a></sup>:<br/><img src="https://files.seeusercontent.com/2026/05/07/3fiQ/20260226135226.webp" alt="20260226135226"/><br/> 之所以要使用该技术是为了解决：<strong>计算与内存开销过高</strong>:传统LMMs将所有视觉visual tokens拼接成一维序列输入到语言模型的第一层，导致需要处理的输入序列长度显著增加，尤其在处理高分辨率图像或多帧视频时，计算和内存成本急剧上升。<strong>细粒度视觉信息丢失</strong>:现有方法通过压缩视觉Token(如空间池化、感知器重采样等)来平衡计算开销与信息保留，但会牺牲高分辨率图像中的细节信息。<strong>视觉与语言交互效率不足</strong>:现有方法仅通过第一层Transformer处理所有视觉Token，未能充分利用语言模型深层结构的层次化特征提取能力。</p><h4 id="源码结构">源码结构</h4><p>对于具体源码（<a href="https://github.com/huggingface/transformers/blob/0419ff881d7bb503f4fc0f0a7a5aac3d012c9b91/src/transformers/models/qwen3_vl/modular_qwen3_vl.py">代码</a>）分析整体模型处理过程如下（<a href="https://github.com/huggingface/transformers/blob/0419ff881d7bb503f4fc0f0a7a5aac3d012c9b91/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L885">代码</a>）</p><blockquote><p><strong>值得注意的是在输入数据预处理阶段QwenVL-3和2.5的处理是相同的通过smart_resize去修改分辨率</strong></p></blockquote><pre><code class="language-python">class Qwen3VLModel(Qwen3VLPreTrainedModel):
  ...
  def __iniit__(...):
    super().__init__(config)
    self.visual = Qwen3VLVisionModel._from_config(config.vision_config)
    self.language_model = Qwen3VLTextModel._from_config(config.text_config)
    self.rope_deltas = None  # cache rope_deltas here
    self.post_init()
  def forward(...):
    ...
    # 图像处理过程
    if pixel_values is not None:
      image_embeds, deepstack_image_embeds = self.get_image_features(pixel_values, image_grid_thw)
      image_embeds = torch.cat(image_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype)
      image_mask, _ = self.get_placeholder_mask(
          input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds
      )
      inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
    ...
    outputs = self.language_model(...,inputs_embeds=inputs_embeds,...)
</code></pre><ul><li><code>get_image_features</code>处理过程：通过Qwen视觉编码其处理并且获取特定层视觉编码特征</li></ul><p>通过视觉编码处理得到<code>image_embeds</code>和 <code>deepstack_image_embeds</code>而后再去对 <code>image_embeds</code>进行裁剪，裁剪的逻辑为：<code>split_sizes = (image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist();image_embeds = torch.split(image_embeds, split_sizes)</code> 回到<code>self.visual</code>中模型具体处理过程如下（<a href="https://github.com/huggingface/transformers/blob/0419ff881d7bb503f4fc0f0a7a5aac3d012c9b91/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L701">代码</a>）：</p><pre><code class="language-python"># https://github.com/huggingface/transformers/blob/0419ff881d7bb503f4fc0f0a7a5aac3d012c9b91/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L701
class Qwen3VLVisionModel(Qwen3VLPreTrainedModel):
  def __init__(...):
    ...
    self.blocks = nn.ModuleList([Qwen3VLVisionBlock(config) for _ in range(config.depth)])
    self.merger = Qwen3VLVisionPatchMerger(...)
    self.deepstack_visual_indexes = config.deepstack_visual_indexes
    self.deepstack_merger_list = nn.ModuleList(
        [
            Qwen3VLVisionPatchMerger(
                config=config,
                use_postshuffle_norm=True,
            )
            for _ in range(len(config.deepstack_visual_indexes))
        ]
    )
  def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, **kwargs):
    ... # 对图像数据通过 patch_embed 进行处理而后补充位置编码
    # Vit处理
    deepstack_feature_lists = []
    for layer_num, blk in enumerate(self.blocks):
        hidden_states = blk(...)
        if layer_num in self.deepstack_visual_indexes:
            deepstack_feature = self.deepstack_merger_list[self.deepstack_visual_indexes.index(layer_num)](hidden_states)
            deepstack_feature_lists.append(deepstack_feature)
    hidden_states = self.merger(hidden_states) # 直接通过两层fc进行处理
    return hidden_states, deepstack_feature_lists
</code></pre><p><code>patch_embed</code>就是直接使用3维卷积（bias为True）：<code>Conv3d(3, 1152, kernel_size=(2, 16, 16), stride=(2, 16, 16))</code>（维度上对应：<code>(grid_t*grid_h*grid_w, hiddend_size)</code>），对于上述DStack过程中也比较好理解直接从需要处理的每层（通过Qwen3VLVisionBlock总共由27层叠加）中挑选出对应的处理后的特征，直接挑选[8, 16, 24]层处理后的特征。</p><blockquote><p>在 ViT 模型的预训练阶段，通常使用固定的输入分辨率（例如 224×224），并将其划分为固定数量的 patch（例如 14×14，共 196 个 patch）。这意味着模型内部的 pos_embed 是一个固定长度的可学习参数矩阵，模型在训练过程中已经隐式地学习到了这些位置编码之间的空间关系。当推理阶段输入的分辨率发生变化时，如果直接重新计算或生成新的位置编码，就会破坏模型在预训练阶段学到的空间语义信息，从而导致性能下降。因此，QwenVL-3 等模型的做法是：<strong>固定一套在预训练阶段学习到的位置编码</strong>，在输入新的分辨率时，不重新生成编码，而是通过 <strong>双线性插值</strong> 将原始位置编码映射到新的空间尺度上，从而在保持预训练空间结构的前提下，适配不同输入尺寸。换句话说，新的 patch 位置不再重新计算 embedding，而是通过插值在原有位置编码上“找到”其对应的空间位置。</p><ul><li>llm处理过程：直接将视觉token位置上补充我的DeepStack特征</li></ul></blockquote><pre><code class="language-python"># https://github.com/huggingface/transformers/blob/0419ff881d7bb503f4fc0f0a7a5aac3d012c9b91/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L760
class Qwen3VLTextModel(Qwen3VLPreTrainedModel):
    def __init__(self, config: Qwen3VLTextConfig):
        super().__init__(config)
        self.padding_idx = config.pad_token_id
        self.vocab_size = config.vocab_size

        self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
        self.layers = nn.ModuleList(
            [Qwen3VLTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
        )
        self.norm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
        self.rotary_emb = Qwen3VLTextRotaryEmbedding(config=config)
        self.gradient_checkpointing = False

    def forward(...,input_ids: Optional[torch.LongTensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,...
        visual_pos_masks: Optional[torch.Tensor] = None,
        deepstack_visual_embeds: Optional[list[torch.Tensor]] = None,
    ):
        ...
        for layer_idx, decoder_layer in enumerate(self.layers):
            layer_outputs = decoder_layer(...) # 模型解码输出
            hidden_states = layer_outputs

            if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)):
                hidden_states = self._deepstack_process(
                    hidden_states,
                    visual_pos_masks,
                    deepstack_visual_embeds[layer_idx],
                )
        hidden_states = self.norm(hidden_states)
        ...
    def _deepstack_process(
        self, hidden_states: torch.Tensor, visual_pos_masks: torch.Tensor, visual_embeds: torch.Tensor
    ):
        visual_pos_masks = visual_pos_masks.to(hidden_states.device) # 形状 batch_size, seqlen
        visual_embeds = visual_embeds.to(hidden_states.device, hidden_states.dtype)
        local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds
        hidden_states[visual_pos_masks, :] = local_this
        return hidden_states
</code></pre><p>其实从上面代码中很容易发现在DeepStack中QwenVL-3处理方式很简单直接选出<strong>所有视觉token位置</strong>而后将视觉特征进行补充，其中visual_pos_masks的形状是batch_size, seqlen</p><h3 id="qwen-35">Qwen-3.5</h3><p>简单总结模型主要亮点在于：1、<strong>使用linear-attention+full+attention的混合</strong>，其中整体的混合比例3：1（3层linear attention后叠加1层full attention）比如说<br/><img src="https://files.seeusercontent.com/2026/05/07/nS8g/image20260308150901627.webp" alt=""/></p><p>2、除此之外引入门控注意力计算（Gate-Attention<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">7</a></sup>）主要过程式在计算QKV三部分的注意力之后引入Gate机制。在代码中的具体实现过程为代码为（图片来源<a href="https://zhuanlan.zhihu.com/p/2006241509226350575">知乎</a>）：<br/><img src="https://files.seeusercontent.com/2026/05/07/3xvY/image20260308164431758.webp" alt=""/></p><p>对于上述过程中的QKV和常规的Attention中计算没差异，关键在于其引入了z、b、a这三组变量，其中b、a主要是用在Gate计算中而z则是在最后计算完attention之后再去<code>self.norm(core_attn_out, z)</code>，在计算门控过程中代码过程如下</p><pre><code class="language-python">core_attn_out = torch.zeros(batch_size, num_heads, sequence_length, v_head_dim).to(value)
last_recurrent_state = (
    torch.zeros(batch_size, num_heads, k_head_dim, v_head_dim).to(value)
    if initial_state is None
    else initial_state.to(value)
)
# q_t: (B, H, d_k) 
# g_t: (B, H, 1, 1)
# beta_t: (B, H, 1)
for i in range(sequence_length):
    q_t = query[:, :, i]
    k_t = key[:, :, i]
    v_t = value[:, :, i]
    
    g_t = g[:, :, i].exp().unsqueeze(-1).unsqueeze(-1)
    beta_t = beta[:, :, i].unsqueeze(-1)

    last_recurrent_state = last_recurrent_state * g_t
    
    kv_mem = (last_recurrent_state * k_t.unsqueeze(-1)).sum(dim=-2)
    
    delta = (v_t - kv_mem) * beta_t
    last_recurrent_state = last_recurrent_state + k_t.unsqueeze(-1) * delta.unsqueeze(-2)
    core_attn_out[:, :, i] = (last_recurrent_state * q_t.unsqueeze(-1)).sum(dim=-2)
</code></pre><p>上述代码整个计算对应下面过程：$S_{t}=\alpha_{t} S_{t-1}+\beta_{t}\left(v_{t}-\alpha_{t} S_{t-1} k_{t}\right) k_{t}^{\top}$ 计算公式中 $\alpha_t$ 对应代码中的 g_t而 $\beta_t$就对应beta_t。通过这部分计算可以实现注意力计算的复杂度降低为 $O(T · d_k · d_v)$ 而对于上述公式中 $\alpha_t$主要是遗忘系数用来控制历史保留和检索衰减，$\beta_t$学习率控制新信息的写入强度，内部的 $v_{t}-\alpha_{t} S_{t-1} k_{t}$主要是用来预测残差只更新”记错的部分”，而非全部覆盖。<br/> 对于上述之所以这么计算简单理解如下：首先在最开始的Attention计算中是这样的：$O_t=\text{Softmax}(Q_tK_{1:t}^T)V_{1:t}=\sum_1^t \text{softmax}(Q_tK_i^T)V_i$ 也就是说在生成过程中每进入一个新的词（$Q_t$）都需要去和前面所有的KV都进行计算，如果直接去掉内部的 $\text{softmax}$<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">8</a></sup>那么就可以得到：$O_t≈\sum_1^t(Q_tK_i^T)V_i$ 此时这个等式可以满足交换运算顺序可以得到 $O_t=Q_t^T\sum_1^t K_iV_i^T$ 对于这个公式内部的 $\sum_1^t K_iV_i^T=S_{t-1}+K_tV_t^T$ 那么最后<strong>注意力计算过程</strong>（最朴素的线性注意力计算过程）为：<br/> \(O_t=Q_t(S_{t-1}+K_tV_t^T))=Q_tS_{t-1}+Q_tK_tV_t^T\)<br/> 但是这个计算过程存在小问题：所有历史$S_{t-1}$都是平等重要，并且随着你推理长度变长新的信息就被淹没了，那么就可以直接 <strong>一次带遗忘 + 误差修正的外积更新</strong>，比如说对于最上面的公式可以直接写成：$\alpha_t(S_{t-1}-\beta_t K_tK_t^T)S_{t-1}+\beta_tV_tK_t^T$ 首先我的 $\alpha_t$ 就对应遗忘机制（对于我的$S_{t-1}$ 需要保存多少）而内部的就是一个误差更新机制（我的$t$对于历史状态有多少的影响）对于$\beta$也可以理解为新信息写入强度，最后回到代码里面这个 $S_{t-1}$就是我们缓存的KV-Cache。</p><blockquote><p>简单解释一下里面 $\text{softmax}$ 作用：都知道计算 $QK^T$ 过程中我们相当于得到了token之间 “相似度”而后将整个相似度加权到 $V$上，而$\text{softmax}$在其中的作用就是去把原始相似度分数变成概率分布并且放大显著差异</p></blockquote><h3 id="总结">总结</h3><p>从QwenVL到QwenVL2.5视觉编码器处理过程：<br/><strong>QwenVL</strong>：将图像转化为<strong>固定的分辨率</strong>而后将输入到Vit-bigG进行处理得到视觉特征之后再去使用类似Q-former处理过程（QwenVL中使用的是<em>一个随机初始化的单层Cross-Attention模块</em>）使用learned-query（压缩到<strong>固定的256长度的token</strong>）将视觉token进行压缩而后输入到LLM中。<br/><strong>QwenVL2</strong>：首先使用<strong>动态分辨率</strong>（将图像<strong>除以固定的factor而后保持横纵比</strong>将其缩减到 <code>[mix_pixels, max_pixels]</code>中）去处理图像而后将其输入到视觉编码器中，而后将<strong>2x2的的相邻的token进行拼接</strong>（也就是将图像补充一个时间帧得到TCHW，而后再去在THW三个维度划分得到不同的patch：grid_t,grid_h,grid_w）到一个token而后通过MLP层进行处理。<br/><strong>QwenVL2.5</strong>：整体框架上和QwenVL2差异不大，区别在于使用了window-attention（主要是对数据进行切块而后在块内做注意力计算）以及2D-RoPE<br/><strong>QwenVL3</strong>：改进点在于使用DeepStack技术将Vit提取得到的视觉特征注入LLM Decoder的特定位置层中，而不是最开始只在输入层注入视觉信息<br/><strong>QwenVL3.5</strong>：改进点在于使用线性注意力机制$\alpha_t(S_{t-1}-\beta_t K_tK_t^T)S_{t-1}+\beta_tV_tK_t^T$主要参数 $\alpha$ 主要是控制历史信息重要性、$\beta$主要是控制当前信息重要性、内部计算 $KK^T$主要是进行方向引导</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:4" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2308.12966">https://arxiv.org/pdf/2308.12966</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="http://arxiv.org/abs/2409.12191">http://arxiv.org/abs/2409.12191</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://arxiv.org/abs/2502.13923">https://arxiv.org/abs/2502.13923</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html">https://www.big-yellow-j.top/posts/2025/08/29/QwenVLCode.html</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://qwen.ai/blog?id=99f0335c4ad9ff6153e517418d48535ab6d8afef&amp;from=research.latest-advancements-list">QwenVL-3-Blog</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2406.04334">https://arxiv.org/pdf/2406.04334</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:10" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2505.06708">https://arxiv.org/pdf/2505.06708</a> <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:11" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2407.10671">https://arxiv.org/pdf/2407.10671</a> <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>多模态</category><pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/08/28/OpenModelQwen.html</guid></item><item><title>开源模型技术总结-2————DeepSeek系列模型</title><link>https://www.big-yellow-j.top/posts/2025/08/28/OpenModelDeepSeek.html</link><description><![CDATA[DeepSeek v3采用混合专家模型，新增辅助损失平衡专家不均衡，结构创新包括低秩优化KV-cache（降维升维减少显存）和混合专家机制（Routed Expert直接传入隐藏层，Shared Expert经门控筛选），集成Multi-Token Prediction技术。DeepSeek-R1基于思维链（CoT），通过Group Relative Policy Optimization（GRPO）训练，含生成回答、评分、裁剪更新（clip函数限定值）及KL散度惩罚偏差步骤。DeepSeek OCR以视觉压缩长文本上下文，将多token文本转为图片减少tokens，模型采用SAM-ba...]]></description><content:encoded><![CDATA[<h2 id="deepseek系列">DeepSeek系列</h2><h3 id="deepseek-v31">DeepSeek v3.1</h3><p><strong>DeepSeek v3.1</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">1</a></sup>（简称<strong>DS</strong>）各类技术细节，对于<strong>DS</strong>在模型结构上和之前迭代版本的 <strong>DS-2</strong>无太大区别，还是使用混合专家模型，只是补充一个辅助损失去平衡不同专家之间的不均衡问题。<br/><img src="https://s2.loli.net/2025/06/21/oqabhYwBMjPSzZU.webp" alt="image" width="889" height="706" loading="lazy" decoding="async"/><br/> 在结构上<strong>DS</strong>主要的创新点在于：1、<a href="https://www.big-yellow-j.top/posts/2025/02/17/Attention.html#:~:text=2%E3%80%81Multi%2Dhead%20Latent%20Attention%EF%BC%88MLA%EF%BC%89">Multi-Head Latent Attention</a>；2、<a href="https://www.big-yellow-j.top/posts/2025/MoE-KV-cache.html">DeepSeekMoE</a>。前者为优化 <strong>KV-cache</strong> 操作，通过一个低秩的$c_r^{KV}$代替原本占用较高的QV的值（首先通过降维方式降低原本维度，这样一来在显存占用上就会降低，而后通过升维方式，恢复到原本的维度），后者为混合专家模型，不过区别于常用的<code>MoE</code>方法，在<strong>DS</strong>中将专家模型分为两类：1、<strong>Routed Expert</strong>；2、<strong>Shared Expert</strong>，前者<strong>直接</strong>将隐藏层的输入进行传入，后者则是通过门控网络<strong>筛选</strong>而后隐藏层的输入进行传入。<br/> 除此之外，在<strong>DS</strong>中使用<strong>Multi-Token Prediction</strong>（MTP）技术<br/><img src="https://s2.loli.net/2025/06/21/4OlDfbA6pgo5NrF.webp" alt="image" width="1002" height="525" loading="lazy" decoding="async"/><br/> 在<strong>DS</strong>中一个很耀眼的功能就是：<strong>DeepSeek-R1</strong>（一种思维链技术：<strong>CoT</strong>:<strong>Chain of Thought</strong>，在GPT-o1中也使用到这种技术）结合论文<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">2</a></sup>中对 <strong>CoT</strong>技术的描述，可以简单的理解为：让LLM可以自主去思考问题，相较之直接让GPT输出答案，区别在于还要他给出推理过程，比如说在DeepSeek中对于思维链的prompt：<br/><img src="https://s2.loli.net/2025/06/21/l7eHa3xMZjwGOP8.webp" alt="image" width="816" height="134" loading="lazy" decoding="async"/><br/> 直接让模型去输出think内容，一般而言如果要训练一个具有思维链功能的模型可以简单按照如下过程进行处理：</p><pre><code class="language-python"># 数据集简单类型
{
  "instruction": "请一步一步思考并解答以下问题。",
  "input": "小明有5个苹果，给了小红3个，还剩多少？",
  "output": "&lt;think&gt;第一步：...\n第二步：...\n第三步：...&lt;/think&gt;答案：2个"
}
# 微调模型过程中 prompt
你是一个严谨的推理助手，回答任何问题前必须先在 &lt;think&gt; 标签内进行完整逐步思考，再给出最终答案。
</code></pre><p>通过上面简单过程去“强迫”模型去输出思考过程而不是直接就给出答案。除此之外在<strong>DS-R1</strong>中模型的整体训练过程使用<strong>Group Relative Policy Optimization</strong>（GRPO）策略进行优化<br/><img src="https://s2.loli.net/2025/06/21/CBnfpTwjQXNkybG.webp" alt="image" width="843" height="326" loading="lazy" decoding="async"/><br/> 对于上述优化过程<a href="https://medium.com/@sahin.samia/the-math-behind-deepseek-a-deep-dive-into-group-relative-policy-optimization-grpo-8a75007491ba">理解</a>：比如说对于一个数学问题：$8+5=?$，这就是上面公式中所提到的question $q$，按照上面的描述，将会生成一系列的输出：${o_1,…,o_G}$<br/><strong>Step-1</strong>：生成若干的回答。${o_1,…,o_G}$<br/><strong>Step-2</strong>：对于生成的回答进行评分。${r_1,…,r_G}$，而后计算$A_i=\frac{r_i- \text{mean}({r_1,…,r_G})}{\text{std}({r_,…,r_G})}$<br/><strong>Step-3</strong>：使用裁剪更新策略：$\text{clip}(\frac{\pi_{\theta}(o_i|q)}{\pi_{\theta_{old}(o_i|q)}},1-\epsilon,\epsilon)$比如说：如果新策略开始给o1分配过高的概率，裁剪机制确保不会过度强调这个响应。这种方式保证了即使在像推理这样复杂的任务中，策略优化也能保持稳定和可靠。通过clip函数将内部值限定在$(1-\epsilon, 1+\epsilon)$之间<br/><strong>Step-4</strong>：通过KL散度（用来度量两个概率分布相似度的指标）惩罚偏差</p><h3 id="deepseek-v32">DeepSeek v3.2</h3><p><img src="https://files.seeusercontent.com/2026/05/07/aI0b/image20260312171724569.webp" alt="20260312171724569"/><br/> 论文中核心点就是Native Sparse Attention，在上一代的MLA中通过对KV-cache进行压缩（降维压缩升维还原）能够有效的节省KV-cache的缓存大小，但是计算复杂度上还是 $O(L^2)$因此在 v3.2中提出的Native Sparse Attention核心点就是去缩小计算的复杂度为 $O(LK)$，其核心计算过程如下：Native Sparse Attention主要是在MLA的基础上补充了一个 <em>lighting indexer</em>主要过程如下<br/> \(I_{t,s} = \sum_{j=1}^{H^I} w_{t,j}^I \cdot \text{ReLU} \left( \mathbf{q}_{t,j}^I \cdot \mathbf{k}_s^I \right)\) <br/> 表示在完成 Q-K 点积之后，用于预测哪些 token 在当前上下文中最为重要。具体实现上，在每个注意力层前会增加一个轻量级索引头，该索引头使用低精度（FP8）计算当前 Query 与所有历史 Key 的粗略相似度分数。随后，从这些分数中选出 top-k 个最相关的历史位置，仅将这 k 个 Key/Value 输入后续的正式注意力计算。通过这种方式，可以显著降低注意力计算的 FLOPs 与内存占用，同时保持长序列的关键依赖关系。在<a href="https://github.com/deepseek-ai/DeepSeek-V3.2-Exp/blob/87e509a2e5a100d221c97df52c6e8be7835f0057/inference/model.py#L545">代码实现过程</a>整体的部分和MLA相似关键在于 <code>Indexer</code> 过程</p><pre><code class="language-python">qr = self.q_norm(self.wq_a(x))
q = self.wq_b(qr)
q = q.view(bsz, seqlen, self.n_local_heads, self.qk_head_dim)
q_nope, q_pe = torch.split(q, [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
q_pe = apply_rotary_emb(q_pe, freqs_cis)
...
if mask is not None: # 训练阶段
 q = torch.cat([q_nope, q_pe], dim=-1)
 kv = self.wkv_b(kv)
 kv = kv.view(bsz, seqlen, self.n_local_heads, self.qk_nope_head_dim + self.v_head_dim)
 k_nope, v = torch.split(kv, [self.qk_nope_head_dim, self.v_head_dim], dim=-1)
 k = torch.cat([k_nope, k_pe.expand(-1, -1, self.n_local_heads, -1)], dim=-1)
 scores = torch.einsum("bshd,bthd-&gt;bsht", q, k).mul_(self.softmax_scale)

 # indexer
 topk_indices = self.indexer(x, qr, start_pos, freqs_cis, mask)
 index_mask = torch.full((bsz, seqlen, seqlen), float("-inf"), device=x.device).scatter_(-1, topk_indices, 0)
 index_mask += mask
 scores += index_mask.unsqueeze(2)

 scores = scores.softmax(dim=-1)
 x = torch.einsum("bsht,bthd-&gt;bshd", scores, v)
else: # 模型推理阶段
 if self.dequant_wkv_b is None and self.wkv_b.scale is not None:
  self.dequant_wkv_b = weight_dequant(self.wkv_b.weight, self.wkv_b.scale)
 wkv_b = self.wkv_b.weight if self.dequant_wkv_b is None else self.dequant_wkv_b
 wkv_b = wkv_b.view(self.n_local_heads, -1, self.kv_lora_rank)
 q_nope = torch.einsum("bshd,hdc-&gt;bshc", q_nope, wkv_b[:, :self.qk_nope_head_dim])
 scores = (torch.einsum("bshc,btc-&gt;bsht", q_nope, self.kv_cache[:bsz, :end_pos]) +
           torch.einsum("bshr,btr-&gt;bsht", q_pe, self.pe_cache[:bsz, :end_pos])) * self.softmax_scale

 # indexer
 topk_indices = self.indexer(x, qr, start_pos, freqs_cis, mask)
 index_mask = torch.full((bsz, 1, end_pos), float("-inf"), device=x.device).scatter_(-1, topk_indices, 0)
 scores += index_mask.unsqueeze(2)

 scores = scores.softmax(dim=-1)
 x = torch.einsum("bsht,btc-&gt;bshc", scores, self.kv_cache[:bsz, :end_pos])
 x = torch.einsum("bshc,hdc-&gt;bshd", x, wkv_b[:, -self.v_head_dim:])
</code></pre><p>在训练过程中：对于里面的 <code>self.indexer(x, qr, start_pos, freqs_cis, mask)</code> 输入的分别是原始的输入x以及通过压缩后的q，<a href="https://github.com/deepseek-ai/DeepSeek-V3.2-Exp/blob/87e509a2e5a100d221c97df52c6e8be7835f0057/inference/model.py#L435">具体处理过程</a>中，<strong>对于压缩后的q以及原始的输入x</strong>进行矩阵计算–&gt;拆分（一部分去计算RoPE）–&gt;两部分进行拼接分别得到Q、K的值对于这两部分的值，而后再去计算 <code>Fast Walsh–Hadamard transform</code>转换（让数值分布均匀），再去将q、k的值转换到fp8再去计算 <code>index_score = fp8_index(q_fp8.contiguous(), weights, self.k_cache[:bsz, :end_pos].contiguous(), self.k_scale_cache[:bsz, :end_pos].contiguous())</code> 在得到index_score之后直接获取每一个token的重要性再去将其融入到最后的socre中即可。<br/> 在回过头去看模型解码过程中这个注意力方式是如何节约计算量的：传统的KV-cache操作中缓存大小是 $O(LHD)$ （分别表示序列长度、注意力头数、每个头维度） 通过MLA压缩得到 $O(LC)$ 在传统的QK计算中复杂度是 $O(L^2D)$ 使用MLA后计算量是 $O(L^2C)$，通过idnexer去选取top-k又可以降低到 $O(LkC)$。这是因为indexer 可以快速预测每个 query 对哪些历史 token 最感兴趣，只需要对这 k 个 token 进行正式的 Q-K 点积和 V 聚合，而不必对所有 L 个历史 token 计算，从而将 $O(L^2)$ 的复杂度降低到 $O(Lk)$。 在代码实现中，为了兼容 dense attention，通常会使用 mask 将非 top-k 的 token 对应的 score 设置为 $-\infty$。</p><blockquote><p>很多人可能和我有一个疑问，你计算QK值而后通过topk获取需要关注的token而后将这个token作为一个mask叠加在score上，这一套下来计算量似乎和原始的没有差异？其实关键点就在这个mask上，我将socre通过mask（比如就是一个上三角矩阵）处理之后只有一半的socre了，那么这一半的socre就可以直接通过写算子去优化计算复杂度！！比如说可以直接测试 <code>F.scaled_dot_product_attention(q, k, v, is_causal=False)</code> 低于 is_causal 参数分别设置True/False去对比计算时间。</p></blockquote><h2 id="deepseek-ocr">DeepSeek OCR</h2><h3 id="deepseek-ocrv1">DeepSeek OCRv1</h3><p>DeepSeek OCR<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">3</a></sup>主要内容就是尝试<strong>使用视觉的方式去压缩长文本上下文</strong>，按照论文里面的描述就是：$f_{dec}:R^{n\times d_{latent}}\rightarrow R^{N\times d_{text}}, \hat{X}=f_{dec}(X)$<br/> 前面部分代表压缩的视觉tokens后面代表重构的文本表述。其实从上面公式就可以了解在DeepSeek OCR中做的就是：对于原始文本输入需要较长的tokens数量（比如说1w个字），但是如果这1w个文本都在图片上可能就是512个tokens。</p><blockquote><p>但是作者只是在OCR邻域做测试，正如论文里面说的： It is reasonable to conjecture that LLMs, through specialized pretraining optimization, would demonstrate more natural integration of such capabilities.</p></blockquote><p><img src="https://s2.loli.net/2025/11/11/IxuHpXCj2hJ3sTU.webp" alt="image" width="968" height="695" loading="lazy" decoding="async"/><br/> 对于传统多模态中的视觉结构：第一种使用多个视觉编码器进行编码处理，第二种：将图片切割为不同的patch而后进行处理，第三种：使用动态分辨率而后将图片去切割为不同patch进行编码。论文中使用的模型结构（为了实现：1、处理高分辨率；2、高分辨率小低激活；3、较少的视觉tokens；4、支持多分辨率输入；5、计算参数少）为：<strong>SAM-base</strong>（patch-size：16）+<strong>Conv</strong>（2层，kernel_size=3,strid=2, paddingg=1去对视觉token进行16倍下采样）+<strong>CLIP-large</strong>（去掉patch-embedding因为我的输入就是patch了），那么对于1024x1024首先划分为1024/16 × 1024/16 = 4096个patch token，在对4096个token进行压缩，数量变为4096/16 = 256。<br/><img src="https://s2.loli.net/2025/11/11/GhRspCQc9LHOJPA.webp" alt="image" width="946" height="398" loading="lazy" decoding="async"/><br/> 在许多论文里面也用到了压缩技术（<em>截至到：2025.10.23</em>部分论文），比如说Glyph（Zhipu-清华）<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">4</a></sup>和另外一篇论文<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">5</a></sup><br/><img src="https://s2.loli.net/2025/11/11/hosSXQyPOlxLYvc.webp" alt="image" width="594" height="307" loading="lazy" decoding="async"/><br/> 对于这些内容核心的思路都是将文本转化为image来进行压缩tokens比如在论文[^11]中直接将text转化为latex格式的图片而后通过模型进行处理。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:4" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2501.12948">DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2201.11903">Chain-of-Thought Prompting Elicits Reasoning in Large Language Models</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://www.arxiv.org/pdf/2510.18234">https://www.arxiv.org/pdf/2510.18234</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2510.17800">https://arxiv.org/pdf/2510.17800</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2510.18279">https://arxiv.org/pdf/2510.18279</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>多模态</category><pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/08/28/OpenModelDeepSeek.html</guid></item><item><title>常见的各类LLM基座模型（GPT、DeepSeek、Qwen等）模型解析以及对比</title><link>https://www.big-yellow-j.top/posts/2025/08/15/LLM.html</link><description><![CDATA[本文汇总各类LLM模型核心技术，涵盖GPT、DeepSeek、LLama系列及BERT等。GPT系列中，GPT v1首创无监督预训练与监督微调两阶段模式，采用Transformer decoder架构；GPT v2调整layer-norm位置至残差连接块内，优化模型训练稳定性。DeepSeek v3基于混合专家模型，通过辅助损失平衡专家不均衡，创新低秩处理优化KV-cache降低显存占用，并将专家分为直接传入的Routed Expert与门控筛选的Shared Expert，同时集成Multi-Token Prediction技术，其DeepSeek-R1模块采用思维链（CoT）结合GR...]]></description><content:encoded><![CDATA[<h2 id="各类llm模型技术汇总">各类LLM模型技术汇总</h2><p><img src="https://s2.loli.net/2025/06/21/KZh5Bwmp43NRHGF.webp" alt="image" width="1593" height="510" loading="lazy" decoding="async"/><br/> 只去对比整体<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>框架，对所采用的激活函数，归一化处理，位置编码等参考：<br/><strong>1、位置编码</strong>：<a href="https://www.big-yellow-j.top/posts/2025/02/03/pos-embedding.html">https://www.big-yellow-j.top/posts/2025/02/03/pos-embedding.html</a><br/><strong>2、归一化处理</strong>：<a href="https://www.big-yellow-j.top/posts/2025/01/05/dl-norm.html">https://www.big-yellow-j.top/posts/2025/01/05/dl-norm.html</a><br/><strong>3、分布式训练</strong>：<a href="https://www.big-yellow-j.top/posts/2025/01/03/DistributeTraining.html">https://www.big-yellow-j.top/posts/2025/01/03/DistributeTraining.html</a></p><h2 id="gpt系列">GPT系列</h2><h3 id="1gpt-v1">1.GPT v1</h3><p>对于大部分的深度学习任务，需要大量的<strong>标记数据</strong>（labeled data），但是如果使用大量的标记数据就会导致一个问题：构建得到的模型缺少适用性（可以理解为模型的泛化性能可能不佳）。那么就尝试使用<strong>非标记的数据</strong>（unlabelled data）但是这样一来又会有一个新的问题：时间消费大（time-consuming and expensive）。所以目前学者提出：使用预训练的词嵌入来提高任务性能。使用 <em>未标注的文本信息</em>（word-level information from unlabelled text）可能会：1、不清楚那种<strong>优化目标</strong>（optimization objective）在学习对迁移有用的文本表示时最有效；2、如何将这些学习到的表征有效的迁移到<strong>目标任务</strong>（target task）中。<br/> 作者提出：1、<strong>无监督的预训练</strong>（unsupervised pre-training）；2、<strong>监督的微调</strong>（supervised fine-tuning）<br/> 1、<strong>Unsupervised pre-training</strong><br/> 给定一些列的的<strong>无标签</strong>的 <strong>token</strong>：$U={u_1,…,u_n}$，构建自回归的模型：</p> \[L_1(U)= \sum_{i}logP(u_i|u_{i-k},...,u_{i-1};\theta)\] <p>其中 $\theta$为模型的参数。作者在模型中使用 <strong>Transforme</strong>作为 <strong>decoder</strong>，在最后的模型上作者构建得到为：</p> \[h_0= UW_e+W_p \\ h_l = transformer\_block(h_{l-1})\forall i \in [1,n]\\ P(u)=softmax(h_nW_e^T)\] <p>其中$n$代表神经网路层的数目，$W_e$代表 <em>token embedding matrix</em>，$W_p$代表 <em>position embedding matrix</em>。对于无监督下的预训练：通过构建的数据集，去对模型的参数进行训练，得到模型的参数。</p><p>2、<strong>Supervised fine-tunning</strong><br/> 作者在此部分提到：通过第一步得到的模型参数去对监督任务进行训练（采用的模型结构是没有变化的）。给定标签数据集$C$，给定输入：${x^1,…,x^m }$以及其标签$y$。将数据投入到预训练得到的模型参数里面得到：$h_l^m$，然后添加一个线性输出层（参数为：$W_y$）去对$y$进行预测。</p> \[P(y|x^1,...,x^m)=softmax(h_l^wW_y)\] <p>对于上述两部分步骤直观上理解：人首先从外界获取大量信息：网络，书本等，把这些信息了解之后，然后去写作文或者去回答问题。<br/> 模型结构：<br/><img src="https://s2.loli.net/2025/06/21/yI7Ms91N6Wif3kY.webp" alt="image" width="879" height="438" loading="lazy" decoding="async"/></p><h3 id="2gpt-v2">2.GPT v2</h3><p><strong>GPT v2</strong>区别前一个模型，区别在于将<strong>layer-norm</strong> 位置替换到每一个残差连接块的里面，也就是说在数据输入到 <strong>Multi-Head-Attention</strong> 以及 <strong>Feed-Forward</strong> 之前提前通过一层标准化处理。</p><h3 id="3gpt-v3">3.GPT v3</h3><h2 id="glm系列">GLM系列</h2><h3 id="glm-v1">GLM v1</h3><p><img src="https://s2.loli.net/2025/10/12/rMLiU3YajAwTFcV.webp" alt="image" width="1330" height="734" loading="lazy" decoding="async"/><br/> 训练过程任务大致如上描述<sup id="fnref:17" role="doc-noteref"><a href="#fn:17" class="footnote" rel="footnote">2</a></sup>，用更加数学的描述是从输入文本$x=[x_1,…,x_n]$中随机采样多个文本片段${s_1,…,s_m}$ 对于每个片段都对应$x$ 中一系列的连续文本，对于采样的文本片段通过[MASK]进行标记这样一来我的输入就变为了 $x_{corrupt}$而后模型就通过自回归的方式去文本中预测缺失的词，这意味着在预测一个片段中的缺失词时，模型以自回归的方式从损坏的文本和<strong>之前预测的片段</strong>。为了充分捕捉不同片段之间的相互依赖关系，通过随机打乱片段的顺序，类似于排列语言模型，对应上面图中的b、c。除此之外模型还有的改进如下：<br/><strong>1、二维位置编码</strong>：第一个位置id用来标记Part A中的位置，第二个位置id用来表示跨度内部的相对位置。这两个位置id会通过embedding表被投影为两个向量，最终都会被加入到输入token的embedding表达中</p><h2 id="qwen模型系列">Qwen模型系列</h2><p>以Qwen2<sup id="fnref:18" role="doc-noteref"><a href="#fn:18" class="footnote" rel="footnote">3</a></sup>为例用经过修改的 Transformer 架构，并采用了最近流行的大型语言模型（LLAMA）的训练方法，Qwen对架构的修改包括:<br/> 1、<strong>Embedding and output projection</strong>。根据初步实验结果，Qwen选择了非约束的嵌入方法，而不是将输入Embedding和输出projection的权重绑定在一起。为了在内存成本上取得更好的性能，Qwen做出了这个决定。<br/> 2、<strong>Positional embedding</strong>。Qwen选择Rotary Positional Embedding（RoPE，旋转位置编码）作为首选选项，以将位置信息纳入Qwen的模型中。RoPE已被广泛采用，并已在当今大型语言模型中取得了成功，尤其是PaLM和LLAMA。特别是，Qwen选择使用FP32精度来计算逆频率矩阵，而不是BF16或FP16，以便优先考虑模型性能并实现更高的准确性。<br/> 3、<strong>Bias</strong>。对于大多数层，Qwen遵循陈卓辉等人的做法删除偏差，但对于注意力中的QKV层添加了偏差，以增强模型的外推能力。<br/> 4、<strong>Pre-Norm&amp; RMSNorm</strong>。在Transformer模型中，预归一化是最常用的方法，它已被证明比后归一化更能提高训练稳定性。此外，Qwen用RMSNorm取代了传统层归一化技术。这一改变带来了相同的表现水平，同时也提高了效率。<br/> 5、<strong>Activation function</strong>。Qwen选择了SwiGLU作为激活函数，它是Swish和门控线性单元的组合。Qwen的初始实验证明，基于GLU的激活函数通常优于其他基准选项，如GeLU。与以往研究常见的做法一样，Qwen将前馈网络（FFN）的维度从Hidden size的四倍减少到三分之八的Hidden size。</p><pre><code class="language-python">QWenLMHeadModel(
  (transformer): QWenModel(
    (wte): Embedding(151936, 4096)
    (drop): Dropout(p=0.0, inplace=False)
    (rotary_emb): RotaryEmbedding()
    (h): ModuleList(
      (0-31): 32 x QWenBlock(
        (ln_1): RMSNorm()
        (attn): QWenAttention(
          (c_attn): Linear(in_features=4096, out_features=12288, bias=True)
          (c_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (attn_dropout): Dropout(p=0.0, inplace=False)
        )
        (ln_2): RMSNorm()
        (mlp): QWenMLP(
          (w1): Linear(in_features=4096, out_features=11008, bias=False)
          (w2): Linear(in_features=4096, out_features=11008, bias=False)
          (c_proj): Linear(in_features=11008, out_features=4096, bias=False)
        )
      )
    )
    (ln_f): RMSNorm()
  )
  (lm_head): Linear(in_features=4096, out_features=151936, bias=False)
)
</code></pre><h2 id="deepseek系列">DeepSeek系列</h2><p>主要介绍<strong>DeepSeek v3</strong><sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">4</a></sup>（简称<strong>DS</strong>）各类技术细节，对于<strong>DS</strong>在模型结构上和之前迭代版本的 <strong>DS-2</strong>无太大区别，还是使用混合专家模型，只是补充一个辅助损失去平衡不同专家之间的不均衡问题。<br/><img src="https://s2.loli.net/2025/06/21/oqabhYwBMjPSzZU.webp" alt="image" width="889" height="706" loading="lazy" decoding="async"/></p><blockquote><p>左侧结构和 <strong>GPT-2</strong>结构类似</p></blockquote><p>在结构上<strong>DS</strong>主要的创新点在于：1、<a href="https://www.big-yellow-j.top/posts/2025/01/29/Attention.html">Multi-Head Latent Attention</a>；2、<a href="https://www.big-yellow-j.top/posts/2025/MoE-KV-cache.html">DeepSeekMoE</a>。前者为优化 <strong>KV-cache</strong> 操作，通过一个低秩的$c_r^{KV}$代替原本占用较高的QV的值（首先通过降维方式降低原本维度，这样一来在显存占用上就会降低，而后通过升维方式，恢复到原本的维度），后者为混合专家模型，不过区别于常用的<code>MoE</code>方法，在<strong>DS</strong>中将专家模型分为两类：1、<strong>Routed Expert</strong>；2、<strong>Shared Expert</strong>，前者<strong>直接</strong>将隐藏层的输入进行传入，后者则是通过门控网络<strong>筛选</strong>而后隐藏层的输入进行传入。<br/> 除此之外，在<strong>DS</strong>中使用<strong>Multi-Token Prediction</strong>（MTP<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">5</a></sup>）技术<br/><img src="https://s2.loli.net/2025/06/21/4OlDfbA6pgo5NrF.webp" alt="image" width="1002" height="525" loading="lazy" decoding="async"/><br/> 在<strong>DS</strong>中一个很耀眼的功能就是：<strong>DeepSeek-R1</strong>（一种思维链技术：<strong>CoT</strong>:<strong>Chain of Thought</strong>，在GPT-o1中也使用到这种技术）结合论文<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">6</a></sup>中对 <strong>CoT</strong>技术的描述，可以简单的理解为：让LLM可以自主去思考问题，比如在论文<sup id="fnref:9:1" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">6</a></sup>中对 <strong>CoT</strong>技术的描述。<br/><img src="https://s2.loli.net/2025/06/21/lXtgUsreIJEv51k.webp" alt="image" width="791" height="389" loading="lazy" decoding="async"/><br/> 相较之直接让GPT输出答案，区别在于还要他给出推理过程。结合在<strong>DS-R1</strong><sup id="fnref:8:1" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">4</a></sup>中的描述对于 <strong>DS-R1</strong>整体过程理解如下：<br/><img src="https://s2.loli.net/2025/06/21/uzTXFV3GNZU2b7g.webp" alt="image" width="610" height="292" loading="lazy" decoding="async"/><br/> 非常明显的一个强化学习过程，在论文里面提到的使用 <strong>Group Relative Policy Optimization</strong>（GRPO）<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">7</a></sup>策略进行优化<br/><img src="https://s2.loli.net/2025/06/21/CBnfpTwjQXNkybG.webp" alt="image" width="843" height="326" loading="lazy" decoding="async"/><br/> 在 <strong>DS-R1</strong>中作者提到的使用的模板<br/><img src="https://s2.loli.net/2025/06/21/l7eHa3xMZjwGOP8.webp" alt="image" width="816" height="134" loading="lazy" decoding="async"/><br/> 对于上述优化过程<a href="https://medium.com/@sahin.samia/the-math-behind-deepseek-a-deep-dive-into-group-relative-policy-optimization-grpo-8a75007491ba">理解</a>：比如说对于一个数学问题：$8+5=?$，这就是上面公式中所提到的question $q$，按照上面的描述，将会生成一系列的输出：${o_1,…,o_G}$<br/><strong>Step-1</strong>：生成若干的回答。${o_1,…,o_G}$<br/><strong>Step-2</strong>：对于生成的回答进行评分。${r_1,…,r_G}$，而后计算$A_i=\frac{r_i- \text{mean}({r_1,…,r_G})}{\text{std}({r_,…,r_G})}$<br/><strong>Step-3</strong>：使用裁剪更新策略：$\text{clip}(\frac{\pi_{\theta}(o_i|q)}{\pi_{\theta_{old}(o_i|q)}},1-\epsilon,\epsilon)$比如说：如果新策略开始给o1分配过高的概率，裁剪机制确保不会过度强调这个响应。这种方式保证了即使在像推理这样复杂的任务中，策略优化也能保持稳定和可靠。通过clip函数将内部值限定在$(1-\epsilon, 1+\epsilon)$之间<br/><strong>Step-4</strong>：通过KL散度（用来度量两个概率分布相似度的指标）惩罚偏差</p><hr/><p><strong>PPO</strong>和 <strong>GRPO</strong></p><p><img src="https://s2.loli.net/2025/06/21/VRKnCpXr8ulmcIk.webp" alt="image" width="1396" height="619" loading="lazy" decoding="async"/></p><p>上面提到的几个模型：<br/> 1、<strong>Policy Model</strong>：我们需要优化的模型<br/> 2、<strong>Value Model</strong>：估计状态的价值，帮助指导策略优化<br/> 3、<strong>Reference Model</strong>：提供历史策略的参考，确保优化过程中策略变化不过度<br/> 4、<strong>Reward Model</strong>：定义奖励信号，用于强化学习中的奖励反馈</p><p><strong>GRPO</strong>实现，参考<a href="https://mp.weixin.qq.com/s/BYPKP5oXg1V4C_vg0VFGhw"><strong>腾讯</strong></a>以及 <a href="https://gist.github.com/willccbb/4676755236bb08cab5f4e54a0475d6fb">Github</a>上实现代码，对于复现可以直接用Huggingface中的 <a href="https://huggingface.co/docs/trl/main/en/grpo_trainer"><strong>trl</strong></a>来进行复现</p><hr/><h2 id="llama系列">LLama系列</h2><h3 id="llama-v1">LLama v1</h3><p>LLaMA<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">8</a></sup> 所采用的 Transformer 结构和细节，与标准的 Transformer 架构不同的地方包括采用了前置层归一化（Pre-normalization）并使用 <strong>RMSNorm归一化函数</strong>、激活函数更换为 SwiGLU，并使用了旋转位置嵌入（RoP），整体 Transformer 架构与 GPT-2 类似，大致模型结构如下：<br/><img src="https://s2.loli.net/2025/10/12/PCeIrmOhHSfEQoy.webp" alt="image" width="682" height="1598" loading="lazy" decoding="async"/></p><blockquote><p>image from: https://zhuanlan.zhihu.com/p/625775403</p></blockquote><p><strong>1、RMSNorm归一化函数</strong>：论文假设 LayerNorm 中的重新中心化不再是必须的（平移不变性不重要），RMSNorm 通过均方根（RMS）对每一层神经元的输入进行归一化，使模型具备重新缩放不变性和隐式学习率调整的能力。相比LayerNorm，RMSNorm计算更为简洁，大约可以节省 7% 到 64% 的运算。</p><blockquote><p>和LayerNorm之间差异在于后者归一化处理计算就是常规的归一化（减均值然后除方差），但是在rmsenorm中直接除均方根，除此之外</p></blockquote> \[\begin{aligned} \text{RMSNorm}(x) &amp;: \hat{x}_i = \gamma \odot \frac{x_i}{\text{RMS}(x)} \\ \text{RMS}(\mathbf{x}) &amp;= \sqrt{\frac{1}{d} \sum_{x_i \in \mathbf{x}} x_i^2} \end{aligned}\] <p><strong>2、SwiGLU激活函数</strong>：SwiGLU 激活函数与 GELU 激活函数类似，但 GELU 激活函数的计算速度更快，并且 GELU 激活函数的输出范围更广，因此 SwiGLU 激活函数被用于 LLaMA 模型。<br/><img src="https://s2.loli.net/2025/10/12/L4RehXGQ1Up59jK.webp" alt="image" width="707" height="315" loading="lazy" decoding="async"/></p> \[\text{SwiGLU}(a, b) = \text{Swish}(a) \otimes \sigma(b)\] <p>其中a、b代表的就是两个线性变化输出（如$a=xW_1+b_1$），$Swish(a) = Sigmoid(\beta a) \cdot a$，和FFN结合代码为：</p><pre><code class="language-python">class FeedForward(nn.Module):
    def __init__(self, dim, hidden_dim):
        super().__init__()
        self.w1 = nn.Linear(dim, hidden_dim, bias=False)
        self.w2 = nn.Linear(hidden_dim, dim, bias=False)
        self.w3 = nn.Linear(dim, hidden_dim, bias=False)

    def forward(self, x):
        # SwiGLU: (SiLU(W1x) * W3x) @ W2
        return self.w2(F.silu(self.w1(x)) * self.w3(x))
</code></pre><p><strong>3、RoPE旋转位置编码</strong>：文本序列是一种“顺序化”序列（文本字符之间排序对于文本的结果存在影响），因此通过位置编码去表示不同位置的输入，从而提高模型对长文本的生成效果。对于RoPE通俗的解释就是：就是对于输入调整向量乘以一个旋转矩阵（内积矩阵），但是为了使用绝对位置信息还会去补充参数m。在Llamav1中处理方式为：对于 token 序列中的每个词嵌入向量，都计算其对应的 query 和 key 向量；然后在得到 query 和 key 向量的基础上，对每个 token 位置都计算对应的旋转位置编码；接着对每个 token 位置的 query 和 key 向量的元素按照两两一组应用旋转变换；最后再计算 query 和 key 之间的内积得到 self-attention 的计算结果。</p><h3 id="llama-v2">LLama v2</h3><p>区别于上一代的LLama v1在LLama v2<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">9</a></sup>中改进如下几点：<br/><strong>1、序列长度</strong>：由原来的2048 tokens变化为4096 tokens<br/><strong>2、使用GQA</strong>：通过使用KV-cache可以加快模型生成速度，但是也会造成过大的显存占用，因此<code>LLama v2</code>在decode阶段使用<code>GQA</code>来减少这个过程中的显存占用。<br/><img src="https://s2.loli.net/2025/06/21/LnbcEZ2BYKpVkeq.webp" alt="image" width="1161" height="334" loading="lazy" decoding="async"/><br/> 在<code>LLama v2</code>中对于GQA<sup id="fnref:16" role="doc-noteref"><a href="#fn:16" class="footnote" rel="footnote">10</a></sup>实现如下：</p><pre><code class="language-python">def repeat_kv(x: torch.Tensor, n_rep: int) -&gt; torch.Tensor:
  """torch.repeat_interleave(x, dim=2, repeats=n_rep)"""
  bs, slen, n_kv_heads, head_dim = x.shape
  # 根据n_rep，拓展KV
  if n_rep == 1:
      return x
  return (x[:, :, :, None, :].expand(bs, slen, n_kv_heads, n_rep, head_dim).reshape(bs, slen, n_kv_heads * n_rep, head_dim))

class Attention(nn.Module):
  def __init__(self, args: ModelArgs):
      super().__init__()
      ...
      self.n_local_heads = args.n_heads // model_parallel_size #Q的头数
      self.n_local_kv_heads = self.n_kv_heads // model_parallel_size  #KV的头数
      self.n_rep = self.n_local_heads // self.n_local_kv_heads 
      ...
      self.wq = ColumnParallelLinear(args.dim,args.n_heads * self.head_dim, # Q的头数* head_dim
                                     ...)
      self.wk = ColumnParallelLinear(args.dim,self.n_kv_heads * self.head_dim, # K的头数* head_dim
                                     ...)
      self.wv = ColumnParallelLinear(args.dim,self.n_kv_heads * self.head_dim,# V的头数* head_dim
                                     ...)
      self.wo = RowParallelLinear(args.n_heads * self.head_dim,args.dim,... )
​
      self.cache_k = torch.zeros((args.max_batch_size,args.max_seq_len,self.n_local_kv_heads, #KV的头数
              self.head_dim,)).cuda()
      self.cache_v = torch.zeros((args.max_batch_size,args.max_seq_len,self.n_local_kv_heads,#KV的头数         
                                  self.head_dim,)).cuda()
  def forward(
      self,
      x: torch.Tensor,
      start_pos: int,
      freqs_cis: torch.Tensor,
      mask: Optional[torch.Tensor],
  ):
      bsz, seqlen, _ = x.shape
      xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
​
      xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
      xk = xk.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)
      xv = xv.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)

      xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis) #嵌入RoPE位置编码
      ...
      # 按此时序列的句子长度把kv添加到cache中
      # 初始在prompt阶段seqlen&gt;=1, 后续生成过程中seqlen==1
      self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
      self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv
      # 读取新进来的token所计算得到的k和v
      keys = self.cache_k[:bsz, : start_pos + seqlen]
      values = self.cache_v[:bsz, : start_pos + seqlen]
​
      # repeat k/v heads if n_kv_heads &lt; n_heads
      keys = repeat_kv(keys, self.n_rep)  # (bs, seqlen, n_local_heads, head_dim)
      values = repeat_kv(values, self.n_rep)  # (bs, seqlen, n_local_heads, head_dim)

      xq = xq.transpose(1, 2)  # (bs, n_local_heads, seqlen, head_dim)
      keys = keys.transpose(1, 2)
      values = values.transpose(1, 2)
      #计算q*k
      scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
      if mask is not None:
          #加入mask，使得前面的token在于后面的token计算attention时得分为0，mask掉
          scores = scores + mask  # (bs, n_local_heads, seqlen, cache_len + seqlen)
      scores = F.softmax(scores.float(), dim=-1).type_as(xq)
      output = torch.matmul(scores, values)  # (bs, n_local_heads, seqlen, head_dim)
      output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1)
      return self.wo(output)
</code></pre><h3 id="llama-v3">LLama v3</h3><p>模型<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">11</a></sup>参数细节：<br/><img src="https://s2.loli.net/2025/06/21/TOHfqx2LnAiz7XN.webp" alt="image" width="683" height="341" loading="lazy" decoding="async"/></p><h2 id="bert">BERT</h2><p>BERT<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">12</a></sup>训练主要分为如下阶段，预训练阶段任务：<br/> 1、<strong>Masked LM(MLM)</strong><br/> MLM是一种预训练任务，通过随机掩蔽输入序列中的部分词元，模型根据上下文预测被掩蔽的词元，从而学习双向语言表示。在模型中作者按照：80：10：10的比例进行处理（80：将词元替换为[MASK]；10：将词元替换为词汇表中随机选取的其他词。；10：保持原词元不变）<br/><strong>例子：</strong><br/> 输入：“今天天气真好，我打算去[MASK]。”<br/> 模型的任务是根据上下文预测“[MASK]”应该是“公园”。<br/> 2、<strong>Next Sentence Prediction(NSP)</strong><br/> NSP是一种预训练任务，模型接收两个句子并预测第二个句子是否是第一个句子的后续。该任务帮助模型理解句子间的逻辑关系。<br/> 例子：<br/> 句子对：<br/> 句子1：“我喜欢去公园散步。”<br/> 句子2：“今天下午我会去跑步。”<br/> 模型的任务是判断第二个句子是否是第一个句子的自然延续，答案是“是”。</p><ul><li><strong>缺点</strong><br/> 1、BERT neglects dependency between the masked positions and suffers from a <strong>pretrain-finetune discrepancy</strong>（忽略了屏蔽位置之间的依赖性，并遭受预训练微调差异的影响）</li></ul><p>这是因为在 BERT模型中，在预训练阶段会添加 [MASK]，但是在 下游任务(downsteram tasks)中并不会使用 <strong>[MASK]</strong></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2307.06435">A Comprehensive Overview of Large Language Models</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:17" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2103.10360">https://arxiv.org/pdf/2103.10360</a> <a href="#fnref:17" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:18" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2309.16609">https://arxiv.org/pdf/2309.16609</a> <a href="#fnref:18" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:8" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2501.12948">DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:8:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2404.19737">Better &amp; Faster Large Language Models via Multi-token Prediction</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2201.11903">Chain-of-Thought Prompting Elicits Reasoning in Large Language Models</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:9:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:10" role="doc-endnote"><p><a href="https://medium.com/@sahin.samia/the-math-behind-deepseek-a-deep-dive-into-group-relative-policy-optimization-grpo-8a75007491ba">The Math Behind DeepSeek: A Deep Dive into Group Relative Policy Optimization (GRPO)</a> <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/abs/2302.13971">LLaMA: Open and Efficient Foundation Language Models</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://arxiv.org/abs/2307.09288">Llama 2: Open Foundation and Fine-Tuned Chat Models</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:16" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/625775403">https://zhuanlan.zhihu.com/p/625775403</a> <a href="#fnref:16" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2407.21783v3">The Llama 3 Herd of Models</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/1810.04805">BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/08/15/LLM.html</guid></item><item><title>图像消除论文-2：RORem、ObjectClear</title><link>https://www.big-yellow-j.top/posts/2025/07/25/ImageEraser2.html</link><description><![CDATA[本文介绍RORem和ObjectClear两种图像擦除论文模型。RORem基于SDXL基座，通过视频帧前后变化物体构建mask数据集并结合开源数据，经人工筛选后训练判别器实现自动化数据筛选（判别器基于SDXL-Inpainting下采样和中间层，Lora微调，得分>0.9为合格），模型通过蒸馏得到LCM模型加速消除过程（从4s缩短至0.5s）。ObjectClear以SDXL-Inpainting为基座，重点在于数据集创建与引入注意力机制（attention-mask），数据集构建类似SmartEraser，先分割小实体（DINO/YOLO+SAM）再贴到图像中，包含2875张拍摄数据集...]]></description><content:encoded><![CDATA[<p>本文主要介绍几篇图像擦除论文模型：RORem、ObjectClear</p><h2 id="rorem">RORem</h2><blockquote><p>https://arxiv.org/pdf/2501.00740<br/> https://github.com/leeruibin/RORem<br/> 基座模型：SDXL</p></blockquote><p><img src="https://s2.loli.net/2025/07/26/mDPjCteaObvRqlT.webp" alt="image" width="1284" height="657" loading="lazy" decoding="async"/><br/> 数据集选择的是 <a href="https://github.com/Forty-lock/RORD"><strong>RORD</strong></a>（<strong>通过视频帧中前后变化的物体就是mask进而构建高质量数据集</strong>）以及 <a href="https://huggingface.co/datasets/mulan-dataset/v1.0"><strong>Mulan</strong></a>数据集。对于输入到SDXL中的数据为：1、mask；2、原始图片；3、消除mask后的图片。而后将这三部分数据进行concat。<br/> 只通过上面过程微调的SDXL模型效果不佳，然后（上图的<strong>Human Annotation过程</strong>）再去从开源数据集中进行筛选（排除部分例子如：衣服、身体等）保证每种实体都有500种再得到数据之后然后就是人工筛选出高质量和低质量数据集。但是整个过程是消耗时间的因此会训练一个<strong>判别器网络结构</strong>（<strong>将SDXL-Inpainting中的下采样和中间层作为backbone而后通过Lora进行微调</strong>）通过人工筛选的数据集进行训练进而实现自动化过程（只有判别器得分&gt;0.9的数据集才能算“合格”）。<br/><img src="https://s2.loli.net/2025/07/26/pUaZcx6Ssm7fHKq.webp" alt="image" width="622" height="251" loading="lazy" decoding="async"/><br/> 本路文中（纯力大飞砖，通过构建大规模数据集去微调模型）对于模型改进不大，为例加速消除过程，再去通过蒸馏得到LCM模型来加速消除过程（从4s到0.5s）。<br/><img src="https://s2.loli.net/2025/07/26/qGwMUjcuvEgmIKR.webp" alt="image" width="631" height="469" loading="lazy" decoding="async"/></p><h2 id="objectclear">ObjectClear</h2><blockquote><p><a href="https://arxiv.org/pdf/2505.22636">https://arxiv.org/pdf/2505.22636</a><br/><a href="https://github.com/zjx0101/ObjectClear">https://github.com/zjx0101/ObjectClear</a><br/><strong>基座模型</strong>：<strong>SDXL-Inpainting</strong></p></blockquote><p>本文出发点主要为两个：1、创建数据集；2、通过引入注意力机制（attention-mask）去引导模型消除，处理思路和论文：<a href="https://arxiv.org/pdf/2403.18818">https://arxiv.org/pdf/2403.18818</a>很相似</p><h3 id="数据集构建">数据集构建</h3><p><img src="https://s2.loli.net/2025/07/26/pR7wX5jTvSa1BgW.webp" alt="image.png" width="1251" height="290" loading="lazy" decoding="async"/></p><p>主要为两部分数据集（数据集构建方式上和SmartEraser论文里面处理方式相似：<strong>首先分割小实体</strong>是从图像中通过检查（DINO/YOLO等）加分割（SAM等）得到“小实体”而后<strong>将这些“小实体”去“合理”的贴到图像中</strong>）：1、拍摄数据集（2875张图片）；2、开源下载数据集。对于 <strong>拍摄数据集</strong>处理方式：首先将图片处理为512x512而后，直接通过DIBO+SAM去识别然后切割实体得到mask（$M_o$）然后去结合“mask相关的语义特征”（比如说阴影 $M_e$ 等）得到$M_{fg}=[M_o,M_e]$。除此之外对于下载得到的数据集首先是通过<strong>Mask2former</strong>（切割图像中的实体然后将一些特殊实体（比如说road等）作为背景）+<strong>DepthAnythin</strong>（通过整个算法去保证后续贴图的质量）后续就是直接将从相机拍摄照片里面抽取出来的小实体帖带开源数据集中。</p><h3 id="模型结构">模型结构</h3><p><img src="https://s2.loli.net/2025/07/26/TpmxR1GePt58HUl.webp" alt="image.png" width="1261" height="532" loading="lazy" decoding="async"/></p><p>将基座模型（<strong>SDXL-Inpainting</strong>）的输入（1、噪声分布$z_t$；2、masked image $I_m$这个一般就是直接将mask从图中扣除；3、mask：$M_o$；4、文本prompt：$c$）中的mask image替换为原始的图像：$I_{in}$（这和之前的SmartEraser处理方式类似），除此之外对于输入DF模型中的处理思路和SmartEraser也是相似的：<strong>图像（需要消除的）和文本分别通过Clip不同编码器处理然后组合</strong>。而后输入到DF的Attention计算当中，对于特征组合部分代码处理方式为：</p><pre><code class="language-python">text_object_embed = self.fuse_fn(object_embeds)#两层mlp+norm
text_embeds_new = text_embeds.clone()
text_embeds_new[:, fuse_index, :] = text_object_embed.squeeze(1)
</code></pre><p>将得到的<code>text_object_embed</code>作为文本特征编码特征输入到unet中进行计算。除此之外论文中添加了一个mask loss计算。<br/> 除此之外使用<code>Attention-Guided Fusion</code>具体代码处理方式为：</p><pre><code class="language-python3">def unet_store_cross_attention_scores(self, unet, attention_scores):
    from diffusers.models.attention_processor import (
        Attention,
        AttnProcessor,
        AttnProcessor2_0,
    )
    import types

    UNET_LAYER_NAMES = [
        "down_blocks.0",
        "down_blocks.1",
        "down_blocks.2",
        "mid_block",
        "up_blocks.1",
        "up_blocks.2",
        "up_blocks.3",
    ]

    start_layer = 0
    end_layer = 2
    applicable_layers = UNET_LAYER_NAMES[start_layer:end_layer]

    def make_new_get_attention_scores_fn(name):
        def new_get_attention_scores(module, query, key, attention_mask=None):
            attention_probs = module.old_get_attention_scores(
                query, key, attention_mask
            )
            attention_scores[name] = attention_probs
            return attention_probs

        return new_get_attention_scores

    for name, module in unet.named_modules():
        if isinstance(module, Attention) and "attn2" in name:
            if not any(layer in name for layer in applicable_layers):
                continue
            if isinstance(module.processor, AttnProcessor2_0):
                module.set_processor(AttnProcessor())
            module.old_get_attention_scores = module.get_attention_scores
            module.new_get_attention_scores = types.MethodType(
                make_new_get_attention_scores_fn(name), module
            )
            module.get_attention_scores = module.new_get_attention_scores

    return unet
....
fuse_index = 5
if self.config.apply_attention_guided_fusion:
    if i == len(timesteps) - 1:
        attn_key, attn_map = next(iter(self.cross_attention_scores.items()))
        attn_map = self.resize_attn_map_divide2(attn_map, mask, fuse_index)
        init_latents_proper = image_latents
        if self.do_classifier_free_guidance:
            _, init_mask = attn_map.chunk(2)
        else:
            init_mask = attn_map
        attn_map = init_mask
    self.clear_cross_attention_scores(self.cross_attention_scores)
...
attn_pils = []
if output_type == "pil" and attn_map is not None:
    for i in range(len(attn_map)):
        attn_np = attn_map[i].mean(dim=0).cpu().numpy() * 255.
        attn_pil = PIL.Image.fromarray(attn_np.astype(np.uint8)).convert("L")
        attn_pils.append(attn_pil)
    
    original_pils = self.image_processor.postprocess(init_image, output_type="pil")

    generated_pils = image

    fused_images = []
    for i in range(len(generated_pils)):
        ori_pil = original_pils[i]
        gen_pil = generated_pils[i]
        attn_pil = attn_pils[i]

        fused_np = attention_guided_fusion(np.array(ori_pil), np.array(gen_pil), np.array(attn_pil))
        fused_pil = PIL.Image.fromarray(fused_np.astype(np.uint8)).resize(ori_pil.size)

        fused_images.append(fused_pil)

    image = fused_images
</code></pre><p><strong>首先</strong>对于<code>unet_store_cross_attention_scores</code>主要是处理如下两个步骤：1、搜集<strong>down_blocks.0和down_blocks.1</strong>中attn2模块的注意力分数；2、将down_blocks.0和down_blocks.1中attn2模块处理器从AttnProcessor2_0替换为AttnProcessor<br/><strong>而后</strong>在<code>attention_guided_fusion</code><a href="https://github.com/zjx0101/ObjectClear/blob/ef3177ed1d270a9b4d74939ef852876552adfa68/objectclear/utils/attention_guided_fusion.py#L50">设计</a>中直接将三部分结果：1、原图 image；2、生成得到结果：generated_pils；3、将注意力得分转化为图像：attn_pil进行融合</p><h2 id="总结">总结</h2><p>总得来说两个论文中都是通过构建数据集去训练模型，对与他们的数据集构建：Rorem中是直接通过“循环”方式去获得高质量数据（首先人工筛选高质量的消除数据集）然后去训练SDXL模型然后会有一个判别判断消除效果如何对于效果好的数据直接加到数据中再去训练模型循环上面过程，而再Object中的处理思路为切割实体然后再去将实体去贴到背景中。</p> ]]></content:encoded><category>图像消除</category><pubDate>Fri, 25 Jul 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/07/25/ImageEraser2.html</guid></item><item><title>深入浅出了解生成模型-7：调度器（scheduler）原理</title><link>https://www.big-yellow-j.top/posts/2025/07/06/DFscheduler.html</link><description><![CDATA[本文介绍了SDE（随机微分方程）与ODE（常微分方程）在扩散模型中的应用差异，因调度器不同其实现方式有所区别，并重点解析了DDPM与DDIM两种扩散模型调度器。DDPM将加去噪视为马尔科夫链过程，需多步（通常1000步）生成图像导致速度较慢；DDIM则通过“跳步”优化，提升生成效率。文中还详细说明了diffusers库中调度器的处理流程：初始化参数时依据beta_schedule生成相关参数并计算累乘结果，加噪过程通过公式直接计算；生成过程涉及model_output（预测噪声）、timestep（时间步）、sample（初始噪声样本），包含epsilon（预测噪声）、sample（直接...]]></description><content:encoded><![CDATA[<p>注意如下内容的描述：1、SDE以及ODE因为不同的调度器可能就是基于不同的方式出发的；2、可以了解一下flow-matching；3、注意LCM模型里面的处理</p><blockquote><p>flow-matching推荐文章<br/> https://diffusionflow.github.io/</p></blockquote><h2 id="sde以及ode">SDE以及ODE</h2><p>在正式介绍之前简短了解一些SDE（随机微分方程）以及ODE（常微分方程），对于<strong>ODE一般定义</strong>就是：$\frac{dx_t}{dt}=f(x_t,t)$，对于<strong>SDE一般定义</strong>就是：$dx_t=f(x_t,t)dt+g(x_t,t)dW_t$。两者之间的差异就是SDE会比ODE多一个随机噪声项目，因为多了这个就会导致SDE的轨迹不在唯一每次的求解都是不同的，那么SDE/ODE和扩散模型之间联系在哪？</p><h2 id="ddpmddim">DDPM、DDIM</h2><p>对于DDPM<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>以及DDIM<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>在之前的<a href="https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html">博客</a>有过简短介绍这里直接将两个放到一起进行介绍。扩散模型过程为：</p> \[X_T=\sqrt{\bar{\alpha_T}}x_0+ \sqrt{1- \bar{\alpha_T}}\epsilon\] <p>通过对图片（$x_0$）不断添加高斯噪声最后得到 $x_T$而后通过反向去噪又得到新的图片。不过DDPM和DDIM之间存在一个很明显的差异就是：DDPM将加（去）噪视作一个马尔科夫链过程（简单理解为每一步 $t$都要依靠上一步 $t-1$），但是在DDIM过程中就会使用“跳步”来进行<br/><img src="https://s2.loli.net/2025/06/21/pwIndituAKX4kjh.webp" alt="image" width="950" height="178" loading="lazy" decoding="async"/></p><p><strong>DDPM生成过程</strong>：</p> \[x_{t-1}=\frac{1}{\sqrt{\alpha_t}}\left(x_t-\frac{1- \alpha_t}{\sqrt{1-\bar{\alpha}_t}}\epsilon_\theta(x_t,t)\right)+\sigma_tz,\quad z\sim\mathcal{N}(0,I)\] <p>但是对于DDPM存在一个最大的问题就是需要多步（一般选择T=1000）来生成图像，这样一来就会导致生成的速度很慢，因此后续就提出了DDIM其中<strong>DDIM生成过程</strong>为：</p> \[x_{t-1}=\sqrt{\alpha_{t-1}}\left(\frac{x_t-\sqrt{1-\alpha_t}\epsilon_\theta(x_t,t)}{\sqrt{\alpha_t}}\right)+\sqrt{1-\alpha_{t-1}-\sigma_t^2}\epsilon_\theta(x_t,t)+\sigma_tz\] <p>重点了解一下在diffusers库中如何处理这两个调度器的，以DDPM（<a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L129">源代码</a>）为例，一般来说使用调度器无疑就是下面几个步骤：</p><pre><code class="language-python">from diffusers import DDPMScheduler
# 直接使用初始化的 调度器
noise_scheduler = DDPMScheduler(num_train_timesteps= config.num_train_timesteps,
                            beta_start= config.beta_start,
                            beta_end= config.beta_end,
                            beta_schedule= 'scaled_linear')
# 或则直接加载其他的模型的调度器
noise_scheduler = DDPMScheduler.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0", cache_dir= config.cache_dir, subfolder="scheduler")
'''
不过值得注意的是在DDIM中会：noise_scheduler.set_timesteps(inference_steps) 来告诉用多少步进行推理（）
'''
# 将噪声添加到图片上
noise = torch.randn(image.shape, device= accelerator.device)
noise_image = noise_scheduler.add_noise(image, noise, timesteps)
...
# 将噪声进行剔除
noise = noise_scheduler.step(predicted_noise, t, noise).prev_sample
</code></pre><p>简短了解一下在<code>DDPMScheduler</code>中设计框架是如何的：</p><pre><code class="language-python">class DDPMScheduler(SchedulerMixin, ConfigMixin):
    @register_to_config
    def __init__(
        self, 
        num_train_timesteps: int = 1000, # 加噪的步数
        beta_start: float = 0.0001,      # \beta 起始数值
        beta_end: float = 0.02,          # \beta 最后数值
        beta_schedule: str = "linear",   # 线性加噪方式
        ...)
        ...
        if ...:
            ...
        elif beta_schedule == "linear":
            self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32)
        # 1、初始化参数
        self.alphas = 1.0 - self.betas
        self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
        ...
        self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy())
    def add_noise(
        self,
        original_samples: torch.Tensor,
        noise: torch.Tensor,
        timesteps: torch.IntTensor,)
        # 2、加噪过程
        self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device)
        alphas_cumprod = self.alphas_cumprod.to(dtype=original_samples.dtype)
        timesteps = timesteps.to(original_samples.device)

        sqrt_alpha_prod = alphas_cumprod[timesteps] ** 0.5
        sqrt_alpha_prod = sqrt_alpha_prod.flatten()
        while len(sqrt_alpha_prod.shape) &lt; len(original_samples.shape):
            sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1)

        sqrt_one_minus_alpha_prod = (1 - alphas_cumprod[timesteps]) ** 0.5
        sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten()
        while len(sqrt_one_minus_alpha_prod.shape) &lt; len(original_samples.shape):
            sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1)

        noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise
        return noisy_samples

    def step(
        self,
        model_output: torch.Tensor,
        timestep: int,
        sample: torch.Tensor,
        generator=None,
        return_dict: bool = True,):
        # 3、生成过程
        t = timestep
        prev_t = self.previous_timestep(t)
        # 首先计算 alpha等参数
        alpha_prod_t = self.alphas_cumprod[t]
        alpha_prod_t_prev = self.alphas_cumprod[prev_t] if prev_t &gt;= 0 else self.one
        beta_prod_t = 1 - alpha_prod_t
        beta_prod_t_prev = 1 - alpha_prod_t_prev
        current_alpha_t = alpha_prod_t / alpha_prod_t_prev
        current_beta_t = 1 - current_alpha_t
        # 而后计算 预测结果 DDPM有3种计算过程 epsilon sample v_prediction
        if self.config.prediction_type == "epsilon":
            pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5)
        ...
        # 裁剪预测值
        ...
        elif self.config.clip_sample:
            pred_original_sample = pred_original_sample.clamp(
                    -self.config.clip_sample_range, self.config.clip_sample_range
                )
        # important
</code></pre><p>1、初始化参数（<strong>DDPM和DDIM中没什么差异</strong>）。首先是根据 <code>beta_schedule</code>来生成在 <code>num_train_timesteps</code>下参数 $\beta$的值（比如说 <code>linear</code>那么在1000步下就会生成（直接通过<code>torch.linspace</code>）从 <code>(1-beta_start)-(1-beta_end)</code> 的1000个数字）而后就是定义好加噪比较重要的几个参数：$\alpha$ 以及迭代次数 $t$，对于<code>self.alphas_cumprod</code>则是直接计算<strong>累乘得到的结果</strong>。上面过程对应：<br/><img src="https://s2.loli.net/2025/07/22/aVTbcnwKBNj4plg.webp" alt="image" width="653" height="97" loading="lazy" decoding="async"/><br/> 2、加噪过程（<strong>DDPM和DDIM中没什么差异</strong>）。这个整个过程也比较简单就是直接通过计算：$X_T=\sqrt{\bar{\alpha_T}}x_0+ \sqrt{1- \bar{\alpha_T}}\epsilon$<br/> 3、生成过程。输入三个参数分别表示：<strong>1、model_output</strong>：模型预测得到的噪声数值；<strong>2、timestep</strong>：时间步；<strong>3、sample</strong>：就是我们加载后的$x_t$（最开始就是一个纯噪声随着迭代逐渐“清晰”）。生成图像过程中无疑就是直接通过$t$去推导 $t-1$的图像结果，因此<strong>在DDPM生成过程中</strong> 首先是分别计算 $\alpha_{t}$以及 $\alpha_{t-1}$，不过生成过程有三种。</p><ul><li><code>epsilon</code>：预测噪声 $\epsilon$（将上面加噪公司逆推得到$x_0$）</li><li><code>sample</code>：直接用 $x_0$就是模型的输出</li><li><code>v_prediction</code>：预测$v$（Stable Diffusion 2.x一般就是这个）</li></ul><p>最重要的是后面的 <code>important</code>部分代码，在DDPM中需要计算：</p> \[x_{t-1}=\frac{\sqrt{\bar{\alpha}_{t-1}}\beta_{t}}{1-\bar{\alpha}_{t}}\mathbf{x}_{0}+\frac{\sqrt{\alpha_{t}}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_{t}}\mathbf{x}_{t}\] <p>代码中对应：</p><pre><code class="language-python">pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * current_beta_t) / beta_prod_t
current_sample_coeff = current_alpha_t ** (0.5) *beta_prod_t_prev / beta_prod_t
pred_prev_sample = pred_original_sample_coeff *pred_original_sample + current_sample_coeff * sample
</code></pre><p>最后在模型里面会返回两部分内容：1、pred_prev_sample；2、pred_original_sample。对于这两个值分别表示的是：模型认为最终的干净图像（完全无噪声）（pred_original_sample）。采样一步后，预计在第 499 步应该长的样子（pred_prev_sample）。<strong>对比在DDIM中的差异</strong>，第一个就是<strong>时间步处理差异</strong>，在DDPM中直接用$t-1$来获取上一步就行，但是在DDIM中需要计算<code>timestep - self.config.num_train_timesteps // self.num_inference_steps</code>这是因为DDIM会使用“跳步”；2、在计算 $x_0$上两者之间不存差异，只是计算上一步在公式上存在差异（DDIM计算公式）：<br/><img src="https://s2.loli.net/2025/08/06/7VyP3ENhK5rWscO.webp" alt="image.png" width="1055" height="152" loading="lazy" decoding="async"/></p><pre><code class="language-python">variance = self._get_variance(timestep, prev_timestep)
std_dev_t = eta * variance ** (0.5)
if use_clipped_model_output:
    pred_epsilon = (sample - alpha_prod_t ** (0.5) * pred_original_sample) / beta_prod_t ** (0.5)
pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * pred_epsilon
prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction
</code></pre><h2 id="不同调度器生成对比">不同调度器生成对比</h2><blockquote><p>只是简单对比不同调度器在生成效果上的速度差异（SDXL模型）</p></blockquote><p><a href="https://1drv.ms/f/c/667854cf645e8766/ElCNxPu93Q5Cp1Tqq8YbVUsBV-pVyGG6HG3FJ2AXAxDYDg?e=0H9btC">不同调度器生成对比</a>，从上面简单比较发现一般来说需要20-30步（建立在不适用LCM模型基础上）才能生成一个效果较好的图像，从测试过程发现基本（20-30步）一张图片消耗时间为0.2s左右（A100-80G以及使用<code>float16</code>）。从上面的测试结果上来看<code>UniPCMultistepScheduler</code>和 <code>DPMSolverMultistepScheduler</code>测试的效果最好（仅仅只从迭代步数上分析），借用ChatGPT对不同生成器的分析如下：<br/><img src="https://s2.loli.net/2025/07/22/LbkEu5hO7y8PURj.webp" alt="image.png" width="942" height="316" loading="lazy" decoding="async"/></p><p>不过如果去仔细看生成图像的细节内容的话（单独对比了Unip、DPM、DDIM从10-50步使用的模型是SDXL并且使用<code>float16</code>）得到测试<a href="https://1drv.ms/f/c/667854cf645e8766/ElCNxPu93Q5Cp1Tqq8YbVUsBV-pVyGG6HG3FJ2AXAxDYDg?e=0H9btC">结果</a></p><blockquote><p>此过程使用的prompt（直接GPT生成）：</p><pre><code class="language-python">validation_prompt = [
    # 1. 动态多主体 + 复杂光影
    "A fierce dog fighting a cat inside a Victorian living room, ultra realistic fur, dynamic cinematic lighting, motion blur, 8k hyper detail, dramatic shadows, volumetric fog",
    # 2. 科幻场景 + 光影/反射
    "A futuristic cyberpunk city skyline at sunset with flying cars, glowing neon signs, reflective glass skyscrapers, cinematic wide-angle shot, ultra realistic 8k textures, complex lighting, atmospheric haze",
    # 3. 写实人像 + 材质对比
    "A close-up portrait of an elderly woman with deep wrinkles, realistic skin texture, soft diffused lighting, cinematic depth of field, ultra detailed eyes, 8k hyper realism",
    # 4. 自然风景 + 微距细节
    "A crystal-clear mountain lake surrounded by pine trees, sunlight filtering through mist, hyper realistic reflections on water, ultra detailed rocks and moss, 8k cinematic composition",
    # 5. 奇幻场景 + 复杂材质
    "A medieval knight in silver armor standing in a glowing enchanted forest, bioluminescent plants, soft god rays, ultra detailed metallic reflections, cinematic epic fantasy lighting, 8k resolution",
    # 6. 室内物品 + 精细纹理
    "A rustic wooden table with a vintage pocket watch, spilled coffee, handwritten letters, soft morning sunlight, ultra detailed textures, shallow depth of field, 8k macro photography style"
]
negative_prompt = "blurry, low quality, distorted, extra limbs, deformed, low contrast, unrealistic lighting, bad anatomy, oversaturated"
</code></pre></blockquote><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/abs/2010.02502">https://arxiv.org/abs/2006.11239</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/abs/2010.02502">https://arxiv.org/abs/2010.02502</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Sun, 06 Jul 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/07/06/DFscheduler.html</guid></item><item><title>深入浅出了解生成模型-6：常用图像生成基座模型与 Adapters等解析</title><link>https://www.big-yellow-j.top/posts/2025/07/06/DFBaseModel.html</link><description><![CDATA[本文主要介绍基于Unet和Dit框架的基座扩散模型，重点对比SD1.5与SDXL的核心差异，包括CLIP编码器（SDXL采用双编码器拼接提升文本理解能力）、图像输出维度（SDXL默认1024x1024优于SD1.5的512x512）及技术优化策略。还涵盖Imagen的多阶段生成与动态调整方法，Dit模型的patch切分与adaLN模块，Hunyuan-DiT的双文本编码器与旋转位置编码，FLUX.1的VAE通道优化与旋转位置编码，以及SD3的三文本编码器与MM-Dit架构。同时涉及VAE模型重构表现对比、guidance_rescale参数对生成效果的影响，和Adapters技术如Con...]]></description><content:encoded><![CDATA[<h2 id="基座扩散模型">基座扩散模型</h2><p>主要介绍基于Unet以及基于Dit框架的基座扩散模型以及部分GAN和VAE模型，其中SD迭代版本挺多的（从1.2到3.5）因此本文主要重点介绍SD 1.5以及SDXL两个基座模型，以及两者之间的对比差异，除此之外还有许多闭源的扩散模型比如说Imagen、DALE等。对于Dit基座模型主要介绍：Hunyuan-DiT、FLUX.1等。对于各类模型评分网站（模型评分仁者见仁智者见智，特别是此类生成模型视觉图像生成是一个很主观的过程，同一张图片不同人视觉感官都是不同的）：<a href="https://lmarena.ai/leaderboard">https://lmarena.ai/leaderboard</a></p><h3 id="sd系列模型">SD系列模型</h3><h4 id="sd15-vs-sdxl">SD1.5 vs SDXL</h4><blockquote><p><strong>SDv1.5</strong>: https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5<br/><strong>SDXL</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>:https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0</p></blockquote><p>两者模型详细的模型结构：<a href="https://drive.google.com/file/d/1-fiJKHVANsghaviyIha8MbbC2Yz4ld6L/view?usp=sharing">SDv1.5–SDXL模型结构图</a>，其中具体模型参数的对比如下：<br/><strong>1、CLIP编码器区别</strong>：<br/> 在SD1.5中选择的是<strong>CLIP-ViT/L</strong>（得到的维度为：768）而在SDXL中选择的是两个CLIP文本编码器：<strong>OpenCLIP-ViT/G</strong>（得到的维度为：1280）以及<strong>CLIP-ViT/L</strong>（得到维度为：768）在代码中对于两个文本通过编码器处理之后SDXL直接通过cat方式拼接：<code>prompt_embeds = torch.concat(prompt_embeds_list, dim=-1)</code> 也就是说最后得到的维度为：[..,..,1280+768]。最后效果很明显：<strong>SDXL对于文本的理解能力大于SD1.5</strong><br/><strong>2、图像输出维度区别</strong>：<br/> 再SD1.5中的默认输出是：512x512而再SDXL中的默认输出是：1024x1024，如果希望将SD1.5生成的图像处理为1024x1024可以直接通过超分算法来进行处理，除此之外在SDXL中还会使用一个refiner模型（和Unet的结构相似）来强化base模型（Unet）生成的效果。<br/><strong>3、SDXL论文中的技术细节</strong>：</p><ul><li>1、<strong>图像分辨率优化策略</strong>。</li></ul><p>数据集中图像的尺寸图像利用率问题（选择512x512舍弃256x256就会导致图像大量被舍弃）如果通过超分辨率算法将图像就行扩展会放大伪影，这些伪影可能会泄漏到最终的模型输出中，例如，导致样本模糊。（The second method, on the other hand, usually introduces upscaling artifacts which may leak into the final model outputs, causing, for example, blurry samples.）作者做法是：<strong>训练阶段</strong>直接将原始图像的分辨率 $c=(h_{org},w_{org})$作为一个条件，通过傅里叶特征编码而后加入到time embedding中，<strong>推理阶段</strong>直接将分辨率作为一个条件就行嵌入，进而实现：<strong>当输入低分辨率条件时，生成的图像较模糊；在不断增大分辨率条件时，生成的图像质量不断提升。</strong><br/><img src="https://s2.loli.net/2025/07/09/pMcLmdHThu2CnNx.webp" alt="image.png" width="1083" height="719" loading="lazy" decoding="async"/></p><ul><li>2、<strong>图像裁剪优化策略</strong></li></ul><p>直接统一采样裁剪坐标top和cleft（分别指定从左上角沿高度和宽度轴裁剪的像素数量的整数），并通过傅里叶特征嵌入将它们作为调节参数输入模型，类似于上面描述的尺寸调节。第1，2点代码中的处理方式为：</p><pre><code class="language-python">def _get_add_time_ids(
        self, original_size, crops_coords_top_left, target_size, dtype, text_encoder_projection_dim=None
    ):
    add_time_ids = list(original_size + crops_coords_top_left + target_size)

    passed_add_embed_dim = (
        self.unet.config.addition_time_embed_dim * len(add_time_ids) + text_encoder_projection_dim
    )
    expected_add_embed_dim = self.unet.add_embedding.linear_1.in_features
    ...
    add_time_ids = torch.tensor([add_time_ids], dtype=dtype)
    return add_time_ids
</code></pre><blockquote><p><strong>推荐阅读</strong>：<br/> 1、<a href="https://www.magicflow.ai/showcase/sd3-sdxl-sd1.5">SDv1.5-SDXL-SD3生成效果对比</a></p></blockquote><h4 id="sd3模型">SD3模型</h4><blockquote><p>SD3的diffusers官方文档：<a href="https://huggingface.co/docs/diffusers/en/api/pipelines/stable_diffusion/stable_diffusion_3#diffusers.StableDiffusion3Pipeline">StableDiffusion3Pipeline</a></p></blockquote><p>https://zhouyifan.net/2024/09/03/20240809-flux1/<br/> SD3<sup id="fnref:12" role="doc-noteref"><a href="#fn:12" class="footnote" rel="footnote">2</a></sup>、FLUX对于这几组模型的前世今生不做介绍，主要了解其模型结构以及论文里面所涉及到到的一些知识点。首先介绍SD3模型在模型改进上<sup id="fnref:16" role="doc-noteref"><a href="#fn:16" class="footnote" rel="footnote">3</a></sup>：1、改变训练时噪声采样方法；2、将一维位置编码改成二维位置编码；3、提升 VAE 隐空间通道数（作者实验发现最开始VAE会将模型<strong>下采样8倍数并且处理通道为4的空间</strong>，也就是说 $512 \times 512 \times 3 \rightarrow 64\times 64 \times 4$，不过在 <strong>SD3</strong>中将通道数由<strong>4改为16</strong>）；4、对注意力 QK 做归一化以确保高分辨率下训练稳定。<br/><img src="https://s2.loli.net/2025/09/01/R5HI3yLPXBEbtzQ.webp" alt="image" width="1112" height="748" loading="lazy" decoding="async"/><br/> 其中SD3模型的整体框架如上所述:<br/><strong>1、文本编码器处理</strong>（<a href="https://github.com/huggingface/diffusers/blob/0f252be0ed42006c125ef4429156cb13ae6c1d60/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py#L972">代码</a>），在text encoder上SD3使用三个文本编码器：<code>clip-vit-large-patch14</code>、 <code>laion/CLIP-ViT-bigG-14-laion2B-39B-b160k</code> 、 <code>t5-v1_1-xxl</code> ，对于这3个文本编码器对于文本的处理过程为：就像SDXL中一样首先3个编码器分别都去对文本进行编码，首先对于两个<a href="https://github.com/huggingface/diffusers/blob/0f252be0ed42006c125ef4429156cb13ae6c1d60/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py#L289">CLIP的文本编码</a>处理过程为直接通过CLIP进行 <code>prompt_embeds = text_encoder(text_input_ids.to(device)...)</code> 而后去选择 <code>prompt_embeds.hidden_states[-(clip_skip + 2)]</code>（默认条件下 <code>clip_skip=None</code>也就是<strong>直接选择倒数第二层</strong>）那么最后得到文本编码的维度为：<code>torch.Size([1, 77, 768]) torch.Size([1, 77, 1280])</code> 而<a href="https://github.com/huggingface/diffusers/blob/0f252be0ed42006c125ef4429156cb13ae6c1d60/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py#L233">T5的encoder</a>就比较检查直接通过encoder进行编码，那么其编码维度为：<code>torch.Size([1, 256, 4096])</code>，这样一来就会得到3组的文编码，对于CLIP的编码结果直接通过 <code>clip_prompt_embeds=torch.cat([prompt_embed, prompt_2_embed], dim=-1)</code> 即可，在将得到后的 <code>clip_prompt_embeds</code>结果再去和T5的编码结果进行拼接之前会首先 <code>clip_prompt_embeds=torch.nn.functional.pad(clip_prompt_embeds, (0, t5_prompt_embed.shape[-1] - clip_prompt_embeds.shape[-1]))</code> 而后将T5的文本内容和 <code>clip_prompt_embeds</code>进行合并 <code>prompt_embeds = torch.cat([clip_prompt_embeds, t5_prompt_embed], dim=-2)</code>。由于使用T5模型导致模型的参数比较大进导致模型的显存占用过大（2080Ti等GPU上轻量化的部署推理SD 3模型，可以只使用CLIP ViT-L + OpenCLIP ViT-bigG的特征，此时需要<strong>将T5-XXL的特征设置为zero</strong>（不加载）<sup id="fnref:14" role="doc-noteref"><a href="#fn:14" class="footnote" rel="footnote">4</a></sup>），选择<strong>不去使用T5模型会对模型对于文本的理解能力有所降低</strong>。<br/><img src="https://s2.loli.net/2025/09/01/RdQCOmXeMfwUYsh.webp" alt="image.png" width="1028" height="819" loading="lazy" decoding="async"/></p><blockquote><p>SD3使用T5-XXL模型。这使得以少于24GB的VRAM在GPU上运行模型，即使使用FP16精度。因此如果需要使用就需要：1、将部分模型<a href="https://github.com/huggingface/diffusers/blob/0f252be0ed42006c125ef4429156cb13ae6c1d60/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py#L186">下放到CPU上</a>；2、直接取消T5的使用（<code>StableDiffusion3Pipeline.from_pretrained("stabilityai/stable-diffusion-3-medium-diffusers",text_encoder_3=None,tokenizer_3=None,torch_dtype=torch.float16)</code>）。<br/> 文本编码过程：1、CLIP编码分别得到：[1, 77, 768]和[1, 77, 1280]；2、T5编码得到：[1, 256, 4096]；3、CLIP文本编码拼接：[1, 77, 2048]在去将其通过pad填充到和T5一致得到最后CLIP编码器维度为：<strong>[1, 77, 4096]</strong>；4、最后文本编码维度：<code>[1, 333, 4096]</code></p></blockquote><p><strong>2、Flow Matching模式</strong>（<a href="https://www.big-yellow-j.top/posts/2025/07/06/DFscheduler.html">原理</a>）；<br/><strong>3、MM-Dit模型架构</strong>（<a href="https://github.com/huggingface/diffusers/blob/d03240801f2ac2b4d1f49584c1c5628b98583f6a/src/diffusers/models/transformers/transformer_sd3.py#L80">代码</a>）：观察上面过程，扩散模型输入无非就是3个内容：1、时间步（$y$）；2、加噪处理的图像（$x$）；3、文本编码（$c$）。首先对于 <strong>时间步</strong>而言处理过程为：直接通过 Sin位置编码然后去和CLIP（两个合并的）进行组合即可对于另外两个部分直接通过<a href="https://github.com/huggingface/diffusers/blob/d03240801f2ac2b4d1f49584c1c5628b98583f6a/src/diffusers/models/transformers/transformer_sd3.py#L80">代码</a>进行理解：</p><pre><code class="language-python">def forward(
    self,
    hidden_states: torch.Tensor, # 加噪声的图片 (batch size, channel, height, width)
    encoder_hidden_states: torch.Tensor = None, # 条件编码比如说：文本prompt (batch size, sequence_len, embed_dims)
    pooled_projections: torch.Tensor = None, # 池化后的条件编码 (batch size, embed_dims)
    timestep: torch.LongTensor = None, # 时间步编码
    block_controlnet_hidden_states: List = None,
    joint_attention_kwargs: Optional[Dict[str, Any]] = None,
    return_dict: bool = True,
    skip_layers: Optional[List[int]] = None,
) -&gt; Union[torch.Tensor, Transformer2DModelOutput]:
    ...
    height, width = hidden_states.shape[-2:]
    # Step-1 
    hidden_states = self.pos_embed(hidden_states) # 直接使用 2D的位置编码
    temb = self.time_text_embed(timestep, pooled_projections)
    encoder_hidden_states = self.context_embedder(encoder_hidden_states) # 一层线性映射
    ...
    # Step-2
    for index_block, block in enumerate(self.transformer_blocks):
        is_skip = True if skip_layers is not None and index_block in skip_layers else False
        if torch.is_grad_enabled() and self.gradient_checkpointing and not is_skip:
            ...
        elif not is_skip:
            encoder_hidden_states, hidden_states = block(
                hidden_states=hidden_states,
                encoder_hidden_states=encoder_hidden_states,
                temb=temb,
                joint_attention_kwargs=joint_attention_kwargs,
            )
        ...
    # Step-3
    hidden_states = self.norm_out(hidden_states, temb)
    hidden_states = self.proj_out(hidden_states)
    patch_size = self.config.patch_size
    height = height // patch_size
    width = width // patch_size

    hidden_states = hidden_states.reshape(
        shape=(hidden_states.shape[0], height, width, patch_size, patch_size, self.out_channels)
    )
    hidden_states = torch.einsum("nhwpqc-&gt;nchpwq", hidden_states)
    output = hidden_states.reshape(
        shape=(hidden_states.shape[0], self.out_channels, height * patch_size, width * patch_size)
    )
    ...
    if not return_dict:
        return (output,)
    return Transformer2DModelOutput(sample=output)
</code></pre><p><strong>Step-1</strong>：首先去将图像 $x$使用2D 正弦-余弦位置编码进行处理，对于时间步直接sin位置编码，对于条件（文本prompt等）直接通过一层线性编码处理。<br/><strong>Step-2</strong>：然后就是直接去计算Attention：<code>encoder_hidden_states, hidden_states = block(hidden_states=hidden_states,encoder_hidden_states=encoder_hidden_states,temb=temb,joint_attention_kwargs=joint_attention_kwargs,)</code>，对于这个<a href="https://github.com/huggingface/diffusers/blob/d03240801f2ac2b4d1f49584c1c5628b98583f6a/src/diffusers/models/attention.py#L570">block</a>的设计过程为：<br/><img src="https://s2.loli.net/2025/09/01/gLkSbrt9vfyQ5uJ.webp" alt="image" width="454" height="741" loading="lazy" decoding="async"/></p><pre><code class="language-python">def forward(
    self,
    hidden_states: torch.FloatTensor,
    encoder_hidden_states: torch.FloatTensor,
    temb: torch.FloatTensor,
    joint_attention_kwargs: Optional[Dict[str, Any]] = None,
):
    joint_attention_kwargs = joint_attention_kwargs or {}
    # Aeetntion Step-1
    if self.use_dual_attention:
        norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp, norm_hidden_states2, gate_msa2 = self.norm1(
            hidden_states, emb=temb
        )
    else:
        ...

    if self.context_pre_only:
        ...
    else:
        norm_encoder_hidden_states, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.norm1_context(
            encoder_hidden_states, emb=temb
        )
    # Attention Step-2
    attn_output, context_attn_output = self.attn(
        hidden_states=norm_hidden_states,
        encoder_hidden_states=norm_encoder_hidden_states,
        **joint_attention_kwargs,
    )
    attn_output = gate_msa.unsqueeze(1) * attn_output
    hidden_states = hidden_states + attn_output
    ...
    norm_hidden_states = self.norm2(hidden_states)
    norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
    if self._chunk_size is not None:
        ...
    else:
        ff_output = self.ff(norm_hidden_states)
    ff_output = gate_mlp.unsqueeze(1) * ff_output
    hidden_states = hidden_states + ff_output
    if self.context_pre_only:
        ...

    return encoder_hidden_states, hidden_states
</code></pre><p>计算注意力过程中，首先 <strong>Attention Step-1</strong>：正则化处理（正如上面<a href="https://www.big-yellow-j.top/posts/2025/07/06/DFBaseModel.html#:~:text=%E5%9C%A8layernorm%E4%B8%AD%E4%B8%80%E8%88%AC%E5%BD%92%E4%B8%80%E5%8C%96%E5%A4%84%E7%90%86%E6%96%B9%E5%BC%8F%E4%B8%BA">Dit中</a>的一样将条件拆分为几个参数，观察SD3图中的MMDit设计，会将 <strong>加噪声处理的图片</strong> 和 <strong>条件编码</strong>都去（处理方式相同）通过 “正则化”，在SD3中处理方式为，直接 <code>shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp, shift_msa2, scale_msa2, gate_msa2 = emb.chunk(9, dim=1)</code> 拆分之后去通过 <code>LayerNorm</code>处理之后得到 <code>norm_hidden_states</code> 而后在去计算 <code>norm_hidden_states * (1 + scale_msa[:, None]) + shift_msa[:, None]</code>）然后后面处理过程就比较简单和上面的流程图是一样的。<br/> 这样一来一个MMDit block就会返回两部分结果 <code>encoder_hidden_states</code>, <code>hidden_states</code>（区<strong>别Dit之间在于，MMDit是将image和text两种模态之间的信息进行融合二Dit只是使用到imgae一种模态</strong>）<br/><strong>Step-3</strong>就比较简单就是一些norm等处理。<br/><strong>总的来说</strong>MMDiT Block 的输入主要有三部分：<strong>时间步嵌入</strong> $y$：通过一个 MLP 投影，得到一组参数，用于调节 Block 内的 LayerNorm / Attention / MLP（类似 FiLM conditioning）。<strong>图像 token</strong> $x$：由加噪图像 latent patch embedding 得到，并加上 2D 正弦余弦位置编码。<strong>文本 token</strong> $c$：来自文本编码器的输出，一般带有 1D 位置编码。<strong>Block 内部机制</strong>：将 $x$ 和 $c$ 拼接在一起，作为 Transformer 的输入序列。在自注意力层中，$x$ token 能和 $c$ token 交互，从而实现 跨模态融合。$y$（timestep embedding）通过投影提供额外的条件控制。</p><blockquote><p><strong>2D 正弦-余弦位置编码</strong><br/><img src="https://s2.loli.net/2025/09/01/lwZns5H9vTpeOgU.webp" alt="image" width="1047" height="461" loading="lazy" decoding="async"/><br/> 左侧为一般的位置编码方式，但是有一个缺点：生成的图像的分辨率是无法修改的。比如对于上图，假如采样时输入大小不是4x3，而是4x5，那么0号图块的下面就是5而不是4了，模型训练时学习到的图块之间的位置关系全部乱套，因此就通过2D位置去代表每一块的位置信息。</p></blockquote><p>简单补充介绍一下Flux.1模型（主要是文生图模型），对于<strong>FLUX.1模型而言其结构如下</strong><br/><img src="https://s2.loli.net/2025/09/01/WTD97u3eFiQwr4d.webp" alt="image" width="922" height="770" loading="lazy" decoding="async"/><br/> 区别SD3模型在于，FLUX.1在文本编码器选择上<strong>只使用了2个编码器</strong>（CLIPTextModel、T5EncoderModel）并且FLUX.1 VAE架构依然继承了SD 3 VAE的<strong>8倍下采样和输入通道数（16）</strong>。在FLUX.1 VAE输出Latent特征，并在Latent特征输入扩散模型前，还进行了 <code>_pack_latents</code>操作，一下子将Latent<strong>特征通道数提高到64（16 -&gt; 64）</strong>，换句话说，FLUX.1系列的扩散模型部分输入通道数为64，是SD 3的四倍。对于 <code>_pack_latents</code>做法是会将一个 $2\times 2$的像素去补充到通道中。</p><pre><code class="language-python">def _pack_latents(latents, batch_size, num_channels_latents, height, width):
    latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
    latents = latents.permute(0, 2, 4, 1, 3, 5)
    latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
    return latents
</code></pre><p>除去改变text的编码器数量以及VAE的通道数量之外，FLUX.1还做了如下的改进：FLUX.1 没有做 Classifier-Free Guidance (CFG)（对于CFG一般做法就是直接去将“VAE压缩的图像信息变量复制两倍” <code>torch.cat([latents] * 2)</code>，文本就是直接将negative_prompt的编码补充到文本编码中 <code>torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)</code>）而是把指引强度 guidance 当成了一个和时刻 t 一样的约束信息，传入去噪模型 transformer 中。在transformer模型结构设计中，SD3是<strong>直接对图像做图块化，再设置2D位置编码</strong><code>PatchEmbed</code>，在FLUX.1中使用的是 <code>FluxPosEmbed</code>（旋转位置编码）</p><pre><code class="language-python"># SD3
self.pos_embed = PatchEmbed(height=sample_size,width=sample_size,patch_size=patch_size,in_channels=in_channels,)
embed_dim=self.inner_dim,pos_embed_max_size=pos_embed_max_size,  # hard-code for now.)
# FLUX.1
self.pos_embed = FluxPosEmbed(theta=10000, axes_dim=axes_dims_rope)
</code></pre><h3 id="imagen">Imagen</h3><blockquote><p>https://imagen.research.google/<br/> https://deepmind.google/models/imagen/<br/> 非官方实现：https://github.com/lucidrains/imagen-pytorch<br/> 类似Github，通过3阶段生成：https://github.com/deep-floyd/IF</p></blockquote><p>Imagen<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">5</a></sup>论文中主要提出：1、纯文本语料库上预训练的通用大型语言模型（例如<a href="https://huggingface.co/collections/google/t5-release-65005e7c520f8d7b4d037918">T5</a>、CLIP、BERT等）在编码图像合成的文本方面非常有效：在Imagen中增加语言模型的大小比增加图像扩散模型的大小更能提高样本保真度和Imagetext对齐。<br/><img src="https://s2.loli.net/2025/07/12/lCFNWwDmgGnZueE.webp" alt="image" width="1050" height="275" loading="lazy" decoding="async"/><br/> 2、通过提高classifier-free guidance weight（$\epsilon(z,c)=w\epsilon(z,c)+ (1-w)\epsilon(z)$ 也就是其中的参数 $w$）可以提高image-text之间的对齐，但会损害图像逼真度，产生高度饱和不自然的图像（论文里面给出的分析是：每个时间步中预测和正式的x都会限定在 $[-1,1]$这个范围但是较大的 $w$可能导致超出这个范围），论文里面做法就是提出 <strong>动态调整方法</strong>：在每个采样步骤中，我们将s设置为 $x_0^t$中的某个百分位绝对像素值，如果s&gt;1，则我们将 $x_0^t$阈值设置为范围 $[-s,s]$，然后除以s。<br/><img src="https://s2.loli.net/2025/07/12/jAEBS7I1Ob6DPal.webp" alt="image" width="1297" height="569" loading="lazy" decoding="async"/><br/> 3、和上面SD模型差异比较大的一点就是，在imagen中直接使用多阶段生成策略，模型先生成64x64图像再去通过超分辨率扩散模型去生成256x256以及1024x1024的图像，在此过程中作者提到使用noise conditioning augmentation（NCA）策略（<strong>对输入的文本编码后再去添加随机噪声</strong>）<br/><img src="https://s2.loli.net/2025/07/12/HJm96oPr2AlXICs.webp" alt="image" width="717" height="708" loading="lazy" decoding="async"/></p><h3 id="dit">Dit</h3><blockquote><p>https://github.com/facebookresearch/DiT</p></blockquote><p><img src="https://s2.loli.net/2025/07/15/CUisy5TPE24kKaH.webp" alt="image" width="1317" height="566" loading="lazy" decoding="async"/><br/> Dit<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">6</a></sup>模型结构上，1、<strong>模型输入</strong>，将输入的image/latent切分为不同patch而后去对不同编码后的patch上去添加位置编码（直接使用的sin-cos位置编码），2、<strong>时间步以及条件编码</strong>，对于时间步t以及条件c的编码而后将两部分编码后的内容进行相加，在 <code>TimestepEmbedder</code>上处理方式是：直接通过<strong>正弦时间步嵌入</strong>方式而后将编码后的内容通过两层liner处理；在 <code>LabelEmbedder</code>处理方式上就比较简单直接通过 <code>nn.Embedding</code>进行编码处理。3、使用Adaptive layer norm（adaLN）block以及adaZero-Block（对有些参数初始化为0，就和lora中一样初始化AB为0，为了保证后续模型训练过程中的稳定）</p><blockquote><p>在<a href="https://docs.pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html">layernorm</a>中一般归一化处理方式为：$\text{Norm}(x)=\gamma \frac{x-\mu}{\sqrt{\sigma^2+ \epsilon}}+\beta$ 其中有两个参数 $\gamma$ 和 $\beta$ 是固定的可学习参数（比如说直接通过 <code>nn.Parameter</code> 进行创建），在模型初始化时创建，并在训练过程中通过梯度下降优化。但是在 adaLN中则是直接通过 $\text{Norm}(x)=\gamma(c) \frac{x-\mu}{\sqrt{\sigma^2+ \epsilon}}+\beta(c)$ 通过输入的条件c进行学习的<br/> 总结Dit训练过程：首先将图片通过VAE进行编码，而后将编码后的内容patch化然后输入到Ditblock（就是通过一些Attention结构进行堆叠）中去预测模型的噪声</p></blockquote><h3 id="hunyuan-dit">Hunyuan-DiT</h3><blockquote><p>https://huggingface.co/Tencent-Hunyuan/HunyuanDiT</p></blockquote><p>腾讯的Hunyuan-DiT<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">7</a></sup>模型整体结构<br/><img src="https://s2.loli.net/2025/07/15/Hum9FCtPbV7do1B.webp" alt="image" width="831" height="494" loading="lazy" decoding="async"/><br/> 整体框架不是很复杂，1、文本编码上直接通过结合两个编码器：CLIP、T5；2、VAE则是直接使用的SD1.5的；3、引入2维的旋转位置编码；4、在Dit结构上（图片VAE压缩而后去切分成不同patch），使用的是堆叠的注意力模块（在SD1.5中也是这种结构）self-attention+cross-attention（此部分输入文本）。论文里面做了改进措施：1、借鉴之前处理，计算attention之前首先进行norm处理（也就是将norm拿到attention前面）。<br/> 简短了解一下模型是如何做数据的：<br/><img src="https://s2.loli.net/2025/07/15/dJZETbyHB6SQPKI.webp" alt="image" width="994" height="719" loading="lazy" decoding="async"/></p><h3 id="pixart">PixArt</h3><blockquote><p>https://pixart-alpha.github.io/</p></blockquote><p>华为诺亚方舟实验室提出的 $\text{PixArt}-\alpha$模型整体框架如下：<br/><img src="https://s2.loli.net/2025/07/15/cWTtLdONRPC9fnz.webp" alt="image" width="527" height="694" loading="lazy" decoding="async"/><br/> 相比较Dit模型论文里面主要进行的改进如下：<br/> 1、<strong>Cross-Attention layer</strong>，在DiT block中加入了一个多头交叉注意力层，它位于自注意力层（上图中的Multi-Head Self<br/> -Attention）和前馈层（Pointwise Feedforward）之间，使模型能够灵活地引入文本嵌入条件。此外，为了利用预训练权重，将交叉注意力层中的输出投影层初始化为零，作为恒等映射，保留了输入以供后续层使用。<br/> 2、AdaLN-single，在Dit中的adaptive normalization layers（adaLN）中部分参数（27%）没有起作用（在文生图任务中）将其替换为adaLN-single</p><h3 id="flux系列模型">Flux系列模型</h3><p>对于Flux系列模型截至到2026-02-14，主要用的比较多的就是Context以及较新的Klein模型（都是文生图+图生图模型），这里简单介绍一下这两个模型。</p><h4 id="flux1-context模型">Flux.1-Context模型</h4><p>对于Flux-Context<sup id="fnref:19" role="doc-noteref"><a href="#fn:19" class="footnote" rel="footnote">8</a></sup>模型在官方hf上开源了多种系列模型，主要流行的有3个版本：1、MAX和PRO：商用闭源；2、DEV：开源-12B，对于dev其官方权重地址为：<a href="https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev">https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev</a>除此之外相对使用比较多的是其通过nunchakus量化后的模型权重：<a href="https://huggingface.co/mit-han-lab/nunchaku-flux.1-kontext-dev/tree/main">mit-han-lab/nunchaku-flux.1-kontext-dev</a>。简单介绍一下其模型原理如下：<br/><img src="https://files.seeusercontent.com/2026/05/07/4ncO/image.webp" alt="1087X533/image.png"/><br/> 仅从上面模型架构很容易发现，在整体架构上还是<strong>采用的是DiT模型框架核心差异在于将原图进行补充输入</strong>，按照论文中的描述其生成过程为：$p(x\vert y,c)$：1、提示词文本输入，对应公式中的c，2、原图输入（这个主要是对于图像编辑任务而言需要的）对应公式中的y，如果是文生图那么$y=\phi$；3、噪声输入（将噪声转换为我的目标输入图像）。区别其他的图像编辑模型，一般而言会直接对原图进行vae编码而后添加噪声，在context上保持原图进行输入，对于这两部分输入都会去通过vae进行编码而后去对编码后的内容直接进行拼接，比如说：</p><pre><code class="language-python">if image_reference_latents is not None:
    latent_model_input = torch.cat([latents, image_reference_latents], dim=1)
elif image_latents is not None:
    latent_model_input = torch.cat([latents, image_latents], dim=1)
</code></pre><p>其中latents、image_reference_latents、image_latents分别表示添加噪声的原始图像、参考图像vae编码、原始图像vae编码。进一步对于其模型结构如下：<br/><img src="https://files.seeusercontent.com/2026/05/07/0Tge/image.webp" alt="478X715"/><br/> 按照论文中第2章描述：从头训练的16通道VAE潜在空间里。它混合使用了19个double-stream（前期分离）和 38个single-stream（后期融合）的 Transformer 结构，用fused FFN 提效，用3D RoPE做位置编码。堆区其中提到的<strong>两种stream方式</strong>简单总结如下：所谓的double以及single无非就是对特征分别使用单独的linear去分别处理文本以及图像特征，而对于single则是将两部分特征拼接起来再用linear处理。<strong>3D rope</strong>：指的是在上述提到的2D位置编码上引入时间特征t。</p><h4 id="flux2-klein">Flux.2-Klein</h4><p>对于Klein模型官方分别开源了：9B、4B两种版本以及所对应的蒸馏版本，其中4B可以被使用与商业用途。Klein模型在模型的推理以及显存占用上都是极大的改善<br/><img src="https://files.seeusercontent.com/2026/05/07/Biz9/image.webp" alt="1741X665"/></p><h3 id="qwen-image系列模型">Qwen image系列模型</h3><h4 id="qwen-image">Qwen image</h4><blockquote><p>官方blog：<a href="https://qwenlm.github.io/zh/blog/qwen-image/">https://qwenlm.github.io/zh/blog/qwen-image/</a><br/> Qwen Image图片编辑：<a href="https://huggingface.co/Qwen/Qwen-Image-Edit">https://huggingface.co/Qwen/Qwen-Image-Edit</a><br/> Qwen Image：<a href="https://huggingface.co/Qwen/Qwen-Image">https://huggingface.co/Qwen/Qwen-Image</a><br/> Qwen Image Lora微调8步生图：<a href="https://huggingface.co/lightx2v/Qwen-Image-Lightning">https://huggingface.co/lightx2v/Qwen-Image-Lightning</a><br/> Qwen Image图片编辑int4量化版本：<a href="https://huggingface.co/nunchaku-tech/nunchaku-qwen-image">https://huggingface.co/nunchaku-tech/nunchaku-qwen-image</a>，<a href="https://github.com/nunchaku-tech/nunchaku/blob/main/examples/v1/qwen-image.py">代码</a></p></blockquote><p>Qwen image<sup id="fnref:18" role="doc-noteref"><a href="#fn:18" class="footnote" rel="footnote">9</a></sup>无论是多行文字、段落布局，还是中英文等不同语种，Qwen-Image都能以极高的保真度进行渲染，尤其在处理复杂的中文（logographic languages）方面，表现远超现有模型（不过目前：2025.08.29模型全权重加载的话一般设备很难使用，不过又量化版本可以尝试）模型整体结构：<br/><img src="https://s2.loli.net/2025/09/01/U7HqQcJxZ96SN3A.webp" alt="image" width="1139" height="667" loading="lazy" decoding="async"/><br/> 整体框架上还是MMDit结构和上面的SD3都是一致的，不过模型的改进在于：1、区别之前的都是使用CLIP模型去对齐图片-文本之间信息，在Qwen Image中则是直接使用<strong>Qwen2.5-VL</strong>；2、对于VAE模型则是直接使用<strong>Wan-2.1-VAE</strong>（不过选择冻结encoder部分只去训练decoder部分）；3、模型的结构还是使用MMDit结构，知识将位置编码方式改为<strong>Multimodal Scalable RoPE (MSRoPE)</strong>，位置编码方式<br/><img src="https://s2.loli.net/2025/09/01/QuEY2gWZFzUMlCK.webp" alt="image" width="1105" height="539" loading="lazy" decoding="async"/><br/> 大致框架了解之后细看他的数据是如何收集的以及后处理的：<br/><img src="https://s2.loli.net/2025/09/01/nzrOe2yaBpL5iwF.webp" alt="image" width="1192" height="682" loading="lazy" decoding="async"/><br/> 对于收集到数据之后，论文里面通过如下操作进行后处理：<strong>1、阶段一过滤数据</strong>：模型预训练是在256x256的图片上进行训练的，因此，过滤掉256x256以外的图片还有一些低质量图片等；<strong>2、阶段二图片质量强化</strong>：主要还是过滤一些低质量图片如亮度纹理等；</p><h4 id="z-image-以及-z-image-turbo">Z-Image 以及 Z-Image-Turbo</h4><p>对于两种模型权重地址如下：1、<a href="https://huggingface.co/Tongyi-MAI/Z-Image">Z-Image HuggingFace</a>；2、<a href="https://huggingface.co/Tongyi-MAI/Z-Image-Turbo">Z-Image-Turbo</a>对于两种模型对比如下：<br/><img src="https://files.seeusercontent.com/2026/05/07/s8iT/image.webp" alt="image.png"/><br/> Turbo是对Z-Image进行蒸馏加速版本。对于Z-Image其具体模型结构如下所示（在模型结构上差异不大，关键点在于数据构建过程见：<a href="https://www.big-yellow-j.top/posts/2026/02/15/DataBuild.html">扩散模型数据构建</a>）<br/><img src="https://files.seeusercontent.com/2026/05/07/a3sI/image20260219131344750.webp" alt=""/><br/> 对于上述模型结构，文本编码器：Qwen3-4B；VAE使用Flux VAE；位置编码上使用3D Unified RoPE编码方式，除此之外对于图像编辑任务上使用SigLIP2去提取图像特征。除此之外再上述结构中Q/K-Norm指的是在计算attention之前提前去对Q/K进行归一化处理（一般的注意力计算是对Q/K/V在计算attention之后进行归一化亦或者提前对3者都进行归一化）除此之外其在训练阶段使用策略是：<br/><img src="https://files.seeusercontent.com/2026/05/07/8gCn/image20260219131448332.webp" alt=""/><br/> 在预训练（pre-training）阶段都是使用flow-matching方法进行训练，对于上述过程中首先在低质量预训练（Low-Resolution Pre-training）中使用256分辨率图像进行训练，在Omni Pre-training中则是直接进行更加复制训练任意分辨率、文生图、图生图等预训练。后续的强化学习中先使用DPO做offline alignment在使用GRPO做online alignment。除此之外使用Distribution Matching Distillation（DMD分布匹配蒸馏方式）去蒸馏模型得到Turbo模型</p><h3 id="firered-image-edit模型">FireRed-Image-Edit模型</h3><p><img src="https://www.big-yellow-j.top/posts/2026/03/09/FireRedModel.html" alt="开源模型技术总结-3————FireRed（小红书）开源模型"/></p><h3 id="基座模型对比效果分析">基座模型对比效果分析</h3><p>具体使用测试服务器为3090-24G进行测试，得到<a href="https://xcnsumle7uuf.feishu.cn/wiki/Tu74wPeCEirZ5XkY3WncaET1nod?from=from_copylink">不同模型在文生图效果对比分析结果</a></p><h3 id="vae基座模型">VAE基座模型</h3><p>对于VAE模型在之前的<a href="https://www.big-yellow-j.top/posts/2025/05/11/VAE.html">博客</a>有介绍过具体的原理，这里主要就是介绍几个常见的VAE架构模型（使用过程中其实很少会去修改VAE架构，一般都是直接用SD自己使用的）所以就简单对比一下不同的VAE模型在图片重构上的表，主要是使用此<a href="https://huggingface.co/spaces/rizavelioglu/vae-comparison">huggingface</a>上的进行比较（比较的数值越小越好，就数值而言 <strong>CogView4-6B</strong>效果最佳），下面结果为随便挑选的一个图片进行测试结果：</p><table><thead><tr><th>模型名称</th><th>数值</th><th>时间(s)</th></tr></thead><tbody><tr><td>stable-diffusion-v1-4</td><td>2,059</td><td>0.5908</td></tr><tr><td>eq-vae-ema</td><td>1,659</td><td>0.0831</td></tr><tr><td>eq-sdxl-vae</td><td>1,200</td><td>0.0102</td></tr><tr><td>sd-vae-ft-mse</td><td>1,204</td><td>0.0101</td></tr><tr><td>sdxl-vae</td><td>929</td><td>0.0105</td></tr><tr><td>playground-v2.5</td><td>925</td><td>0.0096</td></tr><tr><td>stable-diffusion-3-medium</td><td>24</td><td>0.1027</td></tr><tr><td>FLUX.1</td><td>18</td><td>0.0412</td></tr><tr><td><strong>CogView4-6B</strong></td><td><strong>0</strong></td><td><strong>0.1265</strong></td></tr><tr><td>FLUX.1-Kontext</td><td>18</td><td>0.0098</td></tr></tbody></table><h3 id="不同模型参数对生成的影响">不同模型参数对生成的影响</h3><blockquote><p>https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#stable-diffusion-20</p></blockquote><ul><li>参数 <code>guidance_rescale</code>对于生成的影响</li></ul><p>引导扩散模型（如 Classifier-Free Guidance，CFG）中，用于调整文本条件对生成图像的影响强度。它的核心作用是控制模型在生成过程中对文本提示的“服从程度”。公式上，CFG 调整预测噪声的方式如下：</p> \[\epsilon = \epsilon_{\text{uncond}} + \text{guidance\_scale} \cdot (\epsilon_{\text{cond}} - \epsilon_{\text{uncond}})\] <p>其中：1、$\epsilon_{\text{cond}}$：基于文本条件预测的噪声。2、$\epsilon_{\text{uncond}}$：无条件（无文本提示）预测的噪声。3、guidance_scale：决定条件噪声相对于无条件噪声的权重。得到最后测试结果如下（参数分别为[1.0, 3.0, 5.0, 7.5, 10.0, 15.0, 20.0]，<code>prompt = "A majestic lion standing on a mountain during golden hour, ultra-realistic, 8k"</code>， <code>negative_prompt = "blurry, distorted, low quality"</code>），容易发现数值越大文本对于图像的影响也就越大。<br/><img src="https://s2.loli.net/2025/08/06/2jk18UISnqKdPZf.webp" alt="tmp-CFG.png" width="2000" height="500" loading="lazy" decoding="async"/><br/> 其中代码具体操作如下，从代码也很容易发现上面计算公式中的 uncond代表的就是我的negative_prompt，也就是说<strong>CFG做的就是negative_prompt对生成的影响</strong>：</p><pre><code class="language-python">if self.do_classifier_free_guidance:
    prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
    add_text_embeds = torch.cat([negative_pooled_prompt_embeds, add_text_embeds], dim=0)
    add_neg_time_ids = add_neg_time_ids.repeat(batch_size * num_images_per_prompt, 1)
    add_time_ids = torch.cat([add_neg_time_ids, add_time_ids], dim=0)
prompt_embeds = prompt_embeds.to(device)
</code></pre><h2 id="adapters">Adapters</h2><blockquote><p>https://huggingface.co/docs/diffusers/tutorials/using_peft_for_inference</p></blockquote><p>此类方法是在完备的 DF 权重基础上，额外添加一个“插件”，保持原有权重不变。我只需修改这个插件，就可以让模型生成不同风格的图像。可以理解为在原始模型之外新增一个“生成条件”，通过修改这一条件即可灵活控制模型生成各种风格或满足不同需求的图像。</p><h3 id="controlnet">ControlNet</h3><blockquote><p>https://github.com/lllyasviel/ControlNet<br/> 建议直接阅读：<a href="https://github.com/lllyasviel/ControlNet/discussions/categories/announcements">https://github.com/lllyasviel/ControlNet/discussions/categories/announcements</a> 来了解更加多细节</p></blockquote><p><img src="https://s2.loli.net/2025/07/09/Tfji2LMv15tgr6d.webp" alt="image" width="715" height="371" loading="lazy" decoding="async"/><br/> ControlNet<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">10</a></sup>的处理思路就很简单，再左图中模型的处理过程就是直接通过：$y=f(x;\theta)$来生成图像，但是在ControlNet里面会 <strong>将我们最开始的网络结构复制</strong> 然后通过在其前后引入一个 <strong>zero-convolution</strong> 层来“指导”（ $Z$ ）模型的输出也就是说将上面的生成过程变为：$y=f(x;\theta)+Z(f(x+Z(c;\theta_{z_1});\theta);\theta_{Z_2})$。通过冻结最初的模型的权重保持不变，保留了Stable Diffusion模型原本的能力；与此同时，使用额外数据对“可训练”副本进行微调，学习我们想要添加的条件。因此在最后我们的SD模型中就是如下一个结构：<br/><img src="https://s2.loli.net/2025/07/09/uVNAEnleRMJ6p4v.webp" alt="image" width="450" height="622" loading="lazy" decoding="async"/><br/> 在论文里面作者给出一个实际的测试效果可以很容易理解里面条件c（条件 𝑐就是提供给模型的显式结构引导信息，<strong>用于在生成过程中精确控制图像的空间结构或布局</strong>，一般来说可以是草图、分割图等）到底是一个什么东西，比如说就是直接给出一个“线稿”然后模型来输出图像。<br/><img src="https://s2.loli.net/2025/07/09/rkWH3o1MOaNs6pg.webp" alt="image" width="1017" height="581" loading="lazy" decoding="async"/></p><blockquote><p><strong>补充-1</strong>：为什么使用上面这种结构<br/> 在<a href="https://github.com/lllyasviel/ControlNet/discussions/188">github</a>上作者讨论了为什么要使用上面这种结构而非直接使用mlp等（作者给出了很多测试图像），最后总结就是：<strong>这种结构好</strong><br/><strong>补充-2</strong>：使用0卷积层会不会导致模型无法优化问题？<br/> 不会，因为对于神经网络结构大多都是：$y=wx+b$计算梯度过程中即使 $w=0$但是里面的 $x≠0$模型的参数还是可以被优化的</p></blockquote><h4 id="controlnet代码操作">ControlNet代码操作</h4><blockquote><p>Code: <a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/training_controlnet">https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/training_controlnet</a></p></blockquote><p><strong>首先</strong>，简单了解一个ControlNet数据集格式，一般来说数据主要是三部分组成：1、image（可以理解为生成的图像）；2、condiction_image（可以理解为输入ControlNet里面的条件 $c$）；3、text。比如说以<a href="https://huggingface.co/datasets/raulc0399/open_pose_controlnet">raulc0399/open_pose_controlnet</a>为例<br/><img src="https://s2.loli.net/2025/07/12/nphNm3OIebFGazr.webp" alt="image" width="1241" height="200" loading="lazy" decoding="async"/><br/><strong>模型加载</strong>，一般来说扩散模型就只需要加载如下几个：<code>DDPMScheduler</code>、<code>AutoencoderKL</code>（vae模型）、<code>UNet2DConditionModel</code>（不一定加载条件Unet模型），除此之外在ControlNet中还需要加载一个 <code>ControlNetModel</code>。对于 <code>ControlNetModel</code>中代码大致结构为，代码中通过 <code>self.controlnet_down_blocks</code>来存储ControlNet的下采样模块（<strong>初始化为0的卷积层</strong>）。<code>self.down_blocks</code>用来存储ControlNet中复制的Unet的下采样层。在 <code>forward</code>中对于输入的样本（<code>sample</code>）首先通过 <code>self.down_blocks</code>逐层处理叠加到 <code>down_block_res_samples</code>中，而后就是直接将得到结果再去通过 <code>self.controlnet_down_blocks</code>每层进行处理，最后返回下采样的每层结果以及中间层处理结果：<code>down_block_res_samples</code>，<code>mid_block_res_sample</code></p><pre><code class="language-python">class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalModelMixin):
    @register_to_config
    def __init__(...):
        ...
        self.down_blocks = nn.ModuleList([])
        self.controlnet_down_blocks = nn.ModuleList([])
        # 封装下采样过程（对应上面模型右侧结构）
        controlnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1)
        controlnet_block = zero_module(controlnet_block)
        self.controlnet_down_blocks.append(controlnet_block)
        for i, down_block_type in enumerate(down_block_types):
            # down_block_types就是Unet里面下采样的每一个模块比如说：CrossAttnDownBlock2D
            ...
            down_block = get_down_block(down_block_type) # 通过 get_down_block 获取uet下采样的模块
            self.down_blocks.append(down_block)
            for _ in range(layers_per_block):
                controlnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1)
                controlnet_block = zero_module(controlnet_block)
                self.controlnet_down_blocks.append(controlnet_block)
    @classmethod
    def from_unet(cls, unet,...):
        ...
        # 通过cls实例化的类本身ControlNetModel
        controlnet = cls(...)
        if load_weights_from_unet:
            # 将各类权重加载到 controlnet 中
            controlnet.conv_in.load_state_dict(unet.conv_in.state_dict())
            controlnet.time_proj.load_state_dict(unet.time_proj.state_dict())
            ...

        return controlnet
    def forward(...):
        ...
        # 时间编码
        t_emb = self.time_proj(timesteps)
        emb = self.time_embedding(t_emb, timestep_cond)
        if self.class_embedding is not None:
            ...
            class_emb = self.class_embedding(class_labels).to(dtype=self.dtype)
            emb = emb + class_emb
        # 对条件进行编码
        if self.config.addition_embed_type is not None:
            if self.config.addition_embed_type == "text":
                aug_emb = self.add_embedding(encoder_hidden_states)
            elif self.config.addition_embed_type == "text_time":
                time_ids = added_cond_kwargs.get("time_ids")
                time_embeds = self.add_time_proj(time_ids.flatten())
                time_embeds = time_embeds.reshape((text_embeds.shape[0], -1))

                add_embeds = torch.concat([text_embeds, time_embeds], dim=-1)
                add_embeds = add_embeds.to(emb.dtype)
                aug_emb = self.add_embedding(add_embeds)
        emb = emb + aug_emb if aug_emb is not None else emb     

        sample = self.conv_in(sample)
        controlnet_cond = self.controlnet_cond_embedding(controlnet_cond)
        sample = sample + controlnet_cond

        # 下采样处理
        down_block_res_samples = (sample,)
        for downsample_block in self.down_blocks:
            if ...
                ...
            else:
                sample, res_samples = downsample_block(hidden_states=sample, temb=emb)
            down_block_res_samples += res_samples
        # 中间层处理
        ...
        # 将输出后的内容去和0卷积进行叠加
        controlnet_down_block_res_samples = ()
        for down_block_res_sample, controlnet_block in zip(down_block_res_samples, self.controlnet_down_blocks):
            down_block_res_sample = controlnet_block(down_block_res_sample)
            controlnet_down_block_res_samples = controlnet_down_block_res_samples + (down_block_res_sample,)
        ...
        if not return_dict:
            return (down_block_res_samples, mid_block_res_sample)
        ...
</code></pre><p><strong>模型训练</strong>，训练过程和DF训练差异不大。将图像通过VAE处理、产生噪声、时间步、将噪声添加到（VAE处理之后的）图像中，而后通过 <code>controlnet</code>得到每层下采样的结果以及中间层结果：<code>down_block_res_samples, mid_block_res_sample = controlnet(...)</code>而后将这两部分结果再去通过unet处理</p><pre><code class="language-python">model_pred = unet(
    noisy_latents,
    timesteps,
    encoder_hidden_states=encoder_hidden_states,
    down_block_additional_residuals=[
        sample.to(dtype=weight_dtype) for sample in down_block_res_samples
    ],
    mid_block_additional_residual=mid_block_res_sample.to(dtype=weight_dtype),
    return_dict=False,
)[0]
</code></pre><p>后续就是计算loss等处理。<strong>模型验证</strong>，直接就是使用 <code>StableDiffusionControlNetPipeline</code>来处理了。最后随机测试的部分例子（controlnet微调效果不是很好）：<br/><img src="https://s2.loli.net/2025/07/22/SNfEiTVXpeZgOIP.webp" alt="output.jpg" width="2048" height="341" loading="lazy" decoding="async"/></p><h3 id="t2i-adapter">T2I-Adapter</h3><blockquote><p>https://github.com/TencentARC/T2I-Adapter</p></blockquote><p><img src="https://s2.loli.net/2025/07/09/gZLDtFSGr25kCwa.webp" alt="image.png" width="1060" height="508" loading="lazy" decoding="async"/><br/> T2I<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">11</a></sup>的处理思路也比较简单（T2I-Adap 4 ter Details里面其实就写的很明白了），对于输入的条件图片（比如说边缘图像）:512x512，首先通过 pixel unshuffle进行下采样将图像分辨率改为：64x64而后通过一层卷积+两层残差连接，输出得到特征 $F_c$之后将其与对应的encoder结构进行相加：$F_{enc}+ F_c$，当然T2I也支持多个条件（直接通过加权组合就行）</p><h3 id="dreambooth">DreamBooth</h3><blockquote><p>https://huggingface.co/docs/diffusers/v0.34.0/using-diffusers/dreambooth</p></blockquote><p>DreamBooth 针对的使用场景是，期望生成同一个主体的多张不同图像， 就像照相馆一样，可以为同一个人或者物体照多张不同背景、不同姿态、不同服装的照片（和ControlNet不同去添加模型结构，仅仅是在文本 Prompt）。在论文<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">12</a></sup>里面主要出发点就是：1、解决<strong>language drif</strong>（语言偏离问题）：指的是模型通过后训练（微调等处理之后）模型丧失了对某些语义特征的感知，就比如说扩散模型里面，模型通过不断微调可能就不知道“狗”是什么从而导致模型生成错误。2、高效的生成需要的对象，不会产生：生成错误、细节丢失问题，比如说下面图像中的问题：<br/><img src="https://s2.loli.net/2025/07/12/mRaHPOtC23li9Fn.webp" alt="image" width="590" height="638" loading="lazy" decoding="async"/><br/> 为了实现图像的“高效迁移”，作者直接将图像（比如说我们需要风格化的图片）作为一个特殊的标记，也就是论文里面提到的 <code>a [identifier] [class noun]</code>（其中class noun为类别比如所狗，identifier就是一个特殊的标记），在prompt中加入类别，通过利用预训练模型中关于该类别物品的先验知识，并将先验知识与特殊标记符相关信息进行融合，这样就可以在不同场景下生成不同姿势的目标物体。就比如下面的 <code>fine-tuning</code>过程通过几张图片让模型学习到 <em>特殊的狗</em>，然后再推理阶段模型可以利用这个 <em>特殊的狗</em>去生成新的动作。<strong>换言之</strong>就是（以下面实际DreamBooth代码为例）：首先通过几张 <em>狮子狗</em> 图片让模型知道 <em>狮子狗</em>张什么样子，然后再去生成 <em>狮子狗</em>的不同的动作。<br/><img src="https://s2.loli.net/2025/07/12/hYM1VdykDxALrGo.webp" alt="image" width="1181" height="502" loading="lazy" decoding="async"/><br/> 在论文里面作者设计如下的Class-specific Prior Preservation Loss（参考stackexchange）<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">13</a></sup>：</p> \[\begin{aligned} &amp; \mathbb{E}_{x,c,\epsilon,t}\left[\|\epsilon-\varepsilon_{\theta}(z_{t},t,c)\|_{2}^{2}+\lambda\|\epsilon^{\prime}-\epsilon_{pr}(z_{t^{\prime}}^{\prime},t^{\prime},c_{pr})\|_{2}^{2}\right] \end{aligned}\] <p>上面损失函数中后面一部分就是我们的先验损失，比如说$c_{pr}$就是对 “a dog”进行编码然后计算生成损失。在代码中：</p><pre><code class="language-python">if args.with_prior_preservation:
    model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0)
    target, target_prior = torch.chunk(target, 2, dim=0)
    # Compute instance loss
    loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
    # Compute prior loss
    prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean")
    # Add the prior loss to the instance loss.
    loss = loss + args.prior_loss_weight * prior_loss
else:
    loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
</code></pre><h4 id="dreambooth代码操作">DreamBooth代码操作</h4><blockquote><p>代码：<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/training_dreambooth_lora/">https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/training_dreambooth_lora/</a><br/> 权重：<a href="https://www.modelscope.cn/models/bigyellowjie/SDXL-DreamBooth-LOL/files">https://www.modelscope.cn/models/bigyellowjie/SDXL-DreamBooth-LOL/files</a></p></blockquote><p>在介绍DreamBooth代码之前，简单回顾DreamBooth原理，我希望我的模型去学习一种画风那么我就需要准备<strong>样本图片</strong>（如3-5张图片）这几张图片就是专门的模型需要学习的，但是为了防止模型过拟合（模型只学习了我的图片内容，但是对一些细节丢掉了，比如说我提供的5张油画，模型就学会了我的油画画风但是为了防止模型对更加多的油画细节忘记了，那么我就准备 <code>num_epochs * num_samples</code> 张油画<strong>类型图片</strong>然后通过计算 <code>Class-specific Prior Preservation Loss</code>）需要准备 <strong>类型图片</strong>来计算Class-specific Prior Preservation Loss。代码处理（SDXL+Lora）：<br/> DreamBooth中<strong>数据处理过程</strong>：结合上面描述我需要准备两部分数据集（如果需要计算 <code>Class-specific Prior Preservation Loss</code>）分别为：<code>instance_data_dir</code>（与之对应的 <code>instance_prompt</code>）以及 <code>class_data_dir</code>（与之对应的 <code>class_prompt</code>）而后需要做的就是将两部分数据组合起来构成：</p><pre><code class="language-python">batch = {
    "pixel_values": pixel_values,
    "prompts": prompts,
    "original_sizes": original_sizes,
    "crop_top_lefts": crop_top_lefts,
}
</code></pre><p>模型训练过程<strong>首先是lora处理模型</strong>：在基于transformer里面的模型很容易使用lora，比如说下面代码使用lora包裹模型并且对模型权重进行保存：</p><pre><code class="language-python">from peft import LoraConfig
def get_lora_config(rank, dropout, use_dora, target_modules):
    '''lora config'''
    base_config = {
        "r": rank,
        "lora_alpha": rank,
        "lora_dropout": dropout,
        "init_lora_weights": "gaussian",
        "target_modules": target_modules,
    }
    return LoraConfig(**base_config)
# 包裹lora模型权重
unet_target_modules = ["to_k", "to_q", "to_v", "to_out.0"]
unet_lora_config = get_lora_config(
    rank= config.rank,
    dropout= config.lora_dropout,
    use_dora= config.use_dora,
    target_modules= unet_target_modules,
)
unet.add_adapter(unet_lora_config)
</code></pre><p>一般的话考虑SD模型权重都比较大，而且我们使用lora微调模型没必要对所有的模型权重进行存储，那么一般都会定义一个 <code>hook</code>来告诉模型那些参数需要保存、加载，这样一来使用 <code>accelerator.save_state(save_path)</code> 就会先去使用 <code>hook</code>处理参数然后进行保存。：</p><pre><code class="language-python">def save_model_hook(models, weights, output_dir):
    if accelerator.is_main_process:
        unet_lora_layers_to_save = None
      
        for model in models:
            if isinstance(model, type(unwrap_model(unet))):
                unet_lora_layers_to_save = convert_state_dict_to_diffusers(get_peft_model_state_dict(model))
            ...
            weights.pop() # 去掉不需要保存的参数

        StableDiffusionXLPipeline.save_lora_weights(
            output_dir,
            unet_lora_layers= unet_lora_layers_to_save,
            ...
        )
def load_model_hook(models, input_dir):
    unet_ = None

    while len(models) &gt; 0:
        model = models.pop()

        if isinstance(model, type(unwrap_model(unet))):
            unet_ = model

    lora_state_dict, network_alphas = StableDiffusionLoraLoaderMixin.lora_state_dict(input_dir)

    unet_state_dict = {f"{k.replace('unet.', '')}": v for k, v in lora_state_dict.items() if k.startswith("unet.")}
    unet_state_dict = convert_unet_state_dict_to_peft(unet_state_dict)
    incompatible_keys = set_peft_model_state_dict(unet_, unet_state_dict, adapter_name="default")
    ...
accelerator.register_save_state_pre_hook(save_model_hook)
accelerator.register_load_state_pre_hook(load_model_hook)
</code></pre><p><strong>其次模型训练</strong>：就是常规的模型训练（直接在样本图片：<code>instance_data_dir</code>以及样本的prompt：<code>instance_prompt</code>上进行微调）然后计算loss即可，如果涉及到 <code>Class-specific Prior Preservation Loss</code>（除了上面两个组合还需要：<code>class_data_dir</code>以及 <code>class_prompt</code>）那么处理过程为（以SDXL为例），不过需要事先了解的是在计算这个loss之前会将两个数据集以及prompt都<strong>组合到一起成为一个数据集</strong>（<code>instance-image-prompt</code> 以及 <code>class-image-prompt</code>之间是匹配的）：</p><pre><code class="language-python"># 样本内容编码
instance_prompt_hidden_states, instance_pooled_prompt_embeds = compute_text_embeddings(config.instance_prompt, text_encoders, tokenizers)
# 类型图片内容编码
if config.with_prior_preservation:
    class_prompt_hidden_states, class_pooled_prompt_embeds = compute_text_embeddings(config.class_prompt, text_encoders, tokenizers)
...
prompt_embeds = instance_prompt_hidden_states
unet_add_text_embeds = instance_pooled_prompt_embeds
if not config.with_prior_preservation:
    prompt_embeds = torch.cat([prompt_embeds, class_prompt_hidden_states], dim=0)
    unet_add_text_embeds = torch.cat([unet_add_text_embeds, class_pooled_prompt_embeds], dim=0)
...
model_pred = unet(...)
if config.with_prior_preservation:
    model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0)
    target, target_prior = torch.chunk(target, 2, dim=0)
    ...
    prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean")
...
loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
...
loss = loss + config.prior_loss_weight * prior_loss
accelerator.backward(loss)
</code></pre><p>在这个里面之所以用 <code>chunk</code>是因为如果计算 <code>Class-specific Prior Preservation Loss</code>里面的文本prompt是由两部分拼接构成的 <code>torch.cat([prompt_embeds, class_prompt_hidden_states], dim=0)</code>那么可以直接通过chunk来分离出两部分（这个过程和使用参数 <code>guidance_rescale</code>很相似）<br/> 最后测试的结果为（<code>prompt: "A photo of Rengar the Pridestalker in a bucket"</code>，模型<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFModelCode/training_dreambooth_lora/">代码</a>以及<a href="https://www.modelscope.cn/models/bigyellowjie/SDXL-DreamBooth-LOL/files">权重下载</a>）：</p><p><img src="https://s2.loli.net/2025/07/15/7xIPMW6SJ1degZj.webp" alt="image.png" width="2000" height="889" loading="lazy" decoding="async"/></p><h2 id="总结">总结</h2><p>对于不同的扩散（基座）模型（SD1.5、SDXL、Imagen）等大部分都是采用Unet结构，当然也有采用Dit的，这两个模型（SD1.5、SDXL）之间的差异主要在于后者会多一个clip编码器再文本语义上比前者更加有优势。对于adapter而言，可以直接理解为再SD的基础上去使用“风格插件”，这个插件不去对SD模型进行训练（从而实现对参数的减小），对于ControNet就是直接对Unet的下采样所有的模块（前后）都加一个zero-conv而后将结果再去嵌入到下采用中，而T2I-Adapter则是去对条件进行编码而后嵌入到SD模型（上采用模块）中。对于deramboth就是直接通过给定的样本图片去生“微调”模型，而后通过设计的Class-specific Prior Preservation Loss来确保所生成的样本特里不会发生过拟合。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2307.01952">https://arxiv.org/pdf/2307.01952</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:12" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2403.03206">https://arxiv.org/pdf/2403.03206</a> <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:16" role="doc-endnote"><p><a href="https://zhouyifan.net/2024/07/14/20240703-SD3/">https://zhouyifan.net/2024/07/14/20240703-SD3/</a> <a href="#fnref:16" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:14" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/684068402">https://zhuanlan.zhihu.com/p/684068402</a> <a href="#fnref:14" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2205.11487">https://arxiv.org/pdf/2205.11487</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:11" role="doc-endnote"><p><a href="https://openaccess.thecvf.com/content/ICCV2023/papers/Peebles_Scalable_Diffusion_Models_with_Transformers_ICCV_2023_paper.pdf">Scalable Diffusion Models with Transformers</a> <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:8" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2405.08748">https://arxiv.org/pdf/2405.08748</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:19" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2506.15742">https://arxiv.org/pdf/2506.15742</a> <a href="#fnref:19" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:18" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2508.02324">https://arxiv.org/pdf/2508.02324</a> <a href="#fnref:18" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2302.05543">https://arxiv.org/pdf/2302.05543</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2302.08453">https://arxiv.org/pdf/2302.08453</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2208.12242">https://arxiv.org/pdf/2208.12242</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p>https://stats.stackexchange.com/questions/601782/how-to-rewrite-dreambooth-loss-in-terms-of-epsilon-prediction <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Sun, 06 Jul 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/07/06/DFBaseModel.html</guid></item><item><title>CV中常用Backbone-4：目标检查算法原理以及代码操作</title><link>https://www.big-yellow-j.top/posts/2025/06/28/Objectdetection.html</link><description><![CDATA[本文介绍常用目标检测算法，包括R-CNN、Fast RCNN、Faster RCNN及Yolo等。R-CNN通过区域候选框生成器（如Selective Search）产生候选框，经CNN特征提取和非极大值抑制剔除重叠框；Fast RCNN采用ROI pooling实现单图一次CNN推理提升效率；Faster RCNN引入RPN网络，其分类分支判断目标/背景、回归分支预测偏移量，并通过RoI Align解决RoI pooling的数据舍入问题；Yolo直接切割图像避免卷积扫描，优化检测流程。]]></description><content:encoded><![CDATA[<p>前面已经介绍了简单的视觉编码器，这里主要介绍常用的目标检测算法<br/> 1、<a href="https://www.big-yellow-j.top/posts/2025/04/30/ConvNeXt.html">CV中常用Backbone-2：ConvNeXt模型详解</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html">CV中常用Backbone(Resnet/Unet/Vit系列/多模态系列等)以及代码</a><br/> 3、<a href="https://www.big-yellow-j.top/posts/2025/05/18/Clip-sam.html">CV中常用Backbone-3：Clip/SAM原理以及代码操作</a></p><h2 id="目标检测算法">目标检测算法</h2><p>基于卷积的目标检测算法如：R-CNN、FastRCNN、Yolo（部分版本）等，基于Attention如Vit等</p><h3 id="r-cnn目标检测">R-CNN目标检测</h3><p><img src="https://s2.loli.net/2025/09/21/ZEuiMa6QkSFDchj.webp" alt="image" width="1027" height="350" loading="lazy" decoding="async"/><br/> 算法原理<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>如上面所述，主要过程为：1、区域候选框生成器（Region Proposal Extractor）；2、CNN特征提取器；3、SVM分类器根据特征进行分类；4、回归模型用于收紧边界框。其中主要需要着重介绍的 <strong>区域候选款生成器</strong>用像 Selective Search 这样的传统方法从输入图像产生大约 1–2k 个候选框（每个候选框可能包含一个物体或背景）。Selective Search 利用图像分割和层次合并产生高召回率的候选区域。<br/><img src="https://s2.loli.net/2025/09/21/QrumebgvzyDjBkp.webp" alt="image" width="1193" height="786" loading="lazy" decoding="async"/><br/> 整体过程如上面描述一下，通过预训练的神经网络（AlexNet）等去获取CNN特征，然后再去通过<strong>选择性搜索算法</strong>（Selective Search）获取所有的可能的“目标”</p><blockquote><p>对于选择性搜索算法的一个大致思路：大概的意思就是首先根据图像分割的算法来初始化划分区域，然后根据不同颜色模式、目标颜色、纹理、大小、形状等特征来计算相似度合并子区域。</p></blockquote><p>在经过分类以及SVM分类之后直接再去收缩边界框，这个过程主要是通过<strong>非极大值抑制</strong>去剔除重叠的建议框。</p><blockquote><p><strong>非极大值抑制</strong>：对于每一个框都会有一个置信度，首选按照置信度进行排列得到最大的置信度框，然后去计算其他框和这个最大框的IoU，如果某个框与选取框的IoU大于我们设定的阈值（比如0.5），说明它们和选取框检测的是同一个目标，所以需要被抑制（删除）。如果某个框与选取框的IoU小于阈值，说明它们和框A检测的可能是另一个目标（或者只是重叠不多），予以保留。<br/> $\text{IoU}=\frac{\text{Area}(A\cap B)}{\text{Area}(A\cup B)}$ 具体的代码实现如下</p></blockquote><pre><code class="language-python">def bbox_iou(box1, box2):
    inter_x1 = max(box1[0], box2[0])
    inter_y1 = max(box1[1], box2[1])
    inter_x2 = min(box1[2], box2[2])
    inter_y2 = min(box1[3], box2[3])

    # 交集的宽高（注意要 clamp 为 &gt;=0）
    inter_w = max(0, inter_x2 - inter_x1)
    inter_h = max(0, inter_y2 - inter_y1)
    inter_area = inter_w * inter_h

    # 各自面积
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])

    # 并集面积
    union_area = area1 + area2 - inter_area

    # IoU
    iou = inter_area / union_area if union_area &gt; 0 else 0.0
    return iou
</code></pre><h3 id="fast-rcnn以及faster-rcnn目标检测">Fast RCNN以及Faster RCNN目标检测</h3><p>Fast RCNN<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>主要是依次解决上面模型存在的问题，其主要的原理如下：<br/><img src="https://s2.loli.net/2025/09/21/YecT6b9jipBIqH4.webp" alt="image" width="956" height="378" loading="lazy" decoding="async"/><br/> 在Fast RCNN中主要过程：输入是一张图片和多个proposal，经过卷积层计算之后，通过ROI pooling的方式归一到一个fixed size的feature map，最后通过FCs计算分类损失（softmax probabilities）和框回归损失（b-box regression offsets）。这种方法的好处是一张图片只需要经过一次CNN的推理，不再像RCNN那样根据Proposals把原图切成多个子图输入到CNN中再去由SVM处理，大大提升了效率。<br/> 而在Faster CNN<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>主要是为了解决前者依赖于外部候选区域方法，而在该论文中主要提出改进措施是通过使用设计一个<strong>RPN</strong>去替换外部候选区域，整体流程如下：<br/><img src="https://s2.loli.net/2025/09/21/fpH9TkoAKGFEqUN.webp" alt="image" width="789" height="391" loading="lazy" decoding="async"/><br/> 对于上面两个模型主要是需要关注两点：<br/><strong>1、RPN网络结构设计</strong>：在原图尺度上，设置了密密麻麻的候选Anchor。然后用cnn去判断哪些Anchor是里面有目标的positive anchor，哪些是没目标的negative anchor。算法思路为：在得到特征图之后再特征图上每个像素点都预设9个anchor（基本就覆盖需要识别的目标），但是对于这些anchor肯定有很多多余的，因此再RPN中就有两个分支：<strong>1、分类分支</strong>（上面流程图中<em>上面部分</em>）：直接通过<code>(1, 1, 2k)</code>（k=9）卷积去计算得到Objectness Score（标记框是目标还是背景）；<strong>2、回归分支</strong>（上面流程图中<em>下面部分</em>）：微调锚点框的位置和大小，使其更贴合真实目标。它不是直接预测坐标，而是预测偏移量（offsets）<br/><strong>2、RoI Pooling以及RoI Align</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>：两个RoI算法主要是将bbox映射到特征图上获取bbox中特征，后者是为了解决前者纯在的数据舍入问题。</p><h3 id="yolo算法">YoLo算法</h3><p>在Yolov1中其网络结构如下所示：<br/><img src="https://s2.loli.net/2025/09/21/aetD2mQShxCbnAZ.webp" alt="image" width="1272" height="720" loading="lazy" decoding="async"/><br/> 主要解决的是上面提到哪些检测算法需要用卷积核去“扫描”图像问题，在Yolo中直接将图像提前切割为 $S\times S$个格子而后就是按照上面的网络结构进行处理最后输出的张量为 $7\times 7\times 30$，可以理解为每一块都有30个特征值对于这30个特征值分别表示的含义是：$B\times 5+ C$ 其中B代表候选框数量（论文中选择2，具体的bbox坐标是直接通过模型训练得到），C代表类别，之所以用5表示的是 $(x,y,w,h,conf)$ 前面4个不解释后面一个指标代表的的是该框的 <strong>置信度水平</strong>，这样一来可以直接通过<strong>计算每个框属于哪个类别</strong>也就是计算执行都和类别C的乘积结果： $C\times \text{conf}$得到结果为 $20\times 1$这样一来每一个框都会这样计算那么最后得到 $7\times 7\times 2=98$，借鉴<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>中的PPT，那么我最后得到结果如下所述：<br/><img src="https://s2.loli.net/2025/09/21/QmGCguZBn8SwtpA.webp" alt="image" width="1104" height="343" loading="lazy" decoding="async"/><br/> 假设我的第一行是判断“狗”这个类别，那么第一行行都会有关于狗这个类别一个置信度，那么后续就可以直接去计算NMS来得到最后的bbox了。对于Yolo使用可以直接使用<code>ultralytics</code>来使用Yolo权重（<a href="https://github.com/shangxiaaabb/ProjectCode/blob/43bb45d5c02d03cff08b557447cb348e75c346a5/code/Python/DFDataBuild/instance_background.py#L92C1-L92C34">Yolo11-SAM代码</a>）</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/1311.2524">https://arxiv.org/pdf/1311.2524</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/1504.08083">https://arxiv.org/pdf/1504.08083</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/1506.01497">https://arxiv.org/pdf/1506.01497</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://cloud.tencent.com/developer/article/1829792">https://cloud.tencent.com/developer/article/1829792</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://docs.google.com/presentation/d/1aeRvtKG21KHdD5lg6Hgyhx5rPq_ZOsGjG5rJ1HP7BbA/pub?start=false&amp;loop=false&amp;delayms=3000&amp;slide=id.p">Yolo-PPT-GoogleDrive</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>Backbone</category><pubDate>Sat, 28 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/28/Objectdetection.html</guid></item><item><title>图像消除论文-2：SmartEraser、Erase Diffusion、OmniEraser</title><link>https://www.big-yellow-j.top/posts/2025/06/26/ImageEraser2.html</link><description><![CDATA[图像消除是图像生成模型的重要应用领域，本文围绕Erase Diffusion、SmartEraser及OmniEraser等模型，阐述其核心技术与数据集构建方法。Erase Diffusion通过动态图像组合（输入与目标图像随解噪过程动态调整）、改进预测过程（计算“图像链”间损失）及注意力机制（融入mask）优化图像消除效果；SmartEraser构建合成数据集，包括实体过滤（基于语义分割如SAM提取实体，CLIP评分筛选合适大小实体）、背景筛选（从COCONut和SAM-1B数据集选取）、图像组合（保持实体大小一致，计算IoU与位置约束，经alpha blending合成），并改进模型...]]></description><content:encoded><![CDATA[<p>图像生成模型应用系列——图像消除：<br/><a href="https://www.big-yellow-j.top/posts/2025/06/11/ImageEraser1.html">图像消除论文-1：PixelHacker、PowerPanint等</a><br/><a href="https://www.big-yellow-j.top/posts/2025/06/26/ImageEraser2.html">图像消除论文-2：消除类型数据集构建(1)</a></p><h2 id="erase-diffusion">Erase Diffusion</h2><blockquote><p><a href="https://openaccess.thecvf.com/content/CVPR2025/papers/Liu_Erase_Diffusion_Empowering_Object_Removal_Through_Calibrating_Diffusion_Pathways_CVPR_2025_paper.pdf">Erase Diffusion: Empowering Object Removal Through Calibrating Diffusion Pathways</a><br/> https://github.com/longtaojiang/SmartEraser<br/> CVPR-2025</p></blockquote><h3 id="1模型结构">1、模型结构</h3><p><img src="https://s2.loli.net/2025/06/28/dcKx2kr71oGFwV9.webp" alt="image" width="1280" height="648" loading="lazy" decoding="async"/></p><p>论文出发点主要为：1、<strong>动态图像组合</strong>：区别常规的图像去除实验<strong>target image</strong>就是我们的去除内容之后的图片，在该文中将其替换为：$x_t^{mix} = (1-\lambda_t)x_0^{ori}+ \lambda_t x_0^{obj}$ 也就是随着解噪过程（t逐渐减小）图片中所添加的实体（$x^{obj}_0$）所占的权重越来越小，同时将 <strong>input image</strong>也替换为动态的过程：$x_t^{min}=\sqrt{\alpha_t}x_t^{min}+ \sqrt{1- \alpha_t}\epsilon$；2、<strong>改变模型的预测过程</strong>：上面两部分公式处理之后那么得到的输入图像是一个“图像链”输出图像也是一个“图像链”，那么模型需要做的就是将对应“图像链”之间的loss进行计算。<br/><img src="https://s2.loli.net/2025/06/28/XHodtjyncSCDLV6.webp" alt="image" width="799" height="300" loading="lazy" decoding="async"/><br/> 3、<strong>改进注意力计算方式</strong>：这部分比较容易理解在计算注意力过程中将mask加入到计算也就是：$QK^T\bigodot Mask$<br/><img src="https://s2.loli.net/2025/06/28/EXbq2QGRWlImUjK.webp" alt="image" width="779" height="410" loading="lazy" decoding="async"/></p><h2 id="smarteraser">SmartEraser</h2><blockquote><p><a href="https://openaccess.thecvf.com/content/CVPR2025/papers/Jiang_SmartEraser_Remove_Anything_from_Images_using_Masked-Region_Guidance_CVPR_2025_paper.pdf">SmartEraser: Remove Anything from Images using Masked-Region Guidance</a><br/> CVPR-2025</p></blockquote><h3 id="1数据集构建">1、数据集构建</h3><p><img src="https://s2.loli.net/2025/06/28/7ojzDsGYEHKc3XC.webp" alt="image" width="1127" height="370" loading="lazy" decoding="async"/></p><p>合成数据集构建思路上使用思路是：实体过滤背景检测而后将两部分进行组合。<strong>Step-1：实体过滤</strong>：直接通过语义分割模型（如SAM等）分割出实体之后，通过CLIP计算实体的score并且过滤掉过大/小的分割实体（保留5%-95%）进而获得需要粘贴的实体；<strong>Step-2：过滤背景图片</strong>：直接通过计算分辨率等从COCONut 和SAM-1B数据集中挑选出合适图片背景；<strong>Step-3：图片组合</strong>：首先将实体和背景图像中相同实体大小保持一致，而后通过计算我分割实体$c_1$ 以及背景中的实体 $c_i$之间的IoU：$R_1$，以及保证需要粘贴实体在整个背景中的位置（保证不超出背景图片）：$R_2$而后取两部分交集得到图像应该插入的合理位置。最后通过 <code>alpha blending</code>将两部分图像（实体+背景）进行组合。</p><h3 id="2模型结构测试效果">2、模型结构测试效果</h3><p><img src="https://s2.loli.net/2025/06/28/1Wv6XI9bD87UTBs.webp" alt="image" width="1221" height="735" loading="lazy" decoding="async"/></p><p>论文主要就是将模型的输入进行改进：将模型图像输入由$[mask, image\bigodot (1-mask)]$ 改为 $[mask, image]$，除此之外将DF模型的condition改进（将图像编码嵌入到文本编码中）：$[\text{CLIP-TextEncoder(text)}, \text{MLP}(\text{Image}\bigodot \text{Mask})]$。除此之外就是将mask由“规则”（实体分割是规则的）变为“不规则”（将实体分割mask进行额外处理如膨胀处理等）最后测试效果是：</p><p><img src="https://s2.loli.net/2025/06/28/G8HOtWoB1bhYEqP.webp" alt="image" width="1621" height="641" loading="lazy" decoding="async"/></p><blockquote><p>ME：将mask变不规则；RG：改变模型输入；VG：将图像编码嵌入到clip文本编码中</p></blockquote><table><thead><tr><th>微调测试效果</th></tr></thead><tbody><tr><td><img src="https://s2.loli.net/2025/07/01/zkB2nCjVIdSwm6W.webp" alt="55_000000138891.jpg" width="1536" height="512" loading="lazy" decoding="async"/></td></tr><tr><td><img src="https://s2.loli.net/2025/07/01/KrQehLwg1yuaEYB.webp" alt="sa_324589.jpg" width="1536" height="512" loading="lazy" decoding="async"/></td></tr><tr><td><img src="https://s2.loli.net/2025/07/01/fhtiqNJug9Lz4WG.webp" alt="sa_326708.jpg" width="1536" height="512" loading="lazy" decoding="async"/></td></tr><tr><td><img src="https://s2.loli.net/2025/07/01/V7eBwIMGoK9RAzZ.webp" alt="sa_324873.jpg" width="1536" height="512" loading="lazy" decoding="async"/></td></tr><tr><td><img src="https://s2.loli.net/2025/07/01/PWQJ5gi39YthMBf.webp" alt="sa_5278781.jpg" width="1536" height="512" loading="lazy" decoding="async"/></td></tr></tbody></table><p><strong>值得注意的是</strong>，在其合成的数据里面，合成得到结果<strong>很粗糙</strong>（感觉就像是随机贴图），因此感觉数据可用性不高<br/><img src="https://s2.loli.net/2025/07/01/QV4FMjNP2BgfhwS.webp" alt="image.png" width="516" height="516" loading="lazy" decoding="async"/><br/><img src="https://s2.loli.net/2025/07/01/816hmFUBvpQKuJX.webp" alt="image.png" width="619" height="612" loading="lazy" decoding="async"/></p><h2 id="omnieraser">OmniEraser</h2><blockquote><p>https://pris-cv.github.io/Omnieraser/</p></blockquote><h3 id="1数据集构建-1">1、数据集构建</h3><p>通过视频来获取（mask-image）数据集，具体操作流程如下：</p><p><img src="https://s2.loli.net/2025/06/26/LYclhNt4WmgRJpz.webp" alt="image.png" width="1618" height="627" loading="lazy" decoding="async"/></p><p>首先获取一段视频 $\mathbf{V}$ 通过 <strong>混合高斯算法</strong>（MOG）去检查视频中移动的物体以及静止的物体这样一来就可以得到两部分内容：Background和Foreground而后通过计算两部分之间的MSE（$MSE(V_i^{fg}, V_j^{bg})$）就可以得到source-image和 target-image对。对于mask内容直接通过 <em>GroundDINO+SAM2</em> 算法来构建mask这样一来就可以得到：foreground-image，mask，background-image。模型算法这是直接去微调 <code>FLUX.1-dev</code></p><h3 id="2模型结构测试效果-1">2、模型结构测试效果</h3><p><img src="https://s2.loli.net/2025/06/26/tcIhCEDeuGf3UXv.webp" alt="image.png" width="785" height="450" loading="lazy" decoding="async"/></p><p>实际测试效果（使用prompt为：<code>'There is nothing here.'</code>）</p><table><thead><tr><th>原图</th><th>Mask</th><th>结果</th><th>测试细节</th></tr></thead><tbody><tr><td><img src="https://s2.loli.net/2025/06/26/znSUtwamOk9r47I.webp" alt="sa_324952.jpg" width="2258" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/QXdWSb46FREakVN.webp" alt="sa_324952-0.jpg" width="2258" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/7pdgqO45CbDhluw.webp" alt="sa_324952.jpg" width="1024" height="1024" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/Bw4D9pEi7McULbv.webp" alt="sa_325886.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/P8mKbFdTqxZ19Yn.webp" alt="sa_325886-1.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/89qmPaIY3tW1uUv.webp" alt="sa_325886.jpg" width="1024" height="1024" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/kxZjsRLSvpX96ne.webp" alt="sa_324501.jpg" width="1500" height="2250" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/bHMSowgfXm4sqO5.webp" alt="sa_324501-2.jpg" width="1500" height="2250" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/GV9n6u1As3ZoqkJ.webp" alt="sa_324501.jpg" width="1024" height="1024" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/SA8rRFMc4Zjlp21.webp" alt="sa_324930.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/fQdXwRUCg5JVjs6.webp" alt="sa_324930-1.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/Npr6tT9A75gwcY4.webp" alt="sa_324930.jpg" width="1024" height="1024" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/xfBuX4RniAj7Z2D.webp" alt="image.png" width="2094" height="643" loading="lazy" decoding="async"/></td></tr></tbody></table> ]]></content:encoded><category>图像消除</category><pubDate>Thu, 26 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/26/ImageEraser2.html</guid></item><item><title>深入浅出了解生成模型-5：diffuser/accelerate库学习及其数据合成</title><link>https://www.big-yellow-j.top/posts/2025/06/25/accelerate-diffusers.html</link><description><![CDATA[本文介绍生成模型开发常用的Python库Diffusers与Accelerate，助力开发者高效实践。Accelerate提供快速分布式训练方案，无需手动编写torch代码，支持梯度累计、混合精度训练等加速方法，可结合tensorboard/wandb记录训练过程，适配warm-up学习率调整策略及模型不同模块的差异化学习率设置，同时提供模型权重保存与读取功能，使用时需注意通过accelerator.end_training()结束追踪器，以及tqdm进度条仅主进程显示（disable=not accelerator.is_local_main_process）。Diffusers聚焦扩...]]></description><content:encoded><![CDATA[<p>工欲善其事，必先利其器。即便介绍了再多生成模型，没有趁手的工具也难以施展才华。因此，本文将重点介绍几个在生成模型开发中常用的 Python 库，着重讲解 <strong>Diffusers</strong> 和 <strong>Accelerate</strong> 的基本使用。感谢 Hugging Face 为无数算法工程师提供了强大的开源支持！需要注意的是，官方文档对这两个库已有详尽的说明，本文仅作为一篇简明的使用笔记，抛砖引玉，供参考和交流。</p><h2 id="accelerate">accelerate</h2><blockquote><p>推荐直接阅读官方文档：<a href="https://huggingface.co/docs/accelerate/index">https://huggingface.co/docs/accelerate/index</a><br/><a href="https://huggingface.co/docs/accelerate/basic_tutorials/install"><code>pip install accelerate</code></a></p></blockquote><p>介绍之前了解一下这个库是干什么的：这个库主要提供一个快速的分布式训练（避免了直接用torch进行手搓）并且支持各类加速方法：<a href="https://www.big-yellow-j.top/posts/2025/01/01/mixed-precision.html">混合精度训练</a>、<a href="https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html">Deepspeed</a>、梯度累计等</p><h3 id="一个基本使用场景">一个基本使用场景</h3><p>一般任务中一个常见的应用场景是：需要实现一个多显卡（这里假设为双显卡）分布式训练，并且使用梯度累计、混合精度训练，并且训练得到的结果通过tensorboard/wandb进行记录，除此之外还需要使用warm-up学习率调整策略，并且我的模型不同模块使用的学习率不同，训练完成之后所有的模型权重要进行保存/读取权重进行测试。那么可以直接通过下面代码进行实现（部分库的导入以及一些参数比如说config直接忽略）</p><pre><code class="language-python">from accelerate import Accelerator
kwargs_handlers=[DistributedDataParallelKwargs(find_unused_parameters=True)] # 不是必须的
# Step-1 首先初始化 accelerate
accelerator = Accelerator(mixed_precision= 'fp16', 
                            gradient_accumulation_steps= 2,
                            log_with= ['tensorboard', 'wandb'], # 一般来说用一个即可
                            project_dir=os.path.join(config.output_dir, "logs"),
                            kwargs_handlers= kwargs_handlers
                            )
# 仅在主线程上创建文件夹
if accelerator.is_main_process: 
    os.makedirs(config.output_dir, exist_ok=True)
    # 初始化一个实验记录器（此处内容需要注意⭐）
    # accelerator.init_trackers(f"Train-{config.training}")
    log_name = 'Model-Test'
    accelerator.init_trackers(
        project_name= f"Page-Layout-Analysis-{config.pred_heads}",
        init_kwargs={
            "wandb": {
                "name": log_name,
                "dir": os.path.join(config.output_dir, "logs"),
                "config": vars(config)
            }
        }
        )
 
# Step-2 初始化完成之后可以直接将我们需要的内容通过 accelerator.prepare 进行处理
optimizer = torch.optim.AdamW([
        {'params': model.image_model.parameters(), 'lr': 2e-5, 'weight_decay': 1e-4},
        {'params': model.text_model.parameters(), 'lr': 4e-5},
        {'params': [p for n, p in model.named_parameters() 
                    if 'image_model' not in n and 'text_model' not in n], 
        'lr': config.learning_rate, 'weight_decay': 1e-4}, 
    ])
total_steps = config.epochs * len(train_dataloader)
warmup_steps = int(0.15 * total_steps)

# Warmup 调度器：从 0.1*lr 线性增加到 lr
warmup_scheduler = torch.optim.lr_scheduler.LinearLR(optimizer, 
                                                        start_factor=0.1, 
                                                        total_iters=warmup_steps
)

# 余弦退火调度器：添加 eta_min 防止学习率过低
cosine_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, 
                                                                T_max=total_steps - warmup_steps, 
                                                                eta_min=1e-6
)

cosine_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, 
                                                                T_max=total_steps - warmup_steps)
lr_scheduler = torch.optim.lr_scheduler.SequentialLR(
    optimizer,
    schedulers=[warmup_scheduler, cosine_scheduler],
    milestones=[warmup_steps] 
)
dataloader, model, optimizer, scheduler = accelerator.prepare(dataloader, model, optimizer, scheduler)

# Step-3 模型训练以及模型优化
total_data = len(dataloader)
for i, batch in enumerate(dataloader):
    with accelerator.accumulate(model): # 梯度累计
        inputs, targets = batch

        # 下面两句可以不用，但是习惯还是直接使用
        inputs = inputs.to(accelerator.device)
        targets = targets.to(accelerator.device)

        outputs = model(inputs)
        loss = loss_function(outputs, targets)
        accelerator.backward(loss)
        if accelerator.sync_gradients: # 进行梯度裁剪
            accelerator.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()

        # 记录一下实验结果
        logs = {
                "Train/loss": loss.item(),
                "Train/lr": optimizer.param_groups[0]['lr'], # 这里是假设模型使用的优化学习率不同 或者直接使用 scheduler.get_last_lr()[0]
                "Train/ACC": acc,
            }
            progress_bar.set_postfix(
                loss=loss.item(),
                acc=acc, f1=f1)
            accelerator.log(logs, step= epoch* total_data+ i)

# Step-3 同步不同进程
accelerator.wait_for_everyone()
if accelerator.is_main_process:
    model = accelerator.unwrap_model(model)
    model.save_pretrained(os.path.join(args.output_dir, "model"))
accelerator.end_training()
</code></pre><p>不过对于上面的代码需要注意如下几个内容<br/> 1、追踪器使用：一般多显卡使用过程中通过使用 <code>accelerator.end_training()</code> 去结束 <code>tracker</code><br/> 2、tqdm使用：一般只需要主进程进行显示进度条，因此一般直接：<code>tqdm(..., disable=not accelerator.is_local_main_process)</code></p><h2 id="diffuser">diffuser</h2><blockquote><p>推荐直接阅读官方文档：<a href="https://huggingface.co/docs/diffusers/main/en/index">https://huggingface.co/docs/diffusers/main/en/index</a><br/><a href="https://huggingface.co/docs/diffusers/main/en/installation?install=Python"><code>pip install git+https://github.com/huggingface/diffusers</code></a></p></blockquote><h3 id="基本使用">基本使用</h3><p>对于<a href="https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html">Diffusion Model原理</a>理解可以参考，以及直接通过下面<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/DFModelTraining/df_training.py">训练一个Diffusion Model代码</a>（代码不一定很规范）进行解释。</p><pre><code class="language-python">from diffusers import DDPMScheduler

noise_scheduler = DDPMScheduler(num_train_timesteps= config.num_train_timesteps,
                            beta_start= config.beta_start, # 两个beta代表加噪权重
                            beta_end= config.beta_end,
                            beta_schedule= 'scaled_linear')
...
# training
for epoch in range(config.epochs):
    for i, batch in enumerate(train_dataloader):
        image = batch["images"]
        ...
        timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, 
                                    (image.shape[0],), 
                                    device=image.device, 
                                    dtype=torch.int64)
            
        noise = torch.randn(image.shape, device= accelerator.device)
        noise_image = noise_scheduler.add_noise(image, noise, timesteps)
        ...
        noise_pred = model(noise_image, timesteps)
        loss = F.mse_loss(noise_pred, noise)
        ...
# eva
def evaluate(..., noise_scheduler, ):
    ...
    noise = torch.randn((config.eval_batch_size, config.channel, config.image_size, config.image_size)) # 可以选择固定随机数种子
    for t in noise_scheduler.timesteps:
        t_tensor = torch.full((noise.shape[0],), 
                                t, 
                                dtype=torch.long, 
                                device= device)
        predicted_noise = model(noise, t_tensor, text_label)
        noise = noise_scheduler.step(predicted_noise, t, noise).prev_sample
    images = (noise.clamp(-1, 1) + 1) / 2
    ...
</code></pre><p>训练过程<br/><strong>1、加噪处理</strong>：通过选择使用DDPM/DDIM而后将生成的”确定的噪声”添加到图片上 <code>noise_scheduler.add_noise(image, noise, timesteps)</code><br/><img src="https://s2.loli.net/2025/06/27/yLPrx7tkdOh3AiD.webp" alt="image.png" width="880" height="511" loading="lazy" decoding="async"/></p><p><strong>2、模型预测</strong>：通过模型去预测所添加的噪声并且计算loss<br/> 生成过程<br/><strong>3、逐步解噪</strong>：训练好的模型逐步预测噪声之后将其从噪声图片中将噪声剥离出来</p><h3 id="1scheduler">1、Scheduler</h3><blockquote><p>https://huggingface.co/docs/diffusers/api/schedulers/overview<br/> 更加详细的描述：<a href="https://www.big-yellow-j.top/posts/2025/07/06/DFscheduler.html">https://www.big-yellow-j.top/posts/2025/07/06/DFscheduler.html</a></p></blockquote><p>以<a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L129">DDPMScheduler</a>为例主要使用两个功能：<br/><strong>1、add_noise</strong>（<a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L501">输入</a>：<code>sample、noise、timesteps</code>）：这个比较简单就是直接：$x=\sqrt{\alpha}x+ \sqrt{1-\alpha}\epsilon$<br/><strong>2、step</strong>（<a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L398">输入</a>：<code>model_output、timestep、sample</code>）：step做的就是将上面的add_noise进行逆操作。具体代码处理</p><ul><li><a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L437"><strong>Step-1</strong></a> 首先计算几个参数：$\alpha、\beta$</li></ul><pre><code class="language-python">alpha_prod_t = self.alphas_cumprod[t]
alpha_prod_t_prev = self.alphas_cumprod[prev_t] if prev_t &gt;= 0 else self.one
beta_prod_t = 1 - alpha_prod_t
beta_prod_t_prev = 1 - alpha_prod_t_prev
current_alpha_t = alpha_prod_t / alpha_prod_t_prev
current_beta_t = 1 - current_alpha_t
</code></pre><ul><li><a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L445"><strong>Step-2</strong></a> 根据计算得到参数反推$t-1$的计算结果（提供3种类，介绍“epsilon”）$x_0=\frac{x_T- \sqrt{1- \alpha_t}\epsilon}{\alpha_t}$</li></ul><p><code>pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5)</code></p><ul><li><a href="https://github.com/huggingface/diffusers/blob/d7dd924ece56cddf261cd8b9dd901cbfa594c62c/src/diffusers/schedulers/scheduling_ddpm.py#L469C9-L474C109"><strong>Step-3</strong></a>：从数学公式上在上一步就可以计算得到，但是在<a href="https://arxiv.org/pdf/2006.11239">论文</a>中为了更加近似预测结果还会计算：</li></ul> \[\frac{\sqrt{\bar{\alpha}_{t-1}}\beta_{t}}{1-\bar{\alpha}_{t}}\mathbf{x}_{0}+\frac{\sqrt{\alpha_{t}}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_{t}}\mathbf{x}_{t}\] <pre><code class="language-python">pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * current_beta_t) / beta_prod_t
current_sample_coeff = current_alpha_t ** (0.5) * beta_prod_t_prev / beta_prod_t
pred_prev_sample = pred_original_sample_coeff * pred_original_sample + current_sample_coeff * sample
</code></pre><p>区别DDIM的处理过程将DDPM的马尔科夫链替换为非马尔科夫链过程而后进行采样，这样我们就可以每次迭代中跨多个step，从而减少推理迭代次数和时间：</p> \[x_{t-1}=\sqrt{\alpha_{t-1}}\left(\frac{x_t-\sqrt{1-\alpha_t}\epsilon_\theta(x_t,t)}{\sqrt{\alpha_t}}\right)+\sqrt{1-\alpha_{t-1}-\sigma_t^2}\epsilon_\theta(x_t,t)+\sigma_tz\] <pre><code class="language-python">std_dev_t = eta * variance ** (0.5)
pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * pred_epsilon
prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction
 
prev_sample = prev_sample + variance
</code></pre><h3 id="2pipeline">2、pipeline</h3><blockquote><p>所有支持的pipeline：<a href="https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/README.md">Diffusers Pipelines</a></p></blockquote><p>一般来说很多论文里面提出的模型，基本都是基于SD（StableDiffusion）等模型进行“微调”的，因此很多改进也都是去争对输入模型的参数进行调整（换言之就是搭积木讲故事），比如说改变输入图片内容、改变SD中条件等。除此之外分析一个<code>pipeline</code>直接通过分析里面的<code>__call__</code>即可，基本使用：</p><pre><code class="language-python">from diffusers import StableDiffusionPipeline
import torch

model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16)

if torch.cuda.is_available():
    pipe = pipe.to("cuda")

prompt = "A futuristic city at sunset, cyberpunk style, highly detailed, cinematic lighting"
image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0]
image.save("output.png")
</code></pre><h4 id="21-stablediffusionpipeline">2.1 StableDiffusionPipeline</h4><blockquote><p>https://huggingface.co/docs/diffusers/v0.34.0/en/api/pipelines/overview#diffusers.DiffusionPipeline</p></blockquote><p>很多论文里面基本都是直接去微调训练好的模型比如说StableDiffusion等，使用别人训练后的就少不了看到 <code>pipeline</code>的影子，直接介绍<a href="https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py"><code>StableDiffusionPipeline</code></a>的构建（<strong>文生图pipeline</strong>）。在<a href="https://github.com/huggingface/diffusers/blob/v0.34.0/src/diffusers/pipelines/pipeline_utils.py#L180">代码</a>中主要使用到的基础模型如下几个：1、VAE（AutoencoderKL）；2、CLIP（用于文本编码，CLIPTextModel、CLIPTokenizer）；3、Unet（模型骨架，UNet2DConditionModel）<br/><strong>Step-1</strong>：对输入文本进行编码（文生图直接输入文本）通过正、负编码对生成图像进行指导：</p><pre><code class="language-python">def encode_prompt(..., prompt, do_classifier_free_guidance,...,):
    # 1、判断文本编码器是否lora微调
    if lora_scale is not None and isinstance(self, StableDiffusionLoraLoaderMixin):
        self._lora_scale = lora_scale
        if not USE_PEFT_BACKEND:
            adjust_lora_scale_text_encoder(self.text_encoder, lora_scale)
        else:
            scale_lora_layers(self.text_encoder, lora_scale)
    # 2、通过prompt来确定需要生成多少图片
    ...
    # 3、对文本进行编码
    if prompt_embeds is None:
        ...
        text_inputs = self.tokenizer(...)
        text_input_ids = text_inputs.input_ids
            untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
        ...
        # 会显示一个过长截断警告
        ...
        # 选择clip中倒数第几层作为文本编码输出
        if clip_skip is None:
            # 默认直接最后一层
            prompt_embeds = self.text_encoder(text_input_ids.to(device), attention_mask=attention_mask)
                prompt_embeds = prompt_embeds[0]
        else:
            # 倒数层
            prompt_embeds = self.text_encoder(
                text_input_ids.to(device), attention_mask=attention_mask, output_hidden_states=True
            )
            prompt_embeds = prompt_embeds[-1][-(clip_skip + 1)]
            prompt_embeds = self.text_encoder.text_model.final_layer_norm(prompt_embeds)
        # 改变形状得到 batch_size（对应prompt数量）, 77, 748 CLIP: CLIP-ViT-L
        ...
        prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) 
    if do_classifier_free_guidance and negative_prompt_embeds is None:
        # 此部分和上面正常的编码处理方式相似直接对negative_prompt进行编码
        ...
    ...
    return prompt_embeds, negative_prompt_embeds
</code></pre><p><strong>Step-2</strong>：获取推理时间步以及生成latent变量<br/><strong>Step-3</strong>：模型处理</p><pre><code class="language-python"># 首先通过unet逐步进行解码图像
with self.progress_bar(total=num_inference_steps) as progress_bar:
    for i, t in enumerate(timesteps):
        ...
        noise_pred = self.unet(...)[0]
        ...
        # 通过step来从t反推t-1
        latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0]
        ...
        # classifier_free_guidance
if not output_type == "latent":
    # 图片返回
    image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False,generator=generator)[0]
    image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype)
else:
    # 直接返回没被vae处理的结果
    image = latents
    has_nsfw_concept = None

if has_nsfw_concept is None:
    do_denormalize = [True] * image.shape[0]
else:
    do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept]
image = self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize)
...
if not return_dict:
    return (image, has_nsfw_concept)

return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept)
</code></pre><blockquote><p><strong>补充-1</strong>：<code>classifier_free_guidance</code>（CFG） 以及 <code>classifier_guidance</code>（CG）<br/><code>classifier_guidance</code><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>：通过一个分类器来引导模型生成的方向，也就是使得模型按类进行生成。数学上描述为<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>：$\nabla p(x_t\vert y)=\nabla \log p(x_t)+ \nabla \log p(y \vert x_t)$ 也就是说前面部分代表unconditional score后面部分代表分类器的梯度，也就是添加一个分类器梯度来“指导”模型生成方向。<br/><code>classifier_free_guidance</code><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>：对上面的改进版本，上面过程中会额外训练一个分类器进而增加训练成本。因此对于上面计算公式中：$\nabla \log p(y \vert x_t)= \nabla p(x_t\vert y)- \nabla \log p(x_t)= -\frac{1}{\sqrt{1- \alpha_t}}(\epsilon_\theta(x_t, t, y)- \epsilon_\theta(x_t, t))$ 最后得到梯度过程为： $(w+1)\epsilon_\theta(x_t, t, y)- w\epsilon_\theta(x_t, t)$</p></blockquote><p>回到代码中，代码中具体操作过程为：<strong>1、文本编码过程中</strong>，这部分比较简单直接根据对negative_prompt进行CLIP text encoder处理即可（如果没有输入negative_prompt默认就是直接用空字符进行替代）如果进行CFG那么直接将两部分进行拼接（<code>torch.cat([negative_prompt_embeds, prompt_embeds])</code>） <code>prompt_embeds</code>；<strong>2、模型解码过程中</strong>，这部分处理过程比较粗暴，如果要进行CFG那么直接将latent扩展为两份（Uncond+Cond各一份）对应的text输出也是两份，通过一个模型处理之后再通过<code>chunk</code>分出无条件输出、有条件输出，最后计算两部分组合：$\epsilon(x,t)+ w(\epsilon(x,t,y)- \epsilon(x,t))$</p><pre><code class="language-python">if self.do_classifier_free_guidance:
    prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
...
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
...
noise_pred = self.unet(latent_model_input, t, prompt_embeds, ...)[0]

if self.do_classifier_free_guidance:
    noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
    noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond)

if self.do_classifier_free_guidance and self.guidance_rescale &gt; 0.0:
    noise_pred = rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale=self.guidance_rescale)
</code></pre><h4 id="22-stablediffusionxlinpaintpipeline">2.2 StableDiffusionXLInpaintPipeline</h4><blockquote><p><a href="https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl_inpaint.py">https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl_inpaint.py</a></p></blockquote><p>对于图像消除任务而言使用较多的也是此类pipeline（SDXL开源可以商用）具体使用代码如下：</p><pre><code class="language-python">from diffusers import StableDiffusionXLInpaintPipeline
from diffusers.utils import load_image, make_image_grid
import torch
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"

# 加载基础模型
base = StableDiffusionXLInpaintPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,  # 使用半精度浮点数以减少显存占用
    variant="fp16",             # 使用 fp16 变体以优化性能
    use_safetensors=True        # 使用 safetensors 格式以提高加载速度
).to(device)

# 加载优化模型（refiner model）
refiner = StableDiffusionXLInpaintPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-refiner-1.0",
    text_encoder_2=base.text_encoder_2,  # 共享基础模型的第二个文本编码器
    vae=base.vae,                        # 共享基础模型的变分自编码器
    torch_dtype=torch.float16,
    use_safetensors=True,
    variant="fp16",
).to(device)

img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png"
mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png"
init_image = load_image(img_url)
mask_image = load_image(mask_url)


prompt = "A majestic tiger sitting on a bench" 
negative_prompt = "distorted, blurry, low quality" 

num_inference_steps = 75 
high_noise_frac = 0.7

# 使用基础模型进行初步去噪（输出潜在表示）
base_output = base(
    prompt=prompt,
    negative_prompt=negative_prompt,
    image=init_image,
    mask_image=mask_image,
    num_inference_steps=num_inference_steps,
    denoising_end=high_noise_frac,  # 基础模型处理高噪声阶段
    output_type="latent"           # 输出潜在表示以供优化模型使用
).images

# 使用优化模型进行细节增强
refined_image = refiner(
    prompt=prompt,
    negative_prompt=negative_prompt,
    image=base_output,
    mask_image=mask_image,
    num_inference_steps=num_inference_steps,
    denoising_start=high_noise_frac,  # 优化模型处理低噪声阶段
).images[0]

# 可视化结果
grid = make_image_grid([init_image, mask_image, refined_image.resize((512, 512))], rows=1, cols=3)
grid.save("inpainting_result.png")
refined_image.save("refined_image.png")
</code></pre><p>首先模型输入主要为如下几个部分：1、文本输入；2、图片输入（正常图片以及mask图片）。<strong>首先对于文本编码</strong>。对于SDXL模型而言文本会通过两个clip的文本编码器进行编码（<strong>OpenCLIP-ViT/G</strong>：1280、<strong>CLIP-ViT/L</strong>：768）对于两个编码器代码处理思路为：</p><pre><code class="language-python">...
tokenizers = [self.tokenizer, self.tokenizer_2] if self.tokenizer is not None else [self.tokenizer_2]
text_encoders = (
    [self.text_encoder, self.text_encoder_2] if self.text_encoder is not None else [self.text_encoder_2]
)

if prompt_embeds is None:
    prompt_2 = prompt_2 or prompt
    prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2

    prompt_embeds_list = []
    prompts = [prompt, prompt_2]
    for prompt, tokenizer, text_encoder in zip(prompts, tokenizers, text_encoders):
        ...
        text_inputs = tokenizer(prompt,...)
        text_input_ids = text_inputs.input_ids
        untruncated_ids = tokenizer(prompt, ...).input_ids
        ...
        prompt_embeds = text_encoder(text_input_ids.to(device), output_hidden_states=True)
        ...
        prompt_embeds_list.append(prompt_embeds)

    prompt_embeds = torch.concat(prompt_embeds_list, dim=-1)
...
if self.do_classifier_free_guidance:
    prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
    ...
  prompt_embeds = prompt_embeds.to(device)
</code></pre><p>最后得到的<code>prompt_embeds</code>为：<code>[1, 77, 2048]</code>（由<code>[1, 77, 768]</code> 和 <code>[1, 77, 1280]</code>）拼接得到，如果要使用CFG的话就需要输入<code>negative_prompt</code>以及参数<code>guidance_scale</code>，对于<code>negative_prompt</code>的处理方式和上面相同。除此之外再代码中会有<code>added_cond_kwargs = {"text_embeds": add_text_embeds, "time_ids": add_time_ids}</code>这个参数，一般作用是：作为一个“额外”的条件添加到时间编码中（<code>emb = emb + aug_emb if aug_emb is not None else emb</code>）。不过值得注意的是，很多论文里面都使用：<strong>将图像和文本编码组合作为“文本编码”输入</strong>（<a href="https://www.big-yellow-j.top/posts/2025/07/25/ImageEraser2.html#:~:text=4s%E5%88%B00.5s%EF%BC%89%E3%80%82-,ObjectClear,-https%3A//arxiv.org">objectclear</a>）如果要实现这个（objectclear）功能伪代码如下：</p><pre><code class="language-python">...
    masked_image = init_image
    # masked_image = init_image * (mask &lt; 0.5)
    obj_only = init_image * (mask &gt; 0.5)
    obj_only = obj_only.to(device=device)
    object_embeds = self.image_prompt_encoder(obj_only)
prompt_embeds = self.postfuse_module(prompt_embeds, object_embeds, 5)
</code></pre><p>其中<code>prompt_embeds</code>就是正常的文本编码，<code>self.image_prompt_encoder</code>一般就是使用clip image的文本编码器这样一来就会将文本和图片编码成向量，<code>self.postfuse_module</code>一般就是将两个向量进行融合（这个一般就是通过mlp对齐维度之后直接拼接即可）<br/><strong>而后再图像编码</strong>。这部分比较容易直接通过vae去编码即可</p><pre><code class="language-python">...
masked_image = init_image * (mask &lt; 0.5)
...
mask = torch.cat([mask] * 2) if do_classifier_free_guidance else mask
masked_image_latents = self._encode_vae_image(masked_image, generator=generator)
</code></pre><p>对于图片一般做法是直接<code>masked_image = init_image * (mask &lt; 0.5)</code>但是论文里面有些直接使用<code>masked_image = init_image</code>。在文本以及图像都编码之后就是模型处理，只不过如果使用CFG：</p><pre><code class="language-python">if self.do_classifier_free_guidance:
    noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
    noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond)

if self.do_classifier_free_guidance and self.guidance_rescale &gt; 0.0:
    # Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
    noise_pred = rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale=self.guidance_rescale)
</code></pre><blockquote><p><strong>补充一点</strong>：如果要做CFG一般会将文本的prompt：negative_prompt_embeds（如果没有输入negative_prompt会直接用0代替）, prompt_embeds直接拼接起来，而后其他的就直接“拼接本体”</p></blockquote><h3 id="3lora微调">3、Lora微调</h3><p>和大语言模型的处理方式相似，通过<code>peft</code>去微调模型，简单了解一下<code>peft</code>里面微调的处理思路（值得注意的是，使用<code>peft</code>来微调只适用于基于<code>transformer</code>库来搭建的模型对于自己的模型可能没那么好的适应性）：</p><pre><code class="language-python">unet = UNet2DConditionModel.from_pretrained(
        "stable-diffusion-v1-5/stable-diffusion-inpainting", 
        subfolder="unet",
        cache_dir= '/data/huangjie',
    )
unet.requires_grad_(False)
print(unet.down_blocks[0])

unet_lora_config = LoraConfig(
        r=2,
        lora_alpha=2,
        lora_dropout=0.2,
        init_lora_weights="gaussian",
        target_modules=["to_k", "to_q", "to_v", "to_out.0", "add_k_proj", "add_v_proj"],
    )
unet.add_adapter(unet_lora_config)
print("after Lora Model:", unet.down_blocks[0])
</code></pre><p>上面两个过程模型变化为：<br/><img src="https://s2.loli.net/2025/07/02/7KOzpIxEN3bdZQ9.webp" alt="image.png" width="2000" height="501" loading="lazy" decoding="async"/></p><p>仔细分析一下<code>LoraConfig</code>里面的具体原理，因为很多模型（基于attention）基本就是q、k、v三个，因此通过<code>target_modules</code>指定哪些模块的参数是需要通过lora进行调整的模块。<code>init_lora_weights</code>代表lora初始化参数分布策略，参数<code>r</code>以及 <code>lora_alpha</code>代表的含义是：<br/> \(y=Wx+ \text{Dropout}(\text{B}_{out \times r} \text{A}_{r \times in}x) \times \frac{\text{lora\_alpha}}{r}\)</p><p><strong>经典问题</strong>：1、lora里面参数里面之所以初始化为0是因为对于我们的llm/DF模型一般都是“优秀”的，而对于“陌生”的数据通过零初始化确保一切干净，从 0 开始稳步适配（在训练初期引入噪声，可能导致不稳定，尤其在微调少步数、低学习率时，收敛更慢）2、多个lora模型同时作用于一个SD模型，并配置他们的各自权重，并且不同lora参数对模型生成的影响<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>:<br/><img src="https://s2.loli.net/2025/07/02/oi1umR5jek4LIWp.webp" alt="image.png" width="536" height="576" loading="lazy" decoding="async"/></p><h3 id="4adapters使用">4、Adapters使用</h3><p>lora也是Adapters（可以简单理解为对训练好的模型再去添加一个插件，通过这个插件让SD去生成其他的样式的图片）一种，具体见：<a href="https://www.big-yellow-j.top/posts/2025/07/06/DFBaseModel.html">深入浅出了解生成模型-6：常用基础模型与 Adapters等解析</a></p><h3 id="5自注意力技术attnprocessorattnprocessor2_0">5、自注意力技术（AttnProcessor、AttnProcessor2_0）</h3><blockquote><p>https://huggingface.co/docs/diffusers/v0.30.1/en/api/attnprocessor</p></blockquote><ul><li>1、AttnProcessor</li></ul><p>此部分就是非常常规的注意力计算方式</p><ul><li>2、AttnProcessor2_0</li></ul><p>它调用了 PyTorch 2.0 起启用的算子<code> F.scaled_dot_product_attention</code> 代替手动实现的注意力计算。这个算子更加高效，如果你确定 PyTorch 版本至少为 2.0，就可以用 AttnProcessor2_0 代替<br/> 参考知乎<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>中的描述，如何将自注意力进行修改，比如说如下代码：</p><pre><code class="language-python">from diffusers.models.attention_processor import (Attention,AttnProcessor,AttnProcessor2_0)
unet = UNet2DConditionModel()
for name, module in unet.named_modules():
   if isinstance(module, Attention) and "attn2" in name:
      print(f'name: {name}')
      print("*"*20)
      break
</code></pre><p>那么就会得到一个比如说：<code>down_blocks.0.attentions.0.transformer_blocks.0.attn2</code>比如说如果我需要将这个替换那么处理方式为：</p><pre><code class="language-python3">for name, module in unet.named_modules():
   if isinstance(module, Attention) and "attn2" in name:
      print(f'raw name: {name} \n raw module: {module.processor}')
      print("*"*20)
      if isinstance(module.processor, AttnProcessor2_0):
         module.set_processor(AttnProcessor())
      print(f"change name: {name} \n change module: {module.processor}")
      print("*"*20)
      break
</code></pre><p>这样一来有最开始的：<code>&lt;diffusers.models.attention_processor.AttnProcessor2_0 object at 0x7ff392734eb0&gt;</code> 替换为<code>&lt;diffusers.models.attention_processor.AttnProcessor object at 0x7ff5b776bc40&gt;</code>。或者直接改成自定义的处理方式：</p><pre><code class="language-python3">class CustonAttnProcessor(AttnProcessor):
    def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None):
        query = attn.to_q(hidden_states)
        encoder_states = hidden_states if encoder_hidden_states is None else encoder_hidden_states
        key = attn.to_k(encoder_states)
        value = attn.to_v(encoder_states)

        attn_scores = torch.baddbmm(
            torch.empty(query.shape[0], query.shape[1], key.shape[1], device=query.device),
            query,
            key.transpose(-1, -2),
            beta=0,
            alpha=attn.scale,
        )

        # 比如说对 attn_scores 取log
        attn_probs = torch.log(attn_scores) 
        attn_probs = attn_scores.softmax(dim=-1)

        hidden_states = torch.bmm(attn_probs, value)
        hidden_states = attn.to_out[0](hidden_states)
        hidden_states = attn.to_out[1](hidden_states)
        return hidden_states

attn_processor_dict = {}
for k in unet.attn_processors.keys():
    if "attn2" in k:
        attn_processor_dict[k] = CustonAttnProcessor()
    else:
        attn_processor_dict[k] = unet.attn_processors[k]
unet.set_attn_processor(attn_processor_dict)
for name, processor in unet.attn_processors.items():
   print(name, "=&gt;", type(processor))
</code></pre><p><strong>总的来说</strong>如果要去修改注意力处理方式，直接去便利<code>unet.attn_processors.keys()</code>然后去找到需要修改的层将其替换即可，只不过关键在于<code>CustonAttnProcessor</code>的定义方式。</p><h2 id="数据合成">数据合成</h2><p><a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/DFDataBuild">数据合成/标签算法汇总</a></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://arxiv.org/abs/2105.05233 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://zhuanlan.zhihu.com/p/640631667 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p>https://openaccess.thecvf.com/content/WACV2023/papers/Liu_More_Control_for_Free_Image_Synthesis_With_Semantic_Diffusion_Guidance_WACV_2023_paper.pdf <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p>https://github.com/cloneofsimo/lora/discussions/37 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p>https://zhuanlan.zhihu.com/p/680035048 <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Wed, 25 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/25/accelerate-diffusers.html</guid></item><item><title>深入浅出了解生成模型-4：一致性模型（consistency model）</title><link>https://www.big-yellow-j.top/posts/2025/06/17/CM.html</link><description><![CDATA[一致性模型（Consistency Model, CM）是一种针对扩散模型（Diffusion Models, DM）的生成加速方法，旨在解决传统扩散模型（如DDPM）因多步迭代去噪导致的高推理成本问题。其核心原理是通过引入一致性正则化（Consistency Regularization），将传统扩散模型中的随机微分方程（SDE）随机过程转化为可控的常微分方程（ODE）确定过程，使模型在不同时间步能一致预测干净图像。这一转化确保了从任意噪声图像到目标图像的直接映射，实现一步或少数几步即可生成高质量图像，大幅降低推理时间。在潜在一致性模型（LCM）及LCM-Lora中，通过在潜在空间（l...]]></description><content:encoded><![CDATA[<p>前面已经介绍了<a href="https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html">扩散模型</a>，在最后的结论里面提到一点：扩散模型往往需要多步才能生成较为满意的图像。不过现在有一种新的方式来加速（旨在通过少数迭代步骤）生成图像：<strong>一致性模型（consistency model）</strong>，因此这里主要是介绍一致性模型（consistency model）基本原理以及代码实践，值得注意的是本文不会过多解释数学原理，<strong>数学原理推导</strong>可以参考：</p><iframe src="//player.bilibili.com/player.html?isOutside=true&amp;aid=113086069474472&amp;bvid=BV1w1p3eHEtB&amp;cid=28321842065&amp;p=1" scrolling="no" border="0" frameborder="no" framespacing="=50" allowfullscreen="false"></iframe><p>具体代码推导可以直接看最后对于<a href="https://www.big-yellow-j.top/posts/2025/06/17/CM.html#:~:text=%E4%BA%86ODE%20solver%E3%80%82-,LCM%E8%92%B8%E9%A6%8F%E8%AE%AD%E7%BB%83%E5%88%B0%E5%BA%95%E5%9C%A8%E5%81%9A%E4%BB%80%E4%B9%88%EF%BC%9F,-%E9%80%9A%E8%BF%87%E7%BB%93%E5%90%88%E4%BB%A3%E7%A0%81">LCM</a>代码分析。介绍一致性模型之前需要了解几个知识：在传统的扩散模型中无论是加噪还是解噪过程都是随机的，在论文<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">1</a></sup>中（也就是CM作者宋博士的另外一篇论文）将这个随机过程（也就是随机微分方程SDE）转化成“固定的”过程（也就是常微分方程ODE），只有过程可控才能保证下面公式成立。</p><p><img src="https://s2.loli.net/2025/06/21/RxYJFlc3BUbntaE.webp" alt="image" width="1349" height="464" loading="lazy" decoding="async"/></p><h2 id="一致性模型consistency-model">一致性模型（Consistency Model）</h2><p><img src="https://s2.loli.net/2025/06/21/HnPuMUNaSq18jQG.webp" alt="image" width="817" height="304" loading="lazy" decoding="async"/></p><blockquote><p>其中<code>ODE</code>（常微分方程），在传统的扩散模型（Diffusion Models, DM）中，前向过程是从原始图像 $x_0$开始，不断添加噪声，经过 $T$步得到高斯噪声图像 $x_T$。反向过程（如 DDPM）通常通过训练一个逐步去噪的模型，将 $x_T$逐步还原为 $x_0$ ，每一步估计一个中间状态，因此推理成本高（需迭代 T 步）。而在 <strong>Consistency Models（CM）</strong> 中，模型训练时引入了 <strong>Consistency Regularization</strong>，使得模型在不同的时间步 $t$都能一致地预测干净图像。这样在推理时，无需迭代多步，而是可以通过一个单一函数$f(x ,t)$ 直接将任意噪声图像$x_t$ 还原为目标图像$x_0$ 。这大大减少了推理时间，实现了一步（或少数几步）生成。</p></blockquote><p>一致性模型（consistency model）在论文<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>里面主要是通过使用常微分方程角度出发进行解释的。Consistency Model 在 Diffusion Model 的基础上，新增了一个约束：<strong>从某个样本到某个噪声的加噪轨迹上的每一个点，都可以经过一个函数 $f$ 映射为这条轨迹的起点</strong>（也就是通过扩散处理的图像在不同的时间 $t$ 都可以直接转化为最开始的图像 $x_0$），用数学描述就是：$f:(x_t, t)\rightarrow x_\epsilon$，换言之就是需要满足： $f(x_t,t)=f(x_{t^\prime},t^\prime)$ 其中 $t,t^\prime \in [\epsilon,T]$，正如论文里面的图片描述：<br/><img src="https://s2.loli.net/2025/06/21/cXk2KYJA78PbdIW.webp" alt="image" width="781" height="357" loading="lazy" decoding="async"/></p><p>要满足上面的计算关系，作者在论文里面定义如下的等式关系（下面等式关系就是CM中核心概念）：</p> \[f_\theta(x,t)=c_{skip}(t)x+ c_{out}(t)F_\theta(x,t)\] <p>其中等式需要满足：$c_{skip}(\epsilon)=1,c_{out}(\epsilon)=0$ （$c_{skip}(t)=\frac{\sigma_{data}^2}{(t- \epsilon)^2+ \sigma_{data}^2}$， $c_{out}(t)=\frac{\sigma_{data}(t-\epsilon)}{\sqrt{\sigma_{data}^2+ t^2}}$），随着解噪过程（时间从：$T \rightarrow \epsilon$ 其中 $c_{skip}$ 的值逐渐增大，也就是当前的解噪图像占比权重增加），其中我的 $F_\theta$ 就是我们的神经网络模型（比如Unet）。既然使用了神经网络那么必定就需要设计一个损失函数，在论文里面作者设计的损失函数为：<strong>两个时间步之间生成得到的图像距离</strong>通过最小化这个值（比如说 $\Vert x_{t+1} - x_t \Vert_2$）来优化模型参数。作者对于模型训练给出两种训练方式</p><h3 id="直接通过蒸馏模型进行优化">直接通过蒸馏模型进行优化</h3><p>通过直接蒸馏的方式对模型参数进行优化，其中设计的损失函数为：</p> \[\mathcal{L}_{CD}^N(\boldsymbol{\theta},\boldsymbol{\theta}^-;\phi) = \mathbb{E}[\lambda(t_n)d(\boldsymbol{f}_{\boldsymbol{\theta}}(\mathbf{x}_{t_{n+1}},t_{n+1}),\boldsymbol{f}_{\boldsymbol{\theta}^-}(\hat{\mathbf{x}}_{t_n}^{\boldsymbol{\phi}},t_n))]\] <p>其中 $d$代表距离（比如 $l_1$ 或者 $l_2$ ）对于上面公式代表的含义是：从样本集中得到一个样本，而后加噪得到 $x_{t_{n+1}}$ ，然后利用预训练的 Diffusion 模型去一次噪，<strong>预测到另外一个点</strong> $\hat{x}<em>{t_n}^{\phi}$ 然后计算这两个点送入后的结果，用特定损失函数约束其一致（也就是： <strong>模型在两个时间步之间的预测结果是否一致</strong> 也就是 $f</em>\theta(t_{n+k})=f_\theta(t_n)$，其他的DF模型一般学的是噪声是不是一致的）。其中预测过程就是使用ODE solver进行处理，比如说：</p> \[\hat{x}_{t_n}^\phi= x_{t_{n+1}}- (t_n- t_{n+1})t_{n+1}\nabla_{x_{t_{n+1}}}\log p_{t_{n+1}}(x_{t_{n+1}})\] <p>其中DDIM、DPM++就是ODE solver一种。</p><blockquote><p>欧拉法： $y_{n+1}= y_n+h*f(t_n, y_n)$ 其中h代表时间步长，f代表当前导数估计。不过值得进一步了解的是，在DL中大部分函数都是直接通过神经网络进行“估算的”，也就是说对于上面的 $\nabla_{x_{t_{n+1}}}\log p_{t_{n+1}} \textcolor{red}{≈} s_\theta(x_{t_{n+1}},t_{n+1})$ 其中 $s_\theta$代表的是训练好的去噪网络。</p></blockquote><p>那么这样一来整个过程就变成了：<br/><img src="https://s2.loli.net/2025/06/21/ZpA3D7iqJcI5KdV.webp" alt="image" width="473" height="360" loading="lazy" decoding="async"/></p><h3 id="直接训练模型进行优化">直接训练模型进行优化</h3><p>直接训练模型进行优化，其中具体的过程为：<br/><img src="https://s2.loli.net/2025/06/21/Y8QCsmnaqiRlkbP.webp" alt="image" width="742" height="556" loading="lazy" decoding="async"/></p><h2 id="lcmlcm-lora">LCM/LCM-Lora</h2><p>潜在一致性模型（Latent Consistency Model）<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">3</a></sup>以及LCM-Lora<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">4</a></sup>（LCM的Lora优微调）通过再latent space中使用一致性模型（stable diffusion model通过VAE将图像进行压缩到latent sapce而后通过DF模型训练并且最后再通过VAE decoder输出），在LCM中主要提出两点：<br/> 1、<strong>Skipping-Step</strong>：因为在最开始的CM中计算两个相邻的时间步之间的loss由于时间步过于接近，就会导致loss很小，因此通过跳步解决这个问题，这样loss就会变成：$d(f(x_{t_{n+\textcolor{red}{k}}}, t_{n+\textcolor{red}{k}}), f(x_{t_n}, t_n))$。<br/> 2、引入<strong>Classifier-free guidance (CFG)</strong> 那么整个loss计算就会变成：$d(f(x_{t_{n+\textcolor{red}{k}}}, \textcolor{red}{w}+ \textcolor{red}{c}, t_{n+\textcolor{red}{k}}), f(x_{t_n}, \textcolor{red}{w}+ \textcolor{red}{c}+ t_n))$，公式中c代表文本，对于CFG而言其实就是一个改进的ODE solver（见下面算法流程中的蓝色部分）<br/> 对于LCD算法流程，其中蓝色部分为LCM所修改的内容：<br/><img src="https://s2.loli.net/2025/06/21/bftKAHLBJW21QFv.webp" alt="GZ7hs3blVFiJpfN.webp" width="1070" height="386" loading="lazy" decoding="async"/><br/> 对于最后得到的实验结果分析：</p><ul><li>不同的k对结果的影响</li></ul><p><img src="https://s2.loli.net/2025/06/21/JsrT8CbifgUaxv1.webp" alt="download.webp" width="1024" height="283" loading="lazy" decoding="async"/><br/> 在DPM-solver++和DPM-Solver中基本只需要 2000 步迭代，LCM 4 步采样的 FID 就已经基本收敛了</p><ul><li>不同的Guidance Scale对结果的影响</li></ul><p><img src="https://s2.loli.net/2025/06/21/Uz29VWDdXb7hYHx.webp" alt="image" width="998" height="743" loading="lazy" decoding="async"/><br/> LCM 作者用不同 LCM 的迭代次数与不同 Guidance Scale 做了对比。发现 $w$ 增加有助于提升 CLIP Score，但是损失了 FID 指标（即多样性）的表现。另外，LCM 迭代次数为 2、4、8 时，CLIP Score 和 FID 相差都不大，说明了 LCM 的蒸馏性能确实非常强悍，两步前向的效果可能都足够好了，只是一步前向的结果还差些。<br/><strong>总得来说</strong>，在LCM中主要是做了如下几点改进：1、使用skipping-step来“拉大”相邻点之间的距离计算；2、改进了ODE solver。</p><h2 id="lcm蒸馏训练到底在做什么">LCM蒸馏训练到底在做什么？</h2><blockquote><p>通过结合<a href="https://github.com/huggingface/diffusers/blob/main/examples/consistency_distillation/train_lcm_distill_lora_sdxl.py">代码</a>理解</p></blockquote><p><strong>首先</strong>直接使用我们使用我们训练好的unet模型（<code>unet = UNet2DConditionModel.from_pretrained</code>）作为函数$f_\theta$。因为在CM中基于ODE（常微分方程）保证“路径”一致，并且CM核心观点就是希望模型学习从一个“晚”的时间步（接近噪声状态）预测出一个“早”的时间步（接近干净图像）下的表示（<strong>让模型学习 $z_{t_{n+k}}$ 预测出 $z_{t_n}$</strong>）。那么在采样步数过程中代码处理方式如下：</p><pre><code class="language-python">bsz = latents.shape[0]
topk = noise_scheduler.config.num_train_timesteps // args.num_ddim_timesteps #noise_scheduler使用的DDPM topk=1000//50=20 也就是说将连续时间分为 20 段
index = torch.randint(0, args.num_ddim_timesteps, (bsz,), device=latents.device).long() # 得到 t_{n+k}，得到每一段的索引
start_timesteps = solver.ddim_timesteps[index] # 得到 t_{n}
timesteps = start_timesteps - topk #solver使用的DDIM
timesteps = torch.where(timesteps &lt; 0, torch.zeros_like(timesteps), timesteps)

c_skip_start, c_out_start = scalings_for_boundary_conditions(start_timesteps,...)
...
c_skip, c_out = scalings_for_boundary_conditions(timesteps, ...)
...
noisy_model_input = noise_scheduler.add_noise(latents, noise, start_timesteps)
</code></pre><p>对于上述过程定量分析，比如说原始步骤1000步，如果蒸馏到50步LCM，那么计算步幅（topk）得到20，而后去进行随机采样（index）假设值为10，那么就可以得到起点（start_timesteps）:10*20=200，终点（timesteps）：200-20=180，进而计算系数在t=200时候（假设）c_skip、c_out=0.2，0.8，t=180时候c_skip、c_out=0.25，0.75。那么最终的任务中，<strong>学生模型过程</strong>拿着 $t=200$ 的噪声，试图预测 $t=0$ 的样子，<strong>教师模型过程</strong>：拿着 $t=200$ 的噪声，走一步 DDIM 到 $t=180$，告诉学生：“根据我的经验，在 $t=180$ 时图像的一致性终点应该是这个，你照着这个练。”</p><ul><li><strong>学生模型处理过程</strong></li></ul><p>在得到噪声之后直接输入到模型中也就是计算预测噪声（<code>noise_pred = unet(noisy_model_input,...).sample</code>）并且去反推预测结果 $F_\theta(x,t)$（<code>pred_x_0 = get_predicted_original_sample()</code>），LCM核心<strong>从某个样本到某个噪声的加噪轨迹上的每一个点，都可以经过一个函数映射为这条轨迹的起点</strong>，那么可以根据公式（$f_\theta(x,t)=c_{skip}(t)x+ c_{out}(t)F_\theta(x,t)$）就可以得到（学生模型）最后的输出<code>model_pred=c_skip_start * noisy_model_input + c_out_start * pred_x_0</code>。也就<a href="https://github.com/huggingface/diffusers/blob/8c48ec05ede9ea6012b9e3f3bc976d45175381d8/examples/consistency_distillation/train_lcm_distill_lora_sdxl.py#L1274C17-L1289C87">对应下面代码</a>：</p><pre><code class="language-python">noise = torch.randn_like(latents)
noisy_model_input = noise_scheduler.add_noise(latents, noise, start_timesteps)
...
noise_pred = unet(noisy_model_input,start_timesteps,...).sample
pred_x_0 = get_predicted_original_sample(noise_pred,start_timesteps,noisy_model_input,noise_scheduler.config.prediction_type...)#计算反推样本起点x0
model_pred = c_skip_start * noisy_model_input + c_out_start * pred_x_0
</code></pre><ul><li><strong>教师模型处理过程</strong></li></ul><p>处理过程和上面的处理方式是相似的（<strong>让模型学习 $z_{t_{n+k}}$ 预测出 $z_{t_n}$</strong>）也就是对应下面的：<br/><img src="https://s2.loli.net/2025/08/06/dNpLo4DKht1fZB6.webp" alt="image.png" width="785" height="68" loading="lazy" decoding="async"/><br/> 那么<a href="https://github.com/huggingface/diffusers/blob/8c48ec05ede9ea6012b9e3f3bc976d45175381d8/examples/consistency_distillation/train_lcm_distill_lora_sdxl.py#L1297C1-L1380C64">具体的代码</a>操作如下：</p><pre><code class="language-python">noisy_model_input = noise_scheduler.add_noise(latents, noise, start_timesteps)
...
accelerator.unwrap_model(unet).disable_adapters() # 因为我用lora去微调我的模型因此教师模型首先将lora取消掉
with torch.no_grad():
  # 1. Get teacher model prediction on noisy_model_input z_{t_{n + k}} and conditional embedding c
  cond_teacher_output = unet(noisy_model_input,start_timesteps,...).sample
  cond_pred_x0 = get_predicted_original_sample(cond_teacher_output,start_timesteps,noisy_model_input,...)
  cond_pred_noise = get_predicted_noise(cond_teacher_output,start_timesteps,noisy_model_input,...)

  # 2. Get teacher model prediction on noisy_model_input z_{t_{n + k}} and unconditional embedding 0
  uncond_prompt_embeds = torch.zeros_like(prompt_embeds)
  uncond_pooled_prompt_embeds = torch.zeros_like(encoded_text["text_embeds"])
  uncond_added_conditions = copy.deepcopy(encoded_text)
  uncond_added_conditions["text_embeds"] = uncond_pooled_prompt_embeds
  uncond_teacher_output = unet(noisy_model_input,start_timesteps,encoder_hidden_states=uncond_prompt_embeds.to(weight_dtype),encoder_hidden_states=uncond_prompt_embeds.to(weight_dtype),added_cond_kwargs={k: v.to(weight_dtype) for k, v in uncond_added_conditions.items()},).sample
  uncond_pred_x0 = get_predicted_original_sample(uncond_teacher_output,start_timesteps,noisy_model_input,...)
  uncond_pred_noise = get_predicted_noise(uncond_teacher_output,start_timesteps,noisy_model_input,...)

  # 3. Calculate the CFG estimate of x_0 (pred_x0) and eps_0 (pred_noise)
  pred_x0 = cond_pred_x0 + w * (cond_pred_x0 - uncond_pred_x0)
  pred_noise = cond_pred_noise + w * (cond_pred_noise - uncond_pred_noise)
  # 4. Run one step of the ODE solver to estimate the next point x_prev on the
  # augmented PF-ODE trajectory (solving backward in time)
  # Note that the DDIM step depends on both the predicted x_0 and source noise eps_0.
  x_prev = solver.ddim_step(pred_x0, pred_noise, index).to(unet.dtype)
</code></pre><p>对于上述代码可以这么理解，我的学生模型<strong>使用了Lora进行处理</strong>，我的教师模型<strong>不使用Lora处理的模型</strong>那么直接<code>disable_adapters</code>，在LCM的训练过程中<strong>核心思想</strong>是把输入 $x$的一部分“直接跳过” （$c_{skip}$），剩下的部分用模型预测 $F_\theta$修正，对应计算过程：$f_\theta(x,t)=c_{skip}(t)x+ c_{out}(t)F_\theta(x,t)$，那么对应学生和教师模型处理方式是一致的，只不过在LCM中会使用CFG所以处理过程就比学生模型稍复杂一点在得到预测x_0以及噪声noise之后，通过<code>x_prev = solver.ddim_step(pred_x0, pred_noise, index)</code>让 <strong>教师模型为学生模型提供一条确定的去噪路径（这个过程直接通过ODE计算得到），从而让学生模型学习如何从噪声生成高质量样本</strong>，而后就是<a href="https://github.com/huggingface/diffusers/blob/8c48ec05ede9ea6012b9e3f3bc976d45175381d8/examples/consistency_distillation/train_lcm_distill_lora_sdxl.py#L1365C17-L1384C92">计算loss</a>：</p><pre><code class="language-python">start_timesteps = solver.ddim_timesteps[index]
timesteps = start_timesteps - topk
...
accelerator.unwrap_model(unet).enable_adapters()
with torch.no_grad():
  target_noise_pred = unet(x_prev,timesteps,...).sample
  pred_x_0 = get_predicted_original_sample(target_noise_pred,timesteps,x_prev,)
  target = c_skip * x_prev + c_out * pred_x_0

if args.loss_type == "l2":
  loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
</code></pre><p>对于上述loss过程简单理解为：在最开始我的模型直接从200步预测最后图像并通过公式 $f_\theta(x,t)=c_{skip}(t)x+ c_{out}(t)F_\theta(x,t)$ 反推最后的结果得到<strong>model_pred</strong>，而后我的教师模型直接根据200步（start_timesteps）的加噪图像去预测噪声，并通过DDIM采样器去计算20步后的结果（180）得到 <strong>x_prev</strong> 而后我的模型再去预测180步到最后图像并且还是通过上面跳步公式得到 <strong>target</strong>，最后根据“模型在两个时间步之间的预测结果一致”假设去计算loss。</p><h2 id="总结">总结</h2><p>总的来说在LCM中核心概念：1、<strong>从某个样本到某个噪声的加噪轨迹上的每一个点，都可以经过一个函数 $f$ 映射为这条轨迹的起点</strong>，将随机过程变为确定过程；2、<strong>模型在两个时间步之间的预测结果一致</strong>，$f(x_t,t)=f(x_{t^\prime},t^\prime)$ 其中 $t,t^\prime \in [\epsilon,T]$。除此之外为了实现少的步骤（跳步）的生成发生，通过计算公式 $f_\theta(x,t)=c_{skip}(t)x+ c_{out}(t)F_\theta(x,t)$ 直接从 $t$步跳到 $0$ 步。<br/> 训练过程可以简单理解为：对于输入图像 $x$，直接添加 $n$ 步的噪声得到 $x_n$，而后我的学生模型直接去预测 $t_0$ 时候的结果 $y_1$；同时，我的教师模型（预训练好的扩散模型）从 $x_n$ 出发，通过 DDIM 采样器向前走一步（跨越 $k$ 个时间步），得到 $t_{n-k}$ 时刻在轨迹上的观察点 $x_{n-k}$；而后再去用学生模型通过 $x_{n-k}$ 预测 $t_0$ 的结果得到 $y_2$。最后计算 $y_1$ 与 $y_2$ 之间的距离损失（Consistency Loss），迫使模型无论从哪个时间步出发，预测的终点都指向同一点。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:4" role="doc-endnote"><p>https://arxiv.org/pdf/2011.13456 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p>https://arxiv.org/abs/2303.01469 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://arxiv.org/abs/2310.04378 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p>https://arxiv.org/abs/2311.05556 <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Tue, 17 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/17/CM.html</guid></item><item><title>图像消除论文-1：PixelHacker、PowerPanint、Attentive Eraser</title><link>https://www.big-yellow-j.top/posts/2025/06/11/ImageEraser1.html</link><description><![CDATA[本文介绍PixelHacker、PowerPanint、Attentive Eraser等图像擦除模型及其实际测试效果。PixelHacker框架类似Diffusion Model，输入image、mask、mask image并经VAE编码，通过2D遗忘矩阵计算注意力，测试显示仅支持512x512低分辨率图像，复杂场景易丢失背景文字、人物细节或生成错误。PowerPanint基于SD v1.5与CLIP，结构同DF模型，输入噪声潜在分布、mask图像及mask，组合四部分条件优化，可处理部分移除、全部移除及复杂布局场景，但输出分辨率会被修改（如输入2250x1500输出960x640）...]]></description><content:encoded><![CDATA[<p>本文主要介绍几篇图像擦除论文模型：PixelHacker、PowerPanint、Attentive Eraser，并且实际测试模型的表现效果</p><h2 id="pixelhacker">PixelHacker</h2><blockquote><p>Code: https://github.com/hustvl/PixelHacker</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/uEdC6KQFZIa54mH.webp" alt="image.png" width="1269" height="564" loading="lazy" decoding="async"/></p><p>模型整体框架和Diffusion Model相似，输入分为3部分：1、image；2、mask；3、mask image而后将这三部分进行拼接，然后通过VAE进行encoder，除此之外类似Diffusion Model中处理，将condition替换为mask内容（这部分作者分为两类：1、foreground（116种类别）；2、background（21种类别））作为condition（对于foreground直接通过编码处理，对于background的3部分通过：$M_{scene}+M_{rand}P_{rand}+M_{obj}P_{obj}$ 分别对于background的3部分）然后输入到注意力计算中。</p><blockquote><p><img src="https://s2.loli.net/2025/06/21/Tc9vIUFLgtC7hy3.webp" alt="image.png" width="621" height="258" loading="lazy" decoding="async"/></p></blockquote><p>注意力计算过程，对于通过VAE编码后的内容$L_{in}$ 直接通过 $LW$ 计算得到QKV，并且通过 <strong>2D遗忘矩阵</strong> $G_t$计算过程为：</p> \[G_t = \alpha_t^T \beta_t \in \mathbb{R}^{d_k \times d_v}, \alpha_t = \sigma(\text{Lin}_{\alpha} W_\alpha + b_\alpha)^{\frac{1}{2}} \in \mathbb{R}^{L \times d_k}, \beta_t = \sigma(\text{Lin}_{\beta} W_\beta + b_\beta)^{\frac{1}{2}} \in \mathbb{R}^{L \times d_v},\] <p>$L_t$计算过程：<br/><img src="https://s2.loli.net/2025/06/21/z2KI4iwCQn6rugj.webp" alt="image.png" width="522" height="188" loading="lazy" decoding="async"/></p><p><img src="https://s2.loli.net/2025/06/21/MdRjGAcqBtbhs95.webp" alt="image.png" width="605" height="240" loading="lazy" decoding="async"/></p><h3 id="pixelhacker实际测试效果">PixelHacker实际测试效果</h3><table><thead><tr><th style="text-align: center">图像</th><th style="text-align: center">mask</th><th style="text-align: center">结果</th><th style="text-align: center">问题</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/lcig2OIXxqnP5Qe.webp" alt="image" width="640" height="641" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/DIH56QsZqxYV8W7.webp" alt="image" width="640" height="641" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/ia2jrbvQI6dhMDN.webp" alt="image" width="512" height="512" loading="lazy" decoding="async"/></td><td style="text-align: center">背景文字细节丢失</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/ValhFUjG7OzMnR2.webp" alt="image" width="1024" height="1024" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/qhuWIalwOGUY3p6.webp" alt="image" width="1024" height="1024" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/sup9MYevZq24kgE.webp" alt="image" width="512" height="512" loading="lazy" decoding="async"/></td><td style="text-align: center">人物细节</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/ValhFUjG7OzMnR2.webp" alt="image" width="1024" height="1024" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/IJ42xjBqVOvEmY6.webp" alt="image" width="640" height="641" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/KtYfwqe1HRIjJUn.webp" alt="image" width="512" height="512" loading="lazy" decoding="async"/></td><td style="text-align: center">生成错误</td></tr></tbody></table><p><strong>分析</strong>：只能生成较低分辨率图像（512x512，<a href="https://github.com/hustvl/PixelHacker/issues/7">Github</a>），去除过程中对于复杂的图像可能导致细节（背景中的文字、图像任务）处理不好。</p><h2 id="powerpanint">PowerPanint</h2><blockquote><p>A Task is Worth One Word: Learning with Task Prompts for High-Quality Versatile Image Inpainting<br/> From: https://github.com/open-mmlab/PowerPaint<br/> Modle：<em>SD v1.5、CLIP</em></p></blockquote><p><img src="https://s2.loli.net/2025/06/21/kADH1if2yoreSWB.webp" alt="image.png" width="1607" height="660" loading="lazy" decoding="async"/></p><p>模型整体结构和DF模型相同，输入模型内容为：噪声的潜在分布、mask图像（$x \bigodot (1-m)$）、mask；在论文中将condition替换为4部分组合（微调两部分：$P_{obj}$ 以及 $P_{ctxt}$）：1、$P_{obj}$<br/> 1、<strong>增强上下文的模型感知</strong>：使用随机mask训练模型并对其进行优化以重建原始图像可获得最佳效果，通过使用$P_{ctxt}$（可学习的）<strong>让模型学会如何根据图像的上下文信息来填充缺失的部分,而不是依赖于文本描述</strong>，优化过程为：<br/><img src="https://s2.loli.net/2025/06/21/EwPgsX7M1WinzqB.webp" alt="image.png" width="1043" height="99" loading="lazy" decoding="async"/></p><p>2、<strong>通过文本增强模型消除</strong>：通过使用$P_{obj}$：训练过程和上面公式相同，不过将识别得到的物体bbox作为图像mask并且将 $P_{obj}$作为mask区域的文本描述，<strong>引导模型根据给定的文本描述生成对应的对象。</strong></p><blockquote><p>第1和第2点区别在于，第二点输入有文本描述，而第一点就是可学习的文本</p></blockquote><p>3、<strong>物品移除</strong>：使用移除过程中模型很容易进入一个“误解”：模型是新生成一个内容贴在需要消除的内容位置而不是消除内容（比如下面结果），作者的做法是直接将上面两个进行加权：<br/><img src="https://s2.loli.net/2025/06/21/YOE9e6rwBv7qKhL.webp" alt="image.png" width="1080" height="87" loading="lazy" decoding="async"/></p><p>4、<strong>通过形状增强模型消除</strong>：$P_{shape}$：使用精确的对象分割mask和对象描述进行训练，不过这样会使得模型过拟合（输入文本和选定的区域，可能模型只考虑选定区域内容生成），因此替换做法是：直接对精确识别得到内容通过 <em>膨胀操作</em>让他没那么精确，具体处理操作为：<br/><img src="https://s2.loli.net/2025/06/21/m63l7zBZQoOrbvK.webp" alt="image.png" width="762" height="236" loading="lazy" decoding="async"/></p><p>于此同时参考上面过程还是进行加权组合<br/><img src="https://s2.loli.net/2025/06/21/oqywbL7sGHT5Jl3.webp" alt="image.png" width="894" height="68" loading="lazy" decoding="async"/></p><h3 id="powerpanint实际测试效果">PowerPanint实际测试效果</h3><blockquote><p>只测试 <code>Object removal inpainting</code>，测试的权重：<code>ppt-v1</code></p></blockquote><table><thead><tr><th style="text-align: center">图像</th><th style="text-align: center">mask</th><th style="text-align: center">结果</th><th style="text-align: center">测试</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/krH6sUt9YVvnidI.webp" alt="sa_329749.jpg" width="2243" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/yf2pz3aTWQrAvXG.webp" alt="mask-1.png" width="952" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/2M5VKDpa1H9kRUA.webp" alt="gt-1.png" width="952" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">部分移除</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/21/krH6sUt9YVvnidI.webp" alt="sa_329749.jpg" width="2243" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/V8LRsOryWegcKUw.webp" alt="mask-2.png" width="952" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/Cuj24vh3QIGieSk.webp" alt="gt-2.png" width="952" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">全部移除</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/LGjovJgFxflrQU7.webp" alt="sa_325886.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/MavCANuoThiEdPO.webp" alt="mask-image-1.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/pPurFsomIdBAyKW.webp" alt="gt-image-1.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">复杂布局全部移除</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/LGjovJgFxflrQU7.webp" alt="sa_325886.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/QwLKMzPA1NdsDBI.webp" alt="mask-image-2.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/ndiQBHgvwNRFAor.webp" alt="gt-image-2.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">复杂布局细小内容移除</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/LGjovJgFxflrQU7.webp" alt="sa_325886.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/dq86IZAkCo1Sg9i.webp" alt="mask-3.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/AoEXBhQjrNaCwZx.webp" alt="gt-3.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">多目标内容移除</td></tr><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/Z2maup6b5hKBEnv.webp" alt="sa_331946.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/GFwYgCoEaRhVjdx.webp" alt="image-mask _2_.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/06/22/cWGXqlyv6KJia7p.webp" alt="image _1_.png" width="960" height="640" loading="lazy" decoding="async"/></td><td style="text-align: center">多目标内容移除</td></tr></tbody></table><p>总的来说：PowerPanint还是比较优秀的消除模型，总体移除效果“说得过去”（如果不去追求消除的细节，见下面图像，比如说消除带来的图像被扭曲等）不过得到最后的图像的尺寸会被修改（in：2250x1500 out：960x640，此部分没有仔细去检查源代码是否可以取消或者自定义），除此之外，参考Github上提出的<a href="https://github.com/open-mmlab/PowerPaint/issues/111">issue-1</a>：图像 resize 了，修改了分辨率，VAE 对人脸的重建有损失，如果mask没有完全覆盖掉人，留了一些边缘，模型有bias容易重建生成出新的东西。<a href="https://github.com/open-mmlab/PowerPaint/issues/56">issue-2</a>：平均推理速度20s A100 GPU。<br/><img src="https://s2.loli.net/2025/06/22/vZsS4iO6QcWNult.webp" alt="image.png" width="1175" height="571" loading="lazy" decoding="async"/></p><h2 id="improving-text-guided-object-inpainting-with-semantic-pre-inpainting">Improving Text-guided Object Inpainting with Semantic Pre-inpainting</h2><blockquote><p>From: https://github.com/Nnn-s/CATdiffusion.<br/><strong>没有提供权重无法测试</strong></p></blockquote><p><img src="https://s2.loli.net/2025/06/22/DbZat7LKTMCpXhA.webp" alt="image.png" width="1059" height="606" loading="lazy" decoding="async"/></p><p>由于DDM生成过程中是不可控的，本文提出通过text来提高模型可控。相比较之前研究（直接将图片通过VAE处理输入DF中，并且将文本作为条件进行输入），最开始得到的latent space和text feature之间存在“信息不对齐”。在该文中“提前”将text feature输入到模型中。具体做法是：</p><ul><li><strong>首先通过CLIP来对齐特征信息</strong></li></ul><p>将image通过clip image encoder进行编码得到特征而后通过<strong>SemInpainter</strong>：同时结合可学习的位置信息（PE）、可学习的mask图像特征（ME）、文本特征，整个过程为：<br/><img src="https://s2.loli.net/2025/06/22/wZk3FCtjslSy1ir.webp" alt="image.png" width="935" height="123" loading="lazy" decoding="async"/></p><p>其中：<strong>SemInpainter</strong>（和CLIP的image encoder相似结构）根据视觉上下文和文本提示c的条件下，恢复CLIP空间中mask对象的ground-truth语义特征，说人话就是通过知识蒸馏方式来训练这个模块参数。对于两部分特征最后通过下采样方式得到最后特征：<br/><img src="https://s2.loli.net/2025/06/22/V7YQFwaHhKzu8fI.webp" alt="image.png" width="977" height="88" loading="lazy" decoding="async"/></p><ul><li>**reference adapter layer (RefAdapter) **</li></ul><p><img src="https://s2.loli.net/2025/06/22/61q9QjAmYCZLnHx.webp" alt="image" width="962" height="157" loading="lazy" decoding="async"/></p><h2 id="attentive-eraser">Attentive Eraser</h2><blockquote><p><a href="https://ojs.aaai.org/index.php/AAAI/article/view/34285">Attentive Eraser: Unleashing Diffusion Model’s Object Removal Potential via Self-Attention Redirection Guidance</a><br/><a href="https://www.modelscope.cn/studios/Anonymou3/AttentiveEraser">测试demo</a><br/> AAAI-2025</p></blockquote><p>模型结构：</p><p><img src="https://s2.loli.net/2025/06/26/6IeoTPcVBxu9mbz.webp" alt="image.png" width="1625" height="913" loading="lazy" decoding="async"/></p><p>模型出出发点：图像擦除过程中会生成随机伪影，以及在删除后无法用适当的内容重新绘制前景对象区域。主要改进：<br/> 1、<strong>Attention Activation and Suppres-sion (AAS)</strong>：是一种自我注意机制修改操作，专为应对物体移除任务的固有挑战而量身定制，旨在使前景物体区域的生成更加关注背景，同时消除物体的外观信息。此外，”相似性抑制”（SS）可抑制由于自我注意的固有特性而可能导致的对相似物体的高度关注。具体做法：计算得到注意得分：$S$；以及$A=softmax(S)\in R^{N^2 \times N^2}$。其中具体计算方式（对于上面流程图中对呀公式序号，其中$M_{l,t}$代表的是如果属于obg那么标记1否则0）：</p><p><img src="https://s2.loli.net/2025/06/26/ukhXjl5v9Cf71xn.webp" alt="image.png" width="1387" height="488" loading="lazy" decoding="async"/><br/><img src="https://s2.loli.net/2025/06/26/4bJXif5AV2Sku9I.webp" alt="image.png" width="869" height="99" loading="lazy" decoding="async"/></p><p>对于公式13：强化obj信息（将$obj\rightarrow obj$设定为负无穷）；对于公式14：强化$obj\rightarrow bg$ 将其设为负无穷。<br/> 2、<strong>Self-Attention Redirection Guidance (SARG)</strong>：这是一种应用于扩散反向取样过程的引导方法，它通过 AAS 利用重定向自我注意引导取样过程朝物体移除的方向进行。<br/><img src="https://s2.loli.net/2025/06/26/FjqnizZ2AORHgId.webp" alt="image.png" width="883" height="131" loading="lazy" decoding="async"/></p><p>算法流程：<br/><img src="https://s2.loli.net/2025/06/26/lnbURV15qryCQKa.webp" alt="image.png" width="1188" height="1040" loading="lazy" decoding="async"/></p><h3 id="attentive-eraser实际测试效果">Attentive Eraser实际测试效果</h3><table><thead><tr><th>原图</th><th>Mask</th><th>结果</th><th> </th></tr></thead><tbody><tr><td><img src="https://s2.loli.net/2025/06/26/znSUtwamOk9r47I.webp" alt="sa_324952.jpg" width="2258" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/QXdWSb46FREakVN.webp" alt="sa_324952-0.jpg" width="2258" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/BRFf3E2Qamyu8zv.webp" alt="image_2_.webp" width="2258" height="1500" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/Bw4D9pEi7McULbv.webp" alt="sa_325886.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/P8mKbFdTqxZ19Yn.webp" alt="sa_325886-1.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/kl5tOBd4IufT2C9.webp" alt="image_3_.webp" width="2250" height="1500" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/kxZjsRLSvpX96ne.webp" alt="sa_324501.jpg" width="1500" height="2250" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/bHMSowgfXm4sqO5.webp" alt="sa_324501-2.jpg" width="1500" height="2250" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/r7nS6ZQYLs8kuv1.webp" alt="image.webp" width="1500" height="2250" loading="lazy" decoding="async"/></td><td> </td></tr><tr><td><img src="https://s2.loli.net/2025/06/26/SA8rRFMc4Zjlp21.webp" alt="sa_324930.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/fQdXwRUCg5JVjs6.webp" alt="sa_324930-1.jpg" width="2250" height="1500" loading="lazy" decoding="async"/></td><td><img src="https://s2.loli.net/2025/06/26/BXtbq8gEZPAJTjS.webp" alt="image_1_.webp" width="2250" height="1500" loading="lazy" decoding="async"/></td><td> </td></tr></tbody></table><h2 id="总结">总结</h2><p>简单终结上面几篇论文，基本出发思路都是基于Stable diffusion Moddel然后通过修改Condition方式：无论为是CLip编码文本嵌入还是clip编码图像嵌入。不过值得留意几个点：1、对于mask内容可以用“非规则”（类似对mask内容进行膨胀处理）的方式输入到模型中来提高能力。2、在图像擦除中容易出现几个小问题：<strong>图像替换问题</strong>（理论上是擦除图像但是实际被其他图像给“替换”）、<strong>图像模糊问题</strong>（擦除图像之后可能会在图像上加一个“马赛克”，擦除区域模糊）对于这两类问题可以参考<a href="https://openaccess.thecvf.com/content/CVPR2025/papers/Wang_Towards_Enhanced_Image_Inpainting_Mitigating_Unwanted_Object_Insertion_and_Preserving_CVPR_2025_paper.pdf">论文</a>。<br/><strong>进一步阅读</strong>： 1、<a href="https://arxiv.org/pdf/2504.00996">https://arxiv.org/pdf/2504.00996</a>；2、<a href="https://openaccess.thecvf.com/content/CVPR2025/papers/Kim_RAD_Region-Aware_Diffusion_Models_for_Image_Inpainting_CVPR_2025_paper.pdf">RAD: Region-Aware Diffusion Models for Image Inpainting</a></p> ]]></content:encoded><category>图像消除</category><pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/11/ImageEraser1.html</guid></item><item><title>深度学习学习率优化方法——pytorch中各类warm up策略</title><link>https://www.big-yellow-j.top/posts/2025/06/05/WarmUP-Torch.html</link><description><![CDATA[本文介绍如何在PyTorch中使用warm-up策略，涵盖优化器与学习率调度器的应用方法。PyTorch优化器基础类torch.optim.Optimizer支持参数配置，如params可针对不同网络结构（如model.fc1、model.fc2）设置不同学习率（lr）、权重衰减（weight_decay）等，常见优化器如AdamW即继承此类。学习率优化可通过torch.optim.lr_scheduler实现，其方法多基于epoch调整，主要分为三类：基于步数的调度（如CyclicLR、OneCycleLR，需在每个batch后调用scheduler.step()）、基于指标的调度（如...]]></description><content:encoded><![CDATA[<p>warm-up具体原理以及为什么这么做在之前的博客有介绍，这里直接介绍如何直接使用pytorch中的warm-up策略，在pytorch中对于warm-up所有支持的方法都有描述，可以直接阅读<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。<br/><a href="https://www.big-yellow-j.top/posts/2025/01/04/learning_rate.html">深度学习中各类学习率优化方法(AdaGrad/RMSprop/Adam/Warm-UP)原理及其代码</a></p><h2 id="前言">前言</h2><p>在pytorch中，选择优化器（<code>torch.optim</code>）一般在使用过程中直接通过这个去定义我们所需要的优化器，如adam等，因此对于其<strong>基础类</strong>（<code>torch.optim.Optimizer(params, defaults)</code>）一般就是直接输入模型的参数，而后可以直接通过这个类去做一些基本操作，如<code>Optimizer.load_state_dict</code> 加载优化器状态等。直接通过Adamw来解释（都是直接继承这个基础类的）</p><pre><code class="language-python">class AdamW(Optimizer):
    def __init__(
        self,
        params: ParamsT,
        lr: Union[float, Tensor] = 1e-3,
        betas: Tuple[float, float] = (0.9, 0.999),
        eps: float = 1e-8,
        weight_decay: float = 1e-2,
        amsgrad: bool = False,
        *,
        maximize: bool = False,
        foreach: Optional[bool] = None,
        capturable: bool = False,
        differentiable: bool = False,
        fused: Optional[bool] = None,
    ):
</code></pre><p>其中不同变量含义为：<br/> 1、params：一般就是网络结构的优化参数</p><blockquote><p>对于这个参数可以多了解一些，一般使用过程中都是直接 <code>model.parameters()</code>，但是有些时候，比如模型结构复杂可能<strong>对于不同的网络结构选择不同的学习率等</strong>，可以直接 <code>optimizer = torch.optim.AdamW([{'params': model.fc1.parameters(), 'lr': 1e-3},{'params': model.fc2.parameters(), 'lr': 1e-4, 'weight_decay': 0.01},],lr= 1e-5)</code></p></blockquote><p>2、lr：学习率；3、betas：是一阶和二阶矩估计的指数衰减率；4、eps：为了防止除以 0 而加在分母中的一个非常小的数（epsilon）；5、weight_decay：权重衰减系数（L2 正则化强度）<br/> 了解其基本优化算法之后在 <code>torch.optim</code> 也有另外一个“大杀器”：<code>torch.optim.lr_scheduler</code> 直接去对学习率进行优化</p><h2 id="学习率优化器">学习率优化器</h2><p>上面已经介绍了 <code>torch.optim.lr_scheduler</code>这里直接进一步解释其各类方法以及代码实战。其中在 <code>torch.optim.lr_scheduler</code> 中大部分调整学习率的方法都是根据epoch训练次数。直接总结如下所示：<br/><img src="https://s2.loli.net/2025/06/21/ZR5JkIStjF7TsO1.webp" alt="image" width="1283" height="1291" loading="lazy" decoding="async"/></p><p>对于不同的优化器实际测试代码见文件：<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/blob/master/code/Python/learning_rate/learning_rate.ipynb">learning_rate.ipynb</a>。于此同时不同学习率优化得到的学习率变化曲线为：<br/><img src="https://s2.loli.net/2025/06/21/OTmDUKPCgush2Rw.webp" alt="image" width="1500" height="1000" loading="lazy" decoding="async"/></p><p>值得注意的是，在使用<code>lr_scheduler</code>过程中，一般使用套路为：</p><pre><code class="language-python"># 定义学习率变化方式
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max= (page_layout_config.cos_warmup_t_max* len(train_dataloader)),eta_min= page_layout_config.cos_warmup_eta_min)
...
# 如果使用huggingface的并行训练框架
lr_scheduler = accelerator.prepare(lr_scheduler)
out = model(x)
loss = loss_function(out, label)
accelerator.backward(loss)
if accelerator.sync_gradients:
    accelerator.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
lr_scheduler.step() # 更新策略是由讲究的，见下面括号中的描述
optimizer.zero_grad()
# 如果不使用框架
out = model(x)
loss = loss_function(out, label)
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
</code></pre><p>要获取学习率的当前值，可使用 scheduler.get_last_lr()[0]。不同调度器的更新方式存在差异，具体如下：<strong>1、基于步数的调度</strong>（<em>再每一次dataloader处理之后都要进行更新</em>）：CyclicLR 和 OneCycleLR 直接根据训练步数（step）更新学习率，适合在每个 batch 后调用 scheduler.step()。<strong>2、基于指标的调度</strong>：ReduceLROnPlateau 根据验证指标（如损失或准确率）动态调整学习率，需在 scheduler.step(metrics) 中传入指标值。<strong>3、基于 epoch 的调度</strong>（<em>在结束一个epoch后再去更新学习率</em>）：其他调度器（如 CosineAnnealingLR）通常基于 epoch 调整学习率。例如，CosineAnnealingLR(optimizer, T_max=10, eta_min=0) 以 10 个 epoch 为一个周期进行余弦退火调整。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://docs.pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Thu, 05 Jun 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/06/05/WarmUP-Torch.html</guid></item><item><title>多模态算法Clip、Albef、Blip等算法原理</title><link>https://www.big-yellow-j.top/posts/2025/05/28/MultiModal.html</link><description><![CDATA[视觉多模态模型（如CLIP、ALBEF、BLIPv1/v2）核心挑战在于模态信息结合，通常采用Vit/Resnet等视觉编码器与文本编码器处理图像和文本，通过对比学习（如InfoNCE损失）实现跨模态对齐。CLIP侧重学习跨模态相似度表示，具备零样本能力；ALBEF和BLIP系列通过模态对齐（ITC）、图文匹配（ITM）等任务优化，BLIPv2更引入Q-Former将图像特征映射至LLM空间，结合冻结LLM提升生成与零样本性能，适用于图文检索、分类等多任务。]]></description><content:encoded><![CDATA[<p>视觉多模态模型在结构上比较统一，一个视觉编码器（较多使用的是Vit/Resnet等）对图像信息进行处理，然后将其和文本信息一起结合然后输入到LLM模型中得到最后的结果，因此在此过程中一个最大的挑战就是：<strong>如果将不同模态信息进行结合</strong>（当然有些可能还需要考虑如何将图像进行压缩，这里主要是考虑有些图像的分辨率比较高）。</p><h2 id="clip">Clip</h2><p><img src="https://s2.loli.net/2025/06/22/H6kEoxgzYAWNhXp.webp" alt="image" width="1055" height="384" loading="lazy" decoding="async"/></p><p>代表模型 <a href="https://arxiv.org/pdf/2103.00020"><strong>CLIP</strong></a><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>，更加像一种 <strong>图像-文本</strong>对齐模型，按照论文里面他自己提到的计算范式：</p><pre><code class="language-python"># image_encoder - ResNet or Vision Transformer
# text_encoder - CBOW or Text Transformer
# I[n, h, w, c] - minibatch of aligned images
# T[n, l] - minibatch of aligned texts
# W_i[d_i, d_e] - learned proj of image to embed
# W_t[d_t, d_e] - learned proj of text to embed
# t - learned temperature parameter
# extract feature representations of each modality
I_f = image_encoder(I) #[n, d_i]
T_f = text_encoder(T) #[n, d_t]
# joint multimodal embedding [n, d_e]
I_e = l2_normalize(np.dot(I_f, W_i), axis=1)
T_e = l2_normalize(np.dot(T_f, W_t), axis=1)
# scaled pairwise cosine similarities [n, n]
logits = np.dot(I_e, T_e.T) * np.exp(t)
# symmetric loss function
labels = np.arange(n)
loss_i = cross_entropy_loss(logits, labels, axis=0)
loss_t = cross_entropy_loss(logits, labels, axis=1)
loss = (loss_i + loss_t)/2
</code></pre><p>在将 Image 和 Text 编码完成之后，直接计算它们之间的<strong>相似度</strong>，实现模态之间的对齐。优化过程的目标是让匹配的图文对的相似度尽可能大，同时让不匹配对的相似度尽可能小。换言之，CLIP 的对比学习机制本质上是在学习一种跨模态的相似度表示。其核心机制是通过对比学习和嵌入空间对齐，将图像和文本映射到一个共享的语义空间中。<br/> 尽管 CLIP 本身并不直接包含复杂的推理能力或任务特定的知识，但它通过大规模预训练，展现出了强大的通用性和零样本学习能力。在论文中，CLIP 表现出了不俗的零样本性能，但需要注意的是，CLIP 的主要目标是学习跨模态的对齐表示，这使得它能够胜任多种任务（如图文检索、零样本分类等）。相比于传统的目标识别模型，CLIP 更像是一个多模态的基础模型，具备更广泛的适用性和灵活性。</p><h2 id="albef">ALBEF</h2><p>Albef<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>模型基本结构如下：<br/><img src="https://s2.loli.net/2025/09/21/CedgchRjau5lK24.webp" alt="image" width="1117" height="523" loading="lazy" decoding="async"/><br/><strong>模型结构</strong>：1、图像编码器（12层的Vit-B/16）；2、文本编码器（6层 $\text{BERT}<em>{\text{base}}$ ）；3、多模态编码器（6层 $\text{BERT}</em>{\text{base}}$）。对于文本和图像都会编码为带前缀的向量，图像：${v_{cls},v_1,…,v_N}$，文本：${w_{cls},w_1,…,w_N}$。<br/><strong>训练过程</strong>：<strong>1、模态对齐（ITC）</strong>：这个过程主要是计算image-to-text以及text-to-image相似性计算过程如下：<br/><img src="https://s2.loli.net/2025/09/21/6OkhSUmVQMXIbGp.webp" alt="image" width="894" height="161" loading="lazy" decoding="async"/><br/> 其中 $s(I, T_m)=g_v(v_{cls})^Tg^′(w^′_{cls})$，相似性计算公式中 $g$主要是将 <code>[CLS]</code>通过线性处理处理到256维，而 $g^′$则是通过动量编码器的规范化特征表示。$y$代表GT。</p><blockquote><p>对于这个loss计算过程再Albef中会改写为：<br/><img src="https://s2.loli.net/2025/09/21/LNG46cOprqUoilj.webp" alt="image" width="891" height="92" loading="lazy" decoding="async"/><br/> 其中$s$代表score function（比如说直接计算点乘），$\tau$温度稀疏</p></blockquote><p><strong>2、遮蔽语言模型( MLM )</strong>：直接预测被MASK掉的词；<strong>3、图文匹配（ITM）</strong>：主要是判断图文之间匹配，对于这两个过程数据处理为：<br/><img src="https://s2.loli.net/2025/09/21/mTBZDYL1xqjVnWC.webp" alt="image" width="1057" height="326" loading="lazy" decoding="async"/></p><h2 id="blip">BLIP</h2><h3 id="blipv1">BLIPv1</h3><p>BLIP-1<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>模型结构如下<br/><img src="https://s2.loli.net/2025/09/21/nYSQTUMJNKV9iCe.webp" alt="image" width="809" height="328" loading="lazy" decoding="async"/><br/> 对于模型使用对于<strong>视觉编码器直接使用Vit</strong>，对于<strong>文本编码器直接使用BERT</strong>，不过值得注意的是和Albef中处理相同的是在特征前面都会选择添加一个<code>[CLS]</code>标记然后其他结构集合上面的一致。在模型结构上主要分为3块：1、Text Encder；2、Image grounded Text encoder；3、Image-grouned Text decoder；对于这3块都分别对应的去计算ITC、ITM以及LM3个损失，其中前两个和Albef中计算方式相同。除此之外虽然设计了3个模块但是模块之间参数是共享的（<strong>颜色相同那么参数就是相同的</strong>）<br/><img src="https://s2.loli.net/2025/09/21/skyam6uohLHdSQB.webp" alt="image" width="1311" height="431" loading="lazy" decoding="async"/><br/> 论文中数据合成方法，其实还是基于BLIP自身的encoder-decoder结构，首先是通过标注的数据（$I_h,T_h$）进行训练模型在得到很好的效果之后，将未标注的图片 $I_w$直接输入到模型中生成图-文对（$I_w,T_s$）以及从网络上搜索得到的图-文对（$I_w,T_w$）此时这两部分图文对不是很“恰当的”通过filter去过滤掉不合适的配对这样一来最后就可以得到相对干净的图-文对。</p><blockquote><p>其中<code>filter</code>设计就是直接使用 image-ground text encoder通过直接微调来让模型知道 图-文匹配效果</p></blockquote><h3 id="blipv2">BLIPv2</h3><p><strong>BLIP-2</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>模型结构如下：<br/><img src="https://s2.loli.net/2025/09/21/aUS4rPxLlqZvwJY.webp" alt="image" width="769" height="355" loading="lazy" decoding="async"/><br/> 在 BLIP-2中<strong>同时冻结了Image Encoder以及LLM</strong>因此为了弥补不同模态之间的差异，就需要设计一个“模块”来进行表示（在论文中做法是：通过设计一个<a href="https://github.com/salesforce/LAVIS/blob/main/lavis/models/blip2_models/blip2_qformer.py">Q-Former</a><strong>将Image/Text上的信息都”反映“到一个Learned-Queries上</strong>）。</p><blockquote><p><strong>Q-Former</strong>通过初始化的query然后将图片和文本特征都反映到query上，其结构就是直接使用BERT作为主体结构，通过改变BERT的输入数据来保证对于图片和文本的特征“反映”</p></blockquote><p>具体操作分为两个阶段：<br/><img src="https://s2.loli.net/2025/09/21/7DVIGxdnXLPY6KB.webp" alt="image" width="967" height="641" loading="lazy" decoding="async"/><br/><strong>第一阶段</strong>：结构图如上所述通过冻结image-encoder，模型对于输出首先进行处理<a href="https://github.com/salesforce/LAVIS/blob/506965b9c4a18c1e565bd32acaccabe0198433f7/lavis/models/blip2_models/blip2_qformer.py#L91C9-L127C10">过程</a>：</p><pre><code class="language-python">image_embeds = self.ln_vision(self.visual_encoder(image))
query_tokens = self.query_tokens.expand(image_embeds.shape[0], -1, -1)
query_output = self.Qformer.bert(query_embeds=query_tokens,encoder_hidden_states=image_embeds,encoder_attention_mask=image_atts,...)
image_feats = F.normalize(self.vision_proj(query_output.last_hidden_state), dim=-1)
text_tokens = self.tokenizer(text,...)
text_output = self.Qformer.bert(text_tokens.input_ids,...)
</code></pre><p>对于<code>self.query_tokens</code>初始化直接通过<a href="https://github.com/salesforce/LAVIS/blob/506965b9c4a18c1e565bd32acaccabe0198433f7/lavis/models/blip2_models/blip2.py#L57">生成全0的向量</a>。除此之外对于初始化后的 <code>query_tokens</code>之后会通过 <code>self.Qformer.bert</code>（<strong>Qformer采用的还是BERT结构</strong>，因此所有的上面结构图中涉及到的各种attention mask操作也都是再bert中计算只是通过参数：<code>attention_mask</code>控制）将其核图像特征进行“交互”最后得到 <code>image_feats</code>，而对于文本处理过程就比较简单直接tokenizer处理之后再去有bert编码即可得到<code>text_feat</code><br/> 在得到3部分输入之后再Qformer中而后进行3个训练任务：<br/><strong>1、图片对比损失ITC</strong>（<a href="https://github.com/salesforce/LAVIS/blob/506965b9c4a18c1e565bd32acaccabe0198433f7/lavis/models/blip2_models/blip2_qformer.py#L129C9-L174C1">代码</a>）：</p><pre><code class="language-python">sim_q2t = torch.matmul(image_feats.unsqueeze(1), text_feat_all.unsqueeze(-1)).squeeze()
sim_i2t, _ = sim_q2t.max(-1)
sim_i2t = sim_i2t / self.temp
sim_t2q = torch.matmul(text_feat.unsqueeze(1).unsqueeze(1), image_feats_all.permute(0, 2, 1)).squeeze()
sim_t2i, _ = sim_t2q.max(-1)
sim_t2i = sim_t2i / self.temp
...
loss_itc = (
  F.cross_entropy(sim_i2t, targets, label_smoothing=0.1)+ 
  F.cross_entropy(sim_t2i, targets, label_smoothing=0.1)) / 2
</code></pre><p>对于ITC中计算 <strong>InfoNCE</strong><br/><strong>2、图片文本配对ITM</strong>（<a href="https://github.com/salesforce/LAVIS/blob/506965b9c4a18c1e565bd32acaccabe0198433f7/lavis/models/blip2_models/blip2_qformer.py#L176C9-L247C55">代码</a>）：这个过程首先再ITC中会得到 <code>sim_t2i</code> 和 <code>sim_i2t</code>这两个矩阵（分别代表图片文本相似度矩阵），这样一来就可以直接更具这个相似度矩阵去不匹配的图文对和文图对。最终，<strong>作为正样本的图文对就是原始输入的图文对，而作为负样本的图文对和文图对就是通过相似矩阵采样出来的</strong>。</p><pre><code class="language-python">image_embeds_all = torch.cat([image_embeds, image_embeds_neg, image_embeds], dim=0)  # pos, neg, pos
image_atts_all = torch.ones(image_embeds_all.size()[:-1], dtype=torch.long).to(image.device)
...
text_ids_all = torch.cat([text_tokens.input_ids, text_tokens.input_ids, text_ids_neg], dim=0)  # pos, pos, neg
query_tokens_itm = self.query_tokens.expand(text_ids_all.shape[0], -1, -1)
...
output_itm = self.Qformer.bert(
  text_ids_all,
  query_embeds=query_tokens_itm,
  attention_mask=attention_mask_all,
  encoder_hidden_states=image_embeds_all,
  encoder_attention_mask=image_atts_all,...)

vl_embeddings = output_itm.last_hidden_state[:, : query_tokens_itm.size(1), :]
vl_output = self.itm_head(vl_embeddings)
logits = vl_output.mean(dim=1)

itm_labels = torch.cat([torch.ones(bs, dtype=torch.long), torch.zeros(2 * bs, dtype=torch.long)],dim=0,).to(image.device)
loss_itm = F.cross_entropy(logits, itm_labels)
</code></pre><p><strong>3、图片生成文本ITG</strong>：</p><pre><code class="language-python">lm_output = self.Qformer(
            decoder_input_ids,
            attention_mask=attention_mask,
            past_key_values=query_output.past_key_values,
            return_dict=True,
            labels=labels,
        )

loss_lm = lm_output.loss
</code></pre><p>通过上面三个任务，训练好的query tokens和Q-Former就能够将image encoder提取的原始图像特征和文本特征进行拉近。<strong>理论上，这个阶段的模型，就是一个训练完成的图文多模态模型。该模型能够完成图文retrieval、图文匹配、图生文的任务</strong><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>。为了进一步利用LLMs的生成能力和zero-shot能力，训练进入第二阶段。<br/><strong>第二阶段</strong><br/><img src="https://s2.loli.net/2025/09/21/zSHKfXWYNxywItZ.webp" alt="image" width="1448" height="449" loading="lazy" decoding="async"/><br/> LLMs是一个生成式模型，整个流程是：冻结的Image Encoder生成原始的图像特征，而query tokens和Q-Former从原始图像特征中生成转化好的图像特征，然后该图像特征经过全连接层映射到LLMs的文本embedding空间中。然后这些映射后的图像特征，就相当于视觉prompts，和文本embedding一起，输入到冻结的LLMs中，最后生成目标文本。</p><h2 id="总结">总结</h2><p>上面提到几个模型Clip、Albef、Blipv1、Blipv2首先再文本以及图片编码上差异不大，特征对齐上也都是选择 <strong>对比学习方式</strong>去对齐图片和文本之间的模态信息，后面3个模型在模态对齐上选择计算方式都是 <strong>InfoNCE</strong><br/> \(\mathcal{L}_{\text{itc}} = -\frac{1}{2} \mathbb{E}_{p(I,T)} \left[ \log \frac{\exp(s(I,T)/\tau)}{\sum_{m=1}^M \exp(s(I,T_m)/\tau)} + \log \frac{\exp(s(T,I)/\tau)}{\sum_{m=1}^M \exp(s(T,I_m)/\tau)} \right]\)<br/> 不过在Blipv2中是将文本，图片信息都反映到一个初始化的query上。</p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:2" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2103.00020">https://arxiv.org/pdf/2103.00020</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2107.07651">https://arxiv.org/pdf/2107.07651</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2201.12086">https://arxiv.org/pdf/2201.12086</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://arxiv.org/pdf/2301.12597">https://arxiv.org/pdf/2301.12597</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="https://zhuanlan.zhihu.com/p/664601983">https://zhuanlan.zhihu.com/p/664601983</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Wed, 28 May 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/05/28/MultiModal.html</guid></item><item><title>深入浅出了解生成模型-3：Diffusion模型原理以及代码</title><link>https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html</link><description><![CDATA[Diffusion Model（DF）通过前向加噪与反向去噪过程实现生成，前向过程逐步向图像添加噪声（由噪声调度参数控制权重，随时间依赖使图像从清晰变模糊）直至接近标准正态分布，且可一步计算从初始图像到最终噪声状态；反向过程则从标准正态分布逐步去噪还原图像，通过神经网络近似难以直接计算的反向分布，方差可固定或由网络预测（如OpenAI的Improved DDPM采用预测系数方法）。Conditional Diffusion Model通过嵌入文本、标签等条件指导生成，主要方式包括将编码后的条件与噪声/时间步相加的直接相加范式，以及融入注意力机制的注意力融合范式（如Stable Diffu...]]></description><content:encoded><![CDATA[<p>前文已经介绍了VAE以及GAN这里介绍另外一个模型：Diffusion Model，除此之外介绍Conditional diffusion model、Latent diffusion model</p><h2 id="diffusion-model">Diffusion Model</h2><p>diffusion model（后续简称df）模型原理很简单：<em>前向过程</em>在一张图像基础上不断添加噪声得到一张新的图片之后，<em>反向过程</em>从这张被添加了很多噪声的图像中将其还原出来。原理很简单，下面直接介绍其数学原理：<br/><img src="https://s2.loli.net/2025/06/21/7jOQ3YgHxMwouzG.webp" alt="https://arxiv.org/pdf/2208.11970" width="854" height="343" loading="lazy" decoding="async"/></p><blockquote><p>上图中实线代表：反向过程（去噪）；虚线代表：前向过程（加噪）</p></blockquote><p>那么我们假设最开始的图像为 $x_0$通过不断添加噪声（添加噪声过程假设为$t$）那么我们的 <strong>前向过程</strong>：$q(x_1,…,x_T\vert x_0)=q(x_0)\prod_{t=1}^T q(x_t\vert x_{t-1})$，同理 <strong>反向过程</strong>：$p_\theta(x_0,…\vert x_{T})=p(x_T)\prod_{t=1}^Tp_\theta(x_{t-1}\vert x_t)$</p><h3 id="前向过程">前向过程</h3><p>在df的前向过程中：</p> \[q(x_1,...,x_T\vert x_0)=q(x_0)\prod_{t=1}^T q(x_t\vert x_{t-1})\] <p>通常定义如下的高斯分布：$q(x_t\vert x_{t-1})=N(x_t;\sqrt{1-\beta_t}x_{t-1},\beta_tI)$，其中参数$\beta$就是我们的 <strong>噪声调度</strong>参数来控制我们每一步所添加的噪声的“权重”（这个权重可以固定也可以时间依赖，对于时间依赖很好理解最开始图像是“清晰”的在不断加噪声过程中图像变得越来越模糊），于此同时随着不断的添加噪声那么数据$x_0$就会逐渐的接近标准正态分布 $N(0,I)$的 $x_t$，整个加噪过程就为：</p> \[\begin{align*} t=1 \quad &amp; x_1 = \sqrt{1 - \beta_1} x_0 + \sqrt{\beta_1} \epsilon_1 \\ t=2 \quad &amp; x_2 = \sqrt{1 - \beta_2} x_1 + \sqrt{\beta_2} \epsilon_2 \\ &amp;\vdots \\ t=T \quad &amp; x_T = \sqrt{1 - \beta_T} x_{T-1} + \sqrt{\beta_T} \epsilon_T \end{align*}\] <p>在上述过程中我们可以将$t=1$得到的 $x_1$代到下面 $t=2$的公式中，类似的我们就可以得到下面的结果：$x_2=\sqrt{(1-\beta_2)(1-\beta_1)}x_0+ \sqrt{1-(1-\beta_2)(1-\beta_1)}\epsilon$ （之所以用一个$\epsilon$是因为上面两个都是服从相同高斯分布就可以直接等同过来）那么依次类推就可以得到下面结果：</p> \[\begin{align*} x_T=\sqrt{(1-\beta_1)\dots(1-\beta_T)}x_0+ \sqrt{1-(1-\beta_1)\dots(1-\beta_T)}\epsilon \\ \Rightarrow x_T=\sqrt{\bar{\alpha_T}}x_0+ \sqrt{1-\bar{\alpha_T}}\epsilon \end{align*}\] <p>其中：$\bar{\alpha_T}=\sqrt{(1-\beta_1)\dots(1-\beta_T)}$，那么也就是说对于前向过程（加噪过程）可以从$x_0$到 $x_T$一步到位，不需要说再去逐步计算中间状态了。</p><h3 id="反向过程">反向过程</h3><p><strong>反向过程</strong>：$p_\theta(x_0,…\vert x_{T})=p(x_T)\prod_{t=1}^Tp_\theta(x_{t-1}\vert x_t)$，也就是从最开始的标准正态分布的 $x_t$逐步去除噪声最后还原得到 $x_0$。仔细阅读上面提到的前向和反向过程中都是条件概率但是在反向传播过程中会使用一个参数$\theta$，这是因为前向过程最开始的图像和噪声我们是都知道的，而反向过程比如$p(x_{t-1}\vert x_t)$是难以直接计算的，需要知道整个数据分布，因此我们可以通过神经网路去近似这个分布，而这个神经网络就是我们的参数：$\theta$。于此同时反向过程也会建模为正态分布：$p_\theta(x_{t-1}\vert x_t)=N(x_{t-1};\mu_\theta(x_t,t),\sum_\theta(x_t,t))$，其中 $\sum_\theta(x_t,t)$为我们的方差对于在值可以固定也可以采用网络预测<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p><blockquote><p>在OpenAI的Improved DDPM中使用的就是使用预测的方法：$\sum_\theta(x_t,t)=\exp(v\log\beta_t+(1-v)\hat{\beta_t})$，直接去预测系数：$v$</p></blockquote><p>回顾一下生成模型都在做什么。在<a href="./2025-05-08-GAN.md">GAN</a>中是通过 <em>生成器网络</em> 来拟合正式的数据分布也就是是 $G_\theta(x)≈P(x)$，在 <a href="./2025-05-11-VAE.md">VAE</a>中则是通过将原始的数据分布通过一个 低纬的<strong>潜在空间</strong>来表示其优化的目标也就是让 $p_\theta(x)≈p(x)$，而在Diffusion Model中则是直接通过让我们 去噪过程得到结果 和 加噪过程结果接近，什么意思呢？df就像是一个无监督学习我所有的GT都是知道的（每一步结果我都知道）也就是是让：$p_\theta(x_{t-1}\vert x_t)≈p(x_{t-1}\vert x_t)$ 换句话说就是让我们最后解码得到的数据分布和正式的数据分布相似：$p_\theta(x_0)≈p(x_0)$ 既然如此知道我们需要优化的目标之后下一步就是直接构建损失函数然后去优化即可。</p><h3 id="优化过程">优化过程</h3><p>通过上面分析，发现df模型的优化目标和VAE的优化目标很相似，其损失函数也是相似的，首先我们的优化目标是最大化下面的边际对数似然<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">2</a></sup>：$\log p_\theta(x_0)=\log \int_{x_{1:T}}p_\theta(x_0,x_{1:T})dx_{1:T}$，对于这个积分计算是比较困难的，因此引入：$q(x_{1:T}\vert x_0)$ 那么对于这个公式有：</p> \[\begin{align*} \log p_\theta(x_0)&amp;=\log \int_{x_{1:T}}p_\theta(x_{0:T})dx_{1:T} \\ &amp;=\log \int_{x_{1:T}} q(x_{1:T}\vert x_0)\frac{p_\theta(x_{0:T})}{q(x_{1:T}\vert x_0)}dx_{1:T}\\ &amp;=\log\mathbb{E}_{q(x_{1:T|x_0})}[\frac{p_\theta(x_{0:T})}{q(x_{1:T}\vert x_0)}]\\ &amp;≥\mathbb{E}_{q(x_{1:T|x_0})}[\log \frac{p_\theta(x_{0:T})}{q(x_{1:T}\vert x_0)}]\\ &amp;=\underbrace{\mathbb{E}_{q(\boldsymbol{x}_1|\boldsymbol{x}_0)}[\log p_\theta(\boldsymbol{x}_0|\boldsymbol{x}_1)]}_{\text{reconstruction term}} - \underbrace{\mathbb{E}_{q(\boldsymbol{x}_{T-1}|\boldsymbol{x}_0)}[D_{\text{KL}}(q(\boldsymbol{x}_T|\boldsymbol{x}_{T-1})\parallel p(\boldsymbol{x}_T))]}_{\text{prior matching term}} - \sum_{t=1}^{T-1} \underbrace{\mathbb{E}_{q(\boldsymbol{x}_{t-1},\boldsymbol{x}_{t+1}|\boldsymbol{x}_0)}[D_{\text{KL}}(q(\boldsymbol{x}_t|\boldsymbol{x}_{t-1})\parallel p_\theta(\boldsymbol{x}_t|\boldsymbol{x}_{t+1})]}_{\text{consistency term}}\\ &amp;=\underbrace{\mathbb{E}_{q(\boldsymbol{x}_1|\boldsymbol{x}_0)}[\log p_\theta(\boldsymbol{x}_0|\boldsymbol{x}_1)]}_{\text{reconstruction term}} - \underbrace{D_{KL}(q(x_T|x_0)||p(x_T))}_{\text{prior matching term}} - \sum_{t=2}^{T} \underbrace{\mathbb{E}_{q(\boldsymbol{x}_{t}|\boldsymbol{x}_0)}[D_{\text{KL}}(q(\boldsymbol{x}_{t-1}|\boldsymbol{x}_{t}, x_0)\parallel p_\theta(\boldsymbol{x}_{t-1}|\boldsymbol{x}_{t})]}_{\text{denoisiong matching term}} \end{align*}\] <p>中间化简步骤可以见论文<sup id="fnref:5:1" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">2</a></sup>中的描述（论文里面有两个推导，推导步骤直接省略，第二个等式： $q(x_t\vert x_{t-1})=q(x_t\vert x_{t-1},x_0)$），那么上面结果分析，在计算我们的参数$\theta$时候（反向传播求导计算）第2项直接为0，第1项可以直接通过蒙特卡洛模拟就行计算，那么整个结果就只有第三项，因此对于第二个等式为例可以将优化目标变为：$\text{arg}\min_\theta D_{KL}(q(x_{t-1}\vert x_t, x_0)\Vert p_\theta(x_{t-1}\vert x_t))$<br/> 对于这个优化目标根据论文<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">3</a></sup>可以得到：</p> \[L_{\mathrm{simple}}=\mathbb{E}_{t,\mathbf{x}_0,\epsilon}\left[\left\|\epsilon-\epsilon_\theta(\sqrt{\bar{\alpha}_t}\mathbf{x}_0+\sqrt{1-\bar{\alpha}_t}\epsilon,t)\right\|^2\right]\] <p>最终，训练目标是让神经网络 $\epsilon_\theta$ 准确预测前向过程中添加的噪声，从而实现高效的去噪生成,因此整个DF模型训练和<strong>采样过程</strong>就变为<sup id="fnref:6:1" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">3</a></sup>：</p><p><img src="https://s2.loli.net/2025/06/21/STVkbaQ65rhZLdU.webp" alt="image" width="915" height="232" loading="lazy" decoding="async"/></p><p>比如说下面一个例子：对于输入数据$x_0=[1,2]$ 于此同时假设我们的采样噪声 $\epsilon \in[0.5, -0.3]$并且进行500次加噪声处理，假设$\bar{\alpha}_{500} = 0.8$那么计算500次加噪得到结果为：</p> \[x_t=\sqrt{\bar{\alpha_t}}x_0+ \sqrt{1-\bar{\alpha_t}}\epsilon=\sqrt{0.8}\times[1,2]+\sqrt{0.2}[0.5, -0.3]≈[1.118,1.654]\] <p><strong>关键在于损失函数</strong>，通过上面简化过程可以直接通过模型预测噪声因此可以直接计算$\epsilon_\theta(x_t,t)=[0.48，-0.28]$然后去计算loss即可。<strong>直接上代码</strong>，代码实现上面过程可以自定义实现/使用<code>diffusers</code><sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">4</a></sup><br/><strong>diffusers</strong>实现简易demo</p><pre><code class="language-python">from diffusers import DDPMScheduler

# 直接加载训练好的调度器
# scheduler = DDPMScheduler.from_pretrained("google/ddpm-cat-256")
# 初始化调度器
scheduler = DDPMScheduler(num_train_timesteps=1000) #添加噪声步数
...
for image in train_dataloader:
    # 假设 image为 32，3，128，128
    noise = torch.randn(image.shape, device=image.device)
    timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, 
                                      (image.shape[0],), device=image.device, dtype=torch.int64)
    noisy_images = scheduler.add_noise(image, noise, timesteps) # 32 3 128 128
    ...
    noise_pred = model(noisy_images)
    loss = F.mse_loss(noise_pred, noise)
    ...

</code></pre><h2 id="conditional-diffusion-model">Conditional Diffusion Model</h2><p>条件扩散模型（Conditional Diffusion Model）<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">5</a></sup>顾名思义就是在使用DF过程中添加一个 <em>限定条件</em>（文本、图像等）来指导模型的生成（原理很简单，而且 <em>条件扩散模型</em>这个概念比较广泛，只要在生成图片过程中加上一个“条件”），这里主要介绍OpenAI的论文来解释 <em>条件扩散模型</em>。<br/><img src="https://s2.loli.net/2025/06/21/wWsalLZAjrkq719.webp" alt="image.png" width="989" height="459" loading="lazy" decoding="async"/></p><p>在论文里面提到了一点： <strong>可以通过文本来提升模型的生成质量</strong>。主要了解一下对于条件如何嵌入到模型中：<br/> 1、直接相加范式：这类主要就是将文本、标签进行编码之后直接和 <strong>噪声</strong>/ <strong>时间步</strong>进行相加而后进行后续实验；<br/> 2、注意力融合范式：比如下面的Stable Diffusion直接将文本编码之后融入到注意力里面进行计算</p><h2 id="latent-diffusion-model">Latent Diffusion Model</h2><p>对于Latent Diffusion Model（LDM）<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">6</a></sup>主要出发点就是：最开始的DF模型在像素空间（高纬）进行评估这是消耗计算的，因此LDF就是直接通过对 <strong>autoencoding model</strong>得到的 <em>潜在空间</em>（低维）进行建模。整个思路就比较简单，用降低维度的潜在空间来进行建模，整个模型结构为（<a href="#unet模型结构">代码操作</a>）：<br/><img src="https://s2.loli.net/2025/06/21/5eTfQwG6tLDpycv.webp" alt="image.png" width="810" height="386" loading="lazy" decoding="async"/></p><p>对于上述过程，输入图像为$x=[3,H,W]$而后通过encoder将其转化为 潜在空间（$z=\varepsilon(x)$）而后直接在潜在空间 $z$进行扩散处理得到$z_T$直接对这个$z_T$通过U-Net进行建模，整个过程比较简单。不过值得注意的是在U-Net里面因为可能实际使用DF时候会有一些特殊输入（文本、图像等）因此会对这些内容通过一个encoder进行编码得到：$\tau_\theta(y)\in R^{M\times d_\tau}$，而后直接进行注意力计算：</p> \[\text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d}})V\] <p>其中：$Q=W_{Q}^{(i)}\cdot\varphi_{i}(z_{t}),K=W_{K}^{(i)}\cdot\tau_{\theta}(y),V=W_{V}^{(i)}\cdot\tau_{\theta}(y)$并且各个参数维度为：$W_V^{i}\in R^{d\times d_\epsilon^i},W_Q^i\in R^{d\times d_\tau},W_k^i\in R^{d\times d_\tau}$</p><h2 id="df模型生成">DF模型生成</h2><h3 id="ddpm">DDPM</h3><p>最开始上面有介绍如何使用DF模型来进行生成，比如说在DDPM中生成范式为：<br/><img src="https://s2.loli.net/2025/06/21/STVkbaQ65rhZLdU.webp" alt="image" width="915" height="232" loading="lazy" decoding="async"/></p><p>也就是说DDPM生成为：</p> \[x_{t-1}=\frac{1}{\sqrt{\alpha_t}}\left(x_t-\frac{1- \alpha_t}{\sqrt{1-\bar{\alpha}_t}}\epsilon_\theta(x_t,t)\right)+\sigma_tz,\quad z\sim\mathcal{N}(0,I)\] <p>但是这种生成范式存在问题，比如说T=1000那就意味着一张“合格”图片就需要进行1000次去噪如果1次是为为0.1s那么总共时间大概是100s如果要生产1000张图片那就是：1000x1000x0.1/60≈27h。这样时间花销就会比较大</p><h3 id="ddim">DDIM</h3><p>最开始在介绍DDPM中将图像的采样过程定义为马尔科夫链过程，而DDIM<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">7</a></sup>则是相反直接定义为：非马尔科夫链过程<br/><img src="https://s2.loli.net/2025/06/21/pwIndituAKX4kjh.webp" alt="image" width="950" height="178" loading="lazy" decoding="async"/></p><p>并且定义图像生成过程为：</p> \[x_{t-1}=\sqrt{\alpha_{t-1}}\left(\frac{x_t-\sqrt{1-\alpha_t}\epsilon_\theta(x_t,t)}{\sqrt{\alpha_t}}\right)+\sqrt{1-\alpha_{t-1}-\sigma_t^2}\epsilon_\theta(x_t,t)+\sigma_tz\] <h2 id="代码操作">代码操作</h2><h3 id="df模型结构">DF模型结构</h3><blockquote><p>通过上面分析，知道对于 $x_T=\sqrt{\bar{\alpha_T}}x_0+ \sqrt{1-\bar{\alpha_T}}\epsilon$通过这个方式添加噪声，但是实际因为时间是一个标量，就像是最开始的位置编码一样，对于这些内容都会通过“类似位置编码”操作一样将其<strong>进行embedding处理然后在模型里面</strong>一般输入的参数也就是这三部分：<code>noise_image</code>, <code>time_step</code>, <code>class_label</code></p></blockquote><h4 id="dit模型">Dit模型</h4><blockquote><p>更加详细原理描述: <a href="https://www.big-yellow-j.top/posts/2025/07/06/DFBaseModel.html">深入浅出了解生成模型-6：常用基础模型与 Adapters等解析</a></p></blockquote><p>将Transformer使用到Diffusion Model中，而Dit<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">8</a></sup>在论文中进行的操作：通过一个autoencoder来将图像压缩为低维度的latent，扩散模型用来生成latent，然后再采用autoencoder来重建出图像，比如说在Dit中使用KL-f8对于输入图像维度为：256x256x3那么压缩得到的latent为32x32x4。Dit的模型结构为：<br/><img src="https://s2.loli.net/2025/06/21/d2MSPBkTt9aIlZi.webp" alt="image.png" width="1391" height="581" loading="lazy" decoding="async"/></p><p>模型输入参数3个分别为：1、低纬度的latent；2、标签label；3、时间步t。对于latent直接通过一个patch embed来得到不同的patch（得到一系列的token）而后将其和位置编码进行相加得到最后的embedding内容，直接结合代码<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">9</a></sup>来解释模型：<br/> 假设模型的输入为：</p><pre><code class="language-python">#Dit参数为：DiT(depth=12, hidden_size=384, patch_size=4, num_heads=6)
batch_size= 16
image = torch.randn(batch_size, 4, 32, 32).to(device)
t = torch.randint(0, 1000, (batch_size,)).to(device)
y = torch.randint(0, 1000, (batch_size,)).to(device)
</code></pre><p>那么对与输入分别都进行embedding处理：1、<strong>Latent Embedding：得到（8，64，384）</strong>，因为patchembedding直接就是假设我们的patch size为4那么每个patch大小为：4x4x4=64并且得到32/4* 32/4=64个patches，而后通过线linear处理将64映射为hidden_size=384；2、<strong>Time Embedding和Label Embedding：得到（8，384）（8，384）</strong>，因为对于t直接通过sin进行编码，对于label在论文里面提到使用 <em>classifier-free guidance</em>方式，具体操作就是在<strong>训练过程中</strong>通过<code>dropout_prob</code>来将输入标签<strong>随机</strong>替换为无标签来生成无标签的向量，在 <strong>推理过程</strong>可以通过 <code>force_drop_ids</code>来指定某些例子为无条件标签。将所有编码后的内容都通过补充位置编码信息（latent embedding直接加全是1，而label直接加time embedding），补充完位置编码之后就直接丢到 <code>DitBlock</code>中进行处理，对于<code>DitBlock</code>结构：</p><pre><code class="language-python">def forward(self, x, c):
    shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
    x = x + gate_msa.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_msa, scale_msa))
    x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
    return x
</code></pre><p>在这个代码中不是直接使用注意力而是使用通过一个 <code>modulate</code>这个为了实现将传统的layer norm（$\gamma{\frac{x- \mu}{\sigma}}+ \beta$）改为动态的$\text{scale}{\frac{x- \mu}{\sigma}}+ \text{shift}$，直接使用动态是为了允许模型根据时间步和类标签调整 Transformer 的行为，使生成过程更灵活和条件相关，除此之外将传统的残差连接改为 权重条件连接 $x+cf(x)$。再通过线性层进行处理类似的也是使用上面提到的正则化进行处理，处理之后结果通过<code>unpatchify</code>处理（将channels扩展2倍而后还原到最开始的输入状态）</p><h4 id="unet模型结构">Unet模型结构</h4><p><a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html#:~:text=%E6%A2%AF%E5%BA%A6%E6%B6%88%E5%A4%B1%E9%97%AE%E9%A2%98%E3%80%82-,2.Unet%E7%B3%BB%E5%88%97,-Unet%E4%B8%BB%E8%A6%81%E4%BB%8B%E7%BB%8D">Unet模型</a>在前面有介绍过了就是通过下采样和上采用并且同层级之间通过特征拼接来补齐不同采用过程之间的“信息”损失。如果直接使用stable diffusion model（<em>封装不多</em>），假设参数如下进行代码操作：</p><pre><code class="language-python">{
    'ch': 64,
    'out_ch': 3,
    'ch_mult': (1, 2, 4), # 通道增加倍数 in: 2,3,128,128 第一层卷积：2,64,128,128 通过这个参数直接结合 num_res_blocks来判断通道数量增加 ch_mut*num_res_blocks=(1, 1, 2, 2, 4, 4)
    'num_res_blocks': 2,  # 残差模块数量
    'attn_resolutions': (16,),
    'dropout': 0.1,
    'resamp_with_conv': True,
    'in_channels': 3,
    'resolution': 128,
    'use_timestep': True,
    'use_linear_attn': False,
    'attn_type': "vanilla"
}
</code></pre><blockquote><p>基本模块</p></blockquote><p><strong>1、残差模块</strong>：<br/><img src="https://s2.loli.net/2025/06/21/J6RDxyPk17CVIOo.webp" alt="image.png" width="1140" height="754" loading="lazy" decoding="async"/></p><p><strong>2、time embedding</strong>：直接使用attention的sin位置编码</p><blockquote><p>具体过程</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/ZAWCEJSKmMjVuvt.webp" alt="image.png" width="1001" height="730" loading="lazy" decoding="async"/></p><p>在得到的分辨率=attn_resolutions时候就会直接进行注意力计算（直接用卷积处理得到q，k，v然后进行计算attention），整个<a href="/Dio.drawio">结构</a>。如果这里直接使用<code>diffuser</code>里面的<a href="https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unets/unet_2d_blocks.py">UNet模型</a>进行解释（使用UNet2DModel模型解释），整个Unet模型就是3部分：1、下采样；2、中间层；3、上采样。假设模型参数为：</p><pre><code class="language-python">model = UNet2DModel(
    sample_size= 128,
    in_channels=3,
    out_channels=3,
    layers_per_block=2,
    block_out_channels=(128, 128, 256, 256, 512, 512),
    down_block_types=("DownBlock2D", "DownBlock2D", "DownBlock2D", "DownBlock2D", "DownBlock2D", "DownBlock2D"),
    up_block_types=("UpBlock2D", "UpBlock2D", "UpBlock2D", "UpBlock2D", "UpBlock2D", "UpBlock2D")
).to(device)
</code></pre><p>整个过程维度变化，假设输入为：image:(32,3,128,128), time_steps: (32, )：<br/><strong>首先通过第一层卷积</strong>：(32,128,128,128)与此同时会将时间步进行编码得到：(32, 512)（如果有label数据也是(32,)那么会将其加入到time_steps中）<br/><strong>下采样处理</strong>：总共6层下采样，得到结果为：<br/> Down-0: torch.Size([32, 128, 128, 128])<br/> Down-1: torch.Size([32, 128, 64, 64])<br/> Down-2: torch.Size([32, 256, 32, 32])<br/> Down-3: torch.Size([32, 256, 16, 16])<br/> Down-4: torch.Size([32, 512, 8, 8])<br/> Down-5: torch.Size([32, 512, 4, 4])<br/><strong>中间层处理</strong>：torch.Size([32, 512, 4, 4])<br/><strong>上采样处理</strong>：总共6层上采样，得到结果为：<br/> Up-0 torch.Size([32, 512, 8, 8])<br/> Up-1 torch.Size([32, 512, 16, 16])<br/> Up-2 torch.Size([32, 256, 32, 32])<br/> Up-3 torch.Size([32, 256, 64, 64])<br/> Up-4 torch.Size([32, 128, 128, 128])<br/> Up-5 torch.Size([32, 128, 128, 128])<br/><strong>输出</strong>：输出就直接通过groupnorm以及silu激活之后直接通过一层卷积进行处理得到：torch.Size([32, 128, 128, 128])</p><h3 id="df训练">DF训练</h3><blockquote><p><strong>生成具有随机性展示图像效果不能很好说明模型生成能力</strong></p></blockquote><h4 id="1ddpm">1、DDPM</h4><p>对于传统的DF训练（前向+反向）比较简单，直接通过输入图像而后不断添加噪声而后解噪。以huggingface<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">10</a></sup>上例子为例（<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/blob/master/code/Python/DFModelTraining/df_training.py">测试代码</a>），<strong>首先</strong>、对图像进行添加噪声。<strong>而后</strong>、直接去对添加噪声后的模型进行训练“去噪”（也就是预测图像中的噪声）。<strong>最后</strong>、计算loss反向传播。</p><blockquote><p>对于加噪声等过程可以直接借助 <code>diffusers</code>来进行处理，对于diffuser：<br/> 1、schedulers：调度器<br/> 主要实现功能：1、图片的前向过程添加噪声（也就是上面的$x_T=\sqrt{\bar{\alpha_T}}x_0+ \sqrt{1-\bar{\alpha_T}}\epsilon$）；2、图像的反向过程去噪；3、时间步管理等。如果不是用这个调度器也可以自己设计一个只需要：1、前向加噪过程（需要：使用固定的$\beta$还是变化的、加噪就比较简单直接进行矩阵计算）；2、采样策略</p></blockquote><p>测试得到结果为（因为HF官方提供了很好的参数去训练模型，因此测试新的数据集可能就没有那么效果好，只是做一个效果展示<strong>调参可能可以改善模型最后生成效果</strong>）：<br/><img src="https://cdn.z.wiki/autoupload/20250520/CHJj/1000X200/Generate-image.gif" alt=""/></p><blockquote><p><strong>Stable Diffusion Model</strong>代码测试。数据集：”saitsharipov/CelebA-HQ”</p></blockquote><p><img src="https://cdn.z.wiki/autoupload/20250523/jJT2/1000X200/SD-Generate-image-normal.gif" alt="SD-Generate-image-normal.gif"/></p><blockquote><p><strong>Hugging Face</strong>代码测试。数据集：”saitsharipov/CelebA-HQ”</p></blockquote><p><img src="https://cdn.z.wiki/autoupload/20250523/OZmN/1000X200/HF-Generate-image-normal.gif" alt="Generate-image-normal.gif"/></p><blockquote><p><strong>Dit</strong>代码测试。数据集：”saitsharipov/CelebA-HQ”</p></blockquote><p><img src="https://cdn.z.wiki/autoupload/20250525/Cex6/2000X200/Generate-image.gif" alt="image.gif"/></p><h2 id="总结">总结</h2><p>上面介绍了各类DF以及具体的代码操作，总的来说在DF训练过程中（从代码角度）基本上就是这个公式：$x_t=\sqrt{\bar{\alpha_t}}x_0+ \sqrt{1-\bar{\alpha_t}}\epsilon$ 加噪过程得到$x_t$/去噪过程通过$x_t$通过去预测 <strong>噪声</strong>来优化模型的参数。于此同时训练过程中发现：扩散模型（如DDPM）确实倾向于<strong>先学习图像的低频信息</strong>（大致轮廓），<strong>再逐步学习高频信息</strong>（细节），这是由于模型的去噪过程和损失函数设计，因此<strong>扩散模型需要大量迭代（通常数千到数十万步）才能生成高质量图像，尤其在细节上需要长时间优化</strong>。比如下面为实际迭代过程中生成.</p><blockquote><p>从左到右，从上到下，保存频率为每20个epoch保存一次，使用的<strong>模型为dit模型</strong></p></blockquote><p><img src="https://s2.loli.net/2025/06/22/kwny3f9MgRisxWU.webp" alt="image" width="1570" height="769" loading="lazy" decoding="async"/></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://arxiv.org/pdf/2102.09672 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p>https://arxiv.org/pdf/2208.11970 <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:5:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:6" role="doc-endnote"><p>https://arxiv.org/abs/2006.11239 <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:6:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:7" role="doc-endnote"><p>https://huggingface.co/docs/diffusers/en/index <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:11" role="doc-endnote"><p>https://arxiv.org/pdf/2204.06125 <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p>https://arxiv.org/abs/2112.10752 <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:10" role="doc-endnote"><p>https://arxiv.org/pdf/2010.02502 <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://arxiv.org/abs/2212.09748 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p>https://github.com/facebookresearch/DiT <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p>https://huggingface.co/docs/diffusers/en/tutorials/basic_training <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>生成模型</category><pubDate>Mon, 19 May 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html</guid></item><item><title>CV中常用Backbone-3：Clip/SAM原理以及代码操作</title><link>https://www.big-yellow-j.top/posts/2025/05/18/Clip-sam.html</link><description><![CDATA[本文介绍多模态领域常用的两种backbone——SAM和Clip的基本原理与应用。SAM包含v1和v2版本，SAM v1结构由Image Encoder、Prompt Encoder及Mask decoder组成：Image Encoder采用特定方法处理图像；Prompt Encoder分别处理text（CLIP编码）、points/bbox（傅里叶特征映射，提升高频函数学习能力）、mask（卷积编码）；Mask decoder基于Transformer解码器块修改，配备动态掩码预测头，可将图嵌入、提示嵌入映射为掩码，支持单个提示输出多个掩码（整体、部分、子部分）并预测置信分数。SAM...]]></description><content:encoded><![CDATA[<p>前面已经介绍了简单的视觉编码器，这里主要介绍多模态中使用比较多的两种backbone：1、Clip；2、SAM。对于这两个backbone简单介绍基本原理，主要是讨论使用这个backbone。<br/> 1、<a href="https://www.big-yellow-j.top/posts/2025/04/30/ConvNeXt.html">CV中常用Backbone-2：ConvNeXt模型详解</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html">CV中常用Backbone(Resnet/Unet/Vit系列/多模态系列等)以及代码</a></p><h2 id="sam">SAM</h2><p>SAM已经出了两个版本分别是：SAM v1和SAM v2这里对这两种分别进行解释，并且着重了解一下他的数据集是怎么构建的（毕竟很多论文里面都会提到直接用SAM作为一种数据集生成工具）</p><h3 id="sam-v1">SAM v1</h3><blockquote><p>https://arxiv.org/pdf/2304.02643<br/> 官方Blog：<a href="https://ai.meta.com/blog/segment-anything-foundation-model-image-segmentation/">Introducing Segment Anything: Working toward the first foundation model for image segmentation</a><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">1</a></sup></p></blockquote><p><img src="https://s2.loli.net/2025/06/22/ULVBFwPMfqDiTzH.webp" alt="image" width="1563" height="316" loading="lazy" decoding="async"/><br/> 结构<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>上还是比较简单，首先在 <strong>Image Encoder</strong>：选择的是<a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html#:~:text=768-,MAE%20%E4%B8%BB%E8%A6%81%E6%93%8D%E4%BD%9C%E6%B5%81%E7%A8%8B,-1%E3%80%81patch">MAE</a>；<strong>Prompt Encoder</strong>：从上面结构图很容易知道就3类prompt：1、text用CLIP进行编码；2、points、bbox使用论文<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">3</a></sup>（<strong>主要是通过傅里叶特征映射方法来提高模型对于高频函数学习能力</strong>）中的方法来进行编码处理；3、mask这种内容就直接使用卷积进行编码而后将结果和编码后图像相加；</p><blockquote><p>对于points以及bbox<a href="https://github.com/tancik/fourier-feature-networks?tab=readme-ov-file">编码</a>原理很简单就是用点或者坐标直接计算他们的傅里叶特征，比如说points的伪代码</p></blockquote><pre><code class="language-python">import numpy as np
# 假设输入点为2D，[x, y]
points = np.array([[0.5, 0.3], [0.2, 0.7]])  # 形状: (N, 2)
m = 256  # 映射维度
sigma = 10.0  # 频率控制参数

# 生成随机矩阵B
B = np.random.normal(0, sigma, size=(m, 2))  # 形状: (m, 2)
# 计算傅里叶特征
Bx = np.dot(points, B.T)  # 点积，形状: (N, m)
fourier_features = np.concatenate([np.cos(2 * np.pi * Bx), np.sin(2 * np.pi * Bx)], axis=1)  # 形状: (N, 2m)
</code></pre><p><strong>Mask decoder</strong>：掩码解码器可以有效的将图嵌入、提示嵌入和输出标记映射到掩码。本模型的解码器基于Transformer的解码器块修改，在解码器后添加了动态掩码预测头。解码器使用了提示自注意力和交叉注意力在提示到图嵌入（prompt-to-image embedding）和vice-versa两个方面进行了修改。完成这两个部分后，对图像进行上采样再使用MLP将输出标记映射到动态线性分类器上，最终得出每个图像位置的蒙板前景概率。<br/><img src="https://s2.loli.net/2025/06/22/8s2hPy1D4orpvYi.webp" alt="image" width="886" height="311" loading="lazy" decoding="async"/><br/><strong>Resolving ambiguity</strong>：对于一个不确定的提示，模型会给出多个有效掩码，经过修改SAM可以由单个提示预测输出多个掩码（一般是3个–整体、部分、子部分）。训练时，仅掩码进行反向传播。为了对掩码进行排名，模型会预测每个掩码的置信分数（使用IOU度量），所谓的整体、部分、子部分，比如说：<br/><img src="https://s2.loli.net/2025/06/22/EMaLQzBUm5sFtI1.webp" alt="image" width="531" height="586" loading="lazy" decoding="async"/></p><h3 id="sam-v2">SAM v2</h3><p>SAM v2<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">4</a></sup>更像是SAM v1在视频邻域的泛化通过引入Memory结构（Memory Attention/bank来记录之前视频帧的特征），整个模型结构如下所示：<br/><img src="https://s2.loli.net/2025/06/21/GECuoyeilnI2KHO.webp" alt="image" width="1121" height="293" loading="lazy" decoding="async"/><br/><strong>Memory Attention</strong>（<a href="https://github.com/facebookresearch/sam2/blob/main/sam2/modeling/memory_attention.py#L102">代码</a>）：将当前帧的特征与过去帧的特征和预测以及任何新的提示联系起来。通过堆叠了 L 个transformer模块（<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/modeling/memory_attention.py#L93">Attention处理代码</a>），每个transformer块首先执行自注意力（self-attention），这允许模型关注当前帧内部的特征。<br/> 接着执行交叉注意力（cross-attention），这允许模型将当前帧的特征与存储在记忆库中的记忆（包括提示和未提示的帧）以及对象指针进行交互。这些记忆可以是之前帧的特征，也可以是之前预测的结果。<br/><strong>memory encoder</strong>（<a href="https://github.com/facebookresearch/sam2/blob/main/sam2/modeling/memory_encoder.py">代码</a>）：通过使用卷积模块对输出掩码进行下采样，并将其与图像编码器的无条件帧嵌入相加，生成记忆，然后使用轻量级卷积层来融合信息。<br/><strong>memory bank</strong>：<strong>最多N个最近帧的FIFO记忆队列来保留视频中目标对象的过去预测信息，并将提示信息存储在最多M个提示帧的FIFO队列中</strong>。例如，在VOS任务中，初始掩码是唯一的提示，内存库始终保留第一帧的记忆以及最多N个最近(非提示)帧的记忆。两组记忆都以空间特征图的形式存储。<br/> 除空间存储器外，还根据每个帧的掩码解码器输出标记，将对象指针列表作为轻量级向量存储起来，用于存储要分割对象的高级语义信息。<br/> 我们<strong>将时间位置信息嵌入到N个最近帧的memory中</strong>，允许模型表示短期物体运动，但不包含到提示帧的记忆中，因为提示帧的训练信号更稀疏，并且更难以推广到推理设置中，提示帧可能来自与训练期间看到的时间范围非常不同的时间范围。</p><h2 id="clip">Clip</h2><p>Clip<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>模型结构（论文里面提到的）也比较简单，其核心机制为：<strong>核心机制是通过对比学习和嵌入空间对齐，将图像和文本映射到一个共享的语义空间中</strong><br/><img src="https://s2.loli.net/2025/06/21/vzix8HIcwAMDWdZ.webp" alt="image" width="1024" height="384" loading="lazy" decoding="async"/><br/><strong>预训练过程</strong>：直接将文本和图像都进行编码，而后将编码后的内容通过计算他的相似度（比如：cosine similarities）来确保模型最后能够对齐文本和图像之间的特征。<br/><strong>使用过程</strong>：对于给定的图像直接通过Clip的图像编码，而后将文本进行编码（文本编码中会有一个 label dataset通过从label dataset中抽取出标签和自己文本进行组合得到n条微博呢）再去计算最后的结果。</p><h2 id="代码操作">代码操作</h2><p>所有代码见：<a href="https://github.com/Big-Yellow-J/Big-Yellow-J.github.io/tree/master/code/Python/SAM-Clip/sam-clip.ipynb">sam-clip.ipynb</a></p><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:3" role="doc-endnote"><p>https://ai.meta.com/blog/segment-anything-foundation-model-image-segmentation/ <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p>https://arxiv.org/pdf/2304.02643 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p>https://arxiv.org/abs/2006.10739 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://arxiv.org/pdf/2408.00714 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p>https://arxiv.org/pdf/2103.00020 <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>Backbone</category><pubDate>Sun, 18 May 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/05/18/Clip-sam.html</guid></item><item><title>深入浅出了解生成模型-2：VAE模型原理以及代码实战</title><link>https://www.big-yellow-j.top/posts/2025/05/11/VAE.html</link><description><![CDATA[本文详细介绍变分自编码器（VAE）的基本原理、数学框架及代码实战，并对比自编码器（AE）与VAE的差异。AE作为无监督学习模型，通过编码-解码学习数据特征表示，但因潜在空间缺乏明确结构不适合生成任务；而VAE结合概率模型与深度学习，将输入映射为概率分布（如高斯分布），通过变分推理使潜在空间具备概率特性，更适合图像生成。VAE核心目标是学习数据概率分布p(x)，通过编码器（神经网络）用变分分布近似后验分布，解码器基于采样的潜在变量重构输入，实现新样本生成。其损失函数包含重构项（衡量生成数据与真实数据差异）和KL散度（优化近似分布与先验分布的相似度）。此外，文章还介绍VQ-VAE通过离散码本...]]></description><content:encoded><![CDATA[<p>前文已经介绍了<a href="https://www.big-yellow-j.top/posts/2025/05/08/GAN.html">GAN</a>的基本原理以及代码操作，本文主要介绍VAE其基本原理以及代码实战</p><h2 id="vae-or-ae">VAE or AE</h2><p>介绍VAE之前了解两个概念：AE（AutoEncoder，自编码器）和VAE（Variational Autoencoder，变自编码器）。<strong>AE</strong>：自编码器是一种无监督学习神经网络，旨在通过将输入数据压缩到一个低维表示（编码），然后从该表示重建输入数据（解码），来学习数据的特征表示。<strong>VAE</strong>：变分自编码器是自编码器的扩展，结合了概率模型和深度学习，通过引入变分推理使潜在空间具有概率分布特性，适合生成任务。<br/><strong>AE</strong>的数学描述对于输入 $x$通过编码器将输入映射到 <strong>低纬空间</strong> $z=f(x)$而后通过解码器得到输出：$\hat{x}=g(x)$<br/><strong>VAE</strong>的数学描述对于输入 $x$通过编码器将输入映射成 <strong>概率分布</strong> $q(z\vert x)$，假设为高斯分布，输出 𝜇和 𝜎，从 $q(z\vert x)$采样 $z$而后通过 $z=\mu+ \sigma+ \epsilon$ 其中 $\epsilon \in N(0,1)$，而后通过采样得到的$z$重新构建输入，生成$p(x\vert z)$<br/> 前者不适合对于图片进行生成而后者则是更加适合图像生成，这是因为AE将输入映射到一个低纬空间z这个低纬空间并没有明确的结构，进而就可能不适合去生成新的数据，而VAE之所以可以用于生成新的数据是，比如说对于图像数据（比如说：猫）如果知道其分布特征，就可以直接通过分布特征去构建一个新的图像</p><p><img src="https://s2.loli.net/2025/06/21/WHTJDpLjXBcyuhb.webp" alt="image" width="903" height="265" loading="lazy" decoding="async"/></p><h2 id="vaevariational-autoencoder">VAE（Variational Autoencoder）</h2><p>上面简单介绍了VAE数学描述这里重新再描述一下其数学描述（涉及到比较多贝叶斯统计相关内容）：</p><h3 id="1基本框架">1.基本框架</h3><p>VAE 是一种生成模型，<strong>目标是学习数据的概率分布 p(x)，让模型能生成类似真实数据的新样本</strong>，想象我们要制作各种蛋糕（数据 $x$），但不知道蛋糕的“秘方”（潜在变量 $z$）。假设所有蛋糕组成数据集 $X = {x_1, \dots, x_n}$，每种蛋糕（如巧克力蛋糕或水果蛋糕）背后有独特的秘方。VAE 通过学习秘方的分布和生成过程，制造出逼真的蛋糕。<br/><strong>秘方</strong>：VAE 假设秘方 $z$ 服从标准正态分布，即先验分布 $p(z) = \mathcal{N}(0, I)$。这意味着大多数秘方是“普通”的，围绕平均值分布。<br/><strong>生成蛋糕（解码器）</strong>：给定秘方 $z$，VAE 使用一个“蛋糕机”（解码器，参数 $\theta$）生成蛋糕 $x$。解码器建模条件分布 $p_\theta(x\vert z) = \mathcal{N}(x; \mu_\theta(z), \sigma_\theta^2(z))$，表示从 $z$ 生成 $x$ 的概率。<br/><strong>猜测秘方（编码器）</strong>：直接从蛋糕 $x$ 反推秘方（后验分布 $p_\theta(z\vert x)=\frac{p_\theta(x,z)}{p_\theta(x)}=\frac{p_\theta(x\vert )p(z)}{p_\theta(x)}$）很困难（因为我的变量是一个高维的，换句说法就是我的蛋糕他有千奇百怪种组合）。既然如此就只需要将制造蛋糕的组合分解，分解成低维的变量 $z$（也就是上面提到的 <strong>秘方</strong>）然后我去计算下面一个联合分布（$p(x,z)$）：</p> \[p_\theta(x) =\int p_\theta(x\vert z)p(z)dz\] <p>不过就算上面积分会存在困难即使你将蛋糕分解成不同的 <em>潜在变量</em> 但是这些潜在变量种类也是很多的（蛋糕奶油、加不加巧克力等等）那么上面的联合分布就会变成：</p> \[\int p_\theta(x\vert z)p(z)dz = \int_{z_1} ... \int_{z_d}p_\theta(x\vert z)p(z)d_{z_1}...d_{z_d}\] <p>这种高维积分没有解析解，数值积分计算复杂度随维度指数增长，因此在VAE 引入一个“猜测机”（编码器，也就是一个神经网络，参数 $\varphi$），用变分分布：</p> \[q_\varphi(z\vert x) = \mathcal{N}(z; \mu_\varphi(x), \text{diag}(\sigma_\varphi^2(x)))\] <p>近似后验分布，估计可能的秘方也就是去估算我们的：$p_\theta(z\vert x)$</p><blockquote><p>再去引入新的参数 $\varphi$ 不还是很难计算吗？似乎是这么一回事，但是回顾我们需要解决的问题：$p_\theta(z\vert x)=\frac{p_\theta(x,z)}{p_\theta(x)}=\frac{p_\theta(x\vert z)p(z)}{p_\theta(x)}$ 分子分母三项都是很难计算的那么也就意味着如果要通过 $x$ “推算” $z$ 基本就是很难解决的问题，那么“干脆”不去计算用神经网络进行“模拟”也就是说 $p_\theta(z\vert x) ≈ p_\varphi(z\vert x)$</p></blockquote><p>大致总结一下上述内容，<strong>VAE的主要的任务</strong>：从最开始的数据集里面去获取数据的潜在分布进行学习，如果我模型学会了各类数据的分布，那么就可以通过这些分布去进一步生成新的数据。</p><h3 id="2损失函数构建">2.损失函数构建</h3><p>了解模型基本框架之后就需要对整个模型的参数进行求解，正如上面所述对于数据集分布 $p_\theta(x)$ 的计算我们通过构建一个和 $z$的联合分布，因此整个过程计算如下：</p> \[\begin{align} \log p_{\theta}(x) &amp;= \log p_{\theta}\left(x\right) \tag{1} \\ &amp;= \log p_{\theta}\left(x\right)\int q_{\varphi}(z\vert x)dz \tag{2} \\ &amp;= \int\log p_{\theta}\left(x\right)q_{\varphi}(z\vert x)dz \tag{3} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{Z}\vert\mathbf{X})}[\log p_{\theta}(\mathbf{x})] \tag{4} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{Z}\vert\mathbf{X})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{p_{\theta}(\mathbf{z}\vert\mathbf{x})}\right] \tag{5} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{Z}\vert\mathbf{X})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})q_{\varphi}(\mathbf{z}\vert\mathbf{x})}{p_{\theta}(\mathbf{z}\vert\mathbf{x})q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\right] \tag{6} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\right] + \mathbb{E}_{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\left[\log\frac{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}{p_{\theta}(\mathbf{z}\vert\mathbf{x})}\right] \tag{7} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\right] + \underbrace{D_{\mathrm{KL}}\left(q_{\varphi}(\mathbf{z}\vert\mathbf{x}) \parallel p_{\theta}(\mathbf{z}\vert\mathbf{x})\right)}_{\geq 0} \tag{8} \\ &amp;\geq \mathbb{E}_{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{q_{\varphi}(\mathbf{z}\vert\mathbf{x})}\right] \quad (\text{ELBO}) \tag{9} \end{align}\] <blockquote><p>其中 $\text{ELBO}$也就是所谓的变分下界</p></blockquote><p>（1-3）上面介绍对于联合分布 $p_\theta(x) =\int p_\theta(x\vert z)p(z)dz$ 计算存在困难，因此替换为 $p_\theta(x) =\int p_\varphi(x\vert z)p(z)dz$；（4-7）直接就是贝叶斯公式和一些基本变形；（8）最后一项就是 $KL$散度。最后上面公式就可以写成：</p> \[\begin{aligned} \log p_{\theta}(x)\geq E_{q_{\varphi}(\mathbf{z}|\mathbf{x})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{q_{\varphi}(\mathbf{z}|\mathbf{x})}\right] &amp; =E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}\left[\log\frac{p_{\theta}(\mathbf{x}|\mathbf{z})p(\mathbf{z})}{q_{\varphi}(\mathbf{z}|\mathbf{x})}\right] \\ &amp; =E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}[\log p_{\theta}(\mathbf{x}|\mathbf{z})]+E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}\left[\log\frac{p(\mathbf{z})}{q_{\varphi}(\mathbf{z}|\mathbf{x})}\right] \\ &amp; =E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}[\log p_{\theta}(\mathbf{x}|\mathbf{z})]-D_{KL}\left(q_{\varphi}(\mathbf{z}|\mathbf{x})\|p_{\theta}(\mathbf{z})\right) \end{aligned}\] <p>那么我们的<strong>损失函数</strong>就是（<strong>最大化下面计算</strong>）：</p><p>\(L(\theta, \varphi,x)= E_{q_{\varphi}(\mathbf{z}|\mathbf{x})}\left[\log\frac{p_{\theta}(\mathbf{x},\mathbf{z})}{q_{\varphi}(\mathbf{z}|\mathbf{x})}\right]=E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}[\log p_{\theta}(\mathbf{x}|\mathbf{z})]-D_{KL}(q_{\varphi}(\mathbf{z}|\mathbf{x})\|p_{\theta}(\mathbf{z}))\)</p><blockquote><p>两项计算看似复杂，在实际应用过程中<strong>模型分为两部分</strong>encoder、decoder首先通过encoder去获取图像编码之后通过<strong>特定网络结构</strong>（比如说一层线性层）去获取编码特征的分布系数 $\mu$ 和 $\sigma$，而在得到分布系数之后直接用分布系数交给decoder是不行的（以为得到只是分布系数，直接交给decoder不可导）因此需要一次 “模拟生成”（用一个随机噪声+数据分布比如说直接 $\mu+ \sigma * ϵ$）在得到 “模拟生成之后”直接让decoder进行解码即可，回到<strong>loss计算过程</strong>：1、第一项直接去计算模型持重构图像和实际图像之间BCEloss；2、第二项直接去计算 <code>KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())</code></p></blockquote><p>上述等式中，第一项表示的是给定从编码器得到的潜变量 $z$，解码器“生成”原始数据 $x$ 的能力有多强，第二项表示的是编码器学到的 latent 分布和我们预先设定的先验分布 p（通常是标准正态 N(0,I)）之间的差异。 在深度学习中自然就会直接用 <strong>梯度下降</strong>的方法去优化参数，下面推荐借鉴：<a href="https://arxiv.org/pdf/1906.02691">https://arxiv.org/pdf/1906.02691</a> 中的描述，<strong>参数求解之前引入最开始定义的几个变量</strong>：<br/> \(q_\varphi(z\vert x) = \mathcal{N}(z; \mu_\varphi(x), \text{diag}(\sigma_\varphi^2(x))) \qquad p_\theta(x\vert z) = \mathcal{N}(x; \mu_\theta(z), \sigma_\theta^2(z))\)</p><h4 id="21-参数-theta-计算">2.1 参数 $\theta$ 计算</h4><p>对于 $\theta$参数可以得到：</p> \[\begin{align} \nabla_{\theta} \mathcal{L}(x,\theta,\varphi) &amp;= \nabla_{\theta} \mathbb{E}_{q_\varphi(z|x)} \left[ \log p_\theta(x,z) - \log q_\varphi(z|x) \right] \\ &amp;= \mathbb{E}_{q_\varphi(z|x)} \left[ \nabla_{\theta} (\log p_\theta(x,z) - \log q_\varphi(z|x)) \right] \\ &amp;\approx \nabla_{\theta} (\log p_\theta(x,z) - \log q_\varphi(z|x)) \\ &amp;= \nabla_{\theta} (\log p_\theta(x,z)) \end{align}\] <p>（1-2）也需要注意之所以可以将梯度拿到期望里面（直接借鉴 grok里面解释），另外一点在计算梯度时候将 KL散度拿掉这是因为在KL计算中第一部分参数为 $\varphi$，而第二部分参数 $p_\theta(x)$ 我们实现定义他是一个简单的标准正态分布：$\mathcal{N}(0, I)$</p><p><img src="https://s2.loli.net/2025/06/21/F5rtGRuhl9obVSX.webp" alt="image" width="949" height="478" loading="lazy" decoding="async"/></p><p>（3）这是因为在后面一项中参数是 $\varphi$ 因此计算梯度直接为0因此就可以拿掉，对于下面公式：</p> \[\mathbb{E}_{q_\varphi(z|x)} [\nabla_{\theta} (\log p_\theta(x,z)] \rightarrow \nabla_{\theta} (\log p_\theta(x,z))\] <p>一个容易接受的说法：可以直接通过蒙特卡洛方法，通过从分布中抽取少量样本（甚至单样本）来近似期望值。因此对于期望</p> \[\mathbb{E}_{q_\varphi(z\vert x)}[ \nabla_\theta \log p_\theta(x, z)] \approx \frac{1}{S} \sum_{s=1}^S \nabla_\theta \log p_\theta(x, z)\] <p>计算，可以去通过从 $q_\varphi(z\vert x)$ 进行抽样，当 $S=1$ 时候就直接变成单个样本去近似整个期望。（单样本估计的方差可以通过SGD的多次迭代和批量数据的处理来缓解，模型会在优化过程中逐渐收敛到一个较好的解（通常是局部最优或接近全局最优））</p><h4 id="22-参数-varphi-计算">2.2 参数 $\varphi$ 计算</h4><p>对于参数 $\varphi$不能直接像上面计算一样直接将 $\nabla$ 拿到期望里面（期望和分布都依赖参数 $\varphi$）也就是 <a href="https://arxiv.org/pdf/1906.02691">论文</a> 里面提到的下面公式不成立：</p> \[\begin{equation} \nabla_{\varphi} \mathcal{L}_{\theta, \varphi}(x) = \nabla_{\varphi} \mathbb{E}_{q_{\varphi}(z|x)} \left[ \log p_{\theta}(x, z) - \log q_{\varphi}(z|x) \right] \neq \mathbb{E}_{q_{\varphi}(z|x)} \left[ \nabla_{\varphi} \left( \log p_{\theta}(x, z) - \log q_{\varphi}(z|x) \right) \right] \end{equation}\] <blockquote><p>下面是成立的并且可以直接求导变为0<br/> \(\mathbb{E}_{q_\varphi(z\vert x)}[\nabla_\theta \log p_\theta(x, z)]\)</p></blockquote><p>既然如此那么就直接涌入一个随机变量 $\epsilon$ 将最开始的 $z \in q_{\varphi}(z\vert x)$ 转化为：$z=g(\epsilon,\varphi,x)$，回顾最开始定义的损失函数：</p> \[L(\theta,\varphi,x) = E_{q_{\varphi}(\mathbf{z}|\mathbf{x})}[\log p_{\theta}(\mathbf{x}|\mathbf{z})]-D_{KL}(q_{\varphi}(\mathbf{z}|\mathbf{x})\|p_{\theta}(\mathbf{z}))\] <ul><li><strong>第一部重构项计算</strong>：$E_{q_{\varphi}(\mathbf{z}\vert \mathbf{x})}[\log p_{\theta}(\mathbf{x}\vert\mathbf{z})]$</li></ul><p>因为通过引入了随机变量 $\epsilon$因此上面公式可以变为：$E_\epsilon[\log p_{\theta}(\mathbf{x}\vert\mathbf{z})]$，那么对于其梯度进行计算得到：</p> \[\nabla_{\varphi} \mathbb{E}_{q_{\varphi}(z\vert x)} \left[ \log p_{\theta}(\mathbf{x}\vert \mathbf{z}) \right] = \mathbb{E}_{p(\epsilon)} \left[ \nabla_{\varphi} \log p_{\theta}(\mathbf{x}|\mathbf{z}) \right] = \nabla_{\varphi} \log p_{\theta}(\mathbf{x}|\mathbf{z})\] <p>是不是可以直接让其变成0？还是不行因为里面变量 $z$的分布还是依赖我们的变量 $\varphi$（最开始我们假设了$\mathcal{N}(z; \mu_\varphi(x), \text{diag}(\sigma_\varphi^2(x)))$）对于最后的结果就和最开始的 $\theta$求解一样可以直接通过蒙特卡洛模拟来进行计算，也就是：</p> \[\nabla_{\varphi} \mathbb{E}_{q_{\varphi}(z|x)} \left[ \log p_{\theta}(\mathbf{x}|\mathbf{z}) \right]≈\frac{1}{S}\sum_{s=1}^{S}log p_\theta(x\vert z)\] <ul><li><strong>第二部分KL散度计算</strong>：$D_{KL}(q_{\varphi}(\mathbf{z}\vert \mathbf{x})\vert p_{\theta}(\mathbf{z}))$ <blockquote><p>$D_{KL}\left(q_{\varphi}(\mathbf{z}\vert \mathbf{x})\vert p_{\theta}(\mathbf{z})\right)=E_{q_{\varphi}(\mathbf{Z}\vert \mathbf{X})}\left[\log\frac{q_{\varphi}(\mathbf{z}\vert \mathbf{x})}{p_\theta(\mathbf{z})}\right]$</p></blockquote></li></ul> \[\begin{align} \nabla_\varphi D_{KL}(q_\varphi(\mathbf{z}|\mathbf{x}) \| p_\theta(\mathbf{z})) &amp;= \nabla_\varphi \int q_\varphi(\mathbf{z}|\mathbf{x}) [\log q_\varphi(\mathbf{z}|\mathbf{x}) - \log p_\theta(\mathbf{z})] d\mathbf{z} \\ &amp;= \int \left[ \nabla_\varphi q_\varphi(\mathbf{z}|\mathbf{x}) [\log q_\varphi(\mathbf{z}|\mathbf{x}) - \log p_\theta(\mathbf{z})] + q_\varphi(\mathbf{z}|\mathbf{x}) \cdot \frac{\nabla_\varphi q_\varphi(\mathbf{z}|\mathbf{x})}{q_\varphi(\mathbf{z}|\mathbf{x})} \right] d\mathbf{z} \\ &amp;= \int \nabla_\varphi q_\varphi(\mathbf{z}|\mathbf{x}) \left[ \log q_\varphi(\mathbf{z}|\mathbf{x}) - \log p_\theta(\mathbf{z}) + 1 \right] d\mathbf{z} \\ &amp;= \int q_\varphi(\mathbf{z}|\mathbf{x}) \left[ \nabla_\varphi \log q_\varphi(\mathbf{z}|\mathbf{x}) \right] \left[ \log q_\varphi(\mathbf{z}|\mathbf{x}) - \log p_\theta(\mathbf{z}) + 1 \right] d\mathbf{z} \\ &amp;= \mathbb{E}_{q_{\varphi}(\mathbf{z}|\mathbf{x})} \left[ \left( \log q_\varphi(\mathbf{z}|\mathbf{x}) - \log p_\theta(\mathbf{z}) + 1 \right) \nabla_\varphi \log q_\varphi(\mathbf{z}|\mathbf{x}) \right] \\ &amp;\approx \frac{1}{L} \sum_{l=1}^L \left[ \left( \log q_\varphi(\mathbf{z}^{(l)}|\mathbf{x}) - \log p_\theta(\mathbf{z}^{(l)}) + 1 \right) \nabla_\varphi \log q_\varphi(\mathbf{z}^{(l)}|\mathbf{x}) \right], \quad \mathbf{z}^{(l)} \sim q_\varphi(\mathbf{z}|\mathbf{x}) \end{align}\] <p>上面公式中第2项：直接通过求导的乘法法则，对于括号里面内容求导过程中:第二项 $logp_\theta(z)$和 $\varphi$无关因此直接等于0，第4项：$\nabla_\varphi q_\varphi(z \vert x)=q_\varphi(z\vert x)\nabla_ \varphi logq_\varphi(z\vert x)$，最后两项就是直接改写为期望然后通过蒙特卡洛计算得到结果。</p><h2 id="vq-vae">VQ-VAE</h2><blockquote><p>https://arxiv.org/pdf/1711.00937</p></blockquote><p>注意主要简单介绍一下其基本原理，VQ-VAE主要是为了解决VAE在生成样本模糊问题，通过向量量化将编码器的输出映射到离散的码本（codebook）中。这种离散化可以更好地捕捉数据的局部结构，生成更清晰的样本，同时避免了传统 VAE 中后验分布和先验分布之间的 KL 散度优化带来的不稳定性。因此和传统的VAE差异在于将 “潜在空间”替换为离散的 <strong>码本（codebook）</strong><br/><img src="https://s2.loli.net/2025/06/21/dro6EVse1yBXh3j.webp" alt="image" width="1280" height="382" loading="lazy" decoding="async"/></p><blockquote><p><strong>值得注意的</strong>：VQ-VAE不能直接用于生成新的样本，需要一个额外的先验模型去建模离散的潜在变量</p></blockquote><p>这里不对数学原理做过多解释，直接解释代码如何操作，唯一的区别就是在于如何去构建 “离散化的码本”，对于输入图像通过编码器处理之后：<br/><strong>第一步</strong>、生成我们的码本：$C=[e+k]_{k=1}^K$ 代表K个嵌入向量以及每个向量维度为D。可以直接通过：<code>nn.Embedding(num_embeddings, embedding_dim)</code><br/><strong>第二步</strong>、直接将encoder处理后的内容“拉平”（$(B,C,H,W)\rightarrow (N,D)$ 其中 $N=B\times H\times W$）而后计算拉平后的内容和 码本之间的距离，并且返回距离最小的码本向量（$K,D$）索引：$k=argmin_j \Vert z_e^i- e_j \Vert _2^2$，而后将结果丢到one-hot矩阵（$(N,K)$）中指导每个输入向量选择哪些码本向量<br/><strong>第三步</strong>、将码本权重和one-hot相乘得到量化向量：$z_q\in [N,D]$<br/><strong>第四步</strong>、计算损失：1、承诺损失（编码器输出和量化输出均方误差）；2、量化损失（量化输出和编码器输出的均方误差）；而后对两部分进行加权<br/><strong>第五步</strong>、困惑度：监控码本利用率：$exp(-\sum_k p_klogp_k)$</p><p>说人话就是：编码后的向量会将这个向量映射到一个“字典”（也就是我们的码本，可以理解为这个<strong>码本存储了我们所有特征</strong>，最开始VAE是用连续分布做的）上，具体映射方法就是计算编码器的连续输出和码本中向量最接近的向量，然后用这个向量替换原始特征，但是这个码本开始是不行的，因此就需要计算损失来更新，因此就直接计算码本和编码器输出损失，于此同时也为了保证编码器输出和码本接近也计算损失，但是量化过程（选最近向量）没法直接算梯度，所以用“直通”方法：前向传播用码本选择的向量反向传播时假装梯度直接传回编码器输出。困惑度检查码本向量使用情况。如果困惑度低，说明有些向量没被用（“死码”），需要优化。</p><h2 id="3代码操作">3.代码操作</h2><p>说一千到一万不如直接看代码操作！代码上VAE代码比较简单主要是3个模块：1、encoder；2、decoder；3、潜在空间。在encoder/decoder中就都是通过一个神经网络构建而成的，没有太多需要解释的，不过需要注意的是在通过 encoder 直接处理之后，会额外通过神经网络去“拟合”：$\mu$ 和 $\sigma$ 去代表潜在空间的分布，除此之外在 潜在空间 处理上还会计算我们引入的随机变量 $\epsilon$，通过下面图像解释：</p><p><img src="https://s2.loli.net/2025/06/21/R283pxYnkLqTfsZ.webp" alt="image" width="770" height="487" loading="lazy" decoding="async"/></p><p>因此代码结构上（以MNIST数据集为例）：</p><pre><code class="language-python">class VAE(nn.Module):
    def __init__(self, latent_dim=20, input_dim=784):
        super(VAE, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU()
        )
        self.fc_mu = nn.Linear(128, latent_dim)
        self.fc_var = nn.Linear(128, latent_dim)
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, input_dim),
            nn.Sigmoid()
        )

    def encode(self, x):
        h = self.encoder(x)
        return self.fc_mu(h), self.fc_var(h)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z):
        return self.decoder(z)

    def forward(self, x):
        x = x.view(-1, VAE_CONFIG['image_size'])
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        recon_x = self.decode(z)
        return recon_x, mu, logvar
</code></pre><p>整体的代码结构很简单，在loss计算上，最开始我们的优化是：</p> \[L(\theta, \varphi,x)=E_{q_{\varphi}(\mathbf{Z}|\mathbf{X})}[\log p_{\theta}(\mathbf{x}|\mathbf{z})]-D_{KL}(q_{\varphi}(\mathbf{z}|\mathbf{x})\|p_{\theta}(\mathbf{z}))\] <p>其中第一项为我们的重建项（可以直接通过BCE loss来计算）、第二项为KL散度（存在解：$D_{KL}=\frac{1}{2}\sum_{j=1}^{d}(\mu_j^2+\sigma_j^2-log\sigma_j^2-1)$）。</p><pre><code class="language-python">def vae_loss(recon_x, x, mu, logvar):
    recon_x = torch.clamp(recon_x, 1e-8, 1-1e-8)
    x = x.view(-1, VAE_CONFIG['image_size'])
    BCE = nn.functional.binary_cross_entropy(recon_x, x, reduction='sum')
    KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    return (BCE + KLD) / x.size(0)  # 平均损失
</code></pre><p>所有的代码：1、<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/ModelTraining/VAE/VAE.py">VAE</a>；2、<a href="https://github.com/shangxiaaabb/ProjectCode/blob/main/code/Python/ModelTraining/VAE/PixcelCNN.py">PixelCNN</a></p><h3 id="实际生成效果">实际生成效果</h3><blockquote><p>VAE测试的主要是生成效果（MNIST数据集），而VA-VAE则是测试重构效果</p></blockquote><p><strong>VAE</strong>在MNIST数据集上表现</p><table><thead><tr><th style="text-align: center">固定输入</th><th style="text-align: center">重构图像</th><th style="text-align: center">随机生成</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/05/14/ABJDCTjW46Odx3g.gif" alt="image" width="152" height="152" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/14/fQmMFOXRNovIubA.gif" alt="image" width="152" height="302" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/14/28Tln5qcap6HvPf.gif" alt="image" width="152" height="152" loading="lazy" decoding="async"/></td></tr></tbody></table><p>不过值得注意的是 MNIST数据集很简单所以VAE可以很容易就生成需要的图片<br/><strong>VQ-VAE</strong>在CIFAR10数据集上重构图像的表现（<strong>生成图像只是测试代码运行效果</strong>，CIFAR10数据集自身也比较复杂！，在CIFAR10上都没能生成较好的图片）</p><table><thead><tr><th style="text-align: center">重构图像</th><th style="text-align: center">PixelCNN生成图像</th><th style="text-align: center">PixelCNNPlusPlus 生成图像</th><th style="text-align: center">GatedPixelCNN 生成图像</th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/05/14/38zIQHaL9Vukret.gif" alt="image" width="172" height="342" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/15/DHvAXfZIRimwOen.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/15/mQqgLNF83uzoZiJ.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/15/gDcBlhsyS1or8Iu.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td></tr></tbody></table><h2 id="总结">总结</h2><p>本文主要介绍了VAE的基本原理以及不同参数的求解，除此之外介绍了VQ-VAE的基本原理。两种差异在于：潜在用连续分布替换特征，后者用离散分布替换特征。对于VAE代码在模型上不难只需要在最后生成潜在空间过程中通过神经网络拟合 $\mu$ 和 $\sigma$以及会额外的生成一个随机变量 $\epsilon$在损失函数计算中也只需要注意 $D_{KL}$和重建项的计算即可。</p><h2 id="参考">参考</h2><p>1、https://github.com/hkproj/vae-from-scratch-notes/blob/main/VAE.pdf<br/> 2、https://mbernste.github.io/posts/vae/<br/> 3、https://arxiv.org/pdf/1906.02691<br/> 4、https://arxiv.org/pdf/1312.6114<br/> 5、https://arxiv.org/pdf/1711.00937</p> ]]></content:encoded><category>生成模型</category><pubDate>Sun, 11 May 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/05/11/VAE.html</guid></item><item><title>深入浅出了解生成模型-1：GAN模型原理以及代码实战</title><link>https://www.big-yellow-j.top/posts/2025/05/08/GAN.html</link><description><![CDATA[本文主要介绍生成对抗网络（GAN）作为图像生成基础模型的核心原理、数学推导及训练过程。GAN由生成模型G和判别模型D构成，二者形成零和博弈：生成器通过映射随机噪声生成图像以拟合真实数据分布，判别器则判断样本来自真实数据还是生成模型。其训练目标是最小化JS散度以缩小生成分布与真实分布的距离，通过交替优化实现——先最大化判别器对真实样本的识别能力（提升判断真实样本概率），再最小化生成器被识别的概率（提升生成样本真实性）。但GAN存在训练不稳定（判别器过拟合导致JS散度恒为log2，Loss为0）和模型坍塌（生成样本单一）等问题，改进方案如WGAN采用Wasserstein距离替代JS散度，缓...]]></description><content:encoded><![CDATA[<p>日常使用比较多的生成模型比如GPT/Qwen等这些大多都是“文生文”模型（当然GPT有自己的大一统模型可以“文生图”）但是网上流行很多AI生成图像，而这些生成图像模型大多都离不开下面三种模型：1、GAN；2、VAE；3、Diffusion Model。因此本文主要介绍这三个基础模型作为生成模型的入门，本文主要介绍GAN模型。</p><blockquote><p><strong>此处安利一下</strong>何凯明老师在MiT的课程：<br/> https://mit-6s978.github.io/schedule.html</p></blockquote><h2 id="generative-adversarial-netsgan">Generative Adversarial Nets（GAN）</h2><blockquote><p>From: https://arxiv.org/pdf/1406.2661</p></blockquote><p>在GAN里面一个比较核心的概念就是：通过生成模型G去捕获数据分布，而后通过一个判别模型D，判断样品来自训练数据而不是G。</p><blockquote><p>A generative model G that captures the data distribution, and a discriminative model D that estimates the probability that a sample came from the training data rather than G<br/> 通过下面图像来了解：<br/><img src="https://s2.loli.net/2025/06/21/D8jLSTsyvaH7A24.webp" alt="image" width="599" height="321" loading="lazy" decoding="async"/><br/> 其中：<strong>判别模型会尝试在数据空间中划定边界，而生成式模型会尝试对数据在整个空间中的放置方式进行建模</strong></p></blockquote><p>换言之就是：有两组模型1、生成模型G；2、判别模型D。其中生成模型用来生成我们需要的图像而我们的判别模型则是用来判断所生产的图像是不是“合理”的（就像老师和学生关系，老师只去关注学生的作品怎么样，而学生只去关注如何生成老师满足的作品）。了解基本原理之后，接下来深入了解其理论知识：假设数据$x$ 存在一个分布 $p_g$ 那么可以通过随机生成一个噪音变量 $p_z(z)$ 而后通过一个模型（生成模型） $G(z;\theta _g)$ 来将我们的噪音变量映射到我们正式的数据分布上，而后通过另外一个模型（判别模型） $D(x;\theta _d)$ 来判断数据是来自生成模型还是原始数据分布，因此就可以定义一个下面损失函数：</p><p><img src="https://s2.loli.net/2025/06/21/WV9TpxZOEIArUSl.webp" alt="image" width="990" height="122" loading="lazy" decoding="async"/></p><p>1、$D(x)$：判别器给真实样本的概率输出（判断真实的样本标记1，对于生成的样本标记0）；那么对于这部分计算值：$log(D(x))$ 自然而然的希望他是越大越好（<em>希望判别器经可能的判别真实样本</em>）；2、$D(G(z))$：判别器对于生成样本的概率输出，对于这部分值（$D(G(z))$的计算值）我们希望越接近0越好（<em>越接近0也就意味着判别模型能够区分生成样本</em>），但是对于生成器模型而言希望的是：通过随机生成的样本：z越贴近我们真实分布越好。</p><blockquote><p><strong>两个模型就像是零和博弈，一个尽可能的生成假的东西，一个尽可能判别出假东西</strong></p></blockquote><p>整个训练过程如下所示：<br/><img src="https://s2.loli.net/2025/06/21/gXYCONIfKnW9adL.webp" alt="image" width="984" height="274" loading="lazy" decoding="async"/></p><p>从左到右边：最开始生成模型所生成的效果不佳，判别模型可以很容易就判断出哪些是正式数据哪些是生成数据（蓝色线条代表判别模型给出的评分，最开始很容易判断出哪些是生成数据哪些是正式数据），但是随着模型迭代，生成模型所生成的内容越来越贴近正式的数据分布进而导致判别模型越来越难以判断。</p><p>算法流程：<br/><img src="https://s2.loli.net/2025/06/21/yCqM96YL37cNQJB.webp" alt="image" width="1026" height="686" loading="lazy" decoding="async"/></p><p>GAN训练过程分为两部分：第一部分学习优化判别器；第二部分学习优化生成器。模型架构：<br/><img src="https://s2.loli.net/2025/06/21/Ki7jobR1vwSIGym.webp" alt="image" width="845" height="377" loading="lazy" decoding="async"/></p><h3 id="进一步了解gan数学原理">进一步了解GAN数学原理</h3><p>这部分内容主要参考<strong>李宏毅老师Youtube教程</strong>：</p><div class="video-center"><iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/DMA4MrNieWo?si=kk0HuutqIOT-CLp4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div><p>以及博客中的总结:https://alberthg.github.io/2018/05/05/introduction-gan/</p><h4 id="1gan算法推导">1、GAN算法推导</h4><p>首先，重申以下一些重要参数和名词：</p><ol><li>生成器(Generator,G) <ul><li>Generator是一个函数，输入是 $z$ ，输出是 $x$ ；</li><li>给定一个先验分布 $p_{prior}(z)$ 和反映生成器G的分布 $P_G(x)$，$P_G(x)$ 对应的就是上一节的 $p_{model}(x;θ)$ ；</li></ul></li><li>判别器(Discriminator,D) <ul><li>Discriminator也是一个函数，输入是 $x$ ，输出是一个标量；</li><li>主要是评估 $P_G(x)$ 和 $P_{data}(x)$ 之间到底有多不同，也就是求他们之间的交叉熵，$P_{data}(x)$ 对应的是上一节的 $p_{data}(x)$。</li></ul></li></ol><p>引入目标公式：$V(G,D)$</p> \[V = E_{x \sim P_{data} } \left [\ log\ D(x) \ \right ] + E_{x \sim P_{G} } \left [\ log\ (1-D(x)) \ \right ]\] <p>这条公式就是来衡量 $P_G(x)$ 和 $P_{data}(x)$ 之间的不同程度。对于GAN，我们的做法就是：给定 G ，找到一个 $D^{* }$ 使得 $V(G,D)$ 最大，即 $\underset{D}{max}\ V(G,D)$ ,直觉上很好理解：在生成器固定的时候，就是通过判别器尽可能地将生成图片和真实图片区别开来，也就是要最大化两者之间的交叉熵。</p> \[D^{* } = arg\ \underset{D}{max}\ V(G,D)\] <p>然后，要是固定 D ，使得 $\underset{D}{max}\ V(G,D)$ 最小的这个 G 代表的就是最好的生成器。所以 G 终极目标就是找到 $G^{* }$， 找到了 $G^{* }$ 我们就找到了分布 $P_G(x)$ 对应参数的 $θ_{G}$ ：</p> \[G^{* } = arg\ \underset{G}{min}\ \underset{D}{max}\ V(G,D)\] <p>上边的步骤已经给出了常用的组件和一个我们期望的优化目标，现在我们按照步骤来对目标进行推导：</p><h5 id="寻找最好的-d-">寻找最好的 $D^{* }$</h5><p>首先是第一步，给定 G ，找到一个 $D^{* }$ 使得 $V(G,D)$ 最大，即求 $\underset{D}{max}\ V(G,D)$ ：</p> \[\begin{align} V &amp; = E_{x \sim P_{data} } \left [\ log\ D(x) \ \right ] + E_{x \sim P_{G} } \left [\ log\ (1-D(x)) \ \right ] \\ &amp; = \int_{x} P_{data}(x) log D(x) dx+ \int_{x} P_G(x)log(1-D(x))dx \\ &amp; = \int_{x}\left [ P_{data}(x) log D(x) + P_G(x)log(1-D(x)) \right ] dx \end{align}\] <p>这里假定 $D(x)$ 可以去代表任何函数。然后对每一个固定的 $x$ 而言，我们只要让 $P_{data}(x) log D(x) + P_G(x)log(1-D(x))$ 最大，那么积分后的值 $V$ 也是最大的。</p><p>于是，我们设：</p> \[f(D) = P_{data}(x) log D + P_G(x)log(1-D)\] <p>其中 $D = D(x)$ ，而 $P_{data}(x)$ 是给定的，因为真实分布是客观存在的，而因为 G 也是给定的，所以 $P_G(x)$ 也是固定的。那么，对 $f(D)$ 求导，然后令 ${f}’(D) = 0$，发现：</p> \[D^{* } = \frac{P_{data}(x)}{P_{data}(x)+P_G(x)}\] <p>于是我们就找出了在给定的 G 的条件下，最好的 D 要满足的条件。此时，我们求 $\underset{D}{max}\ V(G,D)$ 就非常简单了，直接把前边的 $D^{* }$ 代进去：</p> \[\begin{align} &amp; \underset{D}{max}\ V(G,D) \\ &amp; = V(G,D^{* })\\ &amp; = E_{x \sim P_{data} } \left [\ log\ D^{* }(x) \ \right ] + E_{x \sim P_{G} } \left [\ log\ (1-D^{* }(x)) \ \right ] \\ &amp; = E_{x \sim P_{data} } \left [\ log\ \frac{P_{data}(x)}{P_{data}(x)+P_G(x)} \ \right ] + E_{x \sim P_{G} } \left [\ log\ \frac{P_{G}(x)}{P_{data}(x)+P_G(x)} \ \right ]\\ &amp; = \int_{x} P_{data}(x) log \frac{P_{data}(x)}{P_{data}(x)+P_G(x)} dx+ \int_{x} P_G(x)log(\frac{P_{G}(x)}{P_{data}(x)+P_G(x)})dx \\ &amp; = \int_{x} P_{data}(x) log \frac{\frac{1}{2}P_{data}(x)}{\frac{P_{data}(x)+P_G(x)}{2} } dx+ \int_{x} P_{G}(x) log \frac{\frac{1}{2}P_{G}(x)}{\frac{P_{data}(x)+P_G(x)}{2} } dx \\ &amp; = \int_{x}P_{data}(x)\left ( log \frac{1}{2}+log \frac{P_{data}(x)}{\frac{P_{data}(x)+P_G(x)}{2} } \right ) dx \\ &amp; = 2 log \frac{1}{2} + 2 \times \left [ \frac{1}{2} KL\left( P_{data}(x) || \frac{P_{data}(x)+P_{G}(x)}{2}\right )\right ] \\ &amp; \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ + 2 \times \left [ \frac{1}{2} KL\left( P_{G}(x) || \frac{P_{data}(x)+P_{G}(x)}{2}\right )\right ] \\ &amp; = -2 log 2 + 2 JSD \left ( P_{data}(x) || P_G(x) \right) \end{align}\] <p>$JSD(P_{data}(x) |P_G(x)) \in [0, log2]$。那么，$\underset{D}{max}V(G,D)\in[0,-2log2]$</p><h5 id="寻找最好的-g-">寻找最好的 $G^{* }$</h5><p>这是第二步，给定 D ，找到一个 $G^{* }$ 使得 $\underset{D}{max}\ V(G,D)$ 最小，即求 $\underset{G}{min}\ \underset{D}{max}\ V(G,D)$ :</p><p>根据求得的 $D^{* }$ 我们有：</p> \[\begin{align} G^{* } &amp; =arg\ \underset{G}{min}\ \underset{D}{max}\ V(G,D) \\ &amp; =arg\ \underset{G}{min}\ \underset{D}{max}\ (-2 log 2 + 2 JSD \left ( P_{data}(x) || P_G(x) \right)) \end{align}\] <p>那么根据上式，使得最小化 $G$ 需要满足的条件是：</p> \[P_{data}(x) = P_{G}(x)\] <p>直观上我们也可以知道，当生成器的分布和真实数据的分布一样的时候，就能让 $\underset{D}{max}\ V(G,D)$ 最小。至于如何让生成器的分布不断拟合真实数据的分布，在训练的过程中我们就可以使用梯度下降来计算：</p> \[θ_G := θ_G - \eta \frac{\partial\ \underset{D}{max}\ V(G,D)}{\partial\ θ_G}\] <h4 id="2算法总结">2、算法总结</h4><ol><li>给定一个初始的 $G_0$ ；</li><li>找到 $D_{0}^{* }$ ，最大化 $V(G_0,D)$ ;（这个最大化的过程其实就是最大化 $P_{data}(x)$ 和 $P_{G_0}(x)$ 的交叉熵的过程）</li><li>使用梯度下降更新 $G$ 的参数 $θ_G := θ_G - \eta \frac{\partial\ \underset{D}{max}\ V(G,D_{0}^{* })}{\partial\ θ_G}$ ，得到 $G_1$；</li><li>找到 $D_{1}^{* }$ ，最大化 $V(G_1,D)$ ;（这个最大化的过程其实就是最大化 $P_{data}(x)$ 和 $P_{G_1}(x)$ 的交叉熵的过程）</li><li>使用梯度下降更新 $G$ 的参数 $θ_G := θ_G - \eta \frac{\partial\ \underset{D}{max}\ V(G,D_{1}^{* })}{\partial\ θ_G}$ ，得到 $G_2$；</li><li>循环……</li></ol><h4 id="3实际过程中的算法推导">3、实际过程中的算法推导</h4><p>前面的推导都是基于理论上的推导，实际上前边的推导是有很多限制的，回顾以下在理论推导的过程中，其中的函数 $V$ 是：</p> \[\begin{align} V &amp; = E_{x \sim P_{data} } \left [\ log\ D(x) \ \right ] + E_{x \sim P_{G} } \left [\ log\ (1-D(x)) \ \right ] \\ &amp; = \int_{x} P_{data}(x) log D(x) dx+ \int_{x} P_G(x)log(1-D(x))dx \\ &amp; = \int_{x}\left [ P_{data}(x) log D(x) + P_G(x)log(1-D(x)) \right ] dx \end{align}\] <p>我们当时说 $P_{data}(x)$ 是给定的，因为真实分布是客观存在的，而因为 G 也是给定的，所以 $P_G(x)$ 也是固定的。但是现在有一个问题就是，样本空间是无穷大的，也就是我们没办法获得它的真实期望，那么我们只能使用估测的方法来进行。</p><p>比如从真实分布 $P_{data}(x)$ 中抽样 $\lbrace x^{(1)},x^{(2)},x^{(3)},…,x^{(m)} \rbrace$；从 $P_{G}(x)$ 中抽样 $\lbrace \tilde x^{(1)},\tilde x^{(2)},\tilde x^{(3)},…,\tilde x^{(m)} \rbrace$ ，而函数 $V$ 就应该改写为：</p> \[\tilde V = \frac{1}{m}\sum_{i=1}^{m} log D(x^i) + \frac{1}{m}\sum_{i=1}^{m} log (1-D(\tilde x^i))\] <p>也就是我们要最大化 $\tilde V$，也就是最小化交叉熵损失函数 $L$，而这个 $L$ 长这个样子：</p> \[L = - \left (\frac{1}{m}\sum_{i=1}^{m} log D(x^i) + \frac{1}{m}\sum_{i=1}^{m} log (1-D(\tilde x^i)) \right )\] <p>也就是说 $D$ 是一个由 $θ_G$ 决定的一个二元分类器，从$P_{data}(x)$ 中抽样 $\lbrace x^{(1)},x^{(2)},x^{(3)},…,x^{(m)} \rbrace$ 作为正例；从 $P_{G}(x)$ 中抽样 $\lbrace \tilde x^{(1)},\tilde x^{(2)},\tilde x^{(3)},…,\tilde x^{(m)} \rbrace$ 作为反例。通过计算损失函数，就能够迭代梯度下降法从而得到满足条件的 $D$。</p><h4 id="4实际情况下的算法总结">4、实际情况下的算法总结</h4><ul><li>初始化一个 由 $θ_D$ 决定的 $D$ 和由 $θ_G$ 决定的 $G$；</li><li>循环迭代训练过程： <ul><li>训练判别器（D）的过程，循环 $k$ 次： <ul><li>从真实分布 $P_{data}(x)$ 中抽样 $m$个正例 $\lbrace x^{(1)},x^{(2)},x^{(3)},…,x^{(m)} \rbrace$</li><li>从先验分布 $P_{prior}(x)$ 中抽样 $m$个噪声向量 $\lbrace z^{(1)},z^{(2)},z^{(3)},…,z^{(m)} \rbrace$</li><li>利用生成器 $\tilde x^i = G(z^i)$ 输入噪声向量生成 $m$ 个反例 $\lbrace \tilde x^{(1)},\tilde x^{(2)},\tilde x^{(3)},…,\tilde x^{(m)} \rbrace$</li><li>最大化 $\tilde V$ 更新判别器参数 $θ_D$： <ul><li>$\tilde V = \frac{1}{m}\sum_{i=1}^{m} log D(x^i) + \frac{1}{m}\sum_{i=1}^{m} log (1-D(\tilde x^i))$</li><li>$θ_D := θ_D - \eta \nabla \tilde V(θ_D)$</li></ul></li></ul></li><li>训练生成器（G）的过程，循环 $1$ 次： <ul><li>从先验分布 $P_{prior}(x)$ 中抽样 $m$个噪声向量 $\lbrace z^{(1)},z^{(2)},z^{(3)},…,z^{(m)} \rbrace$</li><li>最小化 $\tilde V$ 更新生成器参数 $θ_G$： <ul><li>$\tilde{V}=\frac{1}{m}\sum_{i=1}^m\log D(x^i)+\frac{1}{m}\sum_{i=1}^m\log(1-D(G(z^i)))$</li><li>$θ_G := θ_G - \eta \nabla \tilde V(θ_G)$</li></ul></li></ul></li></ul></li></ul><p>不过值得注意的一点是世纪在优化生成器的参数时候会直接不去计算：$\frac{1}{m}\sum_{i=1}^m\log D(x^i)$</p><h4 id="5gan算法缺点">5、GAN算法缺点</h4><h5 id="缺点-1训练不稳定">缺点-1：<strong>训练不稳定</strong></h5><p>主要原因在于这个优化过程并非标准的最小化任务，而是一个复杂的双向优化问题。在上面公式推导中我们得到的是：</p> \[\underset{D}{max}\ V(G,D) = -2 log 2 + 2 JSD \left ( P_{data}(x) || P_G(x) \right)\] <p>我们优化的目标函数是 JSD，理想情况是两部分数据分布距离是越来越小。但实际上有两种情况可能会导致 <strong>JSD 永远判定两个分布距离“无穷大”</strong>（$\mathbf{JSD}(P_{data}(x)||P_G(x))=\log 2$）。<strong>从而使得 Loss Function 永远是 0</strong>（实际测试代码也会存在这种问题，判别器的 loss 为 0）：</p> \[\max_D V(G, D) = -2\log2 + 2 \underbrace{\mathbf{JSD}(P_{\text{data}}(x) \| P_G(x))}_{\log2} = 0\] <p><strong>第一种情况，就是判别器 D 太“强”了导致产生了过拟合</strong>。例如下图：<br/><img src="https://s2.loli.net/2025/06/21/axEz3mPCJ1peQr9.webp" alt="image" width="378" height="233" loading="lazy" decoding="async"/></p><p>上图蓝色和橙色分别是两个分布，我们能发现分布之间确实有一些重叠，所以按理来说 JSD 不应该是 log2 。但由于我们是采样一部分样本进行训练，所以当判别器足够“强”的时候，就很有可能找到一条分界线强行将两类样本分开，从而让两类样本之间被认为完全不存在重叠。我们可以尝试传统的正则化方法（regularization等），也可以减少模型的参数让它变得弱一些。但是我们训练的目的就是要找到一个“很强”的判别器，我们在实际操作中是很难界定到底要将判别器调整到什么水平才能满足我们的需要：既不会太强，也不会太弱。还有一点就是我们之前曾经认为这个判别器应该能够测量 JSD，但它能测量 JSD 的前提就是它必须非常强，能够拟合任何数据。这就跟我们“不想让它太强”的想法有矛盾了，所以实际操作中用 regularization 等方法很难做到好的效果。</p><p><strong>第二种情况，就是数据本身的特性</strong>。一般来说，生成器产生的数据都是一个映射到高维空间的低维流型。而低维流型之间本身就“不是那么容易”产生重叠的。如下图所示<br/><img src="https://s2.loli.net/2025/06/21/XJtbR5g4FNGqQdP.webp" alt="image" width="367" height="215" loading="lazy" decoding="async"/></p><p>也就是说，想要让两个概率分布“碰”到一起的概率并不是很高，他们之间的 “Divergence” 永远是 log2。这会导致整个训练过程中，JSD 作为距离评判标准无法为训练提供指导。</p><p>解决方法有两种，一种是<strong>给数据加噪声，让生成器和真实数据分布“更容易”重叠在一起</strong><br/><img src="https://s2.loli.net/2025/06/21/BEMfdinP1hRVQKs.webp" alt="image" width="366" height="245" loading="lazy" decoding="async"/></p><p>但是这个方法缺点在于，我们的目标是训练准确的数据（例如高清图片等）。加入噪声势必会影响我们生成数据的质量。一个简单的做法是让噪声的幅度随着时间缩小。不过操作起来也是比较困难的。除此之外还有另一种方法。既然 JSD 效果不好，那我们可以换一个Loss Function，使得哪怕两个分布一直毫无重叠，但是都能提供一个不同的连续的的“距离的度量” —— WGAN。</p><blockquote><p><strong>补充介绍-1</strong>：<strong>WGAN原理</strong>；<br/> 在WGAN中替换最开始的JSD为：</p> \[W(P_r,P_g)=\inf_{\gamma\in\Pi(P_r,P_g)}\mathbb{E}_{(x,y)\sim\gamma}[\|x-y\|]\] <p>其中 $P_r$、$P_g$、$\gamma$ 分别代表真实数据分布、生成数据分布、所有能将两个分布连接起来的联合分布。之所以这样是因为在使用JS散度时候，正如上面说的一样JS散度可能会饱和（恒等于log2），导致梯度消失，生成器无法学习。因此就会换一种“距离”度量方式。<br/> 对于WGAN可能会存在：1、判别器倾向于学习简单函数（如二值化输出）。2、梯度爆炸或消失（裁剪阈值 c 敏感）。因此在WGAN_GP提出如下损失函数：</p> \[L_D=\mathbb{E}_{x\sim P_g}[D(x)]-\mathbb{E}_{x\sim P_r}[D(x)]+\lambda L_{GP}\] \[L_{GP}=\lambda \mathbb{E}_{\hat{x}\sim P_2}[(||\nabla_{\hat{x}}D(\hat{x})||_2-1)^2]\] </blockquote><h5 id="缺点-1模型坍塌">缺点-1：<strong>模型坍塌</strong></h5><p>训练中可能遇到的另一个问题：所有的输出都一样！这个现象被称为模型坍塌。这个现象产生的原因可能是由于真实数据在空间中很多地方都有一个较大的概率值，但是我们的生成模型没有直接学习到真实分布的特性。为了保证最小化损失，它会宁可永远输出一样但是肯定正确的输出，也不愿意尝试其他不同但可能错误的输出。也就是说，我们的生成器有时可能无法兼顾数据分布的所有内部模式，只会保守地挑选出一个肯定正确的模式。</p><h3 id="进一步了解gan的代码操作">进一步了解GAN的代码操作</h3><p>测试代码用MNIST数据集进行测试,代码模型其实就很简单就是几层线性模型+激活函数处理,主要关注的是模型如何进行训练,上面我么已经介绍了对于GAN训练需要通过同时训练判别器和生成器两个模型,因此对于训练过程代码如下:</p><pre><code class="language-python">for epoch in range(CONFIG['epochs']):
    for real_images, _ in train_loader:
        batch_size = real_images.size(0)
        real_images = real_images.view(batch_size, -1).to(CONFIG['device'])
        
        # 判别器目标就是判断样本来来源 因此只需要生成:1.真是样本标签;2.生成样本标签
        real_labels = torch.ones(batch_size, 1, device=CONFIG['device'])
        fake_labels = torch.zeros(batch_size, 1, device=CONFIG['device'])
        
        # Train Discriminator
        discriminator.zero_grad()
        
        # Real images
        outputs = discriminator(real_images) # 判别器任务就是判断样本来源
        d_loss_real = criterion(outputs, real_labels)
        
        # 随机生成一个向量而后输入到 生成器中 通过生成器生成 "可能"的图片而后再去计算loss
        z = torch.randn(batch_size, CONFIG['latent_dim'], device=CONFIG['device'])
        fake_images = generator(z)
        outputs = discriminator(fake_images.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        
        # 对判别器进行优化
        d_loss = d_loss_real + d_loss_fake
        d_loss.backward()
        d_optimizer.step()
        
        # 对生成器进行优化
        generator.zero_grad()
        outputs = discriminator(fake_images)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        g_optimizer.step()
</code></pre><p>不过值得注意的是在实际的测试过程中GAN会出现上面提到的问题,比如说判别器的loss直接变成了0,因此为了处理这种问题直接选择:1.真实样本添加一个噪声进行处理;2.标签平滑处理(不直接使用1而是用0.9);3.直接使用WGAN;4.直接调参batch_size等参数调整:</p><pre><code class="language-python"># 添加噪声处理
real_images = real_images + 0.05 * torch.randn_like(real_images)
</code></pre><p>值得注意的是如果要使用WGAN或者WGAN-GP需要做如下修改:</p><ul><li><strong>使用WGAN</strong>:</li></ul><p>1、将判别器最后的输出不要去用sigmoid进行处理，直接输出计算得到的概率即可<br/> 2、修改优化器选择不要用基于动量的优化算法（包括momentum和Adam），推荐RMSProp，SGD<br/> 3、计算损失过程中直接使用 判别器（在WGAN里面可能”称呼”为 critic）去计算 真实样本/ 生成样本的loss的均值</p><pre><code class="language-python">outputs = critic(real_images)
c_loss_real = -outputs.mean()
...
outputs = critic(fake_images.detach())
c_loss_fake = outputs.mean()
...
c_loss = c_loss_real + c_loss_fake
c_loss.backward()
c_optimizer.step()
</code></pre><p>4、而后去对 <strong>判别器的梯度进行裁剪</strong></p><pre><code class="language-python">for p in critic.parameters():
    p.data.clamp_(-CONFIG['clip_value'], CONFIG['clip_value'])
</code></pre><p>5、对于生成器而言就比较简单，直接对生成的样本去计算 loss以及反向传播</p><pre><code class="language-python">fake_images = generator(z)
outputs = critic(fake_images)
g_loss = -outputs.mean()
g_loss.backward()
g_optimizer.step()
</code></pre><p>代码：<a href="../code/GAN.py.txt">⚙</a>、<a href="../code/DCGAN.py.txt">⚙</a>、<a href="../code/WGAN.py.txt">⚙</a>、<a href="../code/WGAN_GP.py.txt">⚙</a> ，得到最后实验效果如下</p><table><thead><tr><th style="text-align: center"><strong>GAN</strong></th><th style="text-align: center"><strong>DCGAN</strong></th><th style="text-align: center"><strong>WGAN</strong></th><th style="text-align: center"><strong>WGAN_GP</strong></th></tr></thead><tbody><tr><td style="text-align: center"><img src="https://s2.loli.net/2025/05/11/dfZyQWqoKL94alO.gif" alt="image" width="152" height="152" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/11/huJTmpbEPMvD4gy.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/11/JjLObdsykwXuKmx.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td><td style="text-align: center"><img src="https://s2.loli.net/2025/05/11/PtBf1m8viKGy5oF.gif" alt="image" width="172" height="172" loading="lazy" decoding="async"/></td></tr></tbody></table><p>其中WGAN和WGAN_GP都是直接使用DCGAN作为基础模型进行训练，并且都是在MNIST数据集上进行测试的结果，<strong>实际测试过程中可能最后得到的结果（epoch=100）可能不是最佳的生成的结果（视觉反映上的效果）</strong></p><h2 id="总结">总结</h2><p>本文主要介绍了GAN的基本原理以及数学推导，GAN主要优化目标为：$\underset{D}{max}\ V(G,D) = -2 log 2 + 2 JSD \left ( P_{data}(x) || P_G(x) \right)$ 通过JS散度去度量生成样本和真实样本之间距离，理论上这个距离是减小的（两部分数据是重合的），但是可能会出现实际得到loss是0（JSD=log2）导致GAN的训练困难，因此提出WGAN使用新的距离度量方式去替换掉JSD。</p><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/1406.2661<br/> 2、https://developers.google.cn/machine-learning/gan/gan_structure<br/> 3、https://alberthg.github.io/2018/05/05/introduction-gan/<br/> 4、https://zhuanlan.zhihu.com/p/25071913</p> ]]></content:encoded><category>生成模型</category><pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/05/08/GAN.html</guid></item><item><title>CV中常用Backbone-2：ConvNeXt模型详解</title><link>https://www.big-yellow-j.top/posts/2025/04/30/ConvNeXt.html</link><description><![CDATA[ConvNeXt是Meta提出的卷积神经网络模型，源自《A ConvNet for the 2020s》和《ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders》两篇论文，核心是通过借鉴ViT（Vision Transformer）的超大感受野及各类优化trick，证明卷积网络在性能上可媲美Transformer。ConvNeXt v1主要通过模型调参与结构改进提升效果，包括macro design（调整block堆叠数量、采用4x4卷积核、通道数与Swin Transformer对齐至96）、In...]]></description><content:encoded><![CDATA[<p>之前介绍了CV常用Backbon：<br/><a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html">CV中常用Backbone-1：Resnet/Unet/Vit系列/多模态系列等)以及代码</a><br/> 这里介绍新的一个Backbone：ConvNeXt，主要来自两篇比较老的来自Meta论文：<br/> 1、《<strong>A ConvNet for the 2020s</strong>》</p><blockquote><p>arXiv:2201.03545</p></blockquote><p>2、《<strong>ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders</strong>》</p><blockquote><p>arXiv:2301.00808<br/> 两篇论文讲的都是一个模型：<em>ConvNeXt</em>。这也是证明一点：Vit效果好并不是attention本身而是因为transform的超大感受野和各种trick。因此作者也是不断借鉴Vit的操作（用斜体表示）</p></blockquote><h2 id="convnext-v1">ConvNeXt v1</h2><blockquote><p>A ConvNet for the 2020s<br/> ⚙-官方代码：<a href="https://github.com/facebookresearch/ConvNeXt/blob/main/models/convnext.py">https://github.com/facebookresearch/ConvNeXt/blob/main/models/convnext.py</a><br/> ⚙-自己修改：<a href="https://www.big-yellow-j.top/code/ConvNeXt.py.txt">https://www.big-yellow-j.top/code/ConvNeXt.py</a></p></blockquote><p><img src="https://s2.loli.net/2025/06/21/WZfCUH6FmnKJxgj.webp" alt="image" width="859" height="624" loading="lazy" decoding="async"/><br/> 值得注意的一点是在 <em>ConvNeXt</em> 其实就是一个大型的模型调参（不断调节网络参数取得不错效果，于此同时作者对于模型为什么这么做也都是：对比其他模型做法而后而后借鉴到自己做法中），首先作者在论文中做了如下的一些对比（和采用 swin-transformer的resnet进行对比）：<br/><img src="https://s2.loli.net/2025/06/21/RsDEntGOxLZ6av7.webp" alt="image" width="553" height="712" loading="lazy" decoding="async"/></p><p>模型在改进上主要是如下几点：1、macro design；2、 ResNeXt；3、inverted bottleneck；4、large kernel size；5、various layer-wise micro designs。</p><ul><li><strong>1、Macro design</strong><blockquote><p>这一点主要是对模型的参数结构做了调整在准确率的提升上起到的效果还是比较有限的</p></blockquote></li></ul><p>在这里作者主要是做了如下几点修改：1、<strong>修改堆叠数量</strong>。将ResNet-50中的block堆叠数量从：$(3,4,6,3)$ 改为：$(3,3,9,3)$。之所以这样设计作者对比 <em>Swin Transformers中主要的比率</em> 为：$(1,1,9,1)$ 通过这样调整对于准确率提升还是比较有限的（78.8%–&gt;79.4%），resnet中堆叠数量<br/><img src="https://s2.loli.net/2025/06/21/FkBqSp3rX5uvGC1.webp" alt="image" width="709" height="311" loading="lazy" decoding="async"/><br/> 2、<strong>修改卷积核</strong>。这点没有过多解释直接使用：步长为4，大小也为4的卷积操作（这里是因为：在 <em>Vit网络架构中通常使用一个步长为4，大小也为4的卷积</em> ），准确率有79.4%–&gt;79.5%<br/> 除此之外作者还有一点修改就是将最初的 <em>通道数由64调整成96和Swin Transformer保持一致</em> ，准确率：80.5%</p><ul><li><strong>2、Inverted bottleneck</strong><blockquote><p>上面第1点是做模型宏观参数（卷积核大小等）做修改，而在这里作者做得主要修改网络结构顺序</p></blockquote></li></ul><p><img src="https://s2.loli.net/2025/06/21/CYmcDh9gniwbRKH.webp" alt="image" width="577" height="192" loading="lazy" decoding="async"/></p><blockquote><p>a：resnet；b：MobileNetV2；c：ConvNeXt</p></blockquote><p>这里作者给出的解释是：<em>在Vit中的MLP做的处理和上图中的（b）操作很相像</em>（代码：<a href="https://www.big-yellow-j.top/code/CVBackbone/Vit.py.txt">⚙</a>）</p><pre><code class="language-python">...
self.linear1 = nn.Linear(embed_dim, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, embed_dim)
...
src2 = self.linear2(self.dropout(F.relu(self.linear1(src2))))
</code></pre><p>因此作者给出的做法是：先降低后提高。在较小的模型上准确率由80.5%提升到了80.6%，在较大的模型上准确率由81.9%提升到82.6%。</p><ul><li><strong>3、Large Kernel Sizes</strong><blockquote><p>换成更加大的卷积核操作</p></blockquote></li></ul><p>这里就比较简单直接将最开始的3x3卷积核改为7x7卷积核，它将模型的准确率提升至80.6%</p><ul><li><strong>4、Micro Design</strong></li></ul><p><strong>激活函数替换</strong>：将Relu改为GELU（对结果影响不是很大）；<br/><strong>减少激活函数</strong>：之前网络结构可能对每一个卷积处理之后都会使用一个激活函数处理，这里的话只在 两个 $1\times1$ 卷积后面添加一个激活函数进行处理；<br/><strong>减少归一化层</strong>：因此在ConvNeXt中也使用了更少的归一化操作，它仅在第一个$1\times1$卷积之前添加了一个BN<br/><strong>替换归一化层</strong>：像之前的描述最开始在卷积网络中都是用BN作为归一化层，这里作者使用LN也取得不错效果；<br/><strong>拆分采样层</strong>：在残差网络中，它通常使用的是步长为 2的3x3卷积或者1x1卷积来进行降采样，这使得降采样层和其它层保持了基本相同的计算策略。但是 <em>Swin Transformer将降采样层从其它运算中剥离开来</em> ，即使用一个步长为2的2x2卷积插入到不同的Stage之间。ConvNeXt也是采用了这个策略，而且在降采样前后各加入了一个LN，而且在全局均值池化之后也加入了一个LN，这些归一化用来保持模型的稳定性。这个策略将模型的准确率提升至82.0%<br/><img src="https://s2.loli.net/2025/06/21/W1qf72cmXed9BEh.webp" alt="image" width="569" height="606" loading="lazy" decoding="async"/></p><p>总的来说这篇论文主要还是集中在玄学调参：1、对于ResNet去修改他的堆叠数量（1,1,9,1）；2、换更加大的卷积核（4x4，4）；3、更加少的激活函数核归一化处理（都只在1x1卷积之后进行操作）。提供如下几种模型<br/><img src="https://s2.loli.net/2025/06/21/NumwMVRf1ECkdp9.webp" alt="image" width="574" height="147" loading="lazy" decoding="async"/><br/> 其中C代表4个stage中输入的通道数，B代表每个stage重复堆叠block的次数</p><h2 id="convnext-v2">ConvNeXt v2</h2><blockquote><p>ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders</p></blockquote><p>这篇文章主要就是两个内容：1、Masked Autoencoder（MAE）；2、Global Response Normalization（GRN）。将这两个内容用到 <strong>ConvNeXt</strong> 中来。依次介绍这两个内容。</p><ul><li><strong>1、Masked Autoencoder</strong></li></ul><p>这点其实并不是很新早在 何凯明提出模型（MAE）以及提到过（详细描述：<a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html#:~:text=768-,MAE%20%E4%B8%BB%E8%A6%81%E6%93%8D%E4%BD%9C%E6%B5%81%E7%A8%8B,-1%E3%80%81patch">🔗</a>）在本文中也是：<strong>原始图片随机移除60%的32x32的patches</strong>。不过需要注意一点是：Vit的MAE和FCMAE（全卷积的MAE）有区别的前者是直接通过decoder将图像中被maske进行还原，而后者是“全局还原”，比如下图（上：ConvNeXt，下：Vit MAE）：<br/><img src="https://s2.loli.net/2025/06/21/v4SZOY9IdlRQsu6.webp" alt="image" width="648" height="521" loading="lazy" decoding="async"/></p><ul><li><strong>2、Global Response Normalization</strong></li></ul><p>理解这个概念是作者在做FCMAE发现一个问题：有许多死亡或饱和的特征图，并且激活在各个通道之间变成了冗余。这种行为主要是在Convnext块中的Dimensive expantasion MLP层中观察到的</p><blockquote><p>there are many dead or saturated feature maps and the activation becomes redundant across channels.This behavior was mainly observed in the dimensionexpansion MLP layers in a ConvNeXt block</p></blockquote><p>还是用上面图像在ConvNeXt v1中很多处理后的图像“失真”（可以理解为decoder构建不出较好的图像全局特征）在使用 <strong>GRN</strong>时候就可以解决这个问题。计算公式为：<br/><img src="https://s2.loli.net/2025/06/21/O9Zc8QTtDxWAazk.webp" alt="image" width="577" height="192" loading="lazy" decoding="async"/></p><p>不过值得注意的是其和<a href="https://www.big-yellow-j.top/posts/2025/02/23/dl-norm.html#:~:text=%F0%9D%91%81-,Instance%2Dnorm,-%EF%BC%9A">instance norm</a>区别（虽然都是对channel来计算）用一个例子描述：<br/> 假设输入是一个 H×W×C 的特征图（比如 56×56×64）：<br/> GRN：计算<strong>所有64个通道</strong>在 56×56 空间上的全局平方和，作为归一化分母。每个通道的每个像素都被这个全局值标准化，通道间相互影响。<br/> InstanceNorm：对<strong>每个通道单独计算</strong> 56×56 的均值和方差。每个通道的像素只根据自己的统计量标准化，64个通道互不干扰</p><h2 id="总结">总结</h2><p>提到的论文中可能在学术上可以提供的参考意义不大，毕竟都是拿来主义，先不管他为什么这样只要能够起到好的作用那他就是好的模型（🤪🤪🤪🤪🤪），另外一点值得注意的是在convNeXt论文出发点是：通过实验来证明Vit效果好的原因不是因为attention本身，而是因为transform的超大感受野和各种trick（我在卷积上使用Vit的操作，结果也可以实现这个效果，卷积不输你Vit！）。</p> ]]></content:encoded><category>Backbone</category><pubDate>Wed, 30 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/04/30/ConvNeXt.html</guid></item><item><title>Qwen多模态系列论文</title><link>https://www.big-yellow-j.top/posts/2025/04/28/QwenVL.html</link><description><![CDATA[Qwen-vl系列模型包括Qwen2-vl与Qwen2.5-vl，在多模态处理领域持续优化。Qwen2-vl核心改进为动态分辨率技术，输入图像无需固定尺寸，通过2x2相邻visual-token拼接后经MLP处理减少token数量，并采用多模态旋转位置编码（M-RoPE），融合时序、高度、宽度维度信息。Qwen2.5-vl进一步升级，视觉编码器采用改进ViT模型（结合window-attention与full-attention）及2D-RoPE，MLP处理阶段将ViT输出的patch解析分组（4个一组）拼接后输入两层MLP。其中window-attention类似卷积操作，通过移动步长...]]></description><content:encoded><![CDATA[<p>本文主要介绍Qwen-vl系列模型包括：<a href="#qwen2-vl">Qwen2-vl</a>、<a href="#qwen25-vl">Qwen2.5-vl</a></p><h2 id="qwen2-vl">Qwen2-vl</h2><blockquote><p>http://arxiv.org/abs/2409.12191</p></blockquote><p>模型结构：<br/><img src="https://s2.loli.net/2025/06/21/4TfkDAaULgQP7uw.webp" alt="image" width="1110" height="703" loading="lazy" decoding="async"/></p><p><strong>Qwen2-vl</strong>主要的改进点在于：1、使用动态分辨率（也就是说输入图像不需要再去改变图像尺寸到一个固定值），于此同时为了减少 <strong>visual-token</strong>数量，将<strong>2x2的的相邻的token进行拼接</strong>到一个token而后通过MLP层进行处理。2、使用多模态的旋转位置编码（M-RoPE）,也就是将原来位置编码所携带的信息处理为：时序（temporal）、高度（height）、宽度（width）。比如下图中对于文本处理直接初始化为：$(i,i,i)$。但是对于图片而言就是：$(i,x,y)$ 其中 $i$ 是恒定的，而对于视频就会将 $i$ 换成视频中图像的顺序</p><p><img src="https://s2.loli.net/2025/06/21/fIMYhUK6AHVp1Nj.webp" alt="image.png" width="1197" height="258" loading="lazy" decoding="async"/></p><h2 id="qwen25-vl">Qwen2.5-vl</h2><blockquote><p>http://arxiv.org/abs/2502.13923</p></blockquote><p>模型结构：<br/><img src="https://s2.loli.net/2025/06/21/QUNBjG974sVcgzP.webp" alt="image" width="1057" height="706" loading="lazy" decoding="async"/></p><p>从模型结构上而言在 <strong>Qwen2.5-vl</strong> 中主要改进点在于：</p><ul><li><strong>视觉编码器上</strong></li></ul><p>1、改进的ViT模型（window-attention+ full-attention）；2、2D-RoPE</p><ul><li><strong>MLP处理</strong></li></ul><p>通过ViT得到所有的patch之后，直接将这些patch<strong>解析分组</strong>（4个一组）然后继续拼接在输入到两层MLP中进行处理</p><hr/><p><strong>补充1：window-attention</strong></p><blockquote><p>https://arxiv.org/abs/2004.05150v2</p></blockquote><p>前面有介绍在Kimi和DeepSeek中如何处理稀疏注意力的（<a href="https://www.big-yellow-j.top/posts/2025/02/21/Kimi-DS-Paper.html">🔗</a>），他们都是通过额外的网络结构来处理注意力计算问题，而在上面提到的注意力计算则是直接通过规则范式计算注意力。</p><p><img src="https://s2.loli.net/2025/06/21/c3zeMLOZut2XkWD.webp" alt="image" width="1315" height="267" loading="lazy" decoding="async"/></p><p>上面 <strong>window-attention</strong> 处理范式就和卷积操作类似，直接通过移动“步长”然后对“采集”得到的内容进行计算注意力。代码：<a href="../code/WindowAttention.py.txt">⚙</a>。代码核心点就在于划分，而后对划分结果计算注意力：</p><pre><code class="language-python">q_window = q[:, :, t:window_end, :]  # (B, num_heads, window_size, head_dim)
k_window = k[:, :, t:window_end, :]
v_window = v[:, :, t:window_end, :]
</code></pre><hr/><p>介绍完这部分有必要了解一下他是如何处理数据的（毕竟说实在话，模型（无论为LLM还是MLLM在结构上创新远不如数据集重要）都是数据驱动的）以及他是如何训练模型的。</p><ul><li><strong>1、模型预训练</strong><br/><img src="https://s2.loli.net/2025/06/21/lG5Xd1Ki9hjDBYR.webp" alt="image" width="953" height="279" loading="lazy" decoding="async"/></li></ul><p>从论文里面作者提到如下几种数据以及处理范式如下：<br/><strong>1、Image-Text Data</strong>（图片-文本匹配数据集）：保留较高评分匹配对（这里也就是说文本对于图片描述要丰富）、信息互补（图像和文本各自提供独特信息）、信息密度平衡<br/><strong>2、Video Data</strong>（视频数据）：首先是通过动态采用方式获取视频帧；<br/><strong>3、图像坐标分辨率处理</strong>：直接将原始图像进行输入不去修改分辨率（固定每个patch为112x112对于不足的不去做填补，总共8x8个patches），对于里面的坐标直接使用Grounding DINO 或者SAM进行获取。<br/><strong>4、Omni-Parsing Data</strong>：对于文档数据集直接解析为html格式</p><ul><li><p><strong>3、模型后训练</strong></p></li><li><p><strong>1、监督微调 (SFT)</strong></p></li></ul><p>SFT阶段用到的instruction data包含约 200 万条数据，50% 为纯文本数据，50% 为多模态数据（图文和视频文本）。在数据过滤流程中，先使用 Qwen2-VL-Instag （一个基于Qwen2-VL的分类模型）将 QA 对分层分类为 8 个主要领域和 30 个细粒度子类别，然后对于这些细分类别，使用领域定制过滤，结合基于规则和基于模型的过滤方法。</p><p>基于规则的过滤: 删除重复模式、不完整或格式错误的条目，以及不相关或可能导致有害输出的查询和答案。<br/> 基于模型的过滤: 使用 Qwen2.5-VL 系列训练的奖励模型评估多模态 QA 对。<br/> 此外，在训练中还使用拒绝采样 (Rejection Sampling)技术，增强模型的推理能力。使用一个中间版本的 Qwen2.5-VL 模型，对带有标注（ground truth）的数据集生成响应，将模型生成的响应与标注的正确答案进行比较，只保留模型输出与正确答案匹配的样本，丢弃不匹配的样本。此外还进一步过滤掉不理想的输出，例如：代码切换 (code-switching)、过长 (excessive length)、重复模式 (repetitive patterns)等。通过这种方式，确保数据集中只包含高质量、准确的示例。</p><p>这里会不会因此丢弃掉一些好的困难样本？报告中并没有提及，似乎对于SFT阶段，正确性的要求压倒难度，并不指望通过这一阶段获得更强的能力。</p><ul><li><strong>2、直接偏好优化 (DPO)</strong></li></ul><p>报告中基本一笔带过。仅使用图文和纯文本数据，不使用视频数据，利用偏好数据将模型与人类偏好对齐。没有使用GRPO和基于规则的强化学习。对于数学、代码以外的任务，似乎没有特别好的规则定义方法，还是要回到基于奖励模型或者偏好数据的方法。</p><h2 id="代码对比">代码对比</h2><p>两个模型在代码上差异：</p><p><img src="https://s2.loli.net/2025/06/21/boN6fuXmUGdrTJ1.webp" alt="image" width="2992" height="1632" loading="lazy" decoding="async"/></p><h3 id="1vit代码">1、ViT代码</h3><p>值得注意的在 <strong>Qwen2-vl</strong>中使用了拼接方式，在 <strong>Qwen2.5-vl</strong>依旧使用了这种方式来将Vit得到的token进行减少进而减小计算量。通过 <strong>Qwen2.5-vl</strong>来理解模型（Qwen2.5-vl中vit操作，<a href="https://github.com/huggingface/transformers/blob/a847d4aa6bd2279f5be235dc0fd862f58f7403d1/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py#L406">代码</a>）,官方代码中划分窗口设置：</p><pre><code class="language-python">def get_window_index(self, grid_thw):
    window_index: list = []
    cu_window_seqlens: list = [0]
    window_index_id = 0
    vit_merger_window_size = self.window_size // self.spatial_merge_size // self.patch_size

    for grid_t, grid_h, grid_w in grid_thw:
        #（1）因为位置编码结构是t、h、w（具体描述见Qwen2-vl描述）
        llm_grid_h, llm_grid_w = (
            grid_h // self.spatial_merge_size, # spatial_merge_size：空间合并的尺寸
            grid_w // self.spatial_merge_size,
        )
        index = torch.arange(grid_t * llm_grid_h * llm_grid_w).reshape(grid_t, llm_grid_h, llm_grid_w)

        #（2）计算需要的padding
        pad_h = vit_merger_window_size - llm_grid_h % vit_merger_window_size
        pad_w = vit_merger_window_size - llm_grid_w % vit_merger_window_size

        #（3）计算padding后的窗口数量，并且用 -100 进行填补
        num_windows_h = (llm_grid_h + pad_h) // vit_merger_window_size
        num_windows_w = (llm_grid_w + pad_w) // vit_merger_window_size
        index_padded = F.pad(index, (0, pad_w, 0, pad_h), "constant", -100)
        
        #（4）重塑索引为窗口形式
        index_padded = index_padded.reshape(
            grid_t,
            num_windows_h,
            vit_merger_window_size,
            num_windows_w,
            vit_merger_window_size,
        )
        index_padded = index_padded.permute(0, 1, 3, 2, 4).reshape(
            grid_t,
            num_windows_h * num_windows_w,
            vit_merger_window_size,
            vit_merger_window_size,
        )

        #（5）计算每个窗口中有效元素的数量
        seqlens = (index_padded != -100).sum([2, 3]).reshape(-1)
        index_padded = index_padded.reshape(-1)
        index_new = index_padded[index_padded != -100]
        window_index.append(index_new + window_index_id)
        cu_seqlens_tmp = seqlens.cumsum(0) * self.spatial_merge_unit + cu_window_seqlens[-1]
        # self.spatial_merge_unit = self.spatial_merge_size * self.spatial_merge_size
        cu_window_seqlens.extend(cu_seqlens_tmp.tolist())
        window_index_id += (grid_t * llm_grid_h * llm_grid_w).item()
    # 合并所有的窗口索引
    window_index = torch.cat(window_index, dim=0)
    # window_index: 窗口索引；
    # cu_window_seqlens：每个窗口的间隔
    return window_index, cu_window_seqlens
</code></pre><p>争对上面代码，比如输入数据形状以及参数为：<br/> 1、<code>grid_thw:[2,8,8]</code>；2、<code>self.window_size = 8</code>；3、<code>self.spatial_merge_size=self.patch_size=2</code>。那么每一步得到结果为：<br/> （1）index结果为（因为要进行2x2进行合并操作）:</p><pre><code class="language-python">index: tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11],
         [12, 13, 14, 15]],

        [[16, 17, 18, 19],
         [20, 21, 22, 23],
         [24, 25, 26, 27],
         [28, 29, 30, 31]]])
</code></pre><p>（3）得到index_padded为：</p><pre><code class="language-python">index_padded: tensor([[[   0,    1,    2,    3, -100, -100],
         [   4,    5,    6,    7, -100, -100],
         [   8,    9,   10,   11, -100, -100],
         [  12,   13,   14,   15, -100, -100],
         [-100, -100, -100, -100, -100, -100],
         [-100, -100, -100, -100, -100, -100]],

        [[  16,   17,   18,   19, -100, -100],
         [  20,   21,   22,   23, -100, -100],
         [  24,   25,   26,   27, -100, -100],
         [  28,   29,   30,   31, -100, -100],
         [-100, -100, -100, -100, -100, -100],
         [-100, -100, -100, -100, -100, -100]]])
</code></pre><p>（5）window_size合并得到结果为：</p><pre><code class="language-python">window_index:tensor([ 0,  1,  4,  5,  2,  3,  6,  7,  8,  9, 12, 13, 10, 11, 14, 15, 16, 17,
        20, 21, 18, 19, 22, 23, 24, 25, 28, 29, 26, 27, 30, 31])
cu_window_seqlens:[0, 16, 32, 32, 48, 64, 64, 64, 64, 64, 80, 96, 96, 112, 128, 128, 128, 128, 128]
# 这里有重复数值，后面计算会通过 torch.unique_consecutive 去除得到：[  0,  16,  32,  48,  64,  80,  96, 112, 128]
</code></pre><p>理解上面计算结果（处理思路和卷积神经网络很像）：<code>window_index</code>：因为输入是 $[2,8,8]$ 然后划分大小为2（<code>self.spatial_merge_size=self.patch_size=2</code>）就像是“卷积核”一样。因此得到序列长度就是：32（也就是0-31），其中每一个索引代表图像中的“一块”，比如说：0代表左上角2x2的区域，1：代表0右边2x2区域；<code>cu_window_seqlens</code>：知道每块区域索引之后还需要知道“步长”，$0,16$ 代表第一块和第二块之间间隔为16那么就可以确定有4块（ $4\times2\times2$ ）</p><p>得到window_size之后在forward计算中：</p><pre><code class="language-python">def forward(self, hidden_states, grid_thw):
    hidden_states = self.patch_embed(hidden_states)
    ...
    window_index, cu_window_seqlens = self.get_window_index(grid_thw)
    ...
    cu_window_seqlens = torch.unique_consecutive(cu_window_seqlens)

    #（1）重塑窗口化特征
    seq_len, _ = hidden_states.size()
    hidden_states = hidden_states.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
    # 按照window_size进行排序
    hidden_states = hidden_states[window_index, :, :]
    hidden_states = hidden_states.reshape(seq_len, -1)
    #（2）重塑位置编码
    rotary_pos_emb = rotary_pos_emb.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
    rotary_pos_emb = rotary_pos_emb[window_index, :, :]
    rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1)

    emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1)
    position_embeddings = (emb.cos(), emb.sin())

    #（3）计算序列长度
    cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum(
            dim=0,
            dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,
        )
    cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)

    #（4）遍历而后计算注意力
    for layer_num, blk in enumerate(self.blocks):
        if layer_num in self.fullatt_block_indexes:
            cu_seqlens_now = cu_seqlens
        else:
            cu_seqlens_now = cu_window_seqlens
        # 计算注意力
        if self.gradient_checkpointing and self.training:
            hidden_states = self._gradient_checkpointing_func(
                    blk.__call__, hidden_states, cu_seqlens_now, None, position_embeddings
                )
        else:
            hidden_states = blk(hidden_states, cu_seqlens=cu_seqlens_now, position_embeddings=position_embeddings)

        hidden_states =

      hidden_states = self.merger(hidden_states)
      reverse_indices = torch.argsort(window_index)
      hidden_states = hidden_states[reverse_indices, :]

      return hidden_states
</code></pre><p>其中<code>self.merger</code>为：</p><pre><code class="language-python">class Qwen2_5_VLPatchMerger(nn.Module):
    def __init__(self, dim: int, context_dim: int, spatial_merge_size: int = 2) -&gt; None:
        super().__init__()
        self.hidden_size = context_dim * (spatial_merge_size**2)
        self.ln_q = Qwen2RMSNorm(context_dim, eps=1e-6)
        self.mlp = nn.Sequential(
            nn.Linear(self.hidden_size, self.hidden_size),
            nn.GELU(),
            nn.Linear(self.hidden_size, dim),
        )

    def forward(self, x: torch.Tensor) -&gt; torch.Tensor:
        x = self.mlp(self.ln_q(x).view(-1, self.hidden_size))
        return x
</code></pre><p>总结上面代码过程如下：首先是将图像划分为不同patch（这里操作和常规的Vit操作没有区别）得到特征 <code>hidden_states</code>，而后去划分不同窗口，而这个窗口就是直接去对最开始图像所进行的（比如说图像为：2x8x8，2代表时间帧），首先计算需要合并的块的索引，而后将 <code>hidden_states</code> 根据这个索引进行排序，排序之后就需要对这些排序内容计算注意力即可（很像卷积操作：分块就是我们的卷积核，而cu_window_seqlens就是我们的步长）</p><blockquote><p><code>grid_thw:[2,8,8]</code>；2、<code>self.window_size = 8</code>；3、<code>self.spatial_merge_size=self.patch_size=2</code></p></blockquote><h3 id="2位置编码">2、位置编码</h3><p>在Qwen2-VL中，时间方向每帧之间固定间隔 1 ，没有考虑到视频的采样率，例如四秒的视频每秒采样两帧和一秒的视频每秒采样八帧，这样总的帧数都是8，在原来这种编码方式中时间维度的编码都是1-&gt;8没有任何区别。Qwen-2.5VL在时间维度上引入了动态 FPS (每秒帧数)训练和绝对时间编码，将 mRoPE id 直接与时间流速对齐。描述原理见：https://spaces.ac.cn/archives/10040</p><h2 id="参考">参考</h2><p>1、https://arxiv.org/abs/2004.05150v2<br/> 2、http://arxiv.org/abs/2309.16609<br/> 3、http://arxiv.org/abs/2409.12191<br/> 4、http://arxiv.org/abs/2502.13923<br/> 5、https://zhuanlan.zhihu.com/p/24986805514<br/> 6、https://qwenlm.github.io/zh/blog/qwen2.5-vl/</p> ]]></content:encoded><category>paper</category><pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/04/28/QwenVL.html</guid></item><item><title>强化学习框架：OpenRLHF源码解读，模型训练-1</title><link>https://www.big-yellow-j.top/posts/2025/04/24/OpenRLHF-2.html</link><description><![CDATA[OpenRLHF强化学习框架中PPO范式训练的初始化配置详解，涵盖DeepSpeed配置与vLLM配置核心内容。vLLM配置包含两种资源调度模式：Hybrid模式（多引擎共享GPU）与标准模式（单引擎独占GPU/CPU），并基于Ray分布式架构封装vLLM推理框架（LLMRayActor），涉及Ray核心概念如无状态任务（Task）、有状态计算单元（Actor）及异步远程调用（remote）。关键模块实现方面，experience_maker.py定义BaseExperienceMaker基础类（初始化tokenizer、存储生成内容至Samples）及RemoteExperienceM...]]></description><content:encoded><![CDATA[<p>前文已经介绍了：</p><ul><li><a href="https://www.big-yellow-j.top/posts/2025/04/22/OpenRLHF-1.html"><strong>强化学习框架：OpenRLHF源码解读，模型处理模块解读</strong></a></li></ul><p>本文主要介绍 <strong>强化学习框架：OpenRLHF源码解读，模型训练</strong>。因为在 <strong>OpenRLHF</strong> 中整个模型训练过程代码比较多因此分成多次进行解读，此部分主要介绍一些训练前的初始化配置。因为RL由DPO、GRPO、PPO等几种类别，因此本文主要介绍PPO范式训练。在OpenRLHF训练框架中，主要还会应用到DeepSpeed以及vLLM，因此在介绍PPO训练之前需要回顾一下：<strong>1、DeepSpeed的配置</strong>；<strong>2、vLLM配置</strong>。</p><blockquote><p>在之前Blog已经对<a href="https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html">DeepSpeed</a>以及<a href="https://www.big-yellow-j.top/posts/2025/02/17/Attention.html">vLLM</a>原理进行了解释，因此只需要介绍在OpenRLHF如何去对这两部分进行配置</p></blockquote><p>参数参考脚本：https://github.com/OpenRLHF/OpenRLHF/blob/main/examples/scripts/train_ppo_llama_ray.sh 中的设置</p><ul><li><strong>1、vLLM配置</strong></li></ul><blockquote><p>From:https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/trainer/ray/vllm_engine.py</p></blockquote><pre><code class="language-python">def create_vllm_engines(
    num_engines: int, # 推理引擎数量
    tensor_parallel_size: int, # 张量并行大小
    pretrain: str,
    seed: int,
    full_determinism: bool,
    enable_prefix_caching: bool,
    enforce_eager: bool,
    max_model_len: int,
    num_total_actors: int,
    shared_pg=None,
    gpu_memory_utilization=None,
    vllm_enable_sleep=False,):
    ...
    # 1、资源调度配置。配置参数设置为：num_engines= tensor_parallel_size= 2
    use_hybrid_engine = shared_pg is not None
    num_gpus = int(tensor_parallel_size == 1)
    if use_hybrid_engine and tensor_parallel_size == 1:
        num_gpus = 0.2

    if not use_hybrid_engine:
        bundles = [{"GPU": 1, "CPU": 1} for _ in range(num_engines * tensor_parallel_size)]
        shared_pg = placement_group(bundles, strategy="PACK")
        ray.get(shared_pg.ready())
    ...
    # 2、构建每一个vLLM（=2）
    for i in range(num_engins):
        ...
        scheduling_strategy = PlacementGroupSchedulingStrategy(...) # 调度策略
        ...
        vllm_engines.append(
            LLMRayActor.options(
            num_cpus=num_gpus,
            num_gpus=num_gpus,
            scheduling_strategy=scheduling_strategy,
        ).remote(...)
        )
</code></pre><p>1、<strong>资源调度配置</strong>：第一种Hybrid模式（多个引擎共同占用GPU）；第二种标准模式（每个引擎都单独占用一个GPU和CPU）；<br/> 2、<strong>构建vLLM</strong>：首先是建立资源调度策略，以及使用vLLM。有必要了解一下就是在OpenRLHF中使用的是 <a href="https://www.ray.io/"><strong>ray</strong></a> 分布式架构进行训练。简单了解一下在这个里面他是怎么做的。通过 ray封装了一个vLLM推理架构（ <code>LLMRayActor</code> ）</p><blockquote><p><strong>补充-1</strong>：ray简单使用，代码：<a href="../code/ray_test.py.txt">🔗</a><br/> Ray核心概念：1、任务（Task）：无状态的并行函数调用。<br/> 2、Actor：有状态的计算单元，适合需要持久状态的场景（如模型推理）。3、远程调用（remote）：通过 .remote() 异步调度任务或 Actor 方法。比如说上面代码，初始化我需要的节点</p><pre><code class="language-python">@ray.remote
class PrintActor:
</code></pre><p>在vLLM中可能就需要对资源进行分配：<code>PrintActor.options(...).remote(...)</code>其中 <code>remote</code>就是每个“进程”需要输出的任务参数，而 <code>options</code> 则是资源分配策略，比如GPU（<code>num_gpus</code>）/CPU（<code>num_cpus</code>）数量。在后面获取进程结果可以直接通过：</p><pre><code class="language-python">ray.init()
print_engines = create_print_engines(4)
results = [engine.execture_print.remote(i) for i, engine in enumerate(print_engines)]
print(ray.get(results))
</code></pre></blockquote><h2 id="ppo训练范式">PPO训练范式</h2><p>TODO: 此部分没有写完<br/> 完全了解PPO训练范式之前需要了解一下在OpenRLHF中如何定义 PPO训练器的。</p><h3 id="experience_makerpy"><code>experience_maker.py</code></h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/trainer/ppo_utils/experience_maker.py</p></blockquote><p>改代码中首先定义一个基础类 <code>BaseExperienceMaker</code> 用于初始化，这个类中主要作用为：1、定义一个 <code>tokenizer</code>；2、会将所有生成的内容都存储在 <code>Samples</code> 中。</p><pre><code class="language-python3">@dataclass
class Samples:
    sequences: torch.Tensor
    attention_mask: Optional[torch.LongTensor]
    action_mask: Optional[torch.BoolTensor]
    num_actions: Union[int, torch.Tensor]
    packed_seq_lens: Optional[torch.Tensor]
    response_length: torch.Tensor
    total_length: torch.Tensor
    prompts: list[str]
    labels: list[str]
    pad_len: Optional[int]
</code></pre><p>关键是 <code>RemoteExperienceMaker</code>定义：</p><pre><code class="language-python">class RemoteExperienceMaker(BaseExperienceMaker):
    def __init__(...):
        ...
    #（1）定义生成器，通过LLM生成新的文本内容
    @torch.no_grad()
    def generate_samples(self, all_prompts: List[str], all_labels, **generate_kwargs):
        #（1.1）使用hugginface生成  _generate_with_hf(...)
        #（1.2）使用vLLM进行生成     _generate_vllm(...)
    @torch.no_grad()
    def _generate_with_hf(...):
        ...
    def _generate_vllm(...):
        ....
    #（2）计算
    @torch.no_grad()
    def make_experience_list(self, all_prompts: Union[str, List[str]], all_labels, **generate_kwargs):
        ...
        #（2.1）生成内容
        if self.strategy.ring_attn_group is not None:
            # Only rank 0 in the ring attention group executes the generation function, and then broadcasts it to all other ranks.
            if self.strategy.ring_attn_rank == 0:
                samples_list = self.generate_samples(all_prompts, all_labels, **generate_kwargs)

                dist.broadcast_object_list(samples_list, src=dist.get_rank(), group=self.strategy.ring_attn_group)
            else:
                world_size = torch.distributed.get_world_size() // args.ring_attn_size
                samples_list = [None] * (
                    args.rollout_batch_size * args.n_samples_per_prompt // world_size // args.micro_rollout_batch_size
                )
                dist.broadcast_object_list(
                    samples_list, src=self.strategy.ring_attn_ranks[0], group=self.strategy.ring_attn_group
                )
        else:
            samples_list = self.generate_samples(all_prompts, all_labels, **generate_kwargs)
        ...
        #TODO:（2.2）核心
        experiences = self.make_experience(samples_list)
        # Process experiences (reward shaping, etc.)
        experiences = self.compute_advantages_and_returns(experiences, **generate_kwargs)
        # send experience to critic
        if self.critic is not None:
            for experience in experiences:
                experience_cpu = deepcopy(experience)
                experience_cpu.to_device("cpu")
                self._ref = self.critic.append.remote(experience_cpu)
        return experiences

    @torch.no_grad()
    def make_experience(self, samples_list: List[Samples]) -&gt; List[Experience]:
       pass
    
</code></pre><h4 id="一定义生成器"><strong>一、定义生成器</strong></h4><blockquote><p>通过两种方式（huggingface以及vLLM）</p></blockquote><ul><li>第一种直接使用huggingface操作进行生成：</li></ul><pre><code class="language-python">@torch.no_grad()
def _generate_with_hf(self, all_prompts: List[str], all_labels, **generate_kwargs) -&gt; List[Samples]:
    ...    
    # train_ppo_ray.py n_samples_per_prompt=1 对每一条 prompt 生成内容数量
    all_prompts = sum([[prompt] * args.n_samples_per_prompt for prompt in all_prompts], [])
    all_labels = sum([[label] * args.n_samples_per_prompt for label in all_labels], [])
    samples_list = []
    for i in range(0, len(all_prompts), args.micro_rollout_batch_size): 
        # train_ppo_ray.py micro_rollout_batch_size=8
        prompts = all_prompts[i : i + args.micro_rollout_batch_size]
        labels = all_labels[i : i + args.micro_rollout_batch_size]
        inputs = self.tokenize_fn(prompts, self.prompt_max_len, device="cuda")
        sequences, attention_mask, action_mask = self.actor.generate(**inputs, **generate_kwargs)
        samples = Samples(...) # 将生成内容所有信息存储，比如说sequences这些关键信息
        samples_list.append(samples)
    return samples_list
</code></pre><ul><li>第二种通过vLLM方式进行生成：</li></ul><p>使用vLLM方式进行生成需要注意的是，正如最开始上面介绍的，我的所有的vLLM都是通过ray进行处理了，因此这个就会比较复杂</p><pre><code class="language-python">def _generate_vllm(self, all_prompts: List[str], all_labels, **kwargs):
    ...
    #（1）首先获取所有的设备数量，并且确定 vllm 引擎数量
    rank = torch.distributed.get_rank() // self.strategy.ring_attn_size
    world_size = torch.distributed.get_world_size() // self.strategy.ring_attn_size
    if len(self.vllm_engines) &lt;= world_size:
        llms = [self.vllm_engines[rank % len(self.vllm_engines)]]
    else:
        llms = self.vllm_engines[rank::world_size]
    ...
    #（2）直接通过 from vllm import SamplingParams 来设置生成器策略比如说 top_p等
    sampling_params = SamplingParams(...)
    #（3）将prompt、labels进行重复采用，这里和hf生成处理相同，并且将prompt通过tokenizer进行处理
    all_prompts = sum([[prompt] * args.n_samples_per_prompt for prompt in all_prompts], [])
    all_labels = sum(...)
    all_prompt_token_ids = self.tokenize_fn(all_prompts, self.prompt_max_len, padding=False)["input_ids"]
    #（4）直接将所有的prompt输入到 vllm中进行生成
    refs = []
    batch_size = (len(all_prompt_token_ids) + len(llms) - 1) // len(llms)
    for i, llm in enumerate(llms):
        prompt_token_ids = all_prompt_token_ids[i * batch_size : (i + 1) * batch_size]
        refs.append(
            llm.add_requests.remote(rank, sampling_params=sampling_params, prompt_token_ids=prompt_token_ids)
        )
    ray.get(refs)
    #（5）所有设备之间进行同步
    ...
    #（6）获取vLLM生成内容
    all_output_refs = []
    for i, llm in enumerate(llms):
        all_output_refs.append(llm.get_responses.remote(rank))
    all_outputs = sum(ray.get(all_output_refs), [])
    #（7）处理vLLM输出
    samples_list = []
    for i in range(0, len(all_outputs), args.micro_rollout_batch_size):
        outputs = all_outputs[i : i + self.strategy.args.micro_rollout_batch_size]
        prompts = all_prompts[i : i + self.strategy.args.micro_rollout_batch_size]
        labels = all_labels[i : i + self.strategy.args.micro_rollout_batch_size]
        #（8）处理数据
        if not self.packing_samples:
            # 不对数据进行打包输出
            # | [PAD] [PAD] token token token | token token [EOS] [PAD] |
            # | token token token token token | token token [EOS] [PAD] |
            # | [PAD] [PAD] [PAD] token token | token token token [EOS] |
            # |&lt;---------- prompt -----------&gt;|&lt;-------- answer -------&gt;|
            ...
            samples_list.append(...)
        else:
            # 对数据进行打包输出
            # | token token token | token token [EOS] | token token token token token | token token [EOS] | token token | token token token [EOS] |
            # |&lt;---  prompt -----&gt;|&lt;---- answer -----&gt;|&lt;---------- prompt -----------&gt;|&lt;----- answer ----&gt;|&lt;- prompt --&gt;|&lt;-------- answer -------&gt;|
            ...
            samples_list.append(...)
    return samples_list
</code></pre><p>（4）<code>llm.add_requests.remote(...)</code>、<code>llm.get_responses.remote(...)</code> 其中 <code>add_requests</code> 执行操作：直接通过vLLM生成文本：<code>self.llm.generate(prompts=requests, sampling_params=sampling_params)</code> 而在 <code>get_responses</code>则是直接获取vLLM所生成的内容。<br/> （7-8）在处理输出过程中由于数据可能会将短文本进行拼接（<code>packing_samples</code>）但是不同短文本之间对应问题是不同的，因此就需要将输出进行整理。处理过程比较简单：<br/><strong>不对数据进行打包输出</strong>：只需要就算 <code>outputs</code>（是被切分了每次处理的一块） 中最大长度然后按照这个最大长度进行填补即可（左填充方式：在最左侧添加 [PAD]标记）<br/><strong>如果将数据进行打包输出</strong>：只需要将prompt和输出拼接起来即可</p><h4 id="二计算"><strong>二、计算</strong></h4><p>前面已经介绍了如何通过vllm/hf通过prompt生成内容（<code>generate_samples</code>）因此在 <code>make_experience_list</code>首先也就是直接根据prompt来生成内容，而后 <code>make_experience</code>、<code>compute_advantages_and_returns</code>、<code>critic</code>。分别介绍这三部分代码内容：</p><ul><li>1、<code>make_experience</code></li></ul><h3 id="ppo_actorpy"><code>ppo_actor.py</code></h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/trainer/ray/ppo_actor.py</p></blockquote><p>这里主要是定义了一个trainer类，用来执行数据加载、模型处理、记录loss等操作</p><pre><code class="language-python">class ActorPPOTrainer(BasePPOTrainer):
    def __init__(...):
        ...
        # （1）初始化记录器，比如说wandb、tensorboard，这部分比较简单不做介绍
        ...
        self.experience_maker = RemoteExperienceMaker(...) #TODO: 这部分需要特殊去看一下
        # （2）因为要使用ray就需要设置分布式训练设置
        backend = getattr(self.strategy.args, "vllm_sync_backend", "nccl") # 判断是否使用 nccl 进行通信
        ...
        # （3）ray 分布式节点设置
        if self.vllm_engines is not None and not self.use_cuda_ipc and torch.distributed.get_rank() == 0: # 如果不使用 nccl 进行通信
            # 初始化设置：获取主节点，并且随便绑定一个端口
            master_address = ray._private.services.get_node_ip_address()
            with socket.socket() as sock:
                sock.bind(("", 0))
                master_port = sock.getsockname()[1]

            vllm_num_engines, vllm_tensor_parallel_size = (
                self.strategy.args.vllm_num_engines,
                self.strategy.args.vllm_tensor_parallel_size,
            )
            # 获取规模：计算vllm引擎以及几张卡进行张量并行
            world_size = vllm_num_engines * vllm_tensor_parallel_size + 1

            use_ray = getattr(self.strategy.args, "vllm_sync_with_ray", False)
            group_name = "openrlhf"
            refs = [
                engine.init_process_group.remote(
                    master_address,
                    master_port,
                    i * vllm_tensor_parallel_size + 1,
                    world_size,
                    group_name,
                    backend=backend,
                    use_ray=use_ray,
                )
                for i, engine in enumerate(self.vllm_engines)
            ]
            # (4) 通信之间进行同步
            ...
            ray.get(refs)
        torch.distributed.barrier()
    def fit(
        ...
        prompts_dataloader,
        pretrain_dataloader,
        consumed_samples=0,
        num_update_steps_per_episodes=1,):
        # 主要进行数据加载/模型计算/模型存储等操作
        ...
        for episode in range(start_episode, args.num_episodes):
            ...
            for rand_prompts, labels in self.prompts_dataloader:
                for i, experience in enumerate(
                    self.experience_maker.make_experience_list(rand_prompts, labels, **self.generate_kwargs)
                ):


</code></pre><p>（3）使用 ray 分布式节点设置，初始化设置主节点/端口号/分组信息等，通过 <code>refs</code> 来存储初始化的 vLLM，代码中初始化操作为：</p><pre><code class="language-python">def init_process_group(self, master_address, master_port, rank_offset, world_size, group_name, backend, use_ray):
    return self.llm.collective_rpc(
        "init_process_group",
        args=(master_address, master_port, rank_offset, world_size, group_name, backend, use_ray),
    )
</code></pre><p>其中 <code>self.llm=vllm.LLM(*args, **kwargs)</code></p><h3 id="launcherpy">launcher.py</h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/trainer/ray/launcher.py</p></blockquote><p>在launcher中主要也是定义各类分布式系统方式（如何将各类模型分不到设别上），首先来说定义两个基类：<br/><code>DistributedTorchRayActor</code>:这个类是一个基于 Ray 的分布式 Actor，负责设置分布式训练或推理环境，<strong>它初始化分布式环境的配置，包括节点地址、端口、进程排名等。</strong><br/><code>BasePPORole(DistributedTorchRayActor)</code>:扩展上面所定义的分布式环境配置，并提供了批量推理的功能。<br/> 下面就是对各类模型：rewardmodel等进行分布式进行处理</p><ul><li>1、<code>ReferenceModelRayActor</code> 和 <code>RewardModelRayActor</code></li></ul><pre><code class="language-python">@ray.remote(num_gpus=1)
class ReferenceModelRayActor(BasePPORole):
    def init_model_from_pretrained(self, strategy: DeepspeedStrategy, pretrain):
        self._setup_distributed(strategy)
        model = Actor(...)
        # 1）这里也就是对于一些模型进行初始化操作
        ...
    def forward(...):
        ...
        # 2）这里也就是直接将文本输入到模型中进行计算

@ray.remote(num_gpus=1)
class RewardModelRayActor(BasePPORole):
    def init_model_from_pretrained(self, strategy: DeepspeedStrategy, pretrain):
        self._setup_distributed(strategy)
        model = get_llm_for_sequence_regression(...)
        # 3) 初始化模型操作
        ...
    def forward(...):
        ...
        # 4）这里也就是直接将文本输入到模型中进行计算
</code></pre><p>（1、2、3、4）、模型初始化操作，这里没什么好解释的直接通过继承的类（<code>BasePPORole</code>）中对 <code>Deepspeed</code> 进行初始化。模型计算也是比较常规的 <code>forward</code> 计算范式。对于 <code>Actor</code> 操作：<a href="https://www.big-yellow-j.top/posts/2025/04/22/OpenRLHF-1.html#:~:text=1%E3%80%81-,actor.py,-https%3A//github.com">📃</a>。</p><ul><li>2、<code>PPORayActorGroup</code>：创建、初始化和管理多个分布式 Actor，协调它们的模型加载、训练、推理和保存操作</li></ul><pre><code class="language-python">class PPORayActorGroup:
    def __init(
        ...,
        pg: PlacementGroup = None,
        ...):
        ...
        self._initiate_actors(pg, num_gpus_per_actor)
    def _initiate_actors(
        self, 
        pg, # 通过ray创建资源组
        num_gpus_per_actor):
        ...
        self._actor_handlers = [master_actor]

    def async_fit_actor_model(
        self,
        critic_model_group: "PPORayActorGroup",
        initial_model_group: "PPORayActorGroup",
        reward_model_groups: List["PPORayActorGroup"],
        remote_rm_urls: List[str] = None,
        reward_fn: Callable[[List[torch.Tensor]], torch.Tensor] = None,
        vllm_engines: List = None,):
        ...
        critic_actors = critic_model_group._actor_handlers if critic_model_group else None
        initial_actors = initial_model_group._actor_handlers if initial_model_group else None



    def async_init_model_from_pretrained(...):
        # 初始化模型
        return [actor.init_model_from_pretrained.remote(*args, **kwargs) for actor in self._actor_handlers]

    def async_save_model(self):
        # 保存模型
        return [actor.save_model.remote() for actor in self._actor_handlers]

    def async_run_method(self, method_name, *args, **kwargs):
        refs = []
        for actor in self._actor_handlers:
            method = getattr(actor, method_name)
            refs.append(method.remote(*args, **kwargs))
        return refs

</code></pre><p>（1）、在初始化（ <code>async_init_model_from_pretrained</code> ）以及保存（ <code>async_save_model</code> ）中里面所使用的 <code>actor</code> 就是上面提到的 <code>ReferenceModelRayActor</code></p><h2 id="记录">记录</h2><p>https://github.com/Dao-AILab/flash-attention/issues/432#issuecomment-1698610752</p> ]]></content:encoded><category>OpenRLHF框架解读</category><pubDate>Thu, 24 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/04/24/OpenRLHF-2.html</guid></item><item><title>强化学习框架：OpenRLHF源码解读，模型处理</title><link>https://www.big-yellow-j.top/posts/2025/04/22/OpenRLHF-1.html</link><description><![CDATA[本文主要解读强化学习框架OpenRLHF的模型处理及源码设计，重点介绍Actor model、Critic model和Reward model三类核心模型的功能与实现。Actor model负责加载模型并输出response，其forward方法计算生成token的对数概率（log probs），为PPO、DPO等算法提供优化依据；generate方法则根据输入参数（如top_k）生成文本序列。Critic model通过处理token序列输出action_values（values[:, -num_actions:]），用于优势函数评估策略好坏；Reward model则提取最后一个...]]></description><content:encoded><![CDATA[<p>本文主要介绍 <strong>强化学习框架：OpenRLHF源码解读，模型处理</strong></p><h2 id="models框架设计">models框架设计</h2><p>了解一下 <strong>OpenRLHF</strong>的模型框架设计范式：</p><p><img src="https://s2.loli.net/2025/06/21/H9SrnqOkGP5Juzm.webp" alt="image" width="760" height="587" loading="lazy" decoding="async"/></p><blockquote><p>From:https://arxiv.org/pdf/2405.11143</p></blockquote><p>可以知道一个大概的流程：输入Pormpt通过Actor model输出回复 Response，而后将两部分进行拼接再去由其他模型进行处理</p><h3 id="1actorpy">1、actor.py</h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/models/actor.py</p></blockquote><p>这部分主要为加载所需要的模型</p><pre><code class="language-python">class Actor(nn.Module):
    def __init__(...):
        if isinstance(pretrain_or_model, str):
            ...
            self.model = model_class.from_pretrained(
                pretrain_or_model,
                trust_remote_code=True,
                attn_implementation=attn_implementation,
                quantization_config=nf4_config,
                torch_dtype=torch.bfloat16 if bf16 else "auto",
                device_map=device_map,
            )
            if lora_rank &gt; 0:
                self.model.enable_input_require_grads()
                lora_config = LoraConfig(
                    task_type=TaskType.CAUSAL_LM,
                    r=lora_rank,
                    lora_alpha=lora_alpha,
                    target_modules=target_modules,
                    lora_dropout=lora_dropout,
                    bias="none",
                )
                self.model = get_peft_model(self.model, lora_config)
                ...
        else:
            self.model = pretrain_or_model
    @torch.no_grad()
    def generate(self, input_ids: torch.Tensor, **kwargs):
        ...
        sequences = self.model.generate(**generate_args)
        eos_token_id = generate_args["eos_token_id"]
        pad_token_id = generate_args["pad_token_id"]
        return self.process_sequences(sequences, input_ids.size(1), eos_token_id, pad_token_id)
    def forward(...):
        ...
        output["logits"] = output["logits"].to(torch.float32) # 得到每一个token概率
        ...
        log_probs = log_probs_from_logits(
                    output["logits"][:, :-1, :], sequences[:, 1:], temperature=self.temperature
                )
        ...
        action_log_probs = log_probs[:, -num_actions:]

</code></pre><p>这个actor比较简单，<strong>首先</strong>从huggingface加载需要的模型，并且对模型进行部分设置如：量化/lora微调。或者直接加载自己预训练好的模型。 <br/> 1、<code>generate</code>：模块则是根据输入的内容（比如说被 tokenizer处理好的文本）input_ids通过模型<strong>输出新的内容</strong>（根据 <code>**kwargs</code>获取生成文本参数设置比如说：<code>top_k</code>等）<br/> 2、<code>forward</code>：<strong>根据输入的 token 序列（sequences），计算模型在生成最后若干个 token（即 “动作”）时的对数概率（log probs）</strong>，之所以要这么处理是因为，在强化学习模型中（PPO、DPO等）一般而言模型的输出是一个序列，但优化目标不是“能不能生成这个序列”，而是：这个序列中，哪些 token 是“好”的？模型对这些 token 的概率应该更高！比如说在 <strong>DPO</strong>中：</p> \[L(θ) = E[ min(r(θ) * A, clip(r(θ), 1-ε, 1+ε) * A) ]\] <p>里面的</p> \[r(\theta)=\pi_{\theta}(a|s)/\pi_{old}(a|s)\] <p>就是概率比值，上面代码中：</p><pre><code class="language-python">log_probs_from_logits(output["logits"][:, :-1, :], sequences[:, 1:], temperature=self.temperature)
</code></pre><p>计算的就是：$log(\pi_{\theta}(a|s))$，在具体代码中：</p><pre><code class="language-python">def log_probs_from_logits(logits: torch.Tensor, labels: torch.Tensor, temperature: float = 1.0) -&gt; torch.Tensor:
    if temperature != 1.0:
        logits.div_(temperature)
    if logits.dtype in [torch.float32, torch.float64]:
        batch_dim = logits.shape[:-1]
        last_dim = logits.shape[-1]
        try:
            from flash_attn.ops.triton.cross_entropy import cross_entropy_loss

            output = cross_entropy_loss(logits.reshape(-1, last_dim), labels.reshape(-1))
            log_probs_labels = -output[0].view(*batch_dim)
        except ImportError:
            logits_labels = torch.gather(logits, dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
            logsumexp_values = _logsumexp_by_chunk(logits.reshape(-1, last_dim))
            logsumexp_values = logsumexp_values.view(*batch_dim)
            log_probs_labels = logits_labels - logsumexp_values  # log_softmax(x_i) = x_i - logsumexp(x)
    else:
        log_probs_labels = []
        for row_logits, row_labels in zip(logits, labels):  # loop to reduce peak mem consumption
            row_log_probs = F.log_softmax(row_logits, dim=-1)
            row_log_probs_labels = row_log_probs.gather(dim=-1, index=row_labels.unsqueeze(-1)).squeeze(-1)
            log_probs_labels.append(row_log_probs_labels)
        log_probs_labels = torch.stack(log_probs_labels)
    return log_probs_labels
</code></pre><blockquote><p><strong>补充-1</strong>：<br/> 在使用 <code>AutoModelForCausalLM.from_pretrained</code>使用得到 <code>model</code>之后，其支持输入参数为：</p><pre><code class="language-python">outputs = model(
    input_ids=None,            # 输入的token（batch_size, seq_length）
    attention_mask=None,       # 指示哪些 token 是有效的（非 padding），形状同 input_ids
    position_ids=None,         # 位置编码
    past_key_values=None,
    inputs_embeds=None,
    use_cache=None,            # 是否使用k-v cache
    labels=None,               # 输入标签就直接计算loss
    output_attentions=None,
    output_hidden_states=None,
    return_dict=None,
)
</code></pre><p><strong>补充-2</strong>：<br/> 在LLM训练过程中遇到过短的语句为了节约显存（如果都将内容补充到相同长度，那么就会有较多的padding造成浪费），因此可以将几个短的拼接起来，但是为了区分那些是一个句子那些不是的，在 <strong>OpenRLHF</strong>中通过参数：<code>self.packing_samples</code>。如果没有 <code>packing</code>那么直接根据 <code>attention_mask</code>将位置编码在处理一下</p><pre><code class="language-python">if not self.packing_samples:
    position_ids = attention_mask.long().cumsum(-1) - 1
    position_ids.masked_fill_(attention_mask == 0, 1)
else:
    # convert attention_mask to position_ids
    if ring_attn_group is not None:
        labels = sequences
        sequences, attention_mask, position_ids = convert_ring_attn_params(
            sequences, attention_mask, packed_seq_lens, ring_attn_group
        )
    else:
        position_ids = reset_position_ids(attention_mask)
    # explicitly ignore attention_mask for packing_samples
    attention_mask = None
</code></pre><p>其中 <code>reset_position_ids</code>做的就是重新做位置编码重新处理</p></blockquote><h3 id="2modelpy">2、model.py</h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/models/model.py</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/Yjnc5UyVMGLQgBX.webp" alt="image" width="868" height="355" loading="lazy" decoding="async"/></p><p>主要功能返回所需要的模型，主要返回2个模型：1、<code>CriticModel</code>；2、<code>RewardModel</code> 回顾一下这几类模型的作用：无论是在GRPO还是DPO中都会输出token然后需要去对token进行评分，起评分作用的就是 <code>reward model</code> 对应上面图中 <code>reward model</code>，除此之外都会计算 <strong>优势函数</strong>（$Q(s,a)-V(s)$）来评估策略的好坏优势函数里面计算就是通过 <code>critic model</code>来对某一个策略进行评估对应上面图像中的：<code>value model</code></p><pre><code class="language-python">def _get_reward_model(base_pretrained_model, base_llm_model, value_head_prefix="score", packing_samples=False):
    class RewardModel(base_pretrained_model):
        def __init__(...):
            ...
            # 加载模型
            setattr(self, self.base_model_prefix, base_llm_model(config))
            self.value_head_prefix = value_head_prefix
            setattr(self, value_head_prefix, nn.Linear(config.hidden_size, 1, bias=False) # 输出评分
            ...
        def forward(self, input_ids: torch.LongTensor = None, attention_mask: Optional[torch.Tensor] = None, return_output=False, ring_attn_group=None,pad_sequence=False, packed_seq_lens=None,):
            ...# 1、处理packing
            outputs = getattr(self, self.base_model_prefix)(
                input_ids, attention_mask=attention_mask, position_ids=position_ids
            )
            last_hidden_states = outputs["last_hidden_state"]
            values = getattr(self, self.value_head_prefix)(last_hidden_states).squeeze(-1)
            ...# 1、处理packing
            else:
                # 输出最后一个有效token的评分代替整个句子评分
                eos_indices = attention_mask.size(1) - 1 - attention_mask.long().fliplr().argmax(dim=1, keepdim=True)
                reward = values.gather(dim=1, index=eos_indices).squeeze(1)
            if not self.training and self.normalize_reward:
                reward = (reward - self.mean) / self.std
            return (reward, outputs) if return_output else reward
    return RewardModel

def _get_critic_model(base_pretrained_model, base_llm_model, value_head_prefix="score", packing_samples=False):
    class CriticModel(base_pretrained_model):
        def __init__(...):
            ...
        def forward(...):
            ...# 1、处理packing
            outputs = getattr(self, self.base_model_prefix)(
                input_ids, attention_mask=attention_mask, position_ids=position_ids
            )
            last_hidden_states = outputs["last_hidden_state"]
            values = getattr(self, self.value_head_prefix)(last_hidden_states).squeeze(-1)
            ...
            if num_actions is None:
                assert return_output
                return outputs
            if not self.packing_samples:
                action_values = values[:, -num_actions:]
            else:
                assert isinstance(num_actions, list) and len(num_actions) == len(packed_seq_lens)
                action_values = []
                offset = 0
                for num_action, seq_len in zip(num_actions, packed_seq_lens):
                    start, end = max(0, offset + seq_len - num_action - 1), offset + seq_len - 1
                    action_values.append(values[:, start:end])
                    offset += seq_len
                action_values = torch.cat(action_values, dim=1)

            if return_output:
                return (action_values, outputs)
            else:
                return action_values

    return CriticModel
</code></pre><p>1、<code>reward model</code>: 传入一个 base_pretrained_model（比如 PreTrainedModel）、一个 base_llm_model（比如 AutoModel）以及一些控制参数。函数内部返回一个定制化的奖励模型类 RewardModel，它可以在给定输入句子时，<strong>输出一个数值（reward 分数），反映输出文本的质量</strong>。在forward计算中，直接将输入model使用的几个参数（见上面的补充有具体解释）计算最后取最后一个状态的值，并且将这个值取计算评分。也就是说 reward model：<strong>首先计算下一个预测的token而后对这些token进行打分</strong><br/> 2、<code>critic model</code>：具体输入参数和 <code>reward model</code>相同。参考之前<a href="https://www.big-yellow-j.top/posts/2025/03/23/DPO-PPO.html#:~:text=%E5%AF%B9%E4%BA%8E%E4%B8%8A%E8%BF%B0%E5%85%AC%E5%BC%8F,%E5%B9%B3%E5%9D%87%E7%B4%AF%E8%AE%A1%E6%9C%9F%E6%9C%9B">介绍</a>，上面代码中直接返回<code>action_values = values[:, -num_actions:]</code>（ <code>num_actions</code>存在条件下）这样就会得到不同的Q(s, a1), Q(s, a2), …</p><blockquote><p><strong>总结上面两组模型</strong>，在 LLM 的强化学习场景下，Reward Model 和 Critic Model 都从 last_hidden_state 得到 token-level 表达，再用 Linear 层输出每个 token 的 score。<br/><code>Reward Model</code> 最后提取的是 EOS token 的 score，表示整句话的奖励。<br/><code>Critic Model</code> 会进一步提取最后 num_actions 个 token 的 value，这些 token 是 Actor 生成的动作，对应到 PPO 中的：𝐴(𝑠,𝑎)=𝑄(𝑠,𝑎)−𝑉(𝑠)。</p></blockquote><p>理解上面内容，回顾最上面的框架设计，用下面例子进行解释。<br/> Prompt：<code>"The capital of France is"</code><br/> Actor model：<code>"Paris is beautiful"</code>。那么合并得到：<code>input_ids = ["The", "capital", "of", "France", "is", " Paris", "is", "beautiful"]</code><br/> Reward model：对上面每个单词进行评分，假设：<code>values = [0.1, 0.2, 0.3, 0.2, 0.4, 0.7, 0.5, 0.8] # 每个 token 的 score</code> 而后输出句子中整体评分 0.8<br/> Critic model：只对最后几个 token 的 action 计算 loss，于是：<code>action_values = values[:, -3:] # 即取出最后 3 个生成 token 的 Q 值</code>这些值也就对应了我们模型的生成</p><p>理解完两个模型定义以及处理范式之后，作者直接通过 <code>get_llm_for_sequence_regression</code> 来使用上面所定义的 <code>_get_reward_model</code> 和 <code>_get_critic_model</code> 操作也比较简单：</p><pre><code class="language-python">def get_llm_for_sequence_regression(
    model_name_or_path,
    model_type,
    ...):
    ...
    # 1）通过model_type 选择所使用的模型
    if model_type == "reward":
        cls_class = _get_reward_model(base_pretrained_class, base_class, value_head_prefix, packing_samples)
    else:
        cls_class = _get_critic_model(base_pretrained_class, base_class, value_head_prefix, packing_samples)
    ...
    # 2）加载模型
    model = cls_class.from_pretrained(
        model_name_or_path,
        config=config,
        trust_remote_code=True,
        torch_dtype=torch.bfloat16 if bf16 else "auto",
        quantization_config=nf4_config,
        device_map=device_map,
        **kwargs,
    )
    ...
    return model
</code></pre><h3 id="3losspy">3、loss.py</h3><blockquote><p>https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/models/loss.py</p></blockquote><blockquote><p><strong>补充-1：</strong><br/> 裁剪使用的是<code>torch.clamp</code>（https://pytorch.org/docs/stable/generated/torch.clamp.html）强制将范围外的数值处理为边界值，范围内数字保持不变</p></blockquote><p>1、<code>PolicyLoss</code>：Policy Loss for PPO</p> \[\begin{align*} r_t &amp;= \exp(\log \pi(a_t \mid s_t) - \log \pi_{\text{old}}(a_t \mid s_t)) \\ \mathcal{L}_{\text{clip}}(t) &amp;= \min\left(r_t \cdot A_t,\ \text{clip}(r_t,\ 1 - \epsilon,\ 1 + \epsilon) \cdot A_t\right) \\ \mathcal{L}_{\text{policy}} &amp;= -\mathbb{E}_t \left[ \mathcal{L}_{\text{clip}}(t) \right] \end{align*}\] <p>2、<code>ValueLoss</code>: Value Loss for PPO</p> \[\mathcal{L}_{\text{value}} = \frac{1}{2} \cdot \mathbb{E}_{t \sim \text{mask}} \left[ \max \left( (V_{\text{clip}, t} - R_t)^2, \, (V_t - R_t)^2 \right) \right]\\ \text{其中：}V_{\text{clip}} = V_{\text{old}} + \text{clip}(V - V_{\text{old}}, -\epsilon, \epsilon)\] <h2 id="代码测试">代码测试</h2><p>修改了代码见链接：<a href="https://www.big-yellow-j.top/code/OpenRLHF_model.py.txt">https://www.big-yellow-j.top/code/OpenRLHF_model.py</a></p><h2 id="总结">总结</h2><p>本文主要介绍了在 <strong>OpenRLHF</strong>中模型框架设计，主要分为3类模型：1、<code>actor model</code>；2、<code>critic model</code>；3、<code>reward model</code>这三类模型中分别起到作用：1、直接更具prompt输出response；2、输出token的评分（<code>action_values = values[:, -3:]</code>）；3、返回整句输出评分（找出最后一个有效 token 的索引，然后从 value 向量中提取该位置的值作为 reward。）</p> ]]></content:encoded><category>OpenRLHF框架解读</category><pubDate>Tue, 22 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/04/22/OpenRLHF-1.html</guid></item><item><title>多视觉编码器协同与高低分辨率特征融合技术综述</title><link>https://www.big-yellow-j.top/posts/2025/04/19/MultiVisEncoding.html</link><description><![CDATA[本文主要介绍多模态中多视觉编码器的特征融合方法及高低分辨率图像信息融合策略。针对多视觉编码器处理同一张图像的特征融合，常见策略包括纵向拼接、横向拼接、融合拼接及注意力拼接，相关研究如LEO模型采用动态分辨率划分图像块，通过Intern-Vit和SAM-L编码器处理后，使用Sequence Concatenation横向拼接特征；EAGLE模型探索视觉编码器组合及特征融合设计空间，发现Channel Concat融合效果最优。对于高低分辨率图像融合，LLaVA-HR提出混合分辨率适应方法，高分辨率图像用卷积网络处理、低分辨率用ViT，通过MR-Adapter操作平衡计算量与细节捕捉；Min...]]></description><content:encoded><![CDATA[<p>本文主要介绍（论文发表时间：24.03-25.01）在多模态中使用<strong>多个视觉编码器如何进行特征融合操作</strong>（之所以用多视觉编码器，主要用途在于：有些视觉编码器可能只能提取到部分信息，就想通过另外一个编码器去捕捉之前编码器所没有捕捉得到的信息；另外一种情况就是：同时输入两类输入图像：1、高分辨率图像；2、低分辨率图像。去将这两类图像编码之后图如何处理这两部分图像信息），因为视觉编码器处理的特征都是“相同”的（都是视觉维度上的内容）因此在拼接上所处理的操作也会比较单一，比如说下面这类操作：<br/><img src="https://s2.loli.net/2025/06/21/vjK7YWR4nxTHuLq.webp" alt="image" width="1228" height="262" loading="lazy" decoding="async"/></p><p>对于每类操作都会有一篇论文进行介绍：<br/> 1、<a href="#2eagle">Channel Concatenation</a>：纵向拼接（Image-Encoder-1：1x4；Image-Encoder-2：1x4；得到2x4）<br/> 2、<a href="#1leo">Sequence Concatenation</a>：横向拼接（Image-Encoder-1：1x4；Image-Encoder-2：1x4；得到1x8）<br/> 3、<a href="#3llava-hr">MR-Adapter</a>：融合拼接<br/> 4、<a href="#4mini-gemini">Cross-Attention</a>：注意力拼接</p><h2 id="1leo">1、LEO</h2><blockquote><p>LEO: Boosting Mixture of Vision Encoders for Multimodal Large Language Models<br/> From: https://arxiv.org/pdf/2501.06986</p></blockquote><p>模型框架：</p><p><img src="https://s2.loli.net/2025/06/21/lDNFb5Z9cndY3Oj.webp" alt="image" width="460" height="378" loading="lazy" decoding="async"/></p><p>套路还是往常老套路，输入图像以及问题文本然后分别将两种模态信息进行编码然后进行拼接输入到LLM中进行处理。在处理图像过程中一如既往的使用动态分辨率操作，划分得到不同的小patch之后，就会输入到视觉编码器中，这里使用的是两种：1、Intern-Vit；2、SAM-L通过编码器处理然后通过<strong>Pixel unshuffle操作</strong>（<a href="https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html#:~:text=%E8%A1%A5%E5%85%85%E4%B8%80%E7%82%B9%EF%BC%9A-,%E4%BA%9A%E5%83%8F%E7%B4%A0%E4%B8%8A%E9%87%87%E6%A0%B7%20(Pixel%20Shuffle),-%EF%BC%9A%E6%99%AE%E9%80%9A%E7%9A%84%E4%B8%8A">主要是为了减少token数量</a>）。文章中关键两点在于特征融合模块。</p><blockquote><p><strong>动态分辨率</strong>：首先提前设定切分图像的块的大小以及需要划分块的数量，比如说：切割的每块的分辨率为：224x224以及切分块的数量为：6，而后计算输入图像（假设：800x1300）如果切分到指定块需要修改的分辨率，比如说：224x6/800≈2那么对于宽扩展2倍，类似的长就会扩展3倍，这样分辨率就会变为：896x1344<br/><img src="https://s2.loli.net/2025/06/21/aenZ2hid3yLw4At.webp" alt="image" width="402" height="252" loading="lazy" decoding="async"/></p></blockquote><p><strong>特征融合</strong>：对图像进行encoder之后，类似很多多模态中进行操作一样都会通过一个Projector（也就是简单的线性层进行处理），多视觉编码器处理完毕之后后续关键就是如何将编码后内容进行组合，在本文中所采用的方式是：<strong>Sequence Concatenation</strong>。也就是直接将内容进行横向拼接（比如说：image-encoder-1：1x4；image-encoder-2：1x4。那么拼接得到结果为：1x8）</p><p><img src="https://s2.loli.net/2025/06/21/vjK7YWR4nxTHuLq.webp" alt="image" width="1228" height="262" loading="lazy" decoding="async"/></p><h2 id="2eagle">2、EAGLE</h2><blockquote><p>EAGLE: Exploring The Design Space for Multimodal LLMs with Mixture of Encoders<br/> From: https://arxiv.org/pdf/2408.15998</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/1CzNWlPgDsRrhTm.webp" alt="image" width="1603" height="637" loading="lazy" decoding="async"/></p><p>本文就是一个研究实验报告主要是讨论了：1、各种视觉编码器进行组合效果；2、各类特征融合效果。因此对于该篇论文只需要介绍解实验结果以及实验过程：</p><ul><li>1、视觉编码器类型</li></ul><p>本文主要是分别使用了如下几类视觉编码器：<br/><img src="https://s2.loli.net/2025/06/21/ejH7pqsn15J6IxB.webp" alt="image" width="461" height="219" loading="lazy" decoding="async"/></p><ul><li>2、特征融合策略</li></ul><p>本文主要是分别使用了如下几类特征融合策略：<br/><img src="https://s2.loli.net/2025/06/21/8evCoIBuDhjHyLl.webp" alt="image" width="344" height="141" loading="lazy" decoding="async"/><br/> 从结果上，发现Channel Concat得到的效果是最好的（对比那些花里胡哨的各种混合策略而言）</p><h2 id="3llava-hr">3、LLaVA-HR</h2><blockquote><p>Feast Your Eyes: Mixture-of-Resolution Adaptation for Multimodal Large Language Models<br/> From: https://arxiv.org/pdf/2403.03003</p></blockquote><p>模型框架：<br/><img src="https://s2.loli.net/2025/06/21/J1RWESuQ96ecTfU.webp" alt="image" width="734" height="254" loading="lazy" decoding="async"/></p><p>本文出发点在于：<strong>如何去处理多模态模型对于“细微”信息的捕捉能力</strong>（比如说图像中很细小的一个内容，如何争对这部分内容，模型能否捕捉到这部分信息），比较常见的作法就是：用高分辨率图像作为输入。使用高分辨率图像就会带来许多问题，比如说分辨率增加就会导致计算增加等。本文就提出：<strong>混合分辨率操作</strong>（<em>Mixture-of-Resolution Adaptation</em>）<br/> 本文作法也比较简单：对于高分辨率图像使用：卷积进行操作；对于低分辨率图像使用：Vit进行操作。这样做的目的在于Vit是全局注意力，视觉感受野比较大，如果用到高分辨率图像上形成的token数量就会比较多。而卷积是局部感受野，能够高效捕捉到局部特征，并且计算量上也不会太高。</p><p><img src="https://s2.loli.net/2025/06/21/GgEPmrJWtZa5jUy.webp" alt="image" width="425" height="272" loading="lazy" decoding="async"/></p><p>本文中使用的 <em>MR-Adapter</em> 操作：$F_{vl}=F_{vl}+f_l(F_{vl})+g\times f_h(F_{vh})$。处理思路也比较简答，通过公式也可以很直观理解（其中：$vl$ 代表低分辨率；$vh$ ：代表高分辨率，$f_l, f_h$：则是分别代表卷积block和 MLP layer）值得注意的是其中的gate定义为：</p> \[\begin{aligned} &amp; g=\delta(W_{2}\sigma(W_{1}f_{v})), \\ &amp; f_{v}=\frac{1}{h\times w}\sum_{i}^{h}\sum_{j}^{w}[f_{l}(\mathbf{F}_{vl})^{i,j},f_{h}(\mathbf{F}_{vh})^{i,j}] \end{aligned}\] <p>总的来说本文亮点在于使用高分辨率同时在推理速度以及显存占用上都是比较少的：</p><p><img src="https://s2.loli.net/2025/06/21/XH6lCwNfh4gGTcp.webp" alt="image" width="1226" height="396" loading="lazy" decoding="async"/></p><h2 id="4mini-gemini">4、Mini-Gemini</h2><blockquote><p>Mini-Gemini: Mining the Potential of Multi-modality Vision Language Models<br/> From: https://arxiv.org/pdf/2403.18814</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/TleIB7FYgwGSUyi.webp" alt="image" width="1107" height="378" loading="lazy" decoding="async"/></p><p>在介绍的 <a href="#2llava-hr"><strong>LLaVA-HR</strong></a> 中使用的是结合高分辨率图像和低分辨率图像作为输入，本文也是采用高分辨率+低分辨率（直接对高分辨率图像通过bilinear来降低分辨率）作为输入，在高/低分辨率处理上也是相似的（高分辨率：卷积网络处理；低分辨率：ViT）。不过在本文中对于不同分辨率图像“预处理”和前一篇论文有点区别，本文处理分辨率图像，假设输入高分辨率图像：$X_H = H\times W\times C$。低分辨率图像：$X_L= h\times w\times C$。低分辨率通过Vit模型处理得到：$X_L=N\times C$ 其中 $N$ 代表patch数量，对于高分辨图像处理得到：$X_H=N\times N\times C$ 其中 $N=H/4 \times W/4=N\times M^2$。也就是说在地分辨中的每个patch都会有一块高分辨率图像作为“信息补偿”</p><p><img src="https://s2.loli.net/2025/06/21/up8ZfoCWbEYOPKw.webp" alt="image" width="803" height="253" loading="lazy" decoding="async"/></p><p>特征融合直接通过注意力方式融合即可，其中Q为低分辨率的图像信息，K/V都来自高分辨率图像信息：$T_V=MLP(Q+Attention(Q,K,V))$</p><h2 id="总结">总结</h2><p>本文主要介绍了多视觉内容处理思路：1、多视觉编码器处理同一张图像如何将处理后图像特征进行融合；2、高/低分辨率图像如何将两部分信息进行融合。其中第2点工作（高低分辨率）具有的借鉴意义更加大。</p><h2 id="参考">参考</h2><p>1、<a href="https://arxiv.org/pdf/2501.06986">LEO: Boosting Mixture of Vision Encoders for Multimodal Large Language Models</a><br/> 2、<a href="https://arxiv.org/pdf/2312.14238">InternVL: Scaling up Vision Foundation Models and Aligning for Generic Visual-Linguistic Tasks</a><br/> 3、<a href="https://arxiv.org/pdf/2404.07204">BRAVE : Broadening the visual encoding of vision-language models</a><br/> 4、<a href="https://arxiv.org/pdf/2408.15998">EAGLE: Exploring The Design Space for Multimodal LLMs with Mixture of Encoders</a><br/> 5、<a href="https://arxiv.org/pdf/2403.18814">Mini-Gemini: Mining the Potential of Multi-modality Vision Language Models</a></p> ]]></content:encoded><category>paper</category><pubDate>Sat, 19 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/04/19/MultiVisEncoding.html</guid></item><item><title>Python进阶知识：多进程/多线程/装饰器</title><link>https://www.big-yellow-j.top/posts/2025/03/22/PythonThreading.html</link><description><![CDATA[本文详细解释了Python中多进程、多线程、装饰器的核心概念、应用场景及使用方法。多进程拥有独立内存空间，适用于CPU密集型任务；多线程共享内存资源，适合I/O密集型任务；装饰器则能在不修改原函数代码的情况下添加额外功能，常用@语法实现。文章还介绍了全局解释器锁（GIL）的作用，其限制Python线程并行执行，导致多线程在CPU密集型任务中性能提升有限。通过将8000000个1相加的案例，对比了普通处理、串行分块、多线程（ThreadPoolExecutor）和多进程（ProcessPoolExecutor）的效率，分析得出：多线程受GIL影响适合I/O密集型任务，多进程虽能绕过GIL但...]]></description><content:encoded><![CDATA[<p>本文写作于2025.3.20，恰好作者正好在外面实习，于此同时在实际工作中遇到这些知识点，因此就进行一个简短汇总方便后续回顾，可以直接看<a href="#3如何在代码中使用多进程多线程装饰器">第三节</a></p><h2 id="1简短理解一下什么是多进程多线程装饰器和一些基本概念">1、简短理解一下什么是多进程/多线程/装饰器和一些基本概念</h2><p><strong>多进程</strong>：指在同一个程序中同时运行多个独立的进程。每个进程都有自己的内存空间和资源，互不干扰。常用在CPU密集型任务中。<br/> 比如说：比如你打开了多个浏览器窗口，每个窗口就是一个独立的进程，互不影响。即使一个窗口崩溃，其他窗口也不会受到影响。<br/><strong>多线程</strong>：指在同一个进程内同时运行多个线程，多个线程共享同一块内存空间。适合<strong>I/O密集型任务</strong>（主要涉及到输入输出操作的任务。这些任务的执行时间主要花费在等待IO操作的完成上），线程之间的切换比进程更轻便。<br/> 比如说：你在看视频的同时，后台也在下载文件。这些操作都是通过不同的线程完成的，视频播放和下载互不干扰。<br/><strong>装饰器</strong>：一种特殊的函数，能在不修改原函数代码的情况下，给函数添加额外的功能。<br/> 比如说：在一个函数前后自动记录执行时间或日志，常用 @decorator_name 语法<br/><strong>全局解释器锁</strong>（GIL）：它使得任何时刻仅有一个线程在执行。即便在多核心处理器上，使用 GIL 的解释器也只允许同一时间执行一个线程。常见的使用 GIL 的解释器有CPython与Ruby MRI。</p><h2 id="2在实际任务中使用多进程多线程">2、在实际任务中使用多进程/多线程</h2><p>假设有一个任务是将8000000个数字1相加，我们提前假设我们已经构建好了这样一个数组，并且我们需要记录一下代码运行需要的时间，普通处理思路：</p><pre><code>start_time = time.time()
sum_num = 0
for i in num_list:
    sum_num += i
print(f"Used Time:{time.time()- start_time}")
</code></pre><p>需要时间：<code>Used Time:0.9650969505310059</code>，那么有一个思路我先去把num_list拆分为8份（因为数字相加并不会冲突，用分块知识解决）然后计算时间 <code>Used Time:0.07107281684875488</code>，但是这样数据计算是串行的（执行完第一块，然后去计算第二块），那么我们可以考虑多线程直接8块一起计算然后将最后结果汇总起来，这样得到的时间为：<code>Used Time:0.09244751930236816</code>，于此同时使用多进程计算得到结果：<code>Used Time:0.854262113571167</code>。这样就会又一个有意思现象，理论上来说多进程，多线程速度应该是都大于常规的分割法，出现这个原因是因为：</p><ol><li><p><strong>GIL（全局解释器锁）影响</strong><br/> Python 的 <code>ThreadPoolExecutor</code> 受 <strong>GIL 限制</strong>，多个线程并不会真正并行执行，而是<strong>交替运行</strong>，因此它比单线程快，但 <strong>提升有限</strong>。<br/> 由于 <code>sum</code> 计算是 <strong>CPU 密集型任务</strong>，线程池无法充分发挥 CPU 多核优势，导致性能 <strong>不如串行分块计算</strong>。</p></li><li><p><strong>进程间通信（IPC）开销</strong><br/><code>ProcessPoolExecutor</code> 会 <strong>为每个进程创建独立的 Python 解释器</strong>，数据需要 <strong>在主进程和子进程之间传输</strong>，但 <code>num_list</code> 非常大，导致 <strong>数据传输和进程调度成本过高</strong>，反而影响性能。</p></li><li><p><strong>任务拆分的额外开销</strong><br/> 由于 <code>sum</code> 操作本身非常简单，计算时间短，线程池和进程池的 <strong>管理开销</strong>（线程/进程创建、调度、回收）可能超过计算本身的成本，导致整体运行时间反而变长。用人话来说就是，使用多进程，就需要考虑到通信的花销，用多线程就要考虑到 <strong>GIL</strong>限制，换言之得到的<strong>结论就是</strong>：<br/><strong>多线程（ThreadPoolExecutor）</strong> 适用于 <strong>I/O 密集型任务</strong>（如文件读写、网络请求），但 <strong>CPU 计算任务受 GIL 限制</strong>，提升有限。<br/><strong>多进程（ProcessPoolExecutor）</strong> 适用于 <strong>CPU 密集型任务</strong>，但数据传输开销大，对 <strong>短时间计算任务</strong> 可能不适用。</p></li></ol><h2 id="3如何在代码中使用多进程多线程装饰器">3、如何在代码中使用多进程/多线程/装饰器</h2><h3 id="1多线程使用"><strong>1、多线程使用</strong></h3><p>多线程使用方式比较简单，以下面例子为例：</p><pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers= n) as executor:
    futures = [executor.submit(sum, chunk) for chunk in chunks]  # 提交任务
    results = [future.result() for future in as_completed(futures)]  # 获取结果
</code></pre><p>当软还有另外一种执行方式：</p><pre><code class="language-python">import threading
thread_1 = threading.Thread(target= sum)
thread_2 = threading.Thread(target= sum)

thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

</code></pre><p>第一种相对而言比较简单（自动管理线程），而第二种需要我去创建多个进程，然后对不同进程之间进行 <code>start()</code> 以及 <code>join()</code>，实际使用如果是一个长期执行任务可以用 <code>threading.Thread</code>（比如说要一致保持摄像头开启就可以直接 <code>threading.Thread(target=video_capture_thread, daemon=True).start()</code> ）而并行任务可以选择 <code>ThreadPoolExecutor</code>不用去手动创建</p><p>一般来说使用过程中只需要注意如下几个操作：1、向你创建的进程中提交任务（提交的内容是：你要进行计算的函数，函数所需要的参数）；2、获取你提交任务所得到的结果（因为是多线程，因此返回得到的结果也就是不同线程的结果）<br/> 需要注意的就是下面几个内容：1、<code>submit</code><strong>提交你的任务</strong>；2、<code>as_completed</code><strong>执行你的任务</strong><br/><strong>不过需要小心的一点是</strong>，使用多线程，需要保证 thread-safe（线程安全），比如说同时向一个文件里面写入时候，我通过使用LLM的api执行时候，我有一个较长的文本，先将他拆分（保证是模型的最大允许输入），然后“一次性”（假设的是线程数量恰好和分割数量一致）将其进行api访问（这样时间消耗肯定比普通的要少）将处理结果然后写入到一个文件中就需要考虑进程锁问题，因为所有任务结果都写入同一个问题可能会导致 <strong>进程冲突</strong>，比如说：</p><pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def llm_api_result(num):
    time.sleep(2)
    return f"{num}"*100

def write_to_file(num):
    content = f"Thread-{num}: " + llm_api_result(num)
    with open("./output-without-lock.txt", "a", encoding= "utf-8") as f:
        f.write(content)
        f.write("\n")

def main():
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [executor.submit(write_to_file, num) for num in range(10)]
        for future in futures:
            future.result()

if __name__ == "__main__":
    start_time = time.time()
    main()
    print("Used Time:", time.time()- start_time)
</code></pre><p>这样一来得到的结果为：</p><p><img src="https://s2.loli.net/2025/06/21/TQYPULj4feDlRoq.webp" alt="image" width="560" height="203" loading="lazy" decoding="async"/></p><p>但是上面代码中并没有对进程加锁（<code>lock = threading.Lock()</code>），但是结果还是可以正常（<code>write</code>是一个 <strong>原子操作</strong> ）写入（有时候会出现遗漏掉内容），但是写入顺序是不对的。</p><blockquote><p><strong>原子操作</strong>（atomic operation） 指的是 不可被中断的操作，它要么 完整执行，要么 完全不执行</p></blockquote><p>将代码改为下面代码，通过使用进程锁来保护<strong>原子操作</strong>：</p><pre><code class="language-python">import threading
lock = threading.Lock()
def write_to_file(num):
    content = f"Thread-{num}: " + llm_api_result(num)
    with lock:
        with open("output-with-lock.txt", "a", encoding="utf-8") as f:
            f.write(content)
            f.write("\n")
</code></pre><p><img src="https://s2.loli.net/2025/06/21/jTesJLGzhri7bmY.webp" alt="image" width="404" height="213" loading="lazy" decoding="async"/></p><p>这样一来就可以正常写入结果</p><h3 id="2多进程使用"><strong>2、多进程使用</strong></h3><p>Python 的 multiprocessing 模块基于 fork 或 spawn 机制，可以创建多个独立进程，让它们并行执行任务，从而绕过<strong>GIL（全局解释器锁）</strong>，提高 CPU 密集型任务的性能（数学运算、数据处理等）。使用起来也比较简单</p><p>一个具体例子：</p><pre><code class="language-python">import time
import multiprocessing
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

# 计算斐波那契数列的函数
def fibonacci(n):
    if n &lt;= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

def single_process():
    start_time = time.time()
    for _ in range(4):
        fibonacci(35)
    end_time = time.time()
    print(f"Single-process time: {end_time - start_time:.2f} seconds")

def multi_thread():
    start_time = time.time()
    with ThreadPoolExecutor(max_workers= 4) as executor:
        futures = [executor.submit(fibonacci, 35) for _ in range(4)]
        result = [future.result() for future in futures]
    end_time = time.time()
    print(f"Multi-thread time: {end_time - start_time:.2f} seconds")

def multi_process1():
    start_time = time.time()
    processes = []
    for _ in range(4):
        process = multiprocessing.Process(target=fibonacci, args=(35,))
        processes.append(process)
        process.start()
    for process in processes:
        process.join()
    end_time = time.time()
    print(f"Multi-process-1 time: {end_time - start_time:.2f} seconds")

def multi_process2():
    start_time = time.time()
    with ProcessPoolExecutor(max_workers= 4) as executor:
        futures = [executor.submit(fibonacci, 35) for _ in range(4)]
        result = [future.result() for future in futures]
    end_time = time.time()
    print(f"Multi-process-2 time: {end_time - start_time:.2f} seconds")

if __name__ == "__main__":
    single_process()
    multi_thread()
    multi_process1()
    multi_process2()

Single-process time: 8.93 seconds
Multi-thread time: 9.89 seconds
Multi-process-1 time: 3.81 seconds
Multi-process-2 time: 3.67 seconds
</code></pre><p>python里面使用多进程和多线程代码上没有多大区别，只不过使用多进程需要注意的是上面代码使用必须（在window系统上）要用到下面代码，但是linux系统就没有这个问题，这是因为<a href="https://docs.python.org/zh-cn/3.13/library/multiprocessing.html#multiprocessing-programming:~:text=%E6%A0%B9%E6%8D%AE%E4%B8%8D%E5%90%8C%E7%9A%84%E5%B9%B3%E5%8F%B0%EF%BC%8C%20multiprocessing%20%E6%94%AF%E6%8C%81%E4%B8%89%E7%A7%8D%E5%90%AF%E5%8A%A8%E8%BF%9B%E7%A8%8B%E7%9A%84%E6%96%B9%E6%B3%95">两种启动进程的方式是不同的</a>。</p><pre><code class="language-python">if __name__ == "__main__":
</code></pre><p>这是因为创建子进程时，会重新导入主模块。如果不将多进程代码放在 <code>if __name__ == "__main__":</code> 块中，可能会导致递归创建子进程，甚至引发程序崩溃。更加底层的原因可以直接参考python<a href="https://docs.python.org/zh-cn/3.13/library/multiprocessing.html">官方解释</a>。更加进一步的理解 python<strong>多进程</strong></p><p><strong>首先</strong>在 Python 的多进程编程中，进程之间是相互独立的，它们不能直接共享内存。为了在不同的进程之间传递数据，通常需要将数据序列化为字节流，然后在目标进程中反序列化。pickle（更加详细描述<a href="https://docs.python.org/zh-cn/3.12/library/pickle.html">🔗</a>） 就是用来完成这个任务的，言外之意就是说需要通过 pickle 来将不同进程之间进行传递数据。比如说：</p><pre><code class="language-python">import multiprocessing

def square(number):
    return number ​** 2

if __name__ == "__main__":
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    # 创建一个进程池，使用 4 个子进程
    with multiprocessing.Pool(processes=4) as pool:
        # 使用 map 方法将 square 函数应用到 numbers 列表中的每个元素
        # map 方法会将任务分配给子进程并行执行
        results = pool.map(square, numbers)
    print("原始列表:", numbers)
    print("平方结果:", results)
</code></pre><p>上面例子中 <strong>pickle</strong>（multiprocessing 模块会自动使用 pickle 来<strong>序列化</strong>（<strong>一般而言</strong>：基本数据类型，列表，元组，字典等容器类型，自定义类的实例，函数（但不包括函数中引用的外部对象，如文件对象、数据库连接等））和反序列化）如下内容：<code>square</code> 函数（传递给子进程）。<code>numbers</code> 列表（传递给子进程）。<code>results</code> 列表（从子进程返回给主进程）。</p><h3 id="3装饰器"><strong>3、装饰器</strong></h3><p>装饰器（Decorator）是一种用于修改函数或类行为的高级 Python 语法。它本质上是一个高阶函数，可以在不修改原函数代码的情况下，动态地添加功能。主要作用减少重复代码等，说人话就是<strong>将函数作为一种参数输入到函数中</strong>。使用方法很简单直接在需要使用的函数上面添加 <code>@装饰器</code> 即可。<br/> 比如说，要计算一个函数运行时间一般而言会通过：</p><pre><code class="language-python">import time

def test():
    print("Hello!")

def main():
    start_time = time.time()
    test()
    print(f"Used Time: {time.time()- start_time}")
</code></pre><p>但是如果定义一个计算时间函数，比如说：</p><pre><code class="language-python">def com_time(func):
    start_time = time.time()
    func()
    print(f"Used Time: {time.time()- start_time}")

def main():
    com_time(test)
</code></pre><p>这个里面就是将函数 <code>test</code> 作为一个参数进行输入，更加简便的方法就是直接使用装饰器，比如说：</p><pre><code class="language-python">def com_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 记录开始时间
        result = func(*args, **kwargs)  # 执行被装饰的函数
        print(f"Used Time: {time.time() - start_time:.4f} 秒")  # 计算并输出执行时间
        return result
    return wrapper

@com_time # 等价于 com_time(test)
def test():
    time.sleep(0.5)
    print("Hello!")

def main():
    test()
</code></pre><p>值得注意的是，如果直接下面定义：</p><pre><code class="language-python">def com_time(func):
    start_time = time.time()
    func()
    print(f"Used Time: {time.time()- start_time}")
</code></pre><p>会出现错误，这是因为装饰器中的代码逻辑。具体来说，你在装饰器内部直接调用了 func()，而装饰器的正确用法应该是<strong><em>返回一个包装函数，来替代原函数的执行</em></strong></p><hr/><p>Python一些特殊<a href="https://docs.python.org/zh-cn/3.12/library/stdtypes.html#definition.__name__:~:text=%E5%88%87%E7%89%87%E5%AF%B9%E8%B1%A1%E7%AD%89%E3%80%82-,%E7%89%B9%E6%AE%8A%E5%B1%9E%E6%80%A7,-%C2%B6">属性</a></p><ul><li><ol><li>对象和类的特殊属性（这些属性通常出现在 <strong>类</strong> 和 <strong>对象</strong> 中）</li></ol></li></ul><table><thead><tr><th><strong>属性</strong></th><th><strong>作用</strong></th></tr></thead><tbody><tr><td><code>__dict__</code></td><td>返回对象的属性字典（仅适用于具有 <code>__dict__</code> 的对象）。</td></tr><tr><td><code>__class__</code></td><td>获取对象的类。</td></tr><tr><td><code>__bases__</code></td><td>获取类的所有基类（仅适用于类）。</td></tr><tr><td><code>__name__</code></td><td><strong>返回类或模块的名称</strong>。</td></tr><tr><td><code>__module__</code></td><td>指示类定义所在的模块。</td></tr><tr><td><code>__mro__</code></td><td>返回方法解析顺序（Method Resolution Order）。</td></tr></tbody></table><ul><li><ol><li>模块和文件的特殊属性（这些属性通常用于 <strong>模块</strong> 和 <strong>文件</strong> 级别）</li></ol></li></ul><table><thead><tr><th><strong>属性</strong></th><th><strong>作用</strong></th></tr></thead><tbody><tr><td><code>__file__</code></td><td>返回模块的文件路径（仅适用于 Python 脚本）。</td></tr><tr><td><code>__name__</code></td><td>返回模块的名称，主程序运行时返回 <code>"__main__"</code>。</td></tr><tr><td><code>__package__</code></td><td>返回模块的包名，若模块不是包的一部分，则为 <code>None</code>。</td></tr><tr><td><code>__doc__</code></td><td>返回模块、类或函数的文档字符串（Docstring）。</td></tr><tr><td><code>__annotations__</code></td><td>返回函数的参数和返回值的注解字典。</td></tr></tbody></table><ul><li><ol><li>运行时相关的特殊属性（这些属性与 <strong>Python 运行时</strong> 有关）</li></ol></li></ul><table><thead><tr><th><strong>属性</strong></th><th><strong>作用</strong></th></tr></thead><tbody><tr><td><code>__import__</code></td><td>负责导入模块（通常使用 <code>import</code> 语句，而不是直接调用）。</td></tr><tr><td><code>__builtins__</code></td><td>包含 Python 内置函数和异常的模块。</td></tr><tr><td><code>__debug__</code></td><td>Python 运行时的 <code>debug</code> 模式，默认值为 <code>True</code>。</td></tr><tr><td><code>__loader__</code></td><td>加载模块的加载器对象。</td></tr></tbody></table><ul><li><ol><li>方法相关的特殊属性（这些属性主要与 <strong>方法</strong> 相关）</li></ol></li></ul><table><thead><tr><th><strong>属性</strong></th><th><strong>作用</strong></th></tr></thead><tbody><tr><td><code>__call__</code></td><td>使对象变为可调用（可重载 <code>__call__</code> 方法）。</td></tr><tr><td><code>__getitem__</code></td><td>允许对象使用 <code>obj[key]</code> 访问（可重载 <code>__getitem__</code>）。</td></tr><tr><td><code>__setitem__</code></td><td>允许对象使用 <code>obj[key] = value</code> 赋值（可重载 <code>__setitem__</code>）。</td></tr><tr><td><code>__delitem__</code></td><td>允许对象使用 <code>del obj[key]</code> 删除元素（可重载 <code>__delitem__</code>）。</td></tr><tr><td><code>__len__</code></td><td>允许对象使用 <code>len(obj)</code> 获取长度（可重载 <code>__len__</code>）。</td></tr><tr><td><code>__repr__</code></td><td>返回对象的<strong>官方字符串表示</strong>，用于 <code>repr(obj)</code>。</td></tr><tr><td><code>__str__</code></td><td>返回对象的<strong>可读字符串表示</strong>，用于 <code>str(obj)</code> 或 <code>print(obj)</code>。</td></tr></tbody></table><pre><code class="language-python">class Test():
    def __init__(self, age):
        self.age = age
    
    def add(self):
        '''加一'''
        return self.age+ 1

test = Test(13)
test.__dict__['name'] = 'https://www.big-yellow-j.top/'
print(test.name)
print(test.add.__name__)
print(test.add.__doc__)

https://www.big-yellow-j.top/
add
加一

</code></pre><hr/><p>一些Python内置的装饰器:<br/> 1、<code>@staticmethod</code>：定义静态方法，无须实例化即可调用<br/> 2、<code>@classmethod</code>：定义类方法，可以访问类变量<br/> 3、<code>@property</code>：将方法转换为属性</p><p>比如说：</p><pre><code class="language-python">class Person:
    place= 'bj' # 类变量（所有实例共享）

    def __init__(self, name):
        self.name = name
    
    @staticmethod
    def age1(age):
        print(f"{age}")
    
    @classmethod
    def new_place(cls, new):
        cls.place = new

    def age2(self, age):
        print(f"{self.name}:{age} from {self.place}")
    
    def age3(self, age):
        if age&gt;= 20:
            Person.new_place('sh')
        print(f"{self.name}:{age} from {self.__class__.place}")

Person.age1(13)
Person("Tom").age2(13)
Person("Tom").age3(23)

13
Tom:13 from bj
Tom:23 from sh
</code></pre><p>但是切换顺序：</p><pre><code class="language-python">Person.age1(13)
Person("Tom").age3(23)
Person("Tom").age2(13)

13
Tom:23 from sh
Tom:13 from sh
</code></pre><p>这是因为使用<code>age3</code>时候就将类变量进行了修改，还有一些装饰器比如<code>pytoch</code>里面有些要不进行梯度更新，除了使用<code>with torch.no_grad()</code>可以直接用<code>@torch.no_grad()</code>（这个一般就是在类里面加一个这个（比如生成内容，一般就不需要更新梯度））</p><h2 id="结论">结论</h2><p>解释了python里面的 <strong>多进程/多线程/装饰器</strong>。更加详细准确直接去看 官方文档！！！！链接：https://docs.python.org/zh-cn/3.12</p><h2 id="值得注意的是">值得注意的是</h2><p><strong>1、在使用多进程时候，如果遇到无法 pickle 问题如何处理</strong></p><p>比如说在使用<strong>百度OCR工具</strong>（Win电脑上测试）时候，如果设置不当就会遇到：<code>TypeError: cannot pickle 'paddle.base.libpaddle.PaddleInferPredictor' object</code>，比如说你的代码（省略很多内容）如下：</p><pre><code class="language-python">def main_process():
    ...
    ocr = PaddleOCR(
        det_model_dir="./ch_PP-OCRv4_det_infer",
        rec_model_dir="./ch_PP-OCRv4_rec_infer",
        use_angle_cls=True,
        lang="en",
        use_gpu=True
    )
    with ProcessPoolExecutor(max_workers= max_workers) as executor:
        futures = {executor.submit(process_region, ..., ocr)
                   for region_idx, region in enumerate(detection_region)}
        ...
def process_region(..., ocr):
    ...
    ocr_result = ocr.ocr(roi, cls=True)
    ...

</code></pre><p>一个比较简单处理办法就是在 <code>ProcessPoolExecutor</code>中添加一个 <code>initializer</code><a href="https://docs.python.org/zh-cn/3.13/library/concurrent.futures.html#:~:text=initializer%20%E6%98%AF%E4%B8%80%E4%B8%AA%E5%8F%AF%E9%80%89%E7%9A%84%E5%8F%AF%E8%B0%83%E7%94%A8%E5%AF%B9%E8%B1%A1%EF%BC%8C%E5%AE%83%E4%BC%9A%E5%9C%A8%E6%AF%8F%E4%B8%AA%E5%B7%A5%E4%BD%9C%E8%BF%9B%E7%A8%8B%E5%90%AF%E5%8A%A8%E6%97%B6%E8%A2%AB%E8%B0%83%E7%94%A8">参数</a>保证：<strong>它会在每个工作进程启动时被调用</strong>。那么代码为：</p><pre><code class="language-python">def init_worker():
    global ocr
    ocr = PaddleOCR(
        det_model_dir="./ch_PP-OCRv4_det_infer",
        rec_model_dir="./ch_PP-OCRv4_rec_infer",
        use_angle_cls=True,
        lang="en",
        log_level='ERROR'
    )
def main_process():
    ...
    with ProcessPoolExecutor(max_workers= max_workers, initializer= init_worker) as executor:
        futures = {executor.submit(process_region, ..., ocr)
                   for region_idx, region in enumerate(detection_region)}
def process_region(..., ocr):
    global ocr
    ...
    ocr_result = ocr.ocr(roi, cls=True)
    ...

</code></pre><hr/><p>这里面还有另外一个注意点：如果所有的子进程都要加载同一个内容，就会发生冲突这个需要避免，比如说在<code>process_region</code>里面使用</p><pre><code class="language-python">  ocr = PaddleOCR(
      det_model_dir="./ch_PP-OCRv4_det_infer",
      rec_model_dir="./ch_PP-OCRv4_rec_infer",
      use_angle_cls=True,
      lang="en",
      use_gpu=True
  )
</code></pre><p>所有的进程都需要加载同一个文件夹，这里就会出现错误</p><hr/><h2 id="参考">参考</h2><p>1、https://docs.python.org/zh-cn/3.13/library/concurrent.futures.html<br/> 2、https://docs.python.org/zh-cn/3.13/library/threading.html<br/> 3、https://zh.wikipedia.org/zh-cn/%E5%85%A8%E5%B1%80%E8%A7%A3%E9%87%8A%E5%99%A8%E9%94%81<br/> 4、https://zh.wikipedia.org/wiki/CPU%E5%AF%86%E9%9B%86%E5%9E%8B<br/> 5、https://docs.python.org/zh-cn/3.13/library/multiprocessing.html<br/> 6、https://docs.python.org/zh-cn/3.12/library/pickle.html<br/> 7、https://docs.python.org/zh-cn/3.12/library/stdtypes.html#definition.<strong>name</strong></p> ]]></content:encoded><category>编程</category><pubDate>Sat, 22 Mar 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/03/22/PythonThreading.html</guid></item><item><title>数据蒸馏（Data Distillation）操作原理</title><link>https://www.big-yellow-j.top/posts/2025/03/10/Data-Distillation.html</link><description><![CDATA[数据蒸馏（DD）是一种从大量数据中提取关键信息，生成小规模高质量合成数据集的技术，旨在替代原始数据集用于模型训练、验证等任务，以提高效率、降低成本或保护隐私，其核心是保留原始数据的关键特征与分布信息并去除冗余噪声。数据蒸馏的目标是优化合成数据集与原始数据集的差异，使合成数据在模型训练中表现接近原始数据。论文1中分析了三种损失函数优化思路：Performance Matching通过蒸馏数据优化梯度，并借助源数据训练参数计算蒸馏数据集差异；Parameter Matching促使合成数据集与原始数据集训练同一网络得到的参数一致，包括单步参数匹配（优化单步梯度距离，节省内存）和多步参数匹配（...]]></description><content:encoded><![CDATA[<p>主要介绍数据蒸馏操作，并且介绍CVPR-2025上海交大满分论文：Dataset Distillation with Neural Characteristic Function: A Minmax Perspective。本文主要是借鉴论文1中的整体结构，大致了解什么是DD而后再去介绍（CVPR-2025）论文。</p><h2 id="data-distillation">Data Distillation</h2><p><strong>数据蒸馏</strong>（Data Distillatiob）是一种从大量数据中提取关键信息，生成高质量、小规模合成数据集的技术。它的目标是通过这些合成数据来替代原始数据集，用于模型训练、验证或其他任务，从而提高效率、降低成本或保护隐私。数据蒸馏的核心思想是“从数据中提取数据”，让合成数据集中保留原始数据集的关键特征和分布信息，同时去除冗余和噪声。参考论文1中的描述：</p><p><img src="https://s2.loli.net/2025/06/21/TIe7VApf2cDz1Fw.webp" alt="image" width="625" height="392" loading="lazy" decoding="async"/></p><p>数据蒸馏（DD）目标为：对于一个真实的数据集：$\mathrm{T}=(X_t,Y_t)$ 其中 $X_t\in R^{N\times d}$ 其中 $N$ 代表样本数量 $d$ 代表特征数量，$Y_t\in R^{N\times C}$ 其中$C$为输出实体。对于蒸馏得到的数据集：$\mathrm{S}={X_s,Y_s}$其中$X_s\in R^{M\times D}$其中$M$代表数据蒸馏后的样本数量。最终的优化目标为： $\text{arg min} \mathrm{L}(\mathrm{S}, \mathrm{T})$</p><p>比如说对于图像分类任务而言$D$代表的是：HWC而y代表的是独热编码，C代表类别数量</p><p><img src="https://s2.loli.net/2025/06/21/hkjof59eGIQOyqX.webp" alt="image" width="741" height="529" loading="lazy" decoding="async"/></p><p>论文1中对于损失函数优化主要分析3种处理思路</p><h3 id="1performance-matching">1、Performance Matching</h3> \[\begin{aligned} \mathcal{L}(\mathcal{S},\mathcal{T}) &amp; =\mathbb{E}_{\theta^{(0)}\sim\Theta}[l(\mathcal{T};\theta^{(T)})], \\ \theta^{(t)} &amp; =\theta^{(t-1)}-\eta\nabla l(\mathcal{S};\theta^{(t-1)}) \end{aligned}\] <p>其中$\theta, l, T, \eta$分别代表：神经网络参数、损失函数、迭代次数、学习率</p><p><img src="https://s2.loli.net/2025/06/21/beZiMlSaU3QqjHX.webp" alt="image" width="851" height="409" loading="lazy" decoding="async"/></p><p>对于上面公式以及优化过程理解：似乎整体优化过程没有体现源数据：$\mathrm{T}$ 和蒸馏数据： $\mathrm{S}$ 两者之间是如何进行优化的，第二个过程直接通过 <strong>蒸馏数据</strong>去优化梯度，第一个过程则是借助第$T$步得到的参数去计算蒸馏数据集之间差异（这个过程可以理解为模型参数是固定的，但是数据是变化的，需要的是一个数据集在通过源数据集上也有较好的表现）</p><h3 id="2parameter-matching">2、Parameter Matching</h3><p><strong>分别使用合成数据集和原始数据集对同一个网络进行若干步训练，并促使它们训练得到的神经网络参数保持一致</strong>。根据使用合成数据集（S）和原始数据集（T）进行训练的步数，参数匹配方法可以进一步分为两类：单步参数匹配和多步参数匹配。</p><p><img src="https://s2.loli.net/2025/06/21/EgVvIesf3tST9Ua.webp" alt="Parameter Matching" width="1589" height="499" loading="lazy" decoding="async"/></p><p>左图为单参数匹配，右图为多参数匹配</p><ul><li><strong>1、单参数匹配</strong></li></ul> \[\begin{aligned} \mathcal{L}(S, T) &amp;= \mathbb{E}_{\theta^{(0)} \sim \Theta} \left[ \sum_{t=0}^{T} \mathcal{D}(S, T; \theta^{(t)}) \right] \\ \theta^{(t)} &amp;= \theta^{(t-1)} - \eta \nabla l(S; \theta^{(t-1)}) \end{aligned}\] <p>其中$\mathrm{D}$代表两部分梯度之间的距离</p> \[\begin{aligned} \mathcal{D}(S, T; \theta) &amp;= \sum_{c=0}^{C-1} d(\nabla l(S_c; \theta), \nabla l(T_c; \theta)), \\ d(A, B) &amp;= \sum_{i=1}^{L} \sum_{j=1}^{J_i} \left(1 - \frac{\mathbf{A}_j^{(i)} \cdot \mathbf{B}_j^{(i)}}{\|\mathbf{A}_j^{(i)}\| \|\mathbf{B}_j^{(i)}\|}\right), \end{aligned}\] <ul><li><strong>2、多参数匹配</strong></li></ul><p><img src="https://georgecazenavette.github.io/mtt-distillation/resources/method.gif" alt="https://georgecazenavette.github.io/mtt-distillation/"/></p><p>对于单步参数匹配，由于只匹配单步梯度，因此在评估中可能会积累误差，而模型是通过多步合成数据更新的</p> \[\begin{aligned} \mathcal{L}(S, T) &amp;= \mathbb{E}_{\theta^{(0)} \sim \Theta} \left[ \mathcal{D}(\theta_S^{(T_s)}, \theta_T^{(T_t)}) \right] \\ \theta_S^{(t)} &amp;= \theta_S^{(t-1)} - \eta \nabla l(S; \theta_S^{(t-1)}) \\ \theta_T^{(t)} &amp;= \theta_T^{(t-1)} - \eta \nabla l(T; \theta_T^{(t-1)}) \\ \mathcal{D}(\theta_S^{(T_s)}, \theta_T^{(T_t)}) &amp;= \frac{\|\theta_S^{(T_s)} - \theta_T^{(T_t)}\|^2}{\|\theta_T^{(T_t)} - \theta^{(0)}\|^2} \end{aligned}\] <p>多步参数则是直接对数据S和T参数进行多步更新，优化目标为两部分数据所得到的参数$\theta_S$ 以及 $\theta_ T$</p><p>对比 <strong>多参数匹配</strong>、 <strong>单参数匹配</strong>、 <strong>Performance Matching</strong>三者之间差异：<br/> 1、<strong>单参数匹配</strong>和 <strong>Performance Matching</strong>之间差异：从公式上很好理解，单参数匹配中在计算 $\mathcal{L}$过程中同时还需要使用 <strong>源数据</strong> 和 <strong>蒸馏数据</strong>，由于只需要单步梯度，并且合成数据和网络的更新是解耦的，因此与基于元学习的性能匹配方法相比，这种方法更节省内存<br/> 2、<strong>单参数匹配</strong>和 <strong>多参数匹配</strong>之间差异：多参数匹配梯度分别通过源数据和蒸馏数据两部分进行更新得到，然后再去计算两部分参数梯度之间的差异更新蒸馏数据</p><ul><li><strong>3、分布式匹配</strong></li></ul><p><img src="https://s2.loli.net/2025/06/21/r8Uh95CXKf3asSn.webp" alt="image" width="677" height="360" loading="lazy" decoding="async"/></p><p>首先对于分布式匹配损失函数定义为：</p> \[\mathcal{L}(S,T)=\mathbb{E}_{\theta \in \Theta}[\mathbb{D}(S,T;\theta)]\\ \mathcal{D}(S, T; \theta) = \sum_{c=0}^{C-1} \| \mu_{\theta,s,c} - \mu_{\theta,t,c} \|^2 \\ \mu_{\theta,s,c} = \frac{1}{M_c} \sum_{j=1}^{M_c} f_\theta^{(i)}(X_{s,c}^{(j)}), \quad \mu_{\theta,t,c} = \frac{1}{N_c} \sum_{j=1}^{N_c} f_\theta^{(i)}(X_{t,c}^{(j)})\] <h2 id="cvpr-25上海交大论文">（CVPR-25）上海交大论文</h2><p>首先作者提到一点：通过分布式匹配进行数据蒸馏，容易导致无法获取分布的差异，进而导致效果不佳</p><p><img src="https://s2.loli.net/2025/06/21/n8tTWkR47i2C3KM.webp" alt="image" width="1481" height="405" loading="lazy" decoding="async"/></p><h2 id="参考">参考</h2><p>1、<a href="https://arxiv.org/pdf/2301.07014">Dataset Distillation: A Comprehensive Review</a><br/> 2、A Comprehensive Survey of Dataset Distillation<br/> 3、（CVPR-2025）Dataset Distillation with Neural Characteristic Function: A Minmax Perspective<br/> 4、（CVPR-2024）On the Diversity and Realism of Distilled Dataset: An Efficient Perspective<br/> 5、（CVPR-2023）Accelerating Dataset Distillation via Model Augmentation</p> ]]></content:encoded><category>paper</category><pubDate>Mon, 10 Mar 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/03/10/Data-Distillation.html</guid></item><item><title>Kimi论文——Muon优化器原理/torch优化器</title><link>https://www.big-yellow-j.top/posts/2025/03/04/Muon.html</link><description><![CDATA[AdamW优化器作为主流优化器，是Adam的变种，通过一阶矩（梯度加权平均）和二阶矩（梯度平方加权平均）估计、偏置校正，以及在更新规则中加入权重衰减改进L2正则化。而新优化器Muon（MomentUm Orthogonalized by Newton-Schulz）则通过获取SGD-momentum生成的更新并应用Newton-Schulz迭代后处理步骤优化二维神经网络参数，属自适应学习率优化器，类似Adam。Muon对参数形状有要求，不可直接用于embedding层及最后全连接层（需用其他优化器），4D卷积滤波器建议展平最后三维。其优势在于显存占用低（仅存储动量，AdamW需存一阶、二...]]></description><content:encoded><![CDATA[<p>众所周知，目前主流应用的优化器是AdamW，不过一个新的优化器 <strong>Muon</strong>（仅仅发布在Github上）似乎比 AdamW优化器能够实现更加优异的效果，于此同时Kimi也出了一篇新的<a href="https://arxiv.org/pdf/2502.16982">论文</a>就是使用 <strong>Muon</strong>优化器，有必要了解一些这个优化器以及测试一下这个优化器效果。</p><h2 id="adamw优化器">AdamW优化器</h2><p>Adam优化器在这篇<a href="https://www.big-yellow-j.top/posts/2025/01/04/learning_rate.html">blog</a>有介绍，主要回顾一下<strong>AdamW</strong>，AdamW是 Adam 优化器的一个变种，旨在改进 L2 正则化的处理。AdamW 的数学公式可以通过以下几个步骤来描述：</p><p><strong>1. 动量估计</strong></p><p>AdamW 基于梯度的一阶矩（平均梯度）和二阶矩（梯度的平方）进行估计。对于每个参数 $ \theta_t $，算法计算以下量：</p><ul><li><p><strong>一阶矩（梯度的加权平均）</strong>：</p> \[m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t\] </li></ul><p>其中 $ g_t $ 是当前时刻的梯度，$ \beta_1 $ 是衰减率，通常设置为 0.9。</p><ul><li><p><strong>二阶矩（梯度平方的加权平均）</strong>：</p> \[v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2\] </li></ul><p>其中 $ \beta_2 $ 是衰减率，通常设置为 0.999。</p><p><strong>2. 偏置校正</strong></p><p>由于在训练初期，动量估计 $ m_t $ 和 $ v_t $ 都会偏向零，因此需要进行偏置校正。偏置校正公式如下：</p><ul><li><p>偏置校正后的动量估计：</p> \[\hat{m}_t = \frac{m_t}{1 - \beta_1^t}\] </li><li><p>偏置校正后的二阶矩估计：</p> \[\hat{v}_t = \frac{v_t}{1 - \beta_2^t}\] </li></ul><p><strong>3. 更新规则</strong></p><p>AdamW 的更新规则与传统的 Adam 更新规则相似，<strong>但加入了权重衰减（L2 正则化）</strong>，权重衰减是通过 $ \lambda $ 控制的，其中 $ \lambda $ 是 L2 正则化项的超参数。</p><p>更新公式为（Adam计算公式中只有前面两个部分）：</p> \[\theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \cdot \hat{m}_t - \eta \lambda \theta_{t-1}\] <p>其中：</p><ul><li>$ \theta_{t-1} $ 是上一时刻的参数值，$ \theta_t $ 是当前时刻的参数值。</li><li>$ \eta $ 是学习率。</li><li>$ \epsilon $ 是一个小常数，用于避免除零错误，通常设置为 $ 10^{-8} $。</li><li>$ \lambda $ 是权重衰减的超参数（AdamW 中引入的主要不同点）。</li><li>$ \hat{m}_t $ 和 $ \hat{v}_t $ 是经过偏置校正的一阶和二阶矩。</li></ul><h2 id="muon优化器">Muon优化器</h2><p><img src="https://s2.loli.net/2025/06/21/lXyH48toiDEIksc.webp" alt="image" width="682" height="461" loading="lazy" decoding="async"/></p><p><a href="https://kellerjordan.github.io/posts/muon/">官方</a>提供的Muon效果，Muon（MomentUm Orthogonalized by Newton-Schulz）通过获取由 SGD-momentum 生成的更新，然后对每个更新应用Newton-Schulz (NS)迭代作为后处理步骤，再将其应用于参数，从而优化二维神经网络参数。其更新范式为：</p> \[\begin{aligned} &amp; \text{Compute gradient }G_t\leftarrow\nabla_\theta\mathcal{L}_t(\theta_{t-1}) \\ &amp; B_t\leftarrow\mu B_{t-1}+G_t \\ &amp; O_t\gets\text{NewtonSchulz}5(B_t) \\ &amp; \text{Update parameters }\theta_t\leftarrow\theta_{t-1}-\eta O_t \end{aligned}\] <p>其中$\eta$为学习率。简单回顾更新方式，容易发现 <strong>Muon</strong>是一种类似<strong>Adam</strong>的的自适应学习率的优化器。值得注意的是， <strong>Muon</strong>对参数形状有要求，根据作者给出的要求：1、不能直接对embedding layer以及最后的全连接层使用，这部分要用其他优化器；2、为了将其与 4D 卷积滤波器一起使用，最好将它们的最后三个维度展平。<br/> 在blog中介绍一点，从实验中发现对于Transformer类型的模型，对于一些参数通常是低秩的矩阵，所有神经元的更新都由少数几个方向主导。作者推测，<strong>正交化能有效增加其他 “稀有方向 ”的规模</strong>，这些方向在更新中的影响很小，但对学习却很重要。<br/> 回顾Kimi论文中提到的，基于 <strong>Muon</strong>优化器提出 <strong>Moonlight</strong>所展现的实验效果如下：</p><p><img src="https://s2.loli.net/2025/06/21/YIjPow4xSMqgfba.webp" alt="image" width="1536" height="545" loading="lazy" decoding="async"/></p><p>在kimi论文中对于Muon优化器改进如下：<br/> 1、<strong>增加了Weight Decay</strong>：这点主要是希望match AdamW里的Weight Decay，如果不加入WD的话，收敛速度的优势会随着训练Token慢慢消失。这是考虑到在低精度训练的时候，如果不控制权重的大小，会有表示精度的问题——浮点数的二进制表示方式决定了，在数值较大的范围内，浮点数表示的固定间隔会很大:</p> \[\theta_t\leftarrow\theta_{t-1}-\eta (O_t+ \textcolor{red}{\lambda \theta_{t-1}} )\] <p><img src="https://s2.loli.net/2025/06/21/iFNa89VpqCKkTzZ.webp" alt="image" width="1181" height="588" loading="lazy" decoding="async"/></p><p>2、<strong>通过学习率调节增加了RMS控制</strong>：由于Muon的每次更新的RMS理论值在 $\sqrt{1/\max(\mathrm{fan_in,fan_out})}$ ，因此相应地在O上乘以 $0.2* \sqrt{max(\mathrm{fan_in,fan_out})}$ ，把更新步的RMS保持与Adam一致。</p> \[\theta_t\leftarrow\theta_{t-1}-\eta (O_t*\textcolor{red}{0.2* \sqrt{max(\mathrm{fan\_in,fan\_out})}}+\textcolor{red}{\lambda \theta_{t-1}})\] <h2 id="总结">总结</h2><p>总的来说，<strong>Muon</strong>优化器区别于 <strong>AdamW</strong>而言：因为只存储动量（ <strong>AdamW</strong>需要存储1，2阶动量 ）对于显存的占用是明显减少了，代价是稍多的通讯量，以及Newton-Schulz迭代步带来的额外计算延迟</p><h2 id="代码使用">代码使用</h2><p><a href="https://github.com/KellerJordan/Muon/blob/master/README.md"><strong>Muon</strong></a>给出了示例，不过需要注意的是“ ≥2D parameters in the body of the network – these should be optimized by Muon”也就是使用之前需要对模型的参数进行划分。实际测试结果如下：<br/> GPU 2080 Ti-22G 数量： 1 显存： 11 GB<br/> CPU AMD EPYC 7302 16-Core Processor 实例内存： 30G<br/> 最高CUDA版本 12.2<br/> 显卡驱动版本 535.216.03</p><p>Some warnings:- This optimizer should not be used for the embedding layer, the final fully connected layer,or any {0,1}-D parameters; those should all be optimized by a standard method (e.g., AdamW).</p><ul><li>To use it with 4D convolutional filters, it works well to just flatten their last 3 dimensions.</li></ul><h2 id="torch优化器内部结构">torch优化器内部结构</h2><p>在pytoch常用的优化器中(比如:torch.optim.SGD等)优化算法均继承于Optimizer，所有优化器的基类Optimizer。<a href="https://datawhalechina.github.io/thorough-pytorch/%E7%AC%AC%E4%B8%89%E7%AB%A0/3.9%20%E4%BC%98%E5%8C%96%E5%99%A8.html">定义</a>如下:</p><pre><code class="language-python">class Optimizer(object):
    def __init__(self, params, defaults):        
        self.defaults = defaults # 优化器的超参数
        self.state = defaultdict(dict) # 参数缓存
        self.param_groups = [] # 管理的参数组，是一个list，其中每个元素是一个字典，顺序是params，lr，momentum，dampening，weight_decay，nesterov
</code></pre><p><strong>defaults</strong>：存储的是优化器的超参数，例子如下：</p><pre><code class="language-python">{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}
</code></pre><p><strong>state</strong>：参数的缓存，例子如下</p><pre><code class="language-python">defaultdict(&lt;class 'dict'&gt;, {tensor([[ 0.3864, -0.0131],
        [-0.1911, -0.4511]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}})
</code></pre><p><strong>param_groups</strong>：管理的参数组，是一个list，其中每个元素是一个字典，顺序是params，lr，momentum，dampening，weight_decay，nesterov，例子如下：</p><pre><code class="language-python">[{'params': [tensor([[-0.1022, -1.6890],[-1.5116, -1.7846]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
</code></pre><p>在Optimizer中还有如下方法<br/> zero_grad()：清空所管理参数的梯度，PyTorch的特性是张量的梯度不自动清零，因此每次反向传播后都需要清空梯度。<br/> step()：执行一步梯度更新，参数更新<br/> add_param_group()：添加参数组<br/> load_state_dict() ：加载状态参数字典，可以用来进行模型的断点续训练，继续上次的参数进行训练<br/> state_dict()：获取优化器当前状态信息字典</p><p>在Muon优化器内部设计(不去考虑分布式训练过程)</p><pre><code class="language-python">class Muon(torch.optim.Optimizer):
    """
    Muon - MomentUm Orthogonalized by Newton-schulz
    """
    def __init__(self, params, lr=0.02, weight_decay=0.01, momentum=0.95, nesterov=True, ns_steps=5, rank=0, world_size=1):
        self.rank = rank
        self.world_size = world_size
        defaults = dict(lr=lr, weight_decay=weight_decay, momentum=momentum, nesterov=nesterov, ns_steps=ns_steps)
        params: list[Tensor] = [*params]
        param_groups = []
        for size in {p.numel() for p in params}:
            b = torch.empty(world_size, size, dtype=torch.bfloat16, device="cuda")
            group = dict(params=[p for p in params if p.numel() == size],
                         update_buffer=b, update_buffer_views=[b[i] for i in range(world_size)])
            param_groups.append(group)
        super().__init__(param_groups, defaults)
    @torch.no_grad()
</code></pre><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/2502.16982<br/> 2、https://spaces.ac.cn/archives/10739<br/> 3、https://spaces.ac.cn/archives/10592<br/> 4、https://kellerjordan.github.io/posts/muon/<br/> 5、https://github.com/KellerJordan/Muon<br/> 6、https://www.zhihu.com/question/13193527053/answer/109342120565<br/> 7、https://datawhalechina.github.io/thorough-pytorch/%E7%AC%AC%E4%B8%89%E7%AB%A0/3.9%20%E4%BC%98%E5%8C%96%E5%99%A8.html</p> ]]></content:encoded><category>paper</category><pubDate>Tue, 04 Mar 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/03/04/Muon.html</guid></item><item><title>LLM 生成策略全解析-1：llm模型输出控制</title><link>https://www.big-yellow-j.top/posts/2025/02/27/LLM-OUT.html</link><description><![CDATA[LLM生成分为Prefill与Decoding两个阶段，前者输入全量prompt输出KV Cache和首个生成token的logits，后者基于KV Cache和上一步生成token自回归逐token输出，适配大词表的主流生成策略包含多类。贪婪解码每步取概率最高token，输出完全确定；随机采样通过温度参数调整概率分布锐度，控制输出多样性；Top-k采样仅在概率最高的k个token中重归一化采样，过滤无效候选；核采样动态选择累积概率达阈值p的最小候选集，灵活度优于Top-k；Beam Search为确定性搜索算法，维护k条候选序列选全局最优。此外还有重复惩罚、前瞻搜索等策略，可平衡生成连...]]></description><content:encoded><![CDATA[<p>我们都知道在使用dl模型（比如图像分类）最后的结果都是一个概率值（比如100种类别，输出就是每种类别的概率），常见的作法就是直接取概率最大的作为最终预测结果，但是<a href="https://www.big-yellow-j.top/posts/2025/02/15/LLM.html">LLM</a>里面也用这种方式合理吗（毕竟文本也需要考虑整体的不单单就是让下一个字最佳即可）。本文主要介绍：Beam search、Greedy search等LLM生成策略方式。<br/><img src="https://s2.loli.net/2025/06/21/MH1wFkitDJ3Oy7p.webp" alt="image" width="1398" height="670" loading="lazy" decoding="async"/></p><h2 id="模型生成过程">模型生成过程</h2><p>都知道模型输出过程就是将prompt丢给模型，然后模型输出回答，但是实际上在模型输出的内部中输出过程是：<strong>Prefill阶段+Decoding阶段</strong>两部分组成首先对于这两部分简单介绍如下：1、Prefill阶段该阶段中直接将整个 prompt（用户输入 + system prompt + 历史对话等）的所有token进行输入而后去输出完整的KV Cache以及第一步生成logits（也就是<strong>预测第一个输出token</strong>）；2、Decoding阶段直接将Prefill阶段中的KV Cache以及上一步生成的token进行输入而后进行自回归逐个生成（值得注意的是在模型输出中，直接根据Vocabulary数量得到在Vocabulary中每个token的概率），在面对海量的Vocabulary中就需要去对每一个token输出去使用不同的<a href="https://docs.vllm.ai/en/v0.6.4/dev/sampling_params.html">采样方式</a>，其中使用比较多的有如下几种（以vllm中的采样方式为例）</p><h3 id="贪婪解码">贪婪解码</h3><p>贪婪解码是最简单直接的采样方式：每一步都选择概率最高的那个token。这种方式保证了输出的确定性——同样的输入每次都会得到完全一样的输出。</p><pre><code class="language-python">from vllm import SamplingParams
greedy_params = SamplingParams(
    temperature=0,  # 设置为0即触发贪婪解码
    max_tokens=100
)
</code></pre><h3 id="随机采样">随机采样</h3><p>随机采样按照模型计算出的概率分布进行随机抽取。为了让分布更“平滑”或更“尖锐”，我们引入了温度（Temperature）参数： 低温度（&lt;1.0）：概率分布更集中，高概率token更容易被选中，输出更确定；高温度（&gt;1.0）：概率分布更平坦，低概率token也有更多机会，输出更多样<br/> 直观理解：温度就像给模型的“选择偏好”调节器。<strong>温度越低，模型越“固执”地选最可能的那条路；温度越高，模型越“随性”</strong>，愿意尝试一些意料之外的表达</p><pre><code class="language-python"># 低温度 - 更保守
conservative_params = SamplingParams(
    temperature=0.3,
    max_tokens=100
)

# 高温度 - 更随机
creative_params = SamplingParams(
    temperature=1.2,
    max_tokens=100
)
</code></pre><h3 id="top-k采样">Top-k采样</h3><p>Top-k采样<strong>只保留概率最高的k个token，然后在这k个token中进行重新归一化采样</strong>。这样可以有效过滤掉那些概率极低、毫无意义的token，让生成过程更稳定。<br/> 直观理解：就像让模型只能从“最有可能的前k个答案”里挑选，把那些明显离谱的选项先剔除掉。<br/> 区别于Greedy search每次都会选择一个最优的输出，Beam search则是会选择一个 <em>束宽（beam size）</em>（k）也就是在生成过程中会从生成的内容中选择k个最为第t步的输出，而后在t+1步中会将前k步的输出结合起来构成新的输出。</p><pre><code class="language-python">top_k_params = SamplingParams(
    temperature=0.8,
    top_k=50,  # 只考虑概率最高的50个token
    max_tokens=100
)
</code></pre><h3 id="核采样">核采样</h3><p>核采样是Top-k的“升级版”：不固定k值，而是动态选择一组概率累积和达到p的最小token集合。例如top_p=0.9表示选择概率最高的token，直到它们的累积概率超过90%。 这种方式比Top-k更灵活——当分布很集中时只选几个token；当分布很分散时会选更多token，让模型有更多探索空间。<br/> 直观理解：模型先按概率从高到低“收罗”候选词，直到收罗到的词的总概率达到90%为止，然后只在这批词里做选择。分布集中时候选词少（更确定），分布分散时候选词多（更灵活）。</p><pre><code class="language-python">nucleus_params = SamplingParams(
    temperature=0.8,
    top_p=0.9,  # 累积概率阈值90%
    max_tokens=100
)
</code></pre><h3 id="beam-search">Beam Search</h3><p>与上述采样方式不同，Beam Search不是随机采样，而是一种<strong>确定性搜索算法</strong>。它同时维护k条候选序列（称为“束”），每一步都扩展所有候选，只保留总体概率最高的k条。最终选择概率最高的完整序列作为输出。<br/> 直观理解：不像贪婪解码那样“走一步看一步”，Beam Search同时探索多条路径，保留最有希望的几条往前走，最后选出最好的那条。就像下棋时考虑多步之后的局面。</p><pre><code class="language-python">beam_search_params = SamplingParams(
    use_beam_search=True,
    best_of=5,      # 束宽度（同时探索5条路径）
    temperature=0,  # Beam search时通常设为0
    max_tokens=100
)
</code></pre><h2 id="不同生成参数">不同生成参数</h2><h3 id="温度调节">温度调节</h3><p>比如说温度调节使用：</p><pre><code class="language-python">@torch.no_grad()
def generate(self, idx, eos, max_new_tokens, temperature=1.0, top_k=None):
    for _ in range(max_new_tokens):
        # if the sequence context is growing too long we must crop it at block_size
        idx_cond = idx if idx.size(1) &lt;= self.params.max_seq_len else idx[:, -self.params.max_seq_len:]
        # forward the model to get the logits for the index in the sequence
        logits = self(idx_cond)
        logits = logits[:, -1, :] # crop to just the final time step
        if temperature == 0.0:
            # "sample" the single most likely index
            _, idx_next = torch.topk(logits, k=1, dim=-1)
        else:
            # pluck the logits at the final step and scale by desired temperature
            logits = logits / temperature
            # optionally crop the logits to only the top k options
            if top_k is not None:
                v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
                logits[logits &lt; v[:, [-1]]] = -float('Inf')
            # apply softmax to convert logits to (normalized) probabilities
            probs = F.softmax(logits, dim=-1)
            idx_next = torch.multinomial(probs, num_samples=1)
        # append sampled index to the running sequence and continue
        idx = torch.cat((idx, idx_next), dim=1)
        if idx_next==eos:
            break

    return idx
</code></pre><p>上面生成器中，设定最大生成长度<code>max_new_tokens</code>，通过前馈计算（<code>self(idx_cond)</code>）生成之后通过选取最后时间步的概率（$b,t,vocab_size \rightarrow b, 1, vocab_szie$）这样就相当于得到模型新的输出（每个词的概率）当选择的温度参数为0时，模型会选择最可能的 token（即选择 logits 中最大值对应的索引）。这是确定性的选择。反之就会，logits 会被 temperature 除以，从而影响选择的多样性。较高的温度值会使得概率分布变得更均匀，增加随机性；较低的温度值会让概率分布更加尖锐，使得选择更有偏向性。<br/> 从上面代码其实也很容易发现 <strong>温度系数</strong>是如何影响输出的：</p> \[P(x)=\frac{e^{logit_i/T}}{\sum e^{logit_j/T}}\] <p>通过温度系数$T$来平滑最后输出：<br/> 当$T=1$ 时，公式恢复为标准的softmax计算，logits 被直接用来计算概率。<br/> 当$T&gt;1$ 时，logits 被缩小，概率分布变得更加平滑。这个时候，<strong>低概率的选项也可能被采样到</strong>，从而增加生成的多样性。温度较高时，模型的输出更加随机，生成的文本更加多样化。<br/> 当$T&lt;1$时，logits 被放大，概率分布更加尖锐。这个时候，高概率的选项会变得更加突出，低概率的选项几乎被完全抑制，从而使模型的输出更加确定性。低温度时，模型倾向于生成高概率的单词，<strong>减少了生成的多样性</strong>。</p><blockquote><p>那么按照理论上当设置temperature=0时候输出应该是相同的，因为次数每一步都是使用greedy decoding进行处理，但是实际上不是的这是<strong>因为数值精度原因导致发生差异</strong>。总的来说temperature只能起到控制采用的随机性。</p></blockquote><h3 id="重复惩罚控制">重复惩罚控制</h3><p>具体控制模型输出方法和温度调节相似，在计算方法上两部分都是相似的，不过：温度调节–&gt;对所有的输出token都去调节，重复惩罚控制–&gt;对已经输出的token去调节，在实际控制过程中先重复惩罚控制而后topk温度等这部分参数控制。</p><h2 id="lookahead-search">Lookahead Search</h2><p>在生成过程中，它不仅考虑当前 token，还预先评估多步生成结果的质量，以此来选择最合适的当前步骤。它是一种对未来进行<strong>前瞻评估</strong>的策略，能够平衡生成的连贯性和多样性。使用方法比较简单</p><pre><code class="language-python">probs = F.softmax(logits, dim=-1)
candidate_probs = []
for _ in range(lookahead_depth):
    idx_next = torch.multinomial(probs, num_samples=1)
    candidate_score = self.evaluate_candidate(idx, idx_next, eos)
    candidate_probs.append((candidate_score, idx_next))

candidate_probs.sort(reverse=True, key=lambda x: x[0])
best_candidate = candidate_probs[0][1]
idx = torch.cat((idx, best_candidate), dim=1)
if best_candidate == eos:
    break
</code></pre><h2 id="控制模型输出">控制模型输出</h2><p>根据<a href="https://docs.vllm.ai/en/latest/features/structured_outputs/#experimental-automatic-parsing-openai-api">官方文档</a>在控制模型输出过程中，有两种方式去控制模型输出格式：1、json_schema；2、pydantic model方式，两种方式差异不大。比如说<strong>使用json_schema方式进行控制</strong>，那么模型会按照json_schema进行输出，比如说（使用<a href="https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seed-2-0-lite">豆包模型</a>可以使用openai方式进行调用）:</p><pre><code class="language-python">from openai import OpenAI
client = OpenAI(
    base_url="https://ark.cn-beijing.volces.com/api/v3",
    api_key="xxx", # 调用api和本地vllm在线推理是没有差异的
)

json_schema = {
    "name": "trump_analysis",
    "schema": {
        "type": "object",
        "properties": {
            "姓名": {"type": "string", "minLength": 1},
            "年龄": {
                "type": "string",
                "pattern": "^\\d{1,3}$"          # 限制为合理数字字符串
            },
            "出生年月日": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"   # 强制 YYYY-MM-DD 格式
            },
            "财富世界排名": {"type": "string", "pattern": "^\\d+$"},
            "职位": {
                "type": "array",
                "items": {"type": "string", "minLength": 1},
                "minItems": 1,                    # 至少有一个职位
            }
        },
        "required": ["姓名", "年龄", "出生年月日", "财富世界排名", "职位"],
        "additionalProperties": False
    }
}
completion = client.chat.completions.create(
    model="doubao-seed-1-6-251015",
    messages = [
        {"role": "user", "content": "分析一个特朗普的生平，如年龄、世界富豪排名，按照json格式输出"},
    ],
    response_format=json_schema
)
print(completion.choices[0].message.content)
</code></pre><p>只需要在模型输出过程中<a href="https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema)">进行指定</a>： <code>response_format={"type": "json_object"}</code> 即可控制模型按照json格式输出，除此之外可以使用 <code>json_schema</code> 去控制让模型去填写字段到我的要求格式中。除此之外在书写 <code>json_schema</code> 过程中也可以直接去对字符进行控制比如说：<code>"net_worth": {"type": "string","pattern": r"^\$[0-9]+(\.[0-9]{1,2})? (Billion|Million)$"},</code> 通过正则匹配的方式控制模型输出：</p><pre><code class="language-python"># 不使用正则匹配
"net_worth": "约25亿美元",
# 使用正则匹配
"net_worth": "$2.6 Billion",
</code></pre><p>除此之外如果模型是<strong>使用vllm进行本地部署在控制模型输出上和上面代码没有差异</strong>。</p><blockquote><p>本地启动vllm（<a href="https://docs.vllm.ai/en/stable/cli/serve/#frontend">启动参数</a>）：<code>HF_ENDPOINT=https://hf-mirror.com HF_HUB_CACHE=/root/autodl-tmp/.cache vllm serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 8001 --gpu-memory-utilization 0.5 --max-model-len 16384 --max-num-seqs 256 --trust-remote-code --served-model-name qwen3-0.6B</code></p></blockquote><p><strong>比如说使用pydantic类型进行控制</strong>（测试使用上面提到的 qwen3-0.6b 模型）：</p><pre><code class="language-python">client = OpenAI(base_url="http://127.0.0.1:8001/v1",api_key="EMPTY")

class Step(BaseModel):
    explanation: str
    output: str

class MathResponse(BaseModel):
    steps: list[Step]
    final_answer: str

def pydantic_test():
    completion = client.beta.chat.completions.parse(
        model="qwen3-0.6B",
        messages=[
            {"role": "system", "content": "你是一个数学大师"},
            {"role": "user", "content": "计算 x^2 +2x+ 28 = 32."},
        ],
        response_format=MathResponse,
    )
    message = completion.choices[0].message
    print(message)
    assert message.parsed
    for i, step in enumerate(message.parsed.steps):
        print(f"Step #{i}:", step)
    print("Answer:", message.parsed.final_answer)
</code></pre><h3 id="底层原理">底层原理</h3><p>在使用<a href="https://docs.vllm.ai/en/v0.8.4/serving/openai_compatible_server.html#openai-compatible-server:~:text=1000%20%2D%201K%20%E2%86%92%201024-,%2D%2Dguided%2Ddecoding%2Dbackend,-Which%20engine%20will">vllm中启动过程</a>中通过参数 <code>--guided-decoding-backend guidance</code> 去控制模型输出（支持参数有 <code>guidance</code> 以及 <code>xgrammar</code> 默认是 <code>auto</code>）以 <code>xgrammar</code> 为例，其<a href="https://arxiv.org/pdf/2411.15100">内部原理</a>核心两部分：1、Pushdown Automaton（<strong>PDA</strong>）；2、自适应 Token Mask 缓存。</p><blockquote><p>先用一个具体例子解释生成过程，比如说上面我指定的是：<code>prompt=分析一个特朗普的生平，如年龄、世界富豪排名，按照json格式输出</code>，而后我给定的json格式是：<code>{"姓名": {"type": "string", "minLength": 1},"年龄": {"type": "string","pattern": "^\\d{1,3}$"},}</code> 那么模型（vllm）中输出过程为：<br/><strong>预处理阶段</strong>：首先将我的json schema转化为一个状态机，比如状态0必须输出 <code>{</code>，状态1必须输出：<code>"姓名"</code>，依此类推，而后计算离线mask，直接扫描vllm中的词表，比如状态0只有 <code>{</code> 合法其他直接mask。生成过程：<br/><img src="https://files.seeusercontent.com/2026/05/07/Dmw4/image20260410153654641.webp" alt=""/></p></blockquote><p><strong>对于第一部分</strong>：普通的有限状态机无法处理复杂的嵌套结构，而这正是 XGrammar 引入 PDA 的原因。可以将 PDA 简单理解为一个带有“栈”记忆机制的智能体。以生成标准的 JSON 格式为例：当模型生成一个左大括号 { 时，PDA 会将其入栈（Push）；当需要闭合结构并生成右大括号 } 时，PDA 会将其出栈（Pop）。通过这种严密的“栈”式匹配，XGrammar 能够像编译器一样，在生成层面死死守住语法的嵌套规则，确保输出的每一层级都严格符合规范。<br/><strong>对于第二部分</strong>：仅仅有 PDA 还不够。在当今动辄 128k 词汇量的 LLM 词表中，如果在 Runtime（运行时）逐一让 CPU 去验证数以十万计的 Token 是否符合当前语法状态，这无疑会成为一场性能灾难。XGrammar 通过一套巧妙的分类与缓存机制破局：<br/><strong>1、预处理</strong>：Token 的“双轨制”分类，在模型开始生成前，XGrammar 会将整个词表的 Token 划分为两类：<strong>上下文无关 Token</strong> (Context-Independent Tokens)： 这类 Token 的合法性仅取决于当前 PDA 所在的节点状态，无需回溯栈内的历史记录。<strong>上下文相关</strong> Token (Context-Dependent Tokens)： 这类 Token 的合法性不仅取决于当前节点，还强依赖于栈内信息（例如：当前究竟嵌套了多少层，是否到了该闭合多层右括号的时候）。<br/><strong>2、运行时</strong>：空间换时间的极致压缩基于上述分类，XGrammar 构建了极其高效的自适应缓存机制：预计算缓存： 在生成前，系统预先计算好所有“上下文无关 Token”的合法性，并将其存储在一个高度压缩的位图缓存（Token Mask Cache）中。$O(1)$ 极速读取： 在实际生成阶段，对于绝大部分 Token，系统直接以 $O(1)$ 的时间复杂度从缓存中读取合法性掩码（Mask），瞬间完成筛选。按需校验： 仅仅对极少数的“上下文相关 Token”，才会在 CPU 上进行实时的状态机校验。<br/><strong>在vllm中起作用方式是</strong>：LLM 在自回归生成文本时，每一步都会输出一个包含所有词汇概率的 Logits 向量。XGrammar 作为一个 Logits Processor 插入到了采样（Sampling）之前：获取掩码：XGrammar 根据当前生成的状态，瞬间生成一个布尔型的掩码（Mask），1 代表符合语法规则，0 代表违规。修改概率：XGrammar 遍历 Logits 向量，将所有非法 Token 的对数概率强行修改为 $-\infty$（负无穷）。采样：经过 Softmax 计算后，非法 Token 的生成概率变为绝对的 $0$。模型只能被迫从合法的 Token 中采样。</p> ]]></content:encoded><category>LLM应用</category><pubDate>Thu, 27 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/27/LLM-OUT.html</guid></item><item><title>DeepSpeed基本原理</title><link>https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html</link><description><![CDATA[DeepSpeed是微软开发的深度学习优化库，专为高性能训练和推理设计，适用于GPT、BERT等大规模深度学习模型，通过优化策略帮助高效利用硬件资源，实现快速训练、降低内存使用及提升推理速度。模型训练显存主要被模型状态（优化器状态、梯度、模型参数）和剩余状态（激活值、临时缓冲区、显存碎片）占用。DeepSpeed核心技术ZeRO通过切分（partitioning）优化显存，ZeRO-DP针对模型状态，分三种切分方式：仅优化器状态切分、优化器状态与梯度切分、三者全切分，结合All-Gather、All-Reduce、reduce-scatter等通信操作减少显存占用；ZeRO-R优化剩余状...]]></description><content:encoded><![CDATA[<h2 id="deepspeed原理"><code>DeepSpeed</code>原理</h2><p>DeepSpeed 是由微软开发的一种深度学习优化库，专为高性能训练和推理而设计，尤其适用于大规模深度学习模型（如 GPT 系列、BERT 等）。它通过一系列技术和优化策略，帮助研究者和开发者高效利用硬件资源，实现快速训练、降低内存使用以及提升推理速度。正如其官方描述那样：<br/><img src="https://s2.loli.net/2025/06/21/XCztHyfDvhTQG5x.webp" alt="image" width="1440" height="335" loading="lazy" decoding="async"/></p><blockquote><p>Image From: https://github.com/microsoft/DeepSpeed</p></blockquote><p><code>Deepspeed</code>作为一种显存优化技术，那么就会有一个问题：<strong>模型训练显存都被谁占用了？</strong><br/> 参考论文（https://arxiv.org/pdf/1910.02054）中的描述在一个<code>1.5B</code>的<code>GPT-2</code>模型参数量为<code>3G</code>（半精度）但是一块32G的显卡可能无法训练下来，这是因为显存都被 <strong>模型状态</strong> 以及 <strong>剩余状态</strong>（<code>Residual Memory Consumption</code>）<br/><strong>模型状态</strong>显存占用<br/> 主要指的是：<em>优化器状态，梯度，模型参数</em>。比如说在训练过程中一般都会选择使用<code>Adam</code>作为一种优化器进行使用，而在<code>Adam</code>计算过程中就会存储两部分内容：<strong>1、动量（上一轮梯度累计）；2、二阶动量（存储梯度平方的滑动平均值）</strong>。如何去避免这部分结果对显存占用的影响，就提出了 <em>混合精度训练</em>（用<code>FP16</code>存储和计算梯度及优化器状态）<br/> 比如说：用<code>Adam</code>作为优化器在混合精度下训练参数量为$\Phi$的模型显存占用：1、一部分用来存储<code>FP16</code>的参数以及梯度：$2\Phi, 2\Phi$；2、另外一部分需要存储优化器状态（<code>FP32</code>存储：模型参数，动量，二阶动量）：$4\Phi, 4\Phi, 4\Phi$。那么显存占用上就有：$2+ 2+ 4+ 4+ 4=16\Phi$。那么回到上面提到的<code>1.5B</code>的<code>GPT-2</code>至少需要：$1.5 \times 16=24G$<br/><strong>剩余状态</strong>显存占用<br/> 这部分主要指的是： 除了模型状态之外的显存占用，包括<strong>激活值（activation）</strong>（可以通过<code>Activation checkpointing</code>减少）、<strong>各种临时缓冲区（buffer）</strong>以及无法使用的<strong>显存碎片（fragmentation）</strong></p><h3 id="zero-dp原理"><code>ZeRO-DP</code>原理</h3><p>主要是通过<strong>切分</strong>（<code>partitioning</code>）的方式来减少 <strong>模型状态</strong>显存占用<br/><img src="https://s2.loli.net/2025/06/21/4OUkVeJpjsF8zvc.webp" alt="image" width="889" height="381" loading="lazy" decoding="async"/><br/> 第一种方式为$P_{OS}$：<strong>对优化器的状态进行切分</strong>，将$N$块GPU上每块只存储$\frac{1}{N}$，那么最后显存占用（按上面的显存分析为例）就为：$4\Phi+ \frac{12\times \Phi}{N}$<br/> 第二种方式为$P_{OS+g}$也就是在对优化器切分的基础上补充一个<strong>对梯度的切分</strong>，那么显存占用上就变成为：$2\Phi+ \frac{(2+ 12)\times \Phi}{N}$<br/> 第三种方式为$P_{OS+g+p}$再对<strong>模型的参数进行切分</strong>，显存占用为：$\frac{4\Phi+ 12\Phi}{N}$<br/> 对于上面3种方式显存减少上分别为：$4\text{x}, 8\text{x}, N$（其中N表示的为设备数量）<br/> 进一步理解上面3个操作</p><blockquote><p>Image From: <a href="https://zhuanlan.zhihu.com/p/618865052">https://zhuanlan.zhihu.com/p/618865052</a><br/><code>ZeRO-DP</code>是一种 <em>用完就丢</em> 的套路，<strong>计算时候是完整内容，但是使用完之后就丢掉</strong></p></blockquote><p><strong>第一种方式$P_{OS}$</strong>：对优化器进行切分<br/><strong>Forward过程</strong>：因为每一张卡上参数都是全复制的，那么每张卡正常前向计算；<strong>Backward过程</strong>：每张卡计算<em>完整梯度</em>（第一阶段每张卡上梯度也是完全的不被切分的），因为多卡并行每张卡上处理数据不同对于梯度也就不同那么就需要首先通过 <em>all-reduce</em> （收集每张卡梯度信息而后平均再分发给每张卡）处理每张卡梯度信息；<strong>Optimizer step过程</strong>（每张卡都有完整梯度，但是优化器又被切分了，因此需要首先同步梯度再去切分！）：此时对优化器的状态进行切分让每张卡上只保留 <strong>1/N份优化器状态</strong>（优化器完整的动量等）因为我的梯度都是完整的，那么优化器就可以对他自己“负责”的那块模型进行参数更新，此时每一块显卡都只更新了自己的参数，而后通过<strong>All-Gather</strong>（把每一块更新的参数收集起来然后分配给每张显卡达到参数更新）把更新后的参数同步回所有卡，使参数全部得到更新 <br/><img src="https://s2.loli.net/2025/06/21/zZP5wKRG2duH7L3.webp" alt="image" width="1440" height="933" loading="lazy" decoding="async"/></p><p><strong>第二种方式$P_{OS+g}$</strong>：对模型梯度进行切分<br/><strong>Forward过程</strong>：因为每一张卡上参数都是全复制的，那么每张卡正常前向计算；<strong>Backward过程</strong>（因为输入数据不同，那么每张卡都会去计算梯度，只不过保留梯度不同，比如4卡一个deocoder里面有4层计算那么每一张卡只保留4层计算中一部分）：具体过程，当计算完第 $n$ 层的梯度后，链式法则的导数已经成功传递给了第 $n-1$ 层。此时，第 $n$ 层的梯度在显存中已完成了它的“传导使命”。<em>判断归属</em>：系统会检查第 $n$ 层梯度所属的参数索引。<em>触发同步</em>：如果该层梯度不属于本显卡负责的 $1/N$ 范畴，系统会立即将其通过网络发送给对应的“管家卡”进行聚合。<em>关键动作</em>：一旦发送完成，本显卡会 <strong>立即释放（Free）</strong> 该层梯度占用的显存，而不会等待整个模型反向传播结束。<em>持续推进</em>：由于第 $n-1$ 层的梯度计算只依赖于第 $n$ 层传回的导数（Activation Gradient），而不依赖于第 $n$ 层梯度本身在内存中的留存，因此反向传播可以顺利向第 $n-2$ 层继续推进；<br/><strong>梯度同步过程</strong>：因为每一张卡都只保留了部分梯度，因此就需要将这些梯度都进行“收回”也就是执行 <strong>Reduce-Scatter</strong>（将4层计算梯度都收回组成一个完成的decoder层梯度），每个 GPU 只保留全局平均后“自己负责参数 shard”对应的那 1/N 份梯度（其余丢弃）；Optimizer step：用本地 1/N 梯度 + 本地 1/N 优化器状态，直接更新本地 1/N 参数 shard；参数同步：执行 <strong>All-Gather</strong>（或等效广播），把更新后的完整参数广播给所有卡（保持参数全复制） <br/><img src="https://s2.loli.net/2025/06/21/WSEDgNrws4n6hC1.webp" alt="image" width="1440" height="982" loading="lazy" decoding="async"/></p><p><strong>第三种方式为$P_{OS+g+p}$</strong>：对模型参数进行切分<br/> Forward和Backward（每层/每个 bucket）过程：执行<strong>All-Gather</strong> 把本层/本 bucket 的参数 shard 收集成完整参数–&gt;进行前向/反向计算–&gt;计算完立即释放完整参数（节省内存）；梯度同步：执行 <strong>Reduce-Scatter</strong>，把所有卡的梯度按 shard 聚合，每个 GPU 只拿到“自己负责参数 shard”对应的 1/N 梯度；Optimizer step：每张卡用本地 1/N 梯度 + 本地 1/N 优化器状态，直接更新本地 1/N 参数 shard<br/><img src="https://s2.loli.net/2025/06/21/eViXt9sI2rluF4H.webp" alt="image" width="1440" height="923" loading="lazy" decoding="async"/></p><p>对于上述过程中提到的几个概念：<code>All-Gather</code>, <code>All-Reduce</code>, <code>reduce-scatter</code>其具体的原理如下：<br/> 1、<code>All-Gather</code>：把每个设备上的<strong>同等大小</strong>的数据收集起来，然后把完整集合<strong>广播</strong>给所有设备。最终每个设备都得到所有设备数据的<strong>完整拼接</strong>。。比如说4个GPU分别存储不同的值：GPU₀: [A], GPU₁: [B], GPU₂: [C], GPU₃: [D]，all-gather 后：每个GPU都得到 [A, B, C, D]。<br/> 2、<code>reduce-scatter</code>：先对所有设备<strong>相同位置</strong>的数据做聚合（reduce），然后把聚合后的完整结果<strong>分片</strong>（scatter）分发给每个设备，每个设备只拿到其中一部分。比如说4个GPU计算得到不同的梯度值（reduce操作为sum，输出分4份）：<br/> 初始： <br/> GPU₀: [a0, a1, a2, a3] <br/> GPU₁: [b0, b1, b2, b3] <br/> GPU₂: [c0, c1, c2, c3] <br/> GPU₃: [d0, d1, d2, d3]<br/> reduce-scatter 后（假设按顺序分片）： <br/> GPU₀: [a0+b0+c0+d0] <br/> GPU₁: [a1+b1+c1+d1] <br/> GPU₂: [a2+b2+c2+d2] <br/> GPU₃: [a3+b3+c3+d3]<br/> 3、<code>All-Reduce</code>：对所有设备上的数据做全局聚合（reduce），然后把<strong>同一个聚合结果广播</strong>给所有设备。最终每个设备拿到的值完全相同。比如说4个GPU分别存储不同的值：GPU₀: [10,20], GPU₁: [30,40], GPU₂: [50,60], GPU₃: [70,80]。all-reduce(sum) 后：每个GPU都得到 [160, 200]<br/> 对于<strong>all-gather</strong>和 <strong>all-reduce</strong>简单理解为：前者“把碎片拼成完整，大家都拿完整版”（<strong>只拼接、不计算</strong>），而后者“把不同值加起来（或其他聚合），把同一个答案发给大家”（<strong>先算、再广播</strong>）。<br/> 4、<code>Ring-ALLReduce</code>操作：<br/><strong>第一阶段</strong>，通过<code>reduce-sactter</code>传递参数<br/><img src="https://s2.loli.net/2025/06/21/WipqDmgUbZ9TAnc.webp" alt="image" width="1440" height="718" loading="lazy" decoding="async"/><br/> 通过3次参数更新之后，这样就会出现不同设备上都会有一个都具有参数$a_i+ b_i+ c_i+ d_i$那么下一阶段就是通过<code>all-gather</code>将不同设备上参数广播到不同设备最后实现参数都实现更新。<br/><img src="https://s2.loli.net/2025/06/21/YMbcTewvnJFjDZC.webp" alt="image" width="545" height="306" loading="lazy" decoding="async"/></p><h4 id="通行量分析">通行量分析</h4><p>这部分描述来自论文（<a href="https://arxiv.org/pdf/1910.02054">https://arxiv.org/pdf/1910.02054</a>）中的描述：在<strong>传统的数据并行方式</strong>：传统的<code>DDP</code>主要使用的是<code>Ring AllReduce</code>在通信量上为：$2\Phi$在DeepSpeed中通信量分析如下：<br/> $P_{OS}$ 和 $P_{OS+g}$ 通信量：$2\Phi$。因为每部分设备只保留了<strong>部分/全部</strong>梯度信息，因此首先需要通过<code>reduce-scatter</code>（不管梯度有没有被切分，因为输入数据都是不同的因此先把梯度都统一）操作（$\Phi$）在梯度都通一之后需要对所有的参数进行更新，参数更新之后需要将其分发给每张卡（All-Gather操作）去保证参数都是最新的（$\Phi$）<br/> $P_{OS+g+p}$：$3\Phi$。在前向和反向都需要进行参数All-Gather那么此时通信量就是$2\phi$，而后还需要对提取进行Reduce-Scather此时通信量为 $\phi$</p><h3 id="zero-r原理"><code>ZeRO-R</code>原理</h3><p>1、对于激活值的占用。通过$P_a$：<code>Partitioned Activation Checkpointing</code>通过分区+checkpointing方式<br/> 2、对于临时缓冲区。模型训练过程中经常会创建一些大小不等的临时缓冲区，比如对梯度进行AllReduce，解决办法就是预先创建一个固定的缓冲区，训练过程中不再动态创建，如果要传输的数据较小，则多组数据bucket后再一次性传输，提高效率<br/> 3、对于显存碎片。显存出现碎片的一大原因是时候gradient checkpointing后，不断地创建和销毁那些不保存的激活值，解决方法是预先分配一块连续的显存，将常驻显存的模型状态和checkpointed activation存在里面，剩余显存用于动态创建和销毁discarded activation</p><h2 id="deepspeed代码操作"><code>DeepSpeed</code>代码操作</h2><p><code>Deepspeed</code>代码也比较简单，首先安装<code>deepspeed</code>:<code>pip install deepspeed</code>。使用<code>deepspeed</code>之前一般先去初始化，<a href="https://github.com/microsoft/DeepSpeed/blob/fa8db5cf2f9cf724fd2703353d40e3b37a8e7310/deepspeed/__init__.py#L68">代码</a>如下：</p><pre><code class="language-python">def initialize(args=None,
               model: torch.nn.Module = None,
               optimizer: Optional[Union[Optimizer, DeepSpeedOptimizerCallable]] = None,
               model_parameters: Optional[torch.nn.Module] = None,
               training_data: Optional[torch.utils.data.Dataset] = None,
               lr_scheduler: Optional[Union[_LRScheduler, DeepSpeedSchedulerCallable]] = None,
               distributed_port: int = TORCH_DISTRIBUTED_DEFAULT_PORT,
               mpu=None,
               dist_init_required: Optional[bool] = None,
               collate_fn=None,
               config=None,
               mesh_param=None,
               config_params=None):
    """初始化 DeepSpeed 引擎。
    参数:
        args: 一个包含 `local_rank` 和 `deepspeed_config` 字段的对象。
            如果提供了 `config`，此参数是可选的。

        model: 必填项：在应用任何包装器之前的 nn.Module 类。

        optimizer: 可选：用户定义的 Optimizer 或返回 Optimizer 对象的 Callable。
            如果提供，将覆盖 DeepSpeed JSON 配置中的任何优化器定义。
        model_parameters: 可选：torch.Tensors 或字典的可迭代对象。
            指定需要优化的张量。
        training_data: 可选：torch.utils.data.Dataset 类型的数据集。
        lr_scheduler: 可选：学习率调度器对象或一个 Callable，接收一个 Optimizer 并返回调度器对象。
            调度器对象应定义 `get_lr()`、`step()`、`state_dict()` 和 `load_state_dict()` 方法。
        distributed_port: 可选：主节点（rank 0）用于分布式训练期间通信的空闲端口。
        mpu: 可选：模型并行单元对象，需实现以下方法：
            `get_{model,data}_parallel_{rank,group,world_size}()`。
        dist_init_required: 可选：如果为 None，将根据需要自动初始化 torch 分布式；
            否则用户可以通过布尔值强制初始化或不初始化。
        collate_fn: 可选：合并样本列表以形成一个小批量的张量。
            在从 map-style 数据集中使用批量加载时使用。
        config: 可选：可以作为路径或字典传递的 DeepSpeed 配置，
            用于替代 `args.deepspeed_config`。
        config_params: 可选：与 `config` 相同，为了向后兼容保留。

    返回值:
        返回一个包含 `engine`, `optimizer`, `training_dataloader`, `lr_scheduler` 的元组。
        * `engine`: DeepSpeed 运行时引擎，用于包装客户端模型以进行分布式训练。
        * `optimizer`: 如果提供了用户定义的 `optimizer`，返回包装后的优化器；
          如果在 JSON 配置中指定了优化器也会返回；否则为 `None`。
        * `training_dataloader`: 如果提供了 `training_data`，则返回 DeepSpeed 数据加载器；
          否则为 `None`。
        * `lr_scheduler`: 如果提供了用户定义的 `lr_scheduler`，或在 JSON 配置中指定了调度器，
          返回包装后的学习率调度器；否则为 `None`。
    """

</code></pre><p><code>deepspeed</code>具体案例可以查看其官方示例：https://github.com/microsoft/DeepSpeedExamples.具体使用也很简单,因为<code>Deepspeed</code>将各种功能都封装好了，可以直接使用，一个建议<code>Demo</code>如下：</p><pre><code># 首先初始化
model_engine, optimizer, train_loader, _ = deepspeed.initialize(
        model=model,
        model_parameters=model.parameters(),
        training_data=train_dataset,
        config=config['deepspeed_config'] # 这里的话是直接将deepspeed的设置都存储到一个json文件里面了
    )
def train(model_engine, optimizer, train_loader, ...):
    ...
    image = image.to(model_engine.local_rank)
    out = model_engine(..)
    ...
    model_engine.backward()
    model_engine.step()
    ...
</code></pre><p>值得注意的是：</p><ul><li>1、如果需要访问设备，可以直接用：<code>model_engine.local_rank()</code>进行访问即可</li><li>2、如果再<code>deepspeed</code>参数（更加多的参数可以参考官方文档：<a href="https://www.deepspeed.ai/docs/config-json/#zero-optimizations-for-fp16-training">1</a>，<a href="https://deepspeed.readthedocs.io/en/latest/zero3.html#deepspeed.runtime.zero.config.DeepSpeedZeroConfig.contiguous_gradients">2</a>）中设置了 <em>半精度</em> 训练，在数据里面要设定：<code>images.to(model.local_rank).half()</code></li></ul><pre><code class="language-json">{
  "train_batch_size": 512,
  "gradient_accumulation_steps": 1,
  "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "loss_scale_window": 1000,
    "hysteresis": 2,
    "min_loss_scale": 1
  }, //开启半精度训练
  "optimizer": {
    "type": "Adam",
    "params": {
      "lr": 0.001,
      "betas": [0.9, 0.999],
      "eps": 1e-8,
      "weight_decay": 0.01
    }
  }, // 设置优化器
  "zero_optimization": {
    "stage": 2
  } // 指定zero的方式：1，2，3
}
</code></pre><ul><li>3、理论上分析，在显存占用上是 $P_{OS}&lt;P_{OS+g}&lt;P_{OS+g+p}$ 但是实验过程中会出现相反的情况，参考<a href="https://github.com/microsoft/DeepSpeed/issues/1302">这部分讨论</a>：1、在使用<code>deepspeed</code>中的<code>zero</code>设定时，<strong>需要保证模型的大小足够大（大小&gt;1B的参数）</strong>。于此同时在使用<code>stage=2</code>或者<code>stage=3</code>的时候可以分别指定下面参数：1、<code>reduce_bucket_size</code>，<code>allgather_bucket_size</code>；2、<code>stage3_max_live_parameters</code>， <code>stage3_max_reuse_distance</code></li><li>4、对于<code>zero</code>中<code>stage</code>设定，通过结合github上的<a href="https://zhuanlan.zhihu.com/p/630734624">讨论</a>：</li></ul><p>1、<code>stage=2</code>时：</p><pre><code class="language-json">{
"zero_optimization": {
    "stage": 2,
    "offload_optimizer": {
        "device": "cpu",
        "pin_memory": true
    },
    "allgather_partitions": true,
    "allgather_bucket_size": 2e8,
    "overlap_comm": true,
    "reduce_scatter": true,
    "reduce_bucket_size": 2e8,
    "contiguous_gradients": true
},
}
</code></pre><table><thead><tr><th style="text-align: center">参数</th><th style="text-align: center">含义</th><th style="text-align: center">当前值</th></tr></thead><tbody><tr><td style="text-align: center"><code>stage</code></td><td style="text-align: center"><code>1</code>: 仅优化优化器状态。<code>2</code>: 优化优化器状态和梯度。<code>3</code>: 优化优化器状态、梯度和模型参数。<code>0</code>:普通DDP</td><td style="text-align: center"><code>2</code></td></tr><tr><td style="text-align: center"><code>offload_optimizer</code></td><td style="text-align: center">是否将优化器状态迁移到其他设备（如 CPU 或 NVMe）</td><td style="text-align: center"><code>{ "device": "cpu", "pin_memory": true }</code></td></tr><tr><td style="text-align: center"><code>allgather_partitions</code></td><td style="text-align: center">在每个step结束时，选择用allgather集合通信操作还是一系列的broadcast从所有GPUs收集更新后的参数，一般不需要修改，论文中在分析集合通讯开销时就用了allgather</td><td style="text-align: center"><code>true</code></td></tr><tr><td style="text-align: center"><code>allgather_bucket_size</code></td><td style="text-align: center">动态收集参数时的最大通信块大小（字节）。<strong>较大值</strong>：提高效率但增加显存压力。<strong>较小值</strong>：减少显存压力但增加通信次数。</td><td style="text-align: center"><code>2e8</code> (200MB)</td></tr><tr><td style="text-align: center"><code>overlap_comm</code></td><td style="text-align: center">尝试在反向传播期间并行进行梯度通信</td><td style="text-align: center"><code>true</code></td></tr><tr><td style="text-align: center"><code>reduce_scatter</code></td><td style="text-align: center">是否启用 reduce-scatter 操作，将梯度分片和通信合并以降低显存需求和通信负担</td><td style="text-align: center"><code>true</code></td></tr><tr><td style="text-align: center"><code>reduce_bucket_size</code></td><td style="text-align: center">reduce-scatter 操作的最大通信块大小（字节）。<strong>较大值</strong>：提高效率但增加显存压力。<strong>较小值</strong>：减少显存压力但增加通信次数</td><td style="text-align: center"><code>2e8</code> (200MB)</td></tr><tr><td style="text-align: center"><code>contiguous_gradients</code></td><td style="text-align: center">是否将梯度存储为连续内存块，以减少显存碎片并提升梯度更新效率</td><td style="text-align: center"><code>true</code></td></tr></tbody></table><p>2、<code>stage=3</code>时：</p><pre><code class="language-json">{
"zero_optimization": {
      "stage": 3,
      "offload_optimizer": {
          "device": "cpu",
          "pin_memory": true
      }, //是否将优化器状态迁移到CPU
      "offload_param": {
          "device": "cpu",
          "pin_memory": true
      },
      "overlap_comm": true,
      "contiguous_gradients": true,
      "sub_group_size": 1e9,
      "reduce_bucket_size": "auto",
      "stage3_prefetch_bucket_size": "auto",
      "stage3_param_persistence_threshold": "auto",
      "stage3_max_live_parameters": 1e9,
      "stage3_max_reuse_distance": 1e9,
      "stage3_gather_16bit_weights_on_model_save": true
  },
}
</code></pre><table><thead><tr><th style="text-align: center">参数</th><th style="text-align: center">含义</th><th style="text-align: left">当前值</th></tr></thead><tbody><tr><td style="text-align: center"><code>stage</code></td><td style="text-align: center"><code>1</code>: 仅优化优化器状态。<code>2</code>: 优化优化器状态和梯度。<code>3</code>: 优化优化器状态、梯度和模型参数。<code>0</code>:普通DDP</td><td style="text-align: left"><code>3</code></td></tr><tr><td style="text-align: center"><code>offload_optimizer</code></td><td style="text-align: center">是否将<strong>优化器状态</strong>迁移到其他设备（如 CPU 或 NVMe）。优化器状态的存储可以迁移到 CPU 以释放显存。</td><td style="text-align: left"><code>{ "device": "cpu", "pin_memory": true }</code></td></tr><tr><td style="text-align: center"><code>offload_param</code></td><td style="text-align: center">是否将<strong>模型参数</strong>迁移到其他设备（如 CPU）。类似于优化器状态，模型参数可以迁移到 CPU 以降低显存压力。</td><td style="text-align: left"><code>{ "device": "cpu", "pin_memory": true }</code></td></tr><tr><td style="text-align: center"><code>overlap_comm</code></td><td style="text-align: center">尝试在反向传播期间并行进行梯度通信</td><td style="text-align: left"><code>true</code></td></tr><tr><td style="text-align: center"><code>contiguous_gradients</code></td><td style="text-align: center">是否将梯度存储为连续的内存块，<strong>启用后减少显存碎片，提高梯度更新效率</strong>。</td><td style="text-align: left"><code>true</code></td></tr><tr><td style="text-align: center"><code>sub_group_size</code></td><td style="text-align: center">设置参数分组大小，用于分配和通信的优化。<strong>大的值可以减少通信次数，适用于更大规模的模型</strong></td><td style="text-align: left"><code>1e9</code></td></tr><tr><td style="text-align: center"><code>reduce_bucket_size</code></td><td style="text-align: center">设置 reduce-scatter 操作的最大通信块大小（字节）。如果设置为 <code>auto</code>，DeepSpeed 会自动调整。</td><td style="text-align: left"><code>auto</code></td></tr><tr><td style="text-align: center"><code>stage3_prefetch_bucket_size</code></td><td style="text-align: center">为 stage 3 优化中的预取操作设置桶大小。如果设置为 <code>auto</code>，DeepSpeed 会自动调整。</td><td style="text-align: left"><code>auto</code></td></tr><tr><td style="text-align: center"><code>stage3_param_persistence_threshold</code></td><td style="text-align: center">在 stage 3 中设置模型参数持久化的阈值。如果设置为 <code>auto</code>，DeepSpeed 会自动调整。</td><td style="text-align: left"><code>auto</code></td></tr><tr><td style="text-align: center"><code>stage3_max_live_parameters</code></td><td style="text-align: center">保留在 GPU 上的完整参数数量的上限</td><td style="text-align: left"><code>1e9</code></td></tr><tr><td style="text-align: center"><code>stage3_max_reuse_distance</code></td><td style="text-align: center">是指将来何时再次使用参数的指标，从而决定是丢弃参数还是保留参数。 如果一个参数在不久的将来要再次使用（小于 <code>stage3_max_reuse_distance</code>），可以保留以减少通信开销。 使用<code>activation checkpointing</code>时，这一点非常有用</td><td style="text-align: left"><code>1e9</code></td></tr><tr><td style="text-align: center"><code>stage3_gather_16bit_weights_on_model_save</code></td><td style="text-align: center">在保存模型时是否收集 16 位权重。启用时可以将权重收集为 16 位格式，降低存储开销。</td><td style="text-align: left"><code>true</code></td></tr></tbody></table><p>3、其他<br/> 实际参数过程中，可能还需要设置<code>train_batch_size</code>，<code>gradient_accumulation_steps</code>（梯度累计次数），<code>optimizer</code>（优化器选择）</p><h3 id="代码操作">代码操作</h3><p><strong>使用deepspeed</strong></p><pre><code class="language-python">import json
config_path = '/root/LLM/model/dp_config.json'
with open(config_path, 'r') as f:
    dp_config = json.load(f)
args = ModelArgs

model = Transformer(args)
x = torch.randint(low=0, high= args.vocab_size, size=(1000, 1024), dtype=torch.long)
target = torch.randint(low=0, high= args.vocab_size, size=(1000, 1024), dtype=torch.long)
train_data = torch.utils.data.TensorDataset(x, target)

model, _, train_loader, _ = deepspeed.initialize(
    model= model,
    training_data = train_data,
    config_params= dp_config)

for epoch in range(10):  # 设置训练轮数
    model.train()  # 设置模型为训练模式
    total_loss = 0
    for batch in train_loader:
        inputs, targets = batch
        print(inputs.shape, targets.shape)
        inputs, targets = inputs.to(model.device), targets.to(model.device)
        logits, loss = model(inputs, targets)            
        model.backward(loss)
        model.step()
        total_loss += loss.item()
    avg_loss = total_loss / len(train_loader)
    print(f'Epoch {epoch+1}, Average Loss: {avg_loss}')
</code></pre><p><strong>使用accelerate</strong></p><pre><code class="language-python">from accelerate import Accelerator, DeepSpeedPlugin
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

def train(zero_stage=2):
    ds_plugin = DeepSpeedPlugin(
        zero_stage=zero_stage,
        gradient_accumulation_steps=2,
    )
    accelerator = Accelerator(deepspeed_plugin=ds_plugin)
    ...
    model, optimizer, train_dataloader = accelerator.prepare(model, optimizer, train_dataloader)
   
    model.train()
    for batch in train_dataloader:
        optimizer.zero_grad()
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss) 
        optimizer.step()
</code></pre><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/1910.02054 <br/> 2、https://zhuanlan.zhihu.com/p/513571706 <br/> 3、https://zhuanlan.zhihu.com/p/618865052 <br/> 4、https://zhuanlan.zhihu.com/p/504957661 <br/> 5、https://deepspeed.readthedocs.io/en/latest/initialize.html# <br/> 6、https://www.deepspeed.ai/docs/config-json/#batch-size-related-parameters <br/> 7、https://zhuanlan.zhihu.com/p/630734624</p> ]]></content:encoded><category>DeepSpeed框架</category><pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/24/deepspeed.html</guid></item><item><title>从梯度消失到模型过拟合：归一化、Dropout 和梯度裁剪的深度解析</title><link>https://www.big-yellow-j.top/posts/2025/02/23/dl-norm.html</link><description><![CDATA[神经网络训练中，为提升泛化能力并避免梯度消失/爆炸，常采用归一化、Dropout、梯度裁剪等训练稳定性机制。归一化技术（BatchNorm、LayerNorm、GroupNorm、InstanceNorm）通过统一层输入分布加速收敛，其核心区别在于归一化维度：BatchNorm沿批处理维度对单个特征归一化，适合大batch分类任务；LayerNorm沿单个样本所有特征维度，适用于NLP/RNN及小动态batch场景；InstanceNorm针对单个样本通道，用于风格迁移；GroupNorm将通道分组归一化，适合小batch或单样本CNN。Dropout通过训练时随机丢弃部分神经元（反向传...]]></description><content:encoded><![CDATA[<p>为了避免提高模型的泛化能力以及训练过程中避免梯度消失/梯度爆炸现象发生，我们通常会在神经网络中引入一系列“训练稳定性机制”。其中，归一化（Normalization）技术、Dropout 正则化和梯度裁剪（Gradient Clipping）是被广泛采用的三种核心手段：</p><ul><li>归一化方法（如 BatchNorm、LayerNorm 等）可以加速收敛、缓解梯度消失问题，并使模型对输入分布的变化更具鲁棒性。</li><li>Dropout 通过在训练过程中随机屏蔽部分神经元，有效防止模型对训练数据的过拟合，从而提升泛化能力。</li><li>梯度裁剪 则常用于避免梯度爆炸，尤其在处理长序列（如 RNN/LSTM）或深层网络时尤为关键，它能控制梯度的最大范数，防止参数更新过大导致训练不稳定。 <h2 id="1归一化方法layernormbatchnormgroupnorm">1、归一化方法（LayerNorm/BatchNorm/GroupNorm）</h2><p>归一化层是深度神经网络体系结构中的关键，在训练过程中确保各层的输入分布一致，这对于高效和稳定的学习至关重要。归一化技术的选择（Batch, Layer, GroupNormalization）会显著影响训练动态和最终的模型性能。每种技术的相对优势并不总是明确的，随着网络体系结构、批处理大小和特定任务的不同而变化。<br/> 数据归一化：</p></li></ul><blockquote><p>定义如下计算公式：</p> \[\mu_i= \frac{1}{m}\sum_{k\in S_i}x_k\] \[\sigma_i= \sqrt{\frac{1}{m}\sum_{k\in S_i}(x_k- \mu_i)^2+\epsilon}\] <p>4类标准化区别就在于对于参数$S_i$的定义！！！（<strong>也就是沿着哪个维度进行归一化处理</strong>）<br/> 如：对一组图片定义如下变量：$(N,C,H,W)$分别代表：<code>batch、channel、height、width</code><br/><strong>Bath-norm</strong>：$S_i={k_C=i_C}$<br/><strong>Layer-norm</strong>：$S_i={k_C=i_N}$<br/><strong>Instance-norm</strong>：$S_i={k_C=i_C,K_N=i_N}$<br/><strong>Group-norm</strong>：$S_i={k_N=i_N, \lfloor \frac{k_C}{C/G} \rfloor=\lfloor \frac{i_C}{C/G} \rfloor}$<br/> $G$代表组的数量，$C/G$每个组的通道数量</p></blockquote><p><img src="https://s2.loli.net/2025/06/21/SiEXaUzb9e6hAKf.webp" alt="image" width="789" height="232" loading="lazy" decoding="async"/></p><blockquote><p>上图都是在image上做的例子</p></blockquote><p><strong>BatchNorm</strong><br/> BN应用于<strong>一批数据中的单个特征</strong>，通过计算批处理上特征的均值和方差来独立地归一化每个特征。它允许更高的学习率，并降低对网络初始化的敏感性。<br/> 这种规范化发生在每个特征通道上，并应用于整个批处理维度，它在大型批处理中最有效，因为统计数据是在批处理中计算的。</p><p><strong>LayerNorm</strong><br/> LN计算用于归一化<strong>单个数据样本中</strong>所有特征的均值和方差。它应用于每一层的输出，独立地规范化每个样本的输入，因此不依赖于批大小。<br/> LN有利于循环神经网络(rnn)以及批处理规模较小或动态的情况。</p><blockquote><p><strong>RMSnorm</strong><br/> From：https://pytorch.org/docs/stable/generated/torch.nn.modules.normalization.RMSNorm.html</p><p>$y_i=\frac{x_i}{RMS(x)}*\gamma_i$，其中 $RMS(x)=\sqrt{\epsilon+ \frac{1}{n}\sum_{i=1}^{n}x_i^2}$<br/> 和 <strong>LayerNorm</strong>之间区别就在于没有减均值这一步的操作</p></blockquote><p><strong>InstanceNorm</strong><br/> IN对<strong>每个样本的每个通道</strong>进行独立的归一化处理<br/><strong>GroupNorm</strong><br/> GN将<strong>通道道分成若干组，并计算每组</strong>内归一化的均值和方差。这对于通道数量可能很大的卷积神经网络很有用，将它们分成组有助于稳定训练。GN不依赖于批大小，因此适用于小批大小的任务或批大小可以变化的任务。<br/><strong>不同归一化对比</strong>：</p><table><thead><tr><th><strong>方法</strong></th><th><strong>归一化范围</strong></th><th><strong>依赖 batch size</strong></th><th><strong>应用场景</strong></th><th><strong>原因</strong></th></tr></thead><tbody><tr><td><strong>BatchNorm</strong></td><td>Mini-batch 的每个维度</td><td>是</td><td>分类任务，CNN，较大 batch size</td><td>1. 通过 mini-batch 的统计信息减少特征偏移，保持分布稳定。<br/>2. 在分类任务中，特征分布的稳定性有助于提升模型收敛速度和最终性能。</td></tr><tr><td><strong>LayerNorm</strong></td><td>单个样本的所有特征</td><td>否</td><td>NLP，RNN，Transformer</td><td>1. 在 NLP 和序列建模中，训练的样本通常较小，不依赖 batch 统计量的 LayerNorm 更稳定。<br/>2. 对整个样本归一化，使得模型对序列长度或上下文无关。</td></tr><tr><td><strong>InstanceNorm</strong></td><td>单个样本的每个通道独立</td><td>否</td><td>风格迁移，生成任务</td><td>1. 强调样本内部特征分布的一致性，适合强调局部特征（如风格迁移中的纹理）。<br/>2. 不考虑全局或跨样本的统计信息，能更好保留样本特有的风格特性。</td></tr><tr><td><strong>GroupNorm</strong></td><td>分组特征</td><td>否</td><td>小 batch 或单样本 CNN</td><td>1. 对小 batch 或单样本任务，避免了 BatchNorm 的统计不稳定问题。<br/>2. 分组归一化平衡了跨通道信息的利用和归一化稳定性，适合卷积特征提取。</td></tr></tbody></table><blockquote><p>From: https://blog.csdn.net/qq_36560894/article/details/115017087</p></blockquote><p><strong>BatchNorm</strong>：在同一个特征通道（Channel）内，跨一个批次的 所有样本（N）和空间位置（H, W） 计算均值和方差。对batch size敏感。当batch size很小（比如1或2）时，计算的均值和方差噪声很大，不足以代表整个数据分布，导致效果变差。<br/><strong>LayerNorm</strong>：在单个样本内，对所有特征（通常是所有通道C和空间位置H, W）计算均值和方差。与batch size无关，非常适合小批量或在线学习。因为它对单个样本的所有特征进行归一化，非常适合处理变长序列（如自然语言），因为它的计算不依赖于序列长度<br/><strong>InstanceNorm</strong>：在单个样本的单个通道内，对所有空间位置计算均值和方差。可以看作是“批大小为1的BatchNorm”，但它是逐样本、逐通道独立进行的<br/><strong>GroupNorm</strong>：是LayerNorm和InstanceNorm的折中。将通道（C）分成G个组（Group），然后在单个样本的每个组内，计算所有通道和空间位置的均值和方差。<br/><img src="https://s2.loli.net/2025/11/11/fxepYTQ532y6Cc8.webp" alt="image" width="693" height="286" loading="lazy" decoding="async"/><br/><strong>代码</strong>：（<code>pytorch</code>）</p><pre><code class="language-python">import torch
import torch.nn as nn

x = torch.randn(32, 64, 32, 32)
batch_norm = nn.BatchNorm2d(64)
layer_norm = nn.LayerNorm([64, 32, 32])
group_norm = nn.GroupNorm(8, 64) # 对64通道分8组
isins_norm = nn.InstanceNorm2d(64)
rms_norm = nn.RMSNorm([64, 32, 32])

out_batch = batch_norm(x)
out_layer = layer_norm(x)
out_gropu = group_norm(x)
out_insta = isins_norm(x)
out_rms = rms_norm(x)
</code></pre><blockquote><p><a href="https://pytorch.org/docs/stable/generated/torch.nn.InstanceNorm2d.html"><code>InstanceNorm2d</code></a>、<a href="https://pytorch.org/docs/stable/generated/torch.nn.GroupNorm.html"><code>GroupNorm</code></a>、<a href="https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html"><code>LayerNorm</code></a>、<a href="https://pytorch.org/docs/main/generated/torch.nn.BatchNorm2d.html#torch.nn.BatchNorm2d"><code>BatchNorm2d</code></a><br/><em>假设输入形状</em>: <code>N,C,H,W</code><br/> 1、<code>InstanceNorm2d</code>和<code>BatchNorm2d</code>在代码实践上相似都是对<code>C</code>计算<br/> 2、<code>LayerNorm</code>对于输入为<code>N,C,H,W</code>（比如说图像）那么选择<code>C,H,W</code>，如果输入为<code>N,Dim1,Dim2</code>（比如说文本）那么对 <code>Dim2</code>计算<br/> 3、<code>GroupNorm</code>则是对<code>C</code>分<code>n</code>个组</p></blockquote><hr/><h2 id="2dropout">2、Dropout</h2><p>在 Dropout 中，每个神经元在训练过程中有一定概率$𝑝$, $p$被随机设置为 0（即被“丢弃”），这种行为可以用如下数学公式描述：<br/> 假设某一层神经元的输入表示为一个向量$x=[x_1,… x_n]$该层输出为：$y=[y_1,…,y_n]$，那么Dropout计算过程：首先随机生成掩码向量：$r_i \sim Bernoulli(p),i=1,…,n$（$P(r_i=1)=p, P(r_i=0)=1-p$），对输入进行掩码得到：$\tilde{x}=r \odot x$其中$\odot$代表按元素相乘（其实整个过程也就是随机将神经元乘0）<br/><strong>反向传播过程中</strong>：对于被Dropout的神经元其梯度也会被置于0</p><h2 id="3梯度裁剪策略">3、梯度裁剪策略</h2><p>梯度裁剪（Gradient Clipping）是一种在训练神经网络时常用的技术，它用于防止梯度爆炸问题。梯度爆炸是指在训练过程中，梯度的大小急剧增加，导致权重更新过大，从而使得模型无法收敛或者性能急剧下降的现象。在PyTorch中，可以使用 <code>torch.nn.utils.clip_grad_norm</code> 或 <code>torch.nn.utils.clip_grad_value_</code> 函数来实现梯度裁剪。</p><ul><li>1、梯度范数裁剪：<code>torch.nn.utils.clip_grad_norm</code><br/> 该方法通过限制梯度的整体L2范数（Euclidean norm），防止梯度过大，从而避免梯度爆炸。如果所有梯度的 L2 范数之和超过指定阈值（max_norm），<strong>就将其按比例缩小，保持方向不变，但整体“长度”被控制</strong>。假设梯度为 $\mathbf{g}$，若 $|\mathbf{g}|_{2}&gt;max_norm$，则：</li></ul> \[g\leftarrow{g}\frac{max_norm}{||g||_{2}}\] <pre><code class="language-python">torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
</code></pre><p>所有的模型参数梯度如果整体范数超过1就会被缩放</p><ul><li>2、梯度值裁剪：<code>torch.nn.utils.clip_grad_value_</code></li></ul><p>直接将每一个参数的梯度值限制在指定范围内，超过范围就被“硬截断”。不考虑整体范数，只关注单个数值大小。</p><pre><code class="language-python3">torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
</code></pre><p>这样一来模型梯度就会被限制在$[-0.5,0.5]$</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Sun, 23 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/23/dl-norm.html</guid></item><item><title>Kimi/DeepSeek最新论文MoBA与NSA阅读</title><link>https://www.big-yellow-j.top/posts/2025/02/21/Kimi-DS-Paper.html</link><description><![CDATA[长上下文处理中平方级运算与存储复杂度是大模型推理优化的关键挑战，解决方向包括Token Dropping、KVCache量化压缩、Prompt压缩及结构性稀疏压缩等，核心在于稀疏注意力（Sparse Attention）技术。稀疏注意力通过减少元素关联性计算，让每个元素仅与部分元素关联，高效处理Q、K、V关系。Kimi的MOBA方法让Q与K、V的子集计算，将序列分块后利用改进的Router机制筛选相关块，结合causal mask控制子集访问，通过门控向量打分和top-k选择减少计算量。DeepSeek的NSA则采用动态层次稀疏策略，结合粗粒度Token压缩与细粒度Token选择，分块后...]]></description><content:encoded><![CDATA[<p><strong>DeepSeek</strong>最新论文：<a href="https://arxiv.org/pdf/2502.11089">Native Sparse Attention: Hardware-Aligned and Natively Trainable Sparse Attention</a>以及 <strong>Kimi</strong>最新论文MOBA: <a href="https://arxiv.org/pdf/2502.13189">MIXTURE OF BLOCK ATTENTION FOR LONG-CONTEXT LLMS</a>这几篇文章都是针对长上下文的压缩方法，长上下文带来的平方级别的运算或存储复杂度给推理优化带来非常大的影响，因此是当前大模型推理优化中非常重要的一项研究内容。解决长上下文问题，主要分为稀疏化之后的 Token Dropping、KVCache 的量化压缩、Prompt Compression 提示词压缩、还有结构性稀疏压缩等几大类。里面都提到了 <strong>稀疏</strong>这一个内容，什么是 <strong>稀疏注意力</strong>（Sparse Attention）</p><h2 id="1稀疏注意力sparse-attention">1、稀疏注意力（Sparse Attention）</h2><p>传统self-attention计算在理论上时间和空间占用为$O(n^2)$其中n为序列长度，这是因为对于一个长度为n的序列，任意向量之间都需要计算相关度，得到一个$n^2$的相关度矩阵，因此为$O(n^2)$。借鉴<a href="https://spaces.ac.cn/archives/6853">Blog</a>中描述：<br/> 对于self-attention中 <strong>每个元素都跟序列内所有的元素都有关联</strong> ，那么一个基本的思路就是 <strong>减少关联性的计算</strong>，也就是认为每个元素只跟序列内的一部分元素相关，这就是<strong>稀疏Attention</strong>的基本原理（其实就是如何高效的处理Q，K，V之间关系，不要全部计算）。</p><h2 id="2kimimoba">2、Kimi：MOBA</h2><blockquote><p>修改代码：<a href="../code/MoBAAttention.py.txt">⚙</a></p></blockquote><p><img src="https://s2.loli.net/2025/06/21/cZjgFhXAoEs2Jun.webp" alt="image" width="1085" height="511" loading="lazy" decoding="async"/></p><p>正如上面提到的，文本长度（n）变成导致无论是时间还是空间上消耗增加，因此在MOBA中就是让 <strong>Q</strong>去和K，V的子集进行计算：</p> \[MoBA(q,K,V)=\text{Softmax}(qK[I]^T)V[I]\] <p>$I$代表被筛选的子集。其中如何筛选子集以及如何确定子集个数。对于后者子集个数的确定，对于长度为$N$可以直接划分到$n$个blocks中,至于如何筛选子集，作者提到就是直接通过MoE中的 router机制去筛选出来即可。</p><p><img src="https://s2.loli.net/2025/06/21/pUufnwcH4JZIkTj.webp" alt="image" width="1132" height="609" loading="lazy" decoding="async"/></p><p>在论文中 <strong>Router</strong>设计方法（不是像MoE里面直接简单用一个MLP计算），参考<a href="https://github.com/MoonshotAI/MoBA/blob/master/moba/moba_naive.py#L7">代码</a>以及上面流程图的描述，再 MoBA中做法操作如下：</p><ol><li>遍历 batch 中每个样本。通过 cu_seqlens 获取每个样本的起止位置，对其独立处理。</li><li>将 key 按 moba_chunk_size 分块，并计算每个块的“门控向量”（均值）（<strong>对应算法图第4步</strong>）。这用于构造类似 Gated Attention 的机制。</li><li>对每个 query，根据与每个 block gate 的打分，选出 top-k 个可以“访问”的 block：避免 query attend 到未来（使用了 causal mask 的变种），利用 torch.topk 得到 gating 分数前 k 个最相关的 chunk</li><li>构造注意力 mask：只允许 attend 到 top-k 中的块，叠加 causal mask，防止信息泄漏</li><li>执行注意力计算：qk = q·k^T 得到 attention logits，加入 gate mask，并进行 softmax，最终用加权和生成输出：o = softmax(qk + gate_mask) @ v</li></ol><h2 id="3deepseeknsa">3、DeepSeek：NSA</h2><p>正如论文里面描述的，NSA采用了一种动态层次稀疏策略，将粗粒度的<strong>token压缩</strong>与细粒度的<strong>token选择</strong>相结合，以保持全局上下文感知和局部精度。换言之就是通过：<strong>压缩Token以及筛选Token来实现稀疏注意力</strong></p><p><img src="https://s2.loli.net/2025/06/21/2XJ9tvIcEpZSMh6.webp" alt="image" width="1224" height="360" loading="lazy" decoding="async"/></p><p>从上面提供的结构图，在NSA中的稀疏注意力大致3个部分：</p><ul><li><p><strong>1、compression，压缩</strong>：按照论文里面的描述，作者实现方式为，首先对K/V进行通过一个指定一个窗口进行划分（$id+1:id+l$）（有点像ViT中将图片切分成不同的小batch操作一样）然后再去通过一个 <strong>可学习的MLP</strong>来实现最后压缩</p></li><li><p><strong>2、selection，筛选</strong>：这块比较有意思，因为最开始K/V都已经通过分组了，如果在要去挑选哪些重要/哪些不重要，不去和Q计算你很难得重要性，但是第一步中不是有一个“压缩注意力”得分，那就直接用第一步中计算得到的压缩内容来得到重要性</p></li></ul> \[\mathbf{p}_t^{\mathrm{cmp}}=\mathrm{Softmax}\left(\mathbf{q}_t^T\tilde{K}_t^{\mathrm{cmp}}\right)\] <ul><li><strong>3、sliding window，滑动窗口</strong></li></ul><p>简单总结一下上面处理，处理长度长问题，就可以先 <strong>分块</strong>，然后去对不同块之间进行压缩，但是如果只是简单这样对于信息丢失而言很大，因此会有一个 “筛选”操作来弥补信息丢失问题。对于滑动窗口而言，进一步对信息进行弥补（就比如有些多模态里面除了用batch信息之外还会用到全局信息，不要让模型过度的关注细节内容）</p><h2 id="总结">总结</h2><p>从思路上MoBA和NSA都有一个相通的点，对于 <strong>稀疏注意力</strong>实现，都是通过“筛选”操作，但是“注意力筛选”势必要用到$QK^T$计算，因此两者都有一个有意思点，都会用一个<strong>小的替换大的</strong>（<strong>先分块再去压缩处理</strong>）</p><h2 id="参考">参考</h2><p>1、<a href="https://arxiv.org/pdf/2502.11089">Native Sparse Attention: Hardware-Aligned and Natively Trainable Sparse Attention</a><br/> 2、<a href="https://arxiv.org/pdf/2502.13189">MIXTURE OF BLOCK ATTENTION FOR LONG-CONTEXT LLMS</a><br/> 3、<a href="https://arxiv.org/pdf/1904.10509">Generating Long Sequences with Sparse Transformers</a><br/> 4、https://spaces.ac.cn/archives/6853<br/> 5、https://zhuanlan.zhihu.com/p/24841366485</p> ]]></content:encoded><category>paper</category><pubDate>Fri, 21 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/21/Kimi-DS-Paper.html</guid></item><item><title>Docunmen AI 中图像高分辨率处理思路汇总</title><link>https://www.big-yellow-j.top/posts/2025/02/19/DocAI-HR.html</link><description><![CDATA[本文介绍了三种高分辨率图像处理办法。DocKylin通过图像梯度转化为黑白，去除水平/竖直方向冗余像素，实现高分辨率图像处理；同时采用聚类算法降低token维度，将token分为必要与非必要两类，通过计算余弦相似度将非必要token聚合到必要token中。AdaptVision针对动态分辨率处理，对比LLaVa、Monkey的固定分辨率，思路是创建画布并切割为patch，设定每个patch固定尺寸及横纵比范围，筛选合适横纵比后扩大图像至对应大小再切割。DC²用于处理4K/2K高分辨率图像，指出固定分辨率会导致信息损失，可通过文本信息补偿，分三步：Divide将图像切割为4部分，通过分层聚...]]></description><content:encoded><![CDATA[<h2 id="1冗余内容处理办法">1、冗余内容处理办法</h2><p><code>DocKylin</code>直接对横平竖直去除冗余的像素（比如文本中大量的空白，直接通过将图片梯度转化（黑白），然后分别将水平/竖直方向上的“没有元素”内容进行连接然后继续去除，可以<strong>实现模型对高分辨率的图像的处理</strong>）</p><p><img src="https://s2.loli.net/2025/06/21/O13WYhKe4ZEQIJf.webp" alt="image" width="1071" height="269" loading="lazy" decoding="async"/></p><p>对于编码后的<code>token</code>，认为对于<code>token</code>存在必要/非必要的token，通过<strong>聚类算法降低token维度</strong>，处理为2类（根据相似的token的数量来确定那些是必要的那些是非必要的），而后通过计算这两类之间的余弦相似度将非必要的token aggregate到必要的token中</p><p><img src="https://s2.loli.net/2025/06/21/7BEWlkDA1HrfqQF.webp" alt="image" width="694" height="223" loading="lazy" decoding="async"/></p><p>聚类算法：</p><p><img src="https://s2.loli.net/2025/06/21/GFthKxseAQymS9C.webp" alt="image.png" width="673" height="544" loading="lazy" decoding="async"/></p><p>实际测试：1、裁剪iamge；2、减少冗余token测试效果：</p><p><img src="https://s2.loli.net/2025/06/21/hjdSbTAMwa73Xtm.webp" alt="image.png" width="788" height="543" loading="lazy" decoding="async"/></p><h2 id="2动态分辨率处理办法">2、动态分辨率处理办法</h2><p><code>AdaptVision</code>:动态的处理图像分辨率（对比LLaVa（$224 \times 224$）, Monkey（$896 \times 1344$）都是将图片固定到一个分辨率）</p><p><img src="https://s2.loli.net/2025/06/21/9ZIWfLbrPtj8T7i.webp" alt="image" width="417" height="480" loading="lazy" decoding="async"/></p><p>处理思路比较简单，类似：提前创建一个$1008 \times 1008$的“画布”，然后将画布切割为$3 \times 3$然后将图像放到画布左上角，看其所占用的区域范围，根据空间将图像转化到指定大小。</p><p><img src="https://s2.loli.net/2025/06/21/K6F9mqvCoxitPB7.webp" alt="image" width="495" height="284" loading="lazy" decoding="async"/></p><p>类似的思路如<a href="https://arxiv.org/pdf/2404.16821">论文1</a>、<a href="https://arxiv.org/abs/2501.06986">论文2中</a>处理思路，对于 <strong>动态分辨率</strong>处理，方法如下：</p><p><img src="https://s2.loli.net/2025/06/21/1bkyzXTwflExLqO.webp" alt="image" width="757" height="479" loading="lazy" decoding="async"/></p><p>因为都要将图片切片（分割成不同的小<code>patch</code>）区别之前做法可能是先将图片固定到一个尺寸，然后切割，上面两篇论文中动态分辨率处理思路就是：对于每个<code>patch</code>先提前设定一个大小（比如说每个<code>patch</code>都固定尺寸为$448\times 448$）然后在设定一个 <strong>横纵比</strong>范围，然后将图片筛选出合适的 <strong>横纵比</strong>。比如说上面图片：</p><ul><li>输入：$800\times1300$</li><li>挑选合适比例，因为每个<code>patch</code>都是$448\times448$，因此：$800/448: 1300/448≈2:3$，因此先将图片扩大到：$448\times2,448\times3=896\times1344$，然后再去切割即可</li></ul><h2 id="3dc2">3、$DC^2$</h2><p><code>DC^2</code>:处理高分辨率图像（4K/2K），论文提出通过将<strong>图像固定到一个固定的分辨率会导致较大的信息损失，增加模型的不确定性，但是可以通过添加文本信息进行补偿</strong>（将文本和image进行融合）。论文通过3步处理高分辨率图像：1、Divide：将图像切割为4部分（切割过多的patch会破坏目标完整性），然后计算每个部分之间的相似性，通过设定阈值来对形似部分通过 <strong>分层聚合</strong>的方式进行合并。2、Conquer:对于切分（并且进行合并）的patch以及完整的patch都生成文字描述，并且通过对生成的描述再通过llm抽取出里面的实体。并且将内容加入到完整的图像中并且进行类似处理。3、Combine：通过对leaf node以及no-leaf node提取到的实体，对两部分实体计算交集</p><p><img src="https://s2.loli.net/2025/06/21/GEY5xHRWbloZwcy.webp" alt="image" width="665" height="329" loading="lazy" decoding="async"/></p><h1 id="参考">参考：</h1><p>1、<a href="https://arxiv.org/abs/2406.19101">DocKylin: A Large Multimodal Model for Visual Document Understanding with Efficient Visual Slimming</a><br/> 2、<a href="https://arxiv.org/abs/2408.16986">AdaptVision: Dynamic Input Scaling in MLLMs for Versatile Scene Understanding</a><br/> 3、<a href="https://arxiv.org/pdf/2404.16821">How Far Are We to GPT-4V? Closing the Gap to Commercial Multimodal Models with Open-Source Suites</a><br/> 4、<a href="https://arxiv.org/abs/2501.06986">LEO: Boosting Mixture of Vision Encoders for Multimodal Large Language Models</a></p> ]]></content:encoded><category>paper</category><pubDate>Wed, 19 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/19/DocAI-HR.html</guid></item><item><title>深入探讨Attention变种与内存优化：从MHA到Flash/Page Attention</title><link>https://www.big-yellow-j.top/posts/2025/02/17/Attention.html</link><description><![CDATA[本文详细介绍常用Attention操作及KV-cache内存优化方法。涵盖多头注意力（Multi Head Attention）的QKV计算原理、softmax归一化作用，因果注意力（Casual Attention）的屏蔽机制，滑动窗口注意力（Sliding Window Attention）的局部交互逻辑；同时讲解KV-cache优化技术，包括Flash Attention的分块计算（利用HBM与SRAM提升效率）、Multi-head Latent Attention（MLA）的低维压缩存储，以及Page Attention的内存分页管理（解决预留浪费与碎片化问题），有效降低显存占...]]></description><content:encoded><![CDATA[<p>本文主要介绍常用的Attention操作（多头注意力等）以及在KV-cahce中如何节约内容的操作</p><h2 id="一attention类别">一、Attention类别</h2><table><thead><tr><th style="text-align: center">类别</th><th style="text-align: center">复杂度</th><th style="text-align: center">是否精确</th><th style="text-align: center">典型代表模型/论文</th><th style="text-align: center">当前主流实现方式</th></tr></thead><tbody><tr><td style="text-align: center">Standard Scaled Dot-Product</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">原 Transformer</td><td style="text-align: center">PyTorch SDPA / naive</td></tr><tr><td style="text-align: center">Multi-Head Attention (MHA)</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">几乎所有</td><td style="text-align: center">PyTorch SDPA</td></tr><tr><td style="text-align: center">Multi-Query Attention (MQA)</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">Grok-1, 早期 Llama</td><td style="text-align: center">PyTorch SDPA + head复用</td></tr><tr><td style="text-align: center">Grouped Query Attention (GQA)</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">Llama-3, Mistral, Qwen2</td><td style="text-align: center">PyTorch SDPA</td></tr><tr><td style="text-align: center">FlashAttention-2/3/4</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">几乎所有现代推理框架</td><td style="text-align: center">Dao-AI Lab/flash-attention 包</td></tr><tr><td style="text-align: center">Sparse Attention</td><td style="text-align: center">&lt; O(n²)</td><td style="text-align: center">近似/精确</td><td style="text-align: center">Longformer, BigBird</td><td style="text-align: center">xFormers sparse / Triton 自定义</td></tr><tr><td style="text-align: center">Block-Sparse / Local</td><td style="text-align: center">&lt; O(n²)</td><td style="text-align: center">是/近似</td><td style="text-align: center">Mistral Sliding Window</td><td style="text-align: center">xFormers block-sparse / Flash</td></tr><tr><td style="text-align: center">Linear Attention</td><td style="text-align: center">O(n)</td><td style="text-align: center">近似</td><td style="text-align: center">Performer, Linformer</td><td style="text-align: center">Triton / 自定义</td></tr><tr><td style="text-align: center">Multi-Head Latent Attention (MLA)</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">DeepSeek-V3</td><td style="text-align: center">模型自定义实现</td></tr><tr><td style="text-align: center">PagedAttention</td><td style="text-align: center">O(n²)</td><td style="text-align: center">是</td><td style="text-align: center">vLLM / 连续批处理</td><td style="text-align: center">vLLM 内置</td></tr></tbody></table><p>对于上述各类Attention计算实现代码（抛去那些框架内置的如flash attention等）：<a href="https://github.com/shangxiaaabb/ProjectCode/tree/main/code/Python/Attention/Attention.ipynb">Attention.ipynb</a></p><h3 id="1multi-head-attention">1、Multi Head Attention</h3><p>关于 <strong>Multi Head Attention</strong>网上有较多的解释了，这里主要记录如下几点<br/> 1、对于注意力计算公式的理解：</p> \[Attention(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V\] <p><strong>首先是</strong>对于Q、K、V如此计算缘由，<a href="https://arxiv.org/pdf/1706.03762">论文</a>最开始是用在NLP中，因此我们以 NLP 角度来解释。假设输入的 Q、K、V 形状为 $n \times d_k$，其中 $n$ 是文本 token 的数量，$d_k$ 是键（Key）和查询（Query）的维度。通过线性变换，我们将 token 处理为 $d_k$ 维的表示。计算 $QK^T$ 后，得到一个 $n \times n$ 的矩阵，可以理解为 token 之间的<strong>注意力权重</strong>。随后，我们用这些注意力权重加权求和 Value 矩阵 $V$，从而更新 token 表示。<br/><strong>其次</strong>为什么在公式里面要除$\sqrt{d_k}$呢？<strong>1.防止内积值过大，保持稳定的梯度</strong>。假设 $Q$ 和 $K$ 的每个元素服从均值为 0，方差为 $\sigma^2$ 的分布。$QK^T$ 的每个元素是 $d_k$ 个元素的内积，按照独立同分布假设，结果的方差会随着 $d_k$ 增大而增大，大约是 $\mathbb{V}[QK^T] \approx d_k \sigma^2$。这样，$QK^T$ 的值会随着 $d_k$ 的增大而变大，<strong>导致 softmax 归一化后，梯度变得很小，训练变得不稳定</strong>。通过除以 $\sqrt{d_k}$，可以让 $QK^T$ 的方差大致保持在 1 的数量级，使 softmax 输出不会过于极端（接近 0 或 1），从而保持训练稳定性。<strong>2. 让 softmax 具有合适的分布</strong>，避免梯度消失softmax 计算的是 $e^{x_i}$，如果 $x_i$ 过大，会导致梯度消失，模型难以学习。通过 $\sqrt{d_k}$ 归一化，控制 $QK^T$ 的范围，使 softmax 输出不会过于极端，从而提高训练效果。<br/> 2、之所以要采用多头，这个理由也比较简单，在计算 $QK^T$ 时，只能基于一个相同的查询-键表示来计算注意力分数，可能会偏向某一种关系模式，导致模型难以捕捉更多层次的语义信息<br/> 3、在模型结构里面的残差处理思路是：$\text{Norm}(x+f(x))$也就是说先通过MHA处理而后残差连接欸，但是<strong>残差会进一步放大方差</strong> 因此也有提出：$x+\text{Norm}(f(x))$前面提到的两种分别是Post Norm以及Pre Norm。对于那种好那种坏并没有很好的解释，与此同时有另外一种连接方式：$x+ \alpha f(x)$在后续训练中不断更新$\alpha$，<a href="https://spaces.ac.cn/archives/8620">参考</a>$\alpha$以固定的、很小的步长慢慢递增，直到增加到$\alpha=1$就固定下来。<br/><img src="https://s2.loli.net/2025/06/21/lwJKWxc2oXaFfGS.webp" alt="image" width="300" height="325" loading="lazy" decoding="async"/><br/> 假设输入为：<code>batch_size, seq_length</code>（值得注意的是：一般来说在<code>data_loader</code>中我们会去定义一个<code>collate_fn</code>函数用来弥补文本长度不统一的问题（这里是因为，对于输入输入文本在长度上必然不一致，通过<code>tokenizer</code>进行处理之后，回去额外补充一个填充量，比如说<code>PAD</code>））输入模型首先通过一个<code>nn.embedding</code>进行处理（这个<code>nn.embedding</code>是可学习的）假设输出为<code>512</code>（也就是我们定义的变量<code>d_model</code>）这样一来我们输入就会变成：<code>batch_size,seq_length,d_model</code>然后就是直接输入到<code>attention</code>中进行计算了。有些代码是将单头和多头分开计算，但是结合起来更加便捷。这样就需要首先计算<strong>WQ</strong>等，可以直接用<code>nn.linear(d_model, 3*d_model)</code>然后后续就可以直接再去将其进行拆分拆分到q、k、v中去。因为我是要进行多头计算，因此就会<code>qkv = qkv.reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)</code>然后再去分配到q、k、v中<code>q, k, v = qkv[0], qkv[1], qkv[2]</code>这样每个就会变成：<code>batch_size, n_heads, seq_length, head_dim</code>再去对这个计算attention（里面的<code>head_dim＝d_model/n_heada</code>）计算完成之后再去将所有头的结果拼接起来<code> y = y.transpose(1, 2).contiguous().view(B, T, C)</code><br/> 这样就是一个比较完整的计算过程。<br/><strong>补充Attention复杂度计算</strong>：假设输入数据是 $(bs,n,d)$，那么：<br/> 1、proj计算过程：$3\cdot (bs\cdot n\cdot d \cdot d)=O(bs\cdot n\cdot d^2)$<br/> 2、Attention计算过程：$O(bs\cdot n^2\cdot d)$<br/> 3、输出投影：$O(bs\cdot n\cdot d^2)$，最后得到总计算量（第1和3都是常熟量）为：$O(bs\cdot n^2\cdot d+ bs\cdot n\cdot d^2)$</p><h3 id="2casual-attention">2、Casual Attention</h3><p>因果注意力的主要目的是限制注意力的计算，使得<strong>每个位置的查询只能与当前和之前的位置计算注意力得分，而不能“窥视”未来的位置</strong>。具体来说：对于位置$𝑖$，模型只能考虑位置 $1,2,…,𝑖$的信息，而不能考虑位置$𝑖+1,𝑖+2,…,𝑛$。因此，当计算每个位置的注意力时，键（key）和值（value）的位置会被限制在当前的位置及其之前的位置。实现方式也很简单直接最注意力矩阵进行<strong>屏蔽</strong>即可，比如说注意力矩阵为：<br/><img src="https://s2.loli.net/2025/06/21/thMSJybzu1d395W.webp" alt="image" width="1071" height="503" loading="lazy" decoding="async"/></p><h3 id="3sliding-window-attention">3、Sliding Window Attention</h3><p><img src="https://s2.loli.net/2025/09/22/v9U6Gacbf52uzMs.webp" alt="image" width="242" height="297" loading="lazy" decoding="async"/><br/> SWA的核心思想是指：假设一个单词或token的含义，通常主要受其周围邻近的token影响，也就是说每个 token 仅与窗口内的其他 token 交互。这里的窗口大小为3。包括自己在内，每个位置只能往前看3个输入。</p><h2 id="二内存优化管理">二、内存优化管理</h2><h3 id="1flash-attention">1、Flash Attention</h3><p><a href="https://arxiv.org/pdf/2205.14135">论文</a>提出，是一种高效的注意力计算方法，旨在解决 Transformer 模型在处理长序列时的计算效率和内存消耗问题。<strong>其核心思想是通过在 GPU 显存中分块执行注意力计算，减少显存读写操作，提升计算效率并降低显存占用</strong>。需要了解一点到底是上面导致计算速度慢的问题，按照论文里面的描述（下面右图），在计算耗时中矩阵Matmul相对耗时没那么长，计算耗时大头在Mask、Softmax、Dropout这些（这些需要进行全局计算），而这些之所以耗时比较长是因为比如mask可能就需要遍历所有nxn矩阵然后逐一写入-inf，而softmax中也因为需要计算整个列也会导致耗时较大。<br/><img src="https://s2.loli.net/2025/06/21/rOHS2XYvQh846IK.webp" alt="1" width="896" height="351" loading="lazy" decoding="async"/></p><blockquote><p><strong>HBM</strong>（High Bandwidth Memory，高带宽内存）:是一种专为高性能计算和图形处理设计的内存类型，旨在提供高带宽和较低的功耗。HBM 常用于需要大量数据访问的任务，如图形处理、大规模矩阵运算和 AI 模型训练。 <br/><strong>SRAM</strong>（Static Random Access Memory，静态随机存取存储器）:是一种速度极快的存储器，用于存储小块数据。在 GPU 中，SRAM 主要作为缓存（如寄存器文件、共享内存和缓存），用于快速访问频繁使用的数据。例如在图中 FlashAttention 的计算中，将关键的计算块（如小规模矩阵）存放在 SRAM 中，减少频繁的数据传输，提升计算速度。<br/> 简单理解就是SRAM是我的电脑芯片的内存（小但是计算快）、HBM对应我的内存条（大但是慢）</p></blockquote><p><code>Flash Attention</code>计算机制简单总结就是：<strong>分块计算</strong>在传统注意力计算会将整个注意力矩阵 (N×N) 存入 GPU 内存（HBM），这对长序列来说非常消耗内存，FlashAttention 将输入分块，每次只加载一小块数据到更快的 SRAM 中进行计算，传统<code>Attention</code>计算和<code>flash attention</code>计算：<br/><img src="https://s2.loli.net/2025/06/21/6hLGm7WMqBkgyUr.webp" alt="1" width="852" height="645" loading="lazy" decoding="async"/><br/> 传统的计算和存储都是发生再<code>HBM</code>上，而对于<code>flash attention</code>则是<strong>首先</strong>会将<code>Q,K,V</code>进行划分（算法1-4：整体流程上首先根据<code>SRAM</code>的大小<code>M</code>去计算划分比例（$\lceil \frac{N}{B_r} \rceil$）然后根据划分比例去对<code>QKV</code>进行划分这样一来Q：$N\times d$就会被划分为不同的小块，<strong>然后计算</strong><code>Attention</code>（算法5-15），计算中也容易发现：先将分块存储再<code>HBM</code>上的值读取到<code>SRAM</code>上再它上面进行计算，不过值得注意的是：在传统的$QK^T$计算之后通过<code>softmax</code>进行处理，但是如果将上述值拆分了，再去用普通的<code>softmax</code>就不合适，因此使用<code>safe softmax</code>，对于普通的softmax计算<br/> \(x_i=\frac{e^{x_i}}{\sum e^{x_j}}\)<br/><code>safe softmax</code>（主要防止输出过大溢出，就减最大值）:<br/> \(x_i=\frac{e^{x_i-max(x_{:N})}}{\sum e^{x_j-max(x_{:N})}}\)<br/> 其实这里就提出一个对于Softmax的问题：使用传统的softmax可能会导致一个数值溢出问题。而对于mask部分在flash-attn2/3中优化比较大，比如说casual-attn只能看到过去，那么就有很多是没有参与计算的，比如score中只有 $(1,1)$ 位置有值那么也就只需要q第一行，k第一列没别要取用其他信息（<strong>其实在这里也能够说明使用mask是可以加速计算的</strong>！！！ ）。使用 <strong>Flash Attention</strong>如何去处理 <strong>GQA</strong>以及 <strong>MQA</strong><br/><img src="https://s2.loli.net/2025/06/21/LnbcEZ2BYKpVkeq.webp" alt="1" width="1161" height="334" loading="lazy" decoding="async"/><br/><strong>GQA</strong> 和<strong>MQA</strong> 本质上是对 Key/Value（KV）头的压缩，即 减少 Key/Value 头的数量，从而降低计算和显存开销。因此，在 Flash Attention 中，主要需要：1、为 K/V 头建立索引映射，确保多个 Query 头正确共享相应的 Key/Value。2、在计算 QK^T 时，使用映射索引进行广播，避免存储重复的 K/V，同时保持正确的注意力计算逻辑。3、利用 Flash Attention 的块计算机制，在低显存环境下高效完成 Softmax 归一化和注意力分配<br/> 代码操作，首先安装<code>flash-attn</code>：<code>pip install flash-attn</code>。代码使用：</p><pre><code class="language-python">from flash_attn import flash_attn_func
import torch
import torch.nn as nn

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
q = torch.randn(32, 64, 8, int(1024/8)).to(device, dtype=torch.bfloat16)
out = flash_attn_func(q, q, q, causal= False)
print(out.shape)
</code></pre><p><code>flash_attn_func</code>输入参数：<br/> 1、<code>q,k,v</code>：形状为：<code>(batch_size, seqlen, nheads, headdim)</code>也就是说一般文本输入为：<code>(batch_size, seqlen, embed_dim)</code>要根据设计的<code>nheads</code>来处理输入的维度，并且需要保证：<code>headdim</code>≤256，于此同时要保证数据类型为：<code>float16</code> 或 <code>bfloat16</code><br/> 2、<code>causal</code>：<code>bool</code>判断是不是使用<code>causal attention mask</code></p><h3 id="2multi-head-latent-attentionmla">2、<code>Multi-head Latent Attention</code>（<code>MLA</code>）</h3><p>对于<a href="https://www.big-yellow-j.top/posts/2025/01/27/MoE-KV-cache.html"><code>KV-cache</code></a>会存在一个问题：在推理阶段虽然可以加快推理速度，但是对于显存占用会比较高（因为<code>KV</code>都会被存储下来，导致显存占用高），对于此类问题后续提出<code>Grouped-Query-Attention（GQA）</code>以及<code>Multi-Query-Attention（MQA）</code>可以降低<code>KV-cache</code>的容量问题，但是会导致模型的整体性能会有一定的下降。<br/><img src="https://s2.loli.net/2025/06/21/LnbcEZ2BYKpVkeq.webp" alt="1" width="1161" height="334" loading="lazy" decoding="async"/></p><blockquote><p><code>MHA</code>: 就是普通的计算方法<br/><code>GQA</code>: 将多个<code>Q</code>分组，并共享相同的<code>K</code>和<code>V</code><br/><code>MQA</code>: 所有Attention Head共享同一个<code>K</code>、<code>V</code><br/> 详细代码：<a href="../pages/code.md">🔗</a></p><p><img src="https://s2.loli.net/2025/06/21/CvguOVbp7DtBRWn.webp" alt="1" width="768" height="162" loading="lazy" decoding="async"/></p></blockquote><p>对于<code>MLA</code>（<a href="https://arxiv.org/pdf/2405.04434">DeepSeek-V2</a>以及<a href="https://arxiv.org/pdf/2412.19437v1">DeepSeek-V3</a>中都用到）作为一种<code>KV-cache</code>压缩方法，原理如下：</p> \[\mathbf{c}_{t}^{KV}=W^{DKV}\mathbf{h}_{t} \\ \mathbf{k}_{t}^{C}=W^{UK}\mathbf{c}_{t}^{KV} \\ \mathbf{v}_{t}^{C}=W^{UV}\mathbf{c}_{t}^{KV} \\\] <p><img src="https://s2.loli.net/2025/06/21/tfRXSoD7T68zwnp.webp" alt="MLA完整计算过程" width="822" height="524" loading="lazy" decoding="async"/></p><p>对于输入首先进行压缩得到压缩状态（$c^{KV}$ 和 $c^Q$），去缓存这个压缩状态（而不是原始的完整 K 和 V），而后再对压缩状态进行解压得到 KV 的值。 不过值得注意的是，在计算 RoPE过程中，主要是对 Q 和 K 这两部分值加入位置信息，而且 Q 和 K 的处理方式是不同的，这也是 MLA 的关键设计点之一。 具体来说：</p><ul><li>对于 K 的 RoPE 计算过程：<strong>直接对原始输入 $h_t$ 进行一次独立的线性投影 + RoPE</strong>，得到一个很小的位置专用向量 $k^R$（不经过低维压缩），然后再把这个 $k^R$ 拼接到解压出来的内容部分 $k^C$ 后面，组成完整的 K。</li><li>对于 Q 的 RoPE 计算过程：<strong>先对 Q 也做一次低维压缩得到 $c^Q$</strong>，然后对这个压缩后的 $c^Q$ 进行 RoPE 操作，得到带有位置信息的 $q^R$，再把 $q^R$ 拼接回解压/投影后的内容部分 $q^C$，组成完整的 Q。</li></ul><p>之所以 K 可以直接用原始输入计算 RoPE，而 Q 要先压缩再算 RoPE 主要原因是为了推理阶段的“权重吸收”优化：</p><ul><li>K 的内容部分（$k^C$）是可以通过 $W^{UK}$ 从 $c^{KV}$ 实时解压出来的，这个解压矩阵 $W^{UK}$ 可以和 Q 的投影矩阵融合（吸收）掉，从而在推理时少做一次大矩阵乘法，显著提升速度。 如果 K 的 RoPE 也放在压缩路径里，就会破坏这种融合的可能性（因为 RoPE 是位置相关的非线性操作，不能随便和线性层交换顺序）。</li><li>而 Q 在推理时本身就需要和所有历史 K 计算注意力分数，Q 的计算量相对固定，所以即使 Q 也经过一次压缩再加 RoPE，对速度影响较小，但可以大幅节省训练时的激活内存。</li></ul><p>简单一句话概括 MLA 的 RoPE 拆分逻辑：<br/> “K 的位置信息直接从原始输入独立计算（不压缩），以保证推理时能做权重吸收加速；Q 的位置信息先压缩再计算 RoPE，以节省训练内存。” 这样设计既实现了极高的 KV cache 压缩率（通常 90%+），又保留了位置编码的有效性，同时还获得了推理加速的红利。<br/> 从上述公式也容易发现，在<code>MLA</code>中只是对缓存进行一个“替换”操作，用一个低纬度的$C_t^{KV}$来代替（也就是说：<strong>只需要存储$c_t^{KV}$即可</strong>）原本的<code>KV</code>（或者说将容量多的<code>KV</code>进行投影操作，这个过程和<a href="https://arxiv.org/pdf/2106.09685">LoRA</a>有些许相似），在进行投影操作之后就需要对<code>attention</code>进行计算。对于上述公式简单理解：<br/> 假设输入模型（输入到<code>Attention</code>）数据为$h_t$（假设为：$n\times d$），在传统的<code>KV-cache</code>中会将计算过程中的<code>KV</code>不断缓存下来，在后续计算过程中“拿出来”（这样就会导致随着输出文本加多，导致缓存的占用不断累计：$\sum 2n\times d$），因此在<code>MLA</code>中的操作就是：对于$h_t$进行压缩：$n \times d \times d \times d_s= n \times d_s$这样一来我就只需要缓存：$n \times d_s$即可（如果需要复原就只需要再去乘一下新的矩阵即可）<br/><img src="https://s2.loli.net/2025/06/21/4ZIMukCfQgSWBTJ.webp" alt="MLA" width="684" height="488" loading="lazy" decoding="async"/><br/><a href="https://github.com/deepseek-ai/DeepSeek-V3/blob/b5d872ead062c94b852d75ce41ae0b10fcfa1c86/inference/model.py#L393">部分代码</a>部分参数初始化值按照<a href="https://github.com/deepseek-ai/DeepSeek-V3/blob/main/inference/configs/config_236B.json">236B的设置中的设定</a>：</p><pre><code class="language-python">class MLA(nn.Module):
    def __init__(...):
        super().__init__()
        ...
        self.n_local_heads = args.n_heads // world_size # n_heads=128

        self.q_lora_rank = args.q_lora_rank # q被压缩的维度 || 1536
        self.kv_lora_rank = args.kv_lora_rank # KV被压缩的维度 || 512

        # QK带旋转位置编码维度和不带旋转位置编码维度
        self.qk_nope_head_dim = args.qk_nope_head_dim # 128
        self.qk_rope_head_dim = args.qk_rope_head_dim # 64

        self.qk_head_dim = args.qk_nope_head_dim + args.qk_rope_head_dim # 192
        self.v_head_dim = args.v_head_dim # 128
        ...
        if self.q_lora_rank == 0:
            self.wq = ColumnParallelLinear(self.dim, self.n_heads * self.qk_head_dim)
        else:
            self.wq_a = Linear(self.dim, self.q_lora_rank)
            self.q_norm = RMSNorm(self.q_lora_rank)
            self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.qk_head_dim)

        self.wkv_a = Linear(self.dim, self.kv_lora_rank + self.qk_rope_head_dim)
        self.kv_norm = RMSNorm(self.kv_lora_rank)
        self.wkv_b = ColumnParallelLinear(self.kv_lora_rank, self.n_heads * (self.qk_nope_head_dim + self.v_head_dim))
        self.wo = RowParallelLinear(self.n_heads * self.v_head_dim, self.dim)
        self.softmax_scale = self.qk_head_dim ** -0.5
    
    def forward(self, ...):
        bsz, seqlen, _ = x.size() # 假设为：3, 100, 4096
        ...
        if self.q_lora_rank == 0:
            q = self.wq(x)
        else:
            q = self.wq_b(self.q_norm(self.wq_a(x))) # 3, 100, 192*128
        q = q.view(bsz, seqlen, self.n_local_heads, self.qk_head_dim) # 3, 100, 128, 192
        q_nope, q_pe = torch.split(q, [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1) # (3, 100, 128, 128), (3, 100, 128, 64)
        # 使用RoPE 
        q_pe = apply_rotary_emb(q_pe, freqs_cis)

        kv = self.wkv_a(x) # 3, 100, 576
        kv, k_pe = torch.split(kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1) # (3,100,512) (3,100,64)
        k_pe = apply_rotary_emb(k_pe.unsqueeze(2), freqs_cis)

        if attn_impl == "naive":
            q = torch.cat([q_nope, q_pe], dim=-1) # 3, 100, 128, 192
            kv = self.wkv_b(self.kv_norm(kv)) # 3, 100, 32768
            kv = kv.view(bsz, seqlen, self.n_local_heads, self.qk_nope_head_dim + self.v_head_dim) # 3, 100, 128, 256
            k_nope, v = torch.split(kv, [self.qk_nope_head_dim, self.v_head_dim], dim=-1)
            k = torch.cat([k_nope, k_pe.expand(-1, -1, self.n_local_heads, -1)], dim=-1)
            # 设计到多卡集群start_pos:end_pos是多卡集群上的操作
            self.k_cache[:bsz, start_pos:end_pos] = k
            self.v_cache[:bsz, start_pos:end_pos] = v
            scores = torch.einsum("bshd,bthd-&gt;bsht", q, self.k_cache[:bsz, :end_pos]) * self.softmax_scale
        else:
            wkv_b = self.wkv_b.weight if self.wkv_b.scale is None else weight_dequant(self.wkv_b.weight, self.wkv_b.scale, block_size) 
            wkv_b = wkv_b.view(self.n_local_heads, -1, self.kv_lora_rank)
            q_nope = torch.einsum("bshd,hdc-&gt;bshc", q_nope, wkv_b[:, :self.qk_nope_head_dim])
            self.kv_cache[:bsz, start_pos:end_pos] = self.kv_norm(kv)
            self.pe_cache[:bsz, start_pos:end_pos] = k_pe.squeeze(2)
            scores = (torch.einsum("bshc,btc-&gt;bsht", q_nope, self.kv_cache[:bsz, :end_pos]) +
                      torch.einsum("bshr,btr-&gt;bsht", q_pe, self.pe_cache[:bsz, :end_pos])) * self.softmax_scale
        
        if mask is not None:
            scores += mask.unsqueeze(1)
        scores = scores.softmax(dim=-1, dtype=torch.float32).type_as(x)
        if attn_impl == "naive":
            x = torch.einsum("bsht,bthd-&gt;bshd", scores, self.v_cache[:bsz, :end_pos])
        else:
            x = torch.einsum("bsht,btc-&gt;bshc", scores, self.kv_cache[:bsz, :end_pos])
            x = torch.einsum("bshc,hdc-&gt;bshd", x, wkv_b[:, -self.v_head_dim:])
        x = self.wo(x.flatten(2))
        return x
</code></pre><p>不过 <strong>MLA</strong>存在一个问题，不兼容 <strong>RoPE</strong>（旋转位置编码，因为你将KV进行压缩）从上述代码的角度除法理解如何使用<code>RoPE</code>，从上面代码上，无论是Q还是KV都是从压缩后的内容中分离除部分内容，然后计算结果</p><h3 id="3page-attentionvllm">3、<code>Page Attention</code>（<code>vLLM</code>）</h3><p>上述描述中：<code>Flash Attention</code>（加快速度）、<code>MLA</code>（优化<code>KV-cache</code>存储），而<code>Page Attention</code>也是一种优化方法（区别于<code>MLA</code>，<code>page attention</code>是对内存进行分配管理）。参考<a href="https://dl.acm.org/doi/pdf/10.1145/3600006.3613165">论文</a>中描述，对于<code>KV-cache</code>存在3个问题：<br/><img src="https://s2.loli.net/2025/06/21/9QpfhleHvRPxLmW.webp" alt="image" width="1014" height="190" loading="lazy" decoding="async"/><br/> 1、<strong>预留浪费 (Reserved)</strong>：为将来可能的 token 预留的空间，这些空间被保留但暂未使用，其他请求无法使用这些预留空间；<br/> 2、<strong>内部内存碎片化问题（internal memory fragmentation）</strong>：系统会为每个请求预先分配一块连续的内存空间，大小基于最大可能长度(比如2048个token)，但实际请求长度往往远小于最大长度，这导致预分配的内存有大量空间被浪费。<br/> 3、<strong>外部内存碎片化问题（external memory fragmentation）</strong>：不同内存块之间的零散空闲空间，虽然总空闲空间足够，但因不连续而难以使用。<br/><img src="https://s2.loli.net/2025/06/21/ryt7kgaGZSw32HN.webp" alt="image" width="473" height="262" loading="lazy" decoding="async"/><br/> 只有 <strong>20.4%-38.2%</strong> 的token是被使用的，大部分都被浪费掉了。<code>Page Attention</code>允许在非连续的内存空间中存储连续的 key 和 value 。具体来说，<code>Page Attention</code>将每个序列的 <code>KV-cache</code> 划分为块，每个块包含固定数量 token 的键和值。在注意力计算期间，<code>Page Attention</code>内核可以有效地识别和获取这些块。如何理解上面描述呢？还是借用论文中的描述：<br/><img src="https://s2.loli.net/2025/06/21/sZ1uOlYStP3ehDb.webp" alt="image" width="808" height="462" loading="lazy" decoding="async"/><br/> 比如说按照上面Prompt要输出（假设只输出这些内容）：“fathers brought a car”，一般的套路可能是：比如说：“Four score and seven years ago our xxxxx”（xxx代表预留空间）因为实际不知道到底要输出多少文本，因此会提前预留很长的一部分空间（但是如果只输出4个字符，这预留空间就被浪费了），因此在<code>page attention</code>里面就到用一种“分块”的思想处理，以上图为例，分为8个Block每个Block只能存储4个内容，因此就可以通过一个<code>Block Table</code>来建立一个表格告诉那些Block存储了多少，存储满了就去其他Blobk继续存储。整个过程如下：</p><p><img src="https://s2.loli.net/2025/02/02/3lWpNMUQyLojhP9.webp" alt="image" width="1200" height="590" loading="lazy" decoding="async"/></p><p>这样一来浪费就只会发生在最后一个Block中（比如说存储4个但是只存进去了1个就会浪费3个）<br/><a href="https://docs.vllm.ai/en/latest/index.html">代码</a>操作：</p><pre><code class="language-bash">git lfs clone https://www.modelscope.cn/qwen/Qwen1.5-1.8B-Chat.git
</code></pre><pre><code class="language-python">from vllm import LLM, SamplingParams
import torch

# Sample prompts.
prompts = [
    "Who're you?",
]
# Create a sampling params object.
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)

# Create an LLM.
llm = LLM(model="./Qwen1.5-1.8B-Chat/", dtype= torch.float16, enforce_eager= True)
# Generate texts from the prompts. The output is a list of RequestOutput objects
# that contain the prompt, generated text, and other information.
outputs = llm.generate(prompts, sampling_params)
# Print the outputs.
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
</code></pre><h3 id="4native-sparse-attentionnsa">4、Native Sparse Attention(NSA)</h3><p>主要是在DeepSeek v3.2中提出，详细描述：<a href="https://www.big-yellow-j.top/posts/2025/08/28/OpenModelDeepSeek.html">开源模型技术总结-2————DeepSeek系列模型</a></p><h2 id="参考">参考</h2><p>1、<a href="https://mloasisblog.com/blog/ML/AttentionOptimization">https://mloasisblog.com/blog/ML/AttentionOptimization</a><br/> 2、<a href="https://github.com/vllm-project/vllm">https://github.com/vllm-project/vllm</a><br/> 3、<a href="https://arxiv.org/pdf/2205.14135">https://arxiv.org/pdf/2205.14135</a><br/> 4、<a href="https://zhuanlan.zhihu.com/p/676655352">https://zhuanlan.zhihu.com/p/676655352</a><br/> 5、<a href="https://arxiv.org/pdf/2405.04434">https://arxiv.org/pdf/2405.04434</a><br/> 6、<a href="https://spaces.ac.cn/archives/10091">https://spaces.ac.cn/archives/10091</a><br/> 7、<a href="https://zhuanlan.zhihu.com/p/696380978">https://zhuanlan.zhihu.com/p/696380978</a><br/> 8、<a href="https://dl.acm.org/doi/pdf/10.1145/3600006.3613165">https://dl.acm.org/doi/pdf/10.1145/3600006.3613165</a><br/> 9、<a href="https://zhuanlan.zhihu.com/p/638468472">https://zhuanlan.zhihu.com/p/638468472</a><br/> 10、<a href="https://docs.vllm.ai/en/latest/index.html">https://docs.vllm.ai/en/latest/index.html</a><br/> 11、<a href="https://arxiv.org/pdf/2103.03493">https://arxiv.org/pdf/2103.03493</a><br/> 12、<a href="https://www.cnblogs.com/gongqk/p/14772297.html">https://www.cnblogs.com/gongqk/p/14772297.html</a><br/> 13、<a href="https://spaces.ac.cn/archives/8620">https://spaces.ac.cn/archives/8620</a></p> ]]></content:encoded><category>Attention优化</category><pubDate>Mon, 17 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/17/Attention.html</guid></item><item><title>深度学习中常见的LLM微调技术及其代码</title><link>https://www.big-yellow-j.top/posts/2025/02/12/finetuning.html</link><description><![CDATA[大语言模型微调主要分为全面微调和部分参数微调，其中部分参数微调包括Prompt-tuning和Parameter-efficient fine-tuning（PEFT）等方法。Prompt-tuning分为hard prompt和soft prompt，hard prompt是用户直接提供的自然语言文本，明确表达意图；soft prompt则是通过算法生成的潜在向量，在模型内部引导任务完成。其下有Prefix-Tuning（微调可学习前缀，保持模型参数不变）、P-Tuning（使用预训练词表中unused token作为伪prompt，通过LSTM训练token参数）及P-Tuning ...]]></description><content:encoded><![CDATA[<p>大语言模型微调，一般来说是指在一个预训练完成的LLM上去针对对应的下游任务进行微调，让其更加适应下游任务，一般来说可以：1、全面微调（对LLM中全部参数进行调整，如果模型参数量很大的时候这个花销是比较大的）；2、部分参数微调</p><h2 id="1prompt-tuning">1、<code>Prompt-tuning</code></h2><p>“prompt” 指的是用户提供给模型的输入文本或指令，它用于引导模型生成相关的文本或完成特定的任务。prompt可以是一个问题、一句话、一个主题，或者是一段完整的文本。模型会根据prompt理解用户的意图，并生成相应的文本作为回应。值得注意的是，对于prompt会有下面两种不同的描述：<br/> 1、<code>hard prompt</code>:这种类型的 prompt 是模型输入的一部分，通常由<strong>用户直接提供的自然语言文本构成</strong>，模型在接受到这个文本后会根据文本的内容生成回应。硬提示是显式的，通常是文本的某一部分，用来明确指定模型的任务。例如，在文本生成任务中，用户可能输入：“请写一篇关于气候变化的文章”，这就是一个硬提示。硬提示的特点是它直接表达了用户的意图，通常不需要修改或调整。<br/> 2、<code>soft prompt</code>:软提示则是指通过<strong>某种方法（如优化算法或训练过程）生成的</strong>、在模型中以特定的向量形式存在的输入，它们并不是自然语言文本，而是“学习”到的潜在向量或嵌入表示。软提示并不是由用户手动编写的，而是在训练过程中或微调过程中，通过算法自动生成或优化得到的。这些向量通常不会直接呈现给用户，而是在模型内部起作用，作为一个“潜在的提示”来引导模型完成任务。</p><h3 id="11-prefix-tuning">1.1 <code>Prefix-Tuning</code></h3><p><img src="https://s2.loli.net/2025/06/21/BY5cik7tPNTxz4U.webp" alt="image" width="670" height="490" loading="lazy" decoding="async"/></p><p>上面为全面微调<code>Transformer</code>全部参数，而下面为只微调<code>Prefix</code>（一种可学习的前缀）,如下图描述一样：</p><p><img src="https://s2.loli.net/2025/06/21/fgAZlSHWtab5GR4.webp" alt="image" width="1719" height="709" loading="lazy" decoding="async"/></p><p>对于输入文本：$X=(x_1,…,x_n)$，我在其前面补充一个 <strong>前缀</strong>：$P=(p_1,…,p_m)$，前缀长度是远小于输入文本长度的，在微调过程中保持模型参数$\theta$保持不变，这样一来优化过程就变成了：</p> \[P^*=\arg\min_P\mathcal{L}(M(P\oplus X;\theta),Y)\] <p>其中$P\oplus X$表示将前缀拼接到输入前面，训练过程中只优化前缀即可</p><h3 id="12-p-tuning">1.2 <code>P-Tuning</code></h3><p>对于一段输入，我通过预先生成的Prompt进行embedding（<strong>代码中作者是通过LSTM对prompt的embedding进行训练</strong>）然后融合到input中，然后输入到预训练的模型中去，然后去将预训练模型参数以及LSTM计算得到的Prompt参数一起加入训练</p><p><img src="https://s2.loli.net/2025/06/21/xFYeo9KLA2wlCNH.webp" alt="image" width="1476" height="337" loading="lazy" decoding="async"/></p><p>左侧为传统的Prompt处理方法，通过事先定义好的Prompt进行微调，P-tuning与之存在区别：用预训练词表中的unused token作为伪prompt「BERT的vocab里有unused 1 ~ unused99」，然后通过训练去更新这些token的参数也就是，<code>P-tuning</code>的Prompt不是显式的，不是我们可以看得懂的字符，而是一些隐式的、经过训练的、<strong>模型认为最好的prompt token</strong>。换言之，</p><h3 id="13-p-tuning-v2">1.3 <code>P-Tuning v2</code></h3><h2 id="2parameter-efficient-fine-tuning">2、<code>Parameter-efficient fine-tuning</code></h2><p>参数高效的fine-tuning，简称PEFT，旨在在尽可能减少所需的参数和计算资源的情况下，实现对预训练语言模型的有效微调。它是自然语言处理（NLP）中一组用于将预训练语言模型适应特定任务的方法，其所需参数和计算资源比传统的fine-tuning方法更少</p><h3 id="21-lora">2.1 <code>LoRA</code></h3><p><img src="https://s2.loli.net/2025/06/21/zlLUfV7bmhK9Diy.webp" alt="image" width="289" height="266" loading="lazy" decoding="async"/></p><p>对于预训练权重：$W_0 \in R^{d \times k}$，可以将其表示成一种低序表示：$W_0 + \Delta W= W_0+ BA$ 其中：$B \in R ^{d \times r}，A \in R ^{r \times k}$，其中r远小于$min(d,k)$。训练过程中$W_0$被冻结不接受更新，A，B作参数进行训练，得到：$h= W_0x+ \Delta Wx=W_0x+BAx$。这样一来就很大程度减小了参数调整（比如说：$W_0$：5x5的，设置B：5x1；A：1x5。这样一来较之25参数调整和(5,5)的参数调整，就小了很多）</p><p><img src="https://s2.loli.net/2025/06/21/TJR5nD2QbpXKZhf.webp" alt="image" width="769" height="447" loading="lazy" decoding="async"/></p><p>对于论文中的结果进一步描述：<br/><strong>1、LoRA作用在Transformer的那个参数矩阵</strong>：</p><p><img src="https://s2.loli.net/2025/06/21/aYRSLX97KOHVGsf.webp" alt="image" width="856" height="178" loading="lazy" decoding="async"/></p><p>从上面分析：可以将微调参数平均分配到$W_q$和$W_v$的效果更加好</p><blockquote><p>😶‍🌫️<a href="https://huggingface.co/docs/peft/package_reference/lora">代码</a></p></blockquote><pre><code class="language-python">lora_config = LoraConfig(
    r=8,  # 低秩矩阵秩
    alpha=16,  # 缩放因子，一般指定为r的两倍
    dropout=0.1,  # dropout 比例
    target_modules=["attention.self.query", "attention.self.key", "attention.self.value", "intermediate.dense"],  # 在这些层添加 LoRA
)

lora_model = LoraModel(model, lora_config)
</code></pre><h3 id="22-qlora">2.2 <code>QLoRA</code></h3><p><img src="https://s2.loli.net/2025/06/21/IJEx4Ctr27cQfDj.webp" alt="image" width="1337" height="631" loading="lazy" decoding="async"/></p><p><code>QLoRA</code>的最后一个工作则是将量化的思想和LoRA的低秩适配器的思想结合到一起拿来对大模型进行微调。具体来讲，对于LLM的参数$W$，首先将它量化到NF4的精度，在进行特征计算时，通过双重反量化将它还原到BF16精度。同LoRA一样，QLoRA也在原参数一侧添加了一个与原参数并行的低秩适配器，它的精度是BF16。</p> \[Y^{BF16}= X^{BF16}doubleDequant(c_{1}^{FP32},c_{2}^{k-bit},W^{NF4}) + X^{BF16}L_{1}^{BF16}L_{2}^{BF16}\] <p>双重量化：</p> \[doubleDequant(c_{1}^{FP32},c_{2}^{k-bit},W^{k-bit}) = dequant(dequant(c_{1}^{FP32},c_{2}^{k-bit}), W^{4bit})\\= W^{BF16}\] <h3 id="23-adapter">2.3 <code>Adapter</code></h3><p><img src="https://s2.loli.net/2025/06/21/Q1k9gncXvVrRMp4.webp" alt="image" width="1615" height="558" loading="lazy" decoding="async"/></p><p>结构上很容易理解就在原始的Transformer模块中添加一个$Adapter \quad Layer$。具体微调过程为：将输入特征维度为：m缩小到d。那么得到的每一层的参数量（包含bias）就是：$2md+d+m$。通过设置$m \ll d$。整个实验过程中模型参数大概为原始参数的0.5-8%，在Adapater内部有一个跳跃连接。使用跳跃连接，如果projection layer的参数初始化为接近零，则模块将初始化为近似恒等函数。</p><p>在使用 adapter 进行调整时，adapter layer 被添加到预训练的语言模型中，但在<strong>预训练阶段，其内部参数通常是冻结的，不会进行优化</strong>。这意味着在预训练阶段，adapter layer 中的参数保持不变，不会随着语言模型的参数更新而更新。然而，在<strong>微调阶段，adapter layer 中的参数会被解冻，并与整个模型一起进行微调</strong>，以适应特定的目标任务。在微调过程中，adapter layer 的参数会根据新任务的损失函数进行优化，以使模型在新任务上表现更好。</p><h2 id="参考">参考</h2><p>1、https://github.com/liguodongiot/llm-action<br/> 2、https://arxiv.org/pdf/2106.09685<br/> 3、http://arxiv.org/abs/1902.00751<br/> 4、https://arxiv.org/abs/2103.10385<br/> 5、https://arxiv.org/pdf/2101.00190<br/> 6、https://arxiv.org/pdf/2407.11046v4<br/> 7、https://huggingface.co/docs/peft/package_reference/lora</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/12/finetuning.html</guid></item><item><title>LLM中常用的位置编码原理及其代码</title><link>https://www.big-yellow-j.top/posts/2025/02/03/pos-embedding.html</link><description><![CDATA[位置编码用于标记输入序列中单词或标记的位置，帮助Transformer模型理解相对位置与长距离依赖关系，通常与输入嵌入相加。主要类型包括：绝对位置编码（固定正弦余弦生成的编码，简单易实现但难以泛化至不同长度序列，适用于短文本、序列长度固定的任务）、相对位置编码（通过查询和键的相对位置差编码，动态建模位置关系以处理长距离依赖，计算复杂度较高，适用于长序列建模任务）、RoPE（旋转位置编码，改进的相对位置编码方法，通过旋转变换高效引入相对位置差，兼具长距离依赖捕捉能力与计算效率，适用于文本生成、机器翻译等长序列依赖建模）、Learned Position Encoding（数据自适应学习的编...]]></description><content:encoded><![CDATA[<h2 id="一位置编码">一、位置编码</h2><p>位置编码用于在输入序列中标记每个单词或标记的位置，这有助于模型理解输入序列中各个部分的相对位置，从而更好地捕捉序列中的长距离依赖关系。换言之，如果仅在查询（Q）和键（K）上使用位置编码，当我们计算 $QK^T$ 时，模型可以得到不同词之间的相似度得分。例如，”猫”和”狗”之间的得分理论上应该比较高。然而，仅通过计算这些得分，模型可能会忽略掉文本中的局部依赖关系（即相邻的token之间的得分应该也较高）。因此，位置编码有助于模型不仅理解长距离依赖，还能关注局部的顺序信息。一般而言就是计算得到位置编码之后将其与input embedding进行相加得到结果。对于位置编码借用<a href="https://arxiv.org/pdf/2104.09864">论文</a>中的定义：定义输入$N$个tokens：$S_N={w_i}_{i=1}^{N}$，并且在Transformer中要使用（对QKV）位置编码：</p> \[q_m=f_q(x_m,m)\\ k_n=f_k(x_n,n)\\ v_n=f_v(x_n,n)\] <p>简单对比如下：</p><table><thead><tr><th>类型</th><th>说明</th><th>优势</th><th>劣势</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>绝对位置编码</strong></td><td>每个位置都有一个固定的编码，通常使用正弦和余弦函数生成的编码。</td><td>简单易理解，广泛应用于标准的 Transformer 模型中。</td><td>无法处理长距离依赖，难以泛化到不同长度的序列。</td><td>适用于较短文本的模型，序列长度相对固定的任务。</td></tr><tr><td><strong>相对位置编码</strong></td><td>通过对查询和键的相对位置差进行编码，使得模型在计算注意力时考虑相对距离。</td><td>能够处理长序列中的长距离依赖关系，动态建模位置差。</td><td>相比绝对位置编码，计算复杂度更高，模型实现也较为复杂。</td><td>适用于长序列建模，语言模型等需要建模长距离依赖的任务。</td></tr><tr><td><strong>RoPE (旋转位置编码)</strong></td><td>一种改进的相对位置编码方法，通过旋转变换将相对位置差引入注意力计算。</td><td>通过旋转方式考虑相对位置，能够捕捉长距离依赖，且计算更加高效。</td><td>实现相对复杂，旋转操作可能增加一些计算开销。</td><td>适用于需要捕捉长序列依赖的任务，如文本生成、机器翻译等。</td></tr><tr><td><strong>Learned Position Encoding</strong></td><td>位置编码通过学习得到，而不是通过固定的数学公式生成。</td><td>可以根据数据自适应学习位置编码，具有更强的表达能力。</td><td>需要额外的学习参数，并且可能受到数据集的限制，难以泛化到其他任务或数据集。</td><td>适用于大规模数据训练，模型需要从数据中自动学习位置关系的场景。</td></tr><tr><td><strong>Rotary Position Embedding (RoPE)</strong></td><td>使用旋转变换的相对位置编码，通常与绝对位置编码结合使用，来增强 Transformer 模型的长距离依赖建模能力。</td><td>RoPE 相比传统的位置编码方法具有更好的扩展性，特别适用于长文本或长序列，提升了处理长距离依赖的能力。</td><td>相较于传统位置编码和其他相对位置编码方法，RoPE 的实现和理解略为复杂，增加了计算的复杂度。</td><td>适用于需要建模长序列和长距离依赖的任务，如文本生成、语言建模等。</td></tr></tbody></table><h3 id="1absolute-positon-embedding">1、Absolute Positon Embedding</h3><p>绝对位置编码常规做法是在词嵌入上补充一个位置编码向量然后乘对应的变换矩阵：</p> \[f_{t\in{q,k,v}}=W_{t\in{q,k,v}}(x_i+p_i)\] <p>最原始的方法就是直接使用<code>Sinusoidal</code>函数：</p> \[\text{偶数}:p_{i,2t}=sin(k/10000^{2t/d})\\ \text{奇数}:p_{i,2t+1}=cos(k/10000^{2t/d})\] <p>另外一种方式是使用 <strong>学习位置编码</strong>（用一个学习矩阵表示即可）:</p> \[p_i = W,W:[seq_len, embed_dim]\] <p>随之而来，使用绝对位置编码存在一个缺陷（以第一种为例）：每个token的位置编码都是固定的，这意味着每个词的位置信息是独立的，无法灵活地体现不同“单词”之间的相对距离。具体来说，绝对位置编码只为每个token分配一个固定的位置信息，不会根据token之间的相对位置关系来调整其编码。因此，它不能很好地反映序列中不同单词之间的相对距离，尤其是在处理长距离依赖关系时，可能无法准确捕捉到不同位置的语义依赖</p><h3 id="2relative-position-embedding">2、Relative Position Embedding</h3><p>以<a href="https://arxiv.org/pdf/1901.02860"><code>Transformer-XL</code></a>为例，相对位置编码通过引入动态计算的 相对位置差 来替代传统的 绝对位置编码，这种方法能够有效地捕捉长文本中的长距离依赖。每一层的自注意力机制会结合相对位置编码，进而增强模型的上下文理解能力，尤其在处理长序列时，Transformer-XL 可以显著减少计算开销，并提高模型对长距离依赖的建模能力。</p><h3 id="3rope">3、<code>RoPE</code></h3><p>按照<a href="https://arxiv.org/pdf/2104.09864">论文</a>中描述对于<code>Q</code>和<code>K</code>之间的内积操作可以用一个函数<code>g</code>进行表示，该函数输入为词嵌入向量$x_m$以及$x_n$和他们之间的相对位置$m-n$，因此可以假设下面公式成立：</p> \[&lt;f_q(x_m,m), f_k(x_n, n)&gt;=g(x_m, x_n, m-n)\] <p>对于$f_q$以及 $f_k$都包含位置信息，希望通过一个内积函数，得到一个函数 $g$（包含相对位置m-n）以二维例子为例，</p> \[\begin{aligned} f_{q}(\boldsymbol{x}_{m},m) &amp; =(\boldsymbol{W}_q\boldsymbol{x}_m)e^{im\theta} \\ f_k(\boldsymbol{x}_n,n) &amp; =(\boldsymbol{W}_k\boldsymbol{x}_n)e^{in\theta} \\ g(\boldsymbol{x}_{m},\boldsymbol{x}_{n},m-n) &amp; =\mathrm{Re}[(\boldsymbol{W}_q\boldsymbol{x}_m)(\boldsymbol{W}_k\boldsymbol{x}_n)^*e^{i(m-n)\theta}] \end{aligned}\] <p><a href="https://zhuanlan.zhihu.com/p/642884818">借鉴</a>中的证明，对于$f_{q}(\boldsymbol{x}_{m},m)$其中指数函数$e^{imx}$可以根据欧拉公式改写为：$e^{imx}=cos(m\theta)+isin(m\theta)$，因为是以2维度为例因此对于前部分（$W_qx_m$）可以改写为：</p> \[q_m=W_{q} x_{m}=\left(\begin{array}{ll} W_{q}^{(11)} &amp; W_{q}^{(12)} \\ W_{q}^{(21)} &amp; W_{q}^{(22)} \end{array}\right)\binom{x_{m}^{(1)}}{x_{m}^{(2)}}\] <p>类似的$q_m$也可以用复数进行表示：$q_m=[q_m^{(1)},q_m^{(2)}]=[q_m^{(1)}+iq_m^{(2)}]$，因此代入可以得到：$f_q=(q_m^{(1)}+iq_m^{(2)})(cos(m\theta)+isin(m\theta))$展开得到：</p> \[f_q=\left[ q_m^{(1)} \cos(m\theta) - q_m^{(2)} \sin(m\theta), q_m^{(2)} \cos(m\theta) + q_m^{(1)} \sin(m\theta) \right]\\ = \left( \begin{matrix} \cos(m\theta) &amp; -\sin(m\theta) \\ \sin(m\theta) &amp; \cos(m\theta) \end{matrix} \right) \left( \begin{matrix} q_m^{(1)} \\ q_m^{(2)} \end{matrix} \right)\] <p>从上面公式从几何角度出发（假设$m \theta= 45°$）发现就是简单一个旋转操作，但是用到的也是 <strong>绝对位置</strong>信息：</p><p><img src="https://s2.loli.net/2025/06/22/3c2ZdtUx4FN8Gfo.webp" alt="image" width="342" height="79" loading="lazy" decoding="async"/></p><p>那么内积计算就可以表示为：</p> \[g(\mathbf{x}_m, \mathbf{x}_n, m-n) = \begin{pmatrix} \mathbf{q}_m^{(1)} &amp; \mathbf{q}_m^{(2)} \end{pmatrix} \begin{pmatrix} \cos((m-n)\theta) &amp; -\sin((m-n)\theta) \\ \sin((m-n)\theta) &amp; \cos((m-n)\theta) \end{pmatrix} \begin{pmatrix} \mathbf{k}_n^{(1)} \\ \mathbf{k}_n^{(2)} \end{pmatrix}\] <p>最开始$f_q$和 $f_k$使用的都是绝对位置信息，通过内积计算，最后实现 <strong>相对位置编码</strong>。推广到n维可以最后得到:</p> \[\boldsymbol{R}_{\Theta, m}^{d} \boldsymbol{x}=\left(\begin{array}{c} x_{0} \\ x_{1} \\ x_{2} \\ x_{3} \\ \vdots \\ x_{d-2} \\ x_{d-1} \end{array}\right) \otimes\left(\begin{array}{c} \cos m \theta_{0} \\ \cos m \theta_{0} \\ \cos m \theta_{1} \\ \cos m \theta_{1} \\ \vdots \\ \cos m \theta_{d / 2-1} \\ \cos m \theta_{d / 2-1} \end{array}\right)+\left(\begin{array}{c} -x_{1} \\ x_{0} \\ -x_{3} \\ x_{2} \\ \vdots \\ -x_{d-1} \\ x_{d-2} \end{array}\right) \otimes\left(\begin{array}{c} \sin m \theta_{0} \\ \sin m \theta_{0} \\ \sin m \theta_{1} \\ \sin m \theta_{1} \\ \vdots \\ \sin m \theta_{d / 2-1} \\ \sin m \theta_{d / 2-1} \end{array}\right)\] <p>从几何角度出发进行理解：</p><p><img src="https://s2.loli.net/2025/06/22/nlUAZiDFySaQ3uk.webp" alt="image" width="677" height="386" loading="lazy" decoding="async"/></p><p>1.对于位置为m的d维q向量，我们分为d/2组，每两个相邻维度为一组，共同旋转一个角度$m\theta_i$。<br/> 2.$\theta_i$是一个这是一个从1渐变到接近于0的函数，因此，前面维度的$\theta_i$旋转的更快，后面的旋转的更慢</p><p>借用<a href="https://kexue.fm/archives/8265">博客中</a>的结论：<br/> 1、RoPE具有良好的<strong>外推性</strong>（指的是：大模型在训练时和预测时的输入长度不一致，导致模型的泛化能力下降的问题。例如，如果一个模型在训练时只使用了512个 token 的文本，那么在预测时如果输入超过512个 token，模型可能无法正确处理。这就限制了大模型在处理长文本或多轮对话等任务时的效果。），应用到Transformer中体现出较好的处理长文本的能力。</p><h2 id="二tokenizer">二、<code>tokenizer</code></h2><p><code>tokenizer</code>的原理十分简单,就是将文本进行切割,然后用数字去代表这些文本。常见的主要有<a href="https://zhuanlan.zhihu.com/p/630696264">如下几类</a>：<br/> 1、word: 按照词进行分词，如: <code>Today is sunday</code>. 则根据空格或标点进行分割<code>[today, is, sunday, .]</code><br/> 2、character：按照单字符进行分词，就是以char为最小粒度。 如：Today is sunday. 则会分割成<code>[t， o， d，a，y， .... ，s，u，n，d，a，y， .]</code><br/> 3、subword：按照词的subword进行分词。如：<code>Today is sunday.</code> 则会分割成<code>[to， day，is ， s，un，day， .]</code><br/> 各类<code>LLM</code>所使用分词器如下：</p><table><thead><tr><th>模型</th><th>分词器方法</th></tr></thead><tbody><tr><td><strong>GPT-3 (OpenAI)</strong></td><td>Byte Pair Encoding (BPE)</td></tr><tr><td><strong>BERT (Google)</strong></td><td>WordPiece</td></tr><tr><td><strong>T5 (Google)</strong></td><td>SentencePiece</td></tr><tr><td><strong>RoBERTa (Facebook)</strong></td><td>Byte Pair Encoding (BPE)</td></tr><tr><td><strong>XLNet (Google/CMU)</strong></td><td>SentencePiece</td></tr><tr><td><strong>ALBERT (Google)</strong></td><td>SentencePiece</td></tr><tr><td><strong>DistilBERT (Hugging Face)</strong></td><td>WordPiece</td></tr><tr><td><strong>ELECTRA (Google)</strong></td><td>WordPiece</td></tr><tr><td><strong>BLOOM (BigScience)</strong></td><td>SentencePiece</td></tr><tr><td><strong>GPT-4 (OpenAI)</strong></td><td>Byte Pair Encoding (BPE)</td></tr><tr><td><strong>Mistral (Mistral)</strong></td><td>Byte Pair Encoding (BPE)</td></tr></tbody></table><h3 id="1bpebyte-pair-encoding">1、<strong>BPE(Byte Pair Encoding)</strong></h3><p>基本思路是将使用最频繁的字节用一个新的字节组合代替,比如用字符的<code>n-gram</code>替换各个字符.例如,假设(‘A’, ‘B’) 经常顺序出现，则用一个新的标志’AB’来代替它们.分词算法（<code>word segmentation</code>）构建BPE，并将其应用于机器翻译任务中.论文提出的基本思想是,给定语料库，初始词汇库仅包含所有的单个字符.然后,模型不断地将出现频率最高的<code>n-gram pair</code>作为新的<code>n-gram</code>加入到词汇库中,直到词汇库的大小达到我们所设定的某个目标为止。</p><p><code>sentencepiece</code>是一个google开源的自然语言处理工具包，支持<code>bpe</code>、<code>unigram</code>等多种分词方法。其优势在于：<code>bpe</code>、<code>unigram</code>等方法均假设输入文本是已经切分好的，只有这样<code>bpe</code>才能统计词频（通常直接通过空格切分）。但问题是，汉语、日语等语言的字与字之间并没有空格分隔。<code>sentencepiece</code>提出，可以将所有字符编码成Unicode码（包括空格），通过训练直接将原始文本（未切分）变为分词后的文本，从而避免了跨语言的问题。[^1]</p><p>论文中给出的算法例子如上图所示。算法从所有的字符开始，首先将出现频率最高的 (e, s) 作为新的词汇加入表中，然后是(es, t)。以此类推，直到词汇库大小达到我们设定的值。更清晰的过程如下图所示。其中，Dictionary左列表示单词出现的频率。</p><p><img src="https://s2.loli.net/2025/06/22/UCVrOosv3zDTA1E.webp" alt="image" width="813" height="751" loading="lazy" decoding="async"/></p><p>以<code>sentencepiece</code>测试为例(输入模型的句子必须是单独成行(每一个文本都是单独一行)):</p><pre><code class="language-python"># 第一步预训练一个分词
def train(input_file, vocab_size, model_name, model_type, character_coverage):
    """
    search on https://github.com/google/sentencepiece/blob/master/doc/options.md to learn more about the parameters
    :param input_file: one-sentence-per-line raw corpus file. No need to run tokenizer, normalizer or preprocessor.
                       By default, SentencePiece normalizes the input with Unicode NFKC.
                       You can pass a comma-separated list of files.
    :param vocab_size: vocabulary size, e.g., 8000, 16000, or 32000
    :param model_name: output model name prefix. &lt;model_name&gt;.model and &lt;model_name&gt;.vocab are generated.
    :param model_type: model type. Choose from unigram (default), bpe, char, or word.
                       The input sentence must be pretokenized when using word type.
    :param character_coverage: amount of characters covered by the model, good defaults are: 0.9995 for languages with
                               rich character set like Japanse or Chinese and 1.0 for other languages with
                               small character set.
    """
    input_argument = '--input=%s --model_prefix=%s --vocab_size=%s --model_type=%s --character_coverage=%s ' \
                     '--pad_id=0 --unk_id=1 --bos_id=2 --eos_id=3 '
    cmd = input_argument % (input_file, model_name, vocab_size, model_type, character_coverage)
    spm.SentencePieceTrainer.Train(cmd)

if __name__ == "__main__":
    en_input = '../data/corpus.en'
    en_vocab_size = 32000
    en_model_name = 'eng'
    en_model_type = 'bpe'
    en_character_coverage = 1
    train(en_input, en_vocab_size, en_model_name, en_model_type, en_character_coverage)

    sp = spm.SentencePieceProcessor()
    text = "ZUEL was established in 2000 with the merge of the then Central South University of Finance and Economics and then Central South Political Science and Law College. Its root could be traced to 1948 when then Zhongyuan University was founded in the Province of Henan and later moved to Wuhan."
    sp.load('../Zh-En-translate/tokenizer/eng.model')
    print(sp.EncodeAsPieces(text))
    print(sp.EncodeAsIds(text))
    a = [24588, 3276, 219, 2589, 26, 3203, 115, 10, 20943, 34, 10, 1041, 1929, 1204, 3640, 34, 6958, 39, 8385, 39, 1041, 1929, 1204, 6235, 9093, 39, 6024, 12285, 31843, 3362, 3899, 397, 55, 18112, 31, 20864, 479, 1041, 6723, 201, 31838, 6193, 3640, 219, 8186, 26, 10, 16839, 34, 8643, 18, 39, 2234, 4813, 31, 153, 28941, 31843]
    print(sp.decode_ids(a))

输出结果:
['▁ZU', 'EL', '▁was', '▁established', '▁in', '▁2000', '▁with', '▁the', '▁merge', '▁of', '▁the', '▁then', '▁Central', '▁South', '▁University', '▁of', '▁Finance', '▁and', '▁Economics', '▁and', '▁then', '▁Central', '▁South', '▁Political', '▁Science', '▁and', '▁Law', '▁College', '.', '▁Its', '▁root', '▁could', '▁be', '▁traced', '▁to', '▁1948', '▁when', '▁then', '▁Zh', 'ong', 'y', 'uan', '▁University', '▁was', '▁founded', '▁in', '▁the', '▁Province', '▁of', '▁Hen', 'an', '▁and', '▁later', '▁moved', '▁to', '▁W', 'uhan', '.']
[24588, 3276, 219, 2589, 26, 3203, 115, 10, 20943, 34, 10, 1041, 1929, 1204, 3640, 34, 6958, 39, 8385, 39, 1041, 1929, 1204, 6235, 9093, 39, 6024, 12285, 31843, 3362, 3899, 397, 55, 18112, 31, 20864, 479, 1041, 6723, 201, 31838, 6193, 3640, 219, 8186, 26, 10, 16839, 34, 8643, 18, 39, 2234, 4813, 31, 153, 28941, 31843]
ZUEL was established in 2000 with the merge of the then Central South University of Finance and Economics and then Central South Political Science and Law College. Its root could be traced to 1948 when then Zhongyuan University was founded in the Province of Henan and later moved to Wuhan.
</code></pre><p>对于<code>sentencepiece</code>分词器，一般来说流程为：<br/> 1、用自己数据进行训练得到“分词器”<br/> 2、直接调用分词器进行<code>encoder</code>/<code>decoder</code>：</p><pre><code class="language-python">import sentencepiece as spm

def chinese_tokenizer_load(path):
    sp_chn = spm.SentencePieceProcessor()
    sp_chn.Load(path)
    return sp_chn

def english_tokenizer_load(path):
    sp_eng = spm.SentencePieceProcessor()
    sp_eng.Load(path)
    return sp_eng

sp_chn = chinese_tokenizer_load('..')
sp_eng = english_tokenizer_load('..')
out_a = sp_chn.EncodeAsPieces('介绍Tokenizer使用方法')
print(out_a)
out_b = sp_eng.DecodeIds(out_a)
print(out_b)

# ['▁', '介绍', 'T', 'ok', 'en', 'iz', 'er', '使用', '方法']
# 介绍Tokenizer使用方法
</code></pre><h3 id="2使用预训练好的tokenizer">2、使用预训练好的<code>tokenizer</code></h3><p>1.<code>OpenAI</code>:https://platform.openai.com/tokenizer<br/> 2.<code>Huggingface</code>:https://github.com/huggingface/tokenizers<br/> 3.<code>sentencepiece</code>:https://github.com/google/sentencepiece</p><p>类似的，如果我需要直接使用别人已经训练好的的<code>tokenizer</code>（<a href="https://huggingface.co/THUDM/glm-4-9b-chat/tree/main"><code>GLM</code></a>，<a href="https://huggingface.co/Qwen/Qwen-7B/tree/main"><code>Qwen</code></a>）代码如下：</p><pre><code class="language-python">from ChatGLM.tokenization_chatglm import ChatGLMTokenizer
from ChatGLM.tokenization_chatglm import ChatGLMTokenizer
from Qwen.tokenization_qwen import QWenTokenizer

tokenizer_glm = ChatGLMTokenizer(
    vocab_file="./ChatGLM/tokenizer.model"
)
tokenizer_qwen = QWenTokenizer('./Qwen/qwen.tiktoken')

text = "介绍Tokenizer使用方法!"
encoded_input_glm = tokenizer_glm(text, return_tensors='pt')
encoded_input_qwen = tokenizer_qwen(text, 'pt')
print(encoded_input_glm, encoded_input_qwen)
decoded_output_glm = tokenizer_glm.decode(encoded_input_glm["input_ids"][0])
decoded_output_qwen = tokenizer_qwen.decode(encoded_input_qwen["input_ids"][:-1])
print(decoded_output_glm, decoded_output_qwen)

# {'input_ids': tensor([[64790, 64792, 30910, 32025, 12997,  6486, 31695, 31847, 30992]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'position_ids': tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8]])} {'input_ids': [100157, 37434, 37029, 39907, 0, 417], 'token_type_ids': [0, 0, 0, 0, 0, 1], 'attention_mask': [1, 1, 1, 1, 1, 1]}
# 介绍Tokenizer使用方法! 介绍Tokenizer使用方法!
</code></pre><h2 id="notice">Notice</h2><p>1、值得注意的是，一般来说文本预料，不如图片那般规整（图片可以直接修改尺寸到相同即可），文本可能长可能短，那么在<code>data_loader</code>时候就需要注意需要保证最后长度相同（1、提前固定到相同长度；2、在batch里面补充到相同长度）</p><pre><code class="language-python">from torch.utils.data import Dataset
from torch.nn.utils.rnn import pad_sequence
class Dataset(Dataset):
    def __init__():
        ...
    def __len__():
        ...
    def __getitem__():
        ...
  def collate_fn(self, batch):
      src_text = [x[0] for x in batch]
      tgt_text = [x[1] for x in batch]

      src_tokens = [[self.BOS] + self.sp_eng.EncodeAsIds(sent) + [self.EOS] for sent in src_text]
      tgt_tokens = [[self.BOS] + self.sp_chn.EncodeAsIds(sent) + [self.EOS] for sent in tgt_text]

      src = pad_sequence([torch.LongTensor(np.array(l_)) for l_ in src_tokens],
                                 batch_first=True, padding_value=self.PAD)
      trg = pad_sequence([torch.LongTensor(np.array(l_)) for l_ in tgt_tokens],
                                  batch_first=True, padding_value=self.PAD)
      ....
      return src_text, tgt_text, src, trg
</code></pre><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/2104.09864<br/> 2、https://zhuanlan.zhihu.com/p/642884818<br/> 3、https://kexue.fm/archives/8265<br/> 4、https://zhuanlan.zhihu.com/p/630696264<br/> 5、https://huggingface.co/THUDM/glm-4-9b-chat/tree/main<br/> 6、https://huggingface.co/Qwen/Qwen-7B/tree/main<br/> 7、https://arxiv.org/pdf/1901.02860<br/> 8、https://zhuanlan.zhihu.com/p/8306958113<br/> 9、https://arxiv.org/pdf/2502.20082<br/> 10、https://arxiv.org/pdf/2402.13753</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/02/03/pos-embedding.html</guid></item><item><title>深度学习混合专家模型（MoE）/KV-cache原理及其代码</title><link>https://www.big-yellow-j.top/posts/2025/01/27/MoE-KV-cache.html</link><description><![CDATA[混合专家模型（MoE）通过将门控网络与多个FFN层结合，替代传统Transformer中的FFN层，实现输入数据的动态路由与处理。其核心包括稀疏MoE层和门控网络：门控网络通过线性层与Softmax生成概率分布，选择部分FFN（专家）处理输入（稀疏MoE），而稠密MoE则让输入通过所有FFN；同时通过负载均衡损失（如重要性损失、Load Loss）解决专家负载不均衡问题，典型如DeepSeek-V3采用动态调节参数优化负载。KV cache是Transformer自回归推理中的关键优化技术，通过缓存历史K和V值，避免重复计算，以内存换速度；常见缓存类型包括静态缓存、滑动窗口缓存等，适用于...]]></description><content:encoded><![CDATA[<p>主要介绍 <strong>混合专家模型</strong>（MoE）、<code>KV cache</code>并且结合代码进行解释</p><h2 id="1混合专家模型moe">1、混合专家模型（<code>MoE</code>）</h2><p>参考<a href="https://huggingface.co/blog/zh/moe#%E4%BB%80%E4%B9%88%E6%98%AF%E6%B7%B7%E5%90%88%E4%B8%93%E5%AE%B6%E6%A8%A1%E5%9E%8B">HuggingFace</a>中介绍：混合专家模型主要由两部分构成：<br/> 1、<strong>稀疏的MoE层</strong>：这些层代替了传统 Transformer 模型中的前馈网络 (FFN) 层。MoE 层包含若干“专家”(例如 8 个)，每个专家本身是一个独立的神经网络。在实际应用中，这些专家通常是前馈网络 (FFN)，但它们也可以是更复杂的网络结构，甚至可以是 MoE 层本身，从而形成层级式的 MoE 结构。<br/> 2、<strong>门控网络/路由（Gate Layer/route Layer）</strong>：这个部分用于决定哪些令牌 (token) 被发送到哪个专家。例如，在下图中，“More”这个令牌可能被发送到第二个专家，而“Parameters”这个令牌被发送到第一个专家。有时，一个令牌甚至可以被发送到多个专家。令牌的路由方式是 MoE 使用中的一个关键点，因为路由器由学习的参数组成，并且与网络的其他部分一同进行预训练。</p><p><img src="https://s2.loli.net/2025/06/21/Y5Dw4tTaFg7IZqr.webp" alt="1" width="1095" height="562" loading="lazy" decoding="async"/></p><p>换言之也就是说：将原始的Transformer框架中的<code>FFN Layer</code>（全连接层）替换成一个由<code>Gate Layer</code>和若干的<code>FFN Layer</code>组成的结构，通过<code>Gate</code>来确定一个输入将会被那些<code>FFN</code>进行处理，而后对被<code>FFN</code>处理后的内容进行加权处理。</p><hr/><h3 id="1稠密moe和-稀疏moe">1、<strong>稠密MoE</strong>和 <strong>稀疏MoE</strong></h3><p><img src="https://s2.loli.net/2025/06/21/qYp4yPnNECkvoTb.webp" alt="1" width="1259" height="617" loading="lazy" decoding="async"/></p><p>检验而言：如上图所示，对于<strong>稠密的MoE</strong>（<code>Dense MoE</code>）而言（假设4个FFN）在通过Gate处理之后输入<code>X</code>要通过每一个FFN进行处理，而对于<strong>稀疏的MoE</strong>（<code>Sparse MoE</code>）而言，通过Gate处理只去选择部分FFN进行处理</p><h3 id="2moe原理">2、<code>MoE</code>原理</h3><p>1、<code>Gate/route</code>原理<br/> 输入数据$x$，通过一个线性层进行处理：</p> \[s = W_{gate}x+b\] <p>对于得到的<code>score</code>再通过<code>Softmax</code>函数处理，得到一个概率分布：</p> \[g(x)=\text{Softmax}(s)\] <p>对于<strong>稀疏的MoE</strong>而言还需要去选择部分专家进行激活：</p> \[h(x)=\frac{g(x)_{i\in Top-k}}{\sum_{i\in Top-k }g(x)_i}\] <p>原理很简单，结合代码分析（以<code>Deepseek-v3</code>代码为例）</p><pre><code class="language-python">class Gate(nn.Module):
    """
    Gating mechanism for routing inputs in a mixture-of-experts (MoE) model.
    """
    def __init__(self, args: ModelArgs):
        """
        Initializes the Gate module.

        Args:
            args (ModelArgs): Model arguments containing gating parameters.
        """
        super().__init__()
        self.dim = args.dim
        self.topk = args.n_activated_experts # 选择多少个专家进行使用
        self.n_groups = args.n_expert_groups # Gate数量
        self.topk_groups = args.n_limited_groups # 对于gate中分组数
        self.score_func = args.score_func
        self.route_scale = args.route_scale
        self.weight = nn.Parameter(torch.empty(args.n_routed_experts, args.dim))
        self.bias = nn.Parameter(torch.empty(args.n_routed_experts)) if self.dim == 7168 else None

    def forward(self, x: torch.Tensor) -&gt; Tuple[torch.Tensor, torch.Tensor]:
        """
        Forward pass for the gating mechanism.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            Tuple[torch.Tensor, torch.Tensor]: Routing weights and selected expert indices.
        """
        scores = linear(x, self.weight) # 计算wx+b
        # 归一化处理
        if self.score_func == "softmax":
            scores = scores.softmax(dim=-1, dtype=torch.float32)
        else:
            scores = scores.sigmoid()
        original_scores = scores

        if self.bias is not None:
            scores = scores + self.bias
        if self.n_groups &gt; 1:
            # 如果Gate数量&gt;1
            scores = scores.view(x.size(0), self.n_groups, -1)
            if self.bias is None:
                group_scores = scores.amax(dim=-1)
            else:
                group_scores = scores.topk(2, dim=-1)[0].sum(dim=-1)
            indices = group_scores.topk(self.topk_groups, dim=-1)[1]
            mask = torch.zeros_like(scores[..., 0]).scatter_(1, indices, True)
            scores = (scores * mask.unsqueeze(-1)).flatten(1)
        indices = torch.topk(scores, self.topk, dim=-1)[1]
        weights = original_scores.gather(1, indices)
        if self.score_func == "sigmoid":
            weights /= weights.sum(dim=-1, keepdim=True)
        weights *= self.route_scale
        return weights.type_as(x), indices
</code></pre><p>整个过程分析：输入数据<code>x</code>（假设维度为：（<code>bs, num_tokens, embed_dim</code>）），通过线性（W：（<code>n_routed_experts，embed_dim</code>）计算得到：<code>bs，num_tokens，n_routed_experts</code>）和归一化处理：（<code>bs，num_tokens， n_routed_experts</code>）这样一来就可以知道每个tokens的一个分布概率（到底要走哪一个FFN）。如果<code>n_expert_groups</code>数量&gt;1，也就是说如果<code>Gate</code>数量不止一个，那么也就只需要去选择<code>topK</code>即可然后用一个<code>mask</code>将其他的给mask掉即可（<code>scores = (scores * mask.unsqueeze(-1)).flatten(1)</code>）然后再去对挑选得到的<code>Gate</code>选择<code>topk</code>（<code>indices = torch.topk(scores, self.topk, dim=-1)[1]</code>）</p><p>2、<code>FFN Layer</code>原理</p><p>这部分代码就比较简单（直接用线性层处理即可）：</p><pre><code class="language-python">class Expert(nn.Module):
    """
    Expert layer for Mixture-of-Experts (MoE) models.

    Attributes:
        w1 (nn.Module): Linear layer for input-to-hidden transformation.
        w2 (nn.Module): Linear layer for hidden-to-output transformation.
        w3 (nn.Module): Additional linear layer for feature transformation.
    """
    def __init__(self, dim: int, inter_dim: int):
        """
        Initializes the Expert layer.

        Args:
            dim (int): Input and output dimensionality.
            inter_dim (int): Hidden layer dimensionality.
        """
        super().__init__()
        self.w1 = Linear(dim, inter_dim)
        self.w2 = Linear(inter_dim, dim)
        self.w3 = Linear(dim, inter_dim)

    def forward(self, x: torch.Tensor) -&gt; torch.Tensor:
        """
        Forward pass for the Expert layer.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            torch.Tensor: Output tensor after expert computation.
        """
        return self.w2(F.silu(self.w1(x)) * self.w3(x))
</code></pre><p>最后得到<code>MoE</code>代码如下：</p><pre><code class="language-python">class MoE(nn.Module):
    """
    Mixture-of-Experts (MoE) module.

    Attributes:
        dim (int): Dimensionality of input features.
        n_routed_experts (int): 模型中总的专家数量
        n_local_experts (int): 在分布式系统中，当前设备（节点）负责的专家数量
        n_activated_experts (int): 每个输入激活的专家数量（即每个输入会被路由到多少个专家）。
        gate (nn.Module): Gating mechanism to route inputs to experts.
        experts (nn.ModuleList): List of expert modules.
        shared_experts (nn.Module): Shared experts applied to all inputs.
    """
    def __init__(self, args: ModelArgs):
        """
        Initializes the MoE module.

        Args:
            args (ModelArgs): Model arguments containing MoE parameters.
        """
        super().__init__()
        self.dim = args.dim
        assert args.n_routed_experts % world_size == 0
        self.n_routed_experts = args.n_routed_experts
        self.n_local_experts = args.n_routed_experts // world_size
        self.n_activated_experts = args.n_activated_experts

        # 起始/结束（因为是涉及到分布训练，去寻找每个设备上的”专家“即可）
        self.experts_start_idx = rank * self.n_local_experts
        self.experts_end_idx = self.experts_start_idx + self.n_local_experts
        
        self.gate = Gate(args)
        self.experts = nn.ModuleList([Expert(args.dim, args.moe_inter_dim) if self.experts_start_idx &lt;= i &lt; self.experts_end_idx else None
                                      for i in range(self.n_routed_experts)])
        self.shared_experts = MLP(args.dim, args.n_shared_experts * args.moe_inter_dim)

    def forward(self, x: torch.Tensor) -&gt; torch.Tensor:
        """
        Forward pass for the MoE module.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            torch.Tensor: Output tensor after expert routing and computation.
        """
        shape = x.size()
        x = x.view(-1, self.dim)
        weights, indices = self.gate(x)
        y = torch.zeros_like(x)
        counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist()
        for i in range(self.experts_start_idx, self.experts_end_idx):
            if counts[i] == 0:
                continue
            expert = self.experts[i]
            idx, top = torch.where(indices == i)
            y[idx] += expert(x[idx]) * weights[idx, top, None]
        z = self.shared_experts(x)
        if world_size &gt; 1:
            dist.all_reduce(y)
        return (y + z).view(shape)
</code></pre><p>代码分析：<br/> 1、获取权重以及索引：<code>weights, indices = self.gate(x)</code><br/> 2、计算每个专家索引：<code>counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist()</code>通过<code>torch.bincount</code>返回数值个数，比如说：[1,2,2,2,3]，那么返回：[0,1,3,1]（0出现0次，1出现1次。。。。）相当于对每个专家都编号了，只需要根据出现次数，来索引出来<br/> 3、计算加权得分：</p><pre><code class="language-python">for i in range(self.experts_start_idx, self.experts_end_idx):
    if counts[i] == 0:
        continue
    expert = self.experts[i]
    idx, top = torch.where(indices == i)
    y[idx] += expert(x[idx]) * weights[idx, top, None]
</code></pre><h3 id="3平衡不同专家之间的负载">3、<a href="https://arxiv.org/pdf/2106.05974">平衡不同专家之间的负载</a></h3><p>如果涉及到多个专家，很可能就会出现一种情况：<strong>只有部分的专家被使用</strong>进而导致负载不均衡。那么可以通过 <strong>负载均衡损失</strong>（<code>Load Balancing Loss</code>）计算主要为：<br/> 1、<code>importance loss</code>（重要性损失）<br/> 回到上面对于<code>router</code>函数（$s = \text{softmax}(W_{gate}x+b)$）在<a href="https://arxiv.org/pdf/2106.05974">论文</a>中对于重要性损失定义如下：</p><blockquote><p>路由器函数为：$s = \text{Softmax}(\text{TopK}(W_{gate}x+n, k))$，区别在于补充一个高斯分布的$n$，以及选择<code>TopK</code>个路由器然后再去通过<code>softmax</code>进行处理（因为选择<code>TopK</code>这样一来其他的都是负无穷在通过<code>softmax</code>处理之后值为0）<br/> 在<code>DeepSeek V-3</code>里面也是这样操作的，选择<code>TopK</code></p></blockquote> \[L_{importance}(x)=(\frac{\text{std(x)}}{\text{mean}(x)})^2\] <p>也就是说，通过最小化上式子的最小值（均值大，方差小），进而保证所有的专家平均而言拥有相似的权重。</p><p>2、<code>Load Loss</code>：重要性损失旨在保证所有专家平均而言具有相似的路由权重。但是不难想到这些看上去有着总体趋于平衡的权重的路由配置，仍然有一小部分专家获得了所有分配，如下图：</p><p><img src="https://s2.loli.net/2025/06/21/UXFCamKJd8xBQRG.webp" alt="1" width="1004" height="267" loading="lazy" decoding="async"/></p><p>首先，对于专家选择阈值：$threshold_k(x):= max_k(Wx+ \phi)$，其中$\phi$代表前向传播过程中的采样噪声，$max_k$代表选择第$k$个最大的值，对于专家的负载：指的是在整个批次中的使用情况：$load_i(X)=\sum_{x\in X}p_i(x)$对于这个概率的计算：</p> \[p_i(x):=P(\phi_{new}≥threshold_k(x)-(Wx)_i)\] <p>其中$\phi_{\mathrm{new}}\sim\mathcal{N}(0,\sigma^{2}),\sigma=1/E$最后对于<code>load loss</code>定义为：</p> \[L_{load}(X)=(\frac{std(load(X))}{mean(load(X))})^2\] <p>最后损失函数为：$L=\frac{1}{2}L_{importance}(X)+ \frac{1}{2}L_{load}(X)$</p><hr/><p>借鉴<a href="https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-mixture-of-experts">这部分描述</a>对于<code>load balancing loss</code>描述</p><p><img src="https://s2.loli.net/2025/06/21/eI1i6OZ2sTN4dnm.webp" alt="2" width="919" height="232" loading="lazy" decoding="async"/></p><p>对于每个文本都会选择一个专家模型进行输入，因此都会通过路由器去计算他们的“得分”，去将这部分得分加起来，然后最小化$L_{load}(X)=(\frac{std(load(X))}{mean(load(X))})^2$这部分损失</p><p><img src="https://s2.loli.net/2025/06/21/kzJC6xtVejBWopa.webp" alt="1" width="535" height="360" loading="lazy" decoding="async"/></p><p><code>DeepSeek v3</code>处理负载不均衡问题：在常规的路由器处理得到：</p><p><img src="https://s2.loli.net/2025/06/21/XopAk5NgwasfDYS.webp" alt="image" width="471" height="74" loading="lazy" decoding="async"/></p><p>在<code>DeepSeek</code>中通过不同一个“动态调节”参数：$b_i$来保证不同专家之间的负载平衡</p><p><img src="https://s2.loli.net/2025/06/21/6ZME9YhOTfUbSG1.webp" alt="image" width="578" height="91" loading="lazy" decoding="async"/></p><p>具体实现方法（参考论文：https://arxiv.org/pdf/2408.15664中的描述）：</p><p><img src="https://s2.loli.net/2025/06/21/P6ZteKAWaOEYCFf.webp" alt="image" width="1172" height="414" loading="lazy" decoding="async"/></p><hr/><p>同时也可以采用另外的方法：<br/> 1、<a href="https://arxiv.org/pdf/2006.16668"><code>GShard</code></a>设定一个 <strong>专家容量</strong>：<strong>设定一个阈值，定义一个专家能处理多少令牌。如果两个专家的容量都达到上限，令牌就会溢出，并通过残差连接传递到下一层，或在某些情况下被完全丢弃。</strong>对于专家容量可以借鉴下面处理方式：$\frac{\text{tokens per batch}}{\text{number of experts}}\times \text{capacity factor}$（$\text{capacity factor}$可以选择<strong>1-1.25之间</strong>）</p><h2 id="kv-cache"><code>KV cache</code></h2><p>Transformer模型推理过程为：<br/><img src="https://jalammar.github.io/images/t/transformer_decoding_2.gif" alt="image.png"/></p><p>目前主流的LLM框架主要都是使用的<code>decoder-only</code>（也就是说只用<code>Transformer</code>中的<code>decoder</code>结构）<br/><img src="https://s2.loli.net/2025/06/21/VJ42MkDwaSbmRCt.webp" alt="image.png" width="316" height="581" loading="lazy" decoding="async"/></p><blockquote><p>对于LLM任务（通常采用<strong>自回归过程</strong>）可以简单认为是一种“完形填空”的过程，在输入前面i-1个词然后推测第i个词</p></blockquote><p>回归上面的推理过程（翻译输出：i am a student）：模型中在输出’a’的时候会将’i am’都输入到模型中。理解这个过程（假设就是直接输出文本：i am a student）：</p><blockquote><p>参考：https://zhuanlan.zhihu.com/p/662498827</p></blockquote><p><code>step1</code>: in: Q=K&lt;S&gt; || out: i<br/> $Attention_1: Q_1K_1^T$<br/><code>step2</code>: in: Q=K=&lt;s&gt;,i || out: i am<br/> $Attention_1: Q_1K_1^T \ Attention_2:Q_2K_1^T, Q_2K_2^T$<br/><code>step3</code>: in: Q=K=&lt;s&gt;, i, am || out: i am a<br/> $Attention_1: Q_1K_1^T \ Attention_2:Q_2K_1^T, Q_2K_2^T \ <br/> Attention_3: Q_3K_1^T, Q_3K_2^T, Q_3K_3^T$<br/><code>step4</code>: in: Q=K=&lt;s&gt;, i, am, a || out: i am a student<br/> $Attention_1: Q_1K_1^T \ Attention_2:Q_2K_1^T, Q_2K_2^T \<br/> Attention_3: Q_3K_1^T, Q_3K_2^T, Q_3K_3^T \<br/> Attention_4: Q_4K_1^T, Q_4K_2^T, Q_4K_3^T, Q_4K_4^T<br/> $</p><p>不过上面操作过程中会有问题：<br/> 计算有很大冗余（每次生成新的词，都需要回归一下之前生成的词），<strong>并且每次计算$Attention_i$只与$Q_i$相关</strong>对于后面一点理解（以<code>step2</code>为例）：<br/> 我目前已经有两个$Q$：$Q_1$:&lt;s&gt;, $Q_2:\text{i}$。并且还有K和V（这两个也是有两个值），我会初始化一个$Q_3$对于下一个值我就用$Q_3$进行表示，然后我就需要去计算注意力得分（只用Q,K,V这三个值计算过程举例）：<br/> $QK^T=(bs, 3, embed_dim)(bs, embed_dim, 2)=(bs, 3, 2)$，接下来计算$QK^TV=(bs, 3, 2)(bs,2,embed_dim)=(bs, 3, embed_dim)$<br/> 那么在这个过程中就会有一个有意思问题：<strong>Q会有重复的（dim=3，前面两个都是前面已经计算过的）</strong>（观察上面<code>Attention</code>计算可以发现:每次计算$Attention_i$只与$Q_i$相关）。因此就有<code>KV-cache</code>理论：既然每次都是Q在变化，但是K和V都是用的之前的，那我之前每次就只用新的Q去和旧的KV计算即可（将KV存储起来），<code>KV-cache</code>一种典型的用内存换速度的方法。</p><p><img src="https://s2.loli.net/2025/02/02/b4p9oAyNEWlZ7mr.gif" alt="image" width="2000" height="1125" loading="lazy" decoding="async"/></p><p>对于KV-cache更加直接的了解：<strong>对于模型的新的输出只与最后一个token有关，那么计算过程中没必要将之前的值再拿出来再计算一遍</strong>。而q是由输入数据动态计算出来的，而k，v是历史状态因此只会去缓存KV。</p><p>简易<code>Demo</code>:</p><pre><code class="language-python">import torch

class KVCache:
    def __init__(self):
        self.k = None
        self.v = None

    def update(self, k, v):
        if self.k is None:
            self.k = k
            self.v = v
        else:
            self.k = torch.cat([self.k, k], dim=1)  # 在序列维度上拼接
            self.v = torch.cat([self.v, v], dim=1)

    def get(self):
        return self.k, self.v

class Decoder(torch.nn.Module):
    def __init__(self, embed_dim, hidden_dim, vocab_size, num_heads=8):
        super().__init__()
        self.embedding = torch.nn.Embedding(vocab_size, embed_dim)
        self.attention = torch.nn.MultiheadAttention(embed_dim, num_heads)
        self.linear = torch.nn.Linear(embed_dim, vocab_size)
        self.kv_cache = KVCache()

    def forward(self, input_ids):
        x = self.embedding(input_ids)  # (batch_size, seq_len, embed_dim)

        # 获取 KV-cache
        k, v = self.kv_cache.get()

        # 计算 Attention
        if k is not None and v is not None:
            # 使用 KV-cache
            attn_output, _ = self.attention(x, k, v)  # (batch_size, seq_len, embed_dim)
        else:
            # 初始状态，没有 KV-cache
            attn_output, _ = self.attention(x, x, x)  # (batch_size, seq_len, embed_dim)

        # 更新 KV-cache
        self.kv_cache.update(x, x)

        # 残差连接
        x = x + attn_output

        # 线性变换
        logits = self.linear(x)  # (batch_size, seq_len, vocab_size)

        return logits

batch_size = 2
seq_len = 4
embed_dim = 64
hidden_dim = 256
vocab_size = 10000  # 假设词汇表大小为 10000
decoder = Decoder(embed_dim, hidden_dim, vocab_size)
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))  # (batch_size, seq_len)
logits = decoder(input_ids)  # (batch_size, seq_len, vocab_size)
print("Logits shape:", logits.shape)
</code></pre><p>使用<code>Huggingface</code>的<code>transformers</code>框架代码：https://huggingface.co/docs/transformers/main/en/kv_cache。只需要类似下面操作：</p><pre><code class="language-python">import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
ckpt = "microsoft/Phi-3-mini-4k-instruct"

tokenizer = AutoTokenizer.from_pretrained(ckpt)
model = AutoModelForCausalLM.from_pretrained(ckpt, torch_dtype=torch.float16).to("cuda:0")
inputs = tokenizer("Fun fact: The shortest", return_tensors="pt").to(model.device)

# 具体参数：https://huggingface.co/docs/transformers/en/main_classes/text_generation
out = model.generate(**inputs, do_sample=False, max_new_tokens=23, use_cache=True)
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])

out = model.generate(**inputs, do_sample=False, max_new_tokens=23)
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
</code></pre><p>在<code>Transformers</code>中不同<code>cache</code>方式：</p><table><thead><tr><th>缓存类型</th><th>描述</th><th>适用场景</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td><strong>StaticCache</strong></td><td>静态缓存，缓存所有的 K 和 V，不更新。</td><td>短序列生成、内存充足的场景</td><td>实现简单，快速</td><td>不适合长序列生成，内存消耗较大</td></tr><tr><td><strong>OffloadedStaticCache</strong></td><td>静态缓存，但将缓存内容卸载到外部存储。</td><td>内存受限的环境，长序列生成</td><td>减少显存占用，适合大规模生成</td><td>存取速度较慢，可能影响生成速度</td></tr><tr><td><strong>SlidingWindowCache</strong></td><td>滑动窗口缓存，缓存一个固定大小的窗口。</td><td>长序列生成、内存有限的场景</td><td>限制内存消耗，适合长序列生成</td><td>窗口太小可能丢失上下文信息，影响生成效果</td></tr><tr><td><strong>HybridCache</strong></td><td>混合缓存，结合静态缓存和滑动窗口缓存。</td><td>长序列生成，要求平衡内存和上下文</td><td>平衡内存消耗和上下文保留</td><td>比静态缓存更复杂，可能需要更多内存管理和计算资源</td></tr><tr><td><strong>MambaCache</strong></td><td>高效的缓存实现，针对推理速度和内存占用进行了优化。</td><td>高性能计算环境、高并发推理任务</td><td>高度优化，适合大规模并行推理</td><td>可能需要特定硬件支持，复杂度较高</td></tr><tr><td><strong>QuantizedCache</strong></td><td>量化缓存，减少存储需求。</td><td>内存受限的设备、需要减少内存占用的场景</td><td>大幅度减少内存占用，适合嵌入式设备</td><td>量化可能导致精度损失，影响生成质量</td></tr></tbody></table><p>争对上面描述其实<code>KV-cahce</code>是一种用存储换速度的方法，因此，对于KV存储进行优化就十分有必要了！对于<code>KV-cache</code>存储优化可以<a href="https://www.big-yellow-j.top/posts/2025/01/29/Attention.html">参考文章</a>。</p><h2 id="结论">结论</h2><p>主要介绍了MoE以及KV-cache理论及其代码，前者是将传统的mlp替换为：通过一个路由器将输入x就行选择，然后传入不同的专家模型（mlp）。后者则是对于计算attention过程中将中间过程值：KV进行缓存，这是因为模型新的输出只与最后一个输出的token有关，然后q是动态变化的因此只会去缓存kv</p><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/2101.03961<br/> 2、<a href="https://huggingface.co/blog/zh/moe#%E4%BB%80%E4%B9%88%E6%98%AF%E6%B7%B7%E5%90%88%E4%B8%93%E5%AE%B6%E6%A8%A1%E5%9E%8B">混合专家模型 (MoE) 详解</a><br/> 3、https://arxiv.org/pdf/2407.06204<br/> 4、https://github.com/deepseek-ai/DeepSeek-V3/blob/main/inference/model.py<br/> 5、https://arxiv.org/pdf/2106.05974<br/> 6、https://arxiv.org/pdf/2006.16668<br/> 7、https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf<br/> 8、https://jalammar.github.io/illustrated-transformer/<br/> 9、https://zhuanlan.zhihu.com/p/662498827<br/> 10、https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-mixture-of-experts<br/> 11、https://arxiv.org/pdf/2412.19437<br/> 12、https://arxiv.org/pdf/2408.15664</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Mon, 27 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/27/MoE-KV-cache.html</guid></item><item><title>CV中常用Backbone(Resnet/Unet/Vit系列/多模态系列等)以及代码</title><link>https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html</link><description><![CDATA[本文详细介绍计算机视觉（CV）领域常用Backbone的原理与应用，涵盖基于卷积神经网络、Transformer及多模态三大类。基于卷积神经网络的Backbone包括Resnet系列与Unet系列：Resnet通过残差连接（skip connection）引入恒等映射，有效缓解深层网络的梯度消失与退化问题，不同型号（Resnet18/34/50/101/152）的核心差异在于卷积层数，其Bottleneck结构通过1×1卷积降维、3×3卷积特征提取及1×1卷积升维实现高效计算；Unet系列（Unet1/Unet++/Unet3）主要用于图像分割，通过下采样（卷积+池化）与上采样（插值/亚...]]></description><content:encoded><![CDATA[<p>主要介绍在CV中常用的Backbone<strong>原理</strong>简易<a href="https://www.big-yellow-j.top/code/cv_backbone.html">代码</a>（<em>代码以及原理经常更新</em>），参考论文中的表格，对不同的任务所使用的backbone如下:<br/><img src="https://s2.loli.net/2025/06/22/frtz8wFg1u3m59e.webp" alt="image" width="1319" height="725" loading="lazy" decoding="async"/></p><p>针对上面内容分为两块内容：1、基于卷积神经网络的CV Backbone：1.<code>Resnet</code>系列;2.<code>Unet</code>系列等；2、基于Transformer的 CV Backbone：1.<code>Vit</code>系列等；3、在多模态中常用的backbone如：SAM/Clip等</p><blockquote><p>FROM:https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html</p></blockquote><h2 id="一基于卷积神经网络的cv-backbone">一、基于卷积神经网络的CV Backbone：</h2><h3 id="1-resnet系列">1. <code>Resnet</code>系列</h3><p>主要有<a href="https://arxiv.org/pdf/1512.03385">何凯明大佬</a>提出，主要有<code>resnet18</code>，<code>resnet34</code>，<code>resnet50</code>，<code>resnet101</code>，<code>resnet152</code>，这几种区别主要就在于卷积层数上存在差异（18：18个卷积后面依次类推）,对于<code>Resnet</code>论文中最重要的一个就是<code>残差连接</code>：<br/><img src="https://s2.loli.net/2025/06/22/QcfiNqxBODakKjg.webp" alt="残差连接" width="492" height="269" loading="lazy" decoding="async"/></p><p>因为随着不断的叠加卷积层数，那么就容易导致 <strong>梯度消失</strong>以及 <strong>退化</strong>问题，残差连接就是通过跳跃连接（skip connection），允许输入信息绕过若干层直接传递到后面的层：</p> \[h^{l+1} = h^l + F(h^l, W^l)\] <p>其中$x$表示我们输入，$h^l$第$l$层的输入，$F(h^l, W^l)$残差分支的非线性变换。对于上面提到的两个问题，残差连接之所以能够缓解，是因为：<br/> 1、对于<strong>梯度消失问题</strong>（对于一个神经网络结构，由于反向传播时梯度不断地被链式法则的多个小梯度乘积缩小，最终在靠近输入层的地方梯度变得接近于零，导致参数无法有效更新），残差连接在反向传播时引入了一个 <strong>恒等映射（identity mapping）</strong>，使得梯度可以沿着跳跃路径直接传递给前层。这避免了梯度完全依赖深层网络中的权重进行传播。数学上表述就是，对于残差网络连接而言<strong>梯度传递</strong>为：</p> \[\frac{\partial h^{L}}{\partial h^{l}} = I + \sum_{k=l}^{L-1} \prod_{j=k+1}^{L-1} \frac{\partial F(h^j, W^j)}{\partial h^j}\] <p>$I$: 恒等映射项，确保梯度具有直接路径传播至浅层。$\sum_{k=l}^{L-1}$: 累积的非线性变换贡献。对于前馈神经网络而言<strong>梯度传递</strong>为：</p> \[\frac{\partial h^L}{\partial h^l} = \prod_{k=l}^{L-1} \frac{\partial F(h^k, W^k)}{\partial h^k}\] <p>对比很容易发现，如果某一层出现梯度值很大/小问题，那么就会导致这个效果被不断的扩大，但是残差连接就可以较好的避免这个问题，从另外一种角度而言，<strong>对于第$l+1$层的输入不仅仅只考虑$l$层的信息，还要去结合输入$l$层的信息$x$</strong>（就好比：<strong>传递口号，在A这里错了，后面（B，C）可能就都是错了，但是A传递正确给B，B如果传递错误给C但是C还要听一下A怎么给B讲的，这样就可以很好的保证后面口令都不错误</strong>）</p><p>2、对于 <strong>退化问题</strong>：网络深度增加时，传统深层网络会因优化难度增大而导致训练误差不降反升。残差连接引入恒等映射，使网络每层只需学习输入与目标之间的 <strong>差值（residual）</strong>，降低了优化的难度：</p><ul><li>如果某层优化失败，跳跃连接仍能保留输入特征，从而避免性能下降。</li><li>在极端情况下，残差网络等价于浅层网络（当 $F(x) = 0$时）。</li></ul><p>简易代码<code>Demo</code>（直接用<code>torch</code>）:</p><pre><code class="language-python">import torch
import torchvision.models as models

model = models.resnet50(pretrained=False) # true就会下载权重
</code></pre><p>具体修改某一层参数，可以先将模型<code>print</code>出来然后直接进行修改。比如说修改<code>resnet50</code>中最后的线性层：</p><pre><code># 原始结构：(fc): Linear(in_features=2048, out_features=1000, bias=True)
model.fc = nn.Linear(2048, 10) # 预测10个类别
</code></pre><p>总结一下上面提到的<code>Resnet</code>的输出，假设输入图片为：$1 \times 3 \times 512 \times 512$</p><table><thead><tr><th style="text-align: center">模型</th><th style="text-align: center"><code>Layer1</code> (blocks)</th><th style="text-align: center"><code>Layer2</code> (blocks)</th><th style="text-align: center"><code>Layer3</code> (blocks)</th><th style="text-align: center"><code>Layer4</code> (blocks)</th><th style="text-align: center">总 block 数量</th></tr></thead><tbody><tr><td style="text-align: center"><strong>ResNet-50</strong></td><td style="text-align: center">3:<code>1, 256, 128, 128</code></td><td style="text-align: center">4:<code>1, 512, 64, 64</code></td><td style="text-align: center">6:<code>1, 1024, 32, 32</code></td><td style="text-align: center">3:<code>1, 2048, 16, 16</code></td><td style="text-align: center">16 (Bottleneck)</td></tr><tr><td style="text-align: center"><strong>ResNet-101</strong></td><td style="text-align: center">3:<code>1, 256, 128, 128</code></td><td style="text-align: center">4:<code>1, 512, 64, 64</code></td><td style="text-align: center">23:<code>1, 1024, 32, 32</code></td><td style="text-align: center">3:<code>1, 2048, 16, 16</code></td><td style="text-align: center">33 (Bottleneck)</td></tr><tr><td style="text-align: center"><strong>ResNet-152</strong></td><td style="text-align: center">3:<code>1, 256, 128, 128</code></td><td style="text-align: center">8:<code>1, 512, 64, 64</code></td><td style="text-align: center">36:<code>1, 1024, 32, 32</code></td><td style="text-align: center">3:<code>1, 2048, 16, 16</code></td><td style="text-align: center">50 (Bottleneck)</td></tr><tr><td style="text-align: center"><strong>ResNet-18</strong></td><td style="text-align: center">2:<code>1, 64, 256, 256</code></td><td style="text-align: center">2:<code>1, 128, 128, 128</code></td><td style="text-align: center">2:<code>1, 256, 64, 64</code></td><td style="text-align: center">2:<code>1, 512, 32, 32</code></td><td style="text-align: center">8</td></tr><tr><td style="text-align: center"><strong>ResNet-34</strong></td><td style="text-align: center">3:<code>1, 64, 256, 256</code></td><td style="text-align: center">4:<code>1, 128, 128, 128</code></td><td style="text-align: center">6:<code>1, 256, 64, 64</code></td><td style="text-align: center">3:<code>1, 512, 32, 32</code></td><td style="text-align: center">16</td></tr></tbody></table><p><strong>Bottleneck 层的具体结构</strong>：</p><p>假设我们有一个输入张量 ( X )，其通道数为 ( C_{in} )，输出通道数为 ( C_{out} )，通过 Bottleneck 结构后，网络的计算可以分为以下几个步骤：</p><ol><li><strong>第一层卷积</strong>（瓶颈）：( 1 \times 1 ) 卷积，将输入通道数从 ( C_{in} ) 降低到一个较小的中间通道数 ( C_{mid} )（通常 ( C_{mid} &lt; C_{in} )）。 <ul><li>输出形状：\(H \times W \times C_{mid}\)</li></ul></li><li><strong>第二层卷积</strong>：( 3 \times 3 ) 卷积，进行特征提取。 <ul><li>输出形状：\(H \times W \times C_{mid}\)</li></ul></li><li><strong>第三层卷积</strong>：再次使用 ( 1 \times 1 ) 卷积，将中间通道数 ( C_{mid} ) 恢复到输出通道数 ( C_{out} )。 <ul><li>输出形状：\(H \times W \times C_{out}\)</li></ul></li></ol><p>最终，输入 ( X ) 和输出 ( Y ) 通过残差连接相加，形成一个新的输出。残差连接使得信息能够直接流经网络的不同层，从而避免梯度消失问题。</p><h3 id="2unet系列">2.<code>Unet</code>系列</h3><p><code>Unet</code>主要介绍3种：<code>Unet1</code>，<code>Unet++</code>，<code>Unet3</code>，主要应用在医学影像分割（当然图像分割领域都适用）</p><p><img src="https://s2.loli.net/2025/06/22/Mmz1coPNlqSny7C.webp" alt="2" width="1870" height="685" loading="lazy" decoding="async"/></p><p>对比上面三种结构，主体结构上并无太大差异，都是首先通过下采样（左边），然后通过上采样（右边）+特征融合。主要差异就在于<strong>如何进行特征融合</strong>。以<code>Unet</code>进行理解：</p><p><img src="https://s2.loli.net/2025/06/22/Bil6xMFwr9VXWsS.webp" alt="2" width="1041" height="697" loading="lazy" decoding="async"/></p><p><strong>左侧encoder操作</strong>：首先通过两层$3 \times3$卷积进行处理，然后通过一个 <strong>池化</strong>处理<br/><strong>右侧decoder操作</strong>：一个上采样的卷积层（去卷积层）+特征拼接concat（上图中白色部分就是要拼接的encoder内容）+两个3x3的卷积层（ReLU）反复构成。<br/><code>Unet</code>相比更早提出的<code>FCN</code>网络，使用拼接来作为特征图的融合方式。<code>FCN</code>是通过特征图对应像素值的<strong>相加</strong>来融合特征的；<code>U-net</code>通过<strong>通道数的拼接</strong>。<br/><strong><code>Unet</code>好处就在于，因为是逐层的去累加卷积操作，随着卷积的“深入”，越往下的卷积就拥有更加大的 <em>感受野</em>，但局部细节可能会逐渐丢失。为了解决这个问题，通过 <em>上采样</em>操作来恢复这些细节。上采样操作将低分辨率的特征图尺寸恢复到较高分辨率，从而保留更多的局部特征，弥补下采样过程中丢失的细节。最后将两部分内容继续融合（里面的skip-connection操作）相互进行弥补实现较好性能</strong></p><blockquote><p><strong>感受野</strong>：可以简单理解：比如说一个512x512图像，最开始用卷积核（假设为3x3）去“扫”，那么这个卷积核就会把其“扫”的内容“汇总”起来，比如说某一个值是汇聚了他周围其他的值，这样一来<strong>细节的感知就很多</strong>，但是随着网络层数叠加，这些细节内容就会越来越少，但是计算得到的每个值却是“了解”到了更加“全局”的内容，如下图展示一样<br/><img src="https://s2.loli.net/2025/06/22/3jaHSlBpb5WcCdh.webp" alt="3" width="883" height="537" loading="lazy" decoding="async"/></p><p><strong>上采样</strong>：可以简单理解为：将图片给“扩大”，既然要扩大，那么就会需要对内容进行填补，因此就会有不同的插值方式：’nearest’, ‘linear’, ‘bilinear’, ‘bicubic’（<code>pytorch</code>提供的）<br/><img src="https://s2.loli.net/2025/01/20/UQMEFlPKks8DRLt.webp" alt="4" width="395" height="449" loading="lazy" decoding="async"/></p><p>补充一点： <strong>亚像素上采样 (Pixel Shuffle)</strong>：普通的上采样采用的临近像素填充算法，主要考虑空间因素，没有考虑channel因素，上采样的特征图人为修改痕迹明显，图像分割与GAN生成图像中效果不好。为了解决这个问题，ESPCN中提到了亚像素上采样方式。<a href="https://www.cnblogs.com/zhaozhibo/p/15024928.html">具体原理</a>如下<br/><img src="https://s2.loli.net/2025/06/22/BY8NhfOzQti6brV.webp" alt="image" width="1732" height="522" loading="lazy" decoding="async"/></p><p>根据上图，可以得出将维度为$[B,C,H,W]$的 feature map 通过亚像素上采样的方式恢复到维度$[B,C,sH,sW]$的过程分为两步：<br/> 1.首先通过卷积进行特征提取，将$[B,C,H,W]=&gt;[B,s^2C,H,W]$<br/> 2.然后通过Pixel Shuffle 的操作，将$[B,s^2C,H,W]=&gt;[B,C,sH,sW]$<br/> Pixel Shuffle的主要功能就是将这$s^2$个通道的特征图组合为新的$[B,C,sH,sW]$的上采样结果。具体来说，就是将原来一个低分辨的像素划分为$s^2$个更小的格子，利用$s^2$个特征图对应位置的值按照一定的规则来填充这些小格子。按照同样的规则将每个低分辨像素划分出的小格子填满就完成了重组过程。在这一过程中模型可以调整$s^2$个shuffle通道权重不断优化生成的结果。</p></blockquote><pre><code class="language-python">class Net(nn.Module):
    def __init__(self, upscale_factor):
        super(Net, self).__init__()

        self.conv1 = nn.Conv2d(1, 64, (5, 5), (1, 1), (2, 2))
        self.conv2 = nn.Conv2d(64, 32, (3, 3), (1, 1), (1, 1))
        self.conv3 = nn.Conv2d(32, 1 * (upscale_factor ** 2), (3, 3), (1, 1), (1, 1))	# 最终将输入转换成 [32, 9, H, W]
        self.pixel_shuffle = nn.PixelShuffle(upscale_factor)	# 通过 Pixel Shuffle 来将 [32, 9, H, W] 重组为 [32, 1, 3H, 3W]
    def forward(self, x):
        x = torch.tanh(self.conv1(x))
        x = torch.tanh(self.conv2(x))
        x = torch.sigmoid(self.pixel_shuffle(self.conv3(x)))
        return x
    
if __name__ == "__main__":
    model = Net(upscale_factor=3)
    input = torch.arange(1, 10, dtype = torch.float32).view(1,1,3,3)
    output = model(input)
    print(output.size())

# 输出结果为：
torch.Size([1, 1, 9, 9])

</code></pre><p>对比三种<code>UNet</code>操作，从$1\rightarrow 3$进行特征融合的程度更加多，1：将<strong>同“水平”</strong> 的特征以及“下面”特征进行使用；3：将“左侧”所有的特征以及“下面”特征都进行使用；<code>Unet++</code>:不是直接使用简单的<code>skip-connection</code>而是回去结合 <strong>邻近水平</strong>和 <strong>邻近水平下</strong>的特征，比如说下面图c通过卷积操作结合$X^{0,0}\text{和}X^{1,0}$的特征。</p><p>总结上面三种网络结构改进在于：1、<code>Skip-connection</code>方式上区别（也就是<strong>如何进行特征连接过程</strong>）</p><blockquote><p><code>Unet++</code>网络结构<br/><img src="https://s2.loli.net/2025/06/22/DrdxPM7BUEfgalI.webp" alt="image" width="1142" height="723" loading="lazy" decoding="async"/></p></blockquote><h3 id="3其他">3.其他</h3><p>对于传统的<code>AlexNet</code>，<code>LeNet</code>，<code>GoogleNet</code>可以去看之前写的内容：<br/> 1、https://www.big-yellow-j.top/code/AlexNet.html<br/> 2、https://www.big-yellow-j.top/code/LeNet.html<br/> 3、https://www.big-yellow-j.top/code/googlenet.html</p><h2 id="二基于transformer的cv-backbone">二、基于Transformer的CV Backbone</h2><p>主要介绍两种：<code>Vit</code>和<code>MAE</code>。<code>Vit</code>:核心思想是将图像划分为小块（<code>Patch</code>），将每个小块视为一个 “单词”（类似 NLP 中的 Token），然后通过标准的 Transformer 架构对这些 Patch 进行处理。<br/><img src="https://s2.loli.net/2025/06/22/vEGOWdy1fkgh2JT.webp" alt="2" width="1241" height="436" loading="lazy" decoding="async"/><br/><code>Vit</code>主要操作流程：<br/> 1、<code>patch embedding</code>和<code>position embeeding</code>：将图片进行切分为固定大小的patch，比如说输入一张224x224RGB图像，path=16x16。那么的话就会生成：$\frac{224\times224}{16\times16}=196$个patch，那么输入模型的序列数量：<strong>196</strong>，经过拉长处理之后得到的序列长度为：$16\times 16\times 3=768$。通过线性投射层处理之后维度为：$196\times 768$一共为 <strong>196</strong>个token，然后补充一个位置编码，对于位置编码最简单的就是直接对每一个patch都生成一个1维的向量（类似one-hot，但是对于位置编码的方式有很多）然后去拼接起来（同时还需要补充一个<code>CLS</code>），最后维度就是：$197\times768$</p><blockquote><p><strong>值得注意的是</strong>：正如上提到的<strong>亚像素上采样 (Pixel Shuffle)</strong>可以通过他的逆操作将token数量减少（其实就是将尺寸改变，比如[b,c,w,h]–&gt;[b,c/r^2,w/2,h/2]）<br/><code>patch embedding</code>阶段处理，是直接通过一个16x16的<strong>卷积去处理</strong>（不是物理上的切割）图片维度变化：1x3x224x224 而后通过卷积核处理得到 1x768x14x14最后进行拉平处理（<code>x.flatten(2).transpose(1, 2</code>）得到 1x196x768<br/> 对于cls_token的添加过程直接通过初始化一个参数（<code>nn.Parameter(torch.zeros(1, 1, num_features))</code>）然后拼接到上面的视觉特征上</p></blockquote><p>2、<code>transformer encoder</code>：就是一个正常的transformer的encoder处理输入多少维度输出多少维度，依旧是$197\times768$<br/><code>MAE</code> 主要操作流程<br/> 1、<code>patch embedding</code> 和 <code>position embedding</code>：前面操作和<code>Vit</code>操作差异不大，区别在于<code>MAE</code>进行 <strong>随机遮盖（Masking）</strong>，例如遮盖 75% 的 Patch，只保留 <strong>25%</strong> 的 Token，用于后续的编码器输入。最终，编码器的输入维度变为：$49 \times 768$（假设保留的 Token 为 49）。与此同时，补充位置编码，最简单的方式是为每个 Patch Token 添加一个唯一的向量（类似于 One-Hot），拼接后维度保持不变。<br/> 2、<code>masked token reconstruction</code>：将编码器的输出输入到解码器中，同时将被遮盖的 Token 填充为一个固定的嵌入（称为 Mask Token）。解码器的输入维度恢复为：$196 \times 768$解码器通过 Transformer 操作，将未遮盖的 Token 特征与 Mask Token 结合，并尝试重建完整图像。重建的目标是尽可能接近原始图像像素值。</p><blockquote><p><strong>值得注意的是</strong>：MAE 的优势在于编码器仅处理未遮盖的部分 Token，大大减少了计算成本。同时，解码器可以设计得更轻量，仅用于重建任务，最终可以通过重建损失（如 L2 损失）优化模型。<br/> 在<code>MAE</code>中是分与训练和微调的，与训练就是去预测mask内容，微调就是直接根据不同任务进行微调即可（换输出头/微调里面参数）</p></blockquote><pre><code class="language-python">class DetectionHead(nn.Module):
    def __init__(self, embed_dim, num_classes):
        super().__init__()
        self.cls_head = nn.Linear(embed_dim, num_classes)  # 分类头
        self.reg_head = nn.Linear(embed_dim, 4)           # 边框回归头

    def forward(self, x):
        cls_preds = self.cls_head(x)  # [B, num_patches, num_classes]
        reg_preds = self.reg_head(x)  # [B, num_patches, 4]
        return cls_preds, reg_preds
</code></pre><p><strong>然后将检测头补充到最后的decoder输出后面即可</strong></p><p><strong>补充</strong><br/> 1、在<code>Vit</code>和<code>MAE</code>的代码中（两部分代码差异不大，以<code>MAE</code>为例）一般而言有如下参数：<br/><code>img_size=224, patch_size=16, in_chans=3, embed_dim=768, encoder_layers=12, decoder_embed_dim=512, decoder_layers=4, mask_ratio=0.75</code><br/><strong>第一步</strong>：对于一个输入图片<strong>首先</strong>通过<code>PatchEmbedding</code>（用卷积（<code>in_channels=3</code>， <code>out_channels=embed_dim=768</code>）去“扫”）然后拉平（<code>x.flatten(2).transpose(1, 2) </code>）输出维度为：<code>[B, num_patches, embed_dim]</code>然后与位置编码相加维度为：<code>[B, num_patches, embed_dim]</code><br/><strong>第二步</strong>：随机mask部分内容：<code>x_masked, mask, keep = self.random_masking(x)</code>，<code>x_masked</code>为随机mask后内容，<code>mask</code>为mask掉的内容，<code>keep</code>为mask余下内容（比如1到100，其中<code>x_maked</code>，<code>keep</code>都为25）主要用来保证顺序，在输入<code>decoder</code>之前需要把之前mask内容补充进来<br/> 2、<strong>transformer框架模型一般而言需要较多的数据进行训练，如果数据少还是用卷积效果会好一点</strong></p><p><code>Swin Transformer</code>模型</p><p><img src="https://s2.loli.net/2025/06/22/Sd8O5E1Pouc7x3t.webp" alt="image" width="1329" height="679" loading="lazy" decoding="async"/></p><p>对比之前的<code>Vit</code>和<code>MAE</code>存在<strong>问题</strong>在计算注意力的时候都是<strong>全局计算</strong>的（每个token之间都是进行注意力计算）因此在<code>Swin Transformer</code>中作者认为这种操作不利于：高分辨率图像（像素点多计算量大）以及密集预测任务（全局的话可能对有些细节就会丢失）</p><blockquote><p>The global computation leads to quadratic complexity with respect to the number of tokens, making it unsuitable for many vision problems requiring an immense set of tokens for dense prediction or to represent a high-resolution image.</p></blockquote><p>要去避免全局计算，一个最简单的办法就是：我去从不同的patch中挑选出一部分内容组合起来，然后再组合的这一块内容中去计算注意力。</p><p><img src="https://s2.loli.net/2025/06/22/RJOCIneTc4sFM9t.webp" alt="image" width="1559" height="555" loading="lazy" decoding="async"/></p><ul><li><p>1、<code>Patch Merging</code>操作，这部分操作就是进行 <strong>挑选组合</strong>操作，对<code>patch</code>之间进行组合，作者论文中表示是：挑选2x2的邻居进行分组（这里操作和<code>Unet</code>中下采样很像，每个stage中都进行一次减小尺寸，这样一来就可以看到更加“全局”）<br/><img src="https://s2.loli.net/2025/06/22/7lygxw4G2XmNAOk.webp" alt="image" width="934" height="460" loading="lazy" decoding="async"/></p></li><li><p>2、<code>Swin Transformer Block</code>：在将<code>patch</code>组合操作之后，输入到Transformer中，在这里作者将传统的注意力计算改为两种：<code>W-MSA</code>（Window-Multi-Head Self Attention）和<code>SW-MSA</code>（Shift-Window-Multi-Head Self Attention）之所以这样，作者还是在解决上面提到的问题：去避免全局计算。<br/><code>W-MSA</code>操作：对于传统的计算量大问题（$MSA=4hwC^2+2(hw)^2C$）提出改进（$W\text{-}MSA=4hwC^2+ 2M^2hwC$）这部分操作好理解，对于（H，W，C）划分为MxM的窗口得到：（N，MxM，C），然后就只需要对这部分计算Attention-Score即可<br/><code>SW-MSA</code>操作：弥补上面（<code>W-MSA</code>）存在问题，如果只是计算窗口内部的Attention-Score，就会导致不同窗口之间关系是不知道的，通过下面Shifted-Window来移动窗口位置<br/><img src="https://s2.loli.net/2025/01/23/MI9xOlNzo1C8wPZ.gif" alt="image" width="494" height="490" loading="lazy" decoding="async"/></p></li></ul><p>通过上面移动进而构成下面图像：<br/><img src="https://s2.loli.net/2025/06/22/kmCV5FGcvlAdTWp.webp" alt="image" width="649" height="249" loading="lazy" decoding="async"/></p><p>这里就会有9块，再<code>W-MSA</code>中是4块（都是4x4），无疑加大了计算量，因此只需要将9块重新进行拼接起来（保证最后为4x4即可）就可以，比如下面，<br/><img src="https://s2.loli.net/2025/06/22/Kimuh2CfRrbH3VP.webp" alt="image" width="934" height="249" loading="lazy" decoding="async"/></p><blockquote><p>对于上面的操作，可以直接通过<code>torch.roll</code>实现先<strong>左移动3然后上移动3</strong>。<code>torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2))</code></p></blockquote><p>这样一来就都满足（4x4）还可以实现不同window之间进行交互（5，3为例，将他们视作整体，计算AttentionScore）不过值得注意的是，5和3之间像素都是有差异的，直接计算会引入误差，因此原文在计算注意力时，在执行softmax之前，分别将模块3像素对应的注意力值分别减去100，使得softmax后，权重都是0，从而实现模块<a href="https://www.cnblogs.com/chentiao/p/18379629">3对模块5的影响</a>。<br/><img src="https://s2.loli.net/2025/06/22/seG9NVSqd658QRK.webp" alt="image" width="752" height="235" loading="lazy" decoding="async"/></p><p>具体操作：</p><pre><code class="language-python">...
attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
...
# con atten
q, k, v = qkv[0], qkv[1], qkv[2]  # make torchscript happy (cannot use tensor as tuple)

q = q * self.scale
attn = (q @ k.transpose(-2, -1))
...
if mask is not None:
    nW = mask.shape[0]
    attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
    attn = attn.view(-1, self.num_heads, N, N)
    attn = self.softmax(attn)
else:
    attn = self.softmax(attn)
</code></pre><blockquote><p>上面操作很容易理解：对于<strong>注意力计算</strong>：计算$QK^T$之后，得到各个点的<strong>权重</strong>，然后把这个权重加权到$V$上，上面操作通过加$-100$然后通过softmax处理，那么不属于分区内的“点”（比如53组合）权重就会被处理为0</p><h2 id="三多模态backbone">三、多模态backbone</h2></blockquote><p>这部分内容主要介绍在多模态算法中常用的几类<code>backbone</code>，主要为代码（SAM/Clip等）</p><blockquote><p>多模态算法涉及到的<code>backbone</code>比较杂，传统卷积/Transformer都有</p></blockquote><p>简短介绍：<code>Clip</code>：将文本和图像已经通过对齐；<code>SAM</code>：主要用来作分割（简单理解为：抠图）</p><pre><code class="language-python"># Clip
'''
预先下载/直接transformer下载：
config.json, preprocessor_config.json, tokenizer.json vocab.json pytorch_model.bin
'''
import transformers
import torch
import torch.nn as nn
from transformers import CLIPProcessor, CLIPModel

class ClipBackbone(nn.Module):
    def __init__(self, ):
        super(ClipBackbone, self).__init__()
        self.clip_model = CLIPModel.from_pretrained('./clip/')
        self.processor = CLIPProcessor.from_pretrained('./clip/')
        ...
        # 加载llm
        ...
    def forward(self, image, text):
        # clip提取特征
        inputs = self.processor(images= image, return_tensors= "pt")
        with torch.no_grad():
            image_embeddings = self.clip_model.get_image_features(**inputs)
        ...
        # 将image_embeddings和llm尺寸对其
        ...
        # llm处理
        ...
        return image_embeddings
</code></pre><p>输出为维度<code>config.json</code>中参数<code>"projection_dim": 768</code></p><h2 id="参考">参考:</h2><p>1、https://arxiv.org/pdf/2206.08016<br/> 2、https://arxiv.org/pdf/1512.03385<br/> 3、https://arxiv.org/pdf/2010.11929<br/> 4、https://arxiv.org/pdf/2111.06377<br/> 5、https://arxiv.org/pdf/1505.04597<br/> 6、https://arxiv.org/pdf/2311.17791<br/> 7、https://arxiv.org/pdf/2004.08790<br/> 8、https://pytorch.org/docs/stable/generated/torch.nn.Upsample.html<br/> 9、https://www.cnblogs.com/zhaozhibo/p/15024928.html<br/> 10、https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Shi_Real-Time_Single_Image_CVPR_2016_paper.pdf<br/> 11、https://arxiv.org/pdf/1807.10165v1<br/> 12、https://arxiv.org/pdf/2103.14030<br/> 13、https://www.cnblogs.com/chentiao/p/18379629<br/> 14、https://github.com/facebookresearch/segment-anything<br/> 15、https://github.com/openai/CLIP</p> ]]></content:encoded><category>Backbone</category><pubDate>Sat, 18 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/18/CV-Backbone.html</guid></item><item><title>深度学习Word Embedding原理及其代码</title><link>https://www.big-yellow-j.top/posts/2025/01/06/TextEmbedding.html</link><description><![CDATA[Word Embedding是将文本转化为计算机可理解的数字表示的技术。传统one-hot编码虽简单，但会丧失语义信息，无法体现如“喜欢”与“爱”的语义相似性。静态词向量预训练模型包括Word2vec和GloVe：Word2vec基于简化神经网络，分为CBOW与Skip-gram两种范式，CBOW以上下文词向量输入、目标词向量输出，适合小型数据库；Skip-gram则相反，在大型语料中表现更优。GloVe是基于统计的方法，通过词的共现矩阵（记录两词共同出现次数）生成词向量，但存在矩阵稀疏、维度高的问题。动态词向量为解决静态词向量无法处理一词多义的问题而提出，可根据数据集特点微调，如ELM...]]></description><content:encoded><![CDATA[<p>Word Embedding：将文本处理为计算机可以理解的数字表示。一种最简单的表示就是直接用one-hot，我建立一个字典，然后对每个字符都进行编码比如说：你好（表示为：10，01）。但是这种编码会有一个问题：<strong>丧失语义信息</strong>，比如说对于文本：喜欢和爱这两个词可能会被编码成：1000和0100，但是从语义上这两个词所表达的含义应该是相似的，但是用one-hot编码无法表示这种关系。</p><h2 id="静态词向量预训练模型">静态词向量预训练模型</h2><h3 id="1word2vec词向量">1、<strong>Word2vec词向量</strong></h3><p><strong>Word2vec</strong>其实就是一个简单化的神经网络结构，但是为了文本能够顺利的输入到模型，首先做法还是：对文本通过one-hot进行编码然后通过一个线形层进行处理（没有激活函数）更具模型的输入和输出差异，有两种范式：1、Skip-gram；2、CBOW。<strong>CBOW模型</strong>的训练输入是某一个特征词的上下文相关的词对应的词向量，而输出就是这特定的一个词的词向量。　<strong>Skip-Gram模型</strong>输入是特定的一个词的词向量，而输出是特定词对应的上下文词向量。CBOW对小型数据库比较合适，而Skip-Gram在大型语料中表现更好。</p><p><img src="https://s2.loli.net/2025/06/21/v97DVGROM3Ecj6I.webp" alt="image" width="778" height="493" loading="lazy" decoding="async"/></p><table><tbody><tr><td><strong>CBOW模型</strong>，对于输入词表，设定一个窗口大小然后计算目标词的概率。比如输入文本：预训练模型。对中间词“练”给mask然后设定窗口大小，再去计算概率值：p(练</td><td>预,训,模,型)</td></tr><tr><td><strong>Skip-gram模型</strong>：还是按照上面例子为例，比如输入文本：预训练模型。对于skip-gram则是计算（窗口=2）：P(预</td><td>练)….的概率值。</td></tr></tbody></table><h3 id="2glove词向量">2、<strong>GloVe词向量</strong></h3><p>Glove是一个典型的基于统计的获取词向量的方法，基本思想是：用一个词语周边其他词语出现的次数（或者说两个词共同出现的次数）来表示每一个词语，此时每个词向量的维度等于词库容量，每一维存储着词库对应序号的词语出现在当前词语周围的次数，所有这些词向量组成的矩阵就是共现矩阵。</p><p>比如说（共现矩阵表示的是在文本中两个词同时出现次数，比如说i like两个词出现在一起次数是2）：</p><p><img src="https://s2.loli.net/2025/06/21/ebBMXftyRraizC5.webp" alt="image" width="760" height="557" loading="lazy" decoding="async"/></p><p>不过这样会导致一个文本，矩阵特别稀疏并且矩阵非常大维度高。</p><h2 id="动态词向量预训练模型">动态词向量预训练模型</h2><p><img src="https://s2.loli.net/2025/06/21/hBulgHUGaDCLoPw.webp" alt="image" width="788" height="325" loading="lazy" decoding="async"/></p><p>动态词向量表示为例解决静态词向量无法理解一词多义的现象,不同的数据集中同一个词的表示含义可能是不同的，而且也有可能是该数据集特有的一些含义，虽然静态的词向量可以表现出多义，但是应用在这种情况下可能就显得不太够了，因此，我们需要根据数据集的自身特点来对词向量进行微调，这就是动态词向量。比如说模型ELMo：</p><p><img src="https://s2.loli.net/2025/06/21/lXrTJF4kye1ZQqC.webp" alt="image" width="720" height="322" loading="lazy" decoding="async"/></p><h2 id="补充">补充</h2><p>不过就目前LLM使用的技术而言，大部分模型在处理文本过程中一致的过程是：首先对输入的文本通过 <strong>tokenizer</strong>进行处理（这个就是直接将文本拆分成数字表示），而后在通过 <strong>embedding</strong>进行处理（编码到所需要的维度），而对于 <strong>tokenizer</strong>只需要更具自身文本数据集进行训练或者直接使用预训练好的。</p><pre><code class="language-python">from Qwen.tokenization_qwen import QWenTokenizer
tokenizer_qwen = QWenTokenizer('./Qwen/qwen.tiktoken')

text = "陈准，字道基，颍川郡许昌（今河南许昌）人。西晋官员。"
encoded_input_glm = tokenizer_glm(text, return_tensors='pt')
# 输出
{'input_ids': [100348, 99308, 3837, 18600, 44793, 74046, 3837, 119251, 100410, 106317, 99454, 100763, 9909, 36171, 104185, 99454, 100763, 7552, 17340, 1773, 60686, 100850, 105404, 1773], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

encoded_input_qwen = tokenizer_qwen.encode(text, return_tensors='pt')
# 输出： [[100348, 99308, 3837, 18600, 44793, 74046, 3837, 119251, 100410, 106317, 99454, 100763, 9909, 36171, 104185, 99454, 100763, 7552, 17340, 1773, 60686, 100850, 105404, 1773]]
print(encoded_input_qwen, len(encoded_input_qwen[0]))
embedding_out = nn.Embedding(num_embeddings= tokenizer_qwen.vocab_size, embedding_dim=768)(encoded_input_qwen)
print(embedding_out.shape) # 1 24 768

</code></pre><p><strong>input_ids</strong>: 这个列表包含了输入文本经过分词器（tokenizer）处理后的标记化（tokenized）ID。这些ID是对应每个词或子词的唯一标识符，通常来自于模型的词汇表。每个整数表示文本中的一个分词（token）。<br/><strong>token_type_ids</strong>: 这个列表表示每个token属于哪一部分的输入（例如，句子A或句子B），通常用于处理双句输入（例如问答任务）。对于单一输入文本，token_type_ids一般全为0。如果是双输入任务（如句子对任务），可以通过0和1来区分两个句子。<br/><strong>attention_mask</strong>: 这是一个二进制列表，表示每个token是否应该被模型关注。1表示模型应该关注该位置的token，0表示该位置的token被遮蔽（通常用于填充部分）。这有助于模型知道哪些token是实际数据，哪些是填充的虚拟token。</p><h2 id="参考">参考</h2><p>1、https://code.google.com/archive/p/word2vec/<br/> 2、https://www.big-yellow-j.top/posts/2025/02/03/pos-embedding.html</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/06/TextEmbedding.html</guid></item><item><title>深度学习中各类学习率优化方法(AdaGrad/RMSprop/Adam/Warm-UP)原理及其代码</title><link>https://www.big-yellow-j.top/posts/2025/01/04/learning_rate.html</link><description><![CDATA[学习率是梯度下降、SGD、小批量梯度下降、Adam、RMSprop等优化算法的关键参数，对模型训练速度与稳定性至关重要。其优化过程通过反向传播计算损失函数梯度，结合学习率更新参数，学习率过大会导致模型在最优解附近震荡甚至发散，过小则减慢训练速度或使其陷入局部最优。调整策略包括自适应学习率算法（AdaGrad累积梯度平方动态调整、RMSprop采用梯度平方指数加权移动平均、Adam结合动量与二阶矩估计优化）及warm-up策略（帮助模型适应数据集特征、减少训练震荡、提升效果），实践中可针对Vision-Model和Text-Model等不同模型设置差异化学习率以优化性能。]]></description><content:encoded><![CDATA[<h2 id="学习率基础">学习率基础<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></h2><p><code>学习率（Learning Rate）</code>在优化算法，尤其是梯度下降和其变体中，扮演着至关重要的角色。它影响着模型训练的速度和稳定性，并且是实现模型优化的关键参数之一。</p><blockquote><p>如何理解呢？</p></blockquote><p>在统计学中，线性方程的优化过程通常包括以下步骤：</p><ol><li><strong>构建方程</strong>：定义一个模型，例如线性方程 $y = wx + b$。</li><li><strong>定义损失函数</strong>：选择一个衡量模型预测值与真实值差异的函数，通常是最小二乘法，即最小化误差平方和。</li><li><strong>参数计算</strong>：通过最小二乘法计算方程中的参数 $w$ 和 $b$，使得损失函数最小。</li></ol><p><strong>深度学习中的优化操作</strong><br/> 对于深度学习，这个过程是类似的，但更为复杂：</p><ul><li>假设我们定义的模型为 $f(x)$，其中参数为 $\theta$。</li><li>计算得到的预测值为 $\hat{y}$，真实值为 $y$。</li><li>我们定义损失函数为 $Loss = \sum(\hat{y} - y)^2$（损失函数也可以选择其他的）。</li></ul><p><strong>如何优化损失函数</strong><br/> 那么，如何去优化这个损失函数呢？这里可以考虑以下问题：</p><ul><li><strong>回归问题</strong>：以回归为例，预测值和真实值应该是不断接近的，也就是说，损失值是不断减小的。</li></ul><p><strong>步骤详解</strong></p><ol><li><strong>计算梯度</strong>：首先，我们需要计算损失函数相对于模型参数 $\theta$ 的梯度。这个<strong>梯度告诉我们在参数空间中哪个方向可以使损失函数减少</strong>。在深度学习中，这通常是通过反向传播算法（Backpropagation）来完成的。</li></ol><blockquote><p>这里可以考虑类似小球从山上滚下来，我知道了小球滚到底部的方向（<em>梯度</em>）但是我要小球快速的滚到山底，那么我就可以给他加一个“速度”也就是<strong>学习率</strong>，这样一来优化过程变成：$\theta_{new}=\theta_{old}- \alpha \times \nabla J(\theta_{old})$</p></blockquote><ol><li><strong>选择优化算法</strong>：有了梯度之后，我们需要一个优化算法来更新模型的参数。常用的优化算法包括梯度下降（Gradient Descent）、随机梯度下降（SGD）、小批量梯度下降（Mini-batch Gradient Descent）、Adam、RMSprop 等。这些算法的主要区别在于它们如何处理梯度和更新参数。</li><li><strong>参数更新</strong>：使用优化算法，我们根据梯度和学习率来更新模型的参数。学习率是一个超参数，它决定了每次更新参数时的步长。</li></ol><p><strong>学习率与梯度下降</strong><br/> 学习率在不同类型的梯度下降算法中有不同的应用和解释。最常见的三种梯度下降算法是：</p><ul><li>批量梯度下降（Batch Gradient Descent）</li><li>随机梯度下降（Stochastic Gradient Descent, SGD）</li><li>小批量梯度下降（Mini-batch Gradient Descent）<br/> 在批量梯度下降中，学习率应用于整个数据集，用于计算损失函数的平均梯度。而在随机梯度下降和小批量梯度下降中，学习率应用于单个或一小批样本，用于更新模型参数。</li></ul><p>随机梯度下降和小批量梯度下降由于其高度随机的性质，常常需要一个逐渐衰减的学习率，以帮助模型收敛。</p><p><strong>学习率对模型性能的影响</strong><br/> 选择合适的学习率是非常重要的，因为它会直接影响模型的训练速度和最终性能。具体来说：</p><p><strong>过大的学习率</strong>：可能导致模型在最优解附近震荡，或者在极端情况下导致模型发散。<br/><strong>过小的学习率</strong>：虽然能够保证模型最终收敛，但是会大大降低模型训练的速度。有时，它甚至可能导致模型陷入局部最优解。<br/> 实验表明，不同的模型结构和不同的数据集通常需要不同的学习率设置。因此，实践中常常需要多次尝试和调整，或者使用自适应学习率算法。</p><hr/><h2 id="学习率调整策略">学习率调整策略</h2><h3 id="1自适应学习率">1、自适应学习率</h3><blockquote><p>每个参数的学习率可以根据过去的梯度信息动态调整，而不是使用一个固定的全局学习率</p></blockquote><ul><li><strong>1. AdaGrad</strong></li></ul><p>AdaGrad 是一种自适应学习率的优化算法，它通过累积所有梯度的平方来调整每个参数的学习率。AdaGrad 的更新规则如下：<br/> \(\mathbf{g}_t = \mathbf{g}_{t-1} + \nabla_{\theta} J(\theta_t) \odot \nabla_{\theta} J(\theta_t)\) <br/> \(\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\mathbf{g}_t + \epsilon}} \odot \nabla_{\theta} J(\theta_t)\)</p><p>其中，$\eta$ 是全局学习率，$\epsilon$ 是为了防止除零而添加的一个小常数，$\odot$ 表示元素级的乘法。</p><ul><li><strong>2. RMSprop</strong></li></ul><p>RMSprop 是另一种自适应学习率的优化算法，它使用梯度的指数加权移动平均值来调整每个参数的学习率。RMSprop 的更新规则如下：<br/> \(\mathbf{s}_t = \rho \mathbf{s}_{t-1} + (1 - \rho) \nabla_{\theta} J(\theta_t) \odot \nabla_{\theta} J(\theta_t)\) <br/> \(\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\mathbf{s}_t + \epsilon}} \odot \nabla_{\theta} J(\theta_t)\)</p><p>其中，$\rho$ 是梯度平方的衰减率，$\eta$ 是全局学习率，$\epsilon$ 是为了防止除零而添加的一个小常数。</p><ul><li><strong>3. Adam</strong></li></ul><p>Adam 结合了 Momentum 和 RMSprop 的优点，使用梯度的一阶矩估计和二阶矩估计来动态调整每个参数的学习率。Adam 的更新规则如下：<br/> \(\mathbf{m}_t = \beta_1 \mathbf{m}_{t-1} + (1 - \beta_1) \nabla_{\theta} J(\theta_t)\) <br/> \(\mathbf{v}_t = \beta_2 \mathbf{v}_{t-1} + (1 - \beta_2) \nabla_{\theta} J(\theta_t) \odot \nabla_{\theta} J(\theta_t)\) <br/> \(\hat{\mathbf{m}}_t = \frac{\mathbf{m}_t}{1 - \beta_1^t}\) <br/> \(\hat{\mathbf{v}}_t = \frac{\mathbf{v}_t}{1 - \beta_2^t}\) <br/> \(\theta_{t+1} = \theta_t - \eta \frac{\hat{\mathbf{m}}_t}{\sqrt{\hat{\mathbf{v}}_t + \epsilon}}\)</p><p>其中，$\beta_1$ 和 $\beta_2$ 分别是一阶和二阶矩估计的衰减率，$\eta$ 是全局学习率，$\epsilon$ 是为了防止除零而添加的一个小常数。</p><blockquote><p><strong>动量方法</strong>：深度学习参数的优化不只考虑本次的梯度方向还要去考虑上一次的梯度方向<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup><br/><img src="https://s2.loli.net/2025/06/21/g75bGRLMp4BDyuv.webp" alt="image" width="863" height="517" loading="lazy" decoding="async"/></p><p>初始化动量为0从$\theta_0$到$\theta_1$因为动量为0，那么就直接走梯度方向，从$\theta_1$到$\theta_2$此时就有动量了，不只是走本次的梯度方向（$g^1$）还要考虑上一次的$m^2$因此对两个叠加得到新的方向：$\theta^2$<br/><strong>动量方法如何起作用</strong>：<br/><img src="https://s2.loli.net/2025/06/21/aKdfc9moWwXlJ3s.webp" alt="image" width="859" height="489" loading="lazy" decoding="async"/></p><p>通过考虑前面的梯度避免陷入“鞍点”</p></blockquote><h2 id="学习率调整其他策略">学习率调整其他策略</h2><ul><li><strong>1、<em>warm-up</em></strong></li></ul><p>在深度学习模型训练中，使用较小的学习率进行warm up操作的原理是为了让模型逐渐适应数据集的特征，避免模型在训练开始时出现过拟合或欠拟合的情况。同时，由于较小的学习率可以使得模型的权重更新更加平稳，减少了训练时的震荡和不稳定性，从而提高了模型的训练效果。<br/> 具体来说，warm up 的作用可以分为以下几个方面：</p><ul><li><strong>适应数据集特征</strong>：在训练开始时，模型的权重是随机初始化的，如果直接使用较大的学习率进行训练，模型容易出现过拟合或欠拟合的情况。而使用较小的学习率进行warm up操作，可以让模型逐渐适应数据集的特征，减少过拟合或欠拟合的风险。</li><li><strong>减少训练震荡</strong>：在训练开始时，模型的权重更新可能比较剧烈，导致训练时出现震荡和不稳定性。而使用较小的学习率进行warm up操作，可以使得模型的权重更新更加平稳，减少训练时的震荡和不稳定性。</li><li><strong>提高训练效果</strong>：通过warm up操作，模型可以逐渐适应数据集的特征，减少过拟合或欠拟合的风险，同时减少训练时的震荡和不稳定性，从而提高模型的训练效果。</li></ul><p><strong>实现方法一</strong>：</p><pre><code class="language-python">def decay_lr_poly(base_lr, epoch_i, batch_i, total_epochs, total_batches, warm_up, power=1.0):
    '''
    base_lr: 初始化学习率
    epoch_i, batch_i, total_epochs, total_batches: 第i次epoch/batch;总共的epoch/batch
    warm_up: 训练到多少轮次，才变成开始设定学习率
    '''
    if warm_up &gt; 0 and epoch_i &lt; warm_up:
        rate = (epoch_i * total_batches + batch_i) / (warm_up * total_batches)
    else:
        # 学习率衰减
        rate = np.power(
            1.0 - ((epoch_i - warm_up) * total_batches + batch_i) / ((total_epochs - warm_up) * total_batches),
            power)
    return rate * base_lr
...
batch_lr = decay_lr_poly(conf.lr, epoch, batch_i, conf.epochs, batches, conf.warm_up, conf.power)
for group in optimizer.param_groups:
      group['lr'] = batch_lr
</code></pre><p><strong>实现方法二</strong>：</p><pre><code class="language-python">class GradualWarmupScheduler(_LRScheduler):
    def __init__(self, optimizer, total_epochs, warmup_epochs, last_epoch=-1):
        if not isinstance(optimizer, optim.Optimizer):
            raise TypeError('{} is not an Optimizer'.format(
                type(optimizer).__name__))
        self.total_epochs = total_epochs
        self.warmup_epochs = warmup_epochs
        self.base_lrs = [group['lr'] for group in optimizer.param_groups]
        
        super(GradualWarmupScheduler, self).__init__(optimizer, last_epoch)
        
    def get_lr(self):
        if self.last_epoch &lt; self.warmup_epochs:
            return [(self.base_lrs[i] * self.last_epoch / self.warmup_epochs) 
                    for i in range(len(self.base_lrs))]
        return [self.base_lrs[i] for i in range(len(self.base_lrs))]
</code></pre><p><strong>整个流程代码</strong>：</p><pre><code class="language-python">class LinearModel(nn.Module):
    def __init__(self):
        super(LinearModel, self).__init__()
        self.linear = nn.Linear(10, 1)

    def forward(self, x):
        return self.linear(x)
        
device = 'cuda' if torch.cuda.is_available() else 'cpu'
x = torch.randn(100, 10).to(device)
y = torch.randn(100, 1).to(device)

dataset = TensorDataset(x, y)
dataloader = DataLoader(dataset, batch_size=10, shuffle=True)

model = LinearModel().to(device)
loss_fn = nn.MSELoss()
total_epochs, warmup_epochs, lr = 10, 5, 0.01
optimizer = optim.Adam(model.parameters(), lr=lr)
# 方法2
scheduler = GradualWarmupScheduler(optimizer, total_epochs, warmup_epochs)

for epoch in range(total_epochs):
    model.train()
    for batch_idx, (input, target) in enumerate(dataloader):
        # 方法1
        batch_lr = decay_lr_poly(lr, epoch, batch_idx, total_epochs, len(dataloader), warmup_epochs, 1)
        for group in optimizer.param_groups:
            group['lr'] = batch_lr
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    # scheduler.step()
    # lr = scheduler.get_lr()[0]
    # print(f'Epoch {epoch+1}, Learning Rate: {lr:.6f}, Loss Value: {loss.item()}')
    print(f'Epoch {epoch+1}, Learning Rate: {batch_lr:.6f}, Loss Value: {loss.item()}')
</code></pre><p>争对第一种方式选择的优化器有：<br/> 1、<code>cosine decay schedule</code></p><pre><code class="language-python">progress = (epoch_i - warm_up) * total_batches + batch_i
cosine_decay = 0.5 * (1.0 + np.cos(np.pi * progress / ((total_epochs - warm_up) * total_batches)))
rate = cosine_decay
</code></pre><p>2、<code>Warmup-Stable-Decay</code><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup><br/><img src="https://s2.loli.net/2025/06/21/29cMLnqwbA6rWeK.webp" alt="image" width="470" height="282" loading="lazy" decoding="async"/></p><pre><code class="language-python">def decay_lr_stable(base_lr, epoch_i, batch_i, total_epochs, total_batches, warm_up, stable_epochs=0):
    if warm_up &gt; 0 and epoch_i &lt; warm_up:
        # Warmup阶段
        rate = (epoch_i * total_batches + batch_i) / (warm_up * total_batches)
    elif epoch_i &lt; warm_up + stable_epochs:
        # Stable阶段
        rate = 1.0  # 学习率保持为base_lr
    else:
        # Decay阶段
        progress = (epoch_i - warm_up - stable_epochs) * total_batches + batch_i
        cosine_decay = 0.5 * (1.0 + np.cos(np.pi * progress / ((total_epochs - warm_up - stable_epochs) * total_batches)))
        rate = cosine_decay
    return rate * base_lr
</code></pre><h2 id="实践操作">实践操作</h2><p>有时候需要对不同模型做使用不同学习率（比如说多模态中对于<code>Vision-Model</code>和<code>Text-Model</code>）可以这么操作：</p><pre><code class="language-python">optim.SGD([{'params': model.VisionModel.parameters(), 'lr': 1e-2},{'params': model.TextModel.parameters(), 'lr: 1e-3}], lr=1e-3, momentum=0.9)
</code></pre><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://cloud.tencent.com/developer/article/2351463 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://youtu.be/zzbr1h9sF54?t=1614 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p>https://arxiv.org/abs/2404.06395 <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Sat, 04 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/04/learning_rate.html</guid></item><item><title>通过单/半/混合精度加速训练以及显存优化（gradient-checkpoint）操作</title><link>https://www.big-yellow-j.top/posts/2025/01/01/mixed-precision.html</link><description><![CDATA[不同精度训练包括单精度（FP32）、半精度（FP16/BF16）及混合精度训练。单精度以32位浮点数表示数据，精度高但显存占用大、训练速度慢，适用于小规模任务；半精度（FP16/BF16）显存占用低、速度快，但FP16易因数值问题（如溢出、下溢）导致稳定性差，适用于性能优先的大规模模型；混合精度则平衡性能与稳定，通过同时使用FP16/BF16（计算）和FP32（权重存储更新），精度中高、显存占用较低且训练速度快。混合精度训练解决半精度数值问题的核心方法包括：FP32主权重拷贝（维护精确权重用于更新，FP16用于前向/反向计算）和LOSS SCALING（反向传播前放大loss避免梯度下溢...]]></description><content:encoded><![CDATA[<p>如果熟悉huggingface里面的accelerate库，可以直接使用这个库去实现混合精度训练，详细见：<a href="https://www.big-yellow-j.top/posts/2025/06/25/accelerate-diffusers.html">深入浅出了解生成模型-5：diffuser/accelerate库学习及其数据合成</a>里面的介绍使用。</p><h2 id="不同精度训练">不同精度训练</h2><p><strong>单精度训练</strong>（<code>single-precision</code>）指的是用32位浮点数（FP32）表示所有的参数、激活值和梯度<br/><strong>半精度训练</strong>（<code>half-precision</code>）指的是用16位浮点数（FP16 或 BF16）表示数据。（FP16 是 IEEE 标准，BF16 是一种更适合 AI 计算的变种）<br/><strong>混合精度训练</strong>（<code>mixed-precision</code>）指的是同时使用 FP16/BF16 和 FP32，利用二者的优点。通常，模型权重和梯度使用 FP32，而激活值和中间计算使用 FP16/BF16</p><p><img src="https://s2.loli.net/2025/06/21/ZloTJ2DBX3SWHCE.webp" alt="image" width="945" height="496" loading="lazy" decoding="async"/></p><blockquote><p>Image From: https://www.exxactcorp.com/blog/hpc/what-is-fp64-fp32-fp16</p></blockquote><p>不同精度之间对比：</p><table><thead><tr><th style="text-align: center"><strong>指标</strong></th><th style="text-align: center"><strong>单精度（FP32）</strong></th><th style="text-align: center"><strong>半精度（FP16/BF16）</strong></th><th style="text-align: center"><strong>混合精度</strong></th></tr></thead><tbody><tr><td style="text-align: center"><strong>精度</strong></td><td style="text-align: center">高</td><td style="text-align: center">较低（FP16），中（BF16）</td><td style="text-align: center">中高</td></tr><tr><td style="text-align: center"><strong>显存占用</strong></td><td style="text-align: center">高</td><td style="text-align: center">低</td><td style="text-align: center">较低</td></tr><tr><td style="text-align: center"><strong>训练速度</strong></td><td style="text-align: center">较慢</td><td style="text-align: center">快</td><td style="text-align: center">快</td></tr><tr><td style="text-align: center"><strong>稳定性</strong></td><td style="text-align: center">最佳</td><td style="text-align: center">稳定性低（FP16）</td><td style="text-align: center">稳定</td></tr><tr><td style="text-align: center"><strong>适用场景</strong></td><td style="text-align: center">小规模任务</td><td style="text-align: center">性能优先，大规模模型</td><td style="text-align: center">性能与稳定的平衡</td></tr></tbody></table><p><strong>混合精度训练</strong>（https://arxiv.org/pdf/1710.03740）：</p><blockquote><p><strong>为什么不只用单精度训练（速度快/显存占用少）</strong><br/> 1、直接使用半精度（FP16）容易引发数值问题，如<code>溢出（overflow）</code>和<code>下溢（underflow）</code>：这里是因为<strong>单精度有效尾数（约10位尾数）</strong>较单精度要小得多，那么就会有一个问题因此在训练过程中，如果激活函数的梯度非常小，可能会因<strong>精度不足而被舍弃为零，导致梯度下溢</strong>。此外，当数值超过半精度的表示范围时，也会发生溢出问题。这些限制会使训练难以正常进行，导致模型无法收敛或性能下降；<br/> 2、<strong>舍入误差（Rounding Error）</strong> 舍入误差指的是当梯度过小，小于当前区间内的最小间隔时，该次梯度更新可能会失败，用一张图清晰地表示：</p><p><img src="https://s2.loli.net/2025/06/21/akWAYlZ4THXzBJG.webp" alt="image" width="1409" height="765" loading="lazy" decoding="async"/></p><p>Image: https://zhuanlan.zhihu.com/p/79887894<br/> 总的来说就是：如果只用半精度会导致精度损失严重，因此就会提出用混合精度进行训练</p></blockquote><p>解决上面用单精度造成的问题，在混合精度训练中论文提到的解决办法：</p><ul><li>1、<code>FP32 MASTER COPY OF WEIGHTS</code></li></ul><p>模型权重会同时维护两个版本：1、FP32权重（Master Copy）：以32位浮点数表示，<strong>用于存储和更新权重的精确值</strong>。2、FP16权重（Working Copy）：以16位浮点数表示，用于<strong>前向传播和反向传播的计算，减少显存占用并加速运算</strong>。<br/> 这里就会有一个问题，反向传播过程中要计算梯度，如果（梯度用FP16）<strong>梯度很小</strong>，不也还是会出现溢出问题，作者后续提到<code>LOSS SCALING</code>可以解决这种问题。如果<strong>梯度很大</strong>也会导致溢出问题，梯度计算使用FP16，但在权重更新之前，梯度会转换为 FP32 精度进行累积和存储，从而避免因溢出导致的权重更新错误。另外之所以要用FP32对权重进行保存这是因为，作者研究发现更新 FP16 权重会导致 80% 的相对准确度损失。</p><blockquote><p>we match FP32 training results when updating an FP32 master copy of weights after FP16 forward and backward passes, while updating FP16 weights results in 80% relative accuracy loss</p></blockquote><p>另外一方面，如果拷贝权重，不也等同于把显存的占用拉大了？参考<a href="https://zhuanlan.zhihu.com/p/103685761">知乎</a>上描述显存占用上主要是中间过程值<br/><img src="https://s2.loli.net/2025/06/21/HLfs29UiNaCo48g.webp" alt="image" width="1115" height="541" loading="lazy" decoding="async"/></p><ul><li>2、<code>LOSS SCALING</code></li></ul><p>下图展示了 SSD 模型在训练过程中，激活函数梯度的分布情况，容易发现部分梯度值如果用FP16容易导致最后的梯度值变为0，这样就会导致上面提到的溢出问题，那么论文里面的做法就是：在反向传播前将loss增打$2^k$倍，这样就会保证不发生下溢出（乘一个常数，后面再去除这个常数不影响结果），如何反向传播再去除这个常数即可。</p><p><img src="https://s2.loli.net/2025/06/21/nUQriGIc9HAY1CD.webp" alt="image" width="860" height="550" loading="lazy" decoding="async"/></p><ul><li>3、<code>Apex</code>实现混合精度训练</li></ul><pre><code class="language-cmd">git clone https://github.com/NVIDIA/apex
cd apex
python3 setup.py install
</code></pre><p>分别用<code>Apex</code>和torch原生的<code>amp</code>在<code>MNIST</code>数据集上进行测试（模型：1层卷积+池化+2层全连接层）</p><pre><code class="language-python"># Apex
from apex import amp
...
model, optimizer = amp.initialize(model, optimizer, opt_level="O1", loss_scale="dynamic")
...
with amp.scale_loss(loss, optimizer) as scaled_loss:
            scaled_loss.backward()

# Amp
from torch.cuda.amp import autocast, GradScaler
...
scaler = GradScaler()
...
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

model = CVModel(args= ModelArgs).to(device)
scaler = GradScaler()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
for _ in range(20):
    with autocast():
        out = model(in_data)
        loss = nn.CrossEntropyLoss()(out, labels)
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()
    optimizer.zero_grad()
</code></pre><p><code>Apex</code>中<code>Amp</code>参数（https://nvidia.github.io/apex/amp.html）：</p><p>1、<code>opt_level</code>（<strong>欧1而不是零1</strong>）:<br/><code>O0</code>：纯FP32训练，可以作为accuracy的baseline；<br/><code>O1</code>：混合精度训练（推荐使用），根据黑白名单自动决定使用FP16（GEMM, 卷积）还是FP32（Softmax）进行计算。<br/><code>O2</code>：“几乎FP16”混合精度训练，不存在黑白名单，除了Batch norm，几乎都是用FP16计算。<br/><code>O3</code>：纯FP16训练，很不稳定，但是可以作为speed的baseline；<br/> 2、<code>loss_scale="dynamic"</code><br/> 损失值处理（<code>LOSS SCALING</code>）默认是动态（初始一个较大的值，检查到溢出就减小）<br/><strong>测试效果：</strong><br/><strong>准确率变化上</strong>：<br/> 在公开数据集（<code>CIFAR10</code>）上进行测试（模型为<code>resnet50</code>）测试使用的设备为<code>4090</code><br/><strong>训练集上变化</strong></p><table><thead><tr><th style="text-align: center"><strong>Run</strong></th><th style="text-align: center"><strong>Smoothed</strong></th><th style="text-align: center"><strong>Value</strong></th><th style="text-align: center"><strong>Step</strong></th><th style="text-align: center"><strong>Time</strong></th><th style="text-align: center"><strong>显存占用</strong></th></tr></thead><tbody><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-amp</td><td style="text-align: center">0.8026</td><td style="text-align: center">0.9364</td><td style="text-align: center">11</td><td style="text-align: center">16.99 min</td><td style="text-align: center">15508</td></tr><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-apex</td><td style="text-align: center">0.8093</td><td style="text-align: center">0.9366</td><td style="text-align: center">11</td><td style="text-align: center">16.51 min</td><td style="text-align: center">13166</td></tr><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-fp32</td><td style="text-align: center">0.7946</td><td style="text-align: center">0.9456</td><td style="text-align: center">11</td><td style="text-align: center">22.27 min</td><td style="text-align: center">22818</td></tr></tbody></table><p><strong>测试集上变化</strong></p><table><thead><tr><th style="text-align: center"><strong>Run</strong></th><th style="text-align: center"><strong>Smoothed</strong></th><th style="text-align: center"><strong>Value</strong></th><th style="text-align: center"><strong>Step</strong></th><th style="text-align: center"><strong>Time</strong></th><th style="text-align: center"><strong>显存占用</strong></th></tr></thead><tbody><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-amp</td><td style="text-align: center">0.7302</td><td style="text-align: center">0.8031</td><td style="text-align: center">11</td><td style="text-align: center">16.99 min</td><td style="text-align: center">15508</td></tr><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-apex</td><td style="text-align: center">0.7323</td><td style="text-align: center">0.7956</td><td style="text-align: center">11</td><td style="text-align: center">16.51 min</td><td style="text-align: center">13166</td></tr><tr><td style="text-align: center">scalar-CIFAR10/scalar-256-fp32</td><td style="text-align: center">0.7250</td><td style="text-align: center">0.8092</td><td style="text-align: center">11</td><td style="text-align: center">22.27 min</td><td style="text-align: center">22818</td></tr></tbody></table><p>根据知乎：<a href="https://zhuanlan.zhihu.com/p/79887894">Nicolas</a>和<a href="https://zhuanlan.zhihu.com/p/103685761">Dreaming.O</a>实验建议：</p><ul><li><strong>1、判断你的GPU是否支持FP16：支持的有拥有Tensor Core的GPU（2080Ti、Titan、Tesla等），不支持的（Pascal系列）</strong></li></ul><pre><code class="language-python">import torch

if torch.cuda.is_available():
    device = torch.device("cuda")
    compute_capability = torch.cuda.get_device_capability(device)
    print(f"Compute Capability: {compute_capability[0]}.{compute_capability[1]}")
else:
    print("CUDA is not available.")
</code></pre><p>结果$≥7$说明支持</p><ul><li><strong>2、开启混合精度加速后，Training 对 CPU 的利用率会变得很敏感</strong></li></ul><p>如果训练时候 CPU 大量被占用的话，会导致严重的减速。具体表现在：CPU被大量占用后，GPU-kernel的利用率下降明显。估计是因为混合精度加速有大量的cast操作需要CPU参与，如果CPU拖了后腿，则会导致GPU的利用率也下降。</p><ul><li><strong>3、使用<code>Apex</code>框架会出现</strong>溢出情况</li></ul><p>因为在<code>Apex</code>的<code>amp</code>默认使用的是<code>dynamic</code>可以改为<code>1024</code>或者<code>2048</code></p><h2 id="显存优化">显存优化</h2><p><code>gradient-checkpoint</code>参考：https://www.big-yellow-j.top/posts/2025/01/03/DistributeTraining.html</p><h2 id="参考">参考</h2><p>1、https://arxiv.org/pdf/1710.03740<br/> 2、https://www.exxactcorp.com/blog/hpc/what-is-fp64-fp32-fp16<br/> 3、https://zhuanlan.zhihu.com/p/79887894<br/> 4、https://zhuanlan.zhihu.com/p/84219777<br/> 5、https://nvidia.github.io/apex/amp.html</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/01/mixed-precision.html</guid></item><item><title>深度学习基础理论————常见评价指标以及Loss Function</title><link>https://www.big-yellow-j.top/posts/2025/01/01/evaluation-lossfunction.html</link><description><![CDATA[评价指标涵盖分类任务、机器翻译及模型训练中的损失函数等领域。分类任务常用指标包括准确率、精确率、召回率和F1分数：准确率直观易理解，但在类别不平衡时可能误导评估；精确率衡量预测正类中实际正类的比例，适用于避免假阳性；召回率反映模型对正类样本的识别能力，适用于避免假阴性；F1分数则平衡精确率与召回率，适用于不平衡任务。机器翻译评价指标BLEU基于N-gram匹配规则，通过比较译文与参考译文的n组词相似占比，结合召回率和惩罚因子计算，原理简单且常用第三方库实现。损失函数方面，交叉熵损失用于分类任务，度量预测概率分布与真实标签分布的差异，PyTorch中已集成softmax/sigmoid，支...]]></description><content:encoded><![CDATA[<h2 id="评价指标">评价指标</h2><h3 id="准确率精确率召回率">准确率/精确率/召回率</h3><table><thead><tr><th> </th><th>Positive (预测到的正例)</th><th>Negative (预测到的反例)</th></tr></thead><tbody><tr><td>True (预测结果为真)</td><td>TP</td><td>TN</td></tr><tr><td>False (预测结果为假)</td><td>FP</td><td>FN</td></tr></tbody></table><p>争对正案例的计算：</p><p>1、<strong>准确率</strong>计算方式（ACC）：$Acc= \frac{TP+TN}{TP+TN+FP+FN}$<br/> 2、<strong>精确率</strong>计算方式（Precision）：$\frac{TP}{TP+FP}$<br/> 3、<strong>召回率</strong>计算方式（Recall）：$\frac{TP}{TP+FN}$<br/> 4、<strong>F1</strong>计算方式：$\frac{2\times Precision \times Recall}{Precision+ Recall}$</p><table><thead><tr><th>指标</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td><strong>准确率</strong></td><td>- 直观且易理解</td><td>- 在类别不平衡的情况下可能误导模型评估</td></tr><tr><td><strong>精确率</strong></td><td>- 衡量预测为正类的样本中，实际为正类的比例；适用于避免假阳性</td><td>- 可能忽视召回率，导致漏掉正类样本（假阴性）</td></tr><tr><td><strong>召回率</strong></td><td>- 衡量模型对正类样本的识别能力；适用于避免假阴性</td><td>- 可能导致精确率较低，增加误报（假阳性）</td></tr><tr><td><strong>F1 分数</strong></td><td>- 平衡精确率和召回率，适用于不平衡的任务</td><td>- 不能单独反映精确率或召回率，可能不适用于需要单独关注某一项的场景</td></tr></tbody></table><h3 id="bleu"><code>BLEU</code></h3><p>BLEU 采用一种<code>N-gram</code>的匹配规则，原理比较简单，就是比较译文和参考译文之间n组词的相似的一个占比<br/> 原文：今天天气不错<br/> 机器译文：It is a nice day today<br/> 人工译文：Today is a nice day<br/><code>1-gram</code>:</p><p><img src="https://s2.loli.net/2025/06/22/4GtcvSEy2bBMgrW.webp" alt="image" width="434" height="254" loading="lazy" decoding="async"/></p><p>命中5个词，那么计算得到匹配度为：$5/6$<br/><code>3-gram</code>:</p><p><img src="https://s2.loli.net/2025/06/21/mXOzRurUjsBaAWF.webp" alt="image" width="635" height="282" loading="lazy" decoding="async"/></p><p>计算得到匹配度为：$2/4$</p><p>在通过结合召回率和惩罚因子之后得到<code>BLEU</code>计算公式为：</p> \[BLEU = BP \times exp(\sum_{n=1}^{N}W_nlogP_n)\] <p>使用例子，直接使用第三方库<a href="https://github.com/mjpost/sacrebleu">sacrebleu</a></p><pre><code class="language-python">import sacrebleu
hyps = ['我有一个帽衫', '大大的帽子']
refs = ['你好，我有一个帽衫', '帽子大大的']
bleu = sacrebleu.corpus_bleu(hyps, [refs], tokenize='zh')
print(float(bleu.score))
# 59.809989126151606
</code></pre><h2 id="loss-function"><code>Loss Function</code></h2><h3 id="cross-entropy-loss交叉熵损失"><strong><code>Cross-Entropy Loss</code></strong>(交叉熵损失)</h3><p>交叉熵损失用于分类任务，它度量的是预测概率分布与真实标签分布之间的差异。通常用于多分类问题。交叉熵损失公式（多分类）如下：</p> \[L = -\sum_{i=1}^{N}y_ilog(p_i)\] <p>其中$N$为类别数量，$y_i$真实标签数据，$p_i$模型预测概率。二分类交叉熵损失为：$Loss=−[ylog(p)+(1−y)log(1−p)]$<br/> 在pytorch中对于<a href="https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html">交叉熵损失函数</a>主要参数：</p><ul><li>1、<strong>label_smoothing (float, optional)</strong>：通过平滑标签的方式来避免模型过度自信，提高模型的泛化能力并缓解类别不平衡问题的技术。假设模型有 C 个类别，标签为 y，真实标签的平滑值为 ε，则：对于真实类别 y = 1，标签值变为 1 - ε；对于其他类别 y ≠ 1，标签值变为 ε / (C - 1)</li><li>2、<strong>ignore_index (int, optional)</strong>：<strong>忽略某些特定的标签</strong>，通常用于标记某些数据的特殊情况，如填充（padding）区域、无效标签或其他不需要参与损失计算的标签</li><li>3、<strong>reduction (str, optional)</strong>：’none’、’mean’ 和 ‘sum’分别表示对最后 <strong>不汇总</strong>、<strong>平均值</strong>、<strong>求和</strong></li><li>4、<strong>weight</strong>：相当于在计算损失过程中给每一个标签额外补充一个权重</li></ul><blockquote><p>对于交叉熵损失计算从代码角度出发需要考虑的就是输入数据格式：<br/> InputShape：$(N,C)$ 或者 $(N,C,d_1,…,d_K)$。Target：$(N)$ 或者 $(N,C,d_1,…,d_K)$ 其中 C代表类别，N代表batch_size。从数据格式上可以看出也就是<strong>保证N、C在同一维度即可</strong></p></blockquote><p>⭐<strong>值得注意的是，在pytorch的交叉熵损失里面已经计算了softmax/sigmoid，所以模型输出如果用交叉熵损失函数就不需要用softmax/sigmoid处理</strong></p><h3 id="mean-squared-error均方误差"><strong><code>Mean Squared Error</code></strong>(均方误差)</h3><p>均方误差损失用于回归任务，度量预测值与真实值之间的差异。MSE 计算的是预测值和实际值的平方误差的平均值。MSE 公式：</p> \[L = \frac{1}{N}\sum_{i=1}^{N}(y_i- p_i)^2\] <p>其中$N$为类别数量，$y_i$真实标签数据，$p_i$模型预测概率。<strong>例子</strong>：比如说预测类别（假设为3），模型输出之后通过sigmoid/softmax处理之后得到：</p><table><thead><tr><th>预测</th><th>真实</th></tr></thead><tbody><tr><td>0.3 0.3 0.4</td><td>0 0 1 (A)</td></tr><tr><td>0.3 0.4 0.3</td><td>0 1 0 (B)</td></tr><tr><td>0.1 0.2 0.7</td><td>1 0 0 (C)</td></tr></tbody></table><p><strong>均方误差计算</strong>：$\frac{(0.3-0)^2+(0.3-0)^2+(0.4-1)^2+…}{3}=0.81$<br/><strong>交叉熵计算</strong>：$\frac{-(0\times log0.3+ 0\times log0.3+ 1\times log0.4+ …)}{3}=1.37$</p><h3 id="focal-loss"><strong><code>Focal Loss</code></strong></h3><p><a href="https://arxiv.org/pdf/1708.02002">Focal Loss</a>主要用于处理样本失衡问题（样本里面标签不平衡问题，比如说分类任务标签大部分是类别A只有少部分是标签B），其原理也很简单可以直接在原交叉熵基础上补充一个 <strong>因子</strong>即可。</p> \[FL(p_t)=-\alpha_t(1-p_t)^{\gamma}log(p_t)\] <p><img src="https://s2.loli.net/2025/06/22/IgpT8yhRXoBazOW.webp" alt="image" width="588" height="539" loading="lazy" decoding="async"/><br/> $\gamma$：调节因子，用于控制对易分类样本的惩罚程度。它是一个非负实数，通常设置为大于 0 的值。当$\gamma$&gt;0 时，随着$p_t$的增加，$(1-p_t)^{\gamma}$的值会迅速减小，从而降低易分类样本的损失值。<strong>这样可以使得模型更加关注那些难以分类的样本</strong>。（换言之，如果样本里面大部分都是A那么计算得到 $p_t$也就会越大，那么可以增加 $\gamma$值来减小这部分值对于损失值的影响）<br/> $\alpha$: 平衡因子，用于调整正类和负类之间的权重。它是一个可调参数，通常设置为$\alpha$对于正类和 1−$\alpha$对于负类。当数据集中正负样本数量不均衡时，可以通过调整$\alpha$来<strong>平衡两类样本的贡献</strong>。例如，在一个正负样本比例为 1:9 的数据集中，可以将$\alpha$设置为 0.9，以增加正类样本的权重（<strong>一般而言对于这个参数直接使用我的标签权重即可</strong>）</p><pre><code class="language-python">import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    """Focal Loss implementation."""
    
    def __init__(self, gamma=1.5, alpha=0.25):
        super().__init__()
        self.gamma = gamma
        self.alpha = alpha

    def forward(self, pred, label, mask_labels=None):
        """Calculates focal loss with optional mask_labels."""
        loss = F.binary_cross_entropy_with_logits(pred, label, reduction='none')
        pred_prob = pred.sigmoid()
        p_t = label * pred_prob + (1 - label) * (1 - pred_prob)
        loss *= (1.0 - p_t) ** self.gamma
        
        if self.alpha &gt; 0:
            loss *= label * self.alpha + (1 - label) * (1 - self.alpha)
        
        if mask_labels is not None:
            loss *= mask_labels.float()
            return loss.sum() / mask_labels.sum()
        
        return loss.mean()

if __name__ == '__main__':
    h, w = 500, 500
    labels_parent = torch.randint(0, 2, (h, w), dtype=torch.float32)
    tmp_labels = torch.zeros(1000, 1000)
    tmp_labels[:h, :w] = labels_parent
    tmp_labels_mask = torch.zeros(1000, 1000)
    tmp_labels_mask[:h, :w] = 1 
    pred = torch.randn(1, 1000, 1000)

    focal_loss = FocalLoss()
    loss = focal_loss(pred, tmp_labels.unsqueeze(0), tmp_labels_mask)
    print(loss)

</code></pre><p>对于 <strong>FocalLoss</strong>另外一个改进为：<strong>CB Loss</strong>用于改进样本分布不均衡问题：</p> \[\mathcal{L} = - \frac{1 - \beta}{1 - \beta^{n_y}} \sum (1 - p_y)^\gamma \log(p_y)\] <h3 id="l1-loss"><strong>L1 loss</strong></h3><p><strong>L1 loss</strong>：算预测值与真实值之间的绝对差值来衡量模型的预测误差，公式为：</p> \[L = \frac{1}{N}\sum_{i=1}^{N}|y_i- \hat{y}_i|\] <h3 id="huber-loss"><strong>Huber Loss</strong></h3><p><strong>Huber Loss</strong>用于回归任务的损失函数，它结合了均方误差（MSE）和绝对误差（MAE）的优点，可以减少对异常值（outliers）的敏感性，同时保持较好的梯度性质</p> \[\mathrm{Huber~Loss}= \begin{cases} \frac{1}{2}(y-\hat{y})^2 &amp; \mathrm{if}|y-\hat{y}|\leq\delta \\ \delta*(|y-\hat{y}|-\frac{1}{2}*\delta) &amp; \mathrm{otherwise} &amp; &amp; \end{cases}\] <h2 id="参考">参考</h2><p>1、https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html<br/> 2、https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html#torch.nn.L1Loss<br/> 3、<a href="https://arxiv.org/pdf/1708.02002">Focal Loss for Dense Object Detection</a><br/> 4、https://blog.csdn.net/zhang2010hao/article/details/84559971<br/> 5、https://openaccess.thecvf.com/content_CVPR_2019/papers/Cui_Class-Balanced_Loss_Based_on_Effective_Number_of_Samples_CVPR_2019_paper.pdf</p> ]]></content:encoded><category>深度学习基础理论</category><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2025/01/01/evaluation-lossfunction.html</guid></item><item><title>机器学习基础原理————数据预处理原理及代码</title><link>https://www.big-yellow-j.top/posts/2024/01/06/featureengine.html</link><description><![CDATA[数据和特征决定机器学习上限，特征选择与数据降维是降低数据维度的核心技术。特征选择旨在构建简单可理解模型、提升数据挖掘性能及准备干净数据，常用算法包括依赖学习算法预测性能的Wrapper方法（如SFS、SBS序列选择算法，易面临NP-hard问题）、基于变量排序的Filter方法（如ANOVA、互信息、相关系数、卡方检验、方差选择等）及融合两者的Embedded方法，另有Laplacian Score（通过图构建与拉普拉斯矩阵计算特征重要性）、Fisher Score（监督选取使同类特征相似、异类特征差异大的特征）等方法。数据降维则将高维数据投影到低维空间实现“压缩”（区别于特征筛选的“特...]]></description><content:encoded><![CDATA[<center>数据和特征决定了机器学习的上限，而模型和算法只是逼近这个上限而已</center><h2 id="一特征选择feature-selection">一、特征选择（feature selection）</h2><p><strong>为什么要进行特征选择？</strong></p><pre><code class="language-mermaid">graph LR
B[input]--&gt;A[ML]--&gt;C[output]
</code></pre><p>观察上述流程图，假设$input$的数据特征为：{$t_1…t_n$}我们直接将输入投入到$ML$里面而后得到输出。但是问题来了，我们的特征$t_1…t_n$对于我们的$output$都有作用吗？</p><blockquote><p>例子：定义我们的我们的$ML$为：男孩女孩判断模型，$input$的特征有：长/短头发、有/无喉结、邻居孩子男/女等，$output$为：男孩 or 女孩。</p><p>我相信很容易判断特征：<strong>邻居孩子男/女</strong>在对于我们模型效果上是不起作用的，那么的话在开始$input$时不添加该特征。</p></blockquote><p>回归正题。<strong>特征选择（feature selection）</strong>作为一种数据预处理策略，在各种数据挖掘和机器学习问题准备数据（尤其是高维数据）方面是有效且高效的，<strong>特征选择</strong>的目标包括构建更简单、更易于理解的模型、提高数据挖掘性能以及准备干净、可理解的数据[^1]。</p><p>那么常用特征选择算法是什么呢？</p><hr/><h2 id="11-传统特征选择算法">1.1 传统特征选择算法</h2><h3 id="111-wrapper-method包装法">1.1.1 Wrapper method（包装法）</h3><blockquote><p>Wrapper methods rely on the predictive performance of a predefined learning algorithm to evaluate the quality of selected features.</p><p>wrapper方法依赖<strong>预先定义的学习算法的预测性能</strong>来评估所挑选特征的质量</p></blockquote><p><strong>算法步骤</strong>：</p><pre><code class="language-mermaid">graph LR
A(挑选特征子集)--&gt;B(评估挑选的特征)--&gt;A
</code></pre><blockquote><p>挑选特征子集，评估挑选特征直到合适为止</p></blockquote><p>那么在Wrapper methods中对于特征子集的挑选就显得格外重要了，但是如若特征搜索空间为$d$那么就行搜索次数为$2^d$，也就是说使用Wrapper methods很容易陷入NP-hard problem。两类参见的特征筛选方法：</p><blockquote><table><tbody><tr><td>定义<strong>全部特征</strong>为：$Y={y_1…y_d}$，定义<strong>输出特征</strong>：$X_k={x_j</td><td>j=1,2…k;x_j\in Y}$</td></tr></tbody></table></blockquote><h4 id="1sequential-selection-algorithmssfs3">1、<strong>Sequential selection algorithms</strong>（SFS）[^3]</h4><ul><li>1、SFS</li></ul> \[x^{+}=arg\ max\ J(X_k+x),\ x\in Y- X_k\\ X_k+1=X_k+x^+\\ k=k+1\] <p>在空的特征子集中添加特征$x$使得目标函数$J$最大</p><ul><li>2、Sequential Backward Selection（SBS）</li></ul> \[x^-=arg\ max\ J(X_k-x),\ x\in X_k\\ X_{k-1}=X_k-x^-\\ k=k-1\\\] <p>与SFS相类似，只不过SBS是从完整的特征开始，而后逐渐减少特征。</p><ul><li>3、Sequential Floating Forward Selection （SFFS）</li></ul><p>2、<strong>Heuristic Search Algorithms</strong></p><p>算法步骤：首先初始化：$X_0=\phi$，$k=0$<br/> step1:</p> \[x^{+}=arg\ max\ J(X_k+x),\ x\in Y- X_k\\ X_k+1=X_k+x^+\\ k=k+1\\ go \ to \ step2\] <p>在空的特征子集中添加特征$x$使得目标函数$J$最大<br/> step2:</p> \[x^-=arg\ max\ J(X_k-x),\ x\in X_k\\ if\ J(X_k-x)&gt;J(X_k)\\ X_{k-1}=X_k-x^-\\ k=k-1\\ go\ to\ step1\] <p>SFFS较SFS多一个回溯操作（step2）<br/> 循环step1和step2只到达到目标$k$值停止。</p><pre><code class="language-python">!pip install mlxtend 
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
sfs = SFS(knn, #选择模型
          k_features=3, #选择筛选出的特征 
          forward=True, 
          floating=False, 
          scoring='accuracy', #评价标准
          cv=4, #交叉验证
          n_jobs=-1 #用于并行评估不同特征子集的CPU数量，-1代表全部)
sfs = sfs.fit(X, y)
print(sbfs.k_feature_idx_) #返回选择的特征的序号 k_feature_names_返回特征名称
print(sbfs.k_score_) #返回评分
"""
forward与floating参数选择：
1、SFS算法True False
2、SBS算法False False
3、SFFS算法 True True
"""
</code></pre><h4 id="2heuristic-search-algorithms-启发式搜索算法">2、Heuristic search algorithms 启发式搜索算法</h4><p>Genetic Algorithm（GA）遗传算法</p><h4 id="wrapper-methods缺点">Wrapper methods缺点</h4><p>1、计算代价高<br/> 2、容易过拟合</p><h3 id="112-filter-methods过滤法">1.1.2 Filter methods（过滤法）</h3><blockquote><p>Filter methods use variable ranking techniques as the principle criteria for variable selection by ordering<br/> Filter methods使用变量<strong>排序方法</strong>作为按顺序选择变量的原则标准</p></blockquote><p>也就是说Fileter methods首先通过<em>排序</em>得到特征的相关性，而后进行筛选[^2] !<br/><strong>算法步骤</strong>：</p><pre><code class="language-mermaid">graph LR
A[特征重要性进行排序]--&gt;B[将低特征滤出]
</code></pre><p><strong>排序方法（ranking methods）</strong></p><p>特征重要性计算有：<br/> 1、方差分析（ANOVA）：基于统计方法来计算特征与目标变量之间的相关程度，通过计算特征的F值来评估特征的重要性。<br/><strong>2、互信息（Mutual Information）</strong>：计算特征与目标变量之间的<strong>信息增益</strong>或互信息，衡量特征与目标变量之间的依赖关系。</p><blockquote><p>如：通过信息增益进行评价</p><p>首先计算香农熵：<br/> \(H(Y)=-\sum p(y)log(p(y))\)</p><p>条件熵计算：<br/> \(H(Y|X)=-\sum_{x} \sum_{y}p(x,y)log(p(y|x))\)</p><p>而后计算：<br/> \(I(Y,X)=H(Y)-H(Y|X)\)<br/> 通过计算（3）就可以得到：如果$X$与$Y$之间相互独立那么将会趋近0</p></blockquote><p><strong>3、相关系数（Correlation Coefficient）</strong>：计算特征和目标变量之间的线性相关性，常用的方法有Pearson相关系数和Spearman相关系数。</p><blockquote> \[R(i)=\frac{cov(x_i,Y)}{\sqrt{var(x_i)*var(Y)}}\] <p>其中$Y$作为输出变量（output）而$x_i$则为我们的input</p></blockquote><p>4、卡方检验（Chi-square Test）：针对分类问题，对特征和目标变量之间的关联程度进行检验，判断特征的重要性。</p><p>5、方差选择（Variance Threshold）：通过计算特征的方差来判断其重要性，方差较低的特征可能对目标变量的预测作用较小</p><pre><code class="language-python">from sklearn.feature_selection import VarianceThreshold
X = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], [0, 1, 1]]
sel = VarianceThreshold(threshold=(.8 * (1 - .8))) #threshold指定特征选择的方法
sel.fit_transform(X)
"""
array([[0, 1],
       [1, 0],
       [0, 0],
       [1, 1],
       [1, 0],
       [1, 1]])
""""
</code></pre><blockquote><p>参数说明：</p><p><a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.VarianceThreshold.html#sklearn.feature_selection.VarianceThreshold">sklearn-filter methods</a></p></blockquote><h3 id="113-embedded-methods嵌入法">1.1.3 Embedded methods（嵌入法）</h3><blockquote><p>Embedded methods is a tradeoff between filter and wrapper methods that embed the feature selection into model learning.</p><p>Embedded方法将Wrapped和filter方法进行权衡，将特征选择嵌入到模型学习中去</p></blockquote><h2 id="12-其它特征选择方法">1.2 其它特征选择方法</h2><blockquote><p>基于信息论：一般是指通过计算信息增益等来表示特征重要性</p><p>基于相似：主要通过计算特征体系中的各特征之间的“相似”。比如无监督特征选择：可以通过计算各自特征样本之间距离（距离近则相似）；而对于监督特征选择一般则是通过标签信息得到（比如可以计算相关矩阵等）</p></blockquote><h3 id="121-laplacian-score-拉普拉斯分数5">1.2.1 Laplacian Score 拉普拉斯分数[^5]</h3><blockquote><p>HE X, CAI D, NIYOGI P. Laplacian Score for Feature Selection[C/OL]//Advances in Neural Information Processing Systems: 卷 18. MIT Press, 2005[2023-07-07].</p></blockquote><p><strong>原文描述如下</strong>：</p><p><img src="https://s2.loli.net/2025/06/21/6laiB2YT1DkyRGE.webp" alt="image" width="814" height="541" loading="lazy" decoding="async"/></p><p><strong>解释如下</strong>：</p><blockquote><p>$f_{ri}$代表第$r$个特征的第$i$个样本；</p></blockquote><ol><li><strong>构建图(Graph)</strong>：首先，需要构建一个图$G$，其中节点（$m$个节点）代表样本数据的特征，边代表特征之间的相似度或关联关系。常用的方法是使用<strong>K近邻</strong>来构建图，即对每个特征找到K个最近的邻居，并将它们连接起来。</li><li><table><tbody><tr><td>计算<strong>亲和矩阵(Affinity Matrix)</strong>：在构建了图之后，需要计算特征之间的相似度或关联性。<strong>节点</strong>之间彼此连接时通过使用：$S(i,j)=e^{-\frac{</td><td> </td><td>x_i-x_j</td><td> </td><td>^2}{t}}$，其中$x_i$为$x_j$的$p$个最相邻的节点，$t$为常数。如果节点之间不相互连接那么$S(i,j)=0$</td></tr></tbody></table></li></ol><blockquote><p>$S_{ij}$较大，说明节点比较“接近”</p></blockquote><ol><li>计算<strong>拉普拉斯矩阵(Laplacian Matrix)</strong>：首先定义<strong>对角矩阵D（diagonal matrix D</strong>）:$D(i,j)=\sum_{j=1}^{n}S(i,j)$。那么拉普拉斯矩阵为：$L=D-S$。</li><li>计算每个特征的<strong>拉普拉斯分数（Laplacian Score）</strong>：</li></ol> \[L_r=\frac{\widetilde{f}_{r}^{T}L\widetilde{f_r}}{\widetilde{f}_{r}^{T}D\widetilde{f}_r}\] <p>其中：$\widetilde{f_r}=f_r-\frac{f_r^TD1}{1^TD1}1$、$1=[1,…,1]^T$、$f_r=[f_{r1},…,f_{rm}]^T$、$D=diag(S1)$</p><ol><li>选择k个特征的任务可以通过选择具有<strong>最小拉普拉斯分数</strong>的前k个特征来解决<br/><strong>代码如下</strong>[^6]：</li></ol><details><ummary>详细代码&lt;/summary&gt; ```python import numpy as np from scipy.sparse import * from sklearn.metrics.pairwise import pairwise_distances def lap_score(X, **kwargs): """ This function implements the laplacian score feature selection, steps are as follows: 1. Construct the affinity matrix W if it is not specified 2. For the r-th feature, we define fr = X(:,r), D = diag(W*ones), ones = [1,...,1]', L = D - W 3. Let fr_hat = fr - (fr'*D*ones)*ones/(ones'*D*ones) 4. Laplacian score for the r-th feature is score = (fr_hat'*L*fr_hat)/(fr_hat'*D*fr_hat) Input ----- X: {numpy array}, shape (n_samples, n_features) input data kwargs: {dictionary} W: {sparse matrix}, shape (n_samples, n_samples) input affinity matrix Output ------ score: {numpy array}, shape (n_features,) laplacian score for each feature Reference --------- He, Xiaofei et al. "Laplacian Score for Feature Selection." NIPS 2005. """ # if 'W' is not specified, use the default W if 'W' not in kwargs.keys(): W = construct_W(X) # construct the affinity matrix W W = kwargs['W'] # build the diagonal D matrix from affinity matrix W D = np.array(W.sum(axis=1)) L = W tmp = np.dot(np.transpose(D), X) D = diags(np.transpose(D), [0]) Xt = np.transpose(X) t1 = np.transpose(np.dot(Xt, D.todense())) t2 = np.transpose(np.dot(Xt, L.todense())) # compute the numerator of Lr D_prime = np.sum(np.multiply(t1, X), 0) - np.multiply(tmp, tmp)/D.sum() # compute the denominator of Lr L_prime = np.sum(np.multiply(t2, X), 0) - np.multiply(tmp, tmp)/D.sum() # avoid the denominator of Lr to be 0 D_prime[D_prime &lt; 1e-12] = 10000 # compute laplacian score for all features score = 1 - np.array(np.multiply(L_prime, 1/D_prime))[0, :] return np.transpose(score) def feature_ranking(score): """ Rank features in ascending order according to their laplacian scores, the smaller the laplacian score is, the more important the feature is """ idx = np.argsort(score, 0) return idx def construct_W(X, **kwargs): """ Construct the affinity matrix W through different ways Notes ----- if kwargs is null, use the default parameter settings; if kwargs is not null, construct the affinity matrix according to parameters in kwargs Input ----- X: {numpy array}, shape (n_samples, n_features) input data kwargs: {dictionary} parameters to construct different affinity matrix W: y: {numpy array}, shape (n_samples, 1) the true label information needed under the 'supervised' neighbor mode metric: {string} choices for different distance measures 'euclidean' - use euclidean distance 'cosine' - use cosine distance (default) neighbor_mode: {string} indicates how to construct the graph 'knn' - put an edge between two nodes if and only if they are among the k nearest neighbors of each other (default) 'supervised' - put an edge between two nodes if they belong to same class and they are among the k nearest neighbors of each other weight_mode: {string} indicates how to assign weights for each edge in the graph 'binary' - 0-1 weighting, every edge receives weight of 1 (default) 'heat_kernel' - if nodes i and j are connected, put weight W_ij = exp(-norm(x_i - x_j)/2t^2) this weight mode can only be used under 'euclidean' metric and you are required to provide the parameter t 'cosine' - if nodes i and j are connected, put weight cosine(x_i,x_j). this weight mode can only be used under 'cosine' metric k: {int} choices for the number of neighbors (default k = 5) t: {float} parameter for the 'heat_kernel' weight_mode fisher_score: {boolean} indicates whether to build the affinity matrix in a fisher score way, in which W_ij = 1/n_l if yi = yj = l; otherwise W_ij = 0 (default fisher_score = false) reliefF: {boolean} indicates whether to build the affinity matrix in a reliefF way, NH(x) and NM(x,y) denotes a set of k nearest points to x with the same class as x, and a different class (the class y), respectively. W_ij = 1 if i = j; W_ij = 1/k if x_j \in NH(x_i); W_ij = -1/(c-1)k if x_j \in NM(x_i, y) (default reliefF = false) Output ------ W: {sparse matrix}, shape (n_samples, n_samples) output affinity matrix W """ # default metric is 'cosine' if 'metric' not in kwargs.keys(): kwargs['metric'] = 'cosine' # default neighbor mode is 'knn' and default neighbor size is 5 if 'neighbor_mode' not in kwargs.keys(): kwargs['neighbor_mode'] = 'knn' if kwargs['neighbor_mode'] == 'knn' and 'k' not in kwargs.keys(): kwargs['k'] = 5 if kwargs['neighbor_mode'] == 'supervised' and 'k' not in kwargs.keys(): kwargs['k'] = 5 if kwargs['neighbor_mode'] == 'supervised' and 'y' not in kwargs.keys(): print ('Warning: label is required in the supervised neighborMode!!!') exit(0) # default weight mode is 'binary', default t in heat kernel mode is 1 if 'weight_mode' not in kwargs.keys(): kwargs['weight_mode'] = 'binary' if kwargs['weight_mode'] == 'heat_kernel': if kwargs['metric'] != 'euclidean': kwargs['metric'] = 'euclidean' if 't' not in kwargs.keys(): kwargs['t'] = 1 elif kwargs['weight_mode'] == 'cosine': if kwargs['metric'] != 'cosine': kwargs['metric'] = 'cosine' # default fisher_score and reliefF mode are 'false' if 'fisher_score' not in kwargs.keys(): kwargs['fisher_score'] = False if 'reliefF' not in kwargs.keys(): kwargs['reliefF'] = False n_samples, n_features = np.shape(X) # choose 'knn' neighbor mode if kwargs['neighbor_mode'] == 'knn': k = kwargs['k'] if kwargs['weight_mode'] == 'binary': if kwargs['metric'] == 'euclidean': # compute pairwise euclidean distances D = pairwise_distances(X) D **= 2 # sort the distance matrix D in ascending order dump = np.sort(D, axis=1) idx = np.argsort(D, axis=1) # choose the k-nearest neighbors for each instance idx_new = idx[:, 0:k+1] G = np.zeros((n_samples*(k+1), 3)) G[:, 0] = np.tile(np.arange(n_samples), (k+1, 1)).reshape(-1) G[:, 1] = np.ravel(idx_new, order='F') G[:, 2] = 1 # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W elif kwargs['metric'] == 'cosine': # normalize the data first X_normalized = np.power(np.sum(X*X, axis=1), 0.5) for i in range(n_samples): X[i, :] = X[i, :]/max(1e-12, X_normalized[i]) # compute pairwise cosine distances D_cosine = np.dot(X, np.transpose(X)) # sort the distance matrix D in descending order dump = np.sort(-D_cosine, axis=1) idx = np.argsort(-D_cosine, axis=1) idx_new = idx[:, 0:k+1] G = np.zeros((n_samples*(k+1), 3)) G[:, 0] = np.tile(np.arange(n_samples), (k+1, 1)).reshape(-1) G[:, 1] = np.ravel(idx_new, order='F') G[:, 2] = 1 # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W elif kwargs['weight_mode'] == 'heat_kernel': t = kwargs['t'] # compute pairwise euclidean distances D = pairwise_distances(X) D **= 2 # sort the distance matrix D in ascending order dump = np.sort(D, axis=1) idx = np.argsort(D, axis=1) idx_new = idx[:, 0:k+1] dump_new = dump[:, 0:k+1] # compute the pairwise heat kernel distances dump_heat_kernel = np.exp(-dump_new/(2*t*t)) G = np.zeros((n_samples*(k+1), 3)) G[:, 0] = np.tile(np.arange(n_samples), (k+1, 1)).reshape(-1) G[:, 1] = np.ravel(idx_new, order='F') G[:, 2] = np.ravel(dump_heat_kernel, order='F') # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W elif kwargs['weight_mode'] == 'cosine': # normalize the data first X_normalized = np.power(np.sum(X*X, axis=1), 0.5) for i in range(n_samples): X[i, :] = X[i, :]/max(1e-12, X_normalized[i]) # compute pairwise cosine distances D_cosine = np.dot(X, np.transpose(X)) # sort the distance matrix D in ascending order dump = np.sort(-D_cosine, axis=1) idx = np.argsort(-D_cosine, axis=1) idx_new = idx[:, 0:k+1] dump_new = -dump[:, 0:k+1] G = np.zeros((n_samples*(k+1), 3)) G[:, 0] = np.tile(np.arange(n_samples), (k+1, 1)).reshape(-1) G[:, 1] = np.ravel(idx_new, order='F') G[:, 2] = np.ravel(dump_new, order='F') # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W # choose supervised neighborMode elif kwargs['neighbor_mode'] == 'supervised': k = kwargs['k'] # get true labels and the number of classes y = kwargs['y'] label = np.unique(y) n_classes = np.unique(y).size # construct the weight matrix W in a fisherScore way, W_ij = 1/n_l if yi = yj = l, otherwise W_ij = 0 if kwargs['fisher_score'] is True: W = lil_matrix((n_samples, n_samples)) for i in range(n_classes): class_idx = (y == label[i]) class_idx_all = (class_idx[:, np.newaxis] &amp; class_idx[np.newaxis, :]) W[class_idx_all] = 1.0/np.sum(np.sum(class_idx)) return W # construct the weight matrix W in a reliefF way, NH(x) and NM(x,y) denotes a set of k nearest # points to x with the same class as x, a different class (the class y), respectively. W_ij = 1 if i = j; # W_ij = 1/k if x_j \in NH(x_i); W_ij = -1/(c-1)k if x_j \in NM(x_i, y) if kwargs['reliefF'] is True: # when xj in NH(xi) G = np.zeros((n_samples*(k+1), 3)) id_now = 0 for i in range(n_classes): class_idx = np.column_stack(np.where(y == label[i]))[:, 0] D = pairwise_distances(X[class_idx, :]) D **= 2 idx = np.argsort(D, axis=1) idx_new = idx[:, 0:k+1] n_smp_class = (class_idx[idx_new[:]]).size if len(class_idx) &lt;= k: k = len(class_idx) - 1 G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx, (k+1, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = 1.0/k id_now += n_smp_class W1 = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) # when i = j, W_ij = 1 for i in range(n_samples): W1[i, i] = 1 # when x_j in NM(x_i, y) G = np.zeros((n_samples*k*(n_classes - 1), 3)) id_now = 0 for i in range(n_classes): class_idx1 = np.column_stack(np.where(y == label[i]))[:, 0] X1 = X[class_idx1, :] for j in range(n_classes): if label[j] != label[i]: class_idx2 = np.column_stack(np.where(y == label[j]))[:, 0] X2 = X[class_idx2, :] D = pairwise_distances(X1, X2) idx = np.argsort(D, axis=1) idx_new = idx[:, 0:k] n_smp_class = len(class_idx1)*k G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx1, (k, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx2[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = -1.0/((n_classes-1)*k) id_now += n_smp_class W2 = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W2) &gt; W2 W2 = W2 - W2.multiply(bigger) + np.transpose(W2).multiply(bigger) W = W1 + W2 return W if kwargs['weight_mode'] == 'binary': if kwargs['metric'] == 'euclidean': G = np.zeros((n_samples*(k+1), 3)) id_now = 0 for i in range(n_classes): class_idx = np.column_stack(np.where(y == label[i]))[:, 0] # compute pairwise euclidean distances for instances in class i D = pairwise_distances(X[class_idx, :]) D **= 2 # sort the distance matrix D in ascending order for instances in class i idx = np.argsort(D, axis=1) idx_new = idx[:, 0:k+1] n_smp_class = len(class_idx)*(k+1) G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx, (k+1, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = 1 id_now += n_smp_class # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W if kwargs['metric'] == 'cosine': # normalize the data first X_normalized = np.power(np.sum(X*X, axis=1), 0.5) for i in range(n_samples): X[i, :] = X[i, :]/max(1e-12, X_normalized[i]) G = np.zeros((n_samples*(k+1), 3)) id_now = 0 for i in range(n_classes): class_idx = np.column_stack(np.where(y == label[i]))[:, 0] # compute pairwise cosine distances for instances in class i D_cosine = np.dot(X[class_idx, :], np.transpose(X[class_idx, :])) # sort the distance matrix D in descending order for instances in class i idx = np.argsort(-D_cosine, axis=1) idx_new = idx[:, 0:k+1] n_smp_class = len(class_idx)*(k+1) G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx, (k+1, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = 1 id_now += n_smp_class # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W elif kwargs['weight_mode'] == 'heat_kernel': G = np.zeros((n_samples*(k+1), 3)) id_now = 0 for i in range(n_classes): class_idx = np.column_stack(np.where(y == label[i]))[:, 0] # compute pairwise cosine distances for instances in class i D = pairwise_distances(X[class_idx, :]) D **= 2 # sort the distance matrix D in ascending order for instances in class i dump = np.sort(D, axis=1) idx = np.argsort(D, axis=1) idx_new = idx[:, 0:k+1] dump_new = dump[:, 0:k+1] t = kwargs['t'] # compute pairwise heat kernel distances for instances in class i dump_heat_kernel = np.exp(-dump_new/(2*t*t)) n_smp_class = len(class_idx)*(k+1) G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx, (k+1, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = np.ravel(dump_heat_kernel, order='F') id_now += n_smp_class # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W elif kwargs['weight_mode'] == 'cosine': # normalize the data first X_normalized = np.power(np.sum(X*X, axis=1), 0.5) for i in range(n_samples): X[i, :] = X[i, :]/max(1e-12, X_normalized[i]) G = np.zeros((n_samples*(k+1), 3)) id_now = 0 for i in range(n_classes): class_idx = np.column_stack(np.where(y == label[i]))[:, 0] # compute pairwise cosine distances for instances in class i D_cosine = np.dot(X[class_idx, :], np.transpose(X[class_idx, :])) # sort the distance matrix D in descending order for instances in class i dump = np.sort(-D_cosine, axis=1) idx = np.argsort(-D_cosine, axis=1) idx_new = idx[:, 0:k+1] dump_new = -dump[:, 0:k+1] n_smp_class = len(class_idx)*(k+1) G[id_now:n_smp_class+id_now, 0] = np.tile(class_idx, (k+1, 1)).reshape(-1) G[id_now:n_smp_class+id_now, 1] = np.ravel(class_idx[idx_new[:]], order='F') G[id_now:n_smp_class+id_now, 2] = np.ravel(dump_new, order='F') id_now += n_smp_class # build the sparse affinity matrix W W = csc_matrix((G[:, 2], (G[:, 0], G[:, 1])), shape=(n_samples, n_samples)) bigger = np.transpose(W) &gt; W W = W - W.multiply(bigger) + np.transpose(W).multiply(bigger) return W import pandas as pd from sklearn.model_selection import train_test_split #数据集划分 data = pd.read_excel(r'path') data = data.drop(data[data['PM10']=='—'].index) #去除缺失数据 data.head() x = data.iloc[:,7:] y = data['AQI'] X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=40) kwargs_W = {"metric":"euclidean","neighbor_mode":"knn","weight_mode":"heat_kernel","k":5,'t':1} W = construct_W(x, **kwargs_W) score = lap_score(x.values, W=W) idx = feature_ranking(score) score, idx ``` &lt;/details&gt; 代码运行结果： ![image](https://s2.loli.net/2023/07/07/8SjH1DRrckdMVAO.png" alt="202307071609030) ### 1.2.2 Fisher Score Fisher Score是一种监督特征选取算法，通过挑选特征使得同一样本中特征值相似，不同类别特征值不相似。计算公式如下： $$ f_i=\frac{\sum_{j=1}^{c}n_j(\mu_{ij}-\mu_i)^2}{\sum_{j=1}^{c}n_j\sigma_{ij}^{2}} $$ 其中$n_j$代表类别$j$的数量、$\mu_{ij}$代表$j$类样本特征$f_i$的平均值、$\mu_j$代笔特征$f_i$的平均值 ## 1.3 总结 <img src="https://s2.loli.net/2025/06/21/3uAqnByOUWLYtoS.webp" alt="image" width="1359" height="794" loading="lazy" decoding="async"/><img src="https://s2.loli.net/2025/06/21/l1yvIwgqQAZki4U.webp" alt="image" width="1333" height="611" loading="lazy" decoding="async"/> ## 二、数据降维 为什么要进行数据降维度?数据维度又是什么? &gt; 数据降维:将高维空间数据投影到低维空间,目的在于将数据的特征维度降低,作用和特征筛选的作用相类似但是两种之间有一定区别(两者都属于**降低数据维度方法**): &gt; 数据降维和特征筛选:两者都是对数据的维度进行减少,但是特征筛选:侧重在于从$D$维特征从选择$d$维特征,数据特征个数发生减少;数据降维:从高维向低维投影;通俗易懂描述为:**前者为阉割**(数量变化),**后者为压缩**("形状"变化) 数据降维分类[^4]： <img src="https://s2.loli.net/2025/06/21/Hd3rX4NawJkUMAt.webp" alt="image" width="1174" height="432" loading="lazy" decoding="async"/> ### 2.1 Principal Components Analysis（PCA）主成分分析 PCA是一种**线性降维技术**，即通过将数据嵌入到较低维的线性子空间中来进行降维。 &gt;PCA通过寻找低维数据对高维数据进行描述 数学原理如下：对于原始数据$X$通过找到**映射**$M$将原始数据从较高维度的$D$维转化为较低维度的$d$维即:$Y=XM$($Y$代表转化后的低维数据),那么问题关键在于对于**映射**$M$的寻找. PCA算法流程: 1、对输入数据$X=\{x_1...x_n\}$进行中心化:$x_i=x_i- \frac{1}{m}\sum_{j=1}^{n}x_j $ 2、对中心化的数据计算协方差矩阵:$X^TX$ 3、计算协方差矩阵特征值,并且得到最大的$n^{'}$($n^{'}$代表要降低到的维度)个特征值所对应特征向量:$M=(w_1..w_{n^{'}})$ 4、对样本进行转换:$Y=XM$ ```python from sklearn.decomposition import PCA x = np.random.randn(10,5) pca = PCA(3) pca.fit(x) pca.transform(x) #转化 pca.explained_variance_ratio_ #贡献 ``` ## 参考文献 [1] LI J, CHENG K, WANG S, 等. Feature Selection:A Data Perspective[J/OL]. ACM Computing Surveys, 2018, 50(6): 1-45. https://doi.org/10.1145/3136625. [2]CHANDRASHEKAR G, SAHIN F. A survey on feature selection methods[J/OL]. Computers &amp; Electrical Engineering, 2014, 40(1): 16-28. DOI:[10.1016/j.compeleceng.2013.11.024](https://doi.org/10.1016/j.compeleceng.2013.11.024). [3]https://rasbt.github.io/mlxtend/user_guide/feature_selection/SequentialFeatureSelector/ [4]Van Der Maaten, Laurens, Eric Postma, and Jaap Van den Herik."Dimensionality reduction:a comparative." *J Mach Learn Res* 10.66-71 (2009). [5]HE X, CAI D, NIYOGI P.Laplacian Score for Feature Selection[C/OL]//Advances in Neural Information Processing Systems: 卷 18. MIT Press, 2005[2023-07-07]. ## 推荐阅读 ⭐⭐⭐LI J, CHENG K, WANG S, 等. Feature Selection: A Data Perspective[J/OL]. ACM Computing Surveys, 2018, 50(6): 1-45. https://doi.org/10.1145/3136625. ⭐⭐Van Der Maaten, Laurens, Eric Postma, and Jaap Van den Herik."Dimensionality reduction:a comparative." *J Mach Learn Res* 10.66-71 (2009). [^1]:https://doi.org/10.1145/3136625 [^2]:[10.1016/j.compeleceng.2013.11.024](https://doi.org/10.1016/j.compeleceng.2013.11.024) [^3]:https://rasbt.github.io/mlxtend/user_guide/feature_selection/SequentialFeatureSelector/ [^4]:https://members.loria.fr/moberger/Enseignement/AVR/Exposes/TR_Dimensiereductie.pdf [^5]:https://proceedings.neurips.cc/paper_files/paper/2005/file/b5b03f06271f8917685d14cea7c6c50a-Paper.pdf [^6]:https://github.com/jundongl/scikit-feature </ummary></details> ]]></content:encoded><category>机器学习</category><pubDate>Sat, 06 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/06/featureengine.html</guid></item><item><title>机器学习基础原理————集成学习算法</title><link>https://www.big-yellow-j.top/posts/2024/01/05/Ensemble-Learning.html</link><description><![CDATA[集成学习通过组合多个弱学习机（如决策树、人工神经网络等）提升预测效果，在统计、计算及表示上较单一弱学习机有显著改善。Bagging和Boosting是两种主要集成方法。Bagging中，Random Forest以决策树为弱学习机，常用ID3（基于香农熵分裂）、C4.5（基于信息增益比）、CART（基于Gini不纯度）等决策树算法，通过bootstrap sampling采样构建多棵树，可提高准确率（尤其随机特征选取时）并评估泛化误差、强度及相关性。Boosting则不同，每棵树基于先前树的信息构建，与Bagging的独立采样构建不同。Boosting代表算法包括GBDT和XGBoost...]]></description><content:encoded><![CDATA[<p>传统机器学习算法 (例如：决策树，人工神经网络，支持向量机，朴素贝叶斯等) 都是通过<strong>弱学习机</strong>（weak learners）来对目标进行预测（分类）。但是，以决策树算法为例，决策树算法在递归过程中，可能会过度分割样本空间，最终导致过拟合。集成学习 (Ensemble Learning) 算法的基本思想就是将多个弱学习机组合，从而实现一个预测效果更好的集成学习机<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。集成学习在<strong>统计（Statistical）</strong>、 <strong>计算（computational）</strong> 以及 <strong>表示（representation）</strong> 上相较之弱学习机有较大改善<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>。<code>Bagging</code>和<code>Boosting</code>对比如下：</p><p><img src="https://s2.loli.net/2025/06/21/KHySl63Y7dra8PC.webp" alt="image" width="876" height="762" loading="lazy" decoding="async"/></p><blockquote><p><strong>红色线条</strong>代表训练过程；<strong>绿色线条</strong>代表<code>Boosting</code>更新权重得到的权重训练集；<strong>蓝色线条</strong>代表结合策略；<strong>中间蓝色方块</strong>代表得到的训练集（<code>Bagging</code>通过随机采样，<code>Boosting</code>则是更新权重得到训练集）</p></blockquote><h1 id="1-bagging">1 Bagging</h1><p><code>Bagging</code>方法是一种通过生成多组预测值，然后对这些预测值进行“聚合”的一种方法<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>。<code>Bagging</code>的算法思路为<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>：</p><ul><li>1、每次采用有放回的抽样从训练集中取出$n$个训练样本组成新的训练集</li><li>2、对得到的新的训练集，通过<strong>模型</strong>进行训练得到$M$个子模型:${h_1,…,h_M}$</li><li>3、对于不同的任务所采用的“聚合”方法不同：对于回归任务则是直接对每一个子模型得到的训练结果直接进行平均。而对于分类任务则是对不同子模型得到的结果进行投票。</li></ul><h2 id="12-random-forest">1.2 Random Forest</h2><p><code>Random Forest</code><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>是一种利用决策树算法（决策树算法如：ID3<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">6</a></sup>决策树算法（基于<strong>香农熵</strong>进行节点分裂），CART<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>决策树算法（基于<strong>Gini不纯度</strong>进行节点分裂），C4.5<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">8</a></sup>决策树算法（基于<strong>信息增益比</strong>进行节点分裂））作为弱学习机的<code>Bagging</code>集成学习算法。</p><blockquote><p>在论文<sup id="fnref:5:1" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>中，作者对于<code>Bagging</code>的优势描述如下：<br/> 1、通过<code>bagging</code>可以提高准确率，当随机特征被选取时<br/> 2、<code>Bagging</code>可以被用来对树的泛化误差（The Generalization Error $PE^*$）进行评估，于此同时也可以对强度（strength）以及相关性（correlation）进行评估</p></blockquote><p><code>Random Forest</code>较之<code>Adaboost</code>拥有更加好的<strong>鲁棒性</strong>以及对更强的<strong>抗噪声能力</strong>（more robust and respect to noise）。其算法思路和<code>bagging</code>的基本思路一致：</p><ul><li>给定训练数据集 (<em>Training set</em>) ：$T$，对训练数据集进行<strong>自助法采样（<code>Boostrap Sampling</code>）</strong> 得到一系列样本子集：${T_1,…,T_k}$，根据决策树算法$h$对样本子集构建对应的决策树：$h(x, T_k)$。在决策树每个节点进行分裂时，从全部$K$个特征空间均匀随机的选择一个特征子集（一般选择$log_2K$），然后从这个子集中选择一个最优分裂特征来构建决策树。</li></ul><blockquote><p>在分类任务中，通常将那些不属于类别$x$的样本称之为 <em>out-of-bagged</em> 有论文中通过利用 out-of-bag 的方差估计来估计任意分类器的泛化误差</p></blockquote><h1 id="2boosting">2、Boosting</h1><p><code>Boosting</code>算法相较之<code>Bagging</code>算法区别在于：<code>Bagging</code>是通过<code>bootstrap sampling</code>获取样本之后，而后去对抽取样本来构建树。而<code>Boosting</code>每一颗树都是通过先前的树的信息来进行构建。<code>Boosting</code>基本思想：通过产生数个简单的、精度比随机猜测略好的粗糙估计（<code>Boosting</code>算法中称为弱规则$h_1,…,h_k$），再将这些规则集成构造出一个高精度的估计,其算法步骤如下：</p><ul><li>1、利用初始化训练样本集训练得到一个弱学习器</li><li>2、提高被弱学习器误分的样本的权重，使得那些被错误分类的样本在下一轮训练中可以得到更大的关注，利用调整后的样本训练得到下一个弱学习器</li><li>3、重复上述步骤，直至得到$T$个学习器</li><li>4、对于分类问题，采用有权重的投票方式；对于回归问题，采用加权平均得到预测值</li></ul><h2 id="21-adaboost">2.1 Adaboost</h2><p><code>Adaboot</code><sup id="fnref:12" role="doc-noteref"><a href="#fn:12" class="footnote" rel="footnote">9</a></sup>其算法基本思路如下：<br/> 假设训练样本：<br/> \(T=\{(x_1, y_1),...,(x_m,y_m)\}\)<br/> 训练集在第$k$个弱学习器的输出权重为：<br/> \(D(k)=(w_{k1},...,w_{km});w_{1i}=\frac{1}{m};i=1,2,...,m\)</p><h2 id="22-gbdt">2.2 GBDT</h2><p><code>GBDT(Gradient Boosting Decision Tree)</code>是决策树的集成模型，按顺序训练<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">10</a></sup>。在每次迭代中，GBDT通过拟合负梯度（也称为残差）来学习决策树<sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">11</a></sup>。比如说：假设一个对一个人年龄（40岁）进行预测，第一次迭代：30（10）（预测值（损失值））；第二次迭代（在损失值10的基础上进行迭代）：7（3）；第三次迭代：2（1）；第四次迭代：1（0）。那么可以得到最终年龄的预测值为：30+7+2+1=40。而<code>GBDT</code>主要有3个主要概念构成：1、Regression Decision Tree（DT）；2、Gradient Boosting（GB）；3、Shrinkage</p><blockquote><p>GBDT is an ensemble model of decision trees, which are trained in sequence<sup id="fnref:9:1" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">10</a></sup>. In each iteration, GBDT learns the decision trees by fitting the negative gradients (also known as residual errors)<sup id="fnref:10:1" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">11</a></sup></p></blockquote><p><img src="https://s2.loli.net/2025/06/21/UvXepAYWgdjyKO7.webp" alt="image" width="886" height="269" loading="lazy" decoding="async"/></p><p>其算法过程：假设训练样本：$T={(x_1,y_1),…,(x_m,y_m)}$，最大的迭代次数为：$T$，损失函数：$L$。那么：</p><ul><li>1、对弱学习器进行初始化：</li></ul> \[c_{t j}=\underbrace{\arg \min }_{c} \sum_{x_{i} \in R_{t j}} L(y_{i}, f_{t-1}(x_{i}))\] <ul><li>2、进行$T$次迭代：</li></ul><p>对于样本$i=1,2,…,m$，计算负梯度：</p> \[r_{t j}=-\left[\frac{\left.\partial L\left(y_{i}, f\left(x_{i}\right)\right)\right)}{\partial f\left(x_{i}\right)}\right]_{f(x)=f_{t-1}(x)}\] <p>利用$(x_i,r_{ti})(i=1,…,m)$拟合一颗CART回归树，得到t个回归树，那么对于每棵回归树</p><h2 id="23-xgboost">2.3 XGBoost</h2><p><code>XGBoot</code>是一种端到端的<code>tree Boosting</code>方法<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">12</a></sup>。其基本思想和<code>GBDT</code>一样。</p><blockquote><p>we describe a scalable endto-end tree boosting system called XGBoost</p></blockquote><p>给定拥有$m$个特征的$n$个样本数据： $D={(x_i,y_i)}(|D|=n,x_i \in R^m,y_i \in R)$通过使用 $K$ 个独立函数对结果进行预测：<br/> \(\widehat{y_i}=\sum_{k=1}^{K}f_k(x_i), g_k \in F\)<br/> 其中：$F={f(x)=w_{q(x)}}(q:R^m \rightarrow T, w\in R^T)$为回归树空间，$q$为表示每棵树的结构，样本映射到最终的叶子节点。$T$是树中叶子的数量。$f_k$对应一个独立的树结构$q$和叶子权重。为了得到学习函数集，最小化如下<code>正则化目标（regularized object）</code>：<br/> \(L(\phi)=\sum_i L(\widehat{y}_i,y_i)+ \sum_k \Omega(f_k) \\ 其中\Omega(f)= \gamma T+ \frac{1}{2} \lambda||w||^2\)<br/> 上式子中$L$代表损失函数，$\widehat{y}$代表预测值，$y$代表实际值，$\Omega$代表正则化项。</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p><a href="https://wires.onlinelibrary.wiley.com/doi/10.1002/widm.1249">Sagi,O.&amp;Rokach,L.Ensemble learning: A survey.WIREs Data Min &amp; Knowl 8,e1249(2018).</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p><a href="https://courses.cs.washington.edu/courses/cse446/12wi/tgd-ensembles.pdf">Dietterich T G. Ensemble learning[J]. The handbook of brain theory and neural networks, 2002, 2(1): 110-125.</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p><a href="http://link.springer.com/10.1007/BF00058655">Breiman,L.Bagging predictors.Mach Learn 24,123–140(1996).</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p><a href="https://link.springer.com/chapter/10.1007/3-540-36434-X_4">Meir,R.&amp; Rätsch,G.An Introduction to Boosting and Leveraging. in Advanced Lectures on Machine Learning 118–183 (Springer,Berlin,Heidelberg,2003).doi:10.1007/3-540-36434-X_4.</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:5" role="doc-endnote"><p><a href="http://link.springer.com/10.1023/A:1010933404324">Breiman,L.Random Forests.Machine Learning 45,5–32(2001).</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:5:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:8" role="doc-endnote"><p><a href="http://link.springer.com/10.1007/BF00116251">Quinlan, J. R. Induction of decision trees. Mach Learn 1, 81–106 (1986).</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:7" role="doc-endnote"><p><a href="https://wires.onlinelibrary.wiley.com/doi/10.1002/widm.8">Loh, W. Classification and regression trees. WIREs Data Min &amp; Knowl 1, 14–23 (2011).</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:6" role="doc-endnote"><p><a href="https://doi.org/10.1007/BF00993309">Salzberg,S.L.C4.5:Programs for Machine Learning by J. Ross Quinlan. Morgan Kaufmann Publishers, Inc., 1993. Mach Learn 16, 235–240 (1994).</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:12" role="doc-endnote"><p><a href="[10.1006/jcss.1997.1504](https://linkinghub.elsevier.com/retrieve/pii/S002200009791504X)">Freund,Y.&amp;Schapire,R.E.A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting.Journal of Computer and System Sciences 55,119–139 (1997).</a> <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:9" role="doc-endnote"><p><a href="http://www.jstor.org/stable/2699986">Friedman,J.H.Greedy Function Approximation: A Gradient Boosting Machine. The Annals of Statistics 29,1189–1232(2001).</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:9:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:10" role="doc-endnote"><p><a href="https://proceedings.neurips.cc/paper_files/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html">Ke,G.et al.LightGBM: A Highly Efficient Gradient Boosting Decision Tree. in Advances in Neural Information Processing Systems vol.30(Curran Associates, Inc.,2017).</a> <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:10:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:11" role="doc-endnote"><p><a href="https://dl.acm.org/doi/10.1145/2939672.2939785">Chen,T.&amp;Guestrin,C.XGBoost:A Scalable Tree Boosting System. in Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining 785–794 (Association for Computing Machinery, 2016).doi:10.1145/2939672.2939785.</a> <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>机器学习</category><pubDate>Fri, 05 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/05/Ensemble-Learning.html</guid></item><item><title>机器学习基础原理————可解释性Shap Value原理及代码</title><link>https://www.big-yellow-j.top/posts/2024/01/04/shapvalue.html</link><description><![CDATA[机器学习模型仅靠预测结果和单一指标（如分类准确性）无法满足实际需求，尤其在高风险场景中，需权衡预测性能与决策解释的必要性。机器学习可解释性通过构建解释器解决黑盒模型决策逻辑问题，其核心特征包括：可解释性（需搭配可解释模型如决策树、线性模型及可解释特征，确保非专业人士理解）、局部保真度（在特定样本周围效果接近复杂模型）、与模型无关（适用于SVM、神经网络等各类复杂模型）。常用解释工具涵盖特征重要性（基于信息增益/基尼系数）、ICE和PDP（分析特征取值对输出的影响）、SDT（单棵决策树解释复杂模型）、LIME（为样本构建代理模型近似原模型）及SHAP（源于博弈论的Shapley Value...]]></description><content:encoded><![CDATA[<p>如果⼀个机器学习模型运⾏良好，为什么我们仅仅信任该模型⽽忽略为什么做出特定的决策呢？<br/> 诸如分类准确性之类的单⼀指标⽆法完整地描述⼤多数实际任务。当涉及到预测模型时，需要作出权衡：<strong>你是只想知道预测是什么？</strong>例如，客户流失的概率或某种药物对病⼈的疗效。<strong>还是想知道为什么做出这样的预测？</strong>这种情况下可能为了可解释性付出预测性能下降的代价。在某些情况下，你不必关⼼为什么要做出这样的预测，只要知道模型在测试数据集的预测性能良好就⾜够了。但是在其他情况下，了解 “为什么” 可以帮助你更多地了解问题、数据以及模型可能失败的原因。有些模型可能不需要解释，因为它们是在低风险的环境中使⽤的，这意味着错误不会造成严重后果 (例如，电影推荐系统)，或者该⽅法已经被⼴泛研究和评估 (例如，光学字符识别 OCR)。对可解释性的需求来⾃问题形式化的不完整性，这意味着对于某些问题或任务，仅仅获得预测结果是不够的。该模型还必须解释是怎么获得这个预测的，因为正确的预测只部分地解决了你的原始问题。</p><h2 id="机器学习可解释性">机器学习可解释性</h2><p>需要建立一个解释器来解释黑盒模型，并且这个解释器必须满足以下特征：<br/><strong>可解释性</strong><br/> 要求解释器的模型与特征都必须是可解释的，像决策树、线性模型都是很适合拿来解释的模型；而可解释的模型必须搭配可解释的特征，才是真正的可解释性，让不了解机器学习的人也能通过解释器理解模型。<br/><strong>局部保真度</strong><br/> 既然我们已经使用了可解释的模型与特征，就不可能期望简单的可解释模型在效果上等同于复杂模型（比如原始CNN分类器）。所以解释器不需要在全局上达到复杂模型的效果，但至少在局部上效果要很接近，而此处的局部代表我们想观察的那个样本的周围。<br/><strong>与模型无关</strong><br/> 这里所指的是与复杂模型无关，换句话说无论多复杂的模型，像是SVM或神经网络，该解释器都可以工作。<br/> 除了传统的特征重要性排序外，ICE、PDP、SDT、LIME、SHAP都是揭开机器学习模型黑箱的有力工具。</p><ul><li>特征重要性计算依据某个特征进行决策树分裂时，分裂前后的信息增益（基尼系数）；</li><li>ICE和PDP考察某项特征的不同取值对模型输出值的影响；</li><li>SDT用单棵决策树解释其它更复杂的机器学习模型；</li><li>LIME的核心思想是对于每条样本，寻找一个更容易解释的代理模型解释原模型；</li><li>SHAP的概念源于博弈论，核心思想是计算特征对模型输出的边际贡献；</li></ul><hr/><p>1、<a href="https://www.big-yellow-j.top/posts/2024/01/03/lime.html">机器学习可解释性–LIME</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2024/01/04/shapvalue.html">机器学习可解释性–SHAP</a></p><h2 id="1shapley-value">1、Shapley Value</h2><p><strong>沙普利值</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>（Shapley value），是通过考虑各个代理（agent）做出的贡献，来公平地分配合作收益。代理的沙普利值是对于一个合作项目所期望的贡献量的平均值。计算公式：<br/> 设$I={1,2…n}$为$n$个人的集合那么$i$的贡献为：<br/> \(\varphi_{i}=\sum_{s\in S_{i}}w(|s|)[v(s)-v(s\i)]\)<br/> 其中$S_{i}$是$I$中包含成员$i$的所有子集形成的集合，$w(|s|)$是加权因子，$s\i$表示集合$s$中去掉元素$i$后的集合。<br/> $v(s)-v(s\i)$成员i在联盟中的贡献，即成员i的边际贡献；$w(|s|)$权重$w(|s|=\frac{(|s|-1)!(n-|s|)!}{n!})$</p><blockquote><p>WiKi：<br/> 玩家联盟合作，并从合作中获得一定的整体收益。由于一些参与者可能比其他参与者对联盟贡献更多，或者可能拥有不同的议价能力（例如威胁要摧毁全部盈余），因此在任何特定博弈中，参与者之间产生的盈余的最终分配应该是什么？或者换句话说：每个参与者对整体合作有多重要，他或她可以合理预期的回报是什么？Shapley 值为这个问题提供了一个可能的答案。<br/> 1、https://en.wikipedia.org/wiki/Shapley_value<br/> 《可解释的机器学习》<br/> 开源地址：<br/> 2、https://github.com/MingchaoZhu/InterpretableMLBook<br/> 在线阅读：<br/> 3、https://hjezero.github.io/posts/32fedbdb/</p></blockquote><p>借助《可解释的机器学习》中例子，假设准备购买一间公寓，其中你所感兴趣的是：1、有无公园；2、公寓面积；3、公寓所位于的楼层；4、是否能养猫。而后对公寓价格预测。</p><p><img src="https://s2.loli.net/2025/06/21/N4qyaGSvrYToW7s.webp" alt="iamge" width="669" height="312" loading="lazy" decoding="async"/></p><blockquote><p>比如说1：⼀套 50 平⽅⽶的公寓，附近有公园以及禁⽌猫⼊内，预计价格为 300,000 欧元那么1、公寓面积；2、公寓楼层；3、养猫？4、公园？这4个特征对<strong>预测价格</strong>300000欧元的<strong>贡献</strong>是怎么样的呢？<br/> 比如说<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>：一个程序C=500行代码需要编写，今天产品经理找了三个程序猿来完成，按照完成量发奖金：<br/> 条件一：$V_{1}=100$，$V_{2}=125$，$V_{3}=50$<br/> 解释：1号屌丝程序猿独立能写100行，2号大神程序猿独立能写125行，3号美女程序猿能写50行<br/> 条件二：$V_{12}=270$，$V_{23}=350$，$V_{13}=375$<br/> 解释：1,2号合作能写270行，2,3号合作能写350行，1,3号合作能写375行<br/> 条件三：$V_{123}=500$<br/> 3个人共同能完成500行<br/> 那么根据3组条件，合计6种组合分别如下：<br/> A.1号程序猿邀请2号程序猿加入他组成S联盟，1,2号邀请3号加入共同编写。<br/> B.1号邀请3号加入成为S小组，2号加入S小组<br/> C.2号邀请1号加入成为S小组，3号加入S小组<br/> D.2号邀请3号加入成为S小组，1号加入S小组<br/> E.3号邀请1号加入成为S小组，2号加入S小组<br/> F.3号邀请2号加入成为S小组，1号加入S小组<br/> 计算边际贡献：</p><p><img src="https://pica.zhimg.com/450be1b5a2d5196399f6665086a728d2_r.jpg?source=1940ef5c" alt="image"/></p><p>根据公式1得到：<br/> 1的shapley value：<br/> $\frac{1}{6}(100+100+145+150+325+150)=\frac{970}{6}$</p><p>依次类推得到其他人的shapely value</p></blockquote><h2 id="2a-unified-approach-to-interpreting-model-predictions">2、<a href="https://arxiv.org/pdf/1705.07874.pdf">A Unified Approach to Interpreting Model Predictions</a></h2><p>SHAP Value与LIME的方法类似，都是通过定义简单模型去对复杂模型进行解释。</p><blockquote><p>“we must use a simpler explanation model, which we define as any interpretable approximation of the original model.” (Lundberg 和 Lee, 2017, p. 2)</p><p>我们必须使用一个更简单的解释模型，我们将其定义为对原模型的任何可解释的近似。</p></blockquote><p>回顾LIME模型，利用简单函数$g$去在$x$“周围”去对复杂函数$f$进行局部近似。在本论文作者将部分<strong>解释模型函数</strong>称为<em>Additive feature attribution methods</em>其形式形如：</p> \[g(z)=\phi_{0}+\sum_{i=1}^{M}\phi_{i}z_{i}\] <p>其中：$M$所有的简单输入特征的个数；$\phi_{i}$每一种特征的<strong>贡献</strong>（对于的shapely值）；通过计算所有特征的贡献去近似复杂函数$f(x)$</p><blockquote><p>论文中部分<strong>解释模型函数</strong>有：<br/> 1、LIME模型对<strong>贡献</strong>计算：</p> \[\xi(s)= argmin_{g\in G} L(f,g,\pi_{x})+\Omega(g)\] <p>2、DeepLIFT（面向深度学习的可解释方法）模型对<strong>贡献</strong>计算：</p> \[\sum_{i=1}^{n}C_{\Delta x_{i}\Delta o}=\Delta o\] <p>贡献为：$C_{\Delta x_{i}\Delta o}$，其中$o=f(x)$为模型的输出，其中$r$为参考的样本输入，$\Delta o=f(x)-f(r)$。</p><p>3、经典Shapley Value计算：</p><ul><li>Shapley regression values <blockquote><p>feature importances for linear models in the presence of multicollinearity.<br/> 此模型要求计算模型所有的特征，认为每一个特征在模型中都起到了作用。计算公式如下：</p></blockquote></li></ul><p><img src="https://s2.loli.net/2025/06/21/5a1l897UIxmpjvn.webp" alt="image" width="587" height="95" loading="lazy" decoding="async"/></p><blockquote><p>就是上面列子中提及到的计算方法</p><ul><li>Shapley sampling values</li><li>Quantitative Input Influence</li></ul></blockquote></blockquote><p>对于<em>additive feature attribution methods</em>存在如下三点<a href="https://christophm.github.io/interpretable-ml-book/shap.html">性质</a>：<br/> 1、Local accuracy<br/> 2、Missingness<br/> 3、Consistency</p><h2 id="kernel-shaplinear-lime--shapley-values"><em>Kernel SHAP(Linear LIME + Shapley values)</em></h2><h3 id="kernel-shap计算步骤">Kernel SHAP计算步骤：</h3><p><strong>1. 初始化一些数据, z’, 作为Simplified Features（随机生成（0,1,1,0）,(1,0,1,1)等）</strong>*</p><blockquote><p>其中：$z’_{k}\in{(0,1)}^{M}$（0：缺失特征；1：初始化中存在的特征）其中$M$是我们的维数（理解为样本特征数目），$k\in{(1,…K)}$代表生成数据个数</p></blockquote><p><strong>2. 将上面的Simplified Features转换到原始数据空间, 并计算对应的预测值, f(h(z’))</strong>*</p><blockquote><p>比如说上面卖房子例子，有四个特征所以$M=4$那么假设初始化为$z’_{1}=(0,1,1,0)$那么也就是存在第二和第三个特征的联盟，那么对于第一个和第四个则通过$h$函数进行转换</p><p><img src="https://s2.loli.net/2025/06/21/oVlky7SUPthjQpf.webp" alt="image" width="953" height="434" loading="lazy" decoding="async"/></p></blockquote><p><strong>3. 对每一个z’计算对应的权重</strong>*</p><blockquote><table><tbody><tr><td><strong>Kernel SHAP的权重函数</strong>$\pi$为：$\pi_{x^{‘}}(z^{‘})=\frac{M-1}{(M\;choose\;</td><td>z^{‘}</td><td>)(M-</td><td>z^{‘}</td><td>)}$，其中$M$为维数（所有特征的个数），$</td><td>z^{‘}</td><td>$代表样本中1的个数，$M\;choose\;</td><td>z^{‘}</td><td>$代表$C_{M}^{z^{‘}}</td><td>z^{‘}</td><td>$。容易得到：若有很多1或很多0则取较高的权重，若0和1数量相近则取较低的权重。</td></tr></tbody></table><p><strong>LIME则是通过距离设置权重</strong></p></blockquote><p><strong>4. 拟合线性模型</strong>*<br/><strong>5. 计算出每一个特征的Shapley Value, 也就是线性模型的系数</strong>*</p><h3 id="kernel-shap计算">Kernel SHAP计算</h3><p>损失函数：</p> \[L(f,g,\pi_x^{'})=\sum_{z^{'} \in Z}[f(h_{x}^{-1}(z^{'}))-g(z^{'})]^{2}\pi_{x}^{'}(z^{'})\] <h2 id="代码">代码</h2><pre><code class="language-python">def shap_plot(path):
    plt.rcParams['font.sans-serif'] = ['FangSong']
    plt.rcParams['axes.unicode_minus'] = False
    data_shap = []
    df = pd.read_excel(path)
    for i in range(....): # 不同特征数量
        xi = df.iloc[:, :5]
        ss = StandardScaler()
        xi = ss.fit_transform(xi)
        y_i = df[yi[i]]
        
        model = joblib.load(...)
        feature_name = [....]
        x_train, x_test, y_train, y_test= train_test_split(xi, y_i, test_size= 0.2)

        explainer = shap.ExactExplainer(model.predict, x_train)
        data_shap.append(yi[i])
        data_shap.append(explainer(x_train))
    for value in data_shap:
        if isinstance(value, str):
            print(value)
        else:
            plt.yticks(fontproperties='Times New Roman', size=15, weight='bold')#设置大小及加粗
            plt.xticks(fontproperties='Times New Roman', size=15, weight='bold')
            shap.summary_plot(
                shap_values = value,
                feature_names = feature_name,
                plot_type = 'dot'
            )
</code></pre><h2 id="参考">参考</h2><p>1、https://github.com/MingchaoZhu/InterpretableMLBook<br/> 2、https://www.zhihu.com/question/23180647<br/> 3、<a href="https://zhuanlan.zhihu.com/p/483891565#:~:text=Shapley Value公式如下： 记 I%3D { 1%2C2%2C...%2Cn}为n个合作人的集合 varphi_i (upsilon),- upsilon (s backslash { i })]}">关于Shapley Value（夏普利值）的公式 - 知乎 (zhihu.com)</a><br/> 4、https://e0hyl.github.io/BLOG-OF-E0/LIMEandSHAP/<br/> 5、https://mathpretty.com/10699.html</p><h2 id="推荐阅读">推荐阅读</h2><p>1、https://christophm.github.io/interpretable-ml-book</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>https://github.com/MingchaoZhu/InterpretableMLBook <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://www.zhihu.com/question/23180647 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>机器学习</category><pubDate>Thu, 04 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/04/shapvalue.html</guid></item><item><title>机器学习基础原理————可解释性LIME原理</title><link>https://www.big-yellow-j.top/posts/2024/01/03/lime.html</link><description><![CDATA[机器学习模型在实际应用中，仅依赖预测准确性不足，尤其在高风险场景需解释决策依据，即可解释性。可解释模型需满足可解释性（模型与特征易理解，如决策树、线性模型搭配可解释特征）、局部保真度（在关注样本局部近似复杂模型效果）、与模型无关（适用于SVM、神经网络等各类黑盒模型），常用工具包括特征重要性、ICE、PDP、SDT、LIME、SHAP等。LIME（局部可解释模型无关解释）作为关键方法，通过对目标样本附近生成扰动样本并获取黑盒模型预测值，训练局部代理模型（如线性回归、决策树）实现局部近似，帮助理解单个预测的影响因素（如特征对结果的支持或反对），辅助人类决策。其优点为通用性强（适用于表格、文...]]></description><content:encoded><![CDATA[<p>如果⼀个机器学习模型运⾏良好，为什么我们仅仅信任该模型⽽忽略为什么做出特定的决策呢？<br/> 诸如分类准确性之类的单⼀指标⽆法完整地描述⼤多数实际任务。当涉及到预测模型时，需要作出权衡：<strong>你是只想知道预测是什么？</strong>例如，客户流失的概率或某种药物对病⼈的疗效。<strong>还是想知道为什么做出这样的预测？</strong>这种情况下可能为了可解释性付出预测性能下降的代价。在某些情况下，你不必关⼼为什么要做出这样的预测，只要知道模型在测试数据集的预测性能良好就⾜够了。但是在其他情况下，了解 “为什么” 可以帮助你更多地了解问题、数据以及模型可能失败的原因。有些模型可能不需要解释，因为它们是在低风险的环境中使⽤的，这意味着错误不会造成严重后果 (例如，电影推荐系统)，或者该⽅法已经被⼴泛研究和评估 (例如，光学字符识别 OCR)。对可解释性的需求来⾃问题形式化的不完整性，这意味着对于某些问题或任务，仅仅获得预测结果是不够的。该模型还必须解释是怎么获得这个预测的，因为正确的预测只部分地解决了你的原始问题。</p><h2 id="机器学习可解释性">机器学习可解释性</h2><p>需要建立一个解释器来解释黑盒模型，并且这个解释器必须满足以下特征：<br/><strong>可解释性</strong><br/> 要求解释器的模型与特征都必须是可解释的，像决策树、线性模型都是很适合拿来解释的模型；而可解释的模型必须搭配可解释的特征，才是真正的可解释性，让不了解机器学习的人也能通过解释器理解模型。<br/><strong>局部保真度</strong><br/> 既然我们已经使用了可解释的模型与特征，就不可能期望简单的可解释模型在效果上等同于复杂模型（比如原始CNN分类器）。所以解释器不需要在全局上达到复杂模型的效果，但至少在局部上效果要很接近，而此处的局部代表我们想观察的那个样本的周围。<br/><strong>与模型无关</strong><br/> 这里所指的是与复杂模型无关，换句话说无论多复杂的模型，像是SVM或神经网络，该解释器都可以工作。<br/> 除了传统的特征重要性排序外，ICE、PDP、SDT、LIME、SHAP都是揭开机器学习模型黑箱的有力工具。</p><ul><li>特征重要性计算依据某个特征进行决策树分裂时，分裂前后的信息增益（基尼系数）；</li><li>ICE和PDP考察某项特征的不同取值对模型输出值的影响；</li><li>SDT用单棵决策树解释其它更复杂的机器学习模型；</li><li>LIME的核心思想是对于每条样本，寻找一个更容易解释的代理模型解释原模型；</li><li>SHAP的概念源于博弈论，核心思想是计算特征对模型输出的边际贡献；</li></ul><p>1、<a href="https://www.big-yellow-j.top/posts/2024/01/03/lime.html">机器学习可解释性–LIME</a><br/> 2、<a href="https://www.big-yellow-j.top/posts/2024/01/04/shapvalue.html">机器学习可解释性–SHAP</a></p><h2 id="机器学习的解释性">机器学习的解释性</h2><p>Trusting a prediction, i.e. whether a user trusts an individual prediction sufficiently to take some action based on it, and trusting a model, i.e. whether the user trusts a model to behave in reasonable ways if deployed.” (Ribeiro 等, 2016, p. 1135)</p><blockquote><p>信任一个预测，即用户是否充分信任一个个体的预测，并在此基础上采取行动；信任一个模型，即用户是否信任一个模型在部署后的行为是否合</p></blockquote><p>LIME, an algorithm that can explain the predictions of any classifier or regressor in a faithful way, by approximating it locally with an interpretable model.” (Ribeiro 等, 2016, p. 1135)</p><blockquote><p>LIME是一种算法，通过用可解释的模型对其进行<strong>局部近似</strong>，可以忠实地解释任何分类器或回归器的预测。</p></blockquote><p>所谓机器学习可解释性就是：我们建立了一个模型预测得到了一个结果，但是你这个<strong>结果真的可以让人信服吗</strong>（从实际生产角度考虑，不从模型的准确率的角度考虑）？这个<strong>模型的影响因素</strong>又是什么呢？如下图<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>：<br/><img src="https://s2.loli.net/2023/06/04/Pm6VexD4KinwA2l.png" alt="机器学习可解释性--LIME[^2]" style="zoom:60%;"/></p><p>我们通过模型预测流感（flu），通过LIME得到的解释是：喷嚏（sneeze）、头疼（headche）对流感预测是“支持”，而没有疲劳（no fatigue）是“反对”。我们将模型结果交给决策者（human make decision）让她对模型结果做判断。<strong>机器学习</strong>只能作为我们辅助决策的工具，对于实际决策还是依靠<strong>人类</strong>。而我们的决策的依据就是<strong>解释</strong>（explainer），我们根据给出的解释来判别模型是否可作为我们的决策依据。</p><h2 id="lime原理">LIME原理</h2><p><strong>LIME（Local Interpretable Model-agnostic Explanations）</strong>。该模型是一个<strong>局部</strong>可解释模型，并且是一个与模型自身的无关的可解释方法。使用训练的<strong>局部代理模型</strong>来对单个样本进行解释。假设对于需要解释的黑盒模型，取关注的实例样本，在其附近进行扰动生成新的样本点，并得到黑盒模型的预测值，使用新的数据集训练可解释的模型（如线性回归、决策树），得到对黑盒模型良好的局部近似<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>。</p><p>“The overall goal of LIME is to identify an interpretable model over the interpretable representation that is locally faithful to the classifier.” (Ribeiro 等, 2016, p. 1137)</p><blockquote><p>LIME的总体目标是基于局部可解释性模型 在局部忠实于分类器的可解释表示上识别一个可解释模型。</p></blockquote><p>LIME特点如下：</p><ul><li>Local：局部保证度，即我们希望解释真实反映的分类器</li></ul><h2 id="lime计算">LIME计算</h2><p>假设$f$作为我们需要解释的模型，那么我们定义解释模型$g \in G$ ，$G$作为<strong>解释族函数</strong>（一系列可能的解释模型（线性模型、决策树模型等）），因为并不是每一个$g\in G$都可能是简单到可以解释的，因此定义$\Omega(g)$作为<strong>复杂形测度</strong>，$\pi_{x}$作为实例$z$到$x$之间的邻近度量，从而定义$x$周围的局部性。最后定义$L(f,g,\pi_{x})$作为不忠实$g$在$\pi_{x}$定义的局部中逼近$f$，为了保证<strong>可解释性</strong>和<strong>局部忠实性</strong>计算公式：</p> \[\xi(s)= argmin_{g\in G} L(f,g,\pi_{x})+\Omega(g)\] <h3 id="局部探索">局部探索</h3><p>前面提及到了LIME是一种局部探索可解释模型，那么其局部探索功能如何实现呢？在论文中的<code>3.3 Sampling for Local Exploration</code>作者给出解释如下：<br/> 如公式1所示我们需要最小化$L(f,g,\pi_{x})$，我们在$x$周围生成<strong>扰动样本</strong>（perturbed sample）设生成的扰动样本为$Z$，那么我们可以根据我们的$f$对我们所生成的扰动样本进行处理即：$f(Z)$，我们对扰动样本进行加权（距离$x$近的赋予较大权重，反之则较小权重）</p><blockquote><p>“where we sample instances both in the vicinity of $x$ (which have a high weight due to $\pi_{x}$) and far away from x (low weight from $\pi_{x}$).” ([Ribeiro 等, 2016, p. 1137]</p></blockquote><blockquote><p>其中，我们分别在$x$ (由于$\pi_{x}$而具有很高的权重)附近和远离$x$ (来自$\pi_{x}$的低权重)的地方采样实例。</p></blockquote><p>作者在论文中提到即使原始模型（$f$）很难全局进行解释，但是LIME能够在局部进行合理解释</p><blockquote><p>ven though the original model may be too complex to explain globally, LIME presents an explanation that is locally faithful</p></blockquote><h3 id="稀疏线性解释">稀疏线性解释</h3><p>论文作者设置解释族函数$G$为线性模型（$g(Z)=w_{g}z$），设置$L$为$ \underset{z,z^{‘}\in Z}{\sum}\pi_{x}(z)(f(z)-g(z^{‘})^{2}$，$\pi_{x}$为$exp(\frac{-D(x,z)^{2}}{\sigma^{2}})$，上面提及到的扰动样本$Z$，对于扰动样本设计线性函数去对扰动样本进行区分（已分类算法为例）那么我们所赋予的不同的权重$w_{g}$就是不同的样本中不同特征的影响。</p><h3 id="lime步骤">LIME步骤</h3><ul><li>对整个数据进行训练，模型可以是Lightgbm，XGBoost等复杂的模型（本身不可解释）;</li><li>选择我们想要解释的变量$x$;</li><li>对数据集中的数据进行可解释的N次扰动，生成扰动样本;</li><li>对这些新的样本求出权重，这个权重是这些数据点与我们要解释的数据之间的距离;</li><li>根据上面新的数据集，拟合一个简单的模型$g$，比如Lasso Regression得到模型的权重;</li><li>通过简单模型$g$来对原复杂模型在$x$点附近进行解释;</li></ul><h2 id="lime直观解释">LIME直观解释</h2><h3 id="1分类算法">1、分类算法</h3><p><img src="https://s2.loli.net/2023/06/05/RqaWkvG7wHSo6jF.png" alt="202306052009422" style="zoom:50%;"/></p><p>如上图不同的颜色块代表不同的类别（蓝色和粉色），很难通过线性模型进行近似。因此输入样本（加重红色×）在其周围生成不同的扰动样本（×和·，其大小代表距离），我们可以对所生成的扰动样本构建线性函数进行区分。具体步骤如下图所示：</p><p><img src="https://s2.loli.net/2023/06/05/W2h9NPIY1sDdwi6.png" alt="202306052008339" style="zoom:60%;"/></p><h3 id="2图像识别">2、图像识别</h3><p>在Local Interpretable Model-Agnostic Explanations (LIME): An Introduction<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>中作者对图像识别做出了解释。对一只树蛙进行分类：</p><p><img src="https://s2.loli.net/2023/06/05/srhUdaWXZK1jwgT.png" alt="202306052008339" style="zoom:60%;"/></p><p>将一些可解释的成分 “关闭”（在这种情况下，使它们变成灰色）来生成一组扰乱实例的数据。对于每个被扰乱的实例，我们根据模型得到树蛙在图像中的概率。然后我们在这个数据集上学习一个简单的（线性）模型，这个模型是局部加权的–也就是说，我们更关心在与原始图像更相似的扰动实例中犯错误。最后，我们将具有最高正向权重的超级像素作为解释，而将其他的东西涂成灰色。</p><p><img src="https://s2.loli.net/2023/06/05/xTNHmRpBsW52Lna.png" alt="202306052008339" style="zoom:60%;"/></p><p>我们在任意图像上解释谷歌的 Inception 神经网络。在这种情况下，如下图 所示，分类器将“树蛙”预测为最有可能的类别，其次是概率较低的“台球桌”和“气球”。解释表明分类器主要关注青蛙的脸作为对预测类别的解释。它还阐明了为什么“台球桌”的概率不为零：青蛙的手和眼睛与台球很相似，尤其是在绿色背景下。同样，爱心也很像一个红色的气球。</p><p><img src="https://s2.loli.net/2023/06/05/bE4x2AP1dktK8VZ.png" alt="202306052008339" style="zoom:60%;"/></p><h2 id="lime优缺点">LIME优缺点</h2><p>1、LIME算法有很强的通用性，效果好。LIME除了能够对图像的分类结果进行解释外，还可以应用到自然语言处理的相关任务中，如主题分类、词性标注等。因为LIME本身的出发点就是模型无关的，具有广泛的适用性。</p><p>2、LIME算法速度慢，LIME在采样完成后，每张采样出来的图片都要通过原模型预测一次结果，所以在速度上没有明显优势。</p><p>3、LIME算法拓展方向，本文的作者在18年新提出了Anchors的方法，指的是复杂模型在局部所呈现出来的很强的规则性的规律，注意和LIME的区别，LIME是在局部建立一个可理解的线性可分模型，而Anchors的目的是建立一套更精细的规则系统。在和文本相关的任务上有不错的表现。有待我们继续研究。</p><p>优点：</p><ul><li>表格型数据、文本和图片均适用；</li><li>解释对人友好，容易明白；</li><li>给出一个忠诚性度量，判断可解释模型是否可靠；</li><li>LIME可以使用原模型所用不到的一些特征数据，比如文本中一个词是否出现。</li></ul><p>缺点：</p><ul><li>表格型数据中，相邻点很难定义，需要尝试不同的kernel来看LIME给出的可解释是否合理；</li><li>扰动时，样本服从高斯分布，忽视了特征之间的相关性；</li><li>稳定性不够好，重复同样的操作，扰动生成的样本不同，给出的解释可能会差别很大。</li></ul><h2 id="参考">参考</h2><div class="footnotes" role="doc-endnotes"><ol><li id="fn:2" role="doc-endnote"><p>https://doi.org/10.1145/2939672.2939778 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:1" role="doc-endnote"><p>https://blog.csdn.net/iqdutao/article/details/108397239 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p>https://www.oreilly.com/content/introduction-to-local-interpretable-model-agnostic-explanations-lime/ <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>机器学习</category><pubDate>Wed, 03 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/03/lime.html</guid></item><item><title>机器学习基础原理————贝叶斯优化原理及代码实现</title><link>https://www.big-yellow-j.top/posts/2024/01/02/BayesianOptimization.html</link><description><![CDATA[贝叶斯优化是一种通过有限步骤实现全局优化的方法，适用于解决机器学习超参数优化等黑盒优化问题。其核心框架包括代理模型和采集函数两大组件：代理模型（如高斯过程GP、TPE）通过概率模型替代原始评估代价高昂的复杂目标函数，其中高斯过程通过核函数定义随机变量的联合分布，能有效捕捉函数特性并提供均值与方差预测；采集函数（如概率提升PI、期望提升EI、置信边界策略LCB/UCB等）则用于引导搜索最优解，通过平衡探索与利用，选择潜在提升价值高的决策点。本文结合相关论文与博客，详细介绍了贝叶斯优化的原理、代理模型与采集函数的数学基础，并提供部分代码复现示例，帮助读者理解其在函数优化及模型调参中的应用。]]></description><content:encoded><![CDATA[<p>本文通过结合如下论文以及blog：</p><blockquote><p>1、贝叶斯优化研究综述：https://doi.org/10.13328/j.cnki.jos.005607.<br/> 2、高斯回归可视化：https://jgoertler.com/visual-exploration-gaussian-processes/<br/> 3、贝叶斯优化：http://arxiv.org/abs/1012.2599</p></blockquote><p>对贝叶斯优化进行较为全面的介绍，以及部分代码复现</p><h2 id="介绍">介绍</h2><p>问题一：如果存在函数$y=x^2$那么对于这个函数很容易就可以得到他的最小值$x=0$时取到最小值，但是如果只告诉我们存在函数$y=f(x)$（$f(x)$具体的表达式未知），我们如何找到他的最小值呢？</p><p>问题二：对于机器学习、深度学习模型都是由许多参数所决定的（比如说：深度学习中学习率、网络深度等），假如我们通过计算模型的$R^2$来选择我们的参数，那么如何选择参数的值使得$R^2$最大呢？</p><p><strong>Grid Search？Random Search？Bayesian optimization？</strong></p><blockquote><p>超参数优化</p><p>百度百科：</p><p>https://baike.baidu.com/item/%E8%B6%85%E5%8F%82%E6%95%B0/3101858</p><p>Wiki：</p><p>https://en.wikipedia.org/wiki/Hyperparameter_optimization</p></blockquote><p>本文主要对<strong>Bayesian optimization</strong>进行解释。<strong>贝叶斯优化</strong>通过有限的步骤进行全局优化。定义我们的待优化函数：</p> \[x^{*}=\underset{x\in X}{argmin}f(x)\] <blockquote><p>上式子中：$x$代表<strong>决策向量</strong>（直观理解为：深度学习中的学习率、网络深度等），$X$代表<strong>决策空间</strong>（直观理解为：以学习率为例，假设我们能从学习率集合$\alpha=(0.01,0.02,0.03)$[&lt;–这就是决策空间] 选择最佳学习率[&lt;–这就是我们决策向量]，$f$则代表目标函数（比如上面提到的$R^2$或者机器学习模型$f$））</p></blockquote><p>许多机器学习中的优化问题都是<strong>黑盒优化</strong>问题，我们函数是一个黑盒函数<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>。如何通过<strong>贝叶斯优化</strong>实现<strong>(1)</strong>式子呢？贝叶斯优化的两板斧：（1）surrogate model（<strong>代理模型</strong>）；（2）acquisition function（<strong>采集函数</strong>）。贝叶斯优化框架如下<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup>：</p><p><img src="https://s2.loli.net/2023/06/10/cFwQxP2Doyfldtn.png" alt="202306101452451" style="zoom:80%;"/></p><p>贝叶斯优化框架应用在一维函数$f(x)=(x-0.3)^2+0.2sin(20x)$上3次迭代的示例：</p><p><img src="https://s2.loli.net/2023/06/10/RE2hpHuvJ5wWGnB.png" alt="图一:贝叶斯优化示例" style="zoom:70%;"/></p><h2 id="一代理模型surrogate-models">一、代理模型(surrogate models)</h2><h3 id="1高斯过程gp">1、高斯过程(GP)</h3><p>上面提及到机器学习是一个黑盒子(black box)，即我们只知道input和output，所以很难确直接定存在什么样的函数关系<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">3</a></sup>。既然你的<strong>函数关系</strong>确定不了，那么我们就可以直接找到一个模型对你的函数进行<strong>替代</strong>（代理），这就是贝叶斯优化第一板斧：<strong>代理模型</strong>。（使用概率模型代理原始评估代价高昂的复杂目标函数）<br/> 这里主要解释<strong>高斯过程（Gaussian processes，GP）</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup></p><blockquote><p>其他代理模型，感兴趣的可以阅读这篇<a href="https://doi.org/10.13328/j.cnki.jos.005607">论文</a></p><p>WiKi：</p><p><a href="https://zh.wikipedia.org/wiki/高斯过程">高斯过程 - 维基百科，自由的百科全书 (wikipedia.org)</a></p><p>百度百科：</p><p><a href="https://baike.baidu.com/item/高斯过程/4535435?structureClickId=4535435&amp;structureId=56eafc20675c3e28256b410f&amp;structureItemId=a46e4d355312e203fffb9c11">高斯过程_百度百科 (baidu.com)</a></p><p>高斯过程：<strong>就是一系列关于连续域（时间或空间）的随机变量的联合，而且针对每一个时间或是空间点上的随机变量都是服从高斯分布的</strong></p></blockquote><hr/><blockquote><p>解释高斯过程前了解<strong>高斯分布</strong>学过概率论的应该都了解，高斯分布其实就是<strong>正态分布</strong>平时所学的大多为一元正态分布，推广到$n$维的高斯分布：</p> \[X=\begin{bmatrix}X_1\\X_2\\...\\X_n\end{bmatrix}∼N(\mu,\sum)\] </blockquote><blockquote><p>其中$\mu$代表均值，$\sum$代表协方差。</p></blockquote><hr/><p>高斯过程的数学原理<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>：</p> \[f(x)∼GP(m(x),k(x,x^{'}))\] <p>其中$m(x)$代表<strong>均值</strong>（为了方便令$m(x)=0$），$k$代表<strong>核函数</strong>。常用核函数：</p> \[k(x_i,x_j)=\sum=cov(x_i,x_j)=exp(-\frac{1}{2}||x_i-x_j||^2)\] <blockquote><p>其他核函数</p><p><img src="https://pic4.zhimg.com/80/v2-85fb84d30a68bc03e301ed67d868c38b_720w.webp" alt="202306131551131" style="zoom:90%;"/></p></blockquote><p>在高斯过程中，核函数往往就决定了分布的形状，于此同时也就决定我们需要预测函数所具有的特性，对于不同两点$x_i$和$x_j$两点距离近则值接近1反之则接近0。那么可以得到核矩阵为：</p> \[K=\begin{bmatrix}k(x_1,x_1)&amp;...&amp;k(x_1,x_t)\\...&amp;...&amp;...\\k(x_t,x_1)&amp;...&amp;k(x_1,x_t) \end{bmatrix}\] <p>以<strong>回归任务</strong>为例<sup id="fnref:4:1" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>，高斯过程定义了潜在函数的<strong>概率分布</strong>，由于这是一个多元高斯分布，这些函数也呈正态分布。通常假设$μ= 0$，在没有观察到任何训练数据的情况下。在贝叶斯推断的框架下，将其称之为先验分布 $f(x)∼N(\mu_f,K_f)$。在没观察到任何训练样本，该分布会围绕 $μ_f=0$展开（可定义此时先验分布为$f(x)∼N(0,K_f)$）。先验分布的维数和测试点的数目 $N=∣X∣$一致。我们将用核函数来建立协方差矩阵，维数为$N×N$。</p><p><img src="https://s2.loli.net/2023/06/13/jN7wmdgoxSEu2zb.png" alt="202306131635876" style="zoom:70%;"/></p><blockquote><p>以RBF为核函数生成5组样本</p></blockquote><p>当补充训练样本时得到：</p><p><img src="https://s2.loli.net/2023/06/13/EwdIPmZXJ4Uu3QF.png" alt="202306131650854" style="zoom:70%;"/></p><p>输入样本点，数学原理如下：假设观察到样本点为$(x,y)$那么$y$与先验分布$f(x)$的联合高斯分布为：</p> \[\begin{bmatrix}f(x)\\y \end{bmatrix}∼N(\begin{bmatrix}0\\0 \end{bmatrix},\begin{bmatrix}K_{ff}&amp;&amp;K_{fy}\\K^{T}_{fy}&amp;&amp;K_{yy} \end{bmatrix})\] <table><tbody><tr><td>那么此时可以根据联合分布得到$P(y</td><td>f)$的分布为：</td></tr></tbody></table> \[P(y|f)=N(\mu(x),\sigma^2(x))\] <p>其中：$\mu(x)=K^{T}<em>{fy}K</em>{ff}^{-1}f(x)$，$\sigma^2(x)=K_{yy}-K^{T}<em>{fy}K</em>{ff}^{-1}K_{fy}$</p><p>从<strong>回归</strong>的角度对<strong>高斯过程</strong>进行理解：假设我们需要拟合函数为：</p> \[y=sin(2.5x)+sin(x)+0.05x^2+1\] <p>我们通过设置$x$范围生成输入数据，那么可以得到输出数据$y$那么GP拟合如下：</p><p><img src="https://s2.loli.net/2023/06/13/SeJKV91xCI2g4Qj.png" alt="202306131551131" style="zoom:90%;"/></p><p>上图也很容易理解，在$x&lt;10$以前我们输入了数据那么置信区间范围较小，而$x&gt;10$之后由于没有输入数据置信区间范围较大</p><blockquote><p><img src="https://s2.loli.net/2023/06/10/k8b4stXOKJfA2Ya.png" alt="图一" style="zoom:60%;"/></p><p>具有三个观测值的简单一维高斯过程。实线黑线是给定数据的目标函数的GP代理均值预测，阴影区域表示均值加减方差。</p><p>Simple 1D Gaussian process with three observations. The solid black line is the GP surrogate mean prediction of the objective function given the data, and the shaded area shows the mean plus and minus the variance. The superimposed Gaussians correspond to the GP mean and standard deviation ($μ(·) $and $σ(·)) $of prediction at the points, $x_{1:3}$.</p></blockquote><h3 id="2tpe">2、TPE</h3><table><tbody><tr><td>高斯过程中通过$p(y</td><td>x)$</td></tr></tbody></table><h2 id="二采集函数acquisition-functions">二、采集函数（Acquisition Functions）</h2><p>在<a href="http://arxiv.org/abs/1012.2599">论文</a>中作者对于<strong>采集函数</strong>的描述为：<br/> The role of the acquisition function is to guide the search for the optimum.</p><blockquote><p>个人理解为：上一节介绍了GP过程中引入新的数据点其联合分布，那么的话我们可以直接引入$n$个点直接将全部$x$进行覆盖，但是这样的话Bayesian optimization就失去其意义了，如何通过最少的点去实现$x^{*}=\underset{x\in X}{argmin}f(x)$<br/> Acquisition functions are defined such that high acquisition corresponds to potentially high values of the objective function.</p><p>采集函数被定义为目标函数的潜在高值</p></blockquote><p>可以对采集函数理解为：<strong>去找到一个合适的点</strong>。常用的采集函数：</p><h3 id="1probability-of-improvementpi">1、probability of improvement（PI）</h3><p>PI去尝试最大化现有的概率$f(x^+)$，其中$x^+=\underset{x\in X}{argmax}f(x)$其公式为：</p> \[PI_{t}(x)=P(f_{t}(x) \geq f_{t}(x^+)+\xi)=\phi(\frac{\mu_{t}(x)-f_{t}(x^+)-\xi}{\sigma_{t}(x)})\] <p>其中$\phi(.)$为正则化，$\xi（\xi \geq 0）$为权重。PI策略通过PI提升最大化来选择新一轮的超参组合：</p> \[x_{t+1}=argmax_{x}(PI_{t}(x))\] <p>其中$x_{t+1}$代表新一轮超参组合。</p><h3 id="2expected-improvementei">2、expected improvement（EI）</h3><p>PI策略选择提升<strong>概率最大</strong>的候选点，这一策略值考虑了提升的概率而没有考虑<strong>提升量</strong>的大小，EI针对此提出：$EI(x)=E[max(f_{t+1}(x)-f(x^+),0)]$那么EI函数为：</p> \[f(n)= \begin{cases} (\mu(x)-f(x^+)\phi(Z)+\sigma(x)\phi(Z) &amp; \text {if $\phi(x)&gt;0$} \\ 0 &amp; \text{if $\phi(x)=0$ } \end{cases}\] <p>其中$Z=\frac{\mu(x)-f(x^+)}{\sigma(x)}$</p><blockquote><p>具体公式推导见论文（第13页）：http://arxiv.org/abs/1012.2599</p></blockquote><h3 id="3confidence-bound-criteria置信边界策略">3、Confidence bound criteria（置信边界策略）</h3><p><strong>1. LCB</strong>(置信下界策略，计算目标函数最小值)</p> \[LCB(x)=\mu(x)-\kappa \phi(x)\] <p><strong>2. UCB</strong>（置信上界策略，计算目标函数最大值）</p> \[UCB(x)=\mu(x)+\kappa \phi(x)\] <blockquote><p>LCB、UCB中的$\kappa$ is left to the user</p></blockquote><p><strong>3. GP-UCB</strong></p> \[GP-UCB=\mu(x)+\sqrt{v\tau_{t}}\phi(x)\] <p>GP-UCB很简单的一种采集策略，以随机变量的置信上界最大化为原则选择下一轮的超参组合</p><p><strong>4.其它</strong></p><p>见论文：https://doi.org/10.13328/j.cnki.jos.005607</p><h2 id="总结">总结</h2><h3 id="1常用代理函数">1、常用代理函数</h3><p><img src="https://s2.loli.net/2023/06/13/qRv9E61jIO2hYsn.png" alt="202306132058850" style="zoom:100%;"/></p><h3 id="2常用采集函数">2、常用采集函数</h3><p><img src="https://s2.loli.net/2023/06/13/tfUN829GmbqKka3.png" alt="202306132059038" style="zoom:100%;"/></p><h2 id="代码">代码</h2><p>代码参考：https://github.com/bayesian-optimization/BayesianOptimization</p><pre><code class="language-python">from bayes_opt import BayesianOptimization #调用第三方库
from skelarn.svm import SVR
from sklearn.metrics import r2_score

#交叉验证贝叶斯优化
X_train, X_test, Y_train, Y_test= train_test_split(x,y,test_size=0.2,random_state=100)
from sklearn.svm import SVR
def svr_cv(C, epsilon, gamma):
    kf = KFold(n_splits=5, shuffle=True, random_state=100)
    svr = SVR(C=C, epsilon=epsilon, gamma=gamma)
    for i, (train_index, test_index) in enumerate(kf.split(X_train, Y_train.values)):
        svr.fit(X_train[train_index], Y_train.values[train_index])
        pred = svr.predict(X_train[test_index])
        return r2_score(pred, Y_train.values[test_index])

#输入测试的函数，以及变量的范围
svr_bo = BayesianOptimization(svr_cv,{'C':(1,16), 'epsilon':(0,1), 'gamma':(0,1)})
svr_bo.maximize()
</code></pre><p><img src="https://s2.loli.net/2023/06/10/YTiR82Xsz4SHKxh.png" alt="202306101944250" style="zoom:60%;"/></p><pre><code class="language-python">svr_bo.max #得到最佳参数
#{'target': 0.9875895309185105,
# 'params': {'C': 14.595794386042416,
#  'epsilon': 0.09480102745231553,
#  'gamma': 0.09251046201638335}}
</code></pre><p>通过最佳参数进行测试：</p><pre><code class="language-python">svr1 =  SVR(C=14.595794386042416, epsilon=0.09480102745231553, gamma=0.09251046201638335)
svr1.fit(X_train, Y_train)
r2_score(Y_test.values, svr1.predict(X_test))
#0.9945825852230629
</code></pre><p>高斯拟合代码：</p><pre><code class="language-python">n = 100
x_min = -10
x_max = 10
X = np.sort(np.random.uniform(size=n))*(x_max- x_min) + x_min
X = X.reshape(-1, 1)
eta = np.random.normal(loc=0.0, scale= 0.5, size= n)

y_clean = np.sin(X * 2.5) + np.sin(X * 1.0)  + np.multiply(X, X) * 0.05 + 1
y_clean = y_clean.ravel()
y = y_clean+ eta
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF
 
kernel = RBF(
    length_scale=1, 
    length_scale_bounds=(1e-2, 1e3))
 
gpr = GaussianProcessRegressor( kernel,
                               alpha=0.1,
                               n_restarts_optimizer=5,
                               normalize_y=True)
gpr.fit(X,y )
#print("LML:", gpr.log_marginal_likelihood())
#print(gpr.get_params())
x = np.linspace(x_min - 2.0, x_max + 7.5, n * 2).reshape(-1, 1)
y_pred, y_pred_std = gpr.predict(x, return_std=True)
import matplotlib.pyplot as plt
plt.figure(figsize=(15, 8))
plt.plot(x, y_pred,linewidth = 3, label="GP mean")
plt.plot(X, y_clean, linewidth = 3,  label="Original y")
plt.plot(X, y,linewidth = 3, label="Noisy y")
plt.scatter(X, np.zeros_like(X), marker='x')
plt.fill_between(x.ravel(),
                 y_pred - y_pred_std,
                 y_pred + y_pred_std,
                label="95% confidence interval",
                interpolate=True,
                facecolor='blue',
                alpha=0.5)
plt.xlim(5, 15)
plt.legend()
</code></pre><p><strong>类似的</strong>：对于有些实验（比如：化学、生物）等，如果在已知数据范围中找到了一个最佳的机器学习模型，那么为了寻找到一个最佳参数也可以用贝叶斯优化进行实验。一个简易<code>Demo</code>如下：</p><pre><code class="language-python">def basye_find(run_num):
    def opt_f(...):
        data = np.array([c1, c2, c3, c4, c5]).reshape(1, -1)
        data = ss.transform(data) #数据标准化
        # 使用机器学习模型预测
        pred = model.predict(data)
        return pred

    basye_con_list = []
    for i in range(0, run_num):
        model = joblib.load(...)
        ss = joblib.load('./model/model_cv/CV数据标准化')

        optimizer = BayesianOptimization(
            opt_f,
            params,
            verbose = 0,
            allow_duplicate_points = True)
        optimizer.maximize(n_iter=50, init_points=30)
        basye_find_result = optimizer.max
        
        basye_con = []
        for key, value in basye_find_result['params'].items():
            basye_con.append(value) #获得最佳实验条件

        basye_con_ = np.array(basye_con).reshape(1, -1)
        pred = model.predict(ss.transform(basye_con_))[0]
        basye_con.extend([pred])
        basye_con_list.extend([basye_con])

    return ...

#CV
params = {
    'A': ...,}
basye_con_df_cv = basye_find(30)
</code></pre><h2 id="推荐">推荐</h2><p>1、Gaussian Processes for Machine Learning：https://gaussianprocess.org/gpml/chapters/RW.pdf<br/> 2、贝叶斯优化论文：http://arxiv.org/abs/1012.2599<br/> 3、贝叶斯优化博客：https://banxian-w.com/article/2023/3/27/2539.html<br/> 4、可视化高斯过程：https://jgoertler.com/visual-exploration-gaussian-processes/#MargCond</p><h2 id="参考">参考</h2><p>1、http://krasserm.github.io/2018/03/21/bayesian-optimization/<br/> 2、https://zhuanlan.zhihu.com/p/53826787<br/> 3、崔佳旭, 杨博. 贝叶斯优化方法和应用综述[J/OL]. 软件学报, 2018, 29(10): 3068-3090. https://doi.org/10.13328/j.cnki.jos.005607.<br/> 4、https://jgoertler.com/visual-exploration-gaussian-processes/<br/> 5、http://arxiv.org/abs/1012.2599<br/> 6、https://www.cvmart.net/community/detail/3502<br/> 7、https://gaussianprocess.org/gpml/chapters/RW.pdf</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1" role="doc-endnote"><p>http://krasserm.github.io/2018/03/21/bayesian-optimization/ <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:3" role="doc-endnote"><p>https://doi.org/10.13328/j.cnki.jos.005607. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:2" role="doc-endnote"><p>https://zhuanlan.zhihu.com/p/53826787 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li><li id="fn:4" role="doc-endnote"><p>https://jgoertler.com/visual-exploration-gaussian-processes/ <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:4:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p></li><li id="fn:5" role="doc-endnote"><p>http://arxiv.org/abs/1012.2599 <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p></li></ol></div> ]]></content:encoded><category>机器学习</category><pubDate>Tue, 02 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/02/BayesianOptimization.html</guid></item><item><title>视觉模型————AlexNet原理以及代码</title><link>https://www.big-yellow-j.top/posts/2024/01/01/alexnet.html</link><description><![CDATA[AlexNet是深度学习领域具有里程碑意义的卷积神经网络模型，由Hinton团队于2012年提出，在ImageNet图像识别竞赛中以显著优势夺冠，推动了深度学习在计算机视觉领域的快速发展。该模型创新性地采用ReLU激活函数解决梯度消失问题，通过Dropout技术减少过拟合，使用重叠池化提升特征提取能力，并首次利用GPU实现并行计算加速训练过程。其8层网络结构（5个卷积层+3个全连接层）能够有效提取图像深层特征，为后续卷积神经网络（如VGG、ResNet）的发展奠定了重要基础，至今仍是计算机视觉研究与应用中的经典参考模型。]]></description><content:encoded><![CDATA[<iframe src="/code/AlexNet.html" width="100%" height="600px"></iframe> ]]></content:encoded><category>Jupyter</category><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/01/alexnet.html</guid></item><item><title>视觉模型————LeNet原理以及代码</title><link>https://www.big-yellow-j.top/posts/2024/01/01/LeNet.html</link><description><![CDATA[LeNet是由Yann LeCun提出的经典卷积神经网络（CNN）模型，主要应用于手写数字识别任务，是深度学习领域早期CNN的重要代表。其网络结构包含卷积层、池化层和全连接层，通过局部感受野、权值共享等机制有效提取图像特征，为后续深度学习模型的发展奠定了基础。相关内容可能涉及LeNet的代码实现、结构解析或应用案例，助力理解卷积神经网络的基本原理与实践。]]></description><content:encoded><![CDATA[<iframe src="/code/LeNet.html" width="100%" height="600px"></iframe> ]]></content:encoded><category>Jupyter</category><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/01/LeNet.html</guid></item><item><title>视觉模型————GoogleNet原理以及代码</title><link>https://www.big-yellow-j.top/posts/2024/01/01/GoogleNet.html</link><description><![CDATA[本文介绍GoogLeNet相关代码内容，GoogLeNet是经典的卷积神经网络（CNN）模型，由Google团队提出，以Inception模块为核心设计，在深度学习领域尤其是图像识别任务中具有重要应用价值。]]></description><content:encoded><![CDATA[<iframe src="/code/googlenet.html" width="100%" height="600px"></iframe><p><img src="https://s2.loli.net/2025/06/21/QRTAj1GeivKHrDs.webp" alt="1" width="1656" height="392" loading="lazy" decoding="async"/></p> ]]></content:encoded><category>Jupyter</category><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.big-yellow-j.top/posts/2024/01/01/GoogleNet.html</guid></item></channel></rss>