机器学习-线性回归和线性分类模型

介绍

本次实验简述了最小二乘法、最大似然估计、逻辑回归、正则化、验证和学习曲线的基本概念,搭建了基于逻辑回归的线性模型并进行正则化,通过分析 IBMD 数据集的二元分类问题和一个 XOR 问题阐述逻辑回归的优缺点。

知识点
  • 回归
  • 线性分类
  • 逻辑回归的正则化
  • 逻辑回归的优缺点
  • 验证和学习曲线

最小二乘法

在开始学习线性模型之前,简要介绍一下线性回归,首先指定一个模型将因变量 𝑦和特征联系起来,对线性模型而言,依赖函数的形式如下:

如果为每项观测加上一个虚维度 𝑥0=1(比如偏置),那么就可以把 𝑤0整合进求和项中,改写为一个略微紧凑的形式: 

 如果有一个特征观测矩阵,其中矩阵的行是数据集中的观测,那么需要在左边加上一列。由此,线性模型可以定义为:

 

 

 

 

基于上述的定义和条件,可以说,根据高斯-马尔可夫定理,模型参数的 OLS 估计是所有线性无偏估计中最优的,即通过 OLS 估计可以获得最低的方差。

有人可能会问,为何选择最小化均方误差而不是其他指标?因为若不选择最小化均方误差,那么就不满足高斯-马尔可夫定理的条件,得到的估计将不再是最佳的线性无偏估计。

最大似然估计是解决线性回归问题一种常用方法,下面介绍它的概念。

最大似然估计

 

 

 

 

 

尽管无法消除 𝜎2,但我们可以影响前两项。理想情况下,希望同时消除偏差和方差(见下图中左上),但是在实践中,常常需要在偏置和不稳定(高方差)间寻找平衡。 

一般而言,当模型的计算量增加时(例如,自由参数的数量增加了),估计的方差(分散程度)也会增加,但偏置会下降,这可能会导致过拟合现象。另一方面,如果模型的计算量太少(例如,自由参数过低),这可能会导致欠拟合现象。 

线性回归的正则化

 

 

 

线性分类

线性分类器背后的基本思路是,目标分类的值可以被特征空间中的一个超平面分开。如果这可以无误差地达成,那么训练集被称为线性可分。

上面已经介绍了线性回归和普通最小二乘法(OLS)。现在考虑一个二元分类问题,将目标分类记为「+1」(正面样本)和「-1」(负面样本)。最简单的线性分类器可以通过回归定义: 

 

 

 

 

import warnings
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
warnings.filterwarnings('ignore')
def sigma(z):
    return 1. / (1 + np.exp(-z))


xx = np.linspace(-10, 10, 1000)
plt.plot(xx, [sigma(x) for x in xx])
plt.xlabel('z')
plt.ylabel('sigmoid(z)')
plt.title('Sigmoid function')

 

 

 

 

 

 

 

 

from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV
from sklearn.preprocessing import PolynomialFeatures
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline

使用 Pandas 库的 read_csv() 方法加载数据。这个数据集内有 118 个微芯片(目标),其中有两项质量控制测试的结果(两个数值变量)和微芯片是否投产的信息。变量已经过归一化,即列中的值已经减去其均值

# 读取数据集
data = pd.read_csv('https://labfile.oss.aliyuncs.com/courses/1283/microchip_tests.txt',
                   header=None, names=('test1', 'test2', 'released'))
# 查看数据集的一些信息
data.info()

。所以,微芯片的平均测试值为零。

# 读取数据集
data = pd.read_csv('https://labfile.oss.aliyuncs.com/courses/1283/microchip_tests.txt',
                   header=None, names=('test1', 'test2', 'released'))
# 查看数据集的一些信息
data.info()

查看开始五行和最后五行的数据。

data.head(5)
data.tail(5)

分离训练集和目标分类标签。

X = data.iloc[:, :2].values
y = data.iloc[:, 2].values

绘制数据,橙点对应有缺陷的芯片,蓝点对应正常芯片。

 

plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=1')
plt.legend()

 

定义一个函数来显示分类器的分界线。

def plot_boundary(clf, X, y, grid_step=.01, poly_featurizer=None):
    x_min, x_max = X[:, 0].min() - .1, X[:, 0].max() + .1
    y_min, y_max = X[:, 1].min() - .1, X[:, 1].max() + .1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, grid_step),
                         np.arange(y_min, y_max, grid_step))

    # 在 [x_min, m_max]x[y_min, y_max] 的每一点都用它自己的颜色来对应
    Z = clf.predict(poly_featurizer.transform(np.c_[xx.ravel(), yy.ravel()]))
    Z = Z.reshape(xx.shape)
    plt.contour(xx, yy, Z, cmap=plt.cm.Paired)

 

poly = PolynomialFeatures(degree=7)
X_poly = poly.fit_transform(X)
X_poly.shape
C = 1e-2
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)

plot_boundary(logit, X, y, grid_step=.01, poly_featurizer=poly)

plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()

print("Accuracy on training set:",
      round(logit.score(X_poly, y), 3))

可以尝试减小正则化,即把 𝐶 增加到 1,现在的模型权重可以比之前有更大的值(绝对值更大),这使得分类器在训练集上的精确度提高到 0.831。

C = 1
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)

plot_boundary(logit, X, y, grid_step=.005, poly_featurizer=poly)

plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()

print("Accuracy on training set:",
      round(logit.score(X_poly, y), 3))

 倘若继续增加 𝐶到 10000 会如何?看下面结果,很明显正则化不够强导致了过拟合现象。

C = 1e4
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)

plot_boundary(logit, X, y, grid_step=.005, poly_featurizer=poly)

plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()

print("Accuracy on training set:",
      round(logit.score(X_poly, y), 3))

为了讨论上述的这些结果,改写一下逻辑回归的优化函数:

 

正则化参数的调整

对上述例子中正则化参数 𝐶 进行调参。使用 LogisticRegressionCV() 方法进行网格搜索参数后再交叉验证,LogisticRegressionCV() 是专门为逻辑回归设计的。如果想对其他模型进行同样的操作,可以使用 GridSearchCV() 或 RandomizedSearchCV() 等超参数优化算法。

# 该单元格执行时间较长,请耐心等待
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)
# 下方结尾的切片为了在线上环境搜索更快,线下练习时可以删除
c_values = np.logspace(-2, 3, 500)[50:450:50]

logit_searcher = LogisticRegressionCV(
    Cs=c_values, cv=skf, verbose=1, n_jobs=-1)
logit_searcher.fit(X_poly, y)
logit_searcher.C_

查看超参数 𝐶 是如何影响模型的质量的。

plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel('C')
plt.ylabel('Mean CV-accuracy')

 

最后,选择 𝐶 值「最佳」的区域,即在 Mean CV-accuracy 值达到较大值的前提下选择较小的 𝐶。上图由于 𝐶过大,无法辨认具体哪个较小的 𝐶 达到了较好的 Mean CV-accuracy 值,可以仅画出 𝐶 为 0 到 10 时的验证曲线。

plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel('C')
plt.ylabel('Mean CV-accuracy')
plt.xlim((0, 10))

 

上图可见,𝐶=2 时就达到了较好的 Mean CV-accuracy 值。

逻辑回归的优缺点

通过分析 IMDB 影评的二元分类问题和 XOR 问题来简要说明逻辑回归的优缺点。

分析 IMDB 二元分类问题

IMDB 数据集中的训练集包含标记好的影评,其中有 12500 条好评,12500 条差评。使用词袋模型构建输入矩阵 𝑋 ,语料库包含所有用户影评,影评的特征将由整个语料库中每个词的出现情况来表示。下图展示了这一思路:

import os
import numpy as np
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression

 

# 文件较多,请耐心等待解压完成
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1283/aclImdb_v1.tar.gz"
!tar -zxvf "aclImdb_v1.tar.gz"
PATH_TO_IMDB = "aclImdb/"
reviews_train = load_files(os.path.join(PATH_TO_IMDB, "train"),
                           categories=['pos', 'neg'])
text_train, y_train = reviews_train.data, reviews_train.target
reviews_test = load_files(os.path.join(PATH_TO_IMDB, "test"),
                          categories=['pos', 'neg'])
text_test, y_test = reviews_test.data, reviews_test.target
PATH_TO_IMDB = "aclImdb/"
reviews_train = load_files(os.path.join(PATH_TO_IMDB, "train"),
                           categories=['pos', 'neg'])
text_train, y_train = reviews_train.data, reviews_train.target
reviews_test = load_files(os.path.join(PATH_TO_IMDB, "test"),
                          categories=['pos', 'neg'])
text_test, y_test = reviews_test.data, reviews_test.target

看看训练集和测试集中各有多少条数据

print("Number of documents in training data: %d" % len(text_train))
print(np.bincount(y_train))
print("Number of documents in test data: %d" % len(text_test))
print(np.bincount(y_test))

下面是该数据集中的一些影评。

text_train[1]
单词的简单计数

首先,使用 CountVectorizer() 创建包含所有单词的字典。

cv = CountVectorizer()
cv.fit(text_train)

len(cv.vocabulary_)

查看创建后的「单词」样本,发现 IMDB 数据集已经自动进行了文本处理(自动化文本处理不在本实验讨论范围,如果感兴趣可以自行搜索)。

print(cv.get_feature_names()[:50])
print(cv.get_feature_names()[50000:50050])

接着,使用单词的索引编码训练集的句子,用稀疏矩阵保存

X_train = cv.transform(text_train)
X_train

让我们看看上述转换过程是如何进行的,首先查看需要转换的训练集句子

text_train[19726]

然后将每个单词转换成对应的单词索引。

X_train[19726].nonzero()

接下来,对测试集应用同样的操作。

之后就可以使用逻辑回归来训练模型。

Markdown Code     
logit = LogisticRegression(solver='lbfgs', n_jobs=-1, random_state=7)
logit.fit(X_train, y_train)
​
训练完成后,查看训练集和测试集上的准确率(Accuracy)。

round(logit.score(X_train, y_train), 3), round(logit.score(X_test, y_test), 3),
​
可视化模型的系数。
def visualize_coefficients(classifier, feature_names, n_top_features=25):
    # get coefficients with large absolute values
    coef = classifier.coef_.ravel()
    positive_coefficients = np.argsort(coef)[-n_top_features:]
    negative_coefficients = np.argsort(coef)[:n_top_features]
    interesting_coefficients = np.hstack(
        [negative_coefficients, positive_coefficients])
    # plot them
    plt.figure(figsize=(15, 5))
    colors = ["red" if c < 0 else "blue" for c in coef[interesting_coefficients]]
    plt.bar(np.arange(2 * n_top_features),
            coef[interesting_coefficients], color=colors)
    feature_names = np.array(feature_names)
    plt.xticks(np.arange(1, 1 + 2 * n_top_features),
               feature_names[interesting_coefficients], rotation=60, ha="right")
visualize_coefficients(logit, cv.get_feature_names())

对逻辑回归的正则化系数进行调参。make_pipeline() 确保的序列顺序,在训练数据上应用 CountVectorizer() 方法,然后训练逻辑回归模型。

from sklearn.pipeline import make_pipeline
# 该单元格执行时间较长,请耐心等待
text_pipe_logit = make_pipeline(CountVectorizer(),
                                LogisticRegression(solver='lbfgs',
                                                   n_jobs=1,
                                                   random_state=7))

text_pipe_logit.fit(text_train, y_train)
print(text_pipe_logit.score(text_test, y_test))
from sklearn.model_selection import GridSearchCV
# 该单元格执行时间较长,请耐心等待
param_grid_logit = {'logisticregression__C': np.logspace(-5, 0, 6)[4:5]}
grid_logit = GridSearchCV(text_pipe_logit,
                          param_grid_logit,
                          return_train_score=True,
                          cv=3, n_jobs=-1)

grid_logit.fit(text_train, y_train)

查看一下最佳 𝐶,以及相应的交叉验证评分。

grid_logit.best_params_, grid_logit.best_score_
plot_grid_scores(grid_logit, 'logisticregression__C')

调优后的逻辑回归模型在验证集上的准确率。

grid_logit.score(text_test, y_test)

现在换一种方法,使用随机森林来分类。

forest.fit(X_train, y_train)
round(forest.score(X_test, y_test), 3)

上述结果可见,相较于随机森林,逻辑回归在 IMDB 数据集上表现更优。

XOR 问题

线性分类定义的是一个非常简单的分界平面:一个超平面,这导致线性模型在 XOR 问题上表现不佳。XOR 即异或,其真值表如下:

XOR 是一个简单的二元分类问题,其中两个分类呈对角交叉分布。下面创建数据集。

rng = np.random.RandomState(0)
X = rng.randn(200, 2)
y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0)
​
plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
​
显然,无法划出一条直线无误差地将两个分类分开。因此,逻辑回归在这一任务上的表现很差。

def plot_boundary(clf, X, y, plot_title):
    xx, yy = np.meshgrid(np.linspace(-3, 3, 50),
                         np.linspace(-3, 3, 50))
    clf.fit(X, y)
    # plot the decision function for each datapoint on the grid
    Z = clf.predict_proba(np.vstack((xx.ravel(), yy.ravel())).T)[:, 1]
    Z = Z.reshape(xx.shape)
​
    image = plt.imshow(Z, interpolation='nearest',
                       extent=(xx.min(), xx.max(), yy.min(), yy.max()),
                       aspect='auto', origin='lower', cmap=plt.cm.PuOr_r)
    contours = plt.contour(xx, yy, Z, levels=[0], linewidths=2,
                           linetypes='--')
    plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
    plt.xticks(())
    plt.yticks(())
    plt.xlabel(r'$x_1$')
    plt.ylabel(r'$x_2$')
    plt.axis([-3, 3, -3, 3])
    plt.colorbar(image)
    plt.title(plot_title, fontsize=12)
​
plot_boundary(LogisticRegression(solver='lbfgs'), X, y,
              "Logistic Regression, XOR problem")
​
然而,如果将输入变为多项式特征(这里 𝑑
 = 2),那么这一任务就可以得到较好的解决。

from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline

 

logit_pipe = Pipeline([('poly', PolynomialFeatures(degree=2)),
                       ('logit', LogisticRegression(solver='lbfgs'))])
plot_boundary(logit_pipe, X, y,
              "Logistic Regression + quadratic features. XOR problem")

通过将多项式特征作为输入,逻辑回归在 6 维特征空间(1,𝑥1,𝑥2,𝑥21,𝑥1𝑥2,𝑥221,1,2,12,12,22)中生成了一个超平面。当这个超平面投影到原特征空间(𝑥1,𝑥21,2)时,分界是非线性的。

在实际应用中,多项式特征确实有用,不过显式的创建它们会大大提升计算复杂度。使用核(kernel)函数的 SVM 方法相较逻辑回归要快很多,在 SVM 中,只计算高维空间中目标之间的距离(由核函数定义),而不用生成大量特征组合。

验证和学习曲线

import numpy as np
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV, SGDClassifier
from sklearn.model_selection import validation_curve, learning_curve
from matplotlib import pyplot as plt

上文对模型验证、交叉验证、正则化做了简单介绍,现在考虑一个更大的问题:如果模型的质量不佳,该怎么办?针对这个问题,有很多猜想:

  • 应该让模型更复杂还是更简单?
  • 应该加入更多特征吗?
  • 是否只是需要更多数据用于训练?

这些猜想的答案并不明显,比如有时候一个更复杂的模型会导致表现退化,有时候增加新的特征带来的变化并不直观。事实上,做出正确决定,选择正确方法,从而改进模型的能力是衡量一个人对机器学习知识掌握程度的重要指标。

data = pd.read_csv(
    'https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv').drop('State', axis=1)
data['International plan'] = data['International plan'].map(
    {'Yes': 1, 'No': 0})
data['Voice mail plan'] = data['Voice mail plan'].map({'Yes': 1, 'No': 0})

y = data['Churn'].astype('int').values
X = data.drop('Churn', axis=1).values

使用随机梯度下降训练逻辑回归(在之后的实验中将会专门讨论梯度下降)。

alphas = np.logspace(-2, 0, 20)
sgd_logit = SGDClassifier(loss='log', n_jobs=-1, random_state=17, max_iter=5)
logit_pipe = Pipeline([('scaler', StandardScaler()), ('poly', PolynomialFeatures(degree=2)),
                       ('sgd_logit', sgd_logit)])
val_train, val_test = validation_curve(logit_pipe, X, y,
                                       'sgd_logit__alpha', alphas, cv=5,
                                       scoring='roc_auc')

绘制 ROC-AUC 曲线,查看不同正则化参数下模型在训练集和测试集上的表现有何不同。

def plot_with_err(x, data, **kwargs):
    mu, std = data.mean(1), data.std(1)
    lines = plt.plot(x, mu, '-', **kwargs)
    plt.fill_between(x, mu - std, mu + std, edgecolor='none',
                     facecolor=lines[0].get_color(), alpha=0.2)


plot_with_err(alphas, val_train, label='training scores')
plot_with_err(alphas, val_test, label='validation scores')
plt.xlabel(r'$\alpha$')
plt.ylabel('ROC AUC')
plt.legend()
plt.grid(True)

上图的趋势表明:

  • 简单模型的训练误差和验证误差很接近,且都比较大。这暗示模型欠拟合,参数数量不够多。
  • 高度复杂模型的训练误差和验证误差相差很大,这暗示模型过拟合。当参数数量过多或者正则化不够严格时,算法可能被数据中的噪声「转移注意力」,没能把握数据的整体趋势。

上述结论可以推广到其他问题中。

数据对于模型的影响

一般而言,模型所用的数据越多越好。但新数据是否在任何情况下都有帮助呢?例如,为了评估特征 N ,而对数据集的数据进行加倍,这样做是否合理?

由于新数据可能难以取得,合理的做法是改变训练集的大小,然后看模型的质量与训练数据的数量之间的依赖关系,这就是「学习曲线」的概念。

这个想法很简单:将误差看作训练中所使用的样本数量的函数。模型的参数事先固定。

def plot_learning_curve(degree=2, alpha=0.01):
    train_sizes = np.linspace(0.05, 1, 20)
    logit_pipe = Pipeline([('scaler', StandardScaler()), ('poly', PolynomialFeatures(degree=degree)),
                           ('sgd_logit', SGDClassifier(n_jobs=-1, random_state=17, alpha=alpha, max_iter=5))])
    N_train, val_train, val_test = learning_curve(logit_pipe,
                                                  X, y, train_sizes=train_sizes, cv=5,
                                                  scoring='roc_auc')
    plot_with_err(N_train, val_train, label='training scores')
    plot_with_err(N_train, val_test, label='validation scores')
    plt.xlabel('Training Set Size')
    plt.ylabel('AUC')
    plt.legend()
    plt.grid(True)
def plot_learning_curve(degree=2, alpha=0.01):
    train_sizes = np.linspace(0.05, 1, 20)
    logit_pipe = Pipeline([('scaler', StandardScaler()), ('poly', PolynomialFeatures(degree=degree)),
                           ('sgd_logit', SGDClassifier(n_jobs=-1, random_state=17, alpha=alpha, max_iter=5))])
    N_train, val_train, val_test = learning_curve(logit_pipe,
                                                  X, y, train_sizes=train_sizes, cv=5,
                                                  scoring='roc_auc')
    plot_with_err(N_train, val_train, label='training scores')
    plot_with_err(N_train, val_test, label='validation scores')
    plt.xlabel('Training Set Size')
    plt.ylabel('AUC')
    plt.legend()
    plt.grid(True)

 把正则化系数设定为较大的数(alpha=10),查看线性模型的表现情况。

plot_learning_curve(degree=2, alpha=10)

 

上图表明:对于少量数据而言,训练集和交叉验证集之间的误差差别(方差)相当大,这暗示了过拟合。同样的模型,使用大量数据,误差「收敛」,暗示了欠拟合。加入更多数据,该训练集的误差不会增加,且该验证集上的误差也不会下降。所以,倘若误差「收敛」,如果不改变模型的复杂度,而是仅仅把数据集大小增大 10 倍,或许对最终的表现结果没有太大帮助。

如果将正则化系数 alpha 降低到 0.05,会怎么样?

plot_learning_curve(degree=2, alpha=0.05)

上图表明,降低正则化系数 alpha 至 0.05,曲线将逐渐收敛,如果加入更多数据,可以进一步改善模型在验证集上的表现。

Markdown Code     
如果把 alpha 设为 10−4
,让模型更复杂,会出现什么情况?

plot_learning_curve(degree=2, alpha=1e-4)

 

 

上图表明,与正则化系数 alpha=0.05 相比,在训练集和验证集上,AUC 都下降了,出现过拟合现象。

构建学习曲线和验证曲线可以帮助我们为新数据调整合适的模型复杂度。

关于验证曲线和学习曲线的结论:

  • 训练集上的误差本身不能说明模型的质量。

  • 交叉验证误差除了可以显示模型对数据的拟合程度外,还可以显示模型保留了多少对新数据的概括能力。

  • 验证曲线是一条根据模型复杂度显示训练集和验证集结果的曲线:如果两条曲线彼此接近,且两者的误差都很大,这标志着欠拟合;如果两条曲线彼此距离很远,这标志着过拟合。

  • 学习曲线是一个根据观测数量显示训练集和验证集结果的曲线:如果两条曲线收敛,那么增加新数据收益不大,有必要改变模型复杂度;如果两条曲线没有收敛,增加新数据可以改善结果。

实验总结

本次实验主要使用逻辑回归的方法构建线性回归和线性分类模型,正则化、验证曲线、学习曲线方法可以帮助我们更好更快的构建模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值