☰
人工智能模型与算法练习题:从理论推导到代码复现的完整指南
2026/10/13 8:52:41 网站建设 项目流程

简介:这份PDF文档是《人工智能:模型与算法》课程的配套练习题集,面向正在修读人工智能导论、机器学习基础等课程的高校学生与自学者,用于课后自测与考前复习。内容覆盖人工智能概述、可计算性理论、逻辑斯蒂回归、潜在语义分析、线性判别分析及前馈神经网络等核心章节,题型以单选与概念辨析为主,并附有知识点详解,帮助读者厘清摩尔定律、图灵测试、监督与无监督学习划分、LDA与PCA降维差异等易混淆概念。资源包共1个PDF文件,大小约266KB,轻量便携,适合打印或移动端随时翻阅。目前已有580人学习下载,可作为课程作业与期末复习的辅助材料,通过逐题练习与解析对照,快速定位知识盲区并巩固模型与算法的基本原理。

1. 从一份练习题 PDF 说起:模型与算法到底该怎么练

带过几轮校招和社招的算法岗面试之后,我越来越确信一件事:看懂模型与算法,和能上手把模型与算法跑通,中间隔着一整套练习题的距离。很多同学把《人工智能:模型与算法》这类课程的练习题 PDF 当成考前突击材料,背完公式就翻篇,结果一到真实项目里,连损失函数该选交叉熵还是 MSE、正则项系数该给多大都拿不准。这份练习题 PDF 的价值,恰恰不在“答案”,而在于它把监督学习、无监督学习、神经网络、强化学习这些模块拆成了可验证的小问题——每道题背后都对应一个可以动手复现的最小实验。

这篇文章面向三类人:正在啃《人工智能:模型与算法》课程、手里有练习题但不知道怎么把纸面推导变成代码的在校生;想系统补一遍机器学习基础、但不想再刷一遍纯理论视频的转行工程师;以及需要给团队新人设计一套“能跑起来”的算法练习题的 Tech Lead。我会按“先立住理论、再动手复现、最后避坑”的顺序,把这份练习题里最常出现的几类题型拆成可执行的方案,每一步都给出代码、参数和排错思路。你不需要有 GPU 集群,一台普通笔记本就能跟着走完。

2. 把练习题拆成可运行的最小实验:环境、数据与题型映射

2.1 练习题里到底在考什么:四类高频题型的识别方法

拿到一份《人工智能:模型与算法》练习题 PDF,先别急着从第一题往下做。我的习惯是先花二十分钟把题目按“输出形态”分类,因为不同题型对应的验证方式完全不同。常见的有四类:

第一类是推导验证型,比如“证明逻辑回归的损失函数是凸函数”“推导反向传播中某一层的梯度表达式”。这类题纸面完成即可,但如果你想真正吃透,可以用数值梯度去校验解析梯度,这是最便宜的自检手段。

第二类是手算小样本型,比如给一个三四个样本的小数据集,让你手算一轮梯度下降后的参数。这类题最适合用代码复现,因为手算容易出错,而代码能告诉你每一步的真实数值。

第三类是调参对比型,比如“比较不同学习率对收敛速度的影响”“比较 L1 和 L2 正则的效果差异”。这类题必须跑实验,而且要做多组对照。

第四类是模型选型型,比如“给定一个文本分类任务,选择合适模型并说明理由”。这类题没有唯一答案,但可以用一个小数据集把候选模型都跑一遍,用验证集指标说话。

把这四类分清楚之后,你会发现真正需要写代码的是后三类,而第一类只需要一个数值校验脚本。下面这张表是我常用的题型映射:

题型典型问法验证方式最小代码量
推导验证型证明凸性、推导梯度数值梯度校验20 行左右
手算小样本型手算一轮梯度下降逐步打印中间值50 行左右
调参对比型比较学习率/正则多组对照实验80 行左右
模型选型型选择模型并说明理由候选模型横向对比100 行左右

2.2 最小环境:不装 CUDA 也能跑完 90% 的练习题

很多练习题根本不需要深度学习框架。线性回归、逻辑回归、KNN、决策树、SVM 这些用 NumPy 加 scikit-learn 就够了,神经网络部分用 PyTorch 的 CPU 版本也完全能跑。我一般会建一个干净的 conda 环境,只装这几个包:

conda create -n ai-exercises python=3.10 -y conda activate ai-exercises pip install numpy scikit-learn matplotlib pandas torch --index-url https://download.pytorch.org/whl/cpu

这里有几个参数值得说明。Python 选 3.10 是因为它在 3.9 的稳定性和 3.11 的新特性之间比较平衡,scikit-learn 和 PyTorch 对它的支持都成熟。--index-url指向 CPU 版本的 PyTorch 源,避免默认拉取 CUDA 版本导致下载几个 G 的显卡驱动包。如果你只是做练习题,CPU 版本训练一个几百样本的小数据集,速度完全够用。

装完之后用下面这段代码验证环境:

import numpy as np import sklearn import torch print("numpy:", np.__version__) print("sklearn:", sklearn.__version__) print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) # 做一个最小可运行检查:用 numpy 解一个正规方程 X = np.array([[1, 1], [1, 2], [1, 3]]) y = np.array([2, 4, 6]) theta = np.linalg.inv(X.T @ X) @ X.T @ y print("theta:", theta) # 期望接近 [0, 2]

这段代码做了两件事:打印版本号确认安装成功,以及用正规方程解一个最简单的线性回归,验证 NumPy 的矩阵运算正常。如果theta输出接近[0, 2],说明你的环境已经可以开始做练习题了。注意torch.cuda.is_available()返回False是正常的,不影响后续练习。

2.3 数据从哪来:三类练习题自带数据集的构造方式

练习题里的数据集通常不会直接给你 CSV,而是用文字描述生成规则。我一般会按题目描述构造合成数据,这样既贴合题意,又方便控制变量。常见的有三种:

第一种是线性可分数据,用于逻辑回归和 SVM 的练习题。用sklearn.datasets.make_classification或make_blobs生成,关键是控制n_samples、n_features、class_sep这几个参数。

第二种是非线性数据,用于神经网络和核方法的练习题。用make_moons或make_circles生成,这两个数据集能很好地检验模型是否学到了非线性边界。

第三种是回归数据,用于线性回归和正则化的练习题。用make_regression生成,通过n_targets和noise控制任务难度。

from sklearn.datasets import make_classification, make_moons, make_regression # 线性可分:用于逻辑回归、SVM X_linear, y_linear = make_classification( n_samples=200, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, class_sep=1.5, random_state=42 ) # 非线性:用于神经网络、核 SVM X_moons, y_moons = make_moons(n_samples=200, noise=0.2, random_state=42) # 回归:用于线性回归、岭回归、Lasso X_reg, y_reg = make_regression( n_samples=200, n_features=5, n_informative=3, noise=10.0, random_state=42 ) print(X_linear.shape, X_moons.shape, X_reg.shape)

class_sep控制两类之间的分离程度,值越大越容易分类,我一般从 1.0 开始试,如果模型准确率直接到 100%,就降到 0.8 增加难度。noise控制月牙数据的噪声,0.2 是一个既能看出非线性边界、又不会太乱的值。n_informative=3表示 5 个特征里只有 3 个真正影响目标,剩下 2 个是干扰项,这正好用来练习特征选择。

3. 监督学习练习题:从手算梯度到代码复现的完整路径

3.1 线性回归与梯度下降:手算一轮和代码跑一轮的对照

练习题里最常见的题型之一,是给一个小数据集,让你手算一轮梯度下降。比如三个样本(1,2)、(2,4)、(3,6),初始参数w=0, b=0,学习率 0.1,让你算更新后的参数。手算的过程是:先算预测值,再算损失,再算梯度,最后更新。但手算容易在符号上翻车,我一般会用代码把每一步打印出来对照。

import numpy as np # 练习题数据 X = np.array([1, 2, 3], dtype=float) y = np.array([2, 4, 6], dtype=float) # 初始化参数 w, b = 0.0, 0.0 lr = 0.1 # 一轮梯度下降 y_pred = w * X + b loss = np.mean((y_pred - y) ** 2) grad_w = np.mean(2 * (y_pred - y) * X) grad_b = np.mean(2 * (y_pred - y)) w_new = w - lr * grad_w b_new = b - lr * grad_b print(f"预测值: {y_pred}") print(f"损失: {loss:.4f}") print(f"grad_w: {grad_w:.4f}, grad_b: {grad_b:.4f}") print(f"更新后 w: {w_new:.4f}, b: {b_new:.4f}")

这段代码的关键在于梯度的计算方式。grad_w是损失对w的偏导,公式是mean(2 * (y_pred - y) * X),其中2来自平方项的求导,X来自w * X对w的求导。grad_b少了X因子,因为b是常数项。运行后你会看到grad_w和grad_b都是负数,因为初始预测值 0 小于真实值,梯度指向增加参数的方向,更新后w和b都会变大。如果你手算的结果和代码不一致,优先检查是不是漏了mean或者符号搞反了。

3.2 逻辑回归与交叉熵:为什么不能用 MSE 做分类

练习题里经常问“逻辑回归为什么用交叉熵而不是 MSE”。标准答案是 MSE 在 sigmoid 输出下是非凸的,容易陷入局部最优,而交叉熵是凸的。但光背结论没用,我一般会写一段代码把两种损失函数的梯度都算出来,用数值梯度校验,直观看到 MSE 的梯度在预测值接近 0 或 1 时会消失。

import numpy as np def sigmoid(z): return 1 / (1 + np.exp(-z)) # 单个样本:x=1, 真实标签 y=1 x, y = 1.0, 1.0 w = 0.0 # 前向 z = w * x p = sigmoid(z) # 交叉熵损失及其梯度 ce_loss = -(y * np.log(p + 1e-8) + (1 - y) * np.log(1 - p + 1e-8)) ce_grad = (p - y) * x # MSE 损失及其梯度 mse_loss = 0.5 * (p - y) ** 2 mse_grad = (p - y) * p * (1 - p) * x print(f"p={p:.4f}") print(f"交叉熵损失={ce_loss:.4f}, 梯度={ce_grad:.4f}") print(f"MSE损失={mse_loss:.4f}, 梯度={mse_grad:.4f}")

运行后你会看到,当p接近 0.5 时,MSE 的梯度里多了一个p*(1-p)因子,这个因子在p接近 0 或 1 时会趋近于 0,导致梯度消失。而交叉熵的梯度就是p - y,干净利落。这就是为什么分类任务默认用交叉熵。参数上,1e-8是为了防止log(0)报错,实际写代码时可以用np.clip替代。

3.3 正则化练习题:L1 和 L2 的系数路径怎么画

“比较 L1 和 L2 正则化对系数的影响”是练习题里的常客。L1 会让部分系数变成 0,产生稀疏解;L2 会让系数整体缩小但不为 0。光看公式不够直观,我一般会画一张系数路径图,横轴是正则强度,纵轴是各特征的系数值。

import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import Lasso, Ridge from sklearn.datasets import make_regression X, y = make_regression(n_samples=200, n_features=10, n_informative=5, noise=10, random_state=42) alphas = np.logspace(-3, 2, 50) lasso_coefs, ridge_coefs = [], [] for a in alphas: lasso = Lasso(alpha=a, max_iter=10000).fit(X, y) ridge = Ridge(alpha=a).fit(X, y) lasso_coefs.append(lasso.coef_) ridge_coefs.append(ridge.coef_) lasso_coefs = np.array(lasso_coefs) ridge_coefs = np.array(ridge_coefs) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(alphas, lasso_coefs) axes[0].set_xscale("log") axes[0].set_title("Lasso (L1) 系数路径") axes[1].plot(alphas, ridge_coefs) axes[1].set_xscale("log") axes[1].set_title("Ridge (L2) 系数路径") plt.tight_layout() plt.show()

alphas用logspace从 0.001 到 100 取 50 个点,覆盖从弱正则到强正则的范围。max_iter=10000是因为 Lasso 在强正则下收敛慢,默认的 1000 次可能不够。画出来的图里,Lasso 的系数会在某个 alpha 之后直接掉到 0,而 Ridge 的系数是平滑衰减。这个实验能帮你直观理解为什么 L1 可以做特征选择,而 L2 更适合处理共线性。

4. 无监督与神经网络练习题:聚类、降维和反向传播的代码落地

4.1 K-Means 聚类:肘部法和轮廓系数怎么选 K

练习题里给一个无标签数据集,让你选 K 值,标准做法是肘部法加轮廓系数。但很多人只画肘部图就下结论,忽略了轮廓系数在非凸簇上的局限性。我一般两个指标都算,互相印证。

import numpy as np from sklearn.cluster import KMeans from sklearn.datasets import make_blobs from sklearn.metrics import silhouette_score X, _ = make_blobs(n_samples=300, centers=4, cluster_std=0.8, random_state=42) inertias, silhouettes = [], [] K_range = range(2, 10) for k in K_range: km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X) inertias.append(km.inertia_) silhouettes.append(silhouette_score(X, labels)) for k, inertia, sil in zip(K_range, inertias, silhouettes): print(f"K={k}, inertia={inertia:.2f}, silhouette={sil:.4f}")

n_init=10表示用 10 组不同初始中心跑,取最优结果,避免陷入局部最优。inertia_是簇内平方和,越小越紧凑,但会随 K 增大单调下降,所以要看拐点。silhouette_score范围是 -1 到 1,越接近 1 越好。如果肘部图的拐点和轮廓系数的峰值不在同一个 K,优先信轮廓系数,因为肘部法在簇重叠时拐点不明显。

4.2 PCA 降维:解释方差比和重构误差的联合判断

PCA 练习题通常问“降到几维合适”。标准答案是看累计解释方差比达到 95%,但我会额外算一下重构误差,因为解释方差比高不代表重构误差一定小,尤其在数据尺度差异大时。

import numpy as np from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler from sklearn.datasets import load_digits digits = load_digits() X = StandardScaler().fit_transform(digits.data) pca = PCA().fit(X) cum_var = np.cumsum(pca.explained_variance_ratio_) # 找累计解释方差比达到 95% 的维度 n_95 = np.argmax(cum_var >= 0.95) + 1 print(f"达到 95% 解释方差比需要 {n_95} 维") # 重构误差 for n in [10, 20, n_95]: pca_n = PCA(n_components=n) X_reduced = pca_n.fit_transform(X) X_reconstructed = pca_n.inverse_transform(X_reduced) mse = np.mean((X - X_reconstructed) ** 2) print(f"n_components={n}, 重构 MSE={mse:.4f}")

StandardScaler是必须的,因为 PCA 对尺度敏感,如果某个特征数值范围远大于其他特征,它会主导主成分方向。explained_variance_ratio_是每个主成分解释的方差比例,累计求和后找第一个超过 0.95 的位置。重构误差用inverse_transform把降维后的数据还原,再算 MSE。如果 95% 对应的维度和重构误差明显下降的维度不一致,说明数据里有一些低方差但重构贡献大的方向,这时候可以适当多保留几维。

4.3 反向传播手推与代码校验:数值梯度的用法

神经网络练习题里最硬核的一类,是让你手推反向传播的梯度表达式。手推完之后,用数值梯度校验是最可靠的自检方式。数值梯度的原理是给每个参数加一个极小扰动,看损失变化,再除以扰动值。

import numpy as np def sigmoid(z): return 1 / (1 + np.exp(-z)) def forward(X, W1, b1, W2, b2): Z1 = X @ W1 + b1 A1 = sigmoid(Z1) Z2 = A1 @ W2 + b2 A2 = sigmoid(Z2) return Z1, A1, Z2, A2 def loss_fn(y, A2): return -np.mean(y * np.log(A2 + 1e-8) + (1 - y) * np.log(1 - A2 + 1e-8)) # 构造小数据 np.random.seed(42) X = np.random.randn(5, 3) y = np.random.randint(0, 2, size=(5, 1)).astype(float) W1 = np.random.randn(3, 4) * 0.1 b1 = np.zeros((1, 4)) W2 = np.random.randn(4, 1) * 0.1 b2 = np.zeros((1, 1)) # 解析梯度 Z1, A1, Z2, A2 = forward(X, W1, b1, W2, b2) dZ2 = (A2 - y) / len(y) dW2 = A1.T @ dZ2 db2 = np.sum(dZ2, axis=0, keepdims=True) dA1 = dZ2 @ W2.T dZ1 = dA1 * A1 * (1 - A1) dW1 = X.T @ dZ1 db1 = np.sum(dZ1, axis=0, keepdims=True) # 数值梯度校验 dW1[0, 0] eps = 1e-5 W1_plus = W1.copy(); W1_plus[0, 0] += eps W1_minus = W1.copy(); W1_minus[0, 0] -= eps loss_plus = loss_fn(y, forward(X, W1_plus, b1, W2, b2)[3]) loss_minus = loss_fn(y, forward(X, W1_minus, b1, W2, b2)[3]) num_grad = (loss_plus - loss_minus) / (2 * eps) print(f"解析梯度 dW1[0,0]: {dW1[0, 0]:.6f}") print(f"数值梯度 dW1[0,0]: {num_grad:.6f}") print(f"相对误差: {abs(dW1[0, 0] - num_grad) / max(abs(num_grad), 1e-8):.2e}")

eps=1e-5是数值梯度的经典取值,太小会受浮点误差影响,太大会导致近似不准。相对误差在1e-6量级说明解析梯度正确,如果超过1e-3就要检查推导。这段代码只校验了dW1[0,0]一个元素,实际练习时建议写个循环把所有参数都校验一遍。注意dZ2 = (A2 - y) / len(y)里的/ len(y)是因为损失用了mean,如果损失用sum就不需要除。

5. 练习题复现中的避坑与排查:那些让结果对不上的细节

5.1 随机种子没固定,结果每次都不一样

现象:同一份代码跑两次,损失曲线和准确率都不一样,手算结果和代码结果对不上。

原因:数据划分、参数初始化、K-Means 初始中心、Dropout 掩码都依赖随机数,没有固定种子时每次运行都不同。

解决:在代码开头统一设置np.random.seed(42)和torch.manual_seed(42),scikit-learn 的train_test_split、KMeans、MLPClassifier等都要显式传random_state=42。注意 PyTorch 的 DataLoader 如果用了shuffle=True,还要设generator=torch.Generator().manual_seed(42)。

5.2 特征没标准化,梯度下降震荡不收敛

现象:线性回归或逻辑回归的损失曲线剧烈震荡,学习率调到很小也不收敛。

原因:不同特征的数值范围差异大,比如一个特征在 0 到 1 之间,另一个在 0 到 10000 之间,导致损失曲面在不同方向上曲率差异巨大,梯度下降走的是“之”字形。

解决:用StandardScaler做零均值单位方差标准化,或者用MinMaxScaler缩放到 0 到 1。注意标准化要在训练集上fit,然后transform测试集,不能在全量数据上fit,否则会泄露测试集信息。

5.3 学习率设太大,损失直接变成 NaN

现象:训练几个 epoch 后损失突然变成nan,参数也变成nan。

原因:学习率过大导致参数更新步长太大,越过了损失曲面的最低点,甚至让log或exp的输入溢出。

解决:先把学习率降到 1e-3 或 1e-4 试,确认能正常下降后再逐步调大。如果已经出现nan,在损失函数里加1e-8防止log(0),用torch.nn.utils.clip_grad_norm_做梯度裁剪。另外检查数据里有没有inf或nan,用np.isnan(X).sum()排查。

5.4 训练集和测试集分布不一致,准确率虚高

现象:训练集准确率 99%,测试集只有 60%,差距大得离谱。

原因:划分数据时没有打乱,或者用了时间序列数据但随机划分,导致训练集和测试集分布不同。另一种可能是数据泄露,比如标准化时用了全量数据。

解决:分类任务用train_test_split(stratify=y)保持类别比例,时间序列用时间切分而不是随机切分。标准化、特征选择、PCA 这些步骤都要在训练集上fit,再应用到测试集。如果数据量小,用交叉验证代替单次划分。

5.5 正则化系数没调,模型要么欠拟合要么过拟合

现象:Lasso 的 alpha 设成默认值 1.0,结果所有系数都被压成 0,模型完全欠拟合;或者 alpha 设成 1e-6,正则化形同虚设,过拟合依旧。

原因:正则化系数的合适范围取决于数据尺度和特征数量,没有万能默认值。

解决:用交叉验证选 alpha,LassoCV和RidgeCV会自动在给定范围内搜索。我一般先用np.logspace(-3, 3, 50)粗搜,找到最优量级后再在该量级附近细搜。注意 alpha 是对损失函数的惩罚强度,数据标准化之后 alpha 的范围会更稳定。

6. 把练习题变成可复用的验证脚本:一个进阶习惯

做练习题最大的浪费,是做完就扔。我的习惯是把每类题型沉淀成一个可复用的验证脚本,下次遇到类似题目直接改数据和参数就能跑。比如下面这个模板,把数据生成、模型训练、指标评估、可视化串成一条流水线,换数据集只需要改load_data函数。

import numpy as np from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report def load_data(): from sklearn.datasets import make_classification X, y = make_classification( n_samples=500, n_features=10, n_informative=6, n_redundant=2, class_sep=1.0, random_state=42 ) return X, y def build_pipeline(): return Pipeline([ ("scaler", StandardScaler()), ("clf", LogisticRegression(C=1.0, max_iter=1000, random_state=42)) ]) def evaluate(X, y): X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) pipe = build_pipeline() pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test) print(classification_report(y_test, y_pred)) cv_scores = cross_val_score(pipe, X, y, cv=5, scoring="f1") print(f"5 折交叉验证 F1: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}") if __name__ == "__main__": X, y = load_data() evaluate(X, y)

这个模板的价值在于把标准化和分类器封装进Pipeline,避免每次手动fit_transform和transform搞混。stratify=y保证训练集和测试集的类别比例一致,cross_val_score给出更稳健的性能估计。C=1.0是逻辑回归的默认正则强度,实际调参时用GridSearchCV在[0.01, 0.1, 1, 10, 100]里搜。

我一般会为线性模型、树模型、神经网络各准备一个这样的模板,做练习题时直接套。时间久了你会发现,练习题里 80% 的代码都是重复的,真正需要改的只有数据加载和模型定义。这个习惯帮我省下了大量重复劳动,也让我在面试时能快速写出可运行的代码,而不是临时拼凑。

最后一个血泪经验:练习题里的“标准答案”不一定是最优解。我见过太多题目默认用准确率评估不平衡数据,或者默认用 MSE 做分类,这些在真实场景里都是坑。做练习题时多问一句“这个选择在什么条件下成立”,比单纯做对答案有价值得多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询