【阅读笔记】联邦学习实战——用FATE从零实现横向逻辑回归

前言

FATE是微众银行开发的联邦学习平台,是全球首个工业级的联邦学习开源框架,在github上拥有近4000stars,可谓是相当有名气的,该平台为联邦学习提供了完整的生态和社区支持,为联邦学习初学者提供了很好的环境,否则利用python从零开发,那将会是一件非常痛苦的事情。本篇博客内容涉及《联邦学习实战》第四章和第五章内容,使用的fate版本为1.6.0,下面就让我们开始吧。


1. FATE平台

1.1 FATE平台架构概述

在这里插入图片描述
FATE的官方文档提供了FATE的架构图,可以看到FATE的架构自上而下分为了四层,最上面一层是FATE提供的服务,包括FATE云服务,FATE面板,FATE任务调度管理,生命周期管理等,往下一层是FATE的机器学习核心组件,包括横向联邦学习、纵向联邦学习等,再往下一层是应用在FATE中的安全协议(隐私保护算法),包括同态加密,FedAvg,RSA等,最后一层是底层框架,又可以分为平行的三层,包括计算框架(TensorFlow,Pytorch,Spark),消息队列协议,以及存储框架。
FATE的主要功能如下:

  • 提供了一种基于数据隐私保护的分布式安全计算框架;
  • 为机器学习、深度学习等常用算法提供高性能的安全计算支持;
  • 支持同态加密、秘密共享等多种多方安全计算协议,确保数据和模型的安全;
  • 提供友好的跨域交互信息管理方案和开发文档,极大方便开发人员使用。

2. Docker镜像安装FATE

FATE支持Linux和Mac系统,支持单机部署、集群部署和KubeFATE部署三种方式,具体的部署安装请查看FATE的官方文档,这里只提供利用Docker镜像配置FATE单机部署。

2.1 Docker的安装

Docker的安装见我的另一篇博客,里面安装步骤很详细,官方文档推荐的是18.09版本,但是在Ubuntu20.04中,只提供了19以上的版本,所以我下载的是19版本,但是不影响FATE的使用。

2.2 FATE的安装

2.2.1 拉取镜像

设置部署的环境变量

$ export version={本次部署的FATE版本号, 如1.7.0}

通过镜像包

$ wget https://webank-ai-1251170195.cos.ap-guangzhou.myqcloud.com/fate/${version}/release/standalone_fate_docker_image_${version}_release.tar;
$ docker load < standalone_fate_docker_image_${version}_release.tar;
$ docker images | grep federatedai/standalone_fate

能看到对应的版本则镜像下载成功。

通过公共镜像拉取

$ docker pull federatedai/standalone_fate:${version}

2.2.2 启动

$ docker run -d --name standalone_fate -p 8080:8080 federatedai/standalone_fate:${version};
$ docker ps -a | grep standalone_fate

端口映射到8080,启动的容器名称为standalone_fate ,后面是镜像名(仓库+镜像名)。后面的指令输入后能看到对应的版本容器启动则成功。
ps:在启动的时候我遇到过端口占用的情况,用指令杀死占用端口的进程,还要注意删除容器,再重复run,不然会显示容器重复,如果杀死进程还是有端口占用,建议重启。

2.2.3 测试

进入容器

$ docker exec -it $(docker ps -aqf "name=standalone_fate") bash

如果报错(参数不匹配),就把$中的内容换成容器的ID即可,后面的bash表示用shell界面操作容器。
Toy测试

$ flow test toy -gid 10000 -hid 10000

如果成功,屏幕会显示

$ success to calculate secure_sum, it is 2000.0

单元测试

$ fate_test unittest federatedml --yes

如果成功,会显示下方语句

$ there are 0 failed test

有些用例算法在examples文件夹下,可以自行使用。

3. FATE编程范式

FATE构建联邦学习模型有两种不同编程范式。

  • 组件化配置:模型训练拆分为不同的任务,每个任务以组建的形式通过有向无环图相连,联邦学习所需的配置参数在配置文件中定义。该模式中,用户只需自定义和提交配置文件,就可以直接执行联邦训练。
  • 脚本编程:FATE提供API接口,用户通过脚本编程的方式实现联邦模型,类似直接使用Python编程。

由于脚本编程不够稳定,这里只使用组件化配置阐述。组件化配置需要提供两个配置文件。

  • dsl配置文件:FATE内置的一套自定义领域特定语言,在dsl中,常见的机器学习任务划分为不同的模块,如数据读写、模型训练、模型评估可以通过一个有向无环图连接。
  • conf配置文件:设置dsl中组件模块的参数。

利用FATE组件化配置的优点:

  • 提供多种安全策略机制;
  • 部署简单方便;
  • 提供可视化界面;
  • 支持常用的机器学习算法。

4. 用FATE从零实现横向逻辑回归

4.1 数据集

本章实验使用的数据集是威斯康星州临床科学中心开源的乳腺癌肿瘤数据集,该数据集内置在sklearn库中,可以直接加载查看。

from sklearn.datasets import load_breast_cancer
import pandas as pd

breast_dataset = load_breast_cancer()
breast = pd.DataFrame(breast_dataset.data, columns=breast_dataset.feature_names)
breast['y'] = breast_dataset.target
# 查看前五组数据
breast.head()

在这里插入图片描述可以看到,数据总共有30个特征(10个属性分别以均值、标准差、最大差值出现三次),569个样本中,恶性肿瘤样本212个,良性有357个。

4.2 逻辑回归

这是一个典型的二分类模型的训练,数据集并不大,使用简单的逻辑回归作为实验模型。
传统的线性回归定义:

y = W T X + b y=W^{T}X+b y=WTX+b
但是对于二分类模型来说,简单的函数线性映射是不能够分类的,还需要一个非线性函数映射到离散标签,

y = f ( W T X + b ) y=f(W^{T}X+b) y=f(WTX+b)

在逻辑回归中,使用logistic函数进行非线性映射,logistic表示为:

f ( z ) = 1 1 + e − z f(z)=\frac{1}{1+e^{-z} } f(z)=1+ez1

图像表示为:
在这里插入图片描述
可以看出, z = W T X + b ≥ 0 z=W^{T}X+b\ge 0 z=WTX+b0,则判断为正例,否则判断为反例。

4.3 横向数据切分

假设当前有两方参与横向联邦学习训练,取乳腺癌数据集前469条数据作为训练集,后100条作为测试集,数据切分的策略如下:

  • 训练数据切分:前200条作为公司A的本地数据,存为breast_1_train.csv,剩余269条作为公司B的本地数据,存为breast_2_train.csv
  • 测试数据集:不切分,由双方共用,存为breast_eval.csv

横向数据切分代码:

from sklearn.datasets import load_breast_cancer
import pandas as pd

# 导入并查看数据
breast_dataset = load_breast_cancer()
breast = pd.DataFrame(breast_dataset.data, columns=breast_dataset.feature_names)
breast.head()

# z-score标准化
breast = (breast-breast.mean()) / (breast.std())
# 获取列名
col_names = breast.columns.values.tolist()

# 更换列名
columns = {}
for idx, n in enumerate(col_names):
    columns[n] = "x%d"%idx
breast = breast.rename(columns=columns)

# 插入每行序列和y
breast['y'] = breast_dataset.target
idx = range(breast.shape[0])
breast.insert(0, 'idx', idx)

# 打乱数据并生成csv
breast = breast.sample(frac=1)
train = breast.iloc[:469]
eval =  breast.iloc[469:]
breast_1_train = breast.iloc[:200]
breast_2_train = breast.iloc[200:]
breast_1_train.to_csv('breast_1_train.csv', index=False, header=True)
breast_2_train.to_csv('breast_2_train.csv', index=False, header=True)
eval.to_csv('breast_eval.csv', index=False, header=True)

4.4 横向联邦模型训练

FATE构建联邦学习模型工作:

  • 数据输入:将文件转换为支持的DTable格式。DTable是一个分布式数据集合,FATE所有运算都基于DTable格式进行。
  • 模型训练:FATE为模型训练构建流水线。
  • 模型评估:将训练好的模型在测试集上评估。

4.4.1 数据输入

首先确定基目录,在运行FATE后,可以输入pwd查看,我的基目录为:

fate_dir=/data/projects/fate/

根据数据切分的结果,把三组数据集(.csv)上传到$fate_dir/examples/data/中。
由于文件需要上传到docker容器中,所以要特殊的文件上传工具,这里使用rz,如果docker中没有,则在docker中输入:

$ sudo apt-get install lrzsz

如果是在ubuntu本机上运行的,建议更换设备用xshell远程连接,否则在本机上输入:

$ rz -be

会出现一堆乱码,等半天也不跳出文件框,而在xshell中瞬间弹出,如果出现传输失败,那就要检查要传输的文件是否被使用。
在这里插入图片描述
用ls查看,左边一列即是上传的文件。

4.4.2 上传配置文件

由于FATE所有运算都基于DTable格式进行,所以要把上传的文件转换为DTable。首先上传配置文件,配置文件的实例文件有两种,对应v1和v2两个版本。

  • example/dsl/v1
    upload_data.jsonupload_host.jsonupload_guest.json,结构如下:
{
    "file": "examples/data/breast_hetero_guest.csv",	// 数据文件路径,相对于当前所在路径
    "head": 1,	// 指定数据文件是否包含表头,1: 是,0: 否
    "partition": 16,	// 指定用于存储数据的分区数
    "work_mode": 0, 	 // 指定工作模式,0: 单机版,1: 集群版
    "table_name": "breast_hetero_guest",	// 需要转换为DTable格式的表名(相当于后续需要使用的表)
    "namespace": "experiment"	// DTable格式的表名对应的命名空间
}
  • example/dsl/v2/upload
{
	"file": "/data/projects/fate/examples/data/breast_hetero_guest.csv",	// 数据文件路径,相对于当前所在路径
	"table_name": "breast_hetero_guest",	// 需要转换为DTable格式的表名
	"namespace": "experiment",// DTable格式的表名对应的命名空间
	"head": 1,	// 指定数据文件是否包含表头,1: 是,0: 否
	"partition": 8,	// 指定用于存储数据的分区数
	"work_mode": 0,	 // 指定工作模式,0: 单机版,1: 集群版
	"backend": 0	// 指定后端,0:EggRoll, 1: Spark _ RabbitMQ, 2: Spark + Pulsar
}

在FATE-1.6.0版本中,fate_flow的目录存放在fateflow/python/中,所以执行upload的过程和书中不一样,为:

python /fate/python/fate_flow/fate_flow_client.py -f upload -c upload_data.json

从FATE-1.5开始,推荐使用FATE-Flow-Client Command Line执行FATE-Flow任务,上传命令格式为:

$ flow data upload -c example/dsl/v2/upload/upload_conf.json

本文使用v1版本的上传数据操作,如果有想使用v2的朋友,可以访问参考链接的第一个链接。

配置文件如下:

  • 上传数据至公司A。
{
    "file": "/fate/example/data/breast_1_train.csv",
    "head": 1,
    "partition": 8,
    "work_mode": 0,
    "table_name": "homo_breast_1_train",
    "namespace": "homo_host_breast_train"
}

  • 上传数据至公司B。
{
    "file": "/fate/example/data/breast_2_train.csv",
    "head": 1,
    "partition": 8,
    "work_mode": 0,
    "table_name": "homo_breast_2_train",
    "namespace": "homo_guest_breast_train"
}

  • 上传测试数据至公司A 。
{
    "file": "/fate/example/data/breast_eval.csv",
    "head": 1,
    "partition": 8,
    "work_mode": 0,
    "table_name": "homo_breast_1_eval",
    "namespace": "homo_host_breast_eval"
}

  • 上传测试数据至公司B 。

在根目录下,输入python /fate/python/fate_flow/fate_flow_client.py -f upload -c upload_data.json (视每个人的环境而定)。返回如下代码则成功

{
    "data": {
        "board_url": "http://127.0.0.1:8080/index.html#/dashboard?job_id=202203110325129735931&role=local&party_id=0",
        "job_dsl_path": "/fate/jobs/202203110325129735931/job_dsl.json",
        "job_id": "202203110325129735931",
        "job_runtime_conf_on_party_path": "/fate/jobs/202203110325129735931/local/job_runtime_on_party_conf.json",
        "job_runtime_conf_path": "/fate/jobs/202203110325129735931/job_runtime_conf.json",
        "logs_directory": "/fate/logs/202203110325129735931",
        "model_info": {
            "model_id": "local-0#model",
            "model_version": "202203110325129735931"
        },
        "namespace": "homo_host_breast_train",
        "pipeline_dsl_path": "/fate/jobs/202203110325129735931/pipeline_dsl.json",
        "table_name": "homo_breast_1_train",
        "train_runtime_conf_path": "/fate/jobs/202203110325129735931/train_runtime_conf.json"
    },
    "jobId": "202203110325129735931",
    "retcode": 0,
    "retmsg": "success"
}

还可以在FATEboard上查看可视化结果。
在这里插入图片描述

4.4.3 模型训练

FATE支持常用的机器学习模型,如:

  • 线性模型:横向、纵向的线性回归、logistic回归等线性模型。
  • 树模型:基于纵向的GBDT实现。
  • 神经网络:支持横向的深度神经网络模型DNN。

在这里插入图片描述
可以看到在fate dsl_conf的V2版本,模型种类又比书中所描述的详细不少。
V2版本在预设任务配置上有一些改变和提升,最直观的一点是不会自动为训练任务生成预测dsl,需要用户自定义,也可以使用flow命令自动配置预测dsl。
为了在V2中使用命令行客户端,即flow命令,需要进行一系列配置。参考官方文档FATE Client包含了FATE项目多个客户端:Pipeline, FATE Flow ClientFATE Test
获取所有命令分类和子命令:

    [IN]
    flow

    [OUT]
    Usage: flow COMMAND [OPTIONS]

      Fate Flow Client

    Options:
      -h, --help  Show this message and exit.

    Commands:
      component   Component Operations
      data        Data Operations
      init        Flow CLI Init Command
      job         Job Operations
      model       Model Operations
      queue       Queue Operations
      table       Table Operations
      task        Task Operations

安装FATE CLient

$ pip install fate-client
# 或者
$ pip install fate-client==${version}

集群上安装请移步官方文档。
初始化

# 指定fateflow的IP地址和端口进行初始化
$ flow init --ip 192.168.0.1 --port 9380

获得如下返回视为初始化成功:

{
    "retcode": 0,
    "retmsg": "Fate Flow CLI has been initialized successfully."
}

验证
查询任务情况

$ flow job query

返回以下即可:

{
    "data": [],
    "retcode": 0,
    "retmsg": "no job could be found"
}

本章使用逻辑回归模型,进入$fate_dir/examples/dsl/v1/homo_logistic_regression目录,该目录下已经有很多预设的dsl文件和conf文件。修改homo_lr_train_dsl.jsonhomo_lr_train_conf.json。修改推荐用vim,如果没有,用yumapt-get进行安装:

$ yum install -y vim
# 如果yum没有
$ apt-get install -y vim
  • test_homolr_train_dsl.json:描述任务模块,将任务模块以有向无环图形式组合。包括dataio_0homo_lr_0evaluation_0,这些组件分别用作数据格式转换、自带横向逻辑回归组件、模型评估。
    在这里插入图片描述

  • test_homolr_train_conf.json:设置各个组建的参数。找到role字段,修改三个参数,train_data下的namenamespace,以及表示标签列对应的属性名label_name在这里插入图片描述
    接着是algorithm_parameters字段,它是用来设置训练的超参数信息,包括学习率,优化函数,迭代次数等,可以根据实际需要自行修改。在这里插入图片描述

文件配置结束,在当前位置输入:

$ python /fate/python/fate_flow/fate_flow_client.py -f submit_job -d test_homolr_train_job_dsl.json -c test_homolr_train_job_conf.json

在FATEboard上查看任务运行情况。

在这里插入图片描述在这里插入图片描述

4.4.4 模型评估

常用的模型评估方法包括留出法和交叉验证法。

  • 留出法:将数据按照一定比例切分,预留一部分数据作为评估模型数据。
  • 交叉验证法: 将数据集D切分为k份,D1,D2,…,Dk,这样可以获得k组不同的训练数据集和评估数据集,得到k个评估的结果,取其平均值作为最终模型评估结果。

由于之前已经有了额外的数据集作为评估数据集,这里用留出法。为了将留出的数据用于模型评估,需要修改dsl组建配置。具体来说,在test_homolr_train_job_dsl文件中,在components组件下添加一个新的数据输入组件dataio_1,用来读取测试数据,如下所示。

{
    "components" : {
        "dataio_0": {
            "module": "DataIO",
            "input": {
                "data": {
                    "data": ["args.train_data"]
                }
            },
            "output": {
                "data": ["train"],
                "model": ["dataio"]
            }
         },
        "dataio_1": {
            "module": "DataIO",
            "input": {
                "data": {
                    "data": ["args.eval_data"] # 表示测试数据采用conf文件中的args.eval_data设置的文件
                },
                "model": ["dataio_0.dataio"] # 使用数据训练模块"dataio_0.dataio"的输出作为"dataio_1"的模型输入
            },
            "output": {
                "data": ["eval_data"] # 设置输出的data名称,可任意设定
            }
        },
        "feature_scale_0": {
            "module": "FeatureScale",
            "input": {
                "data": {
                    "data": ["dataio_0.train"]
                }
            },
            "output": {
                "data": ["train"],
                "model": ["feature_scale"]
            }
        },
        "feature_scale_1": {
            "module": "FeatureScale",
            "input": {
                "data": {
                    "data": ["dataio_1.eval_data"]
                }
            },
            "output": {
                "data": ["eval_data"],
                "model": ["feature_scale"]
            }
        },        
        "homo_lr_0": {
            "module": "HomoLR",
            "input": {
                "data": {
                    "train_data": ["feature_scale_0.train"]
                }
            },
            "output": {
                "data": ["train"],
                "model": ["homolr"]
            }
        },
        "homo_lr_1": {
            "module": "HomoLR",
            "input": {
                "data": {
                    "eval_data": ["feature_scale_1.eval_data"]	# 指定训练数据
                },
                "model": ["homo_lr_0.homolr"]
            },
            "output": {
                "data": ["eval_data"],
                "model": ["homolr"]
            }
        },        
        "evaluation_0": {
            "module": "Evaluation",
            "input": {
                "data": {
                    "data": ["homo_lr_0.train"]
                }
            }
        },
        "evaluation_1": {
            "module": "Evaluation",
            "input": {
                "data": {
                    "data": ["homo_lr_1.eval_data"]
                }
            }
        }        
    }
}

然后修改conf文件,在role_parameters字段中为guest和host添加测试数据的DTable表名。

{
    "initiator": {
        "role": "guest",
        "party_id": 10000
    },
    "job_parameters": {
        "work_mode": 0
    },
    "role": {
        "guest": [10000],
        "host": [10000],
        "arbiter": [10000]
    },
    "role_parameters": {
        "guest": {
            "args": {
                "data": {
                    "train_data": [
                        {
                            "name": "homo_breast_2_train",
                            "namespace": "homo_guest_breast_train"
                        }
                    ],
                    "eval_data": [
                        {
                            "name": "homo_breast_2_eval",
                            "namespace": "homo_guest_breast_eval"
                        }
                    ]
                }
            },
            "dataio_0": {
                "with_label": [true],
                "label_name": ["y"],
                "label_type": ["int"],
                "output_format": ["dense"]
            }
        },
        "host": {
            "args": {
                "data": {
                    "train_data": [
                        {
                            "name": "homo_breast_1_train",
                            "namespace": "homo_host_breast_train"
                        }
                    ],
                    "eval_data": [
                        {
                            "name": "homo_breast_1_eval",
                            "namespace": "homo_host_breast_eval"
                        }
                    ]
                }
            },
            "dataio_0": {
                "with_label": [true],
                "label_name": ["y"],
                "label_type": ["int"],
                "output_format": ["dense"]
            },
            "evaluation_0": {
                "need_run": [false]
            },
            "evaluation_1": {
                "need_run": [false]
            }
        }
    },
    "algorithm_parameters": {
        "homo_lr_0": {
            "penalty": "L2",
            "optimizer": "sgd",
            "tol": 1e-05,
            "alpha": 0.01,
            "max_iter": 20,
            "early_stop": "diff",
            "batch_size": 320,
            "learning_rate": 0.05,
            "validation_freqs": 1,
            "init_param": {
                "init_method": "zeros"
            },
            "encrypt_param": {
                "method": null
            },
            "cv_param": {
                "n_splits": 4,
                "shuffle": true,
                "random_seed": 33,
                "need_cv": false
            }
        },
        "evaluation_0": {
            "eval_type": "binary"
        }
    }
}

执行submit_job命令。

$ python /fate/python/fate_flow/fate_flow_client.py -f submit_job -d test_homolr_train_job_dsl.json -c test_homolr_train_job_conf.json

可以查看带有模型评估算法模块的有向无环图,该图由训练模块和评估模块两部分构成。
在这里插入图片描述

4.4.5 多参与方环境配置

对于多个客户端参与的场景,需要在配置文件中修改部分参数值。

  • 首先在conf文件中找到role字段,在默认情况下只有两方,多客户端就是在role下host子字段方添加新的客户端ID即可,各个客户端ID逗号分隔。
  • role_parameters字段中,host子字段,添加对应新的客户端DTable表名和命名空间。

阅读总结

本来这篇博客因该早在半个月前就能写完,但是编写到一半,代码跑不出来了。。。当时用的是FATE1.7.0,单元测试都没问题,但是只要运行甚至是上传数据,都会报错,报错如下:
在这里插入图片描述
不知道访问了多少论坛,在FATE的各个交流群都问了个遍,至今杳无音讯,只能作罢,其中重装fate,重装docker,换fate的旧版本,就差重装系统了。终于测试fate1.6.0版本时,没有这个问题了(虽然有其他小问题但不影响结果)。这半个多月来对我的心智是一个很大的考验,我甚至一度摆烂觉得永远不可能跑通了,好在功夫不负有心人,最后我想对环境有问题的小伙伴们提个建议,如果怎么都没办法跑通,沉淀一下自己,歇几天做别的事情,没准就能转换思路了。最后的最后,如果有朋友知道上图问题如何解决,请务必告诉我,万分感谢!

参考链接

https://blog.csdn.net/Sisyphus_98/article/details/122933110
https://blog.csdn.net/qq_41841524/article/details/117662143?spm=1001.2014.3001.5502

  • 15
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 26
    评论
JDBC(Java Database Connectivity)是Java语言操作数据库的基础API,它提供了一种标准的方法来连接和操作各种关系型数据库。 JDBC的基本使用分为以下几个步骤: 1. 加载驱动程序:在使用JDBC操作数据库之前,需要先加载相应的驱动程序。不同的数据库需要加载不同的驱动程序,一般情况下驱动程序的jar包都会提供。 ```java Class.forName("com.mysql.jdbc.Driver"); ``` 2. 建立连接:使用DriverManager类的getConnection()方法连接数据库。 ```java String url = "jdbc:mysql://localhost:3306/test?useSSL=false"; String username = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, username, password); ``` 其中url为连接数据库的URL,username和password为连接数据库的用户名和密码。 3. 创建Statement对象:通过Connection对象的createStatement()方法创建一个Statement对象,用于执行SQL语句。 ```java Statement stmt = conn.createStatement(); ``` 4. 执行SQL语句:可以通过Statement对象的execute()或executeQuery()方法执行SQL语句,如果是更新操作则使用executeUpdate()方法。 ```java ResultSet rs = stmt.executeQuery("SELECT * FROM users"); ``` 5. 处理结果集:如果执行的SQL语句返回了结果集,则可以通过ResultSet对象进行处理。 ```java while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); System.out.println("id: " + id + ", name: " + name); } ``` 6. 关闭连接:使用完数据库后需要关闭连接,释放资源。 ```java rs.close(); stmt.close(); conn.close(); ``` 以上就是JDBC的基本使用过程,通过JDBC可以实现Java程序与数据库的交互,完成各种数据库操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

HERODING77

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

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

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

打赏作者

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

抵扣说明:

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

余额充值