《机器学习实战 学习笔记》(二):端到端的机器学习项目

文章目录

1. 使用真实数据 ( 加州房价预测 )

   1.1 流行的各个领域的开放数据集存储库:
   ① UC Irvine ML Repository
   ② Kaggle datasets
   ③ Amazon AWS datasets
   1.2 元门户站点
   ①http://dataportals.org/
   ② http://opendatamonitor.eu/
   ③ http://quandl.com/
   1.3 其他一些列出许多流行的开放数据存储库的页面
   ① Wiki list of ML datasets
   ② Quora.com question
   ③ Datasets subreddit

2. 观察大局

在这里插入图片描述

   Pipeline: 一个序列的数据处理组件称为一个数据流水线。
2.1 框架问题
   ① 首先要问老板,我们的业务目标是什么? 因为建立模型本身可能不是最终问题。【审题很重要】
   ② 如果有现成的解决方案,可以参照。【站在巨人的肩膀上】
   ③ 如果数据量巨大,还会用到MapReduce技术。
2.2 选择性能指标
   我们是预测房屋价格,典型的回归问题。回归常用的性能指标:RMSE(均方根误差)、MAE(平均绝对误差)。
   RMSE和MAE如何选择?当异常值非常少时,RMSE更加优异。因为RMSE对异常值非常敏感,为什么?很好理解,平方比绝对值使误差变更大。
2.3 检查假设
   搞清楚上下游系统到底是需要什么数据,比如,下游系统需要输出价格,那就是回归任务,需要输出类别,就是分类任务。所以要沟通清楚后再着手开发,不要盲目。

3. 获取数据

3.1 创建工作区
   关于python的系统环境,我用的是anaconda,而且之前写python爬虫的时候也介绍过隔离环境如何去安装。所以这里就不过多介绍了。
3.2 下载数据
# 下载数据
import os
import tarfile
from six.moves import urllib

downLoad_root = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
housing_path = "datasets/housing"
housing_url = downLoad_root + housing_path + "/housing.tgz"

# 定义获取房子数据的函数
def fetch_housing_data(housing_url = housing_url,housing_path = housing_path):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    print("=======   housing_url ================",housing_url)
    tgz_path = os.path.join(housing_path,"housing.tgz")
    print("=======   tgz_path ================",tgz_path)
    urllib.request.urlretrieve(housing_url,tgz_path)#下载远程文件 并保存为本地名为tgz_path的文件
    housing_taz = tarfile.open(tgz_path)# 读取本地文件
    housing_taz.extractall(path=housing_path)#解压到housing_path目录下
    housing_taz.close()#关闭IO流

#定义加载数据方法 
def load_data(housing_path = housing_path):
    csv_path = os.path.join(housing_path,"housing.csv")
    print("=======   csv_path ================",csv_path)
    return pd.read_csv(csv_path)
3.3 快速查看数据结构
data.shape
data.describe()
data.info()

在这里插入图片描述

   describe依次展示的是:总数、均值、标准差、最小值、25%分位数、50%分位数、75%分位数、最大值。

在这里插入图片描述

   从数据看出,total_bedrooms有缺失值,ocean_proximity是对象类型,可能是分类属性。

在这里插入图片描述

   另一种比较直观的了解数据的方式:直方图。反应的是落在某一范围内(横轴)的实例数量(纵轴)。
data.hist(bins=50,figsize=(20,15))

在这里插入图片描述

3.4 创建测试集(经常被忽视但至关重要)
   ① 可以直接用sklearn提供的切分方法,但是在这之前要考虑数据量,数据量大的话用随机抽样的方法,数据量小的话最好用分层抽样,否则会造成明显的抽样偏差
#分层抽样
from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1,test_size=0.2,random_state=42)
# 分层是基于label进行的,这里基于收入类别属性进行分层!!!
for train_index,test_index in split.split(data,data["income_cat"]):
#     print("==========")
    strat_train_set = data.loc[train_index]
    strat_test_set = data.loc[test_index]

   咦,data[“income_cat”]哪里来的?这是因为我们想让测试集代表整个数据集中各种不同类型的收入而加的新特征—收入类别。处理如下:
data["income_cat"] = np.ceil(data["median_income"] / 1.5)#取整后得到离散类别
data["income_cat"].where(data["income_cat"] < 5,5.0,inplace = True) # 大于5的都归属于类别5
data["income_cat"].value_counts()
data["income_cat"].hist()
   注意:data[“income_cat”]就是为了切分后的数据分布更接近整个数据集的数据分布,所以,切分完后还要删掉。
#删掉income_cat列
for d in (strat_train_set,strat_test_set):
    d.drop(["income_cat"],axis = 1,inplace=True)
   至此,我们就完成了数据集的切分,得到了分层抽样后的测试集。分层抽样的测试集中的比例分布更接近于完整数据集中的分布。

4. 从数据探索和可视化中获得洞见 EDA(pandas、matplotlib、seaborn)

4.1 将地理数据可视化
h.plot(kind="scatter",x="longitude",y="latitude",alpha=0.4,
      s=h["population"]/100,label="population",
      c="median_house_value",cmap=plt.get_cmap("jet"),colorbar=True,
      figsize=(15,10),fontsize=20)
plt.legend()

在这里插入图片描述

4.2 寻找相关性
   数据集不大的话,可以直接用corr()方法计算出每对属性之间的相关系数,即皮尔逊相关系数。
#寻找相关性
corr_matrix = h.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)

在这里插入图片描述

   从上面可以分析出,收入越高,房价越高。越往北走,房价越低。(常识:北纬为正,东经为正,南纬和西经为负
   各种数据集的标准相关系数:

在这里插入图片描述

   还有一种方法可以检测属性之间的相关性,就是使用pandas的scatter_matrix。
from pandas.tools.plotting import scatter_matrix
# h.columns
attr = ["median_house_value","median_income","total_rooms","housing_median_age"]
scatter_matrix(h[attr],figsize=(12,8))

在这里插入图片描述

   最有潜力预测房价中位数的特征是收入中位数,所以我们放大其散点图:
h.plot(kind="scatter",x="median_income",y="median_house_value",alpha=0.1)

在这里插入图片描述

   分析上图:
   ① 相关性很强。
   ② 50万和35附近有一条水平线,可能数据已经经过了处理。
4.3 试验不同属性的组合
   其实这属于是特征工程的部分。尝试不同的组合特征,可以简单的使用四则运算来实验。
   思考:其实我认为这块东西要跟实际业务紧密相连,比如房价,在我们生活中很普遍,从购房者的角度来看,希望买到什么样的房子? 我会考虑是否学区,房屋质量(浇筑或板楼),建筑年代,是否可贷款,周边配套等等方面,那么这些特征较好的房价肯定贵啊,所以多从实际业务思考,当然除了我们能想到的,也有很多想不到的特征,就要多去试验一下(四则运算、决策树等方式),毕竟每个人的想法和心理是不一样的。
#组合特征尝试
h["rooms_per_household"] = h["total_rooms"] / h["households"] #每户多少个房间
h["bedrooms_per_room"] = h["total_bedrooms"] / h["total_rooms"] #房价/卧室比例
h["population_per_household"] = h["population"] / h["households"] #每户有多少人
corr_matrix = h.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)

在这里插入图片描述

   分析:卧室房间比例越低,房价越高;每个家庭的房价数量越多,房价越贵。

5. 机器学习算法的数据准备 ( 数据清洗、特征工程)

   这里我们应该开始编写函数和积累函数,自己逐步建立一个转换函数的函数库,而不是每次都手动操作。最大原因:重用
5.1 数据清理
   对于缺失值,一般有3种方式:
   ① 删除对应的样本;
   ② 删除对应的特征;
   ③ 填充缺失值,填充值:中位数、众数、均值等。
   对于第3种方式,sk提供了imputer来处理缺失值。
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy = "median")
#只保留数值型特征
housing_nums = housing.drop("ocean_proximity",axis=1)
#对数值型的有缺失值的样本进行估算填充
imputer.fit(housing_nums )
   注意:此时imputer的估算只是针对【当前实例】,系统如果重启,则失效。 所以,为了稳妥,要对所有数值属性进行变换transform
#结果为转换后的Numpy数组
X = imputer.transform(housing_nums) 
#再转成df
housing_transform = pd.DataFrame(X,columns=housing_nums.columns)
5.1.1 【插曲1】 数据的拼接和合并:
   ① concat,基于axis拼接,类似union,适合纵向
   ② merge,基于某列做关联,表的合并,类似sql的表连接,适合横向
   ③ join,基于index合并。
5.1.2 【插曲2】 sk的几个常用函数:
   sklearn做了一致性的设计,所以大部分数据集的操作通过以下函数就可以实现:
   ① fit 拟合;
   ② transform 变换;
   ③ fit_transform 拟合+变换;
   ④ predict 预测;
   ⑤ predict_prob 预测概率;
   fit 和 transform 的区别和联系:举个通俗点的例子,从字面上来看,transform更像数据的实际转换方法,比如标准化StandardScaler(),当数据集执行了transform才算标准化完成,而fit更像是在这之前做一些基础数据的计算,为后面的transform作准备,所以一般我们在训练集做fit_transform,然后在测试集直接用transform,因为前面一步已经做了fit生成了一些基础数据比如均值,标准差等。
5.2 处理文本和分类属性
   之前我们排除了分类属性ocean_proximity,因为它是一个文本属性,我们无法计算中位数。大部分的机器学习更易于跟数字打交道,所以我们可以将这种数据转换为数字。sk中提供了这种转换器,比如LabelEncoder。
# 处理文本和分类属性  编码
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
housing_cat = housing["ocean_proximity"]
housing_cat_encoded = encoder.fit_transform(housing_cat)
housing_cat_encoded
encoder.classes_

在这里插入图片描述

   注意:这里有个比较实际的问题,映射器将类别型特征转换为了0,1,2,3,4,但是这样的编码是有大小关系的,而实际ocean_proximity这个特征不具有大小关系的含义,所以通常解决方式:转化为二进制属性。
   如果特征的值为高等、中等、低等,这种具有大小关系的含义,就可以映射为3,2,1这样的编码。
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))#需要一个二维数组,所以需要重塑 housing_cat_encoded(一维) 成二维
housing_cat_1hot

在这里插入图片描述

   这里输出是一个Scipy稀疏矩阵,但是只存非0元素的位置,因为大量0占用内存,所以做了优化。如果想转换成密集的二维数组:
housing_cat_1hot.toarray()

在这里插入图片描述

   这样是不是就更像one-hot了?但是,LabelBinarizer默认返回的是一个密集的Numpy数组

在这里插入图片描述

   但是,是不是很麻烦,难道每次我要先LabelEncoder再OneHotEncoder? sk提供了一个LabelBinarizer可以一次性完成2个转换(从文本类别转化为整数类别,再从整数类别转化为独热向量)
from sklearn.preprocessing import LabelBinarizer
encoder = LabelBinarizer()
housing_cat_1hot = encoder.fit_transform(housing_cat)#直接将文本类别转化为独热向量
housing_cat_1hot
5.3 自定义转化器
   ① 为什么?因为一些清理操作或组合特定特征等任务需要自定义;再就是转换器能和sk的pipeline无缝衔接
   ② 具体实现:新建类,添加两个基类BaseEstimator和TransformerMixin分别得到get_params()、set_params() 和 fit_transform() (这里的基类类似于继承),类中再实现fit和transform即可。
#自定义转化器
from sklearn.base import BaseEstimator,TransformerMixin
#单个特征的列号
rooms_ix,bedrooms_ix,population_ix,household_ix = 3,4,5,6

class CombinedAttributesAdder(BaseEstimator,TransformerMixin):
    def __init__(self,add_bedrooms_per_room = True):
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self,X,y=None):
        return self
    #创建组合特征并加入到原始特征数组即可
    def transform(self,X,y=None):
        rooms_per_household = X[:,rooms_ix] / X[:,household_ix]
        population_per_household = X[:,population_ix] / X[:,household_ix]
        if self.add_bedrooms_per_room :
            bedrooms_per_room = X[:,bedrooms_ix] / X[:,rooms_ix]
            return np.c_[X,rooms_per_household,population_per_household,bedrooms_per_room]
        else:
            # X为原特征数组,在此基础上加上新特征:rooms_per_household 和 population_per_household
            return np.c_[X,rooms_per_household,population_per_household]
        
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room = False)
housing_extra_attr = attr_adder.transform(housing.values)
type(housing_extra_attr)
# attr_adder.get_params


5.4 特征缩放
   为什么?比如有的特征是0 ~ 9999,有的特征是2 ~ 9,会导致算法对取值较大的特征有所偏好,性能不佳,所以如果将两个特征都缩放到 0 ~ 1范围内,就能提高模型算法的泛化能力。
   常用方法:最小最大值缩放 MinMaxScaler(默认范围0~1,可调)和 标准化StandadScaler。
   区别:标准化不会将值缩放到特定范围,但是标准化能减小异常值的影响。
5.5 转换流水线
   为什么? 许多数据转换的步骤需要以正确的顺序执行。
   LabelBinarizer版本更新问题
#自定义转化器
from sklearn.base import BaseEstimator,TransformerMixin
#单个特征的列号
rooms_ix,bedrooms_ix,population_ix,household_ix = 3,4,5,6

class CombinedAttributesAdder(BaseEstimator,TransformerMixin):
    def __init__(self,add_bedrooms_per_room = True):
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self,X,y=None):
        return self
    def transform(self,X,y=None):
        rooms_per_household = X[:,rooms_ix] / X[:,household_ix]
        population_per_household = X[:,population_ix] / X[:,household_ix]
        if self.add_bedrooms_per_room :
            bedrooms_per_room = X[:,bedrooms_ix] / X[:,rooms_ix]
            return np.c_[X,rooms_per_household,population_per_household,bedrooms_per_room]
        else:
            # X为原特征数组,在此基础上加上新特征:rooms_per_household 和 population_per_household
            return np.c_[X,rooms_per_household,population_per_household]
        
#测试举例
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room = False)
housing_extra_attr = attr_adder.transform(housing.values)
type(housing_extra_attr)
# attr_adder.get_params

#需要自定义,sk没有可以处理pd.DataFrame的
class DataFrameSelector(BaseEstimator,TransformerMixin):
    def __init__(self,attribute_names): #可以为列表
        self.attribute_names = attribute_names
    def fit(self,X,y=None):
        return self
    def transform(self,X):
        return X[self.attribute_names].values #返回的为numpy array

#LabelBinarizer参数有问题,可能是版本问题,重写一下即可
class MyLabelBinarizer(TransformerMixin):
    def __init__(self, *args, **kwargs):
        self.encoder = LabelBinarizer(*args, **kwargs)
    def fit(self, x, y=0):
        self.encoder.fit(x)
        return self
    def transform(self, x, y=0):
        return self.encoder.transform(x)

#转换器流水线
from sklearn.pipeline import Pipeline,FeatureUnion
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import Imputer

housing_num = housing.drop("ocean_proximity",axis=1)

#一个完整的处理数值和分类特征的流水线如下: (pipeline必须保证前面都是转换器,最后是估算器)
num_attr = list(housing_num)
cat_attr = ["ocean_proximity"]

#数值特征pipeline
num_pipeline = Pipeline([
    ('selector',DataFrameSelector(num_attr)),
    ('imputer',Imputer(strategy="median")),      # 缺失值填充为中位数               #
    ('attribs_adder',CombinedAttributesAdder()), # 添加组合特征
    ('std_scaler',StandardScaler()),             # 标准化
])

#文本分类特征pipeline
cat_pipeline = Pipeline([
    ('selector',DataFrameSelector(cat_attr)),
    ('label_binarizer',MyLabelBinarizer()),       #LabelEncoder + OneHotEncoder
])

#利用FeatureUnion整合2条流水线
full_pipeline = FeatureUnion(transformer_list=[
    ("num_pipeline",num_pipeline),
    ("cat_pipeline",cat_pipeline),
])

housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
   这一节主要完成了:自动清理 和 准备ML算法的数据的转换流水线

   至此,看看我们完成了哪些内容:明确目标问题、获取数据、EDA、对训练集和测试集进行分层抽样、编写了自动清理和准备数据的转换器。

6. 选择和训练模型

6.1 训练和评估
   首先,测试一下我们前面的组件。
#线性回归测试一下模型
from sklearn.linear_model import LinearRegression
m1  = LinearRegression()
m1.fit(housing_prepared,housing_label)
some_data = housing.iloc[:5]
some_labels = housing_label.iloc[:5]
housing_prepared = full_pipeline.fit_transform(housing)
some_data_prepared = full_pipeline.transform(some_data)

print("Predictions:",m1.predict(some_data_prepared))
print("Labels:",list(some_labels))

在这里插入图片描述

   测试成功,说明我们前面的组件都可以正常工作。但是准确度堪忧~ 所以,先评估一下吧。
#选择评估标准RMSE评估训练效果
from sklearn.metrics import mean_squared_error
housing_predictions = m1.predict(housing_prepared)
m1_mse = mean_squared_error(housin_label,housing_predictions)
m1_rmse = np.sqrt(m1_mse)
m1_rmse

在这里插入图片描述

   貌似损失有点大,说明欠拟合。如何解决?选择一个更复杂的模型试试,比如DecisionTreeRegressor

在这里插入图片描述

   竟然0损失,好吧,模型过拟合了。所以我们还需要模型进行评估,正常来说要对训练集切分成训练集和验证集,然后用交叉验证评估,最后得到验证的模型再去预测测试集
6.2 使用交叉验证来更好地进行评估
   k-fold交叉验证
#决策树10折交叉验证
from sklearn.model_selection import cross_val_score
scores = cross_val_score(m2,housing_prepared,housing_label,scoring="neg_mean_squared_error",cv =10)
rmse_scores = np.sqrt(-scores)

def display_scores(scores):
    print("Scores:",scores)
    print("Mean:",scores.mean())
    print("Std:",scores.std())

display_scores(rmse_scores)

在这里插入图片描述

#线性回归交叉验证
l_scores = cross_val_score(m1,housing_prepared,housing_label,scoring="neg_mean_squared_error",cv =10)
l_rmse_scores = np.sqrt(-l_scores)
display_scores(l_rmse_scores)

在这里插入图片描述

#随机森林
from sklearn.ensemble import RandomForestRegressor
m3 = RandomForestRegressor()
m3.fit(housing_prepared,housing_label)
rf_scores = cross_val_score(m3,housing_prepared,housing_label,scoring="neg_mean_squared_error",cv =10)
rf_rmse_scores = np.sqrt(-rf_scores)
display_scores(rf_rmse_scores)

在这里插入图片描述

   上面试验了3种不同模型的交叉验证效果,随机森林效果最好,但是还是过拟合。注意:此时不要急于去调超参数,而是要多试验几种其他模型(不同内核的SVM,神经网络模型等),我们这个阶段的目的是筛选有效模型(一般选出2~5个)
   模型保存:尝试过的模型要保存下来,包括超参数和训练过的参数,以及交叉验证的评分和实际预测的结果。比赛和实际工作都要这么做。我们通过python的pickel模块或sklearn.externals.joblib,这样可以有效将大型Numpy数组序列化。
#模型保存
from sklearn.externals import joblib
joblib.dump(m1,"linearReg_model.pkl")#保存训练过的线性回归模型
my_model = joblib.load("linearReg_model.pkl")# 加载模型
my_model.predict(some_data_prepared)

在这里插入图片描述

7. 微调模型

   经过上面过程,我们已经有了一个有效的模型列表,下一步需要对它们进行微调。

8. 网格搜索

   可以直接使用sk的GridSearchCV来替代手动调整超参数。
from sklearn.model_selection import GridSearchCV

#候选参数列表
params = {
    'n_estimators': (3, 10, 30), 'max_features': (2, 3, 4, 6, 8),
}


#使用随机森林(评估器)寻找最佳参数
rf_reg = RandomForestRegressor() 
grid_search = GridSearchCV(rf_reg,params,cv=5,scoring="neg_mean_squared_error")
grid_search.fit(housing_prepared,housing_label)

在这里插入图片描述

   查看最佳参数和最优估算器
grid_search.best_params_ 
grid_search.best_estimator_

在这里插入图片描述

   查看评估分数
cvres = grid_search.cv_results_
for mean_score,param in zip(cvres["mean_test_score"],cvres["params"]):
    print(np.sqrt(-mean_score),param)

在这里插入图片描述

8.1 随机搜索
   当数据量很大时,用网格搜索太慢,此时使用随机搜索RandomizedSearchCV:在每次迭代中,为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估。
8.2 集成方法
   将表现最优的几个模型组合起来。集成方法详细介绍
8.3 分析最佳模型及其错误
   RF可以指出每个属性的相对重要程度 ( 怎么算的?根据某种增益和分裂次数等,曾经被面试官问到过。)
extra_attrs = ["rooms_per_hhold","pop_per_hhold","bedrooms_per_room"] #新增的组合特征
cat_one_hot_attrs = list(encoder.classes_) #类别特征名称列表
attrs = num_attr + extra_attrs + cat_one_hot_attrs
# attrs
# housing_prepared
sorted(zip(feature_importances,attrs),reverse=True)

在这里插入图片描述

   从图中看出,收入中位数median_income是最重要的特征。
8.4 通过测试集评估系统
   记住:在这之前我们没有动过测试集,也不应该动。
final_m = grid_search.best_estimator_
X_test = strat_test_set.drop("median_house_value",axis=1)
y_test = strat_test_set["median_house_value"].copy()

X_test_prepared = full_pipeline.transform(X_test)
final_predictions = final_m.predict(X_test_prepared)

final_mse = mean_squared_error(y_test,final_predictions)
final_rmse = np.sqrt(final_mse)
final_rmse
   特别注意: 测试集效果可能略差于交叉验证的结果,但是此时不要再调整超参数了,因为再调可能会导致新数据集上的泛化能力变差。

9. 启动、监控和维护系统

   ① 将生产数据源接入系统。
   ② 编写监控代码,实时监控系统的性能,在性能下降时触发警报。因为模型会慢慢腐坏,除非定期使用新数据训练模型。
   ③ 评估系统性能,需要对系统的预测结果进行抽样评估(人工分析)。这块需要将人工评估的流水线接入学习系统。
   ④ 需要定期用新数据训练模型,并且能保存系统当前状态,以便发生错误能及时回滚。

10. 试试看和练习

   经过这一系列的过程,其实机器学习的大部分工作是:数据准备、构建监控工具、建立人工评估的流水线、自动定期训练模型
   尝试A ~ Z的过程:kaggle竞赛网站

11. 那些年我们踩过的坑。。。

   ① 已经训练好的模型predict数据,报特征维度不一致。报错信息如下:

在这里插入图片描述

   错误代码:
#线性回归测试一下模型
from sklearn.linear_model import LinearRegression
m1  = LinearRegression()
m1.fit(housing_prepared,housing_label)
some_data = housing.iloc[:5]
some_labels = housing_label.iloc[:5]
housing_prepared = full_pipeline.fit_transform(housing)
some_data_prepared = full_pipeline.fit_transform(some_data) 【错误处】

print("Predictions:",m1.predict(some_data_prepared))
   仔细看上述代码,训练数据和测试数据都进行了fit_transform操作,相当于产生了2个不同pipeline实例,编码出来特征维度肯定不一致。我还傻傻的画了个图,造成这种情况的原因是创建了2个pipeline实例去编码:

在这里插入图片描述

   错误修改

some_data_prepared = full_pipeline.transform(some_data) 【修改后】
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值