ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

玻璃成分分类判定:九特征建模与误判排查

玻璃成分分类判定:九特征建模与误判排查

玻璃这东西,看着都是一个样,透明、冰凉、敲起来叮当响。可一旦把它丢进回收产线、质检工位或者材料溯源流程里,它到底是建筑门窗用的浮法玻璃、汽车车窗玻璃、瓶子罐子、餐具器皿,还是车灯罩,判定错一步,后面整批料的去向就全错了。我最近把“根据玻璃的特征对玻璃进行类别判定”这件事从头到尾跑了一遍,用的不是摄像头加视觉模型,而是一张纯数值表:一个折射率加上七八种氧化物的质量百分比。听起来有点朴素,但成分数据的好处是它不讲情面,谁含镁多、谁含钾多、谁掺了钡,数字摆在那儿。这篇我把整个判定流程摊开讲清楚:七类玻璃靠什么区分、九个特征各自在说什么、清洗和构造特征时哪些动作是必需的、模型怎么选、类别极不均衡时评估口径怎么定,以及误判高发区在哪、怎么排查。适合刚接触成分分类的读者照着复现,也适合已经跑通过一遍但模型指标卡住的人对照找问题。

1. 先想清楚要判什么:七类玻璃与成分特征的对应关系

1.1 判定目标不是“玻璃长什么样”,而是“它走的是哪条工艺路线”

刚拿到这个题目的人,第一反应往往是往图像方向走:拍张照、做个纹理分析、跑个卷积网络。我试过这条路,很快放弃了。玻璃在成品状态下视觉差异极小,同一批瓶子换个模具就长得完全不一样,靠外观特征去区分“浮法玻璃”和“非浮法玻璃”基本是缘木求鱼。真正有判别力的信息藏在化学成分里,因为每一类玻璃的配方是被工艺要求和经济性硬约束住的,配方差异会直接体现在氧化物比例上。

所以我们判定的对象不是玻璃的外观,而是它的工艺身份与用途身份。业内常用的一个玻璃成分样本集大致覆盖七类:建筑用浮法玻璃、建筑用非浮法玻璃、车辆用浮法玻璃、车辆用非浮法玻璃、容器玻璃(瓶罐类)、餐具玻璃、车头灯玻璃。这七类两两之间不是等距的——“建筑浮法”和“建筑非浮法”的差异主要来自成型方式,“建筑浮法”和“车辆浮法”的差异则来自原料纯度与铁含量控制。搞清楚这种“层级式”的类别结构,比闷头调参重要得多。

我个人的判断是:这个任务的本质是一个小样本、多类别、类别不均衡、特征量纲不一致的表格分类问题。四个定语缺一不可,后面所有的方案选择都得围绕这四条来,谁忽略了其中一条,模型上线就会在某一类上系统性翻车。

1.2 九个特征各自在说什么,为什么它们有判别力

特征一共九个:折射率 RI,以及 SiO₂、Na₂O、MgO、Al₂O₃、K₂O、CaO、BaO、Fe₂O₃ 八种氧化物的质量百分比。单看名字很枯燥,但每一个背后都对应着明确的配方逻辑,理解了这层逻辑,后面构造特征和解释误判才有依据。

特征在玻璃里的角色主要的判别指向
RI(折射率)成分的综合光学结果,与密度、CaO、BaO、PbO 正相关整体“密度档位”,辅助区分容器玻璃与建筑玻璃
SiO₂网络形成体,占七成左右,是骨架本身判别力弱,主要看它被谁挤占
Na₂O助熔剂,降低熔化温度容器玻璃、餐具玻璃偏高;浮法玻璃控制得较稳
MgO由白云石引入,浮法工艺常用浮法与非浮法的核心分水岭,非浮法常见接近 0
Al₂O₃提高化学稳定性与耐水性非浮法、容器玻璃偏高;浮法玻璃一般压得较低
K₂O助熔兼“拉长料性”餐具玻璃的最强信号,手工成型需要更长的操作窗口
CaO稳定剂,来源是石灰石容器玻璃偏高,与 RI 联动明显
BaO提高折射率与光泽车头灯玻璃几乎绕不开的标记,其他类多为 0
Fe₂O₃杂质着色,控制严格度反映原料档次车辆浮法玻璃压得最低,非浮法偏高

把这张表读透,你会发现一个有意思的事实:最强的判别信号不是绝对值,而是“有还是没有”。MgO 是不是接近零、BaO 是不是存在,这两个近乎二值的判断就能把七类里的一大半先劈开。很多人在这一步就直接把原始特征喂给模型,然后抱怨准确率上不去,问题往往是他没把这种“存在性”显式表达出来。

1.3 数据到手的第一件事:把类别分布摊在桌面上看

我见过太多人拿到数据先head()看一眼就开跑模型,跑到最后发现某一类的召回率是 0,回头一查才知道那一类总共只有个位数样本。这个数据集的类别分布非常不客气:占比最大的一类接近总量的三分之一,而最小的两类样本数只有个位数到十几个,最大类与最小类的比例超过 7:1。

这件事的连锁反应很直接:如果按普通随机划分,测试集里最小类可能只有一两个样本,一次误判就让该类召回率直接砍半,指标波动大到没法做决策。所以我在动手建模前一定会做三件事——统计每类样本数与占比、检查每类的特征分布是否有明显离群、确认最小类在分层抽样后每折还剩几个样本。如果分层后某折里某类只剩一个样本,那这一折的交叉验证结果就没有参考价值,得考虑合并或改用其他验证策略。

提示:类别数多于五个且最小类样本不足二十条时,先别急着建模,先确定评估口径和验证策略。这两件事定不下来,后面所有指标都是幻觉。

2. 特征工程:把材料常识翻译成模型能吃的数字

2.1 三个必须先做的清洗动作

清洗这一步没什么炫技空间,但漏掉任何一项都会在后面放大成难以解释的误差。我固定做三件事。

第一件是量纲与量级检查。RI 在 1.5 附近,SiO₂ 在 70 附近,Fe₂O₃ 常常是 0.05 这种量级,如果直接用距离类模型(KNN、SVM 的 RBF 核),SiO₂ 会单方面主导距离计算,Fe₂O₃ 的信息几乎被抹平。我的做法是先看每列的取值范围,凡是最大值与最小值跨两个数量级以上的,都列入待缩放名单。

第二件是零值与缺失的区分。这个数据集里 0 不代表缺失,它代表“这个成分没有刻意添加”或者“低于检出限”。比如某类玻璃的 BaO 列大量为 0,那是真实的配方事实,绝对不能拿均值或中位数去填。我一般会统计每列 0 值的比例,如果某列 0 值占比超过六成,就把它当作“存在性标记”来对待,而不是连续变量。

第三件是重复样本与近重复样本排查。成分数据容易因为录入或取样重复出现完全相同的行。我习惯先按全部特征做一次完全去重,再用四舍五入到两位小数后的特征做一次近似去重,看看能压缩掉多少行。这一步不做,交叉验证时同一条样本可能同时出现在训练折和验证折里,指标会虚高,上线后一落千丈。

2.2 用比值和差值把“隐含的判别规则”显式化

原始九个特征喂进去,树模型大概能跑到七八十分的水平,但想再往上走,靠的是把化学常识变成新列。我实测下来最有效的几个构造方向,都是围绕前面那张表里的“分水岭”来的。

第一个是镁铝关系。浮法玻璃用白云石引入 MgO,同时会把 Al₂O₃ 压得比较低;非浮法玻璃往往不用或少用白云石,MgO 接近零,而 Al₂O₃ 因为原料和配方原因偏高。所以MgO / (Al₂O₃ + 0.01)这个比值在两类之间的分离度,明显好于两个原始特征中的任何一个。加 0.01 是为了避免除零,这个技巧在处理成分比时很常用。

第二个是钡的存在性编码。BaO 大于某个阈值(我实测下来 0.5 附近比较稳)就置 1,否则置 0。这一列几乎就是车头灯玻璃的专用开关,构造出来之后,决策树的第一层分裂往往会直接用它,模型的解释性也跟着变好。

第三个是钾钠比与钾的绝对量。餐具玻璃因为要手工成型、需要较长的料性,K₂O 明显偏高。单看 K₂O 已经有效,但K₂O / (Na₂O + 0.01)在面对容器玻璃(Na₂O 高、K₂O 低)时能进一步拉开差距。

第四个是折射率与钙的联合项。RI 与 CaO 有较强的正相关,两者一起看能稳住“密度档位”这个维度。我用的是RI × CaO这样一个粗糙的交互项,虽然不优雅,但在小样本下比让模型自己学交互更可靠。

第五个是铁的相对水平。Fe₂O₃ 绝对值很小,但在车辆浮法玻璃里被压到极低,在非浮法玻璃里明显偏高。我对这一列做了一次符号对数变换log1p(x * 100),把它从挤在零点附近的一堆数拉开成一个有区分度的连续变量,实测对提升车辆类的识别率有直接帮助。

import numpy as np import pandas as pd def build_features(df: pd.DataFrame) -> pd.DataFrame: out = df.copy() # 镁铝比:浮法 vs 非浮法的主分割线 out["mgo_al2o3_ratio"] = out["MgO"] / (out["Al2O3"] + 0.01) # 钡存在性:车头灯玻璃的开关 out["has_bao"] = (out["BaO"] > 0.5).astype(int) # 钾钠比:餐具玻璃的助推项 out["k_na_ratio"] = out["K2O"] / (out["Na2O"] + 0.01) # 折射率与钙的交互:稳住密度档位 out["ri_x_cao"] = out["RI"] * out["CaO"] # 铁的相对水平:拉开被压缩的小量级 out["fe_scaled"] = np.log1p(out["Fe2O3"] * 100) return out

2.3 缩放策略的选择要看模型,不要一刀切

缩放这件事最容易被做成机械动作:无脑StandardScaler全表来一遍。我踩过的坑是这样的——树模型对单调变换完全不敏感,缩放与否结果几乎一样;但对逻辑回归、SVM、KNN 这些依赖距离或系数的模型,缩放方式的选择会直接影响结果。

我的实际做法是按模型分管道。树模型管道里只做特征构造,不做缩放;线性模型和距离模型管道里做标准化。另外有一点值得单独说:成分百分比这种数据,各列加起来接近 100,存在天然的成分约束(compositional constraint),严格来说应该做对数比变换(CLR 或 ILR)再进线性模型。我在这个数据集上试过 CLR,效果比直接标准化略好,但提升幅度有限,考虑到实现复杂度和可解释性,最终没有采用。如果你追求极致指标,可以试;如果追求可维护,标准化就够用。

注意:任何缩放、变换、编码都必须放进 Pipeline 里,在交叉验证内部拟合。先在全量数据上 fit 再划分,是最经典的数据泄漏,指标能虚高好几个点。

3. 模型选型与训练:从基线到真正可用的分类器

3.1 先跑一个逻辑回归当“体温计”

不管最后上什么模型,我都会先跑一个带多项式的逻辑回归当基线。原因很实在:它快、稳定、系数可解释,而且能立刻告诉我这批特征到底有没有可分性。如果逻辑回归的 macro-F1 只有 0.3,那说明特征构造出了问题,这时候直接上复杂模型只是在给错误特征做非线性拟合。

我的基线配置是:特征构造后的全部列 + 标准化 + 多项逻辑回归,正则强度用交叉验证在小网格里搜。小样本下C的搜索范围不要太宽,我一般在 0.01 到 10 之间取对数网格,再宽就是在拟合噪声。

基线结果给我的信息比分数本身更多。系数一看就知道,MgO 相关特征和 BaO 存在性这两组的权重最高,符合预期;而 SiO₂ 的系数几乎在零附近晃,说明它在这个任务里确实是陪跑项。这个判断在后面做特征筛选时帮了大忙。

3.2 树模型与小样本的博弈:我实测的四种方案对比

这个数据集样本量只有两百多条,属于典型的“特征不算多、样本更不多”的场景。我拿四种模型在同一套分层交叉验证下做了对照,结果差异比想象中大。

模型关键配置macro-F1(多次重复均值)特点与坑
多项逻辑回归标准化 + L2,C 网格搜索中等偏上稳定、可解释,线性边界对车辆两类区分吃力
随机森林树数 500,最小叶样本 1,class_weight='balanced'较高对特征构造敏感,最小叶样本设 2 以上会明显压掉小类
梯度提升学习率 0.05,深度 3,迭代 300较高但不稳小类样本太少时训练波动大,不同随机种子结果差异明显
KNN(k=5,距离加权)标准化后欧氏距离中等对缩放极其敏感,k 值稍大就吃掉小类

我的结论是:随机森林是这个小样本场景里性价比最高的选择,不需要太多调参就能给出稳定结果,而且特征重要性可以直接拿来和化学常识对账。梯度提升虽然上限可能略高,但在样本量不足三百、最小类只有个位数的情况下,种子敏感性太强,做出来的模型没法给别人复现。KNN 不是不能用,但它的表现高度依赖缩放方式和 k 值,可调空间小,适合作为对照而不是主力。

至于“最小叶样本”这个参数,我要单独强调。它的默认值通常是 1,很多人为了“防过拟合”随手改成 3 或 5,在这个数据集上这等于直接告诉模型“不要学小类”。我实测把最小叶样本从 1 提到 5,最小那一类的召回率会掉得很夸张。小样本多分类里,这个参数要往小调,防过拟合应该交给样本权重和正则,而不是砍掉叶子。

from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import StratifiedKFold, cross_val_score pipe = Pipeline([ ("scaler", StandardScaler()), ("clf", RandomForestClassifier( n_estimators=500, min_samples_leaf=1, max_features="sqrt", class_weight="balanced", random_state=42, n_jobs=-1, )), ]) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(pipe, X, y, cv=cv, scoring="f1_macro") print(scores.mean(), scores.std())

3.3 类别不平衡:别把 SMOTE 当万能药

看到 7:1 的不均衡比例,很多人的第一反应是上过采样。我在这里的建议是:先试类权重,再试采样,最后才考虑合成样本。

理由是样本量太小了。最小类只有个位数样本,SMOTE 在这么少的点上做插值,生成出来的“新样本”基本是在原样本之间连线上取点,等于把原本就很窄的分布进一步压缩,模型学到的是一个更窄的、不真实的决策区域。我实测过在最小类上做 SMOTE,训练集指标很漂亮,验证集上小类召回率反而下降,因为它把边界推得过于激进。

类权重(class_weight='balanced')是更稳的第一步,它只是改变损失函数的加权,不改变样本分布,风险小得多。如果类权重还不够,可以考虑对多数类做欠采样,把最大类的样本数压到次大类的 1.5 倍左右,配合类权重一起用。我在最终方案里用的就是“类权重 + 轻微欠采样”,没有引入任何合成样本。

注意:任何重采样操作都必须在交叉验证的训练折内部完成,不能在划分前对整个数据集做。否则验证折里混入了由验证样本合成的点,评估结果完全失真。

3.4 评估口径:为什么我更信 macro-F1 而不是准确率

这个数据集上,一个把所有样本都预测成最大类的“傻子模型”,准确率就能到三成以上。所以准确率在这里几乎没有决策价值。我用的主指标是macro-F1,它对每个类一视同仁,小类判错会被放大惩罚,正好对应我真正关心的东西——不能因为某一类样本少就放弃它。

辅助指标我还会看两样:每类的召回率和混淆矩阵的对角线分布。召回率能告诉我哪一类在系统性漏判;混淆矩阵能告诉我错判流向了哪里,是“浮法判成非浮法”还是“建筑判成车辆”,这两种错误的业务含义完全不同,前者说明工艺判别特征没抓到,后者说明纯度相关特征不够。

还有一个实操细节:小样本下单次交叉验证的方差很大,我固定做五次不同随机种子的分层五折,报均值和标准差。如果标准差超过均值的三成,说明结果不可靠,得回头看验证策略而不是继续调参。

4. 误判排查实录:错在哪里,为什么错

4.1 混淆矩阵暴露出的三个高发区

跑完一轮之后,我把多次交叉验证的混淆矩阵累加起来看,误判并不是均匀分布的,而是集中在三个区域,而且每一个都能对应到具体的化学原因。

第一个高发区是建筑浮法与建筑非浮法之间。这两类的错判量占全部误判的相当大一部分。原因不复杂:它们的用途相同,配方的大方向接近,差异主要来自成型工艺带来的 MgO 与 Al₂O₃ 差别。当某个样本的 MgO 正好落在中间地带(比如 1% 到 2% 之间),单靠这一个特征就判不准,必须结合 Al₂O₃ 和 RI 一起看。

第二个高发区是建筑浮法与车辆浮法之间。这两类都是浮法工艺,配方骨架几乎一样,差别在于原料纯度和铁含量控制。这是全任务里最难的一段,我实测即使加了铁的对数变换,这两类的区分度依然有限。现实一点的预期是:把它们分到“浮法”这个大层没问题,细到建筑还是车辆,能到八成左右就算不错。

第三个高发区是容器玻璃与餐具玻璃之间。这两类都含较高的 Na₂O 和 CaO,区别在 K₂O。餐具玻璃的 K₂O 明显更高,但如果某条样本的 K₂O 落在中间值,就容易被判反。钾钠比这个构造特征在这块起了明显作用,加进去之后这一对的混淆量下降了不少。

4.2 从一条具体误判反推特征归因

举个我印象比较深的例子。有一条真实类别是“非浮法建筑玻璃”的样本,被模型高置信度判成了“浮法建筑玻璃”。我把它的特征拉出来和两类的均值对比,发现它的 MgO 并不接近零,反而有一个不算低的值,同时 Al₂O₃ 也偏低——这两项组合起来,从化学上讲确实更像浮法配方。

这条样本给我的启发是:“非浮法玻璃的 MgO 一定接近零”这个经验规则是有例外的。某些非浮法工艺也会引入含镁原料,只是比例和来源不同。所以构造特征时用“MgO 是否为零”这种二值判断会误伤,用比值(镁铝比)更稳,因为它同时纳入了 Al₂O₃ 的信息,容错空间更大。这也是我后来把has_mgo这种硬编码标记从特征集里删掉、换成连续比值的原因。

类似的复盘我还做过几条车头灯玻璃的样本。有一条 BaO 不为零但数值偏低,模型没有给出足够置信度。查下来发现它的 BaO 确实在阈值边缘,而我的存在性编码用的是硬阈值 0.5。后来我把这一列改成“BaO 原值 + 存在性编码”两列都保留,让模型有得选,比单一硬阈值稳。

4.3 常见问题速查表

下面这张表是我在实际调试中反复用到的排查清单,按现象倒查原因,基本能覆盖八成的卡点。

现象最可能的原因处理方式
整体准确率还行,但某一类召回率为 0该类样本太少,被多数类淹没加类权重,检查该类的分层抽样是否生效
交叉验证分数很高,换一批数据就崩缩放或采样在划分前做了,数据泄漏全部变换塞进 Pipeline,在折内拟合
加了新特征后指标反而下降新特征与已有特征高度共线,或引入了噪声做相关性检查,构造特征后重新做一次特征筛选
不同随机种子结果差异巨大样本量小 + 模型方差大固定多次重复交叉验证,报均值与标准差
浮法与非浮法总是互相错判只用了 MgO 单一特征,没用比值加入镁铝比,结合 Al₂O₃ 与 RI 一起判断
车辆两类几乎分不开铁含量特征被量级压缩,模型没抓到对 Fe₂O₃ 做对数变换,检查缩放是否正确
KNN 表现忽好忽坏未标准化,或 k 值过大吃掉小类强制标准化,k 取 3 到 7 之间小范围搜索

4.4 上线前我会坚持的几条纪律

最后分享几条我个人在这个任务上吃过亏之后定下来的规矩,不一定适合所有场景,但在这个数据集上确实管用。

第一,特征构造的每一列都要能说出化学理由。我曾经为了刷分堆了十几列多项式交互,交叉验证分数涨了两个点,但换一批样本直接崩。后来我把所有构造特征列出来,凡是说不出“它对应哪个成分关系”的,全部删掉。留下来的五列每一个都有明确的判据支撑,模型的可解释性和泛化性同时变好了。

第二,结论要分层给,不要只给一个类别标签。因为浮法内部的两类确实难分,我在实际交付时会把输出组织成两层:第一层判“浮法 / 非浮法 / 容器 / 餐具 / 车灯”,第二层在浮法内部再判建筑还是车辆,并且对第二层给出置信度。置信度低的样本走人工复核,比强行给一个高置信度错判有用得多。

第三,永远保留一个“未知”出口。这个数据集的七类是封闭的,但真实场景里一定会有配方超出这七类的样本。我在最终方案里加了一条规则:如果模型的最大类别概率低于某个阈值(我实测下来 0.45 附近比较合适),就输出“待定”。这条规则让模型在封闭测试集上的指标略微下降,但在真实数据上的可用性提升非常明显——宁可说不知道,也不要自信地判错。

第四,把特征分布监控做起来。成分数据的漂移往往是渐进的,某段时间原料来源变了,MgO 的整体水平会慢慢移动,模型不会立刻崩,但准确率会一点点掉。我习惯定期统计几个关键特征的分位数,一旦某个特征的中位数偏移超过历史波动范围,就触发重训检查。这个动作比盯着准确率曲线更早发现问题。

最后再补一个我觉得最容易被忽略的点:这个任务里最值钱的东西不是模型,是那张“特征—类别”的对应表。模型可以换、参数可以调,但只要那张表理解到位了,换任何算法都能跑出可用结果;反过来,表没读透,堆再复杂的模型也只是在拟合噪声。我在实际使用中的体会是,花在理解 MgO、K₂O、BaO 这三列上的时间,回报远高于花在调参上的时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进