【深度学习论文翻译】基于LSTM深度神经网络的时间序列预测(Time Series Prediction Using LSTM Deep Neural Networks)

目录

一、前言

二、摘要

三、什么是LSTM神经元?

四、简单正弦波示例

五、 不那么简单的股票市场

六、多维LSTM预测

七、结论

一、前言
最近需要用到时间序列,在网上也找到了一篇相关的文章及源代码,在此分享给大家。

1.文章原文:https://www.altumintelligence.com/articles/a/Time-Series-Prediction-Using-LSTM-Deep-Neural-Networks

2.源码网址:https://github.com/jaungiers/LSTM-Neural-Network-for-Time-Series-Prediction

3.本文中涉及到一个概念叫超参数,这里有有关超参数的介绍(https://blog.csdn.net/shuiyixin/article/details/88996743)

4.运行代码可能会报错:Using TensorFlow backend.解决方案点击:https://blog.csdn.net/shuiyixin/article/details/88928354

如果你还有什么问题,希望跟我能够一起交流,除了通过博客交流外,欢迎你加入我的QQ群,一起交流有关于机器学习、深度学习、计算机视觉有关内容。目前我并未确定具体的研究方向,所以现在 处于广泛涉猎阶段,希望我们能够一起沟通。下图是我的群二维码:

为了方便交流学习,我写论文翻译都会将论文原文中的某段放到论文中,方便大家与译文对应。有些内容如果不是论文原文,我会用不同颜色标注,方便大家区分。

本篇文章是有关于时间序列预测的,该博客翻译内容介绍如下:

1.摘要:通过摘要,我们能够对该技术总体有个大致的了解,知道这篇文章做了什么,有哪些要求以及代码下载。

2.LSTM神经元:通过这一部分,我们能对LSTM神经元有一些基础了解但是该文章不是讲解LSTM原理,而是其应用,所以这部分只是简单介绍其基本概念以及为什么引入LSTM。

3.简单正弦波:通过一个正弦波序列来帮助大家理解LSTM网络。

4.股票市场:利用股票数据进行预测操作。

5.多维LSTM预测:通过不同维度对股票进行预测。

接下来就让我们一起了解一下这篇论文,跟随这篇论文一起走进LSTM的世界吧!

二、摘要


本文重点介绍如何使用深度LSTM神经网络架构提供多维时间序列预测,使用Keras和Tensorflow - 特别是在股票市场数据集上-提供股票价格的动量指标。

这个框架的代码可以在下面的GitHub repo中找到(我们假设你电脑上满足python版本3.5.x和requirements.txt文件中的需求版本。偏离这些版本可能会导致错误):https://github.com/jaungiers/LSTM-Neural-Network-for-Time-Series-Prediction

注:需求版本如下:

numpy==1.15.0

pandas==0.23.3

tensorflow-gpu==1.10.0

keras==2.2.2

matplotlib==2.2.2

以下文章部分将简要介绍LSTM神经元细胞,给出一个预测正弦波的虚拟示例,然后通过应用程序进入随机时间序列。本文假设了简单深度神经网络的基本工作知识。

三、什么是LSTM神经元?


长期困扰传统神经网络架构的基本问题之一是解释相互依赖的信息和上下文输入序列的能力。该信息可以是句子中的先前单词,以允许上下文预测下一个单词可能是什么,或者它可以是序列的时间信息,允许该序列的基于时间的元素的上下文。

简而言之,传统的神经网络每次都会采用独立的数据向量,并且在需要存储的任务中,没有内存概念来帮助他们。

解决这个问题的早期尝试是对网络中的神经元使用简单的反馈类型方法,其中输出被反馈到输入中,用以提供最后可见的输入的上下文。这些被称为递归神经网络(RNN)。虽然这些RNN在一定程度上起作用,但是它们有相当大的下降,它们的任何重大用途都会导致称为消失梯度问题的问题。我们不会进一步扩展消失梯度问题,而是说由于这个问题导致RNN不适合大多数现实问题,因此,需要找到另一种解决上下文存储的方法。

这是长期短期记忆(LSTM)神经网络拯救的地方。与RNN神经元一样,LSTM神经元在其管道中保持记忆的上下文,以允许解决顺序和时间问题,而不会消除影响其性能的消失梯度。

许多研究论文和文章都可以在网上找到,它们在很好的数学细节上讨论了LSTM神经元的工作原理。然而,在本文中,我们不会讨论LSTM的复杂工作原理,因为我们更关心它们对我们问题的使用。

对于上下文,下面是LSTM神经元的典型内部工作图。它由若干层和逐点操作组成,这些操作充当数据输入,输出和遗忘的门,为LSTM单元状态提供信息。这种单元状态是保持网络和输入的长期记忆和上下文的原因。

 

四、简单正弦波示例


为了演示LSTM神经网络在预测时间序列中的使用,让我们从我们能想到的最基本的事情开始:一个时间序列:可信的正弦波。让我们创建数据,我们将需要为这个函数建造许多震荡,以便LSTM网络进行训练。

代码数据文件夹中提供的数据包含我们创建的sinewave.csv文件,该文件包含5001个正弦波时间段,幅度和频率为1(角频率为6.28),时间差值为0.01。绘制时的结果如下所示:


数据集为正弦波
 

现在我们有了数据,我们实际上想要实现什么?好吧,我们只是希望LSTM从我们将提供的数据的设定窗口大小中学习正弦波,并且希望我们可以要求LSTM预测该系列中的后面N个步骤,并且它将继续输出正弦波。

我们将首先将CSV文件中的数据转换并加载到pandas数据帧中,然后将其用于输出将为LSTM提供数据的numpy数组。Keras LSTM层的工作方式是采用3维(N,W,F)的numpy数组,其中N是训练序列的数量,W是序列长度,F是每个序列的特征数。我们选择使用允许网络的序列长度(读取窗口大小)为50,可以看到每个序列的正弦波形状,因此希望自主学习建立一个基于先前接收到的窗口的序列模式。

序列本身是滑动窗口,因此每次移动1,导致与先前窗口的不断重叠。当绘制时,序列长度为50的典型训练窗口如下所示:


Sinewave数据集培训窗口


为了加载这些数据,我们在代码中创建了一个DataLoader类,以便为数据加载层提供抽象数据。您会注意到,在初始化DataLoader对象时,会传入文件名,以及确定用于训练与测试的数据百分比的拆分变量以及允许选择一列或多列数据的列变量用于单维或多维分析。

class DataLoader():
 
    def __init__(self, filename, split, cols):
        dataframe = pd.read_csv(filename)
        i_split = int(len(dataframe) * split)
        self.data_train = dataframe.get(cols).values[:i_split]
        self.data_test  = dataframe.get(cols).values[i_split:]
        self.len_train  = len(self.data_train)
        self.len_test   = len(self.data_test)
        self.len_train_windows = None
 
    def get_train_data(self, seq_len, normalise):
        data_x = []
        data_y = []
        for i in range(self.len_train - seq_len):
            x, y = self._next_window(i, seq_len, normalise)
            data_x.append(x)
            data_y.append(y)
        return np.array(data_x), np.array(data_y)
注:

1.上面的代码在core文件夹下的data_processor.py文件中。

2.只有上面的代码是会报错的,因为,Python不知道什么是pd,不知道什么是np。你需要在前面引入几个包:

import numpy as np
import pandas as pd


在我们有一个允许我们加载数据的数据对象之后,我们需要构建深度神经网络模型。同样,为了抽象化,我们的代码框架在config.json文件的旁边使用一个Model类,在给定所需的体系结构和存储在配置文件中的超参数的情况下,轻松构建模型的实例。构建我们网络主要功能的是build_model()函数,它接收解析的配置文件。

注:hyperparameters(超参数)介绍:https://blog.csdn.net/shuiyixin/article/details/88996743

此功能代码如下所示,并且可以轻松扩展,以便将来在更复杂的架构上使用。

class Model():
    def __init__(self):
        self.model = Sequential()
    def build_model(self, configs):
        timer = Timer()
        timer.start()
        for layer in configs['model']['layers']:
            neurons = layer['neurons'] if 'neurons' in layer else None
            dropout_rate = layer['rate'] if 'rate' in layer else None
            activation = layer['activation'] if 'activation' in layer else None
            return_seq = layer['return_seq'] if 'return_seq' in layer else None
            input_timesteps = layer['input_timesteps'] if 'input_timesteps' in layer else None
            input_dim = layer['input_dim'] if 'input_dim' in layer else None
            if layer['type'] == 'dense':
                self.model.add(Dense(neurons, activation=activation))
            if layer['type'] == 'lstm':
                self.model.add(LSTM(neurons, input_shape=(input_timesteps, input_dim), return_sequences=return_seq))
            if layer['type'] == 'dropout':
                self.model.add(Dropout(dropout_rate))
        self.model.compile(loss=configs['model']['loss'], optimizer=configs['model']['optimizer'])
        print('[Model] Model Compiled')
        timer.stop()
注:

1.上面的代码在core文件夹下的model.py文件中。

2.只有上面的代码是会报错的,你需要在前面引入几个包:

from core.utils import Timer
from keras.layers import Dense,  Dropout, LSTM
from keras.models import Sequential


加载数据并建立模型后,我们现在可以继续使用我们的训练数据训练模型。为此,我们创建了一个单独的运行模块,它将利用我们的Model.py和DataLoader.py抽象,将它们组合起来进行训练,输出和可视化。

下面是训练我们模型的一般运行线程代码。

configs = json.load(open('config.json', 'r'))
 
data = DataLoader(
    os.path.join('data', configs['data']['filename']),
    configs['data']['train_test_split'],
    configs['data']['columns']
)
 
model = Model()
model.build_model(configs)
x, y = data.get_train_data(
    seq_len = configs['data']['sequence_length'],
    normalise = configs['data']['normalise']
)
 
model.train(
    x,
    y,
    epochs = configs['training']['epochs'],
    batch_size = configs['training']['batch_size']
)
 
x_test, y_test = data.get_test_data(
    seq_len = configs['data']['sequence_length'],
    normalise = configs['data']['normalise']
)
注:

1.上面的代码在主文件夹下的run.py文件中,但是代码有一些不同,大家可以自己新建一个Python文件测试代码。

2.只有上面的代码是会报错的,你需要在前面引入几个包:

import os
import json  #json文件操作
from core.data_processor import DataLoader
from core.model import Model


对于输出,我们将运行两种类型的预测:第一种将以逐点方式进行预测,即我们每次仅预测单个点,将此点绘制为预测,然后沿着下一个窗口进行预测使用完整的测试数据并再次预测下一个点。

我们要做的第二个预测是预测一个完整的序列(全序列预测),我们只用训练数据的第一部分初始化一次训练窗口。然后模型预测下一个点,我们移动窗口,就像逐点方法一样。不同之处在于我们使用我们在先前预测中预测的数据来预测。在第二步中,这意味着只有一个数据点(最后一个点)来自先前的预测。在第三个预测中,最后两个数据点将来自先前的预测,依此类推。经过50次预测后,我们的模型将随后根据自己的先前预测进行预测。这使我们可以使用该模型预测未来的许多时间步骤,但正如它预测的那样,这些预测反过来又可以基于预测,这将增加我们预测的未来预测的错误率。

下面我们可以看到逐点预测和完整序列预测的代码和相应的输出。


正弦波逐点预测

正弦波全序列预测


作为参考,可以在下面的配置文件中看到用于正弦波示例的网络架构和超参数。

{
    "data": {
        "filename": "sinewave.csv",
        "columns": [
            "sinewave"
        ],
        "sequence_length": 50,
        "train_test_split": 0.8,
        "normalise": false
    },
    "training": {
        "epochs": 2,
        "batch_size": 32
    },
    "model": {
        "loss": "mse",
        "optimizer": "adam",
        "layers": [
            {
                "type": "lstm",
                "neurons": 50,
                "input_timesteps": 49,
                "input_dim": 1,
                "return_seq": true
            },
            {
                "type": "dropout",
                "rate": 0.05
            },
            {
                "type": "lstm",
                "neurons": 100,
                "return_seq": false
            },
            {
                "type": "dropout",
                "rate": 0.05
            },
            {
                "type": "dense",
                "neurons": 1,
                "activation": "linear"
            }
        ]
    }
}
注:因为要多次用到,建议大家新建一个json文件,专用与正弦波。

基于真实数据我们可以看到,只在1个时期和相当小的训练数据集中,LSTM深度神经网络已经在预测正弦函数方面做得非常好。

您可以看到,随着我们对未来越来越多的预测,误差幅度会随着先前预测中的误差在用于未来预测时被越来越多地放大而增加。因此,我们看到在完整序列示例中,预测值与真实值相差未来越远,我们预测预测的频率和幅度与真实数据相比就越不准确。然而,由于sin函数是一个非常简易的振荡函数,并且没有噪声,它仍然可以在不过度拟合的情况下很好地预测它 - 这很重要,因为我们可以通过增加时期和取出dropout层(防止过拟合)来轻松地过度拟合模型这个训练数据几乎完全准确,与测试数据的模式相同,但是,对于其他真实的世界的案例来说,如果模型与培训数据不一致,那么,试验就不会一般化。

在下一步中,我们将尝试在此类真实数据上使用该模型来查看效果。

 

五、 不那么简单的股票市场


我们在精确的逐点基础上预测了几百个正弦波的步长。因此,我们现在可以在股票市场时间序列中做同样的事情并立即获利,对吗?不幸的是,在现实世界中,这并不是那么简单。

与正弦波不同,股票市场时间序列不是可以映射的任何特定静态函数。描述股票市场时间序列运动的最佳属性是随机走向。作为随机过程,真正的随机走向没有可预测的模式,因此尝试对其进行建模将毫无意义。幸运的是,许多方面都在持续争论说股票市场不是一个纯粹的随机过程,这使我们能够理解时间序列可能具有某种隐藏模式。正是这些隐藏的模式,LSTM深度网络是预测的主要候选者。

此示例将使用的数据是data文件夹中的sp500.csv文件。此文件包含2000年1月至2018年9月标准普尔500股票指数的开盘价,最高价,最低价,收盘价以及每日交易量。

在第一个实例中,我们将仅使用Close price创建单维模型。调整config.json文件以反映新数据,我们将保持大部分参数相同。然而,需要做出的一个改变是,与仅具有介于-1到+1之间的数值范围的正弦波不同,收盘价是股票市场不断变化的绝对价格。这意味着如果我们试图在不对其进行标准化的情况下训练模型,它就永远不会收敛。

为了解决这个问题,我们将采用每个n大小的训练/测试数据窗口并对每个窗口进行标准化以反映从该窗口开始的百分比变化(因此点i = 0处的数据将始终为0)。我们将使用以下等式进行归一化,然后在预测过程结束时进行去标准化,以获得预测中的真实世界数:

n =价格变化的标准化列表[窗口] 

p =调整后的每日回报价格的原始列表[窗口]

我们已将normalise_windows()函数添加到DataLoader类以执行此转换,并且配置文件中包含布尔规范化标志,表示这些窗口的规范化。

def normalise_windows(self, window_data, single_window=False):
    '''Normalise window with a base value of zero'''
    normalised_data = []
    window_data = [window_data] if single_window else window_data
    for window in window_data:
        normalised_window = []
        for col_i in range(window.shape[1]):
            normalised_col = [((float(p) / float(window[0, col_i])) - 1) for p in window[:, col_i]]
            normalised_window.append(normalised_col)
                # reshape and transpose array back into original multidimensional format
        normalised_window = np.array(normalised_window).T                
        normalised_data.append(normalised_window)
    return np.array(normalised_data)
注:

1.上面的代码在core文件夹下的data_processor.py文件中。

2.只有上面的代码是会报错的,因为,Python不知道什么是pd,不知道什么是np。你需要在前面引入几个包:

import numpy as np
import pandas as pd


随着Windows规范化,我们现在可以运行模型,就像我们运行它来对抗正弦波数据一样。然而,我们在运行这些数据时做了一个重要的改变; 而不是使用我们框架的model.train()方法,而是使用我们创建的model.train_generator()方法。我们这样做是因为我们发现在尝试训练大型数据集时很容易耗尽内存,因为model.train()函数将完整数据集加载到内存中,然后将规范化应用于内存中的每个窗口,容易导致内存溢出。因此,我们使用了Keras的fit_generator()函数,允许使用python生成器动态训练数据集来绘制数据,这意味着内存利用率将大大降低。

configs = json.load(open('config.json', 'r'))
 
data = DataLoader(
    os.path.join('data', configs['data']['filename']),
    configs['data']['train_test_split'],
    configs['data']['columns']
)
 
model = Model()
model.build_model(configs)
x, y = data.get_train_data(
    seq_len = configs['data']['sequence_length'],
    normalise = configs['data']['normalise']
)
 
# out-of memory generative training
steps_per_epoch = math.ceil((data.len_train - configs['data']['sequence_length']) / configs['training']['batch_size'])
model.train_generator(
    data_gen = data.generate_train_batch(
        seq_len = configs['data']['sequence_length'],
        batch_size = configs['training']['batch_size'],
        normalise = configs['data']['normalise']
    ),
    epochs = configs['training']['epochs'],
    batch_size = configs['training']['batch_size'],
    steps_per_epoch = steps_per_epoch
)
 
x_test, y_test = data.get_test_data(
    seq_len = configs['data']['sequence_length'],
    normalise = configs['data']['normalise']
)
 
predictions_multiseq = model.predict_sequences_multiple(x_test, configs['data']['sequence_length'], configs['data']['sequence_length'])
predictions_fullseq = model.predict_sequence_full(x_test, configs['data']['sequence_length'])
predictions_pointbypoint = model.predict_point_by_point(x_test)        
 
plot_results_multiple(predictions_multiseq, y_test, configs['data']['sequence_length'])
plot_results(predictions_fullseq, y_test)
plot_results(predictions_pointbypoint, y_test)
注:

1.上面的代码在主文件夹下的run.py文件中,但是代码有一些不同,大家可以自己新建一个Python文件测试代码。

2.config.json文件内容如下:

{
    "data": {
        "filename": "sp500.csv",
        "columns": [
            "Close"
        ],
        "sequence_length": 50,
        "train_test_split": 0.85,
        "normalise": true
    },
    "training": {
        "epochs": 1,
        "batch_size": 32
    },
    "model": {
        "loss": "mse",
        "optimizer": "adam",
        "layers": [
            {
                "type": "lstm",
                "neurons": 100,
                "input_timesteps": 49,
                "input_dim": 1,
                "return_seq": true
            },
            {
                "type": "dropout",
                "rate": 0.2
            },
            {
                "type": "lstm",
                "neurons": 100,
                "return_seq": true
            },
            {
                "type": "lstm",
                "neurons": 100,
                "return_seq": false
            },
            {
                "type": "dropout",
                "rate": 0.2
            },
            {
                "type": "dense",
                "neurons": 1,
                "activation": "linear"
            }
        ]
    }
}


如上所述,在单个逐点预测上运行数据可以非常接近地匹配返回的内容。但这有点欺骗性。经过仔细检查,预测线由奇异的预测点组成,这些预测点在它们后面具有整个先前的真实历史窗口。因此,网络不需要了解时间序列本身,除了每个下一个点很可能不会离最后一点太远。因此,即使它得到错误点的预测,下一个预测也会考虑真实历史并忽略不正确的预测,但又会允许发生错误。

虽然这对于下一个价格点的精确预测最初可能听起来并不乐观,但它确实有一些重要的用途。虽然它不知道确切的下一个价格是多少,但它确实能够准确地表示下一个价格的范围。

此信息可用于波动率预测等应用程序(能够预测市场中高波动率或低波动率的时段对于特定交易策略可能非常有利),或者远离交易这也可以用作良好指标用于异常检测。异常检测可以通过预测下一个点,然后将其与真实数据进行比较来实现,如果真实数据值与预测点明显不同,则可以针对该数据点提出异常标记。


S&P500逐点预测


继续进行全序列预测,似乎这被证明是对这种类型的时间序列用处最小的预测(至少在这个超参数的训练模型上)。我们可以看到预测开始时的轻微碰撞,其中模型遵循某种类型的动量,但是很快我们可以看到模型确定最佳模式是收敛到时间序列的某个均衡。在这个阶段,这可能看起来并没有提供太多价值,但是平均回归交易者可能会在那里宣称该模型只是找到价格序列在波动率被消除时将恢复的平均值。


S&P500全序列预测


最后,我们对该模型进行了第三种预测,我将其称为多序列预测。这是完整序列预测的混合,因为它仍然使用测试数据初始化测试窗口,预测下一个点,然后使用下一个点创建一个新窗口。但是,一旦它到达输入窗口完全由过去预测组成的点,它就会停止,向前移动一个完整的窗口长度,用真实的测试数据重置窗口,然后再次启动该过程。实质上,这给出了对测试数据的多个趋势线预测,以便能够分析模型能够获得未来动量趋势的程度。


S&P500多序列预测


我们可以从多序列预测中看出,网络似乎正确地预测了绝大多数时间序列的趋势(和趋势幅度)。虽然不完美,但它确实表明了LSTM深度神经网络在顺序和时间序列问题中的有用性。通过仔细的超参数调整,肯定可以实现更高的准确性。

 

六、多维LSTM预测


到目前为止,我们的模型仅采用单维输入(在我们的S&P500数据集的情况下为“Close(收盘)”价格)。但是对于更复杂的数据集,序列自然存在许多不同的维度,可用于增强数据集,从而提高模型的准确性。

对于我们的S&P500数据集,我们可以看到我们有五个可能的维度,包括Open(开盘),High(最高),Low(最低),Close(收盘)和Volume(交易量)。我们开发的框架允许使用多维输入数据集,因此我们需要做的就是编辑列并适当地设置第一层input_dim值以运行我们的模型。在这种情况下,我将使用两个维度运行模型: “Close(收盘)”和“Volume(交易量)”。

为了演示LSTM神经网络在预测时间序列中的使用,让我们从我们能想到的最基本的事情开始:一个时间序列:可信的正弦波。让我们创建数据,我们将需要为这个函数建造许多震荡,以便LSTM网络进行训练。

{
    "data": {
        "filename": "sp500.csv",
        "columns": [
            "Close",
            "Volume"
        ],
        "sequence_length": 50,
        "train_test_split": 0.85,
        "normalise": true
    },
    "training": {
        "epochs": 1,
        "batch_size": 32
    },
    "model": {
        "loss": "mse",
        "optimizer": "adam",
        "layers": [
            {
                "type": "lstm",
                "neurons": 100,
                "input_timesteps": 49,
                "input_dim": 2,
                "return_seq": true
            },
            {
                "type": "dropout",
                "rate": 0.2
            },
            {
                "type": "lstm",
                "neurons": 100,
                "return_seq": true
            },
            {
                "type": "lstm",
                "neurons": 100,
                "return_seq": false
            },
            {
                "type": "dropout",
                "rate": 0.2
            },
            {
                "type": "dense",
                "neurons": 1,
                "activation": "linear"
            }
        ]
    }
}

使用“Close(收盘)”和“Volume(交易量)”的S&P500多维多序列预测


我们可以看到第二个“交易量”维度与“收盘”一起添加,输出预测变得更加细化。预测趋势线似乎更准确地预测即将到来的小幅下跌,不仅是从一开始的主流趋势,趋势线的准确性似乎也在这种特殊情况下得到改善。

 

七、结论


虽然本文的目的是在实践中给出LSTM深度神经网络的一个工作实例,但它只是触及了它们在顺序和时间问题中的潜力和应用的表面。

截至撰写本文时,LSTM已成功应用于众多现实问题,从此处所述的经典时间序列问题到文本自动纠正,异常检测和欺诈检测,以及正在开发的自动驾驶汽车技术的核心。

目前使用上述香草LSTM存在一些局限性,特别是在使用金融时间序列时,该系列本身具有很难建模的非平稳特性(尽管使用贝叶斯深度神经网络方法已经取得了进展)解决时间序列的非平稳性)。同样对于一些应用,还发现基于注意力的神经网络机制的新进展已经超过LSTM(并且LSTM与这些基于注意力的机制相结合已经超出了它们自身)。

然而,截至目前,LSTM在更经典的统计时间序列方法上提供了显着的进步,能够非线性地建模关系并且能够以非线性方式处理具有多个维度的数据。

我们开发的框架的完整源代码可以在以下GitHub页面上的MIT许可证下找到(我们要求信用证明显示为“Jakob Aungiers,Altum Intelligence ltd”,无论此代码在何处重复使用):https://github.com/jaungiers/LSTM-Neural-Network-for-Time-Series-Prediction
————————————————
版权声明:本文为CSDN博主「水亦心」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shuiyixin/article/details/88940952

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值