<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>所有文章 - 秦时月</title><link>https://www.qinshiyue.icu/posts/</link><description>所有文章 | 秦时月</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>qinshiyue615@gmail.com (秦时月)</managingEditor><webMaster>qinshiyue615@gmail.com (秦时月)</webMaster><atom:link href="https://www.qinshiyue.icu/posts/" rel="self" type="application/rss+xml"/><item><title>HIMloco 文献阅读</title><link>https://www.qinshiyue.icu/p/himloco-%E6%96%87%E7%8C%AE%E9%98%85%E8%AF%BB/</link><pubDate>Mon, 26 Jan 2026 10:22:16 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/himloco-%E6%96%87%E7%8C%AE%E9%98%85%E8%AF%BB/</guid><description><![CDATA[<h2 id="核心概念与背景">核心概念与背景</h2>
<h3 id="标题拆解">标题拆解</h3>
<p>论文题目是 <strong><a href="https://arxiv.org/abs/2312.11460" target="_blank" rel="noopener noreffer ">《Hybrid Internal Model: Learning Agile Legged Locomotion with Simulated Robot Response》</a></strong> 。</p>
<ul>
<li><strong>Hybrid Internal Model (HIM，混合内部模型):</strong> 这是作者提出的新方法。“混合”指的是它不仅关注显式的速度跟踪，还关注隐式的稳定性 。</li>
<li><strong>Agile Legged Locomotion (敏捷足式运动):</strong> 目标不是走得稳，而是要能跑、能跳、能爬楼梯，适应各种地形。</li>
<li><strong>Simulated Robot Response (仿真机器人响应):</strong> 这是核心创新点。它不直接去“猜测”地形是什么样的（比如摩擦力多少、坡度多少），而是去“猜测”机器人遇到地形后会产生什么<strong>反应</strong> 。</li>
</ul>
<h3 id="为什么要做这件事痛点">为什么要做这件事？(痛点)</h3>
<p>在足式机器人控制中，通常面临两个大难题：</p>
<ol>
<li><strong>“盲人摸象”：</strong> 机器人的传感器（IMU、电机编码器）只能感知自己（本体感知），很难准确知道外部环境的具体参数（比如地面摩擦系数、具体的凹凸不平程度） 。</li>
<li><strong>传统方法的缺陷：</strong> 之前的流行做法是“Teacher-Student”（教师-学生）架构 。
<ul>
<li>先在仿真里用一个拥有上帝视角的“老师”策略（能看到摩擦力等所有信息）训练。</li>
<li>然后再训练一个“学生”策略去模仿老师，但学生只能看本体传感器数据。</li>
<li><strong>问题：</strong> 这个模仿过程会有信息损失，导致效果打折，且训练流程复杂 。</li>
</ul>
</li>
</ol>
<h3 id="论文的解决方案-him">论文的解决方案 (HIM)</h3>
<p>作者提出了一种不依赖“环境参数估计”，也不需要复杂的“教师-学生”两阶段训练的方法：借鉴经典控制理论中的 <strong>Internal Model Control (IMC，内模控制)</strong> ，把所有外部环境因素（摩擦、地形高度等）都看作是<strong>干扰 (Disturbance)</strong> ，训练一个模型，根据机器人过去的一系列动作和传感器数据，去预测**“机器人接下来会发生什么反应”**（即 Response），如果能预测准这个反应，策略网络就能据此调整动作，抵抗干扰。</p>
<p>在技术手段上，使用了 <strong>对比学习 (Contrastive Learning)</strong> 来训练这个预测模型，而不是简单的回归（Regression） 。这提高了鲁棒性。</p>
<h2 id="研究方法">研究方法</h2>
<figure>
    
    <figcatpttion>引自论文图片</figcatpttion>
</figure>
<h3 id="系统的输入与输出-what-goes-in-what-comes-out">系统的输入与输出 (What goes in, What comes out)</h3>
<p>首先，要搞清楚控制器需要什么数据。</p>
<ul>
<li><strong>输入 (Proprioception $o_t^a$):</strong> 这是一个“盲”的输入，只有本体感知。
<ul>
<li><strong>关节编码器:</strong> 关节位置 $\theta$ 和速度 $\dot{\theta}$ 。</li>
<li><strong>IMU:</strong> 基座的角速度 $\omega$ 和重力方向 $g_t$ 。</li>
<li><strong>指令:</strong> 用户想要的速度（比如：前进 1.0 m/s）。</li>
<li><strong>历史信息:</strong> 这一点很关键。模型不仅看当前这一帧，而是看过去 $H$ 帧的数据（论文中默认为 <strong>5帧</strong>）。</li>
</ul>
</li>
<li><strong>输出 (Action $a_t$)</strong>:
<ul>
<li>直接输出 12 个电机的目标位置偏移量（Target Joint Positions）。这和标准的强化学习控制没有区别。</li>
</ul>
</li>
</ul>
<h3 id="核心组件混合内部模型-hybrid-internal-model-him">核心组件：混合内部模型 (Hybrid Internal Model, HIM)</h3>
<p>即上图中间黄色的部分，也是论文最大的创新。它就像在控制回路里加了一个“直觉模块”，接收历史观测数据 $o_{t-H:t}^a$，然后吐出一个 <strong>“混合内部嵌入 (Hybrid Internal Embedding)”</strong> 给策略网络 。</p>
<p>这个“嵌入”由两部分组成，所以叫“混合 (Hybrid)”：</p>
<ol>
<li><strong>显式部分 ($\hat{v}_t$ - Velocity):</strong> 预测机器人的实际线速度。
<ul>
<li><em>作用：</em> 让机器人知道自己实际跑得有多快（因为没有外部传感器，它很容易打滑，不知道自己其实没动）。</li>
<li><em>训练方式：</em> 简单的监督学习（Regression），用仿真里的真值去监督它 。</li>
</ul>
</li>
<li><strong>隐式部分 ($\hat{l}_t$ - Implicit Response):</strong> 一个潜在向量（Latent Vector，维度为16）。
<ul>
<li><em>作用：</em> 捕捉那些难以量化的“状态”，比如“脚下是不是很滑”、“刚才是不是绊了一下”。作者称之为“稳定性的隐式表达” 。</li>
<li><em>训练方式：</em> <strong>对比学习 (Contrastive Learning)</strong>。</li>
</ul>
</li>
</ol>
<h3 id="训练方法对比学习-why-contrastive">训练方法：对比学习 (Why Contrastive?)</h3>
<p>在上图的右上角，可以看到 <strong>&ldquo;Pull Closer&rdquo;</strong> 和 <strong>&ldquo;Push Away&rdquo;</strong> ，这是整篇文章的精华部分。</p>
<ul>
<li><strong>传统做法 (Regression):</strong> 试图让网络直接预测环境参数（例如：预测摩擦系数 = 0.5）。
<ul>
<li><em>缺点：</em> 仿真里的摩擦系数 0.5 和现实世界的 0.5 可能完全不是一回事（Sim-to-Real Gap）。如果网络死记硬背这个数字，到了现实世界就傻了。</li>
</ul>
</li>
<li><strong>本文做法 (Contrastive Learning)</strong>:
<ul>
<li>作者认为：不管环境参数是多少，<strong>“如果两段历史轨迹看起来很像，那么它们接下来的反应（下一帧状态）也应该很像”</strong>。</li>
<li><strong>正样本对 (Positive Pair):</strong> 拿一段“历史观测” ($o_{t-H:t}$) 和它紧接着发生的“未来一帧状态” ($o_{t+1}$)。模型要把这两者的特征向量拉近 (Pull Closer) 。</li>
</ul>
</li>
</ul>
<p>作者使用了 <strong>SwAV</strong> (Swapping Assignments between Views) 算法来实现这个对比学习 。这是一种无监督学习方法，不需要人工标注“这是冰面”或“这是楼梯”，机器人自己通过大量试错就学会了归类。</p>
<h2 id="实验验证">实验验证</h2>
<p>作者在 Unitree A1（小型狗）和 Aliengo（大型狗）上进行了测试。不仅仅是平地走，他们设计了三个极具挑战性的场景 ：</p>
<ul>
<li><strong>长楼梯 (Long-range Stairs):</strong> 这是一个连续决策过程。</li>
<li><strong>混合地形 (Compositional Terrain):</strong> 碎石路接楼梯。这对机器人的状态切换要求很高。</li>
<li><strong>抗干扰 (Anti-disturbance):</strong>
<ul>
<li><strong>拖拽测试 (Dragging):</strong> 在机器人腿上绑重物（模拟被草缠住或甚至电机老化阻力变大）。</li>
<li><strong>侧向撞击 (Lateral Hit):</strong> 用重物从侧面撞击机器人。</li>
</ul>
</li>
</ul>
<figure>
    
    <figccaption>实验结果 引自原论文</figccaption>
</figure>
<p>该表比了 HIM 和 RMA (Rapid Motor Adaptation，之前的SOTA方法)。数据非常惊人：</p>
<ul>
<li><strong>短楼梯成功率：</strong> HIM <strong>100%</strong> vs RMA <strong>60%</strong>。这意味着 RMA 走两次大概率会摔一次，而 HIM 极其稳定。</li>
<li><strong>抗拖拽能力：</strong> HIM 能承受 <strong>10kg</strong> 的拖拽，而 RMA 只能承受 <strong>10kg</strong> (虽然数字一样，但结合楼梯表现看，HIM 的综合鲁棒性更强)。</li>
<li><strong>未知地形 (Unseen Terrains):</strong> 面对训练中没见过的“软坡 (Deformable Slope)”，HIM 成功率 <strong>55%</strong>，而 RMA 只有 <strong>10%</strong>。这证明了 HIM 的<strong>泛化能力</strong>更强。</li>
</ul>
<blockquote>
<p>注意 <strong>MoB (Multiplicity of Behavior)</strong> 和 <strong>Built-in MPC</strong> 这两列。它们在楼梯和复杂地形上的成功率几乎是 <strong>0</strong>。这说明传统的模型预测控制 (MPC) 和一些较早的学习方法在应对这种非结构化极端地形时，几乎完全失效。</p>
</blockquote>
<figure>
    
    <figcaption>消融实验结果 引自原论文</figcaption>
</figure>
<p>作者通过设计消融实验，确定了“隐式响应”的重要性：</p>
<ul>
<li><strong>去掉速度预测 (w/o vel. inp.):</strong> 楼梯成功率从 100% 降到 <strong>85%</strong>。</li>
<li><strong>去掉隐式响应 (w/o lat. inp.):</strong> 楼梯成功率从 100% 骤降到 <strong>50%</strong>。</li>
<li><strong>结论：</strong> 那个通过<strong>对比学习</strong>训练出来的“隐式响应 (Internal Latent)”，才是这一整套系统的<strong>灵魂</strong>。它捕捉到了那些无法用语言描述的动态特性。</li>
</ul>
<h2 id="总结">总结</h2>
<p>这篇论文提出了一种高效、鲁棒的“盲式”足式机器人运动控制框架，核心在于利用<strong>对比学习</strong>来预测机器人的<strong>隐式响应</strong>，从而极大地提高了机器人对未知地形和干扰的适应能力。对于想要低成本实现高性能机器狗控制的开发者来说，这是目前性价比最高的复现方案之一。</p>
]]></description></item><item><title>Legged Gym 环境配置</title><link>https://www.qinshiyue.icu/p/legged-gym-%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/</link><pubDate>Tue, 25 Nov 2025 13:56:52 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/legged-gym-%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/</guid><description><![CDATA[<div class="featured-image">
                <img src="/dog.png" referrerpolicy="no-referrer">
            </div><p><strong>日期</strong>：2025-11</p>
<p><strong>系统环境</strong>：Ubuntu 22.04 LTS</p>
<p><strong>显卡要求</strong>：NVIDIA 显卡（支持 CUDA &gt;= 11.7）</p>
<p><strong>核心组件</strong>：Isaac Gym (Preview 4) + RSL_RL (v1.0.2) + Legged Gym</p>
<hr>
<h2 id="基础环境准备">基础环境准备</h2>
<p>Ubuntu 22.04 默认系统库较新，而 Isaac Gym 较旧，因此必须严格锁定 Python 和 PyTorch 版本。</p>
<blockquote>
<p><em>注</em>：显卡驱动和CUDA安装相对较容易，可以自行寻找教程</p>
</blockquote>
<h3 id="创建-conda-环境">创建 Conda 环境</h3>
<p><a href="https://blog.csdn.net/sunyuhua_keyboard/article/details/143056185" target="_blank" rel="noopener noreffer ">miniconda安装指南</a></p>
<p>使用 Python 3.8 以获得最佳兼容性。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">conda create -n legged_gym python=3.8
</span></span><span class="line"><span class="cl">conda activate legged_gym</span></span></code></pre></div></div>
<h3 id="安装-pytorch-113">安装 PyTorch 1.13</h3>
<p>为了适配 Isaac Gym 且不引发 CUDA 兼容问题，使用以下特定版本：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">torch</span><span class="o">==</span><span class="mf">1.13</span><span class="o">.</span><span class="mi">1</span><span class="o">+</span><span class="n">cu117</span> <span class="n">torchvision</span><span class="o">==</span><span class="mf">0.14</span><span class="o">.</span><span class="mi">1</span><span class="o">+</span><span class="n">cu117</span> <span class="n">torchaudio</span><span class="o">==</span><span class="mf">0.13</span><span class="o">.</span><span class="mi">1</span> <span class="o">--</span><span class="n">extra</span><span class="o">-</span><span class="n">index</span><span class="o">-</span><span class="n">url</span> <span class="p">[</span><span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">download</span><span class="o">.</span><span class="n">pytorch</span><span class="o">.</span><span class="n">org</span><span class="o">/</span><span class="n">whl</span><span class="o">/</span><span class="n">cu117</span><span class="p">](</span><span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">download</span><span class="o">.</span><span class="n">pytorch</span><span class="o">.</span><span class="n">org</span><span class="o">/</span><span class="n">whl</span><span class="o">/</span><span class="n">cu117</span><span class="p">)</span></span></span></code></pre></div></div>
<p><strong>验证安装</strong>： 进入 Python 输入 <code>import torch; print(torch.cuda.is_available())</code> 应返回 <code>True</code>。</p>
<h2 id="安装-isaac-gym-模拟器">安装 Isaac Gym (模拟器)</h2>
<p><a href="https://developer.nvidia.com/isaac-gym/download" target="_blank" rel="noopener noreffer ">Isaac Gym下载地址</a></p>
<h3 id="解压与安装">解压与安装</h3>
<p>建议建立专门的工作目录（如 <code>~/workspace</code>），不要在 <code>Downloads</code> 或 <code>/tmp</code> 下安装。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># 假设压缩包已在当前目录
</span></span><span class="line"><span class="cl">tar -xf IsaacGym_Preview_4_Package.tar.gz
</span></span><span class="line"><span class="cl">cd isaacgym/python
</span></span><span class="line"><span class="cl">pip install -e .</span></span></code></pre></div></div>
<h3 id="解决-ubuntu-2204-核心兼容问题-critical">解决 Ubuntu 22.04 核心兼容问题 (Critical)</h3>
<p><strong>现象</strong>：运行示例时报错 <code>ImportError: libpython3.8.so.1.0: cannot open shared object file</code>。 <strong>原因</strong>：系统找不到 Conda 环境内的动态链接库。 <strong>错误尝试</strong>：不要将路径直接写入 <code>~/.bashrc</code>，这会导致系统命令（如 <code>nano</code>, <code>ls</code>）因库冲突而发生“段错误”或崩溃。</p>
<p><strong>正确解决方案</strong>： 在运行时动态指定环境变量。后续我们将通过启动脚本 (<code>train.sh</code>) 来一劳永逸地解决此问题。</p>
<h2 id="安装-rsl_rl-算法库">安装 RSL_RL (算法库)</h2>
<p><a href="https://github.com/leggedrobotics/rsl_rl#" target="_blank" rel="noopener noreffer ">RSL_RL仓库地址</a></p>
<p>这是配置中最容易出错的环节。官方最新版 (<code>master</code> 分支) 适配了新版 PyTorch 2.x，会导致依赖冲突（如 <code>ModuleNotFoundError: tensordict</code> 或 <code>pyproject.toml</code> 格式错误）。</p>
<p><strong>解决方案：强制降级到 v1.0.2 版本。</strong></p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">cd ~/workspace  # 回到工作根目录
</span></span><span class="line"><span class="cl">git clone [https://github.com/leggedrobotics/rsl_rl](https://github.com/leggedrobotics/rsl_rl)
</span></span><span class="line"><span class="cl">cd rsl_rl
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 切换到稳定旧版本
</span></span><span class="line"><span class="cl">git checkout v1.0.2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 安装
</span></span><span class="line"><span class="cl">pip install -e .</span></span></code></pre></div></div>
<h2 id="安装-legged-gym-训练环境">安装 Legged Gym (训练环境)</h2>
<p><a href="https://github.com/leggedrobotics/legged_gym" target="_blank" rel="noopener noreffer ">legged_gym仓库</a></p>
<h3 id="安装本体">安装本体</h3>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">cd ~/workspace
</span></span><span class="line"><span class="cl">git clone [https://github.com/leggedrobotics/legged_gym](https://github.com/leggedrobotics/legged_gym)
</span></span><span class="line"><span class="cl">cd legged_gym
</span></span><span class="line"><span class="cl">pip install -e .</span></span></code></pre></div></div>
<h3 id="解决-numpy-版本冲突">解决 NumPy 版本冲突</h3>
<p><strong>现象</strong>：运行时报错 <code>AttributeError: module 'numpy' has no attribute 'float'</code>。 <strong>原因</strong>：Isaac Gym 依赖旧版 NumPy，而新版（1.24+）废弃了部分属性。 <strong>解决</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">pip install &#34;numpy&lt;1.24&#34;</span></span></code></pre></div></div>
<h3 id="补充缺失工具">补充缺失工具</h3>
<p>如果不安装 Tensorboard，训练时会报错。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">pip install tensorboard</span></span></code></pre></div></div>
<h2 id="运行与显存优化">运行与显存优化</h2>
<h3 id="创建通用启动脚本-推荐">创建通用启动脚本 (推荐)</h3>
<p>为了解决 <code>libpython</code> 路径问题，且避免使用不稳定的 <code>alias</code>，在 <code>legged_gym/scripts</code> 目录下创建 <code>train.sh</code>。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">cd</span> <span class="n">legged_gym</span><span class="o">/</span><span class="n">scripts</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 写入脚本内容</span>
</span></span><span class="line"><span class="cl"><span class="n">echo</span> <span class="s1">&#39;#!/bin/bash&#39;</span> <span class="o">&gt;</span> <span class="n">train</span><span class="o">.</span><span class="n">sh</span>
</span></span><span class="line"><span class="cl"><span class="n">echo</span> <span class="s1">&#39;export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib&#39;</span> <span class="o">&gt;&gt;</span> <span class="n">train</span><span class="o">.</span><span class="n">sh</span>
</span></span><span class="line"><span class="cl"><span class="n">echo</span> <span class="s1">&#39;python train.py &#34;$@&#34;&#39;</span> <span class="o">&gt;&gt;</span> <span class="n">train</span><span class="o">.</span><span class="n">sh</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 赋予权限</span>
</span></span><span class="line"><span class="cl"><span class="n">chmod</span> <span class="o">+</span><span class="n">x</span> <span class="n">train</span><span class="o">.</span><span class="n">sh</span></span></span></code></pre></div></div>
<h3 id="解决显存溢出-cuda-oom">解决显存溢出 (CUDA OOM)</h3>
<p><strong>现象</strong>：报错 <code>RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED</code>。 <strong>原因</strong>：默认配置开启 4096 个环境并行，显存需求大（需 8G+）。 <strong>解决</strong>： 修改 <code>legged_gym/envs/a1/a1_config.py</code> (或对应机器人的 config)。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">class A1RoughCfg( LeggedRobotCfg ):
</span></span><span class="line"><span class="cl">    class env( LeggedRobotCfg.env ):
</span></span><span class="line"><span class="cl">        num_envs = 1024  # &lt;--- 将 4096 改为 1024 或 512</span></span></code></pre></div></div>
<h3 id="启动训练">启动训练</h3>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">./train.sh --task=a1</span></span></code></pre></div></div>
<h2 id="常见报错速查表">常见报错速查表</h2>
<table>
  <thead>
      <tr>
          <th>错误特征</th>
          <th>原因</th>
          <th>解决方法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ImportError: libpython3.8.so.1.0</code></td>
          <td>找不到 Python 动态库</td>
          <td>使用 <code>LD_LIBRARY_PATH</code> 脚本启动 (见第五步)</td>
      </tr>
      <tr>
          <td><code>nano</code> 打开时报 <code>段错误</code></td>
          <td><code>.bashrc</code> 中污染了全局库路径</td>
          <td>清除 <code>.bashrc</code> 中的 <code>LD_LIBRARY_PATH</code> 设置</td>
      </tr>
      <tr>
          <td><code>ModuleNotFoundError: tensordict</code></td>
          <td><code>rsl_rl</code> 版本过新</td>
          <td>切换 <code>rsl_rl</code> 到 <code>v1.0.2</code> 标签</td>
      </tr>
      <tr>
          <td><code>toml</code> 解析错误 / <code>license</code> 格式错误</td>
          <td><code>rsl_rl</code> 配置文件兼容性差</td>
          <td>切换 <code>rsl_rl</code> 到 <code>v1.0.2</code> 标签</td>
      </tr>
      <tr>
          <td><code>AttributeError: ... no attribute 'float'</code></td>
          <td>NumPy 版本过高</td>
          <td><code>pip install &quot;numpy&lt;1.24&quot;</code></td>
      </tr>
      <tr>
          <td><code>CUDA error: CUBLAS_STATUS_ALLOC_FAILED</code></td>
          <td>显存不足</td>
          <td>减小配置文件中的 <code>num_envs</code></td>
      </tr>
      <tr>
          <td><code>ModuleNotFoundError: tensorboard</code></td>
          <td>缺少依赖包</td>
          <td><code>pip install tensorboard</code></td>
      </tr>
  </tbody>
</table>
]]></description></item><item><title>我们为什么不再充满热情</title><link>https://www.qinshiyue.icu/p/%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%86%8D%E5%85%85%E6%BB%A1%E7%83%AD%E6%83%85/</link><pubDate>Sun, 26 Oct 2025 02:37:01 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%86%8D%E5%85%85%E6%BB%A1%E7%83%AD%E6%83%85/</guid><description><![CDATA[<div class="featured-image">
                <img src="/image.png" referrerpolicy="no-referrer">
            </div><hr>
<p>   习惯是一种可怕的力量。它会给一个人留下刻骨的痕迹，又会将这些痕迹磨灭，直到所有经历都飘散在风中，将一个人变得面目全非，就像忒修斯之船。过去之我，今日之我，未来之我，我们终将是三个互不相交的个体。今日之我不必悔恨过去的选择，未来之我的担子也不该跨过时空压到今日之我的身上。</p>
<p>   恍惚间，我已经习惯了孤独。林语堂说孤独是稚儿擎瓜柳蓬下，细犬逐蝶深巷中。在热闹之中，但是在热闹之外，就是孤独。从什么时候开始的呢，大抵是在高中吧。因为些什么呢，大抵是些情感吧。一些傲娇，一些矫情，再加上一些别扭，青春期啊就是这么奇妙，不是吗。</p>
<p>   细碎的感情线杂乱无章地散步在时间织成的毛衣上，拍不掉，理不清，思不尽。花季少男少女的内心并不如青春文学中描写得那般纯粹，当然更没有日本动漫中展示得那种可爱。长大后的人们似乎都在用时间的滤镜看待过去，扭曲和掩盖了一些黑暗面。所以思来想去，我还是先不剖析一些难以启齿的龌龊想法，以免未来的我不肯原谅现在的我。</p>
<p>   那么对于过去的我，我当然不会释然。有太多太多的遗憾，是他一手酿成的。或许他可以早点开窍不要那么幼稚，或许他可以克制一些不要那么冲动，或许他可以深思熟虑一些不要那么想当然，或许他可以在某个夜晚有耐心一些，或许他可以在面对某个人时更坦白一些，嘴不要那么笨，又或许他可以高考时再多考几分让我现在更轻松一些&hellip;&hellip;人生啊似乎就是由遗憾构成的，大大小小的遗憾围成了大大小小的圈，我们身在圈里，向往被挡住的可能性。然而又能怎么样呢。既然都在说不要美化未走过的那条路，那我只能原谅过去的我了。希望未来的我你也不要怪罪现在的我，有些事就让它失败吧，有些人就让ta错过吧，毕竟你无法穿越回来揍我一顿，那就让我的懦弱和拧巴肆无忌惮地绽放吧。</p>
<p>   那么现在呢，步入大学之后的第三年，成人之后的第三年，我已飘然孤立于人群之外，用刻薄挑剔的眼光观察着世间庸人。当然，也叫不合群，孤僻，自以为是。我并不吝以最恶毒的词汇描述我自己，因为我深知我是怎样的人。当然还因为我内心充满了骄傲，这让自辱变成了一种自我衬托。极度自信又自负的人，强大而脆弱，但归根结底是脆弱的，或许前方某一时刻，就有天降的钢钎准备穿过我的胸膛。</p>
<p>   写到这里似乎有些跑题，但是看到这里，你还认为我会充满热情吗。至于为什么不再充满热情，可能只能怪这个阴的没边的世界了。</p>
<p>   我在无人但不寂静的深夜写下这些文字。因为有舍友翻来覆去哼哼唧唧，有舍友满身搔痒像是爬满了虱子，楼道外有哗哗的流水声，还有不知从哪间宿舍传来的狗叫声。所以我祝他们早日超生，世界早点毁灭。
那么，晚安💤。</p>

<hr>
]]></description></item><item><title>囚笼，以及那个超越逻辑的吻</title><link>https://www.qinshiyue.icu/p/%E5%9B%9A%E7%AC%BC%E4%BB%A5%E5%8F%8A%E9%82%A3%E4%B8%AA%E8%B6%85%E8%B6%8A%E9%80%BB%E8%BE%91%E7%9A%84%E5%90%BB/</link><pubDate>Sat, 18 Oct 2025 23:19:56 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E5%9B%9A%E7%AC%BC%E4%BB%A5%E5%8F%8A%E9%82%A3%E4%B8%AA%E8%B6%85%E8%B6%8A%E9%80%BB%E8%BE%91%E7%9A%84%E5%90%BB/</guid><description><![CDATA[<div class="featured-image">
                <img src="/als.jpg" referrerpolicy="no-referrer">
            </div><hr>
<p>读完《卡拉马佐夫兄弟》许久，许多人影都已模糊，唯有伊凡的身影始终清晰。</p>
<p>他是另一个拉斯柯尔尼科夫，是那种我总能在身上看到自己影子的同一种人。他们都习惯用理性解剖一切，却最终把刀刃对准了自己。这种精神困境，根植于智识上的骄傲——一种最终会为自己建起无形囚笼的骄傲。</p>
<p>而书中那篇石破天惊的《宗教大法官》，便是这种思想的凝练。</p>
<hr>
<blockquote>
<p><em>“如果说，为了赎买真理，必须让孩子们也一同受苦，让他们的眼泪也汇入那苦难的总和，那么我预先声明，这真理根本不值这个价。…… 我不要和谐，我是出于对人类的爱才不要它。我宁愿守着那未获报复的苦难。…… 因此，我赶紧退还我的入场券。…… 我不是不接受上帝，阿辽沙，我只是最恭敬地把入场券还给他。”</em></p>
</blockquote>
<p>伊凡通过《宗教大法官》这则寓言，构建了他的思想核心。故事发生在宗教裁判最严酷的西班牙，耶稣重返人间，施行神迹，却被一位年迈的大法官下令逮捕。在地牢里，面对始终沉默的耶稣，大法官展开了一场振聋发聩的单方面审判。</p>
<p>大法官的逻辑冷酷而清晰：自由，是耶稣赠予人类的根本性错误。因为人的本质是软弱卑贱的，他们无法承受自由选择的重负，他们不需要虚无缥缈的天上面包，而只要实实在在的地上食粮。因此，教会必须“修正”神的错误，收回自由，并代之以<strong>奇迹、神秘和权威</strong>。用这三者，为民众建造一个幸福安稳的蚁穴，让他们交出思考的权利，换取温饱与顺从的一生。</p>
<blockquote>
<p><em>“哦，我们将会说服他们，他们只有把自由交给我们、顺从我们的时候，才会得到真正的自由。…… 我们将给予他们那种温顺的、谦卑的幸福，那种适合于他们这种天生软弱的生物的幸福。…… 他们将惊奇地望着我们，把我们看作神，因为我们既然肯领导他们，就说明我们甘愿为他们忍受他们所畏惧的自由，甘愿统治他们。”</em></p>
</blockquote>
<p>这位大法官，实际上就是伊凡精神的化身。他的全部理论，都源于一个无比骄傲的前提：我，比上帝更懂人性。他用理性剖析一切，得出的结论是：为了绝大多数人的幸福，思考和统治的重负必须由少数精英承担。这便是智识的骄傲。当讲述这个故事时，伊凡正站在自己思想囚笼的顶端，俯瞰着那些他既怜悯又鄙视的芸芸众生。</p>
<p>这座看似坚固的囚笼，实则建立在虚无之上。它的根基，源于一个致命的谬误：为思想的优越性排序。</p>
<p>伊凡的根本问题，在于他坚信思想有高低之分，活法有贵贱之别。然而事实是，思想无所谓高度，人只是活着。用自己的尺度去衡量整个世界，或许正是知识分子永恒的诅咒。而现实，则以最残酷的方式嘲弄了伊凡的骄傲。这嘲弄并非来自更高明的哲学家，而是他最鄙视的私生子斯麦尔佳科夫。斯麦尔佳科夫忠实地理解并执行了伊凡无神论的终极结论——“一切都是允许的”，于是他动手杀死了父亲。</p>
<blockquote>
<p><em>“您才是真正的凶手，我不过是您的工具，是您忠实的仆人，我只是奉了您的意旨办事。”</em></p>
</blockquote>
<p>宏大的哲学思辨，最终沦为了卑劣的弑父借口。当斯麦尔佳科夫对伊凡说出“您才是真正的凶手”时，那座思想的囚笼便从内部被彻底击碎了。他无法忍受自己深刻的思想，竟结出如此肮脏的果实。击垮伊凡的，从来不是更强的逻辑，而是一个他无法否认的、肮脏的事实。他的精神崩溃，也证明了纯粹的理性在人性的混乱面前，终将破产。</p>
<p>大法官说完了。他将自己的骄傲、孤独和罪孽，全部倾泻在那个沉默的囚徒面前，等待着反驳与审判。</p>
<p>但他等来的，不是辩驳，而是一个吻。耶稣走上前，在他那干瘪衰老的唇上，轻轻印了一下。</p>
<p>这个吻，不代表认同，也无关辩论，它是一种超越了所有逻辑的悲悯。它仿佛在说：我看见了你的痛苦，也理解你的挣扎。我不评判你的思想，我只爱你这个受苦的人。</p>
<p>神爱世人，所以他也爱大法官，爱伊凡。在神的眼中，人的思想并无高下之分，大法官自以为是的深刻，与民众浑浑噩噩的幸福，不过都是人的活法而已。这才是对伊凡那座思想囚笼的最终回答：出路，不在于构建一个更完美的逻辑体系，而在于走出逻辑本身，去承认世界的复杂，接受人性的混沌，拥抱一种无条件的、不问对错的爱。</p>
<p>伊凡构建了宏大的理论，却始终无法理解这个简单的吻。所以，他只能被困在自己的囚笼里，走向最后的崩溃。</p>
<blockquote>
<p><em>“你这是要去完成一桩英雄壮举，可是你又不信奉壮举，这就是你的痛苦和愤怒的所在，这就是你满怀怨恨的原因。”</em></p>
</blockquote>
<p>伊凡死了吗？没有。他活在我们中间，甚至就活在我们某些幽暗的角落里。</p>
<p>我们每一个试图用理性丈量世界的人，内心深处都可能藏着一个伊凡。我们用知识构建壁垒，用逻辑审判情感，用自以为是的深刻，去疏远真实的生活。我们看透了世界的荒谬，却也因此失去了拥抱世界的能力，最终把自己关进头脑的囚笼，奇怪于世界的冰冷。</p>
<p>走出头脑，去拥抱一个具体的人；
去做一件无用之事；
去爱一个不完美的人，连同他的愚蠢和不堪。</p>
<p>这或许，才是对所有伊凡们的最终救赎。</p>
]]></description></item><item><title>机器学习笔记(6)：神经网络</title><link>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B06%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/</link><pubDate>Mon, 18 Aug 2025 10:40:32 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B06%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/</guid><description><![CDATA[<div class="featured-image">
                <img src="/img.jpg" referrerpolicy="no-referrer">
            </div><hr>
<h2 id="前言">前言</h2>
<p>   神经网络作为人工智能领域的核心分支，是一种旨在模拟生物神经系统结构与功能的计算范式。其根本目标在于构建能够从经验数据中自动学习复杂模式与潜在规律的计算模型，这标志着从传统基于规则的编程向数据驱动的自适应智能的根本性转变。<br>    神经网络的发展历程是理论突破与技术瓶颈交织的非线性过程。其理论基础可追溯至20世纪40年代 McCulloch 与 Pitts 对神经元的数学形式化，以及50年代 Rosenblatt 提出的“感知机”模型，这些早期工作奠定了连接主义（Connectionism）的基础。然而，由于 Minsky 与 Papert 在1969年深刻揭示了单层感知机的理论局限性（如无法解决XOR问题），该领域的研究一度陷入停滞。直至80年代，随着多层感知机及反向传播（Backpropagation）算法的重新发现与推广，神经网络才得以克服早期瓶颈，具备了表达非线性复杂函数的能力。尽管如此，诸如“梯度消失”等深度网络训练难题，使其在20世纪末的声誉再次被支持向量机（SVM）等统计学习方法所超越。<br>    进入21世纪，神经网络迎来了决定性的复兴。这一复兴并非源于单一突破，而是三大因素协同作用的结果：第一，以图形处理器（GPU）为代表的并行计算硬件提供了前所未有的算力支持；第二，互联网的普及带来了海量、高质量的标注数据集（Big Data）；第三，算法层面的持续创新，包括新型激活函数（如ReLU）、优化策略及正则化技术，有效缓解了深度网络的训练困境。自2012年 AlexNet 在 ImageNet 竞赛中取得革命性成果以来，以深度神经网络（DNN）为核心的深度学习已成为人工智能研究与应用的主流范式。<br>    理解神经网络的意义至关重要，它不仅是实现图像识别、自然语言处理、语音识别等应用任务的关键技术，更代表了一种强大的特征表示学习（Representation Learning）框架。它驱动了从消费电子到自动驾驶、从金融科技到医疗诊断等众多领域的重大技术革新。因此，掌握神经网络的理论基础与实践方法，是理解并参与当前人工智能技术发展的核心要求。</p>
<hr>
<h2 id="神经元模型">神经元模型</h2>
<p>   各学科对神经网络的定义多种多样，而使用得最广泛的一种，即 <strong>“神经网络是由具有适应性的简单单元组成的广泛并行互连的网络，它的组织能够模拟生物神经系统对真实世界物体所作出的交互反应”</strong> 。</p>
<p>   神经网络中最基本的成分是 <strong>神经元(neuron)模型</strong> ，即上述定义中的“简单单元”。在机器学习中，神经元模型也被称为 <strong>“M-P神经元模型”</strong>（以其提出者McCulloch和Pitts的名字命名）。它是一个简化的数学模型，用来模拟生物神经元接受和处理信息的过程。每个神经元接收多个输入，对这些输入进行加权计算，然后通过一个“激活函数”来决定最终的输出。一个神经元主要由以下几个部分组成：</p>
<ul>
<li><strong>输入(Inputs, $x_i$)</strong> ：神经元来自其他神经元或外部数据源的信息。一个神经元可以有多个输入。</li>
<li><strong>权重(Weights. $w_i$)</strong> ：每一个输入都对应一个权重。权重代表了这个输入的重要性。权重越高，说明这个输入对神经元的最终输出影响越大。 <strong>在模型训练过程中，机器主要就是学习和调整这些权重值</strong> 。</li>
<li><strong>阈值(Threshold, $\theta$)</strong> ：神经元被激活所需要达到的内部标准或门槛。</li>
<li><strong>求和函数 (Summation Function)</strong>：神经元会将所有的输入值与它们对应的权重相乘，然后将所有结果加起来，最后再减去阈值项。这个过程叫做“加权求和”。</li>
<li><strong>激活函数(Activation Function, $f$)</strong>：这是神经元的“决策”部分。加权求和的结果会经过一个激活函数处理，最终得到这个神经元的输出。激活函数的主要作用是加入 <strong>非线性</strong> 因素，使得神经网络能够学习和拟合更复杂的模式。如果没有激活函数，那无论多少层的神经网络，最终都只是一个线性模型。</li>
<li><strong>输出 (Output, $y$)</strong>： 经过激活函数处理后得到最终的输出值。这个值会作为下一个神经元的输入，或者直接作为整个网络的最终结果。</li>
</ul>
<figure>
    
    <fitgcaption>M-P 神经元模型  图源《机器学习》[周志华]</fitgcaption>
</figure>
<p>   神经元的工作可以概括为两步：</p>
<ol>
<li>
<p><strong>计算加权和</strong>：将所有输入与其对应的权重相乘后求和，得到输入信号的总强度</p>
$$
   net = \sum_{i=1}^{n} w_i x_i
   $$</li>
<li>
<p><strong>激活判断与输出</strong>：激活函数判断“加权和”是否超过“阈值”，并产生输出</p>
$$
   y = f(net - \theta) = f(\sum_{i=1}^{n} w_i x_i - \theta)
   $$</li>
</ol>
<p>理想的激活函数是如下的阶跃函数，它将输入值映射为输出值“0”或“1”：</p>
$$
\text{sgn}(x) = 
\begin{cases}
1, \ \ x \geqslant 0; \\
0, \ \ x \leqslant 0
\end{cases}
$$<p>
显然“1”对应于神经元兴奋，“0”对应于神经元抑制。然而，阶跃函数具有不连续、不光滑等不太好的性质，因此实际常用 Sigmoid 函数作为激活函数：</p>
$$
\text{sigmoid}(x) = \frac{1}{1 + e^{-x}}
$$<p>它把可能在较大范围内变化的输入值挤压到 $(0, 1)$ 输出值范围内，因此有时也称为“挤压函数”(squashing function)。</p>
<figure>
    
    <figcaption>典型的神经元激活函数</figcaption>
</figure>
<h2 id="感知机与多层网络">感知机与多层网络</h2>
<p>   <strong>感知机(Perceptron)</strong> 是神经元模型的一个实例，由两层神经元组成，输入层接收外接输入信号后传递给输出层，输出层是 M-P 神经元，亦称 <strong>“阈值逻辑单元”(threshold logic unit)</strong> 。</p>
<figure> 
    
    <figcaption>感知机网络结构示意图</figcaption>
</figure>
<p>   注意到如图 $y = f(\sum_i w_ix_i - \theta) = f(w_1 x_1 + w_2 x_2 - \theta)$ ，给定训练数据后，权重 $w_i$ 和阈值 $\theta$ 可以通过学习得到。阈值 $\theta$ 可以看做一个固定输入为 $-1.0$ 的 <strong>“哑结点”(dummy node)</strong> 所对应的连接权重 $w_{n+1}$ 。感知机的学习规则如下</p>
<ol>
<li>
<p>对训练样例 $(\boldsymbol{x}, y)$ ，假设当前感知机的输出为 $\hat y$ ；</p>
</li>
<li>
<p>感知机的权重作如下调整：</p>
$$
   w_i \leftarrow w_i + \Delta w_i ,\\
   \Delta w_i = \eta (y - \hat y)x_i
   $$<p>
其中 $\eta \in (0,1)$ 称为学习率(learning rate)。</p>
</li>
</ol>
<p>可以看出，若感知机对训练样例预测正确，即 $y = \hat y$ ，则不发生变化，否则将根据错误的程度进行权重调整。</p>
<p>   值得注意的是，感知机能实现与、或、非运算，例如：</p>
<ul>
<li>令 $w_1=w_2=1, \theta=2$ ，则 $y = f(1 \cdot x_1 + 1 \cdot x_2 - 2)$ ，仅在 $x_1=x_2=1$ 时，$y=1$ ；</li>
<li>令 $w_1=w_2=1, \theta=0.5$ ，则 $y = f(1 \cdot x_1 + 1 \cdot x_2 - 0.5)$ ，当 $x_1=1$ 或 $x_2=1$ 时，$y=1$ ；</li>
<li>令 $w_1=-0.6, w_2=0, \theta=-0.5$ ，则 $y = f(-0.6 \cdot x_1 + 0 \cdot x_2 +0.5)$ ，当 $x_1 = 1$ 时，$y = 0$ ，当$x_1 = 0$ 时，$y=1$ 。</li>
</ul>
<p>能够实现以上运算的本质原因是与、或、非问题都是线性可分问题，即存在一个线性超平面将它们分开，此时感知机的学习过程一定会收敛，从而求得合适的权重向量。而对于非线性可分问题，感知机学习过程会发生 <strong>振荡(fluctuation)</strong> ，不能求得合适的解，比如异或问题。</p>
<figure>
    
    <figcaption>线性可分问题与非线性可分问题</figcaption>
</figure>
<p>   多层网络通过增加 <strong>”隐藏层“(hidden layer)</strong> 来解决该问题，其核心在于两个方面：</p>
<ul>
<li><strong>功能组合</strong>：复杂问题可以分解为简单问题的组合。比如异或(XOR)的逻辑可以分解为<code>OR AND NAND</code>。从而可以通过两层神经元来拟合出异或的逻辑。</li>
<li><strong>线性变换</strong>：这是多层网络的本质，即神经网络的每一层都在对数据进行一次空间变换。隐藏层接收原始坐标，通过线性变换将原始坐标系变换到新的特征空间，原始的非线性可分数据被重新排列，从而变得线性可分。</li>
</ul>
<figure>
    
    <figcaption>能解决异或问题的两层感知机</figcaption>
</figure>
<p>   更一般的结构称为 <strong>“多层前馈神经网络”(multi-layer feedforward neural networks)</strong> ，每层神经元与下一层神经元全互连，神经元之间不存在同层连接或跨层连接。其中输入层神经元接收外界输入，隐藏层与输出层神经元对信号进行加工，最后由输出层神经元输出。神经网络的学习过程，就是  <strong>根据训练数据来调整神经元之间的“连接权”以及每个功能神经元的阈值；换言之，神经网络“学”到的东西，蕴涵在连接权与阈值中</strong> 。</p>
<figure>
    
    <figcaption>多层前馈神经网络结构示意</figcaption>
</figure>
<h2 id="误差逆传播算法">误差逆传播算法</h2>
<p>   假设我们有一个多层网络，其中有大量的权重( $w$ ) 和阈值( $\theta$ )。这些参数的初始值通常是随机设置的，我们的目标就是找到一套最佳参数，使得网络对于任何给定的输入，其输出都能 <strong>尽可能地接近真实值</strong> 。</p>
<p>   首先需要一个量化的指标来衡量网络输出值与( $\hat y$ )真实值( $y$ )之间的差距，即 <strong>损失函数</strong> 。常用的损失函数是均方误差。对于单个样本，其损失 $E$ 可以表示为：</p>
$$
E = \frac{1}{2}(\hat y - y)^2
$$<blockquote>
<p><em>注：前面的 $\frac{1}{2}$ 是为了后续求导计算方便，约掉一个系数2，对优化目标没有影响。</em></p>
</blockquote>
<p>我们的目标，就是通过调整网络中的所有权重和阈值，来让损失的值变得尽可能小。</p>
<blockquote>
<p>这里需要注意，<strong>标准 BP 算法</strong> 的更新规则是基于单个样本的 $E_k$ 推导而得，如果以累积误差来推导，最终得到 <strong>累积 BP 算法</strong> ，即 <strong>累积误差逆传播(accumulated error backpropagation)算法</strong> 。二者侧重点不同，前者更新频率较快，适合训练集较大的情况，后者更新频率较慢，适合训练集不那么大的情况。下面的推导以累积误差为例。</p>
</blockquote>
<p>   为了实现该目标，我们常使用 <strong>梯度下降</strong> 的方法。给定一个初始值，要找到损失最小的地方，最直观的方法是向减小最快的方向移动，在数学上就是 <strong>负梯度</strong> 的方向，而移动的距离由学习率来控制：</p>
$$
w_{new} = w_{old} - \eta \frac{\partial E}{\partial w_{old}}
$$<p>
这个公式意味着：计算出损失对当前权重的梯度（最速上升方向），然后乘以一个学习率（步长），从当前权重中减去这个值，得到新的、更好的权重。</p>
<p>   <strong>误差逆传播(Backpropagation, BP)算法</strong> ，本质上是一种高效计算梯度的技巧，其数学基础是微积分中的链式法则。</p>
<p>   BP 算法的执行可分为两个阶段：</p>
<p><strong>阶段一：前向传播(Forward Propagation)</strong></p>
<ol>
<li>从输入层开始，将训练数据输入网络；</li>
<li>信号逐层向前传递，计算每一层神经元的净输入和激活后的输出值；</li>
<li>最终在输出层得到网络的预测值 $\hat y$ ；</li>
<li>根据预测值 $\hat y$ 和真实值 $y$ ，计算出总损失 $E$ 。</li>
</ol>
<p>这个过程是为了得到“结果”和“差距”。</p>
<p><strong>阶段二：反向传播(Backward Propagation)</strong></p>
<p>   先定义一个三层神经网络的结构和符号：</p>
<ul>
<li><strong>输入层</strong> (索引为 $i$) -&gt; <strong>隐藏层</strong> (索引为 $j$) -&gt; <strong>输出层</strong> (索引为 $k$)；</li>
<li>$w_{ij}$：从输入层神经元 $i$ 到隐藏层神经元 $j$ 的权重；</li>
<li>$w_{jk}$：从隐藏层神经元 $j$ 到输出层神经元 $k$ 的权重；</li>
<li>$y_j$：隐藏层神经元 $j$ 的输出值；</li>
<li>$y_k$：输出层神经元 $k$ 的输出值（即预测值 $\hat y$ ）；</li>
<li>$t_k$：输出层神经元 $k$ 对应的真实目标值；</li>
<li>$net_j = \sum_i w_{ij}x_i$：隐藏层神经元 $j$ 的加权输入和（为简化，暂时忽略阈值）；</li>
<li>$net_k = \sum_j w_{jk}y_j$：输出层神经元 $k$ 的加权输入和；</li>
<li>$y_j = f(net_j)$：$net_j$ 经过激活函数 $f$ 后的输出；</li>
<li>$E = \frac{1}{2} \sum_k (t_k - y_k)^2$：对单个输出神经元的均方误差损失。</li>
</ul>
<p><strong>首先计算输出层权重的梯度</strong>  $\frac{\partial E}{\partial w_{jk}}$ ，我们想知道改变隐藏层到输出层的权重 $w_{jk}$ 会对最终的损失 $E$ 造成多大影响，根据链式法则，这个影响可以分为两步：</p>
<ol>
<li>权重 $w_{jk}$ 的改变，会引起输出神经元 $k$ 的 <strong>加权输入和</strong> $net_k$ 的改变；</li>
<li>$net_k$ 的改变，会引起最终损失 $E$ 的改变。</li>
</ol>
<p>因此可以写出：</p>
$$
\frac{\partial E}{ \partial w_{jk}} = \frac{\partial E}{\partial net_k} \frac{\partial net_k}{\partial w_{jk}}
$$<p>
因为 $net_k = \sum_{j^{'}} w_{j^{'}k}y_{j^{'}}$ ，显然有</p>
$$
\frac{\partial E}{\partial w_{jk}} = y_j
$$<p>
而 $y_k = f(net_k)$ ，则</p>
$$
\frac{\partial E}{\partial net_k} = \frac{\partial E}{\partial y_k} \frac{\partial y_k}{\partial net_k}
$$<ul>
<li>$\frac{\partial E}{\partial y_k} = -(t_k - y_k)$</li>
<li>$\frac{\partial y_k}{\partial net_k} = f'(net_k)$</li>
</ul>
<p>从而得到：</p>
$$
\frac{\partial E}{\partial net_k} = -(t_k - y_k) f'(net_k)
$$<p>
将这一整项定义为 <strong>输出层误差项</strong> $\delta_k$：</p>
$$
\delta_k = \frac{\partial E}{\partial net_k} = -(t_k - y_k) f'(net_k)
$$<p>
则输出层权重的最终梯度为：</p>
$$
\frac{\partial E}{\partial w_{jk}} = \delta_k y_j
$$<p>
<strong>然后计算隐藏层权重的梯度</strong> $\frac{\partial E}{\partial w_{ij}}$，同理，用链式法则展开为：</p>
$$
\frac{\partial E}{\partial w_{ij}} = \frac{\partial E}{\partial net_j} \frac{\partial net_j}{\partial w_{ij}}
$$<p>
由于 $net_j = \sum_{i^{'}} w_{i^{'}j}x_{i^{'}}$ ，则</p>
$$
\frac{\partial net_j}{\partial w_{ij}} = x_i
$$<p>
而 $net_j$ 通过 $y_j = f(net_j)$ 影响损失，但 $y_j$ 会作为所有下一层神经元的输入，所以它的影响会通过多条路径传递到最终的损失 $E$ ：</p>
$$
\frac{\partial E}{\partial net_j} = \frac{\partial E}{\partial y_j} \frac{\partial y_j}{\partial net_j}
$$<ul>
<li>$\frac{\partial y_j}{\partial net_j} = f'(net_j)$</li>
<li>计算 $\frac{\partial E}{\partial y_j}$ 时，需要将 $y_j$ 传递到所有输出层神经元的路径上的影响全部加起来：$\frac{\partial E}{\partial y_j} = \sum_k \left( \frac{\partial E}{\partial net_k} \frac{\partial net_k}{\partial y_j} \right)$ ，我们可以发现 $\frac{\partial E}{\partial net_k}$ 正是第一步中定义的输出层误差项 $\delta_k$ ，而 $\frac{\partial net_k}{\partial y_j}$ 则是连接神经元 $j$ 和 $k$ 的权重 $w_{jk}$ ，所以有$$\frac{\partial E}{\partial y_j} = \sum_k \delta_k w_{jk}$$</li>
</ul>
<p>将最后的乘积定义为 <strong>隐藏层误差项</strong> $\delta_j$：</p>
$$
\delta_j = \frac{\partial E}{\partial net_j} = \left( \sum_k \delta_k w_{jk} \right) f'(net_j)
$$<p>
隐藏层权重的最终梯度为</p>
$$
\frac{\partial E}{\partial w_{ij}} = \delta_j x_i
$$<p>
可以看出，误差是从后向前、层层传递的，因此称为“误差逆传播算法”。这个“前向计算结果 -&gt; 反向传播误差 -&gt; 更新参数”的完整流程，被称为一次<strong>迭代 (Iteration)</strong>。通过成千上万次迭代，网络的参数被不断微调，使得总损失越来越小，网络的预测也越来越准。</p>
<p>   由于其强大的表示能力，BP 神经网络经常遭遇过拟合，训练误差持续降低，但测试误差却可能上升。由两种策略来缓解过拟合：</p>
<ul>
<li>
<p><strong>“早停”(early stopping)</strong> ：将数据分成训练集和验证集，训练集用来计算梯度、更新连接权和阈值，验证集用来估计误差，若训练集误差降低但验证集误差升高，则停止训练，同时返回具有最小验证集误差的连接权和阈值；</p>
</li>
<li>
<p><strong>“正则化”(regularization)</strong> ：其基本思想是在误差目标函数中增加一个用于描述网络复杂度的部分，例如连接权与阈值的平方和，仍令 $E_k$ 表示第 $k$ 个训练样例上的误差，$w_i$ 表示连接权和阈值，则误差目标函数改变为</p>
$$
  E = \lambda \frac{1}{m} \sum_{k=1}^m E_k + (1-\lambda)\sum_i w_i^2
  $$<p>
其中 $\lambda \in (0, 1)$ 用于对经验误差与网络复杂度这两项进行折中，常通过交叉验证法来估计。</p>
</li>
</ul>
<h2 id="全局最小与局部极小">全局最小与局部极小</h2>
<p>   在上一节中，我们优化的目标是找到一组参数，使得损失函数 $E$ 的值达到最小。</p>
<ul>
<li><strong>全局最小值(Global Minimum)</strong> ：这是损失函数在整个参数空间中的<strong>真正最低点</strong>。找到了它，就意味着我们找到了理论上最优的模型解。</li>
<li><strong>局部极小值 (Local Minima)</strong>: 这是损失函数在一个<strong>局部邻域内的最低点</strong>。</li>
</ul>
<figure>
    
    <figcaption>全局最小与局部极小</figcaption>
</figure>
<p>核心问题在于，标准的梯度下降算法本身无法区分停下的地方是局部极小值还是全局最小值，如果陷入了一个比较差的局部极小值，那么无论再怎么训练，模型的效果也无法达到最优化。尤其是神经网络的损失函数是一个在 <strong>极高维度空间</strong> 中（参数动辄成千上万甚至上亿）的、极其复杂的 <strong>非凸函数 (Non-convex Function)</strong> ，这意味着它的“地形”异常复杂，充满了无数的局部极小值、以及比局部极小值更麻烦的 <strong>鞍点 (Saddle Points)</strong> 和 <strong>平坦区域 (Plateaus)</strong> 。因此，在训练过程中，优化算法很容易被“困住”。</p>
<p>   为了解决这个问题，研究者们提出了很多比朴素梯度下降更强大的优化策略。它们的核心思想，都是 <strong>给优化过程增加一些“扰动”或“惯性”</strong> ，帮助它冲出局部极小值的陷阱。</p>
<ol>
<li><strong>随机梯度下降 (Stochastic Gradient Descent, SGD)</strong> 标准的梯度下降在每一步都使用<strong>全部</strong>训练数据来计算梯度，路线平滑但容易陷入局部最小。而SGD在每一步<strong>只随机取一个样本</strong>来计算梯度。这样做会带来大量的噪声，使得下降的路径变得非常“曲折和摇晃”。这种“摇晃”有时反而能帮助算法“震荡”出浅的局部极小值，去寻找更广阔的空间。<strong>小批量梯度下降 (Mini-batch GD)</strong> 是一个折中方案，每次使用一小批数据，既保证了效率，又引入了适度的噪声。</li>
<li><strong>动量 (Momentum)</strong> 这个方法为梯度下降引入了“惯性”的概念。想象一个从山上滚下来的铁球，即使它滚到一个小坑里（局部极小），由于自身的惯性（动量），它很可能会直接冲出这个小坑，继续向下滚动。在数学上，它不仅考虑了当前步骤的梯度，还结合了一定比例的上一步更新的方向。</li>
<li><strong>自适应学习率算法 (Adaptive Learning Rate)</strong> 例如 <strong>Adam</strong>、<strong>RMSprop</strong> 等。这些是目前最主流的优化器。它们不再对所有参数使用固定的学习率 $\eta$ ，而是为每个参数动态地、自适应地调整学习率。在平坦区域，它们可能会加大步长以快速通过；在陡峭的峡谷，它们会减小步长以防“冲过头”。这种精细化的控制，使得它们在复杂地形中（尤其是穿越鞍点时）表现得非常出色。</li>
<li><strong>随机初始化 (Random Initialization)</strong> 既然一次下降可能会走到错误的谷底，那就多尝试几次不同的位置。在实践中，我们会用不同的随机初始值多次训练同一个网络，然后选择在验证集上表现最好的那个模型。</li>
</ol>
<blockquote>
<p><strong>现代观点</strong>：在现代深度学习中，随着网络变得越来越大、参数越来越多，一个有趣的现象是，真正的“坏”的局部极小值问题似乎没有想象中那么严重了。很多研究表明，在高维空间中，大部分局部极小值点的损失与全局最小值相差无几。目前，优化算法面临的更大挑战是如何快速穿越广阔的<strong>鞍点</strong>和<strong>平坦区域</strong>，而动量和自适应学习率算法在应对这些问题上效果显著。</p>
</blockquote>
<h2 id="深度学习">深度学习</h2>
<p>   由于深度学习将来会专门学习，在此只作为一个拓展与总结。</p>
<p>   深度学习的“深”，直截了当地说，就是其核心模型——<strong>神经网络的层数非常多</strong>。相对于只有一两个隐藏层的传统神经网络，深度神经网络（Deep Neural Networks, DNN）拥有成百上千的层次。我们熟知的CNN、RNN等，在现代应用中都属于DNN的范畴。</p>
<p>   我们为何追求“深度”？因为深度赋予了网络一种强大的能力：<strong>层次化特征学习</strong>。网络会自动地从原始数据中学习：</p>
<ul>
<li><strong>底层网络</strong> 学习基础特征（如边缘、颜色）；</li>
<li><strong>中层网络</strong> 组合基础特征，形成复杂特征（如眼睛、纹理）；</li>
<li><strong>高层网络</strong> 组合复杂特征，识别出具体概念（如人脸、汽车）。</li>
</ul>
<p>这种逐层抽象的能力，使得深度网络能够以一种高效的方式理解高度复杂的数据，而这是浅层网络难以企及的。通过多层处理，逐渐将初始的“底层”特征表示转化为“高层”特征表示后，用“简单模型”即可完成复杂的分类等学习任务，因此可将深度学习理解为进行“特征学习”(feature learning)或“表示学习”(representation learning)。</p>
<p>   深度学习是建立在基础神经网络理论之上，由数据、算力和算法共同推动的一场技术革命。它通过构建深度模型来自动学习数据的层次化表示，从而在计算机视觉、自然语言处理等诸多领域取得了前所未有的突破。</p>
<hr>
]]></description></item><item><title>Git 使用初探</title><link>https://www.qinshiyue.icu/p/git-%E4%BD%BF%E7%94%A8%E5%88%9D%E6%8E%A2/</link><pubDate>Tue, 12 Aug 2025 23:08:00 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/git-%E4%BD%BF%E7%94%A8%E5%88%9D%E6%8E%A2/</guid><description><![CDATA[<hr>
<h2 id="引言">引言</h2>
<h3 id="版本控制">版本控制</h3>
<ul>
<li>
<p><strong>版本控制的必要性</strong>：在软件开发、文档撰写等项目中，我们经常需要追踪文件的每一次变更、回溯到历史版本，或与他人协同工作。传统的文件命名方式（如 <code>report_v1.doc</code>, <code>report_v2.doc</code>, <code>report_final.doc</code>）极易导致版本混乱、修改丢失和协作困难。版本控制系统（Version Control System, VCS）正是为解决这些难题而设计的专业工具。</p>
</li>
<li>
<p><strong>版本控制系统的类型</strong>：</p>
<ul>
<li><strong>集中式版本控制系统 (CVCS)</strong>：如 Subversion (SVN)，所有开发者都从一个中央服务器检出文件和提交更新。其缺点是必须联网才能工作，且中央服务器的单点故障风险较高。</li>
<li><strong>分布式版本控制系统 (DVCS)</strong>：如 Git，每个开发者都拥有一个包含完整历史记录的本地仓库。这使得离线工作成为可能，并极大地提升了数据安全性和操作速度。</li>
</ul>
</li>
</ul>
<h3 id="git-系统简介">Git 系统简介</h3>
<ul>
<li>
<p><strong>Git 的起源</strong>：Git 由 Linux 内核的创造者 Linus Torvalds 于 2005 年开发。其初衷是为了更有效地管理庞大且复杂的 Linux 内核代码库，取代当时使用的商业版本控制软件。</p>
</li>
<li>
<p><strong>Git 的核心特性</strong>：</p>
<ul>
<li><strong>速度与效率</strong>：Git 的绝大部分操作都在本地完成，无需网络延迟，因此响应速度极快。</li>
<li><strong>分布式架构</strong>：每个克隆的仓库都是一个完整的备份，这为备份和恢复提供了极大的便利。</li>
<li><strong>强大的分支模型</strong>：Git 的分支创建和合并操作轻量且高效，极大地鼓励了并行开发和功能实验。</li>
<li><strong>数据完整性</strong>：Git 通过 SHA-1 哈希算法确保内容的完整性。所有文件和提交在存储前都会计算校验和，任何损坏或篡改都能被轻易发现。</li>
</ul>
</li>
</ul>
<h3 id="git-与-github">Git 与 GitHub</h3>
<ul>
<li>
<p><strong>工具与平台的关系</strong>：可以理解为，Git 是一个协议或工具，就像电子邮件协议（SMTP）一样；而 GitHub 则是一个基于该工具构建的服务平台，类似于 Gmail。Git 可以在任何地方独立运行，而 GitHub 则是托管 Git 仓库并提供协作功能的网站。</p>
</li>
<li>
<p><strong>GitHub 提供的附加价值</strong>：GitHub 在 Git 的基础上，提供了图形化的用户界面、强大的协作功能（如 Pull Requests）、问题追踪（Issues）、项目管理、代码审查等一系列服务，构建了一个庞大的开发者社区。</p>
</li>
<li>
<p><strong>其他代码托管平台</strong>：除了 GitHub，还有 GitLab、Bitbucket 等类似的服务平台，它们都使用 Git 作为核心技术。</p>
</li>
</ul>
<hr>
<h2 id="环境搭建安装与初始化配置">环境搭建：安装与初始化配置</h2>
<h3 id="git-的安装流程">Git 的安装流程</h3>
<p>由于笔者使用 Windows 系统，暂时只介绍 Windows 平台的安装流程</p>
<h4 id="windows-平台">Windows 平台</h4>
<ol>
<li>访问<a href="https://git-scm.com/downloads/win" target="_blank" rel="noopener noreffer ">git-scm.com</a></li>
<li>根据自己的系统下载相应的版本</li>
<li>运行安装程序</li>
<li>安装完成后，打开“开始”菜单，找到 &ldquo;Git Bash&rdquo; 并运行它，出现一个命令行窗口，即表示安装成功</li>
</ol>
<h3 id="初始化配置">初始化配置</h3>
<p>安装完成后，需要设置用户名称和电子邮件地址。每一次 Git 提交都会使用这些信息，它们会被永久地嵌入到提交记录中。</p>
<p>打开打开 Git Bash（Windows）或终端（macOS/Linux），执行以下两条命令，请将引号内的内容替换为自己的信息。<code>--global</code> 参数表示正在设置全局配置，这台计算机上所有的 Git 仓库都会默认使用这个配置。</p>
<ul>
<li>
<p><strong>配置用户身份标识（用户名）</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">user</span><span class="p">.</span><span class="py">name</span> <span class="s2">&#34;Your Name&#34;</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>配置用户身份标识（邮箱）</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">user</span><span class="p">.</span><span class="py">email</span> <span class="s2">&#34;youremail@example.com&#34;</span></span></span></code></pre></div></div>
<p><em>建议使用在 GitHub 或其他代码托管平台上注册的邮箱。</em></p>
</li>
<li>
<p><strong>验证配置信息</strong>：</p>
<ul>
<li>
<p>要检查配置，可以运行以下命令：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-list</span></span></span></code></pre></div></div>
</li>
<li>
<p>这会列出所有的 Git 配置信息。请确保能看到刚才设置的 <code>user.name</code> 和 <code>user.email</code>。</p>
</li>
</ul>
</li>
</ul>
<h2 id="核心操作与基础工作流">核心操作与基础工作流</h2>
<p>掌握 Git 的关键在于理解其背后的核心概念和基本操作流程。本章将详细介绍 Git 的三个核心区域，并演示从初始化仓库到创建第一个提交的完整过程。</p>
<ul>
<li>三大核心区域：工作区、暂存区与本地仓库
<ul>
<li><strong>工作区 (Working Directory)</strong>：这是在计算机上能直观看到并进行编辑的文件夹。它包含了项目的所有文件，是进行代码编写、修改、添加或删除文件的场所。</li>
<li><strong>暂存区 (Staging Area / Index)</strong>：这是一个位于 <code>.git</code> 目录下的特殊文件，它记录了即将要被提交到本地仓库的文件快照信息。可以选择性地将工作区的某些修改放入这个清单，准备进行下一次提交。</li>
<li><strong>本地仓库 (Local Repository)</strong>：这也是一个位于项目根目录下的 <code>.git</code> 文件夹。它存储了项目的所有版本历史、元数据以及对象数据库。当执行 <code>git commit</code> 命令时，Git 会将暂存区中的内容制作成一个永久的快照（即一次“提交”），并保存在本地仓库中。</li>
</ul>
</li>
</ul>
<h3 id="1-仓库的初始化">1. 仓库的初始化</h3>
<ul>
<li>
<p>要对一个项目进行版本控制，首先需要将其初始化为一个 Git 仓库。</p>
</li>
<li>
<p><strong>操作步骤</strong>：</p>
<ul>
<li>
<p>通过命令行工具进入项目文件夹；</p>
</li>
<li>
<p>运行以下命令：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">init</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">//</span> <span class="n">如果要将分支设置为远程仓库的主分支</span><span class="err">，</span><span class="n">可以强制命名</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">init</span> <span class="n">-b</span> <span class="n">main</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>命令效果</strong>：执行该命令后，Git 会在当前目录下创建一个名为 <code>.git</code> 的子目录。这个目录包含了所有Git仓库必需的文件和元数据。从此，Git 便开始追踪该目录下的所有文件变更。</p>
</li>
</ul>
</li>
</ul>
<h3 id="2-标准工作流程修改暂存与提交">2. 标准工作流程：修改、暂存与提交</h3>
<ul>
<li>
<p>这是 Git 最基本也是最常用的工作循环。</p>
</li>
<li>
<p>第一步：查看状态 <code>git status</code></p>
<ul>
<li>这是在任何时候都应该首先使用的命令，它可以清晰地告诉你当前仓库的状态。</li>
<li><code>git status</code> 会显示哪些文件被修改过、哪些文件是新增的（未被追踪的）、哪些文件已经被放入暂存区等。</li>
</ul>
</li>
<li>
<p>第二步：添加至暂存区 <code>git add</code></p>
<ul>
<li>
<p>当在工作区完成了一些修改后，需要使用 <code>git add</code> 命令将其添加到暂存区，以备提交。</p>
</li>
<li>
<p>添加单个文件：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">add</span> <span class="p">&lt;</span><span class="n">filename</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p>添加所有已修改或新增的文件：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">add</span> <span class="p">.</span></span></span></code></pre></div></div>
</li>
</ul>
</li>
<li>
<p>第三步：提交至本地仓库 <code>git commit</code></p>
<ul>
<li>
<p>将所有希望本次记录的更改都添加到暂存区后，就可以执行提交操作了。</p>
</li>
<li>
<p>运行以下命令，其中 <code>-m</code> 参数后的字符串是本次提交的说明信息，这是必不可少的</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">commit</span> <span class="n">-m</span> <span class="s2">&#34;必要的说明信息&#34;</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>提交说明 (Commit Message)</strong>：编写清晰、有意义的提交说明是一个至关重要的好习惯。</p>
</li>
</ul>
</li>
</ul>
<h3 id="3-提交历史的审查与追溯">3. 提交历史的审查与追溯</h3>
<ul>
<li>
<p>提交完成后，可以使用 <code>git log</code> 命令来查看仓库的版本历史。</p>
</li>
<li>
<p>查看详细历史 <code>git log</code>：</p>
<p>此命令会按 <strong>时间倒序</strong> 列出所有的提交记录。每条记录包含：</p>
<ul>
<li><strong>Commit Hash</strong>：一个唯一的 SHA-1 校验和，是该次提交的唯一标识符。</li>
<li><strong>Author</strong>：作者的姓名和邮箱地址（即在全局配置中设置的信息）。</li>
<li><strong>Date</strong>：提交的日期和时间。</li>
<li><strong>Commit Message</strong>：提交时填写的说明信息。</li>
</ul>
</li>
<li>
<p>查看简洁历史 <code>git log --oneline</code>：</p>
<p>如果觉得默认的日志输出信息太多，可以使用此命令来查看一个更紧凑的版本，每条提交仅显示一行。</p>
</li>
</ul>
<h2 id="git-分支核心机制与应用">Git 分支：核心机制与应用</h2>
<p>Git 极其重要的一个特性就是其轻量而强大的分支模型。分支允许在主开发线之外开辟一个独立的工作空间，进行新功能开发、Bug 修复或实验性尝试，而不会影响到主线的稳定。</p>
<h3 id="1-分支branch的概念与应用场景">1. 分支（Branch）的概念与应用场景</h3>
<ul>
<li><strong>什么是分支？</strong> 在技术上，Git 的分支本质上是一个指向某次提交（Commit）的可移动指针。默认情况下，Git 创建的第一个分支名为 <code>master</code> 或 <code>main</code>。每当进行一次新的提交，这个指针就会自动向前移动，指向最新的提交。</li>
<li>分支的功用（为什么需要分支）？
<ul>
<li><strong>隔离开发</strong>：假设你需要开发一个复杂的新功能，预计需要几天时间。如果直接在主分支上编码，那么在开发完成前，主分支将一直处于不稳定状态，这会严重影响团队其他成员的工作。通过创建一个新的“功能分支”，就可以在一个完全隔离的环境中工作，直到功能开发完毕、测试通过后，再将其合并回主分支。</li>
<li><strong>并行工作</strong>：在一个团队中，不同的开发者可以创建各自的分支来同时进行不同的任务，互不干扰。</li>
<li><strong>版本管理</strong>：通常会维护一个长期稳定的主分支（如 <code>main</code>），用于发布生产版本。同时创建一个 <code>develop</code> 分支用于日常开发。当需要发布新版本时，再从 <code>develop</code> 分支合并到 <code>main</code> 分支。这种工作流被称为 Git Flow，是业界流行的分支策略之一。</li>
</ul>
</li>
</ul>
<h3 id="2-分支管理的基础指令">2. 分支管理的基础指令</h3>
<ul>
<li>
<p><strong>列出分支</strong>：查看当前仓库中所有的本地分支。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">branch</span></span></span></code></pre></div></div>
<ul>
<li>当前所在的分支名前会有一个星号 <code>*</code> 标记。</li>
</ul>
</li>
<li>
<p><strong>创建新分支</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">branch</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<ul>
<li>例如：<code>git branch feature-login</code>。这个命令只创建了新分支，但仍然停留在当前分支。</li>
</ul>
</li>
<li>
<p><strong>切换分支</strong>：</p>
<ul>
<li>
<p>现代 Git 推荐使用 <code>switch</code> 命令，其语义更清晰。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="k">switch</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p>传统上使用 <code>checkout</code> 命令，至今仍广泛使用。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">checkout</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
</ul>
</li>
<li>
<p><strong>创建并立即切换到新分支</strong>：这是日常开发中最常用的命令之一。</p>
<ul>
<li>
<p>使用 <code>switch</code>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="k">switch</span> <span class="n">-c</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p>使用 <code>checkout</code>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">checkout</span> <span class="n">-b</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
</ul>
</li>
<li>
<p><strong>删除分支</strong>：当一个分支的工作完成并已合并到主线后，通常可以将其删除.</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">branch</span> <span class="n">-d</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<ul>
<li><code>-d</code> 是 <code>--delete</code> 的缩写，它只在分支被完全合并后才允许删除。如果分支上有未合并的提交，Git 会提示错误。</li>
<li>如果一定要强制删除一个未合并的分支，可以使用 <code>-D</code> 大写参数。</li>
</ul>
</li>
</ul>
<h3 id="3-分支的合并策略">3. 分支的合并策略</h3>
<ul>
<li>
<p>当在一个分支上的工作完成后，就需要将其成果合并回主开发线（例如 <code>main</code> 分支）。</p>
</li>
<li>
<p>操作步骤：</p>
<ol>
<li>
<p>首先，切换回希望并入的目标分支。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="k">switch</span> <span class="n">main</span></span></span></code></pre></div></div>
</li>
<li>
<p>然后，执行 <code>merge</code> 命令，将指定分支合并过来。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">merge</span> <span class="p">&lt;</span><span class="nb">branch-name</span><span class="n">-to-merge</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
</ol>
</li>
<li>
<p><strong>合并类型</strong>：</p>
<ul>
<li><strong>快进式合并 (Fast-forward)</strong>：如果目标分支（<code>main</code>）在创建功能分支后没有任何新的提交，那么合并时 Git 只会简单地将 <code>main</code> 指针直接移动到功能分支的最新提交上。这个过程非常快，因为没有需要整合的工作。</li>
<li><strong>三方合并 (Three-way Merge)</strong>：如果在开发功能分支的同时，目标分支（<code>main</code>）上也有了新的提交，Git 就无法进行快进式合并。此时，Git 会执行三方合并。它会找到两个分支的共同祖先，并将两个分支各自的修改与共同祖先进行比较，然后生成一个新的“合并提交”（Merge Commit）。这个新的提交会有两个父提交。</li>
</ul>
</li>
</ul>
<h3 id="4-合并冲突的识别与解决">4. 合并冲突的识别与解决</h3>
<ul>
<li>
<p><strong>什么是合并冲突？</strong> 当两个不同的分支对同一个文件的同一部分进行了修改时，Git 无法自动判断应该保留哪个版本，这时就会产生合并冲突（Merge Conflict）。Git 会暂停合并过程，等待手动解决冲突。</p>
</li>
<li>
<p><strong>解决冲突的流程</strong>：</p>
<ol>
<li>
<p><strong>识别冲突文件</strong>：执行 <code>git merge</code> 后，如果出现冲突，Git 会在命令行中明确提示。也可以使用 <code>git status</code> 查看哪些文件处于冲突状态。</p>
</li>
<li>
<p><strong>编辑冲突文件</strong>：打开冲突的文件，可以看到类似下面的标记：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">&lt;&lt;&lt;&lt;&lt;&lt;&lt;</span> <span class="n">HEAD</span>
</span></span><span class="line"><span class="cl"><span class="n">这是当前分支</span><span class="err">（</span><span class="n">例如</span> <span class="n">main</span><span class="err">）</span><span class="n">的内容</span><span class="err">。</span>
</span></span><span class="line"><span class="cl"><span class="p">=======</span>
</span></span><span class="line"><span class="cl"><span class="n">这是要合并过来的分支</span><span class="err">（</span><span class="n">例如</span> <span class="nb">feature-login</span><span class="err">）</span><span class="n">的内容</span><span class="err">。</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;&gt;&gt;&gt;&gt;</span> <span class="nb">feature-login</span></span></span></code></pre></div></div>
<ul>
<li><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD</code> 到 <code>=======</code> 之间是当前所在分支（HEAD）的内容。</li>
<li><code>=======</code> 到 <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code> 之间是要合并过来的分支的内容。</li>
</ul>
</li>
<li>
<p><strong>手动解决</strong>：需要根据实际需求，决定保留哪部分内容，或者将两部分内容重新整合。<strong>解决冲突的关键就是删除 Git 添加的这些特殊标记 (<code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code>, <code>=======</code>, <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code>)</strong>，并将文件修改为最终想要的样子。</p>
</li>
<li>
<p><strong>标记为已解决</strong>：当完成文件的修改并保存后，需要使用 <code>git add</code> 命令来告诉 Git，这个文件的冲突已经解决了。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">add</span> <span class="p">&lt;</span><span class="nb">conflicted-file</span><span class="n">-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>完成合并</strong>：当所有冲突文件都通过 <code>git add</code> 标记为已解决后，最后执行一次 <code>git commit</code> 来完成整个合并过程。Git 会自动生成一个默认的合并提交信息，也可以自行修改。</p>
</li>
</ol>
</li>
</ul>
<h2 id="远程协作与远程仓库的交互">远程协作：与远程仓库的交互</h2>
<p>到目前为止，我们所有的操作都局限在本地仓库中。为了与团队成员协作，或将代码备份到云端，我们需要与远程仓库（Remote Repository）进行交互。</p>
<h3 id="1-远程仓库remote">1. 远程仓库（Remote）</h3>
<ul>
<li>
<p><strong>什么是远程仓库？</strong> 远程仓库是托管在网络服务器上的项目版本库。它可以被团队中的多名成员访问，用于同步和分享彼此的工作成果。</p>
</li>
<li>
<p><strong>作用</strong>：</p>
<ul>
<li><strong>团队协作</strong>：提供一个所有成员都可以访问的“中央”代码库，方便大家交换代码和协同开发。</li>
<li><strong>代码备份</strong>：将本地的代码库完整地备份到云端，防止因本地设备损坏导致的数据丢失。</li>
</ul>
</li>
<li>
<p><strong>以 GitHub 为例</strong>：GitHub 是全球最大的代码托管平台。您可以在 GitHub 上免费创建公开或私有的远程仓库。本教程将主要以 GitHub 作为远程仓库的实例进行说明。</p>
</li>
</ul>
<h3 id="2-远程仓库的连接与克隆">2. 远程仓库的连接与克隆</h3>
<h4 id="场景一从零开始加入一个现有项目">场景一：从零开始，加入一个现有项目</h4>
<ul>
<li>
<p>如果要参与一个已经在 GitHub 上存在的项目，最直接的方式就是使用 <code>git clone</code> 命令。</p>
</li>
<li>
<p><code>git clone &lt;repository-url&gt;</code>：此命令会完整地复制一个远程仓库到本地。它会自动创建一个与远程仓库同名的文件夹，下载所有版本历史，并自动将远程仓库的地址命名为 <code>origin</code>，设置好跟踪关系。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="err">:</span><span class="p">//</span><span class="n">github</span><span class="p">.</span><span class="n">com</span><span class="p">/</span><span class="n">user</span><span class="p">/</span><span class="n">project</span><span class="p">.</span><span class="n">git</span></span></span></code></pre></div></div>
</li>
</ul>
<h4 id="场景二将已有的本地项目推送到远程">场景二：将已有的本地项目推送到远程</h4>
<ul>
<li>
<p>如果你已经在本地初始化了一个 Git 仓库并进行了一些提交，现在想把它发布到 GitHub 上。</p>
</li>
<li>
<p>步骤：</p>
<ol>
<li>
<p>在 GitHub 上创建一个新的空仓库（不要勾选初始化 README 等文件）。</p>
</li>
<li>
<p>GitHub 会提供该仓库的 URL。</p>
</li>
<li>
<p>在本地仓库中，使用 <code>git remote add</code> 命令将本地仓库与远程仓库关联起来。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">remote</span> <span class="n">add</span> <span class="n">origin</span> <span class="n">https</span><span class="err">:</span><span class="p">//</span><span class="n">github</span><span class="p">.</span><span class="n">com</span><span class="p">/</span><span class="n">user</span><span class="p">/</span><span class="nb">new-project</span><span class="p">.</span><span class="n">git</span></span></span></code></pre></div></div>
<ul>
<li><code>origin</code> 是远程仓库的默认别名，也可以使用其他名称。</li>
</ul>
</li>
<li>
<p>可以使用 <code>git remote -v</code> 命令查看已配置的远程仓库信息。</p>
</li>
</ol>
</li>
</ul>
<blockquote>
<p>需要注意的是，在 GitHub 上创建仓库时创建的主分支名称默认为 <code>main</code> ，而本地使用 <code>git init</code> 初始化的仓库主分支名称默认为 <code>master</code> ，如果想要将本地分支的内容直接推送到 GitHub 上的主分支，需要将两个命名统一。</p>
<p>可以使用前文提到的 <code>git init -b &lt;name&gt;</code>  来强制命名本地分支，也可以在 GitHub 设置中修改创建仓库时的默认分支名称。</p>
<p>如果在 GitHub 上创建仓库时勾选了初始化 README 等文件导致初始仓库不为空，第一次可能会推送不成功。此时需要先使用 <code>git pull</code> 命令将远程仓库合并到本地，再使用 <code>git pull</code> 推送。</p>
</blockquote>
<h3 id="3-数据同步推送push与拉取pull">3. 数据同步：推送（Push）与拉取（Pull）</h3>
<ul>
<li>
<p><strong>推送</strong> <code>git push</code>：将本地的提交分享到远程仓库。</p>
<ul>
<li>
<p><code>git push &lt;remote-name&gt; &lt;branch-name&gt;</code>：此命令会将本地指定分支上的所有新提交上传到远程仓库对应的分支。</p>
</li>
<li>
<p>首次推送一个新创建的本地分支时，通常需要使用 <code>-u</code> (或 <code>--set-upstream</code>) 参数来建立本地分支与远程分支的跟踪关系。之后，就可以直接使用 <code>git push</code> 而无需指定远程和分支名。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># 首次推送 feature 分支并建立跟踪关系</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">push</span> <span class="n">-u</span> <span class="n">origin</span> <span class="n">feature</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># 之后在该分支上，可以直接推送</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">push</span> </span></span></code></pre></div></div>
</li>
</ul>
</li>
<li>
<p><strong>拉取</strong> <code>git pull</code>：从远程仓库获取最新的更改并合并到本地。</p>
<ul>
<li>
<p><code>git pull &lt;remote-name&gt; &lt;branch-name&gt;</code>：此命令会查找远程仓库指定分支上的更新，将其下载到本地，并尝试与本地的当前分支进行合并。</p>
</li>
<li>
<p>如果已经建立了跟踪关系，可以直接使用 <code>git pull</code>。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">pull</span> <span class="n">origin</span> <span class="n">main</span></span></span></code></pre></div></div>
</li>
</ul>
</li>
</ul>
<h3 id="4-git-pull-与-git-fetch-的功能对比">4. <code>git pull</code> 与 <code>git fetch</code> 的功能对比</h3>
<ul>
<li>这是初学者经常混淆的一对命令。理解它们的区别对于避免不必要的合并冲突至关重要。</li>
<li><code>git fetch</code>：<strong>只下载，不合并</strong>
<ul>
<li><code>git fetch</code> 命令会将远程仓库的最新历史记录下载到本地，但它不会修改当前的工作区或分支。它会将这些更新保存在一个特殊的命名空间下（例如 <code>origin/main</code>）。</li>
<li><strong>使用场景</strong>：当想要查看远程仓库有什么新的更新，但又不希望立即将这些更改合并到自己的工作中时，<code>fetch</code> 是一个安全的选择。可以在 <code>fetch</code> 之后，使用 <code>git log origin/main</code> 查看远程分支的提交历史，或者使用 <code>git diff main origin/main</code> 比较本地与远程的差异，然后再决定是否合并。</li>
</ul>
</li>
<li><code>git pull</code>：<strong>下载并合并</strong>
<ul>
<li><code>git pull</code> 命令在本质上是两个命令的组合：
<ol>
<li><code>git fetch</code>：从远程仓库下载最新的内容；</li>
<li><code>git merge</code>：将下载下来的远程分支（如 <code>origin/main</code>）合并到当前的本地分支（如 <code>main</code>）。</li>
</ol>
</li>
<li><strong>使用场景</strong>：当确定希望立即获取远程更新并应用到本地工作时，<code>git pull</code> 是一个方便的命令。但需要注意，如果本地和远程有冲突的提交，<code>pull</code> 会直接触发合并冲突。</li>
</ul>
</li>
</ul>
<h2 id="操作撤销与版本回滚">操作撤销与版本回滚</h2>
<p>在开发过程中，犯错在所难免。无论是写错了代码、提交了多余的文件，还是想彻底放弃某个功能的尝试，Git 都提供了多种撤销操作的手段。本章将介绍如何针对不同阶段的错误进行“反悔”。</p>
<h3 id="1-修订最近一次提交-git-commit---amend">1. 修订最近一次提交 <code>git commit --amend</code></h3>
<ul>
<li>
<p><strong>适用场景</strong>：刚刚完成一次提交（<code>git commit</code>），但是发现：</p>
<ul>
<li>提交说明（Commit Message）写错了或不够清晰。</li>
<li>漏掉了一两个文件没有添加到暂存区。</li>
</ul>
</li>
<li>
<p><strong>操作方法</strong>：</p>
<ol>
<li>
<p>如果只是想修改提交说明，直接运行：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">commit</span> <span class="p">-</span><span class="n">-amend</span></span></span></code></pre></div></div>
<p>这会打开默认文本编辑器，可以重新编辑上次的提交说明。</p>
</li>
<li>
<p>如果想将遗漏的文件也添加到上一次提交中，可以先 <code>git add</code> 这些文件，然后再运行 <code>--amend</code>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-pow">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">git add forgotten-file.txt
</span></span><span class="line"><span class="cl">git commit --amend</span></span></code></pre></div></div>
</li>
</ol>
</li>
<li>
<p><strong>工作原理</strong>：这个命令并不会在原来的提交上进行修改，而是会创建一个全新的提交，用来替换掉刚才那次“错误”的提交。</p>
</li>
<li>
<p><strong>⚠️注意</strong>：如果这个“错误”的提交已经被推送到了远程仓库，那么不建议使用 <code>--amend</code>。因为修改历史会与远程仓库产生冲突，给团队其他成员带来麻烦。<strong>只对尚未推送到公共仓库的提交使用 <code>--amend</code></strong>。</p>
</li>
</ul>
<h3 id="2-撤销暂存区中的文件-git-reset">2. 撤销暂存区中的文件 <code>git reset</code></h3>
<ul>
<li>
<p><strong>适用场景</strong>：使用 <code>git add</code> 将文件添加到了暂存区，但后来又不想将这个文件的更改包含在下一次提交中了。</p>
</li>
<li>
<p><strong>操作方法</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">reset</span> <span class="n">HEAD</span> <span class="p">&lt;</span><span class="nb">file-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<ul>
<li><code>HEAD</code> 是一个指向当前分支最新一次提交的指针。这条命令的意思是，将暂存区里的指定文件恢复成和 <code>HEAD</code> 指向的版本一样，也就是把它从暂存区里“拿出来”。</li>
</ul>
</li>
<li>
<p>文件会从暂存区（Staged）状态变回已修改（Modified）但未暂存（Unstaged）的状态。在工作区对该文件所做的修改内容会<strong>被保留</strong>。</p>
</li>
</ul>
<h3 id="3-废弃工作区的本地修改-git-checkout-或-git-restore">3. 废弃工作区的本地修改 <code>git checkout</code> 或 <code>git restore</code></h3>
<ul>
<li>
<p><strong>适用场景</strong>：在工作区修改了一个文件，但后来觉得这些修改完全是错误的，想彻底放弃，恢复到该文件最近一次提交时的状态。</p>
</li>
<li>
<p><strong>操作方法</strong>：</p>
<ul>
<li>
<p><strong>传统方式</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">checkout</span> <span class="p">--</span> <span class="p">&lt;</span><span class="nb">file-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>现代方式（推荐）</strong>：新版 Git 提供了 <code>restore</code> 命令，语义更清晰。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">restore</span> <span class="p">&lt;</span><span class="nb">file-name</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
</ul>
</li>
<li>
<p><strong>⚠️注意</strong> ：这是一个<strong>危险且不可逆</strong>的操作！它会用仓库中该文件的最新版本直接覆盖掉在工作区的所有修改。一旦执行，所做的本地修改将<strong>永久丢失</strong>，无法找回。</p>
</li>
</ul>
<h3 id="4-历史版本的恢复策略reset-或-revert">4. 历史版本的恢复策略：<code>reset</code> 或 <code>revert</code></h3>
<ul>
<li>
<p>当需要撤销的不是某个文件的修改，而是整个或某几个历史提交时，就需要用到版本回退了。Git 提供了两种主要的回退方式：<code>reset</code> 和 <code>revert</code>。</p>
</li>
<li>
<p><code>git reset</code>：移动指针，重写历史</p>
<ul>
<li><strong>工作原理</strong>：<code>git reset</code> 命令会直接移动当前分支的 <code>HEAD</code> 指针以及分支指针到指定的某个历史提交上。根据参数不同，它对暂存区和工作区的影响也不同。</li>
<li><strong>常用模式</strong> <code>git reset --hard &lt;commit-id&gt;</code>：这是最彻底也是最危险的模式。它会：
<ol>
<li>将分支指针移动到 <code>&lt;commit-id&gt;</code>。</li>
<li>将暂存区更新为 <code>&lt;commit-id&gt;</code> 的内容。</li>
<li>将工作区也强制更新为 <code>&lt;commit-id&gt;</code> 的内容。 这意味着从 <code>&lt;commit-id&gt;</code> 之后的所有提交记录以及所有未提交的本地修改都会<strong>被彻底删除</strong>。</li>
</ol>
</li>
<li><strong>适用场景</strong>：仅用于<strong>私有分支</strong>或<strong>未被推送到公共仓库</strong>的提交。当确定自己本地的某段提交历史完全是错误的，并且想彻底抹除它们时使用。<strong>绝对不要对已经推送到公共仓库的提交使用</strong> <code>git reset --hard</code>。</li>
</ul>
</li>
<li>
<p><code>git revert</code>：创建反向操作，保留历史</p>
<ul>
<li>
<p><strong>工作原理</strong>：<code>git revert</code> 不会删除或修改任何历史记录。相反，它会分析指定的那个历史提交，然后<strong>创建一个全新的提交</strong>，这个新提交的内容刚好是用来抵消掉那个历史提交所做的所有更改。</p>
</li>
<li>
<p><strong>操作方法</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">git</span> <span class="n">revert</span> <span class="p">&lt;</span><span class="nb">commit-id</span><span class="p">&gt;</span></span></span></code></pre></div></div>
</li>
<li>
<p><strong>适用场景</strong>：这是在<strong>公共分支</strong>上撤销提交的<strong>标准且安全</strong>的方式。因为它不改变项目历史，只是在历史的末尾追加一次“反向提交”，所以对于已经推送到远程并被团队成员拉取的提交，使用 <code>revert</code> 不会造成历史冲突。其他成员只需正常 <code>git pull</code> 即可同步这次撤销操作。</p>
</li>
</ul>
</li>
<li>
<p><strong>总结对比</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">特性</th>
          <th style="text-align: left">git reset &ndash;hard</th>
          <th style="text-align: left">git revert</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">历史记录</td>
          <td style="text-align: left">修改（删除）现有历史</td>
          <td style="text-align: left">不修改历史，而是追加新历史</td>
      </tr>
      <tr>
          <td style="text-align: left">操作结果</td>
          <td style="text-align: left">HEAD 指针和分支指针移动到过去</td>
          <td style="text-align: left">在当前分支顶端创建一个新的“反向”提交</td>
      </tr>
      <tr>
          <td style="text-align: left">安全性</td>
          <td style="text-align: left">危险，会丢失数据</td>
          <td style="text-align: left">安全，保留所有记录</td>
      </tr>
      <tr>
          <td style="text-align: left">适用范围</td>
          <td style="text-align: left">仅限本地私有提交</td>
          <td style="text-align: left">公共、共享的提交</td>
      </tr>
  </tbody>
</table>
</li>
</ul>
<h2 id="高级功能与技巧">高级功能与技巧</h2>
<p>此处内容暂不详细学习，仅做了解。</p>
<ol>
<li>使用 <code>.gitignore</code> 排除文件；
<ul>
<li>配置该文件以指定不需要纳入版本控制的文件或目录。</li>
</ul>
</li>
<li>版本标记（Tagging）的应用；
<ul>
<li><code>git tag</code>：为特定的提交（例如软件发布版本）创建永久性的标记。</li>
</ul>
</li>
<li>工作进度的临时储藏（Stashing）
<ul>
<li><code>git stash</code>：临时保存当前工作目录和暂存区的状态。</li>
</ul>
</li>
<li>提交历史的重写（Rebasing）
<ul>
<li><code>git rebase</code>：用于合并一系列提交，以创造一个更线性的提交历史。</li>
</ul>
</li>
</ol>
<h2 id="总结">总结</h2>
<h3 id="1-核心概念回顾">1. 核心概念回顾</h3>
<ul>
<li>
<p><strong>三大区域</strong>：我们所有的本地工作都在<strong>工作区</strong>进行，通过 <code>git add</code> 将选定的更改放入<strong>暂存区</strong>，最后通过 <code>git commit</code> 将这些更改永久记录到<strong>本地仓库</strong>。</p>
</li>
<li>
<p><strong>分支</strong>：分支是 Git 的核心，它允许我们创建独立的工作空间来开发新功能或修复问题，而不影响主线的稳定性。<code>git branch</code>, <code>git switch</code>, <code>git merge</code> 是我们的主要工具。</p>
</li>
<li>
<p><strong>远程协作</strong>：通过 <code>git clone</code>, <code>git push</code>, <code>git pull</code> 等命令，我们可以与 GitHub 等远程平台进行交互，实现团队协作和代码备份。</p>
</li>
</ul>
<h3 id="2-常用命令速查表">2. 常用命令速查表</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">任务分类</th>
          <th style="text-align: left">命令</th>
          <th style="text-align: left">功能描述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>配置</strong></td>
          <td style="text-align: left"><code>git config --global user.name &quot;Name&quot;</code></td>
          <td style="text-align: left">设置全局用户名</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git config --global user.email &quot;Email&quot;</code></td>
          <td style="text-align: left">设置全局用户邮箱</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>创建仓库</strong></td>
          <td style="text-align: left"><code>git init</code></td>
          <td style="text-align: left">在当前目录初始化一个新仓库</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git clone &lt;url&gt;</code></td>
          <td style="text-align: left">从远程地址克隆一个仓库</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>日常操作</strong></td>
          <td style="text-align: left"><code>git status</code></td>
          <td style="text-align: left">查看仓库当前状态</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git add &lt;file&gt;</code> 或 <code>git add .</code></td>
          <td style="text-align: left">将文件更改添加到暂存区</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git commit -m &quot;Message&quot;</code></td>
          <td style="text-align: left">将暂存区内容提交到本地仓库</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git log</code> 或 <code>git log --oneline</code></td>
          <td style="text-align: left">查看提交历史</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>分支管理</strong></td>
          <td style="text-align: left"><code>git branch</code></td>
          <td style="text-align: left">列出、创建或删除分支</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git switch &lt;branch&gt;</code> 或 <code>git checkout &lt;branch&gt;</code></td>
          <td style="text-align: left">切换到指定分支</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git switch -c &lt;branch&gt;</code> 或 <code>git checkout -b &lt;branch&gt;</code></td>
          <td style="text-align: left">创建并切换到新分支</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git merge &lt;branch&gt;</code></td>
          <td style="text-align: left">将指定分支合并到当前分支</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>远程同步</strong></td>
          <td style="text-align: left"><code>git remote -v</code></td>
          <td style="text-align: left">查看已配置的远程仓库</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git fetch</code></td>
          <td style="text-align: left">从远程仓库下载最新历史，不合并</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git pull</code></td>
          <td style="text-align: left">从远程仓库下载并合并</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git push</code></td>
          <td style="text-align: left">将本地提交推送到远程仓库</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>撤销操作</strong></td>
          <td style="text-align: left"><code>git commit --amend</code></td>
          <td style="text-align: left">修改最后一次提交</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git restore &lt;file&gt;</code></td>
          <td style="text-align: left">放弃工作区的修改</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git revert &lt;commit-id&gt;</code></td>
          <td style="text-align: left">创建一个新提交来撤销历史提交</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left"><code>git reset --hard &lt;commit-id&gt;</code></td>
          <td style="text-align: left">（危险）回退到某个历史版本</td>
      </tr>
  </tbody>
</table>
<h3 id="3-编写高质量的-commit-message-选学">3. 编写高质量的 Commit Message (选学)</h3>
<p>业界公认的格式：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="n">type</span><span class="p">&gt;</span><span class="err">:</span> <span class="p">&lt;</span><span class="n">subject</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">optional body</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">optional footer</span><span class="p">]</span></span></span></code></pre></div></div>
<ul>
<li>
<p><strong><code>&lt;type&gt;</code> (类型)</strong>：说明本次提交的类别，常用的有：</p>
<ul>
<li><code>feat</code>: 新功能 (feature)</li>
<li><code>fix</code>: 修复 bug</li>
<li><code>docs</code>: 文档变更</li>
<li><code>style</code>: 代码格式（不影响代码运行的变动）</li>
<li><code>refactor</code>: 重构（既不是新增功能，也不是修改 bug 的代码变动）</li>
<li><code>test</code>: 增加测试</li>
<li><code>chore</code>: 构建过程或辅助工具的变动</li>
</ul>
</li>
<li>
<p><strong><code>&lt;subject&gt;</code> (主题)</strong>：用一句话简要描述本次提交的目的，不超过 50 个字符。</p>
</li>
<li>
<p><strong><code>body</code> (正文)</strong>：可选。对本次提交进行更详细的描述，解释“为什么”要这样修改。</p>
</li>
<li>
<p><strong><code>footer</code> (脚注)</strong>：可选。通常用于关闭 Issue (例如 <code>Closes #123</code>)。</p>
</li>
</ul>
<p><strong>示例</strong>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-powershell">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">feat</span><span class="err">:</span> <span class="n">Add</span> <span class="n">user</span> <span class="n">login</span> <span class="kd">function</span><span class="nb">ality</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Implement</span> <span class="n">the</span> <span class="n">user</span> <span class="n">login</span> <span class="n">API</span> <span class="n">endpoint</span> <span class="n">and</span> <span class="n">basic</span> <span class="n">validation</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="n">The</span> <span class="n">endpoint</span> <span class="n">accepts</span> <span class="n">username</span> <span class="n">and</span> <span class="n">password</span><span class="p">,</span> <span class="n">and</span> <span class="n">returns</span> <span class="n">a</span> <span class="n">JWT</span> <span class="n">token</span> <span class="n">upon</span> <span class="n">successful</span> <span class="n">authentication</span><span class="p">.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Closes</span> <span class="c">#42</span></span></span></code></pre></div></div>
<hr>
]]></description></item><item><title>机器学习笔记(5)：决策树</title><link>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B05%E5%86%B3%E7%AD%96%E6%A0%91/</link><pubDate>Tue, 12 Aug 2025 12:51:04 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B05%E5%86%B3%E7%AD%96%E6%A0%91/</guid><description><![CDATA[<div class="featured-image">
                <img src="/img.jpg" referrerpolicy="no-referrer">
            </div><h2 id="基本概念">基本概念</h2>
<p>   <strong>决策树（Decision Tree）</strong> 是一种基础的监督学习模型，广泛应用于分类与回归任务。其核心思想是通过一个树状结构来对实例进行决策。该结构本质上是一系列<code>if-then</code>规则的集合，具有很强的可解释性。</p>
<p>   一个决策树模型包含以下几个基本组成部分：</p>
<ul>
<li><strong>根节点 (Root Node):</strong> 整棵树的起点，代表了对数据集的第一个划分问题。</li>
<li><strong>内部节点 (Internal Node):</strong> 也叫决策节点，代表一个特征或属性的判断问题。</li>
<li><strong>分支 (Branch):</strong> 从内部节点延伸出来的路径，代表对问题的一个可能回答（比如“是”或“否”，“大于”或“小于”等）。</li>
<li><strong>叶子节点 (Leaf Node):</strong> 也叫终端节点，代表最终的决策结果或分类标签。从根节点到叶子节点的每一条路径，都构成了一条决策规则。</li>
</ul>
<p>   决策树的目标是构建一个泛化能力强，且尽可能简单的模型。它通过从数据特征中学习，自动生成一套简单有效的“决策规则”，从而对新的数据进行预测。例如，根据一个人的年龄、收入、职业等特征，来预测他是否会购买某个产品。其基本流程遵循简单而直观的 <strong>“分而治之”(divide-and-conquer)</strong> 策略。</p>
<figure>
    
    <figcaption>决策树学习基本算法 图源《机器学习》[周志华]</figcaption>
</figure>
<p>   决策树的生成是一个递归过程，在基本算法中有三种情形会导致递归返回：<br>   1. 当前结点包含的样本全属于同一类别，无需划分；<br>   2. 当前属性集为空，或是所有样本在所有属性上取值相同，无法划分；<br>   3. 当前结点包含的样本集合为空，不能划分。</p>
<h2 id="划分选择">划分选择</h2>
<p>   由上述算法可以看出，决策树学习的关键是选择最优划分属性。决策树的目标是，通过每次提问（划分），让数据的“不确定性”变得越来越小，让划分后的每个分支都尽可能地“纯粹”，即结点的 <strong>“纯度”(purity)</strong> 越来越高。衡量“纯度”的最常用的数学指标有<strong>信息熵 (Information Entropy)</strong> 和 <strong>基尼不纯度 (Gini Impurity)</strong> ，后者也称为 <strong>“基尼指数”(Gini index)</strong>。</p>
<h3 id="信息熵与信息增益">信息熵与信息增益</h3>
<p>   <strong>”信息熵(information entropy)</strong> 是信息论中的一个概念，它衡量的是一个系统（在这里就是我们的数据集）的 <strong>不确定性</strong> 或 <strong>混乱程度</strong> 。</p>
<ul>
<li><strong>熵越大</strong> ，代表系统越混乱，不确定性越高。</li>
<li><strong>熵越小</strong> ，代表系统越有序，不确定性越低。</li>
</ul>
<p>假定当前样本集合 $D$ 中第 $k$ 类样本所占的比例为 $p_k(k=1,2,\dots,|\mathcal{Y}|)$ ，则 $D$ 的信息熵定义为：</p>
$$
Ent(D) = -\sum_{k=1}^{|\mathcal{Y}|} p_k \log_2 p_k
$$<p>
$Ent(D)$ 的值越小，则 $D$ 的纯度越高。</p>
<p>   <strong>信息增益 (Information Gain)</strong> 则是用来衡量一个特征划分“好不好”的指标。它的思想很简单：</p>
$$
信息增益 = 划分前的信息熵 - 划分后的信息熵
$$<p>
假定离散属性 $a$ 有 $V$ 个可能的取值 $\{a^1, a^2, \dots, a^V \}$ ，那么使用 $a$ 来划分就会产生 $V$ 个分支结点，每个结点的信息熵之和就是划分后的信息熵。考虑到不同的分支结点包含的样本数不同，给分支结点赋予权重 $|D^v|/|D|$ ，即样本数越多的分支结点的影响越大，于是可计算出信息增益：</p>
$$
\text{Gain}(D, a) = \text{Ent}(D) - \sum_{v=1}^{V} \frac{|D^v|}{|D|} \text{Ent}(D^v)
$$<p>
一般来说，信息增益越大，说明数据集不确定性减少的程度越大，即纯度提升越大。因此决策树会选择信息增益最大的特征进行划分。</p>
<h4 id="信息增益率">信息增益率</h4>
<p>   思考一个极端情况。假设我们的数据集中有一个特征是“学号”或“订单ID”，这个特征的特点是，每个人的学号或 ID 都是独一无二的。如果决策树使用信息增益作为划分标准，那么按照 ID 来划分数据时，每个分支都只会包含一个样本。此时每个分支的“纯度”都达到了100%，信息熵为0，于是决策树会选择“学号”或 ID 来作为根节点划分，但是这样的划分没有意义，我们称之为 <strong>过拟合(Overfitting)</strong> 。<br><strong>结论：信息增益准则对那些“可取值数目较多”的特征有所偏好。</strong></p>
<p>   为了修正这种偏好，我们引出&quot;信息增益率&quot;的概念。它出自 <strong>C4.5 决策树算法</strong>。</p>
<p>    <strong>信息增益率 (Information Gain Ratio)</strong> 在信息增益的基础上，增加了一个<strong>惩罚项</strong>，这个惩罚项会随着特征的可取值数目增多而变大，从而抑制了模型对这类特征的偏好。增益率定义为：</p>
$$
\text{Gain\_ratio}(D, a) = \frac{\text{Gain}(D, a)}{\text{IV}(a)}
$$<p>
其中，</p>
<ul>
<li>$\text{Gain}(D, a)$ 是信息增益。</li>
<li>$\text{IV}(a)$ 就是新增的惩罚项，它被称为特征 $a$ 的 **固有值(Intrinsic Value) ** 或 <strong>分裂信息(Split Information)</strong>。</li>
</ul>
<p>固有值 $\text{IV}(a)$ 的计算公式为：</p>
$$
\text{IV}(a) = - \sum_{v=1}^{V} \frac{|D^v|}{|D|} \log_2 \frac{|D^v|}{|D|}
$$<p>
可以发现这个公式与信息熵的计算公式非常相似，不同之处在于：</p>
<ul>
<li><strong>信息熵</strong> 衡量的是 <strong>类别标签</strong> 的混乱程度（比如“是/否”购买的比例）。</li>
<li><strong>固有值</strong> 衡量的是 <strong>特征取值</strong> 的混乱程度（比如特征“天气”有“晴天、阴天、下雨”3个值的比例）</li>
</ul>
<p>如果一个特征的取值越多，每个取值下的样本数就越少，那么计算出的 $\text{IV}(a)$ 值通常会越大。把它作为分母，就可以有效地 <strong>惩罚</strong> 那些取值数目过多的特征。</p>
<p>   值得注意的是，增益率准则对可取值数目较少的属性有所偏好，因此<strong>C4.5算法</strong>并不是直接选择增益率最大的特征，而是采用一种启发式策略：<br>  1.  先从候选划分特征中找出<strong>信息增益高于平均水平</strong>的特征。<br>  2.  <strong>然后</strong>再从这些特征中选择<strong>信息增益率最高</strong>的那个。<br>这样做可以平衡信息增益和增益率，避免了对取值数目少的特征产生不公平。</p>
<h3 id="基尼指数">基尼指数</h3>
<p>   <strong>基尼指数(Gini index)</strong> ，也称为 <strong>基尼不纯度(Gini Impurity)</strong> ，是另一个衡量数据不确定性的指标，它比信息熵的计算速度更快一些，在很多决策树算法(如 <strong>CART (Classification and Regression Tree) 决策树算法</strong>)中被广泛使用。</p>
<p>   基尼指数的含义是：<strong>从一个数据集中随机抽取两个样本，其类别标签不一致的概率。</strong></p>
<ul>
<li>
<p><strong>基尼指数越小</strong>，代表数据集的纯度越高。</p>
</li>
<li>
<p><strong>基尼指数越大</strong>，代表数据集的纯度越低。</p>
</li>
</ul>
<p>对于一个数据集 $D$ ，其基尼指数的定义为：</p>
$$
\begin{align*}
\text{Gini}(D) &= \sum_{k=1}^{|\mathcal{Y}|} \sum_{k^{'} \neq k} p_kp_{k^{'}} \\
& = 1 - \sum_{k=1}^{|\mathcal{Y}|} p_k^2
\end{align*}
$$<p>
在评估“划分”时，我们用“基尼指数”指代一次划分操作的优劣程度。在这种情况下，“基尼指数”指的是划分后，所有子节点基尼不纯度的<strong>加权平均值</strong> ：</p>
$$
\text{Gini\_index}(D, a) = \sum_{v = 1}^V  \frac{|D^v|}{|D|} \text{Gini}(D^v)
$$<p>
此时，在候选属性集合 $A$ 中，选择那个使得划分后基尼指数最小的属性作为最优划分属性，即 $a_* = \arg \min_{a\in A} \text{Gini\_index}(D, a)$ 。</p>
<h2 id="剪枝处理">剪枝处理</h2>
<p>   到目前为止，我们已经知道决策树是如何通过选择最优特征来一步步构建起来的。但一个关键问题是：<strong>这棵树应该长到什么时候才停下来？</strong> 如果让它一直生长，直到每个叶子节点都只包含一种类型的样本（即达到100%纯度），这样就是最好的吗？答案是否定的。这会引出机器学习中一个非常普遍且重要的问题——过拟合。</p>
<h3 id="过拟合">过拟合</h3>
<p>   <strong>过拟合</strong>指的是一个模型在<strong>训练数据</strong>上表现得过于完美，以至于把训练数据中的一些噪声、特例和无关紧要的细节都学习了进去，导致模型的<strong>泛化能力</strong>下降。这样的模型，在面对它“背过答案”的训练集时，准确率可能高达100%；但一旦遇到新的、没见过的数据（测试集），表现就会非常糟糕。</p>
<p>   为了解决过拟合问题，我们需要对决策树进行简化，这个过程就叫做<strong>剪枝 (Pruning)</strong>。剪枝是一种正则化技术，它通过主动去掉一些分支来降低模型的复杂度，从而提高模型的泛化能力。</p>
<p>   剪枝分为两大类：<strong>预剪枝 (Pre-pruning)</strong> 和 <strong>后剪枝 (Post-pruning)</strong>。</p>
<h3 id="预剪枝">预剪枝</h3>
<p>   <strong>预剪枝 (Pre-pruning)</strong> 是指在决策树生成过程中，对每个结点在划分前先进行估计，若当前结点的划分不能带来决策树泛化性能的提升，则停止划分并将当前结点标记为叶节点。</p>
<p>   判断决策树泛化性能是否提升，可以考虑使用 <a href="https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B02%E6%A8%A1%E5%9E%8B%E8%AF%84%E4%BC%B0%E4%B8%8E%E9%80%89%E6%8B%A9/" target="_blank" rel="noopener noreffer ">模型评估与选择</a> 一节中的性能评估方法，比如采用留出法，预留一部分数据用作”验证集“以进行性能评估。除此之外，还有一些更简单的预剪枝策略：</p>
<ul>
<li>
<p><strong>限制树的深度 (Max Depth)</strong>：规定决策树最多能长多深，一旦达到指定深度就停止生长。</p>
</li>
<li>
<p><strong>限制叶子节点的最少样本数 (Min Samples Leaf)</strong>：规定一个叶子节点至少要包含多少个样本，如果分裂后某个子节点的样本数少于这个阈值，则不进行分裂。</p>
</li>
<li>
<p><strong>限制节点划分的最小样本数 (Min Samples Split)</strong>：规定一个节点至少要有多少个样本才能进行划分。</p>
</li>
<li>
<p><strong>限制信息增益的阈值</strong>：规定信息增益（或基尼增益）必须大于某个阈值，否则不进行分裂。</p>
</li>
</ul>
<p>   预剪枝的优点在于，在书的构建过程中就提前停止，速度快，计算开销小，同时生成的决策树通常规模更小，更简单等；而它的缺点是具有“短视”的风险，有些划分可能当前看起来收益不大（甚至会降低验证集准确率），但它后续的划分却可能会带来巨大的收益。预剪枝基于“贪心”本质禁止这些分支展开，给预剪枝决策树带来了欠拟合的风险。</p>
<h3 id="后剪枝">后剪枝</h3>
<p>   <strong>后剪枝 (Post-pruning)</strong> 与预剪枝相反。它允许决策树先完全生长，然后再从下往上地审视每个结点，尝试将一些子树替换为单个叶子结点，看看这样做能否提升模型的泛化能力。</p>
<p>   最常见的后剪枝方法是<strong>降低错误剪枝 (Reduced Error Pruning, REP)</strong>：</p>
<ol>
<li>
<p>将数据分为训练集和验证集。</p>
</li>
<li>
<p>用训练集生成一棵完整的决策树。</p>
</li>
<li>
<p>从下往上遍历树中的每一个非叶子结点。</p>
</li>
<li>
<p><strong>尝试</strong>将这个结点对应的子树替换成一个叶子结点（该叶子结点的类别由子树中占多数的样本决定）。</p>
</li>
<li>
<p>用<strong>验证集</strong>评估，如果剪枝后的树在验证集上的准确率<strong>更高</strong>或<strong>持平</strong>，则<strong>确定</strong>进行剪枝，用叶子结点替换该子树。</p>
</li>
<li>
<p>重复这个过程，直到无法再通过剪枝提升验证集准确率。</p>
</li>
</ol>
<p>   后剪枝的优点是眼光更长远，因为它是在一棵完全长成的树上进行全局考察，通常能保留更多有用的分支，所以剪枝后模型的泛化性能往往优于预剪枝；缺点是计算开销大，同时需要先生成一棵完整的树，所以时间成本更高。</p>
<h3 id="两种剪枝策略对比">两种剪枝策略对比</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">对比项</th>
          <th style="text-align: left"><strong>预剪枝 (Pre-pruning)</strong></th>
          <th style="text-align: left"><strong>后剪枝 (Post-pruning)</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>时机</strong></td>
          <td style="text-align: left">决策树<strong>构建时</strong>进行，边建边剪。</td>
          <td style="text-align: left">决策树<strong>构建完成后</strong>进行，先建后剪。</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>优点</strong></td>
          <td style="text-align: left">训练时间开销小，生成的树更简单。</td>
          <td style="text-align: left">泛化能力通常更强，不容易错过有潜力的划分，欠拟合风险小。</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>缺点</strong></td>
          <td style="text-align: left">存在“短视”问题，可能因局部判断而过早停止，有欠拟合风险。</td>
          <td style="text-align: left">计算开销大，时间成本更高。</td>
      </tr>
  </tbody>
</table>
<h2 id="连续值与缺失值">连续值与缺失值</h2>
<p>   目前为止我们讨论的例子，特征都是 <strong>离散值 (Categorical Values)</strong> ，但在现实学习任务重，数据往往包含了大量的 <strong>连续值 (Continuous Values)</strong> 。同时，数据也常常不完整，存在 <strong>缺失值 (Missing Values)</strong> 。一个完善的算法必须能够妥善处理这些情况。</p>
<h3 id="连续值处理">连续值处理</h3>
<p>   对于连续的特征（如温度、长度等），我们无法像离散特征那样，为每个可能的值都创建一个分支。因为连续值的可能性有无穷多个。此时，可以使用 <strong>连续值离散化 (Discretization)</strong> 。其核心思想是，将连续特征转换为一个离散的二分特征。具体来说，就是寻找一个最优的 <strong>“切分点” (Split Point)</strong> ，将数据一分为二，比如将“温度”特征变为“温度 ≤ 24.5℃”和“温度 &gt; 24.5℃”两个分支。</p>
<p>   关于如何找到这个最优的切分点，C4.5算法提供了一个简单有效的方法：</p>
<ol>
<li><strong>排序</strong> ：给定样本集 $D$ 和连续属性 $a$ ，假定 $a$ 在 $D$ 上出现了 $n$ 个不同的取值，将这些值从小到大进行排序。假设排序后的值为 ${a^1,a^2,\dots,a^n}$ ；</li>
<li><strong>生成候选切分点</strong> ：基于划分点 $t$ 可将 $D$ 分为 $D_t^-$ 和 $D_t^+$ 。显然对于相邻的属性取值 $a^i$ 与 $a^{i+1}$ 来说，$t$ 在区间 $[a^i, a^{i+1})$ 中取任意值所产生的划分结果相同。因此可以将每两个相邻值的中点作为候选切分点，此时可以得到包含 $n-1$ 个元素的候选切分点集合：<br> $$T_a = \left\{\frac{a^i + a^{i+1}}{2} | 1 \leq i \leq n-1 \right\}；$$</li>
<li><strong>选择最优切分点</strong> ：此时，像计算离散特征一样，我们可以计算每次二次划分的信息增益，从而得到最优切分点对应的划分<br>$$\begin{align*} \text{Gain}(D,a) &= \max_{t \in T_a} \text{Gain}(D,a,t) \\ &= \max_{t \in T_a} \text{Ent}(D) - \sum_{\lambda \in\{-,+\}}\frac{|D_t^{\lambda}|}{|D|} \text{Ent}(D_t^{\lambda}) \end{align*}$$  <br>其中 $\text{Gain}(D,a,t)$ 是样本集 $D$ 基于划分点 $t$ 二分后的信息增益，于是我们就可以选择使 $\text{Gain}(D,a,t)$ 最大化的划分点。</li>
</ol>
<h3 id="缺失值处理">缺失值处理</h3>
<p>   现实任务中常会遇到不完整样本，即样本的某些属性值缺失，我们需要解决两个问题：(1) 如何在属性值缺失的情况下进行划分属性选择？(2) 给定划分属性，若样本在该属性上的值缺失，该如何对样本进行划分？C4.5算法巧妙地解决了这个问题，主要分两步：</p>
<h4 id="第一步在计算信息增益时">第一步：在计算信息增益时</h4>
<p>假设特征 $A$ 有缺失值：</p>
<ol>
<li>首先，忽略掉所有在特征 $A$ 上有缺失值的样本，得到一个无缺失的数据子集 $\widetilde{D}$ ；</li>
<li>只用这个无缺失子集 $\widetilde{D}$ 来正常计算特征 $A$ 的信息增益 $\text{Gain}(\widetilde{D},A)$；</li>
<li>最后，将计算出的信息增益乘以一个权重。这个权重就是 <strong>无缺失样本所占的比例</strong>。<br>例如， 总共有100条数据，特征 A 在其中80条数据上没有缺失值，那么权重就是 80/100 = 0.8。最终特征 A 的信息增益就是 $0.8 \times \text{Gain}(\widetilde{D}, A)$ 。</li>
</ol>
<p>这样做相当于对有缺失值的特征施加了一个“惩罚”，缺失值越多，它最终的信息增益就越低，被选中的概率也就越小。</p>
<h4 id="第二步在划分数据时">第二步：在划分数据时</h4>
<p>   假设最终还是选择了有缺失值的特征 A 来进行划分（比如特征A有“取值1”和“取值2”两个分支）。那么，那条在特征 A 上有缺失值的样本该被分到哪个分支去呢？</p>
<p>   C4.5的做法是，将这个缺失值样本<strong>按比例分配</strong>到所有的子节点中。这个比例，就是无缺失样本在各个分支中所占的比例。例如，在无缺失的样本中，有60%的样本被划入了“取值1”分支，40%被划入了“取值2”分支。那么，这个缺失值样本就会被赋予一个<strong>0.6的权重</strong>并划入“取值1”分支，同时被赋予一个<strong>0.4的权重</strong>并划入“取值2”分支。之后在子节点中继续计算信息熵时，这些带有权重的样本也会被相应地考虑进去。</p>
<h2 id="总结">总结</h2>
<h3 id="主要优缺点">主要优缺点</h3>
<h4 id="优点">优点</h4>
<ul>
<li><strong>可解释性强</strong>：模型的结果易于理解和可视化，可以生成清晰的<code>if-then</code>规则，是典型的“白盒模型”。</li>
<li><strong>数据准备要求低</strong>：通常无需对数据进行归一化或标准化，并且能同时处理数值型和类别型数据。</li>
<li><strong>预测速度快</strong>：一旦决策树构建完成，对新样本的预测过程非常高效。</li>
</ul>
<h4 id="缺点">缺点</h4>
<ul>
<li><strong>容易过拟合</strong>：若不加限制，决策树会倾向于完美拟合训练数据，导致泛化能力差，因此剪枝是必不可少的步骤。</li>
<li><strong>不稳定性</strong>：训练数据中的微小变动可能会导致生成一棵完全不同的树。</li>
<li><strong>贪心本质</strong>：决策树的生成过程是贪心的，在每一步都寻求局部最优解，但这并不能保证得到全局最优的树结构。</li>
<li><strong>学习能力局限</strong>：决策树的划分边界与坐标轴平行，这使得它在学习某些具有对角线关系的数据时效率不高。</li>
</ul>
<h3 id="决策树在现代机器学习中的角色">决策树在现代机器学习中的角色</h3>
<p>   尽管单个决策树存在上述缺点，但它依然是现代机器学习中极为重要的算法之一，因为它构成了许多强大**集成学习(Ensemble Learning)**模型的基础。</p>
<ul>
<li><strong>随机森林 (Random Forest)</strong>：通过构建大量的决策树，并让它们对结果进行“投票”，极大地克服了单个决策树不稳定的问题，是一种非常强大和流行的算法。</li>
<li><strong>梯度提升决策树 (Gradient Boosted Decision Tree, GBDT)</strong>：通过迭代地构建决策树，每一棵新树都致力于修正前面所有树的残差。以GBDT为核心思想的<strong>XGBoost</strong>、<strong>LightGBM</strong>等算法，是当前在处理表格数据任务中最顶尖的算法之一。</li>
</ul>
]]></description></item><item><title>机器学习笔记(4)：线性判别分析</title><link>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B04%E7%BA%BF%E6%80%A7%E5%88%A4%E5%88%AB%E5%88%86%E6%9E%90/</link><pubDate>Tue, 05 Aug 2025 10:02:50 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B04%E7%BA%BF%E6%80%A7%E5%88%A4%E5%88%AB%E5%88%86%E6%9E%90/</guid><description><![CDATA[<div class="featured-image">
                <img src="/img.png" referrerpolicy="no-referrer">
            </div><blockquote>
<p>线性判别分析的代码实现参考<a href="https://github.com/coverMoon/Machine-Learning-practice" target="_blank" rel="noopener noreffer ">GitHub</a></p>
</blockquote>
<hr>
<p>先来回顾一下协方差矩阵的概念。<br> <strong>1. 方差</strong> <br>   方差用来衡量 <strong>单一变量</strong> 的数据有多么分散。数学上表示为 $Var(X)$​ 。<br> <strong>2. 协方差</strong> <br>   将一个变量从一个增加到两个，就引出了协方差。<br></p>
<ul>
<li>
<p><strong>协方差是做什么的？</strong> 它用来衡量 <strong>两个变量</strong> 如何 <strong>协同变化</strong>。也就是说，当一个变量变化时，另一个变量是倾向于朝相同方向变化，还是相反方向变化。</p>
</li>
<li>
<p><strong>三种情况：</strong></p>
<ol>
<li><strong>正协方差 (Positive Covariance):</strong> 当变量X增大时，变量Y也倾向于增大。我们称之为“正相关”。</li>
<li><strong>负协方差 (Negative Covariance):</strong> 当变量X增大时，变量Y却倾向于减小。我们称之为“负相关”。</li>
<li><strong>零协方差 (Zero Covariance):</strong> 两个变量之间没有明确的线性关系。</li>
</ol>
</li>
</ul>
<blockquote>
<p><strong>核心：</strong> 协方差描述的是 <strong>两个</strong> 变量之间的方向关系。数学上表示为 $Cov(X,Y)$。</p>
</blockquote>
<p><strong>3. 协方差矩阵</strong><br>   当有多个变量时，我们需要有一个工具来同时展示所有变量两两之间的协方差关系。</p>
<p><strong>协方差矩阵是做什么的？</strong> 它是一个方阵（行数和列数相等），整齐地排列了多个变量中，每一对变量之间的协方差。</p>
<p><strong>矩阵的结构：</strong> 假设我们有三个变量：X, Y, Z。那么协方差矩阵就是一个 3x3 的矩阵。</p>
<ul>
<li><strong>对角线上的元素 (Diagonal Elements):</strong> 对角线上的值是 <strong>每个变量自身的方差</strong>。因为一个变量和它自己的“协同变化”程度，就是它自身的分散程度（方差）。所以 $Cov(X,X)=Var(X)$。</li>
<li><strong>非对角线上的元素 (Off-diagonal Elements):</strong> 非对角线上的值是 <strong>不同变量两两之间的协方差</strong>。</li>
<li><strong>结构示例：</strong> 对于变量 X, Y, Z，其协方差矩阵看起来是这样的：</li>
</ul>
$$
\begin{bmatrix}
Var(X) & Cov(X) & Cov(X,Z) \\
Cov(Y,X) & Var(Y) & Cov(Y,Z) \\
Cov(Z,X) & Cov(Z,Y) & Var(Z)
\end{bmatrix}
$$<blockquote>
<p><strong>重要特性：对称性 (Symmetry)</strong> 。这个矩阵是<strong>对称</strong>的。因为X和Y的协方差 $(Cov(X,Y))$ 与Y和X的协方差 $(Cov(Y,X))$ 是完全一样的。所以矩阵中，位置 <code>(i, j)</code> 的元素和 <code>(j, i)</code> 的元素是相等的。</p>
</blockquote>
<hr>
<h2 id="线性判别分析">线性判别分析</h2>
<p>   线性判别分析 (Linear Discriminant Analysis, LDA) 是一种经典的线性学习方法，其思想非常朴素：简单来说，LDA 试图找到一种看待数据的方式 (一个视角或者一个投影) ，在这种方式下，不同类别的数据点能够被最清晰地分开。</p>

<p>LDA 的核心思想是：</p>
<ul>
<li><strong>“类间距离最大化，类内方差最小化”</strong> <br> (Maximize the distance between classes, and minimize the variance within each class.)</li>
</ul>
<p>我们把它拆开看：</p>
<ol>
<li><strong>类内方差最小化 (Minimize Within-class Variance):</strong>
<ul>
<li>我们希望<strong>同一个类别</strong>的数据点，在投影到新的维度后，它们的分布尽可能地集中和紧凑。就像把原来散开的队伍，重新整队变得密集一样。这表示“高内聚”。</li>
</ul>
</li>
<li><strong>类间距离最大化 (Maximize Between-class Distance):</strong>
<ul>
<li>我们希望<strong>不同类别</strong>的数据中心点，在投影后，它们之间的距离尽可能地远。这能让不同类别之间有一道清晰的“鸿沟”。这表示“低耦合”。</li>
</ul>
</li>
</ol>
<p>LDA 要找的，就是能够同时满足这两个条件的完美投影方向。</p>
<h2 id="lda-的工作原理">LDA 的工作原理</h2>
<p>   假设我们有一个数据集，包含 $C$ 个类别。</p>
<ul>
<li>$D_i$ ：第 $i$ 个类别 $ (c_i) $ 的样本集合</li>
<li>$N_i$ ：第 $i$ 个类别 $ (c_i) $ 中的样本数量</li>
<li>$x$ ：一个数据样本，多维列向量</li>
<li>$\mu_i$ ：第 $i$ 个类别的均值向量。<br>    $\mu_i = \frac{1}{N_i} \sum x \{x \in D_i\}$</li>
<li>$\mu$ ：全体样本的均值向量</li>
<li>$w$ ：要寻找的最佳投影方向。投影后的新坐标为 $z = w^Tx$ 。</li>
</ul>
<h3 id="类内散度矩阵">类内散度矩阵</h3>
<p>   “类内方差最小化“指的是同一个类别内部的数据点要尽可能紧凑。我们用 <strong>类内散度矩阵(Within-class Scatter Matrix) $S_w$</strong> 来衡量这一点。</p>
<p>   首先，对于单个类别 $c_i$ ，其散度矩阵 $S_i$ 定义为该类中所有样本点到其均值 $\mu_i$ 的协方差矩阵(乘以常数 $N_i$)。</p>
$$
S_i = \sum_{x \in D_i} (x - \mu_i)(x - \mu_i)^T
$$<p>
这里 $(x - \mu_i)(x - \mu_i)^T$ 是一个外积，结果是一个矩阵，它捕捉了数据的分散情况。<br>然后，我们将所有类别的散度矩阵相加，就得到了总的类内散度矩阵 $S_w$ ：</p>
$$
S_w = \sum_{i=1}^{C} S_i = \sum_{i=1}^{C} \sum_{x \in D_i} (x - \mu_i)(x - \mu_i)^T
$$<p>
$S_w$ 综合了所有类别内部的分散程度。我们的目标是让数据在投影后的类内散度尽可能小。</p>
<h3 id="类间散度矩阵">类间散度矩阵</h3>
<p>   “类间距离最大化”指的是不同类别的中心点要尽可能分开。我们用 <strong>类间散度矩阵 (Between-class Scatter Matrix) $S_b$</strong> 来衡量。<br>    $S_b$ 衡量的是各个类别的均值向量 $\mu_i$ 相对于全局均值 $\mu$ 的分散程度，并用该类的样本数 $N_i$ 进行加权。</p>
$$
S_b = \sum_{i=1}^{C} N_i (\mu_i - \mu)(\mu_i - \mu)^T
$$<p>
$S_b$ 越大，说明不同类别中心点之间的距离越远。我们的目标是让数据在投影后的类间散度尽可能大。</p>
<h3 id="构建目标函数">构建目标函数</h3>
<p>   现在，我们有了 $S_w$ 和 $S_b$ 。它们都是矩阵，一个矩阵包含了非常丰富的信息：</p>
<ul>
<li>对角线元素代表了在各个坐标轴方向上的方差。</li>
<li>非对角线元素代表了不同坐标轴之间的协方差(数据分布的倾斜方向和程度)。</li>
</ul>
<p>   这带来了一个核心难题：我们如何判断散度矩阵的“优劣”？我们无法直接比较 $S_w$ 和 $S_b$ 这两个矩阵本身的大小。一个矩阵可能在 X 方向方差大，另一个在 Y 方向方差大，无法简单地说哪个“更优”。所以 LDA 的目标不是在原始的多维空间里进行比较，而是要<strong>降维</strong>。<br>    简单来说，<strong>因为散度矩阵本身描述的是多维度空间中的“总体”混乱程度，而LDA的目标是找到一个“一维”的最佳观察视角（投影线）。因此，我们必须有一种方法来“测量”和“比较”在不同观察视角下，这种混乱程度变成了多少。向量投影 $w^T S w$ 正是完成这个测量的数学工具。</strong> <br>   来做一个简单的推导：假设我们只考虑一个类别，它的散度矩阵是 $S = \sum (x-\mu)(x-\mu)^T$ 。</p>
<ol>
<li><strong>原始数据点</strong>：$x$ (一个向量)</li>
<li><strong>投影后的数据点</strong>：$z = w^T x$ (一个标量，也就是一维直线上的一个坐标点)</li>
<li><strong>原始均值</strong>：$\mu$ (一个向量)</li>
<li><strong>投影后的均值：</strong> $\bar{z} = w^T \mu$ (一个标量)</li>
</ol>
<p>现在，我们来计算投影后这些点 z 的总方差(也就是总散度)：</p>
<ul>
<li><strong>总方差的定义：</strong>  $$ \sum (z_i - \bar{z})^2 $$</li>
<li><strong>代入上面的定义：</strong> $$    \sum (w^T x_i - w^T \mu)^2    $$</li>
<li><strong>提取公共项 $w^T$：</strong> $$    \sum (w^T (x_i - \mu))^2    $$</li>
</ul>
<p>这里的 $w^T (x_i - \mu)$ 是一个标量。对于任何标量 a，我们都有 $a^2 = a \cdot a^T$。所以：</p>
<ul>
<li><strong>展开平方项：</strong>    $$    \sum \left[ w^T (x_i - \mu) \right] \left[ w^T (x_i - \mu) \right]^T    $$</li>
<li>根据矩阵转置法则 $(AB)^T = B^T A^T$，我们对第二部分进行转置：$\left[ (x_i - \mu)^T w \right]$</li>
<li><strong>重新组合：</strong>    $$    \sum w^T (x_i - \mu) (x_i - \mu)^T w    $$</li>
<li><strong>提出 $w$：</strong>    $$    w^T \left( \sum (x_i - \mu) (x_i - \mu)^T \right) w =w^TSw   $$</li>
</ul>
<p>这样，我们就把多维的散度矩阵“压缩”成了一个能够代表投影线上散度大小的标量。</p>
<p>   LDA 的目标就是找到一个投影方向 $w$ ，使得投影后的类间散度尽可能大，同时类内散度尽可能小。这自然地导出了一个目标函数，即两者的比值，我们称之为 <strong>费雪判别准则 (Fisher&rsquo;s Criterion)：</strong></p>
$$
J(w)=\frac{投影后的类间散度}{投影后的类内散度} = \frac{w^TS_b w}{w^TS_w w}
$$<p>
$J(w)$ 也被称为 <strong>广义瑞利商 (Generalized Rayleigh Quotient)</strong> 。这时的任务是一个最优化问题，即找到一个 $w$ ，使 $J(w)$ 最大化。</p>
<h3 id="求解最佳投影">求解最佳投影</h3>
<p>   由瑞利商的性质，我们实际上可直接得到最佳投影的结果，在这里对其做一定的推导。<br>   注意到，对于一个解 $w$ ，任意缩放 $k \cdot w$ (其中 $k$ 是常数)并不会改变 $J(w)$ 的值，因为分子分母中的 $k^2$ 会被约掉，说明我们只关心 $w$ 的方向，不关心其大小。<br>   因此，我们可以为求解过程增加一个约束条件来固定分母，例如令 $w^TS_w w=1$ ，这样，问题转化为：</p>
$$
\begin{align*}
max &  \quad w^T S_b w \\
s.t. & \quad w^T S_w w = 1
\end{align*}
$$<p>
这是一个典型的 <strong>拉格朗日乘数法 (Lagrange Multiplier)</strong> 可以解决的约束优化问题，构造拉格朗日函数 $L(w, \lambda)$ ：</p>
$$
L(w, \lambda) = w^T S_b w - \lambda (w^T S_w w - 1)
$$<p>
对 $w$ 求导并令其为 0：</p>
$$
\frac{\partial L}{\partial w} = 2S_b w - 2\lambda S_w w = 0
$$<p>
整理后得到：</p>
$$
S_b w = \lambda S_w w
$$<p>
通常 $S_w$ 是可逆的，我们可以把它移到左边：</p>
$$
S_w^{-1} S_b w = \lambda w
$$<p>
这正是矩阵的广义特征值问题。</p>
<ul>
<li>$\lambda$ 是矩阵 $S_w^{-1}S_b$ 的特征值。</li>
<li>$w$ 是对应的特征向量。</li>
</ul>
<p>这个方程的解，就是我们所有局部最优解的集合。</p>
<h2 id="特征值求解与投影">特征值求解与投影</h2>
<p>  上一节的推导告诉我们，LDA 的优化目标最终归结为一个特征值问题。</p>
<p> * <strong>特征值 $\lambda$ 的意义</strong>: 根据瑞利商的性质，每一个特征值 $\lambda$ 都直接对应了其特征向量 $w$ 作为投影方向时的输出 $J(w)$。</p>
<ul>
<li><strong>全局最优解</strong>: 因此，我们不需要担心陷入局部最优。我们只需要遍历所有的特征值，然后找到最大的那一个，其对应的特征向量就是全局最优的投影方向。</li>
</ul>
<p><strong>求解步骤如下：</strong></p>
<ol>
<li>根据数据计算类内散度矩阵 $S_w$ 和类间散度矩阵 $S_b$。</li>
<li>求解矩阵 $S_w^{-1}S_b$ 的特征值和特征向量。</li>
<li>将所有特征值从大到小排序：$\lambda_1 \ge \lambda_2 \ge ... \ge \lambda_d$。</li>
<li>根据我们希望降到的维度 $k$，选取前 $k$ 个最大的特征值对应的特征向量，组成一个投影矩阵 $W = [w_1, w_2, ..., w_k]$。</li>
</ol>
<p>这个矩阵 $W$ 就是我们将数据从高维空间投影到低维空间的“桥梁”。</p>
<h2 id="如何用lda进行分类">如何用LDA进行分类</h2>
<p>   找到了最佳投影矩阵 $W$ 之后，我们不仅完成了降维，还可以用它来进行分类。对于一个未知的新数据点 $x_{new}$：</p>
<ol>
<li><strong>投影</strong>: 将新数据点投影到我们找到的低维空间中：
$$
    z_{new} = W^T x_{new}
    $$</li>
<li><strong>计算距离</strong>: 分别计算这个新的投影点 $z_{new}$ 到<strong>每一个类别中心点的投影</strong> $\mu_{i\_proj} = W^T \mu_i$ 的距离。</li>
<li><strong>判定类别</strong>: $z_{new}$ 离哪个类别的中心投影最近，就将其判定为哪个类别。通常使用欧氏距离进行度量。</li>
</ol>
<h2 id="一个经典对比lda-与-pca">一个经典对比：LDA 与 PCA</h2>
<p>   很多人会将 LDA 与主成分分析 (PCA) 混淆，因为它们都能用来降维。但它们的目标和原理完全不同。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">特性</th>
          <th style="text-align: left"><strong>线性判别分析 (LDA)</strong></th>
          <th style="text-align: left"><strong>主成分分析 (PCA)</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>算法类型</strong></td>
          <td style="text-align: left"><strong>监督学习 (Supervised)</strong></td>
          <td style="text-align: left"><strong>无监督学习 (Unsupervised)</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>核心目标</strong></td>
          <td style="text-align: left">最大化<strong>类别可分性</strong></td>
          <td style="text-align: left">最大化<strong>数据方差</strong> (保留信息量)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>是否使用标签</strong></td>
          <td style="text-align: left"><strong>是</strong>，必须使用类别标签计算散度矩阵</td>
          <td style="text-align: left"><strong>否</strong>，完全不关心类别标签</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>应用侧重</strong></td>
          <td style="text-align: left">更侧重于<strong>分类</strong>任务</td>
          <td style="text-align: left">更侧重于<strong>数据压缩、可视化</strong></td>
      </tr>
  </tbody>
</table>
<p>简单说，如果降维的目的是为了后续更好地分类，LDA 通常是更好的选择。如果只是想压缩数据，同时尽量不丢失信息，那就用 PCA。</p>
<h2 id="lda的假设与局限">LDA的假设与局限</h2>
<p>   LDA 虽然强大，但不是万能的，它有一些基本的前提假设：</p>
<ol>
<li><strong>高斯分布</strong>: LDA 假设每个类别的数据都大致呈正态分布（高斯分布）。</li>
<li><strong>同方差性</strong>: 经典 LDA 假设所有类别的协方差矩阵是相同且满秩的。如果这个假设不成立（比如一个类别分布是圆形，另一个是细长的椭圆形），LDA 的效果会打折扣。此时，二次判别分析(QDA)可能是更好的选择。</li>
<li><strong>线性可分</strong>: LDA 寻找的是线性的决策边界。如果类别间的边界是高度非线性的，LDA 就无能为力了。</li>
<li><strong>对离群点敏感</strong>: 由于 LDA 的计算涉及到样本均值和散度，因此离群点（Outliers）会对模型的性能产生较大影响。</li>
</ol>
<hr>
]]></description></item><item><title>机器学习笔记(3)：线性模型</title><link>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B03%E7%BA%BF%E6%80%A7%E6%A8%A1%E5%9E%8B/</link><pubDate>Tue, 22 Jul 2025 23:26:29 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B03%E7%BA%BF%E6%80%A7%E6%A8%A1%E5%9E%8B/</guid><description><![CDATA[<div class="featured-image">
                <img src="/img.jpg" referrerpolicy="no-referrer">
            </div><blockquote>
<p>📷封面源于网络，侵删</p>
</blockquote>
<hr>
<h2 id="基本形式">基本形式</h2>
<p>   给定由 $d$ 个属性描述的示例 $\boldsymbol{x} = (x_1;x_2;\dots;x_d)$ ，其中 $x_i$ 是 $\boldsymbol{x}$ 在第 $i$ 个属性上的取值，线性模型(linear model)是指学得一个通过属性的线性组合来进行预测的函数：</p>
$$
f(\boldsymbol{x}) = w_1x_1 + w_2x_2 + \dots + w_dx_d + b
$$<p>
即</p>
$$
f(\boldsymbol{x}) = \boldsymbol{w}^T\boldsymbol{x} + b
$$<h2 id="线性回归">线性回归</h2>
<p>$\boldsymbol{w}$ 和 $b$ 就是要学得的参数。而确定$\boldsymbol{w}$ 和 $b$  的关键是如何衡量 $f(x)$ 和 $y$ 之间的差别，最常见 的度量是均方误差：</p>
$$
\begin{align*}
(w^*, b^*) &= \arg \min_{(w, b)} \sum_{i=1}^m (f(x_i) - y_i)^2 \\
 &= \arg \min_{(w, b)} \sum_{i=1}^m (y_i - wx_i - b)^2 
\end{align*}
$$<p>
求解 $w^*$ 和 $b^*$ 的过程，称为线性回归模型的最小二乘 <strong>&ldquo;参数估计&rdquo;</strong> ，令</p>
$$
E_{(w, b)} =  \sum_{i=1}^m (y_i - wx_i - b)^2
$$<p>
由于该式是一个凸函数，其全局最优解在梯度为零处，因此我们可将其对 $w$ 和 $b$ 分别求导，得</p>
$$
\begin{align*}
\frac{\partial E_{(w, b)}}{\partial w} &= 2\left(w \sum_{i=1}^m x_i^2 - \sum_{i=1}^m (y_i-b)x_i \right), \\
\frac{\partial E_{(w, b)}}{\partial b} &= 2\left(mb - \sum_{i=1}^m (y_i - wx_i) \right)
\end{align*}
$$<p>
分别令其为零，求解方程组可得到 $w$ 和 $b$ 的闭式解</p>
$$
\begin{align*}
w &= \frac{\sum_{i=1}^m y_i(x_i - \overline x)}{\sum_{i=1}^m x_i^2 - \frac{1}{m} (\sum_{i=1}^m x_i)^2} \\\\
b &= \frac{1}{m} \sum_{i=1}^m (y_i - wx_i)
\end{align*}
$$<p>
其中 $\overline x = \frac{1}{m} \sum_{i=1}^m x_i$ 为 $x$ 的均值。</p>
<p>   对于更一般的问题，样本会有多个属性，此时可行域和参数集都变为高维，我们可将均方误差扩展为欧氏距离，要求得的 <strong>“直线”</strong> 就是所有样本到其的欧式距离之和最小的直线，称为 <strong>“多元线性回归”</strong> 。</p>
<p>   方便起见，我们令 $\hat {\boldsymbol w} = (\boldsymbol w; b) = (w_1;...;w_d;b) \in \mathbb R^{(d+1) \times 1}$ ，$\hat{\boldsymbol x} = (x_{i1}; ...; x_{id}; 1) \in \mathbb R^{(d+1) \times 1}$ ，则有</p>
$$
\begin{align*}
\hat{\boldsymbol w}^* &= \arg \min_{\hat{\boldsymbol w}} \sum_{i=1}^m (y_i - \hat{\boldsymbol w}^T \hat{\boldsymbol x}_i)^2 \\\\
 &= \arg \min_{\hat{\boldsymbol w}} \sum_{i=1}^m (y_i - \hat{\boldsymbol x}_i^T \hat{\boldsymbol w})^2
\end{align*}
$$<p>利用向量内积的定义，有</p>
$$
\hat{\boldsymbol{w}}^* = \arg \min_{\hat{w}} \left[ y_1 - \hat{\boldsymbol{x}}_1^T \hat{\boldsymbol{w}} \quad \cdots \quad y_m - \hat{\boldsymbol{x}}_m^T \hat{\boldsymbol{w}} \right] \begin{bmatrix} y_1 - \hat{\boldsymbol{x}}_1^T \hat{\boldsymbol{w}} \\ \vdots \\ y_m - \hat{\boldsymbol{x}}_m^T \hat{\boldsymbol{w}} \end{bmatrix} \\
$$<p>
其中</p>
$$
\begin{align*}
\begin{bmatrix} y_1 - \hat{\boldsymbol{x}}_1^T \hat{\boldsymbol{w}} \\ \vdots \\ y_m - \hat{\boldsymbol{x}}_m^T \hat{\boldsymbol{w}} \end{bmatrix} &= \begin{bmatrix} y_1 \\ \vdots \\ y_m \end{bmatrix} - \begin{bmatrix} \hat{\boldsymbol{x}}_1^T \hat{\boldsymbol{w}} \\ \vdots \\ \hat{\boldsymbol{x}}_m^T \hat{\boldsymbol{w}} \end{bmatrix} \\\\
&= \boldsymbol{y} - \begin{bmatrix} \hat{\boldsymbol{x}}_1^T \\ \vdots \\ \hat{\boldsymbol{x}}_m^T \end{bmatrix} \hat{\boldsymbol{w}} \\\\
&= \boldsymbol{y} - \boldsymbol{X} \hat{\boldsymbol{w}}
\end{align*}
$$<p>
所以</p>
$$
\hat{\boldsymbol{w}}^* = \arg \min_{\hat{w}} (\boldsymbol{y} - \boldsymbol{X} \hat{\boldsymbol{w}})^T (\boldsymbol{y} - \boldsymbol{X} \hat{\boldsymbol{w}})
$$<p>
同理，可以用梯度为零的方法得到闭式解：</p>
$$
\hat{\boldsymbol{w}}^* = (\boldsymbol{X}^T\boldsymbol{X})^{-1}\boldsymbol{X}^T\boldsymbol{y}
$$<p>
此时要求 $\boldsymbol{X}^T\boldsymbol{X}$ 为满秩矩阵或正定矩阵。</p>
]]></description></item><item><title>基于跨帧相位差法实现混合信号分离</title><link>https://www.qinshiyue.icu/p/%E5%9F%BA%E4%BA%8E%E8%B7%A8%E5%B8%A7%E7%9B%B8%E4%BD%8D%E5%B7%AE%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%B7%B7%E5%90%88%E4%BF%A1%E5%8F%B7%E5%88%86%E7%A6%BB/</link><pubDate>Thu, 22 May 2025 14:12:14 +0800</pubDate><author>qinshiyue615@gmail.com (秦时月)</author><guid>https://www.qinshiyue.icu/p/%E5%9F%BA%E4%BA%8E%E8%B7%A8%E5%B8%A7%E7%9B%B8%E4%BD%8D%E5%B7%AE%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%B7%B7%E5%90%88%E4%BF%A1%E5%8F%B7%E5%88%86%E7%A6%BB/</guid><description><![CDATA[<div class="featured-image">
                <img src="/DSP.png" referrerpolicy="no-referrer">
            </div><h3 id="前言">前言</h3>
<p>   本文的问题背景是设计一个信号分离装置，可以分离由两个正弦波信号合成的混合信号，也就是说要测量两个原始信号的频率、幅度和相位，并尽可能提高精度。原始信号的频率范围为 $100Hz \sim 10kHz$  ，且信号的频率差最小为 $100Hz$ 。<br>   设计的难点在于测量精度，对于分析混合信号，我们会首先想到快速傅里叶变换，即 FFT ，但是 FFT 要做到 5% 甚至 1% 的精度，必须将采样点数取得足够大，根据奈奎斯特采样定律，本设计中采样率最低应为 $20kHz$ ，为了留有一些容差，实际采样率应大于 $25kHz$ ，此时要做到 $1Hz$ 的测量精度，采样点数就应取 $25kHz / 1Hz = 25000$ ，而 FFT 一般要求采样点数为 $2$ 的幂次方，所以必须取到 $32768$ ，显然对于单片机来说这是一个超大的数组，一般 STM32 又以 <code>uint16_t</code> 类型存储 ADC 采样值，这时采样数组所需要的内存就有 $2 \times 32768Bytes = 64KB$ ，而进行 FFT 时又需要将数据进行零填充或转化为复数存储，所需要的空间又会成倍数增长，对于单片机来说这样的大小是无法做到的，所以我们需要采取其他方法来提高测量精度。<br>    如标题所言，本文将介绍一种所谓跨帧相位差法的算法，理论上能够使测量频率无限逼近于真实频率，当频率测量足够准确时，幅度和相位就可以使用很简单的算法得到足够高的精度。在使用该算法之前，首先要得到频谱图中与信号真实频率最接近的频率 bin 索引。</p>
<hr>
<h2 id="在-fft-之前">在 FFT 之前</h2>
<p>   由傅里叶变换衍生而来的 <strong>快速傅里叶变换</strong>(Fast Fourier Transform, FFT) 能够将时域信息转化为频域信息，从而提供了分析信号的工具。理想的傅里叶变换需要求和整个时域的数据，即从0到无穷，而我们采样的点数是有限的，因此直接用采样得到的数据做 FFT 时，就会出现第一个问题：频谱泄露。</p>
<h3 id="频谱泄露">频谱泄露</h3>
<p>   专业地来说，当我们对一个时域信号进行离散傅里叶变换 (Discrete Fourier Transform, DFT) 以分析其频率成分时，如果这个信号不是周期性的，或者它的周期与我们进行DFT的窗口长度不完全匹配，那么信号的能量就会“泄露”到相邻的频率 bin (频率分辨率的最小单位) 中，原本应该集中在某个频率上的能量会扩散开来，形成一些不属于信号真实频率成分的“旁瓣”。</p>
<p>   想象一下，你用一个固定长度的“窗户”去观察一段无限长的波浪。如果这个波浪的完整周期正好能被这个窗户的长度完美包含，那么你看到的频率成分就会很干净。但是，如果波浪的周期和窗户的长度不匹配，那么在窗户的边缘就会出现“截断”效应，这个截断就相当于给原始信号乘以了一个非理想的窗函数。</p>
<p>   这个非理想的窗函数在频域上也有其对应的形状，它不是一个完美的冲击函数，而是具有一定的宽度和旁瓣。原始信号的真实频谱会和这个窗函数的频谱进行卷积，导致原始频谱的能量扩散到周围的频率上，形成了我们所说的频谱泄露。</p>
<p>   从数学角度来讲，给定一个原始连续信号 $y(t)$ ，采样的时间为 $[0, T]$ ，那么采样得到的离散时间信号 $x[n]$ 就可以看做是 $y(t)$ 乘以一个矩形窗函数 $w(t)$ ：</p>
$$
w(t) = \begin{cases}
	1, \quad 0 \leq t \leq T \\
	0, \quad otherwises
\end{cases}
$$<p>
然后对 $y(t) w(t)$ 采样得到 $x[n]$ 。不妨先令 $x[n] = y[n]w[n]$ ，根据傅里叶变换相关定理，时域的乘积的傅里叶变换就是频域的卷积，即 $X(e^{jw}) = Y(e^{jw}) * W(e^{jw})$ ，显然这不是我们想要的结果，事实上，矩形窗函数的频域函数为 $sinc$ 函数，使用 Matlab 等工具可以看到频谱的形状：</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-matlab">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-matlab" data-lang="matlab"><span class="line"><span class="cl"><span class="n">N</span> <span class="p">=</span> <span class="mi">64</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rectangular_window</span> <span class="p">=</span> <span class="nb">ones</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">N</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">rectangular_window</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">:</span><span class="mi">16</span><span class="p">)</span> <span class="p">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">rectangular_window</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">49</span><span class="p">:</span><span class="mi">64</span><span class="p">)</span> <span class="p">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">fft_rectangular_window</span> <span class="p">=</span> <span class="n">fft</span><span class="p">(</span><span class="n">rectangular_window</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">frequency_axis</span> <span class="p">=</span> <span class="nb">linspace</span><span class="p">(</span><span class="o">-</span><span class="nb">pi</span><span class="p">,</span> <span class="nb">pi</span> <span class="o">-</span> <span class="mi">2</span><span class="o">*</span><span class="nb">pi</span><span class="o">/</span><span class="n">N</span><span class="p">,</span> <span class="n">N</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">figure</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">subplot</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">stem</span><span class="p">(</span><span class="mi">0</span><span class="p">:</span><span class="n">N</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">rectangular_window</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">title</span><span class="p">(</span><span class="s">&#39;矩形窗时域波形&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">xlabel</span><span class="p">(</span><span class="s">&#39;采样点 (n)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">ylabel</span><span class="p">(</span><span class="s">&#39;幅度&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">grid</span> <span class="n">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">subplot</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">plot</span><span class="p">(</span><span class="n">frequency_axis</span><span class="p">,</span> <span class="nb">abs</span><span class="p">(</span><span class="n">fftshift</span><span class="p">(</span><span class="n">fft_rectangular_window</span><span class="p">))</span><span class="o">/</span><span class="n">N</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">title</span><span class="p">(</span><span class="s">&#39;矩形窗频域幅度谱 (线性尺度)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">xlabel</span><span class="p">(</span><span class="s">&#39;归一化角频率 (弧度/采样)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">ylabel</span><span class="p">(</span><span class="s">&#39;幅度&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">grid</span> <span class="n">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">xlim</span><span class="p">([</span><span class="o">-</span><span class="nb">pi</span><span class="p">,</span> <span class="nb">pi</span><span class="p">]);</span></span></span></code></pre></div></div>

<br>

<p>   频谱泄露会带来一些问题：</p>
<ul>
<li><strong>频率分辨率降低：</strong> 本来很接近的两个频率成分可能会因为能量泄露而模糊在一起，难以区分。</li>
<li><strong>幅度估计不准确：</strong> 泄露的能量会影响到真实频率成分的幅度估计。</li>
<li><strong>产生虚假的频率成分：</strong> 一些本来不存在的频率成分（旁瓣）可能会被误认为是信号的一部分。</li>
</ul>
<h3 id="处理采样数据">处理采样数据</h3>
<p>   要减轻频谱泄露，常用的有以下几种方法：</p>
<ol>
<li>
<p><strong>加窗(Windowing)</strong></p>
<p>对时域信号乘以窗函数以减小截断效应：</p>
<ul>
<li>常用窗函数：汉宁窗（Hanning）、汉明窗（Hamming）、布莱克曼窗（Blackman）、凯泽窗（Kaiser）、布莱克曼-哈里斯窗(Blackman-Harris)</li>
<li>窗函数可以减少主瓣宽度或降低旁瓣能量，从而减小频谱泄露。</li>
</ul>
</li>
<li>
<p><strong>增加采样点数(Zero Padding)</strong></p>
<p>在原始信号后添加零点(Zero Padding)，提高频率分辨率，使频谱更平滑，便于观察泄露现象。</p>
</li>
<li>
<p><strong>选择整数周期采样</strong></p>
<p>在截取信号时，尽可能让信号包含整数个周期，可显著减少频谱泄露。</p>
</li>
</ol>
<p>   其中加窗和增加采样点数是最常用的方法，但是如果要使用针对 arm 优化后的 CMSIS-DPS 库进行 FFT ，其中的 FFT 函数有点数上的限制，无法随意地进行零填充，因此加窗其实是更普适的方法，在这里给出一些常用的窗函数生成代码，为提高运行速度使用 arm_math 中的函数</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-C">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="c1">// FFT_SIZE 是要进行 FFT 的点数
</span></span></span><span class="line"><span class="cl"><span class="c1">// 汉宁窗
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">FFT_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">window</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.5f</span> <span class="o">*</span> <span class="p">(</span><span class="mf">1.0f</span> <span class="o">-</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl"><span class="c1">// 汉明窗
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">FFT_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">window</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.54f</span> <span class="o">-</span> <span class="mf">0.46f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 布莱克曼窗
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">FFT_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">window</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.42323f</span> <span class="o">-</span> <span class="mf">0.49755f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">+</span> <span class="mf">0.07922f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">4.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl"><span class="c1">// 布莱克曼-哈里斯窗
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">FFT_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>	
</span></span><span class="line"><span class="cl">    <span class="n">window</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.35875f</span> <span class="o">-</span> <span class="mf">0.48829f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">+</span> <span class="mf">0.14128f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">4.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">-</span> <span class="mf">0.01168f</span> <span class="o">*</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="mf">6.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="p">(</span><span class="n">FFT_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">));</span></span></span></code></pre></div></div>
<h3 id="得到频率-bin-索引">得到频率 bin 索引</h3>
<p>   DFT将时域信号转换到频域，频域上的输出也是离散的，分布在所谓的<strong>频率“桶”(frequency bins)</strong> 里。每个频率桶代表一个特定的频率。频率分辨率就是这些相邻频率桶之间的间隔。</p>
<p>   想象一下，在一个固定的时间窗口 $T$ 内观察一个信号。在这个时间窗口内，能够被完整观察到的最低频率（除了直流分量 $0Hz$ 之外）就是其周期恰好等于 $T$ 的频率。这个频率就是 $f_{min} = 1/T$。</p>
<p>   这个 $1/T$ 就是我们能分辨出来的最小频率变化量。任何频率变化如果小于这个值，在长度为 T 的观测窗口内就无法完整地展现出一个周期的差异，从而难以区分。</p>
<p>   显然，这个时间窗口就是采样的总时长，即 $T = NT_S = N/f_s$ ，将其代入最小频率表达式，有</p>
$$
\Delta f = \frac{f_s}{N} \tag 1
$$<h2 id="跨帧相位差法测频">跨帧相位差法测频</h2>
<p>   加窗并进行 FFT 之后，计算频域幅值得到频谱图，就可以得到每个信号的频率 bin 索引，至此完成了算法的前期准备工作，那么，接下来该怎么做呢？</p>
<p>   首先，设信号为正弦波，那么时域中采样得到的数据可表示为</p>
$$
x[n] = Asin(2\pi fnT_s + \varphi_0) \tag 2
$$<p>
 其中 $T_s = \frac{1}{f_s}$ 为采样周期，$f_s$ 为采样频率，我们知道频谱图中的频率分辨率为 $\Delta f = f_s/N$ ，那么对于真实频率 $f$ ，我们假定一个理想索引</p>
$$
m = \frac{f}{\Delta f} \tag 3
$$<p>
此处 $m$ 不一定为整数，因为无法保证所测频率一定卡在频谱图的栅格上。此时真实频率 $f$ 可以表示为</p>
$$
f = m \Delta f = m \frac{f_s}{N} \tag 4
$$<p>
因此我们只要找到这个 $m$ 的值，就可以精准得到真实频率。跨帧相位差法就是间接地计算 $m$ ，从而得到精度极高的测量频率。</p>
<p>   我们先将正弦转换到复数域中：</p>
$$
x[n] = \frac{A}{2j}[e^{j2\pi fnT_s + \varphi_0} + e^{-j2\pi fnT_s - \varphi_0}] \tag 5
$$<p>
对一帧 $N$ 点实信号 $x[n]$ ，我们知道其第 $k$ 个 DFT 系数为</p>
$$
X_k = \sum_{n = 0}^{N-1} x[n]e^{-j2\pi kn/N} \tag 6
$$<p>
所以我们对正频率一侧做 DFT ，有</p>
$$
\begin{align*}
X_k &=\frac{A}{2j} \sum_{n = 0}^{N-1} e^{j2\pi fnT_s + \varphi_0}e^{-j2\pi kn/N} \\\\
&= \frac{A}{2j} \sum_{n = 0}^{N-1} e^{j\varphi_0}e^{j2\pi(n-\frac{N}{2})T_s+j2\pi f\frac{N}{2}T_s - j2\pi kn/N} \\\\
&= \frac{A}{2j} \sum_{n = 0}^{N-1} e^{j\varphi_0}e^{j2\pi(n-\frac{N}{2})(m-k)/N-j\pi (k-m)} \tag 7
\end{align*}
$$<p>
我们不需要关注求和部分，因此将求和无关项提出，这里需要注意如果加了窗函数，那么求和部分也会有窗函数的影响，</p>
$$
X_k = AW(offset)e^{j\varphi_0}e^{-j\pi (k-m)} \tag 8
$$<p>其中 $W(offset)$ 是由窗函数和偏移量共同决定的复因子。</p>
<p>   已知一帧长度为 $N$ ，采样周期为 $T_s$ ，可以得到一帧的时长</p>
$$
T = NT_s	\tag 9
$$<p>
把同一信号往后拖一帧，即 $n' = n + N$ ，那么有</p>
$$
\begin{align*}
x[n'] &= Asin(2\pi fnT_s + 2\pi fNT_s + \varphi_0) \\\\
&= Asin(2\pi fnT_s + 2\pi fT + \varphi_0) \tag {10}
\end{align*}
$$<p>
再次做 DFT ，容易得到</p>
$$
X_k' \propto e^{j(\varphi_0 + 2\pi fT)}
$$<p>
可以看出，同一点的 DFT 系数，在时域信号延后了一帧之后，相位仅多了转过的角度 $2\pi fT$ ，不妨设 $\Delta \varphi = 2\pi fT$ ，即两点绝对相位差，则</p>
$$
f = \frac{\Delta \varphi}{2\pi T}	\tag {11}
$$<p>
显然，如果我们能得到这个绝对相位差，就能完全复原出真实频率。但是我们计算相位差的过程实际上是两点测量的相位作差，得到的结果 $\Delta \varphi_{mea} \in [-\pi, \pi]$ ，这个结果实际上没有加上转过的整圈数，也就是说</p>
$$
\begin{align*}
\Delta \varphi_{mea} &= \Delta \varphi \ \ mod \ \ 2\pi \\\\
&= 2\pi fT \ \ mod \ \ 2\pi		\tag {12}
\end{align*}
$$<p>
注意到我们之前标定的理想索引 $m$ ，</p>
$$
2\pi fT = \frac{2\pi fN}{f_s} = 2\pi m
$$<p>
设 $\Delta bi n = m-k$ ，则</p>
$$
\begin{align*}	\tag {13}
2\pi fT &=2k\pi + 2\pi \Delta bin \\\\
\frac{\Delta \varphi_{mea}}{2\pi T} &= \frac{2\pi fT \ \ mod \ \ 2\pi}{2\pi T} \\\\
&= f -\frac{k}{T}		\tag {14}
\end{align*}
$$<p>
所以真实频率为</p>
$$
f = \frac{k}{T} + \frac{\Delta \varphi_{mea}}{2\pi T}	\tag {15}
$$<p>
   根据此算法，理论上可测得频率无限接近于真实频率，即使考虑到采样过程中存在的噪声以及计算的误差，也可以达到极高的精度，经笔者测试，平均误差仅有 $0.002\%$ 。</p>
<p>   在这里给出实现算法的主要程序，注意要连续采两帧数据运行两次：</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-C">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="c1">// 开启FFT
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">FFT_start</span><span class="p">(</span><span class="n">BLACKMAN_HARRIS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl"><span class="c1">// 找到两信号粗估计bin下标
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">uint32_t</span> <span class="n">k1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">k2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nf">find_peaks</span><span class="p">(</span><span class="o">&amp;</span><span class="n">k1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">k2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">float32_t</span> <span class="n">f1</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">k1</span> <span class="o">*</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">SAMPLE_RATE</span> <span class="o">/</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">FFT_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">float32_t</span> <span class="n">f2</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">k2</span> <span class="o">*</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">SAMPLE_RATE</span> <span class="o">/</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">FFT_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 相位差法进一步精确
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">float32_t</span> <span class="o">*</span><span class="n">c1</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">fft_outputbuf</span><span class="p">[</span><span class="n">k1</span> <span class="o">*</span> <span class="mi">2U</span><span class="p">];</span>	<span class="c1">// 得到复数频率点
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">float32_t</span> <span class="o">*</span><span class="n">c2</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">fft_outputbuf</span><span class="p">[</span><span class="n">k2</span> <span class="o">*</span> <span class="mi">2U</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="kt">float32_t</span> <span class="n">phi1_now</span><span class="p">,</span> <span class="n">phi2_now</span><span class="p">;</span>				<span class="c1">// 计算当前相位
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">arm_atan2_f32</span><span class="p">(</span><span class="n">c1</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">c1</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">phi1_now</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">arm_atan2_f32</span><span class="p">(</span><span class="n">c2</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">c2</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">phi2_now</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">float32_t</span> <span class="n">frameT</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">FFT_SIZE</span> <span class="o">/</span> <span class="p">(</span><span class="kt">float32_t</span><span class="p">)</span><span class="n">SAMPLE_RATE</span><span class="p">;</span>	<span class="c1">// 频移周期，即每帧间隔 4096 / 40k = 0.1024 s
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">prev</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">k</span> <span class="o">==</span> <span class="n">k1</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">phi_prev</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="nf">arm_atan2_f32</span><span class="p">(</span><span class="n">prev</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">im</span><span class="p">,</span> <span class="n">prev</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">re</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">phi_prev</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">delta_phi</span> <span class="o">=</span> <span class="n">phi1_now</span> <span class="o">-</span> <span class="n">phi_prev</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">delta_phi</span> <span class="o">&gt;</span>  <span class="n">M_PI</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">		<span class="n">delta_phi</span> <span class="o">-=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">delta_phi</span> <span class="o">&lt;</span> <span class="o">-</span><span class="n">M_PI</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">		<span class="n">delta_phi</span> <span class="o">+=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">f1</span> <span class="o">+=</span> <span class="n">delta_phi</span> <span class="o">/</span> <span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">frameT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">prev</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">k</span> <span class="o">==</span> <span class="n">k2</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">phi_prev</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="nf">arm_atan2_f32</span><span class="p">(</span><span class="n">prev</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">im</span><span class="p">,</span> <span class="n">prev</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">re</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">phi_prev</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">delta_phi</span> <span class="o">=</span> <span class="n">phi2_now</span> <span class="o">-</span> <span class="n">phi_prev</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">delta_phi</span> <span class="o">&gt;</span>  <span class="n">M_PI</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">		<span class="n">delta_phi</span> <span class="o">-=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">delta_phi</span> <span class="o">&lt;</span> <span class="o">-</span><span class="n">M_PI</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">		<span class="n">delta_phi</span> <span class="o">+=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">f2</span> <span class="o">+=</span> <span class="n">delta_phi</span> <span class="o">/</span> <span class="p">(</span><span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">frameT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 更新前帧结构体
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">prev</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">bin_prev_t</span><span class="p">){</span><span class="n">c1</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">c1</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">k1</span><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">prev</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">bin_prev_t</span><span class="p">){</span><span class="n">c2</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">c2</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">k2</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl"><span class="n">tones</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">f</span> <span class="o">=</span> <span class="n">f1</span><span class="p">;</span>  <span class="n">tones</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">f</span> <span class="o">=</span> <span class="n">f2</span><span class="p">;</span></span></span></code></pre></div></div>
<h2 id="最小二乘法测幅测相">最小二乘法测幅测相</h2>
<p>   在测频算法中已经得到了精度较高的频率，因此使用最小二乘法测量幅度与相位能达到很好的效果。下面介绍一下最小二乘法：</p>
<p>   对于</p>
$$
x[n] = A_1cos(w_1+\varphi_1)+A_2cos(w_2n+\varphi_2)+\varepsilon[n]	\tag 1
$$<p>
其中 $w_1$ ，$w_2$ 已通过 FFT 和相位差法标定，$\varepsilon[n]$ 为残差，有</p>
$$
\begin{align*}
A_kcos(w_kn+\varphi_k) &= A_k[cos(\varphi_k)cos(w_kn)-sin(\varphi_k)sin(w_kn)] \\\\
&= I_kcos(w_kn)-Q_ksin(w_kn)	\tag 2
\end{align*}
$$<p>
其中 $I_k = A_kcos(\varphi_k)$ ，$Q_k = A_ksin(\varphi_k)$ ，于是有</p>
$$
\begin{align*}
x[n] &= I_1C_{1n} - Q_1S_{1n} + I_{2n} - Q_2S_{2n} + \varepsilon [n] \\\\
&= \boldsymbol{h}_n^T\boldsymbol{\theta} + \varepsilon[n] 	\tag 3
\end{align*}
$$<p>
其中</p>
$$
\begin{align*}
&\boldsymbol{h}_n = [C_{1n}, -S_{1n}, C_{2n}, -S_{2n}]^T \\\\
&\boldsymbol{\theta} = [I_1,Q_1,I_2,Q_2]^T \\\\
& C_{kn} = cos(w_kn), \ S_{kn} = sin(w_kn) 
\end{align*}
$$<p>
将其写成矩阵形式，</p>
$$
\boldsymbol{X} = \boldsymbol{H\theta} + \boldsymbol{\varepsilon}, \ \boldsymbol{H}\in R^{N \times 4} 		\tag 4
$$<p>
其中，$N$ 为采样点数，</p>
$$
\begin{align*}
&\boldsymbol{X} = \Big[x[0], x[1], \dots, x[N-1]\Big]^T \\\\
&\boldsymbol{H}= \begin{bmatrix} C_{11} &-S_{11} &C_{21} &-S_{21} \\
								 C_{12} &-S_{12} &C_{22} &-S_{22}  \\
								 {}     &{} \vdots	\\
								 C_{1N} &-S_{1N} &C_{2N} &-S_{2N}
				  \end{bmatrix}_{N \times 4} \\\\
&\boldsymbol{\varepsilon} = \Big[\varepsilon [0],\varepsilon [1], \dots,\varepsilon [N-1] \Big]^T
\end{align*}
$$<p>
   最小二乘估计定义为：</p>
$$
\hat\theta = argmin_\theta ||\boldsymbol{X}-\boldsymbol{H\theta}||^2=argmin_\theta \sum_n \varepsilon[n]^2 				\tag 5
$$<p>
令</p>
$$
\frac{\partial ||\boldsymbol{X}-\boldsymbol{H\theta}||^2}{\partial \boldsymbol{\theta}} = -2\boldsymbol{H}^T(\boldsymbol{X}-\boldsymbol{H\theta}) = \boldsymbol{0}
$$<p>
得到</p>
$$
\boldsymbol{H}^T\boldsymbol{H}\boldsymbol{\hat\theta}=\boldsymbol{H}^T\boldsymbol{X}	\tag 6
$$<p>
使用高斯消元法求解该线性方程组，即可得到</p>
$$
\begin{align*}
\boldsymbol{\hat\theta} &= [I_1,Q_1,I_2,Q_2]^T \\\\
&= [A_1cos{\varphi_1}, A_1sin{\varphi_1},A_2cos{\varphi_2},A_2sin{\varphi_2}]^T	\tag 7
\end{align*}
$$<p>
此时即可计算出幅度与相位</p>
$$
A_k = \sqrt{I_k^2 + Q_k^2} \\\\
\varphi_k = tan^{-1} \frac{Q_k}{I_k}
$$<p>
   最小二乘代码实现如下：</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-c">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="复制到剪贴板"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief       最小二乘法计算幅度与相位
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @note        无
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param       f1, f2:            已确定频率
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param       x:                 采样序列
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param       I1, Q1, I2, Q2:    求解参数组
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @retval      无
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">least_square</span><span class="p">(</span><span class="kt">float32_t</span> <span class="n">f1</span><span class="p">,</span> <span class="kt">float32_t</span> <span class="n">f2</span><span class="p">,</span> <span class="k">const</span> <span class="kt">float32_t</span> <span class="o">*</span><span class="n">x</span><span class="p">,</span> <span class="kt">float32_t</span> <span class="o">*</span><span class="n">I1</span><span class="p">,</span> <span class="kt">float32_t</span> <span class="o">*</span><span class="n">Q1</span><span class="p">,</span> <span class="kt">float32_t</span> <span class="o">*</span><span class="n">I2</span><span class="p">,</span> <span class="kt">float32_t</span> <span class="o">*</span><span class="n">Q2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 计算一次角增量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">float32_t</span> <span class="n">w1</span> <span class="o">=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">f1</span> <span class="o">/</span> <span class="n">SAMPLE_RATE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">w2</span> <span class="o">=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">M_PI</span> <span class="o">*</span> <span class="n">f2</span> <span class="o">/</span> <span class="n">SAMPLE_RATE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">    <span class="c1">// 迭代生成sin, cos序列
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">float32_t</span> <span class="n">c1</span> <span class="o">=</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="n">w1</span><span class="p">),</span> <span class="n">s1</span> <span class="o">=</span> <span class="nf">arm_sin_f32</span><span class="p">(</span><span class="n">w1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">c2</span> <span class="o">=</span> <span class="nf">arm_cos_f32</span><span class="p">(</span><span class="n">w2</span><span class="p">),</span> <span class="n">s2</span> <span class="o">=</span> <span class="nf">arm_sin_f32</span><span class="p">(</span><span class="n">w2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">cn1</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">sn1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">cn2</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">sn2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">Scc1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Sss1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Scc2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Sss2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">Scc12</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Scs12</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Ssc12</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">Sss12</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">SxC1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">SxS1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">SxC2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span><span class="n">SxS2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">n</span> <span class="o">&lt;</span> <span class="n">FFT_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">n</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float32_t</span> <span class="n">xn</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="n">n</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 同频能量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">Scc1</span> <span class="o">+=</span> <span class="n">cn1</span> <span class="o">*</span> <span class="n">cn1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Sss1</span> <span class="o">+=</span> <span class="n">sn1</span> <span class="o">*</span> <span class="n">sn1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Scc2</span> <span class="o">+=</span> <span class="n">cn2</span> <span class="o">*</span> <span class="n">cn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Sss2</span> <span class="o">+=</span> <span class="n">sn2</span> <span class="o">*</span> <span class="n">sn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 跨频交叉项
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">Scc12</span> <span class="o">+=</span> <span class="n">cn1</span> <span class="o">*</span> <span class="n">cn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Scs12</span> <span class="o">+=</span> <span class="n">cn1</span> <span class="o">*</span> <span class="n">sn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Ssc12</span> <span class="o">+=</span> <span class="n">sn1</span> <span class="o">*</span> <span class="n">cn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Sss12</span> <span class="o">+=</span> <span class="n">sn1</span> <span class="o">*</span> <span class="n">sn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 数据投影
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">SxC1</span> <span class="o">+=</span> <span class="n">xn</span> <span class="o">*</span> <span class="n">cn1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">SxS1</span> <span class="o">+=</span> <span class="n">xn</span> <span class="o">*</span> <span class="n">sn1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">SxC2</span> <span class="o">+=</span> <span class="n">xn</span> <span class="o">*</span> <span class="n">cn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">SxS2</span> <span class="o">+=</span> <span class="n">xn</span> <span class="o">*</span> <span class="n">sn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 迭代sin/cos
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="kt">float</span> <span class="n">ncn1</span> <span class="o">=</span> <span class="n">cn1</span> <span class="o">*</span> <span class="n">c1</span> <span class="o">-</span> <span class="n">sn1</span> <span class="o">*</span> <span class="n">s1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float</span> <span class="n">nsn1</span> <span class="o">=</span> <span class="n">sn1</span> <span class="o">*</span> <span class="n">c1</span> <span class="o">+</span> <span class="n">cn1</span> <span class="o">*</span> <span class="n">s1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float</span> <span class="n">ncn2</span> <span class="o">=</span> <span class="n">cn2</span> <span class="o">*</span> <span class="n">c2</span> <span class="o">-</span> <span class="n">sn2</span> <span class="o">*</span> <span class="n">s2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float</span> <span class="n">nsn2</span> <span class="o">=</span> <span class="n">sn2</span> <span class="o">*</span> <span class="n">c2</span> <span class="o">+</span> <span class="n">cn2</span> <span class="o">*</span> <span class="n">s2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">cn1</span> <span class="o">=</span> <span class="n">ncn1</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">        <span class="n">sn1</span> <span class="o">=</span> <span class="n">nsn1</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">        <span class="n">cn2</span> <span class="o">=</span> <span class="n">ncn2</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">        <span class="n">sn2</span> <span class="o">=</span> <span class="n">nsn2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">    <span class="c1">// 构建正交矩阵和投影向量，部分交叉项可近似为0
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">float32_t</span> <span class="n">H</span><span class="p">[</span><span class="mi">4</span><span class="p">][</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="n">Scc1</span><span class="p">,</span>    <span class="mi">0</span><span class="p">,</span>      <span class="n">Scc12</span><span class="p">,</span>  <span class="n">Scs12</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="mi">0</span><span class="p">,</span>       <span class="n">Sss1</span><span class="p">,</span>   <span class="n">Ssc12</span><span class="p">,</span>  <span class="n">Sss12</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="n">Scc12</span><span class="p">,</span>   <span class="n">Ssc12</span><span class="p">,</span>  <span class="n">Scc2</span><span class="p">,</span>   <span class="mi">0</span>    <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="n">Scs12</span><span class="p">,</span>   <span class="n">Sss12</span><span class="p">,</span>  <span class="mi">0</span><span class="p">,</span>      <span class="n">Sss2</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>	
</span></span><span class="line"><span class="cl">    <span class="kt">float32_t</span> <span class="n">b</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">SxC1</span><span class="p">,</span> <span class="n">SxS1</span><span class="p">,</span> <span class="n">SxC2</span><span class="p">,</span> <span class="n">SxS2</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">    <span class="c1">// 高斯消元
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">k</span><span class="o">++</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 主元选择
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="kt">int</span> <span class="n">m</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float_t</span> <span class="n">maxv</span> <span class="o">=</span> <span class="nf">fabsf</span><span class="p">(</span><span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">k</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span><span class="p">(</span><span class="nf">fabsf</span><span class="p">(</span><span class="n">H</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">])</span> <span class="o">&gt;</span> <span class="n">maxv</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="n">m</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="n">maxv</span> <span class="o">=</span> <span class="nf">fabsf</span><span class="p">(</span><span class="n">H</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 交换行		
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kt">float32_t</span> <span class="n">t</span> <span class="o">=</span> <span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">];</span> 
</span></span><span class="line"><span class="cl">            <span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">H</span><span class="p">[</span><span class="n">m</span><span class="p">][</span><span class="n">j</span><span class="p">];</span> 
</span></span><span class="line"><span class="cl">            <span class="n">H</span><span class="p">[</span><span class="n">m</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> 
</span></span><span class="line"><span class="cl">        <span class="kt">float32_t</span> <span class="n">tb</span> <span class="o">=</span> <span class="n">b</span><span class="p">[</span><span class="n">k</span><span class="p">];</span> 
</span></span><span class="line"><span class="cl">        <span class="n">b</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">b</span><span class="p">[</span><span class="n">m</span><span class="p">];</span> 
</span></span><span class="line"><span class="cl">        <span class="n">b</span><span class="p">[</span><span class="n">m</span><span class="p">]</span> <span class="o">=</span> <span class="n">tb</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// 标准化主行
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="kt">float32_t</span> <span class="n">diag</span> <span class="o">=</span> <span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">k</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">            <span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">/=</span> <span class="n">diag</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">test</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="n">k</span> <span class="o">==</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">test</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl">        <span class="n">b</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">/=</span> <span class="n">diag</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl">        <span class="c1">// 消去下方得到上三角式
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">k</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kt">float_t</span> <span class="n">factor</span> <span class="o">=</span> <span class="n">H</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">                <span class="n">H</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">-=</span> <span class="n">factor</span> <span class="o">*</span> <span class="n">H</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">			
</span></span><span class="line"><span class="cl">            <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-=</span> <span class="n">factor</span> <span class="o">*</span> <span class="n">b</span><span class="p">[</span><span class="n">k</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">    <span class="c1">// 回代求解
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">float32_t</span> <span class="n">theta</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="o">--</span><span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">float32_t</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">            <span class="n">sum</span> <span class="o">-=</span> <span class="n">H</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">*</span> <span class="n">theta</span><span class="p">[</span><span class="n">j</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl">        <span class="n">theta</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">sum</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">I1</span> <span class="o">=</span> <span class="n">theta</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">Q1</span> <span class="o">=</span> <span class="n">theta</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">I2</span> <span class="o">=</span> <span class="n">theta</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">Q2</span> <span class="o">=</span> <span class="n">theta</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>   虽然使用跨帧相位差法可以得到精度极高的频率，但是最小二乘法仍有一定的误差，经笔者测试平均误差约为 $1.7\%$ 。其原因可能是单片机 ADC 采样存在一定的误差，且信号本身存在噪声，导致采得的电压值存在误差，这种误差对于测频算法影响较小，但最小二乘法直接用到了每个采样点的信息，因此容易受采样误差的影响。</p>
<hr>
<h2 id="结语">结语</h2>
<p>   以上算法仅测量分离了两个信号的频率、幅度和相位，而要实现“数字滤波”的效果，还需要将其中一个信号输出，这里涉及到与原信号同步稳定显示的问题。</p>
<p>   不论使用单片机的 DAC 模块还是外部 DDS 模块，由于内部模块与外部信号源采用了各自独立的时钟基准，这两个时钟源之间不可避免地存在频率偏差和相位漂移。这种时钟异步性会导致在长时间观测下，单片机采样时刻与信号源实际相位之间的相对关系发生持续变化，即产生相位差的累积效应。累积效应作用的结果是，当通过滤波器对处理后的信号进行观测时（例如在示波器上显示），会发现滤波输出信号与原始输入信号之间存在一个逐渐增大的相位差，表现为两个波形在时间轴上相对滑动或漂移。</p>
<p>   要解决该问题，最粗暴的方法其实是统一信号源与信号发生模块的时钟源，但大多数情况无法实现；其次可以通过引入闭环控制，实时追踪信号源的相位并跟随，做到相对静止。笔者尝试使用了 DAC 模拟 DDS 输出信号，由于硬件和性能原因，引入闭环控制后未能达到理想的效果，可能需要使用专门的 DDS模块来实现。</p>
<hr>
<blockquote>
<p>🍃本文中所涉及的算法已经使用STM32F407VET6单片机实现，工程文件已上传至<a href="https://github.com/coverMoon/Signal_seperate.git" target="_blank" rel="noopener noreffer ">GitHub</a>，欢迎访问。</p>
</blockquote>
]]></description></item></channel></rss>