紅酒產地分類
以下示範一個簡單的「紅酒產地分類」案例:根據紅酒的化學分析資料,預測其產地類別。
> 使用的 Wine Dataset 來自 scikit-learn,共有 3 種紅酒產地、13 個化學特徵與 178 筆資料。這是一個適合示範多分類模型的入門資料集。
---
## 一、問題定義
### 輸入特徵
每筆紅酒資料包含 13 個化學特徵,例如:
- Alcohol
- Malic acid
- Ash
- Alcalinity of ash
- Magnesium
- Total phenols
- Flavanoids
- Nonflavanoid phenols
- Proanthocyanins
- Color intensity
- Hue
- OD280/OD315 of diluted wines
- Proline
### 預測目標
將紅酒分類為 3 種產地:
```text
Class 0
Class 1
Class 2
```
這是一個多分類問題,因此模型最後輸出 3 個類別分數。
---
# 二、完整 PyTorch 實作
## 1. 載入資料與前處理
```python
import copy
import random
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix
)
```
設定隨機種子:
```python
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# 讓結果較容易重現
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
set_seed(42)
```
載入資料:
```python
wine = load_wine()
X = wine.data.astype(np.float32)
y = wine.target.astype(np.int64)
print("特徵形狀:", X.shape)
print("標籤形狀:", y.shape)
print("類別名稱:", wine.target_names)
```
可能輸出:
```text
特徵形狀: (178, 13)
標籤形狀: (178,)
類別名稱: ['class_0' 'class_1' 'class_2']
```
---
## 2. 切分訓練集、驗證集與測試集
先分出測試集,再從剩餘資料中分出驗證集:
```python
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=42
)
X_train, X_val, y_train, y_val = train_test_split(
X_train,
y_train,
test_size=0.2,
stratify=y_train,
random_state=42
)
print("訓練集:", X_train.shape)
print("驗證集:", X_val.shape)
print("測試集:", X_test.shape)
```
這裡使用 `stratify=y`,確保三種產地在訓練、驗證和測試資料中的比例大致相同。
---
## 3. 特徵標準化
由於不同特徵的數值範圍差異很大,例如:
- Alcohol 約為 12~14
- Proline 可能超過 1000
因此需要進行標準化。
重要的是:`StandardScaler` 只能使用訓練集計算平均值與標準差,避免資料洩漏。
```python
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_val = scaler.transform(X_val)
X_test = scaler.transform(X_test)
```
將 NumPy 陣列轉成 PyTorch Tensor:
```python
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
X_val_tensor = torch.tensor(X_val, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val, dtype=torch.long)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test, dtype=torch.long)
```
建立資料載入器:
```python
batch_size = 16
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
train_loader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True
)
val_loader = DataLoader(
val_dataset,
batch_size=batch_size,
shuffle=False
)
test_loader = DataLoader(
test_dataset,
batch_size=batch_size,
shuffle=False
)
```
---
# 三、建立多分類神經網路
這裡使用一個簡單的多層感知器,也就是 MLP。
架構如下:
```text
13 個輸入特徵
↓
Linear(13, 32)
↓
ReLU
↓
Dropout
↓
Linear(32, 16)
↓
ReLU
↓
Linear(16, 3)
```
```python
class WineClassifier(nn.Module):
def __init__(self, input_dim=13, num_classes=3):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, 32),
nn.ReLU(),
nn.Dropout(p=0.2),
nn.Linear(32, 16),
nn.ReLU(),
nn.Linear(16, num_classes)
)
def forward(self, x):
return self.network(x)
```
建立模型:
```python
device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
model = WineClassifier(
input_dim=X_train.shape[1],
num_classes=3
).to(device)
print(model)
```
---
# 四、設定損失函數與最佳化器
## 1. 損失函數
多分類問題使用:
```python
nn.CrossEntropyLoss()
```
模型的最後一層只需要輸出 logits,不要自行加上 `Softmax`,因為 `CrossEntropyLoss` 內部已經包含了相應的計算。
```python
criterion = nn.CrossEntropyLoss()
```
## 2. 最佳化器
使用 Adam:
```python
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.001,
weight_decay=1e-4
)
```
主要設定:
| 設定 | 值 | 說明 |
|---|---:|---|
| Optimizer | Adam | 適合小型神經網路 |
| Learning rate | 0.001 | 每次參數更新幅度 |
| Weight decay | 1e-4 | 輕微的 L2 正則化 |
| Batch size | 16 | 每次使用 16 筆樣本 |
| Loss | CrossEntropyLoss | 多分類損失 |
---
# 五、撰寫訓練與評估函式
## 訓練函式
```python
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0.0
total_correct = 0
total_samples = 0
for X_batch, y_batch in loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
optimizer.zero_grad()
logits = model(X_batch)
loss = criterion(logits, y_batch)
loss.backward()
optimizer.step()
total_loss += loss.item() * X_batch.size(0)
predictions = torch.argmax(logits, dim=1)
total_correct += (predictions == y_batch).sum().item()
total_samples += X_batch.size(0)
avg_loss = total_loss / total_samples
accuracy = total_correct / total_samples
return avg_loss, accuracy
```
## 評估函式
```python
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0.0
total_correct = 0
total_samples = 0
all_predictions = []
all_labels = []
with torch.inference_mode():
for X_batch, y_batch in loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
logits = model(X_batch)
loss = criterion(logits, y_batch)
total_loss += loss.item() * X_batch.size(0)
predictions = torch.argmax(logits, dim=1)
total_correct += (predictions == y_batch).sum().item()
total_samples += X_batch.size(0)
all_predictions.extend(predictions.cpu().numpy())
all_labels.extend(y_batch.cpu().numpy())
avg_loss = total_loss / total_samples
accuracy = total_correct / total_samples
return (
avg_loss,
accuracy,
np.array(all_labels),
np.array(all_predictions)
)
```
---
# 六、訓練模型
這裡訓練 100 個 epoch,並保存驗證集表現最佳的模型。
```python
num_epochs = 100
best_val_loss = float("inf")
best_model_state = None
history = {
"train_loss": [],
"train_acc": [],
"val_loss": [],
"val_acc": []
}
for epoch in range(num_epochs):
train_loss, train_acc = train_one_epoch(
model,
train_loader,
criterion,
optimizer,
device
)
val_loss, val_acc, _, _ = evaluate(
model,
val_loader,
criterion,
device
)
history["train_loss"].append(train_loss)
history["train_acc"].append(train_acc)
history["val_loss"].append(val_loss)
history["val_acc"].append(val_acc)
# 儲存驗證集損失最低的模型
if val_loss < best_val_loss:
best_val_loss = val_loss
best_model_state = copy.deepcopy(model.state_dict())
if (epoch + 1) % 10 == 0:
print(
f"Epoch [{epoch+1:3d}/{num_epochs}] | "
f"Train Loss: {train_loss:.4f}, "
f"Train Acc: {train_acc:.4f} | "
f"Val Loss: {val_loss:.4f}, "
f"Val Acc: {val_acc:.4f}"
)
```
訓練結束後,載入驗證集表現最好的模型:
```python
model.load_state_dict(best_model_state)
```
---
# 七、在測試集上評估
```python
test_loss, test_acc, y_true, y_pred = evaluate(
model,
test_loader,
criterion,
device
)
print(f"\nTest Loss: {test_loss:.4f}")
print(f"Test Accuracy: {test_acc:.4f}")
```
顯示分類報告:
```python
print("\nClassification Report:")
print(
classification_report(
y_true,
y_pred,
target_names=wine.target_names,
digits=4
)
)
```
顯示混淆矩陣:
```python
cm = confusion_matrix(y_true, y_pred)
print("Confusion Matrix:")
print(cm)
```
這個資料集通常可以得到約 90%~100% 的測試準確率,但實際結果會受:
- 隨機切分方式
- 隨機種子
- 網路結構
- 訓練 epoch 數
- 學習率
等因素影響。
---
# 八、單筆資料預測
假設有一筆新的紅酒化學分析資料:
```python
new_wine = np.array([[
13.2, # Alcohol
1.78, # Malic acid
2.14, # Ash
11.2, # Alcalinity of ash
100.0, # Magnesium
2.65, # Total phenols
2.76, # Flavanoids
0.26, # Nonflavanoid phenols
1.28, # Proanthocyanins
4.38, # Color intensity
1.05, # Hue
3.40, # OD280/OD315
1050.0 # Proline
]], dtype=np.float32)
```
必須使用訓練階段的 scaler 進行轉換:
```python
new_wine_scaled = scaler.transform(new_wine)
new_wine_tensor = torch.tensor(
new_wine_scaled,
dtype=torch.float32
).to(device)
```
模型預測:
```python
model.eval()
with torch.inference_mode():
logits = model(new_wine_tensor)
probabilities = torch.softmax(logits, dim=1)
predicted_class = torch.argmax(probabilities, dim=1).item()
print("預測類別:", wine.target_names[predicted_class])
print("各類別機率:", probabilities.cpu().numpy())
```
`softmax` 適合在推論階段將 logits 轉成機率,但訓練時不應在模型最後額外加入 `Softmax`。
---
# 九、關鍵步驟與設定說明
## 1. 使用分層切分
```python
stratify=y
```
確保每個資料子集都包含三種產地,避免某一類在驗證集或測試集中比例過低。
## 2. 避免資料洩漏
正確方式:
```python
scaler.fit(X_train)
scaler.transform(X_val)
scaler.transform(X_test)
```
不能將全部資料一起 `fit_transform`,否則測試資料的統計資訊會被模型間接使用。
## 3. 使用 `CrossEntropyLoss`
對於三分類問題,標籤格式應為:
```text
0、1、2
```
而不是 one-hot 向量。模型輸出形狀為:
```text
[batch_size, 3]
```
例如:
```text
[[ 1.2, -0.3, 0.8],
[ 0.1, 2.4, -1.0]]
```
## 4. 不在模型中加入 Softmax
模型最後一層:
```python
nn.Linear(16, 3)
```
即可。因為:
```python
nn.CrossEntropyLoss()
```
已經會處理 logits 與類別機率之間的轉換。
## 5. 使用驗證集選擇最佳模型
不要直接選擇最後一個 epoch 的模型,而是根據驗證集損失保存最佳模型,能降低過度擬合的風險。
## 6. Dropout 與 Weight Decay
資料集只有 178 筆,樣本數不大,因此使用:
```python
nn.Dropout(0.2)
```
以及:
```python
weight_decay=1e-4
```
來增加正則化效果。
---
# 十、可進一步改善的方向
如果要將此案例拓展到實際應用,可以考慮:
1. 使用交叉驗證,而不只進行一次資料切分。
2. 調整隱藏層大小與學習率。
3. 加入 Early Stopping。
4. 繪製訓練與驗證損失曲線。
5. 使用 Precision、Recall、F1-score 評估各產地的分類效果。
6. 進行特徵重要性分析。
7. 與 Logistic Regression、Random Forest、SVM 等模型比較。
8. 對產地標籤進行實際商業定義與資料品質檢查。
總結而言,這個案例的核心流程是:
```text
載入資料
→ 分層切分資料
→ 只用訓練集進行標準化
→ 建立 PyTorch MLP
→ 使用 CrossEntropyLoss 訓練
→ 以驗證集選擇最佳模型
→ 使用測試集計算 Accuracy、F1-score 與混淆矩陣
```
相關學習地圖、教學課程
Python 人工智慧