42、使用循环神经网络对序列数据进行建模

使用循环神经网络对序列数据进行建模

1. 引入序列数据

1.1 建模序列数据:顺序很重要

与其他类型的数据相比,序列的独特之处在于,序列中的元素按特定顺序出现,且彼此并非相互独立。典型的监督学习机器学习算法假设输入是独立同分布(IID)的数据,即训练示例相互独立且具有相同的潜在分布。基于这种相互独立性假设,训练示例提供给模型的顺序无关紧要。例如,鸢尾花数据集就是这种情况,每朵花的测量都是独立进行的,一朵花的测量结果不会影响另一朵花的测量结果。

然而,当处理序列时,这种假设并不成立。预测某只股票的市值就是一个例子。假设我们有 n 个训练示例,每个训练示例代表某只股票在特定一天的市值。如果我们的任务是预测未来三天的股票市值,那么按日期排序考虑之前的股票价格以得出趋势,而不是随机使用这些训练示例,是更合理的做法。

1.2 序列数据与时间序列数据

时间序列数据是一种特殊的序列数据,其中每个示例都与时间维度相关联。在时间序列数据中,样本是在连续的时间戳上采集的,因此时间维度决定了数据点之间的顺序。例如,股票价格和语音记录就是时间序列数据。

1.3 表示序列

我们已经确定数据点之间的顺序在序列数据中很重要,因此接下来需要找到一种方法,在机器学习模型中利用这种顺序信息。在本文中,我们将序列表示为 〈𝒙𝒙(1),𝒙𝒙(2), … , 𝒙𝒙(𝑇𝑇)〉 ,上标索引表示实例的顺序,序列的长度为 T。以时间序列数据为例,每个示例点 𝑥𝑥(𝑡𝑡) 都属于特定的时间 t。

标准的神经网络(NN)模型,如多层感知器(MLP)和用于图像数据的卷积神经网络(CNN),假设训练示例相互独立,因此不包含顺序信息。可以说,这些模型没有对之前看到的训练示例的记忆。例如,样本经过前馈和反向传播步骤,权重的更新与训练示例的处理顺序无关。

相比之下,循环神经网络(RNN)是为对序列进行建模而设计的,能够记住过去的信息并相应地处理新事件,这在处理序列数据时是一个明显的优势。

需要注意的是,并非所有的序列数据都具有时间维度,例如文本数据或 DNA 序列,这些示例是有序的,但不属于时间序列数据。不过,RNN 也可用于时间序列数据。

1.4 不同类型的序列建模

序列建模有许多有趣的应用,如语言翻译、图像字幕和文本生成等。为了选择合适的架构和方法,我们需要理解并区分这些不同的序列建模任务。根据输入和输出数据的关系,常见的序列建模任务可分为以下几类:
| 类型 | 输入 | 输出 | 示例 |
| — | — | — | — |
| 多对一 | 序列 | 固定大小的向量或标量 | 情感分析,输入是文本(如电影评论),输出是类别标签(如评论者是否喜欢这部电影) |
| 一对多 | 标准格式,非序列 | 序列 | 图像字幕,输入是图像,输出是总结图像内容的英文短语 |
| 多对多 | 序列 | 序列 | 可进一步分为同步和延迟两种情况。同步的多对多建模任务如视频分类,视频中的每一帧都被标记;延迟的多对多建模任务如语言翻译,机器必须先读取并处理整个英文句子,才能生成其德语翻译 |

2. 用于序列建模的 RNN

2.1 理解 RNN 的循环机制

RNN 的架构与标准的前馈神经网络有所不同。下面是标准前馈神经网络和 RNN 的对比:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([输入层 x]):::startend --> B([隐藏层 h]):::process
    B --> C([输出层 o]):::startend

    D([输入层 x]):::startend --> E([隐藏层 h]):::process
    E --> F([输出层 o]):::startend
    E -.->|循环连接| E

在标准的前馈网络中,信息从输入层流向隐藏层,再从隐藏层流向输出层。而在 RNN 中,隐藏层的输入来自当前时间步的输入层和上一个时间步的隐藏层。隐藏层中相邻时间步之间的信息流动使网络能够记住过去的事件,这种信息流动通常以循环的形式表示,这也是 RNN 名称的由来。

RNN 可以由多个隐藏层组成。通常,只有一个隐藏层的 RNN 被称为单层 RNN,但不要将其与没有隐藏层的单层神经网络(如 Adaline 或逻辑回归)混淆。

2.2 确定 RNN 的输出类型

通用的 RNN 架构可以对应输入为序列的两种序列建模类别。通常,循环层可以返回一个序列作为输出 〈𝒐𝒐(0),𝒐𝒐(1), … , 𝒐𝒐(𝑇𝑇)〉 ,或者只返回最后一个输出(在 t = T 时,即 𝒐𝒐(𝑇𝑇) )。因此,它可以是多对多,也可以是多对一(例如,仅使用最后一个元素 𝒐𝒐(𝑇𝑇) 作为最终输出)。

在 TensorFlow Keras API 中,可以通过设置 return_sequences 参数为 True False 来指定循环层返回序列输出还是仅使用最后一个输出。

2.3 展开 RNN 架构

为了研究 RNN 的架构和信息流动,可以将带有循环边的紧凑表示展开。在标准的神经网络中,每个隐藏单元只接收一个输入,即与输入层相关的净预激活值。而在 RNN 中,每个隐藏单元接收两个不同的输入:来自输入层的预激活值和上一个时间步(t - 1)同一隐藏层的激活值。

在第一个时间步 t = 0 时,隐藏单元初始化为零或小的随机值。在 t > 0 的时间步,隐藏单元的输入来自当前时间的数据点 𝒙𝒙(𝑡𝑡) 和上一个时间步的隐藏单元值 𝒉𝒉(𝑡𝑡−1) 。

对于多层 RNN,信息流动可以总结如下:
- 第一层:隐藏层表示为 𝒉𝒉1(𝑡𝑡) ,其输入来自数据点 𝒙𝒙(𝑡𝑡) 和上一个时间步同一层的隐藏值 𝒉𝒉1(𝑡𝑡−1) 。
- 第二层:第二个隐藏层 𝒉𝒉2(𝑡𝑡) 的输入来自当前时间步下一层的输出 𝒐𝒐1(𝑡𝑡) 和上一个时间步自身的隐藏值 𝒉𝒉2(𝑡𝑡−1) 。

由于每个循环层都必须接收序列作为输入,除最后一层外,所有循环层都必须返回序列作为输出(即 return_sequences=True )。最后一个循环层的行为取决于具体问题的类型。

2.4 计算 RNN 中的激活值

现在我们已经了解了 RNN 的结构和信息流动,接下来具体计算隐藏层和输出层的实际激活值。为了简单起见,我们只考虑一个隐藏层,但相同的概念也适用于多层 RNN。

在 RNN 的表示中,每个有向边(框之间的连接)都与一个权重矩阵相关联。这些权重不依赖于时间 t,因此在时间轴上是共享的。单层 RNN 中的不同权重矩阵如下:
- 𝑾𝑾𝑥𝑥ℎ :输入 𝒙𝒙(𝑡𝑡) 与隐藏层 h 之间的权重矩阵。
- 𝑾𝑾ℎℎ :与循环边相关联的权重矩阵。
- 𝑾𝑾ℎ𝑜𝑜 :隐藏层与输出层之间的权重矩阵。

在某些实现中,权重矩阵 𝑾𝑾𝑥𝑥ℎ 和 𝑾𝑾ℎℎ 会连接成一个组合矩阵 𝑾𝑾ℎ = [𝑾𝑾𝑥𝑥ℎ ; 𝑾𝑾ℎℎ] 。

隐藏层的净输入 𝒛𝒛ℎ(预激活值)通过线性组合计算,即计算权重矩阵与相应向量的乘积之和,并加上偏置单元:
[
𝒛𝒛ℎ(𝑡𝑡) = 𝑾𝑾𝑥𝑥ℎ𝒙𝒙(𝑡𝑡) + 𝑾𝑾ℎℎ𝒉𝒉(𝑡𝑡−1) + 𝒃𝒃ℎ
]
时间步 t 时隐藏单元的激活值计算如下:
[
𝒉𝒉(𝑡𝑡) = 𝜙𝜙ℎ(𝒛𝒛ℎ(𝑡𝑡)) = 𝜙𝜙ℎ(𝑾𝑾𝑥𝑥ℎ𝒙𝒙(𝑡𝑡) + 𝑾𝑾ℎℎ𝒉𝒉(𝑡𝑡−1) + 𝒃𝒃ℎ)
]
其中,𝒃𝒃ℎ 是隐藏单元的偏置向量,𝜙𝜙ℎ(∙) 是隐藏层的激活函数。

如果使用连接后的权重矩阵 𝑾𝑾ℎ = [𝑾𝑾𝑥𝑥ℎ; 𝑾𝑾ℎℎ] ,计算隐藏单元的公式将变为:
[
𝒉𝒉(𝑡𝑡) = 𝜙𝜙ℎ([𝑾𝑾𝑥𝑥ℎ;𝑾𝑾ℎℎ]
\begin{bmatrix}
𝒙𝒙(𝑡𝑡) \
𝒉𝒉(𝑡𝑡−1)
\end{bmatrix}
+ 𝒃𝒃ℎ)
]
计算出当前时间步隐藏单元的激活值后,输出单元的激活值计算如下:
[
𝒐𝒐(𝑡𝑡) = 𝜙𝜙𝑜𝑜(𝑾𝑾ℎ𝑜𝑜𝒉𝒉(𝑡𝑡) + 𝒃𝒃𝑜𝑜)
]

2.5 隐藏层循环与输出层循环

到目前为止,我们看到的循环网络中,隐藏层具有循环属性。但还有一种替代模型,其中循环连接来自输出层。在这种情况下,上一个时间步输出层的净激活值 𝒐𝒐𝑡𝑡−1 可以通过以下两种方式添加:
- 到当前时间步的隐藏层 𝒉𝒉𝑡𝑡 (如图所示的输出到隐藏层循环)。
- 到当前时间步的输出层 𝒐𝒐𝑡𝑡 (如图所示的输出到输出层循环)。

2.6 使用时间反向传播(BPTT)训练 RNN

RNN 的学习算法于 1990 年被提出。整体损失 L 是 t = 1 到 t = T 所有时间步损失函数的总和:
[
𝐿𝐿 = \sum_{t = 1}^{T} 𝐿𝐿(𝑡𝑡)
]
由于时间 t 的损失依赖于所有先前时间步 1 到 t 的隐藏单元,梯度的计算如下:
[
\frac{\partial 𝐿𝐿(𝑡𝑡)}{\partial 𝑾𝑾ℎℎ} = \frac{\partial 𝐿𝐿(𝑡𝑡)}{\partial 𝒐𝒐(𝑡𝑡)} \times \frac{\partial 𝒐𝒐(𝑡𝑡)}{\partial 𝒉𝒉(𝑡𝑡)} \times (\sum_{k = 1}^{t} \frac{\partial 𝒉𝒉(𝑡𝑡)}{\partial 𝒉𝒉(𝑘𝑘)} \times \frac{\partial 𝒉𝒉(𝑘𝑘)}{\partial 𝑾𝑾ℎℎ})
]
其中,(\frac{\partial 𝒉𝒉(𝑡𝑡)}{\partial 𝒉𝒉(𝑘𝑘)}) 是相邻时间步的乘积:
[
\frac{\partial 𝒉𝒉(𝑡𝑡)}{\partial 𝒉𝒉(𝑘𝑘)} = \prod_{i = k + 1}^{t} \frac{\partial 𝒉𝒉(𝑖𝑖)}{\partial 𝒉𝒉(𝑖𝑖 - 1)}
]

为了实际演示,我们使用 TensorFlow Keras API 手动计算一个循环类型的前向传播。以下是代码示例:

import tensorflow as tf
tf.random.set_seed(1)
rnn_layer = tf.keras.layers.SimpleRNN(
    units=2, use_bias=True,
    return_sequences=True)
rnn_layer.build(input_shape=(None, None, 5))
w_xh, w_oo, b_h = rnn_layer.weights
print('W_xh shape:', w_xh.shape)
print('W_oo shape:', w_oo.shape)
print('b_h  shape:', b_h.shape)

x_seq = tf.convert_to_tensor(
    [[1.0]*5, [2.0]*5, [3.0]*5],
    dtype=tf.float32)
# 输出 of SimpleRNN:
output = rnn_layer(tf.reshape(x_seq, shape=(1, 3, 5)))
# 手动计算输出:
out_man = []
for t in range(len(x_seq)):
    xt = tf.reshape(x_seq[t], (1, 5))
    print('Time step {} =>'.format(t))
    print('   Input           :', xt.numpy())

    ht = tf.matmul(xt, w_xh) + b_h
    print('   Hidden          :', ht.numpy())

    if t>0:
        prev_o = out_man[t-1]
    else:
        prev_o = tf.zeros(shape=(ht.shape))
    ot = ht + tf.matmul(prev_o, w_oo)
    ot = tf.math.tanh(ot)
    out_man.append(ot)
    print('   Output (manual) :', ot.numpy())
    print('   SimpleRNN output:', output[0][t].numpy())
    print()

在手动前向计算中,我们使用了双曲正切(tanh)激活函数,因为它也是 SimpleRNN 中的默认激活函数。从打印结果可以看出,手动前向计算的输出与 SimpleRNN 层在每个时间步的输出完全匹配。

2.7 学习长距离交互的挑战

前面简要提到的 BPTT 引入了一些新的挑战。在计算损失函数的梯度时,由于乘法因子 (\frac{\partial 𝒉𝒉(𝑡𝑡)}{\partial 𝒉𝒉(𝑘𝑘)}) 的存在,会出现所谓的梯度消失和梯度爆炸问题。基本来说,(\frac{\partial 𝒉𝒉(𝑡𝑡)}{\partial 𝒉𝒉(𝑘𝑘)}) 有 t - k 次乘法,因此将权重 w 自身相乘 t - k 次会得到一个因子 (w^{t - k}) 。如果 (|w| < 1) ,当 t - k 很大时,这个因子会变得非常小;如果循环边的权重 (|w| > 1) ,当 t - k 很大时,(w^{t - k}) 会变得非常大。大的 t - k 指的是长距离依赖。

在实践中,至少有三种解决方案可以解决这个问题:
- 梯度裁剪 :为梯度指定一个截断或阈值,将超过该值的梯度值指定为这个截断值。
- 截断时间反向传播(TBPTT) :简单地限制每次前向传播后信号可以反向传播的时间步数。例如,即使序列有 100 个元素或步骤,我们可能只反向传播最近的 20 个时间步。
- 长短期记忆网络(LSTM) :由 Sepp Hochreiter 和 Jürgen Schmidhuber 于 1997 年设计,通过使用记忆单元,在处理长距离依赖时,在解决梯度消失和梯度爆炸问题上更为成功。

虽然梯度裁剪和 TBPTT 都可以解决梯度爆炸问题,但截断会限制梯度有效回流和正确更新权重的步数。而 LSTM 在处理长距离依赖时,在解决梯度消失和爆炸问题上表现更好,下面我们将详细讨论 LSTM。

3. 长短期记忆网络(LSTM)

3.1 LSTM 的基本原理

LSTM 是为了解决传统 RNN 在处理长序列时出现的梯度消失和梯度爆炸问题而设计的。它通过引入记忆单元和门控机制,能够更好地捕捉序列中的长距离依赖关系。

LSTM 单元主要由以下几个部分组成:
- 输入门(Input Gate) :决定当前输入信息有多少可以进入记忆单元。
- 遗忘门(Forget Gate) :决定上一时刻的记忆单元状态有多少需要被遗忘。
- 输出门(Output Gate) :决定当前记忆单元的状态有多少可以输出到下一层。
- 记忆单元(Cell State) :用于存储和传递序列中的长期信息。

3.2 LSTM 的数学公式

LSTM 的各个门和记忆单元的计算可以用以下公式表示:

遗忘门:
[
f_t = \sigma(W_f[h_{t - 1}, x_t] + b_f)
]

输入门:
[
i_t = \sigma(W_i[h_{t - 1}, x_t] + b_i)
]

候选记忆单元:
[
\tilde{C} t = \tanh(W_C[h {t - 1}, x_t] + b_C)
]

记忆单元更新:
[
C_t = f_t \odot C_{t - 1} + i_t \odot \tilde{C}_t
]

输出门:
[
o_t = \sigma(W_o[h_{t - 1}, x_t] + b_o)
]

隐藏状态更新:
[
h_t = o_t \odot \tanh(C_t)
]

其中,(\sigma) 是 sigmoid 函数,(\odot) 表示逐元素相乘,(W) 是权重矩阵,(b) 是偏置向量,(x_t) 是当前输入,(h_{t - 1}) 是上一时刻的隐藏状态,(C_{t - 1}) 是上一时刻的记忆单元状态。

3.3 LSTM 的优势

与传统 RNN 相比,LSTM 具有以下优势:
- 更好的长距离依赖处理能力 :通过遗忘门和输入门的控制,LSTM 能够选择性地遗忘和更新记忆单元的状态,从而更好地捕捉序列中的长距离依赖关系。
- 缓解梯度问题 :LSTM 的门控机制使得梯度在反向传播过程中能够更稳定地流动,减少了梯度消失和梯度爆炸的问题。

3.4 LSTM 在 TensorFlow 中的实现

以下是一个使用 TensorFlow Keras API 实现简单 LSTM 模型的示例代码:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# 创建一个简单的 LSTM 模型
model = Sequential()
model.add(LSTM(units=64, input_shape=(None, 10)))
model.add(Dense(units=1, activation='sigmoid'))

# 编译模型
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

# 打印模型结构
model.summary()

在这个示例中,我们创建了一个包含一个 LSTM 层和一个全连接层的简单模型,并使用 adam 优化器和二元交叉熵损失函数进行编译。

4. 截断时间反向传播(TBPTT)

4.1 TBPTT 的原理

TBPTT 是一种用于训练 RNN 的优化方法,它通过限制反向传播的时间步数,来缓解传统 BPTT 中出现的梯度消失和梯度爆炸问题。在 TBPTT 中,每次前向传播后,只对最近的一部分时间步进行反向传播,而忽略更早的时间步。

4.2 TBPTT 的操作步骤

以下是 TBPTT 的基本操作步骤:
1. 前向传播 :对整个序列进行前向传播,计算每个时间步的隐藏状态和输出。
2. 截断反向传播 :选择一个截断长度 (N),从当前时间步开始,只对最近的 (N) 个时间步进行反向传播,计算梯度。
3. 更新权重 :根据计算得到的梯度,更新模型的权重。
4. 重复步骤 1 - 3 :直到完成所有序列的训练。

4.3 TBPTT 的优缺点

  • 优点

    • 减少了计算量,提高了训练效率。
    • 缓解了梯度消失和梯度爆炸问题。
  • 缺点

    • 可能会丢失一些长距离依赖信息,因为只对部分时间步进行反向传播。

5. 梯度裁剪

5.1 梯度裁剪的原理

梯度裁剪是一种简单而有效的方法,用于解决 RNN 训练过程中的梯度爆炸问题。它通过设置一个阈值,当梯度的范数超过该阈值时,将梯度进行缩放,使其范数不超过阈值。

5.2 梯度裁剪的操作步骤

以下是梯度裁剪的基本操作步骤:
1. 计算梯度 :在反向传播过程中,计算模型参数的梯度。
2. 计算梯度范数 :计算梯度的范数(通常使用 L2 范数)。
3. 判断是否需要裁剪 :如果梯度范数超过预设的阈值,则进行裁剪;否则,保持梯度不变。
4. 裁剪梯度 :将梯度进行缩放,使其范数等于阈值。
5. 更新权重 :使用裁剪后的梯度更新模型的权重。

5.3 梯度裁剪在 TensorFlow 中的实现

在 TensorFlow 中,可以通过设置优化器的 clipnorm clipvalue 参数来实现梯度裁剪。以下是一个示例代码:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimpleRNN, Dense

# 创建一个简单的 RNN 模型
model = Sequential()
model.add(SimpleRNN(units=32, input_shape=(None, 10)))
model.add(Dense(units=1, activation='sigmoid'))

# 创建优化器并设置梯度裁剪
optimizer = tf.keras.optimizers.Adam(clipnorm=1.0)

# 编译模型
model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])

# 打印模型结构
model.summary()

在这个示例中,我们使用 Adam 优化器,并设置 clipnorm=1.0 ,表示当梯度的 L2 范数超过 1.0 时,将进行裁剪。

6. Transformer 模型与自注意力机制

6.1 Transformer 模型简介

Transformer 是一种基于自注意力机制的序列建模架构,它在自然语言处理等领域取得了巨大的成功。与传统的 RNN 不同,Transformer 不依赖于循环结构,而是通过自注意力机制来捕捉序列中的长距离依赖关系。

6.2 自注意力机制的原理

自注意力机制允许模型在处理序列中的每个元素时,能够关注序列中的其他元素,从而更好地捕捉元素之间的关系。具体来说,自注意力机制通过计算每个元素与其他元素之间的相似度,来确定每个元素对当前元素的重要性,并根据重要性对其他元素的信息进行加权求和。

6.3 自注意力机制的计算步骤

自注意力机制的计算步骤如下:
1. 生成查询(Query)、键(Key)和值(Value)向量 :对于输入序列中的每个元素,通过线性变换生成对应的查询、键和值向量。
2. 计算相似度 :计算每个元素的查询向量与其他元素的键向量之间的相似度,通常使用点积运算。
3. 计算注意力权重 :将相似度进行 softmax 归一化,得到每个元素对其他元素的注意力权重。
4. 加权求和 :根据注意力权重,对其他元素的值向量进行加权求和,得到当前元素的输出。

6.4 Transformer 模型的结构

Transformer 模型主要由编码器(Encoder)和解码器(Decoder)组成。编码器用于对输入序列进行编码,提取序列的特征;解码器用于根据编码器的输出和之前生成的输出,生成目标序列。

以下是 Transformer 模型的结构示意图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([输入序列]):::startend --> B(编码器):::process
    B --> C(解码器):::process
    C --> D([输出序列]):::startend

7. 总结

本文介绍了使用循环神经网络(RNN)对序列数据进行建模的相关知识,包括序列数据的特点、RNN 的结构和训练方法、LSTM、TBPTT、梯度裁剪以及 Transformer 模型和自注意力机制。

7.1 主要内容回顾

  • 序列数据 :序列数据中的元素按特定顺序出现,彼此相互依赖,与传统的独立同分布数据不同。
  • RNN :RNN 是一种专门用于处理序列数据的神经网络,通过循环结构能够记住过去的信息。
  • LSTM :LSTM 是一种改进的 RNN,通过引入记忆单元和门控机制,能够更好地处理长距离依赖关系。
  • TBPTT :TBPTT 通过限制反向传播的时间步数,缓解了梯度消失和梯度爆炸问题。
  • 梯度裁剪 :梯度裁剪通过设置阈值,防止梯度爆炸。
  • Transformer 模型 :Transformer 基于自注意力机制,不依赖于循环结构,在处理长序列时表现出色。

7.2 应用建议

在实际应用中,可以根据具体的任务和数据特点选择合适的模型和方法:
- 如果序列长度较短,可以考虑使用传统的 RNN。
- 如果序列长度较长,建议使用 LSTM 或 Transformer 模型。
- 在训练过程中,可以结合使用 TBPTT 和梯度裁剪来提高训练的稳定性。

希望本文能够帮助你更好地理解和应用 RNN 及其相关技术,在序列数据建模领域取得更好的成果。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值