wine_project_lecture_simple.ipynb)总时长约 90–100 分钟。每段开头标了时间。【运行】 表示此时运行 notebook 里对应的单元格;【停顿提问】 表示停下来让学生回答;【板书】 表示可以写在白板上。
| 段落 | 内容 | 时间 |
|---|---|---|
| 开场 | 这节课做什么 | 3 min |
| 一 | 作业文件地图 + 读懂 winequality.py + 和 Claude 合作的 5
步套路 |
17 min |
| 二 | Task 1.1 热力图 | 20 min |
| 三 | Task 1.2 tSNE | 15 min |
| 四 | Task 2 最好/最差的酒 | 20 min |
| 五 | Task 3 勾兑 | 20 min |
| 六 | 提交清单与口试 | 5 min |
各位同学好。这份作业看起来题目很多、页面很多,但我先告诉大家一个好消息:它其实只讲一个故事——拿到一堆红酒数据,先看懂它(Task 1),再把一个打分模型当机器用,找最高分和最低分(Task 2),最后用这个机器帮酒庄调配比例(Task 3)。
今天我们只做四件事。第一,认识作业里的每个文件;第二,每道题在问什么、怎么拆、怎么跟 Claude 说;第三,对应的写作题怎么写;第四,每一行代码是什么意思。
最后一点我要特别强调:这份作业允许并鼓励你们用 Claude,但最后有口试,缺席是零分,而且考官会问你代码里每一行的意思。所以今天我们不光看”怎么写出来”,更要看”怎么读懂”。
好,打开 notebook,先运行第一个代码格,把需要的库都装进来。【运行第 1 格】看到”环境就绪”就可以了。
我们先看这张表。【指向 notebook 第一部分的表格】
作业给你的文件分三类。
第一类,工具箱:winequality.py。
里面有三个函数:读数据、训练模型、给一款酒打分。规则只有一条:这三个函数不能改,但你可以往里面加新的函数。
第二类,数据,三个 CSV。 一个是全部数据,1359 款酒;一个叫 TRAIN,1087 款,模型只学这些;一个叫 TEST,272 款,模型没见过。大家记住这个比喻:TRAIN 是练习册,TEST 是考试卷。 模型把练习册的答案背熟了,所以拿练习册测它,成绩会虚高。Task 3 要求你只能用 TEST 里的酒,就是这个原因,我们到 Task 3 再用数据证明。
第三类,你要写的三个文件:task1_exploration.py、task2_theoreticalwine.py、task3_blending.py。
另外,网页上还有 4 个写作页和 3 个代码提交页。每个任务的分数,写作占一半,代码占一半。
【运行第 2 格】这是我的文件夹里的文件,和你们的应该一样。
【运行第 3 格】这里把三份数据读进来。大家看形状:全部是 1359 行 10
列,训练 1087 行,测试 272 行,1087 加 272 正好是
1359。再看表:一行是一款酒,前 9 列是化学指标——酸、糖、酒精这些;最后一列
quality 是专家评分,3 到 8 分。
还有一个细节:原始数据里有 pH 和游离二氧化硫,这份作业把它们删掉了。原因是勾兑的时候它们不能简单平均,这个我们在 Task 3 会讲。
winequality.py(7 min)下面我们逐行读这个工具箱,因为后面所有任务都靠它。
先看 build_model,也就是训练模型。【指向 notebook
里的代码块,逐行读】
第一行,pd.read_csv,读训练文件,得到一张表。第二行,drop(columns=[TARGET]),把
quality 这一列去掉,剩下 9
列就是”题目”,也就是指标。第三行,只取 quality
这一列,这就是”答案”,专家评分。第四行,建一个随机森林,300
棵树,random_state=0
的意思是固定随机性,每次运行结果一样。第五行
fit,让森林学习:从指标推出评分。最后一行,把学好的模型交出去。
再看
predict。你给它一个字典,键是指标名,值是数字。它做三件事:第一,拿到模型训练时的
9
个指标名,顺序是固定的;第二,检查你有没有少给或多给;第三,按模型要的顺序把你给的数值排成一行,做成一张只有一行的表,让森林预测,取出结果。一句话:predict(模型, {指标名: 数值}),返回这款酒的预测评分。
【运行第 4 格】我取 TEST 里第一款酒,变成字典,让模型预测。模型说 5.9,专家真实评分是 7。大家看,模型也会错,它只是一个近似。
什么是随机森林?最直观的说法:300 位品酒师投票。每棵树就是一位品酒师,各自打分,最后取平均。
【运行第 5 格】这张图是这 300 位品酒师对同一款酒打的分。大家看红色的柱子,有人打 5 分,有人打 6 分,黑线是平均值 5.9,这就是森林的答案。
请记住两个后果,后面的任务反复用到。
【板书】 1. 平均数不会超过最大值 → 模型打的分永远不会超过训练集里的最高分 8。 2. 模型是”投票表决”,不是一条光滑的曲线 → 没法沿着坡往上爬。
最后给大家一个通用方法,每个任务都用。【指向 notebook 的表】
第一步,目标:告诉它要写哪个函数、做什么。第二步,输入输出:参数是什么,返回什么,存成什么文件。第三步,约束:用哪些库、函数名必须一致、哪些东西不能改。第四步,分步:一次只做一小步,先能跑再加功能。第五步,自测加理解:让它说明怎么验证,然后你自己读懂,并手改一处。
写作题第二问要交的提示词,就是你在这五步里真实发给 Claude 的话。“手改了什么”,就是第五步。注意:这些必须是你自己真实用过的,不能编。
大白话:9 个化学指标加评分,谁和谁一起涨一起跌? 把它画成一张彩色表,叫热力图,并在两边画”家族树”,把相似的指标排在一起。然后回答三个问题:哪 3 个指标和评分最相关?哪些指标成对出现?它们是不是重复的信息?
产出是两个函数:correlation_matrix() 和
plot_heatmap(),还有一张图
task1_heatmap.png。
拆成六小步。【指向 notebook】读数据,要保留列名;两两算相关系数;把”相关”变成”距离”;层次聚类得到家族树;按家族树的顺序重排表格;最后画图。
这六步,就是写作 Q1 的答案骨架。
对 Claude 怎么说?示范是分三轮。第一轮,只让它写
correlation_matrix,先别画图,打印和 quality
的相关,确认对了。第二轮,再写 plot_heatmap,告诉它用 1
减绝对值 r 做距离,色阶固定在负 1 到正 1,不要用 seaborn。第三轮是修
bug:标签被裁掉了,左边的树状图顺序对不上。
大家注意:真实的过程往往就是这样一轮一轮改出来的,把这个过程如实写进 Q2,是加分项。
【运行第 6 格】相关系数 r,就是”一起涨一起跌的程度”。左边 r 接近正 0.9,两个量一起涨;中间接近 0,没关系;右边接近负 0.9,一个涨一个跌。
【运行第 7 格,逐行讲】
第一行,load_frame 读表,1359 行 10
列。第二行,打印形状。第三行
df.corr(method="pearson"),两两列计算皮尔逊相关系数,得到
10 乘 10 的”相关表”。第四行,只看和 quality 的相关,从小到大排。
结果大家看:酒精度正 0.48,挥发酸负 0.40,硫酸盐正 0.25。这三个就是题目第一问的答案。
要提醒:相关不等于因果,而且 r 只衡量直线关系。
【运行第 8 格,逐行讲】
第一行,取绝对值,用 1 减 |r| 当距离。为什么要绝对值?因为我们关心的是”两个指标是不是在说同一件事”,不管方向。强负相关也算近。
第二行,和自己的转置取平均,消除浮点小误差,保证左右对称。第三行,对角线填
0,因为每个指标和自己的距离是 0。第四行,squareform
把方阵压成一维的上三角列表,因为下一行的 linkage
要这种格式。
第五行是核心:linkage
做层次聚类。每次把最近的两堆合并,average
的意思是两堆之间所有点对距离的平均。最后一行,只取”叶子顺序”,也就是谁排在谁旁边,先不画图。
打印出来的顺序大家看,酒精度和质量挨着,固定酸、密度挨着,挥发酸和柠檬酸挨着。
【停顿提问】 为什么用 1 减 |r|,不用 1 减 r?
【运行第 9 格】看这个例子:挥发酸和柠檬酸 r 是负 0.55。如果用 1 减 r,距离是 1.55,被当成”很远”;用 1 减 |r|,距离是 0.45,被当成”较近”。它们高度反相关,说明信息有重叠,所以应该靠近。
【运行第 10 格】这一块代码比较长,但结构很简单:在一张画布上划三块区域——左边放树状图,上边放树状图,中间放热力图。
我挑关键的几行讲。fig.add_axes([左, 下, 宽, 高]),四个数都是画布的比例,用来”划区域”。dendrogram(..., orientation="left")
在左边区域画朝左的树;invert_yaxis()
是上下翻转,让第一个叶子在最上面,和矩阵第一行对齐——这是个容易漏掉的细节。
然后是最关键的一行:corr.iloc[leaves, leaves],按叶子顺序重排行和列。如果不重排,树状图和色块就对不上,图是错的。
imshow
画色块,cmap="coolwarm",红是正相关,蓝是负相关,vmin=-1, vmax=1
把范围固定,这样 0 永远是中性色。后面的
set_xticks、set_xticklabels
是给坐标轴写指标名;双层循环是在每个格子里写数字;最后加色条。
怎么读这张图?红色是一起涨,蓝色是一涨一跌,树状图里连得越低的两个指标越像。
【指向 notebook 的表】三问,字数是 800、2000、1500。
Q1 功能分解,不超过 800 字符。 写法:编号,每步一句,点出函数名。就用刚才那六步。
Q2 提示词,不超过 2000。 贴你真实发给 Claude 的话,两三轮,再加你手改了什么。手改要具体,比如”我把距离从 1 减 r 改成 1 减 |r|,因为我想让强负相关的指标也聚到一起”。不要写”我改了一些代码”。
Q3 讨论,不超过 1500。 回答题目的三个子问。第一问,前三名:酒精度 +0.48、挥发酸 −0.40、硫酸盐 +0.25。第二问,最强的特征对:固定酸和密度 0.67,固定酸和柠檬酸 0.67,挥发酸和柠檬酸 −0.55;几乎不相关的是残糖和质量,0.01。第三问,是不是冗余?部分重叠,但不能直接删:没有一对接近正负 1,最大才 0.67;而且最强的单个指标酒精度,r 平方也只有约 0.23,也就是只解释了 23% 的评分差异——质量是多个因素共同决定的。
【运行第 11 格】这是示范答案和字数计数器。三问都没有超限。注意,这是示范,请用你们自己的话和自己的提示词。
一款酒有 9 个数字,人没办法在脑子里”看”9 维空间。tSNE 像一位摆桌子的人:把 1359 款酒摆到一张平面桌子上,相似的酒挨着放。然后按评分上色,看看好酒是不是聚在一起。
产出:plot_tsne() 和
task1_tsne.png。拆成五步:读数据并把评分单独拿出来,标准化,tSNE
降到 2 维,按评分分颜色画图,保存。
对 Claude 怎么说:告诉它函数名、perplexity 默认
60、先标准化、init='pca'、random_state=0、每个评分一种颜色带图例,保存文件名。
【运行第 12 格】大家看这张表,每一列的标准差。总二氧化硫是 33,而密度是 0.0019。最大和最小相差一万七千多倍。
如果不标准化,总二氧化硫这种大数字会淹没氯化物这种小数字,tSNE 几乎只看它一个指标。标准化就是让每个指标”同等重要”。
【运行第 13 格,逐行讲】
前三行:读表;把评分单独取出来,稍后上色;去掉评分,剩下 9 个指标作为输入。
第四行
StandardScaler().fit_transform,标准化:每列减平均值,除标准差。
第五行设置 tSNE:n_components=2,降到 2
维;perplexity=60,可以理解为”每个点大约参考多少个邻居”;init="pca",用
PCA
给一个好的起点,更稳定;random_state=0,固定随机种子,每次画出同一张图。
下一行 fit_transform 才是真正计算,得到 1359 行 2
列的坐标,也就是每款酒在桌子上的位置。
然后画图。np.unique(quality) 得到 3 到 8
这几个评分;RdYlGn 从红到绿取 6
种颜色。循环里,pick = quality == level
得到一个真假数组,表示哪些酒是这个评分;然后用这些酒的坐标画点;label
里写评分和酒的数量,这样图例会带样本数。
右边以 7、8 分,也就是绿色为主;左边偏低分。说明评分和化学指标确实有规律,所以模型能学会。5 分和 6 分混在一起,它们占绝大多数,而且品酒本身有主观性。3 分只有 10 款,8 分只有 17 款,所以点很稀疏。
最重要的提醒:只有”谁挨着谁”可信。 轴上的数值、两个团之间的距离、团的大小,都不能解读。
【运行第 14 格】左边 perplexity 是 5:碎成很多小岛;右边是 100:更平滑、更看全局;中间 60 比较平衡。作业里用 60,并在讨论里说明你的选择。
Q1 只有 600 字符,比 1.1 更短,五步编号就够。Q2 一样贴真实提示词,手改写具体,比如”我把 perplexity 从默认 30 改成 60,因为样本较多”。Q3 写三件事:图里看到的结构,用数字;perplexity 怎么选的;局限——轴无意义、类别不均衡。【运行第 15 格】看示范和计数。
模型是个”打分机器”:输入 9 个数字,输出评分。现在反过来问:给什么样的 9 个数字,机器会打出最高分?最低分?最高能打多少?这两款酒在 tSNE 图上落在哪里?
打个比方:想象一座看不见的山,山的位置是 9 个指标的组合,山的高度是模型的评分。我们要找最高峰,也就是 perfect,和最低谷,也就是 awful。
但这座山是阶梯状的——还记得吗,随机森林是投票表决,没有平滑的坡可以爬。所以我们派一群登山者随机散开,每一轮留下爬得更高的人,这个方法叫差分进化。
六步。第一,定范围:每个指标只能在训练数据的最小到最大之间取值。第二,写目标:给一个配方,模型打分。第三,选方法:模型不可导,所以用差分进化。第四,最大化得到
perfect,最小化得到 awful。第五,返回配方,并用官方
predict() 再算一次分数。第六,把两款酒放到
tSNE 图上,和真实数据一起嵌入。
对 Claude 怎么说:告诉它有一个随机森林和 predict,要写
perfect 和 awful,用 scipy 的
differential_evolution,在训练集范围内搜索,返回字典和分数,不要改已有函数。追问:搜索太慢,让整批配方一次预测——这就是我们加
predict_batch 的原因。
【运行第 16 格】其他指标取中位数,只改酒精度。训练集里酒精度范围是 8.4 到 14.0。酒精度 15、20、100 的时候,预测都是 5.700,一动不动。
意思是:超出范围,模型只会”复制边缘值”。如果不加范围,优化器会跑到酒精度 100 这种荒谬的点去。
【运行第 17 格,逐行讲】
函数
search_teach(model, maximise)。第一行读训练集,因为要用它的取值范围当边界。第二行,拿模型要的
9 个指标名。第三行,bounds 是一个列表,每个元素是某个指标的
(最小值, 最大值)。
第四行有个小技巧:优化器只会”找最小”,所以想找最大,就把分数取负。sign
就是这个符号:要最大化,取 −1;要最小化,取 +1。
然后定义目标函数
objective。它拿到一批配方,返回”要最小化的数”。这里
x 的形状是 9 行、种群数列,所以要转置成种群数行、9
列,再一次性交给 predict_batch。
接着是 differential_evolution
的各个参数,我挨个说:vectorized=True,告诉它目标函数一次吃一批;updating="deferred",配合向量化,整代一起更新;popsize=30,种群大小是
30 乘 9 个”登山者”;maxiter=300,最多迭代 300
代;tol=1e-10,收敛判据很严,尽量多搜一会;seed=0,固定随机种子;polish=False,不做收尾的局部微调,因为函数是阶梯状的,微调没用。
最后两行:把最优向量还原成”指标名到数值”的字典,然后用官方
predict() 再算一次分数再返回。
下面运行出来,大家看结果表。
perfect 酒精度 13、挥发酸 0.41;awful 酒精度
8.4、挥发酸 1.57。方向和 Task 1 的相关结果一致。
最高预测约 7.73,最低约 3.57。 到不了 8,更到不了 10,因为森林输出是训练评分的平均。
而且要提醒:这是模型眼中的最高分,不是现实里的上限;这些配方也可能是”现实中不存在的组合”。
【停顿提问】 为什么最高分到不了 10?
【运行第 18 格】两颗星是这两款酒。注意一个技术点:tSNE 不能”后加点”,所以必须把这两款酒和真实数据一起算。
请大家自己看图回答:星落在高分区还是低分区?是否远离真实数据?这就是题目问的”它们在 tSNE 图上的位置”。
Q1 800 字符,要求”拆子问题,并提出方法”,所以每一步要点出算法——差分进化、边界、批量预测。Q2 只有 1000 字符,最短,贴最关键的一两条提示和一句手改。Q3 写三件事:最高最低分是多少;它们在 tSNE 的位置;局限。【运行第 19 格】看示范。示范里 tSNE 位置那句我故意留了方括号,要你们看着自己的图写。
你是酒庄主,有一款自己的酒,想从一批候选酒里挑几款按比例混进去,让混合后的预测评分最高。写函数
choose_candidates,回答:挑哪几款?各放多少?
打个比方:调奶茶。自己的茶底 60%,别人的茶 40%,味道就是按比例的平均。所以才把 pH 删了,它们不能简单平均;剩下 9 个指标可以按比例平均。
【运行第 20 格】这是我们说好要用数据证明的。左边是 TRAIN 的酒,模型见过,平均误差很小;右边是 TEST 的酒,没见过,误差大约是三倍。
结论:如果用 TRAIN 的酒当候选,模型”背过答案”,预测会偏准,勾兑的提升就是虚高的。这就是数据泄漏,也是口试很可能问的问题。
六步。第一,勾兑规则:混合酒指标等于各酒指标按比例加权平均。第二,一个方案等于选一款候选加一个比例,算混合后的预测分。第三,把”所有候选乘所有比例”的方案一次交给模型评分。第四,选分数最高的方案,更新”当前这锅酒”。第五,重复,直到没有提升,或者已经加够 3 款。第六,返回混合酒、用到的酒和各自比例,比例和为 1。
对 Claude 怎么说:告诉它函数签名、加权平均、贪心搜索、每轮枚举 5% 到 95% 的比例、最多加 3 款、返回什么、候选只能来自 TEST。追问:逐个调用太慢,一次预测整批。
【运行第 21 格】这个迷你例子是理解后面代码的关键。
先看加权平均:60% 的 A 加 40% 的 B,酒精度手算是 0.6 乘 10 加 0.4 乘 14,等于 11.6,代码算出来也是 11.6。
再看”一批方案怎么一次算”:3 款候选,4 个比例,2
个指标,mixes 的形状是 3、4、2,也就是 12
个混合方案,一次生成。表里是第一款候选在 10%、20%、30%、40%
时的混合酒,酒精度一路从 10.4 涨到 11.6。
那行”广播”代码怎么读?[None, :, None]
是在需要的位置插一个空维度,让 numpy
自动对齐,一次算完所有组合。你不需要背,只要知道:它是把”候选乘比例”全部组合,一次性做完。
【运行第 22 格,逐行讲】
函数 choose_teach。前几行准备:取 9
个指标名;候选酒转成表格,只留 9 列;current
是”当前这锅酒”的配方,一开始就是自己的酒;ratios
是可以试的比例,0.05 到 0.95;used
记录选中的候选,shares 记录各酒占比,第一个是自己的酒,起初
100%;best_q 是当前这锅酒的预测分。
进入循环,最多加 3
款。第一步,生成所有”候选乘比例”的混合方案,就是刚才那个广播。第二步,压平成”方案数乘
9”,一次评分。第三步,还原成”候选数乘比例数”的分数表。第四步,argmax
找分数表里最高分的位置,unravel_index 把它变成”第 i
款候选、第 j 个比例”。
然后判断:如果最高分也没比当前好,就
break,停。否则:取出比例 r;更新最好分数;把
current
换成刚才那个混合方案;更新占比——老酒的占比都乘 (1 减
r),再加上新酒的占比 r。这是最容易写错的地方:比如先加 10%,再加
40%,第一款的实际占比是 0.1 乘 0.6,等于 6%。
循环结束后,如果同一款被选了两次,把占比合并。最后返回混合酒、选中的编号、各酒占比,自己的酒在最前,和为 1。
【运行第 23 格】演示:我取 TEST 里评分最低的酒当”我的酒”,预测 4.50;混合后预测 7.54。占比:自己的酒只剩 6.7%,另外三款分别是 38.3%、45%、10%,合计正好是 1。下面一行是作业成品文件算的结果,也是 7.54,两者一致。
【运行第 24 格】饼图是配方,柱状图是混合前后的对比。
局限有四点,讨论题可以直接用。第一,贪心不保证最优,每步只看当前最好,可能错过”两款一起混才好”的组合。第二,自己的酒可能只剩很小比例,现实中酒庄想保留特色,可以加”自己的酒不少于 X%“的约束。第三,加权平均只是近似。第四,这是预测分,不是真实品鉴分。
写作:Q1 拆六步,点出”加权平均、贪心、批量评分、只用 TEST”;Q2 真实提示词加手改,比如”我加了比例和为 1 的检查”;Q3 写结果——几分到几分、各占多少——加上为什么只用 TEST,再加局限。【运行第 25 格】示范和计数。
最后看清单。【指向 notebook 最后一部分】
提交前请逐条检查:每个代码提交页上传对应的
.py、改过的
winequality.py、需要的 CSV,并在 Ed
里实际运行一遍;winequality.py
里那三个原函数没被改;输入和候选酒只来自
TEST;写作的第二问是你自己真实发给 Claude
的提示词和你自己的手改;每问字数没超限;截止是 10 月 18 日晚上 11
点 55 分,硬截止,没有部分分。
最后是口试自测。我念问题,大家先想,再点开看答案。
为什么聚类用 1 减 |r|?为什么要重排行列?tSNE 为什么要标准化?tSNE 图上两个团离得远能说明什么?为什么不用”爬坡”法找最高分?为什么要给范围,最高分为什么到不了 10?为什么只能用 TEST 里的酒?为什么勾兑能用加权平均?你的方法有什么缺点?
给大家一个口试策略:不知道就说”我的理解是……,但不确定”,比硬编强得多。能主动指出自己方法的局限,比给出”完美答案”更受认可。
今天就到这里。回去之后,请大家按这个顺序做:先自己用 Claude 一步一步写,把真实的提示词存下来;然后对照今天的逐行讲解,把每一行都读懂;最后用字数计数器检查写作,再提交。谢谢大家。