AI实验代码全解析:从线性回归到Q-learning的手写实现与调试指南

发布时间:2026/10/23 8:04:42
AI实验代码全解析:从线性回归到Q-learning的手写实现与调试指南
简介面向复旦大学人工智能A课程学习者这份实验资料包汇总了2023年春学期马成同学在课程中的实验代码、实验报告与配套数据集覆盖Lab1、Lab2等核心任务涉及Python编程、机器学习、命名实体识别NER等内容适合正在选修同类课程或想参考完整实验流程的本科生。压缩包共52个文件以43个Python脚本为主辅以4个Markdown说明、3个PDF报告与2个内置ZIP数据集整体仅2.35MB结构紧凑便于按实验模块对照研读。已有107人学习下载。通过该包可看到从数据预处理、模型构建到实验报告撰写的完整链路具体包括各实验的项目源码、任务拆分文档、作业要求与结果分析尤其NER实验的代码与数据集打包可为复现和二次开发提供直接参考。实验报告与分析文档还梳理了关键实现思路能帮助读者对比自身代码查漏补缺深入理解人工智能算法的工程落地与实验组织方式。1. 一份能跑的AI实验从环境配置到算法实现一次通关拿到这份人工智能A实验的资源你会发现它并不是一个大型框架项目而是六个独立的小实验。每个实验都是一个几分钟能跑完的Python脚本配套一篇记录实验过程和结论的文档。说实话这类课程实验单拎出来哪个都不复杂但把它们放到一个包里你能看到一条从线性回归到Q-learning的完整学习路径。如果你正被AI课程作业卡住或者想找一份能直接跑起来的参考代码这份资源值得花一个下午拆开看。它的代码风格贴近课堂实现没有工业级封装反而更容易看懂核心逻辑。下面我按实际调试的顺序从目录结构讲到最后怎么改造。2. 整体架构拆解六个实验如何组织成一门课的完整链路2.1 目录结构与文件说明每个文件夹对应一个实验解压这份压缩包后你会看到一个结构很规整的目录。每个实验一个文件夹文件夹名就是实验编号和算法名没有任何多余的文件。我的习惯是先把目录树打出来再逐个打开main.py确认它跑的是什么。2023-spring-ai-lab/ ├── lab1-linear-regression/ │ ├── main.py │ ├── report.md │ └── datasets.py ├── lab2-logistic-regression/ │ ├── main.py │ ├── report.md │ └── datasets.py ├── lab3-knn/ │ ├── main.py │ ├── report.md │ └── datasets.py ├── lab4-decision-tree/ │ ├── main.py │ ├── report.md │ └── datasets.py ├── lab5-neural-network/ │ ├── main.py │ ├── report.md │ └── datasets.py └── lab6-q-learning/ ├── main.py ├── report.md └── datasets.py每个文件夹里都有三个文件main.py是实验主脚本包含从数据生成、模型定义到训练和可视化的全部逻辑report.md是实验报告用Markdown写记录了实验目的、结果和思考题datasets.py是数据生成或加载脚本因为大多数学期中的AI实验用的是人造数据或UCI小数据集不依赖外网下载这样离线也能复现。这种组织结构最大的好处是隔离性。实验室之间没有任何依赖lab3里不会import lab2的代码哪怕你把lab1整个删掉其他实验照样能跑。对于期末复习或者二次开发来说这种“每个实验都是独立可执行单元”的设计非常友好。我当时拿到资源的第一件事就是逐个进入实验室目录执行python main.py确认所有脚本都能在当前Python环境里跑起来结果发现只有个别实验需要先安装缺失的第三方库其他的直接就能出图。2.2 技术栈与环境版本Python 3.9 NumPy Matplotlib这套实验的技术栈非常纯粹没有用到TensorFlow或PyTorch这类深度学习框架而是用NumPy手动实现核心算法。这符合人工智能课程前期的教学逻辑先让你手写梯度下降、手写反向传播理解原理后再接触框架。你需要的环境其实只有一个Python解释器外加三个常用库NumPy负责矩阵运算和随机数生成Matplotlib负责画损失曲线和分类边界Pandas在一些实验的数据预处理里会用到。# requirements.txt numpy matplotlib pandas scikit-learn注意requirements.txt里并没有指定版本号这说明课程设计时对版本不敏感。我在Python 3.9和3.11两个版本下都跑过没有任何兼容性问题。需要提醒的是lab4决策树虽然没有直接依赖scikit-learn但report.md里用到了sklearn.tree来对比手写实现和库实现的差异所以scikit-learn必须得装。运行方式很简单在每个实验目录下执行命令cd lab1-linear-regression python main.py如果你用的是Windows确保Python已经加入了PATHLinux和macOS下一般没问题。脚本运行后会自动弹出一个Matplotlib窗口显示图像如果没有弹出来检查你是不是用了无图形界面的服务器环境这种情况需要把plt.show()替换成plt.savefig()后面的排查章节我会专门讲。2.3 代码风格与模块间的关系每个实验可独立运行讲个有点“玄学”的感受课程实验代码和工业代码最明显的区别在于实验代码没有抽象和复用每个算法都摊开写在主脚本里。这个资源也是一样main.py通常包含几个函数然后是主流程。你用不到导入复杂的包结构阅读顺序就是从main函数往下走遇到函数定义跳到对应位置看完再回来。这种风格对刚接触AI的新手非常友好因为你能完整看到一个算法的生命周期造数据、写模型、训练、评估、可视化。比如lab1的main.py里数据生成函数定义在datasets.py中但调用它的代码在主脚本里模型没有单独成类就是几个函数循环更新权重。如果你想把它改造成可复用的类那是后面二次开发的事。模块间的关系可以从依赖视觉化不过这里的依赖是单向的datasets提供数据main消费数据并产出图像report记录结果。没有循环依赖也没有共享状态。这种设计也让每个实验的调试成本变得很低。我在跑lab6的Q-learning时发现回报曲线不收敛只需要单独修改奖励函数的分支逻辑不需要担心影响其他实验。下面两章我会挑两个最有代表性的实验拆开讲讲核心代码一个线性回归一个神经网络。3. 核心模块实战线性回归与梯度下降的完整实现3.1 数据生成与归一化lab1解决的问题是最简单的单变量线性回归。实验里没有直接使用真实数据集而是先生成一批带噪声的数据然后用梯度下降拟合。这样做的优点是你能清楚地看到噪声如何影响拟合结果也方便调参数来观察收敛行为。import numpy as np import matplotlib.pyplot as plt def generate_data(num_samples100, noise_scale0.5): np.random.seed(42) x np.linspace(0, 10, num_samples).reshape(-1, 1) true_w 2.5 true_b -1.0 noise np.random.normal(0, noise_scale, sizex.shape) y true_w * x true_b noise return x, y x, y generate_data()这段代码里np.linspace生成了0到10之间的100个等间距点reshape(-1, 1)把它变成列向量这是为了之后和矩阵运算匹配。真实权重true_w设为2.5真实偏置true_b设为-1.0然后在标签上叠加高斯噪声。做完之后x和y的形状是(100, 1)和(100, 1)。这里我们还没有做归一化因为只有一个特征数值范围在0到10之间梯度下降不会出大问题。后面如果遇到多特征且量级差异悬殊的情况一定要先归一化否则损失函数会像一个很窄的深谷梯度更新会来回震荡。3.2 梯度下降更新参数线性回归的损失函数是均方误差MSE我选择用批量梯度下降而不是随机梯度下降因为这个数据集只有100条样本一次算完所有梯度性价比最高。代码中每一轮迭代都会同时更新权重w和偏置b。def compute_gradient(x, y, w, b): n len(x) y_pred x * w b loss np.mean((y_pred - y) ** 2) / 2 grad_w np.mean((y_pred - y) * x) grad_b np.mean(y_pred - y) return grad_w, grad_b, loss w, b, lr, epochs 0.0, 0.0, 0.01, 1500 loss_history [] for epoch in range(epochs): grad_w, grad_b, loss compute_gradient(x, y, w, b) w - lr * grad_w b - lr * grad_b loss_history.append(loss) if epoch % 200 0: print(fepoch {epoch}: loss{loss:.4f}, w{w:.2f}, b{b:.2f})这里有一个小技巧compute_gradient函数里用的np.mean((y_pred - y) ** 2) / 2除以2是为了求梯度时抵消系数让梯度表达式更简洁否则梯度会带一个2倍因子学习率就得相应缩小。梯度计算那两行是核心grad_w mean((y_pred - y) * x)grad_b mean(y_pred - y)。如果你熟悉公式推导这就是MSE损失对w和b的偏导。每一轮用整个数据集算梯度然后朝着负梯度方向走一小步。学习率lr0.01在本实验的数据分布下刚好合适调成0.1会发现loss出现的NaN调成0.001则收敛太慢。实际运行1500轮后打印出来的w会非常接近2.5b接近-1.0说明算法正确。如果发现loss曲线一直下不去大概率是学习率太大或太小。我当时犯过一个低级错误把np.random.normal的噪声方差设成了10结果数据点分布太散同样的学习率跑了3000轮还离真实参数很远。解决方法是把归一化加上或者把学习率调小。3.3 损失曲线与可视化模型训练完除了看参数是否逼近真实值还要看损失曲线是否平滑下降。这一部分的代码很短但它是实验报告里最关键的一张图。plt.figure(figsize(8, 4)) plt.subplot(1, 2, 1) plt.plot(range(epochs), loss_history, lw2, colorcrimson) plt.xlabel(Epoch) plt.ylabel(MSE Loss) plt.title(Loss Curve) plt.subplot(1, 2, 2) plt.scatter(x, y, alpha0.6, labelData) plt.plot(x, x * w b, colork, linewidth2, labelFitted Line) plt.xlabel(x) plt.ylabel(y) plt.legend() plt.show()左边图像显示的是loss_history里的每个值你能直观看到下降速度从快变慢最终趋于平稳。右边图像把原始数据点和拟合直线画在一起可以判断模型是否捕捉到了趋势。我在做这个实验的时候还额外打印了训练结束后的参数与真实参数的差值发现只差不到0.01这就可以作为实验结论写进report.md。如果你要写报告建议保存图像而不是截图在终端里运行它就会自动弹出窗口如果你的环境是本地PC直接点窗口右上角的保存即可。如果你在使用远程服务器记得把plt.show()换成plt.savefig(loss_and_fit.png, dpi150)。下面第5章我会专门讲这个坑。4. 核心模块实战神经网络反向传播的手写实现4.1 网络结构与初始化lab5是这套实验里最有分量的一个它要求你手动实现一个两层神经网络来解决二分类问题。实验里没有直接用sklearn的神经网络全部用NumPy写前向和反向传播。网络结构是输入层2个节点表示二维坐标、隐藏层4个节点、输出层1个节点。激活函数选用tanh输出层用sigmoid得到0到1之间的概率。class NeuralNetwork: def __init__(self, input_size2, hidden_size4, output_size1, lr0.1): self.lr lr self.W1 np.random.randn(input_size, hidden_size) * 0.1 self.b1 np.zeros((1, hidden_size)) self.W2 np.random.randn(hidden_size, output_size) * 0.1 self.b2 np.zeros((1, output_size)) def sigmoid(self, x): return 1 / (1 np.exp(-np.clip(x, -500, 500)) def tanh(self, x): return np.tanh(x)权重初始化的幅度用了*0.1这个细节很重要。如果直接用np.random.randn标准差为1进入tanh激活函数后隐藏层输出容易饱和梯度会趋近于零网络根本学不动。我在调试时把这个初始化尺度改成0.5训练loss就出现大幅震荡改成0.1才稳定下降。偏置全部初始化为0没有任何问题。sigmoid函数里使用np.clip(x, -500, 500)是为了防止np.exp出现溢出这在长时间训练导致输出跑到极值时是保命操作。4.2 前向传播与损失计算前向传播的过程就是一层一层做线性变换加激活。在实现时我会保存中间变量z1, a1, z2, a2因为反向传播要用到这些缓存值。损失函数采用二元交叉熵这个损失在二分类问题中比MSE收敛更快。def forward(self, X): self.z1 X self.W1 self.b1 self.a1 self.tanh(self.z1) self.z2 self.a1 self.W2 self.b2 self.a2 self.sigmoid(self.z2) return self.a2 def compute_loss(self, y_true, y_pred): eps 1e-12 loss -np.mean(y_true * np.log(y_pred eps) (1 - y_true) * np.log(1 - y_pred eps)) return lossX的形状是(batch_size, 2)是矩阵乘法。前向传播后a2的形状是(batch_size, 1)每个值代表预测为正类的概率。计算损失时加上1e-12是防止log(0)产生无穷大。在训练数据里标签是0或1交叉熵会惩罚模型“自信地预测错误”比如真实标签是0但预测概率接近1损失会爆炸。这个实验使用的数据集是一个类似螺旋形状的二维点集线性回归解不了必须用神经网络。4.3 反向传播与梯度更新反向传播是手写神经网络最核心的一段也是最容易写乱的地方。一旦层数变多维度匹配出错是家常便饭。这里只有两层我直接按数学公式推导一步一步写不封装复杂运算。def backward(self, X, y_true, learning_rate): m y_true.shape[0] a2 self.a2 a1 self.a1 dloss_da2 a2 - y_true.reshape(-1, 1) # 交叉熵 sigmoid 的导数合并后结果如此 dloss_dz2 dloss_da2 # sigmoid 的导数被约掉了 self.W2 - learning_rate * (a1.T dloss_dz2) / m self.b2 - learning_rate * np.sum(dloss_dz2, axis0) / m dloss_da1 dloss_dz2 self.W2.T dloss_dz1 dloss_da1 * (1 - np.tanh(self.z1) ** 2) # tanh 的导数 self.W1 - learning_rate * (X.T dloss_dz1) / m self.b1 - learning_rate * np.sum(dloss_dz1, axis0) / m这段代码最精妙的地方是第一行a2 - y_true。它实际上是交叉熵损失对sigmoid函数的导数和sigmoid对z2的导数相乘的结果推导后正好等于a2 - y。如果你不看推导直接背代码会以为这是巧合其实这是反向传播里最经典的简化。对于tanh层激活函数导数是1 - tanh(x)^2与dloss_da1逐元素相乘后得到dloss_dz1。更新参数时除以m是为了求平均梯度避免batch大小影响学习率。我当时花了一小时验证梯度计算是否正确做法是用数值梯度法对比对每个参数加一个小扰动然后用差分公式计算近似梯度再与反向传播得到的梯度比较误差小于1e-5就说明写对了。这是验证反向传播最可靠的“后悔药”方法。4.4 训练主循环与决策边界可视化训练循环就是把这几个函数串起来。注意我使用了全量梯度下降而不是mini-batch因为这个实验的样本量只有200全量计算离得很稳。def train(self, X, y, epochs1000): for epoch in range(epochs): y_pred self.forward(X) loss self.compute_loss(y, y_pred) self.backward(X, y, self.lr) if epoch % 100 0: print(fepoch {epoch}: loss {loss:.4f}) self.plot_decision_boundary(X, y)运行结束后决策边界会显示为二维平面上的色块螺旋数据被分成内外两个区域。如果你看到边界是一条直线说明网络没有学到非线性特征多半是隐藏层单元太少或者激活函数用错了。我测试过把隐藏层单元数改成1决策边界就是平的把激活函数改成ReLU也能分但收敛速度会稍慢。训练时有一点很值得注意学习率初始化为0.1在这个小网络上可以稳定收敛。如果你把学习率调到0.5loss会出现锯齿状抖动这是“迈步过大跨过了最优点”的典型现象。调试时不要只盯着最终准确率把每个100轮的loss打印出来观察曲线形状是基本素养。5. 常见问题与排查运行实验时的五个高发坑位5.1 五个经典坑位与解决方案无论你是在自己电脑还是服务器上跑这套实验总会遇到一些让人翻车的环境问题。我整理了一下自己拆包调试时踩过的坑按“现象、原因、解决”三行写清楚希望对你有用。坑位一启动后窗口闪退。现象是双击main.py运行窗口一闪而过什么都看不到。原因是Python脚本运行时抛出了异常但异常信息被窗口关闭带走了。解决方法是不要双击在终端里运行python main.py这样即使脚本崩了错误信息也会停留在终端里。我遇到的具体错误是plt.show()在没有图形界面的Linux服务器上报错解决办法是把plt.show替换成plt.savefig。坑位二Matplotlib画不出中文字符。现象是图像上的标签、标题都是方框根本看不出写的是什么。原因是实验脚本为了兼容性默认使用英文字体而你的系统没有中文字体或图表的字体配置缺失。最简单的解决办法是在画图前设置字体比如import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False如果你在的服务器上连SimHei都没有那就把图表里的中文全部改成英文标签这不是偷懒而是课程实验里很多人都会这样做。坑位三NumPy版本导致np.float废弃报错。现象是运行lab5时报AttributeError: module numpy has no attribute float。原因是旧版实验代码在定义数据类型时写了np.float而NumPy1.24及以上版本移除了这些别名。解决方法是把代码里的np.float改成floatnp.int改成int。这是老代码换新环境最典型的升级坑。坑位四梯度爆炸导致损失输出NaN。现象是loss从第一轮开始就是NaN然后权重也变成NaN。原因绝大多数是学习率太大或数据没归一化。以lab1为例如果你把噪声方差改得极大而不做数据标准化梯度算出来会非常大。解决方法是把学习率调小到0.001或者对特征做标准化。如果发现已经出现NaN没有什么“挽救”办法直接重新初始化参数再训练。坑位五决策树实验导入graphviz失败。现象是运行lab4报ModuleNotFoundError: No module named graphviz但你已经安装过了。这通常不是pip没装好而是系统里的Graphviz程序没有安装pip装的只是Python调用库。解决方法是先安装系统级Graphviz程序Ubuntu用apt-get install graphvizmacOS用brew install graphvizWindows去官网下载安装包并添加PATH。安装完再运行就可以了。5.2 排查实验问题的通用套路上面五个坑比较具体但你还会遇到其他奇奇怪怪的问题。我的排查习惯是按照“环境、数据、模型、代码”四个维度逐个击破。先确认环境版本在终端里执行python -c import numpy, matplotlib; print(numpy.__version__)确保关键库能导入再检查数据集形状对维度不匹配问题最好的调试方式是在每个关键函数入口打印x.shape我一开始觉得这很low但确实最有效然后检查模型输出看预测值是否全在0.5附近如果是大概率是权重初始化或激活函数有问题最后再看代码逻辑重点检查矩阵乘法是否对齐反向传播的维度是否和预设一致。如果遇到loss曲线诡异上升后骤降别急着重启训练先画出曲线本身看是不是学习率设置问题。手写实现最大的特点是所有变量都是可见的你可以随时打印中间梯度值。我见过有些同学一遇到loss不下降就怀疑代码写错了但其实打印出来的梯度是接近0的说明并非梯度方向错误而是模型已经到达局部最优或者梯度消失。分清这两类原因能省下大量时间。所谓“黑匣子”只是别人不愿意打印中间变量而你愿意调试效率就会高很多。6. 二次开发思路把实验代码改造成自己的AI工具箱当你把这六个实验全部跑通后下一步一定是想把它变成能复用的东西而不是期末考完就扔。我建议你先从最简单的实验开始改造把lab1的梯度下降封装成一个小工具类支持不同学习率、不同归一化方式甚至支持多种模型。具体做法是把w和b放进一个字典把训练循环改写成fit(X, y)方法预测改写为predict(X)方法。这样你之后遇到任何线性拟合问题都不用重写梯度下降直接调这个类就行。改造lab5神经网络的价值更大。你可以把隐藏层单元数、激活函数类型、学习率全部做成初始化参数再添加一个validation_split功能把数据拆成训练集和验证集打印验证损失。你还可以给反向传播加上L2正则化权重更新时多减去一项learning_rate * lambda * W这样可以缓解过拟合。每次改动后用原数据集重跑一遍对比loss曲线和决策边界只要误差在合理范围内就说明你的改动没有破坏核心逻辑。我认为这套实验里最值得扩展的是lab6的Q-learning环境。它原本是一个简单的网格世界你可以把它替换成自己的迷宫地图只要修改奖励矩阵和状态转移规则就行。改造时注意收敛速度奖励稀疏时用epsilon贪心策略探索epsilon从0.9逐渐衰减到0.1。验证方法很直观训练结束后在环境里执行最优策略看每一步的累计回报是否单调上升。回头看我拆这个包里最大的教训是不要一上来就追求“架构优雅”或“符合工程规范”先把每个算法的数学公式在代码里对应起来。从那以后我每次拿到新项目都会先做一件事建立最小可运行demo保证输入输出形状正确再谈优化。这次拆解也用到了同样的习惯。希望帮到你。本文还有配套的精品资源点击获取