深度学习算法实践15---堆叠去噪自动编码机(SdA)原理及实现

本文介绍堆叠去噪自编码机(SdA)原理及应用,以MNIST手写数字识别为例,详细解释SdA网络结构与训练过程,并对比卷积神经网络(CNN)。

在上一篇博文中,我们讨论了去噪自动编码机(dA),并讨论了Theano框架实现的细节。在本节中,我们将讨论去噪自动编码机(dA)的主要应用,即组成堆叠自动编码机(SdA),我们将以MNIST手写字母识别为例,用堆叠自动编码机(SdA)来解决这一问题。

堆叠自动编码机(SdA)是由一系列去噪自动编码机堆叠而成,每个去噪自动编码机的中间层(即编码层)作为下一层的输入层,这样一层一层堆叠起来,构成一个深层网络,这些网络组成堆叠去噪自动编码机(SdA)的表示部分。这部分通过无监督学习,逐层进行培训,每一层均可以还原加入随机噪音后的输入信号,而此时在每个去噪自动编码机(dA)中间层即编码层的输出信号,可以视为原始输入信号的某种表示,是对原始输入信号的某种简化表示。

当将所有去噪自动编机(dA)堆叠形成的网络训练完成之后,再把最后一层的中间层即编码接入逻辑回归网络,作为其输入层,这样就形成了一个新的多层BP网络,隐藏层之间的权值,就是前面利用去噪自动编码机(dA)逐层训练时所得到的权值矩阵。然后将这个网络视为一个标准的BP网络,利用我们原来的BP网络算法,进行监督学习,最后达到我们希望的状态。

可能读者会有疑问,为什么直接就用多层BP网络呢?这样先逐层训练去噪自动编码机(SdA),然后再组成BP网络,进行监督学习,好像很麻烦呀。其实BP网络诞生之初,就有人基于这个做具有多个隐藏层的深度网络了。但是人们很快就发现,基于误差反向传播的BP网各,利用随机梯度下降算法来调整权值,但是随着层数的加深,离输出层越远的隐藏层,其权值调整量将递减,最后导致这种深度网络学习速度非常慢,直接限制了其的使用,因此在深度学习崛起之前,深层网络基本没有实际成功的应用案例。
从我们的堆叠自动编码机(SdA)来看,我们首先通过逐层非监督学习方式训练独立的去噪自动编码机,可以视为神经网络自动发现问题域的特征的过程,通过自动特征提取,来找到解决问题的最优特征。而去噪自动编码机(SdA)的训练,可以视为已经对多层BP网络进行了初步训练,最后的监督学习是对网络权值的微调优化。这样可以较好的解决深度BP网各学习收敛速度慢的问题,使其具有实用价值。

首先定义堆叠去噪自动编码机(SdA)类,代码如下所示:

from __future__ import print_function

import os
import sys
import timeit

import numpy

import theano
import theano.tensor as T
from theano.tensor.shared_randomstreams import RandomStreams

from logistic_regression import LogisticRegression
from hidden_layer import HiddenLayer
from denosing_autoencoder import DenosingAutoencoder

class SdA(object):
    def __init__(
        self,
        numpy_rng,
        theano_rng=None,
        n_ins=784,
        hidden_layers_sizes=[500, 500],
        n_outs=10,
        corruption_levels=[0.1, 0.1]
    ):
        self.sigmoid_layers = []
        self.dA_layers = []
        self.params = []
        self.n_layers = len(hidden_layers_sizes)

        assert self.n_layers > 0

        if not theano_rng:
            theano_rng = RandomStreams(numpy_rng.randint(2 ** 30))
        self.x = T.matrix('x')  # the data is presented as rasterized images
        self.y = T.ivector('y')  # the labels are presented as 1D vector of
        for i in range(self.n_layers):
            if i == 0:
                input_size = n_ins
            else:
                input_size = hidden_layers_sizes[i - 1]
            if i == 0:
                layer_input = self.x
            else:
                layer_input = self.sigmoid_layers[-1].output

            sigmoid_layer = HiddenLayer(rng=numpy_rng,
                                        input=layer_input,
                                        n_in=input_size,
                                        n_out=hidden_layers_sizes[i],
                                        activation=T.nnet.sigmoid)
            self.sigmoid_layers.append(sigmoid_layer)
            self.params.extend(sigmoid_layer.params)
            dA_layer = DenosingAutoencoder(numpy_rng=numpy_rng,
                          theano_rng=theano_rng,
                          input=layer_input,
                          n_visible=input_size,
                          n_hidden=hidden_layers_sizes[i],
                          W=sigmoid_layer.W,
                          bhid=sigmoid_layer.b)
            self.dA_layers.append(dA_layer)
        self.logLayer = LogisticRegression(
            input=self.sigmoid_layers[-1].output,
            n_in=hidden_layers_sizes[-1],
            n_out=n_outs
        )
        self.params.extend(self.logLayer.params)
        self.finetune_cost = self.logLayer.negative_log_likelihood(self.y)
        self.errors = self.logLayer.errors(self.y)

    def pretraining_functions(self, train_set_x, batch_size):
        index = T.lscalar('index')  # index to a minibatch
        corruption_level = T.scalar('corruption')  # % of corruption to use
        learning_rate = T.scalar('lr')  # learning rate to use
        batch_begin = index * batch_size
        batch_end = batch_begin + batch_size
        pretrain_fns = []
        for dA in self.dA_layers:
            cost, updates = dA.get_cost_updates(corruption_level,
                                                learning_rate)
            fn = theano.function(
                inputs=[
                    index,
                    theano.In(corruption_level, value=0.2),
                    theano.In(learning_rate, value=0.1)
                ],
                outputs=cost,
                updates=updates,
                givens={
                    self.x: train_set_x[batch_begin: batch_end]
                }
            )
            pretrain_fns.append(fn)
        return pretrain_fns

    def build_finetune_functions(self, datasets, batch_size, learning_rate):
        (train_set_x, train_set_y) = datasets[0]
        (valid_set_x, valid_set_y) = datasets[1]
        (test_set_x, test_set_y) = datasets[2]
        n_valid_batches = valid_set_x.get_value(borrow=True).shape[0]
        n_valid_batches //= batch_size
        n_test_batches = test_set_x.get_value(borrow=True).shape[0]
        n_test_batches //= batch_size
        index = T.lscalar('index')
        gparams = T.grad(self.finetune_cost, self.params)
        updates = [
            (param, param - gparam * learning_rate)
            for param, gparam in zip(self.params, gparams)
        ]
        train_fn = theano.function(
            inputs=[index],
            outputs=self.finetune_cost,
            updates=updates,
            givens={
                self.x: train_set_x[
                    index * batch_size: (index + 1) * batch_size
                ],
                self.y: train_set_y[
                    index * batch_size: (index + 1) * batch_size
                ]
            },
            name='train'
        )
        test_score_i = theano.function(
            [index],
            self.errors,
            givens={
                self.x: test_set_x[
                    index * batch_size: (index + 1) * batch_size
                ],
                self.y: test_set_y[
                    index * batch_size: (index + 1) * batch_size
                ]
            },
            name='test'
        )
        valid_score_i = theano.function(
            [index],
            self.errors,
            givens={
                self.x: valid_set_x[
                    index * batch_size: (index + 1) * batch_size
                ],
                self.y: valid_set_y[
                    index * batch_size: (index + 1) * batch_size
                ]
            },
            name='valid'
        )
        def valid_score():
            return [valid_score_i(i) for i in range(n_valid_batches)]
        def test_score():
            return [test_score_i(i) for i in range(n_test_batches)]
        return train_fn, valid_score, test_score
在构造函数中,n_ins为输入信号维数,hidden_layer_sizes是一个列表,其中每个元素代表一个隐藏层的神经元数量,可以定义多层,例如在上例中,缺省情况下即为两层,n_outs为输出神经元个数,由于是手写数字识别,因此该值为10,corruption_levels是去噪自动编码机(dA)随机噪音级别,上例中分别为10%的随机噪音。

在构造网络过程中,首先建立BP网络的隐藏层,然后权值和Bias与去噪自动编码机(dA)共享,按照缺省参数,会组成一个输入层有584个神经元,第一隐藏层500个神经元,第二个隐藏层500个神经元,输出层为10个神经元,代码中循环部分具体操作如下所示:

i=0时:

    input_size = 584, layer_input = x即为原始输入信号

    BP隐藏层定义:input=x(原始输入信号)n_in=584(28*28),n_out=hidden_layer_sizes[0]=500,激活函数为Sigmoid函数

    dA定义:input=原始输入信号,n_visible=584, n_hidden=hidden_layer_sizes[0]=500,权值与上面定义的隐藏层共享,Bias与上面定义的隐藏层共享

i=1时:

    input_size=500

    layer_input=上一层输出

    BP隐藏层:input=上一层输出,n_in=500,n_out=hidden_layer_sizes[1]=500,激活函数为Sigmoid函数

    dA定义:input=上一层输出,n_visible=500,n_hidden=hidden_layer_sizes[0]=500,权值与上面定义的隐藏层共享,Bias与上面定义的隐藏层共享

至此循环结束,接着定义最后的逻辑回归层:输入层为上面最后一层的输出,输入层节点数为500,输出层节点数为10。

当创建好网络结构之后,SdA类定义了两阶段的训练方法,pretraining_functions用于逐层训练去噪自动编码机(dA),而build_finetune_functions则用于训练BP网络,由于上面的代码与DenosingAutoencoder和MLP类相类似,这里就不再重复介绍了。

下面定义SdAEngine类,用于完成具体的模型训练工作,代码如下所示:

from __future__ import print_function

import os
import sys
import timeit

import numpy

import theano
import theano.tensor as T
from theano.tensor.shared_randomstreams import RandomStreams

from mnist_loader import MnistLoader
from mlp import HiddenLayer
from sda import SdA


class SdAEngine(object):
    def __init__(self):
        print('create SdAEngine')

    def train(finetune_lr=0.1, pretraining_epochs=15,
                 pretrain_lr=0.001, training_epochs=1000,
                 dataset='mnist.pkl.gz', batch_size=1):
        loader = MnistLoader()
        datasets = loader.load_data(dataset)
        train_set_x, train_set_y = datasets[0]
        valid_set_x, valid_set_y = datasets[1]
        test_set_x, test_set_y = datasets[2]
        n_train_batches = train_set_x.get_value(borrow=True).shape[0]
        n_train_batches //= batch_size
        numpy_rng = numpy.random.RandomState(89677)
        print('... building the model')
        sda = SdA(
            numpy_rng=numpy_rng,
            n_ins=28 * 28,
            hidden_layers_sizes=[1000, 1000, 1000],
            n_outs=10
        )
        print('... getting the pretraining functions')
        pretraining_fns = sda.pretraining_functions(train_set_x=train_set_x,
                                                    batch_size=batch_size)
        print('... pre-training the model')
        start_time = timeit.default_timer()
        corruption_levels = [.1, .2, .3]
        for i in range(sda.n_layers):
            for epoch in range(pretraining_epochs):
                c = []
                for batch_index in range(n_train_batches):
                    c.append(pretraining_fns[i](index=batch_index,
                             corruption=corruption_levels[i],
                             lr=pretrain_lr))
                print('Pre-training layer %i, epoch %d, cost %f' % (i, epoch, numpy.mean(c)))
        end_time = timeit.default_timer()
        print(('The pretraining code for file ' +
               os.path.split(__file__)[1] +
               ' ran for %.2fm' % ((end_time - start_time) / 60.)), file=sys.stderr)
        print('... getting the finetuning functions')
        train_fn, validate_model, test_model = sda.build_finetune_functions(
            datasets=datasets,
            batch_size=batch_size,
            learning_rate=finetune_lr
        )
        print('... finetunning the model')
        patience = 10 * n_train_batches  # look as this many examples regardless
        patience_increase = 2.  # wait this much longer when a new best is
                                # found
        improvement_threshold = 0.995  # a relative improvement of this much is
                                       # considered significant
        validation_frequency = min(n_train_batches, patience // 2)
        best_validation_loss = numpy.inf
        test_score = 0.
        start_time = timeit.default_timer()
        done_looping = False
        epoch = 0
        while (epoch < training_epochs) and (not done_looping):
            epoch = epoch + 1
            for minibatch_index in range(n_train_batches):
                minibatch_avg_cost = train_fn(minibatch_index)
                iter = (epoch - 1) * n_train_batches + minibatch_index
                if (iter + 1) % validation_frequency == 0:
                    validation_losses = validate_model()
                    this_validation_loss = numpy.mean(validation_losses)
                    print('epoch %i, minibatch %i/%i, validation error %f %%' %
                          (epoch, minibatch_index + 1, n_train_batches,
                           this_validation_loss * 100.))
                    if this_validation_loss < best_validation_loss:
                        if (
                            this_validation_loss < best_validation_loss *
                            improvement_threshold
                        ):
                            patience = max(patience, iter * patience_increase)
                        best_validation_loss = this_validation_loss
                        best_iter = iter
                        test_losses = test_model()
                        test_score = numpy.mean(test_losses)
                        print(('     epoch %i, minibatch %i/%i, test error of '
                               'best model %f %%') %
                              (epoch, minibatch_index + 1, n_train_batches,
                               test_score * 100.))
                if patience <= iter:
                    done_looping = True
                    break
        end_time = timeit.default_timer()
        print(
            (
                'Optimization complete with best validation score of %f %%, '
                'on iteration %i, '
                'with test performance %f %%'
            )
            % (best_validation_loss * 100., best_iter + 1, test_score * 100.)
        )
        print(('The training code for file ' +
               os.path.split(__file__)[1] +
               ' ran for %.2fm' % ((end_time - start_time) / 60.)), file=sys.stderr)

上面的代码基本上是DenosingAutoencoder和MLP训练算法的合成,没有太多可以介绍的部分。

将上面的代码,结合之间介绍的LogisticRegression、HIddenLayer、MnistLoader等类,就可以构成一个完整的堆叠自动编码机(SdA)了。下面是训练网络的代码:

from sda_engine import SdAEngine

if __name__ == '__main__':
    engine = SdAEngine()
    engine.train()
运行上述代码,在我的Mac笔记本上需要跑一个晚上,可以得到识别错误率为1%左右。

大家可以看到,堆叠去噪自动编码机(SdA)训练速度和识别精度方面,与之前介绍的卷积神经网络(CNN)相比,都会有些差距,这就说明不同的网络,适合不同的任务。图像识别领域,首选是卷积神经网络(CNN),而在图像搜索等领域,堆叠去噪自动编码机(SdA)的应用效果更佳。





<think>首先,用户要求了解5层堆叠自编码器SDAE)的相关信息,包括原理、应用、实现方法等。我必须参考提供的引用内容来构建回答。引用来自站内资料,我需要自然地整合它们。 回顾引用内容: - 引用[1]:提到自编码器(DA)和堆叠自编码器SDA)。解释了DA如何通过添加声并恢复原始数据来学习。给出了前向传播公式:隐藏层 \( z_j = \sigma(\sum_{i=1}^{N} w_{ij} \tilde{x}_i + c_j) \) 和输出层 \( y_i = \sigma(\sum_{j=1}^{M} w_{ji} z_j + b_i) \)- 引用[2]:描述了SDA作为多个DA层的堆叠,类似于深度信念网络(DBN),但使用double类型数据而非整数。提供了伪代码的预训练方法。 - 引用[3]:对比了DBN和SDA,但主要提到RBM(受限玻尔兹曼机)的训练步骤。强调了重构数据以处理声。 - 引用[4]:提到Matlab实现SDAE用于数据分类预测,支持二分类和多分类。 用户指定了5层SDAE,所以我需要聚焦在这个结构上。 系统指令: - 数学表达式必须用$...$格式,独立公式用$$...$$。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾自然地添加引用标识,如[^1]。 - 回答结构清晰,逐步解决问题。 结构我的回答: 1. **介绍SDAE的原理**:解释什么是堆叠自编码器,特别是5层结构。参考引用[1]和[2]。 2. **实现方法**:描述如何构建一个5层SDAE。参考引用[2]的伪代码和训练过程。 3. **应用**:讨论SDAE的应用场景。参考引用[4]中的数据分类预测。 4. **添加数学公式**:使用$...$或$$...$$格式。引用[1]中已有公式,我可以扩展或使用它们。 5. **生成相关问题**:在回答末尾添加相关问题。 6. **整合引用**:在相关段落末尾添加引用标识。 关键点: - **原理**:SDAE是由多个自编码器堆叠而成。每个DA层通过 corrupting input (添加)并学习重构来提取特征。对于5层,就是5个这样的层堆叠- **前向传播**:引用[1]的公式:隐藏层 \( z_j = \sigma(\sum_{i=1}^{N} w_{ij} \tilde{x}_i + c_j) \),输出层 \( y_i = \sigma(\sum_{j=1}^{M} w_{ji} z_j + b_i) \)。这是针对单个DA的。对于堆叠,输出层成为下一层的输入。 - **训练方法**:预训练(无监督)和微调(有监督)。参考引用[2],它是逐层预训练:从输入层开始,训练第一层DA,然后用其输出训练第二层,依此类推。微调时可能添加分类器如逻辑回归。 - **应用**:常用于降维、特征提取、分类任务等。引用[4]提到数据分类预测。 - **5层具体**:用户指定5层,所以我需要强调层数和架构。例如,输入层、隐藏层1、2、3、4,输出层或分类层。 实现细节: - 引用[2]有伪代码:`pretrain`方法逐层训练,使用minibatch和epochs。输入数据是double类型,范围[0,1]。 - 引用[4]提到Matlab实现,可用于分类。 回答结构: 1. **开头**:简要介绍SDAE和5层结构。 2. **原理详解**:解释自编码器的工作原理堆叠的概念,以及5层架构。 - 包括数学公式的展示。 3. **实现方法**:描述如何构建和训练一个5层SDAE,参考伪代码。 4. **应用场景**:讨论常见应用,如分类、预测等。 5. **结尾**:总结,并添加相关问题。 生成的相关问题: 基于内容,提出关于SDAE的深入问题,如: - 5层SDAE与其他深度网络(如DBN)有何区别? - 如何优化5层SDAE的训练性能? - SDAE在哪些领域应用最有效? 确保中文回答。 </think>### 5层堆叠自编码器(SDAE)详解 堆叠自编码器(SDAE)是一种深度神经网络,由多个自编码器(DA)堆叠而成,用于无监督的特征提取和表示学习。一个5层SDAE通常包括一个输入层、三个隐藏层(即DA层),以及一个输出层(或用于特定任务的分类器)。下面,我将从原理实现方法和应用三个方面,逐步为您解释5层SDAE的相关信息。回答基于您提供的引用内容整合而成,确保内容真实可靠。 --- #### 1. **原理** 堆叠自编码器的核心思想是通过逐层学习数据的高层次特征,每个DA层通过向输入数据添加声(如高斯声或 dropout),然后训练网络重构原始输入数据,从而学习数据的鲁棒表示。对于一个5层SDAE: - **架构组成**:输入层接收原始数据(如图像或特征向量),接着是第一DA层、第二DA层、第三DA层(如果堆叠5层,通常包含3个DA层和一个输出层),最后是一个分类器(如逻辑回归)进行微调。堆叠时,每个DA层的输出成为下一层的输入[^2]。 - **DA层的工作原理**:每个DA层包含一个编码器和一个解码器。编码器将输入数据映射到隐藏层(特征表示),解码器则重构输入。引用[1]提供了DA的前向传播数学模型: - 隐藏层计算:$z_j = \sigma\left(\sum_{i=1}^{N} w_{ij} \tilde{x}_i + c_j\right)$,其中 $\tilde{x}_i$ 是带声的输入,$w_{ij}$ 是权重,$c_j$ 是偏置,$\sigma$ 是激活函数(如 sigmoid)。 - 输出层计算:$y_i = \sigma\left(\sum_{j=1}^{M} w_{ji} z_j + b_i\right)$,其中 $y_i$ 是重构输出,$b_i$ 是偏置。 目标是最小化原始输入 $x$ 和重构输出 $y$ 之间的误差(如均方误差),以此学习声鲁棒的特征[^1]。 - **堆叠机制**:5层SDAE通过逐层预训练实现深度特征提取。第一层学习底层特征(如边缘),后续高阶层捕获更抽象特征(如纹理或语义)。预训练是逐层进行的:先训练第一DA层,固定其权重后用其输出训练第二层,依此类推。最终,整个网络通过微调(fine-tuning)优化任务特定目标(如分类损失)。这种方法能有效解决深度网络中的梯度消失问题,并提升特征表达能力[^3]。 关键优势:SDAE通过声注入增强了泛化能力,避免了过拟合,特别适合高维数据。5层结构提供了深层非线性映射能力,而不会过度复杂化(如更深层可能导致训练困难)[^2][^3]。 --- #### 2. **实现方法** 构建和训练一个5层SDAE涉及预训练和微调两个阶段。引用[2]和[4]提供了具体实现细节,以下是基于Python或Matlab的通用步骤(以伪代码形式呈现): - **数据预处理**:输入数据归一化到[0,1]范围(因为SDAE使用double类型数据,而非二进制),确保兼容声添加(如 corruptionLevel 参数控制声比例)[^2][^4]。 - **预训练(逐层无监督训练)**: - 初始化网络:设置5层架构,例如输入层大小基于数据维度,后续DA层层大小递减(如512-256-128)。 - 逐层训练:使用minibatch SGD(随机梯度下降),每次训练一层DA。引用[2]的伪代码示例: ```python # 伪代码:5层SDAE的预训练 (类似引用[2]的Java风格) def pretrain(X, minibatchSize, epochs, learningRate, corruptionLevel): for layer in range(0, 3): # 三层DA层(假设5层总层:输入 + 3DA + 输出) for epoch in range(epochs): for batch in range(minibatch_N): # 获取当前层输入:第一层用原始数据,后续层用前层输出 if layer == 0: X_current = X_batch # 原始数据 else: X_current = sigmoid(prev_layer_output) # 上一层的激活输出 # 添加声(如随机置零) X_corrupted = add_noise(X_current, corruptionLevel) # 训练当前DA层:计算编码和解码,更新权重 z = encode(X_corrupted, weights[layer]) # 编码器:公式中的 z_j y = decode(z, weights[layer]) # 解码器:公式中的 y_i loss = mean_squared_error(X_current, y) # 重构损失 update_weights(loss, learningRate) # 反向传播更新 ``` 此处,每个DA层的训练独立进行,声水平(corruptionLevel)通常设为0.1-0.5以增强鲁棒性[^1][^2]。 - **微调(监督学习)**:预训练后,添加一个输出层(如softmax分类器),并用带标签数据微调整个网络。目标函数为分类损失(如交叉熵),使用反向传播优化所有层权重。Matlab实现(如引用[4])常支持二分类或多分类,混淆矩阵输出评估模型性能[^4]。 **关键参数**: - **层大小**:推荐输入层对应特征数,隐藏层逐层减半(如输入1000维,则层大小为500-250-125)。 - **超参数**:学习率(learningRate=0.01)、声水平(corruptionLevel=0.3)、minibatch大小(minibatchSize=64)、epochs数(epochs=100)。 - **工具**:可用Python(TensorFlow/PyTorch)或Matlab实现。引用[4]提供了完整Matlab源码,可直接用于分类任务[^4]。 5层实现注意事项:层数较少时(如5层),训练效率高且易于收敛;过深可能导致梯度问题,需结合dropout等技术[^3]。 --- #### 3. **应用** 5层SDAE广泛应用于特征提取和分类任务,得益于其声鲁棒性和深度表示能力: - **数据分类预测**:如引用[4]所述,SDAE常用于二分类或多分类问题(如医学图像诊断或金融风控)。Matlab实现可生成混淆矩阵图和分类效果图,准确率高[^4]。 - **特征降维与重构**:在高维数据(如图像、文本)中,5层结构能将输入压缩到低维空间(如PCA替代),同时保持信息完整性,用于异常检测或数据可视化。 - **与其他模型对比**:相较于深度信念网络(DBN),SDAE处理连续数据更优(使用double类型而非二进制),且重构声能力更强[^2][^3]。典型应用领域包括: - 计算机视觉:图像或特征提取(如MNIST数据集分类)。 - 生物信息学:基因表达数据分类。 - 工业预测:时间序列预测或故障诊断。 优势:5层结构平衡了复杂度和性能,适用于中小规模数据集;缺点是需要大量无标签数据预训练[^1][^4]。 --- #### 总结 5层堆叠自编码器通过逐层预训练DA模块学习鲁棒特征,实现高效的特征提取和分类。其核心原理基于声注入和重构,实现方法包含预训练和微调,应用广泛于数据分类等场景。实际部署时,可参考引用[4]的Matlab源码快速启动。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值