简介:面向正在系统学习吴恩达机器学习课程、需要课下练习与复习整理的学习者,这份资料包的核心是课后编程作业的无答案版与有答案版,其中有答案部分又分为三版,便于先独立完成再对照检查,也适合教师备课与辅导时参考。包内还附课堂讲义、可直接打印的作业题目合并版以及中文个人笔记,内容覆盖线性回归、逻辑回归、聚类、主成分分析等典型实验场景,既能用于课程同步练习,也适合考前复盘、查漏补缺和整理实验思路。压缩包共一千零二十八个文件,约二百零二兆字节,主要由程序脚本、数据文件、交互式笔记本、PDF讲义与题目、说明文本及图表构成,目录按作业和讲义模块组织,检索与打印都较方便。已有二千五百三十八人学习下载,适合希望把编程作业、课堂讲义和中文笔记配套使用的初学者与进阶学习者。
1. 先弄清这份资源是什么:两版作业加一份笔记
很多人学机器学习的第一周都是这么过的:视频一集不落地看完,公式推导跟着点头,感觉自己懂了;合上电脑想写两行代码,却发现连「数据读进来之后该干什么」都不知道。这份资源针对的就是这个卡点。它是一套经典机器学习入门课的课后编程作业合集,同一个练习给了两个版本:无答案版把核心函数挖空,留给你自己填;有答案版保留完整实现。另外还附了一份学习过程中整理下来的个人笔记。它适合刚看完前几周课程、想动手却不知道从哪下手的新手,也适合学了一半、发现自己只会调包、想补回基本功的人。
2. 为什么先做作业:看懂视频和写出代码之间隔着什么
新手最容易产生的错觉,是把「听懂了」当成「学会了」。看老师推导公式的时候,每一步都顺理成章,大脑会给出一个「这我明白」的信号;可一旦让你自己从空白文件开始写,那个信号就消失了。这中间缺的东西,叫「把符号翻译成可执行动作」的能力。这份作业合集的价值,就在于它把这个翻译过程切成了很多个足够小的台阶,每一级台阶只让你填一个函数,填对了立刻能在图上看到变化。
2.1 视频里的「懂」是一种错觉
举个不太恰当但很贴切的例子:看别人举铁,你能看懂他每个动作在练哪块肌肉,但你自己上手,杠铃可能根本举不起来。机器学习也一样。视频讲的是「代价函数是预测值和真实值差的平方和」,这句话听上去毫无门槛,可落到代码里,你要决定用循环还是向量化、要不要除以样本数、除以 m 还是 2m、得到的标量是 float 还是 numpy 类型。这些细节视频里不会一个个讲,但作业会逼着你一个个面对。
我带过的新手里,进度卡住的人往往不是数学差的,而是「从来没让一段机器学习代码在自己手里跑出过结果」的。只要有过一次从零写代价函数、看着损失值随迭代次数下降的经历,后面再看到任何模型,心里都会有个锚点:哦,无非是定义预测、定义损失、想办法让损失变小。这个锚点,看多少视频都给不了,只能自己跑一遍才有。这也是我建议新手先做作业再看更多视频的原因——顺序反过来,很容易变成收藏夹里躺着一堆课,手上一个能跑的程序都没有。
2.2 无答案版和有答案版,分别什么时候打开
这两版不是让你二选一的,是让你分阶段用的。用错顺序,效果会差很远:一上来就开有答案版,你会不自觉地「参考」,看着看着就变成了抄写;只留无答案版,卡三天没进展,热情先被磨没了。比较稳的用法是这样:
| 阶段 | 打开哪个版本 | 具体做法 |
|---|---|---|
| 刚读完题目 | 无答案版 | 只填空,不追求一次写对,允许写得丑 |
| 卡住 20 分钟以上 | 无答案版 + 笔记 | 先看笔记里有没有记过同类问题的思路 |
| 自己写完并跑通 | 有答案版 | 对照实现差异,重点看向量化和边界处理 |
| 完全跑不出结果 | 有答案版 | 只看当前卡住的那一个函数,看完立刻关掉重写 |
关键在于第三行:哪怕你的实现跑通了,也要去对照一次答案,因为新手写出来的代码常见问题是「能跑但写错了」。比如代价函数里2*m写成m,损失值会整体翻倍,梯度下降照样能收敛,图画出来也像模像样,你要不是跟答案对一遍,根本发现不了。这种「错得很安静」的 bug,才是新手阶段最需要警惕的东西。
提示:对照答案时不要一行一行读,先看函数签名和返回值的形状,再看核心那三五行。读太多容易打击信心,也容易变成抄。
2.3 一份个人笔记的真实价值:它记的是别人的弯路
配套的那份笔记,价值不在知识点本身——知识点视频里都有,书上更系统。它真正有用的地方,是记录了一个人学到这里时具体卡在哪。比如「这里为什么要转置」「这个下标为什么从 0 开始而不是 1」「这个报错是因为形状不对」,这些都是视频不会讲、教材懒得写、但你一定会撞上的内容。
我自己的习惯是:遇到一个让我停下来超过十分钟的问题,就在笔记里加一条,格式固定成三行——报错长什么样、我以为是哪里错了、实际是哪里错了。这份资源里的笔记虽然不是你自己写的,但它能起到的作用类似一张别人画好的地图:你不用重走所有弯路,但至少知道弯在哪。等你把作业做完,最该做的事其实是自己再写一份,因为别人的笔记只能提醒你,自己的笔记才能救你。
3. 动手前的准备:三件事做完再打开第一个文件
很多新手在这一步就翻车:文件下载下来,双击打开,看到一堆.ipynb和.txt,不知道从哪进,于是关掉,明天再说,然后就再也没打开过。准备工作其实只有三件事,做完之后你就能安稳地跑第一个例子。
3.1 装好三样东西就够:Python、Jupyter、三个库
不需要装一堆东西,也不要一上来搞环境管理工具链,那会让你在真正学机器学习之前先学一周环境配置。入门阶段只需要 Python 解释器、能打开.ipynb的 Jupyter 环境,以及三个库:numpy(做数组运算)、matplotlib(画图)、scipy(有些作业会用到它做优化)。
# 在已经装好 Python 的前提下,一条命令装齐三个库 pip install numpy matplotlib scipy # 检查是否装好:能打印出版本号就没问题 python -c "import numpy, matplotlib, scipy; print(numpy.__version__)"pip是 Python 自带的包管理器,install后面跟包名。如果提示pip 不是内部或外部命令,说明 Python 装了但没加进系统路径,重新装一次并勾选「Add Python to PATH」最省事。如果装得很慢,可以在命令后面加上国内镜像源的参数,比如-i加一个镜像地址,这个地址网上搜「pip 镜像」就有,不同网络环境选不同的即可。
至于 Jupyter,如果你装的是完整版 Python 发行包,一般已经自带;没有的话pip install notebook,然后在作业文件夹里执行jupyter notebook,浏览器会自动打开一个页面,点开.ipynb就能一个格子一个格子地跑。
注意:不要为了「更专业」去折腾虚拟环境、容器之类的东西。入门第一个月的目标只有一个——让代码跑起来。工具链可以在你能独立完成三四个练习之后再慢慢补。
3.2 目录怎么摆:给「看答案」这件事加一道门槛
资源到手第一步不是打开,而是先把文件夹理清楚。建议在电脑里建一个总目录,里面按下面这样分:
notes/放那份个人笔记,以及你自己新写的笔记exercise/放无答案版,这是你每天真正动手的地方answer/放有答案版,平时不看,只在自己写完或者完全卡死时才打开data/放作业用到的.txt或.csv数据文件output/放你跑出来的图,方便回头对比
为什么强调分开?因为人是有惰性的。如果两个版本的作业躺在同一个文件夹里,你几乎一定会在第三次报错的时候顺手点开答案。把它们隔开,你就多了一个「我要主动去另一个文件夹」的动作,这个动作很小,但足以让你多坚持十分钟。这十分钟往往就是你自己想出来的时间。
我自己有个笨办法:把answer/文件夹改名成一个很长的名字,比如「先自己写完再看这个文件夹」,每次点进去都要看一眼这句话。听起来有点傻,但血泪经验告诉我,防手滑比靠意志力靠谱得多。
3.3 第一个例子:把数据读进来并画一张图
正式开始之前,先跑一个「什么都不训练」的最小例子,目的只有一个:确认数据能读进来、图能画出来。这一步跑通了,后面所有练习你都有一份可以复制的模板。
import numpy as np import matplotlib.pyplot as plt # 读入两列数据,逗号分隔:第一列是自变量,第二列是目标值 data = np.loadtxt("data/ex1data1.txt", delimiter=",") X = data[:, 0] # 取所有行的第 0 列 y = data[:, 1] # 取所有行的第 1 列 print(X.shape, y.shape) # 期望输出 (97,) (97,) print(X[:5], y[:5]) # 打印前 5 个值,肉眼确认没读错 plt.scatter(X, y, marker="x", c="r") # 散点图:每个点一个样本 plt.xlabel("x") plt.ylabel("y") plt.show()逐行说清楚:np.loadtxt的三个关键参数是文件名、delimiter(分隔符,这里数据是逗号分隔的,csv 一定是逗号,纯空格分隔的文件要写delimiter=" "或者干脆不写)、以及可选的skiprows(跳过前几行表头)。data[:, 0]里的冒号表示「所有行」,后面的 0 表示「第 0 列」,这是 numpy 的切片写法,和 Python 列表的切片是同一套逻辑,只不过多了一个维度。
X.shape打印出来应该是(97,),注意这表示一个一维数组,不是(97, 1)的二维列向量。这个区别在入门阶段会反复折磨你,后面第 5 章会专门讲。plt.scatter画散点图,marker="x"是点的形状,c="r"是颜色。如果plt.show()之后什么都没出现,多半是你用的编辑器不支持内嵌显示,换成 Jupyter 就好了。
如果图出来了,一个红叉一个红叉地铺在那儿,能看出明显的上升趋势,恭喜,环境这关过了。接下来才是真正开始学。
4. 第一个完整练习:单变量线性回归走一遍
这个练习的目的不是让你学会线性回归——线性回归本身十分钟就能讲完——而是让你完整体验一次机器学习项目的循环:看数据、定假设、写损失、优化参数、验证结果。这个循环在后面的逻辑回归、神经网络里会一模一样地重复,只是每一步变复杂一点。所以第一次走的时候慢一点没关系,把每一步都弄明白,后面能省很多时间。
4.1 先看数据长什么样,别急着写公式
新手最常见的做法是拿到题就开始抄公式,完全不看数据。等你写完发现损失怎么都不下降,回头一看,数据里有一列全是空值,或者量纲差了三个数量级。先花五分钟做三件事:打印形状、看前几行、画出来。
print("样本数:", X.shape[0]) print("X 最小值/最大值:", X.min(), X.max()) print("y 最小值/最大值:", y.min(), y.max()) print("有没有缺失值:", np.isnan(X).any(), np.isnan(y).any())这四行能帮你排掉一大半低级问题。np.isnan会返回一个布尔数组,.any()表示「只要有一个 True 就返回 True」,所以输出False False说明数据是干净的。看最大值最小值是为了判断量纲:如果 X 在 5 到 25 之间,y 在 -5 到 25 之间,那基本不用归一化;如果 X 是几万、y 是个位数,就必须先做特征缩放,否则梯度下降会走得极其缓慢,慢到你会以为代码写错了。
画图这一步也不要省。散点图能告诉你两件公式给不了的事:数据是不是大致呈线性关系,以及有没有明显的离群点。如果图上有一个点孤零零地飘在角落,那它对损失函数的影响会非常大,你得心里有数,别到时候发现参数被一个点带偏了还以为是 bug。
4.2 把代价函数抄成代码:公式里每个符号都要有落点
代价函数衡量的是「当前这组参数有多差」。公式写成代码的时候,新手最容易犯的错是把符号照抄一遍,但没搞清每个符号对应数组的哪一部分。先写一个最好懂的循环版本:
def compute_cost(X, y, w, b): """返回当前参数 (w, b) 下的均方误差代价""" m = X.shape[0] # 样本个数,就是数组长度 total = 0.0 # 累加器,初始为 0 for i in range(m): f_wb = w * X[i] + b # 对第 i 个样本的预测 total += (f_wb - y[i]) ** 2 # 残差平方累加 return total / (2 * m) # 除以 2m,这里的 2 是为了后面求导好看三个参数:X是特征数组,y是真实值数组,w和b是你正在评估的那组参数。m是样本数,注意它来自X.shape[0]而不是写死的数字,这样换一份数据代码不用改。最后除以2 * m而不是m,这是这门课约定俗成的写法——多出来的 2 在求导时会和平方项的 2 抵消,让梯度公式更干净。你在别的地方看到除以m的版本也没错,只是梯度会差一个因子 2,学习率相应调一下就行,两种不能混用。
写完之后先别优化,用w=0, b=0调一次,看看输出是不是等于y的平方和除以2m。这是个手算就能验证的检查点,比对着答案看更让人踏实。等这个版本跑通了,再改写成向量化版本:
def compute_cost_vectorized(X, y, w, b): m = X.shape[0] residuals = w * X + b - y # 一次性算出所有样本的残差 return np.sum(residuals ** 2) / (2 * m)向量化版本把循环去掉了,w * X + b - y直接对整 个数组做运算,这叫广播。两个版本结果必须完全一致,如果不一致,说明你循环版本里某个下标写错了。这种「先写笨版本再写快版本,然后互相对照」的习惯,在入门阶段非常值钱,因为它给了你一个不依赖答案的自检手段。
4.3 梯度下降:让参数一小步一小步往下挪
有了代价函数,下一步是找一组让它变小的参数。梯度下降的思路很朴素:算出当前位置的斜率,然后朝着下坡方向挪一小步,重复很多次。
def gradient_descent(X, y, w, b, alpha, num_iters): m = X.shape[0] for i in range(num_iters): dj_dw, dj_db = 0.0, 0.0 for j in range(m): err = (w * X[j] + b) - y[j] # 预测减真实 dj_dw += err * X[j] dj_db += err # 关键:先用临时变量存好,再同时更新 tmp_w = w - alpha * dj_dw / m tmp_b = b - alpha * dj_db / m w, b = tmp_w, tmp_b return w, b四个新参数:alpha是学习率,决定每一步迈多大;num_iters是迭代次数;dj_dw和dj_db是两个方向上的梯度累加器。最容易错的地方是更新顺序——如果你写成先更新w,再用新的w去算b,那就变成了两个参数不同步,结果会有细微偏差,初期不容易发现,迭代多了就明显跑偏。所以一定要用tmp_w、tmp_b过渡,或者像上面那样用元组赋值。
选alpha是个经验活。从0.01开始试是最稳的,跑通之后可以试0.03、0.1,观察代价下降得快不快。如果代价不降反升,甚至变成nan,说明步子迈太大,越过了最低点,把alpha调小十倍再试。反过来如果代价降得慢吞吞,迭代一千次还在高位,说明步子太小,可以适当调大。这个「调参看曲线」的过程没有公式可循,只能自己试几轮,试出来的手感比记住结论有用。
跑完之后把结果画出来:原始散点加上你拟合出的那条直线。如果直线大致穿过数据中间,说明这套流程走通了。
4.4 和答案对不上时的三步自查
第一次跑完,损失值大概率跟有答案版对不上。这种情况太正常了,按下面的顺序查,能解决绝大多数问题。
第一步,查形状。X是(97,)还是(97,1),直接决定了w * X + b是标量还是数组。打印X.shape、w的类型、compute_cost的返回值类型,看看有没有意外出现数组。
第二步,查常数。确认你用的是2*m还是m,学习率是不是同一个数量级,迭代次数是不是一样。这三项只要有一项不同,损失值就不可能对上,但这不代表你的代码错了。
第三步,查初始值。很多作业默认w和b从 0 开始,如果你从随机值开始,第一条损失曲线就对不上。确认初始值之后再看曲线趋势,比看某一个点的数值更有意义。
提示:不要为了对上答案反复微调代码。先确认「趋势对」——损失单调下降、最终参数接近、拟合直线形状相似。这三条满足,基本就是对的,数值上的一点点差异不用纠结。
5. 新手最容易踩的五个坑:报错、发散、对不上怎么排查
这些坑几乎每个人都会踩一遍,提前知道能省好几个小时。下面每一条都按「看到什么现象、为什么会这样、怎么解决」的顺序写,遇到问题时可以直接对照。
5.1 坑一:数组形状对不上,代码直接翻车
现象:报错operands could not be broadcast together with shapes (97,) (97,1),或者代码不报错但结果是个矩阵而不是数字。
原因:一维数组和二维列向量在 numpy 里是两种东西。(97,)是一维,参与运算时行为像行向量;(97,1)是二维,是明确的列向量。这两个一相乘,numpy 会尝试广播,结果变成一个(97,97)的矩阵,然后你的损失值就变成了一大坨。
解决:养成打印.shape的习惯,在所有函数入口处先确认形状。需要转成列向量时用X.reshape(-1, 1),需要压回一维用X.ravel()或X.flatten()。别用np.transpose去猜,直接写reshape意图最清楚。
5.2 坑二:学习率调大一点,损失函数反而飞了
现象:迭代几次之后损失变成nan或者inf,参数变成天文数字。
原因:步子迈太大,每次更新都越过最低点,而且越跳越远,这叫发散。很多新手看到损失下降慢,第一反应是把alpha从 0.01 改成 1,结果直接炸掉。
解决:先把alpha除以 10 再跑,看损失是不是稳定下降。下降太慢就乘 2 到 3 倍慢慢试探,不要一次跳一个数量级。同时确认你有没有对特征做归一化——没归一化的情况下,能用的alpha范围会非常窄,这也是下一坑的内容。
5.3 坑三:特征没归一化,梯度下降像在爬泥地
现象:代码没错,alpha也没错,但损失下降得极慢,迭代几千次还在高位,画出来的等高线图是一个又细又长的椭圆。
原因:不同特征的取值范围差太多,损失函数的等高线被拉成了狭长的形状,梯度方向几乎垂直于最优方向,所以只能一点点蹭过去。
解决:对每个特征做标准化,减去均值再除以标准差,让所有特征大致落在 -1 到 1 之间。做完之后原来的alpha可以放大很多倍,迭代次数也能降到原来的十分之一。这一步在后面的多变量练习里是必须的,早点形成习惯。
5.4 坑四:一出错就翻有答案版
现象:写完发现报错,第一反应是打开答案文件夹对照,看完抄一遍,报错没了,但下次遇到同类问题还是不会。
原因:报错是学习中信息密度最高的时刻,你在这个时刻看到的每一行提示都在告诉你「你的理解在哪里偏了」。直接看答案等于把这个时刻跳过了,只留下了「能跑」的结果。
解决:给自己定一条硬规矩——报错先读三遍错误信息,找到报错指向的行号,打印那一行涉及的所有变量和形状。二十分钟还找不到,再去看答案,而且只看卡住的那一个函数,看完关掉,凭记忆重写一遍。翻车的次数多了你会发现,真正让你进步的往往不是写对的那次,而是查了两小时才查出来的那次。
5.5 坑五:笔记抄成了板书,回头一句看不懂
现象:笔记记得满满当当,公式、定义、推导一样不少,但真卡住的时候翻一遍,发现没有一条能解决当前问题。
原因:把笔记当成了教材的复制品。教材已经写得很好了,抄一遍除了给自己一种「我很努力」的心理安慰,几乎没有别的用处。
解决:笔记只记三类东西——我踩过的坑、我想了很久才想通的点、我能用自己的话讲清楚的类比。每一条都写成「我当时以为……其实是……」的格式,因为这种格式天然带着上下文,三个月后你自己看得懂。至于公式和定义,直接标注教材页码就行,不用抄。
6. 学完之后往哪走:从跑通作业到自己写一个小项目
把这份作业从头到尾跑一遍,大概需要三到六周,取决于你每天能投入多少时间。跑完之后你手上会有一批能运行的函数:代价函数、梯度下降、正则化、前向传播。这些东西本身不值钱,值钱的是你知道了「一个模型从数据到结果中间要经过哪些环节」。接下来往哪走,比跑作业本身更需要想清楚。
我的建议是按下面的顺序推进,不要跳步:
| 阶段 | 做什么 | 判断可以往下走的标志 |
|---|---|---|
| 补完剩余练习 | 把正则化、神经网络前向传播、异常检测这几节做完 | 不看答案能独立写完核心函数 |
| 重写一遍 | 挑一个练习,从空白文件开始,不看任何参考 | 两小时内跑通,且结果与之前一致 |
| 换个数据 | 找一份自己的数据,套用同样的流程 | 能自己判断要不要归一化、学习率怎么选 |
| 用现成库对比 | 用 scikit-learn 的同类模型跑一遍,对比结果 | 能说清自己写的和库里的差在哪 |
第三行是最关键的转折点。很多人做完作业就停在那儿了,因为作业的数据是给定的、特征是给定的、连要预测什么都给定好了,你只需要填空。真正的门槛在于:拿到一份陌生数据,你得自己判断哪些列能当特征、要不要做缩放、用什么指标衡量结果。这件事没有标准答案,只能自己找一份数据折腾一遍。数据不用复杂,一份带数值列和标签列的表格就够了,比如某段时间的销量记录、某类设备的运行参数。
第四行的意义在于破除「手写才是真本事」的执念。手写一遍是为了理解内部发生了什么,理解之后就该用库了,因为真实工作里没人会手写梯度下降。用scikit-learn跑同样的数据,把系数、截距、损失值拿出来跟你手写的结果对比,如果基本一致,说明你真的搞懂了;如果差很多,正好是回去查漏的机会。
再往后,可以碰一碰神经网络。这部分作业里也有,但建议放在最后,因为它的调试体验最差——中间层没有直观解释,梯度不下降的时候你只能一层层打印,像个黑匣子。等你对前面那些流程足够熟了,有耐心一步步排查了,再去碰它。
最后说个自己的习惯。我刚开始学的时候,每次跑完一个练习就把代码一存,觉得「这个我做过」,然后心安理得地进入下一个。结果两个月后回头想改一个参数,发现连文件在哪都找不到,更别说代码里某一行为什么那么写了。从那以后我每次跑完一个练习,都强制走一遍三件事:在文件开头写三行注释说明这份代码干了什么、在哪几行卡过、参数为什么这么选;把跑出来的图存到output/里;在笔记里加一条「这次学到的一件事」。这套动作五分钟就能做完,但它决定了我三个月后还能不能捡起来。
希望帮到你。
本文还有配套的精品资源,点击获取