AI 工程地基 30|好特征值一千个数据点
调了一周超参数,模型还是那个模型。换了个更高级的算法,成绩纹丝不动。这时候有人把原始数据换了种表示,一个最朴素的逻辑回归反超了你。
这不是段子,是经典机器学习里每天都在发生的事。数据的表示方式,往往比算法的选择更重要。一个拿「面积、卧室数」当输入的房价模型,能打赢拿「原始地址字符串」当输入的模型,不管后者的学习器多精巧。算法只能吃到你喂给它的东西。
把原始数据变成模型容易消化的形状,这一步叫特征工程。今天把整条流水线从零走一遍:数值怎么变换、缺失怎么补、类别怎么编码、文本怎么变成向量、多余的特征怎么删。每一步都能跑,每一步都踩过坑。
先说变换
原始数字很少能直接喂模型。最常见的问题是量纲:面积以千计,卧室数以个位计。KNN 和 SVM 这类靠距离吃饭的算法会只看面积、无视卧室——不是它们偏心,是数量级差太远。所以要么把所有特征压进 0 到 1(min-max 缩放),要么变成均值 0、方差 1(z-score 标准化)。
这一课的示例数据是两百套假想房:面积从 500 到 5000 平方英尺,房龄从 0 到 50 年。有个细节值得看一眼:课程的演示矩阵里,面积用了 z-score,房龄用了 min-max,两种刻度混在同一份数据里。课程正文自己说「让特征处于同一范围」,自己的例子却是两种范围并存。真要喂给距离模型,这本身就是个隐患。
对数变换是另一味药,治的是右偏分布。收入、人口、词频,这些量的大部分挤在低端,少数极大值拖出一条长尾。取个对数,乘法关系变成加法关系,长尾被压平。但药要对症:这一课的面积是从 500 到 5000 的均匀分布,本来就不偏。硬上对数,反而把它拧成了左偏(偏度从 0.02 变成 -0.64)。我拿一份真正的右偏数据(对数正态分布,偏度 5.26)试过,取对数后偏度变成 0.01,干干净净。有意思的是,课程的演示代码里算出了 log 变换后的面积,然后这列数字就再也没被用过——药开了,病人没这个病。
分箱呢?把连续的房龄切成 0-10 年、10-20 年这样的档位。它适合一种特定情况:特征和目标的关系是阶梯状的,不是平滑的。房龄对这套数据里的房价是线性关系(每年折旧一千美元),这时候分箱就是纯损失。我量化了一下:拿「房龄预测是否高价」的信息量做标尺,连续房龄是 0.036,切成 5 档后只剩 0.007——近八成的信息被切没了。分箱是拿信息换简单,这笔买卖只在阶梯关系上才划算。
缺失值:补,但有代价
真实数据有洞。删行最省事,但只在缺失稀少且随机时可用。更常见的是填充:数值列填中位数(比均值抗离群点),类别列填众数。
填充有代价,值得亲手量一次。这套数据里房龄缺了 20 个值,用均值 25.2 补上。补完后整列的方差缩了 10%,而且 20 个完全相同的值在分布正中间堆出一个尖峰——原来它们各自是不同的数字,现在它们是同一个数字。模型会以为「25.2 岁」的房特别多。
课程还教了一招:在填充之前,加一列「这里原来缺吗」的指示列。理由很对——数据缺失这件事本身可以携带信息,传感器坏在高温天,缺失就跟故障相关。但这一步恰好在这份数据上演示不出来:这里的缺失是纯随机的(生成时就是掷骰子决定的)。我把指示列跟「是否高价」算了一遍信息量,恰好是 0.0000。指示列加进去了、算出来了、结果是零。这句话不是课程错了,是这份数据的缺失没有故事。真实世界里,先问一句「缺失有没有原因」再决定要不要这列。
类别:三种编码,三种性格
模型要数字,类别得翻译。One-hot 最直白:三个社区变三列,是哪列哪列为 1。缺点是类别一多就爆炸,一千个类目就是一千列。
Label 编码把类别变整数:市区=0、远郊=1、近郊=2。问题在于整数天然带顺序,模型会以为近郊大于远郊大于市区——这个顺序是字典序给的,不是世界给的。课程说它「只适合树模型」,这句其实漏了半个正当用途:当类别本身就有序的时候,label 编码是正确答案。这一课自己的 age 分箱(0 到 4 档)就是有序量,0 档确实比 4 档新。
第三种最有意思:target 编码。用每个社区的平均房价替换社区名。这是把「答案」借来当特征,效果常常很好,但坑也真多。课程警告了最出名的那个:不能用全量数据算均值,否则测试集的答案漏进了训练特征,成绩是虚的。必须只在训练集上算。
但还有一个坑,课程没提,而且恰好被它自己的演示数据踩中了。这套数据里三个社区的真实价差是清楚的:市中心加 5 万、近郊加 1 万、远郊不加。两百个样本算出来的编码呢?市中心 22.8 万——对。但远郊 19.0 万,近郊 18.8 万——反了。近郊应该比远郊贵 1 万,编码里倒过来贵了 2000 多。
为什么?每个社区约 67 个样本,组内房价的抽样波动大约 9000——跟组间那 1 万的真实差距是同一个量级。一次抽样,就足以把小差距抹平甚至翻转。我把样本加到两万重跑,顺序恢复正常:市中心 23.3 万、近郊 19.2 万、远郊 17.9 万。target 编码对小组噪声很敏感,类别多、每类样本少的高基数场景里,它给出的排序未必可信。教科书警告你防泄漏,还要多防一手这个。
顺带一提它的平滑机制:编码公式里,本组均值和全局均值按样本量加权。只有 1 个样本的类别,编码里 91% 是全局均值——稀有类别几乎拿不到自己的信号。这是防过拟合的代价,用的时候心里要有数。
文本:数词,然后给词称重
文本变向量的第一步是数词。第二步是 TF-IDF:一个词在本文档出现得多、在其他文档出现得少,权重就高。拿"large modern house with pool"这句话来说,"house"只在 5 篇文档中的 1 篇出现,权重 0.32;"with"出现在 3 篇里,权重只有 0.10。停用词自动靠边站,不用维护词表。
两个值得知道的事。第一,这门课的公式算出来,一个词如果在所有文档都出现,权重恰好归零——区分度为零的词不算数,这是这套公式干脆的地方。第二,拿手写版去对拍 scikit-learn 会吓一跳:"house" 的权重,手写 0.32,库算 2.61,差 8 倍。两边都没错,是三处约定不同:sklearn 的 IDF 末尾多加 1 保底,TF 不除文档长度,默认还做一层归一化。公式和实现一致只是及格线,实现和生态的约定谁跟谁对齐,才是对拍时真正要问的问题。
多项式特征:一个收了参数不干活的函数
课程的 polynomial_features 函数签名写着 degree=2。我传 3、传 5 试了试,返回的列表和 degree=2 一模一样——只有平方项和交叉项,没有三次项。参数收下了,活没干。要是真需要 x³,这个函数会静默地给你一堆平方项。用第三方代码前,拿非默认参数跑一遍,是最低成本的体检。
特征选择:两个过滤器,零工作量
特征多不总是好事。十个好特征,通常打赢十个好特征加九十个噪声特征——噪声给了模型在训练集上「记住巧合」的机会。
课程实现了两种过滤器,然后在自家数据上演示。结果很戏剧:方差阈值过滤器,7 列进,7 列出。相关过滤器,7 列进,7 列出。一个都没删。
先说方差过滤器为什么删不动。这一列列特征里,面积已经过了 z-score——z-score 的定义就是把方差设成 1。用方差当判据的过滤器,遇到一个方差被人为统一成 1 的矩阵,0.01 的阈值连一根毫毛都摸不到。方差过滤的本意是抓「几乎不变化的列」,它该站在原始刻度上干活。我喂了一列 99% 都是同一个值的数据进去,方差 0.00025,它立刻正确地删掉了——过滤器没坏,是摆放的位置不对。
相关过滤器同理。七列特征里,彼此相关性最高的一对也只有 0.5 左右(one-hot 列之间天然的负相关),够不着 0.9 的删除线。真实数据里最常见的冗余是什么样?同一个量,两种算法各算一遍。我把「面积」和「log(面积+1)」两列一起喂进去:相关系数 0.97,过滤器一删一个准。这份数据干净得没有给过滤器留猎物。
所以这一节的演示输出虽然印着「7 features kept」,真正干了活的反而是旁边那张信息量表:面积对「是否高价」的信息量 0.3886,断层第一;房龄 0.036,社区三列全在 0.016 以下,缺失指示列约等于零。想看特征选择真正动手的样子,把「面积」和「log(面积)」一起喂给相关过滤器,它会在 0.9 的阈值上准确地把后者删掉——一列不多,一列不少。
练习:把这一课的 target 编码改成 leave-one-out 版本(每行的编码用排除自己之后的均值算),在训练集和测试集上分别对比它和朴素版的表现。两万样本才能让三个社区的排序稳定,那么一百个类别、每类两三个样本的场景,你会看到什么?
原文:Feature Engineering & Selection,ai-engineering-from-scratch(Rohit Ghumare,MIT),Phase 2 Lesson 8。
讨论回复
加载中...正在加载回复...
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。