第26课:scikit-learn|主成分分析PCA降维原理、白化处理与特征压缩

在这里插入图片描述


课前导读

欢迎来到第26课!在过去的课程中,我们处理的数据通常只有几个或几十个特征。但真实世界的数据往往高得惊人:一张 512×512 的灰度图像就有 26 万维;基因表达数据可能有 2 万个特征;文本经过独热编码后可达数十万维。直接在这些高维数据上训练模型,不仅训练极慢、内存爆炸,而且由于“维度灾难”,样本在高维空间中变得稀疏,距离度量几乎失效。

降维是解决高维问题的核心手段。本课的主角——主成分分析(PCA),是最常用、最强大的线性降维方法。它的思想很巧妙:找到数据中“信息量最大”的方向(即方差最大的方向),将数据投影到这些方向上,丢弃那些方差小的方向(可以视为噪声)。PCA 不仅能压缩数据、加速后续算法,还能可视化高维数据(降到 2D/3D),甚至去噪和防止过拟合。本课将带你从零推导 PCA 的数学过程,学会用 sklearn 的 PCA 优雅降维,掌握累计方差解释率的解读、主成分个数的选择,以及白化(whitening)的高级用法。学完本课,你将能轻松应对高维数据挑战。

学习目标

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

  1. 解释 PCA 的核心思想:将数据投影到方差最大的方向上,实现降维
  2. 描述 PCA 的数学步骤:中心化 → 计算协方差矩阵 → 特征值分解 → 选择前 k 个特征向量
  3. 使用 PCA 进行降维,理解关键参数:n_componentssvd_solverwhitenrandom_state
  4. 确定 主成分数量:根据累计方差贡献率(如 95%)或指定整数维度
  5. 理解 白化(whitening)的作用:使降维后的特征具有单位方差且不相关,常用于信号处理
  6. 可视化 高维数据降维后的 2D/3D 散点图
  7. 应用 PCA 作为数据预处理步骤集成到机器学习 Pipeline 中,并比较降维前后的模型性能

知识点理论讲解

一、为什么需要降维?

高维数据带来的问题:

  • 计算复杂度:O(N·d) 甚至更高,训练缓慢。
  • 存储开销:内存消耗大。
  • 维度灾难:随着维度 d 增加,样本在空间中越来越稀疏,基于距离的算法(KNN、KMeans)失效。
  • 多重共线性:特征间可能高度相关,浪费信息。
  • 过拟合:模型参数过多,容易捕捉噪声。

降维可以在保留主要信息的同时,缓解上述问题。PCA 通过线性变换将原始特征压缩为少数几个不相关的主成分。

二、PCA 的直观理解

PCA 试图找到一组新的正交坐标系(主成分),使得:

  1. 第一主成分(PC1)的方向上,数据的方差最大(即信息量最大)。
  2. 第二主成分(PC2)在与 PC1 正交的方向上,方差次大。
  3. 以此类推,每个后续主成分都在前面所有主成分正交的方向上最大化剩余方差。

因此,PCA 本质上是方差最大化的投影

几何解释:将原始数据投影到一个超平面上,使得投影点到原投影点的距离平方和(即投影后的方差)最大,等价于最小化投影误差(点到超平面的距离平方和)。

三、PCA 的数学原理

假设我们有中心化的数据矩阵 X X X n n n 个样本, d d d 个特征,每列均值为 0)。

目标:找到单位向量 u 1 u_1 u1,使得投影 X u 1 X u_1 Xu1 的方差最大。投影方差为 u 1 T Σ u 1 u_1^T \Sigma u_1 u1TΣu1,其中 Σ = 1 n − 1 X T X \Sigma = \frac{1}{n-1} X^T X Σ=n11XTX 是协方差矩阵(有时用 X T X X^T X XTX 除以 n)。

通过拉格朗日乘子法,该问题等价于求解 Σ u 1 = λ 1 u 1 \Sigma u_1 = \lambda_1 u_1 Σu1=λ1u1,即 u 1 u_1 u1 Σ \Sigma Σ 的最大特征值对应的特征向量,方差 λ 1 \lambda_1 λ1 即为特征值。

步骤

  1. 中心化 X c e n t e r e d = X − μ X_{centered} = X - \mu Xcentered=Xμ(每个特征减去均值)。
  2. 计算协方差矩阵 C = 1 n − 1 X c e n t e r e d T X c e n t e r e d C = \frac{1}{n-1} X_{centered}^T X_{centered} C=n11XcenteredTXcentered
  3. 特征分解 C = V Λ V T C = V \Lambda V^T C=VΛVT,其中 V V V 是特征向量矩阵(主成分方向), Λ \Lambda Λ 是对角阵(特征值)。
  4. 选择前 k 个最大特征值对应的特征向量,构成投影矩阵 W k W_k Wk d × k d \times k d×k)。
  5. 降维 X n e w = X c e n t e r e d W k X_{new} = X_{centered} W_k Xnew=XcenteredWk n × k n \times k n×k)。

方差解释率:第 i 个主成分的方差贡献率为 λ i / ∑ j = 1 d λ j \lambda_i / \sum_{j=1}^d \lambda_j λi/j=1dλj。累计方差贡献率用于选择 k。

四、PCA 与 SVD 的关系

实际计算中,协方差矩阵的特征分解可能数值不稳定。scikit-learn 的 PCA 使用奇异值分解(SVD) X c e n t e r e d = U Σ V T X_{centered} = U \Sigma V^T Xcentered=UΣVT,则 V V V 的列就是主成分方向, Σ 2 / ( n − 1 ) \Sigma^2 / (n-1) Σ2/(n1) 对应特征值。SVD 更稳定高效,且能处理高维情况(通过 svd_solver 参数)。

五、白化(Whitening)

白化是对降维后的数据做进一步变换,使得:

  • 各主成分的方差归一化为 1。
  • 主成分之间不相关(已经正交,本身不相关)。

在 sklearn 中,whiten=True 会将降维后的数据除以对应主成分的标准差,即 X w h i t e n = X n e w / λ i X_{whiten} = X_{new} / \sqrt{\lambda_i} Xwhiten=Xnew/λi (每个特征独立缩放)。白化后的数据满足各维度均值为 0、方差为 1、协方差为单位矩阵。

用途

  • 使后续算法(如 KMeans、SVM)对特征尺度不敏感。
  • 常用于独立成分分析(ICA)预处理。
  • 在图像处理中可去除像素间的相关性,突出边缘。

注意:白化会放大噪声(因为方差小的成分也会被拉伸),通常只保留足够的主成分后再白化。

六、PCA 的优缺点

优点缺点
线性方法,计算高效只能捕捉线性结构,对非线性数据效果差
降维后可解释:主成分是原始特征的线性组合主成分通常难以解释具体业务含义
去除特征相关性对异常值敏感(因依赖协方差)
可用方差解释率定量评估信息损失失去原始特征的稀疏性
可与监督模型无缝集成需要数据标准化(中心化)

核心原理通俗拆解

找一个最“分散”的方向

想象你有一堆三维空间中的点,你想把它们拍扁到二维平面上,同时保留尽可能多的“形状”。你会选择哪个平面?PCA 告诉你,应该选择使投影点尽可能分散的平面。因为分散的方向包含了更多的区分信息(方差大)。这就像给一个西瓜拍 X 光片,你会选择角度让西瓜的形状最清晰。

主成分:数据的主脉搏

如果数据像一根瘦长的黄瓜,第一主成分就是沿着黄瓜的方向;第二主成分就是垂直于黄瓜的方向(黄瓜的粗细)。你只需记住黄瓜的长度和粗度这两个数,就能近似恢复整个黄瓜的形状。PCA 就是找出这种“主脉搏”。

底层数学逻辑

1. 协方差矩阵的特征分解

协方差矩阵 C C C 是对称半正定的,可以正交对角化。特征值 λ i \lambda_i λi 是各主成分上的方差,特征向量 v i v_i vi 是主成分方向。

2. 投影后的方差

投影到 u u u 方向后的样本方差 = u T C u u^T C u uTCu

3. 累计方差解释率

explained_variance_ratio i = λ i / ∑ j = 1 d λ j \text{explained\_variance\_ratio}_i = \lambda_i / \sum_{j=1}^d \lambda_j explained_variance_ratioi=λi/j=1dλj。选择使累计和 ≥ 0.95 \ge 0.95 0.95 的最小 k。

scikit-learn API详解

PCA

from sklearn.decomposition import PCA

pca = PCA(
    n_components=None,        # 整数(降维后维数)或浮点数(保留方差比例,如0.95)
    copy=True,                # 是否复制数据
    whiten=False,             # 是否白化
    svd_solver='auto',        # 'auto', 'full', 'arpack', 'randomized'
    tol=0.0,                  # 浮点数精度
    iterated_power='auto',    # 随机 SVD 的迭代次数
    random_state=None
)

# 拟合
pca.fit(X)                    # 学习主成分

# 降维
X_reduced = pca.transform(X)  # 投影到主成分空间
# 或一步完成
X_reduced = pca.fit_transform(X)

# 逆变换(近似重建)
X_reconstructed = pca.inverse_transform(X_reduced)

# 属性
pca.components_               # 主成分方向(形状 (n_components, n_features))
pca.explained_variance_       # 各主成分的方差(特征值)
pca.explained_variance_ratio_ # 方差贡献率
pca.singular_values_          # 奇异值
pca.mean_                     # 训练集均值(用于中心化)
pca.n_components_             # 实际保留的主成分数

环境配置与依赖安装

本课使用已有环境,需要 matplotlib 和 numpy。

conda activate sklearn_tutorial
python -c "import sklearn; print(sklearn.__version__)"

完整代码实战(带详细注释)

实战1:PCA 基本步骤与手动对比

# -*- coding: utf-8 -*-
"""
手动实现 PCA(使用特征分解),与 sklearn PCA 对比
"""

import numpy as np
from sklearn.decomposition import PCA

# 生成简单二维数据
np.random.seed(42)
X = np.dot(np.random.randn(200, 2), np.array([[2, 1], [1, 2]]))  # 产生相关结构

# 1. 中心化
X_centered = X - X.mean(axis=0)

# 2. 计算协方差矩阵
C = np.cov(X_centered.T)

# 3. 特征分解
eig_vals, eig_vecs = np.linalg.eig(C)
# 按特征值降序排列
idx = np.argsort(eig_vals)[::-1]
eig_vals = eig_vals[idx]
eig_vecs = eig_vecs[:, idx]

# 4. 选择前 k=1 个主成分
k = 1
W = eig_vecs[:, :k]
X_pca_manual = X_centered @ W

# sklearn PCA
pca = PCA(n_components=k)
X_pca_sk = pca.fit_transform(X)

print("手动计算的主成分方向(单位向量):\n", W.T)
print("sklearn 主成分方向:\n", pca.components_)
print("手动计算的方差解释率:", eig_vals[:k] / eig_vals.sum())
print("sklearn 方差解释率:", pca.explained_variance_ratio_)

# 重建(逆变换)
X_recon_manual = X_pca_manual @ W.T + X.mean(axis=0)
X_recon_sk = pca.inverse_transform(X_pca_sk)

print("重建误差(手动):", np.mean((X - X_recon_manual)**2))
print("重建误差(sklearn):", np.mean((X - X_recon_sk)**2))

实战2:选择主成分数量(累计方差解释率曲线)

# -*- coding: utf-8 -*-
"""
绘制累计方差贡献率曲线,确定 n_components
使用手写数字数据集(64维)
"""

from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np

digits = load_digits()
X = digits.data
y = digits.target

# 拟合 PCA(不降维)
pca = PCA()
pca.fit(X)

# 累计方差解释率
cumsum = np.cumsum(pca.explained_variance_ratio_)

plt.figure(figsize=(8,5))
plt.plot(range(1, len(cumsum)+1), cumsum, 'bo-')
plt.axhline(y=0.95, color='r', linestyle='--', label='95% 方差')
plt.axvline(x=np.argmax(cumsum >= 0.95) + 1, color='g', linestyle='--', label='95% 对应的维数')
plt.xlabel('主成分数量')
plt.ylabel('累计方差贡献率')
plt.title('PCA 累计方差解释率')
plt.legend()
plt.grid(True)
plt.show()

# 保留 95% 方差的维数
n_components_95 = np.argmax(cumsum >= 0.95) + 1
print(f"保留 95% 方差需要 {n_components_95} 个主成分")

# 使用该维数进行降维
pca_95 = PCA(n_components=0.95)
X_95 = pca_95.fit_transform(X)
print(f"原始维度: {X.shape[1]}, 降维后: {X_95.shape[1]}")

实战3:PCA 可视化高维数据(降到2D)

# -*- coding: utf-8 -*-
"""
将手写数字降到2维,并用不同颜色显示类别
"""

from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

digits = load_digits()
X, y = digits.data, digits.target

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='tab10', alpha=0.6, edgecolors='k')
plt.colorbar(scatter, label='数字类别')
plt.xlabel(f'第一主成分 (方差贡献率 {pca.explained_variance_ratio_[0]:.2%})')
plt.ylabel(f'第二主成分 (方差贡献率 {pca.explained_variance_ratio_[1]:.2%})')
plt.title('手写数字 PCA 降维到 2D')
plt.show()

实战4:白化(whiten)的效果

# -*- coding: utf-8 -*-
"""
对比白化前后的数据分布(使用鸢尾花数据)
白化使各主成分方差为1,更适合后续聚类或分类
"""

from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

X, y = load_iris(return_X_y=True)

pca_normal = PCA(n_components=2, whiten=False)
X_pca_normal = pca_normal.fit_transform(X)

pca_white = PCA(n_components=2, whiten=True)
X_pca_white = pca_white.fit_transform(X)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(X_pca_normal[:, 0], X_pca_normal[:, 1], c=y, cmap='viridis')
axes[0].set_title(f'标准 PCA (方差: {pca_normal.explained_variance_})')
axes[0].set_xlabel('PC1')
axes[0].set_ylabel('PC2')

axes[1].scatter(X_pca_white[:, 0], X_pca_white[:, 1], c=y, cmap='viridis')
axes[1].set_title(f'白化 PCA (方差: {pca_white.explained_variance_})')
axes[1].set_xlabel('PC1 (白化)')
axes[1].set_ylabel('PC2 (白化)')
plt.show()

print("白化后各主成分方差被归一化为 1(近似)")

实战5:PCA 重建人脸图像(去噪效果)

# -*- coding: utf-8 -*-
"""
使用 PCA 重建人脸,展示降维后图像质量
数据集:Olivetti Faces(需网络下载)
"""

from sklearn.datasets import fetch_olivetti_faces
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

faces = fetch_olivetti_faces(shuffle=True, random_state=42)
X = faces.data
# 取第一个人的前几张图像
first_person_idx = 0
X_person = X[first_person_idx * 10: (first_person_idx+1) * 10]  # 每人10张

pca = PCA(n_components=50)  # 从 4096 降到 50
X_pca = pca.fit_transform(X_person)
X_recon = pca.inverse_transform(X_pca)

# 显示原始和重建图像
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i in range(5):
    axes[0, i].imshow(X_person[i].reshape(64, 64), cmap='gray')
    axes[0, i].set_title(f'原图{i+1}')
    axes[0, i].axis('off')
    axes[1, i].imshow(X_recon[i].reshape(64, 64), cmap='gray')
    axes[1, i].set_title(f'重建{i+1}')
    axes[1, i].axis('off')
plt.suptitle(f'PCA 降维到 50 维 (保留方差 {pca.explained_variance_ratio_.sum():.2%})')
plt.tight_layout()
plt.show()

实战6:PCA 作为预处理提升分类器性能

# -*- coding: utf-8 -*-
"""
在手写数字上,对比原始数据与 PCA 降维后使用 SVM 的分类性能和时间
"""

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import time

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)

# 1. 原始数据 + SVM(标准化)
pipe_raw = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', SVC(kernel='rbf', gamma='scale'))
])
start = time.time()
pipe_raw.fit(X_train, y_train)
time_raw = time.time() - start
acc_raw = pipe_raw.score(X_test, y_test)

# 2. PCA 降维 + SVM
pipe_pca = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=0.95)),
    ('svm', SVC(kernel='rbf', gamma='scale'))
])
start = time.time()
pipe_pca.fit(X_train, y_train)
time_pca = time.time() - start
acc_pca = pipe_pca.score(X_test, y_test)

print(f"原始数据: 准确率={acc_raw:.4f}, 训练时间={time_raw:.2f}秒, 特征维数={X.shape[1]}")
print(f"PCA降维: 准确率={acc_pca:.4f}, 训练时间={time_pca:.2f}秒, 特征维数={pipe_pca.named_steps['pca'].n_components_}")

实战7:使用随机化 SVD 加速大维度数据(svd_solver=‘randomized’)

# -*- coding: utf-8 -*-
"""
生成高维数据,对比 svd_solver='auto' vs 'randomized' 的时间
"""

import numpy as np
from sklearn.decomposition import PCA
import time

# 高维数据(2000样本,500特征)
X = np.random.randn(2000, 500)

for solver in ['auto', 'randomized']:
    pca = PCA(n_components=50, svd_solver=solver, random_state=42)
    start = time.time()
    pca.fit(X)
    elapsed = time.time() - start
    print(f"solver={solver:10s} 耗时: {elapsed:.3f} 秒")

案例实操演示

案例:PCA 在股票市场中的应用(因子降维)

# 模拟股票因子数据(多个技术指标),用 PCA 提取主要特征
import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

np.random.seed(42)
# 模拟 500 个交易日,10 个技术指标
n_days = 500
n_features = 10
factors = np.random.randn(n_days, n_features)
# 添加相关性结构
factors = factors @ np.random.randn(n_features, n_features) * 0.5 + factors

pca = PCA(n_components=0.90)
factors_pca = pca.fit_transform(factors)
print(f"原始特征数: {n_features}, 降维后: {factors_pca.shape[1]}")
print(f"累计方差贡献率: {pca.explained_variance_ratio_.sum():.2%}")

# 可视化主成分的载荷(第一个主成分对原始因子的权重)
loadings = pca.components_[0]
plt.bar(range(n_features), loadings)
plt.xlabel('原始因子编号')
plt.ylabel('第一主成分载荷')
plt.title('第一主成分的因子载荷')
plt.show()

常见报错与避坑指南

报错1:ValueError: n_components must be between 1 and min(n_samples, n_features)

原因:指定的主成分数大于样本数或特征数。

解决:设置 n_components 不大于 min(n_samples, n_features)。若用方差比例,则不会报错。

报错2:PCA with svd_solver='arpack' requires n_components < min(X.shape)

原因:arpack 要求主成分数严格小于样本数和特征数。

解决:改用 svd_solver='full''auto'

报错3:白化后数据方差为0或极小的主成分

说明:某些主成分的特征值接近0,白化时会除一个很小的数,导致数值爆炸。

解决:确保 n_components 只保留非零特征值的主成分(通过方差解释率选择)。

报错4:PCA 结果不稳定(不同运行结果不一致)

原因:使用了随机 SVD(svd_solver='randomized')且未固定 random_state

解决:设置 random_state 固定种子。

避坑总结

  1. 必须中心化:PCA 依赖数据均值中心化,sklearn 自动处理,但确保不用稀疏矩阵。
  2. 方差解释率选择主成分数:通常取 0.95 或 0.99,而非固定整数。
  3. 白化谨慎使用:会在降维后改变距离尺度,如需保留距离关系,慎用。
  4. 对异常值敏感:PCA 基于协方差,异常值会扭曲主成分方向。可先用 RobustScaler 或做异常检测。
  5. 非线性数据:PCA 效果差,改用 KernelPCA 或 t-SNE/UMAP。

知识点总结

本课系统讲解了主成分分析(PCA)的完整知识体系:

原理

  1. 目标:最大化投影后的方差(等价于最小化投影误差)。
  2. 方法:数据中心化 → 协方差矩阵特征分解 → 选择前 k 个特征向量。
  3. 方差解释率:各主成分方差占比,累计和决定 k。

API

  1. PCA 核心参数:n_components(整数或小数)、whitensvd_solver
  2. 属性:components_explained_variance_ratio_mean_
  3. 方法:fittransformfit_transforminverse_transform

实践

  1. 通过累计方差曲线选择 k。
  2. 白化使各主成分方差为1,用于独立成分分析或预处理。
  3. PCA 可集成到 Pipeline 中提升模型效率。
  4. 随机化 SVD(svd_solver='randomized')加速大数据降维。

局限性

  1. 线性变换,无法捕捉非线性结构。
  2. 对异常值敏感。
  3. 主成分解释性差。

课后练习题

选择题

  1. PCA 寻找的第一主成分是:
    A. 与目标变量相关性最大的方向 B. 数据方差最大的方向 C. 数据均值最小的方向 D. 特征数最多的方向

  2. 白化(whiten=True)的作用是:
    A. 使数据均值为0 B. 使各主成分方差为1 C. 增加特征独立性 D. 提高计算速度

  3. 当特征维度远大于样本数时,推荐使用哪种 SVD 求解器?
    A. ‘full’ B. ‘arpack’ C. ‘randomized’ D. ‘auto’ 会自动选择 ‘randomized’

填空题

from sklearn.decomposition import PCA

# 1. 创建 PCA,保留 95% 方差
pca = PCA(n_components=________)

# 2. 训练并降维
X_pca = pca.fit_transform(X)

# 3. 获取主成分方向(每行是一个主成分)
components = pca.________

# 4. 获取每个主成分的方差贡献率
var_ratio = pca.________

实操题

  1. 累积方差曲线:使用 load_digits,绘制不同主成分数量(1-64)下的累计方差贡献率曲线。找出达到 90%、95%、99% 所需的主成分数。

  2. PCA 重建误差:在 load_digits 上,用 PCA 将维度降到 10、30、50,分别计算重建图像与原始图像的均方误差(MSE),并显示重建图像。观察随着主成分增加,图像质量如何变化。

  3. 白化对 KNN 的影响:在鸢尾花数据集上,先用 PCA 降到 2 维(whiten=False),然后用 KNN(K=3)分类;再同样用 whiten=True 重复。比较两次分类准确率,并分析差异原因。

思考题

在图像压缩中,我们使用 PCA 保留前 k 个主成分重建图像。如果原始图像是 512×512 灰度图(262144 维),使用 k=200 的主成分,压缩比是多少?为什么直接保存主成分而不是原图?PCA 压缩与 JPEG 压缩有何本质区别?


下一课预告:第27课我们将学习随机森林和 Bagging 集成方法——通过组合多个决策树来提升稳定性和准确率,是 Kaggle 竞赛的利器。敬请期待!


🔗《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、付费专栏及课程。

余额充值