【时间序列预测/分类】 全系列60篇由浅入深的博文汇总:传送门


Long Short-Term Memory networks(长-短期记忆网络),简称 LSTMs,可用于时间序列预测。有许多类型的LSTM模型可用于每种特定类型的时间序列预测问题。本文介绍了如何为一系列标准时间序列预测问题开发一套LSTM模型。本文旨在为每种类型的时间序列问题提供所对应模型的示例,你可以依此为模板,针对自己的业务需求进行修改。本文的主要内容为:


代码环境:

日期:2020/4/7



滑动窗口是处理时间序列数据通用的方法,之前的时间序列预测的文章中,示例代码其实是简单的滑动窗口。先通过两张相关图片来直观理解滑动窗口是如何建模的。

  1. 滑动窗口在时间序列预测任务上的建模方法:

    对于时间序列预测任务来讲,以一定滑动窗口大小和滑动步长在原始时间序列上截取数据,以之前的滑动窗口内的数据作为训练样本X,以需要预测的之后的某一时间步长内的的数据作为样本标签y,即从历史数据中学习映射关系来预测未来数据。

  2. 滑动窗口在时间序列分类任务上的建模方法:

    在时间序列分类任务中,使用滑动窗口分割数据并获取标签的过程如上图所示。传感器信号称为特征(features),传感器信号的通道数就是特征的尺寸;传感器信号被一个个滑动窗口按照指定的滑动步长(time steps)截取数据,一个窗口内截取的采样点的数据称为样本。滑动窗口所截取的最后一个采样点的数据标签作为样本标签。


前言

本文分为四个部分,将按照如下行文顺序进行介绍:

  1. Univariate LSTM Models
  2. Multivariate LSTM Models
  3. Multi-step LSTM Models
  4. Multivariate Multi-step LSTM Models

1. 单变量 LSTM 模型(Univariate LSTM Models)

LSTMs可用于单变量时间序列预测问题的建模。这些问题由单个观测序列组成,需要一个模型从过去的观测序列中学习,以预测序列中的下一个值。下文将演示一元时间序列预测的LSTM模型。本节分为六个部分:

  1. Data Preparation
  2. Vanilla LSTM
  3. Stacked LSTM
  4. Bidirectional LSTM
  5. CNN-LSTM
  6. ConvLSTM

上边的5种LSTM模型都可用于单时间步单变量时间序列预测,也可用于其他类型时间序列预测问题。


1.1 数据准备

在对单变量序列建模之前,必须做好准备。LSTM模型学习一个映射规则,该规则以过去的序列观测值作为输入,然后输出预测值。因此,观测序列必须转换成LSTM可以学习的多个样本(由多个观测值组成)。考虑一个单变量序列:

[10, 20, 30, 40, 50, 60, 70, 80, 90]

我们可以将序列分成多个输入/输出模式,称为样本,其中三个时间步(的观测值)作为输入,一个时间步(的观测值)作为输出:

X,        y
10, 20, 30,  40
20, 30, 40,  50
30, 40, 50,  60
...

我们可以通过写一个函数来实现上述功能,为了增加文章可读性,减少重复,此处先不贴出代码,会在完整代码中贴出。先看下划分结果,使用该函数对整个数据集进行划分,将单变量序列拆分为6个样本(sample),其中每个样本有3个输入时间步(的观测值)和1个输出时间步(的观测值)。

[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90

数据集构建好之后,接下来定义网络模型,进行训练。


1.2 Vanilla LSTM

刚开始接触LSTM的时候,碰到“Vanilla”还以为是“香草”的意思,心想作者也太文艺了,后来才知道是自己才疏学浅,“Vanilla”是“普通的,简单的”意思…

Vanilla LSTM是一个LSTM模型,它有一个单隐层的LSTM单元和一个用于预测的输出层。LSTM可以很好的完成序列预测任务。与CNN读取整个输入向量不同,LSTM模型一次读取序列的一个时间步(的观测值),并建立一个内部状态表示,来学习上下文信息,从而进行预测。以下为单变量时间序列预测任务定义了一个简单的LSTM模型:

model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(n_steps, n_features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

该模型中,定义了一个有50个LSTM 单元的单隐层和一个预测单个数值的输出层。该模型利用有效的随机梯度下降Adam模型进行拟合,并利用均方误差(mse)损失函数进行优化。

模型定义中的关键是 input_shape 参数,当初花了很长时间来理解,现在算是稍有眉目吧。如果不好理解,可以用 jupyter notebook 一步一步地看,看每个变量的shape,也可以使用IDE debug,再结合API的官方文档,帮助理解。这里有一点需要注意,如果查LSTM官方文档的话,这个API中是没有input_shape这个参数的,那为什么要指定?这其实是用于构建顺序模型的 Sequential() API所要求提供的的输入数据格式,只需要在第一层输入数据尺寸,后面层Keras会自动计算。我们的示例中使用的是一个单变量序列,因此特征数(features)为1。相信很多人跟我一样,对 n_steps 这个参数也有疑惑,其实就是 滑动窗口的宽度,也可以理解为一个样本中包含观测值的数量。这个参数在 split_sequence() 函数中已经定义了,还不明白的,可以结合这个函数再理解理解,在我们的示例中,这个参数为3。

这里贴一下所用API的官方文档:

  • Sequential
  • LSTM
  • Dense
  • compile
  • fit

模型定义完成之后,需要考虑Keras中模型拟合API fit() 的数据输入维度要求,每个样本的输入形状在单隐层的LSTM层中的 input_shape 参数中指定。因为有多个训练样本,所以模型拟合API fit() 要求输入数据的形状为 [样本,时间步,特征]([samples, timesteps, features])。上一节中的 split_sequence() 函数输出 X 的形状为 [samples,timesteps],因此我们可以通过 reshape() 方法来为训练数据集添加特征维度。代码实现:

n_features = 1
X = X.reshape((X.shape[0], X.shape[1], n_features))

完整代码在该小节结束后贴出,此处只放出结果:

[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
X:
[[[10][20][30]][[20][30][40]][[30][40][50]][[40][50][60]][[50][60][70]][[60][70][80]]]
y:
[40 50 60 70 80 90]X.shape:(6, 3, 1), y.shape:(6,)Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
lstm (LSTM)                  (None, 50)                10400
_________________________________________________________________
dense (Dense)                (None, 1)                 51
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0
_________________________________________________________________
None
[[[70][80][90]]]
yhat: [[102.01498]]

1.3 Stacked LSTM

Stacked LSTM意即堆叠LSTM,可以将多个LSTM层堆叠在一起。LSTM层需要三维输入,LSTM的默认输出为二维,可以通过设置 return_sequences=True 参数使得LSTM的输出形状为三维。这样就可以作为下一层的输入。代码实现:

model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(n_steps,
n_features)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

1.4 Bidirectional LSTM

在一些序列预测问题上,允许LSTM模型向前和向后学习输入序列,并将两种解释连接起来是有益的。这称为双向LSTM。我们可以通过将第一个隐藏层包装在一个称为双向的包装层中来实现用于单变量时间序列预测的双向LSTM。代码实现:

# define model
model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(n_steps, n_features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

1.5 CNN-LSTM

卷积神经网络,简称CNN,是为处理二维图像数据而开发的一种神经网络。CNN可以非常有效地从一维序列数据(如单变量时间序列数据)中自动提取和学习特征。CNN模型可用于具有LSTM后端的混合模型中,其中CNN用于解释输入的子序列,这些子序列作为一个序列提供给LSTM模型来解释。这种混合模型称为CNN-LSTM。

第一步是将输入序列分割成可以由CNN模型处理的子序列。例如,我们可以首先将单变量时间序列数据分成输入/输出样本,其中四个步骤作为输入,一个步骤作为输出。然后,每个样本可以分成两个子样本,每个子样本有两个时间步。CNN可以解释两个时间步的每个子序列,并将子序列的时间序列解释提供给LSTM模型作为输入进行处理。我们可以将其参数化,并将子序列的数量定义为 n_seq,将每个子序列的时间步数定义为n_steps。然后可以对输入数据进行整形,使其具有所需的结构:[示例、子序列、时间步、功能]([samples, subsequences, timesteps, features])。例如:

n_steps = 4
# 划分成样本
X, y = split_sequence(raw_seq, n_steps)
# 重塑形状 [samples, timesteps] 变为 [samples, subsequences, timesteps, features]
n_features = 1
n_seq = 2
n_steps = 2
X = X.reshape((X.shape[0], n_seq, n_steps, n_features)

我们希望在分别读取每个数据子序列时重用相同的CNN模型。这可以通过将整个CNN模型包装在一个 Keras API TimeDistributed 包装器中来实现,该包装器将对每个输入数据子序列应用一次整个模型。定义的CNN模型有一个用于读取子序列的卷积层,该子序列需要指定filter的数目和kernel_size的大小。filter的数目是对输入序列的读取或解释的数目。kernel_size是输入序列的每个读取操作包含的时间步数。卷积层之后是最大池化层,它将filter提取的特征图映射到其大小的1/4,其中包含最显著的特征。然后,这些结构被展平成一个一维向量,用作LSTM层的单个输入时间步。代码实现:

model.add(TimeDistributed(Conv1D(filters=64, kernel_size=1, activation='relu'), input_shape=(None, n_steps, n_features)))
model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
model.add(TimeDistributed(Flatten()))model.add(LSTM(50, activation='relu'))
model.add(Dense(1)

1.6 ConvLSTM

与CNN-LSTM相关的一种LSTM是ConvLSTM,它将输入的卷积读取直接构建到每个LSTM单元中。该方法可用于二维时空数据的读取,也可用于单变量时间序列预测。该层期望输入为二维图像序列,因此输入数据的形状必须是:[样本、时间步长、行、列、特征]( [samples, timesteps, rows, columns, features])

我们可以将每个样本分割成子序列,其中 timesteps 将成为子序列的数量,或 n seq,而列将是每个子序列的时间步长,或 n_steps。对于一维数据,rows 固定为1。分析清楚后,接下来重塑样本。代码实现:

n_steps = 4
# 将序列数据划分成样本
X, y = split_sequence(raw_seq, n_steps)
# 重塑形状 [samples, timesteps] 变为 [samples, timesteps, rows, columns, features]
n_features = 1
n_seq = 2
n_steps = 2
X = X.reshape((X.shape[0], n_seq, 1, n_steps, n_features))

我们可以根据过滤器的数量将ConvLSTM定义为单层,根据 (rows, columns) 将其定义为二维 kernel_size 的大小。对于一维数据,在核函数中,rows 总是固定为1。在解释和预测模型之前,模型的输出必须被展平。

model.add(ConvLSTM2D(filters=64, kernel_size=(1,2), activation='relu', input_shape=(n_seq,
1, n_steps, n_features)))
model.add(Flatten())

以上五个模型的完整代码:

import numpy as np
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, LSTM, Flatten, Bidirectional
from tensorflow.keras.layers import Conv1D, TimeDistributed, MaxPooling1D
from tensorflow.keras.layers import ConvLSTM2Dclass UnivariateModels:'''单变量时间序列LSTM模型'''def __init__(self, sequence, test_seq, n_seq, n_steps, sw_width, features, epochs_num, verbose_set, flag): self.sequence = sequenceself.test_seq = test_seqself.sw_width = sw_widthself.features = featuresself.epochs_num = epochs_numself.verbose_set = verbose_setself.flag = flagself.X, self.y = [], []self.n_seq = n_seqself.n_steps = n_steps       def split_sequence(self):for i in range(len(self.sequence)):# 找到最后一个元素的索引end_index = i + self.sw_width# 如果最后一个滑动窗口中的最后一个元素的索引大于序列中最后一个元素的索引则丢弃该样本if end_index > len(self.sequence) - 1:break# 实现以滑动步长为1(因为是for循环),窗口宽度为self.sw_width的滑动步长取值seq_x, seq_y = self.sequence[i:end_index], self.sequence[end_index]self.X.append(seq_x)self.y.append(seq_y)self.X, self.y = np.array(self.X), np.array(self.y)for i in range(len(self.X)):print(self.X[i], self.y[i])if self.flag == 1:self.X = self.X.reshape((self.X.shape[0], self.n_seq, self.n_steps, self.features))elif self.flag == 2:self.X = self.X.reshape((self.X.shape[0], self.n_seq, 1, self.n_steps, self.features))else:self.X = self.X.reshape((self.X.shape[0], self.X.shape[1], self.features))print('X:\n{}\ny:\n{}\n'.format(self.X, self.y))print('X.shape:{}, y.shape:{}\n'.format(self.X.shape, self.y.shape))return self.X, self.ydef vanilla_lstm(self):model = Sequential()model.add(LSTM(50, activation='relu', input_shape=(self.sw_width, self.features)))model.add(Dense(1))model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])print(model.summary())history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')#         model = Sequential()
#         model.add(LSTM(50, activation='relu', input_shape=(self.sw_width, self.features),
#                        # 其它参数配置
#                        recurrent_activation='sigmoid', use_bias=True, kernel_initializer='glorot_uniform',
#                        recurrent_initializer='orthogonal', bias_initializer='zeros', unit_forget_bias=True, kernel_regularizer=None,
#                        recurrent_regularizer=None, bias_regularizer=None, kernel_constraint=None, recurrent_constraint=None,
#                        bias_constraint=None, dropout=0.0, recurrent_dropout=0.0, implementation=2))#         model.add(Dense(units=1,
#                         # 其它参数配置
#                         activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros',
#                         kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None))#         model.compile(optimizer='adam', loss='mse',
#                       # 其它参数配置
#                       metrics=None, loss_weights=None, sample_weight_mode=None, weighted_metrics=None, target_tensors=None)
#         print(model.summary())#         history = model.fit(self.X, self.y, self.epochs_num, self.verbose_set,
#                   # 其它参数配置
#                   callbacks=None, validation_split=0.0, validation_data=None, shuffle=True, class_weight=None, sample_weight=None,
#                   initial_epoch=0, steps_per_epoch=None, validation_steps=None, validation_freq=1, max_queue_size=10, workers=1, use_multiprocessing=False)#         model.predict(self.test_seq, verbose=self.verbose_set,
#                       # 其它参数配置
#                       steps=None, callbacks=None, max_queue_size=10, workers=1, use_multiprocessing=False)def stacked_lstm(self):model = Sequential()model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(self.sw_width, self.features)))model.add(LSTM(50, activation='relu'))model.add(Dense(1))model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')def bidirectional_lstm(self):model = Sequential()model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(self.sw_width, self.features)))model.add(Dense(1))model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')def cnn_lstm(self):model = Sequential()model.add(TimeDistributed(Conv1D(filters=64, kernel_size=1, activation='relu'),input_shape=(None, self.n_steps, self.features)))model.add(TimeDistributed(MaxPooling1D(pool_size=2)))model.add(TimeDistributed(Flatten()))model.add(LSTM(50, activation='relu'))model.add(Dense(1))model.compile(optimizer='adam', loss='mse',metrics=['accuracy'])history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')def conv_lstm(self):model = Sequential()model.add(ConvLSTM2D(filters=64, kernel_size=(1,2), activation='relu',input_shape=(self.n_seq, 1, self.n_steps, self.features)))model.add(Flatten())model.add(Dense(1))model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')

前三个模型:

if __name__ == '__main__':single_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]sw_width = 3features = 1n_seq = 2n_steps = 2epochs = 300verbose = 0test_seq = np.array([70, 80, 90])test_seq = test_seq.reshape((1, sw_width, features))UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=0)UnivariateLSTM.split_sequence()UnivariateLSTM.vanilla_lstm()UnivariateLSTM.stacked_lstm()UnivariateLSTM.bidirectional_lstm()

输出:

[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
X:
[[[10][20][30]][[20][30][40]][[30][40][50]][[40][50][60]][[50][60][70]][[60][70][80]]]
y:
[40 50 60 70 80 90]X.shape:(6, 3, 1), y.shape:(6,)Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
lstm (LSTM)                  (None, 50)                10400
_________________________________________________________________
dense (Dense)                (None, 1)                 51
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0
_________________________________________________________________
Nonetrain_acc:0.0
train_loss:603.0445827874324
yhat:[[100.85666]]
-----------------------------train_acc:0.0
train_loss:337.3305027484894
yhat:[[102.78299]]
-----------------------------train_acc:0.0
train_loss:283.14844233221066
yhat:[[100.81099]]
-----------------------------

后两个模型:

if __name__ == '__main__':single_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]sw_width = 4features = 1n_seq = 2n_steps = 2epochs = 500verbose = 0test_seq = np.array([60, 70, 80, 90])test_seq = test_seq.reshape((1, n_seq, n_steps, features))UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=1)UnivariateLSTM.split_sequence()UnivariateLSTM.cnn_lstm()test_seq = test_seq.reshape((1, n_seq, 1, n_steps, features))UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=2)UnivariateLSTM.split_sequence()UnivariateLSTM.conv_lstm()

输出:

[10 20 30 40] 50
[20 30 40 50] 60
[30 40 50 60] 70
[40 50 60 70] 80
[50 60 70 80] 90
X:
[[[[10][20]][[30][40]]][[[20][30]][[40][50]]][[[30][40]][[50][60]]][[[40][50]][[60][70]]][[[50][60]][[70][80]]]]
y:
[50 60 70 80 90]X.shape:(5, 2, 2, 1), y.shape:(5,)train_acc:0.0
train_loss:96.9693284504041
yhat:[[100.737885]]
-----------------------------
[10 20 30 40] 50
[20 30 40 50] 60
[30 40 50 60] 70
[40 50 60 70] 80
[50 60 70 80] 90
X:
[[[[[10][20]]][[[30][40]]]][[[[20][30]]][[[40][50]]]][[[[30][40]]][[[50][60]]]][[[[40][50]]][[[60][70]]]][[[[50][60]]][[[70][80]]]]]
y:
[50 60 70 80 90]X.shape:(5, 2, 1, 2, 1), y.shape:(5,)train_acc:0.0
train_loss:236.70842473191
yhat:[[103.98932]]
-----------------------------

关于多变量的LSTM模型,将在下文介绍。


参考:
https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting/

时间序列预测07:如何开发LSTM实现时间序列预测详解 01 Univariate LSTM相关推荐

  1. 时间序列预测05:CNN时间序列预测模型详解 01 Univariate CNN、Multivariate CNN

    [时间序列预测/分类] 全系列60篇由浅入深的博文汇总:传送门 卷积神经网络模型(CNN)可以应用于时间序列预测.有许多类型的CNN模型可用于每种特定类型的时间序列预测问题.在本介绍了在以TF2.1为 ...

  2. 循环神经网络RNN、LSTM、GRU原理详解

    一.写在前面 这部分内容应该算是近几年发展中最基础的部分了,但是发现自己忘得差不多了,很多细节记得不是很清楚了,故写这篇博客,也希望能够用更简单清晰的思路来把这部分内容说清楚,以此能够帮助更多的朋友, ...

  3. PHP开发中常见的安全问题详解和解决方法

    PHP开发中常见的安全问题详解和解决方法 参考文章: (1)PHP开发中常见的安全问题详解和解决方法 (2)https://www.cnblogs.com/walblog/articles/83313 ...

  4. iOS 开发:『Runtime』详解(二)Method Swizzling

    本文用来介绍 iOS 开发中『Runtime』中的黑魔法Method Swizzling. 通过本文,您将了解到: Method Swizzling(动态方法交换)简介 Method Swizzlin ...

  5. 必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法

    必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法 作者 梓沐啊_(KylinDemons) 版权声明 Copyright © 2021 KylinD ...

  6. 【区块链开发入门】(四) Truffle详解篇2

    由于本篇篇幅较长,因此转为两篇文章.Truffle详解篇篇1请见:link 目录导航页 [区块链开发入门](一) 以太坊的搭建与运行 [区块链开发入门](二) 以太坊的编程接口 [区块链开发入门](三 ...

  7. android什么控件能够输入多行文字,Android开发:文本控件详解——EditText(一)基本属性...

    一.简单实例: EditText输入的文字样式部分的属性,基本都是和TextView中的属性一样. 除此之外,EditText还有自己独有的属性. 二.基本属性: hint 输入框显示的提示文本 te ...

  8. 微信公众号 开发详解01【目的、起名、准备工作、注册流程、后台功能】

    [微信公众号--零基础全面系统教学] [P01-P07]微信公众号 开发详解01[目的.起名.准备工作.注册流程.后台功能] [P08-P12]微信公众号 开发详解02[自动回复.发布文章.自定义菜单 ...

  9. 降低游戏陪玩平台系统前端开发复杂度的方式详解

    优秀的程序员总是能优雅的组织自己的代码,编写思路清晰,组织结构划分合理,从小的功能组件,到大的模块结构,都能通过合理巧妙的搭配,化复杂为简单,并且提升游戏陪玩平台系统运行效率,提高游戏陪玩平台系统代码 ...

最新文章

  1. 提高抗打击能力_如果提高心理抗打击能力?
  2. Android 图形系统之图形缓冲区分配
  3. SpringBoot整合Redis集群版本问题
  4. sql server tcp 信号灯超时时间已到_「图文详解」TCP为啥要3次握手和4次挥手?3次挥手不行吗?...
  5. Android安全笔记-Activity基本概念
  6. .NET 内存管理与垃圾回收:实现IDisposable接口和析构函数
  7. matlab制作强光效果代码,自己动手做一个雷达PPI显示器的动态效果图(附Matlab代码)...
  8. [BZOJ4540][HNOI2016]序列(莫队)
  9. for循环中的setTimeout()
  10. bootstraptable 一条数据跨行_据说它是唐山市拥有奶茶店最多的一条街,你一定也逛过!...
  11. xshell4 选中复制,右键粘贴
  12. 小强统一认证中心开源介绍
  13. python学习态度_python基础一 day29 学习方法(课前谈心)
  14. 仓库盘点好方法,使用安卓盘点机PDA扫描商品条码进行超市盘点
  15. 自动化测试工具 Selenium WebDriver 入门教程
  16. 计算机网络——网络时延
  17. 怎么把图片的边缘弄圆_怎么把图片边角变圆?
  18. 什么是ITCM和DTCM?
  19. do-while,if-else:迷你游戏平台,晋级
  20. create table进阶学习(四)

热门文章

  1. 超级计算机学校,超级计算机排名_计算机院校排名_计算机学校排名
  2. Flutter Widget - LinearGradient 渐变
  3. IAP-应用内购买(部分)
  4. vux踩坑指南 i18n国际化踩坑指南 i18n国际化含demo
  5. javascript高级算法题
  6. [Android] 恐龙百科v2.1 收集大量恐龙图片和资料
  7. 最美公式:世上最伟大的十个公式
  8. 技术沙龙 | 数据库技术大会开务分布式数据库专场
  9. 微信公众平台开发之在线翻译功能
  10. [附源码]Java计算机毕业设计SSM高校学生综合素质测评系统