人工智能数学基础:习题答案与NumPy代码验证实战
简介:这份面向人工智能学习者与转岗从业者的数学基础资料包,围绕唐宇迪相关课程内容整理,重点解决线性代数、概率论与统计、微积分、最优化理论等知识“看得懂却不会用”的问题,并借助习题答案与配套源代码把公式推导落到编程实践中。压缩包约6.41MB,上游文件总数与类型明细暂无有效数据,从资源构成看包含习题解答与代码示例,便于对照学习。已有1930人学习,说明其在AI数学入门与复习场景中具有一定参考价值。读者可系统梳理向量矩阵、概率分布、梯度下降、牛顿法、图论与动态规划等核心概念,并通过答案核对思路、通过代码观察算法实现,适合作为课程配套练习、面试前数学复习和项目建模前的知识查漏材料。
1. 从一份“数学补课包”说起:这套资料到底给谁用
有个做算法的朋友转行第二年,模型调不动的时候,回头翻人工智能数学基础,发现当年课堂上的公式全成了黑匣子——知道梯度要下降,说不清为什么沿负梯度方向走最快;知道要算特征值,手推一遍就卡在行列式展开。他后来拿到这套配套资料包,习题答案和源代码放在一起对照着啃,两周把卡了半年的几个点捋顺了。这份资源适合两类人:一类是正在学人工智能数学基础、课后习题做不下去想核对推导过程的学生;另一类是已经上手写模型、但底层数学只有模糊印象的工程开发者,想用可运行的代码把公式重新“摸”一遍。资料按章节组织,答案给推导,代码给验证,两边对着看,比单啃教材省力。
2. 资料包结构拆解:习题答案与源代码怎么对上号
拿到压缩包先别急着全解压到桌面,先看清分层逻辑。这类配套资料一般按“章—知识点—题号”三层组织,答案文档和代码文件用同一套编号,能不能对上号,直接决定后面阅读效率。
2.1 目录分层与文件命名规律
解压后的结构大致是这样,不同章节的题量不一样,但骨架一致:
math-for-ai/ ├── ch02_linear_algebra/ │ ├── answers/ │ │ ├── 2-1_matrix_basic.md │ │ ├── 2-5_eigen.md │ │ └── 2-8_svd.md │ └── code/ │ ├── ex2_1_matrix_basic.py │ ├── ex2_5_eigen.py │ └── ex2_8_svd.py ├── ch03_calculus/ │ ├── answers/ │ └── code/ ├── ch04_probability/ │ ├── answers/ │ └── code/ ├── requirements.txt └── README.md命名上有一条规律值得记住:章节号-题号_知识点英文缩写。前半段是定位,后半段是检索用的。答案文档用.md存,方便直接在编辑器里看公式渲染;代码文件用ex前缀,跟答案的题号一一对应。我一般先扫一遍answers目录,把文件名当目录页用,哪题想深挖再去code里找同名文件。这个习惯比从头读到尾快得多,尤其是第二遍复习的时候。
有一类文件容易看漏:某些章节下面会有extra/或者appendix/目录,放的是教材正文里没编号的补充例题,编号可能是2-x1这种。这些题往往对应更完整的工程实现,别跳过。
2.2 答案文档与代码文件的映射关系
答案文档里通常有三段结构:题目复述、推导过程、结论。代码文件对应的是第三段的验证。举个典型例子,答案里写“矩阵 A 的特征值为 λ₁=3, λ₂=1”,代码里就会构造这个 A 然后调np.linalg.eig打出来对比。这种一对一关系是这份资料最有价值的地方——它把“我推出来的对不对”变成了可执行的问题。
但不是所有题都有代码。纯推导题(比如证明某个不等式成立)通常只有答案文档,代码目录里找不到同名文件。我建议做个简单判断:answers里有的、code里没有的,标记为“纯推导”,读的时候专注逻辑链;两边都有的,优先跑代码,跑通再回头读推导,理解成本会低很多。
还有一种情况:一个代码文件对应多道题。比如ex3_2_gradient.py里可能同时验证了 3-2、3-3、3-4 三题的结论,因为它们共用同一个函数和同一组数据。这时候看代码开头的注释,通常会写明“对应习题 3-2 / 3-3 / 3-4”。养成先看文件头注释的习惯,能省掉不少来回翻找的时间。
如果发现答案和代码的结论对不上,先别怀疑自己。这类资料常见的偏差是:答案用手算精确值,代码用浮点近似,两者在小数点后几位有差异是正常的。判断标准看相对误差,量级在1e-8以内就当一致。
2.3 环境准备与第一次跑通
代码基本是 Python + NumPy 的组合,少量章节会用到 SciPy 和 Matplotlib 画图。不要直接在系统 Python 里装,容易和已有环境打架。建个独立环境,把依赖一次装齐:
# 建独立环境,Python 版本别太低,3.8 以上基本都能跑 python -m venv venv_math source venv_math/bin/activate # Windows 用 venv_math\Scripts\activate # 装依赖,numpy 和 scipy 是必需的 pip install numpy scipy matplotlib # 锁定版本,避免后续 pip 升级把代码跑崩 pip freeze > requirements.lock装完之后先别急着跑具体章节,把requirements.txt里的版本对一遍。这类资料包常见的坑是作者写代码时的 NumPy 是 1.x,你装到 2.x 之后某些 API 行为变了,比如np.float这类别名被移除,直接报AttributeError。碰到这种,两个选择:把 NumPy 降到 1.26 附近,或者手工把代码里的旧别名改掉。
环境好了之后,先跑一个最简单的验证脚本,确认链路通:
import numpy as np # 构造一个 2x2 矩阵,验证环境是否正常 A = np.array([[2.0, 1.0], [1.0, 2.0]]) # 对称矩阵,特征值必然是实数 eigvals, eigvecs = np.linalg.eig(A) print("特征值:", eigvals) print("特征向量:\n", eigvecs) # 验证 A @ v = lambda * v,残差应接近 0 for i in range(len(eigvals)): v = eigvecs[:, i] residual = A @ v - eigvals[i] * v print(f"第 {i} 个特征向量残差范数: {np.linalg.norm(residual):.2e}")这段代码干了三件事:构造矩阵、求特征分解、用定义式Av = λv反验。最后那行残差范数是关键,它把“特征向量算得对不对”变成了一个数值指标,正常输出应该在1e-15量级。如果跑到这里就是一堆nan或者残差是1e-1级别,说明环境或者代码有问题,先解决再往下走。np.linalg.eig返回的eigvals是特征值数组,eigvecs是按列存放的特征向量矩阵,取第 i 个向量要用eigvecs[:, i],这个索引方向搞反是新手高频错误。
residual那个变量名可以改,但建议保留打印格式里的科学计数法,1e-15和1e-3一眼就能分出对错,比直接打浮点数直观。
3. 核心模块实战:把线性代数与微积分的推导写成可执行代码
数学资料最容易读成“看懂了但不会用”。这一层的关键是把每个公式落到一次具体的计算上,让代码替你检查推导。下面挑三个在人工智能里出现频率最高的点,按资料包的思路走一遍。
3.1 矩阵运算与特征值分解:从笔算到 NumPy 校验
线性代数章节的习题里,特征值分解是重灾区。手算 3x3 矩阵的特征多项式就已经很烦,算完还得解三次方程。代码的价值不是替你算,而是让你确认自己算的那个 λ 到底对不对。
import numpy as np # 3x3 对称矩阵,手算特征多项式从这里开始 A = np.array([[4.0, 1.0, 1.0], [1.0, 3.0, 0.0], [1.0, 0.0, 2.0]]) # 检查对称性,对称矩阵的特征值一定是实数 assert np.allclose(A, A.T), "矩阵不对称,特征值可能是复数" eigvals, eigvecs = np.linalg.eig(A) # 对称矩阵理论上特征值为实数,取实部规避浮点噪声 eigvals = np.real(eigvals) print("特征值:", np.sort(eigvals)) # 特征值之和等于迹,之积等于行列式,这两个是必查的恒等式 trace_err = abs(eigvals.sum() - np.trace(A)) det_err = abs(eigvals.prod() - np.linalg.det(A)) print(f"迹校验误差: {trace_err:.2e}") print(f"行列式校验误差: {det_err:.2e}") # 特征向量正交性检查,对称矩阵不同特征值对应的特征向量必然正交 V = np.real(eigvecs) gram = V.T @ V print("特征向量内积矩阵:\n", np.round(gram, 6))这段的重点在后面三个校验,不是np.linalg.eig本身。np.trace(A)是矩阵的迹,等于所有特征值之和;np.linalg.det(A)是行列式,等于所有特征值之积。这两个恒等式是手算之后最省事的自检手段,误差在1e-12以内就说明特征值算对了。最后的V.T @ V得到的是特征向量之间的内积矩阵,对称矩阵的情况下应该接近单位阵(对角为 1,非对角为 0),这一步能查出特征向量是否真的正交。
参数上有一点要注意:np.linalg.eig不保证返回顺序,所以想比对具体某个 λ 对应的向量,得先排序或者按索引找。我一般用np.argsort(eigvals)拿到排序索引,再同时重排特征值和特征向量,避免两者错位。
如果跑到assert就挂了,说明构造的矩阵本身不是对称的,回头检查录入。这类手输矩阵的题,翻车八成是因为某个元素符号写反了。
3.2 梯度下降与导数的数值验证
微积分章节在人工智能里的落点基本就是梯度。资料里的推导会告诉你f'(x)怎么求,代码要回答的是“我求的这个导数公式对不对”。最稳的办法是中心差分,用数值梯度去逼近解析梯度:
import numpy as np def f(x): # 目标函数,选一个导数好推的:f(x) = x^3 - 2x^2 + 1 return x**3 - 2 * x**2 + 1 def grad_analytic(x): # 手推的解析梯度:3x^2 - 4x return 3 * x**2 - 4 * x def grad_numeric(x, h=1e-6): # 中心差分,精度比前向差分高一个量级 return (f(x + h) - f(x - h)) / (2 * h) # 在若干个点上比对解析梯度和数值梯度 for x in [-1.0, 0.0, 0.5, 1.5, 2.0]: ga = grad_analytic(x) gn = grad_numeric(x) rel_err = abs(ga - gn) / max(abs(ga), abs(gn), 1e-12) print(f"x={x:>4} 解析={ga:>8.4f} 数值={gn:>8.4f} 相对误差={rel_err:.2e}")h的取值是个技术活。取太大,差商逼近不准;取太小,f(x+h) - f(x-h)两个相近的数相减会产生有效数字丢失。1e-6是个比较稳的折中,相对误差一般在1e-10到1e-8之间。如果某个点上误差突然跳到1e-3量级,大概率是那个点的梯度值本身接近 0,分母太小把误差放大了——这时候看绝对误差更合理。
max(abs(ga), abs(gn), 1e-12)里的1e-12是防止除零,梯度恰好为 0 的点(比如x=0)如果直接用相对误差会炸。
有了这个校验器,后面手推任何复杂函数的梯度都能套进来验。资料里那些多元函数的偏导题,可以逐个变量做中心差分,把其他变量固定住,思路完全一样。我一般会把grad_numeric单独抽成一个工具函数放在utils.py里,做二次开发时反复用。
顺带说一句梯度下降本身:
def gradient_descent(start, lr=0.1, steps=50, tol=1e-8): x = start history = [x] for _ in range(steps): g = grad_analytic(x) if abs(g) < tol: # 梯度足够小,认为收敛 break x = x - lr * g # 沿负梯度方向更新 history.append(x) return x, history x_star, path = gradient_descent(start=3.0, lr=0.1) print(f"收敛点: {x_star:.6f}, 迭代次数: {len(path)-1}")lr是学习率,tol是收敛阈值,steps是硬性上限防止死循环。这个函数f(x) = x³ - 2x² + 1有两个驻点(x=0极大、x=4/3极小),从3.0出发应该收敛到4/3附近。从-1.0出发就会掉进0这个极大值点,这是数学本身的坑,不是代码的——资料里的相关习题正好用来说明“梯度下降只能找到局部最优”,跑一遍比背结论印象深。
3.3 概率分布与最大似然:把公式落到采样上
概率章节的习题里,最大似然估计是最容易“会推不会用”的部分。答案里给的是求导令其为零这一步,代码要做的是用采样数据反过来验证这个估计量。
import numpy as np rng = np.random.default_rng(42) # 固定种子,保证结果可复现 # 伯努利分布,成功概率真实值设为 0.3 p_true = 0.3 n = 200 samples = rng.binomial(1, p_true, size=n) # 伯努利的最大似然估计就是样本均值,这里直接算 p_hat = samples.mean() print(f"真实 p={p_true}, 估计 p_hat={p_hat:.4f}, 样本量={n}") # 用对数似然函数验证:把 p 扫描一遍,看极大值点在哪 ps = np.linspace(0.01, 0.99, 200) # 对数似然 L(p) = k*log(p) + (n-k)*log(1-p),k 为成功次数 k = samples.sum() log_lik = k * np.log(ps) + (n - k) * np.log(1 - ps) p_mle = ps[np.argmax(log_lik)] print(f"扫描得到的极大似然点: {p_mle:.4f}")rng.binomial(1, p_true, size=n)生成 n 个 0/1 样本,1表示成功。samples.mean()直接给出闭式解的估计值,k * np.log(ps) + (n-k) * np.log(1-ps)是对数似然在参数空间上的扫描。两条路得到的结果应该一致,差在小数点后三位以内。如果不一致,先检查k是不是用samples.sum()算的。
ps的取值范围要避开 0 和 1,否则log(0)会给-inf。这个细节在真跑的时候很容易被忽略,尤其是教材上用闭区间写公式的时候。
高斯分布那一节同理,把np.log换成高斯对数似然,然后在(mu, sigma)二维网格上取argmax,能直观看到似然曲面的形状。这类“扫描 + 取极大”的写法虽然笨,但胜在不用推解析解也能验证结论,资料里凡是标注了“可用数值方法验证”的题,都能套这个模板。
4. 排查手册:跑这份资料时最容易翻车的五处
这份资料包本身不复杂,但“能打开”和“能跑对”是两回事。下面五条是我在实际操作里反复遇到的,按现象、原因、解决依次写清楚。
4.1 报AttributeError: module 'numpy' has no attribute 'float'
现象:代码在import numpy之后、执行到某一行突然挂掉,报错信息指向np.float或np.int、np.bool。
原因:资料里的代码是较早版本写的,那几个别名在 NumPy 1.24 之后被标记弃用,2.0 之后彻底移除。新版环境直接跑旧代码必然报这个。
解决:全局搜一遍np.float、np.int、np.bool,替换成内置的float、int、bool。数量不多的话手工改最省事;文件多就写个正则批量替换,但改完必须逐一跑一遍,因为某些地方可能是np.float64被误伤。
4.2 矩阵维度对不上,shapes not aligned
现象:A @ B或者np.dot(A, B)报维度不匹配,提示里给出的两个形状看着像能乘但就是不行。
原因:两个可能。一是从答案文档里抄矩阵时行数或列数漏了一行;二是 NumPy 里一维数组既不是行向量也不是列向量,(3,)和(3,1)是两回事,跟(3,3)乘的时候行为不同。
解决:在每次乘法前加print(A.shape, B.shape),先看清形状。一维数组想当列向量用,显式写A.reshape(-1, 1);想当行向量用,写A.reshape(1, -1)。别指望 NumPy 自动推断,把形状写死能省掉一半调试时间。
4.3 特征值结果和答案对不上,差一个符号
现象:手算得到λ = 2,代码打出来是-2,或者特征向量整体反向。
原因:特征向量的方向本来就不唯一,v和-v都是合法解,不同库或者同一库的不同版本返回的方向可能相反。特征值符号错了才是真错,但很多人看到向量反了就以为全错了。
解决:比对特征值时只看数值,比对特征向量时用np.allclose并允许符号翻转,比如验证A @ v和λ * v是否同向共线,或者直接算abs(v.T @ v_ref)是否接近 1。只要Av = λv成立,方向无所谓。
4.4 对数似然算出-inf或nan
现象:概率章节的对数似然函数跑到某一步变成-inf,后面全废。
原因:np.log(p)里p取了 0 或者负数。用np.linspace(0, 1, n)生成扫描点时,端点 0 和 1 必然出现;如果p是模型输出,也可能因为数值误差压到负数。
解决:生成扫描区间时留边,写成np.linspace(1e-6, 1-1e-6, n);对模型输出加个np.clip(p, 1e-12, 1-1e-12)。同时顺手加一句assert np.all(p > 0),在出问题的前一步就拦下来,比等nan扩散出去再回头查快得多。
4.5 画图窗口一闪而过,或者中文全变方框
现象:Matplotlib 脚本跑完,图窗口刚出现就关闭;或者标题里的中文显示成方块。
原因:前者是没有调plt.show()或者脚本以非交互模式运行;后者是默认字体不含中文字形。
解决:脚本末尾显式加plt.show(),在无界面环境下改用plt.savefig("out.png", dpi=150)存盘再打开看。中文显示的问题,在画图前统一设置字体,比如指定一个系统里有的中文字体名并关闭axes.unicode_minus之外的负号兼容问题。写在这个项目里就是初始化画图库的时候一次性配好,别每张图重复设。
5. 二次开发:把答案包改成自己的练习题与验证流水线
资料包读完之后,最容易发生的退化是“看懂了,但过两个月全忘”。真正能留下来的是自己搭的那套验证流程。我的做法是把每个知识点的校验函数抽出来,变成一个可复用的断言库,新学一个公式就往里加一条。
具体分三步。第一步,把第 3 章那几段验证代码抽成独立函数,统一放在verify.py里,每个函数只做一件事:给定输入,返回解析结果和数值结果的比对结论。第二步,用pytest把它们串起来,一个知识点一个测试用例,跑一次全套就知道哪块生疏了。第三步,把这些函数当模板,遇到新公式先想“能不能用中心差分验”“能不能用迹和行列式验”,能就立刻写进库。
| 知识点 | 校验手段 | 容差参考 |
|---|---|---|
| 矩阵求逆 | A @ inv(A)与单位阵比对 | 1e-10 |
| 特征分解 | 迹、行列式恒等式 | 1e-12 |
| 导数公式 | 中心差分 | 1e-8 |
| 积分结果 | 数值积分交叉验证 | 1e-6 |
| 概率估计 | 扫描对数似然取极大 | 1e-3 |
这套东西搭起来之后,回头看那份资料包的视角就变了——从“一堆要背的题”变成“一堆待验证的假设”。哪道题的答案你看不懂,就把它的结论写成断言,让代码告诉你它成不成立。这是我的血泪经验:光读答案会产生强烈的“我懂了”的错觉,写断言的那一刻才会暴露真正的空白。
还有个技巧值得说一句:把代码里的随机种子固定下来,np.random.default_rng(42)这种写法一开始就写上。数学验证最怕“上次跑对了这次跑错了”,种子一变结果就飘,排查起来非常玄学。固定种子之后,任何一次结果异常都是代码或理解的问题,不是运气问题。
从那以后我每次看数学资料,都强制自己先把结论写成一条能跑的断言,跑通了才算过。希望帮到你。
本文还有配套的精品资源,点击获取