对于连续信号,确定其状态表示往往颇具挑战性。幸运的是,在实际应用中,我们通常处理的是离散输入(例如文本序列),这使得问题变得相对简单。因此,我们倾向于将模型进行离散化处理,以便更好地适应这种输入类型。
在这个过程中,零阶保持技术(zero-order hold technique)发挥着关键作用。其工作原理大致如下:每当系统接收到一个离散信号时,该技术会记录下这个信号的值,并将其保持不变,直到下一个离散信号的到来。这种处理方式有效地将离散的信号点转换为连续的信号段,为SSM提供了可用的连续信号输入。
注意 更为详细的PyTorch实现请参考第4章的实战部分,本节主要是理论讲解。
状态空间模型(SSM)是一种强大的统计模型,特别适用于处理具有时间依赖性的连续时间序列。在SSM中,找到能够准确描述系统动态变化的状态表示 h ( t )是建模的关键。然而,对于连续信号来说,直接找到这样的状态表示可能相当困难,因为连续信号在时间和幅值上都是连续变化的。
幸运的是,在实际应用中,我们往往能够处理离散的输入数据,例如文本序列、时间序列数据点等。为了将这些离散数据应用于SSM,我们需要对模型进行离散化,使其能够适应离散输入,如图3-17所示。
图3-17 连续信号转换为离散信号
具体而言,对于上图中的
来说,假设我们有一个离散的输入信号序列,每个信号值都是在不同的时间点上获取的。当我们开始处理这个序列时,首先记录下第一个信号值。然后,在下一个信号值到来之前,假设信号值保持不变,即保持为第一个信号值。当收到第二个信号值时,我们再次将其记录下来,并在接下来的时间内保持这个值,直到收到第三个信号值为止。这个过程会持续进行,每次都将新的信号值保持下来,直到整个离散信号序列都被处理完毕。
下面假设一个 A 和 B 的连续时间系统参数,并将其转换为离散时间矩阵,代码如下:
import numpy as np from scipy.linalg import expm # 假设的连续时间系统参数 A_continuous = np.array([[0.9, 0.1], [0.2, 0.8]]) # 连续时间状态转移矩阵 # 连续时间输入矩阵(注意:这里通常是一个矩阵,但为了简化示例,假设它是一个列向量) B_continuous = np.array([[0.5], [0.3]]) # 采样周期T T = 0.1 # 例如0.1秒 # 使用零阶保持技术离散化系统 # expm(A_continuous * T) 是离散时间状态转移矩阵A的近似值 A = expm(A_continuous * T) # 注意:离散时间输入矩阵B的计算依赖于具体的系统模型和离散化方法 # 在这里,我们假设B与连续时间输入矩阵相同(这通常不是真实情况,但为了简化示例) B = B_continuous # 示例中直接使用连续时间输入矩阵(在实际情况中可能不同) # 现在我们有了离散时间系统的A和B矩阵 print("离散时间状态转移矩阵 A:") print(A) print("离散时间输入矩阵 B:") print(B)
打印结果如下:
离散时间状态转移矩阵 A: [[1.09428334 0.01088758] [0.02177516 1.08339576]] 离散时间输入矩阵 B: [[0.5] [0.3]]
通过对比原始的 A 和 B ,可以看到,我们通过转换得到了新的离散矩阵。接下来,我们将使用离散时间系统模拟一个状态序列。完整代码如下:
import numpy as np from scipy.linalg import expm # 假设的连续时间系统参数 A_continuous = np.array([[0.9, 0.1], [0.2, 0.8]]) # 连续时间状态转移矩阵 # 连续时间输入矩阵(注意:这里通常是一个矩阵,但为了简化示例,我们假设它是一个列向量) B_continuous = np.array([[0.5], [0.3]]) # 采样周期T T = 0.1 # 例如,0.1秒 # 使用零阶保持技术离散化系统 # expm(A_continuous * T) 是离散时间状态转移矩阵A的近似值 A = expm(A_continuous * T) # 注意:离散时间输入矩阵B的计算依赖于具体的系统模型和离散化方法 # 在这里,我们假设B与连续时间输入矩阵相同(这通常不是真实情况,但为了简化示例) B = B_continuous # 示例中直接使用连续时间输入矩阵(在实际情况中可能不同) # 示例:使用离散时间系统模拟一个状态序列 # 假设初始状态 x0 和输入序列 u x0 = np.array([[1], [0]]) # 假设u为连续信号 u = np.array([[0.1], [0.2], [0.1], [0.2], [0.1]]) # 5个时间步的输入(注意:这里 假设输入是一个列向量序列) # 初始化状态序列(包括初始状态) x = np.zeros((A.shape[0], len(u) + 1)) x[:, 0] = x0.flatten() # 设置初始状态 # 假设系统是一个没有控制输入的自治系统,或者我们忽略输入u的影响(为了简化示例) # 在实际情况下,你可能需要在每个时间步都考虑输入u的影响 for k in range(len(u)): x[:, k + 1] = A.dot(x[:, k]) + B.dot(u[k]) # 注意:这里包括输入u的影响 print("离散时间状态序列 x:") print(x)
在上面的示例中,我们假设 u 是连续信号(或者说是表示连续时间输入的信号序列的离散近似),因为它在每个时间步(或采样点)都有一个值。然而,严格来说, u 已经是离散化的,因为我们通常不能直接在计算机中表示连续时间的信号,除非使用无限精度的表示方法(这在实践中是不可能的)。
被转换成的离散信号是系统的状态序列 x 。这个序列在每个时间步(或采样点)都有一个值,表示系统在那个时间点的状态。由于我们使用了离散时间状态转移矩阵 A 和(可能的)离散时间输入矩阵 B , x 序列就是一个完全离散化的表示,它描述了系统状态随时间(离散时间点)的变化。
最终结果如下:
离散时间状态序列 x: [[1. 1.14428334 1.3527339 1.53180943 1.77854416 1.99975097] [0. 0.05177516 0.14100994 0.21222556 0.32327967 0.4189679 ]]
Zero-Order Hold(ZOH,零阶保持)是一种在信号处理中广泛采用的插值技术,旨在将连续时间信号转换为离散时间信号。其工作原理是,在连续信号的采样点之间,简单地使用前一个采样点的信号值作为当前时间段的输出值,直至下一个采样点出现。简而言之,当没有新的采样点更新时,Zero-Order Hold保持信号值不变,从而构建出一个等间隔的离散时间信号序列,如图3-18所示。
图3-18 零阶保持技术
每当接收到一个离散信号时,其值会被保留,直至下一个离散信号的到来。这种保留机制实质上创建了一个可供状态空间模型使用的连续信号。保留信号值的时间跨度由一个名为“步长”( Δ ,size)的新学习参数来控制,这个参数反映了输入信号的阶段性保持能力,即分辨率。有了这种连续的输入信号,系统便能生成连续的输出,同时,仅根据输入的时间步长对输出值进行采样。
在两个连续采样点之间的时间段内,输出信号将维持前一个采样点的值,这一特性使得信号在采样点之间保持恒定。因此,Zero-Order Hold技术非常适用于那些需要快速且简便地将连续信号转换为离散信号的场景。
在模拟信号处理领域中,特别是在模拟到数字转换(Analog-to-Digital Converter,ADC)和数字到模拟转换(Digital-to-Analog Converter,DAC)系统中,Zero-Order Hold技术发挥着不可或缺的作用。无论是将连续的模拟信号转换为离散的数字信号以便于数字处理,还是将数字信号重新转换为连续的模拟信号以便于实际应用,Zero-Order Hold都以其简单高效的特性成为工程师的首选工具。
该值的保持时间由一个新的可学习参数表示,称为步长(step size),它代表输入信号的分辨率。
通过这种方式,零阶保持技术将离散的信号值转换为一种“连续”的信号表示。虽然从严格意义上来说,这种信号并不是真正的连续信号(因为它仍然是由离散的信号值组成的),但在SSM的上下文中,我们可以将其视为一种近似的连续信号。
SSM可以利用这种近似的连续信号来建立状态表示 h ( t )。通过定义状态转移矩阵 A 和输入矩阵 B 等参数,SSM能够描述系统状态如何随时间变化以及外部输入如何影响这些状态。然后,SSM可以利用这些参数和零阶保持技术生成的连续信号表示来推断出最有可能的状态序列,从而实现对数据的建模和预测。
把离散信号连续化之后,就有了连续的输入信号。连续信号可以生成连续的输出,并且仅根据输入的时间步长对输出值进行采样,如图3-19所示。
图3-19 将连续信号转换成离散信号
让我们考虑一个连续时间SSM,它通常由一系列微分方程或差分方程描述。为了将这样的系统转换为离散时间模型,我们需要对时间进行离散化,并在每个采样时刻保持信号值不变,直到下一个采样时刻。这正是零阶保持技术的核心思想,如图3-20所示。
图3-20 连续信号的离散化
从图3-20可以看到,我们对连续信号进行了离散化处理。读者可以注意右式中
(状态转移矩阵A_bar)、
(输入矩阵B_bar)、
C
(观测矩阵)的限制,以及这些限制如何影响模型的结构和性能。
●
矩阵:
是一个对角矩阵(
N
×
N
),这意味着
的每一行和每一列只有在主对角线上的元素可能非零,而其他位置的元素均为零。这种对角结构极大地简化了状态方程的计算,因为每个状态变量只与自身有关,而与其他状态变量无关。这种简化可以使SSM在某些情况下具有更快的计算速度和更低的内存需求。
●
矩阵:
是一个
N
×1的列向量,表示在每个时间步,输入对状态变量的影响是通过一个固定的权重向量(即
)来实现的。由于
是列向量,它可以直接与输入序列
X
相乘,从而得到对状态变量的更新。这种结构使得SSM能够高效地处理输入序列,并允许模型在不同的时间步使用相同的权重来处理输入。
● C 矩阵:在S4中, C 是一个1× N 的行向量。这表示观测方程将状态变量映射到可观测数据的方式是通过一个固定的权重向量(即 C )来实现的。由于 C 是行向量,它可以直接与状态变量相乘,从而得到观测值。这种结构使得S4能够方便地从状态变量中提取出可观测的数据,并允许模型在不同的时间步使用相同的权重来生成观测值。
这些限制意味着 A 、 B 、 C 矩阵均能通过 N 个数字表示,这使得SSM在参数数量和计算复杂度上相对较低。此外,由于SSM独立作用于每个通道(或维度),每个输入的总隐藏状态的维度为 N × D ,其中 N 是状态变量的数量, D 是通道(或维度)的数量。这意味着SSM可以独立地处理每个通道的数据,从而增加了模型的灵活性和可扩展性。
下面是实现离散状态空间的状态转移与观测值获取的完整代码:
打印结果如下:
Time step 1: State x = [0.4 0.6], Observation y = 1.6 Time step 2: State x = [0.76 1.08], Observation y = 2.92 Time step 3: State x = [1.084 1.464], Observation y = 4.0120000000000005 Time step 4: State x = [1.3756 1.7712], Observation y = 4.918 Time step 5: State x = [1.63804 2.01696], Observation y = 5.67196 Time step 6: State x = [1.874236 2.213568], Observation y = 6.301372000000001 Time step 7: State x = [2.0868124 2.3708544], Observation y = 6.828521200000001 Time step 8: State x = [2.27813116 2.49668352], Observation y = 7.271498200000002 Time step 9: State x = [2.45031804 2.59734682], Observation y = 7.645011676000001 Time step 10: State x = [2.60528624 2.67787745], Observation y = 7.961041145200001
在上面的Python代码中, y 表示观测值(Observation),它是通过观测矩阵 C 与当前状态x_next(或上一个时间步更新后的状态 x ,但在本例中我们使用x_next来保持一致)相乘得到的。观测值通常代表我们可以从系统中测量或观察到的数据。
其中的np.dot(C, x_next)计算了当前状态x_next通过观测矩阵C映射到观测空间的结果。观测矩阵 C 定义了如何从状态空间映射到观测空间,即它定义了哪些状态变量是可观测的,以及它们如何被组合成观测值。
在实际应用中,观测值 y 可能与真实状态之间存在误差或噪声,这取决于系统的性质以及测量设备的精度。在上面的示例中,我们假设观测值 y 是准确无误的,但在实际系统中,可能需要考虑观测噪声的影响。
SSM的循环计算在状态空间模型中扮演着核心的角色,特别是在处理具有时间依赖性的序列数据时。
首先,状态空间模型在应用时可以转换为类似于循环神经网络(RNN)的计算方法,如图3-21所示。
图3-21 循环神经网络与状态空间模型的计算
将图3-21的右边展开后,如图3-22所示。
图3-22 展开后的状态空间模型的计算流程
离散状态空间模型将连续的信号或系统动态划分为一系列的时间步长,并在这些时间步长上构建模型。循环表征允许模型捕获并存储这些时间步长之间的依赖关系,以便对系统的未来状态进行预测或分析,如图3-23所示。
图3-23 离散状态空间的循环表征
具体来说,循环表征在SSM中的作用体现在以下几个方面。
● 时间依赖性建模:SSM通常用于处理随时间变化的系统或过程。这些系统或过程的状态往往依赖于其过去的状态和当前的输入。循环表征通过引入隐藏状态(hidden state)的概念,允许模型捕获并记忆这种时间依赖性。在每个时间步长,模型会根据当前的输入和先前的隐藏状态来更新隐藏状态,从而捕获系统的动态变化。
● 序列信息整合:在序列数据中,信息通常分布在多个时间步长之间。循环表征允许SSM将这些分散的信息整合在一起,形成一个连贯的表示。这种整合有助于模型理解整个序列的结构和模式,并据此做出更准确的预测或决策。
● 长期依赖处理:在某些情况下,系统的当前状态可能受到较远时间步长之前状态的影响。这种长期依赖关系对于许多实际问题来说非常重要,但传统的方法往往难以有效处理。循环表征通过其特殊的记忆机制,能够捕获并处理这种长期依赖关系,使得SSM能够更准确地描述和预测系统的动态。
● 可解释性:循环表征通常与可解释性相关联。通过分析隐藏状态的变化和更新方式,我们可以了解模型如何捕获和整合序列中的信息,以及这些信息如何影响模型的预测或决策。这种可解释性有助于我们理解模型的工作原理,并对其进行调试和改进。
在每个时间步 k ,我们精心地计算当前输入 Bx k 如何动态地影响先前的状态 AH k -1 。这一计算过程不仅捕捉了外部刺激对当前细胞或系统状态的即时效应,还反映了历史状态对于新输入信息的响应。通过这样的方式,我们能够深入理解单个细胞或系统在时间演变中的复杂动态,如图3-24所示。
图3-24 时间步的计算顺序
具体来说,我们首先考虑当前输入 Bx k (其中 B 是输入矩阵, x k 是时间步 k 的输入向量)如何影响先前的状态 AH k -1 ( A 是状态转移矩阵, H k -1 是时间步 k -1的隐藏状态或内部状态)。
在获得更新后的状态 H k 后,我们进一步计算预测输出 Ch k 。这一步是将内部状态映射到可观测输出的重要过程,它揭示了细胞或系统在当前时间步的功能状态或行为表现。通过比较不同时间步的预测输出,我们可以追踪细胞或系统状态随时间的连续变化,进而揭示其潜在的运行机制和调控规律。
这一步实质上是计算输入对当前系统状态变化的贡献。通过矩阵乘法 Bx k ,我们将输入映射到与状态空间相同的维度上,以便能够直接与先前的状态 AH k -1 相结合。
接下来,我们将 Bx k 与 AH k -1 相结合,以产生新的隐藏状态 h k 。这一步通常通过一个线性组合(可能还包括一个偏置项)来完成,该线性组合加上了某种形式的非线性激活函数(尽管在某些简化版的SSM中可能不包括激活函数)。
在整个过程中,状态转移矩阵 A 、输入矩阵 B 和输出矩阵 C 共同构成了系统动态的核心模型。它们不仅决定了状态如何随时间演变,还决定了系统如何响应外部输入并产生相应的输出。因此,通过对这些矩阵的精细调控和优化,我们能够实现对细胞或系统行为的精确控制和预测。
一旦获得了更新后的隐藏状态 h k ,我们就可以用它来计算预测输出 CH k ( C 是输出矩阵)。这一过程将隐藏状态映射到输出空间,从而允许我们根据系统的当前状态来预测或解释下一个时间步的外部行为或观察结果。
SSM在一定程度上模仿了RNN,这种方法同样带来了RNN的优点和缺点,即快速的推理和相对较缓慢的训练。