@浙大疏錦行
知識點:
- 不平衡數據集的處理策略:過采樣、修改權重、修改閾值
- 交叉驗證代碼
過采樣
過采樣一般包含2種做法:隨機采樣和SMOTE
過采樣是把少的類別補充和多的類別一樣多,欠采樣是把多的類別減少和少的類別一樣
一般都是缺數據,所以很少用欠采樣?
隨機過采樣ROS
隨機過采樣是從少數類中隨機選擇樣本,并將其復制后添加到訓練集。
隨機過采樣的步驟如下:
確定少數類的樣本數。
從少數類中隨機選擇樣本,并將其復制。
將復制的樣本添加到訓練集。
隨機過采樣的優點是,它可以增加少數類的樣本數,從而提高模型的泛化能力小。
隨機過采樣的缺點是,它可能會增加訓練集的大小,從而增加訓練時間。此外,它可能會增加
噪聲,并且可能會增加模型的偏差。
?smote過采樣
smote:過采樣是合成樣本的方法。
對于少數類中的每個樣本,計算它與少數類中其他樣本的距離,得到其k近鄰(一般k取5或其他合適的值)。
從k近鄰中隨機選擇一個樣本。
計算選定的近鄰樣本與原始樣本之間的差值。
生成一個在0到1之間的隨機數。
將差值乘以隨機數,然后加到原始樣本上,得到一個新的合成樣本。
重復上述步驟,直到合成出足夠數量的少數類樣本,使得少數類和多數類樣本數量達到某種平衡。
使用過采樣后的數據集訓練模型并評估模型性能。
SMOTEE的核心思想是通過在少數類樣本的特征空間中進行插值來合成新的樣本
修改權重
在處理類別不平衡的數據集時,標準機器學習算法(如默認的隨機森林)可能會過度偏向多數類,導致對少數類的預測性能很差。為了解決這個問題,常用的策略包括在數據層面(采樣)和算法層面進行調整。本文重點討論兩種算法層面的方法:修改類別權重和修改分類閾值。
挑戰:標準算法的優化目標(如最小化整體誤差)會使其優先擬合多數類,因為這樣做能更快地降低總誤差。
后果:對少數類樣本的識別能力不足(低召回率),即使整體準確率看起來很高。
目標:提高模型對少數類的預測性能,通常關注召回率(Recall)、F1分數(F1-Score))、AUC-PR等指標。
?方法一:修改類別權重(Cost-SensitiveLearning)
這種方法在模型訓練階段介入,通過調整不同類別樣本對損失函數的貢獻來影響模型的學習過程。
核心思想:為不同類別的錯誤分類分配不同的”代價”或”權重”。通常,將少數類樣本錯分為多數類的代價設置得遠高于反過來的情況。
作用機制:修改模型的損失函數。當模型錯誤分類一個具有高權重的少數類樣本時,會受到更大的懲罰(更高的損失值)。
目的:迫使學習算法在優化參數時更加關注少數類,努力學習到一個能夠更好地區分少數類的決策邊界。它試圖從根本上讓模型“學會”識別少數類。
影響:直接改變模型的參數學習過程和最終學到的模型本身。
在RandomForestClassifier中應用(class_weight參數)
Scikit-learn中的RandomForestclassifier提供了class_weight參數來實現代價敏感學
習
1.class_weight=None(默認值):
所有類別被賦予相同的權重(1)。
算法在構建樹和計算分裂標準(如基尼不純度)時,不區分多數類和少數類。
在不平衡數據上,這自然導致模型偏向多數類。
?2.class weight='balanced':
算法自動根據訓練數據y中各類別的頻率來調整權重(1)。
權重計算方式與類別頻率成反比:weight=n_samples/(n_classes*np.bincount(y))。
這意味著少數類樣本獲得更高的權重,多數類樣本獲得較低的權重。
目的是在訓練中“放大"少數類的重要性,促使模型提升對少數類的識別能力。
3.class_weight={dict}(手動設置):
可以提供一個字典,手動為每個類別標簽指定權重,例如class_weight={:1,1:10}表示類別1的權重是類別0的10倍。
●優點:
從模型學習的根本上解決問題。
可能得到泛化能力更強的模型。
許多常用算法內置支持,實現方便。
●注意:使用class_weight時,推薦結合交叉驗證(特別是StratifiedKFold)來可靠地評估其效果和模型的穩定性。
方法二:修改分類閾值
這種方法在模型訓練完成之后介入,通過調整最終分類的決策規則來平衡不同類型的錯誤。
核心思想:改變將模型輸出的概率(或得分)映射到最終類別標簽的門檻。
作用機制:模型通常輸出一個樣本屬于正類(通常設為少數類)的概率p。默認情況下,如果p>0.5,則預測為正類。修改閾值意味著改變這個0.5,例如,如果要求更高的召回率,可以將閾值降低(如p>0.3就預測為正類)。
目的:在不改變已訓練好的模型的情況下,根據業務需求調整精確率(Precision)和召回率(Recall)之間的權衡。通常用于提高少數類的召回率(但可能會犧牲精確率)。
影響:不改變模型學到的參數或決策邊界本身,只改變如何解釋模型的輸出。
優點:
實現簡單,無需重新訓練模型。
非常直觀,可以直接在PR曲線或ROC曲線上選擇操作點。
適用于任何輸出概率或分數的模型。
缺點:
治標不治本。如果模型本身就沒學好如何區分少數類(概率輸出普遍很低),單純降低閾值可能效果有限或導致大量誤報(低精確率)。
?實踐建議
評估指標先行:明確你的目標,使用適合不平衡數據的指標(Recall,F1-Score,AUC-PRBalanced Accuracy,MCC)來評估模型。
優先嘗試根本方法:通常建議首先嘗試修改權重(class_weight='balanced')或數據采樣方法如SMOT),因為它們試圖從源頭改善模型學習。
交叉驗證評估:在使用class_weight或采樣方法時,務必使用分層交叉驗證(Stratified K-Fold來獲得對模型性能的可靠估計。
閾值調整作為補充:修改閾值可以作為一種補充手段或最后的微調。即使使用了權重調整,有時仍需根據具體的業務需求(如必須達到某個召回率水平)來調整閾值,找到最佳的操作點。
組合策略:有時結合多種方法(如SMOTE+class_weight)可能會產生更好的結果。
總之,修改權重旨在訓練一個“更好”的模型,而修改閾值是在一個“已有”模型上調整其表現。
理解它們的差異有助于你選擇更合適的策略來應對不平衡數據集的挑戰。
import numpy as np # 引入 numpy 用于計算平均值等
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold, cross_validate # 引入分層 K 折和交叉驗證工具
from sklearn.metrics import make_scorer, accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report
import time
import warnings
warnings.filterwarnings("ignore")# 假設 X_train, y_train, X_test, y_test 已經準備好
# X_train, y_train 用于交叉驗證和最終模型訓練
# X_test, y_test 用于最終評估# --- 1. 默認參數的隨機森林 (原始代碼,作為對比基準) ---
print("--- 1. 默認參數隨機森林 (訓練集 -> 測試集) ---")
start_time = time.time()
rf_model_default = RandomForestClassifier(random_state=42)
rf_model_default.fit(X_train, y_train)
rf_pred_default = rf_model_default.predict(X_test)
end_time = time.time()
print(f"默認模型訓練與預測耗時: {end_time - start_time:.4f} 秒")
print("\n默認隨機森林 在測試集上的分類報告:")
print(classification_report(y_test, rf_pred_default))
print("默認隨機森林 在測試集上的混淆矩陣:")
print(confusion_matrix(y_test, rf_pred_default))
print("-" * 50)# --- 2. 帶權重的隨機森林 + 交叉驗證 (在訓練集上進行CV) ---
print("--- 2. 帶權重隨機森林 + 交叉驗證 (在訓練集上進行) ---")# 確定少數類標簽 (非常重要!)
# 假設是二分類問題,我們需要知道哪個是少數類標簽才能正確解讀 recall, precision, f1
# 例如,如果標簽是 0 和 1,可以這樣查看:
counts = np.bincount(y_train)
minority_label = np.argmin(counts) # 找到計數最少的類別的標簽
majority_label = np.argmax(counts)
print(f"訓練集中各類別數量: {counts}")
print(f"少數類標簽: {minority_label}, 多數類標簽: {majority_label}")
# !!下面的 scorer 將使用這個 minority_label !!# 定義帶權重的模型
rf_model_weighted = RandomForestClassifier(random_state=42,class_weight='balanced' # 關鍵:自動根據類別頻率調整權重# class_weight={minority_label: 10, majority_label: 1} # 或者可以手動設置權重字典
)# 設置交叉驗證策略 (使用 StratifiedKFold 保證每折類別比例相似)
cv_strategy = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # 5折交叉驗證# 定義用于交叉驗證的評估指標
# 特別關注少數類的指標,使用 make_scorer 指定 pos_label
# 注意:如果你的少數類標簽不是 1,需要修改 pos_label
scoring = {'accuracy': 'accuracy','precision_minority': make_scorer(precision_score, pos_label=minority_label, zero_division=0),'recall_minority': make_scorer(recall_score, pos_label=minority_label),'f1_minority': make_scorer(f1_score, pos_label=minority_label)
}print(f"開始進行 {cv_strategy.get_n_splits()} 折交叉驗證...")
start_time_cv = time.time()# 執行交叉驗證 (在 X_train, y_train 上進行)
# cross_validate 會自動完成訓練和評估過程
cv_results = cross_validate(estimator=rf_model_weighted,X=X_train,y=y_train,cv=cv_strategy,scoring=scoring,n_jobs=-1, # 使用所有可用的 CPU 核心return_train_score=False # 通常我們更關心測試折的得分
)end_time_cv = time.time()
print(f"交叉驗證耗時: {end_time_cv - start_time_cv:.4f} 秒")# 打印交叉驗證結果的平均值
print("\n帶權重隨機森林 交叉驗證平均性能 (基于訓練集劃分):")
for metric_name, scores in cv_results.items():if metric_name.startswith('test_'): # 我們關心的是在驗證折上的表現# 提取指標名稱(去掉 'test_' 前綴)clean_metric_name = metric_name.split('test_')[1]print(f" 平均 {clean_metric_name}: {np.mean(scores):.4f} (+/- {np.std(scores):.4f})")print("-" * 50)# --- 3. 使用權重訓練最終模型,并在測試集上評估 ---
print("--- 3. 訓練最終的帶權重模型 (整個訓練集) 并在測試集上評估 ---")
start_time_final = time.time()
# 使用與交叉驗證中相同的設置來訓練最終模型
rf_model_weighted_final = RandomForestClassifier(random_state=42,class_weight='balanced'
)
rf_model_weighted_final.fit(X_train, y_train) # 在整個訓練集上訓練
rf_pred_weighted = rf_model_weighted_final.predict(X_test) # 在測試集上預測
end_time_final = time.time()print(f"最終帶權重模型訓練與預測耗時: {end_time_final - start_time_final:.4f} 秒")
print("\n帶權重隨機森林 在測試集上的分類報告:")
# 確保 classification_report 也關注少數類 (可以通過 target_names 參數指定標簽名稱)
# 或者直接查看報告中少數類標簽對應的行
print(classification_report(y_test, rf_pred_weighted)) # , target_names=[f'Class {majority_label}', f'Class {minority_label}'] 如果需要指定名稱
print("帶權重隨機森林 在測試集上的混淆矩陣:")
print(confusion_matrix(y_test, rf_pred_weighted))
print("-" * 50)# 對比總結 (簡單示例)
print("性能對比 (測試集上的少數類召回率 Recall):")
recall_default = recall_score(y_test, rf_pred_default, pos_label=minority_label)
recall_weighted = recall_score(y_test, rf_pred_weighted, pos_label=minority_label)
print(f" 默認模型: {recall_default:.4f}")
print(f" 帶權重模型: {recall_weighted:.4f}")
--- 1. 默認參數隨機森林 (訓練集 -> 測試集) ---
默認模型訓練與預測耗時: 1.2171 秒默認隨機森林 在測試集上的分類報告:
? ? ? ? ? ? ? precision ? ?recall ?f1-score ? support? ? ? ? ? ?0 ? ? ? 0.77 ? ? ?0.97 ? ? ?0.86 ? ? ?1059
? ? ? ? ? ?1 ? ? ? 0.79 ? ? ?0.30 ? ? ?0.43 ? ? ? 441? ? accuracy ? ? ? ? ? ? ? ? ? ? ? ? ? 0.77 ? ? ?1500
? ?macro avg ? ? ? 0.78 ? ? ?0.63 ? ? ?0.64 ? ? ?1500
weighted avg ? ? ? 0.77 ? ? ?0.77 ? ? ?0.73 ? ? ?1500默認隨機森林 在測試集上的混淆矩陣:
[[1023 ? 36]
?[ 309 ?132]]
--------------------------------------------------
--- 2. 帶權重隨機森林 + 交叉驗證 (在訓練集上進行) ---
訓練集中各類別數量: [4328 1672]
少數類標簽: 1, 多數類標簽: 0
開始進行 5 折交叉驗證...
交叉驗證耗時: 3.6423 秒帶權重隨機森林 交叉驗證平均性能 (基于訓練集劃分):
? 平均 accuracy: 0.7798 (+/- 0.0085)
? 平均 precision_minority: 0.8291 (+/- 0.0182)
? 平均 recall_minority: 0.2650 (+/- 0.0400)
? 平均 f1_minority: 0.3998 (+/- 0.0455)
--------------------------------------------------
--- 3. 訓練最終的帶權重模型 (整個訓練集) 并在測試集上評估 ---
最終帶權重模型訓練與預測耗時: 1.1657 秒帶權重隨機森林 在測試集上的分類報告:
? ? ? ? ? ? ? precision ? ?recall ?f1-score ? support? ? ? ? ? ?0 ? ? ? 0.76 ? ? ?0.97 ? ? ?0.86 ? ? ?1059
? ? ? ? ? ?1 ? ? ? 0.81 ? ? ?0.27 ? ? ?0.41 ? ? ? 441? ? accuracy ? ? ? ? ? ? ? ? ? ? ? ? ? 0.77 ? ? ?1500
? ?macro avg ? ? ? 0.78 ? ? ?0.62 ? ? ?0.63 ? ? ?1500
weighted avg ? ? ? 0.78 ? ? ?0.77 ? ? ?0.72 ? ? ?1500帶權重隨機森林 在測試集上的混淆矩陣:
[[1030 ? 29]
?[ 320 ?121]]
--------------------------------------------------
性能對比 (測試集上的少數類召回率 Recall):
? 默認模型: 0.2993
? 帶權重模型: 0.2744
作業:
從示例代碼可以看到 效果沒有變好,所以很多步驟都是理想是好的,但是現實并不一定可以變好。這個實驗仍然有改進空間,如下。
1. 我還沒做smote+過采樣+修改權重的組合策略,有可能一起做會變好。
2. 我還沒有調參,有可能調參后再取上述策略可能會變好
針對上面這2個探索路徑,繼續嘗試下去,看看是否符合猜測。