如何建立时间序列预测的卷积神经网络模型

如何建立时间序列预测的卷积神经网络模型


前言

卷积神经网络模型,简称CNNs,可以应用于时间序列预测。

有许多类型的CNN模型可以用于每一种特定类型的时间序列预测问题。

在本教程中,您将了解如何为一系列标准时间序列预测问题开发一套CNN模型。

本教程的目标是提供关于每种类型的时间序列问题的每个模型的独立示例,作为模板,您可以复制并适应特定的时间序列预测问题。

完成本教程后,您将了解:

  • 如何开发用于单变量时间序列预测的CNN模型。
  • 如何建立多元时间序列预测的CNN模型。
  • 如何建立CNN模型进行多步时间序列预测。

这是一篇大而重要的文章;你可能想把它作为书签,以备将来参考。


提示:以下是本篇文章正文内容,下面案例可供参考

概述

在本文中,我们将探讨如何为时间序列预测开发一套不同类型的CNN模型。

这些模型是在小的人为时间序列问题上演示的,目的是给出所处理的时间序列问题类型的特征。选择的模型配置是任意的,不是针对每个问题进行优化的;这不是目标。

本文分为四个部分,分别是

  1. 单变量CNN模型
  2. 多元CNN模型
  3. 多步CNN模型
  4. 多变量多步CNN模型

单变量CNN模型

尽管传统的CNNs是针对二维图像数据开发的,但CNNs可以用来模拟单变量时间序列预测问题。

单变量时间序列是由具有时间顺序的单个观测序列组成的数据集,需要一个模型从过去的观测序列中学习,以预测序列中的下一个值。

本节分为两部分,分别是:

  1. 数据准备
  2. CNN模型

数据准备

在一个单变量序列可以被建模之前,它必须被准备好。

CNN模型将学习一个函数,该函数将一系列过去的观测值映射为一个输出观测值的输入。因此,观测序列必须转化为多个实例,模型可以从中学习。

考虑一个给定的单变量序列:

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

我们可以将序列分成多个输入/输出模式,称为样本,其中三个时间步用作输入,一个时间步用作输出,用于正在学习的一步预测。

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

下面的split_sequence()函数实现此行为,并将给定的单变量序列拆分为多个样本,其中每个样本都有指定数量的时间步,并且输出是单个时间步。

# split a univariate sequence into samples
def split_sequence(sequence, n_steps):
	X, y = list(), list()
	for i in range(len(sequence)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the sequence
		if end_ix > len(sequence)-1:
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

我们可以在上面的小型人工数据集上演示这个函数。

下面列出了完整的示例。

# univariate data preparation
from numpy import array

# split a univariate sequence into samples
def split_sequence(sequence, n_steps):
	X, y = list(), list()
	for i in range(len(sequence)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the sequence
		if end_ix > len(sequence)-1:
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

# define input sequence
raw_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]
# choose a number of time steps
n_steps = 3
# split into samples
X, y = split_sequence(raw_seq, n_steps)
# summarize the data
for i in range(len(X)):
	print(X[i], y[i])

运行该示例将单变量序列拆分为六个样本,其中每个样本有三个输入时间步和一个输出时间步。

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

既然我们知道了如何准备一个用于建模的单变量序列,那么让我们来看看开发一个CNN模型,它可以学习输入到输出的映射。

CNN模型

一维CNN是一种CNN模型,它有一个卷积隐藏层,在一维序列上运行。在某些情况下,这之后可能是第二卷积层,例如很长的输入序列,然后是池层,其工作是将卷积层的输出提取到最显著的元素。

卷积层和池层之后是一个密集的完全连接层,该层解释了模型卷积部分提取的特征。在卷积层和稠密层之间使用平坦层将特征映射简化为单个一维向量。

我们可以为单变量时间序列预测定义一个一维CNN模型,如下所示。

# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_steps, n_features)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

定义中的关键是输入的形状;这就是模型所期望的每个样本输入的时间步数和特征数。

我们使用的是一个单变量序列,所以特征的数量是一个变量。

作为输入的时间步数是我们在准备数据集作为split_sequence()函数的参数时选择的数。

每个样本的输入形状在第一个隐藏层的定义上的输入形状参数中指定。

我们几乎总是有多个样本,因此,模型希望输入部分的训练数据的具有以下维度或形状:

[samples, timesteps, features]

上一节中的split_sequence()函数将输出具有形状[samples,timesteps]的X,所以我们可以很容易地重塑它,为一个特征增加一个维度

# reshape from [samples, timesteps] into [samples, timesteps, features]
n_features = 1
X = X.reshape((X.shape[0], X.shape[1], n_features))

CNN实际上并没有将数据视为具有时间步长,而是将其视为可以执行卷积读取操作的序列,如一维图像。

在本例中,我们定义了一个具有64个过滤器映射和2个内核大小的卷积层。然后是一个最大池层和一个密集层来解释输入特征。指定了预测单个数值的输出层。

该模型采用有效的Adam随机梯度下降法进行拟合,并采用均方误差(mse)损失函数进行优化。

一旦定义了模型,我们就可以将其拟合到训练数据集上。

# fit model
model.fit(X, y, epochs=1000, verbose=0)

在模型拟合之后,我们可以利用它进行预测。

我们可以通过提供输入来预测序列中的下一个值:

[70, 80, 90]

并期望模型能预测:

[100]

该模型期望输入形状是三维的,具有[样本、时间步长、特征],因此在进行预测之前必须对单个输入样本进行整形。

# demonstrate prediction
x_input = array([70, 80, 90])
x_input = x_input.reshape((1, n_steps, n_features))
yhat = model.predict(x_input, verbose=0)

我们可以将所有这些联系在一起,演示如何开发用于单变量时间序列预测的一维CNN模型,并进行单个预测。

# univariate cnn example
from numpy import array
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D

# split a univariate sequence into samples
def split_sequence(sequence, n_steps):
	X, y = list(), list()
	for i in range(len(sequence)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the sequence
		if end_ix > len(sequence)-1:
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

# define input sequence
raw_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]
# choose a number of time steps
n_steps = 3
# split into samples
X, y = split_sequence(raw_seq, n_steps)
# reshape from [samples, timesteps] into [samples, timesteps, features]
n_features = 1
X = X.reshape((X.shape[0], X.shape[1], n_features))
# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_steps, n_features)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
# fit model
model.fit(X, y, epochs=1000, verbose=0)
# demonstrate prediction
x_input = array([70, 80, 90])
x_input = x_input.reshape((1, n_steps, n_features))
yhat = model.predict(x_input, verbose=0)
print(yhat)

运行示例准备数据、拟合模型并进行预测。

注意:由于算法或计算过程的随机性,或数值精度的差异,结果可能会有所不同。考虑运行该示例几次并比较平均结果。

我们可以看到模型预测了序列中的下一个值。

[[101.67965]]

多元CNN模型

多元时间序列数据是指每个时间步有多个观测值的数据。

对于多元时间序列数据,我们可能需要两种主要模型,它们是:

  1. 多输入系列。
  2. 多并联串联。

多输入序列

一个问题可能有两个或多个并行输入时间序列和一个依赖于输入时间序列的输出时间序列。

输入时间序列是平行的,因为每个序列在相同的时间步长上都有观测值。

我们可以用两个平行输入时间序列的简单例子来说明这一点,其中输出序列是输入序列的简单相加。

# define input sequence
in_seq1 = array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])

我们可以将这三个数据数组重塑为一个数据集,其中每一行是一个时间步长,每一列是一个单独的时间序列。

这是在CSV文件中存储并行时间序列的标准方法。

# convert to [rows, columns] structure
in_seq1 = in_seq1.reshape((len(in_seq1), 1))
in_seq2 = in_seq2.reshape((len(in_seq2), 1))
out_seq = out_seq.reshape((len(out_seq), 1))
# horizontally stack columns
dataset = hstack((in_seq1, in_seq2, out_seq))

下面列出了完整的示例。

# multivariate data preparation
from numpy import array
from numpy import hstack
# define input sequence
in_seq1 = array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])
# convert to [rows, columns] structure
in_seq1 = in_seq1.reshape((len(in_seq1), 1))
in_seq2 = in_seq2.reshape((len(in_seq2), 1))
out_seq = out_seq.reshape((len(out_seq), 1))
# horizontally stack columns
dataset = hstack((in_seq1, in_seq2, out_seq))
print(dataset)

运行该示例打印数据集,每个时间步一行,两个输入和一个输出并行时间序列各一列。

[[ 10  15  25]
 [ 20  25  45]
 [ 30  35  65]
 [ 40  45  85]
 [ 50  55 105]
 [ 60  65 125]
 [ 70  75 145]
 [ 80  85 165]
 [ 90  95 185]]

与单变量时间序列一样,我们必须用输入和输出样本将这些数据构造成样本。

一维CNN模型需要足够的上下文来学习从输入序列到输出值的映射。CNNs可以支持作为独立通道的并行输入时间序列,如图像的红、绿、蓝分量。因此,我们需要将数据分割成样本,保持两个输入序列的观测顺序。

如果我们选择三个输入时间步,那么第一个示例如下所示:

输入:

10, 15
20, 25
30, 35

输出:

65

也就是说,每个并行序列的前三个时间步作为输入提供给模型,并且模型将其与第三个时间步(在本例中为65)的输出序列中的值相关联。

我们可以看到,在将时间序列转换为输入/输出样本以训练模型时,我们将不得不丢弃输出时间序列中的一些值,因为在先前的时间步长中,我们在输入时间序列中没有值。反过来,输入时间步数大小的选择将对使用多少训练数据产生重要影响。

我们可以定义一个名为split_sequences()的函数,该函数将接收一个数据集,正如我们所定义的那样,其中的行表示时间步,列表示并行序列,并返回输入/输出样本。

# split a multivariate sequence into samples
def split_sequences(sequences, n_steps):
	X, y = list(), list()
	for i in range(len(sequences)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the dataset
		if end_ix > len(sequences):
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequences[i:end_ix, :-1], sequences[end_ix-1, -1]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

我们可以在数据集上测试这个函数,使用每个输入时间序列的三个时间步作为输入。

下面列出了完整的示例。

# multivariate data preparation
from numpy import array
from numpy import hstack

# split a multivariate sequence into samples
def split_sequences(sequences, n_steps):
	X, y = list(), list()
	for i in range(len(sequences)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the dataset
		if end_ix > len(sequences):
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequences[i:end_ix, :-1], sequences[end_ix-1, -1]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

# define input sequence
in_seq1 = array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])
# convert to [rows, columns] structure
in_seq1 = in_seq1.reshape((len(in_seq1), 1))
in_seq2 = in_seq2.reshape((len(in_seq2), 1))
out_seq = out_seq.reshape((len(out_seq), 1))
# horizontally stack columns
dataset = hstack((in_seq1, in_seq2, out_seq))
# choose a number of time steps
n_steps = 3
# convert into input/output
X, y = split_sequences(dataset, n_steps)
print(X.shape, y.shape)
# summarize the data
for i in range(len(X)):
	print(X[i], y[i])

运行该示例首先打印X和y组件的形状。

我们可以看到X分量具有三维结构。

第一个维度是样本数,在本例中为7。第二个维度是每个样本的时间步数,在本例中是3,即为函数指定的值。最后,最后一个维度指定了并行时间序列的数量或变量的数量,在本例中,2表示两个并行序列。

这是精确的三维结构所期望的一维CNN作为输入。数据已准备就绪,无需进一步整形即可使用。

然后我们可以看到每个样本的输入和输出都被打印出来,显示了两个输入序列中每个序列的三个时间步以及每个样本的相关输出。

(7, 3, 2) (7,)

[[10 15]
 [20 25]
 [30 35]] 65
[[20 25]
 [30 35]
 [40 45]] 85
[[30 35]
 [40 45]
 [50 55]] 105
[[40 45]
 [50 55]
 [60 65]] 125
[[50 55]
 [60 65]
 [70 75]] 145
[[60 65]
 [70 75]
 [80 85]] 165
[[70 75]
 [80 85]
 [90 95]] 185

我们现在准备在这个数据上拟合一个1dcnn模型,为每个输入样本指定预期的时间步数和特性,在本例中分别是3个和2个。

# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_steps, n_features)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

在进行预测时,该模型期望两个输入时间序列有三个时间步长。

我们可以预测输出序列中的下一个值,提供以下输入值:

80,	 85
90,	 95
100, 105

具有三个时间步长和两个变量的一个样本的形状必须是[1,3,2]。

我们希望序列中的下一个值是100+105或205。

# demonstrate prediction
x_input = array([[80, 85], [90, 95], [100, 105]])
x_input = x_input.reshape((1, n_steps, n_features))
yhat = model.predict(x_input, verbose=0)

下面列出了完整的示例。

# multivariate cnn example
from numpy import array
from numpy import hstack
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D

# split a multivariate sequence into samples
def split_sequences(sequences, n_steps):
	X, y = list(), list()
	for i in range(len(sequences)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the dataset
		if end_ix > len(sequences):
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequences[i:end_ix, :-1], sequences[end_ix-1, -1]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

# define input sequence
in_seq1 = array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])
# convert to [rows, columns] structure
in_seq1 = in_seq1.reshape((len(in_seq1), 1))
in_seq2 = in_seq2.reshape((len(in_seq2), 1))
out_seq = out_seq.reshape((len(out_seq), 1))
# horizontally stack columns
dataset = hstack((in_seq1, in_seq2, out_seq))
# choose a number of time steps
n_steps = 3
# convert into input/output
X, y = split_sequences(dataset, n_steps)
# the dataset knows the number of features, e.g. 2
n_features = X.shape[2]
# define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_steps, n_features)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
# fit model
model.fit(X, y, epochs=1000, verbose=0)
# demonstrate prediction
x_input = array([[80, 85], [90, 95], [100, 105]])
x_input = x_input.reshape((1, n_steps, n_features))
yhat = model.predict(x_input, verbose=0)
print(yhat)

注意:由于算法或计算过程的随机性,或数值精度的差异,结果可能会有所不同。考虑运行该示例几次并比较平均结果。

运行示例准备数据、拟合模型并进行预测。

[[206.0161]]

还有另一种更精细的方法来模拟这个问题。

每个输入序列可以由一个单独的CNN来处理,并且在对输出序列进行预测之前,可以将这些子模型的输出进行组合。

我们可以称之为一个多头CNN模型。它可以提供更多的灵活性或更好的性能,这取决于所建模问题的具体情况。例如,它允许您为每个输入序列配置不同的子模型,例如过滤器映射的数量和内核大小。

这种类型的模型可以使用Keras函数API在Keras中定义。

首先,我们可以将第一个输入模型定义为一个一维CNN,它的输入层要求向量具有n个步长和1个特征。

# first input model
visible1 = Input(shape=(n_steps, n_features))
cnn1 = Conv1D(filters=64, kernel_size=2, activation='relu')(visible1)
cnn1 = MaxPooling1D(pool_size=2)(cnn1)
cnn1 = Flatten()(cnn1)

我们可以用同样的方法定义第二个输入子模型。

# second input model
visible2 = Input(shape=(n_steps, n_features))
cnn2 = Conv1D(filters=64, kernel_size=2, activation='relu')(visible2)
cnn2 = MaxPooling1D(pool_size=2)(cnn2)
cnn2 = Flatten()(cnn2)

既然已经定义了两个输入子模型,我们就可以将每个模型的输出合并成一个长向量,在对输出序列进行预测之前可以对其进行解释。

# merge input models
merge = concatenate([cnn1, cnn2])
dense = Dense(50, activation='relu')(merge)
output = Dense(1)(dense)

然后我们可以将输入和输出连接在一起。

model = Model(inputs=[visible1, visible2], outputs=output)

下图提供了该模型的外观示意图,包括每个层的输入和输出的形状。
在这里插入图片描述此模型要求输入作为两个元素的列表提供,其中列表中的每个元素都包含其中一个子模型的数据。

为了实现这一点,我们可以将三维输入数据分成两个独立的输入数据数组;即从一个具有[7,3,2]形状的数组到两个具有[7,3,1]形状的三维数组

# one time series per head
n_features = 1
# separate input data
X1 = X[:, :, 0].reshape(X.shape[0], X.shape[1], n_features)
X2 = X[:, :, 1].reshape(X.shape[0], X.shape[1], n_features)

然后可以提供这些数据来拟合模型。

# fit model
model.fit([X1, X2], y, epochs=1000, verbose=0)

同样,在进行一步预测时,我们必须将单个样本的数据准备为两个独立的二维数组。

x_input = array([[80, 85], [90, 95], [100, 105]])
x1 = x_input[:, 0].reshape((1, n_steps, n_features))
x2 = x_input[:, 1].reshape((1, n_steps, n_features))

我们可以将所有这些联系在一起;下面列出了完整的示例。

# multivariate multi-headed 1d cnn example
from numpy import array
from numpy import hstack
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
from keras.layers.merge import concatenate

# split a multivariate sequence into samples
def split_sequences(sequences, n_steps):
	X, y = list(), list()
	for i in range(len(sequences)):
		# find the end of this pattern
		end_ix = i + n_steps
		# check if we are beyond the dataset
		if end_ix > len(sequences):
			break
		# gather input and output parts of the pattern
		seq_x, seq_y = sequences[i:end_ix, :-1], sequences[end_ix-1, -1]
		X.append(seq_x)
		y.append(seq_y)
	return array(X), array(y)

# define input sequence
in_seq1 = array([10, 20, 30, 40, 50, 60, 70, 80, 90])
in_seq2 = array([15, 25, 35, 45, 55, 65, 75, 85, 95])
out_seq = array([in_seq1[i]+in_seq2[i] for i in range(len(in_seq1))])
# convert to [rows, columns] structure
in_seq1 = in_seq1.reshape((len(in_seq1), 1))
in_seq2 = in_seq2.reshape((len(in_seq2), 1))
out_seq = out_seq.reshape((len(out_seq), 1))
# horizontally stack columns
dataset = hstack((in_seq1, in_seq2, out_seq))
# choose a number of time steps
n_steps = 3
# convert into input/output
X, y = split_sequences(dataset, n_steps)
# one time series per head
n_features = 1
# separate input data
X1 = X[:, :, 0].reshape(X.shape[0], X.shape[1], n_features)
X2 = X[:, :, 1].reshape(X.shape[0], X.shape[1], n_features)
# first input model
visible1 = Input(shape=(n_steps, n_features))
cnn1 = Conv1D(filters=64, kernel_size=2, activation='relu')(visible1)
cnn1 = MaxPooling1D(pool_size=2)(cnn1)
cnn1 = Flatten()(cnn1)
# second input model
visible2 = Input(shape=(n_steps, n_features))
cnn2 = Conv1D(filters=64, kernel_size=2, activation='relu')(visible2)
cnn2 = MaxPooling1D(pool_size=2)(cnn2)
cnn2 = Flatten()(cnn2)
# merge input models
merge = concatenate([cnn1, cnn2])
dense = Dense(50, activation='relu')(merge)
output = Dense(1)(dense)
model = Model(inputs=[visible1, visible2], outputs=output)
model.compile(optimizer='adam', loss='mse')
# fit model
model.fit([X1, X2], y, epochs=1000, verbose=0)
# demonstrate prediction
x_input = array([[80, 85], [90, 95], [100, 105]])
x1 = x_input[:, 0].reshape((1, n_steps, n_features))
x2 = x_input[:, 1].reshape((1, n_steps, n_features))
yhat = model.predict([x1, x2], verbose=0)
print(yhat)

注意:由于算法或计算过程的随机性,或数值精度的差异,结果可能会有所不同。考虑运行该示例几次并比较平均结果。
运行示例准备数据、拟合模型并进行预测。

[[205.871]]
  • 6
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值