幾秒鐘、不破壞,就看穿成分
近紅外光(Near-Infrared, NIR,約 780–2500 nm)會被食品中 O–H、C–H、N–H 等化學鍵吸收。 量一條光譜,等於同時「問」了樣本裡的水分、脂肪、蛋白質——而且不需要試劑、不破壞樣本。
快
一次掃描幾秒鐘,可裝在生產線上即時量測。非破壞
光照進去、反射或穿透出來,樣本完好如初。但是…
一條光譜是上百個高度重疊的數字,必須靠化學計量學才能解讀。Tecator 肉品 NIR(公開資料)
Tecator Infratec 食品分析儀量測的 240 個肉品樣本,每個樣本有 100 個波長(850–1050 nm)的吸光值, 並以濕化學測得水分、脂肪、蛋白質的真值。屬公開資料(StatLib / OpenML id 505)。
| 成分 | 範圍 (%) | 平均 | 本課用途 |
|---|---|---|---|
| 水分 moisture | 32.8 – 76.6 | 62.9 | 備用目標 |
| 脂肪 fat | 0.9 – 58.5 | 18.5 | PLS 預測目標 |
| 蛋白質 protein | 8.8 – 23.2 | 17.8 | 備用目標 |
前處理:SNV 散射校正
不同樣本的顆粒、量測距離會讓基線整條飄移。標準常態變量轉換(SNV)把每條光譜各自校正,凸顯真正的化學差異。
下載 Tecator,讀進 Python(.csv / .arff / .json)
本 repo 的 data/tecator.csv 已是整理好的乾淨版,pd.read_csv 即可用。
但你在公開資料庫遇到的原始檔,常是 .arff(Weka/OpenML 原生格式)或 .json。
這一節把「從哪裡下載、三種格式怎麼讀」一次講清楚。
① 本 repo(最快)
data/tecator.csv:240 列 × 100 波長欄 + moisture/fat/protein,直接 pd.read_csv 就能用。② OpenML(線上)
資料 id 505、原生 .arff 格式、預設目標fat。可用 sklearn 一行抓,或從網站下載。③ StatLib(原始)
原始出處lib.stat.cmu.edu/datasets/tecator,純文字檔,需自行切欄位,最費工。下載:一行抓下來(sklearn → OpenML)
這正是 python/nir_utils.py 取資料的方式——第一次連網下載、快取成 CSV,之後離線可用。
from sklearn.datasets import fetch_openml
# OpenML 上的 Tecator(data id 505),回傳 pandas DataFrame
ds = fetch_openml("tecator", version=1, as_frame=True, parser="auto")
df = ds.frame
df.to_csv("tecator.csv", index=False) # 存一份離線備份
# 欄位:absorbance_1..absorbance_100(吸光值)
# principal_component_1..22(OpenML 附的 PCA 特徵,本課用不到)
# moisture / fat / protein(濕化學真值,% w/w)
abs_cols = [c for c in df.columns if c.startswith("absorbance_")]
X = df[abs_cols].to_numpy(float) # (240, 100) 光譜矩陣
y = df[["moisture", "fat", "protein"]].astype(float)
print(X.shape, list(y.columns)) # (240, 100) ['moisture','fat','protein']
想直接拿原始檔?OpenML 的 .arff 載點(也可在瀏覽器開 openml.org/d/505 點 Download):
curl -L -o tecator.arff https://www.openml.org/data/v1/download/52617/tecator.arff
讀取 .arff 檔(Weka/OpenML 原生格式)
ARFF(Attribute-Relation File Format)是純文字:開頭用 @relation、@attribute 宣告每個欄位,
@data 之後才是逗號分隔的數值列。Python 有兩種常見讀法:
法一 — SciPy(scipy 內建,免額外安裝)
from scipy.io import arff
import pandas as pd
data, meta = arff.loadarff("tecator.arff")
df = pd.DataFrame(data)
print(meta) # 印出每個 @attribute 的型別
X = df[[c for c in df.columns if c.startswith("absorbance_")]].to_numpy(float)
y = df[["moisture", "fat", "protein"]].astype(float)
法二 — liac-arff(pip install liac-arff,保留完整 metadata)
import arff # 套件名 liac-arff,import 名為 arff
import pandas as pd
with open("tecator.arff", encoding="utf-8") as f:
dataset = arff.load(f)
cols = [name for name, attr_type in dataset["attributes"]] # 欄位名稱清單
df = pd.DataFrame(dataset["data"], columns=cols)
print(df.shape) # (240, 125):100 吸光 + 22 PC + 3 目標
b'...'(bytes),
需要 df[col] = df[col].str.decode("utf-8") 轉回字串。Tecator 全為連續數值,可略過此步。讀取 .json 檔
有時資料以 JSON 交換(API 回傳,或自己轉存)。最通用的是「每列一筆物件」的 records 格式:
import pandas as pd, json
# 先把資料存成 JSON(orient="records":list of dict,最通用)
df.to_json("tecator.json", orient="records", indent=2, force_ascii=False)
# 讀回來:pandas 一行搞定
df = pd.read_json("tecator.json", orient="records")
# 或用標準函式庫 json,手動轉 DataFrame(適合巢狀/自訂結構)
with open("tecator.json", encoding="utf-8") as f:
rows = json.load(f) # -> list[dict]
df = pd.DataFrame(rows)
X = df[[c for c in df.columns if c.startswith("absorbance_")]].to_numpy(float)
y = df[["moisture", "fat", "protein"]].astype(float)
▶ 進階:用 OpenML 的 JSON API 取得 metadata 與載點
import requests
# OpenML 的資料描述本身就是 JSON(含格式、預設目標、.arff 載點)
meta = requests.get("https://www.openml.org/api/v1/json/data/505").json()
d = meta["data_set_description"]
print(d["name"], d["format"], d["default_target_attribute"]) # Tecator ARFF fat
print(d["url"]) # -> .arff 下載連結,再用上面的 scipy / liac-arff 讀取
PCA:把上百個波長壓成幾個方向
主成分分析(Principal Component Analysis)在資料最分散的方向上建立新座標軸: PC1 是變異最大的方向、PC2 與它垂直且次大……少數幾個主成分,就能描述幾乎全部的資訊。
要保留幾個主成分?
分數圖:樣本自己排好了
負荷量回連化學
PLS:從光譜「預測」脂肪含量
偏最小二乘回歸(Partial Least Squares)找出的方向,同時解釋光譜的變異、又與目標脂肪相關。 這就是它和 PCA 的關鍵差異:PCA 只看 X,PLS 一手抓 X、一手抓 y。
該用幾個潛在變量?用交叉驗證
成果:預測 vs 真實
R² = 0.96
解釋了測試集 96% 的變異。RMSEP ≈ 2.8%
平均預測誤差(脂肪百分點)。模型在「看」哪裡?
Python 程式(scikit-learn)
整套分析用 numpy / scikit-learn / scipy 完成,可重現。完整檔案見 python/ 目錄。
前處理 — SNV 散射校正(python/nir_utils.py)
def snv(X):
"""Standard Normal Variate:逐樣本(逐列)散射校正。"""
mu = X.mean(axis=1, keepdims=True)
sd = X.std(axis=1, keepdims=True)
return (X - mu) / sd
PCA — 主成分分析(python/02_pca.py)
from sklearn.decomposition import PCA
Xp = snv(X) # 散射校正
pca = PCA(n_components=10).fit(Xp)
scores = pca.transform(Xp) # 樣本在新軸上的位置 (T)
evr = pca.explained_variance_ratio_ * 100
# PC1 = 68.9% | 前2個 = 97.3% | 前3個 = 98.9%
PLS — 偏最小二乘回歸,預測脂肪(python/03_pls.py)
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import train_test_split, cross_val_predict, KFold
Xp = savgol_d(snv(X), window=15, poly=2, deriv=1) # SNV + 一階導數
Xtr, Xte, ytr, yte = train_test_split(Xp, y_fat,
test_size=0.25, random_state=42)
# 用 10-fold 交叉驗證 + 轉折點規則挑潛在變量 -> 4 個
model = PLSRegression(n_components=4).fit(Xtr, ytr)
yte_p = model.predict(Xte).ravel()
# 測試集 R² = 0.964 | RMSEP = 2.79% | RPD = 5.2
▶ 如何在本機重跑
pip install numpy pandas scipy scikit-learn matplotlib
cd python
python 01_data_prep.py # 抓資料 + 原始/SNV 光譜圖
python 02_pca.py # 陡坡 / 分數 / 負荷量
python 03_pls.py # 選 LV / 預測 vs 真實 / 係數
# 所有圖輸出到 python/figures/
用 Orange Data Mining 親手拉一遍
課堂可用 Orange 的拖拉式 widget,不寫一行程式就重現上面的 PCA 與 PLS。
開啟 orange/nir_tecator_workflow.ows,把 File 指向 data/tecator.csv、設 fat 為 target 即可。
┌─ Data Table (檢視原始數字)
│
File ────┼─ PCA ──Transformed Data──▶ Scatter Plot ← PCA 分支(探索;Color = fat)
(tecator) │
├─ PLS ──Learner──▶ Test & Score ← PLS 分支(R² / RMSE)
│ └─ Model ──▶ Predictions ──▶ Scatter Plot(預測 vs 真實)
└────────────────(Data 同時接 Test&Score / Predictions)| 對應概念 | Python 圖 | Orange widget |
|---|---|---|
| 原始光譜 | fig01 | File → Data Table |
| 主成分數量 | fig03 | PCA 視窗內變異曲線 |
| 分數圖(依脂肪上色) | fig04 | PCA → Scatter Plot(Color=fat) |
| 選 LV / 過度擬合 | fig06 | 調 PLS Components + Test&Score |
| 預測 vs 真實 | fig07 | Predictions → Scatter Plot |
| 回歸係數 → 化學 | fig08 | PLS「Coefficients and Loadings」 |
PCA 與 PLS,一張表看懂
| 面向 | PCA | PLS |
|---|---|---|
| 學習方式 | 非監督(只看 X) | 監督(用 X 與 y) |
| 目的 | 探索結構、分群、找異常 | 定量預測(如脂肪 %) |
| 找方向的準則 | 最大化 X 的變異 | 最大化 X 與 y 的共變異 |
| 本課結果 | 前 3 個 PC 抓 98.9% 變異 | 測試集 R²=0.96、RPD 5.2 |
| 關鍵圖 | 陡坡圖、分數圖、負荷量 | RMSECV、預測 vs 真實、係數 |
| 何時用 | 先用它「看」資料 | 確定要預測時用它建模 |