#!/usr/bin/env python3
"""编译单日课程(周六 10:00 批处理)"""
import argparse
import os
import sys
from pathlib import Path
from jinja2 import Template
import yaml
# 添加项目根目录到路径
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
def load_config() -> dict:
"""加载配置"""
config_path = PROJECT_ROOT / "skills" / "mathlab" / "config.yaml"
with open(config_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def generate_symbol_decoder(topic: str) -> str:
"""生成符号解码字典(根据主题动态生成)"""
decoders = {
"感知机": r"""
$w$ → self.weights (权重向量,shape: [d])
$b$ → self.bias (偏置标量,shape: [])
$x$ → input_tensor (输入样本,shape: [d])
$y$ → label (标签,值域:{-1, +1})
$\sign(\cdot)$ → np.sign() (符号函数)
$\xi$ → margin (间隔,用于更新步长)
""",
"K 近邻": r"""
$x$ → input_tensor (待预测样本,shape: [d])
$X$ → self.X_train (训练集,shape: [N, d])
$y$ → label (待预测标签)
$Y$ → self.y_train (训练标签,shape: [N])
$k$ → self.k (近邻数量)
$p$ → p (Lp 范数,p=2 为欧氏距离)
""",
"朴素贝叶斯": r"""
$P(c_k)$ → self.class_priors_[k] (类别先验概率)
$P(x^{(j)} | c_k)$ → self.feature_probs_[k, j] (条件概率)
$P(c_k | x)$ → posterior (后验概率)
$\alpha$ → alpha (拉普拉斯平滑系数)
""",
"EM 算法": r"""
$\theta$ → self.params (模型参数)
$z$ → latent_vars (隐变量)
$Q(\theta | \theta^{(t)})$ → Q_function (期望下界)
$\mathcal{L}(\theta)$ → log_likelihood (对数似然)
""",
"隐马尔可夫模型": r"""
$\pi$ → self.initial_probs (初始状态概率)
$A$ → self.transition_probs (状态转移矩阵)
$B$ → self.emission_probs (观测发射矩阵)
$O$ → observations (观测序列)
$Q$ → states (隐藏状态序列)
""",
"计算图": r"""
$x$ → input_tensor (输入张量)
$y$ → output_tensor (输出张量)
$\frac{\partial y}{\partial x}$ → gradient (梯度)
$\mathcal{G}$ → computation_graph (计算图结构)
""",
"卷积神经网络": r"""
$X$ → input_tensor (输入特征图,shape: [C, H, W])
$W$ → self.weight (卷积核,shape: [C_out, C_in, k, k])
$b$ → self.bias (偏置,shape: [C_out])
$Y$ → output_tensor (输出特征图)
$s$ → stride (步长)
$p$ → padding (填充)
"""
}
return decoders.get(topic, decoders["感知机"])
def generate_math_derivation(topic: str) -> str:
"""生成数学推导(根据主题动态生成)"""
derivations = {
"感知机": r"""
### 感知机预测函数
$$f(x) = w \cdot x + b$$
其中 $\cdot$ 表示向量点积。
### 损失函数(hinge loss 的简化形式)
$$L(w, b) = -\sum_{x_i \in M} y_i (w \cdot x_i + b)$$
$M$ 是误分类点集合。
### 梯度下降更新规则
$$w \leftarrow w + \eta \cdot y_i \cdot x_i$$
$$b \leftarrow b + \eta \cdot y_i$$
其中 $\eta$ 是学习率。
""",
"K 近邻": r"""
### 距离计算(Lp 范数)
$$d(x, x^{(i)}) = \left( \sum_{j=1}^{d} |x^{(j)} - x^{(i)(j)}|^p \right)^{1/p}$$
当 $p=2$ 时为欧氏距离:
$$d(x, x^{(i)}) = \sqrt{\sum_{j=1}^{d} (x^{(j)} - x^{(i)(j)})^2}$$
### 投票规则
$$\hat{y} = \text{argmode}_{c} \sum_{i=1}^{k} \mathbb{I}(y^{(i)} = c)$$
其中 $\mathbb{I}(\cdot)$ 是指示函数,统计 k 个近邻中各类别出现次数。
""",
"朴素贝叶斯": r"""
### 贝叶斯公式
$$P(c_k | x) = \frac{P(x | c_k) P(c_k)}{P(x)}$$
### 朴素条件独立假设
$$P(x | c_k) = P(x^{(1)}, x^{(2)}, ..., x^{(d)} | c_k) = \prod_{j=1}^{d} P(x^{(j)} | c_k)$$
### 后验概率最大化
$$\hat{y} = \text{argmax}_{c_k} P(c_k) \prod_{j=1}^{d} P(x^{(j)} | c_k)$$
### 拉普拉斯平滑
$$P(x^{(j)} = v | c_k) = \frac{\sum_{i=1}^{N} \mathbb{I}(x^{(i)(j)} = v, y^{(i)} = c_k) + \alpha}{\sum_{i=1}^{N} \mathbb{I}(y^{(i)} = c_k) + \alpha \cdot V}$$
其中 $V$ 是特征取值数量,$\alpha$ 是平滑系数。
""",
"EM 算法": r"""
### 对数似然函数
$$\mathcal{L}(\theta) = \log P(X | \theta) = \log \sum_{Z} P(X, Z | \theta)$$
### E 步:构造下界
$$\mathcal{L}(\theta) \geq Q(\theta | \theta^{(t)}) = \sum_{Z} P(Z | X, \theta^{(t)}) \log \frac{P(X, Z | \theta)}{P(Z | X, \theta^{(t)})}$$
### M 步:最大化下界
$$\theta^{(t+1)} = \text{argmax}_{\theta} Q(\theta | \theta^{(t)})$$
### 迭代收敛
$$\mathcal{L}(\theta^{(t+1)}) \geq \mathcal{L}(\theta^{(t)})$$
""",
"隐马尔可夫模型": r"""
### 三大基本问题
**1. 概率计算问题**(前向算法)
$$\alpha_t(i) = P(o_1, o_2, ..., o_t, q_t = S_i | \lambda)$$
递推公式:
$$\alpha_{t+1}(i) = \left[ \sum_{j=1}^{N} \alpha_t(j) a_{ji} \right] b_i(o_{t+1})$$
**2. 学习问题**(Baum-Welch 算法)
$$\gamma_t(i) = P(q_t = S_i | O, \lambda)$$
$$\xi_t(i, j) = P(q_t = S_i, q_{t+1} = S_j | O, \lambda)$$
**3. 预测问题**(Viterbi 算法)
$$\delta_t(i) = \max_{q_1, ..., q_{t-1}} P(q_1, ..., q_t=i, o_1, ..., o_t | \lambda)$$
""",
"计算图": r"""
### 计算图结构
$$y = f(x) = f_L(f_{L-1}(\cdots f_1(x)\cdots))$$
### 链式法则
$$\frac{\partial y}{\partial x} = \frac{\partial f_L}{\partial u_{L-1}} \cdot \frac{\partial f_{L-1}}{\partial u_{L-2}} \cdots \frac{\partial f_1}{\partial x}$$
### 反向传播
前向:计算每一层的输出 $u_l = f_l(u_{l-1})$
反向:从后往前计算梯度
$$\frac{\partial L}{\partial u_l} = \frac{\partial L}{\partial u_{l+1}} \cdot \frac{\partial u_{l+1}}{\partial u_l}$$
""",
"卷积神经网络": r"""
### 卷积运算
$$(X * W)_{i,j} = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} W_{m,n} \cdot X_{i+m, j+n}$$
### 输出尺寸计算
$$H_{out} = \left\lfloor \frac{H_{in} + 2p - k}{s} \right\rfloor + 1$$
$$W_{out} = \left\lfloor \frac{W_{in} + 2p - k}{s} \right\rfloor + 1$$
其中 $k$ 是卷积核尺寸,$p$ 是填充,$s$ 是步长。
### 梯度反向传播
$$\frac{\partial L}{\partial W} = X_{rotated} * \frac{\partial L}{\partial Y}$$
$$\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} * W_{rotated}$$
其中 $rotated$ 表示 180 度旋转。
"""
}
return derivations.get(topic, derivations["感知机"])
def generate_html(day: int, topic: str, config: dict) -> str:
"""生成课程 HTML(根据主题动态生成参数)"""
template_path = PROJECT_ROOT / "skills" / "mathlab" / "templates" / "course_template.html"
with open(template_path, "r", encoding="utf-8") as f:
template_content = f.read()
template = Template(template_content)
# 不同主题的技术参数
topic_params = {
"感知机": {
"technical_debt": "之前的算法无法处理线性不可分数据,导致泛化能力差。感知机通过引入决策边界,将分类问题转化为优化问题。",
"visual_intuition": "想象一条直线(或超平面)将两类点分开。权重向量 $w$ 决定直线的方向,偏置 $b$ 决定直线的位置。",
"bilibili_keyword": "感知机 直观解释",
"optimization_bottleneck": r"全量梯度下降需要遍历所有样本,复杂度 $O(N \cdot d)$。现代框架使用 mini-batch SGD 加速。",
"oj_mission": "实现感知机的 forward 和 update 函数,在 XOR 数据集上验证无法收敛(线性不可分)。"
},
"K 近邻": {
"technical_debt": "基于模型的算法需要训练过程,无法利用新样本。K 近邻是实例学习(惰性学习),直接存储训练数据用于预测。",
"visual_intuition": "想象在一个平面上,新点周围的 k 个最近邻居多数是红色,那它也被预测为红色。距离越近影响越大。",
"bilibili_keyword": "K 近邻算法 直观解释",
"optimization_bottleneck": r"预测时需要计算到所有训练样本的距离,复杂度 $O(N \cdot d)$。使用 KD 树或 Ball 树加速到 $O(\log N)$。",
"oj_mission": "实现 K 近邻的 distance 和 predict 函数,在手写数字数据集上验证 k 值对准确率的影响。"
},
"朴素贝叶斯": {
"technical_debt": "传统分类器需要估计联合概率分布,参数多且易过拟合。朴素贝叶斯通过条件独立假设简化模型。",
"visual_intuition": "想象判断一封邮件是否是垃圾邮件:根据关键词出现的概率,结合先验经验,计算后验概率。",
"bilibili_keyword": "朴素贝叶斯 直观解释",
"optimization_bottleneck": r"需要统计所有特征 - 类别组合的频率,高维数据下计算量大。使用哈希表或稀疏矩阵优化。",
"oj_mission": "实现高斯朴素贝叶斯的 fit 和 predict 函数,在鸢尾花数据集上验证分类效果。"
},
"EM 算法": {
"technical_debt": "监督学习需要完整标注数据,但现实中很多数据缺失或隐变量未知。EM 算法通过迭代估计隐变量和参数。",
"visual_intuition": "想象混合高斯模型:E 步猜测每个点属于哪个高斯,M 步根据猜测更新高斯参数,循环迭代直到收敛。",
"bilibili_keyword": "EM 算法 直观解释",
"optimization_bottleneck": r"每次迭代需要遍历所有样本计算期望,复杂度 $O(N \cdot K \cdot I)$,K 为隐变量数,I 为迭代次数。",
"oj_mission": "实现 EM 算法拟合混合高斯分布,在双峰数据上验证参数收敛过程。"
},
"隐马尔可夫模型": {
"technical_debt": "传统序列模型无法处理隐藏状态。HMM 引入不可观测的状态序列,通过观测序列推断隐藏状态。",
"visual_intuition": "想象通过天气(晴/雨)推断草地干湿状态:天气是隐藏状态,草地干湿是观测,转移概率决定状态变化。",
"bilibili_keyword": "隐马尔可夫模型 直观解释",
"optimization_bottleneck": r"前向 - 后向算法需要 $O(N^2 \cdot T)$ 计算,Viterbi 解码需要 $O(N^2 \cdot T)$ 动态规划。",
"oj_mission": "实现 HMM 的前向算法计算观测概率,在中文分词数据集上验证 Viterbi 解码效果。"
},
"计算图": {
"technical_debt": "手动推导梯度复杂易错,深度学习模型嵌套多层。计算图自动记录前向过程,反向自动求导。",
"visual_intuition": "想象一个流程图:数据从左到右经过各种运算节点,每个节点记录输入输出,反向时从右到左传递梯度。",
"bilibili_keyword": "计算图 自动求导 原理",
"optimization_bottleneck": r"计算图占用内存存储中间结果,大模型下显存爆炸。使用梯度检查点(checkpoint)节省内存。",
"oj_mission": "实现简易计算图的 forward 和 backward,验证链式法则的正确性。"
},
"卷积神经网络": {
"technical_debt": "全连接网络处理图像时参数爆炸,无法捕捉空间局部性。CNN 通过卷积核共享参数,提取局部特征。",
"visual_intuition": "想象一个滤波器在图像上滑动,每个位置计算局部区域的加权和,提取边缘、纹理等特征。",
"bilibili_keyword": "卷积神经网络 CNN 直观解释",
"optimization_bottleneck": r"卷积运算复杂度 $O(C_{in} \cdot C_{out} \cdot k^2 \cdot H \cdot W)$。使用深度可分离卷积(Depthwise Separable)加速。",
"oj_mission": "实现 2D 卷积层的 forward 和 backward,在 MNIST 数据集上验证卷积特征提取效果。"
}
}
params = topic_params.get(topic, topic_params["感知机"])
html_content = template.render(
day=day,
topic=topic,
technical_debt=params["technical_debt"],
visual_intuition=params["visual_intuition"],
bilibili_keyword=params["bilibili_keyword"],
symbol_decoder=generate_symbol_decoder(topic),
math_derivation=generate_math_derivation(topic),
optimization_bottleneck=params["optimization_bottleneck"],
oj_mission=params["oj_mission"],
katex_css="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css",
katex_js="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"
)
return html_content
def generate_exercise(day: int, topic: str) -> str:
"""生成练习题"""
# 将中文主题转换为英文类名
topic_en_map = {
"感知机": "Perceptron",
"K 近邻": "KNearestNeighbor",
"朴素贝叶斯": "NaiveBayes",
"EM 算法": "ExpectationMaximization",
"隐马尔可夫模型": "HiddenMarkovModel",
"计算图": "ComputationalGraph",
"卷积神经网络": "ConvolutionalNeuralNetwork",
"注意力机制": "AttentionMechanism",
"Bellman 方程": "BellmanEquation",
"价值迭代": "ValueIteration",
"策略迭代": "PolicyIteration"
}
class_name = topic_en_map.get(topic, "Model")
return f'''"""
Day {day} - {topic} 练习
任务:实现 {topic} 的核心算法
"""
import numpy as np
import matplotlib.pyplot as plt
class {class_name}:
"""{topic} 类"""
def __init__(self, learning_rate: float = 0.01):
self.learning_rate = learning_rate
self.weights = None
self.bias = None
def forward(self, X: np.ndarray) -> np.ndarray:
"""前向传播
Args:
X: 输入数据,shape: [n_samples, n_features]
Returns:
预测结果,shape: [n_samples]
"""
# TODO: 实现 f(x) = sign(w · x + b)
raise NotImplementedError
def compute_loss(self, X: np.ndarray, y: np.ndarray) -> float:
"""计算损失"""
# TODO: 实现损失函数
raise NotImplementedError
def update(self, X_i: np.ndarray, y_i: int):
"""更新参数
Args:
X_i: 单个样本,shape: [n_features]
y_i: 标签,值域 {-1, +1}
"""
# TODO: 实现梯度下降更新
raise NotImplementedError
def fit(self, X: np.ndarray, y: np.ndarray, max_iter: int = 100):
"""训练模型"""
# TODO: 实现训练循环
raise NotImplementedError
def plot_concept():
"""可视化概念"""
# 生成二维数据
np.random.seed(42)
X = np.random.randn(100, 2)
y = np.sign(X[:, 0] + X[:, 1] - 0.5) * 1
# 绘制散点图
plt.figure(figsize=(8, 6))
scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap="bwr", s=100, edgecolors="black")
plt.xlabel("x1")
plt.ylabel("x2")
plt.title("Day {day} - {topic} 可视化")
plt.colorbar(scatter)
plt.grid(True, alpha=0.3)
plt.savefig("./plots/day{day}_concept.png", dpi=150)
print(f"✅ 可视化已保存:plots/day{day}_concept.png")
if __name__ == "__main__":
plot_concept()
'''
def generate_test(day: int, topic: str) -> str:
"""生成测试用例"""
return f'''"""
Day {day} - {topic} 测试用例
"""
import numpy as np
import sys
sys.path.append("../exercises")
# TODO: 导入对应的类
# from day{day}_task import *
def test_forward_shape():
"""测试前向传播输出形状"""
# TODO: 实现测试
assert True
def test_loss_computation():
"""测试损失计算"""
# TODO: 实现测试
assert True
def test_update_rule():
"""测试参数更新规则"""
# TODO: 实现测试
assert True
def test_convergence():
"""测试收敛性"""
# TODO: 实现测试
assert True
'''
def main():
parser = argparse.ArgumentParser(description="编译单日课程")
parser.add_argument("--day", type=int, required=True, help="天数 (1-7)")
parser.add_argument("--topic", type=str, required=True, help="主题名称")
args = parser.parse_args()
config = load_config()
# 创建 staging 目录
staging_dir = PROJECT_ROOT / config["output"]["staging"]
staging_dir.mkdir(exist_ok=True)
exercises_dir = staging_dir / "exercises"
tests_dir = staging_dir / "tests"
exercises_dir.mkdir(exist_ok=True)
tests_dir.mkdir(exist_ok=True)
# 生成 HTML
html_content = generate_html(args.day, args.topic, config)
html_path = staging_dir / f"course_day{args.day}.html"
with open(html_path, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"✅ 生成课程 HTML: {html_path}")
# 生成练习题
exercise_content = generate_exercise(args.day, args.topic)
exercise_path = exercises_dir / f"day{args.day}_task.py"
with open(exercise_path, "w", encoding="utf-8") as f:
f.write(exercise_content)
print(f"✅ 生成练习题:{exercise_path}")
# 生成测试
test_content = generate_test(args.day, args.topic)
test_path = tests_dir / f"test_day{args.day}.py"
with open(test_path, "w", encoding="utf-8") as f:
f.write(test_content)
print(f"✅ 生成测试用例:{test_path}")
print(f"\n🎉 Day {args.day} 编译完成!文件已保存到 staging/")
if __name__ == "__main__":
main()