【强化学习】基于表格型方法的规划和学习

目录

model和planning

Dyna:集成在一起的Planning、Action和Learning

如果Model是错误的

Prioritized Sweeping

Expected Update Vs.Sample Update

Trajectory Sampling

RTDP

Planning at Decision Time

Heuristic Search

 Rollout Algorithms

Monte Carlo Tree Search

 总结


强化学习可以分为:(1)基于环境模型已知的MDP的方法(model-based),如:动态规划方法(DP),启发式搜索。(2)基于环境模型未知的方法(model-free),如:MC方法、TD方法等。这两种方法有以下共同点:

(1)方法的核心都是计算value function。

(2)都是站在当前state,向前n步,然后计算回溯值。

(3)使用回溯值来更新value function的估计值。

model和planning

agent用model来预测环境对action的反应。根据这个model包含的信息不同,可以分为distribution model(分布模型)和sample model(采样模型)。两者的区别如下:

(1)给定一个开始状态(state)和动作(action),sample model会产生一个可能的概率;distribution model会根据发生的概率产生所有可能的转移。

(2)给定一个开始状态(state)和策略(policy),sample model只会产生一个episode,distribution  model可以获得所有可能的episode及其概率。

distribution model比sample model包含更多信息,但现实中往往更容易获得sample model。model是对环境的一种表达方式,可以用来产生仿真经验(simulation experience)。

从model中生成或提升Policy的计算过程称为Planning

Planning有两种,一种是state-space planning,另一种是plan-space planning。前者根据simulation experience来计算value function,然后通过计算values function 进行 Policy提升。过程如下:

Planning和Learning(Planning方法有DP等,而Learning方法有MC、TD等)方法的核心都是用回溯方法(backup)更新公式计算value function的估计值。Planning所使用的experience是由模型生成的simulated exprience,而Learning method使用的经验是由真实环境生成的real experience。

Planning和Learning都满足上述state space Planning结构。存在通用结构,意味着许多思想和算法可以在规划和学习之间迁移。比如Learning中Value function估计值的更新公式可以替换Planning中的Value function估计值的更新公式。就像可以将Q - Learning和Planning结合,得到随机采样单步表格型Q-planning(Random-sample one-step tabular Q-planning)

Dyna:集成在一起的Planning、Action和Learning

Value Based、Policy Based、Model Based

Value Based学习Value来指导Policy,Policy Based学习策略来收获最大Value,还有将两者融合的AC。但是在学习Value或Policy都很困难的情况下(如:围棋),学习模型可能更好。

什么是模型

可以理解是一个环境状态S与反应A之间的映射集。如果可以学习到这个模型在给定状态后最有可能做出的反应,那么任务就完成了。

怎么学习模型

通过类似监督学习的方法获得模型,通过模型,强化学习算法不再局限于考虑如何最大化奖励,而是通过理解采取的动作,从而具有一定的推理能力。但是模型毕竟是模拟的,会有所偏差。

直接强化学习和间接强化学习

强化学习是基于交互的学习。交互无非是为了获得经验数据,所以强化学习也可以说是基于经验的学习方法。经验有两种,分别是实际经验(real experience)和仿真经验(simulation experience),前者是真实的agent与环境交互得到的数据。比如机械臂从一堆物品中抓取物品,无人车在道路上行走。它们这类交互可以直接用来学习Value function或者Policy。称之为直接强化学习(direct RL);后者由模型(model)产生,模型可以通过real experience来学习(获得)模型,这个过程叫做模型学习(model learning),当然也可以不用学习,直接根据物理定律也能确定。利用仿真经验来辅助策略学习的方法叫做间接强化学习(indirect RL)。 

Dyna-Q

由于model不可能完全拟合环境,所以纯粹model-based的强化学习效果往往很差。所以就有Dyna的算法框架了。这是种将model-based和model-free方法结合起来的算法框架。它即在model中学习,也在交互中(与真实的环境)学习。在Dyna框架的每个迭代轮次中,会先和环境交互,得到Real-experience之后,用于更新价值函数和策略函数。接着进行n次model的模拟预测,然后继续更新价值函数和策略函数。

如下表格型Dyna-Q算法

(1)先初始化状态s和其任意动作a的状态价值Q(s,a)

(2)初始化Model(s,a)。表示基于“状态-动作”二元组(s,a)预测的后继状态和收益的内容。

(3)对于每一次迭代,先根据当前状态和Q(S,A)\epsilon-greedy方式得到新状态S'和奖励R

(4)用Q-Learning更新Q(S,A)

(5)用RS'更新模型Model(s,a),(与真实环境交互之后,进行n次模拟)。

(6)每次模拟都随机选择一个之前出现过的状态S,并在此基础上随机选择一个动作A

(7)基于模型Model(S,A)得到S'R

(8)再使用Q-Learning更新价值函数:Q(S,A)=Q(S,A)+\alpha[R+\gamma max_{a}Q(S_t,a)-Q(S,A)]

Dyna 迷宫

如上图所示,在47(54个格子减去7个障碍格子)个状态中,每一个状态都有四种动作:上、下、左、右,除非agent的移动被障碍或者是迷宫的边缘阻挡,否则它肯定会走到它周围的某个格子(状态)。所有的转移收益都为0,除了目标状态G时+1。到达目标后,agent返回到开始状态(S)以开始新的一幕。

import numpy as np
import os
import matplotlib.pyplot as plt
from time import sleep


StateMemory = []
ActionMemory = {}

class Environment:
	#初始化棋盘环境
    def __init__(self):
		#共有54个格子,6行9列
        self.states = [State(i) for i in range(54)]
		#以下格子处为障碍物 -start
        self.states[7].accessible = False
        self.states[11].accessible = False
        self.states[16].accessible = False
        self.states[20].accessible = False
        self.states[25].accessible = False
        self.states[29].accessible = False
        self.states[41].accessible = False
		#以下格子处为障碍物 -end
		#终点位置:8,到达终点时,收益为1
        self.states[8].reward = 1
		#玩家初始位置
        self.player_pos = 18
        self.done = False
        self.accessible_states = [state.id for state in self.states if state.accessible == True]

	#重置环境
    def reset(self):
        self.player_pos = 18
        self.done = False
        return self.player_pos

	#玩家采取行动,返回行动后的位置,收益,以及是否到达终点
    def step(self, action):
        if action == 1: # up
			#如果行动前的位置在上边界
            if self.states[self.player_pos].id <= 8:
                return self.player_pos, self.states[self.player_pos].reward, self.done
            self.player_pos_change(self.player_pos, -9)
            self.check_done()
            return self.player_pos, self.states[self.player_pos].reward, self.done

        if action == 0: # left
			#如果行动前的位置在左边界
            if self.states[self.player_pos].id in [0, 9, 18, 27, 36, 45]:
                return self.player_pos, self.states[self.player_pos].reward, self.done
            self.player_pos_change(self.player_pos, -1)
            self.check_done()
            return self.player_pos, self.states[self.player_pos].reward, self.done

        if action == 2: # right
			#如果行动前的位置在右边界
            if self.states[self.player_pos].id in [8, 17, 26, 35, 44, 53]:
                return self.player_pos, self.states[self.player_pos].reward, self.done
            self.player_pos_change(self.player_pos, 1)
            self.check_done()
            return self.player_pos, self.states[self.player_pos].reward, self.done

        if action == 3: # down
			#如果行动前的位置在下边界
            if self.states[self.player_pos].id >= 45:
                return self.player_pos, self.states[self.player_pos].reward, self.done
            self.player_pos_change(self.player_pos, 9)
            self.check_done()
            return self.player_pos, self.states[self.player_pos].reward, self.done
    #判断是否到了终点
    def check_done(self):
        if self.player_pos == 8:
            self.done = True

	#修改玩家当前位置
    def player_pos_change(self, pos, value):
        if pos + value in self.accessible_states:
            self.player_pos += value

    def render(self):
        row1 = ['-', '-', '-', '-', '-', '-', '-', 'X', 'G']
        row2 = ['-', '-', 'X', '-', '-', '-', '-', 'X', '-']
        row3 = ['S', '-', 'X', '-', '-', '-', '-', 'X', '-']
        row4 = ['-', '-', 'X', '-', '-', '-', '-', '-', '-']
        row5 = ['-', '-', '-', '-', '-', 'X', '-', '-', '-']
        row6 = ['-', '-', '-', '-', '-', '-', '-', '-', '-']
        rows = [row1, row2, row3, row4, row5, row6]
        loc = self.player_pos
        row_num = loc//9
        col_num = loc%9
        rows[row_num][col_num] = 'o'
        print(rows[0])
        print(rows[1])
        print(rows[2])
        print(rows[3])
        print(rows[4])
        print(rows[5])

    def clear(self):
        os.system("clear")

#每个格子(状态)的收益,是否是障碍
class State:
    def __init__(self, id):
        self.id = id
        self.reward = -0.01
        self.accessible = True

#智能体
class Agent:

    def __init__(self, env):
        self.env = env
        self.Q = {}
        self.model = {}
        self.env.accessible_states.pop(8)
		#为所有格子(障碍物除外)初始化Q值和Model
        for s in self.env.accessible_states:
            self.Q[s] = []
            self.model[s] = []
            for a in range(4):
                self.Q[s] += [np.random.random()]
                self.model[s] += [np.random.random()]

	#开始训练
    def train(self, episode_nums, env, alpha, gamma, eval_epochs, render=True):
        total_reward = 0
        episode_num = 0
        running_average = []
        while episode_num < episode_nums:
            s = env.player_pos
            a = self.sample_action(s)
            p_s = s
            StateMemory.append(s)
            if s not in ActionMemory:
                ActionMemory[s] = []
            ActionMemory[s] += [a]
            s, r, done = env.step(a)
            if render == True:
                env.clear()
                env.render()
                print("Cumulative Reward this episode: %.2f"%total_reward)
            else:
                print("Please wait, training is in progess.")
                env.clear()
            total_reward += r
            self.Q[p_s][a] += alpha * (r + (gamma * np.max(self.Q[s])) - self.Q[p_s][a])
            self.model[p_s][a] = (r, s)
            if done:
                s = env.reset()
                env.clear()
                episode_num += 1
                running_average.append(total_reward)
                total_reward = 0
            for n in range(eval_epochs):
                s1 = np.random.choice(StateMemory)
                a1 = np.random.choice(ActionMemory[s1])
                r1, s_p1 = self.model[s1][a1]
                self.Q[s1][a1] += alpha * (r1 + (gamma * np.max(self.Q[s_p1])) - self.Q[s1][a1])
        return running_average


	#epsilon greedy. epsilon=0.1
    def sample_action(self, s):
        if np.random.random() < 0.1:
            return np.random.choice([0, 1, 2, 3])
        return np.argmax(self.Q[s])

    def print_policy(self):
        best_actions = {}
		#遍历所有的可走的格子
        for s in self.env.accessible_states:
			#根据Q值来选择最佳策略
            a = np.argmax(self.Q[s])
            if a == 1:
                a = '^'
            if a == 0:
                a = '<'
            if a == 2:
                a = '>'
            if a == 3:
                a = 'v'
            best_actions[s] = a
        self.env.clear()
        print("----------------BEST POLICY----------------")
        row1 = ['-', '-', '-', '-', '-', '-', '-', 'X', 'G']
        row2 = ['-', '-', 'X', '-', '-', '-', '-', 'X', '-']
        row3 = ['S', '-', 'X', '-', '-', '-', '-', 'X', '-']
        row4 = ['-', '-', 'X', '-', '-', '-', '-', '-', '-']
        row5 = ['-', '-', '-', '-', '-', 'X', '-', '-', '-']
        row6 = ['-', '-', '-', '-', '-', '-', '-', '-', '-']
        rows = [row1, row2, row3, row4, row5, row6]
        for s in self.env.accessible_states:
            row_num = s//9
            col_num = s%9
            rows[row_num][col_num] = best_actions[s]
        rows[0][8] = 'G'
        print(rows[0])
        print(rows[1])
        print(rows[2])
        print(rows[3])
        print(rows[4])
        print(rows[5])
        print("-------------------------------------------")



def play_human(env):
    s = env.reset()
    done = False
    total_reward = 0
    env.render()
    while not done:
        a = input("Enter action: ")
        if a == 'w':
            a = 1
        if a == 'a':
            a = 0
        if a == 's':
            a = 3
        if a == 'd':
            a = 2
        s, r, done = env.step(a)
        env.clear()
        env.render()
        total_reward += r
    print("Total reward attained is: ", total_reward)

env = Environment()
agent1 = Agent(env)
agent2 = Agent(env)
agent3 = Agent(env)
agent4 = Agent(env)
running_average1 = agent1.train(50, env, 0.1, 0.95, 0)
StateMemory = []
ActionMemory = {}
running_average2 = agent2.train(50, env, 0.1, 0.95, 5)
StateMemory = []
ActionMemory = {}
running_average3 = agent3.train(50, env, 0.1, 0.95, 50)
StateMemory = []
ActionMemory = {}
running_average4 = agent4.train(50, env, 0.1, 0.95, 100)
agent1.print_policy()
plt.plot(running_average1, label="Planning 0 steps")
plt.plot(running_average2, label="Planning 5 steps")
plt.plot(running_average3, label="Planning 50 steps")
plt.plot(running_average4, label="Planning 100 steps")
plt.legend()
plt.title("Running Average")
plt.show()

如果Model是错误的

由环境得到的模型有可能是错误的,原因有可能如下:

(1)在随机环境中,很难观测到所有的样本。

(2)Value function的泛化能力比较差。

(3)环境是动态改变的,但是新的特性没有被观察到。

这也可以看做是exploration与exploitation的矛盾之一。在规划问题中,exploration尝试改善模型的动作。而exploitation选择当前模型下最优的动作。前者能发现环境中的变化,但是会降低平均性能。虽然不存在完美使用的方案,但是简单的启发搜索(heuristic)常常很有效。

Dyna-Q+算法使用了启发算法来解决这个问题。这个算法对每一个“状态-动作”二元组进行跟踪。记录了它(指的是二元组)自上一次与环境进行真实交互时出现以来,过了多少时刻。对于越长时间没有访问的“状态-动作”,就表示它的转移有可能改变也就是模型在此处错误的概率就越大。

为了鼓励测试长期未出现过的动作,一个“额外收益”将会提供给agent。特别是如果模型对单步转移的收益是r,而这个转移在\tau时刻内没有出现,那么在更新时就会采用r+\kappa \sqrt{\tau}的收益。这会鼓励agent不断explore所有可访问的状态转移,甚至使用一长串的动作来完成。虽然这些都有代价,但是许多情况下,这种“计算上的好奇心”是非常值得付出额外代价的。

blocking maze问题

如上图所示,左边从起点到终点的最短路径是从屏障右边绕过。经过1000次学习后,环境发生了变化,变成了从左边才能绕过的路径。学习过程的累计回报的变化如下:

从上图可以看出,Dyna-Q和Dyna-Q+算法都在1000步内找到了最短路径。当环境改变后,曲线变平,表明在这段agent没有得到收益 ,因为它们在找到左边的缺口前,一直都在屏障后面。而后它们依然还是能学习到新的最优动作。

shortcut maze问题

如上图所示,期初最优路径绕过屏障的左侧。然后经过3000步的学习之后,右侧出现了一条捷径,肉眼上看,右边新出现的路可以更快到达目标,但是Dyna-Q不会切换到捷径。事实上,它没有意识到新路径的出现,即使使用了\epsilon-greedy,agent也不大可能采取足够大量的explore动作来发现捷径。

Prioritized Sweeping

在Dyna中,agent是等概率从以往访问过的“状态-动作”对中选择下一个产生simulation experience的起始状态。但是如果重点关注某些状态转移和更新会更好。因为有时状态空间的数量很大,如果不能集中经历更新那些重要的状态可能就会非常低效。

从终点目标状态进行反向工作可能使得搜索范围更为集中。一般来说,我们不仅仅希望可以从目标状态反向计算,而且还要从能使得价值发生变化的任何状态进行反向计算。

 迷宫问题中的Prioritized Sweeping问题

Prioritized Sweeping能够显著提高在迷宫任务中发现最优解决方案的速度,通常能快5-10倍。如下图所示:

优先遍历只是分配计算量以提高规划效率的一种方式。这可能不是最好的方法。优先遍历的局限之一是它采用了期望更新,这在随机环境中可能会浪费大量计算来处理低概率的转移。

Expected Update Vs.Sample Update

这里介绍规划学习框架中涉及的一些核心组件。对于one-step的值函数更新大体可以从三维维度去划分,分别是:

(1)更新状态值还是动作值(v还是q)。

(2)直接估计最优策略的值v_*,还是给定策略的值v_\pi

(3)采用Expected Update还是Sample Update。

所谓Expected Update就是考虑所有可能发生的action,Sample Update就是只考虑一种可能性。根据上面说的3个维度,共有8(4x2)种组合。有7个对应的算法。如下图所示:

one-step的Sample Update(采样更新)是为了解决分布模型未知的情况。采样必然会带来采样误差。但是虽然Expected Update(期望更新)能避免这个问题,但是它需要更多的资源。

假设现在需要处理一个表格型问题。用Q表示值函数,\hat{p}(s' , r|s, a)表示环境模型的估计,则“状态-动作”二元组的期望更新是:

与之对应的采样更新则是一种类似Q学习的更新。给定后继状态S'和收益R(来自模型)的采样更新是:

如果是随机环境,可能会有很多可能的后续状态,此时期望更新花费的时间和计算量会更多。如果用b表示分支因子(branch factor,也就是\hat{p}(s' , r|s, a)>0s'的个数)。则期望更新需要的计算量大约是采样更新的b

如果有足够的时间来完成期望更新,则所得到的估计总体上应该比b次采样更新更好,因为没有采样误差。

如上图所示,横轴表示更新的次数,纵轴表示不同更新次数估计的Q值的误差。灰色线条表示期望更新,彩色线条表示不同分支数对应的采样更新。因为一次期望更新需要b次计算,所以可以看到在1b的位置,期望更新的误差断崖式锐减,说明期望更新完成了。对于采样更新,误差时缓慢递减的,对于比较大的b,采样更新很早就获得了比较小的误差。另外因为一个状态对的更新同时又依赖于后续状态的估计,所以越早的得到一个更好的估计值,将越有利。

Trajectory Sampling

基于动态规划的经典方法会遍历整个(state,action)空间,对待全部状态都赋予了相同重要程度,每遍历一次就对每个(state,action)的价值更新一次。但其实很多状态出现的概率很低,没有必要花费太多计算资源去更新这些状态。

一种更有效的方式为:根据当前策略的概率分布进行采样操作。这样的采样方法为Trajectory Sampling。以下是Trajectory Sampling和Uniform Sampling的效率对比实验。实验中统一使用expected updates:

(1)Uniform Sampling,指的是从整个空间随机采样,并进行更新操作。

(2)Trajectory Sampling,生成模拟序列(其中state和reward由model确定,action由策略确定),然后对模拟序列出现的每个(state,action)进行更新。

(1)Trajectory Sampling在初始阶段速度很快,但后期效果一般。

(2)b越小,Trajectory Sampling速度优势越明显。

(3)状态总数越大,Trajectory Sampling速度优势越明显。

初期,Trajectory Sampling更集中于对那些和初始态相关度进行更新,所以效率更高,但到了后期,所有对应的Q(s)已经都较为准确,无需再进行有倾向性的采样,但是Trajectory Sampling仍去频繁地对他们采样,所以效率较低。

RTDP

实时动态规划(real-time dynamic programming,RTDP),是动态规划价值迭代算法的on-policy trajectory sampling版本。他是异步动态规划(asynchronous DP),异步动态规划不像常规的DP那样对状态空间进行彻底性的扫描,而是任意规定一种顺序来扫描。

实施动态规划方法允许跳过一些与问题目标无关的状态,同样也能收敛到最优策略。常规的DP算法如果想要取得最优策略,需要对每种状态都要更新无穷次,才能确保收敛。但是,对于一些满足特定条件的情况和问题,RTDP在忽略一些状态的情况下,仍然能够以概率1收敛到最优策略:

1、想要达到的目标状态的初始值为0。

2、至少存在一个策略,使得任意初始状态都一概率1达到目标状态。

3、所有从非目标状态出发和转移过程中得到的reward,都严格小于0。

4、所有初始value都要大于等于最优情况下的value(显然将初值全部设为0即可满足)。

用上述性质的任务,称为随机最优路径问题(stochastic optimal path problem),主要指那些需要将代价最小化的问题,而不是使收益最大化。

RTDP高度关注与问题相关的状态子控件,其效率较高,与普通的全扫描迭代相比,时间要少50%。

Planning at Decision Time

规划(Planning)至少有两种运行方式。主要是以动态规划和Dyna为代表的方法。它们从环境模型生成模拟经验,以此为基础采用规划来逐步改进策略或价值函数。动作的选择是从表格中比较当前状态下的动作价值的(表格型的情况),还有一种是通过近似方法中的数学表达式进行评估。

在为当前状态S_t进行选择之前,规划过程都会预先针对多个状态的动作选择所需要的表格条目(表格型方法)或数学表达式(近似方法)进行改善。在这种情况下,规划并不仅仅需要关注当前状态,还有预先处理其他的多个状态。这种规划方式成为后台规划(background planning)

还有一种规划方式是使用模拟经验数据,结合已知信息,直接选出当前状态下的最优策略。这种方式叫做决策时规划(Planning at Decision Time)。它的优点在于,它没有太注重综合性的信息。一些状态出现一次后要很长时间才能再出现一次,此时decision-time planning就能较好地处理这种情况。

Decision-time planning适合那些不太需要快速反应的应用。比如在下棋程序中,每次走棋都可以允许数秒甚至数分钟的计算。另外低延迟动作选择优先,则在后台进行规划通常能够更好地计算出一个策略。

Heuristic Search

启发式搜索在访问到一个状态时,会建立一棵树,代表后续可能出现的情况,然后利用树中的信息进行更新。树的层数越多,每一次更新的计算量就越大,但是层数越多,生成的结果就更好。所以需要寻找一个合适的平衡点。

 Rollout Algorithms

Rollout 算法其实就是将decision-time planning整合进Monte Carlo方法的算法。

Rollout算法对当前状态,从各个action出发,根据策略分别进行采样,通过样本数据来进行计算和更新。

(1)在MC算法中,采样是为了逐步使信息更准确,进而更准确地改善策略。

(2)Rollout中,采样是采出每一步之后的一定信息,利用信息更新后,然后做出选择让这一步进入下一个状态。

Monte Carlo Tree Search

蒙特卡洛树搜索(MCTS)是对上一节Rollout算法的进一步拓展,他会在基础上记录搜索过程中的行动值变化,以便更好地采样。它的核心思想是:设法重点关注从当前状态出发后有高估值的路径。其步骤为:

(1)选择。从根节点开始,使用基于树边缘的动作价值的树策略遍历这棵树来挑选一个叶子节点。

(2)扩展。在某些循环中(根据应用的细节决定),针对选定的叶子节点找到采取非试探性动作可以到达的节点,将一个或多个这样的节点加为该叶子节点的子节点,以此来实现树的扩展。

(3)模拟。从选定的节点,或其中一个它新增加的子节点(如果存在)出发,根据预演策略选择动作进行整个episode的trajectory模拟。得到的结果是一个MC实验,其中动作首先由树策略选取,而到了树外则由预演策略选取。

(4)回溯。模拟整个episode的trajectory的回报值向上回传,对在这次MCTS循环中,树策略所遍历的树边缘上的动作价值进行更新或初始化。预演策略在树外部访问到的状态和动作的任何值都不会被保存下来。

 总结

关于描述环境的模型

(1)规划需要有一个描述环境的模型。概率分布模型由后继状态的概率和可能动作的收益组成。

(2)采样模型根据概率生成单个状态转移以及相应的收益。

(3)进行动态规划需要概率分布模型,因为它要做期望更新。这需要计算所有可能的后继状态和收益的期望。

(4)为了模拟与环境交互的过程,需要一个采样模型,用到采样更新。

关于规划和学习

(1)规划最优行为和学习最优行为都需要估计同样的价值函数。

(2)可以通过一长串的简单回溯操作就可以增量式地更新估计值。

(3)可以通过简单地将算法应用于simulation experience而不是real experience,就可以将学习转化成规划。

(4)只需要让规划和学习都更新同一个价值函数,就可以将学习与规划整合在一起。

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值