前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过支持向量回归和LSTM进行股票价格预测

通过支持向量回归和LSTM进行股票价格预测

作者头像
代码医生工作室
发布2019-09-23 17:59:41
3.2K0
发布2019-09-23 17:59:41
举报
文章被收录于专栏:相约机器人相约机器人

作者 | Scatterday

来源 | Medium

编辑 | 代码医生团队

人工智能(AI)无处不在。机器学习和人工智能正在彻底改变现代问题的解决方式。应用机器学习的一种很酷的方法是使用财务数据。财务数据是机器学习的一个游乐场。

在这个项目中,使用带有sci-kit-learn的支持向量回归和使用Keras的LSTM来分析特斯拉的股票价格。

在使用LSTM和其他算法等技术分析财务数据时,请务必记住这些不是保证结果。股票市场令人难以置信的不可预测且迅速变化。这只是一个有趣的项目,可以学习使用神经网络进行库存分析的一些基本技术。

目录:

1.获取我们的数据:

  • 进口
  • 获取库存数据
  • 修复我们的数据

2.可视化我们的数据:

  • 绘制我们的数据
  • 滚动的意思

3.支持向量回归:

  • 转换日期
  • 线性回归
  • 支持向量机
  • 支持向量回归演练
  • 使用sklearn和可视化内核的SVR代码

4.深度学习:

  • 规范化和准备神经网络的数据
  • 递归神经网络
  • LSTM演练
  • 退出
  • 我们模型的代码

5.结果:

  • 绘制模型损失
  • 做出预测
  • 结论
  • 资源

进口:

代码语言:javascript
复制
import keras
from keras.layers import Dense
from keras.layers import LSTM
from keras.layers import Dropout
import pandas as pd
import pandas_datareader.data as web
import datetime
import numpy as np
from matplotlib import style
 
# ignore warnings
import warnings
warnings.filterwarnings('ignore')

在这里导入:

  • Keras创建我们的神经网络
  • pandas和pandas_data读者可以获取和分析我们的库存数据
  • datetime用于修复数据分析的库存日期
  • numpy重塑我们的数据以提供给我们的神经网络
  • matplotlib用于绘制和可视化我们的数据
  • 警告忽略弹出的任何不需要的警告

获取库存数据:

代码语言:javascript
复制
# Get the stock data using yahoo API:
style.use('ggplot')
 
# get 2014-2018 data to train our model
start = datetime.datetime(2014,1,1)
end = datetime.datetime(2018,12,30)
df = web.DataReader("TSLA", 'yahoo', start, end)
 
# get 2019 data to test our model on
start = datetime.datetime(2019,1,1)
end = datetime.date.today()
test_df = web.DataReader("TSLA", 'yahoo', start, end)
  • 此代码将绘图样式更改为ggplot。将风格改为ggplot,因为更喜欢它的外观。在这里阅读更多关于ggplot的信息。

https://matplotlib.org/3.1.1/gallery/style_sheets/ggplot.html

  • 然后使用pandas_datareader作为'web'来使用DataReader函数获取股票价格数据,该函数获取财务数据并将其存储在pandas数据框中。
  • 从2014 - 2018年获得特斯拉股票数据来训练模型。
  • 从2019年到当天得到特斯拉股票数据,让模型做出预测。
  • “TSLA”是特斯拉的股票代码,指定“雅虎”以使用雅虎财务API获取数据。

修复数据:

代码语言:javascript
复制
# sort by date
df = df.sort_values('Date')
test_df = test_df.sort_values('Date')
 
# fix the date
df.reset_index(inplace=True)
df.set_index("Date", inplace=True)
test_df.reset_index(inplace=True)
test_df.set_index("Date", inplace=True)
 
df.tail()
  • 由于正在进行时间序列预测,因此希望数据是连续的。按日期对列车和测试数据进行排序。
  • 然后,重置索引并设置数据框的索引,以确保股票价格的日期是我们数据框中的一列。

绘制数据和滚动意味着:

代码语言:javascript
复制
# Visualize the training stock data:
import matplotlib.pyplot as plt
%matplotlib inline
 
plt.figure(figsize = (12,6))
plt.plot(df["Adj Close"])
plt.xlabel('Date',fontsize=15)
plt.ylabel('Adjusted Close Price',fontsize=15)
plt.show()
 
 
# Rolling mean
close_px = df['Adj Close']
mavg = close_px.rolling(window=100).mean()
 
plt.figure(figsize = (12,6))
close_px.plot(label='TSLA')
mavg.plot(label='mavg')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()

2014-2018特斯拉关闭股票价格

滚动平均值绘制在数据上

  • 从数据框中得到调整后的收盘价,在数据上绘制滚动均值。
  • 滚动平均值也称为移动平均值。移动平均线有助于平滑具有大量波动的数据,并帮助更好地了解数据的长期趋势。
  • 使用移动平均线,可以定义一段时间,想要取平均值称为窗口。将移动平均窗口定义为100.定义100,因为希望在数据中看到长期移动平均线。

数学:

  • 移动平均线的工作方式是将连续100天的价格相加并除以100得到均值。然后将窗口向右移动一个。因此降低第一个价格,并在最后添加新价格。
  • 考虑滚动意义的另一种方法是将其视为100个价格的数组。将所有元素相加并除以100得到平均值。然后删除元素,a[0]将另一个价格附加到数组的末尾。然后再次对所有元素求和,然后除以100得到下一个平均点。

转换日期:

代码语言:javascript
复制
import matplotlib.dates as mdates
 
# change the dates into ints for training
dates_df = df.copy()
dates_df = dates_df.reset_index()
 
# Store the original dates for plotting the predicitons
org_dates = dates_df['Date']
 
# convert to ints
dates_df['Date'] = dates_df['Date'].map(mdates.date2num)
 
dates_df.tail()
  • 在这里,创建了一个数据帧的副本,并将其命名为dates_df。将原始日期存储在org_dates中。稍后将使用org_dates来绘制预测和日期。
  • 然后,使用mdates.date2num将dates_df日期转换为整数。需要将日期作为整数,因为无法将日期提供给支持向量机和神经网络。

线性回归

  • 线性回归是一种在两个变量之间找到最佳线性关系或最佳拟合线的方法。
  • 给定一个因变量(x)的最佳拟合线,可以预测自变量(y)。
  • 线性回归的目标是找到最适合数据的线,这将导致预测的y与给出的已知y值接近。下面是线性回归方程的有用图像:
  • 还有一个有用的图像,线性回归看起来像图形:
  • 因此,回归试图通过最小化成本函数来学习数据的最佳A和B值。通常用于线性回归的成本函数是均方误差(MSE)。以下是MSE的等式:
  • 最小化此成本函数的方式是使用称为梯度下降的过程。
  • 因此在案例中,将尝试在日期和股票价格之间找到最佳匹配线。由于数据有如此多的波动,因此没有可用于线性回归的最佳拟合线,以便为库存预测提供良好的准确性。因此,在案例中,仅使用线性回归并不准确。具有线性关系的数据,例如基于房屋的大小来预测房价将是线性数据的示例。

支持向量机:

  • 支持向量机(SVM)用于分类。SVM的目标是在图形上定义2个类之间的边界线。可以将此视为以最佳方式“分割”数据。该边界线称为超平面。
  • SVM中的超平面在两个类之间具有“边距”或距离。构成边距的这两条线是从超平面到每个类中最接近的数据示例的距离。这些线称为边界线。
  • 在分割过程完成之后,SVM可以基于其在图上的位置来预测奇异数据点应属于哪个类。以下是帮助可视化的有用图表:
  • 如您所见,在中间有最佳超平面,然后是两条虚线作为边界线,通过每个类中最近的数据点。
  • 在使用SVM确定边界线时,希望边距是两个类之间最宽的距离。这将有助于SVM在看到需要分类的新数据时进行概括。

支持向量回归演练:

  • 现在对线性回归和SVM有了基本的了解,支持向量回归(SVR)是支持向量机和回归的组合。
  • 线性回归不适用于数据,因为数据有很多波动,而最佳拟合的线性线对股票数据的预测很差。SVM不能处理数据,因为没有在两个不同的类之间进行分类。
  • 对于股票数据,不预测一个类,预测一个系列中的下一个值。
  • 使用回归尝试使用梯度下降之类的东西来最小化成本函数。使用SVM,尝试在两个不同的类之间绘制超平面。因此SVR是2的组合,尝试在一定阈值内最小化误差。下面是一篇关于SVR 的有用文章的惊人图像,以帮助可视化SVR:
  • 蓝线是超平面,红线是边界线。希望能开始看到如何结合支持向量机和回归的思想。试图在一定的阈值内准确预测数值。
  • 所以定义边界线以构成边缘+ eplison和-eplison。Eplison是从超平面到每条边界线的距离。
  • 然后可以将回归线定义为y = wx + b
  • 目标是最小化误差并最大化边距。
  • 关于SVR的一个很酷的事情是它可以应用于预测非线性阈值内的值。下面是一个有用的图像,可以看到SVR的样子:

使用sklearn和可视化内核的SVR代码:

代码语言:javascript
复制
# Use sklearn support vector regression to predicit our data:
from sklearn.svm import SVR
 
dates = dates_df['Date'].as_matrix()
prices = df['Adj Close'].as_matrix()
 
#Convert to 1d Vector
dates = np.reshape(dates, (len(dates), 1))
prices = np.reshape(prices, (len(prices), 1))
 
svr_rbf = SVR(kernel= 'rbf', C= 1e3, gamma= 0.1)
svr_rbf.fit(dates, prices)
 
plt.figure(figsize = (12,6))
plt.plot(dates, prices, color= 'black', label= 'Data')
plt.plot(org_dates, svr_rbf.predict(dates), color= 'red', label= 'RBF model')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()
plt.show()

SVR适合数据

  • 在此代码中,使用Sklearn和支持向量回归(SVR)来预测数据的价格。
  • 正如所看到的那样,数据非常合适,但很可能是过度拟合。这个模型很难概括一年看不见的特斯拉股票数据。这就是LSTM神经网络派上用场的地方。
  • 将调整后的收盘价和日期作为整数从数据中得出。将数据重新整形为1D向量,因为我们需要将数据提供给SVR。
  • 内核是将低维数据映射到更高维数据的函数。将内核定义为RBF。RBF代表径向基函数。RBF的等式如下:
  • 这是RBF的核函数方程。RBF将2D空间转移到更高的维度,以帮助更好地拟合数据。该函数采用2个样本之间的欧氏距离平方并除以某个西格玛值。西格玛的价值决定了曲线拟合或数据的“紧密”程度。
  • 为了更好地理解RBF如何将数据传输到更高维度的空间,从Brandon Rohrer的视频中创建了一个gif 。这显示了线性超平面如何无法分离4组数据点。因此,使用内核函数将数据转换为更高维度并“拉伸”数据空间以使数据点适合类别:

内核函数的Gif

  • C是正则化参数。这是希望避免错误分类每个训练示例的程度。
  • 对于较大的C值,算法将选择较小边距的超平面。
  • 对于较小的C值,算法将寻找分离超平面的大余量,即使这意味着对某些点进行了错误分类。下面是一个有用的图像,可视化C值的大小之间的差异。
  • 在例子中,选择C值为1e3,这是C的一个大值,这意味着算法将选择一个边距较小的超平面。
  • 根据sklearn 文档,“gamma参数定义了单个训练示例的影响达到了多远,低值意味着'远',高值意味着'接近'。”
  • 换句话说,在决定超平面的位置时,要考虑边界线附近的高伽马点。并且在确定超平面的位置时考虑到接近并且远离边界线的低伽马点。下面是另一个可视化的有用图片:

规范数据:

代码语言:javascript
复制
# Create train set of adj close prices data:
train_data = df.loc[:,'Adj Close'].as_matrix()
print(train_data.shape) # 1258 

# Apply normalization before feeding to LSTM using sklearn:
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
train_data = train_data.reshape(-1,1)

scaler.fit(train_data)
train_data = scaler.transform(train_data)
  • 在这里,创建训练数据并将其标准化。使用sklearn创建一个MinMaxScaler()对象。
  • MinMaxScaler的工作原理是将值的范围缩小为0或1
  • 下面是min-max缩放器的等式:
  • 这是sklearn在后台进行的将数据转换为所需范围的等式。

为神经网络准备数据:

代码语言:javascript
复制
'''Function to create a dataset to feed into an LSTM'''
def create_dataset(dataset, look_back):
    dataX, dataY = [], []
    for i in range(len(dataset)-look_back):
        a = dataset[i:(i + look_back), 0]
        dataX.append(a)
        dataY.append(dataset[i + look_back, 0])
    return np.array(dataX), np.array(dataY)
    
    
# Create the data to train our model on:
time_steps = 36
X_train, y_train = create_dataset(train_data, time_steps)
 
# reshape it [samples, time steps, features]
X_train = np.reshape(X_train, (X_train.shape[0], 36, 1))
 
print(X_train.shape)
 
 
# Visualizing our data with prints:
print('X_train:')
print(str(scaler.inverse_transform(X_train[0])))
print("\n")
print('y_train: ' + str(scaler.inverse_transform(y_train[0].reshape(-1,1)))+'\n')
  • 这里创建'create_dataset'函数。此函数从(0到数据集长度 - 时间步数)循环。
  • 因此,基本上X_train数组中的每个索引都包含36天收盘价格的数组,y_train数组包含时间步骤后一天的收盘价。
  • 因此,换句话说,在先前的股票数据收盘价格的36天内提供神经网络,然后让它预测收盘价格的第二天。
  • 这可以通过打印输出显示:
代码语言:javascript
复制
X_train:
[[150.1000061 ]
 [149.55999756]
 [147.        ]
 [149.36000061]
 [151.27999878]
 [147.52999878]
 [145.72000122]
 [139.33999634]
 [161.27000427]
 [164.13000488]
 [170.97000122]
 [170.00999451]
 [176.67999268]
 [178.55999756]
 [181.5       ]
 [174.6000061 ]
 [169.61999512]
 [178.38000488]
 [175.22999573]
 [182.83999634]
 [181.41000366]
 [177.11000061]
 [178.72999573]
 [174.41999817]
 [178.38000488]
 [186.52999878]
 [196.55999756]
 [196.61999512]
 [195.32000732]
 [199.63000488]
 [198.22999573]
 [203.69999695]
 [193.63999939]
 [209.97000122]
 [209.6000061 ]
 [217.6499939 ]]
 
 
y_train: [[248.]]

递归神经网络:

  • LSTM代表长期短期记忆。LSTM是递归神经网络的高级版本。递归神经网络(RNN)是一种特殊类型的神经网络。RNN将先前的输出作为输入。在RNN中,先前的输出影响下一个输出。下面是一个有用的图像,显示了克里斯托弗·奥拉写的这篇惊人文章中 RNN的样子:
  • “一个反复出现的神经网络可以被认为是同一网络的多个副本,每个都传递给后继者。” - 赫里奥拉
  • 递归神经网络遭受消失的梯度问题。在反向传播(更新神经网络中的权重的递归过程)期间,更新每个层的权重。然而,对于RNN和消失梯度,梯度变得非常小,因为它继续更新每一层。随着反向传播在层中传播,当它到达第一层时,梯度值是如此微小的值,它使权重几乎无法察觉。由于进行了微小的更改,因此这些初始层不会学习或更改。
  • 因此换句话说,对于较长的数据序列,RNN会忘记它们在早期层中看到的内容,并且由于消失的梯度问题而无法正确学习。例如,如果有多段文字并且你试图预测句子中的下一个单词,那么RNN就不会记住模型已经看过的早期段落中的单词。这是LSTM有用的地方。

LSTM演练:

LSTM是一种在每个LSTM小区内部具有门的RNN。喜欢将LSTM细胞视为一个细胞,每个细胞内部都有自己的微小神经网络。LSTM单元内的这些门有助于LSTM决定记住哪些数据是重要的,甚至在长序列数据中也可以忘记哪些数据。门的类型是遗忘门,输入门和输出门。下面是这个视频中这些LSTM细胞外观的惊人可视化。这部分受到本视频和本文的严重影响,因为这些解释非常棒:

https://colah.github.io/posts/2015-08-Understanding-LSTMs/

所以LSTM就像RNN一样顺序。先前的单元输出作为输入传递给下一个单元。让分解一下LSTM单元内的每个门正在做什么:

盖茨包含sigmoid激活函数。S形激活函数可以被认为是“挤压”函数。它接受数字输入并将数字调整到0到1的范围内。这很重要,因为它允许我们避免网络中的数字变得庞大并导致学习错误。

遗忘门:

遗忘门从先前的LSTM单元和当前输入获取先前的隐藏状态并将它们相乘。接近0的值意味着忘记数据,而接近1的值意味着保留此数据。

  • 数学:

遗忘门是遗忘门权重矩阵乘以先前的隐藏状态,然后输入状态+一些偏差全部传递到sigmoid激活函数。计算完成后,将其传递给单元状态。

输入门:

此门使用要在单元状态中存储的新数据更新单元状态。输入门将先前的隐藏状态乘以输入并将其传递给sigmoid。接近0的值并不重要,接近1的值很重要。然后将前一个隐藏状态乘以输入并传递给tan激活函数,该函数将值调整到-1到1的范围内。然后,将sigmoid输出乘以tan输出。sigmoid输出决定哪些信息对于保持tan输出很重要。

  • 数学:

细胞状态:

网络记忆。这可以被认为是一种“信息高速公路”,它将来自先前细胞的记忆带到未来的细胞上。门进入单元状态,然后将该信息传递给下一个单元。一旦计算了遗忘门和输入门,我们就可以计算出单元状态的值。

  • 数学:

单元状态是遗忘门输出*前一个单元状态+输入门输出*从前一个单元传递的单元状态值。这是为了丢弃想要忘记的接近零的某些值。然后将输入门的值添加到我们想要传递给下一个单元的单元状态值。

输出门:

输出门决定下一个隐藏状态应该是什么。将先前的隐藏状态乘以输入并传递到sigmoid激活函数。然后将单元状态值传递给tan激活函数。然后,将tan输出乘以sigmoid输出,以确定隐藏状态应该携带到下一个LSTM单元的数据。

  • 数学:

退出:

  • Dropout是一种用于深度学习和神经网络的正则化技术。正规化是一种用于帮助网络不过度填充数据的技术
  • 过度拟合是指神经网络在训练数据上表现良好但测试数据非常差。这意味着网络不能很好地概括,这意味着它会对错误/不良之前未见过的新图像进行分类
  • 在官方文件中对辍学进行了解释,“在神经网络中,每个参数接收的导数告诉它应该如何改变,以便最终的损失函数减少,给定所有其他单位正在做的事情。因此,单位可能会以修正其他单位错误的方式改变。这可能导致复杂的协同适应。反过来,这会导致过度拟合,因为这些共同适应并不能推广到看不见的数据。“
  • 因此,基本上关闭一层中的一些神经元,以便在网络权重的更新(反向传播)期间不学习任何信息。这允许其他活跃的神经元更好地学习并减少错误。

模型的代码:

代码语言:javascript
复制
# Build the model
model = keras.Sequential()
 
model.add(LSTM(units = 100, return_sequences = True, input_shape = (X_train.shape[1], 1)))
model.add(Dropout(0.2))
 
model.add(LSTM(units = 100))
model.add(Dropout(0.2))
 
# Output layer
model.add(Dense(units = 1))
 
# Compiling the model
model.compile(optimizer = 'adam', loss = 'mean_squared_error')
 
# Fitting the model to the Training set
history = model.fit(X_train, y_train, epochs = 20, batch_size = 10, validation_split=.30)
  • 顺序 - 在这里建立神经网络。按顺序创建模型。顺序意味着您可以逐层创建模型。顺序意味着有一个输入和单个输出,几乎像一个管道。
  • LSTM图层 - 然后创建两个LSTM图层,每层后面有20%的丢失。
  • 第一层有return_sequences = true。这样做是因为堆叠了LSTM层,希望第二个LSTM层具有三维序列输入。
  • 还将input_shape设置为x.shape,以确保它采用相同的3D形状的数据。
  • 输出层 - 然后创建输出层,它只是一个奇异节点,它会吐出一个介于0和1之间的数字。
  • 编译 - 然后编译模型。使用Adam优化器,它是一种梯度下降优化算法,将损失函数定义为均方误差。使用Adam来最小化均方误差的成本函数。
  • 拟合模型 - 最后,使用反向传播和Adam优化器来拟合模型。将时期定义为20,批量大小为10.还使用内置的Keras分割功能将数据分成70%的训练数据和30%的测试数据。

绘制模型损失:

代码语言:javascript
复制
# Plot training & validation loss values
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()

在这里,使用keras api中的代码来绘制模型损失。当到达第20个时代时,测试损失和火车损失非常接近并且它们被最小化。

做出预测:

代码语言:javascript
复制
# Get the stock prices for 2019 to have our model make the predictions
test_data = test_df['Adj Close'].values
test_data = test_data.reshape(-1,1)
test_data = scaler.transform(test_data)
 
# Create the data to test our model on:
time_steps = 36
X_test, y_test = create_dataset(test_data, time_steps)
 
# store the original vals for plotting the predictions
y_test = y_test.reshape(-1,1)
org_y = scaler.inverse_transform(y_test)
 
# reshape it [samples, time steps, features]
X_test = np.reshape(X_test, (X_test.shape[0], 36, 1))
 
# Predict the prices with the model
predicted_y = model.predict(X_test)
predicted_y = scaler.inverse_transform(predicted_y)
 
 
# plot the results
plt.plot(org_y, color = 'red', label = 'Real Tesla Stock Price')
plt.plot(predicted_y, color = 'blue', label = 'Predicted Tesla Stock Price')
plt.title('Tesla Stock Price Prediction')
plt.xlabel('Time')
plt.ylabel('Tesla Stock Price')
plt.legend()
plt.show()

LSTM预测结果

  • 在这里,神经网络对看不见的2019年特斯拉股票数据进行了预测。
  • 首先从测试数据帧中获取2019年的收盘价格数据,然后将其转换为0到1之间的值。
  • 再次使用create_dataset函数将数据转换为36个股票价格的批次。因此,给神经网络一个X_test数组,其中每个索引包含36天的收盘价格。y_test是36天价格的价值。
  • 然后,将原始y值存储在org_y变量中。将绘制此图并将这些值与模型预测的价格值进行比较。
  • 最后,重塑它,让网络做出价格预测。
  • 正如在上面的预测图中所看到的,模型表现非常好,并且遵循了全年看不见的数据的行为。

结论:

  • LSTM非常吸引人,它们有很多有用的应用程序。它们允许对长序列数据进行准确预测。希望喜欢这篇文章,希望能学到一些东西。如果有任何问题,疑虑或建设性批评,请通过linkedin与我联系,并在github上查看该项目的代码。

https://github.com/DrewScatterday

资源:

  • https://pythonprogramming.net/getting-stock-prices-python-programming-for-finance/
  • https://towardsdatascience.com/in-12-minutes-stocks-analysis-with-pandas-and-scikit-learn-a8d8a7b50ee7
  • https://www.youtube.com/watch?v=4R2CDbw4g88
  • https://medium.com/coinmonks/support-vector-regression-or-svr-8eb3acf6d0ff
  • http://www.saedsayad.com/support_vector_machine_reg.htm?source=post_page-----8eb3acf6d0ff----------------------
  • https://medium.com/machine-learning-101/chapter-2-svm-support-vector-machine-theory-f0812effc72
  • https://www.youtube.com/watch?v=SSu00IRRraY
  • http://benalexkeen.com/feature-scaling-with-scikit-learn/
  • https://colah.github.io/posts/2015-08-Understanding-LSTMs/
  • https://www.youtube.com/watch?v=8HyCNIVRbSU
  • https://towardsdatascience.com/playing-with-time-series-data-in-python-959e2485bff8
  • https://github.com/krishnaik06/Stock-Price-Prediction-using-Keras-and-Recurrent-Neural-Networ/blob/master/rnn.py

推荐阅读

Python爬虫实战:批量采集股票数据,并保存到Excel中

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档