机器学习第二十五周周报 ConvLSTM

news2024/9/25 19:16:40

文章目录

  • week 25 ConvLSTM
  • 摘要
  • Abstract
  • 一、李宏毅机器学习
  • 二、文献阅读
    • 1. 题目
    • 2. abstract
    • 3. 网络架构
      • 3.1降水预报问题的建模
      • 3.2Convolutional LSTM
      • 3.3编码-预测结构
    • 4. 文献解读
      • 4.1 Introduction
      • 4.2 创新点
      • 4.3 实验过程
        • 4.3.1Moving-MNIST Dataset
        • 4.3.2雷达回波数据集
      • 4.4 结论
  • 三、基于pytorch实现ConvLSTM
    • 1.实验内容
    • 2.实验结果
    • 3.实验数据集
      • 3.1数据集处理
    • 4.模型及训练过程实现
    • 小结
    • 参考文献

week 25 ConvLSTM

摘要

本文主要讨论ConvLSTM的模型。本文简要介绍了自注意力机制运行逻辑。其次本文展示了题为Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting的论文主要内容。该论文将降水预报问题建模为时空序列预测问题,并根据FC-LSTM结构进行扩展,提出了ConvLSTM。该结构改善了FC-LSTM的缺点,通过其局部邻域的输入和过去状态来确定网格中某个单元的未来状态。该文在多个数据集上进行实验,从数据角度证明了该网络的优越性。最后,本文基于pytorch实现了ConvLSTM模型并在KTH数据集上进行验证。

Abstract

This article mainly discusses the model of ConvLSTM. This article briefly introduces the operating logic of the self-attention mechanism. Secondly, this paper presents the main content of the paper entitled Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting. In this paper, this paper models the precipitation prediction problem as a spatiotemporal series prediction problem, and proposes the ConvLSTM by extending it according to the FC-LSTM structure. This structure improves on the shortcomings of FC-LSTM by determining the future state of an element in the grid through the input and past state of its local neighborhood. In this paper, experiments are carried out on multiple datasets to prove the superiority of the network from the perspective of data. Finally, this article implements the ConvLSTM model based on pytorch and validates on the KTH dataset.

一、李宏毅机器学习

8029043ece92c4a6a60541eb53067ad

二、文献阅读

1. 题目

题目:Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting

作者:Xingjian Shi, Zhourong Chen, Hao Wang, Dit-Yan Yeung, Wai-kin Wong, Wang-chun Woo

链接:https://arxiv.org/pdf/1506.04214.pdf

发布:NIPS’15: Proceedings of the 28th International Conference on Neural Information Processing Systems - Volume 1December 2015Pages 802–810

2. abstract

在本文中,将降水临近预报表述为时空序列预测问题,其中输入和预测目标都是时空序列。通过扩展全连接 LSTM (FC-LSTM) 以在输入到状态和状态到状态转换中都具有卷积结构,提出了卷积 LSTM (ConvLSTM) 并用它来构建端到端降水临近预报问题的可训练模型。实验表明,ConvLSTM 网络可以更好地捕获时空相关性,并且始终优于 FC-LSTM 和最先进的降水临近预报操作 ROVER 算法。

This paper formulates precipitation nowcasting as a spatiotemporal sequence forecasting problem in which both the input and the prediction target are spatiotemporal sequences. By extending the fully connected LSTM (FC-LSTM) to have convolutional structures in both the input-to-state and
state-to-state transitions, this paper proposes the convolutional LSTM (ConvLSTM) and uses it to build an end-to-end trainable model for the precipitation nowcasting problem. Experiments show that ConvLSTM network captures spatiotemporal correlations better and consistently outperforms FC-LSTM and the state-of-the-art operational ROVER algorithm for precipitation nowcasting.

3. 网络架构

3.1降水预报问题的建模

对流降水临近预报一直是天气预报领域的一个重要问题。该任务的目标是准确、及时地预测局部区域在相对较短的时间内(例如0-6小时)的降雨强度。

假设在一个由M行和N列组成的 M × N M\times N M×N网格表示的空间区域内观测到一个动态系统。在网络个的每个单元格内,都有随着时间变化的P个观测值。因此,任何时刻的观测值都可以使用张量 X ∈ R P × M × N \mathcal X\in \mathbf R^{P\times M\times N} XRP×M×N来表示,其中 R \mathbf R R表示观测到的特征的域。如果定期记录观测值,将得到 X ^ 1 , X ^ 2 , … , X ^ t \mathcal {\hat X}_1,\mathcal {\hat X}_2,\dots, \mathcal {\hat X}_t X^1,X^2,,X^t​。时空序列预测问题是在给定前J个观测值的情况下,预测未来最可能的长度K序列:
X ^ t + 1 , … , X ^ t + K = argmax X t + 1 , … , X t + K p ( X t + 1 , … , X t + K ∣ X ^ t − J + 1 , X ^ t − J + 2 , … , X t ) (1) \mathcal {\hat X}_{t+1},\dots,\mathcal {\hat X}_{t+K}=\text{argmax}_{\mathcal X_{t+1},\dots, \mathcal X_{t+K}}p(\mathcal {X_{t+1}},\dots, \mathcal{X}_{t+K}|\mathcal {\hat X}_{t-J+1},\mathcal {\hat X}_{t-J+2},\dots, \mathcal {X}_t) \tag{1} X^t+1,,X^t+K=argmaxXt+1,,Xt+Kp(Xt+1,,Xt+KX^tJ+1,X^tJ+2,,Xt)(1)
对于降水预报,每个时间戳的观测都是一幅二维雷达回波图。若将地图划分为平铺的、不重叠的贴片,并将贴片内的像素作为其测量值。

LSTM不再赘述直接描述网络结构

3.2Convolutional LSTM

FC-LSTM在处理时空数据中的主要缺点是它在输入到状态和状态到状态转换中的全连接的使用,其中没有空间信息被编码。为了克服这个问题,进行针对性设计,

  • 输入是 X 1 , X 2 , … , X t \mathcal {X}_1,\mathcal {X}_2,\dots, \mathcal {X}_t X1,X2,,Xt
  • 单元输出 C 1 , C 2 , … , C t \mathcal C_1,\mathcal C_2,\dots, \mathcal C_t C1,C2,,Ct
  • 隐藏状态 H 1 , … , H t \mathcal H_1,\dots, \mathcal H_t H1,,Ht
  • ConvLSTM的门: i t , f t , o t i_t,f_t,o_t it,ft,ot

以上均为3D张量,其最后两个维度是空间维度

image-20240106183201762

ConvLSTM 通过其局域邻域的输入和过去状态来确定网格中某个单元的未来状态。通过在状态到状态和输入到状态转换中使用卷积算子轻松实现,如上图。ConvLSTM的关键方程如下面,*为卷积算子, ∘ \circ 为卷积算子

若将状态视为移动对象的隐式表示,则具有较大内核的ConvLSTM应该能够捕获更快的运动,而具有较小卷积核的可以捕获较慢的运动。此外,FC-LSTM可以看作所有特征均在一个单元格上的ConvLSTM。

为了确保状态具有与输入相同的行数和相同数量的列,在应用卷积操作之前需要padding。在这里,隐藏状态的embedding在可以将边界点视为使用外部世界的状态进行计算。

image-20240106191731428

3.3编码-预测结构

ConvLSTM可以作为更为复杂结构的模块。对于时空序列预测问题,使用下图所示的结构。两个网络、一个编码网络和一个预测网络。预测网络的初始状态和单元输出均是从编码网络的最后一个状态复制得到。这两个网络都是通过堆叠几个ConvLSTM层来形成的,由于预测目标具有与输入相同的位数,故将预测网

4. 文献解读

4.1 Introduction

对流降水临近预报一直是天气预报领域的一个重要问题。该任务的目标是准确、及时地预测局部区域在相对较短的时间内(例如0-6小时)的降雨强度。ROVER算法是领域现有的最先进算法。深度学习的最新进展,特别是循环神经网络 (RNN) 和长短期记忆 (LSTM) 模型,提供了一些关于如何解决这个问题的途径。[2]中提出的开创性的 LSTM 编码器-解码器框架通过训练时间级联的 LSTM。但以往的模型均采用的全连接LSTM(FC-LSTM)层没有考虑空间相关性。

在本文中,提出了一种用于降水临近预报的新型ConvLSTM网络。将降水临近预报表述为一个时空序列预测问题,可以在[2]中提出的通用序列到序列学习框架下解决。为了更好地模拟时空关系,将FC-LSTM的思想扩展到在输入-状态和状态-状态转换两者中具有卷积结构的COVLSTM。当对合成Moving-MNIST数据集和雷达回波数据集进行评估时,ConvLSTM模型始终优于FC-LSTM和最先进的操作ROVER算法。

4.2 创新点

  1. 将降水预报问题建模为时空序列预测问题,从而在[2]中提出的框架内解决。
  2. 根据FC-LSTM结构进行扩展,提出了ConvLSTM,该结构改善了FC-LSTM的缺点,通过其局部邻域的输入和过去状态来确定网格中某个单元的未来状态。
  3. 将ConvLSTM网络与一个合成MovingMNIST数据集中的FC-LSTM网络进行比较。建立了一个新的雷达回波数据集,并将该模型与基于几种常用降水预报指标的最先进的ROVER算法进行了比较。

4.3 实验过程

首先将ConvLSTM网络与一个合成MovingMNIST数据集中的FC-LSTM网络进行比较,以获得对模型的行为的一些基本理解。用不同的规格运行模型根据[3]中的层和内核大小,并研究一些“域外域”情况。为了验证模型在更具有挑战性的降水预报问题上的有效性,建立了一个新的雷达回波数据集,并将模型与基于几种常用降水预报指标的最先进的ROVER算法进行了比较。

在python环境下实现了该模型,并在配备了单个NVIDIA K20 GPU的计算机上运行所有实验。

4.3.1Moving-MNIST Dataset

数据预处理以及部分训练参数:使用类似[3]中描述的生成过程。数据集中所有数据实例长20帧(输入10帧,预测10帧),并包含在 64 × 64 64\times 64 64×64patch内的手写数字。移动数字从MNIST数据集中500位子集中随机选择。开始位置和速度方向都是随机选择的,速度振幅是随机选择的。该生成过程重复了15000次,得到了包含10000条训练序列、2000条验证序列和3000条测试序列的数据集。通过使用反向传播时间(BPTT)最小化交叉熵损失来训练所有LSTM模型,并且RMSProp,学习速率为10×3,衰减率为0.9。此外,在验证集上执行early-stop。

实验中使用的网络框架:对于FC-LSTM网络,使用了与[3]中具有两个2048节点的LSTM层的无条件未来预测器模型相同的结构。对于ConvLSTM网络,将patch size设置为4×4,这样每个64×64帧由16×16×16张量表示。用不同的层数测试模型的三种变体。

  • 1层网络包含一个包含256个隐藏状态的ConvLSTM层
  • 2层网络有两个ConvLSTM层,每个层有128个隐藏状态
  • 3层网络在三个ConvLSTM层中分别有128、64和64隐藏状态

所有输入到状态和状态到状态的内核大小为5×5

实验表明,ConvLSTM 网络的性能始终优于 FC-LSTM 网络,更深层的网络结构可以提供更好的结果

image-20240106203941577

上图为ConvLSTM 网络与 FC-LSTM 网络在 Moving-MNIST 数据集上的比较结果,即各算法在测试集上的平均交叉熵损失。‘-5x5’和‘-1x1’表示相应的状态到状态内核大小,即5×5或1×1。 “256”、“128”和“64”指的是 ConvLSTM 层中隐藏状态的数量。 ‘(5x5)’和‘(9x9)’表示输入到状态的内核大小。

可以看出ConvLSTM在参数更少的情况下,达到了更好的结果

4.3.2雷达回波数据集

所使用的雷达回波数据集是2011年至2013年在香港收集的3年天气雷达强度的子集。由于不是每天都下雨,预报目标是降水,选择前97个雨天形成数据集。

数据预处理:首先通过设置 P = Z − min ⁡ ( Z ) max ⁡ ( Z ) − min ⁡ ( Z ) P=\frac{Z-\min(Z)}{\max(Z)-\min(Z)} P=max(Z)min(Z)Zmin(Z)将强度值Z转换为灰度像素P,并在中心 330 × 330 330\times 330 330×330区域裁剪雷达图。之后将带有半径10的磁盘过滤器应用至 100 × 100 100\times 100 100×100范围内,并调整雷达图的大小。为了减少测量仪器带来的噪声,进一步去除了一些噪声区域的像素值,这些区域是通过将K-Means聚类方法应用于每月像素平均值来确定的。天气雷达数据每6分钟记录一次,因此每天有240帧。为了获得不相交的训练、测试和验证子集,将每个日序列划分为40个不重叠的帧块,并随机分配4个块进行训练,1块用于测试并且1块用于验证。数据实例是使用一个20帧宽的滑动窗口从这些块中分割出来的。因此,雷达回波数据集包含8148个训练序列,2037个测试序列和2037个验证序列,所有序列都有20帧长(输入5帧,预测15帧)。

训练以及模型参数设置

  • patch size设置为2,并训练一个包含64个隐藏状态和3×3个内核的2层ConvLSTM网络

  • 对于ROVER算法,在验证集上调整光流估计器的参数,并使用最佳参数报告测试结果。

    此外,还尝试了三种不同的Rover初始化方案:

    • ROVER 1计算最后两个观测帧的光流,然后进行半拉格朗日平流;
    • ROVER 2以最后两个流场的平均值初始化速度;
    • ROVER 3给出最后三个流场的加权平均值(权重分别为0.7、0.2和0.1)初始化
  • 此外,还训练了一个具有两个 2000 节点 LS TM层的 FC-LSTM 网络。

无论是 ConvLSTM 网络还是 FC-LSTM 网络都优化了 15 个预测的交叉

使用几种常用的降水预报指标,即雨量均方误差(雨量均方误差)、关键成功指数(CSI)、虚警率(Far)、检测概率(POD)以及相关性来评价这些方法。

降雨量 MSE 指标定义为预测降雨量与实际降雨量之间的平均平方误差。

三个技能分数定义为

  • C S I = h i t s h i t s + m i s s e s + f a l s e a l a r m s CSI=\frac{hits}{hits+misses+falsealarms} CSI=hits+misses+falsealarmshits

  • F A R = f a l s e a l a r m s h i t s + f a l s e a l a r m s FAR=\frac{falsealarms}{hits+falsealarms} FAR=hits+falsealarmsfalsealarms

  • P O D = h i t s h i t s + m i s s e s POD=\frac{hits}{hits+misses} POD=hits+misseshits

预测框架P与地面真框架T的相关性定义为:
∑ i , j P i , j T i . j ( ∑ i , j P i , j 2 ) ( ∑ i , j T i , j 2 ) + ϵ \frac{\sum_{i,j}P_{i,j}T_{i.j}}{\sqrt{(\sum_{i,j}P_{i,j}^2)(\sum_{i,j}T_{i,j}^2)+\epsilon}} (i,jPi,j2)(i,jTi,j2)+ϵ i,jPi,jTi.j
其中 ϵ = 1 0 − 9 \epsilon=10^{-9} ϵ=109

下图为比较不同模型 15 个预测步骤的平均得分

image-20240106205328229

下图为基于四种降水临近预报指标的不同模型随时间的比较结果

image-20240106205811162

ConvLSTM能够更准确地预测未来的降水等高线,特别是在边界上。虽然 ROVER2 可以给出比 ConvLSTM 更清晰的预测,但它会触发更多的虚假警报,而且通常比 ConvLSTM 更不精确。另外,ConvLSTM的模糊效应可能是由于任务本身的不确定性造成的。

4.4 结论

在这两个数据集上进行的实验结果得出以下结论:

  • ConvLSTM在处理时空相关性方面优于FC-LSTM.
  • 使得状态到状态卷积核的大小大于1对于捕获时空运动模式是必要的。
  • 更深层次的模型可以较少的参数产生更好的结果。
  • ConvLSTM在降水预报方面的性能优于ROVER。

三、基于pytorch实现ConvLSTM

1.实验内容

基于pytorch实现ConvLSTM并使用KTH数据集进行测试

2.实验结果

训练过程如下

Epochs[1/50]--batch[0/402]--Acc: 0.1562--loss: 1.7924
Epochs[1/50]--batch[50/402]--Acc: 0.4375--loss: 1.6179
Epochs[1/50]--batch[100/402]--Acc: 0.375--loss: 1.3734
Epochs[1/50]--batch[150/402]--Acc: 0.3438--loss: 1.2532
Epochs[1/50]--batch[200/402]--Acc: 0.4375--loss: 1.2269
Epochs[1/50]--batch[250/402]--Acc: 0.5625--loss: 0.925
Epochs[1/50]--batch[300/402]--Acc: 0.5938--loss: 0.8918
Epochs[1/50]--batch[350/402]--Acc: 0.5--loss: 1.085
Epochs[1/50]--Acc on val 0.5182
Epochs[30/50]--Acc on val 0.6551

3.实验数据集

本实验使用KTH数据集,共有六个类别,包括Boxing(拳击)、Handclapping(鼓掌)、Handwaving(挥手)、Jogging(慢跑)、Running(快跑)和Walking(行走)。共计600个视频文件。

3.1数据集处理

is_gray:是否转换为灰度图

frame_len以该长度对视频进行分割

transforms:进行图像增强

__init__:初始化操作

def load_avi_frames:数据载入;

  • 创建一个视频捕获对象,用于读取视频文件;
  • 循环读取视频帧,直到视频结束;
  • 检查是否成功读取到帧图像数据;
  • 将原始图片转换为灰度图,因为后续数据集用作分类所以转换为单通道的灰度图可以降低计算量;
  • 返回得到一个4维数组

def data_process:样本构建

  • 缓存预处理结果的修饰器
  • 循环遍历每个目录下的视频文件,并得到该目录下所有视频文件的名称
  • 开始遍历当前文件夹中的每个视频文件
  • 根据文件名获取对应的人物编号
  • 读取得到原始的视频数据;
  • 根据每个视频以固定长度进行采样构造样本,其中sub_frames的形状为[frame_len,120,160,channels]`;
  • 返回最后构造完成的样本数据。

def generate_batch:实现一个辅助函数来处理每个小批量样本的数据

  • 遍历小批量样本中的每个样本;
  • 循环对视频里的每一帧进行图像增强,其中frame的形状为[height, width, channels],在进行图像增强经过ToTensor()变换后形状会变成[channels,height,width]且每个像素值的范围会被缩放至
  • 将所有样本堆叠构造得到一个小批量标准数据,其形状为[batch_size, frame_len, channels, height, width]

def load_train_val_test_data:编码实现迭代器的构建

  • 返回data_process方法采样得到的原始样本数据;
  • 构建得到测试集对应的迭代器,其中generate_batch方法将作为参数传入到类DataLoader中进行使用;
  • 构建得到训练集和验证集对应的迭代器。
class KTHData(object):
    """
    载入KTH数据集,下载地址:https://www.csc.kth.se/cvap/actions/ 一共包含6个zip压缩包
    """
    DATA_DIR = os.path.join(DATA_HOME, 'kth')
    CATEGORIES = ["boxing", "handclapping", "handwaving", "jogging", "running", "walking"]
    TRAIN_PEOPLE_ID = [1, 2, 4, 5, 6, 7, 9, 11, 12, 15, 17, 18, 20, 21, 22, 23, 24]  # 25*0.7 = 17
    VAL_PEOPLE_ID = [3, 8, 10, 19, 25]  # 25*0.2 = 5
    TEST_PEOPLE_ID = [13, 14, 16]  # 25*0.1 = 3
    FILE_PATH = os.path.join(DATA_DIR, 'kth.pt')

    def __init__(self, frame_len=15,
                 batch_size=4,
                 is_sample_shuffle=True,
                 is_gray=True,
                 transforms=None):
        self.frame_len = frame_len  # 即time_step, 以FRAME_LEN为长度进行分割
        self.batch_size = batch_size
        self.is_sample_shuffle = is_sample_shuffle
        self.is_gray = is_gray
        self.transforms = transforms

    @staticmethod
    def load_avi_frames(path=None, is_gray=False):
        """
        用来读取每一个.avi格式的文件
        :param path:
        :return:
        """
        import cv2
        logging.info(f" ## 正在读取原始文件: {path}并划分数据")
        video = cv2.VideoCapture(path)
        frames = []
        while video.isOpened():
            ret, frame = video.read()  # frame: (120, 160, 3) <class 'numpy.ndarray'>
            if not ret:  # ret是一个布尔值,表示是否成功读取帧图像的数据,frame是读取到的帧图像数据。
                break
            if is_gray:
                frame = Image.fromarray(frame)
                frame = frame.convert("L")
                frame = np.array(frame.getdata()).reshape((120, 160, 1))
            frames.append(frame)
        logging.info(f" ## 该视频一共有{len(frames)}帧")
        return np.array(frames, dtype=np.uint8)  # [n, height, width, channels]
        # 必须要转换成np.uint8类型,否则transforms.ToTensor()中的标准化会无效

    @process_cache(unique_key=["frame_len", "is_gray"])
    def data_process(self, file_path=None):
        train_data, val_data, test_data = [], [], []
        for label, dir_name in enumerate(self.CATEGORIES):  # 遍历每个文件夹
            video_dir = os.path.join(self.DATA_DIR, dir_name)  # 构造每个文件夹的路径
            video_names = os.listdir(video_dir)  # 列出当前文件夹的所有文件
            for name in video_names:  # 遍历当前文件夹中的每个视频
                people_id = int(name[6:8])  # 取人员编号
                video_path = os.path.join(video_dir, name)  # 得到文件的绝对路径
                frames = self.load_avi_frames(video_path, self.is_gray)  # 读取该文件
                s_idx, e_idx = 0, self.frame_len
                while e_idx <= len(frames):  # 开始采样样本
                    logging.info(f" ## 截取帧子序列 [{s_idx}:{e_idx}]")
                    sub_frames = frames[s_idx:e_idx]  # [frame_len, 120, 160, channels]
                    if people_id in self.TRAIN_PEOPLE_ID:
                        train_data.append((sub_frames, label))
                    elif people_id in self.VAL_PEOPLE_ID:
                        val_data.append((sub_frames, label))
                    elif people_id in self.TEST_PEOPLE_ID:
                        test_data.append((sub_frames, label))
                    else:
                        raise ValueError(f"people id {people_id} 有误")
                    s_idx, e_idx = e_idx, e_idx + self.frame_len
        logging.info(f" ## 原始数据划分完毕,训练集、验证集和测试集的数量分别为: "
                     f"{len(train_data)}-{len(val_data)}-{len(test_data)}")
        data = {"train_data": train_data, "val_data": val_data, "test_data": test_data}
        return data

    def generate_batch(self, data_batch):
        """
        :param data_batch:
        :return: 每个batch的形状
                 [batch_size, frame_len, channels, height, width]
                 [batch_size, ]
        """
        batch_frames, batch_label = [], []
        for (frames, label) in data_batch:  # 开始对一个batch中的每一个样本进行处理。
            # frames的形状为 [frame_len, height, width,channels]
            if self.transforms is not None:
                # 遍历序列里的每一帧,frame的形状[height, width, channels]
                # 经过transforms.ToTensor()后的形状为[channels, height, width]
                frames = torch.stack([self.transforms(frame) for frame in frames],
                                     dim=0)  # [frame_len, channels, height, width]
            else:
                frames = torch.tensor(frames.transpose(0, 3, 1, 2))  # [frame_len, channels, height, width]
                logging.info(f"{frames.shape}")
            batch_frames.append(frames)  # [[frame_len, channels, height, width], [], []]
            batch_label.append(label)
        batch_frames = torch.stack(batch_frames, dim=0)  # [batch_size, frame_len, channels, height, width]
        batch_label = torch.tensor(batch_label, dtype=torch.long)
        return batch_frames, batch_label

    def load_train_val_test_data(self, is_train=False):
        data = self.data_process(file_path=self.FILE_PATH)
        if not is_train:
            test_data = data['test_data']
            test_iter = DataLoader(test_data, batch_size=self.batch_size,
                                   shuffle=True, collate_fn=self.generate_batch)
            logging.info(f" ## 测试集构建完毕,一共{len(test_data)}个样本")
            return test_iter
        train_data, val_data = data['train_data'], data['val_data']
        train_iter = DataLoader(train_data, batch_size=self.batch_size,  # 构造DataLoader
                                shuffle=self.is_sample_shuffle,
                                collate_fn=self.generate_batch)
        val_iter = DataLoader(val_data, batch_size=self.batch_size,
                              shuffle=False, collate_fn=self.generate_batch)
        logging.info(f" ## 训练集和验证集构建完毕,样本数量为{len(train_data)}:{len(val_data)}")
        return train_iter, val_iter

    def show_example(self, file_path=None, row=3, col=5, begin_id=10):
        """
        可视化
        :param file_path:
        :param row:
        :param col:
        :param begin_id:
        :return:
        """
        import matplotlib.pyplot as plt
        if file_path is None:
            file_path = os.path.join(self.DATA_DIR, self.CATEGORIES[0])
            file_path = os.path.join(file_path, 'person01_boxing_d1_uncomp.avi')
        frames = self.load_avi_frames(file_path)
        fig, ax = plt.subplots(row, col)
        for i, axi in enumerate(ax.flat):  # , figsize=(18, 10)
            image = frames[i + begin_id]
            axi.set_xlabel(f'Frame{i + begin_id}')
            axi.imshow(image)
            axi.set(xticks=[], yticks=[])
        plt.tight_layout()
        plt.show()

4.模型及训练过程实现

参照论文实现ConvLSTM

ConvLSTMCell:记忆单元

class ConvLSTMCell(nn.Module):

    def __init__(self, in_channels, out_channels, kernel_size, bias):
        """
        Initialize ConvLSTM cell.
        Parameters
        ----------
        in_channels: int 输入特征图的通道数
        out_channels: int 输出特征图的通道数
        kernel_size: (int, int) 卷积核的宽和高
        bias: bool 是否使用偏置
        """
        super(ConvLSTMCell, self).__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels

        self.kernel_size = kernel_size
        self.padding = kernel_size[0] // 2, kernel_size[1] // 2
        # 需要强制进行padding以保证每次卷积后形状不发生变化
        # 根据之前第4.3.2节内容的介绍,在stride=1的情况下,padding = kernel_size // 2
        # 如:卷积核为3×3则需要padding=1即可
        # 在下面的卷积操作中stride使用的是默认值1
        self.bias = bias
        self.conv = nn.Conv2d(in_channels=self.in_channels + self.out_channels,
                              out_channels=4 * self.out_channels,
                              kernel_size=self.kernel_size,
                              padding=self.padding,
                              bias=self.bias)

    def forward(self, input_tensor, last_state):
        """

        :param input_tensor: 当前时刻的输入x_t, 形状为[batch_size, in_channels, height, width]
        :param last_state: 上一时刻的状态c_{t-1}和h_{t-1}, 形状均为 [batch_size, out_channels, height, width]
        :return:
        """
        h_last, c_last = last_state
        combined_input = torch.cat([input_tensor, h_last], dim=1)
        # [batch_size, in_channels+out_channels, height, width]
        combined_conv = self.conv(combined_input)  # [batch_size, 4 * out_channels, height, width]
        cc_i, cc_f, cc_o, cc_g = torch.split(combined_conv, self.out_channels, dim=1)
        # 分割得到每个门对应的卷积计算结果,形状均为 [batch_size, out_channels, height, width]
        i = torch.sigmoid(cc_i)
        f = torch.sigmoid(cc_f)
        o = torch.sigmoid(cc_o)
        g = torch.tanh(cc_g)
        c_next = f * c_last + i * g  # [batch_size, out_channels, height, width]
        h_next = o * torch.tanh(c_next)  # [batch_size, out_channels, height, width]
        return h_next, c_next

    def init_hidden(self, batch_size, image_size):
        """
        初始化记忆单元的C和H
        :param batch_size:
        :param image_size:
        :return:
        """
        height, width = image_size
        return (torch.zeros(batch_size, self.out_channels, height, width, device=self.conv.weight.device),
                torch.zeros(batch_size, self.out_channels, height, width, device=self.conv.weight.device))

ConvLSTM:模型

class ConvLSTM(nn.Module):
    """

    Parameters:
        in_channels: 输入特征图的通道数,为整型
        out_channels: 每一层输出特征图的通道数,可为整型也可以是列表;
                      为整型时表示每一层的输出通道数均相等,为列表时则列表的长度必须等于num_layer
                      例如 out_channels =[32,64,128] 表示3层ConvLSTM的输出特征图通道数分别为
                      32、64和128,且此时的num_layer也必须为3
        kernel_size:  每一层中卷积核的长和宽,可以为一个tuple,如(3,3)表示每一层的卷积核窗口大小均为3x3;
                      也可以是一个列表分别用来指定每一层卷积核的大小,如[(3,3),(5,5),(7,7)]表示3层卷积各种的窗口大小
                      此时需要注意的是,如果为列表也报保证其长度等于num_layer
        num_layers: ConvLSTM堆叠的层数
        batch_first: 输入数据的第1个维度是否为批大小
        bias: 卷积中是否使用偏置
        return_all_layers: 是否返回每一层各个时刻的输出结果

    Input:
        A tensor of size B, T, C, H, W or T, B, C, H, W
        [Batch_size, Time_step, Channels, Height, Width]  or [Time_step, Batch_size, Channels, Height, Width]
    Output:
        当return_all_layers 为 True 时:
        layer_output_list: 每一层的输出结果,包含有num_layer个元素的列表,
                           每个元素的形状为[batch_size, time_step, out_channels, height, width]
        last_states: 每一层最后一个时刻的输出结果,同样是包含有num_layer个元素的列表,
                     列表中的每个元素均为一个包含有两个张量的列表,
                     如last_states[-1][0]和last_states[-1][1]分别表示最后一层最后一个时刻的h和c
                     layer_output_list[-1][:, -1] == last_states[-1][0]
                     shape:  [Batch_size, Channels, Height, Width]

        当return_all_layers 为 False 时:
        layer_output_list: 最后一层每个时刻的输出,形状为 [batch_size, time_step, out_channels, height, width]
        last_states: 最后一层最后一个时刻的输出,形状为 [batch_size, out_channels, height, width]

    Example:
        >> model = ConvLSTM(in_channels=3,
                 out_channels=2,
                 kernel_size=(3, 3),
                 num_layers=3,
                 batch_first=True,
                 bias=True,
                 return_all_layers=True)
        x = torch.rand((1, 4, 3, 5, 5)) # [batch_size, time_step, channels, height, width]
        layer_output_list, last_states = model(x)
    """

    def __init__(self, in_channels, out_channels, kernel_size, num_layers,
                 batch_first=False, bias=True, return_all_layers=False):
        super(ConvLSTM, self).__init__()

        self._check_kernel_size_consistency(kernel_size)
        # 检查kernel_size是否符合上面说的取值情况

        # Make sure that both `kernel_size` and `out_channels` are lists having len == num_layers
        kernel_size = self._extend_for_multilayer(kernel_size, num_layers)
        out_channels = self._extend_for_multilayer(out_channels, num_layers)
        # 将kernel_size和out_channels扩展到多层时的情况

        if not len(kernel_size) == len(out_channels) == num_layers:
            raise ValueError('len(kernel_size) == len(out_channels) == num_layers 三者的值必须相等')

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.num_layers = num_layers
        self.batch_first = batch_first
        self.bias = bias
        self.return_all_layers = return_all_layers

        cell_list = []
        for i in range(0, self.num_layers):  # 实例化每一层的ConvLSTM记忆单
            cur_in_channels = self.in_channels if i == 0 else self.out_channels[i - 1]
            # 当前层的输入通道数,除了第一层为self.in_channels之外,其它的均为上一层的输出通道数

            cell_list.append(ConvLSTMCell(in_channels=cur_in_channels, out_channels=self.out_channels[i],
                                          kernel_size=self.kernel_size[i], bias=self.bias))
        self.cell_list = nn.ModuleList(cell_list)
        # 必须要放到nn.ModuleList,否则在GPU上云运行时会报错张量不在同一个设备上的问题

    def forward(self, input_tensor, hidden_state=None):
        """

        Parameters
        ----------
        input_tensor: todo
            5-D Tensor: [Batch_size, Time_step, Channels, Height, Width]  or
                        [Time_step, Batch_size, Channels, Height, Width]
        hidden_state: todo
            None. todo implement stateful

        Returns
        -------
        last_state_list, layer_output
        """
        if not self.batch_first:
            # 将(t, b, c, h, w) 转为 (b, t, c, h, w)
            input_tensor = input_tensor.permute(1, 0, 2, 3, 4)

        batch_size, time_step, _, height, width = input_tensor.size()

        # Implement stateful ConvLSTM
        if hidden_state is not None:
            raise NotImplementedError()
        else:
            # Since the init is done in forward. Can send image size here
            hidden_state = self._init_hidden(batch_size=batch_size,
                                             image_size=(height, width))

        layer_output_list = []  # 保存每一层的输出h,每个元素的形状为[batch_size, time_step, out_channels, height, width]
        last_state_list = []  # 保存每一层最后一个时刻的输出h和c,即[(h,c),(h,c)...]
        cur_layer_input = input_tensor  # [batch_size, time_step, in_channels, height, width]
        for layer_idx in range(self.num_layers):
            h, c = hidden_state[layer_idx]  # 开始遍历每一层的ConvLSTM记忆单元,并取对应的初始值
            # h 和 c 的形状均为[batch_size, out_channels, height, width]
            output_inner = []
            cur_layer_cell = self.cell_list[layer_idx]  # 为一个ConvLSTMCell记忆单元
            for t in range(time_step):  # 对于每一层的记忆单元,按照时间维度展开进行计算
                h, c = cur_layer_cell(input_tensor=cur_layer_input[:, t, :, :, :], last_state=[h, c])
                output_inner.append(h)  # 当前层,每个时刻的输出h, 形状为 [batch_size, out_channels, height, width]

            layer_output = torch.stack(output_inner, dim=1)  # [batch_size, time_step, out_channels, height, width]
            cur_layer_input = layer_output  # 当前层的输出h,作为下一层的输入
            layer_output_list.append(layer_output)
            last_state_list.append([h, c])

        if not self.return_all_layers:
            layer_output_list = layer_output_list[-1:]
            last_state_list = last_state_list[-1:]

        return layer_output_list, last_state_list

    def _init_hidden(self, batch_size, image_size):
        """
        init_states中的每个元素为一个tuple,包含C和H两个部分,如 [(h,c),(h,c)...]
        形状均为 [batch_size, out_channels, height, width]
        :param batch_size:
        :param image_size:
        :return:
        """
        init_states = []
        for i in range(self.num_layers):  # 初始化每一层的初始值
            init_states.append(self.cell_list[i].init_hidden(batch_size, image_size))
        return init_states

    @staticmethod
    def _check_kernel_size_consistency(kernel_size):
        if not (isinstance(kernel_size, tuple) or
                (isinstance(kernel_size, list) and all([isinstance(elem, tuple) for elem in kernel_size]))):
            raise ValueError('`kernel_size` must be tuple or list of tuples')

    @staticmethod
    def _extend_for_multilayer(param, num_layers):
        if not isinstance(param, list):
            param = [param] * num_layers
        return param

ConvLSTMKTH:针对数据集进行改进

class ConvLSTMKTH(nn.Module):
    def __init__(self, config=None):
        super().__init__()
        self.conv_lstm = ConvLSTM(config.in_channels, config.out_channels,
                                  config.kernel_size, config.num_layers, config.batch_first)
        self.max_pool = nn.MaxPool2d(kernel_size=(5, 5), stride=2, padding=2)
        self.hidden_dim = (config.width * config.height) // 4 * self.conv_lstm.out_channels[-1]
        # 除以4是因为长宽均要除以stride, 使用self.conv_lstm.out_channels[-1]
        # 主要是为了兼容out_channels传入整型或列表的情况,因为传入整型的话在ConvLSTM的初始化方法中_extend_for_multilayer()
        # 方法也会将其扩充一个list
        self.classifier = nn.Sequential(nn.Flatten(),
                                        nn.Linear(self.hidden_dim, config.num_classes))

    def forward(self, x, labels=None):
        """
        :param x: [batch_size, time_step, channels, height, width]
        :param labels: [batch_size,]
        :return: logits: [batch_size, num_classes]
        """
        _, layer_output = self.conv_lstm(x)
        # layer_output: [h:[batch_size, out_channels, height, width], c:[batch_size, out_channels, height, width]]
        pool_output = self.max_pool(layer_output[-1][0])  # [batch_size, out_channels, height//2, width//2]
        logits = self.classifier(pool_output)  # [batch_size, num_classes]
        if labels is not None:
            loss_fct = nn.CrossEntropyLoss(reduction='mean')
            loss = loss_fct(logits, labels)
            return loss, logits
        else:
            return logits

ModelConfig:模型参数设置

class ModelConfig(object):
    def __init__(self):
        self.batch_size = 32
        self.epochs = 30
        self.learning_rate = 3e-3
        self.num_classes = 6
        self.in_channels = 1
        self.out_channels = [32,32]
        self.kernel_size = [(3, 3), (3, 3)]
        self.num_layers = len(self.out_channels)
        self.height = 60  # 原始大小为120
        self.width = 80  # 原始大小为160
        self.time_step = 15
        self.num_warmup_steps = 200
        self.model_save_path = 'model.pt'
        self.summary_writer_dir = "runs/model"
        self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
        # 判断是否存在GPU设备,其中0表示指定第0块设备
        logging.info("### 将当前配置打印到日志文件中 ")
        for key, value in self.__dict__.items():
            logging.info(f"### {key} = {value}")

训练过程

def train(config):
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((config.height, config.width)),
        transforms.RandomHorizontalFlip(0.5)])
    data_load = KTHData(frame_len=config.time_step,
                        batch_size=config.batch_size,
                        transforms=trans)
    train_iter, val_iter = data_load.load_train_val_test_data(is_train=True)
    model = ConvLSTMKTH(config)
    if os.path.exists(config.model_save_path):
        logging.info(f" # 载入模型{config.model_save_path}进行追加训练...")
        checkpoint = torch.load(config.model_save_path)
        model.load_state_dict(checkpoint)
    optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)
    writer = SummaryWriter(config.summary_writer_dir)
    model = model.to(config.device)
    max_test_acc = 0
    steps = len(train_iter) * config.epochs
    scheduler = optimization.get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=config.num_warmup_steps,
                                                             num_training_steps=steps, num_cycles=2)
    for epoch in range(config.epochs):
        for i, (x, y) in enumerate(train_iter):
            x, y = x.to(config.device), y.to(config.device)
            loss, logits = model(x, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()  # 执行梯度下降
            scheduler.step()
            if i % 50 == 0:
                acc = (logits.argmax(1) == y).float().mean()
                logging.info(f"Epochs[{epoch + 1}/{config.epochs}]--batch[{i}/{len(train_iter)}]"
                             f"--Acc: {round(acc.item(), 4)}--loss: {round(loss.item(), 4)}")
                writer.add_scalar('Training/Accuracy', acc, scheduler.last_epoch)
            writer.add_scalar('Training/Loss', loss.item(), scheduler.last_epoch)
        test_acc = evaluate(val_iter, model, config.device)
        logging.info(f"Epochs[{epoch + 1}/{config.epochs}]--Acc on val {test_acc}")
        writer.add_scalar('Testing/Accuracy', test_acc, scheduler.last_epoch)
        if test_acc > max_test_acc:
            max_test_acc = test_acc
            state_dict = deepcopy(model.state_dict())
            torch.save(state_dict, config.model_save_path)

模型评估

def evaluate(data_iter, model, device):
    model.eval()
    with torch.no_grad():
        acc_sum, n = 0.0, 0
        for x, y in data_iter:
            x, y = x.to(device), y.to(device)
            logits = model(x)
            acc_sum += (logits.argmax(1) == y).float().sum().item()
            n += len(y)
        model.train()
        return acc_sum / n

使用模型进行预测

def inference(config, ):
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((config.height, config.width)),
        transforms.RandomHorizontalFlip(0.5)])
    data_load = KTHData(frame_len=config.time_step,
                        batch_size=config.batch_size,
                        transforms=trans)
    test_iter = data_load.load_train_val_test_data(is_train=False)
    model = ConvLSTMKTH(config)
    model.to(config.device)
    model.eval()
    if os.path.exists(config.model_save_path):
        logging.info(f" # 载入模型进行推理……")
        checkpoint = torch.load(config.model_save_path)
        model.load_state_dict(checkpoint)
    else:
        raise ValueError(f" # 模型{config.model_save_path}不存在!")
    first_batch = next(iter(test_iter))
    with torch.no_grad():
        logits = model(first_batch[0].to(config.device))
    y_pred = logits.argmax(1)
    logging.info(f"真实标签为:{first_batch[1]}")
    logging.info(f"预测标签为:{y_pred}")

小结

本文主要介绍了自注意力机制以及ConvLSTM,在上周的学习中论文将二者结合,从而实现了时空序列预测领域中较好的结果。本文在KTH数据集上实现了该结构,根据数据集构造了迭代器以及进行了模型重构。最后在该环境下进行了模型有效性验证,得到了较好的结果。

下周将继续阅读序列预测相关论文

参考文献

[1] Shi, X.; Chen, Z.; Wang, H.; Yeung, D.-Y.; Wong, W.-K.;and Woo, W.-c. 2015. Convolutional lstm network: A machine learning approach for precipitation nowcasting. In NIPS 2015, 802–810.

[2]I. Sutskever, O. Vinyals, and Q. V. Le. Sequence to sequence learning with neural networks. In NIPS, pages 3104–3112, 2014.

[3]N. Srivastava, E. Mansimov, and R. Salakhutdinov. Unsupervised learning of video representations using lstms. In ICML, 2015.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1380106.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈Vue3中v-if与v-for优先级问题。

一、注意 在Vue3官方文档中&#xff0c;不推荐同时使用 v-if 和 v-for &#xff0c;因为这样二者的优先级不明显。 当它们同时存在于一个节点上时&#xff0c;v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名。 二、怎么解决 在外…

【数据分析】数据分析方法 | A/B测试与多变量分析

【数据分析】数据分析方法 | A/B测试与多变量分析 上一次与大家讨论了数据分析方法中的市场细分与同期群分析&#xff0c;此次仍然是对数据分析方法的讨论&#xff0c;讨论A/B测试与多变量分析的应用。 如果撇开统计性数据分析不谈&#xff0c;数据分析的最终目的是为了对具体…

神经网络学习小记录77——深入浅出Self-Attention自注意力机制与Transformer模块

神经网络学习小记录77——深入浅出Self-Attention自注意力机制与Transformer模块 学习前言代码下载Self-Attention自注意力机制详解一、Self-attention结构解析二、Self-attention的矩阵运算三、Multi-Head多头注意力机制 TransformerBlock的构建一、视觉部分的TransformerBloc…

10-skywalking告警

https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/backend-alarm.md 5.1&#xff1a;告警指标 ~$ vim /apps/apache-skywalking-apm-bin/config/oal/core.oal service_resp_time # 服务的响应时间 service_sla # 服务http请求成功率SLV&#xff0c;比…

Logstash配置详解

一、配置文件 Logstash配置文件位于Logstash安装目录下bin/logstash.conf 启动命令: logstash -f logstash.conf文件描述logstash.yml配置Logstash的yml。pipelines.yml包含在单个Logstash实例中运行多个管道的框架和说明。jvm.options配置Logstash的JVM&#xff0c;使用此文…

线程安全--互斥锁

文章目录 一.线程安全问题读取无效(脏)数据丢失更新线程安全的保证--操作的原子性 二.互斥锁及其实现原理互斥锁的实现原理pthread线程库提供的锁操作 三.死锁问题 一.线程安全问题 当多个线程并发地对同一个共享资源进行修改操作时,可能会引发数据读写错误(比如读取无效(脏)数…

[Flutter] extends、implements、mixin和 abstract、extension的使用介绍说明

类创建&#xff1a;abstract&#xff08;抽象类&#xff09;、extension&#xff08;扩展&#xff09; 1.abstract&#xff08;抽象类&#xff09; dart 抽象类主要用于定义标准&#xff0c;子类可以继承抽象类&#xff0c;也可以实现抽象类接口。抽象类通过abstract 关键字来…

NLP论文阅读记录 - 2023 | EXABSUM:一种新的文本摘要方法,用于生成提取和抽象摘要

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结思考 前言 EXABSUM: a new text summarization approach for generating ex…

PLC控制脉冲轴绝对位置往复运动(三菱FX系列简单状态机编程)

有关状态机的具体介绍,专栏有很多文章,大家可以通过下面的链接查看: https://rxxw-control.blog.csdn.net/article/details/125488089https://rxxw-control.blog.csdn.net/article/details/125488089三菱FX系列回原功能块介绍 https://rxxw-control.blog.csdn.net/article…

springboot注解@PropertySource作用

简介 PropertySource 是 Spring 框架中的一个注解&#xff0c;用于指定一个或多个属性文件&#xff08;通常是.properties文件&#xff09;这些文件包含了应用程序需要的配置信息。当你在 Spring 的配置类中使用此注解时&#xff0c;Spring 容器会加载这些属性文件&#xff0c…

中科星图——Landsat9_C2_SR大气校正后的地表反射率数据

数据名称&#xff1a; Landsat9_C2_SR 数据来源&#xff1a; USGS 时空范围&#xff1a; 2022年1月-2023年3月 空间范围&#xff1a; 全国 数据简介&#xff1a; Landsat9_C2_SR数据集是经大气校正后的地表反射率数据&#xff0c;属于Collection2的二级数据产品&#…

深入理解 Flink(二)Flink StateBackend 和 Checkpoint 容错深入分析

Flink State 设计详解 State 简单说&#xff0c;就是 Flink Job 的 Task 在运行过程中&#xff0c;产生的一些状态数据。这些状态数据&#xff0c;会辅助 Task 执行某些有状态计算&#xff0c;同时也涉及到 Flink Job 的重启状态恢复。所以&#xff0c;保存和管理每个 Task 的状…

如何制作网址链接活码?网址二维码生成器的使用方法

将网址转二维码图片来使用&#xff0c;是现在很常用的一种二维码类型&#xff0c;一般网址可以根据自己的用途来制作静态码或者活码两种形式。其中静态码只是单纯将网址链接转换成二维码&#xff0c;无法统计与修改&#xff0c;而生成网址活码可以在二维码图片不变情况下替换其…

114.QTimer类和QWidget类

目录 一、QTimer类 定时器使用举例&#xff1a; 二、QWidget类 2.1设置父对象 2.2窗口位置 2.3窗口尺寸 2.4窗口标题和图标 2.5信号 2.6槽函数 示例代码&#xff1a; 一、QTimer类 QTimer 是 Qt 中用于实现定时器的类。它可以在一定的时间间隔内发射信号&#xff0c;…

【小程序开发需要多少钱?】

哈喽&#xff0c;大家好&#xff0c;这里是智创开发。 我们今天聊聊开发一个小程序需要多少钱。 由于自己组建团队来开发小程序成本过高&#xff0c;大品牌的企业一般都不会这么搞&#xff0c;所以我们今天只谈假如我有需求&#xff0c;找服务商来全程搞定的费用大致是多少。和…

Flutter之运行错误:this and base files have different roots

运行时报错&#xff1a; this and base files have different roots: E:\Demolpro\waqu\build\flutter-plugin-_android_lifecycle and C:\Users\78535\AppData\Local\Pub\Cache\hosted\pub.dev\flutter_pulgin_android_lifecycle-2.0.17\android 如图&#xff1a; 这种情况…

半导体抛光用PFA容量瓶耐强酸碱定容瓶

PFA容量瓶又称可溶性聚四氟乙烯容量瓶、特氟龙容量瓶容量瓶&#xff0c;我司新推出螺纹和插口两种可供选择&#xff0c;目前有10ml、25ml、50ml、100ml、250ml、500ml、1000ml的规格可提供&#xff0c;产品质量有保障。 Teflon系列PFA容量瓶是一个透明的长颈瓶&#xff0c;瓶体…

水果音乐编曲软件 FL Studio v21.2.2.3914 中文免费版(附中文设置教程)

FL studio21中文别名水果编曲软件&#xff0c;是一款全能的音乐制作软件&#xff0c;包括编曲、录音、剪辑和混音等诸多功能&#xff0c;让你的电脑编程一个全能的录音室&#xff0c;它为您提供了一个集成的开发环境&#xff0c;使用起来非常简单有效&#xff0c;您的工作会变得…

公司运营数据分析大屏:引领企业决策,驱动业务增长

在数字化时代&#xff0c;数据已经成为企业决策的关键。为了更好地洞察市场趋势、优化业务流程、提升运营效率&#xff0c;越来越多的企业开始引入数据分析大屏以分析公司运营状况。这一创新举措不仅改变了传统的管理模式&#xff0c;更引领企业迈向智能化决策的新篇章。 公司运…

Spring之AOP源码(二)

书接上文 文章目录 一、简介1. 前文回顾2. 知识点补充 二、ProxyFactory源码分析1. ProxyFactory2. JdkDynamicAopProxy3. ObjenesisCglibAopProxy 三、 Spring AOP源码分析 一、简介 1. 前文回顾 前面我们已经介绍了AOP的基本使用方法以及基本原理&#xff0c;但是还没有涉…