五行程式印出前五名特徵和它們的分數。這一題不考計算,考的是你知不知道這串數字「是什麼」、更重要的是「不是什麼」。四個選項就是四種讀法——一種剛剛好,三種讀過頭。讀過頭的那三種,恰好是實務上最常見的三個誤會。
訓練 Random Forest 後取得特徵重要性。對 feature_importances_ 的解讀,何者最正確?
from sklearn.ensemble import RandomForestClassifier # 拿出隨機森林分類器 rf = RandomForestClassifier(n_estimators=300, random_state=42) # 300 棵樹、固定亂數種子 rf.fit(X_train, y_train) # 用訓練資料把森林種出來 importance = dict(zip(feature_names, rf.feature_importances_)) # 把「特徵名字」和「重要性分數」拉鍊成一本字典 print(sorted(importance.items(), key=lambda x: x[1], reverse=True)[:5]) # 依分數由大到小排序,印出前 5 名
一支球隊贏球之後,教練團會整理一份功勞簿:這一季的關鍵時刻,球是交到誰手上的、誰的上場改變了局面。feature_importances_ 就是隨機森林的功勞簿——森林訓練完之後,回頭統計「這 300 棵樹在切分資料時,倚重了哪些特徵、各佔多少」,每個特徵一個分數,全部加起來等於 1。
但功勞簿有它天生的邊界。它記的是「這支球隊、這一季」的倚重程度:換一批對手(資料)、換一套戰術(模型)、甚至只是換個賽程順序(隨機種子),數字就會變。它也從來沒有說「得分王是贏球的原因」——也許他只是站在真正原因旁邊的人。
feature_importances_ 回答的是「這座訓練好的森林,用了誰、用得多兇」——它是模型內的相對分數。至於「誰造成了結果」(因果)、「誰跟結果同進退」(相關係數),是另外兩個不同的問題,要用不同的工具回答。這正是本題四個選項在測試的分界線。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。第 4、5 行是這題的 Python 重頭戲——一長串連環呼叫。
五行,一行一行走完。右上角的「看位置」可以把這一行放回完整程式裡看。
第 4、5 行都是一層包一層的寫法。訣竅只有一個:由最裡面往外讀,把每一層的成品想像出來,再交給外面那層。
第 4 行: dict( zip(名字們, 分數們) )
└ 先拉鍊配對 ──→ 再裝訂成字典
第 5 行: sorted( importance.items(), key=..., reverse=True )[:5]
└ 先攤平成一疊(名字,分數) → 依分數大到小排序 → 只拿前 5 張
from sklearn.ensemble import RandomForestClassifier # 從「集成方法」抽屜拿出隨機森林
from A import B =「去 A 那個抽屜,只把 B 拿出來」。sklearn.ensemble 是集成方法的抽屜——把很多個小模型合起來投票的那一類演算法。隨機森林=一大群故意長得不一樣的決策樹,各自判斷、最後多數決。
名字拆開讀:Random(每棵樹拿到的資料與特徵都刻意隨機打亂)+ Forest(樹很多)+ Classifier(做分類題)。
之所以是隨機森林出這題,是因為樹家族天生就有「重要性」可以回報——每棵樹的每次分岔都記錄了「用哪個特徵、讓資料變純多少」,把這些帳目加總,就是待會的 feature_importances_。
rf = RandomForestClassifier(n_estimators=300, random_state=42) # 300 棵樹、亂數固定住
呼叫 RandomForestClassifier(...) 做出一台還沒訓練的森林,貼上名字 rf。兩個關鍵字引數:n_estimators=300 是樹的棵數;random_state=42 把森林裡所有抽籤(抽資料、抽特徵)固定住,讓結果可重現。
跟上一頁的網格搜尋不同,這題沒有搜參數——直接指定一組超參數就開種。300 棵是常見的「夠穩」的量級。
rf.fit(X_train, y_train) # 用訓練資料把 300 棵樹種出來
rf.fit(X_train, y_train) 用訓練集把 300 棵樹全部種出來。種樹的過程中,每棵樹的每次分岔都會記帳:「這一刀用了哪個特徵、讓資料變純了多少」——feature_importances_ 就是這本帳的總結,所以它天生只反映「訓練資料 × 這個模型」的視角。
X_train、y_train、feature_names 沒有定義?對,考題常省略資料準備的前置(載入資料、切訓練/測試集)。本站實跑用 Wine 資料集(178 瓶酒 × 13 個化學特徵、三個品種)補上這段——完整版按右上角「看位置」或右下角「完整程式碼」。尾巴帶底線的 feature_importances_ 是 sklearn 的慣例:fit 之後才存在的屬性。還沒 fit 就去拿,會噴 NotFittedError。
importance = dict(zip(feature_names, rf.feature_importances_)) # 名字配分數,一對一裝進字典
由內往外讀,兩層:
① zip(feature_names, rf.feature_importances_)
像拉鍊:左排是 13 個名字、右排是 13 個分數,拉起來變成
("alcohol", 0.1435), ("malic_acid", 0.0344), ... 一對一對
② dict(①)
把這疊配對裝訂成字典:{"alcohol": 0.1435, "malic_acid": 0.0344, ...}
之後就能用名字查分數:importance["alcohol"] → 0.1435
rf.feature_importances_ 本身只是一排沒有名字的數字(順序跟資料的欄位順序一樣)。這一行做的事就是幫每個分數掛上名牌——人才讀得懂。
拉鍊有個小脾氣:兩排要一樣長。如果 feature_names 有 13 個、分數也有 13 個,就完美配對;萬一名字只有 12 個,zip 會默默拉到短的那排結束就停,不報錯——這是實務上很陰的 bug 來源。
print(sorted(importance.items(), key=lambda x: x[1], reverse=True)[:5]) # 排序取前 5 印出來
整題最長的一行,還是由內往外拆四步:
① importance.items() 把字典攤回一疊 (名字, 分數) 的配對
② sorted(①, key=lambda x: x[1], reverse=True)
key=lambda x: x[1] 「排序時,看每張配對的第 [1] 格(分數)」
reverse=True 由大到小(預設是由小到大)
③ ②[:5] 切片:只留前 5 張
④ print(③) 印出來
[('color_intensity', np.float64(0.1589...)), ('flavanoids', np.float64(0.1538...)), ('proline', np.float64(0.1525...)), ('alcohol', np.float64(0.1435...)), ('hue', np.float64(0.0962...))]np.float64(...) 是 NumPy 2 的顯示格式——數值本身就是普通的小數。lambda x: x[1] 是現寫現用的一句話小函式:「給我一個 x,我回你 x 的第 1 格」。因為 Python 從 0 開始數,第 [0] 格是名字、第 [1] 格是分數——所以這句的意思就是「按分數排」。
feature_importances_ 的正式身分是 MDI(Mean Decrease in Impurity,不純度下降平均),也叫 Gini importance。原理跟著種樹的過程走:
每棵樹的每一次分岔,都在做同一件事:
挑一個特徵、挑一個切點,把資料切成「更純」的兩堆
(純 = 堆裡幾乎都是同一類;用 Gini 不純度來量)
某特徵的重要性 = 它主刀的每一次分岔「讓不純度下降多少」
(乘上該節點分到的樣本比例)通通加起來,
再對 300 棵樹取平均,最後全體正規化成總和 1
\[ \text{MDI}(f) \;=\; \frac{1}{T}\sum_{t=1}^{T}\;\sum_{\substack{\text{樹 } t \text{ 中}\\ \text{用 } f \text{ 分岔的節點}}} \frac{n_{node}}{n} \,\Delta\text{impurity} \]
三個馬上能用的性質:① 每個分數都 ≥ 0(不純度只會被「下降」記帳,沒有負的貢獻);② 全部加起來恰好等於 1(正規化過,所以它天生是「比例」);③ 它是訓練過程的帳目——只看訓練資料,跟測試集表現無關。
rf.feature_importances_ 的 shape 是 (13,)、每個值都 ≥ 0、總和 = 1.0000000000。前五名:color_intensity 0.1589、flavanoids 0.1538、proline 0.1525、alcohol 0.1435、hue 0.0962;最低的是 ash 0.0121。這座森林在測試集 54 瓶全對(accuracy 1.0——Wine 對隨機森林來說太簡單,這件事待會有戲份)。A 說了兩件事,兩件都對:「此已擬合模型中的相對重要性」+「不能單憑此數值推論因果」。前半句的三個「相對」值得拆開:
| 相對於什麼 | 意思 | 本站實跑的證據 |
|---|---|---|
| 相對於彼此 | 總和正規化成 1,單一數值只有跟其他特徵比才有意義——0.1589 是「份額」,不是絕對強度 | 13 個分數總和 1.0000000000 |
| 相對於這個模型 | 它是 300 棵「隨機長出來的樹」的帳目平均,連換個種子都會動 | seed 42→0:color_intensity 0.159→0.186、第 2/3 名互換 |
| 相對於這份資料 | 換一份資料(或多塞幾個欄位),份額就重新分配 | 加兩個亂數欄位後,13 個真特徵的分數全體被稀釋 |
至於後半句「不能推論因果」——這正是 B 選項踩爆的地雷,下一節用一個你一定聽過的例子實跑給你看。
經典故事:統計上,冰淇淋銷量跟溺水人數高度同進退。冰淇淋是溺水的原因嗎?當然不是——背後有一個沒被寫進表格的混淆因子:夏天。天氣熱,冰淇淋賣得多、玩水的人也多。
我們把這個世界「造」出來實跑一次——因為是模擬,我們知道真相:溺水率 y 完全由「夏天炎熱程度 Z」決定,冰淇淋銷量也只是 Z 的結果,而 Z 沒有被收進特徵表(現實中混淆因子常常就是沒被記錄的那個)。
模擬世界的真相(模型看不到這張圖):
夏天炎熱 Z ────(沒被收進特徵表!)
↙ ↘
冰淇淋銷量 X 溺水率 y X 和 y 之間【沒有】因果箭頭
C 說「數值為 0 的特徵,在任何資料集與任何模型中都永遠無效」——「任何」和「永遠」這種宇宙級的量詞,就是它的死因。重要性 0 只說明一件事:這座森林(在這份訓練資料上)沒有從它身上記到任何帳。換個情境,同一個特徵可以立刻滿血復活。兩個實跑證據:
更常見的情境:兩個高度相關的特徵會互相攤分數。樹分岔時兩個都能用、隨機挑到誰誰記帳——所以其中一個看起來「不重要」,可能只是功勞被分身拿走。
D 把 feature_importances_ 說成「每個特徵與目標之間的機率相關係數」。先說個小事實:「機率相關係數」根本不是統計學的正式名詞——選項自己發明術語,通常就是陷阱的味道。就算好心把它讀成「皮爾森相關係數」,也完全對不上,三個硬差異:
| feature_importances_(MDI) | 相關係數 r | |
|---|---|---|
| 值域與正負 | 0 到 1,沒有正負方向 | −1 到 +1,正負代表同向/反向 |
| 定義的對象 | 模型內的分裂貢獻帳目(一整座森林的性質) | 兩個變數之間的線性同動程度(跟模型無關) |
| 總和 | 被正規化成加總 = 1(互相排擠的份額) | 各算各的,沒有總和限制 |
MDI 快、免費(訓練完就有),但有兩個已知偏誤:偏愛切點多的高基數特徵、只看訓練資料。sklearn 官方文件自己就提醒:對高基數特徵,MDI 可能誤導。常用的補強是 permutation importance:
permutation importance 的做法(在測試集上): 1. 先量模型的正常分數 2. 把「某一欄」整欄打亂(其他欄不動)→ 再量一次分數 3. 分數掉多少,就是這一欄的重要性(重複 30 次取平均) 問的問題從「訓練時誰被用到」變成「現在弄壞誰,模型會真的變差」
「相對」=總和為 1 的份額、隨模型/資料/種子而動(實跑:seed 一換名次就換);「不能推因果」=冰淇淋重要性 0.609,但介入它對真實 y 影響為零。兩個限定詞都下得剛剛好。
錯三層:預測有用不必然是原因(可能只是混淆因子的影子);就算真有因果也不會只有一個原因;而「原因」需要實驗或因果推論才能宣稱。實跑的冰淇淋就是活教材。
「任何」「永遠」是宇宙級量詞。實跑:flavanoids 弄成常數後重要性恰好 0,但它在原資料是第 2 名、|r| 全場最高;分身實驗裡分數還會被相關特徵拿走。0 分只是這座森林的區域性帳目。
「機率相關係數」不是正式名詞;就算當成皮爾森 r 也對不上——MDI 無正負、總和為 1、是模型內帳目;r 有正負、逐對計算、與模型無關。實跑兩張排行榜根本長不一樣。
再看一次同一段程式。這次你知道那串數字的身分了:一本「這座森林的功勞簿」。
訓練 Random Forest 後取得特徵重要性。對 feature_importances_ 的解讀,何者最正確?
from sklearn.ensemble import RandomForestClassifier # 拿出隨機森林 rf = RandomForestClassifier(n_estimators=300, random_state=42) # 300 棵樹、固定種子 rf.fit(X_train, y_train) # 訓練=記功勞簿 importance = dict(zip(feature_names, rf.feature_importances_)) # 名字配分數 print(sorted(importance.items(), key=lambda x: x[1], reverse=True)[:5]) # 印前 5 名
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
zip 把「名字們」和「分數們」拉鍊成一對一對,dict 再裝訂成「用名字查分數」的字典。zip 遇到兩排不一樣長會默默拉到短的那排就停,不報錯。.items() 攤平成 (名字, 分數) 配對 → sorted(..., key=lambda x: x[1], reverse=True) 依第 [1] 格(分數)由大到小排 → [:5] 切前五 → print。feature_importances_ 的正式身分是 MDI(不純度下降平均):統計每個特徵主刀的分岔讓 Gini 不純度下降多少,對 300 棵樹取平均後正規化。每個值 ≥ 0、總和恰為 1、只看訓練資料(實跑總和 1.0000000000)。特徵重要性是中級科目三「機器學習技術與應用」與科目二模型評估的交界考點,通常考觀念不考計算。最常見的五種問法:① 給一段印出 feature_importances_ 的程式,問「何者是正確解讀」(本題這種;錯誤選項固定往三個方向長:因果跳躍、宇宙級量詞、張冠李戴成相關係數);② 問 MDI 的性質(總和為 1、非負、fit 後才有、樹家族才有);③ 問 MDI 對什麼特徵有偏誤(高基數)、該用什麼補強(permutation importance);④ 問兩個高度相關特徵的重要性會怎樣(互相攤分);⑤ 給「冰淇淋與溺水」型情境問該下什麼結論(混淆因子、相關非因果)。抓住一句話:這串數字回答「模型用了誰」,不回答「誰造成結果」也不回答「誰跟結果同進退」。
不是,它完全來自訓練過程:森林在訓練資料上長樹時,把每次分岔的不純度下降記帳、平均、正規化。整個計算跟測試集無關,甚至跟模型「準不準」無關——一個過擬合到滿分的模型照樣會給你一串漂亮的重要性。這也是它跟 permutation importance 的根本差異:後者通常在測試集或驗證集上做,量的是「打亂這一欄,泛化表現實際掉多少」。兩者一個看「訓練時誰被用到」、一個看「現在弄壞誰會出事」,問的問題不同,答案本來就可以不同(本站實跑:od280 的 MDI 0.109、permutation 只有 0.006)。
因為樹在分岔時,兩個高度相關的特徵切出來的效果幾乎一樣好,隨機森林又刻意在每次分岔只抽部分特徵(max_features)來比——這次抽到本尊就本尊記帳、下次抽到分身就分身記帳。本站實跑:把最強特徵 color_intensity 完整複製一份塞回去,原本 0.159 的分數被攤成 0.138+0.136,兩人合計 0.274 大致守恆;相關降到 0.95 時,攤分比例還會隨 random_state 大幅擺動(seed 42 給分身 0.093、seed 0 給 0.125)。實務啟示:看到某特徵分數很低,先檢查表裡有沒有它的近親;要評估「這一群相關特徵」的整體重要性,可以先分群再一起打亂(grouped permutation)。
只代表這座森林在這份訓練資料上,沒有任何一次分岔靠它產生過不純度下降。成因至少三種:這批資料裡它剛好沒有變化(本站實跑:把 flavanoids 弄成常數,MDI 恰好 0.000000,但它在原資料是第 2 名);它的功勞被高度相關的特徵拿走;或這種模型剛好用不上它(例如需要跟其他特徵組合才有意義的訊號,淺樹可能切不出來)。所以 C 選項的「在任何資料集與任何模型中都永遠無效」是把一筆區域性帳目膨脹成宇宙定律。換資料、換模型、換特徵組合,同一個特徵完全可能翻身。
高基數=取值種類很多(連續數值、ID、郵遞區號)。這種特徵提供的候選切點非常多,就算跟目標毫無關係,樹在幾百次分岔中也總能碰巧找到幾刀「看起來讓資料變純」的切法,於是記到一點點分數。本站實跑是個溫和的示範:178 個不同取值的均勻亂數拿到 MDI 0.0057,只有兩種取值的 0/1 亂數拿到 0.0011——同樣是純亂數,切點多的拿了 5 倍分。資料越髒、樹越深、特徵基數差越大,這個偏誤越誇張(sklearn 官方文件對此有明確警告)。解法:用 permutation importance 複核(實跑:兩個亂數欄位都拿到乾淨的 0.0000)。
有,兩個主要的。第一,相關特徵會互相掩護:打亂 A 欄時,跟 A 高度相關的 B 欄還留著,模型照樣預測得好,於是 A 的 permutation 分數被低估——這跟 MDI 的攤分是同一個病灶的兩種症狀,解法是先把相關特徵分群、整群一起打亂。第二,它比較貴:每個特徵要打亂 n_repeats 次、每次都要重新評分。另外要記得在測試集或驗證集上做,在訓練集上做量到的是「模型背了誰」而不是「誰真的有用」。本站實跑還示範了一個微妙現象:資料太簡單(測試 accuracy 1.0)時,很多特徵打亂了分數也不掉,permutation 會給出一排 0——這不是它們沒用,是冗餘度太高。
黃金標準是隨機對照實驗(RCT/A/B 測試):隨機決定誰接受介入,讓混淆因子在兩組間自動抵銷,再比較結果差異。做不了實驗時,才退而求其次用因果推論方法——差異中之差異(DiD)、工具變數、傾向分數配對、以及基於因果圖(DAG)的 do-calculus 等,但每一種都需要「額外的假設」而不只是資料。本站的模擬正是反面教材:冰淇淋在觀察資料裡是最強預測子(重要性 0.609),但在我們造的世界裡對 y 的因果效果是精確的零——把它整欄打亂,真實 y 一個都沒變。任何重要性分數(MDI、permutation、SHAP)都停在「預測」這一層,跨不進「介入會怎樣」那一層。
層級不同。feature_importances_ 是整個模型一串數字(全域、只有樹家族原生提供);SHAP 基於賽局理論的 Shapley 值,先算出每一筆預測中每個特徵的貢獻(局部、帶正負方向),要全域觀點時再把各筆的絕對值平均起來。所以 SHAP 能回答「這位客戶被判高風險,是哪幾個欄位推高的、各推了多少」——MDI 完全答不了這種題。代價是計算量大得多(樹模型有高效的 TreeSHAP)。考點記法:全域快篩看 MDI、全域可信看 permutation、單筆解釋看 SHAP,三個都不是因果。