不只判斷好壞,而是預測「品質數值」
分類模型回答的是「這個樣本屬於哪一類?」回歸模型回答的是「這個樣本的品質指標是多少?」在食品分析中,回歸通常更接近實務決策,因為它能提供連續尺度的風險或品質程度。
分類問題
fresh / acceptable / spoiled
適合快速篩檢與分級。
回歸問題
TVC = 6.2 log CFU/g
適合品質趨勢、保存期限與標準限值判斷。
品質指標
微生物數、FFA、pH、揮發性鹽基態氮、摻偽比例,都可以成為 y。
X,能不能預測牛肉腐敗相關的連續品質指標 y?主範例:用牛肉 e-nose 資料預測 TVC
沿用入門課的牛肉新鮮度資料集,但這次不把 class 當目標,而是用 TVC 作為連續目標值。TVC 是 total viable count,代表微生物總生菌數,是肉品腐敗與食品安全的重要參考。
| 欄位 | 在本章角色 | 注意事項 |
|---|---|---|
MQ1 ... MQ10 | 主要輸入特徵 X | 感測器對腐敗 VOCs 的反應。 |
humidity、temperature | 可選擇加入 X | 比較加入前後是否改善模型;也要思考是否代表環境偏差。 |
TVC | 回歸目標 y | 可能需要 log 轉換或確認單位。 |
class | 輔助解釋 | 這次不是 y,但可用顏色標示預測圖。 |
minute | 輔助觀察 | 不建議直接放入 X,避免模型只記住時間。 |
可延伸資料集
Cocoa butter FFAPork percentageWine spoilage threshold
同樣的回歸流程也可套用到可可脂 FFA 預測、牛/豬混合比例預測,或葡萄酒酸敗程度預測。差別只在於 y 換成不同的品質指標。
回歸模型最怕把「時間」誤當成「氣味」
如果資料依儲存時間排列,TVC 通常也會隨時間上升。這時候如果把 minute 放進模型,模型可能只是在學時間,而不是從氣味指紋預測品質。
建議特徵組 A
只用 MQ sensors。這是最乾淨的電子鼻指紋模型。
建議特徵組 B
MQ sensors + humidity + temperature。測試環境補償是否有效。
不建議一開始使用
minute、class、樣本序號。這些欄位可能造成 data leakage。
PLS regression:化學計量學最常用的品質預測工具
PLS(Partial Least Squares)會尋找一組 latent variables,讓 X 的變化能最大程度解釋 y。它特別適合感測器、光譜、質譜這類變數彼此高度相關的資料。
| 概念 | 意思 |
|---|---|
| Latent variables, LV | 把多個感測器壓縮成少數幾個與品質指標最相關的方向。 |
| 交叉驗證選 LV 數 | LV 太少會 underfit,LV 太多會學到雜訊。 |
| Autoscaling | 感測器尺度不同時,PLS 前通常需要標準化。 |
| Prediction plot | 用 measured vs predicted 檢查模型是否有系統性偏差。 |
用 R²、RMSECV、RMSEP 判斷模型是否真的可用
回歸模型不能只看圖漂亮。你需要用交叉驗證和獨立測試集量化誤差,並確認模型在高品質與低品質區間都沒有明顯偏差。
R²
模型解釋 y 變異的比例。越接近 1 越好,但高 R² 不代表沒有偏差。
RMSECV
交叉驗證誤差。用來選擇 LV 數與比較模型設定。
RMSEP
外部測試集誤差。最接近實際應用表現。
| 檢查項目 | 你要看什麼 |
|---|---|
| Residual plot | 殘差是否隨濃度變大?是否有曲線形狀? |
| LV selection | RMSECV 是否在某個 LV 後不再下降? |
| High TVC region | 腐敗高風險區是否被低估? |
| Train / test split | 切分方式是否符合真實應用情境? |
PLS、SVR、Random Forest regression 的最小完整流程
1. 讀取資料並定義 X、y
import zipfile, requests
from pathlib import Path
import pandas as pd
DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True)
url = "https://data.mendeley.com/public-api/zip/mwmhh766fc/download/2"
zip_path = DATA_DIR / "mendeley_beef_enose.zip"
if not zip_path.exists():
r = requests.get(url, timeout=60)
r.raise_for_status()
zip_path.write_bytes(r.content)
with zipfile.ZipFile(zip_path) as z:
z.extractall(DATA_DIR / "beef_raw")
csv_file = list((DATA_DIR / "beef_raw").rglob("*.csv"))[0]
df = pd.read_csv(csv_file)
print(df.shape)
print(df.columns.tolist())
# 依實際欄位微調:TVC 是本章的連續目標 y
exclude = {"TVC", "class", "minute", "time"}
feature_cols = [c for c in df.columns if c not in exclude]
X = df[feature_cols].apply(pd.to_numeric, errors="coerce")
y = pd.to_numeric(df["TVC"], errors="coerce")
mask = y.notna()
X = X.loc[mask].fillna(X.median(numeric_only=True))
y = y.loc[mask]
print(X.shape, y.describe())
2. 用交叉驗證選擇 PLS 的 LV 數
import numpy as np
from sklearn.model_selection import KFold, cross_val_predict
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.cross_decomposition import PLSRegression
from sklearn.metrics import mean_squared_error, r2_score
cv = KFold(n_splits=5, shuffle=True, random_state=42)
results = []
for n_lv in range(1, min(11, X.shape[1] + 1)):
model = Pipeline([
("scaler", StandardScaler()),
("pls", PLSRegression(n_components=n_lv))
])
y_cv = cross_val_predict(model, X, y, cv=cv).ravel()
rmsecv = mean_squared_error(y, y_cv, squared=False)
r2cv = r2_score(y, y_cv)
results.append((n_lv, rmsecv, r2cv))
res = pd.DataFrame(results, columns=["LV", "RMSECV", "R2CV"])
print(res)
best_lv = int(res.loc[res.RMSECV.idxmin(), "LV"])
print("Best LV:", best_lv)
3. 外部測試集評估
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.30, random_state=42
)
pls = Pipeline([
("scaler", StandardScaler()),
("pls", PLSRegression(n_components=best_lv))
])
pls.fit(X_train, y_train)
y_pred = pls.predict(X_test).ravel()
rmsep = mean_squared_error(y_test, y_pred, squared=False)
r2p = r2_score(y_test, y_pred)
print(f"RMSEP = {rmsep:.3f}")
print(f"R2 test = {r2p:.3f}")
plt.figure(figsize=(5.5,5.5))
plt.scatter(y_test, y_pred, alpha=0.8)
lo = min(y_test.min(), y_pred.min())
hi = max(y_test.max(), y_pred.max())
plt.plot([lo, hi], [lo, hi], "--", color="gray")
plt.xlabel("Measured TVC")
plt.ylabel("Predicted TVC")
plt.title("PLS regression: measured vs predicted")
plt.tight_layout()
plt.show()
4. 與非線性模型比較
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
models = {
"PLS": Pipeline([("scaler", StandardScaler()), ("pls", PLSRegression(n_components=best_lv))]),
"SVR-RBF": Pipeline([("scaler", StandardScaler()), ("svr", SVR(C=10, gamma="scale"))]),
"Random Forest": RandomForestRegressor(n_estimators=300, random_state=42)
}
for name, model in models.items():
model.fit(X_train, y_train)
pred = np.asarray(model.predict(X_test)).ravel()
print(name, "RMSEP", mean_squared_error(y_test, pred, squared=False), "R2", r2_score(y_test, pred))
用 Orange 建立品質指標預測流程
Orange 可以做 regression。重點是在 Select Columns 中把 TVC 設成 Target,而不是把 class 設成 Target。
File
→ Select Columns
Features: MQ sensor responses, humidity, temperature
Target: TVC
Meta: class, minute
→ Preprocess
Impute / Standardize
→ Test and Score
← Linear Regression
← Random Forest
← Support Vector Regression
← PLS Regression(若已安裝相關 add-on;否則用 Python 做)
→ Predictions
→ Scatter Plot
x = measured TVC, y = predicted TVC
→ Data Table
完成本章後,你應該能回答這些問題
觀念題
- 分類與回歸在食品品質分析中差在哪裡?
- 為什麼 PLS 適合感測器陣列資料?
- LV 太多會造成什麼問題?
- 為什麼
minute可能造成 data leakage?
結果解讀題
- Measured vs predicted 圖是否接近對角線?
- RMSECV 最小時的 LV 數是多少?
- 高 TVC 區域是否被系統性低估?
- PLS、SVR、Random Forest 哪個最適合這組資料?為什麼?