
课前导读
欢迎来到第27课!你是否遇到过这样的困境:决策树在训练集上准确率近乎完美,一到测试集就“原形毕露”——过拟合了。这是因为单棵决策树太“敏感”,数据稍有变化就会长出完全不同的树。那么,如果我们种上100棵树,让它们一起投票决定最终结果呢?这就是集成学习的思想——用群体的智慧弥补个体的不足。
本课的主角随机森林,是集成学习中最成功、应用最广泛的算法之一。它通过两个随机性来构建“好而不同”的决策树:一是对训练数据有放回抽样(Bagging),二是对每个节点的分裂随机选择部分特征。这使得随机森林既不容易过拟合,又能处理高维数据,甚至还能输出特征重要性,告诉你哪些特征对预测贡献最大。本课将从Bagging的基本原理开始,逐步剖析随机森林的构建细节,并通过大量实战演示参数调优和性能对比。学完本课,你将拥有一个强大的“即插即用”分类/回归武器。
学习目标
完成本课学习后,你将能够:
- 解释 Bagging 的原理:Bootstrap 采样 + 基学习器并行训练 + 投票/平均
- 理解 随机森林在 Bagging 基础上增加的特征随机性(
max_features) - 使用
BaggingClassifier/BaggingRegressor进行简单的 Bagging 集成 - 使用
RandomForestClassifier/RandomForestRegressor进行随机森林建模 - 掌握 核心参数:
n_estimators(树的数量)、max_depth、max_features、bootstrap、oob_score - 利用 袋外误差(Out-of-Bag Error)作为验证集,无需交叉验证
- 提取 随机森林的特征重要性并可视化
- 调优 随机森林的超参数(网格搜索)
知识点理论讲解
一、集成学习的基本思想
集成学习(Ensemble Learning)通过构建并结合多个学习器来完成学习任务,常可获得比单一学习器显著优越的泛化性能。根据基学习器的生成方式,主要分为:
- 并行方法:基学习器独立生成,如 Bagging、随机森林。
- 串行方法:基学习器依赖生成(如 Boosting),下一课介绍。
二、Bagging(Bootstrap Aggregating)
核心思想:从原始数据集中有放回地随机抽取 n n n 个样本(Bootstrap 采样),重复 T T T 次,得到 T T T 个采样集,分别训练 T T T 个基学习器(通常是决策树)。对于分类任务,通过投票产生最终结果;回归任务则取平均值。
为什么有效:
- 降低方差:由于各个基学习器是在不同数据子集上训练的,它们往往具有较高的差异性。通过平均/投票,可以减少模型的整体方差,从而抑制过拟合。
- Bagging 对不稳定的学习器(如决策树、神经网络)效果显著,对稳定的学习器(如 KNN)提升有限。
Bootstrap 采样:每次从 n n n 个样本中有放回抽取 n n n 个样本,约 63.2 % 63.2\% 63.2% 的原始样本会在一个采样集中出现至少一次(因为 ( 1 − 1 / n ) n → 1 / e (1 - 1/n)^n \to 1/e (1−1/n)n→1/e)。剩余约 36.8 % 36.8\% 36.8% 的样本称为袋外数据(Out-Of-Bag, OOB),可用作验证集。
袋外误差:对于每个样本,使用那些未包含该样本的基学习器进行预测,然后计算整体的平均误差。OOB 误差是对泛化误差的无偏估计,可代替交叉验证。
三、随机森林(Random Forest)
随机森林是 Bagging 的扩展,在构建每棵决策树时引入特征随机性:
- 数据随机性:使用 Bootstrap 采样生成训练子集(与 Bagging 相同)。
- 特征随机性:在分裂每个节点时,不是从所有 d d d 个特征中选择最佳分裂特征,而是随机选择 k k k 个特征子集( k ≤ d k \le d k≤d),再从这 k k k 个特征中找出最佳分裂。通常 k = d k = \sqrt{d} k=d(分类)或 k = d / 3 k = d/3 k=d/3(回归)。
这种双重随机性使得树与树之间更加独立,集成后的模型方差更小,泛化能力更强。
随机森林的重要特性:
- 特征重要性:可以通过计算每个特征在所有树中带来的不纯度减少(基尼系数或信息熵)的总和(或平均),评估特征的重要性。sklearn 中通过
feature_importances_属性获取。 - 袋外评分:
oob_score_,无需单独划分验证集。 - 可并行:树之间独立,可设置
n_jobs=-1加速。
四、随机森林与 Bagging 的对比
| 方法 | 数据采样 | 特征采样 | 树相关性 | 性能 |
|---|---|---|---|---|
| Bagging(决策树) | 有放回 | 无(使用全部特征) | 较高 | 较好 |
| 随机森林 | 有放回 | 有(随机子空间) | 低 | 更优 |
五、随机森林的超参数
重要参数(影响最大):
n_estimators:树的数量,越大通常越好,但边际效益递减,且计算时间线性增长。max_depth:树的最大深度,限制深度可防止过拟合。max_features:每棵树分裂时考虑的最大特征数。分类默认sqrt(n_features),回归默认n_features。调小可增加随机性,降低过拟合。min_samples_split/min_samples_leaf:与决策树相同,控制叶子节点大小。bootstrap:是否使用 Bootstrap 采样,默认 True。若设为 False,则所有树用相同数据,相当于随机子空间法。oob_score:是否使用袋外数据评估模型(分类为准确率,回归为 R²)。
次要参数:
criterion:分裂准则,分类可选'gini'或'entropy',回归可选'squared_error'、'absolute_error'。class_weight:处理类别不平衡。warm_start:是否增量训练。
六、随机森林的优缺点
| 优点 | 缺点 |
|---|---|
| 准确率高,不易过拟合(相比单棵决策树) | 内存占用大(需存储多棵树) |
| 可处理高维数据(特征几万维) | 预测速度比单棵树慢(需遍历所有树) |
| 能输出特征重要性 | 模型可解释性比单棵树差 |
| 天然处理缺失值(在训练时可) | 对于高维稀疏数据(如文本),不如线性模型 |
| 无需复杂的调参(默认参数往往不错) | 对时间序列数据效果不如专门模型 |
| 能处理混合类型特征 | 在噪声很大的数据上可能过拟合 |
核心原理通俗拆解
Bagging:从“一个专家”到“百人陪审团”
单棵决策树像一个专家,有自己独特的视角,但也容易固执己见(过拟合)。如果你找来100个专家,每人基于不同的数据子集(Bootstrap 样本)独立学习,然后投票决定结果,那么少数专家的错误就会被多数正确专家“中和”。这就是 Bagging。
随机森林:让专家们更独立
如果每个专家都用相同的全部特征来学习,他们可能都关注到相同的“强特征”,导致决策趋同(相关性高)。随机森林在每个节点只允许专家看随机一部分特征,这就强迫他们寻找不同的线索。最终,这100个专家虽然都有道理,但思路各异,投票结果更加稳健。
袋外误差:自带验证集
每次 Bootstrap 采样,大约有 36.8% 的样本没被选进当前训练集,它们就成了这棵树的“袋外”样本。对于每个样本,我们只使用那些没见过它的树来预测,然后计算整体准确率。这个 OOB 误差可以近似代表交叉验证误差,省去了特意划分验证集的麻烦。
底层数学逻辑
1. Bootstrap 采样的分布
Bootstrap 模拟了从真实总体中重复抽样的过程,使得我们可以估计统计量的分布(如均值、方差)。
2. 随机森林的方差降低
假设基学习器的方差为
σ
2
\sigma^2
σ2,两两相关系数为
ρ
\rho
ρ,则集成后模型的方差为:
σ
ensemble
2
=
ρ
σ
2
+
1
−
ρ
T
σ
2
\sigma^2_{\text{ensemble}} = \rho \sigma^2 + \frac{1-\rho}{T} \sigma^2
σensemble2=ρσ2+T1−ρσ2
当
T
T
T 增大时,第二项趋近于0,方差主要由相关性
ρ
\rho
ρ 决定。随机森林通过特征采样降低
ρ
\rho
ρ,从而更有效地降低方差。
3. 特征重要性计算
对于分类树,节点 t t t 的不纯度减少量为 Δ I ( t ) = I ( t ) − n l e f t n I ( t l e f t ) − n r i g h t n I ( t r i g h t ) \Delta I(t) = I(t) - \frac{n_{left}}{n} I(t_{left}) - \frac{n_{right}}{n} I(t_{right}) ΔI(t)=I(t)−nnleftI(tleft)−nnrightI(tright),其中 I I I 可以是基尼系数或熵。特征的重要性为该特征在所有节点上引起的 Δ I \Delta I ΔI 之和(按节点样本数加权)。最后归一化到 [ 0 , 1 ] [0,1] [0,1] 之间。
scikit-learn API详解
1. BaggingClassifier / BaggingRegressor
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag = BaggingClassifier(
base_estimator=DecisionTreeClassifier(), # 基学习器
n_estimators=50, # 基学习器数量
max_samples=1.0, # 每个采样集的最大样本数(比例或绝对数)
bootstrap=True, # 是否Bootstrap采样
bootstrap_features=False, # 是否对特征采样
oob_score=False, # 是否使用袋外分
n_jobs=None,
random_state=None
)
2. RandomForestClassifier / RandomForestRegressor
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_estimators=100, # 树的数量
criterion='gini', # 分裂准则
max_depth=None, # 树的最大深度
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
max_features='sqrt', # 特征采样:'sqrt','log2',整数,浮点数
max_leaf_nodes=None,
min_impurity_decrease=0.0,
bootstrap=True, # 是否Bootstrap采样
oob_score=False, # 是否使用袋外评估
n_jobs=None,
random_state=None,
verbose=0,
warm_start=False,
class_weight=None
)
# 训练
rf.fit(X_train, y_train)
# 属性
rf.estimators_ # 所有决策树对象
rf.feature_importances_ # 特征重要性
rf.oob_score_ # 袋外分数(若oob_score=True)
3. 特征重要性可视化
import matplotlib.pyplot as plt
import numpy as np
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
plt.barh(range(X.shape[1]), importances[indices])
plt.yticks(range(X.shape[1]), feature_names[indices])
环境配置与依赖安装
本课使用已有环境,无需额外安装。
conda activate sklearn_tutorial
python -c "import sklearn; print(sklearn.__version__)"
完整代码实战(带详细注释)
实战1:Bagging 基础——以决策树为基学习器
# -*- coding: utf-8 -*-
"""
对比单棵决策树与 Bagging 集成决策树在过拟合数据上的表现
"""
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 生成带噪声的分类数据(过拟合倾向高)
X, y = make_classification(n_samples=1000, n_features=20, n_informative=15,
n_redundant=5, flip_y=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 单棵决策树(不剪枝)
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)
acc_train_dt = accuracy_score(y_train, dt.predict(X_train))
acc_test_dt = accuracy_score(y_test, dt.predict(X_test))
# Bagging 集成(50棵决策树)
bag = BaggingClassifier(DecisionTreeClassifier(random_state=42),
n_estimators=50, bootstrap=True, random_state=42, n_jobs=-1)
bag.fit(X_train, y_train)
acc_train_bag = accuracy_score(y_train, bag.predict(X_train))
acc_test_bag = accuracy_score(y_test, bag.predict(X_test))
print(f"单棵决策树: 训练准确率={acc_train_dt:.4f}, 测试准确率={acc_test_dt:.4f}")
print(f"Bagging (50棵树): 训练准确率={acc_train_bag:.4f}, 测试准确率={acc_test_bag:.4f}")
print("Bagging 显著降低了过拟合(训练与测试差距减小)")
实战2:随机森林 vs 单棵决策树(分类)
# -*- coding: utf-8 -*-
"""
随机森林与单棵决策树在葡萄酒数据集上的性能对比
"""
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
wine = load_wine()
X, y = wine.data, wine.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)
rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
print(f"决策树测试准确率: {dt.score(X_test, y_test):.4f}")
print(f"随机森林测试准确率: {rf.score(X_test, y_test):.4f}")
实战3:随机森林回归(波士顿房价示例,使用糖尿病数据集)
# -*- coding: utf-8 -*-
"""
随机森林回归 vs 决策树回归
数据集:糖尿病进展
"""
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
X, y = load_diabetes(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
dt_reg = DecisionTreeRegressor(random_state=42)
rf_reg = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
dt_reg.fit(X_train, y_train)
rf_reg.fit(X_train, y_train)
y_pred_dt = dt_reg.predict(X_test)
y_pred_rf = rf_reg.predict(X_test)
print("决策树回归:")
print(f" RMSE: {mean_squared_error(y_test, y_pred_dt, squared=False):.2f}")
print(f" R²: {r2_score(y_test, y_pred_dt):.4f}")
print("\n随机森林回归:")
print(f" RMSE: {mean_squared_error(y_test, y_pred_rf, squared=False):.2f}")
print(f" R²: {r2_score(y_test, y_pred_rf):.4f}")
实战4:袋外误差(OOB Score)的使用
# -*- coding: utf-8 -*-
"""
使用袋外数据评估随机森林,无需划分验证集
"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score
X, y = load_iris(return_X_y=True)
# 启用 oob_score
rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf.fit(X, y)
print(f"袋外准确率 (OOB Score): {rf.oob_score_:.4f}")
# 为了对比,我们用交叉验证(5折)计算平均准确率
from sklearn.model_selection import cross_val_score
cv_scores = cross_val_score(rf, X, y, cv=5)
print(f"5折交叉验证平均准确率: {cv_scores.mean():.4f}")
print("OOB 准确率与交叉验证结果接近,可作为有效评估。")
实战5:特征重要性计算与可视化
# -*- coding: utf-8 -*-
"""
在红酒数据集上训练随机森林,计算并绘制特征重要性
"""
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.ensemble import RandomForestClassifier
wine = load_wine()
X, y = wine.data, wine.target
feature_names = wine.feature_names
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
plt.figure(figsize=(10,6))
plt.barh(range(X.shape[1]), importances[indices])
plt.yticks(range(X.shape[1]), [feature_names[i] for i in indices])
plt.xlabel('特征重要性')
plt.title('随机森林特征重要性(红酒数据集)')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
print("特征重要性排序:")
for i in range(X.shape[1]):
print(f"{i+1}. {feature_names[indices[i]]}: {importances[indices[i]]:.4f}")
实战6:随机森林超参数调优(GridSearchCV)
# -*- coding: utf-8 -*-
"""
使用网格搜索调优随机森林的关键参数
"""
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
digits = load_digits()
X, y = digits.data, digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 定义参数网格(为节省时间,使用较小范围)
param_grid = {
'n_estimators': [50, 100],
'max_depth': [10, 20, None],
'max_features': ['sqrt', 'log2'],
'min_samples_split': [2, 5]
}
rf = RandomForestClassifier(random_state=42, n_jobs=-1)
grid = GridSearchCV(rf, param_grid, cv=3, scoring='accuracy', n_jobs=-1, verbose=1)
grid.fit(X_train, y_train)
print("最佳参数:", grid.best_params_)
print("最佳交叉验证准确率: {:.4f}".format(grid.best_score_))
print("测试集准确率: {:.4f}".format(grid.score(X_test, y_test)))
实战7:随机森林处理缺失值(简单策略:用中位数填充)
# -*- coding: utf-8 -*-
"""
随机森林自身不能处理缺失值,但可以在预处理阶段填充
本案例演示填充 + 随机森林流程
"""
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
# 人为引入缺失值
np.random.seed(42)
mask = np.random.random(X.shape) < 0.1
X_missing = X.copy()
X_missing[mask] = np.nan
X_train, X_test, y_train, y_test = train_test_split(X_missing, y, test_size=0.2, random_state=42)
pipeline = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('rf', RandomForestClassifier(n_estimators=50, random_state=42))
])
pipeline.fit(X_train, y_train)
acc = pipeline.score(X_test, y_test)
print(f"缺失值填充 + 随机森林 准确率: {acc:.4f}")
实战8:随机森林的并行加速(n_jobs)
import time
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=5000, n_features=50, random_state=42)
for n_jobs in [1, -1]:
rf = RandomForestClassifier(n_estimators=100, n_jobs=n_jobs, random_state=42)
start = time.time()
rf.fit(X, y)
elapsed = time.time() - start
print(f"n_jobs={n_jobs:3d} 训练时间: {elapsed:.2f} 秒")
案例实操演示
案例:信用卡欺诈检测(随机森林 + OOB + 类别不平衡处理)
# 模拟极度不平衡数据
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
X, y = make_classification(n_samples=10000, n_features=20, weights=[0.99], flip_y=0, random_state=42)
print(f"正例比例: {np.mean(y):.4f}")
rf = RandomForestClassifier(n_estimators=100, class_weight='balanced', oob_score=True, random_state=42)
rf.fit(X, y)
y_pred = rf.predict(X)
print(classification_report(y, y_pred))
print(f"OOB 分数: {rf.oob_score_:.4f}")
常见报错与避坑指南
报错1:ValueError: Found array with 0 sample(s)
原因:数据为空,或拆分后训练集过小。
解决:检查数据量,确保训练集足够大(至少能容纳 Bootstrap 采样)。
报错2:MemoryError 当 n_estimators 或 max_depth 过大
原因:树太多或太深,内存爆炸。
解决:减少 n_estimators,限制 max_depth,或使用 n_jobs 减少内存副本(但实际不会降低内存占用)。可考虑使用 RandomForestClassifier 的 warm_start 增量训练。
报错3:oob_score 要求 bootstrap=True
原因:袋外分数依赖于 Bootstrap 采样。
解决:设置 bootstrap=True(默认)。
报错4:预测慢
原因:n_estimators 太大。
解决:减少树的数量,或使用 predict_proba 时也会慢;可考虑模型压缩或改用其他算法。
避坑总结
- 随机森林默认参数通常表现不错,但调整
n_estimators和max_features往往能进一步提升。 - 样本量较小时,减少
n_estimators或增大min_samples_leaf可防止过拟合。 - 高维稀疏数据(如文本)随机森林效果不一定优于线性模型。
- 特征重要性是基于不纯度减少,可能偏向取值多的特征,需谨慎解读。
- 使用
n_jobs=-1可显著加速训练(利用所有CPU核心)。
知识点总结
本课系统讲解了 Bagging 和随机森林两种集成方法:
Bagging
- 原理:Bootstrap 采样 + 多个基学习器并行 + 投票/平均,降低方差。
- 实现:
BaggingClassifier/BaggingRegressor,基学习器可自定义。
随机森林
- 双重随机性:数据 Bootstrap + 特征子采样(
max_features),使基学习器更独立。 - 关键参数:
n_estimators(树的数量)、max_depth、max_features、min_samples_split、min_samples_leaf。 - 袋外误差(OOB):利用未参与训练的样本验证,设置
oob_score=True。 - 特征重要性:
feature_importances_,基于不纯度减少。 - 调优:通常
n_estimators越大越好,但边际效应递减;max_features默认sqrt(分类)或n_features(回归);可结合 GridSearchCV。
优点与局限性
- 高准确率,不易过拟合,能处理高维数据,输出特征重要性。
- 内存大,预测速度中等,对高维稀疏数据不占优。
课后练习题
选择题
-
随机森林相比单棵决策树,主要优势是:
A. 训练速度更快 B. 降低过拟合 C. 可解释性更强 D. 不需要调参 -
Bagging 方法中,Bootstrap 采样得到的每个子集的大小与原数据集大小关系是:
A. 更小 B. 相等 C. 更大 D. 随机 -
在随机森林中,参数
max_features='sqrt'表示:
A. 使用平方根个数的特征 B. 使用全部特征 C. 使用对数个数的特征 D. 随机选择一个特征
填空题
from sklearn.ensemble import RandomForestClassifier
# 1. 创建随机森林,100棵树,启用袋外评估
rf = RandomForestClassifier(n_estimators=________, oob_score=________, random_state=42)
# 2. 训练
rf.fit(X_train, y_train)
# 3. 获取袋外分数
oob = rf.________
# 4. 获取特征重要性
importance = rf.________
实操题
-
随机森林与决策树对比:使用
load_breast_cancer数据集,分别训练单棵决策树(不剪枝)和随机森林(n_estimators=100)。比较训练集和测试集的准确率,分析哪个模型过拟合更严重。 -
特征重要性分析:在红酒数据集上训练随机森林,输出特征重要性,并画出条形图。找出最重要的两个特征,然后只用这两个特征重新训练随机森林,比较与全特征模型的性能差异。
-
超参数调优:在手写数字数据集上,使用
GridSearchCV调优随机森林的n_estimators(50, 100, 200)和max_depth(10, 20, None)。输出最佳参数及测试集准确率。
思考题
随机森林可以输出特征重要性,但是当特征之间存在强相关性时,重要性可能会被分散或扭曲。请设计一个实验来验证:生成两个高度相关的特征(相关性 0.95),其中只有一个是真正有用的,另一个是噪声。训练随机森林后观察这两个特征的重要性是否相近?如果不相近,解释原因;如果相近,说明随机森林在特征重要性评估上的局限性。
下一课预告:第28课我们将学习 AdaBoost 和 GBDT(梯度提升树)——另一种串行集成方法 Boosting,通过迭代修正前一轮的错误,达到强学习器。敬请期待!
🔗《30节课 scikit-learn 从入门到精通》系列课程导航
第一部分:基础入门 & 环境准备(1-6 课)
第二部分:数据预处理 & 数据集操作(7-12 课)
第三部分:传统机器学习回归算法(13-17 课)
第四部分:分类算法精讲(18-23 课)
第五部分:聚类 & 降维 & 集成学习(24-29 课)
第六部分:结业大型项目实战(第 30 课)
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

462


被折叠的 条评论
为什么被折叠?



