第27课:scikit-learn|随机森林、Bagging算法原理与Sklearn实现

在这里插入图片描述


课前导读

欢迎来到第27课!你是否遇到过这样的困境:决策树在训练集上准确率近乎完美,一到测试集就“原形毕露”——过拟合了。这是因为单棵决策树太“敏感”,数据稍有变化就会长出完全不同的树。那么,如果我们种上100棵树,让它们一起投票决定最终结果呢?这就是集成学习的思想——用群体的智慧弥补个体的不足。

本课的主角随机森林,是集成学习中最成功、应用最广泛的算法之一。它通过两个随机性来构建“好而不同”的决策树:一是对训练数据有放回抽样(Bagging),二是对每个节点的分裂随机选择部分特征。这使得随机森林既不容易过拟合,又能处理高维数据,甚至还能输出特征重要性,告诉你哪些特征对预测贡献最大。本课将从Bagging的基本原理开始,逐步剖析随机森林的构建细节,并通过大量实战演示参数调优和性能对比。学完本课,你将拥有一个强大的“即插即用”分类/回归武器。

学习目标

完成本课学习后,你将能够:

  1. 解释 Bagging 的原理:Bootstrap 采样 + 基学习器并行训练 + 投票/平均
  2. 理解 随机森林在 Bagging 基础上增加的特征随机性(max_features
  3. 使用 BaggingClassifier / BaggingRegressor 进行简单的 Bagging 集成
  4. 使用 RandomForestClassifier / RandomForestRegressor 进行随机森林建模
  5. 掌握 核心参数:n_estimators(树的数量)、max_depthmax_featuresbootstrapoob_score
  6. 利用 袋外误差(Out-of-Bag Error)作为验证集,无需交叉验证
  7. 提取 随机森林的特征重要性并可视化
  8. 调优 随机森林的超参数(网格搜索)

知识点理论讲解

一、集成学习的基本思想

集成学习(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 (11/n)n1/e)。剩余约 36.8 % 36.8\% 36.8% 的样本称为袋外数据(Out-Of-Bag, OOB),可用作验证集。

袋外误差:对于每个样本,使用那些包含该样本的基学习器进行预测,然后计算整体的平均误差。OOB 误差是对泛化误差的无偏估计,可代替交叉验证。

三、随机森林(Random Forest)

随机森林是 Bagging 的扩展,在构建每棵决策树时引入特征随机性

  1. 数据随机性:使用 Bootstrap 采样生成训练子集(与 Bagging 相同)。
  2. 特征随机性:在分裂每个节点时,不是从所有 d d d 个特征中选择最佳分裂特征,而是随机选择 k k k 个特征子集( k ≤ d k \le d kd),再从这 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:MemoryErrorn_estimatorsmax_depth 过大

原因:树太多或太深,内存爆炸。

解决:减少 n_estimators,限制 max_depth,或使用 n_jobs 减少内存副本(但实际不会降低内存占用)。可考虑使用 RandomForestClassifierwarm_start 增量训练。

报错3:oob_score 要求 bootstrap=True

原因:袋外分数依赖于 Bootstrap 采样。

解决:设置 bootstrap=True(默认)。

报错4:预测慢

原因n_estimators 太大。

解决:减少树的数量,或使用 predict_proba 时也会慢;可考虑模型压缩或改用其他算法。

避坑总结

  1. 随机森林默认参数通常表现不错,但调整 n_estimatorsmax_features 往往能进一步提升。
  2. 样本量较小时,减少 n_estimators 或增大 min_samples_leaf 可防止过拟合。
  3. 高维稀疏数据(如文本)随机森林效果不一定优于线性模型。
  4. 特征重要性是基于不纯度减少,可能偏向取值多的特征,需谨慎解读。
  5. 使用 n_jobs=-1 可显著加速训练(利用所有CPU核心)。

知识点总结

本课系统讲解了 Bagging 和随机森林两种集成方法:

Bagging

  1. 原理:Bootstrap 采样 + 多个基学习器并行 + 投票/平均,降低方差。
  2. 实现BaggingClassifier / BaggingRegressor,基学习器可自定义。

随机森林

  1. 双重随机性:数据 Bootstrap + 特征子采样(max_features),使基学习器更独立。
  2. 关键参数n_estimators(树的数量)、max_depthmax_featuresmin_samples_splitmin_samples_leaf
  3. 袋外误差(OOB):利用未参与训练的样本验证,设置 oob_score=True
  4. 特征重要性feature_importances_,基于不纯度减少。
  5. 调优:通常 n_estimators 越大越好,但边际效应递减;max_features 默认 sqrt(分类)或 n_features(回归);可结合 GridSearchCV。

优点与局限性

  1. 高准确率,不易过拟合,能处理高维数据,输出特征重要性。
  2. 内存大,预测速度中等,对高维稀疏数据不占优。

课后练习题

选择题

  1. 随机森林相比单棵决策树,主要优势是:
    A. 训练速度更快 B. 降低过拟合 C. 可解释性更强 D. 不需要调参

  2. Bagging 方法中,Bootstrap 采样得到的每个子集的大小与原数据集大小关系是:
    A. 更小 B. 相等 C. 更大 D. 随机

  3. 在随机森林中,参数 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.________

实操题

  1. 随机森林与决策树对比:使用 load_breast_cancer 数据集,分别训练单棵决策树(不剪枝)和随机森林(n_estimators=100)。比较训练集和测试集的准确率,分析哪个模型过拟合更严重。

  2. 特征重要性分析:在红酒数据集上训练随机森林,输出特征重要性,并画出条形图。找出最重要的两个特征,然后只用这两个特征重新训练随机森林,比较与全特征模型的性能差异。

  3. 超参数调优:在手写数字数据集上,使用 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 课)

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值