WeHelp
PyTorch 可以應用在迴歸預測模型、分類預測模型、影像辨識、物件偵測、生成式模型等等領域。
  1. 房屋價格預測
  2. 乳癌良性 / 惡性分類
  3. 紅酒產地分類
  4. CNN 影像辨識
  5. R-CNN 物件偵測
  6. GAN 生成對抗網路
乳癌良性 / 惡性分類
以下以 **乳癌良性/惡性分類** 為例,使用 scikit-learn 內建的 Breast Cancer Wisconsin 資料集,利用 PyTorch 建立一個多層感知器(MLP)進行二元分類。 > 注意:此案例僅用於示範機器學習流程,不應直接作為醫療診斷工具。 --- ## 1. 案例與資料 資料集包含: - 569 筆樣本 - 30 個數值特徵 - 分類目標: - 0:良性 - 1:惡性 原始資料集的標籤為: - `0`:malignant - `1`:benign 為了讓「1」代表惡性,以下程式會重新轉換標籤。 --- ## 2. 完整 PyTorch 程式碼 ```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_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix ) # -------------------------------------------------- # 1. 設定隨機種子,讓結果較容易重現 # -------------------------------------------------- SEED = 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) if torch.cuda.is_available(): torch.cuda.manual_seed_all(SEED) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("使用裝置:", device) # -------------------------------------------------- # 2. 載入資料 # -------------------------------------------------- data = load_breast_cancer() X = data.data.astype(np.float32) # 原始資料:0 = 惡性,1 = 良性 # 重新定義為:1 = 惡性,0 = 良性 y = (data.target == 0).astype(np.float32) print("特徵數量:", X.shape[1]) print("樣本數量:", X.shape[0]) # -------------------------------------------------- # 3. 切分訓練集、驗證集、測試集 # -------------------------------------------------- X_train_val, X_test, y_train_val, y_test = train_test_split( X, y, test_size=0.2, random_state=SEED, stratify=y ) X_train, X_val, y_train, y_val = train_test_split( X_train_val, y_train_val, test_size=0.2, random_state=SEED, stratify=y_train_val ) # -------------------------------------------------- # 4. 特徵標準化 # 只能使用訓練集 fit,避免資料洩漏 # -------------------------------------------------- scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val) X_test = scaler.transform(X_test) # -------------------------------------------------- # 5. 建立 DataLoader # -------------------------------------------------- def create_dataloader(X, y, batch_size=32, shuffle=False): X_tensor = torch.tensor(X, dtype=torch.float32) y_tensor = torch.tensor(y, dtype=torch.float32).view(-1, 1) dataset = TensorDataset(X_tensor, y_tensor) return DataLoader( dataset, batch_size=batch_size, shuffle=shuffle ) train_loader = create_dataloader( X_train, y_train, batch_size=32, shuffle=True ) val_loader = create_dataloader( X_val, y_val, batch_size=32, shuffle=False ) test_loader = create_dataloader( X_test, y_test, batch_size=32, shuffle=False ) # -------------------------------------------------- # 6. 建立二元分類模型 # -------------------------------------------------- class BinaryClassifier(nn.Module): def __init__(self, input_dim): super().__init__() self.network = nn.Sequential( nn.Linear(input_dim, 32), nn.ReLU(), nn.Dropout(0.2), nn.Linear(32, 16), nn.ReLU(), nn.Dropout(0.2), # 輸出一個 logit nn.Linear(16, 1) ) def forward(self, x): return self.network(x) model = BinaryClassifier(input_dim=X_train.shape[1]).to(device) # -------------------------------------------------- # 7. 損失函數與最佳化器 # -------------------------------------------------- # BCEWithLogitsLoss 內部會自動處理 sigmoid, # 數值穩定性比 Sigmoid + BCELoss 更好 criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam( model.parameters(), lr=0.001, weight_decay=1e-4 ) # -------------------------------------------------- # 8. 評估函式 # -------------------------------------------------- def evaluate(model, dataloader, criterion, device): model.eval() total_loss = 0.0 all_labels = [] all_probs = [] with torch.no_grad(): for features, labels in dataloader: features = features.to(device) labels = labels.to(device) logits = model(features) loss = criterion(logits, labels) total_loss += loss.item() * features.size(0) # logits 經 sigmoid 後才是機率 probs = torch.sigmoid(logits) all_labels.extend(labels.cpu().numpy().ravel()) all_probs.extend(probs.cpu().numpy().ravel()) avg_loss = total_loss / len(dataloader.dataset) all_labels = np.array(all_labels) all_probs = np.array(all_probs) all_preds = (all_probs >= 0.5).astype(int) metrics = { "loss": avg_loss, "accuracy": accuracy_score(all_labels, all_preds), "precision": precision_score( all_labels, all_preds, zero_division=0 ), "recall": recall_score( all_labels, all_preds, zero_division=0 ), "f1": f1_score( all_labels, all_preds, zero_division=0 ), "auc": roc_auc_score(all_labels, all_probs) } return metrics # -------------------------------------------------- # 9. 訓練模型 # -------------------------------------------------- epochs = 100 best_val_auc = 0.0 best_model_state = None for epoch in range(1, epochs + 1): model.train() total_train_loss = 0.0 for features, labels in train_loader: features = features.to(device) labels = labels.to(device) # 清除上一個 batch 的梯度 optimizer.zero_grad() # 前向傳播 logits = model(features) # 計算損失 loss = criterion(logits, labels) # 反向傳播 loss.backward() # 更新模型參數 optimizer.step() total_train_loss += loss.item() * features.size(0) train_loss = total_train_loss / len(train_loader.dataset) val_metrics = evaluate( model, val_loader, criterion, device ) # 儲存驗證集 AUC 最好的模型 if val_metrics["auc"] > best_val_auc: best_val_auc = val_metrics["auc"] best_model_state = copy.deepcopy(model.state_dict()) if epoch == 1 or epoch % 10 == 0: print( f"Epoch [{epoch:03d}/{epochs}] " f"Train Loss: {train_loss:.4f} | " f"Val Loss: {val_metrics['loss']:.4f} | " f"Val Acc: {val_metrics['accuracy']:.4f} | " f"Val AUC: {val_metrics['auc']:.4f}" ) # -------------------------------------------------- # 10. 載入驗證集表現最好的模型 # -------------------------------------------------- model.load_state_dict(best_model_state) # -------------------------------------------------- # 11. 在測試集上評估 # -------------------------------------------------- test_metrics = evaluate( model, test_loader, criterion, device ) print("\n測試集結果") print("-------------------------") for name, value in test_metrics.items(): print(f"{name:10s}: {value:.4f}") # 另外顯示混淆矩陣 model.eval() all_labels = [] all_probs = [] with torch.no_grad(): for features, labels in test_loader: features = features.to(device) logits = model(features) probs = torch.sigmoid(logits) all_labels.extend(labels.numpy().ravel()) all_probs.extend(probs.cpu().numpy().ravel()) all_labels = np.array(all_labels).astype(int) all_preds = (np.array(all_probs) >= 0.5).astype(int) print("\n混淆矩陣") print(confusion_matrix(all_labels, all_preds)) ``` --- ## 3. 關鍵步驟說明 ### 3.1 資料切分 資料被分為: - 訓練集:用來更新模型參數 - 驗證集:用來選擇最佳模型與調整超參數 - 測試集:最後才使用,用來估計模型的泛化能力 使用 `stratify=y` 可以讓各資料集中的良性/惡性比例大致一致,避免切分後類別比例失衡。 --- ### 3.2 特徵標準化 不同特徵可能具有不同量級,例如: - 特徵 A 範圍為 0~1 - 特徵 B 範圍為 0~10,000 若不標準化,神經網路訓練可能較不穩定。因此使用: ```python scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val) X_test = scaler.transform(X_test) ``` 重要的是: ```python scaler.fit_transform(X_train) ``` 只能對訓練集執行 `fit`,驗證集與測試集只能使用: ```python scaler.transform(...) ``` 這樣可以避免測試資料資訊洩漏到模型訓練過程。 --- ### 3.3 模型結構 本例的模型為: ```text 30 個輸入特徵 ↓ Linear(30 → 32) ↓ ReLU ↓ Dropout(0.2) ↓ Linear(32 → 16) ↓ ReLU ↓ Dropout(0.2) ↓ Linear(16 → 1) ``` 最後一層輸出的是 **logit**,不是直接的機率。 --- ### 3.4 為什麼使用 `BCEWithLogitsLoss` 二元分類常見的損失函數是 Binary Cross Entropy: ```python nn.BCELoss() ``` 但如果自行使用: ```python sigmoid + BCELoss ``` 在數值上可能不如直接使用: ```python nn.BCEWithLogitsLoss() ``` `BCEWithLogitsLoss` 已經將 sigmoid 與 binary cross entropy 合併,通常具有較好的數值穩定性。 因此模型的 `forward()` 不需要加入 sigmoid: ```python logits = model(features) loss = criterion(logits, labels) ``` 只有在取得預測機率時,才使用: ```python probs = torch.sigmoid(logits) ``` --- ### 3.5 二元分類預測方式 本例使用 0.5 作為分類門檻: ```python preds = (probs >= 0.5).astype(int) ``` 也就是: - 預測機率 ≥ 0.5:判定為惡性 - 預測機率 < 0.5:判定為良性 在醫療或高風險情境中,門檻不一定要固定為 0.5。若希望降低漏診,可以降低門檻,例如設定為 0.3,以提高 Recall,但可能增加誤報。 --- ## 4. 評估指標 程式計算以下指標: ### Accuracy 整體預測正確的比例: ```text 正確預測數 / 總樣本數 ``` 適合類別比例較平衡的情況。 ### Precision 被模型預測為惡性的樣本中,實際為惡性的比例: ```text TP / (TP + FP) ``` ### Recall 所有實際為惡性的樣本中,成功被模型找出的比例: ```text TP / (TP + FN) ``` 在癌症篩檢中,Recall 通常相當重要,因為漏掉惡性病例的代價可能較高。 ### F1-score Precision 與 Recall 的調和平均: ```text 2 × Precision × Recall / (Precision + Recall) ``` 適合在 Precision 與 Recall 都重要時使用。 ### ROC-AUC 衡量模型區分正負類別的能力,且不依賴單一分類門檻。通常: - 0.5:接近隨機猜測 - 0.7 以上:有一定區分能力 - 0.8 以上:通常算不錯 - 1.0:完美區分 --- ## 5. 主要訓練設定 本例的設定如下: | 設定 | 值 | |---|---:| | Batch size | 32 | | Epochs | 100 | | Optimizer | Adam | | Learning rate | 0.001 | | Weight decay | 0.0001 | | 隱藏層 | 32、16 | | Dropout | 0.2 | | 分類門檻 | 0.5 | | 損失函數 | BCEWithLogitsLoss | 訓練過程中會根據驗證集的 AUC 儲存最佳模型,而不是直接使用最後一個 epoch 的模型。這可以降低模型過度擬合的風險。 --- ## 6. 實務上可進一步改善的方向 1. **加入 Early Stopping** 若驗證集表現連續多個 epoch 沒有改善,就提早停止訓練。 2. **調整分類門檻** 根據實際需求在 Precision 與 Recall 之間取捨。 3. **處理類別不平衡** 可以在 `BCEWithLogitsLoss` 中使用 `pos_weight`。 4. **使用交叉驗證** 當資料量較少時,可以用 K-fold cross-validation 評估模型穩定性。 5. **保存模型與標準化器** ```python torch.save(model.state_dict(), "binary_classifier.pt") ``` 同時也應保存 `scaler`,以確保日後新資料使用相同的標準化方式。
相關學習地圖、教學課程
Python 人工智慧
建議完成「Python 資料工程」教程後,繼續學習以下課程。