化學計量學 · 近紅外光譜 · 機器學習教學

🥚 用近紅外光譜 (NIR) + PLS 迴歸
預測雞蛋的儲存天數

Python & Orange Data Mining 雙工具實作教材
對應論文:Coronel-Reyes, J., Ramirez-Morales, I., Fernandez-Blanco, E., Rivero, D., & Pazos, A. (2018). Determination of egg storage time at room temperature using a low-cost NIR spectrometer and machine learning techniques. Computers and Electronics in Agriculture, 145, 1–10. doi:10.1016/j.compag.2017.12.030
資料集:Mendeley Data — 6hn67h2trb (v2)  dataset_egg_storage.csv
660 筆樣本30 顆褐殼蛋 第 0–21 天740–1070 nm · 331 波長 SCiO 手持式光譜儀

🎯學習目標

完成本教材後,你將能夠:

  1. 說明近紅外光譜 (NIR) 為何能非破壞性地反映雞蛋的老化。
  2. 對光譜資料做 SNV、Savitzky–Golay 微分等前處理,並理解每種方法的目的。
  3. PLS 迴歸處理「特徵多又高度相關」的光譜資料,並用交叉驗證挑選潛在變數數量。
  4. R²、RMSE、RMSECV 評估迴歸模型,並判讀「預測 vs 實際」圖。
  5. 辨識資料洩漏:理解「隨機分折」與「依樣本分組」驗證的差異。
  6. 分別用 Python(程式)Orange Data Mining(視覺化拖拉) 完成同一個分析。
🧰 下載本教材的檔案
▶ 在 Google Colab 開啟筆記本
📄 dataset_egg_storage.csv(原始資料) 📓 PLS_egg_freshness_colab.ipynb(Python/Colab) 🍊 egg_storage_orange.tab(Orange 資料) 🍊 egg_pls_orange.ows(Orange 工作流程) 🐍 run_analysis.py(繪圖腳本)

1為什麼要測雞蛋的新鮮度?

雞蛋從產下那一刻就開始老化:蛋白變稀、氣室變大、蛋白 pH 因二氧化碳逸散而上升、蛋殼外層的角質層 (cuticle)逐漸乾燥變薄。 傳統評估新鮮度的哈夫單位 (Haugh Unit) 雖然準確,卻是破壞性的(必須打破蛋來量濃蛋白高度),無法用在產線上逐顆檢測。

論文指出:在室溫 (23±1°C) 下,雞蛋大約可存放 14 天;之後品質明顯下降。 因此若能用一支接在智慧型手機上的低成本光譜儀不打破蛋就估出「這顆蛋放了幾天」,對產業與消費者都極具價值。 這正是本研究與本教材要解決的問題:用光譜迴歸出儲存天數 storage_days

💡 這是一個「迴歸」問題
目標變數 storage_days 是連續數值(0、1、2 … 21 天),所以我們用迴歸(PLS Regression)而非分類。 輸入則是每顆蛋的一條近紅外光譜。

2近紅外光譜原理 & 蛋的化學變化

近紅外光 (NIR, 約 700–2500 nm) 照到樣品時,會激發分子中 C–H、O–H、N–H 等化學鍵的倍頻 (overtone)合頻 (combination) 振動。不同化學組成 → 不同的吸收/反射特徵,因此光譜等於樣品的「化學指紋」。

本研究使用的 SCiO 手持式光譜儀波長範圍 740–1070 nm,屬於「短波近紅外」。在這個波段:

  • ~890 nm:C–H 第三倍頻
  • ~960 nm:O–H 第二倍頻(與水/角質層含水量有關)
  • ~1020 nm:N–H 第二倍頻(與蛋白質有關)

光在 700–900 nm 可穿透約 3–4 mm,足以穿過蛋殼抵達角質層與蛋白(但到不了蛋黃)。 隨著儲存時間增加,角質層乾燥變薄、蛋殼碳酸鈣礦物更外露、CH/OH/NH 化學鍵改變 —— 這些都會反映在光譜上, 讓模型「看得出」蛋的年紀。

📷 量測方式
每顆蛋從鈍端 (blunt end) 量測、套上遮光配件以隔絕外界光,並把兩次重複量測平均成一筆資料。

3認識資料集

研究團隊監測 30 顆褐殼蛋(H&N 品系、49–52 週齡母雞、蛋重 55–65 g), 從產下當天 (第 0 天) 起連續 22 天、每天量一次,得到 30 × 22 = 660 筆光譜。

欄位意義在建模中的角色
storage_days已儲存天數 0–21目標 y(要預測的值)
sample蛋的編號 1–30(同一顆蛋出現 22 次)分組驗證用(見第 8 節)
Spectra_740Spectra_1070各波長的反射率(331 個)特徵 X
不同儲存天數的平均光譜
圖 1 不同儲存天數的平均 NIR 光譜(重現論文 Fig. 4)。儲存越久,整體反射率越高, 在 C–H/O–H/N–H 對應波段變化最明顯。這就是 PLS 能「讀出」天數的物理基礎。

4PLS 迴歸是什麼?為什麼適合光譜?

我們有 331 個波長(特徵),但它們有兩個棘手特性:

  • 高度共線性:相鄰波長的反射率幾乎一模一樣(高度相關)。
  • 特徵多:波長數量可能比樣本數還多。

普通最小平方法 (OLS) 在這種情況會因矩陣不可逆而崩潰。PLS(偏最小平方, Partial Least Squares)的巧思是:

💡 PLS 的核心想法
不直接用 331 個波長,而是找出少數幾個潛在變數 (Latent Variables, LV)。 每個 LV 都是波長的線性組合,並且被刻意挑選成「同時解釋 X 的變異、又和 y(天數)最相關」的方向。 於是把 331 維壓縮成大約 10 維,還保留了最能預測天數的資訊。

與只看 X 自身變異的 PCA 不同,PLS 是監督式的 —— 它在降維時就「偷看」了 y。 最關鍵的超參數就是 要用幾個 LV:太少會欠擬合,太多會過擬合。我們用交叉驗證來決定(第 7 節)。

5光譜前處理 (Preprocessing)

原始光譜混有與化學無關的干擾 —— 蛋殼粗糙造成的散射、儀器或擺放造成的基線漂移。 前處理的目的就是去除這些干擾、突顯化學訊號。常見方法:

方法做什麼解決什麼
SNV
Standard Normal Variate
每條光譜各自減平均、除標準差散射造成的乘性/加性偏移
Savitzky–Golay 平滑在小窗內用多項式擬合再取值隨機雜訊(降噪)
一階微分 (1st derivative)取相鄰點的斜率固定基線偏移
二階微分 (2nd derivative)取斜率的變化率線性基線;突顯吸收峰(但放大雜訊)
四種前處理比較
圖 2 四種前處理後的平均光譜。原始光譜中各天數只是整條上下平移; 經 SNV/微分後,天數之間的差異被放大且更易區分,有利於 PLS 建模。

6Python 建模流程

以下是用 scikit-learn 的核心程式碼(完整、可一鍵執行的版本在第 12 節的 Colab 筆記本)。

① 載入資料、切出 X / y

import numpy as np, pandas as pd
from scipy.signal import savgol_filter
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import cross_val_predict, KFold, GroupKFold
from sklearn.metrics import r2_score, mean_squared_error

df = pd.read_csv("dataset_egg_storage.csv")
Xcols = [c for c in df.columns if c.startswith("Spectra_")]
X = df[Xcols].values                 # (660, 331) 光譜
y = df["storage_days"].values        # 目標:天數
groups = df["sample"].values          # 蛋編號(分組驗證用)

② 定義前處理

def snv(s):
    return (s - s.mean(axis=1, keepdims=True)) / s.std(axis=1, keepdims=True)

def sg(s, deriv=2, window=15, poly=2):   # Savitzky-Golay 微分
    return savgol_filter(s, window_length=window, polyorder=poly, deriv=deriv, axis=1)

Xp = sg(X, deriv=2)                   # 本例最佳:二階微分

③ 訓練 PLS 並用交叉驗證評估

kf = KFold(n_splits=10, shuffle=True, random_state=42)
yhat = cross_val_predict(PLSRegression(n_components=12, scale=True), Xp, y, cv=kf)

rmse = np.sqrt(mean_squared_error(y, yhat))
print(f"R² = {r2_score(y, yhat):.3f} RMSECV = {rmse:.2f} 天")
# -> R² = 0.820 RMSECV = 2.69 天
🔧 為什麼要 scale=True
PLS 對特徵尺度敏感。scale=True 會在建模前把每個波長標準化(StandardScaler), 避免數值大的波長主導模型。這等同論文流程中的「mean-center / autoscale」步驟。

7選潛在變數數量:一倍標準誤法則

「要用幾個 LV」是 PLS 最重要的決定。作法是對每個候選數量跑 10-fold 交叉驗證, 算出 RMSECV(交叉驗證均方根誤差),畫成曲線。

RMSECV = √( Σ (yi − ŷi)² / n ) (單位:天,越小越好)
RMSECV vs 潛在變數數量
圖 3 四種前處理下 RMSECV 隨 LV 數量變化(誤差棒為 ±1 標準誤)。 SavGol 二階微分(綠線)用最少的 LV 就達到最低誤差,因此雀屏中選。
⚠️ 不要直接抓最低點!
曲線在 LV 很多時往往還在緩緩下降,直接取最低點常選到約 19–20 個 LV,有過擬合風險
💡 一倍標準誤法則 (One-Standard-Error Rule)
在「最低 RMSECV + 1 個標準誤」的範圍內,選最少的 LV。這樣得到的模型更精簡、更穩健, 且和最佳值在統計上沒有顯著差異。本例 → SavGol 二階微分 + 12 個 LV

8⚠️ 驗證的陷阱:隨機分折 vs 依「蛋」分組

這是整份教材最重要的觀念。回想資料結構:同一顆蛋被量了 22 次(第 0 天到第 21 天)。 同一顆蛋第 5 天和第 6 天的光譜長得非常像。

如果用隨機 10-fold,這兩筆很可能一筆落在訓練集、一筆落在驗證集。模型可能靠「認得這顆蛋」 來壓低誤差,而不是真的學會「判斷天數」。這種因資料切分不當而高估表現的現象,叫做資料洩漏 (data leakage)

💡 解法:GroupKFold(依蛋分組)
把同一顆蛋的全部 22 筆資料整組放進同一折,模擬「預測一顆從沒見過的新蛋」。 這才是誠實的泛化能力評估。
# 依蛋編號分組做交叉驗證
gkf = GroupKFold(n_splits=10)
yhat_grp = cross_val_predict(PLSRegression(n_components=12, scale=True),
                             Xp, y, cv=gkf, groups=groups)
驗證方式RMSECV (天)MAE (天)
隨機 10-fold CV0.8202.692.18
依蛋分組 GroupKFold0.8112.752.22
✅ 本例的好消息
兩者差距非常小(R² 只掉 0.009)。這代表模型主要學到的是「老化的化學變化」而非「某顆蛋的身分」, 對全新的蛋一樣有效,泛化能力良好。但你必須親手驗證過,才能這樣說。

9結果與解讀

0.82
R²(決定係數)
2.69
RMSECV(天)
2.18
MAE 平均絕對誤差(天)
12
潛在變數 (LV)
預測 vs 實際
圖 4 預測 vs 實際(重現論文 Fig. 9a)。點越靠近對角線越好。
殘差直方圖
圖 5 預測誤差分布(重現論文 Fig. 9b),大致對稱、集中在 0 附近。

模型可在 ±2.7 天的誤差內估出雞蛋年紀。圖 4 的擬合線斜率略小於 1(約 0.84),代表 PLS 常見的 向均值收縮:很新鮮(接近 0 天)的蛋會被略微高估、很舊(接近 21 天)的蛋會被略微低估。

📊 和論文比較
論文用類神經網路 (ANN) 取得最佳結果 R²=0.832、RMSECV=1.97 天;其 PLS 模型 R² 略低於 0.80。 我們這個教學版 PLS(二階微分 + 12 LV)達到 R²≈0.82,與論文同等級,且用更精簡、更易解釋的線性模型 —— 非常適合教學。

10哪些波長最重要?(VIP 與迴歸係數)

建好模型後,我們想知道它「看了光譜的哪些位置」。VIP (Variable Importance in Projection) 衡量每個波長對 PLS 的貢獻,慣例以 VIP > 1 視為重要波長。

PLS 回歸係數與 VIP
圖 6 上:PLS 迴歸係數;下:VIP 分數(綠色虛線為化學鍵位置)。 最重要的波長集中在 ~1000 nm(N–H/O–H 區)~790–820 nm(C–H 區)

本例 VIP 最高的波長約為 788、821、1002–1007 nm。這與論文觀察一致:蛋在儲存過程中, 角質層含水量與蛋白質的 N–H/O–H 鍵結改變,使得 ~1000 nm 附近成為判斷天數的關鍵區域。 模型不是黑盒子 —— 它的依據可以對應到真實的化學變化。

PCA 探索(補充)

PCA 得分圖
圖 7 PCA 得分圖(SNV 前處理)。前兩個主成分就解釋約 94% 的變異, 且顏色(天數)沿著 PC1 呈現漸層 —— 顯示「老化」是資料中最主要的變異來源。

11用 Orange Data Mining 實作(不寫程式)

Orange Data Mining 是一套視覺化拖拉的資料分析軟體, 特別適合不熟程式的學生:把「元件 (widget)」拉到畫布上、用線連起來,就完成分析。

⚠️ 先安裝光譜外掛
SNV / Savitzky–Golay 等光譜前處理需要 Spectroscopy 外掛(即 Quasar)。 安裝路徑:Orange 選單 Options → Add-ons… → 勾選 Spectroscopy → 安裝後重啟。

步驟:建立工作流程

1
File:載入 egg_storage_orange.tab。 此檔已標好欄位角色(storage_days=target、sample=meta、其餘為光譜特徵),開檔即可用。
2
Data Table:接在 File 後面,檢視資料是否正確載入。
3
Preprocess Spectra(光譜外掛):依序加入 Standard Normal Variate (SNV)Savitzky–Golay(設 2nd derivative,window 15、polynomial order 2)。
4
PLS:把 Components 設為 12
5
Test and Score:接收 Preprocess 的資料 (Data) 與 PLS 的 Learner, 選 Cross validation, 10 folds。即可在表格看到 R²、RMSE、MAE。
6
Predictions → Scatter Plot:把預測值對實際 storage_days 畫散佈圖,重現圖 4。
🔗 連線方式(資料流)
File → Data TableFile → Preprocess SpectraPreprocess Spectra → PLSPreprocess Spectra → Test and Score(Data); PLS → Test and Score(Learner); Preprocess Spectra → PredictionsPLS → PredictionsPredictions → Scatter Plot
✅ 直接打開現成工作流程
本教材附的 egg_pls_orange.ows 已把上述七個元件接好。用 Orange 開啟後: ① 點 File 重新指定到你電腦上的 egg_storage_orange.tab; ② 在 Preprocess Spectra 內加入 SNV 與 SavGol(.ows 提供骨架,前處理步驟請依步驟 3 自行加入); ③ 確認 PLS components=12、Test and Score 選 10-fold。
💡 進階:用 Orange 重現「依蛋分組」驗證
在 Test and Score 中,若資料含一個 meta 欄位 sample,可改用 Cross validation by feature(依特徵分組)並選 sample,即等同 Python 的 GroupKFold —— 對照第 8 節。

12用 Google Colab 跑 Python(免安裝)

Google Colab 是免費的雲端 Python 環境,學生只要有 Google 帳號、用瀏覽器即可執行,無需安裝任何東西。

開啟方式(任選一種)

A
一鍵開啟(推薦):直接點下方按鈕,會用 Colab 開啟本教材的筆記本。
B
直接上傳:到 colab.research.google.com → 選單 檔案 → 上傳筆記本 → 選你下載的 PLS_egg_freshness_colab.ipynb

筆記本第一個資料格會跳出上傳視窗,請選擇 dataset_egg_storage.csv(可從本頁上方「下載」區或 Mendeley 連結取得), 接著由上到下逐格執行 (Shift+Enter) 即可。

▶ 在 Google Colab 開啟筆記本  ⬇ 下載資料 CSV

🈶 關於圖表文字
Colab 預設沒有中文字型,故筆記本內圖表座標軸用英文以免顯示方框;所有教學解說都寫在中文文字格中。 本教學 HTML 的圖(圖 1–7)則已用中文字型繪製。

13Python vs Orange:兩種工具怎麼選?

面向Python (scikit-learn)Orange Data Mining
操作方式寫程式碼拖拉元件、連線
學習曲線較陡(需懂語法)平緩(視覺化、即時預覽)
彈性 / 客製極高(任何流程、自訂函式)受限於現有元件
可重現 / 自動化強(腳本、版本控制)適合互動探索
適合對象需要客製分析、寫論文教學入門、快速試驗、概念理解
💡 建議的教學順序
先用 Orange 拖拉一遍,建立「資料 → 前處理 → 模型 → 驗證」的整體直覺; 再用 Python 把同一流程寫出來,理解每一步的細節並學會客製。兩者結果應一致 —— 這也是很好的交叉驗證!

14動手練習

  1. 換前處理:把二階微分改成 SNV 或原始光譜,重跑第 6 節,比較 RMSECV 差多少。
  2. 改 LV 數量:手動設成 2、5、19 個 LV,觀察圖 4 與過/欠擬合現象。
  3. 窗寬實驗:改 Savitzky–Golay 的 window(如 7、25),看二階微分的雜訊如何變化。
  4. 改成分類題:依「室溫最多放 14 天」,把問題改成「新鮮 (≤7 天) vs 不新鮮 (>7 天)」二元分類, 用 PLS-DA 或邏輯迴歸做做看。
  5. 波長篩選:只用 VIP > 1 的波長重建模型,準確度會變好還是變差?對應論文的「波長選擇」步驟。
  6. 雙工具對照:用 Orange 跑出 R²/RMSE,和 Python 的結果比對,思考為何可能有小差異(亂數種子、分折方式)。

📚參考資料與檔案清單

論文與資料

  • Coronel-Reyes, J. et al. (2018). Determination of egg storage time at room temperature using a low-cost NIR spectrometer and machine learning techniques. Computers and Electronics in Agriculture, 145, 1–10. doi:10.1016/j.compag.2017.12.030
  • 資料集:Mendeley Data — 6hn67h2trb (v2)
  • Wold, S. et al. (2001). PLS-regression: a basic tool of chemometrics. Chemometrics Intell. Lab. Syst.

本教材附帶檔案(點擊下載)

工具