這段程式做的事是:拿乳癌資料,先標準化、再降維、最後分類,然後用 5 折交叉驗證評分。它藏了一個最容易被扣分的觀念——為什麼前處理一定要放進 Pipeline,而不能先對整份資料做。
使用 scikit-learn 的 Breast Cancer 資料集評估 StandardScaler、PCA 與 LogisticRegression 的組合。下列寫法將前處理放入 Pipeline,再交由交叉驗證執行。
1 from sklearn.datasets import load_breast_cancer 2 from sklearn.decomposition import PCA 3 from sklearn.linear_model import LogisticRegression 4 from sklearn.model_selection import StratifiedKFold, cross_val_score 5 from sklearn.pipeline import Pipeline 6 from sklearn.preprocessing import StandardScaler 7 8 X, y = load_breast_cancer(return_X_y=True) 9 10 pipe = Pipeline([ 11 ("scale", StandardScaler()), 12 ("pca", PCA(n_components=10)), 13 ("clf", LogisticRegression(max_iter=2000)) 14 ]) 15 16 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) 17 scores = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")
相較於先在完整 X 上執行 scaler.fit_transform(X) 與 pca.fit_transform(X),上述 Pipeline 在交叉驗證中最重要的優點為何?
點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
17 行,前 6 行是把工具拿進來,第 8 行載入資料,第 10–14 行組裝流程,第 16 行定義切法,第 17 行才真的開始跑。
.這是物件導向的寫法。scikit-learn 整套都建立在它上面——先做出一台機器,再叫這台機器做事,而「叫它做事」就是靠這個點。
sklearn.datasets 點的左邊是模組 唸「裡面的」 scaler.fit(X) 點的左邊是物件 唸「它的」 scaler.fit(X) 有括號 = 方法,叫它做一件事 X.shape 沒括號 = 屬性,看一格資料
這六行的句型完全一樣:from 模組 import 名字,差別只在拿的是哪個東西。import 本身不執行任何運算,它只是把工具擺到桌上,讓你之後可以直接叫它的名字。
from sklearn.datasets import load_breast_cancer
sklearn.datasets 是 scikit-learn 收錄示範資料集的模組,裡面還有 load_iris(鳶尾花)、load_digits(手寫數字)、make_classification(自己生一份假資料)。這行只是把 load_breast_cancer 這個函式拿到手上,資料一筆都還沒讀進來——真正讀取是第 8 行。
名字全小寫加底線(load_breast_cancer)是 Python 對函式的命名慣例,看到這種長相就可以猜「它是一台做事的機器,加括號會吐東西出來」。
from sklearn.decomposition import PCA
decomposition =分解。PCA 被放在這裡,是因為它的數學核心就是對資料的共變異矩陣做特徵分解,把矩陣拆成幾個主要方向。同一個模組裡還有 TruncatedSVD(適合稀疏矩陣)、NMF(非負矩陣分解)、FastICA。
PCA 是大寫開頭,代表它是一個類別,也就是「設計圖」,要加括號才會做出一台真的機器。這和第 1 行小寫的函式是兩種不同的東西。
from sklearn.linear_model import LogisticRegression
linear_model 收的是「輸出=輸入的線性組合」那一族:LinearRegression、Ridge、Lasso、ElasticNet、SGDClassifier。邏輯迴歸雖然是做分類,仍被歸在線性模型,因為它在套 Sigmoid 之前那一段確實是把特徵加權相加。
from sklearn.model_selection import StratifiedKFold, cross_val_score
這是唯一一行拿兩樣東西的,用逗號分隔。兩者的分工要分清楚:
| 名字 | 角色 | 負責什麼 |
|---|---|---|
StratifiedKFold | 規則 | 資料怎麼切:切幾折、要不要洗牌、每折的類別比例怎麼維持。它自己不會訓練任何模型。 |
cross_val_score | 執行者 | 拿著上面那個規則,真的去跑:切折、訓練、預測、計分,最後回傳分數陣列。 |
model_selection 模組專門放「怎麼切資料、怎麼評估、怎麼調參」,常見的還有 train_test_split、GridSearchCV、cross_validate、KFold。
from sklearn.pipeline import Pipeline
用來把多個步驟串成一個整體。同模組還有 make_pipeline(不用自己取步驟名稱的簡寫版)與 FeatureUnion(把多組特徵並聯後接起來)。這行也只是把工具拿到手,還沒串任何東西。
from sklearn.preprocessing import StandardScaler
preprocessing =資料進模型之前的加工。這個模組是特徵工程的大本營:縮放、編碼、離散化都在這裡。這行拿的 StandardScaler 負責把每一欄變成「平均 0、標準差 1」,是最通用的縮放方式,也是 PCA 前面幾乎一定要接的一站。
| 工具 | 做什麼 | 什麼時候用它而不是 StandardScaler |
|---|---|---|
StandardScaler | 平均 0、標準差 1 | 本題用的,最通用 |
MinMaxScaler | 壓到 0~1 之間 | 需要固定值域時(例如餵給神經網路的影像) |
RobustScaler | 用中位數與四分位距 | 資料有明顯離群值,不想被極端值拉歪 |
OneHotEncoder | 類別欄位轉成 0/1 欄 | 處理的是文字類別而非數字 |
X, y = load_breast_cancer(return_X_y=True)
到這行資料才真的被讀進記憶體。拆成三塊看:
| 片段 | 意思 |
|---|---|
load_breast_cancer() | 括號就是啟動鈕——沒有括號只是「指那台機器」,加了括號它才會動。 |
return_X_y=True | 關鍵字參數:告訴它「我只要特徵和答案兩包,其他不用」。不加的話它會回傳一個大包裹,要自己拆 .data 和 .target。 |
X, y = | 多重指派:右邊一次吐出兩包,左邊兩個箱子分別接住。順序不能顛倒。 |
拿到的東西是:X = 569 筆 × 30 個特徵的表格,y = 569 個 0/1 答案。形狀分別寫成 (569, 30) 和 (569,)。
0 = 惡性 malignant、1 = 良性 benign——1 是好消息。scikit-learn 預設把標籤 1 當成正類,所以本題的 ROC-AUC 衡量的是「把良性排在惡性前面的能力」。它本質上仍是兩類的區分能力,數值不受影響。30 個特徵是 10 種測量 × 3 種統計:半徑、紋理、周長、面積、平滑度、緊密度、凹陷度、凹點數、對稱性、碎形維度,每種各取平均值、標準誤、最差值。為什麼要分成 X 和 y?
pipe = Pipeline([Pipeline 要收一個清單當參數,清單裡按順序放各個步驟。所以這行的結尾出現兩個括號連在一起:( 是呼叫 Pipeline、[ 是開一個 list 清單。這不是打錯字。
左邊的 pipe 是你自己取的變數名,換成 model、my_pipeline 都可以。
("scale", StandardScaler()),
每一站都寫成 ("步驟名稱", 工具物件) 這樣的一組,這種括起來的一組叫 tuple。
| 片段 | 是什麼 | 可以改嗎 |
|---|---|---|
"scale" | 你自己取的步驟名稱,加引號代表這是文字 | 可以。之後能用 pipe.named_steps["scale"] 把這一站抓出來檢查;做網格搜尋時寫成 scale__with_mean(兩個底線)指定這一站的參數。 |
StandardScaler() | 實際做出來的機器(注意有括號) | 括號裡可以放設定,例如 StandardScaler(with_mean=False)。這裡留空表示全用預設值。 |
結尾的逗號代表「還有下一站」。前面那四格縮排在括號內部只是為了好讀,怎麼排都不影響執行。
這一站在訓練時會做的事:從訓練資料算出每一欄的平均值與標準差並記住,然後把資料轉成 \(z=(x-\mu)/\sigma\)。詳細在 第 02 節。
("pca", PCA(n_components=10)),
n_components=10 表示最後保留 10 個主成分,所以資料形狀會從 (455, 30) 變成 (455, 10)(455 是一折的訓練筆數)。
10 是整數,不用加引號——加了就變成文字,會報錯。這個位置也可以填小數:PCA(n_components=0.95) 代表「自動選到累積解釋變異達 95% 為止」,這份資料剛好會選到 10 個。詳細在 第 03 節。
("clf", LogisticRegression(max_iter=2000))
"clf" 是 classifier(分類器)的慣用縮寫,跟 cv = cross-validation、pipe = pipeline 一樣純粹是取名。
這是 Pipeline 的最後一站。Pipeline 有個硬性規定:前面每一站都必須會 transform,只有最後一站可以是模型。
max_iter=2000 是最佳化的迭代次數上限,不是「一定要跑 2000 次」。詳細在 第 04 節。
注意這行結尾沒有逗號——因為它是清單的最後一項。(Python 其實允許最後一項後面也加逗號,加了不會錯。)
])
] 關閉步驟清單、) 關閉 Pipeline 的呼叫,剛好跟第 10 行的 ([ 對稱。
pipe 代表整套處理流程——但一克原料都還沒送進去。這叫延遲執行:先把流程定義好,之後才餵資料。好處是同一條生產線可以重複用在不同資料上,也是交叉驗證能「每折重組一次」的前提。輸入原始 X
↓
① scale StandardScaler 每欄減平均、除標準差
↓
② pca PCA(10) 30 維 → 10 維
↓
③ clf LogisticRegression 輸出良性機率
↓
預測結果
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
三個關鍵字參數:
| 參數 | 意思 | 不設會怎樣 |
|---|---|---|
n_splits=5 | 切成 5 份 → 跑 5 輪,每筆資料被訓練 4 次、驗證 1 次 | 預設就是 5。切越多份越接近真實效能但越慢 |
shuffle=True | 切之前先洗牌 | 原始資料若有排序(前面都惡性、後面都良性),不洗牌會切出偏頗的折 |
random_state=42 | 固定亂數種子,每次執行結果一模一樣 | 因為有 shuffle,不固定的話每次跑分數都不同,沒辦法比較模型也沒辦法除錯。42 沒有統計意義,換成任何整數都行,重點是固定 |
Stratified(分層)是關鍵字:普通的 KFold 只管切成 5 等份,不管每份的類別比例;StratifiedKFold 會確保每一折的良性/惡性比例都跟整體一樣(62.7% 良性),避免切出「某一折幾乎全是良性」的荒謬結果。
跟第 10 行一樣,這行也只是定義規則,還沒真的去切。cv 這個名字是自己取的。
scores = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")
前三個是位置參數(照順序認人),後兩個是關鍵字參數:
| 參數 | 意思 |
|---|---|
pipe | 要評估的東西。注意這裡評估的不是單一模型,而是整條 Pipeline(標準化 → PCA → 邏輯迴歸)。 |
X | 輸入特徵,569 × 30 的原始資料——沒有先標準化、沒有先降維,全部交給 Pipeline 處理。 |
y | 每一筆的正確答案(0 惡性、1 良性)。 |
cv=cv | 左邊的 cv 是 sklearn 規定的參數名,右邊的 cv 是第 16 行建立的變數,兩者剛好同名而已。為什麼不是廢話? |
scoring="roc_auc" | 用 ROC-AUC 當評估指標。它需要連續的機率分數,所以 sklearn 會自動去呼叫 predict_proba 而不是 predict。詳細在 第 06 節。 |
它實際會做的事,是把下面這段跑 5 遍:
── 第 k 折 ────────────────────────────
複製一套全新的 Pipeline(不沿用上一折學到的任何東西)
【訓練折 455 筆】
StandardScaler.fit 只從這 455 筆算平均值、標準差
PCA.fit 只從這 455 筆找 10 個主成分方向
LogisticRegression.fit 用轉換後的資料訓練
【驗證折 114 筆】
StandardScaler.transform 用訓練折的平均值 / 標準差,不重學
PCA.transform 用訓練折的主成分方向,不重學
predict_proba → 計算這一折的 ROC-AUC
最後 scores 裡是 5 個數字。本題實跑結果:
array([0.99384, 0.99770, 0.98412, 1.00000, 0.99870])
第1折 第2折 第3折 第4折 第5折
scores.mean() → 0.9949 平均表現
scores.std() → 0.0058 折與折之間的波動,越小越穩定
scores.mean() 這種寫法就是方法——scores 是一個 NumPy 陣列物件,mean() 是它自帶的功能,不用你自己寫迴圈加總再除以 5。
pipe。因為送進去的是整條 Pipeline 而不是「已經處理好的資料」,前處理才會被關在每一折裡面重新學一次——這就是本題答案 A 說的事。詳細在 第 05 節。這 30 個特徵的實際數值範圍差距非常大(本站實跑):
| 特徵 | 最小值 | 最大值 | 平均值 | 標準差 |
|---|---|---|---|---|
| mean area 面積 | 143.5 | 2501.0 | 654.89 | 351.60 |
| worst area 最差面積 | 185.2 | 4254.0 | 880.58 | 568.86 |
| mean perimeter 周長 | 43.79 | 188.5 | 91.97 | 24.28 |
| mean radius 半徑 | 6.981 | 28.11 | 14.13 | 3.52 |
| mean smoothness 平滑度 | 0.0526 | 0.1634 | 0.0964 | 0.0141 |
面積動輒上千,平滑度只有 0.1,差了將近兩萬倍。對 PCA 來說,「變異最大的方向」幾乎一定被面積霸佔;對 LogisticRegression 來說,梯度下降在尺度懸殊的空間裡很難收斂。
\[ z = \frac{x - \mu}{\sigma} \qquad(\mu = \text{該欄平均值},\ \sigma = \text{該欄標準差}) \]
白話是「你比平均高多少個標準差」。轉換後每一欄的平均值都變成 0、標準差都變成 1。
| 演算法 | 需要嗎 | 原因 |
|---|---|---|
| PCA | 強烈需要 | 直接找變異最大方向,數值大的欄位會通吃 |
| Logistic Regression | 需要 | 梯度下降收斂快很多;用正則化時更是必須 |
| KNN / SVM / K-means | 需要 | 都在算距離,尺度大的欄位主宰距離 |
| 決策樹 / 隨機森林 / XGBoost | 不需要 | 只比較大於或小於某個門檻,換刻度不影響切點 |
ConvergenceWarning(迭代 2000 次還沒收斂)。加上 StandardScaler 後升到 0.9953,而且乖乖收斂。PC1 = 0.22×半徑 + 0.10×紋理 + 0.23×周長 + …(30 項全部有份)。為什麼要這樣做?因為原本 30 個特徵高度重複——半徑、周長、面積根本是同一件事的三種說法(周長 ≈ 2πr,面積 ≈ πr²)。PCA 把這種重複壓掉,用比較少的軸描述同樣多的資訊。
它會把新造出來的軸由重要到不重要排序。第 1 主成分是資料變化最大的方向,第 2 主成分是跟第 1 垂直、變化次大的方向,以此類推。每個主成分能解釋多少比例的總變異,就叫 explained variance ratio。以標準化後的乳癌資料實跑:
前 10 個加起來是 95.16%——用 10 個新軸保留了原本 30 欄裡九成五的資訊,維度砍掉三分之二。
| 優點 | 代價 |
|---|---|
| 降低維度、減少計算量與過擬合風險 | 新特徵失去可解釋性(PC1 到底是什麼?說不清) |
| 去除特徵間的重複與共線性 | 可能丟掉變異小但對分類有用的資訊 |
| 壓到 2~3 維就能畫圖視覺化 | 對尺度極度敏感,沒標準化等於白做 |
這也是選項 B 錯的原因:pca.fit(X) 的簽名裡 y 是被忽略的參數,PCA 從頭到尾只看 X 的共變異結構。用標籤決定投影方向的那是 LDA,不是 PCA。
它名字叫迴歸,工作是分類。先算一個迴歸式:把 10 個主成分加權相加得到分數 \(z\),然後用 Sigmoid 壓成 0~1 的機率:
\[ z = w_1x_1 + \dots + w_{10}x_{10} + b \qquad P = \frac{1}{1+e^{-z}} \]
輸出是「良性的機率」,例如 0.93。要變成硬答案時再套門檻(預設 0.5)。所以它輸出的是機率,只是最後被切成類別——這也是為什麼本題能算 ROC-AUC。
max_iter=2000 在幹嘛模型要找出最好的那組權重,是靠反覆微調(預設用 lbfgs 演算法),直到幾乎不再進步為止——這叫收斂。max_iter 是安全上限。
| 情況 | 會發生什麼 |
|---|---|
| 第 180 次就收斂 | 提早停止。max_iter 是上限,不是「一定要跑幾次」 |
| 2000 次還沒收斂 | 印出 ConvergenceWarning,用目前最好的權重交差,結果可能不是最佳解 |
max_iter=100 就足夠收斂;不標準化時,即使給到 2000 次也照樣噴警告。很多人會很自然地這樣寫,因為看起來比較簡單:
# 錯誤示範 X_scaled = scaler.fit_transform(X) # 對全部 569 筆學平均值與標準差 X_pca = pca.fit_transform(X_scaled) # 對全部 569 筆學主成分方向 scores = cross_val_score(model, X_pca, y, cv=cv) # 現在才切折
cross_val_score 把第 1 折當驗證資料時,那 114 筆的資訊早就混進 scaler 的平均值、標準差,以及 PCA 的主成分方向裡。驗證資料本來應該扮演「模型從沒見過的新病人」,現在卻已經偷偷參與過前處理的訓練——這就是資料洩漏。有人會反駁「可是 PCA 又沒用到 y,哪來的洩漏?」——洩漏不只發生在標籤上。即使只看 X,驗證折仍然貢獻了:
結果就是交叉驗證分數偏樂觀,模型真正上線遇到全新病人時,表現會比報告數字差。
scaler.fit_transform(X) 之後,那組被汙染的平均值與標準差就躺在 scaler.mean_、scaler.scale_ 這兩個尾巴帶底線的屬性裡;PCA 的主成分方向則躺在 pca.components_。之後不管你怎麼切折,模型用的都是這幾組已經看過驗證資料的數字。呼叫 pipe.fit(X_train, y_train) 時,內部自動這樣做:
前面每一站(scale、pca)→ fit_transform(訓練資料) 學參數 + 轉換 最後一站(clf) → fit(轉換後資料, y_train) 訓練模型
呼叫 pipe.predict(X_val) 時:
前面每一站(scale、pca)→ 只呼叫 transform(驗證資料) 用訓練學到的參數,不重學 最後一站(clf) → predict()
transform,從來沒有被 fit 過。它完美扮演了「未來的新病人」。Pipeline 天生就分得清「什麼時候該 fit、什麼時候只能 transform」——你自己手寫很容易搞混,Pipeline 不會。ROC-AUC 不是「猜對幾筆」(那是 Accuracy)。它衡量的是:隨便抓一個良性病例和一個惡性病例,模型給良性的分數比較高的機率有多大。
換句話說,它問的是排序能力,不是判斷力。想像把 60 份報告依「模型覺得有多像良性」由高到低排成一列:
完美的模型 ●●●●●●●●●●●●●●●●●●●●●●●●│●●●●●●●●●●●●●●●● AUC = 1.0
←──── 良性全部在左邊 ────┤─── 惡性全部在右邊 ──→
沒用的模型 ●●○●○○●●○●○●●○●○○●●○●○●●○●●○●○○●●○●○●●○● AUC = 0.5
←──────────── 紅綠交錯,排了等於沒排 ────────────→
AUC 就是在量「這一列排得多乾淨」,跟你把切點畫在哪裡完全無關——這也是它比 Accuracy 穩的原因。下面這個實驗室可以直接拖看看。
| AUC | 意義 |
|---|---|
| 0.5 | 跟丟銅板一樣,完全沒有區分能力 |
| 0.7 | 尚可 |
| 0.8 | 良好 |
| 0.9 | 非常好 |
| 1.0 | 完美區分(實務上看到 1.0 通常先懷疑資料洩漏) |
為什麼分類問題常用 AUC 而不是 Accuracy?①不受門檻影響:Accuracy 要先決定 0.5 這個切點,AUC 是把所有可能門檻都掃一遍算出來的。②對類別不平衡比較穩:如果 95% 都是良性,模型全猜良性也有 95% Accuracy,看起來很棒但毫無用處,AUC 會直接打回 0.5。
平均值回答「這個流程大概多強」,標準差回答「這個估計可不可信」。本題標準差 0.0058 很小,代表換一份資料表現也差不多;如果標準差很大(例如 0.08),代表模型對資料切分方式很敏感,平均值那個數字就別太當真。
七題,答錯會直接告訴你錯在哪。
from A import B =去 A 這個模組,把 B 拿到手上,不執行任何運算。PCA 是設計圖,PCA() 是做出來的機器。= 是「存進箱子」,括號內的 = 是「指定參數」,所以 cv=cv 不是廢話。y,所以不保證提高準確率。max_iter 是上限不是固定次數,收斂了會提早停。shuffle=True 要搭配 random_state 才能重現。fit 用錯地方。transform,避免資料洩漏,交叉驗證分數才可信。SelectKBest)、目標編碼、過採樣(SMOTE)、用整體平均補缺失值、標準化與降維。只要它會從資料學到東西,就一定要放進 Pipeline。像「把公斤換成公克」這種固定規則的轉換就沒有洩漏問題。這一題橫跨中級科目二「大數據處理與分析」與科目三「機器學習」。歷屆最常見三種問法:①給你一段有 fit_transform(X) 在交叉驗證之前的程式,問「此寫法有何問題」——答案永遠是資料洩漏;②問「將前處理納入 Pipeline 的主要好處」——答案是每折獨立擬合前處理參數;③問 PCA 是否使用標籤、是否保證提升準確率——答案都是否。另外 StratifiedKFold vs KFold、random_state 的作用、ROC-AUC 為何優於 Accuracy 也都是高頻考點。程式題只要抓住「fit 只給訓練資料」這一條,幾乎都能秒殺。
不只是省事。手寫時最容易犯的錯就是「先對完整資料 fit_transform,再切折」,這會讓驗證折的資訊洩漏進前處理參數,交叉驗證分數偏樂觀。Pipeline 會在每一折自動只用訓練折 fit、只對驗證折 transform,把這個錯誤從根本上排除。此外它也讓超參數搜尋(GridSearchCV)可以一次調整前處理與模型的參數,寫成 pca__n_components(兩個底線)。
都不會。PCA 是非監督式降維,只看 X 的變異與相關結構,fit 的 y 參數被忽略。它挑的是「變異最大」的方向,不是「最會分類」的方向,所以可能丟掉對分類有用但變異小的資訊。本站實跑:30 維 AUC 0.9953、10 維 0.9949、1 維 0.9709——降維換到的是速度與簡潔,不是準確率。想要「會分類」的降維可以改用 LDA(線性判別分析),那才是監督式的。
shuffle=True 是因為原始資料可能有排序(依收案時間、依類別排),不洗牌就直接切會讓某些折特別偏頗。而一旦洗牌就引入隨機性,random_state=42 固定亂數種子後,每次執行的切分方式都完全一樣,才能重現實驗、公平比較不同模型、以及除錯。數字 42 沒有統計意義,換成任何整數都可以,重點是固定住。
不行。這份資料影響小是因為樣本夠多(569 筆)而且 StandardScaler 與 PCA 都是非監督的。一旦換成小樣本、或前處理用到標籤(特徵選擇、目標編碼、SMOTE),差距會非常誇張。本站實測:對一份完全隨機的亂數資料(真實 AUC 應為 0.5),先用全部資料做特徵選擇再交叉驗證,報出來的 AUC 高達 0.929——完全是假的。所以正確的做法是養成永遠用 Pipeline 的習慣,而不是每次評估「這次要不要緊」。
不是,它是上限。模型收斂了就會提早停止。ConvergenceWarning 代表跑到上限還沒收斂,此時模型會用「目前為止最好的權重」交差,結果可能不是最佳解。要解決通常有兩條路:把 max_iter 調大(治標),或先把資料標準化(治本)。本站實測,這份資料不標準化時即使給到 2000 次仍不收斂,加上 StandardScaler 後預設的 100 次就夠了。
因為 ROC 曲線是「把判定門檻從 1 一路掃到 0,記錄每個門檻下的真陽性率與偽陽性率」畫出來的,需要連續的分數才能排序。如果只有 0/1 硬答案就只剩一個點,畫不出曲線。cross_val_score 在 scoring="roc_auc" 時會自動去呼叫 predict_proba(或 decision_function)。順帶一提,本資料集標籤 1 是良性,sklearn 預設把 1 當正類,因此 AUC 衡量的是把良性排在惡性前面的能力,本質上仍是兩類區分能力。