用電子鼻,聞出食品品質

這一章會帶你用真實的 electronic nose 食品資料,從感測器數字開始,做 PCA 分群,再建立分類模型判斷食品品質。

10MOS gas sensors
4553牛肉新鮮度資料列
PCA先看資料結構
ML再做品質分類
先抓住大方向

電子鼻不是「聞到某一種味道」,而是記錄一組氣味指紋

想像你把一塊牛肉放進密閉容器中。牛肉會釋放出許多揮發性化合物,尤其在變質過程中,氣味會明顯改變。電子鼻會用一排氣體感測器同時量測這些變化。

每個感測器都不是完全專一的。它可能同時對酒精、硫化物、胺類或其他 VOCs 有反應。這聽起來像缺點,但多個感測器合在一起時,就會形成一個模式。這個模式就是我們要分析的「氣味指紋」。

食品樣本 牛肉 / 酒 / 水果 釋放 VOCs 感測器陣列 資料分析 PCA / 分類模型 新鮮 / 可接受 / 腐敗
電子鼻把食品氣味轉成一列數字。接下來的工作,是用化學計量學讀懂這列數字。

你會看到什麼

一張資料表:每一列是一個量測時間點,每一欄是一個感測器或環境變數。

你要先做什麼

把資料整理成 XyX 是感測器反應,y 是品質類別。

你最後要回答什麼

只看電子鼻訊號,我們能不能判斷牛肉是新鮮、可接受,還是已經腐敗?
本章資料

牛肉新鮮度 electronic nose dataset

我們使用公開資料集:Electronic nose dataset for beef quality monitoring under an uncontrolled environment。研究者用 MOS 氣體感測器監測牛肉在儲存過程中的氣味變化,並提供品質類別與微生物數量。

4553
筆量測資料
10
個 MOS 氣體感測器
TVC
微生物總生菌數
CSV
可整理後進 Python / Orange

資料表怎麼讀?

先把資料想成這樣:

minute | class | TVC | MQ1 | MQ2 | MQ3 | ... | MQ10 | humidity | temperature
欄位意思這一章怎麼用
minute量測時間先保留觀察,但不要直接放進分類模型。否則模型可能只是在記住時間。
class品質類別這是我們要預測的答案,也就是 y
TVC微生物數量本章先不用。之後可以拿來做 PLS regression。
MQ1 ... MQ10氣體感測器反應這是主要輸入變數,也就是 X
humiditytemperature濕度與溫度可以納入模型,也可以先拿掉,比較結果是否改變。
先記住:在分類問題中,答案欄位不能放進輸入變數。也就是說,class 不能出現在 X 裡。

另一個很適合練習的資料:牛肉中豬肉摻偽

如果你對食品真偽分析有興趣,可以再看這個資料集:The Dataset for Pork Adulteration from Electronic Nose System。它有七個牛肉 / 豬肉混合比例,從 100% beef 到 100% pork。這種資料很適合用 PCA 看比例梯度,也適合用 PLS-DA 做摻偽判別。

食品真偽摻偽檢測halal authenticationPLS-DA

資料前處理

先整理資料,再開始建模

電子鼻資料不能一拿到就丟進模型。不同感測器的數值範圍可能差很多,有些欄位也不是模型應該看的資訊。前處理做得不好,模型看起來可能很準,但其實學到的是錯的東西。

第 1 步:決定 X 和 y

X 放感測器反應;y 放品質類別。不要把 classTVCminute 混進 X。

第 2 步:檢查缺失值

如果某些欄位有空值,可以刪除、補中位數,或回到原始資料確認原因。

第 3 步:標準化

對每一個 sensor 欄位做 autoscaling,讓不同尺度的感測器可以公平比較。
\[ z_{ij}=\frac{x_{ij}-\bar{x}_{j}}{s_j} \]

這個公式的意思是:對每一個感測器欄位,先減掉平均值,再除以標準差。做完後,每個變數的平均值會接近 0,標準差會接近 1。

為什麼要標準化?如果某個 sensor 的數值範圍天生比較大,PCA 可能會被它主導。標準化可以避免「數值大」被誤解成「比較重要」。

如果你拿到的是原始時間序列

有些 e-nose 資料不是一列代表一個樣本,而是一個樣本有一整段反應曲線。這時候可以先把曲線轉成特徵,再分析。

從曲線取出的特徵你可以怎麼理解
baseline放入樣本前的背景反應。
maximum response感測器反應最高點。
steady-state mean最後一段時間的平均反應。
AUC整段反應曲線下面積,代表總反應量。
initial slope反應剛開始上升的速度。
第一個分析

用 PCA 先看資料長什麼樣子

PCA 是非監督式方法。它不看 class,只看感測器資料 X。如果不同品質的樣本在 PCA 圖上自然分開,代表電子鼻訊號中真的有品質差異的資訊。

\[ X \approx T P^\top \]

Scores:樣本的位置

每一個點是一筆樣本。你可以看 fresh、acceptable、spoiled 是否聚在不同區域。

Loadings:感測器的貢獻

哪個 sensor 對 PC1 或 PC2 影響大,就可能是造成樣本分離的重要訊號。
PC1 PC2 fresh acceptable spoiled
如果 fresh 和 spoiled 沿著 PC1 分開,PC1 很可能和腐敗過程中的 VOC 變化有關。
看 PCA 圖時,請試著回答
  1. 不同品質類別有沒有分開?還是混在一起?
  2. 有沒有離群點?它可能是量測錯誤,還是真的特殊樣本?
  3. PC1 可能代表什麼?腐敗程度、時間、溫度,還是其他因素?
  4. 如果 humidity 在 loading 中很重要,你會把它視為有用資訊還是干擾?
第二個分析

建立分類模型:讓電腦判斷食品品質

PCA 幫我們看出資料結構,但 PCA 本身不是分類器。若要預測一個未知樣本是 fresh 還是 spoiled,需要建立 supervised classification model。

kNN

看未知樣本附近的鄰居屬於哪一類。概念最直覺。

SVM

尋找能分開類別的邊界。常用於感測器與光譜資料。

Random Forest

由多棵決策樹投票。可以觀察哪些變數重要。

PLS-DA

化學計量學常用的分類方法。用 latent variables 連結 X 與類別 y。

不要只看 accuracy

食品安全問題中,錯誤的代價不一樣。把 fresh 判成 spoiled 可能造成浪費;把 spoiled 判成 fresh 可能造成健康風險。因此你一定要看 confusion matrix。

真實情況模型預測可能後果
freshspoiled食品被丟掉,造成浪費。
spoiledfresh食品安全風險最高,這種錯誤要特別避免。
判讀重點:如果模型整體 accuracy 很高,但 spoiled 類別的 recall 很低,這個模型在食品安全上仍然不可靠。
Python 實作

一步一步做出 PCA 與分類模型

下面的程式碼可以放進 Jupyter Notebook。你可以先跑通流程,再慢慢修改特徵欄位、分類器與驗證方式。

1. 下載與讀取資料

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_files = list((DATA_DIR / "beef_raw").rglob("*.csv"))
df = pd.read_csv(csv_files[0])
print(df.shape)
df.head()

2. 定義 X 和 y

print(df.columns.tolist())

label_col = "class"
exclude = {"class", "TVC", "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 = df[label_col]

X = X.fillna(X.median(numeric_only=True))
print(X.shape)
print(y.value_counts())

3. PCA score plot

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import seaborn as sns

X_scaled = StandardScaler().fit_transform(X)
pca = PCA(n_components=2, random_state=42)
T = pca.fit_transform(X_scaled)

plt.figure(figsize=(7,5))
sns.scatterplot(x=T[:,0], y=T[:,1], hue=y, s=45, alpha=0.85)
plt.xlabel(f"PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)")
plt.ylabel(f"PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)")
plt.title("PCA score plot of beef e-nose data")
plt.tight_layout()
plt.show()

4. 訓練分類模型

from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report, ConfusionMatrixDisplay
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.30, stratify=y, random_state=42
)

models = {
    "SVM-RBF": Pipeline([
        ("scaler", StandardScaler()),
        ("clf", SVC(kernel="rbf", C=10, gamma="scale"))
    ]),
    "Random Forest": RandomForestClassifier(n_estimators=300, random_state=42)
}

for name, model in models.items():
    model.fit(X_train, y_train)
    pred = model.predict(X_test)
    print("\n===", name, "===")
    print(classification_report(y_test, pred))
    ConfusionMatrixDisplay.from_predictions(y_test, pred, xticks_rotation=45)
    plt.title(name)
    plt.tight_layout()
    plt.show()
如果你想試 PLS-DA
from sklearn.cross_decomposition import PLSRegression
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.metrics import accuracy_score, confusion_matrix

le = LabelEncoder()
y_int = le.fit_transform(y)
Y = OneHotEncoder(sparse_output=False).fit_transform(y_int.reshape(-1, 1))

Xs = StandardScaler().fit_transform(X)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

y_score = cross_val_predict(PLSRegression(n_components=2), Xs, Y, cv=cv)
y_pred = y_score.argmax(axis=1)

print("CV accuracy:", accuracy_score(y_int, y_pred))
print(confusion_matrix(y_int, y_pred))
print(le.classes_)
Orange Data Mining

不用寫太多程式,也能完成同樣流程

Orange 的好處是你可以直接看到資料流。每一個 widget 都對應到一個分析步驟:讀資料、選欄位、標準化、PCA、模型比較、混淆矩陣。

PCA 流程

File
  → Select Columns
      Features: MQ sensor responses, humidity, temperature
      Target: class
      Meta: minute, TVC
  → Preprocess
      Impute / Standardize
  → PCA
  → Scatter Plot
      x = PC1, y = PC2, color = class
  → Data Table

分類流程

File
  → Select Columns
  → Preprocess
  → Test and Score
       ← kNN
       ← Logistic Regression
       ← SVM
       ← Random Forest
  → Confusion Matrix
  → ROC Analysis

試一次:只用感測器

Features 只放 MQ sensor responses。記下 PCA 分群與分類結果。

再試一次:加入溫濕度

把 humidity 和 temperature 加入 Features。看看結果變好還是變差,並思考原因。
檢查自己

完成後,你應該能回答這些問題

觀念題

  1. 為什麼電子鼻需要多變量分析?
  2. PCA 為什麼是非監督式方法?
  3. autoscaling 對 PCA 有什麼影響?
  4. 為什麼不能把 class 放進 X

結果解讀題

  1. PCA score plot 中,fresh 和 spoiled 是否分開?
  2. 哪一個分類模型表現最好?你怎麼判斷?
  3. confusion matrix 中,哪一種錯誤最危險?
  4. 加入溫濕度後,模型是否更可靠?為什麼?
下一章:如果你已經會把樣本分成 fresh / spoiled,可以進入 中階課:Regression 與品質指標預測,直接預測 TVC、FFA 或摻偽比例這類連續品質指標。若你拿到的是原始感測器曲線,請接著看 進階課:Time-series Feature Engineering
最後一句話:電子鼻給我們的是一組感測器指紋;PCA 幫你看出結構,分類模型幫你把結構轉成判斷。真正重要的是,你要能解釋模型為什麼這樣判斷,以及這個判斷在食品安全上代表什麼。