森林MATLAB代碼:TreeBagger調(diào)參與排錯(cuò))
簡(jiǎn)介隨機(jī)森林MATLAB代碼壓縮包面向希望在MATLAB環(huán)境中實(shí)現(xiàn)隨機(jī)森林分類與回歸的學(xué)習(xí)者和開發(fā)者適合機(jī)器學(xué)習(xí)課程實(shí)驗(yàn)、算法復(fù)現(xiàn)及小規(guī)模數(shù)據(jù)建模練習(xí)。包內(nèi)共14個(gè)文件約211KB包含m函數(shù)腳本、txt示例數(shù)據(jù)、Fortran源碼、dll動(dòng)態(tài)庫(kù)以及readme和doc說明文檔其中m文件對(duì)應(yīng)分類、回歸與模型打印入口txt提供訓(xùn)練測(cè)試樣本dll與Fortran部分承擔(dān)底層計(jì)算便于理解從接口調(diào)用到核心實(shí)現(xiàn)的完整結(jié)構(gòu)。該資源已有6072人學(xué)習(xí)配套示例覆蓋分類與回歸兩條使用路徑讀者可借此觀察隨機(jī)森林的樣本與特征采樣、決策樹集成輸出及特征重要性查看方式并對(duì)照說明文檔梳理目錄組織、依賴配置與模型評(píng)估思路作為算法學(xué)習(xí)和代碼研讀的參考資料。1. 一段跑不通的腳本暴露了隨機(jī)森林 MATLAB 代碼的三個(gè)前提很多人找隨機(jī)森林 MATLAB 代碼拿到的第一份東西通常是三五行l(wèi)oad 一份數(shù)據(jù)TreeBagger 建三十棵樹predict 完打印一個(gè)準(zhǔn)確率。這段腳本在自帶示例數(shù)據(jù)上能刷到 0.9 以上換到自己的表上就翻車——要么類別變量直接報(bào)類型錯(cuò)誤要么準(zhǔn)確率死死卡在多數(shù)類占比上不動(dòng)看著像模型沒學(xué)實(shí)際上是數(shù)據(jù)和接口沒對(duì)齊。隨機(jī)森林在 MATLAB 里上手門檻很低但 TreeBagger 與 fitcensemble 兩條路線各有硬性輸入約束樹數(shù)、葉子大小、每次分裂的特征數(shù)這三個(gè)需要調(diào)整的參數(shù)又互相牽制調(diào)錯(cuò)一個(gè)結(jié)果就不可信。這篇按「選哪條路 → 數(shù)據(jù)怎么喂 → 參數(shù)怎么調(diào) → 報(bào)錯(cuò)怎么查」的順序給一份能改能跑、踩過坑再寫出來的實(shí)現(xiàn)說明適合手里已經(jīng)有一張?zhí)卣鞅?、打算?RF 當(dāng)基線模型的從業(yè)者也適合課設(shè)階段需要一份既能運(yùn)行又能解釋清楚的同學(xué)。2. 三條實(shí)現(xiàn)路線的選型TreeBagger、fitcensemble 與手寫腳本MATLAB 里做隨機(jī)森林本質(zhì)上是三種東西一個(gè)封裝好的老牌類、一個(gè)能接進(jìn)交叉驗(yàn)證流水線的建模函數(shù)、一份可以完全控制分裂準(zhǔn)則的自寫腳本。選錯(cuò)路線不是精度問題而是后面每一步都別扭——想算 OOB 誤差卻發(fā)現(xiàn)沒開預(yù)測(cè)、想接超參搜索卻發(fā)現(xiàn)接口不兼容、想把模型導(dǎo)出到別的語言卻只能對(duì)著對(duì)象干瞪眼。所以先把選型擺清楚。2.1 TreeBagger分類回歸一套接口的老牌實(shí)現(xiàn)TreeBagger 是最直接的入口一行構(gòu)造自動(dòng)完成自助采樣bootstrap、隨機(jī)特征子集、多樹匯總投票還內(nèi)置了袋外樣本out-of-bagOOB的評(píng)估能力。它的價(jià)值在于「不用自己寫循環(huán)」每棵樹的訓(xùn)練樣本由有放回抽樣產(chǎn)生約 36.8% 的樣本不進(jìn)袋這些樣本天然構(gòu)成一個(gè)不花錢的驗(yàn)證集能在不切分?jǐn)?shù)據(jù)的前提下給出誤差估計(jì)。對(duì)樣本量小、切出去一塊驗(yàn)證集就心疼的場(chǎng)景這一點(diǎn)很實(shí)用。% X: n×p 數(shù)值特征矩陣Y: n×1 的 categorical 標(biāo)簽 rng(2024); % 固定隨機(jī)種子保證結(jié)果可復(fù)現(xiàn) rf TreeBagger(200, X, Y, ... Method, classification, ... % 分類任務(wù)回歸改成 regression OOBPrediction, on, ... % 開啟袋外預(yù)測(cè)后面才能算 OOB 誤差 OOBPredictorImportance, on, ... % 同時(shí)計(jì)算置換重要性 MinLeafSize, 5, ... % 葉節(jié)點(diǎn)最小樣本數(shù)控制單棵樹深度 NumPredictorsToSample, all, ... % 每次分裂考察的特征數(shù) PredictorSelection, allsplits, ... % 用標(biāo)準(zhǔn) CART 窮舉分裂而非曲率檢驗(yàn) InBagFraction, 1, ... % 自助采樣比例1 表示抽 n 個(gè) UseParallel, true); % 需要并行工具箱否則會(huì)報(bào)錯(cuò) Yfit predict(rf, X); % 返回 cell 數(shù)組元素是字符標(biāo)簽 Yfit categorical(Yfit); % 轉(zhuǎn)回 categorical 才能直接比標(biāo)簽 acc mean(Yfit Y); fprintf(訓(xùn)練集準(zhǔn)確率 %.4f\n, acc);代碼里幾個(gè)參數(shù)值得單獨(dú)說清楚。NumTrees取 200 是經(jīng)驗(yàn)起點(diǎn)不是定值后面 4.1 會(huì)用 OOB 曲線判斷到底該停在哪。MinLeafSize從默認(rèn)的 1 提到 5是給噪聲留一點(diǎn)緩沖葉子太純的樹在訓(xùn)練集上幾乎滿分測(cè)試集上掉得厲害。NumPredictorsToSample設(shè)成all意味著每次分裂都用全部特征這其實(shí)弱化了「隨機(jī)」這一半分類任務(wù)常見做法是取特征數(shù)的平方根取整回歸取三分之一。PredictorSelection默認(rèn)是curvature曲率檢驗(yàn)偏向能產(chǎn)生強(qiáng)交互的分裂想做教科書式的標(biāo)準(zhǔn) CART 就改成allsplits兩者在小樣本上的結(jié)果差異可能達(dá)到好幾個(gè)百分點(diǎn)別默認(rèn)它倆等價(jià)。需要留意的是predict的返回類型。分類模型返回的是 cell 數(shù)組里面裝的是字符直接和數(shù)值標(biāo)簽比較會(huì)得到一個(gè)全 false 的邏輯向量然后你會(huì)以為模型廢了。轉(zhuǎn)成 categorical 或者用str2double走一遍是必須補(bǔ)的一步。2.2 fitcensemble能掛進(jìn)交叉驗(yàn)證和超參搜索的版本如果后面打算做 K 折交叉驗(yàn)證、網(wǎng)格搜索、或者把模型塞進(jìn)一條預(yù)處理加建模的流水線fitcensemble更順。它把「集成方法」和「基學(xué)習(xí)器」拆開了Method選Bag就是袋裝基學(xué)習(xí)器用templateTree描述樹的參數(shù)寫在模板里而不是散在函數(shù)參數(shù)上。t templateTree( ... MinLeafSize, 5, ... NumVariablesToSample, all, ... % 等價(jià)于 TreeBagger 的 NumPredictorsToSample Surrogate, off); % 關(guān)閉代理分裂缺失值需提前處理 mdl fitcensemble(X, Y, ... Method, Bag, ... % 袋裝AdaBoostM2 等是提升法 NumLearningCycles, 200, ... % 基學(xué)習(xí)器個(gè)數(shù)對(duì)應(yīng) NumTrees Learners, t, ... Prior, uniform); % 類別先驗(yàn)處理不平衡時(shí)改成 empirical cvmdl crossval(mdl, KFold, 5); % 直接得到交叉驗(yàn)證模型 fprintf(5 折誤差 %.4f\n, kfoldLoss(cvmdl));這套寫法的好處是crossval、kfoldLoss、oobLoss、plotPartialDependence全都認(rèn)這個(gè)對(duì)象不用自己寫循環(huán)去拼混淆矩陣。Prior參數(shù)也是 TreeBagger 沒有直接對(duì)應(yīng)物的東西類別不平衡時(shí)把先驗(yàn)改成uniform或者傳入自定義向量往往比調(diào)閾值更省事。代價(jià)是參數(shù)名和 TreeBagger 不完全一樣NumLearningCycles對(duì)應(yīng)樹數(shù)、NumVariablesToSample對(duì)應(yīng)每次分裂的特征數(shù)來回切換時(shí)容易寫錯(cuò)。兩條路線的取舍可以壓成一張表維度TreeBaggerfitcensembleMethodBag分類/回歸統(tǒng)一接口靠Method一個(gè)參數(shù)切換同一個(gè)函數(shù)靠標(biāo)簽類型自動(dòng)判斷OOB 誤差oobError/oobLossoobLoss(mdl,Mode,cumulative)交叉驗(yàn)證需自己寫 K 折循環(huán)直接crossvalkfoldLoss超參自動(dòng)搜索不支持需外層包 bayesopt支持OptimizeHyperparameters特征重要性O(shè)OBPermutedPredictorDeltaErrorpredictorImportance 置換法偏依賴圖需手動(dòng)計(jì)算plotPartialDependence直接出圖數(shù)據(jù)量大、只想要一個(gè)能跑的基線TreeBagger 更省事要做實(shí)驗(yàn)對(duì)比、要出可復(fù)現(xiàn)的交叉驗(yàn)證數(shù)字fitcensemble的流水線更穩(wěn)。2.3 手寫版只在這三種情況下值得自己寫遞歸建樹不是炫技絕大多數(shù)場(chǎng)景純屬浪費(fèi)時(shí)間。真正值得動(dòng)手的情況只有三類一是分裂準(zhǔn)則要改比如你需要代價(jià)敏感的分裂或者自定義的不純度二是模型要導(dǎo)出到 C 或嵌入式環(huán)境MATLAB 的對(duì)象沒法直接落地得自己序列化成數(shù)組三是教學(xué)或答辯場(chǎng)景需要指著每一層節(jié)點(diǎn)解釋「這棵樹為什么在這里切」。給一份最小骨架能說明 bagging 內(nèi)部到底在干什么function node growTree(X, y, depth, maxDepth, minLeaf, mtry) % 單棵 CART 分類樹的最小實(shí)現(xiàn)省略缺失值處理與剪枝 if depth maxDepth || numel(unique(y)) 1 || size(X,1) 2*minLeaf node struct(leaf, true, label, mode(y)); return; end p size(X,2); featIdx randperm(p, min(mtry,p)); % 隨機(jī)挑 mtry 個(gè)特征這是隨機(jī)森林的一半 bestGain -inf; bestFeat 0; bestThr 0; parentImp giniImp(y); for f featIdx vals unique(X(:,f)); if numel(vals) 2, continue; end thrCand vals(1:end-1) diff(vals)/2; % 相鄰取值中點(diǎn)作候選閾值 for t thrCand left X(:,f) t; right ~left; if nnz(left) minLeaf || nnz(right) minLeaf, continue; end gain parentImp - (nnz(left)*giniImp(y(left)) ... nnz(right)*giniImp(y(right))) / numel(y); if gain bestGain bestGain gain; bestFeat f; bestThr t; end end end if bestFeat 0 node struct(leaf, true, label, mode(y)); return; end left X(:,bestFeat) bestThr; node struct(leaf, false, feat, bestFeat, thr, bestThr, ... left, growTree(X(left,:), y(left), depth1, maxDepth, minLeaf, mtry), ... right, growTree(X(~left,:), y(~left), depth1, maxDepth, minLeaf, mtry)); end function g giniImp(y) % 基尼不純度分類樹最常用的分裂指標(biāo) [~,~,idx] unique(y); p accumarray(idx, 1) / numel(y); g 1 - sum(p.^2); end外層再套一層 bootstrap 采樣和投票就是完整森林。這段代碼的性能比內(nèi)置實(shí)現(xiàn)差一到兩個(gè)數(shù)量級(jí)因?yàn)樗鼪]有向量化候選閾值、沒有做排序優(yōu)化也完全沒有剪枝和缺失值處理只建議用來理解結(jié)構(gòu)或者做定制化改造不要拿它去跑十萬行數(shù)據(jù)。3. 預(yù)處理鏈路把表、缺失值和類別變量對(duì)齊到 RF 的輸入約定隨機(jī)森林對(duì)特征縮放不敏感這容易讓人誤以為它對(duì)數(shù)據(jù)形態(tài)也沒要求。實(shí)際上 MATLAB 的實(shí)現(xiàn)有一套明確的輸入約定矩陣還是 table、標(biāo)簽是 categorical 還是字符 cell、缺失值走哪條路徑任何一條不對(duì)報(bào)錯(cuò)信息都含糊得讓人抓瞎。3.1 table 輸入與類別變量的三種處理最省事的做法是把整張表連同標(biāo)簽一起喂進(jìn)去讓實(shí)現(xiàn)自己去識(shí)別哪一列是類別變量。table 的優(yōu)勢(shì)是保留了變量名后面看特征重要性時(shí)直接輸出名字不用回頭對(duì)列號(hào)。T readtable(features.csv, TextType, string); labelName label; predictorNames setdiff(T.Properties.VariableNames, labelName, stable); % 字符串列轉(zhuǎn) categorical避免被當(dāng)成 p 個(gè)啞變量處理 for k 1:numel(predictorNames) if isstring(T.(predictorNames{k})) || iscellstr(T.(predictorNames{k})) T.(predictorNames{k}) categorical(T.(predictorNames{k})); end end X T(:, predictorNames); Y categorical(T.(labelName)); rf TreeBagger(200, X, Y, Method, classification, ... CategoricalPredictors, all); % 顯式聲明非數(shù)值列按類別變量處理TextType,string這一步容易被忽略。默認(rèn)讀進(jìn)來是 cell 字符數(shù)組某些版本里categorical轉(zhuǎn)換會(huì)給出警告甚至失敗統(tǒng)一成 string 再轉(zhuǎn) categorical 就干凈了。CategoricalPredictors寫成all是讓實(shí)現(xiàn)自己對(duì)非數(shù)值列做處理但如果表里混進(jìn)了 ID 列這種高基數(shù)變量幾千個(gè)不同取值隨機(jī)森林會(huì)把它當(dāng)成一個(gè)極強(qiáng)的分裂特征訓(xùn)練集精度飆高、測(cè)試集慘不忍睹——這類列在建表階段就該刪掉別指望模型自己判斷。3.2 缺失值填補(bǔ)、代理分裂、還是直接刪老版本的 TreeBagger 遇到 NaN 會(huì)直接拋錯(cuò)新版本和fitcensemble的templateTree支持Surrogate,all做代理分裂也就是在最優(yōu)分裂特征缺失時(shí)用相關(guān)的替代特征頂上。代理分裂在缺失比例低于 5% 時(shí)效果不錯(cuò)缺失比例一高代理分裂本身也沒得選還是老老實(shí)實(shí)填補(bǔ)靠譜。% 數(shù)值列中位數(shù)填補(bǔ)類別列眾數(shù)填補(bǔ)同時(shí)打缺失標(biāo)記 Xnum varfun(median, X, InputVariables, isnumeric, ... OutputFormat, uniform); for k 1:width(X) col X{:,k}; if isnumeric(col) miss isnan(col); if any(miss) X{miss,k} median(col(~miss)); % 中位數(shù)對(duì)偏態(tài)分布更穩(wěn) end else miss isundefined(col); if any(miss) X{miss,k} mode(col(~miss)); % 類別列用眾數(shù) end end if any(miss) X.([miss_ X.Properties.VariableNames{k}]) double(miss); % 缺失本身就是信息 end end補(bǔ)一個(gè)缺失指示列的做法在小樣本上很值缺失往往不是隨機(jī)的某個(gè)字段空著本身就帶信號(hào)把它當(dāng)特征喂進(jìn)去比單純填補(bǔ)保留的信息更多。反過來如果某一列缺失超過 30%更常見的處理是直接刪列因?yàn)樘钛a(bǔ)出來的值基本上等于在主要分裂上人為制造了一個(gè)眾數(shù)堆積會(huì)系統(tǒng)性地帶偏重要性排序。3.3 劃分分層 Hold-Out 與 K 折的對(duì)象設(shè)計(jì)類別不平衡時(shí)最怕的就是普通隨機(jī)劃分某次抽樣某個(gè)少數(shù)類一個(gè)都沒進(jìn)訓(xùn)練集模型直接學(xué)不會(huì)。cvpartition的第一參數(shù)傳分類標(biāo)簽時(shí)默認(rèn)做分層抽樣這一條能擋掉不少意外。cp cvpartition(Y, HoldOut, 0.3); % 傳標(biāo)簽向量自動(dòng)分層 idxTrain training(cp); idxTest test(cp); rf TreeBagger(200, X(idxTrain,:), Y(idxTrain), ... Method, classification, OOBPrediction, on); Ytest categorical(predict(rf, X(idxTest,:))); C confusionmat(Y(idxTest), Ytest); acc sum(diag(C)) / sum(C(:)); fprintf(測(cè)試集準(zhǔn)確率 %.4f\n, acc);分層之后的第二個(gè)坑是順序cvpartition給出的是邏輯索引如果先對(duì)整張表做了標(biāo)準(zhǔn)化、特征篩選或者缺失值填補(bǔ)再劃分那么測(cè)試集的信息已經(jīng)漏進(jìn)訓(xùn)練過程OOB 誤差和測(cè)試誤差會(huì)出現(xiàn)明顯背離。正確順序永遠(yuǎn)是「先切分、再在訓(xùn)練集上擬合任何變換、最后用訓(xùn)練集的統(tǒng)計(jì)量去處理測(cè)試集」。4. 參數(shù)調(diào)整樹數(shù)、葉子大小、特征子集與自動(dòng)調(diào)參隨機(jī)森林被叫「幾乎不用調(diào)參的模型」這句話只在「能跑出個(gè)結(jié)果」的前提下成立。真實(shí)項(xiàng)目里樹數(shù)決定訓(xùn)練成本葉子大小決定過不過擬合每次分裂的特征數(shù)決定樹之間的相關(guān)性三個(gè)參數(shù)串起來看才有效。4.1 NumTrees看 OOB 曲線什么時(shí)候走平樹數(shù)不是越多越好而是「加到 OOB 誤差不再明顯下降」為止。判斷方法很直接開OOBPrediction,on用oobError取隨樹數(shù)累積的誤差序列畫出來看拐點(diǎn)。rf TreeBagger(300, X, Y, Method, classification, ... OOBPrediction, on, MinLeafSize, 5); err oobError(rf); % 長(zhǎng)度 300第 k 個(gè)是前 k 棵樹的 OOB 誤差 [~, kBest] min(err); fprintf(OOB 最優(yōu)樹數(shù) %d誤差 %.4f\n, kBest, err(kBest)); fprintf(300 棵樹時(shí)誤差 %.4f進(jìn)一步增加樹數(shù)收益有限\n, err(end)); figure; plot(err, LineWidth, 1.2); grid on; xlabel(樹的數(shù)量); ylabel(OOB 分類誤差);曲線在 100 到 150 棵之間通常已經(jīng)形成平臺(tái)之后再加樹只是線性增加訓(xùn)練時(shí)間。需要注意的是oobError返回的是累積誤差第 k 個(gè)元素對(duì)應(yīng)前 k 棵樹的投票結(jié)果所以它天然單調(diào)性很差、會(huì)抖動(dòng)——抖動(dòng)幅度大的時(shí)候說明數(shù)據(jù)本身噪聲重這時(shí)候盯著最小值去取樹數(shù)反而過擬合了 OOB取平臺(tái)起點(diǎn)更穩(wěn)。300 棵是個(gè)保守上限演示夠用生產(chǎn)環(huán)境按數(shù)據(jù)量自己壓。4.2 MinLeafSize偏差與方差最直接的旋鈕葉子最小樣本數(shù)是控制單棵樹復(fù)雜度的主要參數(shù)。值越小樹越深訓(xùn)練集誤差越低方差越大值越大樹越淺偏差上升但對(duì)噪聲的容忍度更好。分類任務(wù)上1 到 10 是常見搜索區(qū)間回歸任務(wù)建議從 5 起步。leafList [1 3 5 10 20 50]; oob zeros(size(leafList)); for i 1:numel(leafList) rf TreeBagger(150, X, Y, Method, classification, ... OOBPrediction, on, MinLeafSize, leafList(i)); oob(i) oobError(rf, Mode, last); % 只看最終樹的 OOB 誤差 end [bestErr, bestIdx] min(oob); fprintf(最優(yōu) MinLeafSize %dOOB 誤差 %.4f\n, leafList(bestIdx), bestErr);oobError(rf,Mode,last)跟默認(rèn)的累積序列不是一回事加Mode,last拿到的是最終集成的單個(gè)誤差值做網(wǎng)格循環(huán)時(shí)用它更省事。如果這條曲線一路往下走到 1 才停通常說明數(shù)據(jù)里的噪聲不多、或者樣本量夠大如果曲線在 10 附近觸底再往上走就要考慮特征里是不是混進(jìn)了大量弱相關(guān)列。4.3 NumPredictorsToSamplemtry 的經(jīng)驗(yàn)值與實(shí)測(cè)這是隨機(jī)森林「隨機(jī)」的核心參數(shù)。每次分裂只從 p 個(gè)特征里抽 m 個(gè)候選m 越小樹之間越不相關(guān)集成的方差降得越多但單棵樹的精度也越低。經(jīng)驗(yàn)值是分類取 √p、回歸取 p/3這個(gè)起點(diǎn)在大部分?jǐn)?shù)據(jù)上不會(huì)太離譜但不是最優(yōu)。p size(X, 2); mtryList unique(round([sqrt(p), p/3, p/2, p])); % 經(jīng)驗(yàn)值加兩個(gè)擴(kuò)展點(diǎn) for i 1:numel(mtryList) rf TreeBagger(150, X, Y, Method, classification, ... OOBPrediction, on, NumPredictorsToSample, mtryList(i)); fprintf(mtry%3d OOB誤差%.4f\n, mtryList(i), oobError(rf,Mode,last)); end一個(gè)常見誤用是把 mtry 設(shè)成 p也就是每次分裂都用全部特征然后把模型叫作隨機(jī)森林。這時(shí)候它退化成了純 bagging樹之間的相關(guān)性明顯提高集成降方差的效果打折在特征高度相關(guān)的數(shù)據(jù)上尤其明顯。實(shí)測(cè)中如果 mtryp 和 mtry√p 的 OOB 誤差差不多選 √p 更劃算因?yàn)橛?xùn)練快、樹之間更獨(dú)立。4.4 自動(dòng)調(diào)參與并行把網(wǎng)格交給 bayesopt參數(shù)超過三個(gè)之后手寫循環(huán)的組合爆炸會(huì)吞掉大量時(shí)間。fitcensemble支持內(nèi)置的貝葉斯優(yōu)化把搜索空間一次性描述清楚讓它自己找。rng(2024); t templateTree(Surrogate, off); mdl fitcensemble(X, Y, Method, Bag, Learners, t, ... OptimizeHyperparameters, {NumLearningCycles, MinLeafSize, ... NumVariablesToSample}, ... HyperparameterOptimizationOptions, struct( ... MaxObjectiveEvaluations, 30, ... % 目標(biāo)函數(shù)評(píng)估次數(shù)30 起步 UseParallel, true, ... % 并行評(píng)估需要并行工具箱 Repartition, true, ... % 每輪重新劃分降低劃分偏差 Verbose, 1));Repartition,true值得開著它讓每輪評(píng)估用不同的交叉驗(yàn)證劃分避免某組參數(shù)恰好抽到一份「好切分」而虛高。MaxObjectiveEvaluations給 30 次是在時(shí)間和收益之間的折中數(shù)據(jù)量大的時(shí)候可以先在小樣本上搜一輪拿到大致范圍再回到全量數(shù)據(jù)上手工微調(diào)。并行開啟前最好先手動(dòng)起一次本地池否則第一次調(diào)用會(huì)額外花時(shí)間初始化。5. 常見排查隨機(jī)森林 MATLAB 代碼跑不動(dòng)、跑不準(zhǔn)的五個(gè)現(xiàn)場(chǎng)調(diào)參投入再多也擋不住輸入層面的一些低級(jí)錯(cuò)誤。總結(jié)下來有五類高頻現(xiàn)場(chǎng)每次排查都從這幾條過一遍能省掉大量猜測(cè)。5.1 準(zhǔn)確率恰好等于多數(shù)類占比現(xiàn)象測(cè)試集準(zhǔn)確率 0.83而訓(xùn)練集里正類占比正好 83%混淆矩陣顯示模型把所有樣本都判成了多數(shù)類。原因類別嚴(yán)重不平衡隨機(jī)森林的投票機(jī)制下少數(shù)類的票數(shù)被壓過去樹在分裂時(shí)也傾向于先把多數(shù)類分干凈因?yàn)樗鼘?duì)整體不純度的貢獻(xiàn)更大。這時(shí)準(zhǔn)確率這個(gè)指標(biāo)本身已經(jīng)失效。解決三條一起上更穩(wěn)——cvpartition傳標(biāo)簽做分層抽樣保證每折里少數(shù)類都有fitcensemble把Prior設(shè)成uniform壓制先驗(yàn)帶來的偏置評(píng)估指標(biāo)換成confusionmat之外看召回率或者 F1別只看準(zhǔn)確率。如果少數(shù)類樣本少于幾百條還要考慮過采樣但過采樣必須在訓(xùn)練折內(nèi)部做不能在劃分前對(duì)全量數(shù)據(jù)做。5.2 標(biāo)簽類型報(bào)錯(cuò)或者預(yù)測(cè)結(jié)果全 false現(xiàn)象訓(xùn)練階段報(bào)Y must be a vector或者代碼沒報(bào)錯(cuò)但Yfit Y全是 false。原因標(biāo)簽是字符串 cell 或者表格里的 string 列predict返回的是字符 cell 數(shù)組直接和數(shù)值或 categorical 標(biāo)簽比較類型不匹配比較結(jié)果全是 false看起來像模型完全沒用。解決訓(xùn)練前統(tǒng)一Y categorical(T.label)預(yù)測(cè)后立刻Yfit categorical(predict(rf, Xtest))再做比較?;貧w任務(wù)同理TreeBagger的回歸模型predict返回的是數(shù)值矩陣但如果是多輸出或者用了 cell 輸入也要檢查維度對(duì)齊。這類問題不會(huì)給明確報(bào)錯(cuò)只能靠打印前幾行class(Y)和class(Yfit)定位。5.3 內(nèi)存爆掉或者訓(xùn)練慢到無法接受現(xiàn)象樹數(shù)加到 500 之后訓(xùn)練要跑半小時(shí)內(nèi)存占用飆到十幾 G機(jī)器開始交換。原因每棵樹都把完整訓(xùn)練集副本存進(jìn)對(duì)象里MinLeafSize太小時(shí)單棵樹節(jié)點(diǎn)數(shù)暴漲再加上OOBPrediction會(huì)額外保存袋外索引內(nèi)存是乘法級(jí)增長(zhǎng)的。解決先把MinLeafSize從 1 提到 5 到 10節(jié)點(diǎn)數(shù)能降一個(gè)量級(jí)UseParallel打開用多核分?jǐn)倳r(shí)間但要確認(rèn)并行池的進(jìn)程數(shù)不要超過物理核數(shù)超了反而更慢樹數(shù)按 4.1 的 OOB 曲線取平臺(tái)起點(diǎn)別一上來就 1000 棵。特征維度超過幾百列時(shí)先做一輪基于重要性的粗篩再重新訓(xùn)練比硬扛著跑全量特征劃算得多。5.4 OOB 誤差很低但測(cè)試集一塌糊涂現(xiàn)象訓(xùn)練時(shí) OOB 誤差 0.06交叉驗(yàn)證或者獨(dú)立測(cè)試集誤差 0.25差距大得不正常。原因數(shù)據(jù)泄漏。最常見的是在劃分之前對(duì)整張表做了標(biāo)準(zhǔn)化、缺失值填補(bǔ)、特征篩選或者 SMOTE 過采樣測(cè)試集的分布信息通過這些步驟滲進(jìn)了訓(xùn)練過程。另一種是同一實(shí)體有多條記錄被分到了訓(xùn)練集和測(cè)試集兩側(cè)模型實(shí)際上在背答案。解決把整條流水線重排成「切分 → 訓(xùn)練集上擬合變換 → 應(yīng)用到測(cè)試集」。如果有分組結(jié)構(gòu)同一個(gè)體多次測(cè)量、同一批次多個(gè)樣本用cvpartition的分組形式保證整組進(jìn)同一側(cè)。改完之后 OOB 和測(cè)試誤差的差距通常會(huì)收斂到 3 到 5 個(gè)百分點(diǎn)以內(nèi)還收不攏就要懷疑特征里混了標(biāo)簽派生的列。5.5 特征重要性全是零或者與業(yè)務(wù)直覺相反現(xiàn)象OOBPermutedPredictorDeltaError返回一列數(shù)前幾個(gè)全是 0或者排第一的是一個(gè)業(yè)務(wù)上明顯無關(guān)的字段。原因三個(gè)可能——一是特征之間高度相關(guān)重要性被平攤到幾個(gè)相關(guān)列上單獨(dú)看每個(gè)都不高二是標(biāo)簽編碼或者 ID 列被當(dāng)成了有效特征模型找到了捷徑三是樹太淺某些特征根本沒被用到重要性自然為 0。解決先把 ID 類、時(shí)間戳類、索引類列手工剔掉再重跑一次。相關(guān)特征的問題用分組置換重要性處理把一組相關(guān)特征同時(shí)打亂再算精度下降而不是逐個(gè)打亂。樹太淺就按 4.2 把MinLeafSize降回來。排查時(shí)用predictorImportance和置換重要性各算一遍兩個(gè)結(jié)果差異很大的那幾列往往是需要單獨(dú)看一眼的。6. 進(jìn)階把 OOB 誤差、置換重要性和偏依賴用成決策依據(jù)模型跑通只是起點(diǎn)真正決定這份代碼有沒有用的是后面三件事誤差估計(jì)可不可信、特征排序能不能指導(dǎo)下一步采集、單特征的影響方向說不說得清。誤差估計(jì)上我習(xí)慣同時(shí)看三個(gè)數(shù)OOB 誤差、5 折交叉驗(yàn)證誤差、獨(dú)立測(cè)試集誤差。三個(gè)數(shù)互相之間的差距比絕對(duì)值更有信息量——OOB 和交叉驗(yàn)證差得多說明樣本順序有結(jié)構(gòu)交叉驗(yàn)證和測(cè)試集差得多說明分布漂移或者泄漏。這三個(gè)數(shù)都穩(wěn)定在同一個(gè)量級(jí)模型才算立住。特征重要性不要只信內(nèi)置那一列。手寫一遍置換重要性邏輯簡(jiǎn)單但可控把某一列打亂重新預(yù)測(cè)看精度掉多少掉得越多越重要。重復(fù)多次取均值能壓掉隨機(jī)性。baseAcc mean(categorical(predict(rf, Xtest)) Ytest); imp zeros(1, size(Xtest,2)); nRepeat 10; for j 1:size(Xtest,2) drop zeros(nRepeat,1); for r 1:nRepeat Xp Xtest; Xp{:,j} Xp(randperm(height(Xp)), j); % 打亂第 j 列其余不動(dòng) drop(r) baseAcc - mean(categorical(predict(rf, Xp)) Ytest); end imp(j) mean(drop); end [~, order] sort(imp, descend); disp(Xtest.Properties.VariableNames(order(1:10)));randperm打亂整列是關(guān)鍵不能只交換相鄰行否則相關(guān)性結(jié)構(gòu)還在重要性會(huì)被嚴(yán)重低估。重復(fù) 10 次的成本可以接受如果預(yù)測(cè)很慢降到 5 次也夠看趨勢(shì)。單特征影響方向用偏依賴看fitcensemble訓(xùn)練出的模型直接plotPartialDependence(mdl, j)就能出圖看的是「把第 j 列固定在不同取值上其余列保持原樣時(shí)預(yù)測(cè)概率的平均變化」。圖上出現(xiàn)明顯的臺(tái)階或者非單調(diào)折線通常意味著樹在這個(gè)特征上切出了硬閾值這時(shí)候回頭去看MinLeafSize是不是設(shè)得太小。TreeBagger 對(duì)象沒有這個(gè)接口需要自己構(gòu)造網(wǎng)格、替換列、批量預(yù)測(cè)再取均值代碼不長(zhǎng)但容易寫錯(cuò)列索引建議封裝成小函數(shù)復(fù)用。血淚經(jīng)驗(yàn)是這三件事里最容易被跳過的是置換重要性因?yàn)閮?nèi)置的OOBPermutedPredictorDeltaError一行就能出結(jié)果看起來已經(jīng)夠了。但在相關(guān)特征多的表上內(nèi)置結(jié)果和置換結(jié)果能差出完全不同的排序而排序錯(cuò)了后面所有的特征采集和工程投入都會(huì)跟著錯(cuò)。從那以后我每次拿到新的特征表都強(qiáng)制把分組置換重要性跑一遍再下結(jié)論。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取