ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HCIA-AI题库源码化:用Python+Jupyter实现可运行验证与自动化测试

HCIA-AI题库源码化:用Python+Jupyter实现可运行验证与自动化测试

简介:这份资源是华为HCIA-AI认证V4.0(3+1升级考试)的题库整理包,面向准备考取华为人工智能基础认证的考生与AI入门学习者,作者以950多分通过考试后,将刷题经验与题目答案系统汇总,帮助读者高效复习、查漏补缺。压缩包共3个文件,以html题库页面为主,辅以inscode配置与gitignore文件,整体约13KB,轻量易打开,可直接在浏览器中浏览题目与答案。题库覆盖昇腾AI处理器、Transformer优势、大模型部署、LLM训练挑战、智能驾驶、卷积神经网络、反向传播、知识蒸馏、MindSpore框架与梯度下降等核心考点,每道题均附正确答案,既贴合考试大纲,也呼应行业前沿应用。目前已有127人学习下载,适合希望系统梳理AI基础知识、提升认证通过率的备考人员参考使用。

1. 从 HCIA-AI 备考说起:为什么我把题库做成了可运行源码

很多人准备 HCIA-AI 认证时,第一反应是找一份 PDF 题库,把答案背下来。我最初也是这么干的,结果第一次模拟测试就翻车了——题目问的是某个激活函数在特定输入区间的输出特性,我背过答案,但题目换了个参数问法,我完全不知道从哪推。那次之后我意识到,HCIA-AI 的题库如果只是静态文本,你记住的是答案,不是知识。真正有效的备考方式,是把每道题背后的计算过程、模型结构、评估指标都跑一遍,让代码告诉你为什么选 A 不选 B。

这就是我把题库做成可运行源码的出发点。所谓「可运行源码」,不是把题目和答案塞进一个 Python 文件里打印出来,而是针对题库中涉及计算、推理、数据处理、模型评估的题目,写一段能独立执行、能改参数、能看中间结果的代码。你运行一次,比背十遍答案管用。这篇文章面向的是正在备考 HCIA-AI、或者已经考过但想系统梳理 AI 基础知识体系的从业者。我会把题库里高频出现的几类题型拆开,给出可复现的代码框架、参数设置逻辑,以及我在整理过程中踩过的坑。新手可以跟着代码一步步跑,熟手可以直接看参数边界和避坑部分。

2. 题库源码化:从静态题目到可执行验证的完整路径

2.1 为什么选择 Python + Jupyter 作为题库载体

HCIA-AI 的考纲覆盖机器学习基础、深度学习基础、计算机视觉、自然语言处理、华为 AI 平台应用等模块。题库里大量题目涉及数值计算、张量操作、模型评估指标计算,这些用 Python 表达最自然。我选择 Jupyter Notebook 作为主要载体,原因是它能把题目描述、代码、运行结果、可视化图表放在同一个单元格序列里,复习时从上往下执行一遍,相当于把整个知识链路重新走了一次。

具体来说,每个题目对应一个 Notebook 单元格块,结构是:题目原文(Markdown 单元格)→ 解题代码(Code 单元格)→ 运行输出(自动保存)→ 关键参数注释(Markdown 单元格)。这样你打开一个 Notebook,看到的不只是答案,而是完整的推导过程。对于纯概念题,比如「以下哪个不是监督学习算法」,我会在代码里用 sklearn 快速构造一个分类任务,把选项里的算法都跑一遍,用实际输出说明为什么某个选项不属于监督学习。

环境依赖方面,我固定使用 Python 3.9 + NumPy 1.24 + scikit-learn 1.3 + PyTorch 2.0(CPU 版本即可)。不依赖 GPU,因为题库里的计算规模都很小,CPU 足够。这样做的好处是,你在任何一台普通笔记本上都能复现,不需要额外配置 CUDA 环境。

2.2 题库目录结构与题目分类映射

我把题库按考纲模块分成五个一级目录,每个目录下再按题型细分。目录结构如下:

hcia-ai-question-bank/ ├── 01_ml_basics/ # 机器学习基础 │ ├── linear_regression.ipynb │ ├── logistic_regression.ipynb │ ├── decision_tree.ipynb │ └── evaluation_metrics.ipynb ├── 02_dl_basics/ # 深度学习基础 │ ├── activation_functions.ipynb │ ├── backpropagation.ipynb │ ├── optimizer_comparison.ipynb │ └── regularization.ipynb ├── 03_cv/ # 计算机视觉 │ ├── convolution_demo.ipynb │ ├── pooling_demo.ipynb │ └── image_augmentation.ipynb ├── 04_nlp/ # 自然语言处理 │ ├── tokenization.ipynb │ ├── word_embedding.ipynb │ └── attention_demo.ipynb └── 05_platform/ # AI 平台应用(模拟) ├── dataset_management.ipynb └── model_deployment_sim.ipynb

每个 Notebook 文件对应题库中一个知识点的所有相关题目。比如activation_functions.ipynb里会集中处理 Sigmoid、Tanh、ReLU、Leaky ReLU 等激活函数的题目,包括它们的导数计算、输出范围、优缺点对比。这样分类的好处是,你复习一个知识点时,能一次性看到所有相关题目的变体,而不是在不同页面之间跳来跳去。

2.3 把一道选择题变成可运行验证:以激活函数为例

题库里有一道典型题目:「以下关于 ReLU 激活函数的说法,哪项是正确的?」选项涉及输出范围、梯度消失、计算复杂度等。如果只背答案,你记住的是「ReLU 在负半轴梯度为 0,可能导致神经元死亡」。但如果你跑一遍代码,你会真正理解这个现象。

我写的验证代码如下:

import numpy as np import matplotlib.pyplot as plt # 定义 ReLU 及其导数 def relu(x): return np.maximum(0, x) def relu_derivative(x): return np.where(x > 0, 1.0, 0.0) # 生成输入数据,覆盖负半轴、零点和正半轴 x = np.linspace(-5, 5, 100) y = relu(x) dy = relu_derivative(x) # 打印关键点的值和梯度 print("x = -2.0, ReLU输出 =", relu(-2.0), ", 梯度 =", relu_derivative(-2.0)) print("x = 0.0, ReLU输出 =", relu(0.0), ", 梯度 =", relu_derivative(0.0)) print("x = 3.0, ReLU输出 =", relu(3.0), ", 梯度 =", relu_derivative(3.0)) # 可视化 plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.plot(x, y, label='ReLU') plt.title('ReLU Function') plt.grid(True) plt.subplot(1, 2, 2) plt.plot(x, dy, label='ReLU Derivative', color='orange') plt.title('ReLU Derivative') plt.grid(True) plt.show()

这段代码的逻辑说明:relu函数用np.maximum(0, x)实现,这是最直接的表达。relu_derivative用np.where(x > 0, 1.0, 0.0)实现,注意在 x=0 处我定义为 0,这是工程上的常见处理,因为严格来说 ReLU 在 0 点不可导,但实际训练中我们取次梯度。参数方面,np.linspace(-5, 5, 100)生成 100 个等间距点,覆盖了负半轴、零点和正半轴,这样你能清楚看到负半轴输出恒为 0、梯度恒为 0 的现象。运行后打印的三行输出直接对应题目选项里的关键判断点。

如果你把这段代码跑一遍,再回头看题目选项,你会发现你不需要背「ReLU 可能导致神经元死亡」这句话,因为你亲眼看到了负半轴梯度为 0,这意味着一旦某个神经元的权重更新到使输出恒为负,它的梯度就永远是 0,再也无法恢复。这个理解比背答案深得多。

2.4 参数化设计:让一道题覆盖多个变体

题库里很多题目是同一个知识点的不同参数问法。比如 Sigmoid 函数,有的题问输出范围,有的题问梯度最大值,有的题问与 Tanh 的关系。如果每道题都写一段独立代码,维护成本太高。我的做法是写一个参数化的验证函数,通过改变参数来覆盖不同变体。

import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_derivative(x): s = sigmoid(x) return s * (1 - s) # 参数化验证:输入范围、采样点数、是否打印极值 def verify_sigmoid(x_min=-10, x_max=10, num_points=1000, print_extremes=True): x = np.linspace(x_min, x_max, num_points) y = sigmoid(x) dy = sigmoid_derivative(x) if print_extremes: print(f"输入范围 [{x_min}, {x_max}]") print(f"Sigmoid 输出范围: [{y.min():.6f}, {y.max():.6f}]") print(f"梯度最大值: {dy.max():.6f} (出现在 x = {x[np.argmax(dy)]:.4f})") print(f"梯度最小值: {dy.min():.6f}") return x, y, dy # 覆盖题库中的三个变体 verify_sigmoid(-10, 10, 1000, True) # 标准范围 verify_sigmoid(-1, 1, 100, True) # 小范围,观察线性近似 verify_sigmoid(-100, 100, 10000, True) # 大范围,观察饱和现象

逻辑说明:verify_sigmoid函数把输入范围、采样点数、是否打印极值作为参数暴露出来。题库里问「Sigmoid 输出范围是多少」,你运行第一组参数,看到输出范围是[0.000045, 0.999955],近似(0, 1)。问「Sigmoid 梯度最大值是多少」,你看到梯度最大值是0.25,出现在x=0附近。问「为什么 Sigmoid 会导致梯度消失」,你运行第三组参数,看到当输入范围扩大到[-100, 100]时,梯度在大部分区域都接近 0,只有x=0附近有非零值。这样一道参数化函数就覆盖了题库里所有 Sigmoid 相关题目。

参数选择上,num_points建议至少 1000,否则极值点可能被跳过。x_min和x_max根据题目问法调整:问输出范围用[-10, 10],问饱和现象用[-100, 100],问线性近似用[-1, 1]。这些参数边界是我在整理题库时反复试出来的,你可以直接抄。

3. 高频题型拆解:计算题、概念题、代码题的源码化策略

3.1 计算题:把公式变成可验证的数值实验

HCIA-AI 题库里计算题占比很高,典型的有:卷积输出尺寸计算、池化输出尺寸计算、全连接层参数量计算、准确率/精确率/召回率/F1 计算、交叉熵损失计算。这些题如果只背公式,遇到变体就容易出错。我的策略是每个公式写一个独立函数,输入参数,输出结果,并在函数里加注释说明公式来源。

以卷积输出尺寸计算为例,公式是:输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2 * padding) / stride + 1。我写的验证函数如下:

def conv_output_size(input_size, kernel_size, padding=0, stride=1): """ 计算卷积层输出尺寸 input_size: 输入特征图尺寸(假设宽高相等) kernel_size: 卷积核尺寸 padding: 填充像素数 stride: 步长 """ output_size = (input_size - kernel_size + 2 * padding) / stride + 1 # 检查是否能整除,不能整除时实际输出会向下取整 if output_size != int(output_size): print(f"警告:计算结果 {output_size} 不是整数,实际输出尺寸为 {int(output_size)}") return int(output_size) # 题库中的典型参数组合 print(conv_output_size(32, 5, 0, 1)) # 输出 28 print(conv_output_size(32, 3, 1, 1)) # 输出 32 print(conv_output_size(224, 7, 3, 2)) # 输出 112 print(conv_output_size(64, 3, 0, 2)) # 输出 31,注意不能整除

逻辑说明:函数直接实现公式,但加了一个整除检查。题库里有些题目故意给出不能整除的参数组合,问实际输出尺寸是多少。这时候如果你只背公式,可能会写 31.5,但实际卷积操作中输出尺寸是向下取整的,所以是 31。这个细节我在整理时踩过坑,后来在函数里加了警告输出,提醒自己注意。

参数说明:input_size和kernel_size是必填项,padding默认 0,stride默认 1。题库里常见的参数组合我都列在注释里了,你可以直接改参数验证。对于池化层,公式类似,但通常没有 padding,且 stride 等于 kernel_size 的情况居多,你可以把conv_output_size改个名字复用。

3.2 概念题:用对比实验代替文字辨析

概念题是题库里最容易让人纠结的部分,比如「以下哪个不是监督学习」「Batch Normalization 和 Layer Normalization 的区别」「L1 和 L2 正则化的区别」。这些题如果只背文字描述,很容易混淆。我的做法是写对比实验,用代码输出差异。

以 L1 和 L2 正则化为例,我写了一个简单的线性回归实验,分别加 L1 和 L2 惩罚项,观察权重分布:

import numpy as np from sklearn.linear_model import Lasso, Ridge from sklearn.datasets import make_regression # 生成模拟数据:100 个样本,50 个特征,其中只有 10 个是真正相关的 X, y = make_regression(n_samples=100, n_features=50, n_informative=10, noise=0.1, random_state=42) # L1 正则化(Lasso) lasso = Lasso(alpha=0.1) lasso.fit(X, y) l1_nonzero = np.sum(lasso.coef_ != 0) # L2 正则化(Ridge) ridge = Ridge(alpha=0.1) ridge.fit(X, y) l2_nonzero = np.sum(ridge.coef_ != 0) print(f"L1 正则化后非零权重数量: {l1_nonzero}") print(f"L2 正则化后非零权重数量: {l2_nonzero}") print(f"L1 权重绝对值之和: {np.sum(np.abs(lasso.coef_)):.4f}") print(f"L2 权重平方和: {np.sum(ridge.coef_ ** 2):.4f}")

逻辑说明:make_regression生成的数据里只有 10 个特征真正影响目标值,其余 40 个是噪声特征。L1 正则化倾向于把不相关特征的权重压缩到 0,所以l1_nonzero应该接近 10。L2 正则化只是把权重整体缩小,不会精确到 0,所以l2_nonzero会接近 50。运行这段代码,你就能直观理解「L1 产生稀疏解,L2 产生平滑解」这句话的含义。

参数说明:alpha是正则化强度,值越大惩罚越重。题库里如果问「alpha 增大时权重如何变化」,你可以把alpha改成 0.01、0.1、1.0、10.0 分别跑一遍,观察非零权重数量的变化趋势。n_informative控制真正相关特征的数量,你可以改成 5 或 20,观察 L1 是否还能准确筛选出相关特征。

3.3 代码题:补全代码与调试的标准化流程

题库里有一类题目是给出不完整的代码,要求补全某个函数或某一行。这类题如果只看答案,你很难理解为什么填这一行。我的做法是把不完整代码和完整代码都保存下来,用 diff 对比,并在完整代码里加注释说明每一行的作用。

以「补全一个简单的全连接层前向传播」为例,题库给出的不完整代码是:

def forward(self, x): # 请补全以下代码 pass

我补全后的代码如下:

import numpy as np class SimpleLinearLayer: def __init__(self, input_dim, output_dim): # 权重初始化:使用 Xavier 初始化,适合 Sigmoid/Tanh self.W = np.random.randn(input_dim, output_dim) * np.sqrt(1.0 / input_dim) self.b = np.zeros((1, output_dim)) def forward(self, x): # 前向传播:y = xW + b # x 形状 (batch_size, input_dim) # W 形状 (input_dim, output_dim) # b 形状 (1, output_dim),广播到 (batch_size, output_dim) self.x = x # 保存输入用于反向传播 self.z = np.dot(x, self.W) + self.b return self.z # 测试 layer = SimpleLinearLayer(4, 3) x = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]) output = layer.forward(x) print("输入形状:", x.shape) print("输出形状:", output.shape) print("输出值:\n", output)

逻辑说明:forward函数的核心是np.dot(x, self.W) + self.b。这里的关键是理解矩阵乘法的维度匹配:x是(batch_size, input_dim),W是(input_dim, output_dim),相乘后得到(batch_size, output_dim),再加上b的广播。题库里有些题目会问「如果输入维度是 4,输出维度是 3,权重矩阵的形状是什么」,你运行这段代码,打印self.W.shape,看到是(4, 3),答案就明确了。

参数说明:input_dim和output_dim根据题目给定。权重初始化我用了 Xavier 初始化,这是题库里常考的知识点。如果你把初始化改成np.random.randn(input_dim, output_dim)不加缩放,你会发现输出值的方差会随着input_dim增大而增大,这解释了为什么需要 Xavier 初始化。这个对比实验能帮你理解初始化的重要性。

4. 避坑与排查:题库源码化过程中最容易翻车的五个点

4.1 环境版本不一致导致代码跑不通

现象:你从别人那里拿到一份 Notebook,在自己机器上运行时报AttributeError或ImportError,比如np.float在 NumPy 1.24 里已经被移除,但旧代码里还在用。

原因:题库源码化过程中,不同人用的库版本不同。NumPy、scikit-learn、PyTorch 的 API 在不同版本之间有破坏性变更。比如np.float在 NumPy 1.20 被弃用,1.24 被移除;sklearn.cross_validation在 0.20 被移除,改成sklearn.model_selection。

解决:在项目根目录放一个requirements.txt,固定版本号。我用的版本是numpy==1.24.3、scikit-learn==1.3.0、torch==2.0.1。如果你拿到别人的代码跑不通,先检查版本,用pip install -r requirements.txt重装。另外,在 Notebook 第一个单元格里加一段版本检查代码:

import numpy as np import sklearn import torch print("NumPy:", np.__version__) print("scikit-learn:", sklearn.__version__) print("PyTorch:", torch.__version__)

这样你一眼就能看出环境是否匹配。

4.2 随机种子未固定导致结果不可复现

现象:你运行代码得到准确率 0.85,别人运行同样的代码得到 0.82,题目答案对不上。

原因:机器学习代码里涉及随机初始化、数据划分、Dropout 等随机操作。如果不固定随机种子,每次运行结果都不同。

解决:在代码开头固定所有随机种子。我一般写一个set_seed函数:

import numpy as np import random import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)

注意torch.backends.cudnn.deterministic = True会降低训练速度,但能保证结果可复现。题库里的计算题不涉及大规模训练,所以这个代价可以接受。如果你在 GPU 上跑,还要设置torch.cuda.manual_seed_all。固定种子后,你和别人的运行结果应该完全一致。

4.3 数据形状不匹配导致广播错误

现象:运行代码时报ValueError: operands could not be broadcast together with shapes (100, 10) (100,)。

原因:NumPy 的广播机制虽然方便,但也容易让人忽略形状匹配。题库里很多计算题涉及矩阵乘法、加法、拼接,形状不匹配时要么报错,要么得到意想不到的结果。

解决:在关键操作前后打印形状。我习惯在函数里加print或assert:

def safe_dot(a, b): print(f"a.shape = {a.shape}, b.shape = {b.shape}") assert a.shape[-1] == b.shape[0], f"维度不匹配: {a.shape[-1]} != {b.shape[0]}" return np.dot(a, b)

对于加法,注意广播规则:(100, 10)和(100,)相加时,(100,)会被广播成(1, 100),然后尝试与(100, 10)匹配,失败。正确的做法是把(100,)reshape 成(100, 1)或(1, 100),具体取决于你想加在哪一维。题库里有一道题专门考广播,我踩过这个坑,后来养成了打印形状的习惯。

4.4 忽略数值稳定性导致溢出或下溢

现象:计算 Sigmoid 或 Softmax 时,输入值稍大就得到inf或nan。

原因:np.exp(1000)会溢出,np.exp(-1000)会下溢到 0。Sigmoid 函数1 / (1 + np.exp(-x))在 x 很大时,np.exp(-x)接近 0,没问题;但 x 很负时,np.exp(-x)会溢出。Softmax 函数np.exp(x) / np.sum(np.exp(x))在 x 很大时,分子分母都溢出。

解决:使用数值稳定版本。Sigmoid 可以写成:

def stable_sigmoid(x): # 对正负分别处理,避免 exp 溢出 positive_mask = x >= 0 negative_mask = ~positive_mask result = np.zeros_like(x, dtype=float) result[positive_mask] = 1 / (1 + np.exp(-x[positive_mask])) result[negative_mask] = np.exp(x[negative_mask]) / (1 + np.exp(x[negative_mask])) return result

Softmax 的标准稳定写法是减去最大值:

def stable_softmax(x): # x 形状 (batch_size, num_classes) x_shifted = x - np.max(x, axis=1, keepdims=True) exp_x = np.exp(x_shifted) return exp_x / np.sum(exp_x, axis=1, keepdims=True)

题库里有一道题问「为什么 Softmax 要减去最大值」,你运行稳定版本和不稳定版本,输入[1000, 1001, 1002],不稳定版本会得到nan,稳定版本得到正常概率分布。这个对比能帮你记住数值稳定性的重要性。

4.5 混淆评估指标的计算口径

现象:题目问精确率,你算出来和答案不一样,检查公式发现没错,但就是不对。

原因:精确率、召回率、F1 的计算依赖 TP、FP、FN 的定义,而不同题目对「正类」的定义可能不同。有的题目把类别 1 当正类,有的把类别 0 当正类。另外,多分类问题中,精确率有 macro、micro、weighted 三种平均方式,题目如果不明确,很容易算错。

解决:在代码里显式定义正类,并打印混淆矩阵。我一般用 sklearn 的confusion_matrix和classification_report:

from sklearn.metrics import confusion_matrix, classification_report import numpy as np y_true = np.array([0, 1, 1, 0, 1, 0, 1, 1, 0, 0]) y_pred = np.array([0, 1, 0, 0, 1, 0, 1, 0, 0, 1]) # 显式指定正类为 1 cm = confusion_matrix(y_true, y_pred, labels=[0, 1]) print("混淆矩阵:\n", cm) print("\n分类报告:\n", classification_report(y_true, y_pred, labels=[0, 1])) # 手动计算精确率和召回率,验证口径 TP = np.sum((y_true == 1) & (y_pred == 1)) FP = np.sum((y_true == 0) & (y_pred == 1)) FN = np.sum((y_true == 1) & (y_pred == 0)) precision = TP / (TP + FP) if (TP + FP) > 0 else 0 recall = TP / (TP + FN) if (TP + FN) > 0 else 0 print(f"\n手动计算: 精确率={precision:.4f}, 召回率={recall:.4f}")

逻辑说明:confusion_matrix的labels参数显式指定了类别顺序,这样你就能清楚看到 TP、FP、FN 分别是多少。classification_report会输出精确率、召回率、F1,你可以和手动计算的结果对比。如果题目问的是 macro 平均,你需要在classification_report里加average='macro'。这个坑我在整理题库时踩过好几次,后来养成了先打印混淆矩阵再算指标的习惯。

5. 进阶技巧:用 pytest 给题库做自动化回归测试

5.1 为什么题库也需要单元测试

题库源码化之后,你会有几十个 Notebook 和 Python 文件。每次你修改某个函数的参数,或者升级库版本,都可能影响其他题目的运行结果。如果没有测试,你很难发现哪些题目被改坏了。我给题库加了一层 pytest 测试,把每道计算题的预期输出写成断言,这样每次修改后跑一遍pytest,就能快速定位问题。

测试文件放在tests/目录下,命名规则是test_<模块名>.py。比如test_conv_output_size.py对应卷积输出尺寸计算。

5.2 用参数化测试覆盖题库变体

pytest 的@pytest.mark.parametrize非常适合题库场景,因为同一道题有多个参数变体。我写的测试如下:

import pytest from hcia_ai_question_bank.cv.conv_utils import conv_output_size @pytest.mark.parametrize("input_size, kernel_size, padding, stride, expected", [ (32, 5, 0, 1, 28), # 标准卷积,无填充 (32, 3, 1, 1, 32), # same padding (224, 7, 3, 2, 112), # 典型 ResNet 第一层 (64, 3, 0, 2, 31), # 不能整除,向下取整 (28, 5, 2, 1, 28), # 填充使输出与输入相同 ]) def test_conv_output_size(input_size, kernel_size, padding, stride, expected): result = conv_output_size(input_size, kernel_size, padding, stride) assert result == expected, f"输入 {input_size}, 核 {kernel_size}, 填充 {padding}, 步长 {stride}: 期望 {expected}, 实际 {result}"

逻辑说明:parametrize装饰器把五组参数注入测试函数,每组参数对应题库里的一道题。expected是我根据公式和实际运行结果确定的预期值。如果某次修改导致某个参数组合的输出变了,pytest 会明确指出是哪一组失败。这样你不需要手动打开每个 Notebook 检查,跑一遍测试就知道题库是否还健康。

参数说明:input_size、kernel_size、padding、stride对应卷积层的四个超参数。expected是预期输出尺寸。你可以继续往列表里加参数组合,覆盖题库里所有卷积相关题目。对于池化层,你可以写一个类似的test_pool_output_size,参数组合不同但逻辑相同。

5.3 把测试结果变成复习清单

pytest 运行后会输出一份报告,告诉你哪些测试通过、哪些失败。我把这份报告当成复习清单:通过的题目说明你已经掌握,失败的题目说明你还没理解透。每次复习前跑一遍pytest -v,失败的题目就是你需要重点看的。

另外,我还会在测试文件里加一个test_all_notebooks_run,用nbconvert执行所有 Notebook,确保它们都能从头跑到尾不报错:

import subprocess import glob def test_all_notebooks_run(): notebooks = glob.glob("hcia_ai_question_bank/**/*.ipynb", recursive=True) for nb in notebooks: result = subprocess.run( ["jupyter", "nbconvert", "--to", "notebook", "--execute", "--inplace", nb], capture_output=True, text=True ) assert result.returncode == 0, f"Notebook {nb} 执行失败:\n{result.stderr}"

这个测试会遍历所有 Notebook,逐个执行。如果某个 Notebook 因为库版本升级或代码修改而报错,你会第一时间发现。注意--inplace会覆盖原文件,建议先备份,或者改成--output输出到临时目录。

5.4 我踩过的测试坑与最终习惯

第一个坑是测试文件里硬编码了绝对路径,换一台机器就找不到文件。后来我改用pathlib.Path(__file__).parent来定位相对路径。第二个坑是 Notebook 执行测试太慢,几十个 Notebook 跑一遍要几分钟。后来我加了@pytest.mark.slow标记,日常只跑快速测试,发版前才跑全量。第三个坑是断言里用了浮点数直接比较,比如assert result == 0.25,结果因为浮点精度问题失败。后来我改成assert abs(result - 0.25) < 1e-6。

现在我的习惯是:每整理完一个模块的题目,就同步写对应的测试文件;每次修改公共函数,先跑测试再提交;每次复习前,先跑一遍全量测试,失败的题目优先看。这套流程让我在备考后期节省了大量重复检查的时间,也让我对题库里每个知识点的边界条件有了更清晰的认识。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进