前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python在Keras中使用LSTM解决序列问题

python在Keras中使用LSTM解决序列问题

原创
作者头像
拓端
修改2020-09-27 11:39:39
3.6K0
修改2020-09-27 11:39:39
举报
文章被收录于专栏:拓端tecdat

原文链接:http://tecdat.cn/?p=8461

时间序列预测是指我们必须根据时间相关的输入来预测结果的问题类型。时间序列数据的典型示例是股市数据,其中股价随时间变化。 

递归神经网络(RNN)已被证明可以有效解决序列问题。特别地,作为RNN的变体的长期短期记忆网络(LSTM)当前正在各种领域中用于解决序列问题。

序列问题的类型

序列问题可以大致分为以下几类:

  1. 一对一:其中有一个输入和一个输出。一对一序列问题的典型示例是您拥有一幅图像并且想要为该图像预测单个标签的情况。
  2. 多对一:在多对一序列问题中,我们将数据序列作为输入,并且必须预测单个输出。文本分类是多对一序列问题的主要示例,其中我们有一个单词输入序列,并且我们希望预测一个输出标签。
  3. 一对多:在一对多序列问题中,我们只有一个输入和一个输出序列。典型示例是图像及其相应的说明。
  4. 多对多:多对多序列问题涉及序列输入和序列输出。例如,将7天的股票价格作为输入,并将接下来7天的股票价格作为输出。聊天机器人还是多对多序列问题的一个示例,其中文本序列是输入,而另一个文本序列是输出。

 在本文中,我们将了解如何使用LSTM及其不同的变体来解决一对一和多对一的序列问题。 

阅读本文后,您将能够基于历史数据解决诸如股价预测,天气预报等问题。由于文本也是单词序列,因此本文中获得的知识也可以用于解决自然语言处理任务,例如文本分类,语言生成等。

一对一序列问题

正如我之前所说,在一对一序列问题中,只有一个输入和一个输出。在本节中,我们将看到两种类型的序列问题。首先,我们将了解如何使用单个功能解决一对一的序列问题,然后我们将了解如何使用多个功能解决一对一的序列问题。

单一特征的一对一序列问题

在本节中,我们将看到如何解决每个时间步都有一个功能的一对一序列问题。

首先,我们导入将在本文中使用的必需库:

代码语言:javascript
复制
from numpy import arrayfrom keras.preprocessing.text import one_hotfrom keras.preprocessing.sequence import pad_sequencesfrom keras.models import Sequentialfrom keras.layers.core import Activation, Dropout, Densefrom keras.layers import Flatten, LSTMfrom keras.layers import GlobalMaxPooling1Dfrom keras.models import Modelfrom keras.layers.embeddings import Embeddingfrom sklearn.model_selection import train_test_splitfrom keras.preprocessing.text import Tokenizerfrom keras.layers import Inputfrom keras.layers.merge import Concatenatefrom keras.layers import Bidirectional import pandas as pdimport numpy as npimport re import matplotlib.pyplot as plt

创建数据集

在下一步中,我们将准备本节要使用的数据集。

代码语言:javascript
复制
X = list()Y = list()X = [x+1 for x in range(20)]Y = [y * 15 for y in X] print(X)print(Y)

在上面的脚本中,我们创建20个输入和20个输出。每个输入都包含一个时间步,而该时间步又包含一个功能。每个输出值是相应输入值的15倍。如果运行上面的脚本,应该看到如下所示的输入和输出值:

代码语言:javascript
复制
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285, 300]

LSTM层的输入应为3D形状,即(样本,时间步长,特征)。样本是输入数据中样本的数量。输入中有20个样本。时间步长是每个样本的时间步长数。我们有1个时间步。最后,特征对应于每个时间步的特征数量。每个时间步都有一个功能。

代码语言:javascript
复制
 X = array(X).reshape(20, 1, 1)

通过简单LSTM解决方案

现在,我们可以创建具有一个LSTM层的简单LSTM模型。

代码语言:javascript
复制
model = Sequential()model.add(LSTM(50, activation='relu', input_shape=(1, 1)))model.add(Dense(1))model.compile(optimizer='adam', loss='mse')print(model.summary())

在上面的脚本中,我们创建了一个LSTM模型,该模型具有一层包含50个神经元和relu激活功能的LSTM层。您可以看到输入形状为(1,1),因为我们的数据具有一个功能的时间步长。 

代码语言:javascript
复制
Layer (type)                 Output Shape              Param #=================================================================lstm_16 (LSTM)               (None, 50)                10400_________________________________________________________________dense_15 (Dense)             (None, 1)                 51=================================================================Total params: 10,451Trainable params: 10,451Non-trainable params: 0

现在让我们训练模型:

代码语言:javascript
复制
model.fit(X, Y, epochs=2000, validation_split=0.2, batch_size=5)

我们为2000个时期训练模型,批量大小为5。您可以选择任何数字。训练模型后,我们可以对新实例进行预测。

假设我们要预测输入为30的输出。实际输出应为30 x 15 =450。 首先,我们需要按照LSTM的要求将测试数据转换为正确的形状,即3D形状。以下 预测数字30的输出:

代码语言:javascript
复制
...print(test_output)

我得到的输出值437.86略小于450。

 通过堆叠LSTM解决方案

现在让我们创建一个堆叠的LSTM,看看是否可以获得更好的结果。数据集将保持不变,模型将被更改。看下面的脚本:

代码语言:javascript
复制
...print(model.summary())

在上面的模型中,我们有两个LSTM层。注意,第一个LSTM层的参数return_sequences设置为True。当返回序列设置True为时,每个神经元隐藏状态的输出将用作下一个LSTM层的输入。以上模型的摘要如下:

代码语言:javascript
复制
_________________________________________________________________Layer (type)                 Output Shape              Param #=================================================================lstm_33 (LSTM)               (None, 1, 50)             10400_________________________________________________________________lstm_34 (LSTM)               (None, 50)                20200_________________________________________________________________dense_24 (Dense)             (None, 1)                 51=================================================================Total params: 30,651Trainable params: 30,651Non-trainable params: 0________________________

接下来,我们需要训练我们的模型,如以下脚本所示:

代码语言:javascript
复制
...
代码语言:javascript
复制
print(test_output)

我得到的输出为459.85,好于我们通过单个LSTM层获得的数字437。

具有多个特征的一对一序列问题

在最后一节中,每个输入样本都有一个时间步,其中每个时间步都有一个特征。在本节中,我们将看到如何解决输入时间步长具有多个特征的一对一序列问题。

创建数据集

首先创建数据集。看下面的脚本:

代码语言:javascript
复制
nums = 25 X1 = list()X2 = list()X = list()Y = list()... print(X1)print(X2)print(Y)

在上面的脚本中,我们创建三个列表:X1X2,和Y。每个列表包含25个元素,这意味着总样本大小为25。最后,Y包含输出。X1X2以及Y列表已打印在下面:

代码语言:javascript
复制
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50][3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75][6, 24, 54, 96, 150, 216, 294, 384, 486, 600, 726, 864, 1014, 1176, 1350, 1536, 1734, 1944, 2166, 2400, 2646, 2904, 3174, 3456, 3750]

输出列表中的每个元素基本上都是X1and X2列表中相应元素的乘积。例如,输出列表中的第二个元素是24,这是列表中的第二个元素(X1即4)和列表中的第二个元素(X2即6 )的乘积。

输入将由X1X2列表的组合组成,其中每个列表将表示为一列。以下脚本创建最终输入:

代码语言:javascript
复制
X = np.column_stack((X1, X2))print(X)

这是输出:

代码语言:javascript
复制
[[ 2  3] [ 4  6] [ 6  9] [ 8 12] [10 15] [12 18] [14 21] [16 24] [18 27] [20 30] [22 33] [24 36] [26 39] [28 42] [30 45] [32 48] [34 51] [36 54] [38 57] [40 60] [42 63] [44 66] [46 69] [48 72] [50 75]]

 可以看到它包含两列,即每个输入两个功能。如前所述,我们需要将输入转换为3维形状。我们的输入有25个样本,其中每个样本包含1个时间步,每个时间步包含2个特征。以下脚本可重塑输入。

代码语言:javascript
复制
X = array(X).reshape(25, 1, 2)

通过简单LSTM解决方案

我们现在准备训练我们的LSTM模型。让我们首先像上一节中那样开发一个LSTM层模型:

代码语言:javascript
复制
model = Sequential()model.add(LSTM(80, activation='relu', input_shape=(1, 2)))...print(model.summary())

在这里,我们的LSTM层包含80个神经元。我们有两个神经层,其中第一层包含10个神经元,第二个密集层(也作为输出层)包含1个神经元。该模型的摘要如下:

代码语言:javascript
复制
Layer (type)                 Output Shape              Param #=================================================================lstm_38 (LSTM)               (None, 80)                26560_________________________________________________________________dense_29 (Dense)             (None, 10)                810_________________________________________________________________dense_30 (Dense)             (None, 1)                 11=================================================================Total params: 27,381Trainable params: 27,381Non-trainable params: 0_________________________________________________________________None

以下脚本训练模型:

代码语言:javascript
复制
model.fit(X, Y, epochs=2000, validation_split=0.2, batch_size=5)

让我们在一个新的数据点上测试我们训练有素的模型。我们的数据点将具有两个特征,即(55,80)实际输出应为55 x 80 =4400。让我们看看我们的算法预测了什么。执行以下脚本:

代码语言:javascript
复制
...print(test_output)

我的输出为3263.44,与实际输出相差甚远。

通过堆叠LSTM解决方案

现在,让我们创建一个具有多个LSTM和密集层的更复杂的LSTM,看看是否可以改善我们的答案:

代码语言:javascript
复制
model = Sequential()...print(model.summary())

模型摘要如下:

代码语言:javascript
复制
_________________________________________________________________Layer (type)                 Output Shape              Param #=================================================================lstm_53 (LSTM)               (None, 1, 200)            162400_________________________________________________________________lstm_54 (LSTM)               (None, 1, 100)            120400_________________________________________________________________lstm_55 (LSTM)               (None, 1, 50)             30200_________________________________________________________________lstm_56 (LSTM)               (None, 25)                7600_________________________________________________________________dense_43 (Dense)             (None, 20)                520_________________________________________________________________dense_44 (Dense)             (None, 10)                210_________________________________________________________________dense_45 (Dense)             (None, 1)                 11=================================================================Total params: 321,341Trainable params: 321,341Non-trainable params: 0

下一步是训练我们的模型,并在测试数据点(即(55,80))上对其进行测试。

为了提高准确性,我们将减小批量大小,并且由于我们的模型更加复杂,现在我们还可以减少时期数。以下脚本训练LSTM模型并在测试数据点上进行预测。

代码语言:javascript
复制
...print(test_output)

在输出中,我得到的值3705.33仍小于4400,但比以前使用单个LSTM层获得的3263.44的值好得多。您可以将LSTM层,密集层,批处理大小和时期数进行不同的组合,以查看是否获得更好的结果。

多对一序列问题

在前面的部分中,我们看到了如何使用LSTM解决一对一的序列问题。在一对一序列问题中,每个样本都包含一个或多个特征的单个时间步。具有单个时间步长的数据实际上不能视为序列数据。事实证明,密集连接的神经网络在单个时间步长数据下表现更好。

实际序列数据包含多个时间步长,例如过去7天的股票市场价格,包含多个单词的句子等等。

在本节中,我们将看到如何解决多对一序列问题。在多对一序列问题中,每个输入样本具有多个时间步长,但是输出由单个元素组成。输入中的每个时间步都可以具有一个或多个功能。我们将从具有一个特征的多对一序列问题开始,然后我们将了解如何解决输入时间步长具有多个特征的多对一问题。

具有单个功能的多对一序列问题

首先创建数据集。我们的数据集将包含15个样本。每个样本将具有3个时间步长,其中每个时间步长将包含一个单一功能,即一个数字。每个样本的输出将是三个时间步长中每个步长的数字之和。例如,如果我们的样本包含序列4,5,6,则输出将为4 + 5 + 6 = 10。

创建数据集

首先创建一个从1到45的整数列表。由于我们要在数据集中获得15个样本,因此我们将对包含前45个整数的整数列表进行整形。

代码语言:javascript
复制
X = np.array([x+1 for x in range(45)])print(X)

在输出中,您应该看到前45个整数:

代码语言:javascript
复制
[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45]

我们可以使用以下函数将其重塑为样本数,时间步长和特征:

代码语言:javascript
复制
X = X.reshape(15,3,1)print(X)

上面的脚本将列表X转换为带有15个样本,3个时间步长和1个特征的3维形状。上面的脚本还打印了调整后的数据。

代码语言:javascript
复制
[[[ 1]  [ 2]  [ 3]]  [[ 4]  [ 5]  [ 6]]  [[ 7]  [ 8]  [ 9]]  [[10]  [11]  [12]]  [[13]  [14]  [15]]  [[16]  [17]  [18]]  [[19]  [20]  [21]]  [[22]  [23]  [24]]  [[25]  [26]  [27]]  [[28]  [29]  [30]]  [[31]  [32]  [33]]  [[34]  [35]  [36]]  [[37]  [38]  [39]]  [[40]  [41]  [42]]  [[43]  [44]  [45]]]

我们已经将输入数据转换为正确的格式,现在让我们创建输出向量。正如我之前所说,输出中的每个元素将等于相应输入样本中时间步长中的值之和。以下脚本创建输出向量:

代码语言:javascript
复制
Y = list()for x in X:...print(Y)

输出数组Y如下所示:

代码语言:javascript
复制
[  6  15  24  33  42  51  60  69  78  87  96 105 114 123 132]

通过简单LSTM解决方案

现在让我们用一个LSTM层创建模型。

代码语言:javascript
复制
model = Sequential()...model.compile(optimizer='adam', loss='mse')

以下脚本训练了我们的模型:

代码语言:javascript
复制
history = model.fit(...)

训练完模型后,我们就可以使用它对测试数据点进行预测。让我们预测数字序列50、51、52的输出。实际输出应为50 + 51 + 52 =153。以下脚本将我们的测试点转换为3维形状,然后预测输出:

代码语言:javascript
复制
....print(test_output)

我的输出为145.96,比实际输出值153少7点。

通过堆叠LSTM解决方案

现在,让我们创建一个具有多层的复杂LSTM模型,看看是否可以获得更好的结果。执行以下脚本来创建和训练具有多个LSTM和密集层的复杂模型:

代码语言:javascript
复制
model = Sequential()....model.compile(optimizer='adam', loss='mse') history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1)

现在让我们在测试序列(即50、51、52)上测试模型:

代码语言:javascript
复制
....print(test_output)

我在这里得到的答案是155.37,比我们之前得到的145.96更好。在这种情况下,我们与153的实际差值只有2分。

通过双向LSTM解决方案

双向LSTM是一种LSTM,可以从正向和反向两个方向的输入序列中学习。最终的序列解释是向前和向后学习遍历的串联。让我们看看使用双向LSTM是否可以获得更好的结果。

以下脚本创建了一个双向LSTM模型,该模型具有一个双向层和一个作为模型输出的密集层。

代码语言:javascript
复制
from keras.layers import Bidirectional ...model.compile(optimizer='adam', loss='mse')

以下脚本训练模型并根据测试序列50、51和52进行预测。

代码语言:javascript
复制
...print(test_output)

我得到的结果是152.26,仅比实际结果少一小部分。因此,我们可以得出结论,对于我们的数据集,具有单层的双向LSTM的性能优于单层和堆叠的单向LSTM。

具有多个特征的多对一序列问题

在多对一序列问题中,我们有一个输入,其中每个时间步均包含多个特征。输出可以是一个值或多个值,在输入时间步长中每个功能一个。我们将在本节中介绍这两种情况。

创建数据集

我们的数据集将包含15个样本。每个样本将包含3个时间步。每个时间步都有两个功能。

让我们创建两个列表。一个将包含3的倍数,直到135,即总共45个元素。第二个列表将包含5的倍数,从1到225。第二个列表也将总共包含45个元素。以下脚本创建这两个列表:

代码语言:javascript
复制
X1 = np.array([x+3 for x in range(0, 135, 3)])...print(X2)

您可以在以下输出中看到列表的内容:

代码语言:javascript
复制
[  3   6   9  12  15  18  21  24  27  30  33  36  39  42  45  48  51  54  57  60  63  66  69  72  75  78  81  84  87  90  93  96  99 102 105 108 111 114 117 120 123 126 129 132 135][  5  10  15  20  25  30  35  40  45  50  55  60  65  70  75  80  85  90  95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225]

上面的每个列表代表时间样本中的一个功能。可以通过合并两个列表来创建聚合数据集,如下所示:

代码语言:javascript
复制
X = np.column_stack((X1, X2))print(X)

输出显示汇总的数据集:

代码语言:javascript
复制
 [  6  10] [  9  15] [ 12  20] [ 15  25] [ 18  30] [ 21  35] [ 24  40] [ 27  45] [ 30  50] [ 33  55] [ 36  60] [ 39  65] [ 42  70] [ 45  75] [ 48  80] [ 51  85] [ 54  90] [ 57  95] [ 60 100] [ 63 105] [ 66 110] [ 69 115] [ 72 120] [ 75 125] [ 78 130] [ 81 135] [ 84 140] [ 87 145] [ 90 150] [ 93 155] [ 96 160] [ 99 165] [102 170] [105 175] [108 180] [111 185] [114 190] [117 195] [120 200] [123 205] [126 210] [129 215] [132 220] [135 225]]

我们需要将数据重塑为三个维度,以便LSTM可以使用它。我们的数据集 有45行,两列。我们将数据集重塑为15个样本,3个时间步长和两个特征。

代码语言:javascript
复制
X = array(X).reshape(15, 3, 2)print(X)

您可以在以下输出中看到15个样本:

代码语言:javascript
复制
[[[  3   5]  [  6  10]  [  9  15]]  [[ 12  20]  [ 15  25]  [ 18  30]]  [[ 21  35]  [ 24  40]  [ 27  45]]  [[ 30  50]  [ 33  55]  [ 36  60]]  [[ 39  65]  [ 42  70]  [ 45  75]]  [[ 48  80]  [ 51  85]  [ 54  90]]  [[ 57  95]  [ 60 100]  [ 63 105]]  [[ 66 110]  [ 69 115]  [ 72 120]]  [[ 75 125]  [ 78 130]  [ 81 135]]  [[ 84 140]  [ 87 145]  [ 90 150]]  [[ 93 155]  [ 96 160]  [ 99 165]]  [[102 170]  [105 175]  [108 180]]  [[111 185]  [114 190]  [117 195]]  [[120 200]  [123 205]  [126 210]]  [[129 215]  [132 220]  [135 225]]]

输出还将具有对应于15个输入样本的15个值。输出中的每个值将是每个输入样本的第三时间步中两个特征值的总和。例如,第一个样本的第三时间步长具有特征9和15,因此输出将为24。类似地,第二个样本的第三时间步长中的两个特征值分别为18和30;第二个时间步长中的两个特征值分别为18和30。相应的输出将是48,依此类推。

以下脚本创建并显示输出向量:

代码语言:javascript
复制
[ 24  48  72  96 120 144 168 192 216 240 264 288 312 336 360]

现在让我们通过简单的,堆叠的和双向的LSTM解决多对一序列问题。

通过简单LSTM解决方案

代码语言:javascript
复制
model = Sequential()...history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1)

模型经过训练。我们将创建一个测试数据点,然后将使用我们的模型对测试点进行预测。

代码语言:javascript
复制
...print(test_output)

输入的第三时间步长的两个特征的总和为14 + 61 =75。我们的带有一个LSTM层的模型预测为73.41,这非常接近。

通过堆叠LSTM解决方案

以下脚本训练堆叠的LSTM并在测试点上进行预测:

代码语言:javascript
复制
model = Sequential()model.add(LSTM(200, activation='relu', return_sequences=True, input_shape=(3, 2)))...print(test_output)

我收到的输出为71.56,比简单的LSTM差。似乎我们堆叠的LSTM过度拟合。

通过双向LSTM解决方案

这是简单双向LSTM的训练脚本,以及用于对测试数据点进行预测的代码:

代码语言:javascript
复制
from keras.layers import Bidirectional model = Sequential()...print(test_output)

输出为76.82,非常接近75。同样,双向LSTM似乎胜过其余算法。

到目前为止,我们已经基于来自不同时间步长的多个要素值预测了单个值。在多对一序列的另一种情况下,您希望在时间步长中为每个功能预测一个值。例如,我们在本节中使用的数据集具有三个时间步,每个时间步具有两个特征。我们可能希望预测每个功能系列的单独价值。下面的示例很清楚,假设我们有以下输入:

代码语言:javascript
复制
[[[  3   5]  [  6  10]  [  9  15]]

在输出中,我们需要一个具有两个功能的时间步,如下所示:

代码语言:javascript
复制
[12, 20]

您可以看到输出中的第一个值是第一个系列的延续,第二个值是第二个系列的延续。我们可以通过简单地将输出密集层中神经元的数量更改为我们想要的输出中的特征值的数量来解决此类问题。但是,首先我们需要更新输出向量Y。输入向量将保持不变:

代码语言:javascript
复制
Y = list()for x in X:...print(Y)

上面的脚本创建一个更新的输出向量并将其打印在控制台上,输出如下所示:

代码语言:javascript
复制
[[ 12  20] [ 21  35] [ 30  50] [ 39  65] [ 48  80] [ 57  95] [ 66 110] [ 75 125] [ 84 140] [ 93 155] [102 170] [111 185] [120 200] [129 215] [138 230]]

现在,让我们在数据集上训练我们的简单,堆叠和双向LSTM网络。以下脚本训练了一个简单的LSTM:

代码语言:javascript
复制
model = Sequential()...history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1)

下一步是在测试数据点上测试我们的模型。以下脚本创建一个测试数据点:

代码语言:javascript
复制
test_input = array([[20,34],                    [23,39],                    [26,44]])...print(test_output)

实际输出为[29,45]。我们的模型预测[29.089157,48.469097],这非常接近。

现在让我们训练一个堆叠的LSTM并预测测试数据点的输出:

代码语言:javascript
复制
model = Sequential()model.add(LSTM(100, activation='relu', return_sequences=True, input_shape=(3, 2)))...print(test_output)

输出为[29.170143,48.688267],再次非常接近实际输出。

最后,我们可以训练双向LSTM并在测试点上进行预测:

代码语言:javascript
复制
from keras.layers import Bidirectional model = Sequential()...print(test_output)

输出为[29.2071,48.737988]。

您可以再次看到双向LSTM做出最准确的预测。

结论

简单的神经网络不适用于解决序列问题,因为在序列问题中,除了当前输入之外,我们还需要跟踪先前的输入。具有某种记忆的神经网络更适合解决序列问题。LSTM就是这样一种网络。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原文链接:http://tecdat.cn/?p=8461
  • 序列问题的类型
  • 一对一序列问题
  • 多对一序列问题
  • 结论
相关产品与服务
NLP 服务
NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档