前面我们已经学习了 KNN、逻辑回归等基础算法,今天来介绍集成学习中最经典、最实用的算法 ——随机森林(Random Forest)。它既能处理分类、回归任务,又能输出特征重要性,对不平衡数据也有不错的适应性,是目前机器学习最常用的算法之一。

一、什么是随机森林?

随机森林是由Leo Breiman在 2001 年提出的集成学习算法,核心思想是 “三个臭皮匠,顶个诸葛亮”:通过构建多个相互独立的决策树,将它们的预测结果进行分类或回归,得到最终预测结果,从而提升模型的泛化能力和稳定性。

1.1 核心思想

  • 基学习器:随机森林的 “成员” 是多个决策树(Decision Tree),每个决策树都是一个弱学习器;
  • 随机性:通过 “样本随机采样” 和 “特征随机选择”,让每个决策树学习到不同的数据特征,避免决策树之间的相关性;
  • 集成策略:分类任务采用 “多数投票”,回归任务采用 “均值预测”,综合多个决策树的结果,降低过拟合风险。

集成学习主要有三大经典代表方法,各有侧重:

1.Bagging 方法:并行训练多个独立的基学习器,最终通过投票 / 平均整合结果,随机森林是其典型表;

2.Boosting 方法:串行训练基学习器,后续学习器专注修正前序学习器的错误,XGBoost、LightGBM 是典型代表;

3.tacking 方法:堆叠多个基学习器,将其输出作为新特征训练上层模型,实现多层级集成。

集成学习的应用场景非常广泛,涵盖分类问题集成、回归问题集成、特征选取集成等,是工业界提升模型性能的常用手段。而随机森林,正是 Bagging 方法中最成熟、应用最广的算法。

1.2 为什么叫 “随机森林”

  • 随机:体现在两个层面上:
    1. 样本随机(Bootstrap 抽样):从原始训练集中有放回地随机抽取样本,构建每个决策树的训练集;
    2. 特征随机:在决策树的每个节点分裂时,随机选择部分特征,再从中选择最优分裂特征;
  • 森林:由多个决策树组成,像一片森林一样协同工作。

二、随机森林的核心原理

随机森林的生成遵循 Bagging 的经典结构。

  • 原始数据集准备:准备好标注好的训练数据集,作为整个随机森林的训练基础;
  • 随机采样生成子样本:通过有放回的随机采样,从原始数据集中生成多个相互独立的子样本集 D1、D2、…、Di;
  • 独立训练基分类器:为每个子样本集训练一棵决策树分类器,得到 C1、C2、…、Ci,所有决策树并行训练、相互独立,无信息交互;
  • 集成得到强分类器:对于分类任务,采用多数投票原则,所有决策树中得票最多的类别为最终预测结果;对于回归任务,采用均值平均原则,所有决策树的预测值平均值为最终结果。

整个过程中,每棵决策树都是一个 “弱学习器”,但通过 Bagging 的集成策略,最终组合成一个性能优越的 “强学习器”。

四、随机森林的优缺点

4.1 优点

  • 准确率极高:多棵决策树的集成效果,远优于单棵决策树,在各类分类、回归任务中都有出色表现;
  • 抗噪声能力强:随机性的引入,让模型对数据中的噪声不敏感,即使数据存在少量异常值,也不会大幅影响模型性能;
  • 不易过拟合:随机采样和特征随机选择,让每棵决策树的学习视角不同,有效避免了单棵决策树过拟合的问题,泛化能力优秀;
  • 适配高维数据:能够处理很高维度的数据集,无需提前做复杂的特征选择,模型可自动挖掘特征价值;
  • 易实现并行化计算:每棵决策树的训练相互独立,可利用多节点并行训练,大幅提升训练效率,适配大数据场景;
  • 功能拓展性强:除了分类和回归,还能输出特征重要性,助力特征选取和数据理解。

4.2 缺点

  1. 训练成本较高:当森林中的决策树个数很多时,模型训练需要的存储空间和时间会显著增加,对硬件有一定要求;
  2. 可解释性差:作为黑盒模型,随机森林无法像单棵决策树那样清晰展示决策逻辑,只能得到预测结果,难以对结果进行直观解释;
  3. 对极端不平衡数据敏感:若数据类别极度不平衡,未做处理的随机森林会偏向多数类,需结合采样策略或权重调整优化。

五、随机森林实战:信用卡欺诈检测

对这个数据集构建随机森林模型,并调整参数,提高模型的召回率。

import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics
import matplotlib.pyplot as plt
from pylab import mpl
import warnings

warnings.filterwarnings('ignore', category=FutureWarning)
mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei']
mpl.rcParams['axes.unicode_minus'] = False

data = pd.read_csv(r"creditcard.csv")

scaler = StandardScaler()
data['Amount'] = scaler.fit_transform(data[['Amount']])
data = data.drop(['Time'], axis=1)

X_whole = data.drop('Class', axis=1)
y_whole = data.Class

x_train_w, x_test_w, y_train_w, y_test_w = \
    train_test_split(X_whole, y_whole, test_size=0.3, random_state=1000)

# 2. 下采样处理
train_data = pd.concat([x_train_w, y_train_w], axis=1)
positive_eg = train_data[train_data['Class'] == 0]
negative_eg = train_data[train_data['Class'] == 1]
positive_eg_down = positive_eg.sample(n=len(negative_eg), random_state=1000)
train_data_down = pd.concat([positive_eg_down, negative_eg], axis=0)
x_train_down = train_data_down.drop('Class', axis=1)
y_train_down = train_data_down['Class']

scores = []
n_estimators_range = [4, 5, 6, 10, 15, 20, 25, 50, 75, 100, 150, 200, 250]

for n in n_estimators_range:
    rf = RandomForestClassifier(
        n_estimators=n,
        max_depth=10,
        random_state=41,
        n_jobs=-1,
    )
    score = cross_val_score(rf, x_train_down, y_train_down, cv=10, scoring='recall')
    score_mean = score.mean()
    scores.append(score_mean)
    print(f"n_estimators={n} 时,交叉验证召回率均值:{score_mean:.4f}")

best_n = n_estimators_range[scores.index(max(scores))]
print(f"\n最优决策树数量为:{best_n}")

rf_best = RandomForestClassifier(
    n_estimators=best_n,
    max_depth=10,
    random_state=41,
    n_jobs=-1,
)
rf_best.fit(x_train_down, y_train_down)

print(f"测试集准确率:{rf_best.score(x_test_w, y_test_w):.4f}")

train_predicted = rf_best.predict(x_train_down)
print("\n【训练集分类报告】")
print(metrics.classification_report(y_train_down, train_predicted))

test_predicted = rf_best.predict(x_test_w)
print("\n【测试集分类报告】")
print(metrics.classification_report(y_test_w, test_predicted))

print("\n===== 特征重要性分析 =====")
feature_importance = pd.DataFrame({
    '特征名': X_whole.columns,
    '重要性': rf_best.feature_importances_
}).sort_values(by='重要性', ascending=False)

print("全部特征的重要性(按重要性降序):")
print(feature_importance)

plt.figure(figsize=(16, 10))
bars = plt.bar(
    x=feature_importance['特征名'],
    height=feature_importance['重要性'],
    alpha=0.8
)
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2., height + 0.0005,
             f'{height:.4f}', ha='center', va='bottom', fontsize=8)
plt.title('随机森林全部特征重要性(降序)', fontsize=16, pad=20)
plt.xlabel('特征名', fontsize=14)
plt.ylabel('重要性', fontsize=14)
plt.xticks(rotation=60, ha='right', fontsize=10)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('全部特征重要性.png', dpi=300, bbox_inches='tight')
plt.show()

cm = metrics.confusion_matrix(y_test_w, test_predicted)
plt.figure(figsize=(8, 6))
im = plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.title('测试集混淆矩阵(欺诈检测)', fontsize=14, pad=20)
plt.colorbar(im, shrink=0.8)

classes = ['正常交易', '欺诈交易']
tick_marks = [0, 1]
plt.xticks(tick_marks, classes, fontsize=12)
plt.yticks(tick_marks, classes, fontsize=12)

thresh = cm.max() / 2.
for i in range(cm.shape[0]):
    for j in range(cm.shape[1]):
        plt.text(j, i, format(cm[i, j], 'd'),
                 ha="center", va="center",
                 color="white" if cm[i, j] > thresh else "black",
                 fontsize=14)

plt.ylabel('真实标签', fontsize=12)
plt.xlabel('预测标签', fontsize=12)
plt.tight_layout()
plt.savefig('混淆矩阵.png', dpi=300, bbox_inches='tight')
plt.show()

日记

2月28日,星期六

考研出分,但是我没有考研

今天不是一个好日子,今天是2月的最后一天,我记得我当年复习行测的时候被二月阴了一下。

因为第一年二月29天,第二年二月28天和第一年相比少一天,所以我多算了一天。

今天不只是二月最后一天,还是考研出分的日子,我周围一大圈人,就没有一个对自己的考研成绩是满意的,好多连国家线都没过,超过国家线的也是擦分,而且今年工科国家线还高了。真是让人愁死。不过我就没有考研,但是秋招考央国企颗粒无收,这何尝不是另一种考研落榜。

不过,还是那句话,人生的容错率大的超乎你想象。现在的你只是迷茫的不知道该往哪个方向走,并不是走的每一条路都是错的。

更多推荐