PlatEMO代码解析

PlatEMO代码解析

Created: January 16, 2024
Finished: January 17, 2024
Tags: main机器学习
进度: 已完成

以SparseEA解决ZDT1问题为例 platemo(‘algorithm’,@SparseEA,‘problem’,@ZDT1)

读取当前代码路径,D:\Project\ML\PlatEMO-master-EA\PlatEMO

之后将本路径下的所有文件夹都包括进调佣函数的目录

platemo_path = cd(fileparts(mfilename('fullpath')));
addpath(genpath(cd));

\color{#CD5C5C}\rule{740px}{3px}

if isempty(varargin)
    if verLessThan('matlab','9.9')
        errordlg('Fail to create the GUI of PlatEMO since the version for MATLAB is lower than R2020b. You can use PlatEMO without GUI by calling platemo() with parameters.','Error','modal');
    else
        try
            GUI();
        catch err
            errordlg('Fail to create the GUI, please make sure all the folders of PlatEMO have been added to search path.','Error','modal');
            rethrow(err);
        end
    end
else
    if verLessThan('matlab','9.4')
        error('Fail to use PlatEMO since the version for MATLAB is lower than R2018a. Please update your MATLAB software.');
    else
        [PRO,input] = getSetting(varargin);
        Problem     = PRO(input{:});
        [ALG,input] = getSetting(varargin,Problem);
        if nargout > 0
            Algorithm = ALG(input{:},'save',0);
        else
            Algorithm = ALG(input{:});
        end
        Algorithm.Solve(Problem);
        if nargout > 0
            P = Algorithm.result{end};
            varargout = {P.decs,P.objs,P.cons};
        end
    end
end
end

\color{#CD5C5C}\rule{740px}{3px}

函数解释

isempty函数

varargin是函数的输入变量,是所有参数组成的一个cell

isempty是值如果var为空,则isempty(varargin)为1,否则为0

因此,如果直接运行platemo,并且版本符合要求的话,会调用GUI()函数,生成图形化界面。

isempty(varargin)

\color{#FFAF33}\rule{740px}{2px}

[PRO,input] = getSetting(varargin) 解释

function [name,Setting] = getSetting(Setting,Pro)
    isStr = find(cellfun(@ischar,Setting(1:end-1))&~cellfun(@isempty,Setting(2:end)));
    if nargin > 1 %nargin代表输入参数数量
        index = isStr(find(strcmp(Setting(isStr),'algorithm'),1)) + 1;
        if isempty(index)
            names = {@BSPGA,@GA,@SACOSO,@GA;@PMMOEA,@NSGAIII,@KRVEA,@NSGAIII;@RVEA,@RVEA,@CSEA,@RVEA};
            name  = names{find([Pro.M<2,Pro.M<4,1],1),find([all(Pro.encoding==4),any(Pro.encoding>2),Pro.maxFE<=1000&Pro.D<=10,1],1)};
        elseif iscell(Setting{index})
            name    = Setting{index}{1};
            Setting = [Setting,{'parameter'},{Setting{index}(2:end)}];
        else
            name = Setting{index};
        end
    else
        index = isStr(find(strcmp(Setting(isStr),'problem'),1)) + 1;
        if isempty(index)
            name = @UserProblem;
        elseif iscell(Setting{index})
            name    = Setting{index}{1};
            Setting = [Setting,{'parameter'},{Setting{index}(2:end)}];
        else
            name = Setting{index};
        end
    end
end

var里面的内容是’algorithm’,@SparseEA,‘problem’,@ZDT1

所以,进入getsetting函数后,Setting变量被设置为’algorithm’,@SparseEA,‘problem’,@ZDT1,Pro没有值


cellfun(@ischar,Setting(1:end-1))

cellfun是为cell类型值应用fun函数

在本函数中,就是为Setting(1:end-1)数组应用ischar函数,得到的值为1 0 1

~cellfun(@isempty,Setting(2:end))

则是为Setting(2:end)数组应用isempty函数,得到的值为0 0 0 ,取反可得1 1 1

进行&合并之后,得到1 0 1

find()函数会返回非0元素的索引值 ,即1 3

isStr = find(cellfun(@ischar,Setting(1:end-1))&~cellfun(@isempty,Setting(2:end)));

之后由于nargin(输入参数数量)为1,所以进入了else条件中

经过运行,index的值为4

再次标记,isempty(a) a为空,则值为1,iscell(A) A为元胞数组,则值为1。

所以第一次调用getSetting返回的是name,即@ZDT1,也就是PRO的值,和Setting,即input的值

\color{#FFAF33}\rule{740px}{2px}

Problem = PRO(input{:}); 解释

properties
    N          = 100;      	% Population size
    maxFE      = 10000;     % Maximum number of function evaluations
    FE         = 0;        	% Number of consumed function evaluations
end
properties(SetAccess = protected)
    M;                    	% Number of objectives
    D;                     	% Number of decision variables
    maxRuntime = inf;      	% maximum runtime (in second)
    encoding   = 1;        	% Encoding scheme of each decision variable (1.real 2.integer 3.label 4.binary 5.permutation)
    lower      = 0;     	  % Lower bound of each decision variable
    upper      = 1;        	% Upper bound of each decision variable
    optimum;              	% Optimal values of the problem
    PF;                   	% Image of Pareto front
    parameter  = {};       	% Other parameters of the problem
end

在进入PROBLEM.m文件中,应该运行了如上代码,是obj的参数赋值。

methods(Access = protected)
    function obj = PROBLEM(varargin)
        isStr = find(cellfun(@ischar,varargin(1:end-1))&~cellfun(@isempty,varargin(2:end)));
        for i = isStr(ismember(varargin(isStr),{'N','M','D','maxFE','maxRuntime','parameter'}))
            obj.(varargin{i}) = varargin{i+1};
        end
        obj.Setting();
        obj.optimum  = obj.GetOptimum(10000);
        obj.PF       = obj.GetPF();
    end
end

这是matlab创建类语句,methods

Access=protected表示这个类是受保护的属性,即表示只有该类的成员方法,还有该类的子类可以访问该数据,类之外的函数或者脚本访问不了这个成员变量

methods(Access = protected)
	**
end

for循环时,为 i 赋值,为[],所以进入不了for循环,直接运行到end

for i = isStr(ismember(varargin(isStr),{'N','M','D','maxFE','maxRuntime','parameter'}))
    obj.(varargin{i}) = varargin{i+1};
end

进入obj.Setting()函数

%% Default settings of the problem
function Setting(obj)
    obj.M = 2;
    if isempty(obj.D); obj.D = 30; end
    obj.lower    = zeros(1,obj.D);
    obj.upper    = ones(1,obj.D);
    obj.encoding = ones(1,obj.D);
end

ZDT1是PROBLEM的类中的值,obj.Setting()函数会进入到具体的问题去运行,得出来对应问题的目标数量,决策变量数量,上界和下界,以及编码方式。


obj.optimum = obj.GetOptimum(10000);函数

%% Generate points on the Pareto front
function R = GetOptimum(obj,N)
    R(:,1) = linspace(0,1,N)';
    R(:,2) = 1 - R(:,1).^0.5;
end

R(:,1) = linspace(0,1,N)';,生成0-1之间的10000个等距离点,然后进行转置

R(:,2) = 1 - R(:,1).^0.5; 使这10000个点具体到0-1之间

生成的两列值会保存到obj.optimum里面


obj.PF = obj.GetPF();函数

%% Generate the image of Pareto front
function R = GetPF(obj)
		R = obj.GetOptimum(100);
end

得到R的值,存进obj.PF里面,得到了当前问题的Pareto前沿面

\color{#FFAF33}\rule{740px}{2px}

[ALG,input] = getSetting(varargin,Problem);函数

第二次进入函数,得到的值为ALG,ALG赋值为SparseEA。

\color{#FFAF33}\rule{740px}{2px}

Algorithm = ALG(input{:});

给Algorithm赋值

\color{#FFAF33}\rule{740px}{2px}

然后跑进了Algorithm.Solve(Problem);函数

obj.result = {};
obj.metric = struct('runtime',0);
obj.pro    = Problem;
obj.pro.FE = 0;
addpath(fileparts(which(class(obj))));
addpath(fileparts(which(class(obj.pro))));
tic; 
obj.main(obj.pro);

就是继续给obj赋值,然后跑进了obj.main(obj.pro)中,即具体的SparseEA代码中

function main(Algorithm,Problem)         
    %% Population initialization
    % Calculate the fitness of each decision variable
    TDec    = [];
    TMask   = [];
    TempPop = [];
    ArcPop  = [];
    Fitness = zeros(1,Problem.D);
    for i = 1 : 1+4*any(Problem.encoding~=4)
        Dec = unifrnd(repmat(Problem.lower,Problem.D,1),repmat(Problem.upper,Problem.D,1));%生成0-1之间的30*30矩阵,D*D
        Dec(:,Problem.encoding==4) = 1;
        Mask       = eye(Problem.D);
        Population = Problem.Evaluation(Dec.*Mask);
        TDec       = [TDec;Dec];
        TMask      = [TMask;Mask];
        TempPop    = [TempPop,Population];
        Fitness    = Fitness + NDSort([Population.objs,Population.cons],inf);
    end
    % Generate initial population
    Dec = unifrnd(repmat(Problem.lower,Problem.N,1),repmat(Problem.upper,Problem.N,1));
    Dec(:,Problem.encoding==4) = 1;
    Mask = false(Problem.N,Problem.D);
    for i = 1 : Problem.N
        Mask(i,TournamentSelection(2,ceil(rand*Problem.D),Fitness)) = 1;
    end
    Population = Problem.Evaluation(Dec.*Mask);
    [Population,Dec,Mask] = EnvironmentalSelection([Population,TempPop],[Dec;TDec],[Mask;TMask],Problem.N);

    %% Optimization
    while Algorithm.NotTerminated(Population)
        %AllPop           = [Population,ArcPop];
        MatingPool = randi(Problem.N,1,Problem.N);
        %MatingPool       = MatingSelection(Population,ArcPop,Problem.N);
        %MatingPool       = TournamentSelection(2,2*Problem.N,FrontNo,-CrowdDis);
        [OffDec,OffMask] = Operator(Problem,Dec(MatingPool,:),Mask(MatingPool,:),Fitness);
        Offspring        = Problem.Evaluation(OffDec.*OffMask);
        %ArcPop           = UpdateArc([AllPop,Offspring],Problem.N);
        [Population,Dec,Mask] = EnvironmentalSelection([Population,Offspring],[Dec;OffDec],[Mask;OffMask],Problem.N);
        Allpop = [Population Offspring];
        N          = min(Problem.N,length(Allpop));
        [FrontNo,MaxFNo] = NDSort(Allpop.objs,N);
        
         %% Association operation
        Next   = [find(FrontNo<MaxFNo),find(FrontNo==MaxFNo)];
        Choose = Association(Allpop(FrontNo<MaxFNo).objs,Allpop(FrontNo==MaxFNo).objs,N);
        Next   = Next(Choose);
        score = max(Next) - Next + 1;
        MaskScore = Mask .* repmat(score, [Problem.D 1])';
        decScore = sum(MaskScore);
        Fitness = max(decScore) - decScore + 1;
    end
end

Population = Problem.Evaluation(Dec.*Mask)函数解释

计算种群的各项参数值,

function Population = Evaluation(obj,varargin)
    PopDec     = obj.CalDec(varargin{1});
    PopObj     = obj.CalObj(PopDec);
    PopCon     = obj.CalCon(PopDec);
    Population = SOLUTION(PopDec,PopObj,PopCon,varargin{2:end});
    obj.FE     = obj.FE + length(Population);
end

PopDec = obj.CalDec(varargin{1});函数

function PopDec = CalDec(obj,PopDec)
    Type  = arrayfun(@(i)find(obj.encoding==i),1:5,'UniformOutput',false);
    index = [Type{1:3}];
    if ~isempty(index)
        PopDec(:,index) = max(min(PopDec(:,index),repmat(obj.upper(index),size(PopDec,1),1)),repmat(obj.lower(index),size(PopDec,1),1));
    end
    index = [Type{2:5}];
    if ~isempty(index)
        PopDec(:,index) = round(PopDec(:,index));
    end
end

PopObj = obj.CalObj(PopDec);函数是进入了ZDT1.m文件中运行的

%% Calculate objective values
function PopObj = CalObj(obj,PopDec)
    PopObj(:,1) = PopDec(:,1);
    g = 1 + 9*mean(PopDec(:,2:end),2);
    h = 1 - (PopObj(:,1)./g).^0.5;
    PopObj(:,2) = g.*h;
end

PopCon = obj.CalCon(PopDec);函数

function PopCon = CalCon(obj,PopDec)
    PopCon = zeros(size(PopDec,1),1);
end

Population = SOLUTION(PopDec,PopObj,PopCon,varargin{2:end});函数是将这几个变量都保存到Population结构体中

obj.FE = obj.FE + length(Population);表示适应度评价次数为30次

然后就是在循环中不断对种群进行处理,知道到达了终止条件

\color{#FFAF33}\rule{740px}{2px}

画图函数其实是在Algorithm.NotTerminated(Population)这个里面的

obj.metric.runtime = obj.metric.runtime + toc;
if obj.pro.maxRuntime < inf
    obj.pro.maxFE = obj.pro.FE*obj.pro.maxRuntime/obj.metric.runtime;
end
num   = max(1,abs(obj.save));
index = max(1,min(min(num,size(obj.result,1)+1),ceil(num*obj.pro.FE/obj.pro.maxFE)));
obj.result(index,:) = {obj.pro.FE,Population};
drawnow('limitrate');
obj.outputFcn(obj,obj.pro);
nofinish = obj.pro.FE < obj.pro.maxFE;
assert(nofinish,'PlatEMO:Termination',''); tic;

然后最重要的是,有这么一句强制执行的

outputFcn = @DefaultOutput;     % Function called after each generation

所以会进入DefaultOutput函数,最后输出的数据和图像都在这个函数中可以看到,就运行完毕了

function DefaultOutput(Algorithm,Problem)
% The default output function of ALGORITHM

    clc; fprintf('%s on %d-objective %d-variable %s (%6.2f%%), %.2fs passed...\n',class(Algorithm),Problem.M,Problem.D,class(Problem),Problem.FE/Problem.maxFE*100,Algorithm.metric.runtime);
    if Problem.FE >= Problem.maxFE
        if Algorithm.save < 0
            if Problem.M > 1
                Population = Algorithm.result{end};
                if length(Population) >= size(Problem.optimum,1); name = 'HV'; else; name = 'IGD'; end
                value = Algorithm.CalMetric(name);
                figure('NumberTitle','off','Name',sprintf('%s : %.4e  Runtime : %.2fs',name,value(end),Algorithm.CalMetric('runtime')));
                title(sprintf('%s on %s',class(Algorithm),class(Problem)),'Interpreter','none');
                top = uimenu(gcf,'Label','Data source');
                g   = uimenu(top,'Label','Population (obj.)','CallBack',{@(h,~,Pro,P)eval('Draw(gca);Pro.DrawObj(P);cb_menu(h);'),Problem,Population});
                uimenu(top,'Label','Population (dec.)','CallBack',{@(h,~,Pro,P)eval('Draw(gca);Pro.DrawDec(P);cb_menu(h);'),Problem,Population});
                uimenu(top,'Label','True Pareto front','CallBack',{@(h,~,P)eval('Draw(gca);Draw(P,{''\it f\rm_1'',''\it f\rm_2'',''\it f\rm_3''});cb_menu(h);'),Problem.optimum});
                cellfun(@(s)uimenu(top,'Label',s,'CallBack',{@(h,~,A)eval('Draw(gca);Draw(A.CalMetric(h.Label),''-k.'',''LineWidth'',1.5,''MarkerSize'',10,{''Number of function evaluations'',strrep(h.Label,''_'','' ''),[]});cb_menu(h);'),Algorithm}),{'IGD','HV','GD','Feasible_rate'});
                set(top.Children(4),'Separator','on');
                g.Callback{1}(g,[],Problem,Population);
            else
                best = Algorithm.CalMetric('Min_value');
                if isempty(best); best = nan; end
                figure('NumberTitle','off','Name',sprintf('Min value : %.4e  Runtime : %.2fs',best(end),Algorithm.CalMetric('runtime')));
                title(sprintf('%s on %s',class(Algorithm),class(Problem)),'Interpreter','none');
                top = uimenu(gcf,'Label','Data source');
                uimenu(top,'Label','Population (dec.)','CallBack',{@(h,~,Pro,P)eval('Draw(gca);Pro.DrawDec(P);cb_menu(h);'),Problem,Algorithm.result{end}});
                cellfun(@(s)uimenu(top,'Label',s,'CallBack',{@(h,~,A)eval('Draw(gca);Draw(A.CalMetric(h.Label),''-k.'',''LineWidth'',1.5,''MarkerSize'',10,{''Number of function evaluations'',strrep(h.Label,''_'','' ''),[]});cb_menu(h);'),Algorithm}),{'Min_value','Feasible_rate'});
                set(top.Children(2),'Separator','on');
                top.Children(2).Callback{1}(top.Children(2),[],Algorithm);
            end
        elseif Algorithm.save > 0
            folder = fullfile('Data',class(Algorithm));
            [~,~]  = mkdir(folder);
            file   = fullfile(folder,sprintf('%s_%s_M%d_D%d_',class(Algorithm),class(Problem),Problem.M,Problem.D));
            runNo  = 1;
            while exist([file,num2str(runNo),'.mat'],'file') == 2
                runNo = runNo + 1;
            end
            result = Algorithm.result;
            metric = Algorithm.metric;
            save([file,num2str(runNo),'.mat'],'result','metric');
        end
    end
end

多目标优化问题的Problem.M一定是大于1的,所以只分析多目标的一段

uimenu 在当前图窗中创建菜单,并返回 Menu 对象。

top = uimenu(gcf,'Label','Data source');
g   = uimenu(top,'Label','Population (obj.)','CallBack',{@(h,~,Pro,P)eval('Draw(gca);Pro.DrawObj(P);cb_menu(h);'),Problem,Population});
uimenu(top,'Label','Population (dec.)','CallBack',{@(h,~,Pro,P)eval('Draw(gca);Pro.DrawDec(P);cb_menu(h);'),Problem,Population});
uimenu(top,'Label','True Pareto front','CallBack',{@(h,~,P)eval('Draw(gca);Draw(P,{''\it f\rm_1'',''\it f\rm_2'',''\it f\rm_3''});cb_menu(h);'),Problem.optimum});
cellfun(@(s)uimenu(top,'Label',s,'CallBack',{@(h,~,A)eval('Draw(gca);Draw(A.CalMetric(h.Label),''-k.'',''LineWidth'',1.5,''MarkerSize'',10,{''Number of function evaluations'',strrep(h.Label,''_'','' ''),[]});cb_menu(h);'),Algorithm}),{'IGD','HV','GD','Feasible_rate'});

如上代码是platemo可以画图的主要代码部分

uimenu创建了菜单栏,所以在生成图像中,才可以在data source下选择各项指标

接下来的每一句话其实都代表了每一项数据生成


eval的用法

eval可以动态执行命令,也就是说相当于可以捕捉代码执行过程中某个状态

上述代码也使用了eval,是指的变量不固定,Pro总是随着代码变化的

for i = 1:5
    eval(['variable_' num2str(i) ' = i;']);
end

disp(variable_3); % 输出 3
disp(variable_5); % 输出 5

对’‘\it f\rm_1’‘,’‘\it f\rm_2’‘,’‘\it f\rm_3’’ 的解释: f 1 f 2 f 3 \it f\rm_1 \it f\rm_2 \it f\rm_3 f1f2f3

在latex中:\it 表示意大利体 \rm表示罗马体 即 f 是意大利体 1 是罗马体

\color{#FFAF33}\rule{740px}{2px}

OK,三个画图函数还是有分析的

function currentAxes = Draw(Data,varargin)
    persistent ax;
    if length(Data) == 1 && isgraphics(Data)
        ax = Data;
        cla(ax);
    elseif ~isempty(Data) && ismatrix(Data)
        if isempty(ax) || ~isgraphics(ax)
            ax = gca;
        end
        if size(Data,2) == 1
            Data = [(1:size(Data,1))',Data];
        end
        set(ax,'FontName','Times New Roman','FontSize',13,'NextPlot','add','Box','on','View',[0 90],'GridLineStyle','none');
        if islogical(Data)
            [ax.XLabel.String,ax.YLabel.String,ax.ZLabel.String] = deal('Solution No.','Dimension No.',[]);
        elseif size(Data,2) > 3
            [ax.XLabel.String,ax.YLabel.String,ax.ZLabel.String] = deal('Dimension No.','Value',[]);
        elseif ~isempty(varargin) && iscell(varargin{end})
            [ax.XLabel.String,ax.YLabel.String,ax.ZLabel.String] = deal(varargin{end}{:});
        end
        if ~isempty(varargin) && iscell(varargin{end})
            varargin = varargin(1:end-1);
        end
        if isempty(varargin)
            if islogical(Data)
                varargin = {'EdgeColor','none'};
            elseif size(Data,2) == 2
                varargin = {'o','MarkerSize',6,'Marker','o','Markerfacecolor',[.7 .7 .7],'Markeredgecolor',[.4 .4 .4]};
            elseif size(Data,2) == 3
                varargin = {'o','MarkerSize',8,'Marker','o','Markerfacecolor',[.7 .7 .7],'Markeredgecolor',[.4 .4 .4]};
            elseif size(Data,2) > 3
                varargin = {'-','Color',[.5 .5 .5],'LineWidth',2};
            end
        end
        if islogical(Data)
            C = zeros(size(Data)) + 0.6;
            C(~Data) = 1;
            surf(ax,zeros(size(Data')),repmat(C',1,1,3),varargin{:});
        elseif size(Data,2) == 2
            plot(ax,Data(:,1),Data(:,2),varargin{:});
        elseif size(Data,2) == 3
            plot3(ax,Data(:,1),Data(:,2),Data(:,3),varargin{:});
            view(ax,[135 30]);
        elseif size(Data,2) > 3
            Label = repmat([0.99,2:size(Data,2)-1,size(Data,2)+0.01],size(Data,1),1);
            Data(2:2:end,:)  = fliplr(Data(2:2:end,:));
            Label(2:2:end,:) = fliplr(Label(2:2:end,:));
            plot(ax,reshape(Label',[],1),reshape(Data',[],1),varargin{:});
        end
        axis(ax,'tight');
        set(ax.Toolbar,'Visible','off');
        set(ax.Toolbar,'Visible','on');
    end
    currentAxes = ax;
en

Draw函数的作用有两个,一个是选定合适的坐标系,另一个就是会把种群的数据点绘制到图像上

function DrawObj(obj,Population)
    ax = Draw(Population.objs,{'\it f\rm_1','\it f\rm_2','\it f\rm_3'});
    if ~isempty(obj.PF)
        if ~iscell(obj.PF)
            if obj.M == 2
                plot(ax,obj.PF(:,1),obj.PF(:,2),'-k','LineWidth',1);
            elseif obj.M == 3
                plot3(ax,obj.PF(:,1),obj.PF(:,2),obj.PF(:,3),'-k','LineWidth',1);
            end
        else
            if obj.M == 2
                surf(ax,obj.PF{1},obj.PF{2},obj.PF{3},'EdgeColor','none','FaceColor',[.85 .85 .85]);
            elseif obj.M == 3
                surf(ax,obj.PF{1},obj.PF{2},obj.PF{3},'EdgeColor',[.8 .8 .8],'FaceColor','none');
            end
            set(ax,'Children',ax.Children(flip(1:end)));
        end
    elseif size(obj.optimum,1) > 1 && obj.M < 4
        if obj.M == 2
            plot(ax,obj.optimum(:,1),obj.optimum(:,2),'.k');
        elseif obj.M == 3
            plot3(ax,obj.optimum(:,1),obj.optimum(:,2),obj.optimum(:,3),'.k');
        end
    end
end

DrawObj函数的作用其实更偏向于根据Problem本身绘制真实的Pareto前沿面

function cb_menu(h)
% Switch between the selected menu
    set(get(get(h,'Parent'),'Children'),'Checked','off');
    set(h,'Checked','on');
end

本函数的作用就是选择对应的图像到对应的菜单栏中

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

轻点玩家

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

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

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

打赏作者

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

抵扣说明:

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

余额充值