本节摘要:定性鉴定回答"这是谁"。谱库检索用相似度度量给未知谱排名——相关系数对乘性与加性扰动免疫,欧氏距离会先被整体缩放骗过;PCA 把高维光谱压进二维得分图,谱库里没有的样品靠自然分群也能鉴定。误判的防线在阈值与知识边界。
特征表在手(3.1 节),鉴定进入"比对"环节。这一节回答两个递进的问题:样品在谱库里时怎么比——三种相似度度量各自的脾气;样品不在谱库里怎么办——PCA 得分图让数据自己开口分群。它上承寻峰,下启定量(3.3 节的 PLS 与本节的 PCA 共享降维思想),也是第 5 章掺假鉴别与真伪鉴定两个案例的方法主干。
比对的数学骨架简单:未知谱与库内每条谱算一个数,按大小排名。分歧在用什么数。相关系数看两条曲线的共变关系;夹角余弦看两个向量方向是否一致;欧氏距离直接量点间距。它们的差别在扰动面前才暴露——先用一个混合样品(七成丙酮、三成乙醇,另加 15% 乘性波动与 0.08 平移)测四种"嫌疑人":
# 谱库检索:三种相似度对扰动的反应 import numpy as np rng = np.random.default_rng(9) x = np.linspace(800, 1800, 400) def species(peaks): # 合成纯物质谱 s = np.zeros_like(x) for n, a, w in peaks: s += a*np.exp(-0.5*((x-n)/w)**2) return s lib = { '乙醇': species([(1050,0.9,20),(1250,0.5,25),(1450,0.3,30)]), '丙酮': species([(1150,0.8,22),(1350,0.6,28),(1710,0.4,25)]), '甲苯': species([(1000,0.7,18),(1220,0.6,24),(1600,0.5,30)]), '水': species([(1640,0.95,35)]), } unk = 0.7*lib['丙酮'] + 0.3*lib['乙醇'] unk = 1.15*unk + 0.08 + rng.normal(0, 0.01, x.size) # 乘性+加性+噪声 def pearson(a, b): return float(np.corrcoef(a, b)[0, 1]) def cosine(a, b): return float(a @ b / (np.linalg.norm(a)*np.linalg.norm(b))) def euclid(a, b): return float(np.linalg.norm(a - b)) print("原始坐标(乘性波动与平移都在场):") for name, s in lib.items(): print(f" {name}: pearson={pearson(unk, s):.3f} cosine={cosine(unk, s):.3f} euclid={euclid(unk, s):.3f}")
输出:
原始坐标(乘性波动与平移都在场): 乙醇: pearson=0.148 cosine=0.454 euclid=4.918 丙酮: pearson=0.909 cosine=0.871 euclid=2.501 甲苯: pearson=-0.246 cosine=0.273 euclid=5.645 水: pearson=-0.181 cosine=0.180 euclid=6.287
三个度量异口同声把丙酮排第一——鉴定结论一致。但看清楚它们各自被扰动伤到什么程度:未知样品明明含三成乙醇,乙醇的相关系数只有 0.148——乘性波动与加性平移把绝对幅值搅乱了,余弦与欧氏距离都受了内伤。相关系数对仿射变换(乘个常数加个常数)天然免疫,所以它受的影响最小。再做一个对照——把两边都做 SNV 之后重比:
# SNV 之后再比对 def snv(v): return (v - v.mean()) / v.std() print("SNV 后:") for name, s in lib.items(): print(f" {name}: pearson={pearson(snv(unk), snv(s)):.3f} euclid={euclid(snv(unk), snv(s)):.3f}")
输出:
SNV 后: 乙醇: pearson=0.148 euclid=26.100 丙酮: pearson=0.909 euclid=8.527 甲苯: pearson=-0.246 euclid=31.576 水: pearson=-0.181 euclid=30.739
两个事实跳出来。其一,相关系数的四个数字一个都没变——SNV 本身就是逐谱的减均值除标准差,相关系数对它免疫,这解释了为什么 Pearson 检索常被称作"免预处理"的度量。其二,欧氏距离的数值变了(丙酮 2.501 到 8.527)但排序不变——距离类度量只要扰动是单调的,排名还能保住,但数值大小失真后,阈值就不可信了。用距离类度量做检索,预处理不是可选项;用相关系数,预处理是双保险。
谱库检索有个先天死角:样品不在库里,检索只会硬塞给你一个"最不坏"的答案,0.3 的相关系数它也照排第一名。开放场景的武器是无监督降维——PCA 把四百维的光谱压到两三个主成分,样本在得分图上的自然分群比任何检索都诚实。
# PCA:16 个样本(4 类 x 4 次重复)的得分与方差贡献 X, labels = [], [] for name, s in lib.items(): for _ in range(4): X.append(s + rng.normal(0, 0.02, x.size)) # 每类 4 次重复测量 labels.append(name) X = np.array(X) Xc = X - X.mean(axis=0) # 列中心化 U, S, Vt = np.linalg.svd(Xc, full_matrices=False) # 奇异值分解 T = U * S # 得分矩阵 var = S**2 / (S**2).sum() # 各主成分方差贡献 print("PCA variance ratio:", np.round(var[:4], 3)) for name in ['乙醇', '丙酮', '甲苯', '水']: t = T[[i for i, l in enumerate(labels) if l == name]] print(f"{name}: PC1={t[:,0].mean():+.2f}+-{t[:,0].std():.2f} " f"PC2={t[:,1].mean():+.2f}+-{t[:,1].std():.2f}")
输出:
PCA variance ratio: [0.406 0.388 0.196 0.001] 乙醇: PC1=+1.07+-0.02 PC2=+3.16+-0.01 丙酮: PC1=+3.12+-0.02 PC2=-2.31+-0.02 甲苯: PC1=-1.37+-0.01 PC2=+1.01+-0.02 水: PC1=-2.82+-0.02 PC2=-1.85+-0.02
两个读图要点。方差贡献那行藏着成分数的答案:前三个主成分合计解释 99% 的方差,第四个只剩 0.001——四类样本的信息本来就需要三个方向(类别数减一),第四个方向纯粹是噪声。这个"看方差贡献找拐点"的手法,下一节选 PLS 成分数时原样再用。类均值与类内散布的比例告诉你分群质量:四类的 PC1、PC2 均值彼此相距 1 到 3 个单位,而类内标准差只有 0.02——间隔是散布的上百倍,得分图上四团点会分得清清楚楚。反过来,如果某两团点粘在一起,说明这两个组分在当前波段区分不开,该换波段或换技术(回 1.2 节的选型逻辑),而不是硬调参数。
流程图的右下角是最容易被忽略的一步:判为"库外"。检索系统的失败模式不是报错,而是永远给你一个第一名——分数 0.3 它也说"最匹配丙酮"。所以阈值线必须预先声明(相关系数 0.95 以上才出阳性报告),低于阈值的正确动作是升级到 PCA 分群或补充分析,而不是把低分结果写进报告。
PCA 是无监督的——它不知道你的类别标签,分的群可能按含水量分而不管你想鉴定的掺假。当样本带标签(正品与掺假各一批),监督分类接管:PLS 判别分析(PLS-DA)把类别当 0 和 1 回归;支持向量机在得分空间里画最优边界;随机森林对波段做投票。它们的共同前提只有一条——训练集必须覆盖真实场景的变异(不同批次、温度、装样),训练集太干净的分类器到现场一碰就碎。这块的完整演练留给第 4.2 节与第 5 章案例,此处先记住分工:谱库检索答"像谁",PCA 答"和谁一伙",分类模型答"按既定标准判"。
💡 关键直觉:定性鉴定的可信度不在算法里,在"知识边界"的诚实里。库里没有的东西,任何检索都查不出来——知道自己不知道什么,比多跑一遍算法值钱。
"是谁"有了答案,下一个问题跟着就来:"有多少"。下一节从单波长校准曲线出发,看多元校正怎么在四百个共线变量里解出浓度。