当前位置:   article > 正文

【机器学习】基于卷积LSTM的视频预测

【机器学习】基于卷积LSTM的视频预测

1. 引言

1.1 LSTM是什么

LSTM(Long Short-Term Memory)是一种特殊的循环神经网络(RNN)变体,旨在解决传统RNN在处理长序列数据时遇到的梯度消失和梯度爆炸问题。LSTM通过引入门控机制和细胞状态的概念,使得网络能够捕获长序列中的依赖关系,从而在处理时间序列数据方面表现出色。

1.1.1. 核心结构与原理
  1. 细胞状态(Cell State):LSTM的关键组成部分,类似于信息传送带,贯穿整个链条,只有一些少量的线性交互。信息在上面流传保持不变会很容易。
  2. 门控机制
  • 遗忘门(Forget Gate):决定从细胞状态中丢弃什么信息。它读取上一个隐藏状态 h t − 1 h_{t-1} ht1和当前输入 x t x_t xt,并输出一个介于0和1之间的值,表示对细胞状态的保留程度。
  • 输入门(Input Gate):决定让多少新的信息加入到细胞状态中。它同样读取 h t − 1 h_{t-1} ht1 x t x_t xt,并输出两个值:一个是用于更新值的tanh层,另一个是用于决定哪些值需要更新的sigmoid层。
  • 输出门(Output Gate):基于细胞状态,决定输出什么值。它读取 h t − 1 h_{t-1} ht1 x t x_t xt,并使用sigmoid层来确定细胞状态的哪个部分将用于输出。然后,将细胞状态通过tanh函数(将值规范化到-1和1之间)进行处理,并与sigmoid门的输出相乘,最终确定需要输出的部分。
1.1.2. 优缺点
  1. 优点
  • 改善了RNN中存在的长期依赖问题。
  • 通过各种门函数来将重要特征保留下来,能够有效减缓长序列问题中可能出现的梯度消失或爆炸现象。
  • 在多个领域(如语言建模、机器翻译、语音识别、时间序列预测等)中表现出色。
  1. 缺点
  • 并行处理上存在劣势,只能从前到后,与一些最新的网络相对效果一般。
  • 对于非常长的序列,LSTM仍然可能面临挑战,尽管其性能优于传统RNN。
  • 计算费时,由于其内部结构的复杂性,训练效率在同等算力下较传统RNN低很多。
1.1.3. 应用领域

LSTM在多个领域都有广泛的应用,包括但不限于:

  • 语言建模与生成
  • 机器翻译
  • 语音识别
  • 文本情感分析
  • 时间序列预测(如股票价格预测、天气预测等)

LSTM作为一种强大的循环神经网络变体,通过其独特的门控机制和细胞状态设计,成功解决了传统RNN在处理长序列数据时遇到的问题。尽管存在一些缺点,但LSTM在多个领域都展现出了出色的性能,是处理时间序列数据的重要工具之一。

1.2 视频预测简介

视频预测是指网络模型在给定某个视频序列的前m帧(m为正整数)的情况下,预测接下来的n帧(n为正整数)。这个过程涉及到对画面中的主体的运动趋势及其背景的相关变化做出尽可能准确的预测。
相比于二维图像而言,视频多了一个时间的维度,引入了关注对象的运动变化,使得呈现的视觉信息更加全面。视频预测技术有助于人们通过预测未来的视频帧来更好地理解视频内容,并在多种应用中发挥作用。

1.2.1.主要技术
  1. 第一人称视频预测技术:利用历史记录的第一人称视频数据,通过模型的学习和推理,预测未来的视角和场景变化。这种技术可以填补第一人称视频中的时间空白,提供连续、流畅的观看体验。
  2. 深度学习技术:是实现视频预测的重要工具。基于深度学习的模型,如循环神经网络(RNN)、卷积神经网络(CNN)和生成对抗网络(GAN)等,可以通过学习历史视频数据中的时间和空间模式,推断出未来视角的变化。
1.2.2. 应用场景
  1. 安防监控:AI视频预警技术可应用于安防监控系统中,通过对视频数据进行智能分析,实时监测并识别出异常情况,如人员聚集、异常行为等,提升安防监控效果。
  2. 交通管理:在城市交通管理方面,AI视频预警技术可以监测交通流量、车辆行驶状态等信息,及时发现交通拥堵、事故等问题,提高交通管理的精细化水平。
  3. 环境监测:通过对视频数据中的环境信息进行分析,及时发现环境污染、气象异常等情况,为环境保护提供数据支持。
  4. 工业生产:监测生产设备的运行状态、生产线上的异常情况等,及时预警并减少生产事故的发生。
  5. 医疗保健:监测医疗设备的运行状态、患者的情况等,及时发现异常情况并提醒医护人员,提高医疗保健的效率。

1.2.3. 发展趋势

  1. 智能化提升:未来,视频预测技术将实现智能化提升,通过深度学习、神经网络等技术手段,实现对视频数据的更加精准的分析和识别,提高预测准确率。
  2. 跨行业应用:视频预测技术将在更多的领域得到应用,如智慧城市建设、教育领域、金融行业等,为各行各业带来更多的创新和便利。
  3. 数据分析优化:视频预测技术将结合大数据分析,通过对历史数据和趋势的分析,实现对未来可能发生的情况进行预测和预警,提前做好应对措施。

视频预测技术作为一种新兴的技术手段,在多个领域都具有广泛的应用前景。通过不断的技术创新和应用拓展,视频预测技术将在未来的发展中展现出更大的潜力和价值。

1.3 LSTM用于视频预测的优势和局限

1.3.1. 优势

STM(这里可能是指LSTM或其某种变体,如ConvLSTM)用于视频预测的优势主要体现在以下几个方面:

  1. 时空特征捕捉:STM(特别是ConvLSTM)能够同时捕捉视频数据的空间和时间特征。这种能力对于视频预测至关重要,因为视频不仅包含帧内的空间信息(如对象的形状、位置),还包含帧间的时间信息(如对象的运动轨迹)。STM通过其内部的循环机制和卷积操作,能够有效地提取和利用这些时空特征,从而提高视频预测的准确性。

  2. 长期依赖建模:STM(特别是LSTM)具有捕获长期依赖关系的能力。在视频预测中,某些对象或事件的行为可能受到之前较长时间内发生的事件的影响。STM的记忆单元和门控机制使得模型能够记住并利用这些长期依赖信息,从而更准确地预测未来的视频帧。

  3. 处理复杂模式:STM具有强大的非线性建模能力,能够处理视频数据中的复杂模式和趋势。视频数据通常包含各种动态和静态的场景、对象以及它们之间的相互作用,这些复杂的模式对于传统的线性模型来说很难处理。而STM则能够学习这些复杂的非线性关系,并据此进行准确的预测。

  4. 端到端学习:STM可以实现端到端的学习,直接从原始视频数据中提取特征并进行预测,无需进行繁琐的特征工程。这种端到端的学习方式简化了视频预测的流程,提高了模型的自动化程度和效率。

  5. 扩展性和灵活性:STM可以与其他网络结构(如CNN、Transformer等)结合使用,形成更强大的视频预测模型。此外,STM的模型结构也可以进行灵活的调整和优化,以适应不同的视频预测任务和数据特点。这种扩展性和灵活性使得STM能够适应各种复杂的视频预测场景。

  6. 鲁棒性: STM对于噪声和异常值具有一定的鲁棒性。在视频数据中,由于各种因素的影响(如光照变化、遮挡、运动模糊等),可能会存在噪声和异常值。STM通过其内部的门控机制和记忆单元,能够在一定程度上抑制这些噪声和异常值的影响,从而提高视频预测的鲁棒性。

STM在视频预测领域具有显著的优势,包括时空特征捕捉、长期依赖建模、处理复杂模式、端到端学习、扩展性和灵活性以及鲁棒性等。这些优势使得STM成为视频预测领域的重要工具之一。

1.3.2. 局限性

LSTM用于视频预测的局限主要体现在以下几个方面:

  1. 计算复杂度和资源消耗:LSTM由于其复杂的结构和大量的参数,导致在处理高维、大规模的视频数据时计算复杂度较高。尤其是在处理高分辨率、高帧率的视频时,需要大量的计算资源,包括内存和计算时间。这可能会限制其在实时视频预测等场景中的应用。

  2. 长期依赖的局限性:尽管LSTM设计用于捕捉长期依赖关系,但在处理非常长的视频序列时,仍然存在局限性。因为LSTM的循环结构使得信息在传递过程中可能会逐渐衰减或消失,导致模型难以捕捉非常远的依赖关系。

  3. 参数数量与过拟合风险:LSTM包含大量的参数,特别是在处理视频数据时,由于需要同时考虑空间和时间维度,模型参数数量可能急剧增加。这增加了过拟合的风险,尤其是在训练数据有限的情况下。如果没有足够的正则化措施或充足的训练数据,模型可能会过度拟合训练数据,导致在测试集上表现不佳。

  4. 解释性不足:与一些传统模型相比,LSTM的决策过程相对难以解释。在视频预测中,这可能导致难以理解和解释模型为什么做出特定的预测。这对于一些需要高度解释性的应用场景(如医疗诊断、自动驾驶等)来说可能是一个问题。

  5. 固定时间步长:LSTM在处理视频数据时通常使用固定的时间步长(即每帧作为一个时间步)。然而,在实际情况中,视频中的事件和动作可能具有不同的时间尺度。使用固定的时间步长可能无法充分捕捉这些不同尺度的时间依赖关系。

  6. 对噪声和异常值的敏感性:视频数据中可能存在噪声和异常值,这些因素可能对LSTM的性能产生负面影响。尽管LSTM具有一定的鲁棒性,但在某些情况下,噪声和异常值可能导致模型预测出错或不稳定。

  7. 难以处理高动态性的视频:对于高动态性的视频(如快速运动、剧烈变化等),LSTM可能难以准确捕捉其中的变化模式。这是因为LSTM的循环结构在处理快速变化的信息时可能存在一定的延迟和滞后效应。

尽管LSTM在视频预测中具有显著的优势,但也存在一些局限性。在实际应用中,需要根据具体场景和需求来选择合适的模型结构和参数设置,并采取适当的措施来克服这些局限性。

2. LSTM视频预测过程

2.1. 设置

import numpy as np
import matplotlib.pyplot as plt

import keras
from keras import layers

import io
import imageio
from IPython.display import Image, display
from ipywidgets import widgets, Layout, HBox
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.2. 数据预处理

2.2.1.构建数据集

当我们着手构建数据集时,以Moving MNIST数据集为例,我们的工作流程通常包括以下几个步骤:

  1. 数据获取:首先,我们需要下载Moving MNIST数据集,这是我们进行后续工作的基础。

  2. 数据集划分:下载完成后,我们需要将数据集划分为训练集和验证集。这一步是为了确保模型在训练过程中能够学习到数据的一般规律,并通过验证集来评估模型的泛化能力。

  3. 数据预处理:接下来,我们需要对数据进行预处理。这可能包括归一化、去噪、调整数据格式等步骤,以确保数据适合模型的输入要求。

  4. 构建输入输出对:对于帧序列预测任务,我们需要构建模型的输入和输出。在这个例子中,模型将使用前一帧(记为f_n)来预测下一帧(记为f_(n + 1))。这意味着我们需要调整数据,以便每一对输入和输出帧之间存在一定的时间偏移。

  5. 偏移处理:为了实现帧预测,我们需要对数据进行偏移处理。这通常意味着我们将输入帧x_n与输出帧y_(n + 1)配对,以便模型能够学习如何根据当前帧预测下一帧。

通过这样的流程,我们可以构建一个适合进行下一帧预测任务的数据集。这不仅涉及到技术操作,还需要对数据集的结构和模型的需求有深入的理解。

Moving MNIST数据集简介

Moving MNIST数据集是一个用于机器学习和计算机视觉研究的数据集,它是MNIST数据集的变体,增加了时间维度和运动特性。以下是关于Moving MNIST数据集的详细简介:

  1. 数据集特点

    • 包含10,000个视频序列,每个序列由20帧组成,展示了两个数字在64x64像素的帧内移动。
    • 数字经常相互交错,并从帧的边缘弹出,增加了数据集的复杂性和挑战性。
    • 每个视频序列的前十帧通常被视为输入(input),后十帧被视为输出(target),用于训练和测试时间序列预测模型。
  2. 数据规模

    • 总共包含10,000个包含20帧的视频序列。
    • 每个帧的大小为64x64像素。
  3. 数据来源

    • 基于原始的MNIST数据集,通过添加时间维度和运动特性生成。
    • MNIST数据集本身包含60,000个训练样本和10,000个测试样本的手写数字图像。
  4. 数据用途

    • 主要用于测试机器学习算法在处理时间序列数据方面的能力,特别是视频预测和时空序列预测任务。
    • 通过预测下一个或多个帧来评估模型在捕捉对象运动和场景变化方面的能力。
  5. 数据格式

    • 通常以NumPy文件(如.npy格式)存储,包含多个视频序列的像素数据。
    • 可以通过专门的加载和预处理脚本来读取和准备数据以供模型训练。
  6. 数据预处理

    • 在使用之前,数据通常需要进行归一化、裁剪或缩放等预处理步骤,以适应不同的模型架构和训练要求。
    • 对于视频预测任务,数据需要被组织成适当的输入和输出对,以便模型可以学习从当前帧预测未来帧的能力。

通过利用Moving MNIST数据集,研究人员可以开发和测试各种时间序列预测模型,包括循环神经网络(RNN)、长短期记忆网络(LSTM)和卷积长短期记忆网络(ConvLSTM)等,以改进视频预测和其他相关任务的性能。

数据集处理代码
# 下载并加载Moving MNIST数据集。
fpath = keras.utils.get_file(
    "moving_mnist.npy",  # 指定本地保存的文件名
    "http://www.cs.toronto.edu/~nitish/unsupervised_video/mnist_test_seq.npy",  # 数据集的URL
)
dataset = np.load(fpath)  # 加载数据集文件

# 交换表示帧数和数据样本数的轴,以适应后续处理。
dataset = np.swapaxes(dataset, 0, 1)
# 从10000个样本中选择1000个作为数据集。
dataset = dataset[:1000, ...]
# 由于图像是灰度的,添加一个通道维度。
dataset = np.expand_dims(dataset, axis=-1)

# 使用索引来划分训练集和验证集,以优化内存使用。
indexes = np.arange(dataset.shape[0])
np.random.shuffle(indexes)  # 打乱索引顺序
train_index = indexes[: int(0.9 * dataset.shape[0])]  # 90%的数据作为训练集
val_index = indexes[int(0.9 * dataset.shape[0]) :]  # 剩余10%的数据作为验证集
train_dataset = dataset[train_index]  # 根据索引获取训练集数据
val_dataset = dataset[val_index]  # 根据索引获取验证集数据

# 将数据归一化到0-1范围内。
train_dataset = train_dataset / 255
val_dataset = val_dataset / 255

# 定义一个辅助函数来创建偏移帧,其中`x`是0到n-1帧,`y`是1到n帧。
def create_shifted_frames(data):
    x = data[:, 0 : data.shape[1] - 1, :, :]  # 获取从第0帧到倒数第二帧的数据
    y = data[:, 1 : data.shape[1], :, :]  # 获取从第1帧到最后一帧的数据
    return x, y

# 对训练集和验证集应用处理函数。
x_train, y_train = create_shifted_frames(train_dataset)
x_val, y_val = create_shifted_frames(val_dataset)

# 检查数据集的形状。
print("训练集数据形状:" + str(x_train.shape) + ", " + str(y_train.shape))
print("验证集数据形状:" + str(x_val.shape) + ", " + str(y_val.shape))
  • 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

上述代码脚本主要执行:

  1. 数据获取:首先,脚本通过keras.utils.get_file函数从指定的URL下载Moving MNIST数据集,并使用np.load函数加载本地.npy格式的文件。

  2. 数据预处理:接着,脚本对数据集进行预处理,包括交换数据轴以适应后续操作,选择1000个样本进行处理,并为灰度图像添加通道维度。

  3. 数据集划分:然后,脚本通过随机打乱样本索引并按照90%和10%的比例划分训练集和验证集,以优化内存使用并评估模型的泛化能力。

  4. 数据归一化:将图像像素值从0-255归一化到0-1范围,以便于模型处理。

  5. 帧处理:定义了一个辅助函数create_shifted_frames,用于创建模型训练所需的输入和输出帧。输入帧是序列中除了最后一帧的所有帧,输出帧是序列中除了第一帧的所有帧。

  6. 应用处理:将create_shifted_frames函数应用于训练集和验证集,生成具有时间偏移的输入输出对。

  7. 数据集检查:最后,脚本打印出训练集和验证集的输入和输出帧的形状,以验证数据集的完整性和正确性。

整个过程的目的是准备和处理数据,使其适合于训练一个用于下一帧预测的深度学习模型。通过创建具有时间偏移的输入输出对,模型可以学习如何根据一系列连续图像帧来预测下一帧的图像。

2.2.2. 数据可视化

我们的数据由帧序列组成,每个帧都被用来预测接下来的帧。让我们来查看一些这样的连续帧。

# 构建一个图形,用于可视化图像。
fig, axes = plt.subplots(4, 5, figsize=(10, 8))

# 为一个随机数据示例绘制序列图像。
# 从训练数据集中随机选择一个样本。
data_choice = np.random.choice(range(len(train_dataset)), size=1)[0]

# 遍历所有子图轴,并绘制图像。
for idx, ax in enumerate(axes.flat):
    # 绘制选定样本的第idx帧图像,使用灰度色彩映射。
    ax.imshow(np.squeeze(train_dataset[data_choice][idx]), cmap="gray")
    # 设置子图的标题为“第idx帧”。
    ax.set_title(f"帧 {idx + 1}")
    # 关闭子图的坐标轴显示。
    ax.axis("off")

# 打印信息并显示图形。
print(f"正在显示示例 {data_choice} 的帧。")
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上述代码使用matplotlib库来可视化Moving MNIST数据集中的图像序列:

  1. 创建图形和子图

    • plt.subplots(4, 5, figsize=(10, 8)):创建一个图形(fig)和子图网格,4行5列共20个子图,整个图形的大小设置为宽10英寸、高8英寸。
  2. 随机选择数据样本

    • np.random.choice(range(len(train_dataset)), size=1)[0]:从训练数据集中随机选择一个样本的索引。
  3. 绘制图像序列

    • 通过for循环和enumerate函数遍历每个子图轴(axes.flat),enumerate同时提供了子图索引idx和子图轴ax
    • np.squeeze(train_dataset[data_choice][idx]):从训练数据集中提取所选样本的第idx帧图像,并去除单维度的轴。
    • ax.imshow(..., cmap="gray"):在每个子图上使用灰度色彩映射显示图像。
  4. 设置子图标题和关闭坐标轴

    • ax.set_title(f"Frame {idx + 1}"):为每个子图设置标题,显示当前是序列中的第几帧。
    • ax.axis("off"):关闭子图的坐标轴显示,使图像显示更加清晰。
  5. 打印信息和显示图形

    • print(f"Displaying frames for example {data_choice}."):打印一条信息,说明正在显示哪个样本的图像序列。
    • plt.show():展示构建好的图形。

整个脚本的目的是将一个随机样本的20帧图像以子图的形式展示出来,每帧图像都是灰度显示,没有坐标轴干扰,便于观察图像内容。这对于理解数据集的特点、检查数据预处理的效果以及可视化模型预测结果都是非常有用的。
在这里插入图片描述

2.3. 建立构建

2.3.1. 构建基本模型

为了构建一个卷积长短期记忆(Convolutional LSTM,ConvLSTM)模型,我们将使用ConvLSTM2D层。这个层将接受形状为(batch_size, num_frames, width, height, channels)的输入,并返回具有相同形状的预测视频。

# 构建输入层,不指定确切的帧大小。
inp = layers.Input(shape=(None, *x_train.shape[2:]))

# 构建3个带有批量归一化的ConvLSTM2D层,
# 然后是一个用于时空输出的Conv3D层。
x = layers.ConvLSTM2D(
    filters=64,  # 卷积层的过滤器数量
    kernel_size=(5, 5),  # 卷积核的大小
    padding="same",  # 填充方式,保持输出尺寸不变
    return_sequences=True,  # 返回序列
    activation="relu",  # 激活函数
)(inp)
x = layers.BatchNormalization()(x)  # 批量归一化
x = layers.ConvLSTM2D(
    filters=64,
    kernel_size=(3, 3),
    padding="same",
    return_sequences=True,
    activation="relu",
)(x)
x = layers.BatchNormalization()(x)
x = layers.ConvLSTM2D(
    filters=64,
    kernel_size=(1, 1),
    padding="same",
    return_sequences=True,
    activation="relu",
)(x)

# 最后一个Conv3D层用于生成最终的时空输出。
x = layers.Conv3D(
    filters=1,  # 输出通道数
    kernel_size=(3, 3, 3),  # 3D卷积核大小
    activation="sigmoid",  # Sigmoid激活函数用于二分类问题
    padding="same"  # 填充方式
)(x)

# 接下来,构建完整的模型并编译它。
model = keras.models.Model(inp, x)
model.compile(
    loss=keras.losses.binary_crossentropy,  # 损失函数
    optimizer=keras.optimizers.Adam(),  # 优化器
)
  • 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

上述代码是使用Keras框架构建一个深度学习模型的过程,具体步骤和组件如下:

  1. 输入层定义

    • layers.Input(shape=(None, *x_train.shape[2:])):定义模型的输入层。这里的shape参数指定了输入数据的形状,None表示可以处理任意数量的帧,x_train.shape[2:]获取了除了帧数之外的维度,即图像的高度和宽度。
  2. 构建ConvLSTM2D层

    • layers.ConvLSTM2D:定义了一个二维卷积LSTM层,用于处理视频数据的时间序列特征。
    • filters=64:指定了卷积层中的过滤器数量,即特征图的维度。
    • kernel_size=(5, 5):定义了卷积核的大小。
    • padding="same":指定填充方式,使得输入和输出的空间维度保持一致。
    • return_sequences=True:指定该层的输出是一个序列。
    • activation="relu":指定激活函数为ReLU。
  3. 批量归一化

    • layers.BatchNormalization():在卷积层之后应用批量归一化,有助于加速模型训练并提高模型稳定性。
  4. 堆叠ConvLSTM2D层

    • 代码中堆叠了三个ConvLSTM2D层,每个层后面都跟着批量归一化,以提取不同级别的时空特征。
  5. 构建Conv3D层

    • layers.Conv3D:定义了一个三维卷积层,用于将提取的时空特征转换为最终的预测输出。
    • filters=1:指定输出通道数为1,因为最终的预测是一个单通道的图像。
    • kernel_size=(3, 3, 3):定义了3D卷积核的大小。
    • activation="sigmoid":使用Sigmoid激活函数,适用于二分类问题,输出值在0到1之间。
  6. 模型构建和编译

    • keras.models.Model(inp, x):使用输入层inp和最后一个卷积层的输出x构建完整的模型。
    • model.compile(...):编译模型,指定损失函数为二元交叉熵binary_crossentropy,优化器为Adam。

整个模型的设计考虑了视频数据的时间序列特性,通过ConvLSTM2D层捕捉时间动态,并通过Conv3D层生成预测的下一帧图像。这种类型的模型常用于视频预测、动作识别等任务。

2.3.2. 训练模型
# 定义一些回调函数以改进训练过程
# 早期停止:当验证损失在连续多个epoch内没有改善时,停止训练
early_stopping = keras.callbacks.EarlyStopping(monitor="val_loss", patience=10)  # 监视验证损失,若连续10个epoch没有改善则停止训练

# 学习率降低:当验证损失在连续多个epoch内没有改善时,降低学习率
reduce_lr = keras.callbacks.ReduceLROnPlateau(monitor="val_loss", patience=5)  # 监视验证损失,若连续5个epoch没有改善则降低学习率

# 定义可修改的训练超参数
epochs = 20  # 训练轮次
batch_size = 5  # 批次大小

# 将模型拟合到训练数据上
# 使用fit方法训练模型
model.fit(
    x_train,  # 训练数据输入
    y_train,  # 训练数据标签
    batch_size=batch_size,  # 批次大小
    epochs=epochs,  # 训练轮次
    validation_data=(x_val, y_val),  # 验证数据,用于在训练过程中评估模型性能
    callbacks=[early_stopping, reduce_lr],  # 回调函数列表,包含早期停止和学习率降低
    # 其他可能的参数如shuffle(是否打乱数据)、verbose(日志显示模式)等
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在上面的代码中,keras.callbacks 模块被用来定义回调函数,这些函数可以在训练过程中的特定时间点被调用,以便对训练过程进行干预。EarlyStopping 回调函数会在验证损失连续多个epoch没有改善时停止训练,以避免过拟合。ReduceLROnPlateau 回调函数会在验证损失连续多个epoch没有改善时降低学习率,这有时可以帮助模型在训练后期找到更好的局部最小值。

model.fit() 方法中,x_trainy_train 分别是训练数据和对应的标签,batch_sizeepochs 定义了训练的批次大小和总轮次。validation_data 参数用来提供验证集数据,以便在训练过程中评估模型的性能。callbacks 参数接受一个回调函数列表,这些函数会在训练的不同阶段被调用。

2.3.3. 帧预测可视化

现在我们的模型已经构建并训练完成,我们可以基于一个新的视频生成一些示例帧预测。

我们将从验证集中随机选择一个示例,并选取其中的前十个帧。然后,我们可以让模型预测接下来的10个新帧,并将这些预测与真实帧(ground truth)进行比较。

# 从验证数据集中随机选择一个样本。
example = val_dataset[np.random.choice(range(len(val_dataset)), size=1)[0]]

# 选取样本中的前10帧和后10帧。
frames = example[:10, ...]  # 前10帧作为初始输入
original_frames = example[10:, ...]  # 后10帧作为原始参考

# 预测新的10帧序列。
for _ in range(10):
    # 将当前帧序列扩展为模型的输入格式,并进行预测。
    new_prediction = model.predict(np.expand_dims(frames, axis=0))
    # 去除预测结果的单维度轴。
    new_prediction = np.squeeze(new_prediction, axis=0)
    # 获取预测序列中的最后一个帧作为下一个输入帧。
    predicted_frame = np.expand_dims(new_prediction[-1, ...], axis=0)
    
    # 将预测帧添加到输入帧序列中,准备下一次预测。
    frames = np.concatenate((frames, predicted_frame), axis=0)

# 构建一个图形,用于展示原始帧和新预测的帧。
fig, axes = plt.subplots(2, 10, figsize=(20, 4))

# 绘制原始帧。
for idx, ax in enumerate(axes[0]):
    ax.imshow(np.squeeze(original_frames[idx]), cmap="gray")  # 显示图像
    ax.set_title(f"原始帧 {idx + 11}")  # 设置标题
    ax.axis("off")  # 关闭坐标轴

# 绘制新预测的帧。
new_frames = frames[10:, ...]  # 取新预测的10帧
for idx, ax in enumerate(axes[1]):
    ax.imshow(np.squeeze(new_frames[idx]), cmap="gray")  # 显示图像
    ax.set_title(f"预测帧 {idx + 11}")  # 设置标题
    ax.axis("off")  # 关闭坐标轴

# 展示图形。
plt.show()
  • 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

这段代码的目的是验证模型的预测能力,通过比较原始帧和模型预测的帧,可以直观地评估模型的性能。

  1. 随机选择样本:从验证数据集中随机选取一个样本,用于展示模型的预测效果。

  2. 选取帧序列:从选中样本中提取前10帧作为模型的初始输入,后10帧作为原始帧序列的参考。

  3. 预测新帧:通过循环,使用模型对初始帧序列进行连续预测,每次循环使用模型的预测结果作为下一次预测的输入。

  4. 帧序列扩展:在每次循环中,将模型预测的最后一个帧添加到输入帧序列中,以生成更长的帧序列。

  5. 构建展示图形:创建一个包含两行10列的子图,用于并排展示原始帧和预测帧。

  6. 绘制帧图像:在第一行子图中绘制原始帧序列,在第二行子图中绘制模型预测的帧序列。

  7. 展示图形:使用plt.show()函数展示构建好的图形,以便于直观比较原始帧和预测帧之间的差异。
    在这里插入图片描述

2.3.4.预测视频

为了直观地展示模型在视频帧预测方面的性能,我们可以从验证集中选择几个样本,并使用这些样本来创建GIF动画。这些GIF动画将包含原始视频帧和模型预测的帧,以便我们能够对比两者之间的差异。

如果你没有自己的模型或数据,你可以直接使用Hugging Face Hub上提供的已训练模型。此外,Hugging Face Spaces提供了一个方便的在线平台,你可以在那里直接尝试使用这些模型进行帧预测,并查看生成的GIF动画。这将是一个很好的方式来直观地了解模型在帧预测任务上的表现。
以下是使用中文注释改写后的代码:

# 从数据集中随机选择几个样本。
examples = val_dataset[np.random.choice(range(len(val_dataset)), size=5)]

# 初始化一个列表,用于存储预测的视频。
predicted_videos = []

for example in examples:
    # 选取样本中的前10帧和后10帧。
    frames = example[:10, ...]
    original_frames = example[10:, ...]
    # 初始化一个数组,用于存储预测的10帧。
    new_predictions = np.zeros(shape=(10, *frames[0].shape))

    # 预测新的10帧序列。
    for i in range(10):
        # 更新当前的帧序列,包括已经预测的帧。
        frames = example[: 10 + i + 1, ...]
        # 使用模型进行预测。
        new_prediction = model.predict(np.expand_dims(frames, axis=0))
        # 去除预测结果的单维度轴。
        new_prediction = np.squeeze(new_prediction, axis=0)
        # 获取预测序列中的最后一个帧。
        predicted_frame = np.expand_dims(new_prediction[-1, ...], axis=0)

        # 将预测帧存储到预测数组中。
        new_predictions[i] = predicted_frame

    # 创建原始帧和预测帧的GIF,并保存到列表中。
    for frame_set in [original_frames, new_predictions]:
        # 处理帧数据,使其适合生成GIF。
        current_frames = np.squeeze(frame_set)
        current_frames = current_frames[..., np.newaxis] * np.ones(3)
        current_frames = (current_frames * 255).astype(np.uint8)
        current_frames = list(current_frames)

        # 使用imageio库生成GIF。
        with io.BytesIO() as gif:
            imageio.mimsave(gif, current_frames, "GIF", duration=200)
            predicted_videos.append(gif.getvalue())

# 显示视频。
print(" 真实\t预测")
for i in range(0, len(predicted_videos), 2):
    # 使用ipywidgets的HBox和Image组件构建一个水平布局,展示真实帧和预测帧。
    box = HBox(
        [
            widgets.Image(value=predicted_videos[i]),
            widgets.Image(value=predicted_videos[i + 1]),
        ]
    )
    display(box)
  • 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
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

这段代码的目的是展示模型对随机样本的预测效果,通过生成GIF动画,可以直观地观察模型预测的连续帧与原始帧之间的差异。使用ipywidgets库可以在Jupyter Notebook中以交互式的方式展示结果。

  1. 随机选择样本:从验证数据集中随机选择5个样本用于展示。

  2. 初始化预测视频列表:创建一个空列表predicted_videos,用于存储生成的GIF数据。

  3. 遍历样本并预测帧:对每个样本,选取前10帧作为初始输入,然后使用模型进行连续预测,生成新的10帧序列。

  4. 更新帧序列:在预测循环中,每次迭代都将当前的帧序列扩展,包括新预测的帧。

  5. 生成GIF:对于原始帧和预测帧,将它们转换为适合生成GIF的格式,然后使用imageio.mimsave函数生成GIF。

  6. 存储GIF数据:将生成的GIF数据存储到predicted_videos列表中。

  7. 显示视频:使用ipywidgets库的HBoxImage组件,将真实帧和预测帧并排显示。

3. 总结及未来展望

3.1. 总结

本文详细介绍了长短期记忆网络(LSTM)及其在视频预测领域的应用。LSTM是一种特殊的循环神经网络(RNN),通过引入门控机制和细胞状态,有效解决了传统RNN在处理长序列数据时的梯度消失和梯度爆炸问题。文章首先概述了LSTM的核心结构和原理,包括细胞状态、遗忘门、输入门和输出门的概念,并讨论了LSTM的优缺点及其在多个领域的广泛应用。

接着,文章深入探讨了视频预测技术,这是一种在给定视频序列的前几帧后预测后续帧的技术。视频预测技术有助于更好地理解视频内容,并在安防监控、交通管理、环境监测、工业生产和医疗保健等领域具有应用潜力。文章还讨论了视频预测的主要技术,包括第一人称视频预测技术和深度学习技术,以及视频预测技术的发展趋势。

在LSTM用于视频预测的优势和局限方面,文章指出了LSTM在捕捉时空特征、处理复杂模式、端到端学习、扩展性和灵活性以及鲁棒性方面的优势,同时也指出了其在计算复杂度、长期依赖局限性、参数数量、解释性不足、固定时间步长、对噪声和异常值的敏感性以及处理高动态性视频方面的局限性。

文章的第二部分详细介绍了LSTM视频预测的过程,包括数据预处理、模型构建、训练和帧预测可视化。在数据预处理部分,文章描述了如何下载和加载Moving MNIST数据集,如何划分训练集和验证集,以及如何进行数据归一化和帧处理。在模型构建部分,文章展示了如何使用Keras框架构建一个包含ConvLSTM2D层和Conv3D层的深度学习模型,并进行了编译。在训练模型部分,文章介绍了如何使用回调函数改进训练过程,并定义了训练的超参数。最后,在帧预测可视化部分,文章展示了如何使用模型进行帧预测,并使用matplotlib库可视化原始帧和预测帧。

文章还提供了Python代码示例,用于从验证集中选择样本并生成GIF动画,以直观展示模型在视频帧预测方面的性能。代码首先从验证集中随机选择样本,然后使用模型预测新的帧序列,并将原始帧和预测帧转换为GIF格式。最后,使用ipywidgets库在Jupyter Notebook中以交互式方式展示生成的GIF动画。

总之,本文全面介绍了LSTM在视频预测领域的应用,从理论基础到实践操作,为读者提供了深入的理解和实用的指导。通过本文的阅读,读者可以更好地了解LSTM的工作原理和视频预测技术的发展现状,以及如何利用LSTM进行有效的视频预测。

3.2. 展望

长短期记忆网络(LSTM)作为深度学习中的关键技术之一,在视频预测等多个领域展现出巨大潜力。未来研究可以在模型优化与创新、计算效率提升、多模态学习等方面进行深入。特别是在提升模型的计算效率和降低资源消耗上,通过开发新的算法和利用硬件加速技术,可以显著提高LSTM模型在实际应用中的可行性。同时,增强模型的可解释性和透明度,将有助于提升用户对AI系统的信任,确保其决策过程的可靠性和安全性。

此外,探索LSTM在小样本学习、跨领域应用、强化学习集成等方面的能力,将进一步拓展其应用范围和效能。例如,通过小样本学习,可以在数据稀缺的情况下训练出有效的模型,而强化学习的集成则可以使模型在动态环境中做出更加精准的预测和决策。同时,提高模型对于各种噪声和异常值的鲁棒性,将确保其在现实世界复杂多变条件下的稳定性。

最后,随着技术的不断发展,需要关注视频预测技术在伦理和隐私方面的问题,确保技术应用的合理性和安全性。开源工具和平台的建设将促进知识的共享和技术的协作,加速创新的步伐。建立标准化的评估体系和基准测试,将为模型性能提供客观的评价标准,指导未来的研究方向,推动LSTM及其相关技术向更高层次发展。

参考文献

[1] Keras官方示例. 时空序列预测与ConvLSTM的实现[EB/OL]. Keras官方文档, 访问日期: XXXX年XX月XX日. URL: https://keras.io/examples/vision/conv_lstm/

附录1:示例代码

以下是使用中文注释改写后的代码:

```python
"""
# 引言

卷积长短期记忆网络(ConvLSTM)结合了时间序列处理和计算机视觉,通过在LSTM层中引入卷积递归单元。在本例中,我们将探索使用ConvLSTM模型进行下一帧预测的应用,即在给定一系列过去帧的情况下预测接下来的视频帧。
"""

# 导入所需的库
import numpy as np
import matplotlib.pyplot as plt

import keras
from keras import layers

import io
import imageio
from IPython.display import Image, display
from ipywidgets import widgets, Layout, HBox

"""
# 数据集构建

我们将使用Moving MNIST数据集作为本例的数据源。

我们将下载数据集,然后构建和预处理训练集和验证集。

对于下一帧预测,我们的模型将使用前一帧(记为f_n)来预测新帧(记为f_(n + 1))。
为了让模型能够进行这些预测,我们需要处理数据,使得我们拥有“偏移”的输入和输出,其中输入数据是帧x_n,用于预测帧y_(n + 1)。
"""

# 下载并加载Moving MNIST数据集。
fpath = keras.utils.get_file(
    "moving_mnist.npy",
    "http://www.cs.toronto.edu/~nitish/unsupervised_video/mnist_test_seq.npy",
)
dataset = np.load(fpath)

# 交换帧数和数据样本数的轴。
dataset = np.swapaxes(dataset, 0, 1)
# 从10000个样本中选取1000个进行使用。
dataset = dataset[:1000, ...]
# 由于图像是灰度的,添加一个通道维度。
dataset = np.expand_dims(dataset, axis=-1)

# 使用索引划分训练集和验证集,优化内存使用。
indexes = np.arange(dataset.shape[0])
np.random.shuffle(indexes)
train_index = indexes[: int(0.9 * dataset.shape[0])]
val_index = indexes[int(0.9 * dataset.shape[0]):]
train_dataset = dataset[train_index]
val_dataset = dataset[val_index]

# 将数据归一化到0-1范围内。
train_dataset = train_dataset / 255
val_dataset = val_dataset / 255

# 定义辅助函数来创建偏移帧。
def create_shifted_frames(data):
    x = data[:, 0 : data.shape[1] - 1, :, :]
    y = data[:, 1 : data.shape[1], :, :]
    return x, y

# 对数据集应用处理函数。
x_train, y_train = create_shifted_frames(train_dataset)
x_val, y_val = create_shifted_frames(val_dataset)

# 检查数据集形状。
print("训练集数据形状:" + str(x_train.shape) + ", " + str(y_train.shape))
print("验证集数据形状:" + str(x_val.shape) + ", " + str(y_val.shape))

"""
# 数据可视化

我们的数据由帧序列组成,每个帧都被用来预测接下来的帧。让我们来查看一些这样的连续帧。
"""

# 构建图形以可视化图像。
fig, axes = plt.subplots(4, 5, figsize=(10, 8))

# 为一个随机数据示例绘制序列图像。
data_choice = np.random.choice(range(len(train_dataset)), size=1)[0]
for idx, ax in enumerate(axes.flat):
    ax.imshow(np.squeeze(train_dataset[data_choice][idx]), cmap="gray")
    ax.set_title(f"帧 {idx + 1}")
    ax.axis("off")

# 打印信息并显示图形。
print(f"正在显示示例 {data_choice} 的帧。")
plt.show()

"""
# 模型构建

为了构建ConvLSTM模型,我们将使用ConvLSTM2D层,该层将接受形状为(batch_size, num_frames, width, height, channels)的输入,并返回具有相同形状的预测视频。
"""

# 构建输入层,不指定确切的帧大小。
inp = layers.Input(shape=(None, *x_train.shape[2:]))

# 构建3个带有批量归一化的ConvLSTM2D层,然后是用于时空输出的Conv3D层。
x = layers.ConvLSTM2D(
    filters=64,
    kernel_size=(5, 5),
    padding="same",
    return_sequences=True,
    activation="relu",
)(inp)
x = layers.BatchNormalization()(x)
x = layers.ConvLSTM2D(
    filters=64,
    kernel_size=(3, 3),
    padding="same",
    return_sequences=True,
    activation="relu",
)(x)
x = layers.BatchNormalization()(x)
x = layers.ConvLSTM2D(
    filters=64,
    kernel_size=(1, 1),
    padding="same",
    return_sequences=True,
    activation="relu",
)(x)
x = layers.Conv3D(
    filters=1, kernel_size=(3, 3, 3), activation="sigmoid", padding="same"
)(x)

# 构建完整的模型并编译。
model = keras.models.Model(inp, x)
model.compile(
    loss=keras.losses.binary_crossentropy,
    optimizer=keras.optimizers.Adam(),
)

"""
# 模型训练

模型和数据构建完成后,我们现在可以训练模型。
"""

# 定义回调函数以改进训练过程。
early_stopping = keras.callbacks.EarlyStopping(monitor="val_loss", patience=10)
reduce_lr = keras.callbacks.ReduceLROnPlateau(monitor="val_loss", patience=5)

# 定义可修改的训练超参数。
epochs = 20
batch_size = 5

# 训练模型。
model.fit(
    x_train,
    y_train,
    batch_size=batch_size,
    epochs=epochs,
    validation_data=(x_val, y_val),
    callbacks=[early_stopping, reduce_lr],
)

"""
# 帧预测可视化

模型构建并训练完成后,我们可以根据新视频生成一些示例帧预测。

我们将从验证集中随机选取一个示例,并从中选取前十个帧。之后,我们可以让模型预测10个新帧,并与真实帧预测进行比较。
"""

# 从验证数据集中随机选择一个样本。
example = val_dataset[np.random.choice(range(len(val_dataset)), size=1)[0]]

# 选取样本中的前10帧和后10帧。
frames = example[:10, ...]
original_frames = example[10:, ...]

# 预测新的10帧序列。
for _ in range(10):
    new_prediction = model.predict(np.expand_dims(frames, axis=0))
    new_prediction = np.squeeze(new_prediction, axis=0)
    predicted_frame = np.expand_dims(new_prediction[-1, ...], axis=0)

    # 扩展预测帧序列。
    frames = np.concatenate((frames, predicted_frame), axis=0)

# 构建原始帧和新帧的图形。
fig, axes = plt.subplots(2, 10, figsize=(20, 4))

# 绘制原始帧。
for idx, ax in enumerate(axes[0]):
    ax.imshow(np.squeeze(original_frames[idx]), cmap="gray")
    ax.set_title(f"帧 {idx + 11}")
    ax.axis("off")

# 绘制新预测的帧。
new_frames = frames[10:, ...]
for idx, ax in enumerate(axes[1]):
    ax.imshow(np.squeeze(new_frames[idx]), cmap="gray")
    ax.set_title(f"帧 {idx + 11}")
    ax.axis("off")

# 展示图形。
plt.show()

"""
# 预测视频

最后,我们将从验证集中选取一些示例,并用它们构建GIF来查看模型预测的视频。

你可以使用托管在Hugging Face Hub上的[训练好的模型](https://huggingface.co/keras-io/conv-lstm),并在[Hugging Face Spaces](https://huggingface.co/spaces/keras-io/conv-lstm)上尝试演示。
"""

# 从数据集中随机选择几个示例。
examples = val_dataset[np.random.choice(range(len(val_dataset)), size=5)]

# 遍历示例并预测帧。
predicted_videos = []
for example in examples:
    # 选取示例中的前10帧和后10帧。
    frames = example[:10, ...]
    original_frames = example[10:, ...]
    new_predictions = np.zeros(shape=(10, *frames[0].shape))

    # 预测新的10帧序列。
    for i in range(10):
        frames = example[: 10 + i + 1, ...]
        new_prediction = model.predict(np.expand_dims(frames, axis=0))
        new_prediction = np.squeeze(new_prediction, axis=0)
        predicted_frame = np.expand_dims(new_prediction[-1, ...], axis=0)

        # 扩展预测帧序列。
        new_predictions[i] = predicted_frame

    # 创建原始帧和预测帧的GIF,并保存。
    for frame_set in [original_frames, new_predictions]:
        current_frames = np.squeeze(frame_set)
        current_frames = current_frames[..., np.newaxis] * np.ones(3)
        current_frames = (current_frames * 255).astype(np.uint8)
        current_frames = list(current_frames)

        with io.BytesIO() as gif:
            imageio.mimsave(gif, current_frames, "GIF", duration=200)
            predicted_videos.append(gif.getvalue())

# 显示视频。
print("真实\t预测")
for i in range(0, len(predicted_videos), 2):
    # 使用ipywidgets的HBox和Image组件构建一个水平布局,展示真实帧和预测帧。
    box = HBox(
        [
            widgets.Image(value=predicted_videos[i]),
            widgets.Image(value=predicted_videos[i + 1]),
        ]
    )
    display(box)
  • 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
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号