"""Build standalone HTML and a self-contained executable notebook.""" from pathlib import Path import json import nbformat from algorithms import newton_opt_states, newton_states, bfgs_states, boost_states ROOT = Path(__file__).parent notes = [ ('newton', '牛顿法', '用切线寻找零点', 'xₖ₊₁ = xₖ − f(xₖ) / f′(xₖ),f(x)=x²−2', '''观察动画时,先找到曲线上的当前点,再沿该点的切线走到横轴,交点的横坐标就是下一次迭代值。这里从三开始求解平方根二,前几次移动明显,靠近根以后,两个点几乎重合,但右侧对数误差图仍能展示精度的快速提升。每一帧同时显示当前位置与下一位置,因此不要把切线与横轴的交点误认为已经更新后的曲线点。牛顿法的本质是用当前点附近的一阶线性模型替代原函数,再求这个简单模型的零点。对于足够光滑的函数,如果目标是单根、导数不为零而且初值足够接近根,通常可以获得局部二次收敛。不过,这并不表示任意起点都能成功:导数很小时可能产生极大的步长,复杂函数还可能发生循环或者收敛到另一个根。这个例子选择了稳定的正初值,所以主要展示快速收敛,而不是所有失败情形。图中的误差是函数残差绝对值,并非横坐标与真实根的距离;两者在简单根附近相关,但概念不能混用。'''), ('taylor', '泰勒展开', '围绕非零中心建立局部模型', 'Pₙ(x) = Σₖ₌₀ⁿ f⁽ᵏ⁾(a)(x−a)ᵏ/k!,a=1', '''这组动画把正弦函数放在展开中心一附近观察,橙色曲线随着最高次数增加而变化。零次多项式只保留中心处的函数值,一次多项式再匹配斜率,二次及更高次数则逐渐补充弯曲程度等局部信息。最值得留意的是,中心附近往往先贴近真实函数,而较远的位置需要更多项才能改善。左图固定纵轴在负二到二之间,因此某些低阶曲线在边缘会被截断,不能把曲线离开画面解释为函数没有定义;右侧误差图用于补充这种信息。对于正弦函数,泰勒级数在任意有限实数处都收敛,但有限阶近似的误差仍然与距离和阶数有关。一般函数不能直接照搬这个结论:有的级数只有有限收敛半径,还有光滑函数不等于自己的泰勒级数。这里使用最高次数作为帧编号,包含从零次到该次数的所有项,避免把次数与非零项数混淆。理解泰勒展开时,应同时关注展开中心、截断阶数以及考察区间,而不能只凭某一点的吻合程度判断整个区间上的近似质量。'''), ('maclaurin', '麦克劳林展开', '泰勒展开在原点的特例', 'sin x = x − x³/3! + x⁵/5! − …,a=0', '''麦克劳林展开就是把泰勒展开的中心固定为零,因此它不是与泰勒展开相互独立的另一套原理。对于正弦函数,原点处所有偶数阶导数都为零,所以动画在偶数次数与前一个奇数次数之间可能完全不动。这种停顿是数学结构的真实反映,而不是播放故障。例如三次多项式和四次多项式相同,都等于一次项减去三次项除以六。随着奇数次项加入,近似逐渐从原点附近扩展到更远的位置;然而某个有限位置上的绝对误差不一定每增加一次就严格下降,因为截断项之间存在抵消。用户给出的参考程序用非零项数控制循环,本演示则统一用最高次数控制帧,所以十九次对应十个非零项,并不等于十九个非零项。计算时输入被转换为浮点数组,并递推生成幂与阶乘的比值,减少整数幂溢出以及直接计算大阶乘的问题。右图的误差设置了浮点显示下限,接近这个下限时应理解为数值舍入已经影响结果,而不是计算机证明了无限精度的相等。'''), ('bfgs', '拟牛顿法 · BFGS', '从梯度变化学习曲率', 'pₖ = −Hₖ∇f(xₖ),sₖ = xₖ₊₁−xₖ,yₖ = ∇f(xₖ₊₁)−∇f(xₖ)', '''这里选择狭长弯曲谷底的 Rosenbrock 函数,目标最小点位于一与一。背景等高线展示地形,橙色折线记录 BFGS 的真实迭代路径。开始时方向可能跨向陡峭谷壁,因此程序使用 Armijo 回溯线搜索缩短步长;进入谷底以后,路径逐渐转弯并逼近最优点。拟牛顿法的关键并不是每轮计算真实海森矩阵,而是利用相邻两步的位置变化与梯度变化来更新曲率近似。本例的 H 表示逆海森矩阵的近似,初值取单位阵,所以最初方向与负梯度一致,后续方向则逐渐包含过去观测到的曲率信息。与前面的求根牛顿法相比,这里解决的是多元函数最小化问题,二者相关却不能直接混为同一个任务。动画中目标值下降并不保证每个坐标都单调靠近一,弯曲路径恰好说明耦合变量需要共同调整。只有曲率内积满足正值阈值时才进行矩阵更新,以减少数值不稳定;线搜索也并不意味着所有非凸问题都能找到全局最优解。右图展示目标函数值,便于把几何移动与优化进展对应起来。'''), ('xgboost', 'XGBoost 核心机制', '二阶目标与逐轮加树', 'w* = −G/(H+λ),Gain = ½[Gᴸ²/(Hᴸ+λ)+Gᴿ²/(Hᴿ+λ)−G²/(H+λ)]−γ', '''左图灰点是固定训练样本,绿色阶梯线是当前所有树的累加预测。每推进一轮,右图先展示这一轮加树之前的残差,再用橙色阶梯线展示新树的叶子输出;真正加到总预测上的数值还要乘以零点三的学习率。这里使用平方损失的一半,因此梯度等于预测减目标,海森量恒为一。每个叶子的输出由梯度和海森量的聚合值计算,并通过正则参数一收缩,分裂则根据二阶目标的增益选择。这比只说新树拟合错误更能解释 XGBoost 的核心数学结构。本演示采用相邻样本中点作为精确候选阈值,并按深度优先生长到最大深度二,最多得到四个叶子;页面还给出实际分裂次序和各次增益。需要注意,现代 XGBoost 也支持直方图算法以及其他生长策略,精确分裂与按层生长不是它独有或必须的属性。右图残差属于更新前,页面均方误差属于更新后,所以两者观察时要对准时间。训练误差下降只能说明对当前样本拟合更好,不能证明泛化改善;这个小实验没有验证集,也不用于比较生产模型性能。'''), ('lightgbm', 'LightGBM 核心机制', '直方图候选与最佳叶子优先', '候选阈值来自分箱边界;每次分裂当前增益最大的叶子', '''这一页与前一页使用相同的训练点、初始均值、学习率和正则参数,因此可以把注意力放在树如何生长。连续特征先被划分为十二个等宽箱,分裂只允许发生在内部箱边界上,减少候选阈值数量。程序再从所有可分裂叶子中选取增益最大的一个继续分裂,达到四个叶子或没有正增益时停止。右图的竖线标明这一轮选中的阈值,下面按顺序列出增益,可以观察计算资源如何优先用于当前最能降低目标的位置。这种叶子优先生长允许不同分支具有不同深度,因此与最大深度二的树即使同样最多四个叶子,也可能形成不同的分段结构。不过当前小数据不保证每轮都出现不对称树,更不保证任一方法总是误差更低。这里为便于理解,直接在分箱边界汇总样本梯度与海森量,结果体现直方图分裂的数学含义,但没有实现实际库里的高效直方图缓存和差分构建。LightGBM 的分箱策略也不必是等宽箱;其他优化如特征捆绑与梯度采样没有在本例启用,不能从动画推断它们的作用。叶子数限制有助于控制复杂度,但真实任务仍需要验证集、早停及最小叶样本数等约束。''')] notes[0] = (notes[0][0], '牛顿法 · 求根', *notes[0][2:]) notes.append(('newton_opt', '牛顿法 · 优化', '对导数求根,用二次模型寻找极小值', 'xₖ₊₁ = xₖ − f′(xₖ)/f″(xₖ);f(x)=x⁴/4+x²/2,f′(x)=x³+x,f″(x)=3x²+1', '''牛顿优化法可以从求根法直接推导:令新的求根函数为原目标的一阶导数,对这个导数函数应用牛顿公式,分母就成为原目标的二阶导数。因此不是简单把二阶导数随意代入原公式,而是同时把分子换成一阶导数,把分母换成二阶导数。右图专门展示这一步:绿色曲线是一阶导数,橙色虚线是它在当前点处的切线,切线与横轴交点给出下一个位置。左图从优化角度展示同一更新:用目标函数在当前点处的二阶泰勒多项式建立局部抛物线,该抛物线的最低点横坐标恰好也是下一位置。注意抛物线上的最低点不一定落在真实函数上,左图的圆点显示真实目标值,叉号才表示二次模型的最低点。随着位置靠近零,导数和目标值一起下降,模型与目标在局部更加接近。本例二阶导数始终大于零,因此目标严格凸,唯一驻点零就是全局最小点。但一般非凸函数的驻点也可能是极大点或其他类型的平稳点;二阶导数为零时公式不可直接使用,为负时二次模型甚至没有极小值。实际优化常结合阻尼、线搜索或曲率修正来改善稳定性。多元形式则需要用海森矩阵求解牛顿方向,拟牛顿法正是通过近似曲率来降低这部分计算成本。''')) data = dict(newton_opt=newton_opt_states(), newton=newton_states(), bfgs=bfgs_states(), xgboost=boost_states(), lightgbm=boost_states(True)) template = (ROOT/'template.html').read_text(encoding='utf-8') output = template.replace('__DATA__', json.dumps(data, ensure_ascii=False)).replace('__NOTES__', json.dumps(notes, ensure_ascii=False)) (ROOT/'index.html').write_text(output, encoding='utf-8') nb = nbformat.v4.new_notebook() nb.cells = [nbformat.v4.new_markdown_cell('# 六种方法的动态实验(牛顿法含求根与优化)\n按顺序运行。依赖:numpy、matplotlib、IPython。所有算法代码都包含在本 notebook 内,无需本地导入。\n\n若缺少依赖,取消下一格安装语句的注释。每个动画使用 FuncAnimation 与 HTML(ani.to_jshtml()) 渲染。'), nbformat.v4.new_code_cell('# %pip install numpy matplotlib ipython'), nbformat.v4.new_code_cell((ROOT/'algorithms.py').read_text(encoding='utf-8'))] for key, title, subtitle, formula, note in notes: nb.cells += [nbformat.v4.new_markdown_cell(f'## {title}\n{subtitle}\n\n`{formula}`\n\n### 观察与理解\n{note}'), nbformat.v4.new_code_cell(f"ani_{key} = make_animation('{key}')\nHTML(ani_{key}.to_jshtml())")] nb.metadata = dict(kernelspec=dict(display_name='Python 3', language='python', name='python3')) nbformat.write(nb, ROOT/'六种算法动态演示.ipynb') (ROOT/'使用说明.md').write_text('''# 六种算法动态实验室 - 双击 `index.html`,选择方法后点击播放;支持逐步、重置、进度拖动和速度调节,无需网络。 - 动画需要 JavaScript,样式与布局使用 HTML + CSS,三者已封装在同一个文件里。 - 打开 `六种算法动态演示.ipynb`,从上到下运行;它包含全部 Python 实现和七个动画单元格(含牛顿求根与牛顿优化)。 - `algorithms.py` 是对应的可复用 Python 源码;在同目录的 Jupyter 中可用 `from algorithms import make_animation`。 - Python 依赖:numpy、matplotlib、IPython。仅重新构建交付文件时才需要 nbformat 和 build.py。 - 泰勒展开中心为 1,麦克劳林中心为 0,次数范围均为 0–19。与参考代码的“非零项数”不同。 - XGBoost / LightGBM 页面是透明的核心机制教学实现,不调用官方训练库,不代表完整产品或速度基准。 - 梯度提升采用同一份确定性数据,平方损失、学习率 0.3、lambda=1、gamma=0、叶子最小样本数 3。 - 图中纵轴固定,泰勒多项式可能超出左图显示范围;误差图使用对数刻度并设置显示下限。 ''', encoding='utf-8') print('Built HTML and notebook; Chinese observation lengths:', [sum('\u4e00'<=c<='\u9fff' for c in n[4]) for n in notes])